いつも忘れてしまうRailsの多対多 + 自己(関連先が自身のテーブル)参照 + 条件付き関連の書き方をフォロー・フォロワーを例にまとめる

## 概要

Railsでは has_many, belongs_to の関連をよく書くのでしっかり脳で覚えているのですが、
`has_many through` を使った多対多の関連はたまにしか書かないためくよく忘れてしまいます。

なので個人の備忘録としても、理解をしてより覚えるためにも図でまとめました。

## 例

* Railsチュートリアルでもよく使われている、Twitterのようなサービスを例にした「ユーザの相互フォロー」を記載していきます
* ユーザAさんは複数のユーザをフォローできます
* ユーザAさんは複数のユーザにフォローされます

### テーブル構造

* ユーザ情報(users)
* ユーザのフォロー関連(user_follow_statuses)

![スクリーンショット 2022-01-07 12.18.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/0dfad125-9ac6-485b-49dd-5328d552f89e.png)


## コード


```ruby:user.rb
class User < ApplicationRecord
  has_many :follow_statuses, class_name: UserFollowStatus.to_s,
                             foreign_key: :follower_id,
  # 自分がフォローしているユーザたち
  has_many :following, through: :follow_statuses, source: :followed

  has_many :followed_statuses, class_name: UserFollowStatus.to_s,
                               foreign_key: :followed_id,
  # 自分がフォローされているユーザたち
  has_many :followers, through: :followed_statuses, source: :follower
end
```

```ruby:user_follow_status.rb
class UserFollowStatus < ApplicationRecord
  belongs_to :follower, class_name: User.to_s
  belongs_to :followed, class_name: User.to_s
end
```

## 図

### AさんがBさんをフォローしている

* 以下のような関係になる

![スクリーンショット 2022-01-07 12.24.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/d0bef992-ad3b-1132-ad5b-1f3c9984dd0b.png)


このとき以下のようなプログラムでそれぞれフォローしている相手、されている相手を取得できるようになる。


```ruby
# Aさんがフォローしているユーザたち
user(A).following
#=> [UserB, UserC]

# Bさんがフォローされているユーザたち
user(B).followers
#=> [UserA]
```

なるけど、これを今暗記・コードを使い回す状態なのでもう少し整理する。

### プログラムの関連

プログラムを並べるとこういう関係性になる。ちょっとわかるようで分からない…。

![スクリーンショット 2022-01-07 13.11.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/78cf888f-60a3-97d3-be79-c4b7336958f7.png)


### 順序よく考える

#### AさんがB,Cさんをフォローしているという状況

AさんがB,Cさんをフォローしているという状況を考える。
データは以下のように作成される。

![スクリーンショット 2022-01-07 12.24.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/9a4e48eb-8e12-d9de-858b-f34833b18225.png)

これを has_many, belongs_to の2つだけを使ってまとめると以下になる

![スクリーンショット 2022-01-07 12.24.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/1eb00fc5-f50a-f25b-fc0d-03cf50eb8420.png)


※緑色の字はRailsの命名規則に従っていれば省略可能なものですが、理解を深めるためにあえて記載しています

このときAさんがフォローしているB,Cさんを取得するには以下のプログラムを書けば良い

```ruby
# 自分がフォロー側になっている関連データを取得
user.follow_statuses.map do |follow_status|
  # 各データのフォロー先のユーザのデータを取得
  follow_status.followed
end
#=> [UserB, UserC]
```

この処理で発行されるSQLは下記

```sql
-- AさんのユーザIDである 1 が follower_id に設定されているデータ一覧を取得
UserFollowStatus Load (0.6ms)  SELECT `user_follow_statuses`.* FROM `user_follow_statuses` WHERE `user_follow_statuses`.`follower_id` = 1
  -- 取得されたデータの followed_id に入っているユーザID(Bさんの2, Cさんの3)でユーザデータを取得
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
```

※N+1問題が発生していますがわかりやすさのためそのままにしています

これを `has_many through` を利用すると以下のようにまとめられる

![スクリーンショット 2022-01-07 12.30.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/4149c987-fcd6-05d9-d368-13c104770446.png)

つまり has_many で定義している `follow_statuses` を経由(`through`)して、
そこからさらに各データの `followed` で定義されている先のデータを取得している、といえる。

この定義により


```ruby
# Aさんがフォローしているユーザたち
user(A).following
#=> [UserB, UserC]
```

という簡潔にプログラムが記載できるようになる。
このとき発行されるSQLは下記のようになる。

```sql
 SELECT `users`.* FROM `users` INNER JOIN `user_follow_statuses` ON `users`.`id` = `user_follow_statuses`.`followed_id` WHERE `user_follow_statuses`.`follower_id` = 1
```

これは単純に `簡潔になる` だけではなく、以下のようなメリットも受けることができる

* N+1問題など気にしなくてもSQLの発行がすくなくなりDBのアクセス負荷を減らせる
* 戻り値が `ActiveRecord_Associations_CollectionProxy` になり、クエリのメソッドチェーンができる

---

#### BさんがA,Cさんにフォローされているという状況

次にフォローされている状況を考える。
BさんはA,Cさんに**フォローされている**、とする。

データは以下のように作成される。

![スクリーンショット 2022-01-07 12.37.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/80b55b60-0d3b-195a-dd1c-d765b8db1712.png)


これを has_many, belongs_to の2つだけを使って考えると以下になる

![スクリーンショット 2022-01-07 13.16.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/74f3dfd1-e986-7ec2-c1c0-9886de78788f.png)


※緑色の字はRailsの命名規則に従っていれば省略可能なものですが、理解を深めるためにあえて記載しています

このときBさんが**フォローされている**A,Cさんを取得するには以下のプログラムを書けば良い

```ruby
# Bさんがフォローされている側の関連テーブルのデータを取得
user.followed_statuses.map do |followed_status|
  # フォローしているユーザのデータを取得
  follow_status.follower
end
#=> [UserA, UserC]
```

この処理で発行されるSQLは下記

```sql
-- BさんのユーザIDである 2 が followed_id に設定されているデータ一覧を取得
UserFollowStatus Load (0.9ms)  SELECT `user_follow_statuses`.* FROM `user_follow_statuses` WHERE `user_follow_statuses`.`followed_id` = 2
  -- 取得されたデータの follower_id に入っているユーザID(Aさんの1, Cさんの3)でユーザデータを取得
    User Load (1.0ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
    User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
```

※N+1問題が発生していますがわかりやすさのためそのままにしています

これも同様に `has_many through` を利用すると以下のようにまとめられる


![スクリーンショット 2022-01-07 13.17.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/1202284f-648c-537e-fc5b-9fdcdd257d16.png)


よって

```ruby
# Bさんがフォローされているユーザたち
user(B).followers
#=> [UserA, UserC]
```

という簡潔にプログラムが記載できるようになる。
このとき発行されるSQLは下記のようになる。

```sql
 SELECT `users`.* FROM `users` INNER JOIN `user_follow_statuses` ON `users`.`id` = `user_follow_statuses`.`follower_id` WHERE `user_follow_statuses`.`followed_id` = 2
```

こちらもフォローのときと同じようにメリットを受けられる

ということでこれらをひっくるめて冒頭のコードが出来上がるし、以下のような関連の図ができあがる

![スクリーンショット 2022-01-07 13.11.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/78cf888f-60a3-97d3-be79-c4b7336958f7.png)


## 応用: 条件を付ける

さらに応用としてユーザのフォローに種別を追加する。
例えばこのフォローに「名前のみ閲覧可能」「投稿も閲覧可能」というタイプを持たせる。

以下がデータベースのテーブル構造。

![スクリーンショット 2022-01-07 13.19.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/869a9207-4fa5-1120-c4ce-337b7d2bf4b4.png)


* follow_type: 0=名前のみ閲覧可能
* follow_type: 1=投稿も閲覧可能

とする。

このとき「名前のみ閲覧可能なフォローしているユーザ一覧」の関連を定義する

## コード

```ruby:user.rb
class User < ApplicationRecord
  has_many :name_only_follow_statuses, -> { where(follow_type: 0) },
                                       class_name: UserFollowStatus.to_s,
                                       foreign_key: :follower_id,
                                       primary_key: :id
  has_many :name_only_following, through: :name_only_follow_statuses, source: :followed
end
```

### 順序よく考える

* AさんがB,Cさんをフォローしている
  * Bさんは名前のみ閲覧可能(typeが0)
  * Cさんは投稿も閲覧可能(typeが1)


データは以下のように作成される。

![スクリーンショット 2022-01-07 13.19.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/156cc336-aa79-cf0a-6cdb-88ffd07c3161.png)


これも同様に has_many, belongs_to の2つだけを使ってこの関連を考えると以下になる

![スクリーンショット 2022-01-07 13.34.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/f12c6ded-aa4b-698b-140a-bee0f57ce251.png)


※has_many には条件をブロックオブジェクトで渡すことができる
※belongs_to 側は応用前と変更がないので省略

同様にこれをそのままプログラムで書くと以下のようになる


```ruby
user.name_only_follow_statuses.map do |followed_status|
  followed_status.followed
end
#=> [UserB]
```

発行されるSQLは下記。

```sql
-- ユーザAのidである1が follower_id にあるものを検索
-- かつ follow_typeが0(=名前だけ閲覧可能)のデータだけを検索
UserFollowStatus Load (1.0ms)  SELECT `user_follow_statuses`.* FROM `user_follow_statuses` WHERE `user_follow_statuses`.`follower_id` = 1 AND `user_follow_statuses`.`follow_type` = 0
  -- 取得した結果であるユーザB(idが2)のユーザデータを取得
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
```

これを has_many through を利用すると以下のようにまとめられる

![スクリーンショット 2022-01-07 13.24.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101975/9f3e16ae-eb16-b4a8-f153-420e77505ec4.png)


よって

```ruby
# Aさんが名前のみ閲覧可能でフォローしているユーザたち
user(A).name_only_following
#=> [UserB]
```

という簡潔にプログラムが記載できるようになる。
このとき発行されるSQLは下記のようになる。

```sql
SELECT `users`.* FROM `users` INNER JOIN `user_follow_statuses` ON `users`.`id` = `user_follow_statuses`.`followed_id` WHERE `user_follow_statuses`.`follower_id` = 1 AND `user_follow_statuses`.`follow_type` = 0
```

ということで、以下の2ステップで条件付きの多:多も可能となる

* 1.関連テーブルになる関連先に条件を付けた、新しい has_many を定義
* 2.上記1で定義した関連で has_many through を定義する

## Tips

* この関連テーブルのリレーションは基本的に `dependent:   :destroy` を付けておいた方が良いよ！

## まとめ

* 多:多はそんなに怖くない！(と思う)