これは、なにをしたくて書いたもの?
Spring Frameworkで@Transactional
アノテーションを使った時の、ロールバックに関する設定を確認しておきたいな、と
思いまして。
@Transactionalアノテーションを使ったトランザクション管理
Spring Frameworkのドキュメントとしては、こちらに記述があります。
メソッド、またはクラスに@Transactional
アノテーションを付与することで、そのメソッド、またはクラス(およびサブクラス)の
メソッドをトランザクションに参加させることができます。
Used at the class level as above, the annotation indicates a default for all methods of the declaring class (as well as its subclasses).
@Transactional
アノテーションに設定可能な項目は、こちら。
トランザクション分離レベル、Read-Only、ロールバックに関する設定が可能です。
Javadocを見てもよいでしょう。
Transactional (Spring Framework 5.3.6 API)
ちなみに、従来のXMLで設定する方法についてはこちらに記述があります。
Example of Declarative Transaction Implementation
ロールバックについて
さて、ちょっとロールバックに関する説明を見てみましょう。デフォルトではRuntimeException
がスローされるとロールバックし、
チェック例外ではそうならない、と記述があります。
Any RuntimeException triggers rollback, and any checked Exception does not.
ちなみに、宣言的トランザクションの説明のところには、Error
も対象になることが書かれています。
That is, when the thrown exception is an instance or subclass of RuntimeException. ( Error instances also, by default, result in a rollback).
Rolling Back a Declarative Transaction
@Transactional
のJavadocを見てみましょう。
If no custom rollback rules apply, the transaction will roll back on RuntimeException and Error but not on checked exceptions.
Transactional (Spring Framework 5.3.6 API)
デフォルトではRuntimeException
とError
が対象となる、で良さそうです。
このあたりをソースコードを見つつ、設定を変えたりしてみようというのが今回のお題です。
ちなみに、今回は扱いませんが「ロールバックしない」例外を定義することも可能ですね。
環境
今回の環境は、こちら。
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-72-generic", arch: "amd64", family: "unix"
データベースにはMySQL 8.0.24を使い、172.17.0.2で動作しているものとします。
準備
Spring Initializrでプロジェクトを作成します。jdbc
とmysql
を依存関係に含めました。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.4.5 \ -d javaVersion=11 \ -d name=transactional-rollback-rule \ -d groupId=org.littlewings \ -d artifactId=transactional-rollback-rule \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.jdbc \ -d dependencies=jdbc,mysql \ -d baseDir=transactional-rollback-rule | tar zxvf - $ cd transactional-rollback-rule
Spring Bootのバージョンは2.4.5、Spring Frameworkのバージョンは5.3.6です。
できあがったプロジェクトの依存関係等は、こちら。
<properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</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>
動作確認はテストコードで行うことにします。
テストコードを実行するために、@SpringBootApplication
アノテーションが付与されたソースコードを用意。
src/main/java/org/littlewings/spring/jdbc/App.java
package org.littlewings.spring.jdbc; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { }
テストコードの雛形を作製。
src/test/java/org/littlewings/spring/jdbc/TransactionalRollbackTest.java
package org.littlewings.spring.jdbc; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest class TransactionalRollbackTest { @Autowired JdbcTemplate jdbcTemplate; @BeforeEach public void createTable() { jdbcTemplate.execute("drop table if exists sample"); jdbcTemplate.execute("create table sample(word varchar(10));"); } // ここに、Beanのインジェクションとテストを書く! }
テストの実行ごとに、テーブルをdrop、createするようにしています。
@BeforeEach public void createTable() { jdbcTemplate.execute("drop table if exists sample"); jdbcTemplate.execute("create table sample(word varchar(10));"); }
アプリケーションの設定は、こちらだけです。
src/test/resources/application.properties
spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice spring.datasource.username=kazuhira spring.datasource.password=password
まずは単純に使ってみる
最初は、@Transactional
を単純に使ってみましょう。
@Transactinal
を付与したメソッドを持つ、こんなクラスを用意。
src/main/java/org/littlewings/spring/jdbc/SimpleTransactionalService.java
package org.littlewings.spring.jdbc; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class SimpleTransactionalService { JdbcTemplate jdbcTemplate; public SimpleTransactionalService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Transactional(readOnly = true) public List<String> findAll() { return jdbcTemplate.queryForList("select word from sample order by word", String.class); } @Transactional public void insertSuccess(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); } @Transactional public void insertWithRuntimeException(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new RuntimeException("Oops!!"); } @Transactional public void insertWithException(String value) throws Exception { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new Exception("Commit?"); } }
@Transactional
をつけた、insertが成功するメソッド、RuntimeException
をスローするメソッド、Exception
をスローする
メソッド、そしてデータを取得するメソッドを定義しています。
テストコード。
@Autowired SimpleTransactionalService simpleTransactionalService; @Test public void simply() { simpleTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> simpleTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> simpleTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Commit?"); assertThat(simpleTransactionalService.findAll()).containsExactly("foo", "hoge"); }
結果を見ると、RuntimeException
をスローしたメソッドはロールバックされていますが、Exception
をスローしたメソッドは
ロールバックされていない(コミットされている)ことが確認できます。
実装を確認する
さて、このあたりはどこで定義されているのでしょう?
@Transactional
アノテーションのロールバック対象の例外のデフォルト値は、特になにも指定がないのです。
Spring Frameworkのソースコードを追ってみます。
@Transactional
…つまり、TransactionInterceptor
が適用されたメソッドが実行され、例外がスローされた時にロールバック
するかどうかを確認しているのはこちらのようです。
もうちょっと追っていくと、RuleBasedTransactionAttribute
クラスにたどり着きます。@Transactional
にロールバック対象の
例外を明示的に設定している場合は、ここで確認するようです。
なにも設定していない場合は、親クラスを呼び出します。
親クラスであるDefaultTransactionAttribute
クラスのrollbackOn
メソッドを見ると、RuntimeException
またはError
であれば
true
を返すようになっています。
というわけで、デフォルトの挙動は、設定ではなくてデフォルト実装で実現されているということになります。
また、RuleBasedTransactionAttribute
クラスのインスタンスを作成しているのは、こちらですね。
@Transactional
アノテーションの内容を使って、インスタンスを設定しているようです。
設定してみる
実装を見たところで、ちょっと設定を変えてみましょう。
以下のように、@Transactional
のrollbackFor
にException
を指定してみます。
src/main/java/org/littlewings/spring/jdbc/WithRollbackForTransactionalService.java
package org.littlewings.spring.jdbc; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class WithRollbackForTransactionalService { JdbcTemplate jdbcTemplate; public WithRollbackForTransactionalService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Transactional(readOnly = true) public List<String> findAll() { return jdbcTemplate.queryForList("select word from sample order by word", String.class); } @Transactional(rollbackFor = Exception.class) public void insertSuccess(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); } @Transactional(rollbackFor = Exception.class) public void insertWithRuntimeException(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new RuntimeException("Oops!!"); } @Transactional(rollbackFor = Exception.class) public void insertWithException(String value) throws Exception { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new Exception("Rollback!"); } }
テストコードで確認。
@Autowired WithRollbackForTransactionalService withRollbackForTransactionalService; @Test public void withRollbackFor() { withRollbackForTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!"); assertThat(withRollbackForTransactionalService.findAll()).containsExactly("foo"); }
今度は、スローするのがException
でもロールバックされるようになりました。
アノテーションを合成する
ところで、先ほどのような設定にすると、個々のメソッドやクラスに@Transactional
の設定をそれぞれ書いていくため
設定ミスなどが気になるところです。
XMLで設定していた時は、ある意味一括で設定できていた感じがします。
Rolling Back a Declarative Transaction
@Transactional
でトランザクション管理する場合は、どうすればよいのでしょう?
アノテーションを合成すれば良さそうです。
こちらのドキュメントに従い、@Transactional
をカスタマイズした新しいアノテーションを定義します。
Using Meta-annotations and Composed Annotations
新しいアノテーションでは、@AliasFor
アノテーションを使って@Transactional
の設定へ反映させます。
AliasFor (Spring Framework 5.3.6 API)
こんな感じのものを作成。
src/main/java/org/littlewings/spring/jdbc/MyTransactional.java
package org.littlewings.spring.jdbc; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.transaction.annotation.Transactional; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Transactional public @interface MyTransactional { @AliasFor(annotation = Transactional.class, attribute = "readOnly") boolean readOnly() default false; @AliasFor(annotation = Transactional.class, attribute = "rollbackFor") Class<? extends Throwable>[] rollbackFor() default Exception.class; }
@Transactional
をメタアノテーションとして使い、新しいアノテーションを定義します。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Transactional public @interface MyTransactional {
rollbackFor
の設定は、デフォルトでException
をロールバック対象にしてみました。
@AliasFor(annotation = Transactional.class, attribute = "rollbackFor") Class<? extends Throwable>[] rollbackFor() default Exception.class;
あとは、readOnly
が設定できるようにしています。
@AliasFor(annotation = Transactional.class, attribute = "readOnly") boolean readOnly() default false;
作成したアノテーションを使ったコード。
src/main/java/org/littlewings/spring/jdbc/WithComposedAnnotationTransactionalService.java
package org.littlewings.spring.jdbc; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class WithComposedAnnotationTransactionalService { JdbcTemplate jdbcTemplate; public WithComposedAnnotationTransactionalService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @MyTransactional(readOnly = true) public List<String> findAll() { return jdbcTemplate.queryForList("select word from sample order by word", String.class); } @MyTransactional public void insertSuccess(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); } @MyTransactional public void insertWithRuntimeException(String value) { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new RuntimeException("Oops!!"); } @MyTransactional public void insertWithException(String value) throws Exception { jdbcTemplate.update("insert into sample(word) values(?)", value); throw new Exception("Rollback!"); } }
こちらには、rollbackFor
の設定はありません。
確認用のテストコード。
@Autowired WithComposedAnnotationTransactionalService withComposedAnnotationTransactionalService; @Test public void withComposedAnnotation() { withComposedAnnotationTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!"); assertThat(withComposedAnnotationTransactionalService.findAll()).containsExactly("foo"); }
先ほどの、@Transactional
+rollbackFor
でException
を指定した時と同じ結果が得られました。
というわけで、設定を共通化したかったらこういう感じにしたらよさそう…というか、@Transactional
アノテーションから
トランザクション管理の設定を読み取るこのあたりのソースコードを見ていると、これしか方法なさそうな感じが?
まとめ
今回は、Spring Frameworkの@Transactional
のロールバックに関する設定を見たり、その実装部分を見たり、設定を変えて
確認したりしてみました。
どうなんでしょう?@Transactional
をそのまま使うのならいいのですが、rollbackFor
あたりを一律カスタマイズしたい場合は
合成アノテーションを使った方が良さそうなような、そうでもないような。
Spring Frameworkを使うなら、トランザクション管理は@Transactional
でしょう、みたいなところありそうですからね。
変に自前のものを持ち込むと混乱しそうな感じも。
ツールやテストで確認できたらいいのでしょうか。
XMLで埋め込んでいた時は、メソッド名のルールなどで一律適用といった感じですからね。ちょっと違った悩み?
ちなみに、XMLで埋め込んでいた内容を、JavaConfigで行う場合はこのあたりを使うことになりそうです。
BeanFactoryTransactionAttributeSourceAdvisor (Spring Framework 5.3.6 API)
MethodMapTransactionAttributeSource (Spring Framework 5.3.6 API)
RuleBasedTransactionAttribute (Spring Framework 5.3.6 API)
最後に、作成したテストコード全体を載せておきましょう。
src/test/java/org/littlewings/spring/jdbc/TransactionalRollbackTest.java
package org.littlewings.spring.jdbc; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest class TransactionalRollbackTest { @Autowired JdbcTemplate jdbcTemplate; @BeforeEach public void createTable() { jdbcTemplate.execute("drop table if exists sample"); jdbcTemplate.execute("create table sample(word varchar(10));"); } @Autowired SimpleTransactionalService simpleTransactionalService; @Test public void simply() { simpleTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> simpleTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> simpleTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Commit?"); assertThat(simpleTransactionalService.findAll()).containsExactly("foo", "hoge"); } @Autowired WithRollbackForTransactionalService withRollbackForTransactionalService; @Test public void withRollbackFor() { withRollbackForTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!"); assertThat(withRollbackForTransactionalService.findAll()).containsExactly("foo"); } @Autowired WithComposedAnnotationTransactionalService withComposedAnnotationTransactionalService; @Test public void withComposedAnnotation() { withComposedAnnotationTransactionalService.insertSuccess("foo"); assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!"); assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!"); assertThat(withComposedAnnotationTransactionalService.findAll()).containsExactly("foo"); } }