なにこれ
フォロー機能を実装できたので、備忘録と言語化するために書いてます。
間違いがありましたら、ご指摘いただけると大変ありがたいです!
コツ
current_user.idと@user.idの違いを理解すること
前提条件
UserモデルとUsersテーブルを作成済み
データベース設計
フォローしたデータを保存するためのrelationshipsテーブルを作成します
コントローラーも作成しておきます。
rails g model Relationship
rails g controller Relationships
t.references :follower, foreign_key: { to_table: :users }
t.references :following, foreign_key: { to_table: :users }
t.index [:follower_id, :following_id], unique: true
follower_idがフォローされたユーザーのidにします。
follower_idがフォローしたユーザーのidにします。
t.index [:follower_id, :following_id], unique: trueは、
follower_idとfollowing_idが一意の関係になるように設定してます。被らないように。
つまり、同じユーザーを何回もフォローできたらおかしいので!
終わったらrails db:migrateします。
モデルの編集
Userモデル
has_many :following_relationships, foreign_key: "follower_id", class_name: "Relationship"
has_many :following, through: :following_relationships
has_many :follower_relationships, foreign_key: "following_id", class_name: "Relationship"
has_many :followers, through: :follower_relationships
#フォローしているかを確認するメソッド
def following?(user)
following_relationships.find_by(following_id: user.id)
end
#フォローするときのメソッド
def follow(user)
following_relationships.create!(following_id: user.id)
end
#フォローを外すときのメソッド
def unfollow(user)
following_relationships.find_by(following_id: user.id).destroy
end
userモデルのアソシエーションがフォロー機能を実装する上で一番の山場だと思います。
フォロー機能を実装するための様々なモデルの組み合わせを調べましたが、
個人的にこれが一番シンプルかつ直感的に分かりやすいと感じたものを使用します。
has_many :followingという存在しないモデルとアソシエーションを組んでますが、
これは「仮想のモデルとアソシエーションを組んでいる状態」と考えて下さい。
仮想のモデルを作成して、そちらとアソシエーションを組んでます。
followingとfollowerに注目した方がいいです。
日本語的に見るとfollowとfollowingの表現がおかしいところがありますが、これで正しいです。
ご自身で紙に図で書いてみると理解しやすいと思います。
フォローしているユーザーを調べたい!と思ったら、
そのユーザーのフォロワー(follower_id)を探してくる。みたいなイメージです
逆にフォロワーのユーザーを調べたい!と思ったら、
そのユーザーをフォローしているユーザー(following_id)を取ってくる。イメージです。
🔳following_relationships
class_name: “Relationship”モデルになってるので、そちらを参照してます。
foreign_key: “follower_id”にすることで、カラムにfollower_idが追加されます。
つまり、実際にカラムになるのは、Relationshipモデルのfollower_idです。
申し訳ないですが、この関係を言語化するのは自分には無理なので紙に書いてイメージして下さい。正直、文章書いてる途中でもゴチャ混ぜになってきます。笑
Relationshipモデルのfollower_idを経由して、
Userモデルと仮想のfollowing_relationshipsモデルと
アソシエーションを組んでます。
つまり、following_relationshipsモデルにはRelationshipモデルのfollower_idが
外部キーとして存在してる。みたいなイメージです
🔳following
こいつがやってることは、
フォローしているユーザー達の情報を持ってくるためのアソシエーションです。
through: :following_relationshipsとしてあるので、
仮想モデルのfollowing_relationshipsの情報をガバーっと持ってくるイメージです。
使用例)
@user.following.each do |user|
user.nickname
上記のコードを書くと、@userがフォローしている全てユーザーのidを表示できます。
別の具体例)
URL: /users/1/
@userにid=1というユーザーの情報が代入されてます。
user_id[1]が、user_id[2]とuser_id[3]をフォローしているとします。
上記の使用例で例えると、
eachを実行するとuser_id[2]とuser_id[3]の情報がeach分で出力できます。
🔳follower_relationships
外部キーをfollowing_idとして、Relationshipモデルを参照しています。
follower_relationshipsにはfollowing_idのカラム(情報)が追加されてます。
Relationshipモデルのfollowing_idを経由してUserモデルとアソシエーションを組みます。
🔳followers
こいつがやってることはfollowingのフォロワーバージョンです。
follower_relationshipsを通じて、そのユーザーのフォロワーを探してきてくれます。
使用例)
@user.follower.each do |user|
user.nickname
@userにはuser_id[1]が代入されてます。
user_id[1]はuser_id[4]とuser_id[5]にフォローされてます。
上記のeach文を実行すると、user_id[4]とuser_id[5]の情報が出力されます。
🔳def following?
端的に言うと、following_relationshipsテーブルのfollowing_idにuserのidが存在するか確認しています。
すごく長く説明します。
- if user_signed_in? && @user != current_user
#follow_form
- if current_user.following?(@user)
上記の例ですと、3行目でcurrent_userがそのユーザーをフォローしているか判別しています。
この時のDBの流れは、current_user(follower_idに当たる。)が、
following_idを探してくる。と言った流れです。
1行目でcurrent_user.idが@user.idと一致してないのを確認済みです。
例)
current_user.id[3]の場合だと、現在/users/2/のページを表示しているので、
変数@userにはuser_id[2]が代入されています。
Relationshipテーブルには、follower_idに[3]が、following_idには[2]が代入されます。
一致する条件があるならtrueを返します。みたいなイメージです。
その先は既にフォローしているならフォローを外すリンクを表示、フォローしてないなら
フォローするリンクを表示します。
以下は頑張って言語化しただけなので、読み飛ばしてokです。
仮想モデルのfollowing_relationshipsから、find_byでfollowing_idを基準にfollower_idを探します。
別の言い方をすると、follower_idに対応するfollowing_id
(Relationshipモデルの同じidのレコード)を探してきます。
理由は、仮想モデルのfollowing_relationshipsの内容(カラム)は、実際はRelationshipsモデルのfollower_idだからです。
🔳follow
following?メソッドのidを作る版です。
(user)には現在表示しているページのユーザーが代入されてます。
following_idに@user.id(表示しているページのuser_id)が入ります。
follower_idにはcurrent_user.idが入ります。
🔳unfollow
following?の条件で、following_id(user.id)とfollower_id(current_user.id)の
一致する組み合わせががあったらdestroyしてフォローを解除する流れです。
Relationshipモデル
#自分をフォローしているユーザー
belongs_to :follower, class_name: "User"
#自分がフォローしているユーザー
belongs_to :following, class_name: "User"
#バリデーション
validates :follower_id, presence: true
validates :following_id, presence: true
belongs_to :followerは Userモデルに記述したhas_many :followersに該当します。 同じくbelongs-to :followingも Userモデルに記述したhas_many :followingに該当します。 class_name “User”`にすることで存在しないfollowerモデルを参照することを防いでいます。
ルーティングの作成
Rails.application.routes.draw do
resources :users do
member do
get :following, :followers
end
end
resources :relationships, only: [:create, :destroy]
end
フォローする(create)とフォローを解除する(destroy)をアクションを追加しました。
get :following, :followersはフォローしてる人とフォロワーを一覧で表示するのに使えます。
この記事ではやり方は紹介しませんが、気になる人は実装してみて下さい。
relationships_controllerの編集
class RelationshipsController < ApplicationController
def create
@user = User.find(params[:following_id])
current_user.follow(@user)
end
def destroy
@user = User.find(params[:id])
current_user.unfollow(@user)
end
end
createアクションではparamsでfollowing_idを探してきて@userに代入しています。
ここで言うfollowing_idに当たるものは、
usersのshowアクションで表示しているユーザーページのuser_idです。
例)/users/1/ なら、user_id[1]
destroyもcreateアクションとやってることは同じです。
rails routesで確認すると、destroyアクションは以下のようになってます。
relationship DELETE /relationships/:id(.:format)
paramsが[:id]になってる理由は、
後述するフォローを解除するリンクでfollower_id(current_user.id)が代入されている状態なのと、
既にfollowing_idに値が入っているので、params[:id]でクリックした値を代入するだけでokっていう認識です。
ここは曖昧なので、もしかしたら違うかも知れません。
ビューの作成
必要部分のみ抜粋して表示します。
= render "relationships/follow"
renderで部分テンプレートを表示させてます。
@userに値が入ってるならお好きな部分に設置してもらってokです
- if user_signed_in? && @user.id != current_user.id
#follow_form
- if current_user.following?(@user)
= form_with(model: current_user, url: relationship_path(@user.id), method: :delete, remote: true) do |f|
= f.submit "フォロー解除", class: "btn btn-outline-secondary"
- else
= form_with(model: current_user, url: relationships_path, method: :post, remote: true) do |f|
= hidden_field_tag :following_id, @user.id
= f.submit "フォローする", class: "btn btn-outline-secondary"
relationship DELETE /relationships/:id(.:format) relationships#destroy
relationships POST /relationships(.:format) relationships#create
ここはしっかり解説します。
1行目でユーザーがサインインしている、
かつ@user.idとcurrent_userのidが違うことを確認してます。
3行目でcurrent_userが@userをフォローしているか確認しています。
繰り返しですが、
この@userに代入されているのはuser_id(URLの/users/1/の数字の部分)です。
4行目でform_withでフォロー解除ボタンを表示しています。
model: current_userでfollower_idにcurrent_user.idが代入されていることを表しています。
url: relationship_path(@user.id)
とはrelations#destroyを発火させるパスを指定します。
つまりフォロー解除ボタンです。@user.idは表示しているページのuser_idです。
urlと書いてある理由はデータベースに保存するためか、引数が2つあるからです。
リファレンスを見ても参考になるところが分からなかったので、憶測です。
違ったらすみません!
remote: trueでjs形式でデータを送信してます。
f.submitでフォームを送信します。
下から3行目でフォローするボタンを表示しています。
model: current_userは先程と同じくfollower_idにcurrent_user.idを代入するためです。urlでrelationships#createアクションを発火させるパスを書いてます。
先ほどと一点違う点があります。
= hidden_field_tag :following_id, @user.id
この文が存在する理由です。
following_idに@user.idを追加させるためです。
この文がないと、フォローしようとしてもデータベースに
「誰をフォローすんの?」って分からなくて怒られます。
余談ですが、自分はform_withでmodelを指定する理由と、
:following_id, @user.idが書いてある理由が分からなくて悩みました笑
これでようやく最後です!
createとdestroyのjs.hamlファイルを作成します。
js.hamlファイルについて理解されている前提で進めます。
端的に言うと、アクション発火時に自動的に実行されるJavascriptです。
$("#follow_form").html("#{j(render("relationships/follow"))}");
$("#follow_form").html("#{j(render("relationships/follow"))}");
内容はどちらも同じです。
follow_formにhtml形式でrenderします。
これで非同期通信ができます。
以上で実装完了です!お疲れ様です。
ここまで読んでいただき、ありがとうございます!