【Rails】検索タグ機能の実装 多対多
要件
- 募集(job)と検索タグ(search_tag)は多対多の関係
- モデルバリデーション
- 重複したタグNG
- 空白登録NG
- 募集に同じタグを複数設定NG
- 募集(job)に検索タグ(search_tag)を設定
- 検索タグ(search_tag)は複数設定可能
- 募集作成画面からタグを設定することが出来る
- 募集編集画面からタグを編集することが出来る
- 登録時のバリデーション
- DB(search_tag)に重複したタグNG(既にある場合は既存のものを使用)
- DB(search_tag)に空白登録NG
- 募集(job)に同じタグを複数設定NG
- 英語は全て小文字で登録
- DB登録時の表記揺れをなくすため(jAVa、javAなど)
- 検索タグ登録用のフォーム入力時、DBに存在するタグを予測変換
最低限の機能はこれぐらい。
テーブル作成
- 検索タグ(search_tag)テーブルを作成
- カラム:
name
のみ
- カラム:
- 多対多なので中間テーブル(search_tag_bind_job)の作成
- カラム:
job_id
、search_tag_id
- カラム:
- 募集(job)テーブルの作成は割愛
検索タグ(search_tag)のマイグレーションファイル
class CreateSearchTags < ActiveRecord::Migration[6.0]
def change
create_table :search_tags do |t|
t.string :name
t.timestamps
end
end
end
検索タグ(search_tag)のモデルファイル
class SearchTag < ApplicationRecord
# search_tag削除時、中間テーブルも削除
has_many :search_tag_bind_jobs, dependent: :destroy
has_many :jobs, through: :search_tag_bind_jobs
# 空白登録はNG
validates :name, presence: true
# 重複登録NG
validates :name, uniqueness: true
end
中間テーブル(search_tag_bind_job)のマイグレーションファイル
class CreateSearchTagBindJobs < ActiveRecord::Migration[6.0]
def change
create_table :search_tag_bind_jobs do |t|
t.references :job, null: false, foreign_key: true
t.references :search_tag, null: false, foreign_key: true
t.timestamps
end
# 募集に同じタグが複数設定出来ない複合ユニークキー制約を設定
add_index :search_tag_bind_jobs, [:job_id, :search_tag_id], unique: true
end
end
参考:railsで複合uniqueのvalidationをつけてやる
↓↓以下の部分が完了
-
募集と検索タグは多対多-
モデルバリデーション重複したタグNG空白登録NG募集に同じタグを複数設定NG
-
募集に検索タグを設定
-
やりたいこと
- 大元のフォームは以下のように募集(job)になっているので、このフォームの中で検索タグ(search_tag)のparametarを送信出来るようにする。
<%= form_with model: @job do |form| %>
-
必要なこと
- 関連付けしたモデルを一緒にデータ保存できるようにする
- accepts_nested_attributes_forを設定
- ストロングパラメーターの修正
- 関連付けしたモデルを一緒にデータ保存できるようにする
以下のようなパラメーターを目指します。
募集(job)の中に検索タグ(search_tag)をネストしている。
Parameters: { "job"=>{省略 , "search_tags_attributes"=>{"1644084312487"=>{"name"=>"検索タグ"}, "1644084320258"=>{"name"=>"呪術廻戦"}, "1644084324062"=>{"name"=>"ワンピース"}}},}
jobモデルにaccepts_nested_attributes_forを設定する
# 検索タグ
has_many :search_tag_bind_jobs, dependent: :destroy
has_many :search_tags, through: :search_tag_bind_jobs
accepts_nested_attributes_for :search_tag_bind_jobs, allow_destroy: true
accepts_nested_attributes_for :search_tags
これだけで、複数テーブルへの同時保存が可能。
次に、一般的にストロングパラメーターを設定していると思うので追加します。(設定していない場合は必要なし)
記述の仕方は、関連名_attributes
です。
(これは、accepts_nested_attributes_forのルール)
def job_params
params.require(:job).permit(:catch_copy, search_tags_attributes: [:name])
end
- accepts_nested_attributes_forを設定
- ストロングパラメーターの修正
この二つの作業で、関連付けしたモデルを一緒にデータ保存できるようになりました。
次に、view側ですが、僕が作成したviewは以下のような動きです。
-
やっている内容
- フォームに値を入力し、追加ボタンを押すと、入力した値を持ったチェックボックスが追加される
- 複数追加OK(複数の値をサーバーに渡す)
- 追加後、フォームの値はリセット
- 空欄NG
- 重複NG
- ※関連モデルを同時保存する際は、fields_forを使用することが多いと思いますが、今回は使っていません。(上記のようなUI UXを実現するのに必要なかったため。)
↓↓別記事で書いてます(処理の内容のみ)
参考:【Rails、JQuery】フォーム入力した値を動的にチェックボックスで追加する方法【メモ】
↓↓以下の部分まで完了
-
募集と検索タグは多対多-
モデルバリデーション重複したタグNG空白登録NG募集に同じタグを複数設定NG
-
-
募集(job)に検索タグ(search_tag)を設定検索タグ(search_tag)は複数設定可能募集作成画面からタグを設定することが出来る
募集編集画面からタグを設定、削除することが出来る
編集ページでは以下のような記述をしています。
<%= f.collection_check_boxes(:search_tag_ids, @job.search_tags, :id, :name) do |tag| %>
<%= tag.label do %>
<%= tag.check_box + tag.text%>
<% end %>
<% end %>
- ○○_ids
- has_manyで関連づけされたモデルでは、モデル名の単数形_idsという記述で、従属するモデルのid(主キー)の配列を返すメソッドが使用可能
- 参考:Railsドキュメント
- collection_check_boxes
- 参考:Railsドキュメント
def job_params
params.require(:job).permit(:catch_copy, search_tags_attributes: [:name], search_tag_ids: [])
end
編集時、上記画像の状態で登録した場合、
以下のようにidが1の検索タグと、2の呪術廻戦のみが渡されて、3のワンピースとの関連は削除されます。
Parameters: { "job"=>{省略 , "search_tag_ids"=>["", "1", "2"]},}
SearchTagBindJob Destroy (1.1ms) DELETE FROM `search_tag_bind_jobs` WHERE `search_tag_bind_jobs`.`job_id` = 149559 AND `search_tag_bind_jobs`.`search_tag_id` = 3
↓↓以下の部分まで完了
-
募集と検索タグは多対多-
モデルバリデーション重複したタグNG空白登録NG募集に同じタグを複数設定NG
-
-
募集(job)に検索タグ(search_tag)を設定検索タグ(search_tag)は複数設定可能求人作成画面からタグを設定することが出来る求人編集画面からタグを編集することが出来る
登録時のバリデーション
-
バリデーション
- DB(search_tag)に重複したタグNG
- DB(search_tag)に空白登録NG
- 募集(job)に同じタグを複数設定NG
登録前に処理を入れたい場合(今回でいう上記の三点)、コントローラに記述するか、以下のようにjobモデル内で、def search_tags_attributes=(search_tag_attributes)
メソッドを定義することで可能。
今回はjobモデル内に記述しています。
accepts_nested_attributes_for :search_tagsを定義することで、
Jobモデル内にsearch_tags_attributes=(search_tag_attributes)`というセッターメソッドが生成されて、フォームから送られてくるパラメータのsearch_tag_attributesによりセッターメソッドが呼ばれることで、Job.create()でJobモデルのレコードも同時に作成されるというカラクリ。
引用:https://zenn.dev/murakamiiii/articles/5ecefb7a58d1ef
# 検索タグ登録時の処理
def search_tags_attributes=(search_tag_attributes)
# 送られてきたタグパラメータの重複排除後、タグ追加の処理を行う
search_tag_attributes.values.uniq.each do |tag_params|
if tag_params["name"].present?
# DBに入る値を全て小文字にして統一(jAva、JAVaなどの乱立を防ぐため。)
tag_params["name"] = tag_params["name"].humanize(capitalize: false)
# DBに重複がない場合は作成、重複している場合は既に登録されているデータを使用
tag = SearchTag.find_or_create_by(tag_params)
# 募集に同じタグが紐づいていない場合、募集とタグを紐付け。(複合ユニークキー制約)
self.search_tags << tag if self.search_tags.where(name: tag["name"]).blank?
end
end
end
↓↓別記事のjs側でもある程度の制御はしているが、不十分なのでRails側でも処理を入れている。
参考:【Rails、JQuery】フォーム入力した値を動的にチェックボックスで追加する方法【メモ】
↓↓以下の部分まで完了
-
募集と検索タグは多対多-
モデルバリデーション重複したタグNG空白登録NG募集に同じタグを複数設定NG
-
-
募集(job)に検索タグ(search_tag)を設定検索タグ(search_tag)は複数設定可能求人作成画面からタグを設定することが出来る求人編集画面からタグを編集することが出来る
-
登録時のバリデーションDB(search_tag)に重複したタグNG(既にある場合は既存のものを使用)DB(search_tag)に空白登録NG募集(job)に同じタグを複数設定NG-
英語は全て小文字で登録DB登録時の表記揺れをなくすため(jAVa、javAなど)
検索タグ登録用のフォーム入力時、DBに存在するタグを予測変換
手順
- 検索タグ入力フォームのkeyupイベント時、ajax通信を行えるロジックを作成
- ルーティングを作成
- DBから取得するロジックを作成
1. 入力フォーム(.search-tag-field)を対象に、keyupイベントで通信が走るように設定
var jobs = jobs || {};
// DB(SearchTag)に保存されている検索タグを入力値から予測変換する関数
jobs.searchTag = function (target) {
target.on("keyup", function () {
var input_word = $(this).val(); // inputフィールドに入力された値
const dataList = function(request, response) {
$.ajax({
url: '/ajax/get_search_tag', // 検索タグを取得するメソッドに飛ばす
type: 'post',
data: {name: input_word}, // コントローラに渡すパラメータ(入力値を設定)
success: function (data) {
response(data);
},
});
}
target.autocomplete({
source: dataList, // 返却データ
autoFocus: true, // 自動的に先頭の項目にフォーカスするか
delay: 1000, // 入力してからサジェストが動くまでの時間(ms)
})
})
}
$(function () {
// 検索タグ入力フォーム
var search_tag_field = '.search-tag-field'
// 検索タグ予測変換イベント登録
jobs.searchTag($(search_tag_field))
}
2. ルーティングを設定
# 検索タグの取得
post 'get_search_tag'
3. 入力値を使用してDBに登録済みの値を取得
# 検索タグを取得(入力された文字列と学校IDで曖昧検索)
def get_search_tag
if input_words = params[:name].presence
# 入力された文字から検索タグを取得
# ヒット率を上げるため、【大文字・小文字】、【全角・半角】、【ひらがな・カタカナ】、【濁点・半濁点】を区別せず検索。
registered_tag_list = SearchTag.where("REPLACE(REPLACE(name, '゙', ''), '゚', '') collate utf8_unicode_ci like ?", "%#{input_words.strip}%").pluck(:name)
end
render json: registered_tag_list
end
ここまでで、最初に設定した要件を全てクリアできたと思います
-
募集と検索タグは多対多-
モデルバリデーション重複したタグNG空白登録NG募集に同じタグを複数設定NG
-
-
募集(job)に検索タグ(search_tag)を設定検索タグ(search_tag)は複数設定可能求人作成画面からタグを設定することが出来る求人編集画面からタグを編集することが出来る
-
登録時のバリデーションDB(search_tag)に重複したタグNG(既にある場合は既存のものを使用)DB(search_tag)に空白登録NG募集(job)に同じタグを複数設定NG-
英語は全て小文字で登録DB登録時の表記揺れをなくすため(jAVa、javAなど)
-
検索タグ登録用のフォーム入力時、DBに存在するタグを予測変換
まとめ
改めてまとめてみると、
同じバリデーションをかけていたりする部分はいいのかな?とか、変数名とか直した方がいいなと感じました。笑
Discussion