Railsでこの時だけ特定のバリデーションを検証したくないときしたこと

  • 今回はRails v5.0.2を使用しています

Railsでmodelにバリデーションをつけるがこの時だけはoffにしたいという時がある。
例えば特定のバッチ走らせる時とか?
例えば今回は下記のようなバリデーションを定義していたとして

class Item < ApplicationRecord
  # nameがユニークかつ文字数が1から10文字内におさまっているか
  validates :name, uniqueness: true, length: { in: 1..10 }
end

この Item モデルのバリデーションをすべてOFFにするなら

item = Item.new(name: 'hello!')
item.save!(validate: false)

これでいける

ただ特定の条件だけ外したい場合はどうだろう? 今回の例でいうとユニークは保ちたいから文字数のチェックだけを外したい場合
今回はこのようにモデルを変更した

class Item < ApplicationRecord
  validates :name, uniqueness: true
  validates :name, length: { in: 1..10 }, unless: -> { validation_context == :hoge }
end

このように定義しなおして rails console などで試す

irb> Item.new(name: 'a' * 100).save!(context: :hoge)
true
irb> Item.new(name: 'a' * 100).save!(context: :hoge)
ActiveRecord::RecordInvalid: Validation failed: Name has already been taken

一回目は文字数制限を外した状態でレコードは保存され、もう一度同じレコードを生成しようとするとユニークでないのでエラーになる!
意図した動きをしてくれるようになった

何が置きたのか?

railsのソースを追ってみた

save!(context: :hoge) とcallすると下記が呼ばれる

    # https://github.com/rails/rails/blob/v5.0.2/activerecord/lib/active_record/validations.rb#L50
    def save!(options={})
      perform_validations(options) ? super : raise_validation_error
    end

perform_validations(options) とやらがfalseをかえすとraiseでvalidation errorが出る
perform_validations(options)はすぐ下に定義されていて

    # https://github.com/rails/rails/blob/v5.0.2/activerecord/lib/active_record/validations.rb#L82
    def perform_validations(options={}) # :nodoc:
      options[:validate] == false || valid?(options[:context])
    end

なるほど! save!(validate: false) とした時はここで trueになるのでvalidationを評価せずに保存できるのか
今回は save!(context: :hoge) なので valid?:hoge がわたる
このvalid?は同じクラス内に定義されている

   # https://github.com/rails/rails/blob/v5.0.2/activerecord/lib/active_record/validations.rb#L63
   def valid?(context = nil)
      context ||= default_validation_context
      output = super(context)
      errors.empty? && output
    end

ここはnilガードとかしてcontextに値をつめたりしてるだけで実際の処理は superで親の ActiveModelvalid? が呼び出されている

   # https://github.com/rails/rails/blob/v5.0.2/activemodel/lib/active_model/validations.rb#L335
   def valid?(context = nil)
      current_context, self.validation_context = validation_context, context
      errors.clear
      run_validations!
    ensure
      self.validation_context = current_context
    end

ここで self.validation_context に引数でわたってきたcontextをつめているからmodelで validation_context が参照できたようです