1 引言
在前面我们学习MySQL数据库时,都是利用图形化客户端工具(如:idea、datagrip),来操作数据库的。
我们做为后端程序开发人员,通常会使用Java程序来完成对数据库的操作。Java程序操作数据库的技术有很多,而最为底层、最为基础的就是JDBC。
JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。 【是操作数据库最为基础、底层的技术】
但是使用JDBC来操作数据库会比较繁琐,所以现在在企业项目开发中呢,一般都会使用基于JDBC的封装的高级框架,比如:Mybatis、MybatisPlus、Hibernate、SpringDataJPA。
目前最为主流的就是Mybatis,其次是MybatisPlus。
这两种主流的操作数据库的框架我们都要学习。 而我们在学习这两个主流的框架之前,还需要学习一下操作数据库的基础框架 JDBC。
2 JDBC
2.1 介绍
JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。
本质:sun公司官方定义的一套操作所有关系型数据库的规范,即接口。
各个数据库厂商去实现这套接口,提供数据库驱动jar包。
我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。
有了JDBC之后,就可以直接在java代码中来操作数据库了,只需要编写这样一段java代码,就可以来操作数据库中的数据。 示例代码如下:
java
@Test
public void testUpdate() throws Exception {
//准备工作
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/web";
Connection connection = DriverManager.getConnection(url, "root", "root@1234");
Statement statement = connection.createStatement();
//执行SQL
statement.executeUpdate("update user set password = '1234567890' where id = 1");
//释放资源
statement.close();
connection.close();
}
2.2 查询数据
2.2.1 需求
**需求:**基于JDBC实现用户登录功能。
**本质:**其本质其实就是基于JDBC程序,执行如下select语句,并将查询的结果输出到控制台。
sql
select * from user where username = 'linchong' and password = '123456';
2.2.2 准备工作
-
创建一个maven项目
-
创建一个数据库 web,并在该数据库中创建user表
java
create table user(
id int unsigned primary key auto_increment comment 'ID,主键',
username varchar(20) comment '用户名',
password varchar(32) comment '密码',
name varchar(10) comment '姓名',
age tinyint unsigned comment '年龄'
) comment '用户表';
insert into user(id, username, password, name, age) values (1, 'daqiao', '123456', '大乔', 22),
(2, 'xiaoqiao', '123456', '小乔', 18),
(3, 'diaochan', '123456', '貂蝉', 24),
(4, 'lvbu', '123456', '吕布', 28),
(5, 'zhaoyun', '12345678', '赵云', 27);
2.2.3 代码实现
- 在 pom.xml 文件中引入依赖
XML
<dependencies>
<!-- MySQL JDBC driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
</dependencies>
- 在 src/main/test/java 目录下编写测试类,定义测试方法
java
public class JDBCTest {
/**
* 编写JDBC程序, 查询数据
*/
@Test
public void testJdbc() throws Exception {
// 获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/web", "root", "****");
// 创建预编译的PreparedStatement对象
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM user WHERE username = ? AND password = ?");
// 设置参数
pstmt.setString(1, "daqiao"); // 第一个问号对应的参数
pstmt.setString(2, "123456"); // 第二个问号对应的参数
// 执行查询
ResultSet rs = pstmt.executeQuery();
// 处理结果集
while (rs.next()) {
int id = rs.getInt("id");
String uName = rs.getString("username");
String pwd = rs.getString("password");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println("ID: " + id + ", Username: " + uName + ", Password: " + pwd + ", Name: " + name + ", Age: " + age);
}
// 关闭资源
rs.close();
pstmt.close();
conn.close();
}
}
而上述的单元测试中,我们在SQL语句中,将 用户名 和密码的值都写死了,而这两个值应该是动态的,是将来页面传递到服务端的。 那么,我们可以基于前面所讲解的JUnit中的参数化测试进行单元测试,代码改造如下:
java
public class JDBCTest {
/**
* 编写JDBC程序, 查询数据
*/
@ParameterizedTest
@CsvSource({"daqiao,123456"})
public void testJdbc(String _username, String _password) throws Exception {
// 获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/web", "root", "****");
// 创建预编译的PreparedStatement对象
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM user WHERE username = ? AND password = ?");
// 设置参数
pstmt.setString(1, _username); // 第一个问号对应的参数
pstmt.setString(2, _password); // 第二个问号对应的参数
// 执行查询
ResultSet rs = pstmt.executeQuery();
// 处理结果集
while (rs.next()) {
int id = rs.getInt("id");
String uName = rs.getString("username");
String pwd = rs.getString("password");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println("ID: " + id + ", Username: " + uName + ", Password: " + pwd + ", Name: " + name + ", Age: " + age);
}
// 关闭资源
rs.close();
pstmt.close();
conn.close();
}
}
如果在测试时,需要传递一组参数,可以使用 @CsvSource 注解。
2.2.4 代码剖析
2.2.4.1 ResultSet
ResultSet(结果集对象):封装了DQL查询语句查询的结果。
next():将光标从当前位置向前移一行并判断当前行是否为有效行,返回值为boolean。
true:有效行,当前行有数据
false:无效行,当前行没有数据
getXxx(...):获取数据,可以根据列的编号获取,也可以根据列名获取(推荐)。
结果解析步骤:
java
while (resultSet.next()){
int id = resultSet.getInt("id");
//...
}
2.2.4.2 预编译SQL
其实我们在编写SQL语句的时候,有两种风格:
静态SQL(参数硬编码)
java
conn.prepareStatement("SELECT * FROM user WHERE username = 'daqiao' AND password = '123456'");
ResultSet resultSet = pstmt.executeQuery();
这种就是参数值,直接拼接在SQL语句中,参数值是写死的。
预编译SQL(参数动态传递)
java
conn.prepareStatement("SELECT * FROM user WHERE username = ? AND password = ?");
pstmt.setString(1, "daqiao");
pstmt.setString(2, "123456");
ResultSet resultSet = pstmt.executeQuery();
这种并未将参数值在SQL语句中写死,而是使用 ? 进行占位,然后再指定每一个占位符对应的值是多少,而最终在执行SQL语句的时候,程序会将SQL语句(SELECT * FROM user WHERE username = ? AND password = ?),以及参数值("daqiao", "123456")都发送给数据库,然后在执行的时候,会使用参数值,将?占位符替换掉。
那这种预编译的SQL,也是在项目开发中推荐使用的SQL语句。主要的作用有两个:
1.防止SQL注入
2.性能更高
2.2.4.2.1 SQL注入
SQL注入 :通过控制输入来修改事先定义好的 SQL语句,以达到执行代码对服务器进行攻击的方法。
SQL注入最典型的场景,就是用户登录功能。
举个静态SQL的例子:
比如我们的代码是这个:
java
conn.prepareStatement("SELECT * FROM user WHERE username = 'daqiao' AND password = '123456'");
ResultSet resultSet = pstmt.executeQuery();
在进行登录操作时,怎么样才算登录成功呢? 如果我们查询到了数据,就说明用户名密码是对的。 如果没有查询到数据,就说明用户名或密码错误。
如果在登录界面输入正确密码时,显然可以正常成功地登录,但如果输入错误的密码呢?
我们随便输入一个用户名,然后在密码中输入以下代码:
java
' or '1' = '1
注意到,此时,提供给后端的代码在运行时,变成了
java
SELECT * FROM user WHERE username = 'dadvasdn' AND password = '' or '1' = '1'
发现代码逻辑发生了变化,输入的用户名是乱码,密码为空,但后面多了一个逻辑或运算符or,且条件为真。
也就是说,这条语句会成功运行,而用户就成功登录了。
2.2.4.2.2 SQL注入解决
而通过预编译SQL(select * from user where username = ? and password = ?),就可以直接解决上述SQL注入的问题。 因为在使用预编译SQL时,后端收到的只是一堆字符串,通过进行这些字符串的对比来运行,而且输入的转义字符在进入源码时其破坏作用也会大幅下降。
因此在以后的项目开发中,我们使用的基本全部都是预编译SQL语句。
2.2.4.2.3 性能更高
在MySQL进行数据对比的过程中,首先在缓存中进行SQL语句的语法解析检查、优化和编译,然后再去执行。
那么如果我们用的是静态SQL语句,那么由于每次的SQL语句都不同,在进行他们之间的对比时,SQL语句都存在缓存中了,利用率很低,而且在缓存中进行查询速度更快,效率更高,静态SQL语句显然无法发挥这个优势。
而如果我们用的是预编译的SQL语句,那么在进行上述操作时,显然由于代码框架不是死板的了,具有更强的复用性,用户第一次输入数据时缓存中就存储了优化后的预编译的SQL语句,之后再有数据进来,缓存中就能找到相应的SQL语句,从而进行更加快捷高效的查询。
2.3 增删改数据
2.3.1 需求
需求:基于JDBC程序,执行如下update语句。
sql
update user set password = '123456', gender = 2 where id = 1;
2.3.2 代码实现
java
@ParameterizedTest
@CsvSource({"1,123456,25"})
public void testUpdate(int userId, String newPassword, int newAge) throws Exception {
// 建立数据库连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/web", "root", "1234");
// SQL 更新语句
String sql = "UPDATE user SET password = ?, age = ? WHERE id = ?";
// 创建预编译的PreparedStatement对象
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数
pstmt.setString(1, newPassword); // 第一个问号对应的参数
pstmt.setInt(2, newAge); // 第二个问号对应的参数
pstmt.setInt(3, userId); // 第三个问号对应的参数
// 执行更新
int rowsUpdated = pstmt.executeUpdate();
// 输出结果
System.out.println(rowsUpdated + " row(s) updated.");
// 关闭资源
pstmt.close();
conn.close();
}
JDBC程序执行DML 语句:int rowsUpdated = pstmt.executeUpdate();//返回值是影响的记录数
JDBC程序执行DQL 语句:ResultSet resultSet = pstmt.executeQuery(); //返回值是查询结果集