资讯动态

Mybatis调用存储过程返回sys_refcursor,对象集合映射配置与验证

发布时间:2026/10/24 7:25:42 来源:尧图企业网站定制
1. Oracle 存储过程返回 sys_refcursor 时 Mybatis 映射对象集合到底卡在哪先说清楚要解决的是什么问题。Oracle 存储过程没有像 MySQL 那样直接SELECT返回结果集的能力它输出多行数据的标准做法是开一个SYS_REFCURSOR游标作为 OUT 参数调用方拿到游标后再逐行取。Mybatis 作为持久层框架默认擅长处理SELECT语句到实体的映射但遇到「存储过程 游标 OUT 参数」这种组合配置项就变得很讲究statementType要改成CALLABLE游标参数要声明jdbcTypeCURSOR和javaTypeResultSet还得额外挂一个resultMap告诉 Mybatis 游标里的列该往哪个实体类塞。这三者缺一个运行时就给你抛异常或者返回空集合。这个场景适合谁适合正在做 Oracle 老系统维护、权限菜单树加载、报表数据导出这类功能的 Java 后端。典型症状是存储过程在 PL/SQL Developer 里跑得好好的游标能打开能看到数据但一进 Mybatis 就变成null或者报Invalid column type、ORA-01000之类的错。很多人第一次配的时候会把游标参数写成普通 OUT 参数结果 Mybatis 试图把ResultSet当字符串处理直接崩。我先把整条链路拆开Oracle 端定义一个带SYS_REFCURSOROUT 参数的存储过程Java 端 Dao 方法接收一个Map因为存储过程有多个 IN/OUT 参数用 Map 承载最灵活Mybatis XML 里用{call ...}语法调用游标参数绑定resultMapService 层从 Map 里取出游标映射后的List注意不能直接赋值引用要用clear()addAll()回填到调用方传入的集合里。这条链路里最容易翻车的是两个点一是 XML 里resultMapcursorMap这个属性漏写导致 Mybatis 不知道游标列映射到哪个实体二是 Service 层用menus (List)map.get(menus)直接赋值改变了局部变量引用调用方手里的集合还是空的。下面按「前置准备 → 可复制配置 → 验证 → 排错」的顺序走一遍每一步都给能直接粘贴的代码。你照着配目标是让游标里的行变成ListFrmMenu字段对得上、空值不炸。2. 接入前把 TaoToken 的 Key 和模型 ID 准备好这一节讲前置。虽然本篇核心是 Mybatis 配置但你在调试过程中大概率需要让 AI 帮你读报错、补全 resultMap 字段、解释jdbcTypeCURSOR的行为这时候有一个稳定的模型调用入口会省很多来回。我用 TaoToken 的 API 做这类辅助排查它的接口兼容 OpenAI 风格配起来不折腾。第一步打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册账号进控制台。第二步在控制台左侧找到 API Keys 菜单新建一个 Key复制出来存好——这个 Key 只显示一次丢了只能重建。第三步确认你要用的模型 ID比如做代码补全和报错解释选一个上下文够长的对话模型即可模型 ID 在文档的模型列表里能查到。拿到三件套之后你可以在本地用 curl 先验证 Key 是否可用curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer 你的API_KEY \ -d { model: 你的模型ID, messages: [{role: user, content: 解释 Mybatis jdbcTypeCURSOR 的作用}] }返回里如果有choices[0].message.content说明 Key 和模型 ID 都对。这一步别跳过很多人后面调 Mybatis 报错结果发现是 Key 填错或者模型 ID 写了个不存在的白折腾半小时。如果你打算长期做这类代码排查和 Agent 辅助开发可以看下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 按套餐走比单次调用划算。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 里面有各语言的示例。API Keys 管理页是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 重建 Key 也在这里。想直接在网页里对话验证模型效果用模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 就行。这里提醒一句TaoToken 是模型调用入口不是数据库中间件它不参与你 Oracle 存储过程的执行链路。它的作用是当你遇到ORA-报错或者 Mybatis 映射异常时把堆栈贴给模型让它帮你定位。数据库连接、JDBC 驱动、连接池这些还是走你项目原有的配置。3. Mybatis 调用 sys_refcursor 的完整 XML 与 Java 配置片段这一节是核心给可直接复制的配置。先看 Oracle 存储过程签名假设长这样CREATE OR REPLACE PROCEDURE selectRoleMenu( cdbhs OUT SYS_REFCURSOR, jsjb IN VARCHAR2, sjjs IN VARCHAR2, M_RES OUT INTEGER, M_ERR OUT VARCHAR2 ) IS BEGIN OPEN cdbhs FOR SELECT mlbh, cdbh, cdmc, ymdz, cdtp FROM frm_menu WHERE jsjb selectRoleMenu.jsjb; M_RES : 1; M_ERR : OK; END;注意游标是第一个 OUT 参数后面跟两个 IN 参数和两个 OUT 参数。Mybatis 调用时参数顺序必须和存储过程声明一致。Dao 接口方法接收 Mapvoid selectUserMenu(Param(map) MapString, Object map);Mybatis XML 配置这是最关键的一段resultMap typecom.zlkj.frm.bean.FrmMenu idcursorMap result propertymlbh columnMLBH jdbcTypeVARCHAR/ result propertycdbh columnCDBH jdbcTypeVARCHAR/ result propertycdmc columnCDMC jdbcTypeVARCHAR/ result propertyymdz columnYMDZ jdbcTypeVARCHAR/ result propertycdtp columnCDTP jdbcTypeVARCHAR/ /resultMap select idselectUserMenu statementTypeCALLABLE parameterTypemap {call FRM_DATA_PKG.selectUserMenu( #{map.menus, modeOUT, jdbcTypeCURSOR, javaTypejava.sql.ResultSet, resultMapcursorMap}, #{map.yhdh, modeIN, jdbcTypeVARCHAR}, #{map.code, modeOUT, jdbcTypeINTEGER}, #{map.mess, modeOUT, jdbcTypeVARCHAR} )} /select几个参数逐个解释。statementTypeCALLABLE告诉 Mybatis 这是存储过程调用不是普通查询必须写。游标参数那行modeOUT表示输出jdbcTypeCURSOR是 Oracle 游标的 JDBC 类型javaTypejava.sql.ResultSet指定 Java 侧接收类型resultMapcursorMap把游标结果集和实体映射绑起来——这一项漏了Mybatis 拿到游标也不知道该 new 什么类返回的集合要么是null要么是Object数组。M_RES和M_ERR是普通 OUT 参数jdbcType分别用INTEGER和VARCHAR。Service 层回填集合这里有个坑必须避开public void selectUserMenu(ListFrmMenu menus, FrmSysUser user) { MapString, Object map new HashMap(); map.put(menus, new ArrayListFrmMenu()); map.put(yhdh, user.getYhdh()); map.put(code, null); map.put(mess, null); frmMenuDao.selectUserMenu(map); ListFrmMenu result (ListFrmMenu) map.get(menus); if (result ! null) { menus.clear(); menus.addAll(result); } }为什么不能写menus (List)map.get(menus)因为 Java 是值传递menus这个形参重新赋值只改变了方法内的局部引用调用方传进来的那个 List 对象还是空的。正确做法是clear()清空原集合再addAll()把游标结果灌进去这样调用方手里的集合才有数据。这个坑我在老项目里见过不止一次表现是「存储过程明明返回了 5 条Service 里打印有数据但 Controller 拿到的 List 是空的」。如果你用 Spring Boot数据源配置里记得把 Oracle 驱动和连接池配好application.yml大致这样spring: datasource: url: jdbc:oracle:thin://127.0.0.1:1521/ORCL username: your_user password: your_pwd driver-class-name: oracle.jdbc.OracleDriverMybatis 的 mapper 扫描路径和 XML 位置按你项目实际配这里不展开。配置齐了之后游标到ListFrmMenu的映射链路就通了。4. 用最小 Demo 验证游标映射与空值处理是否一次跑通配完不能直接上生产先写个最小可运行 Demo 验证。我一般分三步先验证存储过程本身能开游标再验证 Mybatis 能拿到集合最后验证空值和字段类型。第一步在数据库客户端里直接调存储过程确认游标有数据DECLARE v_cur SYS_REFCURSOR; v_res INTEGER; v_err VARCHAR2(200); BEGIN selectRoleMenu(v_cur, 01, 00, v_res, v_err); DBMS_OUTPUT.PUT_LINE(res || v_res || , err || v_err); -- 游标内容在客户端结果集里看 END;如果这一步游标打不开问题在存储过程不在 Mybatis先修 SQL。第二步写一个 JUnit 测试直接调 DaoRunWith(SpringRunner.class) SpringBootTest public class FrmMenuDaoTest { Autowired private FrmMenuDao frmMenuDao; Test public void testSelectUserMenu() { MapString, Object map new HashMap(); map.put(menus, new ArrayListFrmMenu()); map.put(yhdh, admin); map.put(code, null); map.put(mess, null); frmMenuDao.selectUserMenu(map); ListFrmMenu list (ListFrmMenu) map.get(menus); System.out.println(size (list null ? null : list.size())); System.out.println(code map.get(code) , mess map.get(mess)); if (list ! null !list.isEmpty()) { FrmMenu first list.get(0); System.out.println(first.mlbh first.getMlbh() , cdmc first.getCdmc()); } } }跑通的标准是size大于 0first.mlbh和first.cdmc有值code和mess是存储过程里赋的值。如果size0但数据库里有数据八成是resultMap的column和游标 SELECT 的列名大小写对不上——Oracle 默认列名大写你resultMap里写小写mlbh在某些驱动版本下匹配不到统一写大写最稳。第三步验证空值处理。把存储过程里某条记录的cdtp置为 NULL再跑一次确认实体里cdtp是null而不是抛NullPointerException。resultMap里jdbcTypeVARCHAR已经声明了类型Mybatis 遇到 NULL 会正常塞null。如果你在实体里用了基本类型int接可能为 NULL 的列会报Cannot assign null to primitive把实体字段改成包装类型Integer即可。再补一个边界游标返回 0 行时map.get(menus)拿到的是空ArrayList还是null取决于驱动实测 Oracle 驱动一般给空集合但保险起见 Service 里判null再clear()addAll()。验证完这三步游标到对象集合的映射就算一次跑通了。5. 报错对照401、local proxy failed、reading choices、OAuth 怎么排这一节把常见报错和真实堆栈对上方便你对号入座。ORA-01000: maximum open cursors exceeded——游标没关。Mybatis 调用存储过程时游标由框架管理但如果你在存储过程里手动OPEN了多个游标没CLOSE或者连接池复用连接时游标泄漏就会累积。排查查v$open_cursor看哪个 session 游标数暴涨存储过程里确保每个OPEN都有对应CLOSE连接池的maxOpenCursors适当调大。Invalid column type: 1111——jdbcType写错。Oracle 游标的 JDBC 类型码是-10OracleTypes.CURSORMybatis 里写jdbcTypeCURSOR即可别写成OTHER或REF。如果报这个错检查游标参数那行是不是漏了jdbcTypeCURSOR。java.sql.SQLException: 无效的列类型——和上面同源多半是javaType没写java.sql.ResultSetMybatis 试图用默认类型处理游标。reading choices类报错——这是你在用 TaoToken 辅助排查时模型返回体解析失败。检查请求体里model字段是否拼错、messages是否是数组、Authorization头是否带了Bearer前缀。用 curl 复现一次看返回的error.message具体说什么。401 Unauthorized——Key 无效或过期。去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 确认 Key 状态重建一个再试。注意 Key 前后别带空格。local proxy failed——本地网络到 API 端点的连接没通。检查你的出口网络、DNS 解析、是否有本地防火墙拦截。这个报错和 Mybatis 无关是模型调用链路的问题换个网络环境或稍后重试。OAuth相关报错——如果你用的是需要 OAuth 授权的客户端比如某些 IDE 插件token 过期会报这个。重新走一遍授权流程或者改用 API Key 方式直连。Cannot assign null to primitive——实体字段用了int/long接可能为 NULL 的列改成Integer/Long。Result Maps collection does not contain value for cursorMap——resultMap的 id 和 XML 里引用的名字不一致或者resultMap定义在另一个 namespace 下没加全限定名。检查resultMap idcursorMap和resultMapcursorMap是否完全一致。排错顺序建议先看数据库客户端能不能跑通存储过程再看 Mybatis 日志里实际执行的{call ...}语句最后看返回 Map 里各 key 的值。Mybatis 开logging.level.your.mapper.packageDEBUG能看到参数绑定详情。6. 后续调试与模型辅助的入口配置跑通之后日常维护中还会遇到字段增减、游标列类型变化、多数据源切换这些情况。我的习惯是把 Mybatis 的 SQL 日志级别常开在开发环境每次改resultMap后跑一遍第 4 节的 Demo 测试确认集合 size 和首条记录字段都对。存储过程那边如果加了新列resultMap里补一行result property新字段 column新列名 jdbcType.../就行不用动 Java 代码。遇到看不懂的堆栈把完整异常贴给模型让它帮你定位比翻文档快。模型调用入口用 API https://taotoken.net/api Key 在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 管理接入示例看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。Claude Code 用户走 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 这个入口配 Base URL 和 Key。长期做代码辅助就上 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 按量走单次调用用模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 验证效果。最后留一个实操细节resultMap里column的大小写跟着游标 SELECT 的列名走Oracle 默认大写你写大写最保险如果用了别名带引号的小写列名column也要对应小写。这个细节不注意表现就是集合有 size 但字段全是null排查起来很费时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑