学習記録 23日目
23日目の学習記録をまとめていきます。
- 学習計画
- 学習内容
- Ruby on Railsチュートリアル 14章
- 14.1 Relationshipモデル
- 14.1.1 データモデルの問題(および解決策)
- 演習
- 14.1.2 User/Relationshipの関連付け
- 演習
- 14.1.3 Relationshipのバリデーション
- 演習
- 14.1.4 フォローしているユーザー
- 演習
- 14.1.5 フォロワー
- 演習
- 14.2.1 フォローのサンプルデータ
- 演習
- 14.2.2 統計と[Follow]フォーム
- 演習
- 14.2.3 [Following]と[Followers]ページ
- 演習
- 14.2.4 [Follow]ボタン(基本編)
- 演習
- 14.2.5 [Follow]ボタン(Ajax編)
- 演習
- 14.2.6 フォローをテストする
- 演習
- 14.3 ステータスフィード
- 14.3.1 動機と計画
- 演習
- 14.3.2 フィードを初めて実装する
- 演習
- 14.3.3 サブセレクト
- Ruby on Railsチュートリアル 14章
学習内容
Ruby on Railsチュートリアル 14章
- ユーザのフォロー機能を実装していきます。
14.1 Relationshipモデル
- トピックブランチを作成します。
$ git checkout -b following-users
14.1.1 データモデルの問題(および解決策)
- ユーザ1がユーザ2をフォローする際フォローする側をfollower_idが1, followed_idが2になります。この関係性をモデルとして保存していきます。
$ rails g model Relationship follower_id:integer followed_id:integer
- マイグレーションを確認します。follower_id, followed_idカラムは頻繁に検索するためインデックスを追加します。またfollower_idとfollowed_idの組み合わせは一意性を保証しておきます。これをしておかないと同じユーザを何度もフォロー出来てしまいます。
# db/migrate/***_create_relationships.rb class CreateRelationships < ActiveRecord::Migration[6.0] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true end end
- マイグレートします。
$ rails db:migrate
演習
1. 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
- user.followingを行うとUserモデルの配列を取得出来るよう設計しています。.map(&:id)で配列の各要素に対し.idメソッドを実行しその結果を配列に入れ出力します。よって図14.7のid=1のユーザにuser.followingを実行したら以下の結果が出力される。
> user.following.map(&:id) # => [2, 7, 10, 8]
2. 図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
- id=1のユーザが返ってくる。user.following.map(&:id)を実行すると1が出力される。
14.1.2 User/Relationshipの関連付け
- 今回はmicropostの時同様にactive_relationships.build(followed_id: )でフォロー処理を行いたいです。has_manyメソッドを使用し関係性を示していきます。ただしhas_many :active_relationshipsとしてしまうとRailsはActiveRelationshipsモデルを参照してしまうためどのモデルを参照するか明示的に示す必要がある。
# app/models/user.rb class User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy #... end
- マイクロポストの時はMicropostsモデルにuser_idというカラムを作成していたためRailsが自動的に外部キーを参照してくれたが今回は<クラス名>_idの形式にはなっていないためforeign_keyも明示的に示す必要がある。またユーザが削除された際Relationshipの情報も削除されてほしいためdependent: destroyを記載しておく。
- Relationship側にはbelongs_toメソッドを追記する。上記同様<クラス名>_idのカラム名になっていないためクラス名を明示的に示す必要がある。
# app/models/relationship.rb class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" end
演習
1. コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
2. 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
$ rails c > user = User.first > user.active_relationships.create(followed_id: 2) > relationship = Relationship.first > relationship.followed_id # => 2 > relationship.follower_id # => 1
14.1.3 Relationshipのバリデーション
- Relationshipに対してバリデーションを作成します。まずはモデルに対してテストを書いていきます。
# test/models/relationships_test.rb require 'test_helper' class RelationshipTest < ActiveSupport::TestCase def setup @relationship = Relationship.new( follower_id: users(:michael).id, followed_id: users(:archer).id ) end test "should be valid" do assert @relationship.valid? end test "should require a follower_id" do @relationship.follower_id = nil assert_not @relationship.valid? end test "should require a followed_id" do @relationship.followed_id = nil assert_not @relationship.valid? end end
- まだバリデーションを追記していないためテストは失敗します。
$ rails test:models 19 tests, 0 assertions, 0 failures, 19 errors, 0 skips
- バリデーションを追記しRelationshipモデル作成時に自動生成されたfixtureを空にしておきます。
# app/models/relationship.rb class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true end
- テストが成功することを確認します。
$ rails test:models 19 tests, 24 assertions, 0 failures, 0 errors, 0 skips $ rails t 63 tests, 328 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
- 省略
14.1.4 フォローしているユーザー
- フォローしているユーザを取得出来るようにします。Userモデルにhas_many followedsと記載すればよいが先ほど定義したactice_relationshops経由で取得する必要があり、またfollowedsというのは英語的におかしいため先述したfollowingという名前に変更する。そのためこのfollowingというのはfollowedの集合であることを明示的に示すsource: :followedの記述が必要になる。
# app/models/user.rb class User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed #... end
- followingを実行するとUserモデルの配列が戻り値として返ってくる。followingメソッド実行→active_relationshipsで現ユーザの関係性を取得→取得した情報のfollowedに該当するidのユーザを配列にして返す。
- 次にfollowとunfollowメソッドをUserモデルに定義します。またそのユーザをフォローしてるか確認するfollowing?メソッドも実装します。
# app/models/user.rb class User < ApplicationRecord #... def follow(other_user) following << other_user end def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end def following?(other_user) following.include?(other_user) end #... end
- 上記メソッドのテストを書きます。
# test/models/users_test.rb require 'test_helper' class RelationshipTest < ActiveSupport::TestCase #... test "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) michael.unfollow(archer) assert_not michael.following?(archer) end end
- テストが成功することを確認します。
$ rails t 64 tests, 331 assertions, 0 failures, 0 errors, 0 skips
演習
1. コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
$ rails c > user_1 = User.find(1) > user_2 = User.find(2) > user_1.following?(user_2) # => false > user_1.follow(user_2) # => #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, ...> > user_1.following?(user_2) # => true > user_1.unfollow(user_2) # => #<Relationship id: 2, follower_id: 1, followed_id: 2, ...> > user_1.following?(user_2) # => false
2. 先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
> user_1.following?(user_2) 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]] > user_1.follow(user_2) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-10-30 05:16:45.000396"], ["updated_at", "2020-10-30 05:16:45.000396"]] > user_1.unfollow(user_2) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]] DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 2]]
14.1.5 フォロワー
- フォローとは対照的なフォロワーの関係性を実装します。
# app/models/user.rb class User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower #... end
- 動作としてはフォローの時と同じである。has_many :followersのsource: :followerは省略が出来るがfollowingとの類似性を示すために敢えて記載してます。
- テストを追記してfollowersの動作確認をします。
# test/models/users_test.rb require 'test_helper' class RelationshipTest < ActiveSupport::TestCase #... test "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) assert archer.followers.include?(michael) michael.unfollow(archer) assert_not michael.following?(archer) end end
$ rails t 64 tests, 332 assertions, 0 failures, 0 errors, 0 skips
演習
1. コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
$ rails c > user = User.first > 2.upto(10) do |n| > User.find(n).follow(user) > end > user.followers.map(&:id) # => [2, 3, 4, 5, 6, 7, 8, 9, 10]
2. 上の演習が終わったら、user.followers.countの実行結果が、
> user.followers.count # => 9
3. user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
> user.followers.count SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
- usersテーブルにrelationshipsテーブルを結合している。条件はrelationships内のfollower_idとuser.idが同じ行のデータ。そしてWHEREを使用してrelationshipsテーブルのfollowed_idが1の行のみ抽出している。
> user.followers.to_a.count
- 一度配列へ変換するとSQL文を実行せずに配列長を出力出来る。だが配列化する際に
> user.followers.to_a # => [#<User id: 2, ...>, #<User id: 3, ...>, #<User id: 4, ...>, #<User id: 5, ...>, #<User id: 6, ...>, #<User id: 7, ...>, #<User id: 8, ...>, #<User id: 9, ...>, #<User id: 10, ...>, ]
- 1ユーザずつ配列へ格納しているため100万人フォロワがいたらデータベースに100万回アクセスすることになり処理がものすごく重くなりそう。
14.2.1 フォローのサンプルデータ
- seedを編集してRelationshipのサンプルデータを作成します。
# db/seeds.rb #... users = User.all user = User.first following = users[2..50] followers = users[3..50] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }
- データベースをリセットします。
$ rails db:migrate:reset $ rails db:seed
演習
1. コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。
- 期待 48
$ rails c > User.first.followers.count # => 48
2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。
- 期待 49
> User.first.following.count # => 49
14.2.2 統計と[Follow]フォーム
- following, followersの一覧ページへのリンクとページを作成していきます。まずはルーティングを実装します。users/:id/followingのようなルーティングを作成します。
# config/routes.rb Rails.application.routes.draw do #... resources :users do member do get :following, :followers end end #... end
- フォロー, フォロワーの統計情報を表示するパーシャルを作成します。
# app/views/shared/_stats.html.erb <% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div>
- パーシャルをHomeページに挿入します。
# app/views/static_pages/_with_micropost_home.html.erb <div class="row"> <aside class="col-md-4"> <section class="user-info"> <%= render 'shared/user_info' %> </section> <section class="stats"> <%= render 'shared/stats'%> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> #... </div>
- CSSでレイアウトを調整し動作確認します。
- [Follow] / [Unfollow]ボタンのパーシャルも作成しておきます。
# app/views/users/_follow_form.html.erb <% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %>
- 各ボタンではform_withを使用するため先にルーティングを設定します。followはrelationshipsコントローラのcreateアクション, unfollowはrelationshipsコントローラのdestroyアクションを使用します。
# config/routes.rb Rails.application.routes.draw do #... resources :relationships, only: [:create, :destroy] end
- 各ボタンのパーシャルを作成します。
# app/views/users/_follow.html.erb <%= form_with(model: current_user.active_relationships.build, local: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: 'btn btn-primary' %> <% end %>
# app/views/users/_unfollow.html.erb <%= form_with(model: current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, local: true) do |f| %> <%= f.submit "Unfollow" %> <% end %>
- createアクションではfollowed_idが必要になるためhidden_fioeld_tagを使用して送信している。
- ユーザのプロフィールページにフォローボタンのパーシャルを挿入します。
# app/views/users/show.html.erb <% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> #... <section class="stasts"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> #... </div> </div>
- 動作確認します。
演習
1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では[Unfollow]ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?
- unless current_user?(@user)とフォローボタンを表示するか制御しているので自分のプロフィールページではフォローボタンは表示されない。
2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
- 省略
3. Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
# test/integration/users_profile_test.rb require 'test_helper' class UsersProfileTest < ActionDispatch::IntegrationTest #... test "profile display" do #... assert_select 'strong#following', text: @user.following.count.to_s assert_select 'strong#followers', text: @user.followers.count.to_s end end
# test/integration/site_layout_test.rb require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest #... test "layout links" do #... # ログインしている場合 log_in_as(@user) get root_path #... assert_select 'strong#following', text: @user.following.count.to_s assert_select 'strong#followers', text: @user.followers.count.to_s end end
$ rails t 64 tests, 336 assertions, 0 failures, 0 errors, 0 skips
14.2.3 [Following]と[Followers]ページ
- following, followersページを作成していきます。
- まずはテストを作成しログインしていない時はログインページにリダイレクトするようにします。
# test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest #... test "should redirect following when not logged in" do get following_user_path(@user) assert_redirected_to login_url end test "should redirect followers when not logged in" do get followers_user_path(@user) assert_redirected_to login_url end end
# app/contollers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers] #... def following @title = "Following" @user = User.find(params[:id]) @users = @user.following.paginate(page: params[:page]) render 'show_follow' end def followers @title = "Followers" @user = User.find(params[:id]) @users = @user.followers.paginate(page: params[:page]) render 'show_follow' end #... end
$ rails t 66 tests, 338 assertions, 0 failures, 0 errors, 0 skips
- show_followビューを作成します。
# app/views/users/show_follow.html.erb <% 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><b>Microposts:</b> <%= @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>
- 動作確認します。
- 次にfollowingページに対する統合テストを作成します。
$ rails g integration_test following
- テスト用のRelationshipモデルのfixtureを作成しておきます。
- 統合テストを書いていきます。
# test/integration/following_test.rb require 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) log_in_as(@user) end test "following page" do get following_user_path(@user) assert_not @user.following.empty? assert_match @user.following.count.to_s, response.body @user.following.each do |user| assert_select "a[href=?]", user_path(user) end end test "followers page" do get followers_user_path(@user) assert_not @user.followers.empty? assert_match @user.followers.count.to_s, response.body @user.followers.each do |user| assert_select "a[href=?]", user_path(user) end end end
- テストが成功することを確認します。
$ rails t 68 tests, 348 assertions, 0 failures, 0 errors, 0 skips
演習
1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
- 動作確認のみのため記載省略
2. リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。
- 省略
14.2.4 [Follow]ボタン(基本編)
- フォローボタンが動作するように実装していきます。Relationshipsコントローラを作成します。
$ rails g controller Relationships
- まずはコントローラに対してテストを作成します。create, destroyアクションはログインしていないとリダイレクトするようにします。
# test/controllers/relationships_controller_test.rb require 'test_helper' class RelationshipsControllerTest < ActionDispatch::IntegrationTest test "create should require logged-in user" do assert_no_difference 'Relationship.count' do post relationships_path end assert_redireceted_to login_path end test "destroy should require logged-in user" do assert_no_difference 'Relationship.count' do delete relationship_path(relationships(:one)) end assert_redireceted_to login_path end end
- before_actionでログインチェックを行います。またcreate, destroyアクションも実装します。
# app/controllers/relationships_controller.rb class RelationshipsController < ApplicationController before_action :logged_in_user def create user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user end def destroy user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user end end
演習
1. ブラウザ上から /users/2 を開き、[Follow]と[Unfollow]を実行してみましょう。うまく機能しているでしょうか?
- している。
2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
- されている。
14.2.5 [Follow]ボタン(Ajax編)
- フォロー機能を実装出来たがフォロー/フォロー解除する度にページがリダイレクトされ煩わしい。リダイレクトされないようにAjax対応していく。
- Ajaxを実装するにはform_withのlocal: trueをremote: trueに変更する。
# app/views/users/_follow.html.erb <%= form_with(model: current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: 'btn btn-primary' %> <% end %>
# app/views/users/_unfollow.html.erb <%= form_with(model: current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %> <%= f.submit "Unfollow" %> <% end %>
# app/controllers/relationships_controller.rb class RelationshipsController < ApplicationController before_action :logged_in_user def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end end
- ブラウザ側でJavaScriptが無効になってた場合でもうまく動くように設定します。
# config/application.rb #... module SampleApp class Application < Rails::Application #... config.action_view.embed_authenticity_token_in_remote_forms = true end end
- Ajaxを実行した際にHTMLの内容を書き換えるJavaScriptファイルを作成します。先ほどのrespond_toはformat.htmlかformat.jsのどちらかを実行します。このformat.jsの部分を書いていきます。
# app/views/relationships/create.js.erb $("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');
# app/views/relationships/destroy.js.erb $("#follow_form").html("<%= escape_javascript(render('users/follow')) %>"); $("#followers").html('<%= @user.followers.count %>');
- コードを見るとjQueryっぽい記述です。
演習
1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
- 動作確認のみのため記載省略
2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
- フォローをすると...
Rendering relationships/create.js.erb app/views/users/_unfollow.html.erb:1 Rendered users/_unfollow.html.erb (Duration: 1.6ms | Allocations: 1032) app/views/relationships/create.js.erb:2 Rendered relationships/create.js.erb (Duration: 3.9ms | Allocations: 2340)
- create.js.erbが実行されます。follow_formの内容がunfollowパーシャルへ書き換えられ実行されます。そしてid="followers"の中身が@user.followers.countに書き換えられます。
14.2.6 フォローをテストする
# test/integration/following_test.rb require 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest #... test "should follow a user the standard way" do assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } end end test "should follow a user with Ajax" do assert_difference '@user.following.count', 1 do post relationships_path,xhr: true, params: { followed_id: @other.id } end end test "should unfollow a user the standard way" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship) end end test "should unfollow a user with Ajax" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true end end end
- テストが成功することを確認します。
$ rails t 74 tests, 358 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
2. リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。
- 質問の意図が理解出来ないため飛ばします。
14.3 ステータスフィード
- フォローしているマイクロポストの配列を作成し描画していきます。
14.3.1 動機と計画
- テストを作成します。
# test/models/user.test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase #... test "feed should have the right posts" do michael = users(:michael) archer = users(:archer) lana = users(:lana) lana.microposts.each do |post_following| assert michael.feed.include?(post_following) end michael.microposts.each do |post_self| assert michael.feed.include?(post_self) end archer.microposts.each do |post_unfollowed| assert_not michael.feed.include?(post_unfollowed) end end
- まだ実装していないのでテストは失敗します。
$ rails test:models 21 tests, 29 assertions, 1 failures, 0 errors, 0 skips
演習
1. マイクロポストのidが正しく並んでいると仮定して(すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。
- 投稿の新しい順に配列される。
14.3.2 フィードを初めて実装する
- 現在のfeedメソッドは以下のとおりである。
# app/models/user.rb class User < ApplicationRecord #... def feed Micropost.where("user_id = ?", id) end #... end
- 実装したいfeedはフォローしているidも含めるので以下のようになる。
Micropost.where("user_id IN (?) OR user_id = ?", following.map(&:id).join(', '), id)
- 「following.map(&:id).join(', ')」はよく使用されるためActiveRecoadでは「following_ids」と記述すると同様の出力結果が得られる。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
- これでテストが成功するようになりました。
$ rails test:models 21 tests, 66 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
- テストはpost_selfの箇所が失敗する。
Micropost.where("user_id IN (?), following_ids)
2. リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
- テストはpost_followingの箇所が失敗する。
Micropost.where("user_id = ?", id)
3. リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
- テストはpost_unfollowedの箇所が失敗する。
Micropost.all
14.3.3 サブセレクト
- 上記まで?を使用していたが名前付きの変数へ変更する。
# app/models/user.rb class User < ApplicationRecord #... def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) end #... end
- following_idsはフォローしている全てのユーザを取得して配列化している。フォローしているユーザが増えるとfeedを実行する度に処理が重くなる。これを解消するためにfollowing_idsをSQL文で書くことにする。
# app/models/user.rb class User < ApplicationRecord #... def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) end #... end
- テストが成功することを確認します。
$ rails test:models 21 tests, 66 assertions, 0 failures, 0 errors, 0 skips
- 動作確認します。
- リモートリポジトリにプッシュしherokuにデプロイします。
$ rails t $ git add -A $ git commit -m "Add user following" $ git push origin following-users $ git checkout master $ git merge following-users $ git push origin master $ git push heroku $ heroku pg:reset DATABASE $ heroku run rails db:migrate $ heroku run rails db:seed
- 本番環境での動作確認をします。
- 以上で14章は終了になります。