トランザクション中にrescueするとロールバックしないので注意!
トランザクション中のrescueはロールバックを発生させない
動画による説明
トランザクション中のrescue
このようにすると、create!
で発生した例外をキャッチして、exec_transaction
の返り値としてfalseを返すことができます。
def exec_transaction ApplicationRecord.transaction do User.create!(name: 'Duplicate') User.create!(name: 'Duplicate') rescue ActiveRecord::RecordInvalid false end end
Userモデルは下記のようにバリデーションが設定してあります。
class User < ApplicationRecord validates :name, uniqueness: true end
このコードを実行すると、二回目のcreate!でエラーが発生しますが、ロールバック処理が実行されません。
Rollbackが起きる仕組み
transactionメソッドに与えられたブロックは、最終的にwithin_new_transactionメソッドの中で実行されます。 ここで、与えられたブロックで発生したすべての例外をキャッチして、ロールバックを発生させたあと、例外をもう一度送出します。
この動作によって、transactionで例外が発生すると、ロールバックが暗黙的に実行されて、例外の創出も行われます。
Exceptionをキャッチするとロールバックしない
上述の通り、発生した例外をトリガーにしてロールバックが発生します。したがって、先に示したコードのようにブロックの中で例外をキャッチしてしまうと、ロールバックが起きません。
ロールバックしつつ例外をキャッチしたい場合
方法は二通り。明示的にロールバックするか、トランザクションの外側で例外をキャッチする。
明示的にロールバック
こちらの動画で紹介されている方法と同じ考え方です。 https://www.youtube.com/watch?v=jFBvEQhApKQ
def exec_transaction success = true ApplicationRecord.transaction do success &= User.create(name: 'Duplicate') success &= User.create(name: 'Duplicate') unless success do rescue ActiveRecord::RecordInvalid end end success end
トランザクションの外側で例外をキャッチする
ロールバックが発生したら例外は再度送出される、という性質を利用します。
def exec_transaction ApplicationRecord.transaction do User.create!(name: 'Duplicate') User.create!(name: 'Duplicate') end rescue ActiveRecord::RecordInvalid false end
そもそもの例外処理の設計
こういう例外処理がある事自体が良くない可能性もあります。もちろん、すべてが悪いということではないです。 エラー処理の設計については、いとうじゅんいちさんが非常に丁寧にまとめてくれています。
Railsアプリケーションにおけるエラー処理(例外設計)の考え方 - Qiita
プロを目指す人のための例外処理(再)入門 / #rubykansai 2018-01-13 - Speaker Deck