はじめに
掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除できる機能を実装したいとします。
今回は非常に難しい実装となったため、順を追って詳しく説明していきたいと思います。
間違いがあれば何なりとご指摘くださいませ。
必要な作業
- Bookmarkモデルを作成する
- バリデーションの追加
- アソシエーションの設定
- ルーティングの設定
- コントローラの設定
- ビューファイルの作成
Bookmarkモデルを作成する
今回のように「どのユーザー」が「どの投稿」をブックマークしたかを記録する為に、データベースに「user_id」と「post_id」の2つのカラムを持つbookmarkテーブル
を用意しましょう。
中間テーブルとは?
2つのテーブルの間をつなぐイメージです。
id | user_id | board_id |
1 | 3 | 2 |
2 | 2 | 5 |
3 | 1 | 6 |
上記のように両方のテーブルの外部キーとして持ち合わせています。
$ rails g model Bookmark user:references board:references
生成されたマイグレーションファイル
class CreateBookmarks < ActiveRecord ::migration[5.2]
def change
create_table :boookmarks do |t|
t.references :user, foreign_key: true
t.references :board, foreign_key: true
t.timestamps
end
add_index :bookmarks, [:user_id, :board_id], unique: :true
end
end
マイグレーションする前に、ユーザが同じ掲示板をブックマーク登録しないようにするためのunique: :true
を追記しましょう。
add_index :bookmarks, [:user_id, :board_id], unique: :true
でuser_id
とboard_id
の組み合わせが一意性を持つことになります。
バリデーションの追加
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
validates :user_id, presence: true, uniqueness: { scope: :board_id }
end
一人のユーザが同じ掲示板に対するブックマークは1つのみなので、user_id
とboard_id
の組み合わせは一意でなければならない。そのためには、一意性チェックの範囲を限定する別の属性を指定する:scope
オプションを設定する必要がある。uniqueness: { scope: :board_id }
とする。こうすることで、ブックマークは掲示板に対してユーザ一人までですよ。という意味合いを持たせることができます。scopeをつけないと、テーブル全体で一意な数字という意味になってしまいます。
マイグレーションしましょう。
$ rails db:migrate
アソシエーションの設定
Boardモデルの設定
has_many :bookmarks, dependent: :destroy
掲示板とブックマークは1対多の関係にあります。
has_many:bookmark
で設定し、掲示板が削除されたらブックマークも削除されるようにdependent: :destroy
も設定しましょう。
Userモデルの設定
has_many :bookmarks, dependent: :destroy
has_many :bookmark_boards, through: :bookmarks, source: :board
ユーザとブックマークも1対多の関係にありますのでhas_many
で設定します。
コードを読み解く
has_many :bookmark_boards, through: :bookmarks, source: :board
has_many
を使ってUserモデルが多数のbookmark_boards
を持っていることを設定しています。:source
オプションは、has_many :through
関連付けにおける「ソースの」関連付け名、つまり関連付け元の名前を指定します。このオプションは、関連付け名から関連付け元の名前が自動的に推論できない場合以外には使う必要はありません。
詳しくはRailsガイドを見ておきましょう。

bookmark_boards
とは何でしょうか?ここではhas_many :through 関連付けという方法と、source:
オプションを使って架空のbookmark_boards
を定義しています。
つまり、Userモデルからbookmark_boards
というメソッドを使用すると、through:
によって定義されたbookmarks
モデルを介して、board
にアクセスできる。ということなのです!
この設定によってuser.bookmark_boards
のような形でブックマークしているBoard
インスタンスを配列として取得できるようになりました。
具体的に内部で行われていることは、
Userモデルのインスタンスにbookmarks
メソッドを実行し、得られたbookmarksレコードひとつひとつに対してboard
メソッドを実行しています。
書き換えると下記のようなコードになります。
user.bookmarks.map{ |bookmark| bookmark.board }
と言われてもわからないと思うので、コンソールで確認してみます。
irb(main):003:0> user.bookmarks # ユーザに対するブックマーク情報を取得する
Bookmark Load (0.9ms) SELECT "bookmarks".* FROM "bookmarks" WHERE "bookmarks"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Bookmark id: 352, user_id: 1, board_id: 72, created_at: "2021-10-02 01:36:06", updated_at: "2021-10-02 01:36:06">, #<Bookmark id: 351, user_id: 1, board_id: 73, created_at: "2021-10-02 01:36:05", updated_at: "2021-10-02 01:36:05">, #<Bookmark id: 348, user_id: 1, board_id: 74, created_at: "2021-10-02 01:36:00", updated_at: "2021-10-02 01:36:00">, #<Bookmark id: 349, user_id: 1, board_id: 75, created_at: "2021-10-02 01:36:01", updated_at: "2021-10-02 01:36:01">, #<Bookmark id: 350, user_id: 1, board_id: 76, created_at: "2021-10-02 01:36:02", updated_at: "2021-10-02 01:36:02">, #<Bookmark id: 347, user_id: 1, board_id: 77, created_at: "2021-10-02 01:35:58", updated_at: "2021-10-02 01:35:58">, #<Bookmark id: 346, user_id: 1, board_id: 78, created_at: "2021-10-02 01:35:58", updated_at: "2021-10-02 01:35:58">, #<Bookmark id: 341, user_id: 1, board_id: 79, created_at: "2021-10-02 01:35:23", updated_at: "2021-10-02 01:35:23">, #<Bookmark id: 340, user_id: 1, board_id: 80, created_at: "2021-10-01 22:45:13", updated_at: "2021-10-01 22:45:13">, #<Bookmark id: 345, user_id: 1, board_id: 81, created_at: "2021-10-02 01:35:55", updated_at: "2021-10-02 01:35:55">]>
irb(main):004:0> user.bookmarks.first # ユーザに対する最初のブックマーク情報を取得する
Bookmark Load (0.6ms) SELECT "bookmarks".* FROM "bookmarks" WHERE "bookmarks"."user_id" = ? ORDER BY "bookmarks"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> #<Bookmark id: 340, user_id: 1, board_id: 80, created_at: "2021-10-01 22:45:13", updated_at: "2021-10-01 22:45:13">
irb(main):005:0> user.bookmarks.first.board # ユーザに対する最初のブックマーク情報にboardメソッドを実行し、紐付いた掲示板情報を取得する
Bookmark Load (0.1ms) SELECT "bookmarks".* FROM "bookmarks" WHERE "bookmarks"."user_id" = ? ORDER BY "bookmarks"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
Board Load (0.3ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 80], ["LIMIT", 1]]
=> #<Board id: 80, user_id: 2, title: "huga9", body: "test text", created_at: "2021-10-01 13:31:35", updated_at: "2021-10-01 13:31:35", board_image: nil>
irb(main):006:0> user.bookmarks.map{ |bookmark| bookmark.board } # mapメソッドを使って、取得したユーザに対するブックマーク情報一つ一つにboardメソッドを実行し、配列を作り直す。
Bookmark Load (0.5ms) SELECT "bookmarks".* FROM "bookmarks" WHERE "bookmarks"."user_id" = ? [["user_id", 1]]
Board Load (0.4ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 72], ["LIMIT", 1]]
Board Load (0.1ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 73], ["LIMIT", 1]]
Board Load (0.1ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 74], ["LIMIT", 1]]
Board Load (0.1ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 75], ["LIMIT", 1]]
Board Load (1.9ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 76], ["LIMIT", 1]]
Board Load (0.0ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 77], ["LIMIT", 1]]
Board Load (0.1ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 78], ["LIMIT", 1]]
Board Load (0.0ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 79], ["LIMIT", 1]]
Board Load (0.0ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 80], ["LIMIT", 1]]
Board Load (0.0ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 81], ["LIMIT", 1]]
=> [#<Board id: 72, user_id: 2, title: "huga1", body: "test text", created_at: "2021-10-01 13:31:13", updated_at: "2021-10-01 13:31:13", board_image: nil>, #<Board id: 73, user_id: 2, title: "huga2", body: "test text", created_at: "2021-10-01 13:31:16", updated_at: "2021-10-01 13:31:16", board_image: nil>, #<Board id: 74, user_id: 2, title: "huga3", body: "test text", created_at: "2021-10-01 13:31:19", updated_at: "2021-10-01 13:31:19", board_image: nil>, #<Board id: 75, user_id: 2, title: "huga4", body: "test text", created_at: "2021-10-01 13:31:22", updated_at: "2021-10-01 13:31:22", board_image: nil>, #<Board id: 76, user_id: 2, title: "huga5", body: "test text", created_at: "2021-10-01 13:31:24", updated_at: "2021-10-01 13:31:24", board_image: nil>, #<Board id: 77, user_id: 2, title: "huga6", body: "test text", created_at: "2021-10-01 13:31:27", updated_at: "2021-10-01 13:31:27", board_image: nil>, #<Board id: 78, user_id: 2, title: "huga7", body: "test text", created_at: "2021-10-01 13:31:29", updated_at: "2021-10-01 13:31:29", board_image: nil>, #<Board id: 79, user_id: 2, title: "huga8", body: "test text", created_at: "2021-10-01 13:31:32", updated_at: "2021-10-01 13:31:32", board_image: nil>, #<Board id: 80, user_id: 2, title: "huga9", body: "test text", created_at: "2021-10-01 13:31:35", updated_at: "2021-10-01 13:31:35", board_image: nil>, #<Board id: 81, user_id: 2, title: "huga10", body: "test text", created_at: "2021-10-01 13:31:38", updated_at: "2021-10-01 13:31:38", board_image: nil>]
irb(main):007:0> user.bookmark_boards # mapメソッドを使った処理と、全く同じ内容の配列が取得される。
Board Load (0.2ms) SELECT "boards".* FROM "boards" INNER JOIN "bookmarks" ON "boards"."id" = "bookmarks"."board_id" WHERE "bookmarks"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 72, user_id: 2, title: "huga1", body: "test text", created_at: "2021-10-01 13:31:13", updated_at: "2021-10-01 13:31:13", board_image: nil>, #<Board id: 73, user_id: 2, title: "huga2", body: "test text", created_at: "2021-10-01 13:31:16", updated_at: "2021-10-01 13:31:16", board_image: nil>, #<Board id: 74, user_id: 2, title: "huga3", body: "test text", created_at: "2021-10-01 13:31:19", updated_at: "2021-10-01 13:31:19", board_image: nil>, #<Board id: 75, user_id: 2, title: "huga4", body: "test text", created_at: "2021-10-01 13:31:22", updated_at: "2021-10-01 13:31:22", board_image: nil>, #<Board id: 76, user_id: 2, title: "huga5", body: "test text", created_at: "2021-10-01 13:31:24", updated_at: "2021-10-01 13:31:24", board_image: nil>, #<Board id: 77, user_id: 2, title: "huga6", body: "test text", created_at: "2021-10-01 13:31:27", updated_at: "2021-10-01 13:31:27", board_image: nil>, #<Board id: 78, user_id: 2, title: "huga7", body: "test text", created_at: "2021-10-01 13:31:29", updated_at: "2021-10-01 13:31:29", board_image: nil>, #<Board id: 79, user_id: 2, title: "huga8", body: "test text", created_at: "2021-10-01 13:31:32", updated_at: "2021-10-01 13:31:32", board_image: nil>, #<Board id: 80, user_id: 2, title: "huga9", body: "test text", created_at: "2021-10-01 13:31:35", updated_at: "2021-10-01 13:31:35", board_image: nil>, #<Board id: 81, user_id: 2, title: "huga10", body: "test text", created_at: "2021-10-01 13:31:38", updated_at: "2021-10-01 13:31:38", board_image: nil>]>
bookmark_boards
メソッドで何が取得できるのかイメージが付いたのでは無いでしょうか?
ルーティングの設定
ブックマークの一覧画面へのリンクはbookmarks_boards_path
という風にしたいです。
index/new/createのような、idを持たないようなアクションのことをコレクション
と呼び、show/edit/update/destroyのような、idを必要とするアクションのことをメンバー
と呼びます。
Rails.application.routes.draw do
root 'static_pages#top'
get 'login', to: 'user_sessions#new'
post 'login', to: 'user_sessions#create'
delete 'logout', to: 'user_sessions#destroy'
resources :users, only: %i[new create]
resources :boards do
resources :comments, only: %i[create], shallow: true
collection do
get 'bookmarks'
end
end
resources :bookmarks, only: %i[create destroy]
end
コレクションルーティングを追加する
デフォルトで作成されるRESTfulなルーティングは7つですが、7つでなければならないということはありません。必要であれば、コレクションやコレクションの各メンバーに対して適用されるリソースを追加することもできます。
今回のようにboards_controllerに新しくbookmarks
というアクションを追加したく、prefixパスもbookmarks_boards_path
のようにidを伴わないパスにしたい場合、collectionを使用します。
collection do
get 'bookmarks'
end
Prefix Verb URI Pattern Controller#Action
bookmarks_boards GET /boards/bookmarks(.:format) boards#bookmarks
コントローラの設定
BookmarksControllerを作成
ビューファイルは必要ないので、手動で作っても良いでしょう。
今回はcontrollerの見通しを良くし、後々メンテナンスしやすくするため、先にUserモデル部分にブックマーク処理のロジックを書いていきます。
Userモデルのインスタンスに対して使うことを想定しているメソッドです。
Userモデルにブックマーク処理のロジックを定義する
# ブックマークに追加する
def bookmark(board)
bookmark_boards << board
end
# ブックマークを外す
def unbookmark(board)
bookmark_boards.destroy(board)
end
# ブックマークをしているか判定する
def bookmark?(board)
bookmark_boards.include?(board)
end
- bookmarkメソッド
# ブックマークに追加
def bookmark(board)
bookmark_boards << board
end
ユーザのbookmark_boards
(ブックマークした掲示板)から渡された引数のboard
の中にあるboard_id
を参照して探します。無ければ<<
メソッドによって末尾に格納され、bookmarks
テーブルに保存されます。save
メソッドは必要ありません。
※すでにある場合はrollbackします。
<<
メソッドについて詳しくはこちらを参照ください。
また、この演算子ではbookmarks.create!(board_id: board.id)
と同様の処理が行われて、CREATE文のSQLが発行されます。
- unbookmarkメソッド
# ブックマークを外す
def unbookmark(board)
bookmark_boards.destroy(board)
end
ユーザの bookmark_boards
(ブックマークした掲示板)から渡された引数のboard
の中にあるboard_id
を参照して探します。存在すれば削除するメソッド です。
- bookmark?メソッド
# ブックマークしているか判定するメソッド
def bookmark?(board)
bookmark_boards.include?(board)
end
ユーザのbookmark_boards
(ブックマークした掲示板)から渡された引数のboard
の中にあるboard_id
を参照して探します。include?
メソッドは、引数(board
)が存在すればtrue
を返し、無ければfalse
を返します。
bookmarks_controllerに追記
class BookmarkController < ApplicationController
def create
board = Board.find(params[:board_id])
current_user.bookmark(board)
redirect_back fallback_location: root_path, success: t('.success')
end
def destroy
board = current_user.bookmarks.find(params[:id]).board
current_user.unbookmark(board)
redirect_back fallback_location: root_path, success: t('defaults.message.unbookmark')
end
end
コードを紐解く
redirect_back fallback_location: root_path
redirect_back
は、直前のページにリダイレクトします。
fallback_location
は直前のページがない場合のリダイレクト先を指定します。
board = current_user.bookmarks.find(params[:id]).board
なぜ params[:id]
を使った取得の仕方をしているかというと、destroyアクションへのルーティングが、bookmarks/:id
となっていることを思い出しましょう。
Prefix Verb URI Pattern Controller#Action
bookmarks POST /bookmarks(.:format) bookmarks#create
bookmark DELETE /bookmarks/:id(.:format) bookmarks#destroy
current_user
に紐ついたブックマークレコードをパラメータ(bookmarks/:id)から取り出して、後ろの.board
によって、board_id
を参照し、そのブックマークレコードに紐ついた掲示板レコードを取得してきています。
Board_controllerに追記
さきほどコレクションルーティングを追加したbookmarks
アクションをboards_controllerに追加していきましょう。
def bookmarks
@bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
end
N + 1問題の対策として、includes(:user)
を使って関連するuserの情報も取得します。
このことをキャッシュを取得する。と言うみたいです。
難しいのでここではincludes
については言及しません。
ビューファイルの作成
あとはビューファイル側の設定をします。今一度、作業の流れを確認しましょう。
ビューファイルの作業手順
- 掲示板一覧画面の中でブックマーク追加, ブックマーク削除を切り替えるパーシャルを用意する(
_bookmark_button.html.erb
) - ブックマーク追加/解除ボタンをそれぞれパーシャルで用意する(
_bookmark.html.erb
/_unbookmark.html.erb
) - 自分の掲示板にはブックマークボタンが表示されないように実装する
- ブックマークした掲示板を一覧表示する画面を用意する(
bookmarks.html.erb
)
ブックマーク追加, ブックマーク削除を切り替えるパーシャルを作成
<% if current_user.bookmark?(board) %>
<%= render 'unbookmark', board: board %>
<% else %>
<%= render 'bookmark', board: board %>
<% end %>
user.rbで定義したbookmark?
メソッドがここで使われています。もし掲示板がブックマークされていたらブックマーク解除ボタンを表示し、ブックマークされていなければブックマーク登録ボタンを表示するように切り分けています。
ブックマーク登録ボタンのパーシャルを作成
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}",class: 'float-right', method: :post do %>
<%= icon 'far', 'star' %>
<% end %>
ここでは私自身、link_to bookmarks_path(board_id: board.id)
という書き方について、「なぜboard_id
を渡せているのか?」という点についてハマりました。
コードを紐解く
今一度、ブックマークを追加する処理の流れを確認しましょう。
boards/_bookmark.html.erb
ブックマーク追加リンク
link_to
↓
ルーティング
↓
bookmarks_controller.rb
createアクション
↓
リダイレクトされる
ここでの処理の流れとして、link_to
からルーティングへ、次にbookmarks_controller
のcreate
アクションに送る必要があると思います。
- ルーティングを確認
resources :boards do
resources :comments, only: %i[create], shallow: true
collection do
get 'bookmarks'
end
end
resources :bookmarks, only: %i[create destroy]
end
Prefix Verb URI Pattern Controller#Action
bookmarks POST /bookmarks(.:format) bookmarks#create
bookmark DELETE /bookmarks/:id(.:format) bookmarks#destroy
bookmarks
コントローラのルーティング設定はboards
コントローラの子要素としてネストしていません。ですので、bookmarks
コントローラのcreate
アクションに送るためのパスはPOST
形式でbookmarks
となっていることが確認できます。
- bookmarks_controllerのcreateアクションに送るためのリンク
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}",class: 'float-right', method: :post do %>
さきほどのルーティングの情報から、method: :post
を付与し、 bookmarks_path
としてパスを渡していることがわかります。
しかし、このbookmarks_path
に渡されている(board_id: board.id)
ですが、本来ルーティングでboards
コントローラの子要素としてネストして、boards/board_id/bookmarks
のような、baord_id
が含まれる形の場合に、params[:board_id]
としてブックマークしようとしている掲示板のidが取得できるのではないかと認識しておりました。
また、デベロッパツールで見てみると/bookmarks? board_id=〇〇
という形でデータが渡っています。

- bookmarksコントローラのcreateアクションでパラメータを取得
board = Board.find(params[:board_id])
/bookmarks? board_id=〇〇
という形からパラメータが渡っており使われています。
- 結論
/bookmarks
に?board_id=〇〇
を付けることができているのは、bookmarks_path(board_id: board.id)
の()内の記述によるもの。ということです。
コンソールで確認してみましょう。
irb(main):008:0> app.bookmarks_path
=> "/bookmarks"
irb(main):009:0> app.bookmarks_path(board_id: 1)
=> "/bookmarks?board_id=1"
irb(main):010:0> app.bookmarks_path(hoge: 'huga')
=> "/bookmarks?hoge=huga"
app.path名
と打つことで、appオブジェクトからURLを確認できます。
path名の後ろに(A: B)と入力すると、URLの末尾に ?A=B
が設定される仕組みになっています。
AやBの値は、どのようなpathにも自由に設定することができますので、今回はルーティング構造とは関係なく手動でパラメータを設定できるというケースでした。
Railsは非常に自由度の高いフレームワークですね。
ブックマーク解除ボタンのパーシャルを作成
<%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class: 'float-right', method: :delete do %>
<%= icon 'fas', 'star' %>
<% end %>
current_userに紐ついているbookmarksレコードの中から、渡されたboard
を使って掲示板のid(board_id)を探し出し、bookmarkレコードを取得します。(掲示板レコードではありません。なぜならこのあと生成されているdestroyアクションのURLパスの/bookmarks/:id
から、bookmarkコントローラのdestroyアクションでbookmarkレコードのidを取得したいから、bookmarkレコードを渡しておく必要があるのです。)そしてそれをbookmark_path
へのリクエストにパラメータとして渡します。
自分の掲示板にはブックマークボタンが表示されないように実装する
<% if current_user.own?(board) %>
<%= render 'crud_menus', board: board %>
<% else %>
<%= render 'bookmark_button', board: board %>
<% end %>
現在ログインしているユーザの掲示板かどうか判定しつつ、一致している場合は編集ボタンと削除ボタンのパーシャルを表示し、一致していない場合はブックマークボタンのっパーシャルを表示するように、_boardパーシャルに組み込みます。
ブックマークした掲示板の一覧表示画面を作成
おおまかなレイアウトは掲示板一覧ページと変わらないため、流用しましょう。
<% content_for(:title, t('.title')) %>
<div class="container pt-3">
<div class="row">
<div class="col-lg-10 offset-lg-1">
<!-- 検索フォーム -->
<form>
<div class="input-group mb-3">
<input class="form-control" placeholder="検索ワード" type="search"/>
<div class="input-group-append">
<input type="submit" value="検索" class="btn btn-primary"/>
</div>
</div>
</form>
</div>
</div>
<!-- 掲示板一覧 -->
<div class="row">
<div class="col-12">
<div class="row">
<% if @bookmark_boards.present? %>
<%= render @bookmark_boards %>
<% else %>
<p><%= t('.no_result') %></p>
<% end %>
</div>
</div>
</div>
</div>
コードを紐解く
重要な部分はこちらになります。
<%= render @bookmark_boards %>
上記コードは下の形を省略形になります。
<%= render partial: 'board', collection: @bookmark_boards %>
@bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
今までの私の認識では、呼び出すパーシャル名(_board.html.erb)と複数形sを除いたインスタンス名が一致する場合のみ省略形にできると思っていました。しかし今回の疑問に差し当たってRailsガイド/3.4.5 コレクションをレンダリングするを見ると、このような記述がありました。
使用するパーシャル名は、コレクション内のモデル名に基いて決定されます。実は、メンバが一様でない (さまざまな種類のメンバが入り混じった) コレクションにも上の方法を使用できます。この場合、コレクションのメンバに応じて適切なパーシャルが自動的に選択されます。
つまり今回のケースでは、boards_controller
のbookmarks
アクションから、Railsが良しなにBoardモデルを解釈して、_board.html.erb
を呼び出しているということになります。
@bookmark_boards
はBoardモデルのインスタンスであり、中身はboards
であると解釈されているということです。
コラム
n + 1を解消するためのリファクタリング手法を紹介します。
難しいので、今は無理に理解する必要は無いでしょう。
def index
# これだとuserはキャッシュしてくれるが、bookmarkはしてくれない
@boards = Board.all.includes(:user).order(created_at: :desc)
# これならbookmarkもキャッシュしてくれる
@boards = Board.all.includes([:user, :bookmarks]).order(created_at: :desc)
# .includes([:user, :bookmarks])がポイント
end
# BAD
# userを起点にbookmark_boardsを取りに行っているので毎回SQLが走ってしまう。
# コントローラ側でキャッシュしているのはあくまでもboard_idを元に検索したbookmarks。
def bookmark?(board)
bookmark_boards.include?(board)
end
# GOOD
# これならboardを起点にSQLを走らせて検索をかける。無駄なSQLが走らず、N+1が起きない。
def bookmark?(board)
bookmark_boards.pluck(:user_id).include?(id)
end
<%= link_to bookmark_path(board.bookmarks.find { |b| b.user_id == current_user.id }),
id: "js-bookmark-button-for-board-#{board.id}",
class:"float-right",
method: :delete,
remote: true do %>
<%= icon 'fas', 'star' %>
<% end %>
current_user.bookmarks.find_by(board_id: board.id)
のようにするとN+1問題が発生してしまうので単なるRubyのメソッドであるfindを使うようにしましょう。detectでもOK
終わりに
これで終わりです。特に重要だったところは、has_many :through 関連付け のところでしたね。お疲れさまでした!
参考記事
https://railsguides.jp/routing.html#restfulなアクションをさらに追加する
https://railsguides.jp/association_basics.html#has-many-through関連付け




コメント