Django Models, Custom Model Managers и Foreign Key - не очень хорошо играют вместе

Я упрощу проблему настолько, насколько смогу.

У меня есть две модели:

MyAbstractModel
Person(MyAbstractModel)
LogoImage(MyAbstractModel)

Каждый Person имеет:

`image = ForeignKey(LogoImage, db_index=True, related_name="person", null=True, 
                         on_delete=models.PROTECT)`

В MyAbstractModel определено несколько менеджеров моделей:

  objects = CustomModelManager()
  objects_all_states = models.Manager()

CustomModelManager определяется как:

class CustomModelManager(models.Manager):
    def get_queryset(self):
        return super().get_query().filter(self.model, using=self._db).filter(state='active') 

В моей базе данных есть два объекта в двух таблицах:

Идентификатор личности 1 состояние = 'активный' Идентификатор изображения 1 состояние = 'неактивный'

------ СЕЙЧАС для выпуска ----------------

CORRECT: дает мне объект person

person = Person.objects.get(id=1)

INCORRECT: Я пытаюсь получить идентификатор изображения, что работает. Я получаю объект изображения.

image = photo.image

ПРОБЛЕМА: Я запросил объект person в objcets менеджере, который должен принести только те элементы со active статусом. изображение во внешнем ключе этого person НЕ active - почему я получаю его? Он не использует objects model manger, чтобы получить его.

ПОПЫТКА ВОРКАУТА:

добавили base_manager_name = "objects" к MyAbstractModel классу meta.

ПОВТОРНАЯ ПОПЫТКА:

CORRECT: дает мне объект person

person = Person.objects.get(id=1)

CORRECT: дает мне исключение "Does not Exist".

image = photo.image

However..... Теперь я пробую следующее:

КОРРЕКТ: получить человека

person.objects_all_states.get(id=1)

INCORRECT: бросает DoesNotExist, так как пытается использовать objects менеджер моделей, который я жестко закодировал в MyAbstractModel классе meta.

image = photo.image

СВОДИТСЯ К СЛЕДУЮЩЕМУ: Как заставить тот же менеджер моделей, который используется для получения объекта, использовать его для получения каждого отдельного ForeignKey объекта, который у него есть? Я не могу найти ответ. Я хожу по кругу уже несколько дней. Просто нигде нет четкого ответа.

Почему они плохо играют вместе

  1. objects_all_manager обоих классов являются разными экземплярами, поэтому нет никакой общей информации.
  2. Также нет информации о менеджере, используемом на экземпляре модели.
  3. В соответствии с django.db.models.Model._base_manager, Django просто использует _base_manager:
    return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()
    
    ... где hints будет {'instance': <Person: Person object (1)>}.

Справедливое предупреждение

Django специально упоминает, что этого делать не следует.

From django.db.models.Model._base_manager:

Не отфильтровывайте результаты в подклассе этого типа менеджера

.

Этот менеджер используется для доступа к объектам, которые связаны с какой-то другой моделью. В таких ситуациях Django должен иметь возможность видеть все объекты модели, на которую он ссылается, чтобы можно было получить все, на что ссылаются.

.

Поэтому вы не должны переопределять get_queryset(), чтобы отфильтровать какие-либо строки. Если вы это сделаете, Django вернет неполные результаты.

Как вы могли бы это реализовать

Вы можете:

  • override get() для активного хранения некоторой информации об экземпляре (которая будет передана как подсказка) о том, был ли использован экземпляр CustomModelManager для его получения, а затем
  • в get_queryset, проверить это и попытаться отступить на objects_all_states.
class CustomModelManager(models.Manager):

    def get(self, *args, **kwargs):
        instance = super().get(*args, **kwargs)
        instance.hint_manager = self
        return instance

    def get_queryset(self):
        hint = self._hints.get('instance')
        if hint and isinstance(hint.__class__.objects, self.__class__):
            hint_manager = getattr(hint, 'hint_manager', None)
            if not hint_manager or not isinstance(hint_manager, self.__class__):
                manager = getattr(self.model, 'objects_all_states', None)
                if manager:
                    return manager.db_manager(hints=self._hints).get_queryset()
        return super().get_queryset().filter(state='active')

Ограничения

Одним из, возможно, многих крайних случаев, когда это не будет работать, является запрос person через Person.objects.filter(id=1).first().

Немного более надежный способ с явным контекстом

Использование:

with CustomModelManager.disable_for_instance(person):
    image = person.image

Выполнение:

class CustomModelManager(models.Manager):
    _disabled_for_instances = set()

    @classmethod
    @contextmanager
    def disable_for_instance(cls, instance):
        is_already_in = instance in cls._disabled_for_instances
        if not is_already_in:
            cls._disabled_for_instances.add(instance)
        yield
        if not is_already_in:
            cls._disabled_for_instances.remove(instance)

    def get_queryset(self):
        if self._hints.get('instance') in self._disabled_for_instances:
            return super().get_queryset()
        return super().get_queryset().filter(state='active')

Что ж, я должен указать на несколько недостатков в вашем подходе. Во-первых, не следует переопределять метод get_queryset для менеджера. Вместо этого сделайте отдельный метод для фильтрации конкретных случаев. Еще лучше, если вы создадите пользовательский класс QuerySet с этими методами, поскольку тогда вы сможете соединить их в цепочку

class ActiveQuerySet(QuerySet):
    def active(self):
        return self.filter(state="active")

# in your model
objects = ActiveQueryset.as_manager()

Также не стоит размещать поле state в каждой модели и ожидать, что Django справится с этим за вас. Будет гораздо проще, если вы решите с точки зрения домена, какая модель является вашей корневой моделью, и будете иметь состояние там. Например, если Person может быть неактивным, то, вероятно, все его изображения также неактивны, поэтому вы можете смело предположить, что статус Persons разделяется всеми связанными моделями.

Я бы специально искал способ избежать такой проблемы с точки зрения дизайна, вместо того, чтобы пытаться заставить Django обрабатывать такие случаи фильтрации

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