Spec-Zone.ru › Django 2.1

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

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

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

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

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

Добавлена поддержка отрицания.

Некоторые примеры

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

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

Примечание

Эти выражения определены в 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 выглядит как обычная присваивание значения атрибуту экземпляра, на самом деле это 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.

Использование 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() для сортировки нулевых значений

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

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

from django.db.models import F
Company.object.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) [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 для функции базы данных.

Методы 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):
        return super().as_sql(
            compiler, connection,
            function='CONCAT_WS',
            template="%(function)s('', %(expressions)s)",
        )

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

END_OF_DOCUMENT_MARKER

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

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

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

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

Выражения агрегирования

Выражение агрегирования — это специальный случай выражения Func(), которое сообщает запросу о необходимости использования GROUP BY клаузы. Все функции агрегирования, такие как Sum() и Count(), наследуются от 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, filter=None, **extra) [source]
template

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

function

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

window_compatible
Добавлена в Django 2.0.

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

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

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

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

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

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

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

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

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

from django.db.models import Aggregate

class Count(Aggregate):
    # supports COUNT(distinct field)
    function = 'COUNT'
    template = '%(function)s(%(distinct)s%(expressions)s)'

    def __init__(self, expression, distinct=False, **extra):
        super().__init__(
            expression,
            distinct='DISTINCT ' if distinct else '',
            output_field=IntegerField(),
            **extra
        )

Выражения со значениями

class Value(value, output_field=None) [source]

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

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

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

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

Выражения ExpressionWrapper

class ExpressionWrapper(expression, output_field) [source]

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

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

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

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

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. Чтобы вернуть все комментарии к публикациям, опубликованным в течение последнего дня:

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

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

(Использование 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 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, например:

>>> Post.objects.filter(Exists(recent_comments))
...
TypeError: 'Exists' object is not iterable

Необходимо фильтровать по выражению подзапроса, сначала аннотировав набор запросов, а затем отфильтровав по этой аннотации:

>>> Post.objects.annotate(
...     recent_comment=Exists(recent_comments),
... ).filter(recent_comment=True)

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

Агрегации могут быть использованы в 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", (someparam,)))

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

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

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

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

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

Вы можете узнать больше о том, как работает защита от SQL-инъекции в Django в защите от SQL-инъекции.

Функции окон

Новое в Django 2.0.

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

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

class Window(expression, partition_by=None, order_by=None, frame=None, output_field=None) [source]
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) [source]
frame_type

Это атрибут имеет значение 'RANGE'.

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

class RowRange(start=None, end=None) [source]
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 [source]
contains_aggregate

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

contains_over_clause
Новое в Django 2.0.

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

filterable
Новое в Django 2.0.

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

window_compatible
Новое в Django 2.0.

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

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

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

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

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

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

summarize — это булево значение, которое, при 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 к более подходящему типу.

get_group_by_cols()

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

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, 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 a 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 Майкрософт, который использует SQL LEN вместо LENGTH для функции Length. Мы подменим новый метод, названный as_sqlserver() в классе %%%CODE_BLOCK_386%%:

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

Мы используем %%%CODE_BLOCK_390%%, потому что 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/2.1/ref/models/expressions/

Spec-Zone.ru

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