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

Jas*_*enX 7 django django-models

使用Django 3.2-- 我将尽可能简化问题。

我有三个模型类:

# abstract base class
MyAbstractModel(models.Model)

# derived model classes
Person(MyAbstractModel)
LogoImage(MyAbstractModel)
Run Code Online (Sandbox Code Playgroud)

每个Person都有:

image = ForeignKey(LogoImage, db_index=True, related_name="person", null=True, 
                         on_delete=models.PROTECT)
Run Code Online (Sandbox Code Playgroud)

定义MyAbstractModel了一些模型管理器:

  objects = CustomModelManager()
  objects_all_states = models.Manager()
Run Code Online (Sandbox Code Playgroud)

以及一个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') 
Run Code Online (Sandbox Code Playgroud)

在我的数据库中,两个表中有两个对象:

Person ID 1 state = 'active'
Image ID 1 state = 'inactive'
Run Code Online (Sandbox Code Playgroud)

Person ID 1Image 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
Run Code Online (Sandbox Code Playgroud)

为什么这是不正确的?因为我使用模型管理器查询人员对象,objects该管理器应该只带来那些具有active状态的项目。它带来了 ,这Person很好,因为Person (ID=1)state==active- 但下面的对象person.imagestate==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
Run Code Online (Sandbox Code Playgroud)

然而......现在我尝试这个:

# 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
Run Code Online (Sandbox Code Playgroud)

objects_all_states因为我得到了不关心的人——我希望我也会以类似的方式state==active得到。person.image但这并没有按预期工作。

根本问题

如何强制使用相同的模型管理器来获取父对象(Person)——在获取 a 拥有的每个ForeignKey对象时Person?我找不到答案。我这几天一直在兜圈子。任何地方都没有明确的答案。要么我错过了一些非常基本的东西,要么 Django 有一个设计缺陷(当然我并不真正相信)——那么,我在这里错过了什么?

aar*_*ron 5

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

\n
    \n
  1. 外键类使用单独的管理器实例,因此不存在共享状态。
  2. \n
  3. 也没有有关父实例上使用的管理器的信息。
  4. \n
  5. 根据django.db.models.Model._base_manager,Django 只需使用_base_manager:\n
    return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()\n
    Run Code Online (Sandbox Code Playgroud)\n...哪里hints会是{\'instance\': <Person: Person object (1)>}
  6. \n
\n

由于我们有对父级的引用,因此在某些情况下,我们可以支持这个推论。

\n

公平警告

\n

Django 特别提到不要这样做。

\n

来自django.db.models.Model._base_manager

\n
\n

Don\xe2\x80\x99t 过滤掉此类管理器子类中的任何结果

\n

该管理器用于访问与其他模型相关的对象。在这些情况下,Django 必须能够看到它正在获取的模型的所有对象,以便可以检索引用的任何内容。

\n

因此,您不应覆盖get_queryset()以过滤掉任何行。如果这样做,Django 将返回不完整的结果。

\n
\n

1. 如何实现这个推论

\n

你可以:

\n
    \n
  • overrideget()以主动存储有关实例的一些信息(将作为提示传递),了解是否CustomModelManager使用 的实例来获取它,然后
  • \n
  • get_queryset,检查并尝试回退到objects_all_states.
  • \n
\n
class CustomModelManager(models.Manager):\n\n    def get(self, *args, **kwargs):\n        instance = super().get(*args, **kwargs)\n        instance.hint_manager = self\n        return instance\n\n    def get_queryset(self):\n        hint = self._hints.get(\'instance\')\n        if hint and isinstance(hint.__class__.objects, self.__class__):\n            hint_manager = getattr(hint, \'hint_manager\', None)\n            if not hint_manager or not isinstance(hint_manager, self.__class__):\n                manager = getattr(self.model, \'objects_all_states\', None)\n                if manager:\n                    return manager.db_manager(hints=self._hints).get_queryset()\n        return super().get_queryset().filter(state=\'active\')\n
Run Code Online (Sandbox Code Playgroud)\n

局限性

\n

这可能不起作用的许多边缘情况之一是如果您person通过Person.objects.filter(id=1).first().

\n

2. 使用显式实例上下文

\n

用法:

\n
person = Person.objects_all_states.get(id=1)\n# image = person.image\nwith CustomModelManager.disable_for_instance(person):\n    image = person.image\n
Run Code Online (Sandbox Code Playgroud)\n

执行:

\n
class CustomModelManager(models.Manager):\n    _disabled_for_instances = set()\n\n    @classmethod\n    @contextmanager\n    def disable_for_instance(cls, instance):\n        is_already_in = instance in cls._disabled_for_instances\n        if not is_already_in:\n            cls._disabled_for_instances.add(instance)\n        yield\n        if not is_already_in:\n            cls._disabled_for_instances.remove(instance)\n\n    def get_queryset(self):\n        if self._hints.get(\'instance\') in self._disabled_for_instances:\n            return super().get_queryset()\n        return super().get_queryset().filter(state=\'active\')\n
Run Code Online (Sandbox Code Playgroud)\n

3. 使用显式线程本地上下文

\n

用法:

\n
# person = Person.objects_all_states.get(id=1)\n# image = person.image\nwith CustomModelManager.disable():\n    person = Person.objects.get(id=1)\n    image = person.image\n
Run Code Online (Sandbox Code Playgroud)\n

执行:

\n
import threading\nfrom contextlib import contextmanager\n\nfrom django.db import models\nfrom django.utils.functional import classproperty\n\n\nclass CustomModelManager(models.Manager):\n    _data = threading.local()\n\n    @classmethod\n    @contextmanager\n    def disable(cls):\n        is_disabled = cls._is_disabled\n        cls._data.is_disabled = True\n        yield\n        cls._data.is_disabled = is_disabled\n\n    @classproperty\n    def _is_disabled(cls):\n        return getattr(cls._data, \'is_disabled\', None)\n\n    def get_queryset(self):\n        if self._is_disabled:\n            return super().get_queryset()\n        return super().get_queryset().filter(state=\'active\')\n
Run Code Online (Sandbox Code Playgroud)\n