Дублирование экземпляров модели и связанных с ними объектов в 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 нестандартный способ сделать это? Как будет выглядеть универсальный алгоритм дублирования модели?
Ура,
Джон
Обновить:
Классы, приведенные выше, являются лишь примером, иллюстрирующим мою проблему!
18 ответов
Это больше не работает в 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
Вот простой способ скопировать ваш объект.
В принципе:
(1) установите идентификатор вашего исходного объекта на None:
book_to_copy.id = Нет
(2) измените атрибут 'author' и сохраните объект:
book_to_copy.author = new_author
book_to_copy.save ()
(3) вместо вставки выполняется вставка
(Это не касается смены автора на странице - я согласен с комментариями относительно реструктуризации моделей)
Я не пробовал это в Django, но глубокая копия Python может просто работать для вас
РЕДАКТИРОВАТЬ:
Вы можете определить пользовательское поведение копирования для ваших моделей, если вы реализуете функции:
__copy__() and __deepcopy__()
Это редактирование 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
РЕДАКТИРОВАТЬ: Удален отладочный оператор "печать".
Я попробовал несколько ответов в 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
Использование приведенного выше фрагмента CollectedObjects больше не работает, но его можно сделать с помощью следующей модификации:
from django.contrib.admin.util import NestedObjects
from django.db import DEFAULT_DB_ALIAS
а также
collector = NestedObjects(using=DEFAULT_DB_ALIAS)
вместо CollectorObjects
В Django 1.5 это работает для меня:
thing.id = None
thing.pk = None
thing.save()
Если в создаваемой вами базе данных есть всего пара копий, я обнаружил, что вы можете просто использовать кнопку "Назад" в интерфейсе администратора, изменить необходимые поля и снова сохранить экземпляр. Это сработало для меня в тех случаях, когда, например, мне нужно создать коктейль "буравчик" и "булавка водка", где единственная разница - это замена названия и ингредиента. Очевидно, это требует небольшого предвидения данных и не так мощно, как переопределение копирования / глубокой копии django - но для некоторых это может помочь.
У Django есть встроенный способ дублировать объект через администратора - как здесь ответили: В интерфейсе администратора Django есть способ дублировать элемент?
Простой не универсальный способ
Предложенные решения не сработали для меня, поэтому я пошел простым, а не умным путем. Это полезно только для простых случаев.
Для модели со следующей структурой
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
Вот несколько простое решение. Это не зависит от каких-либо недокументированных 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)
Библиотека django-clone отлично работает для меня с отношениями ManyToMany. Просто:
- Сделайте модель, которую вы хотите клонировать, подклассом
CloneModel
:
from django.db import models
from model_clone.models import CloneModel
class MyModel(CloneModel):
name = models.CharField(max_length=50)
tags = models.ManyToManyField(Tag)
# You must specify all the ManyToManyField fields
_clone_m2m_fields = ['tags']
- Тогда просто позвони
make_clone
метод:
obj = MyModel.objects.get(pk=some_pk)
cloned = obj.make_clone()
Вы также можете определить определенные значения для клонированного объекта. Читайте документы для получения дополнительной информации!
В админке django есть возможность создать дубликат / clone / save-as-new.
- Создайте класс ModelAdmin модели, которую вы хотите клонировать, в admin.py
- В классе добавьте действие администратора, например:
@admin.register(Book) class BookAdmin(models.ModelAdmin): save_as = True
и это создаст кнопку "Сохранить как новый" на панели администратора, чтобы полностью клонировать объект модели со всеми связанными с ним полями.
Мне не повезло ни с одним из ответов здесь, связанных с 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}'
Я думаю, что вы были бы счастливее и с более простой моделью данных.
Правда ли, что страница в какой-то главе, но в другой книге?
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)
Каждая страница имеет свое авторство. Каждая глава, таким образом, имеет коллекцию авторов, как и книга. Теперь вы можете дублировать книгу, главу и страницы, назначая клонированные страницы новому автору.
Действительно, вы можете захотеть иметь отношения "многие ко многим" между Page и Chapter, что позволит вам иметь несколько копий только Page, без клонирования книги и Chapter.
Я экспериментировал с решением Стивена Г. Тагги и нашел его очень умным, но, к сожалению, он не будет работать в некоторых особых ситуациях.
Давайте предположим следующий сценарий:
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 >= 2.* эта строка:
if isinstance(f, ForeignKey) and f.rel.to in related_models:
Следует заменить на:
if isinstance(f, ForeignKey) and f.remote_field.model in related_models: