基于本文回答
0
评论

如何在MyBatis中实现批量插入?

知识点图片

本文讲解MyBatis批量插入的两种方法:<foreach>标签和ExecutorType.BATCH。内容对比了它们的实现、优缺点和适用场景,并给出了技术选型建议。

在MyBatis中实现批量插入是非常常见的性能优化手段。相比于在循环中逐条插入,批量插入可以显著减少与数据库的网络交互次数,降低数据库的开销,从而大幅提升性能。

主要有两种主流的实现方式:

  1. 使用 <foreach> 标签拼接 SQL(推荐,最常用)
  2. 使用 ExecutorType.BATCH 模式(更底层,适用于海量数据)

下面我将详细介绍这两种方法,并给出它们的优缺点和适用场景。


方法一:使用 <foreach> 标签(推荐)

这是最简单、最灵活且最常用的方法。其原理是在 XML Mapper 文件中,通过 <foreach> 标签动态生成一条包含多个 VALUES 子句的 INSERT 语句。

例如,生成如下的 SQL:

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)

java
public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String email;
    // 省略构造函数、getter和setter
}

2. 定义 Mapper 接口 (UserMapper.java)

java
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface UserMapper {
    /**
     * 批量插入用户
     * @param userList 用户列表
     * @return 受影响的行数
     */
    int insertBatch(@Param("userList") List<User> userList);
}

注意:当参数是 ListMap 等集合类型时,建议使用 @Param 注解指定一个名称(如 "userList"),这样在 XML 中引用时会更清晰。

3. 编写 XML Mapper (UserMapper.xml)
这是最关键的一步,使用 <foreach> 标签来遍历传入的列表。

xml
<?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> 标签上添加 useGeneratedKeyskeyProperty 属性。MyBatis 会自动将生成的主键回填到传入的 List<User> 对象的 id 属性中。

xml
<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 语句。

xml
<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,并指定 ExecutorTypeBATCH

java
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 参数。

plaintext
jdbc:mysql://localhost:3306/mydatabase?rewriteBatchedStatements=true

如果不加这个参数,MySQL 驱动默认会将批量操作退化为一条一条地执行,完全失去了批量优化的意义。加上后,驱动会将多条 INSERT 语句优化为一条类似 <foreach> 的多 VALUES 语句,性能天差地别。


总结与选择建议

特性 <foreach> 拼接 SQL ExecutorType.BATCH
实现复杂度 简单,只需修改 XML 复杂,需要手动管理 SqlSession
适用场景 绝大多数场景(几百到几万条) 海量数据(几十万条以上)或需要绕过 SQL 长度限制的场景
性能 优秀 极致(尤其是海量数据)
SQL 长度限制
主键回填 支持良好 支持不佳,依赖驱动
代码侵入性 低,业务代码无感知 高,业务代码需要特殊处理

最终建议:

  • 首选 <foreach> 方式:对于日常开发中遇到的大部分批量插入需求(例如一次性插入几百、几千条数据),<foreach> 方法足够简单、高效且易于维护。
  • 当且仅当你遇到以下情况时,才考虑使用 ExecutorType.BATCH
    1. 一次性插入的数据量极大,导致 <foreach> 拼接的 SQL 超过了数据库的 max_allowed_packet 限制。
    2. 性能压榨到极致的场景,经过压测验证 ExecutorType.BATCH 确实有明显优势。
    3. 你不需要方便地获取批量插入后的自增主键。
右滑查看面试常问