# 面经手册 · 第42篇《MyBatis 批量插入有几种方式?自增主键怎么获取?多参数怎么传?》
作者:小傅哥
博客:https://bugstack.cn (opens new window)
沉淀、分享、成长,让自己和他人都能有所收获!😄
# 一、前言
实际项目中,批量操作是绕不开的话题。单条 INSERT 循环插入 1000 条数据,性能慢到让人怀疑人生。
MyBatis 提供了多种批量操作方式,多参数传递也有多种手段。面试不仅问"有几种方式",还会追问"为什么 Batch Executor 快"、"@Param 的原理是什么"。
# 二、面试题
谢飞机,小记!,面试继续。
面试官:MyBatis 批量插入有几种方式?
谢飞机:foreach 拼接 SQL,还有 Batch Executor。
面试官:还有吗?
谢飞机:还有……MySQL 的 LOAD DATA?
面试官:对。那 Batch Executor 为什么快?
谢飞机:预编译复用?
面试官:预编译复用是 ReuseExecutor 的特点,Batch Executor 快在哪里?
谢飞机:缓存 SQL 语句,批量提交?
面试官:那自增主键怎么获取?
谢飞机:useGeneratedKeys="true" keyProperty="id"
面试官:原理是什么?底层调用了 JDBC 的什么方法?
谢飞机:getGeneratedKeys()?
面试官:对。那批量插入时,能返回所有自增主键吗?
谢飞机:这个……不知道。再见!ヾ( ̄▽ ̄)
# 三、批量插入三种方式
# 1. 方式一:foreach 拼接 SQL
<!-- 推荐:一条 SQL 插入多行 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user(name, age, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
2
3
4
5
6
7
执行 SQL:
INSERT INTO user(name, age, email)
VALUES ('张三', 18, 'zhangsan@test.com'),
('李四', 20, 'lisi@test.com'),
('王五', 22, 'wangwu@test.com')
2
3
4
优点:一条 SQL,性能好,MyBatis 层面最简单
缺点:SQL 长度受 max_allowed_packet 限制,需要分批
分批处理:
// 每批 500 条
int batchSize = 500;
for (int i = 0; i < userList.size(); i += batchSize) {
List<User> batch = userList.subList(i,
Math.min(i + batchSize, userList.size()));
userMapper.batchInsert(batch);
}
2
3
4
5
6
7
# 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();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
原理:Batch Executor 将多个 INSERT/UPDATE/DELETE 语句缓存,在 commit() 时一次性发送给数据库执行,减少网络往返次数。
优点:真正减少网络 IO,适合超大批量 缺点:需要手动管理 SqlSession,和 Spring 整合需要特殊处理
# 3. 方式三:数据库批量语法
-- MySQL:LOAD DATA
LOAD DATA LOCAL INFILE '/tmp/users.csv'
INTO TABLE user
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(name, age, email);
2
3
4
5
6
优点:性能最高,适合千万级数据导入 缺点:数据库相关,不通用,需要处理文件
# 4. 三种方式对比
| 对比项 | foreach 拼接 | Batch Executor | LOAD DATA |
|---|---|---|---|
| 性能 | 中 | 高 | 最高 |
| 通用性 | 好 | 好 | 差(数据库相关) |
| 代码复杂度 | 低 | 中 | 高 |
| SQL 长度限制 | 有 | 无 | 无 |
| Spring 整合 | 简单 | 需特殊处理 | 需特殊处理 |
| 推荐场景 | 日常批量(<1000) | 大批量插入 | 数据导入 |
# 四、获取自增主键
# 1. useGeneratedKeys 方式(推荐)
<insert id="insert" parameterType="com.example.entity.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, age, email) VALUES(#{name}, #{age}, #{email})
</insert>
2
3
4
User user = new User();
user.setName("张三");
user.setAge(18);
user.setEmail("zhangsan@test.com");
userMapper.insert(user);
// 插入后,自增主键自动回填到 user.id
System.out.println(user.getId()); // 输出:自增 ID
2
3
4
5
6
7
8
# 2. selectKey 方式(非自增主键)
<!-- 先获取序列/UUID,再插入 -->
<insert id="insert" parameterType="com.example.entity.User">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT seq_user.nextval FROM dual <!-- Oracle 序列 -->
<!-- 或:SELECT UUID() MySQL UUID -->
</selectKey>
INSERT INTO user(id, name, age) VALUES(#{id}, #{name}, #{age})
</insert>
2
3
4
5
6
7
8
order="BEFORE":先执行 selectKey,再执行 INSERT
order="AFTER":先执行 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) {
// 遍历结果集,回填主键到参数对象
List<Object> keys = new ArrayList<>();
while (rs.next()) {
keys.add(rs.getObject(1));
}
// 通过 MetaObject 反射设置 keyProperty
MetaObject metaObject = configuration.newMetaObject(parameter);
metaObject.setValue(keyProperty, keys.get(0));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
调用时机:StatementHandler.update() 执行完 INSERT 后,调用 keyGenerator.processAfter()。
# 4. 批量插入返回主键
<!-- foreach 批量插入,返回每个自增 ID -->
<insert id="batchInsert" parameterType="java.util.List"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
2
3
4
5
6
7
8
// 批量插入后,每个 User 对象的 id 都会被回填
userMapper.batchInsert(userList);
for (User user : userList) {
System.out.println(user.getId()); // 每个都有自增 ID
}
2
3
4
5
注意:不同数据库对批量插入返回自增主键的支持不同,MySQL 支持,Oracle 需要用 selectKey。
# 五、多参数传递
# 1. 方式一:@Param 注解(推荐)
List<User> findByCondition(
@Param("name") String name,
@Param("age") Integer age
);
2
3
4
<select id="findByCondition" resultType="com.example.entity.User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
2
3
4
# 2. 方式二:Map 传参
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 18);
userMapper.findByCondition(params);
2
3
4
<select id="findByCondition" parameterType="map"
resultType="com.example.entity.User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
2
3
4
5
# 3. 方式三:JavaBean / DTO 传参
UserQuery query = new UserQuery();
query.setName("张三");
query.setAge(18);
userMapper.findByCondition(query);
2
3
4
<select id="findByCondition" parameterType="com.example.entity.UserQuery"
resultType="com.example.entity.User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
2
3
4
5
# 4. 方式四:索引参数(不推荐)
// 不用 @Param,MyBatis 自动命名为 param1, param2... 或 arg0, arg1...
List<User> findByCondition(String name, Integer age);
2
<!-- 使用 param1, param2 或 arg0, arg1 -->
<select id="findByCondition" resultType="com.example.entity.User">
SELECT * FROM user
WHERE name = #{param1} AND age = #{param2}
</select>
2
3
4
5
# 5. @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 Map<String, 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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
结论:@Param("name") 的本质是把参数名和参数值放入一个 Map,XML 中通过 #{name} 从 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
2
解决:分批插入,每批 500-1000 条。
# 2. 性能优化
<!-- 关闭二级缓存,避免缓存膨胀 -->
<insert id="batchInsert" parameterType="java.util.List" flushCache="true">
INSERT INTO user(name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
2
3
4
5
6
7
# 七、常见面试追问
# Q1:Batch Executor 和 foreach 批量插入哪个快?
取决于数据量:
- 数据量小(< 1000):foreach 一条 SQL 更快
- 数据量大(> 1000):Batch Executor 减少网络 IO,更快
- 超大数据(> 10000):LOAD DATA 或分批 foreach
# Q2:批量插入时怎么返回所有自增主键?
MySQL 中 foreach 批量插入 + useGeneratedKeys="true" 可以返回所有自增 ID。MyBatis 会调用 PreparedStatement.getGeneratedKeys() 获取所有生成的主键。
# Q3:@Param 和不加 @Param 有什么区别?
不加 @Param 时,MyBatis 用 param1, param2... 或 arg0, arg1... 命名参数,XML 中必须用这些名字。加了 @Param("name") 后,可以用 #{name} 引用,可读性更好。
# 八、总结
记住三个核心要点:
1. 批量插入三种方式
foreach 拼接:一条 SQL,简单,注意 max_allowed_packet
Batch Executor:减少网络 IO,需手动管理 SqlSession
LOAD DATA:性能最高,适合数据导入
2. 自增主键获取
useGeneratedKeys="true" keyProperty="id"(推荐)
底层:Jdbc3KeyGenerator.processAfter() → stmt.getGeneratedKeys()
批量插入也能返回所有自增 ID(MySQL 支持)
3. 多参数传递四种方式
@Param 注解(推荐,参数少时)
Map 传参(灵活但无类型检查)
JavaBean/DTO(类型安全,参数多时)
索引参数(不推荐)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
面试回答模板:
MyBatis 批量插入有三种方式:foreach 拼接 SQL(一条 SQL 插入多行,简单但要注意 max_allowed_packet 限制)、Batch Executor(减少网络 IO,适合大批量)、数据库批量语法如 LOAD DATA(性能最高,适合数据导入)。
获取自增主键用 useGeneratedKeys="true" keyProperty="id",底层是 Jdbc3KeyGenerator 调用 JDBC 的 getGeneratedKeys() 获取自增 ID 并反射回填到参数对象。批量插入时 MySQL 也支持返回所有自增主键。
多参数传递推荐用 @Param 注解,原理是 ParamNameResolver 把参数名和值放入 Map,XML 中通过 #{name} 从 Map 取值。参数多时推荐用 JavaBean/DTO。

