Conditional Expressions

Conditional expressions let you use ifelifelse logic within filters, annotations, aggregations, and updates. Aconditional expression evaluates a series of conditions for each row of atable and returns the matching result expression. Conditional expressions canalso be combined and nested like other expressions.

The conditional expression classes

We'll be using the following model in the subsequent examples:

  1. from django.db import models
  2.  
  3. class Client(models.Model):
  4. REGULAR = 'R'
  5. GOLD = 'G'
  6. PLATINUM = 'P'
  7. ACCOUNT_TYPE_CHOICES = (
  8. (REGULAR, 'Regular'),
  9. (GOLD, 'Gold'),
  10. (PLATINUM, 'Platinum'),
  11. )
  12. name = models.CharField(max_length=50)
  13. registered_on = models.DateField()
  14. account_type = models.CharField(
  15. max_length=1,
  16. choices=ACCOUNT_TYPE_CHOICES,
  17. default=REGULAR,
  18. )

When

  • class When(condition=None, then=None, **lookups)[source]
  • A When() object is used to encapsulate a condition and its result for usein the conditional expression. Using a When() object is similar to usingthe filter() method. The condition canbe specified using field lookups orQ objects. The result is provided using the thenkeyword.

Some examples:

  1. >>> from django.db.models import F, Q, When
  2. >>> # String arguments refer to fields; the following two examples are equivalent:
  3. >>> When(account_type=Client.GOLD, then='name')
  4. >>> When(account_type=Client.GOLD, then=F('name'))
  5. >>> # You can use field lookups in the condition
  6. >>> from datetime import date
  7. >>> When(registered_on__gt=date(2014, 1, 1),
  8. ... registered_on__lt=date(2015, 1, 1),
  9. ... then='account_type')
  10. >>> # Complex conditions can be created using Q objects
  11. >>> When(Q(name__startswith="John") | Q(name__startswith="Paul"),
  12. ... then='name')

Keep in mind that each of these values can be an expression.

Note

Since the then keyword argument is reserved for the result of theWhen(), there is a potential conflict if aModel has a field named then. This can beresolved in two ways:

  1. >>> When(then__exact=0, then=1)
  2. >>> When(Q(then=0), then=1)

Case

  • class Case(*cases, **extra)[source]
  • A Case() expression is like the ifelifelse statement in Python. Each condition in the providedWhen() objects is evaluated in order, until one evaluates to atruthful value. The result expression from the matching When() objectis returned.

A simple example:

  1. >>>
  2. >>> from datetime import date, timedelta
  3. >>> from django.db.models import Case, CharField, Value, When
  4. >>> Client.objects.create(
  5. ... name='Jane Doe',
  6. ... account_type=Client.REGULAR,
  7. ... registered_on=date.today() - timedelta(days=36))
  8. >>> Client.objects.create(
  9. ... name='James Smith',
  10. ... account_type=Client.GOLD,
  11. ... registered_on=date.today() - timedelta(days=5))
  12. >>> Client.objects.create(
  13. ... name='Jack Black',
  14. ... account_type=Client.PLATINUM,
  15. ... registered_on=date.today() - timedelta(days=10 * 365))
  16. >>> # Get the discount for each Client based on the account type
  17. >>> Client.objects.annotate(
  18. ... discount=Case(
  19. ... When(account_type=Client.GOLD, then=Value('5%')),
  20. ... When(account_type=Client.PLATINUM, then=Value('10%')),
  21. ... default=Value('0%'),
  22. ... output_field=CharField(),
  23. ... ),
  24. ... ).values_list('name', 'discount')
  25. <QuerySet [('Jane Doe', '0%'), ('James Smith', '5%'), ('Jack Black', '10%')]>

Case() accepts any number of When() objects as individual arguments.Other options are provided using keyword arguments. If none of the conditionsevaluate to TRUE, then the expression given with the default keywordargument is returned. If a default argument isn't provided, None isused.

If we wanted to change our previous query to get the discount based on how longthe Client has been with us, we could do so using lookups:

  1. >>> a_month_ago = date.today() - timedelta(days=30)
  2. >>> a_year_ago = date.today() - timedelta(days=365)
  3. >>> # Get the discount for each Client based on the registration date
  4. >>> Client.objects.annotate(
  5. ... discount=Case(
  6. ... When(registered_on__lte=a_year_ago, then=Value('10%')),
  7. ... When(registered_on__lte=a_month_ago, then=Value('5%')),
  8. ... default=Value('0%'),
  9. ... output_field=CharField(),
  10. ... )
  11. ... ).values_list('name', 'discount')
  12. <QuerySet [('Jane Doe', '5%'), ('James Smith', '0%'), ('Jack Black', '10%')]>

Note

Remember that the conditions are evaluated in order, so in the aboveexample we get the correct result even though the second condition matchesboth Jane Doe and Jack Black. This works just like an ifelifelse statement in Python.

Case() also works in a filter() clause. For example, to find goldclients that registered more than a month ago and platinum clients thatregistered more than a year ago:

  1. >>> a_month_ago = date.today() - timedelta(days=30)
  2. >>> a_year_ago = date.today() - timedelta(days=365)
  3. >>> Client.objects.filter(
  4. ... registered_on__lte=Case(
  5. ... When(account_type=Client.GOLD, then=a_month_ago),
  6. ... When(account_type=Client.PLATINUM, then=a_year_ago),
  7. ... ),
  8. ... ).values_list('name', 'account_type')
  9. <QuerySet [('Jack Black', 'P')]>

Advanced queries

Conditional expressions can be used in annotations, aggregations, lookups, andupdates. They can also be combined and nested with other expressions. Thisallows you to make powerful conditional queries.

Conditional update

Let's say we want to change the account_type for our clients to matchtheir registration dates. We can do this using a conditional expression and theupdate() method:

  1. >>> a_month_ago = date.today() - timedelta(days=30)
  2. >>> a_year_ago = date.today() - timedelta(days=365)
  3. >>> # Update the account_type for each Client from the registration date
  4. >>> Client.objects.update(
  5. ... account_type=Case(
  6. ... When(registered_on__lte=a_year_ago,
  7. ... then=Value(Client.PLATINUM)),
  8. ... When(registered_on__lte=a_month_ago,
  9. ... then=Value(Client.GOLD)),
  10. ... default=Value(Client.REGULAR)
  11. ... ),
  12. ... )
  13. >>> Client.objects.values_list('name', 'account_type')
  14. <QuerySet [('Jane Doe', 'G'), ('James Smith', 'R'), ('Jack Black', 'P')]>

Conditional aggregation

What if we want to find out how many clients there are for eachaccount_type? We can use the filter argument of aggregatefunctions to achieve this:

  1. >>> # Create some more Clients first so we can have something to count
  2. >>> Client.objects.create(
  3. ... name='Jean Grey',
  4. ... account_type=Client.REGULAR,
  5. ... registered_on=date.today())
  6. >>> Client.objects.create(
  7. ... name='James Bond',
  8. ... account_type=Client.PLATINUM,
  9. ... registered_on=date.today())
  10. >>> Client.objects.create(
  11. ... name='Jane Porter',
  12. ... account_type=Client.PLATINUM,
  13. ... registered_on=date.today())
  14. >>> # Get counts for each value of account_type
  15. >>> from django.db.models import Count
  16. >>> Client.objects.aggregate(
  17. ... regular=Count('pk', filter=Q(account_type=Client.REGULAR)),
  18. ... gold=Count('pk', filter=Q(account_type=Client.GOLD)),
  19. ... platinum=Count('pk', filter=Q(account_type=Client.PLATINUM)),
  20. ... )
  21. {'regular': 2, 'gold': 1, 'platinum': 3}

This aggregate produces a query with the SQL 2003 FILTER WHERE syntaxon databases that support it:

  1. SELECT count('id') FILTER (WHERE account_type=1) as regular,
  2. count('id') FILTER (WHERE account_type=2) as gold,
  3. count('id') FILTER (WHERE account_type=3) as platinum
  4. FROM clients;

On other databases, this is emulated using a CASE statement:

  1. SELECT count(CASE WHEN account_type=1 THEN id ELSE null) as regular,
  2. count(CASE WHEN account_type=2 THEN id ELSE null) as gold,
  3. count(CASE WHEN account_type=3 THEN id ELSE null) as platinum
  4. FROM clients;

The two SQL statements are functionally equivalent but the more explicitFILTER may perform better.