4
4

More than 1 year has passed since last update.

MyBatis+Spring Bootで、複数行のinsert(Multi Row Insert)とバッチ更新(Batch Insert)を試す

Last updated at Posted at 2023-01-22

What's?

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

やってみて、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_statementallにしておきました。

Spring InitializrでSpring Bootプロジェクトを作成する

Spring Initializrを使って、MyBatisが使えるようにしたSpring Bootプロジェクトを作成します。

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

ソースコードを作成する

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

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のプロジェクトを勝手にカテゴライズしたものです。

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

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

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

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

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

プロジェクト用。

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は省略
}

タイプ用。

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ファイルも作成します。

プロジェクト用。

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);
}
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

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

タイプ用。

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);
}
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クラス(SimpleServiceMultiRowServiceBatchService)を作成します。
実装は後で載せます。

データは、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で、どのモードで実行するかを指定することにします。

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

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メソッドを持ったクラス。

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ファイルをパースした後に

        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の設定ファイル。

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

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します。これは、他のパターンでも同じ動きにします。

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

data.jsonmodeはこの値です。

{
  "mode": "simple",

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

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クラス。

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します。

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

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

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.jsonmodebatchにします。

{
  "mode": "batch",

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

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

Java API / SqlSessions / SqlSessionFactory

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

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

mybatis-spring / Using an SqlSession / SqlSessionTemplate

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

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

Java API / SqlSessions / SqlSessionFactory / Batch update statement Flush Method

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

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

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

というわけで、ProjectMapperExecutorType#BATCHTypeMapperはデフォルトの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を含めることは許されていないようです。

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

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

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

というわけで、今回は以下のようにすべての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文はどうなるのかなと思ったのですが、どうやら問題なく実行されているようです。

これで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系のクエリーを発行しようとするとフラッシュするように書かれています。

というわけで、今回作成したソースコードの場合は、途中で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で、複数行のinsertとバッチ更新を試してみました。

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

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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4