Expressions conditionnelles

Les expressions conditionnelles permettent d’utiliser de la logique ifelifelse à l’intérieur des filtres, des annotations, des agrégations et des mises à jour. Une expression conditionnelle évalue une série de conditions pour chaque ligne d’une table et renvoie l’expression résultante correspondante. Les expressions conditionnelles peuvent également être combinées et imbriquées comme toute autre expression.

Les classes d’expressions conditionnelles

Nous allons utiliser le modèle suivant dans les exemples qui suivront :

from django.db import models

class Client(models.Model):
    REGULAR = 'R'
    GOLD = 'G'
    PLATINUM = 'P'
    ACCOUNT_TYPE_CHOICES = [
        (REGULAR, 'Regular'),
        (GOLD, 'Gold'),
        (PLATINUM, 'Platinum'),
    ]
    name = models.CharField(max_length=50)
    registered_on = models.DateField()
    account_type = models.CharField(
        max_length=1,
        choices=ACCOUNT_TYPE_CHOICES,
        default=REGULAR,
    )

When

class When(condition=None, then=None, **lookups)

Un objet When() est utilisé pour englober une condition et ses résultats pour leur exploitation dans une expression conditionnelle. L’emploi d’un objet When() est semblable à celui d’une méthode filter(). La condition peut être indiquée en utilisant des objets de recherche de champ, des objets Q ou des objets Expression dont le résultat output_field est un BooleanField. Le résultat est fourni en utilisant le mot-clé then.

Quelques exemples :

>>> from django.db.models import F, Q, When
>>> # String arguments refer to fields; the following two examples are equivalent:
>>> When(account_type=Client.GOLD, then='name')
>>> When(account_type=Client.GOLD, then=F('name'))
>>> # You can use field lookups in the condition
>>> from datetime import date
>>> When(registered_on__gt=date(2014, 1, 1),
...      registered_on__lt=date(2015, 1, 1),
...      then='account_type')
>>> # Complex conditions can be created using Q objects
>>> When(Q(name__startswith="John") | Q(name__startswith="Paul"),
...      then='name')
>>> # Condition can be created using boolean expressions.
>>> from django.db.models import Exists, OuterRef
>>> non_unique_account_type = Client.objects.filter(
...     account_type=OuterRef('account_type'),
... ).exclude(pk=OuterRef('pk')).values('pk')
>>> When(Exists(non_unique_account_type), then=Value('non unique'))

N’oubliez pas que chacune de ces valeurs peut être elle-même une expression.

Note

Comme le paramètre nommé then est réservé au résultat de l’expression When(), un conflit potentiel existe si un Model possède un champ nommé then. Ceci peut être résolu de deux manières :

>>> When(then__exact=0, then=1)
>>> When(Q(then=0), then=1)
Changed in Django 3.2:

Support for using the condition argument with lookups was added.

Case

class Case(*cases, **extra)

Une expression Case() est semblable à une instruction ifelifelse en Python. Chaque condition dans les objets When() fournis est évaluée dans l’ordre, jusqu’à ce que l’une d’elle résulte en une valeur vraie. L’expression result de l’objet When() correspondant est renvoyée.

Un exemple :

>>>
>>> from datetime import date, timedelta
>>> from django.db.models import Case, Value, When
>>> Client.objects.create(
...     name='Jane Doe',
...     account_type=Client.REGULAR,
...     registered_on=date.today() - timedelta(days=36))
>>> Client.objects.create(
...     name='James Smith',
...     account_type=Client.GOLD,
...     registered_on=date.today() - timedelta(days=5))
>>> Client.objects.create(
...     name='Jack Black',
...     account_type=Client.PLATINUM,
...     registered_on=date.today() - timedelta(days=10 * 365))
>>> # Get the discount for each Client based on the account type
>>> Client.objects.annotate(
...     discount=Case(
...         When(account_type=Client.GOLD, then=Value('5%')),
...         When(account_type=Client.PLATINUM, then=Value('10%')),
...         default=Value('0%'),
...     ),
... ).values_list('name', 'discount')
<QuerySet [('Jane Doe', '0%'), ('James Smith', '5%'), ('Jack Black', '10%')]>

Case() accepte un nombre indéfini d’objets When() comme paramètres individuels. D’autres options sont fournies sous forme de paramètres nommés. Si aucune des conditions évaluées ne résulte en une valeur vraie, c’est alors l’expression indiquée dans le paramètre nommé default qui est renvoyée. Si aucun paramètre default n’est fourni, c’est None qui est utilisé.

Si nous voulions modifier notre requête précédente pour obtenir le rabais sur la base de la fidélité de Client, nous pourrions le faire à l’aide d’expressions de recherche :

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

Note

Rappelez-vous que les conditions sont évaluées dans l’ordre, ce qui fait que dans l’exemple ci-dessus, nous obtenons le résultat correct même si la seconde condition correspondait à la fois à Jane Doe et à Jack Black. Cela fonctionne exactement de la même manière qu’avec l’instruction ifelifelse en Python.

Case() fonctionne aussi dans une clause filter(). Par exemple, pour trouver tous les clients « gold » qui se sont inscrits il y a plus d’un mois et les clients « platinum » qui se sont inscrits il y a plus d’un an :

>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> Client.objects.filter(
...     registered_on__lte=Case(
...         When(account_type=Client.GOLD, then=a_month_ago),
...         When(account_type=Client.PLATINUM, then=a_year_ago),
...     ),
... ).values_list('name', 'account_type')
<QuerySet [('Jack Black', 'P')]>

Requêtes avancées

Les expressions conditionnelles peuvent être utilisées dans les annotations, les agrégations, les filtres, les recherches et les mises à jour. Elles peuvent également être combinées et imbriquées avec d’autres expressions. Cela permet d’effectuer des requêtes conditionnelles puissantes.

Mise à jour conditionnelle

Admettons que nous voulions modifier le type de compte account_type de nos clients pour qu’il corresponde aux dates d’enregistrement. Nous pouvons le faire à l’aide d’une expression conditionnelle et de la méthode update():

>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> # Update the account_type for each Client from the registration date
>>> Client.objects.update(
...     account_type=Case(
...         When(registered_on__lte=a_year_ago,
...              then=Value(Client.PLATINUM)),
...         When(registered_on__lte=a_month_ago,
...              then=Value(Client.GOLD)),
...         default=Value(Client.REGULAR)
...     ),
... )
>>> Client.objects.values_list('name', 'account_type')
<QuerySet [('Jane Doe', 'G'), ('James Smith', 'R'), ('Jack Black', 'P')]>

Agrégation conditionnelle

Et si nous voulions savoir combien de clients existent pour chaque type de compte ? Nous pouvons utiliser le paramètre filter des fonctions d’agrégat pour pouvoir faire cela :

>>> # Create some more Clients first so we can have something to count
>>> Client.objects.create(
...     name='Jean Grey',
...     account_type=Client.REGULAR,
...     registered_on=date.today())
>>> Client.objects.create(
...     name='James Bond',
...     account_type=Client.PLATINUM,
...     registered_on=date.today())
>>> Client.objects.create(
...     name='Jane Porter',
...     account_type=Client.PLATINUM,
...     registered_on=date.today())
>>> # Get counts for each value of account_type
>>> from django.db.models import Count
>>> Client.objects.aggregate(
...     regular=Count('pk', filter=Q(account_type=Client.REGULAR)),
...     gold=Count('pk', filter=Q(account_type=Client.GOLD)),
...     platinum=Count('pk', filter=Q(account_type=Client.PLATINUM)),
... )
{'regular': 2, 'gold': 1, 'platinum': 3}

Cet agrégat produit une requête avec la syntaxe SQL 2003 FILTER WHERE pour les bases de données qui la prennent en charge :

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

Pour les autres bases de données, cet effet est émulé à l’aide d’une instruction CASE:

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

Les deux instructions SQL sont fonctionnellement équivalentes mais la variante FILTER plus explicite peut donner de meilleures performances.

Filtre conditionnel

Lorsqu’une expression conditionnelle renvoie une valeur booléenne, il est possible de l’utiliser directement dans des filtres. Cela signifie qu’elle ne sera pas ajoutée aux colonnes SELECT, mais qu’il est quand même possible de l’utiliser pour filtrer les résultats

>>> non_unique_account_type = Client.objects.filter(
...     account_type=OuterRef('account_type'),
... ).exclude(pk=OuterRef('pk')).values('pk')
>>> Client.objects.filter(~Exists(non_unique_account_type))

En termes SQL, cela donne :

SELECT ...
FROM client c0
WHERE NOT EXISTS (
  SELECT c1.id
  FROM client c1
  WHERE c1.account_type = c0.account_type AND NOT c1.id = c0.id
)
Back to Top