【Ruby on Rails】コメントへのいいね機能の実装
はじめに
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対多の関係。
なぜ:shallowオプションをつけたのか
当初commentsルーティングへ:shallowオプション
をつけていませんでしたが、commentfavoritesをネストさせることでpathが冗長になってしまっておりました。
そのため、これを防ぐべく:shallowオプション
を追記しました。
どこでハマってしまったのか
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