在Entity Framework 6 (EF6)中,表对具体类型(TPC)继承

12
为了避免使用层次表(TPH),我一直在寻找如何在我的数据库模型中最好地实现基于具体类的继承(TPC)的示例。我发现了官方文档这篇文章
以下是一些带有简单继承的模拟类。
public class BaseEntity
{
    public BaseEntity()
    {
        ModifiedDateTime = DateTime.Now;
    }

    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public DateTime ModifiedDateTime { get; set; }
}

public class Person : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Business :  BaseEntity
{
    public string Name { get; set; }
    public string Location { get; set; }
}

这是两篇文章中使用的DbModelBuilder配置示例。

modelBuilder.Entity<BaseEntity>() 
    .Property(c => c.Id) 
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); 

modelBuilder.Entity<Person>().Map(m => 
{ 
    m.MapInheritedProperties(); 
    m.ToTable("Person"); 
}); 

modelBuilder.Entity<Business>().Map(m => 
{ 
    m.MapInheritedProperties(); 
    m.ToTable("Business"); 
});

应用程序成功运行,但当我返回到数据库时,我发现了三个(3)表,而不是我预期的两个(2)。经过一些测试,似乎创建了“BaseEntity”表,但从未使用。除了这个空的孤立表之外,一切都正常工作。
我尝试调整DbModelBuilder配置,最终删除了“BaseEntity”配置,得到了预期结果:两个(2)表,每个表都具有正确的属性并正常工作。
我进行最后一次测试,删除所有DbModelBuilder配置,仅包括“Person”和“Business”的两个(2)DbSet属性,并再次测试。
public DbSet<Person> People { get; set; }
public DbSet<Business> Businesses { get; set; }

令我惊讶的是,项目构建成功并与数据库连接,只创建了两个表格,其中包括从“BaseEntity”类继承的所有类属性。我可以无问题地进行CRUD操作。
经过多次测试,我没有发现任何问题,并且我无法重现两篇文章警告的重复键错误。
引用块:
“对数据库的更改已成功提交,但在更新对象上下文时出现错误。ObjectContext可能处于不一致的状态。内部异常消息:AcceptChanges无法继续,因为对象的关键值与ObjectStateManager中的另一个对象冲突。在调用AcceptChanges之前,请确保关键值是唯一的。”
  1. 我很好奇为什么这些示例使用MapInheritedProperties属性; 这是一种过时的方法吗?
  2. 为什么两个示例都说要包括“BaseEntity”的配置属性,但包括“BaseEntity”类的DbSet属性或任何DbModelBuilder配置会导致创建未使用的表。
  3. 关于唯一键错误的参考文章警告; 我无法复现该错误,并且已经多次测试了将主键设置为由数据库生成的int和由数据库生成的guid。关于这个错误的信息是否也已过时,或者是否有一个测试可以运行以产生该错误?

我认为应该创建3个表,尽管基础表似乎无法使用。这是TPC的某种缺点,你可以在客户端代码中获得一些好处,但必须承受一些冗余数据(根据传统数据库设计概念甚至不应该被创建)。我认为你可以尝试使用TPT,其中基础表包含所有公共列,它将不再被视为冗余。 - Hopeless
1
@Hopeless 我没有考虑TPT的原因是,尽管我像强迫症患者一样喜欢设计方面,但由于需要使用所有连接语句来检索稍微复杂的数据而导致性能下降。至于关于3个表的说法,我看到了很多相反的文章,但是我无法正确地运行它们。我想知道我设置模型的最终方式是否有一些缺点;我最终得到了两个表,没有垃圾表,没有垃圾列,没有主键问题,但我找不到任何做我所做的事情的例子。 - Nicholas
对于TPH,BaseEntity类应该是抽象的。我认为这样只会为具体类型生成两个DB表。实际上,我遇到了ObjectContext处于不一致状态的错误,而我唯一能够解决它的方法是将BaseEntity类型中的Id字段从Int更改为Guid(这似乎有些浪费)。 - blgrnboy
不一致的状态是由于 dbcontext 具有包含所有子类的 DbSet<BaseType>,因此仍需要来自所有表的实体才能被唯一标识。TPT 通过将其基本上链接回基类型的 PK 来执行此操作,因此每个子类实例都具有唯一的键。使用 TPC 可能会出现重叠,因此会出现错误。 - Adam Green
2个回答

3
为了让这一切更简单,我已经将强制TablePerConcrete的必要代码开源。它的目的是允许通常只在Fluent接口中可用的功能(您必须将大量代码散布到Db类的OnModelCreating方法中)迁移到基于属性的特性。它允许您做如下事情:
[TablePerConcrete]
public class MySubclassTable : MyParentClassEntity

无论EF可能从您的父类/子类关系中推断出什么,都可以强制使用TPC。

这里有一个有趣的挑战,有时EF会搞砸继承的Id属性,将其设置为填充具有显式值而不是数据库生成。您可以通过使父类实现接口IId(它只是说:这有一个Id属性),然后用[ForcePKId]标记子类来确保它不会这样做。

public class MyParentClassEntity : IId
{
    public int Id { get; set; }
    . . .

[TablePerConcrete]
[ForcePKId]
public class MySubclassTable : MyParentClassEntity
{
    // No need for  PK/Id property here, it was inherited and will work as
    // you intended.

启动处理所有这些的代码非常简单 - 只需向您的Db类添加几行即可:

public class Db : DbContext
{
    . . .
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        var modelsProject = Assembly.GetExecutingAssembly();
        B9DbExtender.New().Extend(modelBuilder, modelsProject);

您可以通过以下两种方式之一进行访问:
  1. 通过单个gist访问,其中包含所有相关类的复制粘贴到一个文件中,此处:https://gist.github.com/b9chris/8efd30687d554d1ceeb3fee359c179f9

  2. 通过我们发布的开源库Brass9.Data进行访问。它还有很多其他EF6工具,如数据迁移。它也更加有组织,将类分解为通常所期望的单独文件:https://github.com/b9chris/Brass9.Data


@AdamGreen 已修复!对此感到抱歉。 - Chris Moschini

1

我使用映射类,但无妨。我是这样解决的:

public class PersonMap : EntityTypeConfiguration<Person>
{
    public PersonMap()
    {
        Map(m => { m.ToTable("Person"); m.MapInheritedProperties(); });

        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
    }

}

记住 - 基类必须是抽象的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接