0x09 -- Django -- 模型介绍 -- 9 -- 模型继承


0x00 – 模型继承

模型继承在 Django 中与普通类继承在 Python 中的工作方式几乎完全相同,其基类应该继承自 django.db.models.Model

你只需要决定父类模型是否需要拥有它们的权利(拥有它们的数据表),或者父类仅作为承载仅子类中可见的公共信息的载体。

Django 有三种可用的继承风格:

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

0x01 – 抽象基类

抽象基类在你要将公共信息放入很多模型时会很有用。

  1. 编写你的基类;
  2. 并在 Meta 类中填入 abstract=True(该模型将不会创建任何数据表);
  3. 当其用作其它模型类的基类时,它的字段会自动添加至子类。

例如:

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 级抽出公共信息的方法,但仍会在子类模型中创建数据表。


0x01 – 1 – Meta 继承

当一个抽象基类被建立,Django 将所有你在基类中申明的 Meta 内部类以属性的形式提供。

  1. 若子类未定义自己的 Meta 类,它会继承父类的 Meta。
  2. 当然,子类定义了 Meta类,也可继承父类的 Meta,比如:
from django.db import models

class CommonInfo(models.Model):
    # ...
    class Meta:
        abstract = True
        ordering = ['name']

class Student(CommonInfo):
    # ...
    class Meta(CommonInfo.Meta):	# 这里设置继承父类的 Meta
        db_table = 'student_info'	# 在继承父类 Meta 的同时添加自己的 Meta
  • 抽象基类的子类不会自动地变成抽象类
    Django 在安装 Meta 属性前,对抽象基类的 Meta 做了一个调整——设置 abstract=False
    为了继承一个抽象基类创建另一个抽象基类,你需要在子类上显式地设置 abstract=True
  • 抽象基类的某些 Meta 属性对子类是没用的
    比如,包含 db_table 意味着所有的子类(你并未在子类中指定它们的 Meta)会使用同一张数据表,这肯定不是你想要的。
  • 如果子类从多个抽象基类继承,默认情况下仅继承第一个列出的类的 Meta 选项
    这是因为Python继承的工作方式是这样的,
    为了从多个抽象类中继承 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

# 需要从多个父类继承 Meta 
class Student(CommonInfo, Unmanaged):	# 需要在这里显式的声明需要继承的两个父类名称
    home_group = models.CharField(max_length=5)

    class Meta(CommonInfo.Meta, Unmanaged.Meta):	# 同时在这里显式的声明需要继承的两个父类的 Meta
        pass

0x01 – 2 – 对 related_namerelated_query_name 要格外小心

若你在 外键多对多字段 使用了 related_namerelated_query_name,你必须为该字段提供一个 独一无二 的反向名字和查询名字。这在抽象基类中一般会引发问题,因为基类中的字段都被子类继承,且保持了同样的值(包括 related_namerelated_query_name)。

为了解决此问题,当你在抽象基类中(也只能是在抽象基类中)使用 related_namerelated_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。


0x02 – 多表继承

Django 支持的第二种模型继承方式是:

  1. 层次结构中的每个模型都是一个单独的模型;
  2. 每个模型都指向分离的数据表,且可被独立查询和创建。
  3. 继承关系介绍了子类和父类之间的连接(通过一个自动创建的 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。


0x02 – 1 – Meta 和多表继承

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

0x02 – 2 – 继承与反向关系

由于多表继承使用隐式的 OneToOneField 连接子类和父类,所以直接从父类访问子类是可能的,就像上述例子展示的那样。然而,使用的名字是 ForeignKeyManyToManyField 关系的默认值。如果你在继承父类模型的子类中添加了这些关联,你 必须 指定 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 字段能解决此错误: 
class Supplier(Place):
	models.ManyToManyField(Place, related_name='provider')

0x02 – 3 – 指定父类连接字段

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


0x03 – 代理模型

使用 多表继承 时,每个子类模型都会创建一张新表。
这一般是期望的行为,因为子类需要一个地方存储基类中不存在的额外数据字段。
不过,有时候你只想修改模型的 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”属性 和普通模型一样。


0x03 – 1 – QuerySet 仍会返回请求的模型

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


0x03 – 2 – 基类约束

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


0x03 – 3 – 代理模型管理器

若你未在代理模型中指定模型管理器,它会从父类模型中继承。
如果你在代理模型中指定了管理器,它会成为默认管理器,但父类中定义的管理器仍是可用的。

随着上面的例子一路走下来,你可以在查询 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

通常情况下,你可能不需要这么做。然而,你需要的时候,这也是可以的。


0x03 – 4 – 代理继承和未托管的模型间的区别

代理模型继承可能看起来和创建未托管的模型很类似,通过在模型的 Meta 类中定义 managed 属性。

通过小心地配置 Meta.db_table,你将创建一个未托管的模型,该模型将对现有模型进行阴影处理,并添加一些 Python 方法。
然而,这会是个经常重复的且容易出错的过程,因为你要在做任何修改时保持两个副本的同步。

另一方面,代理模型意在表现的和所代理的模型一样。它们总是与父模型保持一致,因为它们直接从福利继承字段和管理器。

通用性规则:

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

0x04 – 多重继承

和 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

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

在正常的 Python 类继承中,允许子类覆盖父类的任何属性。
在 Django 中,模型字段通常不允许这样做。
如果一个非抽象模型基类有一个名为 author 的字段,你就不能在继承自该基类的任何类中,创建另一个名为 author 的模型字段或属性。

这个限制并不适用于从抽象模型继承的模型字段。
这些字段可以用另一个字段或值覆盖,或者通过设置 field_name = None 来删除。

警告
模型管理器是从抽象基类中继承的。重写一个被继承的 Manager 所引用的继承字段,可能会导致微妙的错误。参见 自定义管理器和模型继承。
注解
某些字段在模型内定义了额外的属性,例如 ForeignKey 定义了一个额外的属性 _id 附加在字段名上,类似的还有外键上的 related_namerelated_query_name

这些额外的属性不能被覆盖,除非定义它的字段被改变或删除,使它不再定义额外的属性。

重写父模型中的字段会导致一些困难,比如初始化新实例(在 Model.init 中指定哪个字段被初始化)和序列化。这些都是普通的 Python 类继承所不需要处理的功能,所以 Django 模型继承和 Python 类继承之间的区别并不是任意的。

这些限制只针对那些是 Field 实例的属性。普通的 Python 属性可被随便重写。它还对 Python 能识别的属性生效:如果你同时在子类和多表继承的祖先类中指定了数据表的列名(它们是两张不同的数据表中的列)。

若你在祖先模型中重写了任何模型字段,Django 会抛出一个 FieldError。


2021年9月22日

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值