☰
Oracle 存储过程返回结果集:REF CURSOR 与 OUT 参数实战大纲
2026/10/1 19:55:43 网站建设 项目流程

1. 为什么存储过程返回结果集总踩坑:从一次报表接口联调说起

很多做 Oracle 数据接口的朋友都遇到过这种场景:前端要一个列表,后端想用存储过程把查询逻辑封装起来,结果写完SELECT发现存储过程根本不能像函数那样直接RETURN一张表。Oracle 存储过程返回结果集这件事,本质上是把「查询结果」通过一个游标句柄交给调用方,而不是把数据行本身塞进返回值里。这个游标就是 REF CURSOR,配合 OUT 参数使用,才能让 SQL*Plus、JDBC、MyBatis、C# 这些调用端拿到多行数据。

我试过在一个报表项目里,存储过程里写死了SELECT * FROM T_ORDER WHERE STATUS = 1,调用端用 JDBC 的CallableStatement注册Types.CURSOR,结果一直报ORA-01000: maximum open cursors exceeded,排查半天才发现是游标没关。后来把 REF CURSOR 的声明、打开、关闭三件事拆清楚,问题才解决。所以这篇不讲虚的,直接给你能复制的存储过程定义、OUT SYS_REFCURSOR 声明、游标打开与关闭写法,以及 SQL*Plus、JDBC、MyBatis 三种调用端的验证步骤。

适合谁看:正在写 Oracle 存储过程返回多行数据、被REF CURSOR和OUT参数绕晕的后端开发;需要把复杂查询封装进数据库、又不想用视图的 DBA;以及用 MyBatis 调存储过程返回结果集时,resultMap配不对的工程师。核心检索词就三个:Oracle 存储过程、返回结果集、REF CURSOR。你把这三个词对应的链路跑通,后面不管换 Java、C# 还是 Python,调用方式都是同一套逻辑。

先说清楚一个概念:REF CURSOR 是一个指向查询结果集的指针,它本身不存数据,只存「去哪里取数据」的地址。存储过程通过 OUT 参数把这个指针传出去,调用方拿到指针后再遍历。这就像你去图书馆借书,存储过程是管理员,REF CURSOR 是索书号,OUT 参数是把索书号递给你,真正的内容还在书架上。理解这一点,后面所有报错都能对上号。

2. TaoToken 前置准备:把模型对话和 API Key 配好再动手

在写存储过程之前,建议先把调试环境准备好。我平时排查 Oracle 存储过程返回结果集的问题时,会同时开一个模型对话窗口,把报错信息贴进去让它帮我分析游标声明哪里写错了。TaoToken 的模型对话入口在这里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,你可以直接用它来辅助理解SYS_REFCURSOR和自定义TYPE ... IS REF CURSOR的区别。

如果你打算长期做数据库接口开发,建议直接上 Coding Plan,把模型对话、代码补全、报错分析放在一个工作流里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。我实测下来,写 PL/SQL 时最容易错的地方是OPEN ... FOR后面跟动态 SQL 字符串的拼接,以及OUT参数在 JDBC 里注册类型时写成Types.REF_CURSOR而不是Types.CURSOR,这些细节用模型对话快速确认能省不少时间。

API Key 的获取在控制台里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。拿到 Key 之后,你可以把它配到自己的 IDE 插件或者命令行工具里。注意,TaoToken 的 API 地址是 https://taotoken.net/api ,不要加 UTM 参数,直接用于程序调用。接入文档在这里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,里面有 Base URL、Key、Model ID 三件套的完整说明。

为什么要先做这一步?因为 Oracle 存储过程返回结果集的调试过程,往往需要反复改 PL/SQL 块、反复看调用端日志。有一个能随时问的模型对话窗口,比你自己翻文档快得多。特别是遇到ORA-06550这种 PL/SQL 编译错误,把错误行号和代码贴进去,基本能秒定位。另外,如果你用 Claude Code 做辅助开发,可以参考这个入口:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,把数据库连接信息和存储过程定义放在项目里,让模型帮你检查游标是否配对关闭。

这里给一个我常用的配置片段,放在项目的.env或者 IDE 的 settings 里,方便模型对话工具读取上下文:

{ "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "你的_API_KEY", "model_id": "你的_MODEL_ID" }, "oracle": { "host": "127.0.0.1", "port": 1521, "service_name": "ORCLPDB1", "user": "scott", "password": "tiger" } }

注意,API Key 不要提交到 Git 仓库,用环境变量或者本地配置文件管理。Model ID 根据你实际使用的模型填写,接入文档里有完整列表。配好之后,你在写CREATE OR REPLACE PROCEDURE时,可以让模型对话帮你检查OUT SYS_REFCURSOR的声明位置是否正确,避免PLS-00306: wrong number or types of arguments这种调用端和定义端参数不匹配的报错。

3. 可复制配置:REF CURSOR 存储过程定义与三种调用端写法

这一节直接给可复制的代码。先看存储过程定义,分两种写法:用包声明自定义 REF CURSOR 类型,和直接用SYS_REFCURSOR。推荐用SYS_REFCURSOR,少建一个包,调用端也简单。

3.1 存储过程定义:OUT SYS_REFCURSOR 标准写法

CREATE OR REPLACE PROCEDURE PROC_GET_ORDERS ( P_STATUS IN VARCHAR2, P_RESULT OUT SYS_REFCURSOR ) IS V_SQL VARCHAR2(4000); BEGIN V_SQL := 'SELECT ORDER_ID, CUSTOMER_ID, STATUS, AMOUNT, CREATE_TIME ' || 'FROM T_ORDER WHERE STATUS = :1 ORDER BY CREATE_TIME DESC'; OPEN P_RESULT FOR V_SQL USING P_STATUS; EXCEPTION WHEN OTHERS THEN IF P_RESULT%ISOPEN THEN CLOSE P_RESULT; END IF; RAISE; END PROC_GET_ORDERS; /

关键点:OPEN P_RESULT FOR V_SQL USING P_STATUS这里用绑定变量:1,不要用字符串拼接,否则有 SQL 注入风险,而且每次执行都会硬解析。EXCEPTION块里判断P_RESULT%ISOPEN再关闭,避免游标泄漏。存储过程本身不关闭游标,因为结果集要交给调用方遍历,关闭动作由调用方完成。

如果你非要用自定义类型,包声明这样写:

CREATE OR REPLACE PACKAGE PKG_TYPES AS TYPE T_REF_CURSOR IS REF CURSOR; END PKG_TYPES; / CREATE OR REPLACE PROCEDURE PROC_GET_ORDERS2 ( P_STATUS IN VARCHAR2, P_RESULT OUT PKG_TYPES.T_REF_CURSOR ) IS BEGIN OPEN P_RESULT FOR SELECT ORDER_ID, CUSTOMER_ID, STATUS, AMOUNT, CREATE_TIME FROM T_ORDER WHERE STATUS = P_STATUS ORDER BY CREATE_TIME DESC; END PROC_GET_ORDERS2; /

两种写法调用端注册类型时都用Types.CURSOR(JDBC)或OracleType.Cursor(C#),不要写成Types.REF_CURSOR,那个常量在部分驱动版本里不存在。

3.2 SQL*Plus 调用:PRINT 和 COLUMN 格式化

在 SQL*Plus 里调用最简单,用VARIABLE声明绑定变量,EXEC执行,PRINT输出:

VARIABLE RC REFCURSOR; EXEC PROC_GET_ORDERS('PAID', :RC); PRINT RC;

如果结果列太宽,先设置格式:

COLUMN ORDER_ID FORMAT A12; COLUMN CUSTOMER_ID FORMAT A12; COLUMN STATUS FORMAT A8; COLUMN AMOUNT FORMAT 999999.99; SET LINESIZE 200; PRINT RC;

注意,SQL*Plus 里VARIABLE RC REFCURSOR是固定写法,不要写成SYS_REFCURSOR。执行完PRINT RC后,游标自动关闭,不需要手动CLOSE。

3.3 JDBC 调用:CallableStatement 注册 Types.CURSOR

Java 端完整代码:

public List<Order> getOrders(String status) throws SQLException { List<Order> list = new ArrayList<>(); String sql = "{call PROC_GET_ORDERS(?, ?)}"; try (Connection conn = dataSource.getConnection(); CallableStatement cs = conn.prepareCall(sql)) { cs.setString(1, status); cs.registerOutParameter(2, Types.CURSOR); cs.execute(); try (ResultSet rs = (ResultSet) cs.getObject(2)) { while (rs.next()) { Order o = new Order(); o.setOrderId(rs.getString("ORDER_ID")); o.setCustomerId(rs.getString("CUSTOMER_ID")); o.setStatus(rs.getString("STATUS")); o.setAmount(rs.getBigDecimal("AMOUNT")); o.setCreateTime(rs.getTimestamp("CREATE_TIME")); list.add(o); } } } return list; }

registerOutParameter(2, Types.CURSOR)是核心,cs.getObject(2)拿到的是ResultSet,用 try-with-resources 自动关闭。注意{call ...}的转义语法,Oracle JDBC 驱动支持这种写法。

3.4 MyBatis 调用:statementType=CALLABLE 与 resultMap

MyBatis 映射文件写法:

<resultMap id="orderMap" type="com.example.Order"> <id column="ORDER_ID" property="orderId"/> <result column="CUSTOMER_ID" property="customerId"/> <result column="STATUS" property="status"/> <result column="AMOUNT" property="amount"/> <result column="CREATE_TIME" property="createTime"/> </resultMap> <select id="getOrders" statementType="CALLABLE" parameterType="map" resultMap="orderMap"> {call PROC_GET_ORDERS( #{status, mode=IN, jdbcType=VARCHAR}, #{result, mode=OUT, jdbcType=CURSOR, javaType=java.sql.ResultSet, resultMap=orderMap} )} </select>

Mapper 接口:

List<Order> getOrders(Map<String, Object> params);

调用时:

Map<String, Object> params = new HashMap<>(); params.put("status", "PAID"); List<Order> orders = orderMapper.getOrders(params);

注意jdbcType=CURSOR和resultMap=orderMap必须同时写,否则 MyBatis 不知道把游标结果映射成什么对象。statementType="CALLABLE"不能漏,否则会当成普通查询执行。

4. 验证请求与成功结果:从 SQL*Plus 到 JDBC 的完整链路

写完存储过程和调用端代码,怎么确认真的返回了结果集?分三步验证。

第一步,在 SQL*Plus 里直接执行,看游标是否有数据:

SET SERVEROUTPUT ON; VARIABLE RC REFCURSOR; EXEC PROC_GET_ORDERS('PAID', :RC); PRINT RC;

如果输出类似:

ORDER_ID CUSTOMER_ID STATUS AMOUNT CREATE_TIME ---------- ----------- ------ ------- ----------- ORD20240101 CUST001 PAID 1999.00 2024-01-01 10:00:00 ORD20240102 CUST002 PAID 2999.00 2024-01-02 11:30:00

说明存储过程本身没问题。如果PRINT RC报SP2-0552: Bind variable "RC" not declared,检查VARIABLE RC REFCURSOR;是否在同一会话执行。

第二步,JDBC 端加日志,打印rs.getMetaData().getColumnCount()和行数:

try (ResultSet rs = (ResultSet) cs.getObject(2)) { ResultSetMetaData meta = rs.getMetaData(); System.out.println("列数: " + meta.getColumnCount()); int count = 0; while (rs.next()) { count++; System.out.println("行 " + count + ": " + rs.getString("ORDER_ID")); } System.out.println("总行数: " + count); }

如果列数为 0 或行数为 0,先确认P_STATUS传的值在表里确实有数据。我踩过的坑是状态值大小写不一致,'paid'和'PAID'在 Oracle 里不相等,导致游标打开但结果为空。

第三步,MyBatis 端打开 SQL 日志,看是否执行了{call PROC_GET_ORDERS(?, ?)},以及返回的ResultSet是否被正确映射。在application.yml里加:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

如果日志里看到==> Preparing: {call PROC_GET_ORDERS(?, ?)}和==> Parameters: PAID(String), null,但返回列表为空,检查resultMap里的column是否和存储过程SELECT的列名完全一致,包括大小写。Oracle 默认返回大写列名,resultMap里写小写也能映射,但如果你用了别名,别名是什么就写什么。

成功的结果是:SQL*Plus 打印出多行数据,JDBC 控制台输出总行数大于 0,MyBatis 返回的List<Order>长度和数据库里符合条件的记录数一致。三个端都通过,说明 REF CURSOR 和 OUT 参数的链路完全打通。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照

这一节列真实报错和排查方向。注意,这些报错分两类:一类是 Oracle 存储过程本身的,一类是调用 TaoToken 模型对话辅助调试时的。

ORA-01000: maximum open cursors exceeded。这是游标没关。存储过程里OPEN P_RESULT FOR之后,调用方必须关闭ResultSet。JDBC 里用 try-with-resources,MyBatis 里框架会自动关,SQL*Plus 里PRINT RC后自动关。如果你在存储过程里循环打开多个游标,每个都要在异常块里CLOSE。

ORA-06550: line X, column Y: PLS-00306: wrong number or types of arguments。调用端参数个数或类型和存储过程定义不匹配。检查OUT SYS_REFCURSOR是否在参数列表最后,JDBC 里registerOutParameter的索引是否对应。MyBatis 里mode=OUT的参数是否写了jdbcType=CURSOR。

ORA-00932: inconsistent datatypes: expected - got CURSOR。通常是把SYS_REFCURSOR当普通变量赋值了。REF CURSOR 只能通过OPEN ... FOR打开,不能:=赋值。

401 Unauthorized。这是 TaoToken API Key 没配或配错。检查base_url是否是https://taotoken.net/api,Key 是否复制完整,有没有多余空格。如果用的是环境变量,确认变量名和代码里读取的一致。

local proxy failed。这是本地网络或代理配置问题。检查你的 HTTP 客户端是否走了系统代理,把no_proxy设成taotoken.net试试。如果是公司内网,确认防火墙是否放行了 443 端口。

reading choices 相关报错。这通常出现在模型对话返回流式响应时,客户端解析 JSON 失败。检查请求头Content-Type: application/json和Accept: text/event-stream是否匹配。如果你用的是 OpenAI 兼容的 SDK,确认base_url末尾不要多加/v1,TaoToken 的 API 地址已经包含了版本路径。

OAuth 相关报错。如果你用 Claude Code 或者 Cline MCP 接入,OAuth 回调地址要填对。参考文档里的 deep link:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。注意,OAuth 流程里不要手动改redirect_uri,用默认的。

CC Switch / Cline MCP / Codex auth.json 三件套。如果你用这些工具,配置里必须写全 Base URL、Key、Model ID。以 Codex 的auth.json为例:

{ "base_url": "https://taotoken.net/api", "api_key": "你的_API_KEY", "model_id": "你的_MODEL_ID" }

少任何一个都会报authentication failed或model not found。Cline MCP 的配置在mcp_settings.json里,同样三件套。CC Switch 里切换配置时,确认当前激活的 profile 包含完整三件套。

MyBatis 返回结果集为空但 SQL*Plus 有数据。检查resultMap的type是否和 Mapper 接口返回类型一致,column和property是否对应。如果用了mapUnderscoreToCamelCase,ORDER_ID会自动映射到orderId,但resultMap里显式写了column="ORDER_ID"也没问题。另外,MyBatis 的CALLABLE语句里,OUT 参数的resultMap属性必须指向一个已定义的resultMap,不能直接写resultType。

6. 语义一致 CTA:把存储过程调试和模型对话串起来

存储过程返回结果集这件事,核心就是 REF CURSOR 声明、OUT 参数传递、调用端遍历三步。你把第 3 节的代码复制到自己的环境里,改一下表名和字段名,就能跑通。跑的过程中遇到 PL/SQL 编译错误或者 JDBC 类型不匹配,直接把报错贴到模型对话里:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,让它帮你逐行检查。

如果你需要长期做数据库接口开发,建议把 API Key 和接入文档放在手边:API Key 在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,接入文档在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。Coding Plan 适合需要频繁调试存储过程、写调用端代码的场景:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。模型对话入口再放一次,方便你直接点:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

最后给一个实用技巧:在存储过程里加一个DBMS_OUTPUT.PUT_LINE打印动态 SQL,执行前先看拼接出来的语句对不对。SQL*Plus 里SET SERVEROUTPUT ON就能看到输出。这个习惯能帮你快速定位OPEN ... FOR后面的 SQL 是不是少了空格或者多了逗号。游标关闭这件事,记住一句话:谁打开,谁关闭;存储过程打开,调用方关闭。把这句话贴在显示器上,ORA-01000就不会再找你了。

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

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

立即咨询