こちらは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