如何在MyBatis中实现批量插入?
本文讲解MyBatis批量插入的两种方法:
<foreach>标签和ExecutorType.BATCH。内容对比了它们的实现、优缺点和适用场景,并给出了技术选型建议。
在MyBatis中实现批量插入是非常常见的性能优化手段。相比于在循环中逐条插入,批量插入可以显著减少与数据库的网络交互次数,降低数据库的开销,从而大幅提升性能。
主要有两种主流的实现方式:
- 使用
<foreach>标签拼接 SQL(推荐,最常用) - 使用
ExecutorType.BATCH模式(更底层,适用于海量数据)
下面我将详细介绍这两种方法,并给出它们的优缺点和适用场景。
方法一:使用 <foreach> 标签(推荐)
这是最简单、最灵活且最常用的方法。其原理是在 XML Mapper 文件中,通过 <foreach> 标签动态生成一条包含多个 VALUES 子句的 INSERT 语句。
例如,生成如下的 SQL:
INSERT INTO user (name, age, email)
VALUES
('Alice', 25, 'alice@example.com'),
('Bob', 30, 'bob@example.com'),
('Charlie', 28, 'charlie@example.com');
实现步骤:
1. 定义实体类 (User.java)
public class User {
private Integer id;
private String name;
private Integer age;
private String email;
// 省略构造函数、getter和setter
}
2. 定义 Mapper 接口 (UserMapper.java)
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
/**
* 批量插入用户
* @param userList 用户列表
* @return 受影响的行数
*/
int insertBatch(@Param("userList") List<User> userList);
}
注意:当参数是
List或Map等集合类型时,建议使用@Param注解指定一个名称(如 "userList"),这样在 XML 中引用时会更清晰。
3. 编写 XML Mapper (UserMapper.xml)
这是最关键的一步,使用 <foreach> 标签来遍历传入的列表。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<insert id="insertBatch" parameterType="java.util.List">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
</mapper>
<foreach> 标签属性详解:
collection: 指定要遍历的集合。这里的值 "userList" 对应于 Mapper 接口方法中@Param("userList")注解的名称。如果方法只有一个 List 类型的参数且没有使用@Param,默认可以使用list。item: 遍历过程中,当前元素的别名。在循环体内部,我们通过#{user.xxx}来引用当前User对象的属性。separator: 分隔符。用于在每次循环生成的 SQL 片段之间添加指定的分隔符。这里是逗号,,用于分隔每个VALUES子句。open(可选): 整个循环内容开始前的字符串。例如,可以写open="("。close(可选): 整个循环内容结束后的字符串。例如,可以写close=")"。
如何获取自增主键?
如果你的表主键是自增长的,并且你希望在插入后获取这些新生成的主键,只需在 <insert> 标签上添加 useGeneratedKeys 和 keyProperty 属性。MyBatis 会自动将生成的主键回填到传入的 List<User> 对象的 id 属性中。
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
优点:
- 实现简单:只需编写 XML,业务代码调用与普通方法无异。
- 兼容性好:适用于绝大多数数据库(MySQL, PostgreSQL, Oracle 等)。
- 主键回填方便:对自增主键的支持非常友好。
- 原子性:生成的是一条 SQL 语句,整个操作是一个原子操作。
缺点:
- SQL 长度限制:数据库对单条 SQL 语句的长度有限制(例如 MySQL 的
max_allowed_packet参数)。如果一次插入的数据量非常巨大(如几十万条),可能会超出限制。 - 数据库驱动差异:部分数据库(如 Oracle)的批量插入语法不同,需要调整
<foreach>的写法。
方法二:使用 ExecutorType.BATCH
这种方式利用了 MyBatis 内置的 BATCH 执行器,它会调用 JDBC 的 addBatch() 和 executeBatch() 方法,实现真正的批量处理。它不是拼接一条长 SQL,而是将多条独立的 INSERT 语句打包一次性发送给数据库执行。
实现步骤:
1. XML Mapper (UserMapper.xml)
这次,我们只需要一个普通的单条插入的 SQL 语句。
<mapper namespace="com.example.mapper.UserMapper">
<!-- 这就是一个标准的单条插入语句 -->
<insert id="insertOne" parameterType="com.example.model.User">
INSERT INTO user (name, age, email)
VALUES (#{name}, #{age}, #{email})
</insert>
</mapper>
2. 编写业务层代码 (Java)
关键在于手动获取 SqlSession,并指定 ExecutorType 为 BATCH。
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void insertBatchWithExecutor(List<User> userList) {
// 1. 手动开启一个 SqlSession,并指定执行器类型为 BATCH
// BATCH 模式下,MyBatis 会缓存多条 SQL,然后在合适的时机(如 commit 或手动 flush)一次性发送给数据库
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
for (User user : userList) {
// 2. 循环调用单条插入的方法
// 此时 SQL 不会立刻执行,而是被缓存起来
userMapper.insertOne(user);
}
// 3. 提交事务,此时才会将缓存的 SQL 一次性发送到数据库执行
sqlSession.commit();
// 如果不想提交,只想刷新缓存,可以调用 sqlSession.flushStatements();
} catch (Exception e) {
// 处理异常...
}
}
}
提示:在 Spring Boot/Spring 环境中,可以直接注入
SqlSessionFactory。如果是纯 MyBatis 环境,你需要从SqlSessionFactoryBuilder创建它。
优点:
- 无 SQL 长度限制:因为它发送的是多条独立的 SQL,不存在拼接超长 SQL 的问题。
- 性能更优(海量数据):对于非常大的数据集(数十万、数百万条),通常比
<foreach>方式性能更好。 - 内存友好:在某些 JDBC 驱动实现下,可以更有效地管理内存。
缺点:
- 实现复杂:需要手动管理
SqlSession,代码相对繁琐。 - 主键回填困难:在
BATCH模式下,不同数据库驱动对获取自增主键的支持不一,通常无法像<foreach>那样方便地回填到原对象列表中。 - 返回值不直观:
commit()或flushStatements()的返回值可能不是总的受影响行数,其行为依赖于具体的 JDBC 驱动。
特别注意:MySQL 驱动的优化
当使用 ExecutorType.BATCH 模式配合 MySQL 数据库时,务必在数据库连接 URL 中添加 rewriteBatchedStatements=true 参数。
jdbc:mysql://localhost:3306/mydatabase?rewriteBatchedStatements=true
如果不加这个参数,MySQL 驱动默认会将批量操作退化为一条一条地执行,完全失去了批量优化的意义。加上后,驱动会将多条 INSERT 语句优化为一条类似 <foreach> 的多 VALUES 语句,性能天差地别。
总结与选择建议
| 特性 | <foreach> 拼接 SQL |
ExecutorType.BATCH |
|---|---|---|
| 实现复杂度 | 简单,只需修改 XML | 复杂,需要手动管理 SqlSession |
| 适用场景 | 绝大多数场景(几百到几万条) | 海量数据(几十万条以上)或需要绕过 SQL 长度限制的场景 |
| 性能 | 优秀 | 极致(尤其是海量数据) |
| SQL 长度限制 | 有 | 无 |
| 主键回填 | 支持良好 | 支持不佳,依赖驱动 |
| 代码侵入性 | 低,业务代码无感知 | 高,业务代码需要特殊处理 |
最终建议:
- 首选
<foreach>方式:对于日常开发中遇到的大部分批量插入需求(例如一次性插入几百、几千条数据),<foreach>方法足够简单、高效且易于维护。 - 当且仅当你遇到以下情况时,才考虑使用
ExecutorType.BATCH:- 一次性插入的数据量极大,导致
<foreach>拼接的 SQL 超过了数据库的max_allowed_packet限制。 - 性能压榨到极致的场景,经过压测验证
ExecutorType.BATCH确实有明显优势。 - 你不需要方便地获取批量插入后的自增主键。
- 一次性插入的数据量极大,导致