きっかけ
Railsを使って掲示板をブックマークする機能を実装する勉強中、中間テーブルを絡めたアソシエーションの理解に苦しんだ。
特に、後述のbookmarked_boards.destroy(board)
と、bookmark.destroy(bookmark)
が同じ意味であることのイメージができなかった。
図としてイメージをすることでさらに理解を定着させたい。
※部分は独自の解釈が含まれるので、間違いであればご指摘いただけると幸いです。
環境
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図は以下。
Boardモデルへのアソシエーションの重複解消 -bookmark_boardsの誕生-
まず、Userモデル側にBoardモデルからの情報を取得できるようにアソシエーションを定義する。
boardsと定義すれば、勝手にBoardモデルを参照してくれる。
class User < ApplicationRecord
# Userモデルをsorceryというgemを使って作成しているのでこの記述がある。
authenticates_with_sorcery!
# dependent: :destroyで、ユーザーが削除されたら作成した掲示板も削除されるようにしてる。
has_many :boards, dependent: :destroy
has_many :bookmarks, dependent: :destroy
end
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
end
class Board < ApplicationRecord
belongs_to :user
has_many :bookmarks, dependent: :destroy
end
しかし、bookmarkを通してBoardモデルからデータを取得したい場合、下記のように書きたいが...
class User < ApplicationRecord
authenticates_with_sorcery!
has_many :boards, dependent: :destroy
# boardsが2個あることになるのでNG
has_many :boards, through: :bookmarks
end
boardsが重複してしまうので、2個目のアソシエーションは名前を変える。習わし的に、throughのモデル名_参照するモデル名
とするようだ。
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テーブルの情報が拡張された感じと考えておくと、後々の処理が納得できる。
このイメージで以下の処理が理解できるようになった
ブックマークを削除するunbookmarkインスタンスメソッドを用意したいとき、以下のパターンA
とパターンB
はどちらも同じ結果を得られる。
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が入っている。
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
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が入っている。
class BookmarksController < ApplicationController
def destroy
current_user.unbookmark(params[:id])
redirect_to boards_path, success: 'ブックマークを削除しました', status: :see_other
end
end
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の方がイメージしやすいかな。笑
参考にさせていただいた記事、サイト