Dev Study
← 解説「@Transactional はプロキシとスレッドでできている」に戻る

サンプルコードで身につける: @Transactional はプロキシとスレッドでできている

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1コンテナが渡してくる Bean のクラス名

から取り出した の getClass().getName() を表示した結果です。AccountService と書いたの Bean は AccountService$$SpringCGLIB$$0 で、自分が書いたクラスそのものではありません。3行目は、何も始めていない ではが無いことを示しています。

起動直後の出力($Proxy に続く数字は実行で変わる)コマンド出力
AccountService bean class = l21.tx.AccountService$$SpringCGLIB$$0
AccountRepository bean class = l21.tx.$Proxy101
main: txActive=false thread=main

2素の Java で: ThreadLocal は別のスレッドから見えない

だけを取り出した模型です。main スレッドで入れた値は、引数で渡さなくても同じの中ならどこからでも取り出せますが、新しく起こした worker-1 からは null に見えます。 がトランザクションをスレッドに結び付けるときに使っているのも、この ThreadLocal です。

Java

3例外の種類で、行が残るかどうかが変わる

保存した直後にを投げるを3つ呼び、そのつど全行を表示しました。 では行が残らず、の では checked の行が残っています。rollbackFor = Exception.class を付けた3番では、同じ IOException でも行は増えていません(3番の rows に見える checked は2番で残った行です)。

実行結果(Spring Framework 7.0.9、DB は H2)コマンド出力
--- 1. RuntimeException
threw java.lang.IllegalStateException: unchecked
rows = []
--- 2. checked exception (IOException)
threw java.io.IOException: checked
rows = [checked]
--- 3. checked exception + rollbackFor=Exception.class
threw java.io.IOException: checked
rows = [checked]
@Transactional
public void saveThenRuntimeException(String owner) {
    repo.save(new Account(owner));
    throw new IllegalStateException("unchecked");      // 非検査例外
}

@Transactional
public void saveThenCheckedException(String owner) throws IOException {
    repo.save(new Account(owner));
    throw new IOException("checked");                  // 検査例外
}

@Transactional(rollbackFor = Exception.class)
public void saveThenCheckedExceptionRollbackFor(String owner) throws IOException {
    repo.save(new Account(owner));
    throw new IOException("checked");
}

// 呼び出し側(owner に渡した文字列が rows に出る)
// service.saveThenRuntimeException("runtime");
// service.saveThenCheckedException("checked");
// service.saveThenCheckedExceptionRollbackFor("checked-rollbackFor");

4別のスレッドにはトランザクションが引き継がれない

のメソッドの中でスレッドを起こし、両方で保存してから例外を投げました。呼び出し側のスレッドには2件の資源が結び付いていますが、worker-1 では txActive=false、resources=0 で、巻き戻ったのは呼び出し側の行だけでした。thread-spawned が残るのは、 4.1.1 の が save に @Transactional を付けていて、呼び出し側にトランザクションが無いと自分で始めてコミットするためです(DEBUG ログでも worker-1 の上で save の名前のトランザクションが始まり、コミットされました)。

service.saveInAnotherThread("thread") の結果(checked と via-this は前の実験で残った行)追跡ログ
--- 6. save from another thread inside a transaction
caller: txActive=true thread=main
bound resource: $Proxy94 -> EntityManagerHolder
bound resource: HikariDataSource -> ConnectionHolder
spawned: txActive=false thread=worker-1 resources=0
threw java.lang.IllegalStateException: unchecked
rows = [checked, via-this, thread-spawned]
@Transactional
public void saveInAnotherThread(String owner) throws InterruptedException {
    System.out.println("    caller:  " + tx());
    TransactionSynchronizationManager.getResourceMap().forEach((k, v) ->
            System.out.println("    bound resource: " + k.getClass().getSimpleName() + " -> " + v.getClass().getSimpleName()));
    repo.save(new Account(owner + "-caller"));
    Thread t = new Thread(() -> {
        System.out.println("    spawned: " + tx() + " resources=" + TransactionSynchronizationManager.getResourceMap().size());
        repo.save(new Account(owner + "-spawned"));
    }, "worker-1");
    t.start();
    t.join();
    throw new IllegalStateException("unchecked");
}

5Spring のソースで確かめる

ここまでの2つの挙動は、 7.0.9 のソースの短い箇所に対応しています。上は「どの例外で巻き戻すか」の既定で RuntimeException と Error だけが true になり、下はトランザクションの資源を置く場所の宣言で型が ThreadLocal です。なお、な方式では ThreadLocal ではなく を使うとリファレンスにあります。

// spring-tx 7.0.9
// org/springframework/transaction/interceptor/DefaultTransactionAttribute.java(180〜182行)
	public boolean rollbackOn(Throwable ex) {
		return (ex instanceof RuntimeException || ex instanceof Error);
	}

// org/springframework/transaction/support/TransactionSynchronizationManager.java(抜粋。間の宣言は省略)
	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");
	// ...
	private static final ThreadLocal<Boolean> actualTransactionActive =
			new NamedThreadLocal<>("Actual transaction active");

図やコードに出てくる用語(タップで説明)