تكرار مثيلات النموذج والكائنات المرتبطة بها في Django/Algorithm لتكرار كائن بشكل متكرر
-
22-07-2019 - |
سؤال
لدي نماذج ل 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 طريقة خارج الصندوق للقيام بذلك؟كيف ستبدو الخوارزمية العامة لتكرار النموذج؟
هتافات،
جون
تحديث:
الفصول المذكورة أعلاه هي مجرد مثال لتوضيح المشكلة التي أواجهها!
المحلول
وهذا لم يعد يعمل في جانغو 1.3 كما تمت إزالة CollectedObjects. انظر changeset 14507
نشرت لي حل بلدي على جانغو القصاصات. وأنها تقوم بشكل كبير على في 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) تعيين معرف الكائن الأصلي إلى بلا:
وbook_to_copy.id = بلا
و(2) تغيير "المؤلف السمة وحفظ ojbect:
وbook_to_copy.author = new_author
وbook_to_copy.save ()
و(3) تنفيذ INSERT بدلا من UPDATE
و(لا معالجة تغيير المؤلف في الصفحة - وأنا أتفق مع تعليقات بشأن إعادة هيكلة نماذج)
وأنا لم أحاول ذلك في جانغو لكن الثعبان في deepcopy قد تعمل فقط لأجلك
تعديل:
ويمكنك تحديد السلوك نسخة مخصصة للالنماذج الخاصة بك إذا قمت بتطبيق وظائف:
__copy__() and __deepcopy__()
هذا تحرير ل http://www.djangosnippets.org/snippets/1282/
وهو الآن متوافق مع Collector الذي حل محل CollectedObjects في الإصدار 1.3.
لم أختبر هذا كثيرًا حقًا، لكنني اختبرته باستخدام كائن يحتوي على حوالي 20000 كائن فرعي، ولكن في حوالي ثلاث طبقات فقط من عمق المفتاح الخارجي.استخدم على مسؤوليتك الخاصة بالطبع.
بالنسبة للرجل الطموح الذي يقرأ هذا المنشور، يجب أن تفكر في تصنيف فئة فرعية Collector (أو نسخ الفصل بأكمله لإزالة هذا الاعتماد على هذا القسم غير المنشور من Django API) إلى فئة تسمى شيئًا مثل "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
يحرر:تمت إزالة عبارة "الطباعة" لتصحيح الأخطاء.
في جانغو 1.5 يعمل هذا بالنسبة لي:
thing.id = None
thing.pk = None
thing.save()
وباستخدام CollectedObjects المقتطف أعلاه لا يعمل أطول ولكن يمكن القيام به مع إجراء التعديل التالي:
from django.contrib.admin.util import NestedObjects
from django.db import DEFAULT_DB_ALIAS
و
collector = NestedObjects(using=DEFAULT_DB_ALIAS)
وبدلا من CollectorObjects
وإذا كان هناك بضع نسخ في قاعدة البيانات كنت بناء، لقد وجدت يمكنك فقط استخدام الزر مرة أخرى في واجهة الادارية، تغيير الحقول الضرورية وحفظ المثال مرة أخرى. هذا وقد عملت بالنسبة لي في الحالات التي يكون فيها، على سبيل المثال، ولست بحاجة لبناء "مخرز" و "الفودكا مخرز" كوكتيل حيث الفرق الوحيد هو استبدال اسم وعنصر. ومن الواضح أن هذا يتطلب التبصر القليل من البيانات وليست قوية كما تجاوز جانغو لنسخ / deepcopy - لكنها قد تفعل خدعة لبعض
وجانغو لديها وسيلة المدمج في لتكرار كائن عبر المشرف - كما أجاب هنا: في واجهة جانغو الادارية، هل هناك طريقة لتكرار عنصر؟
بسيطة غير بطريقة عامة
لموالحلول المقترحة لا تعمل بالنسبة لي، لذلك ذهبت إلى طريقة بسيطة وليس ذكية. وهذا مفيد فقط للحالات البسيطة.
لنموذج مع هيكل التالي
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
وحاولت عدد قليل من الإجابات في جانغو 2.2 / 3.6 بيثون، وأنها لا يبدو أن نسخ واحد لكثير وكثير إلى العديد من الكائنات ذات الصلة. أيضا، شملت العديد hardcoding / علم مسبق أدرجت من هياكل البيانات.
وكتبت طريقة للقيام بذلك بطريقة أكثر عمومية، والتعامل مع واحد لكثير وكثير إلى العديد من الكائنات ذات الصلة. وتضمنت تعليقات، وأنا أبحث لتحسين عليه إذا كان لديك اقتراحات:
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
وكنت أعتقد أن يكون أكثر سعادة مع نموذج بيانات أكثر بساطة، أيضا.
هل حقا صحيح أن الصفحة هي في بعض الفصل ولكن كتاب مختلف؟
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)
وكل صفحة لها التأليف واضح. كل فصل، ثم، لديه مجموعة من المؤلفين، كما يفعل الكتاب. الآن يمكنك تكرار كتاب والفصول والصفحات، تعيين الصفحات المستنسخة إلى الجديد المؤلف.
والواقع، قد تريد أن يكون لها العديد-إلى العديد من العلاقة بين الصفحة والفصل، مما يتيح لك الحصول على نسخ متعددة من مجرد صفحة، من دون استنساخ الكتاب والفصل.
لم يحالفني الحظ في أي من الإجابات هنا جانغو 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}'
وهنا هو حل بسيط التفكير إلى حد ما. وهذا لا يعتمد على أي واجهات برمجة التطبيقات جانغو غير موثقة. فإنه يفترض أن كنت تريد تكرار السجل الأصل واحد، جنبا إلى جنب مع الطفل لها، حفيد، الخ السجلات. أنت تمر في القائمة البيضاء من الفئات التي يجب فعلا أن تتكرر في شكل 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)
وأنا جربت الحل ستيفن G Tuggy ولقد وجدت أنه ذكي جدا ولكن، للأسف، فإنه لن ينجح في بعض الحالات الخاصة.
ودعونا نفترض السيناريو التالي:
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، الفرن، an_im وسوف الحقول شفوي لم يتم تعيين بشكل صحيح؛ من جهة أخرى ناحية idfatt، faqp والإرادة PERIODO.
وأنا حل المشكلة عن طريق إضافة واحدة أكثر المعلمة إلى وظيفة ومع تعديل طفيف إلى رمز. اختبرته مع بيثون 3.6 و 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)