【Twitterクローン作成】5.フォロー機能作成

プログラミング




 

この記事は【Twitterクローン作成】4.ツイート機能作成の続きとなります。

 




フォロー機能の実装

フォロー機能の考え方

フォローする側は複数なのに対して、フォローされる側は1人です。この1対多多対1をくみ合わせることで多対多の関係が出来上がります。その関係性を中間にデータベースを置くことで表します。

また、AさんのフォローにXがついているのは自分で自分をフォローすることができないためです。

 

構造理解したところで早速実装していきましょう。

作成するモデルは User と User をつなぐModelです。

 

Model

ターミナル

$ rails g model Relationship user:references follow:references

model名はRelationshipとしましょう。

 

マイグレーションファイルの確認

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: true

      t.timestamps
    end
  end
end

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

followsというテーブルはないのでusersのテーブルを参照するように、{ to_table: :users }によって指定してあげます。

これを外部キーと言います。

外部キーについて詳しく知りたい方はこちら

 

上記の通りに記述したら、マイグレーションを実行します

ターミナル

$ rails db:migrate

 

中間のデータベースを通して、多対多の関係であることを示していきます。

app/models/relationship.rb

class Relationship < ApplicationRecord
  belongs_to :user
  belongs_to :follow
end

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

FollowというmodelはないのでUserモデルを参照するように指定します。

 

Userモデルに多対多の関係性を明記します

app/models/user.rb

class User < ApplicationRecord
  before_save { self.email.downcase! }
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },
                    uniqueness: { case_sensitive: false }
  has_secure_password

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

複雑ですので、先ほどの図を使って説明します。

has_many :relationships

has_many :relationshipsは矢印の方向性を表ており、フォローする側がフォローされる側を参照できるようにしてあります。ただあくまでもここは参照であり、ユーザ達を見ているだけです。

 

has_many :followings, through: :relationships, source: :follow

has_many :followings, through: :relationships, source: :followは has_many: relationships で示した矢印の方向性に加えてユーザを取得できるようになっています

has_many: relationships では relationships のデータベースを経由してフォローされる側のユーザを見ているのに対し、has_many :followings, through: :relationships, source: :follow はフォローされる側の取得までできるようになっていることに注意してください。

 

has_many :reverses_of_relationship, class_name: Relationship, foreign_key: follow_id

has_many :followers, through: :reverses_of_relationship, source: :user

これらはただ先ほどと逆の方向性を示しているだけです。

図で表すとこんな感じです。

 

has_many :reverses_of_relationship, class_name: Relationship, foreign_key: follow_id

 

has_many :followers, through: :reverses_of_relationship, source: :user

has_many :reverses_of_relationship, class_name: Relationship, foreign_key: follow_id‘によって、フォローされる側からフォローする側を参照します。

has_many :followers, through: :reverses_of_relationship, source: :userによって、参照したユーザを取得できるようにします。

もっと詳しく知りたい方はこちらのRails tutorial フォロー機能をどうぞ!

 

follow/unfollowメソッドの定義

class User < ApplicationRecord
  before_save { self.email.downcase! }
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },
                    uniqueness: { case_sensitive: false }
  has_secure_password

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

follow(other_user)
unless self == other_userでフォローするユーザとフォローされるユーザが同一ではないことを判断し、同一人物ではなかった場合フォローされるユーザのIDを探してフォローします。

unfollow(other_user)
relationship = self.relationships.find_by(follow_id: other_user.id)で既にフォローしているユーザを見つけ、if relationship フォローしていた場合、relationship.destroyでフォローを外します。

following?(other_user)
上2つのメソッドでは中間テーブルであるrelationshipsを参照してユーザのidを探した上でフォローをするか、外すかを判断していたが、self.followings.include?(other_user) ではhas_many :followings, through: :relationships, source: :followで指定した、followingsテーブルを通してフォローしているかどうかを判断し、すでにフォローしていればtrue、していなければfalseを返す。

 

Router

config/routes.rb

Rails.application.routes.draw do
  root to: 'toppages#index'

  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create'
  delete 'logout', to: 'sessions#destroy'

  get 'signup', to: 'users#new'
  resources :users, only: [:index, :show, :new, :create] do
    member do
      get :followings
      get :followers
    end
  end

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

member do
これはget : followings と get :followers に対して/users/:id/followings、/users/:id/followersというURLを作成します。

 

create,destroy

Controller

$ rails g controller relationships create destroy

 

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :require_user_logged_in

  def create
    user = User.find(params[:follow_id])
    current_user.follow(user)
    flash[:success] = 'ユーザをフォローしました。'
    redirect_to user
  end

  def destroy
    user = User.find(params[:follow_id])
    current_user.unfollow(user)
    flash[:success] = 'ユーザのフォローを解除しました。'
    redirect_to user
  end
end

 

フォローボタン作成

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

 

ユーザ詳細ページにフォローボタンの設置

<div class="row">
  <aside class="col-xs-4">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><%= @user.name %></h3>
      </div>
      <div class="panel-body">
        <img class="media-object img-rounded img-responsive" src="<%= gravatar_url(@user, { size: 500 }) %>" alt="">
      </div>
    </div>
    <%= render 'relationships/follow_button', user: @user %>
  </aside>
  <div class="col-xs-8">
    <ul class="nav nav-tabs nav-justified">
      <li class="<%= 'active' if current_page?(user_path(@user)) %>"><%= link_to user_path(@user) do %>Tweets <% end %></li>
      <li><a href="#">Followings</a></li>
      <li><a href="#">Followers</a></li>
    </ul>
    <%= render 'tweets/tweets', tweets: @tweets %>
  </div>
</div>

 

 

followings/followers

フォローしている人とフォロワーを表示します。

 

Controller

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :require_user_logged_in, only: [:index, :show, :followings, :followers]

  def index
    @users = User.all.page(params[:page])
  end

  def show
    @user = User.find(params[:id])
    @tweets = @user.tweets.order('created_at DESC').page(params[:page])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      flash[:success] = 'ユーザを登録しました。'
      redirect_to @user
    else
      flash.now[:danger] = 'ユーザの登録に失敗しました。'
      render :new
    end
  end

  def followings
    @user = User.find(params[:id])
    @followings = @user.followings.page(params[:page])
  end
  
  def followers
    @user = User.find(params[:id])
    @followers = @user.followers.page(params[:page])
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end

 

View

新規で以下2つのファイルを作成して下さい。

app/views/users/followings.html.erb

<div class="row">
  <aside class="col-xs-4">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><%= @user.name %></h3>
      </div>
      <div class="panel-body">
        <img class="media-object img-rounded img-responsive" src="<%= gravatar_url(@user, { size: 500 }) %>" alt="">
      </div>
    </div>
    <%= render 'relationships/follow_button', user: @user %>
  </aside>
  <div class="col-xs-8">
    <ul class="nav nav-tabs nav-justified">
      <li class="<%= 'active' if current_page?(user_path(@user)) %>"><%= link_to user_path(@user) do %>Tweets <% end %></li>
      <li class="<%= 'active' if current_page?(followings_user_path(@user)) %>"><%= link_to followings_user_path(@user) do %>Followings <% end %></li>
      <li class="<%= 'active' if current_page?(followers_user_path(@user)) %>"><%= link_to followers_user_path(@user) do %>Followers <% end %></li>
    </ul>
    <%= render 'users', users: @followings %>
  </div>
</div>

 

app/views/users/followers.html.erb

<div class="row">
  <aside class="col-xs-4">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><%= @user.name %></h3>
      </div>
      <div class="panel-body">
        <img class="media-object img-rounded img-responsive" src="<%= gravatar_url(@user, { size: 500 }) %>" alt="">
      </div>
    </div>
    <%= render 'relationships/follow_button', user: @user %>
  </aside>
  <div class="col-xs-8">
    <ul class="nav nav-tabs nav-justified">
      <li class="<%= 'active' if current_page?(user_path(@user)) %>"><%= link_to user_path(@user) do %>Microposts <span class="badge"><%= @count_microposts %></span><% end %></li>
      <li class="<%= 'active' if current_page?(followings_user_path(@user)) %>"><%= link_to followings_user_path(@user) do %>Followings <span class="badge"><%= @count_followings %></span><% end %></li>
      <li class="<%= 'active' if current_page?(followers_user_path(@user)) %>"><%= link_to followers_user_path(@user) do %>Followers <span class="badge"><%= @count_followers %></span><% end %></li>
    </ul>
    <%= render 'users', users: @followers %>
  </div>
</div>

 

app/views/users/show.html.erb

<div class="row">
  <aside class="col-xs-4">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><%= @user.name %></h3>
      </div>
      <div class="panel-body">
        <img class="media-object img-rounded img-responsive" src="<%= gravatar_url(@user, { size: 500 }) %>" alt="">
      </div>
    </div>
    <%= render 'relationships/follow_button', user: @user %>
  </aside>
  <div class="col-xs-8">
    <ul class="nav nav-tabs nav-justified">
      <li class="<%= 'active' if current_page?(user_path(@user)) %>"><%= link_to user_path(@user) do %>Tweets <% end %></li>
      <li class="<%= 'active' if current_page?(followings_user_path(@user)) %>"><%= link_to followings_user_path(@user) do %>Followings <% end %></li>
      <li class="<%= 'active' if current_page?(followers_user_path(@user)) %>"><%= link_to followers_user_path(@user) do %>Followers <% end %></li> </ul>
    <%= render 'tweets/tweets', tweets: @tweets %>
  </div>
</div>
以上でフォロー機能の実装は終了です。

まとめ

フォロー機能の実装でやったこと
  • 中間テーブルの作成
  • フォローするユーザとフォローされるユーザの関係を構築

これらによって多対多の関係を作り、フォロー機能を実装しました。

多対多の関係は一見すると複雑ですが、分解して見ると1対多の関係を組み合わせたものです。

もし、この多対多の関係性があまりわからなかったという方は【Twitterクローン作成】4.ツイート機能作成でやった1対多の関係をしっかりと理解してからもう一度トライすることをオススメします!

次は最終回、お気に入り機能の実装です。