Django中的单表继承

37 投票
6 回答
17427 浏览
提问于 2025-04-11 09:33

在Django中有没有明确支持单表继承的功能?我最后听说这个特性还在开发和讨论中。

在这段时间里,有没有什么库或者技巧可以让我实现基本的功能?我有一个层级结构,里面混合了不同的对象。一个经典的例子是公司结构,有一个员工类,下面有不同类型员工的子类,还有一个经理ID(父ID),这大致上可以代表我正在解决的问题。

在我的情况下,我想表示一个员工可以管理其他员工,同时又被另一个员工管理。这里没有单独的经理和员工类,这让在表中分开存储变得困难。子类会代表不同类型的员工,比如程序员、会计、销售等,而这些与谁管理谁并没有关系(好吧,我想在某种程度上这已经不算是典型的公司结构了)。

6 个回答

21

我觉得提问者是在问单表继承,具体可以参考这里的定义

关系型数据库不支持继承,所以当我们把对象映射到数据库时,就得考虑如何在关系表中表示我们漂亮的继承结构。在映射到关系型数据库时,我们会尽量减少连接操作,因为在处理多个表的继承结构时,连接操作会迅速增加复杂度。单表继承就是把所有继承结构中所有类的字段都映射到一个表里。

也就是说,整个实体类的层级结构只用一个数据库表来表示。Django不支持这种类型的继承。

32

总结

Django的代理模型为单表继承提供了基础。

不过,要让它正常工作,还是需要一些努力。

想要快速了解的可以直接跳到最后看一个可重用的例子。

背景

Martin Fowler对单表继承(STI)是这样描述的:

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

这正是Django的代理模型继承所做的事情。

需要注意的是,根据这篇2010年的博客文章proxy模型自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的字段。

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

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

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

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

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

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

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

为了确保Person.objects.all()只返回Person对象,我们需要分配一个单独的模型管理器来过滤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也是如此)。

然而,这在与PartyForeignKey关系中并不适用,比如上面的Address.party,因为那总是返回一个Party实例,而不管proxy_name字段的值是什么(详细信息见文档)。例如,假设我们创建了一个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)

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

19

在Django中,目前有两种继承方式 - 一种是MTI(模型表继承),另一种是ABC(抽象基类)。

我写了一篇教程,详细讲解了这些背后的原理。

你还可以参考官方文档,了解模型继承的相关内容。

撰写回答