Модели Django, менеджеры пользовательских моделей и внешний ключ — плохо сочетаются

#django #django-models

Вопрос:

Используя Django 3.2 — я максимально упрощу проблему.

У меня есть три класса моделей:

 # abstract base class
MyAbstractModel(models.Model)

# derived model classes
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()
 

а также state поле, которое может быть либо active или inactive

CustomModelManager определяется как нечто, что будет содержать только записи, имеющие состояние == ‘active’:

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

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

 Person ID 1 state = 'active'
Image ID 1 state = 'inactive'
 

Person ID 1 имеет подключение к внешнему ключу Image ID 1 через Person.image поле.

—— ТЕПЕРЬ о проблеме —————-

 # CORRECT: gives me the person object
person = Person.objects.get(id=1)
# INCORRECT: I get the image, but it should not work... 
image = person.image
 

Почему это неверно?потому что я запросил объект person, используя диспетчер objects моделей, который должен приносить только те элементы со active статусом. Это привело Person к тому, что это нормально, потому Person (ID=1) что есть state==active — но объект под person.image есть state==inactive . Почему я это понимаю?

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

добавлено base_manager_name = "objects" в MyAbstractModel class Meta: раздел

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

 # CORRECT: gives me the person object
person = Person.objects.get(id=1)
# CORRECT: gives me a "Does not Exist" exception.  
image = person.image
 

Однако….. Теперь я попробую это:

 # CORRECT: getting the person
person.objects_all_states.get(id=1)
# INCORRECT: throws a DoesNotExist, as it's trying to use the `objects` model manager I hard coded in the `MyAbstractModel` class meta. 
image = person.image
 

Поскольку у меня есть человек, objects_all_states которому все равно state==active , я ожидаю, что я также получу person.image аналогичным образом. Но это работает не так, как ожидалось.

ОСНОВНАЯ ПРОБЛЕМА

Как мне заставить тот же менеджер моделей, который использовался для извлечения родительского объекта ( Person ) — при извлечении каждого отдельного ForeignKey объекта, который есть у a Person ? Я не могу найти ответ. Я уже несколько дней хожу по кругу. Четкого ответа просто нет нигде. Либо я упускаю что-то очень фундаментальное, либо у Django есть недостаток дизайна (в который я, конечно, не очень верю) — итак, чего мне здесь не хватает?

Комментарии:

1. Я не думаю, что вы можете обязательно «наследовать» базовый менеджер, который вы использовали для извлечения исходной модели, чтобы повлиять на связанные дескрипторы объектов в модели. (Он должен быть достаточно умен, чтобы влиять только на тех менеджеров, на которых он может повлиять; как насчет внешних ключей к объектам, которых нет state="active" ?) Я бы не сказал, что это недостаток дизайна, это просто не особенность…

Ответ №1:

Почему они плохо сочетаются

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

Из django.db.models .Model._base_manager:

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

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

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

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

Вы могли бы:

  • переопределите 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 via Person.objects.filter(id=1).first() .

2. Использование явного контекста экземпляра

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

 person = Person.objects_all_states.get(id=1)
# image = person.image
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')
 

3. Использование явного локального контекста потока

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

 # person = Person.objects_all_states.get(id=1)
# image = person.image
with CustomModelManager.disable():
    person = Person.objects.get(id=1)
    image = person.image
 

Реализация:

 import threading
from contextlib import contextmanager

from django.db import models
from django.utils.functional import classproperty


class CustomModelManager(models.Manager):
    _data = threading.local()

    @classmethod
    @contextmanager
    def disable(cls):
        is_disabled = cls._is_disabled
        cls._data.is_disabled = True
        yield
        cls._data.is_disabled = is_disabled

    @classproperty
    def _is_disabled(cls):
        return getattr(cls._data, 'is_disabled', None)

    def get_queryset(self):
        if self._is_disabled:
            return super().get_queryset()
        return super().get_queryset().filter(state='active')
 

Ответ №2:

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

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

# in your model
objects = ActiveQueryset.as_manager()

 

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

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