Пользовательские запросы
Django предлагает множество встроенных запросов для фильтрации (например, exact и icontains). В данном документе описано, как создавать пользовательские запросы и как изменять работу существующих запросов. Для ссылок на API запросов см. Справочник по API запросов.
Пример простого запроса
Начнём с простого пользовательского запроса. Мы напишем пользовательский запрос ne, который работает противоположно exact. Author.objects.filter(name__ne='Jack') будет переведён на SQL:
"author"."name" <> 'Jack'
Этот SQL-запрос независим от базы данных, поэтому мы не должны беспокоиться о разных базах данных.
Для работы необходимо выполнить два шага. Во-первых, необходимо реализовать запрос, а затем сообщить Django об этом. Реализация довольно проста:
from django.db.models import Lookup
class NotEqual(Lookup):
lookup_name = 'ne'
def as_sql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s <> %s' % (lhs, rhs), params
Для регистрации запроса NotEqual нужно просто вызвать register_lookup для класса поля, для которого должен быть доступен запрос. В данном случае, запрос имеет смысл для всех подклассов Field, поэтому мы регистрируем его с помощью Field напрямую:
from django.db.models.fields import Field Field.register_lookup(NotEqual)
Регистрация запросов также может быть выполнена с помощью шаблона декоратора:
from django.db.models.fields import Field
@Field.register_lookup
class NotEqualLookup(Lookup):
# ...
Теперь мы можем использовать foo__ne для любого поля foo. Необходимо убедиться, что эта регистрация выполняется до попытки создания наборов запросов с её использованием. Вы можете разместить реализацию в файле models.py, или зарегистрировать запрос в методе ready() класса AppConfig.
Рассмотрим подробнее реализацию. Первым необходимым атрибутом является lookup_name. Это позволяет ORM понять, как интерпретировать name__ne и использовать NotEqual для генерации SQL. По соглашению, эти имена всегда являются строками со строчными буквами, содержащими только буквы, но единственным жёстким требованием является то, что она не должна содержать строку __.
Затем нам необходимо определить метод as_sql. Он принимает объект SQLCompiler, называемый compiler, и активное соединение с базой данных. Объекты SQLCompiler не документированы, но всё, что нам нужно знать о них, это то, что они имеют метод compile(), который возвращает кортеж, содержащий строку SQL и параметры для интерполяции в эту строку. В большинстве случаев вам не нужно использовать его напрямую, и вы можете передать его process_lhs() и process_rhs().
Запрос Lookup работает с двумя значениями, lhs и rhs, обозначающими левую и правую части. Левая часть обычно представляет ссылку на поле, но это может быть что угодно, что реализует API выражений запроса. Правая часть — это значение, заданное пользователем. В примере Author.objects.filter(name__ne='Jack') левая часть — ссылка на поле name модели Author, а 'Jack' — правая часть.
Мы вызываем process_lhs и process_rhs для преобразования их в значения, необходимые для SQL, используя объект compiler, описанный ранее. Эти методы возвращают кортежи, содержащие некоторую SQL и параметры для интерполяции в эту SQL, так же, как мы должны возвращать из нашего метода as_sql. В приведённом выше примере process_lhs возвращает ('"author"."name"', []), а process_rhs возвращает ('"%s"', ['Jack']). В этом примере для левой части не было параметров, но это зависело бы от объекта, поэтому нам всё равно нужно включить их в возвращаемые параметры.
Наконец, мы объединяем части в выражение SQL с помощью <>, и предоставляем все параметры для запроса. Затем мы возвращаем кортеж, содержащий сгенерированную строку SQL и параметры.
Пример простого преобразователя
Пользовательский запрос выше отлично подходит, но в некоторых случаях вам может потребоваться возможность объединять запросы вместе. Например, предположим, что мы разрабатываем приложение, в котором хотим использовать оператор abs(). У нас есть модель Experiment, которая записывает начальное значение, конечное значение и изменение (начальное - конечное). Мы хотим найти все эксперименты, где изменение было равно определённому значению (Experiment.objects.filter(change__abs=27)) или не превышало определённого значения (Experiment.objects.filter(change__abs__lt=27)).
Примечание
Этот пример несколько искусственный, но он хорошо демонстрирует диапазон возможностей, которые возможны в базе данных независимым от неё образом и без дублирования функций, уже имеющихся в Django.
Начнём с написания преобразователя AbsoluteValue. Он будет использовать SQL-функцию ABS() для преобразования значения перед сравнением:
from django.db.models import Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
Далее, зарегистрируем его для IntegerField:
from django.db.models import IntegerField IntegerField.register_lookup(AbsoluteValue)
Теперь мы можем выполнить запросы, которые мы имели ранее. Experiment.objects.filter(change__abs=27) сгенерирует следующий SQL:
SELECT ... WHERE ABS("experiments"."change") = 27
Используя Transform вместо Lookup, это означает, что мы можем объединять последующие запросы. Таким образом, Experiment.objects.filter(change__abs__lt=27) сгенерирует следующий SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
Обратите внимание, что в случае отсутствия других запросов Django интерпретирует change__abs=27 как change__abs__exact=27.
Это также позволяет использовать результат в ORDER BY и DISTINCT ON предложениях. Например, Experiment.objects.order_by('change__abs') генерирует:
SELECT ... ORDER BY ABS("experiments"."change") ASC
И в базах данных, поддерживающих distinct по полям (например, PostgreSQL), Experiment.objects.distinct('change__abs') генерирует:
SELECT ... DISTINCT ON ABS("experiments"."change")
Была добавлена поддержка сортировки и distinct, как описано в последних двух абзацах.
При поиске допустимых запросов после применения Transform, Django использует атрибут output_field . Нам не нужно было указывать это здесь, так как оно не изменилось, но предположим, что мы применяем AbsoluteValue к некоторому полю, которое представляет более сложный тип (например, точку относительно начала координат или комплексное число), тогда мы могли захотеть указать, что преобразование возвращает тип FloatField для последующих запросов. Это можно сделать, добавив атрибут output_field к преобразователю:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
@property
def output_field(self):
return FloatField()
Это гарантирует, что последующие запросы, такие как abs__lte, будут работать так, как они работают для типа FloatField.
Написание эффективного запроса abs__lt
При использовании описанного выше запроса abs, сгенерированный SQL в некоторых случаях не будет эффективно использовать индексы. В частности, когда мы используем change__abs__lt=27, это эквивалентно change__gt=-27 И change__lt=27. (В случае lte мы могли бы использовать SQL BETWEEN).
Поэтому мы хотим, чтобы Experiment.objects.filter(change__abs__lt=27) генерировал следующий SQL:
SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27
Реализация:
from django.db.models import Lookup
class AbsoluteValueLessThan(Lookup):
lookup_name = 'lt'
def as_sql(self, compiler, connection):
lhs, lhs_params = compiler.compile(self.lhs.lhs)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params + lhs_params + rhs_params
return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params
AbsoluteValue.register_lookup(AbsoluteValueLessThan)
Несколько важных моментов. Во-первых, AbsoluteValueLessThan не вызывает process_lhs(). Вместо этого он пропускает преобразование lhs, выполненное AbsoluteValue, и использует исходный lhs. То есть, мы хотим получить "experiments"."change", а не ABS("experiments"."change"). Обращение напрямую к self.lhs.lhs безопасно, так как к AbsoluteValueLessThan можно получить доступ только из запроса AbsoluteValue, то есть lhs всегда является экземпляром AbsoluteValue.
Также обратите внимание, что поскольку обе стороны используются несколько раз в запросе, параметры должны содержать lhs_params и rhs_params несколько раз.
Окончательный запрос выполняет инверсию (27 в -27 ) непосредственно в базе данных. Причина этого заключается в том, что если self.rhs — это не просто целочисленное значение (например, ссылка на F()), то мы не можем выполнить преобразования в Python.
Примечание
На самом деле, большинство запросов с __abs можно реализовать как запросы по диапазонам, подобные этому, и в большинстве баз данных это, вероятно, будет более разумно, так как вы можете использовать индексы. Однако в PostgreSQL может потребоваться добавить индекс на abs(change) , что сделает эти запросы очень эффективными.
Пример двустороннего преобразователя
Пример AbsoluteValue , который мы обсуждали ранее, представляет собой преобразование, которое применяется к левой части запроса. Могут быть случаи, когда вам нужно, чтобы преобразование применялось как к левой, так и к правой части. Например, если вы хотите отфильтровать набор запросов, основываясь на равенстве левой и правой части, нечувствительно к какой-то SQL-функции.
Рассмотрим простой пример преобразования, нечувствительного к регистру. Это преобразование не очень полезно на практике, так как Django уже поставляется с множеством встроенных запросов, нечувствительных к регистру, но это будет хорошей демонстрацией двусторонних преобразований независимым от базы данных образом.
Определим преобразователь UpperCase , который использует SQL-функцию UPPER() для преобразования значений перед сравнением. Мы определяем bilateral = True , чтобы указать, что это преобразование должно применяться как к lhs, так и к rhs:
from django.db.models import Transform
class UpperCase(Transform):
lookup_name = 'upper'
function = 'UPPER'
bilateral = True
Далее, зарегистрируем его:
from django.db.models import CharField, TextField CharField.register_lookup(UpperCase) TextField.register_lookup(UpperCase)
Теперь набор запросов Author.objects.filter(name__upper="doe") сгенерирует запрос, нечувствительный к регистру, такой как этот:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
Написание альтернативных реализаций для существующих запросов
Иногда разные поставщики баз данных требуют разного SQL для одной и той же операции. В этом примере мы перепишем пользовательскую реализацию для MySQL для оператора NotEqual. Вместо оператора <> мы будем использовать оператор !=. (Обратите внимание, что на самом деле почти все базы данных поддерживают оба, включая все официальные базы данных, поддерживаемые Django).
Мы можем изменить поведение на определённой базе данных, создав подкласс NotEqual с методом as_mysql:
class MySQLNotEqual(NotEqual):
def as_mysql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s != %s' % (lhs, rhs), params
Field.register_lookup(MySQLNotEqual)
Затем мы можем зарегистрировать его с помощью Field. Он заменяет исходный класс NotEqual, так как имеет тот же атрибут lookup_name.
При компиляции запроса Django сначала ищет методы as_%s % connection.vendor, а затем переходит к as_sql. Имена поставщиков для встроенных бэкендов — это sqlite, postgresql, oracle и mysql.
Как Django определяет используемые запросы и преобразования
В некоторых случаях вам может потребоваться динамически изменить возвращаемый Transform или Lookup в зависимости от переданного имени, вместо того, чтобы фиксировать его. Например, у вас может быть поле, хранящее координаты или произвольную размерность, и вы хотите позволить синтаксис, подобный .filter(coords__x7=4), для возврата объектов, где 7-я координата имеет значение 4. Для этого вы переопределите get_lookup чем-то вроде:
class CoordinatesField(Field):
def get_lookup(self, lookup_name):
if lookup_name.startswith('x'):
try:
dimension = int(lookup_name[1:])
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super().get_lookup(lookup_name)
Затем вы определите get_coordinate_lookup соответствующим образом, чтобы вернуть подкласс Lookup, который обрабатывает соответствующее значение dimension.
Существует метод с похожим названием get_transform(). get_lookup() всегда должен возвращать подкласс Lookup, а get_transform() — подкласс Transform. Важно помнить, что объекты Transform могут быть дополнительно отфильтрованы, а объекты Lookup — нет.
При фильтрации, если для разрешения осталось только одно имя поиска, мы будем искать Lookup. Если имен несколько, то будет поиск Transform. В ситуации, когда есть только одно имя и Lookup не найден, мы ищем Transform и затем exact поиск на этом Transform. Все последовательности вызовов всегда заканчиваются Lookup. Для уточнения:
-
.filter(myfield__mylookup)вызоветmyfield.get_lookup('mylookup'). -
.filter(myfield__mytransform__mylookup)вызоветmyfield.get_transform('mytransform'), а затемmytransform.get_lookup('mylookup'). -
.filter(myfield__mytransform)сначала вызоветmyfield.get_lookup('mytransform'), который потерпит неудачу, поэтому он вернется к вызовуmyfield.get_transform('mytransform')и затемmytransform.get_lookup('exact').
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.1/howto/custom-lookups/