Rails

RailsでRollbackしたあとにエラーメッセージを返したい

複数件の同じようなデータをインサート/アップデートしたいときに全件成功時のみ保存してnilを返す。
失敗時は全件ロールバックしたうえでエラーメッセージが取得したい…というケースがあった。

このやり方がわからなくて滅茶苦茶困った&PHPのときと同じ考えをしていて大変苦労した。

解決方法:

結論、トランザクションのblock外に変数と返り値を設定すればOK

何故かというとActiveRecord::Rollbackするとtransactionブロックを抜けてしまうから。
よって値を返したい場合はtransactionブロック外でreturnに相当する実装を行えばよい。

今回の実装ではエラーメッセージ、もしくはnilが返るようになっている。
実行検証はしてないので動かなかったら、すまんな。
イメージはこんな感じということで許してくれ。

def save
  errors = []
  ActiveRecord::Base.transaction do
    [[:name => "Alice"], [:name => "Bob"], [:name => "Charlie"]].each do |row|
      person = Person.new(row)
      if !person.save
        errors << person.errors.full_messages
      end
    end
    raise ActiveRecord::Rollback if errors.present?
  end
  # ここが重要だった
  errors.presence || nil
end

勘違いしていたこと:

PHPのようにActiveRecord::Rollback("メッセージ")みたいなことが出来る

実際は出来ない。Syntax Errorになる。

rescue ActiveRecord::Rollbackでキャッチする

rescue節を追加してロールバック後にエラーメッセージ返すのだろうと思っていた、PHPなどではありがちな手法なので。
ところがdef ~ rescue ~ endをしても実際にはキャッチできなくてめちゃくちゃ困惑した。

ロールバックしたあとに返り値を指定できる

そんなことはなかった、ロールバック後nilが返っていた。
↓な感じにコードを書いたら動かないかな?と薄い期待をしていた。
当たり前だがロールバックは実行されない、return errorsは実行されるので失敗しているものは保存されず、成功しているものは保存されてしまうという最悪な状態になる。

ActiveRecord::Rollback
return errors

まとめ

他言語の仕様での考えが強くロールバックするとトランザクションブロックを抜ける…という基礎的な部分を理解していなかった。
ロールバック実行後はメソッドを抜けてしまうものだとばかり考えていたが実際にはそうでないという言われてみればごくごく当たり前の話しに気づくことができず苦労した。

当たり前すぎるのかあまりそういう情報がなくて困ったということもあったので書いておこうと思った次第。