Дублирование экземпляров модели и связанных с ними объектов в Django / Algorithm для повторного дублирования объекта - PullRequest
35 голосов
/ 13 января 2009

У меня есть модели для Books, Chapters и Pages. Все они написаны User:

from django.db import models

class Book(models.Model)
    author = models.ForeignKey('auth.User')

class Chapter(models.Model)
    author = models.ForeignKey('auth.User')
    book = models.ForeignKey(Book)

class Page(models.Model)
    author = models.ForeignKey('auth.User')
    book = models.ForeignKey(Book)
    chapter = models.ForeignKey(Chapter)

Я хотел бы скопировать существующий Book и обновить его User кому-то еще. Я хотел бы также дублировать все связанные экземпляры модели на Book - все это также Chapters и Pages!

Вещи становятся действительно сложными, если взглянуть на Page - не только новый Pages должен будет обновлять свое поле author, но и указывать на новые Chapter объекты!

Поддерживает ли Django нестандартный способ сделать это? Как будет выглядеть универсальный алгоритм дублирования модели?

Приветствия

John


Обновление:

Классы, приведенные выше, являются лишь примером, иллюстрирующим мою проблему!

Ответы [ 14 ]

16 голосов
/ 14 января 2009

Это больше не работает в Django 1.3, поскольку CollectedObjects было удалено. См. changeset 14507

Я разместил свое решение на Django Snippets. В его основе лежит код django.db.models.query.CollectedObject, используемый для удаления объектов:

from django.db.models.query import CollectedObjects
from django.db.models.fields.related import ForeignKey

def duplicate(obj, value, field):
    """
    Duplicate all related objects of `obj` setting
    `field` to `value`. If one of the duplicate
    objects has an FK to another duplicate object
    update that as well. Return the duplicate copy
    of `obj`.  
    """
    collected_objs = CollectedObjects()
    obj._collect_sub_objects(collected_objs)
    related_models = collected_objs.keys()
    root_obj = None
    # Traverse the related models in reverse deletion order.    
    for model in reversed(related_models):
        # Find all FKs on `model` that point to a `related_model`.
        fks = []
        for f in model._meta.fields:
            if isinstance(f, ForeignKey) and f.rel.to in related_models:
                fks.append(f)
        # Replace each `sub_obj` with a duplicate.
        sub_obj = collected_objs[model]
        for pk_val, obj in sub_obj.iteritems():
            for fk in fks:
                fk_value = getattr(obj, "%s_id" % fk.name)
                # If this FK has been duplicated then point to the duplicate.
                if fk_value in collected_objs[fk.rel.to]:
                    dupe_obj = collected_objs[fk.rel.to][fk_value]
                    setattr(obj, fk.name, dupe_obj)
            # Duplicate the object and save it.
            obj.id = None
            setattr(obj, field, value)
            obj.save()
            if root_obj is None:
                root_obj = obj
    return root_obj
9 голосов
/ 25 июня 2010

Вот простой способ скопировать ваш объект.

В основном:

(1) установите идентификатор вашего исходного объекта на None:

book_to_copy.id = Нет

(2) измените атрибут автора и сохраните объект:

book_to_copy.author = new_author

book_to_copy.save ()

(3) вместо UPDATE выполнена вставка

(Это не касается смены автора на странице - я согласен с комментариями относительно реструктуризации моделей)

8 голосов
/ 13 января 2009

Я не пробовал это в django, но глубокая копия Python может просто работать для вас

EDIT:

Вы можете определить пользовательское поведение копирования для ваших моделей, если вы реализуете функции:

__copy__() and __deepcopy__()
7 голосов
/ 19 мая 2011

это редактирование http://www.djangosnippets.org/snippets/1282/

Теперь он совместим с Collector, который заменил CollectedObjects в 1.3.

Я на самом деле не тестировал это слишком сильно, но тестировал его с объектом с примерно 20000 подобъектами, но только с тремя уровнями глубины внешнего ключа. Используйте на свой страх и риск, конечно.

Для амбициозного парня, который читает этот пост, вам следует рассмотреть возможность создания подкласса Collector (или копирования всего класса, чтобы удалить эту зависимость от этого неопубликованного раздела API django) в класс, называемый чем-то вроде «DuplicateCollector», и написание .duplicate метод, который работает аналогично методу .delete. это решило бы эту проблему реальным способом.

from django.db.models.deletion import Collector
from django.db.models.fields.related import ForeignKey

def duplicate(obj, value=None, field=None, duplicate_order=None):
    """
    Duplicate all related objects of obj setting
    field to value. If one of the duplicate
    objects has an FK to another duplicate object
    update that as well. Return the duplicate copy
    of obj.
    duplicate_order is a list of models which specify how
    the duplicate objects are saved. For complex objects
    this can matter. Check to save if objects are being
    saved correctly and if not just pass in related objects
    in the order that they should be saved.
    """
    collector = Collector({})
    collector.collect([obj])
    collector.sort()
    related_models = collector.data.keys()
    data_snapshot =  {}
    for key in collector.data.keys():
        data_snapshot.update({ key: dict(zip([item.pk for item in collector.data[key]], [item for item in collector.data[key]])) })
    root_obj = None

    # Sometimes it's good enough just to save in reverse deletion order.
    if duplicate_order is None:
        duplicate_order = reversed(related_models)

    for model in duplicate_order:
        # Find all FKs on model that point to a related_model.
        fks = []
        for f in model._meta.fields:
            if isinstance(f, ForeignKey) and f.rel.to in related_models:
                fks.append(f)
        # Replace each `sub_obj` with a duplicate.
        if model not in collector.data:
            continue
        sub_objects = collector.data[model]
        for obj in sub_objects:
            for fk in fks:
                fk_value = getattr(obj, "%s_id" % fk.name)
                # If this FK has been duplicated then point to the duplicate.
                fk_rel_to = data_snapshot[fk.rel.to]
                if fk_value in fk_rel_to:
                    dupe_obj = fk_rel_to[fk_value]
                    setattr(obj, fk.name, dupe_obj)
            # Duplicate the object and save it.
            obj.id = None
            if field is not None:
                setattr(obj, field, value)
            obj.save()
            if root_obj is None:
                root_obj = obj
    return root_obj

EDIT: удален отладочный оператор "print".

4 голосов
/ 23 января 2016

Использование приведенного выше фрагмента CollectedObjects больше не работает, но может быть сделано со следующей модификацией:

from django.contrib.admin.util import NestedObjects
from django.db import DEFAULT_DB_ALIAS

и

collector = NestedObjects(using=DEFAULT_DB_ALIAS)

вместо CollectorObjects

4 голосов
/ 22 мая 2013

В Django 1.5 это работает для меня:

thing.id = None
thing.pk = None
thing.save()
3 голосов
/ 25 апреля 2012

Django имеет встроенный способ дублировать объект через администратора - как здесь ответили: В интерфейсе администратора Django есть способ дублировать элемент?

3 голосов
/ 22 июня 2010

Если в создаваемой вами базе данных есть всего пара копий, я обнаружил, что вы можете просто использовать кнопку возврата в интерфейсе администратора, изменить необходимые поля и снова сохранить экземпляр. Это сработало для меня в тех случаях, когда, например, мне нужно создать коктейль «буравчик» и «булавка водка», где единственное отличие заключается в замене названия и ингредиента. Очевидно, что это требует небольшого предвидения данных и не так мощно, как переопределение копирования / глубокой копии django - но для некоторых это может помочь.

2 голосов
/ 11 октября 2018

Я попробовал несколько ответов в Django 2.2 / Python 3.6, и они, похоже, не копировали связанные объекты «один ко многим» и «многие ко многим». Кроме того, многие из них включали жесткое кодирование / предварительное знание структур данных.

Я написал способ сделать это более общим способом, обрабатывая связанные объекты «один ко многим» и «многие ко многим». Комментарии включены, и я надеюсь улучшить их, если у вас есть предложения:

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. 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, 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.
        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
2 голосов
/ 24 февраля 2017

Простой неуниверсальный способ

Предлагаемые решения у меня не сработали, поэтому я пошел простым, а не умным путем. Это полезно только для простых случаев.

Для модели со следующей структурой

Book
 |__ CroppedFace
 |__ Photo
      |__ AwsReco
            |__ AwsLabel
            |__ AwsFace
                  |__ AwsEmotion

это работает

def duplicate_book(book: Book, new_user: MyUser):
    # AwsEmotion, AwsFace, AwsLabel, AwsReco, Photo, CroppedFace, Book

    old_cropped_faces = book.croppedface_set.all()
    old_photos = book.photo_set.all()

    book.pk = None
    book.user = new_user
    book.save()

    for cf in old_cropped_faces:
        cf.pk = None
        cf.book = book
        cf.save()

    for photo in old_photos:
        photo.pk = None
        photo.book = book
        photo.save()

        if hasattr(photo, 'awsreco'):
            reco = photo.awsreco
            old_aws_labels = reco.awslabel_set.all()
            old_aws_faces = reco.awsface_set.all()
            reco.pk = None
            reco.photo = photo
            reco.save()

            for label in old_aws_labels:
                label.pk = None
                label.reco = reco
                label.save()

            for face in old_aws_faces:
                old_aws_emotions = face.awsemotion_set.all()
                face.pk = None
                face.reco = reco
                face.save()

                for emotion in old_aws_emotions:
                    emotion.pk = None
                    emotion.aws_face = face
                    emotion.save()
    return book
...