Про Django ORM и SimpleLazyObject
Недавно я захотел создать собственный middleware, чтобы дополнить объект request
, добавив в него дополнительный атрибут. Но я хотел, чтобы этот атрибут вычислялся лениво. Если у вас есть опыт разработки с Django, вы, вероятно, знаете, что он предоставляет ленивые функции, такие как reverse_lazy
. Изучая внутреннюю реализацию функции, я обнаружил, что Django предоставляет модуль django.utils.functional
, который содержит интересные функции и классы.
Мне понравился SimpleLazyObject
, и я заметил, что он используется в middleware под названием django.contrib.auth.middleware.AuthenticationMiddleware
. Всё казалось просто пока я стал использовать "ленивый" атрибут в ORM-запросе Django 😁
Мой атрибут возвращает список значений, который должен использоваться для фильтрации в запросе, но основная проблема в том, как Django обрабатывает эти значения внутри ORM. Класс Query
имеет метод resolve_lookup_value
, заглянув внутрь него я увидел такое условие:
elif isinstance(value, (list, tuple)):
# The items of the iterable may be expressions and therefore need
# to be resolved independently.
values = (
self.resolve_lookup_value(sub_value, can_reuse, allow_joins, summarize)
for sub_value in value
)
type_ = type(value)
if hasattr(type_, "_make"): # namedtuple
return type_(*values)
return type_(values)
Этот код означает, что ваш экземпляр SimpleLazyObject
будет преобразован в экземпляр SimpleLazyObject
, содержащий генератор, возвращающий список значений от исходного объекта. И это не работает как я изначально ожидал. Моё решение было довольно простым: поскольку у меня был уникальный набор элементов, я заменил список значений на множество (имейте в виду, что кортеж не подойдёт из-за условия elif
, которое проверяет и список, и кортеж).
Если у вас более сложный случай, я бы посоветовал расширить класс LazyObject
(SimpleLazyObject является его подклассом) и реализовать свой собственный метод _setup
.