我会尝试使用MyBatis+Spring Boot来进行多行插入(Multi Row Insert)和批量更新(Batch Insert)

怎么了?

我以前从未使用过MyBatis来执行多行插入和批量更新操作,所以我想试一试。

尝试过后,我觉得MyBatis的批量更新有点难处理。

在一个事务中,不能同时使用多个ExecutorType,并且当使用ExecutorType#BATCH时执行select语句时,累积的语句会被立即执行,这是一个令人感到困扰的问题。

环境

这次的环境。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29)
Maven home: /home/charon/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-58-generic", arch: "amd64", family: "unix"

决定使用 PostgreSQL 14.6 数据库,并在 Docker 上进行准备。

$ docker container run \
  -it --rm --name postgres \
  -p 5432:5432 \
  -e POSTGRES_DB=example \
  -e POSTGRES_USER=charon \
  -e POSTGRES_PASSWORD=password \
  postgres:14.6-bullseye \
  -c log_statement=all

为了确认SQL,我将log_statement设置为all。

使用Spring Initializr创建一个Spring Boot项目

使用Spring Initializr创建一个Spring Boot项目,以便能够使用MyBatis。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.0.2 \
  -d javaVersion=17 \
  -d type=maven-project \
  -d name=mybatis-batch-update \
  -d groupId=com.example \
  -d artifactId=mybatis-batch-update \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=com.example.spring.mybatis \
  -d dependencies=mybatis,postgresql \
  -d baseDir=mybatis-batch-update | tar zxvf -

前往目录内。

$ cd mybatis-batch-update

由于本次不使用自动生成的源代码, 因此将其删除。

$ rm src/main/java/com/example/spring/mybatis/MybatisBatchUpdateApplication.java src/test/java/com/example/spring/mybatis/MybatisBatchUpdateApplicationTests.java

我会查看依赖关系和插件设置等内容。

        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.mybatis.spring.boot</groupId>
                        <artifactId>mybatis-spring-boot-starter</artifactId>
                        <version>3.0.0</version>
                </dependency>

                <dependency>
                        <groupId>org.postgresql</groupId>
                        <artifactId>postgresql</artifactId>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

这次,我也把Jackson添加到这里了。这是为了应用程序的需求。

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>

创建源代码

我们首先决定桌子。我希望以下是关于类型和项目的主题。

drop table if exists project;
drop table if exists type;

create table type(
  id varchar(36),
  name varchar(50),
  primary key(id)
);

alter table type add unique(name);

create table project(
  id varchar(36),
  name varchar(50),
  type_id varchar(36),
  primary key(id),
  foreign key(type_id) references type(id)
);

alter table project add unique(name);

将其定义为Spring Boot中的schema.sql执行。

这个是关于什么话题的呢?是关于自行对Spring项目进行分类的东西。

 

例如,如果是在Spring Data系列中,就将其归类为Spring Data类型;而对于Spring Framework来说,由于它是顶级项目,所以我们将其归类为Spring Projects。虽然这只是一种随意的分类方式,但是以示例数据为前提。

对于类型(type)而言,由于它是与项目(project)的相关数据关联的,我们决定在插入数据之前先确认(查询)是否已在数据库中注册了与项目关联的类型(type)。
至于项目(project),我们会按照正常流程进行插入操作。

我希望以以下三个形式来尝试这个。

    • ふつうに実行

MyBatisではExecutorType#SIMPLE

複数行のinsertで実行
バッチ実行

MyBatisではExecutorType#BATCH

首先,创建一个将表映射到模型的模型。

项目专用。

package com.example.spring.mybatis.model;

public class Project {
    private String id;
    private String name;
    private String typeId;

    // getter/setterは省略
}

适用于类型。

package com.example.spring.mybatis.model;

public class Type {
    private String id;
    private String name;

    // getter/setterは省略
}

同时还创建了Mapper和XML文件。

项目用途。

package com.example.spring.mybatis.mapper;

import java.util.List;

import com.example.spring.mybatis.model.Project;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProjectMapper {
    List<Project> selectOrderById();

    int insert(Project project);

    int multiRowInsert(List<Project> projects);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.spring.mybatis.mapper.ProjectMapper">
  <select id="selectOrderById" resultType="com.example.spring.mybatis.model.Project">
    select
      id, name, type_id
    from
      project
    order by
      id asc
  </select>
  <insert id="insert">
    insert into
      project(id, name, type_id)
    values
      (#{id}, #{name}, #{typeId})
  </insert>
  <insert id="multiRowInsert">
    insert into
      project(id, name, type_id)
    values

    <foreach collection="projects" item="project" separator=",">
      (#{project.id}, #{project.name}, #{project.typeId})
    </foreach>
  </insert>
</mapper>

最后那个是用于插入多行的,对吗?好像可以使用foreach。

请看下列例子。

映射器XML文件 / 插入、更新和删除

以下是PostgreSQL的insert语句语法。其中还包括了有关多行数据的说明。

 

专为类型设计。

package com.example.spring.mybatis.mapper;

import java.util.List;

import com.example.spring.mybatis.model.Type;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TypeMapper {
    Type selectById(String id);

    List<Type> selectAllOrderById();

    int insert(Type type);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.spring.mybatis.mapper.TypeMapper">
  <select id="selectById" resultType="com.example.spring.mybatis.model.Type">
    select
      id, name
    from
      type
    where
      id = #{id}
  </select>
  <select id="selectAllOrderById" resultType="com.example.spring.mybatis.model.Type">
    select
      id, name
    from
      type
    order by
      id asc
  </select>
  <insert id="insert">
    insert into
      type(id, name)
    values
      (#{id}, #{name})
  </insert>
</mapper>

这里不包括多行插入。

根据需求,我们将创建三个Service类(SimpleService、MultiRowService、BatchService)来使用这些类。具体的实现会在之后附上。

我决定用JSON文件来准备数据。

{
  "mode": "simple",
  "projects": [
    {
      "id": "a1899ae3-15d9-4bba-9a23-f09c01ade805",
      "name": "Spring Boot",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "c6696f16-d51c-47ff-b32b-796b160dccd0",
      "name": "Spring Framework",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "f335b6f5-933a-4ab9-9ed6-dc5bba3771e5",
      "name": "Spring Cloud Bus",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "ee7b6be0-63e6-45d6-8018-f08996a988db",
      "name": "Spring Data JDBC",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "d6d42307-1d65-47f3-afdc-1c44c9a81443",
      "name": "Spring Data JPA",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "f35d452e-bbd2-460f-84a6-99e942f2629f",
      "name": "Spring Data MongoDB",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "beafe138-aa76-443b-b575-7ddf54efbe96",
      "name": "Spring Cloud Circuit Breaker",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "ef50288e-6706-4062-ba9b-0133f2ee664a",
      "name": "Spring Cloud Config",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "0651cda2-cfcd-4fe2-8ac4-610e7372f797",
      "name": "Spring Security",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "d77be708-4542-439c-9250-c52d18479412",
      "name": "Spring Data Redis",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "d487ce9c-20e7-4a48-b72c-da048ebfead5",
      "name": "Spring Data R2DBC",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "25bb12b2-0780-4581-b335-12aeb211b6de",
      "name": "Spring Cloud Gateway",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "debd68cc-470b-4a63-a6e4-4a4264fd85b8",
      "name": "Spring Session",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "ee1e1213-e6fe-4f10-aed4-9920327c1b35",
      "name": "Spring Data Elasticsearch",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "0c7ad896-c207-44c8-b88f-c8f00d410430",
      "name": "Spring Cloud Stream",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "d64b621e-ef62-4727-ad9f-d9cfce11f2ba",
      "name": "Spring Integration",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "68cbceb4-99c2-4542-b085-8b47af964ade",
      "name": "Spring Batch",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "1d5409b3-1664-413d-a77d-82b97cd5991a",
      "name": "Spring Data for Apache Cassandra",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "1dc04ac2-c145-4fd0-b155-3e73490414f4",
      "name": "Spring Data for Apache Solr",
      "typeId": "da77ac10-3984-4c3a-b4d9-f45b0319ba90"
    },
    {
      "id": "0d03b65c-16a9-4902-b381-f7b8373cc696",
      "name": "Spring Cloud Vault",
      "typeId": "9b27dca8-142e-4936-a91c-8aa0f97cabcd"
    },
    {
      "id": "4199d05b-54b0-442d-ba8f-a93751908966",
      "name": "Spring AMQP",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    },
    {
      "id": "95552c7b-6458-4866-b151-18e6dce23d28",
      "name": "Spring WebServices",
      "typeId": "fd49aa2c-a4cf-455f-aa7c-423ec259929e"
    }
  ],
  "types": [
    {
      "id": "fd49aa2c-a4cf-455f-aa7c-423ec259929e",
      "name": "Spring Projects"
    },
    {
      "id": "da77ac10-3984-4c3a-b4d9-f45b0319ba90",
      "name": "Spring Data Projects"
    },
    {
      "id": "9b27dca8-142e-4936-a91c-8aa0f97cabcd",
      "name": "Spring Cloud Projects"
    }
  ]
}

在mode中,我们决定指定运行的模式。

创建一个将其映射的类。

package com.example.spring.mybatis;

import java.util.List;

import com.example.spring.mybatis.model.Project;
import com.example.spring.mybatis.model.Type;

public class Data {
    private String mode;
    private List<Project> projects;
    private List<Type> types;

    // getter/setterは省略
}

一个带有main方法的类。

package com.example.spring.mybatis;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.example.spring.mybatis.mapper.ProjectMapper;
import com.example.spring.mybatis.mapper.TypeMapper;
import com.example.spring.mybatis.model.Project;
import com.example.spring.mybatis.model.Type;
import com.example.spring.mybatis.service.BatchService;
import com.example.spring.mybatis.service.MultiRowService;
import com.example.spring.mybatis.service.SimpleService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.Resource;

@SpringBootApplication
public class App implements ApplicationRunner {
    @Value("classpath:data.json")
    private Resource dataJson;

    @Autowired
    private SimpleService simpleService;
    @Autowired
    private MultiRowService multiRowService;
    @Autowired
    private BatchService batchService;

    @Autowired
    private ProjectMapper projectMapper;
    @Autowired
    private TypeMapper typeMapper;

    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        Data data = objectMapper.readValue(dataJson.getInputStream(), Data.class);

        List<Project> projects = data.getProjects();
        Map<String, Type> types = data.getTypes().stream().collect(Collectors.toMap(type -> type.getId(), type -> type));

        System.out.printf("execution mode = %s%n", data.getMode());

        switch (data.getMode()) {
            case "simple" -> simpleService.insert(projects, types);
            case "multiRow" -> multiRowService.insert(projects, types);
            case "batch" -> batchService.insert(projects, types);
        }

        // トランザクション外
        List<Project> savedProjects = projectMapper.selectOrderById();
        List<Type> savedTypesAsList = typeMapper.selectAllOrderById();
        Map<String, Type> savedTypes = savedTypesAsList.stream().collect(Collectors.toMap(type -> type.getId(), Function.identity()));

        savedProjects.forEach(project -> {
            Type type = savedTypes.get(project.getTypeId());
            System.out.printf("project name = %s, type name = %s%n", project.getName(), type.getName());
        });

        System.out.printf("count, projects = %d, types = %d%n", savedProjects.size(), savedTypesAsList.size());
    }
}

解析JSON文件之后

        ObjectMapper objectMapper = new ObjectMapper();
        Data data = objectMapper.readValue(dataJson.getInputStream(), Data.class);

        List<Project> projects = data.getProjects();
        Map<String, Type> types = data.getTypes().stream().collect(Collectors.toMap(type -> type.getId(), type -> type));

根据执行模式,进行Service类的调用切换。

        switch (data.getMode()) {
            case "simple" -> simpleService.insert(projects, types);
            case "multiRow" -> multiRowService.insert(projects, types);
            case "batch" -> batchService.insert(projects, types);
        }

最后,确认结果。

        // トランザクション外
        List<Project> savedProjects = projectMapper.selectOrderById();
        List<Type> savedTypesAsList = typeMapper.selectAllOrderById();
        Map<String, Type> savedTypes = savedTypesAsList.stream().collect(Collectors.toMap(type -> type.getId(), Function.identity()));

        savedProjects.forEach(project -> {
            Type type = savedTypes.get(project.getTypeId());
            System.out.printf("project name = %s, type name = %s%n", project.getName(), type.getName());
        });

        System.out.printf("count, projects = %d, types = %d%n", savedProjects.size(), savedTypesAsList.size());

无论以哪种模式执行,结果都应该保持一致。

Spring Boot的配置文件。

spring.datasource.url=jdbc:postgresql://localhost:5432/example
spring.datasource.username=charon
spring.datasource.password=password

spring.sql.init.mode=always

mybatis.configuration.map-underscore-to-camel-case=true

每次我决定执行DDL。也就是说,我每次都会删除并创建表。

确认

在包含每个Service类的情况下,我们将进行确认。
每个Service都将有事务边界(在方法上标注@Transactional)。

正常执行

我会先尝试普通地执行。

以下是相应的Service类。

package com.example.spring.mybatis.service;

import java.util.List;
import java.util.Map;

import com.example.spring.mybatis.mapper.ProjectMapper;
import com.example.spring.mybatis.mapper.TypeMapper;
import com.example.spring.mybatis.model.Project;
import com.example.spring.mybatis.model.Type;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SimpleService {
    private ProjectMapper projectMapper;
    private TypeMapper typeMapper;

    public SimpleService(ProjectMapper projectMapper, TypeMapper typeMapper) {
        this.projectMapper = projectMapper;
        this.typeMapper = typeMapper;
    }

    @Transactional
    public void insert(List<Project> projects, Map<String, Type> types) {
        for (Project project : projects) {
            Type type = types.get(project.getTypeId());

            if (typeMapper.selectById(type.getId()) == null) {
                typeMapper.insert(type);
            }

            projectMapper.insert(project);
        }
    }
}

如果在表中找不到该类型,则进行插入。这在其他情况下也是相同的操作。

            if (typeMapper.selectById(type.getId()) == null) {
                typeMapper.insert(type);
            }

这个值是data.json的模式。

{
  "mode": "simple",

只会展示执行结果(除了模式以外,其余都会显示相同的内容)。

execution mode = simple
project name = Spring Security, type name = Spring Projects
project name = Spring Cloud Stream, type name = Spring Cloud Projects
project name = Spring Cloud Vault, type name = Spring Cloud Projects
project name = Spring Data for Apache Cassandra, type name = Spring Data Projects
project name = Spring Data for Apache Solr, type name = Spring Data Projects
project name = Spring Cloud Gateway, type name = Spring Cloud Projects
project name = Spring AMQP, type name = Spring Projects
project name = Spring Batch, type name = Spring Projects
project name = Spring WebServices, type name = Spring Projects
project name = Spring Boot, type name = Spring Projects
project name = Spring Cloud Circuit Breaker, type name = Spring Cloud Projects
project name = Spring Framework, type name = Spring Projects
project name = Spring Data R2DBC, type name = Spring Data Projects
project name = Spring Integration, type name = Spring Projects
project name = Spring Data JPA, type name = Spring Data Projects
project name = Spring Data Redis, type name = Spring Data Projects
project name = Spring Session, type name = Spring Projects
project name = Spring Data Elasticsearch, type name = Spring Data Projects
project name = Spring Data JDBC, type name = Spring Data Projects
project name = Spring Cloud Config, type name = Spring Cloud Projects
project name = Spring Cloud Bus, type name = Spring Cloud Projects
project name = Spring Data MongoDB, type name = Spring Data Projects
count, projects = 22, types = 3

以此为基础。

进行多行插入

一个Service类,用来进行多行插入操作。

package com.example.spring.mybatis.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.example.spring.mybatis.mapper.ProjectMapper;
import com.example.spring.mybatis.mapper.TypeMapper;
import com.example.spring.mybatis.model.Project;
import com.example.spring.mybatis.model.Type;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MultiRowService {
    private ProjectMapper projectMapper;
    private TypeMapper typeMapper;

    public MultiRowService(ProjectMapper projectMapper, TypeMapper typeMapper) {
        this.projectMapper = projectMapper;
        this.typeMapper = typeMapper;
    }

    @Transactional
    public void insert(List<Project> projects, Map<String, Type> types) {
        int rowThreshold = 3;

        List<Project> multiInsertProjects = new ArrayList<>();
        int counter = 0;

        for (Project project : projects) {
            counter++;

            multiInsertProjects.add(project);

            Type type = types.get(project.getTypeId());

            if (typeMapper.selectById(type.getId()) == null) {
                typeMapper.insert(type);
            }

            if (counter % rowThreshold == 0) {
                projectMapper.multiRowInsert(multiInsertProjects);
                multiInsertProjects = new ArrayList<>();
            }
        }

        if (!multiInsertProjects.isEmpty()) {
            projectMapper.multiRowInsert(multiInsertProjects);
        }
    }
}

在指定的记录数量达到时,将多行插入。

            if (counter % rowThreshold == 0) {
                projectMapper.multiRowInsert(multiInsertProjects);
                multiInsertProjects = new ArrayList<>();
            }

在Mapper和XML文件中,我们可以分别找到以下内容。

    int multiRowInsert(List<Project> projects);
  <insert id="multiRowInsert">
    insert into
      project(id, name, type_id)
    values
    
    <foreach collection="projects" item="project" separator=",">
      (#{project.id}, #{project.name}, #{project.typeId})
    </foreach>
  </insert>

data.json文件的mode是按照这个值执行。

{
  "mode": "multiRow",

执行后,您应该能够在PostgreSQL的日志中确认以下多行insert语句的存在。

2023-01-22 13:19:21.454 UTC [784] LOG:  execute <unnamed>: insert into
              project(id, name, type_id)
            values


              ($1, $2, $3)
             ,
              ($4, $5, $6)
             ,
              ($7, $8, $9)
2023-01-22 13:19:21.454 UTC [784] DETAIL:  parameters: $1 = 'a1899ae3-15d9-4bba-9a23-f09c01ade805', $2 = 'Spring Boot', $3 = 'fd49aa2c-a4cf-455f-aa7c-423ec259929e', $4 = 'c6696f16-d51c-47ff-b32b-796b160dccd0', $5 = 'Spring Framework', $6 = 'fd49aa2c-a4cf-455f-aa7c-423ec259929e', $7 = 'f335b6f5-933a-4ab9-9ed6-dc5bba3771e5', $8 = 'Spring Cloud Bus', $9 = '9b27dca8-142e-4936-a91c-8aa0f97cabcd'

批量更新

最后是批量更新。相应的Service类在这里。

package com.example.spring.mybatis.service;

import java.util.List;
import java.util.Map;

import com.example.spring.mybatis.mapper.ProjectMapper;
import com.example.spring.mybatis.mapper.TypeMapper;
import com.example.spring.mybatis.model.Project;
import com.example.spring.mybatis.model.Type;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BatchService {
    private SqlSessionTemplate sqlSessionTemplate;
    private ProjectMapper projectMapper;
    private TypeMapper typeMapper;

    public BatchService(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        this.projectMapper = sqlSessionTemplate.getMapper(ProjectMapper.class);

        this.typeMapper = sqlSessionTemplate.getMapper(TypeMapper.class);
    }

    @Transactional
    public void insert(List<Project> projects, Map<String, Type> types) {
        int rowThreshold = 3;
        int counter = 0;

        for (Project project : projects) {
            counter++;

            Type type = types.get(project.getTypeId());

            if (typeMapper.selectById(type.getId()) == null) {
                typeMapper.insert(type);
            }

            projectMapper.insert(project);

            if (counter % rowThreshold == 0) {
                List<BatchResult> batchResults = sqlSessionTemplate.flushStatements();
                batchResults.stream().forEach(result -> {
                    for (int updated : result.getUpdateCounts()) {
                        if (updated < 1) {
                            throw new RuntimeException("update failed");
                        }
                    }
                });
            }
        }

        List<BatchResult> batchResults = sqlSessionTemplate.flushStatements();
        batchResults.stream().forEach(result -> {
            for (int updated : result.getUpdateCounts()) {
                if (updated < 1) {
                    throw new RuntimeException("update failed");
                }
            }
        });
    }
}

将data.json文件的模式设置为批处理。

{
  "mode": "batch",

回到源代码的问题上来。

要在MyBatis中使用批量更新,需要将ExecutorType设置为BATCH。

Java的API / SqlSessions / SqlSessionFactory

默认情况下是SIMPLE,但也可以通过mybatis-spring-boot-autoconfigure的mybatis.executor-type属性进行更改,但整体上不太可能从SIMPLE进行改变,我认为。

 

在使用Mybatis-Spring框架的情况下,要更改ExecutorType,可以考虑使用SqlSessionTemplate来实现。

mybatis-spring / 使用 SqlSession / SqlSessionTemplate

在使用Mapper本身时,并没有更改。似乎是将通过Mapper执行的语句积累到SqlSession中。

然后,要执行通过MyBatis进行批量更新并累积的语句,需要调用SqlSession#flushStatements。

Java API提供了SqlSessions,SqlSessionFactory以及批量更新语句的Flush方法。

                List<BatchResult> batchResults = sqlSessionTemplate.flushStatements();

这似乎不是以语句或映射器作为单元来处理的。

这是使用MyBatis进行批量更新的方法。

所以,我考虑让ProjectMapper使用ExecutorType#BATCH,TypeMapper使用默认的ExecutorType#SIMPLE来运行。

    public BatchService(SqlSessionFactory sqlSessionFactory, TypeMapper typeMapper) {
        this.sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        this.projectMapper = sqlSessionTemplate.getMapper(ProjectMapper.class);

        this.typeMapper = typeMapper;
    }

如果在这个状态下运行,将会抛出以下例外。

Caused by: org.springframework.dao.TransientDataAccessResourceException: Cannot change the ExecutorType when there is an existing transaction
	at org.mybatis.spring.SqlSessionUtils.sessionHolder(SqlSessionUtils.java:168) ~[mybatis-spring-3.0.0.jar:3.0.0]
	at org.mybatis.spring.SqlSessionUtils.getSqlSession(SqlSessionUtils.java:104) ~[mybatis-spring-3.0.0.jar:3.0.0]
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:422) ~[mybatis-spring-3.0.0.jar:3.0.0]
	at jdk.proxy2/jdk.proxy2.$Proxy49.insert(Unknown Source) ~[na:na]
	at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272) ~[mybatis-spring-3.0.0.jar:3.0.0]
	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62) ~[mybatis-3.5.11.jar:3.5.11]
	at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145) ~[mybatis-3.5.11.jar:3.5.11]
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86) ~[mybatis-3.5.11.jar:3.5.11]
	at jdk.proxy2/jdk.proxy2.$Proxy51.insert(Unknown Source) ~[na:na]

	〜省略〜

看起来,在使用MyBatis中,不允许将不同的ExecutorType包含在同一个事务中。

该表单的附带条件是,在调用此方法时,不得存在使用不同ExecutorType运行的现有交易。请确保使用不同执行器类型的SqlSessionTemplates调用在单独的事务中运行(例如,使用PROPAGATION_REQUIRES_NEW),或者完全不在事务之内。

MyBatis-Spring / 使用 SqlSession / SqlSessionTemplate

是否要开始一个新的事务(将事务进行分开),请在事务外执行……。

那可真是挺严厉的呢。

所以,这次我将所有的Mapper都设置为ExecutorType#BATCH。

    public BatchService(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
        this.projectMapper = sqlSessionTemplate.getMapper(ProjectMapper.class);

        this.typeMapper = sqlSessionTemplate.getMapper(TypeMapper.class);
    }

这样一来,执行就能成功了。

我对select语句的执行情况有些疑虑,但看起来它顺利运行了。

这样就可以了吗?我以为可以,但事实并非如此。

在ExecutorType.BATCH的描述中,写道执行以下的select语句时会执行明确的操作,但看起来似乎是执行select语句时会刷新。

如果在它们之间执行SELECT语句,此执行器将批量处理所有的更新语句,并根据需要进行标记,以确保易于理解的行为。

实际上,BatchExecutor的源代码中也写着当尝试执行select类型的查询时需要进行刷新操作。

 

因此,在这次创建的源代码中,由于中间插入了select语句,所以几乎每次都会刷新(也会发送insert语句)。

            if (typeMapper.selectById(type.getId()) == null) {
                typeMapper.insert(type);
            }

            projectMapper.insert(project);

            if (counter % rowThreshold == 0) {
                List<BatchResult> batchResults = sqlSessionTemplate.flushStatements();
                batchResults.stream().forEach(result -> {
                    for (int updated : result.getUpdateCounts()) {
                        if (updated < 1) {
                            throw new RuntimeException("update failed");
                        }
                    }
                });
            }

实际上,每次循环都会将语句记录在PostgreSQL中。

批量更新不是以语句或Mapper作为单位的,所以只要在任意无关的Mapper中执行了select语句,就会进行刷新操作。

如果不这样做,就没有意义,因此必须独立执行更新查询,否则无法有效地使用MyBatis的批量更新。

如果你不知道的话,可能在你没有意识到的时候会发生闪电般的事情,所以要小心注意一下…

总结

我使用MyBatis+Spring Boot尝试了多行插入和批量更新。

我认为批量更新(ExecutorType#BATCH)有一些特点,因此,在MyBatis中,如果想要高效地执行插入语句,可能使用多行插入更好。
虽然这样一来,无法处理更新语句的问题…

不管怎样,我觉得确认一下还是挺好的。

bannerAds