Spring Boot におけるトランザクション処理 (MyBatis/MySQL)
[最終更新] (2019/06/03 00:36:50)
最近の投稿
注目の記事

概要

Spring フレームワークにおける @Transactional アノテーションを利用すると DB トランザクション処理が簡単に設定できます。ここでは特に、こちらのページで環境構築した Spring Boot から MyBatis を経由して MySQL を利用する場合を対象としますが、JDBC を利用して他の DB を操作する場合も考え方は同じです。

参考ドキュメント

サンプルプロジェクト

Spring Boot から MyBatis を利用するための設定』における構成とほぼ同じです。

.
|-- build.gradle
|-- gradle
|   `-- wrapper
|       |-- gradle-wrapper.jar
|       `-- gradle-wrapper.properties
|-- gradlew
|-- gradlew.bat
`-- src
    `-- main
        |-- java
        |   `-- hello
        |       |-- Application.java
        |       |-- HelloController.java
        |       `-- HelloMapper.java
        `-- resources
            |-- application.yml
            |-- data.sql
            `-- schema.sql

build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'gs-spring-boot'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('mysql:mysql-connector-java:6.0.6')
    compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
}

src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: myuser
    password: myuser
    driver-class-name: com.mysql.jdbc.Driver

src/main/resources/schema.sql

DROP TABLE IF EXISTS city;
CREATE TABLE city (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    state VARCHAR(255),
    country VARCHAR(255)
);

src/main/resources/data.sql

INSERT INTO city (id, name, state, country) VALUES (1, 'San Francisco1', 'CA1', 'US1');
INSERT INTO city (id, name, state, country) VALUES (2, 'San Francisco2', 'CA2', 'US2');

src/main/java/hello/Application.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

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

src/main/java/hello/HelloMapper.java

package hello;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface HelloMapper {
    @Select("UPDATE city SET id = #{idAfter} WHERE id = #{idBefore}")
    void updateCityId(@Param("idBefore") int idBefore, @Param("idAfter") int idAfter);
}

src/main/java/hello/HelloController.java

本ページの目的となるトランザクション設定がなされる箇所です。

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloMapper helloMapper;

    @Transactional
    @RequestMapping("/")
    public String index() {
        helloMapper.updateCityId(1, 9991);
        helloMapper.updateCityId(2, 9992);
        throw new RuntimeException(); // 実行時例外を発生させます。
    }
}

@Transactional が設定してあるため http://localhost:8080/ にアクセスした後も

mysql> select * from city;
+------+----------------+-------+---------+
| id   | name           | state | country |
+------+----------------+-------+---------+
| 9991 | San Francisco1 | CA1   | US1     |
| 9992 | San Francisco2 | CA2   | US2     |
+------+----------------+-------+---------+
2 rows in set (0.00 sec)

とはならず、ロールバックされて以下のような初期状態のままになります。

mysql> select * from city;
+----+----------------+-------+---------+
| id | name           | state | country |
+----+----------------+-------+---------+
|  1 | San Francisco1 | CA1   | US1     |
|  2 | San Francisco2 | CA2   | US2     |
+----+----------------+-------+---------+
2 rows in set (0.00 sec)

メソッド全体の処理が完了するまでコミットされず、途中で非検査例外が発生すると、すべての処理が巻き戻ることが確認できました。

meaning that any failure causes the entire operation to roll back to its previous state, and to re-throw the original exception. This means that none of the people will be added to BOOKINGS if one person fails to be added.
https://spring.io/guides/gs/managing-transactions/

トランザクション設定の補足

同一クラス内の別メソッドでは機能しない

同一クラス内の別メソッドに @Transactional を設定したとしても、有効に機能しません。ただし、その場合でもコンパイルエラーにはなりません。@Service や @Repository を設定した Bean クラスを別途用意する必要があります。

This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html

以下のサンプルにおいて、簡単のためインナークラス@Service を付与しています。

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloMapper helloMapper;

    @Autowired
    private HelloService helloService;

    @RequestMapping("/")
    public String index() {
        helloService.updateCities(); // ロールバックされる
        // this.updateCities(); // ロールバックされない
        return "hello";
    }

    // 以下の設定は無効です。
    @Transactional
    public void updateCities() {
        helloMapper.updateCityId(1, 9991);
        helloMapper.updateCityId(2, 9992);
        throw new RuntimeException();
    }

    // 別クラス (ここでは簡単のためインナークラス)
    @Service
    public class HelloService {

        // 以下の設定は有効です。
        @Transactional
        public void updateCities() {
            helloMapper.updateCityId(1, 9991);
            helloMapper.updateCityId(2, 9992);
            throw new RuntimeException();
        }
    }
}

クラス全体に設定可能

クラスに @Transactional アノテーションを設定できます。個別に設定を上書きたい場合はメソッドに別途設定します。

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloMapper helloMapper;

    @Autowired
    private HelloService helloService;

    @RequestMapping("/")
    public String index() {
        helloService.updateCities();
        return "hello";
    }

    @Service
    @Transactional
    public class HelloService {

        public void updateCities() {
            helloMapper.updateCityId(1, 9991);
            helloMapper.updateCityId(2, 9992);
            throw new RuntimeException();
        }
    }
}

public 以外のメソッドに設定しても機能しない

@Transactional アノテーションを public 以外のメソッドに設定した場合、コンパイルエラーにはなりませんが、有効に機能しません。

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloMapper helloMapper;

    @Autowired
    private HelloService helloService;

    @RequestMapping("/")
    public String index() {
        helloService.updateCities();
        return "hello";
    }

    @Service
    public class HelloService {

        @Transactional
        private void updateCities() { // ロールバックされない
        // public void updateCities() { // ロールバックされる
            helloMapper.updateCityId(1, 9991);
            helloMapper.updateCityId(2, 9992);
            throw new RuntimeException();
        }
    }
}

インターフェースに設定することは非推奨

@Transactional アノテーションをインターフェース等に設定した場合、コンパイルエラーにはなりませんが、非推奨であり、有効に機能しない可能性があります。

Spring recommends that you only annotate concrete classes (and methods of concrete classes) with the @Transactional annotation, as opposed to annotating interfaces.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloMapper helloMapper;

    @Autowired
    private HelloService helloService;

    @RequestMapping("/")
    public String index() {
        helloService.updateCities();
        return "hello";
    }

    @Transactional  // ロールバックされない
    public interface IHelloService {
        void updateCities();
    }

    @Service
    public class HelloService implements IHelloService {

        public void updateCities() {
            helloMapper.updateCityId(1, 9991);
            helloMapper.updateCityId(2, 9992);
            throw new RuntimeException();
        }
    }
}

検査例外はロールバックされない

この続きが気になる方は

Spring Boot におけるトランザクション処理 (MyBatis/MySQL)

残り文字数は全体の約 21 %
tybot
100 円
関連ページ