はじめに (対象読者・この記事でわかること)

この記事は、Spring Frameworkで宣言的トランザクション管理(@Transactional)を使っているけど「トランザクションが効いていない気がする」「ロールバックされない」というJava開発者向けです。
特に「アノテーションは付けたはずなのに、意図したタイミングでコミット・ロールバックされない」「同じクラス内のメソッドから呼び出してもトランザクションが継承されない」といった症状で悩んでいる方に読んでいただきたい内容です。

この記事を読むことで、Springのトランザクションプロキシの仕組みの概要、アノテーションが効かない代表的なケース、切り分けに使えるデバッグ手順と対処法が身に付きます。サンプルコードと共に「なぜ動かないのか」を原理原則から理解し、次から同様のハマりどころを自力で解決できるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Java / Spring Bootの基礎(DIコンテナ、Bean登録) - データベーストランザクションの基礎(ACID、コミット、ロールバック) - AOP(アスペクト指向プログラミング)の概念をざっくり理解していると尚良い

Spring宣言的トランザクションの仕組みと「効かない」の正体

Springが@Transactionalを見て自動的にトランザクションを開始・終了する仕組みは「プロキシベースのAOP」に依存しています。つまり対象Beanを継承/実装したプロキシオブジェクトを生成し、メソッド呼び出しの前後でTransactionInterceptorがトランザクション制御を行います。

「効かない」原因の多くはこのプロキシの「縁の下の力」に起因します。代表的な落とし穴は下記の3つです。

  1. 同一クラス内メソッド呼び出し(this呼び出し)
    プロキシ経由でないためインターセプトされない
  2. 非publicメソッド
    Spring AOPのデフォルト設定ではpublicのみ対象
  3. 例外キャッチ・スロー戦略の齟齬
    宣言したロールバック対象外例外をcatchして無視している、あるいはチェック例外をスローしているがロールバック設定がRuntimeExceptionのみ

このセクションで「なぜプロキシが関係するのか」を押さえたうえで、次章で実際に検証・対策を行います。

実践:@Transactionalが効かないパターンを再現して解決する

以下、Spring Boot 3系を前提に「同一クラス内呼び出し」でトランザクションが効かない例を再現し、解決策を示します。コード量を減らすためSpring Data JPAとH2(インメモリ)を使います。

ステップ1 問題の再現

@SpringBootApplicationクラスと同階層にサービスクラスを作り、save→updateの2メソッドを同一クラス内で呼びます。

Java
@Service public class ItemService { @Autowired ItemRepository repo; public void createAndUpdate(String name){ save(name); // 1. 保存 update(name + "X"); // 2. 同一クラス内でupdate呼び出し } @Transactional // ← ここが効かない public void save(String name){ repo.save(new Item(name)); } @Transactional public void update(String name){ Item item = repo.findByName(name.substring(0, name.length()-1)); item.setName(name); if(true) throw new RuntimeException("強制例外"); // ロールバック期待 } }

呼び出し側(例:コマンドラインランナー)でitemService.createAndUpdate("book")とすると、RuntimeExceptionが投げられたにもかかわらずItem(book)がDBに残ってしまいます。トランザクションが効いていない=ロールバックされていない証拠です。

ステップ2 切り分け:トランザクションが有効かを確かめる

  1. ログレベルをDEBUGにしてorg.springframework.jdbc.datasource.DataSourceTransactionManagerJpaTransactionManagerのログを確認
    "Creating new transaction"/"Participating in existing transaction"が出ていなければ開始されていない
  2. デバッグ用にTransactionSynchronizationManager.isActualTransactionActive()を埋め込む
    同一クラス内呼び出しの場合falseになる
Java
public void save(String name){ System.out.println("TX active = " + TransactionSynchronizationManager.isActualTransactionActive()); repo.save(new Item(name)); }

ハマった点とエラー解決

ハマりどころA:this呼び出し

Spring AOPはプロキシ経由でしかインターセプトしないため、内部メソッド呼び出しではトランザション境界が張られない。

ハマりどころB:非publicメソッド

標準設定では@Transactionalを付けてもprotected/private/package-privateメソッドは無視される。(@EnableTransactionManagement(proxyTargetClass=true)にしても同様)

ハマりどころC:自己注入(Self Injection)の落とし穴

@Transactionalを外側に持ったメソッドを別インターフェースとして切り出し、自己注入すると解決するが、Lombok+ファイナルフィールド注入の組み合わせで循環依存エラーが出る

解決策

  1. 自己注入パターンでトランザクション境界を外側に移動
    インターフェースを切って自己注入し、プロキシ経由で呼ぶ
Java
@Service public class ItemService implements ItemCreator { @Autowired ItemRepository repo; @Autowired ItemCreator self; // 自己注入 public void createAndUpdate(String name){ self.save(name); // プロキシ経由 self.update(name + "X"); } @Override @Transactional public void save(String name){ ... } @Override @Transactional(rollbackFor = RuntimeException.class) public void update(String name){ ... } } interface ItemCreator { void save(String name); void update(String name); }
  1. AOPプロキシを強制的に作成し、publicメソッドに限定する
    もしprotectedにしたい場合は@EnableTransactionManagement(mode=AdviceMode.ASPECTJ)でネイティブ織込みに切り替える(ただしLTW設定が必要)

  2. トランザクションを分離してトランザクション伝播ルールを整理する
    REQUIRED/REQUIRES_NEWを使い分け、内部で別トランザクションにしたい場合は@Transactional(propagation = Propagation.REQUIRES_NEW)

  3. ロールバックルールを明示する
    チェック例外をスローしたい場合は@Transactional(rollbackFor = Exception.class)と書く

この例では1番目の自己注入パターンを採用すると、トランザクションが有効になり例外発生時にsaveもロールバックされることが確認できます。

まとめ

本記事では、Springの@Transactionalが「効かない」原因の筆頭である「同一クラス内メソッド呼び出し」とその対策を実践的に解説しました。

  • Spring AOPはプロキシ経由でのみトランザクションインターセプトを行う
  • this呼び出しや非publicメソッドでは@Transactionalが無視される
  • 自己注入・トランザクション分離・ロールバックルールの明示で安全に制御できる

この切り分け手順を頭に入れておけば、次から「トランザクションが効いていない」現場で即座に原因を絞り、修正方針を立案できます。
さらなる発展として、分散トランザクション(JTA)やトランザクション監視ロギックの実装、Reactive Streamsにおけるトランザクション管理(TransactionalOperator)についても掘り下げていく予定です。

参考資料

  • Spring Framework公式ドキュメント "Declarative Transaction Management"
    https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html
  • 日本Spring User Group 和訳ガイド
    https://spring.pleiades.io/guides/gs/managing-transactions/
  • 山田浩明・坂田勉「Spring徹底入門 第3版」(翔泳社)