【问题标题】:Duplicating model instances and their related objects in Django / Algorithm for recusrively duplicating an object在 Django 中复制模型实例及其相关对象/用于递归复制对象的算法
【发布时间】:2010-10-01 00:45:54
【问题描述】:

我有 BooksChaptersPages 的模型。都是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 - 也是ChaptersPages

查看Page 时,事情变得非常棘手 - 不仅新的Pages 需要更新其author 字段,而且它们还需要指向新的Chapter 对象!

Django 支持开箱即用的方式吗?复制模型的通用算法是什么样的?

干杯,

约翰


更新:

上面给出的类只是说明我遇到的问题的一个例子!

【问题讨论】:

    标签: python django django-models duplicates


    【解决方案1】:

    这在 Django 1.3 中不再有效,因为 CollectedObjects 已被删除。见changeset 14507

    I posted my solution on 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
    • 这种方法几乎适用于所有情况(使用NestedObjects 更新)但是,如果对象自身具有递归外键,我认为它不会起作用......我正在寻找进入它。
    • 有没有办法在不使用 ORM 的情况下在数据库级别本身做到这一点?
    【解决方案2】:

    这是复制对象的简单方法。

    基本上:

    (1) 将原始对象的 id 设置为 None:

    book_to_copy.id = 无

    (2) 更改'author'属性并保存对象:

    book_to_copy.author = new_author

    book_to_copy.save()

    (3) 执行 INSERT 而不是 UPDATE

    (它没有解决在页面中更改作者的问题——我同意 cmets 关于重构模型的观点)

    【讨论】:

    • 这个方法从 Django 1.3 开始不再有效——仍然会更新原来的
    • @Alvin:你确定吗?根据文档,它应该仍然有效:docs.djangoproject.com/en/1.4/ref/models/instances/…
    • 我试了几次,一直让它更新原版……可能是我的错误……
    • 这很时尚。我已经工作了一年了。 Django >= 1.11
    【解决方案3】:

    我没有在 django 中尝试过,但是 python 的 deepcopy 可能对你有用

    编辑:

    如果你实现了函数,你可以为你的模型定义自定义复制行为:

    __copy__() and __deepcopy__()
    

    【讨论】:

    • deepcopy 非常适合这类事情。 +1 能够用您自己的复制例程覆盖该功能,很好的发现。
    • 可以用jb的方案覆盖deepcopy吗?你如何处理他在模型中的重复函数中给出的参数?
    【解决方案4】:

    这是http://www.djangosnippets.org/snippets/1282/的编辑

    它现在与在 1.3 中取代 CollectedObjects 的 Collector 兼容。

    我并没有真正对此进行过多的测试,但确实用一个包含大约 20,000 个子对象的对象进行了测试,但外键深度只有大约三层。当然,使用风险自负。

    对于阅读这篇文章的雄心勃勃的人,您应该考虑将 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
    

    编辑:删除了调试“打印”语句。

    【讨论】:

    • 似乎不适用于 Django 1.5.4 - TypeError: hasattr(): attribute name must be string。知道需要改变什么吗?
    • 正如@KrisF 所说,如果在Collector 类的实例化中缺少参数,则会发生此错误。使用collector = Collector(using='default') 而不是collector = Collector({})。使用 Dajngo 1.8.5。
    • 这对我来说不适用于 Django 1.8.4。你检查主键了吗?根据我的经验,子对象的 PK 不会改变。
    【解决方案5】:

    在 Django 1.5 中,这对我有用:

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

    【讨论】:

    • 这个解决方案似乎是非递归的。它是否适用于嵌套对象
    • 这个解决方案确实是非递归的。不包括嵌套对象。
    【解决方案6】:

    使用上面的 CollectedObjects sn-p 不再有效,但可以通过以下修改来完成:

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

    collector = NestedObjects(using=DEFAULT_DB_ALIAS)
    

    而不是 CollectorObjects

    【讨论】:

      【解决方案7】:

      我在 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
      

      【讨论】:

      • 感谢您,为我节省了很多时间!我做了一些调整来让它做我需要的事情,我的代码是here,主要更改突出显示: - 我使用get_accessor_name() 来获取外键字段名称,即使该字段有不同的@987654324 @ - 我跳过了带有直通表的多对多字段,因为它们的处理方式就像直通表的外键一样 - 在复制相关对象时,我再次调用整个 duplicate 方法,因此也为这些对象创建了所有相关对象
      【解决方案8】:

      如果您正在构建的数据库中只有几个副本,我发现您可以使用管理界面中的后退按钮,更改必要的字段并再次保存实例。这对我有用,例如,我需要制作“gimlet”和“vodka gimlet”鸡尾酒,唯一的区别是替换名称和成分。显然,这需要对数据有一点预见性,并且没有覆盖 django 的复制/深度复制那么强大 - 但它可能对某些人有用。

      【讨论】:

        【解决方案9】:

        Django 确实有一种通过管理员复制对象的内置方法 - 如此处回答: In the Django admin interface, is there a way to duplicate an item?

        【讨论】:

          【解决方案10】:

          简单的非通用方式

          建议的解决方案对我不起作用,所以我采用了简单但不聪明的方法。这仅对简单的情况有用。

          对于具有以下结构的模型

          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
          

          【讨论】:

            【解决方案11】:

            这是一个有点简单的解决方案。这不依赖于任何未记录的 Django API。它假定您要复制单个父记录,以及它的子记录、孙记录等。您以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)
            

            【讨论】:

              【解决方案12】:

              我认为你也会更喜欢更简单的数据模型。

              一个页面在某个章节中但在另一本书中是真的吗?

              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。

              【讨论】:

              • 嘿 Lott - 这些课程只是我为了说明我遇到的问题而编造的一个简单的例子。至于在每一页中都有author - 我所有的表格都是非规范化的,所以我可以从拼图中的任何一块得到一个完整的画面。
              • @bisharty:你所展示的那种 FK 非规范化是你的问题的原因。让所有那些无关紧要的外键具有潜在的矛盾值是没有帮助的。它使简单的“克隆”变得比它需要的复杂得多。
              【解决方案13】:

              我对 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}'
              

              【讨论】:

                【解决方案14】:

                在 django admin 中有一个创建副本/克隆/另存为新的选项。

                1. 在 admin.py 中创建要克隆的模型的 ModelAdmin 类
                2. 在类中添加如下管理操作:
                 @admin.register(Book)
                 class BookAdmin(models.ModelAdmin):
                     save_as = True
                

                这将在您的管理面板中创建一个“另存为新”按钮,以完全克隆模型对象及其所有相关字段。

                【讨论】:

                • 我无法添加代码,因为编辑器正在将我的代码转换为项目符号列表
                • 要在项目符号列表中添加代码,您需要将其缩进 8 个空格而不是 4 个。
                【解决方案15】:

                我尝试了 Stephen 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、forn、an_im 和 voce 字段;另一方面 idfatt, faqp, periodo will.

                我通过向函数添加一个参数并稍微修改代码解决了这个问题。我用 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)
                

                【讨论】:

                  【解决方案16】:

                  根据之前的答案详细阐述:

                  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
                  

                  【讨论】:

                    【解决方案17】:

                    Julio Marins 作品的建议!谢谢!

                    对于 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:

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-07-22
                      • 2012-08-24
                      • 2017-06-27
                      • 2017-04-12
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多