« Back

🧬

cloning objects in Rails

on programming
last updated 4/26/21

I messed this up recently, so here we are.

Stack Overflow posts on the topic are littered with gems that haven't been maintained for years. Below is my five-minute summary in case a simplistic overview can help anybody.


Copying over attributes

Ruby's native #dup and #clone methods are perfect for making shallow copies of objects.

#clone copies over all attributes, while #dup doesn't assign an id or timestamps. Because the id (or other primary key) isn't assigned to a duped object, it will be treated as a new record.

The only type of association these methods properly copy is a belongs_to relationship, where the object has a foreign key for an associated object. This is a simple example in the Rails docs.

Even #deep_dup, deceptively named, literally just calls #dup on objects. You can see here.

There's way more nuance to this but the gist is that a different approach is required to clone objects with any other types of associations.


Copying over attributes and associations

Here's one way to make a deep copy of an object.

Look at the object's associations and pick out all which are not belongs_to relationships.

Examine those associated objects to see whether their associations need to be cloned as well.

On the model for the object you want to clone, write an instance method or methods to manually copy over its attributes and associations.


# app/models/user.rb
# copying attributes
def deep_copy_attributes
  attributes.except('id', 'created_at', 'updated_at')
end

# copying associations AND attributes
def deep_copy!
  clone = self.class.create! deep_copy_attributes

  clone.phone_number = self.phone_number.dup
  clone.address = self.address
  self.comments.each { |comment| clone.comments.build(comment.dup) }

  clone.save!
  clone
end
        

With the hypothetical example above, we assume phone numbers, addresses, and comments do not have complex nested associations.

We'll pretend a user has one phone number and a phone number must be unique to the user it belongs to. So, we'll use #dup to copy over all of its attributes except the id and timestamps.

We'll also pretend a user has one address but because multiple users can have identical addresses, it's fair game to assign clone.address to the original user's address, self.address.

Finally, we'll pretend a user has many comments and a comment must be unique to its user. We iterate over each of the original user's comments, dupe them, and associate them with the cloned user.

Upon saving the new, cloned user object to the database, its associated objects will be persisted to the database also if they haven't already.