1. 数据库的重要性

数据库几乎是软件体系中最核心的一个存在。

2. 数据库分类

  • 关系型数据库(SQL DB)

    使用关系模型组织数据;

    包括MySQL、Oracle、SQL Server、SQLite、DB2、…

  • 非关系型数据库(NoSQL DB)

    数据通常以对象的形式存储在数据库中;

    对象之间的关系通过每个对象自身的属性来决定;

    常用于存储非结构化的数据

    包括 Redis、MongoDB、…

3. 数据库操作

MySQL 常用命令参考:MySQL 数据库常用命令大全(完整版)_mysql数据库命令大全

3.1 SQL 语言分类

名称 命令 解释
DDL(数据定义语言) CREATE、DROP、ALTER 维护存储数据的结构,如数据库、表、视图、索引等
DML(数据操作语言) INSERT、UPDATE、DELETE 操作数据库对象中所包含的数据
DQL(数据查询语言) SELECT 查询数据库当中的数据
DCL(数据控制语言) GRANT、commit、rollback 管理数据库,包括管理权限及数

3.2 存储引擎

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方法。

使用命令 show engines; 查看数据库可以支持的存储引擎,命令可以显示当前数据库支持的存储引擎情况。

MySQL 数据库支持多种存储引擎,如 MyISAM、InnoDB、MEMORY、…

MyISAM 与 InnoDB 对比:

特性 MyISAM InnoDB MEMORY
事务安全
空间使用
内存使用
插入数据速度
支持外键
全文索引 ✅ (since MySQL 5.6) -
适用场合 安全性、事务处理以及多用户操作 节约空间、速度快 安全性要求较低、速度快

InnoDB 是 MySQL 的默认存储引擎。

3.3 存储位置(仅针对 MySQL 8.0)

MySQL 数据以文件方式存储在磁盘中,包括表文件、数据文件、以及数据库的选项文件

位置:C:\ProgramData\MySQL\MySQL Server 8.0\Data

Data目录下存储数据,目录名对应数据库名,该目录下文件名对应数据表

注意:

  • InnoDB 类型的数据表只有一个 *.ibd 文件,以及上一级目录(...\Data)中的 ibdata1 文件;
  • MyISAM 类型的数据表对应三个文件:
    • *.sdi 文件 - 表结构信息文件
    • *.MYD 文件 - 数据文件
    • *.MYI 文件 - 索引文件

参考来源:⑩【MySQL】存储引擎详解, InnoDB、MyISAM、Memory。

3.4 MySQL 字符集和排序规则

字符集:推荐 utf8mb4

排序规则:推荐 utf8mb4_0900_ai_ci

参考来源:【MySQL】如何选择字符集与排序规则(字符集校验规则)_mysql字符集和排序规则如何选择

3.5 MySQL 注释

单行注释:以 -- 开头

多行注释:以 /* 开头并以 */ 结尾

3.6 DDL

3.5.1 操作数据库
  • 查看所有数据库:show databases;
  • 创建数据库:create database [if not exists] DBName;
  • 使用数据库:use DBName;
  • 查看当前使用的数据库:select database();
  • 删除数据库:drop database [if exists] DBName;
3.5.2 操作数据表
  • 查看当前数据库下的所有数据表:show tables;

  • 查看数据表的结构:desc tableName;

  • 创建数据表:

    1
    2
    3
    4
    5
    6
    create table [if not exists] `tableName` (
    '字段名1' 数据类型 [属性][索引][注释],
    '字段名2' 数据类型 [属性][索引][注释],
    #...
    '字段名n' 数据类型 [属性][索引][注释]
    );
  • 修改数据表:

    • 修改表名:alter table oldTableName rename as/to newName;
    • 增加字段:alter table tableName add 字段名 数据类型;
    • 修改数据类型:alter table tableName modify 字段名 新数据类型;
    • 修改字段名和数据类型:alter table tableName change 字段名 新字段名 新数据类型;
    • 删除列:alter table tableName drop 字段名;
  • 删除数据表:drop table [if exists] tableName;

3.7 DML

  • 添加数据:INSERT INTO tableName (column1, column2, ...) VALUES (value1, value2, ...), (value1, value2, ...), (value1, value2, ...)...;

  • 修改数据:UPDATE tableName SET column1 = value1, column2 = value2, ... [WHERE condition];

  • 删除数据:DELETE FROM tableName [WHERE condition];

  • 清空表数据但表结构、索引、约束等不变:TRUNCATE TABLE tableName;

    Notice: 使用 TRUNCATE TABLE 会重新设置 AUTO_INCREMENT 计数器

3.8 DQL

  • ImportantSQL 查询语句执行顺序:

    1
    2
    3
    4
    5
    6
    7
    8
    select [all | distinct] {* | table.* | [table.field_1 [as alias_1][, table.field_2 [as alias_2]][, ...]]}
    from table_name [as table_alias]
    [left | right | inner join table_name_2] -- 连接查询
    [where ...] -- 指定结果需满足的条件
    [group by ...] -- 指定结果按照哪几个字段来分组
    [having] -- 过滤分组的记录必须满足的次要条件
    [order by ...] -- 指定查询记录按一个或多个条件排序
    [limit {[offset,] rows | rows OFFSET offset}]; -- 指定查询的记录从哪条至哪条

​ 优先级:where > group by > having > order by > limit

  • 模糊查询(between…and…\like\in)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -- between...and... 示例
    select sno, sname, score from student where score between 0 and 100;

    -- like 示例
    select sno, sname, score from student where sname like "felix_"; -- 下划线 "_" 代表1个字符
    select sno, sname, score from student where sname like "felix%"; -- "%" 代表0到任意个字符

    -- in 示例
    select sno, sname, address from student where address in ('北京', '南京', '重庆');
  • 连接查询

    参考博客:SQL 连接(JOIN)

  • 排序

    语法:order by 字段名 desc/asc,desc 代表降序排序,asc 则表示升序排序

  • 分页

    语法:select * from tableName limit [offset,] rows | rows OFFSET offset

    limit 子句可以被用于强制 select 语句返回指定的记录数;

    limit 接受一个或两个数字参数,参数必须是一个整数常量:

    • 如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目。

      初始记录行的偏移量是 0,而不是 1

    • 如果只给定一个参数,它表示返回最大的记录行数目

  • 子查询

    所谓子查询,就是在查询语句中的 where 条件子句中,又嵌套了另一个查询语句

3.9 外键

在数据库中,外键用于建立表与表之间的引用关系,维护数据的一致性和完整性。

外键字段关联到另一个表的主键字段,以建立两个表之间的关系。

  • 创建外键

    • 方式一:创建数据表时同时创建外键

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      -- 示例
      -- 解释:CONSTRAINT constraintName 子句作用是为外键约束定义约束名称。如果省略它,MySQL 将自动生成一个名称。

      CREATE TABLE `grade` (
      `gradeid` INT(10) NOT NULL AUTO_INCREMENT COMMENT '年级ID',
      `gradename` VARCHAR(50) NOT NULL COMMENT '年级名称',
      PRIMARY KEY (`gradeid`)
      ) ENGINE=INNODB DEFAULT CHARSET=utf8

      CREATE TABLE `student` (
      `studentno` INT(4) NOT NULL COMMENT '学号',
      `studentname` VARCHAR(20) NOT NULL DEFAULT '匿名' COMMENT '姓名',
      `sex` TINYINT(1) DEFAULT '1' COMMENT '性别',
      `gradeid` INT(10) DEFAULT NULL COMMENT '年级',
      `phoneNum` VARCHAR(50) NOT NULL COMMENT '手机',
      `address` VARCHAR(255) DEFAULT NULL COMMENT '地址',
      `borndate` DATETIME DEFAULT NULL COMMENT '生日',
      `email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
      `idCard` VARCHAR(18) DEFAULT NULL COMMENT '身份证号',
      PRIMARY KEY (`studentno`),
      KEY `FK_gradeid` (`gradeid`),
      CONSTRAINT `FK_gradeid` FOREIGN KEY (`gradeid`) REFERENCES `grade` (`gradeid`)
      ) ENGINE=INNODB DEFAULT CHARSET=utf8
    • 方式二:创建数据表完成后,修改数据表添加主键

      1
      2
      3
      4
      -- 示例
      -- 解释:CONSTRAINT FK_gradeid 子句作用是为外键约束定义约束名称。如果省略它,MySQL 将自动生成一个名称。

      ALTER TABLE `student` ADD CONSTRAINT constraintName FOREIGN KEY (`gradeid`) REFERENCES `grade` (`gradeid`);
  • 删除外键:

    1
    2
    3
    4
    5
    -- 第一步:删除外键
    alter table tableName drop foreign key constraintName;

    -- 第二步:删除外键索引
    alter table tableName drop index constrainName;

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

4. MySQL 函数

官方文档:MySQL :: MySQL 8.0 Reference Manual :: 14 Functions and Operators

4.1 常用函数

数据函数

  • SELECT ABS(-8); – 绝对值
  • SELECT CEILING(9.4); --向上取整
  • SELECT FLOOR(9.4); --向下取整
  • SELECT RAND(); --随机数,返回一个 0~1 之间的随机数
  • SELECT SIGN(0); --符号函数: 负数返回-1,正数返回1,0返回0

字符串函数

  • SELECT CHAR_LENGTH('坚持就是胜利'); – 返回字符串包含的字符数
  • SELECT CONCAT('我','爱','编程'); – 合并字符串,参数可以有多个
  • SELECT INSERT(s1, x, len, s2); – 使用字符串 s2 替换字符串 s1 部分,起始于字符串 s1 的 x 位置,共替换字符串 s1 的 len 长度
  • SELECT LOWER('KuangShen'); – 转小写
  • SELECT UPPER('KuangShen'); – 转大写
  • SELECT LEFT('hello,world',5); – 从左边截取
  • SELECT RIGHT('hello,world',5); – 从右边截取
  • SELECT REPLACE('坚持就能成功','坚持','努力'); – 替换字符串
  • SELECT SUBSTR('坚持就能成功',4,6); – 截取子字符串,开始和长度
  • SELECT REVERSE('坚持就能成功'); – 字符反转

日期时间函数

  • SELECT CURRENT_DATE(); – 获取当前日期
  • SELECT CURDATE(); – 获取当前日期
  • SELECT NOW(); – 获取当前日期和时间
  • SELECT LOCALTIME(); – 获取当前日期和时间
  • SELECT SYSDATE(); – 获取当前日期和时间
  • SELECT YEAR(NOW()); – 获取年
  • SELECT MONTH(NOW()); – 获取月
  • SELECT DAY(NOW()); – 获取日
  • SELECT HOUR(NOW()); – 获取时
  • SELECT MINUTE(NOW()); – 获取分
  • SELECT SECOND(NOW()); – 获取秒

系统信息函数

  • SELECT VERSION(); --查看 MySQL 版本
  • SELECT USER(); --查看当前用户

加密函数

  • md5() – 信息加密

4.2 聚合函数

  • COUNT() – 返回满足 select 条件的记录总和数

    • count(字段名) 会统计该字段在表中出现的次数,忽略字段为 null 的情况

    • count(*) 包括了所有的列,相当于行数,在统计结果的时候,包含字段为 null 的记录;

    • count(1) 用 1 代表代码行,在统计结果的时候,包含字段为 null 的记录 。

    从含义上讲,count(1)count(*) 都表示对全部数据行的查询:

    ​ 1)在表没有主键时,count(1)比count(*)快;

    ​ 2)有主键时,主键作为计算条件,count(主键)效率最高;

    ​ 3)若表格只有一个字段,则count(*)效率较高。

    推荐使用 count(1)

  • SUM() – 返回数字字段或表达式列作统计,返回一列的总和。

  • AVG() – 通常为数值字段或表达列作统计,返回一列的平均值

  • MAX() – 可以为数值字段,字符字段或表达式列作统计,返回最大的值。

  • MIN() – 可以为数值字段,字符字段或表达式列作统计,返回最小的值。

5. 事务

事务是一组 SQL 语句的执行,被视为一个单独的工作单元。

  • 在 MySQL 中,只有使用了 InnoDB 数据库引擎的数据库或表才支持事务
  • 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行
  • 事务用来管理 insert、update、delete 语句

事务特性(ACID)

  • 原子性(Atomicity)

    一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

    事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性(Consistency)

    事务完成后,所有数据的状态都是一致的。

    “以转账为例,假设有五个账户,每个账户余额是100元,五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元。”

  • 隔离性(Isolation)

    数据库允许多个事务同时对其数据进行读写和修改,而隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,如:

    • 脏读:A 事务正在修改数据但未提交,此时 B 事务去读取此条数据,B 事务读取的是未提交的数据,A 事务回滚。
    • 不可重复读:事务 A 多次读取同一数据,但事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。
    • 幻读:事务 A 第一次查询查到 5 条数据,第二次查到 8 条数据,这是因为在两次查询的间隙,事务 B 插入了 3 条数据。

    不可重复读与幻读区别:

    • 不可重复读是读取了其他事务更改的数据,针对 update 操作
    • 幻读是读取了其他事务新增的数据,针对 insert 和 delete 操作

    为了防止多个事务执行间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。

    SQL 标准定义四种隔离级别,包括:

    • 读未提交(Read uncommitted)
    • 读提交(read committed)
    • 可重复读(repeatable read)
    • 串行化(Serializable)
  • 持久性(Durability)

    事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务实现

  • Notice: MySQL 中默认是自动提交,使用事务时应先关闭自动提交

    1
    2
    3
    4
    5
    -- 使用 set 语句改变自动提交模式

    SET autocommit = 0; -- 关闭自动提交

    SET autocommit = 1; -- 开启自动提交
  • 事务处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SET autocommit = 0; -- 关闭自动提交

    START TRANSACTION -- 开始一个事务,标记事务的起始点
    /*
    (待执行的SQL 语句)
    */
    COMMIT -- 提交一个事务给数据库

    ROLLBACK -- 将事务回滚,数据回到本次事务的初始状态

    SET autocommit = 1 -- 还原MySQL数据库的自动提交
  • 保存点(SAVEPOINT)

    1
    2
    3
    4
    5
    SAVEPOINT sp_name -- 设置保存点(一个事务中可以有多个 SAVEPOINT)

    ROLLBACK TO SAVEPOINT sp_name -- 回滚到保存点

    RELEASE SAVEPOINT sp_name -- 删除保存点

6. 索引

6.1 索引作用

  • 提高查询速度
  • 确保数据的唯一性
  • 可以加速表和表之间的连接,实现表与表之间的参照完整性
  • 使用分组和排序子句进行数据检索时可以显著减少分组和排序的时间
  • 全文检索字段进行搜索优化

6.2 索引分类

  • 主键索引 - PRIMARY KEY

    特点:

    • 确保数据记录的唯一性
    • 确定特定数据记录在数据库中的位置
    • 一张数据表中最多只能有一个主键
  • 唯一索引 - UNIQUE KEY

    作用: 避免同一个表中某个字段的值重复

    一张数据表中可以创建多个唯一索引

  • 普通索引 - INDEX/KEY

    作用 : 快速定位特定数据

    注意:不宜添加太多普通索引,影响数据的插入、删除和修改操作

  • 全文索引 - FULLTEXT KEY

    作用:快速定位特定数据

    注意:

    • 只能用于 MyISAM 类型的数据表

    • 只能用于 char, varchar, text 数据列类型

    • 适合大型数据集

6.3 创建索引

  • 方式一:创建数据表时同时创建索引

    1
    2
    3
    4
    5
    CREATE TABLE tableName (
    字段名1 数据类型 [完整性约束条件…],
    字段名2 数据类型 [完整性约束条件…],
    [UNIQUE | FULLTEXT | SPATIAL ] INDEX | KEY [索引名] (字段名[(长度)] [ASC |DESC])
    );
  • 方式二:在已创建的数据表上创建索引

    1
    2
    3
    4
    5
    -- CREATE 命令
    CREATE [UNIQUE | FULLTEXT | SPATIAL ] INDEX index_name ON table_name (字段名[(长度)] [ASC |DESC]);

    -- ALTER 命令
    ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL ] INDEX index_name (字段名[(长度)] [ASC |DESC]);

显示索引信息: SHOW INDEX FROM tableName;

6.4 删除索引

命令:

1
2
3
4
5
-- 方式一
DROP INDEX 索引名 ON 表名字;

-- 方式二
ALTER TABLE 表名 DROP IDNEX index_name;

6.5 索引准则

  • 索引不是越多越好
  • 不要对经常变动的数据加索引
  • 小数据量的表建议不要加索引
  • 索引一般应加在查找条件的字段

扩展阅读:CodingLabs - MySQL索引背后的数据结构及算法原理

7. 权限管理

7.1 用户管理

  • 用户信息表:mysql.user

  • 刷新权限

    1
    flush privileges;
  • 增加用户

    1
    CREATE USER 'userName'@'host' IDENTIFIED BY [PASSWORD] 'pwd';

    注意:

    • 必须拥有 MySQL 数据库的全局 CREATE USER 权限,或拥有 INSERT 权限
    • 只能创建用户,不能赋予权限
    • 注意用户名格式,userName 表示用户名,host 指定该用户在哪个主机可以登录
      • 如果是本地用户,host 可用 localhost
      • 如果想让该用户可以从任意远程主机登陆,host 可以使用通配符 %
    • 密码需要引号,即使是纯数字密码也要加引号
      • 如果密码是一个普通的字符串,则不需要使用 PASSWORD 关键字
      • 如果要把密码指定为由PASSWORD()函数返回的混编值,需包含 PASSWORD 关键字
  • 重命名用户

    1
    RENAME USER old_user TO new_user
  • 设置密码

    1
    2
    3
    4
    5
    -- 为当前用户设置密码
    SET PASSWORD = PASSWORD('pwd');

    -- 为指定用户设置密码
    SET PASSWORD FOR 用户名 = PASSWORD('pwd');
  • 删除用户

    1
    DROP USER userName;
  • 查看用户权限

    1
    2
    3
    4
    5
    6
    7
    -- 查看权限
    SHOW GRANTS FOR 用户名

    -- 查看当前用户权限
    SHOW GRANTS; -- 或
    SHOW GRANTS FOR CURRENT_USER; -- 或
    SHOW GRANTS FOR CURRENT_USER();
  • MySQL 权限解释

    权限类型 权限说明
    All/All Privileges 代表全局或者全数据库对象级别的所有权限
    Alter 代表允许修改表结构的权限,但必须要求有create和insert权限配合。如果是rename表名,则要求有alter和drop原表, create和insert新表的权限
    Alter routine 代表允许修改或者删除存储过程、函数的权限
    Create 代表允许创建新的数据库和表的权限
    Create routine 代表允许创建存储过程、函数的权限
    Create tablespace 代表允许创建、修改、删除表空间和日志组的权限
    Create temporary tables 代表允许创建临时表的权限
    Create user 代表允许创建、修改、删除、重命名user的权限
    Create view 代表允许创建视图的权限
    Delete 允许执行delete操作
    Drop 代表允许删除数据库、表、视图的权限,包括truncate table命令
    Event 代表允许查询,创建,修改,删除MySQL事件
    Execute 代表允许执行存储过程和函数的权限
    File 代表允许在MySQL可以访问的目录进行读写磁盘文件操作,可使用的命令包括load data infile,select … into outfile,load file()函数
    Grant option 代表是否允许此用户授权或者收回给其他用户你给予的权限,重新付给管理员的时候需要加上这个权限
    Index 代表是否允许创建和删除索引
    Insert 代表是否允许在表里插入数据,同时在执行analyze table,optimize table,repair table语句的时候也需要insert权限
    Lock tables 代表允许对拥有select权限的表进行锁定,以防止其他链接对此表的读或写
    Process 代表允许查看MySQL中的进程信息,比如执行show processlist, mysqladmin processlist, show engine等命令
    Reference 是在5.7.6版本之后引入,代表是否允许创建外键
    Reload 代表允许执行flush命令,指明重新加载权限表到系统内存中,refresh命令代表关闭和重新开启日志文件并刷新所有的表
    lication client 代表允许执行show master status,show slave status,show binary logs命令
    Replication slave 代表允许slave主机通过此用户连接master以便建立主从复制关系
    Select 允许执行select操作
    Show databases 代表允许执行show databases命令查看所有的数据库名
    Show view 代表允许执行show create view命令查看视图创建的语句
    Shutdown 代表允许关闭数据库实例,执行语句包括mysqladmin shutdown
    Super 代表允许执行一系列数据库管理命令,包括kill强制关闭某个连接命令, change master to创建复制关系命令,以及create/alter/drop server等命令
    Trigger 代表允许创建,删除,执行,显示触发器的权限
    Update 允许执行update操作
    Usage 是创建一个用户之后的默认权限,其本身代表连接登录权限。使用create user语句创建的用户,默认就拥有这个usage权限,但是除了能登录之外,
  • 授权

    1
    2
    3
    4
    GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password']

    -- *.* 表示所有库的所有表
    -- 库名.表名 表示某库下面的某表
  • 撤销权限

    1
    2
    3
    REVOKE 权限列表 ON 表名 FROM 用户名

    REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名 -- 撤销所有权限

7.2 MySQL 备份

  • MySQL 数据库备份方法

    • 备份工具,如 mysqldump, …
    • 数据库管理工具
    • 直接拷贝数据库文件和相关配置文件
  • mysqldump 使用

    • 导出

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      -- 导出一张表
      mysqldump -u用户名 -p密码 库名 表名 > 文件名(一般为.sql后缀)

      -- 导出多张表
      mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名

      -- 导出所有表
      mysqldump -u用户名 -p密码 库名 > 文件名

      -- 导出一个库
      mysqldump -u用户名 -p密码 -B 库名 > 文件名
    • 导入

      1
      2
      3
      4
      5
      -- 在登录mysql的情况下
      source 备份文件

      -- 在不登录的情况下
      mysql -u用户名 -p密码 库名 < 备份文件

8. 数据库规范化

范式

9. JDBC

9.1 数据库驱动

  • 数据库驱动就是直接操作数据库的一个程序
  • 不同数据库产品的数据库驱动名字有差异
  • 在程序中需要依赖数据库驱动来完成对数据库的操作

9.2 JDBC

SUN 公司为了简化、统一对数据库的操作,定义了一套 Java 操作数据库的规范(接口),称之为 JDBC。

这套接口由数据库厂商去实现,这样,开发人员只需要学习 jdbc 接口,并通过 jdbc 加载具体的驱动,就可以操作数据库。

JDBC 全称为:Java Database Connectivity(java 数据库连接),它主要由接口组成。

组成 JDBC 的 2 个包:java.sql、javax.sql

开发 JDBC 应用需要以上 2 个包的支持外,还需要导入相应 JDBC 的数据库实现(即数据库驱动)。

9.3 编写 JDBC 程序

  1. 导入 JDBC 驱动

    驱动下载地址:Maven Repository: com.mysql » mysql-connector-j

  2. 编写程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package pers.afelixliu.study.db;

    import java.sql.*;


    public class DBConnect {

    public static void main(String[] args) {
    // 0. 准备数据库连接信息
    String url = "jdbc:mysql://localhost:3306/tmp?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
    String username = "root";
    String password = "1019";

    try {
    // 1. 加载数据库驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    // 2. 连接数据库
    Connection connect = DriverManager.getConnection(url, username, password);

    // 3. 创建操作对象
    Statement statement = connect.createStatement();

    // 4. 执行SQL语句
    ResultSet result = statement.executeQuery("select * from book");

    // 5. 处理执行结果
    while (result.next()) {
    String name = result.getString("name");
    String author = result.getString("author");
    double price = result.getDouble("price");
    System.out.println("Name: " + name + ", Author: " + author + ", Price: " + price);
    }

    // 6. 释放资源
    result.close();
    statement.close();
    connect.close();
    } catch (ClassNotFoundException | SQLException e) {
    e.printStackTrace();
    }
    }
    }

    详细解释:

    • DriverManager 类用于注册驱动,并创建与数据库的连接

    • Connection 类用于代表数据库的连接,客户端与数据库所有交互都是通过 Connection 对象完成的,常用方法如下:

      • createStatement():创建向数据库发送 sql 的 statement 对象

      • prepareStatement(sql):创建向数据库发送预编译 sql 的 PrepareSatement 对象

      • setAutoCommit(boolean autoCommit):设置事务是否自动提交

      • commit():在连接上提交事务

      • rollback():在此连接上回滚事务


    • Statement 类用于向数据库发送 SQL 语句,常用方法如下:

      • executeQuery(String sql):用于向数据发送查询语句

      • executeUpdate(String sql):用于向数据库发送 insert、update 或 delete 语句

      • execute(String sql):用于向数据库发送任意 sql 语句

      • addBatch(String sql):把多条 sql 语句放到一个批处理中

      • executeBatch():向数据库发送一批 sql 语句执行


    • ResultSet 类用于代表 sql 语句的执行结果。

      ResultSet 封装执行结果时,采用的类似于表格的方式:ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用 ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。

      ResultSet 对象提供的都是用于获取数据的get方法:

      • 获取任意类型的数据

        • getObject(int index)

        • getObject(string columnName)

      • 获取指定类型的数据,例如:

        • getString(int index)

        • getString(String columnName)

      ResultSet 还提供了对结果集进行滚动的方法:

      • next():移动到下一行

      • Previous():移动到前一行

      • absolute(int row):移动到指定行

      • beforeFirst():移动 resultSet 的最前面。

      • afterLast() :移动到resultSet的最后面。


    为确保资源释放代码能运行,资源释放代码一定要放在 finally 语句块中。

9.4 Connection 类处理事务

当 JDBC 程序向数据库获得一个 Connection 对象时,默认情况下这个 Connection 对象会自动向数据库提交在它上面发送的 SQL 语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列的 JDBC 控制事务语句:

1
2
3
4
5
6
7
8
// 关闭事务自动提交(开启事务)
Connection.setAutoCommit(false);

// 回滚事务
Connection.rollback();

// 提交事务
Connection.commit();

9.5 详解 Statement 类

JDBC 中的 Statement 对象用于向数据库发送 SQL 语句,想完成对数据库的增删改查,只需要通过这个对象向数据库发送增删改查语句即可。

  • Statement 类的 executeUpdate() 方法,用于向数据库发送增、删、改的 sql 语句,executeUpdate() 执行完后,将会返回一个整数(即增删改语句导致了数据库几行数据发生了变化)

  • Statement 类的 executeQuery() 方法用于向数据库发送查询语句,executeQuery() 方法返回代表查询结果的ResultSet对象

9.6 自定义 JDBC 工具类

第一步、在 src 目录下创建 db.properties 文件

1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/tmp?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=1019

第二步、在 src 目录下创建 utils 文件夹,并在 utils 文件夹中创建 JDBCUtils.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package utils;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class JDBCUtils {

private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;

static {

try {
InputStream resource = JDBCUtils.class.getClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
properties.load(resource);
driver = properties.getProperty("driver");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");

// 加载数据库驱动
Class.forName(driver);
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}

/**
* 连接数据库
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}


/**
* 释放资源
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

9.7 SQL 注入问题

SQL 注入就是攻击者通过在输入数据中注入恶意的 SQL 语句,利用应用程序对用户输入的不当处理,欺骗数据库服务器执行恶意的 SQL 指令,从而达到获取、篡改或破坏数据库中数据的目的。

举例:

比如一个登录表单,攻击者在用户名字段输入 admin' --,如果后端 SQL 语句是:

1
SELECT * FROM users WHERE username = '[用户输入]' AND password = '[用户输入]

则实际执行的 SQL 语句变成了:

1
SELECT * FROM users WHERE username = 'admin' --' AND password = '[用户输入]'

这里 -- 后面的内容被注释掉,因此 SQL 语句实际上忽略了密码检查。

9.8 PreparedStatement 类

PreparedStatement 类是 Statement 类的子类,它的实例对象可以通过调用 Connection.preparedStatement() 方法获得。

PreparedStatement 类的优点:

  • 相对于 Statement 类而言,PreparedStatement 类可以避免 SQL 注入的问题。

  • Statement 类会使数据库频繁编译 SQL,可能造成数据库缓冲区溢出;PreparedStatement 类可对 SQL 进行预编译,从而提高数据库的执行效率。

  • PreparedStatement 类对于 sql 中的参数,允许使用占位符的形式进行替换,简化 sql 语句的编写。

使用举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package pers.afelixliu.study.db;

import utils.JDBCUtils;
import java.util.Date;

import java.sql.*;

public class PreparedStatementTest {

public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

String preSQL = "insert into book(`name`, `author`, `price`, `create_time`) values(?, ?, ?, ?)";

try {
conn = JDBCUtils.getConnection();

// 预编译
pstmt = conn.prepareStatement(preSQL);

/*
为SQL语句中的参数赋值
注意:索引是从1开始的
*/
pstmt.setString(1, "文城");
pstmt.setString(2, "余华");
pstmt.setDouble(3, 59.00);
// // 只能设定日期
// pstmt.setDate(4, new java.sql.Date(new java.util.Date().getTime()));
// // 只能设定日期
// pstmt.setDate(4, new java.sql.Date(System.currentTimeMillis()));
// // 设定日期和时间
// pstmt.setTimestamp(4, new Timestamp(new java.util.Date().getTime()));
// // 设定日期和时间
// pstmt.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
// 设定日期和时间(Java 8 新特性,推荐使用)
pstmt.setTimestamp(4, Timestamp.valueOf(LocalDateTime.now()));
int i = pstmt.executeUpdate();
if (i > 0) {
System.out.println("insert success");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn, pstmt, rs);
}

}
}

setDate() 方法的注意事项:

  • setDate() 方法接受的是 java.sql.Date 对象,而不是 java.util.Date 对象。如果需要使用 java.util.Date,可以通过将其转换为 java.sql.Date 对象来实现,例如:

    1
    pstmt.setDate(4, new java.sql.Date(new java.util.Date().getTime()));
  • setDate() 方法设置的日期只包含日期部分,不包含时间部分,默认的时间部分将会被设置为 00:00:00

  • 如果需要同时设置日期和时间,可以使用 setTimestamp() 方法,setTimestamp() 方法可以有效地将 java.sql.Timestamp 对象插入到数据库的 datetimetimestamp 字段中。

PreparedStatement 类防止 SQL 注入的原因:

SQL执行的时候把整个参数用引号包起来,并把参数中的引号作为转义字符,从而避免了参数也作为条件的一部分。

9.9 数据库连接池

9.9.1 连接池基础
  • 什么是数据库连接池?

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

  • 为什么要使用连接池?

数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现的尤为突出。一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,用完就关闭连接,这样造成系统的性能低下。

数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并将这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库连接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。

连接池技术尽可能多地重用了消耗内存的资源,大大节省了内存,提高了服务器的服务效率,能够支持更多的客户服务。

通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:

  1. 最小连接数是连接池一直保持的数据库连接,但如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费。

  2. 最大连接数是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作。

  3. 如果最小连接数与最大连接数相差很大,那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。

编写连接池需实现 java.sql.DataSource 接口。

  • 开源数据库连接池

目前很多 WEB 服务器(Weblogic, WebSphere, Tomcat)都提供了 DataSoruce 接口的实现,即连接池的实现。

通常我们把 DataSource 的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。

有一些开源组织提供了数据源的独立实现:

  • DBCP / DPCP2 数据库连接池
  • C3P0 数据库连接池

在使用了数据库连接池之后,在项目的实际开发中就不需要编写连接数据库的代码了,直接从数据源获得数据库的连接。

9.9.2 使用 DBCP2 数据源

第一步、加载以下依赖库

  • commons-logging-版本号.jar
  • commons-pool2-版本号.jar
  • commons-dbcp2-版本号.jar

第二步、在 src 目录下创建 dbcp.properties 文件

1
2
3
4
5
6
7
8
9
10
11
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/tmp?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username=root
password=1019

initialSize=10
maxActive=50
maxIdle=20
minIdle=5
maxWait=60000
defaultAutoCommit=true

第三步、在 src 目录下创建 utils 文件夹,并在 utils 文件夹中创建 DBCPUtils.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package utils;

import org.apache.commons.dbcp2.BasicDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class DBCPUtils {

private static DataSource ds = null;

static {

try {
// 加载配置文件
InputStream resource = DBCPUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
Properties properties = new Properties();
properties.load(resource);

// 创建数据源
ds = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

/**
* 连接数据库
*/
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}


/**
* 释放资源
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
9.9.3 使用 C3P0 数据源

C3P0 与 DBCP 区别:DBCP 没有自动回收空闲连接的功能;C3P0 有自动回收空闲连接功能。

第一步、加载以下依赖库

  • c3p0-版本号.jar
  • mchange-commons-java-版本号.jar

第二步、在 src 目录下创建 c3p0-config.xml 文件

注意事项:

  • 配置文件名只能是 c3p0-config.xml

  • 在 xml 文件中,‘&’ 符号需要进行转义,使用 ‘&amp;’ 来代替 ‘&’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>

<c3p0-config>
<!-- C3P0的缺省(默认)配置
如果在代码中 “ComboPooledDataSource ds = new ComboPooledDataSource();” 这样写就表示使用的是C3P0的缺省(默认)配置信息来创建数据源
-->
<default-config>
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<!-- 在 xml 文件中,'&' 符号需要进行转义,使用 '&amp;' 来代替 '&' -->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/tmp?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai</property>
<property name="user">root</property>
<property name="password">1019</property>
<!-- 当连接池中的连接耗尽的时候 c3p0 一次同时获取的连接数 -->
<property name="acquireIncrement">5</property>
<!-- 初始化连接数,取值应在 minPoolSize 与 maxPoolSize 之间 -->
<property name="initialPoolSize">10</property>
<!-- 设置最小连接数,当连接池还有 5 个的时候,就开始申请连接数-->
<property name="minPoolSize">5</property>
<!-- 设置最大连接数,最多只能有 20 个连接-->
<property name="maxPoolSize">20</property>
<!-- 定义在从数据库获取新连接失败后重复尝试的次数 -->
<property name="acquireRetryAttempts">30</property>
<!-- 两次连接中间隔时间,单位毫秒 -->
<property name="acquireRetryDelay">1000</property>
<!-- 连接关闭时默认将所有未提交的操作回滚 -->
<property name="autoCommitOnClose">false</property>
<!-- 每 60 秒检查所有连接池中的空闲连接 -->
<property name="idleConnectionTestPeriod">0</property>
<!-- 连接超时时间,如果是0,表示无限等待 -->
<property name="checkoutTimeout">0</property>
</default-config>

<!-- C3P0的自定义配置
如果在代码中 “ComboPooledDataSource ds = new ComboPooledDataSource("MySQL");” 这样写就表示使用的是name是MySQL的配置信息来创建数据源
-->
<named-config name="MySQL">
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/tmp?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai</property>
<property name="user">root</property>
<property name="password">1019</property>
<!-- 当连接池中的连接耗尽的时候 c3p0 一次同时获取的连接数 -->
<property name="acquireIncrement">5</property>
<!-- 初始化连接数,取值应在 minPoolSize 与 maxPoolSize 之间 -->
<property name="initialPoolSize">10</property>
<!-- 设置最小连接数,当连接池还有 5 个的时候,就开始申请连接数-->
<property name="minPoolSize">5</property>
<!-- 设置最大连接数,最多只能有 20 个连接-->
<property name="maxPoolSize">20</property>
<!-- 定义在从数据库获取新连接失败后重复尝试的次数 -->
<property name="acquireRetryAttempts">30</property>
<!-- 两次连接中间隔时间,单位毫秒 -->
<property name="acquireRetryDelay">1000</property>
<!-- 连接关闭时默认将所有未提交的操作回滚 -->
<property name="autoCommitOnClose">false</property>
<!-- 每 60 秒检查所有连接池中的空闲连接 -->
<property name="idleConnectionTestPeriod">0</property>
<!-- 连接超时时间,如果是0,表示无限等待 -->
<property name="checkoutTimeout">0</property>
</named-config>
</c3p0-config>

第三步、在 src 目录下创建 utils 文件夹,并在 utils 文件夹中创建 DBCPUtils.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class C3P0Utils {

private static ComboPooledDataSource ds = null;

static {

try {
// 使用 C3P0 的默认配置来创建数据源
//ds = new ComboPooledDataSource();

// 使用 C3P0 的自定义配置来创建数据源
ds = new ComboPooledDataSource("MySQL");
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

/**
* 连接数据库
*/
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}


/**
* 释放资源
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}