【EF Core】继承策略——TPH

📅 2026/6/26 2:24:35
【EF Core】继承策略——TPH
TPH全称 Table Per Hierarchy。其特点是将整个继承链上的实体都存放在一个数据表中。假设有 X、Y 两实体Y 继承 X于是两个实体都存到名为 T_abc 的表中。由于多个类型映射到一个表中为了区分EF Core 会向数据表添加一个额外的列用于标识类型。默认用的是实体类的名称。2、TPTTable-Per-Type。从命名可以看出每个类型对应一个表。假设 B 继承 A那么A 映射到 TA 表B 映射到 TB 表。但数据表只映射当前类所定义的成员不包括从基类继承的成员。假如A 类有 ID、X 两个属性那么TA 表就会映射 ID 和 XB 类定义了 C 属性映射时只把 C 属性映射到 TB 表不会映射从 A 类继承的 ID 和 X 属性。3、TPC即 Table-Per-Concrete-Type。它强调“具体类型”其实它和 TPT 很像都每个实体都映射一个表但是TPC 策略中派生类会把从基类继承的成员也映射到表中。引沿上面举的例子即 A 类会映射 ID 和 X 属性到 TA 表而 B 类会映射 ID、X、C 属性到 TB 表。咱们先来研究 TPH 策略。我们举个例子有两个类它们存在继承关系。// 基类 public class BaseType { public int Bid { get; set; } } // 派生类 public class DeriveType : BaseType { public double SValue { get; set; } }EF Core 的公共约定类具备自动发现继承链的能力前提是你得先把这些实体类添加到数据库模型中。因此重写 DbContext 类的 OnModelCreating 方法时代码可以这样写// 配置基类实体 modelBuilder.EntityBaseType(ent { ent.HasKey(x x.Bid); // 主键 ent.ToTable(tb_zbzb); // 设置表名 }); // 配置派生类实体 modelBuilder.EntityDeriveType();在配置模型时有些地方得注意A、TPH 只映射一个表基类子类都存放一起所以只能在基类的配置中调用 ToTable 方法设置表名在配置派生类时不要再映射表名了B、主键只能在基类的配置中设置不能在派生类的配置中设置。如上面的例子如果在配置 DeriveType 类上设置主键比如这样。// 基类 public class BaseType { public int Bid { get; set; } } // 派生类 public class DeriveType : BaseType {public int Mid { get; set; }public double SValue { get; set; } } //-------------------------------------------------- // 配置基类实体 modelBuilder.EntityBaseType(ent { ent.ToTable(tb_zbzb); // 设置表名 }); // 配置派生类实体 modelBuilder.EntityDeriveType(d { d.HasKey(x x.Mid); // 主键 });运行后就会报以下错误A key cannot be configured on DeriveType because it is a derived type. The key must be configured on the root type BaseType. If you did not intend for BaseType to be included in the model, ensure that it is not referenced by a DbSet property on your context instance, referenced in a configuration call to ModelBuilder, or referenced from a navigation on a type that is included in the model.上面例子构建了以下数据库模型。Model: EntityType: BaseType Properties: Bid (int) Required PK AfterSave:Throw ValueGenerated.OnAddDiscriminator(no field, string) Shadow Required AfterSave:Throw MaxLength(8) Keys: Bid PK EntityType: DeriveTypeBase: BaseTypeProperties: SValue (double) Required在 DeriveType 实体的模型信息中Base: BaseType 表示它的基类是 BaseType。说明 EF Core 已经自动配置好继承关系。如果你想手动配置实体的继承关系可以在配置派生类实体时调用 HasBaseType 方法。modelBuilder.EntityDeriveType() .HasBaseTypeBaseType();调用 HasBaseType 方法所指的基类必须是 .NET 代码中确实存在继承关系的类。比如如果 DeriveType 不是 BaseType 的派生类那么调用 HasBaseType 方法会报错。EF Core 会验证 .NET 类型是否存在继承关系。我们还会发现EF Core 为 BaseType 实体添加了一个叫 Discriminator 的影子属性它会被映射到数据表中的某一列。Discriminator 叫鉴别器或叫判别器。作用是标识类型的——区分这条数据记录是哪个实体类型的。默认实现是在插入数据时存入实体名称。-----------------------------------------------------------------------------------------------接下来咱们用另一个示例来研究一下如何自定义鉴别器。先定义一个 Shape 基类。public abstract class Shape { public int ShapeID { get; set; } }戴上老花镜看清楚它是一个抽象类。你没看错EF Core 是允许向模型添加抽象类的但随后你必须添加其实现类。毕竟银河系人都知道抽象类无法生娃……不是无法实例化。然后有几个类派生自 Shape 类。public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } } public class Triangle : Shape { public int A { get; set; } public int B { get; set; } public int C { get; set; } } public class Circle : Shape { public float R { get; set; } }下面代码定义数据库上下文实现模型配置。public class MyContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 配置数据库连接 SqlConnectionStringBuilder cb new(); cb.DataSource (localdb)\\mssqllocaldb; cb.InitialCatalog testdb; cb.IntegratedSecurity true; optionsBuilder.UseSqlServer(cb.ConnectionString); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityShape(ent { // 配置表名 ent.ToTable(tb_shapes); // 继承映射策略可省略 ent.UseTphMappingStrategy(); // 主键 ent.HasKey(s s.ShapeID).HasName(PK_Shapes); // 自定义鉴定器 ent.HasDiscriminatorint(TypeId).HasValueShape(1) .HasValueRectangle(2) .HasValueTriangle(3) .HasValueCircle(4); }); } }其他代码你已经很熟了不用看了咱们重点关心这里ent.HasDiscriminatorint(TypeId) .HasValueShape(1) .HasValueRectangle(2) .HasValueTriangle(3) .HasValueCircle(4);HasDiscriminator 方法用来自定义类型鉴定器在本示例中它是 int 类型。名字叫 TypeIdEF Core 会自动帮我们往 Shape 实体添加影子属性TypeId。这么一搞咱们就覆盖了默认的类型标识方法HasValue 方法用于为各个派生类实体设置鉴别值。在本示例中Shape 类的对象我们用1表示Rectangle 对象我们用 2 表示Circle 对象用 4 表示。实际上 Shape 类型的标识可能不会出现我们无法把抽象类的实例添加到数据集合中。你也看到了上述代码在配置完 Shape 类后并没有去添加 Rectangle 等几个派生类。这是因为在自定义类型鉴别器时HasValue 方法提到了几个派生类于是EF Core 就帮我们添加了。当然我们也可以像常规实体一样配置自定义的属性与列之间的映射。只要注意 ToTable 方法只能在基类 Shape 中配置。简单地说就是表名、主键都必须在基类中配置。protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityShape(ent { // 配置表名 ent.ToTable(tb_shapes); // 继承映射策略可省略 ent.UseTphMappingStrategy(); // 属性映射 ent.Property(s s.ShapeID).HasColumnName(sid); // 主键 ent.HasKey(s s.ShapeID).HasName(PK_Shapes); // 自定义鉴定器 ent.HasDiscriminatorint(TypeId) .HasValueShape(1) .HasValueRectangle(2) .HasValueTriangle(3) .HasValueCircle(4); ent.Property(TypeId).HasColumnName(_type_id); }); modelBuilder.EntityRectangle(ent { ent.Property(ss.Width) .HasColumnName(rect_wid) .HasColumnType(decimal) .HasPrecision(8, 2); // 精度 ent.Property(mm.Height) .HasColumnName(rect_hei) .HasColumnType(decimal) .HasPrecision(8, 2); }); modelBuilder.EntityCircle(ce { ce.Property(bb.R).HasColumnName(cir_r) .HasColumnType(decimal) .HasPrecision(10, 3); }); modelBuilder.EntityTriangle(ent { ent.Property(aa.A).HasColumnName(a_len); ent.Property(aa.B).HasColumnName(b_len); ent.Property(aa.C).HasColumnName(c_len); }); }然后创建的数据表如下CREATE TABLE [tb_shapes] ( [sid] int NOT NULL IDENTITY, [_type_id] int NOT NULL, [cir_r] decimal(10,3) NULL, [rect_wid] decimal(8,2) NULL, [rect_hei] decimal(8,2) NULL, [a_len] int NULL, [b_len] int NULL, [c_len] int NULL, CONSTRAINT [PK_Shapes] PRIMARY KEY ([sid]) );咱们试着每种类型的实体都添加一条记录。using(var context new MyContext()) { // 获取数据集合 DbSetRectangle rects context.SetRectangle(); DbSetTriangle triangles context.SetTriangle(); DbSetCircle circles context.SetCircle(); // 添加数据 rects.Add(new Rectangle { Width 120.0d, Height 90.0d }); circles.Add(new Circle { R 19.2f }); triangles.Add(new Triangle { A 16, B 14,