Почему пользовательские менеджеры ломают запросы к связанным объектам?

Я знаю, что это длинная статья, но обещаю, что она стоит того, чтобы ее прочитать: полезные решения и знакомство с внутренним устройством 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 не может быть моим пользовательским менеджером.

Мой вопрос

  1. Почему добавление пользовательского менеджера нарушает это? Он возвращает QuerySet, так почему бы не вернуть пустой 'b_set': [] вместо несуществующего?

  2. Документация по менеджерам по умолчанию очень запутанная, в ней говорится, что:

«По умолчанию Django использует экземпляр класса Model._base_manager manager при обращении к связанным объектам, а не _default_manager на связанном объекте.»

Но мой пример, похоже, показывает обратное? Я неправильно читаю документацию?

  1. Буду признателен за любое понимание этого явления или исправление моих утверждений!

Спасибо за уделенное время!

После долгих отладок я нашел проблему!

Выпуск

Этот код не сработал так, как было задумано, потому что 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 действительно выдают ошибку, а не замалчивают ее.

Вернуться на верх