Как создавать пользовательские запросы
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 import Field Field.register_lookup(NotEqual)
Регистрация запроса также может быть выполнена с помощью декоратора:
from django.db.models 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().
Запрос работает с двумя значениями, lhs и rhs, обозначающими левую и правую части. Левая часть обычно является ссылкой на поле, но может быть чем угодно, что реализует API выражения запроса. Правая часть — это значение, заданное пользователем. В примере Author.objects.filter(name__ne='Jack') левая часть — это ссылка на поле name модели Author, а 'Jack' — правая часть.
Мы вызываем process_lhs и process_rhs для преобразования их в значения, необходимые для SQL, с использованием объекта compiler, описанного ранее. Эти методы возвращают кортежи, содержащие некоторую строку 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")
При поиске разрешенных запросов после применения 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, **extra_context):
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), для возврата объектов, где седьмая координата имеет значение 4. Для этого вы переопределите get_lookup следующим образом:
class CoordinatesField(Field):
def get_lookup(self, lookup_name):
if lookup_name.startswith("x"):
try:
dimension = int(lookup_name.removeprefix("x"))
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/5.2/howto/custom-lookups/