JDBC 批量插入参数 rewriteBatchedStatements 的坑

JDBC 批量插入参数 rewriteBatchedStatements 的坑

不支持多表交错插入

先看代码:

 try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
      UserRoleMapper userRoleMapper = sqlSession.getMapper(UserRoleMapper.class);
      UserProfileStudentMapper userProfileStudentMapper = sqlSession.getMapper(UserProfileStudentMapper.class);
      UserRoleScopeMapper userRoleScopeMapper = sqlSession.getMapper(UserRoleScopeMapper.class);

      // 做了简化,省略具体逻辑
      for (Map.Entry<String, Object> entry : studentGroups.entrySet()) {
          // 创建user_profile_student记录
          UserProfileStudentDAO studentProfile = new UserProfileStudentDAO();
          userProfileStudentMapper.insert(studentProfile);

          // 创建user_role记录
          UserRoleDAO userRole = new UserRoleDAO();
          userRoleMapper.insert(userRole);

          // 创建user_role_scope记录
          UserRoleScopeDAO userRoleScope = new UserRoleScopeDAO();
          userRoleScopeMapper.insert(userRoleScope);
      }
      sqlSession.commit();
  }

这段代码做的事情非常简单,针对读入的每一行数据,解析成三个记录插入三个表中。注意看开头的 SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false),这表示我们启用了批量插入,设想中驱动应该帮我们把三个表的所有 insert 语句合并成每个表一条,然后插入数据库,然而调用 Profile 工具会发现 sqlSession.commit() 执行用了非常长的时间(如下图)。

不对劲,看一下 log

==>  Preparing: INSERT INTO user_profile_student ( user_id, gender, class_name, grade ) VALUES ( ?, ?, ?, ? )
==> Parameters: 8390(Long), null, 24机械本6(String), 暂无(String)
==>  Preparing: INSERT INTO user_role_scope (user_id, role_id, college_id, department_id, create_by, update_by, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
==> Parameters: 8390(Long), 1(Long), null, null, 7(Long), 7(Long), 2026-03-17T22:07:19.580212900(LocalDateTime), 2026-03-17T22:07:19.580212900(LocalDateTime)
==>  Preparing: INSERT INTO user_role_scope ( user_id, role_id, college_id, department_id, create_by, update_by, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
==> Parameters: 8390(Long), 1(Long), null, null, 7(Long), 7(Long), 2026-03-17T22:07:19.581212600(LocalDateTime), 2026-03-17T22:07:19.581212600(LocalDateTime)
==>  Preparing: INSERT INTO user_profile_student ( user_id, gender, class_name, grade ) VALUES ( ?, ?, ?, ? )
==> Parameters: 8391(Long), null, 24数媒本1(String), 暂无(String)
==>  Preparing: INSERT INTO user_role_scope (user_id, role_id, college_id, department_id, create_by, update_by, create_time, update_time) VALUES (?, ?, NULL, NULL, ?, ?, ?, ?)
...

可以看到 user_profile_studentuser_role_scope 都出现了多次 insert。

很显然这段 log 中出现的两个表的 insert 语句都没有被合并!问题就出在驱动不能对多表交错插入的语句进行优化,尽管相同的表的插入语句都是一个格式。

解决办法也很简单,每个表分别执行 insert 即可。

try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
    UserProfileStudentMapper userProfileStudentMapper = sqlSession.getMapper(UserProfileStudentMapper.class);
    UserRoleScopeMapper userRoleScopeMapper = sqlSession.getMapper(UserRoleScopeMapper.class);
    int operationCount = 0;
    for (Map.Entry<String, Object> entry : studentGroups.entrySet()) {
        // 创建user_profile_student记录
        UserProfileStudentDAO studentProfile = new UserProfileStudentDAO();
        userProfileStudentMapper.insert(studentProfile);

        operationCount++;
        if (operationCount % 1000 == 0) {
            sqlSession.flushStatements();
        }
    }

    // 推入数据库
    sqlSession.flushStatements();
    operationCount = 0;

    for (Map.Entry<String, List<StudentEnrollmentRowData>> entry : studentGroups.entrySet()) {
        // 创建user_role记录
        UserRoleDAO userRole = new UserRoleDAO();
        userRoleMapper.insert(userRole);

        operationCount++;
        if (operationCount % 1000 == 0) {
            sqlSession.flushStatements();
        }
    }
    sqlSession.flushStatements();
    operationCount = 0;

    for (Map.Entry<String, List<StudentEnrollmentRowData>> entry : studentGroups.entrySet()) {
        // 创建user_role_scope记录
        UserRoleScopeDAO userRoleScope = new UserRoleScopeDAO();
        userRoleScopeMapper.insert(userRoleScope);

        operationCount++;
        if (operationCount % 1000 == 0) {
            sqlSession.flushStatements();
        }
    }
    // 提交事务
    sqlSession.commit();
}

max_allowed_packet 限制

可能有人注意到上面第二版代码多了:

operationCount++;
if (operationCount % 1000 == 0) {
    sqlSession.flushStatements();
}

这是因为 rewriteBatchedStatements 的原理是把成百上千条 INSERT 拼接成一条巨大的 INSERT ... VALUES (...), (...)...,而 MySQL 服务端对单条 SQL 的大小有限制(默认通常是 4MB 或 16MB),如果一次性 flush 的数据量太大(比如攒了 1 万条大字符串记录),生成的 SQL 字符串超过了服务器的 max_allowed_packet,数据库会直接报错:Packet for query is too large

所以最好是每隔一定的批量大小就 flush 一次