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
Event
s:event1 = Event.objects.create(title='event1') event2 = Event.objects.create(title='event2')
-
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)]
- "1519270920 Groupe" tous
Participant
s par leurevent
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 deParticipant
groupé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 lesid
des é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
Participant
avec 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.