アソシエーションのthrough, sourceオプションを図として理解する。

# きっかけ
Railsを使って掲示板をブックマークする機能を実装する勉強中、中間テーブルを絡めたアソシエーションの理解に苦しんだ。
特に、後述の`bookmarked_boards.destroy(board)`と、`bookmark.destroy(bookmark)`が同じ意味であることのイメージができなかった。
図としてイメージをすることでさらに理解を定着させたい。

:::note warn
※部分は独自の解釈が含まれるので、間違いであればご指摘いただけると幸いです。
:::


# 環境
ruby 3.14
rails 7.0.3.1
mac OS Sonoma 14.2.1
docker 24.0.7
docker compose v2.23.3-desktop.2

# したいこと
* 多対多のアソシエーションにおけるthroughオプションと、sourceオプションを理解する。
* `bookmarked_boards.destroy(board)`と`bookmarks.destroy(bookmark)`が何故同じ結果になるのかを図で理解する。

# 今回のポイント
* `has_many: boards` はboardモデルを勝手に参照してくれるが、boards以外の名前にした場合、どのモデルを参照させるかを`source: :モデル名`で指定する
* `has_many: bookmark_boards, through: :bookmarks, source: :board`は
`bookmarks`テーブルを、bookmarkに登録された掲示板の情報を参照する`bookmark_boards`テーブルとしても扱うようにするイメージ

# 詳しく
## まずは各モデルのイメージ
今回はUserモデル、Boardモデル、Bookmarkモデルで考える。
各モデルのER図は以下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3669918/5472048c-6fd8-3ff5-88f1-855611cd0dae.png)


## Boardモデルへのアソシエーションの重複解消 -bookmark_boardsの誕生-
まず、Userモデル側にBoardモデルからの情報を取得できるようにアソシエーションを定義する。
boardsと定義すれば、勝手にBoardモデルを参照してくれる。
```ruby:user.rb
class User < ApplicationRecord
  # Userモデルをsorceryというgemを使って作成しているのでこの記述がある。
  authenticates_with_sorcery!
  # dependent: :destroyで、ユーザーが削除されたら作成した掲示板も削除されるようにしてる。
  has_many :boards, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
end
```
```ruby:bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
end
```
```ruby:board.rb
class Board < ApplicationRecord
  belongs_to :user
  has_many :bookmarks, dependent: :destroy
end
```

しかし、bookmarkを通してBoardモデルからデータを取得したい場合、下記のように書きたいが...
```ruby:user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :boards, dependent: :destroy
  # boardsが2個あることになるのでNG
  has_many :boards, through: :bookmarks
end
```
boardsが重複してしまうので、2個目のアソシエーションは名前を変える。習わし的に、`throughのモデル名_参照するモデル名`とするようだ。
```ruby:user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :boards, dependent: :destroy
  # boardsが2個あることになるのでNG
  has_many :bookmark_boards, through: :bookmarks
end
```

## ※bookmark_boardsのイメージ
ここの自分なりイメージとしては、bookmarksテーブルに対して、
`board_idに紐づくboardモデルのデータを参照できるbookmark_boards`
という別名が与えらえたイメージ。
あくまでbookmarksテーブルの情報が拡張された感じと考えておくと、後々の処理が納得できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3669918/a559730e-821f-c647-a7b2-db428b827d3e.png)


## このイメージで以下の処理が理解できるようになった
ブックマークを削除するunbookmarkインスタンスメソッドを用意したいとき、以下の`パターンA`と`パターンB`はどちらも同じ結果を得られる。
```ruby: 実行ログ
Bookmark Destroy (0.5ms)  DELETE FROM `bookmarks` WHERE `bookmarks`.`id` = 16
↳ app/models/user.rb:18:in `unbookmark'
TRANSACTION (2.0ms)  COMMIT
```

しかし、パターンAが掲示板の情報を渡しているだけなので、なぜbookmarksの該当レコードが削除できるのか理解できなかった。


### パターンA
params[:id]には削除したいbookmarksのidが入っている。

```ruby:bookmarks_controller.rb
class BookmarksController < ApplicationController
  def destroy
    # bookmarksテーブル内のparams[:id]と一致するレコードに紐づいた掲示板のデータを取得
    board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(board)
    redirect_to boards_path, success: 'ブックマークを削除しました', status: :see_other
  end
end
```

```ruby:user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :boards, dependent: :destroy
  has_many :bookmark_boards, through: :bookmarks
  
  def unbookmark(board)
    # bookmark_boardsを別テーブルみたいなイメージをせずに、bookmarksの別名として考える。
    # boardも、bookmarksに拡張されて追加された　”bookmarks.idとかも含んだ掲示板データ”
    # と考えれば、boardと一致するbookmarksレコードを特定し、削除しているのだと納得できる。
    bookmark_boards.destroy(board)
  end
end
```
### パターンB
params[:id]には削除したいbookmarksのidが入っている。
```ruby:bookmarks_controller.rb
class BookmarksController < ApplicationController
  def destroy
    current_user.unbookmark(params[:id])
    redirect_to boards_path, success: 'ブックマークを削除しました', status: :see_other
  end
end
```
```ruby:user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :boards, dependent: :destroy
  has_many :bookmark_boards, through: :bookmarks
  
  def unbookmark(bookmark)
    # 該当するidのbookmarksレコードを削除する。
    bookmarks.destroy(bookmark)
  end
end
```
パターンAのコメント部分で記載したように、bookmarksテーブルがbookmark_boardsという別名をアソシエーションで与えられたというイメージをすれば、2パターンのdestroyアクションが同じ結果になるのも納得できた。
でも、パターンAは二度手間感あるので、パターンBの方がイメージしやすいかな。笑


# 参考にさせていただいた記事、サイト
https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1

https://penguintech.hatenablog.com/entry/2021/05/02/172732

https://qiita.com/imotan/items/036ceffb79e294d8a063

https://railsguides.jp/association_basics.html#belongs-to%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%81%AE%E8%A9%B3%E7%B4%B0