新米パパの育児留学

新米パパの育児留学

『育児留学』とは、育児を通して異なる視点を得たり新しいことに挑戦して自己成長に繋げること。育児奮闘中の新米パパが育児を通して得た気づきや感じたこと、育休中に習得したプログラミングに関する話題を発信していきます。

【第6章】Ruby on Rails チュートリアル 5.0(第4版)演習と解答まとめ

Ruby on Rails Tutorial最新版の演習と解答です。

目的

Ruby on Rails チュートリアル 5.0(第4版)を学習中です。

学習を進める中で演習問題の解答がなかった(*1,2)ので、自分なりにまとめていくこととしました。

アウトプットし、自分の理解を深めることを目的としています。 もし、記載内容に誤りがあった場合はコメントいただけると幸いです。

(*1)著者による有償版の解答はあるようです。正式な解答をご希望の方はこちらを参照ください。

Learn Web Development with Rails: Michael Hartl's Ruby on Rails Tutorial | Softcover.io

(*2)旧版に関する解答はありましたが、最新版 5.0(第4版)に関しては検索しても出てきませんでした。

演習問題と解答

演習6.1.1

演習6.1.1.1

<問題> Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。

<解答>

[schema.rb]

ActiveRecord::Schema.define(version: 20170203234505) do

  create_table "users", force: :cascade do |t|
    t.string   "name"
    t.string   "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

[20170203234505_create_users.rb]

class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

異なる点は以下の2つ。

①「created_at,updated_at」と「t.timestamps」 ⇒本文中に「t.timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成します。」とあるので、実質構成は同じ。

②「create_table」に対して「force: :cascade」の有無 ⇒「force: :cascade」:外部キーが適切であればスキーマが再読み込みできるようになります。(参照元https://railsguides.jp/4_2_release_notes.html

ないとどうなるのかは現時点ではわからない。。。

演習6.1.1.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 マイグレーション」を参照してください。

<解答> 指示通り以下を実行

  $ rails db:rollback

[schema.rb]

ActiveRecord::Schema.define(version: 0) do

end

ロールバック完了。

演習6.1.1.3

<問題> もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。

<解答> 動作確認のみなので省略。

演習6.1.2

演習6.1.2.1

<問題> Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。

<解答>

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> User.superclass
=> ApplicationRecord(abstract)

演習6.1.2.2

<問題> 同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。

<解答>

>> ApplicationRecord.superclass
=> ActiveRecord::Base

演習6.1.3

演習6.1.3.1

<問題> user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。

<解答>

>> user.name.class
=> String

>> user.email.class
=> String

演習6.1.3.2

<問題> created_atとupdated_atは、どのクラスのインスタンスでしょうか?

<解答>

>> user.created_at.class
=> ActiveSupport::TimeWithZone

>> user.updated_at.class
=> ActiveSupport::TimeWithZone

演習6.1.4

演習6.1.4.1

<問題> nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。

<解答>

>> User.find_by(name: "Michael Hartl")
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-02-04 05:46:02", updated_at: "2017-02-04 05:46:02">

>> User.find_by_name("Michael Hartl")                                                                            
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-02-04 05:46:02", updated_at: "2017-02-04 05:46:02">

演習6.1.4.2

<問題> 実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。 User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。

<解答>

>> User.all.class
=> User::ActiveRecord_Relation

演習6.1.4.3

<問題> User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。

<解答>

>> User.all.length
  User Load (0.3ms)  SELECT "users".* FROM "users"
=> 2

演習6.1.5

演習6.1.5.1

<問題> userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。

<解答>

>> user.name="Taro Yamada"
=> "Taro Yamada"
>> user.save
   (0.2ms)  SAVEPOINT active_record_1
  SQL (1.1ms)  UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ?  [["updated_at", 2017-02-04 06:34:26 UTC], ["name", "Taro Yamada"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

演習6.1.5.2

<問題> 今度はupdate_attributes(*)を使って、email属性を更新および保存してみてください。

<解答>

>> user.update_attribute(:email,"yamada@mail.com")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "yamada@mail.com"], ["updated_at", 2017-02-04 06:37:31 UTC], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

>> user.email
=> "yamada@mail.com"

*問題では「update_attributes」となっていますが、最後の「s」は不要です。

演習6.1.5.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", 2016-02-04 06:43:54 UTC], ["updated_at", 2017-02-04 06:42:00 UTC], ["id", 1]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
=> true

>> user.created_at
=> Thu, 04 Feb 2016 06:43:54 UTC +00:00

演習6.2.1

演習6.2.1.1

<問題> コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。

<解答>

>> user=User.new(name:"Taro Yamada")                                                                              
=> #<User id: nil, name: "Taro Yamada", email: nil, created_at: nil, updated_at: nil>

>> user.valid?
=> true

演習6.2.1.2

<問題> 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

<解答>

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

>> 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

演習6.2.2.1

<問題> 新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。

<解答>

>> u=User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

>> u.valid?
=> false

>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]

演習6.2.2.2

<問題> u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?

<解答>

>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

>> u.errors.messages[:email]
=> ["can't be blank"]

演習6.2.3

演習6.2.3.1

<問題> 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

<解答>

>> user=User.new(name:"123456789012345678901234567890123456789012345678901",email:"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890@mail.com")
=> #<User id: nil, name: "12345678901234567890123456789012345678901234567890...", email: "12345678901234567890123456789012345678901234567890...", created_at: nil, updated_at: nil>

>> user.valid?
=> false

演習6.2.3.2

<問題> 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

<解答>

>> user.errors.full_messages
=> ["Name is [f:id:mochikichi321:20170218200721p:plain][f:id:mochikichi321:20170218200723p:plain]too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]

演習6.2.4

演習6.2.4.1

<問題> リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

<解答> f:id:mochikichi321:20170218200721p:plain

演習6.2.4.2

<問題> 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。 <解答> “foo@bar..com"を追加。

[user_test.rb]

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

(中略)

  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

テスト結果はRED。 リスト 6.23のとおり修正してテストすると、GREEN。

演習6.2.4.3

<問題> foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

<解答>

f:id:mochikichi321:20170218200723p:plain

演習6.2.5

演習6.2.5.1

<問題> リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。

<解答> 動作確認のみなので省略。

演習6.2.5.2

<問題> テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。

<解答> リスト6.34参照。

演習6.3.2

演習6.3.2.1

<問題> この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。

<解答>

>> user=User.new(name:"Taro Yamada",email:"yamada@mail.com")
=> #<User id: nil, name: "Taro Yamada", email: "yamada@mail.com", created_at: nil, updated_at: nil, password_digest: nil>

>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "yamada@mail.com"], ["LIMIT", 1]]
=> false

演習6.3.2.2

<問題> なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。

<解答>

>> user.errors.full_messages
=> ["Password can't be blank"]

演習6.3.3

演習6.3.3.1

<問題> 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。

<解答>

>> user=User.new(name:"Taro Yamada",email:"yamada@mail.com",password:"12345")
=> #<User id: nil, name: "Taro Yamada", email: "yamada@mail.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$W699oKChtVu6Hd2D4uDSsOb9KI6i0HiDcNvobt7xhfW...">

>> user.valid?
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "yamada@mail.com"], ["LIMIT", 1]]
=> false

演習6.3.3.2

<問題> 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。

<解答>

>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]

演習6.3.4

演習6.3.4.1

<問題> コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。

<解答>

>> user = User.find_by(email: "mhartl@example.com")
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "mhartl@example.com"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-02-05 02:01:37", updated_at: "2017-02-05 02:01:37", password_digest: "$2a$10$E.gAC.d0Ch5dvmrM6vnNhu6gIpNtwFD8JhHmKPyc/V3...">

演習6.3.4.2

<問題> オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね…、なぜうまくいかなかったのでしょうか?

<解答>

>> user.name="Yamada"
=> "Yamada"

>> user.save
   (0.2ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
=> false

name属性を変更し、.saveを実行すると、変更していない箇所も全て保存を要求される。 すなわち、パスワードをレコードに保存することを要求するため、エラーが生じる。 変更及び保存をname属性のみに限定する必要がある。(6.3.4.3参照)

演習6.3.4.3

<問題> 今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。

<解答>

>> user.update_attribute(:name, "Yamada")
   (0.1ms)  begin transaction
  SQL (0.4ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Yamada"], ["updated_at", 2017-02-05 02:36:54 UTC], ["id", 1]]
   (10.0ms)  commit transaction
=> true

“updated_at"が更新されており、保存がうまくいったことがわかる。

関連記事

mochikichi.hatenablog.com

広告を非表示にする