[Ruby on Rails] タグ付機能を実装してみた

#はじめに
今回は投稿に対してタグ付機能を付けられるようにしてみます。

###作るもの
今回は投稿にタグ付け機能を追加します。
（題材は自分のポートフォリオサイトです。尚、すでにUserテーブルとMicropostテーブルは作成済みです。）

###対象読者
railsチュートリアルに機能を追加したい等自分と同じ位のレベルの人を対象としています。

#作成の流れ
1.TagモデルとTag_relationshipモデルの作成
2.モデルの関連付けとバリテーション
3.対応するビューの作成
4.コントローラーのアクション作成
5.タグ編集機能の作成

#1.TagモデルとTag_relationshipモデルの作成
投稿とタグは多対多の関連付けになる為、中間にTag_relationテーブルを設けます。タグテーブルは投稿との関連付けを中間テーブルが行う為、タグの名前を保存する役割だけでOK。

Tag_relationship

|カラム|型|                    
|:--|:--|
|id|integer|
|micropost_id|integer|
|tag_id|integer|

Tag

|カラム|型|                    
|:--|:--|
|id|integer|
|name|string|

モデルを作成します。

```
rails g model tag name:string
```

```
rails g model tag_relationship micropost:references tag:references 
```
作成されたマイグレーションファイルを確認します。

```ruby:db/migrate
class CreateTags < ActiveRecord::Migration[5.1]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end
```
nameを空で保存されるのを防ぐためにnull:falseを追記してます。

```ruby:db/migrate
class CreateTagRelationships < class CreateTagRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :tag_relationships do |t|
      t.references :micropost, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
      add_index :tag_relationships, [:micropost_id,:tag_id],unique: true
  end
end


```

タグ検索機能を実装するのでnameにindexを付与。unipue:tureで同じ名前のタグを保存できないようにしています。
ちなみにindexは下記のように指定します。
タグ検索機能についてはこちら
[Ruby on Rails タグ検索機能実装](https://qiita.com/E6YOteYPzmFGfOD/items/177f18e706df05f9b42e)

```
add_index :テーブル名, :カラム名
```


```
rails db:migrate
```
#2モデルの関連付けとバリレーション
各モデルの関連付けは以下のようになります。

```ruby:app/model/tag.rb
class Tag < ApplicationRecord
  has_many   :tag_relationships, dependent: :destroy
  has_many   :microposts, through: :tag_relationships
  validates :name, uniqueness: true
end
```

```ruby/app/model/tag_relationship.rb
class TagRelationship < ApplicationRecord
  belongs_to :micropost
  belongs_to :tag
end
```

```ruby:app/model/micropost.rb
has_many  :tag_relationships, dependent: :destroy
  has_many  :tags, through: :tag_relationships
```
ここで「has_many:microposts, through: :tag_relationships」
とすることによって、tag_relationshipモデルを通してタグに紐づくマイクロポストを取得することができるようになります。（マイクロポスト側も同じ）
注意点として、スルーの関連付けの前にrelationshipsの中間テーブルの関連付けを記載しないとうまく動作しません。（先にhas_many:tags~とすると中間テーブルが関連づいてないので中間テーブルがありませんとエラーが出ます。）
validates :name, uniqueness: trueでタグの名前が重複して保存されるのを防ぎます。

#3対応するビューの作成
今回はマイクロポストを作成する際にタグをつけるような設定とします。

```ruby:app/views/microposts/new
<%= form_with model:@micropost, local: true do |f| %>
※タグの部分のみ抜粋
<%= f.text_field :tag_ids, class: "form-control", id:'tag_ids',\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>

<% end %>
```
タグを複数つけるときは（,）で区切ってもらうようにします。
「f.text_field :tag_ids」と記載することでマイクロポストを作成するタイミングでタグの情報も一緒に送ります。
※tag_idsはマイクロポストで「has_many :tags, through: :tag_relationships」の関連付けをすることよってマイクロポストオブジェクトに使用できるようになります。

```
例
Micropost.first.tag_ids
=> [2, 3, 4, 5]
```

タグはマイクロポストの中に表示するので、マイクロポストのビューの中にタグの表示を追記します。

```ruby:app/views/microposts/_micropost.html.erb
※タグを表示する部分のみ抜粋
<% micropost.tags.each do |tag| %>
   <%= tag.name %>
<% end %>
```

#4コントローラのアクション作成
マイクロポストの情報と一緒にタグの情報も送られてくるようになったのでマイクロポストのcreateアクションでタグも保存されるようにします。

```ruby:app/controllers/microposts_controller.rb
def create
    @micropost = current_user.microposts.build(micropost_params)
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.save
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿しました!'
      redirect_to root_url
    else
      render 'new'
    end
  end
```
「tag_list」で送られてくるタグの値を取得します。複数のタグ付を行うときにsplit(',')で区切るようにします。
「@micropost.save_tags(tag_list)」でマイクロポストにタグを関連付けます。save_tagsメゾットの中身はこんな感じです。

```ruby:app/model/micropost.rb
def save_tags(savemicropost_tags)
    savemicropost_tags.each do |new_name|
    micropost_tag = Tag.find_or_create_by(name: new_name)
    self.tags << micropost_tag
end
```
「find_or_create_by」メゾットはカラムの中から同じ値がないか探して、あればそのままfindの動き、なければcreateの動きで新たにカラムに保存します。
これでタグ付機能は作成完了です。

#タグ編集機能の作成
ここからはタグの編集機能を付けていきます。
タグの編集はマイクロポストの編集ページで行います。（すでにマイクロポスト編集ページ作成済みの前提で行います）

※タグの部分のみ抜粋（form_withを使用しているのでrenderでマイクロポストのフォームページと共有できます。）

```ruby:app/micropost/edit
<%= f.text_field :tag_ids,value: @tag_list,\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>
```
ここで「@tag_list」で既存の値を表示しています。
それを踏まえたコントローラのアクションがこちらです。

```ruby:app/controllers/microposts_controller.rb
def edit
    @micropost =Micropost.find(params[:id])
    @tag_list =@micropost.tags.pluck(:name).join(",")
  end

  def update
    @micropost =Micropost.find(params[:id])
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.update_attributes(micropost_params)
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿を編集しました‼'
      redirect_to @micropost
    else
    render 'edit'
    end
  end
```
まずeditアクションには既存のタグを取得すために「@tag_list」を追記します。
tagsでマイクロポストに関連するタグを取得
pluck(:name)でタグのnameの配列を取得
join(",")で配列を,で区切った文字列として取得します。

「save_tags」を更新用に編集します。




※マイクロポスト編集ページへの「link_to」で行き先を「edit_micropost_path」としたときにマイクロポストshowページ以外からリンクを踏んでアクセスするとタグフォームに既存の値が入らずうまくタグの更新ができなかったので「/microposts/#{micropost.id}/editでリンクを設定してます。

```ruby:app/model/micropost.rb
def save_tags(savemicropost_tags)
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    old_tags = current_tags - savemicropost_tags
    new_tags = savemicropost_tags - current_tags

    old_tags.each do |old_name|
      self.tags.delete Tag.find_by(name: old_name)
    end

    new_tags.each do |new_name|
      micropost_tag = Tag.find_or_create_by(name: new_name)
      self.tags << micropost_tag
    end
  end
```
動作は既存のタグを残すものと消すものに分けて、新しいタグを既存の残すタグに追加していくといった形です。
「current_tags」既存のタグを取得
「old_tags」消すタグを取得。
「new_tags」新たに追加するタグを取得。
old_tagsとnew_tagsにある「ー」は引き算です。
それぞれ引き算して残ったタグを繰り返し処理して完了です。
例
 current_tags         - savemicropost_tags =old_tags
「test, test2, test3」  - [test, test3] =[test2]

以上でタグ付機能は完成となります。最後までお読みいただきありがとうございました。
アドバイス等いただけるととても喜びます。次は今回作ったタグを利用した検索機能を作ろうと思います。
ありがとうございました。