##実装したいこと
・掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除出来る機能。
・ユーザーがブックマークした掲示板を一覧できるページを実装。
##お気に入り機能実装の流れ
①中間テーブルとなるBookmarkモデルの実装
②Userモデルにbookmarkの定義を追加
③Bookmarksコントローラーの実装
④Boardコントローラーにbookmarkの定義を追加
⑤Routingの設定
⑥Viewの実装
##①中間テーブルとなるBookmarkモデルの実装
###Bookmarkモデルの仕組み(多対多)
UserとBookmarkとBoardの関係
Bookmarkモデルを実装する前に、モデルの関係を確認する。
・ユーザーはたくさんの掲示板をブックマークすることができる。
・反対に、掲示板はたくさんのユーザーにフォローされることができる。
つまり、UserもBoardもBookmarkをたくさん持っているということになる。このような関係を多対多の関係
と言う。
###中間モデル(Bookmarkモデル)の作成
多対多のモデルを実装するにはお互いのforeign_keyを知っている必要がある。
rails g model Bookmark user:references board:references
class CreateBookmarks < ActiveRecord::Migration[6.0]
def change
create_table :bookmarks do |t|
t.references :user, null: false, foreign_key: true
t.references :board, null: false, foreign_key: true
t.timestamps
end
add_index :bookmarks, [:user_id, :board_id], unique: :true
end
end
add_index :bookmarks, [:user_id, :board_id], unique: :true
でuser_idとboard_idの組み合わせがuniqueであることを設定。
▶︎migrationファイルのadd_indexは何なのか
boardモデルとuserモデルにも追加記入。
#この一文を追加
has_many :bookmarks, dependent: :destroy
#以下2文を追加
has_many :bookmarks, dependent: :destroy
# ↓お気に入りにしている掲示板を取得する
has_many :bookmarks_boards, through: :bookmarks, source: :board
throughオプション
によりbookmarkテーブル経由でboardテーブルにアクセスできるようになる。
多対多のモデルを作った時に必ずと言っていいほど活躍するオプションである。
◆has_many :through とは
多対多で別のモデルと関連している
従属している第3のモデル(結合モデル)を介して、対象のモデルと多対多の関連付けになっている
◆source とは
has_many :through関連付けの関連付け元(従属するモデル)名を指定する
またユーザーが同じ掲示板をお気に入り登録しないようにunique: :true
をつける必要がある。
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
validates :user_id, uniqueness: { scope: :board_id}
end
scope
をつけることによって、1投稿に対して、1人のユーザーが1ブックマーク(いいね)しかできなくする。
▶︎【uniqueness: scopeの使い方】ブックマーク、いいね機能実装に使える
##②Userモデルにbookmarkの定義を追加
controllerの可読性を上げるためにまずはモデルにお気に入り登録のギミックを定義する。
# お気に入り追加
# <<で引数で渡した掲示板の情報がbookmark_boardsに入っている
def bookmark(board)
bookmarks_boards << board
end
# お気に入りを外す
def unbookmark(board)
bookmarks_boards.delete(board)
end
# お気に入り登録しているか判定するメソッド
def bookmark?(board)
bookmarks_boards.include?(board)
end
・bookmarkメソッド
掲示板の情報のレコードが引数boardに格納されbookmarks_boardsに<<演算子
で追加されている。
<<
は指定されたオブジェクトの末尾に破壊的に追加できるメソッド。
強制的に追加されて保存もされているのでsaveメソッドなどは必要ない。
bookmarks_boards << board
はbookmarks.create!(board_id: board.id)
と同様の処理がされている。
・unbookmarkメソッド
bookmarks_boards
からboardの引数に入っている掲示板idが入ったレコードを探し出して削除(delete)するメソッド。
・bookmark?メソッド
bookmarks_boards
にboardの引数に入っている掲示板idが含まれているレコードがあるかどうか判定するメソッド。
なぜBookmarkモデルではなくUserモデルにbookmarkを定義するのか。
もし後から機能を(無料会員はブックマークが10個まで、ブックマークしたら、ブックマークされた側に通知とか)追加するとなった場合、Userモデルの方が後々管理しやすいからである。
またBookmarkモデル
は、Userモデル
とBoardモデル
の中間テーブルなので、イメージとしては紐付け役として存在するので実態が薄い。そのためUserモデル
もしくはBoardモデル
に記入した方が分かりやすい。
もっと細かく言うとbookmarkを
・Userモデル
に記入した場合、user.bookmark(board)
となる。
・Boardモデル
に記入した場合、board.bookmarked_by(user)
となる。
この2つのどちらが分かりやすいか?と見比べた時に、直下感的にもuser.bookmark(board)
の方が見やすいのでよりUserモデル
に記入した方がいいとなる。
##③Bookmarksコントローラーの実装
viewの必要がないのでcontrollerファイル
だけ作って記載する。
class BookmarksController < ApplicationController
def create
board = Board.find(params[:board_id])
current_user.bookmark(board)
redirect_back fallback_location: root_path, success: 'ブックマークしました'
end
def destroy
board = current_user.bookmarks.find_by(params[:id]).board
current_user.unbookmark(board)
redirect_back fallback_location: root_path, success: 'ブックマークを外しました'
end
end
redirect_back fallback_location:
を使うと、直前のページにリダイレクトをしてくれる。
▶︎【Rails】redirect_backとは
##④Boardコントローラーにbookmarkの定義を追加
無駄にSQL文を発行させない様にincludes(:user)
を記載して関連するuserの情報も取得している。(n + 1問題の解消)
def bookmarks
@bookmark_boards = current_user.bookmarks_boards.includes(:user).order(created_at: :desc)
end
##⑤Routingの設定
Rails.application.routes.draw do
root 'static_pages#top'
resources :users
get 'login', to: 'user_sessions#new'
post 'login', to: 'user_sessions#create'
delete 'logout', to: 'user_sessions#destroy'
resources :boards do
resources :comments,only: [:create,:destroy], shallow: true
collection do
get :bookmarks
end
end
resources :bookmarks, only: %i[create destroy]
end
railsの基本的なアクションはresources
で定義した時に作られる7つのアクションだが、更に別のアクションを追加したい時はcoolection(またはmember)を利用する。(この場合はbookmarksというアクションを追加したいということになる)
collectionとmemberの違いは、生成するroutingに、:idの有無で決まる。
menber
はidが有り、collection
はidが無い。
▶︎【Rails】memberとcollectionの違い
##⑥Viewの実装
☆ボタンの実装
まずはブックマークをするボタンを作成。(見やすいようにrbファイルにしているが本来はerbファイル。)
<%= link_to board_bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
<%= icon 'far', 'star' %>
<% end%>
次にブックマーク解除ボタンを作成。
<%= link_to board_bookmarks_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", method: :delete do %>
<%= icon 'fas', 'star' %>
<% end %>
↑の2つ作ったボタンをパーシャルでブックマークするボタンと、ブックマーク解除ボタンを切り替えるページを作成。
<% if current_user.bookmark?(board) %>
<%= render 'bookmarks/unbookmark', board: board %>
<% else %>
<%= render 'bookmarks/bookmark', board: board %>
<% end %>
user.rb
で定義されたbookmark?メソッド
がここで使われる。 掲示板がブックマークされていたら掲示板解除ボタン、掲示板がブックマークされていなかったらブックマーク登録ボタンに切り替わる仕組みになる。
この↑完成した_bookmark.html.erb
を掲示板のパーシャルに記入する。
<% if current_user.own?(board) %>
<div class='mr10 float-right'>
<%= render 'crud_menus', board: board %>
<% else %>
<%= render 'bookmarks/bookmark_area', board: board %>
</div>
<% end %>
掲示板がログインしているユーザーのものだったらcrud_menus
ボタンが表示され、ユーザーのものではなかった場合はお気に入りボタンに切り替わるようになる。
お気に入り掲示板一覧機能ページの作成
boards
のbookmarks.html.erbに注意。
<% 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 partial: "board", collection: @bookmark_boards %>
<% else %>
<p>ブックマーク中の掲示板がありません</p>
<% end%>
</div>
</div>
</div>
</div>
##参考記事
中間テーブルを使ったお気に入り機能の実装!
【Rails初心者必見】has_manyでデータ管理を行おう!
Rails4のhas_many throughで多対多のリレーションを実装する