首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Oracle + MyBatis foreach 批量插入報錯 ORA-00933 根因與解決方案

     編輯:Java 相關知識

問題現場

使用 MyBatis `` 批量插入時,生成 SQL 如下:


INSERT INTO t_ocl_supcity (city_id, city_code, city_name, area_desc, sup_id, stat)
VALUES (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)

Oracle 報錯:ORA-00933: SQL 命令未正確結束。

根因分析

Oracle 不支援標準 SQL 的 `VALUES (...), (...), ...` 多行插入語法。 此語法屬於 MySQL、PostgreSQL、SQL Server (2008+) 支援的擴展,Oracle 需用其他方式實現批量插入。

三種 Oracle 批量插入方案對比

方案一:INSERT ALL ... SELECT FROM DUAL(推薦,單條 SQL)


<insert id="batchInsert" parameterType="java.util.List">
    INSERT ALL
    <foreach collection="list" item="item" index="index" separator="">
        INTO t_ocl_supcity (city_id, city_code, city_name, area_desc, sup_id, stat)
        VALUES (
            SEQ_OCL_SUPCITY.NEXTVAL,
            #{item.cityCode,jdbcType=VARCHAR},
            #{item.cityName,jdbcType=VARCHAR},
            #{item.areaDesc,jdbcType=VARCHAR},
            #{item.supId,jdbcType=VARCHAR},
            #{item.stat,jdbcType=VARCHAR}
        )
    </foreach>
    SELECT * FROM DUAL
</insert>
  • 單次解析、單次網絡往返、原子性提交
  • 適用於 500 條以內(超過可能超過 SQL 長度限制或解析開銷大)

方案二:UNION ALL 子查詢(原文方案,等效)


<insert id="batchInsert" parameterType="java.util.List">
    INSERT INTO t_ocl_supcity (city_id, city_code, city_name, area_desc, sup_id, stat)
    SELECT SEQ_OCL_SUPCITY.NEXTVAL, t.* FROM (
        <foreach collection="list" item="item" separator="UNION ALL">
            SELECT 
                #{item.cityCode,jdbcType=VARCHAR} city_code,
                #{item.cityName,jdbcType=VARCHAR} city_name,
                #{item.areaDesc,jdbcType=VARCHAR} area_desc,
                #{item.supId,jdbcType=VARCHAR} sup_id,
                #{item.stat,jdbcType=VARCHAR} stat
            FROM DUAL
        </foreach>
    ) t
</insert>

本質同方案一,寫法偏好差異。

方案三:PL/SQL 批量綁定(大數據量、最高效能)


<insert id="batchInsertPlsql" parameterType="java.util.List">
    DECLARE
        TYPE city_code_arr IS TABLE OF VARCHAR2(50);
        TYPE city_name_arr IS TABLE OF VARCHAR2(100);
        -- 其它欄位陣列...
        v_codes city_code_arr := city_code_arr();
        v_names city_name_arr := city_name_arr();
        -- ...
    BEGIN
        <foreach collection="list" item="item" index="index">
            v_codes.EXTEND; v_codes(#{index+1}) := #{item.cityCode,jdbcType=VARCHAR};
            v_names.EXTEND; v_names(#{index+1}) := #{item.cityName,jdbcType=VARCHAR};
            -- ...
        </foreach>
        
        FORALL i IN 1..v_codes.COUNT
            INSERT INTO t_ocl_supcity (city_id, city_code, city_name, area_desc, sup_id, stat)
            VALUES (SEQ_OCL_SUPCITY.NEXTVAL, v_codes(i), v_names(i), v_areas(i), v_sups(i), v_stats(i));
    END;
</insert>
  • 繞過 SQL 引擎,直接數組綁定,萬級以上數據首選
  • 需注意:MyBatis 需關閉 `useGeneratedKeys`,序列在 PL/SQL 內部處理

序列值獲取關鍵點

Oracle 序列 `NEXTVAL` 在單條 SQL 中每行調用一次。方案一、二中放在 `VALUES` 或 `SELECT` 列表即可。不要在 `` 中獲取,那會導致所有行使用同一序列值。

MyBatis 批量執行模式優化


// Mapper 介面
int batchInsert(@Param("list") List<SupCity> list);

// SqlSession 執行(批量模式)
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    SupCityMapper mapper = session.getMapper(SupCityMapper.class);
    for (List<SupCity> batch : Lists.partition(list, 1000)) { // 分批 1000
        mapper.batchInsert(batch);
    }
    session.commit(); // 一次性提交
}
  • ExecutorType.BATCH:JDBC 批量處理,發送 `addBatch()` / `executeBatch()`
  • 配合方案一/二,單批 500-1000 條,平衡解析開銷與網絡往返

完整最佳實踐範本


<!-- SupCityMapper.xml -->
<insert id="batchInsert" parameterType="java.util.List">
    INSERT ALL
    <foreach collection="list" item="item" separator="">
        INTO t_ocl_supcity (city_id, city_code, city_name, area_desc, sup_id, stat)
        VALUES (
            SEQ_OCL_SUPCITY.NEXTVAL,
            #{item.cityCode,jdbcType=VARCHAR},
            #{item.cityName,jdbcType=VARCHAR},
            #{item.areaDesc,jdbcType=VARCHAR},
            #{item.supId,jdbcType=VARCHAR},
            #{item.stat,jdbcType=VARCHAR}
        )
    </foreach>
    SELECT * FROM DUAL
</insert>

// Service 實現
@Transactional
public void importCities(List<SupCityDTO> dtos) {
    List<SupCity> entities = dtos.stream().map(this::toEntity).collect(toList());
    
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        SupCityMapper mapper = session.getMapper(SupCityMapper.class);
        // 分批避免單條 SQL 過大
        for (List<SupCity> batch : Lists.partition(entities, 500)) {
            mapper.batchInsert(batch);
        }
        session.commit();
    }
}

避坑清單

  1. ❌ 直接用 `VALUES (...), (...)` → Oracle 報 ORA-00933
  2. ❌ `` 獲取序列 → 所有行同一值
  3. ❌ 單次插入超過 1000 行 → SQL 解析緩慢、可能超過長度限制
  4. ✅ 使用 `INSERT ALL` 或 `UNION ALL` 子查詢
  5. ✅ 序列直接寫在 `VALUES`/`SELECT` 列表中
  6. ✅ `ExecutorType.BATCH` + 分批提交
  7. ✅ 大數據量(>5000)改用 PL/SQL `FORALL` 方案
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved