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
объекта, который у него есть? Я не могу найти ответ. Я хожу по кругу уже несколько дней. Просто нигде нет четкого ответа.
Почему они плохо играют вместе
objects_all_manager
обоих классов являются разными экземплярами, поэтому нет никакой общей информации.- Также нет информации о менеджере, используемом на экземпляре модели.
- В соответствии с 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 обрабатывать такие случаи фильтрации