54. django之模型层_多表查询
1. 正反向
1.1 概念
当前对象是否含有外键字段,有则是正向,没有则是反向
正向
由书籍查询出版社,外键字段在书籍表中,书籍查出版社就是'正向'
由书籍查询作者,外键字段在书籍表中,书籍查作者就是'正向'
由作者查询作者详情,外键字段在作者表中,作者查作者详情就是'正向'
反向
由出版社查询书籍,外键字段不在出版社表中, 出版社查书籍就是'反向'
1.2 查询口诀
正向查询按照外键字段名
反向查询按照表名小写
反向查询结果有多个对象,小写后需要加 _set
反向查询结果只有一个对象,小写后不需要加 _set
2. 多表查询
2.1 子查询(基于对象的跨表查询)
[1] 前言
子查询方法:分步操作基础数据准备





[2] 正向查询示例
(1) 查询物理学书籍对应的出版社
书籍查出版社--正向--按外键字段查
书籍与出版社是一对多的关系,外键字段放在多的一方(书籍)
# 1.获取书籍对象
book_obj = models.Book.objects.filter(title='物理学').first()
# 2.按正反向的概念用外键字段进行跨表查询
res = book_obj.publish
print(res) # 得到的是Publish对象
print(res.name) # 加州出版社
print(res.id) # 1
print(res.addr) # 美国
(2) 查询文学书籍对应的作者
书籍查作者--正向--按外键字段查
书籍与作者是多对多的关系,相比于步骤[2]要加上 .all( ) 即得到的结果有多个对象需要.all()
from app01 import models # 只能在django启动之后再导入模型表
# 1.获取书籍对象
book_obj = models.Book.objects.filter(title='文学').first()
# 2.按正反向的概念用外键字段进行跨表查询
res = book_obj.authors.all()
print(res) # 得到的是数据集对象 <QuerySet [<Author: Author object (1)>, <Author: Author object (2)>]>
# 得到对象
author_obj1 = res.first()
author_obj2 = res.last()
# 打印数据集的每个对象具体信息
print(author_obj1.id)
print(author_obj1.name)
print(author_obj1.age)
print(author_obj2.id)
print(author_obj2.name)
print(author_obj2.age)

(3) 查询某个作者的详细信息
作者查作者信息--正向--按外键字段查
作者与作者信息是一对一关系,外键字段放在查询频率高的一方
from app01 import models # 只能在django启动之后再导入模型表
# 1.获取作者对象
author_obj = models.Author.objects.filter(name='avril').first()
# 2.按正反向的概念用外键字段进行跨表查询
res = author_obj.author_detail
print(res) # 得到的是作者详细信息对象AuthorDetail object (1)
print(res.id) # 1
print(res.phone) # 139666
print(res.addr) # 上海
[3] 反向查询示例
(1) 查询牛津出版社出版的书籍
出版社查询书籍--外键字段不在出版社表中--出版社查书籍就是反向--按表名小写查询
由于出版社有多本书籍,结果有多个对象需要加 _set.all( )
from app01 import models # 只能在django启动之后再导入模型表
# 1.获取出版社对象
pub_obj = models.Publish.objects.filter(name='牛津出版社').first()
# 2.反向,用表名小写进行跨表查询
res = pub_obj.book_set.all()
print(res) # 得到的是数据集
# 得到对象
book_obj1 = res.first() # 也可以通过遍历的方式获取对象
book_obj2 = res.last()
# 打印数据集的每个对象具体信息
print(book_obj1 .id)
print(book_obj1 .title)
print(book_obj1 .price)
print(book_obj2.id)
print(book_obj2.title)
print(book_obj2.price)

(2) 查询某个作者编写的书籍
作者的模型表中没有与书有关的外键字段--反向--按表名小写查询
作者与书是多对多关系
一个作者可以有多本书籍,结果有多个对象需要加 _set.all( )
from app01 import models # 只能在django启动之后再导入模型表
# 1.获取作者对象
author_obj = models.Author.objects.filter(name='avril').first()
# 2.反向,用表名小写进行跨表查询
res = author_obj.book_set.all() # 一个作者可能对应多本书,需要.all
print(res) # 得到的是数据集
# 得到对象
book_obj1 = res.first() # 也可以通过遍历的方式获取对象
book_obj2 = res.last()
# 打印数据集的每个对象具体信息
print(book_obj1 .id)
print(book_obj1 .title)
print(book_obj1 .price)
print(book_obj2.id)
print(book_obj2.title)
print(book_obj2.price)

(3) 查询某个手机号对应的作者
作者详细信息表没有与作者关联的外键字段--反向
作者详细信息与作者是一对一关系
结果只有一个对象不需要加 _set
from app01 import models # 只能在django启动之后再导入模型表
# 1.获取作者详细信息对象
author_detail_obj = models.AuthorDetail.objects.filter(phone=139666).first()
# 2.反向,用表名小写进行跨表查询
res = author_detail_obj.author
print(res) # Author object (1)
print(res.id) # 1
print(res.name) # avril
print(res.age) # 19
[4] 总结
正向查询结果有多个对象需要加 .all( )
反向查询结果有多个对象需要加 _set.all( )
反向查询结果只有一个对象不需要加 _set
2.2 联表查询(基于双下划线的跨表查询)
[1] 联表查询方法
主表对象.values(外键字段_ _从表的查询字段,其他字段)
括号内既可以通过外键字段查询从表的字段,也可以查询主表本身的字段
[2] 正向查询示例--与子查询保持一致
(1) 查询物理学书籍对应的出版社
相比于2.1的分步操作,使用一条orm实现查询
书籍表有出版社id外键字段--正向--按外键字段查询
from app01 import models # 只能在django启动之后再导入模型表
# 查询出版社名称,也查询出版社地址
res = models.Book.objects.filter(title='物理学').values('publish__name', 'publish__addr')
print(res) # <QuerySet [{'publish__name': '加州出版社', 'publish__addr': '美国'}]>
print(res.first()) # {'publish__name': '加州出版社', 'publish__addr': '美国'}
(2) 查询文学书籍对应的作者
书籍表有作者外键字段--正向--按外键字段查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.filter(title='文学').values('authors__name', 'authors__age')
print(res) # 有两个作者,得到的是数据集对象
print(res.first())
print(res.last())

(3) 查询某个作者的详细信息
作者表有作者详细信息外键字段--正向--按外键字段查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Author.objects.filter(name='avril').values('author_detail__addr', 'author_detail__phone')
print(res) # <QuerySet [{'author_detail__addr': '上海', 'author_detail__phone': 139666}]>
print(res.first()) # {'author_detail__addr': '上海', 'author_detail__phone': 139666}
[3] 反向查询示例--与子查询保持一致
(1) 查询牛津出版社出版的书籍名称和价格
出版社没有书籍的外键字段--反向--按表名小写查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Publish.objects.filter(name='牛津出版社').values('book__title', 'book__price')
print(res) # 得到的数据集对象

(2) 查询某个作者编写的书籍名称和出版日期
作者没有书籍的外键字段--反向--按表名小写查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Author.objects.filter(name='avril').values('book__title', 'book__pub_time')
print(res) # 得到的数据集对象

(3) 查询某个手机号对应的作者的姓名和年龄
作者详细信息表没有作者的外键字段--反向--按表名小写查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.AuthorDetail.objects.filter(phone=139666).values('author__name', 'author__age')
print(res) # <QuerySet [{'author__name': 'avril', 'author__age': 19}]>
2.3 联表查询扩展--使用从表查询
前提:不能以 models.主表.objects.filter 的形式
(1) 查询物理学书籍对应的出版社
models不能调用主表,就调用从表
由出版社查询书籍--外键字段不在出版社表--反向--按表名小写查询
相比于2.2,在filter之后不加values得到主表数据集对象
from app01 import models # 只能在django启动之后再导入模型表
res = models.Publish.objects.filter(book__title='物理学')
print(res) # <QuerySet [<Publish: Publish object (1)>]>
要获取出版社名称,此时调用主表过滤得到的对象,在values里面填入主表的字段即可
from app01 import models # 只能在django启动之后再导入模型表
res = models.Publish.objects.filter(book__title='物理学').values('name')
print(res) # <QuerySet [{'name': '加州出版社'}]>
print(res.first()) # {'name': '加州出版社'}
由作者查询书籍--外键字段不在作者表--反向--按表名小写查询
由详细信息表查询作者--外键字段不在详细信息表--反向--按表名小写查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.AuthorDetail.objects.filter(author__name='avril').values('phone', 'addr')
print(res) # <QuerySet [{'phone': 139666, 'addr': '上海'}]>
print(res.first()) # {'phone': 139666, 'addr': '上海'}
由书籍查询出版社--外键字段在书籍表--正向--按外键字段名查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.filter(publish__name='牛津出版社').values('title', 'price')
print(res)
![]()
由书籍查询作者--外键字段在书籍表--正向--按外键字段名查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.filter(authors__name='avril').values('title', 'pub_time')
print(res)

(3) 查询某个手机号对应的作者的姓名和年龄
由作者查询详细信息--外键字段在书作者表--正向--按外键字段名查询
from app01 import models # 只能在django启动之后再导入模型表
res = models.Author.objects.filter(author_detail__phone='139666').values('name', 'age')
print(res) # <QuerySet [{'name': 'avril', 'age': 19}]>
2.4 三张表查询
[1] 使用主表查询
查询文学书籍对应的作者的手机号
书籍表--[中间:作者表]--详细信息表
书籍表有作者外键字段(正向)--按外键字段名查询--作者表有详细信息外键字段--作者表调用详细信息外键
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.filter(title='文学').values('authors__author_detail__phone')
print(res) # <QuerySet [{'authors__author_detail__phone': 139666}, {'authors__author_detail__phone': 139999}]>
[2] 使用从表查询
查询文学书籍对应的作者的手机号
详细信息--[中间:作者表]--书籍表
详细信息表没有作者外键字段(反向)--按表名小写查询--作者表没有书籍外键字段(反向)--按表名小写(作者表调用书籍表名小写)
from app01 import models # 只能在django启动之后再导入模型表
res = models.AuthorDetail.objects.filter(author__book__title='文学').values('phone')
print(res) # <QuerySet [{'phone': 139666}, {'phone': 139999}]>
3. 聚合查询
3.1 前言
MySQL的聚合函数:Max, Min, Sum, Count, Avg
django使用聚合函数需要先导入模块
和数据库相关的模块,在 django.db和django.db.models里面
一般情况下,需要先分组再进聚合函数运算,但是Django提供了一种方法 :aggregate 可以不分组对某个字段使用聚合函数
3.2 代码
[1] 查询书籍表平均的价格--一次使用一个聚合函数
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Book.objects.aggregate(Avg('price'))
print(res) # {'price__avg': Decimal('11.301667')}
[2] 一次使用多个聚合函数
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Book.objects.aggregate(Avg('price'), Max('price'), Min('price'), Count('id')) # 统计id的个数
print(res)

4. 分组查询
4.1 前言
MySQL分组操作:group by
django中分组的关键字是annotate( )
models.Book.objects.annotate( ) 按照每一本书进行分组,每本书是一个组
annotate( )的参数:
聚合函数(最常用):Max, Min, Sum, Count, Avg
高级表达式:
F('字段名'):引用字段值进行计算
ExpressionWrapper(...):复杂计算逻辑
Func(...):自定义数据库函数
4.2 以表为单位进行分组
[1] 统计每一本书的作者个数
书籍进行分组,每本书是一个组
分组之后书籍表查询作者表--书籍表有作者外键字段--正向(外键字段名)--使用Count计算id个数得到作者个数
自定义的变量名存储每本书作者个数--通过.values获取书籍名和作者个数
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Book.objects.annotate(Count('authors__id')) # 通过分组得到6个对象
print(res)
res2 = models.Book.objects.annotate(author_num=Count('authors__id')).values('title', 'author_num')
print(res2)

[2] 统计每个出版社最便宜的书的价格
对出版社分组,每个出版社是一个组
出版社查询书籍--出版社表没有书籍外键字段--反向(表名小写)
自定义变量名存储最低价格
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Publish.objects.annotate(MinPrice=Min('book__price')).values('name', 'MinPrice')
print(res)

[3] 统计不止一个作者的书籍
根据书籍进行分组
分组之后书籍表查询作者表--书籍表有作者外键字段--正向(外键字段名)--使用Count计算id个数得到作者个数
自定义的变量名存储每本书作者个数
使用filter过滤作者个数大于1
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Book.objects.annotate(AuthorNum=Count('authors__id')).filter(AuthorNum__gt=1).values('title', 'AuthorNum')
print(res)

[4] 查询每个作者出的书的总价格
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Max, Min, Avg, Count, Sum
res = models.Author.objects.annotate(TotPrice=Sum('book__price')).values('name', 'TotPrice')
print(res)
![]()
4.3 以字段为单位进行分组
annotate在values之前,以表为单位进行分组
models.表名.objects.annotate().values('字段名')
annotate在values之后,以字段为单位进行分组
models.表名.objects.values('字段名').annotate()
5. F与Q查询
5.1 前言
[1] orm迁移知识补充
当orm已经迁移,数据库中有数据时,在django的模型表中新添加字段,需要指定默认值或为null
方式1:models.IntegerField(verbose_name='...', default=...)
方式2:models.IntegerField(verbose_name='...', null=True)
[2] 基础数据准备
在书籍表中添加两个字段
class Book(models.Model):
title = models.CharField(max_length=32, verbose_name='书名')
# 整数位加小数位最多只能有8位,小数部分固定2位
price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格')
pub_time = models.DateTimeField(auto_now_add=True, verbose_name='出版时间')
# 书与出版社的外键字段 一对多
# 默认关联主键字段,models.CASCADE级联更新与级联删除
publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE)
# 书与作者的外键字段 多对多
authors = models.ManyToManyField(to='Author') # 自动创建书与作者的关联关系表
"""F与Q查询额外添加的字段"""
sold = models.IntegerField(verbose_name='卖出数量', null=True)
stock = models.IntegerField(verbose_name='库存', null=True)
运行manage.py任务--makemigrations--migrate
填入基础数据

5.2 F查询
[1] 引入
查询库存大于卖出数量的书籍,传统查询方法无法实现
[2] F查询概念
直接引用模型字段的值,在数据库层面执行计算或比较,不将数据加载到 Python 内存。
核心作用:用于操作 / 比较字段值
[3] 代码
(1) 比较字段值
查询库存大于卖出数量的书籍
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import F
res = models.Book.objects.filter(stock__gt=F('sold'))
print(res) # <QuerySet [<Book: Book object (1)>, <Book: Book object (2)>]>
print(res.first().title) # 物理学
print(res.last().title) # 数学
(2) 操作字段值--计算
将所有书籍的价格提高1000
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import F
models.Book.objects.update(price=F('price')+1000)

(3) 操作字段值--拼接字符串
F查询不能直接用于操作字段值时拼接字符串,需要配合Concat
需求:给所有书名称之后加上经典两个字
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import F, Value
from django.db.models.functions import Concat
models.Book.objects.update(title=Concat(F('title'), Value('经典')))

5.3 Q查询
[1] 引入
查询价格大于1011.26或卖出数量大于33的书籍,传统查询方法无法实现
filter( )方法中的关键字参数查询逻辑关系为或 "and"
models.Book.objects.filter(price__gt=1011.26, sold__gt=33) price__gt与sold__gt是或的关系
[2] Q查询概念
作用:构建复杂的逻辑查询条件,支持 或(|)、与(&)、非(~)组合。
核心用途: filter() 是 与 逻辑,Q() 用于实现 或、非 等复杂逻辑。
django中只能写 | & ~,不能写or and not
[3] 代码
使用 | 连接或使用or连接都可以达到或的效果
(1) 或 |
查询价格大于1011.26或卖出数量大于33的书籍
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Q
res = models.Book.objects.filter(Q(price__gt=1011.26) | Q(sold__gt=33))
print(res)

(2) 取反 ~
在步骤(1)的基础上对价格大于1011.26取反,即价格小于等于1011.26或卖出数量大于33
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Q
res = models.Book.objects.filter(~Q(price__gt=1011.26) | Q(sold__gt=33))
print(res)

[3] Q对象用于当作查询条件
append括号内元组的第一个元素会被当作查询条件的左边,第二个元素会被当作查询条件右边
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Q
q_obj = Q()
# Q对象添加第一个查询条件
q_obj.children.append(('sold__gt', 40)) # 里面必须放元组
# Q对象添加第二个查询条件
q_obj.children.append(('price__lt', 1011.30))
# filter除了可以放条件,还可以放Q对象
res = models.Book.objects.filter(q_obj)
print(res)
print(res.query)
![]()
Q对象append的两个查询条件是and关系
Q对象append的查询条件左边不再是字段变量名,而是字符串
将多个查询条件默认and关系改为or,q_obj.connector = 'or'
from app01 import models # 只能在django启动之后再导入模型表
from django.db.models import Q
q_obj = Q()
q_obj.connector = 'or'
# Q对象添加第一个查询条件
q_obj.children.append(('sold__gt', 40)) # 里面必须放元组
# Q对象添加第二个查询条件
q_obj.children.append(('price__lt', 1011.30))
# filter除了可以放条件,还可以放Q对象
res = models.Book.objects.filter(q_obj)
print(res)
print(res.query)
![]()
6. ORM查询优化
6.1 前言
[1] 代码示例
在项目的settings.py文件中增加LOGGING 配置,用于后端打印SQL语句
运行以下代码时没有后端不会打印SQL语句
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.all()

打印res时后端会同步打印SQL语句
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.all()
print(res) # 只有需要用到真正的数据时,才会通过数据库查询数据

[2] ORM语句的特点
(1)惰性查询:
如果仅仅是书写了ORM语句,在后面没有使用到就不会对数据库进行查询,当需要用到查询的数据时,ORM就会从数据库查询并返回数据。
(2) ORM查询默认自带分页功能
在以上打印的SQL语句中,ORM自动使用了LIMIT 21对查询结果进行分页
6.2 ORM查询优化方法only/defer
[1] 前言
获取每一本书的名称和价格
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.values('title', 'price')
print(res)

遍历结果得到每一个字典
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.values('title', 'price')
for i in res:
print(i)

字典通过.get取值
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.values('title', 'price')
for i in res:
print(i.get('title'))

通过字典.title的方式无法获取值
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.values('title', 'price')
for i in res:
print(i.title)

需求:通过i.title能够获取值
[2] only():只加载指定字段
作用:仅选取模型的特定字段,其他字段不会从数据库中获取。
效果:生成的 SQL 语句只会 SELECT 指定的字段。
模型表(类)调用only之后得到的结果是数据集,遍历数据集可以得到类,类调用属性(无需字符串)即可获取值
区别:步骤[1]遍历结果得到的是字典,遍历only得到的结果是类
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.only('title', 'price')
print(res)
for i in res:
print(type(i))
print(i.title)

only会产生数据集,数据集的类调用括号内已有的属性不会再从数据库查询
而结果数据集的类调用only中没有的属性,会从数据库中逐一查询,增加了SQL语句
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.only('title', 'price')
print(res)
for i in res:
print(type(i))
print(i.title)
print(i.sold)

[3] defer():排除指定字段
作用:与 only() 相反,加载除了指定字段之外的所有字段。
效果:生成的 SQL 语句会排除指定的字段。
场景:当不需要某些字段时使用。
defer括号内的属性不在查询结果数据集里面 ,查询该属性需要重新运行SQL语句
而如果查询的是非括号内的属性,则不需要再运行SQL语句
from app01 import models # 只能在django启动之后再导入模型表
res = models.Book.objects.defer('title', 'price')
print(res)
for i in res:
print(type(i))
print(i.sold)

6.3 ORM查询优化方法select_related /prefetch_related
[1] select_related选择关联(或称跟随关联)
(1) 概念
跟随外键(SQL JOIN 实现)
作用:在一次查询中,通过 SQL 的 JOIN 语句,把主表和从表的数据一次性加载出来。
原理:数据库层面直接连表查询,Python 层面无需额外操作。
适用场景:ForeignKey(多对一)和 OneToOneField(一对一)关系。
不能多对多或一对多(反向ForeignKey)
效果:只运行 1 次查询(通过 JOIN 连表)。
(2) 代码
select_related将主表和从表的数据一次性加载出来
# 使用 select_related一次性加载书和出版社
res = models.Book.objects.select_related('publish')
print(res)

select_related一次加载之后通过类调用属性不会触发新的查询
类调用自身的属性无需额外添加,调用从表的属性需要通过外键字段
from app01 import models # 只能在django启动之后再导入模型表
# 使用 select_related一次性加载书和出版社
res = models.Book.objects.select_related('publish')
print(res)
for i in res:
print(type(i))
print(i.title) # 不会触发新的查询
print(i.sold)
print(i.publish.name) # 不会触发新的查询
print(i.publish.addr)

[2] prefetch_related预加载关联
(1) 概念
预加载集合(Python 层面拼接)
作用:先运行一次主查询获取主表数据,再运行一次关联查询获取相关数据,最后在 Python 层面把它们拼接起来。分两步查询。
原理:不使用 SQL JOIN,而是分两次查询,在内存中组装数据。
适用场景:ManyToManyField(多对多)和 反向 ForeignKey(一对多)关系。
(2) 代码示例--反向 ForeignKey
需求:想要查询所有出版社,以及出版的所有的书;外键字段在Book,Publish 反向关联 Book
方法1:每次都查询数据库
res = Publish.objects.all()
for i in res:
print(i.book_set.all()) # 每次都查数据库
方法2:使用prefetch_related
prefetch_related先查询主表,再查询从表,分为两步查询
from app01 import models # 只能在django启动之后再导入模型表
# 先查所有出版社,再查出版社所有的书,在python中拼接
# 出版社查书反向查询结果有多个对象,需要加_set
res = models.Publish.objects.prefetch_related('book_set')
print(res)

只读取主表数据,要将结果数据集转列表才不会触发新的查询
from app01 import models
# 1. 预取关联数据
res = models.Publish.objects.prefetch_related('book_set')
# 2. 转列表:强制一次性完整求值并缓存全量结果(核心优化)
res = list(res)
# 3. 打印(可选,不会触发新查询)
print(res)
# 4. 保留的 for 循环(全程读内存,零新查询)
for i in res:
print(type(i))
print(i.name)
print(i.addr)
读取从表数据
from app01 import models
# 【核心优化1】预取关联数据 + 转list强制完整缓存
# 作用:一次性执行1次Publish主查询+1次Book关联查询,并将全量结果缓存到内存
# 效果:后续print(res)和for循环都直接读内存,彻底消除重复查询
res = list(models.Publish.objects.prefetch_related('book_set'))
print(res)
for i in res:
print(type(i))
print(i.name)
print(i.addr)
# 【核心优化2】直接从预取缓存的完整列表取数据
# 作用:不调用first()/filter()等会修改QuerySet的方法,完全复用prefetch_related的缓存
# 效果:遍历Book数据时零新查询
book_list = list(i.book_set.all())
if book_list:
for book in book_list:
print(book.title)
7. ORM常用的字段及参数
7.1 ORM常用字段
【1】AutoField
int自增列,必须填入参数 primary_key=True。
当model中如果没有自增列,则自动会创建一个列名为id的列。
【2】IntegerField
一个整数类型
范围在 -2147483648 to 2147483647。(一般不用它来存手机号(位数也不够),直接用字符串存,)
【3】BigIntegerField(IntegerField)
长整型(有符号的)
范围在 -9223372036854775808 ~ 9223372036854775807
【4】CharField
字符类型,必须提供max_length参数, max_length表示字符长度。
verbox_name 标识字段的注释
【5】EmailField(CharField)
varchar(254)
【6】DecimalField(Field)
max_digits,小数总长度
decimal_places,小数位长度
【7】TextField(Field)
文本类型
支持大段内容,无字数限制
【8】FileField(Field)
字符串,路径保存在数据库,文件上传到指定目录
参数:
upload_to = "/data"
给该字段传一个文件对象,会自动将文件保存到 /data 目录下,然后将文件路径保存到数据库中
【9】BooleanField(Field)
字段为布尔值
数据库里面可以存 0/1
【10】DateField和DateTimeField
(1)DateField
日期字段
日期格式 YYYY-MM-DD,相当于Python中的datetime.date()实例。
(2)DateTimeField
日期时间字段
格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ],相当于Python中的datetime.datetime()实例。
(3)重要参数
[1]auto_now_add
配置auto_now_add=True
创建数据记录的时候会把当前时间添加到数据库。
[2]auto_now
配置上auto_now=True
每次更新数据记录的时候会更新该字段。
【11】ForeignKey
(1)介绍
外键类型在ORM中用来表示外键关联关系,一般把ForeignKey字段设置在 '一对多'中'多'的一方。
ForeignKey可以和其他表做关联关系同时也可以和自身做关联关系。
(2)重要参数
[1]to
设置要关联的表
[2]to_field
设置要关联的表的字段
[3]on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。
on_delete=models.CASCADE
删除关联数据,与之关联也删除
【12】OneToOneField
(1)介绍
一对一字段。
通常一对一字段用来扩展已有字段。(通俗的说就是一个人的所有信息不是放在一张表里面的,简单的信息一张表,隐私的信息另一张表,之间通过一对一外键关联)
(2)重要参数
[1]to
设置要关联的表。
[2]to_field
设置要关联的字段。
[3]on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。
7.2 ORM字段与MySQL字段的对应关系
'AutoField': 'integer AUTO_INCREMENT',
'BigAutoField': 'bigint AUTO_INCREMENT',
'BinaryField': 'longblob',
'BooleanField': 'bool',
'CharField': 'varchar(%(max_length)s)',
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',
'DateField': 'date',
'DateTimeField': 'datetime',
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'DurationField': 'bigint',
'FileField': 'varchar(%(max_length)s)',
'FilePathField': 'varchar(%(max_length)s)',
'FloatField': 'double precision',
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'char(15)',
'GenericIPAddressField': 'char(39)',
'NullBooleanField': 'bool',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer UNSIGNED',
'PositiveSmallIntegerField': 'smallint UNSIGNED',
'SlugField': 'varchar(%(max_length)s)',
'SmallIntegerField': 'smallint',
'TextField': 'longtext',
'TimeField': 'time',
'UUIDField': 'char(32)'
7.3 字段参数
| 参数 | 作用说明 | 用法示例 |
|---|---|---|
primary_key |
设置该字段为模型的主键(一个模型只能有一个主键) | id = models.AutoField(primary_key=True) |
max_length |
指定字符型字段的最大长度(CharField 等字段必填) |
name = models.CharField(max_length=100) |
verbose_name |
设置字段在 Admin 后台或表单中的可读名称 | name = models.CharField(max_length=100, verbose_name="姓名") |
null |
数据库层面是否允许该字段为空(True/False,默认 False) |
email = models.EmailField(null=True) |
default |
设置字段的默认值 | status = models.IntegerField(default=1) |
max_digits |
DecimalField 必填,指定数字的总位数(整数位 + 小数位) |
price = models.DecimalField(max_digits=8, decimal_places=2) |
decimal_places |
DecimalField 必填,指定小数位数 |
同上 |
unique |
设置字段值在表中必须唯一(True/False,默认 False) |
username = models.CharField(max_length=50, unique=True) |
db_index |
为该字段创建数据库索引(True/False,默认 False),提升查询速度 |
title = models.CharField(max_length=200, db_index=True) |
auto_now |
DateTimeField/DateField 专用,每次保存对象时自动设为当前时间 |
update_time = models.DateTimeField(auto_now=True) |
auto_now_add |
DateTimeField/DateField 专用,对象首次创建时自动设为当前时间 |
create_time = models.DateTimeField(auto_now_add=True) |
choices |
设置字段的可选值,接收一个二元组列表(格式:[(数据库值, 显示值), ...]) |
GENDER_CHOICES = [('M', '男'), ('F', '女')]; gender = models.CharField(max_length=1, choices=GENDER_CHOICES) |
to |
ForeignKey/ManyToManyField 必填,指定关联的模型(字符串或模型类) |
author = models.ForeignKey(to='Author', on_delete=models.CASCADE) |
to_field |
ForeignKey 可选,指定关联模型的具体字段(默认关联主键) |
author = models.ForeignKey(to='Author', to_field='name', on_delete=models.CASCADE) |
db_constraint |
ForeignKey 可选,是否在数据库层面创建外键约束(True/False,默认 True) |
author = models.ForeignKey(to='Author', on_delete=models.CASCADE, db_constraint=False) |
7.4 字段参数choices用法
models.py中定义模型表
class Info(models.Model):
name = models.CharField(max_length=32)
pwd = models.IntegerField()
data = ((1, 'avril'), (2, 'kylian'), (3, 'haaland'))
rapper = models.IntegerField(choices=data)
迁移至数据库 makemigrations-- migrate

添加基础数据

tests.py中测试
from app01 import models # 只能在django启动之后再导入模型表
info_obj = models.Info.objects.get(pk=2)
print(info_obj.name) # john
print(info_obj.pwd) # 456
# 获取choices的内容
print(info_obj.rapper) # 3
print(info_obj.get_rapper_display()) # haaland
# 当choices对应的内容不存在时
info_obj2 = models.Info.objects.get(pk=1)
print(info_obj2.rapper) # 4
print(info_obj2.get_rapper_display()) # 4
8. ORM事务操作
8.1 事务的四大特性
(1)原子性(Atomicity)
事务被视为一个不可分割的原子操作单元。
这意味着要么全部操作成功并永久保存,要么全部操作失败并回滚到事务开始前的状态,不存在部分成功或部分失败的情况。
(2)一致性(Consistency)
事务在执行前后,数据库都必须保持一致状态。
这意味着事务执行前后,数据库中的数据必须满足所有定义的完整性约束,例如列级别的约束、外键关系等。
(3)隔离性(Isolation)
事务之间应该相互隔离,每个事务的执行应该与其他事务的执行相互独立,互不干扰。
隔离性确保了多个事务可以并发执行,而不会产生不一致的结果。
(4)持久性(Durability)
一旦事务成功提交后,其所做的修改将永久保存在数据库中,即使发生系统故障或重启,数据也能够恢复到提交后的状态。
持久性通过将事务日志写入非易失性存储介质来实现,如硬盘驱动器或固态硬盘。
8.2 事务名词
开启事务:Start Transaction
提交事务:Commit Transaction 提交事务才能保存
回滚事务:Rollback Transaction
事务结束:End Transaction
8.3 默认事务行为
Django是支持事务操作的,它的默认事务行为是自动提交。
具体表现形式为:每次数据库操作(比如调用save方法)会立即提交到数据库中。
但是如果想把连续的SQL操放在在一个事务里,就需要手动开启事务。
8.4 开启事务的两种方法
[1] 全局开启事务
全局开启事务只需要将数据库的配置项ATOMIC_REQUESTS设置为True
当 ATOMIC_REQUESTS = True 时,Django 会自动为每一个视图函数包裹在一个独立的数据库事务中:视图正常运行完成则提交事务,抛出未捕获异常则自动回滚整个事务。
transaction.non_atomic_requests的核心作用:为指定视图取消 ATOMIC_REQUESTS 的全局自动事务包裹,让该视图恢复 Django 默认的自动提交模式,不受全局事务规则约束,自主控制事务的提交与回滚
transaction.non_atomic_requests只有在开启了全局请求事务时才有意义
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库引擎
'NAME': "dj03_db", # 数据库名字
"USER": "root", # 数据库实际用户名
"PASSWORD": "123456", # 数据库实际密码
"HOST": "127.0.0.1", # 数据库 IP
"PORT": 3306,
"CHARSET": "utf8mb4", # 数据库编码
# 全局开启事务,绑定的是http请求响应整个过程
'ATOMIC_REQUESTS': True
}
}
app01的views.py中定义事务存在、事务不存在的视图函数,定义根目录函数
from django.shortcuts import render, HttpResponse
from django.db import transaction
from app01.models import *
# Create your views here.
def index(request):
return render(request, 'index.html', locals())
def y_transaction(request):
Book.objects.create(
title='y_transaction1',
price=6,
publish=Publish.objects.get(id=1)
)
# 抛出未捕获异常自动回滚整个事务
raise Exception("测试异常")
# 取消全局的事务特性
@transaction.non_atomic_requests
def n_transaction(request):
# 此处的数据库操作会进入django默认的自动提交模式
# 每条SQL运行之后立即提交,不会被放在全局事务中
Book.objects.create(
title='n_transaction2',
price=9,
publish=Publish.objects.get(id=1)
)
# 即使此处抛出异常,上面的增加操作也不会回滚(已自动提交)
raise Exception("测试异常")
app01的templates目录里定义index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p><a href="{% url 'y_transaction6' %}">事务存在的</a></p>
<p><a href="{% url 'n_transaction6' %}">事务不存在的</a></p>
</body>
</html>
项目的urls.py中添加路由
from django.contrib import admin
from django.urls import path, include
from app01.views import index, y_transaction, n_transaction
urlpatterns = [
path('admin/', admin.site.urls),
path('', index),
path('y_transaction/', y_transaction, name='y_transaction6'),
path('n_transaction/', n_transaction, name='n_transaction6'),
]
根页面分别访问事务存在与不存在的链接,报错可用忽略



当全局事务存在时,抛出未捕获异常自动回滚整个事务,不会向数据库新增数据
取消全局事务时,抛出未捕获异常之前的操作不会回滚,会向数据库新增数据

[2] 局部开启事务
(1) 方式一:作为装饰器使用
transaction.atomic 保证代码块内的所有数据库操作要么全部成功提交,要么全部失败回滚,是确保数据一致性的最常用手段。
代码块正常执行完毕 → 提交事务,所有操作永久生效。
代码块内抛出未捕获异常 → 回滚事务,所有操作撤销,数据库回到代码块执行前的状态。
装饰视图函数
from django.db import transaction
from django.http import HttpResponse
from .models import User, Account
# 整个视图函数包裹在事务中
@transaction.atomic
def transfer_view(request):
# 操作1:扣减A账户余额
account_a = Account.objects.get(user_id=1)
account_a.amount -= 100
account_a.save()
# 操作2:增加B账户余额
account_b = Account.objects.get(user_id=2)
account_b.amount += 100
account_b.save()
# 若此处抛出异常,操作1和操作2都会回滚
# raise Exception("转账失败")
return HttpResponse("转账成功")
(2) 方式二:作为上下文管理器使用
用 with transaction.atomic(): 包裹特定代码块,仅让该部分代码运行在事务中
from django.db import transaction
from django.http import HttpResponse
from .models import User, Log
def my_view(request):
# 操作1:不在事务中,自动提交
Log.objects.create(action="进入视图")
try:
# 操作2:仅该代码块在事务中
with transaction.atomic():
user = User.objects.create(username="test_user")
# 若此处抛出异常,仅回滚该代码块内的操作
raise Exception("创建用户失败")
except Exception as e:
print(e)
# 操作3:不在事务中,自动提交(不受上面异常影响)
Log.objects.create(action="视图结束")
return HttpResponse("done")
(3) 异常处理:关键注意事项
atomic 回滚事务的唯一触发条件是:代码块内抛出未捕获的异常。若异常被提前捕获,事务不会回滚,这是最常见的注意事项。
错误示例:在 atomic 块内捕获异常
@transaction.atomic
def bad_example(request):
try:
User.objects.create(username="test")
raise Exception("失败")
except Exception as e:
# 异常被捕获,atomic 认为代码块正常执行,事务会提交!
# test 用户会被创建,数据不一致
print(e)
正确示例 1:在 atomic 块外捕获异常
def good_example_1(request):
try:
with transaction.atomic():
User.objects.create(username="test")
raise Exception("失败")
except Exception as e:
# 异常在 atomic 块外捕获,atomic 块内的操作已回滚
print("事务已回滚")
正确示例 2:捕获后重新抛出异常
@transaction.atomic
def good_example_2(request):
try:
User.objects.create(username="test")
raise Exception("失败")
except Exception as e:
print(e)
# 重新抛出异常,触发 atomic 回滚
raise
9. 多对多的三种创建方式
9.1 自动创建
使用ManyToManyField自动创建关联关系表
class Book(models.Model):
title = models.CharField(max_length=32)
authors = models.ManyToManyField(to='Author')
class Author(models.Model):
title = models.CharField(max_length=32)
9.2 手动创建
使用ForeignKey手动创建关联关系表
class Book(models.Model):
title = models.CharField(max_length=32)
class Author(models.Model):
title = models.CharField(max_length=32)
class BookToAuthor(models.Model):
book_id = models.ForeignKey(to='Book')
author_id = models.ForeignKey(to='Author')
优点:扩展性好
缺点:无法使用外键方法和正反向
9.3 半自动创建
核心规则:through_fields 接收一个固定长度为 2 的元组,顺序绝对不能颠倒
第一个元素:关联关系表中,指向当前表(定义 ManyToManyField 的表)的外键字段名
第二个元素:关联关系表中,指向从表的外键字段名
class Book(models.Model):
name = models.CharField(max_length=32)
# through_fields : 第一个参数是当前表名称(对应关联关系表中的字段名)
authors = models.ManyToManyField(to='Author', through='BookAuthor', through_fields=('book', 'author'))
class Author(models.Model):
name = models.CharField(max_length=32)
class BookAuthor(models.Model):
book = models.ForeignKey(to='Book')
author = models.ForeignKey(to='Author')
多对多的ManyToManyField可以放在任意一方
当放在Author表时,through_fields的第一个参数应为关联关系表中的Author表外键字段名
class Book(models.Model):
title = models.CharField(max_length=32)
class Author(models.Model):
title = models.CharField(max_length=32)
# through_fields : 第一个参数是当前表名称(对应关联关系表中的字段名)
books = models.ManyToManyField(to='Book', through='BookAuthor', through_fields=('author', 'book'))
class BookAuthor(models.Model):
book = models.ForeignKey(to='Book')
author = models.ForeignKey(to='Author')
优点:可用使用正反向、关联关系表可以扩展
缺点:不能使用add,set,remove,clear

浙公网安备 33010602011771号