railsのvalid?メソッドに潜んだ罠

railsのvalid?メソッドで、少し変わった仕様があったので備忘録として残しておきますー。

環境

rails1.2.3

序章

railsのvalid?メソッドは、意図しない挙動を起こすので、使う時には注意が必要です。
普通メソッド名の末尾に「?」をつけたメソッドは、メソッド内で真偽判定をして「true/false」で返してくれることを期待します。
しかし、valid?メソッドの中ではコチラが意図していない挙動が行われています。
てっきりvalid?メソッドは、レシーバのオブジェクトがsave可能かどうか(validationに引っかからないかどうか)を判定して「true/false」で返すものだと思っていました。(※1)
実際にメソッド名から想像すると、誰しもがその挙動を期待すると思います。
それが違うのです。。

内容

ある特定のアクションのみに適用させたいvalidateメソッド(以下、my_validateとする)を定義し、そのアクション内でmy_validateとvalid?メソッドを実行して、errorがないことをチェックしたいとします。
valid?メソッドが※1のような挙動であると信じている人は以下のようなコードを書くと思います。

# params[:test]はmy_validateに引っかかるとする

>> test = Test.new(params[:test])
>> test.my_validate
#=> false
>> test.errors.empty?
#=> false
def check_nedded_action
  test = Test.new(params[:test])
  test.my_validate

  if test.save
    true
  else
    false
  end
end

このように書くと不思議なことが起きます。
params[:new]の値ではmy_validationにひっかかるはず。
しかし、check_needed_actionにparams[:test]を渡すと「true」が返ってきます!

>> test = Test.new(params[:new])

>> test.my_validate
#=> false
>> test.errors.empty?
#=> false
>> test.valid?
#=> true
>> test.errors.empty?
#=> true

なぜ…??

答えはソースコードを見れば一目瞭然です。

# activerecord/lib/active_record/validations.rb
# Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
777     def valid?
778       errors.clear
779 
780       run_validations(:validate)
781       validate
782 
783       if new_record?
784         run_validations(:validate_on_create)
785         validate_on_create
786       else
787         run_validations(:validate_on_update)
788         validate_on_update
789       end
790 
791       errors.empty?
792     end
793 
794     # Returns the Errors object that holds all information about attribute error messages.
795     def errors
796       @errors ||= Errors.new(self)
797     end

つまりはvalid?メソッドを呼び出した時点で、errorsオブジェクトをすでに持っている場合は、errors.clearされた後にvalidateを走らせるようになっているのです。
また、見てわかる通りvalid?メソッドの内部でvalidateを走らせて、errorがあった場合には、errorsオブジェクトの中にerrorを挿入しています。
そして、valid?メソッドはerrors.empty?で「true/false」を返り値として渡しているのです。

なるほどー。
これを受けて先ほどのコードを書き直すと、以下のようになるかと思います。

def check_nedded_action
  test = Test.new(params[:test])
  test.valid?
  test.my_validate

  if test.errors.full_messages.empty?
    test.save(false)
    true
  else
    false
  end
end

※saveメソッドでは保存の前にvalid?が呼び出されることになるので、敢えてsave(false)にしました

これで意図した挙動は保証できました!めでたしめでたし。

何だかスッキリしないですが、以上です。
もっとより良い実装方法があればFBお願いします!!!

最後までお読み頂き、ありがとうございます。