[Ruby on Rails] フォロー機能を実装しよう

#はじめに
今回はフォロー機能を作成します。railsチュートリアルではform_forを利用していましたが、今回はform_withを利用して作成します。
(題材は自分のポートフォリオです。Userモデルはある前提で進めます。)

###対象読者
railsチュートリアル終わったレベルくらいの人

###作成の流れ
1.relationshipモデルの作成
2.各モデルの関連付け、フォロー機能のメゾット作成
3.対応するコントローラーの作成
4.form_withを利用したフォローボタン作成
5.jsファイルの作成
6.フォロー機能の流れ


###今回のAjaxの流れ
①fomr_withで作成したフォロー/アンフォローボタンを押すとPost/deleteリクエストが送られる。
↓
②対応するコントローラのcreate/destroyアクションを実行
↓
③js.erbファイルをレンダリング
↓
④Ajax対象範囲のフォロー/アンフォローボタンを非同期で切り替え

#1.relationshipモデルの作成
relationshipモデルの中身はこんな感じです。中間テーブルを作成しますが別にフォローしているユーザーの情報を記録するようなテーブルは作りません。実際のフォローする際のテーブルの流れは
Userモデル→Follow_relationship→Userモデルと帰ってくる流れになります。
follower,followingと名前がついていますがどちらもUserのidが入ります。

|カラム|型|
|:--|:--|
|id|integer|
|follower_id|integer|
|following_id|integer|

モデルを作成します。
それぞれのカラムに外部キー制約とindexをこの後つけるのでreferencesを付けて生成します。（referencesをつけると自動で〇〇_idの形にしてくれます。）

```
rails g model Follow_Relationship follower:references following:references
```

#2.各モデルの関連付け、フォロー機能のメゾット作成
作成されたマイグレーションファイルを確認します。

```ruby:db/migrate
class CreateFollowRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :follow_relationships do |t|
      t.references :follower, foreign_key: { to_table: :users }
      t.references :following, foreign_key: { to_table: :users }

      t.timestamps
    end
    add_index :follow_relationships, [:follower_id, :following_id], unique: true
  end
end
```
ここでforegin_key（外部キー）を設定しています。よく外部キーを設定する時に使う「foreign_key: true」としてしまうと存在しないfollowersデーブルを参照してしまうので「to_table: :users」として参照はusersテーブルのidであることを指定してます。
外部キーを設定することによりindexの追加と存在するuserのidのみをdbに保存するようになります。
また、unique: trueとすることにより、同じ組み合わせでデータを保存するのを防ぐようにしているので同じユーザーを２回フォローできなくしています。

各モデルの関連付けはこのような形になります。

```ruby:app/model/follow_relationship.rb
belongs_to :follower, class_name: "User"
belongs_to :following, class_name: "User"

validates :follower_id, presence: true
validates :following_id, presence: true
```


```ruby:app/model/user.rb
has_many :following_relationships,foreign_key: "follower_id", class_name: "FollowRelationship",  dependent: :destroy
has_many :followings, through: :following_relationships
has_many :follower_relationships,foreign_key: "following_id",class_name: "FollowRelationship", dependent: :destroy
has_many :followers, through: :follower_relationships
```
それぞれのオプションの意味は
foreign_key: "follower_id" 　外部キーの名前を直接参照します。
※マイグレーションで設定した物はあくまでもUserのidがこのカラムに入りますと指定しただけなのでフォローしているユーザーを検索等するときはfollowingカラムを参照するよと指定しないといけません。
class_name:関連名（following_relationships）としていますがそのようなテーブルは存在しないので実際に参照するテーブルであるFollowRelationshipを指定してあげます。
dependent: :destroy　Userが削除された時にrelationshipのuseridが削除されるようにしています。

has_many :followings, through: :following_relationships
とすると先ほど指定したfollowing_relationshipsを通してフォローしているユーザーのidを取得できるようになります。

###フォロー関連のメゾットを作成します。

```ruby:app/model/user.rb
#すでにフォロー済みであればture返す
  def following?(other_user)
    self.followings.include?(other_user)
  end
  
  #ユーザーをフォローする
  def follow(other_user)
    self.following_relationships.create(following_id: other_user.id)
  end
  
  #ユーザーのフォローを解除する
  def unfollow(other_user)
    self.following_relationships.find_by(following_id: other_user.id).destroy
  end
```

#3.コントローラーの作成
まずはコントローラーを作成します。

```
rails g controller follow_relationships
```
:
続いてルーディングの設定です。フォロー/フォロワーの一覧ページ用のアクションはUserオブジェクト絡みの機能なのでコントローラー内に記載します。
そのために、resources: userにmemberを使用してルートを追加します。
memberを使用するとURL内にユーザーを識別するidが追加されます。
idは後ほどUserコントローラーの対応するアクションないで必要となるのでmemberでルートを追加しないといけません。（idを追加しないcollectionというメゾットもあります。）

```ruby:confing.routes.rb
resources :users do
      member do
        get :following, :followers
      end
    end
resources :follow_relationships, only: [:create, :destroy]
```

一覧ページ用のアクションはこのようになります。（kaminariのページネーション機能を利用しています。）
※それぞれ対応するビューを作成してください。

```ruby:app/controllers/users.rb
def followings
    @user =User.find(params[:id])
    @users =@user.followings.page(params[:page]).per(5)
    render 'show_followings'
  end
  
  def followers
    @user =User.find(params[:id])
    @users =@user.followers.page(params[:page]).per(5)
    render 'show_followers'
  end
```

follow_relationshipのコントローラーはこんな感じです。
followとunfollowが先ほどUser.rbに記載したメゾットになります。

```ruby:app/controllers/follow_relationships.rb
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end
  
  def destroy
    @user = User.find(params[:follow_relationship][:following_id])
    current_user.unfollow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end
end
```
respond_to do |format|はリクエストの種類によってフォロー/アンフォローした際にレンダリングするビューを指定しています。クライアント側の設定でjsが無効になっている場合にhtml側の処理を書いておかないとエラーが出るので注意してください。
[format.html]ではredirect_backメゾットで直前のページを表示、表示できなければroot_urlに戻しています。※redirect_toを使用してflashを表示するパターンも設定できます。
jsでリクエストがくれば[format.ｊs]を通って、folloｗ_relationshipのcreate.js.erbへ処理が向かいます。format.jsの後にrender先を指定しない場合は自動的にアクションに対応するjsファイルを読み込みます。この場合はこの後記載するcreate.js.erbが該当するファイルになります。

```ruby:app/controllers/follow_relationships.rb
※flashバージョン
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user, flash: {success: 'フォローしました！'} }
      format.js
    end
  end
```

#4.フォローボタン作成
コントローラーのアクションができたのフォローボタンを作成します。その前にフォロー一覧とフォロワー一覧ページを作成しておきます。今回はUserのshowページにリンクを用意してます。※フォロー機能には直接の関係はありません。

```ruby:app/views/users/show_html
<%= link_to "フォロー(#{@user.followings.count})", followings_user_path(@user), class: "nav-link" %>
  
<%= link_to "フォロワー(#{@user.followers.count})", followers_user_path(@user), class: "nav-link" %>
```
フォローボタンも同じくshowページに配置しますがコードがグチャグチャにならないようにボタンのフォームはrenderしてます。

```ruby:app/views/users/show_html
<% if logged_in? && @user != current_user%>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render "unfollow" %>
    <% else %>
      <%= render "follow" %>
    <% end %>
  </div>
<% end %>
```
if logged_in? && @user != current_user
→ログイン済みであることと、このshowページが自分以外のユーザーのページであることを確認しています。&&はAND条件を表すので全ての条件を満たさないとボタンが
表示されません。!=は等しくないときtureとなります。
if current_user.following?(@user)
→ログイン中のユーザーがすでにフォロー済みであればunfollowをまだフォローしていなければfollowボタンを表示するようにしています。
次は実際のボタンの中身です。

```ruby:app/views/users/follow_html
<%= form_with(model: current_user.following_relationships.build) do |f| %>
  <%= f.hidden_field :following_id, value: @user.id %>
  <%= f.submit "フォローする", class: "btn btn-outline-secondary" %>
<% end %>
```

from_withはデフォルトでAjax通信をするようになっています。
modelにはモデルクラスのインスタンス(@userとか)を渡す必要があるので、空のfollow_relationshipインスタンスを作成してpostリクエストを動作させ、follow_relationshipのコントローラーのcreateアクションに渡します。（userを作成するときにコントローラーのアクションでUser.newをする意味と同じです。多分）
f.hidden_fieldでfollowing_idにユーザーidを入れるようにしています。
※クライアント側には見えずに、ボタンを押した時にパラメーターに入ります。

```ruby:app/views/users/unfollow_html
<%= form_with(model: current_user.following_relationships.find_by(following_id: @user.id),method: :delete) do |f| %>
  <%= f.hidden_field :following_id %>  
  <%= f.submit "フォロー", class: "btn btn-outline-secondary" %>
<% end %>
```
フォロー解除ボタンはフォローボタンとは違ってdeleteリクエストを指定してdestroyアクションを動作させています。

#5.jsファイルの作成
フォームからのPostリクエストをcreateアクションで処理したあとはcreate.js.erbにたどり着きます。そこでAjaxでレンダリングする部分（今回の場合はボタン）を指定します。create.js.erbにたどり着くのはcreateアクション後なのでレンダリングするのはフォロー解除ボタンになります。

```ruby:app/views/follow_relationships/create.js.erb
$("#follow_form").html("<%= j(render("users/unfollow")) %>");
```

```ruby:app/views/follow_relationships/destroy.js.erb
$("#follow_form").html("<%= j(render("users/follow")) %>");
```
それぞれ対応するフォームを読み込みます。follow_formはshowページに記載したcssのidです。
以上でフォロー機能が動作するようになりました。
最後にフォロー機能の流れだけまとめで記載しておきます。

6.フォロー機能の流れ
実際にフォローボタンを押すとこのような動きをサーバーのログから確認できます。
※流れを追うだけなので一部省略してます。

```ruby
Started POST "/follow_relationships"→フォローボタンが押される
Processing by FollowRelationshipsController#create as JS
Parameters:"follow_relationship"=>{"following_id"=>"3"}→パラメータにfollow_relationshipをキーとしたフォロー対象のユーザーidが入ります。
コントローラーのアクション完了！
Rendering follow_relationships/create.js.erb→アクション後にjsのファイル読み込み。
Rendered users/_unfollow.html.erb (2.1ms)
Rendered follow_relationships/create.js.erb (3.2ms)
```

以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。