Mybatis-Plus通过SQL注入器实现真正的批量插入

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Mybatis-Plus通过SQL注入器实现真正的批量插入

前言


批量插入是实际工作中常见的一个功能,mysql支持一条sql语句插入多条数据。但是Mybatis-Plus中默认提供的saveBatch方法并不是真正的批量插入,而是遍历实体集合每执行一次insert语句插入一条记录。相比批量插入,性能上显然会差很多。

今天谈一下,在Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。


一、mysql批量插入的支持


insert批量插入的语法支持:

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');


二、Mybatis-Plus默认saveBatch方法解析


1、测试工程建立

测试的数据表:


CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


在IDEA中配置好数据库连接,并安装好MybatisX-Generator插件,生成对应表的model、mapper、service、xml文件。

21.png

生成的文件推荐保存在工程目录下,generator目录下。先生成文件,用户根据自己的需要,再将文件移动到指定目录,这样避免出现文件覆盖。

20.png

生成实体的配置选项,这里我勾选了Lombok和Mybatis-Plus3,生成的类更加优雅。

19.png

移动生成的文件到对应目录:

18.png

由于都是生成的代码,这里就不补充代码了。


2、默认批量插入saveBatch方法测试

@Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        userService.saveBatch(list);
    }

执行日志:

17.png

显然,这里每次执行insert操作,都只插入了一条数据。


3、saveBatch方法实现分析

//批量保存的方法,做了分批请求处理,默认一次处理1000条数据
default boolean saveBatch(Collection<T> entityList) {
    return this.saveBatch(entityList, 1000);
}
//用户也可以自己指定每批处理的请求数量
boolean saveBatch(Collection<T> entityList, int batchSize);


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", new Object[0]);
    return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> {
        int size = list.size();
        int idxLimit = Math.min(batchSize, size);
        int i = 1;
        for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) {
            E element = var7.next();
            consumer.accept(sqlSession, element);
            //每次达到批次数,sqlSession就刷新一次,进行数据库请求,生成Id
            if (i == idxLimit) {
                sqlSession.flushStatements();
                idxLimit = Math.min(idxLimit + batchSize, size);
            }
        }
    });
}


我们将批次数设置为3,用来测试executeBatch的处理机制。


@Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        //批次数设为3,用来测试
        userService.saveBatch(list,3);
    }

执行结果,首批提交的请求,已经生成了id,还没有提交的id为null。

(这里的提交是sql请求,而不是说的事物提交)

16.png

小结:

Mybatis-Plus中默认的批量保存方法saveBatch,底层是通过sqlSession.flushStatements()将一个个单条插入的insert语句分批次进行提交。

相比遍历集合去调用userMapper.insert(entity),执行一次提交一次,saveBatch批量保存有一定的性能提升,但从sql层面上来说,并不算是真正的批量插入。


补充:

遍历集合单次提交的批量插入。


@Test
    public void forEachInsert() {
        System.out.println("forEachInsert 插入开始========");
        long start = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            userMapper.insert(list.get(i));
        }
        System.out.println("foreach 插入耗时:"+(System.currentTimeMillis()-start));
    }


三、Mybatis-plus中SQL注入器介绍


SQL注入器官方文档:https://baomidou.com/pages/42ea4a/


1.sqlInjector介绍

SQL注入器sqlInjector 用于注入 ISqlInjector 接口的子类,实现自定义方法注入。

参考默认注入器 DefaultSqlInjector。


Mybatis-plus默认可以注入的方法如下,大家也可以参考其实现自己扩展:

15.png


默认注入器DefaultSqlInjector的内容:

public class DefaultSqlInjector extends AbstractSqlInjector {
    public DefaultSqlInjector() {
    }
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        //注入通用的dao层接口的操作方法
        return (List)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(Collectors.toList());
    }
}


2.扩展中提供的4个可注入方法实现

目前在mybatis-plus的扩展插件中com.baomidou.mybatisplus.extension,给我们额外提供了4个注入方法。

14.png


1.AlwaysUpdateSomeColumnById 根据Id更新每一个字段,全量更新不忽略null字段,解决mybatis-plus中updateById默认会自动忽略实体中null值字段不去更新的问题。

2.InsertBatchSomeColumn 真实批量插入,通过单SQL的insert语句实现批量插入

3.DeleteByIdWithFill 带自动填充的逻辑删除,比如自动填充更新时间、操作人

4.Upsert 更新or插入,根据唯一约束判断是执行更新还是删除,相当于提供insert on duplicate key update支持

insert into t_name (uid, app_id,createTime,modifyTime)
values(111, 1000000,'2017-03-07 10:19:12','2017-03-07 10:19:12')
on duplicate key update uid=111, app_id=1000000, 
createTime='2017-03-07 10:19:12',modifyTime='2017-05-07 10:19:12'


mysql在存在主键冲突或者唯一键冲突的情况下,根据插入策略不同,一般有以下三种避免方法。

insert ignore
replace into
insert on duplicate key update

这里不展开介绍,大家可以自行查看:

https://blog.csdn.net/weixin_42506706/article/details/113301248


四、通过SQL注入器实现真正的批量插入


通过SQL注入器sqlInjector 增加批量插入方法InsertBatchSomeColumn的过程如下:


1.继承DefaultSqlInjector扩展自定义的SQL注入器

代码如下:

/**
 * 自定义Sql注入
 */
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
         //更新时自动填充的字段,不用插入值
         methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
        return methodList;
    }
}

2.将自定义的SQL注入器注入到Mybatis容器中

代码如下:

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MySqlInjector sqlInjector() {
        return new MySqlInjector();
    }
}

3.继承 BaseMapper 添加自定义方法

public interface CommonMapper<T> extends BaseMapper<T> {
    /**
     * 全量插入,等价于insert
     * @param entityList
     * @return
     */
    int insertBatchSomeColumn(List<T> entityList);
}


4.Mapper层接口继承新的CommonMapper

public interface UserMapper extends CommonMapper<User> {
}


5.单元测试


@Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        userMapper.insertBatchSomeColumn(list);
    }

执行结果:

13.png

可以看到已经实现单条insert语句支持数据的批量插入。


注意⚠️:

默认的insertBatchSomeColumn实现中,并没有类似saveBatch中的分配提交处理,

这就存在一个问题,如果出现一个非常大的集合,就会导致最后组装提交的insert语句的长度超过mysql的限制。


6.insertBatchSomeColumn添加分批处理机制

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private UserMapper userMapper;
    /**
     * 采用insertBatchSomeColumn重写saveBatch方法,保留分批处理机制
     * @param entityList
     * @param batchSize
     * @return
     */
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public boolean saveBatch(Collection<User> entityList, int batchSize) {
        try {
            int size = entityList.size();
            int idxLimit = Math.min(batchSize, size);
            int i = 1;
            //保存单批提交的数据集合
            List<User> oneBatchList = new ArrayList<>();
            for(Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) {
                User element = var7.next();
                oneBatchList.add(element);
                if (i == idxLimit) {
                    userMapper.insertBatchSomeColumn(oneBatchList);
                    //每次提交后需要清空集合数据
                    oneBatchList.clear();
                    idxLimit = Math.min(idxLimit + batchSize, size);
                }
            }
        }catch (Exception e){
            log.error("saveBatch fail",e);
            return false;
        }
        return  true;
    }


更好的实现是继承ServiceImpl实现类,自己扩展通用的服务实现类,在其中重写通用的saveBatch方法,这样就不用在每一个服务类中都重写一遍saveBatch方法。


单元测试:

@Test
    public void testBatchInsert() {
        System.out.println("----- batch insert method test ------");
        List<User> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setName("test");
            user.setAge(13);
            user.setEmail("101@qq.com");
            list.add(user);
        }
        //批次数设为3,用来测试
        userService.saveBatch(list,3);
    }


执行结果:

11.png

分4次采用insert批量新增,符合我们的结果预期。


总结


本文主要介绍了Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。主要掌握如下内容:

1、了解Mybatis-Plus中SQL注入器有什么作用,如何去进行扩展。

2、默认的4个扩展方法各自的作用。

3、默认的saveBatch批量新增和通过insertBatchSomeColumn实现的批量新增的底层实现原理的区别,为什么insertBatchSomeColumn性能更好以及存在哪些弊端。

4、为insertBatchSomeColumn添加分批处理机制,避免批量插入的insert语句过长问题。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
3月前
|
SQL XML Java
通过MyBatis的XML配置实现灵活的动态SQL查询
总结而言,通过MyBatis的XML配置实现灵活的动态SQL查询,可以让开发者以声明式的方式构建SQL语句,既保证了SQL操作的灵活性,又简化了代码的复杂度。这种方式可以显著提高数据库操作的效率和代码的可维护性。
259 18
|
8月前
|
SQL Java 数据库连接
【YashanDB知识库】解决mybatis的mapper文件sql语句结尾加分号";"报错
【YashanDB知识库】解决mybatis的mapper文件sql语句结尾加分号";"报错
|
7月前
|
SQL Java 数据库连接
MyBatis动态SQL字符串空值判断,这个细节99%的程序员都踩过坑!
本文深入探讨了MyBatis动态SQL中字符串参数判空的常见问题。通过具体案例分析,对比了`name != null and name != &#39;&#39;`与`name != null and name != &#39; &#39;`两种写法的差异,指出后者可能引发逻辑混乱。为避免此类问题,建议在后端对参数进行预处理(如trim去空格),简化MyBatis判断逻辑,提升代码健壮性与可维护性。细节决定成败,严谨处理参数判空是写出高质量代码的关键。
955 0
|
3月前
|
SQL Java 数据库连接
SSM相关问题-1--#{}和${}有什么区别吗?--Mybatis都有哪些动态sql?能简述一下动 态sql的执行原理吗?--Spring支持的几种bean的作用域 Scope
在MyBatis中,`#{}`是预处理占位符,可防止SQL注入,适用于大多数参数传递场景;而`${}`是直接字符串替换,不安全,仅用于动态表名、列名等特殊场景。二者在安全性、性能及使用场景上有显著区别。
78 0
|
6月前
|
SQL XML Java
菜鸟之路Day35一一Mybatis之XML映射与动态SQL
本文介绍了MyBatis框架中XML映射与动态SQL的使用方法,作者通过实例详细解析了XML映射文件的配置规范,包括namespace、id和resultType的设置。文章还对比了注解与XML映射的优缺点,强调复杂SQL更适合XML方式。在动态SQL部分,重点讲解了`&lt;if&gt;`、`&lt;where&gt;`、`&lt;set&gt;`、`&lt;foreach&gt;`等标签的应用场景,如条件查询、动态更新和批量删除,并通过代码示例展示了其灵活性与实用性。最后,通过`&lt;sql&gt;`和`&lt;include&gt;`实现代码复用,优化维护效率。
544 5
|
8月前
|
SQL Java 数据库连接
【YashanDB 知识库】解决 mybatis 的 mapper 文件 sql 语句结尾加分号";"报错
【YashanDB 知识库】解决 mybatis 的 mapper 文件 sql 语句结尾加分号";"报错
|
8月前
|
SQL 缓存 Java
框架源码私享笔记(02)Mybatis核心框架原理 | 一条SQL透析核心组件功能特性
本文详细解构了MyBatis的工作机制,包括解析配置、创建连接、执行SQL、结果封装和关闭连接等步骤。文章还介绍了MyBatis的五大核心功能特性:支持动态SQL、缓存机制(一级和二级缓存)、插件扩展、延迟加载和SQL注解,帮助读者深入了解其高效灵活的设计理念。
|
9月前
|
SQL XML Java
九、MyBatis动态SQL
九、MyBatis动态SQL
149 2
|
10月前
|
SQL Java 数据库连接
【潜意识Java】MyBatis中的动态SQL灵活、高效的数据库查询以及深度总结
本文详细介绍了MyBatis中的动态SQL功能,涵盖其背景、应用场景及实现方式。
1058 6
|
8月前
|
SQL XML Java
六、MyBatis特殊的SQL:模糊查询、动态设置表名、校验名称唯一性
六、MyBatis特殊的SQL:模糊查询、动态设置表名、校验名称唯一性
230 0