系列文章目录
MyBatis缓存原理
Mybatis plugin 的使用及原理
MyBatis+Springboot 启动到SQL执行全流程
数据库操作不再困难,MyBatis动态Sql标签解析
Mybatis的CachingExecutor与二级缓存
使用MybatisPlus还是MyBaits ,开发者应该如何选择?
巧用MybatisPlus的SQL注入器提升批量插入性能
上一次我们给大家详细讲解 MybatisPlus 的条件构造器wrapper。这次我们来讲另一个开发者需要了解的功能——SQL注入器,并以实战视角讲述如何利用该特性提升MybatisPlus 的批量插入性能
📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 MyBatis专栏 专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待
一、SQL注入器是什么?
在上次的文章《MybatisPlus 构造器wrapper的使用与原理》 的第三部分,我们讲解了 MybatisPlus 运行的一些原理
-
- 获取语法模板SQL
-
- 代入表及字段信息
-
- 代入条件构造器逻辑
如果不太熟悉的,可以再去复习一遍。不难发现这第一步,就是获取语法模板SQL,MybatisPlus源码中就有不少简单的模板,如下图。但是坦白来说,对于一些大型的项目来说,内置的SQL可能还是少了点
如果只是个别SQL有特殊的语法,那倒罢了,针对个别SQL写个xml文件就可以解决。然而如果有很多SQL都使用同一个模板,那我们一个个去改就比较费事了,而SQL注入器
就能为我们解决这个困扰
二、批量插入的性能问题
1. saveBatch 的运行原理
我们在日常使用中,可能会使用到Iservice
的 saveBatch
方法来实现批量插入
但是如果我们继续在源码中深入,就可以发现这个批量插入本质上是在for循环中插入,默认以1000为一批,进行一次刷新flush
虽然这里使用的同一个sqlSession能够提高一些效率,但毕竟是for循环内的插入,真在大批量插入时性能还是有待提升。而一个大项目中,批量插入的功能还是很常见的,本期我们就看看,如何使用 MybatisPlus的SQL注入器提升批量插入性能
2. 理想的批量插入形式
我们都知道,数据库其实提供了批量插入的SQL样式,比如Mysql的
INSERT INTO your_table_name (column1, column2, column3)
VALUES
(value1a, value2a, value3a),
(value1b, value2b, value3b),
(value1c, value2c, value3c);
而对于Oracle,其也有对应的样式
INSERT ALL
INTO table_name (column1, column2, ...) VALUES (value1a, value2a, ...)
INTO table_name (column1, column2, ...) VALUES (value1b, value2b, ...)
INTO table_name (column1, column2, ...) VALUES (value1c, value2c, ...)
SELECT * FROM dual;
或者
INSERT INTO table_name (column1, column2, ...)
SELECT value1a, value2a, ...
FROM dual
UNION ALL
SELECT value1b, value2b, ...
FROM dual
UNION ALL
SELECT value1c, value2c, ...
FROM dual;
使用对应数据库的批量插入样式,毫无疑问是最合适的,所以我们需要在MybatisPlus中加入这样的模板
三、构造批量插入的SQL注入器
1. 自定义方法
首先我们要自定义一个方法,因为Mysql 和 Oracle 的批量插入样式不一样,所以理论上要写两个方法,但实际上MybatisPlus 针对 Mysql 已经有了一个内置方法 InsertBatchSomeColumn,所以我们只需要针对 Oracle 自定义方法即可 继承自·AbstractMethod
public class InsertBatchSomeColumnOracle extends AbstractMethod {
// oracle 批量插入的格式
private static String ORACLE_INSERT_SQL_MODE = "<script>\nINSERT ALL\n %s \n SELECT 1 FROM DUAL</script>";
// 字段筛选条件
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;
public InsertBatchSomeColumnOracle() {
super("insertBatchSomeColumnOracle");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
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 oracleModelInsertSql = "INTO " + tableInfo.getTableName() + columnScript + " VALUES " + insertSqlProperty;
String valuesScript = SqlScriptUtils.convertForeach(oracleModelInsertSql, "list", null, ENTITY, " ");
String keyProperty = null;
String keyColumn = null;
//表包含主键处理逻辑,如果不包含主键当普通字段处理
if (tableInfo.havePK()) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
Keybenerator:
TableInfoHelper.genKeyGenerator("insertBatchSomeColumnOracle", tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(ORACLE_INSERT_SQL_MODE, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatchSomeColumnOracle", sqlSource, keyGenerator, keyProperty, keyColumn);
}
}
2. 将方法注入 MybatisPlus
仅仅写一个类,并不能直接就使用,我们还需要把这个方法,融入 MybatisPlus 的运行时。这时就要用到另一个类,这个类要继承
DefaultSqlInjector
,如下,我们把mysql 和 oracle 的两个批量插入都加入 methodList 中
public class CustomerSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperclass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperclass, tableInfo);
// 真正的批量插入,添加 InsertBatchSomeColumn 方法,以下仅适用于 MySQL
methodList.add(new InsertBatchSomeColumn());
// 真正的批量插入,添加 InsertBatchSomeColumn 方法,以下仅适用于 oracle
methodList.add(new InsertBatchSomeColumnOracle());
return methodList;
}
}
3. 创建使用方法的接口
上面的内容是把新增的方法放入MybatisPlus,下面我们还需要一个给开发者使用的入口
public interface CustomerMapper<T> extends BaseMapper<T> {
void insertBatchSomeColumn(@Param("list") List<T> list);
void insertBatchSomeColumnOracle(@Param("list") List<T> list);
}
当然,构建完Mapper级别,你也可以继续往上构建,比如构建自己的Service级(接口及实现类),如下
public interface CustomerService<T> extends Iservice<T> {
void insertBatchSomeColumn(List<T> list);
void insertBatchSomeColumnOracle(List<T> list);
}
4. 启用该注入器
现在代码层面的配置解决了,想要使用批量插入功能的,只要让我们的Mapper接口继承CostomerMapper就可以,如下:
@Mapper
public interface CsdnUserInfoMapper extends CustomerMapper<CsdnUserInfo> {
}
然后我们还需要在配置中开启这个注入器。我们可以在yml文件中这么配置
mybatis-plus:
global-config:
sql-injector: com.example.MyLogicSqlInjector
四、总结
在上面的学习里,我们详细阐述了如何使用 MybatisPlus的SQL注入器提升批量插入性能,希望帮大家更深入的了解 MybatisPlus 的特性及其使用,也希望能帮助大家开阔性能优化的思路,后续我们将继续为大家讲解 MybatisPlus 的更多内容