フォロー・フォロワー機能の実装

フォロー・フォロワー機能の実装

以前作成したBookers2というアプリケーションにフォロー・フォロワー機能を実装していく。

実装する機能

コントローラ
  • relationshipsコントローラの作成

  • createアクションを追加 (用途:フォローを作成)

  • destroyアクションを追加 (用途:フォローを削除)

  • フォローする・外すボタンをクリックしたら元画面に遷移すること

モデル
  • relationshipモデルの作成
ビュー
  • サイドバーにフォロー数・フォロワー数を表示

  • マイページ以外のサイドバーにフォローする・外すボタンを追加

  • ユーザー一覧画面にフォロー数・フォロワー数・フォローする・外すボタンの設置

  • フォロー・フォロワー一覧画面を作成すること

モデル

テーブルの設計

今回作成するフォロー・フォロワー機能について考える。

Aさん(user)がBさんを(user)をフォローしているとすると、

Aさんからみて、Bさんをフォローしている(follow)。

Bさんからみて、Aさんにフォローされている(followed)。

上記のような関係性になる。

そして、あるユーザーをフォローしている全ての集合体をfollowers、

あるユーザーにフォローされている全ての集合体をfollowingとする。

この関係性を完結に表すと、

followers(user) : following(user) → 多: 多

の関係性になる。

ER図で表すと下記のようになる。

relationshipsテーブルを設計すると下記のようになる。

カラム名 データ型 カラムの説明
id integer relationshipのID
follower_id integer フォローしているユーザーのID
followed_id integer フォローされているユーザーのID
モデル・テーブルの作成

テーブルの設計ができたので、テーブルを作成していく。

$ rails g model Relationship follower_id:integer followed_id:integer

今回は実装しないが、頻繁にfollower_id、followed_idを使用し、検索をかける場合やデータ量が多い場合はインデックスを追加する。

また、あるユーザーが同じユーザーを2回以上フォローできないように、複合キーインデックスを使用して、一意性を保証させたりする記述も実際に運用する上では必要となる。

参考URL 第12章 ユーザーをフォローする - Railsチュートリアル

作成されたマイグレーションファイルをデータベースに反映させる。

$ rails db:migrate

上記でモデル・テーブルの準備は完了。

関係性(アソシエーション)の記述

follower、followedはどちらもUsersテーブルのため、リレーションの書き方もコメント機能などの時とは変わってきます。

モデルに下記のように記述していく。

app/models/relationship.rb

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

belongs_to :followerだけの記述だと存在しないfollowersテーブルを参照してしまうため、

class_name : "User"を記述することで、usersテーブルに参照していくように記述する。

app/models/user.rb

:
  # 自分がフォローする側の関係
  has_many :relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
  # 与フォロー関係を通じて参照→自分がフォローしている人
  has_many :following, through: :relationships, source: :followed
  # 自分がフォローされる側の関係
  has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
  # 被フォロー関係を通じて参照→自分をフォローしている人
  has_many :followers, through: :reverse_of_relationships, source: :follower
:

上記のように記述し、フォロー・フォロワーのアソシエーションの記述は完了です。

メソッドの記述

user.rbにメソッドを記述し、コントローラで記述が簡潔になるようにしていきます。

app/models/user.rb

:
  # フォローするとき
  def follow(user)
    relationships.create(followed_id: user.id)
  end
  
  # フォローを外すとき
  def unfollow(user)
    relationships.find_by(followed_id: user.id).destroy
  end

  # フォローしているか確認するとき
  def following?(user)
    following.include?(user)
  end
:

follow(user)で引数で渡したユーザーをフォローするメソッド。

unfollow(user)で引数で渡したユーザーのフォローを外すメソッド。

followings?(user)で引数で渡したユーザーをフォローしているか確認するメソッド。フォローしていればtrue、していなければfalseを返す。

コントローラの作成

relationshipsコントローラの作成
$ rails g controller relationships
アクションの記述

relationshipsコントローラにアクションを記述していく。

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  # フォロー追加機能
  def create
    user = User.find(params[:user_id])
    current_user.follow(user)
    redirect_to request.referer
  end
  # フォロー削除機能
  def destroy
    user = User.find(params[:user_id])
    current_user.unfollow(user)
    redirect_to request.referer
  end
  # フォロワー一覧表示
  def followers
    user = User.find(params[:user_id])
    @users = user.followers
  end
  # フォロー一覧表示
  def following
    user = User.find(params[:user_id])
    @users = user.following
  end
end

ルーティング

ルーティングの記述

config/routes.rb

  resources :users, only: [:index, :show, :update, :edit] do
    resource :relationships, only: [:create, :destroy]
    get "followers" => "relationships#followers", as: "followers"
    get "following" => "relationships#following", as: "following"
  end

コントローラのアクションでURLからuser_idを取得するため、usersにネストした記述を行う。

URLに/:idを含む必要がないため、resourceで記述。

ルーティングの確認
$ rails routes

ビュー

ビューは、フォローの追加・削除ボタンを表示させる部分テンプレート_button.html.erbを作成し、表示させる部分に記述を追加する。

また、フォロワーの一覧表示としてfollowers.html.erb、フォローの一覧表示としてfollowing.html.erbを作成し、記述する。

フォローの追加・削除ボタンの部分テンプレートの作成

views/relationshipsフォルダに_button.html.erbの部分テンプレートファイルを作成する。

app/views/relationships/_button.html.erb

<% if current_user.following?(user) %>
  <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete, class: "btn btn-info" %>
<% else %>
  <%= link_to "フォローする", user_relationships_path(user.id), method: :post, class: "btn btn-success" %>
<% end %>

ログインユーザーがフォローしていない場合、フォローするボタンを表示、

フォローしている場合、フォローを外すボタンを表示。

サイドバーにフォロー、フォロワー数の表示、フォローボタンの追加

ユーザー情報のサイドバー部分、users/_info.html.erbに記述を追加する。

app/views/users/_info.html.erb

<h2>User info</h2>
<table class="table">
  <tbody>
    <tr><%= image_tag user.get_profile_image(100,100) %></tr>
    <tr>
      <th>name</th>
      <th><%= user.name %></th>
    </tr>
    <tr>
      <th>introduction</th>
      <th><%= user.introduction %></th>
    </tr>
    # ここから追加
    <tr>
      <th>follows</th>
      <th><%= link_to user.following.count, user_following_path(user) %></th>
    </tr>
    <tr>
      <th>followers</th>
      <th><%= link_to user.followers.count, user_followers_path(user) %></th>
    </tr>
    # ここまで
  </tbody>
</table>

<div class="row">
  # ここから追加
  <% if current_user == user %>
  # ここまで
    <%= link_to edit_user_path(user), class: "btn btn-block btn-outline-secondary" do %>
      <i class="fas fa-user-cog"></i>
    <% end %>
  # ここから追加
  <% else %>
    <%= render 'relationships/button', user: user %>
  <% end %>
  # ここまで
</div>
ユーザー一覧画面にフォロー・フォロワー数の追加、フォロー追加・削除リンクを表示

ユーザー一覧画面のusers/indexに記述を追加。

app/views/users/index.html.erb

:
<tbody>
  <% @users.each do |user| %>
    <tr>
      <td><%= image_tag user.get_profile_image(80,80) %></td>
      <td><%= user.name %></td>
      # ここから追加
      <td>フォロー数: <%= user.following.count %></td>
      <td>フォロワー数: <%= user.followers.count %></td>
      <td>
        <% if current_user != user %>
          <% if current_user.following?(user) %>
            <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete %>
          <% else %>
            <%= link_to "フォローする", user_relationships_path(user.id), method: :post %>
          <% end %>
        <% end %>
      </td>
      # ここまで
      <td>
        <%= link_to "Show", user_path(user.id) %>
      </td>
    </tr>
  <% end %>
</tbody>
:
フォロー一覧表示、フォロワー一覧表示

フォロー一覧とフォロワー一覧のビューを作成する。

users/indexのuser一覧部分と表示記述を同じにできるため、usersテーブルに_index.html.erb部分テンプレートを作成する。

app/views/users/_index.html.erb

<table class="table">
  <thead>
    <tr>
      <th>image</th>
      <th>name</th>
      <th colspan="4"></th>
    </tr>
  </thead>
  <tbody>
    <% users.each do |user| %>
      <tr>
        <td><%= image_tag user.get_profile_image(80,80) %></td>
        <td><%= user.name %></td>
        <td>フォロー数: <%= user.following.count %></td>
        <td>フォロワー数: <%= user.followers.count %></td>
        <td>
          <% if current_user != user %>
            <% if current_user.following?(user) %>
              <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete %>
            <% else %>
              <%= link_to "フォローする", user_relationships_path(user.id), method: :post %>
            <% end %>
          <% end %>
        </td>
        <td>
          <%= link_to "Show", user_path(user.id) %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

users/indexのビューを部分テンプレートに変更。

app/views/users/index.html.erb

:
    <!--ユーザー一覧表示-->
    <div class="col-md-8 offset-md-1">
      <h2>Users</h2>
      <%= render 'index', users: @users %>
    </div>
:

フォロー一覧(relationships/following)とフォロワー一覧(relationships/followers)のビューを作成する。

app/views/relationships/following.html.erb

<h2>Follow Users</h2>
<% if @users.exists? %>
  <%= render 'users/index', users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>

app/views/relationships/following.html.erb

<h2>Follower Users</h2>
<% if @users.exists? %>
  <%= render 'users/index', users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>

exists?メソッド (オブジェクト.exists?(条件))

指定した条件のレコードが存在するか確認するメソッド。存在する場合trueを返し、存在しない場合falseを返す。

今回は特に条件は指定していないため、@usersにレコードが存在すればtrue、レコードが存在しなければfalseを返す。

以上で、フォロー・フォロワー機能の実装が完了。