Выражения запросов
Выражения запросов описывают значение или вычисление, которое можно использовать в обновлении, создании, фильтрации, сортировке, аннотации или агрегировании. Если выражение возвращает логическое значение, его можно напрямую использовать в фильтрах. Существует ряд встроенных выражений (описанных ниже), которые помогут вам составлять запросы. Выражения можно комбинировать, а в некоторых случаях и вкладывать друг в друга, чтобы формировать более сложные вычисления.
Поддерживаемые арифметические операции
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")))
>>> 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[исходный код]
Объект 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.
Python никогда не узнаёт, какое значение было или есть у reporter.stories_filed, — с ним полностью работает база данных. Всё, что делает Python с помощью класса F() Django, — создаёт синтаксис SQL для обращения к полю и описания операции.
Помимо использования в операциях с отдельными экземплярами, как описано выше, F() можно использовать с update() для массового обновления QuerySet. Это позволяет сократить два запроса, которые мы выполняли выше, — 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()
Для строковых полей, текстовых полей и 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.name
'riya'
Предотвращение состояния гонки с помощью F()
Ещё одно полезное преимущество F() заключается в том, что обновление значения поля базой данных, а не Python, позволяет избежать состояния гонки.
Если два потока Python выполнят код из первого примера выше, один из них может получить, увеличить и сохранить значение поля после того, как второй поток уже получил его из базы данных. Значение, сохранённое вторым потоком, будет основано на исходном значении; результат работы первого потока будет потерян.
Если за обновление поля отвечает база данных, процесс становится надёжнее: поле будет обновлено на основе его значения в базе данных на момент выполнения save() или update(), а не на основе значения на момент получения экземпляра.
Присваивания F() обновляются после Model.save()
Объекты F(), присвоенные полям модели, обновляются из базы данных при вызове save() на поддерживающих это бэкендах (SQLite, PostgreSQL и Oracle) без дополнительного запроса; на остальных бэкендах обновление откладывается (MySQL или MariaDB). Например:
>>> reporter = Reporters.objects.get(name="Tintin")
>>> reporter.stories_filed = F("stories_filed") + 1
>>> reporter.save()
>>> reporter.stories_filed # This triggers a refresh query on MySQL/MariaDB.
14 # Assuming the database value was 13 when the object was saved.
В предыдущих версиях Django объекты F() не обновлялись из базы данных при вызове save(). В результате они вычислялись и сохранялись при каждом сохранении экземпляра.
Использование F() в фильтрах
F() также очень полезны в фильтрах QuerySet, где позволяют фильтровать набор объектов по критериям, основанным на значениях их полей, а не на значениях Python.
Это описано в разделе Использование выражений F() в запросах.
Использование F() с аннотациями
С помощью F() можно создавать динамические поля в моделях, комбинируя разные поля с помощью арифметических операций:
company = Company.objects.annotate(chairs_needed=F("num_employees") - F("num_chairs"))
Если объединяемые поля имеют разные типы, необходимо указать Django, поле какого типа будет возвращено. Большинство выражений поддерживает для этого output_field, но поскольку 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"
Список встроенных функций базы данных см. в разделе Функции базы данных.
API Func выглядит следующим образом:
-
class Func(*expressions, **extra)[исходный код] -
-
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)[исходный код] -
Генерирует фрагмент SQL для функции базы данных. Возвращает кортеж
(sql, params), гдеsql— строка SQL, аparams— список или кортеж параметров запроса.Методы
as_vendor()должны использовать параметрыfunction,template,arg_joinerи любые другие параметры**extra_contextдля настройки SQL по мере необходимости. Например:django/db/models/functions.pyclass 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 — это пары key=value, которые можно подставить в атрибут template. Чтобы избежать уязвимости SQL-инъекции, extra не должны содержать недоверенные пользовательские данные, поскольку эти значения подставляются в строку SQL, а не передаются как параметры запроса, которые драйвер базы данных экранировал бы.
Ключевые аргументы function, template и arg_joiner позволяют заменить атрибуты с такими же именами без создания собственного класса. Аргумент output_field позволяет определить ожидаемый тип возвращаемого значения.
Выражения Aggregate()
Выражение агрегирования — это частный случай выражения Func(), которое сообщает запросу о необходимости предложения GROUP BY. Все функции агрегирования, например Sum() и Count(), наследуются от Aggregate().
Поскольку Aggregate являются выражениями и оборачивают выражения, с их помощью можно представить сложные вычисления:
from django.db.models import Count
Company.objects.annotate(
managers_required=(Count("num_employees") / 4) + Count("num_managers")
)
API Aggregate выглядит следующим образом:
-
class Aggregate(*expressions, output_field=None, distinct=False, filter=None, default=None, order_by=None, **extra)[исходный код] -
-
template -
Атрибут класса, представляющий собой строку формата и описывающий SQL, генерируемый для этого агрегата. По умолчанию —
'%(function)s(%(distinct)s%(expressions)s)'.
-
function -
Атрибут класса, описывающий генерируемую функцию агрегирования. В частности,
functionподставляется в качестве заполнителяfunctionвtemplate. По умолчанию —None.
-
window_compatible -
По умолчанию —
True, поскольку большинство функций агрегирования можно использовать как исходное выражение вWindow.
-
allow_distinct -
Атрибут класса, определяющий, допускает ли эта функция агрегирования передачу именованного аргумента
distinct. Если установлено значениеFalse(по умолчанию), при передачеTypeErrorвызывается исключениеdistinct=True.
-
allow_order_by -
Добавлено в Django 6.0.
Атрибут класса, определяющий, допускает ли эта функция агрегирования передачу именованного аргумента
order_by. Если установлено значениеFalse(по умолчанию), при передачеTypeErrorсо значением, отличным отorder_by, вызывается исключениеNone.
-
empty_result_set_value -
По умолчанию —
None, поскольку большинство функций агрегирования возвращаютNULLпри применении к пустому набору результатов.
-
Позиционные аргументы expressions могут включать выражения, преобразования поля модели или имена полей модели. Они преобразуются в строку и используются в качестве заполнителя expressions в template.
Аргумент distinct определяет, должна ли функция агрегирования применяться к каждому уникальному значению expressions (или к набору значений, если указано несколько expressions). Этот аргумент поддерживается только агрегатами, у которых для allow_distinct установлено значение True.
Аргумент filter принимает объект Q object, используемый для фильтрации строк, участвующих в агрегировании. Примеры использования см. в разделах Условное агрегирование и Фильтрация по аннотациям.
Аргумент order_by работает аналогично аргументу field_names функции order_by(): он принимает имя поля (с необязательным префиксом "-", указывающим на сортировку по убыванию) или выражение (либо кортеж или список строк и/или выражений), задающее порядок элементов в результате.
Аргумент default принимает значение, которое будет передано вместе с агрегатом в Coalesce. Это удобно, если нужно указать значение, возвращаемое вместо None, когда в наборе запросов (или группе) нет записей.
Именованные аргументы **extra — это пары key=value, которые можно подставить в атрибут template.
Добавлен аргумент order_by.
Создание собственных функций агрегирования
Вы также можете создавать собственные функции агрегирования. Как минимум необходимо определить 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()
-
class Value(value, output_field=None)[исходный код]
Объект 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)[исходный код]
ExpressionWrapper оборачивает другое выражение и предоставляет доступ к свойствам, например output_field, которые могут быть недоступны у других выражений. ExpressionWrapper необходим при выполнении арифметических операций над выражениями F() разных типов, как описано в разделе Использование F() с аннотациями.
Условные выражения
Условные выражения позволяют использовать в запросах логику if … elif … else. Django изначально поддерживает выражения SQL CASE. Подробнее см. в разделе Условные выражения.
Subquery() expressions
-
class Subquery(queryset, output_field=None)[источник]
Вы можете добавить явный подзапрос в 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 выполнить подзапрос. В некоторых случаях можно написать эквивалентный queryset, который выполнит ту же задачу более понятно или эффективно.
Ссылки на столбцы внешнего queryset
-
class OuterRef(field)[источник]
Используйте OuterRef, если queryset в Subquery должен ссылаться на поле внешнего запроса или его преобразование. Это выражение работает подобно F, за исключением того, что проверка на наличие допустимого поля выполняется только после разрешения внешнего queryset.
Экземпляры OuterRef можно использовать вместе с вложенными экземплярами Subquery, чтобы ссылаться на содержащий queryset, который не является непосредственным родительским. Например, этот queryset должен находиться внутри вложенной пары экземпляров 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(), чтобы вернуть только один столбец: первичный ключ публикации.
Ограничение подзапроса одной строкой
Чтобы подзапрос не возвращал несколько строк, используется срез queryset ([:1]):
>>> subquery = Subquery(newest.values("email")[:1])
>>> Post.objects.annotate(newest_commenter_email=subquery)
В этом случае подзапрос должен возвращать только один столбец и одну строку: адрес электронной почты автора самого недавно созданного комментария.
(Использование get() вместо среза приведёт к ошибке, поскольку OuterRef нельзя разрешить, пока queryset не используется внутри Subquery.)
Exists() подзапросы
-
class Exists(queryset)[источник]
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 или для непосредственной фильтрации queryset:
>>> 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(...) выполняет агрегирование. Порядок применения этих методов queryset важен. В этом случае требуется values('total'), поскольку подзапрос должен быть ограничен одним столбцом.
Это единственный способ выполнить агрегирование внутри Subquery, поскольку вызов aggregate() пытается вычислить queryset (а если имеется OuterRef, разрешить его будет невозможно).
Выражения с необработанным SQL
-
class RawSQL(sql, params, output_field=None)[источник]
Иногда с помощью выражений базы данных сложно задать сложное предложение 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-кода.
Оконные функции
Оконные функции позволяют применять функции к разделам. В отличие от обычной агрегатной функции, которая вычисляет итоговый результат для каждой группы, определённой предложением group by, оконные функции работают с рамками и разделами и вычисляют результат для каждой строки.
В одном запросе можно задать несколько окон; в Django ORM это эквивалентно добавлению нескольких выражений в вызов QuerySet.annotate(). ORM не использует именованные окна: вместо этого они входят в список выбираемых столбцов.
-
class Window(expression, partition_by=None, order_by=None, frame=None, output_field=None)[источник] -
-
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 в качестве оператора) и применяются к queryset, выполняющему агрегирование.
Например, не поддерживается запрос с агрегированием, в котором фильтр с оператором 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)[источник] -
-
frame_type -
Этот атрибут имеет значение
'RANGE'.
Поддержка
ValueRangeв PostgreSQL ограничена: поддерживаются только стандартные начальная и конечная точки, напримерCURRENT ROWиUNBOUNDED FOLLOWING. -
-
class RowRange(start=None, end=None, exclusion=None)[источник] -
-
frame_type -
Этот атрибут имеет значение
'ROWS'.
-
Оба класса возвращают SQL с таким шаблоном:
%(frame_type)s BETWEEN %(start)s AND %(end)s
-
class WindowFrameExclusion[источник] -
-
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 добавляет к каждому фильму средний рейтинг аналогов, выпущенных в период от двенадцати месяцев до и до двенадцати месяцев после даты выхода этого фильма:
>>> 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),
... ),
... )
Техническая информация
Ниже приведены сведения о технической реализации, которые могут быть полезны авторам библиотек. Описания технического API и примеры ниже помогут создавать универсальные выражения запросов, расширяющие встроенные возможности Django.
API выражений
Выражения запросов реализуют API выражений запросов, а также предоставляют ряд дополнительных методов и атрибутов, перечисленных ниже. Все выражения запросов должны наследоваться от Expression() или соответствующего подкласса.
Если выражение запроса оборачивает другое выражение, оно должно вызывать соответствующие методы обёрнутого выражения.
-
class Expression[исходный код] -
-
allowed_default -
Сообщает Django, что это выражение можно использовать в
Field.db_default. По умолчанию —False.
-
constraint_validation_compatible -
Сообщает 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определяют порядок сортировки значений NULL. Пример использования см. в разделе Сортировка значений NULL с помощью F().
-
desc(nulls_first=None, nulls_last=None) -
Возвращает выражение, подготовленное для сортировки по убыванию.
Параметры
nulls_firstиnulls_lastопределяют порядок сортировки значений NULL. Пример использования см. в разделе Сортировка значений NULL с помощью F().
-
reverse_ordering() -
Возвращает
selfс необходимыми изменениями для обратной сортировки при вызовеorder_by. Например, выражение, реализующееNULLS LAST, изменит своё значение наNULLS FIRST. Изменения требуются только для выражений, задающих порядок сортировки, таких какOrderBy. Этот метод вызывается при вызовеreverse()для набора запросов.
-
Создание собственных выражений запросов
Можно создавать собственные классы выражений запросов, использующие другие выражения и интегрирующиеся с ними. Рассмотрим пример реализации SQL-функции COALESCE без использования встроенных выражений Func().
SQL-функция COALESCE принимает список столбцов или значений. Она возвращает первый столбец или значение, отличное от NULL.
Сначала определим шаблон для генерации SQL и метод __init__() для задания некоторых атрибутов:
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
Мы выполняем базовую проверку параметров: требуем не менее двух столбцов или значений и проверяем, что они являются выражениями. Здесь мы требуем указать 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, tuple(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. Если используется серверная часть Oracle, вместо as_sql() будет вызвана функция as_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-инъекций в выражениях запросов
Поскольку именованные аргументы __init__() (**extra) и as_sql() (**extra_context) метода Func подставляются непосредственно в строку 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 для определённой функции, можно добавить поддержку, динамически добавив новый метод в класс этой функции.
Предположим, мы пишем серверную часть для Microsoft SQL Server, которая использует SQL LEN вместо LENGTH для функции Length. Добавим в класс Length новый метод с именем as_sqlserver():
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/6.0/ref/models/expressions/