我有以下内容:
target_content_type = models.ForeignKey(ContentType, related_name='target_content_type')
target_object_id = models.PositiveIntegerField()
target = generic.GenericForeignKey('target_content_type', 'target_object_id')
我希望dumpdata --natural为这种关系发出一个自然键。这可能吗?如果没有,是否有一种替代策略不会将我绑定到目标的主键?
答案 0 :(得分:7)
TL; DR - 目前没有明智的做法,因为没有创建自定义Serializer
/ Deserializer
对。
具有通用关系的模型的问题是Django根本没有看到target
作为字段,只有target_content_type
和target_object_id
,并且它尝试序列化和反序列化它们单独
负责序列化和反序列化Django模型的类位于模块django.core.serializers.base
和django.core.serializers.python
中。所有其他人(xml
,json
和yaml
)都会延伸其中任何一个(python
扩展base
)。字段序列化是这样完成的(无关的行无关):
for obj in queryset:
for field in concrete_model._meta.local_fields:
if field.rel is None:
self.handle_field(obj, field)
else:
self.handle_fk_field(obj, field)
这是第一个复杂问题:ContentType
的外键处理正常,使用我们预期的自然键。但是PositiveIntegerField
由handle_field
处理,实现方式如下:
def handle_field(self, obj, field):
value = field._get_val_from_obj(obj)
# Protected types (i.e., primitives like None, numbers, dates,
# and Decimals) are passed through as is. All other values are
# converted to string first.
if is_protected_type(value):
self._current[field.name] = value
else:
self._current[field.name] = field.value_to_string(obj)
即。这里自定义的唯一可能性(子类PositiveIntegerField
和定义custom value_to_string
)将无效,因为序列化程序不会调用它。将target_object_id
的数据类型更改为除整数之外的其他内容可能会破坏许多其他内容,因此它不是一个选项。
我们可以定义我们的自定义handle_field
以在这种情况下发出自然键,但接下来是第二个并发症:反序列化就像这样完成:
for (field_name, field_value) in six.iteritems(d["fields"]):
field = Model._meta.get_field(field_name)
...
data[field.name] = field.to_python(field_value)
即使我们自定义了to_python
方法,它也会在对象的上下文中单独作用于field_value
。使用整数时这不是问题,因为它将被解释为模型的主键,无论它是什么型号。但是为了反序列化自然键,首先我们需要知道该键所属的模型,并且除非我们获得对象的引用(并且target_content_type
字段已被反序列化),否则该信息不可用。 / p>
正如您所看到的,这不是一项不可能完成的任务 - 支持泛型关系中的自然键 - 但要实现这一点,需要在序列化和反序列化代码中更改许多内容。必要的步骤(如果有人感觉到任务)是:
Field
扩展PositiveIntegerField
,其中包含对对象进行编码/解码的方法 - 调用引用的模型“natural_key
和get_by_natural_key
; handle_field
以调用编码器(如果存在); field_value
,还传递解码的ContentType
。答案 1 :(得分:0)
I've written a custom Serializer and Deserializer which supports GenericFK's. Checked it briefly and it seems to do the job.
This is what I came up with:
import json
from django.contrib.contenttypes.generic import GenericForeignKey
from django.utils import six
from django.core.serializers.json import Serializer as JSONSerializer
from django.core.serializers.python import Deserializer as \
PythonDeserializer, _get_model
from django.core.serializers.base import DeserializationError
import sys
class Serializer(JSONSerializer):
def get_dump_object(self, obj):
dumped_object = super(CustomJSONSerializer, self).get_dump_object(obj)
if self.use_natural_keys and hasattr(obj, 'natural_key'):
dumped_object['pk'] = obj.natural_key()
# Check if there are any generic fk's in this obj
# and add a natural key to it which will be deserialized by a matching Deserializer.
for virtual_field in obj._meta.virtual_fields:
if type(virtual_field) == GenericForeignKey:
content_object = getattr(obj, virtual_field.name)
dumped_object['fields'][virtual_field.name + '_natural_key'] = content_object.natural_key()
return dumped_object
def Deserializer(stream_or_string, **options):
"""
Deserialize a stream or string of JSON data.
"""
if not isinstance(stream_or_string, (bytes, six.string_types)):
stream_or_string = stream_or_string.read()
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode('utf-8')
try:
objects = json.loads(stream_or_string)
for obj in objects:
Model = _get_model(obj['model'])
if isinstance(obj['pk'], (tuple, list)):
o = Model.objects.get_by_natural_key(*obj['pk'])
obj['pk'] = o.pk
# If has generic fk's, find the generic object by natural key, and set it's
# pk according to it.
for virtual_field in Model._meta.virtual_fields:
if type(virtual_field) == GenericForeignKey:
natural_key_field_name = virtual_field.name + '_natural_key'
if natural_key_field_name in obj['fields']:
content_type = getattr(o, virtual_field.ct_field)
content_object_by_natural_key = content_type.model_class().\
objects.get_by_natural_key(obj['fields'][natural_key_field_name][0])
obj['fields'][virtual_field.fk_field] = content_object_by_natural_key.pk
for obj in PythonDeserializer(objects, **options):
yield obj
except GeneratorExit:
raise
except Exception as e:
# Map to deserializer error
six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2])