6

我正在处理一个我没有开始的 Django 项目,我面临着继承问题。
我有一个大模型(在示例中进行了简化)MyModel,它应该代表不同类型的项目。

的所有实例对象MyModel都应具有相同的字段,但方法行为因项目类型而异。

到目前为止,它是使用一个MyModel名为item_type.
然后 MyModel 中定义的方法检查该字段并使用多个 if 执行不同的逻辑:

def example_method(self):
    if self.item_type == TYPE_A:
        do_this()
    if self.item_type == TYPE_B1:
        do_that()

更重要的是,一些子类型有很多共同点,所以我们说子类型BC代表第一级继承。然后这些类型有子类型,例如B1, B2, C1, C2(在下面的示例代码中有更好的解释)。

我会说这不是执行多态性的最佳方法。

现在我想更改这些模型以使用真正的继承。

由于所有子模型都具有相同的字段,我认为不需要多表继承。我正在考虑使用代理模型,因为只有它们的行为应该根据它们的类型而改变。

这是我想出的伪解决方案:

ITEM_TYPE_CHOICES = (
    (TYPE_A, _('Type A')),
    (TYPE_B1, _('Type B1')),
    (TYPE_B2, _('Type B2')),
    (TYPE_C1, _('Type C1')),
    (TYPE_C2, _('Type C2')))


class MyModel(models.Model):
    item_type = models.CharField(max_length=12, choices=ITEM_TYPE_CHOICES)

    def common_thing(self):
        pass

    def do_something(self):
        pass


class ModelA(MyModel):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_A

    def do_something(self):
        return 'Hola'


class ModelB(MyModel):
    class Meta:
        proxy = True

    def common_thing(self):
        pass

class ModelB1(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B1

    def do_something(self):
        pass


class ModelB2(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B2

    def do_something(self):
        pass

如果我们已经知道我们正在处理的对象的类型,这可能会起作用。
假设我们要实例化 C1 类型的 MyModel 对象,那么我们可以简单地实例化 aModelC1并且 item_type 将被正确设置。

问题是如何从通用 MyModel 实例中获取正确的代理模型?

最常见的情况是当我们得到一个查询集结果时:MyModel.objects.all()所有这些对象都是 MyModel 的实例,它们对代理一无所知。

我见过不同的解决方案,比如 django-polymorphic,但据我所知,它依赖于多表继承,不是吗?

我见过的几个 SO 答案和自定义解决方案:

但他们都没有100%说服我..

考虑到这可能是一种常见的情况,是否有人提出了更好的解决方案?

4

3 回答 3

1

我对模型代理的经验很少,所以我不知道这是否会正常工作(我的意思没有任何意义),也不知道这可能有多复杂,但您可以使用item_type:ProxyClass映射并覆盖模型的查询集(或提供第二个经理自定义查询集等)实际查找此映射并实例化正确的代理模型。

顺便说一句,您可能想要django.models.base.Model.from_db,它(从源代码的快速浏览)似乎是QuerySet.populate()实例化模型所调用的方法。只需覆盖此方法可能就足以解决问题 - 但在这里它也可能会破坏某些东西......

于 2018-05-25T11:08:47.317 回答
1

我想出了一个受此SO 答案和此博客文章启发的自定义解决方案:

from django.db import models
from django.dispatch.dispatcher import receiver

ITEM_TYPE_CHOICES = (
  (TYPE_A, _('type_a')),
  (TYPE_B1, _('type_b1')),
  (TYPE_B2, _('type_b2')),
  (TYPE_C1, _('type_c1')),
  (TYPE_C2, _('type_c2')),
)

class MyModel(models.Model):
    item_type = models.CharField(max_length=12, choices=ITEM_TYPE_CHOICES)        
    description = models.TextField(blank=True, null=True)

    def common_thing(self):
        pass

    def do_something(self):
        pass

    # ****************
    # Hacking Django *
    # ****************
    PROXY_CLASS_MAP = {}  # We don't know this yet

    @classmethod
    def register_proxy_class(cls, item_type):
        """Class decorator for registering subclasses."""
        def decorate(subclass):
            cls.PROXY_CLASS_MAP[item_type] = subclass
            return subclass
        return decorate

    def get_proxy_class(self):
        return self.PROXY_CLASS_MAP.get(self.item_type, MyModel)


# REGISTER SUBCLASSES

@MyModel.register_proxy_class(TYPE_A)
class ModelA(MyModel):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_A

    def do_something(self):
        pass

# No need to register this, it's never instantiated directly 
class ModelB(MyModel):
    class Meta:
        proxy = True

    def common_thing(self):
        pass

@MyModel.register_proxy_class(TYPE_B1)
class ModelB1(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B1

    def do_something(self):
        pass

@MyModel.register_proxy_class(TYPE_B2)
class ModelB2(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B2

    def do_something(self):
        pass


# USING SIGNAL TO CHANGE `__class__` at runtime

@receiver(models.signals.post_init, sender=MyModel)
def update_proxy_object(sender, **kwargs):
    instance = kwargs['instance']
    if hasattr(instance, "get_proxy_class") and not instance._meta.proxy:
        proxy_class = instance.get_proxy_class()
        if proxy_class is not None:
            instance.__class__ = proxy_class

我使用装饰器在声明register_proxy_class后注册每个子类,MyModel否则我需要{type: subclass}在 MyModel 中显式声明一个映射。这会很糟糕:

  1. 因为在声明时我们不能引用 MyModel 中的任何代理子类(我们可以用字符串名称解决这些问题)
  2. 父母会知道它的子类违反了 OOP 原则。

它是如何工作的

使用@register_proxy_class(type)装饰器每个子类注册自己,实际上MyModel.PROXY_CLASS_MAP在加载模块时创建一个条目到 dict 中。

update_proxy_object每当MyModel分派post_init信号时执行。它在运行时更改__class__实例MyModel以选择正确的代理子类。

所以基本上:

# a1: MyModel dispatch a post_init signal -> `update_proxy_object` set the proper instance __class__ = ModelA
# Do NOT call ModelA.__init__
a1 = MyModel(item_type=TYPE_A)  
isinstance(a1, MyModel) # True
isinstance(a1, ModelA)  # True

# a2: calls ModelA.__init__ that call the parent MyModel.__init__ then it sets up the item_type for us
a2 = ModelA() # <- no need to pass item_type
isinstance(a2,MyModel) # True
isinstance(a2, ModelA)  #True

# Using custom managers of MyModel return all objects having item_type == 'TYPE_B1'
b1 = MyModel.objects.b1()[0]  # get the first one
isinstance(b1, ModelB1)  # True
isinstance(b1, ModelB)   # True
isinstance(b1, MyModel)  # True
isinstance(b1, ModelA)   # False

到目前为止它似乎有效,但我会针对我没有考虑过的可能问题进行更多实验。

凉爽的!

于 2018-05-25T14:19:25.847 回答
1

当您在基本模型中使用 django-polymorphic 时,您将免费获得这种转换行为:

class MyModel(PolymorphicModel):
    pass

从它扩展的每个模型(代理模型或具体模型),当您执行MyModel.objects.all()

于 2018-08-23T15:43:22.710 回答