🦓
[Rails]ポリモーフィック関連付け⑤:タグ
はじめに
タグの実装、ポリモーフィック関連付けでやった方が良いと思いやり直しました。
修正したところ:
- 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