Пользовательские запросы
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 выражений запроса 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.
При поиске разрешённых запросов после применения 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.0/howto/custom-lookups/