Django annotate on BooleanField

2019-06-17 08:20发布

问题:

I have the following models:

class Foo(models.Model):
    pass

class Bar(models.Model):
    foo = models.ForeignKey(Foo)
    is_successful = models.BooleanField()

I would like to get all foo objects with an annotation if all of the bar objects associated with foo object have is_successful as True

So far my queryset is:

foos = Foo.objects.all().annotate(all_successful=Min('bar__is_successful'))

The idea for the all_successful annotation is that if the minimum value of all is_successful rows is 1, then all of them must be True (assuming 0 is False and 1 is True). So knowing that I can use the queryset like so:

foo = foos[0]

if foo.all_successful == 1:
    print 'All bars are successful'
else:
    print 'Not all bars are successful'

This works great in sqlite however it fails in PostgreSQL because PostgreSQL can't execute MIN aggregate on a boolean column. I guess this works in sqlite because sqlite treats bools as integers hence it can execute the aggregate.

My question is how can I make this queryset work in PostgreSQL without converting my is_successful field to an IntegerField?

Thanx

回答1:

I know this is an old question, but I ran up against this recently. Django v1.8 now has built in support for case/when, so you can use the ORM instead of hacking around with custom SQL.

https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/#case

Foo.objects.annotate(
    all_successful=Case(
        When(bar__is_successful=False, then=False),
        When(bar__is_successful=True, then=True),
        default=False,
        output_field=BooleanField()
    ))

I haven't tried this out, but something similar worked for me on a recent project.



回答2:

FOR DJANGO <= 1.7: to get an annotation I think you can simply use Extra

foos = Foo.objects.extra(select={'all_successful': 'CASE WHEN COUNT(b.foo) > 0 THEN 0 ELSE 1 END FROM yourapp_bar as b WHERE b.is_successful = false and b.foo = yourapp_foo.id' })

if your system is running Django 1.8+ please follow Dav3xor answer.



回答3:

being inspired by https://docs.djangoproject.com/en/dev/topics/db/managers/ I suggest to use a custom manager for Bar class instead of annotation

class BarManager(models.Manager):
    def get_all_successful_foos_ids(self):
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("""
            SELECT foo, COUNT(*)
            FROM yourapp_bar
            GROUP BY 1
            WHERE is_successful = true""")  # <-- you have to write the correct table name here
        result_list = []
        for row in cursor.fetchall():
            if row[1] > 0:
                result_list.append(row[0])
        return result_list

class Bar(models.Model):
    foo = models.ForeignKey(Foo)
    is_successful = models.BooleanField()
    objects = BarManager()  # here I'm changing the default manager

then, in your code:

foos = foo.objects.filter(id__in=Bar.objects.get_all_successful_foos_ids())