【rails】フォロー機能の実装方法

f:id:utr066:20170223120253p:plain

下準備

フォロー機能を作るまで下準備します。 あくまでフォロー機能を試したいだけなので、デザインはゴミです。

まずは、deviseインストール。pry-railsも一応。

gem 'devise'
gem 'pry-rails'

deviseのコマンドをもろもろ。

rails g devise:install
rails g devise:views
rails g devise user

usersコントローラーを作る。

rails g controller users

とりあえず適当に書く。

class UsersController < ApplicationController
before_action :authenticate_user!
def index
    @users = User.all
end

def show
    @user = User.find(params[:id])
end

end

対応するルーティングとviewを設定。

Rails.application.routes.draw do
  devise_for :users
  root 'users#index'
  resources :users
end

index.html.erb

<h1>ユーザー一覧</h1>
<% @users.each do |user| %>
  <%= user.name %>
  <br>
  <%= image_tag(user.avatar, class: "avatar_image") %>
  <%= link_to("#{user.name}のページだよ。", user_path(user.id) ) %>
  <br>
<% end %>

show.html.erb

<%= @user.name %>のページ

プロフィールに画像が欲しいのでcarrierwaveで画像投稿機能をつける。

まずは、usersテーブルに画像用のカラムを追加しよう。 nameも登録させたいから入れておきます。

rails g migration AddColumnToUsers
class AddColumnToUers < ActiveRecord::Migration
  def change
    add_column :users, :avatar, :text
    add_column :users, :name, :string
  end
end

carrierwaveインストール。

gem 'carrierwave'

bundle。

rails g uploader avatar

Userモデル。

  mount_uploader :avatar, AvatarUploader

deviseのnew.html.erbにnameとavatarのフィールドを追加。

<div class="field">
  <%= f.label :avatar %>
  <%= f.file_field :avatar %>
</div>

<div class="field">
  <%= f.label :name %>
  <%= f.text_field :name %>
</div>

deviseは初期段階では、nameやavatarは認証に使えないので記述を追加。 application.html.erb

  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up).push(:name, :avatar)
  end

フォロー機能

参考記事はこちらです。

qiita.com

フォロー機能の難しい部分は、多対多の関係にもかかわらず、Userモデルが一つだけの点だね。 普通だったら異なるモデルが多対多の関係で結ばれるけど、フォローの場合はユーザー同士だからこんなイメージになる。 f:id:utr066:20170221191051p:plain

これを実現するために、アソシエーションの記述をちょっと工夫しなくちゃいけない。 モデルはUserモデルとRelationshipモデルの二つを使う。アソシエーションを工夫して、両者でhas_many throughの関係を行う。

f:id:utr066:20170223102747j:plain

第12章 ユーザーをフォローする | Rails チュートリアル

railsチュートリアルだけど、目指す関係性はこれです。

relationshipモデル追加。

rails g model relationship
class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :following_id
      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :following_id
    add_index :relationships, [:follower_id, :following_id], unique: true
  end
end

フォローのみを考える。

まずは、フォローのみを考えましょう。

user.rb

  has_many :active_relationships,class_name:  "Relationship", foreign_key: "follower_id", dependent: :destroy

relationship.rb

  belongs_to :follower, class_name: "User"

こんなんでできる。active_relationshipsってなに?って思いますね。class_nameをつけることによってモデルの指定。follower_idで外部キーを指定。指定することによってそのレコードを特定することができる。そのレコードの名前がactive_relationship。

f:id:utr066:20170220164118j:plain 例としてUser_idが1のユーザーを見て見ます。User_idが1のユーザーは何人フォローしているか。relationshipsテーブルのfollower_idでそのユーザーがフォローしているかどうかは確認できるので今回は2人です。

フォローされることを考える

次にフォローされることを考えます。

さっきはuser_idが1のユーザーがフォローしたので、その結果誰がフォローされたのかを考えましょう。 つまり、user_idが1のユーザーにフォローされるユーザーです。フォローされたユーザーをidが2と3にします。 f:id:utr066:20170221192003p:plain

これはuser_idが1のユーザーがuser_idが2と3のユーザーをフォローしていることを表しています。

f:id:utr066:20170223103317j:plain

user_idが1のユーザーが2,3,4のユーザーをフォローするならこんな感じになる。

モデルに定義を追加する

上記であげた関係性を保つために、モデルに定義を追加しましょう。まずはフォローしているユーザーを特定するための記述です。

   has_many :active_relationships,class_name:  "Relationship", foreign_key: "follower_id", dependent: :destroy

ユーザーidが1のユーザーがいたとして、そのフォローしている人やフォロワーはrelationshipモデルのfollowing_idとfollower_idを通して行います。follwer_idとなっていて分かりにくいけど、実際にはフォローしているidが入るのでこのようにします。

上記のように記述することにより以下のようなメソッドが使用可能になります。

@user = User.find(1)

@user.active_relationships
これでユーザーid1がフォローしているユーザーのレコードが全て取ってこれます。

次にフォローされている人を特定するための記述を追加しましょう。

has_many :passive_relationships, class_name: "Relationship", foreign_key: "following_id", dependent: :destroy

ここもfollowing_idとなっていて分かりにくいですが、実際はフォローされているユーザーを表しています。

f:id:utr066:20170223105318j:plain

following_idがフォローするユーザー、follower_idがそのユーザーが、フォローしているユーザーです。 一番上で言えば、1のユーザーが2のユーザーをフォローしています。

上記のように書くことで以下のようなメソッドが使用可能となります。

@user = User.find(1)
@user.passive_relationships
これでユーザーid1がフォローされている、つまり「フォロワー」のレコードが全て取ってこれます。

今後のことを考えて、user.followingやuser.followersを使えるようにする。

user.rb

has_many :following, through: :active_relationships, source: :following
has_many :followers, through: :passive_relationships, source: :follower

これは別に書かなくてもactive_relationshipsやpassive_relationshipsを通して同じ値を得ることはできそう。だけど、user.active_relationships.countなんて分かりにくいですね。user.following.countの方がわかりやすいです。

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

ちょっとこれは後で使うので書いておきます。

コントローラーに記述する

ああ、もう面倒臭くなってきたので、コード記載して終わろう。そうしよう。 UsersControllerはこんな感じ。

class UsersController < ApplicationController
before_action :authenticate_user!
def index
    @users = User.all
end

def show
    @user = User.find(params[:id])
end

  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_follower'
  end

end

Relationshipsコントローラー。

class RelationshipsController < ApplicationController

    def create
      Relationship.create(create_params)
      redirect_to controller: 'users', action: 'index'
    end

    def destroy
      relationship =  Relationship.find(params[:id])
      relationship.destroy
      redirect_to controller: 'users', action: 'index'
    end

    private

    def create_params
        params.permit(:following_id).merge(follower_id: current_user.id)
    end

end

viewの記述

index.html.erb

<h1>ユーザー一覧</h1>
<% @users.each do |user| %>
  <%= user.name %>
  <br>
  <%= image_tag(user.avatar, class: "avatar_image") %>
  <%= link_to("#{user.name}のページだよ。", user_path(user.id) ) %>
  <br>
<% end %>

show.html.erb こいつは部分テンプレートとか使って、もっとわかりやすくした方がいいですね。

<%= @user.name %>のページ

<% if user_signed_in? %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>


<a href="<%= following_user_path(@user) %>">
  <%= @user.following.count %>
following
</a>
<a href="<%= followers_user_path(@user) %>">
  <%= @user.followers.count %>
followers
</a>

show_follow.erb

<% @user.following.each do |user| %>
<table>
<tr>
  <td>  <%= user.name %> </td>
  <td><%= link_to '詳細', user_path(user) %></td>
</tr>

</table>
<% end %>

show_follower.erb

<% @user.followers.each do |user| %>
<table>
<tr>
  <td>  <%= user.name %> </td>
  <td><%= link_to '詳細', user_path(user) %></td>
</tr>

</table>
<% end %>

_follow_form.erb

  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>

_follow.html.erb

<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :following_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

_unfollow.html.erb

<%= form_for(current_user.active_relationships.find_by(following_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

参考記事はこちらです。

qiita.com