Skip to content

继承映射(TPH/TPT/TPC) ​

概述 ​

继承映射(Inheritance Mapping)是 EF Core 中将面向对象继承关系映射到关系数据库的技术。EF Core 支持三种继承映射策略:

  1. TPH (Table-Per-Hierarchy): 所有类型共享一个表,使用鉴别器列区分
  2. TPT (Table-Per-Type): 每个类型独立表,通过外键关联
  3. 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 查询复杂

三种策略对比 ​

特性TPHTPTTPC
表数量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 的强大功能,选择合适的策略可以显著提升开发效率和系统性能! 🚀

基于 MIT 许可发布