Railsでフォロー機能を作ろう
個人ブログから移行しました。
はじめに
このページでは、Twitterのようなアプリケーションにあるフォローフォロワー機能を作ります。railsチュートリアルのフォローフォロワー機能で理解に苦しむ方を多く見るため、できるだけわかりやすく説明します。
そのため、railsチュートリアルの記述と若干異なります。
考え方を理解する
【前提】
まず、以下を前提としましょう。
・フォローする人をfollowing
・フォローされる人をfollowed
【イメージ】
フォローする人もフォローされる人もUserであるため、こうなってしまいそう、、、
これじゃ、よくわからん、、、
ここで、Userはフォローする側とフォローされる側に分けて考える必要がありそうです。
そうすると、多対多のリレーションになるので、Relationshipという中間テーブルを用意することで、1対多の関係に変えます。
下の図のように、2つの関連付けが必要になります。
モデルの関係を理解する
上のイメージを参考にして、実際に関係をコード化します。
まずは、簡単な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通りを考える必要があります。
このとき、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 %