この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。
記事投稿の背景
Xのクローンサービスを制作している時に自分が躓いたユーザーフォロー機能の実装内容について知識整理も兼ねてまとめたものです。
実装時のER図(最終版)
実相当初のコード
has_many :relations, dependent: :destroy
has_many :followers, through: :relations
class Relation < ApplicationRecord
belongs_to :user
belongs_to :follower
end
class Follower < ApplicationRecord
has_many :relations, dependent: :destroy
has_many :users, through: :relations
end
class FollowersController < ApplicationController
before_action :set_followers, only: %i[destroy]
# ログインしていない場合、ログインページへ遷移させる
before_action :authenticate_user!, only: %i[create destroy]
def create
@follower = Follower.create(user_id: current_user.id)
@relation = Relation.new(user_id: params[:user_id], follower_id: @follower.id)
if @follower.save && @relation.save
redirect_to home_index_path, notice: 'フォローしました。'
else
redirect_to home_index_path, alert: 'フォローに失敗しました。'
end
end
def destroy
@follower.destroy!
redirect_to home_index_path, notice: 'フォローを解除しました。'
end
private
def set_followers
@follower = Follower.find(params[:id])
end
end
モデルの関連としては
User ↔︎ Relation ↔︎ Followerとなっています。
Relationのカラムには外部キーとしてuser_id
とfollower_id
を設定しています。
Followerのカラムにはuser_id
を設定していました。
Follower(フォローする人), Followee(フォローされる人)と別々にモデルを分けて実装する方法もネット上では見かけたのですが、誰かが誰かをフォローするというシンプルな考え方を基に実装したものです。
※フォローもフォローバックもフォローしている事には変わりないという意味です。
実装当初、上記のコードで機能としては問題無く動いていたのですが、Followerモデルは必要無いという判断に至りました。外部キーのfollower_id
を通してFollowerモデルに保管されているuser_id
を確認して誰がフォローしているかを確認するという方法ではなく、Relationモデルにフォローするユーザーとフォローされるユーザーの両方を保管すればUserとRelationモデルの2つで完結できるのではないかという考えです。
修正後のコード
has_many :relations, foreign_key: :user_id, dependent: :destroy
has_many :following, through: :relations, source: :follower
has_many :passive_relations, class_name: 'Relation', foreign_key: :follower_id, dependent: :destroy,
inverse_of: :follower
has_many :followers, through: :passive_relations, source: :user
- 上の2行で user → relations → followingという関連を作り出し、このユーザーがフォローしている他のユーザーを取得しています。
foreign_key: :user_id
で「relationテーブルのuser_id
はこのユーザーを示している」ことを明示しています。
source: :follower
でrelationテーブルのfollower_id
カラムを使ってフォローしている他のユーザー(Userオブジェクト)を取得します。
- 下の2行で user ← passive_relatons ← followersという関連を作り出し、このユーザーをフォローしている他のユーザーを取得しています。
foreign_key: :follower_id
で「passive_relationsテーブルのfollower_id
はこのユーザーを示している」ということを明示しています。
また、class_name: 'Relation'
と記載することでpassive_relationsテーブルもRelationモデルのテーブルであることを明示しています。
source: :user
ではrelationテーブルのuser_id
カラムを使って、自分をフォローしているユーザー(Userオブジェクト)を取得します。
class Relation < ApplicationRecord
belongs_to :user
belongs_to :follower, class_name: 'User'
end
relationモデルがuserとfollowerに属することを設定しています。
その中でfollowerもUserモデルであることを明示しています。
before_action :set_followers, only: %i[unfollow]
# ログインしていない場合、ログインページへ遷移させる
before_action :authenticate_user!, only: %i[show edit update follow unfollow]
def follow
@relation = current_user.relations.build(follower_id: params[:id])
if @relation.save
redirect_to home_index_path, notice: 'フォローしました。'
else
redirect_to home_index_path, alert: 'フォローに失敗しました。'
end
end
def unfollow
@relation.destroy!
redirect_to home_index_path, notice: 'フォローを解除しました。'
end
private
def set_followers
@relation = current_user.relations.find_by(follower_id: params[:id])
end
モデル以外で実装時のポイント
Followerモデルを削除してUser, Relationモデルだけで実装した時に最初は下にある変更前の内容で実装していました。
@relation = Relation.new(user_id: current_user.id, follower_id: params[:id])
def set_followers #既存のフォロー関係を削除する為に特定する
@relation = Relation.find_by(follower_id: params[:id])
end
後で調べていてbuild
メソッドを使えばパフォーマンス改善やセキュリティ面の向上を見られることが分かりました。
@relation = current_user.relations.build(follower_id: params[:id])
def set_followers #既存のフォロー関係を削除する為に特定する
@relation = current_user.relations.find_by(follower_id: params[:id])
end
変更前のRelation.new(user_id: current_user.id, follower_id: params[:id])
でも機能はするのですが、変更後のようbuild
メソッドを使えばアソシエーションを考慮したインスタンスの生成ができます。
また、@relation = Relation.find_by(follower_id: params[:id])
はお勧めできないです。
変更後のように@relation = current_user.relations.find_by(follower_id: params[:id])
とすべきです。
理由は2つあります。
- 理由1:パフォーマスが悪い
@relation
はフォローする人、される人の関係を保存する為のインスタンス変数なのですが、片方のフォローする側はログインしているユーザーとなります。よって、ログインしているユーザーが関わる関係に限定するべきです。変更前はRelation...
とRelationのテーブル全体から探し出そうとしています。
一方で、変更後はcurrent_user...
とログインユーザーに関わるオブジェクトに絞ることで検索をかける母数に制限をかけています。限定することで全体ではなく、限られた中から探すので検索の手間を短縮できます。この制作しているサービスはDeviseというGemを導入しているので、ログインユーザーをcurrent_user
メソッドで取得できます。
- 理由2:セキュリティに問題がある
理由1と内容が若干重複します。
変更前はRelation...
とRelationテーブルの全体から探すように意図しているので、誤って他のユーザーのフォロー関係にアクセスしてしまう可能性があります。current_user...
とログインユーザーに関わるフォロー関係に限定することで無関係のフォロー関係にアクセスする可能性を無くすことができます。
参考にしたサイト
【Rails】buildメソッド結構有能かもしれない
Railsモデルの関連付けで、buildを使うときのメソッド名
[Rails]フォロー、フォロワー機能