今後、お気に入りマーク(中間テーブル)をアプリに実装したい時に、この記事見れば全解決するじゃんってなるための記事です。
大まかな流れ
- モデルを作成
- ルーティングを設定する
- コントローラーを追加する
- アクションをビューに反映させる
それぞれ具体的に見ていきます。
モデルを追加、編集
- 今回の難しいポイント
- 一対多、多対多のイメージに苦しむ
- バリデーションの書き方に苦しむ
- モデル間のアソシエーションに謎の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
# 1対多の関係だと、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にてアクション定義することで、リファクタリングができます。
コントローラーの追加
- 今回の難しいポイント
- @を使うか、使わないかの違いの理解に苦しむ
- 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で見つけたidをログイン中のユーザーと紐付けでdbに保存する
Book mark機能削除(destroyアクション)の流れは以下の通り。
- ログイン中のユーザーがいいねした投稿idを検索する
- 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
ルーティングを設定する
- 今回の難しいポイント
- params[:id]に何が受け渡されているかイメージしづらい
- HTTPの受け渡しがイメージしづらい
次に、bookmarkアクションが実行された時のルーティングの設定をしていきます。
collectionという機能をcontrollerに追加することで、個別のルーティング(idを付けない)追加することができます。下記のようにネストして書くことで、/board/bookmarksのようにパスを作ることができます。
resources :users, only: %i[new create]
resources :boards do
resorces collection do
get :bookmarks
end
resources :bookmarks, only: %i [create destroy]
アクションをビューに反映させる
コントローラーで作成したインスタンス変数、モデルで定義したアクションを上手い具合にビューに反映させていきます。肌感、これが一番難しいです。
- 今回の難しいポイント
- parialを呼び出すときの値の受け渡し方が難しい。(@つけるの?付けないの?)
- pathとurlの違いが理解しづらい。(form_withの話やルーティングの話)
- 条件分岐で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ファイルを編集していく。今回実装したい内容は以下の通り。
- 他人のユーザーに対してのみブックマークができる。
- 自分の投稿しか編集削除ができないようにする
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>
完成です。
参考文献