Help us understand the problem. What is going on with this article?

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 = 出口

というのを念頭においてください。
これで100%理解出来るはずです。

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
    following = current_user.follow(@user)
    if following.save
      flash[:success] = 'ユーザーをフォローしました'
      redirect_to @user
    else
      flash.now[:alert] = 'ユーザーのフォローに失敗しました'
      redirect_to @user
    end
  end

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

  private
  def
    @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

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

mitsumitsu1128
バックエンド。最近はGo書いてます。TOEIC890/英検準1級。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした