mybatis-plus 批量插入效率低的问题解决

 更新时间:2026年01月26日 09:21:55   作者:没钱的程序员  
本文揭示了Mybatis-Plus批量插入效率低的原因,并提供了解决方案,包括自定义SQL注入和重写BaseMapper实现高效批量插入,具有一定的参考价值,感兴趣的可以了解一下

背景

由于项目中需要大批量将数据插入数据库,直接使用mybatis-plus中的批量插入方法,结果发现效率奇低无比,线上批量插入一千条数据居然花销八九秒的时间。而我们的目标是想要单次插入一万条数据,这样的效率完全无法接受。

问题追踪

mybatis-plus的源码IService中是有单次批量插入的大小,默认的DEFAULT_BATCH_SIZE=1000,可以看到很多批量方法里面都有设置;通过修改调用方法的入参值,可以增加单次批量插入的数据,但实际发现并没有什么提升。以下为mybatis-plus中service源码:

public interface IService<T> {
 
    /**
     * 默认批次提交数量
     */
    int DEFAULT_BATCH_SIZE = 1000;
 
    /**
     * 插入一条记录(选择字段,策略插入)
     *
     * @param entity 实体对象
     */
    default boolean save(T entity) {
        return SqlHelper.retBool(getBaseMapper().insert(entity));
    }
 
    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveBatch(Collection<T> entityList) {
        return saveBatch(entityList, DEFAULT_BATCH_SIZE);
    }
 
    /**
     * 插入(批量)
     *
     * @param entityList 实体对象集合
     * @param batchSize  插入批次数量
     */
    boolean saveBatch(Collection<T> entityList, int batchSize);
 
    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean saveOrUpdateBatch(Collection<T> entityList) {
        return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE);
    }
 
    /**
     * 批量修改插入
     *
     * @param entityList 实体对象集合
     * @param batchSize  每次的数量
     */
    boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
 
    ......
 
}

继续接着上插入的源码研究,发现底层在SqlHeper类中有个executeBatch的方法有点异常。该方法显示 sqlSession.flushStatements()的调用居然是循环的。也就是说sql层面实际上是一堆insert语句再sqlSession中循环flush,而不是一个大insert一次flush操作完。这就是效率低的本质原因。

/**
     * 执行批量操作
     *
     * @param entityClass 实体类
     * @param log         日志对象
     * @param list        数据集合
     * @param batchSize   批次大小
     * @param consumer    consumer
     * @param <E>         T
     * @return 操作结果
     * @since 3.4.0
     */
    public static <E> boolean executeBatch(Class<?> entityClass, Log log, 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(entityClass, log, 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++;
            }
        });
    }

解决方案

因为上述原因,考虑自己写一个批量插入的sql语句,这是最简单的。

但我们此处不采取此方法,而是直接重写sql注入。以下DefaultSqlInjector为mybatis-plus默认的sql注入实现类,继承的是AbstractSqlInjector类。该默认实现类中添加的Insert、Delete、DeleteByMap等等,实际上就是对应Mapper中所调用的各种方法。

/**
 * SQL 默认注入器
 *
 * @author hubin
 * @since 2018-04-10
 */
public class DefaultSqlInjector extends AbstractSqlInjector {
 
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

 我们需要在默认实现的基础上将额外的sql注入进去,所以直接继承默认的实现类做改进。以下为源码:

/**
 * 重写DefaultSqlInjector
 */
public class SqlInjectorPlus extends DefaultSqlInjector {
 
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //继承原有方法
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //注入新方法
        methodList.add(new InsertBatchSomeColumn());
        return methodList;
    }
 
}
 
/**
 * 注入
 */
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
 
    /**
     * 增强sql注入的Bean
     *
     * @return
     */
    @Bean
    public SqlInjectorPlus sqlInjectorPlus() {
        return new SqlInjectorPlus();
    }
}
 
 
/**
 * 重写BaseMapper
 */
public interface BaseMapperPlus <T> extends BaseMapper<T> {
 
    /**
     * 高效率批量插入
     * entityList数量不能太大,否则存在丢包问题;
     * 单次entityList数量务必控制在一万内,或者在service中再次封装控制数量;
     * @param entityList 数据列表
     * @return 成功标示
     */
    Integer insertBatchSomeColumn(Collection<T> entityList);
 
}

SqlInjectorPlus继承 DefaultSqlInjector 进行重写,继承原有增删改查方法,加入新方法InsertBatchSomeColumn(该类为mybatis-plus源码中就有的,但并未放开来使用)。用MybatisPlusConfig 将Bean注入到spring管理,最后再重写一个BaseMapper并加入新方法。后续的mapper直接继承BaseMapperPlus 就可以调用批量插入的insertBatchSomeColumn方法了。

InsertBatchSomeColumn类

该类为mybatis-plus源码中就有的,但并未放开来使用。主要目的就是实现批量插入,生产的是一个单个大insert语句,注意如果数据量也不宜过大。因为单次flush一个大sql过去,如果数据量过大,产生丢包,则会导致该此批量插入失败。最好再service中再封装一次,做成分批循环调用。该类的源码如下:

 
/**
 * 批量新增数据,自选字段 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 (tableInfo.havePK()) {
            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";
    }
}

总结:

mybatis-plus批量插入效率低的本质原因是底层代码中在sqlsession中循环flush的多条insert语句,因此改进方案有两个:1.写一个sql实现循环插入;2.重写DefaultSqlInjector类,加入自带的InsertBatchSomeColumn。

到此这篇关于mybatis-plus 批量插入效率低的问题解决的文章就介绍到这了,更多相关mybatis-plus 批量插入效率低内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决Eclipse的Servers视图中无法添加Tomcat6/Tomcat7的方法

    解决Eclipse的Servers视图中无法添加Tomcat6/Tomcat7的方法

    这篇文章主要介绍了解决Eclipse的Servers视图中无法添加Tomcat6/Tomcat7的方法的相关资料,需要的朋友可以参考下
    2017-02-02
  • 多个版本Java切换环境变量配置的三种高效方法

    多个版本Java切换环境变量配置的三种高效方法

    这篇文章主要为大家详细介绍了多个版本Java切换环境变量配置的三种高效方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-10-10
  • 关于maven打包出错的解决方案

    关于maven打包出错的解决方案

    这篇文章主要介绍了关于maven打包出错的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • Spring整合CXF webservice restful实例详解

    Spring整合CXF webservice restful实例详解

    这篇文章主要为大家详细介绍了Spring整合CXF webservice restful的实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • Eclipse 安装 SVN 在线插件教程

    Eclipse 安装 SVN 在线插件教程

    这篇文章主要介绍了Eclipse 安装 SVN 在线插件教程的相关资料,这里对安装步骤进行了详细介绍,需要的朋友可以参考下
    2016-11-11
  • mybatis plus时间判断问题

    mybatis plus时间判断问题

    在MyBatisPlus中,时间判断可以通过XML转义的方式实现,例如使用>、<、<>、>=、<=进行比较,这种方法涉及到SQL符号的转义,确保查询语句的安全性和准确性,特别是在处理大于、小于和等于等逻辑时,正确的转义能够防止SQL注入等安全问题
    2024-09-09
  • 深度解析MyBatis 动态 SQL 与缓存机制

    深度解析MyBatis 动态 SQL 与缓存机制

    本文从动态SQL核心语法、缓存实现原理、性能优化及面试高频问题四个维度,结合源码与工程实践,系统解析MyBatis的核心特性与最佳实践,感兴趣的朋友一起看看吧
    2025-06-06
  • MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not found Error处理)

    MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not f

    这篇文章主要介绍了MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not found)的Error处理方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • Maven依赖作用域和依赖传递的使用

    Maven依赖作用域和依赖传递的使用

    本文主要介绍了Maven依赖作用域和依赖传递的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • 教你怎么用Java数组和链表实现栈

    教你怎么用Java数组和链表实现栈

    本篇文章为大家详细介绍了怎么用Java数组和链表实现栈,文中有非常详细的代码示例及注释,对正在学习java的小伙伴们很有帮助,需要的朋友可以参考下
    2021-05-05

最新评论