Duplicar la instancia del modelo Django y todas las claves externas apuntando a ella

Quiero crear un método en un modelo de Django, model.duplicate() , que duplica la instancia del modelo, incluidas todas las claves externas que lo señalan. Sé que puedes hacer esto:

 def duplicate(self): self.pk = None self.save() 

… pero de esta manera todos los modelos relacionados apuntan a la instancia anterior.

No puedo simplemente guardar una referencia al objeto original porque lo que apunta a los cambios durante la ejecución del método:

 def duplicate(self): original = self self.pk = None self.save() assert original is not self # fails 

Podría intentar guardar una referencia al objeto relacionado:

 def duplicate(self): original_fkeys = self.fkeys.all() self.pk = None self.save() self.fkeys.add(*original_fkeys) 

… pero esto los transfiere del registro original al nuevo. Los necesito copiados y señaló el nuevo disco.

Varias respuestas en otros lugares (y aquí antes de actualizar la pregunta) han sugerido el uso de la copy de Python, que sospecho que funciona con claves externas en este modelo , pero no con claves externas en otro modelo que lo apunta.

 def duplicate(self): new_model = copy.deepcopy(self) new_model.pk = None new_model.save() 

Si haces esto, new_model.fkeys.all() (para seguir mi esquema de nombres hasta ahora) estará vacío.

Puedes crear una nueva instancia y guardarla como esta

 def duplicate(self): kwargs = {} for field in self._meta.fields: kwargs[field.name] = getattr(self, field.name) # or self.__dict__[field.name] kwargs.pop('id') new_instance = self.__class__(**kwargs) new_instance.save() # now you have id for the new instance so you can # create related models in similar fashion fkeys_qs = self.fkeys.all() new_fkeys = [] for fkey in fkey_qs: fkey_kwargs = {} for field in fkey._meta.fields: fkey_kwargs[field.name] = getattr(fkey, field.name) fkey_kwargs.pop('id') fkey_kwargs['foreign_key_field'] = new_instance.id new_fkeys.append(fkey_qs.model(**fkey_kwargs)) fkeys_qs.model.objects.bulk_create(new_fkeys) return new_instance 

No estoy seguro de cómo se comportará con los campos ManyToMany. Pero para campos simples funciona. Y siempre puede abrir los campos que no le interesan para su nueva instancia.

Los bits en los que estoy iterando sobre _meta.fields pueden hacerse con copia, pero lo importante es usar la nueva id para el campo foreign_key_field .

Estoy seguro de que es posible detectar de manera programática qué campos son claves externas para el self.__class__ ( foreign_key_field ) pero como puedes tener más de ellas, es mejor nombrar uno (o más) explícitamente.

Aunque acepté la respuesta del otro póster (ya que me ayudó a llegar aquí), quería publicar la solución que terminé en caso de que ayude a alguien más en el mismo lugar.

 def duplicate(self): """ Duplicate a model instance, making copies of all foreign keys pointing to it. This is an in-place method in the sense that the record the instance is pointing to will change once the method has run. The old record is still accessible but must be retrieved again from the database. """ # I had a known set of related objects I wanted to carry over, so I # listed them explicitly rather than looping over obj._meta.fields fks_to_copy = list(self.fkeys_a.all()) + list(self.fkeys_b.all()) # Now we can make the new record self.pk = None # Make any changes you like to the new instance here, then self.save() foreign_keys = {} for fk in fks_to_copy: fk.pk = None # Likewise make any changes to the related model here # However, we avoid calling fk.save() here to prevent # hitting the database once per iteration of this loop try: # Use fk.__class__ here to avoid hard-coding the class name foreign_keys[fk.__class__].append(fk) except KeyError: foreign_keys[fk.__class__] = [fk] # Now we can issue just two calls to bulk_create, # one for fkeys_a and one for fkeys_b for cls, list_of_fks in foreign_keys.items(): cls.objects.bulk_create(list_of_fks) 

Lo que parece cuando lo usas:

 In [6]: model.id Out[6]: 4443 In [7]: model.duplicate() In [8]: model.id Out[8]: 17982 In [9]: old_model = Model.objects.get(id=4443) In [10]: old_model.fkeys_a.count() Out[10]: 2 In [11]: old_model.fkeys_b.count() Out[11]: 1 In [12]: model.fkeys_a.count() Out[12]: 2 In [13]: model.fkeys_b.count() Out[13]: 1 

Se cambiaron los nombres de los modelos y modelos relacionados para proteger a los inocentes.

Probé las otras respuestas en Django 2.1 / Python 3.6 y no parecían copiar objetos relacionados uno a muchos y muchos a muchos ( self._meta.fields no incluye campos relacionados uno a muchos, pero self._meta.get_fields() hace). Además, las otras respuestas requerían un conocimiento previo del nombre del campo relacionado o el conocimiento de qué claves foráneas copiar.

Escribí una manera de hacer esto de una manera más genérica, manejando campos relacionados de uno a muchos y de muchos a muchos. Se incluyen comentarios y sugerencias de bienvenida:

 def duplicate_object(self): """ Duplicate a model instance, making copies of all foreign keys pointing to it. There are 3 steps that need to occur in order: 1. Enumerate the related child objects and m2m relations, saving in lists/dicts 2. Copy the parent object per django docs (doesn't copy relations) 3a. Copy the child objects, relating to the copied parent object 3b. Re-create the m2m relations on the copied parent object """ related_objects_to_copy = [] relations_to_set = {} # Iterate through all the fields in the parent object looking for related fields for field in self._meta.get_fields(): if field.one_to_many: # One to many fields are backward relationships where many child objects are related to the # parent (ie SelectedPhrases). Enumerate them and save a list so we can copy them after # duplicating our parent object. print(f'Found a one-to-many field: {field.name}') # 'field' is a ManyToOneRel which is not iterable, we need to get the object attribute itself related_object_manager = getattr(self, field.name) related_objects = list(related_object_manager.all()) if related_objects: print(f' - {len(related_objects)} related objects to copy') related_objects_to_copy += related_objects elif field.many_to_one: # In testing so far, these relationships are preserved when the parent object is copied, # so they don't need to be copied separately. print(f'Found a many-to-one field: {field.name}') elif field.many_to_many: # Many to many fields are relationships where many parent objects can be related to many # child objects. Because of this the child objects don't need to be copied when we copy # the parent, we just need to re-create the relationship to them on the copied parent. print(f'Found a many-to-many field: {field.name}') related_object_manager = getattr(self, field.name) relations = list(related_object_manager.all()) if relations: print(f' - {len(relations)} relations to set') relations_to_set[field.name] = relations # Duplicate the parent object self.pk = None self.save() print(f'Copied parent object ({str(self)})') # Copy the one-to-many child objects and relate them to the copied parent for related_object in related_objects_to_copy: # Iterate through the fields in the related object to find the one that relates to the # parent model (I feel like there might be an easier way to get at this). for related_object_field in related_object._meta.fields: if related_object_field.related_model == self.__class__: # If the related_model on this field matches the parent object's class, perform the # copy of the child object and set this field to the parent object, creating the # new child -> parent relationship. related_object.pk = None setattr(related_object, related_object_field.name, self) related_object.save() text = str(related_object) text = (text[:40] + '..') if len(text) > 40 else text print(f'|- Copied child object ({text})') # Set the many-to-many relations on the copied parent for field_name, relations in relations_to_set.items(): # Get the field by name and set the relations, creating the new relationships field = getattr(self, field_name) field.set(relations) text_relations = [] for relation in relations: text_relations.append(str(relation)) print(f'|- Set {len(relations)} many-to-many relations on {field_name} {text_relations}') return self