多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java调用Oracle分页存储过程:TaoToken统一Key下的接口联调与分页验证

Java调用Oracle分页存储过程:TaoToken统一Key下的接口联调与分页验证 1. Java 调用 Oracle 分页存储过程为什么总在游标出参上翻车先说清楚这篇要解决的是什么问题Java 后端通过 JDBC 调用 Oracle 分页存储过程存储过程放在 package 里、用 ref cursor 做游标出参同时接口层用 TaoToken 统一 Key 做鉴权与联调。适合谁看正在写 Oracle 分页、被ORA-01000、ORA-06550、getCursor拿不到结果集折腾过的 Java 后端同学。Oracle 存储过程和 MySQL 不一样它没有return值所有返回值都靠 out 参数往外带。普通字段用NUMBER、VARCHAR2就行但分页返回的是一个结果集普通参数装不下必须用SYS_REFCURSOR或者自定义的ref cursor类型。而自定义类型又不能直接建得先建一个 package 把类型声明进去这就是为什么分页存储过程天然是「package procedure」两件套。我见过太多人卡在三个地方第一registerOutParameter里游标类型写错用了Types.OTHER或者Types.CURSOR而不是OracleTypes.CURSOR第二取结果集时用proc.getObject(6)强转失败其实应该用OracleCallableStatement.getCursor()第三分页 SQL 拼接时rownum的边界算错导致第一页和最后一页数据对不上。再叠加一层现实问题现在很多团队的后端接口不是直连数据库就完事中间还有一层统一网关做鉴权。TaoToken 在这里扮演的角色就是统一 Key 和 API 通道——你的 Java 服务在调用模型能力或者做接口联调时不用每个服务各存一套密钥而是走同一个 Key。这篇就把「Oracle 分页存储过程」和「TaoToken 统一 Key 联调」这两条线拧在一起讲从建包、写过程、JDBC 绑定参数到用 SQL 日志和返回行数验证分页正确性一步步给可复制的代码。先明确一个边界TaoToken 不是数据库代理它管的是接口鉴权和 API 通道Oracle 那部分还是你自己的 JDBC 直连。两者是并行的两条链路别混在一起理解。2. TaoToken 统一 Key 与 Oracle 分页存储过程的前置准备在动手写 Java 之前得把两边的环境都备齐。Oracle 这边要确认你有建 package 的权限Java 这边要确认ojdbc8.jar或更高版本已经在依赖里TaoToken 这边要拿到一个可用的 Key。2.1 Oracle 侧建 package 和分页存储过程Oracle 的 ref cursor 类型不能裸建必须先声明在 package 里。先建包头create or replace package testpackage as type test_cursor is ref cursor; end testpackage;包头只声明类型不写实现。接着建存储过程入参是表名、当前页、每页条数出参是总行数、总页数、游标create or replace procedure my_page( v_in_tableName in varchar2, v_in_pageNow in number, v_in_pagesize in number, myrows out number, myPageCount out number, p_cursor out testpackage.test_cursor ) is v_sql varchar2(500); v_begin number : (v_in_pageNow - 1) * v_in_pagesize 1; v_end number : v_in_pageNow * v_in_pagesize; begin v_sql : select t2.* from (select t1.*, rownum rn from (select * from || v_in_tableName || ) t1 where rownum || v_end || ) t2 where rn || v_begin; open p_cursor for v_sql; select count(*) into myrows from employee; if mod(myrows, v_in_pagesize) 0 then myPageCount : myrows / v_in_pagesize; else myPageCount : myrows / v_in_pagesize 1; end if; end;这里有个坑要提前说原示例里select count(*) into myrows from employee是写死表名的实际用的时候应该把表名也拼进 count 语句否则你传别的表进来总行数永远是 employee 的。改成动态 SQL 更稳妥execute immediate select count(*) from || v_in_tableName into myrows;注意动态 SQL 拼表名有注入风险生产环境一定要对表名做白名单校验别直接把前端传的值拼进去。2.2 TaoToken 侧拿统一 KeyTaoToken 的作用是给你的接口联调提供统一鉴权。你不需要在每个 Java 服务里散落不同的密钥而是申请一个 Key所有走 API 通道的请求都带它。获取入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentoracle_page_proc拿到 Key 之后接口联调时把它放进请求头。具体字段名以接入文档为准文档地址https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentoracle_page_proc如果你后面要做的是长期编码或 Agent 类任务可以考虑 Coding Plan它更适合持续性的调用场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentoracle_page_procAPI 的基础地址是https://taotoken.net/api这个不带 UTM配置里直接写就行。2.3 Java 侧依赖与 DataSourceMaven 里加 Oracle JDBC 驱动dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version21.9.0.0/version /dependencyDataSource 建议用连接池别每次DriverManager.getConnection。下面是一个最小可用的 HikariCP 配置HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:oracle:thin:10.10.1.29:1521:testdb); config.setUsername(neb); config.setPassword(testneb); config.setDriverClassName(oracle.jdbc.OracleDriver); config.setMaximumPoolSize(10); config.setConnectionTimeout(3000); DataSource dataSource new HikariDataSource(config);连接池的好处是分页查询这种高频短连接场景下不会每次都握手。注意setConnectionTimeout别设太大联调阶段设 3 秒能快速暴露网络问题。3. 可复制的 CallableStatement 配置与分页参数绑定这一节是核心直接给能跑的代码。分页存储过程的调用分四步准备 CallableStatement、绑定入参、注册出参、执行并取游标。3.1 完整的调用方法import oracle.jdbc.OracleCallableStatement; import oracle.jdbc.OracleTypes; import java.sql.*; public class PageQueryService { private final DataSource dataSource; public PageQueryService(DataSource dataSource) { this.dataSource dataSource; } public PageResult query(String tableName, int pageNow, int pageSize) throws SQLException { String call {call my_page(?,?,?,?,?,?)}; try (Connection conn dataSource.getConnection(); CallableStatement proc conn.prepareCall(call)) { // 入参 proc.setString(1, tableName); proc.setInt(2, pageNow); proc.setInt(3, pageSize); // 出参总行数、总页数、游标 proc.registerOutParameter(4, OracleTypes.NUMBER); proc.registerOutParameter(5, OracleTypes.NUMBER); proc.registerOutParameter(6, OracleTypes.CURSOR); proc.execute(); int totalRows proc.getInt(4); int totalPages proc.getInt(5); ResultSet rs ((OracleCallableStatement) proc).getCursor(6); ListMapString, Object rows new ArrayList(); ResultSetMetaData meta rs.getMetaData(); int colCount meta.getColumnCount(); while (rs.next()) { MapString, Object row new LinkedHashMap(); for (int i 1; i colCount; i) { row.put(meta.getColumnName(i), rs.getObject(i)); } rows.add(row); } rs.close(); return new PageResult(totalRows, totalPages, rows); } } }关键点registerOutParameter(6, OracleTypes.CURSOR)这里必须用OracleTypes.CURSOR不是java.sql.Types.CURSOR。取结果集必须强转成OracleCallableStatement再调getCursor(6)直接用proc.getObject(6)在某些驱动版本上会抛类型转换异常。3.2 用 TOML 管理联调配置联调阶段参数经常改建议把数据库和 TaoToken 的配置抽到 TOML 里别硬编码[datasource] url jdbc:oracle:thin:10.10.1.29:1521:testdb username neb password testneb pool_size 10 [taotoken] base_url https://taotoken.net/api api_key sk-你的Key timeout_ms 5000Java 侧用tomlj或jackson-dataformat-toml读进来即可。这样切换环境时只改配置文件不动代码。3.3 分页参数绑定的边界处理pageNow从 1 开始pageSize建议设上限比如最大 200防止有人传 100000 把库拖垮。在方法入口加校验if (pageNow 1) pageNow 1; if (pageSize 1 || pageSize 200) pageSize 20;存储过程里v_begin (pageNow - 1) * pageSize 1v_end pageNow * pageSize这个公式对第一页是1到pageSize对第二页是pageSize1到2*pageSize边界是对的。但要注意rownum是 Oracle 的伪列where rownum v_end先截断外层再rn v_begin过滤这个嵌套顺序不能反反了就拿不到正确分页。4. 验证请求与分页正确性SQL 日志加返回行数双保险代码写完不代表分页就是对的。验证分页正确性有两个手段看 SQL 日志确认拼接的语句对不对看返回行数确认每页数据量符合预期。4.1 打开 SQL 日志联调阶段把执行的 SQL 打出来最直接。在存储过程里加dbms_output.put_line(v_sql)Java 侧开启输出try (Connection conn dataSource.getConnection()) { try (Statement st conn.createStatement()) { st.execute(begin dbms_output.enable(null); end;); } // ... 调用存储过程 ... try (CallableStatement cs conn.prepareCall( begin dbms_output.get_line(?, ?); end;)) { cs.registerOutParameter(1, Types.VARCHAR); cs.registerOutParameter(2, Types.NUMERIC); while (true) { cs.execute(); String line cs.getString(1); if (line null) break; System.out.println([SQL] line); } } }这样你能看到实际拼出来的 SQL 长什么样。比如pageNow2, pageSize10时应该输出select t2.* from (select t1.*, rownum rn from (select * from employee) t1 where rownum 20) t2 where rn 11如果v_end或v_begin算错了日志里一眼就能看出来。4.2 用返回行数验证写一个验证方法遍历所有页检查每页行数public void verifyPaging(String table, int pageSize) throws SQLException { int pageNow 1; int totalRows -1; while (true) { PageResult pr query(table, pageNow, pageSize); if (totalRows -1) { totalRows pr.getTotalRows(); System.out.println(总行数 totalRows , 总页数 pr.getTotalPages()); } int expected Math.min(pageSize, totalRows - (pageNow - 1) * pageSize); if (expected 0) expected 0; System.out.println(第 pageNow 页 实际 pr.getRows().size() 预期 expected); if (pr.getRows().size() ! expected) { System.out.println(!!! 分页不一致); } if (pageNow pr.getTotalPages()) break; pageNow; } }跑一遍如果每页的「实际」和「预期」都对得上分页逻辑就是正确的。最后一页通常不满expected用Math.min算出来正好是剩余行数。4.3 接口层联调带 TaoToken Key如果你的分页接口是对外暴露的 REST 接口联调时用 curl 带上 TaoToken 的 Keycurl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}这一步是验证你的 Key 和通道是通的。模型对话入口在这里可以先用它确认 Key 有效https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentoracle_page_proc确认 Key 通了之后再回到你的 Java 分页接口做端到端联调。两条链路分开验证出问题时好定位是数据库的事还是鉴权的事。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth联调过程中最容易撞上的几类报错这里逐个对照。5.1 401 Unauthorized这是 TaoToken 鉴权失败。先检查请求头里的 Key 是不是完整有没有多空格或者少字符。再确认 Key 有没有过期或者被禁用。如果 Key 是从环境变量读的打印一下长度确认没被截断。401 基本就是 Key 本身的问题跟 Oracle 无关。5.2 local proxy failed这个报错通常出现在你本地配了转发但目标地址写错的时候。检查你的base_url是不是https://taotoken.net/api别多写或少写路径。如果你在 Java 里用了 HttpClient 或者 OkHttp确认没有额外的代理配置覆盖了目标地址。这个错跟网络环境有关排查时先确认能直接访问https://taotoken.net/api。5.3 reading choices 相关报错如果你在解析模型返回的 JSON 时报reading choices之类的错说明返回体结构跟你预期的不一样。大概率是请求没成功返回的是错误对象而不是正常的choices数组。先把原始响应体打印出来看别直接.choices[0]。正常返回长这样{ choices: [ {message: {role: assistant, content: ...}} ] }如果返回的是{error: {...}}那就是请求本身失败了先解决请求问题。5.4 OAuth 相关报错如果你用的是 Claude Code 或者 Codex 这类工具可能会碰到 OAuth 认证的问题。这类工具通常需要配置三件套Base URL、Key、Model ID。以 Codex 的auth.json为例{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: gpt-4o-mini }三个字段缺一不可。Base URL 写https://taotoken.net/apiKey 用你申请的那个Model ID 按你实际要用的模型填。如果只填了 Key 没填 Base URL工具会走默认地址可能就 401 了。5.5 Oracle 侧的 ORA 报错ORA-06550通常是存储过程调用签名不匹配检查{call my_page(?,?,?,?,?,?)}里的问号数量是不是和过程参数个数一致。ORA-01000是游标打开太多说明你的 ResultSet 没关确保rs.close()在 finally 或者 try-with-resources 里执行。ORA-00942是表或视图不存在检查表名拼写和当前用户的权限。6. 把分页存储过程接入统一 Key 通道的落地建议走到这一步你的 Java 分页存储过程应该已经能跑通了。最后说几个落地时的实用建议。第一存储过程里的动态 SQL 一定要做表名白名单。别觉得内部接口就安全表名拼接是注入的高发点。维护一个允许分页的表名集合传进来的值不在集合里直接拒绝。第二分页查询的 count 语句和列表查询最好在同一个事务快照里否则高并发下可能出现总数和列表对不上的情况。Oracle 的select count(*)和游标查询如果不在同一一致性读里中间有数据变更就会不一致。第三TaoToken 的 Key 别硬编码在代码里用环境变量或者配置中心。联调阶段放 TOML 里方便改上线前换成环境变量注入。Key 泄露的后果比数据库密码泄露还麻烦因为它可能关联多个服务。第四接口联调时把 SQL 日志和 TaoToken 的请求日志分开打用不同的 logger 名字。出问题时能快速判断是数据库链路的问题还是鉴权链路的问题不用在一堆日志里翻。第五如果你后面要做的是持续性的编码任务或者 Agent 调度Coding Plan 比按次调用更划算配置方式在文档里有说明。分页存储过程这种偏底层的活配合统一的 API 通道做联调能把「数据库正确性」和「接口鉴权」两件事解耦开验证出问题的时候定位范围小很多。最后留一个我踩过的坑Oracle 的rownum在order by之前生效如果你的分页需要按某个字段排序排序必须放在最内层子查询里否则分页结果会乱。这个坑不报错但数据是错的只能靠返回行数验证发现。
返回列表