资讯详情

MyBatis 调 Oracle 存储过程返回游标,如何优雅映射到 List<Map>?TaoToken 统一 Key 通道实测

📅 2026/10/11 15:49:17 | 华诺云谱 👁 阅读
MyBatis 调 Oracle 存储过程返回游标,如何优雅映射到 List<Map>?TaoToken 统一 Key 通道实测
1. 为什么 MyBatis 调 Oracle 存储过程返回游标总踩坑很多同学第一次在 MyBatis 里调 Oracle 存储过程返回SYS_REFCURSOR都会遇到一个尴尬局面存储过程在 PL/SQL Developer 里跑得好好的一放到 Java 里就报ORA-17004: 列类型无效或者ORA-01000: 超出打开游标的最大数再不然就是ResultSet拿到了但rs.next()永远返回 false。核心原因在于 Oracle 的游标是输出参数不是普通查询结果集MyBatis 需要靠statementTypeCALLABLE加jdbcTypeCURSOR才能正确注册出参类型。这篇内容聚焦的场景很具体Oracle 端已经写好一个返回SYS_REFCURSOR的存储过程Java 端用 MyBatis 调用最终把游标里的数据映射成ListMapString, Object。适合谁看适合正在做老系统对接、报表导出、数据同步的 Java 后端尤其是那些表结构经常变、不想为每个存储过程写一个实体类的项目。ListMap的好处就是字段随游标走不用改 Java 代码。我会把整条链路拆开Oracle 包和过程怎么定义、Mapper XML 怎么写、Java 怎么调、怎么验证跑通、报错怎么排查。中间还会顺带说清楚resultMap和resultTypemap在游标场景下到底该选哪个。最后给一个统一 Key 通道的接入方式方便你在本地或测试环境快速验证不用来回改配置文件。先说结论游标映射到ListMap最稳的写法是手动遍历 ResultSet而不是指望 MyBatis 自动映射。原因后面会展开但你可以先记住这个判断。2. TaoToken 统一 Key 通道前置准备在动手写 Mapper 之前先把调用通道理顺。很多团队本地开发时数据库连接、模型调用、密钥管理是散的改一个环境要动好几处配置。我习惯用一个统一的 Key 通道来收敛这些入口TaoToken 就是干这个的它提供一个统一的 API 入口和 Key 管理模型对话、编码计划、控制台、API Keys 都在同一套体系里。你需要先拿到一个可用的 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进入控制台在 API Keys 页面创建一个 Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建时建议按环境命名比如dev-mybatis-oracle方便后面排查是哪个环境在用。API 的基础地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接用于代码里的base_url。如果你只是想先验证模型通道是否通可以用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条消息试试。如果你在做长期编码或 Agent 类项目Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 里有套餐说明。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数不确定时优先查这里。这里要强调一点TaoToken 是统一 Key 通道不是数据库代理也不是让你绕过任何合规限制的工具。它的作用是让你在多个模型/服务之间用一套 Key 和入口减少配置漂移。数据库连接本身还是走你本地的 JDBC两者互不干扰。把 Key 准备好之后我们进入正题。3. 可复制配置Oracle 包、Mapper XML 与 Java 调用这一节是全文的核心所有代码都可以直接复制。我按 Oracle 端、Mapper 端、Java 端三段来写每段都给出完整片段。3.1 Oracle 端定义包和返回游标的过程先在 Oracle 里建一个包声明一个返回SYS_REFCURSOR的过程。注意游标类型用SYS_REFCURSOR这是 Oracle 内置的弱类型游标适合字段不固定的场景。CREATE OR REPLACE PACKAGE PKG_TEST AS PROCEDURE P_TEST( P_DEPT_NO IN VARCHAR2, V_CURSOR OUT SYS_REFCURSOR ); END PKG_TEST; / CREATE OR REPLACE PACKAGE BODY PKG_TEST AS PROCEDURE P_TEST( P_DEPT_NO IN VARCHAR2, V_CURSOR OUT SYS_REFCURSOR ) IS BEGIN OPEN V_CURSOR FOR SELECT EMP_NO, EMP_NAME, SALARY, HIRE_DATE FROM EMP WHERE DEPT_NO P_DEPT_NO ORDER BY EMP_NO; END P_TEST; END PKG_TEST; /建完后在 PL/SQL Developer 里先自测一下确认游标能打开DECLARE V_CUR SYS_REFCURSOR; V_NO VARCHAR2(20); V_NM VARCHAR2(50); BEGIN PKG_TEST.P_TEST(D001, V_CUR); LOOP FETCH V_CUR INTO V_NO, V_NM; EXIT WHEN V_CUR%NOTFOUND; DBMS_OUTPUT.PUT_LINE(V_NO || - || V_NM); END LOOP; CLOSE V_CUR; END; /如果这一步就报错先别往下走问题在 Oracle 端。常见的是包体没编译通过或者EMP表字段名对不上。3.2 Mapper XMLstatementTypeCALLABLE 与 jdbcTypeCURSORMapper 接口先定义方法参数用Map承载因为游标是出参需要从同一个 Map 里取回public interface TestMapper { void testP(MapString, Object param); }Mapper XML 的关键就三处statementTypeCALLABLE、modeOUT、jdbcTypeCURSOR。缺一个都会出问题。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.TestMapper select idtestP statementTypeCALLABLE parameterTypemap {call PKG_TEST.P_TEST( #{pDeptNo, modeIN, jdbcTypeVARCHAR}, #{vCursor, modeOUT, jdbcTypeCURSOR, javaTypejava.sql.ResultSet, resultMapempMap} )} /select resultMap idempMap typejava.util.Map result columnEMP_NO propertyempNo/ result columnEMP_NAME propertyempName/ result columnSALARY propertysalary/ result columnHIRE_DATE propertyhireDate/ /resultMap /mapper这里有个取舍要讲清楚resultMap和resultTypemap在游标场景下行为不一样。如果你写resultMapMyBatis 会尝试按你定义的列名映射列名对不上就丢字段如果你写resultTypemapMyBatis 会用数据库返回的列名做 key字段随游标走。对于ListMap这种需求推荐用resultTypemap因为你要的就是动态字段。但注意resultTypemap在部分 MyBatis 版本里对游标出参支持不稳定所以更稳的做法是不依赖自动映射手动遍历 ResultSet也就是下一节的写法。3.3 Java 调用手动遍历 ResultSet 映射成 List这是最稳的一段。不要指望 MyBatis 把游标自动塞进返回值而是从入参 Map 里把ResultSet取出来自己遍历。Service public class TestService { Autowired private TestMapper testMapper; public ListMapString, Object queryByDept(String deptNo) { MapString, Object param new HashMap(); param.put(pDeptNo, deptNo); testMapper.testP(param); ListMapString, Object list new ArrayList(); ResultSet rs (ResultSet) param.get(vCursor); if (rs null) { return list; } try { ResultSetMetaData meta rs.getMetaData(); int columnCount meta.getColumnCount(); while (rs.next()) { MapString, Object row new LinkedHashMap(); for (int i 1; i columnCount; i) { String key meta.getColumnLabel(i); Object val rs.getObject(i); row.put(key, val); } list.add(row); } } catch (SQLException e) { throw new RuntimeException(读取游标失败, e); } finally { try { rs.close(); } catch (SQLException ignore) { } } return list; } }几个细节值得说。第一用LinkedHashMap而不是HashMap保证字段顺序和游标列顺序一致前端展示时不会乱。第二用getColumnLabel(i)而不是getColumnName(i)因为前者会返回别名后者在某些驱动下返回的是原始列名。第三rs.close()一定要放在finally否则游标不释放跑几次就ORA-01000。如果你确实想用 MyBatis 自动映射可以把 XML 里的resultMap换成resultTypemap然后 Java 端直接接收返回值select idtestP statementTypeCALLABLE parameterTypemap resultTypemap {call PKG_TEST.P_TEST( #{pDeptNo, modeIN, jdbcTypeVARCHAR}, #{vCursor, modeOUT, jdbcTypeCURSOR, javaTypejava.sql.ResultSet, resultMapempMap} )} /select但实测下来这种写法在不同 MyBatis 版本上表现不一致有的版本能返回 List有的返回空。所以生产环境我还是推荐手动遍历。4. 验证请求与成功结果配置写完后怎么确认真的跑通了我一般分三步验证。第一步写一个单元测试直接调 ServiceRunWith(SpringRunner.class) SpringBootTest public class TestServiceTest { Autowired private TestService testService; Test public void testQueryByDept() { ListMapString, Object list testService.queryByDept(D001); System.out.println(size list.size()); for (MapString, Object row : list) { System.out.println(row); } } }第二步看控制台输出。成功的话你会看到类似size 3 {EMP_NO1001, EMP_NAME张三, SALARY12000, HIRE_DATE2021-03-15} {EMP_NO1002, EMP_NAME李四, SALARY9800, HIRE_DATE2022-07-01} {EMP_NO1003, EMP_NAME王五, SALARY15000, HIRE_DATE2020-11-20}第三步确认游标被释放。可以在 Oracle 里查一下当前打开的游标数SELECT COUNT(*) FROM V$OPEN_CURSOR WHERE USER_NAME YOUR_USER;跑几次测试后这个数字不应该持续增长。如果一直涨说明rs.close()没生效或者 MyBatis 的SqlSession没关。如果你用的是 TaoToken 的统一 Key 通道来管理模型调用验证方式类似在模型对话页面发一条消息确认返回正常说明 Key 和通道没问题。数据库这条链路和模型通道是独立的两边都通才算环境就绪。5. 本篇常见错排查清单这一节按真实报错来每条都给出原因和改法。ORA-17004: 列类型无效。这是最常见的。原因通常是jdbcTypeCURSOR没写或者写成了jdbcTypeOTHER。Oracle 的游标必须显式声明jdbcTypeCURSOR并且javaTypejava.sql.ResultSet。检查 Mapper XML 里出参那一行三个属性缺一不可。ORA-01000: 超出打开游标的最大数。游标没关。检查 Java 代码里rs.close()是否在finally块以及SqlSession是否被正确关闭。如果你用的是 Spring 管理的事务确认方法上有Transactional否则连接可能不释放。local proxy failed / 401。这类报错通常出现在你通过统一 Key 通道调模型时Key 无效或环境变量没读到。检查base_url是否写成 https://taotoken.net/api Key 是否从 API Keys 页面正确复制有没有多余空格。401 就是鉴权失败别去改数据库配置。reading choices 报错。这是模型返回结构解析失败一般出现在你用统一通道调对话接口时。确认请求体里的model字段和文档一致响应解析按choices[0].message.content取。接入文档里有完整示例。OAuth 相关报错。如果你在配 Claude Code 或类似工具OAuth 回调地址要和控制台里登记的一致。ClaudeCodeAnthropic 的接入说明在文档里有专门章节按步骤走就行。ResultSet 拿到但 rs.next() 返回 false。两种可能一是存储过程里游标没 OPEN二是你在 Java 里提前把游标读了一次。确认 Oracle 端OPEN V_CURSOR FOR执行了Java 端只读一次。字段名全是大写或带下划线。Oracle 默认返回大写列名ListMap的 key 就是大写。如果前端要小驼峰在遍历时用meta.getColumnLabel(i).toLowerCase()转换或者用resultMap显式映射。CC Switch / Cline MCP / Codex auth.json 配置。如果你在用这些工具记住三件套Base URL 填 https://taotoken.net/api Key 填你创建的 KeyModel ID 按文档里的模型名填。三者缺一工具就连不上。6. 统一 Key 通道接入与后续建议把数据库链路跑通之后如果你还想把模型调用也收敛到同一套 Key 体系可以按下面的路径操作。先到 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 有效再到接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 查对应工具的配置格式。如果你只是临时验证模型是否可用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 最快。长期做编码或 Agent 项目的话Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 有更合适的方案。回到 MyBatis 这条链路最后给几个实用建议。第一游标出参的 Map key 命名要固定比如统一用vCursor别这次叫cursor下次叫outCursor否则维护时容易找不到。第二ListMap虽然灵活但字段类型全是Object前端拿到的日期可能是Timestamp建议在遍历时按meta.getColumnTypeName(i)做一次类型归一化。第三如果存储过程返回的游标字段特别多手动遍历的性能瓶颈在getObject可以考虑用rs.getObject(i, Class)指定类型减少装箱开销。我试过在同一个项目里混用resultMap和手动遍历最后统一成手动遍历因为排查问题时能直接打断点看 ResultSet比猜 MyBatis 映射规则快得多。你可以先按本文的代码跑通再根据自己项目的字段稳定性决定要不要换成自动映射。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑