Пользовательские поиски ¶
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()
.
A 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
А в базах данных, которые поддерживают отдельные поля (например, 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
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
доступна только из AbsoluteValue
поиска, то lhs
есть всегда является экземпляром AbsoluteValue
.
Замечу также , что , поскольку обе стороны используют несколько раз в запросе в PARAMS должен содержать 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[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')
.