Перейти к содержанию

Описание фильтров и сортировок

Model._meta.get_lookup_info(key) сообщает, к чему относится ключ .filter(): через какие связи он проходит, с каким полем сравнивает, какой это оператор фильтра и какое значение он принимает, — ничего не строя и не выполняя. get_ordering_info(name) делает то же для имени в .order_by(), а get_lookups(path, dialect) перечисляет все операторы поля, которые выполняет диалект.

Это нужно коду, который превращает внешний ввод в фильтры: слою API, который при запуске проверяет объявленные фильтры и разбирает каждый параметр запроса в нужный тип, построителю форм, обозревателю таблиц. Такому коду не нужна своя копия списков операторов hare — он спрашивает модель.

hare сам пользуется этими описаниями: .filter(), .exclude() и .order_by() проверяют по ним каждый ключ при вызове, а запрос сверяет каждый оператор с диалектом своего подключения до построения SQL.

get_lookup_info()

from hare.query.enums import Lookup, LookupValueShape

info = Book._meta.get_lookup_info("author__name__icontains")
info.relations      # (Book.author,)
info.field          # Author.name
info.lookup         # Lookup.ICONTAINS
info.value_shape    # LookupValueShape.VALUE
info.value_type     # str

Принимает любой ключ, который принимает .filter():

Ключ relations field lookup value_shape value_type
title, title__icontains () title EXACT, ICONTAINS VALUE str
id__in () id IN LIST int
id__range () id RANGE RANGE int
editor_id__isnull () editor_id ISNULL VALUE bool
author, author__in (ключ или объект) (author,) Author.id EXACT, IN VALUE, LIST int
author_id__gte () author_id GTE VALUE int
author__pk, author__id__in (author,) Author.id EXACT, IN VALUE, LIST int
tags__in («многие-ко-многим») (tags,) Tag.id IN LIST int
books__isnull у Author (обратный внешний ключ) (books,) Book.id (первичный ключ Book) ISNULL VALUE bool
pk составного первичного ключа (Draft) () (id, version) EXACT VALUE (UUID, int)
pk__in составного первичного ключа () (id, version) IN LIST (UUID, int)
draft — внешний ключ на составной ключ (draft,) (Draft.id, Draft.version) EXACT VALUE (UUID, int)
added_at__year__gte () added_at GTE VALUE int
added_at__date () added_at EXACT VALUE date
details__publisher__name__icontains (JSON) () details ICONTAINS VALUE str
details__has_keys () details HAS_KEYS LIST str
keywords__contains (массив) () keywords CONTAINS LIST тип элемента
keywords__len__gt () keywords GT VALUE int
lending__overlap (диапазон) () lending OVERLAP RANGE тип границы
генерируемое поле () GeneratedField тип его output_field
title__<custom> () title имя оператора как при регистрации как при регистрации

Оператор по самой связи (author=, author__in=, tags__isnull=) сравнивает ключ связанной модели, поэтому field — это поле ключа: поле (поля), на которое ссылается прямая связь, или первичный ключ связанной модели для обратной связи и «многие-ко-многим», — а relations заканчивается этой связью.

LookupInfo

Неизменяемый класс данных. Описания строятся один раз и сохраняются:

  • собственные ключи фильтров модели, имена сортировки (OrderingInfo) и результаты get_lookups() — для каждой модели, а get_lookups() — для каждого пути и диалекта;
  • ключ или имя, начинающиеся с вычисляемого значения, — для каждого запроса вместе с выходными полями вычисляемых значений: копия запроса их делит, а annotate()/alias(), заменяющие вычисляемое значение, их сбрасывают;
  • изменение полей, операторов или связей любой модели (регистрация или отмена регистрации модели во время работы, добавленное или удалённое поле) сбрасывает все сохранённые описания.

Ключ или имя с ошибкой дают свою FieldError заново при каждом вызове — ошибки не сохраняются.

Атрибут Что в нём
key Ключ фильтра.
model Модель, с которой начинается ключ.
relations Связи, через которые проходит ключ, по порядку: прямые и обратные внешние ключи и «один-к-одному», поля «многие-ко-многим».
field Поле, с которым сравнивается значение; кортеж полей для составного ключа; None для вычисляемого значения, тип которого станет известен только при выполнении запроса.
transforms Путь, который читается внутри значения поля перед оператором: часть даты (("year",)), date/time, путь по ключам JSON (("publisher", "name")), индекс массива, срез или len, граница или признак диапазона, unaccent, ключ hstore.
lookup Элемент Lookup или имя оператора, зарегистрированного через register_lookup().
value_shape LookupValueShape.VALUE (одно значение), LIST (список: in, not_in, has_keys, contains у массива...) или RANGE (два элемента: range, overlap у поля диапазона...).
value_type Тип значения — каждого элемента списка или диапазона. Кортеж типов для составного ключа; bool для isnull/not_isnull; int для части даты или len; str для текстового оператора или ключа JSON/hstore; object для любого значения JSON.
crosses_to_many Проходит ли ключ через связь, у которой на одну строку приходится много строк (обратный внешний ключ или «многие-ко-многим»): фильтр соединяет её, и запрос может вернуть одну строку несколько раз.
requires_extension Расширение базы, нужное оператору ("pg_trgm" для операторов триграмм, "unaccent"), иначе None.
dialects Имена диалектов, на которых есть поле и оператор (frozenset({"postgresql"}) у поля массива), None — на всех, а также у поля, тип колонки которого даёт каждый диалект (GeometryField, VectorField): его is_supported() проверяет на том диалекте, о котором спрашивают.
is_supported(dialect) Выполняет ли запрос на dialect этот оператор — см. ниже.

Ошибки

  • Путь, который не называет ни поле, ни связь, ни вычисляемое значение, даёт FieldError: Unknown filter param 'author__nme': Author has no field 'nme'.
  • Оператор, которого у поля нет, даёт FieldError: Book.title has no lookup 'foo'; так же и оператор вне supported_lookups поля (у зашифрованного поля).
  • pk составного первичного ключа принимает только pk=, pk__in=, pk__not= и pk__not_in=; всё остальное — FieldError.
  • Оператор, отличный от равенства, принадлежности или isnull, у прямой связи на составной ключ (draft__gt=) даёт QueryError: он был бы неоднозначным — фильтруйте по каждой колонке ключа.
  • Модель, которая ещё не привязана (см. Пока подключения не настроены), ничего не описывает: get_lookup_info(), get_lookups() и get_ordering_info() — и у Model._meta, и у запроса — дают ConfigurationError: Book is not bound yet: call Hare.bind_models() or Hare.init() before describing its filters or orderings.

Поддержка диалектом

dialect.filter_operators.supports_lookup(info) или info.is_supported(dialect) сообщает, выполняет ли запрос на этом диалекте оператор. Оператор не поддерживается, если:

  • поля нет на этом диалекте (поле с COLUMN_TYPE_FROM_DIALECT, которому диалект не даёт типа колонки, например поле массива или диапазона вне PostgreSQL);
  • оператор реализуют только диалекты (search, операторы триграмм, операторы вложенности для массивов, диапазонов и JSON, оператор одной базы), а этот диалект его не реализует;
  • оператору нужно расширение, а у диалекта расширений нет.
from hare.dialects.dialect_registry import DialectRegistry

sqlite = DialectRegistry.get_dialect("sqlite")
info = Book._meta.get_lookup_info("title__search")
info.is_supported(sqlite)                                  # False
info.is_supported(DialectRegistry.get_dialect("postgresql"))  # True

Запрос с оператором, который диалект его подключения не поддерживает, даёт UnSupportedError ещё до построения SQL:

Book.objects.filter(title__search=...) can't run on sqlite: the sqlite dialect doesn't implement the __search lookup

get_lookups()

lookups = Book._meta.get_lookups("added_at", connection.dialect)
lookups[""]            # простое равенство
lookups["year__gte"]   # LookupInfo(value_type=int, ...)

path — поле или связь, после любых связей ("author__name", "tags", "pk"). Результат сопоставляет окончанию каждого оператора после path — "" для простого равенства, "icontains", "year__gte" — его описание и содержит только операторы, которые диалект поддерживает. У самой связи — операторы её ключа ("", in, not, not_in, isnull, not_isnull); у прямой связи с ключом из одной колонки — операторы этой колонки (у author — те же, что у author_id, с учётом диалекта); у pk составного первичного ключа — "" и in.

Оператору может понадобиться и возможность самого подключения, на котором выполняется запрос, а не только диалекта: posix_regex/iposix_regex требуют features.supports_posix_regex — в SQLite это подключение, в адресе которого есть ?install_regexp_functions=true. get_lookups() описывает диалект и перечисляет их; запрос с таким оператором на подключении без этой возможности даёт UnSupportedError до выполнения.

get_ordering_info()

info = Book._meta.get_ordering_info("-author__name")
info.relations    # (Book.author,)
info.fields       # (Author.name,)
info.paths        # ("author__name",)
info.descending   # True

У OrderingInfo есть name, model, relations, fields (поля, по которым идёт сортировка), paths (имена, по которым сортирует запрос), transforms (путь внутри значения JSON, массива или диапазона либо часть даты: get_ordering_info("added_at__year").transforms == ("year",)), descending и crosses_to_many. Прямая связь сортирует по своим колонкам ключа без соединения таблиц ("author" → пути ("author_id",)); собственный pk модели — по полям ключа, у составного первичного ключа — по каждому, в порядке ключа (("id", "version")); обратная связь или «многие-ко-многим» ("tags") и pk связанной модели ("author__pk") — по полям первичного ключа связанной модели, прочитанным через названную связь. Неизвестное имя даёт FieldError: Unknown field nme for ordering: Book has no field 'nme'.

У запроса

QuerySet.get_lookup_info(), get_lookups() и get_ordering_info() учитывают ещё и вычисляемые значения запроса: ключ может начинаться с такого значения, и тогда он описывается типом этого значения (Count() — целое число).

queryset = Author.objects.annotate(book_count=Count("books"))
queryset.get_lookup_info("book_count__gte").value_type    # int

Пока подключения не настроены

Для любого описания нужны только модели, подключения не нужны. Hare.bind_models() привязывает модели всей конфигурации — связи между ними, заменяемые модели, фильтры и сортировки каждой модели — синхронно и не оставляет текущего HareContext:

from hare import Hare

Hare.bind_models(config=CONFIG)  # та же конфигурация, что принимает Hare.init()

Book._meta.get_lookup_info("author__name__icontains").value_type  # str
Book.objects.all().get_ordering_info("-published_at").descending           # True

Он принимает конфигурацию в любом виде, который принимает Hare.init() (словарь, HareConfig, путь к файлу или "module.VARIABLE"), и описывает все ключи, которые описывает полный Hare.init(). Фреймворк, который строит сигнатуры и схемы обработчиков запросов при создании приложения, ещё до того, как при запуске откроются подключения, читает фильтры отсюда. Hare.init() — или HareContext.init() в любом контексте, в том числе с глобальным запасным контекстом, — позже настраивает подключения как обычно.

Model._meta.is_bound показывает, привязана ли модель: False сразу после объявления класса и True, как только его привяжет любой из этих вызовов:

Book._meta.is_bound  # False
Hare.bind_models(config=CONFIG)
Book._meta.is_bound  # True

До этого любое описание даёт ConfigurationError (см. Ошибки). Запрос, созданный до привязки (BOOKS = Book.objects.all() на уровне модуля), описывает свои ключи, как только модель будет привязана.

Проверка в .filter() и .order_by()

.filter(), .exclude() и .order_by() проверяют каждый ключ при вызове — весь путь через связи, поле и оператор, в том числе ключи внутри объектов Q:

Book.objects.filter(author__nme="Le Guin")
# FieldError: Book.objects.filter(author__nme=...): Author has no field 'nme'

Запрос, построенный до Hare.init(), проверяется, когда init() проходит по нему заново, и init() сообщает обо всех ошибочных запросах вместе с местом, где каждый был создан.