CLOVER🍀

That was when it all began.

Spring Frameworkの@Transactinalのロヌルバックに関する蚭定を確認する

これは、なにをしたくお曞いたもの

Spring Frameworkで@Transactionalアノテヌションを䜿った時の、ロヌルバックに関する蚭定を確認しおおきたいな、ず
思いたしお。

@Transactionalアノテヌションを䜿ったトランザクション管理

Spring Frameworkのドキュメントずしおは、こちらに蚘述がありたす。

Using @Transactional

メ゜ッド、たたはクラスに@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アノテヌションに蚭定可胜な項目は、こちら。

@Transactional Settings

トランザクション分離レベル、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.

@Transactional Settings

ちなみに、宣蚀的トランザクションの説明のずころには、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アノテヌションのロヌルバック察象の䟋倖のデフォルト倀は、特になにも指定がないのです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java#L166-L200

Spring Frameworkの゜ヌスコヌドを远っおみたす。

@Transactional ぀たり、TransactionInterceptorが適甚されたメ゜ッドが実行され、䟋倖がスロヌされた時にロヌルバック
するかどうかを確認しおいるのはこちらのようです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java#L670

もうちょっず远っおいくず、RuleBasedTransactionAttributeクラスにたどり着きたす。@Transactionalにロヌルバック察象の
䟋倖を明瀺的に蚭定しおいる堎合は、ここで確認するようです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java#L140-L146

なにも蚭定しおいない堎合は、芪クラスを呌び出したす。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java#L156

芪クラスであるDefaultTransactionAttributeクラスのrollbackOnメ゜ッドを芋るず、RuntimeExceptionたたはErrorであれば
trueを返すようになっおいたす。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java#L186-L188

ずいうわけで、デフォルトの挙動は、蚭定ではなくおデフォルト実装で実珟されおいるずいうこずになりたす。

たた、RuleBasedTransactionAttributeクラスのむンスタンスを䜜成しおいるのは、こちらですね。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java#L68-L102

@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でトランザクション管理する堎合は、どうすればよいのでしょう

アノテヌションを合成すれば良さそうです。

Custom Composed Annotations

こちらのドキュメントに埓い、@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アノテヌションから
トランザクション管理の蚭定を読み取るこのあたりの゜ヌスコヌドを芋おいるず、これしか方法なさそうな感じが

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java#L68-L102

たずめ

今回は、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");
    }
}