Spring フレームワークにおける @Transactional
アノテーションを利用すると DB トランザクション処理が簡単に設定できます。ここでは特に Spring Boot から MyBatis を経由して MySQL を利用する場合を対象としますが、JDBC を利用して他の DB を操作する場合も考え方は同じです。
参考ドキュメント
- Managing Transactions
- Spring Framework Reference Documentation / Using @Transactional
- mybatis-spring / Transactions
サンプルプロジェクト
.
|-- 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();
}
}
}
検査例外はロールバックされない
@Transactional
既定の設定では、実行時例外のみロールバックされます。
Any RuntimeException triggers rollback, and any checked Exception does not.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative-attransactional-settings
検査例外をロールバックの対象に含めるためには、以下のいずれかを設定します。
@Transactional(rollbackFor = Exception.class)
@Transactional(rollbackForClassName = "Exception")
MyException
は Exception
を直接継承しており、検査例外です。
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() {
try {
helloService.updateCities();
}
catch (MyException e) {
e.printStackTrace();
}
return "hello";
}
public class MyException extends Exception {
private static final long serialVersionUID = 1L;
public MyException(Exception e) {}
}
@Service
public class HelloService {
@Transactional // ロールバックされない
// @Transactional(rollbackFor = Exception.class) // ロールバックされる
// @Transactional(rollbackForClassName = "Exception") // ロールバックされる
public void updateCities() throws MyException {
try {
helloMapper.updateCityId(1, 9991);
helloMapper.updateCityId(2, 9992);
throw new Exception();
}
catch(Exception e) {
throw new MyException(e);
}
}
}
}
読取専用のトランザクションを設定
@Transactional(readOnly = true)
を設定することで、更新系のクエリが発行された際に例外が発生するようになります。
java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
以下のサンプルにおける UPDATE クエリは失敗します。
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(readOnly = true)
public void updateCities() {
helloMapper.updateCityId(1, 9991);
helloMapper.updateCityId(2, 9992);
}
}
}
関連記事
- Spring Security フォームログインのサンプルコードSpring フレームワークによる Web アプリケーション開発で、ログイン処理を実装する際は Spring Security が便利です。ここでは特に Spring Boot で Web アプリケーションを開発する場合を対象とし、フォームによる ID/Password ログインを行うためのサンプルコードをまとめます。 公式ドキュメント [Spring Security チュートリアル](http...
- Java配列の宣言方法 (C/C++との違い)Javaの配列 Javaの配列宣言方法はC/C++と似ているようで若干異なる。 初期化しない場合 C/C++の int array[10]; はJavaでは int array[] = new int[10]; となる。同様にC/C++の int array[3][3]; はJavaでは int array[][] = new int[3][3]; となる。 初期化
- PlantUML による UML 図の描き方PlantUML はテキスト形式で表現されたシーケンス図やクラス図といった UML (Unified Modeling Language) 図の情報から画像を生成するためのツールです。簡単な使い方をまとめます。 インストール方法の選択 Atom や Eclipse のプラグインをインストールしてエディタから利用する方法、JAR をダウンロードして Java コマンドで実行する方法、Redmine ...
- Akka HTTP サンプルコード (Scala)Akka アクターを用いて実装された汎用 HTTP フレームワークです。Spray の後継です。コアモジュールである akka-http-core は 2016/2/17 に experimental が外れました。akka-http などのいくつかのサブモジュールは 2016/3/1 現在 experimental のままですが、基本的な
- Kestrel の使用例Kestrel は Message Queue (MQ) の実装のひとつです。一般に MQ はアプリケーション間やプロセス間、スレッド間で非同期に通信するために用いられます。メッセージの送信側は MQ に書き込めば受信側の応答を待たずに次の処理に非同期に進むことができます。Kestrel はわずか 2500 行程の Scala で実装されており JVM で動作します。MQ 自体はメモリ上に存在する...