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_statement
をall
にしておきました。
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>
ソースコードを作成する
最初にテーブルを決めます。以下のような、タイプとプロジェクトをお題にしたいと思います。
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
- MyBatisでは
- 複数行のinsertで実行
- バッチ実行
- MyBatisでは
ExecutorType#BATCH
- MyBatisでは
まずはテーブルにマッピングするモデルを作成します。
プロジェクト用。
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>
最後にあるのは、複数行のinsert向けのものですね。foreach
を使えばよいみたいです。
以下に例が記載されています。
Mapper XML Files / insert, update and delete
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>
こちらは、複数行のinsertは含めません。
これらのクラスを使う、目的に応じた3つの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は毎回実行することにしました。つまり、テーブルは毎回dropしてcreateします。
確認する
では、各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);
}
}
}
タイプの方は、テーブルに存在していなければinsertします。これは、他のパターンでも同じ動きにします。
if (typeMapper.selectById(type.getId()) == null) {
typeMapper.insert(type);
}
data.json
のmode
はこの値です。
{
"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クラス。
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.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
にします。
{
"mode": "batch",
ソースコードの方に話を戻して。
MyBatisでバッチ更新を使うには、ExecutorType
をBATCH
にする必要があります。
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でのバッチ更新に関する使い方です。
というわけで、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
を含めることは許されていないようです。
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文はどうにもならないという話はありますが…。
なんにせよ、確認しておいて良かったかなと思います。