将引用属性映射到抽象父

时间:2017-10-23 18:35:03

标签: c# entity-framework inheritance entity-framework-6 tph

我在企业应用程序中有一个复杂的对象层次结构。我会尽量保持简单,抽象,但仍能代表我所处理的内容。

我的项目处理同一类型对象的几种样式。为此,我们为实体对象实现了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值应该是以逗号分隔的外键属性名称列表。

这让我相信我无法做我想做的抽象属性。有没有办法描绘我失踪的这种关系?我不想为每个人提供一个特定的参考属性,因为我知道在一两个月内会有更多类型,并且属性列表将变得难以处理。

1 个答案:

答案 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)DetailsWidget没有导航属性。

生成的数据库模式如下:

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);