核心API之preparedstatement用法

引言

上一篇jdbc系列文章中介绍了概念与statement的使用,但是statement会有一些缺处,所以这篇来学习一下preparedstatement这个API的用法。

Statement的缺点

Statement的最大缺点是会有SQL注入的风险。使用Statement时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。

例如以下SQL语句

1
2
String username = "lyd";
String sql = "SELECT * FROM tb_user WHERE username = '" + username + "'";

假设username是通过输入传过来的,如果再没做校验的情况下,当输入的值为lyd' or '1' = '1最后形成的SQL语句就变成了

1
String sql = "SELECT * FROM tb_user WHERE username = 'lyd' or '1' = '1'";

这样我们就能查到指定数据之外的数据了,这就会造成安全隐患。

更多缺点如下

  1. SQL注入风险: 使用Statement时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。
  2. 性能问题: Statement执行SQL查询时,每次都要将SQL语句发送给数据库服务器进行编译。对于重复执行的查询,这会导致性能问题,因为数据库服务器需要重复编译相同的查询语句。
  3. 可读性和维护性: 使用Statement时,SQL查询通常以字符串形式嵌入Java代码中,这可能导致SQL语句在代码中分散分布,降低代码的可读性和维护性。特别是对于复杂的SQL查询,代码的可读性将变得更差。
  4. 不支持参数化查询:Statement中,SQL查询通常是硬编码在Java代码中的,没有很好地支持参数化查询。参数化查询是一种更安全和高效的方式,它允许将参数值与SQL语句分开,避免了SQL注入风险,并且可以利用数据库的查询缓存。

预编译statement - PreparedStatement

使用preparedstatement与使用statement的流程是一致的,只是预编译的需要多加一部去传入参数。

1、简单例子演示预编译statement

用个简单的例子来演示一下。与statement没什么太大的区别,主要是创建传输sql的对象不一样,不然其他基本都是相同的,这里需要注意的是,在使用预编译statement的SQL语句需要用“?”占位符来替代动态数据。在创建PreparedStatement对象之后,需要根据下标进行给占位符赋值,顺序是从左往右,从1开始。最后执行与statement大致相同,单纯查询就是用executeQuery。最后得到的结果集可以直接解析。

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
/**
* @Author: lyd
* @Description: 使用预编译Statement
* @Date: 2023/7/23
*/
public class JDBCPreparedStatement {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String name = "lyd";
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、创建连接
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
// 3、创建preparedstatement
// 3.1、编写sql语句 不包含动态部分,动态值用占位符?来替代
String sql = "SELECT * FROM tb_user WHERE username = ?";
// 3.2、创建preparedstatement,传入动态值
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.3、占位符赋值: 从左向右,从1开始。set是有类型的,可以使用Object
preparedStatement.setObject(1, name);
// 4、发送sql 查询就用executeQuery
ResultSet resultSet = preparedStatement.executeQuery();
// 5、解析结果
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
System.out.println(resultSet.getString(2));
System.out.println(resultSet.getString(3));
}
// 6、释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}

2、进一步了解PreparedStatement

通过上面的简单实例,我们可以大致了解预编译statement无论是在安全上还是性能上都会比statement更胜一筹。我们进入PreparedStatement接口中可以看到他实际上是继承了Statement。

image-20230723140703940.png

官方也提供了一个例子,通过下标给占位符去赋值。这样就能够很好的解决SQL注入的风险。

1)、执行DML语句

使用预编译statement执行DML语句,操作步骤与查询是一致的,只是在发送SQL的时候使用的是executeUpdate,他返回的并不是结果集,而是行数,正如我们用Navicat执行时显示的几行受影响一样,成功执行了多少行数据就会返回多少行,当执行失败就会返回0。

代码如下

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
public class JDBCDMLPreparedStatement {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String name = "lyd";
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、创建连接
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
// 3、创建preparedstatement
// 3.1、编写sql语句 不包含动态部分,动态值用占位符?来替代
String sql = "INSERT INTO tb_user(id,username,address) VALUES (?,?,?)";
// 3.2、创建preparedstatement,传入动态值
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 3.3、占位符赋值: 从左向右,从1开始。set是有类型的,可以使用Object
preparedStatement.setObject(1, 3);
preparedStatement.setObject(2, "opo");
preparedStatement.setObject(3, "quanzhou");
// 4、发送sql DML语句使用
int row = preparedStatement.executeUpdate();
// 5、解析结果
System.out.println(row + "行受影响");
// 6、释放资源
preparedStatement.close();
connection.close();
}
}

执行后,可以在数据库中看到新增了这条数据。

image-20230723141932197.png

2)、执行DQL语句

这次我们获取所有数据,并且遍历结果集,将每行的数据保存到一个链表当中,用map进行key-value进行映射。

注册驱动

还是一样的方法,我们采用反射的方式来注册驱动。

1
Class.forName("com.mysql.cj.jdbc.Driver");
创建连接
1
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
创建preparedstatement

因为不需要传入参数,也就没有使用占位符,然而这里也就不需要去对占位符进行赋值,只需要创建preparedstatement对象,将SQL语句放入就行。

1
2
3
// 编写sql语句 不包含动态部分,动态值用占位符?来替代
String sql = "SELECT * FROM tb_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
发送SQL
1
2
// 发送sql  查询就用executeQuery
ResultSet resultSet = preparedStatement.executeQuery();
解析结果集

我们需要将每行数据进行提取,并且提取列名作为key而列值作为value来进行类似映射封装,最后存在List中。在之前就知道了ResultSet包含了一个行的游标,通过next可以将游标移动到下一行。然后可以通过get方法,来根据列标签或者是下表来获取值。如果要达成我们的目的,可以直接自定义key名称,再通过get方法获取值。虽然这样可以做到,但是灵活度不高。而本次要学习的是灵活度相对高一点的方案。

我们看一下数据集,这有3行数据,每行有3列,我们可以通过next方法来做到移动游标获取行数据,那么列呢?我们要如何才能获得数据库的列名或者查询数据的列标签呢?

image-20230723145713518.png

ResultSetMetaData

ResultSet中有一个getMetaData()方法,它能够返回一个ResultSetMetaData对象,这个对象就是当前结果集列信息对象。我们通过源码可以看到这个列信息对象中包含了会话和字段等信息。

1
2
3
4
5
6
7
8
public class ResultSetMetaData implements java.sql.ResultSetMetaData {
private Session session;
private Field[] fields;
boolean useOldAliasBehavior = false;
boolean treatYearAsDate = true;
private ExceptionInterceptor exceptionInterceptor;
// ...
}

我们可以通过getColumnCount()方法获取这个结果集有多少个字段(列)。在package com.mysql.cj.jdbc.result.ResultSetMetaData#getColumnCount,实际上就是统计了 Field[] fields 这个字段数组的长度。

1
2
3
4
5
6
7
public int getColumnCount() throws SQLException {
try {
return this.fields.length;
} catch (CJException var2) {
throw SQLExceptionsMapping.translateException(var2, this.exceptionInterceptor);
}
}

这样我们就可以在每行数据集中去遍历每列数据,这里需要注意的是,遍历的下标是从1开始到列长度数量。

获取列的信息就通过ResultSetMetaData对象获取,获取行相关信息就通过ResultSet对象获取,ResultSetMetaData能够获取列名或者列标签,建议使用列标签,因为有可能在查询的时候取了别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ResultSet resultSet = preparedStatement.executeQuery();
List<Map> list = new ArrayList<>();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 解析结果
while (resultSet.next()) {
Map map = new HashMap<>();
// 遍历数据
for (int i = 1; i <= columnCount; i++) {
// 通过下标获取列名 - 通过 ResultSetMetaData对象获取
String columnLabel = metaData.getColumnLabel(i);
// 通过下标获取列值 - 通过 ResultSet对象获取
String value = resultSet.getString(i);
map.put(columnLabel, value);
}
list.add(map);
}

运行之后的结果

image-20230723151558555.png

ResultSetMetaData对象中还有许多的方法,如果是学习mybatis底层源码的时候,这些都是必不可少的资料。通过断点可以查看这个对象中的属性有哪些。这些后续会慢慢深入研究,并把学习成果分享出来。

image-20230723151906760.png