使用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
通过Person.image
字段有一个外键连接到Image ID 1
。
------现在的问题----------------
# 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
为什么不正确?因为我使用objects
模型管理器查询person对象,它应该只带来那些具有active
状态的项目。它带来了Person
,这很好,因为Person (ID=1)
是state==active
——但person.image
下的对象是state==inactive
。为什么我得到它?
WORKAROND尝试:
将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
因为我得到了不关心state==active
的objects_all_states
下的Person——我希望我也能以类似的方式得到person.image
。但这并不像预期的那样工作。
根源问题
我如何强制相同的模型管理器用于获取父对象(Person
) -在Person
的每一个ForeignKey
对象的抓取?我找不到答案。我这几天一直在兜圈子。根本没有明确的答案。要么我遗漏了一些非常基本的东西,要么Django有一个设计缺陷(当然我不相信)——所以,我在这里遗漏了什么
为什么他们在一起打得不好
- 外键类使用单独的管理器实例,因此没有共享状态。
- 没有关于父实例上使用的管理器的信息。
- 根据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:
<标题>不要过滤掉这种类型的管理器子类中的任何结果
此管理器用于访问与其他模型相关的对象。在这些情况下,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.objects.filter(id=1).first()
查询person
,这可能是无法工作的许多边缘情况之一。
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')
标题>标题>标题>好吧,我必须指出你的方法中的一些设计缺陷。首先,您不应该为管理器重写get_queryset
方法。相反,应该创建一个单独的方法来过滤特定的情况。如果使用这些方法创建一个自定义QuerySet类就更好了,因为这样就可以将它们链接起来
class ActiveQuerySet(QuerySet):
def active(self):
return self.filter(state="active")
# in your model
objects = ActiveQueryset.as_manager()
同样,你不应该把state
字段放在每个模型中,并期望Django会为你处理它。如果您从域的角度决定哪个模型是您的根模型并在那里拥有状态,那么处理起来会容易得多。例如,如果Person可以处于非活动状态,那么可能他的所有图像也都处于非活动状态,因此您可以放心地假设,所有相关模型都共享Person状态。
我会从设计的角度寻找一种方法来避免这样的问题,而不是试图强迫Django处理这样的过滤情况