🦓

[Rails]ポリモーフィック関連付け②:いいね!

2023/07/24に公開

はじめに

ポリモーフィック関連付けの2回目の記事となります。
コメント機能の続きで「いいね!」機能も実装していきます。
turboについても見ていきます。

環境

Rails 7.0.4
ruby 3.2.2

Likeモデルを作成する

bin/rails generate model Like user:references likeable:references{polymorphic}
      create    db/migrate/20230723063007_create_likes.rb
      create    app/models/like.rb
bin/rails db:migrate
== 20230723063007 CreateLikes: migrating ======================================
-- create_table(:likes)
   -> 0.0047s
== 20230723063007 CreateLikes: migrated (0.0047s) =============================
db/schema.rb
  create_table "likes", force: :cascade do |t|
    t.integer "user_id", null: false
    t.string "likeable_type", null: false
    t.integer "likeable_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["likeable_type", "likeable_id"], name: "index_likes_on_likeable"
    t.index ["user_id"], name: "index_likes_on_user_id"
  end

モデルの関連付けを設定する

「いいね!」を入れたいモデルにポリモーフィック関連付けを追加します。

app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :likeable, polymorphic: true
  
  validates :user_id, uniqueness: { scope: [:likeable_id, :likeable_type] }
end
app/models/user.rb
class User < ApplicationRecord
   has_many :likes, dependent: :destroy
end

コメント、投稿、アイデアに「いいね!」ができるようにします。

app/models/comment.rb
class Comment < ApplicationRecord
   has_many :likes, as: :likeable, dependent: :destroy
end
app/models/article.rb
class Article < ApplicationRecord
   has_many :likes, as: :likeable, dependent: :destroy
end
app/models/idea.rb
class Idea < ApplicationRecord
   has_many :likes, as: :likeable, dependent: :destroy
end

「いいね!」用urlを追加する

likesリソースを作成します。

config/routes.rb
Rails.application.routes.draw do
...
resources :likes, only: %i[create destroy]
end
rails routes -g like
Prefix Verb   URI Pattern          Controller#Action
 likes POST   /likes(.:format)     likes#create
  like DELETE /likes/:id(.:format) likes#destroy

Likeコントローラーを作成する

ブックマーク機能を作る時にcreatedestroyアクションを作成しましたが、もっとシンプルにupdateアクションを追加して、ユーザーが「いいね!」をしたり、「いいね!」を解除したりするためのアクションを定義します。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
    include ActionView::RecordIdentifier
    before_action :authenticate_user!

    def create
        @like = current_user.likes.new(like_params)
        @likeable = @like.likeable
        if @like.save
            respond_to do |format|
                format.turbo_stream do
                    render turbo_stream: [
                    turbo_stream.replace(dom_id(@likeable, 'like'), partial: 'likes/like', locals: {likeable: @likeable, like:@like}),
                    turbo_stream.replace(dom_id(@likeable, 'like_counts'), partial: 'likes/like_count', locals: {likeable: @likeable })
                    ]
                end
            end
        else
            respond_to do |format|
                format.turbo_stream do
                    render turbo_stream: turbo_stream.prepend('error_messages', partial: 'shared/error_messages', locals: { object: @like })
                end
            end
        end
    end


    def destroy
        @like = current_user.likes.find_by(id: params[:id])
        if @like
            @likeable = @like.likeable
            @like.destroy
            respond_to do |format|
                format.turbo_stream do
                    render turbo_stream: [
                        turbo_stream.replace(dom_id(@likeable, 'like'), partial: 'likes/like', locals: {likeable: @likeable, like: @like}),
                        turbo_stream.replace(dom_id(@likeable, 'like_counts'), partial: 'likes/like_count', locals: {likeable: @likeable })
                    ]
                end
            end
        end
    end

    private

    def like_params
        params.require(:like).permit(:likeable_id, :likeable_type)
    end
end

ActionView::RecordIdentifierモジュールは、RailsのAction Viewで提供されるモジュールです。このモジュールを使用すると、Active Recordモデルオブジェクトに関連するHTML要素のIDやクラス名を生成するためのヘルパーメソッドを利用できます。

このモジュールをインクルードすることで、以下のようなヘルパーメソッドを使用できます:

  1. dom_id(record, prefix = nil): レコードに対応するHTML要素のIDを生成します。レコードが新規作成(まだデータベースに保存されていない)場合は、デフォルトで"new"プレフィックスが付けられます。
  1. dom_class(record, prefix = nil): レコードに対応するHTML要素のクラス名を生成します。レコードが新規作成の場合は、デフォルトで"new"プレフィックスが付けられます。

関連付けのコントローラーに@likeを渡す

app/controllers/articles_controller.rb
class ArticleController < ApplicationController
...
 def show
   @like = Like.first_or_create(likeable: @article, user: current_user)
 end
end

「いいね!」パーシャルを作成する

app/views/likes/_like.html.erb
<% if current_user %>
  <%= turbo_frame_tag dom_id(likeable, 'like') do %>
    <% if likeable.likes.exists?(user: current_user) %>
      <%= button_to "Unlike", like_path(like), method: :delete, data: { turbo: true }, form: { method: :delete } %>
    <% else %>
      <%= button_to "Like", likes_path, params: { like: { likeable_id: likeable.id, likeable_type: likeable.class.name } }, method: :post, data: { turbo: true } %>
    <% end %>
  <% end %>
<% end %>

コメント「いいね!」フレーム:

投稿「いいね!」フレーム:

「いいね!」数パーシャルを作成する

app/views/likes/_like_count.html.erb
<%= turbo_frame_tag 'like_counts' do %>
  <%= likeable.likes.count %>
  
  <% likeable.likes.each do |like| %>
    <% if like.user.profile.present? %>
          <%= image_tag like.user.profile.url, size: '25x25' %>
    <% else %>
          <%= image_tag 'user.png',size:'25x25' %>
    <% end %>
  <% end %>
<% end %>

「いいね!」数フレーム:

「いいね!」パーシャルを読み込む

app/views/articles/show.html.erb
<%= render 'likes/like', { like: @like, likeable: @article } %>
<%= render 'likes/like_count', likeable: @article %>
app/views/comments/_comment.html.erb
 <% if current_user %>
     <%= render 'likes/like', { like:current_user.likes.find_or_initialize_by(likeable: comment), likeable: comment } %>
     <%= render 'likes/like_count', { likeable: comment } %>
<% end %>

動きを確認します。

Image from Gyazo

ログも確認します。

Started POST "/articles/17/likes" for ::1 at 2023-07-23 23:48:12 +0900
Processing by LikesController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "like"=>{"likeable_id"=>"17", "likeable_type"=>"Article"}, "button"=>"", "article_id"=>"17"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 13], ["LIMIT", 1]]
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT ?  [["id", 17], ["LIMIT", 1]]
  ↳ app/controllers/likes_controller.rb:6:in `create'
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/likes_controller.rb:7:in `create'
  Like Exists? (0.1ms)  SELECT 1 AS one FROM "likes" WHERE "likes"."user_id" = ? AND "likes"."likeable_id" = ? AND "likes"."likeable_type" = ? LIMIT ?  [["user_id", 13], ["likeable_id", 17], ["likeable_type", "Article"], ["LIMIT", 1]]
  ↳ app/controllers/likes_controller.rb:7:in `create'
  Like Create (0.3ms)  INSERT INTO "likes" ("user_id", "likeable_type", "likeable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["user_id", 13], ["likeable_type", "Article"], ["likeable_id", 17], ["created_at", "2023-07-23 23:48:12.920128"], ["updated_at", "2023-07-23 23:48:12.920128"]]
  ↳ app/controllers/likes_controller.rb:7:in `create'
  TRANSACTION (0.9ms)  commit transaction
  ↳ app/controllers/likes_controller.rb:7:in `create'
  Like Load (0.1ms)  SELECT "likes".* FROM "likes" WHERE "likes"."likeable_type" = ? AND "likes"."likeable_id" = ? AND "likes"."user_id" = ? LIMIT ?  [["likeable_type", "Article"], ["likeable_id", 17], ["user_id", 13], ["LIMIT", 1]]
  ↳ app/helpers/likes_helper.rb:4:in `liked_by_current_user?'
  Rendered likes/_like.html.erb (Duration: 2.5ms | Allocations: 1534)
  Like Count (0.1ms)  SELECT COUNT(*) FROM "likes" WHERE "likes"."likeable_id" = ? AND "likes"."likeable_type" = ?  [["likeable_id", 17], ["likeable_type", "Article"]]
  ↳ app/views/likes/_like_count.html.erb:2
  Rendered likes/_like_count.html.erb (Duration: 1.5ms | Allocations: 1024)
Completed 200 OK in 15ms (Views: 0.1ms | ActiveRecord: 1.7ms | Allocations: 9222)
Started DELETE "/articles/17/likes/76" for ::1 at 2023-07-23 23:48:13 +0900
Processing by LikesController#destroy as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "like"=>{"likeable_id"=>"17", "likeable_type"=>"Article"}, "button"=>"", "article_id"=>"17", "id"=>"76"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 13], ["LIMIT", 1]]
  Like Load (0.1ms)  SELECT "likes".* FROM "likes" WHERE "likes"."user_id" = ? AND "likes"."id" = ? LIMIT ?  [["user_id", 13], ["id", 76], ["LIMIT", 1]]
  ↳ app/controllers/likes_controller.rb:23:in `destroy'
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT ?  [["id", 17], ["LIMIT", 1]]
  ↳ app/controllers/likes_controller.rb:24:in `destroy'
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/likes_controller.rb:25:in `destroy'
  Like Destroy (0.3ms)  DELETE FROM "likes" WHERE "likes"."id" = ?  [["id", 76]]
  ↳ app/controllers/likes_controller.rb:25:in `destroy'
  TRANSACTION (1.0ms)  commit transaction
  ↳ app/controllers/likes_controller.rb:25:in `destroy'
  Like Load (0.1ms)  SELECT "likes".* FROM "likes" WHERE "likes"."likeable_type" = ? AND "likes"."likeable_id" = ? AND "likes"."user_id" = ? LIMIT ?  [["likeable_type", "Article"], ["likeable_id", 17], ["user_id", 13], ["LIMIT", 1]]
  ↳ app/helpers/likes_helper.rb:4:in `liked_by_current_user?'
  Rendered likes/_like.html.erb (Duration: 2.2ms | Allocations: 1493)
  Like Count (0.1ms)  SELECT COUNT(*) FROM "likes" WHERE "likes"."likeable_id" = ? AND "likes"."likeable_type" = ?  [["likeable_id", 17], ["likeable_type", "Article"]]
  ↳ app/views/likes/_like_count.html.erb:2
  Rendered likes/_like_count.html.erb (Duration: 1.7ms | Allocations: 1023)
Completed 200 OK in 14ms (Views: 0.1ms | ActiveRecord: 1.8ms | Allocations: 8527)

終わり

2回目のポリモーフィック関連付けですがやっぱり難しいですね。

Discussion