54. django之模型层_多表查询

1. 正反向

1.1 概念

当前对象是否含有外键字段,有则是正向,没有则是反向

正向
  由书籍查询出版社,外键字段在书籍表中,书籍查出版社就是'正向'
  由书籍查询作者,外键字段在书籍表中,书籍查作者就是'正向'
  由作者查询作者详情,外键字段在作者表中,作者查作者详情就是'正向'
反向
  由出版社查询书籍,外键字段不在出版社表中, 出版社查书籍就是'反向'

1.2 查询口诀

正向查询按照外键字段名

反向查询按照表名小写   

  反向查询结果有多个对象,小写后需要加 _set

  反向查询结果只有一个对象,小写后不需要加 _set

2. 多表查询

2.1 子查询(基于对象的跨表查询)

[1] 前言

子查询方法:分步操作

基础数据准备

image

image

image

image

image

[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)

image

(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)

image

(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)

image

(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())

image

(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)  # 得到的数据集对象

image

(2) 查询某个作者编写的书籍名称和出版日期

作者没有书籍的外键字段--反向--按表名小写查询

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Author.objects.filter(name='avril').values('book__title', 'book__pub_time')
    print(res)  # 得到的数据集对象

image

(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': '加州出版社'}
(2) 查询文学书籍对应的作者姓名和年龄

由作者查询书籍--外键字段不在作者表--反向--按表名小写查询

from app01 import models # 只能在django启动之后再导入模型表 res = models.Author.objects.filter(book__title='文学').values('name', 'age') print(res) # <QuerySet [{'name': 'avril', 'age': 19}, {'name': 'kylian', 'age': 20}]> print(res.first()) # {'name': 'avril', 'age': 19}
(3) 查询某个作者的详细信息

由详细信息表查询作者--外键字段不在详细信息表--反向--按表名小写查询

    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': '上海'}
(1) 查询牛津出版社出版的书籍名称和价格

由书籍查询出版社--外键字段在书籍表--正向--按外键字段名查询

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.filter(publish__name='牛津出版社').values('title', 'price')
    print(res)

image

(2) 查询某个作者编写的书籍名称和出版日期

 由书籍查询作者--外键字段在书籍表--正向--按外键字段名查询

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.filter(authors__name='avril').values('title', 'pub_time')
    print(res)

image

(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)

image

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)

image

[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)

image

[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)

image

[4] 查询每个作者出的书的总价格

 根据作者进行分组
作者表查询书籍--作者表没有书籍外键字段--反向(表名小写)--使用Sum计算书籍总价格
自定义变量名存储书籍总价格
    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)

image

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

填入基础数据

image

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)

image

(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('经典')))

image

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)

image

(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)

image

[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)

image

 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)

image

6. ORM查询优化

6.1 前言

[1] 代码示例

在项目的settings.py文件中增加LOGGING 配置,用于后端打印SQL语句

运行以下代码时没有后端不会打印SQL语句

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.all()

image

 打印res时后端会同步打印SQL语句

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.all()
    print(res)  # 只有需要用到真正的数据时,才会通过数据库查询数据

image

[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)

image

遍历结果得到每一个字典

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.values('title', 'price')
    for i in res:
        print(i)

image

字典通过.get取值

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.values('title', 'price')
    for i in res:
        print(i.get('title'))

image

通过字典.title的方式无法获取值

    from app01 import models  # 只能在django启动之后再导入模型表

    res = models.Book.objects.values('title', 'price')
    for i in res:
        print(i.title)

image

需求:通过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)

image

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)

image

[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)

image

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)

image

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)

image

[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)

image

只读取主表数据,要将结果数据集转列表才不会触发新的查询

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

image

添加基础数据

image

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'),
]

根页面分别访问事务存在与不存在的链接,报错可用忽略

image

image

image

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

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

image

[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


 

posted @ 2026-04-13 19:48  pythondjango  阅读(3)  评论(0)    收藏  举报