目录
- 一、模型的定义
- 二、数据迁移
- 三、数据表关系
- 四、数据表操作
- 4.1 Shell工具
- 4.2 数据新增
- 4.3 数据修改
- 4.4 数据删除
- 4.5 数据查询
- 4.6 多表查询
- 4.7 执行SQL语句
- 4.8 数据库事务
Django 对各种数据库提供了很好的支持,包括 PostgreSQL、MySQL、SQLite 和 Oracle,而且为这些数据库提供了统一的 API 方法,这些 API 统称为 ORM 框架。通过使用 Django 内置的 ORM 框架可以实现数据库连接和读写操作。本文以 MySQL 数据库为例,分别讲述 Django 的模型定义与数据迁移、数据表关系、数据表操作和多数据库的连接与使用。
关于数据库的基础知识可以参考专栏:https://blog.csdn.net/xw1680/category_12277246.html?spm=1001.2014.3001.5482
一、模型的定义
ORM 框架是一种程序技术,用于实现面向对象编程语言中不同类型系统的数据之间的转换。从效果上说,它创建了一个可在编程语言中使用的 "虚拟对象数据库"
, 通过对虚拟对象数据库的操作从而实现对目标数据库的操作,虚拟对象数据库与目标数据库是相互对应的。在 Django 中,虚拟对象数据库也称为模型,通过模型实现对目标数据库的读写操作,实现方法如下:
- 配置目标数据库,在 settings.py 中设置配置属性,配置步骤可参考 Django 2024全栈开发指南(二):Django项目配置详解 一文的
四、数据库配置
小节。 - 构建虚拟对象数据库,在 App 的 models.py 文件中以类的形式定义模型。
- 通过模型在目标数据库中创建相应的数据表。
- 在其他模块(如视图函数)里使用模型来实现目标数据库的读写操作。
在项目的配置文件 settings.py 里设置数据库配置信息。以 Django5Study 项目为例,其配置信息如下:
DATABASES = {
# 默认数据库
"default": {
"ENGINE": "django.db.backends.mysql",
"OPTIONS": {'read_default_file': str(BASE_DIR / 'my.cnf')},
},
}
我们通过终端登录 mysql,查看 django5study 数据库,数据库中只有内置的迁移文件生成的数据表,如下图所示:
我们想要的数据表可以通过模型创建,因为 Django 对模型和目标数据库之间有自身的映射规则,如果自己在数据库中创建数据表,就可能不符合 Django 的建表规则,从而导致模型和目标数据库无法建立有效的通信联系。大概了解项目的环境后,在 Django5Study 项目中新建子应用 chapter03_Model,命令如下:
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py startapp chapter03_Model
接下来我们在子应用 chapter03_Model 目录下的 models.py 文件中定义图书模型,字段名称包括书名、发布日期、阅读量、评论量、与是否下架,示例代码如下:
from django.db import models
# Create your models here.
# 准备书籍列表信息的模型类
# chapter03_Model的models.py
class BookInfo(models.Model):
# 创建字段,字段类型...verbose_name主要是在admin站点中使用
book_name = models.CharField(max_length=20, verbose_name='名称')
pub_date = models.DateField(verbose_name='发布日期', null=True)
read_count = models.IntegerField(default=0, verbose_name='阅读量')
comment_count = models.IntegerField(default=0, verbose_name='评论量')
is_delete = models.BooleanField(default=False, verbose_name='逻辑删除')
class Meta:
db_table = 'book_info' # 指明数据库表名
verbose_name = '图书' # 在admin站点中显示的名称
def __str__(self):
"""定义每个数据对象的显示信息"""
return self.book_name
模型 BookInfo 定义了5个字段,分别代表字符类型、日期类型、整型、整型和布尔类型。但在实际开发中,我们需要定义不同的字段类型来满足各种开发需求,因此Django划分了多种字段类型,在源码目录 django\db\models\fields
的 __init__.py
和 files.py
文件里找到各种模型字段,如下图所示:
说明如下:
# AutoField: 自增长类型,数据表的字段类型为整数,长度为11位
# BigAutoField: 自增长类型,数据表的字段类型为bigint,长度为20位
# CharField: 字符类型
# BooleanField: 布尔类型
# CommaSeparatedIntegerField: 用逗号分隔的整数类型
# DateField: 日期(Date)类型
# DateTimeField: 日期时间(Datetime)类型
# Decimal: 十进制小数类型
# EmailField: 字符类型,存储邮箱格式的字符串
# FloatField: 浮点数类型,数据表的字段类型变成Double类型
# IntegerField: 整数类型,数据表的字段类型为11位的整数
# BigIntegerField: 长整数类型
# IPAddressField: 字符类型,存储Ipv4地址的字符串
# GenericIPAddressField: 字符类型,存储Ipv4和Ipv6地址的字符串
# NullBooleanField: 允许为空的布尔类型
# PositiveIntegerFiel: 正整数的整数类型
# PositiveSmallIntegerField: 小正整数类型,取值范围为0~32767
# SlugField: 字符类型,包含字母、数字、下画线和连字符的字符串
# SmallIntegerField: 小整数类型,取值范围为-32,768~+32,767
# TextField: 长文本类型
# TimeField: 时间类型,显示时分秒HH:MM[:ss[.uuuuuu]]
# URLField: 字符类型,存储路由格式的字符串
# BinaryField: 二进制数据类型
# FileField: 字符类型,存储文件路径的字符串
# ImageField: 字符类型,存储图片路径的字符串
# FilePathField: 字符类型,从特定的文件目录选择某个文件
每个模型字段都允许设置参数,这些参数来自父类 Field,我们在源码里查看 Field 的定义过程 (django\db\models\fields\__init__.py
) ,如下图所示:
对模型字段的参数进行分析:
# verbose_name: 默认为None,在Admin站点管理设置字段的显示名称
# primary_key: 默认为False,若为True,则将字段设置成主键
# max_length: 默认为None,设置字段的最大长度
# unique: 默认为False,若为True,则设置字段的唯一属性
# blank: 默认为False,若为True,则字段允许为空值,数据库将存储空字符串
# null: 默认为False,若为True,则字段允许为空值,数据库表现为NULL
# db_index: 默认为False,若为True,则以此字段来创建数据库索引
# default: 默认为NOT_PROVIDED对象,设置字段的默认值
# editable: 默认为True,允许字段可编辑,用于设置Admin的新增数据的字段
# serialize: 默认为True,允许字段序列化,可将数据转化为JSON格式
# unique_for_date: 默认为None,设置日期字段的唯一性
# unique_for_month: 默认为None,设置日期字段月份的唯一性
# unique_for_year: 默认为None,设置日期字段年份的唯一性
# choices: 默认为空列表,设置字段的可选值
# help_text: 默认为空字符串,用于设置表单的提示信息
# db_column: 默认为None,设置数据表的列名称,若不设置,则将字段名作为数据表的列名
# db_tablespace: 默认为None,如果字段已创建索引,那么数据库的表空间名称将作为该字段的索引名注意: 部分数据库不支持表空间
# auto_created: 默认为False,若为True,则自动创建字段,用于一对一的关系模型
# validators: 默认为空列表,设置字段内容的验证函数
# error_messages: 默认为None,设置错误提示
上述参数适用于所有模型字段,但不同类型的字段会有些特殊参数,每个字段的特殊参数可以在字段的初始化方法 __init__
里找到,比如字段 DateField 和 TimeField 的特殊参数 auto_now_add 和 auto_now,字段 FileField 和 ImageField 的特殊参数 upload_to。
在定义模型时,一般情况下都会重写函数 __str__
, 这是设置模型的返回值,默认情况下,返回值为模型名+主键。函数 __str__
可用于外键查询,比如模型A设有外键字段F,外键字段F关联模型B,当查询模型A时,外键字段F会将模型B的函数 __str__
返回值作为字段内容。需要注意的是,函数 __str__
只允许返回字符类型的字段,如果字段是整型或日期类型的,就必须使用 Python 的 str() 函数将其转化成字符类型。模型除了定义模型字段和重写函数 __str__
之外,还有 Meta 选项,这三者是定义模型的基本要素。Meta 选项里设有19个属性,每个属性的说明如下:
# abstract: 若设为True,则该模型为抽象模型,不会在数据库里创建数据表
# app_label: 属性值为字符串,将模型设置为指定的项目应用,比如将index的models.py定义的模型A指定到其他App里
# db_table: 属性值为字符串,设置模型所对应的数据表名称
# db_teblespace: 属性值为字符串,设置模型所使用数据库的表空间
# get_latest_by: 属性值为字符串或列表,设置模型数据的排序方式
# managed: 默认值为True,支持Django命令执行数据迁移;若为False,则不支持数据迁移功能
# order_with_respect_to: 属性值为字符串,用于多对多的模型关系,指向某个关联模型的名称,
# 并且模型名称必须为英文小写比如模型A和模型B,模型A的一条数据对应模型B的多条数据,两个模型关联后,
# 当查询模型A的某条数据时,可使用get_b_order()和set_b_order()来获取模型B的关联数据,
# 这两个方法名称的b为模型名称小写,此外get_next_in_order()和get_previous_in_order()可以
# 获取当前数据的下一条和上一条的数据对象
# ordering: 属性值为列表,将模型数据以某个字段进行排序
# permissions: 属性值为元组,设置模型的访问权限,默认设置添加、删除和修改的权限
# proxy: 若设为True,则为模型创建代理模型,即克隆一个与模型A相同的模型B
# required_db_features: 属性值为列表,声明模型依赖的数据库功能比如['gis_enabled'],表示模型依赖GIS功能
# required_db_vendor: 属性值为列表,声明模型支持的数据库,默认支持SQLite、PostgreSQL,MySQL和Oracle
# select_on_save: 数据新增修改算法,通常无须设置此属性,默认值为False
# indexes: 属性值为列表,定义数据表的索引列表
# unique_together: 属性值为元组,多个字段的联合唯一,等于数据库的联合约束
# verbose_name: 属性值为字符串,设置模型直观可读的名称并以复数形式表示
# verbose_name_plural: 与verbose_name相同,以单数形式表示
# label: 只读属性,属性值为app_label.object_name,如index的模型PersonInfo,值为index.PersonInfo
# label_lower: 与label相同,但其值为字母小写,如index.personinfo
综上所述,模型字段、函数 __str__
和 Meta 选项是模型定义的基本要素,模型字段的类型、函数 __str__
和 Meta 选项的属性设置需由开发需求而定。在定义模型时,还可以在模型里定义相关函数,如 get_absolute_url(),当视图类没有设置属性 success_url 时,视图类的重定向路由地址将由模型定义的 get_absolute_url() 提供。除此之外,Django 支持开发者自定义模型字段,从源码文件得知,所有模型字段继承 Field 类,只要将自定义模型字段继承 Field 类并重写父类某些属性或方法即可完成自定义过程,具体的自定义过程不再详细讲述,可以参考内置模型字段的定义过程。
二、数据迁移
数据迁移是将项目里定义的模型生成相应的数据表,本小节将会深入讲述数据迁移的操作,包括数据表的创建和更新。以上一小节的 chapter03_Model 子应用为例,项目所配置的数据库中只有内置的迁移文件生成的数据表,我们想要通过模型创建数据表 BookInfo,可使用 Django 的操作指令完成创建过程。
中间有个小插曲,执行迁移命令一直不成功,提示:No changes detected,我还以为数据库连接等出现了问题,结果发现是在上一小节中创建的 chapter03_Model 子应用没有在 settings.py 文件中注册,要仔细呀,注册如下:
# Application definition
# 子应用列表
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# TODO 1.添加(注册)子应用 chapter01_HelloDjango5
'chapter01_HelloDjango5',
# TODO 2024-11-14.添加(注册)子应用 chapter02_DjangoSettings
'chapter02_DjangoSettings',
# TODO 2024-11-14 注册关于模型学习的子应用
'chapter03_Model',
]
注册完成之后在终端下输入 Django 的操作指令:
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py makemigrations
Migrations for 'chapter03_Model':
chapter03_Model\migrations\0001_initial.py
+ Create model BookInfo
(django5_study) D:\Code\dream\PythonStudy\Django5Study>
当 makemigrations 指令执行成功后,在项目子应用 chapter03_Model 的 migrations 文件夹里创建 0001_initial.py 文件,如果项目里有多个子应用,并且每个子应用 的 models.py 文件里定义了模型对象,当首次执行 makemigrations 指令时,Django 就在每个子应用的 migrations 文件夹里创建 0001_initial.py 文件。打开查看 0001_initial.py 文件,文件内容如下图所示:
0001_initial.py 文件将 models.py 定义的模型生成数据表的脚本代码,该文件的脚本代码可被 migrate 指令执行,migrate 指令会根据脚本代码的内容在数据库里创建相应的数据表,在终端下输入 migrate 指令即可完成数据表的创建,代码如下:
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, chapter03_Model, contenttypes, sessions
Running migrations:
Applying chapter03_Model.0001_initial... OK
(django5_study) D:\Code\dream\PythonStudy\Django5Study>
指令运行完成后,打开数据库就能看到新建的数据表,其中数据表 book_info 由项目应用 chapter03_Model 定义的模型 BookInfo 创建,而其他数据表是 Django 内置的功能所使用的数据表,分别是会话 Session、用户认证管理和 Admin 后台系统等。
在开发过程中,开发者因为开发需求而经常调整数据表的结构,比如新增功能、优化现有功能等。假如在上述例子里新增模型 PeopleInfo 及其数据表,为了保证不影响现有的数据表,如何通过新增的模型创建相应的数据表?针对上述问题,我们只需再次执行 makemigrations 和 migrate 指令即可,比如在 chapter03_Model 的 models.py 里定义模型 PeopleInfo,代码如下:
# 准备人物列表信息的模型类
class PeopleInfo(models.Model):
GENDER_CHOICES = (
(0, 'male'),
(1, 'female')
)
name = models.CharField(max_length=20, verbose_name='名称')
gender = models.SmallIntegerField(choices=GENDER_CHOICES, default=0, verbose_name='性别')
description = models.CharField(max_length=200, null=True, verbose_name='描述信息')
book = models.ForeignKey(BookInfo, on_delete=models.CASCADE, verbose_name='图书') # 外键
is_delete = models.BooleanField(default=False, verbose_name='逻辑删除')
class Meta:
db_table = 'people_info'
verbose_name = '人物信息'
def __str__(self):
return self.name
在终端下输入并运行 makemigrations 指令,Django 会在 chapter03_Model 的 migrations 文件夹里创建 0002_peopleinfo.py 文件;然后输入并运行 migrate 指令即可完成数据表 people_info 的创建。
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py makemigrations
Migrations for 'chapter03_Model':
chapter03_Model\migrations\0002_peopleinfo.py
+ Create model PeopleInfo
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, chapter03_Model, contenttypes, sessions
Running migrations:
Applying chapter03_Model.0002_peopleinfo... OK
makemigrations 和 migrate 指令还支持模型的修改,从而修改相应的数据表结构,比如在模型 PeopleInfo 里新增字段 age,代码如下:
# 准备人物列表信息的模型类
class PeopleInfo(models.Model):
GENDER_CHOICES = (
(0, 'male'),
(1, 'female')
)
name = models.CharField(max_length=20, verbose_name='名称')
# 增加字段年龄
age = models.PositiveSmallIntegerField(verbose_name='年龄', default=0)
gender = models.SmallIntegerField(choices=GENDER_CHOICES, default=0, verbose_name='性别')
description = models.CharField(max_length=200, null=True, verbose_name='描述信息')
book = models.ForeignKey(BookInfo, on_delete=models.CASCADE, verbose_name='图书') # 外键
is_delete = models.BooleanField(default=False, verbose_name='逻辑删除')
class Meta:
db_table = 'people_info'
verbose_name = '人物信息'
def __str__(self):
return self.name
新增模型字段必须将属性 null 和 blank 设为 True 或者为模型字段设置默认值(设置属性 default),否则执行 makemigrations 指令会提示字段修复信息。当 makemigrations 指令执行完成后,在 index 的 migrations 文件夹创建相应的 .py 文件,只要再次执行 migrate 指令即可完成数据表结构的修改。
每次执行 migrate 指令时,Django 都能精准运行 migrations 文件夹尚未被执行的 .py 文件,它不会对同一个 .py 文件重复执行,因为每次执行时,Django 会将该文件的执行记录保存在数据表 django_migrations 中,数据表的数据信息如下图所示:
如果要重复执行 migrations 文件夹的某个 .py 文件,就只需在数据表里删除相应的文件执行记录。一般情况下不建议采用这种操作,因为这样很容易出现异常,比如数据表已存在的情况下,再次执行相应的 .py 文件会提示 table "xxx" already exists
异常。migrate 指令还可以单独执行某个 .py 文件,首次在项目中使用 migrate 指令时,Django 会默认创建内置功能的数据表,如果只想执行 chapter03_Model 的 migrations 文件夹的某个 .py 文件,那么可以在 migrate 指令里指定文件名,代码如下:
python manage.py migrate chapter03_Model 0001_initial
在 migrate 指令末端设置项目应用名称 chapter03_Model 和 migrations 文件夹的 0001_initial 文件名,三者(migrate 指令、项目应用名称 chapter03_Model 和 0001_initial 文件名)之间使用空格隔开即可,指令执行完成后,数据库只有数据表 django_migrations 和 people_info。我们知道,migrate 指令根据 migrations 文件夹的 .py 文件创建数据表,但在数据库里,数据表的创建和修改离不开 SQL 语句的支持,因此 Django 提供了 sqlmigrate 指令,该指令能将 .py 文件转化成相应的 SQL 语句。以 chapter03_Model 的 0001_initial.py 文件为例,在终端输入 sqlmigrate 指令,指令末端必须设置项目应用名称和 migrations 文件夹的某个 .py 文件名,三者之间使用空格隔开即可,指令输出结果如下图所示:
除此之外,Django 还提供了很多数据迁移指令,如 squashmigrations、inspectdb、showmigrations、sqlflush、sqlsequencereset 和 remove_stale_contenttypes,这些指令在 Django 2024全栈开发指南(一):框架简介、环境搭建与项目结构 一文中的 2.5.1 Django的操作指令
小节里已说明过了,此处不再重复讲述。当我们在操作数据迁移时,Django 会对整个项目的代码进行检测,它首先执行 check 指令,只要项目里某个功能文件存在异常,Django 就会终止数据迁移操作。也就是说,在执行数据迁移之前,可以使用 check 指令检测整个项目,项目检测成功后再执行数据迁移操作,如下图所示:
三、数据表关系
一个模型对应数据库的一张数据表,但是每张数据表之间是可以存在外键关联的,表与表之间有3种关联:一对一、一对多和多对多。一对一关系存在于两张数据表中,第一张表的某一行数据只与第二张表的某一行数据相关,同时第二张表的某一行数据也只与第一张表的某一行数据相关,这种表关系被称为一对一关系,以下面两张表为例进行说明:
上面两张表中的字段 ID 分别是一一对应的,并且不会在同一表中有重复 ID,使用这种外键关联通常是一张数据表设有太多字段,将常用的字段抽取出来并组成一张新的数据表。在模型中可以通过 OneToOneField 来构建数据表的一对一关系,代码如下:
class Performer(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
nationality = models.CharField(max_length=20)
masterpiece = models.CharField(max_length=50)
class PerformerInfo(models.Model):
id = models.IntegerField(primary_key=True)
performer = models.OneToOneField(Performer, on_delete=models.CASCADE)
birth = models.CharField(max_length=20)
elapse = models.CharField(max_length=20)
对上述模型执行数据迁移,在数据库中分别创建数据表 chapter03_model_performer 和 chapter03_model_performerinfo,打开 Navicat Premium 查看两张数据表的表关系,如下图所示:
一对多关系存在于两张或两张以上的数据表中,第一张表的某一行数据可以与第二张表的一到多行数据进行关联,但是第二张表的每一行数据只能与第一张表的某一行进行关联,以下面两张表为例进行说明。
第一张表的字段 ID 是唯一的,但是第二张表字段 ID 允许重复,字段 ID 相同的数据对应第一张表某一行数据,这种表关系在日常开发中最为常见。在模型中可以通过 ForeignKey 来构建数据表的一对多关系,代码如下:
class Performer(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
nationality = models.CharField(max_length=20)
class Program(models.Model):
id = models.IntegerField(primary_key=True)
performer = models.ForeignKey(Performer, on_delete=models.CASCADE)
name = models.CharField(max_length=20)
对上述模型执行数据迁移,在数据库中分别创建数据表 Performer 和 Program,然后打开 Navicat Premium 查看两张数据表的表关系,如下图所示:
多对多关系存在于两张或两张以上的数据表中,第一张表的某一行数据可以与第二张表的一到多行数据进行关联,同时第二张表中的某一行数据也可以与第一张表的一到多行数据进行关联,以下面的表为例进行说明:
从3张数据表中可以发现,一个演员可以参加多个节目,而一个节目也可以由多个演员来共同演出。每张表的字段 ID 都是唯一的。从最后一张表中可以发现,节目 ID 和演员 ID 出现了重复的数据,分别对应表2和表1的字段 ID,多对多关系需要使用新的数据表来管理两张表的数据关系。在模型中可以通过 ManyToManyField 来构建数据表的多对多关系,代码如下:
class Performer(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
nationality = models.CharField(max_length=20)
class Program(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
performer = models.ManyToManyField(Performer)
数据表之间创建多对多关系时,只需在项目里定义两个模型对象即可,在执行数据迁移时,Django 自动生成3张数据表来建立多对多关系,如下图所示:
综上所述,模型之间的关联是由 OneToOneField、ForeignKey 和 ManyToManyField 外键字段实现的,每个字段设有特殊的参数,参数说明如下:
- to:必选参数,关联的模型名称。
- on_delete:必选参数,设置数据的删除模式,删除模型包括:CASCADE、PROTECT、SET_NULL、SET_DEFAULT、SET和DO_NOTHING。
- CASCADE 级联,删除主表数据时连通一起删除外键表中数据
- PROTECT 保护,通过抛出 ProtectedError 异常,来阻止删除主表中被外键应用的数据
- SET_NULL 设置为 NULL,仅在该字段 null=True 允许为 null 时可用
- SET_DEFAULT 设置为默认值,仅在该字段设置了默认值时可用
- SET() 设置为特定值或者调用特定方法
- DO_NOTHING 不做任何操作,如果数据库前置指明级联性,此选项会抛出 IntegrityError 异常
- limit_choices_to:设置外键的下拉框选项,用于模型表单和 Admin 后台系统。
- related_name:用于模型之间的关联查询,如反向查询。
- related_query_name:设置模型的查询名称,用于 filter 或 get 查询,若设置参数 related_name,则以该参数为默认值,若没有设置,则以模型名称的小写为默认值。
- to_field:设置外键与其他模型字段的关联性,默认关联主键,若要关联其他字段,则该字段必须具有唯一性。
- db_constraint:在数据库里是否创建外键约束,默认值为 True。
- swappable:设置关联模型的替换功能,默认值为 True,比如模型A关联模型B,想让模型C继承并替换模型B,使得模型A与模型C之间关联。
- symmetrical:仅限于 ManyToManyField,设置多对多字段之间的对称模式。
- through:仅限于 ManyToManyField,设置自定义模型C,用于管理和创建模型A和B的多对多关系。
- through_fields:仅限于 ManyToManyField,设置模型C的字段,确认模型C的哪些字段用于管理模型A和B的多对多关系。
- db_table:仅限于 ManyToManyField,为管理和存储多对多关系的数据表设置表名称。
四、数据表操作
本小节讲述如何使用 ORM 框架实现数据新增、修改、删除、查询、执行 SQL 语句和实现数据库事务等操作,具体说明如下:
- 数据新增:由模型实例化对象调用内置方法实现数据新增,比如单数据新增调用 create,查询与新增调用 get_or_create,修改与新增调用 update_or_create,批量新增调用 bulk_create。
- 数据修改必须执行一次数据查询,再对查询结果进行修改操作,常用方法有:模型实例化、update 方法和批量更新 bulk_update。
- 数据删除必须执行一次数据查询,再对查询结果进行删除操作,若删除的数据设有外键字段,则删除结果由外键的删除模式决定。
- 数据查询分为单表查询和多表查询,Django 提供多种不同查询的 API 方法,以满足开发需求。
- 执行 SQL 语句有3种方法实现:extra、raw 和 execute,其中 extra 和 raw 只能实现数据查询,具有一定的局限性;而 execute 无须经过 ORM 框架处理,能够执行所有 SQL 语句,但很容易受到 SQL 注入攻击。
- 数据库事务是指作为单个逻辑执行的一系列操作,这些操作具有原子性,即这些操作要么完全执行,要么完全不执行,常用于银行转账和火车票抢购等。
4.1 Shell工具
Django 的 manage 工具提供了 shell 命令,帮助我们配置好当前工程的运行环境(如连接好数据库等),以便可以直接在终端中执行测试 python 语句,通过如下命令进入 shell:
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py shell
Python 3.12.7 | packaged by Anaconda, Inc. | (main, Oct 4 2024, 13:17:27) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.29.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: # 导入两个模型类,以便后续使用
In [2]: from chapter03_Model.models import BookInfo, PeopleInfo
这样可以更好地演示数据库的增、删、改操作,该模式非常方便开发人员开发和调试程序。
4.2 数据新增
Django 对数据库的数据进行增、删、改操作是借助内置 ORM 框架所提供的 API 方法实现的,简单来说,ORM 框架的数据操作 API 是在 QuerySet 类里面定义的,然后由开发者自定义的模型对象调用 QuerySet 类,从而实现数据操作。
先在 Navicat 中添加测试数据:
insert into book_info(book_name, pub_date, read_count,comment_count, is_delete) values
('射雕英雄传', '1980-5-1', 12, 34, 0),
('天龙八部', '1986-7-24', 36, 40, 0),
('笑傲江湖', '1995-12-24', 20, 80, 0),
('雪山飞狐', '1987-11-11', 58, 24, 0);
select * from book_info;
insert into people_info(name, gender, book_id, description, is_delete,age) values
('郭靖', 1, 1, '降龙十八掌', 0,25),
('黄蓉', 0, 1, '打狗棍法', 0,23),
('黄药师', 1, 1, '弹指神通', 0,45),
('欧阳锋', 1, 1, '蛤蟆功', 0,47),
('梅超风', 0, 1, '九阴白骨爪', 0,33),
('乔峰', 1, 2, '降龙十八掌', 0,37),
('段誉', 1, 2, '六脉神剑', 0,28),
('虚竹', 1, 2, '天山六阳掌', 0,32),
('王语嫣', 0, 2, '神仙姐姐', 0,25),
('令狐冲', 1, 3, '独孤九剑', 0,19),
('任盈盈', 0, 3, '弹琴', 0,18),
('岳不群', 1, 3, '华山剑法', 0,30),
('东方不败', 0, 3, '葵花宝典', 0,33),
('胡斐', 1, 4, '胡家刀法', 0,28),
('苗若兰', 0, 4, '黄衣', 0,26),
('程灵素', 0, 4, '医术', 0,20),
('袁紫衣', 0, 4, '六合拳', 0,20);
在 Shell 模式下,若想对数据表 people_info 插入数据,则可输入以下代码实现:
In [11]: p1 = PeopleInfo()
In [12]: p1.name='洪七公'
In [13]: p1.gender=1
In [14]: p1.book_id=1
In [15]: p1.description='降龙十八掌'
In [16]: p1.is_delete=False
In [17]: p1.age=48
In [18]: p1.save()
上述代码是对模型 PeopleInfo 进行实例化,再对实例化对象的属性进行赋值,从而实现数据表 people_info 的数据插入,代码说明如下:
- 从项目子应用 chapter03_Model 的 models.py 文件中导入模型 PeopleInfo
- 对模型 PeopleInfo 声明并实例化,生成对象 p1
- 对对象 p1 的属性进行逐一赋值,对象 p1 的属性来自于模型 PeopleInfo 所定义的字段。完成赋值后,再由对象 p1 调用 save 方法进行数据保存
需要注意的是,模型 PeopleInfo 的外键命名为 book,但在数据表 people_info 中变为 book_id,因此对象 p1 设置外键字段 book 的时候,外键字段应以数据表的字段名为准。上述代码运行结束后,在数据表 people_info 里查看数据的插入情况,如下图所示:
除了上述方法外,数据插入还有以下3种常见方法,代码如下:
In [19]: from chapter03_Model.models import BookInfo, PeopleInfo
In [20]: # 方法一: 使用create方法实现数据插入
In [21]: p = BookInfo.objects.create(book_name='红楼梦',pub_date='1978-03-05',
read_count=50,comment_count=70,is_delete=0)
In [22]: # 数据新增后,获取新增数据的主键id
In [23]: p.id
Out[23]: 5
In [24]: # 方法二: 同样使用create方法,但数据以字典格式表示
In [25]: d = dict(book_name='西游记',
pub_date='1988-05-03',read_count=150,comment_count=90,is_delete=0)
In [26]: p = BookInfo.objects.create(**d)
In [27]: # 数据新增后,获取新增数据的主键id
In [28]: p.id
Out[28]: 6
In [29]: # 方法三: 在实例化时直接设置属性值
In [30]: p = BookInfo(book_name='水浒传',pub_date='1988-10-23',read_count=45,comment_count=38,is_delete=0)
In [31]: p.save()
In [32]: # 数据新增后,获取新增数据的主键id
In [33]: p.id
Out[33]: 7
执行数据插入时,为了保证数据的有效性,我们需要对数据进行去重判断,确保数据不会重复插入。以往的方案都是对数据表进行查询操作,如果查询的数据不存在,就执行数据插入操作。为了简化这一过程,Django 提供了 get_or_create 方法,使用如下:
In [34]: d = dict(book_name='西游记',pub_date='1988-05-03',read_count=150,comment_count=90,is_delete=0)
In [35]: p = BookInfo.objects.get_or_create(**d)
In [39]: p
Out[39]: (<BookInfo: 西游记>, False)
In [40]: d = dict(book_name='西游记1',pub_date='1988-05-03',read_count=150,comment_count=90,is_delete=0)
In [41]: p = BookInfo.objects.get_or_create(**d)
In [42]: p
Out[42]: (<BookInfo: 西游记1>, True)
get_or_create 根据每个模型字段的值与数据表的数据进行判断,判断方式如下:
- 只要有一个模型字段的值与数据表的数据不相同(除主键之外),就会执行数据插入操作。
- 如果每个模型字段的值与数据表的某行数据完全相同,就不执行数据插入,而是返回这行数据的数据对象。若执行结果显示为 False,则数据表已存在数据,不再执行数据插入,若执行结果显示为 True,则代表数据插入。
除了 get_or_create 之外,Django 还定义了 update_or_create 方法,这是判断当前数据在数据表里是否存在,若存在,则进行更新操作,否则在数据表里新增数据,使用说明如下:
In [43]: # 第一次是新增数据
In [44]: d = dict(book_name='三国演义',pub_date='1985-11-15',read_count=800,comment_count=520,is_delete=0)
In [45]: p = BookInfo.objects.update_or_create(**d)
In [46]: p
Out[46]: (<BookInfo: 三国演义>, True)
In [47]: # 第二次是修改数据
In [48]: p = BookInfo.objects.update_or_create(**d, defaults={'book_name': '三国演义2'})
In [50]: p[0].book_name
Out[50]: '三国演义2'
update_or_create 是根据字典 d 的内容查找数据表的数据,如果能找到相匹配的数据,就执行数据修改,修改内容以字典格式传递给参数 defaults 即可;如果在数据表找不到匹配的数据,就将字典 d 的数据插入数据表里。如果要对某个模型执行数据批量插入操作,那么可以使用 bulk_create 方法实现,只需将数据对象以列表或元组的形式传入 bulk_create 方法即可:
In [55]: b1 = BookInfo(book_name='遮天',pub_date='2008-10-23',read_count=40,comment_count=30,is_delete=0)
In [56]: b2 = BookInfo(book_name='遮天2',pub_date='2012-01-10',read_count=68,comment_count=88,is_delete=0)
In [57]: book_list = [b1, b2]
In [58]: BookInfo.objects.bulk_create(book_list)
Out[58]: [<BookInfo: 遮天>, <BookInfo: 遮天2>]
在使用 bulk_create 之前,数据类型为模型 BookInfo 的实例化对象,并且在实例化过程中设置每个字段的值,最后将所有实例化对象放置在列表或元组里,以参数的形式传递给 bulk_create,从而实现数据的批量插入操作。
4.3 数据修改
数据修改的步骤与数据插入的步骤大致相同,唯一的区别在于数据对象来自数据表,因此需要执行一次数据查询,查询结果以对象的形式表示,并将对象的属性进行赋值处理,代码如下:
In [59]: b2 = BookInfo.objects.get(id=11)
In [60]: b2
Out[60]: <BookInfo: 遮天2>
In [61]: b2.read_count
Out[61]: 68
In [62]: b2.read_count=100
In [63]: b2.save()
上述代码获取数据表 people_info 里主键 id 等于 11 的数据对象 b2,然后修改数据对象 b2 的 read_count 属性,从而完成数据修改操作。打开数据表 people_info 查看数据修改情况,如下图所示:
除此之外,还可以使用 update 方法实现数据修改,使用方法如下:
In [64]: # 批量更新一条或多条数据,查询方法使用filter
In [65]: # filter以列表格式返回,查询结果可能是一条或多条数据
In [66]: BookInfo.objects.filter(id=11).update(book_name='遮天2-1')
Out[66]: 1
In [67]: # 更新数据以字典格式表示
In [68]: d = dict(book_name='遮天2-2')
In [69]: BookInfo.objects.filter(book_name='遮天2-1').update(**d)
Out[69]: 1
In [70]: # 不使用查询方法,默认对全表的数据进行更新
In [71]: BookInfo.objects.update(read_count=666)
Out[71]: 11
In [72]: # 使用内置F方法实现数据的自增或自减
In [73]: # F方法还可以在annotate或filter方法里使用
In [75]: from django.db.models import F
In [76]: b1 = BookInfo.objects.filter(id=11)
In [77]: # 将read_count字段原有的数据自增加一
In [78]: b1.update(read_count=F('read_count')+1)
Out[78]: 1
In [80]: print(b1)
<QuerySet [<BookInfo: 遮天2-2>]>
在 Django 2.2 或以上版本新增了数据批量更新方法 bulk_update,它的使用与批量新增方法 bulk_create 相似,使用说明如下:
# 新增两行数据
b1 = BookInfo.objects.create(book_name='xx1', pub_date='1978-03-05',
read_count=5, comment_count=7, is_delete=0)
b2 = BookInfo.objects.create(book_name='xx2', pub_date='1977-04-05',
read_count=3, comment_count=6, is_delete=0)
# 修改字段read_count和book_name的数据
b1.read_count=6
b2.book_name='xx3'
In [87]: # 批量修改字段read_count和book_name的数据
In [88]: BookInfo.objects.bulk_update([b1,b2], fields=['read_count','book_name'])
Out[88]: 2
4.4 数据删除
数据删除有3种方式:删除数据表的全部数据、删除一行数据和删除多行数据,实现方式如下:
In [91]: # 删除一条id为13的数据
In [92]: BookInfo.objects.get(id=13).delete()
Out[92]: (1, {'chapter03_Model.BookInfo': 1})
In [93]: # 删除多条数据
In [94]: BookInfo.objects.filter(read_count=70).delete()
Out[94]: (0, {})
In [95]: BookInfo.objects.filter(comment_count=70).delete()
Out[95]: (2, {'chapter03_Model.BookInfo': 2})
In [96]: # 删除数据表中的全部数据
In [97]: BookInfo.objects.all().delete()
Out[97]: (21, {'chapter03_Model.PeopleInfo': 12, 'chapter03_Model.BookInfo': 9})
删除数据的过程中,如果删除的数据设有外键字段,就会同时删除外键关联的数据,因为我们建立模型的时候使用的是:
比如删除数据表 book_info 里主键等于 2 的数据(简称为数据A),在数据表 people_info 里,有些数据(简称为数据B)关联了数据A,那么在删除数据A时,也会同时删除数据B。
4.5 数据查询
首先将测试数据恢复至原样,如下图所示:
以数据表 book_info 和 people_info 为例,在 Django5Study 项目的 Shell 模式下使用 ORM 框架提供的 API 方法实现数据查询,代码如下:
In [1]: from chapter03_Model.models import *
# 全表查询
# SQL: Select * from book_info,数据以列表返回
In [2]: b1 = BookInfo.objects.all()
# 查询第一条数据,序列从0开始
In [3]: b1[0]
Out[3]: <BookInfo: 射雕英雄传>
# 查询前3条数据
# SQL: Select * from book_info LIMIT 3
# SQL语句的LIMIT方法,在Django中使用列表截取即可
In [4]: b_list = BookInfo.objects.all()[:3]
In [5]: print(b_list)
<QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 笑傲江湖>]>
# 查询某个字段
# SQL: Select book_name from book_info
# values方法,数据以列表返回,列表元素以字典表示
In [6]: b = BookInfo.objects.values('book_name')
In [7]: print(b)
In [8]: b[1].get('book_name')
Out[8]: '天龙八部'
# values_list方法,数据以列表返回,列表元素以元组表示
In [9]: BookInfo.objects.values_list('read_count')[:2]
Out[9]: <QuerySet [(12,), (36,)]>
# 使用get方法查询数据
# SQL: select * from book_info where id=2
In [10]: b1 = BookInfo.objects.get(id=1)
In [11]: b1.book_name
Out[11]: '射雕英雄传'
# 使用filter方法查询数据,注意区分get和filter的差异
In [12]: b2 = BookInfo.objects.filter(id=2)
In [13]: print(b2)
<QuerySet [<BookInfo: 天龙八部>]>
In [14]: print(b1)
射雕英雄传
In [15]: b2[0].book_name
Out[15]: '天龙八部'
# SQL的and查询主要在filter里面添加多个查询条件
In [17]: b = BookInfo.objects.filter(book_name='射雕英雄传', read_count=12)
In [18]: b
Out[18]: <QuerySet [<BookInfo: 射雕英雄传>]>
# filter的查询条件可设为字典格式
In [19]: d = dict(book_name='天龙八部', read_count=36)
In [20]: b = BookInfo.objects.filter(**d)
In [21]: b
Out[21]: <QuerySet [<BookInfo: 天龙八部>]>
# SQL的or查询,需要引入Q,编写格式:Q(field=value)|Q(field=value)
# 多个Q之间使用"|"隔开即可
# SQL: select * from book_info where id=1 or book_name='雪山飞狐'
>>> from django.db.models import Q
In [23]: b1 = BookInfo.objects.filter(Q(id=1)|Q(book_name='雪山飞狐'))
In [24]: b1
Out[24]: <QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 雪山飞狐>]>
# SQL的不等于查询,在Q查询前面使用"~"即可
# SQL语句: select * from book_info where not (id=3)
In [25]: b2 = BookInfo.objects.filter(~Q(id=3))
In [26]: b2
Out[26]: <QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 雪山飞狐>]>
# 还可以使用exclude实现不等于查询
In [27]: b3 = BookInfo.objects.exclude(id=3)
In [28]: b3
Out[28]: <QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>, <BookInfo: 雪山飞狐>]>
# 使用count方法统计查询数据的数据量
In [31]: BookInfo.objects.exclude(id=3).count()
Out[31]: 3
# 去重查询,distinct方法无须设置参数,去重方式根据values设置的字段执行
# SQL: select distinct gender from people_info;
In [32]: PeopleInfo.objects.values('gender').distinct()
Out[32]: <QuerySet [{'gender': 1}, {'gender': 0}]>
# 根据字段id降序排列,降序只要在order_by里面的字段前面加"-"即可
# order_by可设置多字段排列,如BookInfo.objects.order_by('-id', 'job')
In [43]: BookInfo.objects.order_by('-id').all()[:2]
Out[43]: <QuerySet [<BookInfo: 雪山飞狐>, <BookInfo: 笑傲江湖>]>
# 聚合查询,实现对数据值求和、求平均值等。由annotate和aggregate方法实现
# annotate类似于SQL里面的GROUP BY方法
#如果不设置values,默认对主键进行GROUP BY分组
# SQL: select gender,count(gender) as 'gender__count' from people_info group by gender
In [44]: from django.db.models import Sum, Count
In [50]: b = PeopleInfo.objects.values('gender').annotate(Count('gender'))
In [51]: print(b.query) # 返回查询的sql语句
# aggregate是计算某个字段的值并只返回计算结果
# SQL: select count(id) as 'id_count' from PeopleInfo
In [52]: b = PeopleInfo.objects.aggregate(count_id=Count('id'))
In [53]: b
Out[53]: {'count_id': 17}
# union、intersection和difference语法
# 每次查询结果的字段必须相同
# 第一次查询结果v1
In [55]: p1 = PeopleInfo.objects.filter(gender=1)
# 第二次查询结果v2
In [57]: p2 = PeopleInfo.objects.filter(gender=0)
# 使用SQL的UNION来组合两个或多个查询结果的并集
# 获取两次查询结果的并集
In [58]: p1.union(p2)
# 使用SQL的INTERSECT来获取两个或多个查询结果的交集
# 获取两次查询结果的交集
In [60]: p1 = PeopleInfo.objects.filter(gender=1)
In [61]: p2 = PeopleInfo.objects.filter(age__lt=26)
In [62]: p1.intersection(p2)
Out[62]: <QuerySet [<PeopleInfo: 郭靖>, <PeopleInfo: 令狐冲>]>
# 使用SQL的EXCEPT来获取两个或多个查询结果的差
# 以p1为目标数据,去除p1和p2的共同数据
In [63]: p1.difference(p2)
上述例子讲述了开发中常用的数据查询方法,但有时需要设置不同的查询条件来满足多方面的查询要求。上述的查询条件 filter 和 get 是使用等值的方法来匹配结果。若想使用大于、不等于或模糊查询的匹配方法,则可在查询条件 filter 和 get 里使用下表所示的匹配符实现:
综上所述,在查询数据时可以使用查询条件 get 或 filter 实现,但是两者的执行过程存在一定的差异,说明如下:
- 查询条件 get:查询字段必须是主键或者唯一约束的字段,并且查询的数据必须存在,如果查询的字段有重复值或者查询的数据不存在,程序就会抛出异常信息。
- 查询条件 filter:查询字段没有限制,只要该字段是数据表的某一字段即可。查询结果以列表形式返回,如果查询结果为空(查询的数据在数据表中找不到),就返回空列表。
4.6 多表查询
在日常的开发中,常常需要对多张数据表同时进行数据查询。多表查询需要在数据表之间建立表关系才能够实现。一对多或一对一的表关系是通过外键实现关联的,而多表查询分为正向查询和反向查询。
以模型 BookInfo 和 PeopleInfo 为例,模型 PeopleInfo 定义的外键字段 book 关联到模型 BookInfo。如果查询对象的主体是模型 PeopleInfo,通过外键字段 book 去查询模型 BookInfo 的关联数据,那么该查询称为正向查询;如果查询对象的主体是模型 BookInfo,要查询它与模型 PeopleInfo 的关联数据,那么该查询称为反向查询。无论是正向查询还是反向查询,两者的实现方法大致相同,代码如下:
# 正向查询,即: 多对应的模型类对象.多对应的模型类中的关系类属性名
# 查询模型PeopleInfo某行数据对象p1
In [26]: p1 = PeopleInfo.objects.get(id=1)
# p1.book代表外键book
In [27]: p1.book
Out[27]: <BookInfo: 射雕英雄传>
# 通过外键book去查询模型BookInfo所对应的数据
In [28]: p1.book.book_name, p1.book.id, p1.book.read_count
Out[28]: ('射雕英雄传', 1, 11)
# 访问一对应的模型类关联对象的id语法: 多对应的模型类对象.关联类属性_id
In [29]: p1 = PeopleInfo.objects.get(id=7)
In [30]: p1.book_id
Out[30]: 2
# 反向查询
In [1]: from chapter03_Model.models import *
In [3]: book = BookInfo.objects.get(id=2)
In [5]: # 反向查询即由一到多的访问语法: 一对应的模型类对象.多对应的模型类名小写_set
In [4]: book.peopleinfo_set.all()
Out[4]: <QuerySet [<PeopleInfo: 乔峰>, <PeopleInfo: 段誉>, <PeopleInfo: 虚竹>, <PeopleInfo: 王语嫣>]>
# 方法一
# peopleinfo_set的返回值为queryset对象,即查询结果
# peopleinfo_set的peopleinfo为模型PeopleInfo的名称小写
# 模型PeopleInfo的外键字段book不能设置参数related_name
# 若设置参数related_name,则无法使用peopleinfo_set
In [33]: b1 = BookInfo.objects.filter(id=1)
In [34]: b1.first().peopleinfo_set.first()
Out[34]: <PeopleInfo: 郭靖>
In [35]: b1.first().peopleinfo_set.first().name
Out[35]: '郭靖'
# 方法二
# 由模型PeopleInfo的外键字段book的参数related_name实现
# 外键字段book必须设置参数related_name才有效,否则无法查询
# 将外键字段book的参数related_name设为peopleinfo
In [1]: from chapter03_Model.models import *
In [2]: b1 = BookInfo.objects.filter(id=1)
In [3]: b1.first().peopleinfo_set.first().name
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[3], line 1
----> 1 b1.first().peopleinfo_set.first().name
AttributeError: 'BookInfo' object has no attribute 'peopleinfo_set'
In [4]: b1.first().peopleinfo.first().name
Out[4]: '郭靖'
正向查询和反向查询还能在查询条件(filter 或 get)里使用,这种方式用于查询条件的字段不在查询对象里,比如查询对象为模型 PeopleInfo,查询条件是模型 BookInfo 的某个字段,对于这种查询可以采用以下方法实现:
# 正向查询,一模型类关联属性名__一模型类属性名__条件运算符=值(由一模型类条件查询多模型类数据)
# book__book_name,前面的book是模型PeopleInfo的字段book
# 后面的book_name是模型BookInfo的字段book_name,两者使用双下画线连接
In [5]: p1 = PeopleInfo.objects.filter(book__book_name='天龙八部').first()
In [6]: p1.book
Out[6]: <BookInfo: 天龙八部>
# p1.book代表外键book
In [8]: p1.book.id, p1.book.read_count, p1.book.book_name
Out[8]: (2, 34, '天龙八部')
In [9]: p1 = PeopleInfo.objects.filter(book__book_name='天龙八部')
In [10]: p1
Out[10]: <QuerySet [<PeopleInfo: 乔峰>, <PeopleInfo: 段誉>, <PeopleInfo: 虚竹>, <PeopleInfo: 王语嫣>]>
# 反向查询: 关联模型类名小写__属性名__条件运算符=值(由多模型类条件查询一模型类数据)
# 通过外键book的参数related_name实现反向条件查询
# peopleinfo代表外键name的参数related_name
# description代表模型PeopleInfo的字段description
In [13]: # 查询图书,要求图书中人物的描述包含"八"
In [14]: book = BookInfo.objects.filter(peopleinfo__description__contains='八')
In [15]: book
Out[15]: <QuerySet [<BookInfo: 射雕英雄传>, <BookInfo: 天龙八部>]>
# 通过参数related_name反向获取模型PeopleInfo的数据
In [17]: p1 = book.first().peopleinfo.first()
In [18]: p1.gender, p1.age, p1.name
Out[18]: (1, 25, '郭靖')
无论是正向查询还是反向查询,它们在数据库里需要执行两次 SQL 查询,第一次是查询某张数据表的数据,再通过外键关联获取另一张数据表的数据信息。为了减少查询次数,提高查询效率,我们可以使用 select_related 或 prefetch_related 方法实现,该方法只需执行一次 SQL 查询就能实现多表查询。select_related 主要针对一对一和一对多关系进行优化,它是使用 SQL 的 JOIN 语句进行优化的,通过减少 SQL 查询的次数来进行优化和提高性能,其使用方法如下:
# select_related方法,参数为字符串格式
# 以模型BookInfo为查询对象
# select_related使用LEFT OUTER JOIN方式查询两个数据表
# 查询模型BookInfo的字段book_name和模型PeopleInfo的字段name
# select_related参数为peopleinfo,代表外键字段name的参数related_name
# 若要得到其他数据表的关联数据,则可用双下画线"__"连接字段名
# 双下画线"__"连接字段名必须是外键字段名或外键字段参数related_name
In [20]: p1 = BookInfo.objects.select_related('peopleinfo').values('book_name', 'peopleinfo__name')
In [21]: print(p1)
In [22]: p1.values()
# 查看SQL查询语句
In [25]: print(p1.query)
# SELECT `book_info`.`book_name`, `people_info`.`name` FROM `book_info` LEFT OUTER
# JOIN `people_info` ON (`book_info`.`id` = `people_info`.`book_id`)
# 以模型PeopleInfo为查询对象
# select_related使用INNER JOIN方式查询两个数据表
# select_related的参数为book,代表外键字段book
In [26]: p1 = PeopleInfo.objects.select_related('book').values('book', 'book__book_name')
In [27]: print(p1.query)
SELECT `people_info`.`book_id`, `book_info`.`book_name` FROM `people_info`
# INNER JOIN `book_info` ON (`people_info`.`book_id` = `book_info`.`id`)
# 获取两个模型的数据,以模型PeopleInfo的description中包含'八'为查询条件
In [29]: p1 = PeopleInfo.objects.select_related('book').filter(description__contains="八")
# 查看SQL查询语句
In [30]: print(p1.query)
# 获取查询结果集的首个元素的字段book_name的数据
# 通过外键字段book定位模型BookInfo的字段book_name
In [31]: p1[0].book.book_name
Out[31]: '射雕英雄传'
除此之外,select_related 还可以支持3个或3个以上的数据表同时查询,以下面的例子进行说明:
# D:\Code\dream\PythonStudy\Django5Study\chapter03_Model\models.py
# ①: 建立模型
# 省份信息表
class Province(models.Model):
name = models.CharField(max_length=10)
def __str__(self):
return str(self.name)
# 城市信息表
class City(models.Model):
name = models.CharField(max_length=5)
province = models.ForeignKey(Province, on_delete=models.CASCADE)
def __str__(self):
return str(self.name)
# 人物信息表
class Person(models.Model):
name = models.CharField(max_length=10)
living = models.ForeignKey(City, on_delete=models.CASCADE)
def __str__(self):
return str(self.name)
# ②: 执行迁移命令
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py makemigrations
Migrations for 'chapter03_Model':
chapter03_Model\migrations\0003_city_province_person_city_province.py
+ Create model City
+ Create model Province
+ Create model Person
+ Add field province to city
(django5_study) D:\Code\dream\PythonStudy\Django5Study>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, chapter03_Model, contenttypes, sessions
Running migrations:
Applying chapter03_Model.0003_city_province_person_city_province... OK
(django5_study) D:\Code\dream\PythonStudy\Django5Study>
# ③: 使用Navicat Premium 17执行如下sql语句添加测试数据
'''
INSERT into chapter03_model_province(id, name) values
(1, '广东省'), (2, '浙江省'), (3, '海南省')
INSERT into chapter03_model_city(id, name, province_id) values
(1, '广州', 1), (2, '苏州', 2), (3, '杭州', 2),(4, '海口', 3),(5, '深圳', 1)
INSERT into chapter03_model_person(id, name, living_id) values
(1, 'Lily', 1), (2, 'Tom', 2), (3, 'Lucy', 3),(4, 'Tim', 4),(5, 'Mary', 5)
'''
在上述模型中,模型 Person 通过外键 living 关联模型 City,模型 City 通过外键 province 关联模型 Province,从而使3个模型形成一种递进关系。表中数据如下:
mysql> use django5study
Database changed
mysql> select * from chapter03_model_province;
+----+--------+
| id | name |
+----+--------+
| 1 | 广东省 |
| 2 | 浙江省 |
| 3 | 海南省 |
+----+--------+
3 rows in set (0.00 sec)
mysql> select * from chapter03_model_city;
+----+------+-------------+
| id | name | province_id |
+----+------+-------------+
| 1 | 广州 | 1 |
| 2 | 苏州 | 2 |
| 3 | 杭州 | 2 |
| 4 | 海口 | 3 |
| 5 | 深圳 | 1 |
+----+------+-------------+
5 rows in set (0.00 sec)
mysql> select * from chapter03_model_person;
+----+------+-----------+
| id | name | living_id |
+----+------+-----------+
| 1 | Lily | 1 |
| 2 | Tom | 2 |
| 3 | Lucy | 3 |
| 4 | Tim | 4 |
| 5 | Mary | 5 |
+----+------+-----------+
5 rows in set (0.00 sec)
例如,查询 Tom 现在所居住的省份,首先通过模型 Person 和模型 City 查出 Tom 所居住的城市,然后通过模型 City 和模型 Province 查询当前城市所属的省份。因此,select_related 的实现方法如下:
In [1]: from chapter03_Model.models import *
In [2]: p = Person.objects.select_related('living__province').get(name='Tom')
In [3]: p.living.province
Out[3]: <Province: 浙江省>
In [4]: p.living.province.name
Out[4]: '浙江省'
从上述例子可以发现,通过设置 select_related 的参数值可实现3个或3个以上的多表查询。例子中的参数值为 living__province,参数值说明如下:
- living 是模型 Person 的外键字段,该字段指向模型 City
- province 是模型 City 的外键字段,该字段指向模型 Province
两个外键字段之间使用双下画线连接,在查询过程中,模型 Person 的外键字段 living 指向模型 City,再从模型 City 的外键字段 province 指向模型 Province,从而实现3个或3个以上的多表查询。
prefetch_related 和 select_related 的设计目的很相似,都是为了减少 SQL 查询的次数,但是实现的方式不一样。select_related 是由 SQL 的 JOIN 语句实现的,但是对于多对多关系,使用 select_related 会增加数据查询时间和内存占用;而 prefetch_related 是分别查询每张数据表,然后由 Python 语法来处理它们之间的关系,因此对于多对多关系的查询,prefetch_related 更有优势。定义模型 Performer 和 Program,分别代表人员信息和节目信息,然后对模型执行数据迁移,生成相应的数据表,模型定义以及添加测试数据如下:
# D:\Code\dream\PythonStudy\Django5Study\chapter03_Model\models.py
class Performer(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
nationality = models.CharField(max_length=20)
def __str__(self):
return str(self.name)
class Program(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=20)
performer = models.ManyToManyField(Performer)
def __str__(self):
return str(self.name)
# python manage.py makemigrations
# python manage.py migrate
# 添加测试数据:
In [3]: performer1 = Performer(id=1, name='Lily', nationality='USA')
...: performer2 = Performer(id=2, name='Lilei', nationality='CHINA')
...: performer3 = Performer(id=3, name='Tom', nationality='US')
...: performer4 = Performer(id=4, name='Hanmei', nationality='CHINA')
...: performer_list = [performer1, performer2, performer3, performer4]
...: Performer.objects.bulk_create(performer_list)
Out[3]: [<Performer: Lily>, <Performer: Lilei>, <Performer: Tom>, <Performer: Hanmei>]
In [4]: program1 = Program(id=1, name='喜洋洋')
...: program2 = Program(id=2, name='小猪佩奇')
...: program3 = Program(id=3, name='白雪公主')
...: program4 = Program(id=4, name='小王子')
...: program_list = [program1, program2, program3, program4]
...: Program.objects.bulk_create(program_list)
Out[4]: [<Program: 喜洋洋>, <Program: 小猪佩奇>, <Program: 白雪公主>, <Program: 小王子>]
# chapter03_model_program_performer
INSERT into chapter03_model_program_performer(program_id, performer_id) values
(1, 1), (1, 2), (1, 3), (1, 4)
例如,查询 "喜洋洋"
节目有多少个人员参与演出,首先从节目表 chapter03_model_program 里找出 "喜洋洋"
的数据信息,然后通过外键字段 performer 获取参与演出的人员信息,实现过程如下:
In [9]: # 查询模型Program的某行数据
In [10]: p = Program.objects.prefetch_related('performer').filter(name='喜洋洋').first()
In [11]: # 根据外键字段performer获取当前数据的多对多或一对多关系
In [12]: p.performer.all()
Out[12]: <QuerySet [<Performer: Lily>, <Performer: Lilei>, <Performer: Tom>, <Performer: Hanmei>]>
从上述例子看到,prefetch_related 的使用与 select_related 有一定的相似之处。如果是查询一对多关系的数据信息,那么两者皆可实现,但 select_related 的查询效率更佳。除此之外,Django 的 ORM 框架还提供很多 API 方法,可以满足开发中各种复杂的需求,由于篇幅有限,就不再一一介绍了,有兴趣的读者可在官网上查阅。
4.7 执行SQL语句
Django 在查询数据时,大多数查询都能使用 ORM 提供的 API 方法,但对于一些复杂的查询可能难以使用 ORM 的 API 方法实现,因此 Django 引入了 SQL 语句的执行方法,有以下3种实现方法。
- extra:结果集修改器,一种提供额外查询参数的机制。
- raw:执行原始 SQL 并返回模型实例对象。
- execute:直接执行自定义 SQL。
extra 适合用于 ORM 难以实现的查询条件,将查询条件使用原生 SQL 语法实现,此方法需要依靠模型对象,在某程度上可防止 SQL 注入。在 PyCharm 里打开 extra 源码,如下图所示:
它一共定义了6个参数,每个参数说明如下:
- select:添加新的查询字段,即新增并定义模型之外的字段。
- where:设置查询条件。
- params:如果 where 设置了字符串格式化 %s,那么该参数为 where 提供数值。
- tables:连接其他数据表,实现多表查询。
- order_by:设置数据的排序方式。
- select_params:如果 select 设置字符串格式化 %s,那么该参数为 select 提供数值。
上述参数都是可选参数,我们可根据实际情况选择所需的参数。以模型 PeopleInfo 为例,使用 extra 实现数据查询,代码如下:
# 查询字段name等于'郭靖'的数据
# params为where的%s提供数值
In [19]: PeopleInfo.objects.extra(where=['name=%s'], params=['郭靖'])
Out[19]: <QuerySet [<PeopleInfo: 郭靖>]>
# 新增查询字段age,select_params为select的%s提供数值
In [54]: p1 = PeopleInfo.objects.extra(select={'age': '%s'}, select_params=['age'])
In [55]: for _ in p1:
...: print(_.name)
...:
郭靖
黄蓉
黄药师
欧阳锋
梅超风
乔峰
段誉
虚竹
王语嫣
令狐冲
任盈盈
岳不群
东方不败
胡斐
苗若兰
程灵素
袁紫衣
# 连接数据表book_info
In [61]: b1 = BookInfo.objects.extra(tables=['book_info'])
In [62]: for _ in b1:
...: print(_.read_count, _.book_name)
...:
11 射雕英雄传
34 天龙八部
18 笑傲江湖
58 雪山飞狐
下一步分析 raw 的语法,它和 extra 所实现的功能是相同的,只能实现数据查询操作,并且也要依靠模型对象,但从使用角度来说,raw 更为直观易懂。在 PyCharm 里打开 raw 源码,如下图所示:
它一共定义了4个参数,每个参数说明如下:
- raw_query:SQL 语句
- params:如果 raw_query 设置字符串格式化 %s,那么该参数为 raw_query 提供数值
- translations:为查询的字段设置别名
- using:数据库对象,即 Django 所连接的数据库
上述参数只有 raw_query 是必选参数,其他参数可根据需求自行选择。我们以模型 PeopleInfo 为例,使用 raw 实现数据查询,代码如下:
# FieldDoesNotExist: Raw query must include the primary key
In [66]: b = BookInfo.objects.raw('select id, book_name, read_count from book_info')
In [67]: b[0]
Out[67]: <BookInfo: 射雕英雄传>
最后分析 execute 的语法,它执行 SQL 语句无须经过 Django 的 ORM 框架。我们知道 Django 连接数据库需要借助第三方模块实现连接过程,如 MySQL 的 mysqlclient 模块和 SQLite 的 sqlite3 模块等,这些模块连接数据库之后,可通过游标的方式来执行 SQL 语句,而 execute 就是使用这种方式执行 SQL 语句,使用方法如下:
In [68]: from django.db import connection
In [69]: cursor = connection.cursor()
In [70]: # 执行sql语句
In [71]: cursor.execute('select id,book_name from book_info;')
Out[71]: 4
In [72]: # 读取第一行数据
In [73]: cursor.fetchone()
Out[73]: (1, '射雕英雄传')
In [74]: # 读取所有的数据
In [75]: cursor.fetchall()
Out[75]: ((2, '天龙八部'), (3, '笑傲江湖'), (4, '雪山飞狐'))
execute 能够执行所有的 SQL 语句,但很容易受到 SQL 注入攻击,一般情况下不建议使用这种方式实现数据操作。尽管如此,它能补全 ORM 框架所缺失的功能,如执行数据库的存储过程。
4.8 数据库事务
事务是指作为单个逻辑执行的一系列操作,这些操作具有原子性,即这些操作要么完全执行,要么完全不执行。事务处理可以确保事务性单元内的所有操作都成功完成,否则不会执行数据操作。事务应该具有4个属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),这4个属性通常称为 ACID 特性,说明如下:
- 原子性:一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
- 一致性:事务必须使数据库从某个一致性状态变到另一个一致性状态,一致性与原子性是密切相关的。
- 隔离性:一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他事务是隔离的,各个事务之间不能互相干扰。
- 持久性:持久性也称永久性(Permanence),指一个事务一旦提交,它对数据库中数据的改变应该是永久性的,其他操作或故障不应该对其有任何影响。
事务在日常开发中经常使用,比如银行转账、火车票抢购等。以银行转账为例,假定A账户目前有100元,B账户向A账户转账100元。在这个转账的过程中,必须保证A账户的资金增加100元,B账户的资金减少100元,如果刚完成A账户增加100元的操作,系统就发生瘫痪而无法执行B账户减少100元的操作,这时A账户就凭空多出100元。为了解决这种问题,这个转账过程需由事务完成,说明如下:
- 原子性和一致性:B账户减少100元后,A账户增加100元。假使交易途中发生故障,B账户不应减少100元,A账户也不应增加100元。
- 隔离性:如果B账户执行两次转账,应有先后次序,两次交易不可在原来同一个余额上重复执行,以确保交易后A和B账户的余额正确。
- 持久性:交易记录应在交易完成后永久记录。
Django 的事务定义在 transaction.py 文件中,在 PyCharm 里打开该文件并分析其定义的函数方法,如下图所示:
从 transaction.py 文件发现,该文件共定义了两个类和16个函数方法,而在开发中常用的函数方法如下:
- atomic():在视图函数或视图类里使用事务。
- savepoint():开启事务。
- savepoint_rollback():回滚事务。
- savepoint_commit():提交事务。
以 Django5Study 为例,将模型 BookInfo 作为事务的操作对象,分别在 Django5Study 中的 urls.py、chapter03_Model 中的 views.py 中定义路由信息和视图函数,代码如下:
# D:\Code\dream\PythonStudy\Django5Study\Django5Study\urls.py
from chapter03_Model.views import chapter03_model
urlpatterns = [
path("chapter03_Model/", chapter03_model, name='chapter03_Model'),
]
# D:\Code\dream\PythonStudy\Django5Study\chapter03_Model\views.py
from django.shortcuts import render
from .models import *
from django.db import transaction
from django.db.models import F
@transaction.atomic
def chapter03_model(request):
# 开启事务保护
sid = transaction.savepoint()
try:
_id = request.GET.get('id', '')
# print(type(_id))
if _id:
b = BookInfo.objects.filter(id=int(_id))
b.update(read_count=F('read_count') + 1)
print('Done')
# 提交事务
# 如果不设置,当程序执行完成后,会自动提交事务
transaction.savepoint_commit(sid)
else:
# 全表的read_count字段减1
BookInfo.objects.update(read_count=F('read_count') - 1)
# 事务回滚,将全表read_count字段减1的操作撤回
transaction.savepoint_rollback(sid)
except Exception as e:
# 事务回滚
transaction.savepoint_rollback(sid)
return render(request, 'chapter03_Model.html', locals())
上述代码的视图函数 chapter03_model 是通过事务来操作模型 BookInfo 的,函数的执行过程说明如下:
- 视图函数使用装饰器 @transaction.atomic,使函数支持事务操作。
- 在开始事务操作之前,必须使用 savepoint 方法来创建一个事务对象,便于 Django 的识别和管理。
- 事务操作引入 try…except 机制,如果在执行过程中发生异常,就执行事务回滚,使事务里所有的数据操作无效,确保数据的一致性。
- 在 try 模块里,首先获取请求参数 id,如果存在请求参数 id,就根据请求参数 id 的值去查询模型 BookInfo 的数据,并对字段 read_count 执行自增1操作;如果请求参数 id 不存在,就将模型 BookInfo 的所有数据执行自减1操作和事务回滚。
如果没有事务机制,那么当请求参数 id 不存在时,模型 BookInfo 的所有数据完成自减1操作后,数据表立即能看到操作结果;而引入事务机制后,由于在自减1操作后设置了事务回滚,因此程序执行完成后,数据表的数据不会发生改变。除了在视图函数中使用装饰器 @transaction.atomic 之外,还可以在视图函数中使用 with 模块实现事务操作,代码如下:
from django.db import transaction
def chapter03_model(request):
pass
# with模块下的代码可支持事务操作
with transaction.atomic():
pass
运行 Django5Study 项目,分别访问 http://127.0.0.1:8000/chapter03_Model/ 和 http://127.0.0.1:8000/chapter03_Model/?id=1,每次访问分别查看数据表 book_info 的数据变化情况,这样有助于深入了解事务机制的运行过程。