EF Core 继承映射
大约 20 分钟约 6135 字
EF Core 继承映射
简介
EF Core 继承映射用于把面向对象中的继承关系映射到关系型数据库。它最常见的三种策略是 TPH(Table Per Hierarchy)、TPT(Table Per Type)和 TPC(Table Per Concrete Type),不同策略在查询性能、表结构清晰度、迁移复杂度上差异明显。
在实际项目中,继承映射的选型直接影响数据库表结构、查询效率以及后续的维护成本。理解每种策略的底层 SQL 生成逻辑和适用场景,是做出正确技术决策的前提。
特点
实体模型设计
基础领域模型
在深入三种策略之前,先定义一个完整的领域模型,后续所有示例都基于此模型展开。
/// <summary>
/// 支付方式基类 — 抽象类,不能直接实例化
/// </summary>
public abstract class PaymentMethod
{
public int Id { get; set; }
public string DisplayName { get; set; } = string.Empty;
public decimal FeeRate { get; set; }
public bool IsEnabled { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// 信用卡支付
/// </summary>
public class CreditCardPayment : PaymentMethod
{
public string CardLast4 { get; set; } = string.Empty;
public string CardBrand { get; set; } = string.Empty; // Visa, MasterCard, etc.
public int ExpiryMonth { get; set; }
public int ExpiryYear { get; set; }
}
/// <summary>
/// 银行转账支付
/// </summary>
public class BankTransferPayment : PaymentMethod
{
public string BankAccountNo { get; set; } = string.Empty;
public string BankName { get; set; } = string.Empty;
public string SwiftCode { get; set; } = string.Empty;
}
/// <summary>
/// 第三方支付(支付宝、微信等)
/// </summary>
public class ThirdPartyPayment : PaymentMethod
{
public string ProviderName { get; set; } = string.Empty;
public string AppId { get; set; } = string.Empty;
public string MerchantNo { get; set; } = string.Empty;
}更复杂的继承层级
/// <summary>
/// 三级继承示例:人员 -> 员工 -> 管理者
/// </summary>
public abstract class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class Employee : Person
{
public string Department { get; set; } = string.Empty;
public decimal Salary { get; set; }
}
public class Manager : Employee
{
public string Title { get; set; } = string.Empty;
public int Level { get; set; }
}
public class Contractor : Person
{
public string Company { get; set; } = string.Empty;
public DateTime ContractEnd { get; set; }
}实现
TPH(Table Per Hierarchy)
TPH 是 EF Core 的默认继承映射策略。所有继承层级中的实体都映射到同一张表中,通过一个"鉴别列"(Discriminator)来区分不同类型的行。
基础配置
public class AppDbContext : DbContext
{
public DbSet<PaymentMethod> PaymentMethods => Set<PaymentMethod>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentMethod>()
.HasDiscriminator<string>("payment_type")
.HasValue<CreditCardPayment>("card")
.HasValue<BankTransferPayment>("bank")
.HasValue<ThirdPartyPayment>("third_party");
}
}TPH 生成的表结构
-- TPH 典型生成表结构
CREATE TABLE PaymentMethods (
Id INT PRIMARY KEY IDENTITY(1,1),
DisplayName NVARCHAR(100) NOT NULL,
FeeRate DECIMAL(18,4) NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
payment_type NVARCHAR(20) NOT NULL, -- 鉴别列
-- CreditCardPayment 专属字段
CardLast4 NVARCHAR(4) NULL,
CardBrand NVARCHAR(50) NULL,
ExpiryMonth INT NULL,
ExpiryYear INT NULL,
-- BankTransferPayment 专属字段
BankAccountNo NVARCHAR(50) NULL,
BankName NVARCHAR(100) NULL,
SwiftCode NVARCHAR(20) NULL,
-- ThirdPartyPayment 专属字段
ProviderName NVARCHAR(50) NULL,
AppId NVARCHAR(100) NULL,
MerchantNo NVARCHAR(100) NULL
);
-- 鉴别列必须非空,且建议添加索引
CREATE INDEX IX_PaymentMethods_payment_type
ON PaymentMethods(payment_type);TPH 查询操作
// 查询某一子类 — OfType<T> 会自动添加 WHERE payment_type = 'card'
var cardMethods = await db.PaymentMethods
.OfType<CreditCardPayment>()
.Where(x => x.CardLast4 == "8888")
.ToListAsync();
// 生成的 SQL:
// SELECT [p].[Id], [p].[DisplayName], [p].[FeeRate], [p].[IsEnabled],
// [p].[CreatedAt], [p].[payment_type],
// [p].[CardLast4], [p].[CardBrand], [p].[ExpiryMonth], [p].[ExpiryYear]
// FROM [PaymentMethods] AS [p]
// WHERE [p].[payment_type] IN (N'card') AND ([p].[CardLast4] = N'8888')
// 查询基类 — 返回所有子类型
var allMethods = await db.PaymentMethods
.Where(x => x.IsEnabled)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync();
// 使用 AsSplitQuery 避免笛卡尔爆炸(多对多关联时)
var methodsWithOrders = await db.PaymentMethods
.OfType<CreditCardPayment>()
.Include(x => x.Orders)
.AsSplitQuery()
.ToListAsync();TPH 鉴别列的高级配置
// 使用枚举作为鉴别器值
public enum PaymentType
{
CreditCard = 1,
BankTransfer = 2,
ThirdParty = 3
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentMethod>()
.HasDiscriminator<PaymentType>("PaymentTypeId")
.HasValue<CreditCardPayment>(PaymentType.CreditCard)
.HasValue<BankTransferPayment>(PaymentType.BankTransfer)
.HasValue<ThirdPartyPayment>(PaymentType.ThirdParty);
// 为鉴别列设置默认值
modelBuilder.Entity<PaymentMethod>()
.Property("PaymentTypeId")
.HasDefaultValue(PaymentType.CreditCard);
}TPH 索引策略
-- 复合索引:按类型 + 启用状态查询
CREATE INDEX IX_PaymentMethods_Type_Enabled
ON PaymentMethods(payment_type, IsEnabled)
INCLUDE (DisplayName, FeeRate);
-- 按品牌查询信用卡的索引
CREATE INDEX IX_PaymentMethods_CardBrand
ON PaymentMethods(CardBrand)
WHERE payment_type = 'card';
-- 筛选索引只对特定子类有效,大幅减少索引大小TPT(Table Per Type)
TPT 将基类映射到一张表,每个子类各自映射到一张独立的表,子类表通过外键引用基类表。
基础配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 每个实体显式指定表名即可启用 TPT
modelBuilder.Entity<PaymentMethod>().ToTable("PaymentMethods");
modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
modelBuilder.Entity<ThirdPartyPayment>().ToTable("ThirdPartyPayments");
}TPT 生成的表结构
-- 基类表:只包含公共字段
CREATE TABLE PaymentMethods (
Id INT PRIMARY KEY IDENTITY(1,1),
DisplayName NVARCHAR(100) NOT NULL,
FeeRate DECIMAL(18,4) NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
-- 信用卡子类表
CREATE TABLE CreditCardPayments (
Id INT PRIMARY KEY,
CardLast4 NVARCHAR(4) NOT NULL,
CardBrand NVARCHAR(50) NOT NULL,
ExpiryMonth INT NOT NULL,
ExpiryYear INT NOT NULL,
CONSTRAINT FK_CreditCardPayments_PaymentMethods
FOREIGN KEY (Id) REFERENCES PaymentMethods(Id)
);
-- 银行转账子类表
CREATE TABLE BankTransferPayments (
Id INT PRIMARY KEY,
BankAccountNo NVARCHAR(50) NOT NULL,
BankName NVARCHAR(100) NOT NULL,
SwiftCode NVARCHAR(20) NOT NULL,
CONSTRAINT FK_BankTransferPayments_PaymentMethods
FOREIGN KEY (Id) REFERENCES PaymentMethods(Id)
);
-- 第三方支付子类表
CREATE TABLE ThirdPartyPayments (
Id INT PRIMARY KEY,
ProviderName NVARCHAR(50) NOT NULL,
AppId NVARCHAR(100) NOT NULL,
MerchantNo NVARCHAR(100) NOT NULL,
CONSTRAINT FK_ThirdPartyPayments_PaymentMethods
FOREIGN KEY (Id) REFERENCES PaymentMethods(Id)
);TPT 查询分析
// 查询基类 — EF Core 会 LEFT JOIN 所有子类表
var allMethods = await db.PaymentMethods
.OrderBy(x => x.DisplayName)
.ToListAsync();
// 生成的 SQL(注意多个 LEFT JOIN):
// SELECT [p].[Id], [p].[DisplayName], [p].[FeeRate], [p].[IsEnabled], [p].[CreatedAt],
// [c].[Id], [c].[CardLast4], [c].[CardBrand], [c].[ExpiryMonth], [c].[ExpiryYear],
// [b].[Id], [b].[BankAccountNo], [b].[BankName], [b].[SwiftCode],
// [t].[Id], [t].[ProviderName], [t].[AppId], [t].[MerchantNo]
// FROM [PaymentMethods] AS [p]
// LEFT JOIN [CreditCardPayments] AS [c] ON [p].[Id] = [c].[Id]
// LEFT JOIN [BankTransferPayments] AS [b] ON [p].[Id] = [b].[Id]
// LEFT JOIN [ThirdPartyPayments] AS [t] ON [p].[Id] = [t].[Id]
// ORDER BY [p].[DisplayName]
// 按子类查询 — 只 JOIN 对应的子类表
var bankMethods = await db.PaymentMethods
.OfType<BankTransferPayment>()
.Where(x => x.BankName.Contains("工商"))
.ToListAsync();
// 生成的 SQL:
// SELECT [p].[Id], [p].[DisplayName], ..., [b].[BankAccountNo], ...
// FROM [PaymentMethods] AS [p]
// INNER JOIN [BankTransferPayments] AS [b] ON [p].[Id] = [b].[Id]
// WHERE [b].[BankName] LIKE N'%工商%'TPT 的性能陷阱
// 危险操作:基类查询 + Include 关联实体 + 多个子类
// 假设 PaymentMethod 有一个 Orders 导航属性
var dangerousQuery = await db.PaymentMethods
.Include(x => x.Orders) // 每个子类都要 JOIN Orders
.OrderBy(x => x.CreatedAt)
.Skip(10).Take(10)
.ToListAsync();
// 上面的查询在 TPT 下可能生成:
// SELECT ... FROM PaymentMethods p
// LEFT JOIN CreditCardPayments c ON p.Id = c.Id
// LEFT JOIN BankTransferPayments b ON p.Id = b.Id
// LEFT JOIN ThirdPartyPayments t ON p.Id = t.Id
// LEFT JOIN Orders o ON p.Id = o.PaymentMethodId
// -- 子类数量 + 关联数量 = JOIN 数量,笛卡尔积风险极高
// 推荐做法:按子类型分别查询再合并
var cardPayments = await db.PaymentMethods
.OfType<CreditCardPayment>()
.Include(x => x.Orders)
.OrderBy(x => x.CreatedAt)
.ToListAsync();
var bankPayments = await db.PaymentMethods
.OfType<BankTransferPayment>()
.Include(x => x.Orders)
.OrderBy(x => x.CreatedAt)
.ToListAsync();
// 或者使用 AsSplitQuery
var safeQuery = await db.PaymentMethods
.Include(x => x.Orders)
.AsSplitQuery()
.OrderBy(x => x.CreatedAt)
.Skip(10).Take(10)
.ToListAsync();TPC(Table Per Concrete Type)
TPC 将每个具体类型映射到完全独立的表,每张表都包含基类的所有字段。不映射抽象基类本身。
基础配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// EF Core 7+ 使用 UseTpcMappingStrategy
modelBuilder.Entity<PaymentMethod>().UseTpcMappingStrategy();
// 每个具体类型映射到独立表
modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
modelBuilder.Entity<ThirdPartyPayment>().ToTable("ThirdPartyPayments");
}TPC 生成的表结构
-- 每张表都包含基类字段的完整副本
CREATE TABLE CreditCardPayments (
Id INT PRIMARY KEY IDENTITY(1,1),
DisplayName NVARCHAR(100) NOT NULL,
FeeRate DECIMAL(18,4) NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
CardLast4 NVARCHAR(4) NOT NULL,
CardBrand NVARCHAR(50) NOT NULL,
ExpiryMonth INT NOT NULL,
ExpiryYear INT NOT NULL
);
CREATE TABLE BankTransferPayments (
Id INT PRIMARY KEY IDENTITY(1,1),
DisplayName NVARCHAR(100) NOT NULL,
FeeRate DECIMAL(18,4) NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
BankAccountNo NVARCHAR(50) NOT NULL,
BankName NVARCHAR(100) NOT NULL,
SwiftCode NVARCHAR(20) NOT NULL
);
CREATE TABLE ThirdPartyPayments (
Id INT PRIMARY KEY IDENTITY(1,1),
DisplayName NVARCHAR(100) NOT NULL,
FeeRate DECIMAL(18,4) NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
ProviderName NVARCHAR(50) NOT NULL,
AppId NVARCHAR(100) NOT NULL,
MerchantNo NVARCHAR(100) NOT NULL
);TPC 查询操作
// 查询基类 — EF Core 使用 UNION ALL 合并所有具体类型表
var allMethods = await db.PaymentMethods
.Where(x => x.IsEnabled)
.ToListAsync();
// 生成的 SQL:
// SELECT [c].[Id], [c].[DisplayName], ..., [c].[CardLast4], ...
// FROM [CreditCardPayments] AS [c]
// WHERE [c].[IsEnabled] = CAST(1 AS BIT)
// UNION ALL
// SELECT [b].[Id], [b].[DisplayName], ..., [b].[BankAccountNo], ...
// FROM [BankTransferPayments] AS [b]
// WHERE [b].[IsEnabled] = CAST(1 AS BIT)
// UNION ALL
// SELECT [t].[Id], [t].[DisplayName], ..., [t].[ProviderName], ...
// FROM [ThirdPartyPayments] AS [t]
// WHERE [t].[IsEnabled] = CAST(1 AS BIT)
// 按子类查询 — 直接查单表,无 UNION
var cardMethods = await db.PaymentMethods
.OfType<CreditCardPayment>()
.Where(x => x.CardBrand == "Visa")
.ToListAsync();
// 生成的 SQL:
// SELECT [c].[Id], [c].[DisplayName], ..., [c].[CardLast4], ...
// FROM [CreditCardPayments] AS [c]
// WHERE [c].[CardBrand] = N'Visa'TPC 的 Id 冲突问题
// TPC 的关键问题:各表独立自增,Id 可能冲突
// 例如 CreditCardPayments 的 Id=1 和 BankTransferPayments 的 Id=1 同时存在
// 解决方案 1:使用 GUID 作为主键
public abstract class PaymentMethod
{
public Guid Id { get; set; } = Guid.NewGuid();
// ...
}
// 解决方案 2:使用 Sequence 替代 Identity(EF Core 7+)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("PaymentMethodSeq")
.StartsAt(1)
.IncrementsBy(1);
modelBuilder.Entity<CreditCardPayment>()
.Property(x => x.Id)
.UseSequence("PaymentMethodSeq");
modelBuilder.Entity<BankTransferPayment>()
.Property(x => x.Id)
.UseSequence("PaymentMethodSeq");
modelBuilder.Entity<ThirdPartyPayment>()
.Property(x => x.Id)
.UseSequence("PaymentMethodSeq");
}
// 解决方案 3:使用 HiLo 模式
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CreditCardPayment>()
.Property(x => x.Id)
.UseHiLo("PaymentMethodHiLo");
modelBuilder.Entity<BankTransferPayment>()
.Property(x => x.Id)
.UseHiLo("PaymentMethodHiLo");
}三种策略深度对比
查询性能对比
场景 1:按子类查询(OfType<T>)
- TPH: SELECT ... FROM Table WHERE type = 'X' -- 最快,单表 + 索引过滤
- TPT: SELECT ... FROM Base JOIN Sub WHERE ... -- 快,单次 JOIN
- TPC: SELECT ... FROM SubTable WHERE ... -- 最快,直接查子类表
场景 2:按基类查询(所有子类)
- TPH: SELECT ... FROM Table -- 最快,单表扫描
- TPT: SELECT ... FROM Base LEFT JOIN Sub1 LEFT JOIN Sub2 -- 最慢,多个 LEFT JOIN
- TPC: SELECT ... FROM T1 UNION ALL SELECT ... FROM T2 -- 中等,UNION ALL 成本
场景 3:基类查询 + 分页
- TPH: 单表 ORDER BY + OFFSET FETCH -- 最优
- TPT: 多表 JOIN + ORDER BY + OFFSET FETCH -- 最差,需合并排序
- TPC: UNION ALL + ORDER BY + OFFSET FETCH -- 中等,UNION 后排序
场景 4:基类查询 + Include 关联
- TPH: 单表 JOIN 关联表 -- 快
- TPT: 多表 LEFT JOIN + JOIN 关联表 -- 笛卡尔积风险
- TPC: UNION ALL JOIN 关联表 -- 每个 UNION 分支都 JOIN写入性能对比
插入操作:
- TPH: INSERT INTO 一张表 -- 单次写入
- TPT: INSERT INTO 基类表 + INSERT INTO 子类表 -- 两次写入(事务)
- TPC: INSERT INTO 子类表 -- 单次写入(数据量较大)
更新基类字段:
- TPH: UPDATE Table SET ... WHERE Id = @id -- 单次更新
- TPT: UPDATE BaseTable SET ... WHERE Id = @id -- 单次更新(只改基类表)
- TPC: 需要知道具体子类,更新对应子类表 -- 需要先查询确定类型
删除操作:
- TPH: DELETE FROM Table WHERE Id = @id -- 单次删除
- TPT: DELETE FROM SubTable + DELETE FROM BaseTable -- 级联删除或两次操作
- TPC: DELETE FROM SubTable WHERE Id = @id -- 单次删除存储空间对比
-- 假设有 10000 条记录:信用卡 5000,银行转账 3000,第三方 2000
-- TPH:1 张表,约 10000 行,每行约 500 字节(大量 NULL)
-- 有效数据占比 ≈ 50%,浪费空间 ≈ 50%
-- TPT:4 张表,基类表 10000 行 + 子类表各自行数
-- 无 NULL 冗余,但需要额外存储外键索引
-- 总空间略小于 TPH
-- TPC:3 张表,每张表包含基类字段的完整副本
-- 基类字段在 3 张表中重复存储
-- 总空间 ≈ TPH 的 1.5 倍(因为基类字段重复 3 次)实战模式
模式一:支付系统中的继承映射
// 完整的支付系统继承模型
public abstract class PaymentMethod
{
public int Id { get; set; }
public string DisplayName { get; set; } = string.Empty;
public decimal FeeRate { get; set; }
public bool IsEnabled { get; set; } = true;
public int SortOrder { get; set; }
// 导航属性
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}
public class CreditCardPayment : PaymentMethod
{
public string CardLast4 { get; set; } = string.Empty;
public string CardBrand { get; set; } = string.Empty;
public int ExpiryMonth { get; set; }
public int ExpiryYear { get; set; }
public decimal MinAmount { get; set; }
public decimal MaxAmount { get; set; }
}
public class BankTransferPayment : PaymentMethod
{
public string BankAccountNo { get; set; } = string.Empty;
public string BankName { get; set; } = string.Empty;
public string SwiftCode { get; set; } = string.Empty;
public decimal DailyLimit { get; set; }
}
public class ThirdPartyPayment : PaymentMethod
{
public string ProviderName { get; set; } = string.Empty;
public string AppId { get; set; } = string.Empty;
public string MerchantNo { get; set; } = string.Empty;
public string CallbackUrl { get; set; } = string.Empty;
}
// 服务层使用多态处理
public class PaymentService
{
private readonly AppDbContext _db;
public PaymentService(AppDbContext db)
{
_db = db;
}
// 获取所有可用的支付方式
public async Task<List<PaymentMethod>> GetEnabledMethodsAsync()
{
return await _db.PaymentMethods
.Where(x => x.IsEnabled)
.OrderBy(x => x.SortOrder)
.ToListAsync();
}
// 按类型获取支付方式
public async Task<List<CreditCardPayment>> GetCreditCardMethodsAsync()
{
return await _db.PaymentMethods
.OfType<CreditCardPayment>()
.Where(x => x.IsEnabled && x.ExpiryYear >= DateTime.UtcNow.Year)
.ToListAsync();
}
// 多态处理:计算手续费
public decimal CalculateFee(PaymentMethod method, decimal amount)
{
return method switch
{
CreditCardPayment cc when amount < cc.MinAmount =>
throw new InvalidOperationException("金额低于最低限额"),
BankTransferPayment bt when amount > bt.DailyLimit =>
throw new InvalidOperationException("超出每日限额"),
_ => amount * method.FeeRate
};
}
// 使用策略模式处理不同支付类型
public async Task ProcessPaymentAsync(int methodId, decimal amount)
{
var method = await _db.PaymentMethods.FindAsync(methodId)
?? throw new NotFoundException("支付方式不存在");
// 使用模式匹配进行多态处理
switch (method)
{
case CreditCardPayment cc:
await ProcessCreditCardAsync(cc, amount);
break;
case BankTransferPayment bt:
await ProcessBankTransferAsync(bt, amount);
break;
case ThirdPartyPayment tp:
await ProcessThirdPartyAsync(tp, amount);
break;
default:
throw new NotSupportedException($"不支持的支付类型: {method.GetType().Name}");
}
}
private Task ProcessCreditCardAsync(CreditCardPayment cc, decimal amount)
{
// 信用卡支付逻辑
Console.WriteLine($"信用卡支付: {cc.CardBrand} ****{cc.CardLast4}, 金额: {amount}");
return Task.CompletedTask;
}
private Task ProcessBankTransferAsync(BankTransferPayment bt, decimal amount)
{
// 银行转账逻辑
Console.WriteLine($"银行转账: {bt.BankName} ({bt.SwiftCode}), 金额: {amount}");
return Task.CompletedTask;
}
private Task ProcessThirdPartyAsync(ThirdPartyPayment tp, decimal amount)
{
// 第三方支付逻辑
Console.WriteLine($"第三方支付: {tp.ProviderName}, 金额: {amount}");
return Task.CompletedTask;
}
}模式二:通知系统的多态渠道
/// <summary>
/// 通知基类
/// </summary>
public abstract class NotificationChannel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public string TemplatePrefix { get; set; } = string.Empty;
}
public class EmailChannel : NotificationChannel
{
public string SmtpHost { get; set; } = string.Empty;
public int SmtpPort { get; set; } = 587;
public string SenderAddress { get; set; } = string.Empty;
public bool EnableSsl { get; set; } = true;
}
public class SmsChannel : NotificationChannel
{
public string ProviderName { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public string ApiUrl { get; set; } = string.Empty;
public int MaxLength { get; set; } = 70;
}
public class PushChannel : NotificationChannel
{
public string FirebaseProjectId { get; set; } = string.Empty;
public string ServerKey { get; set; } = string.Empty;
public string AndroidTopic { get; set; } = string.Empty;
public string IosBundleId { get; set; } = string.Empty;
}
// TPH 配置 — 通知渠道数量有限且查询频繁,TPH 是最佳选择
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<NotificationChannel>()
.HasDiscriminator<string>("channel_type")
.HasValue<EmailChannel>("email")
.HasValue<SmsChannel>("sms")
.HasValue<PushChannel>("push");
}模式三:设备管理中的继承
// 设备类型差异大,各自独立管理,TPC 更合适
public abstract class Device
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;
public string Location { get; set; } = string.Empty;
}
public class TemperatureSensor : Device
{
public double MinRange { get; set; }
public double MaxRange { get; set; }
public double CurrentValue { get; set; }
public string Unit { get; set; } = "Celsius";
}
public class Camera : Device
{
public string IpAddress { get; set; } = string.Empty;
public int Resolution { get; set; }
public bool HasNightVision { get; set; }
public string StoragePath { get; set; } = string.Empty;
}
public class AccessController : Device
{
public string Protocol { get; set; } = string.Empty;
public int MaxUsers { get; set; }
public bool RequiresAuth { get; set; } = true;
}
// TPC 配置 — 设备类型差异大且各自独立管理
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Device>().UseTpcMappingStrategy();
modelBuilder.Entity<TemperatureSensor>().ToTable("TemperatureSensors");
modelBuilder.Entity<Camera>().ToTable("Cameras");
modelBuilder.Entity<AccessController>().ToTable("AccessControllers");
}策略迁移
从 TPH 迁移到 TPT
// 迁移步骤:
// 1. 创建新的子类表
// 2. 将数据从单表迁移到子类表
// 3. 删除原表中的子类字段
// 迁移类示例
public partial class MigrateTphToTpt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. 创建子类表
migrationBuilder.CreateTable(
name: "CreditCardPayments",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
CardLast4 = table.Column<string>(type: "nvarchar(4)", maxLength: 4, nullable: false),
CardBrand = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
ExpiryMonth = table.Column<int>(type: "int", nullable: false),
ExpiryYear = table.Column<int>(type: "int", nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_CreditCardPayments", x => x.Id);
table.ForeignKey(
name: "FK_CreditCardPayments_PaymentMethods_Id",
column: x => x.Id,
principalTable: "PaymentMethods",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
// 2. 迁移数据
migrationBuilder.Sql(@"
INSERT INTO CreditCardPayments (Id, CardLast4, CardBrand, ExpiryMonth, ExpiryYear)
SELECT Id, CardLast4, CardBrand, ExpiryMonth, ExpiryYear
FROM PaymentMethods
WHERE payment_type = 'card'
");
// 3. 删除原表中的子类字段
migrationBuilder.DropColumn(name: "CardLast4", table: "PaymentMethods");
migrationBuilder.DropColumn(name: "CardBrand", table: "PaymentMethods");
migrationBuilder.DropColumn(name: "ExpiryMonth", table: "PaymentMethods");
migrationBuilder.DropColumn(name: "ExpiryYear", table: "PaymentMethods");
migrationBuilder.DropColumn(name: "payment_type", table: "PaymentMethods");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 回滚操作...
}
}在 OnModelCreating 中切换策略
// 切换前:TPH(默认)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 不做任何配置,默认就是 TPH
modelBuilder.Entity<PaymentMethod>();
}
// 切换后:TPT
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentMethod>().ToTable("PaymentMethods");
modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
}
// 切换后:TPC
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentMethod>().UseTpcMappingStrategy();
modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
}生产环境注意事项
索引优化
-- TPH 策略索引
-- 鉴别列索引(必须)
CREATE INDEX IX_PaymentMethods_Discriminator ON PaymentMethods(payment_type);
-- 子类字段 + 鉴别列的复合索引(避免全表扫描)
CREATE INDEX IX_PaymentMethods_CardBrand
ON PaymentMethods(CardBrand)
WHERE payment_type = 'card';
-- TPT 策略索引
-- 基类表索引
CREATE INDEX IX_PaymentMethods_IsEnabled ON PaymentMethods(IsEnabled);
-- 子类表独立索引
CREATE INDEX IX_CreditCardPayments_Brand ON CreditCardPayments(CardBrand);
CREATE INDEX IX_BankTransferPayments_Bank ON BankTransferPayments(BankName);
-- TPC 策略索引
-- 每张表独立索引,无跨表索引
CREATE INDEX IX_CreditCardPayments_Brand ON CreditCardPayments(CardBrand);
CREATE INDEX IX_BankTransferPayments_Bank ON BankTransferPayments(BankName);数据量评估
-- TPH 适合场景
-- 数据量 < 100 万行
-- 子类数量 <= 5 个
-- 子类专属字段 <= 10 个
-- 基类查询频率 > 子类查询频率
-- TPT 适合场景
-- 子类有大量专属字段(> 15 个)
-- 数据规范化要求高
-- 基类查询频率低,主要是子类查询
-- 数据量 < 50 万行
-- TPC 适合场景
-- 子类差异极大(字段完全不同)
-- 子类数据量差异大(某类特别多)
-- 主要按子类独立查询
-- 需要更好的并发写入性能监控与告警
// 使用 EF Core 的日志来监控继承映射查询性能
public class QueryLoggingInterceptor : DbCommandInterceptor
{
private readonly ILogger<QueryLoggingInterceptor> _logger;
public QueryLoggingInterceptor(ILogger<QueryLoggingInterceptor> logger)
{
_logger = logger;
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command, CommandExecutedEventData eventData, DbDataReader result,
CancellationToken cancellationToken = default)
{
// 检测包含 JOIN 的继承查询(TPT 模式)
if (command.CommandText.Contains("LEFT JOIN") &&
eventData.Duration.TotalMilliseconds > 500)
{
_logger.LogWarning(
"慢查询检测: 耗时 {Duration}ms, 可能是 TPT 继承映射查询\nSQL: {Sql}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
// 检测 UNION ALL(TPC 模式)
if (command.CommandText.Contains("UNION ALL") &&
eventData.Duration.TotalMilliseconds > 1000)
{
_logger.LogWarning(
"慢 UNION ALL 查询: 耗时 {Duration}ms\nSQL: {Sql}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
}
// 注册拦截器
services.AddScoped<DbCommandInterceptor, QueryLoggingInterceptor>();与其他 EF Core 特性的配合
继承 + 全局查询过滤器
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 全局过滤器与继承映射配合使用
modelBuilder.Entity<PaymentMethod>()
.HasQueryFilter(x => x.IsEnabled);
// 注意:OfType<T> 查询会自动应用基类的全局过滤器
// 查询 CreditCardPayment 时会自动添加 IsEnabled = true 条件
}继承 + 拥有类型(Owned Types)
// 继承实体可以包含拥有类型
public class CreditCardPayment : PaymentMethod
{
public string CardLast4 { get; set; } = string.Empty;
// 拥有类型:信用卡限额配置
public BillingLimit Limit { get; set; } = new();
}
public record BillingLimit
{
public decimal DailyLimit { get; set; }
public decimal MonthlyLimit { get; set; }
public decimal SingleTransactionLimit { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CreditCardPayment>()
.OwnsOne(x => x.Limit, limit =>
{
limit.Property(l => l.DailyLimit).HasColumnName("DailyLimit");
limit.Property(l => l.MonthlyLimit).HasColumnName("MonthlyLimit");
limit.Property(l => l.SingleTransactionLimit).HasColumnName("SingleTxLimit");
});
}继承 + 并发控制
// 为基类添加并发令牌
public abstract class PaymentMethod
{
public int Id { get; set; }
public string DisplayName { get; set; } = string.Empty;
// 并发令牌
[Timestamp]
public byte[]? RowVersion { get; set; }
}
// TPH 模式下并发控制最简单
// TPT 模式下 RowVersion 放在基类表
// TPC 模式下 RowVersion 在每张子类表中都存在
// 处理并发冲突
try
{
var method = await db.PaymentMethods.FindAsync(id);
method.IsEnabled = false;
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// 并发冲突处理
foreach (var entry in ex.Entries)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
// 记录已被删除
}
else
{
// 使用数据库值刷新
entry.OriginalValues.SetValues(databaseValues);
}
}
await db.SaveChangesAsync();
}优点
缺点
总结
EF Core 继承映射没有绝对最优解,关键在于业务场景。如果更看重查询性能和实现简单,TPH 通常是第一选择;如果更看重表结构表达和严格拆分,TPT 更自然;而 TPC 更适合子类差异大且单独查询多的场景。在大多数项目中,TPH 应该是默认选项,只有遇到明确的痛点时再考虑其他策略。
关键知识点
- TPH 最常用,默认也最省查询成本。
- TPT 更接近关系建模,但查询复杂度更高。
- TPC 适合具体子类分布差异大、跨类查询少的情况。
- 继承映射设计前要先看真实查询模式,而不是只看模型美观度。
- TPC 需要特别注意主键冲突问题,推荐使用 GUID 或 Sequence。
- 策略迁移需要仔细编写数据迁移脚本,避免数据丢失。
- 配合全局查询过滤器和并发令牌使用时需注意各策略的差异。
项目落地视角
- 支付方式、通知渠道、设备类型很适合做继承建模。
- 后台列表若常按基类统一查询,优先考虑 TPH。
- 若每个子类有大量专属字段且独立查询多,可评估 TPC。
- 上线前应用真实数据量测试查询 SQL 和索引策略。
- 建议从 TPH 开始,遇到明确痛点再切换策略。
- 在子类数量超过 5 个时要特别关注 TPT 和 TPC 的复杂度。
常见误区
- 为了"结构更优雅"盲目用 TPT,忽略查询性能。
- 子类差异很小却拆太多表,增加迁移和维护成本。
- 不测试生成 SQL,就在生产使用复杂继承结构。
- 把继承用在其实更适合组合建模的场景上。
- TPC 模式下使用 Identity 自增主键导致 Id 冲突。
- 在基类查询中使用 Include + 多个子类导致笛卡尔积。
- 切换策略时忘记处理已有数据的迁移。
进阶路线
- 深入理解 EF Core 生成 SQL 与执行计划。
- 学习 Owned Types、Value Objects 与继承的边界。
- 将继承映射与领域驱动设计中的聚合建模结合起来。
- 在大数据量环境下测试索引、分表与查询优化策略。
- 学习 EF Core 拦截器(Interceptor)监控继承映射查询性能。
- 探索继承映射与 CQRS 模式的结合使用。
适用场景
- 支付方式、消息渠道、运输方式等多态实体。
- 设备/账号/会员等级等基类 + 多子类的领域模型。
- 需要以统一接口处理多个具体实现的数据模型。
- 后端管理系统中的多态配置与策略实体。
- 通知系统中的多渠道(邮件、短信、推送)管理。
- IoT 系统中的多类型设备管理。
落地建议
- 从 TPH 开始,只有明确痛点再考虑 TPT/TPC。
- 为鉴别列、常用过滤字段建立合适索引。
- 用真实查询压测不同策略,而不是仅凭理论选择。
- 迁移前确认旧数据如何映射到新继承结构。
- TPC 策略务必使用 GUID 或 Sequence 避免主键冲突。
- 生产环境配置慢查询监控,及时发现继承映射性能问题。
- 编写集成测试覆盖每种子类的 CRUD 操作。
排错清单
- 检查生成的 SQL 是否出现过多 JOIN 或 UNION。
- 检查 Discriminator 配置和值是否与现有数据一致。
- 检查迁移脚本是否正确处理不同子类数据。
- 检查查询是否频繁按基类遍历所有子类数据。
- 检查 TPC 模式下是否存在主键冲突。
- 检查 TPT 模式下基类查询是否出现笛卡尔积。
- 检查索引是否覆盖了鉴别列和常用查询字段。
- 检查 Include 操作是否在继承查询中触发了 N+1 问题。
复盘问题
- 你的继承关系是真实业务差异,还是仅仅代码层习惯?
- 当前主要查询是按基类统一查,还是按子类分别查?
- 如果数据量扩大 10 倍,当前策略还能接受吗?
- 哪些场景其实更适合组合,而不是继承?
- 你是否测试过不同策略下的实际 SQL 执行计划?
- 切换策略的成本(数据迁移、代码改动)是否可接受?
- 你的子类数量未来会增长吗?增长后当前策略是否仍合适?
