Relationships#

在關聯式資料庫中,表之間的關聯透過 foreign key 建立。每個資料庫關聯都建構在 foreign key 之上,產生三種基本的 table relationship 類型:

  • one-to-many:最常見的關聯,將 parent table 的一行對應到 child table 的多行
  • one-to-one:child table 的 foreign key 帶有 uniqueness constraint,確保一個 parent 只對應一個 child
  • many-to-many:需要一個 junction table(link table)包含兩個 foreign key 分別參考兩個 parent table

Foreign key 是建構 table relationship 最重要的元素。在關聯式資料庫中,child-side 控制著 table relationship,因為 foreign key 僅存在於 child-side。

Relationship Types#

JPA 定義了五種 association mapping 來對應 table relationship:

  • @ManyToOne:對應 one-to-many table relationship 的 child-side(foreign key 所在處)
  • @OneToMany:對應 one-to-many table relationship 的 parent-side
  • @ElementCollection:定義 entity 與多個 value type(basic 或 embeddable)之間的 one-to-many 關聯
  • @OneToOne:用於 one-to-one table relationship 的雙方
  • @ManyToMany:對應 many-to-many table relationship

Figure 10.1: Table relationships

Mapping Collections 的注意事項#

雖然 @OneToMany@ManyToMany@ElementCollection 從資料存取角度很方便(可以將 entity state transition 從 parent 向 child 傳播),但它們並非零成本:

  • Collection mapping 會將 parent entity 綁定到一個查詢,通常會抓取所有關聯的 child records
  • 當 child records 數量增長過大時,抓取整個 collection 可能成為效能瓶頸
  • 每個 collection mapping 都可以用 data access query 替代,提供更靈活的查詢條件

在對 child records 數量沒有清楚了解之前,最好延後 collection mapping 的決定。對高效能系統而言,data access query 通常是更靈活的替代方案。

@ManyToOne#

@ManyToOne 是最常見的 JPA association,精確對應 one-to-many table relationship。使用 @ManyToOne 時,foreign key 始終由 child-side 控制,無論 association 是 unidirectional 還是 bidirectional。

Figure 10.2: The one-to-many table relationship

  • 透過 @JoinColumn 指定 foreign key column 名稱
  • Hibernate 將 @ManyToOne 的 object reference 內部狀態翻譯為 foreign key column 值
  • 設定 reference 時自動產生正確的 INSERT,設定為 null 則更新為 NULL

@ManyToOne association 直接控制 foreign key,因此自動產生的 DML statements 非常高效。最佳效能的 JPA association 始終依賴 child-side 來將 JPA state 翻譯為 foreign key column value。

Figure 10.3: @ManyToOne relationship

@OneToMany#

@OneToMany association 可以對應 one-to-many 的 parent-side,但只有作為 bidirectional mapping 時才能正確映射 one-to-many table relationship 語義。Unidirectional @OneToMany 會使用額外的 junction table。

Bidirectional @OneToMany#

Bidirectional @OneToMany 搭配對應的 @ManyToOne child-side mapping,是最高效的做法:

  • 必須定義 mappedBy 屬性,指向 child-side 的 @ManyToOne 屬性
  • 可搭配 CascadeType.ALLorphanRemoval = true 來傳播 entity state transition
  • 需要提供 parent-side 的 helper methodsaddComment / removeComment)來保持雙方同步
  • Entity state transition 可從 parent 向 child 傳播:persist parent 即可自動 persist 所有 child entities

Bidirectional @OneToMany 產生高效的 DML statements,因為 @ManyToOne mapping 負責控制 table relationship。當 child records 數量較少時,值得考慮使用此 association。

Figure 10.4: Bidirectional @OneToMany relationship

Unidirectional @OneToMany#

Unidirectional @OneToMany 看似簡單,但效率遠低於 bidirectional 版本:

  • 因為沒有 @ManyToOne side 控制 foreign key,Hibernate 使用額外的 junction table 管理關聯
  • 需要 join 三個 table(而非兩個),增加 index memory footprint
  • 刪除操作極為低效:Hibernate 會先刪除所有 junction table rows,再重新插入剩餘的 in-memory records

Figure 10.5: The @OneToMany table relationship

Ordered Unidirectional @OneToMany#

加上 @OrderColumn 可以儲存每個 collection element 的 index:

  • 從 collection 尾端移除元素時較為高效(單一 DELETE)
  • 從 collection 頭部移除元素時,需要額外的 UPDATE 來重新排列 index 順序
  • @OrderColumn@OrderBy 不同:前者將 index 持久化到資料庫 column,後者在 runtime 用 ORDER BY 排序

Figure 10.6: The unidirectional @OneToMany with an @OrderColumn

@OneToMany with @JoinColumn#

JPA 2.0 支援在 @OneToMany 上使用 @JoinColumn,直接控制 child table 的 foreign key,不需要 junction table:

  • 新增 child 時,Hibernate 先 INSERT(foreign key 為 null),再發出額外的 UPDATE 設定 foreign key
  • 刪除 child 時,同樣產生多餘的 UPDATE(先將 foreign key 設為 null,再 DELETE)
  • 無論是 unidirectional 還是 bidirectional,都會產生多餘的 UPDATE statements

不管是哪種 unidirectional @OneToMany 變體,都會產生冗餘的 UPDATE statements。最高效的 foreign key mapping 策略仍是 @ManyToOne association。

Unidirectional @OneToMany Set#

使用 Set 取代 List 可以改善 unidirectional @OneToMany 的效率:

  • 使用 junction table 時,remove 操作更高效(不需要重建整個 collection)
  • 使用 @JoinColumn 時,INSERT 仍會產生額外的 UPDATE,但 DELETE 有所改善
  • 然而,即使使用 Set,效率仍然不如 bidirectional @OneToMany association

@ElementCollection#

@ElementCollection 用於映射 basic type(如 Stringint)或 embeddable type 的 collection,行為類似 unidirectional @OneToMany

  • Value type 的生命週期完全繼承自 parent entity
  • 資料庫層面是一個 child table,包含 foreign key column 和 collection element value

Figure 10.7: The @ElementCollection table relationship

@ElementCollection List#

使用 List 時,@ElementCollection 被視為 bag

  • 刪除任何一個元素時,Hibernate 會先刪除所有關聯的 child records,再重新插入剩餘的 records
  • 這種行為與 unidirectional @OneToMany 的 bag 行為相同,非常低效

@ElementCollection Set#

使用 Set 時,效率明顯改善:

  • 刪除操作只產生單一 DELETE statement,精確刪除目標 record
  • 相比 ListSet 是使用 @ElementCollection 時的更好選擇

即使 SetList 更高效,最好還是用 bidirectional @OneToMany 或單純的 @ManyToOne 來替代 @ElementCollection。這樣可以在不需要透過 parent entity 抓取整個 collection 的情況下刪除元素。

Figure 10.8: The @ElementCollection relationship

@OneToOne#

從資料庫角度看,one-to-one association 基於帶有 uniqueness constraint 的 foreign key,確保一個 parent row 最多只被一個 child record 參考。

Figure 10.9: The one-to-one table relationship

Unidirectional @OneToOne#

  • 類似 unidirectional @ManyToOne,透過 @JoinColumn 控制 foreign key
  • @MapsId 是推薦做法:讓 child entity 的 primary key 同時作為 foreign key 參考 parent 的 primary key(shared key)
  • 使用 shared key 的好處:
    • 減少 child table index 的 memory footprint(一個 indexed column 而非兩個)
    • 可直接透過 entityManager.find() 用 parent 的 ID 查詢 child entity
    • 支援 second-level cache 直接查詢,避免額外的 database hit

Figure 10.10: The unidirectional @OneToOne relationship

Figure 10.11: The shared key one-to-one

Bidirectional @OneToOne#

Bidirectional @OneToOne 讓 parent entity 也能映射 child-side:

  • Parent-side 定義 mappedBy 屬性
  • Child-side 仍負責控制 foreign key(可搭配 @MapsId
  • 效能隱患:即使設定為 FetchType.LAZY,Hibernate 在載入 parent entity 時仍會發出額外的 secondary query 來判斷 child reference 是 null 還是 proxy

Parent-side 的 @OneToOne 即使設定 fetch = FetchType.LAZY,Hibernate 仍然會產生 secondary SELECT。這是因為 Persistence Context 需要同時知道 entity type 和 identifier。Bytecode enhancement 搭配 @LazyToOne(LazyToOneOption.NO_PROXY) 是目前唯一有效的 workaround。通常更推薦使用 unidirectional @OneToOne

Figure 10.12: The bidirectional @OneToOne relationship

@ManyToMany#

@ManyToMany 對應 many-to-many table relationship,需要一個 junction table 包含兩個 foreign key。

Figure 10.13: The many-to-many table relationship

Unidirectional @ManyToMany List#

  • 使用 List 時,@ManyToMany 的行為等同於 bag
  • 刪除一個 junction record 時,Hibernate 會先刪除所有關聯的 junction rows,再重新插入剩餘的
  • 應避免使用 CascadeType.REMOVE,因為兩端是獨立的 entity,刪除一端不應連帶刪除另一端

Unidirectional @ManyToMany Set#

  • 使用 Set 時,刪除操作只產生單一 DELETE statement
  • Set@ManyToMany association 的最佳 collection type

Figure 10.14: The unidirectional @ManyToMany relationship

Bidirectional @ManyToMany#

  • 需要選擇一個 owning side 和一個 mappedBy side
  • 使用 Set collection type 最高效
  • 需要定義 helper methods(addTag / removeTag)保持雙方同步
  • Junction table 扮演 hidden child-side 角色

@ManyToMany association,無論是 unidirectional 還是 bidirectional,使用 Set 都比 List 高效得多。同時應避免 CascadeType.REMOVEorphanRemoval,只使用 CascadeType.PERSISTCascadeType.MERGE

Figure 10.15: The bidirectional @ManyToMany relationship

The @OneToMany Alternative#

可以用兩個 bidirectional @OneToMany association 取代 @ManyToMany,透過將 junction table 映射為一個獨立的 entity:

  • 建立 PostTag entity 映射 junction table,使用 @EmbeddedId 組合 primary key
  • PostTag 包含兩個 @ManyToOne association,搭配 @MapsId 指向各自的 parent
  • PostTag 各自定義 bidirectional @OneToMany 指向 PostTag
  • 優勢:可在 junction table 中新增額外的 column(如 created_oncreated_by),這是標準 @ManyToMany 做不到的
  • 刪除操作只需單一 DELETE statement,精確移除目標 junction record

Figure 10.16: The @OneToMany as a many-to-many table relationship

Hypersistence Optimizer#

選擇正確的 association mapping 對設計高效能資料存取層至關重要,但光選對 mapping 還不夠,還需要檢查 Hibernate configuration properties 和執行的 queries。

Hypersistence Optimizer 是作者開發的自動化工具,可以:

  • 自動驗證 JPA 和 Hibernate 的 mapping 與 configuration
  • 在測試階段偵測效能影響問題
  • 透過 assert 偵測到的 issue 數量,在 CI build 中觸發失敗,確保未來的變更不會影響效能