LoginSignup
1
0

More than 3 years have passed since last update.

nested_form_fieldsを使って複数情報を一括で「登録+削除」する場合、登録情報の制約は独自バリデーションにする必要があるっぽい?

Posted at

やりたいこと

「タスクテーブル(tasks)」に、「タグマスタ(tags)」情報を複数紐付ける。

  • 「tasks」と「tags」の関係は【N : N】
    • 中間テーブルとして「task_tags」テーブルを使用。 スクリーンショット 2019-05-05 11.52.18.png
task.rb
  ####↓↓↓↓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関係のアソシエーション情報↑↑↑↑############
tag.rb
  ####↓↓↓↓アソシエーション情報↓↓↓↓############
  has_many :task_tags, dependent: :restrict_with_error
  ####↑↑↑↑アソシエーション情報↑↑↑↑############
task_tag.rb
  ####↓↓↓↓アソシエーション情報↓↓↓↓############
  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.hetl.erb
<%= 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」は

tags_helper.rb
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」として登録することにする。

↓↓ 実装結果は、こんな感じ ↓↓ ……ここまでは(比較的)順調だった。

スクリーンショット 2019-05-05 12.54.03.png
これで通常の登録、削除はできるようになった。
ただ↑だと一つのtaskに「001:社長案件」という同じtag(画面表示上は「ラベル」)を複数登録しようとしている。
……こういった情報をバリデーションでどう弾くか?

 標準の「uniqueness」制約を加えればいい……そんなふうに考えていた時期が俺にもありました。

え、だってつまり、中間テーブル「task_tags」で【「task_id」+「tag_id」】をユニークにすればいいんでしょ?

task_tag.rb
  ########↓バリデーション情報↓########
  validates :task_id,  uniqueness: { scope: :tag_id}
  ########↑バリデーション情報↑########

としてみたら、通るはずのものがバリデーションエラーになったり、エラーになるはずのものが通ったり。
これは推測も含みますが、

【rails標準のバリデーション】
→登録しようとしている情報を、一行ずつ順番に現在時点(=登録前)のテーブル情報と比較して、それを登録できるか判断。
【「nested_form_fields」による登録……いや、普通の登録でも同じかも?】
→上記バリデーションが全て終わった後で、改めてデータを登録。

としているっぽい。

なので、
タグ「001:社長案件」が登録されているタスクに対し、
スクリーンショット 2019-05-05 13.17.33.png

例①)
①ー1:登録されている「001:社長案件」をDelete
①ー2:「Add new」で追加した「タグのselect枠」で、改めて「001:社長案件」を選択
(画面上の表示は変化なし)

→「①ー2」についてのバリデーション時は、DBにはまだ「①ー1」情報が残っている
→バリデーションに引っかかる

例②)
②ー1:「Add new」で追加した「タグのselect枠」で、「101:要、社長決済」を選択
②ー2:「Add new」で追加した「タグのselect枠」で、「101:要、社長決済」を(追加でもう一つ)選択
スクリーンショット 2019-05-05 13.26.31.png

→「②ー2」についてのバリデーション時は、DBにはまだ「②ー1」情報が登録されていない
→バリデーションに引っかからない

となってしまう模様……orz。

仕方がないから独自バリデーションを……掛けるためのデータはどう取得すればいいんだろう?

「task_tags」に登録する情報についてのバリデーションではあるのだが……

「task_tags」モデルでのバリデーションだと、【登録する「task_tags」、1レコードごと 】についてしか確認できない。
仕方がないので「tasks」モデルのバリデーションで、【紐付いて登録するはずの「task_tags」情報を確認】する方法を探してみた。

スクリーンショット 2019-05-05 13.47.29.png
↑の情報を、
・1行目、2行目をDelete(削除)→「500:中止の可能性あり」が新たな1行目になる。
・新たな2行目、3行目として「003:部長案件」「103:要、部長決済」を追加
スクリーンショット 2019-05-05 13.51.15.png

この登録時、taskモデルバリデーションを走るプログラムをbinding.pryで止めてみると

ターミナルの.log

[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モデルにて、

task.rb
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)は、先程のものとは一致していません)。

ターミナルの.log
    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>

あるいは、もっと簡単なやり方があるのだろうか?

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0