
文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载本篇技术指南以 MyBatis 面试与实战中最高频的批量操作场景为主线系统讲解 foreach 拼接 SQL、Batch Executor、LOAD DATA 三种批量插入方式的优劣与适用边界深入剖析 useGeneratedKeys 与 selectKey 获取自增主键的底层原理Jdbc3KeyGenerator 与 getGeneratedKeys并完整梳理 Param、Map、JavaBean、索引参数四种多参数传递方式及其背后的 ParamNameResolver 实现。读者学完后既能直接上手写出高性能的批量插入代码也能在面试中被追问原理时从容应答。一、前言为什么批量操作是面试与实战的必考题实际项目中批量操作是绕不开的话题。单条 INSERT 循环插入 1000 条数据性能慢到让人怀疑人生。MyBatis 提供了多种批量操作方式多参数传递也有多种手段。面试不仅问有几种方式还会追问为什么 Batch Executor 快、Param 的原理是什么。本文从用法到源码把批量操作和参数传递彻底讲透。配套的源码级佐证可参考仓库中的 手写 MyBatis 专栏从零实现 ORM 框架与面经手册系列中的 第34篇MyBatis 工作原理、第35篇Mapper 动态代理源码解析、第43篇MyBatis 和 Spring 整合。二、面试场景还原谢飞机小记面试继续。面试官MyBatis 批量插入有几种方式谢飞机foreach 拼接 SQL还有 Batch Executor。面试官还有吗谢飞机还有……MySQL 的 LOAD DATA面试官对。那 Batch Executor 为什么快谢飞机预编译复用面试官预编译复用是 ReuseExecutor 的特点Batch Executor 快在哪里谢飞机缓存 SQL 语句批量提交面试官那自增主键怎么获取谢飞机useGeneratedKeystrue keyPropertyid面试官原理是什么底层调用了 JDBC 的什么方法谢飞机getGeneratedKeys()面试官对。那批量插入时能返回所有自增主键吗谢飞机这个……不知道。再见ヾ(▽)可以看到从知道有几种方式到讲清楚原理中间隔着对 Executor 体系、JDBC 底层 API 和参数解析器的理解。下面逐一展开。三、批量插入三种方式1. 方式一foreach 拼接 SQL推荐使用一条 SQL 插入多行的写法!-- 推荐一条 SQL 插入多行 -- insert idbatchInsert parameterTypejava.util.List INSERT INTO user(name, age, email) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}, #{user.email}) /foreach /insert执行 SQLINSERT INTO user(name, age, email) VALUES (张三, 18, zhangsantest.com), (李四, 20, lisitest.com), (王五, 22, wangwutest.com)优点一条 SQL性能好MyBatis 层面最简单缺点SQL 长度受max_allowed_packet限制需要分批分批处理// 每批 500 条 int batchSize 500; for (int i 0; i userList.size(); i batchSize) { ListUser batch userList.subList(i, Math.min(i batchSize, userList.size())); userMapper.batchInsert(batch); }这里foreach是 MyBatis 动态 SQL 的核心标签之一其底层通过ForeachSqlNode参与动态 SQL 的组合式拼装MixedSqlNode递归调用apply()最终把多组(#{user.name}, ...)拼进同一条 SQL。动态 SQL 的完整解析机制可参考 第36篇MyBatis 动态 SQL 怎么实现的9个标签用法与 OGNL 解析原理。2. 方式二Batch Executor// 手动使用 Batch Executor SqlSession sqlSession sqlSessionFactory .openSession(ExecutorType.BATCH, false); UserMapper mapper sqlSession.getMapper(UserMapper.class); try { for (User user : userList) { mapper.insert(user); } sqlSession.commit(); // 批量提交 } finally { sqlSession.close(); }原理Batch Executor 将多个 INSERT/UPDATE/DELETE 语句缓存在 commit() 时一次性发送给数据库执行减少网络往返次数。优点真正减少网络 IO适合超大批量缺点需要手动管理 SqlSession和 Spring 整合需要特殊处理源码层面的补充认识MyBatis 的 Executor 一共有三种实现——SimpleExecutor每次创建新的 Statement、ReuseExecutor复用预编译 Statement、BatchExecutor批量执行不立即提交三者都继承自BaseExecutor通过模板方法模式定义执行骨架update()中调用抽象方法doUpdate()。预编译复用确实是 ReuseExecutor 的特点而 BatchExecutor 快在它把多条 JDBC 语句累积在ListBatchResult中直到commit()或flushStatements()时才真正批量发送显著减少了网络往返次数。关于三种 Executor 的完整对比见 第34篇 的Executor 有几种小节。3. 方式三数据库批量语法-- MySQLLOAD DATA LOAD DATA LOCAL INFILE /tmp/users.csv INTO TABLE user FIELDS TERMINATED BY , LINES TERMINATED BY \n (name, age, email);优点性能最高适合千万级数据导入缺点数据库相关不通用需要处理文件4. 三种方式对比对比项foreach 拼接Batch ExecutorLOAD DATA性能中高最高通用性好好差数据库相关代码复杂度低中高SQL 长度限制有无无Spring 整合简单需特殊处理需特殊处理推荐场景日常批量1000大批量插入数据导入关于Spring 整合需特殊处理Spring 环境中默认的 SqlSessionTemplate 使用的是ExecutorType.SIMPLE若要让整个 Spring 管理的 SqlSession 走 BATCH 模式需要在构造SqlSessionTemplate时显式指定ExecutorType.BATCH如new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH)。SqlSessionTemplate是线程安全的 SqlSession 代理每次操作通过SqlSessionInterceptor获取新建或事务绑定的 SqlSession具体原理见 第43篇。四、获取自增主键1. useGeneratedKeys 方式推荐insert idinsert parameterTypecom.example.entity.User useGeneratedKeystrue keyPropertyid INSERT INTO user(name, age, email) VALUES(#{name}, #{age}, #{email}) /insertUser user new User(); user.setName(张三); user.setAge(18); user.setEmail(zhangsantest.com); userMapper.insert(user); // 插入后自增主键自动回填到 user.id System.out.println(user.getId()); // 输出自增 ID2. selectKey 方式非自增主键!-- 先获取序列/UUID再插入 -- insert idinsert parameterTypecom.example.entity.User selectKey keyPropertyid resultTypelong orderBEFORE SELECT seq_user.nextval FROM dual !-- Oracle 序列 -- !-- 或SELECT UUID() MySQL UUID -- /selectKey INSERT INTO user(id, name, age) VALUES(#{id}, #{name}, #{age}) /insertorderBEFORE先执行 selectKey再执行 INSERT适用于序列/UUID 等需提前生成主键的场景orderAFTER先执行 INSERT再执行 selectKey自增主键场景3. 源码追踪Jdbc3KeyGenerator// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator public class Jdbc3KeyGenerator implements KeyGenerator { Override public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { // 核心调用 JDBC 的 getGeneratedKeys() ResultSet rs stmt.getGeneratedKeys(); if (rs ! null) { // 遍历结果集回填主键到参数对象 ListObject keys new ArrayList(); while (rs.next()) { keys.add(rs.getObject(1)); } // 通过 MetaObject 反射设置 keyProperty MetaObject metaObject configuration.newMetaObject(parameter); metaObject.setValue(keyProperty, keys.get(0)); } } }调用时机StatementHandler.update()执行完 INSERT 后调用keyGenerator.processAfter()。为什么要强调同一个连接插入后查询自增 ID 本质上是两条 SQL 的协作——先执行 INSERT再通过getGeneratedKeys()读取数据库生成的主键。如果这两步不在同一个数据库连接下执行就失去了事务特性返回的自增 ID 会是一个错误值如 0。这正是仓库中《手写 MyBatis》专栏 第15章返回Insert操作自增索引值 反复强调的核心必须在同一个 DB 连接下、保持事务特征才能返回正确的自增索引值。这也是Jdbc3KeyGenerator.processAfter()被设计为紧跟在 INSERT 执行之后、由同一个Statement对象调用getGeneratedKeys()的原因。4. 批量插入返回主键!-- foreach 批量插入返回每个自增 ID -- insert idbatchInsert parameterTypejava.util.List useGeneratedKeystrue keyPropertyid INSERT INTO user(name, age) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}) /foreach /insert// 批量插入后每个 User 对象的 id 都会被回填 userMapper.batchInsert(userList); for (User user : userList) { System.out.println(user.getId()); // 每个都有自增 ID }注意不同数据库对批量插入返回自增主键的支持不同MySQL 支持Oracle 需要用 selectKey。五、多参数传递1. 方式一Param 注解推荐ListUser findByCondition( Param(name) String name, Param(age) Integer age );select idfindByCondition resultTypecom.example.entity.User SELECT * FROM user WHERE name #{name} AND age #{age} /select2. 方式二Map 传参MapString, Object params new HashMap(); params.put(name, 张三); params.put(age, 18); userMapper.findByCondition(params);select idfindByCondition parameterTypemap resultTypecom.example.entity.User SELECT * FROM user WHERE name #{name} AND age #{age} /select3. 方式三JavaBean / DTO 传参UserQuery query new UserQuery(); query.setName(张三); query.setAge(18); userMapper.findByCondition(query);select idfindByCondition parameterTypecom.example.entity.UserQuery resultTypecom.example.entity.User SELECT * FROM user WHERE name #{name} AND age #{age} /select4. 方式四索引参数不推荐// 不用 ParamMyBatis 自动命名为 param1, param2... 或 arg0, arg1... ListUser findByCondition(String name, Integer age);!-- 使用 param1, param2 或 arg0, arg1 -- select idfindByCondition resultTypecom.example.entity.User SELECT * FROM user WHERE name #{param1} AND age #{param2} /select5. Param 原理// org.apache.ibatis.reflection.ParamNameResolver public class ParamNameResolver { public Object getNamedParams(Object[] args) { final int paramCount params.size(); if (args null || paramCount 0) return null; if (!hasParamAnnotation paramCount 1) { return args[0]; // 只有一个参数直接返回 } // 构建参数名 → 参数值的 Map final MapString, Object param new MapperMethod.ParamMap(); for (int i 0; i paramCount; i) { if (hasParamAnnotation) { param.put(paramAnnotations.get(i).value(), args[i]); // Param 的值 } else { param.put(param (i 1), args[i]); // param1, param2... } } return param; } }结论Param(name)的本质是把参数名和参数值放入一个 MapXML 中通过#{name}从 Map 中取值。与动态代理调用链的衔接参数转换的入口在MapperMethod.execute()——Mapper 接口方法被MapperProxy.invoke()拦截后会调用method.convertArgsToSqlCommandParam(args)完成参数转换再把转换后的参数传给sqlSession.insert()/selectOne()等。这条调用链接口方法 → 代理拦截 → 方法路由 → SqlSession 执行的完整源码解析见 第35篇。仓库《手写 MyBatis》专栏的 第10章使用策略模式调用参数处理器 也复现了这一过程MapperMethod#execute将原始Object[] args先转换再传递给SqlSession单个参数直接返回多个参数则按注解名或paramN键构建参数 Map。6. 四种方式对比方式优点缺点推荐场景Param简单直观类型安全参数多时长方法签名参数 5 个Map灵活不修改接口无类型检查易出错动态条件查询JavaBean/DTO类型安全可复用需定义类参数多或复用场景索引参数无需注解可读性差易出错不推荐六、foreach 的注意事项1. max_allowed_packet 限制MySQL 的max_allowed_packet限制了单条 SQL 的最大长度默认 4MB。foreach 拼接的 SQL 过长会报错Packet for query is too large (XXX 4194304). You can change this value on the server by setting max_allowed_packet解决分批插入每批 500-1000 条。2. 性能优化!-- 关闭二级缓存避免缓存膨胀 -- insert idbatchInsert parameterTypejava.util.List flushCachetrue INSERT INTO user(name, age) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}) /foreach /insert七、常见面试追问Q1Batch Executor 和 foreach 批量插入哪个快取决于数据量数据量小 1000foreach 一条 SQL 更快数据量大 1000Batch Executor 减少网络 IO更快超大数据 10000LOAD DATA 或分批 foreachQ2批量插入时怎么返回所有自增主键MySQL 中 foreach 批量插入 useGeneratedKeystrue可以返回所有自增 ID。MyBatis 会调用PreparedStatement.getGeneratedKeys()获取所有生成的主键。从源码结构看Jdbc3KeyGenerator.processAfter()中会遍历整个ResultSet将每个主键依次回填到 List 中的各个参数对象这正是批量插入也能逐条回填主键的实现基础。Q3Param 和不加 Param 有什么区别不加Param时MyBatis 用param1, param2...或arg0, arg1...命名参数XML 中必须用这些名字。加了Param(name)后可以用#{name}引用可读性更好。八、总结记住三个核心要点 1. 批量插入三种方式 foreach 拼接一条 SQL简单注意 max_allowed_packet Batch Executor减少网络 IO需手动管理 SqlSession LOAD DATA性能最高适合数据导入 2. 自增主键获取 useGeneratedKeystrue keyPropertyid推荐 底层Jdbc3KeyGenerator.processAfter() → stmt.getGeneratedKeys() 批量插入也能返回所有自增 IDMySQL 支持 3. 多参数传递四种方式 Param 注解推荐参数少时 Map 传参灵活但无类型检查 JavaBean/DTO类型安全参数多时 索引参数不推荐面试回答模板MyBatis 批量插入有三种方式foreach 拼接 SQL一条 SQL 插入多行简单但要注意 max_allowed_packet 限制、Batch Executor减少网络 IO适合大批量、数据库批量语法如 LOAD DATA性能最高适合数据导入。获取自增主键用 useGeneratedKeystrue keyPropertyid底层是 Jdbc3KeyGenerator 调用 JDBC 的 getGeneratedKeys() 获取自增 ID 并反射回填到参数对象。批量插入时 MySQL 也支持返回所有自增主键。多参数传递推荐用 Param 注解原理是 ParamNameResolver 把参数名和值放入 MapXML 中通过 #{name} 从 Map 取值。参数多时推荐用 JavaBean/DTO。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐Luigi 任务编排模式实战指南批量触发、周期调度、回填、批处理与原子写入Luigi 任务编排模式实战指南批量触发、周期调度、回填、批处理与原子写入 本指南以 doc/luigi_patterns.rst https://link.任务调度工作流自动化批处理后端MyBatis-Plus批量插入返回值问题解析与解决方案MyBatis Plus批量插入返回值问题解析与解决方案 问题背景 在使用MyBatis Plus 3.5.7版本时开发者发现 Db.saveBatch 方法后端ORM代码生成MyBatis-Plus批量插入返回值异常问题分析与解决方案MyBatis Plus批量插入返回值异常问题分析与解决方案 问题背景 在使用MyBatis Plus 3.5.7版本时开发者发现调用 Db.saveBatc后端ORM代码生成上一篇使用 Rube MCP 自动化 The Odds APIawesome-codex-skills 实战指南下一篇ncmdumpGUI终极指南5分钟学会网易云音乐NCM格式转换创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考