2012-06-22 17 views
6

我有以下几点:是否可以在Django中使用GenericForeignKey的自然键?

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

我很好奇,你有没有发现这方面的任何解决方案?我做了一些搜索,但没有任何帮助。 – shanyu

+0

还没有,但我会更新此解决方案,如果我找到一个 – Riz

+0

你可以详细说明你的问题吗?一些例子。 – Rohan

回答

6

TL; DR - 目前有这样做的任何理智的方式,短创建自定义Serializer /Deserializer双。

与具有通用关系模型的问题是,Django不看target作为现场可言,只有target_content_typetarget_object_id,并尝试序列化和反序列化单独他们。

负责序列化和反序列化Django模型的类位于模块django.core.serializers.basedjango.core.serializers.python中。所有其他(xml,jsonyaml)扩展其中的任何一个(并且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字段已被反序列化),否则该信息不可用。如你所见,这不是一个不可能完成的任务 - 在普通关系中支持自然键 - 但是要完成这个任务,需要在序列化和反序列化代码中改变很多东西。有必要的话(如果有人认为直到任务)的步骤如下:

  • 创建Field延伸PositiveIntegerField自定义,使用方法的编码/解码的对象 - 调用参考模型natural_keyget_by_natural_key;
  • 覆盖串行器的handle_field以调用编码器(如果存在);
  • 实现一个自定义的反序列化器:1)在字段中强加一些顺序,确保内容类型在自然键之前反序列化; 2)调用解码器,不仅通过field_value,而且还参考解码的ContentType
0

我写了一个支持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]) 
相关问题