railsでタグ機能を実装する

railsで作っているサービスでタグ機能をつけたいなと思ったのでそのやり方をgem、gem以外でそれぞれ調べてみた。考えている仕様は下記。

# 要望・仕様

要望

* タグで検索できるように（タイトルなどと一緒に調べる可能性もある）
* タグの一覧も作る可能性あり
* タグページを作る
* タグに対して情報を付与する（公式タグと非公式タグを作りたい）

必要な仕様

* タグを検索できるようにする
* postgresで実装する

# タグのDB設計

タグのDB設計は主にはその規模によって設計が変わってくる。下記３つがメジャー。

「タグ機能を実現するための便利なデータベース設計を３つ紹介」
https://colo-ri.jp/develop/2012/02/tag-database-schema-methods.html

この記事から引用。

## MySQLicious法

投稿に対してタグを付ける場合、１行でその投稿、タグを詰め込んいく方法。タグは複数の場合、改行で入れていく。DBが一つですむので実装は最もシンプル。

### メリット

* 最もシンプルで実装が楽

### デメリット

* 改行でいくつもテキストで入れるため型をテキストなどにしないと数が制限される。しかし、テキストだと重い。
* 検索をLIKE "%HTML%"のように部分一致で検索していく（改行で1セルに全部入っているため）しかし、そのままやるとXHTMLのように部分一致のものも引っかかる
    * これは単語の間にスペースを入れると解決

## Scuttle法

投稿にタグを付与する場合、投稿の他にもう一つタグ管理用のDBを用意する方法。

* 投稿DB：id
* tag_DB:id,投稿DB_id,tag名

複数タグをつける場合、投稿DB_idに同じ値のものがある状態になる。タグのリストを作成する場合にはtag名でユニーク関数。

### メリット

* 数の制限はなくなった

## Toxi法

3つのテーブルを作る方法。最も正規化でき、タグに情報を付与することもできる。Scuttle法と違うのはtagを違うDBで管理することで情報を付与しやすくした。（疑問tagmapDBは1行につき1tag_idにすべきか改行、もしくはコンマで1セルに入れてしまうか）

* 投稿DB:投稿_id
* tagmapDB:投稿_id,tag_id
* tagDB:tag_id,tag名,tagの情報（あったら）

# タグ実装のgem

acts-as-taggable-onが一番有名で下記の記事で実装方法がまとまっている

「動的なタグ生成をするgem「acts-as-taggable-on」を使ってみました」
https://qiita.com/guri3/items/c667ce2bfbb5baca4b5a

# gem以外での実装方法（Toxi法にて）

## 全体の流れ

* tableの作成
* modelの関連付け（tableの関連付けを行い、tagに保存したらtag_mapにpost_idとtag_idが保存されるようにする）
* formからpostを投稿する際にtagも入力
* createでformから飛ばすがpostとは別にtagを飛ばし、paramsでtag_tableに飛ばす
* tagが新しいものか古いものかで処理を変える

## tableの作成

* post_DB:投稿_id
* tagmap_DB:投稿_id,tag_id
* tag_DB:tag_id,tag名,tagのタイプ（公式か非公式か）,tagの説明

上記３つをまず作成。

### modelの関連付け

* tag,tag_mapのmodelを作成（https://qiita.com/you88/items/32f9799e79b9941dd01a）
* modelを関連付け（post_DB,tagmap_DB,tag_DB）

注意

```ruby:post.rb
  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps
```

上記のように先にtag_mapをhas_manyで関連付けしないとエラーが出る。throughを定義する場合、それに関連するもの通過するものは先に関連付けさせる。

```tag.rb
  has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id'
  has_many :posts, through: :tag_maps
```

```tag_map.rb
  belongs_to :post
  belongs_to :tag
  validates :post_id,presence:true
  validates :tag_id,presence:true
```

tagとtagmapにも関連づけを

### formからpostを投稿する際にtagも入力-controller

* tagの入力
    * viewにformを書く
    * controllerにpostをnewで作成するときに一緒にtagも保存。この時tagmap,tagDBも一緒に
* tagの削除（投稿の編集ページにて削除する）
    * viewに編集画面
    * destroyをcontrollerに
* 投稿のtagの編集

```ruby:post_controller.rb
  def create
    @post = Post.new(title:params[:title],location:params[:location],gender:params[:gender],line_id:params[:line_id],user_id:params[:user_id],description:params[:description])
    @post.save
    redirect_to("/")
    tag_list = params[:tag_name].split(",")
    if @post.save
      @post.save_posts(tag_list)
    end
  end
```

上記のようにPost.newとは違い、postではなくtagに送るので別個で処理する。
同じリクエストでこのように違うtableへの処理ができる。tag_list = params[:tag_name].split(",")はstringを配列で渡すために間に,を入れて仕切っている。

### tagが新しいものか古いものかで処理を変える

```ruby:post.rb
class Post < ApplicationRecord
  has_many :tag_maps, dependent: :destroy
  has_many :tags, through: :tag_maps

  def save_posts(savepost_tags)
  # 現在のユーザーの持っているskillを引っ張ってきている
  current_tags = self.tags.pluck(:name) unless self.tags.nil?
  # 今postが持っているタグと今回保存されたものの差をすでにあるタグとする。古いタグは消す。
  old_tags = current_tags - savepost_tags
  # 今回保存されたものと現在の差を新しいタグとする。新しいタグは保存
  new_tags = savepost_tags - current_tags

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

    # Create new taggings:
    new_tags.each do |new_name|
      post_tag = Tag.find_or_create_by(name:new_name)
      # 配列に保存
      self.tags << post_tag
    end
  end
end
```

old_tags = current_tags - savepost_tags

このように配列から配列を除外することができる。


## タグページ（viewの作成）

viewでタグの入力部分はjsで入力しやすくしたい。vue.jsでそれができるかどうか検討。

* viewにtagページ作成
* tagごとに/tag/tag_idのページを生成
    * SEO的にtag名をURLに入れたほうが良いという話もあるが日本語だと長くなりがちなので無し

```ruby:form
    <label for="exampleInputEmail1">タグ</label>
    <textarea name="tag_name" class="form-control post_form"></textarea>
```

* すごく簡単に書くと上記のようにtag_nameで渡し、
* controllerのtag_list = params[:tag_name].split(",")で配列化
* @post.save_posts(tag_list)でpost.rbのsave_postメソッドを実行。新しいものの場合、tagtableに保存する
* その際、同時にtag_mapにも関係性を記載