☰
Oracle PL/SQL 存储过程返回 Sys_refcursor 的完整实践:从定义到调用
2026/10/10 11:54:30 网站建设 项目流程

1. 为什么存储过程返回结果集总踩坑:sys_refcursor 到底解决什么问题

如果你写过 Oracle 存储过程,大概率遇到过这种需求:前端或者 Java 服务想调用一个过程,直接拿回一批数据,而不是只拿一个OUT参数里的单值。传统做法要么在过程里SELECT INTO一个变量,要么用临时表绕一圈,代码又长又难维护。sys_refcursor就是 Oracle 给这个场景准备的答案——它是一个内置的弱类型游标引用,你不需要像 9i 之前那样先TYPE ... IS REF CURSOR自定义类型,直接声明OUT SYS_REFCURSOR就能把整个结果集“抛”给调用方。

它适合谁?适合做企业级后台的开发者:Java 通过 JDBCCallableStatement调过程、PL/SQL 匿名块里循环遍历、甚至报表工具通过绑定变量取数。核心检索词就是sys_refcursor、oracle procedure 返回结果集、plsql 游标遍历。我见过太多人卡在“过程建好了,但 Java 端getResultSet报错”或者“PL/SQL 里FETCH拿不到数据”,本质是没搞清游标的生命周期和绑定变量的用法。

这篇就按“建包 → 建过程 → 调用 → 验证 → 排错”的完整链路走一遍,所有脚本可直接复制。另外,多工具协同调试时(比如同时用 SQL 客户端、Java 服务、AI 辅助编码工具),统一 Key/API 通道能省掉反复配环境的麻烦,TaoToken 就是干这个的,后面会给出具体接入方式。

先明确一个概念:sys_refcursor是“弱类型”的,它不绑定具体的SELECT列结构,所以同一个OUT参数可以被不同OPEN ... FOR语句复用。这既是灵活点,也是坑点——调用方必须知道当前打开的是哪套列,否则FETCH INTO会类型不匹配。下面从包头声明开始。

2. 前置准备:TaoToken 统一 Key 与 API 通道配置

在真正写包之前,先把调试环境理顺。很多人的痛点是:本地 SQL 客户端一套连接、Java 项目一套配置、AI 编码助手又要单独填 Key,改来改去容易出错。TaoToken 提供统一的 Key 和 API 通道,把模型对话、编码计划、控制台管理收敛到一个入口,多工具协同调试时只维护一份凭证。

你需要先拿到 API Key。访问控制台创建:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建后复制那串sk-开头的 Key,后面所有工具都用它。

Base URL 统一用https://taotoken.net/api(注意这个地址不加 UTM 参数,直接作为接口根路径)。Model ID 按你实际使用的模型填,比如做代码补全和 SQL 审查时选对应的编码模型。这三件套——Base URL、Key、Model ID——在任何接入场景里都要写全,缺一个就连不上。

如果你用的是 Claude Code 这类命令行编码工具,接入时在配置里填:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "你的ModelID" }

如果是 Cline 配合 MCP 的场景,MCP server 配置同样指向这个 Base URL 和 Key。Codex 的auth.json里也是这三项:

{ "baseURL": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "你的ModelID" }

配好之后,你在写 PL/SQL 包的时候,可以让 AI 助手直接读你的建包脚本做审查,不用来回切窗口。模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite ,需要长期跑编码和 Agent 任务的用 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到 401 先查这里。

环境理顺后,回到 Oracle 本身。确认你的数据库版本在 9i 以上(现在基本都是 11g/12c/19c),sys_refcursor原生支持,不需要自定义 type。用sqlplus / as sysdba或普通用户登录都行,下面用scott演示。

3. 可复制配置:建包、建过程、包头声明完整脚本

这一步给出可直接执行的脚本。先建一个包规范(package spec),在包头里声明过程和OUT SYS_REFCURSOR参数。注意:包头只声明,包体才实现。

-- 包头:声明返回结果集的过程 CREATE OR REPLACE PACKAGE pkg_emp_query AS -- 按部门号返回员工结果集 PROCEDURE get_emp_by_dept( in_deptno IN emp.deptno%TYPE, out_cur OUT SYS_REFCURSOR ); -- 按员工编号返回单条记录的结果集 PROCEDURE get_emp_by_id( in_empno IN emp.empno%TYPE, out_cur OUT SYS_REFCURSOR ); END pkg_emp_query; /

包体实现,核心就是OPEN out_cur FOR SELECT ...:

CREATE OR REPLACE PACKAGE BODY pkg_emp_query AS PROCEDURE get_emp_by_dept( in_deptno IN emp.deptno%TYPE, out_cur OUT SYS_REFCURSOR ) AS BEGIN OPEN out_cur FOR SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno FROM emp WHERE deptno = in_deptno ORDER BY empno; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20101, 'Error in get_emp_by_dept: ' || SQLCODE || ' - ' || SQLERRM); END get_emp_by_dept; PROCEDURE get_emp_by_id( in_empno IN emp.empno%TYPE, out_cur OUT SYS_REFCURSOR ) AS BEGIN OPEN out_cur FOR SELECT empno, ename, job, sal, deptno FROM emp WHERE empno = in_empno; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20102, 'Error in get_emp_by_id: ' || SQLCODE || ' - ' || SQLERRM); END get_emp_by_id; END pkg_emp_query; /

如果你不想用包,直接建独立过程也行,效果一样:

CREATE OR REPLACE PROCEDURE get_emp_by_dept( in_deptno IN emp.deptno%TYPE, out_cur OUT SYS_REFCURSOR ) AS BEGIN OPEN out_cur FOR SELECT * FROM emp WHERE deptno = in_deptno; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20101, 'Error in get_emp_by_dept: ' || SQLCODE); END get_emp_by_dept; /

关键点:OUT SYS_REFCURSOR是输出参数,过程内部用OPEN ... FOR把查询结果挂上去。不要在过程里CLOSE游标——关闭的责任在调用方,否则调用方FETCH会报ORA-01001: invalid cursor。这是最常见的坑之一。

建完后用DESC pkg_emp_query确认包头声明生效。如果报ORA-00955: name is already used,说明对象已存在,加OR REPLACE即可。包体编译报错用SHOW ERRORS看具体行号。

4. 验证请求与成功结果:PL/SQL 绑定变量与 Java 端遍历

先看 PL/SQL 端怎么调。用绑定变量VAR声明一个 refcursor,EXEC执行过程,再PRINT输出:

-- SQL*Plus 环境 VAR rset REFCURSOR; EXEC pkg_emp_query.get_emp_by_dept(10, :rset); PRINT rset;

预期输出(部门 10 的三条记录):

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- ---------- --------- ---------- --------- ---------- ---------- ---------- 7782 CLARK MANAGER 7839 09-1月 -81 2450 10 7839 KING PRESIDENT 17-11月-81 5000 10 7934 MILLER CLERK 7782 23-1月 -82 1300 10

如果要在匿名块里循环遍历,用FETCH ... INTO配合%ROWTYPE或逐个变量:

SET SERVEROUTPUT ON; DECLARE v_cur SYS_REFCURSOR; v_empno emp.empno%TYPE; v_ename emp.ename%TYPE; v_sal emp.sal%TYPE; BEGIN pkg_emp_query.get_emp_by_dept(20, v_cur); LOOP FETCH v_cur INTO v_empno, v_ename, v_sal; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_empno || ' - ' || v_ename || ' - ' || v_sal); END LOOP; CLOSE v_cur; -- 调用方负责关闭 END; /

注意FETCH的列顺序必须和OPEN ... FOR里的SELECT列顺序一致,否则会ORA-06502: PL/SQL: numeric or value error。这是弱类型游标的代价。

Java 端用 JDBCCallableStatement:

// 假设 conn 已建立 String sql = "{ call pkg_emp_query.get_emp_by_dept(?, ?) }"; try (CallableStatement cs = conn.prepareCall(sql)) { cs.setInt(1, 10); cs.registerOutParameter(2, OracleTypes.CURSOR); cs.execute(); try (ResultSet rs = (ResultSet) cs.getObject(2)) { while (rs.next()) { System.out.println( rs.getInt("empno") + " - " + rs.getString("ename") + " - " + rs.getDouble("sal") ); } } }

关键:registerOutParameter(2, OracleTypes.CURSOR)必须写,否则getObject拿不到结果集。OracleTypes.CURSOR对应-10,用java.sql.Types.OTHER也行。执行后getObject(2)强转ResultSet遍历即可。实测下来,这套写法在 11g/12c/19c 都稳定。

5. 本篇常见错排查:401、ORA-01001、列不匹配与 OAuth 类问题

排错分两块:Oracle 侧和工具接入侧。

Oracle 侧最常见的是ORA-01001: invalid cursor。原因通常是过程内部提前CLOSE了游标,或者调用方重复FETCH已关闭的游标。检查你的过程里有没有多余的CLOSE out_cur,删掉。另一个是ORA-06502,几乎都是FETCH INTO的变量类型或数量与SELECT列不匹配,逐列核对。

ORA-20101这类自定义错误,说明过程进了EXCEPTION WHEN OTHERS,看SQLERRM里的原始信息。常见触发是in_deptno传了NULL,WHERE deptno = NULL永远不成立,结果集为空但不报错——如果你期望有数据,先确认参数非空。

工具接入侧,如果你在配置 AI 编码助手时遇到401 Unauthorized,先检查 Key 是否复制完整(sk-开头那串),以及 Base URL 是否写成了https://taotoken.net/api而不是带路径的地址。local proxy failed一般是本地网络或端口占用,换端口重试。reading choices报错通常是 Model ID 填错,去控制台核对模型名称。OAuth 类问题(比如 Claude Code 登录态失效)重新走一遍授权流程,或者直接用 API Key 模式。

对照表:

报错可能原因处理
ORA-01001游标被提前关闭删除过程内 CLOSE
ORA-06502FETCH 列不匹配核对 SELECT 与 INTO 顺序
ORA-20101过程异常看 SQLERRM 原始信息
401Key 错误重新复制 API Key
local proxy failed本地端口/网络换端口或检查网络
reading choicesModel ID 错控制台核对模型名

排查顺序建议:先确认过程本身在 SQL*Plus 里能PRINT出数据,再排查 Java 或工具侧。这样能把问题范围缩小一半。

6. 语义一致 CTA:把调试链路收敛到统一通道

回到实际开发节奏:你写完包、建完过程,接下来是 Java 联调、AI 助手审查 SQL、可能还要跑自动化测试。如果每个环节都单独配 Key 和地址,改一次环境要动好几处。TaoToken 的价值就是把这些收敛成一份凭证。

需要创建或管理 Key,去 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。接入细节和参数说明看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先验证模型对 PL/SQL 的理解能力,用模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。长期做编码和 Agent 任务,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。

最后给一个实用技巧:把建包脚本、调用示例、Java 代码放在同一个项目目录里,让 AI 助手一次性读全,它给出的排错建议会准得多。sys_refcursor本身不复杂,复杂的是调用方和过程之间的“契约”——列顺序、关闭责任、参数非空。把这三点写进你的代码注释,下次联调能省半小时。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询