12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsでブックマーク機能を実装する時のざっくりの流れ

Posted at

今後、お気に入りマーク(中間テーブル)をアプリに実装したい時に、この記事見れば全解決するじゃんってなるための記事です。

大まかな流れ

  1. モデルを作成
  2. ルーティングを設定する
  3. コントローラーを追加する
  4. アクションをビューに反映させる

それぞれ具体的に見ていきます。

モデルを追加、編集

  • 今回の難しいポイント
    1. 一対多、多対多のイメージに苦しむ
    2. バリデーションの書き方に苦しむ
    3. モデル間のアソシエーションに謎のscopeが出てくるため、戸惑う

まずは、お気に入り(Bookmarks)機能のエンティティを作成していきます。

UserモデルとBoardモデルの中間テーブルとしての役割を果たしてもらうために、Bookmarksモデルを作っていきます。

ターミナルで、下記コマンドを打った後、マイグレーションファイル、モデルファイルを編集していきます。

$ rails g model Bookmark user:references board:references

マイグレーションファイルを下記のように編集してください。

〇〇〇〇_create_bookmarks.rb
class CreateBookmarks < ActiveRecord::Migration[5.2]
  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

編集後は下記のコードを忘れずに打ちます。

$ rails db:migrate

作成されたbookmark.ebは下記のように編集します。

Bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
  validates_uniqueness_of :board_id, scope: :user_id 
  // 上のコードは下の書き方と一緒 
  validates :user_id, uniqueness: { scope: :board_id}
  // 一つのユーザーは同じ投稿に対して一回しかブックマークができませんというバリデーション
end

同様に、board.rb、user.rbもbookmarkとのアソシエーションを作っていきます。それぞれの対応関係は以下の通り。

・UserとBookmarkは多対多の関係

・BoardとBookmarkは1対多の関係

board.rb

class Board < ApplicationRecord

  belongs_to :user
  has_many :comments, dependent: :destroy //追加
  has_many :bookmarks, dependent: :destroy//追加
  validates :title, presence: true,length: { maximum: 255 }
  validates :body, presence: true,length: { maximum: 65535 }
  mount_uploader :board_image, ImageUploader

  def bookmarked_by?(user)
    bookmarks.where(user_id: user).exists?
    //下記の書き方でも可能
    bookmarks.exists?(user_id: user)
  end

end
user.rb

class User < ApplicationRecord
  authenticates_with_sorcery!

# sorceryを使用したログイン機能のバリデーション
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, uniqueness: true, presence: true
  validates :last_name, presence: true
  validates :first_name, presence: true

# 対多の関係だとUser has many 〇〇になるからこのようになる
  has_many :boards, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :bookmarks_boards, through: :bookmarks, source: :board
  
// fat controllerにならないように、modelにロジックを記載する。
// viewに呼び出すことができる
// self.が省略されている点に注意。
// own?(object)の場合、self.own?(object)という意味になる

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

  def bookmark(board)
    bookmarks_boards << board
  end

  def unbookmark(board)
    bookmarks_boards.delete(board)
  end

  def bookmark?(board)
    bookmarks_boards.include?(board)
    // Bookmark.where(user_id: id, board_id: board.id).exists?と同じ
  end
end
ちなみに下記三つの書き方は同じ意味になる

controller内に書く場合
 user.bookmarks.map{|bookmark| bookmark.board}

 user.bookmarks.map(&:board)

model内に書く場合
 has_many :bookmarks_boards, through: :bookmarks, source: :board

つまり、has_many through: を使うことで、user.bookmarksをboardに結び付けることができるわけです。
そして、結びつけたモデル名をbookmarks_boardsとすることができます。

そのbookmarks_boardsをuser.rbにてアクション定義することで、リファクタリングができます。

コントローラーの追加

  • 今回の難しいポイント
    1. @を使うか、使わないかの違いの理解に苦しむ
    2. saveとsave!の違いの理解に苦しむ

まずはrails g controllerでアクションを追加していく。実際に使うアクションはcreateとdestory Actionのみなので、以下のように記載する流れになります。作成後にcreate.html.erbとdestroy.html.erbは使わないので削除して構いません。もしくはrails g controller Bookmarkのみにして、手動でcreateとdestroyを追加してください。こっちの方が楽かもしれませんね。

$ rails g controller Bookmark create destroy

作成したbookmark_controllerをスッキリ書くために、ロジックをモデルに記載しておきます。

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

!を付けた場合の挙動に関して

代表例) saveとsave!の違い

save -> 保存できない場合はfalse

save! -> 保存できない場合はActiveRecord::RecordInvalid発生

感嘆詞を付けない場合(saveやcreate等)は、レコードの作成、保存に失敗した際、通常の場合はnilを返します。しかし、感嘆詞をつける場合(create!、save!)は、ActiveRecord::RecordNotFoundエラーを発生させることができます。
つまり!をつけることにより、例外を発生させるか発生させないかの違いです。

!の使い分け方

 saveメソッドの場合は保存の成功と失敗を true/false で返します。一方で、save!メソッドは保存を失敗した場合に例外を返します。
 つまり、save!を使えば例外をトリガーにしてロールバックなどの処理を行うことができるため、比較的使用頻度が高いかと思います。

Bookmarks_controllerの作成

Bookmark機能作成(createアクション)の流れは以下の通り。

  1. 対応する掲示板を検索する
  2. 1で見つけたidをログイン中のユーザーと紐付けでdbに保存する

Book mark機能削除(destroyアクション)の流れは以下の通り。

  1. ログイン中のユーザーがいいねした投稿idを検索する
  2. 2をmodelに定義したunbookmarkインスタンスを用いて削除する

それでは実装していきます。

Bookmarks_controller.rb

class BookmarksController < ApplicationController

  def create
    @board = Board.find(params[:board_id])
#Boardモデルからboard_idを探してくる
    current_user.bookmark(@board)
# ログイン中のユーザーと紐づけられたidを取ってくるこの時user.rbに定義したaliasを
使用しidの情報を保存する
  end

  def destroy
    @board = current_user.bookmarks.find(params[:board_id]).board
    current_user.unbookmark(@board)
    # redirect_backはユーザーが直前にリクエストを送ったページに戻す
    # fallback_location: デフォルトの設定
  end

end

ルーティングを設定する

  • 今回の難しいポイント
    1. params[:id]に何が受け渡されているかイメージしづらい
    2. HTTPの受け渡しがイメージしづらい

次に、bookmarkアクションが実行された時のルーティングの設定をしていきます。
collectionという機能をcontrollerに追加することで、個別のルーティング(idを付けない)追加することができます。下記のようにネストして書くことで、/board/bookmarksのようにパスを作ることができます。

Rails のルーティング - Railsガイド

resources :users, only: %i[new create]
resources :boards do 
  resorces collection do
   get :bookmarks
end

resources :bookmarks, only: %i [create destroy]

アクションをビューに反映させる

コントローラーで作成したインスタンス変数、モデルで定義したアクションを上手い具合にビューに反映させていきます。肌感、これが一番難しいです。

  • 今回の難しいポイント
    1. parialを呼び出すときの値の受け渡し方が難しい。(@つけるの?付けないの?)
    2. pathとurlの違いが理解しづらい。(form_withの話やルーティングの話)
    3. 条件分岐でpartialを表示させるのが難しい

partialを作成して、可読性を高めていきます。必要なpartialは以下の通り。

・ボタンパーシャル(ブックマーク作成削除を含むパーシャル)

・ブックマーク作成パーシャル

・ブックマーク削除パーシャル

また、作成するviewファイルは以下の通り。

・ブックマーク一覧ファイル

・renderと条件分岐を使用して既存のviewを編集していきます。

それでは、実装していきます。

まずはボタンパーシャルの作成から。

_bookmark.button.html.erb

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

次に、ブックマーク作成と削除パーシャルを実装していきます。


_bookmark.html.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(board_id: board.id)


_unbookmark.html.erb

<%= 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 %>

link_to board_bookmarks_path(current_user.bookmarks.find_by(board_id: board.id))

次に、既存のviewファイルを編集していく。今回実装したい内容は以下の通り。

  1. 他人のユーザーに対してのみブックマークができる。
  2. 自分の投稿しか編集削除ができないようにする

boards/_board.html.erb

~省略~
#ログイン中のユーザーがboardで渡されたidを持っているかどうかの場合分け。

<% if current_user.own?(board) %>
   <div class='mr10 float-right'>
      <%= render 'crud_menus', board: board %>
<% else %>
     <%= render 'bookmarks/bookmark_area', board: board %>
   </div>
<% end %>

boards/bookmarks.html.erb  ブックマーク一覧表示view

<% 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">
     #collection 
      <% if @bookmark_boards.present? %>
        <%= render partial: "board", collection: @bookmark_boards %>
      <% else %>
        <p>ブックマーク中の掲示板がありません</p>
      <% end%>

      </div>
    </div>
  </div>
</div>

完成です。

参考文献

12
9
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
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?