mybatis-plus批量插入优化方式
作者:hogenlaw
mybatis-plus批量插入优化
背景
使用的mybatisplus的批量插入方法:
saveBatch(),打印 sql 日志发现,底层还是一条条的 insert 语句,这显然是不行的
优化
之前就看到过网上都在说在jdbc的url路径上加上rewriteBatchedStatements=true 参数mysql底层才能开启真正的批量插入模式。但是我已经添加了
通过查阅相关文档后,发现mybatisPlus提供了sql注入器,我们可以自定义方法来满足业务的实际开发需求。
sql 注入器官网:https://baomidou.com/guides/sql-injector/
mybatis-plus -core 核心包提供了基本的增删查改注入器,在批量插入数据这里显然不够,所以可以看到在 mybaits-plus-extension 包下还额外提供了批量插入的可注入方法
AlwaysUpdateSomeColumnById
: 根据Id更新每一个字段,全量更新不忽略null字段,解决mybatis-plus中updateById默认会自动忽略实体中null值字段不去更新的问题;InsertBatchSomeColumn
: 真实批量插入,通过单SQL的insert语句实现批量插入;Upsert
:更新or插入,根据唯一约束判断是执行更新还是删除,相当于提供insert on duplicate key update支持。
我们只需要把这个方法添加进我们的sql注入器即可。
config包新增如下两个配置
public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); //更新时自动填充的字段,不用插入值 methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); return methodList; } }
@Configuration public class MybatisPlusConfig { @Bean public MySqlInjector sqlInjector() { return new MySqlInjector(); } }
原先的 mapper 是这么写的
public interface UserMapper extends BaseMapper<User> { }
我们新增了 InsertBatchSomeColumn 方法,需要重新定义一个 BaseMapper
public interface CommonMapper<T> extends BaseMapper<T> { /** * 真正的批量插入 * @param entityList * @return */ int insertBatchSomeColumn(List<T> entityList); }
public interface UserMapper extends CommonMapper<User> { }
优化后的接口就对了,sql 显示确实是 批量插入的语句
新的问题
上面虽然实现了真正意义上的sql层面的批量插入。
但是,到这里并没有结束,mybatisPlus官方提供的 insertBatchSomeColumn 方法不支持分批插入,也就是有多少直接全部一次性插入,这就可能会导致最后的 sql 拼接语句特别长,超出了mysql 的限制, 可能会报下面这个错,
说你这个包太大了。可以通过设置 max_allowed_packet 来改变包大小。
当然我们可以通过下面的语句查询当前的配置大小:
select @@max_allowed_packet;
我这里就使用 sql 语句把值修改为 64M:
set global max_allowed_packet = 1024*1024*64;
但是改这个配置治标不治本,能不能从代码层面对拼接的 sql 语句做个优化呢,限制不要太大,于是我们还要实现一个类似于saveBatch 分批的批量插入方法。
分批插入
模仿原来的saveBatch方法:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @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> it = entityList.iterator(); it.hasNext(); ++i) { User element = it.next(); oneBatchList.add(element); if (i == idxLimit) { baseMapper.insertBatchSomeColumn(oneBatchList); //每次提交后需要清空集合数据 oneBatchList.clear(); idxLimit = Math.min(idxLimit + batchSize, size); } } } catch (Exception e) { log.error("saveBatch fail", e); return false; } return true; } }
从下面结果可以看到,最终的 sql 分成了两个批次,这样的话 sql 语句就不会太长
springboot3整合mybaits-plus
这里就简单粘贴一下pom文件,注意 用mybatis-plus-spring-boot3-starter 这个依赖,不是用 mybatis-plus-spring-boot-starter ,不然报错
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example.springbootV3</groupId> <artifactId>springbootV3</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springbootV3</name> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
spring.datasource.url=jdbc:mysql://192.168.133.128:3306/wxpay?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus.mapper-locations=classpath:/com/example/demo/**/*Mapper.xml
mybatis 插入后返回主键
如果是使用了 mybatis-plus,可以直接使用封装好的 insert 方法,通过 service直接调用
userService.save(user); Integer id = user.getId();
如果直接使用 mybatis,有下面两种方法。
- 一种是 在 insert 标签加入
useGeneratedKeys="true" keyProperty="id"
属性, - 一种是
selectKey
标签
<?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.demo.mapper.UserMapper"> <insert id="saveReturnPK1" parameterType="com.example.demo.entity.User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO `wxpay`.`t_user`(`name`, age) VALUES(#{name}, #{age}) </insert> <insert id="saveReturnPK2" parameterType="com.example.demo.entity.User"> <selectKey keyProperty="id" resultType="int" order="AFTER"> SELECT LAST_INSERT_ID() </selectKey> INSERT INTO `wxpay`.`t_user`(`name`, age) VALUES(#{name}, #{age}) </insert> </mapper>
userMapper.saveReturnPK1(user); Integer id = user.getId();
mybaits-plus 代码生成器
mybatis-plus新版本通过 builder 模式可以快速生成你想要的代码,快速且优雅,官网在这里
public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://192.168.133.128:3306/wxpay", "root", "root") .globalConfig(builder -> { builder.author("guang") // 设置作者 .enableSwagger() // 开启 swagger 模式 .outputDir("D://MP//"); // 指定输出目录 }) .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { int typeCode = metaInfo.getJdbcType().TYPE_CODE; if (typeCode == Types.SMALLINT) { // 自定义类型转换 return DbColumnType.INTEGER; } return typeRegistry.getColumnType(metaInfo); }) ) .packageConfig(builder -> builder .moduleName("com.example.demo") // 设置父包模块名 .entity("entity") .mapper("mapper") .service("service") .serviceImpl("service.impl") .xml("mapper.xml") .pathInfo(Collections.singletonMap(OutputFile.xml, "D://MP//")) // 设置mapperXml生成路径 ) .strategyConfig(builder -> builder.addInclude("t_user") // 设置需要生成的表名 .addTablePrefix("t_", "c_") // 设置过滤表前缀 .serviceBuilder().formatServiceFileName("%sService") ) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); } }
注意需要引入Freemarker 依赖,不然报错
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。