web-dev-qa-db-fra.com

QuerySet et Manager personnalisés sans casser DRY?

J'essaie de trouver un moyen d'implémenter à la fois un QuerySet personnalisé et un Manager personnalisé sans casser DRY. 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 ça:

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

Cela casse rapidement tout parce que le QuerySet n'a pas les mêmes méthodes que le Manager. J'ai essayé de créer une classe QuerySet personnalisée et de l'implémenter dans MyInquiryManager, mais je finis par répliquer toutes mes définitions de méthode.

J'ai également trouvé cet extrait qui fonctionne, mais je dois passer l'argument supplémentaire à for_user donc il tombe en panne car il dépend fortement de la redéfinition get_query_set.

Existe-t-il un moyen de le faire sans redéfinir toutes mes méthodes dans les sous-classes QuerySet et Manager?

52
Jack M.

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


J'ai implémenté ceci en ajoutant le get_active_for_account Comme méthode d'un QuerySet personnalisé. Ensuite, pour le faire fonctionner hors du gestionnaire, vous pouvez simplement piéger le __getattr__ Et le renvoyer en conséquence

Pour rendre ce modèle réutilisable, j'ai extrait les bits Manager dans 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 cela, sur vos modèles, tout ce que vous avez à faire est de définir un QuerySet en tant que classe interne personnalisée et de définir le gestionnaire sur votre gestionnaire personnalisé:

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)

Avec ce modèle, l'un d'eux fonctionnera:

>>> 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 changer
de

class CustomQuerySetManager(models.Manager):

à

from Django.contrib.auth.models import UserManager

class CustomQuerySetManager(UserManager):
    ***
50
T. Stone

La Django 1.7 a publié une nouvelle et simple façon de créer un ensemble de requêtes et un gestionnaire de modèle combinés:

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

Voir Création de Manager avec les méthodes QuerySet pour plus de détails.

32
iMom0

Vous pouvez fournir les méthodes sur le gestionnaire et l'ensemble de requêtes à l'aide d'un mixin. Voir la technique suivante:

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

Cela évite également l'utilisation d'une approche __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)
10
vdboor

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: prise en charge des classes de base Manager et QuerySet non standard, 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)
3
Roman Odaisky

Vous pouvez maintenant utiliser la méthode from_queryset () sur votre gestionnaire pour modifier son Queryset de base.

Cela vous permet de définir vos méthodes Queryset et vos méthodes de gestion une seule fois

des documents

Pour une utilisation avancée, vous souhaiterez peut-être à la fois un gestionnaire personnalisé et un ensemble de requêtes personnalisé. Vous pouvez le faire en appelant Manager.from_queryset () qui retourne une sous-classe de votre gestionnaire de base avec une copie des méthodes QuerySet personnalisées:

class InqueryQueryset(models.Queryset):
    def custom_method(self):
        """ available on all default querysets"""

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

MyInquiryManager = BaseInquiryManager.from_queryset(InquiryQueryset)

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