必须显式调用 ResultSet.close() 才能释放 Oracle 服务器端 SYS_REF_CURSOR,PreparedStatement.close() 仅释放客户端语句句柄;多游标需按 OPEN 顺序反向关闭并配异常兜底;JDBC 中每个 OUT 游标都须单独 getObject() 并 close()。

REF CURSOR 关闭不等于 PreparedStatement 关闭
很多人在 Java 里调用 Oracle 存储过程返回 SYS_REFCURSOR 后,只对 PreparedStatement 调用 close(),就以为游标已释放——这是最常见、最危险的误解。每个 SYS_REFCURSOR 在 Oracle 服务器端会占用独立的私有 SQL 区(PGA 中的游标工作区),它和 JDBC 的 PreparedStatement 是两个生命周期:前者在数据库侧,后者在应用侧。
不显式关闭游标的结果是:PGA 内存持续增长,V$OPEN_CURSOR 中游标数飙升,严重时触发 ORA-01000: maximum open cursors exceeded。
- 必须通过 JDBC 的
ResultSet.close()触发底层OCIStmtClose,才能真正释放服务器端游标 - 如果存储过程返回多个
OUT SYS_REFCURSOR参数,每个都对应一个独立的ResultSet,必须分别close() -
PreparedStatement.close()只释放语句句柄,不会自动级联关闭其打开的 REF CURSOR(Oracle 12c+ 的“自动关闭”行为不可靠,别依赖)
PL/SQL 层关闭顺序必须反向且带异常兜底
在 PL/SQL 块中手动管理多个 REF CURSOR 时,关闭顺序不是按声明顺序,而是按 OPEN FOR 的执行顺序**反向关闭**。因为后打开的游标可能依赖先打开游标的上下文(尤其涉及临时表或会话级变量时),正向关闭容易触发 ORA-01001: invalid cursor。
更关键的是:一旦某个 FETCH 过程抛异常(比如数据类型转换失败),后续的 CLOSE 语句根本不会执行——所以不能裸写 CLOSE。
- 正确做法是每个游标单独配
EXCEPTION块或统一用FINALLY(在匿名块中需用BEGIN...EXCEPTION...END套一层) - 示例片段:
DECLARE c1 SYS_REFCURSOR; c2 SYS_REFCURSOR; r1 emp%ROWTYPE; r2 dept%ROWTYPE; BEGIN OPEN c1 FOR SELECT * FROM emp WHERE ROWNUM <= 10; OPEN c2 FOR SELECT * FROM dept; <p>LOOP FETCH c1 INTO r1; EXIT WHEN c1%NOTFOUND; END LOOP;</p><p>LOOP FETCH c2 INTO r2; EXIT WHEN c2%NOTFOUND; END LOOP;</p><p>EXCEPTION WHEN OTHERS THEN IF c2%ISOPEN THEN CLOSE c2; END IF; IF c1%ISOPEN THEN CLOSE c1; END IF; RAISE; END;
- 注意:
c2先关,c1后关;且两个CLOSE都在EXCEPTION块里做防护
JDBC 调用多 REF CURSOR 时必须逐个消费并关闭
Java 客户端拿到多个 OUT SYS_REFCURSOR 参数后,不能只调用一次 stmt.getResultSet() 就完事。Oracle JDBC 驱动要求按参数声明顺序,依次调用 getResultSet() 获取每个结果集,并且每个都必须显式 close()。
典型错误现象:第二个游标返回空结果、SQLException: Invalid operation for read only resultset,或者连接池里连接被长期占用。
- 使用
try-with-resources最稳妥:try (CallableStatement cs = conn.prepareCall("{call my_proc(?, ?)}")) { cs.registerOutParameter(1, OracleTypes.CURSOR); cs.registerOutParameter(2, OracleTypes.CURSOR); cs.execute(); <p>try (ResultSet rs1 = (ResultSet) cs.getObject(1)) { while (rs1.next()) { /<em> 处理第一个结果集 </em>/ } }</p><p>try (ResultSet rs2 = (ResultSet) cs.getObject(2)) { while (rs2.next()) { /<em> 处理第二个结果集 </em>/ } } } - 不要复用同一个
ResultSet变量名去接多个游标——驱动内部用参数索引定位,变量名无关紧要 - 如果某游标不需要读取(比如只用于日志或审计),也必须调用
getObject(n)+close(),否则服务器端游标永不释放
动态 SQL 中 OPEN FOR 的字符串拼接必须加绑定变量防泄漏
用 OPEN c FOR 'SELECT ... FROM ' || table_name 这种纯字符串拼接方式打开动态游标,看似方便,但一旦 table_name 来自用户输入或配置表,就会引入 SQL 注入风险;更隐蔽的问题是:拼错表名或字段名导致 OPEN 失败时,游标变量本身未被初始化,后续 CLOSE 会直接报 ORA-01001。
- 优先用
EXECUTE IMMEDIATE+ 绑定变量替代字符串拼接,尤其是表名、列名等无法绑定的部分,应走白名单校验 - 若必须拼接,
OPEN后立即检查%ISOPEN:OPEN c FOR 'SELECT * FROM ' || v_table_name; IF NOT c%ISOPEN THEN RAISE_APPLICATION_ERROR(-20001, 'Failed to open dynamic cursor for ' || v_table_name); END IF;
- 所有动态游标变量声明后,初始值设为
NULL,并在EXCEPTION块中只关%ISOPEN为TRUE的游标
真正难的不是知道要关,而是关的时机、关的顺序、关的兜底逻辑——这三个点漏掉任何一个,游标就卡在 PGA 里不动了。


















