Дублирование экземпляров модели и связанных с ними объектов в 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 ]

1 голос
/ 15 ноября 2018

Мне не повезло ни с одним из ответов здесь с 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}'
1 голос
/ 13 января 2009

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

Правда ли, что страница в какой-то главе, но в другой книге?

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)

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

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

0 голосов
/ 04 мая 2019

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

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

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)
0 голосов
/ 11 апреля 2019

Вот несколько простое решение. Это не зависит от каких-либо недокументированных 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)
...