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

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

A 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 BYDISTINCT ONExperiment.objects.order_by('change__abs')

SELECT ... ORDER BY ABS("experiments"."change") ASC

А в базах данных, которые поддерживают отдельные поля (например, 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=-27AND 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.

Замечу также , что , поскольку обе стороны используют несколько раз в запросе в PARAMS должен содержать lhs_paramsи rhs_paramsнесколько раз.

Последний запрос выполняет инверсию ( 27в -27) непосредственно в базе данных. Причина этого заключается в том, что если self.rhsэто что-то иное, чем простое целочисленное значение (например, F()ссылка), мы не можем выполнять преобразования в Python.

Примечание

Фактически, большинство поисков с помощью __absможет быть реализовано как запросы диапазона, подобные этому, и на большинстве бэкэндов баз данных, вероятно, будет более разумным сделать это, поскольку вы можете использовать индексы. Однако с PostgreSQL вы можете добавить индекс, abs(change)который позволит этим запросам быть очень эффективными.

Пример двустороннего трансформатора

AbsoluteValueПример , который мы обсудили ранее это преобразование , которое применяется к левой стороне поиска. В некоторых случаях вы хотите, чтобы преобразование применялось как к левой, так и к правой стороне. Например, если вы хотите отфильтровать набор запросов на основе равенства левой и правой части, нечувствительно к некоторой функции SQL.

Давайте рассмотрим здесь преобразования без учета регистра. Это преобразование не очень полезно на практике, поскольку Django уже поставляется с набором встроенных поисков без учета регистра, но это будет хорошая демонстрация двусторонних преобразований, не зависящих от базы данных.

Мы определяем UpperCaseпреобразователь, который использует функцию SQL UPPER()для преобразования значений перед сравнением. Мы определяем, чтобы указать, что это преобразование должно применяться к обоим и :bilateral = Truelhsrhs

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

Как 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').

Copyright ©2021 All rights reserved