こちらはLinc'well Advent Calendar 2019の14日目の記事となります。
タイトルの通りですが、これまでRailsを数カ月間使用してきて、
Modelで何十回もバリデーションを定義したり、
create!したらバリデーションに引っかかってActiveRecord::RecordInvalid
例外を起こしたり、
何十回もvalid?
メソッドを呼び出したりとしてきたのに、
そういえばvalidationの仕組みを何も知らないな。と思ったので、
バリデーション機能をコードを追いながら調べてみて、その過程をまとめてみました。
間違い等見つけていただけましたら、コメント欄でご指摘いただけたら嬉しいです!
また、文中のRailsのコードはtag v6.0.0
のものです。
調べた中での一番の学び
いきなりですが、今回調べてみての一番の学びを先に述べておくと、下記の事柄でした。
- Railsのバリデーション機能は、コールバックを利用して実現されている。
バリデーションと呼ばれる機能の構成
今回調べてみるに当たって、分けたほうが調べやすいと思ったので、
Railsのバリデーション機能を構成しているものを大きく以下の2つに分けてみました。
- Modelに
validates :name, presence: true
のような文言を記述すると、バリデーションを定義できる。1 - 1で定義されたバリデーションに応じて、オブジェクトがデータベースに保存されるかが決まる。
(save
やcreate
をするときに、バリデーションに引っかかると保存されない。)
便宜上、以下では1を『バリデーション定義』、2を『バリデーション実行』という言葉で表現しています。
『バリデーション定義』のざっくり概要
そもそもバリデーションとしてModelに書いているvalidates
は何なのかというところですが、
これはクラスメソッドにあたります。
(カスタムバリデーションの定義で使うvalidate
やvalidate_with
も同様です。)
私自身、正直validates
がクラスメソッドであることは意識することはあまりなく、なんとなく使っていますが、
Modelの定義でよく目にする、attr_accessor
やhas_many
も同様にクラスメソッドです。
(参考: Railsによるメタプログラミング入門 前田 修吾さん)
また、このようなclass定義の直下で呼び出されるクラスメソッドを、書籍『メタプログラミングRuby 第2版』では『クラスマクロ』と紹介しています。
ということで、validates
というクラスメソッドを探して、内容を確認すればどのようにバリデーションが定義されるかが分かるということになります。
『バリデーション実行』のざっくり概要
実際に、create
のコードを確認してみると、以下の様にsave
が呼ばれ、そのsave
の中でperform_validations
というバリデーションらしきものが行われていることが分かります。
(ActiveRecordのどこに何が定義されているのかなどは、下記の書籍を参考にさせていただいて探しました。
参考: ActiveRecord完全に理解した kinoppydさん)
def create(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
object = new(attributes, &block)
object.save
object
end
end
# The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced
# with this when the validations module is mixed in, which it is by default.
def save(**options)
perform_validations(options) ? super : false
end
~省略~
def perform_validations(options = {})
options[:validate] == false || valid?(options[:context])
end
そして、このperform_validations
ではvalidateを実行しないというオプションがない場合に、valid?
を呼びだしており、(Railsガイドでは『バリデーションをトリガする』と表現されていました。)
バリデーション実行の起点となるものはvalid?
であることが窺えます。
上記を踏まえると、超ざっくりですがバリデーション実行とは、valid?
がtrueを返すか、falseを返すかによって、データベースにオブジェクトを保存するかどうかを決めることだと言い換えることができます。
順番が前後してしまいますが、先にバリデーション実行の詳細を追ってみた過程を記述していきます。
valid?
から始まる 『バリデーション実行』の処理の詳細を追ってみる
上で出てきたactive_record/validations.rb
でvalid?
を探して見てみると下記のようになっています。
context ||= default_validation_context
は、このvalid?
の呼び出し元の処理がcreateとupdateのどちらによるものかという情報であるcontextを定義しているもので、
contextを引数に取るsuper
を呼び出していることから、valid?
をオーバーライドしていることが分かります。
def valid?(context = nil)
context ||= default_validation_context
output = super(context)
errors.empty? && output
end
オーバーライドされているvalid?
を探してみると、ActiveRecord::Validations
がincludeしているActiveModel::Validations
というモジュールに、valid?
が定義されているのが見つかります。
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
(どうして2つのValidationsというモジュールが存在するのかに関して、書籍『メタプログラミングRuby 第2版』の『9.2.3 Validations モジュール』において言及されていました。
元々はActiveRecord::Validations
のみしか存在せず、後から機能がActiveModel::Validations
に分割された経緯や理由が書かれており面白かったです。)
そして、このvalid?
を見ると、明らかにバリデーションを実行していそうなrun_validations!
というメソッドの呼び出し箇所があります。
run_validations!
は以下のようになっており、中身を確認してきます。
_run_validate_callbacks
はひとまず置いて、
errors.empty?
を見ると、インスタンス変数の@errors
の中身の存在確認をしているだけのメソッドであることから、
_run_validate_callbacks
がバリデーションっぽいことをして、@errors
に値を入れるのだなと予測できました。
def run_validations!
_run_validate_callbacks
errors.empty?
end
そこで、この_run_validate_callbacks
を見に行こうと思ったのですが、
コードジャンプしても全文検索しても見つからなかったため、
動的に定義されるメソッドであると推測されました。
が、それらしきものを発見することができず、ググったところ下記の記事が見つかりました。
(参考: Railsのvalidationの実行過程を調べる2)
上記の記事を引用させていただくと、この_run_validate_callbacks
は、
ざっくりまとめると、当該モデルに設定されたcallbacksの内、 :validateのキーに引っかかるcallbacksを実行していっていると言うことになります。
とのことでした。
この時点では、『:validateのキーに引っかかるcallbacks』というものが一体何なのか知らなかったのですが、
ここまでのバリデーション実行の流れをざっくりまとめると、下のようになりました。
- オブジェクトを
save
する際に、valid?
が呼ばれる。 -
valid?
から始まる処理の中で、_run_validate_callbacks
が実行されることで『:validateのキーに引っかかるcallbacks』というものが実行され、@errors
に何かしらの値が入る(これはこの時点では予想ですが、カスタムバリデーションを定義する時に、record.errors.add
のように@errors
に値を入れることからも予想ができます。) -
@errors
の中身が空であるかを確認し、空であればtrueを、そうでなければfalseを返し、それによってvalid?
の戻り値が決まる。 -
valid?
の戻り値がtrueなら、オブジェクトを保存する処理が続けられ、falseなら保存する処理が中止される。
(『:validateのキーに引っかかるcallbacks』が、Modelに定義されているコールバックの中で、:validate
キーに対応するActiveSupport::Callbacks::CallbackChain
を指しているということが後で分かりました。)
『バリデーション定義』の処理の詳細を追ってみる
バリデーション実行の詳細がある程度分かったことから、
あとは、バリデーション定義の処理の中で、『:validateのキーに引っかかるcallbacks』が定義されているところを見つけることができれば、
バリデーションの機能の全体感を把握することができるので、そこをゴールとして進んでいきます。
validates
がクラスの読み込み時に実行されるところから定義の処理は始まります。
処理の中身としては、受け取った引数を([:name, :presence: true]
みたいな)、
良い感じに加工し、最終的にvalidates_with
を呼び出すものです。
def validates(*attributes)
defaults = attributes.extract_options!.dup
validations = defaults.slice!(*_validates_default_keys)
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
defaults[:attributes] = attributes
validations.each do |key, options|
key = "#{key.to_s.camelize}Validator"
begin
validator = key.include?("::") ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
next unless options
validates_with(validator, defaults.merge(_parse_validates_options(options)))
end
end
軽く流れを追うと、ブロック引数のkey
には:presence
や:length
などが入り、key = "#{key.to_s.camelize}Validator"
では、"PresenceValidator"といった文字列が作られます。
そして、その文字列からvalidator = key.include?("::") ? key.constantize : const_get(key)
でクラス名の定数を取得し、validates_with
に引数で渡しています。
(activemodel/lib/active_model/validations
配下に、LengthValidator
、InclusionValidator
などのバリデーションの種類に対応したValidatorクラスがそれぞれ用意されています。)
最後のvalidates_with
の呼び出しは、カスタムバリデータを使ったバリデーションの定義と同じ形になります。
そして、呼び出されたvalidates_with
が何をしているかというと、引数として渡されたValidatorクラス名を使って、インスタンスを作り、最後にvalidate
メソッド呼び出しの引数に渡しています。
このvalidate
はカスタムメソッドを使ったバリデーションを定義するときに使うものと同じです。
(validate :expiration_date_cannot_be_in_the_past
みたいに使うやつです。)
def validates_with(*args, &block)
options = args.extract_options!
options[:class] = self
args.each do |klass|
validator = klass.new(options, &block)
if validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes.each do |attribute|
_validators[attribute.to_sym] << validator
end
else
_validators[nil] << validator
end
validate(validator, options)
end
end
end
続けて、validate
を見ていきます。
すると、set_callback(:validate, *args, options, &block)
という、いかにも、『:validateのキーに引っかかるcallbacks』に関係していそうなメソッドを呼び出しているのが発見でき、
さらに、同じファイルにdefine_callbacks :validate, scope: :name
という、これまた関係していそうなメソッドがincluded
の中で呼ばれているのが見つかります。
define_callbacks :validate, scope: :name_badge:
~省略~
def validate(*args, &block)
options = args.extract_options!
if args.all? { |arg| arg.is_a?(Symbol) }
options.each_key do |k|
unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
end
end
end
if options.key?(:on)
options = options.dup
options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
!(options[:on] & Array(o.validation_context)).empty?
}
end
set_callback(:validate, *args, options, &block)
end
見つけたは良いものの、define_callbacks
とset_callback
が何なのか知らなかったので、ググってみると下記の記事が見つかりとても勉強になりました。
(参考: ActiveModelでコールバックを使いたい時のまとめ - Qiita | Issus(イシューズ)
https://issus.me/projects/34/issues/71)
(参考: railsのbefore_actionはどうやってうごいているか)
上記記事を参考にざっくりまとめると、
define_callbacks :validate
で:validate
というnameを持つ一連のコールバックをまとめるCallbackChain
インスタンスを作成し、
(コード中のコメントでは、sets of events in the object life cycle that support callbacksと表現されてました。)
set_callback(:validate, *args, options, &block)
で、
先ほど定義した:validate
という一連のコールバックの一つとして実行する処理として、引数で渡されたものを設定しています。
これで _run_validate_callbacks
が具体的に何をするのかに繋がりました。
set_callback(:validate, *args, options, &block)
で:validate
というキーに引っかかる様にセットされた処理を実行していくということになります。
(また、複数形であることからも、複数のバリデーションがある場合は、複数回set_callback
が呼ばれることもあるだろうと予想できます。)
set_callback(:validate, *args, options, &block)
でコールバックの一つとして定義された処理こそがバリデーションの中身ということなります。
ということで次は、set_callback(:validate, *args, options, &block)
がどのようなcallbackを定義するのかを追っていきます。
def set_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
self_chain = get_callbacks name
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
end
__update_callbacks(name) do |target, chain|
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
target.set_callbacks name, chain
end
end
Callback.buildで作られるActiveSupport::Callbacks::Callback
インスタンスの一例としては、下記のようなものです。
@kind
には:beforeや:afterなどの実行のタイミングを表す値が入り、指定がなかった場合は:beforeが入ります。
@filter
には、引数で渡されたValidatorインスタンスが入り、これがコールバックで実行する処理の元となるっぽいです。
そして、それらを最後に__update_callbacks
で一連のコールバック処理(chain)に追加しています。
#<ActiveSupport::Callbacks::Callback:0x0000565553a47960
@chain_config={:scope=>:name, :terminator=>#<Proc:0x0000565549744b50@/usr/local/bundle/gems/activesupport-5.2.2.1/lib/active_support/callbacks.rb:603>},
@filter=#<ActiveRecord::Validations::PresenceValidator:0x0000565553a47cd0 @attributes=[:user], @options={:message=>:required}>,
@if=[],
@key=#<ActiveRecord::Validations::PresenceValidator:0x0000565553a47cd0 @attributes=[:user], @options={:message=>:required}>,
@kind=:before,
@name=:validate,
@unless=[]>
あとは、ActiveSupport::Callbacks::
が、どのようにActiveModel::Validations::
配下のValidatorクラスのインスタンスを元にして、コールバックの処理を定義・実行しているのかを追えば、恐らく終わりというところまで来ました。
ところが、そのコールバックのActiveSupport::Callbacks
の仕組みを追っていったところ、
とても難しく、本記事にうまくまとめられなそうだったため、また別の記事として調べようと思います。(もしくはこの記事に追記します。)
(完全に実力不足でした。ここが一番気になるところなのにすみません。。)
ということで、一旦ここまでのバリデーション定義の流れをざっくりまとめると、下記のようになりました。
- クラスの読み込み時に、
validates
といったバリデーション定義のクラスメソッドが実行される。 -
validates
→validates_with
(カスタムバリデータを用いたバリデーション定義で使うやつ)→validate
(カスタムメソッドを用いたバリデーション定義で使うやつ)の順でメソッドが呼ばれる。 -
validate
では、バリデーションの種類(:presenceとか:length)に応じて、対応するValidatorクラスのインスタンスを生成し、それを引数に含めてset_callback
メソッドを呼び出す。 -
set_callback
メソッドでは、引数で受け取ったValidatorを使用しながら、バリデーションを実行する処理を含むコールバックを定義する。(そして、このコールバックはvalid?
が呼ばれると実行されることになる。)
感想
色々と知らなかったことを知ることができ、とても勉強になりました。
余談として、attr_accessor
といったアクセサや、has_many
といったアソシエーションも同じクラスマクロとして調べる対象の候補だったのですが、
『ActiveRecord完全に理解した』にて、前者は『すごく長い』、後者は『難しい』と紹介されていたので、特に何も書かれていなかったValidationsを今回は選択してみました。
勉強になったので、今度は他のクラスマクロも同様に調べてみようと思います。
(その前にCallbackとValidatorをきちんと調べてから。)
参考文献
下記の文献を見て色々と勉強させていただきながら、この記事を書いてみました。
どれも勉強になりました。
-
Paolo Perrotta(著),角 征典(翻訳)『メタプログラミングRuby 第2版』 オライリージャパン
-
kinoppyd 著 『ActiveRecord 完全に理解した』技術書典 7(2019年秋)新刊 2019年9⽉22⽇ ver 1.0