06-Web后端基础(java操作数据库)
1. 前言
在前面我们学习MySQL数据库时,都是利用图形化客户端工具(如:idea、datagrip),来操作数据库的。
我们做为后端程序开发人员,通常会使用Java程序来完成对数据库的操作。Java程序操作数据库的技术呢,有很多啊,而最为底层、最为基础的就是JDBC。
JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。 【是操作数据库最为基础、底层的技术】
但是使用JDBC来操作数据库,会比较繁琐,所以现在在企业项目开发中呢,一般都会使用基于JDBC的封装的高级框架,比如:Mybatis、MybatisPlus、Hibernate、SpringDataJPA。
而这些技术,目前的市场占有份额如下图所示:
从上图中,我们也可以看到,目前最为主流的就是Mybatis,其次是MybatisPlus。
所以,在我们的课程体系中呢,这两种主流的操作数据库的框架我们都要学习。 而我们在学习这两个主流的框架之前,还需要学习一下操作数据库的基础基础 JDBC。 然后接下来,再来学习Mybatis。 而在我们后面的课程中,我们还要学习MybatisPlus框架。 那么今天呢,我们就先来学习 JDBC 和 Mybatis。
今天课程安排:
- JDBC
- Mybatis
- SpringBoot配置文件
2. JDBC
2.1 介绍
JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API。
本质:
- sun公司官方定义的一套操作所有关系型数据库的规范,即接口。
- 各个数据库厂商去实现这套接口,提供数据库驱动jar包。
- 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。
那有了JDBC之后,我们就可以直接在java代码中来操作数据库了,只需要编写这样一段java代码,就可以来操作数据库中的数据。 示例代码如下:
2.2 查询数据
2.2.1 需求
需求:基于JDBC实现用户登录功能。
本质:其本质呢,其实就是基于JDBC程序,执行如下select语句,并将查询的结果输出到控制台。SQL语句:
select * from user where username = 'linchong' and password = '123456';
2.2.2 准备工作
1). 创建一个maven项目
2). 创建一个数据库 web,并在该数据库中创建user表
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 代码实现
AI提示词(prompt):
你是一名java开发工程师,帮我基于JDBC程序来操作数据库,执行如下SQL语句:
select * from user where username = 'daqiao' and password = '123456'
具体的代码为:
1). 在 pom.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>
2). 在 src/main/test/java
目录下编写测试类,定义测试方法
public class JDBCTest {/*** 编写JDBC程序, 查询数据*/@Testpublic void testJdbc() throws Exception {// 获取连接Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/web", "root", "1234");// 创建预编译的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中的参数化测试进行单元测试,代码改造如下:
版本1:
@Test
public void testSelect() throws Exception {Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;try {String url = "jdbc:mysql://localhost:3306/web01";String username = "root";String password = "fu921400521";Class.forName("com.mysql.cj.jdbc.Driver");connection = DriverManager.getConnection(url, username, password);preparedStatement = connection.prepareStatement("select * from user where username = ? and password = ?");preparedStatement.setString(1, "daqiao");preparedStatement.setString(2, "123456");resultSet = preparedStatement.executeQuery();while (resultSet.next()) {User user = new User(resultSet.getInt("id"),resultSet.getString("username"),resultSet.getString("password"),resultSet.getString("name"),resultSet.getInt("age"));System.out.println(user);}} catch (SQLException se) {se.printStackTrace();} catch (Exception e) {e.printStackTrace();}finally {try {if (resultSet != null) resultSet.close();if (preparedStatement != null) preparedStatement.close();if (connection != null) connection.close();}catch (SQLException se) {se.printStackTrace();}}}
版本2:
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", "1234");// 创建预编译的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(…):获取数据,可以根据列的编号获取,也可以根据列名获取(推荐)。
结果解析步骤:
2.2.4.2 预编译SQL
其实我们在编写SQL语句的时候,有两种风格:
- 静态SQL(参数硬编码)
conn.prepareStatement("SELECT * FROM user WHERE username = 'daqiao' AND password = '123456'");
ResultSet resultSet = pstmt.executeQuery();
这种呢,就是参数值,直接拼接在SQL语句中,参数值是写死的。
- 预编译SQL(参数动态传递)
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语句。主要的作用有两个:
- 防止SQL注入
- 性能更高
那接下来,我们就来介绍一下这两点。
2.2.4.2.1 SQL注入
- SQL注入:通过控制输入来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
SQL注入最典型的场景,就是用户登录功能。
注入演示:
1). 打开课程资料中的文件夹 资料/02. SQL注入演示
,运行其中的jar包 sql_Injection_demo-0.0.1-SNAPSHOT.jar
,进入该目录后,执行命令:
java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar
2). 打开浏览器访问 http://localhost:9090/ ,必须登录后才能访问到系统。我们先测试正常的用户名和密码
3). 接下来,我们再来测试一下错误的用户名和密码 。
我们看到,如果用户名密码错误,是不能进入到系统中进行访问的,会提示 用户名和密码错误
。
4). 那接下来,我们就要演示一下SQL注入现象,我们可以通过控制表单输入,来修改事先定义好的SQL语句的含义。 从而来攻击服务器。
点击登录后,我们看到居然可以成功进入到系统中。
为什么会出现这种现象呢?
在进行登录操作时,怎么样才算登录成功呢? 如果我们查询到了数据,就说明用户名密码是对的。 如果没有查询到数据,就说明用户名或密码错误。
而出现上述现象,原因就是因为,我们我们编写的SQL语句是基于字符串进行拼接的 。 我们输入的用户名无所谓,比如:shfhsjfhja
,而密码呢,就是我们精心设计的,如:' or '1' = '1
。
那最终拼接的SQL语句,如下所示:
我们知道,or
连接的条件,是或的关系,两者满足其一就可以。 所以,虽然用户名密码输入错误,也是可以查询返回结果的,而只要查询到了数据,就说明用户名和密码是正确的。
2.2.4.2.2 SQL注入解决
而通过预编译SQL(select * from user where username = ? and password = ?),就可以直接解决上述SQL注入的问题。 接下来,我们再来演示一下,通过预编译SQL是否能够解决SQL注入问题。
1). 打开课程资料中的文件夹 资料/02. SQL注入演示
,运行其中的jar包 sql_prepared_demo-0.0.1-SNAPSHOT.jar
,进入该目录后,执行命令:
java -jar sql_prepared_demo-0.0.1-SNAPSHOT.jar
2). 打开浏览器访问 http://localhost:9090/
,必须登录后才能访问到系统 。我们先测试正常的用户名和密码
3). 那接下来,我们就要演示一下是否可以基于上述的密码 ' or '1' = '1
,来完成SQL注入 。
通过控制台,可以看到输入的SQL语句,是预编译SQL语句。
而在预编译SQL语句中,当我们执行的时候,会把整个' or '1'='1
作为一个完整的参数,赋值给第2个问号(' or '1'='1
进行了转义,只当做字符串使用)
那么此时再查询时,就查询不到对应的数据了,登录失败。
注意:在以后的项目开发中,我们使用的基本全部都是预编译SQL语句。
2.2.4.2.3 性能更高
2.3 增删改数据
2.3.1 需求
- 需求:基于JDBC程序,执行如下update语句。
- SQL:
update user set password = '123456', gender = 2 where id = 1;
2.3.2 代码实现
AI提示词(prompt):
你是一名java开发工程师,帮我基于JDBC程序来操作数据库,执行如下SQL语句:
update user set password = '123456', gender = 2 where id = 1;
代码实现如下:
@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(); //返回值是查询结果集