【Rails】modelのバリデーションのerrors.addが、controllerのrescueに入るまでを追ってみた
errors.addの実態
あるmodelでerrors.add()しているとする。
なぜこれがcontrollerの e.record.errors.full_messages
で使えるのか疑問に思ったので、ソースコードを追ってみる
# app/models/product.rb
class Product < ApplicationRecord
belongs_to :category
validate :price_must_be_reasonable
private
def price_must_be_reasonable
if price.present? && price <= 0
errors.add(:price, "は0より大きい値にしてください")
end
end
end
errorsの実態
継承しているApplicationRecord
=> ActiveRecord::Base
のソースの中に、includeがあり、validations.rb
のなかに def errors
があった。
model内で、errors.class
を出力してみると ActiveModel::Errors
であった。
errorsメソッドで返却している Errors.new(self)
と一致していることがわかる。
Errors.new(self)の実態
同じ active_model
ディレクトリ内に、Errorsというclassが無いか探した。あった。
activemodel/lib/active_model/errors.rb
module ActiveModel
class Errors
def initialize(base)
@base = base
@errors = []
end
def add(attribute, type = :invalid, **options)
attribute, type, options = normalize_arguments(attribute, type, **options)
error = Error.new(@base, attribute, type, **options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
raise exception, error.full_message
end
@errors.append(error)
error
end
Errors.new(self)
のタイミングでinitializeが発火。
initializeで@errorsを初期化しておいて、addでは@errorsにappendしている。
覚えておくこと
errorsはモデルのインスタンスメソッド。
controllerでエラーを取り出すまでの実態
例えば以下のコードがあるとする。
e.record.errors
で取り出せるのはなぜなのか? ActiveRecord::RecordInvalid
とは?
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
begin
@product.save! # save!メソッドはレコードが無効な場合にActiveRecord::RecordInvalidを発生させる
redirect_to @product, notice: '商品が正常に作成されました。'
rescue ActiveRecord::RecordInvalid => e
# バリデーションエラーが発生した場合
error_messages = e.record.errors.full_messages.join(', ')
flash.now[:alert] = "商品の作成に失敗しました: #{error_messages}"
render :new, status: :unprocessable_entity
end
end
まずsave!やcreate!で何が起きるのかを知る必要がある。
save!の実態
save!でバリデーションに引っかかった場合
RecordInvalid.new(self)
が raise
される。
この self
には、モデルのインスタンスが入る。
RecordInvalidの実態
RecordInvalid.new(self)
のselfは、initialize内で@recordに保存される。
そして、
attr_reader :record
が定義されているため、{RecordInvalidのインスタンス}.recordの形でモデルのインスタンスにアクセスすることができる。
まとめ
controllerでは、RecordInvalidインスタンスから、モデルのインスタンスにrecord
でアクセスでき、さらにモデルのインスタンスメソッドerrors
呼び出すことで、バリデーションエラーを保存しておいたものを参照することができる。
Discussion