Batching#

概述#

如同 JDBC Batch Updates 章節所述,將多個 SQL 語句分組執行可以減少資料庫的 round-trip 次數,進而降低交易的回應時間。Hibernate 使用 PreparedStatement 來處理自動產生的 insert、update 和 delete DML 操作,這不僅能防止 SQL injection 攻擊,也能更好地利用 JDBC batching 和 statement caching。

與原生 JDBC 不同,Hibernate 只需一個設定屬性就能啟用 batching,不需要修改任何程式碼:

<property name="hibernate.jdbc.batch_size" value="5"/>

hibernate.jdbc.batch_size 設定會全域套用到所有 Session

Session 層級的 JDBC Batching#

Hibernate 5.2 開始支援 Session-level JDBC batching,允許針對不同的業務場景自訂 batch size:

entityManager.unwrap(Session.class).setJdbcBatchSize(10);

EntityManager 使用 PersistenceContextType.EXTENDED,建議在業務方法結束前將 batch size 重設為 null,以恢復 SessionFactory 的全域設定。


Batching Insert Statements#

設定 batch size 後,Hibernate 會將多筆 insert 合併為單一 batched PreparedStatement,而非逐筆執行。

Identity 欄位與 JDBC Batching 的限制#

  • 若 entity 的主鍵使用 identity column(自動遞增),Hibernate 無法對 insert 進行 batching
  • 原因是 Persistence Context 需要在 entity 變成 managed 時立即知道其 identifier,而 identity column 只有在執行 insert 後才能取得主鍵值

此限制僅適用於 insert 語句。即使 entity 使用 identity 策略,update 和 delete 語句仍然可以受益於 JDBC batching。

父子關聯的 Insert 排序#

當同時 persist 父子 entity(例如 PostPostComment)時,即使啟用了 JDBC batching,Hibernate 仍可能逐筆執行 insert,因為:

  • JDBC batching 要求使用相同的 PreparedStatement
  • 父子 entity 的 persist 操作交錯執行,每次切換 entity 類型時 batch 就必須先 flush

解決方法是啟用 insert 排序

<property name="hibernate.order_inserts" value="true"/>

啟用後,Hibernate 會先按 entity 類型分組排序,再執行 batched insert,使同類型的 insert 語句能合併在同一個 batch 中。


Batching Update Statements#

hibernate.jdbc.batch_size 設定同樣適用於 SQL update 語句。當只更新同一類型的 entity 時,Hibernate 會產生單一 batched update statement。

不同類型 Entity 的 Update 排序#

當同時更新不同類型的 entity(例如 PostCommentPost)時,Hibernate 會在切換 entity 類型前 flush 當前的 batched PreparedStatement,導致產生多個獨立的 update 語句。

啟用 update 排序 可解決此問題:

<property name="hibernate.order_updates" value="true"/>

啟用後,Hibernate 會將相同類型的 update 語句分組,產生更少的 batched statement。

Batching Versioned Data#

  • 使用 @Version 註解的 entity 會啟用 implicit optimistic locking,Hibernate 會在 update 和 delete 的 WHERE 子句中加入版本號
  • 在 Hibernate 5 之前,versioned entity 的 JDBC batching 在 update 和 delete 時預設是關閉的
  • 自 Hibernate 5 起,hibernate.jdbc.batch_versioned_data 預設為 true
  • 當 JDBC driver 無法在 batching 時正確回傳 update count,可能會錯誤地拋出 StaleObjectStateException

對於 Hibernate 3 和 4,如果 JDBC driver 支援,應手動啟用 hibernate.jdbc.batch_versioned_data


Batching Delete Statements#

單一類型的 Delete#

當刪除同一類型的 entity 時,Hibernate 會產生單一 batched PreparedStatement

posts.forEach(entityManager::remove);
// 產生: DELETE FROM post WHERE id = ?
// Params: [(1), (2), (3)]

父子關聯的 Delete 問題#

Post entity 有 @OneToMany 關聯到 PostComment,且使用 CascadeType.REMOVE 時,刪除父 entity 會導致每筆 delete 逐一執行,無法 batching。

Hibernate 目前不支援 delete statement 的排序(不像 insert 和 update 有 order_insertsorder_updates)。

解決方案#

有三種方式改善 delete batching 效能:

  • 手動移除子 entity 後再刪除父 entity:先用 Iterator 移除所有子 entity,呼叫 flush(),再刪除父 entity,使 Persistence Context 不會交錯執行不同類型的 delete
  • 使用 bulk HQL delete:移除 orphanRemovalCascadeType.REMOVE 設定,改用 HQL 批次刪除子 entity,再以 batched PreparedStatement 刪除父 entity
  • 使用資料庫層級的 cascading:在外鍵上設定 ON DELETE CASCADE,只需刪除父 entity,資料庫會自動處理子 entity 的刪除

移除 orphanRemovalCascadeType.REMOVE 設定後,不僅 SQL 語句更有效率(得益於 batching),flushing 也更快,因為 Persistence Context 不需要逐一傳播 remove 動作。

使用 ON DELETE CASCADE 的注意事項#

  • 由於 Hibernate Session 不知道資料庫端的 cascade 刪除行為,應避免 fetch 即將被資料庫自動刪除的關聯
  • EntityDeleteAction 是 flush 操作中最後執行的動作,即使子 entity 在刪除前被修改,update 語句仍會在 delete 之前執行
  • 若在刪除後才修改已被 cascade 刪除的子 entity,會因為找不到對應的資料列而拋出 OptimisticLockException