Mybatis 批量插入的“键”之困:当 useGeneratedKeys 遇上批量操作

张开发
2026/5/24 0:10:18 15 分钟阅读
Mybatis 批量插入的“键”之困:当 useGeneratedKeys 遇上批量操作
1. 当批量插入遇上自增主键一个常见的MyBatis陷阱第一次在项目里用MyBatis做批量插入时我信心满满地复制了单条插入的配置结果控制台突然抛出Too many keys are generated的红色错误整个人都懵了。这就像你拿着单程票想混上地铁却被闸机无情拦下——明明单条插入时运行得好好的配置怎么批量操作就翻车了呢这里的关键矛盾点在于数据库每次批量插入确实会生成多个自增ID但MyBatis的useGeneratedKeys机制默认只准备了一个口袋target object来装这些ID。想象你网购了十件商品快递小哥却把所有包裹硬塞进一个快递柜格子结果当然是灾难性的。具体到代码层面当我们同时配置了useGeneratedKeystrue启用自增主键获取keyPropertyid主键映射属性keyColumnid数据库主键列 这三个参数时MyBatis会尝试把批量生成的所有主键都塞进同一个对象的id属性里这就是报错的根本原因。2. 解剖MyBatis的主键生成机制2.1 单条插入时的工作原理在单条插入场景下useGeneratedKeys的工作流程堪称完美执行INSERT语句数据库返回生成的自增主键JDBC驱动获取这个主键值MyBatis将值注入keyProperty指定的对象属性这个过程就像单人单次的快递配送——一个包裹对应一个收货人流程清晰明确。我们来看个典型配置insert idinsertUser useGeneratedKeystrue keyPropertyid keyColumnid INSERT INTO users(name, email) VALUES(#{name}, #{email}) /insert2.2 批量插入时的机制冲突但当我们切换到批量模式情况就变得复杂了。假设执行这样的操作insert idbatchInsert useGeneratedKeystrue keyPropertyid keyColumnid INSERT INTO users(name, email) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.email}) /foreach /insert数据库会为每条记录生成独立的主键比如返回100,101,102三个ID但MyBatis默认只会尝试将这些值全部设置到同一个参数对象的id属性上。这就好比把三个不同尺寸的拼图块强行塞进同一个凹槽最终要么损坏拼图要么撑破凹槽。3. 解决方案的演进之路3.1 最直接的修复方案原始文章给出的解决方案简单粗暴——直接移除所有主键相关配置insert idbatchInsert INSERT INTO users(name, email) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.email}) /foreach /insert这种方法确实能避免报错但也意味着我们完全放弃了获取自增主键的能力。对于需要立即使用这些ID进行后续操作的业务场景比如建立关联关系这就成了致命缺陷。3.2 更优雅的解决之道经过多次实践我发现这几个方案更值得推荐方案一分批次单条插入public void batchInsert(ListUser users) { for (User user : users) { userMapper.insert(user); // 使用单条插入 } }虽然性能稍差但保证能获取每个ID适合数据量不大的场景。方案二使用批量插入单独查询先执行不带主键配置的批量插入通过特定条件查询出刚插入的数据获取这些记录的ID集合方案三预分配ID策略对于允许客户端生成ID的场景可以使用UUID或者雪花算法预先设置ID完全避开自增主键问题。4. 深度技术原理探究4.1 MyBatis与JDBC的交互细节问题的根源其实在JDBC规范层面。当调用PreparedStatement#executeBatch()时数据库会批量执行所有SQL返回的是每组操作的更新计数数组update counts但获取自增键的getGeneratedKeys()方法通常只返回最后一条记录的主键部分数据库驱动如MySQL确实支持返回多个主键但MyBatis 3.4.6之前的版本在处理这种case时存在缺陷。这就是为什么我们会看到两种不同的报错信息Too many keys are generatedMyBatis检测到键值不匹配Cant get the auto-generated keyJDBC驱动返回空值4.2 不同数据库的兼容性问题我在不同数据库上实测发现数据库批量插入返回值需要特殊配置MySQL可返回多个自增键需要连接参数useAffectedRowstruePostgreSQL只返回最后一个自增键需要RETURNING子句Oracle依赖序列和触发器必须使用SQL Server通过OUTPUT子句返回需要特殊语法这解释了为什么同样的配置在不同数据库环境可能表现迥异。5. 最佳实践与避坑指南经过多个项目的实战检验我总结出这些经验明确需求优先级如果需要立即使用自增ID → 考虑单条插入或方案二如果追求极致性能 → 使用方案三预分配ID如果只是简单存储 → 移除主键配置MyBatis版本选择3.5.0版本对批量操作的支持更好考虑使用MyBatis-Plus的批量插入方法性能优化技巧合理设置rewriteBatchedStatements参数MySQL控制单次批量操作的数据量建议500-1000条/批考虑使用ExecutorType.BATCH模式事务边界注意批量操作通常需要显式事务控制避免部分失败导致数据不一致。6. 从源码看问题本质最后我们深入MyBatis核心源码以3.5.6为例关键逻辑在Jdbc3KeyGenerator类public void processBatch(Statement stmt, ListObject parameters) { ResultSet rs null; try { rs stmt.getGeneratedKeys(); // 获取所有生成键 for (Object parameter : parameters) { if (!rs.next()) break; // 这里出现键值数量不匹配 setValue(parameter, rs); // 尝试设置值 } } finally { if (rs ! null) rs.close(); } }当批量插入10条记录但只传入了1个参数对象时循环会尝试把10个键值都设置到同一个对象上这就是报错的直接原因。新版本中可以通过Options(keyPropertylist.id)指定集合元素的属性路径算是部分解决了这个问题。在实际项目中我通常会根据数据量和业务需求灵活选择方案。比如用户注册场景可能采用方案一保证数据一致性而日志记录这种非关键数据则用纯批量插入。记住没有放之四海而皆准的解决方案理解原理才能做出最适合的技术选型。

更多文章