2020年3月MySQL和JDBC

(一) 数据库概述
数据库(DataBase,DB): 数据库是指可永久存储在计算机内、有组织、可共享的大量数据的集合

**关键词:可永久存储、有组织、可共享
数据库管理系统(DBMS):安装在操作系统之上,是一个管理、控制数据库中各种数据库对象的系统软件。
DBMS常用的数学模型有: 层次模型、网状模型、关系模型、面向对象模型,其中关系数据库管理系统已占据主导地位。
数据库系统(DBS): 是指计算机引入数据库后的系统,它能够有组织、动态地存储大量的数据、提供数据处理和数据共享机制。
组成: 一般由硬件系统、软件系统、数据库和人员组成。
DBMS主要功能:

提供了数据定义语言(Data Definition Language ,DDL),用户可以通过这种语言来定义数据库中的表结构。
提供了数据操作语言(Data manipulation Language , DML),用户可以对数据库 进行基本的操作,如增删改查。

(二)
通俗的说,数据库系统包含了DBMS、数据库、软件平台与硬件支持环境以及各类人员。
而DBMS与数据库的关系:DBMS是管理数据库的

(三) 数据库管理系统的发展
三个阶段:

  1. 人工管理阶段
  2. 文件系统管理
  3. 数据库系统管理

(四) 数据库系统结构
数据库通常的体系结构都具有相同的特征,即采用三级模式结构、并提供两级映射。

三级模式:外模式、模式、内模式。
外模式:是数据库用户所见和使用的局部数据的逻辑结构和特征的描述,是用户所用的数据库结构。一个数据库可以有多个外模式
模式:是数据库中全体数据的逻辑结构的特征的描述。一个数据库只有一个模式
内模式:是数据物理结构和存储方法的描述。一个数据库只有一个内模式

数据库系统提醒结构:
1.客户/服务器的结构(C/S)结构,特点就是需要安装,如电脑中的qq、微信就是使用这种结构。
2.浏览器/服务器结构(B/S)结构,只要电脑联网,安装了浏览器就能访问各种web网站。

C为客户端,指的是Client、S为服务器,Server,B为浏览器,Browser

第一章 MySQL的存储引擎和数据库操作管理
(一) 数据库存储引擎
概念:数据库存储引起是数据库底层软件组件,DBMS使用数据引擎进行创建、查询、更新删除数据操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平。

即:存储引擎实际上就是数据库如何存储数据、如何为存储的数据建立索引和如何更新、查询数据的机制。

MySQL数据库常用的存储引擎有InnoDB和MyISAM

(二) InnoDB存储引擎
在MySQL5.5.5之后,默认选择InnoDB作为存储引擎:
InnoDB提供了事务能力,即完整的提交、回滚、崩溃回复能力的事物安全。
InnoDB为处理巨大数据量提供最大性能而设计的。(性能不及MyISAM)
InnoDB支持外键完整性约束(Foreign key)

(三) MyISAM存储引擎
不支持事务能力,所以安全性不足。
与InnoDB相比,速度非常快,适用于小型网站使用。

(四) MEMORY存储引擎
这种存储引擎是将表中的数据存储在内存当中的,存储的速度非常快,但是当断电或宕机时表中的数据就会消失。它适用于存储临时数据的临时表。
默认使用Hash(哈希)索引
速度比B+树索引更快。

(五) MERGE存储引擎
MERGE是一组MyISAM表的集合,这些MyISAM表结构必须完全相同,MERGE本身没有任何数据,对MERGE的增删改查操作实际上就是对内部的MyISAM进行操作的。

(六) 不同存储引擎的选择
InnoDB:适用于需要事务支持、行级锁定,对高并发有很好的适用能力,但是需要确保查询是通过索引完成的,数据更新较为频繁。抓住关键词:事务支持
MyISAM:适用于不需要事务支持、并发相对低、数据修改少、以读为主,对数据要求一致不是非常高。
MEMORY:适用于追求很快的读写 能力,对数据安全性要求很低的场景。

第二章 MySQL的操作
本章开始主要是相关的语法操作,方便自己的复习。其中,SQL语句是非常重要的部分,整个章节都是围绕SQL语句比编写。

此处的知识是结合网上资料、课本上的知识,外加自己的理解而整理的,主要内容是SQL的概念、有条理的复习如何操作数据库、个人总结等内容,希望这样能够帮助大家复习吧。

(一) 结构化语句
Structured Query Language 结构化查询语言,简称SQL。我们平时使用的各种语句对数据库的操作,其实就是SQL语句,因为MYSQL是支持SQL语句的。

作用:

是一种所有关系型数据库的查询规范,不同的数据库都支持。
通用的数据库操作语言,可以用在不同的数据库中。
不同的数据库 SQL 语句有一些区别
SQL 语句分类

Data Definition Language (DDL 数据定义语言) 如:建库,建表
Data Manipulation Language(DML 数据操纵语言),如:对表中的记录操作增删改
Data Query Language(DQL 数据查询语言),如:对表中的查询操作
Data Control Language(DCL 数据控制语言),如:对用户权限的设置
MySQL 的语法

每条语句以分号结尾,如果在 SQLyog 或者Navicat当中不是必须加的。
SQL 中不区分大小写,关键字中认为大写和小写是一样的
3 种注释:

我是MYSQL中独特的注释方法

/*我是多行注释 */
– 空格

(二) DDL语句操作数据库
主要围绕增删改查,相关的数据库的名称为palewl

创建数据库create database 数据库名称"=; – 注意是database 不是databases
– 例
CREATE DATABASE palewl;

创建数据库之前判断数据库是否存在,不存在就执行创建create database if not exists 数据库名称 ;
– 例
CREATE DATABASE IF NOT EXISTS palewl;

创建数据库,并指定字符集,如为utfcreate database 数据库名称 character set utf8
– 例
CREATE DATABASE palewl CHARACTER SET utf8;;

查询数据库SHOW DATABASES; – 注意是DATABASES,后面有s
技巧补充: 在输入相关的关键字时,可以按Tab键自动补充

修改数据库-- 修改palewl中字符集为gbk
ALTER DATABASE palewl DEFAULT CHARACTER SET gbk;

删除数据库DROP DATABAS 数据库名称;
– 例
DROP DATABASE palewl;

使用或者切换某个数据库use 数据库名称;
– 例
use palewl;

查看正在使用的数据库select database();

1

总结:

其实可以发现规律很统一,创建就用CREATE,修改用ALTER,删除就用DROP
在这里插入图片描述
byte/short/int/long tinyint/samllint/int/bigint
float float
double double
boolean bit
char/String bchar/varchar(固定长度和可变)
Date date/time/datetime/timestamp(默认null/默认系统时间)
(三) DDL语句操作数据表
创建表create table 表名(
字段名1 字段类型1,
字段名2 字段类型2,
) – 注意是(),不是大括号{}

具体操作:在创建了一个叫palewl的数据库,创建一个表user,有id,NAME,birthday这些字段
CREATE TABLE USER( id INT, NAME VARCHAR(20),birthday DATE);

查看数据库里面的表
show tables

查看表结构
DESC 表名

查看创建某个表所用的DDL语句
show create table 表名;

创建一个表结构相似的表
create table 新表名 like 旧表名;
– 例
CREATE TABLE l_user LIKE USER;

修改表名称
RENAME TABLE 表名 TO 新表名;
– 例 将user表名改为tb_user
RENAME TABLE user TO tb_user;

删除表
drop table 表名;
– 例
DROP TABLE USER;

创建表之前判断表是否存在,存在就删除
DROP TABLE IF EXISTS “表名”;
– 例
DROP TABLE IF EXISTS USER;

新增表中字段添加列
ALTER TABLE 表名 ADD 字段名 字段类型;
– 如在user表中新增一个字段remark,类型为VARCHAR(50);
ALTER TABLE USER ADD remark VARCHAR(50);
添加列alter table 表名 add 列名 类型(长度) 约束

修改表中某个字段类型
ALTER TABLE 表名 MODIFY 列名 新的类型;
– 如在user表中将字段remark类型改为VARCHAR(100);
ALTER TABLE USER MODIFY remark VARCHAR(100);

修改表中字段名称
ALTER TABLE 表名 CHANGE 旧列名 新列名 类型;
– 如在user表中将字段remark字段改为nickName;
alter table user change remark nickName varchar(50);

删除表中的某列
ALTER TABLE 表名 DROP 列名
– 如在user表中将字段nickName删除
ALTER TABLE USER DROP nickName;

修改表的字符集alter table 表明 character 字符集

字段约束
MySQL支持7种外键约束:主键约束(PRIMARY KEY)、外键约束(FOREIGN KEY),非空约束(NOT NULL)、唯一性约束(UNIQUE)、默认值约束(DEFAULT)、自增约束(AUTO_INCREMENT)、检查约束

–创建一个t_user表
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(25),
score INT NOT NULL,
TYPE VARCHAR(25) UNIQUE,
flag VARCHAR(20) DEFAULT 60
)

PRIMARY KEY 指的是主键约束默认唯一和非空
AUTO_INCREMENT 主键id自增
NOT NULL 非空约束
UNIQUE 唯一性约束
DEFAULT 60 默认值约束,令flag的默认值为60
给已经创建好的表增加主键约束

ALTER TABLE USER ADD PRIMARY KEY(id);
– USER 为表名 id为需要设为主键的字段
修改user表的主键,删除原来的主键,增加id为主键

ALTER TABLE USER DROP PRIMARY KEY, ADD PRIMARY KEY(id);
在user表中给name增加非空验证、唯一性验证

ALTER TABLE USER MODIFY NAME VARCHAR(20) NOT NULL UNIQUE;
–注意此处的关键字为

(四) DML 操作表中的数据
操作表的数据,其实就是对某个表中的数据进行增删改,注意没有查

插入一条记录
–所有的字段名都写出来
insert into 表名(字段1,字段2,字段3) values(字段1,字段2,字段3)
–不写字段名
insert into 表名 values(字段1,字段2,字段3)
–例 向user表插入一条记录
INSERT INTO USER(id,NAME,birthday) VALUES(“1”,“小红”,“2000-12-20”);
INSERT INTO USER VALUES(“1”,“小红”,“2000-12-20”);
添加中文报错解决方法,找到MySQL数据库服务器中的客户端部分的字符集修改为gbk,找到安装路径的my.ini文件修改中client下的字符集:
【client】
port=3306
【mysql】
default-character-set=gbk
然后重新启动MySQL服务器services.msc

更新一条记录
update 表名 set 字段1= ?,字段2= ?, where id = ?
–此处?表示占位符,无实际意义。
–例 在用户表中,将id为1的用户名修改为小蓝
UPDATE USER SET NAME=“小蓝” WHERE id=1;

删除一条记录
delete from 表名 where id = ?
–此处?表示占位符,无实际意义。
–例 在用户表中,将id为1的记录删除
DELETE FROM USER WHERE id=1;
删除所有记录
delete from user DML语句一条条删除 可以回滚
truncate table user DDL语句 删除表再重新建一个结构一样的表 不能回滚

以上的例子是最为常用的,实际上使用还有别的方式,因为使用的相对少,所以没有举例出来。

为什么需要where ?
where在此处的意思是条件的意思,比如:
//我要更新或删除某一条记录,我就得知道这条记录的唯一标识(一般为id),这样才能准确定位。

(五) DQL 查询表中的数据
在众多语句当中,其实查询是相对复杂的,也是能玩出花样特别多的,所以这里尽量总结的完善,使用相关的例子来举例。

查询不会对原有的数据造成影响,只是一种数据的展现形式。

基本格式:
SELECT * FROM 表名 where [条件]

SELECT表示查询语句
*标识为通配符,SELECT 语句会返回表的所有字段数据
where 表示查询的条件
为了讲解,导入user表,有三个字段。

CREATE TABLE user (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(20) DEFAULT NULL,
birthday date DEFAULT NULL,
score int(10) DEFAULT NULL,
type varchar(225) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

查询所有
SELECT *FROM USER;

根据ID查询,如查询id为1的信息
SELECT *FROM USER WHERE id = ‘1’;

查询单个字段,如查询user表中的score成绩。
SELECT score FROM USER;

起别名,在查询单个字段的同时起个别名.如查询score,并给它取别名为 “成绩”
SELECT score AS “成绩” FROM USER;

使用别名的好处: 显示的时候使用新的名字,并不修改表的结构。
– 格式: SELECT 字段名 1 AS 别名, 字段名 2 AS 别名… FROM 表名;
– 例 给表中的字段加上别名
SELECT NAME AS “姓名”,birthday AS “出生日”,score AS “成绩”,TYPE AS “类型” FROM USER;

查询结果参与运算 给查询的成绩+5分
SELECT score+5 AS ‘成绩’ FROM USER;

避免重复数据查询user表的score字段–使用DISTINCT关键字
SELECT DISTINCT score FROM USER;

(六) DQL 条件查询
条件查询的意思是对查询的内容进行筛选,从而获得自己需要的信息.
格式:
SELECT 字段名 FROM 表名 WHERE 条件;

MYSQL中常用的运算符:
>、<、<=、>=、=、<> <>在 SQL 中表示不等于,在 mysql 中也可以使用!= , 没有==
BETWEEN…AND 在一个范围之内,如:between 100 and 200相当于条件在 100 到 200 之间,包头又包尾
IN(集合) 集合表示多个值,使用逗号分隔
LIKE 模糊查询
IS NULL 查询某一列为 NULL 的值,注:不能写=NULL
具体实例:
SELECT *FROM USER WHERE score>80; – 查询成绩大于80的记录
SELECT *FROM USER WHERE score<=80 AND score>=60; – 查询成绩大于等于60小于等于80的数据
SELECT *FROM USER WHERE score BETWEEN 60 AND 80; --查询成绩在于60到80的记录(包括60/80)

使用IN关键字查询
IN关键字可以判断某个字段的值是否在某个指定的集合当中,如果字段的值在集合中,就满足查询条件

– 查询成绩为50/60/80的数据。注意,不是50-80这个范围,而是成绩等于50/60/80的数据
SELECT *FROM USER WHERE score IN(50,60,80);

使用LIKE关键字查询
“%” 可以代表任意长度的字符串,长度可以是0.
“_” 只能表示单个字符
– 使用LIKE关键字查询姓名当中含有“张”的数据(模糊查询)
SELECT *FROM USER WHERE NAME LIKE “%张%”;
– 使用LIKE关键字查询姓名中以冯开头二字姓名。如 冯森,冯后仅接一个字
SELECT *FROM USER WHERE NAME LIKE “冯_”;

对查询结果排序:使用关键字ORDER BY
DESC为倒序,ASC为升序
– 对查询的结果按成绩进行倒序
SELECT *FROM USER ORDER BY score DESC;

限制查询结果数量:使用关键字LIMIT
可做分页
– 查询从第2名用户开始的3名学生信息
SELECT *FROM USER LIMIT 2,3;

分组查询:使用关键字 GROUP BY
– 查询user表,对学生内的type进行分组
SELECT * FROM USER GROUP BY TYPE;

聚合函数
where后不能跟聚合函数
SELECT TYPE,sum(price) FROM USER GROUP BY TYPE having sum(price) > 5000;
select…from…where…group by…having…order by;
SFWGHO

–使用count函数统计user表中的记录
SELECT COUNT() AS “总记录” FROM USER;
– 使用COUNT函数统计user表中不同的name值,并与GROUP BY关键字一起使用
SELECT TYPE,COUNT(
) AS “总记录” FROM USER GROUP BY TYPE;

– 使用SUM函数统计不同院系的总成绩
SELECT SUM(score) ‘总成绩’ ,TYPE FROM USER GROUP BY TYPE;

– 使用AVG函数计算不同院系的平均分
SELECT AVG(score) AS “平均分” ,TYPE FROM USER GROUP BY TYPE;

– 使用MAX函数计算user表最分数最高的
SELECT MAX(score) AS “最大值” FROM USER;

– 使用MIN函数计算user表最分数最低的
SELECT MIN(score) AS “最小值” FROM USER;

数据库的备份

  1. 打开cmd
  2. 进入管理员模式
  3. mysqldump -u root -p web_test1 >c:/web_test1.sql
  4. 输入密码

数据库的还原

  1. 数据库服务器内部创建数据库create database 名字
  2. 进入管理员模式
  3. mysql -u root -p web_test1 <c:/web_test1.sql
  4. 第二种 使用source
  5. source c:/web_test.sql

可视化数据库

(七) 多表查询

外键(foreign key)
一、基本概念

1、MySQL中“键”和“索引”的定义相同,所以外键和主键一样也是索引的一种。不同的是MySQL会自动为所有表的主键进行索引,但是外键字段必须由用户进行明确的索引。用于外键关系的字段必须在所有的参照表中进行明确地索引,InnoDB不能自动地创建索引。

2、外键可以是一对一的,一个表的记录只能与另一个表的一条记录连接,或者是一对多的,一个表的记录与另一个表的多条记录连接。

3、如果需要更好的性能,并且不需要完整性检查,可以选择使用MyISAM表类型,如果想要在MySQL中根据参照完整性来建立表并且希望在此基础上保持良好的性能,最好选择表结构为innoDB类型。

4、外键的使用条件

① 两个表必须是InnoDB表,MyISAM表暂时不支持外键

② 外键列必须建立了索引,MySQL 4.1.2以后的版本在建立外键时会自动创建索引,但如果在较早的版本则需要显式建立;

③ 外键关系的两个表的列必须是数据类型相似,也就是可以相互转换类型的列,比如int和tinyint可以,而int和char则不可以;

5、外键的好处:可以使得两张表关联,保证数据的一致性和实现一些级联操作。

二、使用方法

1、创建外键的语法:

外键的定义语法:

alter table employee add foreign key (dno) references dept(did);
[CONSTRAINT symbol] FOREIGN KEY [id] (index_col_name, …)

REFERENCES tbl_name (index_col_name, …)

[ON DELETE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}]

[ON UPDATE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}]

该语法可以在 CREATE TABLE 和 ALTER TABLE 时使用,如果不指定CONSTRAINT symbol,MYSQL会自动生成一个名字。

ON DELETE、ON UPDATE表示事件触发限制,可设参数:

① RESTRICT(限制外表中的外键改动,默认值)

② CASCADE(跟随外键改动)

③ SET NULL(设空值)

④ SET DEFAULT(设默认值)

⑤ NO ACTION(无动作,默认的)

2、示例

1)创建表1

create table repo_table(

repo_id char(13) not null primary key,

repo_name char(14) not null)

type=innodb;

创建表2

mysql> create table busi_table(

-> busi_id char(13) not null primary key,

-> busi_name char(13) not null,

-> repo_id char(13) not null,

-> foreign key(repo_id) references repo_table(repo_id))

-> type=innodb;

2)插入数据

insert into repo_table values(“12”,“sz”); //success
insert into repo_table values(“13”,“cd”); //success
insert into busi_table values(“1003”,“cd”, “13”); //success
insert into busi_table values(“1002”,“sz”, “12”); //success
insert into busi_table values(“1001”,“gx”, “11”); //failed,提示:

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (smb_man.busi_table, CONSTRAINT busi_table_ibfk_1 FOREIGN KEY (repo_id) REFERENCES repo_table (repo_id))

3)增加级联操作
mysql> alter table busi_table
-> add constraint id_check
-> foreign key(repo_id)
-> references repo_table(repo_id)
-> on delete cascade
-> on update cascade;


ENGINE=InnoDB DEFAULT CHARSET=gb2312; //另一种方法,可以替换type=innodb;

3、相关操作
外键约束(表2)对父表(表1)的含义:

在父表上进行update/delete以更新或删除在子表中有一条或多条对应匹配行的候选键时,父表的行为取决于:在定义子表的外键时指定的on update/on delete子句。

关键字
含义

CASCADE
删除包含与已删除键值有参照关系的所有记录

SET NULL
修改包含与已删除键值有参照关系的所有记录,使用NULL值替换(只能用于已标记为NOT NULL的字段)

RESTRICT
拒绝删除要求,直到使用删除键值的辅助表被手工删除,并且没有参照时(这是默认设置,也是最安全的设置)

NO ACTION
啥也不做

4、其他
在外键上建立索引:
index repo_id (repo_id),
foreign key(repo_id) references repo_table(repo_id))

表之间的关系
关系 说明
一对一 通过主外键连接①假设是一对多,再多的乙方创建外键只想一的一方的主键,将外键设置为唯一②将两个表的主键建立对应关系
多对多 需要中间表,中间表对其余两表的关系为多对一并且中间表至少两个字段分别指向两个表的主键
一对多 通过主外键连接,需要在多的一方增加一的一方的外键。

三范式
第一范式:原子不可再分
第二范式:不产生局部依赖,表中的记录完全依赖唯一标识的主键
第三范式:不产生传递关系,表中的每一列都直接依赖于主键。

准备一张部门表dept 和员工表emp

CREATE TABLE dept (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(20) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

insert into dept(id,name) values (1,‘开发部’),(2,‘市场部’),(3,‘财务部’),(4,‘销售部’);

CREATE TABLE emp (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(10) DEFAULT NULL,
gender char(1) DEFAULT NULL,
salary double DEFAULT NULL,
join_date date DEFAULT NULL,
dept_id int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY dept_id (dept_id),
CONSTRAINT emp_ibfk_1 FOREIGN KEY (dept_id) REFERENCES dept (id)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

insert into emp(id,name,gender,salary,join_date,dept_id) values (1,‘孙悟空’,‘男’,7200,‘2013-02-24’,1),(2,‘猪八戒’,‘男’,3600,‘2010-12-02’,2),(3,‘白骨精’,‘女’,5000,‘2015-10-07’,3),(4,‘蜘蛛精’,‘女’,4500,‘2011-03-14’,1);

内连接查询与外连接查询的区别:内连接查询仅仅查出表之间互相匹配的记录,而外连接则会选出其他不匹配的记录。

内连接:使用关键字INNER JOIN
select *from tab1 inner join tab2 on 关联条件
隐式内连接:隐藏INNER JOIN转而使用where
– 查询员工表的同时查询员工所属部门的相关信息
SELECT *FROM emp,dept WHERE emp.dept_id = dept.id;

显示内连接:使用 INNER JOIN … ON 语句,**
– 查询员工表的同时查询员工所属部门的相关信息
SELECT *FROM emp INNER JOIN dept ON emp.dept_id = dept.id;

左外连接:使用关键字LEFT JOIN
用左边表的记录去匹配右边表的记录,如果符合条件的则显示;否则,显示 NULL 可以理解为:在内连接的基础上保证左表的数据全部显示(左表是部门,右表员工)
– 查询员工表的同时查询员工所属部门的相关信息
SELECT *FROM emp LEFT JOIN dept ON emp.dept_id = dept.id;

右外连接:使用关键字RIGHT JOIN
用右边表的记录去匹配左边表的记录,如果符合条件的则显示;
– 查询员工表的同时查询员工所属部门的相关信息
SELECT *FROM emp LEFT JOIN dept ON emp.dept_id = dept.id;

子连接:
一个查询语句的条件可以是在另一条查询语句的查询结果当中,这时候可以使用IN的关键字查询
– 查询员工表的同时查询员工所属部门的相关信息
SELECT *FROM emp WHERE emp.dept_id IN (SELECT id FROM dept) ;
SELECT *FROM emp WHERE emp.dept_id EXISTS (SELECT id FROM dept) ;里面存在前面才会执行
SELECT *FROM emp WHERE emp.dept_id ANY (SELECT id FROM dept) ;大于里面最小的那个
SELECT *FROM emp WHERE emp.dept_id ALL (SELECT id FROM dept) ;大于里面最大的那个

MYSQL事务

  1. 开启事务start transaction
  2. 提交事务commit
  3. 回滚事务Rollback

事务通常包含一系列更新操作(增删改),这些更新操作都是作为一个不可分割的逻辑单元

如果事务中的某个更新操作执行失败,那么事务中所有更新的操作均被撤销,所有影响到的数据将返回事务开始以前的状态。

事务的更新操作要么全部更新,要么都不更新,这个特征叫事务的原子性。

存储引擎: InnoDB支持事务,MyISAM是不支持事务的。

事务的特性:

原子性:原子性意味着每一个事务都必须被认为不可分割的单元,事务的更新操作要么全部更新,要么都不更新。
一致性: 事务在执行前数据库的状态与执行后数据库的状态保持一致。如:在转账操作中,转账前2个人的 总金额是 2000,转账后 2 个人总金额也是 2000 。事务的一致性保证了数据库从不返回一个未处理完的事务
隔离性: 事务与事务之间不应该相互影响,执行时保持隔离的状态。 事务的隔离性原则保证了某个特点事务在未完成之前,其结果是看不见的。
持久性: 一旦事务执行成功,对数据库的修改是持久的。就算关机,也是保存下来的。
事务的隔离性级别:

事务在操作时的理想状态: 所有的事务之间保持隔离,互不影响。因为并发操作,多个用户同时访问同一个 数据。

**MYSQL并发的问题: **

脏读 一个事务读取到了另一个事务中尚未提交的数据

问题 解释
脏读 一个事务读取到了另一个事务中尚未提交的数据
不可重复读 一个事务中两次读取的数据内容不一致,要求的是一个事务中多次读取时数据是一致的,这 是事务 update 时引发的问题(读取另一个事务已提交的update的数据)
虚读/幻读 一个事务中两次读取的数据的数量不一致,要求在一个事务多次读取的数据的数量是一致 的,这是 insert 或 delete 时引发的问题(读取到另一个事务已提交的insert数据)
丢失更新 一个事务不知道其他事务的存在,就会发烧丢失更新问题-最后的更新覆盖了由其他事务所做的更新。

事务的隔离级别
当多个事务同时进行的时候,如何确保当前事务中数据的正确性,比如A、B两个事物同时进行的时候,A是否可以看到B已提交的数据或者B未提交的数据,这个需要依靠事务的隔离级别来保证,不同的隔离级别中所产生的效果是不一样的。

事务隔离级别主要是解决了上面多个事务之间数据可见性及数据正确性的问题。

隔离级别分为4种:

读未提交:READ-UNCOMMITTED 脏读,不可重复读,虚读
读已提交:READ-COMMITTED 不可重复读,虚读
可重复读:REPEATABLE-READ 虚读
串行:SERIALIZABLE 全都避免
上面4中隔离级别越来越强,会导致数据库的并发性也越来越低。

隔离级别的设置
分2步骤,修改文件、重启mysql,如下:
修改mysql中的my.init文件,我们将隔离级别设置为:READ-UNCOMMITTED,如下:
隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行
transaction-isolation=READ-UNCOMMITTED

MySQL索引
(一) 索引概述和分类
概念:索引是一种特殊的数据库结构,其作用相当于一本书的目录,可以用来快速查询数据库表中的特点记录。索引是提高数据库性能的重要方式

索引的优点:

通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
可以大大的加快数据的检索速度。
可以加速表和表之间的连接,特别是实现数据的参考完整性方面。
索引的缺点:

创建索引和维护索引需要消耗时间
索引需要占用物理空间。
对表的数据进行增删改时,索引也需要动态维护。
索引的特征:索引的两大特征为唯一性索引和复合索引

索引的分类:

普通索引:在创建普通索引时,不附加任何限制条件
唯一性索引:使用UNIQUE参数设置索引为唯一性索引
全文索引:使用FULLTEXT参数设置索引为全文索引
单列索引:在表中的单个字段上创建索引
多列索引:多列索引是在表中的多个字段上创建索引。
空间索引:使用SPATIAL参数可以设置索引为空间索引。
(二) 索引的操作
创建索引:关键字index [索引名] (属性名[长度])

1.在创建表的时候创建索引

CREATE TABLE test1 (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(255) NOT NULL,
INDEX t_name(NAME(5))
);

2.在已经存在的表中创建索引

CREATE INDEX t_name ON test2 (NAME(10));

3.使用ALTER TABLE语句创建索引

ALTER TABLE test3 ADD INDEX t_name (NAME(10));

4.创建唯一性索引或全文索引

CREATE TABLE test2 (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX t_name (NAME(10))
);
–或 在已经存在的表中创建全文索引
CREATE FULLTEXT INDEX t_name ON test2 (NAME(10));

5.多列索引

CREATE TABLE test3 (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX t_name_age (NAME,age)
);

6.查看索引

– 格式 SHOW INDEX FROM 数据库名.表名
SHOW INDEX FROM palewl.test3;
– SHOW INDEX FROM 表名 FROM 数据库名
SHOW INDEX FROM test3 FROM palewl;

7.删除索引

– DROP INDEX 索引名 ON 表名
DROP INDEX idx_name ON test1;
– ALTER TABLE 表名 DROP INDEX 索引名
ALTER TABLE test2 DROP INDEX t_name;

(三) 设计原则和注意事项
合理使用索引:

在经常需要搜索的列上创建索引,可以加快搜索的速度
在作为主键上的列,强制该列的唯一性和组织表中的数据排列结构
在经常用在连接的列上,这些列主要作为外键,加快连接的速度
在经常使用WHERE子句中的列上创建索引,加快条件的判断速度
不合理使用索引:

对于在查询中很少使用或者参考的列不应该创建索引。
对于只有很少数据值的列也不应该创建索引。
对于定义text、image和bit的数据类型不应该创建索引
当修改性能远远大于检索性能时,不应该创建索引。
MYSQL视图
(一) 视图概述
概念:

​ 视图是从一个或多个表中导出的表,是一种虚拟存在的表。视图就像一个窗口,通过这个窗口可以看到系统专门提供的数据。

视图与索引视图不需要占据存储空间,索引需要占据物理空间视图的优势:增强数据安全性、提高灵活性,操作变简单、提高数据的逻辑独立性

(二) 视图的操作
视图的创建使用关键字:CREATE VIEW

–格式 CREATE VIEW 视图名 AS SELECT *FROM 表名
–AS后面的语句是正常的查询语句,指将查询的结果导入视图
CREATE VIEW user_view AS SELECT *FROM t_user;

– 可指定视图里的具体参数
CREATE VIEW my_user(id,NAME) AS SELECT id,NAME FROM t_user;

–可借助视图查询 SELECT *FROM 视图名;
SELECT *FROM user_view;

删除视图

– 格式 DROP VIEW 视图名;
DROP VIEW user_view;

查看视图定义

–格式 SHOW CREATE VIEW 视图名
SHOW CREATE VIEW my_user;

还有别的方式可以查看视图定义

SHOW TABLE STATUS LIKE 视图名
DESC 视图名

修改视图定义

修改视图是指修改数据库中已经存在表的定义。当基本表的某些字段发送改变时,可以通过修改视图来保持视图与基本表之间的一致。

– 格式CREATE OR REPLACE VIEW 视图名 AS 语句
CREATE OR REPLACE VIEW my_user AS SELECT *FROM tb_user;

更新视图数据

对视图的更新其实就是对表的更新,更新视图是指通过视图来插入、更新、删除表中的数据。视图的更新会对表的数据进行修改,但是修改视图定义并不会修改原表中的数据

(三) 对视图的进一步说明
视图是在原有的表或者视图的基础上重新定义的虚拟表,这样可以从原有的表选取对用户有用的信息

主要有:

使操作变的更简单。视图的目的就是要所见即所需
增加数据的安全性
提高表的逻辑独立性
MySQL触发器与事件
(一) 存储过程与函数介绍
概念:存储过程是一组为了完成特定功能的SQL语句集,经过编译后存储在数据库中,用户可以通过指定存储过程的名字来调用执行。

​ 存储过程是一个可编程的函数,它在数据库中创建并保存,由SQL语句和一些特殊的控制结构组成。

优点:

存储过程增强了SQL语言的功能和灵活性
存储过程运行标准组件的编程。
存储过程能够实现较快的执行速度
存储过程能够减少网络流量。
存储过程与函数的区别:

一般存储过程实现的功能要复杂一些,而函数实现的功能针对性比较强
存储过程可以返回参数,如记录集,而函数只能返回值或表对象。
(二) 存储过程和函数的操作
​ 创建一个存储过程的基本格式

create procedure 名称()
begin

end

一个简单的存储过程test

create procedure test1()
begin
select * from users;
select * from orders;
end;

调用存储过程

CALL test1();

但是:

在 MySQL 中,服务器处理 SQL 语句默认是以分号作为语句结束标志的。然而,在创建存储过程时,存储过程体可能包含有多条 SQL 语句,这些 SQL 语句如果仍以分号作为语句结束符,那么 MySQL 服务器在处理时会以遇到的第一条 SQL 语句结尾处的分号作为整个程序的结束符,而不再去处理存储过程体中后面的 SQL 语句,这样显然不行.

为解决以上问题,通常使用 DELIMITER 命令将结束命令修改为其他字符。语法格式如下:

– DELIMITER − − 其 中 --其中 是用户自定义的介绍符号

所以,创建存储过程的语句就成为了:

DELIMITER C R E A T E P R O C E D U R E t e s t 2 ( ) B E G I N S E L E C T ∗ F R O M t u s e r ; S E L E C T ∗ F R O M t o r d e r ; E N D CREATE PROCEDURE test2() BEGIN SELECT * FROM t_user; SELECT * FROM t_order; END CREATEPROCEDUREtest2()BEGINSELECTFROMtuser;SELECTFROMtorder;END
– 如果需要在结束时将结束语句换回; 则:
DELIMITER C R E A T E P R O C E D U R E m y p r o 4 ( ) B E G I N S E L E C T ∗ F R O M t u s e r ; S E L E C T ∗ F R O M t o r d e r ; E N D CREATE PROCEDURE mypro4() BEGIN SELECT * FROM t_user; SELECT * FROM t_order; END CREATEPROCEDUREmypro4()BEGINSELECTFROMtuser;SELECTFROMtorder;END
DELIMITER ;
– 注意:DELIMITER 和分号“;”之间一定要有一个空格

此时就可以正常使用了

CALL test();

删除存储过程

– 格式 drop procedure 名称
DROP PROCEDURE MYPRO4

JDBC
JDBC 规范定义接口,具体的实现由各大数据库厂商来实现。
JDBC 是 Java 访问数据库的标准规范,真正怎么操作数据库还需要具体的实现类,也就是数据库驱动。每个数据库厂商根据自家数据库的通信格式编写好自己数据库的驱动。所以我们只需要会调用 JDBC 接口中的方法即可,数据库驱动由数据库厂商提供。

使用 JDBC 的好处:

  1. 程序员如果要开发访问数据库的程序,只需要会调用 JDBC 接口中的方法即可,不用关注类是如何实现的。
  2. 使用同一套 Java 代码,进行少量的修改就可以访问其他 JDBC 支持的数据库

JDBC开发步骤
第一步:加载驱动
Class.forname(“com.mysql.jdbc.Driver”)
第二步:获得连接
Connection connection = DriverManager.getConnection(url, “root”, “root”);
url:jdbc:mysql://localhost:3306/web_test3
第三步:基本操作
Statement sql=“select * from user”;
String sql=“select * from user”;
ResultSet rs=statement.executeQuery(sql);
while(rs.next()){
sout(rs.getInt(“id”));
}

第四步:释放操作
rs.close();
statement.close();
conn.close();

DriverManager类
2.1 DriverManager 作用:

  1. 管理和注册驱动(一般不用:调用方法注册需要传递驱动作为参数,传递驱动new会自动注册驱动,这样导致会注册两次)
  2. 创建数据库的连接!

2.2 类中的方法:
DriverManager 类中的静态方法
描述
Connection getConnection (String url, String user, String password)
通过连接字符串,用户名,密码来得到数据 库的连接对象

Connection getConnection (String url, Properties info)
通过连接字符串,属性对象来得到连接对象

Conection接口!!非常稀有,尽量晚创建早关闭

3.1 Connection 作用:
Connection 接口,具体的实现类由数据库的厂商实现,代表一个连接对象。

  1. 创建执行sql语句的对象
    statement 执行sql
    callablestatement 执行数据库中存储过程
    preparedstatement 执行SQL进行sql预处理
  2. 管理事务

3.2 Connection 方法:
Connection 接口中的方法
描述
Statement createStatement()
创建一条 SQL 语句对象

Statement接口
4.1 JDBC 访问数据库的步骤

  1. 注册和加载驱动(可以省略)
  2. 获取连接
  3. Connection 获取 Statement 对象
  4. 使用 Statement 对象执行 SQL 语句
  5. 返回结果集
  6. 释放资源

4.2 Statement 作用:
代表一条语句对象,用于发送 SQL 语句给服务器,用于执行静态 SQL 语句并返回它所生成结果的对象。

4.3 Statement 中的方法:
Statement 接口中的方法

描述
int executeUpdate(String sql)
用于发送 DML 语句,增删改的操作,insert、update delete
参数:SQL 语句
返回值:返回对数据库影响的行数

ResultSet executeQuery(String sql)
用于发送 DQL 语句,执行查询的操作。select
参数:SQL 语句
返回值:查询的结果集
rs.next()指向下一个元素,没有返回false,boolean类型
rs.GetXXXX();

执行 DDL 操作 使用 JDBC 在 MySQL 的数据库中创建一张学生表

public static void main(String[] args) {
    
    
//1. 创建连接
Connection conn = null;
Statement statement = null;
try {
    
    
conn = DriverManager.getConnection("jdbc:mysql:///day24", "root", "root");
//2. 通过连接对象得到语句对象
statement = conn.createStatement();
//3. 通过语句对象发送 SQL 语句给服务器
//4. 执行 SQL
statement.executeUpdate("create table student (id int PRIMARY key auto_increment, " +
"name varchar(20) not null, gender boolean, birthday date)");
//5. 返回影响行数(DDL 没有返回值)
System.out.println("创建表成功");
    } catch (SQLException e) {
    
    
e.printStackTrace();
    }
//6. 释放资源
finally {
    
    
//关闭之前要先判断
if (statement != null) {
    
    
try {
    
    
statement.close();
} catch (SQLException e) {
    
    
e.printStackTrace();
    }
7 / 21}
if (conn != null) {
    
    
try {
    
    
conn.close();
} catch (SQLException e) {
    
    
e.printStackTrace();
          }
        }
      }
   }
}

执行 DML 操作 向学生表中添加 4 条记录,主键是自动增长

public static void main(String[] args) throws SQLException {
    
    
// 1) 创建连接对象
Connection connection = DriverManager.getConnection("jdbc:mysql:///day24", "root",
"root");
// 2) 创建 Statement 语句对象
Statement statement = connection.createStatement();
// 3) 执行 SQL 语句:executeUpdate(sql)
int count = 0;
// 4) 返回影响的行数
count += statement.executeUpdate("insert into student values(null, '孙悟空', 1, '1993-03-24')");
count += statement.executeUpdate("insert into student values(null, '白骨精', 0, '1995-03-24')");
count += statement.executeUpdate("insert into student values(null, '猪八戒', 1, '1903-03-8 / 2124')");
count += statement.executeUpdate("insert into student values(null, '嫦娥', 0, '1993-03-11')");
System.out.println("插入了" + count + "条记录");
// 5) 释放资源
statement.close();
connection.close();
    }
}

执行 DQL 操作 封装数据库查询的结果集,对结果集进行遍历,取出每一条记录。

public static void main(String[] args) throws SQLException {
    
    
    //1) 得到连接对象
    Connection connection =
    DriverManager.getConnection("jdbc:mysql://localhost:3306/day24","root","root");
    //2) 得到语句对象
    Statement statement = connection.createStatement();
    //3) 执行 SQL 语句得到结果集 ResultSet 对象
    ResultSet rs = statement.executeQuery("select * from student");
    //4) 循环遍历取出每一条记录
    while(rs.next()) {
    
    
        int id = rs.getInt("id");
        String name = rs.getString("name");
        boolean gender = rs.getBoolean("gender");
        Date birthday = rs.getDate("birthday");
    //5) 输出的控制台上
System.out.println("编号:" + id + ", 姓名:" + name + ", 性别:" + gender + ", 生日:" +birthday);
}
//6) 释放资源
rs.close();
statement.close();
connection.close();
    }
}

数据库工具类JdbcUtils

如果一个功能经常要用到,我们建议把这个功能做成一个工具类,可以在不同的地方重用。

/**
 * 这个类是jdbc的工具类
 * 提供getConnection方法
 * 提供close方法
 * 开发步骤:
 * 1.私有化构造函数,防止外界直接new对象
 * 2.提供getConnection,用来对外界提供获取数据连接
 * 3.提供close方法,用来关闭资源
 * 4.测试
 *
 */
public class JDBCUtils {
    
    
    //将读取属性文件放在静态代码块中
    //保证文件只被读取一次,节省资源
    static Properties prop =null;
    static{
    
    
        try{
    
    
            //读取配置文件jdbc.properties
            prop = new Properties();
            String pathname="jdbc.properties";
            prop.load(new FileInputStream(pathname));
        }catch (Exception e) {
    
    
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    //1.私有化构造函数,
    // 防止外界直接new对象
    private JDBCUtils(){
    
    }
    //2.提供getConnection,
    // 用来对外界提供获取数据连接
    public static Connection getConnection(){
    
    
        try {
    
    
            //1.注册驱动
            Class.forName(
                    prop.getProperty("driverClass"));
            //2.获取数据库连接
            Connection conn = DriverManager.getConnection(
                    prop.getProperty("jdbcUrl"),
                    prop.getProperty("user"),
                    prop.getProperty("password"));
            return conn;
        } catch (Exception e) {
    
    
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
    // 3.提供close方法,用来关闭资源
    public static void close(Connection conn,Statement st,ResultSet rs){
    
    
        if(conn!=null){
    
    
            try {
    
    
                conn.close();
            } catch (SQLException e) {
    
    
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally{
    
    
                //保证资源一定会被释放
                conn=null;
            }
        }
        if(st!=null){
    
    
            try {
    
    
                st.close();
            } catch (SQLException e) {
    
    
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally{
    
    
                //保证资源一定会被释放
                st=null;
            }
        }
        if(rs!=null){
    
    
            try {
    
    
                rs.close();
            } catch (SQLException e) {
    
    
                // TODO Auto-generated catch block
                e.printStackTrace();
            }finally{
    
    
                //保证资源一定会被释放
                rs=null;
            }
        }
    }
}

SQL 注入问题

当我们输入以下密码,我们发现我们账号和密码都不对竟然登录成功了
请输入用户名:
newboy
请输入密码:
a’ or ‘1’='1
select * from user where name=‘newboy’ and password=‘a’ or ‘1’=‘1’
登录成功,欢迎您:newboy

问题分析:
select * from user where name=‘newboy’ and password=‘a’ or ‘1’=‘1’
name=‘newboy’ and password=‘a’ //为假
‘1’=‘1’ //真
相当于
select * from user where true; //查询了所有记录
a’ or ‘1’=‘1
a’ 空格–空格
我们让用户输入的密码和 SQL 语句进行字符串拼接。用户输入的内容作为了 SQL 语句语法的一部分,改变了原有 SQL 真正的意义,以上问题称为 SQL 注入。要解决 SQL 注入就不能让用户输入的密码和我们的 SQL 语句进行简单的字符串拼接。

PreparedStatement
PreparedStatement 是 Statement 接口的子接口,继承于父接口中所有的方法。它是一个预编译的 SQL 语句

PreparedSatement 的执行原理

  1. 因为有预先编译的功能,提高 SQL 的执行效率。
  2. 可以有效的防止 SQL 注入的问题,安全性更高。

Connection 接口中的方法
描述
PreparedStatement prepareStatement(String sql)
指定预编译的 SQL 语句,SQL 语句中使用占位符?
创建一个语句对象

PreparedStatement 接口中的方法
描述
int executeUpdate()
执行 DML,增删改的操作,返回影响的行数。

ResultSet executeQuery()
执行 DQL,查询的操作,返回结果集
PreparedSatement 的好处
1.prepareStatement()会先将 SQL 语句发送给数据库预编译。PreparedStatement 会引用着预编译后的结果。
2.可以多次传入不同的参数给 PreparedStatement 对象并执行。减少 SQL 编译次数,提高效率。
3.安全性更高,没有 SQL 注入的隐患。
4.提高了程序的可读性

使用 PreparedStatement 的步骤:

  1. 编写 SQL 语句,未知内容使用?占位:“SELECT * FROM user WHERE name=? AND password=?”;
  2. 获得 PreparedStatement 对象
  3. 设置实际参数:setXxx(占位符的位置, 真实的值)
  4. 执行参数化 SQL 语句
  5. 关闭资源

使用 PreparedStatement 查询一条数据,封装成一个学生 Student 对象

public static void main(String[] args) throws SQLException {
    
    
    //创建学生对象
    Student student = new Student();
    17 / 21
    Connection connection = JdbcUtils.getConnection();
    PreparedStatement ps = connection.prepareStatement("select * from student where              id=?");
    //设置参数
    ps.setInt(1,2);
    ResultSet resultSet = ps.executeQuery();
    if (resultSet.next()) {
    
    
    //封装成一个学生对象
    student.setId(resultSet.getInt("id"));
    student.setName(resultSet.getString("name"));
    student.setGender(resultSet.getBoolean("gender"));
    student.setBirthday(resultSet.getDate("birthday"));
    }
    //释放资源
    JdbcUtils.close(connection,ps,resultSet);
    //可以数据
    System.out.println(student);
    } 
}

executebatch批处理

JDBC的事务处理
之前我们是使用 MySQL 的命令来操作事务。接下来我们使用 JDBC 来操作银行转账的事务。

准备数据
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
balance DOUBLE
);
– 添加数据
INSERT INTO account (NAME, balance) VALUES (‘Jack’, 1000), (‘Rose’, 1000);

API 介绍
Connection 接口中与事务有关的方法

说明
void setAutoCommit(boolean autoCommit)
参数是 true 或 false

如果设置为 false,表示关闭自动提交,相当于开启事务
void commit()

提交事务
void rollback()

回滚事务
开发步骤

  1. 获取连接
  2. 开启事务
  3. 获取到 PreparedStatement
  4. 使用 PreparedStatement 执行两次更新操作
  5. 正常情况下提交事务
  6. 出现异常回滚事务
  7. 最后关闭资源

案例代码

package com.lqg;
import com.lqg.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class Demo12Transaction {
    
    
    //没有异常,提交事务,出现异常回滚事务
    public static void main(String[] args) {
    
    
    20 / 21
    //1) 注册驱动
    Connection connection = null;
    PreparedStatement ps = null;
    try {
    
    
    //2) 获取连接
    connection = JdbcUtils.getConnection();
    //3) 开启事务
    connection.setAutoCommit(false);//!!!!!
    //4) 获取到 PreparedStatement
    //从 jack 扣钱
    ps = connection.prepareStatement("update account set balance = balance - ? where
    name=?");
    ps.setInt(1, 500);
    ps.setString(2,"Jack");
    ps.executeUpdate();
    //出现异常
    System.out.println(100 / 0);
    //给 rose 加钱
    ps = connection.prepareStatement("update account set balance = balance + ? where
    name=?");
    ps.setInt(1, 500);
    ps.setString(2,"Rose");
    ps.executeUpdate();
    //提交事务
    connection.commit();//!!!!
    System.out.println("转账成功");
    } catch (Exception e) {
    
    
    e.printStackTrace();
    try {
    
    
    //事务的回滚
    connection.rollback();//!!!!!
    } catch (SQLException e1) {
    
    
    e1.printStackTrace();
    }
    System.out.println("转账失败");
    }
    finally {
    
    
    //7) 关闭资源
    JdbcUtils.close(connection,ps);
        } 
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44177643/article/details/114646064