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.
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.
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()
)))
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`"
})
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:
-
Créer 2
Events:event1 = Event.objects.create(title='event1') event2 = Event.objects.create(title='event2') -
ajouter
Participants à 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)] - "1519270920 Groupe" tous
Participants par leureventdomaine: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
.valueset.distinctfont ici, c'est qu'ils créent deux seaux deParticipantgroupés par leur élémentevent. Notez que ces seaux contiennentParticipant. -
vous pouvez alors annoter ces les seaux contiennent l'ensemble d'origine
Participant. Ici, nous voulons compter le nombre deParticipant, cela se fait simplement en comptant lesiddes éléments dans ces seaux (puisque ceux-ci sontParticipant):Participant.objects\ .values('event')\ .distinct()\ .annotate(models.Count('id')) > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]> -
Enfin, vous voulez seulement
Participantavec unis_paidêtreTrue, 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.