ブックマーク機能の追加

ブックマーク機能の追加

掲示板にブックマーク機能を設定する手順は、以下になります。

①Bookmarkモデル作成(中間テーブル)
※中間テーブルとは?
多対多の関係にある2つのテーブルの間に挟まって、2つの組み合わせパターンだけをレコードとして保存する。 多対多を実装するためには、お互いがお互いの外部キーを知る必要がある。 UserとBoard(ブックマークの関係)は、中間テーブルを通しての関係になっているので、そのような時は、has_many thorughを使う(多対多を直接参照できるようにするもの。)

$ rails g model Bookmark user:references board:references


②作成したマイグレーションファイルを下記のように設定
マイグレーションファイルの(A)は、user_idとboard_idの組み合わせが一意だよってこと。(1人のユーザーが同じboardにブックマークするのを防ぐ為)
※ポイント
テーブルのカラムに一意性制約をかけるときは、インデックスの作成も必要になる。全てのデータを検索しないと、過去のデータと重複しているかわからない為。add_indexメソッドの中でunique: trueと記述する。

def change
    create_table :bookmarks do |t|
      t.references :user, foreign_key: true
      t.references :board, foreign_key: true

      t.timestamps
    end

    add_index :bookmarks, [:user_id, :board_id], unique: true    # (A)
end


③[Userモデル]と[Boardモデル]と[Bookmarkモデル]にアソシエーションとbookmarksテーブルの[user_id]と[board_id]が一意になるバリデーションを設定する

[app/models/user.rb]

  has_many :boards, dependent: :destroy  # (F)
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy  # (G)
  has_many :bookmark_boards, through: :bookmarks, source: :board  # (H)
[app/models/board.rb]

  belongs_to :user  # (C)
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy  # (D)
  has_many :bookmark_users, through: :bookmarks, source: :user  # (E)
[app/models/bookmark.rb]

  belongs_to :board  # (B)
  belongs_to :user  # (B)
  validates :user_id, uniqueness: { scope: :board_id }   # (A)

※ポイント
・Bookmarkモデルに[user_id]と[board_id]の組み合わせが一意になるように設定 (A)
scope は範囲を指定して、一意かどうかをチェックしてくれる。(1つのuser_idが同じboard_idを持たないという事になる)

・1つのブックマークは、1つの掲示板と1つのユーザーに属しているので、belongs_toを記載する。(B)

・1つの掲示板は、掲示板の作成者は1人なので、1人のユーザーに属しているので、belongs_toを記載する。 (C)

・1つの掲示板は、複数のブックマークを持てるので、このように記載。(dependent: :destroyは、親である掲示板が削除されたらbookmarksテーブルからブックマークを削除するように記述している。)(D)

・1つの掲示板は、bookmarksテーブル(user_idとboard_idを持っているテーブル)を通したbookmark_users(bookmarksテーブルを通したusersテーブル[この掲示板にブックマークしているユーザーのみのusersテーブル])を複数持っているので、このように記載する。(E)
関連名に既に[users]を使っているので、[bookmark_users]と名前を変えおり、通常の[モデル名の複数形]と関連名を定義していないので、source: :userでuserモデルを参照するとしている

・1人のユーザーは、複数の掲示板を投稿できるので、このように記載する。 (F)

・1人のユーザーは、複数のブックマークを持てるので、このように記載。(dependent: :destroyは、親であるユーザーが削除されたらbookmarksテーブルからブックマークを削除するように記述している。)(G)

・1人のユーザーは、bookmarksテーブル(user_idとboard_idを持っているテーブル)を通したbookmark_boards(bookmarksテーブルを通したboardsテーブル[このユーザーがブックマークしている掲示板のみのboardrsテーブル])を複数持っているので、このように記載する。(H)
関連名に既に[boards]を使っているので、[bookmark_boards]と名前を変えている通常の[モデル名の複数形]と関連名を定義していないので、source: :boardでboardモデルを参照するとしている

・belongs_toを設定すると対象カラムに対するpresence: trueは、自動で設定される。

has_many throughの使い方

has_many :関連名, through: :中間テーブル名
has_many :中間テーブル名


④ルーティングを設定する

[config/routes.rb]

  resources :boards do
    resources :comments, only: %i[create], shallow: true
    get 'bookmarks', on: :collection
    resources :bookmarks, only: %i[create destroy], shallow: true
  end

ブックマークしている掲示板の一覧ページを表示する際は、http://localhost:3000/boardsというURLにした方が直感的で分かりやすいので、このように設定したい。
このようにURLを設定するには、resourcesを使うと[new, create,index,show,edit,update,destroy]アクションに相当する決まった形のURLしか作れない為、違う形のURLを作成するのにcollectionを利用する。(URLにidを含めて記載する時は、memberを利用する。)


⑤ブックマーク一覧の掲示板を取得するアクションを以下のように定義

[app/controllers/boards_controller.rb]

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


⑥ブックマーク一覧のビューファイルを以下のように記述

[app/views/boards/bookmark.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 @favorites.present? %>
            <%= render @favorites %>   #(A)
        <% else %>
          <p><%= (t '.no_result') %></p>
        <% end %>
      </div>
    </div>
  </div>
</div>

↓⑦ 上記のビューファイル(A)の部分で@favorites(ブックマークされている掲示板の一覧を取得)をrenderに渡しているので、railsが推測して[board.html.erb]ファイルをrenderする。
[
board.html.erb]ファイルは、以下のように記載している。

[app/views/boards/_board.html.erb]

        <div class="col-sm-12 col-lg-4 mb-3">
          <div id="board-id-1">
            <div class="card">
              <%= image_tag board.image.thumb.url, class: 'card-img-top' %>
              <div class="card-body">
                <h4 class="card-title">
                  <%= link_to board.title, board_path(board) %>
                </h4>
                <% if current_user.own?(board) %>
                  <%= render 'crud_menus', {board: board} %>
                <% else %>
                  <%= render 'bookmark_button', {board: board} %>
                <% end %>
                <ul class="list-inline">
                  <li class="list-inline-item"><i class="far fa-user"></i><%= board.user.decorate.full_name %></li>
                  <br><li class="list-inline-item"><i class="far fa-calendar"></i><%= l board.created_at %></li></br>
                </ul>
                <p class="card-text"><%= simple_format(board.body) %></p>
              </div>
            </div>
          </div>
        </div>


⑧bookmarksコントローラにブックマークする時の処理を記載

[app/controllers/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


⑨ブックマークされているかの判断やブックマークする処理、ブックマークを解除するメソッドを以下のように定義

[app/models/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


⑩未ブックマークのボタンと済ブックマークのボタンを以下のように記述することで出し分ける

[app/views/boards/_bookmark_button.html.erb]

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

<div class='mr10 float-right'>
  <%= link_to board_bookmarks_path(board), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <i class="far fa-star"></i>
<% end %>
</div>
[app/views/boards/_bookmark.html.erb]

<div class='mr10 float-right'>
<%= link_to bookmark_path(board.bookmarks.find_by(user_id: current_user.id)), id: "js-bookmark-button-for-board-#{board.id}", method: :delete do %>
  <i class="fas fa-star"></i>
<% end %>
</div>

参考記事:

railsの中間テーブルでデータの組み合わせを一意にする方法 - Qiita

掲示板にお気に入り機能を実装する① - Ruby on Rails Learning Diary

【Rails】複数のカラムを使ったユニーク制約の方法 | Always be myself

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

【Rails】アソシエーションを図解形式で徹底的に理解しよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

中間テーブル|えーと|note

【Rails】多対多のアソシエーションに別名をつけたいあなたに - ひよっこエンジニアの雑多な日記

Rails 一意性制約のかけ方|Nori|note

ルーティングにアクションを追加 - Ruby on Rails入門