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

#実装の流れ

* [tagモデルと中間テーブルを作成](#-tagモデルと中間テーブルを作成)

* [モデルの関連付けとバリデーション] (#-モデルの関連付けとバリデーション)

* [ルーティングを設定] (#-ルーティングを設定)

* [formオブジェクトの作成] (#-formオブジェクトの作成)

* [controllerの処理] (#-controllerの処理)  
 
* [viewの作成](#-viewの作成)

___

#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 
```

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

```ruby: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
```

```ruby: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
```

```ruby: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
```
___

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


```ruby: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モデルを通してアイテムに紐づくタグを取得します。

```ruby: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」の記述によって、タグの名前が重複して登録されるの防ぎます。（何故かうまく働きませんでした．．．）


```ruby:app/models/item_tag_relation.rb
class ItemTagRelation < ApplicationRecord
  belongs_to :item
  belongs_to :tag
end
```
___
#ルーティングを設定

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

#formオブジェクトの作成


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

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

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

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

```ruby: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
```

```ruby: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の作成
関連する所のみ記述します。

```ruby: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