Spec-Zone.ru › Django 4.1

Как создавать пользовательские запросы

Django предлагает множество встроенных запросов для фильтрации (например, exact и icontains). Данная документация объясняет, как создавать пользовательские запросы и изменять работу существующих запросов. Справочная информация по запросам представлена в справочной информации по 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().

Запрос Lookup работает с двумя значениями, lhs и rhs, обозначающими левую и правую части выражения. Левая часть обычно является ссылкой на поле, но может быть любым объектом, реализующим API выражений запроса. Правая часть – это значение, заданное пользователем. В примере Author.objects.filter(name__ne='Jack') левая часть – это ссылка на поле name модели Author, а 'Jack' – это правая часть.

Мы вызываем process_lhs и process_rhs для преобразования этих значений в значения, необходимые для SQL-запроса, используя объект compiler , описанный ранее. Эти методы возвращают кортежи, содержащие некоторую 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/4.1/howto/custom-lookups/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API