0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2つのモデルを使ってユーザーフォロー機能の実装

Posted at

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

記事投稿の背景

Xのクローンサービスを制作している時に自分が躓いたユーザーフォロー機能の実装内容について知識整理も兼ねてまとめたものです。

実装時のER図(最終版)

スクリーンショット 2024-10-14 20.41.05.png

実相当初のコード

user.rbの関連箇所を抜粋
has_many :relations, dependent: :destroy
has_many :followers, through: :relations
relation.rb
class Relation < ApplicationRecord
  belongs_to :user
  belongs_to :follower
end
follower.rb
class Follower < ApplicationRecord
  has_many :relations, dependent: :destroy
  has_many :users, through: :relations
end
followers_controller.rb
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_idfollower_idを設定しています。
Followerのカラムにはuser_idを設定していました。
Follower(フォローする人), Followee(フォローされる人)と別々にモデルを分けて実装する方法もネット上では見かけたのですが、誰かが誰かをフォローするというシンプルな考え方を基に実装したものです。
※フォローもフォローバックもフォローしている事には変わりないという意味です。

実装当初、上記のコードで機能としては問題無く動いていたのですが、Followerモデルは必要無いという判断に至りました。外部キーのfollower_idを通してFollowerモデルに保管されているuser_idを確認して誰がフォローしているかを確認するという方法ではなく、Relationモデルにフォローするユーザーとフォローされるユーザーの両方を保管すればUserとRelationモデルの2つで完結できるのではないかという考えです。

修正後のコード

user.rb
  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オブジェクト)を取得します。
relation.rb
class Relation < ApplicationRecord
  belongs_to :user
  belongs_to :follower, class_name: 'User'
end

relationモデルがuserとfollowerに属することを設定しています。
その中でfollowerもUserモデルであることを明示しています。

users_controller.rb
  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モデルだけで実装した時に最初は下にある変更前の内容で実装していました。

users_controller.rb(変更前)の抜粋
@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メソッドを使えばパフォーマンス改善やセキュリティ面の向上を見られることが分かりました。

users_controller.rb(変更後)の抜粋
@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]フォロー、フォロワー機能

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?