Как написать пользовательские поисковые запросы
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().
Поисковый запрос Lookup работает с двумя значениями, lhs и rhs, обозначающими левую и правую части. Левая часть обычно является ссылкой на поле, но может быть чем угодно, реализующим API выражения запроса 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), чтобы возвращать объекты, где 7-я координата имеет значение 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.1/howto/custom-lookups/