2012-09-17 29 views
12

Supongamos que tengo un modelo Box con un GenericForeignKey que apunta a o bien un Apple instancia o una instancia de Chocolate. Apple y Chocolate, a su vez, tienen ForeignKeys a Farm y Factory, respectivamente. Quiero mostrar una lista de Box es, para lo cual necesito acceder a Farm y Factory. ¿Cómo hago esto en la menor cantidad posible de consultas DB?django: objetos relacionados de recuperación previa de un GenericForeignKey

Mínimo ejemplo ilustrativo:

class Farm(Model): 
    ... 

class Apple(Model): 
    farm = ForeignKey(Farm) 
    ... 

class Factory(Model): 
    ... 

class Chocolate(Model): 
    factory = ForeignKey(Factory) 
    ... 

class Box(Model) 
    content_type = ForeignKey(ContentType) 
    object_id = PositiveIntegerField() 
    content_object = GenericForeignKey('content_type', 'object_id') 
    ... 

    def __unicode__(self): 
     if self.content_type == ContentType.objects.get_for_model(Apple): 
      apple = self.content_object 
      return "Apple {} from Farm {}".format(apple, apple.farm) 
     elif self.content_type == ContentType.objects.get_for_model(Chocolate): 
      chocolate = self.content_object 
      return "Chocolate {} from Factory {}".format(chocolate, chocolate.factory) 

Estas son algunas cosas que he probado. En todos estos ejemplos, N es el número de cuadros. El recuento de consultas supone que ContentType s para Apple y Chocolate ya se han almacenado en caché, por lo que las llamadas get_for_model() no llegan a la base de datos.

1) Naive:

print [box for box in Box.objects.all()]

Esto (traiga cajas) + N (fetch Apple o chocolate para cada caja) + N (fetch Granja para cada Apple y Fábrica para cada chocolate) consultas.

2) select_related no ayuda aquí, porque Box.content_object es un GenericForeignKey.

3) A partir de django 1.4, prefetch_related puede obtener GenericForeignKey s.

print [box for box in Box.objects.prefetch_related('content_object').all()]

Esto (traiga cajas) + (ir a buscar las manzanas y chocolates para todas las cajas) + N (fetch Granja para cada manzana y la fábrica de chocolate) para cada pregunta.

4) Al parecer prefetch_related no es lo suficientemente inteligente como para seguir ForeignKeys de GenericForeignKeys. Si trato:

print [box for box in Box.objects.prefetch_related( 'content_object__farm', 'content_object__factory').all()]

que con razón se queja de que Chocolate objetos no tienen un campo farm, y viceversa.

5) que podía hacer:

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes_with_apples = Box.objects.filter(content_type=apple_ctype).prefetch_related('content_object__farm') 
boxes_with_chocolates = Box.objects.filter(content_type=chocolate_ctype).prefetch_related('content_object__factory') 

Esto (fetch Cajas) + (podido recuperar Manzanas y Chocolates para todas las cajas) + (FETCH de campo en todas las manzanas y Fábricas para todos los chocolates) consultas. La desventaja es que tengo que fusionar y ordenar los dos conjuntos de consultas (boxes_with_apples, boxes_with_chocolates) manualmente. En mi aplicación real, estoy mostrando estas cajas en un ModelAdmin paginado. No es obvio cómo integrar esta solución allí. ¿Tal vez podría escribir un Paginator personalizado para hacer este almacenamiento en caché de forma transparente?

6) Podría improvisar algo basado en this que también hace O (1) consultas. Pero prefiero no meterme con los internos (_content_object_cache) si puedo evitarlo.

En resumen: La impresión de un cuadro requiere acceso a las teclas externas de un GenericForeignKey. ¿Cómo puedo imprimir N Boxes en O (1) consultas? ¿Es (5) lo mejor que puedo hacer, o hay una solución más simple?

Puntos de bonificación: ¿Cómo podría refaccionar este esquema de base de datos para facilitar tales consultas?

+0

Si cambia el nombre 'farm' /' factory' a un nombre común, como 'creator', se prefetch_related trabajo? – Igor

+0

De hecho, 'prefetch_related ('content_object__creator')' funciona después de su cambio de nombre sugerido. Desafortunadamente, el cambio de nombre puede o no tener sentido según los modelos reales que tenga en lugar de Apple/Farm y Chocolate/Factory. – cberzan

Respuesta

8

Puede implementar manualmente algo como prefetch_selected y usar el método select_related de Django, que hará que join en la consulta de la base de datos.

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes = Box.objects.all() 
content_objects = {} 
# apples 
content_objects[apple_ctype.id] = Apple.objects.select_related(
    'farm').in_bulk(
     [b.object_id for b in boxes if b.content_type == apple_ctype] 
    ) 
# chocolates 
content_objects[chocolate_ctype.id] = Chocolate.objects.select_related(
    'factory').in_bulk(
     [b.object_id for b in boxes if b.content_type == chocolate_ctype] 
    ) 

Esto debería hacer sólo 3 consultas (get_for_model consultas se omiten). El método in_bulk le devuelve un dict en formato {id: modelo}. Así que para obtener su content_object necesita un código como:

content_obj = content_objects[box.content_type_id][box.object_id] 

Sin embargo no estoy seguro de si este código será más rápida, entonces su O (5) solución, ya que requiere iteración adicional sobre las cajas y también queryset genera una consulta con la declaración WHERE id IN (...)

Pero si clasifica las casillas solo por campos del modelo Box puede completar el content_objects dict después de la paginación. Pero hay que pasar a content_objects__unicode__ alguna manera

¿Cómo refactorizar este esquema de base para hacer este tipo de consultas más fácil?

Tenemos una estructura similar. Almacenamos content_object en Box, pero en lugar de object_id y content_object utilizamos ForeignKey(Box) en Apple y Chocolate. En Box tenemos un método get_object para devolver el modelo Apple o Chocolate. En este caso, podemos usar select_related, pero en la mayoría de nuestros casos de uso filtramos Boxes por content_type. Entonces tenemos los mismos problemas que su 5ta opción. Pero comenzamos el proyecto en Django 1.2 cuando no había prefetch_selected.

Si cambia el nombre de granja/fábrica por un nombre común, como creador, ¿se realizará una restauración previa?

Acerca de su opción

me puede decir nada en contra de llenado _content_object_cache. Si no les gusta tratar con las partes internas se puede llenar propiedad personalizada y luego usar

apple = getattr(self, 'my_custop_prop', None) 
if apple is None: 
    apple = self.content_object 
+0

Acabo de notar que mi respuesta está muy cerca de su * opción 6 * pero con menos automatización. Nunca antes había leído ese artículo.Además, eso no se ve como O (1), es más bien O (2 + number_of_unique_ctypes) – Igor

Cuestiones relacionadas