Appearance
继承映射(TPH/TPT/TPC)
概述
继承映射(Inheritance Mapping)是 EF Core 中将面向对象继承关系映射到关系数据库的技术。EF Core 支持三种继承映射策略:
- TPH (Table-Per-Hierarchy): 所有类型共享一个表,使用鉴别器列区分
- TPT (Table-Per-Type): 每个类型独立表,通过外键关联
- TPC (Table-Per-Concrete-Type): 每个具体类型独立表,包含所有字段
TPH (Table-Per-Hierarchy)
概念
TPH 是 EF Core 的默认策略,将整个继承层次结构映射到单个表中,使用鉴别器列(Discriminator Column)来区分不同的子类型。
数据库表: PaymentMethods
┌────┬──────────┬──────────────┬────────────────┬───────────┐
│ Id │ Type │ CreditCardNo │ BankAccountNo │ Amount │
├────┼──────────┼──────────────┼────────────────┼───────────┤
│ 1 │ CC │ 1234-5678 │ NULL │ 100.00 │
│ 2 │ Bank │ NULL │ ACC-9876 │ 200.00 │
│ 3 │ CC │ 8765-4321 │ NULL │ 150.00 │
└────┴──────────┴──────────────┴────────────────┴───────────┘
↑ 鉴别器列示例: 支付方式系统
定义实体
csharp
// 基类
public abstract class PaymentMethod
{
public int Id { get; set; }
public string OwnerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
}
// 派生类 1: 信用卡
public class CreditCardPayment : PaymentMethod
{
public string CardNumber { get; set; } = string.Empty;
public string ExpiryDate { get; set; } = string.Empty;
public string CVV { get; set; } = string.Empty;
}
// 派生类 2: 银行账户
public class BankTransferPayment : PaymentMethod
{
public string BankName { get; set; } = string.Empty;
public string AccountNumber { get; set; } = string.Empty;
public string RoutingNumber { get; set; } = string.Empty;
}
// 派生类 3: 数字钱包
public class DigitalWalletPayment : PaymentMethod
{
public string WalletProvider { get; set; } = string.Empty;
public string WalletId { get; set; } = string.Empty;
}配置(可选,EF Core 会自动配置)
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PaymentMethod>(entity =>
{
// 指定鉴别器列名
entity.HasDiscriminator<string>("PaymentType")
.HasValue<CreditCardPayment>("CC")
.HasValue<BankTransferPayment>("Bank")
.HasValue<DigitalWalletPayment>("Wallet");
// 配置各属性
entity.Property(p => p.OwnerName).IsRequired().HasMaxLength(200);
entity.Property(p => p.Amount).HasColumnType("decimal(18,2)");
});
}使用示例
csharp
// 添加不同类型的支付记录
context.PaymentMethods.Add(new CreditCardPayment
{
OwnerName = "John Doe",
Amount = 100m,
CardNumber = "1234-5678-9012-3456",
ExpiryDate = "12/25",
CVV = "123"
});
context.PaymentMethods.Add(new BankTransferPayment
{
OwnerName = "Jane Smith",
Amount = 200m,
BankName = "Chase Bank",
AccountNumber = "ACC-9876",
RoutingNumber = "RTN-123"
});
await context.SaveChangesAsync();
// 查询所有支付方式
var allPayments = await context.PaymentMethods.ToListAsync();
// 只查询特定类型
var creditCards = await context.PaymentMethods
.OfType<CreditCardPayment>()
.ToListAsync();
var bankTransfers = await context.PaymentMethods
.OfType<BankTransferPayment>()
.Where(b => b.Amount > 150)
.ToListAsync();
// 生成的 SQL:
// SELECT [Id], [OwnerName], [Amount], [CardNumber], [ExpiryDate], [CVV]
// FROM [PaymentMethods]
// WHERE [PaymentType] = N'CC'TPH 优缺点
优点:
- ✅ 性能最佳(单表查询,无 JOIN)
- ✅ 简单直观
- ✅ EF Core 默认支持
缺点:
- ❌ 大量 NULL 值(未使用的列为 NULL)
- ❌ 无法对子类字段添加 NOT NULL 约束
- ❌ 表结构不够规范化
TPT (Table-Per-Type)
概念
TPT 为继承层次中的每个类型创建独立的表,基类和派生类通过主键/外键关联。
数据库表: PaymentMethods (基类表)
┌────┬───────────┬────────┐
│ Id │ OwnerName │ Amount │
├────┼───────────┼────────┤
│ 1 │ John Doe │ 100.00 │
│ 2 │ Jane Smith│ 200.00 │
└────┴───────────┴────────┘
数据库表: CreditCardPayments (派生类表)
┌────┬──────────────────┬────────────┬─────┐
│ Id │ CardNumber │ ExpiryDate │ CVV │
├────┼──────────────────┼────────────┼─────┤
│ 1 │ 1234-5678-... │ 12/25 │ 123 │
└────┴──────────────────┴────────────┴─────┘
↑ 外键引用 PaymentMethods.Id
数据库表: BankTransferPayments (派生类表)
┌────┬────────────┬───────────────┬─────────────┐
│ Id │ BankName │ AccountNumber │ RoutingNumber│
├────┼────────────┼───────────────┼─────────────┤
│ 2 │ Chase Bank │ ACC-9876 │ RTN-123 │
└────┴────────────┴───────────────┴─────────────┘配置
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 基类
modelBuilder.Entity<PaymentMethod>(entity =>
{
entity.ToTable("PaymentMethods");
entity.HasKey(p => p.Id);
});
// 派生类 1
modelBuilder.Entity<CreditCardPayment>(entity =>
{
entity.ToTable("CreditCardPayments");
entity.HasBaseType<PaymentMethod>(); // 关键: 指定基类
});
// 派生类 2
modelBuilder.Entity<BankTransferPayment>(entity =>
{
entity.ToTable("BankTransferPayments");
entity.HasBaseType<PaymentMethod>();
});
// 派生类 3
modelBuilder.Entity<DigitalWalletPayment>(entity =>
{
entity.ToTable("DigitalWalletPayments");
entity.HasBaseType<PaymentMethod>();
});
}使用示例
csharp
// 添加数据(与 TPH 相同)
context.PaymentMethods.Add(new CreditCardPayment
{
OwnerName = "John Doe",
Amount = 100m,
CardNumber = "1234-5678",
ExpiryDate = "12/25",
CVV = "123"
});
await context.SaveChangesAsync();
// 查询所有支付方式(需要 JOIN)
var allPayments = await context.PaymentMethods.ToListAsync();
// 查询特定类型
var creditCards = await context.PaymentMethods
.OfType<CreditCardPayment>()
.ToListAsync();
// 生成的 SQL:
// SELECT [p].[Id], [p].[OwnerName], [p].[Amount],
// [c].[CardNumber], [c].[ExpiryDate], [c].[CVV]
// FROM [PaymentMethods] AS [p]
// INNER JOIN [CreditCardPayments] AS [c] ON [p].[Id] = [c].[Id]TPT 优缺点
优点:
- ✅ 数据库规范化,无冗余
- ✅ 可以对子类字段添加约束
- ✅ 表结构清晰
缺点:
- ❌ 性能较差(需要 JOIN)
- ❌ 查询复杂度高
- ❌ 多层继承时 JOIN 更多
TPC (Table-Per-Concrete-Type)
概念
TPC 为每个具体(非抽象)类创建独立的表,每个表包含基类和子类的所有字段,没有共享的基类表。
数据库表: CreditCardPayments
┌────┬───────────┬────────┬──────────────────┬────────────┬─────┐
│ Id │ OwnerName │ Amount │ CardNumber │ ExpiryDate │ CVV │
├────┼───────────┼────────┼──────────────────┼────────────┼─────┤
│ 1 │ John Doe │ 100.00 │ 1234-5678-... │ 12/25 │ 123 │
└────┴───────────┴────────┴──────────────────┴────────────┴─────┘
数据库表: BankTransferPayments
┌────┬───────────┬────────┬────────────┬───────────────┬─────────────┐
│ Id │ OwnerName │ Amount │ BankName │ AccountNumber │ RoutingNumber│
├────┼───────────┼────────┼────────────┼───────────────┼─────────────┤
│ 2 │ Jane Smith│ 200.00 │ Chase Bank │ ACC-9876 │ RTN-123 │
└────┴───────────┴────────┴────────────┴───────────────┴─────────────┘
注意: 每个表都包含基类字段(OwnerName, Amount)配置
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 基类标记为抽象,不创建表
modelBuilder.Entity<PaymentMethod>()
.ToTable(t => t.ExcludeFromMigrations()); // EF Core 8+
// 派生类 1
modelBuilder.Entity<CreditCardPayment>(entity =>
{
entity.ToTable("CreditCardPayments");
entity.HasKey(p => p.Id);
// 配置基类字段
entity.Property(p => p.OwnerName).IsRequired();
entity.Property(p => p.Amount).HasColumnType("decimal(18,2)");
});
// 派生类 2
modelBuilder.Entity<BankTransferPayment>(entity =>
{
entity.ToTable("BankTransferPayments");
entity.HasKey(p => p.Id);
entity.Property(p => p.OwnerName).IsRequired();
entity.Property(p => p.Amount).HasColumnType("decimal(18,2)");
});
// 派生类 3
modelBuilder.Entity<DigitalWalletPayment>(entity =>
{
entity.ToTable("DigitalWalletPayments");
entity.HasKey(p => p.Id);
entity.Property(p => p.OwnerName).IsRequired();
entity.Property(p => p.Amount).HasColumnType("decimal(18,2)");
});
}使用示例
csharp
// 添加数据
context.CreditCardPayments.Add(new CreditCardPayment
{
OwnerName = "John Doe",
Amount = 100m,
CardNumber = "1234-5678",
ExpiryDate = "12/25",
CVV = "123"
});
context.BankTransferPayments.Add(new BankTransferPayment
{
OwnerName = "Jane Smith",
Amount = 200m,
BankName = "Chase Bank",
AccountNumber = "ACC-9876",
RoutingNumber = "RTN-123"
});
await context.SaveChangesAsync();
// 查询特定类型(单表查询,快速)
var creditCards = await context.CreditCardPayments.ToListAsync();
// 查询所有类型(需要 UNION)
var allPayments = await context.PaymentMethods.ToListAsync();
// 生成的 SQL:
// SELECT [Id], [OwnerName], [Amount], [CardNumber], [ExpiryDate], [CVV], 'CC' AS [Type]
// FROM [CreditCardPayments]
// UNION ALL
// SELECT [Id], [OwnerName], [Amount], [BankName], [AccountNumber], [RoutingNumber], 'Bank'
// FROM [BankTransferPayments]TPC 优缺点
优点:
- ✅ 查询性能好(单表查询)
- ✅ 可以对所有字段添加约束
- ✅ 表结构独立
缺点:
- ❌ 基类字段重复(数据冗余)
- ❌ 修改基类需要更新多个表
- ❌ UNION 查询复杂
三种策略对比
| 特性 | TPH | TPT | TPC |
|---|---|---|---|
| 表数量 | 1个 | N+1个(N=派生类数) | N个 |
| JOIN 操作 | ❌ 不需要 | ✅ 需要 | ❌ 不需要(但UNION) |
| NULL 值 | ⚠️ 大量NULL | ✅ 无NULL | ✅ 无NULL |
| 数据冗余 | ✅ 无冗余 | ✅ 无冗余 | ❌ 基类字段冗余 |
| 查询性能 | ⭐⭐⭐ 最快 | ⭐ 较慢 | ⭐⭐ 中等 |
| 插入性能 | ⭐⭐⭐ 最快 | ⭐⭐ 中等 | ⭐⭐ 中等 |
| 规范化程度 | ⭐ 低 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 扩展性 | ⚠️ 加字段影响所有类型 | ✅ 独立扩展 | ⚠️ 需修改多个表 |
| EF Core支持 | ✅ 默认 | ✅ 需配置 | ✅ 需配置(EF Core 7+) |
选择指南
推荐使用 TPH 的场景
✅ 大多数情况下的首选
- 继承层次较浅(2-3层)
- 子类字段差异不大
- 性能要求高
- 数据量不是特别大
csharp
// 典型场景: 通知系统
public abstract class Notification { }
public class EmailNotification : Notification { }
public class SmsNotification : Notification { }
public class PushNotification : Notification { }推荐使用 TPT 的场景
✅ 需要严格数据完整性
- 子类有大量独有字段
- 需要对子类字段添加约束
- 数据库规范化要求高
- 继承层次较深
csharp
// 典型场景: 支付系统(不同支付方式差异大)
public abstract class PaymentMethod { }
public class CreditCardPayment : PaymentMethod { }
public class BankTransferPayment : PaymentMethod { }
public class CryptocurrencyPayment : PaymentMethod { }推荐使用 TPC 的场景
✅ 子类完全独立
- 子类之间几乎没有共同行为
- 每个子类独立演进
- 需要避免 NULL 值
- EF Core 7+ 版本
csharp
// 典型场景: 报表系统
public abstract class Report { }
public class SalesReport : Report { }
public class InventoryReport : Report { }
public class FinancialReport : Report { }高级主题
1. 多层继承
csharp
public abstract class Vehicle
{
public int Id { get; set; }
public string Model { get; set; }
}
public class Car : Vehicle
{
public int Doors { get; set; }
}
public class ElectricCar : Car
{
public int BatteryCapacity { get; set; }
}
// TPH: 所有层级在一个表中
// TPT: 三个表(Vehicles, Cars, ElectricCars)
// TPC: 两个表(Cars, ElectricCars),Vehicle 不创建表2. 抽象基类
csharp
// 抽象基类不会被实例化
public abstract class Shape
{
public int Id { get; set; }
public string Color { get; set; }
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea() => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
// 配置
modelBuilder.Entity<Shape>()
.HasDiscriminator<string>("ShapeType")
.HasValue<Circle>("Circle")
.HasValue<Rectangle>("Rectangle");3. 接口实现
csharp
// EF Core 不支持直接映射接口
// 需要使用抽象基类或具体类
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime? ModifiedAt { get; set; }
}
// 方案: 使用抽象基类
public abstract class AuditableEntity : IAuditable
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
}
public class Product : AuditableEntity { }
public class Order : AuditableEntity { }最佳实践
✅ 推荐做法
1. 优先使用 TPH
csharp
// TPH 是默认且最高效的策略
// 除非有特殊需求,否则不要更改2. 合理使用 OfType
csharp
// 查询特定类型
var creditCards = context.PaymentMethods
.OfType<CreditCardPayment>()
.Where(cc => cc.Amount > 100)
.ToList();3. 为鉴别器创建索引
sql
-- TPH: 为鉴别器列创建索引
CREATE INDEX IX_PaymentMethods_PaymentType
ON PaymentMethods(PaymentType);❌ 避免的陷阱
1. 不要过度使用继承
csharp
// ❌ 错误: 继承层次过深
public class Entity { }
public class AuditableEntity : Entity { }
public class SoftDeleteEntity : AuditableEntity { }
public class Product : SoftDeleteEntity { }
// ✅ 正确: 使用组合
public class Product
{
public int Id { get; set; }
public AuditInfo Audit { get; set; }
public SoftDeleteInfo SoftDelete { get; set; }
}2. 不要在运行时更改鉴别器
csharp
// ❌ 错误: 不允许更改鉴别器值
var payment = context.PaymentMethods.Find(1);
payment.GetType().GetProperty("PaymentType").SetValue(payment, "Bank");
// ✅ 正确: 删除旧记录,创建新记录
context.PaymentMethods.Remove(payment);
context.PaymentMethods.Add(new BankTransferPayment { ... });
await context.SaveChangesAsync();总结
核心要点
| 策略 | 适用场景 | 性能 | 复杂度 |
|---|---|---|---|
| TPH | 大多数场景 | ⭐⭐⭐ | 低 |
| TPT | 需要规范化 | ⭐ | 中 |
| TPC | 完全独立类型 | ⭐⭐ | 中高 |
决策流程
开始
↓
继承层次是否简单? → 是 → 使用 TPH ✅
↓ 否
是否需要严格规范化? → 是 → 使用 TPT ✅
↓ 否
子类是否完全独立? → 是 → 使用 TPC ✅
↓ 否
使用 TPH(默认) ✅建议
🎯 默认选择 TPH,除非有明确理由使用其他策略
🎯 测试性能,根据实际场景调整
🎯 保持一致,项目中统一使用一种策略
🎯 文档化,记录选择的原因
继承映射是 EF Core 的强大功能,选择合适的策略可以显著提升开发效率和系统性能! 🚀