【Rails】ブックマーク機能の実装

はじめに

今回、Railsで初めてブックマーク機能を実装したので、ブログにまとめていきます。

今回したいこと

Image from Gyazo
掲示板の⭐️マークをクリックすると、

Image from Gyazo
⭐️マークの色が変わって、ブックマーク登録されるという機能を実装していきます。

ER図

Image from Gyazo

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

このようにして、ブックマーク機能を実装しています。