2010-01-29 14 views
43

Estoy tratando de encontrar una forma de implementar tanto un QuerySet personalizado como un Manager personalizado sin romper DRY. Esto es lo que tengo hasta ahora:QuerySet y Manager personalizados sin romper DRY?

class MyInquiryManager(models.Manager): 
    def for_user(self, user): 
     return self.get_query_set().filter(
        Q(assigned_to_user=user) | 
        Q(assigned_to_group__in=user.groups.all()) 
       ) 

class Inquiry(models.Model): 
    ts = models.DateTimeField(auto_now_add=True) 
    status = models.ForeignKey(InquiryStatus) 
    assigned_to_user = models.ForeignKey(User, blank=True, null=True) 
    assigned_to_group = models.ForeignKey(Group, blank=True, null=True) 
    objects = MyInquiryManager() 

Esto funciona bien, hasta que lo haga algo como esto:

inquiries = Inquiry.objects.filter(status=some_status) 
my_inquiry_count = inquiries.for_user(request.user).count() 

Esto rompe con prontitud todo porque el QuerySet no tiene los mismos métodos que el Manager . Intenté crear una clase personalizada QuerySet e implementarla en MyInquiryManager, pero termino replicando todas las definiciones de mis métodos.

También encontré this snippet que funciona, pero tengo que pasar en el argumento adicional para for_user por lo que se rompe porque se basa en gran medida en la redefinición de get_query_set.

¿Hay alguna manera de hacerlo sin redefinir todos mis métodos en las subclases QuerySet y Manager?

+0

Advertencia: La respuesta seleccionada por T.Stone resulta en una grave penalización del rendimiento (desde los tiempos de respuesta de milisegundos hasta las respuestas de varios segundos) cuando se utilizan los métodos .defer o .only. Por ejemplo, en Django 1.3 una consulta como: MyModel.objects.only ('some_field'). Get (id = 1) => regresa en 3.7ms pero agrega el CustomManager como se describió anteriormente y obtengo: MyModel.objects .only ('some_field'). get (id = 1) => regresa en ~ 357ms –

+0

¿Alguien más ha reproducido esto? ¿Qué tal con Django 1.4? – fletom

+0

Bien. Pero, ¿por qué y cómo sucede esto? ¿Las consultas son diferentes o describió esa operación sin llegar a la base de datos? –

Respuesta

45

Django ha cambiado! Antes de usar el código en esta respuesta, que fue escrita en 2009, asegúrese de revisar el resto de las respuestas y la documentación de Django para ver si hay una solución más adecuada.


La forma He implementado esto es mediante la adición de la actual get_active_for_account como un método de una costumbre QuerySet. Entonces, para hacer que funcione fuera de la gerente, simplemente puede atrapar el __getattr__ y devolverlo en consecuencia

Para hacer este patrón re-utilizable, he extraído los Manager bits a un administrador de modelo separado:

custom_queryset/models.py

from django.db import models 
from django.db.models.query import QuerySet 

class CustomQuerySetManager(models.Manager): 
    """A re-usable Manager to access a custom QuerySet""" 
    def __getattr__(self, attr, *args): 
     try: 
      return getattr(self.__class__, attr, *args) 
     except AttributeError: 
      # don't delegate internal methods to the queryset 
      if attr.startswith('__') and attr.endswith('__'): 
       raise 
      return getattr(self.get_query_set(), attr, *args) 

    def get_query_set(self): 
     return self.model.QuerySet(self.model, using=self._db) 

una vez que tenga que, en sus modelos todo lo que necesita hacer es definir un QuerySet como una clase personalizada interior y seleccionan el controlador a su gerente de encargo:

your_app/models.py

from custom_queryset.models import CustomQuerySetManager 
from django.db.models.query import QuerySet 

class Inquiry(models.Model): 
    objects = CustomQuerySetManager() 

    class QuerySet(QuerySet): 
     def active_for_account(self, account, *args, **kwargs): 
      return self.filter(account=account, deleted=False, *args, **kwargs) 

Con este patrón, cualquiera de estos funcionará:

>>> Inquiry.objects.active_for_account(user) 
>>> Inquiry.objects.all().active_for_account(user) 
>>> Inquiry.objects.filter(first_name='John').active_for_account(user) 
+1

Esto es tan genial – wakandan

+0

¿Puedes decidir qué se debe hacer con http://stackoverflow.com/edit-suggestions/1216 –

+0

@Sam Saffron Me gustaría tomar esa edición –

-1

Lo siguiente funciona para mí.

def get_active_for_account(self,account,*args,**kwargs): 
    """Returns a queryset that is 
    Not deleted 
    For the specified account 
    """ 
    return self.filter(account = account,deleted=False,*args,**kwargs) 

Esto está en el administrador predeterminado; así que solía hacer algo como:

Model.objects.get_active_for_account(account).filter() 

Pero no hay razón por la que no debería funcionar para un administrador secundario.

+3

Intenta hacer un 'filtro', luego usa' get_active_for_account'. Funciona en tu ejemplo, pero no una vez que ya has usado un 'filtro', y luego estás trabajando con un' QuerySet', que fue mi ejemplo. –

3

una versión ligeramente mejorada de T.El enfoque de piedra:

def objects_extra(mixin_class): 
    class MixinManager(models.Manager, mixin_class): 
     class MixinQuerySet(QuerySet, mixin_class): 
      pass 

     def get_query_set(self): 
      return self.MixinQuerySet(self.model, using=self._db) 

    return MixinManager() 

decoradores de clase hacen uso tan simple como:

class SomeModel(models.Model): 
    ... 
    @objects_extra 
    class objects: 
     def filter_by_something_complex(self, whatever parameters): 
      return self.extra(...) 
     ... 

actualización: soporte para el Administrador no estándar y las clases base de QuerySet, e. gramo. @objects_extra (django.contrib.gis.db.models.GeoManager, django.contrib.gis.db.models.query.GeoQuerySet):

def objects_extra(Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet): 
    def oe_inner(Mixin, Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet): 
     class MixinManager(Manager, Mixin): 
      class MixinQuerySet(QuerySet, Mixin): 
       pass 

      def get_query_set(self): 
       return self.MixinQuerySet(self.model, using=self._db) 

     return MixinManager() 

    if issubclass(Manager, django.db.models.Manager): 
     return lambda Mixin: oe_inner(Mixin, Manager, QuerySet) 
    else: 
     return oe_inner(Mixin=Manager) 
+0

Eso es bastante increíble, tienen un decorador como este en el propio django –

+1

Mi Django quiere que se anule 'get_queryset', no' get_query_set'. –

10

le puede proporcionar los métodos en el gerente y queryset utilizando un mixin . Ver la siguiente técnica:

http://hunterford.me/django-custom-model-manager-chaining/

Esto también evita el uso de un enfoque __getattr__().

from django.db.models.query import QuerySet 

class PostMixin(object): 
    def by_author(self, user): 
     return self.filter(user=user) 

    def published(self): 
     return self.filter(published__lte=datetime.now()) 

class PostQuerySet(QuerySet, PostMixin): 
    pass 

class PostManager(models.Manager, PostMixin): 
    def get_query_set(self): 
     return PostQuerySet(self.model, using=self._db) 
22

El Django 1.7 lanzado una manera nueva y sencilla para crear gerente queryset y el modelo combinado:

class InquiryQuerySet(models.QuerySet): 
    def for_user(self): 
     return self.filter(
      Q(assigned_to_user=user) | 
      Q(assigned_to_group__in=user.groups.all()) 
     ) 

class Inquiry(models.Model): 
    objects = InqueryQuerySet.as_manager() 

Ver Creating Manager with QuerySet methods para más detalles.

+0

jaja, ¡la respuesta china es lo mejor para mí! – tcpiper

+1

Esta es la mejor manera de hacerlo, pero tendría que ejemplificar cómo el método 'for_user' debe tomar a un usuario y devolver' self. [...] 'para encadenar varias operaciones. –

Cuestiones relacionadas