Quelle est la façon la plus simple de dupliquer un enregistrement activerecord?

412

Je veux faire une copie d'un enregistrement activerecord, en changeant un seul champ dans le processus (en plus de l' id ). Quelle est la manière la plus simple d'y parvenir?

Je me rends compte que je pourrais créer un nouvel enregistrement, puis itérer sur chacun des champs en copiant les données champ par champ - mais je me suis dit qu'il devait y avoir un moyen plus simple de le faire ...

tel que:

 @newrecord=Record.copy(:id)  *perhaps?*
Brent
la source

Réponses:

622

Pour obtenir une copie, utilisez la méthode clone (ou dup for rails 3.1+):

# rails < 3.1
new_record = old_record.clone

#rails >= 3.1
new_record = old_record.dup

Ensuite, vous pouvez modifier les champs que vous souhaitez.

ActiveRecord remplace le clone Object # intégré pour vous donner un nouvel enregistrement (non enregistré dans la base de données) avec un ID non attribué.
Notez qu'il ne copie pas les associations, vous devrez donc le faire manuellement si vous en avez besoin.

Le clone de Rails 3.1 est une copie superficielle, utilisez plutôt dup ...

Michael Sepcot
la source
6
Est-ce que cela fonctionne toujours dans Rails 3.1.0.beta? Quand je le fais q = p.clone, puis p == qje truereviens. En revanche, si j'utilise q = p.dup, je falsereviens en les comparant.
Autumnsault
1
Les documents Rails 3.1 sur clone disent que cela fonctionne toujours, mais j'utilise Rails 3.1.0.rc4 et même la new?méthode ne fonctionne pas.
Turadg
12
Il semble que cette fonctionnalité ait été remplacée par dup: gist.github.com/994614
skattyadz
74
Certainement NE PAS utiliser de clone. Comme d'autres affiches l'ont mentionné, la méthode clone délègue désormais l'utilisation de Kernel # clone qui copiera l'identifiant. Utilisez ActiveRecord :: Base # dup à partir de maintenant
bradgonesurfing
5
Je dois dire que ce fut une vraie douleur. Un simple changement comme celui-ci aux fonctionnalités prévues pourrait paralyser certaines fonctionnalités importantes si vous n'aviez pas une bonne couverture des spécifications.
Matt Smith
74

Selon vos besoins et votre style de programmation, vous pouvez également utiliser une combinaison de la nouvelle méthode de la classe et de la fusion. Faute d'un meilleur simple exemple , supposons qu'une tâche soit planifiée pour une certaine date et que vous souhaitiez la dupliquer à une autre date. Les attributs réels de la tâche ne sont pas importants, donc:

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

créera une nouvelle tâche avec :id => nil, :scheduled_on => some_new_dateet tous les autres attributs de la même chose que la tâche d' origine. En utilisant Task.new, vous devrez appeler explicitement save, donc si vous voulez qu'il soit enregistré automatiquement, changez Task.new en Task.create.

Paix.

Phillip Koebbe
la source
5
Je ne sais pas vraiment à quel point cette idée est bonne b / c vous êtes WARNING: Can't mass-assign protected attributes: id, due_date, created_at, updated_atretourné
bcackerman
Lorsque je fais cela, j'obtiens une erreur d'attribut inconnue avec une colonne en raison d'une colonne qui est là en raison d'une relation has_many. Y a-t-il un moyen de contourner cela?
Ruben Martinez Jr.
2
@RubenMartineJr. Je sais que c'est un ancien message, mais oui, vous pouvez contourner ce problème en utilisant '.except' sur le hachage des attributs: new_task = Task.new (old_task.attributes.except (: attribute_you_dont_want,: another_aydw) .merge ({: schedule_on => some_new_date}))
Ninigi
@PhillipKoebbe merci - mais que faire si je veux que l'identifiant ne soit pas nul? Je veux que les rails attribuent automatiquement un nouvel identifiant lorsque je crée le doublon - est-ce possible?
BKSpurgeon
1
old_task.attribtes affecte également le champ ID malheureusement. Ça ne marche pas pour moi
BKSpurgeon
32

Vous aimerez peut-être aussi la gemme Amoeba pour ActiveRecord 3.2.

Dans votre cas, vous souhaiterez probablement utiliser le nullify, regexouprefix des options disponibles dans le DSL de configuration.

Il prend en charge la duplication récursive facile et automatique has_one, has_manyet les has_and_belongs_to_manyassociations, le prétraitement sur le terrain et une configuration DSL hautement flexible et puissante qui peut être appliquée à la fois au modèle et à la volée.

assurez-vous de consulter la documentation d'Amoeba mais l'utilisation est assez facile ...

juste

gem install amoeba

ou ajouter

gem 'amoeba'

à votre Gemfile

puis ajoutez le bloc amibe à votre modèle et exécutez la dupméthode comme d'habitude

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

Vous pouvez également contrôler les champs qui seront copiés de nombreuses manières, mais par exemple, si vous vouliez empêcher la duplication des commentaires mais que vous vouliez conserver les mêmes balises, vous pourriez faire quelque chose comme ceci:

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

  amoeba do
    exclude_field :comments
  end
end

Vous pouvez également prétraiter les champs pour aider à indiquer l'unicité des préfixes et suffixes ainsi que des regex. En outre, il existe également de nombreuses options pour que vous puissiez écrire dans le style le plus lisible à votre intention:

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

La copie récursive des associations est facile, activez simplement l'amibe sur les modèles enfants

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

La configuration DSL a encore plus d'options, alors assurez-vous de consulter la documentation.

Prendre plaisir! :)

Vaughn Draughon
la source
Très bonne réponse. Merci pour le détail!
Derek Prior
Merci ça marche !! Mais j'ai une question: comment ajouter de nouvelles entrées avec le clonage avant d'enregistrer l'objet cloné?
Mohd Anas
1
Juste une solution ici. La bonne méthode n'est .amoeba_duppas seulement .dup. J'essayais d'exécuter ce code, mais cela ne fonctionnait pas ici.
Victor
31

Utilisez ActiveRecord :: Base # dup si vous ne voulez pas copier l'identifiant

bradgonesurfing
la source
1
@Thorin selon la réponse acceptée ci-dessus, il semble que la méthode correcte pour Rails <3.1 soit.clone
Dan Weaver
24

Je copie généralement les attributs, en changeant tout ce que j'ai besoin de changer:

new_user = User.new(old_user.attributes.merge(:login => "newlogin"))
François Beausoleil
la source
Lorsque je fais cela, j'obtiens une unknown attributeerreur avec une colonne à cause d'une colonne qui est là en raison d'une relation has_many. Y a-t-il un moyen de contourner cela?
Ruben Martinez Jr.
avec rails4, il ne crée pas un identifiant unique pour le record
Ben
4
Pour créer un nouvel enregistrement avec avec Rails 4, faites User.create(old_user.attributes.merge({ login: "newlogin", id: nil })). Cela permettra d'économiser un nouvel utilisateur avec l'ID unique correct.
RajeshM
Rails a Hash # except et Hash # slice , ce qui rend potentiellement la méthode suggérée la plus puissante et la moins sujette aux erreurs. Pas besoin d'ajouter des bibliothèques supplémentaires, faciles à étendre.
kucaahbe
10

Si vous avez besoin d'une copie complète avec des associations, je recommande la gemme deep_cloneable .

raidfive
la source
Moi aussi. J'ai essayé ce bijou et cela a fonctionné la première fois, très facile à utiliser.
Rob
4

Dans Rails 5, vous pouvez simplement créer un objet en double ou enregistrer comme ceci.

new_user = old_user.dup
Foram Thakral
la source
2

Le moyen le plus simple est:

#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

Ou

# if your rails < 3.1
 o = Model.find(id)
 (1..109).each do |item|
   new_record = o.clone
   new_record.save
 end     
ThienSuBS
la source
2

Voici un exemple de #dupméthode ActiveRecord prioritaire pour personnaliser la duplication d'instance et inclure également la duplication de relation:

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

Remarque: cette méthode ne nécessite aucune gemme externe mais elle nécessite une version plus récente d'ActiveRecord avec la #dupméthode implémentée

Zoran Majstorovic
la source
0

Vous pouvez également vérifier la gemme actes_as_inheritable .

"Agit en tant qu'héritable est une gemme Ruby spécifiquement écrite pour les modèles Rails / ActiveRecord. Elle est destinée à être utilisée avec l'association auto-référentielle , ou avec un modèle dont un parent partage les attributs héritables. Cela vous permettra d'hériter de tout attribut ou relation du modèle parent. "

En ajoutant acts_as_inheritableà vos modèles, vous aurez accès à ces méthodes:

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

J'espère que cela peut vous aider.

esbanarango
la source
0

Puisqu'il pourrait y avoir plus de logique, lors de la duplication d'un modèle, je suggère de créer une nouvelle classe, où vous manipulez toute la logique nécessaire. Pour faciliter cela, il existe un joyau qui peut vous aider: clowne

Selon leurs exemples de documentation, pour un modèle utilisateur:

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

Vous créez votre classe de cloneur:

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

puis l'utiliser:

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

Exemple copié du projet, mais il donnera une vision claire de ce que vous pouvez réaliser.

Pour un enregistrement simple et rapide, je choisirais:

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

Paulo Fidalgo
la source