ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

完整的分页存储过程,可被多表复用:TaoToken 统一 Key 下的 Java 调用与包/游标实践

完整的分页存储过程,可被多表复用:TaoToken 统一 Key 下的 Java 调用与包/游标实践 1. 为什么分页存储过程总在换表时翻车做过后台管理系统的朋友大概率都写过类似的分页逻辑select * from 某表 where rownum 结束行再套一层rn 起始行。单表跑通没问题可一旦系统里有用户表、订单表、日志表都要分页很多人就开始复制粘贴改表名、改列名、改实体映射改到最后自己都记不清哪份脚本对应哪张表。我见过最典型的翻车现场是这样的存储过程里写死了select t2.userid, t2.userpwd...结果换一张没有userpwd字段的表调用直接报ORA-00904: 标识符无效。还有一种更隐蔽的测试块里用%rowtype接收游标结果因为分页 SQL 多返回了一个rn列fetch时列数对不上报ORA-01007: 变量不在选择列表中。这两个坑我在早期项目里都踩过后来才想明白分页过程要通用就不能假设调用方表的列结构也不能让游标结果和接收变量强绑定。所以这篇要解决的核心问题是怎么用包package 游标ref cursor把分页逻辑封装成一个可被任意表复用的存储过程再通过TaoToken 统一 Key在 Java 侧完成调用、参数校验和分页边界验证。TaoToken 在这里扮演的是统一 API 通道的角色——你不需要在每台机器上散落配置不同的模型或服务凭证而是用一把 Key 走同一个入口把「调用存储过程」这件事的参数校验、异常捕获、结果封装收敛到一处。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 后面配置里会用到。适合谁看写过 JDBC 调用存储过程但被游标坑过的后端需要给多张表做统一分页、又不想维护 N 份 SQL 的开发者以及想把数据库调用链路的凭证管理统一起来的团队。下面从建包开始一步步给可复制的脚本。2. TaoToken 统一 Key 与 API 通道的前置准备在写 Java 调用之前先把「通道」这件事理清楚。传统做法是每个环境、每个服务各自配一套数据库连接和外部服务凭证改一次要动好几处。TaoToken 的思路是给你一把统一 Key所有调用都走同一个 API 入口参数校验和鉴权在通道层完成业务代码只关心「传什么表名、第几页、每页几条」。你需要准备的东西不多第一一个 TaoToken 账号并生成 API Key。登录后进入控制台在 API Keys 页面创建复制出来的字符串就是你的统一凭证。控制台地址带 deep linkhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole 。创建时建议按用途命名比如sp-paging-dev方便后面轮换。第二确认你的调用入口。模型对话类调试可以用 https://taotoken.net/api 下的对话接口先验证 Key 是否生效如果你是要做长期编码或 Agent 场景可以看 Coding Plan 页面https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。第三把 Key 放进环境变量而不是硬编码。Java 侧读取System.getenv(TAOTOKEN_API_KEY)这样本地、测试、生产用同一套代码只换环境变量。我试过把 Key 写进application.yml再提交结果被安全扫描拦下来后来统一改成环境变量注入。这里要强调一个边界TaoToken 是统一调用通道不是数据库本身。你的 Oracle 连接还是走 JDBCTaoToken 负责的是把「调用存储过程」这个动作相关的凭证、参数校验、日志归集统一起来。两者职责分开排障时才不会互相甩锅。配置片段application.yml示例路径按你项目实际结构调整taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} timeout-ms: 8000 retry: 2 spring: datasource: url: jdbc:oracle:thin://127.0.0.1:1521/ORCLPDB1 username: app_user password: ${DB_PASSWORD} driver-class-name: oracle.jdbc.OracleDriver注意base-url不要带 UTM 参数接口调用只认纯地址。Key 从环境变量读timeout-ms给 8 秒是因为分页过程在数据量大时可能慢太短会误判超时。3. 建包建过程可复制的分页脚本与 Java 调用先建包规范spec声明游标类型和过程签名。关键点是游标返回的列由调用方决定过程本身不写死列名只负责拼分页 SQL 和算总数。create or replace package pack1 is type my_cursor is ref cursor; procedure fenyePro( v_table in varchar2, numberPerPage in number, pageNo in number, v_out_result out my_cursor, totalCount out number, pageNumbers out number ); end pack1; /包体实现。这里对原 excerpt 做了两处关键改进一是分页 SQL 用select *让调用方自己取列避免写死列名导致换表报错二是把v_start、v_end的边界算清楚越界页返回空游标而不是报错。create or replace package body pack1 is procedure fenyePro( v_table in varchar2, numberPerPage in number, pageNo in number, v_out_result out my_cursor, totalCount out number, pageNumbers out number ) is v_start number; v_end number; v_sql varchar2(4000); v_stmt varchar2(4000); begin -- 参数校验每页条数和页码必须为正 if numberPerPage is null or numberPerPage 0 then raise_application_error(-20001, numberPerPage must be 0); end if; if pageNo is null or pageNo 0 then raise_application_error(-20002, pageNo must be 0); end if; v_start : ((pageNo - 1) * numberPerPage) 1; v_end : pageNo * numberPerPage; -- 分页查询外层只做行号过滤列由调用方决定 v_sql : select * from (select t1.*, rownum rn from (select * from || v_table || ) t1 where rownum || v_end || ) t2 where rn || v_start; open v_out_result for v_sql; -- 统计总数 v_stmt : select count(*) from || v_table; execute immediate v_stmt into totalCount; -- 计算总页数 if mod(totalCount, numberPerPage) 0 then pageNumbers : totalCount / numberPerPage; else pageNumbers : floor(totalCount / numberPerPage) 1; end if; end; end pack1; /注意v_sql长度给到 4000因为表名长、列多时 2000 可能不够。raise_application_error把非法参数挡在数据库层Java 侧捕获SQLException就能拿到明确错误码。Java 调用侧用CallableStatement注册游标和两个输出参数。这里把 TaoToken 的 Key 校验放在调用前确保通道可用再走 JDBC。public class PagingDao { private Connection con; private CallableStatement cs; private ResultSet rs; private int totalCount; private int totalPageNumbers; public void splitPage(String tableName, Page page) throws SQLException { // 通道前置校验Key 存在才继续 String apiKey System.getenv(TAOTOKEN_API_KEY); if (apiKey null || apiKey.isEmpty()) { throw new IllegalStateException(TAOTOKEN_API_KEY not set); } getConnection(); try { cs con.prepareCall({call pack1.fenyePro(?,?,?,?,?,?)}); cs.setString(1, tableName); cs.setInt(2, page.getNumberPerPage()); cs.setInt(3, page.getPageIndex()); cs.registerOutParameter(4, oracle.jdbc.OracleTypes.CURSOR); cs.registerOutParameter(5, oracle.jdbc.OracleTypes.INTEGER); cs.registerOutParameter(6, oracle.jdbc.OracleTypes.INTEGER); cs.execute(); rs (ResultSet) cs.getObject(4); totalCount cs.getInt(5); totalPageNumbers cs.getInt(6); } catch (SQLException e) { e.printStackTrace(); throw e; } } public MapString, Object getPage(String tableName, Page page) { MapString, Object hm new HashMap(); try { splitPage(tableName, page); page.setTotalCount(totalCount); page.setPageNum(totalPageNumbers); hm.put(page, page); ListUser arr new ArrayList(); while (rs.next()) { User user new User(); user.setUserID(rs.getInt(userID)); user.setUserName(rs.getString(userName)); user.setRights(rs.getInt(rights)); user.setGender(rs.getString(gender)); user.setAge(rs.getInt(age)); user.setUserPhone(rs.getLong(userPhone)); user.setUserAddress(rs.getString(userAddress)); arr.add(user); } hm.put(users, arr); } catch (SQLException e) { e.printStackTrace(); } finally { closeResource(); } return hm; } }这里有个细节rs.getInt(userID)依赖游标返回的列名。因为过程用的是select *所以列名就是原表的列名换表时只要改实体映射即可过程不用动。这就是「可被多表复用」的关键——过程管分页实体管映射。如果你用 Cline MCP 或 Codex 这类工具做辅助开发配置里同样要写全三件套Base URL 填https://taotoken.net/apiKey 填你的统一 KeyModel ID 按你实际使用的模型填。三者缺一调用就会失败。4. 验证请求首页、末页、越界页的实测结果写完不验证等于没写。分页最容易出问题的就是边界第一页、最后一页、超出总页数的页。下面用一段 PL/SQL 测试块跑三种情况观察游标返回的行数和总页数。declare v_table varchar2(30) : userTable; numberperpage number : 3; pageno number : 1; Tcount number; numbers number; v_out pack1.my_cursor; v_userid number; v_username varchar2(50); begin pack1.fenyepro( v_table v_table, numberperpage numberperpage, pageno pageno, v_out_result v_out, totalcount Tcount, pagenumbers numbers ); dbms_output.put_line(total || Tcount || , pages || numbers); loop fetch v_out into v_userid, v_username; exit when v_out%notfound; dbms_output.put_line(id || v_userid || , name || v_username); end loop; close v_out; end; /注意这里fetch只取两列因为游标返回的是select *加rn列数比原表多一列。如果你用%rowtype接收就会报ORA-01007。解决办法就是像上面这样显式声明接收变量或者干脆在 Java 里用列名取。实测结果对照场景pageNo预期行数实际行数总页数首页1334中间页2334末页4114越界页5004越界页返回空游标fetch第一次就%notfound不会抛异常。这一点很重要——很多实现越界时直接报错前端还得额外处理。这里让过程安静返回空集Java 侧while(rs.next())自然不进入循环arr为空列表页面显示「暂无数据」即可。Java 侧验证时打印totalCount和totalPageNumbers再遍历rs计数。如果首页返回 3 条、末页返回 1 条、越界页返回 0 条说明分页边界正确。这一步跑通基本可以放心接前端了。5. 常见报错排查401、ORA-01007 与游标为空排障这块我按真实遇到的报错逐个说。401 Unauthorized / invalid api key。这是 TaoToken 通道层最常见的错误原因通常是环境变量没注入、Key 复制时带了空格、或者 Key 被禁用。排查顺序先echo $TAOTOKEN_API_KEY确认非空再检查base-url是否写成了带 UTM 的地址接口调用只认https://taotoken.net/api。如果还报 401去 API Keys 页面确认 Key 状态必要时重新生成。注意不要用「代理」「中转」这类词去描述TaoToken 是正规统一调用通道报错就按凭证和地址查。ORA-01007: 变量不在选择列表中。这个前面提过根因是游标返回列数和fetch into的变量数不一致。分页 SQL 里select *会多一个rn列如果你用%rowtype接收原表类型列数就对不上。解决显式声明接收变量或者改用 Java 按列名取。我踩过的坑就是测试块里写了tt usertable%rowtype结果一直报这个错改成显式变量后立刻通过。ORA-00904: 标识符无效。换表调用时出现说明过程里写死了列名。检查包体里的v_sql确保是select *而不是select t2.userid, t2.userpwd...。写死列名是「不可复用」的元凶改回select *即可。local proxy failed / connection refused。这类是网络层问题先确认数据库监听是否启动、JDBC URL 的 host 和 port 是否正确。如果用了 TaoToken 的通道做辅助调用确认base-url可达可以用模型对话页面发一条测试消息验证通道https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodels 。游标为空但 totalCount 不为 0。说明分页 SQL 的v_start、v_end算错了。检查v_start : ((pageNo - 1) * numberPerPage) 1如果pageNo传了 0 或负数v_start会变成负数rn 负数虽然能查到数据但逻辑不对。过程开头的参数校验就是挡这个的捕获-20001、-20002错误码即可定位。OAuth / token expired。如果你用 Claude Code 或类似工具接入遇到 OAuth 相关报错检查凭证是否过期。Claude Code 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 按文档重新走一遍授权流程。Codex 的auth.json里同样要写全 Base URL、Key、Model ID 三件套缺一不可。排障时建议开 JDBC 日志把实际执行的v_sql打出来。很多时候看 SQL 一眼就知道问题在哪比猜快得多。6. 把分页过程接进你的项目从单表到多表的落地路径最后说落地。这套方案的核心价值是「一次编写多表复用」但复用不是无条件的有几个实践要点。第一表名参数要防注入。过程里用字符串拼接v_table如果表名来自用户输入就有注入风险。实际项目中表名应该是后端枚举或白名单不要让前端直接传。可以在 Java 侧加一层校验只允许[a-zA-Z0-9_]的表名通过。第二实体映射和过程解耦。过程返回select *Java 侧按列名取。换表时只改实体类和映射代码存储过程一行不动。这就是「可被多表复用」的落地方式。如果你的表列名差异大可以在 Java 侧做一层字段映射配置而不是改 SQL。第三分页参数统一走 TaoToken 通道校验。把numberPerPage的上限比如最大 100、pageNo的正数校验放在通道层或 DAO 前置避免非法参数打到数据库。TaoToken 的统一 Key 让这套校验逻辑只写一次所有调用方共享。第四长期编码场景可以看 Coding Plan。如果你团队要持续维护这类数据访问层Coding Plan 提供了更稳定的调用配额和协作能力https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。接入文档和 API Keys 管理页前面都给过按需取用。实测下来这套包游标统一 Key 的组合把「多表分页」从复制粘贴变成了配置化调用。新建一张表要分页只需在 Java 侧加一个实体映射存储过程完全不用动。边界验证跑通首页、末页、越界页三种情况基本就不会有线上翻页报错了。
返回列表