Edited at

Railsでフォロー機能を作る方法


前提

☆必要なテーブル

・usersテーブル

・relationshipsテーブル(中間テーブルです)

☆ポイント

・アソシエーションが普通の「多対多」とは違う事(ちゃんと解説します!!)

Userモデル、usersテーブルは作っている前提で説明します!!


流れ

⓵relationshipsモデルを作る

⓶relationshipsのマイグレーションファイルを編集&実行

⓷userモデルとrelationshipsモデルにアソシエーションを書く

⓸userモデルにフォロー機能のメソッドを書く

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

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

⓻ルーティングを書く!終了!


⓵relationshipsモデルを作る

今回はuserとtweetsの関係性とは違い、userテーブル同士で「多対多」の関係を作ります。何故ならフォロワーもまたuserだからです。イメージとしてはuserテーブル同士をrelationshipsという中間テーブルでアソシエーションを組むイメージです!

まずは、realtionshipsモデルを作っていきます。


ターミナル

$ rails g model Relationship



⓶relationshipsのマイグレーションファイルを編集&実行

下記のように編集してください。


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


しっかり解説します。まず、relationshipsテーブルのカラムは

カラム
タイプ
オプション

user_id
integer
foreign_key: true

follow_id
integer
foreign_key:{to_table: users}

となります。

そもそも、relationshipsテーブルは中間テーブルなので、user_idfollow_idは「t.references」で作ってあげる必要があります。

そして、外部キーとしての設定をするためにオプションは「foreign_key: true」とします。

でも!注意したいのがfollow_idの参照先のテーブルはusersテーブルにしてあげたいので、{to_table: :users}としてあげてます。

foreign_key: trueにすると存在しないfollowsテーブルを参照してしまうからです。

t.index [:user_id, :follow_id], unique: true は、 user_idfollow_id のペアで重複するものが保存されないようにするデータベースの設定です!

これは、あるユーザがあるユーザをフォローしたとき、フォローを解除せずに、重複して何度もフォローできてしまうような事態を防いでいるだけです。

終わったら、マイグレーションファイルを実行してください!


ターミナル

$ rails db:migrate



⓷relationshipsモデルとuserモデルにアソシエーションを書く

まずは、relationshipsモデルにアソシエーションを書いていきます!


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 クラスであることを明示しています。

要は「followモデルなんて存在しないので、userモデルにbelongs_toしてね!」って事です。

さらに、バリデーションも追加してどちらか一つでも無かった場合保存されないようにします!

次にuserモデルにアソシエーションを書いていくのですが、、、

ここが山場です。理解しにくい部分なのでしっかり解説します。


app/models/user.rb

class User < ApplicationRecord

has_many :relationships
has_many :followings, through: :relationships, source: :follow
has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id'
has_many :followers, through: :reverse_of_relationships, source: :user
end

1行目のhas_many :relationshipsは大丈夫ですね。

2行目のhas_many :followingsとありますが、これはいまこのタイミングで命名したものです!followingクラス(モデル)を架空で作り出しました。

勿論、followingクラス(モデル)なんて存在しません。

なので、補足を付け足す必要があります。

through: :relationships は「中間テーブルはrelationshipsだよ」って設定してあげてるだけです。

source: :followとありますが、これは

「relationshipsテーブルのfollow_idを参考にして、followingsモデルにアクセスしてね」って事です。

結果として、user.followings と打つだけで、user が中間テーブル relationships を取得し、その1つ1つの relationship のfollow_idから、「フォローしている User 達」を取得しています。

次にフォロワー(フォローされているuser達)をとってくるための記述をします。

結論から言うとフォローの逆をしてあげればいいのです。

3行目のhas_many :reverse_of_relationships

has_many :relaitonships「逆方向」って意味です。

これはこのタイミングで命名したものです。勿論reverse_of_relationshipsなんて中間テーブルは存在しません。なので、これも補足を付け足してやります。

class_name: 'Relationship'で「relationsipモデルの事だよ〜」と設定してあげます。

次のforeign_key: 'follow_id'ですが、これ何のこっちゃ分からないと思います。

これ、「relaitonshipsテーブルにアクセスする時、follow_idを入口として来てね!」っていう事です。

ちょっと1行目のhas_many :relationshipsを思い出してください。実はこれ

has_many :relationships, foreign_key: 'user_id'

って意味なんです!

要はこれもuser_idを入り口にしてね、っていうだけです!

user_idを入口として、relationshipsテーブルという家に「おじゃましま〜す」と入って、follow_idという出口(=source: :follow)から出て、followingsテーブルからフォローしている人のデーターをとってくるイメージです!



foregin_key = 入口

source = 出口



というのを念頭においてください。

これで理解できるはずです!!

4行目に行きます。has_many :followersもこのタイミングで命名してます。勿論、followersなんてクラス存在しません。

through: :reverses_of_relationshipで「中間テーブルはreverses_of_relationshipにしてね」と設定し、

source: :userで「出口はuser_idね!それでuserテーブルから自分をフォローしているuserをとってきてね!」と設定してます。


⓸userモデルにフォロー機能のメソッドを書く

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

これやった方が後々めちゃくちゃ楽です。


app/models/user.rb

class User < ApplicationRecord

has_many :relationships
has_many :followings, through: :relationships, source: :follow
has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id'
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


注意すべき点は、フォローが自分自身ではないか?とすでにフォローしていないか?の2点です!!!!

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文はこのように書けます!

def following? では、self.followings によりフォローしている User 達を取得し、include?(other_user) によって other_user が含まれていないかを確認しています!含まれている場合には、true を返し、含まれていない場合には、false を返します!


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

relationshipsコントローラ作ってください。


ターミナル

$ rails g controller relationships


下記のように書いていきます。


app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController

before_action :set_user

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

private

def set_user
user = User.find(params[:relationship][:follow_id])
end

end



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


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 %>

※hamlで書くと下のような感じになります!

- 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|
= f.hidden_field :follow_id, value: user.id
= f.submit 'フォロー中', class: 'follow-now'
- else
= form_for(current_user.relationships.build) do |f|
= f.hidden_field :follow_id, value: user.id
= f.submit 'フォロー', class: 'follows'

あとは部分テンプレートでご自身の好きなところに置いちゃって下さい!!!

<%= render ‘relationships/follow_button’, user: @user %> みたいな感じで!


⓻ルーティングを書く!終了!


config/routes.rb

Rails.application.routes.draw do

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

ありがとうございました!