前回の続き。
今回は、導入した際に引っかかった障害と、その解決方法です。
変更時の二重登録について
cocoonを導入し、変更すると子が削除されずに残る(二重登録)
とあるチュートリアルを見つつ、cocoonを導入し、モデル、ビュー、コントローラーの順に追記・変更し、実行したところ、追加・削除は上手く行ったのですが、変更すると、前に入力した子がそのまま残ってしまいました。
親子関係の子の更新について(一般論)
親子を一括入力する場合の、更新ロジックについて、一般的には、
- 追加
- 親子ともにinsert
- 変更
- 親はupdate
- 子は親に関わるレコードを全てdeleteした後、insert
- 削除
- 親子ともにdelete
ですが、なぜか変更のdeleteが上手く行かないようです。
検索してもヒットしない現象なので、私の初歩的なミスなのかもしれません。
updateには何も記載がない…
app/controllers/projects_controller.rb
を見たところ…
●app/controllers/projects_controller.rb
# PATCH/PUT /projects/1 or /projects/1.json def update respond_to do |format| if @project.update(project_params) format.html { redirect_to project_url(@project), notice: "Project was successfully updated." } format.json { render :show, status: :ok, location: @project } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @project.errors, status: :unprocessable_entity } end end end
updateに限らずcreateもdestroyも特に子に対する更新処理は定義されてないのですよね。
Railsは一般的な更新処理を自動でやってくれると思っているので、なんかちょっと変ですね…
正しい解決方法
もう一度コントローラーを見てみる
もう一度、app/controllers/projects_controller.rb
を見てみます。
●app/controllers/projects_controller.rb
private # Use callbacks to share common setup or constraints between actions. def set_project @project = Project.find(params[:id]) end # Only allow a list of trusted parameters through. def project_params params.require(:project).permit(:name, :description, tasks_attributes: [:description, :done, :_destroy]) end
もしかして、子の:id
って必要?
●app/controllers/projects_controller.rb
private # Use callbacks to share common setup or constraints between actions. def set_project @project = Project.find(params[:id]) end # Only allow a list of trusted parameters through. def project_params params.require(:project).permit(:name, :description, tasks_attributes: [:id, :description, :done, :_destroy]) end
子の方に:id
を追記して、変更したところ、二重登録がなくなりました。
めでたしめでたし。
参考:間違った解決方法
参考までに、試行錯誤した段階でのソースも載せておきます。
間違った解決方法(その1)
●app/controllers/projects_controller.rb
# PATCH/PUT /projects/1 or /projects/1.json def update @tasks = Task.where(project_id: params[:id]) @tasks.each do |task| task.delete end #@tasks.destory_all #if @tasks.size == nil respond_to do |format| if @project.update(project_params) format.html { redirect_to project_url(@project), notice: "Project was successfully updated." } format.json { render :show, status: :ok, location: @project } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @project.errors, status: :unprocessable_entity } end end end
とりあえず、応急処置的に、コントローラーに削除処理を組み込みましたが、この手法ではバリデーションエラーが発生した時点で、子が削除されてしまうので、(エラーを直し更新完了せずに)処理を中断し他の画面に移った場合に、原状回復できないという問題が発生します。
間違った解決方法(その2)
なので、上記のロジックは除去し、モデル側(app/models/project.rb
)に以下のコードを追加しました。
●app/controllers/projects_controller.rb
before_validation :delete_tasks (中略) def delete_tasks # こちらは子供のレコードを一度削除する必要がある @tasks = task.where(project_id: self.id) @tasks.each do |task| task.delete end end
この手法ですと、バリデーションエラーの場合に自動回復(rollback)しますので、子が消えてしまった…ということはなくなります。
とはいえ、「正しい解決方法」を実践すれば、記述する必要は全くありません。
あくまでも原因不明で困った困った…という場合の、代替案として頭の隅においてくださいね。
次回
次回は、子が0件でも更新できてしまう現象を解決します。