ChatGPT解决这个技术问题 Extra ChatGPT

What is the easiest way to duplicate an activerecord record?

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)

g
gogaz

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...


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.
It looks like this functionality has been replaced with dup: gist.github.com/994614
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 on
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.
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}" }
P
Phillip Koebbe

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.


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 returned
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?
@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}))
@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?
old_task.attribtes assigns the ID field as well unfortunately. It's not working for me
V
Vaughn Draughon

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! :)


Great answer. Thanks for the detail!
Thanks it works!! But i have one question how do i add new entries with the cloning before saving the cloned object?
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.
B
Bryan Ash

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


@Thorin as per the accepted answer above, it looks like the correct method for Rails < 3.1 is .clone
L
Luís Ramalho

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

new_user = User.new(old_user.attributes.merge(:login => "newlogin"))

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?
with rails4, it does not create a unique id for the record
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.
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.
r
raidfive

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


Me too. I tried this gem and it worked first time, very easy to use.
F
Foram

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

new_user = old_user.dup

Z
Zoran Majstorovic

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


T
ThienSuBS

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     

e
esbanarango

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.


P
Paulo Fidalgo

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: 'clown@circus.example.com')>

cloned = UserCloner.call(user, email: 'fake@example.com')
cloned.persisted?
# => false

cloned.save!
cloned.login
# => nil
cloned.email
# => "fake@example.com"

# 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'}


S
Sachin Singh

Try rails's dup method:

new_record = old_record.dup.save