🔥一个人走得远了,就会忘记自己为了什么而出发,希望你可以不忘初心,不要随波逐流,一直走下去🎶
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🦄 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:👉JavaWeb从入门到实战
🌠 首发时间:2022年8月24日
✅ 如果觉得博主的文章还不错的话,希望小伙伴们三连支持一下哦
阅读指南
一、JDBC学习目标
- 掌握 JDBC 的的 CRUD
- 理解 JDBC 中各个对象的作用
- 掌握 Druid 的使用
二、JDBC概述
在开发中,我们使用的是 Java 语言,那么势必要通过 Java 语言操作数据库中的数据,这就是我们接下来要学习的 JDBC
1 - JDBC的概念
JDBC (Java DataBase Connectivity),中文为 Java 数据库连接,就是使用 Java 语言操作关系型数据库的一套 API
我们开发的同一套 Java 代码是无法操作不同的关系型数据库,因为每一个关系型数据库的底层实现细节都不一样。如果这样,问题就很大了,在公司中可以在开发阶段使用的是 MySQL 数据库,而上线时公司最终选用 Oracle 数据库,我们就需要对代码进行大批量修改,这显然并不是我们想看到的
我们要做到的是同一套 Java 代码操作不同的关系型数据库,所以 sun 公司就指定了一套标准接口(JDBC),JDBC 中定义了所有操作关系型数据库的规则。众所周知,接口是无法直接使用的,我们需要使用接口的实现类,而这套实现类(称之为:驱动)就由各自的数据库厂商给出
2 - JDBC的本质
- 官方(sun公司)定义的一套操作所有关系型数据库的规则,即接口
- 各个数据库厂商去实现这套接口,提供数据库驱动jar包
- 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类
3 - JDBC的优点
- 各数据库厂商使用相同的接口,Java 代码不需要针对不同数据库分别开发
- 可随时替换底层数据库,访问数据库的 Java 代码基本不变
以后编写操作数据库的代码只需要面向 JDBC(接口),操作哪个关系型数据库就需要导入该数据库的驱动包,如需要操作 MySQL 数据库,就需要再项目中导入 MySQL 数据库的驱动包
下图所示就是 MySQL 的驱动包:
三、JDBC快速上手
我们先来看看通过 Java 操作数据库的流程
第一步:编写 Java 代码
第二步:Java 代码将 SQL 发送到 MySQL 服务端
第三步:MySQL 服务端接收到 SQL 语句并执行该 SQL 语句
第四步:将 SQL 语句执行的结果返回给 Java 代码
1 - 编写代码步骤
创建工程,导入驱动 jar 包
包的获取:https://pan.baidu.com/s/1HVOir_iYz8ml1-HtWwqBcA?pwd=667n
注册驱动
Class.forName("com.mysql.jdbc.Driver");
获取连接
Connection conn = DriverManager.getConnection(url, username, password);
我们的 Java 代码想要发送 SQL 给 MySQL 服务端,就需要先建立连接
定义SQL语句
String sql = "update…" ;
获取执行SQL对象
执行 SQL 语句需要 SQL 执行对象,而这个执行对象就是 Statement 对象
Statement stmt = conn.createStatement();
执行SQL
stmt.executeUpdate(sql);
处理返回结果
释放资源
2 - 具体操作
创建一个空项目
定义项目的名称,并指定位置
对项目进行设置,JDK版本、编译版本
创建模块,指定模块的名称及位置
导入驱动包
将 mysql 的驱动包放在模块下自己创建的一个 lib 目录(随意命名)下,并将该 jar 包添加为库文件
在添加为库文件的时候,有如下三个选项
- Global Library : 全局有效
- Project Library : 项目有效
- Module Library : 模块有效
在src下创建一个类 JDBC_Demo
编写代码如下
import java.sql.*; public class JDBC_Demo { public static void main(String[] args) throws Exception { //1. 注册驱动 Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接 String url = "jdbc:mysql://127.0.0.1:3306/db1"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "update account set money = 2000 where id = 1"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//受影响的行数 //6. 处理结果 System.out.println(count); //7. 释放资源 stmt.close(); conn.close(); } }
3 - JDBC API详解
(1) DriverManager
DriverManager(驱动管理类)作用:
注册驱动
在使用手册中查询该类,我们可以看到有一个 registerDriver 方法:
registerDriver 方法是用于注册驱动的,但是我们之前写的入门案例并不是这样写的,而是通过下面的语句来实现
Class.forName("com.mysql.jdbc.Driver");
查询一下 MySQL 提供的 Driver 类,源码如下:
在该类中的静态代码块中已经执行了
DriverManager
对象的registerDriver()
方法进行驱动的注册了,那么我们只需要加载Driver
类,该静态代码块就会执行,而Class.forName("com.mysql.jdbc.Driver");
就可以加载Driver
类提示:
- MySQL 5之后的驱动包,可以省略注册驱动的步骤
- 自动加载jar包中META-INF/services/java.sql.Driver文件中的驱动类
获取数据库连接
参数说明:
url : 连接路径
语法:jdbc:mysql://ip地址(域名):端口号/数据库名称?参数键值对1&参数键值对2…
示例:jdbc:mysql://127.0.0.1:3306/db1
细节:
- 如果连接的是本机 mysql 服务器,并且 mysql 服务默认端口是 3306,则url可以简写为:jdbc:mysql://数据库名称?参数键值对
- 配置 useSSL=false 参数,禁用安全连接方式,解决警告提示
user :用户名
poassword :密码
(2) Connection
Connection(数据库连接对象)作用:
- 获取执行 SQL 的对象
- 管理事务
获取执行对象
普通执行 SQL 对象
Statement createStatement()
入门案例中就是通过该方法获取的执行对象
预编译 SQL 的执行 SQL 对象:防止 SQL 注入
PreparedStatement prepareStatement(sql)
通过这种方式获取的
PreparedStatement
SQL 语句执行对象是我们一会重点要进行讲解的,它可以防止 SQL 注入执行存储过程的对象
CallableStatement prepareCall(sql)
通过这种方式获取的
CallableStatement
执行对象是用来执行存储过程的,而存储过程在 MySQL 中不常用,所以这个我们不进行讲解
事务管理
我们先回顾一下 MySQL 事务管理的操作:
- 开启事务 :
BEGIN;
或者START TRANSACTION;
- 提交事务 :
COMMIT;
- 回滚事务 :
ROLLBACK;
MySQL默认是自动提交事务
接下来我们学习 JDBC 事务管理的方法
Connection 接口中定义了 3 个对应的方法:
开启事务
参与 autoCommit 表示是否自动提交事务,true 表示自动提交事务,false 表示手动提交事务,而开启事务需要将该参数设置为 false
提交事务
回滚事务
具体代码实现如下:
import java.sql.*;
public class JDBCDemo_Connection {
public static void main(String[] args) throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql1 = "update account set money = 3000 where id = 1";
String sql2 = "update account set money = 3000 where id = 2";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
try {
// ============开启事务==========
conn.setAutoCommit(false);
//5. 执行sql
int count1 = stmt.executeUpdate(sql1);//受影响的行数
//6. 处理结果
System.out.println(count1);
//5. 执行sql
int count2 = stmt.executeUpdate(sql2);//受影响的行数
//6. 处理结果
System.out.println(count2);
// ============提交事务==========
//程序运行到此处,说明没有出现任何问题,则需求提交事务
conn.commit();
} catch (Exception e) {
// ============回滚事务==========
//程序在出现异常时会执行到这个地方,此时就需要回滚事务
conn.rollback();
e.printStackTrace();
}
//7. 释放资源
stmt.close();
conn.close();
}
}
(3) Statement
概述
Statement 对象的作用就是用来执行 SQL 语句,而针对不同类型的 SQL 语句使用的方法也不一样
执行DDL、DML语句
执行DQL语句
该方法涉及到了
ResultSet
对象,而这个对象我们还没有学习,一会再重点讲解
代码实现
执行DML语句
public void testDML() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "update account set money = 3000 where id = 1"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//执行完DML语句,受影响的行数 //6. 处理结果 //System.out.println(count); if(count > 0){ System.out.println("修改成功~"); }else{ System.out.println("修改失败~"); } //7. 释放资源 stmt.close(); conn.close(); }
执行DDL语句
public void testDDL() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "drop database db2"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//执行完DDL语句,可能是0 //6. 处理结果 System.out.println(count); //7. 释放资源 stmt.close(); conn.close(); }
注意:以后开发很少使用 Java 代码操作 DDL 语句
(4) ResultSet
概述
ResultSet(结果集对象)作用:
- 封装了SQL查询语句的结果
而执行了 DQL 语句后就会返回该对象,对应执行 DQL 语句的方法如下:
ResultSet executeQuery(sql):执行DQL 语句,返回 ResultSet 对象
那么我们就需要从 ResultSet
对象中获取我们想要的数据,ResultSet
对象提供了操作查询结果数据的方法,如下:
boolean next()
- 将光标从当前位置向前移动一行
- 判断当前行是否为有效行
方法返回值说明:
- true : 有效行,当前行有数据
- false : 无效行,当前行没有数据
xxx getXxx(参数):获取数据
- xxx : 数据类型;如: int getInt(参数) ;String getString(参数)
- 参数
- int类型的参数:列的编号,从1开始
- String类型的参数: 列的名称
如下图为执行 SQL 语句后的结果
一开始光标指定于第一行前,如图所示红色箭头指向于表头行。当我们调用了 next()
方法后,光标就下移到第一行数据,并且方法返回 true,此时就可以通过 getInt("id")
获取当前行 id 字段的值,也可以通过 getString("name")
获取当前行 name 字段的值。如果想获取下一行的数据,继续调用 next()
方法,以此类推
代码实现
public void testResultSet() throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "select * from account";
//4. 获取statement对象
Statement stmt = conn.createStatement();
//5. 执行sql
ResultSet rs = stmt.executeQuery(sql);
//6. 处理结果, 遍历rs中的所有数据
// 6.1 光标向下移动一行,并且判断当前行是否有数据
while (rs.next()){
//6.2 获取数据 getXxx()
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
System.out.println(id);
System.out.println(name);
System.out.println(money);
System.out.println("--------------");
}
//7. 释放资源
rs.close();
stmt.close();
conn.close();
}
(5) 案例
- 需求:查询 account 账户表数据,封装为 Account 对象中,并且存储到 ArrayList 集合中
代码实现
创建一个 Account 类来存储信息,便于存进集合
package pojo; public class Account { private int id; private String name; private double money; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } }
import java.sql.*; import java.util.*; import pojo.Account; public class JDBCDemo_ResultSet { /* * 查询account账户表数据,封装为Account对象中,并且存储到ArrayList集合中 * 1. 定义实体类Account * 2. 查询数据,封装到Account对象中 * 3. 将Account对象存入ArrayList集合中 */ public static void testResultSet2() throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "select * from account"; //4. 获取statement对象 Statement stmt = conn.createStatement(); //5. 执行sql ResultSet rs = stmt.executeQuery(sql); //6.处理结果,遍历rs中的所有数据 // 6.1 光标向下移动一行,并且判断当前行是否有数据 // 创建集合 List<Account> list = new ArrayList<>(); while (rs.next()){ Account account = new Account(); //6.2 获取数据 getXxx() int id = rs.getInt("id"); String name = rs.getString("name"); double money = rs.getDouble("money"); //赋值 account.setId(id); account.setName(name); account.setMoney(money); // 存入集合 list.add(account); } for(Account a:list){ System.out.println(a.getId()); System.out.println(a.getName()); System.out.println(a.getMoney()); } //7. 释放资源 rs.close(); stmt.close(); conn.close(); } public static void main(String[] args) { try { testResultSet2(); } catch (Exception e) { e.printStackTrace(); } } }
执行结果如下:
(6) PreparedStatement
作用:预编译 SQL 语句并执行,预防 SQL 注入问题
对上面的作用中 SQL 注入问题你可能不理解,接下来我们先对 SQL 注入进行说明
① 什么是SQL注入
SQL注入是通过操作输入来修改事先定义好的SQL语句,用以达到执行代码对服务器进行攻击的方法
② 代码模拟SQL注入问题
首先准备数据:
CREATE TABLE tb_user( id int PRIMARY KEY auto_increment, username varchar(10), password int(10) ); -- 添加数据 INSERT INTO tb_user(username,password) VALUES('zhangsan',123),('lisi',123); SELECT * FROM tb_user;
看一下这段代码
public void testLogin() throws Exception { //获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 String url = "jdbc:mysql:///db1?useSSL=false"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); // 接收用户输入 用户名和密码 String name = "sjdljfld"; String pwd = "' or '1' = '1"; String sql = "select * from tb_user where username = '"+name+"' and password = '"+pwd+"'"; // 获取stmt对象 Statement stmt = conn.createStatement(); // 执行sql ResultSet rs = stmt.executeQuery(sql); // 判断登录是否成功 if(rs.next()){ System.out.println("登录成功~"); }else{ System.out.println("登录失败~"); } //释放资源 rs.close(); stmt.close(); conn.close(); }
上面代码是将用户名和密码拼接到 sql 语句中,拼接后的 sql 语句如下
select * from tb_user where username = 'sjdljfld' and password = ''or '1' = '1'
从上面语句可以看出条件
username = 'sjdljfld' and password = ''
不管是否满足,而or
后面的'1' = '1'
是始终满足的,最终条件是成立的,就可以正常的进行登陆了。如果此时有一个登录系统是这样写的,那么很容易就被人入侵
接下来我们来学习 PreparedStatement 对象
③ PreparedStatement概述
获取 PreparedStatement 对象
// SQL语句中的参数值,使用?占位符替代 String sql = "select * from user where username = ? and password = ?"; // 通过Connection对象获取,并传入对应的sql语句 PreparedStatement pstmt = conn.prepareStatement(sql);
设置参数值
上面的 sql 语句中参数使用 ? 进行占位,在之前肯定要设置这些 ? 的值
PreparedStatement对象:setXxx(参数1,参数2):给 ? 赋值
- Xxx:数据类型 ; 如 setInt (参数1,参数2)
- 参数:
- 参数1: ?的位置编号,从1 开始
- 参数2: ?的值
执行SQL语句
executeUpdate():执行DDL语句和DML语句
executeQuery():执行DQL语句
注意:
- 调用这两个方法时不需要传递SQL语句,因为获取SQL语句执行对象时已经对SQL语句进行预编译了。
④ 使用PreparedStatement改进
import java.sql.*;
public class JDBC_PreparedStatement {
public static void testPreparedStatement() throws Exception {
//获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
// 接收用户输入 用户名和密码
String name = "zhangsan";
String pwd = "' or '1' = '1";
// 定义sql
String sql = "select * from tb_user where username = ? and password = ?";
// 获取pstmt对象
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置?的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);
// 执行sql
ResultSet rs = pstmt.executeQuery();
// 判断登录是否成功
if(rs.next()){
System.out.println("登录成功~");
}else{
System.out.println("登录失败~");
}
//释放资源
rs.close();
pstmt.close();
conn.close();
}
public static void main(String[] args) throws Exception {
testPreparedStatement();
}
}
执行结果如下:
此时以无法登录成功,说明执行上面语句就不会出现 SQL 注入漏洞问题了
那么 PreparedStatement 又是如何解决的呢?
其实它是将特殊字符进行了转义,转义的 SQL 如下:
select * from tb_user where username = 'sjdljfld' and password = '\'or \'1\' = \'1'
⑤ PreparedStatement原理
PreparedStatement 好处:
- 预编译SQL,性能更高
- 防止SQL注入:将敏感字符进行转义
Java 代码操作数据库流程如上图所示:
将 sql 语句发送到 MySQL 服务器端
MySQL 服务端会对 sql 语句进行如下操作
检查 SQL 语句
检查 SQL 语句的语法是否正确
编译 SQL 语句,将 SQL 语句编译成可执行的函数
检查 SQL 和编译 SQL 花费的时间比执行 SQL 的时间还要长。如果我们只是重新设置参数,那么检查 SQL 语句和编译 SQL 语句将不需要重复执行,这样就提高了性能
执行 SQL 语句
接下来我们通过查询日志来看一下原理
开启预编译功能
在代码中编写 url 时需要加上以下参数,而我们之前根本就没有开启预编译功能,只是解决了 SQL 注入漏洞
useServerPrepStmts=true
配置MySQL执行日志(重启mysql服务后生效)
在 mysql 配置文件 (my.ini) 中添加如下配置
log-output=FILE general-log=1 general_log_file="D:\mysql.log" slow-query-log=1 slow_query_log_file="D:\mysql_slow.log" long_query_time=2
Java测试代码如下
public void testPreparedStatement2() throws Exception { //获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写 // useServerPrepStmts=true 参数开启预编译功能 String url = "jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); // 接收用户输入 用户名和密码 String name = "zhangsan"; String pwd = "' or '1' = '1"; // 定义sql String sql = "select * from tb_user where username = ? and password = ?"; // 获取pstmt对象 PreparedStatement pstmt = conn.prepareStatement(sql); // 设置?的值 pstmt.setString(1,name); pstmt.setString(2,pwd); ResultSet rs = null; // 执行sql rs = pstmt.executeQuery(); // 设置?的值 pstmt.setString(1,"aaa"); pstmt.setString(2,"bbb"); // 执行sql rs = pstmt.executeQuery(); // 判断登录是否成功 if(rs.next()){ System.out.println("登录成功~"); }else{ System.out.println("登录失败~"); } //释放资源 rs.close(); pstmt.close(); conn.close(); }
执行SQL语句,查看
D:\mysql.log
日志如下上图中第三行中的
Prepare
是对 SQL 语句进行预编译,第四行和第五行是执行了两次 SQL 语句,而第二次执行前并没有对 SQL 进行预编译
小结:
- 在获取 PreparedStatement 对象时,将sql语句发送给mysql服务器进行检查,编译(这些步骤很耗时)
- 执行时就不用再进行这些步骤了,速度更快
- 如果sql模板一样,则只需要进行一次检查、编译
🧸 这次的分享就到这里啦,继续加油哦^^
🐱 我是程序喵,陪你一点点进步
🍭 有出错的地方欢迎在评论区指出来,共同进步,谢谢啦