🐈

Rails トランザクションの挙動・注意点について

2021/12/13に公開

本記事では、Railsのトランザクションの使い方、挙動、注意点についてまとめています!
基本的にトランザクションで囲いたい処理はDBに手を加える場合が多いと思います。一気に行いたい処理が途中までしか処理が行われなかったなどあるとなかなか面倒です、、
トランザクションの挙動についてしっかり理解して使用していきたいと思いました。
特に本記事でお伝えしたいのは注意点です! 個人的にトランザクションについて理解が乏しい状態で使用し、ハマったところをお伝えできればと思っています

トランザクションとは

トランザクションとは複数の処理をまとめて1つの大きな処理として扱う機能です。
複数のリソースにまたがった変更をひとまとまりで扱う時、処理の1つで例外が発生したら、処理全体を巻き戻すことができます。

例:処理A、B、Cを同時に行いたい!一つでもかけたらダメなんだよな、、
  処理A、B、Cをトランザクションで囲おう!

def exec_transaction
  ApplicationRecord.transaction do
    処理A  # ← 成功!
    処理B  # ← 成功!
    処理C  # ← 失敗!
  end
end

処理A、Bは上手くいったけど、Cはダメだった、、ここで処理Cで例外を発生させれば処理A、B、Cは全て無かったことに!!

使用方法

ApplicationRecord.transaction do
  # 例外が発生するかもしれない処理
end

使用方法は簡単、処理を囲うだけ!!

トランザクション仕組み

transactionメソッドに与えられたブロックは、最終的にwithin_new_transactionメソッドの中で実行!
ここで、与えられたブロックで発生した例外をキャッチして、ロールバックを発生させたあと、例外をもう一度送出する。

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L313-L318

まとめると、、

  1. トランザクション内で例外が発生した時点でその例外をキャッチ
  2. ロールバック
  3. 発生した例外を返す

※注意点

今回お伝えしたいのはここ!!トランザクションを行うにあたっての注意点です!
以下では当たり前のことを書いているかもしれませんが、挙動を理解していないと思わぬ落とし穴にはまってしまうかもしれません

・トランザクションブロック内の処理が例外を起こさない限りロールバック処理は行われない

ApplicationRecord.transaction do
  A.save
  B.save
end

上記のコードでは、A、Bのどちらかのsaveに失敗しても返り値falseが返るだけで、例外は発生せず、ロールバックも行われません

・ブロック内で例外が発生した時点でロールバックする → 例外発生のその後の処理は行われない

ApplicationRecord.transaction do
  p "処理A"
  p "処理A"
  raise 例外クラス
  p "処理C"
end

"処理A"
"処理B"
=> 例外発生

上記のコードでは、例外は発生後の、「p "処理C"」の処理は行われません

・ブロック内でrescueで例外をキャッチしてしまうとロールバックしない

def exec_transaction
  ApplicationRecord.transaction do
    user_1 = User.create!(name: 'Duplicate')
    user_2 = User.create!(name: 'Duplicate') <=ここで例外
  rescue ActiveRecord::RecordInvalid
    false
  end
end

上記のコードのようにブロック内でrescueで例外をキャッチしてしまうとロールバックは行われません。結果的にuser_1のみ作成され、返り値にfalseが返ります。

番外編

・ロールバックしつつ例外をキャッチしたい場合

https://qiita.com/ytnk531/items/a0db31ee4311425a3933#ロールバックしつつ例外をキャッチしたい場合

・処理を全て実行後、エラー箇所を出力したい場合

ApplicationRecord.transaction do
  save_faild_users = users.each_with_object([]) do |user, array|
    user.name = 'Invalid'
    array.push(user) unless user.save
  end

  if save_faild_users.present?
    save_faild_users.each { |user| p user }
    raise ActiveRecord::RecordInvalid
  end
end

少々荒っぽいですが、一旦処理は最後まで実行して、最後に不正な箇所を一編に出力したい時などはこういった方法が良いでしょう

最後に

トランザクションは、複数の処理や変更をひとまとまりで扱い、処理の1つで例外が発生したら、処理を巻き戻すことができます。
予期せぬバグを未然に防げ、安全に処理を行うことができます。

トランザクションの挙動を理解し、安全安心に処理を実行していきましょう!

Discussion