演習1
図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。(ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。)
id1のユーザがフォローしているユーザーidを配列で表示します
図で言えば(2,7,10,8)の順になります
図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? 想像してみてください。また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
id2のユーザーがフォローしているユーザーのid1が表示される
演習2
コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
> user1 =User.first
> user2 = User.second
> user1.active_relationships.create(followed_id: 2)
=>
#<Relationship:0x00007f7df4b29890
id: 1,
follower_id: 1,
followed_id: 2,
created_at: Thu, 19 Oct 2023 21:53:50.727762000 UTC +00:00,
updated_at: Thu, 19 Oct 2023 21:53:50.727762000 UTC +00:00>
先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
user1.active_relationships
Relationship Load (0.2ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? [["follower_id", 1]]
=>
[#<Relationship:0x00007f7df4b29890
id: 1,
follower_id: 1,
followed_id: 2,
created_at: Thu, 19 Oct 2023 21:53:50.727762000 UTC +00:00,
updated_at: Thu, 19 Oct 2023 21:53:50.727762000 UTC +00:00>]
演習3
リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5以降は必須ではなくなりました。ここでは念のためこのバリデーションを省略していませんが、このバリデーションが省略されているのを見かけるかもしれないので、覚えておくと良いでしょう。)
rails testをして、GREENになることを確認しましょう
演習4
コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
リスト 14.9ではmichael,archerとなっていますが、gemのseedがあるので、その中のキャラクター同士でフォローしてしまいます
user1 = User.first
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=>
#<User:0x00007fca3077e320
...
irb(main):002> user2 = User.second
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=>
#<User:0x00007fca304a4078
...
irb(main):005> user1.following?(user2)
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", 1], ["id", 2], ["LIMIT", 1]]
=> false
user1.follow(user2)
TRANSACTION (0.1ms) begin transaction
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Relationship Create (0.3ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2023-10-21 05:12:17.324615"], ["updated_at", "2023-10-21 05:12:17.324615"]]
TRANSACTION (29.0ms) commit transaction
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]]
=>
(中略)
irb(main):007> user1.following?(user2)
=> true
irb(main):008> user1.unfollow(user2)
TRANSACTION (0.1ms) begin transaction
Relationship Delete All (0.3ms) DELETE FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? [["follower_id", 1], ["followed_id", 2]]
TRANSACTION (28.4ms) commit transaction
=>
(中略)
user1.following?(user2)
=> false
先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
上記のSELECT、FROMなどが記述されている部分がSQLの出力されている部分になります
演習5
コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
#2番目のユーザーと、3番目のユーザをusersとし、代入します
users = User.second,User.third
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 2]]
=>
[#<User:0x00007fe665a51a60
...
#eachメソッドを使い、usersの2つの要素に順番に1番目のユーザーをフォローさせていきます
users.each do |user|
user.follow(User.first)
end
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
TRANSACTION (0.1ms) begin transaction
#2番目と3番目のユーザーが1番目のユーザーをフォローしていることを確認
User.second.following?(User.first)
=> true
User.third.following?(User.first)
=> true
user = User.first
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=>
#<User:0x00007fe6657147f8
...
1番目のユーザーが2、3番目のユーザーにフォローされていることを確認
irb(main):021> user.followers.map(&:id)
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> [2, 3]
上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
user.followers.count
User Count (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> 2
user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか?(ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。)
user.followers.to_a.countはSQLが出力されません
user.followers.to_は一度配列を取り出すメソッドになります
それをcountメソッドでカウントするので、100万人のフォロワーを一度取り出し計算するわけですから
膨大な量のデータを取り出すので、DBに負担が掛かります
user.followers.count
User Count (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> 2
irb(main):023> user.followers.to_a.count
=> 2
演習6
コンソールを開いて、User.first.followers.countの結果がリスト 14.14で期待される結果と一致していることを確認してみましょう。
User.first.followers.count
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
User Count (0.1ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> 38
上の演習と同様に、User.first.following.countの結果も一致していることを確認してみましょう。
User.first.following.count
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
User Count (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]]
=> 49
演習7
ブラウザから/users/2にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5では[Unfollow]ボタンが表示されているはずです。さて、/users/1にアクセスすると、どのような結果が表示されるでしょうか?
users/1
ログインしているページなので、自分にフォローの操作はできません
ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
一致していることを確認しましょう
Homeページに表示されている統計情報に対してテストを書いてみましょう。同様にして、プロフィールページにもテストを追加してみましょう。(ヒント: リスト 13.29で示したテストに追加してみてください。)
test/integration/site_layout_test.rb
※home.html.erbをパーシャルしている場合は、ここではエラーが出ます
_home_logged_in.html.erbに統計を記述しているため
require "test_helper"
class SiteLayoutTest < ActionDispatch::IntegrationTest
test "layout links" do
get root_path
assert_template 'static_pages/home'
assert_select "a[href=?]", root_path, count: 2
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path
assert_match @user.following.count.to_s, response.body
assert_match @user.followers.count.to_s, response.body
end
end
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.microposts.count.to_s, response.body
assert_select 'div.pagination',count: 1 #ここを追加
@user.microposts.paginate(page: 1).each do |micropost|
assert_match micropost.content, response.body
end
assert_match @user.following.count.to_s, response.body
assert_match @user.followers.count.to_s, response.body
end
end
演習8
ブラウザで/users/1/followersと/users/1/followingを開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像のリンクが正常に機能していることも確認してみましょう。
それぞれブラウザで開けたらOKです
リスト 14.29のassert_selectのテストが正しく動作することを、関連するアプリケーションのコードをコメントアウトして確認してみましょう。
・link_to gravatar
・render @user
の2箇所の部分をコメントアウトしましょう
link_to gravatarはeach文と一緒にコメントアウトしましょう
<% provide(:title, @title) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= gravatar_for @user %>
<h1><%= @user.name %></h1>
<span><%= link_to "view my profile", @user %></span>
<span><strong>Microposts:</strong> <%= @user.microposts.count %></span>
</section>
<section class="stats">
<%= render 'shared/stats' %>
<% if @users.any? %>
<div class="user_avatars">
#ここと
<%# @users.each do |user| %>
<%#= link_to gravatar_for(user, size: 30), user %>
<%# end %>
#ここまで
</div>
<% end %>
</section>
</aside>
<div class="col-md-8">
<h3><%= @title %></h3>
<% if @users.any? %>
<ul class="users follow">
#ここ
<%#= render @users %>
</ul>
<%= will_paginate %>
<% end %>
</div>
</div>
演習9
ブラウザ上から/users/2を開き、[Follow]と[Unfollow]を実行してみましょう。うまく機能しているでしょうか?
followとunfollowをしてみて、機能していればOKです
先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
Started GET "/users/2" for 127.0.0.1 at 2023-10-21 23:21:18 +0000
Processing by UsersController#show as TURBO_STREAM
Parameters: {"id"=>"2"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/users_controller.rb:12:in `show'
Rendering layout layouts/application.html.erb
Rendering users/show.html.erb within layouts/application
(中略)
Started POST "/relationships" for 127.0.0.1 at 2023-10-21 23:21:17 +0000
Processing by RelationshipsController#create as TURBO_STREAM
Parameters: {"authenticity_token"=>"2PClFS-m-AeIPuB-cOQGms2ZVGDA5FcE7jb4tbY7aqiYKr_uZ8BIBtyFP0E4s6Aytpt-Pf2z71TfRZ_uS4--jQ", "followed_id"=>"2", "commit"=>"Follow"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/helpers/sessions_helper.rb:18:in `current_user'
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/relationships_controller.rb:5:in `create'
CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/helpers/sessions_helper.rb:18:in `current_user'
CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/models/user.rb:90:in `follow'
TRANSACTION (0.1ms) begin transaction
↳ app/models/user.rb:90:in `follow'
演習10
フォロー機能やフォロー解除機能を「標準の方法」または「Turboによる方法」で実現する方法の違いは、Turboは標準の方法よりもサーバーリクエストが1回多いという点しかありません。このため、Turboが確実に動作しているかどうかを判定するのが難しくなることがあります。リスト 14.35およびリスト 14.36にあるフォロワー数の後ろに一時的に「Turbo利用中」という文字を追加し、次に[Follow]ボタンや[Unfollow]ボタンをクリックして、Turboのテンプレートが確実にレンダリングすることを確認してみてください。確認が終わったら元に戻しておきましょう。
それぞれフォロワーの数の後ろに「Turbo利用中」と追加してみましょう
app/views/relationships/create.turbo_stream.erb
<%= turbo_stream.update "follow_form" do %>
<%= render partial: "users/unfollow" %>
<% end %>
<%= turbo_stream.update "followers" do %>
<%= @user.followers.count %>Turbo利用中
<% end %>
app/views/relationships/destroy.turbo_stream.erb
<%= turbo_stream.update "follow_form" do %>
<%= render partial: "users/follow" %>
<% end %>
<%= turbo_stream.update "followers" do %>
<%= @user.followers.count %>Turbo利用中
<% end %>
フォロワーの後ろに「Turbo利用中」が確認されればOKです
演習11
リスト 14.34のcrete、destroyメソッドでrespond_toブロック内のformat.html、format.turbo_streamを順にコメントアウトしていき、それぞれテストの結果がどうなるか確認してみましょう。
それぞれ全てREDになる
演習12
マイクロポストのidが数字の小さい順に並び、数字が大きいほど新しいと仮定すると、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。(ヒント: 13.1.4の実装で使ったdefault_scopeを思い出してください。)
13.1.4で行った実装にこのような記述がありましたね
default_scope -> { order(created_at: :desc) } |
---|
これによりマイクロポストは新しい投稿が上から順に(降順に)表示されるように実装されていますね
なのでこの状態でuser.feed.map(&:id)を実行すると、投稿が新しい物から順に表示されます
演習13
リスト 14.41において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?
user_id = ?の部分を削除する
# ユーザーのステータスフェードを返す
def feed
# Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
Micropost.where("user_id IN (?)", following_ids)
end
自信の投稿を確認できなくなるため、以下のテストがREDになる
# フォロワーがいるユーザー自身の投稿を確認
michael.microposts.each do |post_self|
assert michael.feed.include?(post_self)
end
リスト 14.41において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?
次はuser_id IN (?)と following_idsを削除する
def feed
# Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
Micropost.where("user_id = ?", id)
end
フォローしているユーザーの投稿を確認できないのでこちらがREDになる
# フォローしているユーザーの投稿を確認
lana.microposts.each do |post_following|
assert michael.feed.include?(post_following)
end
リスト 14.41において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.39のどのテストが失敗するでしょうか?(ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。)
全ての投稿を表示させます
# ユーザーのステータスフィードを返す
def feed
# Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
Micropost.all
end
フォローしていないユーザーが含まれるのでこちらがREDになります
# フォローしていないユーザーの投稿を確認
archer.microposts.each do |post_unfollowed|
assert_not michael.feed.include?(post_unfollowed)
end
演習14
Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
get root_path
@user.feed.paginate(page: 1).each do |micropost|
assert_match CGI.escapeHTML(micropost.content), response.body
リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています(このメソッドは11.2.3で扱ったCGI.escapeと密接に関連します)。このコードでHTMLをエスケープする必要がある理由を考えてみてください。(ヒント: 試しにエスケープ処理を削除して、得られるHTMLのソースコードと一致しないマイクロポストのコンテンツがないかどうか、注意深く調べてみてください。また、ターミナルの検索機能Cmd-FもしくはCtrl-Fで「sorry」を検索すると、原因の究明に役立つでしょう。)
エスケープされていない部分が記号になってしまうため
リスト 14.44のコードは、実はRailsのleft_outer_joinsメソッドを使うと、いわゆるLEFT OUTER JOINで直接表現できます。リスト 14.50のコード23 を適用してテストを実行し、このコードが返すフィードがテストでパスすることを確かめてみましょう。 残念ながら、テストがパスするにもかかわらず、実際のフィードにはユーザー自身のマイクロポストがいくつも重複表示されている(図 14.25)24 ので、次はリスト 14.51のテストを使ってこのエラーをキャッチしてください。このテストで使っているdistinctは、コレクション内の要素を重複抜きで返します。エラーをキャッチできたら、クエリにdistinctメソッドを追加したコード(リスト 14.52)に置き換えるとテストが green になることを確かめてください。次は、生成されたSQLを直接調べて、DISTINCTという語がクエリ自身に含まれていることを確認してください。これは、DISTINCTを指定した要素がアプリケーションのメモリ上ではなく、データベース上で効率よくSELECTされていることを示しています。(ヒント: SQLを直接調べるには、RailsコンソールでUser.first.feedを実行します。)
演習に沿って順番にやってみましょう。最後のDISTINCTは以下のコードの入力で確認することが出来ます
User.first.feed.paginate(page: 1)
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
Micropost Load (0.8ms) SELECT DISTINCT "microposts".* FROM "microposts" LEFT OUTER JOIN "users" ON "users"."id" = "microposts"."user_id" LEFT OUTER JOIN "relationships" ON