ブックマーク機能の追加

アソシエーション

ユーザーは複数の掲示板をブックマークでき、掲示板は複数のユーザーにブックマークされるというようにモデル間で多対多の関係がある時、中間テーブル(今回の場合はブックマーク)を作成してモデル間の情報をやり取りする このテーブルによって、今回ならどのユーザーがどの掲示板と関連づいているのかという情報を管理することで、ユーザーが掲示板をブックマークといった機能を実装できる UserモデルとBoardモデルとBookmarkモデルの関係は以下のようになる User : Bookmark = 1 : 多 Board : Bookmark = 1 : 多

bundle exec rails g model Bookmark user:references board:references

を実行、bookmarkのモデルファイルとマイグレーションファイルを作成

#bookmark.rb
class Bookmark < ApplicationRecord
   belongs_to :user
   belongs_to :board
 
   validates :user_id, uniqueness: { scope: :board_id }
 end

uniqunessヘルパーのscopeオプションにboard_idを指定することで、各掲示板(board_id)別にユーザー(user_id)との関係性を一意にすることができる

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

      #user_idとboard_idカラムにuniqueインデックスを作成して、dbレベルで一意性を保つ
      t.index [:user_id, :board_id], unique: true
      t.timestamps
    end
  end
end

これにより一人のユーザーが同じ投稿を複数回bookmarkできないようにしている

bundle exec rails db:migrate

を実行して中間テーブルは作成完了

#user.rb
class User < ApplicationRecord
  #dependent: :destroyでユーザーが削除された時、そのユーザーに関連する情報を削除するようにしている
   has_many :bookmarks, dependent: :destroy
  #throughオプションでbookmarkテーブルを経由してuserモデルとアソシエーションを作成して、bookmark_boardsは仮のテーブル名なので、sourceオプションで参照するテーブルを指定している
   has_many :bookmark_boards, through: :bookmarks, source: :board
end

ブックマークの処理

ブックマークの処理はモデルに記載すること 理由はコントローラーに記載すると可読性が落ちるため

#user.rb
class User < ApplicationRecord
  #<<演算子でbookmarks.create!(board_id: board.id)と同様の処理を行う
  def bookmark(board)
    bookmark_boards << board
  end

  def unbookmark(board)
    bookmark_boards.destroy(board)
  end
  #ログインしているユーザーに紐づいたブックマークのレコードに引数で渡された掲示板モデルと紐づいたレコードが存在するか。存在すればユーザーはその掲示板をブックマークしていると判定、存在しなければブックマークされていないと判定できる
  def bookmark?(board)
    bookmark_boards.include?(board)
  end
end

ルーティングの設定

今回ブックマーク一覧ページをboards/bookmarksにルーティングを設定したい urlから何を表示しているか推測しやすくするため また、bookmarksコントローラのcreateとdestroyアクションだけルーティングを設定したいので

#routes.rb
resources :boards do
  collection do
    get :bookmarks
  end
resources :bookmarks, only; %i[create destroy]

とcollectionオプションを使うことでboardsリソースの中にbookmarksアクションを追加できる、この時bookmarks_boards_urlやbookmarks_boards_pathといったルーティングヘルパーも使えるようになる。

Bookmarkコントローラ作成

bundle exec rails g controller bookmarks create destroy

を実行してbookmarks_controller.rbを作成して、以下を記述

#bookmarks_controller.rb
class BookmarksController < ApplicationController
  #ブックマークボタンをクリックされた掲示板を取得して、Userモデルのインスタンスメソッドとして作成したbookmarkメソッドの引数に渡してブックマークする
  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
  #ログインしているユーザーに関連づいてクリックしたbookmarksモデルに関連づいたboardモデルを取得してunbookmarkメソッドの引数に渡してブックマーク解除をしている
  def destroy
    @board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(@board)
    redirect_back fallback_location: root_path, success: t('.success')
  end
end

redirect_backを使うとユーザーを直前のページに戻すことができる。この時fallback_locationは必ず設定する必要がある。HTTP_REFERER(リンク元のURLが入っているためどこのURLからアクセスしてきたかを知ることができる)がブラウザが必ずしも設定しているとは限らないため

boards_controllerでbookmarksアクションの実装

#boards_controller.rb
class BoardsController < ApplicationController
    def bookmarks
      @bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
    end
end

ビュー実装

#boards/_board.html.erb
                 中略
<% if current_user.own?(board) %> #ログインしているユーザーが掲示板作成者であるならcrud_menusのパーシャルを表示する、そうでなければ、bookmark_buttonのパーシャルを表示する
  <%= render 'crud_menus', board: board %>
<% else %>
   <%= render 'bookmark_button', board: board  %>
 <% end %>

#boards/_bookmark.html.erb
user.rbにて作成したbookmark?メソッドによってログインしているユーザーが掲示板をブックマークしているか判定、ブックマークしていたら、unbookmarkパーシャルを表示、そうでなければbookmarkパーシャルを表示
<% if current_user.bookmark?(board) %>
   <%= render 'unbookmark', { board: board } %>
<% else %>
   <%= render 'bookmark', { board: board } %>
<% end %>

#boards/_bookmark.html.erb
<%= 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 %>

#boards/_unbookmark.html.erb
<%= 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 %>

#boards/bookmarks.html.erb
                        中略
#ブックマークされた掲示板があればbookmark_boardパーシャルを表示、なければ、ブックマーク中の掲示板がありませんと表示
<% if @bookmark_boards.present? %>
  <%= render @bookmark_boards %>
<% else %>
   <p><%= t('.no_result') %></p>
 <% end %>

参考文献

Active Record の関連付け - Railsガイド

https://railsguides.jp/layouts_and_rendering.html#redirect-to%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B

https://railsguides.jp/routing.html#%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B

https://qiita.com/niwa1903/items/8a0374155cd18adbd7cf

uniqueness: scope を使ったユニーク制約方法の解説 - Qiita

Railsでお気に入り機能の実装方法をまとめてみる(初心者向け) - Qiita