1. 为什么 Oracle 存储过程返回结果集总踩坑:SYS_REFCURSOR 与 OUT 参数的真实场景
很多从 MySQL 转过来的朋友第一次写 Oracle 存储过程返回结果集时都会懵:MySQL 里SELECT * FROM emp直接写在存储过程里就能返回一张表,Oracle 却不行。Oracle 的存储过程本身不直接返回结果集,必须借助游标变量(SYS_REFCURSOR)配合OUT参数,把结果集的“句柄”传出去,调用方再从这个句柄里逐行 FETCH。
这个场景在实际工作中非常高频:报表系统要调用存储过程拿数据、Java 服务通过 JDBC 调 Oracle 存储过程返回列表、数据同步任务需要批量拉取结果集。核心检索词就是Oracle 存储过程返回结果集,能做什么?它让你把复杂查询逻辑封装在数据库端,应用层只负责消费结果,减少网络往返和 SQL 拼接。
适合谁看?适合正在写 PL/SQL 的 DBA、后端开发、数据工程师,尤其是需要在统一 API 通道下验证数据库调用链路的同学。我这次的做法是:在本地 Oracle 环境写好存储过程,然后通过 TaoToken 的统一 Key 通道,用 SQL*Plus 和 JDBC 两种方式调用验证,确保结果集能正确返回并校验行数。
先说清楚一个概念:SYS_REFCURSOR是 Oracle 预定义的弱类型游标变量,本质上是一个指向结果集的指针。存储过程通过OUT参数把这个指针交给调用者,调用者拿到指针后可以像操作普通游标一样FETCH、LOOP、CLOSE。理解这一点,后面所有代码都顺了。
我试过直接在存储过程里dbms_output.put_line打印,但那只适合调试,真正返回结果集必须用OUT SYS_REFCURSOR。下面从环境准备开始,一步步跑通。
2. TaoToken 统一 Key 前置准备:API 通道与连接信息配置
在动手写存储过程之前,先把调用通道准备好。TaoToken 提供统一的 API 入口,官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。它的作用是让你用一套 Key 管理多个模型和数据库相关的调用通道,避免到处散落凭证。
你需要先拿到 API Key。进入控制台页面 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建一个新的 Key。创建时注意选择对应的权限范围,如果你只是做数据库调用验证,选基础调用权限即可。Key 生成后只显示一次,复制保存好。
接下来是模型和通道的选择。如果你后续要用 AI 辅助生成 PL/SQL 或者排查报错,可以在模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 里测试模型连通性。对于长期编码和 Agent 场景,Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 提供了更稳定的配额方案。
这里要强调三件套的完整性:Base URL + Key + Model ID。无论你用的是 Cline、Claude Code 还是 Codex,配置时这三个字段必须齐全。Base URL 填https://taotoken.net/api,Key 填你刚创建的,Model ID 根据你选的模型填。缺一个都会导致 401 或连接失败。
如果你用的是 Claude Code 做 PL/SQL 润色,可以参考文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里的接入说明。API Keys 管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,可以随时轮换 Key。
配置完成后,建议先用一个最简单的请求验证通道是否通。比如用 curl 测试模型对话接口,确认返回 200 再继续。这一步别跳过,否则后面存储过程调不通你会以为是数据库问题,其实是 Key 没配对。
3. 可复制配置:存储过程 DDL 与调用脚本完整片段
现在进入核心部分。先创建测试表和数据,然后写返回结果集的存储过程。以下 DDL 可以直接在 SQL*Plus 或 SQL Developer 里执行。
-- 创建测试表 create table emp ( empno number(4) primary key, ename varchar2(20), job varchar2(20), sal number(7,2) ); -- 插入测试数据 insert into emp values (7369,'SMITH','CLERK',800); insert into emp values (7499,'ALLEN','SALESMAN',1600); insert into emp values (7521,'WARD','SALESMAN',1250); insert into emp values (7566,'JONES','MANAGER',2975); insert into emp values (7654,'MARTIN','SALESMAN',1250); insert into emp values (7698,'BLAKE','MANAGER',2850); insert into emp values (7782,'CLARK','MANAGER',2450); insert into emp values (7788,'SCOTT','ANALYST',3000); insert into emp values (7839,'KING','PRESIDENT',5000); insert into emp values (7844,'TURNER','SALESMAN',1500); insert into emp values (7876,'ADAMS','CLERK',1100); insert into emp values (7900,'JAMES','CLERK',950); insert into emp values (7902,'FORD','ANALYST',3000); insert into emp values (7934,'MILLER','CLERK',1300); commit;接下来创建返回结果集的存储过程。关键点是OUT SYS_REFCURSOR参数:
create or replace procedure pro_emp( result out sys_refcursor ) is begin open result for select empno, ename, job, sal from emp order by empno; end pro_emp; /这个存储过程接收一个OUT类型的SYS_REFCURSOR,在过程体内用OPEN ... FOR打开游标并绑定查询语句。执行完OPEN后,结果集就已经准备好,调用方拿到游标句柄即可读取。
如果你需要带参数的版本,比如按部门过滤,可以这样写:
create or replace procedure pro_emp_by_job( p_job in varchar2, result out sys_refcursor ) is begin open result for select empno, ename, job, sal from emp where job = p_job order by empno; end pro_emp_by_job; /调用脚本用匿名块,声明一个SYS_REFCURSOR变量和一行记录变量:
set serveroutput on size 1000000 declare cur1 sys_refcursor; result_row emp%rowtype; v_count number := 0; begin pro_emp(cur1); loop fetch cur1 into result_row; exit when cur1%notfound; v_count := v_count + 1; dbms_output.put_line('员工编号:' || result_row.empno || ' 姓名:' || result_row.ename || ' 岗位:' || result_row.job || ' 薪资:' || result_row.sal); end loop; close cur1; dbms_output.put_line('总行数:' || v_count); end; /注意emp%rowtype要求查询列和表结构完全匹配。如果你只 select 部分列,需要自定义记录类型或者用多个变量接收。这是新手最容易踩的坑之一。
4. 验证请求与成功结果:SQL*Plus 执行与 JDBC 读取行数校验
先在 SQL*Plus 里跑一遍。执行上面的匿名块后,预期输出如下:
员工编号:7369 姓名:SMITH 岗位:CLERK 薪资:800 员工编号:7499 姓名:ALLEN 岗位:SALESMAN 薪资:1600 员工编号:7521 姓名:WARD 岗位:SALESMAN 薪资:1250 员工编号:7566 姓名:JONES 岗位:MANAGER 薪资:2975 员工编号:7654 姓名:MARTIN 岗位:SALESMAN 薪资:1250 员工编号:7698 姓名:BLAKE 岗位:MANAGER 薪资:2850 员工编号:7782 姓名:CLARK 岗位:MANAGER 薪资:2450 员工编号:7788 姓名:SCOTT 岗位:ANALYST 薪资:3000 员工编号:7839 姓名:KING 岗位:PRESIDENT 薪资:5000 员工编号:7844 姓名:TURNER 岗位:SALESMAN 薪资:1500 员工编号:7876 姓名:ADAMS 岗位:CLERK 薪资:1100 员工编号:7900 姓名:JAMES 岗位:CLERK 薪资:950 员工编号:7902 姓名:FORD 岗位:ANALYST 薪资:3000 员工编号:7934 姓名:MILLER 岗位:CLERK 薪资:1300 总行数:14看到总行数:14就说明结果集完整返回,没有丢行。如果行数不对,检查exit when cur1%notfound的位置,必须在fetch之后立即判断。
再用 JDBC 验证一遍。Java 代码核心片段:
CallableStatement cs = conn.prepareCall("{call pro_emp(?)}"); cs.registerOutParameter(1, OracleTypes.CURSOR); cs.execute(); ResultSet rs = (ResultSet) cs.getObject(1); int rowCount = 0; while (rs.next()) { rowCount++; System.out.println("员工:" + rs.getString("ename") + " 薪资:" + rs.getBigDecimal("sal")); } rs.close(); cs.close(); System.out.println("JDBC读取行数:" + rowCount);JDBC 调用时注意registerOutParameter的类型要用OracleTypes.CURSOR,这是 Oracle 驱动特有的。用标准Types.OTHER在某些驱动版本上也能工作,但推荐用 Oracle 类型更稳。
如果你通过 TaoToken 的 API 通道做远程调用验证,可以在模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 里让模型帮你生成对应的 JDBC 代码,然后本地执行。这样能把 AI 辅助和实际数据库验证结合起来。
行数校验是重点:SQL*Plus 输出 14 行,JDBC 也应该是 14 行。两边一致才说明存储过程返回结果集完全正确。如果 JDBC 少行,检查连接字符集和fetchSize设置。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth 报错对照
调用过程中会遇到几类典型报错,逐个拆解。
401 Unauthorized:这个最常见,基本是 Key 没配对或者过期。检查三件套:Base URL 是否为https://taotoken.net/api,Key 是否复制完整(注意前后空格),Model ID 是否拼写正确。如果用的是 Cline 或 Claude Code,去 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个再试。
local proxy failed:这个报错通常出现在本地代理配置冲突时。检查你的环境变量HTTP_PROXY、HTTPS_PROXY是否指向了不可用的地址。如果你在 Cline 的 MCP 配置里填了本地代理端口,确认那个端口没有被占用。解决方法是清空代理环境变量,或者把 Base URL 直接写成完整地址不走代理。
reading choices 报错:这个一般出现在模型返回格式解析阶段。如果你用 Codex 的auth.json配置,检查文件里的字段是否完整。auth.json需要包含 Base URL、Key 和 Model ID 三项。缺 Model ID 时,请求发出去但返回体里没有choices字段,解析就报错。补全后重启客户端。
OAuth 相关报错:如果你用的是 Claude Code 的 Anthropic 接入方式,OAuth 流程走不通时先确认回调地址是否被拦截。文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有完整的接入步骤。实在不行改用 API Key 方式,跳过 OAuth。
数据库侧的报错也要注意:
| 报错 | 原因 | 解决 |
|---|---|---|
| ORA-01001 无效的游标 | 游标未 OPEN 就 FETCH | 确认存储过程内执行了 OPEN FOR |
| ORA-06550 PLS 编译错误 | 参数类型不匹配 | 检查 OUT 参数是否为 SYS_REFCURSOR |
| ORA-01002 提取越界 | 循环退出条件写错 | exit when cur1%notfound放在 fetch 后 |
| 结果集为空 | 查询条件过滤掉了所有行 | 先用 SELECT 单独验证 |
Cline MCP 配置里如果出现连接失败,同样检查三件套。MCP 的配置文件通常是 JSON 格式,字段名要和文档一致。Codex 的auth.json路径一般在用户目录下的.codex文件夹里,改完记得重启。
6. 语义一致 CTA:统一 Key 下的长期编码与 Agent 调用建议
跑通这个存储过程返回结果集的案例后,你会发现统一 Key 通道的价值在于:数据库调用、模型辅助、代码生成都在一套凭证下完成,不用来回切换配置。对于长期做 PL/SQL 开发和数据库 Agent 的场景,建议把常用存储过程的调用脚本沉淀成模板,配合 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 的稳定配额,日常开发和排障效率会高很多。
如果你还需要验证其他模型对 PL/SQL 的理解能力,模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 可以快速切换测试。接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有各客户端的完整配置示例,遇到配置问题先查文档再排查。
最后一个实用技巧:存储过程返回结果集时,如果结果集很大,别一次性 FETCH 到内存。用BULK COLLECT配合LIMIT分批读取,比如fetch cur1 bulk collect into v_array limit 500,这样能控制内存占用。这个技巧在处理百万行级报表时特别有用,你可以先在小表上验证逻辑,再放大到生产数据。