トランザクション中のrescueはロールバックを発生させない
動画による説明
[Ruby on Rails] トランザクション中の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メソッドの中で実行されます。
ここで、与えられたブロックで発生したすべての例外をキャッチして、ロールバックを発生させたあと、例外をもう一度送出します。
activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L313-L318
この動作によって、transactionで例外が発生すると、ロールバックが暗黙的に実行されて、例外の送出も行われます。
Exceptionをキャッチするとロールバックしない
上述の通り、発生した例外をトリガーにしてロールバックが発生します。したがって、先に示したコードのようにブロックの中で例外をキャッチしてしまうと、ロールバックが起きません。
ロールバックしつつ例外をキャッチしたい場合
方法は二通り。明示的にロールバックするか、トランザクションの外側で例外をキャッチする。
明示的にロールバック
こちらの動画で紹介されている方法と同じ考え方です。
https://www.youtube.com/watch?v=jFBvEQhApKQ
ActiveRecord::Rollbackを送出してロールバックを行い、全て正常終了したかどうかを返り値とします。
def exec_transaction
success = true
ApplicationRecord.transaction do
success &= User.new(name: 'Duplicate').save
success &= User.new(name: 'Duplicate').save
unless success do
raise ActiveRecord::Rollback
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アプリケーションにおけるエラー処理(例外設計)の考え方
プロを目指す人のための例外処理(再)入門 / #rubykansai 2018-01-13