该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程

所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址。


很多时候,我们都不是从‘一穷二白’开始编写模型的,有时候可以从第三方库中继承,有时候可以从以前的代码中继承,甚至现写一个模型用于被其它模型继承。这样做的好处,我就不赘述了,每个学习Django的人都非常清楚。

类同于Python的类继承,Django也有完善的继承机制。

Django中所有的模型都必须继承django.db.models.Model模型,不管是直接继承也好,还是间接继承也罢。

你唯一需要决定的是,父模型是否是一个独立自主的,同样在数据库中创建数据表的模型,还是一个只用来保存子模型共有内容,并不实际创建数据表的抽象模型。

Django有三种继承的方式:

  • 抽象基类:被用来继承的模型被称为Abstract base classes,将子类共同的数据抽离出来,供子类继承重用,它不会创建实际的数据表;
  • 多表继承:Multi-table inheritance,每一个模型都有自己的数据库表;
  • 代理模型:如果你只想修改模型的Python层面的行为,并不想改动模型的字段,可以使用代理模型。

注意!同Python的继承一样,Django也是可以同时继承两个以上父类的!

一、 抽象基类:

只需要在模型的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)

Student模型将拥有name,age,home_group三个字段,并且CommonInfo模型不能当做一个正常的模型使用。

抽象基类的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'

这里有几点要特别说明:

  • 抽象基类中有的元数据,子模型没有的话,直接继承;
  • 抽象基类中有的元数据,子模型也有的话,直接覆盖;
  • 子模型可以额外添加元数据;
  • 抽象基类中的abstract=True这个元数据不会被继承。也就是说如果想让一个抽象基类的子模型,同样成为一个抽象基类,那你必须显式的在该子模型的Meta中同样声明一个abstract = True
  • 有一些元数据对抽象基类无效,比如db_table,首先是抽象基类本身不会创建数据表,其次它的所有子类也不会按照这个元数据来设置表名。

警惕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

对于另外一个应用中的rare/models.py:

from common.models import Base

class ChildB(Base):
pass

对于上面的继承关系:

  • common.ChildA.m2m字段的reverse name(反向关系名)应该是common_childa_relatedreverse query name(反向查询名)应该是common_childas
  • common.ChildB.m2m字段的反向关系名应该是common_childb_related;反向查询名应该是common_childbs
  • rare.ChildB.m2m字段的反向关系名应该是rare_childb_related;反向查询名应该是rare_childbs

当然,如果你不设置related_name或者related_query_name参数,这些问题就不存在了。


二、 多表继承

这种继承方式下,父类和子类都是独立自主、功能完整、可正常使用的模型,都有自己的数据库表,内部隐含了一个一对一的关系。例如:

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)

Restaurant将包含Place的所有字段,并且各有各的数据库表和字段,比如:

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

如果一个Place对象同时也是一个Restaurant对象,你可以使用小写的子类名,在父类中访问它,例如:

>>> p = Place.objects.get(id=12)
# 如果p也是一个Restaurant对象,那么下面的调用可以获得该Restaurant对象。
>>> p.restaurant
<Restaurant: ...>

但是,如果这个Place是个纯粹的Place对象,并不是一个Restaurant对象,那么上面的调用方式会弹出Restaurant.DoesNotExist异常。

让我们看一组更具体的展示,注意里面的注释内容。

>>> from app1.models import Place, Restaurant  # 导入两个模型到shell里
>>> p1 = Place.objects.create(name='coff',address='address1')
>>> p1 # p1是个纯Place对象
<Place: Place object>
>>> p1.restaurant # p1没有餐馆属性
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "C:\Python36\lib\site-packages\django\db\models\fields\related_descriptors.py", line 407, in __get__
self.related.get_accessor_name()
django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist: Place has no restaurant.
>>> r1 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False)
>>> r1 # r1在创建的时候,只赋予了2个字段的值
<Restaurant: Restaurant object>
>>> r1.place # 不能这么调用
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Restaurant' object has no attribute 'place'
>>> r2 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False, name='pizza', address='address2')
>>> r2 # r2在创建时,提供了包括Place的字段在内的4个字段
<Restaurant: Restaurant object>
>>> r2.place # 可以看出这么调用都是非法的,异想天开的
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Restaurant' object has no attribute 'place'
>>> p2 = Place.objects.get(name='pizza') # 通过name,我们获取到了一个Place对象
>>> p2.restaurant # 这个P2其实就是前面的r2
<Restaurant: Restaurant object>
>>> p2.restaurant.address
'address2'
>>> p2.restaurant.serves_hot_dogs
True
>>> lis = Place.objects.all()
>>> lis
<QuerySet [<Place: Place object>, <Place: Place object>, <Place: Place object>]>
>>> lis.values()
<QuerySet [{'id': 1, 'name': 'coff', 'address': 'address1'}, {'id': 2, 'name': '', 'address': ''}, {'id': 3, 'name': 'pizza', 'address': 'address2'}]>
>>> lis[2]
<Place: Place object>
>>> lis[2].serves_hot_dogs
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Place' object has no attribute 'serves_hot_dogs'
>>> lis2 = Restaurant.objects.all()
>>> lis2
<QuerySet [<Restaurant: Restaurant object>, <Restaurant: Restaurant object>]>
>>> lis2.values()
<QuerySet [{'id': 2, 'name': '', 'address': '', 'place_ptr_id': 2, 'serves_hot_dogs': True, 'serves_pizza': False}, {'id': 3, 'name': 'pizza', 'address
': 'address2', 'place_ptr_id': 3, 'serves_hot_dogs': True, 'serves_pizza': False}]>

其机制内部隐含的OneToOne字段,形同下面所示:

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

可以通过创建一个OneToOneField字段并设置 parent_link=True,自定义这个一对一字段。


Meta和多表继承

在多表继承的情况下,由于父类和子类都在数据库内有物理存在的表,父类的Meta类会对子类造成不确定的影响,因此,Django在这种情况下关闭了子类继承父类的Meta功能。这一点和抽象基类的继承方式有所不同。

但是,还有两个Meta元数据特殊一点,那就是orderingget_latest_by,这两个参数是会被继承的。因此,如果在多表继承中,你不想让你的子类继承父类的上面两种参数,就必须在子类中显示的指出或重写。如下:

class ChildModel(ParentModel):
# ... class Meta:
# 移除父类对子类的排序影响
ordering = []

多表继承和反向关联

因为多表继承使用了一个隐含的OneToOneField来链接子类与父类,所以象上例那样,你可以从父类访问子类。但是这个OnetoOneField字段默认的related_name值与ForeignKey和 ManyToManyField默认的反向名称相同。如果你与父类或另一个子类做多对一或是多对多关系,你就必须在每个多对一和多对多字段上强制指定related_name。如果你没这么做,Django就会在你运行或验证(validation)时抛出异常。

仍以上面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'.

解决方法是:向customers字段中添加related_name参数.

customers = models.ManyToManyField(Place, related_name='provider')。

三、 代理模型

使用多表继承时,父类的每个子类都会创建一张新数据表,通常情况下,这是我们想要的操作,因为子类需要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在Python层面的行为,比如更改默认的manager管理器,或者添加一个新方法。

代理模型就是为此而生的。你可以创建、删除、更新代理模型的实例,并且所有的数据都可以像使用原始模型(非代理类模型)一样被保存。不同之处在于你可以在代理模型中改变默认的排序方式和默认的manager管理器等等,而不会对原始模型产生影响。

声明一个代理模型只需要将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>

下面的例子通过代理进行排序,但父类却不排序:

class OrderedPerson(Person):
class Meta:
# 现在,普通的Person查询是无序的,而OrderedPerson查询会按照`last_name`排序。
ordering = ["last_name"]
proxy = True

一些约束:

  • 代理模型必须继承自一个非抽象的基类,并且不能同时继承多个非抽象基类;
  • 代理模型可以同时继承任意多个抽象基类,前提是这些抽象基类没有定义任何模型字段。
  • 代理模型可以同时继承多个别的代理模型,前提是这些代理模型继承同一个非抽象基类。(早期Django版本不支持这一条)

代理模型的管理器

如不指定,则继承父类的管理器。如果你自己定义了管理器,那它就会成为默认管理器,但是父类的管理器依然有效。如下例子:

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

四、 多重继承

注意,多重继承和多表继承是两码事,两个概念。

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

或者使用一个共同的祖先来持有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

警告

在Python语言层面,子类可以拥有和父类相同的属性名,这样会造成覆盖现象。但是对于Django,如果继承的是一个非抽象基类,那么子类与父类之间不可以有相同的字段名!

比如下面是不行的!

class A(models.Model):
name = models.CharField(max_length=30) class B(A):
name = models.CharField(max_length=30)

如果你执行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'.

但是!如果父类是个抽象基类就没有问题了(1.10版新增特性),如下:

class A(models.Model):
name = models.CharField(max_length=30) class Meta:
abstract = True class B(A):
name = models.CharField(max_length=30)

模型的继承 -- Django从入门到精通系列教程的更多相关文章

  1. 模型和字段 -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  2. 模型的元数据Meta -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  3. 第一章:模型层model layer -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. 题外话: Django的教程写到这里,就进入 ...

  4. Part 2:模型与后台管理admin站点--Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  5. 用包来组织模型 -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. 在我们使用python manage.py ...

  6. Part 7:自定义admin站点--Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  7. Part 4:表单和类视图--Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  8. 查询操作 -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

  9. Django中不返回QuerySets的API -- Django从入门到精通系列教程

    该系列教程系个人原创,并完整发布在个人官网刘江的博客和教程 所有转载本文者,需在顶部显著位置注明原作者及www.liujiangblog.com官网地址. Python及Django学习QQ群:453 ...

随机推荐

  1. Zedboard(一)开发环境Vivado

    Vivado是Xilinx(赛灵思)公司出品的开发软件平台,适用于Zedboard开发板. 下面介绍Vivado搭建的过程: 一.注册Xilinx账号.下载安装包 推荐到Xilinx(赛灵思)英文官网 ...

  2. 学习 node.js 搭建web服务器

    开始 学习使用 node.js 首先完成搭建一个 web服务器.myweb.js var http = require('http'); var url = require('url'); var h ...

  3. RabbitMQ 学习开发笔记

    基本概念 ConnectionFactory.Connection.Channel ConnectionFactory.Connection.Channel,这三个都是RabbitMQ对外提供的API ...

  4. Free Pascal初次体验(有亮点哦)

    感觉上Pascal语言写的非常有条理,和英语很像,应该是比较容易学,但是写起来真的是麻烦的要死,平时一行代码用C/C++可能就是几秒钟,用Pascal就要几分钟,Free Pascal感觉也不是很好用 ...

  5. 洛谷 2634&&BZOJ 2152: 聪聪可可【点分治学习+超详细注释】

    2152: 聪聪可可 Time Limit: 3 Sec  Memory Limit: 259 MBSubmit: 3435  Solved: 1776[Submit][Status][Discuss ...

  6. Vijos P1785 同学排序【模拟】

    同学排序 描述 现有m位同学,第1位同学为1号,第2位同学为2号,依次第m位同学为m号.要求双号的学生站出来,然后余下的重新组合,组合完后,再次让双号的学生站出来,重复n次,问这时有多少同学出来站着? ...

  7. noi 2016 游记

    先挖个坑..这回大概不会太监吧(大雾 day -2 下午起飞的飞机,晚上到了成都..把东西扔到旅馆后就组队外出觅食了... 街上人不多,逛了半天才发现一家卖本地小吃的小店. KPM:诶诶给我来碗酸辣粉 ...

  8. ubuntu配置服务器apache

    在配置apache之前我们需要先配置好ubuntu中的网络,如果不太懂的话可以看看这我的这篇文章:配置ubuntu网络,里面详细的介绍了怎么配置ubuntu的网络. 1.安装apache服务器 sud ...

  9. 程序员之殇 —— (The Beginning of the End)噩梦、崩坏

    Look at all those faces out there (当我环视周遭的一张张脸孔) We are so different(我们是如此的不同) But we have one thing ...

  10. Python 使用 virtualenvwrapper 安装虚拟环境

    装载于https://www.jianshu.com/p/9f47a9801329 Python 使用 virtualenvwrapper 安装虚拟环境 Tim_Lee 关注 2017.05.04 2 ...