Python: 数据库

配置 python 开发环境和编程思想入门

主要内容:

关系模型和SQL语句

主要内容:

Python 关系数据访问编程

主要内容:

数据库

数据库: 按照数据结构来组织、存储、管理数据的仓库

诞生

计算机的发明是为了做科学计算的,而科学计算需要大量的数据输入和输出。

早期,可以使用打孔卡片的孔、灯泡的亮灭来表示数据输入、输出数据。

1940年,数据可以存储在磁带上,顺序的读取、写入磁带。

1956年IBM发明了磁盘驱动器这个革命性产品,支持随机访问。

随着信息化时代的到来,有了硬件存储技术的发展,有大量的数据需要存储和管理。

数据库DBMS发展

  • 萌芽期:文件管理
  • 第一代:层次数据库、网状数据库
  • 第二代:SQL、关系型数据库
  • 第三代:面向对象的DBMS(OODBMS)、对象关系的DBMS(ORDBMS)

文件系统管理

  • 磁盘上一个个文件,数据孤立,数据冗余
  • 格式不统一,很难统一管理
  • 无法高效查询,无法灵活查询

层次数据库

  • 以树型结构表示实体及其之间的联系。关系只支持一对多
  • 代表数据库IBM IMS

网状数据库

通用电气最早在1964年开发出网状数据库IDS,只能运行在GE自家的主机上。

结点描述数据,结点的联系就是数据的关系。

能够直接描述客观世界,可以表示实体间多种复杂关系,而这是层次数据模型无法做到的。比如,一个结点可以有多个父结点,结点之间支持可以多对多关联

关系数据库

使用行、列组成的二维表来组织数据和关系,表中行(记录)即可以描述数据实体,也可以描述实体间关系。

关系模型比网状模型、层次模型更简单,不需要关系数存储的物理细节,专心于数据的逻辑构建,而且关系模型有论文的严格的数学理论基础支撑。

1970年6月,IBM的研究员E.F.Codd发表了名为“A Relational Model of Data for Large Shared Data Banks”的论文,提出了关系模型的概念,奠定了关系模型的理论基础。

1976年,IBM实验室System R项目,通过实现数据结构和操作来证明关系模型实用性,并直接产生了结构化查询语言SQL。1987年,SQL被ISO组织标准化。

关系模型,有严格的数学基础,抽象级别较高,简单清晰,便于理解和使用。

  • 经过几十年的发展,关系数据库百花齐放,技术日臻成熟和完善。
  • 基于关系模型构建的数据库系统称为RDBMS(Relational DataBase System)。
  • BM DB2、Oracle的Oracle和Mysql、微软的MS SQL。以前的Infomix、Sybase等。
Oracle的发展

拉里·埃里森(Larry Ellison)仔细阅读了IBM的关系数据库的论文,敏锐意识到 在这个研究基础上可以开发商用软件系统。他们几个创始人决定开发通用商用数 据库系统Oracle,这个名字来源于他们曾给中央情报局做过的项目名。

1979年发布了ORACLE 2.0版本(实际上是1.0)。1983年,Oracle v3。1984年, Oracle v4。1985年,Oraclev5。1988年,Oracle v6引入了行级锁等新技术,然 而这是个不稳定的版本。直到1992年的时候,Oracle7才逐渐稳定下来,并取得 巨大成功。2001年的9i版本被广泛应用。

2009年4月20日,甲骨文公司宣布将以每股9.50美元,总计74亿美金收购SUN(计算机系统)公司。2010年1月成功收购。

2013年,甲骨文超过IBM,成为继微软之后的全球第二大软件公司。

Mysql发展

1985年几个瑞典人为大型零售商的项目设计了一种利用索引顺序存取数据的软件, 这就是MyISAM的前身。1996年,MySQL 1.0发布,随后发布了3.11.1版本,并开 始往其它平台移植。2000年MySQL采用GPL协议开源。

MySQL 4.0开始支持MyISAM、InnoDB引擎。2005年10月,MySQL 5.0成为里程碑版本。

2008年1月被Sun公司收购。

2009年1月,在Oracle收购MySQL之前,Monty Widenius担心收购,就从MySQL Server 5.5开始一条新的GPL分支,起名MariaDB。

MySQL的引擎是插件化的,可以支持很多种引擎:

  • MyISASM,不支持事务,插入、查询速度快。
  • InnoDB,支持事务,行级锁,MySQL 5.5起的默认引擎。
去IOE

它是阿里巴巴造出的概念。其本意是,在阿里巴巴的IT架构中,去掉IBM的小型 机、Oracle数据库、EMC存储设备,取而代之使用自己在开源软件基础上开发的 系统。传统上,一个高端大气的数据中心,IBM小型机、Oracle数据库、EMC存储 设备,可以说缺一不可。而使用这些架构的企业,不但采购、维护成本极高,核 心架构还掌握在他人手中。

对于阿里巴巴这样大规模的互联网应用,应该采用开源、开放的系统架构。这种 思路并不是阿里巴巴的新发明,国外的谷歌、Facebook、亚马逊等早已为之。只 不过它们几乎一开始就有没有采用IT商业公司的架构,所以他们也不用“去IOE”。

去IOE,转而使用廉价的架构,稳定性一定下降,需要较高的运维水平解决。

NoSQL

NoSQL是对非SQL、非传统关系型数据库的统称。

NoSQL一词诞生于1998年,2009年这个词汇被再次提出指非关系型、分布式、不提供ACID的数据库设计模式

随着互联网时代的到来,数据爆发式增长,数据库技术发展日新月异,要适应新的业务需求。

随着移动互联网、物联网的到来,大数据的技术中NoSQL也同样重要。

数据库流行度排名: https://db-engines.com/en/ranking

关系模型和SQL

为了介绍关系模型,以MySQL数据库为例

安装

MariaDB 安装

yum list | grep mariadb

#安装mariadb 服务,会自动安装mairadb
yum install mariadb-server
systemctl start mariadb.service

ss -tanl

State       Recv-Q  Send-Q Local Address:Port    Peer Address:Port
LISTEN      0       50              *:3306              *:*

#开机启动
systemctl enable mariadb.service

初始化脚本提高安全性

# mysql_secure_installation
设置数据管理员root口令
禁止root远程登录
删除anonymous用户帐号
删除test数据库

Docker 安装 MySQL

docker pull mysql
docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest
数据库密码登录
mysql -u root -p

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

#创建并授权用户
mysql> grant all on *.* to 'tom'@'%' identified by 'tom';
mysql> flush privileges;

#导入测试脚本 testsql文件
# mysql -u root -p < test.sql

SQL语句

SQL是结构化查询语言Structured Query Language。1987年被ISO组织标准化。

所有主流的关系型数据库都支持SQL,NoSQL也有很大一部分支持SQL。

SQL语句分为

  • DDL: Data Defination Language 数据定义语言,定义或修改数据库对象(如表、索引、视图等)的结构。关键字:
    • CREATE(创建数据库或表)
    • ALTER(修改表结构)
    • DROP(删除数据库或表)
    • TRUNCATE(快速清空表数据并重置结构)
  • DML: Data Manipulation Language 数据操纵语言,对表中的数据进行增、删、改操作。关键字:
    • INSERT(插入数据)
    • UPDATE(更新数据)
    • DELETE(删除数据)
  • DQL:Data Query Language 数据查询语言,从数据库中检索数据,是SQL的核心部分。关键字:
    • SELECT(查询数据)。特点:支持单表查询、多表连接(如内连接、外连接)、聚合函数(SUM、COUNT等)、分组(GROUP BY)和条件筛选(HAVING)
  • DCL:Data Control Language 数据控制语言,管理数据库权限和事务控制。关键字:
    • GRANT(授予权限)
    • REVOKE(撤销权限)
    • COMMIT(提交事务)
    • ROLLBACK(回滚事务)
  • TCL: Transaction Control Language 事务控制语言,负责处理ACID事务,确保数据库操作的原子性和一致性。关键字:
    • BEGIN TRANSACTION(开始事务)
    • SAVEPOINT(设置保存点)
    • COMMIT(提交事务)
    • ROLLBACK(回滚到保存点或事务起点)

SQL的CRUD(Create-Read-Update-Delete),即 Insert,update,delete,select 四条语句

语言规范

  • SQL语句大小写不敏感
    • 一般建议,SQL的关键字、函数等大写
  • SQL语句末尾应该使用分号结束
  • 注释
    • 多行注释 /*注释内容*/
    • 单行注释 -- 注释内容
    • MySQL 注释可以使用 #
  • 使用空格或缩进来提高可读性
  • 命名规范
    • 必须以字母开头
    • 可以使用数字、#、$和_
    • 不可使用关键字

DCL

GRANT授权、REVOKE撤销

GRANT ALL ON employees.* TO 'tom'@'%' IDENTIFIED by 'tom';
REVOKE ALL ON *.* FROM tom;
*为通配符,指代任意库或者任意表。
*.* 所有库的所有表; employees.* 表示employees库下所有的表
% 为通配符,它是SQL语句的通配符,匹配任意长度字符串

DDL

删除用户(慎用)

DROP USER tom;

创建数据库

库是数据的集合,所有数据按照数据模型组织在数据库中

CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8;
CHARACTER SET指定字符集
utf8mb4是utf8的扩展,支持4字节utf8mb4,需要MySQL5.5.3+
COLLATE指定字符集的校对规则,用来做字符串的比较的。例如a、A谁大?

删除数据库

DROP DATABASE IF EXISTS test;

创建表

  • 表分为行和列,MySQL是行存数据库。数据是一行行存的,列必须固定多少列。
  • 行Row,也称为记录Record,元组
  • 列Column,也称为字段Field、属性
  • 字段的取值范围叫做 域Domain。例如gender字段的取值就是M或者F两个值
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` enum('M','F') NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

反引号标注的名称,会被认为是非关键字,使用反引号避免冲突

DESC

查看列信息 {DESCRIBE | DESC} tbl_name [col_name | wild]

DESC employees;
DESC employees '';

练习 设计一张表,记录登录账户的注册信息,应该存储用户的姓名、登录名、密码

DROP DATABASE IF EXISTS test;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE `reg` (
  `id` int(11) NOT NULL,
  `loginname` varchar(50) NOT NULL,
  `name` varchar(64) DEFAULT NULL,
  `password` varchar(128) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `loginname` (`loginname`)
) ENGINE=InnoDB;

关系

在关系数据库中,关系就是二维表,由行和列组成。

行Row,也称为记录Record,元组。

列Column,也称为字段Field、属性。

字段的取值范围叫做 域Domain。例如gender字段的取值就是M或者F两个值

维数:关系的维数指关系中属性的个数。

基数:元组的个数。

注意在关系中,属性的顺序并不重要。理论上,元组顺序也不重要,但是由于元组顺序与存储相关,会影响查询效率。

候选键

关系中,能唯一标识一条元组的属性或属性集合,称为候选键。

候选键,表中一列或者多列组成的唯一key,通过这一个或者多个列能唯一的标识一条记录。

表中可能有多个候选键。

id name  loginname age
1  tom   tom       20
2  jerry jerry     30
3  tom   tom1      20

loginname 不能为null,唯一键

id可以是候选键,loginname可以是候选键

PRIMARY KEY主键

从候选键中选择出主键。

主键的列不能包含空值null。主键往往设置为整型、长整型,可以为自增AUTO_INCREMENT字段。

表中可以没有主键,但是,一般表设计中,往往都会有主键,以避免记录重复。

InnoDB的表要求使用主键。

Foreign KEY外键

严格来说,当一个关系中的某个属性或属性集合与另一个关系(也可以是自身)的候选键匹配时,就称作这个属性或属性集合是外键。

id name  loginname age  dept_id
1  tom   tom       20   1
2  jerry jerry     30   
3  tom   tom1      20


dept 表
id name
1  it

#一张表用了别的表的主键,那个键就外键。 dept_id

索引Index

可以看做是一本字典的目录,为了快速检索用的。空间换时间,显著提高查询效率。

可以对一列或者多列字段设定索引。

  • 主键索引,主键会自动建立主键索引,主键本身就是为了快速定位唯一记录的。
  • 唯一索引,表中的索引列组成的索引必须唯一,但可以为空,非空值必须唯一。
  • 普通索引,没有唯一性的要求,就是建了一个字典的目录而已。
  • 联合索引,多个字段组合创建索引,使用条件查询时,先匹配左边字段。
  • 全文索引,MyISAM使用,对char, varchar, text类型使用
  • 空间索引,SPATIAL,基本不用。

在MySQL中,InnoDB和MyISAM的索引数据结构可以使用hash和BTree, innoDB默认是BTree。

Hash的时间复杂度是O(1),但是只能进行精确匹配,也就是Hash值的匹配,比如范围匹配就没办法了, hash值无序所以无法知道原有记录的顺序。Hash问题较多。

BTree索引,以B+树为存储结构。

虽然,索引可以提高查询,但却影响增删改的效率,因为需要索引更新或重构。频繁出现在where子句中的列可以考虑 使用索引。要避免把性别这种字段设索引。

B+树

           |15         |        | 56   |           |  77 |  | 
           
    |15   |     |  20   |      | 49   |    |   | 

|15   | 18  | -> |20   | 30   | -> |49   | 50  | ->
|data | data|    |data | data |    |data | data|

B+树节点组织成一颗树。节点分为内部节点和叶子节点。

内部节点不存储数据,叶子节点不存储指针。

每个left node保存数据,所有的left node组织成链表。假设读取16到22的数据,找到18后,顺着链表往后遍历读取即可。

InnoDB中,数据文件本身就是按主键索引存储的,叶子节点中保存的就是数据记录。

如果在其他字段上定义B+Tree索引,叶子节点的数据记录的是主键,这种称为辅助索引。

约束Constraint

为了保证数据的完整正确,数据模型还必须支持完整性约束

  • “必须有值”约束
  • 某些列的值必须有值,不许为空NULL

域约束Domain Constraint

限定了表中字段的取值范围

实体完整性Entity Integrity

PRIMARY KEY约束定义了主键,就定义了主键约束。主键不重复且唯一,不能为空

引用完整性Referential Integrity *

外键定义中,可以不是引用另一张表的主键,但是,往往实际只会关注引用主键

外键:在表B中的列,引用了表A中的主键,表B中的列就是外键

A表称为主表,B表称为从表

插入规则

  • 不需要指定
  • 如果在表B插入一条记录,B的外键列插入了一个值,这个值必须是表A中存在的主键值

更新规则

  • 定义外键约束时指定该规则

删除规则

  • 定义外键约束时指定该规则

外键约束的操作

设定值 说明
CASCADE 级联,从父表删除或更新会自动删除或更新子表中匹配的行
SET NULL 从父表删除或更新行,会设置子表中的外键列为NULL,但必须保证子表列没有指定NOT NULL,也就是说子表的字段可以为NULL才行
RESTRICT 如果从父表删除主键,如果子表引用了,则拒绝对父表的删除或更新操作
NO ACTION 标准SQL的关键字,在MySQL中与RESTRICT相同。拒绝对父表的删除或更新操作

外键约束,是为了保证数据完整性、一致性,杜绝数冗余、数据错误。

范例:

emp 从表
id name  loginname age  dept_id deleted 外键引用,参考、参照
1  tom   tom       20   1       1
2  jerry jerry     30   1       0
3  tom   tom1      20   null       
15 ben   ben            3
18 sam   sam            4        外键约束,删除这条记录
20 adam  adam      18   6        这条不可能成功。从表新增数据约束,天生就有,要求必须参照主表的主键的值


dept 主表
id name
1  人事部
2  市场部
3  IT部    set null设置从表引用的字段为null
4  售后部
5  运维部

级联删除:真删除
dept 主表
id name    deleted
1  人事部   0
2  市场部   0 
3  IT部     1
4  售后部   0
5  运维部   0

3  IT部     1   删除这一行,主表不允许随便删除。主键值被引用,更不能随便删除。


采用联合查询。

实体-联系E-R

数据库建立,需要收集用户需求,设计符合企业要求的数据模型。而构建这种模 型需要方法,这种方法需要成为E-R实体-联系建模。也出现了一种建模语 言——UML(Unified Modeling Language)统一建模语言。

实体Entity:现实世界中具有相同属性的一组对象,可以是物理存在的事物或抽象的事物。

联系Relationship:实体之间的关联集合。

实体间联系类型-一对多-多对多

假设有实体部门,实体员工

类型 描述 解决方案
一对多联系 1:n 一个员工属于一个部门,一个部门有多个员工 员工外键;部门主键
多对多联系 m:n 一个员工属于多个部门,一个部门有多个员工 建立第三表
一对一联系 1:1 假设有实体管理者,一个管理者管理一个部门,一个部门只有一个管理者 字段建在哪张表都行

一对一关系用的较少,往往表示表A的一条记录唯一关联表B的一条记录,反之亦然。

它往往是为了将一张表多列分割并产生成了多张表,合起来是完整的信息,或为了方便查询,或为了数据安全隔离一部分字段的数据等等。

范例:多对多

n:n 一个员工属于n个部门,一个部门有n个员工。
建立第三张表,必须使用两个表的主键联合都得放到第三表中。

emp 从表
id name  loginname age 
1  tom   tom       20
2  jerry jerry     30
3  tom   tom1      20    
15 ben   ben       
18 sam   sam       
20 adam  adam      18   6 

dept 主表
id name
1  人事部
2  市场部
3  IT部
4  售后部
5  运维部

dept_emp 部门员工
dept_id   emp_id  联合起来就是候选键
1 fk      2 fk
3         3
4         3
2         15
习惯上,可以把第3张表中每一个字段都是原来业务的外键,建议开始时请使用外键约束。

视图

视图,也称虚表,看起来像表。它是由查询语句生成的。可以通过视图进行CRUD操作。

视图的作用

  • 简化操作,将复杂查询SQL语句定义为视图,可以简化查询
  • 数据安全,视图可以只显示真实表的部分列,或计算后的结果,从而隐藏真实表的数据

必学必会SQL

数据类型

MySQL中的数据类型

类型 含义
tinyint 1字节,带符号的范围是-128到127。无符号的范围是0到255。bool或boolean,就是tinyint,0表示假,非0表示真
smallint 2字节,带符号的范围是-32768到32767。无符号的范围是0到65535
int 整型,4字节,同Integer,带符号的范围是-2147483648到2147483647。无符号的范围是0到4294967295
bigint 长整型,8字节,带符号的范围是-9223372036854775808到9223372036854775807。无符号的范围是0到18446744073709551615
float 单精度浮点数精确到大约7位小数位
double 双精度浮点数精确到大约15位小数位
DATE 日期。支持的范围为’1000-01-01’到’9999-12-31’
DATETIME 支持的范围是’1000-01-01 00:00:00’到’9999-12-31 23:59:59’
TIMESTAMP 时间戳。范围是’1970-01-01 00:00:00’到2037年
char(M) 固定长度,右边填充空格以达到长度要求。M为长度,范围为0~255。M指的是字符个数
varchar(M) 变长字符串。M 表示最大列长度。M的范围是0到65,535。但不能突破行最大字节数65535
text 大文本。最大长度为65535(2^16-1)个字符
BLOB 大字节。最大长度为65535(2^16–1)字节的BLOB列
  • LENGTH函数返回字节数。而char和varchar定义的M是字符数限制
  • char可以将字符串定义为固定长度,空间换时间,效率略高;varchar为变长,省了空间

关系操作

关系:在关系数据库中,关系就是二维表。

关系操作就是对表的操作。

  • 选择(selection):又称为限制,是从关系中选择出满足给定条件的元组
  • 投影(projection):在关系上投影就是从选择出若干属性列组成新的关系
  • 连接(join):将不同的两个关系连接成一个关系

CRUD增删改查

所有操作一定要加条件

Insert语句

INSERT INTO table_name(col_name,...) VALUES (value1,...);
-- 向表中插入一行数据,自增字段、缺省值字段、可为空字段可以不写

INSERT INTO table_name SELECT ... ;
-- 将select查询的结果插入到表中

INSERT INTO table_name (col_name1,...) VALUES (value1,...) ON DUPLICATE KEY
UPDATE col_name1=value1,...;
-- 如果主键冲突、唯一键冲突就执行update后的设置。这条语句的意思,
-- 就是主键不在新增记录,主键在就更新部分字段

INSERT IGNORE INTO table_name (col_name,...) VALUES (value1,...);
-- 如果主键冲突、唯一键冲突就忽略错误,返回一个警告

例:

INSERT INTO reg(loginname, `name`, `password`) 
VALUES('tom', 'tom', 'tom');

INSERT INTO reg(if, loginname, `name`, `password`) 
VALUES (5, 'tom', 'tom', 'tom');

-- 主键冲突新增变更新
INSERT INTO reg(id, loginname, `name`, `password`) 
VALUED (1, 'tom', 'tom', 'tom') 
ON DUPLICATE KEY UPDATE name = 'jerry'
DROP DATABASE IF EXISTS test;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

use test

-- 有冲突用反引号、没冲突也可以用反引号
DROP TABLE IF EXISTS `reg`;
CREATE TABLE `reg` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(48) DEFAULT NULL,
  `loginname` varchar(48) NOT NULL COMMENT '注册名必须大于4个小于24个字符',
  `password` varchar(128) NOT NULL COMMENT '密码字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `loginname` (`loginname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


insert into reg(loginname, name, password) VALUES('tom','tom','tom'),('jerry','jerry','jerry');

mysql> select * from reg;
+----+-------+-----------+----------+
| id | name  | loginname | password |
+----+-------+-----------+----------+
|  1 | tom   | tom       | tom      |
|  2 | jerry | jerry     | jerry    |
+----+-------+-----------+----------+

insert into reg(loginname, name, password, id) values('ben', 'ben', 'benpwd', 100);
insert into reg(loginname, name, password) values('sam', 'sam', 'sampwd');

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | ben   | ben       | benpwd   |
| 101 | sam   | sam       | sampwd   |
+-----+-------+-----------+----------+

mysql> insert into reg(loginname, name, password) VALUES('tom','tom','tom');
ERROR 1062 (23000): Duplicate entry 'tom' for key 'reg.loginname'
mysql> insert into reg(loginname, name, password, id) VALUES('tom','tom','tom', 100);
ERROR 1062 (23000): Duplicate entry '100' for key 'reg.PRIMARY'

-- 主键冲突了,更新这一行内容
insert into reg(loginname, name, password, id) VALUES('tom','tom','tom', 100) on duplicate key update loginname='benny', name='benny';

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | sampwd   |
+-----+-------+-----------+----------+
test.sql完整语句
DROP DATABASE IF EXISTS test;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE test;

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for departments
-- ----------------------------
DROP TABLE IF EXISTS `departments`;
CREATE TABLE `departments` (
  `dept_no` char(4) NOT NULL,
  `dept_name` varchar(40) NOT NULL,
  PRIMARY KEY (`dept_no`),
  UNIQUE KEY `dept_name` (`dept_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of departments
-- ----------------------------
INSERT INTO `departments` VALUES ('d009', 'Customer Service');
INSERT INTO `departments` VALUES ('d005', 'Development');
INSERT INTO `departments` VALUES ('d002', 'Finance');
INSERT INTO `departments` VALUES ('d003', 'Human Resources');
INSERT INTO `departments` VALUES ('d001', 'Marketing');
INSERT INTO `departments` VALUES ('d004', 'Production');
INSERT INTO `departments` VALUES ('d006', 'Quality Management');
INSERT INTO `departments` VALUES ('d008', 'Research');
INSERT INTO `departments` VALUES ('d007', 'Sales');

-- ----------------------------
-- Table structure for dept_emp
-- ----------------------------
DROP TABLE IF EXISTS `dept_emp`;
CREATE TABLE `dept_emp` (
  `emp_no` int(11) NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`dept_no`),
  KEY `dept_no` (`dept_no`),
  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,
  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of dept_emp
-- ----------------------------
INSERT INTO `dept_emp` VALUES ('10001', 'd005', '1986-06-26', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10002', 'd007', '1996-08-03', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10003', 'd004', '1995-12-03', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10004', 'd004', '1986-12-01', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10005', 'd003', '1989-09-12', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10006', 'd005', '1990-08-05', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10007', 'd008', '1989-02-10', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10008', 'd005', '1998-03-11', '2000-07-31');
INSERT INTO `dept_emp` VALUES ('10009', 'd006', '1985-02-18', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10010', 'd004', '1996-11-24', '2000-06-26');
INSERT INTO `dept_emp` VALUES ('10010', 'd006', '2000-06-26', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10011', 'd009', '1990-01-22', '1996-11-09');
INSERT INTO `dept_emp` VALUES ('10012', 'd005', '1992-12-18', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10013', 'd003', '1985-10-20', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10014', 'd005', '1993-12-29', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10015', 'd008', '1992-09-19', '1993-08-22');
INSERT INTO `dept_emp` VALUES ('10016', 'd007', '1998-02-11', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10017', 'd001', '1993-08-03', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10018', 'd004', '1992-07-29', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10018', 'd005', '1987-04-03', '1992-07-29');
INSERT INTO `dept_emp` VALUES ('10019', 'd008', '1999-04-30', '9999-01-01');
INSERT INTO `dept_emp` VALUES ('10020', 'd004', '1997-12-30', '9999-01-01');

-- ----------------------------
-- Table structure for employees
-- ----------------------------
DROP TABLE IF EXISTS `employees`;
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

-- ----------------------------
-- Records of employees
-- ----------------------------
INSERT INTO `employees` VALUES ('10001', '1953-09-02', 'Georgi', 'Facello', '1', '1986-06-26');
INSERT INTO `employees` VALUES ('10002', '1964-06-02', 'Bezalel', 'Simmel', '2', '1985-11-21');
INSERT INTO `employees` VALUES ('10003', '1959-12-03', 'Parto', 'Bamford', '1', '1986-08-28');
INSERT INTO `employees` VALUES ('10004', '1954-05-01', 'Chirstian', 'Koblick', '1', '1986-12-01');
INSERT INTO `employees` VALUES ('10005', '1955-01-21', 'Kyoichi', 'Maliniak', '1', '1989-09-12');
INSERT INTO `employees` VALUES ('10006', '1953-04-20', 'Anneke', 'Preusig', '2', '1989-06-02');
INSERT INTO `employees` VALUES ('10007', '1957-05-23', 'Tzvetan', 'Zielinski', '2', '1989-02-10');
INSERT INTO `employees` VALUES ('10008', '1958-02-19', 'Saniya', 'Kalloufi', '1', '1994-09-15');
INSERT INTO `employees` VALUES ('10009', '1952-04-19', 'Sumant', 'Peac', '2', '1985-02-18');
INSERT INTO `employees` VALUES ('10010', '1963-06-01', 'Duangkaew', 'Piveteau', '2', '1989-08-24');
INSERT INTO `employees` VALUES ('10011', '1953-11-07', 'Mary', 'Sluis', '2', '1990-01-22');
INSERT INTO `employees` VALUES ('10012', '1960-10-04', 'Patricio', 'Bridgland', '1', '1992-12-18');
INSERT INTO `employees` VALUES ('10013', '1963-06-07', 'Eberhardt', 'Terkki', '1', '1985-10-20');
INSERT INTO `employees` VALUES ('10014', '1956-02-12', 'Berni', 'Genin', '1', '1987-03-11');
INSERT INTO `employees` VALUES ('10015', '1959-08-19', 'Guoxiang', 'Nooteboom', '1', '1987-07-02');
INSERT INTO `employees` VALUES ('10016', '1961-05-02', 'Kazuhito', 'Cappelletti', '1', '1995-01-27');
INSERT INTO `employees` VALUES ('10017', '1958-07-06', 'Cristinel', 'Bouloucos', '2', '1993-08-03');
INSERT INTO `employees` VALUES ('10018', '1954-06-19', 'Kazuhide', 'Peha', '2', '1987-04-03');
INSERT INTO `employees` VALUES ('10019', '1953-01-23', 'Lillian', 'Haddadi', '1', '1999-04-30');
INSERT INTO `employees` VALUES ('10020', '1952-12-24', 'Mayuko', 'Warwick', '1', '1991-01-26');

-- ----------------------------
-- Table structure for salaries
-- ----------------------------
DROP TABLE IF EXISTS `salaries`;
CREATE TABLE `salaries` (
  `emp_no` int(11) NOT NULL,
  `salary` int(11) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`from_date`),
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of salaries
-- ----------------------------
INSERT INTO `salaries` VALUES ('10001', '60117', '1986-06-26', '1987-06-26');
INSERT INTO `salaries` VALUES ('10001', '62102', '1987-06-26', '1988-06-25');
INSERT INTO `salaries` VALUES ('10001', '66074', '1988-06-25', '1989-06-25');
INSERT INTO `salaries` VALUES ('10001', '66596', '1989-06-25', '1990-06-25');
INSERT INTO `salaries` VALUES ('10001', '66961', '1990-06-25', '1991-06-25');
INSERT INTO `salaries` VALUES ('10001', '71046', '1991-06-25', '1992-06-24');
INSERT INTO `salaries` VALUES ('10001', '74333', '1992-06-24', '1993-06-24');
INSERT INTO `salaries` VALUES ('10001', '75286', '1993-06-24', '1994-06-24');
INSERT INTO `salaries` VALUES ('10001', '75994', '1994-06-24', '1995-06-24');
INSERT INTO `salaries` VALUES ('10001', '76884', '1995-06-24', '1996-06-23');
INSERT INTO `salaries` VALUES ('10001', '80013', '1996-06-23', '1997-06-23');
INSERT INTO `salaries` VALUES ('10001', '81025', '1997-06-23', '1998-06-23');
INSERT INTO `salaries` VALUES ('10001', '81097', '1998-06-23', '1999-06-23');
INSERT INTO `salaries` VALUES ('10001', '84917', '1999-06-23', '2000-06-22');
INSERT INTO `salaries` VALUES ('10001', '85112', '2000-06-22', '2001-06-22');
INSERT INTO `salaries` VALUES ('10001', '85097', '2001-06-22', '2002-06-22');
INSERT INTO `salaries` VALUES ('10001', '88958', '2002-06-22', '9999-01-01');
INSERT INTO `salaries` VALUES ('10002', '65828', '1996-08-03', '1997-08-03');
INSERT INTO `salaries` VALUES ('10002', '65909', '1997-08-03', '1998-08-03');
INSERT INTO `salaries` VALUES ('10002', '67534', '1998-08-03', '1999-08-03');
INSERT INTO `salaries` VALUES ('10002', '69366', '1999-08-03', '2000-08-02');
INSERT INTO `salaries` VALUES ('10002', '71963', '2000-08-02', '2001-08-02');
INSERT INTO `salaries` VALUES ('10002', '72527', '2001-08-02', '9999-01-01');
INSERT INTO `salaries` VALUES ('10003', '40006', '1995-12-03', '1996-12-02');
INSERT INTO `salaries` VALUES ('10003', '43616', '1996-12-02', '1997-12-02');
INSERT INTO `salaries` VALUES ('10003', '43466', '1997-12-02', '1998-12-02');
INSERT INTO `salaries` VALUES ('10003', '43636', '1998-12-02', '1999-12-02');
INSERT INTO `salaries` VALUES ('10003', '43478', '1999-12-02', '2000-12-01');
INSERT INTO `salaries` VALUES ('10003', '43699', '2000-12-01', '2001-12-01');
INSERT INTO `salaries` VALUES ('10003', '43311', '2001-12-01', '9999-01-01');
INSERT INTO `salaries` VALUES ('10004', '40054', '1986-12-01', '1987-12-01');
INSERT INTO `salaries` VALUES ('10004', '42283', '1987-12-01', '1988-11-30');
INSERT INTO `salaries` VALUES ('10004', '42542', '1988-11-30', '1989-11-30');
INSERT INTO `salaries` VALUES ('10004', '46065', '1989-11-30', '1990-11-30');
INSERT INTO `salaries` VALUES ('10004', '48271', '1990-11-30', '1991-11-30');
INSERT INTO `salaries` VALUES ('10004', '50594', '1991-11-30', '1992-11-29');
INSERT INTO `salaries` VALUES ('10004', '52119', '1992-11-29', '1993-11-29');
INSERT INTO `salaries` VALUES ('10004', '54693', '1993-11-29', '1994-11-29');
INSERT INTO `salaries` VALUES ('10004', '58326', '1994-11-29', '1995-11-29');
INSERT INTO `salaries` VALUES ('10004', '60770', '1995-11-29', '1996-11-28');

-- ----------------------------
-- Table structure for titles
-- ----------------------------
DROP TABLE IF EXISTS `titles`;
CREATE TABLE `titles` (
  `emp_no` int(11) NOT NULL,
  `title` varchar(50) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date DEFAULT NULL,
  PRIMARY KEY (`emp_no`,`title`,`from_date`),
  CONSTRAINT `titles_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of titles
-- ----------------------------
INSERT INTO `titles` VALUES ('10001', 'Senior Engineer', '1986-06-26', '9999-01-01');
INSERT INTO `titles` VALUES ('10002', 'Staff', '1996-08-03', '9999-01-01');
INSERT INTO `titles` VALUES ('10003', 'Senior Engineer', '1995-12-03', '9999-01-01');
INSERT INTO `titles` VALUES ('10004', 'Engineer', '1986-12-01', '1995-12-01');
INSERT INTO `titles` VALUES ('10004', 'Senior Engineer', '1995-12-01', '9999-01-01');
INSERT INTO `titles` VALUES ('10005', 'Senior Staff', '1996-09-12', '9999-01-01');
INSERT INTO `titles` VALUES ('10005', 'Staff', '1989-09-12', '1996-09-12');
INSERT INTO `titles` VALUES ('10006', 'Senior Engineer', '1990-08-05', '9999-01-01');
INSERT INTO `titles` VALUES ('10007', 'Senior Staff', '1996-02-11', '9999-01-01');
INSERT INTO `titles` VALUES ('10007', 'Staff', '1989-02-10', '1996-02-11');
INSERT INTO `titles` VALUES ('10008', 'Assistant Engineer', '1998-03-11', '2000-07-31');
INSERT INTO `titles` VALUES ('10009', 'Assistant Engineer', '1985-02-18', '1990-02-18');
INSERT INTO `titles` VALUES ('10009', 'Engineer', '1990-02-18', '1995-02-18');
INSERT INTO `titles` VALUES ('10009', 'Senior Engineer', '1995-02-18', '9999-01-01');
INSERT INTO `titles` VALUES ('10010', 'Engineer', '1996-11-24', '9999-01-01');
INSERT INTO `titles` VALUES ('10011', 'Staff', '1990-01-22', '1996-11-09');
INSERT INTO `titles` VALUES ('10012', 'Engineer', '1992-12-18', '2000-12-18');
INSERT INTO `titles` VALUES ('10012', 'Senior Engineer', '2000-12-18', '9999-01-01');
INSERT INTO `titles` VALUES ('10013', 'Senior Staff', '1985-10-20', '9999-01-01');
INSERT INTO `titles` VALUES ('10014', 'Engineer', '1993-12-29', '9999-01-01');
INSERT INTO `titles` VALUES ('10015', 'Senior Staff', '1992-09-19', '1993-08-22');

Update语句

UPDATA [IGNORE] tal_name SET col_name1=expr1 [, col_name2=expr2 ...] [WHERE where_definition]
-- IFNORE 意义同Insert语句

UPDATE reg SET name='tom· WHERE id=5;

更新一定要加条件

-- 注意这一句非常危险,会更新所有数据
UPDATE reg SET name ='ben';

-- 指定范围
UPDATE reg SET name = 'ben', password = 'benpwd' WHRER id = 1;

Delete语句

DELETE FROM tal_name [WHERE where_defintion]
-- 删除符合条件的记录

DELETE FROM reg WHERE id =1;
-- 删除一定要有条件

Select语句

SELECT
    [DISTINCT]
    select_expr, ...
    [FROM table_references
    [WHERE where_definition]
    [FROUP BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [HAVING whrer_definition]
    [ORDER BY {col_name | expr | position}
      [ASC | DESC], ...]
    [LIMIT {[offset,} roe_count | row_count OFFSET offset}]
    [FOR UPDATE | LOCK IN SHARE MODE]]

FOR UPDATE会把行进行写锁定,这是排它锁

查询

查询的结果成为结果集recordset

最简单的查询

mysql> select 1;
+---+
| 1 |
+---+
| 1 |
+---+

mysql> select 1, 'ab';
+---+----+
| 1 | ab |
+---+----+
| 1 | ab |
+---+----+

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | sampwd   |
+-----+-------+-----------+----------+
SELECT emp_no, first_name + last_name FROM employees;
-- 字符串合并

SELECT emp_no, CONCAT(first_name,' ',last_name) FROM employees;
-- 使用字符串相加函数 CONCAT

SELECT emp_no as en, CONCAT(first_name,' ',last_name) as name FROM employees;
-- AS定义别名,可选。写AS是一个好习惯
Limit子句
-- 返回5条记录, [1,5]左闭右闭
SELECT * FROM employees as emp LIMIT 5;

-- 返回5条记录,偏移3条,limit3, 5 ---  (3,3+5]左开右闭 start:end start:len 分页用到
SELECT * FROM employees as emp LIMIT 3, 5;
-- 等价于
SELECT * FROM employees as emp LIMIT 5 offset 3; 
Where子句
运算符 描述符
= 等于
<> 或 != 不等于
>、 <、 >=、 <= 大于、小于、大于等于、小于等于
BETWEEN 在某个范围之内,between a and b等价于[a, b]
LIKE 字符串模式匹配,%表示任意多个字符,_表示一个字符
IN 指定针对某个列的多个可能值
AND
OR
  • 能用键匹配用键
  • LIKE 只能使用左前缀,尽量不使用,性能差

注意:如果很多表达式需要使用AND、OR计算逻辑表达式的值的时候,由于有结合律的问题,建议使用小括号来避免产生错误

查询条件

--  条件
SELECT emps.emp_no as id, last_name FROM employees emps WHERE not (emps.emp_no = 10008 or emps.emp_no = 10010);
SELECT emps.emp_no as id, last_name FROM employees emps WHERE not emp_no <> 10008 and emp_no <> 10010;
SELECT emps.emp_no as id, last_name FROM employees emps WHERE not emp_no in (10008, 10010);
SELECT emps.emp_no as id, last_name FROM employees emps WHERE not emp_no not in (10008, 10010);
-- 范围
SELECT * FROM employees WHERE emp_no < 10015 and last_name LIKE 'P%';  -- mysql中str默认忽略大小写
SELECT * FROM employees WHERE emp_no < 10015 and last_name LIKE 'P__%';  -- 下划线 _ 代表单个匹配
SELECT * FROM employees WHERE emp_no BETWEEN 10010 AND 10015 AND last_name LIKE 'P%';

EXPLAIN 可判断查询条件性能如何

EXPLAIN SELECT emp_no AS id, birth_date, concat(FIRST_name, ' ', last_name) as name
  FROM employees as emp WHERE last_name='Sluis';


EXPLAIN SELECT emp_no AS id, birth_date, concat(FIRST_name, ' ', last_name) as name
  FROM employees as emp WHERE emp_no BETWEEN 10010 and 10015 and last_name='Sluis';

type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:NULL >system > const >eq_ref > ref > fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range >index > ALL,一般来说,得保 证查询至少达到range级别,最好能达到ref

NULL >system > const >eq_ref > ref > fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range >index > ALL //最好到最差
备注:掌握以下10种常见的即可
NULL >system > const >eq_ref >ref > ref_or_null > index_merge >range >index > ALL
类型 说明
All 最坏的情况,全表扫描
index 和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。如在Extra列看到Using index,说明正在使用覆盖索引,只扫描索引的数据,它比按索引次序全表扫描的开销要小很多
range 范围扫描,一个有限制的索引扫描。key 列显示使用了哪个索引。当使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比较关键字列时,可以使用 range
ref 一种索引访问,它返回所有匹配某个单个值的行。此类索引访问只有当使用非唯一性索引或唯一性索引非唯一性前缀时才会发生。这个类型跟eq_ref不同的是,它用在关联操作只使用了索引的最左前缀,或者索引不是UNIQUE和PRIMARY KEY。ref可以用于使用=或<=>操作符的带索引的列。
eq_ref 最多只返回一条符合条件的记录。使用唯一性索引或主键查找时会发生 (高效)
const 当确定最多只会有一行匹配的时候,MySQL优化器会在查询前读取它而且只读取一次,因此非常快。当主键放入where子句时,mysql把这个查询转为一个常量(高效)
system 这是const连接类型的一种特例,表仅有一行满足条件。
Null 意味着mysql能在优化阶段分解查询语句,在执行阶段甚至用不到访问表或索引(高效)
Order by 子句

对查询结果进行排序,可以升序ASC、降序DESC。默认不填为升序。

先以按照第一字段排序,第一字段相同再以第二字段排序。

-- 升序
SELECT * FROM employees WHERE emp_no in (10001, 10002, 10010) ORDER BY emp_no;

-- 降序
SELECT * FROM employees WHERE emp_no in (10001, 10002, 10010) ORDER BY emp_no DESC

-- OROER BY 先执行,在执行 LIMIT
SELECT * FROM employees WHERE emp_no in (10001, 10002, 10010) 
 ORDER BY birth_date, emp_no DESC LIMIT 1, 2;  -- 以这2个字段排序,其中以birth_date升序,emp_no 降序
DISTINCT

不返回重复记录,去重。

SELECT DISTINCT dept_no FROM dept_emp;

SELECT DISTINCT emp_no FROM dept_emp;

SELECT dept_no,emp_no FROM dept_emp;
-- 会将dept_no与emp_no看成二元祖来筛选
聚合函数
函数 描述
COUNT(expr) 返回记录中记录的数目,如果指定列,则返回非NULL值的行数
COUNT(DISTINCT expr,[expr…]) 返回不重复的非NULL值的行数
AVG([DISTINCT] expr) 返回平均值,返回不同值的平均值
MIN(expr), MAX(expr) 最小值,最大值
SUM([DISTINCT] expr) 求和,Distinct返回不同值求和
-- 聚合为一行
SELECT COUNT(*), AVG(emp_no), sum(emp_no), min(emp_no), max(emp_no) FROM employees;
mysql> SELECT COUNT(*), AVG(emp_no), sum(emp_no), min(emp_no), max(emp_no) FROM employees;
+----------+-------------+-------------+-------------+-------------+
| COUNT(*) | AVG(emp_no) | sum(emp_no) | min(emp_no) | max(emp_no) |
+----------+-------------+-------------+-------------+-------------+
|       20 |  10010.5000 |      200210 |       10001 |       10020 |
+----------+-------------+-------------+-------------+-------------+
分组查询
SELECT emp_no, COUNT( emp_no ), sum( emp_no ), avg( emp_no ) AS sal_avg 
  FROM employees  WHERE emp_no > 10001 
  GROUP BY emp_no HAVING sal_avg > 10005
  ORDER BY sal_avg DESC LIMIT 1;

执行顺序 :where > HAVING > select > GROUP BY > HAVING > ORDER BY

原表

-- ----------------------------
-- Table structure for salaries
-- ----------------------------
DROP TABLE IF EXISTS `salaries`;
CREATE TABLE `salaries` (
  `emp_no` int(11) NOT NULL,
  `salary` int(11) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`from_date`),
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of salaries
-- ----------------------------
INSERT INTO `salaries` VALUES ('10001', '60117', '1986-06-26', '1987-06-26');
INSERT INTO `salaries` VALUES ('10001', '62102', '1987-06-26', '1988-06-25');
INSERT INTO `salaries` VALUES ('10001', '66074', '1988-06-25', '1989-06-25');
INSERT INTO `salaries` VALUES ('10001', '66596', '1989-06-25', '1990-06-25');
INSERT INTO `salaries` VALUES ('10001', '66961', '1990-06-25', '1991-06-25');
INSERT INTO `salaries` VALUES ('10001', '71046', '1991-06-25', '1992-06-24');
INSERT INTO `salaries` VALUES ('10001', '74333', '1992-06-24', '1993-06-24');
INSERT INTO `salaries` VALUES ('10001', '75286', '1993-06-24', '1994-06-24');
INSERT INTO `salaries` VALUES ('10001', '75994', '1994-06-24', '1995-06-24');
INSERT INTO `salaries` VALUES ('10001', '76884', '1995-06-24', '1996-06-23');
INSERT INTO `salaries` VALUES ('10001', '80013', '1996-06-23', '1997-06-23');
INSERT INTO `salaries` VALUES ('10001', '81025', '1997-06-23', '1998-06-23');
INSERT INTO `salaries` VALUES ('10001', '81097', '1998-06-23', '1999-06-23');
INSERT INTO `salaries` VALUES ('10001', '84917', '1999-06-23', '2000-06-22');
INSERT INTO `salaries` VALUES ('10001', '85112', '2000-06-22', '2001-06-22');
INSERT INTO `salaries` VALUES ('10001', '85097', '2001-06-22', '2002-06-22');
INSERT INTO `salaries` VALUES ('10001', '88958', '2002-06-22', '9999-01-01');
INSERT INTO `salaries` VALUES ('10002', '65828', '1996-08-03', '1997-08-03');
INSERT INTO `salaries` VALUES ('10002', '65909', '1997-08-03', '1998-08-03');
INSERT INTO `salaries` VALUES ('10002', '67534', '1998-08-03', '1999-08-03');
INSERT INTO `salaries` VALUES ('10002', '69366', '1999-08-03', '2000-08-02');
INSERT INTO `salaries` VALUES ('10002', '71963', '2000-08-02', '2001-08-02');
INSERT INTO `salaries` VALUES ('10002', '72527', '2001-08-02', '9999-01-01');
INSERT INTO `salaries` VALUES ('10003', '40006', '1995-12-03', '1996-12-02');
INSERT INTO `salaries` VALUES ('10003', '43616', '1996-12-02', '1997-12-02');
INSERT INTO `salaries` VALUES ('10003', '43466', '1997-12-02', '1998-12-02');
INSERT INTO `salaries` VALUES ('10003', '43636', '1998-12-02', '1999-12-02');
INSERT INTO `salaries` VALUES ('10003', '43478', '1999-12-02', '2000-12-01');
INSERT INTO `salaries` VALUES ('10003', '43699', '2000-12-01', '2001-12-01');
INSERT INTO `salaries` VALUES ('10003', '43311', '2001-12-01', '9999-01-01');
INSERT INTO `salaries` VALUES ('10004', '40054', '1986-12-01', '1987-12-01');
INSERT INTO `salaries` VALUES ('10004', '42283', '1987-12-01', '1988-11-30');
INSERT INTO `salaries` VALUES ('10004', '42542', '1988-11-30', '1989-11-30');
INSERT INTO `salaries` VALUES ('10004', '46065', '1989-11-30', '1990-11-30');
INSERT INTO `salaries` VALUES ('10004', '48271', '1990-11-30', '1991-11-30');
INSERT INTO `salaries` VALUES ('10004', '50594', '1991-11-30', '1992-11-29');
INSERT INTO `salaries` VALUES ('10004', '52119', '1992-11-29', '1993-11-29');
INSERT INTO `salaries` VALUES ('10004', '54693', '1993-11-29', '1994-11-29');
INSERT INTO `salaries` VALUES ('10004', '58326', '1994-11-29', '1995-11-29');
INSERT INTO `salaries` VALUES ('10004', '60770', '1995-11-29', '1996-11-28');

使用Group by子句,如果有条件,使用Having子句过滤分组、聚合过的结果

-- 聚合所有
SELECT SUM(salary), avg(salary), count(emp_no) from salaries;
-- 聚合被选择的记录
SELECT SUM(salary), avg(salary), count(emp_no) from salaries WHERE emp_no < 10003;

-- 分组
SELECT emp_no FROM salaries GROUP BY emp_no;
SELECT emp_no FROM salaries WHERE emp_no < 10003 GROUP BY emp_no;

-- 按照不同emp_no分组,每组分别聚合
SELECT emp_no, SUM(salary), AVG(salary), count(emp_no) 
  from salaries WHERE emp_no < 10003 GROUP BY emp_no;

-- HAVING子句对分组结果过滤
SELECT emp_no, SUM(salary), AVG(salary), count(emp_no) 
  from salaries GROUP BY emp_no HAVING AVG(salary) > 45000 -- 员工平均工资大于45000的有谁
-- 使用别名
SELECT emp_no, SUM(salary), AVG(salary) as sal_avg, count(emp_no) 
  from salaries GROUP BY emp_no HAVING sal_avg > 45000

-- 最后对分组过滤后的结果排序
SELECT emp_no, SUM(salary), AVG(salary) AS sal_avg, COUNT(emp_no) 
  from salaries GROUP BY emp_no HAVING sal_avg > 60000 ORDER BY sal_avg; 

范例:

#聚合所有
mysql> SELECT SUM(salary), avg(salary), count(emp_no) from salaries;
+-------------+-------------+---------------+
| SUM(salary) | avg(salary) | count(emp_no) |
+-------------+-------------+---------------+
|     2491668 |  62291.7000 |            40 |
+-------------+-------------+---------------+

#聚合被选择的记录
mysql> SELECT SUM(salary), avg(salary), count(emp_no) from salaries WHERE emp_no < 10003;
+-------------+-------------+---------------+
| SUM(salary) | avg(salary) | count(emp_no) |
+-------------+-------------+---------------+
|     1694739 |  73684.3043 |            23 |
+-------------+-------------+---------------+

#分组查询
mysql> select emp_no from salaries group by emp_no;
+--------+
| emp_no |
+--------+
|  10001 |
|  10002 |
|  10003 |
|  10004 |
+--------+
mysql> SELECT emp_no FROM salaries WHERE emp_no < 10003 GROUP BY emp_no;
+--------+
| emp_no |
+--------+
|  10001 |
|  10002 |
+--------+

#按照不同emp_no分组,每组分别聚合
#分组不管是否聚合,最后每组出一行
mysql> SELECT emp_no,sum(salary) FROM salaries WHERE emp_no in (10002,10004) GROUP BY emp_no;
+--------+-------------+
| emp_no | sum(salary) |
+--------+-------------+
|  10002 |      413127 |
|  10004 |      495717 |
+--------+-------------+
mysql> SELECT emp_no, SUM(salary) sum, AVG(salary) avg, count(emp_no) cnt from salaries WHERE emp_no < 10003 GROUP BY emp_no;
+--------+---------+------------+-----+
| emp_no | sum     | avg        | cnt |
+--------+---------+------------+-----+
|  10001 | 1281612 | 75388.9412 |  17 |
|  10002 |  413127 | 68854.5000 |   6 |
+--------+---------+------------+-----+

分组是将数据按照指定的字段分组,最终每组只能出来一条记录。这就带来了问题,每一组谁做代表,其实谁做代表都不合适。

如果只投影分组字段、聚合数据,不会有问题,如果投影非分组字段,显示的时候不能确定是组内谁的数据。

-- 分组
SELECT emp_no, MAX(salary) FROM salaries; -- 10001 88958
SELECT emp_no, MIN(salary) FROM salaries; -- 10001 40006

上例很好的说明了使用了聚合函数,虽然没有显式使用Group By语句,但是其实就是把所有记录当做一组,每组只能出一条,那么一组也只能出一条,所以结果就一条。

但是emp_no就是非分组字段,那么它就要开始覆盖,所以,显示为10001。当求 最大值的时候,正好工资表中10001的工资最高,感觉是对的。但是,求最小工 资的时候,明明最小工资是10003的40006,由于emp_no不是分组字段,导致最后 被覆盖为10001

SELECT emp_no, MIN(salary) FROM salaries GROUP BY emp_no;

上句才是正确的语义,按照不同员工emp_no工号分组,每一个人一组,每一个人有多个工资记录,按时每组只能按照人头出一条记录。

单标较为复杂的语句

SELECT
  emp_no,
  AVG( salary ) AS avg_sal 
FROM
  salaries 
WHERE
  salary > 70000 
GROUP BY
  emp_no 
HAVING
  avg( salary ) > 50000 
ORDER BY
  avg_sal DESC 
  LIMIT 1;
子查询
  • 查询语句可以嵌套,内部查询就是子查询
  • 子查询必须在一组小括号中
  • 子查询中不能使用Order by
SELECT
  * 
FROM
  employees 
WHERE
  emp_no IN ( SELECT emp_no FROM employees WHERE emp_no > 10015 ) 
ORDER BY
  emp_no DESC;


-- 当表用
SELECT
  emp.emp_no,
  emp.first_name,
  gender 
FROM
  ( SELECT * FROM employees WHERE emp_no > 10015 ) AS emp 
WHERE
  emp.emp_no < 10019 
ORDER BY
  emp_no DESC;
连接jon和联合查询UNION

多表查询,即查询结果来自于多张表

  • 子查询:在SQL语句嵌套着查询语句,性能较差,基于某语句的查询结果再次进行的查询
  • 联合查询:UNION
  • 交叉连接:笛卡尔乘积
  • 内连接:
    • 等值连接:让表之间的字段以“等值”建立连接关系
    • 不等值连接
    • 自然连接:去掉重复列的等值连接,语法:FROM table NATURAL JOIN table2;
  • 外连接:
    • 左外连接:FROM tb1 LEFT JOIN tb2 ON tb1.col=tb2.col
    • 右外连接:FROM tb1 RIGHT JOIN tb2 ON tb1.col=tb2.col
    • 完全外连接:FROM tb1 FULL OUTER JOIN tb2 ON tb1.col=tb2.col 注意:MySQL不支持此SQL语法
  • 自连接:本表和本表进行连接查询

交叉连接 cross join

  • 笛卡尔乘积,全部交叉
  • 在MySQL中,CROSS JOIN从语法上说与INNER JOIN等同

Join会构建一张临时表

-- 工资40行
SELECT * FROM salaries;
-- 20行
SELECT * FROM employees;
-- 800行
SELECT * FROM employees CROSS JOIN salaries;
-- 隐式连接,800行
SELECT * FROM employees, salaries;

注意:salaries和employees并没有直接的关系,做笛卡尔乘积只是为了看的清楚。

内连接 inner join

  • 可省略为join
  • 等值连接,只选某些field相等的元组(行),使用On限定关联的结果

自然连接,特殊的等值连接,会去掉重复的列。用的少

-- 内连接,笛卡尔乘积 800 行
SELECT * FROM employees JOIN salaries;
SELECT * FROM employees INNER JOIN salaries;

-- on等值连接 40行
SELECT * FROM employees JOIN salaries ON employees.emp_no = salaries.emp_no;
-- 自然连接,去掉了重复列,且自行使用 employees.emp_no = salaries.emp_no的条件
SELECT * FROM employees NATURAL JOIN salaries;

外连接 outer join

  • 可以省略为join
  • 分为左外连接,即左连接;右外连接,即右连接;全外连接
    • 左连接,左表全部出来向右对应,不对应的右表值为null
-- 左连接 (56条记录)
SELECT * FROM employees LEFT JOIN salaries ON employees.emp_no = salaries.emp_no;
-- 右连接 (40 条记录)
SELECT * FROM employees RIGHT JOIN salaries ON employees.emp_no = salaries.emp_no;
-- 这个右连接等价于上面的左连接(56条记录)
SELECT * FROM salaries RIGHT JOIN employees ON employees.emp_no = salaries.emp_no;

SELECT
  employees.*
FROM
  salaries
  RIGHT JOIN employees ON employees.emp_no = salaries.emp_no 
WHERE
  salaries.emp_no IS NULL

自连接

  • 表,自己和自己连接
select manager.* from emp manager,emp worker where 
manaer.empno=worker.mgr and worker.empno=1;

select manager.* from emp manager inner join emp 
worker on manaer.empno=worker.mgr where worker.empno=1;
/*
假设有表manager,字段和记录如下
empno  name  mgr
1      tom  
2      jerry 1
3      ben   2
*/

-- 有领导的员工
select * from manager where mgr is not null

-- 所有有领导的员工及领导名字
select worker.*, mgr.name from manager worker INNER JOIN manager mgr ON mgr.id = worker.mgr

联合查询:UNION

  • 联合查询 Union 实现的条件,多个表的字段数量相同,字段名和数据类型可以 不同,但一般数据类型是相同的。
SELECT Name,Age FROM students UNION SELECT Name,Age FROM teachers;

union 去重 union all 重复字段也会显示

事务和锁

事务Transaction

InnoDB引擎,支持事务。

事务: 由若干条语句组成的,指的是要做的一系列操作。

关系型数据库中支持事务,必须支持其四个属性(ACID):

特性 描述
原子性(atomicity) 一个事务是一个不可分割的工作单位,事务中包括的所有操作要么全部做完,要么什么都不做
一致性(consistency) 事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的
隔离性(isolation) 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
持久性(durability) 持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
  • 原子性,要求事务中的所有操作,不可分割,不能做了一部分操作,还剩一部分操作
  • 一致性,多个事务并行执行的结果,应该和事务排队执行的结果一致。如果事务的并行执行和多线程读写共享资源一样不可预期,就不能保证一致性
  • 隔离性,就是指多个事务访问共同的数据了,应该互不干扰。隔离性,指的是究竟在一个事务处理期间,其他事务能不能访问的问题
  • 持久性,比较好理解,就是事务提交后,数据不能丢失

MySQL隔离级别

隔离性不好,事务的操作就会互相影响,带来不同严重程度的后果。

首先看看隔离性不好,带来哪些问题:

  1. 更新丢失Lost Update

    事务A和B,更新同一个数据,它们都读取了初始值100,A要减10,B要加100,A减去10后更新为90,B加100更新为200,A的更新丢失了,就像从来没有减过10一样。

  2. 脏读

    事务A和B,事务B读取到了事务A未提交的数据(这个数据可能是一个中间值,也可能事务A后来回滚事务)。事务A是否最后提交并不关心。只要读取到了这个被修改的数据就是脏读。

  3. 不可重复读Unrepeatable read

    事务A在事务执行中相同查询语句,得到了不同的结果,不能保证同一条查询语句重复读相同的结果就是不可以重复读。

    例如,事务A查询了一次后,事务B修改了数据,事务A又查询了一次,发现数据不一致了。

    注意,脏读讲的是可以读到相同的数据的,但是读取的是一个未提交的数据,而不是提交的最终结果。

  4. 幻读Phantom read

    事务A中同一个查询要进行多次,事务B插入数据,导致A返回不同的结果集,如同幻觉,就是幻读。

    数据集有记录增加了,可以看做是增加了记录的不可重复读。

有了上述问题,数据库就必须要解决,提出了隔离级别。

隔离级别由低到高,如下表

隔离级别 描述
READ UNCOMMITTED 读取到未提交的数据
READ COMMITTED 读已经提交的数据,ORACLE默认隔离级别
REPEATABLE READ 可以重复读,MySQL的 默认隔离级别
SERIALIZABLE 可串行化。事务间完全隔离,事务不能并发,只能串行执行

隔离级别越高,串行化越高,数据库执行效率低;隔离级别越低,并行度越高,性能越高。

隔离级别越高,当前事务处理的中间结果对其它事务不可见程度越高。

  • SERIALIZABLE,串行了,解决所有问题
  • REPEATABLE READ,事务A中同一条查询语句返回同样的结果,就是可以重复读数据了。例如语句为(select * from user)。解决的办法有:

    1、对select的数据加锁,不允许其它事务删除、修改的操作

    2、第一次select的时候,对最后一次确切提交的事务的结果做快照

    解决了不可以重复读,但是仍有可能出现幻读。例如,事务A事务B开启,事务A中增加了一条数 据并提交,事务B反复查询看不到新的数据,但是可以使用updaate语句更新成功,再查询就看到 了。这也是幻读,如同出现了幻觉。

  • READ COMMITTED,在事务中,每次select可以读到别的事务刚提交成功的新的数 据。因为读到的是提交后的数据,解决了脏读,但是不能解决不可重复读和幻读 的问题。因为其他事务前后修改了数据或增删了数据。
  • READ UNCOMMITTED,能读取到别的事务还没有提交的数据,完全没有隔离性可言,出现了脏读,当前其他问题都可能出现。

事务语法

START TRANSACTION或BEGIN开始一个事务,START TRANSACTION是标准SQL的语法。

使用COMMIT提交事务后,变更成为永久变更。

ROLLBACK可以在提交事务之前,回滚变更,事务中的操作就如同没有发生过一样(原子性)。

SET AUTOCOMMIT语句可以禁用或启用默认的autocommit模式,用于当前连接。 SET AUTOCOMMIT = 0禁用自动提交事务。如果开启自动提交,如果有一个修改表 的语句执行后,会立即把更新存储到磁盘。

-- 设置会话级或者全局隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

-- 查询隔离级别
SELECT @@global.tx_isolation;
SELECT @@tx_isolation;

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 禁用自动提交
SET AUTOCOMMIT = 0

范例:测试默认隔离级别 REPEATABLE-READ

#开2个窗口, 关闭自动提交
set autocommit=0;

#查看当前会话的隔离级别
mysql> select @@transaction_isolation;  #8.0
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)


#窗口1 修改
mysql> update reg set password='123' where id = 101;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+
4 rows in set (0.00 sec)

#窗口2, 看到还是之前的事务
mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | sampwd   |
+-----+-------+-----------+----------+

#窗口1,提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

#窗口2,查看表,但仍然看不到。因为隔离级别REPEATABLE READ,可以重复读
mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | sampwd   |
+-----+-------+-----------+----------+

#事务完成,开启另一个事物。就能看到了。
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+

范例:测试默认隔离级别 READ COMMITTED

#开2个窗口, 关闭自动提交
set autocommit=0;

#设置隔离级别
set session transaction isolation level read committed;
#查看当前会话的隔离级别
mysql> select @@transaction_isolation;  #8.0
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-COMMITTED          |
+-------------------------+

#窗口1 改动
mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+
4 rows in set (0.00 sec)

mysql> update reg set password='tompwd' where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tompwd   |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+

#窗口2查看, 无变化 
mysql>  select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tom      |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+

#窗口1 提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

#窗口2 查询能看到。因为隔离级别为READ COMMITTED 不可以重复读到当前事务数据。
mysql>  select * from reg;
+-----+-------+-----------+----------+
| id  | name  | loginname | password |
+-----+-------+-----------+----------+
|   1 | tom   | tom       | tompwd   |
|   2 | jerry | jerry     | jerry    |
| 100 | benny | benny     | benpwd   |
| 101 | sam   | sam       | 123      |
+-----+-------+-----------+----------+


FOR UPDATE 和锁

数据库不可避免会发生并发,有人读取,就有人写入,如果读写同一张表、同一条记录,就会有冲突。

  • 读锁,共享锁,多用户同一时刻读取同一个资源,互不干扰
  • 写锁,排他锁,有更高优先级,它会阻塞其它用户对该资源读写操作而使用的读锁或写锁

SELECT…For Update会把行进行写锁定,这是排它锁。

使用主键明确记录行,那么就对存在的主键的记录使用行级锁。例如id=100

使用主键,但不能确定记录,使用表级锁,例如id<>3。

条件中不使用主键,会使用表级锁。例如,name='tom'

悲观锁,我怕别人在我用的时候抢资源,先锁上,独占。悲观锁往往用在数据库的锁机制中,因为 独占,所以会影响并发。所以,For Update非要使用,请一定要保证时间短,且一定利用行级锁。

乐观锁,就是心宽,认为极少冲突,只有写入才加锁。但需要检测数据冲突。

数据仓库和数据库的区别

本质上来说没有区别,都是存放数据的地方。

  • 但是数据库关注数据的持久化、数据的关系,为业务系统提供支持,事务支持;
  • 数据仓库存储数据的是为了分析或者发掘而设计的表结构,可以存储海量数据。

数据库存储在线交易数据OLTP(联机事务处理OLTP,On-line Transaction Processing);数据仓库存储历史数据用于分析OLAP(联机分析处理OLAP,On-Line Analytical Processing)。

数据库支持在线业务,需要频繁增删改查;数据仓库一般囤积历史数据支持用于分析的SQL,一般不建议删改。

存储过程、触发器

存储过程(Stored Procedure),数据库系统中,一段完成特定功能的SQL语句。编写成类似函数的方式,可以传参并调用。支持流程控制语句。

触发器(Trigger),由事件触发的特殊的存储过程,例如insert数据时触发。

这两种技术,虽然是数据库高级内容,性能不错,但基本很少用了。

它们移植性差,使用时占用的服务器资源,排错、维护不方便。

最大的原因,不太建议把逻辑放在数据库中。

游标Cursor

操作查询的结果集的一种方法。

可以将游标当做一个指针,指向结果集中的某一行。

数据库开发

驱动

与MySQL通信就是典型的CS模式。Server就是服务端,使用客户端先建立连接,数据库编程时, 这个客户端变成了程序。

MySQL基于TCP协议之上开发,传输的数据必须遵循MySQL的协议。封装好MySQL协议的包,就是驱动程序。

MySQL的驱动

pymysql使用

安装

pip install pymysql
pip install simplejson

#MySQL 8.0+ 默认使用 caching_sha2_password 认证插件,该插件需要 cryptography 处理加密。
pip install cryptography

simplejson库处理json文件方便。

创建数据库和表

CREATE DATABASE IF NOT EXISTS school;
SHOW DATABASES;
USE school;

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) NOT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

import pymysql

IP = '192.168.1.6' USERNAME = 'lqx' PASSWORD = 'lqx' DBNAME ='test' PORT = 3306

conn = None currsor = None # 对结果集的操作 try: conn = pymysql.connect(IP, USERNAME, PASSWORD, DBNAME, PORT) currsor = conn.cursor()

sql = "CREATE TABLE `student` (" \ "`id` int(11) NOT NULL AUTO_INCREMENT," \ "`name` varchar(30) NOT NULL," \ "`age` int(11) DEFAULT NULL," \ "PRIMARY KEY (`id`)" \ ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"

r = currsor.execute(sql) print(r)

conn.commit() except: conn.rollback() print('roll back') finally: if currsor: currsor.close() if conn: conn.close()

连接Connect

首先,必须建立一个传输数据通加粗样式道——连接。

  • pymysql.connect()方法返回的是Connections模块下的Connection类实例
  • connect方法传参就是给Connection类的 init 提供参数
Connection初始化常用参数 说明
host 主机
user 用户名
password 密码
database 数据库
port 端口

Connection.ping()方法,测试数据库服务器是否活着。有一个参数reconnect表示断开与服务器连接是否重连。连接关闭抛出异常

范例:

创建配置文件 config.json

{
  "host": "192.168.226.128",
  "password": "123456",
  "database": "test",
  "user": "root",
  "port": 3306
}
import pymysql
import simplejson #pip install simplejson
import os

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
try:
    conn = pymysql.connect(**conf)
    conn.ping(False)  # ping不通则抛异常
    print(conn)
finally:
    if conn:
        conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
<pymysql.connections.Connection object at 0x0000015C3DE72270>

游标Cursor

操作数据库,必须使用游标,需要先获取一个游标对象。

Connection.cursor(cursor=None) 方法返回一个新的游标对。

连接没有关闭前,游标对象可以反复使用。

cursor参数,可以指定一个Cursor类。如果为None,则使用默认Cursor类。

Cursor类的实例,提供execute() 方法,执行SQL语句,成功返回影响的行数。

新增记录

使用insert into语句插入数据

import pymysql
import simplejson #pip install simplejson
import os

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor()
    sql = """insert into student (name, age) values ('tom', 18)"""
    rows = cursor.execute(sql) #真的执行了,只是没有提交。 自增id受影响。
    print(type(rows), rows) #受影响1行

    conn.commit() #提交事物。因为默认autocommit为False,关闭的。
except Exception as e:
    conn.rollback()
    print(e)
finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
<class 'int'> 1
提交时需提供commit()方法,原因在于,在Connection类的 __init__方法的注释中有这么一句话

autocommit: Autocommit mode. None means use server default. (default: False)

一般不用开启自动提交功能,使用手动管理事务。

事务管理

Connection类有三个方法:

  • begin 开始事务
  • commit 将变更提交
  • rollback 回滚事务

批量增加数据

可以小规则执行几百条sql,统一提交

for i in range(19, 29):
    sql = """insert into student (name, age) values ('tom{0}', 18{0})""".format(i)
    x = cursor.execute(sql)
conn.commit() #提交事物。因为默认autocommit为False,关闭的。

查询数据库

mysql> select * from student;
+----+-------+------+
| id | name  | age  |
+----+-------+------+
|  2 | tom   |   18 |
|  3 | tom19 | 1819 |
|  4 | tom20 | 1820 |
|  5 | tom21 | 1821 |
|  6 | tom22 | 1822 |
|  7 | tom23 | 1823 |
|  8 | tom24 | 1824 |
|  9 | tom25 | 1825 |
| 10 | tom26 | 1826 |
| 11 | tom27 | 1827 |
| 12 | tom28 | 1828 |
+----+-------+------+

使用cursor.executemany() 方法,后面说。不常用。

一般流程

  • 建立连接
  • 获取游标
  • 执行SQL
  • 提交事务
  • 释放资源

查询

Cursor类的获取查询结果集的方法有fetchone()、fetchmany(size=None)、fetchall()

查询会根据Cursor游标移动

from ctypes import c_ushort

import pymysql
import simplejson #pip install simplejson
import os

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor()
    sql = "select * from student"
    x = cursor.execute(sql) #返回影响的行数
    print(type(x), x)

    #游标默认向前走的
    print(cursor.rownumber, cursor.rowcount)
    print(cursor.fetchone()) #拿一个
    print(cursor.rownumber, cursor.rowcount)
    print(cursor.fetchone())
    print(cursor.rownumber, cursor.rowcount)
    print(cursor.fetchmany(2)) #二元组,每一个元素一行也是元组
    print(cursor.rownumber, cursor.rowcount)
    print(cursor.fetchall())
    print(cursor.rownumber, cursor.rowcount)
    print('-' * 30)
    cursor.rownumber = 0 #游标从0开始
    print(cursor.fetchall())

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
<class 'int'> 11
0 11
(2, 'tom', 18)
1 11
(3, 'tom19', 1819)
2 11
((4, 'tom20', 1820), (5, 'tom21', 1821))
4 11
((6, 'tom22', 1822), (7, 'tom23', 1823), (8, 'tom24', 1824), (9, 'tom25', 1825), (10, 'tom26', 1826), (11, 'tom27', 1827), (12, 'tom28', 1828))
11 11
------------------------------
((2, 'tom', 18), (3, 'tom19', 1819), (4, 'tom20', 1820), (5, 'tom21', 1821), (6, 'tom22', 1822), (7, 'tom23', 1823), (8, 'tom24', 1824), (9, 'tom25', 1825), (10, 'tom26', 1826), (11, 'tom27', 1827), (12, 'tom28', 1828))

import pymysql

IP = '192.168.1.6' USERNAME = 'lqx' PASSWORD = 'lqx' DBNAME ='test' PORT = 3306

conn = None currsor = None # 对结果集的操作 try: conn = pymysql.connect(IP, USERNAME, PASSWORD, DBNAME, PORT) currsor = conn.cursor()

sql = 'select * from student' rows = currsor.execute(sql) # 返回影响的行数

print(currsor.fetchone()) print(currsor.fetchone()) print(currsor.rownumber, currsor.rowcount) print('1 ~~~~~~~~~~')

print(currsor.fetchmany(2)) print(currsor.rownumber, currsor.rowcount) print('2 ~~~~~~~~~~~')

print(currsor.fetchall()) print(currsor.rownumber, currsor.rowcount)

for x in currsor.fetchall(): print(x, '~') currsor.rownumber = 0 for x in currsor.fetchall(): print(x, '=')

finally: if currsor: currsor.close() if conn: conn.close()

执行结果 (1, 'tom', 20) (2, 'ben0', 20) 2 6 1 ~~~~~~~~~~ ((3, 'ben1', 21), (4, 'ben2', 22)) 4 6 2 ~~~~~~~~~~~ ((5, 'ben3', 23), (6, 'ben4', 24)) 6 6 (1, 'tom', 20) = (2, 'ben0', 20) = (3, 'ben1', 21) = (4, 'ben2', 22) = (5, 'ben3', 23) = (6, 'ben4', 24) =

Process finished with exit code 0

名称 说明
fetchone() 获取结果集的下一行
fetchmany(size=None) size指定返回的行数的行,None则返回空元组
fetchall() 返回剩余所有行,如果走到末尾,就返回空元组,否则返回一个元组,其元素是每一行的记录封装的一个元组
cursor.rownumber 返回当前行号。可以修改,支持负数。负向超界,拉到起始位置;正向超界,拉到末位位置
cursor.rowcount 返回的总行数

注意:fetch操作的是结果集,结果集是保存在客户端的,也就是说fetch的时候,查询已经结束了

范例:游标是在客户端操作的

from ctypes import c_ushort

import pymysql
import simplejson #pip install simplejson
import os

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor()
    sql = "select * from student"
    x = cursor.execute(sql) #返回影响的行数
    print('=' * 30)
    conn.close()
    print('=' * 30)
    print(cursor.fetchall())

finally:
    if cursor:
        cursor.close()
    # if conn:
    #     conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
==============================
==============================
((2, 'tom', 18), (3, 'tom19', 1819), (4, 'tom20', 1820), (5, 'tom21', 1821), (6, 'tom22', 1822), (7, 'tom23', 1823), (8, 'tom24', 1824), (9, 'tom25', 1825), (10, 'tom26', 1826), (11, 'tom27', 1827), (12, 'tom28', 1828))

先查后改 import pymysql

IP = '192.168.1.6' USERNAME = 'lqx' PASSWORD = 'lqx' DBNAME ='test' PORT = 3306

conn = None currsor = None # 对结果集的操作 try: conn = pymysql.connect(IP, USERNAME, PASSWORD, DBNAME, PORT) currsor = conn.cursor()

sql = 'select * from student' rows = currsor.execute(sql) # 返回影响的行数 userid = currsor.fetchone()[0] print(userid) sql = "update student set name='jerry' where id='{}'".format(userid) currsor.execute(sql)

conn.commit()

except: conn.rollback() finally: if currsor: currsor.close() if conn: conn.close()

带列名查询

Cursor类有一个Mixin的子类DictCursor

只需要

  • 导入from pymysql.cursors import DictCursor库
  • cursor = conn.cursor(DictCursor) 就可以了

具体实现

import pymysql
import simplejson #pip install simplejson
import os
from pymysql.cursors import DictCursor

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor(DictCursor)
    sql = "select * from student"
    x = cursor.execute(sql) #返回影响的行数

    print(cursor.fetchmany(2))

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
[{'id': 2, 'name': 'tom', 'age': 18}, {'id': 3, 'name': 'tom19', 'age': 1819}]

返回一行,是一个字典。

返回多行,放在列表中,元素是字典,代表一行。

SQL注入攻击

#找出用户id为3的用户信息的SQL语句如下
SELECT * from student WHERE id = 3

#现在,要求可以找出某个id对应用户的信息,代码如下
#userid = 5 # 用户id可以变
sql = 'SELECT * from student WHERE id = {}'.format(userid)

#userid可以变,例如从客户端request请求中获取,直接拼接到查询字符串中
#如果userid = '5 or 1=1'
sql = 'SELECT * from student WHERE id = {}'.format('5 or 1=1')

运行的结果竟然是返回了全部数据

import pymysql
import simplejson #pip install simplejson
import os
from pymysql.cursors import DictCursor

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor(DictCursor)

    userid = input('>>>') # tom' or '1'='1
    print(type(userid), userid)
    sql = "select * from student where name = '{}'".format(userid)
    print(sql)
    x = cursor.execute(sql) #返回影响的行数

    print(cursor.fetchall())

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
>>>tom' or '1'='1
<class 'str'> tom' or '1'='1
select * from student where name = 'tom' or '1'='1'
[{'id': 2, 'name': 'tom', 'age': 18}, {'id': 3, 'name': 'tom19', 'age': 1819}, {'id': 4, 'name': 'tom20', 'age': 1820}, {'id': 5, 'name': 'tom21', 'age': 1821}, {'id': 6, 'name': 'tom22', 'age': 1822}, {'id': 7, 'name': 'tom23', 'age': 1823}, {'id': 8, 'name': 'tom24', 'age': 1824}, {'id': 9, 'name': 'tom25', 'age': 1825}, {'id': 10, 'name': 'tom26', 'age': 1826}, {'id': 11, 'name': 'tom27', 'age': 1827}, {'id': 12, 'name': 'tom28', 'age': 1828}]
SQL注入攻击

猜测后台数据库的查询语句使用拼接字符串等方式,从而经过设计为服务端传参,令其拼接出特殊字符串的SQL语句,返回攻击者想要的结果。

永远不要相信客户端传来的数据是规范及安全的!!!

如何解决注入攻击

参数化查询

  • 可以有效防止注入攻击,并提高查询的效率
Cursor.execute(query, args=None)
  • args,必须是元组、列表或字典。
  • 如果查询字符串使用%(name)s,就必须使用字典
import pymysql
import simplejson #pip install simplejson
import os
from pymysql.cursors import DictCursor

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = None
cursor = None
try:
    conn = pymysql.connect(**conf)
    #cursor 游标
    cursor = conn.cursor(DictCursor)

    userid = input('>>>') # 2 or 1=1
    print(type(userid), userid)
    # sql = "select * from student where name = '{}'".format(userid)
    # print(sql)
    #参数化查询,sql字符串必须使用c printf风格
    sql = "select * from student where id=%s"
    #会转换成 select * from student where id='2 or 1=1'
    x = cursor.execute(sql, userid) #args 可以是元组、列表、字典

    print(cursor.fetchall())

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()
#返回结果
{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
>>>2 or 1=1
<class 'str'> 2 or 1=1
[{'id': 2, 'name': 'tom', 'age': 18}]


#sql执行
mysql> select * from student where id='2 or 1=1'
    -> ;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | tom  |   18 |
+----+------+------+
参数化查询为什么提高效率?

原因就是——SQL语句缓存

  • 数据库服务器一般会对SQL语句编译和缓存,编译只对SQL语句部分,所以参数中就算有SQL指令也不会被当做指令执行。
  • 编译过程,需要词法分析、语法分析、生成AST、优化、生成执行计划等过程,比较耗费资源。
  • 服务端会先查找是否对同一条查询语句进行了缓存,如果缓存未失效,则不需要再次编译,从而降低了编译的成本,降低了内存消耗。
  • 可以认为SQL语句字符串就是一个key,如果使用拼接方案,每次发过去的SQL语句都不一样,都需要编译并缓存。

大量查询的时候,首选使用参数化查询,以节省资源。

开发时,应该使用参数化查询。主要目的不是为了语句缓存,而是为了有效消除注入攻击。

注意:这里说的是查询字符串的缓存,不是查询结果的缓存。

批量执行executemany()

import pymysql
from pymysql.cursors import DictCursor

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306

conn = None
currsor = None # 对结果集的操作
try:
    conn = pymysql.connect(IP, USERNAME, PASSWORD, DBNAME, PORT)
    currsor = conn.cursor()

    sql = "insert into student (name,age) values(%s, %s)"
    currsor.executemany(sql, (
        ('ben{}'.format(i), 20+i) for i in range(5)
    ))

    conn.commit()
except Exception as e:
    print(e)
    conn.rollback()
finally:
    if currsor:
        currsor.close()
    if conn:
        conn.close()

上下文支持

查看连接类和游标类的源码

# 连接类
class Connection(object): 
    def __enter__(self):
        return self
    def __exit__(self, *exc_info):
        del exc_info
        self.close()

# 游标类
class Cursor(object): 
  def __enter__(self): 
    return self

    def __exit__(self, *exc_info): 
      del exc_info 
      self.close()

    def close(self):
        conn = self.connection
        if conn is None:
            return
        try:
            while self.nextset():
                pass
        finally:
            self.connection = None

连接类进入上下文的时候会返回一个游标对象,退出时如果没有异常会提交更改 游标类也使用上下文,在退出时关闭游标对象

import pymysql
import simplejson #pip install simplejson
import os
from pymysql.cursors import DictCursor

with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,

conn = pymysql.connect(**conf)

with conn:
    #cursor 游标
    # cursor = conn.cursor(DictCursor)
    with conn.cursor() as cursor:
        userid = 2
        print(type(userid), userid)
        # sql = "select * from student where name = '{}'".format(userid)
        # print(sql)
        #参数化查询,sql字符串必须使用c printf风格
        sql = "select * from student where id=%s"
        #会转换成 select * from student where id='2 or 1=1'
        x = cursor.execute(sql, userid) #args 可以是元组、列表、字典
        print(cursor.fetchall())

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
<class 'int'> 2
((2, 'tom', 18),)

mysqlclient

安装

pip install mysqlclient

使用

import MySQLdb #mysqlclient是MySQLdb上一个分支项目
import simplejson #pip install simplejson
import os


with open(os.path.join(os.path.dirname(__file__), 'config.json')) as f:
    conf = simplejson.load(f)  #反序列化,字典
print(conf)

#connection #从配置文件来. ini, json
#user = None, password = "",host = None,database = None,port = 0,
conn = MySQLdb.connect(**conf)
# conn = pymysql.connect(**conf)

with conn:
    #cursor 游标
    # cursor = conn.cursor(DictCursor)
    with conn.cursor() as cursor:
        userid = 2
        print(type(userid), userid)
        # sql = "select * from student where name = '{}'".format(userid)
        # print(sql)
        #参数化查询,sql字符串必须使用c printf风格
        sql = "select * from student where id=%s"
        #会转换成 select * from student where id='2 or 1=1'
        x = cursor.execute(sql, (userid, )) #args 可以是bytes
        print(cursor.fetchall())

返回结果

{'host': '192.168.226.128', 'password': '123456', 'database': 'test', 'user': 'root', 'port': 3306}
<class 'int'> 2
((2, 'tom', 18),)

你这样直接使用sql是最快的,但要面向对象地使用mysql,就得用ORM。

元编程

元编程概念来自LISP和smalltalk。

我们写程序是直接写代码,是否能够用代码来生成未来我们需要的代码吗?这就是元编程。

例如,我们写一个类class A,能否用代码生成一个类出来?

用来生成代码的程序称为元程序metaprogram,编写这种程序就称为元编程metaprogramming。

Python语言能够通过反射实现元编程。

Python中

  • 所有非object类都继承自object类
  • 所有类的类型包括type类都是type
  • type类继承自object类
  • object类的类型也是type类

type类

type构建类

class type(object):
    def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
        """
        type(object_or_name, bases, dict)
        type(object) -> the object's type
        type(name, bases, dict) -> a new type
        # (copied from class doc)
        """
        pass   
  • type(object) -> the object's type,返回对象的类型,例如type(10)
  • type(name, bases, dict) -> a new type, 返回一个新的类型
XClass = type('myclass', (object,), {'a':100, "b":'string'}) # 类名myclass;父类是(objcect,), 可直接为空元组()。{}中是属性。
print(XClass)
print('-' * 30)
print(XClass.__dict__)
print('-' * 30)
print(XClass.__name__)
print('-' * 30)
print(XClass.__bases__)
print('-' * 30)
print(XClass.mro())

# 执行结果
<class '__main__.myclass'>
------------------------------
{'a': 100, 'b': 'string', '__module__': '__main__',\
 '__dict__': <attribute '__dict__' of 'myclass' objects>, \
 '__weakref__': <attribute '__weakref__' of 'myclass' objects>, '__doc__': None}
------------------------------
myclass
------------------------------
(<class 'object'>,)
------------------------------
[<class '__main__.myclass'>, <class 'object'>]

创建更加复杂的类

def __init__(self):
    self.x = 1000

def show(self):
    print(self.__dict__)

XClass = type('myclass', (object,), {'a':100, 'show':show, '__init__':__init__})
print(XClass)
print('-' * 30)
print(XClass.__dict__)
print('-' * 30)
print(XClass.__name__)
print('-' * 30)
print(XClass.mro())
print('-' * 30)
XClass().show()

# 执行结果
<class '__main__.myclass'>
------------------------------
{'a': 100, 'show': <function show at 0x0000024D0C7B5158>,\
 '__init__': <function __init__ at 0x0000024D0C7B51E0>, \
 '__module__': '__main__', '__dict__': <attribute '__dict__' of 'myclass' objects>,\
  '__weakref__': <attribute '__weakref__' of 'myclass' objects>, '__doc__': None}
------------------------------
myclass
------------------------------
[<class '__main__.myclass'>, <class 'object'>]
------------------------------
{'x': 1000}

总结:可以借助type构造任何类,用代码来生成代码,这就是元编程。

构建元类

一个类可以继承自type类

注意不是继承自object类

#type是元类, 是构建其他类的类
#构建元类,必须继承type,且必须重写__new__方法
class ModelMeta(type):
    def __new__(cls, *args, **kwargs):
        print(cls)
        print(args)
        print(kwargs)
        return super().__new__(cls, *args, **kwargs)
  • 1.继承自type,ModelMeta就是元类,它可以创建出其它类
#type是元类, 是构建其他类的类
#构建元类,必须继承type,且必须重写__new__方法
class ModelMeta(type):
    def __new__(cls, *args, **kwargs):
        print(cls) #ModeMeta
        print(args) #输出有name, bases, dict
        print(kwargs) #输出为空,没用
        return super().__new__(cls, *args, **kwargs)

class A: pass #继承自object,使用元类type构造A类的对象
print(type(A), A.__bases__) #print(type(A), A.__bases__)
print('-' * 30)

class A(metaclass=ModelMeta):  #继承自object,但元类变了,使用ModelMeta构造A类的对象
    pass

print(type(A), A.__bases__) #<class '__main__.ModelMeta'> (<class 'object'>,)

返回结果

<class 'type'> (<class 'object'>,)
------------------------------
<class '__main__.ModelMeta'>
('A', (), {'__module__': '__main__', '__qualname__': 'A', '__firstlineno__': 14, '__static_attributes__': ()})
{}
<class '__main__.ModelMeta'> (<class 'object'>,)

从运行结果还可以分析出·__new__(cls, *args, **kwargs)的参数结构:

  • 中间是一个元组

    ('A', (), {'module': 'main', 'qualname': 'A', 'firstlineno': 14, 'static_attributes': ()})

  • 对应 (name, bases, dict)

修改代码如下

#type是元类, 是构建其他类的类
#构建元类,必须继承type,且必须重写__new__方法
class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        print(cls) #ModelMeta
        print(name, bases, attrs)
        return super().__new__(cls, name, bases, attrs)

class A(metaclass=ModelMeta):  #继承自object,但元类变了,使用ModelMeta构造A类的对象
    pass

print(type(A), A.__bases__)  #查元类和基类ModelMeta (object, ) 
print(A.__dict__)

返回结果

<class '__main__.ModelMeta'>
A () {'__module__': '__main__', '__qualname__': 'A', '__firstlineno__': 9, '__static_attributes__': ()}
<class '__main__.ModelMeta'> (<class 'object'>,)
{'__module__': '__main__', '__firstlineno__': 9, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
  • 第二种 B继承自A后,依然是从ModelMeta的类型
#type是元类, 是构建其他类的类
#构建元类,必须继承type,且必须重写__new__方法
class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        print(cls) #ModelMeta
        print(name, bases, attrs)
        return super().__new__(cls, name, bases, attrs)

class A(metaclass=ModelMeta):  #继承自object,但元类变了,使用ModelMeta构造A类的对象
    pass

print(type(A), A.__bases__) #查元类和基类 ModelMeta (object, ) 
print('-' * 30)

# 第二种 B继承自A后,依然是从ModelMeta的类型
class B(A): pass
print(type(B), B.__bases__) #ModelMeta 父类元类改了,子孙都改; (A, )

返回结果

<class '__main__.ModelMeta'>
A () {'__module__': '__main__', '__qualname__': 'A', '__firstlineno__': 9, '__static_attributes__': ()}
<class '__main__.ModelMeta'> (<class 'object'>,)
------------------------------
<class '__main__.ModelMeta'>
B (<class '__main__.A'>,) {'__module__': '__main__', '__qualname__': 'B', '__firstlineno__': 16, '__static_attributes__': ()}
<class '__main__.ModelMeta'> (<class '__main__.A'>,)
  • 第三种 元类就可以使用下面的方式创建新的类
#type是元类, 是构建其他类的类
class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        print(cls) #ModelMeta
        print(name, bases, attrs)
        return super().__new__(cls, name, bases, attrs)

# 第三种 元类就可以使用下面的方式创建新的类
C = ModelMeta('CCC', (), {})
print(type(C), C.__bases__) #ModelMeta, (object, ) #和class CCC(metaclass=ModelMeta): pass 是一样的效果

返回结果

<class '__main__.ModelMeta'>
CCC () {}
<class '__main__.ModelMeta'> (<class 'object'>,)
# D、E是type的实例
class D:pass #
print(type(D), D.__bases__) #type, (object,)
E = type('E', (), {})
print(type(E), E.__bases__) #type, (object,)

返回结果

<class 'type'> (<class 'object'>,)
<class 'type'> (<class 'object'>,)
#type是元类, 是构建其他类的类
class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        print(cls) #ModelMeta
        print(name, bases, attrs)
        return super().__new__(cls, name, bases, attrs)

class F(ModelMeta): pass
print(type(F), F.__bases__) #type, (ModelMeta,)
print(F.mro(F))

返回结果

<class 'type'> (<class '__main__.ModelMeta'>,)
[<class '__main__.F'>, <class '__main__.ModelMeta'>, <class 'type'>, <class 'object'>]
  • 从运行结果可以看出,只要元类是ModelMeta,创建类对象时,就会调用ModelMeta的__new__方法
  • 上例中,F也是元类, F –继承自–> ModelMeta –继承自–> type
  • type(元类) 返回type。但是type被metaclass修改了的元类的类返回其元类

元类的应用

class ModelMeta(type):
    def __new__(cls, name, bases, attrs): #构造类对象Student时进来了
        print("ModelMeta ~~~~")
        return super().__new__(cls, name, bases, attrs)

class Model(metaclass=ModelMeta):
    pass

class Student(Model):  #Model子类,Model元类改一下。我想对类对象Student修改 ORM初步
    pass

返回结果

ModelMeta ~~~~
ModelMeta ~~~~
class Field:
    def __init__(self, fieldname=None, pk=False, nullable=False):
        self.fieldname = fieldname
        self.pk = pk
        self.nullable = nullable
    def __repr__(self):
        return "<Field ({}, {}, {})>".format(
            self.fieldname, self.pk, self.nullable
        )

    __str__ = __repr__

class ModelMeta(type):
    def __new__(cls, name, bases, attrs): #构造类对象Student时进来了
        print("ModelMeta ~~~~")
        if name != 'Model':
            print(name)
            print(bases)
            print(attrs)
        return super().__new__(cls, name, bases, attrs)

class Model(metaclass=ModelMeta):
    pass

class Student(Model):  #Model子类,Model元类改一下。我想对类对象Student修改 ORM初步
    id = Field(pk=True)
    name = Field(fieldname='username', nullable=False)
    age = Field(nullable=True)

返回结果

ModelMeta ~~~~
ModelMeta ~~~~
Student
(<class '__main__.Model'>,)
{'__module__': '__main__', '__qualname__': 'Student', '__firstlineno__': 25, \
             'id': <Field (None, True, False)>, \
             'name': <Field (username, False, False)>,\
             'age': <Field (None, False, True)>, \
             '__static_attributes__': ()}

解决字段问题

class Field:
    def __init__(self, fieldname=None, pk=False, nullable=False):
        self.fieldname = fieldname
        self.pk = pk
        self.nullable = nullable
    def __repr__(self):
        return "<Field ({}, {}, {})>".format(
            self.fieldname, self.pk, self.nullable
        )

    __str__ = __repr__

class ModelMeta(type): 
    def __new__(cls, name, bases, attrs): #构造类对象Student时进来了
        print("ModelMeta ~~~~")
        if name != 'Model':
            print(attrs)
            #加表名
            if 'db_table' not in attrs:
                attrs['db_table'] = name.lower()
            #判断主键、列名
            primarykey = []
            for k, v in attrs.items():
                if isinstance(v, Field):
                    if not v.fieldname or v.fieldname.strip() == '':
                        v.fieldname = k
                    if v.pk:
                        primarykey.append(k)
            attrs['__primarykey__'] = primarykey
            print(attrs)

        return super().__new__(cls, name, bases, attrs)

class Model(metaclass=ModelMeta):
    pass

#通过元编程,Student写的很少。这样用户在使用中写很少代码,在元类中做了很多工作
class Student(Model):  #Model子类,Model元类改一下。我想对类对象Student修改 ORM初步
    id = Field(pk=True)
    name = Field(fieldname='username', nullable=False)
    age = Field(nullable=True)

print('=' * 30)
print(Student.__dict__)

s1 = Student()
print(s1.db_table, s1.__primarykey__)

返回结果

ModelMeta ~~~~
ModelMeta ~~~~
{'__module__': '__main__', '__qualname__': 'Student', '__firstlineno__': 37, \
             'id': <Field (None, True, False)>, \
             'name': <Field (username, False, False)>, \
             'age': <Field (None, False, True)>,\
             '__static_attributes__': ()}
{'__module__': '__main__', '__qualname__': 'Student', '__firstlineno__': 37, \
             'id': <Field (id, True, False)>, \
             'name': <Field (username, False, False)>, \
             'age': <Field (age, False, True)>, \
             '__static_attributes__': (), \
             'db_table': 'student', \
             '__primarykey__': ['id']}
==============================
{'__module__': '__main__', '__firstlineno__': 37, \
             'id': <Field (id, True, False)>, \
             'name': <Field (username, False, False)>, \
             'age': <Field (age, False, True)>, \
             '__static_attributes__': (), \
             'db_table': 'student', '__primarykey__': ['id'], '__doc__': None}
student ['id']

元编程总结

元类是制造类的工厂,是用来构造类的类。

构造好元类,就可以在类定义时,使用关键字参数·metaclass·指定元类,可以使用最原始的·metatype(name, bases, dict)·的方式构造一个类。

元类的 __new__()方法中,可以获取元类信息、当前类、基类、类属性字典。

元编程一般用于框架开发中。

  • 开发中除非你明确的知道自己在干什么,否则不要随便使用元编程
  • 99%的情况下用不到元类,可能有些程序员一辈子都不会使用元类

Django、SQLAlchemy使用了元类,让我们使用起来很方便

ORM

ORM,对象关系映射,对象和关系之间的映射,使用面向对象的方式来操作数据库。

关系模型和Python对象之间的映射
table => class     ,表映射为类
row => object      ,行映射为实例
column => property ,字段映射为属性

举例, 有表student,字段为id int,name varchar,age int

映射到Python为

class Student: 
    id = ?某类型字段 
    name = ?某类型字段 
    age = ?某类型字段

最终得到实例
class Student: 
    def __init__(self):
        self.id = ?
        self.name = ?
        self.age = ?

SQLALchemy ORM

SQLAlchemy是一个ORM框架

安装

pip install sqlalchemy

文档

官方文档 http://docs.sqlalchemy.org/en/latest/

查看版本

import sqlalchemy
print(sqlalchemy.__version__)# 1.3.5

开发

SQLAlchemy内部使用了连接池

创建连接

数据库连接的事情,交给引擎

from sqlalchemy import create_engine
dialect+driver://username:password@host:port/database

#mysqldb的连接
mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = sqlalchemy.create_engine("mysql+mysqldb://lqx:[email protected]:3306/hello")

#pymysql的连接
mysql+pymysql://<username>:<password>@<host>/<dbname>[?<options>]
engine = sqlalchemy.create_engine("mysql+pymysql://lqx:[email protected]:3306/hello")
engine = sqlalchemy.create_engine("mysql+pymysql://lqx:[email protected]:3306/hello", echo=True)
  • echo=True

    所有的操作都输入到日志。引擎是否打印执行的语句,调试的时候打开很方便

  • lazy connecting

    懒连接。创建引擎并不会马上连接数据库,直到让数据库执行任务时才连接

import sqlalchemy
from sqlalchemy import create_engine

# mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306

engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True)

print(engine)

# 执行结果
Engine(mysql+pymysql://lqx:***@192.168.1.6:3306/test)

Declare a Mapping创建映射

创建基类

from sqlalchemy.ext.declarative import declarative_base
# 创建基类,便于实体类继承。SQLAlchemy大量使用了元编程
Base = declarative_base()

创建实体类

student表

CREATE TABLE student (
    id INTEGER NOT NULL AUTO_INCREMENT,
    name VARCHAR(64) NOT NULL,
    age INTEGER,
    PRIMARY KEY (id)
)

做关系映射

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base

# ORM Mapping
Base = declarative_base() # 基类

class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)
    # 第一个参数是字段名,如果和属性名不一致,一定要指定
    # age = Column('age', Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )

# 查看表结构
print(Student)
print(repr(Student.__table__))

# 显示结果
<class '__main__.Student'>
Table('student', MetaData(bind=None), 
  Column('id', Integer(), table=<student>, primary_key=True, nullable=False),  
  Column('name', String(length=64), table=<student>, nullable=False), 
  Column('age', Integer(), table=<student>), 
  schema=None)

__tablename__指定表名

Column类指定对应的字段,必须指定

实例化

s = Student(name='tom')
print(s.name) # tom
s.age= 20
print(s.age) # 20 

创建表

可以使用SQLAlchemy来创建、删除表

  1. 删除继承自Base的所有表

    Base.metadata.drop_all(engine)

  2. 创建继承自Base的所有表

    Base.metadata.create_all(engine)

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

print(engine)


# ORM Mapping
Base = declarative_base() # 基类

class Student(Base):
    # 指定表名
    __tablename__= 'student'

    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )

student = Student(id=1, name='jerry')
student.name = 'tom'
student.age= 20

print(student)

生产环境很少这样创建表,都是系统上线的时候由脚本生成

生成环境很少删除表,宁可废弃都不能删除

创建回话session

在一个会话中操作数据库,会话建立在连接上,连接被引擎管理。

当第一次使用数据库时,从引擎维护的连接池中获取一个连接使用。

from sqlalchemy.orm import sessionmaker

# 创建session
Session = sessionmaker(bind=engine) # 工厂方法返回类
session = Session() # 实例化
# 依然在第一次使用时连接数据库

session对象线程不安全。所以不同线程应该使用不用的session对象。

Session类和engine有一个就行了

CRUD操作

  • add():增加一个对象
  • add_all():可迭代对象,元素是对象
import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )

# 删除表
Base.metadata.drop_all(engine)
# 创建表
Base.metadata.create_all(engine)

# 创建seesion
Session = sessionmaker(bind=engine)
session = Session()

s = Student(name='tom') # 构造时传入
s.age = 20 # 属性赋值
print(s)

session.add(s)
print(s)
session.commit()
print(s)
print('~~~~~~')

try:
    session.add_all([s])
    print(s)
    session.commit() # 能提交成功吗?
    print(s)
except:
    session.rollbake()
    print('roll back')
    raise

add_all()方法不会提交成功的,不是因为它不对,而是s,s成功提交后,s的主键就有了值,所以,只要s没有修改过,就认为没有改动。如下,s变化了,就可以提交修改了

s.name = 'jerry' # 修改
session.add_all([s])

s主键没有值,就是新增;主键有值,就是找到主键对应的记录修改

简单查询 query()

使用query()方法,返回一个Query对象

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )

# # 删除表
# Base.metadata.drop_all(engine)
# 创建表
# Base.metadata.create_all(engine)

# 创建seesion
Session = sessionmaker(bind=engine)
session = Session()

students = session.query(Student)
print(students.count())
for student in students:
    print(student)
print('~~~~~~~~~~~')

student = session.query(Student).get(1) # 通过主键查询
print(student)

query方法将实体类传入,返回类的对象可迭代对象,这时候并不查询。迭代它就执行SQL来查询数据库,封装数据到指定类的实例

get方法使用主键查询,返回一条传入类的一个实例

修改
  • 修改需先查询,在修改,不然会调用插入方法
import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )

# # 删除表
# Base.metadata.drop_all(engine)
# 创建表
# Base.metadata.create_all(engine)

# 创建seesion
Session = sessionmaker(bind=engine)
session = Session()

student = session.query(Student).get(1) # 通过主键查询
print(student)
student.name = 'ben'
student.age = 30
print(student)
session.add(student)
session.commit()
删除

先看下数据库,表中有

1 ben 30

编写如下程序来删除,会发生什么?

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )


Session = sessionmaker(bind=engine)
session = Session()

try:
    student = Student(id=1, name='ben', age=30)

    session.delete(student)
    session.commit()
except Exception as e:
    session.rollback()
    print('roll back')
    print(e)

# 执行结果
roll back
Instance '<Student at 0x1779fea9b70>' is not persisted

会产生一个异常

Instance '<Student at 0x3e654e0>' is not persisted 未持久的异常!

状态**

需导入from sqlalchemy.orm.state import InstanceState库

  • 每一个实体,都有一个状态属性_sa_instance_state,其类型是sqlalchemy.orm.state.InstanceState
  • 使用sqlalchemy.inspect(entity)函数查看状态

    常见的状态值有transient、pending、persistent、deleted、detached

状态 说明
transient 实体类尚未加入到session中,同时并没有保存到数据库中
pending transient的实体被add()到session中,状态切换到pending,但它还没有flush到数据库中
persistent session中的实体对象对应着数据库中的真实记录。pending状态在提交成功后可以变成persistent状态,或者查询成功返回的实体也是persistent状态
deleted 实体被删除且已经flush但未commit完成。事务提交成功了,实体变成detached,事务失败,返回persistent状态
detached 删除成功的实体进入这个状态
新建一个实体,状态是transient临时的
一旦add()后从transient变成pending状态
成功commit()后从pending变成persistent状态
成功查询返回的实体对象,也是persistent状态

persistent状态的实体,修改依然是persistent状态
persistent状态的实体,删除后,flush但没有commit,就变成deteled状态
成功提交,变为detached状态提交失败,还原到persistent状态。flush方法,主动把改变应用到数据库中去

删除、修改操作,需要对应一个真实的记录,所以要求实体对象是persistent状态

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.state import InstanceState

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )


Session = sessionmaker(bind=engine)
session = Session()

def getstate(instance, i):
    inp:InstanceState = sqlalchemy.inspect(student)
    states = "{}: key={}\nid={}, attached={}, transient={}," \
             "pending={}, \npersistant={}, deleted={}, detached={}".format(
        i ,inp.key,
        inp.session_id, inp._attached, inp.transient,
        inp.pending, inp.persistent, inp.deleted, inp.detached
    )
    print(states, end='\n~~~~~~~~~\n')

student = session.query(Student).get(1)
getstate(student, 1) # persistent

try:
    student = Student(id=1, name='ben', age=30)
    getstate(student, 2) # transit

    student = Student(name='tom', age=20)
    getstate(student, 3) # transit
    session.add(student) # add后变成pending
    getstate(student,4) # pending
    # session.delete(student) # 异常,删除的前提必须是persistent,也就是说先查后删
    session.commit() # 提交后,变成persistent
    getstate(student, 6) # persistent
except Exception as e:
    session.rollback()
    print('roll back')

运行结果

1: key=(<class '__main__.Student'>, (1,), None)
id=1, attached=True, transient=False,pending=False, 
persistant=True, deleted=False, detached=False
persistent就是key不为None,附加的,且不是删除的,有sessionid
~~~~~~~~~
2: key=None
id=None, attached=False, transient=True,pending=False, 
persistant=False, deleted=False, detached=False
transient的key为None,且无附加
~~~~~~~~~
3: key=None
id=None, attached=False, transient=True,pending=False, 
persistant=False, deleted=False, detached=False
同上
~~~~~~~~~
4: key=None
id=1, attached=True, transient=False,pending=True, 
persistant=False, deleted=False, detached=False
add后变成pending,已附加,但是没有key,有了sessionid
~~~~~~~~~
6: key=(<class '__main__.Student'>, (2,), None)
id=1, attached=True, transient=False,pending=False, 
persistant=True, deleted=False, detached=False
提交成功后,变成persistent,有了key
~~~~~~~~~
Process finished with exit code 0

删除状态的变化

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.state import InstanceState

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True) # lazy 懒

# ORM Mapping
Base = declarative_base() # 基类

# 创建实体类
class Student(Base):
    # 指定表名
    __tablename__= 'student'
    # 定义类属性对应字段
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    age = Column(Integer)

    def __repr__(self):
        return "<{} id={}, name={}, age={}>".format(
            __class__.__name__, self.id, self.name, self.age
        )


Session = sessionmaker(bind=engine)
session = Session()

def getstate(instance, i):
    inp:InstanceState = sqlalchemy.inspect(student)
    states = "{}: key={}\nid={}, attached={}, transient={}," \
             "pending={}, \npersistant={}, deleted={}, detached={}".format(
        i ,inp.key,
        inp.session_id, inp._attached, inp.transient,
        inp.pending, inp.persistent, inp.deleted, inp.detached
    )
    print(states, end='\n~~~~~~~~~\n')

student = session.query(Student).get(1)
getstate(student, 7) # persistent

try:
    session.delete(student) # 删除的前提是persistent

    getstate(student, 8) # persistent
    session.flush()
    getstate(student, 9) # deleted
    session.commit()
    getstate(student, 13) # detached
except Exception as e:
    session.rollback()
    print('roll back')

执行结果

7: key=(<class '__main__.Student'>, (1,), None)
id=1, attached=True, transient=False,pending=False, 
persistant=True, deleted=False, detached=False
~~~~~~~~~
8: key=(<class '__main__.Student'>, (1,), None)
id=1, attached=True, transient=False,pending=False, 
persistant=True, deleted=False, detached=False
~~~~~~~~~
2019-06-27 16:49:04,554 INFO sqlalchemy.engine.base.Engine DELETE FROM student WHERE student.id = %(id)s
2019-06-27 16:49:04,554 INFO sqlalchemy.engine.base.Engine {'id': 1}
9: key=(<class '__main__.Student'>, (1,), None)
id=1, attached=True, transient=False,pending=False, 
persistant=False, deleted=True, detached=False
delete后flush,状态变成deleted,不过是附加的
~~~~~~~~~

2019-06-27 16:49:04,556 INFO sqlalchemy.engine.base.Engine COMMIT
一旦提交后
13: key=(<class '__main__.Student'>, (1,), None)
id=None, attached=False, transient=False,pending=False, 
persistant=False, deleted=False, detached=True
状态转为detached
~~~~~~~~~
复杂查询 filter

实体类

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer, Date, Enum, ForeignKey
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.state import InstanceState
import enum

Base = declarative_base() # 基类

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True)

Session = sessionmaker(bind=engine)
session = Session()

class MyEnum(enum.Enum):
    M = 'M'
    F = 'F'


class Employee(Base):
    # 指定表名
    __tablename__ = 'employees'

    # 定义类属性对应字段
    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(Enum(MyEnum), nullable=False)
    hire_date = Column(Date, nullable=False)

    def __repr__(self):
        return "<{} no={}, name={}, gender={}>".format(
            __class__.__name__, self.emp_no, "{} {}".format(
                self.first_name, self.last_name), self.gender.value
        )

# 打印函数
def show(emps):
    for x in emps:
        print(x)
    print('~~~~~~~~~~~~\n')

# 简单条件查询
emps = session.query(Employee).filter(Employee.emp_no > 10015)
show(emps)
  • 与或非

    需导入from sqlalchemy import or_, and_, not_

    查询 与 and 四种方式

    emps = session.query(Employee).filter(Employee.emp_no > 10015, Employee.emp_no < 10018)
    show(emps)
    
    emps = session.query(Employee).filter(emps.emp_no > 10015).filter(Employee.emp_no < 10018)
    show(emps)
    
    emps = session.query(Employee).filter(and_(Employee.emp_no > 10015, Employee.emp_no <10018))
    show(emps)
    
    emps = session.query(Employee).filter((Employee.emp_no > 10015) & (Employee.emp_no < 10018))
    show(emps)
    

    查询 或 or 两种方法

    emps = session.query(Employee).filter(or_(Employee.emp_no > 10015, Employee.emp_no < 10018))
    show(emps)
    
    emps = session.query(Employee).filter((Employee.emp_no > 10015) | (Employee.emp_no < 10018))
    show(emps)
    

    查询 非 两种方法

    emps = session.query(Employee).filter(not_(Employee.emp_no < 10018))
    show(emps)
    
    emps = session.query(Employee).filter(~(Employee.emp_no > 10018))
    show(emps)
    

    in 操作

    emps = session.query(Employee).filter(Employee.emp_no.in_([10015, 10018, 10020]))
    show(emps)
    

    not in 操作

    emps = session.query(Employee).filter(~Employee.emp_no.in_([10015, 10018, 10020]))
    show(emps)
    
    emps = session.query(Employee).filter(~Employee.emp_no.notin_([10015, 10018, 10020]))
    show(emps)
    

    like 字符串匹配操作

    emps = session.query(Employee).filter(Employee.last_name.like('p%'))
    show(emps)
    

    not like

    emps = session.query(Employee).filter(Employee.last_name.notlike('p%'))
    show(emps)
    

    ilike可以忽略带小写匹配

排序 order_by
#升序
emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no)
emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no.asc())
show(emps)

#降序
emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no.desc())
show(emps)

#多列排序
emps = session.query(Employee).filter(Employee.emp_no >
    10010).order_by(Employee.last_name).order_by(Employee.emp_no.desc())
show(emps)
分页 limit
emps = session.query(Employee).limit(4)
show(emps)

emps = session.query(Employee).limit(4).offset(18)
show(emps)
消费者方法

消费者方法调用后,Query对象(可迭代)就转换成了一个容器

# 总行数
emps = session.query(Employee)
print(emps.count()) # 聚合函数count(*)的查询

# 取所有数据
print(emps.all()) # 返回列表,查不到返回空列表

# 取首行
print(emps.first()) # 返回首行,查不到返回None,等价limit

# 有且只能有一行
print(emps.one()) #如果查询结果是多行抛异常
print(emps.limit(1).one())

# 删除 delete by query
session.query(Employee).filter(Employee.emp_no > 10018).delete()
session.commmit # 提交则删除
聚合、分组

需导入 from sqlalchemy import func

聚合函数

# count
from sqlalchemy import func

query = session.query(func.count(Employee.emp_no)) 
print(query.all()) # 列表中一个元素
print(query.first) # 一个只有一个元组的元组
print(query.one()) # 只能有一行返回,一个元组
print(query.scalar()) # 取one()的第一个元素

# max/min/avg
print(session.query(func.max(Employee.emp_no)).scalar())
print(session.query(func.min(Employee.emp_no)).scalar())
print(session.query(func.avg(Employee.emp_no)).scalar())

分组

query = session.query(Employee.gender,
    func.count(Employee.emp_no)).group_by(Employee.gender).all()
for g,y in query:
    print(g.value, y)
关联查询
-- 员工表
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- 部门表
CREATE TABLE `departments` (
  `dept_no` char(4) NOT NULL,
  `dept_name` varchar(40) NOT NULL,
  PRIMARY KEY (`dept_no`),
  UNIQUE KEY `dept_name` (`dept_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- 第三张表
CREATE TABLE `dept_emp` (
  `emp_no` int(11) NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`dept_no`),
  KEY `dept_no` (`dept_no`),
  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,
  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

从语句看出员工、部门之间的关系是多对多关系。

先把这些表的Model类和字段属性建立起来。

import sqlalchemy
from sqlalchemy import create_engine, Column, String, Integer, Date, Enum, ForeignKey
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.state import InstanceState
from sqlalchemy import or_, and_, not_
import enum
from sqlalchemy import func

Base = declarative_base() # 基类

IP = '192.168.1.6'
USERNAME = 'lqx'
PASSWORD = 'lqx'
DBNAME ='test'
PORT = 3306
engine = create_engine("mysql+pymysql://{}:{}@{}:{}/{}".format(
    USERNAME, PASSWORD, IP, PORT, DBNAME
), echo=True)

Session = sessionmaker(bind=engine)
session = Session()

class MyEnum(enum.Enum):
    M = 'M'
    F = 'F'

# 打印函数
def show(emps):
    for x in emps:
        print(x)
    print('~~~~~~~~~~~~\n')

class Employee(Base):
    # 指定表名
    __tablename__ = 'employees'

    # 定义类属性对应字段
    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(Enum(MyEnum), nullable=False)
    hire_date = Column(Date, nullable=False)

    def __repr__(self):
        return "<{} no={}, name={}, gender={}>".format(
            __class__.__name__, self.emp_no, "{} {}".format(
                self.first_name, self.last_name), self.gender.value
        )

class Department(Base):
    __tablename__ = 'departments'

    dept_no = Column(String(4), primary_key=True)
    dept_name = Column(String(40), nullable=False, unique=True)

    def __repr__(self):
        return "{} no={} name={}".format(
            type(self).__name__, self.dept_no, self.dept_name)

class Dept_emp(Base):
    __tablename__ = 'dept_emp'

    emp_no = Column(Integer, ForeignKey('employees.emp_no',
                    ondelete='CASCADE'), primary_key=True)
    dept_no = Column(String(4), ForeignKey('depatments.dept_no', ondelete='CASCADE'),
                     primary_key=True)
    from_date = Column(Date, nullable=False)
    to_date = Column(Date, nullable=False)

    def __repr__(self):
        return "{} empno={} deptno={}".format(
            type(self).__name__, self.emp_no, self.dept_no)

ForeignKey('employees.emp_no', ondelete='CASCADE')定义外键约束

需求:查询10010员工的所在的部门编号及员工信息

  • 1、使用隐式内连接
# 查询10010员工的所在的部门编号及员工信息
results = session.query(Employee, Dept_emp).filter(
    Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010).all()

show(results)

# 查询结果2行
(<Employee no=10010, name=Duangkaew Piveteau, gender=F>, Dept_emp empno=10010 deptno=d004)
(<Employee no=10010, name=Duangkaew Piveteau, gender=F>, Dept_emp empno=10010 deptno=d006)

这种方式会产生隐式连接的语句

SELECT * 
FROM employees, dept_emp 
WHERE employees.emp_no = dept_emp.emp_no AND employees.emp_no = 10010
  • 2、使用join
# 查询10010员工的所在的部门编号及员工信息
# 第一种写法
results = session.query(Employee).join(Dept_emp).filter(Employee.emp_no == 10010).all()

第二种写法
results = session.query(Employee).join(Dept_emp,
            Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010).all()

show(results)

这两种写法,返回都只有一行数据,为什么?

  • 它们生成的SQL语句是一样的,执行该SQL语句返回确实是2行记录,可是Python中的返回值列表中只有一个元素?
  • 原因在于 query(Employee) 这个只能返回一个实体对象中去,为了解决这个问题,需要修改实体类Employee,增加属性用来存放部门信息

sqlalchemy.orm.relationship(实体类名字符串), 需导入from sqlalchemy.orm import sessionmaker,relationship

from sqlalchemy.orm import sessionmaker,relationship

class Employee(Base):
    # 指定表名
    __tablename__ = 'employees'

    # 定义类属性对应字段
    emp_no = Column(Integer, primary_key=True)
    birth_date = Column(Date, nullable=False)
    first_name = Column(String(14), nullable=False)
    last_name = Column(String(16), nullable=False)
    gender = Column(Enum(MyEnum), nullable=False)
    hire_date = Column(Date, nullable=False)

    departmens = relationship('Dept_emp') #

    def __repr__(self): # 注意增加self.dept_emps
        return "<{} no={}, name={}, gender={} depts={}>".format(
            __class__.__name__, self.emp_no, "{} {}".format(
            self.first_name, self.last_name), self.gender.value,
            self.departmens
        )

查询信息

# 查询10010员工的所在的部门编号及员工信息
# 第一种
results = session.query(Employee).join(Dept_emp).filter(
            Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010)

# 第二种
results = session.query(Employee).join(Dept_emp,
            Employee.emp_no == Dept_emp.emp_no).fiter(Employee.emp_no == 10010)

# 第三种
results = session.query(Employee).join(Dept_emp, 
            (Employee.emp_no == Dept_emp.emp_no) & (Employee.emp_no == 10010))
show(results.all()) # 打印结果
  • 第一种方法join(Dept_emp)中没有等值条件,会自动生成一个等值条件,如果后面有filter,哪怕是filter(Employee.emp_no == Dept_emp.emp_no),这个条件会在where中出现。第一种这种自动增加join的等值条件的方式不好,不要这么写
  • 第二种方法在join中增加等值条件,阻止了自动的等值条件的生成。这种方式推荐
  • 第三种方法就是第二种,这种方式也可以

再看一个现象

results = session.query(Employee).join(Dept_emp).filter(
            Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010)

for x in results:
    print(x.emp_no)

可以看出只要不访问departments属性,就不会查dept_emp这张表

总结

在开发中,一般都会采用ORM框架,这样就可以使用对象操作表了。

定义表映射的类,使用Column的描述器定义类属性,使用ForeignKey来定义外键约束。

如果在一个对象中,想查看其它表对应的对象的内容,就要使用relationship来定义关系。

是否使用外键约束?

  • 力挺派,能使数据保持完整性一致性。
  • 弃用派,开发难度增加,大量数据的时候影响插入、修改、删除的效率。在业务层保证数据的一致性。

Django ORM

django官网https://www.djangoproject.com/download/

文档https://docs.djangoproject.com/

对模型对象的CRUD,被Django ORM转换成相应的SQL语句以操作不同的数据源。

安装Django

安装官网LTS版本

python -m pip install Django==5.1.7

Django命令

(.venv) PS D:\project\pyproj\pycharm> django-admin    

Type 'django-admin help <subcommand>' for help on a specific subcommand.

Available subcommands:

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations #迁移
    migrate
    optimizemigration
    runserver #启动
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject #创建项目
    test
    testserver
Note that only Django core commands are listed as settings are not properly configured (error: Requested setting INSTALLED_APPS, but settings a
re not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.).

startproject

  • 创建Django项目
  • 语法:django-admin startproject name [directory]
  • 命令默认在当前目录创建一个文件夹,文件夹下包含manage.py文件以及工程文件夹,在工程文件夹下包含settings.py文件和其他必要文件。
  • django-admin startproject stduTest与django-admin startproject stduTest .的区别
    • 不带点,会多新建一个文件夹
    • 带点,当前目录创建

startapp

  • python manage.py startapp <app name>
  • 在manage管理的这个项目下创建一个app

makemigrations

  • python manage.py makemigrations
  • 在manage管理的这个项目下创建迁移文件

migrate

  • 执行迁移
  • python manage.py migrate
  • 当需要迁移的时候,执行迁移,第一次建议完全迁移
  • migrate后接app名,便是迁移某个app
    • python manage.py migrate <app name>

createsuperuser

  • 创建管理员用户和密码
  • python manage.py createsuperuser

runserver

  • 启动一个测试用的server
  • python manage.py runserver 0.0.0.0:8080
    • 直接就可以访问本机的8080端口

项目准备

django-admin startproject salary .

#创建应用
python manage.py startapp employee

范例:

#创建项目
django-admin startproject salary .

#会生成项目目录和manage.py文件
./
- salary/
- manage.py

python manage.py #django-admin能做的事,manage.py基本上都能做
Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[contenttypes]
    remove_stale_contenttypes

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    optimizemigration
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

[sessions]
    clearsessions

[staticfiles]
    collectstatic
    findstatic
    runserver


python manage.py startapp --help
usage: manage.py startapp [-h] [--template TEMPLATE] [--extension EXTENSIONS] [--name FILES] [--exclude [EXCLUDE]] [--version] [-v {0,1,2,3}]
                          [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
                          name [directory]

Creates a Django app directory structure for the given app name in the current directory or optionally in the given directory.

positional arguments:
  name                  Name of the application or project.
  directory             Optional destination directory

options:
  -h, --help            show this help message and exit
  --template TEMPLATE   The path or URL to load the template from.
  --extension, -e EXTENSIONS
                        The file extension(s) to render (default: "py"). Separate multiple extensions with commas, or use -e multiple times.   
  --name, -n FILES      The file name(s) to render. Separate multiple file names with commas, or use -n multiple times.
  --exclude, -x [EXCLUDE]
                        The directory name(s) to exclude, in addition to .git and __pycache__. Can be used multiple times.
  --version             Show program's version number and exit.
  -v, --verbosity {0,1,2,3}
                        Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output
  --settings SETTINGS   The Python path to a settings module, e.g. "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be used.
  --pythonpath PYTHONPATH
                        A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
  --traceback           Raise on CommandError exceptions.
  --no-color            Don't colorize the command output.
  --force-color         Force colorization of the command output.

#创建应用
python manage.py startapp employee

./
- employee/
- salary/
- manage.py

会生成一个应用名命名的文件夹

配置: 打开salary/settings.py主配置文件 -​ 修改数据库 -​ 配置修改时区 -​ 注册应用

#生产环境下一定要把DEBUG设置为False
DEBUG = True

#注册应用,添加上自己的应用
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'employee',         #添加上自己的刚刚创建的应用
]

#修改数据库配置
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
#ENGINE 要使用的数据库后端。内置的数据库后端有:
#- 'django.db.backends.postgresql'
#- 'django.db.backends.mysql'
#- 'django.db.backends.sqlite3'
#- 'django.db.backends.oracle'
#加上自己数据库的配置段
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'test',   #数据库名
        'USER': 'root',     #用户名
        'PASSWORD': '123456',   #密码
        'HOST': '10.0.0.12',        #主机
        'PORT': '3306',         #端口
    }
}
#修改时区
TIME_ZONE = 'Asia/Shanghai'
#修改语言
#LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-Hans'

#修改全局的编码
#需要添加DEFAULT_CHARSET关键字
DEFAULT_CHARSET='utf-8' #默认utf-8

Django日志

https://docs.djangoproject.com/en/5.1/topics/logging/#configuring-logging

logger: https://docs.djangoproject.com/zh-hans/5.1/ref/logging/#loggers

Django的日志配置在setting.py中

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

配置后,就可以在控制台看到执行的SQL语句。

注意,settings.py中必须DEBUG=True,同时loggers的level是DEBUG,否则从控制台看不到SQL语句。

模型Model

字段类型

官方文档

字段类 说明
AutoField 自增的整数字段。如果不指定,django会为模型类自动增加主键字段
BooleanField 布尔值字段,True和False。对应表单控件CheckboxInput
NullBooleanField 比BooleanField多一个null值
CharField 字符串,max_length设定字符长度。对应表单控件TextInput
TextField 大文本字段,一般超过4000个字符使用。对应表单控件Textarea
IntegerField 整数字段, 一般4字节
BigIntegerField 更大整数字段,8字节
DecimalField 使用Python的Decimal实例表示十进制浮点数。max_digits总位数,decimal_places小数点后的位数
FloatField Python的Float实例表示的浮点数
DateField 使用Python的datetime.date实例表示的日期
  auto_now=False每次修改对象自动设置为当前时间。
  auto_now_add=False对象第一次创建时自动设置为当前时间。
  auto_now_add、auto_now、default互斥。
  对应控件为TextInput,关联了一个Js编写的日历控件
TimeField 使用Python的datetime.time实例表示的时间,参数同上
DateTimeField 使用Python的datetime.datetime实例表示的时间,参数同上
FileField 一个上传文件的字段
ImageField 继承了FileField的所有属性和方法,但是对上传的文件进行校验,确保是一个有效的图片
EmailField 能做Email检验,基于CharField,默认max_length=254
GenericIPAddressField 支持IPv4、IPv6检验,缺省对应文本框输入
URLField 能做URL检验,基于基于CharField,默认max_length=200

缺省主键

缺省情况下,Django的每一个Model都有一个名为id的AutoField字段,如下

id = models.AutoField(primary_key=True)

如果显式定义了主键,这种缺省主键就不会被创建了。Python之禅中说“显示优于隐式”,所以,尽量使用自己定义的主键, 哪怕该字段就是id,也是一种不错的选择。

字段选项

官方文档

说明
db_column 表中字段的名称。如果未指定,则使用属性名
primary_key 是否主键
unique 是否是唯一键
default 缺省值。这个缺省值不是数据库字段的缺省值,而是新对象产生的时候被填入的缺省值
null 表的字段是否可为null,默认为False
blank Django表单验证中,如果True,则允许该字段为空。默认为False
db_index 字段是否有索引
to_field 关联对象的字段。默认情况下,Django 使用相关对象的主键。如果你引用了一个不同的字段,这个字段必须有 unique=True

关系类型字段类

说明
ForeignKey 外键,表示一对多
  ForeignKey('production.Manufacturer')
  自关联ForeignKey('self')
ManyToManyField 表示多对多
OneToOneField 表示一对一

一对多时,自动创建会增加_id后缀。

  • 从一访问多,使用 对象.小写模型类_set
  • 从一访问一,使用 对象.小写模型类

访问id 对象.属性_id

Model类

  • 基类 django.db.models.Model
  • 表名不指定默认使用 <appname>_<model_name> 。使用Meta类修改表名

由于Django不支持枚举类型字段,为了不引入额外的第三库,将gender字段改为整型。

模型 Meta 选项

编辑应用下的models.py文件(这里是employee/models.py)

from django.db import models
"""
`gender` enum('M','F') NOT NULL,

CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
"""
# Create your models here.
class Employee(models.Model): #metaclass=ModelBase
    class Meta:
        db_table = 'employees' #定义表名

    #将列名一一映射出来
    emp_no = models.IntegerField(primary_key=True)
    birth_date = models.DateField(null=False)
    first_name = models.CharField(null=False, max_length=14) #varchar(14) NOT NULL,
    last_name = models.CharField(null=False, max_length=16)
    gender = models.SmallIntegerField(null=False)
    hire_date = models.DateField(null=False)

    def __repr__(self):#重写可视化效果
        return '<Employee {}, {} {}>'.format(self.emp_no, self.first_name, self.last_name)

    __str__ = __repr__

在项目根目录编写一段测试代码test.py

import os
import django

#参考wsgi.py 加载初始环境
#简单的连接前,需要安装mysqlclient,使用pip install mysqlclient
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
#导入必须在连接setup之后,不然会报应用未注册
from employee.models import Employee
emps = Employee.objects.all() #这是懒查询. 结果集,本句不发起查询
# print(emps)#查询集
for i in emps: #遍历
    print(type(i), i)

返回结果

(0.002) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None; alias=default
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default       
(0.001) SELECT `employees`.`emp_no`, `employees`.`birth_date`, `employees`.`first_name`, `employees`.`last_name`, `employees`.`gender`, `employees`.`hire_date` FROM `employees`; args=(); alias=default
<class 'employee.models.Employee'> <Employee 10001, Georgi Facello>
<class 'employee.models.Employee'> <Employee 10002, Bezalel Simmel>
<class 'employee.models.Employee'> <Employee 10003, Parto Bamford>
<class 'employee.models.Employee'> <Employee 10004, Chirstian Koblick>
<class 'employee.models.Employee'> <Employee 10005, Kyoichi Maliniak>
<class 'employee.models.Employee'> <Employee 10006, Anneke Preusig>
<class 'employee.models.Employee'> <Employee 10007, Tzvetan Zielinski>
<class 'employee.models.Employee'> <Employee 10008, Saniya Kalloufi>
<class 'employee.models.Employee'> <Employee 10009, Sumant Peac>
<class 'employee.models.Employee'> <Employee 10010, Duangkaew Piveteau>
<class 'employee.models.Employee'> <Employee 10011, Mary Sluis>
<class 'employee.models.Employee'> <Employee 10012, Patricio Bridgland>
<class 'employee.models.Employee'> <Employee 10013, Eberhardt Terkki>
<class 'employee.models.Employee'> <Employee 10014, Berni Genin>
<class 'employee.models.Employee'> <Employee 10015, Guoxiang Nooteboom>
<class 'employee.models.Employee'> <Employee 10016, Kazuhito Cappelletti>
<class 'employee.models.Employee'> <Employee 10017, Cristinel Bouloucos>
<class 'employee.models.Employee'> <Employee 10018, Kazuhide Peha>
<class 'employee.models.Employee'> <Employee 10019, Lillian Haddadi>
<class 'employee.models.Employee'> <Employee 10020, Mayuko Warwick>

查出了所有记录

管理器对象

Django会为模型类提供一个objects对象,它是 django.db.models.manager.Manager类型,用于与数据库交互。当定义模型类的 时候没有指定管理器,则Django会为模型类提供一个objects的管理器。如果在 模型类中手动指定管理器后,Django不再提供默认的objects的管理器了。

管理器是Django的模型进行数据库查询操作的接口,Django应用的每个模型都至少拥有一个管理器。

用户也可以自定义管理器类,继承自django.db.models.manager.Manager,实现表级别控制。

源码

class ModelBase(type):
    """Metaclass for all models."""

    def __new__(cls, name, bases, attrs, **kwargs):
        super_new = super().__new__
        ... ...
        new_class._prepare()

    def _prepare(cls):
        """Create some methods once self._meta has been populated."""
        ... ...

        if not opts.managers:
            ... ...
            cls.add_to_class("objects", manager)

查询

查询集

查询会返回结果的集,它是django.db.models.query.QuerySet类型。

import os
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salay.settings')
django.setup()

#
from employee.models import Employee
emps=Employee.objects.all()
print(type(emps))

#结果
<class 'django.db.models.query.QuerySet'>

它是惰性求值,和sqlalchemy一样。结果就是查询的集。

它是可迭代对象。

  1. 惰性求值:

    创建查询集不会带来任何数据库的访问,直到调用方法使用数据时,才会访问数据库。在迭代、序列 化、if语句中都会立即求值。

  2. 缓存:

    每一个查询集都包含一个缓存,来最小化数据库的访问。

    新建查询集,缓存为空。首次查询集求值时,会发生数据库的查询,Django会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。

    观察下面2个例子是要看真正生成的语句了。

    1. 没有使用缓存,每次都要去查库,查了2次库
    [user.name for user in User.objects.all()]
    [user.name for user in User.objects.all()]
    
    1. 下面的语句使用缓存,因为使用同一个结果集
    qs = User.objects.all()
    [user.name for user in qs]
    [user.name for user in qs]
    

限制查询集(切片)

分页功能实现,使用限制查询集。

查询集对象可以直接使用索引下标的方式(不支持负索引),相当于SQL语句中的limit和offset子句。注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee
mgr = Employee.objects 
print(mgr.all()[1:3]) #1: 3-1 #LIMIT 2 OFFSET 1
# print(mgr.all()[10:30]) #10: 30-10 #LIMIT 20 OFFSET 10
# print(mgr.all()[:5])  #0: 5-0  #LIMIT 5

返回结果

(0.005) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None; alias=default
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
(0.003) SELECT `employees`.`emp_no`, `employees`.`birth_date`, `employees`.`first_name`, `employees`.`last_name`, `employees`.`gender`, `employees`.`hire_date` FROM `employees` LIMIT 2 OFFSET 1; args=(); alias=default
<QuerySet [<Employee 10002, Bezalel Simmel>, <Employee 10003, Parto Bamford>]>  

结果集方法

名称 说明
all() 所有的
filter() 过滤,返回满足条件的数据,如:mgr.filter(emp_no=10010) 如果未查询到,返回空
exlude() 排除,排除满足条件的数据,多条排除条件则为and关系
order_by() 排序,注意参数是字符串,倒序需要在字符串前面加 - 号,如:mgr.order_by('-emp_no')
values() 返回一个对象字典的列表,列表的元素是字典,字典内的字段和值的键值对,将字段以参数的形式带入,可以控制投影的字段
mgr =Employee.objects
print(mgr.all())
print(mgr.values())
print(mgr.values('pk','first_name'))
print(mgr.filter(pk=10010).values())
print(mgr.exclude(emp_no=10001))
print(mgr.exclude(emp_no=10002).order_by('emp_no'))
print(mgr.exclude(emp_no=10002).order_by('-pk'))
print(mgr.exclude(emp_no=10002).order_by('-pk').values())
#pk就是主键
#all\filter\exclude 返回可迭代的查询集,其中都是实例
#values 返回可迭代的字典。values中写入字段名就是投影
#order_by 返回结果集,排序。 字符串前面加-号, 降序

filter(k1=v1).filter(k2=v2) 等价于 filter(k1=v1, k2=v2)

filter(pk=10)这里pk指的就是主键,不用关心主键字段名,当然也可以使用使用主键名filter(emp_no=10)

返回单个值的方法

名称 说明
get() 仅返回单个满足条件的对象
  如果未能返回对象则抛出DoesNotExist异常;
  如果能返回多条,抛出MultipleObjectsReturned异常
count() 返回当前查询的总条数
first() 返回第一个对象,找不到返回None
last() 返回最后一个对象,返回最后一个对象,找不到返回None
exists() 判断查询集中是否有数据,如果有则返回True
#访问数据
from employee.models import Employee
mgr = Employee.objects
x = mgr.filter(emp_no=10002)
print(bool(x), len(x)) #True 1

print(mgr.filter(emp_no=10002).get()) #get严格一个
print(mgr.get(emp_no=10002)) #同上等效
# print(mgr.exclude(emp_no=10002).get()) #返回多个,报错,MultipleObjectsReturned
print(mgr.exclude(emp_no=10002).first()) #limit 1
print(mgr.exclude(emp_no=10002).values('pk').first())
print(mgr.filter(pk=10002, gender=1).first()) #AND, 找不到返回None
print(mgr.exclude(emp_no=10002).last()) #desc, limit 1
print(mgr.exclude(emp_no=10002).count())
print(mgr.count())

#get严格要求仅返回一个的对象,多于1个少于1个都报错
#first 一条数据,limt 1,找不到返回None。 last 倒排取第一条limit 1,找不到返回None
#count 计数,返回一个值

字段查询(Field Lookup)表达式

字段查询表达式可以作为filter()、exclude()、get()的参数,实现where子句。

语法: 属性名称__比较运算符=值

注意:属性名和运算符之间使用 双下划线

比较运算符如下

名称 例子 说明
exact filter(emp_no=10002) 等价于SQL语句SELECT … WHERE emp_no = 10002; 严格等于,可省略不写
  filter(emp_no__exact=10002) 等价于SQL语句SELECT … WHERE emp_no = 10002; 严格等于,可省略不写
contains exclude(title__contains='天')等价于SQL语句 SELECT … WHERE title LIKE '%天%'; 是否包含,大小写敏感,等价于like '%天%' 性能低
startswith    
endswith filter(title__startswith='天')等价于SQL语句SELECT … WHERE headline LIKE '天%';  
  filter(headline__endswith='天')等价于SQL语句SELECT … WHERE headline LIKE '%天'; 以什么开头或结尾,大小写敏感
isnull    
isnotnull filter(pub_date__isnull=True)等价于SQL语句SELECT … WHERE pub_date IS NULL; 是否为null
iexact    
icontains    
istartswith    
iendswith   i的意思是忽略大小写
in filter(emp_no__in=[1,2,3,100])等价于SQL语句SELECT … WHERE emp_no IN (1,2,3 100); 是否在指定范围数据中
gt、gte    
lt、lte filter(id__gt=3)等价于SQL语句SELECT … WHERE id>3;  
  filter(id__lte=6)等价于SQL语句SELECT … WHERE id<=6;  
  filter(pub_date__gt=datetime.date(2000,1,1))等价于SQL语句SELECT … WHERE pub_date > 2000-1-1; 大于,大于等于
    小于,小于等于
year、month、dayweek_dayhour、minute、second filter(pub_date__year=2000)等价于SELECT … WHERE pub_date between '2000-01-01' and '2000-12-31' 对日期类型属性处理
#访问数据
from employee.models import Employee
mgr = Employee.objects
print(mgr.filter(emp_no__exact=10002)) #WHERE emp_no = 10002
print(mgr.filter(pk__gt=10002)) #大于WHERE emp_no > 10002
print(mgr.filter(pk__gte=10002)) #大于等于WHERE emp_no >= 10002
print(mgr.filter(pk__lt=10002)) #WHERE emp_no < 10002
print(mgr.filter(pk__lte=10002)) #WHERE emp_no <= 10002
print(mgr.filter(pk__gt=10002, gender=1)) #WHERE emp_no > 10002 AND gender = 1
print(mgr.filter(pk__gt=10002, last_name__startswith='P')) #以P开头 WHERE emp_no > 10002 AND last_name LIKE  BINARY 'P%'
print(mgr.filter(pk__gt=10002, last_name__istartswith='P')) #忽略大小写WHERE emp_no > 10002 AND last_name LIKE 'P%'
print(mgr.filter(pk__gt=10002).filter(last_name__istartswith='P')) #同上等价
print(mgr.filter(last_name__icontains='ea')) #包含,忽略大小写。性能太低,能不用就不用。WHERE last_name LIKE '%ea%'
print(mgr.filter(pk__in=[10002,10003])) #指定范围tuple,list,集合都可 WHERE emp_no IN (10002,10003)
print(mgr.filter(pk__in=(10000+i for i in range(1, 5)))) #指定范围tuple,list,集合都可 HERE emp_no IN (10001, 10002, 10003, 10004)
print(mgr.filter(birth_date__year=2020)) #日期WHERE birth_date BETWEEN '2020-01-01' AND '2020-12-31'
#访问数据
from employee.models import Employee
mgr = Employee.objects
print(mgr.filter(pk__gt=10002, pk__lt=10010))
print(mgr.filter(pk__gt=10002).filter(pk__lt=10010))
print(mgr.filter(pk__gt=10002) & mgr.filter(pk__lt=10010)) #查询集 运算符重载

#以上三种方式等价于:
#WHERE (`employees`.`emp_no` > 10002 AND `employees`.`emp_no` < 10010)

Q对象

虽然Django提供传入条件的方式,但是不方便,它还提供了Q对象来解决。

Q对象是django.db.models.Q

可以使用&、|操作符来组成逻辑表达式。

~表示not。

from employee.models import Employee
from django.db.models import Q
mgr = Employee.objects
print(mgr.filter(Q(pk__lt=10006))) # 不如直接写filter(pk__lt=10006)


# 下面几句一样
print(mgr.filter(pk__gt=10003).filter(pk__lt=10006)) # 
print(mgr.filter(pk__gt=10003, pk__lt=10006)) # 
print(mgr.filter(Q(pk__gt=10003), Q(pk__lt=10006)))
print(mgr.filter(Q(pk__gt=10003) & Q(pk__lt=10006))) # 
print(mgr.filter(pk__gt=10003) & mgr.filter(pk__lt=10006))


# 下面几句等价
print(mgr.filter(pk__in=[10003, 10006])) # in
print(mgr.filter(Q(pk=10003) | Q(pk=10006))) # 
print(mgr.filter(pk=10003) | mgr.filter(pk=10006))
print(mgr.filter(~Q(pk__gt=10003))) # 

可使用 &,|和Q 对象来构造复杂的逻辑表达式,可以使用一个或多个Q对象。

如果混用关键字参数和Q对象,那么Q对象必须位于关键字参数的前面。

聚合、分组

aggregate()

aggregate() 返回字典,方便使用,只能返回一条记录

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee
from django.db.models import Q, Max, Min, Avg, Count, Sum

mgr = Employee.objects
print(mgr.filter(pk__gt=10010).count()) # 单值。将所有数据看做一行出结果
print(mgr.filter(pk__gt=10010).aggregate(Count('pk'), Max('pk'), Min('pk'), Sum('pk'), Avg('pk'))) # aggregate 聚合函数,出结果,字典。
print(mgr.aggregate(Max('pk'), min=Min('pk'), sum=Sum('pk'))) # 别名

#aggregate 聚合函数,返回字典。默认key命名为 '字段名__聚合函数名'

返回结果

(0.001) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None; alias=default
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
(0.001) SELECT COUNT(*) AS `__count` FROM `employees` WHERE `employees`.`emp_no` > 10010; args=(10010,); alias=default
10
(0.004) SELECT COUNT(`employees`.`emp_no`) AS `pk__count`, MAX(`employees`.`emp_no`) AS `pk__max`, MIN(`employees`.`emp_no`) AS `pk__min`, SUM(`employees`.`emp_no`) AS `pk__sum`, AVG(`employees`.`emp_no`) AS `pk__avg` FROM `employees` WHERE `employees`.`emp_no` > 10010; args=(10010,); alias=default
{'pk__count': 10, 'pk__max': 10020, 'pk__min': 10011, 'pk__sum': 100155, 'pk__avg': 10015.5}
(0.001) SELECT MIN(`employees`.`emp_no`) AS `min`, SUM(`employees`.`emp_no`) AS `sum`, MAX(`employees`.`emp_no`) AS `pk__max` FROM `employees`; args=(); alias=default
{'min': 10001, 'sum': 200210, 'pk__max': 10020}

annotate()方法用来分组聚合,返回查询集。

annotate()

annotate()中结合统计方法Avg, Sum, Max, Min, Count等等做聚合,返回聚合后的查询集

  • 只有annotate()时,就是以annotate()中的字段为聚合对象,以主键为分组基准,就是先以主键来分组,再对每个分组中annotate()中的字段进行聚合,并且投影所有字段

所得到的结果,以一个个的对象组成的列表,数据并没有显示出来,需要手动去取

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee
from django.db.models import Q, Max, Min, Avg, Count, Sum

mgr = Employee.objects
print(mgr.filter(pk__gt=10010).count()) # 单值。将所有数据看做一行出结果
print(mgr.filter(pk__gt=10010).aggregate(Count('pk'), Max('pk'), Min('pk'), sum=Sum('pk'), avg=Avg('pk'))) # aggregate 聚合函数,出结果,字典。
print(mgr.filter(pk__gt=10010).annotate(Count('pk')))
#aggregate 聚合函数,出统计函数的结果。返回字典。默认key命名为 '字段名__聚合函数名'
#annotate 返回查询集,元素是对象。用聚合函数,聚合函数会分组,没有指定分组使用pk分组,行行分组。

s = mgr.filter(pk__gt=10010).annotate(Count('pk'))
for x in s:
    print(x)
    print(x.__dict__) #里面多了一个属性pk_count

-- 等价SQL语句
SELECT
  `employees`.`emp_no`,
  `employees`.`birth_date`,
  `employees`.`first_name`,
  `employees`.`last_name`,
  `employees`.`gender`,
  `employees`.`hire_date`,
  COUNT(`employees`.`emp_no`) AS `pk__count`
FROM
  `employees`
WHERE
  `employees`.`emp_no` > 10010
GROUP BY
  `employees`.`emp_no`;
  • 有annotate()和values()的情况

1、如果values()在annotate()前面,则是以values()中的字段为分组基准,也就是先以values()中的字段来分组,在对各组中的annotate()中的字段进行聚合

​ 如果values()中没有字段,则跟上面那种情况是一样的,只是返回的不再是对象,而是字典组成的查询集,数据都已经显示出来了

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk'))
-- 等价SQL语句
SELECT
    `gender`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `gender`

2、如果values()在annotate()后面,则values()中的字段就表名了投影的字段,都是以主键来分组的,当values()中为空,则投影所有字段

返回的结果是字典组成的查询集,数据已经出来了

mgr.filter(pk__lt=10005).annotate(c=Count('gender'),a=Sum('pk')).values('gender','c','a','first_name')
-- 等价SQL语句
SELECT
    `gender`,
    `first_name`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `emp_no`

3、如果annotate()前后都有values(),

前面的values()为空,无论后面的values()是否为空,都是以主键来分组,

mgr.filter(pk__lt=10005).values().annotate(c=Count('gender'),a=Sum('pk')).values('c','a','first_name')
-- 等价SQL语句
SELECT
    `first_name`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `emp_no`;

后面的values()为空,无论前面的values()是否为空,都以主键来分组,并且投影所有字段

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values()
-- 等价SQL语句
SELECT
    `emp_no`,
    `birth_date`,
    `first_name`,
    `last_name`,
    `gender`,
    `hire_date`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `emp_no`;

后面的values()有主键存在,则无论前面的values()有什么,都是以主键来分组。

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values('c','a','first_name','emp_no')
-- 等价SQL语句
SELECT
    `first_name`,
    `emp_no`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `emp_no`;

后面的values()没有主键存在,也没有除了聚合后结果的字段,哪怕后面的也没有前面values()的字段,都有以 前面values()的字段来分组

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values('c','a')
#前面的value控制分组,后面的values控制投影
-- 等价SQL语句
SELECT
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `gender`;

后面的values()没有主键存在,但是有其他字段存在,则是以前面values()字段和后面values()中存在的其他字段联合起来分组,也就是联合起来的字段必须完全相同才能被分到同一组

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values('c','a','first_name','last_name')
-- 等价SQL语句
SELECT
    `first_name`,
    `last_name`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `gender`,
    `first_name`,
    `last_name`;

order_by()—》排序,一般在最后使用

升序就是原字段,例如order_by('c')

降序就是在原字段前面加个-,例如order_by('-c')

注意,如果order_by()中出现了主键,则annotate()前面的values()将无效,

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values('c','a','first_name').order_by('emp_no')
-- 等价SQL语句
SELECT
    `first_name`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `emp_no`
ORDER BY
    `emp_no` ASC;

如果order_by()中出现了annotate()后面的values()中没有的字段,则联合起来分组的字段中会添加上这个字段,投影也会添加这个字段

mgr.filter(pk__lt=10005).values('gender').annotate(c=Count('gender'),a=Sum('pk')).values('c','a','first_name').order_by('last_name')
-- 等价SQL语句
SELECT
    `first_name`,
    COUNT(`gender`) AS `c`,
    SUM(`emp_no`) AS `a`
FROM
    `employees`
WHERE
    `emp_no` < 10005
GROUP BY
    `gender`,
    `first_name`,
    `last_name`
ORDER BY
    `last_name` ASC;

一对多

构建一对多表

员工表和员工工资表

-- 员工表
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- 员工工资表
DROP TABLE IF EXISTS `salaries`;
CREATE TABLE `salaries` (
  `emp_no` int(11) NOT NULL,
  `salary` int(11) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`from_date`),
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

联合主键问题

SQLAlchemy提供了联合主键支持,Django至今没有支持。

  • 直接支持? 原生不支持。
  • 替代方案:优先使用 unique_together 或第三方库。
  • 推荐做法:使用代理主键(如 id)配合唯一约束,既符合 Django 习惯,又能保证数据完整性。

Djang不能直接添加自己的2个字段的联合主键,我们手动为表创建一个自增id主键。操作顺序如下:

  • 取消表所有联合主键,并删除所有外键约束后保存
  • 为表增加一个id字段,自增、主键。保存后,它会自动填充数据。
  • 重建原来的外键约束
-- 删除外键
ALTER TABLE salaries DROP FOREIGN KEY salaries_ibfk_1;
-- 删除原有的联合主键
ALTER TABLE salaries DROP PRIMARY KEY;
-- 添加新的自增主键列 id。 FIRS将 id 列放在表的第一列(默认添加到末尾)
ALTER TABLE salaries 
ADD COLUMN id INT NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;

-- 添加新外键(名称改为 salaries_ibfk_1)
ALTER TABLE salaries 
ADD CONSTRAINT `salaries_ibfk_1` 
FOREIGN KEY (`emp_no`) 
REFERENCES `test`.`employees` (`emp_no`) 
ON DELETE CASCADE 
ON UPDATE RESTRICT;
mysql> select * from salaries;
+----+--------+--------+------------+------------+
| id | emp_no | salary | from_date  | to_date    |
+----+--------+--------+------------+------------+
|  1 |  10001 |  60117 | 1986-06-26 | 1987-06-26 |
|  2 |  10001 |  62102 | 1987-06-26 | 1988-06-25 |
|  3 |  10001 |  66074 | 1988-06-25 | 1989-06-25 |
|  4 |  10001 |  66596 | 1989-06-25 | 1990-06-25 |
|  5 |  10001 |  66961 | 1990-06-25 | 1991-06-25 |
|  6 |  10001 |  71046 | 1991-06-25 | 1992-06-24 |
|  7 |  10001 |  74333 | 1992-06-24 | 1993-06-24 |
|  8 |  10001 |  75286 | 1993-06-24 | 1994-06-24 |
|  9 |  10001 |  75994 | 1994-06-24 | 1995-06-24 |

新表结构

-- 员工工资表
CREATE TABLE `salaries` (
  `id` int NOT NULL AUTO_INCREMENT,
  `emp_no` int NOT NULL,
  `salary` int NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `emp_no` (`emp_no` ASC) USING BTREE,
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) 
  REFERENCES `test`.`employees` (`emp_no`) 
  ON DELETE CASCADE 
  ON UPDATE RESTRICT
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb3;

新建模型

在employee/models.py文件下创建工资表的模型

from django.db import models
"""
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
"""
# Create your models here.
class Employee(models.Model): #metaclass=ModelBase
    class Meta:
        db_table = 'employees' #定义表名

    #将列名一一映射出来
    emp_no = models.IntegerField(primary_key=True)
    birth_date = models.DateField(null=False)
    first_name = models.CharField(null=False, max_length=14) #varchar(14) NOT NULL,
    last_name = models.CharField(null=False, max_length=16)
    gender = models.SmallIntegerField(null=False)
    hire_date = models.DateField(null=False)

    def __repr__(self):#重写可视化效果
        return '<Employee {}, {} {}>'.format(self.emp_no, self.first_name, self.last_name)

    __str__ = __repr__

"""
CREATE TABLE `salaries` (
  `id` int NOT NULL AUTO_INCREMENT,
  `emp_no` int NOT NULL,
  `salary` int NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `emp_no` (`emp_no` ASC) USING BTREE,
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) 
  REFERENCES `test`.`employees` (`emp_no`) 
  ON DELETE CASCADE 
  ON UPDATE RESTRICT
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb3;
"""
class Salary(models.Model):
    class Meta:
        db_table = 'salaries'

    id = models.AutoField(primary_key=True, null=False)
    emp_no = models.ForeignKey('Employee', on_delete=models.CASCADE,db_column='emp_no') #类名称,直接将emp_no新建成外键
    #主表的字段名emp_no,从表中的外键字段名默认为emp_no_id ,可以通过db_column='emp_no'修改外键字段名
    salary = models.IntegerField(null=False)
    from_date = models.DateField(null=False)
    to_date = models.DateField(null=False)

    def __repr__(self):#重写可视化效果
        return '<Salary {}, {} {}>'.format(
            self.id, 
            # self.emp_no, #返回<Employee 10001, Georgi Facello> 是Employee类的对象
            self.emp_no_id,  #这样不会引起第2张表的查询
            self.salary)
        #返回<Salary 1, <Employee 10001, Georgi Facello> 60117>
    __str__ = __repr__

emp_no = models.ForeignKey('Employee', on_delete=models.CASCADE,db_column='emp_no')

直接将emp_no设置成外键,查询时,如果不设置db_column,则以emp_no_id来搜索,在主表中是搜索不到的,所以需要写上真实的名字,所以写上db_column='emp_no'

测试代码

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects
smgr = Salary.objects
print(smgr.all())

返回结果

(0.002) 
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None; alias=default
(0.001) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
(0.001) SELECT `salaries`.`id`, `salaries`.`emp_no`, `salaries`.`salary`, `salaries`.`from_date`, `salaries`.`to_date` FROM `salaries` LIMIT 21; args=(); alias=default
<QuerySet [<Salary 1, 10001 60117>, <Salary 2, 10001 62102>, <Salary 3, 10001 66074>, <Salary 4, 10001 66596>, <Salary 5, 10001 66961>, <Salary 6, 10001 71046>, <Salary 7, 10001 74333>, <Salary 8, 10001 75286>, <Salary 9, 10001 75994>, <Salary 10, 10001 76884>, <Salary 11, 10001 80013>, <Salary 12, 10001 81025>, <Salary 13, 10001 81097>, <Salary 14, 10001 84917>, <Salary 15, 10001 85112>, <Salary 16, 10001 85097>, <Salary 17, 10001 88958>, <Salary 18, 10002 65828>, <Salary 19, 10002 65909>, <Salary 20, 10002 67534>, '...(remaining elements truncated)...']>
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects
smgr = Salary.objects
for x in smgr.all()[2:4]:
    print(x) #Salary对象
    print(x.emp_no) #会对employees表查询

返回结果

(0.002) SELECT `salaries`.`id`, `salaries`.`emp_no`, `salaries`.`salary`, `salaries`.`from_date`, `salaries`.`to_date` FROM `salaries` LIMIT 2 OFFSET 2; args=(); alias=default
<Salary 3, 10001 66074>
(0.002) SELECT `employees`.`emp_no`, `employees`.`birth_date`, `employees`.`first_name`, `employees`.`last_name`, `employees`.`gender`, `employees`.`hire_date` FROM `employees` WHERE `employees`.`emp_no` = 10001 LIMIT 21; args=(10001,); alias=default
<Employee 10001, Georgi Facello>
<Salary 4, 10001 66596>
(0.002) SELECT `employees`.`emp_no`, `employees`.`birth_date`, `employees`.`first_name`, `employees`.`last_name`, `employees`.`gender`, `employees`.`hire_date` FROM `employees` WHERE `employees`.`emp_no` = 10001 LIMIT 21; args=(10001,); alias=default
<Employee 10001, Georgi Facello>

外键

Employee是一个模型,也可以看作一个表,就是一对多的那个一,外键也会取这个模型的主键

emp_no = models.ForeignKey(Employee, on_delete=models.CASCADE,db_column='emp_no')
db_column

用来设置外键的被搜索的字段,因为被设置为外键,当联合搜索时,会被自动 默认为字段_id的字段。比如,emp_no被设置为外键,那么联合搜索时的主表 中的字段就是emp_no_id,如果不设置db_column,就会报错,无法搜索到字段, 设置db_column相当与设置成真实字段的名字

这个是设置本表的字段名

related_name

用来给主表搜索从表使用的,如果不设置,主表中就会多一个表名_set的属性, 用来方便主表对从表进行操作,比如,主表的对象为Employee,那么在主表中 就会产生一个employee_set的属性。如果手动设置一下,会方便自己查询。

on_delete

位于 django.db.models

  • CASCADE

    级联删除。Django 模拟 SQL 约束 ON DELETE CASCADE 的行为,并删除包 含 ForeignKey 的对象。Model.delete()不会在相关模型上调用,但 会为 所有已删除的对象发送pre_delete和 post_delete信号。

  • PROTECT

    通过提高ProtectedError的子类来 防止删除引用的对象 django.db.IntegrityError。

特殊属性

如果增加了外键后,Django会对一端和多端增加一些新类属性。

from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects
smgr = Salary.objects
print(*Employee.__dict__.items(), sep='\n')
print('-' * 30)
print(*Salary.__dict__.items(), sep='\n')
# for x in smgr.all()[2:4]:
#     print(x) #Salary对象
#     print(x.emp_no) 
#     print(x.__dict__)

返回结果

......
#'salary_set' 一端,Employee类中多了一个类属性
('salary_set', <django.db.models.fields.related_descriptors.ReverseManyToOneDescriptor object at 0x000001F099C85F30>)
------------------------------
.....
#多端,Salary类中也多了一个类属性
#emp_no_id这才是salary自己的类属性,对应数据库中的emp_no字段
('emp_no_id', <django.db.models.fields.related_descriptors.ForeignKeyDeferredAttribute object at 0x000001F0998CBB50>)
#emp_no 类名称,指向Emploee类的一个实例。关联员工实例 
('emp_no', <django.db.models.fields.related_descriptors.ForwardManyToOneDescriptor object at 0x000001F0998CBC50>)

从一端往多端查<Emmployee_instance>.salary_set

从多端往一端查<Salary_instance>.emp_no

查询

例子:查询员工10004的所有工资

准备,修改employee/models.py文件, 为Employee类加个name属性,方便打印

class Employee(models.Model): #metaclass=ModelBase
    class Meta:
        db_table = 'employees' #定义表名

    #将列名一一映射出来
    emp_no = models.IntegerField(primary_key=True)
    birth_date = models.DateField(null=False)
    first_name = models.CharField(null=False, max_length=14) #varchar(14) NOT NULL,
    last_name = models.CharField(null=False, max_length=16)
    gender = models.SmallIntegerField(null=False)
    hire_date = models.DateField(null=False)

    #添加属性
    @property
    def name(self): #fullname()
        return "% {}-{} %".format(self.first_name,self.last_name)

    def __repr__(self):#重写可视化效果
        return '<Employee {}, {} {}>'.format(self.emp_no, self.first_name, self.last_name)

    __str__ = __repr__

查询

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects

#查询员工10004的所有工资
#方案1 从一端往多端上查(从员工往工资表查) 好
emp = emgr.get(emp_no=10004) #一个Employee类的对象。只用主键定位了一条数据。好。
print(emp) #没有查询工资表
"""
SELECT
  `employees`.`emp_no`,
  `employees`.`birth_date`,
  `employees`.`first_name`,
  `employees`.`last_name`,
  `employees`.`gender`,
  `employees`.`hire_date`
FROM
  `employees`
WHERE
  `employees`.`emp_no` = 10004
"""
print(emp.salary_set.all()) #返回一个查询集,触发salary表的查询。
"""
SELECT
  `salaries`.`id`,
  `salaries`.`emp_no`,
  `salaries`.`salary`,
  `salaries`.`from_date`,
  `salaries`.`to_date`
FROM
  `salaries`
WHERE
  `salaries`.`emp_no` = 10004
"""

#方案2 从多端往一端上查(从工资表往员工表查) 不好 因为不知道员工的id
salaries = smgr.filter(emp_no=10004).all()
for x in salaries:

    print(x)
    """
    SELECT
    `salaries`.`id`,
    `salaries`.`emp_no`,
    `salaries`.`salary`,
    `salaries`.`from_date`,
    `salaries`.`to_date`
    FROM
    `salaries`
    WHERE
    `salaries`.`emp_no` = 10004;
    """
    e = x.emp_no #会引发填充Employee类的对象
    print(e.name) #触发了员工表的查询。 不好。多次同一SQL查询。
    """
    SELECT
    `employees`.`emp_no`,
    `employees`.`birth_date`,
    `employees`.`first_name`,
    `employees`.`last_name`,
    `employees`.`gender`,
    `employees`.`hire_date`
    FROM
    `employees`
    WHERE
    `employees`.`emp_no` = 10004;
    """

如果觉得salary_set不好用,可以使用related_name修改该属性名

class Salary(models.Model):
    ... ...
    emp_no = models.ForeignKey('Employee', on_delete=models.CASCADE, db_column='emp_no', related_name='salaries') #类名称,直接将emp_no新建成外键
emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects
print(*Employee.__dict__.items(), sep='\n')

#返回结果。salary_set属性名替换为salaries
#('salaries', <django.db.models.fields.related_descriptors.ReverseManyToOneDescriptor object at 0x000001EABFF55F30>)
#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects

#查询员工10004的所有工资
#方案1 从一端往多端上查(从员工往工资表查) 好
emp = emgr.get(emp_no=10004) #一个Employee类的对象。只用主键定位了一条数据。好。
print(emp) #没有查询工资表
print(emp.salaries.all()) #返回一个查询集,触发salary表的查询。
print(emp.salaries.values('emp_no', 'from_date', 'salary')) #投影
print(emp.salaries.filter(salary__gt=55000).all()) #工资大于55000

distinct–—》去重

目标,查询工资salary>60000的所有员工信息

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects

#查询所有发了工资的员工
print(smgr.values('emp_no').distinct())
#SELECT DISTINCT `salaries`.`emp_no` FROM `salaries` LIMIT 21;

#工资大于55000的所有员工的姓名
emps = smgr.filter(salary__gt=55000).values('emp_no').distinct() #查询集 queryset
print(type(emps))
print(emps.filter(emp_no__in=[d.get('emp_no') for d in emps])) #in列表
#SELECT DISTINCT `salaries`.`emp_no` FROM `salaries` WHERE `salaries`.`salary` > 55000; 
# | emp_no |
# +--------+
# |  10001 |
# |  10002 |
# |  10004 |
#SELECT DISTINCT `salaries`.`emp_no` FROM `salaries` 
# WHERE (`salaries`.`salary` > 55000 AND `salaries`.`emp_no` IN (10001, 10002, 10004));

print(emps.filter(emp_no__in=emps)) #in子查询
#SELECT DISTINCT `salaries`.`emp_no` FROM `salaries` 
# WHERE (`salaries`.`salary` > 55000 AND `salaries`.`emp_no` IN 
# (SELECT DISTINCT U0.`emp_no` FROM `salaries` U0 WHERE U0.`salary` > 55000)) LIMIT 21;

raw的使用—>执行原生SQL语句

-- 将salaries表和employees表合并,然后查询salary大于55000的员工名
MariaDB [test]> select distinct e.first_name,e.last_name from salaries as s  join employees as e on s.emp_no=e.emp_no where s.salary>55000;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| Georgi     | Facello   |
| Bezalel    | Simmel    |
| Chirstian  | Koblick   |
+------------+-----------+

mysql> select DISTINCT e.* from employees e JOIN salaries s on e.emp_no = s.emp_no WHERE salary > 55000;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  |
+--------+------------+------------+-----------+--------+------------+
|  10001 | 1953-09-02 | Georgi     | Facello   |      1 | 1986-06-26 |
|  10002 | 1964-06-02 | Bezalel    | Simmel    |      2 | 1985-11-21 |
|  10004 | 1954-05-01 | Chirstian  | Koblick   |      1 | 1986-12-01 |
+--------+------------+------------+-----------+--------+------------+

使用raw,在django中执行

import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects

#工资大于55000的所有员工的姓名
sql = """\
select
  DISTINCT e.*
from
  employees e
  JOIN salaries s ON e.emp_no = s.emp_no
WHERE
  salary > 55000;
"""

emps = emgr.raw(sql)
print(type(emps)) #RawQuerySet
print(list(emps))
# [<Employee 10001, Georgi Facello>, <Employee 10002, Bezalel Simmel>, <Employee 10004, Chirstian Koblick>]

for e in emps:
    print(e.emp_no, e.first_name)
# 10001 Georgi
# 10002 Bezalel
# 10004 Chirstian
#工资大于70000的所有员工的姓名
sql = """\
select
  DISTINCT 
  e.emp_no,
  e.first_name,
  e.last_name,
  s.salary
from
  employees e
  JOIN salaries s ON e.emp_no = s.emp_no
WHERE
  salary > 70000;
"""

emps = emgr.raw(sql)
for e in emps:
    # print(e.__dict__)
    # print(e.salaries.all())
    # print(e.gender) #e.gender 在sql中没有投影,会触发查库,效率低
    print(e.emp_no, e.name, e.salary)
# 10001 % Georgi-Facello % 71046
# 10001 % Georgi-Facello % 74333
# ... ...
#这里查询了前面投影的字段,也有没有投影的字段.

多对多

多对多表

三个表

-- 员工表
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- 部门表
CREATE TABLE `departments` (
  `dept_no` char(4) NOT NULL,
  `dept_name` varchar(40) NOT NULL,
  PRIMARY KEY (`dept_no`),
  UNIQUE KEY `dept_name` (`dept_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

-- 第三张表
CREATE TABLE `dept_emp` (
  `emp_no` int(11) NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`dept_no`),
  KEY `dept_no` (`dept_no`),
  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,
  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

联合主键问题依然存在。需要修改dept_emp表,增加id自增主键

-- 删除外键
ALTER TABLE dept_emp DROP FOREIGN KEY dept_emp_ibfk_1;
ALTER TABLE dept_emp DROP FOREIGN KEY dept_emp_ibfk_2;
-- 删除原有的联合主键
ALTER TABLE dept_emp DROP PRIMARY KEY;
-- 添加新的自增主键列 id。 FIRS将 id 列放在表的第一列(默认添加到末尾)
ALTER TABLE dept_emp ADD COLUMN id INT NOT NULL AUTO_INCREment PRIMARY KEY FIRST;

-- 添加新外键(名称改为 salaries_ibfk_1)
ALTER TABLE dept_emp ADD CONSTRAINT `dept_emp_ibfk_1` 
FOREIGN KEY (`emp_no`) 
REFERENCES `employees` (`emp_no`) 
ON DELETE CASCADE 
ON UPDATE RESTRICT;

ALTER TABLE dept_emp ADD CONSTRAINT `dept_emp_ibfk_2` 
FOREIGN KEY (`dept_no`) 
REFERENCES `departments` (`dept_no`) 
ON DELETE CASCADE 
ON UPDATE RESTRICT;

-- 筛选出外键约束
SELECT 
    TABLE_NAME, -- 子表名
    COLUMN_NAME, -- 子表外键列名
    CONSTRAINT_NAME, -- 外键约束名
    REFERENCED_TABLE_NAME, -- 父表名
    REFERENCED_COLUMN_NAME -- 父表关联列名
FROM 
    INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE 
    TABLE_SCHEMA = 'test' -- 指定数据库名(模式名)
    AND REFERENCED_TABLE_NAME IS NOT NULL; -- 筛选出外键约束
mysql> SELECT 
    ->     TABLE_NAME, -- 
    ->     COLUMN_NAME, -- 
    ->     CONSTRAINT_NAME, -- 
    ->     REFERENCED_TABLE_NAME, -- 
    ->     REFERENCED_COLUMN_NAME -- 
    -> FROM 
    ->     INFORMATION_SCHEMA.KEY_COLUMN_USAGE
    -> WHERE 
    ->     TABLE_SCHEMA = 'test' -- 
    ->     AND REFERENCED_TABLE_NAME IS NOT NULL; -- 
+------------+-------------+-----------------+-----------------------+------------------------+
| TABLE_NAME | COLUMN_NAME | CONSTRAINT_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+------------+-------------+-----------------+-----------------------+------------------------+
| dept_emp   | emp_no      | dept_emp_ibfk_1 | employees             | emp_no                 |
| dept_emp   | dept_no     | dept_emp_ibfk_2 | departments           | dept_no                |
| salaries   | emp_no      | salaries_ibfk_1 | employees             | emp_no                 |
| titles     | emp_no      | titles_ibfk_1   | employees             | emp_no                 |
+------------+-------------+-----------------+-----------------------+------------------------+

新表结构

CREATE TABLE `dept_emp` (
  `id` int NOT NULL AUTO_INCREMENT,
  `emp_no` int NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`id`),
  KEY `dept_no` (`dept_no`),
  KEY `dept_emp_ibfk_1` (`emp_no`),
  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE ON UPDATE RESTRICT,
  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

创建模型

在employee/models.py文件下

from django.db import models
"""
CREATE TABLE `employees` (
  `emp_no` int(11) NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) NOT NULL,
  `last_name` varchar(16) NOT NULL,
  `gender` smallint(6) NOT NULL DEFAULT 1 COMMENT 'M=1, F=2',
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
"""
# Create your models here.
class Employee(models.Model): #metaclass=ModelBase
    class Meta:
        db_table = 'employees' #定义表名

    #将列名一一映射出来
    emp_no = models.IntegerField(primary_key=True)
    birth_date = models.DateField(null=False)
    first_name = models.CharField(null=False, max_length=14) #varchar(14) NOT NULL,
    last_name = models.CharField(null=False, max_length=16)
    gender = models.SmallIntegerField(null=False)
    hire_date = models.DateField(null=False)

    #添加属性
    @property
    def name(self): #fullname()
        return "% {}-{} %".format(self.first_name,self.last_name)

    def __repr__(self):#重写可视化效果
        return '<Employee {}, {} {}>'.format(self.emp_no, self.first_name, self.last_name)

    __str__ = __repr__

"""
CREATE TABLE `salaries` (
  `id` int NOT NULL AUTO_INCREMENT,
  `emp_no` int NOT NULL,
  `salary` int NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `emp_no` (`emp_no` ASC) USING BTREE,
  CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) 
  REFERENCES `test`.`employees` (`emp_no`) 
  ON DELETE CASCADE 
  ON UPDATE RESTRICT
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb3;
"""
class Salary(models.Model):
    class Meta:
        db_table = 'salaries'

    id = models.AutoField(primary_key=True, null=False)
    emp_no = models.ForeignKey('Employee', on_delete=models.CASCADE, db_column='emp_no', related_name='salaries') #类名称,直接将emp_no新建成外键
    #主表的字段名emp_no,从表中的外键字段名默认为emp_no_id ,可以通过db_column='emp_no'修改外键字段名
    salary = models.IntegerField(null=False)
    from_date = models.DateField(null=False)
    to_date = models.DateField(null=False)

    def __repr__(self):#重写可视化效果
        return '<Salary {}, {} {}>'.format(
            self.id, 
            # self.emp_no, #返回<Employee 10001, Georgi Facello> 是Employee类的对象
            self.emp_no_id,  #这样不会引起第2张表的查询
            self.salary)
        #返回<Salary 1, <Employee 10001, Georgi Facello> 60117>
    __str__ = __repr__

"""
CREATE TABLE `departments` (
  `dept_no` char(4) NOT NULL,
  `dept_name` varchar(40) NOT NULL,
  PRIMARY KEY (`dept_no`),
  UNIQUE KEY `dept_name` (`dept_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
"""
class Department(models.Model):
    class Meta:
        db_table = 'departments'
    dept_no = models.CharField(primary_key=True, null=False, max_length=4)
    dept_name = models.CharField(unique=True, null=False, max_length=40)

    def __repr__(self):
        return '<Dept: {}, {} >'.format(self.dept_no, self.dept_name)

    __str__ = __repr__

"""
CREATE TABLE `dept_emp` (
  `id` int NOT NULL AUTO_INCREMENT,
  `emp_no` int NOT NULL,
  `dept_no` char(4) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`id`),
  KEY `dept_no` (`dept_no`),
  KEY `dept_emp_ibfk_1` (`emp_no`),
  CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE ON UPDATE RESTRICT,
  CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY (`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
"""
class Dept_emp(models.Model):
    class Meta:
        db_table = 'dept_emp'
    id = models.IntegerField(primary_key=True)
    emp_no = models.ForeignKey('Employee', on_delete=models.CASCADE, db_column='emp_no', related_name='dept_emp') #类名称
    dept_no = models.ForeignKey('Department', on_delete=models.CASCADE, db_column='dept_no', related_name='dept_emp') #类名称
    from_date = models.DateField(null=False)
    to_date = models.DateField(null=False)

    def __repr__(self):
        return '<D-E: {}: {}, {}>'.format(self.id, self.emp_no_id, self.dept_no_id)
               #这里改成emp_no_id 是为了防止二次查询造成资源浪费
    __str__ = __repr__

使用查询

查询emp_no=10010的部门编号以及部门名称和员工信息

mysql> select * from employees e join dept_emp de on e.emp_no = de.emp_no AND e.emp_no=10010;
+--------+------------+------------+-----------+--------+------------+----+--------+---------+------------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date  | id | emp_no | dept_no | from_date  | to_date    |
+--------+------------+------------+-----------+--------+------------+----+--------+---------+------------+------------+
|  10010 | 1963-06-01 | Duangkaew  | Piveteau  |      2 | 1989-08-24 | 10 |  10010 | d004    | 1996-11-24 | 2000-06-26 |
|  10010 | 1963-06-01 | Duangkaew  | Piveteau  |      2 | 1989-08-24 | 11 |  10010 | d006    | 2000-06-26 | 9999-01-01 |
+--------+------------+------------+-----------+--------+------------+----+--------+---------+------------+------------+
import os
import django

#参考wsgi.py 加载初始环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'salary.settings')
django.setup()

#访问数据
from employee.models import Employee, Salary
from django.db.models import Q, Max, Min, Avg, Count, Sum

emgr = Employee.objects #salary_set 在Employee类中定义的
smgr = Salary.objects

print(*Employee.__dict__.items(), sep='\n')

#10010 员工的信息和部门编号
e = emgr.filter(pk=10010).first()
if e:
    print(e) #<Employee 10010, Duangkaew Piveteau>
    print(e.dept_emp.all()) #<QuerySet [<D-E: 10: 10010, d004>, <D-E: 11: 10010, d006>]>
    print([d.dept_no_id for d in e.dept_emp.all()]) #['d004', 'd006']

迁移

如果建立好模型类,想从这些类来生成数据库的表,使用下面语句。

python manage.py makemigrations #生成迁移文件
#会在app名下的migrations文件夹下生成一个迁移文件。 如0001_initial.py文件。
#可以修改DATABASES中的库名来实现指定库名。

python manage.py migrate employee #指定app进行迁移

第一次迁移,最好全部迁移. python manage.py migrate 是Django中所有为迁移的模型都生成表。

显示事务处理

因为需要数据的完整性,所以需要显式控制事务

from django.db.transaction import atomic

两种方法

  • 作为装饰器,为整个视图函数
from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()
  • 作为上下文,对部分代码执行显示事务
from django.db import transaction

def viewfunc(request):
    # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic():
        # This code executes inside a transaction.
        do_more_stuff()

测试代码

import os
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
django.setup()

from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic
post=Post()
content=Content()
try:
    post.title='sadasd'
    post.author_id=User.objects.get(id=2)
    with atomic():
        post.save()
        content.id=post
        raise BufferError
        content.content='qwer'
        content.save()
except Exception as e:
    print(e)

当第一个表数据插入完成,抛出异常,事务会回滚

对数据库的增删改查

增加

不指定主键,就默认为插入数据

例如,插入一条,作者为admin,标题为abcd 的数据

from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic

post = Post()
content = Content()
try:
    post.title = 'abcd'
    post.author_id = User.objects.get(username='admin')
    with atomic():
        post.save()
        content.id = post
        content.content = 'qwer'
        content.save()
except Exception as e:
    print(e)

修改

先查出title='aaaaa'的post表中的数据,再把数据组成的对象与content表对应,然后修改content字段

先查再修改

多表修改
from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic

content = Content()
try:
    post = Post.objects.filter(title='aaaaa').first()
    with atomic():
        content.id = post
        content.content = '1111111'
        content.save()
except Exception as e:
    print(e)
单表本表修改
from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic
import datetime
try:
    post = Post.objects.filter(title='aaaaa').first()
    with atomic():
        post.postdate=datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))
        post.save()
except Exception as e:
    print(e)

删除

当有外键,且从表有数据

  • 借助模板中从表中定义的related_name='content',用来删除从表中的数据,再删除主表的数据

    通过related_name='content'返回的数据是一个关联管理器对象,需要转换成数据对象,再进行删除

from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic
import datetime
content=Content()
try:
    post = Post.objects.filter(title='aaaaa').first()
    with atomic():
        post.content.first().delete()
        post.delete()
except Exception as e:
    print(e)

手动找到从表中的数据,然后删除

  • 删除单个
from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic
import datetime
content=Content()
try:
    post = Post.objects.filter(title='aaaaa').first()
    with atomic():
        content.id=post
        content.delete()
        post.delete()
except Exception as e:
    print(e)
  • 删除多个
from post.models import *
from django.contrib.auth.models import User
from django.db.transaction import atomic
import datetime
content=Content()
try:
    post = Post.objects.filter(title='sadasd').all()
    with atomic():
        for i in post:
            content.id=i
            content.delete()
        post.delete()
except Exception as e:
    print(e)

以上都是查从表,然后删主表

现在的需求时知道从表内容,要连主表一起删

由从表删主表

import datetime, os, django

print(datetime.date.fromtimestamp(datetime.datetime.now().timestamp()))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')
django.setup(set_prefix=False)

from post.models import Post, Content
from django.db.transaction import atomic

cont = Content()

with atomic():
    content = Content.objects.filter(content="内容")
    post = Post()
    for i in content:
        post = Post.objects.filter(id=i.id_id).first()
        i.delete()
        post.delete()

Django后台管理

创建管理员

> python manage.py createsuperuser

Username : admin
Email address: [email protected]
Password:
Password (again):
Superuser created successfully.

创建管理员,实际上是在auth_user表中增加一个特殊用户

mysql> select username,password from auth_user;
| admin    | pbkdf2_sha256$1000000$Jxxxxx$xxxxxxxxxxxxxxxxxx= |

本地化

settings.py中设置语言、时区

语言名称可以查看 django\contrib\admin\locale 目录

LANGUAGE_CODE = 'zh-Hans' #'en-us' 中文简体
USE_TZ = True
TIME_ZONE = 'Asia/Shanghai' #'UTC'

启动WEB Server

$ python manage.py runserver

Starting development server at http://127.0.0.1:8000/

默认启动8000端口

登录后台管理

因为在urls.py文件中默认配置了一条,当访问根目录下的admin自动跳准的路由,所以后台登录地址http://127.0.0.1:8000/admin

登录后如果希望后来能够管理这些表,就需要在admin文件中注册对应的models

举例

#models.py
from django.db import models

# Create your models here.
class Us(models.Model):
    class meta:
        db_table="a"
    pass

在admin.py文件中注册

from .models import Us
admin.site.register(Us)

在后台web端查看

总结

在开发中,一般会采用ORM框架,这样可以使用对象来操作表了。

Django中,定义表映射的类,继承自Model类。Model类使用了元编程,改变了元类。

使用Field实例作为类属性来描述字段。

使用ForeignKey来定义外键约束。

是否使用外键约束?

  • 力挺派,能使数据保持完整性一致性。
  • 弃用派,开发难度增加,大量数据的时候影响插入、修改、删除的效率。在业务层保证数据的一致性。
emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike