Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Treat NULL as '0' in Django model

I use the following bit of code in my Django app:

pictures = gallery.picture_set.annotate( score=models.Sum( 'picturevote__value' ) ).order_by( '-score' )

There is a table of galleries. In each of them are some pictures. When a user votes up or down a picture, a new row in 'picturevote' is inserted and connected to the picture. Then I can get the total score for the pictures. Now I want to order the pictures of one gallery by their score values. But due to the table joins there can be the value NULL for score when there were no votes at all. Nevertheless a score of 'NULL' shall be treated as '0'.

Any ideas?

edit: Okay, here some additional explanation: The problem is that the aggregation in the above example sets score to NULL. When I want to display the score I use something like this:

score = self.picturevote_set.aggregate( models.Sum( 'value' ) )[ 'value__sum' ] or 0

Then the aggregation leads either to NULL (if there are no picturevote rows) or a certain value. If it is NULL the or-expression converts it to a displayable integer value. But this solves just the display problems which are caused by the NULL value. When I want to sort the pictures by this score value as in the first code example all entries with NULL are put at the end of the ordered result set. First there are pictures with positive scores, then there are the pictures with negative values and THEN there are the pictures that were not voted up or down so far, because they have NULL as score.

My question is how this behaviour can be changed so that the order is correct.

like image 216
okoman Avatar asked Feb 16 '09 11:02

okoman


2 Answers

From Django 1.8, there is a Coalesce database function. Your query might look like this:

from django.db.models.functions import Coalesce    

score = self.picturevote_set.aggregate(Coalesce(models.Sum('value'), 0))
like image 108
Anton Belonovich Avatar answered Nov 08 '22 21:11

Anton Belonovich


Prior to the introduction of annotations, you might have used extra to do something like this, which I think should return 0 in cases where there are no votes (if it doesn't for any particular database implementation, you can at least directly insert the necessary COALESCE function call - COALESCE(SUM(value), 0) - using this method):

pictures = gallery.picture_set.extra(
    select={
        'score': 'SELECT SUM(value) FROM yourapp_picturevote WHERE yourapp_picturevote.picture_id = yourapp_picture.id',
    },
    order_by=['-score']
)

I can't see any built-in way to add your own SQL to the new annotation stuff (which I haven't personally used yet), but it looks like you should be able to create a new annotation like so:

from django.db.models import aggregates
from django.db.models.sql import aggregates as sql_aggregates

class SumWithDefault(aggregates.Aggregate):
    name = 'SumWithDefault'

class SQLSumWithDefault(sql_aggregates.Sum):
    sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)'

setattr(sql_aggregates, 'SumWithDefault', SQLSumWithDefault)

This looks rather ugly as you need to monkeypatch the new aggregate into django.db.models.sql.aggregates due to the way the SQL aggregate classes are looked up, but all we've done here is added a new aggregate which subclasses Sum, hardcoding a call to the COALESCE function and adding a placeholder for the default value, which you must supply as a keyword argument (in this very basic example implementation, at least).

This should let you do the following:

pictures = gallery.picture_set.annotate(score=SumWithDefault('picturevote__value', default=0).order_by('-score')
like image 26
Jonny Buchanan Avatar answered Nov 08 '22 21:11

Jonny Buchanan