やりたいこと
「タスクテーブル(tasks)」に、「タグマスタ(tags)」情報を複数紐付ける。
####↓↓↓↓tag関係のアソシエーション情報↓↓↓↓############
has_many :task_tags , dependent: :destroy
# nestedfieldの使用にあたり、↓を追記
accepts_nested_attributes_for :task_tags, allow_destroy: true
has_many :pasted_tags , through: :task_tags , source: :tag
####↑↑↑↑tag関係のアソシエーション情報↑↑↑↑############
####↓↓↓↓アソシエーション情報↓↓↓↓############
has_many :task_tags, dependent: :restrict_with_error
####↑↑↑↑アソシエーション情報↑↑↑↑############
####↓↓↓↓アソシエーション情報↓↓↓↓############
belongs_to :task
belongs_to :tag
####↑↑↑↑アソシエーション情報↑↑↑↑############
- 「tasks」の登録時に、複数の「tags」を紐付けられるようにする。
- 一つの 「tasks」情報に対しては、同じ「tags」情報は紐付けられないようにする。
「nested_form_fields」を使ってみると……
使い方については、
https://qiita.com/classe729/items/651c0f07e1f442d99c74
https://www.virment.com/add_and_remove_rails_nested_form_dynamically/
参照(特に1つ目のリンクにあるように、requireの入れ忘れには要注意)。
なおrequireは、記述する順番にも注意しないと既存のjs関係に影響を与える場合もありそう
……自分の場合、なんにも考えず
//= require jquery3 ←他のjs用に、すでに書いていたもの
……中略……
//= require jquery ←今回追記したもの
とかしたら、rspecテストでjsが動かなくなった(いや、「jquery3」があるなら「jquery」要らんだろ、気付けよ!)。
【ちょっと寄り道】
task_tagsの「tag_id」は、【登録されているtags情報のどれか】を選択して登録する
→「nested_form_fields」内の入力フォームも一工夫(本題とは関係ないけど)。
tasksの登録、更新画面
<%= form.nested_fields_for :task_tags, wrapper_tag: :tr do |q| %>
<td ><%= q.select :tag_id, get_tags_as_selectbox_info , class: 'form-control' %></td>
<td ><%= q.remove_nested_fields_link 'Delete', class: 'btn btn-danger', role: 'button' %></td>
<% end %>
で、この二行目の「get_tags_as_selectbox_inf」は
module TagsHelper
def get_tags_as_selectbox_info
tags = Tag.all.order(cd: "ASC")
selectbox_info = []
tags.each do |tag|
# 表示形式は「コード:名称」、登録はidで行う。
selectbox_info.push(["#{tag.cd}:#{tag.name}",tag.id])
end
return selectbox_info
end
end
とすることで、tagの「コード:名称」を画面上で表示・選択。
選択されたtagのidを「task_tags」の「tag_id」として登録することにする。
↓↓ 実装結果は、こんな感じ ↓↓ ……ここまでは(比較的)順調だった。
これで通常の登録、削除はできるようになった。
ただ↑だと一つのtaskに「001:社長案件」という同じtag(画面表示上は「ラベル」)を複数登録しようとしている。
……こういった情報をバリデーションでどう弾くか?
標準の「uniqueness」制約を加えればいい……そんなふうに考えていた時期が俺にもありました。
え、だってつまり、中間テーブル「task_tags」で【「task_id」+「tag_id」】をユニークにすればいいんでしょ?
########↓バリデーション情報↓########
validates :task_id, uniqueness: { scope: :tag_id}
########↑バリデーション情報↑########
としてみたら、通るはずのものがバリデーションエラーになったり、エラーになるはずのものが通ったり。
これは推測も含みますが、
【rails標準のバリデーション】
→登録しようとしている情報を、一行ずつ順番に現在時点(=登録前)のテーブル情報と比較して、それを登録できるか判断。
【「nested_form_fields」による登録……いや、普通の登録でも同じかも?】
→上記バリデーションが全て終わった後で、改めてデータを登録。
としているっぽい。
なので、
タグ「001:社長案件」が登録されているタスクに対し、
例①)
①ー1:登録されている「001:社長案件」をDelete
①ー2:「Add new」で追加した「タグのselect枠」で、改めて「001:社長案件」を選択
(画面上の表示は変化なし)
→「①ー2」についてのバリデーション時は、DBにはまだ「①ー1」情報が残っている
→バリデーションに引っかかる
例②)
②ー1:「Add new」で追加した「タグのselect枠」で、「101:要、社長決済」を選択
②ー2:「Add new」で追加した「タグのselect枠」で、「101:要、社長決済」を(追加でもう一つ)選択
→「②ー2」についてのバリデーション時は、DBにはまだ「②ー1」情報が登録されていない
→バリデーションに引っかからない
となってしまう模様……orz。
仕方がないから独自バリデーションを……掛けるためのデータはどう取得すればいいんだろう?
「task_tags」に登録する情報についてのバリデーションではあるのだが……
「task_tags」モデルでのバリデーションだと、【登録する「task_tags」、1レコードごと 】についてしか確認できない。
仕方がないので「tasks」モデルのバリデーションで、【紐付いて登録するはずの「task_tags」情報を確認】する方法を探してみた。
↑の情報を、
・1行目、2行目をDelete(削除)→「500:中止の可能性あり」が新たな1行目になる。
・新たな2行目、3行目として「003:部長案件」「103:要、部長決済」を追加
この登録時、taskモデルバリデーションを走るプログラムをbinding.pryで止めてみると
[1] pry(#<Task>)> task_tags
TaskTag Load (0.2ms) SELECT "task_tags".* FROM "task_tags" WHERE "task_tags"."task_id" = $1 [["task_id", 47]]
=> [#<TaskTag:0x00007f960d851bf0 id: 40, task_id: 47, tag_id: 3, created_at: Sun, 05 May 2019 13:45:41 JST +09:00, updated_at: Sun, 05 May 2019 13:46:46 JST +09:00>,
#<TaskTag:0x00007f960d8519e8 id: 41, task_id: 47, tag_id: 5, created_at: Sun, 05 May 2019 13:45:41 JST +09:00, updated_at: Sun, 05 May 2019 13:46:46 JST +09:00>,
#<TaskTag:0x00007f960d8517e0 id: 42, task_id: 47, tag_id: 11, created_at: Sun, 05 May 2019 13:45:41 JST +09:00, updated_at: Sun, 05 May 2019 13:46:46 JST +09:00>,
#<TaskTag:0x00007f960d85b3a8 id: nil, task_id: 47, tag_id: 15, created_at: nil, updated_at: nil>,
#<TaskTag:0x00007f960d858f18 id: nil, task_id: 47, tag_id: 17, created_at: nil, updated_at: nil>]
[2] pry(#<Task>)> task_tags[0]._destroy
=> true
[3] pry(#<Task>)> task_tags[1]._destroy
=> true
[4] pry(#<Task>)> task_tags[2]._destroy
=> false
・『task_tags[行番号]』で、【今回追加登録,更新,削除する情報(及び、変更なしで登録したままにしておく情報)】を取得できる。
・『task_tags[行番号]._destroy』で、それが【今回削除する情報】かどうかを確認できる(削除ならTrue、そうでないならFalseが返される。
ーーこれだ! これが欲しかったんだ!(見つけるのに数時間掛かった)
後は、これを使ってバリデーションをでっち上げれば解決。
taskモデルにて、
class Task < ApplicationRecord
validate :tag_not_deplicate
def tag_not_deplicate
#「今回登録しようとしているタグ」を集める枠を用意する
tags_try_to_save=[]
task_tags.each do |task_tag|
#「_destroy」ではない→「今回登録しようとしているタグ」であるならば
if task_tag._destroy == false
#そのタグidを枠に格納
tags_try_to_save.push(task_tag["tag_id"] )
end
end
########中略########
end
書き方はもう少し工夫できるかもだけど、これでとりあえずバリデーションが効くようになった。
でも削除データかどうかを識別するための「_destroy」のT/F判定とか、gemは何かカスタマイズしようとした時の情報取得方法が難しすぎる(今回も、コントローラ内で取得できるparams情報とは、びみょーにデータの持ち方が違うんです)。
参考用に、登録時にコントローラ内でbinding.pryした場合の、paramsが持っているデータ
(細かい数値(id)は、先程のものとは一致していません)。
77: def update
78: binding.pry
=> 79: respond_to do |format|
80: if @task.update(task_params)==true
81: format.html{redirect_to edit_task_path(@task) , notice: t('activerecord.normal_process.do_update') }
82: else
83: format.html{render "edit"}
84: end
85: end
86: end
[1] pry(#<TasksController>)> params
=> <ActionController::Parameters {"utf8"=>"✓", "_method"=>"patch", "authenticity_token"=>"S3OMv57a9ZUTRB8qbJ8YvdFBc3hyZZX0aOcPTuS27HeOgaStaopBXdsVsUChtGBupzsT36DZ+zUGKt7qzkyNDQ==", "task"=>{"user_id"=>"2", "status"=>"0", "name"=>"qwe", "content"=>"wer", "limit"=>"", "priority"=>"2", "task_tags_attributes"=>{"0"=>{"_destroy"=>"1", "tag_id"=>"11", "id"=>"42"}, "1"=>{"_destroy"=>"1", "tag_id"=>"15", "id"=>"43"}, "2"=>{"tag_id"=>"17", "id"=>"44"}, "3"=>{"tag_id"=>"4"}, "4"=>{"tag_id"=>"3"}}}, "commit"=>"登録する", "controller"=>"tasks", "action"=>"update", "id"=>"47"} permitted: false>
[2] pry(#<TasksController>)> params[:task][:task_tags_attributes]
=> <ActionController::Parameters {"0"=>{"_destroy"=>"1", "tag_id"=>"11", "id"=>"42"}, "1"=>{"_destroy"=>"1", "tag_id"=>"15", "id"=>"43"}, "2"=>{"tag_id"=>"17", "id"=>"44"}, "3"=>{"tag_id"=>"4"}, "4"=>{"tag_id"=>"3"}} permitted: false>
あるいは、もっと簡単なやり方があるのだろうか?