简体   繁体   中英

Django exclude from annotation count

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

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 :)

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!

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))
)

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM