現在個人開発しているアプリにタグ検索機能を実装したのでここに備忘録としてまとめさせていただきます。
あえてgemを使っていないので本質的なところまで理解することができました。
【環境】
- windows10 Pro
- Rails: 6.0.3.2
- ruby: 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
- Docker for windows
- MySQL 5.7
- nginx:1.15.8
##機能の概要
新規投稿フォームにタグを入力すると、入力したタグが投稿一覧とサイドバーに反映されます。
サイドバーに反映されたタグをクリックするとそのタグが付いた投稿を絞り込むことができるという機能です。
↑で入力したタグがサイドバーと投稿一覧に表示される
サイドバーに表示されたタグをクリックするとそのタグを含む記事を絞り込むことができる。(今回はサンプルなので一件のみ)
##実装手順
Itemモデル及びPostモデルなど作成済みという前提で話しをします。(ここではitemモデルとします。各自置き換えてください。)
まず、Tagモデル、Tagmapモデルをいつものように作成します。
$ rails g model Tagmap item:references tag:references
$ rails g model tag tag_name:string
TagmapはItemモデルとTagモデルに紐づくのでデータ型にreferencesを付けてます。
これをするとRailsが良きにはからってくれますので詳しく知りたい方はググってみてください。
#マイグレーションファイル
class CreateTagmaps < ActiveRecord::Migration[6.0]
def change
create_table :tagmaps do |t|
t.references :item, null: false, foreign_key: true
t.references :tag, null: false, foreign_key: true
t.timestamps
end
end
end
こちらはインデックスを付けます。
class CreateTags < ActiveRecord::Migration[6.0]
def change
create_table :tags do |t|
t.string :tag_name, null: false
t.timestamps
end
add_index :tags, :tag_name, unique: true
end
end
その後いつものようにマイグレーションをします。
$ rails db:migrate
#アソシエーション
has_many :tagmaps, dependent: :destroy
has_many :tags, through: :tagmaps
class Tag < ApplicationRecord
has_many :tagmaps, dependent: :destroy
has_many :items, through: :tagmaps
end
tagもtagmapsと関連付けさせています。↑
thoroughを使うことで、tagmaps経由でitemsにアクセスできるようになってます。
class Tagmap < ApplicationRecord
belongs_to :item
belongs_to :tag
end
↑references形にするとbelongs_toの部分をRailsが自動で記述してくれます!
#コントローラとモデルの記述
def index
if params[:search].present?
items = Item.items_serach(params[:search])
elsif params[:tag_id].present?
@tag = Tag.find(params[:tag_id])
items = @tag.items.order(created_at: :desc)
else
items = Item.all.order(created_at: :desc)
end
@tag_lists = Tag.all
@items = Kaminari.paginate_array(items).page(params[:page]).per(10)
end
こちらのコードの解説をします。
indexアクションでif文を使い3パターン場合分けしていて、この結果によりitem変数に代入される値を変えています。
1.検索フォームに入力があった場合
2.paramsで:tag_idを受け取った場合(クリックしたときにパラメータでtag_idを受け取ったときに発動する。タグで絞込む機能)
3.普通にページを表示させた場合
ページネーション機能も設定しているので、item変数に格納される変数が3パターンになり、それぞれ最適化されたものが表示できるようになっています。
ページネーションではKaminariというgemを使っております。
def create
@item = Item.new(item_params)
tag_list = params[:item][:tag_name].split(nil)
@item.image.attach(params[:item][:image])
@item.user_id = current_user.id
if @item.save
@item.save_items(tag_list)
redirect_to items_path
else
flash.now[:alert] = '投稿に失敗しました'
render 'new'
end
end
itemsコントローラーのcreateアクションでのポイントは3行目のtag_list変数への代入部分です。
rubyのString#split は第 1 引数(今回はnil)で指定されたセパレータによって文字列を分割し、結果を文字列の配列で返します。ブロックを指定すると、配列を返す代わりに分割した文字列でブロックを呼び出します。
また、1 バイトの空白文字 ' 'で区切られたタグデータをフォームから送ると先頭と末尾の空白を除いたうえで、空白文字列で分割します。今回はこれを利用します。
新規投稿フォームから送られてきたパラメーターを空白文字(nil)でセパレイトして配列化し、Itemクラスで定義したsave_itemsメソッドを使い、データベースに保存します。(フォームにはタグをスペース区切りで入力してもらうように使用者にお願いする。)
#検索メソッド、タイトルと内容をあいまい検索する
def self.items_serach(search)
Item.where(['title LIKE ? OR content LIKE ?', "%#{search}%", "%#{search}%"])
end
def save_items(tags)
current_tags = self.tags.pluck(:tag_name) unless self.tags.nil?
old_tags = current_tags - tags
new_tags = tags - current_tags
# Destroy
old_tags.each do |old_name|
self.tags.delete Tag.find_by(tag_name:old_name)
end
# Create
new_tags.each do |new_name|
item_tag = Tag.find_or_create_by(tag_name:new_name)
self.tags << item_tag
end
end
↑タグデータを保存するとき、フォームから送られてきたタグデータのうち、すでに存在するタグネームがひとつでもあった場合はtagsテーブルのtag_nameカラムからpluckメソッドを使い一旦すべてのデータを引っ張ってきてcurrent_tagsに代入します。(すべて新しいものだった場合はnilになる。)
そしてすでに存在するタグデータの集合であるcurrent_tagsから、コントローラーから引数で渡ってきたtagsの配列を引くと古いタグ(old_tags)を定義することができます。
rubyでは配列の引き算をすると共通する要素を取り出すことができるためです。
具体例:
tags(tag_list = params[:item][:tag_name].split(nil)でコントローラーから渡ってくる配列) = ["Rails" "ruby" "React"]
current_tags(現在DBに存在しているタグデータ) = ["Rails" "ruby" "Vue.js"]
old_tags = ["Rails" "ruby" "Vue.js" "Docker"] - ["Rails" "ruby" "React"]
old_tags = ["Rails" "ruby"]
↑二つの配列の共通の要素が残る(すでに存在している古いタグを算出することができる)
#ビューの記述
新規投稿フォームの記述一部抜粋(CSSフレームワークにUIkitというのを使ってます)
.uk-form.new_post_form
= form_with(model: [@tag,@item], local: true) do |f|
.uk-margin-small
= f.text_field :title, placeholder: "タイトルを入力(35文字まで)", class: 'uk-input'
.uk-margin-small
= f.text_field :tag_name, placeholder: "プログラミング技術や募集要項に関するタグをスペース区切りで5つまで入力(例 Ruby Rails )", class: 'uk-input uk-form-small'
サイドバーのビュー一部抜粋
%li.search_friend_by_categorize
.uk-text-secondary.uk-text-bold
タグで探す
%ul.uk-flex.uk-flex-wrap
- @tag_lists.each do |list|
%li
= link_to list.tag_name, items_path(tag_id: list.id), class: 'tag_list'
投稿記事一一覧へのタグの表示
%p.tag_list_box
- item.tags.each do |tag|
= link_to "##{tag.tag_name}", items_path(tag), class: 'smaller tag_list'
viewはこのような感じになります。特に難しいことはないですね!
##最後まで読んでいただきありがとうございます!
少し駆け足で書いたので間違いなどあるかもしれません。もしなにかご指摘や感想などあればコメントいただけると嬉しいです!!