(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第6章】


前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

続いて第6章入りまーす。こっから第12章まで、ログインと認証システムの開発に取り掛かるそうな。長丁場ですがやりきってやりましょう。

本日の一曲はこちら。
17歳とベルリンの壁 "プリズム"
ここ数年あんまり音楽開拓してなかったけど、良い国産シューゲバンドが出てきるな〜。

 

【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 マイグレーション」を参照してください。
→ rails db:migrateを実行。スキーマの中身が消えてます。

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

end

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

 

【6.1.2 modelファイル 演習】

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

 

【6.1.3 ユーザーオブジェクトを作成する 演習】

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

>> user.name.class
=> String
>> user.email.class
=> String

 
2. created_atとupdated_atは、どのクラスのインスタンスでしょうか?
→ 両方ともActiveSupport::TimeWithZoneクラス(この記事が参考になりそう)

>> 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: "shoji")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "shoji"], ["LIMIT", 1]]
=> #<User id: 2, name: "shoji", email: "[email protected]", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">
>> User.find_by_name("shoji")
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "shoji"], ["LIMIT", 1]]
=> #<User id: 2, name: "shoji", email: "[email protected]", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">

 
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: "miura", email: "[email protected]", created_at: "2020-09-08 22:53:59", updated_at: "2020-09-08 22:53:59">, #<User id: 2, name: "shoji", email: "[email protected]", created_at: "2020-09-08 22:54:09", updated_at: "2020-09-08 22:54:09">]>
>> users.class
=> User::ActiveRecord_Relation
>> users.class.superclass
=> ActiveRecord::Relation
>> users.class.superclass.superclass
=> Object
>> users.class.superclass.superclass.superclass
=> BasicObject
>> users.class.superclass.superclass.superclass.superclass
=> nil

 
3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
→ データ数が表示されました。

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

 

【6.1.5 ユーザーオブジェクトを更新する 演習】

1. userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
→ 下記

>> user1.name = "yongon"
=> "yongon"
>> user1.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.6ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "yongon"], ["updated_at", "2020-09-08 23:12:52.428275"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 
2. 今度はupdate_attributesを使って、email属性を更新および保存してみてください。
→ 下記(ミスっていろいろやり直してるのidがずれてます)

>> user1.update_attributes(name: "yongon", email: "[email protected]")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "[email protected]"], ["updated_at", "2020-09-09 03:12:33.687572"], ["id", 4]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 
3. 同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
→ 下記

>> user1.update_attribute(:created_at, 1.year.ago)
   (0.1ms)  SAVEPOINT active_record_1
  SQL (1.1ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2019-09-09 03:18:24.829284"], ["updated_at", "2020-09-09 03:18:24.830017"], ["id", 4]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true

 

【6.2.1 有効性を検証する メモと演習】

 setupメソッドを使うと、メソッド内に書かれた処理がテスト直前に実行される。この中でインスタンス変数を定義しておけば、すべてのテスト内で使えるようになる。

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オブジェクトも有効であるかどうか、確認してみましょう。
→ 一回コンソール閉じてるから消えてるよ〜。どうせ有効なので割愛。

 

【6.2.2 存在性を検証する メモと演習】

 assert_not @user.valid? がRED
→ 「@userは有効ちゃうよな?」と主張してるのに、「有効やんけ!」とツッコミが入ってる状態 と考えると分かりやすい。
 validatesにも出てくるが、メソッドの最後の引数としてハッシュを渡す場合、{ }は省略可能

1. 新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
→ nameもemailも入力してないからヴァリデーションが働いてます。

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

  
2. u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
→ このページに書いてました。.messagesつけてもつけなくても一緒ですね。

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

 

【6.2.3 長さを検証する 演習】

1. 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
→ まとめて下記

>> user = User.new(name: "a"*55, email: "e"*244 + "@example.com")
=> #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee...", created_at: nil, updated_at: nil>
>> user.valid?=> false
>> user.errors.full_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のメールアドレスチェックする正規表現は、[email protected]のようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
→ これもやるだけー。
 
3. [email protected]をRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
→ 確認しました。

 

【6.2.5 一意性を検証する メモと演習】

 ここの内容ちょっとややこしいけど、要はメールアドレスに関して大文字・小文字を区別しないように設定してると。テストは大文字ユーザーが有効ででない(大文字でも同じアドレス)ことを確かめてるわけか。
 そして、メールアドレスがデータベースに保存される前に、すべてを小文字にするためにコールバックメソッドが登場しました。軽く調べたところ、コールバックの利用は慎重にしないといけないようです。コールバックの中で条件分岐とか避けた方がいいみたい。

1. リスト 6.33のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
→ 指示通り実行。before_saveで保存前に小文字に変換しているので、コメントアウトするとREDに、解除するとGREENになります。

2. テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
→ ここで出てくる「!」は、破壊的な処理を表します。つまり、メールアドレスが小文字変換されたままで維持されるということ。書き換えてもテストはGREENです。

 

【6.3.2 ユーザーがセキュアなパスワードを持つ 演習】

1. この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。 
2. なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
→ まとめていくよ!パスワードが空白はダメだってよ。

>> user = User.new(name: "kote", email: "[email protected]")
=> #<User id: nil, name: "kote", email: "[email protected]", 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", "[email protected]"], ["LIMIT", 1]]
=> false
>> user.errors.messages
=> {:password=>["can't be blank"]}

 

【6.3.3 パスワードの最小文字数 演習】

1. 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
2. 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
→ 今回もまとめていくよ!もちろんパスワードが短すぎると怒られます。ここでhas_secure_passwordが働いて、パスワードがハッシュ化されているのが分かりますね。

>> user = User.new(name: "kote", email: "[email protected]", password: "kotte")
=> #<User id: nil, name: "kote", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: "$2a$10$7Svz/KnRoF7zab0PnhKFL.n/OsSltRvvREHECcmuq.D...">
>> user.valid?  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
=> false
>> user.errors.messages=> {:password=>["is too short (minimum is 6 characters)"]}

 

【6.3.4 ユーザーの作成と認証 演習】

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

>> user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]", created_at: "2020-09-10 02:37:56", updated_at: "2020-09-10 02:37:56", password_digest: "$2a$10$A5n.HFBigQfwnWVJZw2N0e4M9sxPaR8ndLZwqtZWYS7...">

 
2. オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
→ 下記。saveメソッドだと全ての属性を更新しようとするから、パスワードの更新でエラー吐いてるみたい。

>> user.name = "meshino"
=> "meshino"
>> user.save
   (0.1ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "[email protected]"], ["id", 1], ["LIMIT", 1]]
   (0.0ms)  rollback transaction
=> false
>> user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

 
3. 今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
→ ということで、update_attributeを使って更新します。(ただし、さっきエラー吐いたuserを再利用しているので、name更新後も有効ではありません。パスワードの再設定が必要と思われます)

>> user.update_attribute(:name, "nakamura")
   (0.1ms)  begin transaction
  SQL (4.2ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "nakamura"], ["updated_at", "2020-09-10 03:07:11.190666"], ["id", 1]]
   (6.4ms)  commit transaction
=> true
>> user.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "[email protected]"], ["id", 1], ["LIMIT", 1]]
=> false
>> user.errors.messages=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

 

第6章まとめ

・データベースを更新する時はつどつどマイグレーションを作成してマイグレートしよう。
・Active Recordは便利。Railsは下火になってきてるけどActive Recordは利点とどっかで書かれてた。
・バリデーションで無効な入力内容を設定して弾こう。
正規表現は多分そういうもんなんだと認識する程度でよいと思う。
R3.1.9 正規表現大事だし、すごい機能を持ってます!ある程度の知識は身に付けましょう!
・データベースにインデックス追加で検索効率向上。一意性も保証。
・has_secure_passwordはgemで利用しているわけだから、他にも便利なgemでセキュアな処理が実装できるんかな。と思って検索したらやっぱり出てきました。今後学んでいこう。

 
 いろいろ気になることを寄り道して調べているので時間がかかります。でも絶対チュートリアルの内容だけでは通用しないと思うので、他の様々なことにも興味を持って吸収していきます。
 さて次!第7章はユーザー登録の実装です!sign up!

 

⇨ 第7章へ!
⇦ 第5章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・assert_not
 notで否定してるので、まんま逆の意味。対象が真なら失敗、偽なら成功。

・Active Record コールバック
 何かの処理の前/後に呼び出すメソッドを設定できる。詳しくはRailsガイドへ。

・スタブ
 テスト時に用意する代用品。テスト対象の処理から呼び出される代用品がスタブ、テスト対象の処理を呼び出す代用品がドライバ。