2012-06-22 7 views
6

Ho il seguente:È possibile utilizzare una chiave naturale per un GenericForeignKey in Django?

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') 

Vorrei DumpData --natural per emettere una chiave naturale per questa relazione. È possibile? In caso contrario, esiste una strategia alternativa che non mi legherà alla chiave primaria dell'obiettivo?

+0

Sono curioso, hai trovato una soluzione per questo? Ho fatto qualche ricerca ma non è venuto fuori nulla di utile. – shanyu

+0

non ancora ma aggiornerò questo con una soluzione se trovo uno – Riz

+0

Potresti elaborare la tua domanda? Qualche esempio – Rohan

risposta

6

TL; DR - Al momento non esiste un modo sano di mente di farlo, a corto di creare un custom Serializer/Deserializer coppia.

Il problema con i modelli che hanno rapporti generici è che Django non vede target come un campo a tutti, solo target_content_type e target_object_id, e cerca di serializzare e deserializzare singolarmente.

Le classi responsabili della serializzazione e deserializzazione dei modelli Django sono nei moduli django.core.serializers.base e django.core.serializers.python. Tutti gli altri (xml, json e yaml) estendono uno di essi (e python estende base). La serializzazione campo è fatto in questo modo (linee irrilevanti ommited):

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) 

Ecco la prima complicazione: la chiave esterna per ContentType è gestito bene, con le chiavi naturali come ci aspettavamo. Ma il PositiveIntegerField è gestita da handle_field, implementata in questo modo:

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) 

cioè l'unica possibilità di personalizzazione here (sottoclassi PositiveIntegerField e definente un custom value_to_string) non ha alcun effetto, dal momento che il serializzatore non chiamare. Cambiando il tipo di dati di target_object_id in qualcosa di diverso da un intero probabilmente si romperanno molte altre cose, quindi non è un'opzione.

Abbiamo potrebbe definire il nostro personalizzato handle_field per emettere chiavi naturale in questo caso, ma poi arriva la seconda complicazione: la deserializzazione è fatto in questo modo:

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) 

Anche se abbiamo personalizzato il metodo to_python, si agisce solo sul field_value, fuori dal contesto dell'oggetto. Non è un problema quando si usano gli interi, poiché sarà interpretato come la chiave primaria del modello indipendentemente dal modello che è. Ma per deserializzare una chiave naturale, in primo luogo abbiamo bisogno di sapere a quale modello appartiene quella chiave, e quell'informazione non è disponibile a meno che non abbiamo ottenuto un riferimento all'oggetto (e il campo target_content_type era già stato deserializzato).

Come si può vedere, non è un compito impossibile - supportare le chiavi naturali in relazioni generiche - ma per realizzare che molte cose dovrebbero essere cambiate nel codice di serializzazione e deserializzazione.I passi necessari, quindi (se qualcuno si sente all'altezza del compito) sono:

  • Creare un costume Field estende PositiveIntegerField, con i metodi di codifica/decodifica di un oggetto - chiamando i modelli di riferimento natural_key e get_by_natural_key;
  • Sovrascrivere il numero handle_field del serializzatore per chiamare l'encoder se presente;
  • Implementare un deserializzatore personalizzato che: 1) impone un ordine nei campi, assicurando che il tipo di contenuto sia deserializzato prima della chiave naturale; 2) chiama il decoder, passando non solo il field_value ma anche un riferimento al decodificato ContentType.
0

Ho scritto un serializzatore e un deserializzatore personalizzati che supportano GenericFK. Controllato brevemente e sembra fare il lavoro.

Questo è ciò che mi si avvicinò con:

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]) 
Problemi correlati