一、 ORM
在 MVC 或者说 MTV 设计模式中,模型(M)代表对数据库的操作。那么如何操作数据库呢?
我们可以在 Python 代码中嵌入 SQL 语句。
但是问题又来了,Python 怎么连接数据库呢?可以使用类似 pymysql 这一类的第三方模块(针对不同的数据库,有不同的模块)。
如果你有很多的数据库操作,并且你的 Python 程序员不是专业的 DBA,写的 SQL 语句很烂,甚至经常写错,怎么办?
聪明的人想出了一个办法:用 Python 语法来写,然后使用一个中间工具将 Python代码翻译成原生的 SQL 语句。这个中间工具就是所谓的 ORM(对象关系映射)!
ORM(object—relationship--mapping)将一个 Python 的对象映射为数据库中的一张关系表。它将 SQL 封装起来,程序员不再需要关心数据库的具体操作,只需要专注于自己本身代码和业务逻辑的实现。
于是,整体的实现过程就是:Python 代码,通过 ORM 转换成 SQL 语句,再通过pymysql 去实际操作数据库。
最典型的 python ORM 就是 SQLAlchemy 了,如果你的 Web 框架自身不带 ORM 系统,那么你可以安装使用它。
Django 自带 ORM 系统,不需要额外安装别的 ORM。当然,也可以安装并使用其它的 ORM,比如 SQLAlchemy,但是不建议这么做,因为 Django 系统庞大,集成完善,模型层与视图层、模板层结合得比较紧密,使用自带的 ORM 更方便更可靠,并且Django 自带的 ORM 功能也非常强大,也不难学。
Django 的 ORM 系统体现在框架内就是模型层。想要理解模型层的概念,关键在于理解用 Python 代码的方式来定义数据库表的做法!一个 Python 的类,就是一个模型,代表数据库中的一张数据表!Django 奉行 Python 优先的原则,一切基于 Python代码的交
流,完全封装 SQL 内部细节。
Django 默认支持的数据库类型(查看预先定义好的 engine):
可以配置不在默认支持类型中的其他类型的数据库,比如 sqlserver,需要下载相对应的包来支持。
在 Django 中配置使用 MySQL 数据库:
1. 修改项目的 settings.py 文件,将 default 数据库改为 mysql,具体参数如下:
2. 修改项目的__init__.py 文件,添加如下代码:
import pymysql pymysql.install_as_MySQLdb()
二、模型和字段
一个模型(model)就是一个单独的、确定的数据的信息源,包含了数据的字段和操作方法。通常,每个模型映射为一张数据库中的表。
基本的原则如下:
1.每个模型在 Django 中的存在形式为一个 Python 类
2.每个模型都是 django.db.models.Model 的子类
3.模型的每个字段(属性)代表数据表的某一列
4.Django 将自动为你生成数据库访问 API
5.Django 会为表增加自动增长的主键列,每个模型只能有一个主键列。如果自定义主键列,则 Django 不会再生成默认的主键列
6.表名由 Django 自动生成,默认格式为“项目名称_小写模型类名”,也可以自己通过 Meta 来自定义
例如:
定义模型类 Person:
相当于下面的 SQL 语句:
创建了模型之后,在使用它之前,你需要先在 settings 文件中的INSTALLED_APPS 处,注册 models.py 文件所在的 myapp。当你每次对模型进行增、删、修改时,请务必执行命令 python manage.py migrate,让操作实际应用到数据库上。这里可以选择在执行 migrate 之前,先执行 python manage.py makemigrations 让修改动作保存到记录文件中。
2.1 模型字段 fields
字段是模型中最重要的内容之一,也是唯一必须的部分。字段在 Python 中表现为一个类属性,体现了数据表中的一个列。
字段命名约束:
Django 不允许下面两种字段名:
1.与 Python 关键字冲突。这会导致语法错误。
2.字段名中不能有两个以上下划线在一起,因为两个下划线是 Django 的查询语法。
由于你可以自定义表名、列名,上面的规则可能被绕开,但是请养成良好的习惯,一定不要那么起名。
2.2 常用字段类型
字段类型的作用:
1.决定数据库中对应列的数据类型(例如:INTEGER, VARCHAR, TEXT)
2. HTML 中对应的表单标签的类型,例如
3.在 admin 后台和自动生成的表单中最小的数据验证需求
Django 内置了许多字段类型,它们都位于 django.db.models 中,例如models.CharField。这些类型基本满足需求,如果还不够,你也可以自定义字段。
下表列出了所有 Django 内置的字段类型,但不包括关系字段类型(字段名采用驼峰命名法)
类型 | 说明 |
AutoField | 一个自动增加的整数类型字段。通常你不需要自己编写它,Django 会自动帮你添加字段:id =models.AutoField(primary_key=True),这是一个自增字段,从 1 开始计数。 如果你非要自己设置主键,那么请务必将字段设置为 primary_key=True。Django 在一个模型中只允许有一个自增字段,并且该字段必须为主键! |
BigAutoField | (1.10 新增)64 位整数类型自增字段,数字范围更大,从 1 到 9223372036854775807 |
BigIntegerField | 64 位整数字段(看清楚,非自增),类似 IntegerField ,-9223372036854775808 到9223372036854775807。在 Django 的模板表单里体现为一个 textinput 标签 |
BinaryField | 二进制数据类型。使用受限,少用。 |
BooleanField | 布尔值类型。默认值是 None。在 HTML 表单中体现为 CheckboxInput 标签。如果要接收 null 值,请使用 NullBooleanField。 |
CharField | 字符串类型。必须接收一个 max_length 参数,表示字符串长度不能超过该值。默认的表单标签是input text。最常用的 filed,没有之一! |
CommaSeparatedIntegerField | 逗号分隔的整数类型。必须接收一个 max_length 参数。常用于表示较大的金额数目,例如1,000,000 元。 |
DateField | class DateField(auto_now=False, auto_now_add=False, **options)日期类型。一个Python 中的 datetime.date 的实例。在 HTML 中表现为 TextInput 标签。 在 admin 后台中,Django 会帮你自动添加一个 JS 的日历表和一个“Today”快捷方式,以及附加的日期合法性验证。两个重要参数:(参数互斥,不能共存) auto_now: 每当对象被保存时将字段设为当前日期,常用于保存最后修改时间。auto_now_add:每当对象被创建时,设为当前日期,常用于保存创建日期(注意,它是不可修改的)。 例子:pub_time = models.DateField(auto_now_add=True),自动添加发布时间。 |
DateTimeField | 日期时间类型。Python 的 datetime.datetime 的实例。与 DateField 相比就是多了小时、分和秒的显示,其它功能、参数、用法、默认值等等都一样。 |
DecimalField | 固定精度的十进制小数。相当于 Python 的 Decimal 实例,必须提供两个指定的参数!参数max_digits:最大的位数,必须大于或等于小数点位数 。 decimal_places:小数点位数,精度。当 localize=False 时,它在 HTML 表现为 NumberInput 标签,否则是 text 类型。例子:储存最大不超过 999, 带有 2 位小数位精度的数, 定义如下:models.DecimalField(...,max_digits=5, decimal_places=2)。 |
DurationField | 持续时间类型。存储一定期间的时间长度。类似 Python 中的 timedelta。在不同的数据库实现中有不同的表示方法。常用于进行时间之间的加减运算。 |
EmailField | 邮箱类型,默认 max_length 最大长度 254 位。使用这个字段的好处是,可以使用 DJango 内置的EmailValidator 进行邮箱地址合法性验证。 |
FileField | class FileField(upload_to=None, max_length=100, **options)上传文件类型 |
FilePathField | 文件路径类型 |
FloatField | 浮点数类型,参考整数类型 |
ImageField | 图像类型 |
IntegerField | 整数类型,最常用的字段之一。取值范围-2147483648 到 2147483647。在 HTML 中表现为NumberInput 标签。 |
GenericIPAddressField | class GenericIPAddressField(protocol='both', unpack_ipv4=False,**options)[source],IPV4 或者 IPV6 地址,字符串形式, 例如 192.0.2.30 或者 2a02:42fe::4 在HTML 中表现为 TextInput 标签。参数 protocol 默认值为‘both’,可选‘IPv4’或者‘IPv6’,表示你的 IP 地址类型。 |
NullBooleanField | 类似布尔字段,只不过额外允许 NULL 作为选项之一。 |
PositiveIntegerField | 正整数字段,包含 0,最大 2147483647。 |
PositiveSmallIntegerField | 较小的正整数字段,从 0 到 32767。 |
SlugField | slug 是一个新闻行业的术语。一个 slug 就是一个某种东西的简短标签,包含字母、数字、下划线或者连接线,通常用于 URLs 中。可以设置 max_length 参数,默认为 50。 |
SmallIntegerField | 小整数,包含-32768 到 32767。 |
TextField | 大量文本内容,在 HTML 中表现为 Textarea 标签,最常用的字段类型之一!如果你为它设置一个max_length 参数,那么在前端页面中会受到输入字符数量限制, 然而在模型和数据库层面却不受影响。只有 CharField 才能同时作用于两者。 |
TimeField | 时间字段,Python 中 datetime.time 的实例。接收同 DateField 一样的参数,只作用于小时、分和秒。 |
URLField | 一个用于保存 URL 地址的字符串类型,默认最大长度 200。 |
UUIDField | 用于保存通用唯一识别码(Universally Unique Identifier)的字段。使用 Python 的 UUID 类。 |
其中,
1. FileField:
class FileField(upload_to=None, max_length=100, **options)[source]
上传文件字段(不能设置为主键)。默认情况下,该字段在 HTML 中表现为一个ClearableFileInput 标签。在数据库内,我们实际保存的是一个字符串类型,默认最大长度 100,可以通过 max_length 参数自定义。真实的文件是保存在服务器的文件系统内的。
重要参数 upload_to 用于设置上传地址的目录和文件名。如下例所示:
class MyModel(models.Model): # 文件被传至`MEDIA_ROOT/uploads`目录,MEDIA_ROOT 由你在 settings 文件中设置 upload = models.FileField(upload_to='uploads/')
2. ImageField
class ImageField(upload_to=None, height_field=None, width_field=None,max_length=100, **options)[source]
用于保存图像文件的字段。其基本用法和特性与 FileField 一样,只不过多了两个属性height 和 width。默认情况下,该字段在 HTML 中表现为一个 ClearableFileInput 标签。
在数据库内,我们实际保存的是一个字符串类型,默认最大长度 100,可以通过max_length 参数自定义。真实的图片是保存在服务器的文件系统内的。
height_field 参数:保存有图片高度信息的模型字段名。 width_field 参数:保存有图片宽度信息的模型字段名。
使用 Django 的 ImageField 需要提前安装 pillow 模块。
使用 FileField 或者 ImageField 字段的步骤:
1. 在 settings 文件中,配置 MEDIA_ROOT,作为你上传文件在服务器中的基本路径(为了性能考虑,这些文件不会被储存在数据库中)。再配置个 MEDIA_URL,作为公用 URL,指向上传文件的基本路径。
2. 添加 FileField 或者 ImageField 字段到你的模型中,定义好 upload_to 参数,文件最终会放在 MEDIA_ROOT 目录的“upload_to”子目录中。
3. 真正被保存在数据库中的,只是指向你上传文件路径的字符串而已。可以通过 url属性,在 Django 的模板中方便的访问这些文件。例如,假设你有一个 ImageField字段,名叫 mug_shot,那么在 Django 模板的 HTML 文件中,可以使用{{ object.mug_shot.url }}来获取该文件。其中的 object 用你具体的对象名称代替。
4. 可以通过 name 和 size 属性,获取文件的名称和大小信息。
安全建议
无论你如何保存上传的文件,一定要注意他们的内容和格式,避免安全漏洞!务必对所有的上传文件进行安全检查,确保它们不出问题!
2.3 关系类型字段
除了前面说过的普通类型字段,Django 还定义了一组关系类型字段,用来表示模型与模型之间的关系。
主要包括:
1.多对一:ForeignKey
2.多对多:ManyToManyField
3.一对一:OneToOneField
1)、多对一(ForeignKey)
多对一的关系,通常被称为外键。
外键要定义在‘多’的一方!
from django.db import models class Car(models.Model): manufacturer = models.ForeignKey( 'Manufacturer', ) class Manufacturer(models.Model): pass
上面的例子中,每辆车都会有一个生产工厂,一个工厂可以生产 N 辆车,于是用一个外键字段 manufacturer 表示,并放在 Car 模型中。注意,此 manufacturer 非彼Manufacturer 模型类,它是一个字段的名称。
如果要关联的对象在另外一个 app 中,可以显式的指出。下例假设 Manufacturer 模型存在于 production 这个 app 中,则 Car 模型的定义如下:
class Car(models.Model): manufacturer = models.ForeignKey( 'production.Manufacturer', # 关键在这里!! )
如果要创建一个递归的外键,也就是自己关联自己的的外键,使用下面的方法:
models.ForeignKey('self', on_delete=models.CASCADE)
核心在于‘self’这个引用。什么时候需要自己引用自己的外键呢?典型的例子就是评论系统!
在实际的数据库后台,Django 会为每一个外键添加_id 后缀,并以此创建数据表里的一列。在上面的工厂与车的例子中,Car 模型对应的数据表中,会有一列叫做manufacturer_id。但实际上,在 Django 代码中你不需要使用这个列名,一般我们都直接使用字段名 manufacturer。
参数说明:
外键还有一些重要的参数,说明如下:
on_delete
当一个被外键关联的对象被删除时,Django 将模仿 on_delete 参数定义的 SQL 约束执行相应操作。
该参数可选的值都内置在 django.db.models 中,包括:
1.CASCADE:模拟 SQL 语言中的 ON DELETE CASCADE 约束,将定义有外键的模型对象同时删除!(该操作为当前 Django 版本的默认操作!)
2.PROTECT:阻止上面的删除操作,但是弹出 ProtectedError 异常
3.SET_NULL:将外键字段设为 null,只有当字段设置了 null=True 时,方可使用该值。
4.SET_DEFAULT:将外键字段设为默认值。只有当字段设置了 default 参数时,方可使用。
5.DO_NOTHING:什么也不做。
6.SET():设置为一个传递给 SET()的值或者一个回调函数的返回值。注意大小写。
limit_choices_to
该参数用于限制外键所能关联的对象,只能用于 Django 的 ModelForm(Django 的表单模块)和 admin 后台,对其它场合无限制功能。其值可以是一个字典、Q 对象或者一个返回字典或 Q 对象的函数调用。
related_name
用于关联对象反向引用模型的名称。以英雄和书的例子解释,就是从书反向关联到英雄的关系名称。通常情况下,这个参数我们可以不设置,Django 会默认以模型的小写作为反向关联名。
related_query_name
反向关联查询名。用于从目标模型反向过滤模型对象的名称。
to_field
默认情况下,外键都是关联到被关联对象的主键上(一般为 id)。如果指定这个参数,可以关联到指定的字段上,但是该字段必须具有 unique=True 属性,也就是具有唯一属性。
db_constraint
默认情况下,这个参数被设为 True,表示遵循数据库约束,这也是大多数情况下你的选择。如果设为 False,那么将无法保证数据的完整性和合法性。
swappable
控制迁移框架的动作,如果当前外键指向一个可交换的模型。使用场景非常稀少,通常请将该参数保持默认的 True。
2)、多对多(ManyToManyField)
多对多关系在数据库中也是非常常见的关系类型。比如一本书中可以有好几个英雄,一个英雄也可以出现在几本书中。多对多的字段可以定义在任何的一方,请尽量定义在符合人们思维习惯的一方,但不要同时都定义。
在数据库后台,Django 实际上会额外创建一张用于体现多对多关系的中间表。默认情况下,该表的名称是“多对多字段名+关联对象模型名+一个独一无二的哈希码”
重要参数:
through
(定义中间表)
如果你想自定义多对多关系的那张额外的关联表,可以使用这个参数。参数的值为一个中间模型。
看下面的例子:
from django.db import models
class Person(models.Model): name = models.CharField(max_length=50)
class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField( Person, through='Membership', ## 自定义中间表 through_fields=('group', 'person'), )
class Membership(models.Model): # 这就是具体的中间表模型
group = models.ForeignKey(Group, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
inviter = models.ForeignKey(
Person,
on_delete=models.CASCADE,
related_name="membership_invites",
)
invite_reason = models.CharField(max_length=64)
上面的代码中,通过 class Membership(models.Model)定义了一个新的模型,用来保存 Person 和 Group 模型的多对多关系,并且同时增加了‘邀请人’和‘邀请原因’的字段。
through_fields
接着上面的例子。Membership 模型中包含两个关联 Person 的外键,Django 无法确定到底使用哪个作为和 Group 关联的对象。所以,在这个例子中,必须显式的指定through_fields 参数,用于定义关系。
through_fields 参数接收一个二元元组('field1', 'field2'),field1 是指向定义有多对多关系的模型的外键字段的名称,这里是 Membership 中的‘group’字段(注意大小写),另外一个则是指向目标模型的外键字段的名称,这里是 Membership 中的‘person’,而不是‘inviter’。
再通俗的说,就是 through_fields 参数指定从中间表模型 Membership 中选择哪两个字段,作为关系连接字段。
db_table
设置中间表的名称。不指定的话,则使用默认值。
db_constraint
参考外键的相同参数。
swappable
参考外键的相同参数。
ManyToManyField 多对多字段不支持 Django 内置的 validators 验证功能。
null 参数对 ManyToManyField 多对多字段无效!设置 null=True 毫无意义
3)、一对一(OneToOneField)
这种关系类型多数用于当一个模型需要从别的模型扩展而来的情况。比如,Django 自带 auth 模块的 User 用户表,如果你想在自己的项目里创建用户模型,又想方便的使用 Django 的认证功能,那么一个比较好的方案就是在你的用户模型里,使用一对一关系,添加一个与 auth 模块 User 模型的关联字段。
看下面的例子:
from django.conf import settings from django.db import models # 两个字段都使用一对一关联到了 Django 内置的 auth 模块中的 User 模型 class MySpecialUser(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) supervisor = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='supervisor_of', )
这样下来,你的 User 模型将拥有下面的属性:
>>> user = User.objects.get(pk=1)
>>> hasattr(user, 'myspecialuser')
True
>>> hasattr(user, 'supervisor_of')
True
跨模块的模型:
有时候,我们关联的模型并不在当前模型的文件内,没关系,就像我们导入第三方库一样的从别的模块内导入进来就好,如下例所示:
from django.db import models from geography.models import ZipCode class Restaurant(models.Model): # ... zip_code = models.ForeignKey( ZipCode, on_delete=models.SET_NULL, blank=True, null=True, )
2.4 字段的参数
所有的模型字段都可以接收一定数量的参数,比如 CharField 至少需要一个max_length 参数。下面的这些参数是所有字段都可以使用的,并且是可选的。
null
该值为 True 时,Django 在数据库用 NULL 保存空值。默认值为 False。
blank
True 时,字段可以为空。默认 False。和 null 参数不同的是,null 是纯数据库层面的,而 blank 是验证相关的,它与表单验证是否允许输入框内为空有关,与数据库无关。
choices
用于页面上的选择框标签,需要先提供一个二维的二元元组,第一个元素表示存在数据库内真实的值,第二个表示页面上显示的具体内容。在浏览器页面上将显示第二个元素的值。
db_column
该参数用于定义当前字段在数据表内的列名。如果未指定,Django 将使用字段名作为列名。
db_index
该参数接收布尔值。如果为 True,数据库将为该字段创建索引。
db_tablespace
用于字段索引的数据库表空间的名字,前提是当前字段设置了索引。默认值为工程的DEFAULT_INDEX_TABLESPACE 设置。如果使用的数据库不支持表空间,该参数会被忽略。
default
字段的默认值,可以是值或者一个可调用对象。如果是可调用对象,那么每次创建新对象时都会调用。设置的默认值不能是一个可变对象,比如列表、集合等等。lambda匿名函数也不可用于 default 的调用对象,因为匿名函数不能被 migrations 序列化。
editable
如果设为 False,那么当前字段将不会在 admin 后台或者其它的 ModelForm 表单中显示,同时还会被模型验证功能跳过。参数默认值为 True。
error_messages
用于自定义错误信息。参数接收字典类型的值。字典的键可以是null、 blank、 invalid、 invalid_choice、 unique 和 unique_for_date 其中的一个。
help_text
额外显示在表单部件上的帮助文本。使用时请注意转义为纯文本,防止脚本攻击。
primary_key
如果你没有给模型的任何字段设置这个参数为 True,Django 将自动创建一个AutoField 自增字段,名为‘id’,并设置为主键。也就是 id =models.AutoField(primary_key=True)。
如果你为某个字段设置了 primary_key=True,则当前字段变为主键,并关闭 Django自动生成 id 主键的功能。
primary_key=True 隐含 null=False 和 unique=True 的意思。一个模型中只能有一个主键字段!
unique_for_date
日期唯一。可能不太好理解。举个栗子,如果你有一个名叫 title 的字段,并设置了参数 unique_for_date="pub_date",那么 Django 将不允许有两个模型对象具备同样的title 和 pub_date。有点类似联合约束。
unique_for_month
同上,只是月份唯一。
unique_for_year
同上,只是年份唯一。
verbose_name
为字段设置一个人类可读,更加直观的别名。
对于每一个字段类型,除了 ForeignKey、ManyToManyField 和 OneToOneField 这三个特殊的关系类型,其第一可选位置参数都是 verbose_name。如果没指定这个参数,Django 会利用字段的属性名自动创建它,并将下划线转换为空格。
下面这个例子的 verbose name 是"person’s first name":
first_name = models.CharField("person's first name", max_length=30)
下面这个例子的 verbose name 是"first name":
first_name = models.CharField(max_length=30)
对于外键、多对多和一对一字字段,由于第一个参数需要用来指定关联的模型,因此必须用关键字参数 verbose_name 来明确指定。
validators
运行在该字段上的验证器的列表。
三、 模型的元数据 Meta
模型的元数据,指的是“除了字段外的所有内容”,例如排序方式、数据库表名、人类可读的单数或者复数名等等。所有的这些都是非必须的,甚至元数据本身对模型也是非必须的。
想在模型中增加元数据,方法很简单,在模型类中添加一个子类,名字是固定的 Meta,然后在这个 Meta 类下面增加各种元数据选项或者说设置项。参考下面的例子:
from django.db import models class Ox(models.Model): horn_length = models.IntegerField() class Meta: # 注意,是模型的子类,要缩进! ordering = ["horn_length"]
上面的例子中,我们为模型 Ox 增加了一个元数据‘ordering’,表示排序,下面我们会详细介绍有哪些可用的元数据选项。
强调:每个模型都可以有自己的元数据类,每个元数据类也只对自己所在模型起作用。
元数据选项:
abstract
如果 abstract=True,那么模型会被认为是一个抽象模型。抽象模型本身不实际生成数据库表,而是作为其它模型的父类,被继承使用。
app_label
如果定义了模型的 app 没有在 INSTALLED_APPS 中注册,则必须通过此元选项声明它属于哪个 app,例如:app_label = 'myapp'
base_manager_name
自定义模型的_base_manager 管理器的名字。模型管理器是 Django 为模型提供的 API所在。
db_table
指定在数据库中,当前模型生成的数据表的表名。比如:db_table = 'my_freinds'
友情建议:使用 MySQL 数据库时,db_table 用小写英文。
db_tablespace
自定义数据库表空间的名字。默认值是工程的 DEFAULT_TABLESPACE 设置。
default_manager_name
自定义模型的_default_manager 管理器的名字。
default_related_name
默认情况下,从一个模型反向关联设置有关系字段的源模型,我们使用
get_latest_by
Django 管理器给我们提供有 latest()和 earliest()方法,分别表示获取最近一个和最前一个数据对象。但是,如何来判断最近一个和最前面一个呢?也就是根据什么来排序呢?
get_latest_by 元数据选项帮你解决这个问题,它可以指定一个类似 DateField、DateTimeField 或者 IntegerField 这种可以排序的字段,作为 latest()和 earliest()方法的排序依据,从而得出最近一个或最前面一个对象。例如:get_latest_by = "order_date"
managed
该元数据默认值为 True,表示 Django 将按照既定的规则,管理数据库表的生命周期。如果设置为 False,将不会针对当前模型创建和删除数据库表。在某些场景下,这可能有用,但更多时候,你可以忘记该选项。
order_with_respect_to
其用途是根据指定的字段进行排序,通常用于关系字段。
ordering
用于指定该模型生成的所有对象的排序方式,接收一个字段名组成的元组或列表。默认按升序排列,如果在字段名前加上字符“-”则表示按降序排列,如果使用字符问号“?”表示随机排列。请看下面的例子:
ordering = ['pub_date'] # 表示按'pub_date'字段进行升序排列
ordering = ['-pub_date'] # 表示按'pub_date'字段进行降序排列
ordering = ['-pub_date', 'author'] # 表示先按'pub_date'字段进行降序排列,
再按`author`字段进行升序排列。
permissions
该元数据用于当创建对象时增加额外的权限。它接收一个所有元素都是二元元组的列表或元组,每个元素都是(权限代码, 直观的权限名称)的格式。比如下面的例子:permissions = (("can_deliver_pizzas", "可以送披萨"),)
default_permissions
Django 默认给所有的模型设置('add', 'change', 'delete')的权限,也就是增删改。你可以自定义这个选项,比如设置为一个空列表,表示你不需要默认的权限,但是这一操作必须在执行 migrate 命令之前。
proxy
如果设置了 proxy = True,表示使用代理模式的模型继承方式。
required_db_features
声明模型依赖的数据库功能。
required_db_vendor
声明模型支持的数据库。Django 默认支持 sqlite, postgresql, mysql, oracle。
indexes
Django1.11 新增的选项。
接收一个应用在当前模型上的索引列表。
unique_together
举个例子,假设有一张用户表,保存有用户的姓名、出生日期、性别和籍贯等等信息。
要求是所有的用户唯一不重复,可现在有好几个叫“张伟”的,如何区别它们呢?(不要和我说主键唯一,这里讨论的不是这个问题)
我们可以设置不能有两个用户在同一个地方同一时刻出生并且都叫“张伟”,使用这种联合约束,保证数据库能不能重复添加用户。在 Django 的模型中,如何实现这种约束呢?
使用 unique_together,也就是联合唯一!比如:unique_together = (('name', 'birth_day', 'address'),)
这样,哪怕有两个在同一天出生的张伟,但他们的籍贯不同,也就是两个不同的用户。
一旦三者都相同,则会被 Django 拒绝创建。这一元数据经常被用在 admin 后台,并且强制应用于数据库层面。
unique_together 接收一个二维的元组((xx,xx,xx,...),(),(),()...),每一个元素都是一个元组,表示一组联合唯一约束,可以同时设置多组约束。为了方便,对于只有一组约束的情况下,可以简单地使用一维元素,例如:
unique_together = ('name', 'birth_day', 'address')
联合唯一无法作用于普通的多对多字段。
verbose_name
用于设置模型对象的直观、人类可读的名称。可以用中文。例如:
verbose_name = "story"
verbose_name = "披萨"
如果你不指定它,那么 Django 会使用小写的模型名作为默认值。
verbose_name_plural
英语有单数和复数形式。这个就是模型对象的复数名,比如“apples”。因为我们中
文通常不区分单复数,所以保持和 verbose_name 一致也可以。
verbose_name_plural = "stories"
verbose_name_plural = "披萨"
如果不指定该选项,那么默认的复数名字是 verbose_name 加上‘s’
label
前面介绍的元数据都是可修改和设置的,但还有两个只读的元数据,label 就是其中之一。
label 等同于 app_label.object_name。例如 booktest.BookInfo,polls 是应用名,Question 是模型名。
label_lower
同上,不过是小写的模型名。
四、 模型对象查询操作
查询操作是 Django 的 ORM 框架中最重要的内容之一。我们建立模型、保存数据为的就是在需要的时候可以查询得到数据。Django 自动为所有的模型提供了一套完善、方便、高效的 API,一些重要的,我们要背下来,一些不常用的,要有印象,使用的时候可以快速查找参考手册。
4.1、创建对象
假设模型位于 mysite/booktest/models.py 文件中,那么创建对象的方式如下:
>>> from booktest.models import BookInfo
>>> b = BookInfo(btitle='笑傲江湖',
bpub_date=datetime(year=1968,month=1,day=1))
>>> b.save()
在后台,这会运行一条 SQL 的 INSERT 语句。如果你不显式地调用 save()方法,Django 不会立刻将该操作反映到数据库中。save()方法没有返回值,它可以接受一些额外的参数。
如果想要一行代码完成上面的操作,请使用 creat()方法,它可以省略 save 的步骤:
b = Blog.objects.create(btitle='笑傲江湖',
bpub_date=datetime(year=1968,month=1,day=1))
4.2、保存对象
使用 save()方法,保存对数据库内已有对象的修改。例如如果已经存在 b5 对象在数据库内:
>>> b5.name = 'New name'
>>> b5.save()
在后台,这会运行一条 SQL 的 UPDATE 语句。如果你不显式地调用 save()方法,Django 不会立刻将该操作反映到数据库中。
4.3、检索对象
想要从数据库内检索对象,你需要基于模型类,通过管理器(Manager)构造一个查询结果集(QuerySet)。
每个 QuerySet 代表一些数据库对象的集合。它可以包含零个、一个或多个过滤器(filters)。Filters 缩小查询结果的范围。在 SQL 语法中,一个 QuerySet 相当于一个SELECT 语句,而 filter 则相当于 WHERE 或者 LIMIT 一类的子句。
通过模型的 Manager 获得 QuerySet,每个模型至少具有一个 Manager,默认情况下,它被称作 objects,可以通过模型类直接调用它,但不能通过模型类的实例调用它。
4.3.1. 检索所有对象
使用 all()方法,可以获取某张表的所有记录。
>>> all_books = BookInfo.objects.all()
4.3.2. 过滤对象
有两个方法可以用来过滤 QuerySet 的结果,分别是:
1.filter(**kwargs):返回一个根据指定参数查询出来的 QuerySet
2.exclude(**kwargs):返回除了根据指定参数查询出来结果的 QuerySet
其中,**kwargs 参数的格式必须是 Django 设置的一些字段格式。例如:
BookInfo.objects.filter(pub_date__year=2006)
链式过滤
filter 和 exclude 的结果依然是个 QuerySet,因此它可以继续被 filter 和 exclude,这就形成了链式过滤:
>>> BookInfo.objects.filter(
... btitle__startswith='笑'
... ).exclude(
... bpub_date__gte=datetime.date.today()
... ).filter(
... bpub_date__gte=datetime(2005, 1, 30)
... )
被过滤的 QuerySets 都是唯一的
每一次过滤,你都会获得一个全新的 QuerySet,它和之前的 QuerySet 没有任何关系,可以完全独立的被保存,使用和重用。例如:
>>> q1 = BookInfo.objects.filter(btitle__startswith="笑")
>>> q2 = q1.exclude(bpub_date__gte=datetime.date.today())
>>> q3 = q1.filter(bpub_date__gte=datetime.date.today())
例子中的 q2 和 q3 虽然由 q1 得来,是 q1 的子集,但是都是独立自主存在的。同样 q1也不会受到 q2 和 q3 的影响。
QuerySets 都是懒惰的
一个创建 QuerySets 的动作不会立刻导致任何的数据库行为。你可以不断地进行 filter动作一整天,Django 不会运行任何实际的数据库查询动作,直到 QuerySets 被提交(evaluated)。
简而言之就是,只有碰到某些特定的操作,Django 才会将所有的操作体现到数据库内,否则它们只是保存在内存和 Django 的层面中。这是一种提高数据库查询效率,减少操作次数的优化设计。看下面的例子:
>>> q = BookInfo.objects.filter(btitle__startswith="笑")
>>> q = q.filter(bpub_date__lte=datetime.date.today())
>>> print(q)
上面的例子,看起来执行了 2 次数据库访问,实际上只是在 print 语句时才执行 1 次访问。通常情况,QuerySets 的检索不会立刻执行实际的数据库查询操作,直到出现类似print 的请求,也就是所谓的 evaluated。
返回新 QuerySets 的 API
方法名 | 解释 |
filter() | 过滤查询对象。 |
exclude() | 排除满足条件的对象 |
annotate() | 使用聚合函数 |
order_by() | 对查询集进行排序 |
reverse() | 反向排序 |
distinct() | 对查询集去重 |
values() | 返回包含对象具体值的字典的 QuerySet |
values_list() | 与 values()类似,只是返回的是元组而不是字典。 |
dates() | 根据日期获取查询集 |
datetimes() | 根据时间获取查询集 |
none() | 创建空的查询集 |
all() | 获取所有的对象 |
union() | 并集 |
intersection() | 交集 |
difference() | 差集 |
select_related() | 附带查询关联对象 |
prefetch_related() | 预先查询 |
extra() | 附加 SQL 查询 |
defer() | 不加载指定字段 |
only() | 只加载指定的字段 |
using() | 选择数据库 |
select_for_update() | 锁住选择的对象,直到事务结束。 |
raw() | 接收一个原始的 SQL 查询 |
可以使用下列方法对 QuerySet 提交查询操作:
1.迭代:QuerySet 是可迭代的,在首次迭代查询集时执行实际的数据库查询。
2.切片:如果使用切片的”step“参数,Django 将执行数据库查询并返回一个列表。
3. len():当你对 QuerySet 调用 len()时, 将提交数据库操作。
4.list():对 QuerySet 调用 list()将强制提交操作 book_list =
list(BookInfo.objects.all())
5.bool()
4.3.3. 检索单一对象
filter 方法始终返回的是 QuerySets,那怕只有一个对象符合过滤条件,返回的也是包含一个对象的 QuerySets,这是一个集合类型对象,你可以简单的理解为 Python 列表,可迭代可循环可索引。
如果你确定你的检索只会获得一个对象,那么你可以使用 Manager 的 get()方法来直接返回这个对象。
>>> one_book = BookInfo.objects.get(pk=1)
在 get 方法中你可以使用任何 filter 方法中的查询参数,用法也是一模一样。
注意:如果在查询时没有匹配到对象,那么 get()方法将抛出 DoesNotExist 异常。
这个异常是模型类的一个属性,在上面的例子中,如果不存在主键为 1 的 Entry 对象,那么 Django 将抛出 Entry.DoesNotExist 异常。
类似地,在使用 get()方法查询时,如果结果超过 1 个,则会抛出MultipleObjectsReturned 异常,这个异常也是模型类的一个属性。
所以:get()方法要慎用!
不返回 QuerySets 的 API
方法名 | 解释 |
get() | 获取单个对象 |
create() | 创建对象,无需 save() |
get_or_create() | 查询对象,如果没有找到就新建对象 |
update_or_create() | 更新对象,如果没有找到就创建对象 |
bulk_create() | 批量创建对象 |
count() | 统计对象的个数 |
in_bulk() | 根据主键值的列表,批量返回对象 |
iterator() | 获取包含对象的迭代器 |
latest() | 获取最近的对象 |
earliest() | 获取最早的对象 |
first() | 获取第一个对象 |
last() | 获取最后一个对象 |
aggregate() | 聚合操作 |
exists() | 判断 queryset 中是否有对象 |
update() | 批量更新对象 |
delete() | 批量删除对象 |
as_manager() | 获取管理器 |
聚合操作
使用 aggregate()函数返回聚合函数的值。
聚合函数包括:Avg,Count,Max,Min,Sum
用法:
from django.db.models import Max
maxDate = list.aggregate(Max(‘bpub_date’))
4.3.4. QuerySet 中使用限制
使用类似 Python 对列表进行切片的方法可以对 QuerySet 进行范围取值。它相当于SQL 语句中的 LIMIT 和 OFFSET 子句。参考下面的例子:
>>> BookInfo.objects.all()[:5] # 返回前 5 个对象
>>> BookInfo.objects.all()[5:10] # 返回第 6 个到第 10 个对象
注意:不支持负索引!例如 Entry.objects.all()[-1]是不允许的
通常情况,切片操作会返回一个新的 QuerySet,并且不会被立刻执行。但是有一个例外,那就是指定步长的时候,查询操作会立刻在数据库内执行,如下:
>>> BookInfo.objects.all()[:10:2]
4. 3.5. 字段查询
字段查询其实就是 filter()、exclude()和 get()等方法的关键字参数。 其基本格式是:
field__lookuptype=value,注意其中是双下划线。 例如:
>>> BookInfo.objects.filter(bpub_date__lte='2006-01-01')
# 相当于:
SELECT * FROM booktest_bookinfo WHERE bpub_date <= '2006-01-01';
其中的字段必须是模型中定义的字段之一。但是有一个例外,那就是 ForeignKey 字段,你可以为其添加一个“_id”后缀(单下划线)。例如:
>>> HeroInfo.objects.filter(book_id=4)
如果你传递了一个非法的键值,查询函数会抛出 TypeError 异常。
Django 的数据库 API 支持 20 多种查询类型,下面介绍一些常用的:
exact:
默认类型。如果你不提供查询类型,或者关键字参数不包含一个双下划线,那么查询类型就是这个默认的 exact。
>>> Entry.objects.get(headline__exact="Cat bites dog")
# 相当于
# SELECT ... WHERE headline = 'Cat bites dog';
iexact:
不区分大小写。
>>> Blog.objects.get(name__iexact="beatles blog")
# 匹配"Beatles Blog", "beatles blog",甚至"BeAtlES blOG".
contains:
表示包含的意思!大小写敏感!
Entry.objects.get(headline__contains='Lennon')
# 相当于
# SELECT ... WHERE headline LIKE '%Lennon%';
# 匹配'Today Lennon honored',但不匹配'today lennon honored'
icontains:
contains 的大小写不敏感模式。
startswith 和 endswith
以什么开头和以什么结尾。大小写敏感!
istartswith 和 iendswith
是不区分大小写的模式。
下表列出了所有的字段查询参数:
字段名 | 说明 |
exact | 精确匹配 |
iexact | 不区分大小写的精确匹配 |
contains | 包含匹配 |
icontains | 不区分大小写的包含匹配 |
in | 在..之内的匹配 |
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
startswith | 从开头匹配 |
istartswith | 不区分大小写从开头匹配 |
endswith | 从结尾处匹配 |
iendswith | 不区分大小写从结尾处匹配 |
range | 范围匹配 |
date | 日期匹配 |
year | 年份 |
month | 月份 |
day | 日期 |
week | 第几周 |
week_day | 周几 |
time | 时间 |
hour | 小时 |
minute | 分钟 |
second | 秒 |
isnull | 判断是否为空 |
search | 1.10 中被废弃 |
regex | 区分大小写的正则匹配 |
iregex | 不区分大小写的正则匹配 |
4.3.6. 跨越关系查询
Django 提供了强大并且直观的方式解决跨越关联的查询,它在后台自动执行包含JOIN 的 SQL 语句。要跨越某个关联,只需使用关联的模型字段名称,并使用双下划线分隔,直至你想要的字段(可以链式跨越,无限跨度)。例如:
# 返回所有 BookInfo 的 btitle 为'笑傲江湖'的 HeroInfo 对象
# 一定要注意,返回的是 HeroInfo 对象,而不是 BookInfo 对象。
# objects 前面用的是哪个 class,返回的就是哪个 class 的对象。
>>> HeroInfo.objects.filter(book__btitle)='笑傲江湖’)
反之亦然,如果要引用一个反向关联,只需要使用模型的小写名!
# 获取所有的 BookInfo 对象,前提是它所关联的 HeroInfo 的 hcomment 包含'八'
>>>BookInfo.objects.filter(heroinfo__hcomment__contains='八')
4.3.7. 使用 F 表达式引用模型的字段
到目前为止的例子中,我们都是将模型字段与常量进行比较。但是,如果你想将模型的一个字段与同一个模型的另外一个字段进行比较该怎么办?
使用 Django 提供的 F 表达式!
例如,为了查找 comments 数目多于 read 数目的 BookInfo,可以构造一个 F()对象来引用 read 数目,并在查询中使用该 F()对象:
>>> from django.db.models import F
>>> BookInfo.objects.filter(ncomments__gt=F('nread'))
Django 支持对 F()对象进行加、减、乘、除、取模以及幂运算等算术操作。两个操作数可以是常数和其它 F()对象。例如查找 comment 数目比 read 两倍还要多的
BookInfo,我们可以这么写:
>>> Entry.objects.filter(ncomments__gt=F('nread') * 2)
主键的快捷查询方式:pk
pk 就是 primary key 的缩写。通常情况下,一个模型的主键为“id”,所以下面三个语句的效果一样:
>>> BookInfo.objects.get(id__exact=14)
>>> BookInfo.objects.get(id=14)
>>> BookInfo.objects.get(pk=14)
可以联合其他类型的参数:
>>> BookInfo.objects.filter(pk__in=[1,4,7])
>>> BookInfo.objects.filter(pk__gt=14)
4.3.8. 在 LIKE 语句中转义百分符号和下划线
在原生 SQL 语句中%符号有特殊的作用。Django 帮你自动转义了百分符号和下划线,你可以和普通字符一样使用它们,如下所示:
>>> BookInfo.objects.filter(btitle__contains='%')
# 它和下面的一样
# SELECT ... WHERE btitle LIKE '%\%%';
4.3.9. 缓存与查询集
每个 QuerySet 都包含一个缓存,用于减少对数据库的实际操作。
对于新创建的 QuerySet,它的缓存是空的。当 QuerySet 第一次被提交后,数据库执行实际的查询操作,Django 会把查询的结果保存在 QuerySet 的缓存内,随后的对于该 QuerySet 的提交将重用这个缓存的数据。
要想高效的利用查询结果,降低数据库负载,你必须善于利用缓存。看下面的例子,这会造成 2 次实际的数据库操作,加倍数据库的负载,同时由于时间差的问题,可能在两次操作之间数据被删除或修改或添加,导致脏数据的问题:
>>> print([b.btitle for b in BookInfo.objects.all()])
>>> print([b.bpub_date for b in BookInfo.objects.all()])
为了避免上面的问题,好的使用方式如下,这只产生一次实际的查询操作,并且保持了数据的一致性:
>>> queryset = BookInfo.objects.all()
>>> print([b.btitle for b in queryset]) # 提交查询
>>> print([b.bpub_date for b in queryset]) # 重用查询缓存
何时不会被缓存
有一些操作不会缓存 QuerySet,例如切片和索引。这就导致这些操作没有缓存可用,每次都会执行实际的数据库查询操作。例如:
>>> queryset = BookInfo.objects.all()
>>> print(queryset[5]) # 查询数据库
>>> print(queryset[5]) # 再次查询数据库
但是,如果已经遍历过整个 QuerySet,那么就相当于缓存过,后续的操作则会使用缓存,例如:
>>> queryset = BookInfo.objects.all()
>>> [entry for entry in queryset] # 查询数据库
>>> print(queryset[5]) # 使用缓存
>>> print(queryset[5]) # 使用缓存
4.3.10 使用 Q 对象进行复杂查询
普通 filter 函数里的条件都是“and”逻辑,如果你想实现“or”逻辑怎么办?用 Q 查询!
Q 来自 django.db.models.Q,用于封装关键字参数的集合,可以作为关键字参数用于filter、exclude 和 get 等函数。 例如:
from django.db.models import Q
Q(btitle__startswith='笑')
可以使用“&”或者“|”或“~”来组合 Q 对象,分别表示与或非逻辑。它将返回一个新的 Q 对象。
Q(btitle__startswith='笑')|Q(btitle__startswith='神')
# 这相当于:
WHERE btitle LIKE '笑%' OR btitle LIKE '神%'
你也可以这么使用,默认情况下,以逗号分隔的都表示 AND 关系:
BookInfo.objects.get( Q(btitle__startswith='笑'), Q(bpub_date=date(2005, 5, 2)) | Q(bpub_date=date(2005, 5, 6)) )
当关键字参数和 Q 对象组合使用时,Q 对象必须放在前面,如下例子:
BookInfo.objects.get( Q(bpub_date=date(2005, 5, 2)) | Q(bpub_date=date(2005, 5,6)),btitle__startswith='a',)
如果关键字参数放在 Q 对象的前面,则会报错。
4.4 比较对象
要比较两个模型实例,只需要使用 python 提供的双等号比较符就可以了。在后台,其实比较的是两个实例的主键的值。下面两种方法是等同的:
>>> some_entry == other_entry
>>> some_entry.id == other_entry.id
4.5、删除对象
删除对象使用的是对象的 delete()方法。该方法将返回被删除对象的总数量和一个字典,字典包含了每种被删除对象的类型和该类型的数量。如下所示:
>>> e.delete()
(1, {'weblog.Entry': 1})
也可以批量删除。每个 QuerySet 都有一个 delete()方法,它能删除该 QuerySet 的所有成员。例如:
>>> BookInfo.objects.filter(bpub_date__year=2005).delete()
(5, {booktest.BookInfo': 5})
当 Django 删除一个对象时,它默认使用 SQL 的 ON DELETE CASCADE 约束,也就是说,任何有外键指向要删除对象的对象将一起被删除。例如:
b = BookInfo.objects.get(pk=1)
# 下面的动作将删除该条 BookInfo 和所有的它关联的 HeroInfo 对象
b.delete()
这种级联的行为可以通过的 ForeignKey 的 on_delete 参数自定义。
注意,delete()是唯一没有在管理器上暴露出来的方法。这是刻意设计的一个安全机制,用来防止你意外地请求类似 Entry.objects.delete()的动作,而不慎删除了所有的条目。如果你确实想删除所有的对象,你必须明确地请求一个完全的查询集,像下面这样:
BookInfo.objects.all().delete()
4.6、批量更新对象
使用 update()方法可以批量为 QuerySet 中所有的对象进行更新操作。
BookInfo.objects.filter(bpub_date__year=1959).update(bcomments=100)
只可以对普通字段和 ForeignKey 字段使用这个方法。若要更新一个普通字段,只需提供一个新的常数值。若要更新 ForeignKey 字段,需设置新值为你想指向的新模型实例。
例如:
>>> b = BookInfo.objects.get(pk=1)
# 修改所有的 HeroInfo,让他们都属于 b
>>> HeroInfo.objects.all().update(book=b)
update 方法会被立刻执行,并返回操作匹配到的行的数目(有可能不等于要更新的行的数量,因为有些行可能已经有这个新值了)。唯一的约束是:只能访问一张数据库表。
update 方法可以配合 F 表达式。这对于批量更新同一模型中某个字段特别有用。
>>> BookInfo.objects.all().update(ncomments=F('ncomments') + 1)
4.7、关系的对象
利用本节一开始的模型,一个 HeroInfo 对象 h 可以通过 book 属性 h.book 获取关联的BookInfo 对象。反过来,BookInfo 对象 b 可以通过 heroinfo_set 属性b.heroinfo_set.all()访问与它关联的所有 HeroInfo 对象。
4.7.1. 一对多(外键)
正向查询:
直接通过圆点加属性,访问外键对象:
>>> h = HeroInfo.objects.get(id=2)
>>> h.book # 返回关联的 BookInfo 对象
要注意的是,对外键的修改,必须调用 save 方法进行保存,例如:
>>> h = HeroInfo.objects.get(id=2)
>>> h.book = some_book
>>> h.save()
在第一次对一个外键关系进行正向访问的时候,关系对象会被缓存。随后对同样外键关系对象的访问会使用这个缓存,例如:
>>> h = HeroInfo.objects.get(id=2)
>>> print(h.book) # 访问数据库,获取实际数据
>>> print(h.book) # 不会访问数据库,直接使用缓存的版本
反向查询:
如果一个模型有 ForeignKey,那么该 ForeignKey 所指向的外键模型的实例可以通过一个管理器进行反向查询,返回源模型的所有实例。默认情况下,这个管理器的名字为FOO_set,其中 FOO 是源模型的小写名称。该管理器返回的查询集可以用前面提到的方式进行过滤和操作。
>>> b = BookInfo.objects.get(id=1)
>>> b.heroinfo_set.all()
4.7.2. 多对多
多对多关系的两端都会自动获得访问另一端的 API。这些 API 的工作方式与前面提到的“反向”一对多关系的用法一样。
唯一的区别在于属性的名称:定义 ManyToManyField 的模型使用该字段的属性名称,而“反向”模型使用源模型的小写名称加上'_set' (和一对多关系一样)。
4.7.3. 一对一
一对一非常类似多对一关系,可以简单的通过模型的属性访问关联的模型。
class HeroDetail(models.Model): heroinfo = models.OneToOneField(HeroInfo,on_delete=models.CASCADE) details = models.TextField() hd = HeroInfoDetail.objects.get(id=2)
hd.heroinfo
4.7.4. 通过关联对象进行查询
涉及关联对象的查询与正常值的字段查询遵循同样的规则。当你指定查询需要匹配的值时,你可以使用一个对象实例或者对象的主键值。
例如,如果你有一个 id=5 的 BookInfo 对象 b,下面的三个查询将是完全一样的:
HeroInfo.objects.filter(book=b) # 使用对象实例
HeroInfo.objects.filter(book =b.id) # 使用实例的 id
HeroInfo.objects.filter(book =5) # 直接使用 id
五、 模型的继承
很多时候,我们都不是从‘一穷二白’开始编写模型的,有时候可以从第三方库中继承,有时候可以从以前的代码中继承,甚至现写一个模型用于被其它模型继承。
类同于 Python 的类继承,Django 也有完善的继承机制。
首先,Django 中所有的模型都必须继承 django.db.models.Model 模型,不管是直接继承也好,还是间接继承也罢。
其次,模型还可以继承自其他自定义的父模型。你唯一需要决定的是,父模型是否是一个独立自主的,同样在数据库中创建数据表的模型,还是一个只用来保存子模型共有内容,并不实际创建数据表的抽象模型。
Django 有三种继承的方式:
1.抽象基类:被用来继承的模型被称为 Abstract base classes,将子类共同的数据抽离出来,供子类继承重用,它不会创建实际的数据表;
2.多表继承:Multi-table inheritance,每一个模型都有自己的数据库表;
3.代理模型:如果你只想修改模型的 Python 层面的行为,并不想改动模型的字段,可以使用代理模型。
同 Python 的继承一样,Django 也是可以同时继承两个以上父类的。
5.1、 抽象基类:
只需要在模型的 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 类。
这里有几点要特别说明:
1.抽象基类中有的元数据,子模型没有的话,直接继承;
2.抽象基类中有的元数据,子模型也有的话,直接覆盖;
3.子模型可以额外添加元数据;
4.抽象基类中的 abstract=True 这个元数据不会被继承。也就是说如果想让一个抽象基类的子模型,同样成为一个抽象基类,那你必须显式的在该子模型的 Meta 中同样声明一个 abstract = True;
5.有一些元数据对抽象基类无效,比如 db_table,首先是抽象基类本身不会创建数据表,其次它的所有子类也不会按照这个元数据来设置表名。
5.2、 多表继承
这种继承方式下,父类和子类都是独立自主、功能完整、可正常使用的模型,都有自己的数据库表,内部隐含了一个一对一的关系。例如:
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
但是,如果这个 Place 是个纯粹的 Place 对象,并不是一个 Restaurant 对象,那么上面的调用方式会弹出 Restaurant.DoesNotExist 异常。
Meta 和多表继承
在多表继承的情况下,由于父类和子类都在数据库内有物理存在的表,父类的 Meta 类会对子类造成不确定的影响,因此,Django 在这种情况下关闭了子类继承父类的Meta 功能。这一点和抽象基类的继承方式有所不同。但是,还有两个 Meta 元数据特殊一点,那就是 ordering 和 get_latest_by,这两个参数是会被继承的。因此,如果在多表继承中,你不想让你的子类继承父类的上面两种参数,就必须在子类中显示的指出或重写。如下:
class ChildModel(ParentModel): # ... class Meta: # 移除父类对子类的排序影响 ordering = []
5.3、 代理模型
使用多表继承时,父类的每个子类都会创建一张新数据表,通常情况下,这是我们想要的操作,因为子类需要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在 Python 层面的行为,比如添加一个新方法。
代理模型就是为此而生的。你可以创建、删除、更新代理模型的实例,并且所有的数据都可以像使用原始模型(非代理类模型)一样被保存。不同之处在于你可以在代理模型中改变默认的排序方式,而不会对原始模型产生影响。
声明一个代理模型只需要将 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")
一些约束:
1.代理模型必须继承自一个非抽象的基类,并且不能同时继承多个非抽象基类;
2.代理模型可以同时继承任意多个抽象基类,前提是这些抽象基类没有定义任何模型字段。
3.代理模型可以同时继承多个别的代理模型,前提是这些代理模型继承同一个非抽象基类。(早期 Django 版本不支持这一条)
5.4、 多重继承
注意,多重继承和多表继承是两码事,两个概念。
Django 的模型体系支持多重继承,就像 Python 一样。如果多个父类都含有 Meta 类,则只有第一个父类的会被使用,剩下的会忽略掉。
一般情况,能不要多重继承就不要,尽量让继承关系简单和直接,避免不必要的混乱和复杂。
请注意,继承同时含有相同 id 主键字段的类将抛出异常。为了解决这个问题,你可以在基类模型中显式的使用 AutoField 字段。
警告
在 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)