技術系ブログ

とにかく小ネタで

【rails】フォロー機能。追記あり

整理していなかったのでまとめておきます。
参考:第14章 ユーザーをフォローする - Railsチュートリアル

※devise導入済みですすめます。

この機能を導入したら

①view側にフォロー中とフォロワーの数の表示
②フォローボタンの作成
③実際にフォロー中の人、フォロワーを表示
この3つをview側に追加することを目指して作っていきます。

導入前に

twitterのようなフォロー機能は非対称性があります。つまり自分がフォローしていたとしても、
フォローが返されない、とう言う状況もあります。
このことを頭に入れなければいけません。

モデルの導入

rails generate model Relationship follower_id:integer followed_id:integer

そして、 できたファイルに

class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

これでfollower_idとfollowed_idの組み合わせが必ずユニークであることを保証します。
マイグレーションファイルに書いてある add_index - Qiita
データベースにindexを張る方法 - Qiita
DBのインデックスと複合インデックス - Qiita

次に$ rails db:migrate

関連付け

userとreleationshipは1対多の関係にあります。

class User < ApplicationRecord
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
end

いつもの関連付けなら、has_many :relationshipsとすることろを、自分がフォローした(能動関係)を作るためにhas_many :active_relationshipsにして、
次に、has_many :active_relationshipsというモデルは無いので
class_name: "Relationship"と書いて、Railsに探して欲しいモデルのクラス名を明示的に伝える。
更に、外部キーを先程作った foreign_key: "follower_id",で指定します。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があります。
そのため、関連付けにdependent: :destroyも追加しています。)    

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

本来ならbelongs_to :user、外部キーがuser_idとしますが、今回は仮想的にフォローする人される人に分けたいのでfollower_idfollowed_idとして関連付けます。
belongs_to :follower, class_name: "User"とすることで
userと関連付けるのをfollower_idと指定します。
また、followerfollowerdというクラスは存在しないのでclass_name: "User" として明記します。
このように書くと、今回はuser_idfollwer_idfollowed_idとして認識されます。
つまり、relationshipモデルのfollower_idやfollowed_idとuserモデルのidがひも付きます。
この結果、下記のメソッドが使えるようになります。

active_relationship.follower                                                             フォロワーを返します
active_relationship.followed                                                                フォローしているユーザーを返します
user.active_relationships.create(followed_id: other_user.id)    userと紐付けて能動的関係を作成/登録する
user.active_relationships.create!(followed_id: other_user.id)   userを紐付けて能動的関係を作成/登録する (失敗時にエラーを出力)
user.active_relationships.build(followed_id: other_user.id)          userと紐付けた新しいRelationshipオブジェクトを返す

バリテーションの追加

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"

  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

現時点では、フォローするuserとの1対多の関係ができました。この時点で②フォローボタンの作成 のみならほとんどてきてしまいます。

多対多

一人のユーザーはいくつもフォローするしされる、つまり多対多の関係にあります。

class User < ApplicationRecord
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :following, through: :active_relationships, source: :followed
  #追加したのを和訳すると  userはactive_relationshipsを通ってfollowingをたくさん持っている。
 さらに細かく言うと、外部キーがfollower_idと紐付いているuserはactive_relationshipsを通ってfollowed_idをたくさん持っている
end

Active Record の関連付け - Railsガイド
:sourceパラメーター を使って、「following配列の元はfollowed idの集合である」ということを明示的にRailsに伝えます。

この結果、user.followingでユーザーがフォロー中の人がわかります。 つまりfollowed_idを抜き出すことができます。
さらに、user.following.count でフォロー中の人数がわかるようにます。

follow、unfollow、following?メソッドを作成

followingで取得した集合をより簡単に取り扱うために、followやunfollowといった便利メソッドを追加します。

class User < ApplicationRecord

  # ユーザーをフォローする
 #followed_idにother_userを追加
 def follow(other_user)
    following << other_user
  end

# ユーザーをフォロー解除する
  # followed_idを削除
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしてたらtrueを返す
  def following?(other_user)
    following.include?(other_user)
  end
end

フォロワー側

今まではすべて自分からフォローした側でしたが、自分をもフォローされます。
しかし、これはいままでとすべて反対のことをすればいいだけです。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
end

この結果、user.followersでユーザーをフォロワーがわかります。 つまりfollower_idを抜き出すことができます。
さらに、user.followers.count でフォロワー数がわかるようにます。

view側

①view側にフォロー中とフォロワーの数の表示
②フォローボタンの作成
③実際にフォロー中の人、フォローされた人を表示
をいよいよ作っていきます。

①view側にフォロー中とフォロワーの数の表示

touch app/views/devise/shared/_stats.html.slimこの中にフォロー中とフォロワーの数の表示を書きます。

#app/views/devise/shared/_stats.html.slim
- @user ||= current_user
.stats
  | フォロー中のユーザー
  = link_to following_user_path(@user)
      div#following = @user.following.count

  | フォロワーリスト
  = link_to followers_user_path(@user)

      div#followers =@user.followers.count

そして、 表示させます。

#app/views/users/show.html.slim(わたしの場合)
= render 'devise/shared/stats'

これで、①view側にフォロー中とフォロワーの数の表示 ができました。 また、

= link_to following_user_path(@user)
= link_to followers_user_path(@user)

次にこのリンクから③実際にフォロー中の人、フォローされた人を表示のviewを作っていきます。

③実際にフォロー中の人、フォローされた人を表示

まずはルーティングから

config/routes.rb
Rails.application.routes.draw do

  resources :users do
    member do
      get :following, :followers
    end
  end

end

このURLで下記のようになります。

HTTPリクエスト  URL                           アクション    名前付きルート
GET                           /users/1/following    following             following_user_path(1)
GET                          /users/1/followers followers             followers_user_path(1)

次に、コントローラー

class UsersController < ApplicationController
  def following
    @user = User.find(params[:id])
    @users = @user.following
    render 'show_follow'
  end
  def followers
    @user = User.find(params[:id])
    @users = @user.followers
    render 'show_follow'
  end
end

render 'show_followを2つのアクションで使い回しても@users の中身は違うので問題有りません。
show_followのviewを作っていきます。 touch app/views/users/ahow_follow.htnl.slim

#follow_show
  .container
    - @users.each do |user|
      .user_content
        = link_to user_path(user)
          = user.username #←.usernameはuserモデルの中のカラム名に合わせてください
②フォローボタンの作成

これで最後です。
まずはコントローラーを作っていきます。  rails generate controller Relationships
次にルーティングです。 フォロー関係を作るか、消すかだけなので

Rails.application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end

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

次に、コントローラーを書きます

#app/comtroller/relationships_controller.rb
class RelationshipsController < ApplicationController
  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    redirect_to @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    redirect_to @user
  end
end

さらにボタンを作っていきます。 フォローするボタン
touch app/views/relationships/_follow.html.slim

=button_to current_user.active_relationships.build, params: { followed_id: @user.id }
  | フォローする

フォローを解除するボタン touch app/views/relationships/_unfollow.html.slim

=button_to current_user.active_relationships.find_by(followed_id: @user.id), method: :delete
  | 解除する

そして上記のボタンをまとめます。
touch app/views/users/_follow_form.htnl.slim

- unless @user == current_user
  #follow_form
    - if current_user.following?(@user)
      = render 'relationships/unfollow'
    - else
      = render 'relationships/follow'

最後にこれを表示させます。

#app/views/users/show.html.slim(わたしの場合)
= render 'devise/shared/stats'
= render 'users/follow_form'

おしまい。

追記

【rails】フォロー機能。 非同期編 - 技術系ブログ