transaction doの中でrescueを使った時に起こったことと調査メモ

RailsでDB操作を行う際に、複数の状態を変更を全部成功できるようにを使用することがあります。

基本的な使い方

上記のコードは、user、post、commentを一括して保存します。
いずれかの保存が失敗した場合、全ての変更がロールバックされます。
どれかだけが成功する、といったことが起こらないので便利です。
 
ロールバックとは:データ更新などでエラーが起こったときに、その前の状態にまで戻ること

例外処理の注意点

今回、実体験をもとに記事で取り上げようと思うのは、ブロック内で例外が発生した時に、rescueを実装したら予想していない動作をしたことです。
 
具体的には、rescueをロールバック処理内に書いてしまったせいで、ロールバックをすると思ったらしなかったという内容です。
 
例えば、このようなコードを書いたとします。
期待したこと:エラーログの表示とともに各データが保存されない
起きたこと:エラーログは表示され、各データが保存された
 

なぜそうなるのか

rails実装を読みつつ、pry-byebugを使って調査しました。
 
検証環境
 
ざっと関連しそうな記事やドキュメントを見てみると、以下の箇所が関係していそうでした。
 
読んでいきます。

1. トランザクションの開始

ここでトランザクションが開始されていそうです。そのトランザクションオブジェクトをに代入します。オプションは、トランザクションの分離レベルを指し、オプションは、トランザクションがネスト可能かどうかを指定するそうです。
 

2.ensureに捕捉

ensureは必ず実行されるので、 の条件分岐に入ります。今回はerrorはnilなので、そのまま次のブロックに進みます。
 
 

3. if Thread.current.status == "aborting"

Thread.current.statusはスレッドの状態を示しており、rescue内で処理されている状態のはabortingではなくrunなので、の方の処理に入ります。
 

4. elseの中の commit_transaction

 
ここで、commit_transactionされてしまうので、例外が発生したにも関わらず、 で処理してしまったため、トランザクションは成功したとみなされていそうです。
 

解決策

トランザクション内で例外をキャッチした上でロールバックさせたい場合、対策の1つとして以下のような方法があります。
 

例外を再スローする

 
これで、5.のensureブロックで以下の部分に入ってロールバックが起こります。
 

transaction外でrescueする

 
こちらの方が今回やりたかったことでした。