【Django】模型
1.模型
模型(model, MVC中的M)对应一个数据库表,相当于实体类。
官方文档:https://docs.djangoproject.com/en/stable/topics/db/models/
1.1 定义模型
应用目录下的models.py定义应用中使用的模型,每个模型都是django.db.models.Model的子类,类属性(Field类的实例)对应数据库中的字段,默认情况下属性名即为字段名,主键(id字段)将被自动添加。
例如:
1
2
3
4
5
6
7
8
9
10
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
相当于创建了以下两个表:
1
2
Question(id, question_text, pub_date)
Choice(id, question_id, choice_text, votes)
1.2 修改模型
初次创建模型后,使用makemigrations命令创建模型更改记录(migration,对应数据库表结构的修改):
1
python manage.py makemigrations polls
可使用sqlmigrate命令查看migration对应的SQL:
1
python manage.py sqlmigrate polls 0001
使用migrate命令将更改应用到数据库:
1
python manage.py migrate
修改模型的三个步骤:
- 修改models.py中的模型定义
- 运行
python manage.py makemigrations,为更改创建migration - 运行
python manage.py migrate,将更改应用到数据库
1.3 CRUD
模型类本身充当DAO,模型类本身及其objects属性(django.db.models.manager.Manager类型的对象)提供CRUD接口,查询语法见第3节。
1.4 admin应用
官方文档:https://docs.djangoproject.com/en/stable/ref/contrib/admin/
Django默认提供了一个admin应用,可通过UI进行模型的增删改查操作(admin应用会根据每个模型的字段自动生成表单,如下图所示)。
创建管理员用户:
1
python manage.py createsuperuser
在admin.py中注册想要管理的模型:
1
admin.site.register(Question)
访问 http://127.0.0.1:8000/admin/ 并使用管理员用户登录后进入管理界面。
ModelAdmin选项
可通过继承admin.ModelAdmin类来自定义admin应用中某个模型的展示界面
1
2
3
4
5
6
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'publisher')
list_filter = ['tag']
search_fields = ['title']
admin.site.register(Book, BookAdmin)
列表界面常用选项:
list_display:字段名列表,指定显示的字段(默认只有一列显示__str__()的结果)list_filter:字段名列表,指定右侧显示过滤器的字段search_fields:字段名列表,指定显示搜索框的字段list_per_page:指定列表界面每页显示的条目数,默认为100ordering:字段名列表,指定对象的排序方式,字段名前加"-"表示倒序排序,例如['-pub_date', 'author']表示先按pub_date倒序排序、再按author升序排序
详情界面常用选项:
fields:字段名称列表,指定表单中包含的字段exclude:字段名称列表,指定表单中排除的字段fieldsets:控制表单中的字段分组显示readonly_fields:字段名列表,指定表单中只读的字段raw_id_fields:字段名列表,表单中的外键字段默认显示为<select>或<select multiple>标签(此时下拉列表需要查询全部关联对象,当数据量很大时会非常慢);raw_id_fields中指定的(外键)字段将改为<input>标签,内容为逗号分隔的外键id,从而省去大量数据库查询
1.5 为模型提供初始数据
https://docs.djangoproject.com/en/stable/howto/initial-data/
可以使用数据迁移:https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations
也可以使用manage.py loaddata命令加载fixture:https://docs.djangoproject.com/en/stable/topics/db/fixtures/
1.6 模型继承
https://docs.djangoproject.com/en/stable/topics/db/models/#model-inheritance
- 如果只是使用父模型保存公共字段,父模型不会被单独使用,则使用抽象基类
- 如果要继承现有模型,每个模型有自己的数据库表,则使用多表继承
- 如果只是改变模型类的行为,而不改变模型字段,则使用代理模型
1.7 Meta类
https://docs.djangoproject.com/en/stable/ref/models/options/
每个模型类都可以定义一个名为Meta的内部类,用于指定元数据(例如数据库表名、人类可读名称、排序方式等)。
例如:
1
2
3
4
5
6
7
8
from django.db import models
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
常用选项:
db_table:数据库表名verbose_name:人类可读名称verbose_name_plural:名称复数形式ordering:字段列表,指定查询时的默认排序方式,添加"-"前缀表示倒序排序,例如ordering = ['-pub_date', 'author']permissions:二元组列表,自定义权限,二元组格式为(permission_code, human_readable_permission_name)
2.字段
官方文档:
- https://docs.djangoproject.com/en/stable/topics/db/models/#fields
- https://docs.djangoproject.com/en/stable/ref/models/fields/
2.1 Field类通用属性
db_column:数据库字段名,若未指定则使用field的名字primary_key:该字段是否为主键,如果一个模型未指定主键则Django将自动添加主键blank:如果为True则表单允许该字段为空,默认为Falsenull:如果为True则将空值存储为NULL(数据库允许该字段为空),默认为Falsedefault:字段默认值,可以是一个值或callable对象choices:字段可选值,相当于枚举类型(见“choices和枚举类型”)db_index:是否为该字段创建索引,默认Falseunique:如果为True则该字段的值唯一,此时自动创建UNIQUE索引verbose_name:人类可读名称(对于普通字段作为第一个位置参数,对于外键字段作为关键字参数)
注:字段默认均为非空,即SQL类型后添加NOT NULL。要允许字符串类型的字段为空则将null设置为True即可;要允许整型、日期等类型的字段为空,即要想实现表单中留空则数据库中存储为NULL,则将blank和null均设置True。例如:
1
2
foo = CharField(max_length=32) # foo VARCHAR(32) NOT NULL
bar = CharField(max_length=32, null=True) # bar VARCHAR(32) NULL
2.2 常用字段类型
| Field子类 | 等价SQL类型(MySQL) |
|---|---|
CharField(max_length=n) | VARCHAR(n) |
TextField() | LONGTEXT |
IntegerField() | INT |
BigIntegerField() | BIGINT |
AutoField() | INT AUTO_INCREMENT |
DecimalField(max_digits=m, decimal_places=n) | DOUBLE(m, n) |
DateField(auto_now=False, auto_now_add=False) | DATE |
DateTimeField(auto_now=False, auto_now_add=False) | DATETIME |
2.3 外键
官方文档:
- https://docs.djangoproject.com/en/stable/topics/db/models/#relationships
- https://docs.djangoproject.com/en/stable/ref/models/fields/#module-django.db.models.fields.related
查询语法:https://docs.djangoproject.com/en/stable/topics/db/queries/#related-objects
2.3.1 多对一
1
ForeignKey(to, on_delete)
例如,一个Question对应多个Choice,则在Choice类中定义:
1
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice.question是与choice关联的Question对象choice.question_id是与choice关联的Question的idquestion.choice_set.all()是与question关联的所有Choice对象
可通过related_name属性覆盖"choice_set"名称,设置为'+'则不创建反向关联。
on_delete指定当外键关联的对象被删除时执行的动作,可选值(都定义在django.db.models模块):
CASCADE:级联删除PROTECT:阻止删除,抛出ProtectedError异常SET_NULL:置为NULL,要求外键字段的null=TrueSET_DEFAULT:置为默认值,外键字段必须指定了default属性SET(value):置为指定值,如果value是函数则使用函数返回值
递归关系(一个模型和自己有多对一的关系):
1
models.ForeignKey('self', on_delete=models.CASCADE)
2.3.2 多对多
1
ManyToManyField(to)
例如:一个Author对应多个Paper,一个Paper也对应多个Author,则在Author类中定义:
1
papers = models.ManyToManyField(Paper)
author.papers.all()是与author关联的所有Paper对象paper.author_set.all()是与paper关联的所有Author对象
2.3.3 一对一
1
OneToOneField(to)
例如:User和Reader一一对应,则在Reader类中定义:
1
user = models.OneToOneField(User)
reader.user是与reader关联的User对象user.reader是与user关联的Reader对象
2.4 choices和枚举类型
https://docs.djangoproject.com/en/stable/ref/models/fields/#choices
Field.choices属性是二元组(value, name)的列表,二元组的第一项是字段的真实值,第二项是人类可读名称,例如:
1
2
3
4
5
6
7
8
9
class Student(models.Model):
YEAR_IN_SCHOOL_CHOICES = [
('FR', 'Freshman'),
('SO', 'Sophomore'),
('JR', 'Junior'),
('SR', 'Senior'),
('GR', 'Graduate'),
]
year_in_school = models.CharField(max_length=2, choices=YEAR_IN_SCHOOL_CHOICES, default='FR')
对于每一个设置了choices的字段,Django会自动为模型添加一个get_foo_display()方法,用于获取该字段当前值的人类可读名称:
1
2
3
>>> s = Student(year_in_school='GR')
>>> s.get_year_in_school_display()
'Graduate'
另外Django还专门为choices提供了枚举类型(内部使用Python的enum模块实现):
1
2
3
4
5
6
7
8
9
class Student(models.Model):
class YearInSchool(models.TextChoices):
FRESHMAN = 'FR', 'Freshman'
SOPHOMORE = 'SO', 'Sophomore'
JUNIOR = 'JR', 'Junior'
SENIOR = 'SR', 'Senior'
GRADUATE = 'GR', 'Graduate'
year_in_school = models.CharField(max_length=2, choices=YearInSchool.choices, default=YearInSchool.FRESHMAN)
枚举类的values, labels, choices, names属性分别为值列表、标签(人类可读名称)列表、二元组列表(同上)和枚举成员名称列表,枚举成员的label属性为对应的标签
1
2
3
4
5
class Suit(models.IntegerChoices):
SPADE = 1, 'Spade'
HEART = 2, 'Heart'
CLUB = 3, 'Club'
DIAMOND = 4, 'Diamond'
1
2
3
4
5
6
7
8
9
10
11
>>> Suit.values
[1, 2, 3, 4]
>>> Suit.labels
['Spade', 'Heart', 'Club', 'Diamond']
>>> Suit.choices
[(1, 'Spade'), (2, 'Heart'), (3, 'Club'), (4, 'Diamond')]
>>> Suit.names
['SPADE', 'HEART', 'CLUB', 'DIAMOND']
>>> suit = Suit.SPADE
>>> suit.label
'Spade'
3.查询
官方文档:https://docs.djangoproject.com/en/stable/topics/db/queries/
下面以Question和Choice两个模型为例(q表示Question类的对象,c表示Choice类的对象)。
3.1 创建对象
使用关键字参数创建模型对象,调用save()保存到数据库:
1
2
q = Question(question_text="What's new?", pub_date=datetime.now())
q.save()
捷径:create(),以上两行代码等价于
1
q = Question.objects.create(question_text="What's new?", pub_date=datetime.now())
批量创建:bulk_create(objs, batch_size=None)
3.2 更新对象
假设q已被保存到数据库
1
2
q.question_text = "What's up?"
q.save()
q.id为空时q.save()执行INSERT语句;q.id非空时q.save()执行UPDATE语句。
捷径:update(),以上两行代码等价于
1
q.update(question_text="What's up?")
更新外键字段
更新ForeignKey字段的方式与普通字段完全相同:
1
2
c.question = q
c.save()
使用关联对象更新ForeignKey字段的“另一边”(如q.choice_set)或ManyToManyField字段,参考:https://docs.djangoproject.com/en/stable/ref/models/relations/。例如:
q.choice_set.add(c)q.choice_set.set([c1, c2, ...])q.choice_set.remove(c)q.choice_set.clear()
3.3 删除对象
1
q.delete()
3.4 查询对象
查询的本质是通过模型类的Manager构造一个QuerySet。QuerySet相当于一个SELECT语句,每个模型类都有一个默认的Manager,即objects属性(注意是类属性,通过模型类访问,而不是模型类的对象)。
QuerySet API参考:https://docs.djangoproject.com/en/stable/ref/models/querysets/
(1)查询全部对象:Question.objects.all()
(2)查询记录数量:Question.objects.count()
(3)条件过滤
filter(**kwargs)返回一个新的QuerySet,包含满足指定条件的对象exclude(**kwargs)返回一个新的QuerySet,包含不满足指定条件的对象
注意:QuerySet是惰性求值的,构造的过程不执行任何数据库查询,只有最终获取对象时才执行查询。
kwargs格式:field__lookuptype=value
完整语法:https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups
(4)获取单个对象
get(**kwargs)返回满足条件的单个对象,如果不存在则产生Question.DoesNotExist,如果匹配到多个对象则产生MultipleObjectsReturnedget_or_create(**kwargs)查询单个对象,如果不存在则创建,返回(object, created)
(5)限制查询结果:可以使用Python的列表切片语法对QuerySet进行限制,等价于SQL的LIMIT和OFFSET子句
Question.objects.all()[:5]返回前5个对象(LIMIT 5)Question.objects.all()[5:10]返回第6~10个对象(OFFSET 5 LIMIT 5)Question.objects.order_by('-pub_date')[0]返回最新发布的一个问题(LIMIT 1)
(6)排序:order_by(*fields)
示例
(1)基本查询
①查询主键(id)为1的问题
1
2
Question.objects.filter(id=1)
Question.objects.get(pk=1)
等价于
1
SELECT * FROM Question WHERE id = 1
②查询票数小于1的选项
1
Choice.objects.filter(votes__lt=1)
等价于
1
SELECT * FROM Choice WHERE votes < 1
③查询以 “What” 开头的问题
1
Question.objects.filter(question_text__startswith='What')
等价于
1
SELECT * FROM Question WHERE question_text LIKE 'What%'
④查询包含 “up” 的问题
1
Question.objects.filter(question_text__contains='up')
等价于
1
SELECT * FROM Question WHERE question_text LIKE '%up%'
⑤查询2019年发布的问题
1
Question.objects.get(pub_date__year=2019)
等价于
1
SELECT * FROM Question WHERE YEAR(pub_date) = 2019
(2)连接查询
Question和Choice是一对多的关系,q.choice_set是一个包含q关联的所有Choice的QuerySet,c.question是c关联的Question。
①查询q关联的所有选项
1
q.choice_set.all()
等价于
1
2
3
4
SELECT Choice.*
FROM Choice INNER JOIN Question
ON Choice.question_id = Question.id
WHERE Question.id = q.id
②为q创建一个名为 “Not much” 、票数为0的选项
1
c = q.choice_set.create(choice_text='Not much', votes=0)
等价于
1
INSERT INTO Choice VALUES (NULL, 'Not much', 0, q.id)
③查询c关联的问题
1
c.question
等价于
1
SELECT * FROM Question WHERE id = c.question_id
④查询2019年发布的问题的所有选项
1
Choice.objects.filter(question__pub_date__year=2019)
等价于
1
2
3
4
SELECT Choice.*
FROM Choice INNER JOIN Question
ON Choice.question_id = Question.id
WHERE YEAR(Question.pub_date) = 2019
⑤查询q关联的以 “Just hacking” 开头的选项
1
c = q.choice_set.filter(choice_text__startswith='Just hacking')
等价于
1
SELECT * FROM Choice WHERE Choice.question_id = q.id AND Choice.choice_text LIKE 'Just hacking%'
4.使用现有数据库表
官方文档:https://docs.djangoproject.com/en/stable/ref/models/options/#managed
在模型的内部类Meta中指定managed = False,则Django不对该模型类进行创建表等操作,此时应明确定义主键字段。
1
2
3
4
5
6
class Foo(models.Model):
id = models.AutoField(primary_key=True)
# ...
class Meta:
managed = False
在单元测试中需要手动创建表
1
2
3
4
5
6
7
8
9
10
11
from django.db import connections
from django.test import TestCase
from foo.models import Foo
class FooTests(TestCase):
@classmethod
def setUpClass(cls):
with connection.schema_editor() as editor:
editor.create_model(Foo)
super().setUpClass()
如果要使用其他数据库则将connection改为connections['xxx'],xxx为DATABASES设置中定义的数据库别名。
