【Rails】いいね機能を非同期で実装する方法をわかりやすく解説

「【Rails】いいね機能を非同期で実装する方法をわかりやすく解説」のアイキャッチ画像

本記事では、いいね(お気に入り)機能の実装手順を解説します。フリマアプリを題材として、あるユーザーが出品した商品に対して、非同期でいいね登録が出来ることを目標としています。


完成画面



ER図


今回、中間テーブル(Favorite)を作成・使用します。いいねをしたユーザーのid、商品のidを格納するものです。

※「User」「Item」テーブルは既に作成済みとして進めます。


いいね機能実装の流れ


いいね機能は、大まかに以下の流れで実装します。

①favoriteモデル作成
②モデル間のアソシエーション設定
③ルーティング設定
④favoriteコントローラ作成、編集
⑤ビュー実装(いいね機能用の部分テンプレート作成)
⑥非同期通信の実装


実装手順

⓪はじめに


コーディングはhaml/Scssを利用しています。

商品情報一覧が格納されている「itemsテーブル」の特定商品に「いいね」をする想定で実装を行っています。

また今回は、いいね登録されたユーザーID、商品IDを格納するための「Favorite」テーブルを作成します。そしてFavoriteテーブルは「User」テーブル、「Item」テーブルの中間テーブルの位置付けとなりますが、これらのテーブルは既に作成済みとして、解説をしていきます。



①favoriteモデル作成


まずいいね機能用のfavoriteモデルを作成します。

$ rails g model Bookmark user:references item:references

上記コマンドにより、(a)モデルファイルと(b)マイグレーションファイルが作成されます。


(a) favorite.rb
(b) xxxxx_create_favorites.rb


このまま rails db:migrate としたいところですが、その前にマイグレーションファイルに1点追加すべき処理があります。それは同じユーザーが同じ商品に対して、複数回いいね登録できないようにするものです。

そのために「t.index [:user_id, :item_id], unique: true」を追記しましょう。これにより、use_idとitem_idのセットの重複登録を防ぐことができるようになります。

class CreateFavorites < ActiveRecord::Migration[5.0]
  def change
    create_table :favorites do |t|
      t.references :user, foreign_key: true
      t.references :post, foreign_key: true

      t.timestamps
      t.index [:user_id, :item_id], unique: true #追加
    end
  end
end

「t.references :XX」について

references型を指定することで、以下2つのメリットがあります。

・userではなく、user_idというカラム名を作成
・インデックスを自動で設定す

– インデックスの設定とは?
インデックスとは、どこにどの行があるかを示した索引のことです。
設定により、データの読み込み、取得速度向上のメリットがあります。

「foreign_key: true」について

外部キーの設定を示します。
外部キーとは、テーブルにあるカラム(例.favoriteテーブルの「user_id」)に、別のテーブルの特定の列(例.userテーブルの「id」)の項目しか格納できないようにする制約を指します。仮に今回、favoriteテーブルの「user_id」に、userテーブルの「id」に存在しない値が格納される場合、エラーになるというわけです。

– 外部キーの設定の役割とは?
参照するカラム(カラム名_id)への不適切な値の設定を防ぎ、DBの一貫性を保つ役割を持ちます。


最後に rails db:migrate を実行して、中間(favorite)テーブルの完成です。

$ rails db:migrate


②モデル間のアソシエーション設定


冒頭のER図を再掲します。このモデル間の関係を踏まえて、設定を進めましょう


既に出品機能は完成している前提で進めているため、新規実装するのは以下3点です。


・favoriteモデル
・itemモデルの”#いいね機能のアソシエーション処理”以下
・userモデルの”#いいね機能のアソシエーション処理”以下


その上で、各コードのポイントは次のとおりです。


#ⅰ:「belong to」「has many」について
Itemモデル・Userモデルは、Favoriteモデルに対して1対多の関係にあるため、記載のとおり定義

#ⅱ:オプション「dependent: :destroy」について
特定のitemやuserを削除した際、それに紐づくfavoriteも自動削除させるために、追加したオプション

#ⅲ:オプション「has many thorough」について
ユーザーがいいねした商品情報を直接アソシエーションで取得するために、追加したオプション


class Favorite < ApplicationRecord
  belongs_to :user #ⅰ
  belongs_to :item #ⅰ
end
class Item < ApplicationRecord
  belongs_to :user #ⅰ

  #いいね機能のアソシエーション処理
  has_many :favorites, dependent: :destroy #ⅰ、ⅱ
  has_many :favorite_users, through: :favorites, source: :user #ⅲ
end
class User < ApplicationRecord
  has_many :items #ⅰ

  #いいね機能のアソシエーション処理
  has_many :favorites, dependent: :destroy #ⅰ、ⅱ
  has_many :favorite_items, through: :favorites, source: :item #ⅲ
end

「has_many :A, through: :B, source: :C」について

「has_many :favorite_items, through: :favorites, source: :item」の場合で解説します。

今回いいね機能実装前の段階で、出品機能として、user・itemで一対多の関係でアソシエーションを組んでいます。
Userモデルの場合は「has_many :items」ですね。

そして新たに、いいね機能用の「has_many :items」を作成する際、既に同じ名前(:items)があるためエラーとなります。したがって、いいね機能用の別の任意の名前を付与数る必要が出てくるわけです。

それが「has_many :A, through: :B, source: :C」によって実現できます。
Aは「任意の名前」、Bは「経由モデル」、Cは「経由先のモデル」を指しています。


これでアソシエーション設定は完了です。



③ルーティング設定


次はルーティングの設定をします。

itemsとfavoritesをネスト構造としているのは、いいねは商品に紐づくためです。またfavoritesコントローラでは「createアクション」「destroyアクション」のみ使用するため、only: [:create, :destroy] と記載しています(コントローラ設定詳細は④で解説します)

Rails.application.routes.draw do
  resources :items do
    resource :favorites, only: [:create, :destroy]
  end
end

「resources」「resource」について

resourcesとresourceの大まかな違いは以下のとおりです。

・resourcesメソッド(複数形)
7つのアクションのルーティングがid付きで生成される

・resourceメソッド(単数形)
indexを除いたアクションのルーティングがid無しで生成される


つまり人や商品など、リソースが複数存在する場合は「resources」を使用し、リソースが単一の場合は「resource」を使用します。
商品は「商品1・商品2・商品3..」と複数存在する一方で、いいねは「いいね1・いいね2..」とはなりませんね。いいね(favorites)は商品に紐づくだけで、idは必要ありません。

それゆえ、今回いいねは「resource(単数形)」としています。



④favoriteコントローラ作成、編集


まずは次のコマンドで、favoritesコントローラを作成しましょう。

$ rails g controller favorites create destroy

そして作成したコントローラーを、次のように編集します。

class FavoritesController < ApplicationController
  before_action :set_item 

  def create
    @favorite = Favorite.new(user_id: current_user.id,  item_id: @item.id)
    @favorite.save
  end

  def destroy
    @favorite = Favorite.find_by(user_id: current_user.id, item_id:  @item.id)
    @favorite.destroy
  end

  private

  def set_item
    @item = Item.find_by(id: params[:item_id])
  end
end

各アクションの内容の解説をします。


・set_item
いいねを押した(または現在表示している)商品情報を取得し、@itemに格納しています。

・create
current_user.favorites.newで、リソースを新規作成します。その際、user_idにはcurrent_userのidを、item_id:には@itemのidを代入します。

・destroy
基本的にcreateアクションと同様の流れです。最後に@favorite.destroyでDBから削除しています。


以上の情報で新規作成したリソースを、@favoriteに代入。それを@favorite.saveでDBに格納しています。



⑤ビュー実装(いいね機能用の部分テンプレート作成)


続いてビュー実装に取り掛かります。

全体像は次のとおりです。


1:_favorites.html.haml(部分テンプレート)を作成し、いいねボタン部分を記述
2:items/show.html.haml からrenderで1を読み込む


階層構造は以下となっています。(記述対象のファイルを★で示しています)

  app
  views

    favorites
      _favorites.html.haml★
      create.js.haml
      destroy.js.haml
    items
      show.html.haml

※「create.js.haml」「destroy.js.haml」は次のステップで使用しますので、ここでは無視してください。


.favorites{id: "favorite_#{@item.id}"}
  = render 'favorites/favorite', item: @item
- if user_signed_in?
  - if current_user.favorites.find_by(item_id: item.id)
    = link_to item_favorites_path(item.id), method: :delete, remote: true do
      .icon
        = icon('fa', 'star')
      .text
        = 'いいね!'
      .count
        = item.favorites.count
  - else
    = link_to item_favorites_path(item.id), method: :post, remote: true do
      .icon
        = icon('far', 'star')
      .text
        = 'いいね!'
      .count
        = item.favorites.count
- else
  = link_to new_user_session_path do
    .icon
      = icon('far', 'star')
    .text
      = 'いいね!'
    .cnunt
      = item.favorites.count

上から解説します。


・{id: “favorite_#{@item.id}”}
@item.idで、どの商品かを指定しています。仮に商品idが「1」の場合、id属性は「favorite_1」となりますね。これをコードの記述理由は次のステップで分かりますので、覚えておいてください。

・render ‘favorites/favorite’, item: @item
部分テンプレート「_favorite.html.haml」を呼び出します。その際、@itemを部分テンプレート内では「item」という変数名で使用します。

・if user_signed_in?、else
if “いいねしている場合、” else “いいねしていない場合” で条件分岐させます。前者の場合はいいね登録/解除処理へ、後者はログイン画面に遷移させています。

・if current_user.favorites.find_by(item_id: item.id)
現在ログイン中のユーザーIDが「1」、商品IDが「1」の場合、favoritesテーブルでuser_idが「1」と同じレコードのitem_idカラムで、item_idが「1」のものは存在しているか、を調べているようなイメージです。

・link_to item_favorites_path(item.id), method: :delete, remote: true do
存在する場合は、deleteでいいねを削除します。また「remote: true」を使用することで、リンクを押したときに、画面遷移なしでAjaxが発火すうようになります。Ajaxの実装内容については次のステップで解説します。

・link_to item_favorites_path(item.id), method: :post, remote: true do
存在しない場合は、postでいいねを作成します。deleteをpostにしただけですね。


※ renderメソッドについて

renderメソッドは、部分テンプレートの呼び出しに使用します。そして「partial オプション」「locals オプション」を使用すると、以下構文となります。

= render partial: ‘hoge’, locals: { piyo: ‘huga’

partialは部分テンプレート名の指定に、localsは部分テンプレート内で使用する変数の指定の役割を持ちます。

つまり上の構文は、「_hoge.html.haml」という部分テンプレートを呼び出す。そしてhugaという変数を、部分テンプレート内ではpiyoという名前で使用する。」という感じの意味になります。

さらに「paratial:」「locals:{}」は省略可能なため、今回のようなコードとなっている訳です。


いいね部分テンプレート作成は以上となります。完成まであと少しですので頑張りましょう。



⑥非同期通信の実装


最後に、ajaxを使用した、画面遷移せずにいいねを可能とするjavascriptファイルを作ります。

$('#favorite_#{@item.id}').html("#{escape_javascript(render "favorites/favorite", item: @item )}");
$('#favorite_#{@item.id}').html("#{escape_javascript(render "favorites/favorite", item: @item )}");

まずcreateアクション発火で実行される「create.js.haml」、destroyアクション発火で実行される「destroy.js.haml」を作成します。そしてここで、先ほどの部分テンプレート作成で、以下コードを実装したことを思い出してください。

.favorites{id: "favorite_#{@item.id}"}
  = render 'favorites/favorite', item: @item

ここでは表示される商品の@item.idのを入ったid属性を生成しました。これを、いいねボタンがクリックされた時に、htmlメソッドによって新しく書き換えている訳です。

また「escape_javascript」は、javascriptファイル内にHTMLを挿入するときに必要なメソッドで、今回renderでHTMLを呼び出しているので記述しています。

最後はシンプルでしたね。以上で実装が完了です。




おわりに


非同期通信(Ajax)によるいいね機能の実装手順は以上になります。

今回はフリマアプリを題材としていますが、他サービスで実装する際も、構造は同様です。いいね機能に限らず、多対多などのテーブル構造がやや複雑になる際は、事前にER図に構造を落とし込み、全体像を理解してから実装するようにしましょう。