我会尝试使用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中,如果想要高效地执行插入语句,可能使用多行插入更好。
虽然这样一来,无法处理更新语句的问题…
不管怎样,我觉得确认一下还是挺好的。