Выражения запросов
Выражения запросов описывают значение или вычисление, которое может использоваться в качестве части обновления, создания, фильтрации, сортировки, аннотации или агрегации. Когда выражение возвращает булево значение, оно может быть использовано непосредственно в фильтрах. Существует ряд встроенных выражений (документированных ниже), которые могут быть использованы для написания запросов. Выражения могут быть объединены или в некоторых случаях вложены, чтобы сформировать более сложные вычисления.
Поддерживаемая арифметика
Django поддерживает отрицание, сложение, вычитание, умножение, деление, остаток от деления, возведение в степень над выражениями запросов, используя Python-константы, переменные и даже другие выражения.
Примеры
from django.db.models import Count, F, Value
from django.db.models.functions import Length, Upper
# 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
Company.objects.filter(
Exists(Employee.objects.filter(company=OuterRef('pk'), salary__gt=10))
)
Встроенные выражения
Примечание
Эти выражения определены в 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.
Любое значение, которое было или есть на reporter.stories_filed, Python никогда не узнает — это обрабатывается исключительно базой данных. Все, что делает Python через класс F() Django, — это создаёт 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.all().update(stories_filed=F('stories_filed') + 1)
F() поэтому может предлагать преимущества производительности за счёт:
- передачи работы базе данных, а не Python
- сокращения количества запросов, необходимых для некоторых операций
Избегание гонок при использовании 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() не поддерживает напрямую output_field, вам необходимо обернуть выражение в 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))
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 — пары «ключ-значение», которые могут быть интерполированы в атрибут 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, **extra) -
-
template -
Атрибут класса в формате строки, описывающий SQL, генерируемый для этой агрегации. По умолчанию
'%(function)s(%(distinct)s%(expressions)s)'.
-
function -
Атрибут класса, описывающий функцию агрегирования, которая будет сгенерирована. В частности,
functionбудет интерполирован как заполнительfunctionвнутриtemplate. По умолчаниюNone.
-
window_compatible -
По умолчанию
True, так как большинство функций агрегирования могут быть использованы в качестве исходного выражения вWindow.
-
allow_distinct -
Новое в Django 2.2.
Атрибут класса, определяющий, разрешает ли эта функция агрегирования передачу ключевого аргумента
distinct. Если установленоFalse(по умолчанию),TypeErrorбудет поднята, еслиdistinct=Trueпередано.
-
Позиционные аргументы expressions могут включать выражения или имена полей модели. Они будут преобразованы в строку и использованы как заполнитель expressions внутри template.
Аргумент output_field требует экземпляра поля модели, например, IntegerField() или BooleanField(), в который Django загрузит значение после извлечения его из базы данных. Обычно при создании экземпляра поля модели аргументы, относящиеся к проверке данных (max_length, max_digits, и т. д.), не будут применены к значению результата выражения.
Обратите внимание, что output_field требуется только тогда, когда Django не может определить, какого типа должен быть результат. Сложные выражения, которые смешивают типы полей, должны определять желаемый output_field.
Например, при сложении IntegerField() и FloatField() вместе, вероятно, следует определить output_field=FloatField().
Аргумент distinct определяет, следует ли вызывать функцию агрегирования для каждого уникального значения expressions (или набора значений для нескольких expressions).
Аргумент поддерживается только для агрегатов, у которых allow_distinct установлено в True.
Аргумент filter принимает Q object, который используется для фильтрации строк, которые агрегируются. См. Условную агрегацию и Фильтрацию по аннотациям для примеров использования.
Ключевые параметры **extra — пары «ключ-значение», которые могут быть интерполированы в атрибут template.
Были добавлены атрибут allow_distinct и аргумент distinct.
Создание собственных функций агрегирования
Вы также можете создавать свои собственные функции агрегирования. Как минимум, вам нужно определить 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
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.
Аргумент output_field должен быть экземпляром поля модели, например, IntegerField() или BooleanField(), в который Django загрузит значение после извлечения его из базы данных. Обычно при создании экземпляра поля модели аргументы, относящиеся к проверке данных (max_length, max_digits, и т. д.), не будут применены к значению результата выражения.
ExpressionWrapper() выражения
-
class ExpressionWrapper(expression, output_field)
ExpressionWrapper окружает другое выражение и предоставляет доступ к свойствам, таким как output_field, которые могут быть недоступны для других выражений. ExpressionWrapper необходимо при использовании арифметических операций с F() выражениями разных типов, как описано в Использование F() с аннотациями.
Условные выражения
Условные выражения позволяют использовать логику if … elif … else в запросах. Django напрямую поддерживает SQL CASE выражения. Дополнительные сведения см. в Условных выражениях.
Subquery() выражения
-
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 выполнить подзапрос. В некоторых случаях можно написать эквивалентный набор запросов, который выполняет ту же задачу более наглядно или эффективно.
Ссылка на столбцы из внешнего набора запросов
-
class OuterRef(field)
Используйте 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)
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 U0."id", U0."post_id", U0."email", U0."created_at"
FROM "comment" U0
WHERE (
U0."created_at" >= YYYY-MM-DD HH:MM:SS AND
U0."post_id" = ("post"."id")
)
) 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 столбцы, что может привести к лучшей производительности.
В предыдущих версиях Django необходимо было сначала добавить аннотацию, а затем отфильтровать по аннотации. В результате анотированное значение всегда присутствовало в результате запроса, и часто это приводило к запросу, который выполнялся дольше.
Использование агрегатов в 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)
Иногда выражения базы данных не могут легко выразить сложное условие WHERE. В этих случаях используйте выражение RawSQL. Например:
>>> from django.db.models.expressions import RawSQL
>>> queryset.annotate(val=RawSQL("select col from sometable where othercol = %s", (someparam,)))
Эти дополнительные поиски могут быть не портативными для разных баз данных (потому что вы явно пишете SQL-код) и нарушают принцип DRY, поэтому следует избегать их, если это возможно.
Предупреждение
Для защиты от атак с помощью внедрения SQL необходимо экранировать любые параметры, которые может контролировать пользователь, используя params. params является обязательным аргументом, чтобы заставить вас признать, что вы не интерполируете свой SQL с данными, предоставленными пользователем.
Также не следует заключать в кавычки плейсхолдеры в строке SQL. Этот пример уязвим для внедрения SQL из-за кавычек вокруг %s:
RawSQL("select col from sometable where othercol = '%s'") # unsafe!
Дополнительные сведения о том, как работает защита от внедрения SQL в Django, можно найти в разделе Защита от внедрения SQL.
Функции окон
Функции окон обеспечивают способ применения функций к разделам. В отличие от обычной функции агрегирования, которая вычисляет окончательный результат для каждого набора, определённого группировкой, функции окон работают с кадрами и разделами и вычисляют результат для каждой строки.
Вы можете указать несколько окон в одном запросе, что в Django ORM эквивалентно включению нескольких выражений в вызов QuerySet.annotate(). ORM не использует именованные окна, вместо этого они являются частью выбранных столбцов.
-
class Window(expression, partition_by=None, order_by=None, frame=None, output_field=None) -
-
filterable -
По умолчанию
False. Стандарт SQL не допускает ссылки на функции окон вWHEREи Django поднимает исключение при построенииQuerySet, которое будет это делать.
-
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
>>> from django.db.models.functions import ExtractYear
>>> Movie.objects.annotate(
>>> avg_rating=Window(
>>> expression=Avg('rating'),
>>> partition_by=[F('studio'), F('genre')],
>>> order_by=ExtractYear('released').asc(),
>>> ),
>>> )
Это позволяет проверить, выше или ниже рейтинг фильма по сравнению со своими коллегами.
Возможно, вам захочется применить несколько выражений к одному окну, т.е. к одному разделу и фрейму. Например, вы можете изменить предыдущий пример, чтобы он также включал наилучший и наихудший рейтинг в каждой группе фильмов (одинаковая киностудия, жанр и год выпуска), используя три функции окна в одном запросе. Раздел и порядок из предыдущего примера извлекаются в словарь для уменьшения повторений:
>>> from django.db.models import Avg, F, Max, Min, Window
>>> from django.db.models.functions import ExtractYear
>>> window = {
>>> 'partition_by': [F('studio'), F('genre')],
>>> 'order_by': ExtractYear('released').asc(),
>>> }
>>> Movie.objects.annotate(
>>> avg_rating=Window(
>>> expression=Avg('rating'), **window,
>>> ),
>>> best=Window(
>>> expression=Max('rating'), **window,
>>> ),
>>> worst=Window(
>>> expression=Min('rating'), **window,
>>> ),
>>> )
Среди встроенных баз данных Django, MySQL 8.0.2+, PostgreSQL и Oracle поддерживают выражения окон. Поддержка различных функций выражений окон различается в разных базах данных. Например, параметры в asc() и desc() могут не поддерживаться. При необходимости обратитесь к документации вашей базы данных.
Фреймы
Для фрейма окна можно выбрать либо последовательность строк на основе диапазона, либо обычную последовательность строк.
-
class ValueRange(start=None, end=None) -
-
frame_type -
Это атрибут, установленный в
'RANGE'.
PostgreSQL имеет ограниченную поддержку
ValueRangeи поддерживает только использование стандартных начальных и конечных точек, таких какCURRENT ROWиUNBOUNDED FOLLOWING. -
-
class RowRange(start=None, end=None) -
-
frame_type -
Этот атрибут установлен в
'ROWS'.
-
Оба класса возвращают SQL с шаблоном:
%(frame_type)s BETWEEN %(start)s AND %(end)s
Фреймы сужают строки, используемые для вычисления результата. Они сдвигаются от некоторой начальной точки к некоторой заданной конечной точке. Фреймы могут использоваться с разделами и без них, но часто рекомендуется указать порядок окна для обеспечения детерминированного результата. В рамках фрейма, коллега во фрейме — это строка с эквивалентным значением или все строки, если условие упорядочивания отсутствует.
По умолчанию начальная точка фрейма — UNBOUNDED PRECEDING, что соответствует первой строке раздела. Конечная точка всегда явно включается в SQL, генерируемый ORM, и по умолчанию — UNBOUNDED FOLLOWING. Фрейм по умолчанию включает все строки от раздела до последней строки в наборе.
Допустимые значения для аргументов start и end — None, целое число или ноль. Отрицательное целое число для start приводит к N preceding, в то время как None приводит к UNBOUNDED PRECEDING. Для start и end ноль возвращает CURRENT ROW. Положительные целые числа принимаются для end.
Различие в том, что включает CURRENT ROW. При указании в режиме ROWS фрейм начинается или заканчивается текущей строкой. При указании в режиме RANGE фрейм начинается или заканчивается первой или последней строкой в соответствии с условием упорядочивания. Таким образом, RANGE CURRENT ROW вычисляет выражение для строк, имеющих одинаковое значение, указанное в условии упорядочивания. Поскольку шаблон включает обе точки start и end, это может быть выражено следующим образом:
ValueRange(start=0, end=0)
Если «коллеги» фильма описываются как фильмы, выпущенные той же киностудией в том же жанре в том же году, этот RowRange пример аннотирует каждый фильм средним рейтингом двух предыдущих и двух последующих коллег:
>>> from django.db.models import Avg, F, RowRange, Window
>>> from django.db.models.functions import ExtractYear
>>> Movie.objects.annotate(
>>> avg_rating=Window(
>>> expression=Avg('rating'),
>>> partition_by=[F('studio'), F('genre')],
>>> order_by=ExtractYear('released').asc(),
>>> frame=RowRange(start=-2, end=2),
>>> ),
>>> )
Если база данных это поддерживает, вы можете указать начальные и конечные точки на основе значений выражения в разделе. Если поле released модели Movie хранит месяц выпуска каждого фильма, этот ValueRange пример аннотирует каждый фильм средним рейтингом коллег, выпущенных за 12 месяцев до и 12 месяцев после каждого фильма.
>>> from django.db.models import Avg, ExpressionList, F, ValueRange, Window
>>> Movie.objects.annotate(
>>> avg_rating=Window(
>>> expression=Avg('rating'),
>>> partition_by=[F('studio'), F('genre')],
>>> order_by=F('released').asc(),
>>> frame=ValueRange(start=-12, end=12),
>>> ),
>>> )
Техническая информация
Ниже приведены технические детали реализации, которые могут быть полезны авторам библиотек. Технический API и примеры ниже помогут в создании универсальных выражений запросов, которые могут расширить встроенную функциональность, предоставляемую Django.
API выражений
Выражения запросов реализуют API выражения запроса, но также предоставляют ряд дополнительных методов и атрибутов, перечисленных ниже. Все выражения запросов должны наследоваться от Expression() или соответствующего подкласса.
Когда выражение запроса оборачивает другое выражение, оно отвечает за вызов соответствующих методов обернутого выражения.
-
class Expression -
-
contains_aggregate -
Указывает Django, что данное выражение содержит агрегат, и что в запрос необходимо добавить
GROUP BYпредложение.
-
contains_over_clause -
Указывает Django, что данное выражение содержит выражение
Window. Оно используется, например, для запрета выражений оконных функций в запросах, которые изменяют данные.
-
filterable -
Указывает Django, что данное выражение может быть использовано в
QuerySet.filter(). По умолчаниюTrue.
-
window_compatible -
Указывает Django, что это выражение может быть использовано в качестве исходного выражения в
Window. По умолчанию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(alias=None) -
Несет ответственность за возврат списка столбцов, на которые ссылается данное выражение.
get_group_by_cols()следует вызвать для всех вложенных выражений. В частности, объектыF()содержат ссылку на столбец. ПараметрaliasбудетNoneза исключением случаев, когда выражение было аннотировано и используется для группировки.Изменено в Django 3.0:Параметр
aliasбыл добавлен.
-
asc(nulls_first=False, nulls_last=False) -
Возвращает выражение, готовое к сортировке по возрастанию.
nulls_firstиnulls_lastопределяют, как сортируются значения NULL. См. Использование F() для сортировки значений NULL для примера использования.
-
desc(nulls_first=False, nulls_last=False) -
Возвращает выражение, готовое к сортировке по убыванию.
nulls_firstиnulls_lastопределяют, как сортируются значения NULL. См. Использование F() для сортировки значений NULL для примера использования.
-
reverse_ordering() -
Возвращает
selfс необходимыми изменениями для изменения порядка сортировки внутри вызоваorder_by. В качестве примера, выражение, реализующееNULLS LAST, изменит своё значение наNULLS FIRST. Изменения необходимы только для выражений, реализующих порядок сортировки, таких какOrderBy. Этот метод вызывается при вызовеreverse()для набора запросов.
-
Написание собственных выражений запроса
Вы можете написать собственные классы выражений запроса, которые используют и могут интегрироваться с другими выражениями запроса. Давайте разберем пример написания реализации 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 для определённой функции, вы можете добавить поддержку, подменяя новым методом функцию в классе.
Предположим, что мы пишем бэкенд для Microsoft SQL Server, который использует 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/3.0/ref/models/expressions/