1. iBatis 调用 Oracle 存储过程返回 SYS_REFCURSOR 到底难在哪
iBatis(现在更多人叫它 MyBatis 的前身)调用 Oracle 存储过程返回SYS_REFCURSOR,是 Java 持久层里一个经典但容易翻车的场景。核心检索词就是ibatis 配置 oracle 存储过程返回 cursor 类型:它指的是在 iBatis 的sqlMap里,通过parameterMap声明一个jdbcType="ORACLECURSOR"、javaType="java.sql.ResultSet"的 OUT 参数,再配合resultMap把游标里的列映射成 Java 对象,最终在 Java 代码里用List接住结果集。适合谁?适合还在维护老系统、用 iBatis 2.x 做持久层、又必须对接 Oracle 存储过程的 Java 开发者。
为什么它难?因为这条链路涉及四个环节同时对齐:存储过程里游标不能提前close、parameterMap里参数顺序必须和?占位符一一对应、jdbcType必须写成ORACLECURSOR而不是CURSOR、Java 端必须用HashMap接收 OUT 参数。任何一个环节错位,报错信息都不会直接告诉你「是第几个参数错了」,而是抛出PLS-00306或Ref 游标无效这种让人摸不着头脑的异常。我试过在项目里调一个带 5 个参数的存储过程,光参数顺序就来回改了三遍才跑通。
这篇内容会按「存储过程怎么写 → iBatis 怎么配 → Java 怎么调 → 报错怎么排」的顺序,给出可以直接复制的sqlMap片段和最小验证步骤。你不需要先理解 iBatis 的全部机制,跟着配置链路走一遍,就能把游标结果集正确拿到手。下面先从最容易埋坑的存储过程本身讲起。
2. 存储过程与 iBatis 前置准备:SYS_REFCURSOR 声明和 sqlMap 骨架
在动 iBatis 配置之前,先把 Oracle 这边的存储过程写对。很多Ref 游标无效的根因不在 iBatis,而在存储过程里手贱加了close outCursor。游标是「借」给调用方的,存储过程负责open,结果集由 Java 端消费完再释放,过程内部绝对不能关。
先看一个最小可用的存储过程,返回单列游标:
create or replace procedure ww(outCursor out SYS_REFCURSOR) as begin open outCursor for select PAY_ORDER from st_bank_payinfo_temp_t; -- 注意:这里不能 close outCursor end ww;如果你需要返回多列,把select换成多字段即可,比如select ONLYID, SEARCHNO from ...。存储过程编译通过后,可以在 SQL 客户端里先单独验证一次:
var c refcursor; exec ww(:c); print c;能打印出结果集,说明存储过程这端没问题,问题就一定出在 iBatis 配置或 Java 调用上。这一步能帮你把排查范围砍掉一半。
接下来是 iBatis 的sqlMap骨架。一个完整的调用链需要三块:resultMap(列到属性的映射)、parameterMap(入参和出参声明)、procedure(调用语句)。三者通过id互相引用。resultMap的class指向你的 VO 全限定名,column是游标结果集的列名,property是 VO 的属性名,大小写要和数据库列名严格对应,Oracle 默认返回大写列名。
parameterMap是重灾区。每个<parameter>的mode决定它是 IN 还是 OUT,jdbcType决定 JDBC 驱动怎么绑定。游标参数必须写成jdbcType="ORACLECURSOR",javaType="java.sql.ResultSet",并且带上resultMap指向前面定义的映射。参数在parameterMap里的书写顺序,就是它们在?占位符里的顺序,这一点后面会反复强调。
procedure标签里的调用语句,推荐写成{call ww(?)}这种纯占位符形式。网上很多文章写{call ww()}或? = call ww(),前者会漏掉 OUT 参数绑定,后者在 iBatis 2.x 里经常触发PLS-00306。先把骨架搭对,再往里填业务字段。
3. 可复制配置:parameterMap、resultMap 与 jdbcType=CURSOR 的完整写法
这一节给出可以直接粘贴进sqlMap的完整配置。假设你的 VO 叫generalBookingVO,存储过程ww接收一个 IN 参数fltDispatchplace,返回三个 OUT 参数searchNo、flag、msg,外加一个游标result。
先写resultMap,把游标结果集的列映射到 VO 属性:
<resultMap id="corp-map" class="com.example.vo.generalBookingVO"> <result column="ONLYID" property="onlyid" /> <result column="SEARCHNO" property="searchNo" /> </resultMap>再写parameterMap,注意游标参数放在最后,且jdbcType用ORACLECURSOR:
<parameterMap class="java.util.HashMap" id="curseHashMap"> <parameter property="fltDispatchplace" jdbcType="VARCHAR" javaType="java.lang.String" mode="IN" /> <parameter property="searchNo" jdbcType="VARCHAR" javaType="java.lang.String" mode="OUT" /> <parameter property="flag" jdbcType="VARCHAR" javaType="java.lang.String" mode="OUT" /> <parameter property="msg" jdbcType="VARCHAR" javaType="java.lang.String" mode="OUT" /> <parameter property="result" jdbcType="ORACLECURSOR" javaType="java.sql.ResultSet" resultMap="corp-map" mode="OUT" /> </parameterMap>最后是procedure调用语句:
<procedure id="getPackage" parameterMap="curseHashMap"> <![CDATA[{call ww(?)}]]> </procedure>这里有几个必须对齐的点。第一,parameterMap里参数的顺序是fltDispatchplace → searchNo → flag → msg → result,那么存储过程的形参顺序也必须是这个顺序,{call ww(?)}里的?会按这个顺序依次绑定。第二,jdbcType="ORACLECURSOR"是 iBatis 对 Oracle 游标的专用类型名,写成CURSOR或REF都会绑定失败。第三,resultMap="corp-map"必须和上面resultMap的id完全一致,大小写敏感。
如果你用的是SqlMapClient的 XML 配置文件,还需要确认sqlMapConfig里已经加载了这个sqlMap文件:
<sqlMapConfig> <sqlMap resource="com/example/sqlmap/BookingSqlMap.xml" /> </sqlMapConfig>配置写完后,建议先用一个空HashMap跑一次,确认没有 XML 解析错误。iBatis 在启动时会校验parameterMap和resultMap的引用关系,如果resultMap的id写错,启动阶段就会报resultMap not found,这比运行时报错好排查得多。
4. 验证请求:Java 端用 HashMap 接收游标结果集并打印
配置就绪后,Java 端的调用方式决定了你能不能拿到游标。关键点:必须用HashMap作为参数对象,因为 OUT 参数是通过 key 回填到 map 里的,用 VO 接收 OUT 参数在 iBatis 2.x 里支持很差。
SqlMapClient client = SqlMapClientBuilder.buildSqlMapClient(reader); HashMap<String, Object> mm = new HashMap<String, Object>(); mm.put("fltDispatchplace", "SHANGHAI"); client.insert("PY_PAY_DETAIL_INFO_T.getPackage", mm); List<generalBookingVO> list = (List<generalBookingVO>) mm.get("result"); System.out.println("游标返回条数: " + (list == null ? 0 : list.size())); for (generalBookingVO vo : list) { System.out.println(vo.getOnlyid() + " | " + vo.getSearchNo()); } System.out.println("searchNo=" + mm.get("searchNo")); System.out.println("flag=" + mm.get("flag")); System.out.println("msg=" + mm.get("msg"));注意这里用的是insert而不是queryForList。因为存储过程调用在 iBatis 里被当作更新操作处理,insert会执行procedure并回填 OUT 参数。如果你用queryForList,游标结果不会被回填到 map,mm.get("result")会是null。
mm.get("result")返回的实际上是一个List,iBatis 已经帮你把ResultSet按resultMap转换成了 VO 列表。如果返回null,先检查parameterMap里游标参数的property是不是叫result,Java 端取的 key 必须和它一致。
一个最小验证流程:先跑存储过程确认能出数据,再跑 iBatis 配置确认启动无报错,最后跑 Java 调用打印条数和字段。三步都通过,说明整条链路打通。如果中间某步失败,对照下一节的报错表定位。
5. 本篇常见错排查:PLS-00306、Ref 游标无效与 401 类报错对照
调通过程中我踩过的坑基本集中在下面几类,对照真实报错逐条排查效率最高。
PLS-00306: 调用 'P_HR_CUSTOMER_INFO' 时参数个数或类型错误。这个报错几乎都是参数顺序或个数不匹配。检查parameterMap里<parameter>的数量和顺序,是否和存储过程形参完全一致。特别注意:如果存储过程第一个参数是 OUT 游标,而你在parameterMap里把它放在最后,就会报这个错。另外{call ww()}这种漏掉?的写法也会触发它。
ORA-06550 / PL/SQL: Statement ignored。通常伴随PLS-00306出现,是上一条的连带错误,解决参数匹配后自动消失。
Ref 游标无效 / retrieval of output parameters failed。根因是存储过程内部close outCursor了。游标一旦关闭,Java 端再去读就是无效引用。把close语句删掉即可。另一个可能是jdbcType写成了CURSOR而不是ORACLECURSOR,驱动无法正确绑定。
resultMap not found。parameterMap里游标参数的resultMap属性值和resultMap的id不一致,或者resultMap定义在parameterMap之后但没被正确加载。把resultMap放在parameterMap前面。
mm.get("result") 返回 null。三种可能:用了queryForList而不是insert;parameterMap里游标参数的property名和 Java 端取的 key 不一致;存储过程没有真正open游标。
local proxy failed / 401 类报错。这类通常出现在你通过 HTTP 接口或网关调用后端服务时,和 iBatis 本身无关,属于鉴权或网络层问题。检查请求头里的 Key 是否正确、Base URL 是否可达。如果你在联调阶段用 API 网关转发存储过程调用,确认网关没有把 OUT 参数截断。
排查顺序建议:先看 Oracle 端存储过程能否单独执行,再看 iBatis 启动日志有无resultMap引用错误,最后看 Java 端 map 里的 key 是否齐全。按这个顺序,90% 的报错能在五分钟内定位。
6. 语义一致 CTA:把存储过程调用接进你的持久层工作流
配置跑通之后,下一步是把它接进日常开发流。如果你在本地调试存储过程调用,需要频繁验证模型输出或接口返回,可以用模型对话快速比对结果:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 配合接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 把请求链路固定下来,避免每次手改参数。
长期做 Java 持久层和 Agent 类编码的,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,把存储过程调用、参数映射、结果集解析这类重复劳动沉淀成可复用的配置模板。API Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 管理,控制台入口是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。Claude Code 相关的接入配置参考 https://taotoken.net/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,官网总入口 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。
最后留一个实用技巧:把parameterMap和resultMap抽成独立的sqlMap片段文件,按存储过程名命名,比如BookingProcSqlMap.xml。这样下次新增存储过程时,复制骨架改字段就行,不用重新踩一遍参数顺序的坑。游标参数永远放最后,jdbcType永远写ORACLECURSOR,存储过程里永远不close游标——这三条记住,基本不会再翻车。