Django4 中文入门教程 Django4.0 模型-继承

2024-02-25 开发教程 Django4 中文入门教程 匿名 8

模型继承在 Django 中与普通类继承在 Python 中的工作方式几乎完全相同,但也仍应遵循本页开头的内容。这意味着其基类应该继承自 ​​django.db.models.Model​​。你只需要决定父类模型是否需要拥有它们的权利(拥有它们的数据表),或者父类仅作为承载仅子类中可见的公共信息的载体。Django 有三种可用的继承风格。

  1. 常见情况下,你仅将父类用于子类公共信息的载体,因为你不会想在每个子类中把这些代码都敲一遍。这样的父类永远都不会单独使用,所以 抽象基类 是你需要的。
  2. 若你继承了一个模型(可能来源其它应用),且想要每个模型都有对应的数据表,请使用多表继承。
  3. 最后,若你只想修改模型的 Python 级行为,而不是以任何形式修改模型字段, 请使用代理模型。

抽象基类

抽象基类在你要将公共信息放入很多模型时会很有用。编写你的基类,并在 ​​Meta​​类中填入​​abstract=True​​。该模型将不会创建任何数据表。当其用作其它模型类的基类时,它的字段会自动添加至子类。

一个例子:

from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class Student(CommonInfo):
home_group = models.CharField(max_length=5)

​​Student​​模型拥有3个字段: ​​name​​, ​​age​​和 ​​home_group​​。 ​​CommonInfo​​模型不能用作普通的 Django 模型,因为它是一个抽象基类。它不会生成数据表,也没有管理器,也不能被实例化和保存。从抽象基类继承来的字段可被其它字段或值重写,或用 ​​None​​删除。对很多用户来说,这种继承可能就是你想要的。它提供了一种在 Python 级抽出公共信息的方法,但仍会在子类模型中创建数据表。

Meta继承

当一个抽象基类被建立,Django 将所有你在基类中申明的 ​​Meta​​内部类以属性的形式提供。若子类未定义自己的 ​​Meta​​类,它会继承父类的 ​​Meta​​。当然,子类也可继承父类的 ​​Meta​​,比如:

from django.db import models
class CommonInfo(models.Model):
# ...
class Meta:
abstract = True
ordering = ['name']
class Student(CommonInfo):
# ...
class Meta(CommonInfo.Meta):
db_table = 'student_info'

Django 在安装 ​​Meta​​属性前,对抽象基类的 ​​Meta​​做了一个调整——设置 ​​abstract=False​​。这意味着抽象基类的子类不会自动地变成抽象类。为了继承一个抽象基类创建另一个抽象基类,你需要在子类上显式地设置 ​​abstract=True​​。抽象基类的某些 ​​Meta​​属性对子类是没用的。比如,包含 ​​db_table​​意味着所有的子类(你并未在子类中指定它们的 ​​Meta​​)会使用同一张数据表,这肯定不是你想要的。由于Python继承的工作方式,如果子类从多个抽象基类继承,则默认情况下仅继承第一个列出的类的 ​​Meta​​选项。为了从多个抽象类中继承 ​​Meta​​选项,必须显式地声明 ​​Meta​​继承。例如:

from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
ordering = ['name']
class Unmanaged(models.Model):
class Meta:
abstract = True
managed = False
class Student(CommonInfo, Unmanaged):
home_group = models.CharField(max_length=5)
class Meta(CommonInfo.Meta, Unmanaged.Meta):
pass

对 related_name 和 related_query_name 要格外小心

若你在 外键 或 多对多字段 使用了 ​​related_name​​或 ​​related_query_name​​,你必须为该字段提供一个 独一无二 的反向名字和查询名字。这在抽象基类中一般会引发问题,因为基类中的字段都被子类继承,且保持了同样的值(包括 ​​related_name​​和 ​​related_query_name​​)。为了解决此问题,当你在抽象基类中(也只能是在抽象基类中)使用 ​​related_name​​和 ​​related_query_name​​,部分值需要包含 ​​'%(app_label)s'​​ 和 ​​'%(class)s'​​。

  • ​'%(class)s'​​用使用了该字段的子类的小写类名替换。
  • ​'%(app_label)s'​​ 用小写的包含子类的应用名替换。每个安装的应用名必须是唯一的,应用内的每个模型类名也必须是唯一的。因此,替换后的名字也是唯一的。

举个例子,有个应用​​common/models.py​​:

from django.db import models
class Base(models.Model):
m2m = models.ManyToManyField(
OtherModel,
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss",
)
class Meta:
abstract = True
class ChildA(Base):
pass
class ChildB(Base):
pass

附带另一个应用 ​​rare/models.py​​:

from common.models import Base
class ChildB(Base):
pass

​common.ChildA.m2m​​ 字段的反转名是 ​​common_childa_related​​,反转查询名是 ​​common_childas​​。 ​​common.ChildB.m2m​​ 字段的反转名是 ​​common_childb_related​​, 反转查询名是 ​​common_childbs​​。 ​​rare.ChildB.m2m​​ 字段的反转名是 ​​rare_childb_related​​,反转查询名是 ​​rare_childbs​​。这决定于你如何使用​​'%(class)s'​​ 和​​'%(app_label)s'​ ​构建关联名字和关联查询名。但是,若你忘了使用它们,Django 会在你执行系统检查(或运行 ​​migrate​​)时抛出错误。如果你未指定抽象基类中的 ​​related_name​​属性,默认的反转名会是子类名,后接 ​​'_set'​​ 。这名字看起来就像你在子类中定义的一样。比如,在上述代码中,若省略了 ​​related_name​​属性, ​​ChildA​​的 ​​m2m​​字段的反转名会是 ​​childa_set​​, ​​ChildB​​的是 ​​childb_set​​。

多表继承

Django 支持的第二种模型继承方式是层次结构中的每个模型都是一个单独的模型。每个模型都指向分离的数据表,且可被独立查询和创建。继承关系介绍了子类和父类之间的连接(通过一个自动创建的 ​​OneToOneField​​)。比如:

from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)

​​Place​的所有字段均在 ​​Restaurant​​中可用,虽然数据分别存在不同的表中。所有,以下操作均可:

>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")

若有一个 ​​Place​​同时也是 ​​Restaurant​​,你可以通过小写的模型名将 ​​Place​​对象转为 ​​Restaurant​​对象。

>>> p = Place.objects.get(id=12)
# If p is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>

然而,若上述例子中的 ​​p​​ 不是 一个 ​​Restaurant​​(它仅是个 ​​Place​​对象或是其它类的父类),指向 ​​p.restaurant​​ 会抛出一个 ​​Restaurant.DoesNotExist​​ 异常。​​Restaurant​​中自动创建的连接至 ​​Place​​的 ​​OneToOneField​​看起来像这样:

place_ptr = models.OneToOneField(
Place, on_delete=models.CASCADE,
parent_link=True,
primary_key=True,
)

你可以在 ​​Restaurant​​中重写该字段,通过申明你自己的 ​​OneToOneField​​,并设置 ​​parent_link=True​​。

Meta和多表继承

多表继承情况下,子类不会继承父类的 ​​Meta​​。所以的 ​​Meta​​类选项已被应用至父类,在子类中再次应用会导致行为冲突(与抽象基类中应用场景对比,这种情况下,基类并不存在)。故子类模型无法访问父类的 ​​Meta​​类。不过,有限的几种情况下:若子类未指定 ​​ordering​​属性或 ​​get_latest_by​​属性,子类会从父类继承这些。如果父类有排序,而你并不期望子类有排序,你可以显示的禁止它:

class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []

继承与反向关系

由于多表继承使用隐式的 ​​OneToOneField​​连接子类和父类,所以直接从父类访问子类是可能的,就像上述例子展示的那样。然而,使用的名字是 ​​ForeignKey​​和 ​​ManyToManyField​​关系的默认值。如果你在继承父类模型的子类中添加了这些关联,你 必须 指定 ​​related_name​​属性。假如你忘了,Django 会抛出一个合法性错误。比如,让我们用上面的 ​​Place​​类创建另一个子类,包含一个 ​​ManyToManyField​​:

class Supplier(Place):
customers = models.ManyToManyField(Place)

这会导致以下错误:

Reverse query name for 'Supplier.customers' clashes with reverse query
name for 'Supplier.place_ptr'.
HINT: Add or change a related_name argument to the definition for
'Supplier.customers' or 'Supplier.place_ptr'.

将 ​​related_name​​像下面这样加至 ​​customers​​字段能解决此错误:​models.ManyToManyField(Place, related_name='provider')​​。

指定父类连接字段

如上所述,Django 会自动创建一个 ​​OneToOneField​​,将子类连接回非抽象的父类。如果你想修改连接回父类的属性名,你可以自己创建 ​​OneToOneField​​,并设置 ​​parent_link=True​​,表明该属性用于连接回父类。

代理模型

使用 多表继承 时,每个子类模型都会创建一张新表。这一般是期望的行为,因为子类需要一个地方存储基类中不存在的额外数据字段。不过,有时候你只想修改模型的 Python 级行为——可能是修改默认管理器,或添加一个方法。这是代理模型继承的目的:为原模型创建一个 代理。你可以创建,删除和更新代理模型的实例,所以的数据都会存储的像你使用原模型(未代理的)一样。不同点是你可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。代理模型就像普通模型一样申明。你需要告诉 Django 这是一个代理模型,通过将 ​​Meta​​类的 ​​proxy​​属性设置为 ​​True​​。例如,假设你想为 ​​Person​​模型添加一个方法。你可以这么做:

from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
class MyPerson(Person):
class Meta:
proxy = True
def do_something(self):
# ...
pass

​​MyPerson​​类与父类 ​​Person​​ 操作同一张数据表。特别提醒, ​​Person​​的实例能通过 ​​MyPerson​​访问,反之亦然。

>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>

你也可以用代理模型定义模型的另一种不同的默认排序方法。你也许不期望总对 ​​“Persion”​​ 进行排序,但是在使用代理时,总是依据 ​​“last_name”​​ 属性进行排序:

class OrderedPerson(Person):
class Meta:
ordering = ["last_name"]
proxy = True

现在,普通的 ​​Person​​查询结果不会被排序,但 ​​OrderdPerson​​查询结果会按 ​​last_name​​排序。代理模型继承​“​Meta​”​属性 和普通模型一样。

QuerySet仍会返回请求的模型

当你用 ​​Person​​对象查询时,Django 永远不会返回 ​​MyPerson​​对象。​​Person​​对象的查询结果集总是返回对应类型。代理对象存在的全部意义是帮你复用原 ​​Person​​提供的代码和自定义的功能代码(并未依赖其它代码)。不存在什么方法能在你创建完代理后,帮你替换所有 ​​Person​​或其它模型。

基类约束

一个代理模型必须继承自一个非抽象模型类。你不能继承多个非抽象模型类,因为代理模型无法在不同数据表之间提供任何行间连接。一个代理模型可以继承任意数量的抽象模型类,假如他们 没有 定义任何的模型字段。一个代理模型也可以继承任意数量的代理模型,只需他们共享同一个非抽象父类。

代理模型管理器

若你未在代理模型中指定模型管理器,它会从父类模型中继承。如果你在代理模型中指定了管理器,它会成为默认管理器,但父类中定义的管理器仍是可用的。随着上面的例子一路走下来,你可以在查询 ​​Person​​模型时这样修改默认管理器:

from django.db import models
class NewManager(models.Manager):
# ...
pass
class MyPerson(Person):
objects = NewManager()
class Meta:
proxy = True

若你在不替换已存在的默认管理器的情况下,为代理添加新管理器,你可以创建一个包含新管理器的基类,在继承列表中,主类后追加这个基类:

# Create an abstract class for the new manager.
class ExtraManagers(models.Model):
secondary = NewManager()
class Meta:
abstract = True
class MyPerson(Person, ExtraManagers):
class Meta:
proxy = True

代理继承和未托管的模型间的区别

代理模型继承可能看起来和创建未托管的模型很类似,通过在模型的 ​​Meta​​类中定义 ​​managed​​属性。通过小心地配置 ​​Meta.db_table​​,你将创建一个未托管的模型,该模型将对现有模型进行阴影处理,并添加一些 Python 方法。然而,这会是个经常重复的且容易出错的过程,因为你要在做任何修改时保持两个副本的同步。另一方面,代理模型意在表现的和所代理的模型一样。它们总是与父模型保持一致,因为它们直接继承其字段和管理器。

通用性规则:

  1. 当你克隆一个已存在模型或数据表时,并且不想要所以的原数据表列,配置 ​​Meta.managed=False​​。这个选项在模型化未受 Django 控制的数据库视图和表格时很有用。
  2. 如果你只想修改模型的 Python 行为,并保留原有字段,配置 ​​Meta.proxy=True​​。这个配置使得代理模型在保存数据时,确保数据结构和原模型的完全一样。

多重继承

和 Python 中的继承一样,Django 模型也能继承自多个父类模型。请记住,Python 的命名规则这里也有效。第一个出现的基类(比如 ​​Meta​​)就是会被使用的那个;举个例子,如果存在多个父类包含 ​​Meta​​,只有第一个会被使用,其它的都会被忽略。一般来说,你并不会同时继承多个父类。常见的应用场景是 “混合” 类:为每个继承此类的添加额外的字段或方法。试着保持你的继承层级尽可能的简单和直接,这样未来你就不用为了确认某段信息是哪来的而拔你为数不多的头发了。注意,继承自多个包含 ​id​主键的字段会抛出错误。正确的使用多继承,你可以在基类中显示使用 ​​AutoField​​:

class Article(models.Model):
article_id = models.AutoField(primary_key=True)
...
class Book(models.Model):
book_id = models.AutoField(primary_key=True)
...
class BookReview(Book, Article):
pass

或者在公共祖先中存储 ​​AutoField​​。这会要求为每个父类模型和公共祖先使用显式的 ​​OneToOneField​​,避免与子类自动生成或继承的字段发生冲突:

class Piece(models.Model):
pass
class Article(Piece):
article_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
...
class Book(Piece):
book_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
...
class BookReview(Book, Article):
pass

字段名“隐藏”是不允许的

在正常的 Python 类继承中,允许子类覆盖父类的任何属性。在 Django 中,模型字段通常不允许这样做。如果一个非抽象模型基类有一个名为 ​​author​​的字段,你就不能在继承自该基类的任何类中,创建另一个名为 ​​author​​的模型字段或属性。这个限制并不适用于从抽象模型继承的模型字段。这些字段可以用另一个字段或值覆盖,或者通过设置 ​​field_name = None​​ 来删除。

模型管理器是从抽象基类中继承的。重写一个被继承的 ​​Manager​​所引用的继承字段,可能会导致微妙的错误。

某些字段在模型内定义了额外的属性,例如 ​​ForeignKey​​定义了一个额外的属性 ​​_id​​ 附加在字段名上,类似的还有外键上的 ​​related_name​​和 ​​related_query_name​​。这些额外的属性不能被覆盖,除非定义它的字段被改变或删除,使它不再定义额外的属性。

重写父模型中的字段会导致一些困难,比如初始化新实例(在 ​​Model.__init__​​ 中指定哪个字段被初始化)和序列化。这些都是普通的 Python 类继承所不需要处理的功能,所以 Django 模型继承和 Python 类继承之间的区别并不是任意的。这些限制只针对那些是 ​​Field​​实例的属性。普通的 Python 属性可被随便重写。它还对 Python 能识别的属性生效:如果你同时在子类和多表继承的祖先类中指定了数据表的列名(它们是两张不同的数据表中的列)。若你在祖先模型中重写了任何模型字段,Django 会抛出一个 ​​FieldError​​。

请注意,由于在类定义期间解析字段的方式,从多个抽象父模型继承的模型字段以严格的深度优先顺序解析。这与标准 Python MRO 形成对比,后者在菱形继承的情况下以广度优先解决。这种差异只影响复杂的模型层次结构,(根据上面的建议)你应该尽量避免。