2010-01-29 13 views
43

Sto cercando di trovare un modo per implementare sia una QuerySet personalizzata che una personalizzata Manager senza interrompere DRY. Questo è quello che ho finora:Custom QuerySet e Manager senza interrompere 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() 

Questo funziona bene, fino a quando faccio qualcosa di simile:

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

Questo rompe tempestivamente tutto perché il QuerySet non hanno gli stessi metodi come la Manager . Ho provato a creare una classe personalizzata QuerySet e ad implementarla in MyInquiryManager, ma alla fine eseguo la replica di tutte le definizioni dei miei metodi.

Ho anche trovato this snippet che funziona, ma ho bisogno di passare l'argomento in più per for_user così si rompe perché si basa pesantemente sulla ridefinizione get_query_set.

C'è un modo per farlo senza ridefinire tutti i miei metodi nelle sottoclassi e Manager?

+0

Avvertenza: la risposta selezionata di T.Stone determina una severa penalizzazione delle prestazioni (dai tempi di risposta del millisecondo alle risposte di più secondi) quando vengono utilizzati metodi .defer o .only. Ad esempio, in Django 1.3 una query come: MyModel.objects.only ('some_field'). Get (id = 1) => ritorna in 3.7ms ma, aggiungi il CustomManager come descritto sopra, e ottengo: MyModel.objects .only ('some_field'). get (id = 1) => restituisce in ~ 357ms –

+0

Qualcun altro ha riprodotto questo? Che ne dici di Django 1.4? – fletom

+0

Ok. Ma perché e come succede? Le query sono diverse o l'utente ha eseguito il profilo dell'operazione, senza effettivamente raggiungere il database? –

risposta

45

Django è cambiato! Prima di usare il codice in questa risposta, che è stato scritto nel 2009, assicurati di controllare il resto delle risposte e la documentazione di Django per vedere se esiste una soluzione più appropriata.


Il modo in cui ho implementato questo è aggiungendo l'attuale get_active_for_account come metodo di un costume QuerySet. Poi, per farlo funzionare fuori il manager, si può semplicemente intrappolare il __getattr__ e restituire di conseguenza

Per rendere questo modello riutilizzabile, ho estratto i Manager bit per un manager modello separato:

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 volta che hai ottenuto che, sui modelli tutto quello che dovete fare è definire un QuerySet come classe interna personalizzata e impostare il manager al gestore personalizzato:

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 questo modello, uno di questi funzionerà:

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

Questo è così bello – wakandan

+0

puoi decidere cosa dovrebbe essere fatto con http://stackoverflow.com/edit-suggestions/1216 –

+0

@Sam Saffron Vorrei prendere quella modifica –

-1

I seguenti lavori per me.

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) 

Si tratta del gestore predefinito; quindi ero solito fare qualcosa del tipo:

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

Ma non c'è motivo per cui non dovrebbe funzionare per un manager secondario.

+3

Prova a fare un 'filter', quindi usando' get_active_for_account'. Funziona nel tuo esempio, ma non una volta che hai già usato un 'filtro', e quindi stai lavorando con un' QuerySet', che era il mio esempio. –

3

leggermente migliorata versione di T.L'approccio di pietra:

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

decoratori di classe a rendere l'utilizzo il più semplice:

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

Aggiornamento: il supporto per non standard Manager e classi di base QuerySet, e. g. @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

È meraviglioso, hanno un decoratore del django stesso –

+1

My Django vuole che 'get_queryset' sia sovrascritto, non' get_query_set'. –

10

È in grado di fornire i metodi per il manager e set di query usando un mixin . Vedere la seguente tecnica:

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

Questo evita anche l'uso di un approccio __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

Il Django 1.7 rilasciato un nuovo e semplice modoper creare combinato set di query e Model Manager:

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

Vedere Creating Manager with QuerySet methods per maggiori dettagli.

+0

haha, la risposta cinese è la migliore per me! – tcpiper

+1

Questo è il modo migliore per farlo, ma dovrebbe essere esemplificativo di come il metodo 'for_user' dovrebbe prendere un utente e restituire' self. [...] 'per concatenare più operazioni. –