重复的Django模型实例和指向它的所有外键

时间:2015-08-26 19:19:43

标签: python django python-2.7

我想在Django模型上创建一个方法,称之为model.duplicate(),它复制模型实例,包括指向它的所有外键。我知道你可以这样做:

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

...但是这样所有相关模型仍然指向旧实例。

我不能简单地保存对原始对象的引用,因为self指向执行方法期间的更改:

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

我可以尝试保存对相关对象的引用:

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

...但是这会将它们从原始记录转移到新记录。我需要将它们复制并指向新记录。

其他地方的几个答案(在此之前我更新了问题)建议使用Python的copy,我怀疑它适用于此模型上的外键 ,而不是其他模型上的外键指着它。

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

如果你这样做new_model.fkeys.all()(到目前为止遵循我的命名方案)将是空的。

4 个答案:

答案 0 :(得分:3)

您可以创建新实例并像这样保存

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

我不确定它对ManyToMany字段的行为如何。但是对于简单的字段,它可以工作。您可以随时弹出您对新实例不感兴趣的字段。

我在 _meta.fields 上迭代的位可以通过复制来完成,但重要的是对id使用新的foreign_key_field

我确信它可以通过编程方式检测哪些字段是self.__class__foreign_key_field)的外键,但由于您可以拥有更多字段,因此可以使用它们。\ n \ n最好明确地命名一个(或更多)。

答案 1 :(得分:1)

虽然我接受了另一张海报的回答(因为它帮助我到了这里),我想发布我最终得到的解决方案,以防其他人卡在同一个地方。

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)

使用它时的样子:

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

更改了模型和related_model名称以保护无辜者。

答案 2 :(得分:1)

我尝试了Django 2.1 / Python 3.6中的其他答案,但它们似乎没有复制一对多和多对多相关对象(self._meta.fields不包括一对多相关字段,但self._meta.get_fields()确实如此)。另外,其他答案也需要先了解相关字段名称或要复制哪些外键。

我写了一种更通用的方法来处理一对多和多对多相关字段。包括评论,欢迎提出建议:

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 (i.e. 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

答案 3 :(得分:0)

这是一个思路简单的解决方案。这不依赖于任何未公开的Django API。它假定您要复制单个父记录及其子记录,孙记录等。您传入实际上应该复制的类的白名单,形式为list,指向每个父对象上指向其子对象的一对多关系的名称。此代码假定,根据上述白名单,整个树都是独立的,无需担心外部引用。

关于此代码的另一件事:它是真正的递归,因为它为每个新的子孙级别调用自己。

from collections import OrderedDict

def duplicate_model_with_descendants(obj, whitelist, _new_parent_pk=None):
    kwargs = {}
    children_to_clone = OrderedDict()
    for field in obj._meta.get_fields():
        if field.name == "id":
            pass
        elif field.one_to_many:
            if field.name in whitelist:
                these_children = list(getattr(obj, field.name).all())
                if children_to_clone.has_key(field.name):
                    children_to_clone[field.name] |= these_children
                else:
                    children_to_clone[field.name] = these_children
            else:
                pass
        elif field.many_to_one:
            if _new_parent_pk:
                kwargs[field.name + '_id'] = _new_parent_pk
        elif field.concrete:
            kwargs[field.name] = getattr(obj, field.name)
        else:
            pass
    new_instance = obj.__class__(**kwargs)
    new_instance.save()
    new_instance_pk = new_instance.pk
    for ky in children_to_clone.keys():
        child_collection = getattr(new_instance, ky)
        for child in children_to_clone[ky]:
            child_collection.add(duplicate_model_with_descendants(child, whitelist=whitelist, _new_parent_pk=new_instance_pk))
    return new_instance

用法示例:

from django.db import models

class Book(models.Model)

class Chapter(models.Model)
    book = models.ForeignKey(Book, related_name='chapters')

class Page(models.Model)
    chapter = models.ForeignKey(Chapter, related_name='pages')

WHITELIST = ['books', 'chapters', 'pages']
original_record = models.Book.objects.get(pk=1)
duplicate_record = duplicate_model_with_descendants(original_record, WHITELIST)