【问题标题】:Single Table Inheritance in DjangoDjango中的单表继承
【发布时间】:2008-10-27 20:18:08
【问题描述】:

Django 中是否明确支持单表继承?上次我听说,该功能仍在开发和辩论中。

我是否可以同时使用库/黑客来捕获基本行为?我有一个混合不同对象的层次结构。具有 Employee 类、员工类型的子类和 manager_id (parent_id) 的公司结构的典型示例将是我正在解决的问题的一个很好的近似值。

就我而言,我想表达这样一种想法,即一名员工可以管理其他员工,同时由不同的员工管理。 Manager 和 Worker 没有单独的类,这使得它很难跨表传播。子类将代表员工的类型——程序员、会计师、销售等,并且独立于谁监督谁(好吧,我想它在某些方面不再是典型的公司)。

【问题讨论】:

标签: python django django-models single-table-inheritance


【解决方案1】:

总结

Django 的proxy models 提供了单表继承的基础。

但是,需要付出一些努力才能使其发挥作用。

跳到最后查看可重复使用的示例。

背景

Martin Fowler 对单表继承 (STI) 的描述如下:

单表继承将继承结构的所有类的所有字段映射到一个表中。

这正是 Django 的 proxy model inheritance 所做的。

请注意,根据blog post from 2010proxy 模型自 Django 1.1 以来就已经存在。

“正常”的 Django 模型是一个具体的模型,即它在数据库中有一个专用表。 有两种类型的 Django 模型没有有专用的数据库表,即。 抽象模型和代理模型:

  • 抽象模型充当具体模型的超类。抽象模型可以定义字段,但它没有数据库表。这些字段仅添加到其具体子类的数据库表中。

  • 代理模型充当具体模型的子类。代理模型不能定义新字段。相反,它对与其具体超类关联的数据库表进行操作。换句话说,一个 Django 具体模型和它的代理都共享一个表。

Django 的代理模型为单表继承提供了基础,即。它们允许不同的模型共享一个表,并且它们允许我们在 Python 端定义代理特定的行为。但是,Django 的默认对象关系映射 (ORM) 并没有提供所有预期的行为,因此需要进行一些定制。多少,这取决于您的需求。

让我们根据下图中的简单数据模型逐步构建一个最小示例:

第 1 步:基本的“代理模型继承”

这是models.py 的内容,用于基本的代理继承实现:

from django.db import models


class Party(models.Model):
    name = models.CharField(max_length=20)
    person_attribute = models.CharField(max_length=20)
    organization_attribute = models.CharField(max_length=20)


class Person(Party):
    class Meta:
        proxy = True


class Organization(Party):
    class Meta:
        proxy = True

PersonOrganization 是两种类型的聚会。

只有Party 模型具有数据库表,因此所有字段都在此模型上定义,包括特定于PersonOrganization 的任何字段。 p>

因为PartyPersonOrganization 都使用Party 数据库表,我们可以为ForeignKey 定义一个ForeignKey 字段,并将三个模型中的任何一个分配给该字段字段,如图中的继承关系所暗示的那样。请注意,如果没有继承,我们需要为每个模型创建一个单独的 ForeignKey 字段。

例如,假设我们定义一个Address模型如下:

class Address(models.Model):
    party = models.ForeignKey(to=Party, on_delete=models.CASCADE)

然后我们可以使用例如初始化Address 对象。 Address(party=person_instance)Address(party=organization_instance)

到目前为止,一切都很好。

但是,如果我们尝试获取与代理模型对应的对象列表,使用例如Person.objects.all(),我们得到所有 Party 对象的列表,即Person 对象和Organization 对象。这是因为代理模型仍然使用超类中的模型管理器(即Party)。

第 2 步:添加代理模型管理器

为了确保Person.objects.all() 只返回Person 对象,我们需要分配一个单独的model manager 来过滤Party 查询集。要启用此过滤,我们需要一个字段来指示对象应该使用哪个代理模型。

明确一点:创建Person 对象意味着向Party 表中添加一行。 Organization 也是如此。为了区分这两者,我们需要一个列来指示一行是代表Person 还是Organization。为了方便和清晰起见,我们添加了一个名为 proxy_name 的字段(即列),并使用它来存储代理类的名称。

所以,输入ProxyManager 模型管理器和proxy_name 字段:

from django.db import models


class ProxyManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(proxy_name=self.model.__name__)


class Party(models.Model):
    proxy_name = models.CharField(max_length=20)
    name = models.CharField(max_length=20)
    person_attribute = models.CharField(max_length=20)
    organization_attribute = models.CharField(max_length=20)

    def save(self, *args, **kwargs):
        self.proxy_name = type(self).__name__
        super().save(*args, **kwargs)


class Person(Party):
    class Meta:
        proxy = True

    objects = ProxyManager()


class Organization(Party):
    class Meta:
        proxy = True

    objects = ProxyManager()

现在Person.objects.all() 返回的查询集将只包含Person 对象(Organization 也是如此)。

但是,这在ForeignKeyParty 的关系的情况下不起作用,如上面的Address.party,因为无论proxy_name 的值如何,这将始终返回一个Party 实例字段(另见docs)。例如,假设我们创建了一个address = Address(party=person_instance),那么address.party 将返回一个Party 实例,而不是Person 实例。

第 3 步:扩展 Party 构造函数

处理相关字段问题的一种方法是扩展Party.__new__ 方法,因此它返回“proxy_name”字段中指定的类的实例。最终结果如下所示:

class Party(models.Model):
    PROXY_FIELD_NAME = 'proxy_name'
    
    proxy_name = models.CharField(max_length=20)
    name = models.CharField(max_length=20)
    person_attribute = models.CharField(max_length=20)
    organization_attribute = models.CharField(max_length=20)

    def save(self, *args, **kwargs):
        """ automatically store the proxy class name in the database """
        self.proxy_name = type(self).__name__
        super().save(*args, **kwargs)

    def __new__(cls, *args, **kwargs):
        party_class = cls
        try:
            # get proxy name, either from kwargs or from args
            proxy_name = kwargs.get(cls.PROXY_FIELD_NAME)
            if proxy_name is None:
                proxy_name_field_index = cls._meta.fields.index(
                    cls._meta.get_field(cls.PROXY_FIELD_NAME))
                proxy_name = args[proxy_name_field_index]
            # get proxy class, by name, from current module
            party_class = getattr(sys.modules[__name__], proxy_name)
        finally:
            return super().__new__(party_class)

如果proxy_name 字段是Person,现在address.party 实际上会返回一个Person 实例。

作为最后一步,我们可以让整个东西可重复使用:

第 4 步:使其可重复使用

为了使我们基本的单表继承实现可重用,我们可以使用 Django 的抽象继承:

inheritance/models.py:

import sys
from django.db import models


class ProxySuper(models.Model):
    class Meta:
        abstract = True

    proxy_name = models.CharField(max_length=20)

    def save(self, *args, **kwargs):
        """ automatically store the proxy class name in the database """
        self.proxy_name = type(self).__name__
        super().save(*args, **kwargs)

    def __new__(cls, *args, **kwargs):
        """ create an instance corresponding to the proxy_name """
        proxy_class = cls
        try:
            field_name = ProxySuper._meta.get_fields()[0].name
            proxy_name = kwargs.get(field_name)
            if proxy_name is None:
                proxy_name_field_index = cls._meta.fields.index(
                    cls._meta.get_field(field_name))
                proxy_name = args[proxy_name_field_index]
            proxy_class = getattr(sys.modules[cls.__module__], proxy_name)
        finally:
            return super().__new__(proxy_class)


class ProxyManager(models.Manager):
    def get_queryset(self):
        """ only include objects in queryset matching current proxy class """
        return super().get_queryset().filter(proxy_name=self.model.__name__)

那么我们就可以实现我们的继承结构如下:

parties/models.py:

from django.db import models
from inheritance.models import ProxySuper, ProxyManager


class Party(ProxySuper):
    name = models.CharField(max_length=20)
    person_attribute = models.CharField(max_length=20)
    organization_attribute = models.CharField(max_length=20)


class Person(Party):
    class Meta:
        proxy = True

    objects = ProxyManager()


class Organization(Party):
    class Meta:
        proxy = True

    objects = ProxyManager()


class Placement(models.Model):
    party = models.ForeignKey(to=Party, on_delete=models.CASCADE)

根据您的需要,可能需要做更多的工作,但我相信这涵盖了一些基础知识。

【讨论】:

  • 非常好的答案,@djvg。 objects = ProxyManager() 不能在 ProxySuper 类里面吗?
  • @artu-hnrq:谢谢你的客气话。乍一看,将ProxyManager 放入ProxySuper 看起来可以工作,而且看起来肯定会更干净。但是,这里是它崩溃的地方:假设,从一张白纸开始,您创建了一个 Person 对象和一个 Organization 对象。在这种情况下,Party.objects.count() 应该产生 2,但是如果将 objects = ProxyManager() 放在 ProxySuper 类中,Party.objects.count() 将产生 0,因为它会查找带有 proxy_name == 'Party' 的对象。跨度>
  • 你是对的!谢谢开导。考虑将子模型视为完全不同的模型,即使它们共享数据库表,这对我来说似乎并没有那么糟糕,但肯定是要意识到这一点。
【解决方案2】:

我认为 OP 以 defined here 的身份询问单表继承:

关系数据库不支持继承,因此在从对象映射到数据库时,我们必须考虑如何在关系表中表示我们良好的继承结构。当映射到关系数据库时,我们会尽量减少在处理多个表中的继承结构时可能快速安装的连接。单表继承将继承结构的所有类的所有字段映射到一个表中。

也就是说,实体类的整个层次结构的单个数据库表。 Django 不支持这种继承。

【讨论】:

  • 只用一张表,django有什么不可以的?
  • 对多个模型使用一个表 - 不支持
  • @eugene 单表继承是在关系数据库中获取继承的几种方法之一。而且我认为它比django目前支持的两种方式要好。
【解决方案3】:

看看我的尝试:

http://djangosnippets.org/snippets/2408/

在 Django 中模拟“每个层次结构的表”,也就是“单表继承”。基类必须包含所有字段。它的子类不允许包含任何额外的字段,最好它们应该是代理。

不完全是“单表继承”,但对于许多情况来说已经足够接近了。

【讨论】:

    【解决方案4】:

    目前Django中有两种继承形式——MTI(模型表继承)和ABC(抽象基类)。

    我写了一个tutorial 来了解幕后发生的事情。

    您也可以参考model inheritance上的官方文档。

    【讨论】:

    【解决方案5】:

    我认为你可以做类似的事情。

    我必须自己实现这个问题的解决方案,我是这样解决的:

    class Citrus(models.Model):
        how_acidic = models.PositiveIntegerField(max_value=100)
        skin_color = models.CharField()
        type = models.CharField()
    
    class TangeloManager(models.Manager):
        def get_query_set(self):
            return super(TangeloManager, self).get_query_set().filter(type='Tangelo')
        
    class Tangelo(models.Model):
       how_acidic = models.PositiveIntegerField(max_value=100)
       skin_color = models.CharField()
       type = models.CharField()
       objects = TangeloManager()
    
       class Meta:
           # 'appname' below is going to vary with the name of your app
           db_table = u'appname_citrus'
    

    这可能有一些锁定问题...我不太确定 django 是如何处理这个问题的。另外,我并没有真正测试上面的代码,纯粹是为了娱乐目的,希望能让你走上正轨。

    【讨论】:

    • 有趣的想法...但我认为 syncdb 会引发诸如“表 appname_citrus 已存在”之类的错误,因为它会在 Citrus 类已创建表 appname_citrus 后尝试创建表。不过我没试过。如果这确实有效,我想您将能够使用抽象基类来避免重新输入字段名称。
    【解决方案6】:

    这可能有用:https://github.com/craigds/django-typed-models 它看起来有点像单表继承的实现,但它的限制是子类不能有任何额外的字段。

    这是 django 开发者邮件列表上关于 STI 的最新讨论: https://groups.google.com/forum/#!msg/django-developers/-UOM8HNUnxg/6k34kopzerEJ

    【讨论】:

      猜你喜欢
      • 2012-09-20
      • 1970-01-01
      • 2017-02-19
      • 2012-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多