T_Y_CODE

プログラミング学習記録

学習記録 23日目

23日目の学習記録をまとめていきます。

学習計画

学習内容

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でレイアウトを調整し動作確認します。

f:id:t_y_code:20201030150539p:plain

  • [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>
  • 動作確認します。

f:id:t_y_code:20201030152033p:plain
f:id:t_y_code:20201030152139p:plain

演習

1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では[Unfollow]ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?

  • unless current_user?(@user)とフォローボタンを表示するか制御しているので自分のプロフィールページではフォローボタンは表示されない。

f:id:t_y_code:20201030152313p:plain
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>
  • 動作確認します。

f:id:t_y_code:20201030155229p:plain

  • 次に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 %>
  • コントローラでAjax対応をしていきます。ローカル変数userをインスタンス変数@userへ変更する必要があります。
# 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 フォローをテストする
  • フォローのテストを書いていきます。Ajax対応したテストはリクエストにxhr: tureを記述することでテスト出来ます。
# 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
  • 動作確認します。

f:id:t_y_code:20201030170911p:plain

  • リモートリポジトリにプッシュし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
  • 本番環境での動作確認をします。

f:id:t_y_code:20201030171650p:plain

  • 以上で14章は終了になります。