railsで作っているサービスでタグ機能をつけたいなと思ったのでそのやり方をgem、gem以外でそれぞれ調べてみた。考えている仕様は下記。
要望・仕様
要望
- タグで検索できるように(タイトルなどと一緒に調べる可能性もある)
- タグの一覧も作る可能性あり
- タグページを作る
- タグに対して情報を付与する(公式タグと非公式タグを作りたい)
必要な仕様
- タグを検索できるようにする
- postgresで実装する
タグのDB設計
タグのDB設計は主にはその規模によって設計が変わってくる。下記3つがメジャー。
「タグ機能を実現するための便利なデータベース設計を3つ紹介」
https://colo-ri.jp/develop/2012/02/tag-database-schema-methods.html
この記事から引用。
MySQLicious法
投稿に対してタグを付ける場合、1行でその投稿、タグを詰め込んいく方法。タグは複数の場合、改行で入れていく。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の説明
上記3つをまず作成。
modelの関連付け
- tag,tag_mapのmodelを作成(https://qiita.com/you88/items/32f9799e79b9941dd01a)
- modelを関連付け(post_DB,tagmap_DB,tag_DB)
注意
has_many :tag_maps, dependent: :destroy
has_many :tags, through: :tag_maps
上記のように先にtag_mapをhas_manyで関連付けしないとエラーが出る。throughを定義する場合、それに関連するもの通過するものは先に関連付けさせる。
has_many :tag_maps, dependent: :destroy, foreign_key: 'tag_id'
has_many :posts, through: :tag_maps
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の編集
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が新しいものか古いものかで処理を変える
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に入れたほうが良いという話もあるが日本語だと長くなりがちなので無し
<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にも関係性を記載