ruby-on-railsrspecnested-forms

Rails: Child record created before parent and throws RecordNotFound error


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?


Solution

  • 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