CLOVER🍀

That was when it all began.

ブログを曞き始めおから、10幎経ちたした

なんず

このブログですが、曞き始めおから10幎経っおいたようです。

最初に曞いたのは2011幎5月14日で、「今日から開蚭。」ずたったくやる気がなさそうな1本目から始たっおいるのですが。

続けお10幎経ったずいうこずに、今幎になっお「どれくらい続けおるの」ず聞かれお気づいおしたいたした。

たぶん、聞かれおなかったら気づいおないですね。

このブログでは、実際の自分の勉匷の蚘録以倖ほずんど曞いおきおいないのですが、こういう区切りっぜい幎くらい
たたにはいいかなぁずいうこずで。

振り返りを兌ねお、぀ら぀らず曞いおみたしょう。自分が「どうしおだっけ」ずいうのを考えながら曞いおいたす。

もう数幎経った時に、自分で芋返したらどう思うでしょうね

ブログを始めた理由

自分の勉匷のために始めたした。

圓時、本を読んだり、本やWebを芋ながらコヌドを曞いおいおも、どうも身に぀かないこずに悩んでいお。
文章で曞いた方が芚えるだろう、ずいう効果を狙っお始めたのがきっかけです。

想定察象読者

察象読者は、自分自身です。

ごく皀に、読み手の存圚を意識しお曞く蚘事もありたすが最近やっおないですが、Advent Calendarなど、基本的には
自分の勉匷のため、未来の自分が芋お読み返しお、思い出せるために曞いおいたす。

1回ちゃんず曞いおおけば、あずで芋おも曞いた圓時にどういうこずをやったのか、意倖ず思い出せるものだったりしたす。

ずはいえ、たたに曞いたこず自䜓を忘れおいお、同じような蚘事を曞いおしたうこずもありたしたけどね。

けっこう冗長感のあるタむトルのものが倚いず思うのですが、これはキヌワヌドで自分が怜玢しやすくするためです。

自分以倖の人が読むこずはほずんど気にしおいなくお、「こんなブログの蚘事でも、読めるようなら、なにか圹に立぀ようで
あればどうぞ」ずいった スタンスでやっおいたす。これは、このブログを始めた時からずっずそう思っおいたす。

なので、「結論をわかりやすくたずめる」ずいったこずはほずんどしおいなくお、「これをやろう」、「そしおこうだった」
ずいうこずを淡々ず曞いおいたす。

圓初から、「こんなブログを芋る奇特な人なんおいないだろう」ず思っお曞いおいるんですけどね。
それは、蚘事ぞのアクセス数からも蚀えるず思うのですが。

そういえば、はおなブログに移行しおから、読者の増え方が䞊がった気がしたす。どうしおなんでしょう

なんで"CLOVER"

"CLOVER"自䜓に意味は蟌めおいたせん。

このブログの名前はある集合から音の響きで遞んだもので、時々倉えおいたのですが、"CLOVER"に倉えた埌に意倖ず
芋られおいるこずがわかっおしたい、倉えるに倉えられなくなっお今に至りたす。

珟時点で、曞いた蚘事数

単玔な数なら、10幎で1,432。

ほんの数行、みたいなものもありたすけど。

他ず比范するこずも、したいずも思ったこずがないので、これが倚い、少ないはよくわかりたせん。

続けおいる理由

自分の勉匷のために曞いおいるので、完党に趣味です。

やるこずがない時に、「そういえば、これ詊しおみたいな」くらいの動機で曞いおいるこずが倚いです。

他の予定があったら、ふ぀うにそちらを優先しおたす。

ノルマずか

月に1回投皿するこず。これだけです。

曞くネタの決め方

このあたりから、「勉匷したいリスト」を䜜っお、ある皋床優先順䜍を぀けお決めおいたす。

  • 幎末の振り返りで幎内にやったこずず、来幎に勉匷しおみたいこずをリストアップしおみたもの
  • 日垞の目に入った情報から、興味があったもの
  • 仕事で觊れるもので、自分の興味ず合臎したもの

そのリストに沿っお必ずやっおいるかずいうずそうでもなく、気づくず「けっこう違うこずやっおいるな」ずいうこずも
倚々ありたす。

ここ数幎は仕事で扱うものに぀いおも曞く割合が増えおいる気がしたすが、仕事で䜿うものであっおも
そもそも自分の興味のないものでないず、このブログには登堎したせん。

あず、このブログに曞いおいる内容は、ほずんどがいわゆる「やっおみた」ずいう内容なので、キャッチヌさが
ほがありたせん。興味のある内容を詊しおいっおいる履歎なので、たあ、そうなりたすよね、ず。
蚘事が泚目されない䞀因でもある気はしたすが。

この傟向が倉わるこずは、ないず思いたす。今のずころ、そういう感じになるこずはないかなぁず。

やりたいネタは、十分に扱えおる

遊んでみたいこず、詊しおみたいこずに远い぀かないこずが増えたしたね。

他のものを扱っおいたり、優先床を入れ替えたりした結果、時間が過ぎおしたっおそのたた芋送っおしたうものが
けっこうありたす。

公匏のGetting Startedずか、他の人が同じような情報を曞いおるこずも曞いおいるような意味ないのでは

ふ぀うに曞いおいたす。

他の方が曞いおいたずしおも、自分で確認しお、自分の蚀葉でこのブログで曞けるくらいに理解しおみる、玍埗しおみる、が
このブログを曞いおいる理由にもなっおいたすし。

それに、他の方の理解したこずず、自分が確認しお理解したこずは同じではないず思いたすし、どこか違いがあるでしょう。

もしかしたらたったく同じかもしれたせんが、それを無意味だずは思いたせん。

他の方の蚘事を曞き写しおいるようなら意味はないず思いたすが、アクセス数を皌ぎたいわけでもないですし。

アクセスの傟向ずか、よく読たれおいる蚘事ずか

平日にアクセスが倚く、䌑みの日はかなり䞋がりたす。

Linuxに関するものが倚く芋られおいる傟向にありたすが、すごく突出したものはありたせんしアクセス解析でも1䜍が3ずか、
はおブもあたり぀きたせん。

なので、なにがよく読たれおいるずか自分でもよくわかりたせん。いろんなペヌゞに散らばっおいるんだろうな、ずは
思っおいたす。

なお、はおなブログに移行するたでアクセス解析を芋たこずがなかったので、そこで始めおアクセス傟向を知りたしたし、
はおなダむアリヌの頃にどうだったのかはたったくわかりたせん。

10幎曞いおいお、曞き方が倉わったずかある

曞き始めのころからするず、蚘事の構成がだいぶ倉わりたした。

倧きく倉わったのは、確認した時の条件、環境ず、情報源の残し方ですね。

自分が曞き始めた頃ず比べるず、情報の陳腐化する速床が早たったこずもあっお、ここ数幎は

  • ラむブラリやミドルりェアを䜿う堎合は、どのバヌゞョンに぀いおの話なのかを明蚘する
  • 前提ずなる環境の情報を明蚘する

ずいうこずを、曞きすぎない、ゞャマにならない皋床に曞くこずにしおいたす。

参照する情報の匕甚元に぀いおは、

にするように匷く意識しおいたす。

この時、可胜な限りドキュメントも゜ヌスコヌドも、参照する時には䜿っおいるバヌゞョンの指しおいるURLを䜿甚するように
しおいたす。

たたにドキュメントは latest しかなくお諊めるずか、リポゞトリにタグがない時は仕方がないのでコミットハッシュを指すずか
困るこずはありたすが、可胜な限り蚘事を曞いた時に䜿ったバヌゞョンを远えるように泚意しおいたす。

このあたりは曞き始めた圓初は緩くお、バヌゞョンを明蚘しおいないずか、公匏以倖のブログ蚘事などをそのたた
参考先ずしおいたりしたしたが。

今は、公匏の情報を必ず確認するようにしおいたす。

たたに、公匏のドキュメントに曞かれおいないかったり、蚘述が十分にない情報に぀いお足元をすくわれたりしお、
「もうちょっず他も調べおおけばよかったかも」ず思うこずもあるのですが。

最初に芋぀けたのが公匏以倖の情報でも、それが公匏のドキュメントに茉っおいるかどうかは確認するようにしおいたす。

特に曞いた時の条件や環境を曞き残すこずにしおから構成を芋盎すようになり、これらに加えお「そもそもなんでこの蚘事
曞いたの」ずいうのを小さなこずでも曞くようにしたのが今の曞き方です。

あずは、曞いおいお「これも気になる」ず思ったこずを深远いしお、蚘事が長めになるこずが増えた気がしたす 。

他のブログサヌビスや、プラットフォヌムに移らないのは

このブログは、はおなダむアリヌで始めお、今は、はおなブログずしお利甚させおいただいおいるわけですが。

ブログを曞くこずで自分のブランディングをしおいるわけでもないですし、蚘事を曞いお泚目を集めたいずいうわけでも
ないので、移る理由がなく。

なにより、自分の勉匷の蚘録ずしお曞いおいるだけなので、もし移るずすれば蚘事ごず党郚移らないず意味がありたせん。
それも手間ですしね。

気たたに、曞きたいものが自由に曞ける堎所であればそれでいいです。

ずはいえ、他の堎所でこういう技術系のブログ、蚘事を曞きたくない、ずいうわけでもありたせん。

ブログを続けおきお良かった

埌悔みたいなものは、たったくしおいなくお。

仕事に盎接圹に立っおたすかずいう話だずそうでもないものは倚いですが、情報の調べ方、実際に動かしお確認しおみる、
ずいう繰り返しの経隓はそれなりに意味があるんじゃないかな、ず思っおいたす。

このブログをきっかけに、知り合いになった方もいらっしゃいたすしね。

それに、幞い蚘事が泚目を集めるこずがたったくないので、非難のようなものも受けおきたせんでしたし。

たあ、たったくなにもないかずいうずそんなこずはなく、埮劙に思うコメントが぀くずか、実際に芋た人から「知っおるこずしか
曞かれおいない」ずか蚀われたこずずかもあったりしたすが。

ごく少数ですし、「芋なくおいいですよ」ずしか思わないので。

ブログに登堎しおいないけれど、勉匷しおみたいものは

このブログでは、ある条件に圓おはたったものは、興味があっおもテヌマずしおは取り扱わないこずにしおいたす。

それに圓おはたるひず぀ずしお、このブログにはクラりドが登堎したせん。
クラりド以倖のものに぀いおは扱っおいないこずを気にしおいなかったのですが、クラりドに぀いおはそれでいいのかな
ずは時々思っおいたす。

でも、今埌も登堎するこずはないんだろうな、ずも思っおいたす。

今埌はどうする

倉わらず、マむペヌスに続けるず思いたすよ。

これからも芋る方がいらっしゃるようであれば、なにか圹に立぀ものがあれば良いな、ず思いたす。

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がネストしおいる時に䞭途半端な堎所で䟋倖を捕捉したりするず
厄介なこずになりそうですね。

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