[Rails] 掲示板にブックマーク機能の実装方法

学習記録

はじめに

掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除できる機能を実装したいとします。

今回は非常に難しい実装となったため、順を追って詳しく説明していきたいと思います。

間違いがあれば何なりとご指摘くださいませ。

必要な作業

  • Bookmarkモデルを作成する
  • バリデーションの追加
  • アソシエーションの設定
  • ルーティングの設定
  • コントローラの設定
  • ビューファイルの作成

Bookmarkモデルを作成する

今回のように「どのユーザー」が「どの投稿」をブックマークしたかを記録する為に、データベースに「user_id」と「post_id」の2つのカラムを持つbookmarkテーブル を用意しましょう。

中間テーブルとは?

2つのテーブルの間をつなぐイメージです。

iduser_idboard_id
132
225
316

上記のように両方のテーブルの外部キーとして持ち合わせています。

$ 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: :trueuser_idboard_idの組み合わせが一意性を持つことになります。

バリデーションの追加

class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
  validates :user_id, presence: true, uniqueness: { scope: :board_id }
end

一人のユーザが同じ掲示板に対するブックマークは1つのみなので、user_idboard_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ガイドを見ておきましょう。

Active Record の関連付け - Railsガイド
Active Recordが提供するすべての関連付け機能(アソシエーション)について解説します。

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します。

<< メソッドについて詳しくはこちらを参照ください。

Array#<< (Ruby 3.2 リファレンスマニュアル)
指定された obj を自身の末尾に破壊的に追加します。

また、この演算子では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_controllercreateアクションに送る必要があると思います。


  • ルーティングを確認
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=〇〇 という形でデータが渡っています。

https://i.gyazo.com/aafefcb521f064de6bca2b6384bdc937.png

  • 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_controllerbookmarksアクションから、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

Enumerable#detect (Ruby 3.2 リファレンスマニュアル)
要素に対してブロックを評価した値が真になった最初の要素を返します。

終わりに

これで終わりです。特に重要だったところは、has_many :through 関連付け のところでしたね。お疲れさまでした!

参考記事

https://railsguides.jp/routing.html#restfulなアクションをさらに追加する

https://railsguides.jp/association_basics.html#has-many-through関連付け

railsで多対多のアソシエーションの作り方と、出来ること - Qiita
この記事で説明していることアソシエーションでどんなことが出来るかアソシエーションとは?多対多のアソシエーションとはなにかrailsで多対多のアソシエーションを作り方アソシエーション(関連付…
Active Record バリデーション - Railsガイド
Active Recordのバリデーション機能について解説します。
uniqueness: scope を使ったユニーク制約方法の解説 - Qiita
#uniqueness: scope を使ったユニーク制約方法の解説uniqueness: scopeを利用して一意性検証をする方法について解説します。目次動作環境実装例解説複数のsco…
超実践型オンラインエンジニア育成スクール | RUNTEQ(ランテック)
エンジニア育成オンラインスクールならRUNTEQ(ランテック)。業務未経験者が転職時に求められるプログラミングスキル、コミュニケーションスキル、チーム開発スキル、カルチャーフィットを総合的に学び、就職後に求められる事業を推進する力を身に付けることができます。

https://railsguides.jp/routing.html#コレクションルーティングを追加する

コメント

タイトルとURLをコピーしました