LoginSignup
5
4

More than 1 year has passed since last update.

<<演算子を使って、掲示板にブックマーク(いいね)機能実装手順

Last updated at Posted at 2022-01-07

実装したいこと

・掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除出来る機能。
・ユーザーがブックマークした掲示板を一覧できるページを実装。

お気に入り機能実装の流れ

①中間テーブルとなるBookmarkモデルの実装
②Userモデルにbookmarkの定義を追加
③Bookmarksコントローラーの実装
④Boardコントローラーにbookmarkの定義を追加
⑤Routingの設定
⑥Viewの実装

①中間テーブルとなるBookmarkモデルの実装

Bookmarkモデルの仕組み(多対多)

UserとBookmarkとBoardの関係
Bookmarkモデルを実装する前に、モデルの関係を確認する。

・ユーザーはたくさんの掲示板をブックマークすることができる。
・反対に、掲示板はたくさんのユーザーにフォローされることができる。

つまり、UserもBoardもBookmarkをたくさん持っているということになる。このような関係を多対多の関係と言う。

Image from Gyazo

中間モデル(Bookmarkモデル)の作成

多対多のモデルを実装するにはお互いのforeign_keyを知っている必要がある。

ターミナル.
rails g model Bookmark user:references board:references
db/migrateファイル.rb
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モデルにも追加記入。

model/board.rb
#この一文を追加
has_many :bookmarks, dependent: :destroy
model/user.rb
#以下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をつける必要がある。

model/bookmark.rb
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の可読性を上げるためにまずはモデルにお気に入り登録のギミックを定義する。

model/user.rb
  # お気に入り追加
  # <<で引数で渡した掲示板の情報が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 << boardbookmarks.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ファイルだけ作って記載する。

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: 'ブックマークしました'
  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問題の解消)

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

⑤Routingの設定

routes.rb
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ファイル。)

bookmarks/_bookmark_button.html.rb
<%= link_to board_bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <%= icon 'far', 'star' %>
<% end%>

次にブックマーク解除ボタンを作成。

bookmarks/_unbookmark.html.rb
<%= 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つ作ったボタンをパーシャルでブックマークするボタンと、ブックマーク解除ボタンを切り替えるページを作成。

bookmarks/_bookmark.html.rb
<% if current_user.bookmark?(board) %>
  <%= render 'bookmarks/unbookmark', board: board %>
<% else %>
  <%= render 'bookmarks/bookmark', board: board %>
<% end %>

user.rbで定義されたbookmark?メソッドがここで使われる。 掲示板がブックマークされていたら掲示板解除ボタン、掲示板がブックマークされていなかったらブックマーク登録ボタンに切り替わる仕組みになる。

この↑完成した_bookmark.html.erbを掲示板のパーシャルに記入する。

boards/_board.html.rb
<% 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に注意。

boards/bookmarks.html.rb
<% 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で多対多のリレーションを実装する

5
4
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
5
4