Writing readable queries in Django

Posted on Sun 18 December 2016 in Django

For my first technical post, let's talk about queries in Django.

In a view, typically you'd want to make some queries to add to the template context. For example:

def books_list(request):
    books = Book.objects.all()
    render(request, 'books/book_list.html', {'books': books})

Sometimes, these queries can get a bit more complicated.

For example, say, for whatever reason, you want to display a list of people whose age is an even number.

(The example doesn't really matter to the point of this post. Basically, what I was getting at is, sometimes we need to do complicated queries.)

# in models.py
class Person(models.Model):
    age = models.IntegerField()
    name = models.CharField(max_length=256)

# in views.py
def people_with_even_numbered_ages(request):
    even_aged_people = Person.objects.annotate(modulo_two=F('age') % 2).filter(modulo_two=0)
    render(request, 'person/even_aged_people.html', {'even_aged_people': even_aged_people})

Unfortunately, that isn't very readable. The intent of that query isn't immediately clear.

We could add a comment, but as a general rule, when you need a comment, it's an indicator that whatever you're doing is difficult to understand/explain.

When you have a complicated query, I would recommend using QuerySet methods.

QuerySet methods

Did you know you can add methods to Managers and QuerySets? See Django's documentation on Managers and QuerySets.

A nice thing you can do is to override a QuerySet and have it act as a Manager, so that you don't write the same logic in two classes:

# in models.py
class PersonQuerySet(models.QuerySet):
    def with_even_numbered_ages(self):
        """Filters people with even-numbered ages"""
        return self.annotate(modulo_two=F('age') % 2).filter(modulo_two=0)


class Person(models.Model):
    objects = PersonQuerySet.as_manager()
    age = models.IntegerField()
    name = models.CharField(max_length=256)


# in views.py
def people_with_even_numbered_ages(request):
    even_aged_people = Person.objects.with_even_numbered_ages()
    render(request, 'person/even_aged_people.html', {'even_aged_people': even_aged_people})

The method name with_even_numbered_ages() tells you exactly what the query is doing, which is what we want. You could change the method name if you don't like the constant use of with_ in the method names.

Another benefit of doing it this way is that you can chain QuerySet methods:

class PersonQuerySet(models.QuerySet):
    def with_even_numbered_ages(self):
        """Filters people with even-numbered ages"""
        return self.annotate(modulo_two=F('age') % 2).filter(modulo_two=0)

    def with_names_containing(self, name=None):
        """Filters people with names that contain a given string"""
        return self.filter(name__icontains=name)


def some_view(request):
    people = PersonQuerySet.objects.with_names_containing("Barry").with_even_numbered_ages()
    render(request, 'person/some_people.html', {'people': people})

I think that's a nice way to move filtering logic out of views.

Subscribe to receive free Python tips

Sometimes I keep my best content for my email subscribers. Subscribe now so that you don't miss out! You can unsubscribe at any time, and I will never spam you.
* indicates required

Comments