繼承映射核心問題
面向對象的繼承關係如何映射到關係型數據庫?Hibernate 提供三種標準策略,各有優劣,選型需根據查詢性能、數據完整性、表結構清晰度權衡。
領域模型示例
// 父類
@Entity @Inheritance(strategy = InheritanceType.XXX)
public abstract class Payment {
@Id @GeneratedValue Long id;
BigDecimal amount;
// getter/setter
}
// 子類
@Entity public class CreditCardPayment extends Payment {
String cardNumber; String cardType;
}
@Entity public class CashPayment extends Payment {
String currency;
}
@Entity public class ChequePayment extends Payment {
String chequeNumber; String bankName;
}
策略一:單表策略 — SINGLE_TABLE (默認)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "PAYMENT_TYPE", discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue("CREDIT")
public class CreditCardPayment extends Payment { ... }
表結構
PAYMENT_TABLE
+----+-------------+--------+------------+-----------+------------+--------------+
| ID | PAYMENT_TYPE| AMOUNT | CARD_NUMBER| CARD_TYPE | CURRENCY | CHEQUE_NUMBER|
+----+-------------+--------+------------+-----------+------------+--------------+
| 1 | CREDIT | 100.00 | 1234-5678 | VISA | NULL | NULL |
| 2 | CASH | 50.00 | NULL | NULL | USD | NULL |
| 3 | CHEQUE | 200.00 | NULL | NULL | NULL | CHK-001 |
+----+-------------+--------+------------+-----------+------------+--------------+
優缺點
| 優點 | 缺點 |
| 查詢最快:無 JOIN,單表掃描 |
子類特有欄位不能加 NOT NULL 約束 (需 nullable=true) |
| 插入/更新性能最優 |
表列數隨子類增長膨脹,稀疏列浪費空間 |
| 結構簡單,易維護 |
違反第三範式,數據完整性依賴應用層 |
適用場景
- 繼承層級淺、子類少、字段差異小
- 高頻多態查詢 (如
from Payment)
- 讀多寫少、性能敏感
策略二:連表策略 — JOINED
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment { ... }
@Entity
@PrimaryKeyJoinColumn(name = "PAYMENT_ID") // 共享主鍵
public class CreditCardPayment extends Payment { ... }
表結構
PAYMENT (父表) CREDIT_CARD_PAYMENT (子表)
+----+--------+ +------------+------------+------------+
| ID | AMOUNT | | PAYMENT_ID | CARD_NUMBER| CARD_TYPE |
+----+--------+ +------------+------------+------------+
| 1 | 100.00 | ←1:1→ | 1 | 1234-5678 | VISA |
+----+--------+ +------------+------------+------------+
優缺點
| 優點 | 缺點 |
| 符合範式化設計,數據完整性強 |
多態查詢需 多表 JOIN,性能下降 |
| 子類欄位可加 NOT NULL |
插入需多表事務,寫入性能較差 |
| 表結構清晰,擴展性好 |
深層繼承導致 JOIN 鏈過長 |
適用場景
- 子類字段差異大、需強約束
- 較少多態查詢,多針對具體子類查詢
- 數據一致性要求極高 (金融、醫療)
策略三:每類一表 — TABLE_PER_CLASS
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment { ... }
表結構
CREDIT_CARD_PAYMENT CASH_PAYMENT CHEQUE_PAYMENT
+----+--------+--------+ +----+--------+---------+ +----+--------+------------+
| ID | AMOUNT | CARD...| | ID | AMOUNT | CURRENCY| | ID | AMOUNT | CHEQUE... |
+----+--------+--------+ +----+--------+---------+ +----+--------+------------+
優缺點
| 優點 | 缺點 |
| 無 JOIN,查詢具體類最快 |
多態查詢需 UNION ALL,極慢 |
| 每表獨立,結構最乾淨 |
主鍵生成複雜 (需共享序列/UUID) |
| 子類完全獨立演進 |
無法在父表加外鍵引用 |
適用場景
- 極少多態查詢,幾乎只操作具體子類
- 子類之間無共同業務邏輯
- 歷史系統重構、微服務拆分邊界
策略對比總覽
| 維度 | SINGLE_TABLE | JOINED | TABLE_PER_CLASS |
| 表數量 | 1 | 1 + N | N |
| 多態查詢性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 具體類查詢性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 寫入性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| NOT NULL 支持 | ❌ | ✅ | ✅ |
| 外鍵引用父類 | ✅ | ✅ | ❌ |
| 主鍵策略 | 統一 | 共享主鍵 | 需協調 |
多態查詢行為差異
// HQL 多態查詢
List<Payment> all = session.createQuery("from Payment").list();
// SINGLE_TABLE: 單表 SELECT * FROM PAYMENT WHERE PAYMENT_TYPE IN (...)
// JOINED: LEFT JOIN 子表 (或多條 SQL)
// TABLE_PER_CLASS: UNION ALL 多表合併
多態查詢時 TABLE_PER_CLASS 不支援延遲加載,需 @Polymorphism(type = PolymorphismType.EXPLICIT) 或改用 JOINED。
混合策略:JOINED + Discriminator (罕見但實用)
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "PAYMENT_TYPE")
@DiscriminatorValue("CREDIT")
@Entity
@Table(name = "CREDIT_PAYMENT")
@SecondaryTable(name = "PAYMENT", pkJoinColumns = @PrimaryKeyJoinColumn(name = "ID"))
public class CreditCardPayment extends Payment { ... }
結合 @SecondaryTable 將父類字段映射到父表,子類特有字段映射到子表,同時保留判別列供快速類型過濾。
最佳選型決策樹
開始
│
├─ 多態查詢頻繁? ──是──► 子類字段少/差異小? ──是──► SINGLE_TABLE
│ │
│ └─ 否 ──► 需 NOT NULL 約束? ──是──► JOINED
│ │
│ └─ 否 ──► SINGLE_TABLE (可接受稀疏列)
│
└─ 多態查詢極少? ──是──► 子類獨立演進? ──是──► TABLE_PER_CLASS
│
└─ 否 ──► JOINED (平衡選擇)
實戰建議
- 默認首選 SINGLE_TABLE:簡單、快、滿足 80% 場景
- 遇到 NOT NULL 需求 → JOINED:犧牲查詢性能換數據完整性
- 避免 TABLE_PER_CLASS:除非極其確定無多態查詢,否則 UNION ALL 是性能殺手
- 判別列優化:使用
@DiscriminatorValue 短字符串 (如 'CC','CA','CQ') 而非全類名
- 索引策略:SINGLE_TABLE 上為判別列建索引;JOINED 子表主鍵即外鍵自動索引
總結
Hibernate 三種繼承映射策略本質是對象模型與關係模型阻抗失配的不同妥協。沒有銀彈,只有基於業務訪問模式、數據量級、完整性要求的權衡選擇。理解各策略的 SQL 生成邏輯、優缺點邊界,才能在設計評審中做出經得起時間考驗的決策。