1. 为什么 Oracle 存储过程返回结果集总让人卡壳
如果你是从 SQL Server 转过来的开发者,第一次在 Oracle 里写「返回一张表」的存储过程,大概率会懵:SQL Server 里直接SELECT * FROM student就完事了,Oracle 却报一堆语法错误。原因在于 Oracle 的存储过程(Procedure)本身不把结果集当作返回值,它只认OUT参数。想让调用方拿到一批行数据,标准做法是声明一个REF CURSOR(游标变量)类型的OUT参数,在过程体里OPEN ... FOR打开它,调用方再从这个游标里FETCH数据。
这套机制在数据库开发、老系统迁移、报表接口对接里非常常见。比如你正在把一个 SQL Server 的存储过程迁到 Oracle,或者要给 Java/MyBatis 提供一个返回列表的存储过程,绕不开REF CURSOR和OUT参数。本文给出一套可以直接复制运行的骨架:Package 声明、Package Body 实现、静态 SQL 与动态 SQL 两种打开方式、匿名块调用验证,以及 Java 侧拿结果集的写法。同时,如果你在用 AI 辅助写这些 SQL、做迁移改写,我会顺带说明怎么用 TaoToken 把多个 AI 工具的 Key 和 API 通道统一管起来,避免每个工具单独配一遍。
先说清楚适用人群:有基础 SQL 能力、需要在 Oracle 里返回结果集的开发者;正在做 SQL Server 到 Oracle 迁移、需要对照改写存储过程的人;以及想用 AI 工具辅助生成/审查 PL/SQL 的工程师。下面所有代码都在 Oracle 11g/12c/19c 的 SQL*Plus 或 SQL Developer 里验证过,你可以直接建表跑通。
2. 前置准备:TaoToken 统一 Key 与 API 通道
写存储过程本身不需要任何外部服务,但实际开发里你往往会同时用几个 AI 工具:一个在 IDE 里补全 PL/SQL,一个在命令行里做迁移改写,还有一个在网页端问报错。每个工具都要单独填 API Key、单独配 base_url,换一个模型就得改一遍配置,很烦。TaoToken 的作用就是把这些工具的 Key 和 API 通道收敛到一处:你申请一个 Key,各工具都指向同一个 API 地址,模型切换在服务端完成,本地配置只维护一份。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个地址不加 UTM 参数,配置里直接写它)。你需要先去控制台创建一个 API Key,后面配置文件里会用到。
注意:TaoToken 是 AI 工具的 API 通道管理服务,和 Oracle 数据库本身没有关系。它解决的是「你写 SQL 时用的 AI 助手怎么统一配置」这个问题,不要把它当成数据库中间件。
拿到 Key 之后,不同工具的配置方式不一样。下面给两个最常见的骨架:一个是 VS Code 系插件常用的settings.json,一个是命令行工具常用的config.toml。你按自己实际用的工具挑一个改就行。
settings.json骨架(适用于支持 OpenAI 兼容接口的编辑器插件):
{ "ai.provider": "openai-compatible", "ai.baseUrl": "https://taotoken.net/api", "ai.apiKey": "sk-你的TaoToken密钥", "ai.model": "claude-sonnet-4-20250514", "ai.temperature": 0.2 }config.toml骨架(适用于命令行 AI 编码工具):
[provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的TaoToken密钥" [model] name = "claude-sonnet-4-20250514" max_tokens = 8192 temperature = 0.2把api_key换成你在控制台生成的那串,model换成你实际要用的模型名。这样配好之后,你在编辑器里让它帮你写REF CURSOR的 Package 骨架,或者在命令行里让它把一段 SQL Server 的SELECT改写成 Oracle 的OPEN ... FOR,走的都是同一条通道。如果你后面要长期做编码和 Agent 类任务,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合高频调用场景。
3. 可复制配置:REF CURSOR + OUT 参数的完整骨架
这一节是核心。我按「建表 → 建 Package 声明 → 建 Package Body → 调用验证」的顺序给全套代码,你从头到尾复制执行即可。
3.1 建一张测试表并插入数据
先准备数据,后面所有例子都基于这张student表:
CREATE TABLE student ( id NUMBER(10) PRIMARY KEY, name VARCHAR2(50), sex VARCHAR2(10), address VARCHAR2(200), postcode VARCHAR2(20), birthday DATE ); INSERT INTO student VALUES (1, '张三', '男', '北京市海淀区', '100080', DATE '1995-03-12'); INSERT INTO student VALUES (2, '李四', '女', '上海市浦东新区', '200120', DATE '1996-07-25'); INSERT INTO student VALUES (3, '王五', '男', '广州市天河区', '510630', DATE '1994-11-08'); COMMIT;3.2 Package 声明:定义 REF CURSOR 类型和过程接口
Oracle 里返回结果集,推荐用 Package 把「游标类型」和「过程接口」放在一起,这样调用方引用类型时路径清晰。注意TYPE ... IS REF CURSOR必须定义在 Package 声明里,不能只写在过程内部,否则外部调用方拿不到这个类型。
CREATE OR REPLACE PACKAGE pkg_student AS -- 声明一个强类型/弱类型游标,这里用弱类型,灵活度高 TYPE myrctype IS REF CURSOR; -- 过程接口:p_id 为 0 时返回全部,否则按 id 过滤 PROCEDURE get_student(p_id IN NUMBER, p_rc OUT myrctype); END pkg_student; /这里p_rc OUT myrctype就是返回结果集的关键:调用方传入一个游标变量,过程负责打开它。
3.3 Package Body:静态 SQL 与动态 SQL 两种打开方式
过程体里用OPEN p_rc FOR ...打开游标。静态 SQL 直接写死语句,动态 SQL 用字符串拼接加USING绑定变量。两种都要会,因为迁移场景里动态条件很常见。
CREATE OR REPLACE PACKAGE BODY pkg_student AS PROCEDURE get_student(p_id IN NUMBER, p_rc OUT myrctype) IS v_sql VARCHAR2(500); BEGIN IF p_id = 0 THEN -- 静态 SQL:直接返回全表 OPEN p_rc FOR SELECT id, name, sex, address, postcode, birthday FROM student ORDER BY id; ELSE -- 动态 SQL:用 :w_id 占位,USING 绑定,避免拼接注入 v_sql := 'SELECT id, name, sex, address, postcode, birthday FROM student WHERE id = :w_id'; OPEN p_rc FOR v_sql USING p_id; END IF; END get_student; END pkg_student; /注意:动态 SQL 里千万不要用
'WHERE id = ' || p_id这种字符串拼接,一是注入风险,二是每次不同 id 都会产生硬解析,性能差。用:w_id加USING才是正确姿势。
3.4 用 Function 返回结果集(原理相同)
有些团队习惯用 Function 返回游标,写法上把OUT参数换成RETURN值即可,内部逻辑一模一样:
CREATE OR REPLACE PACKAGE pkg_student_fn AS TYPE myrctype IS REF CURSOR; FUNCTION get_student_fn(p_id IN NUMBER) RETURN myrctype; END pkg_student_fn; / CREATE OR REPLACE PACKAGE BODY pkg_student_fn AS FUNCTION get_student_fn(p_id IN NUMBER) RETURN myrctype IS rc myrctype; v_sql VARCHAR2(500); BEGIN IF p_id = 0 THEN OPEN rc FOR SELECT id, name, sex, address, postcode, birthday FROM student ORDER BY id; ELSE v_sql := 'SELECT id, name, sex, address, postcode, birthday FROM student WHERE id = :w_id'; OPEN rc FOR v_sql USING p_id; END IF; RETURN rc; END get_student_fn; END pkg_student_fn; /Function 和 Procedure 的选择:如果调用方是 Java 的CallableStatement且习惯用registerOutParameter,Procedure 更自然;如果是在 SQL 语句里直接调用(比如SELECT pkg.get(...) FROM dual),Function 更方便。迁移时按原系统的调用方式选。
4. 验证请求:匿名块与 SQL*Plus 里跑通结果集
代码建好了,得验证它真的返回了数据。下面给三种验证方式,从简单到完整。
4.1 匿名块 + DBMS_OUTPUT 打印
这是最快的验证方式,在 SQL Developer 或 SQL*Plus 里执行:
SET SERVEROUTPUT ON; DECLARE v_rc pkg_student.myrctype; v_id student.id%TYPE; v_name student.name%TYPE; v_sex student.sex%TYPE; BEGIN -- 传 0 返回全部 pkg_student.get_student(0, v_rc); LOOP FETCH v_rc INTO v_id, v_name, v_sex; EXIT WHEN v_rc%NOTFOUND; DBMS_OUTPUT.PUT_LINE('id=' || v_id || ', name=' || v_name || ', sex=' || v_sex); END LOOP; CLOSE v_rc; END; /预期输出:
id=1, name=张三, sex=男 id=2, name=李四, sex=女 id=3, name=王五, sex=男把pkg_student.get_student(0, v_rc)改成pkg_student.get_student(2, v_rc),应该只输出李四那一行,说明动态 SQL 分支也正常。
4.2 SQL*Plus 的 REFCURSOR 打印
如果你在 SQL*Plus 里,可以用内置的print直接输出游标:
VARIABLE rc REFCURSOR; EXEC pkg_student.get_student(0, :rc); PRINT rc;这种方式不用写FETCH循环,适合快速看结果。
4.3 Java 侧调用(MyBatis / JDBC)
实际项目里调用方多半是 Java。用 JDBC 的CallableStatement拿结果集:
CallableStatement cs = conn.prepareCall("{ call pkg_student.get_student(?, ?) }"); cs.setInt(1, 0); cs.registerOutParameter(2, OracleTypes.CURSOR); cs.execute(); ResultSet rs = (ResultSet) cs.getObject(2); while (rs.next()) { System.out.println(rs.getInt("id") + " - " + rs.getString("name")); } rs.close(); cs.close();MyBatis 里则是在 XML 中把statementType设为CALLABLE,mode=OUT的jdbcType用CURSOR,返回类型映射成resultMap。核心点就一个:OUT参数注册成游标类型,执行后从该参数取ResultSet。
5. 本篇常见错排查
这一节列几个我实际踩过的坑,基本都是REF CURSOR和OUT参数相关的。
报错 ORA-06550 / PLS-00306:参数类型或个数不对。最常见的原因是 Package 声明和 Body 里的过程签名不一致,比如声明里是p_rc OUT myrctype,Body 里写成了p_rc OUT SYS_REFCURSOR。两者虽然兼容,但类型名不一致时调用方引用会出问题。统一用 Package 里声明的类型名。
报错 ORA-01000:超出最大打开游标数。调用方FETCH完没有CLOSE v_rc。游标是资源,用完必须关。Java 侧同理,ResultSet和CallableStatement都要关,建议用 try-with-resources。
调用方拿不到数据,游标是空的。检查OPEN ... FOR的 SQL 是否真的匹配到行。动态 SQL 分支里USING的变量顺序要和:w_id占位顺序一致,多个占位符时顺序错了不会报错,但结果不对,很难查。
Package Body 编译不过,提示表或视图不存在。多半是权限问题:当前用户对student表没有SELECT权限,或者表在别的 schema 下没加前缀。用SELECT * FROM user_tab_privileges确认一下。
动态 SQL 字符串超过 500 字符被截断。VARCHAR2(500)在复杂 SQL 下不够用,改成VARCHAR2(32767)或者用CLOB。这个错误不会报错,只是 SQL 被悄悄截断,结果莫名其妙。
用 AI 工具生成 PL/SQL 时,模型给的代码用了 SQL Server 语法。这是迁移场景的高频问题。你可以在配置好 TaoToken 的编辑器里,明确告诉模型「目标数据库是 Oracle,结果集用 REF CURSOR + OUT 参数返回,不要用 SELECT 直接返回」,它生成的骨架就对了。如果生成结果还是不对,去模型对话页面对话调试:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,把报错原文贴进去让它改,比反复猜快得多。
6. 把 Key 和通道收拢,专注写 SQL
回到工具链这件事。上面这些存储过程代码,你完全可以让 AI 助手帮你生成初稿、做 SQL Server 到 Oracle 的语法转换、审查动态 SQL 的绑定变量写法。真正费时间的不是写,而是每个工具都要重新配一遍 Key 和地址。用 TaoToken 把settings.json和config.toml里的base_url统一指向https://taotoken.net/api,Key 只维护一份,换模型时改一个字段就行。
如果你还没建 Key,去控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入细节和参数说明看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你用的是 Claude Code 这类命令行编码工具,配置参考:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
配好之后,你在这个通道里让 AI 帮你写pkg_student的变体、生成测试数据、把匿名块改成 Java 调用,都是同一套配置。存储过程本身跑通了,工具链也别再一个个手动配——把省下来的时间用在排查ORA-01000这种真问题上。