LoginSignup
1
0

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

Posted at

きっかけ

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図は以下。
image.png

Boardモデルへのアソシエーションの重複解消 -bookmark_boardsの誕生-

まず、Userモデル側にBoardモデルからの情報を取得できるようにアソシエーションを定義する。
boardsと定義すれば、勝手にBoardモデルを参照してくれる。

user.rb
class User < ApplicationRecord
  # Userモデルをsorceryというgemを使って作成しているのでこの記述がある。
  authenticates_with_sorcery!
  # dependent: :destroyで、ユーザーが削除されたら作成した掲示板も削除されるようにしてる。
  has_many :boards, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
end
bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
end
board.rb
class Board < ApplicationRecord
  belongs_to :user
  has_many :bookmarks, dependent: :destroy
end

しかし、bookmarkを通してBoardモデルからデータを取得したい場合、下記のように書きたいが...

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のモデル名_参照するモデル名とするようだ。

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

このイメージで以下の処理が理解できるようになった

ブックマークを削除する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が入っている。

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
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が入っている。

bookmarks_controller.rb
class BookmarksController < ApplicationController
  def destroy
    current_user.unbookmark(params[:id])
    redirect_to boards_path, success: 'ブックマークを削除しました', status: :see_other
  end
end
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の方がイメージしやすいかな。笑

参考にさせていただいた記事、サイト

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0