☰
Oracle Procedure 返回数据集:TaoToken 场景下的 Ref Cursor 与临时表选型小结
2026/10/17 23:03:12 网站建设 项目流程

1. 从一次报表接口超时说起:Oracle Procedure 返回数据集到底怎么选

上周帮朋友看一个报表接口,Java 端调用 Oracle 存储过程,返回一个结果集,数据量不大,但接口偶尔超时,日志里还夹着ORA-01000: maximum open cursors exceeded。翻代码发现,存储过程里用sys_refcursor输出,调用端用 MyBatis 的Cursor接,但连接池配置和游标关闭没对齐。这类问题在「Oracle Procedure 返回数据集」的场景里非常典型:Ref Cursor 直接输出和临时表落地,两条路都能走通,但踩的坑完全不一样。

这篇就围绕这个场景展开。如果你正在做 Java/Go 后端对接 Oracle,或者写报表取数逻辑,需要让调用方稳定拿到结构化数据集,那 Ref Cursor 和全局临时表(GTT)的选型会直接影响你的接口稳定性和排障成本。我会给出可复制的建包脚本、两套完整示例、调用端取值代码,以及结果集字段与行数的一致性验证步骤。另外,如果你的调用链路里还接了统一 Key/API 通道(比如 TaoToken,官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end),那存储过程返回的数据集最终要经过这层通道透出,字段类型和行数的一致性就更要提前验证,不然后面排查会绕远路。

先说结论方向:逻辑能用一个 SQL 表达清楚的,优先 Ref Cursor 动态 SQL;中间运算复杂、需要多步落地的,考虑嵌套表或 GTT。但 GTT 在存储过程里动态drop/create是有代价的,后面会细说。

2. TaoToken 前置准备:统一 Key 与 API 通道的接入位置

在讲存储过程之前,先把调用链路的上游说清楚。很多同学写 Procedure 返回数据集,只关注数据库这一端,结果数据集透出到 API 网关或统一通道时,字段类型对不上、行数被截断,回头查半天。TaoToken 在这里的角色是统一 Key/API 通道,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end,API 地址是 https://taotoken.net/api(这个不加 UTM)。你需要先拿到 API Key,再去控制台确认模型和通道配置。

具体操作路径:

  • 模型对话入口:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
  • Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
  • 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
  • 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
  • Claude Code / Anthropic 相关:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite

拿到 Key 之后,你的后端服务在调用 Oracle 存储过程拿到数据集后,如果要经过这层通道透出,建议在配置里把 Base URL、Key、Model ID 三件套写全。以常见的settings.json或config.toml为例,路径和字段名按你项目实际来,但结构可以参考:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model_id": "你的模型ID", "timeout_ms": 30000 }

如果是 TOML 风格:

[taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model_id = "你的模型ID" timeout_ms = 30000

这里要提醒一句:存储过程返回的 Ref Cursor 数据集,字段类型如果是NUMBER、DATE,在透出到 JSON 时容易变成字符串或时间戳偏移,最好在调用端做一次显式转换,别指望通道帮你兜底。

3. 可复制配置:Ref Cursor 与临时表两套 Procedure 脚本

这一节给两套能直接跑的脚本。先建测试表,再建包,最后给调用端代码。

3.1 测试表与基础数据

-- 会话级临时表,用于演示 CREATE GLOBAL TEMPORARY TABLE sd_current_box ( box_id VARCHAR2(30), pallet_id VARCHAR2(30), material_no VARCHAR2(30), glass_qty NUMBER, unit VARCHAR2(10), status CHAR(1), create_time DATE ) ON COMMIT PRESERVE ROWS; INSERT INTO sd_current_box VALUES ('B001','P001','M100',10,'PCS','0',SYSDATE); INSERT INTO sd_current_box VALUES ('B002','P001','M100',20,'PCS','0',SYSDATE); INSERT INTO sd_current_box VALUES ('B003','P002','M200',15,'PCS','1',SYSDATE); COMMIT;

3.2 方案一:Ref Cursor 动态 SQL

适合逻辑能用一条 SQL 表达的场景,比如按参数拼条件、多表 Join。

CREATE OR REPLACE PROCEDURE sp_getcurrentstockpallet ( p_material_no IN VARCHAR2, p_status IN CHAR, RESULT OUT SYS_REFCURSOR ) IS v_sql VARCHAR2(2000); BEGIN v_sql := 'SELECT b.status, b.pallet_id, b.material_no, COUNT(b.box_id) box_count, SUM(b.glass_qty) total_qty, b.unit FROM sd_current_box b WHERE 1=1 '; IF p_material_no IS NOT NULL THEN v_sql := v_sql || ' AND b.material_no = ''' || p_material_no || ''''; END IF; IF p_status IS NOT NULL THEN v_sql := v_sql || ' AND b.status = ''' || p_status || ''''; END IF; v_sql := v_sql || ' GROUP BY b.status, b.pallet_id, b.material_no, b.unit'; OPEN RESULT FOR v_sql; EXCEPTION WHEN OTHERS THEN RAISE; END sp_getcurrentstockpallet;

注意拼接字符串时单引号的处理,''''这种写法容易数错,建议用绑定变量或q'[...]'替代,减少注入风险。

3.3 方案二:全局临时表 GTT

适合中间运算复杂、需要多步落地的场景。关键点是 GTT 结构在 PL/SQL 外建一次,过程里只做INSERT和查询,不要每次drop/create。

CREATE GLOBAL TEMPORARY TABLE box_tmp_table ( box_id VARCHAR2(30), pallet_id VARCHAR2(30), material_no VARCHAR2(30) ) ON COMMIT PRESERVE ROWS; CREATE OR REPLACE PROCEDURE sp_getpalletbytemptable_v1 ( RESULT OUT SYS_REFCURSOR ) AUTHID CURRENT_USER IS BEGIN DELETE FROM box_tmp_table; INSERT INTO box_tmp_table (box_id, pallet_id, material_no) SELECT box_id, pallet_id, material_no FROM sd_current_box WHERE ROWNUM < 10; COMMIT; OPEN RESULT FOR 'SELECT * FROM box_tmp_table'; EXCEPTION WHEN OTHERS THEN RAISE; END sp_getpalletbytemptable_v1;

AUTHID CURRENT_USER的作用是让过程内操作的对象按当前连接用户的权限解析,而不是按过程属主。跨 schema 调用时这个很关键,否则容易报权限不足。

3.4 调用端取值代码

Java 端用 JDBC 接 Ref Cursor:

CallableStatement cs = conn.prepareCall("{call sp_getcurrentstockpallet(?,?,?)}"); cs.setString(1, "M100"); cs.setString(2, "0"); cs.registerOutParameter(3, OracleTypes.CURSOR); cs.execute(); ResultSet rs = (ResultSet) cs.getObject(3); while (rs.next()) { System.out.println(rs.getString("pallet_id") + " " + rs.getInt("box_count")); } rs.close(); cs.close();

Go 端用godror接:

rows, err := db.QueryContext(ctx, "BEGIN sp_getcurrentstockpallet(:1,:2,:3); END;", "M100", "0", sql.Out{Dest: &refCursor})

Go 这边 Ref Cursor 的处理相对绕,建议封装一层,把*sql.Rows转成结构体切片再透出。

4. 验证请求与成功结果:字段与行数一致性检查

写完脚本别急着接接口,先在 SQL 层验证。用匿名块打开游标,逐行取,对比行数和字段。

DECLARE v_cur SYS_REFCURSOR; v_pid VARCHAR2(30); v_cnt NUMBER; v_rows NUMBER := 0; BEGIN sp_getcurrentstockpallet('M100', '0', v_cur); LOOP FETCH v_cur INTO v_pid, v_cnt; EXIT WHEN v_cur%NOTFOUND; v_rows := v_rows + 1; DBMS_OUTPUT.PUT_LINE(v_pid || ' -> ' || v_cnt); END LOOP; CLOSE v_cur; DBMS_OUTPUT.PUT_LINE('total rows: ' || v_rows); END;

预期结果:P001 -> 2,total rows: 1。如果行数对不上,先查WHERE条件拼接是否正确,再查GROUP BY字段是否漏了。

GTT 方案验证:

DECLARE v_cur SYS_REFCURSOR; v_id VARCHAR2(30); BEGIN sp_getpalletbytemptable_v1(v_cur); LOOP FETCH v_cur INTO v_id; EXIT WHEN v_cur%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_id); END LOOP; CLOSE v_cur; END;

预期输出 3 行(ROWNUM < 10且表里只有 3 条)。如果输出为空,检查DELETE后是否COMMIT,以及 GTT 的ON COMMIT模式是否和你的会话行为匹配。

透出到 TaoToken 通道后,再在调用端打一次日志,确认字段名和行数与 SQL 层一致。这一步别省,我见过NUMBER被转成String导致前端排序错乱的案例。

5. 本篇常见错排查:401、游标超限与权限不足

5.1 401 与 local proxy failed

如果你在调用 TaoToken 通道时遇到 401,先查 API Key 是否带上了Bearer前缀,以及 Key 是否在控制台被禁用。local proxy failed通常是本地代理配置和通道地址冲突,检查你的base_url是否写成了https://taotoken.net/api,别多写路径。

5.2 ORA-01000 maximum open cursors exceeded

这是 Ref Cursor 场景最高频的报错。原因一般是调用端没关ResultSet或CallableStatement。Java 端务必在finally里close(),或者用 try-with-resources。连接池的maxOpenCursors也要调,默认值往往偏小。

5.3 ORA-01031 insufficient privileges

GTT 方案里动态drop/create会触发这个。两种解法:一是grant create any table to 用户名;二是在过程上加AUTHID CURRENT_USER,让权限按当前用户解析。更推荐第二种,维护成本低。

5.4 ORA-06550 与 reading choices 类报错

ORA-06550多半是 PL/SQL 编译错误,检查OPEN RESULT FOR后面的 SQL 是否是字符串或合法查询。如果你在 Go 端看到类似reading choices的解析错误,通常是驱动对 Ref Cursor 的元数据读取不兼容,换godror或升级驱动版本。

5.5 OAuth 与 Codex auth.json 场景

如果你的调用链路里用了 Codex 的auth.json做鉴权,注意auth.json里的 token 过期时间,过期后会直接 401。CC Switch 或 Cline MCP 场景下,Base URL、Key、Model ID 三件套要写全,缺一个都会导致通道握手失败。

6. 语义一致 CTA:按场景选对入口

排障和接入相关的,直接去 API Keys 和接入文档:

  • 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

想先验证模型返回效果的,去模型对话:

  • https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_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

最后补一个实操细节:GTT 的ON COMMIT PRESERVE ROWS和ON COMMIT DELETE ROWS选哪个,取决于你的调用端是否在同一事务里多次取数。报表场景一般用PRESERVE ROWS,取完手动DELETE,避免会话复用时的脏数据。这个坑我在两个项目里都踩过,写在这里省得你再花时间。

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

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

立即咨询