🦓

[Rails]ポリモーフィック関連付け⑤:タグ

2023/08/06に公開

はじめに

タグの実装、ポリモーフィック関連付けでやった方が良いと思いやり直しました。

修正したところ:
- Tagging中間テーブルを作成しました。
- タグの作成と更新をヘルパーメソッドに移動しました。
- タグのフォームとビューをパーシャルにしました。
- N+1問題を回避しました。

環境

Rails 7.0.4.3
ruby 3.2.1

タグモデルを作成する

bin/rails generate model Tag name:string
      invoke  active_record
      create    db/migrate/20230806094040_create_tags.rb
      create    app/models/tag.rb
bin/rails db:migrate
== 20230806094040 CreateTags: migrating =======================================
-- create_table(:tags)
   -> 0.0354s
== 20230806094040 CreateTags: migrated (0.0355s) ==============================

中間モデルを作成する

多対多の関連付けのため中間モデルを作成します。

bin/rails generate model Tagging taggable:references{polymorphic} tag:references
      invoke  active_record
      create    db/migrate/20230806094230_create_taggings.rb
      create    app/models/tagging.rb
bin/rails db:migrate
== 20230806094230 CreateTaggings: migrating ===================================
-- create_table(:taggings)
   -> 0.0251s
== 20230806094230 CreateTaggings: migrated (0.0252s) ==========================

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

app/models/tag.rb
class Tag < ApplicationRecord
  has_many :taggings, dependent: :destroy
  has_many :ideas, through: :taggings, source: :taggable, source_type: 'Idea'
  has_many :posts, through: :taggings, source: :taggable, source_type: 'Post'
end
app/models/tagging.rb
class Tagging < ApplicationRecord
  belongs_to :taggable, polymorphic: true
  belongs_to :tag
end
app/models/idea.rb
class Idea < ApplicationRecord
  has_many :taggings, as: :taggable, dependent: :destroy
  has_many :tags, through: :taggings
end
app/models/post.rb
class Post < ApplicationRecord
  has_many :taggings, as: :taggable, dependent: :destroy
  has_many :tags, through: :taggings
end

Tagのバリデーションを設定する

app/models/tag.rb
class Tag < ApplicationRecord
   before_validation :downcase_name
   validates :name, presence: true, uniqueness: { case_sensitive: false }

   private

   def downcase_name
        self.name = name.downcase if name.present?
   end
end

パラメーターを渡す

関連付けしたモデルのコントローラーに[:tag_names]パラメーターを渡します。

app/controllers/ideas_controller.rb
class PostsController < ApplicationController
    include TagHelper
    
    def create
        @post = current_user.posts.build(post_params)
        tag_names = params[:post][:tag_names]
        if @post.save
            tags = tag_names.split("\n").map(&:strip).uniq if tag_names.present?
            create_or_update_tags(@post, tags)
            redirect_to @post
        else
            render :new, status: :unprocessable_entity
        end
    end
    
    def update
        if @post.update(post_params)
            tag_names = params[:post][:tag_names]
            tags = tag_names.split("\n").map(&:strip).uniq if tag_names.present?
            create_or_update_tags(@post, tags)
            redirect_to @post
        else
            render :edit, status: :unprocessable_entity
        end
    end
end

ヘルパーメソッドを作成する

タグの作成および更新に使うヘルパーメソッドを作成します。
また、以前のタグと比べてる処理を追加しました。

app/helpers/tag_helper.rb
module TagHelper
    def create_or_update_tags(item, tags)
        return if tags.blank?
+        current_tags = item.tags.pluck(:name).sort
+        news_tags = tags.sort

+     if current_tags != news_tags
        item.tags.destroy_all
        begin
        tags.each do |tag|
          tag = Tag.find_or_create_by(name: tag)
          item.tags << tag
          rescue ActiveRecord::RecordInvalid
            false
          end
        end
      end
+    end
end

タグフォームを作成する

app/views/tags/_form.html.erb
<div>
  <%= form.label :tag_names %>
  <%= form.text_area :tag_names, 
    class: 'tag-input',
    value: item.tags.map(&:name).join("\n"), 
    rows: 2 %>
</div>

タグフォームを読み込みます。

app/views/post/form.html.erb
<%= render 'tags/form', form: form, item: @post %>

タグビューを作成する

app/views/tags/_tags.html.erb
<% if item.tags.present? %>
  <% item.tags.each do |tag| %>
    <span><%= tag.name %></span>
  <% end %>
<% end %>

タグビューを読み込みます。

app/views/post/show.html.erb
<%= render 'tags/tags', item: post %>

Tagコントローラー

タグ一覧

関連付けしたモデルと中間テーブルも取得します。

irb(main):160:0> Tag.includes(:taggings, taggings: :taggable)
  Tag Load (0.3ms)  SELECT "tags".* FROM "tags"
  Tagging Load (0.3ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."tag_id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)  [["tag_id", 1], ["tag_id", 2], ["tag_id", 3], ["tag_id", 4], ["tag_id", 5], ["tag_id", 6], ["tag_id", 7], ["tag_id", 8], ["tag_id", 9], ["tag_id", 10], ["tag_id", 11], ["tag_id", 12], ["tag_id", 13], ["tag_id", 14]]
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" IN ($1, $2, $3, $4)  [["id", 4], ["id", 8], ["id", 9], ["id", 6]]

タグ詳細

タグと紐付いたアイデムの配列を取得します。

irb(main):001:0> Tagging.includes(:taggable).where(tag_id: Tag.where(name: 'tag')).map(&:taggable)
  Tagging Load (0.4ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."tag_id" IN (SELECT "tags"."id" FROM "tags" WHERE "tags"."name" = $1)  [["name", "tag"]]
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" IN ($1, $2)  [["id", 9], ["id", 8]]
=> 
[#<Post:0x000000010a30c310
  id: 9,
  title: "post",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00>,
 #<Post:0x000000010a30c3b0
  id: 8,
  title: "rails",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00>]

タグと紐付いたアイデムの配列をモデルごとに取得します。

irb(main):002:0> Tag.find_by(id: 5).taggings.includes(:taggable).where(taggable_type: 'Post').map(&:taggable)
  Tag Load (0.3ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
  Tagging Load (0.2ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."tag_id" = $1 AND "taggings"."taggable_type" = $2  [["tag_id", 5], ["taggable_type", "Post"]]
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" IN ($1, $2)  [["id", 8], ["id", 9]]
=> 
[#<Post:0x000000010a2ec128
  id: 8,
  title: "rails",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00>,
 #<Post:0x000000010a2ebf48
  id: 9,
  title: "post",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00>]

中間テーブルを通して取得します。

irb(main):003:0> Tagging.includes(:taggable).where(tag_id: 5, taggable_type: 'Post').map(&:taggable)
  Tagging Load (0.3ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."tag_id" = $1 AND "taggings"."taggable_type" = $2  [["tag_id", 5], ["taggable_type", "Post"]]
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "osts"."id" IN ($1, $2)  [["id", 8], ["id", 9]]
=> 
[#<Post:0x000000010a309570
  id: 8,
  title: "rails",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:43:11.242975000 JST +09:00>,
 #<Post:0x000000010a3094d0
  id: 9,
  title: "post",
  content: "test",
  user_id: 4,
  created_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00,
  updated_at: Sun, 06 Aug 2023 21:46:40.295254000 JST +09:00>]

終わりに

モデルが増えるとやっぱりポリモーフィック関連付けの方が実装しやすいです。

Discussion