Django 模型、自定义模型管理器和外键——不能很好地协同工作



使用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字段,可以是activeinactive

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"添加到MyAbstractModelclass 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==activeobjects_all_states下的Person——我希望我也能以类似的方式得到person.image。但这并不像预期的那样工作。

根源问题

我如何强制相同的模型管理器用于获取父对象(Person) -在Person的每一个ForeignKey对象的抓取?我找不到答案。我这几天一直在兜圈子。根本没有明确的答案。要么我遗漏了一些非常基本的东西,要么Django有一个设计缺陷(当然我不相信)——所以,我在这里遗漏了什么

?

为什么他们在一起打得不好

  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:

不要过滤掉这种类型的管理器子类中的任何结果

此管理器用于访问与其他模型相关的对象。在这些情况下,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处理这样的过滤情况