UM@の日常

Rails フォローフォロワー機能を作る

~参考リスト~

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

・今回、以下の方の記事を拝見してフォロー・フォロワー機能を作らせていただきました。
★フォロー・フォロワー機能について
qiita.com
www.y-techmemo.work

★アソシエーションについて
web-camp.io
qiita.com



~前提~

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

■利用するテーブル
usersテーブル
relationshipsテーブル(中間テーブル)
■ポイント
アソシエーション「多対多」(user対user)「一対多」(user対relationships)の形をとる
・アソシエーションが普通の「多対多」とは違う
Userモデルusersテーブルは作っている前提
※僕が機能を実装する際はgemでdeviseを導入していました


~作成手順~

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

1⃣relationshipモデルを作る

2⃣relationshipのマイグレーションファイルを編集&実行(db:migrate)

3⃣userモデル(user.rb)relationship(relationship.rb)モデルにアソシエーション(belongs_to/has_many)を書く

4⃣userモデル(user.rb)にフォロー機能のメソッドを書く

5⃣relationshipsコントローラを作成&編集

6⃣フォローボタン(form_for)をviewに設置



1⃣~relationshipモデルを作る~

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

$ rails g model Relationship

userテーブル(テーブル名:users)同士で「多対多」の関係を作ります。何故ならフォロワーもまたuserだからです。userテーブル(テーブル名:users)同士をrelationshipsという中間テーブルでアソシエーションを組むイメージです。
そのため、初めにrelationshipモデルを作成します。
※フォロー機能の難しい部分は、多対多の関係にもかかわらず、Userモデルが一つだけの点です。

参照図引用(【rails】フォロー機能の実装方法 - わいの技術メモ)
f:id:UMAweb1:20191025135946p:plain

これを実現するために、アソシエーションの工夫が必要になります。
両者でhas_many throughの関係を行うのですが、こちらは後ほど詳細を記載しようと思います。

参照図引用(第12章 ユーザーをフォローする - Railsチュートリアル)
f:id:UMAweb1:20191025140632j:plain



2⃣relationshipのマイグレーションファイルを編集&実行(db:migrate)


ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

マイグレーションファイルを以下のように編集

|db/migrate/年月日時_create_relationships.rb|

class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.references :user, foreign_key: true
      t.references :follow, foreign_key: { to_table: :users }

      t.timestamps

      t.index [:user_id, :follow_id], unique: true
    end
  end
end


◎relationshipテーブル(テーブル名:relationships)のカラムは以下になります。

                                                             
カラムタイプオプション
user_idintegerforeign_key
follow_idintegerforeign_key:{to_table: users}
relationshipsテーブルは中間テーブルのため、user_id(user)とfollow_id(follow)は「t.references」で作る必要があります。 外部キーとしての設定をするためにオプションは「foreign_key: true」を利用します。
注意点としてはfollow_idの参照先のテーブルはuserテーブル(テーブル名:users)にする必要があるため、{to_table: :users}とすることで、存在しないfollowテーブルの参照を防ぎます。

foreign_key: trueにすると存在しないfollowsというテーブルを参照しようとする。

t.index [:user_id, :follow_id], unique: true」 は、 user_idfollow_id のペアで重複するものが保存されないようにするデータベースの設定です。
これは、あるユーザーがあるユーザーをフォローしたとき、フォローを解除せずに、同じユーザーを重複して何度もフォローできてしまうような事態を防いでいます。 終わったら、db:migrateを実行します。

$ rails  db:migrate



3⃣userモデル(user.rb)relationship(relationship.rb)モデル
アソシエーション(belongs_to/has_many)を書く

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

初めにrelationshipモデルにアソシエーションを記載していきます。

|app/models/relationship.rb|

class Relationship < ApplicationRecord
  belongs_to :user
  belongs_to :follow, class_name: 'User'

  validates :user_id, presence: true
  validates :follow_id, presence: true
end


class_name: ‘User’ と補足設定することで、followモデルという存在しないモデルを参照することを防ぎ、Userモデルであることを明示しています。
さらに、バリデーションも追加してどちらか一つでも無かった場合保存されないようにします!
※1
class_name:’model名’のオプションは、モデル名を指定することができるオプションである。
belongs_toの引数に任意の名称(モデル)を設定することができ、追加されるメソッド名を変更することができるが、
必ずforeign_keyオプションも設定すること(今回)

※2
railsのデフォルトでは、外部キーを表す命名規則が${model名}_idと決まっているため、
同じmodelを参照する外部キーがそのままでは設定できないことからclass_nameオプションを利用する


◎次にuserモデルにアソシエーションを記載します。

|app/models/user.rb|

class User < ApplicationRecord
# 能動的関係
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follow
# 受動的関係
  has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id', dependent: :destroy
  has_many :followers, through: :reverse_of_relationships, source: :user
end

# throughオプションによりrelationships経由でfollowings・followersにアクセスできるようになる 
# = 架空のモデルを介して、対象のモデルと多対多の関連付け => これにより情報抽出可能
# sourceオプション = has_many :through関連付けの関連付け元(従属するモデル※今回はモデルではないもの〔follow〕も含む)名を指定する
# foreign_keyオプション = 関連付けるモデルを指す外部キーのカラム名を設定する。このオプションを使用しなければ、「belongs_toの引数_id」が指定される



■1行目の has_many :relationships はuserモデルとrelationshipモデルとで一対多の関係を表しています。

■2行目のhas_many :followingsですが、これはいまこのタイミングで架空で作り出された、followingクラス(モデル)です。
勿論、followingクラス(モデル)なんて存在しないため、補足を付け足す必要があります。
through: :relationships(オプション) は「中間テーブルはrelationshipsである」と設定しています。

source: :followとありますが、これは
「following配列の元はfollow_idの集合である」ということを明示的にRailsに伝えています。
結果として、user.followings と打つだけで、user が中間テーブル relationships を取得し、その1つ1つのfollow_idから、「フォローしている User 達」を取得しています。

◎次にフォロワー(フォローされているuser達)をとってくるための記述をします。
1,2行目(フォローしている側)と逆方向(フォローされている側)を記入していきます

■3行目has_many :reverse_of_relationshipsは、
1行目のhas_many :relaitonships がフォローしている側と仮定して、その「逆方向」:フォローされている側を仮定する意味で命名しています。
これもこのタイミングで命名したものです。勿論reverse_of_relationshipsという中間テーブルは存在しないため、これも補足を付け足す必要があります。
class_name: 'Relationship'で「reverse_of_relationshipsをrelationsipモデルの事だ」と設定しています。

■4行目は、2行目と同じく架空で作り出された、followersクラス(モデル)に対して、through: :relationships(オプション) を用いて「中間テーブルはrelationshipsである」と設定し、sourceオプションにて、「follower配列の元はuser_idの集合である」ということを明示的にRailsに伝えています。

ここで実は1行目の内容は一部省略されている内容があります。
その内容は以下になります。

has_many :relationships, foreign_key: 'user_id', dependent: :destroy

◎アソシエーションによる流れをまとめると以下になります

●1行目で、フォローしている側のUserから見て、フォローされている側のUserを(中間テーブルを介して)集める。参照するカラムは 'user_id(フォローする側)

●2行目で、中間テーブル(relationships)を介して「follow」モデルのUser(フォローされた側)「follow_id」を集めることを「followings」と定義

●3行目で、フォローされている側のUserから見て、フォローしてくる側のUserを(中間テーブルを介して)集める。参照するカラムは’follow_id’(フォローされる側)

●4行目で、中間テーブル(relationships)を介して「user」モデルのUser(フォローする側)「user_id」を集めることを「followers」と定義



4⃣userモデル(user.rb)にフォロー機能のメソッドを書く

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
※userモデルにフォロー機能のメソッドを書いておきます。

|app/models/user.rb|

class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follow
  has_many :reverse_of_relationships, class_name: 'relationships', foreign_key: 'follow_id', dependent: :destroy
  has_many :followers, through: :reverse_of_relationships, source: :user

  def follow(other_user)
    unless self == other_user
      self.relationships.find_or_create_by(follow_id: other_user.id)
    end
  end

  def unfollow(other_user)
    relationship = self.relationships.find_by(follow_id: other_user.id)
    relationship.destroy if relationship
  end

  def following?(other_user)
    self.followings.include?(other_user)
  end

end

●def follow では、unless self == other_user によって、フォローしようとしている other_user が自分自身ではないかを検証しています。
self には user.follow(other) を実行したとき user が代入されます。つまり、実行した User のインスタンスが self です(以降のコントローラーより詳細確認)

更に、self.relationships.find_or_create_by(follow_id: other_user.id) は、見つかれば Relation を返し、見つからなければ、 self.relationships.create(follow_id: other_user.id) としてフォロー関係を保存(create = new + save)することができます。これにより、既にフォローされている場合にフォローが重複して保存されることがなくなる

●def unfollow では、フォローがあればアンフォローしています。また、relationship.destroy if relationshipは、relationship が存在すれば destroy します。
if文は後置ifで記載することが可能です。

●def following? では、self.followings によりフォローしている User 達を取得し、include?(other_user) によって other_user が含まれていないかを確認しています!含まれている場合には、true を返し、含まれていない場合には、false を返します。そのため、既にフォローしているか・していないかはこれで定義されます。
※self .(user自身を表すオブジェクト) は省略可能です。



5⃣relationshipsコントローラを作成&編集

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

コントローラーは(Railsでフォロー機能を作る方法 - Qiita)を参考に若干僕のアプリに導入できるよう、訂正したものが以下になります。
コントローラーはターミナルより【relationships】と名付けて作成してください。

|app/controllers/relationships_controller.rb|

class RelationshipsController < ApplicationController
  def create
    user = User.find(params[:follow_id])
    following = current_user.follow(user)
    if following.save
      flash[:success] = 'ユーザーをフォローしました'
      redirect_to user
    else
      flash.now[:alert] = 'ユーザーのフォローに失敗しました'
      redirect_to user
    end
  end

  def destroy
    user = User.find(params[:follow_id])
    following = current_user.unfollow(user)
    if following.destroy
      flash[:success] = 'ユーザーのフォローを解除しました'
      redirect_to user
    else
      flash.now[:alert] = 'ユーザーのフォロー解除に失敗しました'
      redirect_to user
    end
  end
end



6⃣フォローボタン(form_for)をviewに設置

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

こちらもまた、(Railsでフォロー機能を作る方法 - Qiita)を参考に僕のアプリに導入できるよう、引数を若干訂正しております。
※部分テンプレートで利用する際、インスタンス変数をrenderメソッドで渡すため

|app/views/relationships/_follow_button.html.erb|

<% unless current_user == @user %>
  <% if current_user.following?(@user) %>
    <%= form_for(current_user.relationships.find_by(follow_id: @user.id), html: { method: :delete }) do |f| %>
      <%= hidden_field_tag :follow_id, @user.id %>
      <%= f.submit 'Unfollow', class: 'btn btn-danger btn-block' %>
    <% end %>
  <% else %>
    <%= form_for(current_user.relationships.build) do |f| %>
      <%= hidden_field_tag :follow_id, @user.id %>
      <%= f.submit 'Follow', class: 'btn btn-primary btn-block' %>
    <% end %>
  <% end %>
<% end %>

# hidden_field_tag :follow_id, @user.id は、follow_idというパラメータに、 @user.idの情報を渡しています。
# = hidden_field_tag どのようなシンボルに(第一引数)、どの値を渡すか(第二引数) 



あとは、フォロー・フォロワー機能を実装したいところに

<%= render ‘relationships/follow_button’, user: @user %> 

といった形で記述をすれば問題ありません。

※route.rbへのルーティングも忘れないように

resources :relationships, only: [:create, :destroy]


                                  • -

自分への備忘録という形でブログに記載させていただきました。
先人の方たちに続けれるように頑張りたいです。