تكرار مثيلات النموذج والكائنات المرتبطة بها في Django/Algorithm لتكرار كائن بشكل متكرر

StackOverflow https://stackoverflow.com/questions/437166

سؤال

لدي نماذج ل 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)
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top