JDBC学习记录2 - API详解
2024年7月9日18:08:13
上接JDBC学习记录1-快速入门
三、JDBC API详解
1. DriverManager
DriverManager(驱动管理类)两个作用:
- 注册驱动
- 获取数据库连接
其中有两个静态方法十分重要:getConnection()
、registerDriver()
其中registerDriver()
在Driver的一个静态代码块里,所以真正注册驱动的是registerDriver()
,在驱动类被加载进内存时候这个静态方法就自动执行了,完成了驱动的注册。
前面是MySQL协议jdbc:mysql
,后面跟着的是IP地址(127.0.0.1也可以写为localhost),接着是端口号、数据库名称,最后是参数列表?参数1&参数2&...
。
2. Connection
**Connection(数据库 连接/会话 对象)**的作用:
-
获取执行SQL的对象(会在后面重点学习)
-
管理事务
复制第一个Demo类,来测试事务管理的功能,若现在有两个SQL原子性语句需要并发执行,要么同时执行成功要么同时失败,那么我们需要开启事务、提交事务。若出现异常,我们需要回滚事务,利用Java的
try/catch
语句来处理异常,快捷键Ctrl+Alt+T
将代码段使用try/catch
语句包围起来。要注意的是同一个Statement对象能执行两个Update语句,而不能执行两个Query语句,需要有两个Statement对象。
思考:在一个事务里的两个查询语句的ResultSet对象的声明位置和内存释放问题
若是在try/catch语句块外声明,不能保证正确回滚,若在tyr语句块里面声明,那么释放也放在try语句块里又是否不妥?
3. Statement
只有一个功能:执行SQL语句。
有两个方法需要重点学习:
***DDL(Data Definition Language)***语句:
数据定义语言
,主要是进行定义/改变表的结构、数据类型、表之间的链接等操作。常用的语句关键字有 CREATE、DROP、ALTER 等。***DML(Data Manipulation Language)***语句:
数据操纵语言
,主要是对数据进行增加、删除、修改操作。常用的语句关键字有 INSERT、UPDATE、DELETE 等。***DQL(Data Query Language)***语句:
数据查询语言
,主要是对数据进行查询操作。常用关键字有 SELECT、FROM、WHERE 等。***DCL(Data Control Language)***语句:
数据控制语言
,主要是用来设置/更改数据库用户权限。常用关键字有 GRANT、REVOKE 等。
一般人员很少用到DCL语句。
-
int executeUpdate(sql);
执行DDL、DML语句 -
ResultSet executeQuery(sql);
执行DQL语句 -
boolean execute(sql);
可执行任意语句,返回的bool值表示是否返回ResultSet对象
创建一个新类Demo3_Statement用以测试,
package com.niko.jdbc;
import org.junit.Test;
import java.sql.*;
/*
* JDBC API 详解 Statement
* */
public class JDBCDemo3_Statement {
/*
* 执行DML语句
* */
@Test
public void testDML() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
String url="jdbc:mysql://localhost:3306/db?useSSL=false";
String username="root";
String password="1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3.定义sql
String sql = "update houselist set price=1400 where houseid =17";
//4.获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5.执行sql
int count =stmt.executeUpdate(sql); //返回值是受影响的行数
//ResultSet rs = stmt.executeQuery(sql);
//6.处理结果
if(count>0){
System.out.println("success");
}else {
System.out.println("fail");
}
//System.out.println(count);
// while (rs.next()) {
// int id = rs.getInt("houseid");
// String address = rs.getString("address");
// System.out.println("ID: " + id + ", Address: " + address);
// }
//7.释放资源
stmt.close();
conn.close();
}
/*
* 执行DDL语句
* */
@Test
public void testDDL() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
String url="jdbc:mysql://localhost:3306/db?useSSL=false";
String username="root";
String password="1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3.定义sql
String sql = "CREATE database db2";
//4.获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5.执行sql
int count =stmt.executeUpdate(sql); //执行完DDL语句,成功后返回值也可能是0
//ResultSet rs = stmt.executeQuery(sql);
//6.处理结果
// if(count>0){
// System.out.println("success");
// }else {
// System.out.println("fail");
// }
System.out.println(count);
// while (rs.next()) {
// int id = rs.getInt("houseid");
// String address = rs.getString("address");
// System.out.println("ID: " + id + ", Address: " + address);
// }
//7.释放资源
stmt.close();
conn.close();
}
}
可以看到编写了两个测试单元,分别用于测试DML和DDL。
需要注意的是在编写测试单元的过程中可能会出现@Test
报错的情况,不要慌张,从本地的Maven仓库导入Junit的 jar包 即可,导入方法为文件->项目结构->项目设置->库->点“+”号
,如下,我导入的版本是4.12。
另外把JDBC操作语句的错误抛出,抛出的错误范围大点,设置的是Exception。
接着运行程序,查询数据库能够发现对应的操作成功实现了。
4. ResultSet
**ResultSet(结果集对象)**的作用:
- 封装了DQL查询语句的结果
- 提供了一些方法用以获取查询结果。可以粗略地分为如下两类。
要注意:ResultSet
列的编号(column index)从1开始!!!
JDBC API 案例
以后的应用场景常有:
首先从数据库中查询数据,把这些散着的数据使用Java对象封装起来,这些对象不是直接用来放到网页中去的,需要存储到容器中去,而集合又是专门用来存放对象的容器,再把集合交给对应的页面,页面再通过遍历集合等等操作,实现各种各样的显示。
、
下面是具体操作步骤:
-
首先需要新建一个pojo包,里面存放用到的实体类,路径为
com.niko.pojo
,这样src目录下会自动多出两个文件夹,一个就是之前的包文件的文件夹jdbc
,另一个是新建的包pojo
。 -
接着编写Account.java,快捷键
Alt+Insert
选中变量自动添加getter
和setter
方法如下package com.niko.pojo; public class Account { private int houseid; private String address; private double price; public int getHouseid() { return houseid; } public void setHouseid(int houseid) { this.houseid = houseid; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
继续添加
toString
方法便于查看,还是使用相同的快捷键添加。 -
接下来完成JDBC代码的编写,新建一个类叫做
JDBCExample1_ResultSet_HouseList
,与pojo包里的文件名字对应(养成好习惯)。接着明确我们的实现思路:定义实体类->从数据库查询数据(JDBC)->查询出来的数据封装到HouseList对象中->输出对象结果 或 进行网页显示等进一步的需求。
我们的实体类
HouseList
已经定义完成,接着我们在程序里要做的包括实体类实例化、数据库数据赋值给实体类HouseList
、将实体类实例存入集合。关键代码如下:
public class JDBCExample1_ResultSet_HouseList { /* * 查询houselist房屋表数据,封装为HouseList对象中,并且存储到ArrayList集合中 * 1. 定义实体类HouseList * 2. 查询数据,封装到HouseList对象中 * */ @Test public void test() throws Exception { //1.注册驱动 Class.forName("com.mysql.jdbc.Driver"); //2.获取连接 String url="jdbc:mysql://localhost:3306/db?useSSL=false"; String username="root"; String password="1234"; Connection conn = DriverManager.getConnection(url, username, password); //3.定义sql String sql = "SELECT * FROM houselist"; //4.获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5.执行sql //int count =stmt.executeUpdate(sql); //返回值是受影响的行数 ResultSet rs = stmt.executeQuery(sql); //创建集合(用到了泛型和多态) List<HouseList> list = new ArrayList<HouseList>(); //6.处理结果 //光标向下移动一行,并且判断当前行是否有数据 while (rs.next()) { HouseList houseList=new HouseList(); int id = rs.getInt("houseid"); String address = rs.getString("address"); double price = rs.getDouble("price"); //赋值 houseList.setHouseid(id); houseList.setAddress(address); houseList.setPrice(price); //存入集合 list.add(houseList); } //查看数据是否输出成功了 System.out.println(list); //7.释放资源 rs.close(); stmt.close(); conn.close(); } }
一个对象(也就是表里的一行)的输出结果如下:
要是没有在实体类里重载toString方法,会出现这样的输出结果:
查资料可知,我们所看到的输出是Java中对象的默认
toString()
方法生成的结果。默认情况下,Java的toString()
方法返回类名的完全限定名称(即包名+类名)后跟对象的哈希码的十六进制表示。具体来说,
com.niko.pojo.HouseList@1c2c22f3
表示:com.niko.pojo.HouseList
是对象的类名。1c2c22f3
是对象的哈希码的十六进制表示。
5.PreparedStatement
PreparedStatement就是继承自Statement 的执行SQL语句的对象,作用就是预编译SQL语句并执行来预防SQL注入问题。
1)SQL注入问题
摘自维基百科SQL注入 - 维基百科,自由的百科全书 (wikipedia.org)
SQL注入(英语:SQL injection),也称SQL注入或SQL注码,是发生于应用程序与数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的SQL指令而执行,因此遭到破坏或是入侵。
最简单的例子就是输入用户名和密码,这个过程中肯定会用到数据库的查询操作,比如说:
SELECT * FROM users WHERE username = 'username' AND password = 'password';
如果攻击者输入
admin' --
则服务器端接收到后可能会形成如下错误的SQL查询:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'password';
注释符
--
后面的文本将被视为注释,因此该查询实际上变成了:SELECT * FROM users WHERE username = 'admin';
这样攻击者无需正确的密码即可成功登录。
因此PreparedStatement
就相当于一道门卫岗,需要拦住特殊的非法查询,能增强数据库的安全性。(现在知道了为什么用户名或密码会限制特殊符号了)
idea快捷输入
sout
,回车后会自动补全为System.out.println()
package com.niko.jdbc;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
/*
* JDBC API 详解 PreparedStatement
* 以用户登录为例
* 附上SQL语句:
CREATE TABLE USER(
id int,
username VARCHAR(20),
password VARCHAR(32)
);
INSERT INTO USER VALUES(1,'NIKO','123'),(2,'MONESY','456');
* */
public class JDBCDemo5_PreparedStatement {
/*
* 正常登录
* */
@Test
public void testDQL() throws Exception {
//获取连接
String url="jdbc:mysql://localhost:3306/db2?useSSL=false";
String username="root";
String password="1234";
Connection conn = DriverManager.getConnection(url, username, password);
//接收用户输入 用户名和密码
String name="NIKO";
String pwd="123";
String sql="select * from user where username='"+name+"'and password='"+pwd+"'";
//获取stmt对象
Statement stmt=conn.createStatement();
//执行sql
ResultSet rs=stmt.executeQuery(sql);
//判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
if(rs.next()) {
System.out.println("SUCCESS");
}else {
System.out.println("FAIL");
}
//释放资源
rs.close();
stmt.close();
conn.close();
}
/*
* SQL注入
* */
@Test
public void testDQL_Injection() throws Exception {
//获取连接
String url="jdbc:mysql://localhost:3306/db2?useSSL=false";
String username="root";
String password="1234";
Connection conn = DriverManager.getConnection(url, username, password);
//接收用户输入 用户名和密码
String name="NIKO";//随便写,不用管数据库里有没有
String pwd="' OR '1'='1";
String sql="select * from user where username='"+name+"'and password='"+pwd+"'";
System.out.println(sql);
//获取stmt对象
Statement stmt=conn.createStatement();
//执行sql
ResultSet rs=stmt.executeQuery(sql);
//判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
if(rs.next()) {
System.out.println("SUCCESS 注入成功!");
}else {
System.out.println("FAIL");
}
//释放资源
rs.close();
stmt.close();
conn.close();
}
}
2)使用PreparedStatement解决问题
使用方法:
-
获取 PreparedStatement 对象
//初始:sql注入本质就是在拼sql语句字符串 String sql="select * from user where username='"+name+"'and password='"+pwd+"'"; //解决:SQL语句中的参数值现在使用占位符代替 String sql="select * from user where username=? and password=?"; //调用conn对象的方法,将这个sql语句作为参数传入Pstmt对象 PreparedStatement pstmt=conn.prepareStatement(sql);
-
设置参数值
参数按照顺序 一 一对应各个?号。
调用
pstmt
的方法setXxx()
,在此示例中用户名和密码两个都是字符串类型。//设置?号的值 pstmt.setString(1,name); pstmt.setString(2,pwd);
-
执行SQL并处理结果
要注意这里不能调用父类的带参数的
executeQuery
方法,因为预编译的特性,SQL语句已经和pstmt对象绑定,所以调用的是空参的同名方法。//执行sql ResultSet rs=pstmt.executeQuery(); //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行) if(rs.next()) { System.out.println("SUCCESS"); }else { System.out.println("FAIL"); } //释放资源 rs.close(); pstmt.close(); conn.close(); }
很显然,再次传入一样的账户名和密码进行SQL注入测试,输出FAIL表示登录失败,登录被拒绝。
类型。//设置?号的值 pstmt.setString(1,name); pstmt.setString(2,pwd);
-
执行SQL并处理结果
要注意这里不能调用父类的带参数的
executeQuery
方法,因为预编译的特性,SQL语句已经和pstmt对象绑定,所以调用的是空参的同名方法。//执行sql ResultSet rs=pstmt.executeQuery(); //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行) if(rs.next()) { System.out.println("SUCCESS"); }else { System.out.println("FAIL"); } //释放资源 rs.close(); pstmt.close(); conn.close(); }
很显然,再次传入一样的账户名和密码进行SQL注入测试,输出FAIL表示登录失败,登录被拒绝。
3) PreparedStatement原理
2024年7月10日09:31:35更新
PreparedStatement
有这样几个好处:
-
预编译SQL,提高SQL语句执行性能
我们已经学了Java代码是如何和MySQL数据库进行交互的:Java代码将待执行的SQL语句发给MySQL服务器,MySQL服务器在接收SQL语句并执行后返回结果给Java代码。
其中MySQL服务器在接受SQL语句之后并不是直接执行SQL代码的,而是需要经过 检查SQL语法->编译SQL得到可执行的函数 -> 执行SQL 几个步骤。其中前两个步骤----检查和编译 过程相比 执行 需要耗费相对更多的时间。
对于同样结构的SQL语句,比如
SELECT * FROM USER WHERE USERNAME = ? setString(1,"Mike"); setString(2,"Jane");
只需执行一次检查与编译步骤,节省了时间,因此能够提高性能。
但是值得注意的是
PreparedStatement
的预编译功能默认是关闭的,需要在数据库URL的后面加上参数useServerPrepStmts=true
才能开启。String url="jdbc:mysql://localhost:3306/db2?useSSL=false"?useServerPrepStmts=true;
-
解决SQL注入问题
上一次我们学到Pstmt防止SQL注入的原理是 将敏感字符进行转义。
为了可视化这个过程,我们还需要配置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" # 设置慢查询的阈值时间为2秒(查询时间超过2秒的语句将被记录到慢查询日志中) long_query_time=2
找不到这个文件的可以找找隐藏目录
C:\ProgramData\MySQL\MySQL Server 8.0\my.ini
(Windows)关于如何查找MySQL安装目录,可参考如何查看MySql的安装位置?_mysql安装路径怎么找-CSDN博客
接着重启MySQL服务,这里使用的是以管理员身份运行的cmd,当然使用services.msc命令进入图形界面修改也可以。
C:\Windows\System32>net stop mysql MySQL 服务正在停止. MySQL 服务已成功停止。 C:\Windows\System32>net start mysql MySQL 服务正在启动 . MySQL 服务已经启动成功。
查看log文件来了解是否开启了预编译,可以看到开启预编译之后确实是通过对敏感字符转义字符来实现。还可以测试一下同一结构的两个语句,可以发现只有一次prepare提示,两次execute提示,说明只有一次检查和编译过程,但是有两次执行过程。
开启预编译之后:
开启预编译之前:
学习过程参考05-JDBC-API详解-ResultSet_哔哩哔哩_bilibili黑马程序员