6
3

More than 1 year has passed since last update.

僕がハマったRailsのsave(context: hoge)の落とし穴

Last updated at Posted at 2022-08-17

落とし穴

備忘録です。

saveメソッドの引数にcontextを渡す処理があったとします。

user_controller.rb
def update
  #
  #
  user.save(context: :hoge)
  #
  #
end

なぜなら一部のバリデーションをスキップしたいから。

user.rb
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は通らない。

解決

なので、今回意図した挙動にするには、こうしないといけない。

user.rb
  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メソッドを見てみる。

rails/activerecord/lib/active_record/validations.rb
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、ここにある。

rails/activemodel/lib/active_model/validations.rb
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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3