[Rails]フォロー、フォロワー機能のアソシエーションについて

多対多のアソシエーションの解説をフォローフォロワー機能を例に解説していきます。

中間テーブル、モデルの設定は難しいですが、できるだけ理解できるように解説していきます。

# 開発環境
> ruby 2.6.3
> rails 6.1.4

# 前提
- Userモデル同士でフォローフォロワー機能を実装する
- Userモデルを実装済みである
- 1:多のアソシエーションは理解っている前提で話を進めます

# 作成イメージ
初めに、今回作成するフォローフォロワー機能について、イメージを掴んでおきましょう！
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967037/c52d62f3-bf96-e454-b93b-e75c3c2f02dd.png)
この図にある、Followerはフォローする人、Followedはフォローされる人を指しています。
フォローフォロワー機能の複雑なところは、図のように1:nのアソシエーションが同一モデルに2つ存在しているという点にあります。1つずつ考えるとそこまで難しくないので、それぞれのアソシエーションを分けて、解説していこうと思います。

# 実装
## DB構築
```shell
rails g model Relationship follower:references followed:references
```
references型に関しては、以下の記事を参考にしてください。

https://qiita.com/ryouzi/items/2682e7e8a86fd2b1ae47

上記のコマンドを実行したらマイグレーションファイルが作成されるので、
以下のコマンドを忘れないように実行しましょう！！
```bash
rails db:migrate
```
## モデルの記述
**relationshipモデル**には、以下のように記述します。
```ruby:app/models/relationship.rb
class Relationship < ApplicationRecord
# 1
  belongs_to :follower, class_name: "User"
# 2
  belongs_to :followed, class_name: "User"
end
```
ここで行っている設定は、
1. `follower`というオブジェクト名で`Userモデル`とのリレーションを行う設定
2. `followed`というオブジェクト名で`Userモデル`とのリレーションを行う設定

この2つになります。
<details><summary><b>belongs_toについて</b></summary>
ここでは、belongs_toについて補足説明します。
以下のようにモデルの設定がされているとします。

```ruby:Xxx.rb
class Xxx < ApplicationRecord
  belongs_to :yyy
end
```
`belongs_to`の設定をすると、XXXテーブルにあるYYYモデルの外部キー`YYY_id`を参考に、YYYモデルのレコードを参照することができます。
ここで、アソシエーションによって動作する内容としては以下になります。

```ruby
def self.yyy
  return Yyy.find(self.yyy_id)
end
```
この処理によって、親モデルのレコードを取得することができます。

どの外部キーを参照するかについては、`belongs_to :`の後ろに設定されているものに`_id`を付けたものを外部キーとして扱います。
例えば、`belongs_to :user`の場合だと、`user_id`が外部キーとなります。
一方で、`belongs_to :users`だと、`users_id`となるため、単数形、複数形の設定に注意しましょう。
</details>

**userモデル**には、以下のように記述します。
```ruby:app/models/user.rb
class User < ApplicationRecord
  # あるユーザーをフォローしている人(フォロワー)の一覧を取得するアソシエーション##########
# 1
  has_many :reverse_of_relationships, class_name:  "Relationship",
                                      foreign_key: "followed_id",
                                      dependent:   :destroy
# 2
  has_many :followers, through: :reverse_of_relationships, source: :follower
  ################################################################################
  
  # あるユーザーがフォローしている人(フォロイー)の一覧を取得するアソシエーション##########
# 3
  has_many :relationships, class_name:  "Relationship",
                           foreign_key: "follower_id",
                           dependent:   :destroy
# 4
  has_many :followings, through: :relationships, source: :followed
  ################################################################################
end
```
ここで行っている設定は、
1. `reverse_of_relationships`というオブジェクト名で、`Relationshipモデル`と`followed_id`を外部キーとしてアソシエーションを行う設定
2. `followers`というオブジェクト名で、1で設定した`reverse_of_relationships`というアソシエーションを利用して`Relationshipモデル`を参照し、参照先の`Relationshipモデル`の`follower`というアソシエーションを行う設定
3. `relationships`というオブジェクト名で、`Relationship`モデルと`follower_id`を外部キーとしてアソシエーションを行う設定
4. `followings`というオブジェクト名で、3で設定した`relationships`というアソシエーションを利用して`Relationshipモデル`を参照し、参照先の`Relationshipモデル`の`followed`というアソシエーションを行う設定

この4つの設定になります。
以上でアソシエーションの実装は終わりです。
## オプションについて
今回使用されている`has_many`のオプションについてそれぞれ解説していきます。
#### class_name
このオプションは、関連付け相手のモデル名を直接指定できるオプションとなります。
これを設定することによって、任意のオブジェクト名にモデルを関連付けることができます。
今回は、`Relationshipモデル`との関連付けを行いたいため、`Relationship`と設定しています。
#### foreign_key
このオプションは、外部キーの名前を直接指定することができます。
今回は、Relationshipモデルに2つの外部キーがあるため、これを使用して外部キーを識別します。
#### dependent
このオプションは、オブジェクトのオーナー(1:nの1の方)のレコードが削除されたとき、それに関連付けられたオブジェクトを制御することができます。
よく指定するのは、`:destroy`オプションで、これを指定すると、関連付けられたオブジェクトも同時に削除することができます。
#### through
このオプションは、3つのモデル間でアソシエーションする際に使用します。
これは、2つのモデルの中間にモデルが介在し、中間のモデルを経由し、2つのモデルにおいてアソシエーションの関係を作ります。（親 - 子 - 孫の関係を作成）
```ruby
class Xxx < ApplicationRecord
  has_many :yyy
  has_many :zzz, through: :yyy
end

class Yyy < ApplicationRecord
  belongs_to :xxx
  has_many :zzz
end

class Zzz < ApplicationRecord
  belongs_to :yyy
end
```
このように設定し、以下のコードを実行します。
```ruby
Xxx.find(1).zzz
```
これを実行すると、図の赤枠のレコードを取得することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967037/05cf2045-6d63-0cbd-06c9-e736cf71d82f.png)
`Xxxモデル`に紐づいている`Yyyモデル`を参照し、それぞれの`Yyyモデル`に紐づいている`Zzzモデル`のレコードを参照し、親モデル(Xxxモデル)の情報から孫モデル(Zzzモデル)の情報すべてを取得できます。
#### source
`throughオプション` を適用したオブジェクト名から、中間のモデル内のアソシエーションのうち、どれを使用して孫モデルを参照するのか推測できない場合に、中間のモデル内のアソシエーションを直接指定する際に使用します。
throughオプションで紹介したコードを、`source`を使用して**丁寧に**書くと以下のようになります。
```diff_ruby
class Xxx < ApplicationRecord
  has_many :yyy
- has_many :zzz, through: :yyy
+ has_many :zzz, through: :yyy, source: :zzz
end

class Yyy < ApplicationRecord
  belongs_to :xxx
  has_many :zzz
end

class Zzz < ApplicationRecord
  belongs_to :yyy
end
```
今回は動作自体は同じですが、このように設定することによって、Yyyモデル(子モデル)からZzzモデル(孫モデル)を参照する際に使用するアソシエーションを指定することができます。

# 解説
ここから、フォローフォロワーのアソシエーションについて詳細に解説していきます。
## フォロワーを取得するアソシエーションについて
フォロワーを取得するアソシエーションは**2つ**に分かれます。
それぞれ解説していきます！
#### 1
```ruby
has_many :reverse_of_relationships, class_name:  "Relationship",
                                      foreign_key: "followed_id",
                                      dependent:   :destroy
```
まず、こちらのアソシエーションについてです。
![画像3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967037/29b40b8a-6c55-70c8-ca6d-81a7d4187ae4.png)
このようなイメージとなります。
フォロワーを取得するため、フォローされているユーザーの情報(followed_id)を元に、`Relationshipモデル`の情報を参照します。
つまり、このアソシエーションを使用すると、`Relationshipモデル`を扱っている状態となります。
#### 2
次に、最もややこしい部分になります。
```ruby:user.rb
has_many :followers, through: :reverse_of_relationships, source: :follower
```
こちらのアソシエーションに関しては、一つ目のアソシエーションを`throughオプション`で利用しています。
この状態では、Relationshipモデルのレコードを参照するため、フォロワーの情報を取得することができません。
そこで、`sourceオプション`によって、`Relationshipモデル`に定義されている、
```ruby:relationship.rb
belongs_to :follower, class_name: "User"
```
このアソシエーションを使用して、参照しているrelationshipモデルのレコード内の`follower_idカラム`とUserモデルの`idカラム`が同一のユーザーのレコードを参照しています。![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967037/3d2dc6b6-de4e-3009-5028-66a118ebdb9f.png)


フォロイーのアソシエーションについても同様に働きます。

# まとめ
フォロー、フォロワー機能のアソシエーションがどのように働いているか掴めたでしょうか。
中間テーブルは難しい内容ですが、理解できるまで何回でも見返していただけると嬉しいです！！

# 参考文献
https://railsguides.jp/association_basics.html

https://api.rubyonrails.org/v7.0/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to