我在企业应用程序中有一个复杂的对象层次结构。我会尽量保持简单,抽象,但仍能代表我所处理的内容。
我的项目处理同一类型对象的几种样式。为此,我们为实体对象实现了TPT结构:
public abstract class BaseWidget {
public int Id { get; set; }
// etc...
}
// About a dozen concrete implementations already exist and work great!
public class ExistingWidget : BaseWidget {
// Other properties
}
现在我有一种我正在做的新类型。我们在对象上有共同的属性,但根据子类型,有一些不同的细节集。为此,我设置了TPH,因为该类型的属性在所有子类型中都是相同的。唯一的区别是需要哪些细节对象。
public abstract NewWidgetBase : BaseWidget {
public int EmployeeNumber { get; set; }
public DateTime EffectiveDate { get; set; }
}
public NewWidgetA : NewWidgetBase {
}
public NewWidgetB : NewWidgetBase {
}
我将此映射到我的DbContext中,如下所示:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Entity<NewWidgetBase>()
.Map<NewWidgetA>(w => w.Requires("Discriminator").HasValue("a"))
.Map<NewWidgetB>(w => w.Requires("Discriminator).HasValue("b"));
此时,我已经使用了集成测试并成功检查了我可以保存到两个表。
现在,我想添加详细信息:
public class FooDetails {
public int Id { get; set; }
public int NewWidgetId { get; set; }
// ...
[ForeignKey(nameof(NewWidgetId))]
public NewWidgetBase NewWidget { get; set; }
}
public class BarDetails {
public int Id { get; set; }
public int NewWidgetId { get; set; }
// ...
[ForeignKey(nameof(NewWidgetId))]
public NewWidgetBase NewWidget { get; set; }
}
然后我将这些引用属性添加到相应的NewWidget
对象中。
public class NewWidgetA {
// ...
public FooDetails Foo { get; set; }
}
public class NewWidgetB {
// ...
public FooDetails Foo { get; set; }
public BarDetails Bar { get; set; }
}
我尝试执行此操作,假设典型的映射可行,并得到以下错误:
System.Data.Entity.Infrastructure.DbUpdateException:保存未公开其关系的外键属性的实体时发生错误。 EntityEntries属性将返回null,因为无法将单个实体标识为异常源。通过在实体类型中公开外键属性,可以更轻松地在保存时处理异常。有关详细信息,请参阅InnerException。 ---&GT; System.Data.Entity.Core.UpdateException:无法确定相关操作的有效排序。由于外键约束,模型要求或存储生成的值,可能存在依赖关系。
有了这个,我明白它没有正确的关系方向和键映射。所以我再次在DbContext中明确地设置它:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredDependent();
然而,这给了我错误:
System.InvalidOperationException:ReferentialConstraint中的依赖属性映射到存储生成的列。专栏:&#39; WidgetId&#39;。
我看了一下&#34; some other&#34; &#34; questions&#34;,这些答案都没有帮助我。
作为最后的努力,我尝试使用带有Func的.WithRequiredDependent()
重载。但是,因为它与我的映射完全相同,因为我将该属性作为抽象基础,所以它会抱怨。因此,我尝试这样做:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredDependent(f => (NewWidgetA)f.Widget);
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Foo)
.WithRequiredDependent(f => (NewWidgetB).Widget);
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Bar)
.WithRequiredDependent(b => (NewWidgetB).Widget);
但是,这也会出错:
属性&#39; Widget&#39;上的ForeignKeyAttribute在类型&#39; ... Foo&#39;无效。外键名称&#39; WidgetId&#39;没有在依赖类型&#39; NewWidgetA&#39;上找到。 Name值应该是以逗号分隔的外键属性名称列表。
这让我相信我无法做我想做的抽象属性。有没有办法描绘我失踪的这种关系?我不想为每个人提供一个特定的参考属性,因为我知道在一两个月内会有更多类型,并且属性列表将变得难以处理。
答案 0 :(得分:1)
这是可能的,但只能使用单向(仅在Widget
侧具有导航属性)一对一Shared Primary Key Association,其中Widget
方是主体和Details
方是依赖。
首先从Details
实体中删除导航和FK属性:
public class FooDetails {
public int Id { get; set; }
// ...
}
public class BarDetails {
public int Id { get; set; }
// ...
}
并使用以下流畅配置:
modelBuilder.Entity<NewWidgetA>()
.HasRequired(w => w.Foo)
.WithRequiredPrincipal();
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Foo)
.WithRequiredPrincipal();
modelBuilder.Entity<NewWidgetB>()
.HasRequired(w => w.Bar)
.WithRequiredPrincipal();
请注意WithRequiredPrincipal()
来电。它告诉EF(1)Widget
是主体,(2)Details
到Widget
没有导航属性。
生成的数据库模式如下:
CreateTable(
"dbo.BaseWidget",
c => new
{
Id = c.Int(nullable: false, identity: true),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.ExistingWidget",
c => new
{
Id = c.Int(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.BaseWidget", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.NewWidgetBase",
c => new
{
Id = c.Int(nullable: false),
EmployeeNumber = c.Int(nullable: false),
EffectiveDate = c.DateTime(nullable: false),
Discriminator = c.String(nullable: false, maxLength: 128),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.BaseWidget", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.FooDetails",
c => new
{
Id = c.Int(nullable: false),
Data = c.String(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.NewWidgetBase", t => t.Id)
.Index(t => t.Id);
CreateTable(
"dbo.BarDetails",
c => new
{
Id = c.Int(nullable: false),
Data = c.String(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.NewWidgetBase", t => t.Id)
.Index(t => t.Id);