1
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を利用するときはpropagationに注意する

Last updated at Posted at 2024-11-01

spring の @Transactional を使用するとき、トランザクションの伝播の動きを制御する propagation というオプションがあります。
これの初期値は Propagation.REQUIRED になっているのですが、これが原因となりハマったので備忘録です。

何がしたかったのか?

DB の内容を処理するメソッドを書いていて、エラーが起きた際にきちんとロールバックされるのかをテストしようとしました。
テストクラスにも @Transactional を付与することでテスト終了時に自動でロールバックされるようにしていました。

テストは途中までデータの更新がされるが最終的に RuntimeException が発生する状態とし、その前後で内容が変わっていないことをもってロールバックされていることを確認しようとしました。
ざっくり以下のような形です。

class SampleService {
    @Transactional
    void methodA() {
        /* DB 操作 */
    }
}

@Transactional
class SampleServiceTest {
    @Autowired
    SampleService service;
    
    @Autowired
    JdbcTemplate jt;

    @Test
    void test() {
        var before = jt.query(...);
        // 何かしらデータを弄るが途中で RuntimeException が発生
        assertThrows(service.methodA());
        var after = jt.query(...);

        // methodA の実行前後で内容が変わっていないことを確認
        assertThat(after).isEqualTo(before);
    }
}

この状態でテストを行うとデータの変更がされた状態になっており、methodA の前後で結果が一致しない (= この時点ではロールバックされていない) となりました。

原因:propagation = Propagation.REQUIRED になっているため

冒頭でも記述した通り @Transactional の propagation の初期値は REQUIRED です。
これは 現在のトランザクションをサポートし、存在しない場合は新しいトランザクションを作成 するという設定です。

つまり上記の書き方の場合、SampleService#methodA() のトランザクションは SampleServiceTest のトランザクションとなります。
そのため

methodA の中で RuntimeException が発生
→ この時点ではロールバックされない
→ テスト側では assertThrows によって methodA が例外を投げることを検証しているので期待通りということで正常に処理継続
assertThat(after).isEqualTo(before); の時点ではロールバックされていないのでテストに失敗

となっていました。

対応:REQUIRES_NEW NESTED にする or テストクラスの @Transactional を外す

REQUIRES_NEW にする NESTED にする

REQUIRES_NEW はトランザクションが存在する場合はそれを一時停止して新しいトランザクションを作成します。
処理内容によっては呼び出し元と同じトランザクションで管理したいということもあるので一概にこの対応ができるわけでは無いですが、そうで無いのであれば REQUIRES_NEW にするのが良いと思います。
今回のメソッドはそもそも別のトランザクションが開いていても別途処理して欲しい内容でしたのでこちらで対応しました。

今回のようなテストを実施したいときは NESTED にすることで内側のトランザクション内でコミット / ロールバックできるようにすることが望ましいのかもしれません。
REQUIRES_NEW の場合は別のトランザクションを開くのでテスト内のトランザクションではテスト対象のメソッド内で行われたコミットの内容が反映されておらず、ロールバックされたように見えていたというだけでした。

テストクラスの @Transactional を外す

処理の都合上 REQUIRED になる場合はテストクラスの @Transactional を外す対応になるのかなと思います。
この場合は @BeforeEach でテストデータを挿入して @AfterEach でテストデータを全て消す、などテスト後にテストデータが残らないように管理する必要があるので多少手間でしょうか。

まとめ

@Transactional(propagation = Proagation.REQUIRED) が付与されたメソッドが例外を投げた時にロールバックされることをテストする際、テストクラスにも @Transactional を付与していたためにロールバックされませんでした。
トランザクションの伝播についての知見不足を知らされる一件でした。

参考

1
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
1
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?