Railsのcontrollerで、トランザクションを使っているところで、rescueブロックの処理が書いてあるのにそこに辿り着けていない現象に遭遇しました。
悩んでいることをチャットワークに書いたら、同僚の @kazuhisa1976 さんが、小さいRailsアプリを作って検証してくれたのですが、そこで面白い挙動が見つかりました。
検証環境を作る
小さいRailsアプリを作る
まず、小さいRailsアプリを作ります。
rails new sample cd sample bin/rails g scaffold User name:string bin/rails db:migrate
Userモデルにvalidationを加えます。
class User < ApplicationRecord validates :name, presence: true end
UsersController#createで、検証のためにトランザクションを使ってみます。保存に失敗したら、root_pathにリダイレクトするという設定です。
class UsersController < ApplicationController # 省略 def create @user = User.new(user_params) ActiveRecord::Base.transaction do @user.save! end redirect_to @user, notice: 'User was successfully created.' rescue => e redirect_to root_path end # 省略 end
Railsアプリを起動しておきます。
bin/rails s
これで、とりあえずの検証環境は出来上がりました。
検証する
正常系
まずは、nameを入力して投稿してみます。
当然ながら成功。
異常系
次は、nameを何も入力せずに投稿してみます。nameが空なので、@user.save!
が失敗して、root_path
にリダイレクトされるはずです。
おおっと!リダイレクトされませんでした!rescueブロックに辿りつかずに、@user.save!
でActiveRecord::RecordInvalidが発生していると言われました。私が遭遇した現象と全く同じ!
原因を探る
エラー画面のWebコンソールで、実験してみましょう。
はい。実はこのRailsアプリには、root_pathが定義されてません。それが原因です。例外の情報が入っている変数eもあることから、rescueブロックには実は辿りついています。
そうなんです。rescueブロックで発生したエラーの原因を表示せず、元の例外が発生した原因を表示してしまうということです。
修正する
このRailsアプリでいえば、routes.rbでroot_pathを定義してあげれば、直ります。
Rails.application.routes.draw do resources :users root to: 'users#index' # root_pathを定義 end
まとめ
rescueブロックで例外が発生すると、大元の例外の情報を基にエラー画面が作られてしまうので、rescueブロックに辿り着けないようなエラー画面が表示されたら、rescueブロック内にブレイクポイントを置いて1つ1つ検証していくのがいいです。恐らく、今回と同様に変数かメソッドの定義がないなどが原因です。
自分のエラーの原因も、メソッドがないことが原因でした。
作った当初はRails3か4くらいで、paramsがHashを継承していたのですが、Rails5にアップグレードして(今使ってるのは5.1.4)Hash継承ではなくActionController::Parametersになった影響で、map
メソッドがなくなっていました。
似たようなコードを書くと、以下のような感じ。
# users_paramsはfields_forで作られた配列のデータ def update ActiveRecord::Base.transaction do users_params.each do |id, values| user = User.find id user.assign_attributes values user.save! end end redirect_to users_path rescue => e @users = users_params.map do |id, values| # ここでundefined method user = User.find id user.assign_attributes values user.valid? user end render :edit end
参考情報はこちら。
そこで、以下のように修正。
rescue => e @users = users_params.to_h.map do |id, values| # to_hメソッドを挟む # 略 end render :edit end
今回は管理画面側のため、網羅的にテストを書いていなかったことが気づけなかった原因だったので、例外時のテストも追加しておこうと思います。