Настраиваемые запросы
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'
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.
При поиске разрешённых запросов после применения 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 для одного и того же действия. В этом примере мы перепишем пользовательскую реализацию для 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.9/howto/custom-lookups/