【Rails】お気に入り機能の実装
お気に入り機能の実装をしたので、手順のアウトプットと言語化をしていく。 前提として、deviseを使ったユーザー管理機能が実装済みで、お気に入りの対象となる本が一覧表示されている状態とする。
実装したいこと
- 投稿された本を「お気に入り」すると★マーク、「お気に入り解除」すると☆マークに変わる。
- ユーザーは自分の投稿もお気に入りすることができるが、1冊の本に対して複数回お気に入りすることはできない。
- ユーザーがお気に入りした本のみを集めた、お気に入り一覧ページがあること。
実装の流れ
- モデルの設定
- 各モデルのアソシエーション設定
- ルーティング設定
- コントローラーの設定
- ビューファイル記述
モデルの設定
UserモデルとBookモデルがある。今回のポイントとしては、「お気に入り」は「誰が」というユーザー情報と、「どの本」にという対象となる本の情報を持つことなので、ユーザーと本の情報がセットで保存される中間テーブルを用意すること。
Favoriteモデルを生成
% rails g model favorite
マイグレーションファイルに外部キー制約をつけてテーブル生成
class CreateFavorites < ActiveRecord::Migration[6.0] def change create_table :favorites do |t| t.references :user, null: false, foreign_key: true t.references :book, null: false, foreign_key: true t.timestamps t.index [:user_id, :book_id], unique: true end end end
% rails db:migrate
中間テーブルには、ユーザーと本それぞれのidのみを保存。
t.index [:user_id, :book_id], unique: true
という記述は、1人のユーザーが1つの本に複数回お気に入りすることを防ぐため。つまり、userとbookのセットの情報が一意性を持つようにして被らせないようにする。
各モデルのアソシエーション設定
app/models/user.rb
class User < ApplicationRecord has_many :books, dependent: :destroy has_many :favorites, dependent: :destroy has_many :favorite_books, through: :favorites, source: :book # 省略 end
app/models/book.rb
class Book < ApplicationRecord belongs_to :user has_many :favorites, dependent: :destroy has_many :favorite_books, through: :favorites # 省略 end
app/models/favorite.rb
class Favorite < ApplicationRecord belongs_to :user belongs_to :book validates :user_id, uniqueness: { scope: :book_id } end
先に説明しておくと、Favoriteモデルのバリデーションにuser_id, uniqueness: { scope: :book_id }という記述をしているが、これもさっきの情報の一意性を保つため。 uniquenessというヘルパーがデータを保存する前に、すでにDBにそのデータがないかを確認していくれている。 scope は範囲を指定して、一意性をチェックしている。例えば、
validates :user_id, uniqueness: true
だけだと、1つの本に対して1人のユーザーしか「お気に入り」できないため、早いものがちになってしまう。 お気に入りいっぱいしてもらうためにも、user_idで範囲を限定してあげる。 こうすると、ユーザーは被ることはあってはならないけど、1つの本に対する「お気に入り」はいくらでも被っていいよ。ってできる。
Userモデルの
has_many :favorite_books, through: :favorites, source: :book
について。 UserモデルとBookモデルは1対多の関係で直接結びついてはいるが、その関係は「Userが投稿したBook」であり、「Userがお気に入りしたBook」ではない。 つまりここでは、「Userがお気に入りしたBook」の関係を作るために、Favoriteモデルを通してBookモデルからデータを参照するというアソシエーションを記述をしている。
dependentオプションは親モデルが削除されると子モデルも同時に削除されるように設定している。
中間テーブルのアソシエーションについては以下の記事
ルーティング設定
Rails.application.routes.draw do devise_for :users root to: 'books#index' resources :users, only: [:show, :edit, :update] resources :books, shallow: true do resource :favorites, only: [:create, :destroy] collection do get :favorites end end end
shallow: true do~end という記述は、入れ子などで冗長になりがちなURLを"浅く"することができ、取得できるパラメータをシンプルにして見やすくするオプション。
「お気に入り」はcreate
「お気に入り解除」はdestroy
collection do get :favorites end
という記述は、特定のid指定なしでfavoriteアクションにルーティングを結んでいる。 ターミナルでルーティングを確認すると:idがついていない。
Prefix Verb URI Pattern # 省略 favorites_books GET /books/favorites(.:format) books#favorites
コントローラーの設定
favorites_controllerを生成
% rails g controller favorites
生成できたら、createアクションとdestroyアクションを用意
class FavoritesController < ApplicationController def create @book = Book.find(params[:book_id]) favorite = current_user.favorites.create(book_id: params[:book_id]) redirect_to root_path end def destroy @book = Book.find(params[:book_id]) favorite = Favorite.find_by(book_id: params[:book_id], user_id: current_user.id) favorite.destroy end end
createアクションの翻訳 Booksテーブルの中からparamsでbook_idを取得して特定の本に対して、現ユーザーがその本に「お気に入り」する。お気に入りするとトップページに戻るという処理。 destroyアクションの翻訳 Booksテーブルの中からparamsでbook_idを取得。Favoriteテーブルの中から、現ユーザーと前に取得した本のデータセットを取得して、その本の「お気に入り解除」する。お気に入り解除するとトップページに戻るという処理。
また、Userモデルに「お気に入り」する処理とユーザーが「お気に入り」したかを判別する処理を記述する。
class User < ApplicationRecord # 省略 def favorite(book) favorite_books << book end def unfavorite(book) favorite_books.destroy(book) end def favorite?(book) favorite_books.include?(book) end end
コントローラーではなくモデルにこれらの処理を記述した理由は、オブジェクト指向の考え方より。 コントローラーはリクエストを受けっ取ってレスポンスを返すと言う役割を持つオブジェクトなため、具体的な「お気に入り」するとか「お気に入り」したかしてないかといった判別処理はコントローラーではなくモデルで行なう。それぞれのオブジェクトが持つ役割を考慮したから。
ビューファイル
部分テンプレートファイルを作成して書き出し app/views/books/_favorite.html.erb
<% if book.favorite_by?(current_user) %> <%= link_to icon('fas', 'star'), book_favorites_path(book.id), method: :delete %> <% else %> <%= link_to icon('far', 'star'), book_favorites_path(book.id), method: :post %> <% end %> <span class='star-count'><%= book.favorites.count %></span>
ログインユーザーであれば、お気に入り機能を使える app/views/books/index.html.erb
<% if user_signed_in? %> <div class='star-btn' > <%= render "favorite" ,locals{ book: book } %> </div> <% end %>
localsオプションでさっきの部分テンプレートで変数bookが使えるようにする。
<span class='star-count'><%= book.favorites.count %></span>
という記述でお気に入り数をカウントして、表示するようにしている。
お気に入り一覧画面
app/views/books/favorites.html.erb
<% if @favorite_books.present? %> <%= render @favorite_books %> <% else %> <p>お気に入りはありません</p> <% end %>
現ユーザーがお気に入りしている本があれば表示する
Font Awesome
<%= icon 'fas', 'star' %>
という記述ではFont Awesomeを導入している。
終わりに
これでお気に入り機能のサーバーサイドの実装はできたため、お気に入りしたユーザーや本の情報は保存されるが、フロント部分の実装はこれでは不十分。また、お気に入りしてもリロードしないと結果が反映されない。Ajax(非同期通信)を使ってお気に入り機能をより使いやすくしていきたい。