MyBatis＋Spring Bootで、複数行のinsert（Multi Row Insert）とバッチ更新（Batch Insert）を試す

# What's？

MyBatisで複数行のinsert文の実行と、バッチ更新を扱ったことがなかったので試してみようかなと。

やってみて、MyBatisのバッチ更新はちょっと扱いにくいな、という気がしました。

ひとつのトランザクション内で、複数の`ExecutorType`を使い分けることができないのと、`ExecutorType#BATCH`を使用した時にselect文を実行すると、そこまで溜め込んだステートメントがフラッシュされてしまうのが難点に感じました。

# 環境

今回の環境。

```shell
$ 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で用意。

```shell
$ 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を使って、MyBatisが使えるようにしたSpring Bootプロジェクトを作成します。

```shell
$ 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 -
```

ディレクトリ内に移動。

```shell
$ cd mybatis-batch-update
```

自動生成されたソースコードは、今回は使わないので削除。

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

依存関係やプラグインの設定なども見ておきます。

```xml
        <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も追加しておきました。作成するアプリケーションの都合です。

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

# ソースコードを作成する

最初にテーブルを決めます。以下のような、タイプとプロジェクトをお題にしたいと思います。

```sql:src/main/resources/schema.sql
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のプロジェクトを勝手にカテゴライズしたものです。

https://spring.io/projects/spring-boot

たとえば、Spring Dataファミリーであればタイプを`Spring Data`に、Spring Frameworkであればトップレベルなので`Spring Projects`ということにします。  
勝手なカテゴライズですが、サンプルデータということで…。

typeの方はprojectの関連データということで、projectに紐付いたtypeがすでにデータベースに登録済みか確認（select）してからinsertすることにします。  
projectは、ふつうにinsertします。

これを、以下の3つの形態で試してみたいと思います。

- ふつうに実行
    - MyBatisでは`ExecutorType#SIMPLE`
- 複数行のinsertで実行
- バッチ実行
    - MyBatisでは`ExecutorType#BATCH`

まずはテーブルにマッピングするモデルを作成します。

プロジェクト用。

```java:src/main/java/com/example/spring/mybatis/model/Project.java
package com.example.spring.mybatis.model;

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

    // getter／setterは省略
}
```

タイプ用。

```java:src/main/java/com/example/spring/mybatis/model/Type.java
package com.example.spring.mybatis.model;

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

    // getter／setterは省略
}
```

MapperとXMLファイルも作成します。

プロジェクト用。

```java:src/main/java/com/example/spring/mybatis/mapper/ProjectMapper.java
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:src/main/resources/com/example/spring/mybatis/mapper/ProjectMapper.xml
<?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>
```

最後にあるのは、複数行のinsert向けのものですね。`foreach`を使えばよいみたいです。

以下に例が記載されています。

[Mapper XML Files / insert, update and delete](https://mybatis.org/mybatis-3/sqlmap-xml.html#insert_update_and_delete)

PostgreSQLのinsert文の構文はこちら。複数行に関することも書かれています。

https://www.postgresql.jp/document/14/html/sql-insert.html

タイプ用。

```java:src/main/java/com/example/spring/mybatis/mapper/TypeMapper.java
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:src/main/resources/com/example/spring/mybatis/mapper/TypeMapper.xml
<?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>
```

こちらは、複数行のinsertは含めません。

これらのクラスを使う、目的に応じた3つのServiceクラス（`SimpleService`、`MultiRowService`、`BatchService`）を作成します。  
実装は後で載せます。

データは、JSONファイルで用意することにしました。

```json:src/main/resources/data.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`で、どのモードで実行するかを指定することにします。

これをマッピングするクラスを作成。

```java:src/main/java/com/example/spring/mybatis/Data.java
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`メソッドを持ったクラス。

```java:src/main/java/com/example/spring/mybatis/App.java
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ファイルをパースした後に

```java
        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クラスの呼び分けを行います。

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

最後に、結果確認。

```java
        // トランザクション外
        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の設定ファイル。

```properties:src/main/resources/application.properties
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は毎回実行することにしました。つまり、テーブルは毎回dropしてcreateします。

# 確認する

では、各Serviceクラスを載せつつ、確認していきます。  
どのServiceも、トランザクション境界（メソッドに`@Transactional`を付与する）とします。

## ふつうに実行する

まずはふつうに実行してみます。

対応するServiceクラスはこちら。

```java:src/main/java/com/example/spring/mybatis/service/SimpleService.java
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);
        }
    }
}
```

タイプの方は、テーブルに存在していなければinsertします。これは、他のパターンでも同じ動きにします。

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

`data.json`の`mode`はこの値です。

```json
{
  "mode": "simple",
```

実行結果は、ここだけ載せます（`mode`以外は同じ表示になるので）。

```shell
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
```

これを基本に。

## 複数行のinsertを行う

複数行のinsertを行うServiceクラス。

```java:src/main/java/com/example/spring/mybatis/service/MultiRowService.java
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);
        }
    }
}
```

指定の件数まとまったところで、複数行をinsertします。

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

MapperとXMLファイルでは、それぞれ以下が該当しますね。

```java
    int multiRowInsert(List<Project> projects);
```

```xml
  <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`は、この値で実行。

```xml
{
  "mode": "multiRow",
```

実行すると、PostgreSQLのログで以下のように複数行のinsert文になっていることが確認できると思います。

```shell
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クラスはこちら。

```java:src/main/java/com/example/spring/mybatis/service/BatchService.java
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`にします。

```json
{
  "mode": "batch",
```

ソースコードの方に話を戻して。

MyBatisでバッチ更新を使うには、`ExecutorType`を`BATCH`にする必要があります。

[Java API / SqlSessions / SqlSessionFactory](https://mybatis.org/mybatis-3/java-api.html#sqlsessionfactory)

デフォルトは`SIMPLE`で、mybatis-spring-boot-autoconfigureの`mybatis.executor-type`プロパティで変更することもできますが、全体は`SIMPLE`から変えないだろうな、と思います。

http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

mybatis-springを使った状態で、`ExecutorType`を変更するには`SqlSessionTemplate`を使うのが良さそうです。

[mybatis-spring / Using an SqlSession / SqlSessionTemplate](http://mybatis.org/spring/sqlsession.html#SqlSessionTemplate)

**Mapper自体の使い方は`SIMPLE`の時と変わりません。**  
Mapperを介して実行したステートメントを、`SqlSession`に溜め込むことになるようです。

そして、MyBatisでバッチ更新で溜め込んだステートメントを実行するには`SqlSession#flushStatements`を呼び出します。

[Java API / SqlSessions / SqlSessionFactory / Batch update statement Flush Method](https://mybatis.org/mybatis-3/java-api.html#batch-update-statement-flush-method)

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

ステートメント単位やMapper単位で扱うものではなさそうですね。

ここまでが、MyBatisでのバッチ更新に関する使い方です。

というわけで、`ProjectMapper`は`ExecutorType#BATCH`、`TypeMapper`はデフォルトの`ExecutorType#SIMPLE`で動かそうかなと思ったのですが、

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

        this.typeMapper = typeMapper;
    }
```

この状態で実行すると、以下のような例外がスローされます。

```shell
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`を含めることは許されていないようです。

> The caveat to this form is that there **cannot** be an existing transaction running with a different ExecutorType when this method is called. Either ensure that calls to SqlSessionTemplates with different executor types run in a separate transaction (e.g. with PROPAGATION_REQUIRES_NEW) or completely outside of a transaction.

[mybatis-spring / Using an SqlSession / SqlSessionTemplate](http://mybatis.org/spring/sqlsession.html#SqlSessionTemplate)

新しいトランザクションを始めるか（トランザクションを別々にするか）、トランザクション外で実行しなさい、と…。

それはなかなか厳しいですね。

というわけで、今回は以下のようにすべてのMapperを`ExecutorType#BATCH`にしてみました。

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

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

これで実行は成功するようになります。

select文はどうなるのかなと思ったのですが、どうやら問題なく実行されているようです。

これでOKかな？と思いきや、そうでもなかったり。

`ExecutorType.BATCH`の説明には、以下のようにselect文を実行するとわかりやすい動作を行うと書いてありますが、どうやらこれはselect文を実行するとフラッシュするということみたいです。

> This executor will batch all update statements and demarcate them as necessary if SELECTs are executed between them, to ensure an easy-to-understand behavior.

実際、`BatchExecutor`のソースコードにも、`select`系のクエリーを発行しようとするとフラッシュするように書かれています。

https://github.com/mybatis/mybatis-3/blob/mybatis-3.5.11/src/main/java/org/apache/ibatis/executor/BatchExecutor.java#L86

https://github.com/mybatis/mybatis-3/blob/mybatis-3.5.11/src/main/java/org/apache/ibatis/executor/BatchExecutor.java#L100

というわけで、今回作成したソースコードの場合は、途中でselect文が挟まっているのでほぼ毎回フラッシュ（insert文も送信）されていることになります。

```java
            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で、複数行のinsertとバッチ更新を試してみました。

バッチ更新（`ExecutorType#BATCH`）に癖があるように思うので、MyBatisで効率的にinsert文を実行しようと思うと複数行のinsertの方がよいのかもしれません。  
それだと、update文はどうにもならないという話はありますが…。

なんにせよ、確認しておいて良かったかなと思います。