0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring @Transactionalのself-invocation問題

0
Last updated at Posted at 2026-02-23
  • 執筆は自分で行い、翻訳にはAIを使用しました
  • 現在日本語を勉強しています。翻訳内容を確認しましたが、もし間違いや不自然な表現があれば教えてください
  • 内容についてのフィードバックも歓迎です

前の記事

前の記事で@Transactionalがなぜprivateメソッドで動作しないかを見ました。
その記事の延長線として、Self-Invocationの問題についてもまとめてみました。

問題の状況

  • クラスの内部から@Transactionalを呼び出すとどうなるのか?
  • 正常な呼び出しと、問題のあるSelf-Invocationの状況を作ってみました

image.png

image.png

@Transactionalの自己呼び出し(実質的にターゲットオブジェクト内のメソッドが同じオブジェクト内の別のメソッドを呼び出すこと)は、実行時に実際のトランザクションを発生させません

IntelliJでも前回と同様に警告が表示され、答えを教えてくれます。

なぜ発生するのか?

前の記事で@Transactionalはプロキシがメソッドの呼び出しをインターセプトして動作すると整理しました。
Self-Invocation問題の核心は、同じクラスの内部から呼び出すとプロキシを経由しないという点です。

 @Transactional
    public void saveInternalCall(String tag) {
        hashtagRepository.save(Hashtag.create(tag));
        throw new RuntimeException("RollBack!");
    }

    public void saveViaSelfInvocation(String tag) {
        this.saveInternalCall(tag);
    }
  • saveViaSelfInvocation()からthis.saveInternalCall()を呼び出すと
  • thisはプロキシではなく実際のオブジェクトを指します
  • つまり、TransactionInterceptorが介入できず、トランザクションは動作しません

In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.

プロキシモード(デフォルト)では、プロキシを通じて入ってくる外部メソッドの呼び出しのみがインターセプトされます。つまり、Self-Invocation(ターゲットオブジェクト内のメソッドが同じオブジェクトの別のメソッドを呼び出すこと)は、呼び出されるメソッドに@Transactionalが付いていても、実行時に実際のトランザクションは動作しません。

  • Spring公式ドキュメントでもこの問題を明確に説明しています

テスト結果

image.png

前回と同様に

  1. 外部から直接呼び出し:@Transactionalが動作する
  2. 内部からthisで呼び出し:@Transactionalが動作しない

この2つを検証してみます。

image.png

テストは両方とも通過しました。
これにより、動作について検証されました。

結果分析

外部から直接呼び出し

Hibernate: delete from hashtags where id=?
2026-02-24 02:51:49.948 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [com.example.demo.blog.TestService.saveDirectCall]
2026-02-24 02:51:49.948 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:51:49.950 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2026-02-24 02:51:49.950 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [com.example.demo.blog.TestService.saveDirectCall] after exception: java.lang.RuntimeException: RollBack!
2026-02-24 02:51:49.954 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByTag]: This method is not transactional.
Hibernate: select h1_0.id,h1_0.created_at,h1_0.tag,h1_0.updated_at,h1_0.usage_count from hashtags h1_0 where h1_0.tag=?
2026-02-24 02:51:49.948 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [com.example.demo.blog.TestService.saveDirectCall]
  • TestService(プロキシ)がトランザクションを開きました
2026-02-24 02:51:49.948 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
  • SimpleJpaRepositorysave()がサービスのトランザクションに参加します
2026-02-24 02:51:49.950 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [com.example.demo.blog.TestService.saveDirectCall] after exception: java.lang.RuntimeException: RollBack!
  • 例外を検知してトランザクション全体をロールバックします。save()のinsertも取り消されます

内部からthisで呼び出し

2026-02-24 02:51:49.821 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:51:49.853 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2026-02-24 02:51:49.875 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByTag]: This method is not transactional.
Hibernate: select h1_0.id,h1_0.created_at,h1_0.tag,h1_0.updated_at,h1_0.usage_count from hashtags h1_0 where h1_0.tag=?
2026-02-24 02:51:49.821 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
Hibernate: insert into hashtags (created_at,tag,updated_at,usage_count) values (?,?,?,?)
2026-02-24 02:51:49.853 [Test worker] [] [userId=anonymous] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
  • save()が自らトランザクションを開いてコミットしてしまいます
  • ログのどこにも例外関連のログがありません
  • したがって、プロキシが生成されておらず、データが残っていることがわかります

解決策

  • 解決策については次の記事に書きたいと思います
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?