以前,当我要递归克隆对象时遇到问题。 我知道克隆对象的简单方法是这样的:
obj = Foo.objects.get(pk=<some_existing_pk>)
obj.pk = None
obj.save()
但是,我想做得更多。例如,我有一个models.py
class Post(TimeStampedModel):
author = models.ForeignKey(User, related_name='posts',
on_delete=models.CASCADE)
title = models.CharField(_('Title'), max_length=200)
content = models.TextField(_('Content'))
...
class Comment(TimeStampedModel):
author = models.ForeignKey(User, related_name='comments',
on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
comment = models.TextField(_('Comment'))
...
class CommentAttribute(TimeStampedModel):
comment = models.OneToOneField(Comment, related_name='comment_attribute',
on_delete=models.CASCADE)
is_bookmark = models.BooleanField(default=False)
...
class PostComment(TimeStampedModel):
post = models.ForeignKey(Post, related_name='post_comments',
on_delete=models.CASCADE)
comments = models.ManyToManyField(Comment)
...
当我从
Post
克隆父对象时,像Comment
,CommentAttribute
和PostComment
这样的子对象也将通过跟随新克隆的Post
对象来克隆。 子模型是 动态 。因此,我想通过创建对象克隆器之类的工具使其变得简单。
下面的代码段是我所做的;
from django.db.utils import IntegrityError
class ObjectCloner(object):
"""
[1]. The simple way with global configuration:
>>> cloner = ObjectCloner()
>>> cloner.set_objects = [obj1, obj2] # or can be queryset
>>> cloner.include_childs = True
>>> cloner.max_clones = 1
>>> cloner.execute()
[2]. Clone the objects with custom configuration per-each objects.
>>> cloner = ObjectCloner()
>>> cloner.set_objects = [
{
'object': obj1,
'include_childs': True,
'max_clones': 2
},
{
'object': obj2,
'include_childs': False,
'max_clones': 1
}
]
>>> cloner.execute()
"""
set_objects = [] # list/queryset of objects to clone.
include_childs = True # include all their childs or not.
max_clones = 1 # maximum clone per-objects.
def clone_object(self, object):
"""
function to clone the object.
:param `object` is an object to clone, e.g: <Post: object(1)>
:return new object.
"""
try:
object.pk = None
object.save()
return object
except IntegrityError:
return None
def clone_childs(self, object):
"""
function to clone all childs of current `object`.
:param `object` is a cloned parent object, e.g: <Post: object(1)>
:return
"""
# bypass the none object.
if object is None:
return
# find the related objects contains with this current object.
# e.g: (<ManyToOneRel: app.comment>,)
related_objects = object._meta.related_objects
if len(related_objects) > 0:
for relation in related_objects:
# find the related field name in the child object, e.g: 'post'
remote_field_name = relation.remote_field.name
# find all childs who have the same parent.
# e.g: childs = Comment.objects.filter(post=object)
childs = relation.related_model.objects.all()
for old_child in childs:
new_child = self.clone_object(old_child)
if new_child is not None:
# FIXME: When the child field as M2M field, we gote this error.
# "TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use comments.set() instead."
# how can I clone that M2M values?
setattr(new_child, remote_field_name, object)
new_child.save()
self.clone_childs(new_child)
return
def execute(self):
include_childs = self.include_childs
max_clones = self.max_clones
new_objects = []
for old_object in self.set_objects:
# custom per-each objects by using dict {}.
if isinstance(old_object, dict):
include_childs = old_object.get('include_childs', True)
max_clones = old_object.get('max_clones', 1)
old_object = old_object.get('object') # assigned as object or None.
for _ in range(max_clones):
new_object = self.clone_object(old_object)
if new_object is not None:
if include_childs:
self.clone_childs(new_object)
new_objects.append(new_object)
return new_objects
但是,问题是当子字段作为M2M字段时,我们得到了此错误。
>>> cloner.set_objects = [post]
>>> cloner.execute()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 114, in execute
self.clone_childs(new_object)
File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 79, in clone_childs
self.clone_childs(new_child)
File "/home/agus/envs/env-django-cloner/django-object-cloner/object_cloner_demo/app/utils.py", line 76, in clone_childs
setattr(new_child, remote_field_name, object)
File "/home/agus/envs/env-django-cloner/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 546, in __set__
% self._get_set_deprecation_msg_params(),
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use comments.set() instead.
>>>
来自setattr(...)
的错误,以及“改为使用comments.set()” ,但是我仍然困惑如何更新该m2m值?
new_child = self.clone_object(old_child)
if new_child is not None:
setattr(new_child, remote_field_name, object)
new_child.save()
我也在下面尝试了此代码段,但仍然存在错误。克隆的m2m对象很多,没有填充m2m值。
if new_child is not None:
# check the object_type
object_type = getattr(new_child, remote_field_name)
if hasattr(object_type, 'pk'):
# this mean is `object_type` as real object.
# so, we can directly use the `setattr(...)`
# to update the old relation value with new relation value.
setattr(new_child, remote_field_name, object)
elif hasattr(object_type, '_queryset_class'):
# this mean is `object_type` as m2m queryset (ManyRelatedManager).
# django.db.models.fields.related_descriptors.\
# create_forward_many_to_many_manager.<locals>.ManyRelatedManager
# check the old m2m values, and assign into new object.
# FIXME: IN THIS CASE STILL GOT AN ERROR
old_m2m_values = getattr(old_child, remote_field_name).all()
object_type.add(*old_m2m_values)
new_child.save()
答案 0 :(得分:1)
我试图用一些有效的代码来解决这个有趣的问题……这比我最初想象的要困难!
因为我在遵循ObjectCloner逻辑上遇到了一些困难,所以我偏离了您的原始解决方案。
以下是我能想到的最简单的解决方案;我选择使用单个帮助函数 clone_object()来处理单个对象,而不是使用类。
您当然可以使用第二个函数来处理对象列表或查询集,方法是扫描序列并多次调用clone_object()。
def clone_object(obj, attrs={}):
# we start by building a "flat" clone
clone = obj._meta.model.objects.get(pk=obj.pk)
clone.pk = None
# if caller specified some attributes to be overridden,
# use them
for key, value in attrs.items():
setattr(clone, key, value)
# save the partial clone to have a valid ID assigned
clone.save()
# Scan field to further investigate relations
fields = clone._meta.get_fields()
for field in fields:
# Manage M2M fields by replicating all related records
# found on parent "obj" into "clone"
if not field.auto_created and field.many_to_many:
for row in getattr(obj, field.name).all():
getattr(clone, field.name).add(row)
# Manage 1-N and 1-1 relations by cloning child objects
if field.auto_created and field.is_relation:
if field.many_to_many:
# do nothing
pass
else:
# provide "clone" object to replace "obj"
# on remote field
attrs = {
field.remote_field.name: clone
}
children = field.related_model.objects.filter(**{field.remote_field.name: obj})
for child in children:
clone_object(child, attrs)
return clone
一个经过Python 3.7.6和Django 3.0.6测试的POC示例项目已保存在github上的公共仓库中。
答案 1 :(得分:0)
由于具有M2M关系,因此需要在相关表中创建新记录。 为此,add()似乎更合适。 您可以尝试这样的事情:
for old_child in relation.related_model.objects.all():
new_child = self.clone_object(old_child)
setattr(new_child, remote_field_name, object)
relation.related_model.objects.add(new_child)
请注意,此代码未试用,因此可能需要进行一些调整。
答案 2 :(得分:0)
最初的一些担忧:
related_objects
是受限制的,它仅返回反向关系-如果子级对此父级具有外键,则不具有此父级对子级的外键。虽然这可能是有效的方法(从上到下),但实际上关系可能以不同的方式组织,或者子代可能具有另一个模型的另一个外键(也可能需要克隆)。需要查看所有字段和关系。更不用说Django推荐了
(
related_objects
是)专用API,仅供Django本身使用; get_fields() 结合字段属性过滤是用于 获取此字段列表。
可能会建议一些不同的方法。
使用get_fields()
代替related_objects
。
fields = object._meta.get_fields()
将返回模型中所有字段的列表-在模型本身以及正向/反向访问字段中定义,这些字段由Django自动添加(如related_objects
返回的字段)
可以过滤此列表以仅获取必填字段:
field.is_relation
-对于关系和ForeignKey字段,ManyToMany字段等,将为True
field.auto_created
-对于由django自动创建的字段,将为True
-反向关系,pk / id为AutoField(但它将具有is_relation == False)
field.many_to_many
-对于ManyToMany字段和关系而言都是正确的
通过这种方式,您可以选择必填字段或关系,无论是正向还是反向,无论多对多。并且知道确切的关系类型-相应地创建对象,即为ManyToMany添加设置。
关系字段值(一个或多个相关对象)可以通过getattr,_meta访问或类似以下查询来访问:
children = field.related_model.objects.filter(
**{field.remote_field.name: object}
)
对关系和具有关系的字段有效。
注意:
因为这很可能是特定于应用程序的,因此您想要向上,向下和横向克隆关系的距离(包括父母及其父母;孩子的孩子;与fk on 模型的关系或与fk to 模型的关系;将fks跟随到另一个关于孩子/父母的模型)或过滤允许使用的模型,依此类推-将clone
方法更多地绑定到特定模型可能是可以的结构
还存在隐藏关系-在ForeignKey或ManyToManyField上用related_name = "+"
定义的关系。这些仍然可以通过 include_hidden 参数发现:object._meta.get_fields(include_hidden=True)
答案 3 :(得分:0)
如果您可以告诉您正在使用的Django版本以及通过克隆实际实现的目标,那么对帮助的理解会更容易理解。 Django相关字段在不同版本中的工作方式有所不同,因为前向引用和后向引用的处理方式不同。 因此,如果您能说出您要使用代码执行的实际操作?