Настраиваемые запросы
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().
Запрос работает с двумя значениями, lhs и rhs, обозначающими левую и правую части. Левая часть обычно является ссылкой на поле, но может быть чем угодно, реализующим API выражения запроса 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
А в базах данных, поддерживающих 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()
Это гарантирует, что дальнейшие запросы, такие как 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, **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').
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/3.2/howto/custom-lookups/