【Rails】フォロー機能の実装（非同期）

# 目的
Railsで作成したアプリに非同期でのフォロー機能を導入する。

# 開発環境
macOS: *Big Sur*
Rubyバージョン: *2.6.5*
Railsバージョン: *6.0.0*

# 前提

- アプリ`test-app`が作成されている。
[【Rails】簡単な投稿アプリの作成](https://qiita.com/oak1331/items/18a4f3a3386aab0fa354)
- `devise`が導入されている。
[【Rails】ユーザー管理機能(devise)の導入](https://qiita.com/oak1331/items/0759bd297e9fd3b81ed2)
- `jQuery`が導入されている。
[【Rails】iQueryの導入](https://qiita.com/oak1331/items/68d301cf0c448bb29356)

# 手順

1. [はじめに](#はじめに)
1. [ルーティングの設定](#ルーティングの設定)
1. [Relationshipモデルの作成](#relationshipモデルの作成)
1. [アソシエーション設定](#アソシエーションの設定)
1. [バリデーションの設定](#バリデーションの設定)
1. [メソッドの定義](#メソッドの定義)
1. [relationshipsコントローラーの作成](#relationshipsコントローラーの作成)
1. [アクションの定義](#アクションの定義)
1. [フォローボタンの表示](#フォローボタンの表示)
1. [jsファイルの作成](#jsファイルの作成)

# はじめに

今回はフォロー機能を実装していきます。
フォロー機能に必要なテーブルは中間テーブルである`relationships`ですが、
`フォローするuser`と`フォローされるuser`は同じなのでアソシエーションが通常の「多対多」とは異なります。

# ルーティングの設定

それでは早速始めていきます！
まずはルーティングの設定です。

```ruby:config/routes.rb
Rails.application.routes.draw do
#省略
  resources :relationships, only: [:create, :destroy]
end
```

機能としては`フォロー`と`フォロー解除`のみなので、`create`と`destroy`のみを設定します。

# Relationshipモデルの作成

```:ターミナル
% rails g model relationship
```

続いてテーブルを作成していきます。

```ruby:db/migrate/20XXXXXXXXXXXX_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.references :user, foreign_key: true
      t.references :follower, foreign_key: { to_table: :users }
      t.timestamps

      t.index [:user_id, :follower_id], unique: true
    end
  end
end
```

`t.references :follower, foreign_key: { to_table: :users }`の記述ですが、
`follower_id`は`usersテーブル`を参照しなければならないため`{ to_table: :users }`という記述でテーブルを指定しています。

`t.index [:user_id, :follower_id], unique: true`は`user_id`と`follower_id`のペアが重複しないための記述です。
ある1人のユーザーが、あるユーザーを何度もフォローできないようにしています。

```:ターミナル
% rails db:migrate
```

マイグレーションも忘れずに！

# アソシエーションの設定

```ruby:app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :user
  belongs_to :follower, class_name: 'User'
end
```

`class_name: 'User'`の記述で、`Userモデル`を参照するように指示しています。

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follower
  has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy
  has_many :followers, through: :passive_relationships, source: :user
end
```

`followings, through: :relationships, source: :follower`の記述は、
`relationshipsテーブル`の`follower_id`を参考にして、`followingsモデル`にアクセスするという意味です。
結果として、`user.followings`と記述するだけで、`フォローしているUser`を取得できます。

`has_many :followers, through: :passive_relationships, source: :user`の記述も同様で、`user.followers`と記述するだけでｍ`自分をフォローしているUser`を取得できます。


# バリデーションの設定

```ruby:app/models/relationship.rb
class Relationship < ApplicationRecord
#省略
  with_options presence: true do
    validates :user_id
    validates :follower_id
  end
end
```

`user_id`と`follow_id`は必須というバリデーションを追加します。

# メソッドの定義

後ほど使用するメソッドを定義します。

```ruby:app/models/user.rb
class User < ApplicationRecord
#省略
  def following?(user)
    followings.include?(user)
  end

  def follow(user_id)
    relationships.create(follower: user_id)
  end

  def unfollow(relationship_id)
    relationships.find(relationship_id).destroy!
  end
end
```

`following?`は既にフォローしているか？という記述、`follow`はフォローを保存する記述、`unfollow`はフォロー解除する記述です。

# relationshipsコントローラーの作成

```:ターミナル
% rails g controller relationships
```

# アクションの定義

`フォロー`と`フォロー解除`機能のため、`createアクション`と`destroyアクション`を定義します。

```ruby:app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :authenticate_user!

  def create
    @user = User.find(params[:follower])
    current_user.follow(@user)
  end

  def destroy
    @user = current_user.relationships.find(params[:id]).follower
    current_user.unfollow(params[:id])
  end
end
```

先ほど定義したメソッドはここで使用します。

# フォローボタンの表示

フォローボタンの表示は部分テンプレートとして切り分け、ユーザー詳細ページに部分テンプレートを呼び出す記述をします。

```html:app/views/users/show.html.erb
<div id = "relationship_<%= @user.id %>">
  <%= render partial: "relationships/relationship", locals: { user: @user } %>
</div>
```

部分テンプレートとして切り分けたページです。

```html:app/views/users/_relationship.html.erb
<% if user_signed_in? && current_user.id != @user.id %>
  <% if current_user.following?(@user) %>
    <%= button_to "フォロー解除", relationship_path(current_user.relationships.find_by(follower: @user)), method: :delete, remote: true %>
  <% else %>
    <%= button_to "フォロー", relationships_path(follower: @user), method: :post, remote: true %>
  <% end %>
<% end %>
```

`サインインしているか？`と`既にフォローしていないか？`という条件で`フォローボタン`と`フォロー解除ボタン`を条件分岐しています。

# jsファイルの作成

フォロー機能を非同期で行うためのjsファイルを作成します。

```javascript:app/views/relationships/create.js.erb
$('#relationship_<%= @user.id %>').html("<%= j(render partial: 'relationships/relationship', locals: { user: @user }) %>");
```

```javascript:app/views/relationships/destroy.js.erb
$('#relationship_<%= @user.id %>').html("<%= j(render partial: 'relationships/relationship', locals: { user: @user }) %>");
```

これで非同期でのフォロー機能が実装できたと思います。
確認してみてください！

# 最後に

以上でフォロー機能の実装は完了です。
CSSは実装していないため、簡素な見た目となっています。各自必要に応じて編集してください。
では。