Tintin81
Tintin81

Reputation: 10207

How to duplicate a record and its associated records in Ruby on Rails?

In my Rails app I have invoices and their associated items.

In my InvoicesController I added this method:

def duplicate
  invoice = Invoice.find(params[:id])
  @invoice = invoice.dup
  invoice.items.each do |item|
    @invoice.items << item
  end      
  render :new    
end

def create
  @invoice = current_user.invoices.build(params[:invoice])    
  if @invoice.save
    flash[:success] = "Invoice created."
    redirect_to edit_invoice_path(@invoice)
  else
    render :new
  end
end

Clicking the link instantiates a form with the correct invoice and items data.

However, when trying to Create the record I get an error:

Couldn't find Item with ID=4 for Invoice with ID=

Can anybody tell me what I am missing here or if there's a smarter way to duplicate a record including its associated records?

Thanks for any help.

Upvotes: 4

Views: 1003

Answers (2)

Jesse Wolgamott
Jesse Wolgamott

Reputation: 40277

Here's cose that will duplicate the main object, and each of the items underneath it. The new object will not be saved, nor will the items (yet).

  def duplicate
    dup.tap do |new_invoice|
      items.each do |item|
        new_invoice.items.push item.dup
      end
    end
  end

And a quick test to prove the things:

require 'test_helper'

class InvoiceTest < ActiveSupport::TestCase

  def original_invoice
    Invoice.create(number: 5).tap do |invoice|
      invoice.items.create(name: "a", price: "5")
      invoice.items.create(name: "b", price: "50")
    end
  end
  test "duplication" do
    new_invoice = original_invoice.duplicate
    new_invoice.save
    assert_equal 2, new_invoice.items.count
  end
end

Upvotes: 3

James Brewer
James Brewer

Reputation: 1755

Let's deconstruct this a little bit, starting with your error message.

Couldn't find Item with ID=4 for Invoice with ID=

Now, at first thought, one might be tempted to consider that there is no Item with ID 4. It's a good sanity check to make sure that simple things like this aren't the issue. In this case, we have the appropriate item, so we can move on.

What struck me as odd at first was the lack of a number following ID=. It turns out that this hints as to what the problem is.

Let's have a look at some console output. I will be using Cat objects, simply because they are awesome.

The first thing we want to do is to get a Cat:

Cat.first
=> Cat Load (0.2ms)  SELECT "cats".* FROM "cats" LIMIT 1
=> #<Cat id: 2, age: 6, birthdate: "2013-06-08", color: "brown", name: "Aaron", gender: "m", created_at: "2013-06-08 21:44:22", updated_at: "2013-06-08 21:44:22", user_id: 1> 

After we have the cat, let's duplicate it.

Cat.first.dup
Cat Load (0.3ms)  SELECT "cats".* FROM "cats" LIMIT 1
=> #<Cat age: 6, birthdate: "2013-06-08", color: "brown", name: "Aaron", gender: "m", created_at: nil, updated_at: nil, user_id: 1> 

What do we notice about the duplicated cat? Well, for starters, both created_at and updated_at are nil. This is usually a sign that the object hasn't be saved to the database. If we go to look for the Cat's ID, we notice that there isn't even a column for that!

Let's try saving the new object.

Cat.first.dup.save
Cat Load (0.3ms)  SELECT "cats".* FROM "cats" ORDER BY "cats"."id" DESC LIMIT 1
(0.1ms)  begin transaction
SQL (0.7ms)  INSERT INTO "cats" ("age", "birthdate", "color", "created_at", "gender", "name", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?, ?, ?, ?)  [["age", 6], ["birthdate", Sat, 08 Jun 2013], ["color", "brown"], ["created_at", Sun, 09 Jun 2013 18:10:47 UTC +00:00], ["gender", "m"], ["name", "Aaron"], ["updated_at", Sun, 09 Jun 2013 18:10:47 UTC +00:00], ["user_id", 1]]
(0.7ms)  commit transaction
=> true 

Now, if we call Cat.last, it will return this object.

Cat.last
Cat Load (0.3ms)  SELECT "cats".* FROM "cats" ORDER BY "cats"."id" DESC LIMIT 1
=> #<Cat id: 11, age: 6, birthdate: "2013-06-08", color: "brown", name: "Aaron", gender: "m", created_at: "2013-06-09 18:10:47", updated_at: "2013-06-09 18:10:47", user_id: 1> 

Your problem is that, while you are duplicating the Invoice, you aren't saving it to the database. Couldn't find Item with ID=4 for Invoice with ID= confirms this as it tells us the duplicated Invoice has no ID, which would only be the case if it had not been saved.

Cheers!

Upvotes: 2

Related Questions