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(例如 Post 和 PostComment)時,即使啟用了 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(例如 PostComment 和 Post)時,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_inserts和order_updates)。
解決方案#
有三種方式改善 delete batching 效能:
- 手動移除子 entity 後再刪除父 entity:先用 Iterator 移除所有子 entity,呼叫
flush(),再刪除父 entity,使 Persistence Context 不會交錯執行不同類型的 delete - 使用 bulk HQL delete:移除
orphanRemoval和CascadeType.REMOVE設定,改用 HQL 批次刪除子 entity,再以 batchedPreparedStatement刪除父 entity - 使用資料庫層級的 cascading:在外鍵上設定
ON DELETE CASCADE,只需刪除父 entity,資料庫會自動處理子 entity 的刪除
移除
orphanRemoval和CascadeType.REMOVE設定後,不僅 SQL 語句更有效率(得益於 batching),flushing 也更快,因為 Persistence Context 不需要逐一傳播 remove 動作。
使用 ON DELETE CASCADE 的注意事項#
- 由於 Hibernate
Session不知道資料庫端的 cascade 刪除行為,應避免 fetch 即將被資料庫自動刪除的關聯 EntityDeleteAction是 flush 操作中最後執行的動作,即使子 entity 在刪除前被修改,update 語句仍會在 delete 之前執行- 若在刪除後才修改已被 cascade 刪除的子 entity,會因為找不到對應的資料列而拋出
OptimisticLockException