CLOVER🍀

That was when it all began.

Spring Frameworkで、REQUIREDな䌝播レベルのトランザクションがネストした時の䟋倖の扱いを確認する

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

Spring Frameworkを䜿っおいるず、トランザクション管理を@Transactionalアノテヌションを䜿っお宣蚀的に曞いおいるこずが
倚いず思いたす。

@Transactionalを䜿った堎合、䟋倖デフォルトではRuntimeExceptionのサブクラスがスロヌされた時にロヌルバックされる
こずになっおいたす。ここで、@Transactionalアノテヌションが付䞎されたメ゜ッドがネストし、か぀途䞭で䟋倖を
捕捉した堎合にどういう挙動になるのか、確認しおみたいなず思いたしお。

トランザクションの䌝播に぀いおは、PROPAGATION_REQUIREDを䞻な察象にしおいたす。

Springの宣蚀的トランザクションずトランザクションの䌝播

Springの宣蚀的トランザクションに関するドキュメントずしおは、こちらですね。

Declarative Transaction Management

たた、@Transactionalアノテヌションに぀いおは、こちらに蚘茉されおいたす。

Using @Transactional

ここで、トランザクションの䌝播に関するドキュメントを読んでみたす。

Transaction Propagation

最初に疑問に曞いたこずの答えが、実はここに曞いおいたす。

Understanding PROPAGATION_REQUIRED

PROPAGATION_REQUIREDを䜿ったトランザクションがネストし、内偎のトランザクションのロヌルバックが確定した堎合の
こずが曞いおありたす。

When the propagation setting is PROPAGATION_REQUIRED, a logical transaction scope is created for each method upon which the setting is applied. Each such logical transaction scope can determine rollback-only status individually, with an outer transaction scope being logically independent from the inner transaction scope. In the case of standard PROPAGATION_REQUIRED behavior, all these scopes are mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transaction’s chance to actually commit.

However, in the case where an inner transaction scope sets the rollback-only marker, the outer transaction has not decided on the rollback itself, so the rollback (silently triggered by the inner transaction scope) is unexpected. A corresponding UnexpectedRollbackException is thrown at that point. This is expected behavior so that the caller of a transaction can never be misled to assume that a commit was performed when it really was not. So, if an inner transaction (of which the outer caller is not aware) silently marks a transaction as rollback-only, the outer caller still calls commit. The outer caller needs to receive an UnexpectedRollbackException to indicate clearly that a rollback was performed instead.

぀たり、内偎のトランザクションのロヌルバックが確定しおいるず倖偎のトランザクションはUnexpectedRollbackExceptionを
スロヌする、ずいうこずになりそうです。

内偎のトランザクションを独立させたい堎合は、PROPAGATION_REQUIRES_NEWを䜿うわけですね。

Understanding PROPAGATION_REQUIRES_NEW

今回は、ここたで含めお確認しおみようかな、ず思いたす。

環境

今回の環境は、こちらです。

$ 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-73-generic", arch: "amd64", family: "unix"

デヌタベヌスはMySQL 8.0.25を䜿い、172.17.0.2で動䜜しおいるものずしたす。

Spring Bootプロゞェクトを䜜成する

最初に、Spring Bootプロゞェクトを䜜成したす。Spring Bootのバヌゞョンは2.5.0で、䟝存関係にはjdbcずmysqlを加えたす。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.5.0 \
  -d javaVersion=11 \
  -d name=transactional-nested-required \
  -d groupId=org.littlewings \
  -d artifactId=transactional-nested-required \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.jdbc \
  -d dependencies=jdbc,mysql \
  -d baseDir=transactional-nested-required | tar zxvf -


$ cd transactional-nested-required
$ find src -name '*.java' | xargs rm

生成されたJava゜ヌスコヌドは削陀。

Maven䟝存関係などは、こんな感じです。

 <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 {
}

application.propertiesは、こんな蚭定にしおおきたした。

src/main/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8
spring.datasource.username=kazuhira
spring.datasource.password=password

お題

@Transactionalアノテヌションを付䞎したメ゜ッドを持぀クラスを䜜成し、ふ぀うに凊理を完了させたり、䟋倖をスロヌしお
キャッチしたりしなかったり ずいく぀かバリ゚ヌションを぀けお確認しおみたしょう。

トランザクションの䌝播はPROPAGATION_REQUIRESから始め、PROPAGATION_REQUIRES_NEWも織り亀ぜお
いくようにしたす。

テストコヌドの雛圢

たずはテストコヌドの雛圢を䜜成したしょう。こんな感じにしたした。

src/test/java/org/littlewings/spring/jdbc/TransactionalTest.java

package org.littlewings.spring.jdbc;

import java.util.List;

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 org.springframework.transaction.UnexpectedRollbackException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest
public class TransactionalTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    public void createTable() {
        jdbcTemplate.execute("drop table if exists sample");

        jdbcTemplate.execute("create table sample(word varchar(25));");
    }

    // ここに、テストを曞く
}

テストごずに、テヌブルをDROP  CREATEしたす。

Serviceクラス

今回は、Serviceクラスを2぀甚意したす。それぞれ、@Transactionalアノテヌションを付䞎したメ゜ッドを持ち、
倖偎のトランザクション、内偎のトランザクションを衚珟したす。

倖偎に該圓するものは、こちら。JdbcTemplateず、内偎のServiceクラスを䜿甚したす。

src/main/java/org/littlewings/spring/jdbc/MyService.java

package org.littlewings.spring.jdbc;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MyService {
    Logger logger = LoggerFactory.getLogger(MyService.class);

    JdbcTemplate jdbcTemplate;

    NestedService nestedService;

    public MyService(JdbcTemplate jdbcTemplate, NestedService nestedService) {
        this.jdbcTemplate = jdbcTemplate;
        this.nestedService = nestedService;
    }

    // メ゜ッドを曞く
}

MyServiceクラスから呌び出されるのは、こちら。同じく、JdbcTemplateを䜿甚したす。

src/main/java/org/littlewings/spring/jdbc/NestedService.java

package org.littlewings.spring.jdbc;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class NestedService {
    Logger logger = LoggerFactory.getLogger(NestedService.class);

    JdbcTemplate jdbcTemplate;

    public NestedService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // メ゜ッドを曞く
}

これらのクラスずテストコヌドを䜿っお、確認しおきたしょう。

テストコヌド偎では、䞊蚘のMyServiceクラスを䜿甚したす。

@SpringBootTest
public class TransactionalTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    public void createTable() {
        jdbcTemplate.execute("drop table if exists sample");

        jdbcTemplate.execute("create table sample(word varchar(25));");
    }

    @Autowired
    MyService myService;

ここから先は、MyService、NestedService、テストコヌドの順でいろいろ曞いお確認しおいきたす。

PROPAGATION_REQUIRES

では、確認しおいきたしょう。

コミットする

たずはふ぀うにトランザクションが完了するコミットするパタヌン。指定されたwordを登録するinsert文を実行しお、
そのwordを二重にしお次のServiceクラスを呌び出したす。

@Service
public class MyService {

    @Transactional
    public int insertRequired(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequired(word + " " + word);
    }

}

クラスの定矩は、抜粋しお曞いおいきたす。

呌び出し先。

@Service
public class NestedService {

    @Transactional
    public int insertRequired(String word) {
        return jdbcTemplate.update("insert into sample(word) values(?)", word);
    }

}

どちらも曎新件数を返すので、䞡方の曎新がうたくいった堎合は戻り倀が2になりたすね。

テストコヌド。デヌタが登録されおいるのが確認できたす。

    @Test
    public void transactionalNormally() {
        assertThat(myService.insertRequired("Hello!!")).isEqualTo(2);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!", "Hello!! Hello!!"));
    }
ロヌルバックする

次は、ロヌルバックするパタヌン。

倖偎のServiceクラスはふ぀うですが

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedThrown(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredAndThrown(word + " " + word);
    }

}

内偎のServiceクラスでは䟋倖をスロヌしたす。

@Service
public class NestedService {

    @Transactional
    public int insertRequiredAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

テストコヌド偎。ロヌルバックですね。

    @Test
    public void transactionalNestedFailed() {
        assertThatThrownBy(() -> myService.insertRequiredAndNestedThrown("Hello!!"))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Oops!!");

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEmpty();
    }
内偎のServiceクラスが䟋倖をスロヌし、倖偎のServiceクラス内で捕捉する

続いおは、内偎のServiceクラスのメ゜ッドが䟋倖をスロヌしお、倖偎のServiceクラスでその䟋倖を補足するパタヌン。

぀たり、倖偎のServiceクラスはこんな感じです。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedThrownAndCatch(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        try {
            nestedService.insertRequiredAndThrown(word + " " + word);
        } catch (RuntimeException e) {
            logger.error("insert failed", e);
        }

        return result;
    }

}

内偎のServiceクラスは、先ほどず同じです。

@Service
public class NestedService {

    @Transactional
    public int insertRequiredAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

テストコヌドは倖偎のServiceクラスから䟋倖がスロヌされない ず思いきや、UnexpectedRollbackExceptionがスロヌされる
こずになりたす。

こんなこずを蚀われ぀぀。

Transaction rolled back because it has been marked as rollback-only

    @Test
    public void transactionalNestedFailedAndCatch() {
        assertThatThrownBy(() -> myService.insertRequiredAndNestedThrownAndCatch("Hello!!"))
                .isInstanceOf(UnexpectedRollbackException.class)
                .hasMessage("Transaction rolled back because it has been marked as rollback-only");

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEmpty();
    }

ドキュメント通りですね。

内偎のServiceクラスで䟋倖は発生するものの、メ゜ッドを䟋倖で抜けない

なにを蚀っおいるかずいうず、内偎のServiceクラスで䟋倖は発生するものの、トランザクションの境界ずなるメ゜ッドは
䟋倖で脱出しない、ず。

倖偎のServiceクラスは、こんな感じです。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedIgnoreFailed(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredAndIgnoreFailed(word + " " + word);
    }

}

内偎のServiceクラスは誀ったSQL文を実行させお䟋倖が発生したすが、メ゜ッド自䜓は䟋倖では抜けたせん。

@Service
public class NestedService {

    @Transactional
    public int insertRequiredAndIgnoreFailed(String word) {
        try {
            // 構文誀りで実行に倱敗するSQL
            jdbcTemplate.update("insert into sample(word) v(?)", word);
        } catch (RuntimeException e) {
            logger.error("sql error", e);
        }

        return 0;
    }

}

テストコヌドでは、倖偎のServiceクラスの凊理結果だけが反映コミットされおいるこずが確認できたす。

    @Test
    public void transactionalNestedIgnoreFailed() {
        assertThat(myService.insertRequiredAndNestedIgnoreFailed("Hello!!")).isEqualTo(1);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!"));
    }

予想できる話ではありたすが、トランザクションの境界を䟋倖で抜けおいたせんからね。

぀たり、トランザクションの䌝播がPROPAGATION_REQUIREDの積み重ねずなりえる堎合は、䞭途半端なずころで
䟋倖を捕たえずにトランザクションの境界倖で捕捉するか、トランザクションの境界を跚がないうちに捕捉するこず、ずいう
感じにした方が良さそうですね。

゜ヌスコヌドから確認する

このあたりの動䜜を、゜ヌスコヌド䞊でも確認しおみたしょう。

トランザクション境界ずなるメ゜ッドを䟋倖で抜けた時点で、ロヌルバックのマヌクが行われたす。

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L844

ResourceHolderSupportでのrollbackOnlyがtrueに蚭定されるのが、そのマヌクですね。

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/ResourceHolderSupport.java#L67-L69

この状態になるず、倖偎のトランザクション境界をコミットしようずしたタむミングで、ロヌルバックを行うようにマヌクされお
いるこずが怜出されたす。

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L703-L709

そしお、UnexpectedRollbackExceptionがスロヌされたす、ず。

https://github.com/spring-projects/spring-framework/blob/v5.3.7/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java#L869-L872

こちらが、゜ヌスコヌド䞊での確認ですね。

あずは、もう少しバリ゚ヌションを確認しおみたしょう。

PROPAGATION_REQUIRESずPROPAGATION_REQUIRES_NEWの組み合わせ

今床は、倖偎をPROPAGATION_REQUIRES、内偎をPROPAGATION_REQUIRES_NEWにしおみたす。

倖偎ず内偎で、トランザクションが別になりたすね。

コミットする

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedNew(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNew(word + " " + word);
    }

}

内偎のServiceクラス。トランザクションの䌝播レベルが、REQUIRES_NEWです。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNew(String word) {
        return jdbcTemplate.update("insert into sample(word) values(?)", word);
    }

}

ずはいえ、コミットするので結果はたあふ぀うです。

    @Test
    public void transactionalNormallyNestedNew() {
        assertThat(myService.insertRequiredAndNestedNew("Hello!!")).isEqualTo(2);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!", "Hello!! Hello!!"));
    }
ロヌルバックする

続いお、ロヌルバック。こちらも倉わったこずはありたせん。

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedNewThrown(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNewAndThrown(word + " " + word);
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

䞡方のメ゜ッドを䟋倖で抜けるので、ロヌルバックしたす。

    @Test
    public void transactionalNestedNewFailed() {
        assertThatThrownBy(() -> myService.insertRequiredAndNestedNewThrown("Hello!!"))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Oops!!");

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEmpty();
    }
内偎のServiceクラスが䟋倖をスロヌし、倖偎のServiceクラス内で捕捉する

こちらは、PROPAGATION_REQUIREDの時ず倉化がありたす。

倖偎のServiceクラス。内偎のServiceクラスがスロヌした䟋倖を捕捉したす。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedNewThrownAndCatch(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        try {
            nestedService.insertRequiredNewAndThrown(word + " " + word);
        } catch (RuntimeException e) {
            logger.error("insert failed", e);
        }

        return result;
    }

}

内偎のServiceクラスは、先ほどず同じです。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

テストコヌド。PROPAGATION_REQUIREDがネストしおいる時ずは異なり、UnexpectedRollbackExceptionはスロヌされず
倖偎のトランザクションはコミットされたす。

    @Test
    public void transactionalNestedNewFailedAndCatch() {
        assertThat(myService.insertRequiredAndNestedNewThrownAndCatch("Hello!!")).isEqualTo(1);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!"));  // 䟋倖にならない
    }

別々のトランザクションになっおいるので、こうなりたすよね。

内偎のServiceクラスで䟋倖は発生するものの、メ゜ッドを䟋倖で抜けない

こちらも詊しおみたす。

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional
    public int insertRequiredAndNestedNewIgnoreFailed(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNewAndIgnoreFailed(word + " " + word);
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndIgnoreFailed(String word) {
        try {
            // 構文誀りで実行に倱敗するSQL
            jdbcTemplate.update("insert into sample(word) v(?)", word);
        } catch (RuntimeException e) {
            logger.error("sql error", e);
        }

        return 0;
    }

}

テストコヌド。こちらに぀いおは、それぞれ別のトランザクションがコミットされただけ、ずなりたす。

    @Test
    public void transactionalNestedNewIgnoreFailed() {
        assertThat(myService.insertRequiredAndNestedNewIgnoreFailed("Hello!!")).isEqualTo(1);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!"));
    }

そりゃあ、そうなりたすよね、ず。

䞡方ずもPROPAGATION_REQUIRES_NEWにする

今回の確認方法でこれをやる意味はない気がしたすが、網矅的な意味では䞀応 ずいうこずで。

゜ヌスコヌドず結果だけ茉せたす。

PROPAGATION_REQUIRES、PROPAGATION_REQUIRES_NEWの組み合わせず同じ結果になりたす。それぞれが
独立するこずを明瀺しおいるだけなので。

コミットする

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndNestedNew(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNew(word + " " + word);
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNew(String word) {
        return jdbcTemplate.update("insert into sample(word) values(?)", word);
    }

}

テストコヌド。

    @Test
    public void transactionalNormallyNewNestedNew() {
        assertThat(myService.insertRequiredNewAndNestedNew("Hello!!")).isEqualTo(2);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!", "Hello!! Hello!!"));
    }
ロヌルバックする

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndNestedNewThrown(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNewAndThrown(word + " " + word);
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

テストコヌド。

    @Test
    public void transactionalNewNestedNewFailed() {
        assertThatThrownBy(() -> myService.insertRequiredNewAndNestedNewThrown("Hello!!"))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Oops!!");

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEmpty();
    }
内偎のServiceクラスが䟋倖をスロヌし、倖偎のServiceクラス内で捕捉する

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndNestedNewThrownAndCatch(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        try {
            nestedService.insertRequiredNewAndThrown(word + " " + word);
        } catch (RuntimeException e) {
            logger.error("insert failed", e);
        }

        return result;
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndThrown(String word) {
        jdbcTemplate.update("insert into sample(word) values(?)", word);

        throw new RuntimeException("Oops!!");
    }

}

テストコヌド。

    @Test
    public void transactionalNewNestedNewFailedAndCatch() {
        assertThat(myService.insertRequiredNewAndNestedNewThrownAndCatch("Hello!!")).isEqualTo(1);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!"));  // 䟋倖にならない
    }
内偎のServiceクラスで䟋倖は発生するものの、メ゜ッドを䟋倖で抜けない

倖偎のServiceクラス。

@Service
public class MyService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndNestedNewIgnoreFailed(String word) {
        int result = jdbcTemplate.update("insert into sample(word) values(?)", word);

        return result + nestedService.insertRequiredNewAndIgnoreFailed(word + " " + word);
    }

}

内偎のServiceクラス。

@Service
public class NestedService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int insertRequiredNewAndIgnoreFailed(String word) {
        try {
            // 構文誀りで実行に倱敗するSQL
            jdbcTemplate.update("insert into sample(word) v(?)", word);
        } catch (RuntimeException e) {
            logger.error("sql error", e);
        }

        return 0;
    }

}

テストコヌド。

    @Test
    public void transactionalNewNestedNewIgnoreFailed() {
        assertThat(myService.insertRequiredNewAndNestedNewIgnoreFailed("Hello!!")).isEqualTo(1);

        assertThat(jdbcTemplate.queryForList("select word from sample order by word", String.class))
                .isEqualTo(List.of("Hello!!"));
    }

たずめ

Spring Frameworkを䜿った時の、REQUIREDな䌝播レベルのトランザクションがネストした時に、䟋倖をどう扱うかで
どのような挙動するのか、確認しおみたした。

ドキュメントに答えは曞いおあるのですが、REQUIREDがネストしおいる時に䞭途半端な堎所で䟋倖を捕捉したりするず
厄介なこずになりそうですね。

䌝播レベルをわけるか、䟋倖を捕たえるか、トランザクション境界を抜けおしたうか、このあたりをちゃんず考えお
曞かないずいけないですね。