【13.0】Django框架之模型层

【一】前言

  • Django自带的sqlite3数据对日期格式不敏感,处理的时候容易出错

【1】测试脚本

  • 测试脚本

    • 当我们只想要测试Django中的某一个py文件的内容时,我们可以不需要书写前后端交互的形式,而是直接写一个测试脚本即可
  • 测试环境的准备

    • 在测试文件中书写下面内容
  • 这内容其实就是最外部 manage.py 文件中的上面几句话

    • 脚本代码无论是写在应用下的 tests.py文件还是自己新建文件,将内容写在新文件中,都会生效
from django.test import TestCase

# Create your tests here.
import os

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "day07.settings")

    import django
    django.setup()
    
    # 在下面书写我们需要测试的代码
    # 即所有的测试代码都必须等待环境准备完毕之后才能书写

【2】数据准备

  • 在models里面创建我们需要的数据库中的表
from django.db import models


# Create your models here.

class User(models.Model):
    # 创建一个字段,字段类型为字符串类型 ,最大长度为 32
    name = models.CharField(verbose_name="姓名", help_text="姓名", max_length=32)
    # 创建一个字段,字段类型为数字类型
    age = models.IntegerField(verbose_name="年龄", help_text="年龄")
    # 创建一个字段,字段类型为日期类型
    register_time = models.DateTimeField(verbose_name="注册时间", help_text="注册时间")
    '''
    DateField
    DateTimeField
        两个关键参数
            auto_now : 每次操作数据的时候该字段会自动将当前时间更新
            auto_now_add : 在创建数据的时候会自动将当前创建时间记录下来,只要不是人为修改,就不会发生更改
    '''
  • 配置数据库文件(settings.py)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        # 数据库名字
        'NAME': 'django_day10',
        # 用户
        "USER": "root",
        # 密码
        "PASSWORD": "1314521",
        # IP
        "HOST": "127.0.0.1",
        # 端口
        "PORT": 3306,
        # 编码集
        "CHARSET": "utf8",
    }
}
  • 在项目下的 init.py 中声明数据库类型
import pymysql

pymysql.install_as_MySQLdb()

【二】单表操作

【1】数据的增加

from django.test import TestCase

# Create your tests here.
import os

if __name__ == "__main__":
    # 这段代码来自你项目中的 manage.py 文件,每一个项目都是不一样的!
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "day07.settings")

    import django

    django.setup()

    # 在下面书写我们需要测试的代码
    # 即所有的测试代码都必须等待环境准备完毕之后才能书写
    from app01 import models

    # (1)增加数据
    # register_time (1)支持自己指定传值
    res = models.User.objects.create(name="dream", age=18, register_time='2023-06-09')
    # 返回值为对象本身
    print(res)  # User object
    
    # register_time (2)支持传入日期对象
    import datetime
    # 生成一个当前的时间对象
    c_time = datetime.datetime.now()
    user_obj = models.User.objects.create(name="chimeng", age=18, register_time=c_time)

【2】数据的删除

# pk : 自动查找到当前表的主键字段,指代的就是当前表的主键字段
# 使用 pk 后不需要知道当前表的主键字段 , 他会自动帮我们查找并匹配

(1)查询后直接删除

res = models.User.objects.filter(pk=4).delete()
print(res) # (1, {'app01.User': 1})

(2)先查询再删除

# 拿到当前用户对象
user_obj = models.User.objects.filter(pk=1).first()
# 利用对象的方法进行删除
user_obj.delete()

【3】数据的更改

(1)查询后直接修改

res = models.User.objects.filter(pk=5).update(name="mengmeng")
print(res)  # 1

(2)先查询再修改

# 返回的就是当前的数据对象
# 不推荐使用 : 如果查询的数据不存在会直接报错 ,fileter不会
user_onj = models.User.objects.get(pk=5)
# 调用对象更改数据
user_onj.name = "xiaomeng"
user_onj.save()

【4】数据的查询

(1)查询全部:all

user_obj = models.User.objects.all()
print(user_obj) # <QuerySet [<User: User object (1)>, <User: User object (2)>]>

(2)按指定条件过滤:filter和get

  • 方式一:filter
user_obj = models.User.objects.filter(age=18)
# 筛选后得到的对象是一个 QuerySet 对象,里面会有所有符合添加的数据对象
print(user_obj) # <QuerySet [<User: User object (1)>, <User: User object (2)>]>

# 方法补充
# (1)获取到当前 QuerySet 对象的第一个对象
print(user_obj.first())
# User object (1)
print(user_obj.last())
# User object (2)
  • 方式二:get
user_obj = models.User.objects.get(pk=1)
# 获取到的是一个数据对象,只能获取到唯一条件的数据对象
print(user_obj) # User object (1)

# 如果有多个符合条件的数据会报错
# app01.models.User.MultipleObjectsReturned: get() returned more than one User -- it returned 2!
# 如果有不符合条件的数据会报错
# app01.models.User.DoesNotExist: User matching query does not exist.

【5】获取到指定字段的数据

(1)获取到一个字段的数据:values

  • 返回的数据格式为列表套字典 - 本质上是一个 QuerySet 对象 ,而不是真的列表
user_obj = models.User.objects.values("name")
print(user_obj)
# <QuerySet [{'name': 'dream'}, {'name': 'chimeng'}]>

(2)获取到多个字段的数据:values_list

  • 返回的数据格式为列表套元祖 - 本质上是一个 QuerySet 对象 ,而不是真的列表
user_obj = models.User.objects.values_list("name","age")
print(user_obj) 
# <QuerySet [('dream', 18), ('chimeng', 18)]>

【6】去重

  • 去重(带有主键就意味着数据存在不一样的地方,所以一定要去除主键后再去重)
  • distinct 方法在主键存在时,无法对含有不同主键的相同数据进行去重
  • 但是我们可以通过拿到指定字段的数据后,对筛选出的数据进行去重
user_obj = models.User.objects.values('name', 'age').distinct()
print(user_obj)
# <QuerySet [{'name': 'dream', 'age': 18}, {'name': 'chimeng', 'age': 18}]>

【7】排序

(1)默认升序

user_obj = models.User.objects.order_by('age')
print(user_obj)
# <QuerySet [<User: User object (1)>, <User: User object (2)>, <User: User object (3)>, <User: User object (4)>]>

(2)降序

user_obj = models.User.objects.order_by('-age')
print(user_obj)
# <QuerySet [<User: User object (4)>, <User: User object (3)>, <User: User object (1)>, <User: User object (2)>]>

(3)反转

  • 反转的前提是数据已经经过排序过的数据

  • 只能对有序的数据进行反转

user_obj = models.User.objects.order_by('age').reverse()
print(user_obj)
# <QuerySet [<User: User object (4)>, <User: User object (3)>, <User: User object (1)>, <User: User object (2)>]>

【8】统计个数

user_obj = models.User.objects.count()
print(user_obj)
# 4

【9】剔除结果

  • 排出在外
  • 将某个数据排出在结果之外
user_obj = models.User.objects.exclude(name="dream")
print(user_obj)
# res = models.User.objects.exclude(user="dream")

【10】是否存在

  • 是否存在
  • 返回布尔值
  • 用处不大,因为数据本身就有布尔值的状态
user_obj = models.User.objects.filter(name="dream").exists()
print(user_obj)
# True

【总结】必知必会十三条

# 以下方法的前提均为 models.模型表名.objects 之后的方法

# 【0】新增数据
.create(字段名=字段值)
# 【1】查询全部数据
.all()
# 【2】带有筛选条件的过滤
.filter(筛选字段名=筛选字段值)
# 【3】获取筛选结果的第一条数据
.filter(筛选字段名=筛选字段值).first()
# 【4】获取筛选结果的最后一条数据
.filter(筛选字段名=筛选字段值).last()
# 【4.1】获取筛选后的结果进行修改
.filter(筛选字段名=筛选字段值).update(字段名=字段值)
# 【4.2】获取筛选后的结果进行删除
.filter(筛选字段名=筛选字段值).delete()
# 【5】根据指定条件获取数据对象,条件不存在或者数据大于2则会报错
.get(筛选字段名=筛选字段值)
# 【6】获取单个字段名所对应的数据
.values(字段名)
# 【7】获取多个字段名所对应的数据
.value_list(字段名1,字段名2)
# 【8】去重,拿到指定字段的数据后,对筛选出的数据进行去重
.values(字段名1,字段名2).distinct()
# 【9】排序
# 正序
.order_by(字段名)
# 倒序
.order_by(-字段名)
# 【10】反转的已经经过排序过的数据
.order_by(字段名).reverse()
# 【11】统计当前数据对象的个数
.count()
# 【12】排除指定条件的数据
.exclude(字段名=字段值)
# 【13】判断符合当前条件的数据是否存在
.filter(筛选字段名=筛选字段值).exists()

# 补充:查看当前ORM语句的SQL查询语句
# 注意可以使用此方法的必须是 QuerySet 对象
.query

【三】神奇的双下划线查询

【1】条件大于

  • 年龄大于35岁的数据
res = models.User.objects.filter(age__gt=35)
print(res)

【2】条件小于

  • 年龄小于35岁的数据
res = models.User.objects.filter(age__lt=35)
print(res)

【3】条件大于等于

年龄大于等于35岁的数据

res = models.User.objects.filter(age__gte=35)
print(res)

【4】条件小于等于

  • 年龄小于等于35岁的数据
res = models.User.objects.filter(age__lte=35)
print(res)

【5】或条件

  • 年龄是18或者32或者40
res = models.User.objects.filter(age__in=(18, 32, 40))
print(res)

【6】两个条件之间

  • 年龄是18-40之间
  • 首尾都要
res = models.User.objects.filter(age__range=(18, 40))
print(res)

【7】模糊查询

  • 查询出名字中含有 n 的数据 -- 模糊查询
res = models.User.objects.filter(name__contains='n')
print(res)
  • 默认区分大小写
res = models.User.objects.filter(name__contains='N')
print(res)
  • 忽略大小写
res = models.User.objects.filter(name__icontains='N')
print(res)

【8】以指定条件开头/结尾

  • 以什么开头/结尾
res = models.User.objects.filter(name__startswith='d')
print(res)
  • 以什么结尾
res = models.User.objects.filter(name__endswith='m')
print(res)

【9】查询时间日期

  • 查询出注册时间是2020年1月份的数据/年/月/日
res = models.User.objects.filter(register_time__month='1')
print(res)
res = models.User.objects.filter(register_time__year='2020')
print(res)
res = models.User.objects.filter(register_time__day='28')
print(res)

【四】多表查询引入

【1】数据准备

(1)创建图书表

  • 一本书只能有一个出版社,建立外键关系为一对一
  • 一本书可以有多个作者,一个作者可以写多本书,建立外键关系为多对多
class Book(models.Model):
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    publish_date = models.DateField(auto_now_add=True)

    # 一对多
    publish = models.ForeignKey(to='Publish')
    # 多对多
    authors = models.ManyToManyField(to='Author')

(2)创建出版社表

  • 出版社不需要额外的外键关系
class Publish(models.Model):
    name = models.CharField(max_length=32)
    addr = models.CharField(max_length=64)
    # 该字段不是给models看的,而是给校验行组件使用的
    email = models.EmailField()

(3)创建作者表

  • 作者具有作者详情,所以作者和作者详情表是一对一关系
class Author(models.Model):
    name = models.CharField(max_length=32)
    age = models.IntegerField()

    # 一对一
    author_detail = models.OneToOneField(to='AuthDetail')

(4)创建作者详情表

class AuthDetail(models.Model):
    phone = models.BigIntegerField()
    addr = models.CharField(max_length=40)

(5)迁移数据库

# 生成迁移记录
python manage.py makemigrations

# 迁移记录对数据库生效
python manage.py migrate

【2】外键的增删改查

(1)一对多外键的增删改查

1.1 外键的增加

# (1)一对多的外键的增删改查
# (1.1)外键的增加 - 直接写实际字段
models.Book.objects.create(title="三国演义", price=1369.25, publish_id=1)
# (1.2)外键的增加 - 虚拟字段
publish_obj = models.Publish.objects.filter(pk=2).first()
models.Book.objects.create(title="红楼梦", price=1569.25, publish=publish_obj)

1.2 外键的删除

# (2)一对多的外键的删除
models.Publish.objects.filter(pk=1).delete()

1.3 外键的修改

# (2)一对多的外键的修改
#  - 直接写实际字段
models.Book.objects.filter(pk=1).update(publish_id=2)
#  - 虚拟字段
publish_obj = models.Publish.objects.filter(pk=1).first()

models.Book.objects.filter(pk=1).update(publish=publish_obj)

(2)多对多外键的增删改查

  • 多对多 增删改查 就是在操作第三张表

2.1 增加

# 多对多外键的增删改查 - 就是在操作第三张表
# (1)如何给书籍添加作者
book_obj = models.Book.objects.filter(pk=1).first()
# book_obj.authors - 这样我们就已经能操作第三张关系表了
# 书籍ID为1的书籍绑定了一个主键为1的作者
book_obj.authors.add(1)
# 可以传多个参数
book_obj.authors.add(2,3)
# 支持参数传对象 - 且支持多个对象
book_obj.authors.add(author_obj)

2.2 删除

  • 支持多个参数/支持对象
# (2)删除
book_obj = models.Book.objects.filter(pk=1).first()
# 支持多个参数 - 支持多个对象
book_obj.authors.remove(2)

2.3 更改

  • 先删除后增加
# (3)修改
book_obj = models.Book.objects.filter(pk=1).first()
# 括号内必须给一个可迭代对象
# 把 1 删掉 替换成 2
book_obj.authors.set([1, 2])
# 把原来都删掉 , 替换成 3
book_obj.authors.set([3])
# 支持放对象
book_obj.authors.set([author_obj])

2.4 清空

# (4) 清空
# 在第三张表中清除某一本书和作者的绑定关系
book_obj = models.Book.objects.filter(pk=1).first()
# 不要加任何参数
book_obj.authors.clear()

【补充】正反向的概念

(1)正向

  • 外键字段在我手上,那么我查你就是正向
  • book >>>> 外键字段在书这边(正向) >>>> 出版社

(2)反向

  • 外键字段不在我手上,那么我查你就是反向

  • 出版社>>>> 外键字段在书这边(反向) >>>> book

  • 一对一和一对多的判断也是这样

(3)查询方法

  • 正向查询按字段
  • 反向查询按表明名(小写),__set

【五】多表查询案例

【1】子查询(基于对象的跨表查询)

(1)查询书籍主键为1的出版社

book_obj = models.Book.objects.filter(pk=1).first()
# 书查出版社 - 正向 - 按字段查
res = book_obj.publish
print(res)  # Publish object
print(res.name)  # 东方出版社
print(res.addr)  # 东方

(2)查询书籍主键为2的作者

book_obj = models.Book.objects.filter(pk=1).first()
# 书查作者 - 正向查询按字段
res = book_obj.authors
print(res)  # app01.Author.None
# 列表中存放的是作者对象
print(res.all())  # <QuerySet [<Author: Author object>]>

(3)查询作者 的 电话号码

author_obj = models.Author.objects.filter(name="dream").first()
# 作者查询作者详情 - 正向查询按字段
res = author_obj.author_detail
print(res)  # AuthDetail object
print(res.phone)  # 110
print(res.addr)  # 山东

'''
    在书写ORM语句的时候跟写SQL语句一样的
    不要企图一次性将ORM语句写完,如果比较复杂,需要写一些看一些

    正向 什么时候需要加 .all()
        当查询返回的结果是多个的时候就需要用 .all()
        当查询的结果只有一个的时候就不需要加
    '''

(4)查询出版社是东方出版社出版的书

# 先拿到出版社对象
publish_obj = models.Publish.objects.filter(name="东方出版社").first()
# 出版社查书 - 主键字段在书 - 反向查询
res = publish_obj.book_set.all()
# publish_obj.book_set
# print(res) # app01.Book.None
# publish_obj.book_set.all()
print(res)  # <QuerySet [<Book: Book object>, <Book: Book object>, <Book: Book object>]>

(5)查询作者是dream写过的书

# 先拿到作者对象
author_obj = models.Author.objects.filter(name="dream").first()
# 作者查书 - 主键在书 - 反向
res = author_obj.book_set.all()
print(res)  # <QuerySet [<Book: Book object>]>

(6)查询手机号是 110的作者姓名

# 先拿到作者详情的对象
author_detail_obj = models.AuthDetail.objects.filter(phone=110).first()
# 详情查作者 - 主键在作者 - 反向
res = author_detail_obj.author
print(res)  # Author object
print(res.name)  # dream

(7)小结

  • 基于对象 - 反向查询
  • 什么时候需要加 _set.all()
    • 查询结果是多个的时候需要加
    • 查询结果是多个的时候需要加

【补充】_set.all()(反向查询)

  • 查询结果是多个的时候需要加
  • 查询结果是多个的时候需要加

【2】联表查询(基于双下划线的跨表查询)

(1)查询dream的手机号和作者的姓名

  • 正向:先查询到作者信息再 .value(需要查询信息的表__需要查询的字段,其他字段)
res = models.Author.objects.filter(name="dream").values('author_detail__phone', 'name')
print(res)  # <QuerySet [{'author_detail__phone': 110, 'name': 'dream'}]>
  • 反向:先拿到详情,再用作者详情关联作者表,通过 __字段的方法 过滤出我们想要的指定数据
res = models.AuthDetail.objects.filter(author__name="dream").values('phone', 'author__name')
# AuthDetail.objects.filter(author__name="dream")
print(res)  # <QuerySet [<AuthDetail: AuthDetail object>]>
# AuthDetail.objects.filter(author__name="dream").values('phone','author__name')
print(res)  # <QuerySet [{'phone': 110, 'author__name': 'dream'}]>

(2)查询书籍主键ID为1的出版社名字和书的名字

  • 正向:先过滤出书籍ID为1的书籍对象,再去关联出版者表,利用__字段取值
res = models.Book.objects.filter(pk=1).values('title', 'publish__name')
print(res)  # <QuerySet [{'title': '三国演义', 'publish__name': '东方出版社'}]>
  • 反向:先查询到指定出版社,再从出版社反向找到书籍名字
res = models.Publish.objects.filter(book__id=1).values('name', 'book__title')
print(res)  # <QuerySet [{'name': '东方出版社', 'book__title': '三国演义'}]>

(3)查询书籍主键ID为1的作者姓名

  • 正向:先拿到 书籍主键ID为1的对象,再关联作者信息表,通过__字段取值
res = models.Book.objects.filter(pk=1).values('authors__name')
print(res)  # <QuerySet [{'authors__name': 'dream'}]>
  • 反向 : 先拿到 书籍ID为1的作者数据再去取作者的名字
res = models.Author.objects.filter(book__id=1).values('name')
print(res)  # <QuerySet [{'name': 'dream'}]>

(4)查询书籍主键是1的作者的手机号

# book author authordetail
res = models.Book.objects.filter(pk=1).values('authors__author_detail__phone')
print(res)  # <QuerySet [{'authors__author_detail__phone': 110}]>

(5)小结

  • 只要掌握了正反向的概念
  • 以及双下划线查询
  • 就可以无限跨表

【六】聚合查询

【1】引入

  • 聚合查询通常情况下都是配合分组一起使用的
  • 只要是和数据库相关的模块
  • 基本上都在 django.db.models 里面
  • 如果这里面没有 那大概率可能在 django.db 里面

【2】案例

  • 导入聚合函数
from django.db.models import Max, Min, Sum, Count, Avg

(1)所有书的平均价格

  • 正常情况下,我们是需要 先进行分组再进行 聚合函数运算的
  • 但是Django给我们提供了一种方法 : aggregate 可以不分组进行某个字段的聚合函数
res = models.Book.objects.aggregate(Avg('price'))
print(res)  # {'price__avg': 1890.083333}

(2)一次性使用

res = models.Book.objects.aggregate(Avg('price'), Max('price'), Min('price'), Sum('price'), Count('pk'))
print(res) # {'price__avg': 1890.083333, 'price__max': Decimal('5959.25'), 'price__min': Decimal('555.25'), 'price__sum': Decimal('11340.50'), 'pk__count': 6}

【七】分组查询

【1】引入

  • MySQL中的分组查询
  • 分组之后只能获取到分组的依据,组内其他字段都无法获取
  • 严格模式中可以修改

【2】语法

  • models 后面跟的是什么,就是按什么分组
res = models.Book.objects.annotate()

【3】案例

(1)统计每一本书的作者个数

# author_number 是我们自己定义的字段,用来存储统计出来的每本书的作者个数
res = models.Book.objects.annotate(author_number=Count('authors')).values('title','author_number')
print(res) # <QuerySet [{'title': '三国演义', 'author_number': 1}, {'title': '红楼梦', 'author_number': 0}, {'title': '水浒传', 'author_number': 0}, {'title': '论语', 'author_number': 0}, {'title': '孙子兵法', 'author_number': 0}, {'title': '镇魂街', 'author_number': 0}]>
  • 等价于
res = models.Book.objects.annotate(author_number=Count('authors__pk')).values('title','author_number')
print(res) # <QuerySet [{'title': '三国演义', 'author_number': 1}, {'title': '红楼梦', 'author_number': 0}, {'title': '水浒传', 'author_number': 0}, {'title': '论语', 'author_number': 0}, {'title': '孙子兵法', 'author_number': 0}, {'title': '镇魂街', 'author_number': 0}]>

(2)统计每个出版社最便宜的书的价格

res = models.Publish.objects.annotate(min_price=Min('book__price')).values('name', 'min_price')
print(res) # <QuerySet [{'name': '东方出版社', 'min_price': Decimal('555.25')}, {'name': '北方出版社', 'min_price': Decimal('888.25')}]>

(3)统计不止一个作者的图书

# (3.1)先按照图书分组
# (3.2)过滤出不止一个作者的图书
# 我的数据有限,我统计的是大于 0 的作者的图书
res = models.Book.objects.annotate(author_num=Count('authors')).filter(author_num__gt=0).values('title','author_num')
print(res)  # <QuerySet [{'title': '三国演义', 'author_num': 1}]>
  • 补充

    • 只要ORM语句得到的是 一个 queryset 对象

    • 那么就可以继续无限制的调用封装 的方法

(4)查询每个作者出的书的总价格

res = models.Author.objects.annotate(sum_price=Sum('book__price')).values('name', 'sum_price')
print(res)  # <QuerySet [{'name': 'dream', 'sum_price': Decimal('1369.25')}, {'name': 'hope', 'sum_price': None}, {'name': 'sad', 'sum_price': None}]>

(5)小结

  • 如果想按照指定的字段分组该如何处理

  • 如果 annotate 前面没东西 则会按照 Book 分组 ,如果前面有参数 就会按照前面的参数进行分组 price

models.Book.objects.values('price').annotate()

【八】F与Q查询

【0】引入

from django.db.models import F, Q

【1】F查询

  • 能够帮助你直接获取到列表中某个字段对应的数据

注意: 在操作字符串类型的数据的时候, F不能够直接做到字符串的拼接

(1)查出卖出数大于库存数的书籍

# F 查询 : 帮助我们直接获取到表中的某个字段对应的数据
res = models.Book.objects.filter(sales__gt=F('stock'))
print(res)  # <QuerySet [<Book: 水浒传>]>

(2) 将所有书籍的价格提升 50

res = models.Book.objects.update(price=F('price') + 500)
print(res)  # 6 - 影响到了 6 条数据

(3)将所有书的名称后边加上 爆款两个字

# 在操作字符串的时候,F查询不能够直接做到字符串的拼接
from django.db.models.functions import Concat
from django.db.models import Value

res = models.Book.objects.update(title=Concat(F('title'), Value('爆款')))
print(res) # 6 - 影响到了 6 条数据

【2】Q查询

  • filter()等方法中的关键字参数查询都是一起进行"and"
  • 如果你需要执行更复杂的查询(列如ORM语句),你可以使用Q对象。

(1)查询卖出数大于100或者价格小于500的书籍

# (1.1)直接使用 filter 查询数据,逗号隔开,里面放的参数是 and 关系
res = models.Book.objects.filter(sales__gt=100, price__lt=500)
print(res)  # <QuerySet []>

# (1.2)直接使用 Q 查询数据,逗号隔开,里面放的参数还是 and 关系
res = models.Book.objects.filter(Q(sales__gt=100), Q(price__lt=500))
print(res)  # <QuerySet []>

# (1.3)直接使用 Q 查询数据,逗号可以换成其他连接符达到效果
res = models.Book.objects.filter(Q(sales__gt=100) or Q(price__lt=500))
# 二者等价 (| :或关系) ( ~ : 取反 not 关系)
res = models.Book.objects.filter(Q(sales__gt=100) | Q(price__lt=500))
print(res)  # <QuerySet [<Book: 三国演义爆款>, <Book: 水浒传爆款>, <Book: 论语爆款>, <Book: 孙子兵法爆款>]>

(2) Q的高阶用法 能够将查询条件的左边也变成 字符串形式

  • 能够将查询条件的左边也变成字符串的形式 而 不是变量形式
# 先产生一个空对象  实列化
q = Q()
q.connector = 'or'  # and修改成or
# q对象里面有一个children
q.children.append(('maichu__gt', 100))
# 第一个元素就会被当作查询条件的左边 第二个元素会被当作查询条件右边
q.children.append(('price__lt', 600))
res = models.Book.objects.filter(q)  # filter 除了可以放条件 还可以放对象
print(res)  # 默认还是and关系
  • 可以在去对象内 children里面 无限制的添加元素 添加元组,两个元素。
  • 而且还支持修改 orandnot
  • 默认是and
# 或查询
q = Q()
q |= Q(条件一)
q |= Q(条件二)
qs = Model.objects.filter(q)

# 并查询
q = Q()
q &= Q(条件一)
q &= Q(条件二)
qs = Model.objects.filter(q)

【九】Django中如何开启事务

【1】ACID特性

  • 它代表了原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

(1)原子性(Atomicity)

  • 事务被视为一个不可分割的原子操作单元。

  • 这意味着要么全部操作成功并永久保存,要么全部操作失败并回滚到事务开始前的状态,不存在部分成功或部分失败的情况。

(2)一致性(Consistency)

  • 事务在执行前后,数据库都必须保持一致状态。

  • 这意味着事务执行前后,数据库中的数据必须满足所有定义的完整性约束,例如列级别的约束、外键关系等。

(3)隔离性(Isolation)

  • 事务之间应该相互隔离,每个事务的执行应该与其他事务的执行相互独立,互不干扰。

  • 隔离性确保了多个事务可以并发执行,而不会产生不一致的结果。

(4)持久性(Durability)

  • 一旦事务成功提交后,其所做的修改将永久保存在数据库中,即使发生系统故障或重启,数据也能够恢复到提交后的状态。
  • 持久性通过将事务日志写入非易失性存储介质来实现,如硬盘驱动器或固态硬盘。

【2】事务名词

  • 开启事务:Start Transaction
  • 事务结束:End Transaction
  • 提交事务:Commit Transaction
  • 回滚事务:Rollback Transaction

【3】默认事务行为

  • Django是支持事务操作的,它的默认事务行为是自动提交
  • 具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。
  • 但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务。

【4】开启事务

(1)全局开启事务

[1]配置

  • 在Web应用中,常用的事务处理方式是将每次请求都包裹在一个事务中。
  • 全局开启事务只需要将数据库的配置项ATOMIC_REQUESTS设置为True,如下所示:
DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.mysql',
       'NAME': 'db1',
       'HOST': 'dbhost',
       'PORT': '3306',
       'USER': 'dbuser',
       'PASSWORD': 'password',
        #全局开启事务,绑定的是http请求响应整个过程
       'ATOMIC_REQUESTS': True, 
   }

[2]原理

  • 每当有请求过来时,Django会在调用视图方法前开启一个事务。
  • 如果完成了请求处理并正确返回了结果,Django就会提交该事务。
  • 否则,Django会回滚该事务。

[3]局部取消事务

  • 如果你全局开启了事务,你仍然可以使用non_atomic_requests装饰器让某些视图方法不受事务控制
from django.db import transaction

 
@transaction.non_atomic_requests
def my_view(request):
    do_stuff()
 
 
# 如有多个数据库,让使用otherdb的视图不受事务控制
@transaction.non_atomic_requests(using='otherdb')
def my_other_view(request):
    do_stuff_on_the_other_database()
  • 虽然全局开启事务很简单,但Django并不推荐开启全局事务。
  • 因为一旦将事务跟 HTTP 请求绑定到一起时,每一个请求都会开启事务,当访问量增长到一定的时候会造成很大的性能损耗。
  • 在实际开发过程中,很多GET请求根本不涉及到事务操作,一个更好的方式是局部开启事务按需使用。

(2)局部开启事务

  • Django项目中局部开启事务,可以借助于transaction.atomic方法。
  • 使用它我们就可以创建一个具备原子性的代码块,一旦代码块正常运行完毕,所有的修改会被提交到数据库。
  • 反之,如果有异常,更改会被回滚。

[1]装饰器使用

  • atomic经常被当做装饰器来使用,如下所示:
# 案例一:函数视图
 from django.db import transaction
 
 
 @transaction.atomic
 def viewfunc(request):
     # This code executes inside a transaction.
     do_stuff()
 
 
 # 案例二:基于类的视图
 from django.db import transaction
 from rest_framework.views import APIView
 
 
 class OrderAPIView(APIView):
       # 开启事务,当方法执行完以后,自动提交事务
       @transaction.atomic  
       def post(self, request):
           pass 

[2]局部代码

  • 使用了atomic装饰器,整个视图方法里的代码块都会包裹着一个事务中运行。
  • 有时我们希望只对视图方法里一小段代码使用事务,这时可以使用transaction.atomic()显式地开启事务,如下所示:
 from django.db import transaction
 
 
 def viewfunc(request):
     # 默认自动提交
     do_stuff()
       
     # 显式地开启事务
     with transaction.atomic():
         # 下面这段代码在事务中执行
         do_more_stuff()

【5】Savepoint回滚

  • 在事务操作中,我们还会经常显式地设置保存点(savepoint)。
  • 一旦发生异常或错误,我们使用savepoint_rollback方法让程序回滚到指定的保存点。
  • 如果没有问题,就使用savepoint_commit方法提交事务。
def viewfunc(request):
   # 默认自动提交
   do_stuff()

   # 显式地开启事务
   with transaction.atomic():
       # 创建事务保存点
       sid = transaction.savepoint()

       try:
           do_more_stuff()
       except Exception as e:
           # 如发生异常,回滚到指定地方。
           transaction.savepoint_rollback(sid)          
       # 如果没有异常,显式地提交一次事务
       transaction.savepoint_commit(sid)


   return HttpResponse("Success")
  • 注意:虽然SQLite支持保存点,但是sqlite3 模块设计中的缺陷使它们很难使用。

【6】事务提交后回调函数

  • 有的时候我们希望当前事务提交后立即执行额外的任务
  • 比如客户下订单后立即邮件通知卖家,这时可以使用Django提供的on_commit方法
# 例1
from django.db import transaction

def do_something():
  pass  # send a mail, invalidate a cache, fire off a Celery task, etc.


transaction.on_commit(do_something)
# 例2:调用celery异步任务
transaction.on_commit(lambda: some_celery_task.delay('arg1'))

【十】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

  • 设置要关联的表的字段

[5]on_delete

  • 当删除关联表中的数据时,当前表与其关联的行的行为。

  • on_delete=models.CASCADE

  • 删除关联数据,与之关联也删除

【12】OneToOneField

(1)介绍

  • 一对一字段。
  • 通常一对一字段用来扩展已有字段。(通俗的说就是一个人的所有信息不是放在一张表里面的,简单的信息一张表,隐私的信息另一张表,之间通过一对一外键关联)

(2)重要参数

[1]to

  • 设置要关联的表。

[2]to_field

  • 设置要关联的字段。

[3]on_delete

  • 当删除关联表中的数据时,当前表与其关联的行的行为。(参考上面的例子)

【13】字段参数

(1)null

  • 用于表示某个字段可以为空。

(2)unique

  • 如果设置为unique=True 则该字段在此表中必须是唯一的 。
Foregin(unique = True) ---->  OneToOneField

(3)db_index

  • 如果db_index=True 则代表着为此字段设置索引。

(4)default

  • 为该字段设置默认值。

【14】字段之间对应关系

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

【15】支持自定义字段

from django.db import models

# Create your models here.
#Django中没有对应的char类型字段,但是我们可以自己创建
class FixCharField(models.Field):
    '''
    自定义的char类型的字段类
    '''
    def __init__(self,max_length,*args,**kwargs):
        self.max_length=max_length
        super().__init__(max_length=max_length,*args,**kwargs)

    def db_type(self, connection):
        '''
        限定生成的数据库表字段类型char,长度为max_length指定的值
        :param connection:
        :return:
        '''
        return 'char(%s)'%self.max_length
#应用上面自定义的char类型
class Class(models.Model):
    id=models.AutoField(primary_key=True)
    title=models.CharField(max_length=32)
    class_name=FixCharField(max_length=16)
    gender_choice=((1,'男'),(2,'女'),(3,'保密'))
    gender=models.SmallIntegerField(choices=gender_choice,default=3)

【十一】数据库查询优化

【1】ORM语句的特点

  • 惰性查询:如果仅仅是书写了ORM语句,在后面没有使用到的话就不会对数据库进行查询,当我们需要用到查询的数据时,ORM会从数据库查询数据并返回数据

【2】only/defer

  • 运行这句话的时候没有SQL语句执行
models.Book.objects.all()
  • 这样运行的时候就会返回相应的数据
res = models.Book.objects.all()
print(res)  # 只有需要用到真正的数据时,才会走数据库查询数据

(1)获取所有书籍表中书的名字

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

(2)only

  • 实现获取到一个数据对象,通过 .属性 拿到值并且无其他字段
res = models.Book.objects.only('title')
print(res)
for k in res:
  print(k.title)  # .only括号内的字段不会走数据库
  print(k.price)  # only中没有的字段,会重新走数据库并逐一从数据库中查找

(3)defer

  • defer 和 only 相反
  • defer 括号内放的字段不在查询的对象里面 ,查询该字段需要重新走数据库
  • 而如果查询的是非括号内的字段 则不需要走数据库
res = models.Book.objects.defer('title')
print(res)
for s in res:
  print(res.price)

(4)小结

  • 在数据库查询中,onlydefer是两个常用的方法
  • 用于优化查询性能和减少数据传输量。

[1]only

  • only方法允许我们指定只查询我们需要的字段,而忽略其他字段。

  • 通过只选择必要的字段,可以减少数据库返回的数据量,从而提高查询速度和减少网络传输开销。

  • 在Django ORM中,可以使用.only()方法来限定查询字段。

  • 例如,假设我们有一个名为Product的模型,其中包含许多字段,但我们只对名称和价格感兴趣。

  • 我们可以使用only方法来只选择这两个字段:

products = Product.objects.only('name', 'price')

[2]defer

  • defer方法与only相反,它允许我们延迟加载某些字段,将它们从查询中排除。

  • 这在我们有一些大的、不常用的字段时非常有用,可以避免在每次查询时都加载这些字段,从而提高查询性能。

  • 在Django ORM中,可以使用.defer()方法来延迟加载字段。

  • 例如,假设我们有一个名为Product的模型,其中包含一个很大的description字段,但我们只在特定情况下使用它。

  • 我们可以使用defer方法将其从查询中排除:

products = Product.objects.defer('description')
  • 与跨表操作有关
res = models.Book.objects.all()
for i in res:
    print(i.publish.name)  # 每循环一次就要走一次数据库查询
  • 内部直接将book表和publish表的数据连起来,一次性将所有数据封装到查询出来的对象

  • 这个时候对象可以直接点去取两个变中的数据而不需走数据库

  • 参数只能是外键字段

  • 但是一对一或一对多或多对多的外键字段不行

res = models.Book.objects.select_related('publish')  # INNER JOIN:联表操作
print(res)
for i in res:
    print(i.publish.name)
  • 将子查询查询出来的所有结果也是封装到一个对象中返回
  • 但是在使用的时候给我们的感觉是一次性完成的
  • 较联表操作多了一步
res = models.Book.objects.prefetch_related('publish')  # 子查询
for i in res:
    print(i.publish.name)  # 每循环一次就要走一次数据库查询

(3)小结

  • 在数据库查询中,select_relatedprefetch_related是两个常用的方法
  • 用于解决查询中的关联对象性能问题。
  • select_related方法用于减少数据库查询次数,通过在查询时同时加载相关联的对象。
  • 例如,如果我们有一个Product模型,其中包含一个外键字段category,我们可以使用select_related方法来在查询产品时一起加载其所属的类别信息:
products = Product.objects.select_related('category')
  • 这样一来,当我们访问每个产品的category属性时,不会再触发额外的数据库查询,提高了查询效率。
  • prefetch_related方法则是用于解决一对多或多对多关系的性能问题。
  • 当我们查询包含关联对象集合(例如一个订单和其包含的商品)时,prefetch_related方法可以一次性加载所有的关联对象,避免了N+1查询问题。
orders = Order.objects.prefetch_related('products')
  • 这样一来,在访问每个订单对应的产品集合时,不会再触发额外的查询操作,大大提高了查询性能。

需要注意的是,使用select_relatedprefetch_related方法时,需谨慎选择要加载的关联对象

避免过度加载导致的性能问题,同时也要注意数据库索引的优化以提高查询效率。

【十二】choice参数(数据库字段设计常见)

【1】引入

  • 以一张信息表为例
性别
学历
工作经验
是否婚配
是否生子
客户来源
...
  • 针对某个可以列举完全的可能性字段,我们应该如何存储?

  • 只要某个字段的可能性是列举完全的,那么一般情况下都会采用choice参数

【2】数据准备

from random import choices

from django.db import models


# Create your models here.
class User(models.Model):
    username = models.CharField(max_length=32)
    age = models.IntegerField()
    # 性别
    gender_choices = (
        (1, 'male'),
        (2, 'female'),
        (3, 'other'),
    )
    score = (
        ('1', 'male'),
        ('2', 'female'),
        ('3', 'other'),
    )
    # 保证字段类型跟列举出来的元祖第一个数据类型一致即可
    gender = models.IntegerField(choices=gender_choices)

    '''
    该gender字段存的还是数字 但是如果存的数据在上面元祖列举的范围之内
    那么可以非常轻松的获取到数字对应的真正内容
    '''

1.gender字段存的数字不在上述元祖列举的范围内,会如何?
2.如过数字在 如何获取对应的中文注释?

【3】问题解决

(1)gender字段存的数字在上述元祖列举的范围内

from django.test import TestCase

# !/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "day08.settings")
    import django

    django.setup()

    from app01 import models

    models.User.objects.create(username="dream", age=18, gender=1)
    models.User.objects.create(username="mengmeng", age=28, gender=2)
    models.User.objects.create(username="chimeng", age=39, gender=3)
    # 存的时候,没有列举出来的数字也能被存进去
    models.User.objects.create(username="hope", age=44, gender=4)

没有报错,且第四条已经插入到数据库中

(2)gender字段存的数字在上述元祖列举的范围内并获取

# 取 - 如果有对应关系
user_obj = models.User.objects.filter(pk=1).first()
print(user_obj.gender)  # 1
# 只要是 choice字段的注释,如果想要获取到注释的信息,固定写法 get_字段名_display()
print(user_obj.get_gender_display())  # male

(3)gender字段存的数字不在上述元祖列举的范围内

# 取 - 如果没有对应关系
user_obj = models.User.objects.filter(pk=4).first()
# 如果没有对应关系 字段是什么 返回的就是什么
print(user_obj.get_gender_display())  # 4

【4】总结

  • choice参数使用场景非常广泛

  • 例如

    • 支付方式的选择
    • 生源的来源地
    • 分数的分类
    • 学历的分类
    • ...

【十三】MTV与MVC模型

  • MTV模型和MVC模型是两种常见的软件设计模式,用于组织和管理用户界面和应用程序的逻辑。
  • 虽然它们存在一些相似之处,但它们在设计和应用上有一些不同。

【1】MTV模型:

MTV模型是指Model-Template-View(模型-模板-视图)模型,是Django框架中采用的一种设计模式。它的核心思想是将应用程序分为三个主要部分:

  • 模型(Model):

    • 模型表示应用程序中处理数据的结构和行为。
    • 它通常与数据库交互,并定义了数据的存储和操作方式。
  • 模板(Template):

    • 模板负责处理用户界面的显示。
    • 它定义了应用程序的外观和布局,并将动态数据与静态页面结合在一起,生成最终的用户界面。
  • 视图(View):

    • 视图处理应用程序的逻辑和业务流程。
    • 它接收用户的请求,从模型中获取数据,将数据传递给模板进行渲染,并生成响应返回给用户。

MTV模型的优点在于它可以很好地将应用程序的逻辑和用户界面进行分离,使代码更容易维护和扩展。

【2】MVC模型:

MVC模型是指Model-View-Controller(模型-视图-控制器)模型,是一种常见的软件设计模式,广泛应用于Web开发和其他应用程序中。

  • 模型(Model):

    • 模型负责处理应用程序的数据逻辑。
    • 它包含了数据的存储和操作方式,并定义了数据在应用程序内部如何交互和被操作。
  • 视图(View):

    • 视图是用户界面的表示,负责展示数据给用户并接收用户的输入操作。
    • 它通常从模型中获取数据,并将其显示给用户。
  • 控制器(Controller):

    • 控制器处理用户的交互和请求,并根据用户的行为作出相应的响应。
    • 它接收用户的输入,并更新模型和视图以反映用户的操作。

MVC模型的优点在于它可以很好地分离应用程序的不同组件,使得代码更易于维护、测试和重用。

【3】总结:

  • MTV模型主要用于Django框架中,通过将应用程序分为模型、模板和视图,提供了一种清晰的架构方案。
  • MVC模型则是一个通用的设计模式,广泛应用于各种类型的应用程序中。
  • 无论使用哪种模型,都能帮助开发者更好地组织和管理代码,并实现可扩展和可维护的应用程序。

【十四】多对多三种创建方式

【1】全自动

  • 利用ORM自动帮我们创建第三张表关系
class Book(models.Model):
    name = models.CharField(max_length=32)
    # 全自动
    authors = models.ManyToManyField(to='Author')


class Author(models.Model):
    name = models.CharField(max_length=32)
  • 优点
    • 代码不需要自己写,非常方便,还支持ORM提供操作第三张表的方法
  • 缺点
    • 第三张关系表的扩展性极差(没办法添加额外字段)

【2】纯手动(不建议使用)

class Book(models.Model):
    name = models.CharField(max_length=32)

class Author(models.Model):
    name = models.CharField(max_length=32)

class BookAuthor(models.Model):
    book_id = models.ForeignKey(to='Book')
    author_id = models.ForeignKey(to='Author')
  • 优点
    • 第三张表完全取决于自己进行额外的拓展
  • 缺点
    • 需要写代码较多
    • 不能使用ORM提供的相关方法

【3】半自动

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_id = models.ForeignKey(to='Book')
    author_id = models.ForeignKey(to='Author')
  • 这样虽然可以使用ORM的正反向查询,但是没法使用add,set,remove,clear这四个方法

【补充】查看内部SQL语句的方式

【1】方式一

  • 只有queryset对象才能使用该方法
res_list = models.User.objects.values_list('name', 'age')
# 返回的数据格式为列表套元祖 - 本质上是一个 QuerySet 对象 ,而不是真的列表
print(res_list)  # <QuerySet [('xiaomeng', 18)]>
# 该语句可以查看当前执行命令的 SQL 语句 - 只有queryset对象才能使用该方法
print(res_list.query)  # SELECT `app01_user`.`name`, `app01_user`.`age` FROM `app01_user`

【2】方式二

  • 所有 SQL语句 都可以使用

  • 在项目的settings.py文件中增加默认配置

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level':'DEBUG',
        },
    }
}

posted @ 2024-03-18 22:59  Chimengmeng  阅读(21)  评论(0编辑  收藏  举报