• 3.1
  • 5.0
  • 6.1
  • Версия документации: 3.2

Custom Lookups

Django предлагает широкий спектр встроенных запросов (lookups) для фильтрации (например, 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)

Регистрацию lookup’а можно также выполнить с помощью шаблона декоратора:

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().

Lookup работает с двумя значениями, lhs и rhs, обозначающими левую и правую стороны. Левая сторона обычно является ссылкой на поле, но может быть чем угодно, реализующим выражение query 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 и параметры.

Пример преобразователя

В некоторых случаях вам может понадобиться связать lookup’ы вместе. Например, предположим, что мы создаем приложение, в котором мы хотим использовать оператор 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

By using Transform instead of Lookup it means we are able to chain further lookups afterwards. So Experiment.objects.filter(change__abs__lt=27) will generate the following SQL:

SELECT ... WHERE ABS("experiments"."change") < 27

Обратите внимание, что если не указан другой lookup, 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")

При поиске допустимых lookups после применения Transform, Django использует атрибут output_field. Нам не нужно было указывать это здесь, так как оно не менялось, но предположим, что мы применяем AbsoluteValue к некоторому полю, представляющему более сложный тип (например, точку относительно начала координат или комплексное число), тогда мы могли бы захотеть указать, что преобразование возвращает тип FloatField для дальнейших lookup’ов. Это можно сделать, добавив атрибут output_field к ``transform:

from django.db.models import FloatField, Transform

class AbsoluteValue(Transform):
    lookup_name = 'abs'
    function = 'ABS'

    @property
    def output_field(self):
        return FloatField()

Это гарантирует, что дальнейшие lookup’ы, такие как abs__lte, будут вести себя так же, как и для FloatField.

Написание эффективного lookup 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 может быть доступна только из lookup AbsoluteValue, то есть lhs всегда является экземпляром AbsoluteValue.

Обратите внимание, что поскольку обе стороны используются в запросе несколько раз, параметры должны содержать lhs_params и rhs_params несколько раз.

Финальный запрос выполняет инверсию (27 в -27) непосредственно в базе данных. Причина этого в том, что если self.rhs - это что-то иное, чем простое целочисленное значение (например, ссылка F()), мы не можем выполнить преобразования в Python.

Примечание

На самом деле, большинство поисков с __abs можно реализовать как запросы по диапазону подобно этому, и на большинстве бэкэндов баз данных, вероятно, будет разумнее сделать это, поскольку вы можете использовать индексы. Однако с PostgreSQL вы можете захотеть добавить индекс на abs(change), что позволит этим запросам быть очень эффективными.

Пример двустороннего преобразования

Пример AbsoluteValue, который мы обсуждали ранее, представляет собой преобразование, которое применяется к левой стороне поиска. Могут быть случаи, когда вы хотите, чтобы преобразование применялось как к левой, так и к правой стороне. Например, если вы хотите отфильтровать набор запросов, основываясь на равенстве левой и правой сторон.

Давайте рассмотрим здесь преобразования, нечувствительные к регистру. Это преобразование не очень полезно на практике, так как 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)

Теперь queryset Author.objects.filter(name__upper="doe") будет генерировать запрос без учета регистра, например:

SELECT ... WHERE UPPER("author"."name") = UPPER('doe')

Написание альтернативных реализаций для существующих lookups

Иногда разные поставщики баз данных требуют разный 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 определяет используемые lookups и преобразования

В некоторых случаях вы можете захотеть динамически изменить, какой 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)

You would then define get_coordinate_lookup appropriately to return a Lookup subclass which handles the relevant value of dimension.

Существует метод с похожим названием get_transform(). get_lookup() всегда должен возвращать подкласс Lookup, а get_transform() - подкласс Transform. Важно помнить, что объекты Transform можно дополнительно фильтровать, а объекты Lookup - нет.

При фильтрации, если осталось разрешить только одно имя лукапа, мы будем искать Lookup. Если имен несколько, будет искать Transform. В ситуации, когда есть только одно имя и `Lookup не найден, мы ищем Transform, а затем точный поиск по этому 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').

Back to Top