一、PreparedStatement 和 Statement 的含义和区别
1、含义
PreparedStatement
:PreparedStatement
是预编译的,对于批量处理可以大大提高效率,也叫JDBC存储过程
。Statement
:使用Statement
对象,在对数据库只执行一次性存取的时侯,用Statement
对象进行处理。
2、区别
区别一:开销
PreparedStatement
对象的开销比Statement
大,对于一次性操作并不会带来额外的好处。
区别二:编译
Statement
每次执行sql语句
,相关数据库都要执行sql语句
的编译,而PreparedStatement
是预编译的,且支持批处理。
代码片段一
Statement stmt = conn.createStatement();
String updateString = "UPDATE COFFEES SET SALES = 75 " + "WHERE COF_NAME LIKE ′Colombian′";
stmt.executeUpdate(updateString);
代码片段二
PreparedStatement updateSales = 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程序
产生大量的Statement
和PreparedStatement
对象。 - 通常认为
PreparedStatement
对象比Statement
对象更有效,特别是如果带有不同参数的同一SQL语句
被多次执行的时候。 PreparedStatement
对象允许数据库预编译SQL语句
,这样在随后的运行中可以节省时间并增加代码的可读性。
特殊情况区别四:缓存
- 在
Oracle
环境中,开发人员实际上有更大的灵活性。 - 当使用
Statement
或PreparedStatement
对象时,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=?"); 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);
二、深入理解 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
的调用次数改善性能,你可以使用PreparedStatement
的AddBatch()方法
一次性发送多个查询给数据库。 - 例如, 让我们来比较一下下面的例子
例1:多次执行 PreparedStatement,多次数据库请求(网络请求)
PreparedStatement ps = 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,以此请求执行多条
PreparedStatement ps = 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 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、选择合适的光标类型
- 合适的光标类型以最大限度的适用你的应用程序,本节主要讨论三种光标类型的性能问题。
- 对于从一个表中顺序读取所有记录的情况来说,
Forward-Only型
的光标提供了最好的性能。 - 获取表中的数据时,没有哪种方法比使用
Forward-Only型
的光标更快。 - 但不管怎样,当程序中必须按无次序的方式处理数据行时,这种光标就无法使用了。
- 对于程序中要求与数据库的数据同步以及要能够在结果集中前后移动光标,使用
JDBC
的Scroll-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语句
来取得隐含列的值.
// 插入行
int rowcount = stmt.executeUpdate("insert into LocalGeniusList (name) values ('Karen')");
// 现在为新插入的行取得磁盘位置 - rowid
ResultSet rs = 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
规范中的一个可选特性提供了一种能力,可以取得刚刚插入到表中的记录的自动生成的键值。
int rowcount = stmt.executeUpdate("insert into LocalGeniusList (name) values ('Karen')",
// 插入行并返回键值
Statement.RETURN_GENERATED_KEYS);
ResultSet rs = 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
的语句中,即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配。比如:
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]
作为 var passwd
传入进来,用户名 随意
,看看会成为什么?
select * from tb_name = '随意' and passwd = '' or '1' = '1'
因为 '1'='1'
肯定成立,所以可以任何通过验证,更有甚至把 [';drop table tb_name;]
作为 var passwd
传入进来,则
select * from tb_name = '随意' and passwd = '';drop table tb_name
- 有些数据库是不会让你成功的,但也有很多数据库就可以使这些语句得到执行。
- 而如果你使用预编译语句,你传入的任何内容就不会和原来的语句发生任何匹配的关系
- 前提是数据库本身支持预编译,但上前可能没有什么服务端数据库不支持编译了,只有少数的桌面数据库,就是直接文件访问的那些
- 只要全使用预编译语句,你就用不着对传入的数据做任何过滤。
- 而如果使用普通的
Statement
,有可能要对drop
,等做费尽心机的判断和过滤。 - 上面的几个原因,还不足让你在任何时候都使用
PreparedStatement
吗?
四、总结
上面是三篇文章,三篇文章详细介绍了PreparedStatement
和 Statement
两个对象的使用以及效率、安全问题。
1、在实际项目中如果能够使用 PreparedStatement 还是建议使用 PreparedStatement 的原因
1、上面说了如果 sql
中只有数值在变则效率高
2、PreparedStatement
具有 防sql注入
3、代码可读性比较好
2、实例易理解(可以看出在批量添加的时候 PreparedStatement 为什么比 Statement 快的原因)
PreparedStatement 的 addBatch 和 executeBatch 实现批量添加
1、建立链接
Connection connection = getConnection();
2、不自动 Commit(瓜子不是一个一个吃,全部剥开放桌子上,然后一口舔了)
connection.setAutoCommit(false);
3、预编译SQL语句,只编译一回哦,效率高啊(发明一个剥瓜子的方法,以后不要总想怎么剥瓜子好,就这样剥)
PreparedStatement statement = connection.prepareStatement("INSERT INTO TABLEX VALUES(?, ?)");
4、来一个剥一个,然后放桌子上
Connection connection = getConnection();
Statement statement = connection.createStatement();
// 记录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();
5、statement 对象的 addBatch 和 executeBatch 来实现批量添加
Connection connection = getConnection();
Statement stmt = 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、实例:批量添加
public static void insertData(List<Map<String, String>> list, Logger log) {// 获取的数据 List<Map<String, String>> nlist = list;String upsql = "update hrd_staff set position=? where id=?";Iterator<Map<String, String>> iter = nlist.iterator();Connection con = Utils.getCon();int count = 0;try {// 在数据添加的时候注意事务提交方式 con.setAutoCommit(false);// PreparedStatement方法的使用 PreparedStatement pstm = con.prepareStatement(upsql);while (iter.hasNext()) {count++;Map<String, String> map = iter.next();String jon_name = map.get("job_name");String uid = 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 (SQLException e) {try {con.rollback();} catch (SQLException e1) {// TODO Auto-generated catch block e1.printStackTrace();}e.printStackTrace();} finally {try {if (null != con) {con.close();con.setAutoCommit(true);}} catch (SQLException e) {// 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 后面添加手动设置支持批量添加,实例如下:
String url = "jdbc:mysql://localhost:3306/music?rewriteBatchedStatements=true";
默认情况下 rewriteBatchedStatements
的值为 false
,也就是批量添加功能是关闭的,如果使用则要手动开启!
还有就是事务的设置,不能使自动提交,要批量添加后才提交!!!
参考
Statement、PreparedStatement 的用法和解释