11

我有以下内容:

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

4

3 回答 3

9

TL;DR - 目前没有健全的方法这样做,除非创建一个自定义Serializer/Deserializer对。

具有通用关系的模型的问题在于 Django 根本不将target其视为一个字段,而只是target_content_typeand target_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处理得很好,正如我们预期的那样使用自然键。但是由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和定义自定义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但是要反序列化一个自然键,首先我们需要知道该键属于哪个模型,并且除非我们获得对该对象的引用(并且该字段已经被反序列化),否则该信息不可用。

正如您所看到的,这不是一项不可能完成的任务——支持泛型关系中的自然键——但要实现这一点,需要在序列化和反序列化代码中进行很多更改。必要的步骤,然后(如果有人觉得可以完成任务)是:

  • 创建一个自定义Field扩展PositiveIntegerField,使用编码/解码对象的方法 - 调用引用的模型natural_keyget_by_natural_key
  • 覆盖序列化handle_field器以调用编码器(如果存在);
  • 实现一个自定义的反序列化器:1)在字段中强加一些顺序,确保内容类型在自然键之前反序列化;2) 调用解码器,不仅传递 ,field_value而且传递对解码的引用ContentType
于 2013-02-15T16:59:28.037 回答
3

我编写了一个支持 GenericFK 的自定义序列化器和反序列化器。简单地检查了一下,它似乎完成了这项工作。

这就是我想出的:

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])
于 2016-04-04T17:08:31.360 回答
0

我更新了 Django 2.2 及更高版本的 OmriToptix答案

在 Django 2.0 中:

Model._meta.virtual_fields 属性被移除。

所以,新的序列化器和反序列化器:

import json

from django.contrib.contenttypes.fields 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(JSONSerializer, self).get_dump_object(obj)

        if hasattr(obj, 'natural_key'):
            dumped_object['pk'] = obj.natural_key()
            for field in obj._meta.get_fields():
                if type(field) == GenericForeignKey:
                    content_object = getattr(obj, field.name)
                    dumped_object['fields'][field.name + '_natural_key'] = content_object.natural_key()
        return dumped_object


def Deserializer(stream_or_string, **options):
    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
                for field in Model._meta.get_fields():
                    if type(field) == GenericForeignKey:
                        natural_key_field_name = field.name + '_natural_key'
                        if natural_key_field_name in obj['fields']:
                            content_type = getattr(o, field.ct_field)
                            content_object_by_natural_key = content_type.model_class().\
                            objects.get_by_natural_key(*obj['fields'][natural_key_field_name])
                            obj['fields'][field.fk_field] = content_object_by_natural_key.pk
                            del obj['fields'][natural_key_field_name]

        for obj in PythonDeserializer(objects, **options):
            yield obj
    except GeneratorExit:
        raise
    except Exception as e:
        six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2])

然后,在您的 settings.py 中,设置此配置:

    SERIALIZATION_MODULES = {
    "json": "path.to.serializer_file"
  }

现在,您可以使用:

python3 manage.py dumpdata --natural-foreign --natural-primary > dump.json

其他方式,如果您需要转储一些数据(过滤查询集),您可以从代码中进行:

from path.to.serializers import Serializer, Deserializer

# Serialize
registers = YourModel.objects.filter(some_attribute=some_value)
dump = Serializer().serialize(registers, use_natural_foreign_keys=True, use_natural_primary_keys=True)

# Deserialize
for deserialized_object in Deserializer(dump, use_natural_foreign_keys=True, use_natural_primary_keys=True):
    print(deserialized_object.object)  # See here https://docs.djangoproject.com/en/2.2/topics/serialization/
于 2022-01-13T16:57:01.467 回答