Spec-Zone.ru › Django 4.2

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

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

Запрос 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.

Это также позволяет использовать результат в 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.2/howto/custom-lookups/

Spec-Zone.ru

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