サンプルコードで身につける: @Transactional はプロキシとスレッドでできている
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1コンテナが渡してくる Bean のクラス名
から取り出した の getClass().getName() を表示した結果です。AccountService と書いたの Bean は AccountService$$SpringCGLIB$$0 で、自分が書いたクラスそのものではありません。3行目は、何も始めていない ではが無いことを示しています。
AccountService bean class = l21.tx.AccountService$$SpringCGLIB$$0AccountRepository bean class = l21.tx.$Proxy101main: txActive=false thread=main
2素の Java で: ThreadLocal は別のスレッドから見えない
だけを取り出した模型です。main スレッドで入れた値は、引数で渡さなくても同じの中ならどこからでも取り出せますが、新しく起こした worker-1 からは null に見えます。 がトランザクションをスレッドに結び付けるときに使っているのも、この ThreadLocal です。
3例外の種類で、行が残るかどうかが変わる
保存した直後にを投げるを3つ呼び、そのつど全行を表示しました。 では行が残らず、の では checked の行が残っています。rollbackFor = Exception.class を付けた3番では、同じ IOException でも行は増えていません(3番の rows に見える checked は2番で残った行です)。
--- 1. RuntimeExceptionthrew java.lang.IllegalStateException: uncheckedrows = []--- 2. checked exception (IOException)threw java.io.IOException: checkedrows = [checked]--- 3. checked exception + rollbackFor=Exception.classthrew java.io.IOException: checkedrows = [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 の名前のトランザクションが始まり、コミットされました)。
--- 6. save from another thread inside a transactioncaller: txActive=true thread=mainbound resource: $Proxy94 -> EntityManagerHolderbound resource: HikariDataSource -> ConnectionHolderspawned: txActive=false thread=worker-1 resources=0threw java.lang.IllegalStateException: uncheckedrows = [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");