Comment filtrer les objets pour l'annotation des nombres dans Django?

Considérer simple Django modèles Event et Participant :

class Event(models.Model):
    title = models.CharField(max_length=100)

class Participant(models.Model):
    event = models.ForeignKey(Event, db_index=True)
    is_paid = models.BooleanField(default=False, db_index=True)

il est facile d'annoter la requête d'événements avec le nombre total de participants:

events = Event.objects.all().annotate(participants=models.Count('participant'))

comment annoter avec le compte des participants filtré par is_paid=True ?

j'ai besoin de requête tous les événements quel que soit le nombre de participants, par exemple, je n'ai pas besoin de filtrer par annoté résultat. S'il y a des participants 0 , c'est ok, j'ai juste besoin de 0 en valeur annotée.

le exemple de documentation ne fonctionne pas ici, car il exclut les objets de la requête au lieu de les annoter avec 0 .

mise à Jour. Django 1.8 a la nouvelle expression conditionnelle caractéristique , donc maintenant nous pouvons faire comme ceci:

events = Event.objects.all().annotate(paid_participants=models.Sum(
    models.Case(
        models.When(participant__is_paid=True, then=1),
        default=0,
        output_field=models.IntegerField()
    )))

mise à jour 2. Django 2.0 a le nouveau agrégation conditionnelle caractéristique, voir la réponse acceptée ci-dessous.

67
demandé sur rudyryk 2015-06-10 12:22:55

4 réponses

l'agrégation conditionnelle dans Django 2.0 vous permet de réduire davantage la quantité de faff cela a été dans le passé. Cela utilisera également la logique filter de Postgres, qui est un peu plus rapide qu'un cas de somme (j'ai vu des nombres comme 20-30% autour de).

quoi qu'il en soit, dans votre cas, nous regardons quelque chose d'aussi simple que:

events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)

il y a une section séparée dans le docs sur filtrant sur annotations . C'est la même chose conditionnelle à l'agrégation, mais plus comme mon exemple ci-dessus. De toute façon, c'est plus sain que les sous-séries que je faisais avant.

26
répondu Oli 2018-02-06 21:07:44

vient de découvrir que Django 1.8 a une nouvelle expression conditionnelle caractéristique , donc maintenant nous pouvons faire comme ceci:

events = Event.objects.all().annotate(paid_participants=models.Sum(
    models.Case(
        models.When(participant__is_paid=True, then=1),
        default=0, output_field=models.IntegerField()
    )))
79
répondu rudyryk 2016-10-17 09:44:40

mise à JOUR

l'approche de sous-requête que je mentionne est maintenant prise en charge dans Django 1.11 via subquery-expressions .

Event.objects.annotate(
    num_paid_participants=Subquery(
        Participant.objects.filter(
            is_paid=True,
            event=OuterRef('pk')
        ).values('event')
        .annotate(cnt=Count('pk'))
        .values('cnt'),
        output_field=models.IntegerField()
    )
)

je préfère cela à l'agrégation (somme+cas) , parce qu'il devrait être plus rapide et plus facile d'être optimisé (avec l'indexation appropriée) .

pour les versions plus anciennes, la même chose peut être réalisée en utilisant .extra

Event.objects.extra(select={'num_paid_participants': "\
    SELECT COUNT(*) \
    FROM `myapp_participant` \
    WHERE `myapp_participant`.`is_paid` = 1 AND \
            `myapp_participant`.`event_id` = `myapp_event`.`id`"
})
33
répondu Todor 2017-07-03 02:55:51

je suggérerais d'utiliser la .values méthode de votre Participant queryset à la place.

en bref, ce que vous voulez faire est donné par:

Participant.objects\
    .filter(is_paid=True)\
    .values('event')\
    .distinct()\
    .annotate(models.Count('id'))

un exemple complet est le suivant:

  1. Créer 2 Event s:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    
  2. ajouter Participant s à eux:

    part1l = [Participant.objects.create(event=event1, is_paid=((_%2) == 0))\
              for _ in range(10)]
    part2l = [Participant.objects.create(event=event2, is_paid=((_%2) == 0))\
              for _ in range(50)]
    
  3. "1519270920 Groupe" tous Participant s par leur event domaine:

    Participant.objects.values('event')
    > <QuerySet [{'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, '...(remaining elements truncated)...']>
    

    ici distinct est nécessaire:

    Participant.objects.values('event').distinct()
    > <QuerySet [{'event': 1}, {'event': 2}]>
    

    ce que .values et .distinct font ici, c'est qu'ils créent deux seaux de Participant groupés par leur élément event . Notez que ces seaux contiennent Participant .

  4. vous pouvez alors annoter ces les seaux contiennent l'ensemble d'origine Participant . Ici, nous voulons compter le nombre de Participant , cela se fait simplement en comptant les id des éléments dans ces seaux (puisque ceux-ci sont Participant ):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
    
  5. Enfin, vous voulez seulement Participant avec un is_paid être True , vous pouvez simplement ajouter un filtre devant l'expression précédente, et ce rendement de l'expression ci-dessus:

    Participant.objects\
        .filter(is_paid=True)\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]>
    

le seul inconvénient est que vous devez récupérer le Event après que vous avez seulement le id de la méthode ci-dessus.

3
répondu Raffi 2017-11-26 20:37:43