60、Django模型层

Django框架 / 2021-02-25

模型层即models.py文件,它负责Django与数据库的交互,我们曾在Django数据库交互初步学习过如何操作Django与数据库进行交互,此次为上次学习的补充。

此篇博文篇幅较长,请耐心阅读。部分内容参考博客园-JsonJi,原文地址:https://www.cnblogs.com/Dominic-Ji/p/9203990.html

一、单表查询

1.1、models补充

在DateTimeField字段中,有两个非常重要的参数:auto_now、auto_now_add,二者不可混用,以下仅为示意。

register_time = models.DateTimeField(auto_now=True, auto_now_add=True)
auto_now

每次操作数据的时候,该字段会自动将当前时间录入,多用于如文章最后更新时间。

auto_now_add

在创建数据时,自动将当前时间记录下来,之后只要不是人为修改就不会变。

1.2、测试脚本

之前我们每次操作数据库时都要在models模块书写代码并执行makemigration和migrate两条命令,这样对于开发过程来说挺麻烦的;当我们只是想测试Django中的某一个py文件内容,那么你可以不用书写前后端交互的形式,直接写一个测试脚本即可,一般都是在test.py文件进行操作。

1.2.1、测试环境的准备

先去manage.py将以下代码拷贝到test.py

import os


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

接着还需要手动写一些代码,然后在main下面就可以测试django里面的单个py文件了

import os


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

1.3、常见的查询方法

查询主键

filter里面用pk=2,就会自动去表里面查询主键字段ID号为2的数据,而无需关心这个主键字段究竟是uid还是id还是别的名称。

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', "djangoProject.settings")
    import django
    django.setup()
    from study import models
    user_obj = models.User.objects.filter(pk=2).delete()
    print(user_obj)
    
# 返回结果
(1, {'study.User': 1})  # 返回操作所影响的行数,即删除了1行。
查看内部SQL语句

1、结果.query查看内部封装的SQL,这种方式只能用于queryset对象,只有queryset对象才能用query查看内部的SQL语句。

user_obj = models.User.objects.values_list('username')
print(user_obj.query)

# 执行结果
SELECT `study_user`.`username` FROM `study_user`

2、在settings.py中配置如下代码后,执行任何对数据库的操作都会自动将SQL打印在pycharm终端上。

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

已经学过的就不再重复了

all

查询所有数据,已学。

filter

筛选数据,已学。

get

类似filter,它是直接拿数据对象,但是条件不满足直接报错,所以还是推荐用filter然后再用all或者first取。

user_obj = models.User.objects.get(pk=3)
print(user_obj.pk)
first

拿queryset里面第一个元素,已学。

last

拿queryset里面最后一个元素,和first相反,就不演示了。

values

用列表套字典形式来获取指定字段的数据

user_obj = models.User.objects.values('username')  # 相当于select username from user;
print(user_obj)
# z执行结果
<QuerySet [{'username': 'sanxi'}, {'username': 'kristal'}, {'username': 'teacher'}]>
values_list

列表套元祖,也是获取指定字段的数据,跟values没什么区别。

user_obj = models.User.objects.values_list('username')
print(user_obj)
distinct

去重,以前MySQL的学习过程中我们知道有主键的情况下是无法去重的,要用values取特定字段的值再去重。

我先添加重复数据

image-20210222173526438

接着开始去重

user_obj = models.User.objects.values('username', 'password')
print(user_obj.distinct())

# 执行结果
<QuerySet [{'username': 'sanxi', 'password': 123}, {'username': 'kristal', 'password': 666}, {'username': 'teacher', 'password': 777}]>
order_by

按指定字段排序,默认升序、

user_obj = models.User.objects.order_by('password')
print(user_obj.first().password)

# 执行结果
123

降序就直接在前面加个-减号、

user_obj = models.User.objects.order_by('-password')
print(user_obj.first().password)

# 执行结果
777
reverse

反转逆向,前提是数据已经排序完毕,如果是混乱的不行,没什么用

count

统计当前数据的个数,有用。

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

# 执行结果
3
exclude

排除指定条件的数据在外

user_obj = models.User.objects.exclude(username='sanxi')
exists

是否存在,基本用不到,因为数据自带布尔值。

user_obj = models.User.objects.filter(username='andy').exists()
print(user_obj)

# 执行结果
False

1.4、神奇的双下划线查询

记得要在这些双下划线前面加上字段,如username__startswith='s',日期也可以和大于等于混合使用,如

__gt=1大于1
__lt=1小于1
__in=[11,22,33]11,22,33其中一个
__contains='sanxi'包含'sanxi'
__icontains='sanxi'忽略大小写
__range=[1,10]范围1-10
__startswith='j'j开头的
__istartswith='J'j开头的,且忽略大小写
__endswith='j'j结尾的,且忽略大小写
__iendswith='j'j结尾的,且忽略大小写
register_date=(2020, 2, 22)日期是2020年2月22号的
register_date__day=22日期是22号的
register_date__month=2日期是2月的
register_date__week_day=2日期是星期二的
register_date__year=2020年份是2020

date双下划线,可以通过后面添加month、year、day、week_day等等获取特定数据,也可以在后面跟上大于小于等于。

二、多表操作

2.1、表结构与数据准备

建立表关系

学习此小节需要提前弄明白表之间的关系。

from django.db import models

# Create your models here.


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


class Book(models.Model):
    book_id = models.AutoField(primary_key=True, verbose_name='pk')
    book_name = models.CharField(max_length=32, verbose_name='book')

    book_author = models.ManyToManyField(to='Authors')  # 书籍与作者为多对多关系

    book_price = models.IntegerField()
    register_date = models.DateField(auto_now_add=True)

    book_publish = models.ForeignKey(to='PublishingHouse', to_field='id')  # 书籍与出版社为一对多关系


class Authors(models.Model):
    name = models.CharField(max_length=32)
    author_detail = models.OneToOneField(to='AuthorDetail', null=True)  # 作者与作者详情为一对一关系


class AuthorDetail(models.Model):
    phone = models.BigIntegerField()
录入数据
书籍表
MariaDB [book_system]> SELECT * FROM study_book;
+---------+------------+------------+---------------+-----------------+
| book_id | book_name  | book_price | register_date | book_publish_id |
+---------+------------+------------+---------------+-----------------+
|      11 | 三体       |         35 | 2006-05-01    |               1 |
|      12 | 球状闪电   |         25 | 2005-02-01    |               2 |
|      13 | 超新星纪元 |         30 | 2014-02-02    |               3 |
|      14 | 骆驼祥子   |         18 | 1936-08-04    |               2 |
|      15 | 四世同堂   |         25 | 2017-10-03    |               3 |
|      16 | 今村异闻录 |          0 | 2017-07-15    |               4 |
|      17 | 津门除妖记 |          0 | 2019-10-04    |               4 |
|      18 | 天津姑娘   |          0 | 2019-10-06    |               1 |
+---------+------------+------------+---------------+-----------------+
8 rows in set (0.004 sec)
作者表
MariaDB [book_system]> SELECT * FROM study_authors;
+----+--------+------------------+
| id | name   | author_detail_id |
+----+--------+------------------+
|  1 | 刘慈欣 |                1 |
|  2 | 老舍   |                2 |
|  4 | 三溪   |                3 |
+----+--------+------------------+
3 rows in set (0.000 sec)
作者详情表
MariaDB [book_system]> SELECT * FROM study_authordetail;
+----+-------+
| id | phone |
+----+-------+
|  1 |   110 |
|  2 |   120 |
|  3 |   119 |
+----+-------+
3 rows in set (0.000 sec)
出版社表
MariaDB [book_system]> SELECT * FROM study_publishinghouse;
+----+------------+
| id | name       |
+----+------------+
|  1 | 人民出版社 |
|  2 | 北京出版社 |
|  3 | 上海出版社 |
|  4 | 未出版     |
+----+------------+
4 rows in set (0.000 sec)
书籍-作者外键表
MariaDB [book_system]> SELECT * FROM study_book_book_author;
+----+---------+------------+
| id | book_id | authors_id |
+----+---------+------------+
|  1 |      11 |          1 |
|  2 |      12 |          1 |
|  3 |      13 |          1 |
|  4 |      14 |          2 |
|  5 |      15 |          2 |
|  6 |      16 |          4 |
|  7 |      17 |          4 |
|  9 |      18 |          4 |
+----+---------+------------+
8 rows in set (0.000 sec)

2.2、外键字段的增删改查

以下所有实验均在test.py环境下执行

2.2.1、一对多增删改查

create方法直接写实际字段

data_obj = models.Book.objects.create(book_name='再见燕子', book_price=0, book_publish_id=4)
MariaDB [book_system]> SELECT * FROM study_book WHERE book_name='再见燕子';
+---------+-----------+------------+---------------+-----------------+
| book_id | book_name | book_price | register_date | book_publish_id |
+---------+-----------+------------+---------------+-----------------+
|      19 | 再见燕子  |          0 | 2021-02-23    |               4 |
+---------+-----------+------------+---------------+-----------------+
1 row in set (0.000 sec)

delete()方法在Django 1.X版本默认级联更新级联删除;删除后再去查询显示为空了

data_obj = models.Book.objects.filter(book_name='天津姑娘').delete()
MariaDB [book_system]> SELECT book_name FROM study_book WHERE book_name='天津姑娘';
Empty set (0.000 sec)

MariaDB [book_system]> SELECT book_id FROM study_book_book_author WHERE book_id='18';
Empty set (0.003 sec)

update(),同样级联更新至有关系的表

data_obj = models.Book.objects.filter(book_id=17).update(book_name='津门除妖记', book_publish_id=4)
2.2.2、多对多增删改查

多对多关系的增删查改就是在操作第三张表!那么如何给书籍添加作者?作者表不是它创建的,所以它无法通过方法创建,需要通过外键字段到达第三张表,这也是Django给我们提供的非常方便地方法,只需要点点点!

首先,我们先添加书籍基本信息;接着通过点书籍对象的外键book_authors到达第三张关系表,然后用Django提供的add()方法,为书籍id为20的书籍绑定一个主键为1的作者。

所以,add是专门给第三张关系表添加数据的,括号内可以传数字也可以传对象,并且支持多个。

book_obj = models.Book.objects.create(book_name='流浪地球', book_price=33, book_publish_id=2)
book_obj.book_author.add(1)
MariaDB [book_system]> SELECT * FROM study_book WHERE book_name='流浪地球';

+---------+-----------+------------+---------------+-----------------+
| book_id | book_name | book_price | register_date | book_publish_id |
+---------+-----------+------------+---------------+-----------------+
|      20 | 流浪地球  |         33 | 2021-02-24    |               2 |
+---------+-----------+------------+---------------+-----------------+
1 row in set (0.015 sec)

MariaDB [book_system]> SELECT * FROM study_book_book_author WHERE book_id=20;
+----+---------+------------+
| id | book_id | authors_id |
+----+---------+------------+
| 10 |      20 |          1 |
+----+---------+------------+
1 row in set (0.000 sec)

remove(),跟add用法一样,括号内可以传数字也可以传对象,并且支持多个;以下因为是删除书籍的作者,所以并不会删除书籍,因为书籍表里没有字段不会触发级联删除

book_obj = models.Book.objects.filter(book_id=23).first()
book_obj.book_author.remove(1)

set,括号内必须是一个可迭代对象,也支持多个,覆盖新增;刚刚我们把流浪地球的作者给删了,用set加回去。

book_obj = models.Book.objects.filter(book_id=23).first()
book_obj.book_author.set([1])  # 如果已存在则需要先写旧,再写新;比如原来id是1,改成3,是set([1, 3])
清空

clear括号内不能加参数,可以在第三张关系表中清空某个书籍与作者绑定的关系,比remove更加激进。

book_obj = models.Book.objects.filter(book_id=23).first()
book_obj.book_author.clear()

三、跨表查询(重点)

口诀:正向查询按字段,反向查询按表名小写加_set_

3.1、正反向基本概念

正向

外键字段在哪张表,那张表就是正向;比如书籍表和出版社表,外键字段在书籍表,那么从书籍表去查出版社就是正向!

反向

与正向相反;比如出版社表查书籍就是反向。

3.2、多表查询

3.2.1、正向子查询

跟MySQL一样,根据上一条语句的查询结果再次进行查询筛选;在Django叫基于对象的跨表查询。

查询主键为11的书籍的出版社名称
book_obj = models.Book.objects.filter(book_id=11).first()
print(book_obj.book_publish.name)
# 执行结果
人民出版社
查询主键为12的书籍的作者

如果发现查询结果是None,可能是因为少些了all方法

book_obj = models.Book.objects.filter(pk=12).first()
print(book_obj.book_author.all()[0].name)
# 执行结果
刘慈欣
查询作者三溪的电话号码
book_obj = models.Authors.objects.filter(name='三溪').first()
print(book_obj.author_detail.phone)
# 执行结果
119

在书写ORM语句的时候跟写SQL语句一样,不要老想着一次性写完,如果比较复杂,就写一点看一点;正向什么时候需要加all?结果可能有多个的时候就加,只有一个就直接拿数据

3.2.2、反向子查询

反向查询用表名小写,加_set,当你的查询结果可以有多个的时候,就必须加_set.all(),只有一个结果时,不用加_set

查询是北京出版社的书
book_obj = models.PublishingHouse.objects.filter(name='北京出版社').first()
book = book_obj.book_set.all()
for i in book:
    print(i.book_name)
# 执行得到结果
球状闪电
骆驼祥子
流浪地球
查询三溪写过的书
book_obj = models.Authors.objects.filter(name='三溪').first()
book = book_obj.book_set.all()
for i in book:
    print(i.book_name)
# 执行结果
今村异闻录
津门除妖记
查询手机号是120的作者
author_obj = models.AuthorDetail.objects.filter(phone=120).first()
print(author_obj.authors.name)
# 执行结果
老舍

3.3、联表查询

要求一行代码搞定,一题不准用.author,filter里面用.外键字段加属性;只要掌握了正反向概念以及双下划线,就可以无限制地跨表!

3.3.1、双下划线跨表查询

用values取特定字段,再用双下划线取值。

查询三溪手机号

values同样适用正向反向概念,author_detail意味着从作者表已经跨到作者详情表,后面跟上双下划线即可直接获取其特定字段!

author_obj = models.Authors.objects.filter(name='三溪').values('author_detail__phone', 'name').first()
print(author_obj)  # 上面一行就已经拿到结果了,这行只是展示结果而已,别钻牛角尖哈!
# 执行结果
{'author_detail__phone': 119, 'name': '三溪'}
查询主键为11的书的出版社和书名
book_obj = models.Book.objects.filter(pk=11).values('book_publish__name', 'book_name').first()
print(book_obj)
# 执行结果
{'book_publish__name': '人民出版社', 'book_name': '三体'}
查询主键为11的书籍的作者
book_obj = models.Book.objects.filter(pk=11).values('book_author__name').first()
print(book_obj)
# 执行结果
{'book_author__name': '刘慈欣'}
查询主键为11的书籍的作者手机号
book_obj = models.Book.objects.filter(pk=11).values('book_author__author_detail__phone').first()
print(book_obj)
# 执行结果
{'book_author__author_detail__phone': 110}

3.4、聚合查询

还记得MySQL的聚合函数吗?AVG MIN MAX COUNT SUM,在Django需要用aggregate括起来才能用这些聚合函数,而且聚合函数通常是配合分组一起使用,而且这些方法可以一次性全部使用。

Django中只要是跟数据库相关的模块,基本都在django.db.models里,如没有就在django.db。

所有书的平均价格

聚合函数里直接写字段名即可

book_obj = models.Book.objects.aggregate(Avg('book_price'))
print(book_obj)
# {'book_price__avg': 20.75}

3.5、分组查询

在原生MySQL分组叫group by,在Django叫annotate;MySQL针对分组查询都有哪些特点?分组之后默认只能获取到分组的依据,组内其它字段都无法直接获取了,因为默认开了严格模式。

3.5.1、annotate用法

models后面点什么就是按什么表来分组,还需要自己定义一个别名来接收查询的结果,再用values来取。

3.5.2、练习题
统计每本书的作者个数
book_obj = models.Book.objects.annotate(author_number=Count('book_author')).values('book_name', 'author_number')
print(book_obj)
# 执行结果
<QuerySet [{'book_name': '三体', 'author_number': 1}, {'book_name': '球状闪电', 'author_number': 1}, {'book_name': '超新星纪元', 'author_number': 1}, {'book_name': '骆驼祥子', 'author_number': 1}, {'book_name': '四世同堂', 'author_number': 1}, {'book_name': '今村异闻录', 'author_number': 1}, {'book_name': '津门除妖记', 'author_number': 1}, {'book_name': '流浪地球', 'author_number': 1}]>
统计每个出版社卖得最便宜的书的价格
book_obj = models.PublishingHouse.objects.annotate(min_price=Min('book__book_price')).values('name', 'book__book_name', 'min_price')
print(book_obj)

# 执行结果
<QuerySet [{'name': '人民出版社', 'book__book_name': '三体', 'min_price': 35}, {'name': '北京出版社', 'book__book_name': '球状闪电', 'min_price': 18}, {'name': '上海出版社', 'book__book_name': '超新星纪元', 'min_price': 25}, {'name': '未出版', 'book__book_name': '今村异闻录', 'min_price': 0}]>
统计不止一个作者的图书

先临时给津门除妖记增加一个作者andy

book_obj = models.Book.objects.annotate(authors=Count('book_author')).filter(authors__gt=1).values('book_name', 'authors')
print(book_obj)

# 执行结果
<QuerySet [{'book_name': '津门除妖记', 'authors': 2}]>
查询每个作者出的书总价格
book_obj = models.Authors.objects.annotate(total_price=Sum('book__book_price')).values('name', 'total_price')
print(book_obj)

# 执行结果
<QuerySet [{'name': '刘慈欣', 'total_price': 123}, {'name': '老舍', 'total_price': 43}, {'name': '三溪', 'total_price': 0}, {'name': 'andy', 'total_price': 0}]>
3.5.3、指定的字段分组

先values取数据再用annotate分组,后续BBS作业会使用;如果出现分组查询报错的情况,需要看看数据库的严格模式。

3.6、F与Q查询

先加两个字段:库存和已卖出;F查询,需要import F,能够帮你直接获取到表中某个字段对应的数据

MariaDB [book_system]> SELECT * FROM study_book;
+---------+------------+------------+---------------+-----------------+-----------+------+
| book_id | book_name  | book_price | register_date | book_publish_id | inventory | sold |
+---------+------------+------------+---------------+-----------------+-----------+------+
|      11 | 三体       |         35 | 2006-05-01    |               1 |       800 |  200 |
|      12 | 球状闪电   |         25 | 2005-02-01    |               2 |       700 |  300 |
|      13 | 超新星纪元 |         30 | 2014-02-02    |               3 |       600 |  400 |
|      14 | 骆驼祥子   |         18 | 1936-08-04    |               2 |       500 |  500 |
|      15 | 四世同堂   |         25 | 2017-10-03    |               3 |       400 |  600 |
|      16 | 今村异闻录 |          0 | 2017-07-15    |               4 |       300 |  700 |
|      17 | 津门除妖记 |          0 | 2019-10-04    |               4 |       600 |  200 |
|      23 | 流浪地球   |         33 | 2021-02-24    |               2 |         0 | 1000 |
+---------+------------+------------+---------------+-----------------+-----------+------+
8 rows in set (0.000 sec)
3.6.1、F查询练习题
查询卖出数大于库存数的书籍
from django.db.models import F, Q
book_obj = models.Book.objects.filter(sold__gt=F('inventory')).values('book_name', 'sold')
print(book_obj)

# 执行结果
<QuerySet [{'book_name': '四世同堂', 'sold': 600}, {'book_name': '今村异闻录', 'sold': 700}, {'book_name': '流浪地球', 'sold': 1000}]>
所有书籍的价格提升50元

Django支持F()对象之间以及F()对象和常数之间的加减乘除和取模的操作。基于此可以对表中的数值类型进行数学运算。

book_obj = models.Book.objects.update(book_price=F('book_price')+50)
MariaDB [book_system]> SELECT * FROM study_book;
+---------+------------+------------+---------------+-----------------+-----------+------+
| book_id | book_name  | book_price | register_date | book_publish_id | inventory | sold |
+---------+------------+------------+---------------+-----------------+-----------+------+
|      11 | 三体       |         85 | 2006-05-01    |               1 |       800 |  200 |
|      12 | 球状闪电   |         75 | 2005-02-01    |               2 |       700 |  300 |
|      13 | 超新星纪元 |         80 | 2014-02-02    |               3 |       600 |  400 |
|      14 | 骆驼祥子   |         68 | 1936-08-04    |               2 |       500 |  500 |
|      15 | 四世同堂   |         75 | 2017-10-03    |               3 |       400 |  600 |
|      16 | 今村异闻录 |         50 | 2017-07-15    |               4 |       300 |  700 |
|      17 | 津门除妖记 |         50 | 2019-10-04    |               4 |       600 |  200 |
|      23 | 流浪地球   |         83 | 2021-02-24    |               2 |         0 | 1000 |
+---------+------------+------------+---------------+-----------------+-----------+------+
8 rows in set (0.000 sec)
所有书名后加爆款两字

在操作字符类型的数据时,F不能直接做到字符串的拼接;需要使用concat和value模块;直接用+号会将所有名称变成空白

book_obj = models.Book.objects.update(book_name=Concat(F('book_name'), Value('热销')))
MariaDB [book_system]> SELECT * FROM study_book;
+---------+----------------+------------+---------------+-----------------+-----------+------+
| book_id | book_name      | book_price | register_date | book_publish_id | inventory | sold |
+---------+----------------+------------+---------------+-----------------+-----------+------+
|      11 | 三体热销       |         85 | 2006-05-01    |               1 |       800 |  200 |
|      12 | 球状闪电热销   |         75 | 2005-02-01    |               2 |       700 |  300 |
|      13 | 超新星纪元热销 |         80 | 2014-02-02    |               3 |       600 |  400 |
|      14 | 骆驼祥子热销   |         68 | 1936-08-04    |               2 |       500 |  500 |
|      15 | 四世同堂热销   |         75 | 2017-10-03    |               3 |       400 |  600 |
|      16 | 今村异闻录热销 |         50 | 2017-07-15    |               4 |       300 |  700 |
|      17 | 津门除妖记热销 |         50 | 2019-10-04    |               4 |       600 |  200 |
|      23 | 流浪地球热销   |         83 | 2021-02-24    |               2 |         0 | 1000 |
+---------+----------------+------------+---------------+-----------------+-----------+------+
8 rows in set (0.000 sec)
3.6.2、Q查询练习题

Q支持与或非运算,其中逗号是与运算,|是或运算,~是非运算。

查询卖出数大于100或者价格小于70的书籍
book_obj = models.Book.objects.filter(Q(sold__gt=800) | Q(book_price__lt=70)).values('book_name', 'sold', 'book_price')
print(book_obj)

# 执行结果
<QuerySet [{'book_name': '骆驼祥子热销', 'sold': 500, 'book_price': 68}, {'book_name': '今村异闻录热销', 'sold': 700, 'book_price': 50}, {'book_name': '津门除妖记热销', 'sold': 200, 'book_price': 50}, {'book_name': '流浪地球热销', 'sold': 1000, 'book_price': 83}]>
3.6.3、Q进阶用法

在上面的Q查询中,比如sold__gt=800直接就是变量名形式;但是我们平常在百度谷歌等搜索引擎输入的是什么?是字符串,后台能不能将用户输入的字符串转换为变量形式作为后端查询条件呢?

这就需要用到Q的另外一种功能了,它能够将查询条件的左边也变成字符串的形式。

q = Q()  # 先实例化一个空对象
q.connector = 'or'  # 默认是and关系,如果需要改就这么干。
q.children.append(('sold__gt', 800))  # 把条件追加进去,括号内必须是可迭代对象,列表元祖字典都行
result = models.Book.objects.filter(q).values('book_name', 'sold')  # 查询数据
print(result)

# 执行结果
<QuerySet [{'book_name': '流浪地球热销', 'sold': 1000}]>

四、Django事务

先来简单复习一下MySQL的事务和ACID四大特性。

4.1、事务

事务用于将多条SQL语句或者说多条操作看做是一个不可分割的整体(原子性),

4.2、ACID

  • A:atomicity,原子性;一个事务要么全部执行成功,要么全部失败,因为原子是不可分割的最小单位。
  • C:consistency,一致性;事务的执行不能破坏数据库的一致性和完整性,在其执行前后,数据库都必须处于一致性状态!
  • I:insolution,隔离性;事务之间是彼此相互隔离、不互相干扰的。
  • D:durability,持久性;一旦事务执行成功,那么它对数据库的改变就是永久的,即使发生故障,只要数据能重新启动就一定能恢复到事务执行成功后的状态。

4.3、Django开启事务

以上即为事务的简单复习,我们也知道现在无法做到同时满足ACID!而Django目前的学习阶段,我们只需要掌握如何开启事务即可!

from django.db import transaction


with transaction.atomic():
    # SQL语句1
    # SQL语句2
    ...

五、ORM常用字段及参数

ORM中常用字段罗列如下:

CharField

IntegerField

BigintegerField

DecimalField

EmailTield

DaTe

DateTime

BooleanField,布尔值类型,该字段传True False,数据库存0和1,用来标记比如是否删除状态。

TextField,文本类型,用来存大段内容,没有字数限制,BBS会用到。

FileField,也是字符串类型

​ upload_to = "/data"给该字段传一个文件对象,会自动将文件保存到/data目录下,接着将文件路径保存到数据库中,BBS作业会涉及

5.1、字段集合

Django字段参数说明对应MySQL字段
AutoFieldint自增列,必须填入参数 primary_key=TrueInteger AUTO_INCREMENT
SmallIntegerField小整数 -32768 ~ 32767smallint
PositiveSmallIntegerField正小整数 0 ~ 32767smallint UNSIGNED
IntegerField整数列(带符号的) -2147483648 - 2147483647integer
PositiveIntegerField正整数 0 ~ 2147483647integer UNSIGNED
BigIntegerField长整型(有符号的) -9223372036854775808 - 9223372036854775807bigint
FloatField浮点型double precision
DecimalField10进制小数,max_digits,小数总长度,decimal_places,小数位长度numeric(%(max_digits)s, %(decimal_places)s)
BinaryField二进制类型longblob
BooleanField布尔值类型,True Falsebool
NullBooleanField可以为空的布尔值
CharField字符类型,必须提供max_length参数, max_length表示字符长度varchar(%(max_length)s)
TextField文本类型longtext
EmailField字符串类型,Django Admin以及ModelForm中提供验证机制
IPAddressField字符串类型,Django Admin以及ModelForm中提供验证 IPV4 机制char(15)
GenericIPAddressField字符串类型,Django Admin以及ModelForm中提供验证 Ipv4和Ipv6 protocol,用于指定Ipv4或Ipv6, 'both',"ipv4","ipv6" unpack_ipv4, 如指定为True,则输入::ffff:192.0.2.1时,可解析为192.0.2.1,开启此功能,需要protocol="both"char(39)
URLField字符串类型,Django Admin以及ModelForm中提供验证 URL
SlugField字符串类型,Django Admin以及ModelForm中提供验证支持 字母、数字、下划线、连接符(减号)varchar(%(max_length)s)
CommaSeparatedIntegerField字符串类型,格式必须为逗号分割的数字varchar(%(max_length)s)
UUIDField字符串类型,Django Admin以及ModelForm中提供对UUID格式的验证char(32)
FilePathField字符串,Django Admin以及ModelForm中提供读取文件夹下文件的功能varchar(%(max_length)s)
FileField字符串,路径保存在数据库,文件上传到指定目录varchar(%(max_length)s)
ImageField字符串,路径保存在数据库,文件上传到指定目录
DateTimeField日期+时间格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]datetime
DateField日期格式 YYYY-MM-DDdate
TimeField时间格式 HH:MM[:ss[.uuuuuu]]time
DurationField长整数,时间间隔,数据库中按照bigint存储,ORM中获取的值为datetime.timedelta类型bigint
FloatField

5.2、自定义字段

django除了给你提供了很多字段类型外,还支持自定义字段,比如来自定义个char

class MyCharField(models.fields):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length  # 给对象添加属性    
        super().__init__(max_length=max_length, *args, **kwargs)  # 调用父类方法,必须以关键字形式传递max

    def db_type(self, connection):
        return f'char({self.max_length})'  # 返回真正的数据类型及各种约束条件
    
myfield = MyCharField(max_length=16, null=True)  # 实例化使用

5.3、关系字段及参数

5.3.1、ForeignKey

外键字段在ORM中用来表示外键关联关系,一般把它设置在一对多关系中多的一方,它也有很多属性:

book_publish = models.ForeignKey(to='PublishingHouse', to_field='id', on_delete=models.CASCADE())
to

设置要关联的表

to_field

设置要关联的表的字段

on_delete

删除关联表中的数据时,当前表与其关联的行为(Django1.X默认级联更新级联删除);它也有很多行为

on_delete行为效果
models.CASCADE删除关联数据,与之关联的也删除(级联删除)
models.DO_NOTHING删除关联数据,引发错误IntegrityError
models.PROTECT删除关联数据,引发错误ProtectedError
models.SET_NULL删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)
models.SET_DEFAULT删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)
models.SET删除关联数据, a. 与之关联的值设置为指定值,设置:models.SET(值) b. 与之关联的值设置为可执行对象的返回值,设置:models.SET(可执行对象)
db_constraint

是否在数据库中创建外键约束,默认为True。

unique

models.ForeignKey(unique=True) 等同于 models.OneToOneField()

六、数据库查询优化

据说面试可能会问到这些知识点,罗列一下,便于后续查看。

正常情况下,ORM每条语句都会发起若干次数据库请求,这对于大规模线上环境来说很可能是一种巨大的负担,有没有办法减少ORM对数据库读写的频率呢?

6.1、ORM语句特点

6.1.1、惰性查询

如果你仅仅是书写了ORM语句,在后面根本没有用到该语句;那么ORM会自动识别,然后不执行该语句。

6.1.2、only与defer

假设我想要获取书籍表中所有书名,要求获取到的是一个数据对象,只需要.name字段就能拿到书名,且不能存在其它字段。

为了确保效果,我们先提前在settings配置好显示SQL语句,这样一来只要是发起数据库请求,就一定会打印SQL语句在pycharm终端,这样我们就可以知道它是不是真的只存在这些字段,还是偷偷跑去数据库拿到数据再展示出来。

only

only,点击only括号内的字段不会走数据库;点它没有的字段,会重新走数据库查询,而all因为获取了所有数据所以点别的字段也不用走

book_obj = models.Book.objects.only('book_name').first()
print(book_obj.book_name)

# 执行结果,可以看出就第一次查询时发起数据库请求,第二次print没有发起数据库请求,证明确实
(0.000) SELECT `study_book`.`book_id`, `study_book`.`book_name` FROM `study_book` ORDER BY `study_book`.`book_id` ASC LIMIT 1; args=()
三体热销

上面的演示是only里面的字段,证明不了什么,这次来点它没有的字段。

book_obj = models.Book.objects.only('book_name').first()
print(book_obj.book_price)

# 执行结果,可以看出,当点出only里面没有的字段时,它就会去数据库拿到数据再展示
(0.001) SELECT `study_book`.`book_id`, `study_book`.`book_name` FROM `study_book` ORDER BY `study_book`.`book_id` ASC LIMIT 1; args=()
(0.000) SELECT VERSION(); args=None
(0.001) SELECT `study_book`.`book_id`, `study_book`.`book_price` FROM `study_book` WHERE `study_book`.`book_id` = 11; args=(11,)
85
defer

与only相反,defer括号内放的字段不在查询出来的对象里面;查询该字段需要重新走数据库,而如果是查询的是非括号内的字段,就不用走数据库。

既然如此,先来个defer里面没有的字段:

book_obj = models.Book.objects.defer('book_name').first()
print(book_obj.book_price)

# 执行结果,确实如此,不在defer里面的都不需要重新发起数据库请求
(0.000) SELECT `study_book`.`book_id`, `study_book`.`book_price`, `study_book`.`register_date`, `study_book`.`book_publish_id`, `study_book`.`inventory`, `study_book`.`sold` FROM `study_book` ORDER BY `study_book`.`book_id` ASC LIMIT 1; args=()
85

再看看defer里面的字段会如何:

book_obj = models.Book.objects.defer('book_name').first()
print(book_obj.book_name)

# 执行结果,可以看到,当点defer里面的字段时,它又跑去发起数据库请求了。
(0.000) SELECT @@SQL_AUTO_IS_NULL; args=None
(0.001) SELECT `study_book`.`book_id`, `study_book`.`book_price`, `study_book`.`register_date`, `study_book`.`book_publish_id`, `study_book`.`inventory`, `study_book`.`sold` FROM `study_book` ORDER BY `study_book`.`book_id` ASC LIMIT 1; args=()
(0.000) SELECT VERSION(); args=None
(0.002) SELECT `study_book`.`book_id`, `study_book`.`book_name` FROM `study_book` WHERE `study_book`.`book_id` = 11; args=(11,)
三体热销
6.1.3、select和prefetch

select_related和prefetch_related跟跨表操作有关

select_related内部是inner join联表操作,一次性将所有数据塞给查询出来的对象,此时这个对象无论点两个表中的任意一个都不用再走数据库查询了;它括号内只能放外键字段,一对多、一对一都可以,唯独多对多不行。

select是通过将很多操作合并在一条SQL语句然后一次性全部塞给数据对象,这样可以减少查询频率、减轻数据库压力、提升查询速度;但在单张表特别庞大的情况下,效率非常低。

"""
这句意思拿到Book表和其关联的出版社表所有数据,如果出版社表还有关联的表,就用双下划线方式即可
字段之间还是用逗号隔开,要记得只能是一对一、一对多关系的外键字段才行!!!
"""
book_obj = models.Book.objects.all().select_related('book_publish')
for i in book_obj:
    print(i.book_publish.name)

# 执行结果,只有一条长SQL语句,通过减少查询次数来提升性能。
(0.001) SELECT `study_book`.`book_id`, `study_book`.`book_name`, `study_book`.`book_price`, `study_book`.`register_date`, `study_book`.`book_publish_id`, `study_book`.`inventory`, `study_book`.`sold`, `study_publishinghouse`.`id`, `study_publishinghouse`.`name` FROM `study_book` INNER JOIN `study_publishinghouse` ON (`study_book`.`book_publish_id` = `study_publishinghouse`.`id`); args=()
人民出版社
北京出版社
上海出版社
北京出版社
上海出版社
未出版
未出版
北京出版社

prefetch_related方法支持一对多、多对多关系;它内部其实就是子查询,它将子查询的结果全部塞给对象,给你的感觉好像也是一次性搞定的,它用执行多次SQL查询在Python代码中实现连表操作。

它适合单表查询,多表和连表操作时速度会慢;下面看看查询同样的数据就知道了:

book_obj = models.Book.objects.all().prefetch_related('book_publish')
for i in book_obj:
    print(i.book_publish.name)
    
# 执行结果,比select多了一条子查询的SQL语句。
(0.000) SELECT @@SQL_AUTO_IS_NULL; args=None
(0.000) SELECT `study_book`.`book_id`, `study_book`.`book_name`, `study_book`.`book_price`, `study_book`.`register_date`, `study_book`.`book_publish_id`, `study_book`.`inventory`, `study_book`.`sold` FROM `study_book`; args=()
(0.001) SELECT VERSION(); args=None
(0.000) SELECT `study_publishinghouse`.`id`, `study_publishinghouse`.`name` FROM `study_publishinghouse` WHERE `study_publishinghouse`.`id` IN (1, 2, 3, 4); args=(1, 2, 3, 4)
人民出版社
北京出版社
上海出版社
北京出版社
上海出版社
未出版
未出版
北京出版社
6.1.4、select和prefetch总结

prefetch_related比select_related多一条查询语句,但如果是拼大表则select_related耗时更长,相反则有优势

如果是跨表查询一对多、一对一关系的数据,且每个表数据量量比较小的情况下可以考虑用select_related,如果单表过大则会被拖累无法提升性能;否则考虑支持一对多、多对多的prefetch_related。

七、补充知识

7.1、choices参数

在设计表结构时,我们会遇到比如说性别有男、女、其它;比如衣服码数有S、M、L、XL等;这些该怎么存呢?在MySQL中有枚举,那么ORM也有对应的参数来实现:choice。

先来定义有choice参数的字段,在定义GENDER的数据后,如果写入数据时比如是4:'hihi'不在GENDER也能存而不直接报错,你存什么就是什么。

但是要保证字段类型跟列举出来的元祖第一个数据类型一致!!!

class UserInfo(models.Model):
    GENDER = ((1, 'male'), (2, 'female'), (3, 'others'))
    name = models.CharField(max_length=32, null=True)
    gender = models.IntegerField(max_length=16, choices=GENDER)

验证

user_obj = models.UserInfo.objects.filter(pk=1).first()
print(user_obj.gender)

# 执行结果
1

获取对应信息

上面查询结果是1,这很明显不是我们想要的,我们想要的是1对应的值;Django为此提供了固定语法来获取对应值

user_obj = models.UserInfo.objects.filter(pk=1).first()
print(user_obj.get_gender_display())  # get_字段名_display(),括号内留空。

# 执行结果
male

7.2、多对多关系的三种创建方式

7.2.1、全自动

其实就是利用orm自动创建第三张关系表,我们之前干的ManyToManyField就是。

优点

代码不用自己写,很方便,还支持orm提供操作第三张关系表的各种方法。

缺点

第三张关系表的扩展性极差,因为没办法额外添加字段。

7.2.2、纯手动
# book_author = models.ManyToManyField(to='Authors'),全自动一行搞定,纯手动要三行。
class BookAuthor(models.Model):
    book_id = models.ForeignKey(to='Book')
    author_id = models.ForeignKey(to='Authors')
优点

第三张表想怎么改就怎么改。

缺点

需要写的代码多,而且不能再使用ORM提供的丰富的方法,比如正反向查询;因此不建议用该方式。

7.2.3、半自动

还是自己创建第三张表,然后告诉ORM不用创表了,直接把方法给我用就好了!这就有点像你去KTV或者酒楼消费,告诉商家我自带酒水你们不用给我上酒水了,但是其它服务还是要给我服务周到了。

那么这through_fields应该把谁放在前面?外键字段放在哪张表,就把它的字段放在前面。

class Book(models.Model):
    book_id = models.AutoField(primary_key=True, verbose_name='pk')
    book_name = models.CharField(max_length=32, verbose_name='book')
    # 告诉ORM我自己有第三张表了,你不用创建,还得告诉它是哪些字段。
    # through即通过自建的第三张表到达to的那张表,这有点类似中介,through_fields是首尾相连的两个字段
    book_author = models.ManyToManyField(to='Authors', through='BookAuthor', through_fields=('book_id', 'author_id'))

优点

扩展性也高,还可以使用orm正反向查询。

缺点

没法使用add,set,remove,clear这四个方法

7.2.4、总结

我们需要掌握的是全自动和半自动,但为了扩展性,一般都会用半自动;扩展性就好比代码不要写死,写代码跟做人一样,要给自己留条后路。

7.3、批量插入

7.3.1、普通批量插入

先创建个最简单的表

class TestInfo(models.Model):
    name = models.CharField(max_length=32, null=True)

views代码

def ajax_test(request):
    for name in range(1000):
        models.TestInfo.objects.create(name=f'第{name}')
    user_obj = models.TestInfo.objects.all()
    return render(request, 'json_info.html', locals())

HTML代码

<body>
{% for foo in user_obj %}
    <p>{{ foo.name }}</p>
{% endfor %}
</body>

跑起来后我电脑卡了好几秒才刷出来,可以看出这种批量插入与显示的效率是非常低下的。

7.3.2、bulk_create

views代码

def ajax_test(request):
    name_list = []
    for name in range(1000):
        name_obj = models.TestInfo(name=f'第{name}个人')
        name_list.append(name_obj)
    models.TestInfo.objects.bulk_create(name_list)
    return render(request, 'json_info.html', locals())

HTML代码

<body>
{% for foo in name_list %}
    <p>{{ foo.name }}</p>
{% endfor %}
</body>

执行后页面非常快地刷新并显示数据出来,速度是普通插入的不知道多少倍!!!

世间微尘里 独爱茶酒中