DBCP连接池可以缓存PreparedStatement,本质上就是缓存游标。
一个SQL语句,无论是Insert,Update,Delete还是Select都是游标操作,只不过Select游标指向查询结果,而其余的指向修改的目标。
除了连接可以缓存,游标也是可以缓存的,主要是避免游标的反复创建。虽然Oracle对完全相同的SQL可以共享执行计划,但是也需要去共享池查询这个SQL的信息(该SQL的Hash值是否在共享池内)。缓存游标,则进一步优化,避免了反复查询共享池的操作(个人臆测).
首先,做一个实验,证明游标可以反复利用。
--创建实验表
create table t as select rownum r from dual connect by level<10;
set serveroutput on
declare
cursor cur is select * from t;
v_record t%rowtype;
begin
open cur;
fetch cur into v_record;
dbms_output.put_line(v_record.r);
fetch cur into v_record;
dbms_output.put_line(v_record.r);
close cur;
open cur;
fetch cur into v_record;
dbms_output.put_line(v_record.r);
fetch cur into v_record;
dbms_output.put_line(v_record.r);
close cur;
end;
/
实验结果:
1
2
1
2
可以看到游标在关闭之后,可以重新打开。并且重新打开的游标,与前次打开的游标,在数据上没有任何关系。第一次读到2,重新打开之后,会从1开始,而不是从3开始。
这个代码如果在JAVA程序中,就是这个样子的。
-
Class.forName("oracle.jdbc.OracleDriver"); Connection conn = DriverManager.getConnection("jdbc:oracle:thin:127.0.0.1:1521:orcl", "edmond", "edmond"); PreparedStatement cmd = conn.prepareStatement("select * from t"); //第一次调用 ResultSet rs = cmd.executeQuery(); rs.next(); System.out.println(rs.getString(1)); rs.next(); System.out.println(rs.getString(1)); //第二次调用 rs = cmd.executeQuery(); rs.next(); System.out.println(rs.getString(1)); rs.next(); System.out.println(rs.getString(1)); cmd.close(); conn.close();
值得注意的是,PreparedStatement就表示Oracle的游标,但是一旦PreparedStatement关闭,就无法重新打开。所以复用PreparedStatement只需要在关闭之前重新调用executeQuery方法即可。
如果连接池启动PoolPreparedStatements,则可能在每一个Connection的代理对象中,包括下面的结构
Map> poolPreparedStatements
其中Key是SQL语句或者SQL语句的Hash值,代理的Connection会根据SQL返回一个可用的prepareStatement;如果没有,则会创建新的prepareStatement对象。而这个返回的prepareStatement对象,也同样是代理对象。
因为在调用连接池返回的prepareStatement的close方法时,不会真正的close这个对象,因为这样就无法实现复用的效果。可能只是修改了这个对象的标志位,标明其可用。
下面是DBCP连接池开启游标缓存的 代码。
可以想见 ds.getConnection()返回的Connection和PreparedStatement应该都是代理对象。
private static void testDataSource() throws SQLException {
BasicDataSource ds = new BasicDataSource();
ds.setUrl("jdbc:oracle:thin:127.0.0.1:1521:orcl");
ds.setUsername("edmond");
ds.setPassword("edmond");
ds.setPoolPreparedStatements(true);
ds.setMaxOpenPreparedStatements(300);
Connection conn = ds.getConnection();
PreparedStatement cmd = conn.prepareStatement("select * from t");
ResultSet rs = cmd.executeQuery();
rs.next();
System.out.println(rs.getString(1));
rs.next();
System.out.println(rs.getString(1));
cmd.close();
conn.close();
}
另外,Oracle游标对应的是PreparedStatement,而不是ResultSet。
并且MaxOpenPreparedStatements的设置应该小于Oracle的Open_Cursor的数值。
public static void main(String[] args) throws ClassNotFoundException, SQLException {
List<PreparedStatement> list = new ArrayList<PreparedStatement>();
Class.forName("oracle.jdbc.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:127.0.0.1:1521:orcl", "edmond", "edmond");
for (int i = 0; i < 305; i++) {
PreparedStatement cmd = conn.prepareStatement("select * from t");
ResultSet rs = cmd.executeQuery();
rs.next();
rs.close();
rs = null;
list.add(cmd);
}
conn.close();
}
结果出现异常:
Exception in thread "main" java.sql.SQLException: ORA-00604: 递归 SQL 级别 1 出现错误
ORA-01000: 超出打开游标的最大数
ORA-00604: 递归 SQL 级别 1 出现错误
ORA-01000: 超出打开游标的最大数
ORA-01000: 超出打开游标的最大数
可以看到,如果PreparedStatement没有关闭,则Oracle那端的游标就没有释放。
最终这个连接的游标超过Oracle的open_cursor数值(默认300),就会报错。
所以启用了PoolPreparedStatements,一定注意设置MaxOpenPreparedStatements小于Oracle Open_Cursor的数值。