Django中的单表继承
在Django中有没有明确支持单表继承的功能?我最后听说这个特性还在开发和讨论中。
在这段时间里,有没有什么库或者技巧可以让我实现基本的功能?我有一个层级结构,里面混合了不同的对象。一个经典的例子是公司结构,有一个员工类,下面有不同类型员工的子类,还有一个经理ID(父ID),这大致上可以代表我正在解决的问题。
在我的情况下,我想表示一个员工可以管理其他员工,同时又被另一个员工管理。这里没有单独的经理和员工类,这让在表中分开存储变得困难。子类会代表不同类型的员工,比如程序员、会计、销售等,而这些与谁管理谁并没有关系(好吧,我想在某种程度上这已经不算是典型的公司结构了)。
6 个回答
我觉得提问者是在问单表继承,具体可以参考这里的定义:
关系型数据库不支持继承,所以当我们把对象映射到数据库时,就得考虑如何在关系表中表示我们漂亮的继承结构。在映射到关系型数据库时,我们会尽量减少连接操作,因为在处理多个表的继承结构时,连接操作会迅速增加复杂度。单表继承就是把所有继承结构中所有类的字段都映射到一个表里。
也就是说,整个实体类的层级结构只用一个数据库表来表示。Django不支持这种类型的继承。
总结
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
Person
和Organization
是两种类型的派对。
只有Party
模型有数据库表,因此所有字段都在这个模型中定义,包括任何特定于Person
或Organization
的字段。
因为Party
、Person
和Organization
都使用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
也是如此)。
然而,这在与Party
的ForeignKey
关系中并不适用,比如上面的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)
根据你的需求,可能还需要更多的工作,但我相信这涵盖了一些基础知识。