实体框架6 (EF6)中按具体类型(TPC)继承的表

本文关键字:TPC 类型 继承 框架 EF6 实体 | 更新日期: 2023-09-27 18:12:57

为了避免使用每层次表(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"answers"Business"的两个(2)DbSet属性,然后再次测试。

public DbSet<Person> People { get; set; }
public DbSet<Business> Businesses { get; set; }

令我惊讶的是,项目构建,出去到数据库,只创建两个表与所有的类属性,包括从"BaseEntity"类继承的那些。我可以毫无问题地执行CRUD操作。

经过多次测试后,我无法在最终测试中发现任何问题,并且我无法重现两篇文章警告的重复键错误。

对数据库的更改已成功提交,但出现错误在更新对象上下文时发生。ObjectContext可能是处于不一致的状态。内部异常消息:AcceptChanges无法继续,因为对象的键值与另一个键值冲突ObjectStateManager. object。确保键值是

  1. 我很好奇为什么示例使用MapInheritedProperties属性;这是过时的方法吗?
  2. 为什么两个例子都说包含"BaseEntity"的配置属性,而包括DbSet属性或"BaseEntity"类的任何DbModelBuilder配置都会导致创建一个未使用的表?
  3. 针对物品警告的唯一键错误;我无法重现错误,并且我已经用主键作为数据库生成的int和数据库生成的guid进行了多次测试。关于这个错误的信息是否也过时了,或者我是否可以运行一个测试来产生这个错误?

实体框架6 (EF6)中按具体类型(TPC)继承的表

为了使这一切更简单,我移动了强制TablePerConcrete开放源代码所必需的代码。它的目的是允许通常只在Fluent接口中可用的特性(您必须将大量代码分散到Db类的OnModelCreating方法中)迁移到基于属性的特性。

它允许你做这样的事情:

[TablePerConcrete]
public class MySubclassTable : MyParentClassEntity

强制TPC不管EF可能决定从你的父类/子类关系中推断什么。

这里的一个有趣的挑战是,有时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. 通过将所有相关类复制粘贴到一个文件中,在这里:https://gist.github.com/b9chris/8efd30687d554d1ceeb3fee359c179f9

  2. 通过一个库,我们的Brass9。数据,我们将其开源。它有很多其他的EF6工具,比如数据迁移。它也更有组织,类被分解到单独的文件中,就像你通常期望的那样:https://github.com/b9chris/Brass9.Data

我使用映射类,但没关系。我是这样解决的:

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

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