Spec-Zone.ru › Django 1.10

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

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.

Как 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(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__mylookup) сначала вызовет 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.10/howto/custom-lookups/

Spec-Zone.ru

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