Rails: Child record created before parent and throws RecordNotFound error

100 Views Asked by At

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?

2

There are 2 best solutions below

1
Alex On BEST ANSWER

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

0
smathy On

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).