目录
正文:
在上一篇文章中,我们对JDBC的基础概念和使用做了介绍,现在我们再对JDBC的其他功能进行进一步的讲解,主要包括JDBC的预编译功能,批处理功能,事务控制功能。
JDBC预编译
什么是预编译
JDBC的预编译功能是一种执行SQL语句的方式,通过预先编译SQL语句并将其缓存在数据库中,可以提高数据库操作的安全性和效率。
JDBC预编译如何使用
JDBC预编译是通过PreparedStatement对象替代Statement对象来实现的,在使用PreparedStatement对象时,我们不需要将参数预先拼接到sql字符串中,而是在sql中以问号(?)来占位,如:
String sql = "select * from user where login_name= ? and password = ?";
之后使用Connection的prepareStatement方法来创建预编译对象:
PreparedStatement statement = connection.prepareStatement(sql);
再使用预编译对象的setXXX方法来设置参数:
statement.setString(1, loginName);
statement.setString(2, password);
最后查询ResultSet:
ResultSet resultSet = statement.executeQuery();
以上就是预编译对象的使用方法了,值得注意的是以下几点 :
预编译对象除了上面示例中的setString方法之外,还有setInt,setArray,setDate等一系列的set方法用于设置参数,具体如何选择要视sql参数类型而定;
预编译对象的setXXX方法的第一个参数为参数占位符的位置,即sql中的第几个问号,位置从1开始而不是从0开始;
设置好参数之后直接执行预编译对象的无参数的executeQuery()方法即可获取到查询结果,不需要再传sql作为参数了。
预编译如何提升安全性
我们先来看一张表:
这是一张简单的用户表,用于实现一个最简便的登录操作,里面包含用户的用户id,用户名,登录名以及登录密码,当用户需要登陆时,输入登录名和登录密码,通过登录名和登录密码在表里查找用户信息,如果能找到,则登录成功,如果找不到,则登录失败,此时如果简单的使用上一篇文章提到的jdbc基础知识的话,我们写出的代码大致是这样的:
a. 拼接待执行的sql:
String sql = "select * from user where login_name='" + loginName + "' and password = '" + password + "'";
b. 通过statement的executeQuery(sql)方法查询ResultSet对象,从而获取到用户信息:
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()) {
//构建用户信息并返回
}else{
//报错登录失败
}
此时当我们正常输入登录名和密码时,是可以完成登录功能的,但是这里有一个巨大的安全隐患,就是当用户输入password="zzz' or 'a' = 'a"时,以上代码拼接出来的sql就变成了:
select * from user where login_name='lisi' and password = 'zzz' or 'a' = 'a';
而由于or后面的条件‘a’='a'是恒成立的,则sql相当于全表查询了user表,必然会登录成功,这也就是我们常说的sql注入攻击。
SQL注入攻击是一种常见的网络安全漏洞,攻击者利用该漏洞通过向应用程序的输入框或参数中插入恶意的SQL语句,来实现对数据库的非法访问和操作。SQL注入攻击通常发生在需要用户输入数据并将其传递给数据库执行SQL查询的应用程序中,如Web应用程序、数据库驱动程序等。攻击者利用SQL注入攻击的原理是通过在输入字段中注入恶意的SQL代码,使得应用程序在拼接SQL语句时无法正确过滤用户输入的内容,从而导致数据库执行了攻击者构造的恶意SQL语句。
而当我们使用预编译的方式来重写这段登录逻辑时,则会发现,不论我们的参数如何传值,都不会出现上述问题,这是因为JDBC的预编译对象在设置参数时,会帮我们对单引号进行转义处理,即上述问题示例中的password参数会被处理成“zzz\' or \'a\' = \'a”,这样最终执行的sql实际为:
select * from user where login_name='lisi' and password = 'zzz\' or \'a\' = \'a';
如此一来,不论用户输入什么参数,都不会破坏sql的原有结构,大大提高了数据库操作的安全性。
预编译如何提升效率
数据库在执行一个sql语句时,数据库会首先查询缓存,查询一下这个sql是否已经缓存了执行计划,如果没有缓存,则会对sql进行解析,编译,以及优化,最终生成执行计划,再由执行引擎执行执行计划,查询到数据结果之后,将结果返回给客户端,并将sql与对应的执行计划缓存起来,下图以Mysql数据库为例:
如果使用PreparedStatement进行预编译,当PreparedStatement对象被创建时,对应的SQL语句会被发送到数据库进行预编译。数据库会对SQL语句进行解析、优化,并生成执行计划。这个执行计划会被缓存起来,当PreparedStatement被多次执行时,即使参数不断变化,数据库也可以直接使用这个缓存的执行计划,而不需要重新进行解析和编译。而尽管PreparedStatement对象本身是在客户端(即JDBC)创建和管理的,但预编译的过程以及执行计划的生成和缓存都是在数据库端完成的。同时,使用PreparedStatement时,只需要发送一次SQL语句模板到数据库服务器,之后每次执行只需发送参数值即可,减少了网络传输的数据量,特别是在执行大量相似SQL语句的批处理操作时,这种优势更为明显。
增强代码的可读性和可维护性
JDBC的预编译功能除了以上优点之外,还有一个附带的优势,就是这种写法本身对开发者的友好性,相对于生硬的拼接字符串,这种一开始便确定了sql字符串,在查询时随时设置参数获取结果的方式明显更加简便、优雅。
JDBC批处理
什么是批处理
在前面说明预编译如何提升效率时,我们提到了一种操作叫做批处理,那么什么是批处理呢,见名知意,批处理就是将一批sql语句一起处理的操作,它允许应用程序将一组SQL语句作为一个批次发送到数据库进行执行,而不是一条一条地发送执行,在需要执行大量插入、更新或删除操作时,这种方式可以显著提高数据库操作的效率。
JDBC如何进行批处理
JDBC中的Statement语句对象提供了addBatch和executeBatch方法,继承了Statement语句对象的PreparedStatement预编译语句对象同样支持这两个方法,我们可以通过addBatch方法将要处理的sql添加到同一个批次中去,最后通过executeBatch方法一次性执行完成所有sql,executeBatch方法会返回一个int数组,数组内是每条sql影响的行数,如:
String insertSql = "insert into user values (?, ?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(insertSql);
for (int i = 0; i < 1000; i++) {
statement.setString(1, String.valueOf(i));
statement.setString(2, "userName"+i);
statement.setString(3, "loginName"+i);
statement.setString(4, "password");
statement.addBatch();
}
int[] resArr = statement.executeBatch();
int res = 0;
for (int i : resArr) {
res += i;
}
System.out.println("成功插入" + res + "条数据");
批处理的优缺点
优点
- 减少网络往返:批处理减少了应用程序与数据库服务器之间的通信次数,因为是多条语句一次性发送,而不是一条一条地发送。
- 提高性能:通过减少网络延迟和数据库接收处理请求的次数,批处理可以显著提高数据操作的性能。
- 减少资源消耗:批处理还可以减少数据库资源的消耗,因为它减少了解析、编译和执行SQL语句的次数。
缺点
- SQL语句限制:使用
Statement
的批处理可以添加不同的SQL语句,但使用PreparedStatement
的批处理只能执行相同的SQL语句,只是参数不同。 - 异常处理更复杂:如果批处理中的一条语句失败,可能会导致整个批次失败,异常处理相对更复杂。
事务控制
什么是事务
在说明JDBC如何控制事务之前,我们先简单复习一下什么是事务:事务(Transaction)是指作为单个逻辑工作单元执行的一组数据库操作。事务通常由一系列的数据库操作(例如插入、更新、删除)组成,这些操作要么全部成功执行,要么全部失败回滚,保持数据库的一致性和完整性。事务是数据库管理系统(DBMS)中确保数据操作的原子性、一致性、隔离性和持久性(ACID特性)的重要概念。
JDBC如何控制事务
JDBC是默认自动提交事务的,即每执行一条SQL都会自动提交一次事务,如果要关闭JDBC的自动提交,则可以通过调用connection的setAutoCommit方法并传参数false来关闭自动提交,在需要手动提交的时候,只需要调用connection的commit方法即可,而程序出现异常需要回滚操作时,则需要调用connection的rollback方法。需要注意的是,无论是否需要进行回滚,最终事务都需要进行提交操作,如我们常见的简单的转账的例子,我们需要从id为1的钱包中转出100元钱,转出到id为2的钱包中,如果在钱从1号钱包中转出来后,但是还没有到达2号钱包的时候,程序发生了异常,我们可以采用如下写法来保证转入转出操作的原子性:
//关闭自动提交
connection.setAutoCommit(false);
String sql = "update wallet set money = money+? where id = ?";
preparedStatement = connection.prepareStatement(sql);
try {
//钱包1转出100元
preparedStatement.setInt(1, -100);
preparedStatement.setString(2, "1");
preparedStatement.executeUpdate();
//此处可能发生异常
throw new RuntimeException("此时发生异常,导致钱从1号钱包转出来了,但是没有转到2号钱包");
//钱包2转入100元
preparedStatement.setInt(1, 100);
preparedStatement.setString(2, "2");
preparedStatement.executeUpdate();
}catch (Exception e){
try {
//回滚
connection.rollback();
}catch (SQLException e2){
e.printStackTrace();
}
}finally {
try {
//提交
connection.commit();
}catch (SQLException e){
e.printStackTrace();
}
}
如何设置回滚点
现在我们想一下另一个场景:1号钱包要给2号钱包转账1000次,每次转100元,但是我们需要每10次转账视为一个回滚点,即每转1000元确认一次,比如在第102次转账时发生了异常,此时不可以全部回滚所有转账操作,而是只回滚到第100次,也就是说最后的结果为转账了10000元,我们就可以每10次操作设置一次回滚点,在发生异常时只回滚到上一个回滚点,而不是回滚所有操作,我们可以这样写:
//关闭自动提交
connection.setAutoCommit(false);
String sql = "update wallet set money = money+? where id = ?";
preparedStatement = connection.prepareStatement(sql);
Savepoint savepoint = null;
try {
for (int i = 1; i <= 1000; i++) {
preparedStatement.setInt(1, -100);
preparedStatement.setString(2, "1");
preparedStatement.executeUpdate();
//第102次转账发生了异常
if(i==102) {
throw new RuntimeException("第102次转账发生了异常");
}
preparedStatement.setInt(1, 100);
preparedStatement.setString(2, "2");
preparedStatement.executeUpdate();
if(i%10==0){
//设置回滚点
savepoint = connection.setSavepoint();
}
}
}catch (Exception e){
try {
//有回滚点则回滚到回滚点位置,否则全部回滚
if(savepoint!=null) {
connection.rollback(savepoint);
}else{
connection.rollback();
}
}catch (SQLException e2){
e.printStackTrace();
}
}finally {
try {
//提交
connection.commit();
}catch (SQLException e){
e.printStackTrace();
}
}
总结
以上就是本文的全部内容了,包括JDBC的预编译,批处理以及事务控制几部分内容,希望对您能有些许的帮助,谢谢阅读~