【rails】create/update時に特有のvalidationの実装

RailsActiveRecordのお話です。
recordを新しく作成する時にだけかけたいvalidationがあったり、更新する時にだけかけたいvalidationがあった経験はありませんか?
そんな時、ActiveRecordはとってもお利口さん。
対象のクラスにおいて、魔法のメソッド「validate_on_create」「validate_on_update」メソッドを定義してあげると、上記を実現できます。

  • 例えば、以下のような状況設定とする。
Studentテーブル
└ 作成するときだけ、パスワード(password)を確実に登録してもらう。
└ 更新する時だけ、年齢(old)を入力してもらう。
  • 実現方法は多数ありますが、代表的なものを2つほど紹介します。

実現方法1

class Student < ActiveRecord::Base
  validates_presence_of :password, :if => Proc.new{|p| |p.new_record?}
  validates_presence_of :old, :if => Proc.new{|p| !p,new_record?}
end
  • ちなみに、このifの条件が複雑になった場合はProcを使わず、メソッドを定義してあげることも可能
class Student < ActiveRecord::Base
  validates_presence_of :password, :if => :password_required?
  
  def password_required?
    #判定ロジック
  end
end

実現方法2

class Student < ActiveRecord::Base
  def validate_on_create
    validates_presence_of :password
  end

  def validate_on_update
    validates_presence_of :old
  end
end
  • 個人的には条件が複雑にならない限り、作成/更新時に特有なvalidation1を実装する場合は実装方法2で実装する方が好きです。
  • ちなみに、validate_on_createやvalidate_on_updateを定義するだけこのことが実現できるのかが謎だったので、ソースコード読んでみました。

validate_on_updateの仕組み

  • 前提としてARオブジェクトをsaveする際、以下のcallbacksが呼び出されます。
(-) save
(-) valid?
(1) before_validation
(2) before_validation_on_create
(-) validate
(-) validate_on_create
(3) after_validation
(4) after_validation_on_create
(5) before_save
(6) before_create
(-) create
(7) after_create
(8) after_save
  • この中で、今回はvalid?メソッドに着目して、validations.rbを見ていきます。
    • callbacksはまたの機会にまとめます。
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L786
  def valid?
    errors.clear

    run_validations(:validate)
    validate

    if new_record?
      run_validations(:validate_on_create)
      validate_on_create
    else
      run_validations(:validate_on_update)
      validate_on_update
    end

    errors.empty?
  end

# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L823
  def run_validations(validation_method)
    validations = self.class.read_inheritable_attribute(validation_method.to_sym)
    if validations.nil? then return end
    validations.each do |validation|
      if validation.is_a?(Symbol)
        self.send(validation)
      elsif validation.is_a?(String)
        eval(validation, binding)
      elsif validation_block?(validation)
        validation.call(self)
      elsif validation_class?(validation, validation_method)
        validation.send(validation_method, self)
      else
        raise(
          ActiveRecordError,
          "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
          "class implementing a static validation method"
        )
      end
    end
  end
  • valid?メソッドを見るとわかる通り、まずはvalidateメソッドが走り、次に対象がnew_recordかどうか判定してvalidate_on_createかvalida_on_updateを走らせるかを決定しています。
  • つまり簡単な流れは以下のような流れになる。
    • ARのsaveが呼び出される
    • valid?メソッドが呼び出される
    • validatesメソッドが呼び出される
    • new_record?かどうかを判定する
    • validate_on_create / validate_on_updateメソッドが呼び出される
  • このように定義されているために、ARを継承しているStudentクラスでvalidate_on_updateメソッドを定義すると、更新時にだけ「validates_presence_of」が呼び出された訳ですね!
    • ちなみに、validates_presence_ofのメソッドも面白かったので転記
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L400
  def validates_presence_of(*attr_names)
    configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
    configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)

     # can't use validates_each here, because it cannot cope with nonexistent attributes,
     # while errors.add_on_empty can
     attr_names.each do |attr_name|
       send(validation_method(configuration[:on])) do |record|
         unless configuration[:if] and not evaluate_condition(configuration[:if], record)
           record.errors.add_on_blank(attr_name,configuration[:message])
         end
       end
     end
   end
    • errors.add_on_blank?って何だ?今までerrors.addしか知らなかったよ。。
# https://github.com/rails/rails/blob/v1.2.3/activerecord/lib/active_record/validations.rb#L63
  def add_on_blank(attributes, msg = @@default_error_messages[:blank])
    for attr in [attributes].flatten
      value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
      add(attr, msg) if value.blank?
    end
  end
    • 要はvalueがblank?の時にaddする訳ですね。fmfm。どこかで使えそう!