MyBatis-plus
中默认提供了一个批量保存数据到数据库的方法,也就是 IService#saveBatch()
接口方法。这个方法的实现为 ServiceImpl#saveBatch()
,其源码实际处理的关键如下,从中可以知道 IService#saveBatch()
并不是一个真正的批量插入数据的方法
调用ServiceImpl#sqlStatement()
使用SqlMethod.INSERT_ONE
枚举结合实体类确定一个全路径方法名称,这个名称将用于匹配实体对应的库表的单个插入方法的MappedStatement
对象调用ServiceImpl#executeBatch()
方法遍历Entity
的集合,使用单个插入的方法为每个实体组装一个INSERT INTO
语句,遍历结束后 flush,一次性将所有生成的INSERT INTO
语句推给数据库执行
举例来说,如果调用
存在 2 条:IService#saveBatch()
方法保存有2个元素的实体集合List
数据到数据库,其执行的 SQL 语句如下
INSERT INTO node (name, version) VALUES (‘nathan’,1);
INSERT INTO node (name, version) VALUES (‘bob’,1);
而如果是数据库批量插入,其执行的 SQL 语句应该如下
只有 1 条:
INSERT INTO node (name, version) VALUES (‘nathan’,1), (‘bob’,1);
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = sqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
protected String sqlStatement(SqlMethod sqlMethod) {
return SqlHelper.table(entityClass).getSqlStatement(sqlMethod.getMethod());
}
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(sqlSession -> {
int size = list.size();
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if ((i % batchSize == 0) || i == size) {
sqlSession.flushStatements();
}
i++;
}
});
}
2. MyBatis-plus 的批量插入方法
2.1 通用批量插入方法 InsertBatchSomeColumn
事实上 MyBatis-plus
提供了真正的批量插入方法 InsertBatchSomeColumn
,只不过这个方法只在 MySQL 数据库下测试过,所以没有将其作为默认通用方法添加到 SqlMethod
中
从其源码实现不难看出,
InsertBatchSomeColumn
其实就是提供了一个使用foreach
标签的 SQL 脚本,不了解这个标签的读者参考 自定义批量插入 大致理解即可
/**
* 批量新增数据,自选字段 insert
* 不同的数据库支持度不一样!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!! 只在 mysql 下测试过!!!
* 除了主键是 数据库自增的未测试 外理论上都可以使用!!!
* 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!!
*
* 自己的通用 mapper 如下使用:
*
* int insertBatchSomeColumn(List entityList);
*
*
*
* 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值
*
*
* 常用的 {@link Predicate}:
*
*
* 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段
* 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段
* 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段
*
* @author miemie
* @since 2018-11-29
*/
@NoArgsConstructor
@AllArgsConstructor
public class InsertBatchSomeColumn extends AbstractMethod {
/**
* 字段筛选条件
*/
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;
@SuppressWarnings("Duplicates")
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) +
this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(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 (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), 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);
}
@Override
public String getMethod(SqlMethod sqlMethod) {
// 自定义 mapper 方法名
return "insertBatchSomeColumn";
}
}
2.2 InsertBatchSomeColumn 的使用
由于InsertBatchSomeColumn
是框架已经定义好的通用方法,所以使用者只要引入即可,简单来说只需要进行以下几个步骤:
新增 SQL 注入器新增配置类将 SQL 注入器添加到容器新增基类 Mapper,注意这个基类中的批量插入方法名称要和InsertBatchSomeColumn#getMethod()
方法返回的字符串一致,也就是 insertBatchSomeColumn
具体做法读者请参考 MyBatis-plus 自定义通用方法及其实现原理,本文不再赘述
经过以上配置,最终具体的业务类 Mapper 只要继承新增的基类 Mapper 就具备了批量插入的功能,笔者习惯将 Mapper 封装在一个 RepositoryService
中对外提供能力,则各个业务类只需要实现类似如下的 NodeRepositoryServiceImpl#insertBatch()
方法即可以对外提供批量插入的功能
@Override
public int insertBatch(List<Node> entityList) {
if (CollectionUtils.isEmpty(entityList)) {
return 0;
}
return getBaseMapper().insertBatchSomeColumn(entityList);
}
3. 批量插入 MySQL 数据库的坑
3.1 MySQL 对非 NULL 字段插入 NULL 值的处理
使用 MyBatis-plus
批量插入的方法插入 MySQL 记录时需要注意,调用批量插入的方法一定要保证确实是要插入多条数据,如果调用批量插入的方法只插入了单条数据,非常有可能遇到非 NULL 字段插入 NULL 值的错误:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'xxx_name' cannot be null
这是因为我们借助 Entity
插入数据时经常会忽略一些表中有默认值的非 NULL 字段对应的属性的赋值,而从批量插入的 SQL 语句的执行角度来看,这样做也就是往非 NULL 字段插入了 NULL 值。实际上 MySQL 对于非 NULL 字段插入 NULL 值是有兼容处理的,感兴趣的读者可前往 官方传送门,本文摘录如下:
简单来说,对于插入 NULL 值到非 NULL 字段的情况分为两种处理方式:
如果是批量插入多条数据,则会将 NULL 值转化为默认值插到非 NULL 字段(也就是本文批量插入方法插入多条数据的情形)如果是单条数据插入,则抛出异常,失败结束(对应本文批量插入方法只插入了单条数据的情形)
Inserting NULL into a column that has been declared NOT NULL. For multiple-row INSERT statements or
INSERT INTO ... SELECT statements, the column is set to the implicit default value for the column
data type. This is 0 for numeric types, the empty string ('') for string types, and the “zero” value
for date and time types. INSERT INTO ... SELECT statements are handled the same way as multiple-row
inserts because the server does not examine the result set from the SELECT to see whether it returns
a single row. (For a single-row INSERT, no warning occurs when NULL is inserted into a NOT NULL column.
Instead, the statement fails with an error.)
3.2 解决方法
解决方法很简单,只要在批量插入的时候判断一下 Entity
集合的大小即可,如果集合中只有一条数据,则调用插入单条数据的方法
MyBatis-plus
单条数据插入之所以不会有往非 NULL 字段插入 NULL 值的问题,是因为其单条插入数据的 SQL 脚本能根据Entity
的属性赋值情况动态调整,对于Entity
中值为 NULL 的属性,默认不会将其对应的字段添加到执行的 SQL 语句中
举例来说,如
version 为 NULLNode
含有两个属性,分别是name
和version
,则对于属性值不同的情况最终执行的 SQL 语句也不一样
INSERT INTO node (name) VALUES (‘nathan’);version 不为 NULL
INSERT INTO node (name, version) VALUES (‘nathan’,1);
@Override
public int insertBatch(List<Node> entityList) {
if (CollectionUtils.isEmpty(entityList)) {
return 0;
}
if (1 == entityList.size()) {
return getBaseMapper().insert(entityList.get(0));
}
return getBaseMapper().insertBatchSomeColumn(entityList);
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)