mybatis 批量插入的两种实现方式

1.情景展示

在实际开发过程中,我们有时候会遇到前端批量提交的数据。

诚然,如果我们使用for循环一次一次插入,也是可以的。

但这会涉及到对数据库频繁操作的问题,有没有更好的办法呢?

2.具体分析

我们可以通过mybatis的批量插入功能来解决这个问题。

只需要操作一次数据库,就能完成多条数据的插入操作。

3.解决方案

第一种实现方式

XML

<!--批量插入-->
<insert id="addFlinkDataFieldBatch" parameterType="java.util.List">
    insert into flink_data_field
        (pipelineid, DATABASEID_SOURCE, TABLENAME_SOURCE, FIELDNAME_SOURCE, status, createtime, TABLESTATUS_TARGET)
    values
    <foreach collection="fieldList" separator="," item="row">
        (
            #{row.pipelineid},#{row.databaseidSource},#{row.tablenameSource},#{row.fieldnameSource}, #{row.status} ,now(), #{row.tablestatusTarget}
        )
    </foreach>
</insert>

说明:

这里是mysql数据库,表主键使用的是自增属性,所以插入的时候,可以不用管主键列。

实际在执行的时候,输出的SQL结构为:

insert into flink_data_field
(pipelineid, DATABASEID_SOURCE, TABLENAME_SOURCE, FIELDNAME_SOURCE, status, createtime, TABLESTATUS_TARGET)
values
(?,?,?,?,?,当前时间,?),(?,?,?,?,?,当前时间,?),(?,?,?,?,?,当前时间,?), ...

映射层(dao层、mapper层)

Boolean addFlinkDataFieldBatch(List<FlinkDataField> fieldList);

说明:

插入成功,返回:true;插入失败,返回:false。

方法入参的参数名称:fileldList,与foreach标签的collection的属性值相对应。

如果不对应的话,也可以,只不过,需要我们使用注解@Param。

Boolean addFlinkDataFieldBatch(@Param("fieldList") List<FlinkDataField> list);

说明:

当方法入参的参数名称与foreach标签的collection的属性值不一致时,我们有两种解决办法。

第1种:修改方法入参的参数名称,使其与foreach标签的collection的属性值保持一致。

第2种:在方法入参的变量类型前面增加@Param注解,使该注解的值与foreach标签的collection的属性值保持一致。

业务层(Bo层)

/**
 * 插入管道信息及字段映射信息
 * @param pipelineFieldVo
 * @return
 */
@Override
public Boolean addPipelineAndField(PipelineFieldVo pipelineFieldVo) {
    // 获取管道信息
    FlinkDataPipeline pipeline = pipelineFieldVo.getPipeline();
    if (null == pipeline) {
        // 抛出运行时异常,触发提交事务进行回滚
        throw new RuntimeException("管道信息不能为空");
    }
    // 插入flink_data_pipeline表
    Boolean isSuccess = pipelineMapper.addFlinkDataPipeline(pipeline);

    // 获取插入的管道主键
    Long pipelineid = pipeline.getPipelineid();
    if (!isSuccess || pipelineid == null) {// 插入失败
        throw new RuntimeException("管道信息保存失败");
    }

    // 获取表信息
    List<TableVo> tables = pipelineFieldVo.getTables();
    if (null == tables || tables.isEmpty()) {
        throw new IllegalArgumentException("保存失败:字段映射不能为空");
    }

    List<FlinkDataField> fieldList = new ArrayList<>();
    // 遍历取出每个table里面包含的字段映射信息
    tables.forEach(tableVo -> {
        ColumnVo columnVo = tableVo.getTable();
        if (null == columnVo) {
            throw new IllegalArgumentException("保存失败:字段映射不能为空");
        }

        List<FlinkDataFieldVo> columns = columnVo.getColumns();
        if (null == columns || columns.isEmpty()) {
            throw new IllegalArgumentException("保存失败:字段映射不能为空");
        }

        columns.forEach(flinkDataFieldVo -> {
            FlinkDataField flinkDataField = FlinkDataField.builder().build();
            flinkDataField.setPipelineid(pipelineid)// 管道主键
                .setDatabaseidSource(columnVo.getDatabaseidSource())
                .setDatabaseidTarget(columnVo.getDatabaseidTarget())
                .setTablenameSource(columnVo.getTablenameSource())
                .setTablenameTarget(columnVo.getTablenameTarget())
                .setTablestatusTarget(columnVo.getTablestatusTarget())
                ;

            flinkDataField.setFieldnameSource(flinkDataFieldVo.getFieldnameSource())
                .setFieldtypeSource(flinkDataFieldVo.getFieldtypeSource())
                .setFieldisnullSource(flinkDataFieldVo.getFieldisnullSource())
                .setFieldiskeySource(flinkDataFieldVo.getFieldiskeySource())
                .setFieldnameTarget(flinkDataFieldVo.getFieldnameTarget())
                .setFieldtypeTarget(flinkDataFieldVo.getFieldtypeTarget())
                .setFieldisnullTarget(flinkDataFieldVo.getFieldisnullTarget())
                .setFieldiskeyTarget(flinkDataFieldVo.getFieldiskeyTarget())
                .setStatus(flinkDataFieldVo.getStatus())
                ;

            // 将表的字段映射关系放到List当中
            fieldList.add(flinkDataField);
        });
    });

    return flinkDataFieldMapper.addFlinkDataFieldBatch(fieldList);
}

控制层(Controller层) 

@ApiOperation(value = "添加管道信息", notes = "添加管道信息", httpMethod = "POST")
@PostMapping(value = "/add", produces = {"application/json;charset=UTF-8"})
public Boolean addPipelineAndField(@RequestBody @Valid PipelineFieldVo pipelineFieldVo){
    return pipelineService.addPipelineAndField(pipelineFieldVo);
}

4.补充说明

第一种方式的弊端:

当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长。

从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有<foreach>的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。

一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

另一种折中的办法是:

如果要插入的总行数在100行以内的话,使用这种方式是没有问题的。

实在不行,我们还可以按100行进行拆分成N次,进行批量插入。

2024年4月25日10:41:50

第二种实现方式(推荐使用)

实际上,mybatis官方推荐的是:使用了 ExecutorType.BATCH 的插入方式。

以springboot项目为例

#配置 MyBatis 批处理
mybatis:
     configuration:
       default-executor-type: batch

其余的,与第一种实现方式一致。 

说明:这种方式的执行结果(返回的影响行数)有可能是负数。

写在最后

  哪位大佬如若发现文章存在纰漏之处或需要补充更多内容,欢迎留言!!!

 相关推荐:

posted @ 2023-10-07 18:02  Marydon  阅读(1620)  评论(0编辑  收藏  举报