疑問:空文字列がnil判定される?
会社の研修で Rails Tutorial をやっていたとき、第10章「ユーザーの更新」のところでパスワードの入力欄が空の場合でもエラーを出さないようにする、という項目がありました
Tutorialの説明によると User Model のバリデーションのところに allow_nil?
をつけることによって例外処理を加える、とあります
password 属性のバリデーションに例外として allow_nil?
を加えることによって条件を満たしていない場合でも値が nil の場合には例外的にバリデーションを通過させるようにする、という記述です
しかし User Controller に debugger を埋め込んで実際にフォームに渡っているデータを確認すると
(ruby) user_params
<ActionController::Parameters {"name"=>"Rails Tutorial", "email"=>"example@railstutorial.com", "password"=>"", "password_confirmation"=>""} permitted: true>
ということで password は nil ではなく空文字列("")が入っていました
ruby の仕様では空文字列は nil とは判定されません
3.1.2 :001 > "".nil?
=> false
allow_nil?
はあくまでnilのみを例外として扱い、空文字列や空白文字は通常のバリデーション処理が行われるため、一見するとこのままでは例外処理がうまく動かなさそうです
答え:has_secure password の仕様
しかし実際には User model で記述されている has_secure_password の仕様によって、空文字列("")を代入した場合はユーザーの password 属性に値が代入されないようになっています
このことを簡単な実験によって確かめてみましょう
まず適当なユーザーインスタンスを作成し name 属性に空文字列を代入してみます
3.1.2 :002 > user = User.first
=> ""
3.1.2 :003 > user.name = ""
=> ""
3.1.2 :004 > user.name
=> ""
想定通り user.name
には空文字列("")が代入されています
今度は password 属性に空文字列を代入してみましょう
3.1.2 :005 > user.password = ""
=> ""
3.1.2 :006 > user.password
=> nil
なんと空文字列を代入したはずなのに実際に user.password
に代入されている値は nil となっています
どうしてこんなことが起こるのか、has_secure_password のソースコードを見て挙動を確認してみます
該当箇所は118-127行目のInstanceMethodActivationクラスの中にある以下の記述です
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
instance_variable_set("@#{attribute}", nil)
self.public_send("#{attribute}_digest=", nil)
elsif !unencrypted_password.empty?
instance_variable_set("@#{attribute}", unencrypted_password)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
この部分はセッターと呼ばれる部分でインスタンス変数の password 属性への値の代入が定義されています
このメソッドで unencrypted_password
が空文字列("")である場合を考えてみましょう
まず空文字列は nil ではないので一番目の条件式は false となります
続いて二番目の条件式も "".empty?
が true であることを考えると、その否定なので条件式全体としては false となります
ここで password 属性への代入は instance_variable_set
メソッドで行われていることを考慮すると、値が空文字列の場合このメソッドでは何も実行されないことになります
結果的に password 属性に代入した空文字列("")はモデルのインスタンス変数としては nil が代入されるわけです
allow_nil?
が評価されるタイミングはフォームのパラメータがPOSTされるタイミングではなくモデルのインスタンスが save
されるタイミングですので、今回password属性のバリデーションとして評価されるのは空文字列ではなく nil
となり、無事バリデーションを通過することができるのでした
ひとこと
今回のようなポイントは何気なくチュートリアルを進めてしまうと疑問に思わずに通り抜けてしまう人も多いと思います
しかしよくよく考えてみると空文字列が渡っているはずの password で allow_nil?
が成立するのは不思議です
初心者向けのTutorialでも、丁寧に読んでいくと、最終的には rails のソースコードを見なければわからないようなトピックが隠れているのは面白いと思いました
本記事で書いた現象について一緒に調査してくれた会社の同期3人に感謝します