落とし穴
備忘録です。
saveメソッドの引数にcontextを渡す処理があったとします。
def update
#
#
user.save(context: :hoge)
#
#
end
なぜなら一部のバリデーションをスキップしたいから。
class User < ApplicationRecord
# 処理1
before_validation :set_bar, unless: -> { validation_context == :hoge }
# 処理2
validate :validate_foo, on: :update
def validate_foo
# バリデーション処理
end
end
この例では、user.save(context: :hoge)の時にbefore_validationを走らせたくないけど、他のバリデーションはやって欲しいという意図。saveでcontextは指定しているけど、controllerの処理はupdateではあるからon: :updateのバリデーションのメソッドは呼び出されるだろうと。
しかし、これが間違いだった。仮に処理がupdateだとしてもcontextが:hogeで、validateの:onは:updateなので、一致しないという結果になってしまいvalidate_fooは通らない。
解決
なので、今回意図した挙動にするには、こうしないといけない。
validate :validate_foo, on: %i[update hoge]
仕組みが気になった
:create, :update, :hogeの関係はどうなってる?
各メソッドの格納場所はこんな感じだった。
save: rails/activerecord/lib/active_record/validations.rb
validate(*args, &block): rails/activemodel/lib/active_model/validations/callbacks.rb
before_validation(*args, &block): rails/activemodel/lib/active_model/validations.rb
saveはActiveRecord、validateはActiveModel。
以下、今回に必要な部分だけソースコードを抜粋。
saveメソッドを見てみる。
module ActiveRecord
module Validations
include ActiveModel::Validations # ここでvalidateたちのいるValidationsを読み込んでる
def save(**options)
perform_validations(options) ? super : false
end
def valid?(context = nil)
context ||= default_validation_context
output = super(context)
errors.empty? && output
end
private
# これや!
def default_validation_context
new_record? ? :create : :update
end
def perform_validations(options = {})
options[:validate] == false || valid?(options[:context])
end
end
end
↑ここですな。
:create、:updateもプログラマがsave時に指定するcontextと同列で、contextを指定しない時にデフォルトで入ってたのか。だから、save(context: :hoge)とvalidate :validate_foo, on: :updateはすれ違う訳ですね。
ちなみにsaveがどうやってvalidateを呼び出すのか
ここまでで今回の疑問は解けただけど、ディレクトリの違うsaveメソッドがどうやってバリデーションのメソッドを呼び出すのか気になったので、続き。
valid?のsuperはincludeしたActiveModel::Validations、ここにある。
module ActiveModel
module Validations
# ActiveRecord/Validations#valid?のsuperはここ
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
end
private
def run_validations!
_run_validate_callbacks
errors.empty?
end
end
最後にrun_validations!してる。ここがバリデーションたちを呼び出しているぽい。
_run_validate_callbacks
で検索したけど何も出てこないと思ったら、こんなメタプロをしているらしい↓
module ActiveSupport
module Callbacks
def define_callbacks(*names)
options = names.extract_options!
names.each do |name|
name = name.to_sym
([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
target.set_callbacks name, CallbackChain.new(name, options)
end
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
run_callbacks #{name.inspect}, &block
end
def self._#{name}_callbacks
get_callbacks(#{name.inspect})
end
def self._#{name}_callbacks=(value)
set_callbacks(#{name.inspect}, value)
end
def _#{name}_callbacks
__callbacks[#{name.inspect}]
end
RUBY
end
end
う...、よくわからないけど、ここでmodelに定義されたvalidateメソッドが呼ばれるぽい。
ドキュメント
before_validation(*args, &block)
https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation
validate(*args, &block)
https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate
save
https://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-save