Пользовательский поиск¶
Django предлагает широкий выбор built-in lookups для фильтрации (например, 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
, обозначающих левую сторону и правую сторону. Левая сторона обычно является ссылкой на поле, но это может быть что угодно, реализующее query expression 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
А в базах данных, поддерживающих раздельные поля (например, 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 для одной и той же операции. В данном примере мы перепишем пользовательскую реализацию оператора 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 определяет используемый поиск и преобразования¶
В некоторых случаях вы можете пожелать динамически изменять возвращаемое значение 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')
.