1. 为什么 parameterType 写错就报 "无效的列类型"
先说结论:MyBatis 调 Oracle 或达梦的存储过程、函数返回游标时,parameterType必须写成java.util.Map,不是因为它"看起来方便",而是因为游标这个 OUT 参数在 JDBC 层面根本没有对应的 Java 基本类型可以承载,只能靠一个可变的容器把结果"回填"回来。
我见过太多人卡在这一步:mapper 里写了parameterType="java.util.Map",但调用时传了个实体对象,或者干脆不传参,结果控制台甩出一句无效的列类型: 1111或者ORA-17004。1111 就是java.sql.Types.OTHER,Oracle 的游标(REF CURSOR)在 JDBC 里就注册成这个类型。MyBatis 处理 CALLABLE 语句时,会遍历你传入的参数对象,把每个mode=OUT的参数按名字塞回去。如果传入的不是 Map,它没有"按名字回填"的能力,游标就丢了。
这篇面向的是正在用 Cline、CC Switch 这类 AI 工具辅助排查 MyBatis 配置的开发者。你可能会让 AI 帮你补全 mapper XML,但 AI 经常把parameterType写成实体类,因为它不知道游标要回填。下面我把可复制的骨架、TaoToken 的统一 Key 配置、以及一次调用后打印 Map 里游标键值的验证动作都写清楚,你照着改就能跑通。
核心检索词先摆出来:MyBatis 调用存储过程返回游标、parameterType 必须是 java.util.Map、游标结果回填 Map、map.get("result")取游标记录。适合谁?适合用 Oracle/达梦、需要调P_xxx或FN_xxx返回结果集、并且被 1111 报错折磨过的后端同学。
2. TaoToken 前置:把 Key 和 API 通道统一起来
在动手改 mapper 之前,先把调用链路的"通道"理顺。很多排查场景里,AI 工具(Cline、CC Switch)需要访问模型来帮你分析报错,而你的业务代码又要访问数据库。这两条链路如果 Key 散落在各处,改一次配置要翻五个文件。我的做法是用 TaoToken 做统一入口,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 地址是 https://taotoken.net/api 。
你需要先拿到一个 Key。进控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。这个 Key 同时给 AI 工具和你的调试脚本用,省得来回切。
为什么要在讲 MyBatis 的文章里提这个?因为游标报错的排查过程,本质是"让 AI 读你的 mapper XML 和异常栈,然后给出修改建议"。如果 AI 工具的模型通道没配好,你连让它帮你分析1111的机会都没有。把通道配好,后面排错效率完全不一样。
如果你只是想让 AI 帮你解释一段 mapper 配置,用模型对话就行:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。如果你在长期做编码、想让 Agent 持续读你的工程文件,那更适合 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,ClaudeCode 相关的配置参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。
3. 可复制配置:mapper XML 骨架与工具配置片段
3.1 函数返回游标的 mapper 骨架
先看函数版本。Oracle 函数返回SYS_REFCURSOR,MyBatis 里用{#{result,mode=OUT,...}=call FN_xxx(...)}这种带等号的写法,等号左边就是接收返回值的占位符。
<resultMap id="cursorMap" type="com.vcare.model.Photo"> <result column="AD_PIC_ID" property="photoId" jdbcType="INTEGER"/> <result column="SRC" property="url" jdbcType="VARCHAR"/> </resultMap> <select id="findAllPhoto" parameterType="java.util.Map" statementType="CALLABLE"> {#{result,mode=OUT,jdbcType=CURSOR,resultMap=cursorMap} = call FN_QUERY_APP_AD_PIC(#{agencyId,mode=IN,jdbcType=INTEGER})} </select>注意resultMap="cursorMap"这个属性,它告诉 MyBatis 游标里的每一行按什么规则映射成Photo对象。没有它,游标就算回填进 Map,你也拿不到结构化数据。
3.2 存储过程返回游标的 mapper 骨架
存储过程是另一种写法,游标作为普通 OUT 参数放在参数列表里,没有等号。
<select id="findAllPhotoProc" parameterType="java.util.Map" statementType="CALLABLE"> {call P_QUERY_APP_AD_PIC( #{agencyId,mode=IN,jdbcType=INTEGER}, #{result,mode=OUT,jdbcType=CURSOR,resultMap=cursorMap} )} </select>两种写法的共同点:parameterType都是java.util.Map,result这个名字随便起,不需要和存储过程里的形参名一致,它只是 Map 里的一个 key。
3.3 Cline 的 settings.json 配置片段
如果你用 Cline 辅助排查,把模型通道指向 TaoToken,Key 填你生成的那个。
{ "cline.apiProvider": "openai", "cline.openAiBaseUrl": "https://taotoken.net/api", "cline.openAiApiKey": "sk-你的TaoTokenKey", "cline.model": "claude-sonnet-4-20250514" }3.4 CC Switch 的 config.toml 配置片段
CC Switch 用 TOML,结构类似,核心是 base_url 和 api_key 两项。
[provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的TaoTokenKey" model = "claude-sonnet-4-20250514"配好之后,你让 AI 读 mapper XML,它就能基于真实文件给出修改建议,而不是凭空猜。
4. 验证请求:一次调用后打印 Map 里的游标键值
配置写完,必须验证游标真的回填了。写个最小测试,调用后把 Map 里的result取出来打印。
@Autowired private PhotoMapper photoMapper; public void testCursor() { Map<String, Object> param = new HashMap<>(); param.put("agencyId", 1001); photoMapper.findAllPhoto(param); Object cursor = param.get("result"); System.out.println("游标类型: " + cursor.getClass().getName()); if (cursor instanceof List) { List<Photo> list = (List<Photo>) cursor; System.out.println("游标记录数: " + list.size()); for (Photo p : list) { System.out.println("photoId=" + p.getPhotoId() + ", url=" + p.getUrl()); } } }跑通后你会看到类似输出:
游标类型: java.util.ArrayList 游标记录数: 3 photoId=101, url=https://cdn.example.com/a.jpg photoId=102, url=https://cdn.example.com/b.jpg photoId=103, url=https://cdn.example.com/c.jpg关键点:param.get("result")拿到的就是游标记录,类型是ArrayList,里面是按cursorMap映射好的Photo对象。如果你打印出来是null,说明 OUT 参数没回填,八成是parameterType写错了,或者传参时没传 Map。
注意:
result这个名字只是 Map 的 key,和存储过程里的形参名没有任何对应关系。你叫它cursorData、outRows都行,只要 mapper 里的占位符名和map.get()的 key 一致即可。
5. 本篇常见错排查
5.1 报 "无效的列类型: 1111"
这是最高频的错。1111 是Types.OTHER,Oracle 游标就注册成这个。原因通常是parameterType写成了实体类,或者jdbcType=CURSOR漏了。检查两处:mapper 的parameterType必须是java.util.Map,OUT 参数必须带jdbcType=CURSOR。
5.2 游标取出来是 null
传参时传了实体对象而不是 Map,或者 Map 里没有对应的 key。MyBatis 回填 OUT 参数时,是往你传入的那个 Map 里 put,如果你传的是new Photo(),它没有地方放游标。统一用HashMap。
5.3 达梦数据库上的差异
达梦的游标类型和 Oracle 基本兼容,但驱动版本不同时,jdbcType可能需要写成OTHER而不是CURSOR。如果CURSOR报错,换成jdbcType=OTHER试试。另外达梦的存储过程调用语法和 Oracle 一致,mapper 骨架不用改。
5.4 resultMap 映射不上,字段全是 null
检查resultMap里的column是否和游标返回的列名完全一致,包括大小写。Oracle 默认返回大写列名,如果你写的是小写,映射不上。达梦可能返回大写也可能返回小写,取决于建表时的设置。用column="AD_PIC_ID"这种大写形式最稳。
5.5 函数和存储过程写法混了
函数用等号:{#{result,...}=call FN_xxx(...)}。存储过程不用等号:{call P_xxx(..., #{result,...})}。写混了会报语法错误。如果你不确定,先看数据库里是 FUNCTION 还是 PROCEDURE。
6. 把 Key 和游标配置一起固化下来
排查完这一轮,你会发现真正花时间的不是写 mapper,而是"改一处配置、跑一次、看报错、再改"的循环。把 TaoToken 的 Key 固化到 Cline 的 settings.json 和 CC Switch 的 config.toml 里,让 AI 工具随时能读你的工程文件,这个循环会快很多。API 通道统一走 https://taotoken.net/api ,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 生成一次就够。
回到游标本身,记住三件事:parameterType必须是java.util.Map,OUT 参数必须带jdbcType=CURSOR和resultMap,取结果用map.get("result")。这三件事做对,Oracle 和达梦的游标都能正常回填。至于result这个名字,真的随便起,别被它和存储过程形参名的"看起来对应"骗了。