Reputation: 3567
I have following application:
from django.db import models
class Worker(models.Model):
name = models.CharField(max_length=60)
def __str__(self):
return self.name
class Job(models.Model):
worker = models.ForeignKey(Worker)
is_completed = models.BooleanField()
I want to annotate Workers query with count of completed jobs.
I'll try to do it with following script:
from myapp.models import Worker, Job
from django.db.models import Count
w = Worker.objects.create(name='Worker1')
Job.objects.create(worker=w, is_completed=False)
Job.objects.create(worker=w, is_completed=False)
Job.objects.create(worker=w, is_completed=True)
Job.objects.create(worker=w, is_completed=True)
workers = Worker.objects.all().annotate(num_jobs=Count('job'))
workers[0].num_jobs
# >>> 4
workers = Worker.objects.all().exclude(job__is_completed=False).annotate(num_jobs=Count('job'))
# >>> []
Result of the last query is empty. How to exclude elements from reverse relation?
Django 1.8, python 2.7
UPD. I would like to have all workers in queryset, even those, who has a zero jobs
Upvotes: 7
Views: 7813
Reputation: 5789
There is 2 options here. First one is straight Q
filter on Count
:
from myapp.models import Worker
from django.db.models import Count, Q
workers = Worker.objects.annotate(
completed_jobs_count=Count("job", filter=Q(job__is_completed=True))
)
And the second is excluding Q
filter on Count
(sometimes it's needed because Count
doesn't has straight exclude
option):
from myapp.models import Worker
from django.db.models import Count, Q
workers = Worker.objects.annotate(
completed_jobs_count=Count("job", filter=~Q(job__is_completed=False))
)
Upvotes: 3
Reputation: 23871
Update
If you want a count of completed job for every worker, than we could use subquery through .extra()
:
Worker.objects.extra(select={'jobs_done':
'SELECT COUNT(*) FROM {job_tbl} WHERE worker_id = {worker_tbl}.id AND is_completed'
.format(worker_tbl=Worker._meta.db_table, job_tbl=Job._meta.db_table)})
It's great that Django now supports the Python mapping of conditional expression SUM(CASE WHEN is_completed = True THEN 1 ELSE 0 END)
in syntax described in @BogdiG's answer.
Removed things about filter/exclude
Upvotes: 1
Reputation: 1297
update: ok I played a bit with this to generate the solution and I think I got it using Conditional Expressions:
Conditional expressions let you use if ... elif ... else logic within filters, annotations, aggregations, and updates. A conditional expression evaluates a series of conditions for each row of a table and returns the matching result expression.
Note: Conditional Expressions (such as Case
and When
) are new in Django 1.8, as pointed out by @Pynchia
from django.db.models import IntegerField, Sum, Case, When
workers = Worker.objects.annotate(
num_jobs=Sum(
Case(
When(job__is_completed=True, then=1),
default=0,
output_field=IntegerField()
)
)
)
now, each worker will have a num_jobs which will be an Integer that shows how many completed jobs that worker has :)
Upvotes: 13
Reputation: 11590
The following
workers = Worker.objects.filter(job__is_completed=True)).annotate(num_jobs=Count('job__is_completed'))
annotates those workers who have at least one job completed. Those whth a count of zero job completed are not included in the result queryset.
In case you want ALL workers to appear in the result queryset, it would be great if we could write
workers = Worker.objects.annotate(num_jobs=CountIf('job__is_completed', job__is_completed=True))
but unfortunately, we cannot. So I am out of my depth here and I believe my answer is partial. I welcome the intervention of somebody more competent then I am who could shed some light on the matter.
For reference, see this Django proposed feature (closed)
and this SO QA
UPDATE: Django 1.8 introduced conditional expressions. @BogdiG's answer uses such new operators to solve the problem. Kudos!
Upvotes: 2