【Rails6】友達登録機能（ユーザーフォロー機能）の実装方法

※2022年から技術系の記事は[個人ブログ](https://yamaday0u.net/category/programmer/)に投稿しております。ぜひこちらもご覧ください→[yamaday0u Blog](https://yamaday0u.net/category/programmer/)

Railsで友達登録機能を実装する方法を紹介します。
※あくまで業界未経験からのエンジニア転職を目指しているぼくが実際にオリジナルアプリに実装した1つの例です。

**目次**

- 前提条件
- 友達登録機能の仕様
- 動作イメージ
- 実装手順1：Modelの作成
    - Relationshipsテーブルの作成
    - アソシエーションとバリデーションの定義
    - メソッドの定義
- 実装手順2：コントローラーの作成
- 実装手順3：ルーティングを設定
- 実装手順4：Viewの作成

## 前提条件

- Rails 6.0.3.5
- deviseなどでユーザー管理機能を実装済み

## 友達登録機能の仕様
TwitterやInstagramのように一方的にフォローするようなものではなく、Facebookのようにお互いにフォローし合うことで友達としてつながるようにします。

## 動作イメージ
リクエストボタンをクリックすることで相手をフォローしている状態になります。
[![Image from Gyazo](https://i.gyazo.com/16ed56969e13a37670f26749520d4b99.gif)](https://gyazo.com/16ed56969e13a37670f26749520d4b99)
相手からフォローされている状態で、相手をフォローすることで友達（GIFイメージのアプリでは"Mate"と呼んでいます）になります。
[![Image from Gyazo](https://i.gyazo.com/b21d9b7f72d45ac53d5e471a68553058.gif)](https://gyazo.com/b21d9b7f72d45ac53d5e471a68553058)

## 実装手順1：Modelの作成
### Relationshipsテーブルの作成

```zsh:ターミナル
$ rails g model relationship
```
生成されたマイグレーションファイルに以下のように記述します。

```db/migrate/2021XXXXXXXXX_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[6.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id # フォローしている側のユーザー (active relationship)
      t.integer :followed_id # フォローされている側のユーザー(passive relationship)
      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    # 保存するデータの組み合わせが一意になるよう設定
    add_index :relationships, [:follower_id, :followed_id], unique: true
```

### アソシエーションとバリデーションの定義
```app/models/user.rb
class User < ApplicationRecord
  # アソシエーションの定義
  # フォローしている側のユーザー (active relationship)
  has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
  has_many :followings, through: :active_relationships, source: :followed

  # フォローされている側のユーザー(passive relationship)
  has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
  has_many :followers, through: :passive_relationships, source: :follower
```
##### has_manyの補足
`:active_relationships`や`:passive_relationships`関連モデルを指定します。
`class_name:`関連名と参照元のクラス名を異なるものにしたい場合に指定します。
`foreign_key:`参照元のテーブルに定義されている外部キーの名前を指定します。
`dependent:`値を:destroyにすると、参照先が削除される場合に参照元もDBから削除します。

```app/models/relationship.rb
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
  # follower_idとfollowed_idの組み合わせが一意になるようバリデーション
  validates :follower_id, uniqueness: { scope: :followed_id }
end
```
### メソッドの定義
フォロー状態などを確認するメソッドを定義します。

```app/models/user.rb
class User < ApplicationRecord
  # アソシエーションの定義（省略）

  # メソッドの定義
  # ユーザーをフォロー
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # ユーザーをアンフォロー
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 相手をフォローしていればtrueを返す
  def following?(other_user)
    active_relationships.find_by(followed_id: other_user.id)
  end

  # 友達（互いにフォローしている）をデータベースから取得
  def matchers
    followings & followers
  end

  # 相手と友達になっていればtrueを返す
  def matchers?(other_user)
    active_relationships.find_by(followed_id: other_user.id) && passive_relationships.find_by(follower_id: other_user.id)
  end

  # 自分はフォローしていない&相手からフォローされていればtrueを返す
  def follow_request?(user, other_user)
    !user.matchers?(other_user) && other_user.following?(user)
  end
end
```

## 実装手順2：コントローラーの作成
usersコントローラーはユーザー一覧ページを表示するためのindexアクションと、ユーザー詳細ページを表示するためのshowアクションを定義します。

```app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @mates = current_user.matchers
    @users = User.all
  end

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

次にrelationshipsコントローラーを作成します。

```zsh:ターミナル
rails g controller relationships
```
作成したrelationshipsコントローラーに以下のように記述します。

```app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  def create
    Relationship.create(follower_id: current_user.id, followed_id: params[:user_id])
    redirect_to user_path(params[:user_id]), notice: "Requested successfully!"
  end

  def destroy
    other_user = User.find(params[:user_id])
    current_user.unfollow(other_user)
    redirect_to user_path(params[:user_id]), notice: "Canceld request"
  end
end
```
## 実装手順3：ルーティングを設定
relationshipsコントローラーでparams[:user_id]を利用するアクションを定義しているので、`relationships_controller`のルーティングは`users_controller`のルーティングの中にネストします。

```config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:index, :show] do
    resources :relationships, only: [:create, :destroy]
  end
end
```
## 実装手順4：Viewの作成
- ユーザー一覧ページ

```erb:app/views/users/index.html.erb
<div class="item-list">
  
  <div class="item-list-header">
    <p class="bold">Mate list</p>
  </div>

  <div class="item-list-content">
    <ul>
      <% @mates.each do |user| %>
        <%# 友達（互いにフォローしている）を一覧表示 %>
        <div class="each-item">
          <li>
            <div class="each-item-content">

              <div class="item-content-left">
                <p>Mate<br>image</p>
              </div>

              <div class="item-content-right">
                <p><%= link_to user.name, user_path(user.id) %></p>
                <p><%= user.identity %></p>
              </div>

            </div>
          </li>
        </div>
      <% end %>
    </ul>
  </div><%# End of item-list-content %>

  <div class="item-list-header">
    <p class="bold">User list</p>
  </div>

  <div class="item-list-content">
    <ul>
      <% @users.each do |user| %>
        <%# 自分以外のユーザーを一覧表示 %>
        <% unless current_user.id == user.id %>
          <div class="each-item">

            <li>
              <div class="each-item-content">
                <div class="item-content-left">
                  <p>User<br>image</p>
                </div>
                <div class="item-content-right">
                  <p><%= link_to user.name, user_path(user.id) %></p>
                  <p><%= user.identity %></p>
                  <% if user.follow_request?(current_user, user) %>
                    <p><i class="fas fa-star"></i></p>
                  <% end %>
                </div>
              </div>
            </li>

          </div>
        <% end %>
      <% end %>
    </ul>
  </div><%# End of item-list-content %>
</div>
```
**Viewイメージ**
上記コードにない記述もしているViewです。
[![Image from Gyazo](https://i.gyazo.com/80359a6088b7a1e4cb2e8d16f0e93a16.png)](https://gyazo.com/80359a6088b7a1e4cb2e8d16f0e93a16)

- ユーザー詳細ページ

```erb:app/views/users/show.html.erb
<div class="form-wrap">
  <div class="form-group">
    <div class="schedule-item">
      <div class="schedule-item-header text-left">
        <p class="bold">User name</p>
      </div>
      <div class="schedule-item-body text-left">
        <p><%= @user.name %></p>
      </div>
    </div>
  </div>

  <div class="form-group">
    <div class="schedule-item">
      <div class="schedule-item-header text-left">
        <p class="bold">Identity</p>
      </div>
      <div class="schedule-item-body text-left">
        <p><%= @user.identity %></p>
      </div>
    </div>
  </div>

  <% if @user.follow_request?(current_user, @user) %>
    <div class="form-group">
      <div class="schedule-item">
        <div class="schedule-item-header text-left">
          <p class="bold"><i class="fas fa-star"></i>You have follow request<i class="fas fa-star"></i></p>
        </div>
        <div class="schedule-item-body text-left">
          <p><%= @user.name %> requests you to be mate</p>
        </div>
      </div>
    </div>
  <% end %>

  <% if current_user.matchers?(@user) %>
    <div class="form-group">
      <div class="schedule-item">
        <div class="schedule-item-header text-left">
          <p class="bold"><%= @user.name %> is your mate</p>
        </div>
      </div>
    </div>
  <% end %>

  <% if current_user.id == @user.id %>
    
  <% else %>
    <% unless current_user.following?(@user) %>
      <div class="actions">
        <%= link_to 'Request to be mate', user_relationships_path(@user.id), method: :post, class:"sign-up-btn" %>
      </div>
    <% else %>
      <div class="actions">
        <%= link_to 'Cancel to your request', user_relationship_path(@user.id), method: :delete, class:"sign-up-btn" %>
      </div>
    <% end %>
  <% end %>

  <% if current_user.id == @user.id %>
    <div class="actions">
      <%= link_to 'Edit profile', edit_user_registration_path, class:"sign-up-btn" %>
    </div>
  <% end %>
</div>
```

**Viewイメージ**
上記コードにない記述もしているViewです。
[![Image from Gyazo](https://i.gyazo.com/a011b8c55830d78e87949e0b5f0e43a1.png)](https://gyazo.com/a011b8c55830d78e87949e0b5f0e43a1)

以上、みなさんのアプリ開発の参考になれば幸いです。
## 参考資料
- Ruby on Railsチュートリアル
    - [第12章ユーザーをフォローする](https://railstutorial.jp/chapters/following_users?version=4.2)

- Qiita
    - [[HowTo]マッチングアプリ的な友達機能をお手軽に実装してみる。](https://qiita.com/Tatsu88/items/b186df1afa3dbd2a37c4)