Spec-Zone.ru › Django 1.11

Настраиваемые запросы

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

При поиске разрешенных запросов после применения 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):
        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.

END_OF_DOCUMENT_MARKER

Как Django определяет используемые запросы и преобразования

В некоторых случаях вам может потребоваться динамически изменить возвращаемое Transform или Lookup в зависимости от переданного имени, а не фиксировать его. Например, вы можете иметь поле, хранящее координаты или произвольную размерность, и хотите разрешить синтаксис, такой как .filter(coords__x7=4), для возвращения объектов, где седьмая координата имеет значение 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(CoordinatesField, self).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/1.11/howto/custom-lookups/

Spec-Zone.ru

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