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お願いします!!!
最後までお読み頂き、ありがとうございます。