如何克隆Django模型实例对象并保存到数据库?

365
Foo.objects.get(pk="foo")
<Foo: test>

在数据库中,我想添加另一个对象,它是上面对象的副本。

假设我的表有一行。我想将第一行对象插入到具有不同主键的另一行中。我该怎么做?

15个回答

639

8
在1.4.1版本中运行良好,这可能是那些将继续长时间运作的事情之一。 - frnhr
13
为了使这在Django 1.4中起作用,我不得不同时设置obj.pkobj.id - Petr Peller
6
@PetrPeller - 文档表明这是因为您正在使用模型继承。 - Dominic Rodger
21
请注意:如果涉及到外键、一对一关系或多对多关系,情况可能会更加复杂(也就是说,可能存在更加复杂的“深度复制”场景)。 - B Robster
4
如果存在一个 datetime 字段,它将会改变。 - suhailvs
显示剩余11条评论

164

Django的数据库查询文档包括有关复制模型实例的部分。 假设您的主键是自动生成的,您可以获取要复制的对象,将主键设置为None,然后再次保存该对象:

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1

blog.pk = None
blog.save() # blog.pk == 2
在这个片段中,第一个 save() 创建原始对象,第二个 save() 创建副本。
如果您继续阅读文档,还有关于处理两个更复杂情况的示例:(1)复制作为模型子类实例的对象,以及(2)复制相关对象,包括在多对多关系中的对象。
关于miah的回答的注意事项:在miah的回答中提到了将pk设置为None,虽然没有直接强调。因此,我的回答主要是强调该方法是Django推荐的方法。
历史注释:在1.4版本之前,Django文档中并没有解释这一点。尽管如此,在1.4版本之前就已经可以实现这个功能了。
可能的未来功能:上述文档更改包含在此票证中。在票证的评论线程中,还讨论了添加模型类内置copy函数的问题,但据我所知,他们决定暂时不解决这个问题。因此,现在可能需要使用这种“手动”复制方式。

64

在这里要小心。如果你正在循环中逐个检索对象,这可能会非常昂贵。如果您不想调用数据库,请执行以下操作:

from copy import deepcopy

new_instance = deepcopy(object_you_want_copied)
new_instance.id = None
new_instance.save()

它做了与其他答案相同的事情,但它不会进行数据库调用以检索对象。如果你想复制尚未存在于数据库中的对象,这也很有用。


1
如果你有一个对象,那么可以在进行修改之前深度复制原始对象,然后对新对象进行更改并保存,这样就非常有效。接着你可以进行一些条件检查,根据它们是否通过(例如该对象是否在另一个你正在检查的表中),来设置 new_instance.id = original_instance.id 并保存 :) 谢谢! - radtek
4
如果模型存在多级继承,这种方法就不起作用了。 - chubao
1
在我的情况下,我想为该模型创建一个克隆方法,它将使用"self"变量。我不能简单地设置self.pk = None,因此这个解决方案非常有效。我考虑了下面的model_to_dict解决方案,但它需要额外的步骤,并且它会遇到与通过关系相同的问题,所以我必须手动处理它们,对我来说没有太大的影响。 - Anderson Santos

54

请使用以下代码:

from django.forms import model_to_dict

instance = Some.objects.get(slug='something')

kwargs = model_to_dict(instance, exclude=['id'])
new_instance = Some.objects.create(**kwargs)

11
model_to_dict函数有一个exclude参数,这意味着你不需要单独使用pop函数:可以使用以下方式调用model_to_dict(instance, exclude=['id']) - georgebrock
4
这将会导致外键异常。 - SEDaradji

23

这里有一个克隆代码片段(在这里),你可以添加到你的模型中来实现此操作:

def clone(self):
  new_kwargs = dict([(fld.name, getattr(old, fld.name)) for fld in old._meta.fields if fld.name != old._meta.pk]);
  return self.__class__.objects.create(**new_kwargs)

@user426975 - 啊,好吧(我已经从我的答案中删除了它)。 - Dominic Rodger
不确定这是否是Django版本的问题,但现在if需要是if fld.name!= old._meta.pk.name,即_meta.pk实例的name属性。 - Chris

22

当您打开链接时,它会显示页面未找到。 - Amrit
Django 1.4 的文档已经不存在了。我会更新答案,指向最新的文档。 - Michael Bylstra
1
@MichaelBylstra 保持链接长青的好方法是在URL中使用stable而不是版本号,就像这样:https://docs.djangoproject.com/en/stable/topics/db/queries/#copying-model-instances - Flimm

13
我遇到了一些接受答案时出现的问题。这是我的解决方案。
import copy

def clone(instance):
    cloned = copy.copy(instance) # don't alter original instance
    cloned.pk = None
    try:
        delattr(cloned, '_prefetched_objects_cache')
    except AttributeError:
        pass
    return cloned

注意:这里使用的解决方案并未在 Django 文档中得到官方认可,这些方法可能会在以后的版本中停止工作。我在 1.9.13 版本中测试过。
第一个改进是通过使用 copy.copy,使您可以继续使用原始实例。即使您不打算重用该实例,如果您克隆的实例作为参数传递给函数,则执行此步骤可能更安全。如果没有这样做,当函数返回时调用者将意外地拥有不同的实例。 copy.copy 似乎以所需的方式产生 Django 模型实例的浅复制。这是我没有找到记录的事情之一,但它通过 pickling 和 unpickling 工作,因此它可能得到了很好的支持。
其次,批准的答案将保留任何预取结果附加到新实例上。除非您明确复制了多对多关系,否则这些结果不应与新实例相关联。如果您遍历预取关系,您将获得与数据库不匹配的结果。添加预取时破坏现有代码可能是一个令人讨厌的惊喜。
删除 _prefetched_objects_cache 是一个快速而粗略的方法,可以去除所有预取。随后的多对多访问的效果就好像从未进行过预取一样。使用以下划线开头的未记录属性可能会导致兼容性问题,但现在可以使用。

我已经成功让它工作了,但看起来在1.11中可能已经发生了变化,因为我有一个名为_[model_name]_cache的属性,一旦删除,我就能为相关模型分配一个新的ID,然后调用save()。仍然可能存在我尚未确定的副作用。 - trpt4him
如果你正在类/混入中的函数中进行克隆操作,这是非常重要的信息,否则它会搞乱'self',让你感到困惑。 - Andreas Bergström
非常有用,如果你计划在代码中进一步使用原始对象的话。我疯狂地花了2个小时试图弄清楚为什么我的下一个查询使用原始主题却没有任何结果。 - undefined

7
这是另一种克隆模型实例的方法:
d = Foo.objects.filter(pk=1).values().first()   
d.update({'id': None})
duplicate = Foo.objects.create(**d)

5

将pk设置为None更好,因为Django可以正确地为您创建一个pk。

object_copy = MyObject.objects.get(pk=...)
object_copy.pk = None
object_copy.save()

3

这将进行内存中的复制,您可以独立地对其进行变异。

original = CheckoutItem(title="test", ...)
copy = CheckoutItem()

for f in CheckoutItem._meta.fields:
   setattr(copy, f.attname, getattr(original, f.attname))

或者,作为一种方法:


    def clone(self):
        """Returns a clone of this instance."""

        clone = self.__class__()
        for f in self.__class__._meta.fields:
            setattr(clone, f.attname, getattr(self, f.attname))

        return clone

1
这个代码不像预期一样工作,因为它复制了 pkid,保存克隆对象实际上会更新它。 - The-null-Pointer-
确实,克隆将是完全相同的。如果您想将其保存为“新”实例,则只需设置clone.pk = None。(我建议使用pk而不是id,以防主键是其他字段,例如:uuid)。 - WhyNotHugo
它适用于外键和多对多字段吗? - Sandeep Balagopal
@SandeepBalagopal 我不是完全确定(我鼓励你尝试并确认一下),但我认为克隆只会有指向原始实例相同的ForeignKey。 - WhyNotHugo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接