Rails お気に入り機能 手順 (自分用)
#中間モデルの作成
・UserモデルとBoardモデルの中間モデル(Bookmarkモデル)を作成する。
・中間モデル(Bookmarkモデル)を作成することによってuser_idとboard_idを抱えることができ、お気に入り機能を追加するに当たって便利になる。
###Bookmarkモデルの作成
$ rails g model Bookmark user:references board:references
・user_idとboard_idの外部キーが欲しいので、「user:references board:references」を付ける。
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
t.index [:user_id, :board_id], unique: true #追記
end
end
end
t.index [:user_id, :board_id], unique: true
・ユニーク制約(unipue)を付けることによって、DBにおいてデータ一を追加する際の制約に一意性制約(1つしか存在しないこと)を付けることができる。
・例えば「user_id/1」と「board_id/2」の組み合わせはひとつしかなく、2つ作ろうとしたらエラーを返す。という制約を作れる。これで「user_id/1」のユーザーが「board_id/2」に1度しかお気に入りすることができなくなる。
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
validates :user_id, uniqueness: { scope: :board_id } #追記
end
validates :user_id, uniqueness: { scope: :board_id }
・「1ユーザー」は「1投稿」に「1お気に入り」しかできないというバリデーション。
・「validates :user_id, uniqueness」だけだと、1ユーザー(user_id)は1お気に入りしかできないということになって位しまうので、「scope」を付けることによって範囲を指定することができる。
・ { scope: :board_id }を加えることでBookmarkモデルでレコードを作る際に、特定のboard_idに対しては特定のuser_idは2つ以上入れることができない。例えば、Bookmarkモデルでレコードを作る際に、1つの投稿に対して、お気に入りをするのであらかじめ、「board_id」カラムにはすでに2が入っているとすると(下記の図)「user_id/3」が「board_id/2」に対してお気に入りしようとしても、すでに「id/1」のレコードで一度お気に入りしているので、お気に入りできない。→user_idとboard_idの組み合わせで被りをなくす。
#####Bookmarkモデルのテーブル
id | user_id | board_id |
---|---|---|
1 | 3 | 2 |
2 | 2 |
#UserモデルとBoardモデルにアソシエーション(関連付け)をする
has_many :boards, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :bookmarks, dependent: :destroy #追記
has_many :bookmark_boards, through: :bookmarks, source: :board #追記
has_many :bookmarks, dependent: :destroy
・userは多くのbookmaksを持っている。(has_many :bookmarks)
・1つのuserが削除されたらそのuserが持っているbookmarksも削除される。(dependent: :destroy)
has_many :bookmark_boards, through: :bookmarks, source: :board
・「has_many :bookmark_boards」でUserモデルは多くのbookmark_boardsを持っている。
・ 「through: :bookmarks」bookmarksモデルを通して、
・「source: :board」boradモデルからデータを参照する。
*これはお気に入り一覧を取得するときに使う。
*has_many throuth オプションをつけたことによって、Userクラスのインスタンスメソッド「bookmark_boards」が定義されるようになる。
def bookmarks
@bookmarks_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)#後で定義する
end
・currrent_userにuser.rbに設定したbookmark_boardsメソッドを使うことで、一覧を取得する際に中間テーブルbookmarksのuser_idを見に行き、user_idが1のレコード(current_userのidが1とする)を持っているboard_idを取得することができるようになる。下の図だと、id1,3,5のレコードを取得できる。
・これにより、ログインしているユーザー(current_user)のお気に入り一覧を取得できる。(中間テーブルbookmarksにはuser_idとboard_idの組み合わせでお気に入りを作っているから)
#####Bookmarkモデルのテーブル
id | user_id | board_id |
---|---|---|
1 | 1 | 2 |
2 | 3 | 1 |
3 | 1 | 1 |
4 | 2 | 3 |
5 | 1 | 4 |
#ルーティングの設定
resources :users, only: %i[new create]
resources :boards do
resources :comments, only: %i[create update destroy], shallow: true
collection do #ここから追記
get :bookmarks
end #ここまで
end
resources :bookmarks, only: %i[create destroy] #追記
collection do #ここから追記
get :bookmarks
end #ここまで
・お気に入り一覧の「boards#bookmarks」を作るためのルーティング。「collection do」を付けることによって、親のidをパスに含めないようにできる。お気に入り一覧はuser.rbに設定した「bookmark_boards」メソッドを使ってこれからboardsコントローラーに@bookmarks_boardsインスタンス変数を作るので深いネストをしてなくても良い。
resources :bookmarks, only: %i[create destroy] #追記
・お気に入りを作成、削除する際も
・
#bookmarksのコントローラーを生成
$ rails g controller bookmarks create destroy
お気に入り機能はcreate(作成)、destroy(削除)だけ作る。
#Userモデルにインスタンスメソッドを定義する
def bookmark(board)
bookmark_boards << board
end
def unbookmark(board)
bookmark_boards.destroy(board)
end
def bookmark?(board)
bookmark_boards.include?(board)
end
def bookmark(board)
bookmark_boards << board
end
・引数で渡したboardレコードを中間テーブルに自動的に保存している。この中でsaveメソッドも自動で行っているので記述する必要はない。
def unbookmark(board)
bookmark_boards.destroy(board)
end
・引数で渡したboardレコードを中間テーブルからdestroy(削除)する。
def bookmark?(board)
bookmark_boards.include?(board)
end
・お気に入りしているかどうかを判断するメソッド
・include?で引数で渡したboardがbookmark_boardsに含まれているのかを判断し、含まれていればtrue含まれていなければfalseを返す。
#bookmarksコントローラーにcreateとdestroyを作成
class BookmarksController < ApplicationController
def create
board = Board.find(params[:board_id])
current_user.bookmark(board)
redirect_back fallback_location: root_path, success: t('defaults.message.bookmark')
end
def destroy
board = current_user.bookmarks.find(params[:id]).board
current_user.unbookmark(board)
redirect_back fallback_location: root_path, success: t('defaults.message.unbookmark')
end
end
board = Board.find(params[:board_id])
・Boardテーブルからパスで送られてくる(/boards/:board_id/comments)のboard_idのレコードを取得して「board」に代入している。
current_user.bookmark(board)
・userモデルに定義した「current_user.bookmark(board)」メソッドを使っている。引数のboardには上記で定義したboardレコードが入っていてそこからboard_idを取得。current_userでuser_idを取得し、中間テーブルに保存している。*<<を使っていることでsaveを使わずに保存することができている。
board = current_user.bookmarks.find(params[:id]).board
・current_userからuser_idを取得し、パスから送られてくるbookmarksテーブルのid(/bookmarks/:id)を取得。boardからboard_idを取ってきて変数boardに代入。これで中間テーブルに保存されているレコードのuse_id(ログインしているユーザー)とboard_id(コメントしている投稿)が一致していることを確認している・
current_user.unbookmark(board)
・userモデルで定義した「unbookmark(board)」を使用。
・current_userのuser_idを取得して中間テーブルbookmarksのuser_id(current_userで取得したuser_id)を見に行き、上記で取得したbookmarkレコード(board)と照らし合わせて一致しているものを削除。
#ユーザーのお気に入り一覧表示のためのbookmarksアクションを追加
def bookmarks
@bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
end
・一覧を表示する際にN+1問題を起こさないために、include(user)で関連するユーザー情報も取得。
#ビューの設定①お気に入りボタン
boards/_board.html.erb → 投稿がログインしているユーザーのものかを判断するファイル。
<% if current_user.own?(board) %>
<%= render 'crud_menus', board: board %>
<% else %>
<%= render 'bookmark_button', board:board %>
<% end %>
・userモデルに定義したown?(object)メソッドを使用している。
・引数で渡したobject(board)のuser_idとcurrent_userのuser_idが一致しているのかを検証。
・一致していれば、パーシャルの「crud_menus(編集と削除ボタン)」をレンダー。
一致していなければ、パーシャルの「bookmark_button(お気に入りボタン)」をレンダー。
・これで、ログインしているユーザーの投稿ならお気に入りボタンを表示させず、編集と削除ボタンを表示させ、ログインしているユーザーの投稿でなければ、お気にボタンを表示させることができている。
def own?(object)
id == object.user_id
end
#パーシャルのbookmark_buttonを作成
<% if current_user.bookmark?(board) %>
<%= render 'unbookmark', { board: board } %>
<% else %>
<%= render 'bookmark', { board: board } %>
<% end %>
・userモデルに定義した「bookmark?(board)」を使用している。
def bookmark?(board)
bookmark_boards.include?(board)
end
・current_userがお気に入りしていたら、パーシャルの「unbookmark(お気に入り解除ボタン)」をレンダー。
・current_userがお気に入りしていなければ、パーシャルの「bookmark(お気に入りボタン)」をレンダー。
*bookmark_bordsを通して引数で渡されたboardレコードのboard_idを取得。include?で、この投稿はbookmark_boardsに含まれているかを確認。含まれていればtrue含まれていなければ、falseを返す。current_userをつけることで中間テーブルにboard_idとuser_idに同じ組み合わせがないか確認し、なければfalse(render 'bookmark')同じ組み合わせがあればtrue(render 'umbookmark')。→ログインしているユーザーが1投稿をお気に入りしているかしてないかを検証している。
<%= 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 %>
・link_to bookmark_pathで「bookmarks#destroy」にデータを送っている。
・「(current_user.bookmarks.find_by(board_id: board.id))」でcurrent_user(ログインしているユーザー)のからusre_idを取得し、board_idにはパスから受け取れるパラメーターでその投稿のidを入れている。これを引数に「bookmarks#destroy」にデータを送っている。
・method: :deleteを指定することで、HTTPメソッドを指定し、削除を行う。
*link_toで、パス(bookmark_path)にパラメータ((current_user.bookmarks.find_by(board_id: board.id)))を付与することができる。
<%= 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 %>
・link_to bookmarks_pathで「bookmarks#create」にデータを送っている。
・「(board_id: board.id)」でbookmarksテーブルのboard_idにその投稿のidを入れている。これを引数に「bookmarks#create」へデータを送っている。
・method: :postを指定することで、HTTPメソッドを指定し、削除を行う。
*link_toで、パス(bookmarks_path)にパラメータ((board_id: board.id))を付与することができる。
#お気に入り一覧画面
<% 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 @bookmark_boards %>
<% else %>
<p><%= t('.no_result') %></p>
<% end %>
</div>
</div>
</div>
</div>
・boardsコントローラーで作成した、インスタンス変数「@bookmark_boards」をrenderしている。
・「<% if @bookmark_boards.present? %> 」→「present?」は@bookmark_boardsに中身があるかを検証している。中身があればtrue,なければfalseを返す。