Почему пользовательские менеджеры ломают запросы к связанным объектам?
Я знаю, что это длинная статья, но обещаю, что она стоит того, чтобы ее прочитать: полезные решения и знакомство с внутренним устройством Django!
Настройка
Я столкнулся с этой проблемой при работе с моделью журнала, имеющей отношение ForeignKey к другой модели журнала. При десериализации экземпляра я хочу включить связанные с ним экземпляры.
Обратите внимание, что я использую сериализаторы django_rest_framework
и указываю связанные поля в опции Meta.fields
. Это не очень важно, поэтому я не буду приводить код, но могу, если попросят.
Маленький пример:
models.py
class A(models.Model):
...
class B(models.Model):
a = models.models.ForeignKey(A, on_delete=models.CASCADE)
...
Десериализация экземпляра A
должна возвращать что-то вроде: {..., 'b_set': [3, 6, 7], ...}
. Мы получаем возможно пустой массив связанных идентификаторов.
Вопрос
Проблема возникает при добавлении пользовательских менеджеров:
- Сначала мы определяем модель
LoggedModelManager
, все, что она делает, это отфильтровывает журналы текущих экземпляров. - Во-вторых, мы определяем модели
A
и ее логиALog
.A
получает пользовательский менеджер ->, чтобы гарантировать, чтоA.objects.all()
возвращает только экземплярыA
, а неALog
, а затем мы гарантируем, чтоALog
имеет менеджер по умолчанию (django позаботится о фильтрации экземпляров, не относящихся к логам). - В-третьих, сделаем то же самое для
B
. Обратите внимание, чтоB
имеет внешний ключ кA
.
# Defining the Manager
class LoggedModelManager(models.Manager):
# Save the name of the logging model, which are `ALog` and `BLog` in our example.
def __init__(self, *args, **kwargs):
logging_model = kwargs.pop('logging_model')
self.logging_model_name = logging_model.lower()
super().__init__(*args, **kwargs)
# Here we filter out the logs from the logged instances.
def get_queryset(self) -> models.QuerySet:
return super().get_queryset().filter(**{self.logging_model_name+"__isnull": True})
# Define A and its log
class A(models.Model):
objects = LoggedModelManager(logging_model='ALog')
...
class ALog(A):
objects = models.Manager()
instance = models.ForeignKey(A, on_delete=models.CASCADE)
# Define B and its log
class B(models.Model):
objects = LoggedModelManager(logging_model='BLog')
...
class BLog(B):
objects = models.Manager()
instance = models.ForeignKey(B, on_delete=models.CASCADE)
Результат: A десериализуется в: {..., 'alog_set': [...], ...}
. Но обратите внимание, что b_set
отсутствует!
Мое (неинформированное) решение.
После многих часов разочарования и чтения документации, следующая переработка B
исправила эту проблему:
Обратите внимание, что BLog
тоже должен перезаписать эти модификации, но я это опустил.
class B(models.Model):
objects = LoggedModelManager(logging_model='BLog')
objects_as_related = models.Manager()
...
class Meta:
base_manager_name = 'objects'
default_manager_name = 'objects_as_related'
Это работает: A
теперь десериализуется в {..., 'alog_set': [...], 'b_set': [...] ...}
.
Обратите внимание, что менеджер default_manager_name
не может быть моим пользовательским менеджером.
Мой вопрос
Почему добавление пользовательского менеджера нарушает это? Он возвращает QuerySet, так почему бы не вернуть пустой
'b_set': []
вместо несуществующего?Документация по менеджерам по умолчанию очень запутанная, в ней говорится, что:
«По умолчанию Django использует экземпляр класса
Model._base_manager
manager при обращении к связанным объектам, а не_default_manager
на связанном объекте.»
Но мой пример, похоже, показывает обратное? Я неправильно читаю документацию?
- Буду признателен за любое понимание этого явления или исправление моих утверждений!
Спасибо за уделенное время!
После долгих отладок я нашел проблему!
Выпуск
Этот код не сработал так, как было задумано, потому что LoggedModelManager.__init__
не срабатывает при отсутствии параметров.
Я предположил, что менеджер mycustom инстанцируется только тогда, когда определена моя модель B:
class B(models.Model):
objects = LoggedModelManager(logging_model='BLog') # I thought this was the only place the constructor was called.
Однако это не так. При разрешении связанных наборов моделей класс ModelSerializer
из библиотеки Django Rest Framework инстанцирует менеджер для связанной модели. Это инстанцирование происходит без ожидаемых параметров, поэтому возникает исключение, и связанный набор игнорируется. К сожалению, эта ошибка не сообщается сериализатором, что сильно затрудняет отладку.
Решение
Убедитесь, что ваш пользовательский менеджер может быть создан в последующем без параметров. Я сделал это, вернув значение по умолчанию QuerySet
, если это было так:
# Defining the Manager
class LoggedModelManager(models.Manager):
# Save the name of the logging model, which are `ALog` and `BLog` in our example.
def __init__(self, *args, **kwargs):
if 'logging_model' in kwargs:
logging_model = kwargs.pop('logging_model')
self.logging_model_name = logging_model.lower()
else:
self.logging_model_name = None
super().__init__(*args, **kwargs)
# Here we filter out the logs from the logged instances.
def get_queryset(self) -> models.QuerySet:
if self.logging_model_name is None:
return super().get_queryset()
return super().get_queryset().filter(**{self.logging_model_name+"__isnull": True})
Надеюсь, это было полезно! Интересно, стоит ли написать тикет, чтобы убедиться, что сериализаторы Django Rest Framework действительно выдают ошибку, а не замалчивают ее.