LoginSignup
1
3

More than 3 years have passed since last update.

formオブジェクトでタグ付け機能を実装

Last updated at Posted at 2021-01-16

実装の流れ


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 

マイグレーションファイルを確認します。

db/migrate/create_items.rb
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
db/migrate/create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :word
      t.timestamps
    end
  end
end
db/migrate/create_item_tag_relations.rb
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

モデルの関連付けとバリデーション

app/models/item.rb
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モデルを通してアイテムに紐づくタグを取得します。

app/models/tag.rb
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」の記述によって、タグの名前が重複して登録されるの防ぎます。(何故かうまく働きませんでした...)

app/models/item_tag_relation.rb
class ItemTagRelation < ApplicationRecord
  belongs_to :item
  belongs_to :tag
end

ルーティングを設定

config/routes.rb
Rails.application.routes.draw do
 root to: 'items#index'
 resources :items
end

formオブジェクトの作成

Formオブジェクトは、1つのフォーム送信で複数のモデルを更新するときに使用するツールです。自分で定義したクラスをモデルのように扱うことができます。

modelsディレクトリ配下に「app/models/item_tag.rb」ファイルを作成します。

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で使用したいカラム名をセットします。

 続いて、フォームからパラメーターとして送られてきた情報をテーブルに保存する処理を追加します。

app/models/item_tag.rb
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
app/controllers/items_controller.rb
  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の作成

関連する所のみ記述します。

app/views/items/new.html.erb
<%= 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

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3