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

## 落とし穴
備忘録です。

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

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

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

```ruby:user.rb
class User < ApplicationRecord

  # 処理１
  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は通らない。

## 解決

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

```ruby: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メソッドを見てみる。

```ruby: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、ここにある。

```ruby: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`で検索したけど何も出てこないと思ったら、こんなメタプロをしているらしい↓

```ruby
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
```
https://github.com/rails/rails/blob/85e8e4ce5adadbff08a9c1367a7759a23b7da821/activesupport/lib/active_support/callbacks.rb#L824

う...、よくわからないけど、ここで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