在 Django 中复制模型实例及其相关对象/用于递归复制对象的算法
问题描述
我有 Books
、Chapters
和 Pages
的模型.它们都是由 User
编写的:
I've models for Books
, Chapters
and Pages
. They are all written by a 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
也是如此!
What I'd like to do is duplicate an existing Book
and update it's User
to someone else. The wrinkle is I would also like to duplicate all related model instances to the Book
- all it's Chapters
and Pages
as well!
当查看 Page
时,事情变得非常棘手 - 新的 Pages
不仅需要更新其 author
字段,而且它们会还需要指向新的Chapter
对象!
Things get really tricky when look at a Page
- not only will the new Pages
need to have their author
field updated but they will also need to point to the new Chapter
objects!
Django 支持开箱即用的方式吗?复制模型的通用算法是什么样的?
Does Django support an out of the box way of doing this? What would a generic algorithm for duplicating a model look like?
干杯,
约翰
更新:
上面给出的类只是说明我遇到的问题的一个例子!
The classes given above are just an example to illustrate the problem I'm having!
解决方案
这不再适用于 Django 1.3,因为 CollectedObjects 已被删除.请参阅 变更集 14507
This no longer works in Django 1.3 as CollectedObjects was removed. See changeset 14507
我在 Django Snippets 上发布了我的解决方案. 它主要基于 django.db.models.query.CollectedObject
用于删除对象的代码:
I posted my solution on Django Snippets. It's based heavily on the django.db.models.query.CollectedObject
code used for deleting objects:
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 应该有一些微小的变化.所以输出会是这样的:
For django >= 2 there should be some minimal changes. so the output will be like this:
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
相关文章