Как писать пользовательские lookup-выражения
Django предлагает широкий выбор встроенных lookup-выражений для фильтрации (например, exact и icontains). В этой документации объясняется, как писать пользовательские lookup-выражения и изменять работу существующих. Справочную информацию по API lookup-выражений см. в справочнике по API lookup-выражений.
Пример lookup-выражения
Начнём с небольшого пользовательского lookup-выражения. Мы напишем пользовательское lookup-выражение ne, которое работает противоположно exact. Author.objects.filter(name__ne='Jack') будет преобразовано в SQL:
"author"."name" <> 'Jack'
Этот SQL не зависит от используемого сервера БД, поэтому нам не нужно беспокоиться о различиях между базами данных.
Чтобы это заработало, нужно выполнить два шага. Сначала реализовать lookup-выражение, а затем сообщить о нём 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
Чтобы зарегистрировать lookup-выражение NotEqual, нужно вызвать register_lookup у класса поля, для которого оно должно быть доступно. В этом случае lookup-выражение имеет смысл для всех подклассов Field, поэтому зарегистрируем его непосредственно в Field:
from django.db.models import Field Field.register_lookup(NotEqual)
Регистрацию lookup-выражения также можно выполнить с помощью шаблона декоратора:
from django.db.models import Field @Field.register_lookup class NotEqualLookup(Lookup): ...
Теперь мы можем использовать foo__ne для любого поля foo. Убедитесь, что эта регистрация выполняется до создания любых наборов запросов, в которых используется lookup-выражение. Реализацию можно поместить в файл models.py или зарегистрировать lookup-выражение в методе 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 и параметрами для подстановки в неё — именно такой результат должен возвращать наш метод as_sql. В приведённом выше примере process_lhs возвращает ('"author"."name"', []), а process_rhs возвращает ('"%s"', ['Jack']). В этом примере для левой части не было параметров, но это зависит от используемого объекта, поэтому их всё равно нужно включить в возвращаемые параметры.
Наконец, мы объединяем части в SQL-выражение с помощью <> и передаём все параметры запроса. Затем возвращаем кортеж со сгенерированной строкой SQL и параметрами.
Пример трансформера
Приведённое выше пользовательское lookup-выражение полезно, но в некоторых случаях может понадобиться объединять lookup-выражения в цепочки. Например, предположим, что мы разрабатываем приложение, в котором хотим использовать оператор 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, мы можем добавлять последующие lookup-выражения в цепочку. Поэтому Experiment.objects.filter(change__abs__lt=27) сгенерирует следующий SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
Если другое lookup-выражение не указано, 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 on для полей (например, PostgreSQL), Experiment.objects.distinct('change__abs') генерирует:
SELECT ... DISTINCT ON ABS("experiments"."change")
Определяя, какие lookup-выражения допустимы после применения Transform, Django использует атрибут output_field. Здесь его не нужно было указывать, поскольку он не изменился. Но если бы мы применяли AbsoluteValue к полю, представляющему более сложный тип (например, точку относительно начала координат или комплексное число), то, возможно, захотели бы указать, что трансформация возвращает тип FloatField для последующих lookup-выражений. Для этого можно добавить атрибут output_field к трансформеру:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = "abs"
function = "ABS"
@property
def output_field(self):
return FloatField()
Это гарантирует, что последующие lookup-выражения, например abs__lte, будут работать так же, как для FloatField.
Создание эффективного lookup-выражения abs__lt
В некоторых случаях SQL, созданный при использовании описанного выше lookup-выражения abs, неэффективно использует индексы. В частности, при использовании change__abs__lt=27 это эквивалентно change__gt=-27 AND 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 можно получить только из lookup-выражения AbsoluteValue, то есть lhs всегда является экземпляром AbsoluteValue.
Обратите также внимание: поскольку обе стороны используются в запросе несколько раз, параметры должны содержать lhs_params и rhs_params по несколько раз.
Итоговый запрос выполняет инверсию (27 в -27) непосредственно в базе данных. Это необходимо, потому что, если self.rhs — не простое целочисленное значение (например, ссылка F()), мы не можем выполнить преобразования в Python.
Примечание
На самом деле большинство lookup-выражений с __abs можно реализовать как запросы диапазона, как в этом примере. Для большинства серверов БД это, скорее всего, будет разумнее, поскольку так можно использовать индексы. Однако при работе с PostgreSQL можно добавить индекс для abs(change), что позволит выполнять такие запросы очень эффективно.
Пример двустороннего трансформера
Рассмотренный ранее пример AbsoluteValue представляет собой трансформацию, применяемую к левой части lookup-выражения. В некоторых случаях может понадобиться применять трансформацию и к левой, и к правой части. Например, если нужно отфильтровать набор запросов по равенству левой и правой частей без учёта регистра с помощью некоторой SQL-функции.
Рассмотрим здесь трансформации без учёта регистра. На практике эта трансформация не очень полезна, поскольку Django уже включает ряд встроенных lookup-выражений без учёта регистра, но она хорошо продемонстрирует двусторонние трансформации независимо от базы данных.
Определим трансформер 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')
Создание альтернативных реализаций существующих lookup-выражений
Иногда для одной и той же операции разным производителям баз данных требуется разный SQL. В этом примере мы перепишем пользовательскую реализацию оператора NotEqual для MySQL. Вместо <> мы будем использовать оператор !=. (На практике почти все базы данных поддерживают оба варианта, включая все официально поддерживаемые 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 определяет используемые lookup-выражения и трансформации
В некоторых случаях может понадобиться динамически менять возвращаемое значение 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-выражения, мы ищем Lookup. Если имён несколько, выполняется поиск Transform. Если осталось только одно имя, но Lookup не найден, мы ищем Transform, а затем lookup-выражение 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/6.0/howto/custom-lookups/