☰
SSM-Mybatis调用Oracle存储过程返回结果集(游标):从配置到验证的完整实践
2026/10/2 12:24:41 网站建设 项目流程

1. 为什么 SSM 调 Oracle 游标存储过程总在返回结果集这一步翻车

如果你正在做 SSM(Spring + SpringMVC + Mybatis)项目,数据库是 Oracle,业务里又要求把一段复杂查询逻辑封装进存储过程、通过游标把结果集吐回 Java 层,那你大概率会遇到一个很别扭的现象:存储过程在 PL/SQL Developer 里跑得好好的,一放到 Mybatis 里就报错,要么是参数类型对不上,要么是结果集拿不到,要么干脆抛一个invalid column type或者ORA-01000之类的异常。

这个问题的本质,是 Mybatis 对 Oracle 存储过程游标出参的处理方式和 MySQL 完全不一样。MySQL 的存储过程返回结果集相对随意,直接select就能映射;而 Oracle 必须显式声明一个ref cursor类型,通过out参数把游标传出来,Mybatis 侧还要用statementType="CALLABLE"、jdbcType=CURSOR、javaType=ResultSet加上resultMap四件套配合,缺一个都跑不通。

我见过太多同学卡在parameterType上——有人写int,有人写实体类,结果全报错。实际上调用带游标出参的 Oracle 存储过程时,parameterType必须是java.util.Map,因为入参和出参要放在同一个 Map 里传递,出参的游标结果会被 Mybatis 回填到这个 Map 的对应 key 上,你再从 Map 里取出来强转成 List。

这篇内容就围绕「SSM-Mybatis 调用 Oracle 存储过程返回结果集(游标)」这条完整链路展开,从 Oracle 建包、建过程,到 Mapper XML 的statementType=CALLABLE配置、jdbcType=CURSOR出参声明、resultMap字段映射,再到 Service 层调用和 Controller 验证,每一步都给可复制的代码。适合正在做 SSM 老项目维护、或者需要对接 Oracle 存储过程的 Java 后端开发者。跟着走一遍,游标结果集映射这块基本就能一次跑通。

2. TaoToken 前置准备:把模型对话和编码辅助接进来辅助排障

在正式写代码之前,先说一个能明显提升排障效率的前置动作。调 Oracle 游标存储过程这类问题,报错信息往往很隐晦,比如ORA-06550、PLS-00306、invalid column type: 1111,光看堆栈很难定位到底是 SQL 写错了、参数类型错了还是 Mybatis 映射错了。这时候如果有一个能理解代码上下文、又能帮你解释 Oracle 报错的模型对话入口,会省很多时间。

TaoToken 提供的就是这样一个统一入口,它把模型对话、编码辅助、API 调用这几件事整合在一起。你可以通过官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 了解整体能力,API 接入地址是 https://taotoken.net/api(这个地址不加 UTM 参数,直接用于程序调用)。

具体到本篇场景,我建议你先做两件事。第一,打开模型对话入口,把 Oracle 的建包建过程脚本贴进去,让它帮你检查ref cursor类型声明和open ... for语法有没有问题。第二,把 Mybatis 的 Mapper XML 贴进去,重点确认statementType、jdbcType、mode、resultMap这几个属性是否齐全。很多时候报错就是因为少写了一个mode=OUT或者jdbcType写成了OTHER。

如果你打算长期做这类 SSM + Oracle 的维护工作,可以考虑 Coding Plan,它更适合持续性的编码和 Agent 场景,把常见的存储过程调用模板沉淀下来,下次直接复用。需要拿 Key 的话走 API Keys 页面,接入细节看接入文档。这里要提醒一句:TaoToken 是模型调用和编码辅助的入口,不是数据库中间件,也不替代你的 IDE 和 Oracle 客户端,它的价值在于帮你快速定位报错原因、生成可复制的配置片段。

前置准备做完,下面进入正题。整个链路我按「Oracle 侧 → Mybatis 侧 → Service 侧 → Controller 侧 → 验证」的顺序来写,你可以直接照着敲。

3. 可复制配置:Oracle 建包建过程 + Mapper XML 的 CALLABLE 与 CURSOR 出参

这一节是整篇的核心,配置写对了,后面基本就顺了。先看 Oracle 侧。

3.1 创建包声明游标类型

Oracle 里用游标作为out出参时,必须先在一个包里声明ref cursor类型,因为存储过程的参数类型不能直接用匿名游标。这一步很多人会漏掉,直接在建过程时写out sys_refcursor,虽然sys_refcursor也能用,但为了类型可控,建议自定义。

-- 创建一个包,用于声明游标类型 create or replace package types as type empListCursor is ref cursor; end types;

这个包只做类型声明,没有实现体,所以不需要package body。

3.2 创建带游标出参的存储过程

CREATE OR REPLACE PROCEDURE QUERYEMPSBYDEPTNO( pdeptno in Integer, empList out types.empListCursor ) is BEGIN if pdeptno = 0 then open empList for select * from emp; else open empList for select * from emp where deptno = pdeptno; end if; END QUERYEMPSBYDEPTNO;

这里pdeptno是in入参,empList是out游标出参。逻辑很简单:传 0 查全部,否则按部门编号过滤。建完之后你可以在 PL/SQL Developer 里先测一下,确认游标能正常打开。

3.3 Mapper XML 配置(重点)

这是最容易出错的地方,我把关键属性逐个标出来。

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.casic.dao.EmpMapper"> <resultMap id="resultMap3" type="com.casic.model.Emp"> <result property="empno" column="empno"/> <result property="ename" column="ename"/> <result property="job" column="job"/> <result property="mgr" column="mgr"/> <result property="hiredate" column="hiredate"/> <result property="sal" column="sal"/> <result property="comm" column="comm"/> <result property="deptno" column="deptno"/> </resultMap> <!-- statementType="CALLABLE" 表明调用的是存储过程 --> <!-- parameterType 必须是 java.util.Map,入参出参都放这个 Map --> <select id="queryEmpByDeptno" statementType="CALLABLE" parameterType="java.util.Map"> {call QUERYEMPSBYDEPTNO( #{pdeptno, mode=IN, jdbcType=INTEGER}, #{result, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=resultMap3} )} </select> </mapper>

几个必须注意的点,我用引用块强调一下。

注意:parameterType只能是java.util.Map。我试过写int或者实体类,全部报错,这点和 MySQL 调存储过程不一样。入参pdeptno和出参result都是这个 Map 的 key。

注意:出参必须写mode=OUT、jdbcType=CURSOR、javaType=ResultSet,并且用resultMap指定映射关系。少任何一个,要么报invalid column type,要么结果集为空。

3.4 Mapper 接口

package com.casic.dao; import java.util.List; import java.util.Map; import com.casic.model.Emp; public interface EmpMapper { /** * 根据部门编号加载员工信息列表 * 注意:返回的不是 List,而是通过 Map 出参回填 */ List<Emp> queryEmpByDeptno(Map<String, Object> param); }

接口方法返回List<Emp>是可以的,但实际数据是通过param这个 Map 回填的,下面 Service 层会体现。

3.5 Service 与实现类

package com.casic.service; import java.util.List; import java.util.Map; import com.casic.model.Emp; public interface EmpService { List<Emp> queryDeptEmps(Map<String, Object> param); }
package com.casic.service.impl; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.casic.dao.EmpMapper; import com.casic.model.Emp; import com.casic.service.EmpService; @Service("empService") public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public List<Emp> queryDeptEmps(Map<String, Object> param) { // 调用过程中,游标结果集已经被回填到 param 的 "result" key 上 empMapper.queryEmpByDeptno(param); // 从 Map 里取出结果集并强转 List<Emp> empList = (List<Emp>) param.get("result"); return empList; } }

这里的关键是:empMapper.queryEmpByDeptno(param)执行完之后,param.get("result")就是游标返回的 List。这个回填机制是 Mybatis 对 CALLABLE 语句的特殊处理,不需要你手动接收返回值。

3.6 Controller 与参数准备

package com.casic.controller; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import com.casic.model.Emp; import com.casic.service.EmpService; import oracle.jdbc.driver.OracleTypes; @Controller @RequestMapping("/empController") public class EmpController { @Autowired private EmpService empService; @RequestMapping("/queryEmp") public String showDeptEmps(Emp emp, Model model) { Map<String, Object> param = new HashMap<String, Object>(); // in 参数赋值 param.put("pdeptno", emp.getDeptno()); // out 参数声明,用 OracleTypes.CURSOR 占位 param.put("result", OracleTypes.CURSOR); List<Emp> emps = empService.queryDeptEmps(param); model.addAttribute("emps", emps); return "showEmps"; } }

param.put("result", OracleTypes.CURSOR)这一步是给 out 参数占位,值本身不重要,重要的是 key 要和 XML 里的#{result,...}对应上。

4. 验证请求:从页面提交到游标结果集成功映射

配置写完,接下来验证整条链路是否跑通。我按实际运行顺序拆成几步。

4.1 确认 Oracle 侧数据

先确认emp表里有数据,并且部门编号分布正常:

select deptno, count(*) from emp group by deptno order by deptno;

假设返回 10、20、30 三个部门各有若干条记录,那传 0 应该返回全部,传 10 只返回 10 号部门。

4.2 启动 SSM 项目并访问页面

项目部署到 Tomcat 后,访问类似这样的地址:

http://localhost:8080/your-app/empController/queryEmp?deptno=10

页面上的下拉框选择部门编号,点 Research 提交。如果配置正确,你会看到表格里渲染出对应部门的员工列表,序号、编号、姓名、职位、入职日期、工资等字段都正常显示。

4.3 看日志确认 CALLABLE 执行

在 Mybatis 日志里,你应该能看到类似这样的输出:

==> Preparing: {call QUERYEMPSBYDEPTNO(?, ?)} ==> Parameters: 10(Integer), 1111(Integer) <== Total: 14

这里的1111就是OracleTypes.CURSOR对应的整数值,说明 out 参数被正确识别为游标类型。Total: 14表示游标返回了 14 条记录,映射成功。

4.4 验证结果集映射是否准确

重点检查hiredate字段。Oracle 的date类型映射到 Java 的java.util.Date,页面上用fmt:formatDate格式化。如果这里显示正常,说明resultMap的字段映射没问题。如果某个字段为空,检查列名和 property 名是否大小写一致——Oracle 默认列名大写,Mybatis 的resultMap里写小写通常也能匹配,但保险起见建议和实际列名对齐。

4.5 边界验证

传deptno=0应该返回全部员工;传一个不存在的部门编号,比如deptno=99,游标会打开一个空结果集,页面表格为空但不报错。这两种情况都验证一下,确认存储过程的if/else分支和 Mybatis 的空结果集处理都正常。

5. 本篇常见错排查:401、invalid column type、ORA-06550 逐个击破

这一节把调 Oracle 游标存储过程时最常撞到的几个报错列出来,对照着排查。

5.1 invalid column type: 1111

这是最典型的报错,完整信息类似:

org.springframework.jdbc.UncategorizedSQLException: ### Error querying database. Cause: java.sql.SQLException: invalid column type: 1111

原因通常是出参的jdbcType没写或者写错了。1111是OTHER类型的编码,Mybatis 默认把未知类型当成OTHER传给 Oracle,Oracle 不认。解决办法就是显式写jdbcType=CURSOR,并且加上javaType=ResultSet和resultMap。

5.2 ORA-06550 / PLS-00306 参数个数或类型错误

ORA-06550: line 1, column 7: PLS-00306: wrong number or types of arguments in call to 'QUERYEMPSBYDEPTNO'

这个报错说明 Mybatis 传给存储过程的参数和过程定义对不上。检查两点:一是{call ...}里的参数个数是否和过程定义一致;二是入参的jdbcType是否匹配,pdeptno是Integer,对应jdbcType=INTEGER,别写成NUMERIC或DECIMAL。

5.3 结果集为空但没报错

如果日志显示Total: 0,但数据库里明明有数据,先确认param.put("result", OracleTypes.CURSOR)这一步有没有做。如果 Controller 里忘了给 out 参数占位,Mybatis 可能不会正确回填结果。另外确认 XML 里出参的 key 和 Controller 里 put 的 key 完全一致,都是result。

5.4 local proxy failed 类连接问题

如果你的项目通过某种本地代理访问数据库,偶尔会看到local proxy failed之类的连接层报错。这类问题一般和存储过程配置无关,先确认数据库连接串、监听端口、服务名是否正确,再回来排查 Mybatis 配置。别一看到报错就改 XML,先分层定位。

5.5 OAuth / 认证类报错

有些团队会把数据库访问包装成带认证的服务,这时候可能出现 OAuth 相关的 token 失效报错。这类问题同样和游标映射无关,属于接入层问题,检查 token 有效期和刷新逻辑即可。

5.6 参数 Map 的 key 拼写错误

这个不报错但结果不对,最难查。XML 里写#{pdeptno,...},Controller 里 put 的是pdeptNo,大小写不一致,Mybatis 找不到参数,可能传 null 进去,存储过程按 null 处理,返回空结果。建议 key 统一用小写加下划线或全小写。

排查顺序建议:先看日志里的Preparing和Parameters,确认参数传对了;再看Total,确认结果集条数;最后看页面字段映射。三步定位,基本不会绕远路。

6. 语义一致 CTA:把游标存储过程模板沉淀下来

游标结果集映射这块配置一旦跑通,其实是可以模板化的。statementType="CALLABLE"+jdbcType=CURSOR+javaType=ResultSet+resultMap这四件套,换个存储过程名和字段映射就能复用。我建议你把本篇的 Mapper XML 和 Service 调用代码存成一个模板文件,下次遇到类似的 Oracle 存储过程直接改。

如果你在排障过程中需要快速解释 Oracle 报错、生成对应的 Mybatis 配置片段,可以走 API Keys 拿 Key,接入方式看接入文档,把模型对话能力接到你的开发流程里。验证模型返回是否准确,用模型对话入口直接试。长期做 SSM + Oracle 维护和 Agent 辅助编码的,Coding Plan 更适合持续沉淀这类模板。

最后留一个实操建议:每次改完 Mapper XML,先把 Mybatis 日志级别调到 DEBUG,确认Preparing里的 SQL 和Parameters里的参数值都对,再去页面看结果。这一步能帮你省掉大量「改了不知道哪错了」的时间。

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

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

立即咨询