マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

トランザクション中にrescueするとロールバックしないので注意!

トランザクション中のrescueはロールバックを発生させない

動画による説明

www.youtube.com

トランザクション中の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メソッドの中で実行されます。 ここで、与えられたブロックで発生したすべての例外をキャッチして、ロールバックを発生させたあと、例外をもう一度送出します。

https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L313-L318

この動作によって、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