🔍

Railsのvalidatesメソッドについて調べてみた

2023/06/07に公開

railsでは下記のように書くことでバリデーションを行える(コード参照元)。

class Person < ApplicationRecord
  validates :name, presence: true
end

このvalidatesメソッドがどのように実装されているか気になったので調べてみた。

どこで定義されているのか

ApplicationRecord

ActiveRecord::Baseを継承しているクラスであり、rails newで自動生成される。

https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt

ActiveRecord::Base

ActiveRecord::Validationsをincludeしている。
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activerecord/lib/active_record/base.rb#L307-L311

ActiveRecord::Validations

ActiveModel::Validationsをincludeしている。
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activerecord/lib/active_record/validations.rb#L40-L42

ActiveModel::Validations

validatesメソッドが実装されているファイルを読み込んでいる。
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations.rb#L471

ActiveModel::Validations::ClassMethods#validates

validatesメソッドが定義されている。
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations/validates.rb#L106-L128

ActiveModel::Validations::ClassMethods#validatesの中身を読む

下記の場合を考える(コード参照元)。

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

1. validatesメソッドの引数を確認する

https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations/validates.rb#L106-L108

railsはアスタリスクを付けることで引数を配列に指定できるので、attributes = [:name, {:presence=>true, :if=>:paid_with_card?}]となる。
extract_options!は下記のようになっている(このメソッドを使用している理由はRailsガイドが参考になる)。

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activesupport/lib/active_support/core_ext/array/extract_options.rb#L24-L30

これにより、

defaults = {:presence=>true, :if=>:paid_with_card?}

また、
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations/validates.rb#L157-L159
https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activesupport/lib/active_support/core_ext/hash/slice.rb#L10-L17
により、

defaults = {:if=>:paid_with_card?}
validations = {:presence=>true}

2. validationのクラスを取得する

https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations/validates.rb#L113-L127
key = "PresenceValidator"となり、const_getメソッドにより、
validator = ActiveModel::Validations::PresenceValidatorとなる(クラスが定義されるときに、PresenceValidatorという名前で、ActiveModel::Validations::PresenceValidatorを値として追加しているため)。

今回はoptionstrueのため、
https://github.com/rails/rails/blob/66099147482ea431febf20936cec903f197d24be/activemodel/lib/active_model/validations/validates.rb#L161-L172
により、

validates_with(ActiveModel::Validations::PresenceValidator, {:if=>:paid_with_card?, :attributes=>[:name]})

ActiveModel::Validations::ClassMethods#validates_withの中身を読む

1. validates_withメソッドの引数を確認する

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations/with.rb#L88-L90

args = [ActiveModel::Validations::PresenceValidator, {:if=>:paid_with_card?, :attributes=>[:name]}]となり、blockにはnilが入る。
self = Orderなので、

options = {:if=>:paid_with_card?, :attributes=>[:name], :class=>Order}
args = [ActiveModel::Validations::PresenceValidator]

2. validatorのインスタンスを作成する

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations/with.rb#L92-L93
今回はActiveModel::Validations::PresenceValidatorのインスタンスを作成する。

3. validatorのインスタンスを使ってvalidateメソッドを呼び出す

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations/with.rb#L95-L105

ActiveModel::Validations::PresenceValidatorActiveModel::EachValidatorを継承しているので、validator.attributes = [:name]となる。
https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validator.rb#L134-L145

_validatorsはクラス属性として定義されており、_validators[:name] = [ActiveModel::Validations::PresenceValidator.new]となる。
https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations.rb#L71

validateメソッドは次で呼び出される。

validate(ActiveModel::Validations::PresenceValidator.new, {:if=>:paid_with_card?, :attributes=>[:name], :class=>Order})

ActiveModel::Validations::ClassMethods#validateの中身を読む

1. validateメソッドの引数を確認する

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations.rb#L171-L172

args = [ActiveModel::Validations::PresenceValidator.new, {:if=>:paid_with_card?, :attributes=>[:name], :class=>Order}]となり、blockにはnilが入る。
extract_options!により、

options = {:if=>:paid_with_card?, :attributes=>[:name], :class=>Order}
args = [ActiveModel::Validations::PresenceValidator.new]

2. set_callbackメソッドを呼び出す

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations.rb#L174-L180
args.all?(Symbol)falseとなるので、ここは呼び出されない。

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations.rb#L182-L184
options.key?(:on)falseとなるので、ここは呼び出されない。

https://github.com/rails/rails/blob/9d74450a0f8ae658207f1f662367381c248eb7a8/activemodel/lib/active_model/validations.rb#L186
set_callbackメソッドは次で呼び出される。

set_callback(:validate, ActiveModel::Validations::PresenceValidator.new, {:if=>:paid_with_card?, :attributes=>[:name], :class=>Order})

まとめ

Railsのvalidatesメソッドはコールバックを利用していることがわかった。
別記事でコールバックの仕組みを調べる。

GitHubで編集を提案

Discussion