Rails　フォロー機能の実装

# はじめに
今回はXクローンの実装にてフォロー機能を実装したので、自分自身の備忘録として、また今後フォロー機能を実装する方のお役に立てればと思いこちらの記事を書くことにしました。

# フォロー機能の作成
要件として、ユーザーをフォローしたり、フォロー解除できる機能を実装する。

※ 前提として、ユーザー登録がすでに実装されているものとします。

# フォロー機能実装に必要なファイル
#### 作成するモデル・テーブル
- relationsipモデル
- relationsipテーブル
#### 作成するコントローラー
- relationsipsコントローラー

# フォロー機能について
- フォロー機能では、`フォローする人`と`フォローされる人`がいる
- `フォローする人`と`フォローされる人`はどちらも、`"User"`である
- Userは複数のUserをフォローすることができる
- Userは複数のUserにフォローされることができる

この関係性はN:Nの関係性なので中間テーブルが必要!

![IMG_F513C6EFE3E4-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/d858dfcf-869d-7682-213e-6620c21a3c79.jpeg)

#### ER図で表すと...

フォローする側もフォローされる側もどちらもUserなので、今回は区別するために
- フォローするユーザー　`following`
- フォローされたユーザー　`follower`
というテーブルを使用します。
（実際にはfollowingsテーブルとfollowersテーブルは存在せずuserテーブルになります）
そして、誰が誰をフォローしているかと言う情報を格納するためにrelationsipsテーブルを作成します。![スクリーンショット 2024-03-04 23.41.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/16cd690a-69da-63ef-eab0-f212fe088043.png)

カラムの
`following_id : フォローしたユーザー`
`follower_id : フォローされたユーザー`
はややこしいので、特に注意して実装していきたいと思います。


# モデルの作成
ここまで見て来たフォロー機能についてを参考にしてモデルの作成をしていきたいと思います。
followingsテーブルと、followersテーブルは実際には、usersテーブルなので、今回作成するのは、relationsipsテーブルだけになります。

```
rails g model Relationsip
```

```ruby: create_relationsips.rb
class CreateRelationships < ActiveRecord::Migration[7.0]
  def change
    create_table :relationships do |t|
      t.references :following, foreign_key: { to_table: :users }
      t.references :follower, foreign_key: { to_table: :users }
      
      t.timestamps
    end
  end
end
```

```
rails db:migrate
```

# 関連付けの設定
userモデルと、relationshipモデルの関連付けを記述していきます。

```ruby: relationsip.rb
class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end
```

本当であれば、`belongs_to :user`とするところだけど、どっちがどっちのuserかわからなくなるので、
ここでは、フォローする側とされる側のfollowingとfollowerに分けて記述しています。
また、class_nameがないと、followingsテーブルと、followersテーブルを探しに行ってしまうので、`class_name: 'User'`として、usersテーブルからデータを持って来てもらうように設定しています。

#### class_nameとは
関連名と参照先のクラス名を異なるものに置き換えることができるオプション。
モデル名を直接指定できる。


```ruby: user.rb
class User < ApplicationRecord
  # フォローの関係,フォローした
  has_many :relationships, foreign_key: :following_id, dependent: :destroy
  # 一覧画面で使用する（あるユーザーがフォローしている人全員をとってくる）
  has_many :followings, through: :relationships, source: :follower
  
  # フォロワーの関係、フォローされた
  has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: :follower_id, dependent: :destroy
  # 一覧画面で使用する（あるユーザーをフォローしている人全員をとってくる）
  has_many :followers, through: :reverse_of_relationships, source: :following
end
```

まずは、フォローをした、されたの関係から見ていきます。
ここでは、relationshipsとreverse_of_relationshipsと設定しました。先ほどと同じ考え方で、わかりにくいため名前を変更してつけているだけです。
class_name: "Relationship"でRelationshipテーブルを参照します。
`foreign_key(外侮キー)`で参照するカラムを指定しています。

次に、フォロー・フォロワーの一覧画面で、user.followersという記述でフォロワーを表示したいので、throughでスルーするテーブル、sourceで参照するカラムを指定。

上の例では、relationshipsテーブルからfollowing_idのデータをreverse_of_relationshipsテーブルからfollower_idのデータを参照します。


# メソッドの作成
```ruby: user.rb
# あるユーザが引数で渡されたuserにフォローされているか調べるメソッド
  def is_followed_by?(user)
    reverse_of_relationships.find_by(following_id: user.id).present?
  end
```

# ルーティングの設定
userとrelationshipは関連づけられているためuserのidが必要になる。
そのため、relationsipsをネストする。

```ruby: routes.rb
resources :users, only: %i[show edit update] do
    member do
      get :followings, :followers
    end
    resource :relationships, only: %i[create destroy]
  end
```
![スクリーンショット 2024-03-06 17.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/57ab9d71-48b3-d3da-68e3-847d1ff8c2d4.png)

上記のルーティングの設定ができました。

userのidを含んだ形で、個々のリソースに対してfollowingとfollowersアクションの設定ができました。


### menberとは？
resourecesで作成されるルートに、決められたルート以外のルートを追加するための処理のこと。
resources以外の自分で定義したアクションへのルーティングを設定する場合に使用する。

同じような場面で使用するものとして、`collection`もある。こちらの使い分けは、
- member: アクションにidが渡されるため、**idを使用した特定のデータに対するアクションの場合**
- collection: idを渡さない。**idの必要ない全体のデータに対するアクションの場合**

今回は、following,followerアクションが必要かつ、それにはidが含まれている必要があるから、`member`を使用しました。


# controllerの作成
```
rails g controller relationships
```

```ruby: relationsips_controller
class RelationshipsController < ApplicationController
  before_action :authenticate_user!

  def create
    following = current_user.relationships.build(follower_id: params[:user_id])
    following.save
    redirect_to request.referer
  end

  def destroy
    following = current_user.relationships.find_by(follower_id: params[:user_id])
    following.destroy
    redirect_to request.referer
  end
end
```

```ruby: users_contoroller
  def followings
    # フォローしている人の一覧
    @user = User.find(params[:id])
    @users = @user.followings
  end

  def followers
    # フォローされている人の一覧
    @user = User.find(params[:id])
    @users = @user.followers
  end
```

# viewの設定
今回はこちらの画像のように、自分自身の場合は、`プロフィール変更`ボタン、自分ではないユーザーの場合は、`フォローする`ボタンまたは`フォロー解除する`ボタンを設置しました。

また、フォローしている人数と、フォローされている人数の表示もしています。

![スクリーンショット 2024-03-06 18.21.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/7417a938-6fba-4155-dd9b-4a06a5f44df0.png)
![スクリーンショット 2024-03-06 18.22.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/6a3cc0e2-7f90-6f5c-8e42-570f5f239c55.png)
![スクリーンショット 2024-03-06 18.22.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/7a104eda-1a49-730e-be90-eb20197e8505.png)


```ruby: users/show.html.slim
:
省略
:

#　もし、user.idがcurrent_userと一致するならプロフィール編集ボタンを表示させる
- if @user.id == current_user.id 
  = link_to "プロフィールを編集", edit_user_path(@user), class: "edit"

  .text-end.mt-4.me-4
    # もし、user.idがcurrent_userと一致しないならフォローボタンかフォロー解除ボタンを表示させる
    - if current_user.id != @user.id
      - if @user.is_followed_by?(current_user)
        = link_to user_relationships_path(@user), data: { "turbo-method": :delete }, class: "btn btn-outline-dark" do
          = @user.name
          | をフォロー解除
      - else
        = link_to user_relationships_path(@user), data: { "turbo-method": :post }, class: "btn btn btn-dark" do
          = @user.name
          | をフォロー

:
省略
:
.d-flex.relationsip
  .follow 
    p
      span 
        = link_to @user.followings.count, followings_user_path(@user), class: "follow-link-count"
      | Following
  .follow  
    p
      span 
        = link_to @user.followers.count, followers_user_path(@user), class: "follow-link-count"
      | Followers
```


フォローしている人の人数または、フォローされている人の人数の部分をクリックすると、フォロー一覧と、フォロワー一覧に遷移するようになっているため、そのページの作成もしておく。



![スクリーンショット 2024-03-06 21.35.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/2f826570-1342-da87-af02-b2f1a3be01cb.png)



![スクリーンショット 2024-03-06 21.35.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/6a5c6f18-4f5a-40a8-1adb-c4984aa0a635.png)



```ruby:users/following.html.slim
.col-md-12.border_right.border_left
  .d-flex
    div
      = link_to root_path do 
        i.fa-solid.fa-arrow-left.arrow-left
    .profile_top_name 
      h3 
        = @user.name
        | さんがフォローしている人

      table.table.text-center
        thead
          tr
            th
              | Users
            th

        tbody
          - @users.each do |user|
            tr
              td 
                - if user.icon.attached?
                  = link_to user_path(user) do
                    = image_tag user.icon, class: "icon_img"
                - else 
                  = link_to user_path(user) do
                    img.home_img[src="/assets/icon.png"]
              td
              td 
                = link_to user_path(user), class: "text_deco_no" do
                  = user.name
              td
              td
                = link_to user_path(user), class: "text_deco_no" do
                  | @
                  = user.username
              td

                - if user != current_user
                  - if user.is_followed_by?(current_user)
                    =link_to user_relationships_path(user), data: { "turbo-method": :delete } do
                      button.btn.btn.btn-danger[type="button"]
                        | フォロー解除
                  - else
                    =link_to user_relationships_path(user), data: { "turbo-method": :post } do
                      button.btn.btn.btn-primary[type="button"]
                        | フォロー
```

```ruby:users/follower.html.slim
.col-md-12.border_right.border_left
  .d-flex
    div
      = link_to root_path do 
        i.fa-solid.fa-arrow-left.arrow-left
    .profile_top_name 
      h3 
        = @user.name
        | さんをフォローしている人
      
      table.table.text-center
        thead
          tr
            th
              | Users
            th
        tbody
          - @users.each do |user|
            tr
              td 
                - if user.icon.attached?
                  = link_to user_path(user) do
                    = image_tag user.icon, class: "icon_img"
                - else 
                  = link_to user_path(user) do
                    img.home_img[src="/assets/icon.png"]
              td
              td 
                = link_to user_path(user), class: "text_deco_no" do
                  = user.name
              td
              td
                = link_to user_path(user), class: "text_deco_no" do
                  | @
                  = user.username
              td
                - if user != current_user
                  - if user.is_followed_by?(current_user)
                    =link_to user_relationships_path(user), data: { "turbo-method": :delete } do
                      button.btn.btn.btn-danger[type="button"]
                        | フォロー解除
                  - else
                    =link_to user_relationships_path(user), data: { "turbo-method": :post } do
                      button.btn.btn.btn-primary[type="button"]
                        | フォロー
```


これで、フォローしている人と、フォローされている人の一覧を表示することができました。


# 最後に
今回初めてフォロー機能を実装して、アソシエーションの部分が少し難しく、まだ完全に理解しきれていない部分になっていると思いました。なんとなくですが、いろんな記事や動画を参考にしながら今回実装を進めることができたので良かったです。


# 参考ページ
https://youtu.be/gATeEnr8gh4?si=1V57NR0krnjmy9QT

https://youtu.be/iAIRNEsA-7g?si=YdWr6_bL_IwCWKBP

https://qiita.com/nakachan1994/items/e6107fe3003f6515e385