Spec-Zone.ru › Django 5.2

Выражения запросов

Выражения запросов описывают значение или вычисление, которые могут быть использованы как часть обновления, создания, фильтрации, сортировки, аннотации или агрегации. Когда выражение выводит булево значение, оно может быть использовано непосредственно в фильтрах. Существует ряд встроенных выражений (документированных ниже), которые могут помочь вам написать запросы. Выражения могут быть объединены или, в некоторых случаях, вложены, чтобы сформировать более сложные вычисления.

Поддерживаемые арифметические операции

Django поддерживает отрицание, сложение, вычитание, умножение, деление, остаток от деления и операцию возведения в степень для выражений запросов, используя Python-константы, переменные и даже другие выражения.

Поле вывода

Многие из документированных в этом разделе выражений поддерживают необязательный output_field параметр. Если он задан, Django загрузит значение в это поле после его получения из базы данных.

output_field принимает экземпляр поля модели, например, IntegerField() или BooleanField(). Обычно поле не требует каких-либо аргументов, например, max_length, так как аргументы поля относятся к проверке данных, которая не будет выполняться для выходного значения выражения.

output_field требуется только тогда, когда Django не может автоматически определить тип поля результата, например, в сложных выражениях, которые смешивают типы полей. Например, добавление DecimalField() и FloatField() требует поля вывода, такого как output_field=FloatField().

Примеры

>>> from django.db.models import Count, F, Value
>>> from django.db.models.functions import Length, Upper
>>> from django.db.models.lookups import GreaterThan

# Find companies that have more employees than chairs.
>>> Company.objects.filter(num_employees__gt=F("num_chairs"))

# Find companies that have at least twice as many employees
# as chairs. Both the querysets below are equivalent.
>>> Company.objects.filter(num_employees__gt=F("num_chairs") * 2)
>>> Company.objects.filter(num_employees__gt=F("num_chairs") + F("num_chairs"))

# How many chairs are needed for each company to seat all employees?
>>> company = (
...     Company.objects.filter(num_employees__gt=F("num_chairs"))
...     .annotate(chairs_needed=F("num_employees") - F("num_chairs"))
...     .first()
... )
>>> company.num_employees
120
>>> company.num_chairs
50
>>> company.chairs_needed
70

# Create a new company using expressions.
>>> company = Company.objects.create(name="Google", ticker=Upper(Value("goog")))
# Be sure to refresh it if you need to access the field.
>>> company.refresh_from_db()
>>> company.ticker
'GOOG'

# Annotate models with an aggregated value. Both forms
# below are equivalent.
>>> Company.objects.annotate(num_products=Count("products"))
>>> Company.objects.annotate(num_products=Count(F("products")))

# Aggregates can contain complex computations also
>>> Company.objects.annotate(num_offerings=Count(F("products") + F("services")))

# Expressions can also be used in order_by(), either directly
>>> Company.objects.order_by(Length("name").asc())
>>> Company.objects.order_by(Length("name").desc())
# or using the double underscore lookup syntax.
>>> from django.db.models import CharField
>>> from django.db.models.functions import Length
>>> CharField.register_lookup(Length)
>>> Company.objects.order_by("name__length")

# Boolean expression can be used directly in filters.
>>> from django.db.models import Exists, OuterRef
>>> Company.objects.filter(
...     Exists(Employee.objects.filter(company=OuterRef("pk"), salary__gt=10))
... )

# Lookup expressions can also be used directly in filters
>>> Company.objects.filter(GreaterThan(F("num_employees"), F("num_chairs")))
# or annotations.
>>> Company.objects.annotate(
...     need_chairs=GreaterThan(F("num_employees"), F("num_chairs")),
... )

Встроенные выражения

Примечание

Эти выражения определены в django.db.models.expressions и django.db.models.aggregates, но для удобства они доступны и обычно импортируются из django.db.models.

F() выражения

class F [source]

Объект F() представляет значение поля модели, преобразованное значение поля модели или аннотированный столбец. Это позволяет ссылаться на значения полей модели и выполнять операции базы данных с ними, не фактически извлекая их из базы данных в память Python.

Вместо этого Django использует объект F() для генерации SQL-выражения, которое описывает требуемую операцию на уровне базы данных.

Попробуем это на примере. Обычно можно сделать что-то вроде этого:

# Tintin filed a news story!
reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed += 1
reporter.save()

Здесь мы извлекли значение reporter.stories_filed из базы данных в память, обработали его с помощью знакомых операторов Python и затем сохранили объект обратно в базу данных. Но вместо этого мы также могли бы сделать:

from django.db.models import F

reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed = F("stories_filed") + 1
reporter.save()

Хотя reporter.stories_filed = F('stories_filed') + 1 выглядит как обычное Python-присваивание значения атрибуту экземпляра, на самом деле это SQL-конструкция, описывающая операцию в базе данных.

Когда Django встречает экземпляр F(), он переопределяет стандартные операторы Python для создания инкапсулированного SQL-выражения; в данном случае, выражения, которое инструктирует базу данных увеличить поле базы данных, представленное reporter.stories_filed.

Какое значение было или есть на reporter.stories_filed, Python никогда не узнает об этом — с ним полностью работает база данных. Всё, что делает Python через класс Django F(), это создать синтаксис SQL для ссылки на поле и описания операции.

Для доступа к новому сохранённому таким образом значению объект необходимо перезагрузить:

reporter = Reporters.objects.get(pk=reporter.pk)
# Or, more succinctly:
reporter.refresh_from_db()

Помимо использования в операциях с отдельными экземплярами, как показано выше, F() может использоваться с QuerySets экземпляров объектов с помощью update(). Это сокращает две запросы, которые мы использовали выше — get() и save() — до одной:

reporter = Reporters.objects.filter(name="Tintin")
reporter.update(stories_filed=F("stories_filed") + 1)

Мы также можем использовать update() для увеличения значения поля для нескольких объектов — что может быть намного быстрее, чем извлечение их всех из базы данных в Python, перебор их, увеличение значения поля каждого из них и сохранение каждого из них обратно в базу данных:

Reporter.objects.update(stories_filed=F("stories_filed") + 1)

Таким образом, F() может предлагать преимущества производительности, за счёт:

  • передачи работы базе данных, а не Python
  • сокращения количества запросов, необходимых для некоторых операций

Вырезание F() выражений

Новое в Django 5.1.

Для строковых полей, полей текстового типа и ArrayField, вы можете использовать синтаксис срезов Python. Индексы начинаются с 0, а аргумент step для slice не поддерживается. Например:

>>> # Replacing a name with a substring of itself.
>>> writer = Writers.objects.get(name="Priyansh")
>>> writer.name = F("name")[1:5]
>>> writer.save()
>>> writer.refresh_from_db()
>>> writer.name
'riya'

Избегание гонок с использованием F()

Ещё одно полезное преимущество F() заключается в том, что обновление значения поля базой данных, а не Python, избегает возникновения гонки.

Если два потока Python выполняют код в первом примере выше, один поток может извлечь, увеличить и сохранить значение поля после того, как другой поток извлёк его из базы данных. Значение, которое сохраняет второй поток, будет основано на исходном значении; работа первого потока будет потеряна.

Если базой данных отвечает за обновление поля, процесс более надёжен: он будет обновлять поле только на основе значения поля в базе данных, когда выполняется save() или update(), а не на основе его значения при извлечении экземпляра.

F() присваивания сохраняются после Model.save()

F() объекты, присвоенные полям модели, сохраняются после сохранения экземпляра модели и будут применяться при каждом save(). Например:

reporter = Reporters.objects.get(name="Tintin")
reporter.stories_filed = F("stories_filed") + 1
reporter.save()

reporter.name = "Tintin Jr."
reporter.save()

stories_filed будет обновлено дважды в этом случае. Если изначально оно равно 1, конечное значение будет 3. Это сохранение можно избежать, перезагрузив объект модели после его сохранения, например, с помощью refresh_from_db().

Использование F() в фильтрах

F() также очень полезно в QuerySet фильтрах, где они позволяют фильтровать набор объектов по критериям, основанным на их значениях полей, а не на значениях Python.

Это документировано в использовании выражений F() в запросах.

Использование F() с аннотациями

F() может быть использован для создания динамических полей в ваших моделях путём объединения различных полей с помощью арифметики:

company = Company.objects.annotate(chairs_needed=F("num_employees") - F("num_chairs"))

Если поля, которые вы объединяете, имеют разные типы, вам нужно сказать Django, какого типа будет возвращаемое поле. Большинство выражений поддерживают поле вывода для этого случая, но так как F() это не поддерживает, вам нужно обернуть выражение в ExpressionWrapper:

from django.db.models import DateTimeField, ExpressionWrapper, F

Ticket.objects.annotate(
    expires=ExpressionWrapper(
        F("active_at") + F("duration"), output_field=DateTimeField()
    )
)

При ссылке на реляционные поля, такие как ForeignKey, F() возвращает значение первичного ключа, а не экземпляр модели:

>>> car = Company.objects.annotate(built_by=F("manufacturer"))[0]
>>> car.manufacturer
<Manufacturer: Toyota>
>>> car.built_by
3

Использование F() для сортировки значений NULL

Используйте F() и nulls_first или nulls_last ключевой аргумент для Expression.asc() или desc(), чтобы управлять порядком значений NULL поля. По умолчанию порядок зависит от вашей базы данных.

Например, для сортировки компаний, с которыми не было контакта (last_contacted равно NULL) после компаний, с которыми был контакт:

from django.db.models import F

Company.objects.order_by(F("last_contacted").desc(nulls_last=True))

Использование F() с логическими операциями

F() выражения, которые выводят BooleanField, могут быть логически инвертированы с помощью оператора инверсии ~F(). Например, для смены статуса активации компаний:

from django.db.models import F

Company.objects.update(is_active=~F("is_active"))

Func() выражения

Func() выражения являются базовым типом всех выражений, которые включают базовые функции базы данных, такие как COALESCE и LOWER, или агрегаты, такие как SUM. Их можно использовать непосредственно:

from django.db.models import F, Func

queryset.annotate(field_lower=Func(F("field"), function="LOWER"))

или они могут быть использованы для построения библиотеки баз данных функций:

class Lower(Func):
    function = "LOWER"


queryset.annotate(field_lower=Lower("field"))

Но в обоих случаях результатом будет набор запросов, где каждая модель аннотирована дополнительным атрибутом field_lower, произведённым примерно из следующего SQL:

SELECT
    ...
    LOWER("db_table"."field") as "field_lower"

См. Функции базы данных для списка встроенных функций базы данных.

Func API выглядит следующим образом:

class Func(*expressions, **extra) [source]
function

Атрибут класса, описывающий функцию, которая будет сгенерирована. В частности, function будет интерполирован как заполнитель function в template. По умолчанию None.

template

Атрибут класса в формате строки, описывающий SQL, который генерируется для этой функции. По умолчанию '%(function)s(%(expressions)s)'.

Если вы строите SQL, подобный strftime('%W', 'date') и вам нужен буквальный % символ в запросе, удвойте его (%%%%) в атрибуте template, потому что строка интерполируется дважды: один раз во время интерполяции шаблона в as_sql() и один раз во время интерполяции SQL с параметрами запроса в курсоре базы данных.

arg_joiner

Атрибут класса, обозначающий символ, используемый для объединения списка expressions. По умолчанию ', '.

arity

Атрибут класса, обозначающий количество аргументов, принимаемых функцией. Если этот атрибут установлен, а функция вызывается с другим количеством выражений, будет поднято исключение TypeError. По умолчанию None.

as_sql(compiler, connection, function=None, template=None, arg_joiner=None, **extra_context) [source]

Генерирует фрагмент SQL для функции базы данных. Возвращает кортеж (sql, params), где sql — строка SQL, а params — список или кортеж параметров запроса.

as_vendor() методы должны использовать function, template, arg_joiner и любые другие **extra_context параметры для настройки SQL по мере необходимости. Например:

django/db/models/functions.py
class ConcatPair(Func):
    ...
    function = "CONCAT"
    ...

    def as_mysql(self, compiler, connection, **extra_context):
        return super().as_sql(
            compiler,
            connection,
            function="CONCAT_WS",
            template="%(function)s('', %(expressions)s)",
            **extra_context
        )

Чтобы избежать уязвимости SQL-инъекции, extra_context не должно содержать небезопасные данные от пользователя, так как эти значения интерполируются в строку SQL, а не передаются в качестве параметров запроса, где драйвер базы данных их бы экранировал.

Аргумент *expressions — это список позиционных выражений, к которым будет применена функция. Выражения будут преобразованы в строки, объединены с помощью arg_joiner и затем интерполированы в template в качестве заполнителя expressions.

Позиционные аргументы могут быть выражениями или значениями Python. Строки предполагаются как ссылки на столбцы и будут обернуты в F() выражения, в то время как другие значения будут обернуты в Value() выражения.

**extra kwargs — это пары key=value, которые могут быть интерполированы в атрибут template. Чтобы избежать уязвимости SQL-инъекции, extra не должны содержать небезопасные данные от пользователя, поскольку эти значения интерполируются в строку SQL, а не передаются как параметры запроса, где драйвер базы данных их бы экранировал.

function, template и arg_joiner ключевые слова могут использоваться для замены атрибутов с таким же именем без необходимости определения собственного класса. output_field может использоваться для определения ожидаемого типа возвращаемого значения.

Aggregate() выражения

Агрегированное выражение — это специальный случай выражения Func() expression, который сообщает запросу, что требуется GROUP BY клауза. Все Sum() и Count(), такие как Aggregate(), наследуются от.

Поскольку Aggregate — это выражения и обертывают выражения, вы можете представлять некоторые сложные вычисления:

from django.db.models import Count

Company.objects.annotate(
    managers_required=(Count("num_employees") / 4) + Count("num_managers")
)

Aggregate API выглядит следующим образом:

class Aggregate(*expressions, output_field=None, distinct=False, filter=None, default=None, **extra) [source]
template

Атрибут класса в формате строки, описывающий SQL, который генерируется для этого агрегата. По умолчанию '%(function)s(%(distinct)s%(expressions)s)'.

function

Атрибут класса, описывающий функцию агрегирования, которая будет сгенерирована. Конкретно, function будет интерполирован как function в template. По умолчанию None.

window_compatible

По умолчанию True, так как большинство функций агрегирования могут быть использованы в качестве исходного выражения в Window.

allow_distinct

Атрибут класса, определяющий, разрешено ли передавать distinct ключевой аргумент. Если установлено в значение False (по умолчанию), TypeError поднимается, если distinct=True передается.

empty_result_set_value

По умолчанию None, так как большинство функций агрегирования возвращают NULL при применении к пустому набору результатов.

expressions позиционные аргументы могут включать выражения, преобразования поля модели или имена полей модели. Они будут преобразованы в строку и использованы в качестве expressions в template.

Аргумент distinct определяет, нужно ли вызывать функцию агрегирования для каждого уникального значения expressions (или набора значений для нескольких expressions). Аргумент поддерживается только для агрегатов, у которых allow_distinct установлено в True.

Аргумент filter принимает Q object, который используется для фильтрации строк, которые агрегируются. Смотрите Условное агрегирование и Фильтрование по аннотациям для примеров использования.

Аргумент default принимает значение, которое будет передано вместе с агрегатом в Coalesce. Это полезно для указания значения, которое должно быть возвращено вместо None, когда набор запросов (или группировка) не содержит записей.

**extra kwargs — это пары key=value, которые могут быть интерполированы в template атрибут.

Создание собственных функций агрегирования

Вы также можете создать свои собственные функции агрегирования. Как минимум, вам нужно определить function, но вы также можете полностью настроить генерируемый SQL. Вот краткий пример:

from django.db.models import Aggregate


class Sum(Aggregate):
    # Supports SUM(ALL field).
    function = "SUM"
    template = "%(function)s(%(all_values)s%(expressions)s)"
    allow_distinct = False
    arity = 1

    def __init__(self, expression, all_values=False, **extra):
        super().__init__(expression, all_values="ALL " if all_values else "", **extra)

Value() выражения

value [source]

Объект Value() представляет собой наименьший возможный компонент выражения: простое значение. Если вам нужно представить значение целого числа, булевого значения или строки в рамках выражения, вы можете обернуть это значение в Value().

Вам вряд ли понадобится использовать Value() напрямую. Когда вы пишете выражение F('field') + 1, Django неявно оборачивает 1 в Value(), что позволяет использовать простые значения в более сложных выражениях. Вам потребуется использовать Value(), когда вы хотите передать строку в выражение. Большинство выражений интерпретируют строковый аргумент как имя поля, как Lower('name').

Аргумент value описывает значение, которое должно быть включено в выражение, например, 1, True или None. Django знает, как преобразовать эти значения Python в соответствующий тип базы данных.

Если не указано поле output_field, оно будет определено по типу предоставленного value для многих распространённых типов. Например, передача экземпляра datetime.datetime в качестве value по умолчанию устанавливает output_field в DateTimeField.

ExpressionWrapper() выражения

class ExpressionWrapper(expression, output_field) [source]

ExpressionWrapper оборачивает другое выражение и предоставляет доступ к свойствам, таким как output_field, которые могут быть недоступны для других выражений. ExpressionWrapper необходимо при использовании арифметических операций над F() выражениями с разными типами, как описано в Использование F() с аннотациями.

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

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

Subquery() выражения

class Subquery(queryset, output_field=None) [source]

Вы можете добавить явное подзапрос к QuerySet, используя выражение Subquery.

Например, чтобы аннотировать каждый пост адресом электронной почты автора самого нового комментария к этому посту:

>>> from django.db.models import OuterRef, Subquery
>>> newest = Comment.objects.filter(post=OuterRef("pk")).order_by("-created_at")
>>> Post.objects.annotate(newest_commenter_email=Subquery(newest.values("email")[:1]))

В PostgreSQL SQL будет выглядеть так:

SELECT "post"."id", (
    SELECT U0."email"
    FROM "comment" U0
    WHERE U0."post_id" = ("post"."id")
    ORDER BY U0."created_at" DESC LIMIT 1
) AS "newest_commenter_email" FROM "post"

Примечание

Примеры в этом разделе предназначены для демонстрации того, как заставить Django выполнить подзапрос. В некоторых случаях можно написать эквивалентный запрос, который выполняет ту же задачу более ясно или эффективно.

Ссылка на столбцы из внешнего набора результатов

class OuterRef(field) [source]

Используйте OuterRef, когда набор результатов в Subquery должен ссылаться на поле из внешнего набора результатов или его преобразования. Он работает как выражение F, за исключением того, что проверка на корректность ссылки на поле выполняется только при решении внешнего набора результатов.

Экземпляры OuterRef могут использоваться совместно с вложенными экземплярами Subquery для ссылки на содержащий набор результатов, который не является непосредственным родителем. Например, для корректного разрешения этот набор результатов должен находиться внутри вложенных пар экземпляров Subquery:

>>> Book.objects.filter(author=OuterRef(OuterRef("pk")))

Ограничение подзапроса одним столбцом

Иногда требуется вернуть один столбец из Subquery, например, для использования Subquery в качестве целевого значения __in поиска. Для возврата всех комментариев для постов, опубликованных в течение последнего дня:

>>> from datetime import timedelta
>>> from django.utils import timezone
>>> one_day_ago = timezone.now() - timedelta(days=1)
>>> posts = Post.objects.filter(published_at__gte=one_day_ago)
>>> Comment.objects.filter(post__in=Subquery(posts.values("pk")))

В этом случае подзапрос должен использовать values(), чтобы вернуть только один столбец: первичный ключ поста.

Ограничение подзапроса одной строкой

Для предотвращения возврата подзапросом нескольких строк используется срез ([:1]) набора результатов:

>>> subquery = Subquery(newest.values("email")[:1])
>>> Post.objects.annotate(newest_commenter_email=subquery)

В этом случае подзапрос должен вернуть только один столбец и одну строку: адрес электронной почты самого последнего комментария.

(Использование get() вместо среза привело бы к ошибке, потому что OuterRef нельзя разрешить, пока набор результатов не будет использован в Subquery).

Exists() подзапросы

class Exists(queryset) [source]

Exists — это подкласс Subquery, который использует SQL-выражение EXISTS. Во многих случаях он будет работать быстрее, чем подзапрос, поскольку база данных может остановить вычисление подзапроса при обнаружении первой подходящей строки.

Например, чтобы аннотировать каждый пост значением, указывающим, есть ли у него комментарий за последние сутки:

>>> from django.db.models import Exists, OuterRef
>>> from datetime import timedelta
>>> from django.utils import timezone
>>> one_day_ago = timezone.now() - timedelta(days=1)
>>> recent_comments = Comment.objects.filter(
...     post=OuterRef("pk"),
...     created_at__gte=one_day_ago,
... )
>>> Post.objects.annotate(recent_comment=Exists(recent_comments))

В PostgreSQL SQL будет выглядеть так:

SELECT "post"."id", "post"."published_at", EXISTS(
    SELECT (1) as "a"
    FROM "comment" U0
    WHERE (
        U0."created_at" >= YYYY-MM-DD HH:MM:SS AND
        U0."post_id" = "post"."id"
    )
    LIMIT 1
) AS "recent_comment" FROM "post"

Нет необходимости принудительно заставлять Exists ссылаться на один столбец, так как столбцы отбрасываются, и возвращается булевое значение. Аналогично, так как порядок не важен в подзапросе SQL EXISTS и только ухудшит производительность, он автоматически удаляется.

Вы можете выполнять запросы с помощью NOT EXISTS и ~Exists().

Фильтрация по Subquery() или Exists() выражениям

Subquery(), возвращающее булевое значение, и Exists() могут использоваться в качестве условия condition в выражениях When или для прямой фильтрации набора результатов:

>>> recent_comments = Comment.objects.filter(...)  # From above
>>> Post.objects.filter(Exists(recent_comments))

Это гарантирует, что подзапрос не будет добавлен в столбцы SELECT, что может улучшить производительность.

Использование агрегаций в Subquery выражении

Агрегации могут использоваться в Subquery, но требуют определенной комбинации filter(), values() и annotate(), чтобы правильно сгруппировать подзапрос.

Предположим, что у обеих моделей есть поле length, чтобы найти посты, длина которых больше общей длины всех совокупных комментариев:

>>> from django.db.models import OuterRef, Subquery, Sum
>>> comments = Comment.objects.filter(post=OuterRef("pk")).order_by().values("post")
>>> total_comments = comments.annotate(total=Sum("length")).values("total")
>>> Post.objects.filter(length__gt=Subquery(total_comments))

Начальный filter(...) ограничивает подзапрос соответствующими параметрами. order_by() удаляет стандартный ordering (если таковой имеется) у модели Comment. values('post') агрегирует комментарии по Post. Наконец, annotate(...) выполняет агрегацию. Порядок применения этих методов набора результатов важен. В данном случае, так как подзапрос должен быть ограничен одним столбцом, требуется values('total').

Это единственный способ выполнить агрегацию в Subquery, так как использование aggregate() пытается оценить набор результатов (и если есть OuterRef, это будет невозможно разрешить).

Сырые SQL-выражения

class RawSQL(sql, params, output_field=None) [source]

Иногда базовые выражения базы данных не могут легко выразить сложное WHERE условие. В таких крайних случаях используйте выражение RawSQL. Например:

>>> from django.db.models.expressions import RawSQL
>>> queryset.annotate(val=RawSQL("select col from sometable where othercol = %s", (param,)))

Эти дополнительные запросы могут быть не переносимы на разные СУБД (потому что вы явно пишете код SQL) и нарушают принцип DRY, поэтому следует избегать их, если это возможно.

Выражения RawSQL также могут использоваться в качестве целевого объекта __in фильтров:

>>> queryset.filter(id__in=RawSQL("select id from sometable where col = %s", (param,)))

Предупреждение

Чтобы защититься от атак SQL-инъекции, необходимо экранировать любые параметры, контролируемые пользователем, используя params. params — обязательный аргумент, чтобы вы осознавали, что вы не интерполируете свой SQL с данными, предоставленными пользователем.

Также не следует заключать в кавычки заполнитель в строке SQL. Этот пример уязвим к SQL-инъекции из-за кавычек вокруг %s:

RawSQL("select col from sometable where othercol = '%s'")  # unsafe!

Дополнительную информацию о том, как Django защищает от атак SQL-инъекции, можно найти здесь.

Функции окон

Функции окон предоставляют способ применения функций к разделам. В отличие от обычной агрегационной функции, которая вычисляет конечный результат для каждого набора, определяемого группировкой, функции окон работают с кадрами и разделами и вычисляют результат для каждой строки.

Вы можете указать несколько окон в одном запросе, что в Django ORM эквивалентно включению нескольких выражений в вызов QuerySet.annotate(). ORM не использует именованные окна; вместо этого они являются частью выбранных столбцов.

class Window(expression, partition_by=None, order_by=None, frame=None, output_field=None) [source]
template

По умолчанию %(expression)s OVER (%(window)s). Если предоставлен только аргумент expression, клауза окна будет пустой.

Класс Window является основным выражением для клаузы OVER.

Аргумент expression — это функция окна, агрегационная функция или выражение, совместимое в клаузе окна.

Аргумент partition_by принимает выражение или последовательность выражений (имена столбцов должны быть заключены в объект F), которые контролируют разбиение строк. Разбиение сужает строки, используемые для вычисления результата.

Поле вывода output_field указывается либо как аргумент, либо выражением.

Аргумент order_by принимает выражение, на котором вы можете вызвать asc() и desc(), строку имени поля (с необязательным префиксом "-", указывающим убывающий порядок) или кортеж или список строк и/или выражений. Сортировка управляет порядком применения выражения. Например, если вы суммируете строки в разделе, первый результат — значение первой строки, второй — сумма первой и второй строки.

Параметр frame указывает, какие другие строки должны быть использованы в вычислении. Подробности см. в разделе Кадры.

Например, чтобы добавить к каждому фильму средний рейтинг фильмов того же студийного производства, жанра и года выпуска:

>>> from django.db.models import Avg, F, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...     ),
... )

Это позволяет проверить, оценен ли фильм лучше или хуже, чем его аналоги.

Вы можете применить несколько выражений над одним окном, т. е. одним разделом и одним кадром. Например, можно изменить предыдущий пример, чтобы также включить наилучший и наихудший рейтинг в группе каждого фильма (такой же студийный производством, жанр и год выпуска), используя три функции окна в одном запросе. Раздел и сортировка из предыдущего примера извлекаются в словарь для уменьшения повторений:

>>> from django.db.models import Avg, F, Max, Min, Window
>>> window = {
...     "partition_by": [F("studio"), F("genre")],
...     "order_by": "released__year",
... }
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         **window,
...     ),
...     best=Window(
...         expression=Max("rating"),
...         **window,
...     ),
...     worst=Window(
...         expression=Min("rating"),
...         **window,
...     ),
... )

Фильтрация по функциям окон поддерживается, пока запросы не являются дизъюнктивными (не используется OR или XOR в качестве соединителя) и по отношению к набору запросов, выполняющему агрегирование.

Например, запрос, который зависит от агрегирования и имеет отфильтрованный по функции окна и полю запрос OR, не поддерживается. Применение объединенных предикатов после агрегирования может привести к включению строк, которые обычно исключаются из групп:

>>> qs = Movie.objects.annotate(
...     category_rank=Window(Rank(), partition_by="category", order_by="-rating"),
...     scenes_count=Count("actors"),
... ).filter(Q(category_rank__lte=3) | Q(title__contains="Batman"))
>>> list(qs)
NotImplementedError: Heterogeneous disjunctive predicates against window functions
are not implemented when performing conditional aggregation.

Среди встроенных баз данных Django MySQL, PostgreSQL и Oracle поддерживают выражения окон. Поддержка различных функций выражений окон варьируется в разных базах данных. Например, параметры в asc() и desc() могут не поддерживаться. При необходимости обратитесь к документации вашей базы данных.

Кадры

Для кадра окна вы можете выбрать либо последовательность строк на основе диапазона, либо обычную последовательность строк.

class ValueRange(start=None, end=None, exclusion=None) [source]
frame_type

Это атрибут, установленный как 'RANGE'.

PostgreSQL имеет ограниченную поддержку ValueRange и поддерживает только использование стандартных начальных и конечных точек, таких как CURRENT ROW и UNBOUNDED FOLLOWING.

Изменено в Django 5.1:

Добавлен аргумент exclusion.

class RowRange(start=None, end=None, exclusion=None) [source]
frame_type

Этот атрибут установлен как 'ROWS'.

Изменено в Django 5.1:

Добавлен аргумент exclusion.

Оба класса возвращают SQL с шаблоном:

%(frame_type)s BETWEEN %(start)s AND %(end)s
class WindowFrameExclusion [source]
Добавлена в Django 5.1.
CURRENT_ROW
GROUP
TIES
NO_OTHERS

Аргумент exclusion позволяет исключать строки (CURRENT_ROW), группы (GROUP) и связи (TIES) из оконных кадров в поддерживаемых базах данных:

%(frame_type)s BETWEEN %(start)s AND %(end)s EXCLUDE %(exclusion)s

Кадры сужают строки, используемые для вычисления результата. Они смещаются от некоторой начальной точки до указанной конечной точки. Кадры могут использоваться с разделами и без них, но часто полезно указать порядок окна для обеспечения детерминированного результата. В кадре строка-аналог в кадре — это строка с эквивалентным значением или все строки, если клауза сортировки отсутствует.

Начальная точка кадра по умолчанию — UNBOUNDED PRECEDING, что является первой строкой раздела. Конечная точка всегда явно включена в SQL, генерируемый ORM, и по умолчанию равна UNBOUNDED FOLLOWING. По умолчанию кадр включает все строки от раздела до последней строки в наборе.

Приемлемые значения для аргументов start и end — None, целое число или ноль. Отрицательное целое число для start приводит к N PRECEDING, а None приводит к UNBOUNDED PRECEDING. В режиме ROWS положительное целое число можно использовать для start, что приводит к N FOLLOWING. Положительные целые числа принимаются для end и приводят к N FOLLOWING. В режиме ROWS отрицательное целое число можно использовать для end, что приводит к N PRECEDING. Для обоих start и end ноль вернёт CURRENT ROW.

Существует разница в том, что включает CURRENT ROW. При указании в режиме ROWS кадр начинается или заканчивается текущей строкой. При указании в режиме RANGE кадр начинается или заканчивается первой или последней строкой-аналогом в соответствии с клаузой сортировки. Таким образом, RANGE CURRENT ROW вычисляет выражение для строк, которые имеют одинаковое значение, указанное клаузой сортировки. Поскольку шаблон включает как start, так и end точки, это можно выразить следующим образом:

ValueRange(start=0, end=0)

Если «строки-аналоги» фильма описываются как фильмы, выпущенные той же студией в том же жанре в том же году, этот пример RowRange добавляет к каждому фильму средний рейтинг двух предыдущих и двух последующих строк-аналогов:

>>> from django.db.models import Avg, F, RowRange, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...         frame=RowRange(start=-2, end=2),
...     ),
... )

Если поле базы данных поддерживается, вы можете указать начальную и конечную точки на основе значений выражения в разделе. Если поле released модели Movie хранит месяц выпуска каждого фильма, этот пример ValueRange добавляет к каждому фильму средний рейтинг строк-аналогов, выпущенных за 12 месяцев до и 12 месяцев после каждого фильма:

>>> from django.db.models import Avg, F, ValueRange, Window
>>> Movie.objects.annotate(
...     avg_rating=Window(
...         expression=Avg("rating"),
...         partition_by=[F("studio"), F("genre")],
...         order_by="released__year",
...         frame=ValueRange(start=-12, end=12),
...     ),
... )
Изменено в Django 5.1:

Добавлена поддержка положительных целых чисел start и отрицательных целых чисел end для RowRange.

Технические сведения

Ниже приведены технические детали реализации, которые могут быть полезны авторам библиотек. Технический API и примеры ниже помогут при создании универсальных выражений запросов, которые могут расширить встроенную функциональность, предоставляемую Django.

API выражений

Выражения запросов реализуют API выражений запроса, но также предоставляют ряд дополнительных методов и атрибутов, перечисленных ниже. Все выражения запросов должны наследоваться от Expression() или соответствующего подкласса.

Когда выражение запроса оборачивает другое выражение, оно отвечает за вызов соответствующих методов обернутого выражения.

class Expression [source]
allowed_default

Сообщает Django, что это выражение может использоваться в Field.db_default. По умолчанию False.

constraint_validation_compatible
Новое в Django 5.1.

Сообщает Django, что это выражение может использоваться при валидации ограничений. Выражения с constraint_validation_compatible, установленным в False, должны иметь только одно исходное выражение. По умолчанию True.

contains_aggregate

Сообщает Django, что это выражение содержит агрегат и что необходимо добавить предложение GROUP BY в запрос.

contains_over_clause

Сообщает Django, что это выражение содержит выражение Window. Оно используется, например, для запрета выражений оконных функций в запросах, которые изменяют данные.

filterable

Сообщает Django, что это выражение может быть упомянуто в QuerySet.filter(). По умолчанию True.

window_compatible

Сообщает Django, что это выражение может использоваться в качестве исходного выражения в Window. По умолчанию False.

empty_result_set_value

Сообщает Django, какое значение должно возвращаться, когда выражение используется для применения функции к пустому набору результатов. По умолчанию NotImplemented, что заставляет выражение вычисляться в базе данных.

set_returning
Новое в Django 5.2.

Сообщает Django, что это выражение содержит функцию возвращающую множество, принуждая к вычислению подзапроса. Используется, например, для разрешения некоторых функций Postgres, возвращающих множества (например, JSONB_PATH_QUERY, UNNEST и т.д.), чтобы они пропускали оптимизацию и правильно вычислялись, когда аннотации порождают строки сами. По умолчанию False.

allows_composite_expressions
Новое в Django 5.2.

Сообщает Django, что это выражение допускает составные выражения, например, для поддержки составных первичных ключей. По умолчанию False.

resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)

Предоставляет возможность выполнить предварительную обработку или валидацию выражения перед его добавлением в запрос. resolve_expression() также должен быть вызван для всех вложенных выражений. Должен быть возвращен copy() вида self с необходимыми преобразованиями.

query — реализация запроса на уровне бэкенда.

allow_joins — булево значение, разрешающее или запрещающее использование соединений в запросе.

reuse — набор повторно используемых соединений для сценариев с несколькими соединениями.

summarize — булево значение, которое, когда True, сигнализирует, что вычисляемый запрос — это терминальный агрегирующий запрос.

for_save — булево значение, которое, когда True, сигнализирует, что выполняемый запрос выполняет создание или обновление.

get_source_expressions()

Возвращает упорядоченный список внутренних выражений. Например:

>>> Sum(F("foo")).get_source_expressions()
[F('foo')]
set_source_expressions(expressions)

Принимает список выражений и хранит их так, чтобы get_source_expressions() мог их вернуть.

relabeled_clone(change_map)

Возвращает клон (копию) self с переименованными именами столбцов. Имена столбцов переименовываются при создании подзапросов. relabeled_clone() также должен быть вызван для всех вложенных выражений и присвоен клону.

change_map — словарь, сопоставляющий старые имена столбцов с новыми.

Пример:

def relabeled_clone(self, change_map):
    clone = copy.copy(self)
    clone.expression = self.expression.relabeled_clone(change_map)
    return clone
convert_value(value, expression, connection)

Створка, позволяющая выражению преобразовать value в более подходящий тип.

expression эквивалентно self.

get_group_by_cols()

Отвечает за возврат списка столбцов, на которые ссылается это выражение. get_group_by_cols() должен быть вызван для всех вложенных выражений. Объекты F(), в частности, хранят ссылку на столбец.

asc(nulls_first=None, nulls_last=None)

Возвращает выражение, готовое для сортировки в порядке возрастания.

nulls_first и nulls_last определяют, как сортируются нулевые значения. См. Использование F() для сортировки нулевых значений для примера использования.

desc(nulls_first=None, nulls_last=None)

Возвращает выражение, готовое для сортировки в порядке убывания.

nulls_first и nulls_last определяют, как сортируются нулевые значения. См. Использование F() для сортировки нулевых значений для примера использования.

reverse_ordering()

Возвращает self с любыми необходимыми изменениями для изменения порядка сортировки в вызове order_by. Например, выражение, реализующее NULLS LAST, изменит своё значение на NULLS FIRST. Изменения необходимы только для выражений, которые реализуют порядок сортировки, таких как OrderBy. Этот метод вызывается при вызове reverse() на наборе запросов.

END_OF_DOCUMENT_MARKER

Создание собственных выражений запросов

Вы можете создавать собственные классы выражений запросов, которые используют и могут интегрироваться с другими выражениями запросов. Давайте рассмотрим пример, написав реализацию функции SQL COALESCE, не используя встроенные выражения Func().

Функция SQL COALESCE определена как принимающая список столбцов или значений. Она вернёт первый столбец или значение, которое не является NULL.

Начнём с определения шаблона для генерации SQL и метода __init__() для установки некоторых атрибутов:

import copy
from django.db.models import Expression


class Coalesce(Expression):
    template = "COALESCE( %(expressions)s )"

    def __init__(self, expressions, output_field):
        super().__init__(output_field=output_field)
        if len(expressions) < 2:
            raise ValueError("expressions must have at least 2 elements")
        for expression in expressions:
            if not hasattr(expression, "resolve_expression"):
                raise TypeError("%r is not an Expression" % expression)
        self.expressions = expressions

Мы выполняем базовую валидацию параметров, включая требование по меньшей мере 2 столбцов или значений и проверку того, что они являются выражениями. Здесь требуется output_field, чтобы Django знал, к какому типу поля модели назначить конечный результат.

Теперь реализуем предобработку и валидацию. Поскольку у нас пока нет собственной валидации, мы делегируем её вложенным выражениям:

def resolve_expression(
    self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
):
    c = self.copy()
    c.is_summary = summarize
    for pos, expression in enumerate(self.expressions):
        c.expressions[pos] = expression.resolve_expression(
            query, allow_joins, reuse, summarize, for_save
        )
    return c

Далее, мы напишем метод, отвечающий за генерацию SQL:

def as_sql(self, compiler, connection, template=None):
    sql_expressions, sql_params = [], []
    for expression in self.expressions:
        sql, params = compiler.compile(expression)
        sql_expressions.append(sql)
        sql_params.extend(params)
    template = template or self.template
    data = {"expressions": ",".join(sql_expressions)}
    return template % data, sql_params


def as_oracle(self, compiler, connection):
    """
    Example of vendor specific handling (Oracle in this case).
    Let's make the function name lowercase.
    """
    return self.as_sql(compiler, connection, template="coalesce( %(expressions)s )")

Методы as_sql() могут поддерживать пользовательские ключевые аргументы, позволяющие методам as_vendorname() переопределять данные, используемые для генерации строки SQL. Использование ключевых аргументов as_sql() для кастомизации предпочтительнее, чем изменение self внутри методов as_vendorname(), так как последнее может приводить к ошибкам при работе с различными бэкендами баз данных. Если ваш класс полагается на атрибуты класса для определения данных, рассмотрите возможность разрешить переопределения в методе as_sql().

Мы генерируем SQL для каждого из expressions, используя метод compiler.compile(), и объединяем результат запятыми. Затем шаблон заполняется нашими данными, и возвращаются SQL и параметры.

Мы также определили пользовательскую реализацию, специфичную для бэкэнда Oracle. Функция as_oracle() будет вызвана вместо as_sql(), если используется бэкенд Oracle.

Наконец, мы реализуем остальные методы, которые позволяют нашему выражению запроса работать с другими выражениями запросов:

def get_source_expressions(self):
    return self.expressions


def set_source_expressions(self, expressions):
    self.expressions = expressions

Посмотрим, как это работает:

>>> from django.db.models import F, Value, CharField
>>> qs = Company.objects.annotate(
...     tagline=Coalesce(
...         [F("motto"), F("ticker_name"), F("description"), Value("No Tagline")],
...         output_field=CharField(),
...     )
... )
>>> for c in qs:
...     print("%s: %s" % (c.name, c.tagline))
...
Google: Do No Evil
Apple: AAPL
Yahoo: Internet Company
Django Software Foundation: No Tagline

Избегание SQL-инъекций

Поскольку ключевые аргументы Func для __init__() (**extra) и as_sql() (**extra_context) интерполируются в строку SQL, а не передаются как параметры запроса (где драйвер базы данных их экранирует), они не должны содержать небезопасные данные пользователя.

Например, если substring предоставляется пользователем, эта функция уязвима для SQL-инъекций:

from django.db.models import Func


class Position(Func):
    function = "POSITION"
    template = "%(function)s('%(substring)s' in %(expressions)s)"

    def __init__(self, expression, substring):
        # substring=substring is an SQL injection vulnerability!
        super().__init__(expression, substring=substring)

Эта функция генерирует строку SQL без параметров. Поскольку substring передаётся в super().__init__() в качестве ключевого аргумента, она интерполируется в строку SQL до отправки запроса в базу данных.

Вот исправленная версия:

class Position(Func):
    function = "POSITION"
    arg_joiner = " IN "

    def __init__(self, expression, substring):
        super().__init__(substring, expression)

Передав substring в качестве позиционного аргумента, он будет передан как параметр в запросе к базе данных.

Добавление поддержки в сторонних бэкендах баз данных

Если вы используете бэкенд базы данных, который использует другой синтаксис SQL для определённой функции, вы можете добавить поддержку, подменяя новый метод в классе функции.

Допустим, мы пишем бэкенд для SQL Server Microsoft, который использует SQL LEN вместо LENGTH для функции Length. Мы подменяем новый метод, называемый as_sqlserver(), в классе Length:

from django.db.models.functions import Length


def sqlserver_length(self, compiler, connection):
    return self.as_sql(compiler, connection, function="LEN")


Length.as_sqlserver = sqlserver_length

Вы также можете настроить SQL, используя параметр template функции as_sql().

Мы используем as_sqlserver(), потому что django.db.connection.vendor возвращает sqlserver для бэкэнда.

Сторонние бэкэнды могут регистрировать свои функции в файле верхнего уровня __init__.py пакета бэкэнда или в файле (или пакете) верхнего уровня expressions.py, импортируемом из файла верхнего уровня __init__.py.

Для пользовательских проектов, желающих подменить используемый бэкенд, этот код должен находиться в методе AppConfig.ready().

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

Spec-Zone.ru

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