What is the easiest way to duplicate an activerecord record?
Asked Answered
P

12

474

I want to make a copy of an ActiveRecord object, changing a single field in the process (in addition to the id). What is the simplest way to accomplish this?

I realize I could create a new record, and then iterate over each of the fields copying the data field-by-field - but I figured there must be an easier way to do this.

Perhaps something like this:

 new_record = Record.copy(:id)
Plerre answered 12/9, 2008 at 21:48 Comment(0)
S
708

To get a copy, use the dup (or clone for < rails 3.1+) method:

#rails >= 3.1
new_record = old_record.dup

# rails < 3.1
new_record = old_record.clone

Then you can change whichever fields you want.

ActiveRecord overrides the built-in Object#clone to give you a new (not saved to the DB) record with an unassigned ID.
Note that it does not copy associations, so you'll have to do this manually if you need to.

Rails 3.1 clone is a shallow copy, use dup instead...

Shastashastra answered 12/9, 2008 at 21:56 Comment(10)
Does this still work in Rails 3.1.0.beta? When I do q = p.clone, and then p == q, I get true back. On the other hand, if I use q = p.dup, I get false back when comparing them.Waterfall
The Rails 3.1 docs on clone say it still works, but I'm using Rails 3.1.0.rc4 and even the new? method isn't working.Ousel
When I try to clone and then save in Rails 3.1 RC4, I get a Mysql2::Error: Duplicate entry error on the primary key.Titanism
It looks like this functionality has been replaced with dup: gist.github.com/994614Titanism
Definitely DO NOT use clone. As other posters have mentioned the clone method now delegates to using Kernel#clone which will copy the id. Use ActiveRecord::Base#dup from now onHannelorehanner
Does dup do the same pre-rails 3.1? Or is its functionality different?Abase
I have to say, this was a real pain. A simple change like this to intended functionality could cripple some important features if you didn't have good spec coverage.Rouse
An addition for dup or clone if you want to change specific attributes is to use tap e.g. clone = record.dup.tap { |new_clone| new_clone.name = "dup_#{new_clone.name}" }Iseult
My test show that myrecord.clone gives me an exact copy of the record including its id. so myrecord.save I'm back where i started from. myrecord.dup gives me and exact copy but the ID is nil. Now myrecord.save means I have an additional copy. It copied all the attributes including my binary data. So use Luis's answerBerner
After that you should new_record.save! to persist the object.Rusticate
D
79

Depending on your needs and programming style, you can also use a combination of the new method of the class and merge. For lack of a better simple example, suppose you have a task scheduled for a certain date and you want to duplicate it to another date. The actual attributes of the task aren't important, so:

old_task = Task.find(task_id)
new_task = Task.new(old_task.attributes.merge({:scheduled_on => some_new_date}))

will create a new task with :id => nil, :scheduled_on => some_new_date, and all other attributes the same as the original task. Using Task.new, you will have to explicitly call save, so if you want it saved automatically, change Task.new to Task.create.

Peace.

Duaneduarchy answered 15/9, 2008 at 13:48 Comment(5)
Not quite sure how good of idea this is b/c you get WARNING: Can't mass-assign protected attributes: id, due_date, created_at, updated_at returnedMillenarianism
When I do this, I get an unknown attribute error with one column because of a column that is there due to a has_many relationship. Is there any way around this?Brno
@RubenMartineJr. I know this is an old post, but yeah you can get around this by using '.except' on the attributes hash: new_task = Task.new(old_task.attributes.except(:attribute_you_dont_want, :another_aydw).merge({:scheduled_on => some_new_date}))Crowther
@PhillipKoebbe thank you - but what if I want the id to not be null? I want rails to automatically assign a new id when i create the duplicate - is this possible?Extraversion
old_task.attribtes assigns the ID field as well unfortunately. It's not working for meExtraversion
C
35

You may also like the Amoeba gem for ActiveRecord 3.2.

In your case, you probably want to make use of the nullify, regex or prefix options available in the configuration DSL.

It supports easy and automatic recursive duplication of has_one, has_many and has_and_belongs_to_many associations, field preprocessing and a highly flexible and powerful configuration DSL that can be applied both to the model and on the fly.

be sure to check out the Amoeba Documentation but usage is pretty easy...

just

gem install amoeba

or add

gem 'amoeba'

to your Gemfile

then add the amoeba block to your model and run the dup method as usual

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

class PostsController < ActionController
  def some_method
    my_post = Post.find(params[:id])
    new_post = my_post.dup
    new_post.save
  end
end

You can also control which fields get copied in numerous ways, but for example, if you wanted to prevent comments from being duplicated but you wanted to maintain the same tags, you could do something like this:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    exclude_field :comments
  end
end

You can also preprocess fields to help indicate uniqueness with both prefixes and suffixes as well as regexes. In addition, there are also numerous options so you can write in the most readable style for your purpose:

class Post < ActiveRecord::Base
  has_many :comments
  has_and_belongs_to_many :tags

  amoeba do
    include_field :tags
    prepend :title => "Copy of "
    append :contents => " (copied version)"
    regex :contents => {:replace => /dog/, :with => "cat"}
  end
end

Recursive copying of associations is easy, just enable amoeba on child models as well

class Post < ActiveRecord::Base
  has_many :comments

  amoeba do
    enable
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post
  has_many :ratings

  amoeba do
    enable
  end
end

class Rating < ActiveRecord::Base
  belongs_to :comment
end

The configuration DSL has yet more options, so be sure to check out the documentation.

Enjoy! :)

Corkscrew answered 28/2, 2012 at 16:12 Comment(2)
Thanks it works!! But i have one question how do i add new entries with the cloning before saving the cloned object?Raseda
Just a fix here. The correct method is .amoeba_dup, not just .dup. I was trying to execute this code, but it was not working on here.Disillusion
H
33

Use ActiveRecord::Base#dup if you don't want to copy the id

Hannelorehanner answered 5/8, 2011 at 13:59 Comment(1)
@Thorin as per the accepted answer above, it looks like the correct method for Rails < 3.1 is .cloneTectonic
C
27

I usually just copy the attributes, changing whatever I need changing:

new_user = User.new(old_user.attributes.merge(:login => "newlogin"))
Cynarra answered 15/9, 2008 at 13:36 Comment(4)
When I do this, I get an unknown attribute error with one column because of a column that is there due to a has_many relationship. Is there any way around this?Brno
with rails4, it does not create a unique id for the recordAberration
To create a new record with with Rails 4, do User.create(old_user.attributes.merge({ login: "newlogin", id: nil })). This will save a new user with the correct unique id.Sarinasarine
Rails has Hash#except and Hash#slice, potentially making suggested method most powerful and less error-prone. No need to add additional libs, easy to extend.Borreri
S
10

If you need a deep copy with associations, I recommend the deep_cloneable gem.

Spray answered 1/9, 2011 at 2:22 Comment(1)
Me too. I tried this gem and it worked first time, very easy to use.Neman
C
6

In Rails 5 you can simply create duplicate object or record like this.

new_user = old_user.dup
Cental answered 4/1, 2019 at 8:56 Comment(0)
M
4

Here is a sample of overriding ActiveRecord #dup method to customize instance duplication and include relation duplication as well:

class Offer < ApplicationRecord
  has_many :offer_items

  def dup
    super.tap do |new_offer|

      # change title of the new instance
      new_offer.title = "Copy of #{@offer.title}"

      # duplicate offer_items as well
      self.offer_items.each { |offer_item| new_offer.offer_items << offer_item.dup }
    end
  end
end

Note: this method doesn't require any external gem but it requires newer ActiveRecord version with #dup method implemented

Mclaughlin answered 20/8, 2018 at 7:4 Comment(0)
S
2

The easily way is:

#your rails >= 3.1 (i was done it with Rails 5.0.0.1)
  o = Model.find(id)
 # (Range).each do |item|
 (1..109).each do |item|
   new_record = o.dup
   new_record.save
 end

Or

# if your rails < 3.1
 o = Model.find(id)
 (1..109).each do |item|
   new_record = o.clone
   new_record.save
 end     
Superabundant answered 30/10, 2016 at 13:58 Comment(0)
L
0

You can also check the acts_as_inheritable gem.

"Acts As Inheritable is a Ruby Gem specifically written for Rails/ActiveRecord models. It is meant to be used with the Self-Referential Association, or with a model having a parent that share the inheritable attributes. This will let you inherit any attribute or relation from the parent model."

By adding acts_as_inheritable to your models you will have access to these methods:

inherit_attributes

class Person < ActiveRecord::Base

  acts_as_inheritable attributes: %w(favorite_color last_name soccer_team)

  # Associations
  belongs_to  :parent, class_name: 'Person'
  has_many    :children, class_name: 'Person', foreign_key: :parent_id
end

parent = Person.create(last_name: 'Arango', soccer_team: 'Verdolaga', favorite_color:'Green')

son = Person.create(parent: parent)
son.inherit_attributes
son.last_name # => Arango
son.soccer_team # => Verdolaga
son.favorite_color # => Green

inherit_relations

class Person < ActiveRecord::Base

  acts_as_inheritable associations: %w(pet)

  # Associations
  has_one     :pet
end

parent = Person.create(last_name: 'Arango')
parent_pet = Pet.create(person: parent, name: 'Mango', breed:'Golden Retriver')
parent_pet.inspect #=> #<Pet id: 1, person_id: 1, name: "Mango", breed: "Golden Retriver">

son = Person.create(parent: parent)
son.inherit_relations
son.pet.inspect # => #<Pet id: 2, person_id: 2, name: "Mango", breed: "Golden Retriver">

Hope this can help you.

Lonely answered 27/10, 2015 at 22:27 Comment(0)
H
0

Since there could be more logic, when duplicating a model, I would suggest to create a new class, where you handle all the needed logic. To ease that, there's a gem that can help: clowne

As per their documentation examples, for a User model:

class User < ActiveRecord::Base
  # create_table :users do |t|
  #  t.string :login
  #  t.string :email
  #  t.timestamps null: false
  # end

  has_one :profile
  has_many :posts
end

You create your cloner class:

class UserCloner < Clowne::Cloner
  adapter :active_record

  include_association :profile, clone_with: SpecialProfileCloner
  include_association :posts

  nullify :login

  # params here is an arbitrary Hash passed into cloner
  finalize do |_source, record, params|
    record.email = params[:email]
  end
end

class SpecialProfileCloner < Clowne::Cloner
  adapter :active_record

  nullify :name
end

and then use it:

user = User.last
#=> <#User(login: 'clown', email: '[email protected]')>

cloned = UserCloner.call(user, email: '[email protected]')
cloned.persisted?
# => false

cloned.save!
cloned.login
# => nil
cloned.email
# => "[email protected]"

# associations:
cloned.posts.count == user.posts.count
# => true
cloned.profile.name
# => nil

Example copied from the project, but it will give a clear vision of what you can achieve.

For a quick and simple record I would go with:

Model.new(Model.last.attributes.reject {|k,_v| k.to_s == 'id'}

Helen answered 16/3, 2018 at 14:19 Comment(0)
H
0

Try rails's dup method:

new_record = old_record.dup.save
Hanleigh answered 4/4, 2021 at 17:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.