Validationの動き

こうかくと、name がチェックされるわけだが、

class Hoge < ActiceRecord::Base
  validates_presence_of :name,
                        :message => "を入力しようよ。"
end

どうしてこんな動きがなるのか気になる。

バリデーションの登録の流れ

まずは、ActiceRecord::Base は、actice_record/base.rb の中

module ActiveRecord
  ・・・
  class Base
    ・・・
  end
  ・・・
end

このBaseクラスが、actice_record.rb の中で拡張されている。

ActiceRecord::Base.class_eval do
  ・・・
  include ActiveRecord::Validations
  ・・・
end

上の、include ActiceRecord::Validations が呼ばれると。
actice_record/validations.rb の以下の部分に処理が移る。

module ActiceRecord
  module Validations
    def self.included(base)
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
        alias_method_chain :update_attribute, :validation_skipping
      end
    end
  end
end

この部分が最初どうなってるかよくわからなかったんだけど、

Base -include-> Validations
     -extend-> Validations::ClassMethods

となっていて、
Validationsの、def ... end が Base クラスのインスタンスメソッドになり、
Validations::ClassMethodsの、def ... end が Base クラスのクラスメソッドになる。
ということらしい。

Rubyのinclude、extend ってなんか覚えられん。
このやり方って定石なのかな。

ともあれ。

このClassMethodsの中に、validates_presence_ofやら、validates_format_of が定義されていて、
ActiveRecord::Baseを継承したクラスの中で、呼び出せるようになっている。

  module Validations
    module ClassMethods
    ・・・
      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
    ・・・
    end
  end

この中でポイントは以下の部分。

  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

configrationは、validates_* の最後にわたすHashのこと。

validation_method(configuration[:on]) で、:onの値によって :validate/:validate_on_create/:validate_on_update のいずれかが返る。
ここで返ったシンボルの名前のメソッドが、最後のブロックを引数にして呼び出される。

  module ClassMethods
    def validate(*methods, &block)
      methods << block if block_given?
      write_inheritable_set(:validate, methods)
    end

    def validate_on_create(*methods, &block)
      methods << block if block_given?
      write_inheritable_set(:validate_on_create, methods)
    end

    def validate_on_update(*methods, &block)
      methods << block if block_given?
      write_inheritable_set(:validate_on_update, methods)
    end
  end

先の send で渡されたブロックは、これらのメソッドに渡されて、それぞれのメソッドごとのキー名(:validate/:validate_on_create/:validate_on_update)の名前でクラス変数として定義されているHashの中に保存される。

大体こんな感じ。

バリデーションの呼び出し。

バリデーションの呼び出しの最初の入り口はここから。

module Validations
  def save_with_validation(perform_validation = true)
    if perform_validation && valid? || !perform_validation
      save_without_validation
    else
      false
    end
  end

  def save_with_validation!
    if valid?
      save_without_validation!
    else
      raise RecordInvalid.new(self)
    end
  end
end

なぜここからかというと、Validationsモジュールの self.included っでこんなことをしている。

module Validations
    def self.included(base) # :nodoc:
      base.extend ClassMethods
      base.class_eval do
        alias_method_chain :save, :validation
        alias_method_chain :save!, :validation
        alias_method_chain :update_attribute, :validation_skipping
      end
    end
end

ポイントは、alias_method_chain の部分。
何をしているかというと、こういうことをしている。

  alias_method :save_without_validation, :save
  alias_method :save, :save_with_validation

詳しくはここをみるとよくわかる。
http://wota.jp/ac/?date=20060503#p03

ようは、Baseクラスのsaveメソッドを、save_without_validation でも呼び出せるように退避してから、saveメソッドの呼び出しを、:save_with_validation に変更している。
これによって、ActiceRecord::Baseを継承したクラスで、saveメソッドを呼び出すと、save_with_validation が呼び出されるようになる。

で、save_with_validation の中で呼び出されている save_without_validation が本来のベースとなっている saveメソッドをあらわす。

save_with_validation で呼び出されている valid?メソッドの中は、こんな感じになっている。
まぁ、そのまんまですな。

module Validations
  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
end

この中のrun_validations が validates_presense_of などで登録したバリデータを呼び出している部分で、引数のシンボルは登録したときのキー。
run_validationsの中身は以下のとおり、

module Validations
private
  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
end

この中の、

validations = self.class.read_inheritable_attribute(validation_method.to_sym)

で、先に登録したバリデーションの定義を取り出している。
run_validationsの残りでは、定義の配列をeachでまわして、それぞれ呼び出しているだけ。

ここを見ると、バリデータとして、シンボル、文字列、ブロック、クラスを定義可能なことがわかる。