Spec-Zone.ru › Django 2.2

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

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

Это также позволяет использовать результат в 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")
Изменено в Django 2.1:

Добавлена поддержка сортировки и distinct, как описано в двух последних абзацах.

При поиске допустимых запросов после применения 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/2.2/howto/custom-lookups/

Spec-Zone.ru

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