大量資料操作 (插入/更新/刪除) 時,批次處理可將多條 SQL 合併為單次網路往返,效能提升可達數十倍甚至上百倍。實例:刪除 5 萬筆資料,無批次需 30 分鐘,啟用批次 < 1 分鐘。
// 關鍵:關閉自動提交
conn.setAutoCommit(false);
PreparedStatement stmt = conn.prepareStatement("INSERT INTO employees VALUES (?, ?)");
for (Employee e : employees) {
stmt.setInt(1, e.getId());
stmt.setString(2, e.getName());
stmt.addBatch(); // 累積到批次
}
int[] updateCounts = stmt.executeBatch(); // 單次發送執行
conn.commit();
核心原理:PreparedStatement 內部維護命令列表,executeBatch() 一次性提交給資料庫執行。
SqlMapClient client = ...;
client.startBatch(); // 開啟批次模式
for (int i = 0; i < 2000; i++) {
client.delete("deleteOrder", i); // 映射檔中定義的 statement
}
client.executeBatch(); // 執行批次
底層實現委託 JDBC PreparedStatement.addBatch()/executeBatch()。
sqlWrapper.startBatch();
for (int i = from; i < to; i++) {
sqlWrapper.delete(DELETE_STRG_CNTR_BL, list.get(i)); // SQL 1
sqlWrapper.delete(DELETE_STRG_CNTR, list.get(i)); // SQL 2
sqlWrapper.delete(DELETE_CNTR, list.get(i)); // SQL 3
}
sqlWrapper.execBatch();
sqlWrapper.startBatch();
for (int i = from; i < to; i++) sqlWrapper.delete(DELETE_STRG_CNTR_BL, list.get(i));
for (int i = from; i < to; i++) sqlWrapper.delete(DELETE_STRG_CNTR, list.get(i));
for (int i = from; i < to; i++) sqlWrapper.delete(DELETE_CNTR, list.get(i));
sqlWrapper.execBatch();
來源:com.ibatis.sqlmap.engine.execution.BatchExecutor
public void addBatch(RequestScope request, Connection conn, String sql, Object[] parameters) {
PreparedStatement ps = null;
// 關鍵判斷:僅比較 SQL 字串 hashCode 與長度
if (currentSql != null
&& sql.hashCode() == currentSql.hashCode()
&& sql.length() == currentSql.length()) {
// 同一 SQL:復用最後一個 PreparedStatement
int last = statementList.size() - 1;
ps = (PreparedStatement) statementList.get(last);
} else {
// 不同 SQL:建立新 PreparedStatement
ps = conn.prepareStatement(sql);
currentSql = sql;
statementList.add(ps);
}
request.getParameterMap().setParameters(request, ps, parameters);
ps.addBatch();
size++;
}
PreparedStatementORA-01000: maximum open cursors exceeded將 List<PreparedStatement> statementList 改為 Map<String, PreparedStatement> stmtMap
public void addBatch(RequestScope request, Connection conn, String sql, Object[] parameters) {
PreparedStatement ps = stmtMap.get(sql); // O(1) 查找
if (ps == null) {
ps = conn.prepareStatement(sql);
stmtMap.put(sql, ps);
}
request.getParameterMap().setParameters(request, ps, parameters);
ps.addBatch();
size++;
}
ibatis-2.3.4.726-src.zip)BatchExecutor.java (通常在 com.ibatis.sqlmap.engine.execution 套件)private List statementList; → private Map<String, PreparedStatement> stmtMap = new HashMap<>();addBatch() 方法為上述邏輯executeBatch():遍歷 stmtMap.values() 呼叫 ps.executeBatch()close():遍歷關閉所有 PreparedStatementibatis-sqlmap-xxx.jar,替換專案依賴// 現在單迴圈多表真正享受批次優勢
sqlWrapper.startBatch();
for (int i = from; i < to; i++) {
sqlWrapper.delete(DELETE_STRG_CNTR_BL, list.get(i)); // 復用 PS1
sqlWrapper.delete(DELETE_STRG_CNTR, list.get(i)); // 復用 PS2
sqlWrapper.delete(DELETE_CNTR, list.get(i)); // 復用 PS3
}
sqlWrapper.execBatch(); // 僅 3 個 PreparedStatement,各自執行 executeBatch()
executeBatch(),每次批次傳輸 N 條資料| 風險點 | 緩解措施 |
|---|---|
| 修改開源框架核心代碼 | 建立專案專用 fork、單元測試全覆蓋、文檔記錄變更原因 |
| 升級 iBatis 版本衝突 | 鎖定版本、持續整合測試、評估官方新版是否已修復 |
| SQL 字串比較效能 | HashMap O(1) 查找,無效能損耗;大量不同 SQL 場景記憶體略增 |
| 參數映射錯誤導致批次失敗 | BatchUpdateException.getUpdateCounts() 定位失敗索引、記錄完整 SQL 與參數 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 修改 iBatis 原始碼 (本文方案) | 零業務代碼變更、真正多表批次、效能最優 | 維護成本、升級風險 | 大量多表批次、效能敏感、長期維護專案 |
| 多迴圈單表 (官方支援) | 無風險、標準 API | 程式碼冗餘、迴圈開銷、難維護 | 少量表、批次小、快速交付 |
| 手工 JDBC 批次 | 完全控制、無框架限制 | 失去 iBatis 映射優勢、SQL 分散 | 極致效能、特殊邏輯 |
| 升級 MyBatis 3.x | 官方可能已優化、現代架構 | 遷移成本大、API 不相容 | 新專案、技術棧升級窗口 |
iBatis 批次處理的核心限制在於「以 SQL 字串為鍵的 PreparedStatement 快取機制」。透過將 List 改為 Map,即可實現真正的多表批次支援,將 PreparedStatement 數量從 O(N×M) 降為 O(M),徹底解決游標耗盡與效能歸零問題。此修改僅涉及 BatchExecutor 核心類約 20 行代碼變更,風險可控、收益顯著。對於深度依賴 iBatis 且面臨大量多表批次場景的專案,這是一個高性價比的技術債償還方案。