MyBatis-plus批量插入的通用方法使用

 更新时间:2023年04月10日 14:16:59   作者:谈谈1974  
mybatis-plus的IService接口默认提供saveBatch批量插入,也是唯一一个默认批量插入,在数据量不是很大的情况下可以直接使用,本文带你详细了解MyBatis-plus 批量插入的通用方法及使用方法,需要的朋友可以参考一下

1. MyBatis-plus 的批量保存方法

MyBatis-plus 中默认提供了一个批量保存数据到数据库的方法,也就是 IService#saveBatch() 接口方法。这个方法的实现为 ServiceImpl#saveBatch(),其源码实际处理的关键如下,从中可以知道 IService#saveBatch() 并不是一个真正的批量插入数据的方法

  1. 调用 ServiceImpl#sqlStatement() 使用 SqlMethod.INSERT_ONE 枚举结合实体类确定一个全路径方法名称,这个名称将用于匹配实体对应的库表的单个插入方法的 MappedStatement 对象
  2. 调用 ServiceImpl#executeBatch() 方法遍历 Entity 的集合,使用单个插入的方法为每个实体组装一个 INSERT INTO 语句,遍历结束后 flush,一次性将所有生成的 INSERT INTO 语句推给数据库执行

举例来说,如果调用 IService#saveBatch() 方法保存有2个元素的实体集合 List<Node> 数据到数据库,其执行的 SQL 语句如下

存在 2 条:
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
 * <p> 不同的数据库支持度不一样!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!! </p>
 * <p> 除了主键是 <strong> 数据库自增的未测试 </strong> 外理论上都可以使用!!! </p>
 * <p> 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!! </p>
 * <p>
 * 自己的通用 mapper 如下使用:
 * <pre>
 * int insertBatchSomeColumn(List<T> entityList);
 * </pre>
 * </p>
 *
 * <li> 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值 </li>
 *
 * <p>
 * 常用的 {@link Predicate}:
 * </p>
 *
 * <li> 例1: t -> !t.isLogicDelete() , 表示不要逻辑删除字段 </li>
 * <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名为 version 的字段 </li>
 * <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段 </li>
 *
 * @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 语句中

举例来说,如 Node 含有两个属性,分别是 name 和 version,则对于属性值不同的情况最终执行的 SQL 语句也不一样

1. version 为 NULL
INSERT INTO node (name) VALUES (‘nathan’);
2. 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);
    }

到此这篇关于MyBatis-plus批量插入的通用方法使用的文章就介绍到这了,更多相关MyBatis-plus批量插入方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBean依赖和三级缓存的案例讲解

    SpringBean依赖和三级缓存的案例讲解

    这篇文章主要介绍了SpringBean依赖和三级缓存的案例讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • 详解IDEA中SpringBoot整合Servlet三大组件的过程

    详解IDEA中SpringBoot整合Servlet三大组件的过程

    这篇文章主要介绍了详解IDEA中SpringBoot整合Servlet三大组件的过程,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Spring CGLlB动态代理实现过程解析

    Spring CGLlB动态代理实现过程解析

    这篇文章主要介绍了Spring CGLlB动态代理实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • Java编程实现中英混合字符串数组按首字母排序的方法

    Java编程实现中英混合字符串数组按首字母排序的方法

    这篇文章主要介绍了Java编程实现中英混合字符串数组按首字母排序的方法,涉及Java字符串操作及拼音转换的相关使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • 深入浅析JSON在java中的使用

    深入浅析JSON在java中的使用

    这篇文章主要介绍了JSON在java中的使用,包括javaBean和json的互转,List 和 json 的互转及map 和 json 的互转,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-04-04
  • SpringBoot启用GZIP压缩的代码工程

    SpringBoot启用GZIP压缩的代码工程

    经常我们都会与服务端进行大数据量的文本传输,例如 JSON 就是常见的一种格式,通过 REST API 接口进行 GET 和 POST 请求,可能会有大量的文本格式数据提交、返回,压缩和解压在提升网络带宽的同时,会带来 CPU 资源的损耗,本文介绍了SpringBoot启用GZIP压缩的代码工程
    2024-08-08
  • Java中零拷贝和深拷贝的原理及实现探究(代码示例)

    Java中零拷贝和深拷贝的原理及实现探究(代码示例)

    深拷贝和零拷贝是两个在 Java 中广泛使用的概念,它们分别用于对象复制和数据传输优化,下面将详细介绍这两个概念的原理,并给出相应的 Java 代码示例,感兴趣的朋友一起看看吧
    2023-12-12
  • Java函数式编程(十一):遍历目录

    Java函数式编程(十一):遍历目录

    这篇文章主要介绍了Java函数式编程(十一):遍历目录,本文是系列文章的第11篇,其它文章请参阅本文底部的相关文章,需要的朋友可以参考下
    2014-09-09
  • Spring Boot日志的打印与持久化详细解析

    Spring Boot日志的打印与持久化详细解析

    Spring Boot默认使用SLF4J+Logback 记录日志,并提供了默认配置,即使我们不进行任何额外配,也可以使用SLF4J+Logback进行日志输出
    2022-07-07
  • java.lang.FileNotFoundException 异常的正确解决方法(亲测有效)

    java.lang.FileNotFoundException 异常的正确解决方法(亲测有效)

    java.io.FileNotFoundException是一个在文件操作过程中常见的异常,它属于IOException的一个子类,这篇文章主要介绍了java.lang.FileNotFoundException 异常的正确解决方法(亲测有效),需要的朋友可以参考下
    2024-01-01

最新评论