【Rails】ブックマーク機能の実装
はじめに
今回、Railsで初めてブックマーク機能を実装したので、ブログにまとめていきます。
今回したいこと
掲示板の⭐️マークをクリックすると、
⭐️マークの色が変わって、ブックマーク登録されるという機能を実装していきます。
ER図
Bookmarkモデルを中間テーブルとして使用していきます。
まず、モデルの作成
中間テーブルであるBookmarkモデルを作成します。
$ rails g model Bookmark user:references board:references
referencesを使用することで、モデル間の関連付けであるbelongs_toを自動で追加してくれます。今回はUserモデルとBoardモデルにBookmarkモデルがbelongs_toで紐づいているという状況です。あるモデルが他のモデルに従属している(belongs_to)と宣言すると、2つのモデルのそれぞれのインスタンス間で「主キー - 外部キー」情報を保持しておくようにRailsに指示が伝わります。
マイグレーションファイル
class CreateBookmarks < ActiveRecord::Migration[5.2] def change create_table :bookmarks do |t| t.references :user, foreign_key: true t.references :board, foreign_key: true t.timestamps end # bookmarksテーブルに置いてuser_idとboard_idの組み合わせを一意性のあるものしている。 add_index :bookmarks, [:user_id, :board_id], unique: true end end
上記のように、外部キーの設定がされていますね。また、Bookmarkモデルは下記のようになります。
bookmark.rb
class Bookmark < ApplicationRecord # belongs_toは対象カラムに対するpresence: trueは自動で設定されている。 belongs_to :user # 外部キー user_id belongs_to :board # 外部キー board_id # user_id と board_idの組み合わせを一意性のあるものにしている validates :user_id, uniqueness: { scope: :board_id } end
このような形で、referencesはbookmark.rbにbelongs_toを付与してくれます。また、今回は1人のuserが何回もbookmarkするということはおこらないので、user_idとborad_idの組み合わせが一意性を保てるようなコードも入れてあります。ちなみに、先ほどのマイグレーションファイルでもuser_idとboard_idの組み合わせで一意性を保つためのコードを書いています。
それでは、マイグレーションからテーブルを作成しましょう。
$ rails db:migrate
UserモデルとBoardモデルのアソシエーションも書こう
user.rb
class User < ApplicationRecord # ここからが他のモデルとの関係性 has_many :boards, dependent: :destroy has_many :comments, dependent: :destroy has_many :bookmarks, dependent: :destroy # userのidを入れて、bookmarksメソッドを入れて、それぞれのboardを出す # 下記の記述はuser.bookmarks.map(&:board)これをしているのと一緒 has_many :bookmark_boards, through: :bookmarks, source: :board # 引数に渡されたものが、userのものであるか? def own?(object) id == object.user_id end # 引数に渡されたboardがブックマークされているか? def bookmark?(board) bookmark_boards.include?(board) end # board_idを入れてブックマークしてください def bookmark(board) # current_userがブックマークしているboardの配列にboardを入れる bookmark_boards << board end # 引数のboardのidをもつ、レコードを削除してください def unbookmark(board) bookmark_boards.destroy(board) end end
このコードは、Userモデルと他のモデルの関係を表しています。has_many関連付けは、他のモデルとの間に「1対多」のつながりがあることを示します。has_many関連付けが使われている場合、「反対側」のモデルでは多くの場合belongs_toが使われます。さきほどのbookmark.rbでbelongs_toのコードを書きましたね。has_many関連付けが使われている場合、そのモデルのインスタンスは、反対側のモデルの「0個以上の」インスタンスを所有することを表します。最後のコードは簡単に言うとbookmard_boardsメソッドを作り、bookmarksメソッドを使って、その中からそれぞれのboardを出力するというものです。後々活用していきます。また、ここで、さまざまなメソッドを定義しています。bookmark?(board)やbookmark(board)などのメソッドを定義しています。
Boardモデルは下記のようになります。
board.rb
class Board < ApplicationRecord belongs_to :user has_many :bookmarks, dependent: :destroy end
ルーティング
routes.rb
Rails.application.routes.draw do root 'static_pages#top' resources :boards do resources :comments, only: %i[create], shallow: true # /boards/bookmarksのURLを作っている。このURLのブックマークの一覧を表示する。 collection do get 'bookmarks' end end # ブックマークのcreateアクションとdestroyアクション resources :bookmarks, only: %i[create destroy] end
ブックマークされたものの一覧を/boards/bookmarksというURLに表示したいので、board :resoucesにネストして、上記のようなコードを書いています。また、ブックマークの関係を作ったり、消したりするためにcreateアクションとdestroyアクションを作成しています。
Veiwでの動きを確認していきます。
掲示板の⭐️マーク部分が分岐されるところから記載していきます。
<% if current_user.own?(board) %> <%= render 'crud_menus', board: board %> <% else %> <%= render 'bookmark_area', board: board %> <% end %>
上記のif文は,current_userが所有する掲示板ではない時に⭐️マークを表示するものです。 renderした先は、このようになっています。
<% if current_user.bookmark?(board) %> <%# bookmarkしていれば、ブックマークしているボタンのとこへ %> <%= render 'unbookmark', board: board %> <% else %> <%# bookmarkしていなければ、ブックマークしていないボタンのとこへ %> <%= render 'bookmark', board: board %> <% end %>
user.rbで定義したbookmark?メソッドを使用し、current_userが指定の掲示板をブックマークしていれば、色付き★ボタンのところへ、ブックマークしていなかったら☆ボタンのところは遷移します。
ブックマークしていない場合
_bookmark.hrml.erb
<div class='mr10 float-right'> <%= link_to bookmarks_path(board_id: board.id), method: :post , id: "js-bookmark-button-for-board-#{board.id}" do %> <i class="far fa-star"></i> <% end %> </div>
ブックマークされていないので、表示は☆ボタンを表示しています。link_toで囲んでbookmarksコントローラーのcreateアクションに遷移するpathが記述されています。つまり、☆ボタンを押すとbookmarksコントローラのcreateアクション動きます。
ブックマークしている場合
_unbookmark.html.erb
<div class='mr10 float-right'> <%= link_to bookmark_path(current_user.bookmarks.find_by(board_id:board.id)), method: :delete , id: "js-bookmark-button-for-board-#{board.id}" do %> <i class="fas fa-star"></i> <% end %> </div>
こちらは、ブックマークしていない場合と逆になり、色あり星ボタンを表示し、destroyコントローラが動くpathが記載されています。
ブックマークを実現するcreateアクションとdestroyアクション
createアクション
bookmarks_controller.rb
class BookmarksController < ApplicationController def create board = Board.find(params[:board_id]) current_user.bookmark(board) # 元のコード bookmark = current_user.bookmarks.build(board_id: params[:board_id]) # 元のコード bookmark.save! redirect_to boards_path, success: t('.success') end
createアクションはまず、送られてきたboard_idから結びつくboardを呼び出し、bookmarkメソッドの引数としてそれを使用し、ブックマークを実現しています。
user.rb
# board_idを入れてブックマークしてください def bookmark(board) # bookmarkのリレーションにboardを入れる、user_idはcurrent_userとするのだろう bookmark_boards << board end
destroyアクション
bookmarks_controller.rb
def destroy # 中間テーブルを取り出して、そのboardを取り出している。belong_toで紐づいているからboardで取り出せる。 board = current_user.bookmarks.find(params[:id]).board current_user.unbookmark(board) # 元々のコード current_user.bookmarks.find(params[:id]).destroy! redirect_to boards_path, success: t('.success') end end
destroyアクションでは、送られてきたidを利用し中間テーブルのレコードを一つ取り出し、そこから指定のboardを取り出しています。それをunbookmarkでdestroyしています。 user.rb
def unbookmark(board) bookmark_boards.destroy(board) end
このようにして、ブックマーク機能を実装しています。