Пользовательские запросы
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.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, обозначающими левую и правую стороны. Левая сторона обычно является ссылкой на поле, но может быть любым объектом, реализующим 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'
def as_sql(self, compiler, connection):
lhs, params = compiler.compile(self.lhs)
return "ABS(%s)" % lhs, params
Далее, давайте зарегистрируем его для 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.
При поиске разрешённых запросов после применения Transform, Django использует атрибут output_field . Нам не нужно было это указывать, так как оно не изменилось, но если бы мы применяли AbsoluteValue к какому-либо полю, представляющему более сложный тип (например, точку относительно начала координат или комплексное число), то мы могли бы указать, что преобразование возвращает тип FloatField для дальнейших запросов. Это можно сделать, добавив атрибут output_field в преобразование:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
def as_sql(self, compiler, connection):
lhs, params = compiler.compile(self.lhs)
return "ABS(%s)" % lhs, params
@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'
bilateral = True
def as_sql(self, compiler, connection):
lhs, params = compiler.compile(self.lhs)
return "UPPER(%s)" % lhs, params
Далее, давайте зарегистрируем его:
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):
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(CoordinatesField, self).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/1.8/howto/custom-lookups/