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

Есть модели для 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 нестандартный способ сделать это? Как будет выглядеть общий алгоритм для копирования модели?

Ваше здоровье,

Джон


Обновлять:

Приведенные выше классы - это просто пример, иллюстрирующий мою проблему!

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
42
0
30 487
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

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

Обновлено:

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

__copy__() and __deepcopy__()

Deepcopy хорошо подходит для такого рода вещей. +1 за возможность переопределить функциональность своими собственными подпрограммами копирования, хорошая находка.

Soviut 13.01.2009 04:23

Можно ли использовать решение jb для переопределения deepcopy? Что вы делаете с аргументами, которые он дал в своей дублирующей функции в модели?

Neil 12.02.2011 17:27

Думаю, вам также понравится более простая модель данных.

Неужели правда, что страница находится в какой-то главе, а в другой книге?

userMe = User( username = "me" )
userYou= User( username = "you" )
bookMyA = Book( userMe )
bookYourB = Book( userYou )

chapterA1 = Chapter( book= bookMyA, author=userYou ) # "me" owns the Book, "you" owns the chapter?

chapterB2 = Chapter( book= bookYourB, author=userMe ) # "you" owns the book, "me" owns the chapter?

page1 = Page( book= bookMyA, chapter= chapterB2, author=userMe ) # Book and Author aggree, chapter doesn't?

Похоже, ваша модель слишком сложна.

Думаю, тебе бы больше понравилось что-нибудь попроще. Я просто догадываюсь об этом, так как не знаю всей вашей проблемы.

class Book(models.Model)
    name = models.CharField(...)

class Chapter(models.Model)
    name = models.CharField(...)
    book = models.ForeignKey(Book)

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

Каждая страница имеет собственное авторство. Таким образом, у каждой главы есть свой набор авторов, как и у книги. Теперь вы можете дублировать книгу, главу и страницы, назначая клонированные страницы новому автору.

В самом деле, вы можете захотеть иметь отношение «многие ко многим» между страницей и главой, что позволит вам иметь несколько копий только страницы без клонирования книги и главы.

Привет, Лотт, классы - это просто тривиальный пример, который я придумал, чтобы проиллюстрировать мою проблему. Что касается наличия author на каждой странице - все мои таблицы денормализованы, поэтому я могу получить полную картину из любой части головоломки.

jb. 13.01.2009 02:29

@bisharty: Такая денормализация FK, которую вы показали, является причиной вашей проблемы. Бесполезно иметь все эти посторонние внешние ключи с потенциально противоречивыми значениями. И это делает простое «клонирование» намного более сложным, чем должно быть.

S.Lott 13.01.2009 05:17
Ответ принят как подходящий

Это больше не работает в Django 1.3, так как CollectedObjects был удален. См. набор изменений 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

Для django> = 2 там должны быть минимальные изменения. поэтому вывод будет таким:

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.
    """
    from django.db.models.deletion import Collector
    from django.db.models.fields.related import ForeignKey

    collector = Collector(using='default')
    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.remote_field.related_model 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.remote_field.related_model]
                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

Я использую Django 1.6 и обнаружил, что Collector не захватывает все связанные объекты (ниже трех уровней отношений). Вместо этого я использовал NestedObjects, и это помогло: from django.contrib.admin.util import NestedObjects; collector = NestedObjects(using='default'). Ссылка: stackoverflow.com/a/12162619/199754

Neil 06.12.2013 17:48

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

jhrr 27.02.2018 20:43

Есть ли способ сделать это на уровне самой базы данных без использования ORM?

Ajai 15.09.2020 14:32

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

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

По сути:

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

book_to_copy.id = Нет

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

book_to_copy.author = new_author

book_to_copy.save ()

(3) INSERT выполняется вместо UPDATE

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

этот метод больше не работает с Django 1.3 - по-прежнему будет обновлять оригинал

Alvin 18.02.2012 02:46

@Alvin: Ты уверен? Согласно документам, он все еще должен работать: docs.djangoproject.com/en/1.4/ref/models/instances/…

Jonatan Littke 16.05.2012 16:43

Я пробовал несколько раз и продолжал обновлять оригинал ... возможно, это была ошибка с моей стороны ...

Alvin 21.05.2012 02:31

Это довольно гладко. У меня уже год работает. Джанго> = 1.11

juan Isaza 09.12.2020 17:46

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

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

На самом деле я не тестировал это слишком сильно, но тестировал его с объектом, имеющим около 20 000 подобъектов, но всего с тремя уровнями глубины внешнего ключа. Конечно, используйте на свой страх и риск.

Для амбициозного парня, который читает этот пост, вам следует рассмотреть возможность создания подкласса 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

Обновлено: Удален отладочный оператор «печати».

похоже, не работает с Django 1.5.4 - TypeError: hasattr (): имя атрибута должно быть строкой. Есть идеи, что нужно изменить?

KrisF 23.05.2014 01:55

Как сказал @KrisF, эта ошибка возникает, если в экземпляре класса Collector отсутствует аргумент. Используйте collector = Collector(using='default') вместо collector = Collector({}). Использование Dajngo 1.8.5.

Caco 20.02.2018 20:57

У меня это не сработало с Django 1.8.4. Вы проверяли первичные ключи? По моему опыту, PK дочерних объектов не меняются.

Stephen G Tuggy 11.04.2019 21:11

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

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

thing.id = None
thing.pk = None
thing.save()

Это решение кажется нерекурсивным. Работает ли это для вложенных объектов

Adrian B 14.02.2019 21:16

Это решение действительно нерекурсивное. Не включает вложенные объекты.

Stephen G Tuggy 11.04.2019 21:09

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

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

а также

collector = NestedObjects(using=DEFAULT_DB_ALIAS)

вместо CollectorObjects

Простой не общий способ

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

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

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

Я попробовал несколько ответов в 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

Спасибо за это, сэкономили мне много времени! Я сделал несколько настроек, чтобы заставить его делать то, что мне нужно, мой код - здесь с выделенными основными изменениями: - Я использовал get_accessor_name(), чтобы получить имя поля внешнего ключа, которое работает, даже если поле имеет другой related_name - Я пропускаю многие, чтобы много полей со сквозной таблицей, потому что они обрабатываются как внешние ключи для сквозной таблицы - я снова вызываю весь метод duplicate при дублировании связанных объектов, поэтому все связанные объекты создаются и для этих объектов

Mick 21.05.2019 14:02

Мне не повезло ни с одним из ответов здесь с Django 2.1.2, поэтому я создал общий способ выполнения глубокой копии модели базы данных, которая в значительной степени основана на ответах, опубликованных выше.

Ключевые отличия от приведенных выше ответов заключаются в том, что ForeignKey больше не имеет атрибута rel, поэтому его необходимо изменить на f.remote_field.model и т. д.

Кроме того, из-за сложности определения порядка копирования моделей базы данных я создал простую систему очередей, которая перемещает текущую модель в конец списка, если она копируется безуспешно. Код размещен ниже:

import queue
from django.contrib.admin.utils import NestedObjects
from django.db.models.fields.related import ForeignKey

def duplicate(obj, field=None, value=None, max_retries=5):
    # Use the Nested Objects collector to retrieve the related models
    collector = NestedObjects(using='default')
    collector.collect([obj])
    related_models = list(collector.data.keys())

    # Create an object to map old primary keys to new ones
    data_snapshot = {}
    model_queue = queue.Queue()
    for key in related_models:
        data_snapshot.update(
            {key: {item.pk: None for item in collector.data[key]}}
        )
        model_queue.put(key)

    # For each of the models in related models copy their instances
    root_obj = None
    attempt_count = 0
    while not model_queue.empty():
        model = model_queue.get()
        root_obj, success = copy_instances(model, related_models, collector, data_snapshot, root_obj)

        # If the copy is not a success, it probably means that not
        # all the related fields for the model has been copied yet.
        # The current model is therefore pushed to the end of the list to be copied last
        if not success:

            # If the last model is unsuccessful or the number of max retries is reached, raise an error
            if model_queue.empty() or attempt_count > max_retries:
                raise DuplicationError(model)
            model_queue.put(model)
            attempt_count += 1
    return root_obj

def copy_instances(model, related_models, collector, data_snapshot, root_obj):

# Store all foreign keys for the model in a list
fks = []
for f in model._meta.fields:
    if isinstance(f, ForeignKey) and f.remote_field.model in related_models:
        fks.append(f)

# Iterate over the instances of the model
for obj in collector.data[model]:

    # For each of the models foreign keys check if the related object has been copied
    # and if so, assign its personal key to the current objects related field
    for fk in fks:
        pk_field = f"{fk.name}_id"
        fk_value = getattr(obj, pk_field)

        # Fetch the dictionary containing the old ids
        fk_rel_to = data_snapshot[fk.remote_field.model]

        # If the value exists and is in the dictionary assign it to the object
        if fk_value is not None and fk_value in fk_rel_to:
            dupe_pk = fk_rel_to[fk_value]

            # If the desired pk is none it means that the related object has not been copied yet
            # so the function returns unsuccessful
            if dupe_pk is None:
                return root_obj, False

            setattr(obj, pk_field, dupe_pk)

    # Store the old pk and save the object without an id to create a shallow copy of the object
    old_pk = obj.id
    obj.id = None

    if field is not None:
        setattr(obj, field, value)

    obj.save()

    # Store the new id in the data snapshot object for potential use on later objects
    data_snapshot[model][old_pk] = obj.id

    if root_obj is None:
        root_obj = obj

return root_obj, True

Надеюсь, это поможет :)

Ошибка дублирования - это просто расширение исключения:

class DuplicationError(Exception):
    """
    Is raised when a duplication operation did not succeed

    Attributes:
        model -- The database model that failed
    """

    def __init__(self, model):
        self.error_model = model

    def __str__(self):
        return f'Was not able to duplicate database objects for model {self.error_model}'

Вот несколько простоватое решение. Это не зависит от каких-либо недокументированных API-интерфейсов Django. Предполагается, что вы хотите продублировать единственную родительскую запись вместе с дочерними, внучатыми и т. д. Записями. Вы передаете белый список классов, которые фактически должны быть дублированы, в форме list имен отношений «один ко многим» для каждого родительского объекта, которые указывают на его дочерние объекты. В этом коде предполагается, что с учетом приведенного выше белого списка все дерево является самодостаточным, без внешних ссылок, о которых следует беспокоиться.

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

И еще кое-что об этом коде: он действительно рекурсивен в том смысле, что он вызывает себя для каждого нового уровня потомков.

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)
    author = models.ForeignKey('auth.User')

class Chapter(models.Model)
    # author = models.ForeignKey('auth.User')
    book = models.ForeignKey(Book, related_name='chapters')

class Page(models.Model)
    # author = models.ForeignKey('auth.User')
    # book = models.ForeignKey(Book)
    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)

Я экспериментировал с решением Стивена Дж. Тагги и нашел его очень умным, но, к сожалению, в некоторых особых ситуациях оно не сработает.

Предположим следующий сценарий:

class FattAqp(models.Model):    
    descr = models.CharField('descrizione', max_length=200)
    ef = models.ForeignKey(Esercizio, ...)
    forn = models.ForeignKey(Fornitore, ...)

class Periodo(models.Model):
    #  id usato per identificare i documenti
    # periodo rilevato in fattura
    data_i_p = models.DateField('data inizio', blank=True)
    idfatt = models.ForeignKey(FattAqp, related_name='periodo')

class Lettura(models.Model):
    mc_i = models.DecimalField(max_digits=7, ...)
    faqp = models.ForeignKey(FattAqp, related_name='lettura')
    an_im = models.ForeignKey('cnd.AnagImm', ..)

class DettFAqp(models.Model):
    imponibile = models.DecimalField(...)
    voce = models.ForeignKey(VoceAqp, ...)
    periodo = models.ForeignKey(Periodo, related_name='dettfaqp')

В этом случае, если мы попытаемся глубоко скопировать экземпляр FattAqp, поля ef, forn, an_im и voce не будут правильно установлены; с другой стороны idfatt, faqp, periodo будет.

Я решил проблему, добавив к функции еще один параметр и немного изменив код. Я тестировал его на Python 3.6 и Django 2.2. Вот:

def duplicate_model_with_descendants(obj, whitelist, _new_parent_pk=None, static_fk=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 field.name in children_to_clone:
                    children_to_clone[field.name] |= these_children
                else:
                    children_to_clone[field.name] = these_children
            else:
                pass
        elif field.many_to_one:
            name_with_id = field.name + '_id'
            if _new_parent_pk:
                kwargs[name_with_id] = _new_parent_pk

            if name_with_id in static_fk:
                kwargs[name_with_id] = getattr(obj, name_with_id)

        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,static_fk=static_fk))

Пример использования:

original_record = FattAqp.objects.get(pk=4)
WHITELIST = ['lettura', 'periodo', 'dettfaqp']
STATIC_FK = ['fornitore_id','ef_id','an_im_id', 'voce_id']
duplicate_record = duplicate_model_with_descendants(original_record, WHITELIST, static_fk=STATIC_FK)

Разработано на основе предыдущих ответов:

def derive(obj):
    import copy
    from django.contrib.admin.utils import NestedObjects
    from django.db import DEFAULT_DB_ALIAS
    from django.db.models.fields.related import ForeignKey
    """
        Derive a new model instance from previous one,
        and duplicate all related fields to point to the new instance
    """
    obj2 = copy.copy(obj)
    obj2.pk = None
    obj2.save()
    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    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]]
                )
            )
        })

    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:
                dupe_obj = copy.copy(obj)
                setattr(dupe_obj, fk.name, obj2)
                dupe_obj.pk = None
                dupe_obj.save()
    return obj2

В админке django есть возможность создать дубликат / clone / save-as-new.

  1. Создайте класс ModelAdmin модели, которую вы хотите клонировать, в admin.py
  2. В классе добавьте действие администратора, например:
 @admin.register(Book)
 class BookAdmin(models.ModelAdmin):
     save_as = True

и это создаст кнопку «Сохранить как новый» на панели администратора, чтобы полностью клонировать объект модели со всеми связанными с ним полями.

Мне не удалось добавить код, поскольку редактор преобразовывает мой код в список маркеров.

ParfectShot 06.12.2019 11:06

Чтобы добавить код в маркированный список, вам нужно сделать отступ на 8 пробелов вместо 4.

DavidW 06.12.2019 11:26

Предложение Хулио Маринса работает! Спасибо!

Для Django> = 2. * эта строка:

if isinstance(f, ForeignKey) and f.rel.to in related_models:

Следует заменить на:

if isinstance(f, ForeignKey) and f.remote_field.model in related_models:

Другие вопросы по теме