DEV Community

Cover image for Advanced Django queries
Juhana Jauhiainen
Juhana Jauhiainen

Posted on • Originally published at juhanajauhiainen.com

Advanced Django queries

Django Framework comes with a powerful ORM and query capabilities built-in. If you're only familiar with the basics of Djangos Query API, this article will introduce some more advanced queries and methods you can use.

In the examples, I'll be using the following data models.

class Author(models.Model):
    nickname = models.CharField(max_length=20, null=True, blank=True)
    firstname = models.CharField(max_length=20)
    lastname = models.CharField(max_length=40)
    birth_date = models.DateField()


class Book(models.Model):
    author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE)
    title = models.CharField(unique=True, max_length=100)
    category = models.CharField(max_length=50)
    published = models.DateField()
    price = models.DecimalField(decimal_places=2, max_digits=6)
    rating = models.IntegerField()
Enter fullscreen mode Exit fullscreen mode

Filtering

Basic filtering in Django can be done using something called field lookups with the filter method. Field lookups consist of the field and a suffix defining the lookup type. If no suffix is defined, the default behavior is equivalent to using the exact suffix.

books = Book.objects.filter(title="Crime and Punishment")
books = Book.objects.filter(title__startswith="Crime")
Enter fullscreen mode Exit fullscreen mode

Querying by using field lookups is already very powerful. We can filter with different suffixes like gte, lte, contains, and a myriad of other lookups depending on the field type.

# Books published in 2021
books = Book.objects.filter(published__year=2021)
# Books published in the year 2000 or after
books = Book.objects.filter(published__year__gte=2000)
# Books published before the year 2000
books = Book.objects.filter(published__year__lt=2000)
Enter fullscreen mode Exit fullscreen mode

With time fields we can even filter using a range lookup to query objects within a specific time range.

start_date = datetime.date(2021, 1, 1)
end_date = datetime.date(2021, 1, 3)
books = Book.objects.filter(published__range=(start_date, end_date))
Enter fullscreen mode Exit fullscreen mode

Using the built-in expressions and Q objects we can do some more advanced queries.

Q Objects

Keyword arguments in a filter query are "AND"ed together. If we want to execute OR queries we can use the Q object. Q objects encapsulate keyword arguments for filtering just like filter, but we can combine Q objects using & or |.

from django.db.models import Q
# Get all books published in 2018 or 2020
books = Book.objects.filter(Q(published__year=2018) | Q(published__year=2020))
# Get all books published in
Enter fullscreen mode Exit fullscreen mode

This will query all books published in 2018 or 2020.

You can combine any number of Q objects into more complex queries.

F expressions

F expressions represent a value of a model field. It makes it possible to use field values in queries without actually pulling the value from the database. This is possible because Django creates a SQL query that handles everything for us.

from django.db.models import F
# Query books published by authors under 30 years old (this is not exactly true because years vary in length)
books = Book.objects.filter(published__lte=F("author__birth_date") + datetime.timedelta(days=365*30))
Enter fullscreen mode Exit fullscreen mode

We can also use F expressions when updating data to avoid performing multiple queries.

from django.db.models import F
book = Book.objects.get(title="Crime and Punishment")
book.update(rating=F("rating") + 1)
Enter fullscreen mode Exit fullscreen mode

This will result in an SQL query that will add one to the rating of the book without first querying the current value.

Annotation

Most often we just want to query the values defined in a model with some filtering criteria. But sometimes you'll be calculating or combining values that you need from the result of the query. This can, of course, be done in Python but for performance reasons, it might be worth it to let the database handle the calculations. This is where Djangos annotations come in handy.

from django.db.models import F, Value as V
from django.db.models.functions import Concat
author = Author.objects.annotate(full_name=Concat(F("firstname"), V(" "), F("lastname")))
Enter fullscreen mode Exit fullscreen mode

Here, we are adding a new dynamic field full_name to our query results. It will contain a concatenation of the authors firstname and lastname done using the Concat function, F expressions, and Value.

Concat is just one example of a database function available in Django. Database functions are a great way to add dynamic fields into our queries. Django supports a number of database functions like comparison and conversion functions (Coalesce, Greatest, ...), math functions (Power, Sqrt, Ceil, ...), text functions (Concat, Trim, Upper, ...), and window functions (FirstValue, NthValue, ...)

Here's an example on how we can use Coalesce to get either the nickname or the firstname of a author.

from django.db.models import F
from django.db.models.functions import Coalesce
books = Author.objects.annotate(known_as=Coalesce(F("nickname"), F("firstname")))
Enter fullscreen mode Exit fullscreen mode

Now our results have a new field known_as, that holds the value we wanted for each Author in the result set.

Using F expressions we can also do basic arithmetic to calculate the value for a dynamic field.

# Add  a new field with the authors age at the time of publishing the book
books = Book.objects.annotate(author_age=F("published") - F("author__birth_date"))
# Add a new field with the rating multiplied by 100
books = Book.objects.annotate(rating_multiplied=F("rating") * 100)
Enter fullscreen mode Exit fullscreen mode

Aggregation

While annotate can be used to add new values to the returned data, aggregation can be used to derive values by summarizing or aggregating a result set. The difference between aggregation and annotation is that annotating adds a new field to every row of a result set and aggregating reduces the results into a single row with the aggregated values.

Common uses for aggregation are calculating counting, averaging, or finding maximum or minimum.

from django.db.models import Avg
result = Book.objects.aggregate(Avg("price"))
# {'price__avg': Decimal('13.50')}
result = Book.objects.aggerate(Max("price"))
# {'price__max: Decimal('13.50')}
result = Book.objects.aggerate(Min("published"))
# {'published__min': datetime.date(1866, 7, 25)}
Enter fullscreen mode Exit fullscreen mode

Aggregation can also be done without the aggregate method by using the database functions we talked about earlier. This way we can add a dynamic field into our results with annotate and calculate the value for it with aggregation. 😎

from django.db.models import Count
authors = Author.objects.annotate(num_books=Count("books"))
Enter fullscreen mode Exit fullscreen mode

This will add a dynamic field num_books to every row of the result set with the number of books the author has.

Using values, annotate and a aggregation function allows us to do a GROUP BY query with a dynamic field.

# Calculate average prices for books in all categories.
Book.objects.values("category").annotate(Avg("price"))
# {'category': 'Historical fiction', 'price__avg': Decimal('13.9900000000000')}, {'category': 'Romance', 'price__avg': Decimal('16.4950000000000')}
Enter fullscreen mode Exit fullscreen mode

Case...When

The last example I want to show is a bit more complex but showcases some of Djangos query features together. It includes using conditional expressions when calculating the value of a dynamic field.

from django.db.models import F, Q, Value, When, Case
from decimal import Decimal
books = Book.objects.annotate(discounted_price=Case(
    When(category="Romance", then=F("price") * Decimal(0.95)),
    When(category="Historical fiction", then=F("price") * Decimal(0.8)),
    default=None
))
Enter fullscreen mode Exit fullscreen mode

Here we're calculating a discounted prices on all books based on the category using Case and When

Further reading

Django docs on queries

Photo by Jan Antonin Kolar on Unsplash

Discussion (0)