61 votes

QuerySet et gestionnaire personnalisés sans rompre le DRY ?

J'essaie de trouver un moyen d'implémenter à la fois une fonction personnalisée QuerySet et un Manager sans se déshydrater. Voici ce que j'ai jusqu'à présent :

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

Cela fonctionne bien, jusqu'à ce que je fasse quelque chose comme ceci :

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

Cela casse rapidement tout, car le QuerySet n'a pas les mêmes méthodes que le Manager . J'ai essayé de créer un QuerySet et l'implémenter dans la classe MyInquiryManager mais je finis par reproduire toutes les définitions de mes méthodes.

J'ai aussi trouvé cet extrait ce qui fonctionne, mais j'ai besoin de passer l'argument supplémentaire à for_user donc il s'effondre parce qu'il s'appuie fortement sur la redéfinition get_query_set .

Existe-t-il un moyen d'y parvenir sans redéfinir toutes mes méthodes dans l'environnement de travail de l'entreprise ? QuerySet y el Manager sous-classes ?

1 votes

Avertissement : La réponse choisie par T.Stone entraîne une grave pénalité de performance (des temps de réponse de quelques millisecondes aux réponses de plusieurs secondes) lorsque les méthodes .defer ou .only sont utilisées. Par exemple, dans Django 1.3, une requête telle que : MyModel.objects.only('some_field').get(id=1) => retourne en 3.7ms mais, ajoutez le CustomManager comme décrit ci-dessus, et j'obtiens : MyModel.objects.only('some_field').get(id=1) => retour en ~ 357ms

0 votes

Quelqu'un d'autre a-t-il reproduit ce phénomène ? Qu'en est-il avec Django 1.4 ?

0 votes

D'accord. Mais pourquoi et comment cela se produit-il ? Les requêtes sont-elles différentes, ou avez-vous profilé cette opération, sans toucher réellement la base de données ?

52voto

T. Stone Points 10782

Django a changé ! Avant d'utiliser le code de cette réponse, qui a été écrit en 2009, assurez-vous de consulter les autres réponses et la documentation de Django pour voir s'il existe une solution plus appropriée.


La façon dont j'ai implémenté ceci est en ajoutant l'actuel get_active_for_account en tant que méthode d'une méthode personnalisée QuerySet . Ensuite, pour le faire fonctionner en dehors du gestionnaire, vous pouvez simplement piéger l'option __getattr__ et le renvoyer en conséquence

Pour que ce modèle puisse être réutilisé, j'ai extrait la partie Manager à un gestionnaire de modèle distinct :

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)

Une fois que vous avez obtenu cela, sur vos modèles, tout ce que vous devez faire est de définir un QuerySet en tant que classe interne personnalisée et définissez le gestionnaire comme étant votre gestionnaire personnalisé :

votre_app/modèles.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)

Avec ce modèle, n'importe lequel d'entre eux fera l'affaire :

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

UPD si vous l'utilisez avec un utilisateur personnalisé( AbstractUser ), vous devez modifier
de

class CustomQuerySetManager(models.Manager):

à

from django.contrib.auth.models import UserManager

class CustomQuerySetManager(UserManager):
    ***

0 votes

Pouvez-vous décider de ce qu'il faut faire avec stackoverflow.com/edit-suggestions/1216

0 votes

Pierre, vous allez devoir l'éditer vous-même, il n'y a aucun moyen de l'éditer après qu'elle ait été refusée.

4 votes

AVERTISSEMENT : J'ai essayé cette méthode et découvert qu'elle sévèrement ralentit les appels .defer et .only.

50voto

iMom0 Points 4855

La version 1.7 de Django a publié une nouveau et simple manière de créer un queryset et un gestionnaire de modèles combinés :

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

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

Voir Création d'un gestionnaire avec des méthodes QuerySet pour plus de détails.

5 votes

C'est la meilleure façon de le faire, mais il faudrait exemplifier la façon dont les for_user doit prendre un utilisateur et retourner self.[...] pour enchaîner plusieurs opérations.

0 votes

C'est drôle, ce n'est pas sa première réponse ici. Professionnellement, c'est la solution la plus propre.

12voto

vdboor Points 6259

Vous pouvez fournir les méthodes sur le gestionnaire et le queryset en utilisant un mixin.

Cela permet également d'éviter l'utilisation d'un __getattr__() approche.

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)

2voto

Roman Odaisky Points 798

Une version légèrement améliorée de l'approche de T. Stone :

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

Les décorateurs de classe rendent l'utilisation aussi simple que :

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

Mise à jour : support des classes de base non standard Manager et QuerySet, par exemple @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 votes

C'est assez impressionnant, ils ont un tel décorateur dans le django lui-même.

1 votes

Mon Django veut get_queryset à remplacer, et non get_query_set .

-1voto

Lakshman Prasad Points 24002

Ce qui suit fonctionne pour moi.

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)

C'est sur le gestionnaire par défaut ; donc j'avais l'habitude de faire quelque chose comme :

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

Mais il n'y a aucune raison pour que cela ne fonctionne pas pour un gestionnaire secondaire.

3 votes

Essayez de faire un filter puis en utilisant get_active_for_account . Cela fonctionne dans votre exemple, mais pas lorsque vous avez déjà utilisé un filter et travaillent ensuite avec un QuerySet qui était mon exemple.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X