実装の流れ
tagモデルと中間テーブルを作成
投稿とタグは多対多の関係になるため、中間テーブルを作成します。
投稿のモデルを作成します。
$ rails g model item name:string user:references
タグモデルを作成します。
$ rails g model tag word:string
中間テーブルを作成します。
$ rails g model item_tag_relation item:references tag:references
マイグレーションファイルを確認します。
class CreateItems < ActiveRecord::Migration[6.0]
def change
create_table :items do |t|
t.string :name
t.references :user, foreign_key: true
t.timestamps
end
end
end
class CreateTags < ActiveRecord::Migration[6.0]
def change
create_table :tags do |t|
t.string :word
t.timestamps
end
end
end
class CreateItemTagRelations < ActiveRecord::Migration[6.0]
def change
create_table :item_tag_relations do |t|
t.references :item, foreign_key: true
t.references :tag, foreign_key: true
t.timestamps
end
end
end
unique: trueで同じタグ名を登録しないようにできますが、うまく実装できなかったので別の方法で一意性を持たせました。
$ rails db:migrate
モデルの関連付けとバリデーション
class Item < ApplicationRecord
has_many :item_tag_relations
has_many :tags, through: :item_tag_relations, dependent: :destroy
belongs_to :user
end
「has_many :tags, through: :item_tag_relations」の記述にによって、item_tag_relationsモデルを通してアイテムに紐づくタグを取得します。
class Tag < ApplicationRecord
has_many :item_tag_relations, dependent: :destroy
has_many :items, through: :item_tag_relations
validates :word, uniqueness: true
end
「validates :word, uniqueness: true」の記述によって、タグの名前が重複して登録されるの防ぎます。(何故かうまく働きませんでした...)
class ItemTagRelation < ApplicationRecord
belongs_to :item
belongs_to :tag
end
ルーティングを設定
Rails.application.routes.draw do
root to: 'items#index'
resources :items
end
formオブジェクトの作成
Formオブジェクトは、1つのフォーム送信で複数のモデルを更新するときに使用するツールです。自分で定義したクラスをモデルのように扱うことができます。
modelsディレクトリ配下に「app/models/item_tag.rb」ファイルを作成します。
class ItemTag
include ActiveModel::Model
attr_accessor :name, :user_id, :item_id, :tag_ids
end
ActiveModel::Modelをincludeすることで、そのクラスのインスタンスはActiveRecordを継承したクラスのインスタンスと同様に form_with や render などのヘルパーメソッドの引数として扱えたり、バリデーションの機能が使えるようになります。
attr_accessorで使用したいカラム名をセットします。
続いて、フォームからパラメーターとして送られてきた情報をテーブルに保存する処理を追加します。
class ItemTag
include ActiveModel::Model
attr_accessor :name, :user_id, :item_id, :tag_ids
def save
@item = Item.create(name: name, user_id: user_id)
tag_list = tag_ids.split(/[[:blank:]]+/).select(&:present?)
tag_list.each do |tag_name|
@tag = Tag.where(word: tag_name).first_or_initialize
@tag.save
unless ItemTagRelation.where(item_id: @item.id,tag_id: @tag.id).exists?
ItemTagRelation.create(item_id: @item.id, tag_id: @tag.id)
end
end
end
end
アイテムの情報を保存し「@item」という変数に代入しています。
tag_list = tag_ids.split(/[[:blank:]]+/).select(&:present?)は入力フォームのf.text_fieldから送られたタグをtag_idsとしてparamsで送信します。
そして、split(/[[:blank:]]+/)によってtag_ids内の文字列を空白で区切り、バラバラの単語にして配列に入れていきます。
最後に、select(&:present?)は、配列化した値をそれぞれpresent?メソッドで判定して、真であれば取り出します。
@tag = Tag.where(word: tag_name).first_or_initializeで新規タグか既存タグかの判別をします。
判別をして既存タグなら既存のidを使用。新規ならidを生成します。
unless ItemTagRelation.where(item_id: @item.id,tag_id: @tag.id).exists?は今回重複したタグを保存できないようにしたかったのですが、バリデーションやマイグレーションファイルに一意性を持たせても保存されてしまったので、苦肉の策でモデルにて対処しました。
ItemTagRelation.where(item_id: @item.id,tag_id: @tag.id).exists?で、中間テーブルであるitem_tag_relationモデルの投稿に対して、同じ名前のタグが存在していないかを.exists?で判定しています。
タグが重複した場合はtureになるのでunlessで条件式をかけています。
controllerの処理
$ rails g controller items
def new
@item = ItemTag.new
end
def create
@item = ItemTag.new(itemtags_params)
if @item.valid?
@item.save
redirect_to items_path(@item)
else
render :new
end
end
private
def itemtags_params
params.require(:item_tag).permit(:name, :text, :image, :tag_ids).merge(user_id: current_user.id)
end
formオブジェクトに対してnewメソッドを使用しています。
viewの作成
関連する所のみ記述します。
<%= form_with model: @item, url: items_path, local: true do |f| %>
<%= f.text_field :tag_ids %>
<%= f.submit "投稿する" %>
tag_idsはitemモデルで「 has_many :tags, through: :item_tag_relations」の関連付けをすることよってアイテムオブジェクトに使用できるようになります。
ひとまずこれで、タグ付け機能を実装できます。
参考にさせていただいた記事
【Ruby on Rails】タグ検索機能を実装してみた
https://qiita.com/E6YOteYPzmFGfOD/items/177f18e706df05f9b42e
初心者が手探りで Rails のタグ付機能を gem なしで実装してみる
https://qiita.com/ryutaro9595/items/042a1ec713c8c1f2c1d6
rails 投稿記事にタグをつける機能を実装する。
https://shirohige3.hatenablog.com/entry/2020/11/08/013327