Why JPA and Hibernate Matter#

雖然 JDBC 提供了良好的通用 API 來隱藏資料庫廠商的通訊協定差異,但它仍有以下不足:

  • API 過於冗長,即使是簡單任務也需要大量程式碼
  • Batching 對資料存取層不透明,需要使用與非批次完全不同的 API
  • 缺乏對 explicit lockingoptimistic concurrency control 的內建支援
  • 對於 local transactions,資料存取與交易管理語意糾纏在一起
  • 擷取 joined relations 時需要額外處理,將 ResultSet 轉換為 Domain Model 或 DTO 物件圖

ORM (Object-Relational Mapping) 工具的首要目標是自動將物件狀態轉換翻譯為 SQL 語句,而本章旨在展示 Hibernate 如何解決上述 JDBC 的缺點。

Java Persistence 歷史:EJB 1.1 的 Entity Beans 因效能問題失敗;Hibernate 作為開源專案崛起,成為事實上的 Java 持久化框架。最終 Java Community Process 以 Hibernate 和 TopLink 為基礎制定了 JPA (Java Persistence API) 規範,成為標準的 Java 持久化技術。

The Impedance Mismatch#

當透過物件導向程式操作關聯式資料庫時,兩種不同的資料表示方式會開始衝突:

  • 關聯式資料庫:資料儲存在表格中,透過關聯代數定義資料關聯
  • 物件導向程式語言:物件同時擁有狀態和行為,允許雙向關聯

這兩種截然不同的方法之間的融合負擔,長久以來一直困擾著企業系統。ORM 設計模式正是用來橋接這兩種資料表示,縮小技術鴻溝:

  • 每個資料庫列對應一個 Domain Model 物件(JPA 術語中稱為 Entity
  • ORM 工具負責將 entity 狀態轉換翻譯為 DML 語句
  • 從應用開發角度看,操作 Domain Model 的關聯比直接撰寫底層 SQL 語句更為直覺

Figure 7.1: Object/Relational Mapping

JPA vs. Hibernate#

  • JPA 僅是一個規範,定義了客戶端操作的介面以及標準的 object-relational mapping metadata(Java annotations 或 XML descriptors)
  • Hibernate 在 JPA 規範發佈前就已經是完整的 Java ORM 實作,它實作了 JPA 規範,同時保留原生 API 以支援向後相容及非標準功能

如果 JPA 是介面,Hibernate 就是實作,而從效能角度來看,實作細節至關重要。

Hibernate 非標準功能#

Hibernate 提供了許多超越 JPA 規範的功能,對高效能資料存取尤為重要:

  • 擴展的 identifier generators(hi/lo, pooled, pooled-lo)
  • 透明的 prepared statement batching
  • 可自訂的 CRUD 語句(@SQLInsert, @SQLUpdate, @SQLDelete
  • 靜態/動態 entity/collection filters(@FilterDef, @Filter, @Where
  • 將屬性映射到 SQL 片段(@Formula
  • Immutable entities@Immutable
  • 更多 flush modes(FlushMode.MANUAL, FlushMode.ALWAYS
  • 透過 natural key 查詢 second-level cache
  • Entity-level cache concurrency strategies
  • 透過 HQL 進行 versioned bulk updates
  • 排除特定欄位的 optimistic locking check
  • Versionless optimistic lockingOptimisticLockType.ALL, OptimisticLockType.DIRTY
  • 支援 skip(無等待)pessimistic lock requests
  • 支援 multitenancy

JPA 的實作細節可能洩漏且影響效能。例如 FlushModeType.AUTO 對 native SQL 查詢不會觸發 flush;FetchType.EAGER 的行為取決於是透過 EntityManager 還是 JPQL 查詢載入 entity。因此理解 Hibernate 的實作行為非常重要。

Portability 的考量#

  • 追求可移植性而放棄資料庫特有功能可能影響效能
  • 實務上,遇到資料存取效能問題比需要遷移資料庫或 JPA provider 更為常見
  • 透過分層架構,切換 JPA 實作其實已經相當容易

Schema Ownership#

由於資料表示的二元性,schema 的所有權一直存在爭議。雖然理論上 Domain Model 和資料庫都能驅動 schema 演進,但實務上 schema 歸屬於資料庫

原因在於:

  • 一個企業系統可能被拆分為多個子系統(Web 應用、整合服務、搜尋引擎、批次處理器等),它們都需要使用底層資料庫
  • 關聯式資料庫的 concurrency model 提供強一致性保證,使其適合作為中央整合點
  • 資料庫系統支援多種 replication topologies,可擴展容量

Figure 7.2: Database-centric integration

對於超大型企業系統,資料分散在不同提供者(關聯式資料庫、快取、Hadoop、Spark)時,可使用 Apache Kafka 等 distributed commit log 作為整合點。

Figure 7.3: Distributed commit log integration

Schema 遷移#

  • 無論選擇哪種架構風格,都需要將瞬態的 Domain Model 與底層持久化資料對應
  • 資料 schema 隨企業系統演進,兩種 schema 表示必須保持一致
  • 即使資料存取框架可以自動生成 schema,資料庫 schema 仍必須以增量方式遷移,且所有變更需在 VCS 中可追蹤
  • 工具如 Flyway 可以自動化資料庫 schema 遷移

Entity State Transitions#

JPA 將開發者的思維從 SQL 語句轉移到 entity 狀態轉換。Entity 可處於以下四種狀態:

狀態說明
New (Transient)新建的 entity,尚未映射到任何資料庫列。一旦變為 managed,Persistence Context 會在 flush 時發出 INSERT
Managed (Persistent)與資料庫列關聯,由當前 Persistence Context 管理。狀態變更由 dirty checking 機制偵測,在 flush 時傳播為 UPDATE
Detached當 Persistence Context 關閉後,先前的 managed entities 變為 detached。後續變更不再被追蹤,不會自動同步到資料庫
RemovedEntity 被排程刪除,實際的 DELETE 語句在 Persistence Context flush 時執行

狀態轉換操作#

  • JPApersist(entity) / merge(entity) / remove(entity) / detach(entity) / clear() / close()

Figure 7.4: JPA entity state transitions

  • Hibernate 額外提供:save(entity) / update(entity) / delete(entity)

Figure 7.5: Hibernate entity state transitions

  • 從資料庫載入 entity 到 Managed 狀態:find() / getReference() / createQuery().getResultList()

Persistence Context 捕獲 entity 狀態變更,並在 flushing 時將其翻譯為 SQL 語句。JPA 的 EntityManager 和 Hibernate 的 Session 是操作 Persistence Context 的閘道介面。

Figure 7.6: Persistence Context

Write-based Optimizations#

SQL Injection 防護#

  • JPA 工具管理 SQL 語句生成,降低 SQL injection 風險
  • Hibernate 專門使用 PreparedStatement(s),不僅防範 SQL injection,還能利用 server-side 和 client-side statement caching

自動生成 DML 語句#

  • JPA provider 自動生成 INSERT 和 UPDATE 語句,能輕鬆適應資料庫表結構變更
  • 更新 entity model schema 後,Hibernate 會自動調整對應的修改語句
  • Hibernate 也允許自訂所有 CRUD 語句,由開發者自行維護

Write-behind Cache#

Persistence Context 作為交易性的 write-behind cache 運作,將 entity 狀態 flushing 延遲到最後一刻:

  • 所有修改都在記憶體中累積,在 flush 時一次性寫入資料庫
  • 這種機制可以縮短資料庫鎖的持有時間,從而提升並發效能

快取會帶來一致性挑戰。Persistence Context 在執行任何 JPQL 或 native SQL 查詢前需要進行 flush,否則可能破壞 read-your-own-write 一致性保證。值得注意的是,Hibernate 在執行 native query 時不會自動 flush pending changes,開發者必須明確指示需要同步的資料庫表。

Transparent Statement Batching#

  • 由於所有變更在同一時間 flush,Hibernate 可以將 JDBC 語句進行 batching
  • Batch updates 可以在資料存取邏輯實作完成後透明地啟用
  • 只需一個配置,Hibernate 就能以批次方式執行所有 prepared statements

Application-level Concurrency Control#

  • 在跨多個 web request 的 long conversation 中,資料庫 isolation level 無法防止 lost updates
  • JPA 同時支援 optimistic lockingpessimistic locking
  • Optimistic locking 透過 version 欄位實現 happens-before 事件排序,可透過 Extended Persistence Contexts 或 detached entities 在多個請求間維護舊 entity snapshots
  • Pessimistic locking 在使用較低交易隔離等級時很實用;Hibernate 原生 API 還支援鎖定逾時和跳過已取得的鎖

Read-based Optimizations#

JDBC 的 ResultSet 是底層擷取資料的表格式表示,而 Domain Model 是 entity graph 結構,資料存取層必須將扁平的 ResultSet 轉換為階層式結構。

  • 使用 JDBC 時,開發者必須手動解析關聯並建構階層式 entity 結構
  • 使用 JPA 時,object-relation transformation 會根據 entity schema 自動完成

資料庫不能被抽象化,不能假裝 entities 像其他普通物件一樣操作。在讀取資料時,impedance mismatch 更加明顯,基於效能考量,必須時刻留意每次 fetching 操作背後的 SQL 語句。

Fetching 策略#

  • Entity 關聯可以用物件關係表示,fetching 行為可以硬編碼在 entity schema 中,但這通常是很糟糕的做法
  • 擷取多個 one-to-many 或 many-to-many 關聯可能產生 Cartesian Product,嚴重影響效能
  • Fetching strategy 應該以每個查詢為基準來設定,而非固定在 entity schema 層級

使用 Projection 而非完整 Entity#

  • 並非所有讀取操作都需要載入完整的 entity 及其所有關聯
  • 對於唯讀視圖,使用 custom projection(僅選取需要的欄位)通常更合適
  • 原則:當邏輯交易需要修改資料時才載入 entity;唯讀場景優先使用 projection

Second-level Cache#

  • First-level cache(Persistence Context)的生命週期綁定在邏輯交易上,不能被多個並發交易共享
  • Second-level cache 關聯到 EntityManagerFactory,所有 Persistence Contexts 都能存取,可儲存 entities、entity associations 和 query results
  • JPA 未強制要求 second-level cache,Hibernate 預設停用(相對於 EclipseLink 預設啟用)

Second-level cache 可以緩解 entity fetching 效能問題,但 caching 本質上是一致性和效能之間的取捨。在分散式環境中,需要 distributed caching 實作,而網路開銷可能仍然存在。

Wrap-up#

  • 橋接物件導向和關聯式兩種高度特化的技術始終是困難的問題
  • Object-relational impedance mismatch 不可避免,ORM 模式旨在縮小差距但無法完全消除
  • 所有通訊最終都流經 JDBC,所有執行都發生在資料庫引擎中
  • 高效能的企業應用必須與底層資料庫系統共鳴,ORM 工具不應破壞這層關係
  • Hibernate 是一個非常複雜的框架,需要對資料庫系統、JDBC 和框架本身都有深入理解