Railsでフォロー機能を作ろう

個人ブログから移行しました。

はじめに

このページでは、Twitterのようなアプリケーションにあるフォローフォロワー機能を作ります。railsチュートリアルのフォローフォロワー機能で理解に苦しむ方を多く見るため、できるだけわかりやすく説明します。
そのため、railsチュートリアルの記述と若干異なります。

考え方を理解する

【前提】
まず、以下を前提としましょう。
・フォローする人をfollowing
・フォローされる人をfollowed

【イメージ】
フォローする人もフォローされる人もUserであるため、こうなってしまいそう、、、

画像1

これじゃ、よくわからん、、、
ここで、Userはフォローする側とフォローされる側に分けて考える必要がありそうです。

画像2

そうすると、多対多のリレーションになるので、Relationshipという中間テーブルを用意することで、1対多の関係に変えます。

画像3


下の図のように、2つの関連付けが必要になります。

画像4

モデルの関係を理解する

上のイメージを参考にして、実際に関係をコード化します。
まずは、簡単なRelationshipから見ていきます。

# app/models/relationship.rb (図2を参照)
class Relationship < ApplicationRecord
 belongs_to :follower, class_name: "User"
 belongs_to :followed, class_name: "User"
end

ここで、class_nameというものが登場しました。
フォローする人(follower)もされる人(followed)もUserモデルでした。
本来、followerやfollowedといったモデルは存在しません。
class_nameをつけることで、関連先のモデルを参照する際の名前を変更できるということです。
「Userをfollowとfollowedに分ける」ことをclass_nameがやってくれるんですね。

次に、Userモデルを考えていきます。
図3の、赤枠と青枠の2つの関係を作ります。
そのため、「フォローしている人の取得」と「フォローされている人の取得」でここでも2通りを考える必要があります。

画像5

このとき、Relationshipも2つに分けているので、class_nameが使えますね。
foreign_keyは外部キーで、「userのid」とforeign_keyが合致するものを持ってこれます。

# app/models/user.rb 図4参照
has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy # ① フォローしている人取得(Userのfollowerから見た関係)
has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy # ② フォローされている人取得(Userのfolowedから見た関係)

これで、大枠は完成です。
ただ、このままでは、自分がフォローしているユーザーやフォローされているユーザーを取得する際に、大変になります。

そこで、簡単にとってくるためにthroughを使った関連付けをUserモデルに追記します。
sourceは関連先モデル名を指定します。

# User.rbに追記
has_many :following_user, through: :follower, source: :followed # 自分がフォローしている人
has_many :follower_user, through: :followed, source: :follower # 自分をフォローしている人(自分がフォローされている人)

 これは、図3の矢印に書いた言葉をコード化しているだけです。

フォローする人(follower)は中間テーブル(Relationshipのfollower)を通じて(through)、フォローされる人(followed)と紐づく
フォローされる人(followed) は中間テーブル(Relationshipのfollowed)を通じて(through)、 フォローする人(follower) と紐づく
これで、フォローフォロワー機能の説明は終わりです。
以下おまけです。

実装(前準備)

ここから、理屈は置いといて、とりあえず作りたい人向けにまとめておきます。
【環境】
Rails 5.2.3
Ruby 2.6.3
【deviseの導入】
フォローフォロワー機能を作る前の準備をしていきましょう。
まずは、ターミナルでアプリケーションとusersコントローラを作成します。

userはdevise機能を使えば、簡単に作成できます。

viewに新規登録、ログイン、ログアウトは追記しましょう。

# app/views/layouts/application.html.erb <body>直下に追記
<header>
 <nav>
   <ul>
     <% unless user_signed_in? %>
       <li><%= link_to '新規登録', new_user_registration_path %></li>
       <li><%= link_to 'ログイン', new_user_session_path %></li>
     <% else %>
       <li><%= link_to 'ログアウト', destroy_user_session_path, method: :delete %></li>
     <% end %>
   </ul>
 </nav>
</header>

これで、前準備は終了です。ログイン・ログアウトができるようになりました。

実装(フォローフォロワー機能)

モデルに関連付けを行っていきます。
また、モデルに フォローする・外す・フォロー確認を行うメソッドを追記します。

# model/user.rb
...
has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
has_many :following_user, through: :follower, source: :followed
has_many :follower_user, through: :followed, source: :follower
# model/user.rb
# ユーザーをフォローする
def follow(user_id)
 follower.create(followed_id: user_id)
end
# ユーザーのフォローを外す
def unfollow(user_id)
 follower.find_by(followed_id: user_id).destroy
end
# フォロー確認をおこなう
def following?(user)
 following_user.include?(user)
end
# model/relationship.rb
...
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"

relationshipsコントローラを作成し、アクションを追加します。

$ rails g controller relationships
# relationship_controller.rb
def follow
 current_user.follow(params[:id])
 redirect_to root_path
end
def unfollow
 current_user.unfollow(params[:id])
 redirect_to root_path
end

あとは、ユーザーの詳細画面を作って表示するだけです。

# users_controller.rbに追記
def show
 @user = User.find(params[:id])
end
# route.rbに追記
post 'follow/:id' => 'relationships#follow', as: 'follow' 
post 'unfollow/:id' => 'relationships#unfollow', as: 'unfollow' 
resources :users, only: :show
<!-- view/users/show.html.erbを編集 -->
<p><%= "id: #{@user.id}" %></p>
<p><%= "フォロー数: #{@user.follower.count}" %></p>
<p><%= "フォロワー数: #{@user.followed.count}" %></p>
<% unless @user == current_user %>
  <% if current_user.following?(@user) %>
    <%= link_to 'フォロー外す', unfollow_path(@user.id), method: :POST %>
  <% else %>
    <%= link_to 'フォローする', follow_path(@user.id), method: :POST %>
  <% end %>
<% end %>

<h2>フォロー一覧</h2>
 <% @user.following_user.where.not(id: current_user.id).each do |user| %>
   <p>
     <%= "id: #{user.id} email: #{user.email}" %>
     <% if current_user.following?(user) %>
       <%= link_to 'フォロー外す', unfollow_path(user.id), method: :POST %>
     <% else %>
       <%= link_to 'フォローする', follow_path(user.id), method: :POST %>
     <% end %>
     <%= link_to "show", user_path(user) %>
   </p>
 <% end %>
<h2>フォロワー一覧</h2>
<% @user.follower_user.where.not(id: current_user.id).each do |user| %>
  <p>
    <%= "id: #{user.id} email: #{user.email}" %>
    <% if current_user.following?(user) %>
      <%= link_to 'フォロー外す', unfollow_path(user.id), method: :POST %>
    <% else %>
      <%= link_to 'フォローする', follow_path(user.id), method: :POST %>
    <% end %>
    <%= link_to "show", user_path(user) %>
  </p>
<% end %>
<!-- view/users/index.html.erbを編集 -->
<% if user_signed_in? %>
  <p><%= link_to "マイページへ", user_path(current_user) %></p>
  <h2>ユーザー一覧画面</h2>
  <% User.all.where.not(id: current_user.id).each do |user| %>
    <p>
      <%= "id: #{user.id} email: #{user.email}" %>
      <% if current_user.following?(user) %>
        <%= link_to 'フォロー外す', unfollow_path(user.id), method: :POST %>
      <% else %>
        <%= link_to 'フォローする', follow_path(user.id), method: :POST %>
      <% end %>
      <%= link_to "show", user_path(user) %>
    </p>
  <% end %>
<% end %