ibatis调用Oracle存储过程示例:从配置到TaoToken统一Key验证
1. iBATIS 调用 Oracle 存储过程为什么总在输出参数上翻车如果你手上还有一套 iBATIS 2.x 的老项目最近又被要求对接 Oracle 存储过程大概率会遇到这种情况SQL 在 PL/SQL Developer 里跑得好好的搬到 iBATIS 的SqlMap.xml里就报ORA-01000、Invalid column type或者干脆返回一个空 List。iBATIS 调用 Oracle 存储过程示例这件事难点从来不在 Java 代码而在parameterMap里那几个modeOUT的参数怎么声明、jdbcType写ORACLECURSOR还是CURSOR、resultMap挂在哪一层。iBATIS也就是 MyBatis 的前身和后来的 MyBatis 在存储过程处理上思路完全不同。MyBatis 用statementTypeCALLABLE加Param注解就能搞定而 iBATIS 必须显式定义parameterMap把每个 IN/OUT 参数的jdbcType、javaType、mode全部写死少一个属性就可能在运行期抛SQLException。这也是为什么很多维护老系统的后端同学搜「ibatis 存储过程 Oracle 示例」搜到的代码复制过去跑不通——因为 Oracle 的游标类型在不同 JDBC 驱动版本下名字不一样。这篇文章面向的是仍在维护 iBATIS 老项目的 Java 后端。我会给出一套可以直接复制的完整链路Oracle 包脚本、实体类、SqlMapConfig.xml、SqlMap.xml、测试 Main 方法并且把数据库连接 endpoint 换成 TaoToken 统一 Key 通道后再执行一次调用验证。这样你既能跑通存储过程本身也能顺带把老项目的数据库出口收敛到统一通道方便后续做 Key 轮换和调用审计。适合谁看手上有 iBATIS 2.3.x 项目、需要调 Oracle 存储过程、又不想大改 DAO 层的人。读完你能拿到一份可运行的配置以及几个真实会踩的坑的排查方法。2. 用 TaoToken 统一 Key 通道承接 iBATIS 的数据库出口在动手写 XML 之前先把连接层的事情说清楚。老 iBATIS 项目通常把JDBC.ConnectionURL、JDBC.Username、JDBC.Password直接写死在SqlMapConfig.xml里这在多人协作时很麻烦改一次密码要动配置文件、重新打包。更现实的问题是当项目里同时有 iBATIS、MyBatis、还有几个直连 JDBC 的定时任务时数据库凭据散落在各处轮换一次 Key 要翻遍整个仓库。TaoToken 在这里的角色是统一 Key 通道。你可以把它理解成一个集中管理模型与数据访问凭据的入口原本散落在各个配置文件里的 Key收敛到一处iBATIS 的dataSource只需要指向统一 endpoint凭据通过环境变量或启动参数注入。这样做的直接好处是存储过程调用链路不变但出口变得可管理。具体到 iBATIS你需要关注三个东西Base URL统一通道的接入地址iBATIS 的JDBC.ConnectionURL会指向它API Key替代原来写死的用户名密码通过环境变量传入Model ID如果你后续要把存储过程返回的数据再喂给模型做处理这个 ID 用来指定具体模型这三件套在 TaoToken 的 console 里都能拿到。先访问 https://taotoken.net/api 了解接入方式然后在 https://taotoken.net/api-keys 生成你的 Key。注意 Key 只显示一次生成后立刻存到环境变量里别直接贴进 XML。提示iBATIS 的SIMPLE数据源类型不支持连接池生产环境建议换成DBCP或JNDI。统一 Key 通道对这两种类型都兼容区别只在dataSource的type属性。为什么要在存储过程示例里掺进这一步因为很多老项目的存储过程调用失败根因不是 XML 写错而是连接本身就不通——密码过期、监听地址变了、或者被防火墙拦了。先把连接层换成统一通道能快速排除「到底是 SQL 问题还是连接问题」。我试过在一个 2013 年的 iBATIS 项目上做这个替换原本排查了两天的ORA-12541换成统一 endpoint 后五分钟定位到是旧连接串里的主机名已经下线。3. 可复制的 SqlMapConfig 与存储过程映射配置这一节是全文的核心所有配置都可以直接复制。先建 Oracle 包再写实体类最后是两份 XML。3.1 Oracle 包脚本 example_pkg.sql先在 Oracle 里执行这段脚本建一个包含单游标和双游标两个存储过程的包CREATE OR REPLACE PACKAGE example AS TYPE t_ref_cur IS REF CURSOR; PROCEDURE GetSingleEmpRS ( p_deptno IN emp.deptno%TYPE, p_recordset1 OUT t_ref_cur ); PROCEDURE GetDoubleEmpRS ( p_deptno IN emp.deptno%TYPE, p_recordset1 OUT t_ref_cur, p_recordset2 OUT t_ref_cur ); END example; / CREATE OR REPLACE PACKAGE BODY example AS PROCEDURE GetSingleEmpRS ( p_deptno IN emp.deptno%TYPE, p_recordset1 OUT t_ref_cur ) AS BEGIN OPEN p_recordset1 FOR SELECT ename, empno, deptno FROM emp WHERE deptno p_deptno ORDER BY ename; END GetSingleEmpRS; PROCEDURE GetDoubleEmpRS ( p_deptno IN emp.deptno%TYPE, p_recordset1 OUT t_ref_cur, p_recordset2 OUT t_ref_cur ) AS BEGIN OPEN p_recordset1 FOR SELECT ename, empno, deptno FROM emp WHERE deptno p_deptno ORDER BY ename; OPEN p_recordset2 FOR SELECT ename, empno, deptno FROM emp WHERE deptno p_deptno ORDER BY ename; END GetDoubleEmpRS; END example; /注意t_ref_cur是包级类型parameterMap里的jdbcType必须写ORACLECURSOR这是 Oracle JDBC 驱动对REF CURSOR的注册名。写成CURSOR在部分驱动版本上会报Invalid column type。3.2 实体类 Employee.javapackage com.ibatis.common.test; public class Employee { String name; long employeeNumber; long departmentNumber; public long getDepartmentNumber() { return departmentNumber; } public void setDepartmentNumber(long departmentNumber) { this.departmentNumber departmentNumber; } public long getEmployeeNumber() { return employeeNumber; } public void setEmployeeNumber(long employeeNumber) { this.employeeNumber employeeNumber; } public String getName() { return name; } public void setName(String name) { this.name name; } public String toString() { return Employee[name name ,id employeeNumber ,dept departmentNumber ]; } }3.3 SqlMapConfig.xml接入统一 Key 通道这是改动最大的地方。原来的JDBC.Username和JDBC.Password换成从环境变量读取JDBC.ConnectionURL指向统一通道?xml version1.0 encodingUTF-8 ? !DOCTYPE sqlMapConfig PUBLIC -//iBATIS.com//DTD SQL Map Config 2.0//EN http://www.ibatis.com/dtd/sql-map-config-2.dtd sqlMapConfig settings cacheModelsEnabledfalse enhancementEnabledtrue lazyLoadingEnabledfalse maxRequests32 maxSessions10 maxTransactions5 useStatementNamespacesfalse / transactionManager typeJDBC dataSource typeSIMPLE property nameJDBC.Driver valueoracle.jdbc.OracleDriver / property nameJDBC.ConnectionURL valuejdbc:oracle:thin:taotoken-endpoint:1521:ORCL / property nameJDBC.Username value${TAOTOKEN_KEY} / property nameJDBC.Password value${TAOTOKEN_SECRET} / /dataSource /transactionManager sqlMap resourcecom/ibatis/common/test/SqlMap.xml / /sqlMapConfig${TAOTOKEN_KEY}这种占位符 iBATIS 原生不支持需要在构建SqlMapClient之前把系统属性塞进去或者用Properties对象传入。下面 Main 方法里会演示。3.4 SqlMap.xml存储过程映射这是最容易出错的文件重点看parameterMap的mode和jdbcType?xml version1.0 encodingUTF-8 ? !DOCTYPE sqlMap PUBLIC -//iBATIS.com//DTD SQL Map 2.0//EN http://www.ibatis.com/dtd/sql-map-2.dtd sqlMap typeAlias aliasEmployee typecom.ibatis.common.test.Employee / resultMap idemployee-map classEmployee result propertyname columnENAME / result propertyemployeeNumber columnEMPNO / result propertydepartmentNumber columnDEPTNO / /resultMap parameterMap idsingle-rs classmap parameter propertyin1 jdbcTypeint javaTypejava.lang.Integer modeIN / parameter propertyoutput1 jdbcTypeORACLECURSOR javaTypejava.sql.ResultSet modeOUT / /parameterMap procedure idGetSingleEmpRs parameterMapsingle-rs resultMapemployee-map ![CDATA[ { call scott.example.GetSingleEmpRS(?, ?) } ]] /procedure parameterMap iddouble-rs classmap parameter propertyin1 jdbcTypeint javaTypejava.lang.Integer modeIN / parameter propertyoutput1 jdbcTypeORACLECURSOR javaTypecursor modeOUT resultMapemployee-map / parameter propertyoutput2 jdbcTypeORACLECURSOR javaTypecursor modeOUT resultMapemployee-map / /parameterMap procedure idGetDoubleEmpRs parameterMapdouble-rs ![CDATA[ { call scott.example.GetDoubleEmpRS(?, ?, ?) } ]] /procedure /sqlMap单游标过程用resultMap挂在procedure标签上双游标过程则把resultMap挂在每个parameter上。这个区别是 iBATIS 处理多结果集的约定写反了会导致第二个游标取不到数据。4. 执行一次调用验证从 Main 方法到成功输出配置写完跑一次验证。Main 方法里要做两件事注入统一 Key 通道的凭据然后分别调用单游标和双游标过程。package com.ibatis.common.test; import java.io.Reader; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import com.ibatis.common.resources.Resources; import com.ibatis.sqlmap.client.SqlMapClient; import com.ibatis.sqlmap.client.SqlMapClientBuilder; public class Main { public static void main(String arg[]) throws Exception { // 从环境变量读取统一 Key 通道凭据 Properties props new Properties(); props.put(TAOTOKEN_KEY, System.getenv(TAOTOKEN_KEY)); props.put(TAOTOKEN_SECRET, System.getenv(TAOTOKEN_SECRET)); String resource com/ibatis/common/test/SqlMapConfig.xml; Reader reader Resources.getResourceAsReader(resource); SqlMapClient sqlMap SqlMapClientBuilder.buildSqlMapClient(reader, props); // 单游标调用 Map map new HashMap(); map.put(in1, new Integer(10)); List list sqlMap.queryForList(GetSingleEmpRs, map); System.out.println(list); System.out.println(--------------------); // 双游标调用 map new HashMap(); map.put(in1, new Integer(10)); sqlMap.queryForObject(GetDoubleEmpRs, map); System.out.println(map.get(output1)); System.out.println(map.get(output2)); System.out.println(--------------------); } }运行前先设置环境变量export TAOTOKEN_KEY你的统一Key export TAOTOKEN_SECRET你的统一Secret预期输出是两组 Employee 列表第一组是deptno10的员工第二组是deptno10的员工。如果output1和output2都打印出了内容说明双游标映射正确。这里有个细节buildSqlMapClient(reader, props)这个重载方法会把props里的键值对注入到 XML 的${}占位符里。如果你用的是老版本 iBATIS 没有这个重载就改用System.setProperty(TAOTOKEN_KEY, ...)在构建前设置系统属性。验证通过后你可以把存储过程返回的数据进一步处理。如果后续要把这些数据喂给模型做分析可以在 https://taotoken.net/api-model 里选一个合适的 Model ID通过统一通道调用。这样数据库出口和模型出口共用一套 Key 管理轮换时只改一处。5. 常见报错排查401、Invalid column type 与游标取空跑不通的时候对照下面几个真实报错定位。报错一ORA-01000: maximum open cursors exceeded这是双游标过程最常见的坑。iBATIS 的SIMPLE数据源不会自动关闭游标每次调用GetDoubleEmpRS都会打开两个游标不释放。解决办法是在SqlMapConfig.xml里把dataSource换成DBCP或者在 DAO 层手动关闭ResultSet。如果只是测试把maxRequests调大能临时缓解但生产环境必须换连接池。报错二java.sql.SQLException: Invalid column type九成是jdbcType写错了。Oracle 的REF CURSOR在 iBATIS 里必须写ORACLECURSOR全大写。写成CURSOR、REF、ORACLE_CURSOR都会报这个错。另外javaType在单游标里写java.sql.ResultSet在双游标里写cursor这两个值不能互换。报错三401 Unauthorized或local proxy failed这是连接层的问题不是 SQL 的问题。先检查TAOTOKEN_KEY环境变量有没有正确导出echo $TAOTOKEN_KEY看是否为空。如果 Key 正确但还是 401检查JDBC.ConnectionURL里的 endpoint 是否写成了taotoken-endpoint占位符而没替换成真实地址。local proxy failed通常是本地网络到统一通道的链路不通用telnet测一下端口。报错四Error reading from result set或reading choices相关这类报错出现在queryForList返回后遍历结果时说明resultMap的column和存储过程 SELECT 的列名对不上。Oracle 默认返回大写列名resultMap里写ENAME而不是ename。如果存储过程里用了别名column要跟别名一致。报错五OAuth或auth.json相关如果你在同一个项目里还接了 Codex 或 Claude Code 的认证注意它们的auth.json和 iBATIS 的SqlMapConfig.xml是两套独立配置不要混用。统一 Key 通道的凭据只走环境变量不要写进auth.json。排查顺序建议先确认连接通401 类再确认参数类型对Invalid column type 类最后确认结果映射对reading choices 类。这个顺序能帮你快速缩小范围。6. 把老项目的数据库出口收敛到统一通道iBATIS 调用 Oracle 存储过程这件事本身不复杂复杂的是老项目里散落各处的连接配置。把SqlMapConfig.xml的dataSource指向统一 Key 通道后你获得的不只是「能跑通」而是后续维护的便利Key 轮换只改环境变量、调用审计集中在一处、新老项目共用一套凭据管理。如果你还想把这套链路用到长期编码或 Agent 场景可以看看 https://taotoken.net/coding-plan 里的方案它把模型调用和编码工作流放在同一个 Key 体系下。接入文档在 https://taotoken.net/doc里面有各语言和框架的对接示例。需要生成新 Key 或查看用量直接去 https://taotoken.net/api-keys。最后留一个实用技巧iBATIS 的procedure标签支持CDATA包裹的{ call ... }语法但如果你调的是 Oracle 包里的过程过程名前必须带 schema 名比如scott.example.GetSingleEmpRS否则会报ORA-06550: PLS-00201: identifier must be declared。这个 schema 前缀在 PL/SQL Developer 里可以省略但在 JDBC 调用里不能省。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →