ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring 调用 Oracle 存储过程返回结果集:完整实例与 TaoToken 配置验证

Spring 调用 Oracle 存储过程返回结果集:完整实例与 TaoToken 配置验证 1. Spring 调用 Oracle 存储过程返回结果集从建表到跑通的完整链路Spring 调用 Oracle 存储过程返回结果集是很多做企业级后台、报表查询、批量数据加工的同学绕不开的一个场景。简单说就是 Java 侧通过 Spring 的JdbcTemplate或SimpleJdbcCall去执行一个 Oracle 存储过程而这个存储过程不是返回单个值而是通过SYS_REFCURSOR游标把一整张结果集吐回来Java 侧再把它映射成ListMap或者实体列表。它适合谁适合正在维护老系统、需要对接 Oracle 存量存储过程、又不想手写一堆CallableStatement样板代码的开发者。我见过太多项目里存储过程逻辑已经写好了DBA 说“你直接调就行”结果 Java 这边卡在游标注册、参数顺序、结果集读取上报错一个接一个。这篇就按“能直接复制去跑”的标准把建表、建包、Spring 配置、调用代码、验证请求、常见报错排查整条链路走一遍。同时因为现在很多团队会把模型调用凭证也统一管理我会顺带说明怎么用 TaoToken 把这类 Key/API 通道收口避免凭证散落在各个配置文件里。核心检索词先明确Spring 调用 Oracle 存储过程返回结果集关键点有三个——OracleTypes.CURSOR注册出参、CallableStatementCallback里读取ResultSet、以及 DataSource 的正确注入。下面从问题场景开始拆。2. 原问题与场景为什么直接调存储过程总是拿不到结果集先说清楚问题出在哪。Oracle 存储过程返回结果集和 MySQL 那种直接SELECT返回结果不一样。Oracle 必须通过一个OUT参数类型是REF CURSOR把结果集“挂”在这个游标上返回。Java 侧如果只把它当普通出参处理就会拿到一个游标对象而不是数据或者干脆报类型不匹配。典型场景是这样的你有一张用户表TEST_USERSDBA 写了一个包display_users_package里面有个过程display_users_proc接收一个userId前缀参数返回匹配的用户列表。你想在 Spring 里调用它拿到ListMap然后转成业务对象返回给前端。这里有几个容易踩的坑。第一参数顺序。游标出参通常放在第一个位置输入参数放后面注册的时候registerOutParameter(1, OracleTypes.CURSOR)的索引必须和过程定义一致。第二OracleTypes这个类来自ojdbc驱动不是 JDK 自带的依赖没引对会直接编译不过。第三JdbcTemplate.execute的回调里cs.getObject(1)拿到的才是ResultSet很多人写成cs.getResultSet()那是拿不到游标结果的。我试过在一个老项目里DBA 给的过程出参在第二个位置输入参数在第一个结果代码里按“游标在前”写一直报invalid column type。后来对着包定义一行行核对才改过来。所以参数顺序这件事一定要以CREATE PACKAGE里的声明为准不能凭经验猜。再补充一个场景差异如果你用的是SimpleJdbcCall它比裸JdbcTemplate更省事能自动处理游标声明但前提是你要把declareParameters配清楚并且returningResultSet的名字要和过程里open ... for的游标对应上。两种方式我都会给出来你可以按项目现状选。另外现在很多团队在 Java 服务里还会调用大模型做数据摘要、字段补全之类的辅助逻辑这些模型调用的 Key 如果和数据库凭证一样散落在application.yml里管理起来很乱。所以本文在讲完数据库链路后会用 TaoToken 做一个统一的凭证通道配置示例让数据库调用和模型调用各走各的、互不干扰。3. TaoToken 前置统一 Key 与 API 通道的配置准备在进入 Spring 配置之前先把凭证管理这块理清楚。TaoToken 在这里的角色是帮你把模型调用的 Key 和 API 通道统一收口而不是替代你的数据库连接。数据库还是连你自己的 OracleTaoToken 管的是模型侧那部分调用凭证。你需要先拿到一个可用的 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。API 基础地址统一用 https://taotoken.net/api 注意这个地址后面不要加 UTM 参数保持干净。拿到 Key 之后建议不要硬编码在 Java 代码里而是放进环境变量或者配置中心。下面是一个可复制的application.yml片段把数据库和模型通道分开配置spring: datasource: url: jdbc:oracle:thin:127.0.0.1:1521/ORCLPDB1 username: scott password: your_oracle_password driver-class-name: oracle.jdbc.OracleDriver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-id: your-model-id这里TAOTOKEN_API_KEY从环境变量读取model-id填你在控制台看到的模型标识。如果你用的是 Claude Code 这类编码工具配置方式类似Base URL 填https://taotoken.net/apiKey 填你的 KeyModel ID 填对应模型。文档入口在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的接入示例。如果你需要长期跑编码 Agent 或者批量任务可以考虑 Coding Plan入口是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。模型对话调试可以用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 先验证通道是否通。注意一点TaoToken 是模型调用的统一通道不是数据库中间件也不是让你绕过任何合规要求。数据库连接该用 JDBC 还是 JDBC凭证该加密还是加密。把这两件事分清楚后面配置才不会乱。4. 可复制配置Oracle 建表建包 Spring XML/Java 完整代码这一节是重头戏全部给可复制的代码。先建表再建包然后给 Spring 的两种调用方式。4.1 Oracle 建表与存储过程 DDL先创建测试表CREATE TABLE TEST_USERS ( USER_ID VARCHAR2(10) NOT NULL, NAME VARCHAR2(10) NOT NULL, PASSWORD VARCHAR2(20) NOT NULL ); INSERT INTO TEST_USERS VALUES (test1, aa, aa); INSERT INTO TEST_USERS VALUES (test2, bb, bb); INSERT INTO TEST_USERS VALUES (test3, cc, cc); COMMIT;然后创建包和包体注意游标类型定义在包规范里CREATE OR REPLACE PACKAGE display_users_package IS TYPE search_results IS REF CURSOR; PROCEDURE display_users_proc( results_out OUT search_results, userId IN test_users.user_id%TYPE ); END display_users_package; / CREATE OR REPLACE PACKAGE BODY display_users_package IS PROCEDURE display_users_proc( results_out OUT search_results, userId IN test_users.user_id%TYPE ) IS BEGIN IF userId IS NOT NULL THEN OPEN results_out FOR SELECT * FROM test_users WHERE user_id LIKE userId || %; ELSE OPEN results_out FOR SELECT * FROM test_users; END IF; END display_users_proc; END display_users_package; /这里results_out是第一个参数类型search_results也就是REF CURSOR。Java 侧注册出参时索引就是 1。4.2 Spring XML 方式配置 DataSource 与 JdbcTemplate如果你还在用 XML 配置可以这样写bean iddataSource classcom.zaxxer.hikari.HikariDataSource destroy-methodclose property namejdbcUrl valuejdbc:oracle:thin:127.0.0.1:1521/ORCLPDB1/ property nameusername valuescott/ property namepassword valueyour_oracle_password/ property namedriverClassName valueoracle.jdbc.OracleDriver/ property namemaximumPoolSize value10/ /bean bean idjdbcTemplate classorg.springframework.jdbc.core.JdbcTemplate property namedataSource refdataSource/ /bean4.3 Java 方式JdbcTemplate CallableStatementCallback这是最通用的写法参数顺序完全可控import oracle.jdbc.OracleTypes; import org.springframework.jdbc.core.CallableStatementCallback; import org.springframework.jdbc.core.CallableStatementCreator; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class SpringStoredProce { private final JdbcTemplate jdbcTemplate; public SpringStoredProce(DataSource dataSource) { this.jdbcTemplate new JdbcTemplate(dataSource); } public ListMapString, Object execute(String storedProc, String userId) { return jdbcTemplate.execute( new ProcCallableStatementCreator(storedProc, userId), new ProcCallableStatementCallback() ); } private static class ProcCallableStatementCreator implements CallableStatementCreator { private final String storedProc; private final String userId; ProcCallableStatementCreator(String storedProc, String userId) { this.storedProc storedProc; this.userId userId; } Override public CallableStatement createCallableStatement(Connection conn) throws SQLException { String call {call storedProc (?, ?)}; CallableStatement cs conn.prepareCall(call); cs.registerOutParameter(1, OracleTypes.CURSOR); cs.setObject(2, userId); return cs; } } private static class ProcCallableStatementCallback implements CallableStatementCallbackListMapString, Object { Override public ListMapString, Object doInCallableStatement(CallableStatement cs) throws SQLException { ListMapString, Object results new ArrayList(); cs.execute(); try (ResultSet rs (ResultSet) cs.getObject(1)) { while (rs.next()) { MapString, Object row new HashMap(); row.put(userId, rs.getString(USER_ID)); row.put(name, rs.getString(NAME)); row.put(password, rs.getString(PASSWORD)); results.add(row); } } return results; } } }注意{call ...}这种转义写法Oracle JDBC 驱动支持比裸call更稳。cs.getObject(1)拿到的就是游标对应的ResultSet。4.4 Java 方式SimpleJdbcCall 更省事的写法如果你不想手写回调SimpleJdbcCall更简洁import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcCall; import oracle.jdbc.OracleTypes; import javax.sql.DataSource; import java.util.HashMap; import java.util.List; import java.util.Map; public class SimpleJdbcCallService { private final SimpleJdbcCall jdbcCall; public SimpleJdbcCallService(DataSource dataSource) { JdbcTemplate jdbcTemplate new JdbcTemplate(dataSource); this.jdbcCall new SimpleJdbcCall(jdbcTemplate) .withCatalogName(DISPLAY_USERS_PACKAGE) .withProcedureName(DISPLAY_USERS_PROC) .declareParameters( new org.springframework.jdbc.core.SqlOutParameter( results_out, OracleTypes.CURSOR), new org.springframework.jdbc.core.SqlParameter( userId, java.sql.Types.VARCHAR) ) .returningResultSet(results_out, (rs, rowNum) - { MapString, Object row new HashMap(); row.put(userId, rs.getString(USER_ID)); row.put(name, rs.getString(NAME)); row.put(password, rs.getString(PASSWORD)); return row; }); } public ListMapString, Object query(String userId) { MapString, Object params new HashMap(); params.put(userId, userId); MapString, Object result jdbcCall.execute(params); return (ListMapString, Object) result.get(results_out); } }returningResultSet的名字要和declareParameters里的出参名一致否则拿不到数据。这一点在SimpleJdbcCall里特别容易错。5. 验证请求与成功结果跑一遍看输出配置写完了得验证。先写一个 JUnit 测试import org.junit.jupiter.api.Test; import org.springframework.jdbc.datasource.DriverManagerDataSource; import javax.sql.DataSource; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class SpringStoredProceTest { Test public void testExecute() { DataSource ds new DriverManagerDataSource( jdbc:oracle:thin:127.0.0.1:1521/ORCLPDB1, scott, your_oracle_password ); SpringStoredProce service new SpringStoredProce(ds); ListMapString, Object result service.execute(display_users_package.display_users_proc, test); assertNotNull(result); assertTrue(result.size() 0); for (MapString, Object row : result) { System.out.println(USER_ID row.get(userId) \tname row.get(name) \tpassword row.get(password)); } } }预期输出USER_IDtest1 nameaa passwordaa USER_IDtest2 namebb passwordbb USER_IDtest3 namecc passwordcc如果传空字符串或者null过程会走ELSE分支返回全表数据。你可以把参数改成null再跑一次验证两条分支都通。验证模型通道是否可用可以用模型对话入口 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条测试消息确认 Key 和 Base URL 配置正确。这一步和数据库验证是独立的别混在一起排查。成功的关键标志有三个result.size()大于 0、字段名和表列名对得上、没有SQLException。如果这三个都满足说明整条链路通了。6. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来对照方便你快速定位。报错一ORA-01000: maximum open cursors exceeded游标没关。检查ResultSet是否在try-with-resources里关闭CallableStatement也要关。用SimpleJdbcCall的话框架会帮你关但裸JdbcTemplate回调里要自己保证。报错二invalid column type: 1111OracleTypes.CURSOR没注册对或者ojdbc版本和数据库不匹配。确认依赖里是com.oracle.database.jdbc:ojdbc8或对应版本别用老旧的ojdbc14。报错三401 Unauthorized这个通常出现在模型通道调用上不是数据库。检查 TaoToken 的 API Key 是否填对、是否过期、Base URL 是否是https://taotoken.net/api。Key 管理页 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以重新生成。报错四local proxy failed本地网络或代理配置问题。先确认能直连https://taotoken.net/api再检查环境变量里有没有残留的代理设置。这个报错和数据库无关别去改 JDBC 配置。报错五reading choices相关解析错误模型返回结构不符合预期通常是 Model ID 填错或者请求体格式不对。对照文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查请求参数。报错六OAuth相关失败如果你用的是 Claude Code 或类似工具OAuth 流程没走完。重新执行登录流程确认回调地址正确。Claude Code 的接入说明在文档里有专门章节。报错七Cannot get JDBC ConnectionDataSource 没注入成功或者连接池耗尽。检查application.yml里的 URL、用户名、密码以及hikari的maximum-pool-size是否太小。排查顺序建议先确认数据库连通性用 SQL 客户端直连跑一遍过程再确认 Java 侧参数注册最后确认结果集读取。模型通道的问题单独排查不要和数据库问题混在一起。7. 语义一致 CTA把凭证和调用通道收口整条链路跑通之后建议做一件事把模型调用的 Key 和 API 通道统一收口到 TaoToken别让它们散落在各个application.yml和测试代码里。数据库凭证走配置中心或环境变量模型凭证走 TaoToken 的 Key 管理两边职责清晰。需要新建或轮换 Key去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。想先验证模型通道是否通用模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条消息即可。如果是要长期跑编码 Agent 或批量任务Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后给一个实用技巧在 Spring 项目里把SimpleJdbcCall的实例缓存起来不要每次调用都 new 一个因为declareParameters和returningResultSet的元数据解析有开销。对于高频调用的存储过程缓存后性能提升很明显。数据库连接池的maximum-pool-size也别拍脑袋设按实际并发压测结果调一般 10 到 20 够中小系统用。
返回列表