Spring Framework 6.2 になると @Transactionalで検査例外もロールバックするための設定が増える (予定)
Spring Frameworkの @Transactional
は、デフォルトで
「非検査例外(RuntimeException
およびそのサブクラス)ならロールバックする」
という挙動です。(2024/05現在)
Spring Framework 6.2からは設定項目が増え、アプリ全体での設定として「全ての例外でロールバックする」と指定できるようになる見込みなので、現行の挙動の整理とあわせて紹介します。
対象読者
- Spring Frameworkの
@Transactional
でトランザクション制御をしてDBアクセスしている - 次のいずれかにあてはまる
- Kotlinを使っている
- Javaの検査例外のために
rollbackFor
属性を毎回固定で指定している、もしくは検査例外/非検査例外での挙動の違いを気にしていない(不具合リスクに気づいていない)
おことわり
背景
@Transactional をつけたときの動き
Springの @Transaction
は、そのメソッドがエラーになったら(例外がスローされてメソッドを抜けたら)ロールバックする という動きをさせるために使われている。
しかし、EJBの仕様とそろえたという歴史的経緯により、「検査例外はコミットする」「非検査例外はロールバックする」というのがデフォルトの動きとなっている。
例外の種類によらずロールバックさせるためには、
/* Java */
@Transactional(rollbackFor=Exception.class)
public void hoge() throws IOException {
...
}
のように、rollbackFor
属性を毎回指定する必要がある。
これでもいいのだが、最近のJavaは(あるいはSpring自体も)基本的には非検査例外を使うことにしていて、
メソッドに throws
節が付いていないことを確認して、rollbackFor
は特に指定しない という規約にしていることが多いかと思う。
/* Java */
@Transactional
public void hoge() { // throws節がない = 非検査例外しかスローされない = 例外スローされたら必ずロールバックされる
...
}
SpringのKotlinサポートにより発生した問題
Kotlinの言語仕様との相性
Spring FrameworkはKotlinを公式にサポートしている。
Kotlinは言語仕様として「検査例外」「非検査例外」を区別しておらず、throws
節が存在しない。
そのため、例えば次のコードは例外スローされてコミットされるが、メソッドのシグネチャ等から判断することができない。
/* Kotlin */
@Transactional
fun hoge() {
// ... なにかDB書き込みを行う操作...
throw IOException("error") // IOExceptionはJava検査例外
}
検査例外はJava/Kotlin標準ライブラリを代表とする多数の由緒正しいライブラリでスローすることがある。
また、発生原因はディスクIOエラーやネットワークIOエラーなど、発生頻度が低いものが多い。
つまり、Kotlinで予期せぬトランザクション不整合を防ぎたければ、毎回 @Transactional(rollbackFor = [Exception::class])
と rollbackFor
を指定する必要がある。
このことは、Spring FrameworkのIssueとしても2019/08に起票されていた。が、フレームワークとしての対応は長らくされていなかった。
対する Kotlin界隈での知名度
上のことについて サーバーサイドKotlin LT大会 vol.10 というイベントで発表したことがあり、
そのときに「ヤベ、知らなかった」という反応がちらほらあった。
その後、ラクス Advent Calendar 2023 でも 「KotlinにおけるJava検査例外の扱いとSpringの@Transactionalの罠」として、同様の内容の記事が書かれていた。
Java + Spring Framework の組み合わせは鉄板であるが、Kotlin の場合は他のフレームワークを使うことも多くSpringの話題は多くないこともあってか、
Kotlin + Spring Transaction で注意が必要な点は、そこそこ知られているがまだ注意喚起が必要 といった感覚である。
新しい設定の登場
2019/08の起票なので5年越しの解決になるが、2024/11リリース予定の Spring Framework 6.2 に向けてPull Requestがマージされた。
これにより、アプリ全体でのロールバック判定方法を
- 非検査例外をロールバック(これまで通り、6.2でもデフォルト)
- 全ての例外をロールバック
の二択で選択できるようになる。
使い方
残念ながらSpring Bootでの自動構成の方法はまだ議論されていないようであった。
そのため、自分でアノテーションで指定する方法を記載する。
といっても簡単で、@EnableTransactionManagement
の rollbackOn
属性値を1つ指定するだけである。
org.springframework.transaction.annotation.RollbackOn
という enum
があるのでそれを使い指定する。 上で述べた二択がそれぞれ、
RollbackOn.RUNTIME_EXCEPTIONS
RollbackOn.ALL_EXCEPTIONS
と対応する。
/* Java */
@Configuration
@EnableTransactionManagement(rollbackOn =RollbackOn.ALL_EXCEPTIONS)
public class AppConfig {}
/* kotlin */
@Configuration
@EnableTransactionManagement(rollbackOn = RollbackOn.ALL_EXCEPTIONS)
class AppConfig
ただし、Spring Boot の自動構成の動きに合わせるなら、 proxyTargetClass = true
とCGLibを使う設定が入っているので合わせるのが無難と思う。
(※ここで指定をしなくても、他の設定でどこか1箇所でも true
なら全体が true
になる。)
/* Java */
@Configuration
@EnableTransactionManagement(
proxyTargetClass = true, // CGLibプロキシを利用する
rollbackOn = RollbackOn.ALL_EXCEPTIONS // 全ての例外でロールバック
)
public class AppConfig {}
/* kotlin */
@Configuration
@EnableTransactionManagement(
proxyTargetClass = true,
rollbackOn = RollbackOn.ALL_EXCEPTIONS
)
class AppConfig
なお、現行バージョンである Spring Boot 3.2 では、TransactionAutoConfiguration で @EnableTransactionManagement
の設定をしているので、 Spring Boot 3.4 ではこのあたりの実装が変わるのではないかと思われる。
感想
これまでも、@Transactional(rollbackFor=Exception.class)
と必ず rollbackFor
を指定するルールとしていれば不具合を避けることができていました。
また、TransactionAttributeSource
をBean登録することで全体設定を自前で指定するワークアラウンドも前述のIssueには紹介されていました。
そのため、対応の優先度も低く5年かかったのでしょう。
しかし、トランザクション制御というとても気を遣う部分で(EJBに親しみのない身としては)不自然と感じるルールで、毎回 rollbackFor
を指定するのは煩わしくまた忘れそうだと思っていました。
TransactionAttributeSource
をBean登録する方法でも、今度は細かい制御が効かなくなったり、メジャーアップデート時に破壊的変更が入るリスクがあったりしたので、
公式で解決方法を用意してくれたのは助かります。
欲を言うと...
あまり話を聞きませんが、@Transactional
は例外によるロールバックのほかに、vavr を使った宣言的なトランザクション制御も対応しています。
ただ、
- vavrが開発停滞して代わりのメンテナを探している状態である
- Kotlin向けとしては arrow の方が名前を聞く
など合って、そのあたりまで手が入るかと期待した部分もありましたが…さすがに過剰でしたね。
Discussion