JDBC中PrepareStatement 和Statement

注:本文摘抄自:https://blog.csdn.net/QH_JAVA/article/details/48245945

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

  第一:

    prepareStatement会先初始化SQL,先把这个SQL提交到数据库中进行预处理,多次使用可提高效率

    Statement不会初始化,没有预处理,没次都是从0开始执行SQL.

  第二:

    prepareStatement可以替换变量 :

    在SQL语句中可以包含?,可以用ps=conn.prepareStatement(“select * from Cust where ID=?“);  
    int sid=1001; 

    ps.setInt(1, sid); 
    rs = ps.executeQuery(); 

    可以把?替换成变量。 
    而Statement只能用 
    int sid=1001; 
    Statement stmt = conn.createStatement(); 
    ResultSet rs = stmt.executeQuery(“select * from Cust where ID=”+sid); 
    来实现。

使用PrepareStatement 和Statement的区别:

  1.statement每次执行sql语句,相关数据库都要执行sql语句的编译,preparedstatement是预编译得, preparedstatement支持批处理,对于批量处理可以大大提高效率. 

  2.在对数据库只执行一次性存取的时侯,可以优先使用 Statement 对象进行处理。因为PreparedStatement 对象的开销比Statement大,对于一次性操作并不会带来额外的好处。

  3.当你需要执行Statement对象多次的时候,PreparedStatement对象将会大大降低运行时间,当然也加快了访问数据库的速度。

  这种转换也给你带来很大的便利,不必重复SQL语句的句法,而只需更改其中变量的值,便可重新执行SQL语句。选择PreparedStatement对象与否,在于相同句法的SQL语句是否执行了多次,而且两次之间的差别仅仅是变量的不同。如果仅仅执行了一次的话,它应该和普通的对象毫无差异,体现不出它预编译的优越性。

  4.在Oracle环境中,开发人员实际上有更大的灵活性。当使用Statement或PreparedStatement对象时,Oracle数据库会缓存SQL语句以便以后使用。在一些情况下,由于驱动器自身需要额外的处理和在Java应用程序和Oracle服务器间增加的网络活动,执行PreparedStatement对象实际上会花更长的时间。然而,除了缓冲的问题之外,至少还有一个更好的原因使我们在企业应用程序中更喜欢使用PreparedStatement对象,那就是安全性。传递给PreparedStatement对象的参数可以被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。

PrepareStatement和Statement在实际应用中该如何选择?

  1.优先选择使用Statement而不是PreparedStatement对象:

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

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

  2.使用PreparedStatement的Batch功能

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

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

1 PreparedStatement ps = conn.prepareStatement(  
2    "INSERT into employees values (?, ?, ?)");   
3 for (n = 0; n < 100; n++) {   
4    ps.setString(name[n]);  
5    ps.setLong(id[n]);  
6    ps.setInt(salary[n]);  
7    ps.executeUpdate();  
8 }  

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

1 PreparedStatement ps = conn.prepareStatement(  
2    "INSERT into employees values (?, ?, ?)");  
3 for (n = 0; n < 100; n++) {  
4     ps.setString(name[n]);  
5     ps.setLong(id[n]);  
6     ps.setInt(salary[n]);  
7     ps.addBatch();  
8 }  
9 ps.executeBatch();  

  分析:

  在例 1中, PreparedStatement被用来多次执行INSERT语句. 在这里, 执行了100次INSERT操作, 共有101次网络往返.其中,1次往返是预储PreparedStatement, 另外100次往返执行每个迭代.

  在例2中, 当在100次INSERT操作中使用addBatch()方法时, 只有两次网络往返.1次往返是预储PreparedStatement, 另一次是执行batch命令. 虽然Batch命令会用到更多的数据库的CPU周期, 但是通过减少网络往返,性能得到提高.记住, JDBC的性能最大的增进是减少JDBC驱动与数据库之间的网络通讯次数.

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

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

  3.使用有效的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). 性能就会有显著改善.

  4.获取自动生成的键值

    有许多数据库提供了隐藏列为表中的每行记录分配一个唯一键值. 很典型, 在查询中使用这些字段类型是取得记录值的最快的方式, 因为这些隐含列通常反应了数据在磁盘上的物理位置. 在JDBC3.0之前, 应用程序只可在插入数据后通过立即执行一个SELECT语句来取得隐含列的值.

  例 3: JDBC3.0之前

1 //插入行  
2 int rowcount = stmt.executeUpdate (  
3    "insert into LocalGeniusList (name) values (‘Karen‘)");  
4 // 现在为新插入的行取得磁盘位置 - rowid  
5 ResultSet rs = stmt.executeQuery (  
6    "select rowid from LocalGeniusList where name = ‘Karen‘");  

  这种取得隐含列的方式有两个主要缺点.

  第一, 取得隐含列是在一个独立的查询中, 它要通过网络送到服务器后再执行.

  第二, 因为不是主键, 查询条件可能不是表中的唯一性ID. 在后面一个例子中, 可能返回了多个隐含列的值, 程序无法知道哪个是最后插入的行的值.

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

  例 4: JDBC3.0之后 

1 // 插入行并返回键值 
2 int rowcount = stmt.executeUpdate (  
3    "insert into LocalGeniusList (name) values (‘Karen‘)", Statement.RETURN_GENERATED_KEYS);  
4 // 得到生成的键值 
5 ResultSet rs = stmt.getGeneratedKeys (); 

  现在, 程序中包含了一个唯一性ID, 可以用来作为查询条件来快速的存取数据行, 甚至于表中没有主键的情况也可以.

  这种取得自动生成的键值的方式给JDBC的开发者提供了灵活性, 并且使存取数据的性能得到提升.

Preparestatement 使用的好处:

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

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

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

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

  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、最重要的一点是极大地提高了安全性.

  对于一些恶意语句:String sql = “select * from tb_name where name= ‘”+varname+”‘ and passwd=‘”+varpasswd+”‘”;如果我们把[‘ or ‘1‘ = ‘1]作为varpasswd传入进来.因为‘1‘=‘1‘肯定成立,所以可以任何通过验证.

  如果你使用预编译语句.你传入的任何内容就不会和原来的语句发生任何匹配的关系.(前提是数据库本身支持预编译,但目前可能没有什么服务端数据库不支持预编译了,只有少数的桌面数据库,就是直接文件访问的那些)只要全使用预编译语句,你就用不着对传入的数据做任何过虑.而如果使用普通的statement, 有可能要对drop,or 1=1;等做费尽心机的判断和过虑.

PreparedStatement最重要的addbatch()结构的使用.

  PreparedStatement  的addBatch和executeBatch实现批量添加.

  1.建立链接  

    Connection    connection =getConnection();

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

    connection.setAutoCommit(false);   

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

    PreparedStatement statement = connection.prepareStatement(“INSERT INTO TABLEX VALUES(?, ?)”);   

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

    //记录1

    statement.setInt(1, 1);
    statement.setString(2, “Cujo”);
    statement.addBatch();   

    //记录2

    statement.setInt(1, 2);
    statement.setString(2, “Fred”);
    statement.addBatch();   

    //记录3

    statement.setInt(1, 3);
    statement.setString(2, “Mark”);
    statement.addBatch();   

    //批量执行上面3条语句. 一口吞了,很爽

    int [] counts = statement.executeBatch();   

    //Commit it 咽下去,到肚子(DB)里面

    connection.commit();

 

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

      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();

    cn.commit();

  实例:批量添加

 

 1 public static void insertData(List<Map<String,String>> list,Logger log){
 2     //获取的数据
 3     List <Map<String,String>> nlist= list;
 4     String upsql="update   hrd_staff  set position =?  where id=?";
 5     Iterator<Map<String,String>> iter= nlist.iterator();
 6     Connection con= Utils.getCon();
 7     int count=0;
 8     try {
 9         //在批量添加的时候注意事务提交方式
10         con.setAutoCommit(false);
11         //PreparedStatement方法的使用
12         PreparedStatement pstm = con.prepareStatement(upsql);
13         while(iter.hasNext()){
14             count++;
15             Map<String,String> map= iter.next();
16             String jon_name= map.get("job_name");
17             String uid= map.get("uid");
18             pstm.setString(1,jon_name);
19             pstm.setString(2,uid);
20             //添加到缓存中
21             pstm.addBatch();
22             // 如果数据量很大,不能一次性批量添加所以我们要分批次添加,这里就是300条一次
23             if(count%300==0){
24                 //持久化
25                 int []res=pstm.executeBatch();
26                 //提交事务,持久化数据
27                 con.commit();
28                 pstm.clearBatch();
29                 log.info("300整除插入结果: "+res.length);
30             }
31         }
32         //小于300条的在这里持久化
33         int []ress= pstm.executeBatch();
34         //事务提交持久化
35         con.commit();
36         pstm.clearBatch();
37         log.info("插入数据结果:"+ress.length);
38     } catch (SQLException e) {
39         try {
40             con.rollback();
41         } catch (SQLException e1) {
42             // TODO Auto-generated catch block
43             e1.printStackTrace();
44         }
45         e.printStackTrace();
46     }finally{
47         try {
48             if(null!=con){
49             con.close();
50             con.setAutoCommit(true);
51             }
52         } catch (SQLException e) {
53             // TODO Auto-generated catch block
54             e.printStackTrace();
55         }
56     }
57 }

 

  我们也要注意事务的设置,不能设置为自动提交,要批量添加后在提交事务

  总结:

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

  使用批量插入的好处:

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

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

  String url=”jdbc:mysql://localhost:3306/music?rewriteBatchedStatements=true“;

  默认情况下rewriteBatchedStatements 的值为false 也就是批量添加功能是关闭的,如果使用则要手动开启!

  还有就是事务的设置,不能使自动提交,要批量添加后才提交!!!