【JDBC系列】- jdbc的概念以及与数据库的交互流程
核心API之preparedstatement用法
引言
上一篇jdbc系列文章中介绍了概念与statement的使用,但是statement会有一些缺处,所以这篇来学习一下preparedstatement这个API的用法。
Statement的缺点
Statement的最大缺点是会有SQL注入的风险。使用Statement时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。
例如以下SQL语句
1 | String username = "lyd"; |
假设username是通过输入传过来的,如果再没做校验的情况下,当输入的值为lyd' or '1' = '1
最后形成的SQL语句就变成了
1 | String sql = "SELECT * FROM tb_user WHERE username = 'lyd' or '1' = '1'"; |
这样我们就能查到指定数据之外的数据了,这就会造成安全隐患。
更多缺点如下
- SQL注入风险: 使用
Statement
时,SQL查询通常是通过字符串拼接构建的。如果没有正确地验证和处理用户输入,可能会导致SQL注入攻击,即恶意用户可以在输入中插入恶意SQL代码,从而执行非授权的数据库操作。- 性能问题:
Statement
执行SQL查询时,每次都要将SQL语句发送给数据库服务器进行编译。对于重复执行的查询,这会导致性能问题,因为数据库服务器需要重复编译相同的查询语句。- 可读性和维护性: 使用
Statement
时,SQL查询通常以字符串形式嵌入Java代码中,这可能导致SQL语句在代码中分散分布,降低代码的可读性和维护性。特别是对于复杂的SQL查询,代码的可读性将变得更差。- 不支持参数化查询: 在
Statement
中,SQL查询通常是硬编码在Java代码中的,没有很好地支持参数化查询。参数化查询是一种更安全和高效的方式,它允许将参数值与SQL语句分开,避免了SQL注入风险,并且可以利用数据库的查询缓存。
预编译statement - PreparedStatement
使用preparedstatement与使用statement的流程是一致的,只是预编译的需要多加一部去传入参数。
1、简单例子演示预编译statement
用个简单的例子来演示一下。与statement没什么太大的区别,主要是创建传输sql的对象不一样,不然其他基本都是相同的,这里需要注意的是,在使用预编译statement的SQL语句需要用“?”占位符来替代动态数据。在创建PreparedStatement对象之后,需要根据下标进行给占位符赋值,顺序是从左往右,从1开始。最后执行与statement大致相同,单纯查询就是用executeQuery。最后得到的结果集可以直接解析。
1 | /** |
2、进一步了解PreparedStatement
通过上面的简单实例,我们可以大致了解预编译statement无论是在安全上还是性能上都会比statement更胜一筹。我们进入PreparedStatement接口中可以看到他实际上是继承了Statement。
官方也提供了一个例子,通过下标给占位符去赋值。这样就能够很好的解决SQL注入的风险。
1)、执行DML语句
使用预编译statement执行DML语句,操作步骤与查询是一致的,只是在发送SQL的时候使用的是executeUpdate,他返回的并不是结果集,而是行数,正如我们用Navicat执行时显示的几行受影响一样,成功执行了多少行数据就会返回多少行,当执行失败就会返回0。
代码如下
1 | public class JDBCDMLPreparedStatement { |
执行后,可以在数据库中看到新增了这条数据。
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 | // 编写sql语句 不包含动态部分,动态值用占位符?来替代 |
发送SQL
1 | // 发送sql 查询就用executeQuery |
解析结果集
我们需要将每行数据进行提取,并且提取列名作为key而列值作为value来进行类似映射封装,最后存在List中。在之前就知道了ResultSet包含了一个行的游标,通过next可以将游标移动到下一行。然后可以通过get方法,来根据列标签或者是下表来获取值。如果要达成我们的目的,可以直接自定义key名称,再通过get方法获取值。虽然这样可以做到,但是灵活度不高。而本次要学习的是灵活度相对高一点的方案。
我们看一下数据集,这有3行数据,每行有3列,我们可以通过next方法来做到移动游标获取行数据,那么列呢?我们要如何才能获得数据库的列名或者查询数据的列标签呢?
ResultSetMetaData
ResultSet中有一个getMetaData()方法,它能够返回一个ResultSetMetaData对象,这个对象就是当前结果集列信息对象。我们通过源码可以看到这个列信息对象中包含了会话和字段等信息。
1 | public class ResultSetMetaData implements java.sql.ResultSetMetaData { |
我们可以通过getColumnCount()方法获取这个结果集有多少个字段(列)。在package com.mysql.cj.jdbc.result.ResultSetMetaData#getColumnCount,实际上就是统计了 Field[] fields 这个字段数组的长度。
1 | public int getColumnCount() throws SQLException { |
这样我们就可以在每行数据集中去遍历每列数据,这里需要注意的是,遍历的下标是从1开始到列长度数量。
获取列的信息就通过ResultSetMetaData对象获取,获取行相关信息就通过ResultSet对象获取,ResultSetMetaData能够获取列名或者列标签,建议使用列标签,因为有可能在查询的时候取了别名。
1 | ResultSet resultSet = preparedStatement.executeQuery(); |
运行之后的结果
ResultSetMetaData对象中还有许多的方法,如果是学习mybatis底层源码的时候,这些都是必不可少的资料。通过断点可以查看这个对象中的属性有哪些。这些后续会慢慢深入研究,并把学习成果分享出来。