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的數值。