Railsチュートリアルの演習で初めてちょっと考えて答えがわからなかったので、備忘録としてまとめておく。
#1.
コンソールを一度再起動して(userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。
$ user = User.find_by_name "Michael Hartl"
(2.1ms) SELECT sqlite_version(*)
User Load (1.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-02-05 15:19:40.788533000 +0000", updated_at: "2021-02-05 15:19:40.788533000 +0000", password_digest: [FILTERED]>
これは問題ない。
#2.
オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
$ user.name = "Foo Bar"
=> "Foo Bar"
$ user.save
TRANSACTION (0.2ms) begin transaction
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]
TRANSACTION (0.1ms) rollback transaction
=> false
たしかにうまく行かないようだ。
$ user.reload
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-02-05 15:19:40.788533000 +0000", updated_at: "2021-02-05 15:19:40.788533000 +0000", password_digest: [FILTERED]>
$ user.update(name:"Foo Bar")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]
TRANSACTION (0.1ms) rollback transaction
=> false
これもダメ。
なぜか調べてみるとこのような結果だった
$ user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}
パスワードが空、あるいは短すぎると怒られている。
モデルにhas_secure_passwordメソッドを適用すると、そのモデルには仮想的な属性passwordとpassword_confirmationが追加されるのだが、DBに登録されるとそれらはハッシュ化されpassword_digestという属性になってしまい、オブジェクトとして取り出した際にはpassword、password_confirmationは空になってしまうようだ。
というわけで、演習の本旨からは外れてしまうが、passwordとpassword_confirmationを適当に再度決め直してやれば、saveに成功するのではないだろうか。
$ user.password = "foobaz"
=> "foobaz"
$ user.password_confirmation = "foobaz"
=> "foobaz"
$user.save
TRANSACTION (0.1ms) begin transaction
User Exists? (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.7ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Foo Bar"], ["updated_at", "2021-02-05 16:19:49.863052"], ["id", 1]]
TRANSACTION (2.2ms) commit transaction
=> true
いけた。
3.
今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
6.15に、上で書いたようなことがちゃんと解説されていた。
例えば6.3で実装すると、パスワードの保存を要求するようになり、検証で失敗するようになります。特定の属性のみを更新したい場合は、次のようにupdate_attributeを使います。このupdate_attributeには、検証を回避するといった効果もあります。
というわけで、コンソールを再起動して再度userオブジェクトを取得し、update_attributeで名前を書き換えてみる。
$ user.update_attribute :name, "FooBar Baz"
TRANSACTION (0.1ms) begin transaction
User Update (2.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "FooBar Baz"], ["updated_at", "2021-02-05 16:26:11.383446"], ["id", 1]]
TRANSACTION (2.1ms) commit transaction
=> true
いけた。
特定の属性を更新するだけなら、たしかにupdateよりもupdate_attributeを使った方が楽そうだ。
ところで、Userモデルのname属性には最大50文字という制限がついているわけだが、検証を回避するupdate_attributeならば51文字以上の名前を設定できてしまうのだろうか?
$ user.update_attribute :name, "a" * 100
TRANSACTION (0.1ms) begin transaction
User Update (0.7ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], ["updated_at", "2021-02-05 16:29:29.627643"], ["id", 1]]
TRANSACTION (2.9ms) commit transaction
=> true
できてしまった。
$ user.reload
User Load (3.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "michael@example.com", created_at: "2021-02-05 15:19:40.788533000 +0000", updated_at: "2021-02-05 16:29:29.627643000 +0000", password_digest: [FILTERED]>
$ user.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :password=>["can't be blank", "is too short (minimum is 6 characters)"]}
奇妙な状態だが、当然reloadした直後からはnameが長すぎると怒られるようになる。
#まとめ
has_secure_passwordを適用したモデルはpassword、password_confirmationという仮想的な属性を持つようになるが、それらはDBに登録される際にハッシュ化され、password_digestという属性に変わってしまう。
再びそのオブジェクトをDBから取り出した際はpassword及びpassword_confirmationは空になってしまっているため、不適切なオブジェクトとして扱われる(valid?メソッドもfalseを返す)。
password及びpassword_confirmationを再度決めてやれば適切なオブジェクトとして扱われるようになるが、特定の属性を変更するだけならupdate_attributeメソッドを使った方が楽。
しかし、update_attributeメソッドを使う場合はバリデーションが行われず、新たに入る値が適切かは一切検証されないので、使う場合は直前で値が適切かをチェックする仕組みが必要そう。