MyBatis-Plus(简称MP)是一个MyBatis的增强工具,旨在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。
特点
-
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
-
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
-
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
-
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
-
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
-
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
-
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
-
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
-
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
-
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
-
内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
-
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
MP功能强大,但是上线后发现有一个问题,他内置的saveBatch批量插入功能有着巨大的隐患,查看源码发现他是这么实现的
其中关键的方法executeBatch源码为:
启动服务后,用Postman调试,后台打印如下:
从图上可以看出这个所谓的批量插入接口,其实就是一个for循环插入,所以会对数据库产生很大的压力,第一反应是手工实现,这样
INSERT INTO test (a, b, c) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.a}, #{item.b}, #{item.c})
</foreach>
但是仔细阅读源码发现,他的CRUD方法其实都是实现了一个叫做AbstractMethod的类的,类继承如图
其中我们看到有一个方法是InsertBatchSomeColumn,这个方法实际上就是我们手写的那种批量插入的功能的实现了,我们现在要做的是把这个方法注入到我们的工程中,并且替换掉之前的saveBatch方法
第一步:新增我们自己的SQL注入器,用来扩展批量插入
public class CxmSqlInjector extends DefaultSqlInjector{
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 防止父类的方法不可使用
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 添加批量插入的方法
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
其中DefaultSqlInjector的源码如下:
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
package com.baomidou.mybatisplus.core.injector;
import com.baomidou.mybatisplus.core.injector.methods.*;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import java.util.List;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
/**
* SQL 默认注入器
*
* @author hubin
* @since 2018-04-10
*/
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
.add(new Insert())
.add(new Delete())
.add(new DeleteByMap())
.add(new Update())
.add(new SelectByMap())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectMapsPage())
.add(new SelectObjs())
.add(new SelectList())
.add(new SelectPage());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}
}
通过查看DefaultSqlInjector中的getMethodList方法我们可以发现上面说的那些基础的crud接口都是在这个方法里面注册好了的,所以我们可以直接使用,这个类最终是实现了ISqlInjector接口,我们可以看下ISqlInjector源码:
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
package com.baomidou.mybatisplus.core.injector;
import org.apache.ibatis.builder.MapperBuilderAssistant;
/**
* SQL 自动注入器接口
*
* @author hubin
* @since 2016-07-24
*/
public interface ISqlInjector {
/**
* 检查SQL是否注入(已经注入过不再注入)
*
* @param builderAssistant mapper 信息
* @param mapperClass mapper 接口的 class 对象
*/
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
InsertBatchSomeColumn源码中我们可以看到是支持批量插入的:
public class InsertBatchSomeColumn extends AbstractMethod {
/**
* 字段筛选条件
*/
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;
/**
* 默认方法名
*/
public InsertBatchSomeColumn() {
super("insertBatchSomeColumn");
}
/**
* 默认方法名
*
* @param predicate 字段筛选条件
*/
public InsertBatchSomeColumn(Predicate<TableFieldInfo> predicate) {
super("insertBatchSomeColumn");
this.predicate = predicate;
}
/**
* @param name 方法名
* @param predicate 字段筛选条件
* @since 3.5.0
*/
public InsertBatchSomeColumn(String name, Predicate<TableFieldInfo> predicate) {
super(name);
this.predicate = predicate;
}
@SuppressWarnings("Duplicates")
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) +
this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) +
this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (tableInfo.havePK()) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
}
第二步:把上面的类注入到Spring中
第三步:在我们的BaseDAO中声明该方法
第四步:重写saveBatch
最后再调用saveBatch的时候就会发现完成了批量插入的时候不是for 循环插入了,大功告成!!!