# 面经手册 · 第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>
1
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')
1
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);
}
1
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();
}
1
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);
1
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>
1
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
1
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>
1
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));
        }
    }
}
1
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>
1
2
3
4
5
6
7
8
// 批量插入后,每个 User 对象的 id 都会被回填
userMapper.batchInsert(userList);
for (User user : userList) {
    System.out.println(user.getId());  // 每个都有自增 ID
}
1
2
3
4
5

注意:不同数据库对批量插入返回自增主键的支持不同,MySQL 支持,Oracle 需要用 selectKey。

# 五、多参数传递

# 1. 方式一:@Param 注解(推荐)

List<User> findByCondition(
    @Param("name") String name, 
    @Param("age") Integer age
);
1
2
3
4
<select id="findByCondition" resultType="com.example.entity.User">
    SELECT * FROM user 
    WHERE name = #{name} AND age = #{age}
</select>
1
2
3
4

# 2. 方式二:Map 传参

Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 18);
userMapper.findByCondition(params);
1
2
3
4
<select id="findByCondition" parameterType="map" 
         resultType="com.example.entity.User">
    SELECT * FROM user 
    WHERE name = #{name} AND age = #{age}
</select>
1
2
3
4
5

# 3. 方式三:JavaBean / DTO 传参

UserQuery query = new UserQuery();
query.setName("张三");
query.setAge(18);
userMapper.findByCondition(query);
1
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>
1
2
3
4
5

# 4. 方式四:索引参数(不推荐)

// 不用 @Param,MyBatis 自动命名为 param1, param2... 或 arg0, arg1...
List<User> findByCondition(String name, Integer age);
1
2
<!-- 使用 param1, param2 或 arg0, arg1 -->
<select id="findByCondition" resultType="com.example.entity.User">
    SELECT * FROM user 
    WHERE name = #{param1} AND age = #{param2}
</select>
1
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;
    }
}
1
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
1
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>
1
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(类型安全,参数多时)
   索引参数(不推荐)
1
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。