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

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

Теперь мы можем писать champ__ne для всех champ . Убедитесь, что регистрация происходит перед созданием любых наборов запросов, использующих это выражение. Эту реализацию можно поместить в файл 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 return и return . В этом примере левая сторона не имеет параметров, но это зависит от задействованного объекта, поэтому мы все равно должны включить их в возвращаемые параметры.('"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()

Это гарантирует, что последующие поиски, такие как in, будут abs__lte вести себя так же, как с полем

Написание 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) возвращать объекты, где седьмая координата равна 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') .

Copyright ©2021 All rights reserved