Entity Framework:如何使用 TPT 将继承类型拆分到多个表
结论
通过 Fluent API 分别为基类和每个派生类指定表名,就可以把当前的 TPH 映射改为 TPT:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<PaymentComponent>()
.ToTable("PaymentComponent");
modelBuilder.Entity<GiftCouponPayment>()
.ToTable("GiftCouponPayment");
modelBuilder.Entity<ClubCardPayment>()
.ToTable("ClubCardPayment");
}
生成的数据库会保留 PaymentComponent 基表。公共属性存放在基表中,各派生表只保存自身新增的属性,并通过共享主键与基表关联。这就是 Table Per Type(TPT)的结构。
TPT 的表结构
假设两个派生类都有自己的属性,数据库结构大致如下:
PaymentComponent
----------------
PaymentComponentID
MyValue
MyType
GiftCouponPayment
-----------------
PaymentComponentID
GiftCoupon-specific columns...
ClubCardPayment
---------------
PaymentComponentID
ClubCard-specific columns...
GiftCouponPayment.PaymentComponentID 和 ClubCardPayment.PaymentComponentID 既是主键,也是指向 PaymentComponent 的外键。
保存一个 GiftCouponPayment 时,EF 通常会写入两行数据:
- 将公共字段写入
PaymentComponent。 - 将派生类字段写入
GiftCouponPayment。
查询派生类型时,EF 会连接这两个表,组合出完整对象。
完整映射示例
下面省略题目中尚未实现完整的方法,只保留与继承映射和关联有关的代码:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
public abstract class PaymentComponent
{
public int PaymentComponentID { get; set; }
public int MyValue { get; set; }
public string MyType { get; set; }
public abstract int GetEffectiveValue();
}
public class GiftCouponPayment : PaymentComponent
{
public override int GetEffectiveValue()
{
return MyValue;
}
}
public class ClubCardPayment : PaymentComponent
{
public override int GetEffectiveValue()
{
return MyValue;
}
}
public class Payment
{
public Payment()
{
PaymentComponents = new List<PaymentComponent>();
}
public int PaymentID { get; set; }
public DateTime PayedTime { get; set; }
public virtual ICollection<PaymentComponent> PaymentComponents { get; set; }
}
public class NerdDinners : DbContext
{
public NerdDinners(string connString)
: base(connString)
{
}
public DbSet<Payment> Payments { get; set; }
public DbSet<PaymentComponent> PaymentComponents { get; set; }
public DbSet<GiftCouponPayment> GiftCouponPayments { get; set; }
public DbSet<ClubCardPayment> ClubCardPayments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<PaymentComponent>()
.ToTable("PaymentComponent");
modelBuilder.Entity<GiftCouponPayment>()
.ToTable("GiftCouponPayment");
modelBuilder.Entity<ClubCardPayment>()
.ToTable("ClubCardPayment");
base.OnModelCreating(modelBuilder);
}
}
改用 TPT 后,客户端的保存代码不需要调整:
using (var db = new NerdDinners(connectionString))
{
var giftCouponPayment = new GiftCouponPayment
{
MyValue = 250,
MyType = "GiftCouponPayment"
};
var clubCardPayment = new ClubCardPayment
{
MyValue = 5000,
MyType = "ClubCardPayment"
};
var payment = new Payment
{
PayedTime = DateTime.Now,
PaymentComponents = new List<PaymentComponent>
{
giftCouponPayment,
clubCardPayment
}
};
db.Payments.Add(payment);
int recordsAffected = db.SaveChanges();
}
也可以使用数据注解
如果项目允许实体类依赖 System.ComponentModel.DataAnnotations.Schema,也可以在类型上添加 TableAttribute:
using System.ComponentModel.DataAnnotations.Schema;
[Table("PaymentComponent")]
public abstract class PaymentComponent
{
public int PaymentComponentID { get; set; }
public int MyValue { get; set; }
public string MyType { get; set; }
public abstract int GetEffectiveValue();
}
[Table("GiftCouponPayment")]
public class GiftCouponPayment : PaymentComponent
{
public override int GetEffectiveValue()
{
return MyValue;
}
}
[Table("ClubCardPayment")]
public class ClubCardPayment : PaymentComponent
{
public override int GetEffectiveValue()
{
return MyValue;
}
}
Fluent API 和数据注解任选一种即可。映射较复杂时,使用 Fluent API 可以把配置集中在一起,通常更便于维护。
注意事项
TPT 不会把完整对象只放进派生表
如果希望 GiftCouponPayment 表直接包含 MyValue、MyType 等全部继承属性,而且保存时完全不写入 PaymentComponent,这种结构就不是 TPT,而是更接近 Table Per Concrete Type(TPC)。
TPT 必须保留基表,派生实例的数据会分别保存在基表和派生表中。
MyType 通常不再用作鉴别器
TPH 需要通过鉴别器列判断一行数据属于哪个派生类型。TPT 则根据对应派生表中是否有记录来识别类型,因此 EF 不需要依赖 MyType 完成继承映射。
如果 MyType 只是用来区分 GiftCouponPayment 和 ClubCardPayment,可以考虑删除。若它还有其他业务含义,则可以继续保留。
派生类型最好有自己的字段
即使两个派生类没有新增的持久化属性,TPT 仍然可以为它们建立派生表,但每张表可能只有一个同时作为主键和外键的列。这种设计可以表达类型差异,却也会增加连接和写入成本,是否值得采用需要结合实际业务判断。
修改映射后需要更新数据库
修改 OnModelCreating 不会让已有数据库自动、安全地从 TPH 转换为 TPT。需要使用 EF Migration 或手工迁移完成以下操作:
- 创建两个派生表。
- 按原鉴别器将现有数据拆分到对应的派生表。
- 建立主键和外键约束。
- 确认数据完整后,移除旧的鉴别器列或旧结构。
如果数据库中已有生产数据,不应直接删除并重建数据库。
TPT 查询通常需要连接表
查询 GiftCouponPayment 时,需要连接 PaymentComponent 和 GiftCouponPayment。查询整个继承层次时,可能还会涉及更多表连接。如果派生类型较多或查询量较大,应检查执行计划和索引情况。
备注:内容仅供参考。
