JDBC为什么要使用PreparedStatement而不是Statement
JDBC 为什么要使用 PreparedStatement 而不是 Statement
PreparedStatement 是用来执行 SQL 查询语句的 API 之一。Java 提供了 Statement、PreparedStatement 和 CallableStatement 三种方式来执行查询语句:
Statement:用于通用查询。PreparedStatement:用于执行参数化查询。CallableStatement:用于执行存储过程。
同时,PreparedStatement 经常会在 Java 面试中被提及,譬如:Statement 与 PreparedStatement 的区别是什么?如何使用 PreparedStatement 避免 SQL 注入式攻击?这篇教程中我们会讨论为什么要用 PreparedStatement?使用 PreparedStatement 有什么样的优势?它又是如何避免 SQL 注入攻击的?
PreparedStatement 是什么?
PreparedStatement 是 java.sql 包下面的一个接口,用来执行 SQL 语句查询。通过调用 connection.prepareStatement(sql) 方法可以获得 PreparedStatement 对象。
数据库系统会对 SQL 语句进行预编译处理(如果 JDBC 驱动支持的话)。预处理语句将被预先编译好,这条预编译的 SQL 查询语句能在将来的查询中重用。这样一来,它比 Statement 对象生成的查询速度更快。下面是一个例子:
public class PreparedStmtExample {
public static void main(String args[]) throws SQLException {
// 注意:JDBC URL 通常以 jdbc: 开头,此处修正为规范格式
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306", "root", "root");
PreparedStatement preStatement = conn.prepareStatement("select distinct loan_type from loan where bank=?");
preStatement.setString(1, "Citibank");
ResultSet result = preStatement.executeQuery();
while(result.next()){
System.out.println("Loan Type: " + result.getString("loan_type"));
}
}
}Output:
Loan Type: Personal Loan
Loan Type: Auto Loan
Loan Type: Home Loan
Loan Type: Gold Loan在这个例子中,如果还是用 PreparedStatement 做同样的查询,哪怕参数值不一样,比如:"Standard Chartered" 或者 "HSBC" 作为参数值,数据库系统还是会去调用之前编译好的执行语句(系统库初次会对查询语句做最大的性能优化)。默认会返回 TYPE_FORWARD_ONLY 类型的结果集(ResultSet),当然你也可以使用 prepareStatement() 的重载方法返回不同类型的结果集。
预处理语句的优势
PreparedStatement 提供了诸多好处,企业级应用开发中强烈推荐使用 PreparedStatement 来做 SQL 查询。下面列出 PreparedStatement 的几点优势。
PreparedStatement 可以写动态参数化的查询
用PreparedStatement你可以写带参数的 SQL 查询语句。通过使用相同的 SQL 语句和不同的参数值来做查询,比创建一个不同的查询语句要好。下面是一个参数化查询:SELECT interest_rate FROM loan WHERE loan_type=?现在你可以使用任何一种 loan 类型,如:"personal loan"、"home loan" 或者 "gold loan" 来查询。这个例子叫做参数化查询,因为它可以用不同的参数调用它,这里的
?就是参数的占位符。PreparedStatement 比 Statement 更快
使用PreparedStatement最重要的一点好处是它拥有更佳的性能优势,SQL 语句会预编译在数据库系统中。执行计划同样会被缓存起来,它允许数据库做参数化查询。使用预处理语句比普通的查询更快,因为它做的工作更少(数据库对 SQL 语句的分析、编译、优化已经在第一次查询前完成了)。为了减少数据库的负载,生产环境中的 JDBC 代码你应该总是使用
PreparedStatement。值得注意的一点是:为了获得性能上的优势,应该使用参数化 SQL 查询而不是字符串追加的方式。下面两个 SELECT 查询,第一个 SELECT 查询就没有任何性能优势。SQL Query 1:字符串追加形式的 PreparedStatement
String loanType = getLoanType(); PreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=" + loanType);SQL Query 2:使用参数化查询的 PreparedStatement
PreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=?"); prestmt.setString(1, loanType);第二个查询就是正确使用
PreparedStatement的查询,它比 SQL 1 能获得更好的性能。PreparedStatement 可以防止 SQL 注入式攻击
如果你是做 Java Web 应用开发的,那么必须熟悉那声名狼藉的 SQL 注入式攻击。去年 Sony 就遭受了 SQL 注入攻击,被盗用了一些 Sony PlayStation (PS 机) 用户的数据。在 SQL 注入攻击里,恶意用户通过 SQL 元数据绑定输入。比如:某个网站的登录验证 SQL 查询代码为:
strSQL = "SELECT * FROM users WHERE name = '" + userName + "' and pw = '"+ passWord +"';"恶意填入:
userName = "1' OR '1'='1"; passWord = "1' OR '1'='1";那么最终 SQL 语句变成了:
strSQL = "SELECT * FROM users WHERE name = '1' OR '1'='1' and pw = '1' OR '1'='1';"因为 WHERE 条件恒为真,这就相当于执行:
strSQL = "SELECT * FROM users;"因此可以达到无账号密码亦可登录网站。如果恶意用户要是更坏一点,用户填入特定指令:
strSQL = "SELECT * FROM users WHERE name = 'any_value' and pw = ''; DROP TABLE users"这样一来,虽然没有登录,但是数据表都被删除了。
然而使用
PreparedStatement的参数化查询可以阻止大部分的 SQL 注入。在使用参数化查询的情况下,数据库系统(eg: MySQL)不会将参数的内容视为 SQL 指令的一部分来处理,而是在数据库完成 SQL 指令的编译后,才套用参数运行。因此就算参数中含有破坏性的指令,也不会被数据库所运行。补充:避免 SQL 注入的第二种方式
在组合 SQL 字符串的时候,先对所传入的参数做字符取代(将单引号字符取代为连续 2 个单引号字符,因为连续 2 个单引号字符在 SQL 数据库中会视为字符中的一个单引号字符)。譬如:strSQL = "SELECT * FROM users WHERE name = '" + userName + "';"传入字符串:
userName = " 1' OR 1=1 "把
userName做字符替换后变成:userName = " 1'' OR 1=1"最后生成的 SQL 查询语句为:
strSQL = "SELECT * FROM users WHERE name = '1'' OR 1=1'"这样数据库就会去系统查找 name 为"1' ' OR 1=1"的记录,而避免了 SQL 注入。
- 可读性与安全性
比起凌乱的字符串追加似的查询,PreparedStatement查询可读性更好、更安全。
PreparedStatement 的局限性
尽管 PreparedStatement 非常实用,但是它仍有一定的限制。
为了防止 SQL 注入攻击,
PreparedStatement不允许一个占位符 (?) 有多个值。在执行有 IN 子句查询的时候这个问题变得棘手起来。下面这个 SQL 查询使用PreparedStatement就不会返回任何结果:SELECT * FROM loan WHERE loan_type IN (?)preparedSatement.setString(1, "'personal loan', 'home loan', 'gold loan'");那如何解决这个问题呢?请你继续关注本博客,下期告诉你答案。
不算总结的总结
关于 PreparedStatement 接口,需要重点记住的是:
PreparedStatement可以写参数化查询,比Statement能获得更好的性能。- 对于
PreparedStatement来说,数据库可以使用已经编译过及定义好的执行计划,这种预处理语句查询比普通的查询运行速度更快。 PreparedStatement可以阻止常见的 SQL 注入式攻击。PreparedStatement可以写动态查询语句。PreparedStatement与java.sql.Connection对象是关联的,一旦你关闭了connection,PreparedStatement也没法使用了。?叫做占位符。PreparedStatement查询默认返回FORWARD_ONLY的ResultSet,你只能往一个方向移动结果集的游标。当然你还可以设定为其他类型的值,如:CONCUR_READ_ONLY。- 不支持预编译 SQL 查询的 JDBC 驱动,在调用
connection.prepareStatement(sql)的时候,它不会把 SQL 查询语句发送给数据库做预处理,而是等到执行查询动作的时候(调用executeQuery()方法时)才把查询语句发送给数据库,这种情况和使用Statement是一样的。 - 占位符的索引位置从 1 开始而不是 0,如果填入 0 会导致
java.sql.SQLException invalid column index异常。所以如果PreparedStatement有两个占位符,那么第一个参数的索引是 1,第二个参数的索引是 2。
以上就是为什么要使用 PreparedStatement 的全部理由。不过你仍然可以使用 Statement 对象用来做测试,但是在生产环境下你一定要考虑使用 PreparedStatement。
更多参考:
原文链接: Javarevisited
说明:本文核心原理基于 JDBC 规范,适用于大多数 Java 版本。文中部分案例引用自 2012 年左右的技术文章,虽然具体安全事件已过时,但 PreparedStatement 防止 SQL 注入及性能优化的机制在现代 JDBC 驱动中依然有效。 版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/jdbc-wei-shen-me-yao-shi-yong-preparedstatement-er-bu-shi-statement.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。