🧬
cloning objects in Rails
on programminglast 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.