チワワかわいいブログ

RUNTEQでrails勉強する日々の記録

2021/01/07 掲示板のbookmarkの追加

やること

掲示板をブックマーク/解除出来る機能の追加
ブックマークの一覧表示の機能を追加

考えること

一つの掲示板に対し、ブックマークは複数
一つのユーザーに対し、ブックマークは複数
一つのブックマークに対し、掲示板はひとつ、ユーザーはひとつ
bookmarkテーブルには掲示板とユーザーIDのカラムを持たせ、bookmarkのcreateアクションでそれを 保存し、掲示板の中でbookmarkテーブルにその掲示板のidがあるかどうかでだしわけできるようにすれば良い?
bookmarkのカラムは以下の通り作成。

class CreateBookmarks < ActiveRecord::Migration[5.2]
  def change
    create_table :bookmarks do |t|
      t.references :board_id, foreign_key: true
      t.references :user_id, foreign_key: true

      t.timestamps
    end
    add_index :bookmarks, :board_id
    add_index :bookmarks, :user_id
    add_index :bookmarks, [:board_id, :user_id], unique: true
  end
end

board/userモデルにhas_many、bookmarkモデルにbelongs_toをそれぞれ記述。
bookmarksコントローラを作成し、パーシャルをbookmark_area.html.erb(showテンプレートに記述する用)、 bookmark.html.erb(ブックマーク済ボタンを表示する用)、unbookmark.html.erb(未ブックマークを表示する用)で3つ作成。
やることは、

  • 現在のユーザーがそのboardに紐づくbookmarkを持っているか判断するメソッドを作り、ボタンのだしわけができるようにする
  • フォームからboard_id、user_idを取得し、bookmarkのcreateアクションでbookmarkモデルに保存(このフォームはbookmark.html.erbに記述)
  • 削除用のdestroyアクションも作る(フォームはunbookmark.html.erbに作成)
  • ブックマークボタンを表示したい掲示板一覧に、bookmark_area用のパーシャルを追加、この中で、bookmark有無によって表示をだし分けできるようにメソッドを作っていく
  • 現在のユーザーのブックマーク一覧を取得し、表示するページを作る
    やったこと

    ルーティングの追加

resources :boards, only: %i[index new create show edit destroy update] do
    resources :comments, shallow: true
    resources :bookmarks, shallow: true
    collection do
      get :bookmarks
    end
end

アソシエーションの追加

#user.rb
has_many :bookmarks, dependent: :destroy

#board.rb
has_many :bookmarks, dependent: :destroy

#bookmark.rb
belongs_to :board
belongs_to :user

切り分け用のメソッド

#board.rb
def bookmarked?(user)
    bookmarks.where(user_id: user.id).exists?
end

ブックマークボタン作成

<% unless current_user.own?(board) %> #掲示板が現在のユーザーのものでなければブックマークボタンを表示
  <% if board.bookmarked?(current_user) %> #現在のユーザーがブックマークしている掲示板かを判定
    <%= link_to board_bookmarks_path(board),method: :post, local: true, id: "js-bookmark-button-for-board-#{board.id}" do %> #ネストしている
      <i class="far fa-star"></i>
    <% end %>
  <% else %>
    <%= link_to bookmark_path(board),method: :delete, local: true, id: "js-bookmark-button-for-board-#{board.id}" do %> #shallowオプション
      <i class="fas fa-star"></i>
    <% end %>
  <% end %>
<% end %>

create、destoryアクションを作成

class BookmarksController < ApplicationController
  def create
    board = Board.find(params[:board_id])
    board.bookmarks.create!(user_id: current_user.id)
    redirect_to boards_path, success: t('.success')
  end

  def destroy
    bookmark = Bookmark.find_by(board_id: params[:id], user_id: current_user.id)
    bookmark.destroy!
    redirect_to boards_path, success: t('.success')
  end
end

これで一通り動くようになった!回答例見るとあまりいい出来ではないようなので、解説見ながらリファクタリング

改修

boardとuserを関連付ける。

#user.rb
has_many :bookmarks, dependent: :destroy
has_many :bookmark_boards, through: :bookmarks, source: :board

railsチュートリアルで出てきたけど全然理解できていなかったhas_manyのthroughオプションを使う
userモデル、boardモデルそれぞれに関連づけられているbookmarkモデルを通じてuserとboardを関連づける
この関連付けを行うことでbookmarkにあるboardの情報を、bookmarkを介してuserから取得できるようになる
具体的には

user_object.bookmark_boards.all

のように、第一引数をメソッド名として、userがブックマークしている掲示板の情報を取得するというようなメソッドが使えるようになる
続く引数でなんの情報が欲しいかを指定しているため、第一引数のメソッド名はなんでもいいが他と名前が被らないように指定する必要がある
これを使って、userがブックマークしている掲示板一覧を出すメソッドを書き換える

#board.rb
def bookmarked?(board)
    bookmark_boards.include?(board) #メソッドの引数に指定したboardがレシーバーのuserがブックマークしている中に含まれているか?
end

userオブジェクトのインスタンスメソッドになるため、user.rbに変更
これに応じてviewも書き換え

<% if current_user.bookmarked?(board) %>

また、create、editアクションもthroughオプションで使えるようになったメソッドを使って書き換える
コントローラの可読性をよくするために、メソッドはuser.rbに記載

#user.rb
def bookmark(board)
   bookmark_boards << board
end

def unbookmark(board)
   bookmark_boards.delete(board)
end

collection<<メソッドは、1つ以上のオブジェクトをコレクションに追加します。このとき、追加されるオブジェクトの外部キーは、呼び出し側モデルの主キーに設定されます。

user_object.bookmark(board) で、user_idを外部キーとしてboardがbookmarkに追加される

# 中間テーブルを削除する
bookmarks.destroy(bookmarks)

# お気に入りの掲示板の集合から対象の掲示板を削除する
bookmark_boards.destroy(board)

destroyアクションは中間テーブル(bookmark)を削除するか、userがブックマークしている掲示板をbookmarkから削除するか、どちらでも挙動は同じ

#bookmarks_controller.rb
class BookmarksController < 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('.success')
  end
end

redirect_back fallback_location:で、元のページにリダイレクト、例外ならrootへという指定ができる(アクションが例外を出した時用?)