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
@ManyToOneassociation 直接控制 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.ALL和orphanRemoval = true來傳播 entity state transition - 需要提供 parent-side 的 helper methods(
addComment/removeComment)來保持雙方同步 - Entity state transition 可從 parent 向 child 傳播:persist parent 即可自動 persist 所有 child entities
Bidirectional
@OneToMany產生高效的 DML statements,因為@ManyToOnemapping 負責控制 table relationship。當 child records 數量較少時,值得考慮使用此 association。

Figure 10.4: Bidirectional @OneToMany relationship
Unidirectional @OneToMany#
Unidirectional @OneToMany 看似簡單,但效率遠低於 bidirectional 版本:
- 因為沒有
@ManyToOneside 控制 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 策略仍是@ManyToOneassociation。
Unidirectional @OneToMany Set#
使用 Set 取代 List 可以改善 unidirectional @OneToMany 的效率:
- 使用 junction table 時,remove 操作更高效(不需要重建整個 collection)
- 使用
@JoinColumn時,INSERT 仍會產生額外的 UPDATE,但 DELETE 有所改善 - 然而,即使使用
Set,效率仍然不如 bidirectional@OneToManyassociation
@ElementCollection#
@ElementCollection 用於映射 basic type(如 String、int)或 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
- 相比
List,Set是使用@ElementCollection時的更好選擇
即使
Set比List更高效,最好還是用 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是@ManyToManyassociation 的最佳 collection type

Figure 10.14: The unidirectional @ManyToMany relationship
Bidirectional @ManyToMany#
- 需要選擇一個 owning side 和一個 mappedBy side
- 使用
Setcollection type 最高效 - 需要定義 helper methods(
addTag/removeTag)保持雙方同步 - Junction table 扮演 hidden child-side 角色
對
@ManyToManyassociation,無論是 unidirectional 還是 bidirectional,使用Set都比List高效得多。同時應避免CascadeType.REMOVE和orphanRemoval,只使用CascadeType.PERSIST和CascadeType.MERGE。

Figure 10.15: The bidirectional @ManyToMany relationship
The @OneToMany Alternative#
可以用兩個 bidirectional @OneToMany association 取代 @ManyToMany,透過將 junction table 映射為一個獨立的 entity:
- 建立
PostTagentity 映射 junction table,使用@EmbeddedId組合 primary key PostTag包含兩個@ManyToOneassociation,搭配@MapsId指向各自的 parentPost和Tag各自定義 bidirectional@OneToMany指向PostTag- 優勢:可在 junction table 中新增額外的 column(如
created_on、created_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 中觸發失敗,確保未來的變更不會影響效能