🥨

【Ruby on Rails】コメントへのいいね機能の実装

2024/07/28に公開

はじめに

Ruby on Railsで、コメントへのいいねボタンを実装しました。
今回URLを短く設定したいという考えから、commentルーティングへ:shallowオプションを追加しました。
しかし、:shallowオプションを追加したことにより、実装ではまってしまった部分があったため記事にしております。
参考になりましたら幸いです。

環境

Rails 7.1.3.4
"@hotwired/turbo-rails": "^8.0.4"

ルーティング・ER図

ルーティングは下記の通りです。
articlesルーティング配下で、favorites, commentsルーティングをネストさせています。
またcommentsルーティング内でも、commentsfavoritesルーティングをネストさせることで親子関係をわかりやすくなるよう設定しました。

resources :articles do
    resources :favorites, only: %i[create destroy]
    resources :comments, shallow: true do
      resources :commentfavorites, only: %i[create destroy]
    end
  end

ER図は下記の通りです。

  • usersとarticlesは、1対多の関係。
  • usersとcommentsは、1対多の関係。
  • usersとfavotitesは、1対多の関係。
  • usersとcommentfavoritesは、1対多の関係。
  • articlesとfavoritesは、1対多の関係。
  • articlesとcommentsは、1対多の関係。
  • commentsとcommentsfavoritesは、1対多の関係。

Image from Gyazo

なぜ:shallowオプションをつけたのか

当初commentsルーティングへ:shallowオプションをつけていませんでしたが、commentfavoritesをネストさせることでpathが冗長になってしまっておりました。

  • :shallowオプション未使用の時のcommentfavoritesのpath
    Image from Gyazo

そのため、これを防ぐべく:shallowオプションを追記しました。

  • :shallowオプション追記の時のcommentfavoritesのpath
    Image from Gyazo

どこでハマってしまったのか

ER図でもわかる通り、commentfavoritesはuser_idとcomment_idを外部キーに持っており、

  • usersとcommentfavoritesは、1対多の関係。
  • commentsとcommentsfavoritesは、1対多の関係。
    でした。

:shallowオプションを追記したときのpathから見て取れるように、createアクションのpathにはcomment_idが含まれていますが、destroyアクションではcomment_idが含まれていません。
commentが親、commentfavoriteが子の関係にあるため、createアクション、destroyアクション共に、親リソースの情報が必要でした。

上記になかなか気づけなかったため、コメントのいいね登録実装はできたものの、いいね解除の際にUrlGenerationError: No route matches missing required keys: [:id]がエラーとして吐き出されていました。

コントローラーからビューへのインスタンス変数の受け渡しを何度も確認していましたが、エラーが一向に解消されなかったためルーティングを見直しました。
すぐには気づけませんでしたが、commentが親、commentfavoriteが子の関係にあるため、いいね解除の際もcomment_idが必要なのでは?と考え直し、気づくことができました。
今回のケースは初めてだったため、大変勉強になりました。

結局どのように実装したのか

:shallowオプションを使わなければ、destroyアクションにもcomment_idがpathに含まれましたが、やはり冗長になってしまうので、:shallowオプションを使ってコントローラー内で、親コメントの情報を取得するような実装に切り替えました。

  • app/controllers/commentfavorites_controller.rb
class CommentfavoritesController < ApplicationController
  def create
    @comment = Comment.find(params[:comment_id])
    @commentfavorite = current_user.commentfavorites.new(comment_id: params[:comment_id])
    @commentfavorite.save
  end

  def destroy
    @commentfavorite = Commentfavorite.find(params[:id])
    @comment = @commentfavorite.comment
    @commentfavorite.destroy
  end
end

ActiveRecordのアソシエーションを利用し、@comment = @commentfavorite.commentと書くことで、Commentfavoriteが属するCommentオブジェクトを取得させました。
このように実装することで、:shallowオプションを使用していてもdestroyアクション内で親リソースのCommentを簡単に取得することができました。

ビューファイルは下記のように設定しています。
今回HotwireのTurboStreamsを使用し、非同期処理を行なっています。
commentsfavoritesコントローラーのcreateアクション、destroyアクションが呼ばれると、それぞれのビューファイルを探しに行きます。

  • app/views/commentfavorites/create.turbo_stream.erb
<%= turbo_stream.replace "first-favorite-#{@comment.id}" do %>
  <%= render 'comments/unfavorite', comment: @comment, commentfavorite: @commentfavorite %>
<% end %>
  • app/views/commentfavorites/destroy.turbo_stream.erb
<%= turbo_stream.replace "first-unfavorite-#{@comment.id}" do %>
  <%= render 'comments/favorite', comment: @comment %>
<% end %>

上記のいいねボタンの登録解除自体は、_commentfavorite.html.erbで条件分岐させています。

  • app/views/comments/_commentfavorite.html.erb
    ユーザーがいいねボタンを押すと、'comments/unfavorite'ファイルがレンダリングされ、そうでなければ'comments/favorite'が表示されています。
<% if logged_in? %>
  <% if @comment.favorited?(current_user) %>
    <%= render 'comments/unfavorite', comment:, commentfavorite: %>
  <% else %>
    <%= render 'comments/favorite', comment: %>
  <% end %>
<% else %>
  <%= link_to login_path do %>
    <i class="bi bi-arrow-through-heart"></i>
    <%= comment.favorites.count %>
  <% end %>
<% end %>

'comments/unfavorite'と'comments/favorite'ファイルの中身は、下記の通りです。

  • app/views/comments/_unfavorite.html.erb
<%= link_to commentfavorite_path(commentfavorite), data: { turbo_method: :delete }, id: "first-unfavorite-#{comment.id}",
                                                   class: 'app-link' do %>
  <i class="bi bi-arrow-through-heart-fill"></i>
  <%= comment.commentfavorites.count %>
<% end %>
  • app/views/comments/_favorite.html.erb
<%= link_to comment_commentfavorites_path(comment), data: { turbo_method: :post, turbo_stream: true }, id: "first-favorite-#{comment.id}",
                                                    class: 'app-link' do %>
  <i class="bi bi-arrow-through-heart"></i>
  <%= comment.commentfavorites.count %>
<% end %>

最後に

今回はそれぞれのアソシエーションの理解不足、ネスト構造の理解、ルーティングのオプションへの理解の浅さから、エラーにハマってしまいました。
いいね機能の実装の個人記事は多く拝見しましたが、コメントへのいいね機能の記事はあまりなかったようなので、自分の備忘録も踏まえ記事に残します。
ありがとうございました。

Discussion