Inheritance#

Java 大量使用繼承與多型,允許定義提供共同介面的不同實作類別階層。然而,關聯式資料庫並未原生支援繼承,因此將物件導向的繼承對應到資料庫表是最明顯的 object-relational impedance mismatch 之一。

Martin Fowler 在 Patterns of Enterprise Application Architecture 中定義了三種將繼承對應到關聯式資料庫的方式,JPA 規範也對應提供了三種策略:

  • InheritanceType.SINGLE_TABLE — Single Table Inheritance
  • InheritanceType.JOINED — Class Table Inheritance (Join Table)
  • InheritanceType.TABLE_PER_CLASS — Concrete Table Inheritance (Table-per-class)

此外,JPA 也支援 @MappedSuperclass,讓繼承只存在於 Domain Model 而不對應到資料庫。

本章以一個論壇系統為範例:Board 為根 entity,Topic 為基礎類別,PostAnnouncement 繼承自 TopicTopicStatistics 則透過 @OneToOne 關聯到 Topic

Figure 11.1: Domain Model Inheritance

Single Table#

Single table inheritance 是 JPA 的預設策略,將整個繼承階層放入單一資料庫表。

  • Topic entity 上標註 @Inheritance@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  • 子類別 PostAnnouncement 不需要額外的 mapping annotation,只需 Java 繼承語法
  • 資料庫表包含所有子類別的欄位,並透過 DTYPE discriminator column 區分實體類型

Figure 11.2: Single table

多型查詢#

  • Topic entity 執行查詢時,Hibernate 會掃描單一表格,並根據 DTYPE 欄位將每一列對應到正確的子類別(PostAnnouncement
  • @OneToMany 基礎類別的關聯也能自動解析子類別型別
  • 載入 TopicStatistics 並 join fetch Topic 時,Hibernate 透過 INNER JOIN 即可建構正確的子類別物件參考

效能與資料完整性#

  • 讀寫效能佳:只使用單一表格,即使搭配 @ManyToOne@OneToOne 等關聯也只需一次 join
  • 缺點:子類別專屬欄位無法設定 NOT NULL constraint,因為所有子類別共用同一張表,其他子類別的欄位必然為 null,這違反了 Consistency 原則

Data Integrity Constraints#

由於 single table 策略無法直接使用 NOT NULL,可透過以下方式補強資料完整性:

  • CHECK constraint(支援 Oracle、SQL Server、PostgreSQL、MySQL 8.0.16+):根據 DTYPE 值判斷特定欄位是否不為 null
  • Database trigger(適用於較舊版本的 MySQL):在 BEFORE INSERTBEFORE UPDATE trigger 中驗證子類別專屬欄位
  • 應用層驗證:使用 Bean Validation(@NotNull)或 JPA callback methods(@PrePersist@PreUpdate)及 @EntityListeners

CHECK constraint 和 trigger 雖然較為冗長,但能在資料庫層面確保 single table inheritance 的資料完整性,是非常有用的防護機制。

Join Table#

Join table inheritanceInheritanceType.JOINED)將每個類別對應到獨立的表格,子類別表透過 foreign key 參照基礎類別表的 primary key。

  • Topic entity 上標註 @Inheritance(strategy = InheritanceType.JOINED)
  • 子類別可使用 @PrimaryKeyJoinColumn 定義 foreign key 欄位,預設使用子類別表的 primary key 同時作為 foreign key
  • 基礎類別資訊存於 topic 表,子類別專屬內容分別存於 postannouncement

Figure 11.3: Join table

多型查詢#

  • 查詢 Topic 時,Hibernate 必須 LEFT OUTER JOIN 所有子類別表,並使用 CASE WHEN 判斷實際類型
  • 載入 TopicStatistics 及其 Topic 關聯時,需要 join 四張表(topic_statisticstopicpostannouncement
  • Eager fetch Boardtopics 集合時,也需要三個 join

效能考量#

  • 優點:子類別欄位可設定 NOT NULL constraint,資料完整性較佳
  • 寫入較慢:每個子類別 entity 需要兩條 INSERT 語句(基礎表 + 子類別表),index 記憶體佔用也較高
  • 讀取較慢:多型查詢需要 N+1 個 join(N 為子類別數量),子類別越多,查詢效能越差

Table-per-class#

Table-per-classInheritanceType.TABLE_PER_CLASS)讓每個子類別表都包含基礎類別的所有欄位,各表之間沒有 foreign key 關聯。

  • Topic entity 上標註 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
  • postannouncement 表各自儲存完整的 Topic 屬性加上自身的專屬欄位
  • topic 表僅存放不屬於任何子類別的純 Topic 記錄

Figure 11.4: Table-per-class

多型查詢#

  • 查詢 Topic 時,Hibernate 使用 UNION ALL 合併基礎表和所有子類別表的結果
  • 載入 TopicStatisticsTopic 關聯同樣需要 UNION ALL
  • Eager fetch Boardtopics 集合時,需要 join UNION ALL 子查詢的結果

效能考量#

  • 寫入較快:每個子類別只需一條 INSERT 語句
  • 讀取受限:多型查詢需要 UNION ALL,子類別越多效能越差
  • Identity generator 不可用:不同子類別表的列可能共享相同 identifier,在多型 @ManyToOne@OneToOne 關聯中會產生衝突

Table-per-class 在處理多型查詢和關聯時效能最差。若每個子類別都儲存在獨立表格中,@MappedSuperclass 通常是更好的替代方案。

Mapped Superclass#

若基礎類別不需要作為獨立的 entity(例如不需要多型查詢),可使用 @MappedSuperclassTopic 設為 abstract 類別,讓繼承只存在於 Java 層而不反映到資料庫。

Figure 11.5: Mapped superclass class diagram

  • Topic 標註 @MappedSuperclass 而非 @Entity
  • 基礎類別屬性會被合併到子類別表中(類似 table-per-class,但基礎類別不對應任何表)
  • 因為 Topic 不是 entity,無法使用多型查詢Board 也無法定義 @OneToMany Topic 集合
  • 原本的 TopicStatistics 必須拆分為 PostStatisticsAnnouncementStatistics,各自關聯到對應的子類別

Figure 11.6: Mapped superclass

效能考量#

  • 讀寫效能最佳:寫入只需一條 INSERT,讀取只需查詢單一表格
  • 代價:放棄多型查詢與基礎類別的 entity 關聯能力

繼承策略最佳實踐#

Domain Model inheritance 是強大的概念,但應謹慎使用,僅在效益大於取捨時才採用。

策略寫入效能讀取效能資料完整性多型查詢
Single Table最佳最佳需額外 CHECK/Trigger支援
Join Table較慢(多條 INSERT)較慢(多 JOIN)佳(可用 NOT NULL)支援
Table-per-class佳(單條 INSERT)差(UNION ALL)支援但效能差
Mapped Superclass最佳最佳不支援
  • Single table:讀寫效能最佳,適合子類別數量少且資料庫支援 trigger/CHECK constraint 的場景
  • Join table:適合子類別數量少但各自有大量專屬欄位的場景;子類別數量增多時多型查詢效能顯著下降
  • Table-per-class:多型查詢效能最差,若不需多型查詢建議改用 @MappedSuperclass
  • Mapped superclass:讀寫最高效但犧牲多型能力,適合子類別完全獨立運作的場景