Как написать пользовательские правила поиска
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 выражения запроса. Правая часть — это значение, заданное пользователем. В примере 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")
При поиске разрешенных правил поиска после применения 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.0/howto/custom-lookups/