Spec-Zone.ru › Django 6.0

Условные выражения

Условные выражения позволяют использовать логику if … elif … else в фильтрах, аннотациях, агрегатах и обновлениях. Условное выражение проверяет последовательность условий для каждой строки таблицы и возвращает соответствующее выражение результата. Условные выражения также можно комбинировать и вкладывать друг в друга, как и другие выражения.

Классы условных выражений

В следующих примерах мы будем использовать такую модель:

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) [исходный код]

Объект When() используется для инкапсуляции условия и его результата, чтобы применять их в условном выражении. Использование объекта When() похоже на использование метода filter(). Условие можно задать с помощью поиска по полям, объектов Q или объектов Expression, у которых атрибут output_field имеет тип BooleanField. Результат задаётся с помощью ключевого слова then.

Несколько примеров:

>>> 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"))
>>> # Condition can be created using lookup expressions.
>>> from django.db.models.lookups import GreaterThan, LessThan
>>> When(
...     GreaterThan(F("registered_on"), date(2014, 1, 1))
...     & LessThan(F("registered_on"), date(2015, 1, 1)),
...     then="account_type",
... )

Помните, что каждое из этих значений может быть выражением.

Примечание

Поскольку аргумент с ключевым словом then зарезервирован для результата When(), может возникнуть конфликт, если у Model есть поле с именем then. Это можно исправить двумя способами:

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

Case

class Case(*cases, **extra) [исходный код]

Выражение Case() похоже на инструкцию if … elif … else в Python. Каждый condition в переданных объектах When() проверяется по порядку, пока один из них не даст истинное значение. Возвращается выражение result из соответствующего объекта When().

Например:

>>>
>>> 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() принимает любое количество объектов When() в качестве отдельных аргументов. Другие параметры передаются с помощью именованных аргументов. Если ни одно из условий не даст значение TRUE, будет возвращено выражение, указанное в именованном аргументе default. Если аргумент default не передан, используется None.

Если мы хотим изменить предыдущий запрос, чтобы размер скидки зависел от того, как долго Client является нашим клиентом, это можно сделать с помощью поисковых выражений:

>>> 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%')]>

Примечание

Помните, что условия проверяются по порядку. Поэтому в примере выше мы получаем правильный результат, хотя второе условие подходит и для Jane Doe, и для Jack Black. Это работает так же, как инструкция if … elif … else в Python.

Case() также работает в предложении filter(). Например, чтобы найти золотых клиентов, зарегистрировавшихся более месяца назад, и платиновых клиентов, зарегистрировавшихся более года назад:

>>> 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')]>

Расширенные запросы

Условные выражения можно использовать в аннотациях, агрегатах, фильтрах, поисковых выражениях и обновлениях. Их также можно комбинировать и вкладывать в другие выражения. Это позволяет создавать мощные условные запросы.

Условное обновление

Предположим, что мы хотим изменить account_type наших клиентов в соответствии с датами их регистрации. Это можно сделать с помощью условного выражения и метода 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')]>

Условная агрегация

Что, если мы хотим узнать, сколько клиентов относится к каждой категории account_type? Для этого можно использовать аргумент filter агрегатных функций:

>>> # 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}

Для баз данных, которые поддерживают синтаксис SQL 2003 FILTER WHERE, этот агрегат формирует запрос с таким синтаксисом:

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;

В других базах данных это эмулируется с помощью инструкции 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;

Обе инструкции SQL функционально эквивалентны, но более явный вариант FILTER может работать быстрее.

Условная фильтрация

Если условное выражение возвращает логическое значение, его можно непосредственно использовать в фильтрах. Это значит, что оно не будет добавлено в столбцы SELECT, но его всё равно можно использовать для фильтрации результатов:

>>> 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))

В терминах SQL это даст следующий результат:

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
)

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/ref/models/conditional-expressions/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API