Considérez 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 une requête d'événements avec le nombre total de participants:
events = Event.objects.all().annotate(participants=models.Count('participant'))
Comment annoter avec le nombre de participants filtrés par is_paid=True
?
Je dois interroger tous les événements quel que soit le nombre de participants, par exemple. Je n'ai pas besoin de filtrer par résultat annoté. S'il y a 0
participants, ça va, j'ai juste besoin de 0
en valeur annotée.
Le exemple tiré de la 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 une nouvelle fonctionnalité d'expressions conditionnelles , nous pouvons donc maintenant faire comme cette:
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 une nouvelle fonction Agrégation conditionnelle , voir la réponse acceptée ci-dessous.
Agrégation conditionnelle in Django 2.0 vous permet de réduire davantage la quantité de faff que cela a été dans le passé. Ceci utilisera également la logique filter
de Postgres , ce qui est un peu plus rapide qu’une somme (j’ai vu des chiffres comme 20 à 30% circuler).
Quoi qu'il en soit, dans votre cas, nous cherchons quelque chose d'aussi simple que:
from Django.db.models import Q, Count
events = Event.objects.annotate(
paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)
Il y a une section séparée dans la documentation sur filtrage sur les annotations . C'est la même chose que l'agrégation conditionnelle mais ressemble plus à mon exemple ci-dessus. Quoi qu’il en soit, c’est beaucoup plus sain que les sous-requêtes ignobles que je faisais auparavant.
Je viens de découvrir que Django 1.8 a une nouvelle fonctionnalité d’expressions conditionnelles , nous pouvons donc procéder comme suit:
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()
)))
[~ # ~] met à jour [~ # ~]
L'approche de sous-requête que je mentionne est maintenant supportée dans Django 1.11 via sous-requêtes-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 ceci à l'agrégation (somme + cas), car il devrait être plus rapide et plus facile à optimiser (avec une indexation correcte).
Pour les versions plus anciennes, il est possible d’atteindre le même résultat avec .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ère d'utiliser le .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éez 2 Event
s:
event1 = Event.objects.create(title='event1')
event2 = Event.objects.create(title='event2')
Ajoutez-y Participant
s:
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)]
Regroupez tous Participant
s par leur champ event
:
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 distincte est nécessaire:
Participant.objects.values('event').distinct()
> <QuerySet [{'event': 1}, {'event': 2}]>
Quoi .values
et .distinct
_ sont en train de créer deux compartiments de Participant
s regroupés par leur élément event
. Notez que ces compartiments contiennent Participant
.
Vous pouvez ensuite annoter ces compartiments car ils contiennent l'ensemble de Participant
d'origine. Ici, nous voulons compter le nombre de Participant
, ceci simplement en comptant les id
s des éléments de ces compartiments (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}]>
Enfin, vous ne voulez que Participant
avec un is_paid
étant True
, vous pouvez simplement ajouter un filtre devant l'expression précédente, ce qui donne 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
ultérieurement car vous ne disposez que du id
de la méthode ci-dessus.
Quel résultat je recherche:
En général, je devrais utiliser deux requêtes différentes:
Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()
Mais je veux les deux dans une requête. Par conséquent:
Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))
Résultat:
<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>