9
7

More than 1 year has passed since last update.

Rails お気に入り機能 解説

Last updated at Posted at 2021-12-13

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度しかお気に入りすることができなくなる。

app/models/bookmark.rb
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 2
2 

UserモデルとBoardモデルにアソシエーション(関連付け)をする

user.rb
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」が定義されるようになる。

boards_controller
  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
3 1 1
4 2 3
5 1 4

ルーティングの設定

routes.rb
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モデルにインスタンスメソッドを定義する

user.rb
  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を作成

boards_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('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アクションを追加

boards_controller.rb
def bookmarks
    @bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
  end 

・一覧を表示する際にN+1問題を起こさないために、include(user)で関連するユーザー情報も取得。

ビューの設定①お気に入りボタン

boards/_board.html.erb → 投稿がログインしているユーザーのものかを判断するファイル。

app/views/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(お気に入りボタン)」をレンダー。
・これで、ログインしているユーザーの投稿ならお気に入りボタンを表示させず、編集と削除ボタンを表示させ、ログインしているユーザーの投稿でなければ、お気にボタンを表示させることができている。

user.rb
 def own?(object)
    id == object.user_id
 end

パーシャルのbookmark_buttonを作成

app/views/boards/_bookmark_button.html.erb
<% if current_user.bookmark?(board) %>
  <%= render 'unbookmark', { board: board } %>
<% else %>
  <%= render 'bookmark', { board: board } %>
<% end %>

・userモデルに定義した「bookmark?(board)」を使用している。

user.rb
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投稿をお気に入りしているかしてないかを検証している。

app/views/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 %>

・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)))を付与することができる。

app/views/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 %>

・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))を付与することができる。

お気に入り一覧画面

app/views/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 @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を返す。

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7