Rails: 在创建父记录之前创建子记录并引发RecordNotFound错误

huangapple go评论53阅读模式
英文:

Rails: Child record created before parent and throws RecordNotFound error

问题

我有一个奇怪的工作流程,首先创建一个子记录,然后我希望通过嵌套表单将其与父记录关联。这是一个使用 rspec 的示例:

这是模型:

class Parent < ApplicationRecord
  has_many :children, class_name: 'Child', dependent: :destroy

  accepts_nested_attributes_for :children, allow_destroy: true, reject_if: :all_blank
end

class Child < ApplicationRecord
  belongs_to :parent, foreign_key: 'child_id', optional: true
end

我的控制器是标准的:

def create
  @parent = Parent.new(parent_params)

  respond_to do |format|
    if @parent.save
      format.html { redirect_to @parent }
    else
      p @recipe.errors.full_messages
    end
  end
end

def parent_params
  params.require(:parent).permit(
    :name,
    children_attributes: [
      :id, :_destroy
    ]
  )
end

由于子记录已经存在,我只是尝试将其与父记录关联起来,而不是创建一个全新的记录。这是我的测试,但它失败并显示错误:

child = Child.create

expect {
  post "/parent", params: {
    parent: {
      name: "test",
      children_attributes: [{
        id: child.id
      }]
    }
  }.to change(Parent.all, :size).by(+1)
  .and change(Child.all, :size).by(0)
end

我收到的错误是:

ActiveRecord::RecordNotFound:
  Couldn't find Child with ID=1 for Parent with ID=

显然,这是因为 ChildParent 之前存在,但我仍然觉得应该有可能使其工作,使现有的子记录与保存的父记录关联起来。

英文:

I have a strange workflow whereby a child record is created first and then I want it associated with the parent through a nested form. Here is an example using rspec:

Here are the models:

class Parent < ApplicationRecord
  has_many :children, class_name: 'Child', dependent: :destroy

  accepts_nested_attributes_for :children, allow_destroy: true, reject_if: :all_blank
end

class Child < ApplicationRecord
  belongs_to :parent, foreign_key: 'child_id', optional: true
end

My controller is standard:

def create
  @prent = Parent.new(parent_params)

  respond_to do |format|
    if @parent.save
      format.html { redirect_to @parent }
    else
      p @recipe.errors.full_messages
    end
  end
end

def parent_params
  params.require(:parent).permit(
    :name,
    children_attributes: [
      :id, :_destroy
    ]
  )
end

Since the child already exists, I am only trying to associate it with the parent, not create an entirely new record. Here is my test that fails with an error:

child = Child.create

expect {
  post "/parent", params: {
	parent: {
	  name: "test",
	  children_attributes: [{
		id: child.id
	  }]
	}
  }.to change(Parent.all, :size).by(+1)
  .and change(Child.all, :size).by(0)
end

And the error I'm receiving is

 ActiveRecord::RecordNotFound:
   Couldn't find Child with ID=1 for Parent with ID=

Clearly it's because Child exists before Parent but I still feel this should be possible. Is there a way to get this to work such that the existing child is associated with the parent when the parent is saved?

答案1

得分: 3

If you only need to associate existing records when creating another, use child_ids= method.

> collection_singular_ids=ids
> 使用 ids 替换集合中由主键标识的对象。该方法加载模型并调用 collection=

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many

# db/migrate/20230601025957_create_parents.rb
class CreateParents < ActiveRecord::Migration[7.0]
  def change
    create_table :parents
    create_table :children do |t|
      t.references :parent
    end
  end
end
# app/models/parent.rb
class Parent < ApplicationRecord
  has_many :children
end

# app/models/child.rb
class Child < ApplicationRecord
  belongs_to :parent, optional: true
end
# spec/requests/parent_spec.rb
require "rails_helper"
RSpec.describe "Parents", type: :request do
  it "POST /parents" do
    child = Child.create!
    expect do
      # 不要忘记允许 `child_ids: []`
      post "/parents", params: {parent: {child_ids: [child.id]}}
    end.to(
      change(Parent, :count).by(1).and(
        change(Child, :count).by(0)
      )
    )
  end
end
# 1 个示例,0 个失败

在这里,children 不是一个连接表,但如果你设置了 through 关联,*_ids 方法也可以正常工作。类似于这样:https://stackoverflow.com/a/75972917/207090

英文:

If you only need to associate existing records when creating another, use child_ids= method.

> collection_singular_ids=ids
> Replace the collection with the objects identified by the primary keys in ids. This method loads the models and calls collection=.

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many

# db/migrate/20230601025957_create_parents.rb
class CreateParents < ActiveRecord::Migration[7.0]
  def change
    create_table :parents
    create_table :children do |t|
      t.references :parent
    end
  end
end
# app/models/parent.rb
class Parent < ApplicationRecord
  has_many :children
end

# app/models/child.rb
class Child < ApplicationRecord
  belongs_to :parent, optional: true
end
# spec/requests/parent_spec.rb
require "rails_helper"
RSpec.describe "Parents", type: :request do
  it "POST /parents" do
    child = Child.create!
    expect do
      # don't forget to permit `child_ids: []`
      post "/parents", params: {parent: {child_ids: [child.id]}}
    end.to(
      change(Parent, :count).by(1).and(
        change(Child, :count).by(0)
      )
    )
  end
end
# 1 example, 0 failures

children is not a join table here, but if you have through associations set up, *_ids method will also just work. Something like this: https://stackoverflow.com/a/75972917/207090

答案2

得分: 0

accepts_nested_attributes_for 不具备您所需的功能,您只能创建新的子项或更新已关联子项的属性,无法添加现有子项。

如果存在 id 但尚未成为 parent.children 的一部分,您可以重写 children_attributes= 方法,以提供您所寻求的功能,然后将其传递给 super 以进行正常行为。

英文:

accepts_nested_attributes_for does not have the feature you're after, you can only create new children, or update the attributes of already associated children, you can't add existing children.

You could override the children_attributes= method so that it provided the functionality you were looking for if an id existed but was not already part of the parent.children (and then pass off to super for the normal behavior).

huangapple
  • 本文由 发表于 2023年6月1日 04:36:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76377097.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定