Ruby on Railsチュートリアルの演習問題と解答をまとめる。
第6章 ユーザーのモデルを作成する - Railsチュートリアル
アウトプットすることで、より自分の理解を深めることを目的としています。
自分なりに調べて考えた回答のため、記載内容に誤りがある場合はコメントいただけると幸いです。
- 演習6.1.1 データベースの移行
- 演習6.1.2 modelファイル
- 演習6.1.3 ユーザーオブジェクトを作成する
- 演習6.1.4 ユーザーオブジェクトを検索する
- 演習6.1.5 ユーザーオブジェクトを更新する
- 演習6.2.1 有効性を検証する
- 演習6.2.2 存在性を検証する
- 演習6.2.3 長さを検証する
- 演習6.2.4 フォーマットを検証する
- 演習6.2.5 一意性を検証する
- 演習6.3.2 ユーザーがセキュアなパスワードを持っている
- 演習6.3.3 パスワードの最小文字数
- 演習6.3.4 ユーザーの作成と認証
演習6.1.1 データベースの移行
問題1
Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
解答
マイグレーションファイルの情報がスキーマファイルに反映されている!?
問題2
ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。 $ rails db:rollback 上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。
解答
動作確認のため省略
問題3
もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください
解答
動作確認のため省略
演習6.1.2 modelファイル
問題1
Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。
解答
>> user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> user.class => User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime) >> user.class.superclass => ApplicationRecord(abstract)
User > ApplicationRecord(abstract)
問題2
同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。
解答
>> user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> user.class => User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime) >> user.class.superclass => ApplicationRecord(abstract) >> user.class.superclass.superclass => ActiveRecord::Base
User < ApplicationRecord(abstract) < ActiveRecord::Base
演習6.1.3 ユーザーオブジェクトを作成する
問題1
user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
解答
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil> >> user.save (中略) => true >> user.name.class => String >> user.email.class => String
問題3
created_atとupdated_atは、どのクラスのインスタンスでしょうか?
解答
>> user.created_at.class => ActiveSupport::TimeWithZone >> user.updated_at.class => ActiveSupport::TimeWithZone
演習6.1.4 ユーザーオブジェクトを検索する
問題1
nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。
解答
>> User.find_by(name: "Michael Hartl") User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Har tl"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-10-21 07:34:03", upda ted_at: "2019-10-21 07:34:03">
>> User.find_by_name("Michael Hartl") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Har tl"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2019-10-21 07:34:03", upda ted_at: "2019-10-21 07:34:03">
どちらも同じ機能を持っている。
問題2
実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。
解答
>> users = User.all User Load (0.2ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: " 2019-10-21 07:34:03", updated_at: "2019-10-21 07:34:03">]> >> users.class => User::ActiveRecord_Relation
問題3
User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
解答
>> User.all.length User Load (0.1ms) SELECT "users".* FROM "users" => 1
一人しかユーザーを作っていないので結果は、1
演習6.1.5 ユーザーオブジェクトを更新する
問題1
userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
解答
>> user = User.new(name: "panda", email: "panda@example.com") => #<User id: nil, name: "panda", email: "panda@example.com", created_at: nil, updated_at: nil> >> user.save (中略) => true >> user.name = "panda P" => "panda P" >> user.save (0.1ms) SAVEPOINT active_record_1 SQL (0.2ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "panda P"], ["updated_at", "2019-10-21 07:52:36.867592"], ["id", 2]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
問題2
今度はupdate_attributesを使って、email属性を更新および保存してみてください。
解答
>> user.update_attribute(:email, "pandap@example.com") (0.1ms) SAVEPOINT active_record_1 SQL (0.2ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "pandap@example.com"], ["updated_at", "2019-10-21 07:56:14.272613"], ["id", 2]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
特定の属性のみ更新したい場合は、update_attributeを使う。
update_attributesだと失敗する。
update_attribute(s)を使うとsaveしなくても保存してくれる。
問題3
同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
解答
>> user.update_attribute(:created_at, 1.year.ago) (0.1ms) SAVEPOINT active_record_1 SQL (0.2ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", "2018-10-21 08:03:12.515150"], ["updated_at", "2019-10-21 08:03:12.525676"], ["id", 2]] (0.2ms) RELEASE SAVEPOINT active_record_1 => true
演習6.2.1 有効性を検証する
問題1
コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。
解答
>> user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> user.valid? => true
問題2
6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
解答
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil> >> user.valid? => true
演習6.2.2 存在性を検証する
問題1
新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
解答
>> u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> u.save (0.1ms) begin transaction (0.1ms) rollback transaction => false >> u.valid? => false >> u.invalid? => true >> u.errors.messages => {:name=>["can't be blank"], :email=>["can't be blank"]}
問題2
u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
解答
>> u.errors.messages[:email] => ["can't be blank"]
演習6.2.3 長さを検証する
問題1
長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
解答
>> user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> >> user.name = "a" * 100 => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >> user.email = "a" * 300 => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" >> user.save (0.2ms) begin transaction (0.1ms) rollback transaction => false >> user.errors.messages => {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}
問題2
長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
解答
>> user.errors.messages => {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}
演習6.2.4 フォーマットを検証する
問題1
リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
解答
なぜか一つだけマッチしない。。
問題2
先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
解答
[test/models/user_test.rb] require 'test_helper' class UserTest < ActiveSupport::TestCase (中略) test "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com foo@bar..com] <- #追加 invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end end
foo@bar..comをつかするとテストが失敗する。
リスト6.23を参照し、修正するとテストが通る
問題3
foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
解答
なんか一人、、
演習6.2.5 一意性を検証する
問題1
リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
解答
リスト6.33参照
問題2
テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
解答
リスト6.34に変更してもテストは通る
演習6.3.2 ユーザーがセキュアなパスワードを持っている
問題1
この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。
解答
>> user = User.new(name: "honda", email: "honda@example.com") => #<User id: nil, name: "honda", email: "honda@example.com", created_at: nil, updated_at: nil, password_dig est: nil> >> user.valid? User Exists (1.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["ema il", "honda@example.com"], ["LIMIT", 1]] => false
問題2
なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
解答
>> user.errors.messages => {:password=>["can't be blank"]}
演習6.3.3 パスワードの最小文字数
問題1
有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
解答
>> user = User.new(name: "ooota", email: "ooota@example.com", password: "ota", password_confirmation: "ota") => #<User id: nil, name: "ooota", email: "ooota@example.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$GOHDmSnX12eGpVRw3NmHLe2liJXXSBqiwNaoAUtRTV...."> >> user.save (中略) => false
問題2
上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
解答
>> user.errors.messages => {:password=>["is too short (minimum is 6 characters)"]}
演習6.3.4 ユーザーの作成と認証
問題1
コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。
解答
>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "MIchael Hartl", email: "mhartl@example.com", created_at: "2019-10-21 13:25:29", updated_at: "2019-10-21 13:25:29", password_digest: "$2a$10$7NLTw29m6mN8OY5aghw.hOW7UQIMuo5VnruWHtE61ky...">
問題2
オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
解答
>> user.name = "boby" => "boby" >> user.save (中略) => false >> user.errors.messages => {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}
パスワードが空です、短すぎます。
問題3
今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
解答
>> user.update_attribute(:name, "Kitaro") (中略) => true >> user.name => "Kitaro"
更新成功
おわりや!