2011-02-03 14 views
30

He creado un modelo y estoy representando el modelo de modelo predeterminado/no modificado para él. Esto solo genera 64 consultas SQL porque tiene bastantes claves externas, y éstas a su vez tienen más claves foráneas.Django: ¿Selección de fuerza relacionada?

¿Es posible para forzarlo a siempre (por defecto) realizar una select_related cada vez que se devuelven uno de estos modelos?

Respuesta

41

Puede crear un administrador personalizado y simplemente anular get_queryset para que se aplique en todas partes. Por ejemplo:

class MyManager(models.Manager): 
    def get_queryset(self): 
     return super(MyManager, self).get_queryset().select_related('foo', 'bar') 

(Antes de Django 1.6, fue get_query_set).

+0

¿Cómo hacemos esto cuando usamos modelos.QuerySet? –

+0

Creo que necesita proporcionar más información, o algún código de ejemplo, sobre lo que quiere decir; si tiene un QuerySet, puede llamar directamente a 'select_related'. –

+0

Quería decir que si usa objects = MyQuerySet.as_manager() –

2

Cree un models.Manager personalizado y anule todos los métodos (filter,etc.) y anexe select_related a cada consulta. Luego configure este administrador como el atributo objects en el modelo.

Recomendaría revisar su código y agregar el select_related cuando sea necesario, ya que al seleccionar select_related en todo va a causar problemas graves de rendimiento más adelante (y no estaría del todo claro de dónde viene).

+0

Luego tengo que anular todos los campos de formulario en el formulario del modelo para poder establecer el conjunto de consulta manualmente ... – mpen

+0

Bueno, siempre puede ir con mi primera sugerencia. Solo te advertí sobre los posibles problemas de rendimiento perjudiciales con los que te puedes encontrar al final de la línea. Hay formas de hacerlo solo para 'ModelForm' sin anular todo, pero la respuesta dependerá realmente de lo que necesite hacer exactamente. Si quieres ayuda con eso, simplemente crea otra pregunta con más detalles. – sdolan

+0

Bueno, tengo un modelo de dirección que tiene enlaces a código postal, ciudad, provincia y país. Casi nunca se mostrará sin esos campos, así que pensé que podría incluirlo de manera predeterminada. – mpen

28

Aquí es también un truco divertido:

class DefaultSelectOrPrefetchManager(models.Manager): 
    def __init__(self, *args, **kwargs): 
     self._select_related = kwargs.pop('select_related', None) 
     self._prefetch_related = kwargs.pop('prefetch_related', None) 

     super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs) 

    def get_queryset(self, *args, **kwargs): 
     qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs) 

     if self._select_related: 
      qs = qs.select_related(*self._select_related) 
     if self._prefetch_related: 
      qs = qs.prefetch_related(*self._prefetch_related) 

     return qs 


class Sandwich(models.Model): 
    bread = models.ForeignKey(Bread) 
    extras = models.ManyToManyField(Extra) 

    # ... 

    objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',)) 

A continuación, se puede volver a utilizar el gestor fácilmente entre las clases del modelo. Como ejemplo de caso de uso, esto sería apropiado si tuviera un método __unicode__ en el modelo que representa una cadena que incluye información de un modelo relacionado (o cualquier otra cosa que signifique que un modelo relacionado fue casi siempre se requiere).

... y si realmente quieren ponerse raro, aquí hay una versión más generalizada. Le permite llamar a cualquier secuencia de métodos en el conjunto de consulta predeterminado con cualquier combinación de args o kwargs. Puede haber algunos errores en el código, pero entiendes la idea.

from django.db import models 


class MethodCalls(object): 
    """ 
    A mock object which logs chained method calls. 
    """ 
    def __init__(self): 
     self._calls = [] 

    def __getattr__(self, name): 
     c = Call(self, name) 
     self._calls.append(c) 
     return c 

    def __iter__(self): 
     for c in self._calls: 
      yield tuple(c) 


class Call(object): 
    """ 
    Used by `MethodCalls` objects internally to represent chained method calls. 
    """ 
    def __init__(self, calls_obj, method_name): 
     self._calls = calls_obj 
     self.method_name = method_name 

    def __call__(self, *method_args, **method_kwargs): 
     self.method_args = method_args 
     self.method_kwargs = method_kwargs 

     return self._calls 

    def __iter__(self): 
     yield self.method_name 
     yield self.method_args 
     yield self.method_kwargs 


class DefaultQuerysetMethodCallsManager(models.Manager): 
    """ 
    A model manager class which allows specification of a sequence of 
    method calls to be applied by default to base querysets. 
    `DefaultQuerysetMethodCallsManager` instances expose a property 
    `default_queryset_method_calls` to which chained method calls can be 
    applied to indicate which methods should be called on base querysets. 
    """ 
    def __init__(self, *args, **kwargs): 
     self.default_queryset_method_calls = MethodCalls() 

     super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs) 

    def get_queryset(self, *args, **kwargs): 
     qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs) 

     for method_name, method_args, method_kwargs in self.default_queryset_method_calls: 
      qs = getattr(qs, method_name)(*method_args, **method_kwargs) 

     return qs 


class Sandwich(models.Model): 
    bread = models.ForeignKey(Bread) 
    extras = models.ManyToManyField(Extra) 

    # Other field definitions... 

    objects = DefaultQuerysetMethodCallsManager() 
    objects.default_queryset_method_calls.filter(
     bread__type='wheat', 
    ).select_related(
     'bread', 
    ).prefetch_related(
     'extras', 
    ) 

El MethodCalls objeto python-maqueta inspirado es un intento de hacer que el API más natural. Algunos pueden encontrar eso un poco confuso. Si es así, puede sumar ese código para un __init__ arg o kwarg que solo acepta una tupla de información de llamada de método.

+0

Esto realmente me salvó el día, en lugar de escribir toneladas de 'ModelForm's, reemplazando 'ModelChoiceField's hasta que las vacas vuelvan a casa. (Espacialmente si el 'MyModel .__ unicode __()' usa lo que necesita ser 'select_related'. –

+0

Gran respuesta. Esto resuelve la mayoría de mis n + 1 problemas relacionados – Eldamir

+1

Nota:' get_query_set' fue [obsoleto en Django 1.6] (https : //docs.djangoproject.com/en/1.10/releases/1.6/#get-query-set-and-similar-methods-renamed-to-get-queryset). Debería reemplazarse por 'get_queryset' ahora. – keithb

Cuestiones relacionadas