【Java】JDBC 之 PreparedStatement 和 Statement 的区别和理解【转载并梳理】
人民网>>社会·法治

【Java】JDBC 之 PreparedStatement 和 Statement 的区别和理解【转载并梳理】

2025-06-24 12:02:53 | 来源:人民网
小字号

一、PreparedStatement 和 Statement 的含义和区别

1、含义

  • PreparedStatementPreparedStatement是预编译的,对于批量处理可以大大提高效率,也叫 JDBC存储过程
  • Statement:使用 Statement对象,在对数据库只执行一次性存取的时侯,用 Statement对象进行处理。

2、区别

区别一:开销

  • PreparedStatement对象的开销比 Statement大,对于一次性操作并不会带来额外的好处。

区别二:编译

  • Statement每次执行 sql语句,相关数据库都要执行 sql语句的编译,而 PreparedStatement是预编译的,且支持批处理。
代码片段一
Statementstmt =conn.createStatement();StringupdateString ="UPDATE COFFEES SET SALES = 75 "+"WHERE COF_NAME LIKE ′Colombian′";stmt.executeUpdate(updateString);
代码片段二
PreparedStatementupdateSales =con.prepareStatement("UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? ");updateSales.setInt(1,75);updateSales.setString(2,"Colombian");updateSales.executeUpdate();
  • 代码片段二和代码片段一的区别在于,后者使用了 PreparedStatement对象,而前者是普通的 Statement对象。
  • PreparedStatement对象不仅包含了 SQL语句,而且大多数情况下这个语句已经被预编译过,因而当其执行时, 只需 DBMS(数据库管理系统)运行 SQL语句,而不必先编译。
  • 当你需要执行 Statement对象多次的时候,PreparedStatement对象将会大大降低运行时间,当然也加快了访问数据库的速度。
  • 这种转换也给你带来很大的便利,不必重复 SQL语句的句法,而只需更改其中变量的值,便可重新执行 SQL语句
  • 选择 PreparedStatement对象与否,在于相同句法的 SQL语句是否执行了多次,而且两次之间的差别仅仅是变量的不同。
  • 如果仅仅执行了一次的话,它应该和普通的对象毫无差异,体现不出它预编译的优越性。

区别三:可读性

  • 执行许多 SQL语句JDBC程序产生大量的 StatementPreparedStatement对象。
  • 通常认为 PreparedStatement对象比 Statement对象更有效,特别是如果带有不同参数的同一 SQL语句被多次执行的时候。
  • PreparedStatement对象允许数据库预编译 SQL语句,这样在随后的运行中可以节省时间并增加代码的可读性。

特殊情况区别四:缓存

  • Oracle环境中,开发人员实际上有更大的灵活性。
  • 当使用 StatementPreparedStatement对象时,Oracle数据库会缓存 SQL语句以便以后使用。
  • 在一些情况下,由于驱动器自身需要额外的处理和在 Java应用程序Oracle服务器间增加的网络活动,执行 PreparedStatement对象实际上会花更长的时间。

区别五:安全性

  • 在企业应用程序中更喜欢使用 PreparedStatement对象,那就是安全性。
  • 传递给 PreparedStatement对象的参数可被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。
  • 当处理 公共Web站点上的用户传来的数据的时候,安全性的问题就变得极为重要。
  • 传递给 PreparedStatement的字符串参数会自动被驱动器忽略,这就意味着当你的程序试着将 字符串“D'Angelo”插入到 VARCHAR2中时,该语句将不会识别第一个 “,”,从而导致悲惨的失败,几乎很少有必要创建你自己的字符串忽略代码。
  • Web环境中,有恶意的用户会利用那些设计不完善的、不能正确处理字符串的应用程序。
  • 特别是在 公共Web站点上,在没有首先通过 PreparedStatement对象处理的情况下,所有的用户输入都不应该传递给 SQL语句
  • 此外,在用户有机会修改 SQL语句的地方,如 HTML的隐藏区域或一个查询字符串上,SQL语句都不应该被显示出来。

  • 在执行 SQL命令时,我们有二种选择:可以使用 PreparedStatement对象,也可以使用 Statement对象。
  • 无论多少次地使用同一个 SQL命令PreparedStatement都只对它解析和编译一次。
  • 当使用 Statement对象时,每次执行一个 SQL命令时,都会对它进行解析和编译。

PreparedStatement会先初始化 SQL,先把这个 SQL提交到数据库中进行预处理,多次使用可提高效率。
Statement不会初始化,没有预处理,没次都是从 0开始执行 SQL

PreparedStatement可以使用 ? 替换变量

// 在 SQL语句中可以包含 ?ps =conn.prepareStatement("select * from Cust where ID=?");intsid =1001;ps.setInt(1,sid);rs =ps.executeQuery();

Statement只能按下面写法来实现

intsid =1001;Statementstmt =conn.createStatement();ResultSetrs =stmt.executeQuery("select * from Cust where ID="+sid);

二、深入理解 PreparedStatement 和 Statement

1、使用 Statement 而不是 PreparedStatement 对象

  • JDBC驱动的最佳化是基于使用的是什么功能,选择 PreparedStatement还是 Statement取决于你要怎么使用它们。
  • 对于只执行一次的 SQL语句选择 Statement是最好的。相反,如果 SQL语句被多次执行选用 PreparedStatement是最好的。

  • PreparedStatement的第一次执行消耗是很高的,它的性能体现在后面的重复执行。
  • 例如,假设我使用 Employee ID,使用 prepared的方式来执行一个针对 Employee表的查询,JDBC驱动会发送一个网络请求到数据解析和优化这个查询,而执行时会产生另一个网络请求,在 JDBC驱动中,减少网络通讯是最终的目的。
  • 如果我的程序在运行期间只需要一次请求,那么就使用 Statement
  • 对于 Statement,同一个查询只会产生一次网络到数据库的通讯。

  • 对于使用 PreparedStatement池的情况下,本指导原则有点复杂。
  • 当使用 PreparedStatement池时,如果一个查询很特殊,并且不太会再次执行到,那么可以使用 Statement
  • 如果一个查询很少会被执行,但连接池中的 Statement池可能被再次执行,那么请使用 PreparedStatement
  • 在不是Statement池的同样情况下,请使用 Statement

2、使用 PreparedStatement 的 Batch 功能

  • Update大量的数据时,先 Prepare一个 INSERT语句再多次的执行,会导致很多次的网络连接。
  • 要减少 JDBC的调用次数改善性能,你可以使用 PreparedStatementAddBatch()方法一次性发送多个查询给数据库。
  • 例如,让我们来比较一下下面的例子

例1:多次执行 PreparedStatement,多次数据库请求(网络请求)

PreparedStatementps =conn.prepareStatement("INSERT into employees values (?, ?, ?)");for(n =0;n <100;n++){ ps.setString(name[n]);ps.setLong(id[n]);ps.setInt(salary[n]);ps.executeUpdate();}
  • 在例1中,PreparedStatement被用来多次执行 INSERT语句
  • 在这里,执行了 100次INSERT操作,共有 101次网络往返
  • 其中,1次往返是预储 PreparedStatement,另外 100次往返执行每个迭代。

例2:使用 Batch,以此请求执行多条

PreparedStatementps =conn.prepareStatement("INSERT into employees values (?, ?, ?)");for(n =0;n <100;n++){ ps.setString(name[n]);ps.setLong(id[n]);ps.setInt(salary[n]);ps.addBatch();}ps.executeBatch();
  • 在例2中,当在 100次INSERT操作中使用 addBatch()方法时,只有 2次网络往返
  • 1次往返是预储 PreparedStatement,另一次是执行 batch命令
  • 虽然 Batch命令会用到更多的数据库的 CPU周期,但是通过减少网络往返,性能得到提高。

总结

JDBC的性能最大的增进是减少 JDBC驱动与数据库之间的网络通讯次数

注意

  • Oracel 10GJDBC Driver限制最大 Batch size16383条
  • 如果 addBatch超过这个限制,那么 executeBatch时就会出现 “无效的批值”(Invalid Batch Value)异常。
  • 因此在如果使用的是 Oracle10G,在此 bug减少前,Batch size需要控制在一定的限度。

  • 同样 mysql 5.5.28批量执行的数据最大限度是多少不清楚,但自己试了 1w2w3w都没问题,
  • 记得在 url后面添加:rewriteBatchedStatements=true表示批量插入,
  • 如果不添加的话即使使用 addbatch()executeBatch()在后台入库的地方还是不会一次请求入库而是多次请求入库。

3、选择合适的光标类型

  • 合适的光标类型以最大限度的适用你的应用程序,本节主要讨论三种光标类型的性能问题。
  • 对于从一个表中顺序读取所有记录的情况来说,Forward-Only型的光标提供了最好的性能。
  • 获取表中的数据时,没有哪种方法比使用 Forward-Only型的光标更快。
  • 但不管怎样,当程序中必须按无次序的方式处理数据行时,这种光标就无法使用了。

  • 对于程序中要求与数据库的数据同步以及要能够在结果集中前后移动光标,使用 JDBCScroll-Insensitive型光标是较理想的选择。
  • 此类型的光标在第一次请求时就获取了所有的数据(当 JDBC驱动采用 'lazy'方式获取数据时或许是很多的而不是全部的数据)并且储存在客户端。
  • 因此,第一次请求会非常慢,特别是请求长数据时会理严重。
  • 而接下来的请求并不会造成任何网络往返(当使用 'lazy'方法时或许只是有限的网络交通) 并且处理起来很快。
  • 因为第一次请求速度很慢,Scroll-Insensitive型光标不应该被使用在单行数据的获取上。
  • 当有要返回长数据时,开发者也应避免使用 Scroll-Insensitive型光标,因为这样可能会造成内存耗尽。
  • 有些 Scroll-Insensitive型光标的实现方式是在数据库的临时表中缓存数据来避免性能问题,但多数还是将数据缓存在应用程序中。

  • Scroll-Sensitive型光标,有时也称为 Keyset-Driven光标,使用标识符,像数据库的 ROWID之类。
  • 当每次在结果集移动光标时,会重新该标识符的数据。
  • 因为每次请求都会有网络往返,性能可能会很慢。
  • 无论怎样,用无序方式的返回结果行对性能的改善是没有帮助的。

  • 现在来解释一下一个程序要如何正常的返回 1000行数据到程序中。
  • 在执行时或者第一行被请求时,JDBC驱动不会执行程序提供的 SELECT语句
  • 相反,它会用键标识符来替换 SELECT查询,例如 ROWID
  • 然后修改过的查询都会被驱动程序执行,跟着会从数据库获取所有 1000个键值
  • 每一次对一行结果的请求都会使 JDBC驱动直接从本地缓存中找到相应的键值,然后构造一个包含了 'WHERE ROWID=?'子句的最佳化查询,再接着执行这个修改过的查询,最后从服务器取得该数据行。

  • 当程序无法像 Scroll-Insensitive型光标一样提供足够缓存时,Scroll-Sensitive型光标可以被替代用来作为动态的可滚动的光标。

4、使用有效的 getter 方法

  • JDBC提供多种方法从 ResultSet中取得数据,像 getInt()getString()getObject()等等,而 getObject()方法是最泛化了的,提供了最差的性能。
  • 这是因为 JDBC驱动必须对要取得的值的类型作额外的处理以映射为特定的对象,所以就对特定的数据类型使用相应的方法。
  • 要更进一步的改善性能,应在取得数据时提供字段的索引号,例如,getString(1)getLong(2)getInt(3)等来替代字段名。

  • 如果没有指定字段索引号,网络交通不会受影响,但会使转换和查找的成本增加。
  • 例如,假设你使用 getString("foo")JDBC驱动可能会将字段名转为大写(如果需要),并且在到字段名列表中逐个比较来找到 "foo"字段。

  • 如果可以,直接使用字段索引,将为你节省大量的处理时间。
  • 例如,假设你有一个 100行15列ResultSet,字段名不包含在其中。
  • 你感兴趣的是三个字段 EMPLOYEENAME(字串型)EMPLOYEENUMBER(长整型)SALARY(整型),你指定getString(“EmployeeName”)getLong(“EmployeeNumber”)getInt(“Salary”),查询每个字段名必须被转换为 metadata中相对应的大小写,然后才进行查找,如果你使用 getString(1)getLong(2)getInt(15),性能就会有显著改善.

5、获取自动生成的键值

  • 有许多数据库提供了隐藏列为表中的每行记录分配一个唯一键值。
  • 很典型,在查询中使用这些字段类型是取得记录值的最快的方式,因为这些隐含列通常反应了数据在磁盘上的物理位置。

例3:JDBC3.0 之前

JDBC3.0之前,应用程序只可在插入数据后通过立即执行一个 SELECT语句来取得隐含列的值.

// 插入行  introwcount =stmt.executeUpdate("insert into LocalGeniusList (name) values ('Karen')");// 现在为新插入的行取得磁盘位置 - rowid  ResultSetrs =stmt.executeQuery("select rowid from LocalGeniusList where name = 'Karen'");

这种取得隐含列的方式有两个主要缺点
1、取得隐含列是在一个独立的查询中,它要透过网络送到服务器后再执行。
2、因为不是主键,查询条件可能不是表中的 唯一性ID。在后个例子中,可能返回了多个隐含列的值,程序无法知道哪个是最后插入的行的值。

译者:
1、由于不同的数据库支持的程度不同,返回 rowid的方式各有差异。
2、在 SQL Server中,返回最后插入的记录的 id可以用这样的查询语句:SELECT @IDENTITY

例4:JDBC3.0 之后

JDBC3.0规范中的一个可选特性提供了一种能力,可以取得刚刚插入到表中的记录的自动生成的键值。

introwcount =stmt.executeUpdate("insert into LocalGeniusList (name) values ('Karen')",// 插入行并返回键值  Statement.RETURN_GENERATED_KEYS);ResultSetrs =stmt.getGeneratedKeys();// 得到生成的键值  

现在,程序中包含了一个 唯一性ID,可以用来作为查询条件来快速的存取数据行,甚至于表中没有主键的情况也可以。
这种取得自动生成的键值的方式给 JDBC的开发者提供了灵活性,并且使存取数据的性能得到提升。

6、选择合适的数据类型

  • 接收和发送某些数据可能代价昂贵。当你设计一个 schema时,应选择能被最有效地处理的数据类型。
  • 例如,整型数就比浮点数或实数处理起来要快一些。
  • 浮点数的定义是按照数据库的内部规定的格式,通常是一种压缩格式。
  • 数据必须被解压和转换到另外种格式,这样它才能被数据的协议处理。

7、获取 ResultSet

  • 由于数据库系统对可滚动光标的支持有限,许多 JDBC驱动程序并没有实现可滚动光标。
  • 除非你确信数据库支持可滚动光标的结果集,否则不要调用 rs.last()rs.getRow()方法去找出数据集的最大行数。
  • 因为 JDBC驱动程序模拟了可滚动光标,调用 rs.last()导致了驱动程序透过网络移到了数据集的最后一行。
  • 取而代之,你可以用 ResultSet遍历一次计数或者用 SELECT查询COUNT函数来得到数据行数。
  • 通常情况下,请不要写那种依赖于结果集行数的代码,因为驱动程序必须获取所有的数据集以便知道查询会返回多少行数据。

三、PreparedStatement 防止 sql 注入

JDBC应用中,如果你已经是稍有水平开发者,你就应该始终以 PreparedStatement代替 Statement
也就是说,在任何时候都不要使用 Statement。基于以下的原因:

1、代码的可读性和可维护性

虽然用 PreparedStatement来代替 Statement会使代码多出几行,但这样的代码无论从可读性还是可维护性上来说,都比直接用 Statement的代码高很多档次

stmt.executeUpdate("insert into tb_name (col1,col2,col2,col4) values ('"+var1+"','"+var2+"',"+var3+",'"+var4+"')");
perstmt =con.prepareStatement("insert into tb_name (col1,col2,col2,col4) values (?,?,?,?)");perstmt.setString(1,var1);perstmt.setString(2,var2);perstmt.setString(3,var3);perstmt.setString(4,var4);perstmt.executeUpdate();

不用我多说,对于第一种方法,别说其他人去读你的代码,就是你自己过一段时间再去读,都会觉得伤心。

2、PreparedStatement 尽最大可能提高性能

  • 每一种数据库都会尽最大努力对预编译语句提供最大的性能优化。
  • 因为预编译语句有可能被重复调用,所以语句在被DB的编译器编译后的执行代码被缓存下来,
  • 那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行。
  • 这并不是说只有一个 Connection中多次执行的预编译语句被缓存,而是对于整个 DB中,只要预编译的语句语法和缓存中匹配。
  • 那么在任何时候就可以不需要再次编译而可以直接执行。
  • Statement的语句中,即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配。比如:
insertintotb_name (col1,col2)values('11','22');insertintotb_name (col1,col2)values('11','23');
  • 即使是相同操作但因为数据内容不一样,所以整个个语句本身不能匹配,没有缓存语句的意义。
  • 事实是没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。
  • 当然并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略,
  • 比如使用频度等因素来决定什么时候不再缓存已有的预编译结果,以保存有更多的空间存储新的预编译语句。

3、最重要的一点是极大地提高了安全性

即使到目前为止,仍有一些人连基本的 恶义SQL语法都不知道。

Stringsql ="select * from tb_name where name= '"+varname+"' and passwd='"+varpasswd+"'"

如果我们把 [' or '1' = '1]作为 var passwd传入进来,用户名 随意,看看会成为什么?

select*fromtb_name ='随意'andpasswd =''or'1'='1'

因为 '1'='1'肯定成立,所以可以任何通过验证,更有甚至把 [';drop table tb_name;]作为 var passwd传入进来,则

select*fromtb_name ='随意'andpasswd ='';droptabletb_name
  • 有些数据库是不会让你成功的,但也有很多数据库就可以使这些语句得到执行。
  • 而如果你使用预编译语句,你传入的任何内容就不会和原来的语句发生任何匹配的关系
  • 前提是数据库本身支持预编译,但上前可能没有什么服务端数据库不支持编译了,只有少数的桌面数据库,就是直接文件访问的那些
  • 只要全使用预编译语句,你就用不着对传入的数据做任何过滤。
  • 而如果使用普通的 Statement,有可能要对 drop,等做费尽心机的判断和过滤。
  • 上面的几个原因,还不足让你在任何时候都使用 PreparedStatement吗?

四、总结

上面是三篇文章,三篇文章详细介绍了PreparedStatementStatement两个对象的使用以及效率、安全问题。

1、在实际项目中如果能够使用 PreparedStatement 还是建议使用 PreparedStatement 的原因

1、上面说了如果 sql中只有数值在变则效率高
2、PreparedStatement具有 防sql注入
3、代码可读性比较好

2、实例易理解(可以看出在批量添加的时候 PreparedStatement 为什么比 Statement 快的原因)

PreparedStatement 的 addBatch 和 executeBatch 实现批量添加

1、建立链接

Connectionconnection =getConnection();

2、不自动 Commit(瓜子不是一个一个吃,全部剥开放桌子上,然后一口舔了)

connection.setAutoCommit(false);

3、预编译SQL语句,只编译一回哦,效率高啊(发明一个剥瓜子的方法,以后不要总想怎么剥瓜子好,就这样剥)

PreparedStatementstatement =connection.prepareStatement("INSERT INTO TABLEX VALUES(?, ?)");

4、来一个剥一个,然后放桌子上

Connectionconnection =getConnection();Statementstatement =connection.createStatement();// 记录1statement.setInt(1,1);statement.setString(2,"Cujo");statement.addBatch();// 记录2statement.setInt(1,2);statement.setString(2,"Fred");statement.addBatch();// 记录3statement.setInt(1,3);statement.setString(2,"Mark");statement.addBatch();// 批量执行上面 3 条语句,一口吞了,很爽int[]counts =statement.executeBatch();// Commit it 咽下去,到肚子(DB)里面connection.commit();

5、statement 对象的 addBatch 和 executeBatch 来实现批量添加

Connectionconnection =getConnection();Statementstmt =connection.createStatement();stmt.addBatch("update  TABLE1 set 题目="盛夏话足部保健1"   where id="3407"");stmt.addBatch("update  TABLE1 set 题目="夏季预防中暑膳食1" where id="3408"");stmt.addBatch("INSERT INTO  TABLE1  VALUES("11","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("12","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("13","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("14","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("15","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("16","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("17","12","13","","")");stmt.addBatch("INSERT INTO  TABLE1  VALUES("18","12","13","","")");int[]updateCounts =stmt.executeBatch();connection.commit();

6、实例:批量添加

publicstaticvoidinsertData(List<Map<String,String>>list,Loggerlog){ // 获取的数据  List<Map<String,String>>nlist =list;Stringupsql ="update hrd_staff set position=? where id=?";Iterator<Map<String,String>>iter =nlist.iterator();Connectioncon =Utils.getCon();intcount =0;try{ // 在数据添加的时候注意事务提交方式  con.setAutoCommit(false);// PreparedStatement方法的使用  PreparedStatementpstm =con.prepareStatement(upsql);while(iter.hasNext()){ count++;Map<String,String>map =iter.next();Stringjon_name =map.get("job_name");Stringuid =map.get("uid");pstm.setString(1,jon_name);pstm.setString(2,uid);// 添加到缓存中  pstm.addBatch();// 如果数据量很大,不能一次性批量添加所以我们要分批次添加,这里就是300条一次  if(count %300==0){ // 持久化  int[]res =pstm.executeBatch();// 提交事务,持久化数据  con.commit();pstm.clearBatch();log.info("300整除插入结果: "+res.length);}}// 小于300条的在这里持久化  int[]ress =pstm.executeBatch();// 事务提交持久化  con.commit();pstm.clearBatch();log.info("插入数据结果:"+ress.length);}catch(SQLExceptione){ try{ con.rollback();}catch(SQLExceptione1){ // TODO Auto-generated catch block  e1.printStackTrace();}e.printStackTrace();}finally{ try{ if(null!=con){ con.close();con.setAutoCommit(true);}}catch(SQLExceptione){ // TODO Auto-generated catch block  e.printStackTrace();}}}

注意:这里除了下面说的 url中的批量设置外,我们也要注意事务的设置,不能设置为自动提交,要批量添加后在提交事务

7、总结

  • addBatch()就是把你的处理内容添加到批处理单元中,即添加到了 batch中。
  • 你可以循环加入很多,数据库都不会处理,直到调用如下代码 executeBatch()此时,数据库把刚才加到 batch中的命令批量处理。

使用批量插入的好处:

  • 当在 100次INSERT操作中使用 addBatch()方法时,只有 两次网络往返,一次往返是预储 statement,另一次是执行 batch命令
  • 虽然 Batch命令会用到更多的数据库的 CPU周期,但是通过减少网络往返,性能得到提高。
  • 记住,JDBC的性能最大的增进是减少 JDBC驱动与数据库之间的网络通讯。
  • 如果没有使用批处理则 网络往返101次这样会耗很多时间,自然效率也就一般。

这里要注意:
mysql下使用批量执行的时候要在,url后面添加手动设置支持批量添加,实例如下:

Stringurl ="jdbc:mysql://localhost:3306/music?rewriteBatchedStatements=true";

默认情况下 rewriteBatchedStatements的值为 false,也就是批量添加功能是关闭的,如果使用则要手动开启!
还有就是事务的设置,不能使自动提交,要批量添加后才提交!!!

参考

Statement、PreparedStatement 的用法和解释

(责编:人民网)

分享让更多人看到