Help us understand the problem. What is going on with this article?

【Rails】errors.addって何?

モデルでは特に考えずにerrors.add(:base, '名前の文字数オーバー')とかしておけば良いかーみたいな風潮ありますよね:church:
これ

> user = User.new
> user.errors
=> #<ActiveModel::Errors:0x00007fc0ed8b5a50 @base=#<User id: nil, name: nil, created_at: nil, updated_at: nil>, @messages={}, @details={}>
> user.errors.add(:base, '名前の文字数オーバー')
> user.errors.full_messages
=> ["名前の文字数オーバー"] 

そもそも.addってなんなのさと、すぐraiseされるならわかるけど、何個も追加(add)できるの???😴

errors.addの使い方一覧

結論から書きます。

# シンプルな構文(どんなエラーなのか)
> user.errors.add(:base, '名前の文字数オーバー')
> user.errors.full_messages
 => ["名前の文字数オーバー"]

# 一般的な構文(どのカラムの、どんなエラーなのか)
> user.errors.add(:name, '文字数オーバー')
> user.errors.full_messages
=> ["Name 文字数オーバー"] 

# I18nを利用した構文
# (対象がないと translation missing)
## activerecord.errors.models.user.attributes.name.over_char_limit
## activerecord.errors.models.user.over_char_limit
## activerecord.errors.messages.over_char_limit
## errors.attributes.name.over_char_limit
## errors.messages.over_char_limit
> user.errors.add(:name, :over_char_limit)
> user.errors.full_messages
=> ["Name 文字数オーバー"] 

# すぐraiseしたい構文
> user.errors.add(:name, :over_char_limit, strict: true)
ActiveModel::StrictValidationFailed (Name 文字数オーバー)
# StrictValidationFailed < StandardError です

# 何の為にあるのか不明
> user.errors.add(:name, :over, message: '文字数オーバー')
> user.errors.full_messages
=> ["Name 文字数オーバー"] 
# .details のため?
> user.errors.details
=> {:name=>[{:error=>:over}]}

すぐraiseできるじゃん!
良い機会なので、ここからerrorsvalidationsの関係性を考えます。

ここから下は読まなくて良いです。

railsを調べてみた。

rails 5-1-stableブランチを見てます!

errorsのmethodはどこから?

すぐ見つけた
active_model/validations.rb

activemodel/lib/active_model/validations.rb
# Errors の引数でモデルのオブジェクトを渡してる
def errors
  @errors ||= Errors.new(self)
end

モデルはActiveRecord::Baseを継承しているので、辿っていくと以下のようにincludeされてる

# activerecord/lib/active_record/base.rb
include Validations

# activerecord/lib/active_record/validations.rb
include ActiveModel::Validations

.addのmethodはどうなってるの?

normalize_messageI18nで使えるように頑張ってパースしてた
raiseとかはここで制御してるんだね
active_model/errors.rb

activemodel/lib/active_model/errors.rb
def add(attribute, message = :invalid, options = {})
  message = message.call if message.respond_to?(:call)
  detail  = normalize_detail(message, options)
  message = normalize_message(attribute, message, options)
  if exception = options[:strict]
    exception = ActiveModel::StrictValidationFailed if exception == true
    raise exception, full_message(attribute, message)
  end

  details[attribute.to_sym]  << detail
  messages[attribute.to_sym] << message
end

.addしたけど、いつraiseされるの?

railsでは.create!.update!も結果的に.save!が呼ばれます。
perform_validationsvalid?でfalseになるとraiseされます。

activerecord/lib/active_record/validations.rb
def save!(options = {})
  perform_validations(options) ? super : raise_validation_error
end

private

def raise_validation_error
  raise(RecordInvalid.new(self))
end

def perform_validations(options = {})
  options[:validate] == false || valid?(options[:context])
end

RecordInvalidが呼び出されたら

errorsの配列のメッセージはここでjoinされてるんだね

activerecord/lib/active_record/validations.rb
class RecordInvalid < ActiveRecordError
  def initialize(record = nil)
    if record
      @record = record
      errors = @record.errors.full_messages.join(", ")
      message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
    else
      message = "Record invalid"
    end
    super(message)
  end
end

以下のja.ymlが必要なので忘れないように!

config/locales/models/ja.yml
ja:
  activerecord:
    errors:
      messages:
        record_invalid: "%{errors}"

valid?では何をしてるの?

  1. errors.clear : valid?以前に追加されたerrorsを削除
  2. run_validations! : モデルに紐づいてる:validateを1つずつ実行してる
  3. errors.empty? : errorsの配列にあればraise!

つまり、コンソール上でerrors.addをしても何も起きないんです🙄(clearされる)

activerecord/lib/active_record/validations.rb
def valid?(context = nil)
  context ||= default_validation_context
  output = super(context)
  errors.empty? && output
end
activemodel/lib/active_model/validations.rb
def valid?(context = nil)
  current_context, self.validation_context = validation_context, context
  errors.clear
  run_validations!
ensure
  self.validation_context = current_context
end

run_validations!はちょっと難しかった
active_support/callbacks.rbrun_callbacksが呼ばれるんだけど、これが結構難しい
(結果的に、:validateを1つずつ実行してるだけなんだけどね)

解説は以下のqiitaにありました
Railsのvalidationの実行過程を調べる

どんな:validateが実行するのか知りたいなら

# モデルに紐づいてる:validateの一覧
> user.__callbacks[:validate]
# 自作validatorの一覧
> user._validators

自作validatorを作りたいなら

これは弊社でもおなじみのvalidatorの作り方です!
オリジナルのバリデーションクラス:validates_with

所感

初めてrailsのソースコード読んでみたけど、結構読みやすかった!
でもprocとかyieldとか意味は知ってるけど、普段使わない関数が急に出てくると困惑するし、急にわからなくなるなあ。

これを機に俺もrailsのソースコードを読み込んでみよう!と10分思ったが、めんどくさいし、タピオカ飲みたいので辞めた🧸

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした