inuinu blog(開発用)

BOT @wagagun の開発ノウハウや、IT向け?の雑記ブログです。

【Ruby on Rails7】cocoonを導入(その4:変更時に子が二重登録されてしまう場合の解決方法)

この記事では、Ruby on Rails7のgemパッケージcocoonについて、導入・カスタマイズ行います。
今回は変更時に、子のレコードがダブってしまう現象についての解決方法を考えてきます。

前回の続き。

inuinu-tech.hatenablog.jp

今回は、導入した際に引っかかった障害と、その解決方法です。

変更時の二重登録について

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件でも更新できてしまう現象を解決します。

inuinu-tech.hatenablog.jp