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
を付与していたためにロールバックされませんでした。
トランザクションの伝播についての知見不足を知らされる一件でした。
参考