Ruby on Rails チュートリアルで30歳までに人生を変える(第14章 完)
こんにちは。opiyoです。
今回は、第14章をやっていきます。
第14章はフォロー、フォロワーする機能を追加します。
なんとなんと最後の章までやってまいりました。
ではでは、早速行ってみましょう。
railsチュートリアル14章の演習解説
14.1.1 演習 データモデルの問題 (および解決策)
14.1.1.1
<問題> 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
<回答>
[2,7,10,8]
14.1.1.2
<問題>図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
<回答>
[1]
14.1.2 演習 User/Relationshipの関連付け
14.1.2.1
<問題> コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
<回答>
> user_a.active_relationships.create(followed_id: user_b.id) (0.1ms) begin transaction User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 101], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 102], ["LIMIT", 1]] SQL (7.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 101], ["followed_id", 102], ["created_at", 2017-08-31 01:06:19 UTC], ["updated_at", 2017-08-31 01:06:19 UTC]] (1.9ms) commit transaction => #<Relationship:0x007fdb1fbac6b8 id: 88, follower_id: 101, followed_id: 102, created_at: Thu, 31 Aug 2017 01:06:19 UTC +00:00, updated_at: Thu, 31 Aug 2017 01:06:19 UTC +00:00>
14.1.2.2
<問題> 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
<回答>
active_relationship.followed
とactive_relationship.follower
を実行するとエラーになるので確認できない。
# 14.1.4 フォローしているユーザー で出てくるやり方を使えば、フォローしている、していないの確認ができそう! > user_a.following?(user_b) User Exists (13.0ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 101], ["id", 102], ["LIMIT", 1]] => true
14.1.3 演習 Relationshipのバリデーション
14.1.3.1
<問題>リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
<回答> コメントアウトしてもテストは成功することを確認
14.1.4 演習 フォローしているユーザー
14.1.4.1
<問題>コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
<回答>
> michael = User.find_by(name: "michael") User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "michael"], ["LIMIT", 1]] => #<User:0x007fdb1b727628 id: 103, name: "michael", email: "michael@co.jp", created_at: Thu, 31 Aug 2017 02:00:21 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:00:28 UTC +00:00, password_digest: "$2a$10$2hC.1.fUg3T8GUKMX.EnTOGDcIEHpHryFQg69YKmljRALWUeKsyYG", remember_digest: nil, admin: false, activation_digest: "$2a$10$rglVP8eQVwnwE/diLFGPyuZJjA0lDGOtMwqSmewgeDIEdxU9N7Fk6", activated: true, activated_at: Thu, 31 Aug 2017 02:00:28 UTC +00:00, reset_digest: nil, reset_sent_at: nil> [50] pry(main)> archer = User.find_by(name: "archer") User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "archer"], ["LIMIT", 1]] => #<User:0x007fdb19dfc1a8 id: 104, name: "archer", email: "archer@co.jp", created_at: Thu, 31 Aug 2017 02:00:39 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:00:41 UTC +00:00, password_digest: "$2a$10$QMPgyffkll/ps.Mh798Kd.97jtJa.gW3mjEGmLlAme5tmL//IhXJG", remember_digest: nil, admin: false, activation_digest: "$2a$10$PygPxQjeBFgeXIKwPuQcqOIbwLevHEs2QXs5Tyvn6m05T7w7Vsc4C", activated: true, activated_at: Thu, 31 Aug 2017 02:00:41 UTC +00:00, reset_digest: nil, reset_sent_at: nil> [51] pry(main)> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 103], ["id", 104], ["LIMIT", 1]] => false [52] pry(main)> michael.follow(archer) (0.1ms) begin transaction User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 103], ["LIMIT", 1]] User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 104], ["LIMIT", 1]] SQL (14.0ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 103], ["followed_id", 104], ["created_at", 2017-08-31 02:01:33 UTC], ["updated_at", 2017-08-31 02:01:33 UTC]] (1.9ms) commit transaction => #<Relationship:0x007fdb221c9620 id: 89, follower_id: 103, followed_id: 104, created_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00> [53] pry(main)> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 103], ["id", 104], ["LIMIT", 1]] => true [54] pry(main)> michael.unfollow(archer) Relationship Load (0.2ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 103], ["followed_id", 104], ["LIMIT", 1]] (0.0ms) begin transaction SQL (0.3ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 89]] (1.8ms) commit transaction => #<Relationship:0x007fdb22bae5f0 id: 89, follower_id: 103, followed_id: 104, created_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00, updated_at: Thu, 31 Aug 2017 02:01:33 UTC +00:00> [55] pry(main)> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 103], ["id", 104], ["LIMIT", 1]] => false
14.1.4.2
<問題>先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
<回答>
> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 103], ["id", 104], ["LIMIT", 1]] > michael.follow(archer) (0.1ms) begin transaction User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 103], ["LIMIT", 1]] User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 104], ["LIMIT", 1]] SQL (14.0ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 103], ["followed_id", 104], ["created_at", 2017-08-31 02:01:33 UTC], ["updated_at", 2017-08-31 02:01:33 UTC]] > michael.unfollow(archer) Relationship Load (0.2ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 103], ["followed_id", 104], ["LIMIT", 1]] SQL (0.3ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 89]]
14.1.5 演習 フォロワー
14.1.5.1
<問題>コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
<回答>
# ランダムに6人取ってくる > users = User.order("RANDOM()").limit(6) # その6人が一番目のユーザーをフォローする > users.each {|user| user.follow(User.first)} # 一番目のユーザーのフォロワーを確認 > User.first.followers.map(&:id) User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => [36, 20, 60, 74, 87, 53]
14.1.5.2
<問題>上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
<回答>
> User.first.followers.count User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 6
14.1.5.2
<問題>user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
<回答>
> User.first.followers.to_a.count User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 6
to_a
付けても変化はない。が、多分配列にした数を数える感じになるから遅くなる。
14.2.1 演習 フォローのサンプルデータ
14.2.1.1
<問題>コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。
<回答>
> User.first.followers.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 38 > (3..40).size => 38
14.2.1.2
<問題>先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。
<回答>
> User.first.following.count User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 49 > (2..50).count => 49
14.2.2 演習 統計と [Follow] フォーム
14.2.2.1
<問題>ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?
<回答> - /users/2:フォローボタンが表示 - /users/5:アンフォローボタンが表示 - /users/1:何も表示されない(ログインユーザーだから)
14.2.2.1
<問題>ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
<回答> 表示されている。
14.2.2.1
<問題>Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28に示したテストを追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
<回答>
# test/integration/users_profile_test.rb require 'test_helper' class UsersProfileTest < ActionDispatch::IntegrationTest include ApplicationHelper def setup @user = users(:michael) end test "profile display" do get user_path(@user) assert_template 'users/show' assert_select 'title', full_title(@user.name) assert_select 'h1', text: @user.name assert_select 'h1>img.gravatar' assert_match @user.following.count.to_s, response.body assert_match @user.followers.count.to_s, response.body assert_match @user.microposts.count.to_s, response.body assert_select 'div.pagination' @user.microposts.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end end end
14.2.3 演習 [Following] と [Followers] ページ
14.2.3.1
<問題>ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
<回答> 表示されている。また、画像のリンクも機能している。
14.2.3.1
<問題>リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。
<回答>
# app/views/users/show_follow.html.erb <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div>
多分ここだと思う。assert_select
で確認しているのはaタグのリンク先とユーザーのパス(users/5)が合致しているかだから。
14.2.4 演習 [Follow] ボタン (基本編)
14.2.4.1
<問題>ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
<回答> followersの数字が変わるし、ボタンも変わるので機能している。
14.2.4.1
<問題>先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
<回答>
users/show.html.erb
が描画される
14.2.5 演習 [Follow] ボタン (Ajax編)
14.2.5.1
<問題>ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
<回答> うまく動いていることを確認。
14.2.5.2
<問題>先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
<回答>
relationships/create.js.erb
とrelationships/destroy.js.erb
が呼ばれているログが見つからない。
14.2.6 演習 フォローをテストする
14.2.6.1
<問題>リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
<回答>
- format.html { redirect_to @user and return}
の場合は、htmlを呼び出すテストが失敗する
- 28: "should follow a user the standard way"
- 40: "should unfollow a user the standard way"
format.js
の場合は、jsを呼び出すテストが失敗しない
14.2.5.2といい、ちゃんと呼ばれてないのかな?
14.2.6.2
<問題>リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。
<回答>
format.html
、format.js
のどちらかの行を削除した時ってことかな?
その場合は、14.2.6.1の回答と同じになっちゃうな。
とりあえずパス。
14.3.1 演習 動機と計画
14.3.1.1
<問題>マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。
<回答>
[10,9,7,5,4,2,1]
14.3.2 演習 フィードを初めて実装する
14.3.2.1
<問題>リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
<回答>
def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids})", user_id: id) end
# 取得件数が減ったからページネーションされないってことじゃないかな? FAIL["test_micropost_interface", MicropostsInterfaceTest, 1.4457496699978947] test_micropost_interface#MicropostsInterfaceTest (1.45s) Expected at least 1 element matching "div.pagination", found 0.. Expected 0 to be >= 1. test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>' # こっちが今回で欲しかったエラーだね FAIL["test_feed_should_have_the_right_posts", UserTest, 1.505586264996964] test_feed_should_have_the_right_posts#UserTest (1.51s) Expected false to be truthy. test/models/user_test.rb:106:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:105:in `block in <class:UserTest>'
14.3.2.2
<問題>リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
<回答>
def feed Micropost.where(user_id: id) end
FAIL["test_feed_should_have_the_right_posts", UserTest, 3.7484980859990173] test_feed_should_have_the_right_posts#UserTest (3.75s) Expected false to be truthy. test/models/user_test.rb:102:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:101:in `block in <class:UserTest>'
14.3.2.3
<問題>リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
<回答>
ようは全部!ってことだと思うんだが。
def feed Micropost.all end
FAIL["test_feed_should_have_the_right_posts", UserTest, 6.150512954001897] test_feed_should_have_the_right_posts#UserTest (6.15s) Expected true to be nil or false test/models/user_test.rb:110:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:109:in `block in <class:UserTest>'
14.3.3 演習 サブセレクト
14.3.3.1
<問題>Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
<回答>
# test/integration/following_test.rb test "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end
14.3.3.2
<問題>リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。
<回答>
(byebug) CGI.escapeHTML(micropost.content) "I'm sorry. Your words made sense, but your sarcastic tone did not." # 「'」これがエスケープされる、されないの違い (byebug) micropost.content "I'm sorry. Your words made sense, but your sarcastic tone did not."