深入理解PreparedStatement和Statement

预编译

  • MySQL数据库服务器的预编译功能在4.1之后才支持预编译功能的,4.1之前的版本是不支持预编译的。如果数据库服务器不支持预编译功能时,并且使用PreparedStatement开启预编译功能是会抛出异常的。
  • MySQL驱动5.0.5以后的版本默认PreparedStatement是关闭预编译功能的,所以需要我们手动开启。而之前的JDBCMySQL驱动版本默认是开启预编译功能的。

预编译的好处:

大家平时都使用过JDBC中的PreparedStatement接口,它有预编译功能。什么是预编译功能呢?它有什么好处呢?
当客户发送一条SQL语句给服务器后,服务器总是需要校验SQL语句的语法格式是否正确,然后把SQL语句编译成可执行的函数,最后才是执行SQL语句。其中校验语法,和编译所花的时间可能比执行SQL语句花的时间还要多。
注意:可执行函数存储在MySQL服务器中,并且当前连接断开后,MySQL服务器会清除已经存储的可执行函数。
如果我们需要执行多次insert语句,但只是每次插入的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时间。如果使用预编译功能,那么只对SQL语句进行一次语法校验和编译,所以效率要高。

在PreparedStatement中开启预编译功能

我们一般用的PreparedStatement并没有用到预编译功能的,只是用到了防止sql注入攻击的功能。防止sql注入攻击的实现是在PreparedStatement中实现的,和服务器无关。笔者在源码中看到,PreparedStatement对敏感字符已经转义过了。

在我们以前写项目的时候,貌似都没有注意是否开启PreparedStatement的预编译功能,以为它一直都是在使用的。下面在PreparedStatement中开启预编译功能:

  • 设置MySQL连接URL参数:useServerPrepStmts=true,如下所示。
    jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true
  • 这样才能保证mysql驱动会先把SQL语句发送给服务器进行预编译,然后在执行executeQuery()时只是把参数发送给服务器。

注意:
我们设置的是MySQL连接参数,目的是告诉MySQL JDBC的PreparedStatement使用预编译功能(5.0.5之后的JDBC驱动版本需要手动开启,而之前的默认是开启的),不管我们是否使用预编译功能,MySQL Server4.1版本以后都是支持预编译功能的。

cachePrepStmts参数

当使用不同的PreparedStatement对象来执行相同的SQL语句时,还是会出现编译两次的现象,这是因为驱动没有缓存编译后的函数key,导致二次编译。如果希望缓存编译后函数的key,那么就要设置cachePrepStmts参数为true。例如:jdbc:mysql://localhost:3306/mybatis?useServerPrepStmts=true&cachePrepStmts=true
**注意:**每次使用PreparedStatement对象后都要关闭该PreparedStatement对象流,否则预编译后的函数key是不会缓存的。

MySQL执行预编译

MySQL执行预编译分为如三步:
1.执行预编译语句,例如:prepare showUsersByLikeName from ‘select * from user where username like ?’;
2.设置变量,例如:set @username=’%小明%’;
3.执行语句,例如:execute showUsersByLikeName using @username;

如果需要再次执行myfun,那么就不再需要第一步,即不需要再编译语句了:
1.设置变量,例如:set @username=’%小宋%’;
2.执行语句,例如:execute showUsersByLikeName using @username;

总结:

  • 所以到了这里我的疑惑都解开了,PreparedStatement的预编译是数据库进行的,编译后的函数key是缓存在PreparedStatement中的,编译后的函数是缓存在数据库服务器中的。预编译前有检查sql语句语法是否正确的操作。只有数据库服务器支持预编译功能时,JDBC驱动才能够使用数据库的预编译功能,否则会报错。预编译在比较新的JDBC驱动版本中默认是关闭的,需要配置连接参数才能够打开。在已经配置好了数据库连接参数的情况下,Statement对于MySQL数据库是不会对编译后的函数进行缓存的,数据库不会缓存函数,Statement也不会缓存函数的key,所以多次执行相同的一条sql语句的时候,还是会先检查sql语句语法是否正确,然后编译sql语句成函数,最后执行函数。
  • 对于PreparedStatement在设置参数的时候会对参数进行转义处理。
  • 因为PreparedStatement已经对sql模板进行了编译,并且存储了函数,所以PreparedStatement做的就是把参数进行转义后直接传入参数到数据库,然后让函数执行。这就是为什么PreparedStatement能够防止sql注入攻击的原因了。
  • PreparedStatement的预编译还有注意的问题,在数据库端存储的函数和在PreparedStatement中存储的key值,都是建立在数据库连接的基础上的,如果当前数据库连接断开了,数据库端的函数会清空,建立在连接上的PreparedStatement里面的函数key也会被清空,各个连接之间的预编译都是互相独立的。

在持久层框架中存在的问题

很多主流持久层框架(MyBatis,Hibernate)其实都没有真正的用上预编译,预编译是要我们自己在参数列表上面配置的,如果我们不手动开启,JDBC驱动程序5.0.5以后版本 默认预编译都是关闭的。

所以我们要在参数列表中配置,例如:jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true

注意:
在MySQL中,既要开启预编译也要开启缓存。因为如果只是开启预编译的话效率还没有不开启预编译效率高,大家可以做一下性能测试,其中性能测试结果在这篇博客中有写到,探究mysql预编译,而在MySQL中开启预编译和开启缓存,其中的查询效率和不开启预编译和不开启缓存的效率是持平的。这里用的测试类是PreparedStatement。

使用PreparedStatement的Batch功能

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

为了区分 “Statement、PreparedStatement、PreparedStatement + 批处理” 这三者之间的效率,下面的示例执行过程都是在数据库表t1中插入1万条记录,并记录出所需的时间(此时间与电脑硬件有关)。实验结果如下:
1.使用Statement对象 用时312.预编译PreparedStatement 用时143.使用PreparedStatement + 批处理   用时485毫秒***
-------------------------------------------------------
1.使用Statement对象
使用范围:当执行相似SQL(结构相同,具体值不同)语句的次数比较少
优点:语法简单
缺点:采用硬编码效率低,安全性较差。
原理:硬编码,每次执行时相似SQL都会进行编译   

示例执行过程:
   public void exec(Connection conn){
        try {
                Long beginTime = System.currentTimeMillis();
                conn.setAutoCommit(false);//设置手动提交
                Statement st = conn.createStatement();
                for(int i=0;i<10000;i++){
                   String sql="insert into t1(id) values ("+i+")";
                   st.executeUpdate(sql);
                }
                Long endTime = System.currentTimeMillis();
                System.out.println("Statement用时:"+(endTime-beginTime)/1000+"秒");//计算时间
                st.close();
               conn.close();
        } catch (SQLException e) {             
                  e.printStackTrace();
        }
   }
执行时间:Statement用时:31----------------------------------------------------------------
2.预编译PreparedStatement
使用范围:当执行相似sql语句的次数比较多(例如用户登陆,对表频繁操作..)语句一样,只是具体的值不一样,被称为动态SQL
优点:语句只编译一次,减少编译次数。提高了安全性(阻止了SQL注入)
缺点: 执行非相似SQL语句时,速度较慢。
原理:相似SQL只编译一次,减少编译次数
事例执行过程:
      public void exec2(Connection conn){
         try {
                Long beginTime = System.currentTimeMillis();
                conn.setAutoCommit(false);//手动提交
                PreparedStatement pst = conn.prepareStatement("insert into t1(id) values (?)");
                //占位符的索引位置从1开始而不是0,如果填入0会导致java.sql.SQLException invalid column index异常。
                //所以如果PreparedStatement有两个占位符,那么第一个参数的索引时1,第二个参数的索引是2.
                for(int i=0;i<10000;i++){
                       pst.setInt(1, i);
                       pst.execute();   
                }
                conn.commit();
                Long endTime = System.currentTimeMillis();
                System.out.println("Pst用时:"+(endTime-beginTime)+"秒");//计算时间
                pst.close();
                conn.close();
         } catch (SQLException e) {               
                  e.printStackTrace();
         }
    }
执行时间:Pst用时:14------------------------------------------------------------------
3.使用PreparedStatement + 批处理
使用范围:一次需要更新数据库表多条记录
优点:减少和SQL引擎交互的次数,再次提高效率,相似语句只编译一次,减少编译次数。提高了安全性(阻止了SQL注入)
缺点:
原理:批处理: 减少和SQL引擎交互的次数,一次传递给SQL引擎多条SQL。
名词解释:
PL/SQL引擎:在oracle中执行pl/sql代码的引擎,在执行中发现标准的sql会交给sql引擎进行处理。
SQL引擎:执行标准sql的引擎。
事例执行过程:
public void exec3(Connection conn){
     try {
           conn.setAutoCommit(false);
           Long beginTime = System.currentTimeMillis();
           PreparedStatement pst = conn.prepareStatement("insert into t1(id) values (?)");

          for(int i=1;i<=10000;i++){   
                pst.setInt(1, i);
                pst.addBatch();//加入批处理,进行打包
                if(i%1000==0){//可以设置不同的大小;如50,100,500,1000等等
                      pst.executeBatch();
                      conn.commit();
                      pst.clearBatch();
                }//end of if
           }//end of for
           pst.executeBatch();
           Long endTime = System.currentTimeMillis();
           System.out.println("pst+batch用时:"+(endTime-beginTime)+"毫秒");
           pst.close();
           conn.close();
      } catch (SQLException e) {
            e.printStackTrace();
      }
}
执行时间:pst+batch用时:485毫秒

//代码转自:http://blog.itpub.net/90618/viewspace-607949/

总结PreparedStatement与Statement的区别

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

stmt.executeUpdate(“insert into tb_name (col1,col2,col2,col4) values (‘”+var1+”’,’”+var2+”’,+var3+,’”+var4+”’));//stmt是Statement对象实例

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(); //prestmt是 PreparedStatement 对象实例

2.PreparedStatement尽最大可能提高性能与执行效率
语句在被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个涵数)就会得到执行.这并不是说只有一个Connection中多次执行的预编译语句被缓存,而是对于整个DB中,只要预编译的语句语法和缓存中匹配.那么在任何时候就可以不需要再次编译而可以直接执行.而statement的语句中,即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配.比如:

insert into tb_name (col1,col2) values (11,22);
insert into tb_name (col1,col2) values (11,23);

即使是相同操作但因为数据内容不一样,所以整个个语句本身不能匹配,没有缓存语句的意义.事实是没有数据库会对普通语句编译后的执行代码缓存.

当然并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略,比如使用频度等因素来决定什么时候不再缓存已有的预编译结果.以保存有更多的空间存储新的预编译语句.

3.极大地提高了安全性
即使到目前为止,仍有一些人连基本的恶义SQL语法都不知道.

String sql = “select * from tb_name where name= ‘”+varname+”’ and passwd=’”+varpasswd+”’”;

如果我们把’ or ‘1’ = ‘1作为varpasswd传入进来.用户名随意,看看会成为什么?

select * from tb_name = ‘随意’ and passwd = ''or '1'= '1';

因为’1’=’1’肯定成立,所以可以任何通过验证.更有甚者:
‘;drop table tb_name;作为varpasswd传入进来,则:
select * from tb_name = ‘随意’ and passwd = ”;drop table tb_name;有些数据库是不会让你成功的,但也有很多数据库就可以使这些语句得到执行.

而如果你使用预编译语句.你传入的任何内容就不会和原来的语句发生任何匹配的关系.只要全使用预编译语句,你就用不着对传入的数据做任何过虑.而如果使用普通的statement,有可能要对drop,;等做费尽心机的判断和过虑.

参考资料:
原文链接:JDBC:深入理解PreparedStatement和Statement

你可能感兴趣的:(进化)