【DB系列】Mybatis之批量插入的几种姿势

文章目录
  1. I. 环境配置
    1. 1. 项目配置
    2. 2. 数据库表
  2. II. 批量插入
    1. 1. 单个插入,批量调用方式
    2. 2. BATCH批处理模式
    3. 3. foreach实现sql拼接
    4. 4. 分批BATCH模式
    5. 5. 如何选择
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 微信公众号: 一灰灰Blog

在日常的业务需求开发过程中,批量插入属于非常常见的case,在mybatis的写法中,一般有下面三种使用姿势

  • 单个插入,业务代码中for循环调用
  • <foreach>标签来拼接批量插入sql
  • 复用会话,拆分小批量插入方式

I. 环境配置

我们使用SpringBoot + Mybatis + MySql来搭建实例demo

  • springboot: 2.2.0.RELEASE
  • mysql: 5.7.22

1. 项目配置

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

核心的依赖mybatis-spring-boot-starter,至于版本选择,到mvn仓库中,找最新的

另外一个不可获取的就是db配置信息,appliaction.yml

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

2. 数据库表

用于测试的数据库

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 批量插入

1. 单个插入,批量调用方式

这种方式理解起来最简单,一个单独的插入接口,业务上循环调用即可

1
2
3
4
5
6
7
8
9
@Mapper
public interface MoneyInsertMapper {
/**
* 写入
* @param po
* @return
*/
int save(@Param("po") MoneyPo po);
}

对应的xml如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap id="BaseResultMap" type="com.git.hui.boot.mybatis.entity.MoneyPo">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="money" property="money" jdbcType="INTEGER"/>
<result column="is_deleted" property="isDeleted" jdbcType="TINYINT"/>
<result column="create_at" property="createAt" jdbcType="TIMESTAMP"/>
<result column="update_at" property="updateAt" jdbcType="TIMESTAMP"/>
</resultMap>
<insert id="save" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo" useGeneratedKeys="true" keyProperty="po.id">
INSERT INTO `money` (`name`, `money`, `is_deleted`)
VALUES
(#{po.name}, #{po.money}, #{po.isDeleted});
</insert>

使用姿势如下

1
2
3
4
5
6
7
8
9
10
11
12
13
private MoneyPo buildPo() {
MoneyPo po = new MoneyPo();
po.setName("mybatis user");
po.setMoney((long) random.nextInt(12343));
po.setIsDeleted(0);
return po;
}

public void testBatchInsert() {
for (int i = 0; i < 10; i++) {
moneyInsertMapper.save(buildPo());
}
}

小结

上面这种方式的优点就是简单直观,缺点就是db交互次数多,开销大

2. BATCH批处理模式

针对上面做一个简单的优化,使用BATCH批处理模式,实现会话复用,避免每次请求都重新维护一个链接,导致额外开销,可以如下操作

1
2
3
4
5
6
7
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
MoneyInsertMapper moneyInsertMapper = sqlSession.getMapper(MoneyInsertMapper.class);
for (int i = 0; i < 10; i++) {
moneyInsertMapper.save(buildPo());
}
sqlSession.commit();
}

说明

  • sqlSession.commit若放在for循环内,则每保存一个就提交,db中就可以查询到
  • 若如上面放在for循环外,则所有的一起提交

3. foreach实现sql拼接

另外一种直观的想法就是组装批量插入sql,这里主要是借助foreach来处理

1
2
3
4
5
6
7
<insert id="batchSave" parameterType="com.git.hui.boot.mybatis.entity.MoneyPo"  useGeneratedKeys="true" keyProperty="id">
insert ignore into `money` (`name`, `money`, `is_deleted`)
values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.name}, #{item.money}, #{item.isDeleted})
</foreach>
</insert>

对应的mapper接口如下

1
2
3
4
5
6
/**
* 批量写入
* @param list
* @return
*/
int batchSave(@Param("list") List<MoneyPo> list);

实际使用case如下

1
2
3
4
5
6
7
8
List<MoneyPo> list = new ArrayList<>();
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
list.add(buildPo());
moneyInsertMapper.batchSave(list);

小结

使用sql批量插入的方式,优点是db交互次数少,在插入数量可控时,相比于前者开销更小

缺点也很明显,当一次插入的数量太多时,组装的sql既有可能直接超过了db的限制,无法执行了

4. 分批BATCH模式

接下来的这种方式在上面的基础上进行处理,区别在于对List进行拆分,避免一次插入太多数据,其次就是真个操作复用一个会话,避免每一次的交互都重开一个会话,导致额外的开销

其使用姿势如下

1
2
3
4
5
6
7
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
MoneyInsertMapper moneyInsertMapper = sqlSession.getMapper(MoneyInsertMapper.class);
for (List<MoneyPo> subList : Lists.partition(list, 2)) {
moneyInsertMapper.batchSave(subList);
}
sqlSession.commit();
}

与第二种使用姿势差不多,区别在于结合了第三种批量的优势,对大列表进行拆分,实现复用会话 + 批量插入

5. 如何选择

上面介绍了几种不同的批量插入方式,那我们应该选择哪种呢?

就我个人的观点来讲,2,3,4这三个在一般的业务场景下并没有太大的区别,如果已知每次批量写入的数据不多(比如几十条),那么使用3就是最简单的case了

如果批量插入的数据非常多,那么方案4可能更加优雅

如果我们希望开发一个批量导数据的功能,那么方案2无疑是更好的选择

III. 不能错过的源码和相关知识点

0. 项目

系列博文:

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到