TojiTech No Programming, No Life.

Railsで簡単に1テーブルでフォロー機能を実装する

更新

2022/08/16 : 読者さんからのご指摘により、ルーティングにdestroyの記述忘れを修正しました。

2024/04/22: 読者さんからのご指摘により、一部変更しています。(Show関連)

背景

  • 色々記事を見たけど、1テーブルで実装している記事が少ない
  • 1テーブルでシンプルな方が良い
  • ユーザーのフォロー情報が見られるようにしたい

サンプルリポジトリ

こちらから、実装済みソースコードが見られます。

注意

サンプルのリポジトリは、Dockerで起動することを前提に作成されています。
Dockerを使用しない場合は、appフォルダ以下で通常のrailsの起動処理を行ってください。

なお、記事ではDockerを使用せず導入する手順で記載していますのでご注意ください。

Rails6で記述していますが、Rails5でもロジックは同様に動作すると思います。

ソースコードのコメントについて

基本的にコメントを多めに記載しています。こちらを参考に実装して、ロジックの理解を深めてください。

実装

Deviseの導入は、今回は紹介しません。すでにUserを作成している前提で進めます

まずは、モデル、コントローラーなど作成します。

rails g model follow
rails g controller homes
rails g controller follows

ルーティング

Rails.application.routes.draw do
  root to: 'homes#index'
  devise_for :users
  resources :follows, only: %w(index show create destroy)
end

マイグレーションファイル

class CreateFollows < ActiveRecord::Migration[6.1]
  def change

    # TODO : SQLiteでは、デフォルトでは外部キーを使用できませんのでご注意ください

    create_table :follows do |t|
      # foreign_keyで外部キー制約
      #   意図しないユーザーIDが入らないようにしている
      #     to_tableキーの値で実際のテーブル名を指定
      # TODO : フォロー主のIDが入る
      t.references :follow, foreign_key: { to_table: :users }

      # foreign_keyで外部キー制約
      #   意図しないユーザーIDが入らないようにしている
      # TODO : フォロー先のIDが入る
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

モデル

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  # アソシエーション
  has_many :follows, dependent: :destroy

  # フォローユーザーの検索
  #   フォロー済みの場合、ユーザーデータを返す
  #   フォローしていない場合は、nilを返す
  def following?(current_user, follow_id)
    # Followから自分と対象のユーザーがANDになっているレコードを検索
    Follow.find_by(user_id: follow_id, follow_id: current_user.id)
  end

  # フォローしている一覧取得
  def followed(user)
    Follow.where(follow_id: user.id)
  end

  # フォローされている一覧取得
  def follower(current_user)
    Follow.where(user_id: current_user.id)
  end
end
class Follow < ApplicationRecord
  # アソシエーション
  belongs_to :user
end

コントローラー

class HomesController < ApplicationController
  def index; end
end
class FollowsController < ApplicationController
  before_action :authenticate_user!

  def index
    @users = User.all # ユーザー一覧取得
  end

  def show
    @user = User.find(params[:id]) # ユーザー情報取得
    @follows = @user.followed(@user) # フォローしているユーザー一覧取得
    @followers = User.where(id: @user.follower(@user).pluck(:follow_id)) # フォローされているユーザー一覧取得
  end

  def create
    # フォローレコードが1件もない場合は、レコードを作る
    Follow.find_or_create_by(user_id: params[:follow_id], follow_id: current_user.id)
    redirect_to follows_path
  end

  def destroy
    # フォローレコードを探し、フォローレコードを削除する
    #   TODO : 勝手に他のユーザーのフォローを外されないようにparams[:id]で対象のユーザーIDを受け取る
    Follow.find_by(user_id: params[:id], follow_id: current_user.id).delete
    redirect_to follows_path
  end
end

ビュー

<h1>Homes#index</h1>
<p>Find me in app/views/homes/index.html.erb</p>

<ul>
  <li>
    <%= link_to '新規登録', new_user_registration_path %>
  </li>
  <li>
    <%= link_to 'ログイン', new_user_session_path %>
  </li>
  <%# ログイン済みならログアウト・ユーザー一覧を表示 %>
  <% if user_signed_in? %>
    <li>
      <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
    </li>
    <li>
      <%= link_to 'ユーザー一覧', follows_path %>
    </li>
  <% end %>
</ul>
<h1>Follows#index</h1>
<p>Find me in app/views/follows/index.html.erb</p>

<% @users.each do |user| %>
  <div>
    <%# ユーザーのメールアドレスを表示 %>
    <%= link_to user.email, follow_path(user) %>

    <%# 自分の場合は、フォロー対象外 %>
    <% unless user == current_user %>
      <%# フォロー済みかどうか確認する %>
      <%# following?は、Userモデルで定義しており、見つからない場合はnilが戻る %>
      <% if user.following?(current_user, user.id).nil? %>
        <%# フォローする : follows#create %>
        <%# TODO: current_userをここで渡すと勝手に追加されるため、フォロー対象のユーザーの情報のみ渡す %>
        <%= link_to 'Follow', follows_path(follow_id: user.id), method: :post %>
      <% else %>
        <%# フォローを外す : follows#destroy %>
        <%# TODO: current_userをここで渡すと勝手に削除されるため、フォロー対象のユーザーをidで渡す %>
        <%= link_to 'UnFollow', follow_path(user.id), method: :delete %>
      <% end %>
    <% end %>
  </div>
<% end %>
<h1>Follows#show</h1>
<p>Find me in app/views/follows/show.html.erb</p>

<h2>
  <%= @user.email %>
</h2>

<%# フォロー一覧表示 %>
<h3>Follows --></h3>
<% @follows.each do |follower| %>
  <div>
    <%= follower.user.email %>
  </div>
<% end %>


<%# フォローワー一覧表示 %>
<h3>Followers --></h3>
<% @followers.each do |follower| %>
  <div>
    <%= follower.email %>
  </div>
<% end %>

最後に

以上で、実装は完了のはずです。うまく動かない場合は、サンプルリポジトリを確認してください。

ぜひ、実装の参考にしてください!

Profile

Yuki Tojima

RubyやPhp、JavaScriptまわりのことを徒然と記録に残す技術ブログです。

至らぬところもあると思いますが、見守っていただけると幸いです。

記事のリクエストや、間違いなどありましたら X (旧 Twitter) のDMなどでお気軽にご連絡ください。

ytojima @TojiTech
プロフィール画像