Railsのvalidateメソッドを読む
前回 validates_with
メソッドは内部的には validate
を呼び出しているだけということがわかったので、当然 validate
メソッドも読んでいく
ファイルが require されたり、include/extend される点についてはここで書いたので省略する
Rails: 7.0.0.alpha
Ruby: 2.7.3
validates :name, presence: true
を Book クラスに書いた時の処理を追っているものとする
validate メソッド
メソッドはここにある
def validate(*args, &block)
options = args.extract_options!
if args.all?(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] = [
->(o) { !(options[:on] & Array(o.validation_context)).empty? },
*options[:if]
]
end
set_callback(:validate, *args, options, &block)
end
まず args には [#<ActiveRecord::Validations::PresenceValidator:0x00007fcfa7ce6110 @attributes=[:name], @options={}>, {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}]
という感じの配列が渡ってくる(block は渡してないので nil)
options = args.extract_options!
前回の記事で処理を見て行ったが、 extract_options!
によって args 最後の引数の {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}
の部分が options となる
args.all?(Symbol)
options.key?(:on)
今回の例では、このどちらも false となるので、中の処理は行われないため即座に set_callback(:validate, *args, options, &block)
まで到達する
(どこかでここも見ていきたい)
わかりやすくすると、この時点では set_callback(:validate, [#<ActiveRecord::Validations::PresenceValidator:0x00007fcfa7ce6110 @attributes=[:name], @options={}>], {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}, nil)
という呼び出しが行われる
set_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
渡ってくる引数は ↑ で書いた通り
type, filters, options = normalize_callback_params(filter_list, block)
normalize_callback_params
メソッドは同ファイルのここにある
def normalize_callback_params(filters, block)
# CALLBACK_FILTER_TYPES は [:before, :after, :around]なので :before が typeとなる
type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
# options はいつもの {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}
options = filters.extract_options!
# blockはないので処理は行われない
filters.unshift(block) if block
# type => :before
# filters => [#<ActiveRecord::Validations::PresenceValidator:0x00007fa03a630948 @attributes=[:name], @options={}>]
# options.dup => {:class=>Book(id: integer, name: string, created_at: datetime, updated_at: datetime)}
[type, filters, options.dup]
end
戻り値をそれぞれ変数に展開している
self_chain = get_callbacks name
get_callbacks
では __callbacks
という class_attribute
から ActiveSupport::Callbacks::CallbackChain
のインスタンスを取得している
(こいつの定義や、中身については一旦置いておく)
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
end
では ActiveSupport::Callbacks::Callback
のインスタンスを取得している
(こいつも一旦内部は見にいかないでおく)
__update_callbacks(name) do |target, chain|
__update_callbacks
メソッドには block を渡していて、内部で yield が呼ばれている
def __update_callbacks(name)
# [self] + ActiveSupport::DescendantsTracker.descendants(self) の部分はBookクラスが入った配列 => [Book]
# reverse_each はRuby組み込みのメソッドで、配列を逆から処理していく
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
# name は :validate なので Book.get_callbacks :validate を呼び出している
# chain には ActiveSupport::Callbacks::CallbackChain のインスタンスが入る
chain = target.get_callbacks name
# target(=Bookクラス)とchain(=ActiveSupport::Callbacks::CallbackChainのインスタンス)
# を引数に渡されたブロックを実行する
yield target, chain.dup
end
end
__update_callbacks
に戻って見てみる
# ブロック引数のtargetとchainは↑の通り
__update_callbacks(name) do |target, chain|
# optionsは { class: Book } というハッシュなので、 options[:prepend] はnil
# よってここではchainに対してappendされている
# mappedは ActiveSupport::Callbacks::Callback のインスタンスで、nameに対してPresenceValidatorを処理するなどの情報が入っている
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
# Book.set_callbacks :validate, ActiveSupport::Callbacks::CallbackChainのインスタンス をやっている
target.set_callbacks name, chain
end
set_callbacks
は同ファイルの protected メソッドとして定義されている
protected
def set_callbacks(name, callbacks)
# 特異クラスに __callbacks メソッドがあるかどうか確認している(
# ここではfalseとなるので、 self.__callbacks に値がセットされる
# 第二引数がfalseなので、スーパークラスやincludeしたモジュールに定義されたメソッドは対象としていない
unless singleton_class.method_defined?(:__callbacks, false)
self.__callbacks = __callbacks.dup
end
# 登録されている__callbacksからvalidateのものを更新されたcallbacksで上書きしている
self.__callbacks[name.to_sym] = callbacks
self.__callbacks
end
結局どう言うことか?
残念ながら、Rails のコールバックチェインの部分の知識がないため、詳細まではわからなかった
しかし、以下のような流れがなんとなく見えてきた
Book.__callbacks
を見ると、save
destroy
create
validate
などを対象とした、ActiveSupport::Callbacks::CallbackChain
のインスタンスが多数格納されているvalidate
メソッドなどを実行すると、それに関係するActiveSupport::Callbacks::CallbackChain
が都度 update されていき、どんどん追加されていく
名前などから察するに、 __callbacks
に validation やコールバックの情報をためていき、必要な箇所で必要なコールバックを呼び出している感じだろうか
もう少し内部まで踏み込む必要がありそうだ
ソースコードにメソッドの説明書きなどもあるので、コードを読むばかりではなく説明も読んでいこう
Rails はそう簡単に理解できるものではないので、焦らずやっていこう