🤝

【Rails】フォロー・フォロワー機能

2023/06/29に公開

本の投稿サイトにフォロー・フォロワー機能を追加します。

実装機能の要件

実装機能の要件は以下の通りです。

コントローラー アクション 用途
RelationshipsController create フォローを作成
RelationshipsController destroy フォローを削除

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

モデル
Relationship フォロー関係を管理するモデル
ビュー
サイドバー フォロー数・フォロワー数を表示
マイページ以外のサイドバー フォローする・外すボタンを追加
ユーザー一覧画面 フォロー数・フォロワー数・フォローする・外すボタンの設置
フォロー・フォロワー一覧画面 フォロー・フォロワーの一覧表示

実装手順

  1. 中間テーブルの作成
  2. Relationshipモデルの作成
  3. RelationshipモデルとUserモデルにリレーションの設定
  4. ルーティングの設定
  5. Relationshipsコントローラの作成
  6. Viewの編集

考え方

フォロー機能の場合、フォローする人もフォローされる人もユーザーであるため、ユーザー同士がお互いにフォローする関係です。そうすると以下のイメージが考えられます。

これではよくわからないですよね。
よって、中間テーブルにおいて「フォローする人(follower_id)」と「フォローされる人(followed_id)」を区別する必要があります。

このような「多対多」の関係を表現するために、中間テーブル(Relationship)を導入します。

このように、中間テーブルを使用することで、「1対多」 の関係に変換することができます。ユーザーは複数のフォローを持つことができ、また複数のユーザーをフォローすることも可能です。
例として、下図のように関連付けができるようになります。

上記を表にすると以下の通りです。

follower_id followed_id 詳細
1 2 ユーザー1がユーザー2をフォロー
1 2 ユーザー1がユーザー3をフォロー
2 1 ユーザー2がユーザー1をフォロー
2 3 ユーザー2がユーザー3をフォロー
3 1 ユーザー3がユーザー1をフォロー
3 2 ユーザー3がユーザー2をフォロー

以上が、フォロー機能における中間テーブルの役割と、1対多の関係に変換する理由です。

中間テーブル設計

今回は以下のような、テーブル設計にします。

カラム名 データ型 カラムの説明
id (初期カラム) 主キー(PK) ※マイグレーションファイルには記載不要
follower_id integer フォローするユーザーのid
followed_id integer フォローされるユーザーのid

Model作成

上述のテーブル設計に基づいてモデルを作成します。
ターミナルでの入力方法は以下の通りです。

  1. Relationshipモデルの生成
ターミナル
rails g model Relationship follower_id:integer followed_id:integer

上記のコマンドを実行すると、db/migrateディレクトリにマイグレーションファイルが生成されます。

  1. データベースへのマイグレーション実行
ターミナル
rails db:migrate

上記のコマンドを実行することで、マイグレーションファイルに基づいてデータベースにテーブルが作成されます。

以上の手順により、Relationshipモデルが作成され、follower_idfollowed_idのカラムが含まれるテーブルがデータベースに作成されます。

リレーションの実装

RelationshipモデルとUserモデルにリレーションの設定をしていきます。

  • Relationshipモデル
app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

解説:

  • belongs_to :follower, class_name: "User"は、Relationshipモデルがfollowerという名前の関連付けを持ち、それがUserモデルと紐付いていることを示しています。class_name: "User"は、関連付ける対象のモデルがUserモデルであることを指定しています。

  • belongs_to :followed, class_name: "User"も同様に、Relationshipモデルがfollowedという名前の関連付けを持ち、Userモデルと紐付いていることを示しています。

  • Userモデル
app/models/user.rb
class User < ApplicationRecord
  # ...
  # フォローしている関連付け
  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 :followings, through: :active_relationships, source: :followed
  
  # フォロワーを取得
  has_many :followers, through: :passive_relationships, source: :follower
  
  # 指定したユーザーをフォローする
  def follow(user)
    active_relationships.create(followed_id: user.id)
  end
  
  # 指定したユーザーのフォローを解除する
  def unfollow(user)
    active_relationships.find_by(followed_id: user.id).destroy
  end
  
  # 指定したユーザーをフォローしているかどうかを判定
  def following?(user)
    followings.include?(user)
  end
end

解説:

  1. has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
    この行は、ユーザーモデル(User)がフォローしている関連付けを定義しています。Relationshipモデルとの関連付けを行い、active_relationshipsという名前で参照します。外部キーとしてfollower_idカラムを使い、dependent: :destroyオプションで関連するデータが削除されるように設定しています。

  2. has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
    この行は、ユーザーモデル(User)がフォローされている関連付けを定義しています。Relationshipモデルとの関連付けを行い、passive_relationshipsという名前で参照します。外部キーとしてfollowed_idカラムを使い、dependent: :destroyオプションで関連するデータが削除されるように設定しています。

  3. has_many :followings, through: :active_relationships, source: :followed
    この行は、ユーザーがフォローしているユーザーの一覧を取得するための関連付けを定義しています。Userモデルは、active_relationships経由でfollowings(フォローしているユーザー)と関連付けられます。source: :followedにより、active_relationshipsテーブルのfollowedカラムを参照します。

  4. has_many :followers, through: :passive_relationships, source: :follower
    この行は、ユーザーをフォローしているユーザー(フォロワー)の一覧を取得するための関連付けを定義しています。Userモデルは、passive_relationships経由でfollowers(フォロワー)と関連付けられます。source: :followerにより、passive_relationshipsテーブルのfollowerカラムを参照します。

  5. def follow(user), def unfollow(user), def following?(user)
    これらのメソッドは、ユーザーが他のユーザーをフォローしたり、フォローを解除したり、特定のユーザーをフォローしているかどうかを判定するためのメソッドです。followメソッドは、active_relationshipsを経由して指定したユーザーをフォローし、unfollowメソッドは、指定したユーザーのフォローを解除します。following?メソッドは、指定したユーザーをフォローしているかどうかを真偽値で返します。

Routing

以下の通りルーティングの設定をします。

config/routes.rb
  :
  resources :users, only: [:index, :show, :edit, :update] do
    resource :relationships, only: [:create, :destroy]
  	get "followings" => "relationships#followings", as: "followings"
  	get "followers" => "relationships#followers", as: "followers"
  end

解説:
users内にさらにrelationshipsをネストします。これにより、特定のユーザーに関連するフォロー情報を管理することができます。例えば、/users/1/followingsというURLで、ユーザーIDが1のユーザーがフォローしているユーザー一覧にアクセスできます。

また、followingsfollowersアクションに対して、以下のURLを作成します。

  1. followingsアクションのURL: /users/:user_id/followings
  2. followersアクションのURL: /users/:user_id/followers

Controller作成

ターミナルで以下のコマンドを実行してコントローラを作成します。

ターミナル
rails g controller relationships

Relationshipsコントローラへの記述

以下の通り記述します。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :authenticate_user!
  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 followings
    user = User.find(params[:user_id])
    @users = user.followings
  end
  
  def followers
    user = User.find(params[:user_id])
    @users = user.followers
  end
end

解説:

  1. createアクション: ユーザーをフォローするためのアクションです。params[:user_id]からフォロー対象のユーザーを特定し、current_user(現在のログインユーザー)がそのユーザーをフォローします。その後、リダイレクト先を元のページに戻します。

  2. destroyアクション: ユーザーのフォローを解除するためのアクションです。params[:user_id]からフォロー解除対象のユーザーを特定し、current_userがそのユーザーのフォローを解除します。その後、リダイレクト先を元のページに戻します。

  3. followingsアクション: 特定のユーザーがフォローしているユーザーの一覧を表示するためのアクションです。params[:user_id]から対象のユーザーを特定し、そのユーザーがフォローしているユーザー(user.followings)を@usersに代入します。

  4. followersアクション: 特定のユーザーをフォローしているユーザーの一覧を表示するためのアクションです。params[:user_id]から対象のユーザーを特定し、そのユーザーをフォローしているユーザー(user.followers)を@usersに代入します。

Viewページ

以下を作成します。

  1. サイドバーにフォロー数・フォロワー数・フォローする・外すボタンの設置
  2. ユーザー一覧画面にフォロー数・フォロワー数・フォローする・外すボタンの設置
  3. フォロー・フォロワー一覧画面

1. サイドバーにフォロー数・フォロワー数・フォローする・外すボタンの設置

  • フォローボタン
    複数ページでフォローボタンを使用したいので、フォローボタンを部分テンプレートにします。
app/views/relationships/_btn.html.erb
<% if current_user != user %>
  <% if current_user.following?(user) %>
    <%= link_to "フォロー外す", user_relationships_path(user.id), method: :delete, class: options[:class].presence || "btn btn-info" %>
  <% else %>
    <%= link_to "フォローする", user_relationships_path(user.id), method: :post, class: options[:class].presence || "btn btn-success" %>
  <% end %>
<% end %>

解説:

  1. current_user != user
    現在のユーザーと表示対象のユーザーが異なるかをチェックしています。自分自身をフォローすることはできないため、自分自身の場合はフォローボタンを表示しません。
  2. current_user.following?(user)
    現在のユーザーが表示対象のユーザーを既にフォローしているかどうかをチェックしています。フォローしている場合は「フォロー外す」ボタンを表示し、フォローしていない場合は「フォローする」ボタンを表示します。
  3. class: options[:class].presence || "btn btn-info"
    ボタンのクラスを呼び出し場所によって変えるようにします。
    options[:class].presence は、options[:class] の値が存在する場合はその値を返し、存在しない(空の文字列である)場合は nil を返すメソッドです。
    || 演算子を使用して、options[:class] が指定されている場合はその値が使用され、指定されていない場合はデフォルトのクラス "btn btn-info" が適用されます。

例えば、options[:class]"follow-link" という値を指定している場合、class: options[:class].presence || "btn btn-info" の結果は "follow-link" です。

  • サイドバーへのフォロー数・フォロワー数の表示とフォローボタンの呼び出し
app/views/users/_info.html.erb
 <table class="table">
   <tr><%= image_tag user.get_profile_image, size:'100x100' %></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.followings.count, user_followings_path(user) %></th>
+  </tr>
+  <tr>
+    <th>followers</th>
+    <th><%= link_to user.followers.count, user_followers_path(user) %></th>
+  </tr>
 </table>

+<div class="row">
+  <% if current_user != user %>
+    <td><%= render "relationships/btn", user: user, options: { class: ""} %></td>
+  <% else %>
+    <%= link_to edit_user_path(user), class: "btn btn-outline-secondary btn-block" do %>
+      <i class="fas fa-user-cog"></i>
+    <% end %>
+  <% end %>
+</div>

解説:
このコードは、現在のユーザーと表示対象のユーザーが異なる場合にフォローボタンを表示し、同じ場合はユーザーのプロフィール編集ボタンを表示するためのものです。
<% if current_user != user %> の条件式は、現在のユーザーと表示対象のユーザーが異なる場合に true となります。その場合、フォローボタンが表示されます。
フォローボタンは空のクラス(options: { class: ""})を指定してるため、デフォルトのクラスが適用されます。

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

app/views/users/_index.html.erb
 <table class="table">
   <thead>
     <tr>
       <th>image</th>
       <th>name</th>
       <th colspan="3"></th>
     </tr>
   </thead>
   <tbody>
     <% users.each do |user| %>
       <tr>
         <td><%= image_tag user.get_profile_image, size: '50x50' %></td>
         <td><%= user.name %></td>
+        <td>フォロー数: <%= user.followings.count %></td>
+        <td>フォロワー数: <%= user.followers.count %></td>
+        <td><%= render "relationships/btn", user: user, options: { class: "follow-link" } %></td>
         <td><%= link_to "Show", user %></td>
       </tr>
     <% end %>
   </tbody>
 </table>

3. フォロー・フォロワー一覧画面

  • フォローの一覧画面
app/views/relationships/followings.html.erb
<h2>Follow Users</h2>
<% if @users.exists? %>
  <%= render "/users/index", users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>
  • フォロワーの一覧画面
app/views/relationships/followers.html.erb
<h2>Follower Users</h2>
<% if @users.exists? %>
  <%= render "/users/index", users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>

解説:
フォロー、フォロワーユーザーの一覧を表示するためのViewです。フォロー/フォロワーユーザーがいれば一覧を表示し、いなければメッセージを表示しています。

参照

以下参考にさせてもらった記事です。
皆さんとてもわかりやすくお手本にさせてもらいました。
ありがとうございました♪

https://zenn.dev/airiswim/articles/f854a095a3f3b8
https://note.com/memomemotarou/n/n673f99a79ffe
https://zenn.dev/masanarih0ri/articles/dbc544a79a26ee


中間テーブルに苦戦しましたが、色々な方のページを参考に理解していくことができました☆
記事の書き方の勉強にもなるし、皆さんのページが本当にありがたい!

続きはこちら。
https://zenn.dev/ganmo3/articles/61143b30a08272

Discussion