注:本文摘抄自: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 也就是批量添加功能是关闭的,如果使用则要手动开启!
还有就是事务的设置,不能使自动提交,要批量添加后才提交!!!