Django 中的 model 扮演了什么样的一种角色呢? 有点像我们在 SQL 中初始化一个数据表的格式时需要做的工作,即定义这个数据表的名字,各个字段,各个字段的类型,还有各个字段的一些限制,以及表与表之间的关联。
这里我们主要是根据 Django 官网上给出的 models 的一些解释,对其中的一些需要注意的技巧进行讲解,可能会起到事半功倍的效果。
1. 字段
如果我们把每一个 model 看成一个表,那么 model 里面的每个定义的变量,相当于数据表的一个字段,那么我们就需要对字段的格式做一些定义,包括SQL里面常用的 int, float, char 类型等等,Django 在整个的接口里面给我们提供了相对于SQL更加丰富的数据格式。这边我们不做详细概述,只是对其中特别有意思的点:字段选项 拿出来解析
字段选项:
blank : 这个是个布尔值,将这个值放在null 选项前面的原因是因为这个字段对 null 取到一个过滤的作用。blank 的意思是能否让该字段接受空值,即没有值的情况,如果可以,再去检查如果对空值进行处理,即查看null 选项。
null : 这个是个布尔值,即只能选择 True 或者 False,如果是 True,那么当 blank 为 True,即该字段可以接受空值的时候,将空值设置为数据库中的 NULL,否则保留原来的空值进入数据库。
choices: 这个字段给该字段的值划定了一个范围,即该字段的可以选择的值的范围。如果写入该字段的值不在范围之内,则被拒绝。
default: 该字段的默认对象,即默认该字段有这个值,所以永远不会为空,除非认为设置为 NULL 值。
help_text:这个是额外的帮助文本,在将model当作表单输出到前端的HTML页面的时候,非常有用,主要是用来对该字段进行解释,而且可以进行国际化操作,即可以设置多种语言,针对不同的人群进行说明 model 的字段。
2. Meta 选项
使用内部 Meta类 来给模型赋予元数据,就像:
from django.db import models
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
这里的 Meta 元数据里面包含了排序的字段,model 对应的复数名字,即 Ox 的复数英文为 oxen
3. 模型方法
在模型中添加自定义方法会给你的对象提供自定义的“行级”操作能力。与之对应的是类 Manager 的方法意在提供“表级”的操作,模型方法应该在某个对象实例上生效。
这是一个将相关逻辑代码放在一个地方的技巧——模型。
比如,该模型有一些自定义方法:
3.1 模型内部自定义方法
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birth_date = models.DateField()
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
elif self.birth_date < datetime.date(1965, 1, 1):
return "Baby boomer"
else:
return "Post-boomer"
@property
def full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)
这里的 baby_boomer_status 就是模型的一个方法,而加了 property 修饰符的 full_name 函数就是模型的属性函数之一,可以直接通过 实例.full_name 来达到 实例.full_name() 的同样的效果。
3.2 执行自定义 SQL
最好用例子来解释。假设你有以下模型:
class Person(models.Model):
first_name = models.CharField(...)
last_name = models.CharField(...)
birth_date = models.DateField(...)
然后你可以像这样执行自定义 SQL:
>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
... print(p)
John Smith
Jane Jones
这里的 model.manager.raw('自己的SQL语句') 的方法,能够实现一些在 Django 自己的 QuerySet 查询语句中实现起来比较困难的SQL语句。
或者如以下的方法
生成新的字段
>>> Person.objects.raw('''SELECT first AS first_name,
... last AS last_name,
... bd AS birth_date,
... pk AS id,
... FROM some_other_table''')
将参数传给 raw
>>> lname = 'Doe'
>>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname])
直接执行自定义 SQL
from django.db import connection
def my_custom_sql(self):
with connection.cursor() as cursor:
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
row = cursor.fetchone()
return row
要避免 SQL 注入,你绝对不能在 SQL 字符串中用引号包裹 %s
占位符。
注意,若要在查询中包含文本的百分号,你需要在传入参数使用两个百分号:
cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id])
默认情况下,Python DB API 返回的结果不会包含字段名,这意味着你最终会收到一个 list
,而不是一个 dict
。要追求较少的运算和内存消耗,你可以以 dict
返回结果,通过使用如下的语法:
def dictfetchall(cursor):
"Return all rows from a cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]
另一个选项是使用来自 Python 标准库的 collections.namedtuple()。 namedtuple
是一个类元组对象,可以通过属性查找来访问其包含的字段;也能通过索引和迭代。结果都是不可变的,但能通过字段名或索引访问,这很实用:
from collections import namedtuple
def namedtuplefetchall(cursor):
"Return all rows from a cursor as a namedtuple"
desc = cursor.description
nt_result = namedtuple('Result', [col[0] for col in desc])
return [nt_result(*row) for row in cursor.fetchall()]
这有个例子,介绍了三者之间的不同:
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> cursor.fetchall()
((54360982, None), (54360880, None))
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> dictfetchall(cursor)
[{'parent_id': None, 'id': 54360982}, {'parent_id': None, 'id': 54360880}]
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> results = namedtuplefetchall(cursor)
>>> results
[Result(id=54360982, parent_id=None), Result(id=54360880, parent_id=None)]
>>> results[0].id
54360982
>>> results[0][0]
54360982
3.3 重写之前定义的模型方法
还有一个 模型方法 的集合,包含了一些你可能自定义的数据库行为。尤其是这两个你最有可能定制的方法 save()和 delete()。
你可以随意地重写这些方法(或其它模型方法)来更改方法的行为。
一个典型的重写内置方法的场景是你想在保存对象时额外做些事。比如(查看文档 save() 了解其接受的参数):
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, *args, **kwargs):
do_something()
super().save(*args, **kwargs) # Call the "real" save() method.
do_something_else()
4. 模型继承
Django 中的模型继承方法,和一般的 Python 模型继承类似,同时,也有自己的一些特征。
Django 有三种可用的继承风格。
- 常见情况下,你仅将父类用于子类公共信息的载体,因为你不会想在每个子类中把这些代码都敲一遍。这样的父类永远都不会单独使用,所以 抽象基类 是你需要的。
- 若你继承了一个模型(可能来源于其它应用),且想要每个模型都有对应的数据表,那么应该尝试 多表继承。
- 最后,若你只想修改模型的 Python 级行为,而不是以任何形式修改模型字段, 代理模型 会是比较适合的选择。
4.1 抽象基类
抽象基类在你要将公共信息放入很多模型时会很有用。编写你的基类,并在 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)
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'
由于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。如果你未指定抽象基类中的 related_name 属性,默认的反转名会是子类名,后接 '_set' 。这名字看起来就像你在子类中定义的一样。比如,在上述代码中,若省略了 related_name 属性, ChildA 的 m2m 字段的反转名会是 childa_set , ChildB 的是 childb_set。
5. 在一个包中管理模型
manage.py startapp 命令创建了一个应用结构,包含一个 models.py
文件。若你有很多 models.py
文件,用独立的文件管理它们会很实用。
为了达到此目的,创建一个 models
包。删除 models.py
,创建一个 myapp/models
目录,包含一个 __init__.py
文件和存储模型的文件。你必须在 __init__.py
文件中导入这些模块。
比如,若你在 models
目录下有 organic.py
和 synthetic.py
:
from .organic import Person
from .synthetic import Robot
6. 在model作为其他 model 的 foreign key 的情况下更新主键
由于在这种情况下我们需要更改已经完成的外键关系,所以我们
1. 首先删除所有的数据,尤其是外键所在的model 中的全部数据
2. 删除APP下的所有的 migrations 文件夹数据
3.新建对应的SQL代码
python manage.py makemigrations
4. 同步数据库
python manage.py migrate APP_NAME --run-syncdb