Dev Study
← Spring コース

54. @Transactional はプロキシとスレッドでできている

を付けたを持つの は、実体ではなくです。実測では AccountService$$SpringCGLIB$$0 というクラスでした。外からメソッドを呼ぶと、プロキシが先にを始め、戻るときにコミットします。始めたトランザクションと DB 接続は、(ごとに別の値を持つ変数)で今のスレッドに結び付けられます。

この形なら、接続を引数で渡して回らなくても、同じスレッドで動く処理はみな同じトランザクションに入れます。1リクエストを1本のスレッドが受け持つ作りと噛み合っています。代わりに、メソッドの中で別のスレッドを起こすと、そちらには引き継がれません。実測でも、起こしたスレッドではトランザクションが無い状態で、そこで保存した行は呼び出し側が巻き戻しても残りました。

つまずきどころは2つです。1つは プロキシの回と同じで、同じクラスの中から this 経由で呼ぶとプロキシを通らず、トランザクションは始まりません。もう1つはの種類で、既定で巻き戻るのは と Error だけです。 のような( での宣言をコンパイラが求める例外)を投げたメソッドは、コミットされて行が残りました。

点線の付いた語はタップすると説明が出ます。用語集を開く →

外からの呼び出しだけがプロキシを通る(Spring Framework 7.0.9 での実測をもとに作図)
外からの呼び出しservice.innerWithTx(...)
コンテナが渡した参照はプロキシ
プロキシAccountService$$SpringCGLIB$$0
トランザクションを始める
今のスレッドに結び付ける
ThreadLocal(スレッド main)EntityManagerHolder と ConnectionHolder の2件
別のスレッド worker-1結び付いた物は 0 件
実体のメソッドを呼ぶ
AccountService の実体innerWithTx の本体。txActive=true
ここから this.xxx() と呼んでもプロキシには戻らない
戻りをプロキシが受ける
正常終了・検査例外コミット
RuntimeException・Errorロールバック
  • ・検査例外でコミットされるのは Spring の既定です。rollbackFor = Exception.class を付けると検査例外でも巻き戻りました
  • ・ここで扱ったのはスレッドに結び付く方式です。リアクティブな方式では ThreadLocal ではなく Reactor のコンテキストを使うとリファレンスにあります
  • ・リポジトリの Bean も l21.tx.$Proxy101 というプロキシでした($Proxy に続く数字は実行で変わります)

サンプルコード(フレームワーク環境が必要なため表示のみ)

// TxApp.java(抜粋)— Spring Boot 4.1.1 / Spring Framework 7.0.9、DB は H2
@Service
class AccountService {
    private final AccountRepository repo;
    AccountService(AccountRepository repo) { this.repo = repo; }

    static String tx() {
        return "txActive=" + TransactionSynchronizationManager.isActualTransactionActive()
                + " thread=" + Thread.currentThread().getName();
    }

    public void outerWithoutTx(String owner) {
        System.out.println("    outerWithoutTx: " + tx());
        this.innerWithTx(owner);            // プロキシではなく this を通る呼び出し
    }

    @Transactional
    public void innerWithTx(String owner) {
        System.out.println("    innerWithTx:    " + tx());
        repo.save(new Account(owner));
        throw new IllegalStateException("unchecked");
    }
}

// main から順に呼び、そのつど全行の owner を表示した結果(rows の checked は前の実験で残った行)
//
// AccountService bean class = l21.tx.AccountService$$SpringCGLIB$$0
// --- 4. innerWithTx called from outside (through the proxy)
//     innerWithTx:    txActive=true thread=main
//     threw java.lang.IllegalStateException: unchecked
//     rows = [checked]
// --- 5. innerWithTx called via this.innerWithTx (self-invocation)
//     outerWithoutTx: txActive=false thread=main
//     innerWithTx:    txActive=false thread=main
//     threw java.lang.IllegalStateException: unchecked
//     rows = [checked, via-this]

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

サンプルコードで身につける(5本) →

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

公式ドキュメントで詳しく ↗