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