文章目录
- 前言
- 一、mysql批量插入的支持
- 二、Mybatis-Plus默认saveBatch方法解析
- 1、测试工程建立
- 2、默认批量插入saveBatch方法测试
- 3、saveBatch方法实现分析
- 三、Mybatis-plus中SQL注入器介绍
- 1.sqlInjector介绍
- 2.扩展中提供的4个可注入方法实现
- 四、通过SQL注入器实现真正的批量插入
- 1.继承DefaultSqlInjector扩展自定义的SQL注入器
- 2.将自定义的SQL注入器注入到Mybatis容器中
- 3.继承 BaseMapper 添加自定义方法
- 4.Mapper层接口继承新的CommonMapper
- 5.单元测试
- 6.insertBatchSomeColumn添加分批处理机制
- 总结
前言
批量插入是实际工作中常见的一个功能,mysql支持一条sql语句插入多条数据。但是Mybatis-Plus中默认提供的saveBatch方法并不是真正的批量插入,而是遍历实体集合每执行一次insert语句插入一条记录。相比批量插入,性能上显然会差很多。
今天谈一下,在Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。
一、mysql批量插入的支持
insert批量插入的语法支持:
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
二、Mybatis-Plus默认saveBatch方法解析
1、测试工程建立
测试的数据表:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在IDEA中配置好数据库连接,并安装好MybatisX-Generator插件,生成对应表的model、mapper、service、xml文件。
生成的文件推荐保存在工程目录下,generator目录下。先生成文件,用户根据自己的需要,再将文件移动到指定目录,这样避免出现文件覆盖。
生成实体的配置选项,这里我勾选了Lombok和Mybatis-Plus3,生成的类更加优雅。
移动生成的文件到对应目录:
由于都是生成的代码,这里就不补充代码了。
2、默认批量插入saveBatch方法测试
@Test
public void testBatchInsert() {
System.out.println("----- batch insert method test ------");
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("test");
user.setAge(13);
user.setEmail("[email protected]");
list.add(user);
}
userService.saveBatch(list);
}
执行日志:
显然,这里每次执行insert操作,都只插入了一条数据。
3、saveBatch方法实现分析
//批量保存的方法,做了分批请求处理,默认一次处理1000条数据
default boolean saveBatch(Collection<T> entityList) {
return this.saveBatch(entityList, 1000);
}
//用户也可以自己指定每批处理的请求数量
boolean saveBatch(Collection<T> entityList, int batchSize);
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]);
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) {
E element = var7.next();
consumer.accept(sqlSession, element);
//每次达到批次数,sqlSession就刷新一次,进行数据库请求,生成Id
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
}
});
}
我们将批次数设置为3,用来测试executeBatch的处理机制。
@Test
public void testBatchInsert() {
System.out.println("----- batch insert method test ------");
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("test");
user.setAge(13);
user.setEmail("[email protected]");
list.add(user);
}
//批次数设为3,用来测试
userService.saveBatch(list,3);
}
执行结果,首批提交的请求,已经生成了id,还没有提交的id为null。
(这里的提交是sql请求,而不是说的事物提交)
小结:
Mybatis-Plus中默认的批量保存方法saveBatch,底层是通过sqlSession.flushStatements()
将一个个单条插入的insert语句分批次进行提交。
相比遍历集合去调用userMapper.insert(entity),执行一次提交一次,saveBatch批量保存有一定的性能提升,但从sql层面上来说,并不算是真正的批量插入。
补充:
遍历集合单次提交的批量插入。
@Test
public void forEachInsert() {
System.out.println("forEachInsert 插入开始========");
long start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
userMapper.insert(list.get(i));
}
System.out.println("foreach 插入耗时:"+(System.currentTimeMillis()-start));
}
三、Mybatis-plus中SQL注入器介绍
SQL注入器官方文档:https://baomidou.com/pages/42ea4a/
1.sqlInjector介绍
SQL注入器sqlInjector 用于注入 ISqlInjector
接口的子类,实现自定义方法注入。
参考默认注入器 DefaultSqlInjector
。
Mybatis-plus默认可以注入的方法如下,大家也可以参考其实现自己扩展:
默认注入器DefaultSqlInjector的内容:
public class DefaultSqlInjector extends AbstractSqlInjector {
public DefaultSqlInjector() {
}
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
//注入通用的dao层接口的操作方法
return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectOne(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList());
}
}
2.扩展中提供的4个可注入方法实现
目前在mybatis-plus
的扩展插件中com.baomidou.mybatisplus.extension
,给我们额外提供了4个注入方法。
-
AlwaysUpdateSomeColumnById
根据Id更新每一个字段,全量更新不忽略null字段,解决mybatis-plus中updateById默认会自动忽略实体中null值字段不去更新的问题。 -
InsertBatchSomeColumn
真实批量插入,通过单SQL的insert语句实现批量插入 -
DeleteByIdWithFill
带自动填充的逻辑删除,比如自动填充更新时间、操作人 -
Upsert
更新or插入,根据唯一约束判断是执行更新还是删除,相当于提供insert on duplicate key update支持
insert into t_name (uid, app_id,createTime,modifyTime)
values(111, 1000000,'2017-03-07 10:19:12','2017-03-07 10:19:12')
on duplicate key update uid=111, app_id=1000000,
createTime='2017-03-07 10:19:12',modifyTime='2017-05-07 10:19:12'
mysql在存在主键冲突或者唯一键冲突的情况下,根据插入策略不同,一般有以下三种避免方法。
- insert ignore
- replace into
- insert on duplicate key update
四、通过SQL注入器实现真正的批量插入
通过SQL注入器sqlInjector 增加批量插入方法InsertBatchSomeColumn的过程如下:
1.继承DefaultSqlInjector扩展自定义的SQL注入器
代码如下:
/**
* 自定义Sql注入
*/
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
//更新时自动填充的字段,不用插入值
methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
return methodList;
}
}
2.将自定义的SQL注入器注入到Mybatis容器中
代码如下:
@Configuration
public class MybatisPlusConfig {
@Bean
public MySqlInjector sqlInjector() {
return new MySqlInjector();
}
}
3.继承 BaseMapper 添加自定义方法
public interface CommonMapper<T> extends BaseMapper<T> {
/**
* 全量插入,等价于insert
* @param entityList
* @return
*/
int insertBatchSomeColumn(List<T> entityList);
}
4.Mapper层接口继承新的CommonMapper
public interface UserMapper extends CommonMapper<User> {
}
5.单元测试
@Test
public void testBatchInsert() {
System.out.println("----- batch insert method test ------");
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("test");
user.setAge(13);
user.setEmail("[email protected]");
list.add(user);
}
userMapper.insertBatchSomeColumn(list);
}
执行结果:
可以看到已经实现单条insert语句支持数据的批量插入。
注意⚠️:
默认的insertBatchSomeColumn实现中,并没有类似saveBatch中的分配提交处理,
这就存在一个问题,如果出现一个非常大的集合,就会导致最后组装提交的insert语句的长度超过mysql的限制。
6.insertBatchSomeColumn添加分批处理机制
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private UserMapper userMapper;
/**
* 采用insertBatchSomeColumn重写saveBatch方法,保留分批处理机制
* @param entityList
* @param batchSize
* @return
*/
@Override
@Transactional(rollbackFor = {Exception.class})
public boolean saveBatch(Collection<User> entityList, int batchSize) {
try {
int size = entityList.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
//保存单批提交的数据集合
List<User> oneBatchList = new ArrayList<>();
for(Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) {
User element = var7.next();
oneBatchList.add(element);
if (i == idxLimit) {
userMapper.insertBatchSomeColumn(oneBatchList);
//每次提交后需要清空集合数据
oneBatchList.clear();
idxLimit = Math.min(idxLimit + batchSize, size);
}
}
}catch (Exception e){
log.error("saveBatch fail",e);
return false;
}
return true;
}
更好的实现是继承ServiceImpl实现类,自己扩展通用的服务实现类,在其中重写通用的saveBatch方法,这样就不用在每一个服务类中都重写一遍saveBatch方法。
单元测试:
@Test
public void testBatchInsert() {
System.out.println("----- batch insert method test ------");
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setName("test");
user.setAge(13);
user.setEmail("[email protected]");
list.add(user);
}
//批次数设为3,用来测试
userService.saveBatch(list,3);
}
执行结果:
分4次采用insert批量新增,符合我们的结果预期。
总结
本文主要介绍了Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。主要掌握如下内容:
1、了解Mybatis-Plus中SQL注入器有什么作用,如何去进行扩展。
2、默认的4个扩展方法各自的作用。
3、默认的saveBatch批量新增和通过insertBatchSomeColumn实现的批量新增的底层实现原理的区别,为什么insertBatchSomeColumn性能更好以及存在哪些弊端。
4、为insertBatchSomeColumn添加分批处理机制,避免批量插入的insert语句过长问题。