是否可以在Django中使用GenericForeignKey的自然键?

时间:2012-06-22 15:32:13

标签: django natural-key generic-foreign-key

我有以下内容:

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为这种关系发出一个自然键。这可能吗?如果没有,是否有一种替代策略不会将我绑定到目标的主键?

2 个答案:

答案 0 :(得分:7)

TL; DR - 目前没有明智的做法,因为没有创建自定义Serializer / Deserializer对。

具有通用关系的模型的问题是Django根本没有看到target作为字段,只有target_content_typetarget_object_id,并且它尝试序列化和反序列化它们单独

负责序列化和反序列化Django模型的类位于模块django.core.serializers.basedjango.core.serializers.python中。所有其他人(xmljsonyaml)都会延伸其中任何一个(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的外键处理正常,使用我们预期的自然键。但是PositiveIntegerFieldhandle_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_keyget_by_natural_key;
  • 覆盖序列化程序的handle_field以调用编码器(如果存在);
  • 实现一个自定义反序列化器:1)在字段中强加一些顺序,确保在自然键之前反序列化内容类型; 2)调用解码器,不仅传递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])