Пользовательские выражения поиска ¶
Django предлагает широкий спектр встроенных поисковых выражений для фильтрации запросов (например, exact
и icontains
). В этой документации объясняется, как писать собственные выражения и как изменять поведение существующих выражений. Для справки по поисковому API см. Search Expression Reference 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):
# ...
Теперь мы можем писать champ__ne
для всех champ
. Убедитесь, что регистрация происходит перед созданием любых наборов запросов, использующих это выражение. Эту реализацию можно поместить в файл 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, а также параметры, которые нужно вставить в этот код, точно так же, как они нам нужны для возвращаемого значения нашего метода as_sql
. В приведенном выше примере process_lhs
return и return . В этом примере левая сторона не имеет параметров, но это зависит от задействованного объекта, поэтому мы все равно должны включить их в возвращаемые параметры.('"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()
Это гарантирует, что последующие поиски, такие как in, будут abs__lte
вести себя так же, как с полем
Написание 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[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')
.