model 的继承
# model 的继承
Django 中所有的模型都必须直接或间接地继承自 django.db.models.Model。
而在实际开发中,对于 model 的继承我们需要决定,父模型是否是一个在数据库中创建数据表的模型,还是一个只用来保存子模型共有内容、并不实际创建数据表的抽象模型。
基于不同的需求,在 Django 中有三种继承方式:
- 抽象基类(Abstract base classes):父类只用来保存每个子类共有的信息,它本身是不会被独立使用的,而且它不会创建实际的数据库表。
- 多表继承(Multi-table inheritance):每一个模型都有自己的数据库表,父子之间独立存在。
- 代理模型(Proxy models):如果你只想修改模型的 Python 层面的行为,并不想改动模型的字段,可以使用代理模型。
# 抽象基类
# 用法示例
只需要在模型的 Meta 类里添加 abstract=True
元数据项,就可以将一个模型转换为抽象基类。Django 不会为这种类创建实际的数据库表,它们也没有管理器,不能被实例化也无法直接保存。抽象基类完全就是用来保存子模型们共有的内容部分,达到重用的目的。当它们被继承时,它们的字段会全部复制到子模型中。
比如下面的例子:
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)
2
3
4
5
6
7
8
9
10
11
# Meta 数据
如果子类没有声明自己的 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'
2
3
4
5
6
7
8
9
10
11
12
总结一下:
- 抽象基类中有的元数据,子模型没有的话,直接继承。
- 抽象基类中有的元数据,子模型也有的话,直接覆盖。
- 子模型可以额外添加元数据。
- 抽象基类中的
abstract=True
这个元数据不会被继承。也就是说如果想让一个抽象基类的子模型,同样成为一个抽象基类,那必须显式地在该子模型的 Meta 中同样声明一个abstract = True
。 - 有一些元数据对抽象基类无效,比如
db_table
,因为抽象基类本身不会创建数据表,而且它的所有子类也不会按照这个元数据来设置表名。 - 由于 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# related_name 和 related_query_name
如果在抽象基类中存在 ForeignKey 或者 ManyToManyField 字段,并且使用了 related_name
或者 related_query_name
参数,那么会导致抽象基类出现问题。因为按照默认规则,每一个子类都将拥有同样的字段,在关联对象反查的时候会出现混乱。
为了解决这个问题,当在抽象基类中使用 related_name
或者 related_query_name
参数时,它们两者的值中应该包含 %(app_label)s
和 %(class)s
部分:
%(class)s
用字段所属子类的小写名替换%(app_label)s
用子类所属 app 的小写名替换
例如,对于 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
对于另外一个应用中的 rare/models.py:
from common.models import Base
class ChildB(Base):
pass
2
3
4
对于上面的继承关系:
common.ChildA.m2m
字段的反向名称应该是common_childa_related
;反向查询名称应该是common_childas
。common.ChildB.m2m
字段的反向名称应该是common_childb_related
;反向查询名称应该是common_childbs
。rare.ChildB.m2m
字段的反向名称应该是rare_childb_related
;反向查询名称应该是rare_childbs
。
当然,如果你不设置 related_name
或者 related_query_name
参数,这些问题就不存在了。因为默认反向名称将是子类的名称,后跟 _set
。
# 多表继承
# 用法示例
这种继承方式下,父类和子类都是独立自主、功能完整、可正常使用的模型,都有自己的数据库表,内部隐含了一个一对一的关系(通过自动创建的 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)
2
3
4
5
6
7
8
9
所有 Place 的字段都可以在 Restaurant 中使用,虽然数据存放在不同的数据表中。如下:
Place.objects.filter(name="Bob's Cafe")
Restaurant.objects.filter(name="Bob's Cafe")
2
如果一个 Place 对象存在相应的 Restaurant 对象,那么就可以使用 Place 对象通过关系获得 Restaurant 对象。如下:
p = Place.objects.get(id=12)
# 如果 p 也是一个 Restaurant 对象,那么下面的调用可以获得该 Restaurant 对象
p.restaurant
2
3
但是,如果上面示例中的 p 不是 Restaurant 对象(它已直接创建为 Place 对象或是其他类的父对象),那么上面的调用方式会弹出 Restaurant.DoesNotExist
异常。
在 Restaurant 上自动创建的 OneToOneField 将其链接到 Place 如下所示:
place_ptr = models.OneToOneField(
Place, on_delete=models.CASCADE,
parent_link=True,
)
2
3
4
可以通过创建一个 OneToOneField 字段并设置 parent_link=True
,自定义这个一对一字段。
从上面的API操作展示可以看出,这种继承方式还是有点混乱的,不如抽象基类来得直接明了。
# Meta 和多表继承
在多表继承的情况下,由于父类和子类都在数据库内有物理存在的表,父类的 Meta 类会对子类造成不确定的影响,因此,Django 在这种情况下关闭了子类继承父类的 Meta 功能。这一点和抽象基类的继承方式有所不同。
但是,还有两个 Meta 元数据属性特殊一点,那就是 ordering
和 get_latest_by
,这两个参数是会被继承的。因此,如果在多表继承中,如果不想让子类继承父类的上面两种参数,就必须在子类中显示地指出或重写。如下:
class ChildModel(ParentModel):
# ...
class Meta:
# 移除父类对子类的排序影响
ordering = []
2
3
4
5
6
# 多表继承和反向关联
因为多表继承使用了一个隐含的 OneToOneField 来链接子类与父类,所以可以从父类访问子类。但是这个 OnetoOneField 字段默认的 related_name
值与 ForeignKey 和 ManyToManyField 默认的反向名称相同。
也就是说,如果你要与父类或另一个子类做多对一或是多对多关系,就必须在每个多对一和多对多字段上强制指定 related_name
。否则 Django 就会在运行或验证(validation)时抛出异常。
例如上面的例子,我们再创建一个子类,其中包含一个到父 model 的 ManyToManyField 关系字段:
class Supplier(Place):
customers = models.ManyToManyField(Place)
2
这会产生下面的错误:
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'.
2
3
4
解决方法是:向 customers 字段中添加 related_name
参数:
customers = models.ManyToManyField(Place, related_name='provider')
# 代理模型
# 用法示例
使用多表继承时,父类的每个子类都会创建一张新数据表,通常情况下,这是我们想要的操作,因为子类需要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在 Python 层面的行为,比如更改默认的 manager 管理器,或者添加一个新方法。
这时就应该使用代理模式的继承:创建原始 model 的代理。你可以创建一个用于 create,delete 和 update 的代理 model,使用代理 model 的时候数据将会真实保存。这和使用原始 model 是一样的,所不同的是当你改变 model 操作时,不需要去更改原始的 model。
代理模型其实就是给原模型换了件衣服(API),实际操作的还是原来的模型和数据。
声明一个代理模型只需要将 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
2
3
4
5
6
7
8
9
10
11
12
13
MyPerson 类将操作和 Person 类同一张数据库表。并且任何新的 Person 实例都可以通过 MyPerson 类进行访问,反之亦然。
p = Person.objects.create(first_name="foobar")
MyPerson.objects.get(first_name="foobar") # -> <MyPerson: foobar>
2
下面的例子通过代理进行排序,但父类却不排序:
class OrderedPerson(Person):
class Meta:
# 现在,普通的Person查询是无序的,而 OrderedPerson 查询会按照 last_name 排序
ordering = ["last_name"]
proxy = True
2
3
4
5
# 一些约束
- 代理模型必须继承自一个非抽象的基类,并且不能同时继承多个非抽象基类;
- 代理模型可以同时继承任意多个抽象基类,前提是这些抽象基类没有定义任何模型字段。
- 代理模型可以同时继承多个别的代理模型,前提是这些代理模型继承同一个非抽象基类。
# 代理模型的管理器
如不指定,则继承父类的管理器。如果你自己定义了管理器,那它就会成为默认管理器,但是父类的管理器依然有效。如下例子:
from django.db import models
class NewManager(models.Manager):
# ...
pass
class MyPerson(Person):
objects = NewManager()
class Meta:
proxy = True
2
3
4
5
6
7
8
9
10
11
如果你想要向代理中添加新的管理器,而不是替换现有的默认管理器,你可以创建一个含有新的管理器的基类,并在继承时把他放在主基类的后面:
# Create an abstract class for the new manager.
from django.db import models
class NewManager(models.Manager):
# ...
pass
class ExtraManagers(models.Model):
secondary = NewManager()
class Meta:
abstract = True
class MyPerson(Person, ExtraManagers):
class Meta:
proxy = True
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 多重继承
多重继承和多表继承是两码事,两个概念。
Django 的模型体系支持多重继承,就像 Python 一样。如果多个父类都含有 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
2
3
4
5
6
7
8
9
10
或者使用一个共同的祖先来持有 AutoField 字段,并在直接的父类里通过一个 OneToOne 字段保持与祖先的关系,如下所示:
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
2
3
4
5
6
7
8
9
10
11
12
13
注意
在 Python 语言层面,子类可以拥有和父类相同的属性名,这样会造成覆盖现象。
但是对于 Django,如果继承的是一个非抽象基类,那么子类与父类之间不可以有相同的字段名。比如下面是不行的:
class A(models.Model):
name = models.CharField(max_length=30)
class B(A):
name = models.CharField(max_length=30)
2
3
4
5
上述代码如果执行 python manage.py makemigrations
会弹出下面的错误:
django.core.exceptions.FieldError: Local field 'name' in class 'B' clashes with field of the same name from base class 'A'.
但是,如果父类是个抽象基类就没有问题,如下:
class A(models.Model):
name = models.CharField(max_length=30)
class Meta:
abstract = True
class B(A):
name = models.CharField(max_length=30)
2
3
4
5
6
7
8
# 用包来组织模型
在我们使用 python manage.py startapp xxx
命令创建新的应用时,Django 会自动帮我们建立一个应用的基本文件组织结构,其中就包括一个 models.py
文件。通常,我们把当前应用的模型都编写在这个文件里,但是如果你的模型很多,那么将单独的 models.py
文件分割成一些独立的文件是个更好的做法。
首先,我们需要在应用中新建一个叫做 models
的包,再在包下创建一个 __init__.py
文件,这样才能确立包的身份。然后将 models.py
文件中的模型分割到一些 .py
文件中,比如 organic.py
和 synthetic.py
,然后删除 models.py
文件。最后在 __init__.py
文件中导入所有的模型。如下例所示:
# myapp/models/__init__.py
from .organic import Person
from .synthetic import Robot
2
3
4
要显式明确地导入每一个模型,而不要使用 from .models import *
的方式,这样不会混淆命名空间,让代码更可读,更容易被分析工具使用。
(完)