railsでタグ付け機能を実装する方法(投稿から編集まで)

# 開発環境
Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系

# 前提

- 投稿機能は実装済み(今回の例ではpostモデル)
- 編集機能は実装済み

- 手法として今回はFormオブジェクトモデルで実装します。(1つのフォームからpostsテーブルとtagsテーブルに保存する実装を目指します)



以下が今回登場する各テーブルです。
## postsテーブル

| Column             | Type       | Options                         |
| ------------------ | ---------- | ------------------------------- |
| title              | string     | null: false                     |
| explanation        | text       |                                 | 
| category_id        | integer    | null: false                     |
| animal_name        | string     |                                 |
| user               | references | null: false, foreign_key: true  |

### Association

- belongs_to :user
- has_many   :comments
- has_many   :likes
- has_many   :tags
- has_many   :post_tag_relations

## tagsテーブル

| Column           | Type       | Options                        |
| ---------------- | ---------- | ------------------------------ |
| name             | string     | null: false, uniqueness:: true |

### Association

- has_many :posts
- has_many :post_tag_relations

## post_tag_relationテーブル

| Column           | Type       | Options                        |
| ---------------- | ---------- | ------------------------------ |
| tag              | references | null: false, foreign_key: true |
| post             | references | null: false, foreign_key: true |

### Association

- belongs_to :post
- belongs_to :tag


# モデル作成

まずはモデルを作成します。
postモデルとtagモデルは多対多の関係になるので、中間テーブル(post_tag_relation)も同時に設ける必要があります。

```
# ターミナル
% rails g model tag
```

```
# ターミナル
% rails g model post_tag_relation
```

# マイグレーションファイルを編集

```20210206083851_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      # 下記1行を追加
      t.string :name, null:false, uniqueness: true

      t.timestamps
    end
  end
end
```

```20210206083924_create_post_tag_relations.rb
class CreatePostTagRelations < ActiveRecord::Migration[6.0]
  def change
    create_table :post_tag_relations do |t|
      # 下記2行を追加
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
  end
end
```

編集が終わったら、マイグレーションを行います

```
# ターミナル
% rails db:migrate
```

# アソシエーションを記述

```post.rb
class Post < ApplicationRecord
 # 省略

  has_many :post_tag_relations, dependent: :destroy
  has_many :tags, through: :post_tag_relations

 # 省略 
end
```

```post_tag_relation.rb
class PostTagRelation < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end
```

```tag.rb
class Tag < ApplicationRecord
  has_many :post_tag_relations, dependent: :destroy
  has_many :posts, through: :post_tag_relations

  validates :name, uniqueness: true
end
```
tag.rbでは同じタグが複数生成されないように、バリデーションを設けました。

# Formオブジェクト導入

modelsディレクトリにposts_tag.rbファイルを作成します。
作成したら、１つのフォームから複数テーブルにデータを保存するためにファイルを編集していきます。

完成形は以下になります。(後から順番に説明します。)

```posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id

  with_options presence: true do
    validates :images
    validates :title
    validates :name
    validates :category_id, numericality: { other_than: 1 , message: "は--以外から選んでください"} 
  end

  def save
    post = Post.create(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    PostTagRelation.create(post_id: post.id, tag_id: tag.id)
  end

  def update
    @post = Post.where(id: post_id)
    post = @post.update(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    tag = Tag.where(name: name).first_or_initialize
    tag.save
    
    map = PostTagRelation.where(post_id: post_id )
    map.update(post_id: post_id, tag_id: tag.id)
  end
end
```
---

順番に実装していくと、まずはPostsTagクラスを作り、include ActiveModel::Modelを追加します。

```posts_tag.rb
class PostsTag
  include ActiveModel::Model
end
```
Formオブジェクトでは、クラスを自分で定義してモデルのように扱うことができ、ActiveModel::Modelというモジュールを読み込むことで、ヘルパーメソッドなどが使えます。

---
その後、1つのフォームで同時に保存したい値をattr_accessorを使ってPostsTagクラスで使えるようにします。


```posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id
end
```

コントローラーに記述しているであろうparamsで送られてきたデータを渡すようなイメージです。

```posts_controller.rb
#わかりやすくするために出してきただけなので、このタイミングで編集する必要はありません
private

  def post_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id)
  end

  def update_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id, post_id: params[:id])
  end
```



---
次に必要なバリデーションを記述します。
かなり人によって違うと思うので、必要なバリデーションを記述しましょう。

基本的には投稿機能を実装した際のバリデーションに、タグ名のバリデーションを足すような形になると思います。

```posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id

  with_options presence: true do
    validates :images
    validates :title
    validates :name
    validates :category_id, numericality: { other_than: 1 , message: "は--以外から選んでください"} 
  end

end
```

---

保存と編集をするためのメソッドを作ります。
やっていることとしては、順番に保存する記述をしているだけです。(編集は該当のレコードを探してくるコードが増えます。)
これを記述するとコントローラーで、PostsTag.saveやPostsTag.updateが使えるようになります。

```posts_tag.rb
 def save
    # まずは投稿を保存
    post = Post.create(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    # その後タグを保存
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    # 最後に中間テーブルにpostとtagを紐付け、保存する
    PostTagRelation.create(post_id: post.id, tag_id: tag.id)
  end

  def update
    # 編集したい投稿をとってくる
    @post = Post.where(id: post_id)
    # データを更新
    post = @post.update(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    # その後タグを保存
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    # 最後に更新したpostとtagを紐付け、中間テーブルを更新する
    map = PostTagRelation.where(post_id: post_id )
    map.update(post_id: post_id, tag_id: tag.id)
  end
```
formオブジェクト導入は以上です。

# コントローラー編集

続いてコントローラーを編集します。

```post_controller.rb

# 関係あるところだけ抜粋

before_action :find_post, only: [:show, :edit, :update, :destroy]

  def new
    @post = PostsTag.new
  end

  def create
    @post = PostsTag.new(post_params)
    if @post.valid?
      @post.save
      redirect_to root_path
    else
      render :new
    end
  end
  
  def edit
    @form = PostsTag.new(title: @post.title, animal_name: @post.animal_name, category_id: @post.category_id, 
      explanation: @post.explanation)
  end

  def update
    @form = PostsTag.new(update_params)
    if @form.valid?
      @form.update
      redirect_to root_path
    else
      render :edit
    end
  end

  private
  def post_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id)
  end

  def update_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id, post_id: params[:id])
  end

  def find_post
    @post = Post.find(params[:id])
  end

```
ポイントは2つあるかなと思います

- PostTagクラスのインスタンスに変更すること  (例: Post.new　→　PostTag.new)
- updateの際は、どの投稿かを判別する必要があるので、post_idもマージしてあげること



# その他(該当する方だけ)

アクティブストレージで、画像投稿機能を実装している場合や、変数を@postから@formなどに変更している場合は、投稿フォームのビューのコードを修正する必要があります。

コントローラーで変数名を変えた場合はモデル名を合わせる必要があります。

```
# 変更なしの場合
<%= form_with model: @post, url: posts_path, local: true do |f| %>
<%= render "shared/error_messages", model: @post %>

# コントローラーで@formに変更した場合
<%= form_with model: @form, url: posts_path, local: true do |f| %>
<%= render "shared/error_messages", model: @form %>
```


アクティブストレージで、画像投稿機能を実装している場合はフォームのname属性を変更する必要があります。

```
# 変更前
<%= f.file_field(:images, name: 'post[images][]', class:"post-image", id:"post_image")%>

# 変更後
<%= f.file_field(:images, name: 'posts_tag[images][]', class:"post-image", id:"post_image")%>
```

また、エラーメッセージの日本語化を行っている場合は再び英語に戻っていると思うので、再度configファイルを編集する必要があります。

```config/locales/models/ja.yml
# 変更前
ja:
  activerecord:
    models:
      post: 投稿
    attributes:
      post:
        images: ペットの写真
        title: 写真のタイトル
        category_id: ペットの種類
  time:
    formats:
      default: "%Y/%m/%d %H:%M"
```

```config/locales/models/ja.yml
# 変更後
ja:
  activemodel:
    models:
      posts_tag: 投稿 
    attributes:
      posts_tag: 
        images: ペットの写真
        title: 写真のタイトル
        category_id: ペットの種類
        name: タグ
  time:
    formats:
      default: "%Y/%m/%d %H:%M"
```

# ビューでタグを出力

あとはコントローラーとビューを編集し、タグを出力します。

```posts_controller.rb
def show
   @tags = @post.tags
 end
```

```show.html.erb
<tr>
  <th class="detail-item">タグ</th>
  <td class="detail-value">
     <% @tags.each do |tag| %>
       <span class="tags"><%= tag.name %></span>
     <% end %>
  </td>
</tr>
```


以上です。
参考になれば幸いです。