MyBatis foreach 批量插入存在事務控制難、主鍵回填難、SQL 長度受限等問題。將邏輯下沉到 Oracle 存儲過程,通過集合類型參數一次性傳入,由數據庫內部循環執行,完美解決上述問題。
-- 1. 對象類型 (對應 Java 實體屬性)
CREATE OR REPLACE TYPE unit_menu_obj AS OBJECT(
unitId VARCHAR2(32),
menuId VARCHAR2(32)
);
-- 2. 表類型 (對應 List<UnitMenu>)
CREATE OR REPLACE TYPE unit_menu_table AS TABLE OF unit_menu_obj;
CREATE OR REPLACE PROCEDURE save_unit_power(
list0 IN unit_menu_table,
result0 OUT INT
) AS
sql_bind VARCHAR2(200);
BEGIN
FOR i IN 1 .. list0.COUNT LOOP
sql_bind := 'INSERT INTO ZD_UNIT_MENU(UNIT_ID, MENU_ID) VALUES('''
|| list0(i).unitId || ''', ''' || list0(i).menuId || ''')';
EXECUTE IMMEDIATE sql_bind;
END LOOP;
COMMIT;
result0 := 1; -- 成功
EXCEPTION
WHEN OTHERS THEN
result0 := -1; -- 失敗
ROLLBACK;
END save_unit_power;
<parameterMap type="java.util.Map" id="_map">
<parameter property="list0" jdbcType="ARRAY" javaType="java.util.List"
mode="IN" typeHandler="com.zd.util.ListHandler"/>
<parameter property="result0" jdbcType="DECIMAL" javaType="java.lang.Integer" mode="OUT"/>
</parameterMap>
<select id="addUnitPower" statementType="CALLABLE" parameterMap="_map">
<![CDATA[ CALL save_unit_power(?,?) ]]>
</select>
package com.zd.util;
import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import com.zd.model.system.UnitMenu;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ListHandler extends BaseTypeHandler<List<UnitMenu>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<UnitMenu> list, JdbcType jdbcType) throws SQLException {
Connection conn = null;
try {
// 必須獲取物理連接 (取消包裝)
conn = ps.getConnection().unwrap(OracleConnection.class);
// 或 DriverManager.getConnection(...) 但需注意連接池管理
ARRAY array = createOracleArray(conn, list);
ps.setArray(i, array);
} finally {
if (conn != null && !conn.isClosed()) conn.close(); // 若手動獲取需關閉
}
}
private ARRAY createOracleArray(Connection conn, List<UnitMenu> list) throws SQLException {
// 1. 創建表類型描述符
ArrayDescriptor tableDesc = ArrayDescriptor.createDescriptor("UNIT_MENU_TABLE", conn);
// 2. 創建對象類型描述符
StructDescriptor structDesc = StructDescriptor.createDescriptor("UNIT_MENU_OBJ", conn);
// 3. 構建 STRUCT 數組
STRUCT[] structs = new STRUCT[list.size()];
for (int idx = 0; idx < list.size(); idx++) {
UnitMenu um = list.get(idx);
Object[] attrs = { um.getUnitId(), um.getMenuId() };
structs[idx] = new STRUCT(structDesc, conn, attrs);
}
// 4. 組裝 ARRAY
return new ARRAY(tableDesc, conn, structs);
}
// 查詢端不需要實現
@Override public List<UnitMenu> getNullableResult(ResultSet rs, String col) { return null; }
@Override public List<UnitMenu> getNullableResult(CallableStatement cs, int col) { return null; }
}
@Service
public class UnitMenuService {
@Autowired private UnitMenuMapper unitMenuMapper;
public int addUnitPower(String unitId, String menuTreeIds) {
List<UnitMenu> list = new ArrayList<>();
for (String menuId : menuTreeIds.split(",")) {
UnitMenu um = new UnitMenu();
um.setUnitId(unitId);
um.setMenuId(menuId);
list.add(um);
}
Map<String, Object> params = new HashMap<>();
params.put("list0", list);
params.put("result0", 0);
unitMenuMapper.addUnitPower(params);
return (Integer) params.get("result0"); // 1 成功, -1 失敗
}
}
ojdbc8.jar。orai18n.jar(STRUCT 構建時需將字符串正確轉為 Oracle 內部編碼,否則中文亂碼)。ps.getConnection().unwrap(OracleConnection.class) 獲取物理連接,避免連接池包裝類導致 createDescriptor 失效。COMMIT/ROLLBACK 管理事務,外層 Spring 事務建議標註 @Transactional(propagation=NOT_SUPPORTED) 或不加事務,避免嵌套事務衝突。| 維度 | MyBatis foreach | 存儲過程 + ARRAY |
|---|---|---|
| 事務控制 | 應用層難以精確控制 | 數據庫原子事務 |
| 主鍵回填 | 需 selectKey 複雜配置 | 過程內可 RETURNING INTO |
| SQL 長度 | 受限於 max_allowed_packet | 無長度限制 |
| 性能 | 多次網絡往返 | 單次往返,服務端批量 |
| 維護性 | SQL 分散在 XML | 邏輯集中在數據庫 |
MyBatis 通過自定義 TypeHandler 將 List<POJO> 轉為 Oracle ARRAY of OBJECT,配合 statementType="CALLABLE" 調用存儲過程,實現了高性能、強事務、易維護的批量入庫方案。核心難點在於 STRUCT/ARRAY 構建與物理連接獲取,掌握後可廣泛應用於 Oracle 批量業務場景。