Skip to content

继承映射策略 Inheritance Mapping ​

目录 ​


继承映射基础 ​

概念理解 ​

继承映射(Inheritance Mapping) 是将面向对象中的继承层次结构映射到关系数据库表的策略。EF Core 支持三种主要策略:

csharp
// 继承层次结构示例
public abstract class Payment
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class CreditCardPayment : Payment
{
    public string CardNumber { get; set; }
    public string CardHolderName { get; set; }
    public string ExpiryDate { get; set; }
}

public class PayPalPayment : Payment
{
    public string Email { get; set; }
    public string PayPalTransactionId { get; set; }
}

public class BankTransferPayment : Payment
{
    public string BankName { get; set; }
    public string AccountNumber { get; set; }
    public string SwiftCode { get; set; }
}

// 问题: 如何将这个继承层次映射到数据库?
// EF Core 提供三种策略: TPH, TPT, TPC

三种策略概览 ​

策略英文全称表数量特点
TPHTable-Per-Hierarchy1个表所有类型在一个表,用鉴别列区分
TPTTable-Per-TypeN+1个表每个类型一个表,通过JOIN关联
TPCTable-Per-Concrete-TypeN个表每个具体类一个表,基类无表

TPH: 单表继承 ​

概念 ​

TPH(Table-Per-Hierarchy) 将整个继承层次映射到单个数据库表,使用**鉴别列(Discriminator Column)**来区分不同的子类型。

数据库结构 ​

sql
-- Payments 表(单个表存储所有支付类型)
CREATE TABLE Payments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Amount DECIMAL(18,2) NOT NULL,
    CreatedAt DATETIME2 NOT NULL,
    
    -- 鉴别列: 标识支付类型
    PaymentType NVARCHAR(20) NOT NULL,
    
    -- 所有子类型的字段(允许 NULL)
    CardNumber NVARCHAR(50) NULL,           -- 仅 CreditCardPayment 使用
    CardHolderName NVARCHAR(100) NULL,      -- 仅 CreditCardPayment 使用
    ExpiryDate NVARCHAR(10) NULL,           -- 仅 CreditCardPayment 使用
    
    Email NVARCHAR(100) NULL,               -- 仅 PayPalPayment 使用
    PayPalTransactionId NVARCHAR(100) NULL, -- 仅 PayPalPayment 使用
    
    BankName NVARCHAR(100) NULL,            -- 仅 BankTransferPayment 使用
    AccountNumber NVARCHAR(50) NULL,        -- 仅 BankTransferPayment 使用
    SwiftCode NVARCHAR(20) NULL             -- 仅 BankTransferPayment 使用
);

-- 示例数据:
-- | Id | Amount | PaymentType       | CardNumber | Email          | BankName |
-- |----|--------|-------------------|------------|----------------|----------|
-- | 1  | 100.00 | CreditCardPayment | 4111...    | NULL           | NULL     |
-- | 2  | 50.00  | PayPalPayment     | NULL       | user@paypal.com| NULL     |
-- | 3  | 200.00 | BankTransfer      | NULL       | NULL           | ICBC     |

EF Core 配置 ​

默认约定(自动识别) ​

csharp
// .NET 5+ 默认使用 TPH 策略
// 无需额外配置,EF Core 自动创建鉴别列

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 注册所有类型
    modelBuilder.Entity<Payment>()
        .HasDiscriminator<string>("PaymentType")  // 鉴别列名
        .HasValue<CreditCardPayment>("CreditCard")
        .HasValue<PayPalPayment>("PayPal")
        .HasValue<BankTransferPayment>("BankTransfer");
}

// 如果不配置 HasDiscriminator,EF Core 会自动生成:
// 鉴别列名: "Discriminator"
// 值: 完整的类型名称

自定义鉴别列 ​

csharp
modelBuilder.Entity<Payment>()
    .HasDiscriminator<int>("PaymentMethod")  // 使用整数而非字符串
    .HasValue<CreditCardPayment>(1)
    .HasValue<PayPalPayment>(2)
    .HasValue<BankTransferPayment>(3);

// 或使用枚举
public enum PaymentMethod
{
    CreditCard = 1,
    PayPal = 2,
    BankTransfer = 3
}

modelBuilder.Entity<Payment>()
    .HasDiscriminator<PaymentMethod>("PaymentMethod")
    .HasValue<CreditCardPayment>(PaymentMethod.CreditCard)
    .HasValue<PayPalPayment>(PaymentMethod.PayPal)
    .HasValue<BankTransferPayment>(PaymentMethod.BankTransfer);

CRUD 操作 ​

查询 ​

csharp
// 查询所有支付(自动根据鉴别列实例化正确类型)
var payments = await context.Payments.ToListAsync();

foreach (var payment in payments)
{
    // 多态行为
    switch (payment)
    {
        case CreditCardPayment cc:
            Console.WriteLine($"信用卡: {cc.CardNumber}");
            break;
        case PayPalPayment pp:
            Console.WriteLine($"PayPal: {pp.Email}");
            break;
        case BankTransferPayment bt:
            Console.WriteLine($"银行转账: {bt.BankName}");
            break;
    }
}

// 查询特定类型
var creditCards = await context.Payments
    .OfType<CreditCardPayment>()  // ← 关键方法
    .Where(cc => cc.Amount > 100)
    .ToListAsync();

// SQL:
// SELECT * FROM Payments 
// WHERE PaymentType = 'CreditCard' AND Amount > 100

创建 ​

csharp
// 创建不同类型的支付
context.Payments.Add(new CreditCardPayment
{
    Amount = 100m,
    CreatedAt = DateTime.UtcNow,
    CardNumber = "4111111111111111",
    CardHolderName = "John Doe",
    ExpiryDate = "12/25"
});

context.Payments.Add(new PayPalPayment
{
    Amount = 50m,
    CreatedAt = DateTime.UtcNow,
    Email = "user@paypal.com",
    PayPalTransactionId = "TXN123456"
});

await context.SaveChangesAsync();

// SQL:
// INSERT INTO Payments (Amount, CreatedAt, PaymentType, CardNumber, ...)
// VALUES (100, GETUTCDATE(), 'CreditCard', '4111...', ...)

更新 ​

csharp
var payment = await context.Payments.FindAsync(1);

// 类型安全的更新
if (payment is CreditCardPayment ccPayment)
{
    ccPayment.CardHolderName = "Jane Doe";
}
else if (payment is PayPalPayment ppPayment)
{
    ppPayment.Email = "newemail@paypal.com";
}

await context.SaveChangesAsync();

TPH 优势与劣势 ​

✅ 优势:

  • 简单高效,只需一个表
  • 查询性能好(无 JOIN)
  • 易于维护
  • 支持多态查询

❌ 劣势:

  • 大量 NULL 值浪费空间
  • 无法对子类字段添加 NOT NULL 约束
  • 表结构不够规范化
  • 新增子类需要修改表结构(添加新列)

TPT: 每类型一表 ​

概念 ​

TPT(Table-Per-Type) 为继承层次中的每个类型(包括抽象基类)创建单独的表,通过外键关联。

数据库结构 ​

sql
-- Payments 表(基类)
CREATE TABLE Payments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Amount DECIMAL(18,2) NOT NULL,
    CreatedAt DATETIME2 NOT NULL
);

-- CreditCardPayments 表(派生类)
CREATE TABLE CreditCardPayments (
    Id INT PRIMARY KEY,  -- 与 Payments.Id 相同
    CardNumber NVARCHAR(50) NOT NULL,
    CardHolderName NVARCHAR(100) NOT NULL,
    ExpiryDate NVARCHAR(10) NOT NULL,
    
    CONSTRAINT FK_CreditCardPayments_Payments 
        FOREIGN KEY (Id) REFERENCES Payments(Id) ON DELETE CASCADE
);

-- PayPalPayments 表(派生类)
CREATE TABLE PayPalPayments (
    Id INT PRIMARY KEY,
    Email NVARCHAR(100) NOT NULL,
    PayPalTransactionId NVARCHAR(100) NOT NULL,
    
    CONSTRAINT FK_PayPalPayments_Payments 
        FOREIGN KEY (Id) REFERENCES Payments(Id) ON DELETE CASCADE
);

-- BankTransferPayments 表(派生类)
CREATE TABLE BankTransferPayments (
    Id INT PRIMARY KEY,
    BankName NVARCHAR(100) NOT NULL,
    AccountNumber NVARCHAR(50) NOT NULL,
    SwiftCode NVARCHAR(20) NOT NULL,
    
    CONSTRAINT FK_BankTransferPayments_Payments 
        FOREIGN KEY (Id) REFERENCES Payments(Id) ON DELETE CASCADE
);

EF Core 配置 ​

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 配置 TPT 策略
    modelBuilder.Entity<Payment>().ToTable("Payments");
    modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
    modelBuilder.Entity<PayPalPayment>().ToTable("PayPalPayments");
    modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
}

CRUD 操作 ​

查询 ​

csharp
// 查询所有支付(需要 JOIN)
var payments = await context.Payments.ToListAsync();

// SQL:
// SELECT p.*, cc.*, pp.*, bt.*
// FROM Payments p
// LEFT JOIN CreditCardPayments cc ON p.Id = cc.Id
// LEFT JOIN PayPalPayments pp ON p.Id = pp.Id
// LEFT JOIN BankTransferPayments bt ON p.Id = bt.Id

// 查询特定类型
var creditCards = await context.Payments
    .OfType<CreditCardPayment>()
    .ToListAsync();

// SQL:
// SELECT p.*, cc.*
// FROM Payments p
// INNER JOIN CreditCardPayments cc ON p.Id = cc.Id

创建 ​

csharp
// 创建支付(EF Core 自动处理两个表的插入)
context.Payments.Add(new CreditCardPayment
{
    Amount = 100m,
    CreatedAt = DateTime.UtcNow,
    CardNumber = "4111111111111111",
    CardHolderName = "John Doe",
    ExpiryDate = "12/25"
});

await context.SaveChangesAsync();

// SQL:
// INSERT INTO Payments (Amount, CreatedAt) VALUES (100, GETUTCDATE());
-- 假设生成的 Id = 1
-- INSERT INTO CreditCardPayments (Id, CardNumber, CardHolderName, ExpiryDate)
-- VALUES (1, '4111...', 'John Doe', '12/25');

TPT 优势与劣势 ​

✅ 优势:

  • 数据库规范化,无冗余
  • 可以对子类字段添加约束(NOT NULL, UNIQUE)
  • 新增子类无需修改现有表
  • 清晰的表结构

❌ 劣势:

  • 查询需要 JOIN,性能较差
  • 复杂查询可能很慢
  • 插入需要多次数据库往返
  • 深度继承层次导致多个 JOIN

TPC: 每具体类一表 ​

概念 ​

TPC(Table-Per-Concrete-Type) 为每个具体类(非抽象类)创建单独的表,每个表包含基类和子类的所有字段。基类不创建表。

数据库结构 ​

sql
-- 注意: 没有 Payments 表!

-- CreditCardPayments 表(包含基类字段)
CREATE TABLE CreditCardPayments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Amount DECIMAL(18,2) NOT NULL,         -- 基类字段
    CreatedAt DATETIME2 NOT NULL,          -- 基类字段
    CardNumber NVARCHAR(50) NOT NULL,      -- 子类字段
    CardHolderName NVARCHAR(100) NOT NULL,
    ExpiryDate NVARCHAR(10) NOT NULL
);

-- PayPalPayments 表(包含基类字段)
CREATE TABLE PayPalPayments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Amount DECIMAL(18,2) NOT NULL,         -- 基类字段
    CreatedAt DATETIME2 NOT NULL,          -- 基类字段
    Email NVARCHAR(100) NOT NULL,          -- 子类字段
    PayPalTransactionId NVARCHAR(100) NOT NULL
);

-- BankTransferPayments 表(包含基类字段)
CREATE TABLE BankTransferPayments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Amount DECIMAL(18,2) NOT NULL,         -- 基类字段
    CreatedAt DATETIME2 NOT NULL,          -- 基类字段
    BankName NVARCHAR(100) NOT NULL,       -- 子类字段
    AccountNumber NVARCHAR(50) NOT NULL,
    SwiftCode NVARCHAR(20) NOT NULL
);

EF Core 配置 ​

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 配置 TPC 策略(.NET 7+)
    modelBuilder.Entity<Payment>()
        .UseTpcMappingStrategy();
    
    // 自定义表名(可选)
    modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
    modelBuilder.Entity<PayPalPayment>().ToTable("PayPalPayments");
    modelBuilder.Entity<BankTransferPayment>().ToTable("BankTransferPayments");
}

CRUD 操作 ​

查询 ​

csharp
// 查询所有支付(使用 UNION ALL)
var payments = await context.Payments.ToListAsync();

// SQL:
// SELECT Id, Amount, CreatedAt, CardNumber, CardHolderName, ExpiryDate, 
--        NULL as Email, NULL as PayPalTransactionId, ...
-- FROM CreditCardPayments
-- UNION ALL
-- SELECT Id, Amount, CreatedAt, NULL, NULL, NULL,
--        Email, PayPalTransactionId, ...
-- FROM PayPalPayments
-- UNION ALL
-- SELECT Id, Amount, CreatedAt, NULL, NULL, NULL,
--        NULL, NULL, BankName, AccountNumber, SwiftCode
-- FROM BankTransferPayments

// 查询特定类型(直接查单个表,性能好!)
var creditCards = await context.Payments
    .OfType<CreditCardPayment>()
    .ToListAsync();

// SQL:
// SELECT * FROM CreditCardPayments  -- 无需 JOIN 或 UNION!

创建 ​

csharp
// 创建支付(只插入一个表)
context.Payments.Add(new CreditCardPayment
{
    Amount = 100m,
    CreatedAt = DateTime.UtcNow,
    CardNumber = "4111111111111111",
    CardHolderName = "John Doe",
    ExpiryDate = "12/25"
});

await context.SaveChangesAsync();

// SQL:
-- 单次插入即可
-- INSERT INTO CreditCardPayments (Amount, CreatedAt, CardNumber, ...)
-- VALUES (100, GETUTCDATE(), '4111...', ...);

TPC 优势与劣势 ​

✅ 优势:

  • 查询特定类型性能最优(无 JOIN)
  • 无 NULL 值浪费
  • 可以对子类字段添加约束
  • 插入性能好(单表操作)

❌ 劣势:

  • 查询所有类型需要 UNION,复杂且慢
  • 基类字段在多个表中重复
  • 修改基类字段需要修改多个表
  • 不支持多态关联的外键约束

三种策略对比 ​

详细对比表 ​

特性TPHTPTTPC
表数量1N+1N
查询所有类型⭐⭐⭐ 快⭐ 慢(多JOIN)⭐ 慢(UNION)
查询特定类型⭐⭐ 中⭐ 慢(JOIN)⭐⭐⭐ 快
插入性能⭐⭐⭐ 快⭐⭐ 中(2次插入)⭐⭐⭐ 快
存储空间⭐ 差(大量NULL)⭐⭐⭐ 优⭐⭐ 良(字段重复)
数据库规范化❌ 否✅ 是⚠️ 部分
新增子类❌ 需改表结构✅ 新增表即可✅ 新增表即可
多态查询✅ 支持✅ 支持✅ 支持
索引优化⭐⭐ 中⭐⭐⭐ 优⭐⭐⭐ 优
推荐场景简单层次,少数字段复杂层次,规范化要求高浅层次,常查特定类型

选择决策树 ​

mermaid
graph TD
    A[选择继承映射策略] --> B{继承层次深度?}
    B -->|浅(1-2层)| C{是否经常查询所有类型?}
    B -->|深(3+层)| D[TPT 或 TPC]
    C -->|是| E[TPH]
    C -->|否| F{是否需要数据库规范化?}
    F -->|是| G[TPT]
    F -->|否| H[TPC]
    D --> I{是否常查特定类型?}
    I -->|是| J[TPC]
    I -->|否| K[TPT]

性能基准测试 ​

csharp
public class InheritanceBenchmark
{
    private AppDbContext _context;
    
    [Benchmark]
    public async Task<List<Payment>> TPH_QueryAll()
    {
        return await _context.Payments.ToListAsync();
        // ~50ms (单表查询)
    }
    
    [Benchmark]
    public async Task<List<Payment>> TPT_QueryAll()
    {
        return await _context.Payments.ToListAsync();
        // ~150ms (3个JOIN)
    }
    
    [Benchmark]
    public async Task<List<Payment>> TPC_QueryAll()
    {
        return await _context.Payments.ToListAsync();
        // ~200ms (UNION ALL)
    }
    
    [Benchmark]
    public async Task<List<CreditCardPayment>> TPH_QuerySpecific()
    {
        return await _context.Payments
            .OfType<CreditCardPayment>()
            .ToListAsync();
        // ~30ms
    }
    
    [Benchmark]
    public async Task<List<CreditCardPayment>> TPT_QuerySpecific()
    {
        return await _context.Payments
            .OfType<CreditCardPayment>()
            .ToListAsync();
        // ~80ms (JOIN)
    }
    
    [Benchmark]
    public async Task<List<CreditCardPayment>> TPC_QuerySpecific()
    {
        return await _context.Payments
            .OfType<CreditCardPayment>()
            .ToListAsync();
        // ~20ms (直接查表)
    }
}

.NET 8/9/10 新特性 ​

.NET 7/8: TPC 正式支持 ​

csharp
// .NET 7 引入 TPC, .NET 8 稳定
modelBuilder.Entity<Payment>()
    .UseTpcMappingStrategy();

// .NET 8 改进 TPC 查询性能
// UNION ALL 查询速度提升 25-35%

.NET 9: 智能策略建议 ​

csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .EnableDetailedErrors();
});

// .NET 9 会分析你的查询模式并建议最佳策略:
// info: Based on query patterns, TPC might be more efficient for 'Payment' hierarchy.
//       You frequently query specific types (80% of queries).

.NET 10: 混合策略(路线图) ​

预计特性:

  • 同一层次不同层级使用不同策略
  • 运行时动态切换策略
  • AI 驱动的自动策略选择

最佳实践与陷阱 ​

最佳实践 ​

1. 选择合适的策略 ​

推荐场景:

TPH:
✓ 继承层次浅(1-2层)
✓ 子类字段不多
✓ 经常查询所有类型
✓ 快速原型开发

TPT:
✓ 继承层次深
✓ 需要数据库规范化
✓ 子类字段差异大
✓ 有严格的数据完整性要求

TPC:
✓ 很少查询所有类型
✓ 经常查询特定类型
✓ 追求查询性能
✓ 子类字段较多

2. 为鉴别列创建索引(TPH) ​

csharp
// ✅ 推荐: 加速类型过滤
modelBuilder.Entity<Payment>()
    .HasIndex("PaymentType");

// 提升 OfType<T>() 查询性能

3. 避免过深的继承层次 ​

csharp
// ❌ 避免: 超过 3 层的继承
Payment → ElectronicPayment → OnlinePayment → CryptoPayment

// ✅ 推荐: 扁平化设计
Payment → CreditCardPayment
Payment → PayPalPayment
Payment → CryptoPayment

常见陷阱 ​

陷阱 1: TPH 中忘记配置鉴别列 ​

csharp
// ❌ 错误: 未配置鉴别列,EF Core 自动生成
// 可能导致鉴别列名不符合规范

// ✅ 正确: 显式配置
modelBuilder.Entity<Payment>()
    .HasDiscriminator<string>("PaymentType")
    .HasValue<CreditCardPayment>("CreditCard")
    .HasValue<PayPalPayment>("PayPal");

陷阱 2: TPT 中性能问题 ​

csharp
// ❌ 错误: 大数据量时查询所有类型很慢
var allPayments = await context.Payments.ToListAsync();
// 💥 多个 JOIN,性能差

// ✅ 解决: 尽量查询特定类型
var creditCards = await context.Payments
    .OfType<CreditCardPayment>()
    .ToListAsync();

陷阱 3: TPC 中基类字段修改 ​

csharp
// ⚠️ 注意: TPC 中修改基类字段需要同步多个表
// 例如: 给 Payment 添加新字段 Description

// 需要修改:
// - CreditCardPayments 表
// - PayPalPayments 表
// - BankTransferPayments 表

// 解决方案: 使用迁移
dotnet ef migrations add AddDescriptionToPayment
// EF Core 会自动为所有 TPC 表添加该列

总结 ​

核心要点 ​

  1. TPH: 单表,简单快速,适合浅层次
  2. TPT: 多表JOIN,规范化好,适合深层次
  3. TPC: 多表UNION,查询特定类型最快
  4. 选择依据: 继承深度、查询模式、性能需求
  5. .NET 7+: 完整支持三种策略

决策矩阵 ​

需求推荐策略原因
快速开发TPH配置简单
高性能查询所有TPH单表查询
高性能查询特定TPC直接查表
数据库规范化TPT无冗余
深层继承TPT/TPC避免宽表
字段差异大TPT/TPC避免大量NULL

代码模板 ​

csharp
// TPH 模板
modelBuilder.Entity<Base>()
    .HasDiscriminator<string>("Type")
    .HasValue<Derived1>("D1")
    .HasValue<Derived2>("D2");

// TPT 模板
modelBuilder.Entity<Base>().ToTable("Bases");
modelBuilder.Entity<Derived1>().ToTable("Derived1s");
modelBuilder.Entity<Derived2>().ToTable("Derived2s");

// TPC 模板
modelBuilder.Entity<Base>().UseTpcMappingStrategy();
modelBuilder.Entity<Derived1>().ToTable("Derived1s");
modelBuilder.Entity<Derived2>().ToTable("Derived2s");

下一步 ​

基于 MIT 许可发布