ResultSet游标详解:JDBC Statement 游标类型与 TaoToken 配置骨架
1. 为什么你的 ResultSet 一 scroll 就报错刚接触 JDBC 分页或双向遍历时很多人会写出这样的代码拿到ResultSet后直接调rs.last()或rs.previous()结果运行时报出SQLException: Operation not supported或者结果集只进不退。这不是数据库坏了而是你用的游标类型根本不支持这些操作。ResultSet游标cursor决定了结果集能不能前后滚动、能不能感知别人改了数据。JDBC 2.0 之后Statement创建时可以指定两个参数游标类型和并发模式。默认情况下createStatement()返回的是TYPE_FORWARD_ONLY只能一路next()向前想回头就得重新查一次。这个默认值效率最高但功能也最受限。这篇内容面向正在写 JDBC 查询、做分页组件、或者用 ORM 底层调优的开发者。我会把三种游标类型的行为差异讲清楚给出可直接复制的Statement配置片段再补上通过 TaoToken 统一 Key/API 通道时的settings.json骨架最后用一段验证代码确认你的游标到底生效了没有。读完你能自己判断这个场景该用哪种游标以及为什么。2. TaoToken 前置统一 Key 与 API 通道在写 JDBC 代码之前先把模型调用和编码辅助的通道理顺。TaoToken 提供统一的 Key 和 API 入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。它的作用是让你在多个模型、多个编码工具之间用同一套凭证不用每个工具单独配一遍。如果你只是想让 AI 帮你解释某段 JDBC 游标代码用模型对话就行https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel 。如果你在长期写 Java 后端、需要 Agent 帮你改代码那更适合 Coding Planhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。Key 的创建在控制台完成https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole 具体 Key 列表在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。注意TaoToken 是模型与 API 通道的统一入口不替代你的数据库驱动也不替代 JDBC 本身。JDBC 连接串、驱动 jar、数据库账号密码仍然由你自己管理。3. 三种游标类型与并发模式的选择逻辑3.1 TYPE_FORWARD_ONLY默认最快只能向前ResultSet.TYPE_FORWARD_ONLY是默认游标类型类似单向链表只支持next()不支持previous()、first()、last()、absolute()。它的效率最高因为数据库只需要维护一个向前的游标不需要在 JVM 里缓存记录。适合场景一次性顺序读取、流式处理大结果集、不需要回头的报表导出。如果你只是while(rs.next())遍历永远优先选它。3.2 TYPE_SCROLL_INSENSITIVE可滚动但对数据变更不敏感ResultSet.TYPE_SCROLL_INSENSITIVE支持previous()、first()、last()、absolute()等操作。它的实现方式是把查询 fetch 到的记录全部缓存到 JVM 的ResultSet对象里。比如查出 10 条记录直接跳到最后一条JDBC 会先把所有记录拉到 JVM 端缓存再在内存数组里做随机定位。对数据变更不敏感的意思是查询完成后其他 session 对数据库做的修改不会同步到这个缓存里。因为数据库把name,id字段缓存到 cache 后cache 和真正的数据文件已经没有联系了。适合场景结果集不大、需要双向遍历或随机定位、且能接受数据快照语义。大量记录集返回时要慎用因为全量缓存在 JVM 内存里。3.3 TYPE_SCROLL_SENSITIVE可滚动且对 update 可见ResultSet.TYPE_SCROLL_SENSITIVE同样支持滚动操作但它对数据变更敏感。实现上数据库会把rowid缓存到 cache 中用户代码 fetch 记录时再做二次查询select name,id from foo where rowid?。所以期间发生的 update 是实时可见的。但这种可见性仅限于 update。insert 和 delete 同样不可见因为 insert 生成的 rowid 不会反映到已缓存的 rowid 结果集上delete 通常是标记删除rowid 对应那行可能还能取到。适合场景需要滚动且对 update 实时性有要求的场景。效率最低、开销最大因为每次 fetch 都要二次查询。3.4 并发模式CONCUR_READ_ONLY 与 CONCUR_UPDATABLE第二个参数是并发模式。CONCUR_READ_ONLY表示结果集只读CONCUR_UPDATABLE表示可以通过rs.updateXxx()直接更新数据库。绝大多数查询用只读就够了可更新结果集对驱动和数据库支持要求更高且容易踩坑。游标类型可滚动数据变更可见性内存开销典型场景TYPE_FORWARD_ONLY否不适用最低顺序遍历、大结果集TYPE_SCROLL_INSENSITIVE是不敏感高缓存记录小结果集双向遍历TYPE_SCROLL_SENSITIVE是update 可见最高缓存 rowid 二次查询需滚动且关注 update4. 可复制的 JDBC 配置片段4.1 创建 Statement 时指定游标import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class CursorDemo { public static void main(String[] args) throws Exception { String url jdbc:mysql://127.0.0.1:3306/testdb?useUnicodetruecharacterEncodingutf8; String user root; String password your_password; try (Connection conn DriverManager.getConnection(url, user, password)) { // 可滚动、只读游标 Statement stmt conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY ); ResultSet rs stmt.executeQuery(select id, name from foo); // 向后遍历 while (rs.next()) { System.out.println(forward: rs.getInt(id) - rs.getString(name)); } // 回到最后一行 if (rs.last()) { System.out.println(last row id: rs.getInt(id)); } // 向前遍历 while (rs.previous()) { System.out.println(backward: rs.getInt(id)); } rs.close(); stmt.close(); } } }4.2 分页场景下的游标选择分页如果只是「查第 N 页」用LIMIT offset, size配合TYPE_FORWARD_ONLY就够了不需要可滚动游标。可滚动游标适合「已经拿到结果集要在里面来回跳」的场景比如本地缓存了一份小结果集做 UI 翻页。// 分页查询优先 FORWARD_ONLY LIMIT Statement stmt conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY ); stmt.setFetchSize(100); // 流式拉取避免一次性加载 ResultSet rs stmt.executeQuery(select id, name from foo limit 0, 20);4.3 TaoToken settings.json 骨架示例如果你用支持settings.json的编码工具把 TaoToken 的 Key 和 API 地址填进去骨架如下{ provider: taotoken, apiBase: https://taotoken.net/api, apiKey: sk-your-taotoken-key, models: { default: your-preferred-model, coding: your-coding-model }, features: { chat: true, codeCompletion: true, agent: false } }提示apiKey从控制台的 API Keys 页面获取不要把它提交到 Git 仓库。apiBase固定为 https://taotoken.net/api 不要加多余路径。5. 验证游标类型是否生效写完配置后别假设它一定生效。用下面这段代码打印实际游标类型和滚动能力import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class CursorVerify { public static void main(String[] args) throws Exception { String url jdbc:mysql://127.0.0.1:3306/testdb; try (Connection conn DriverManager.getConnection(url, root, your_password)) { Statement stmt conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY ); ResultSet rs stmt.executeQuery(select id from foo); System.out.println(actual type: rs.getType()); System.out.println(expected : ResultSet.TYPE_SCROLL_INSENSITIVE); if (rs.getType() ResultSet.TYPE_SCROLL_INSENSITIVE) { System.out.println(scroll supported: yes); } else { System.out.println(scroll supported: no, driver downgraded); } rs.close(); stmt.close(); } } }运行后如果actual type和你请求的不一致说明驱动或数据库不支持该游标被降级成了TYPE_FORWARD_ONLY。这时候调previous()就会抛异常。成功结果示例actual type: 1005 expected : 1005 scroll supported: yes1005就是TYPE_SCROLL_INSENSITIVE的常量值。如果打印出1003那就是TYPE_FORWARD_ONLY说明降级了。6. 本篇常见错排查6.1 报错 Operation not supported原因用了TYPE_FORWARD_ONLY却调了previous()或last()。解决创建Statement时显式指定TYPE_SCROLL_INSENSITIVE并确认驱动支持。6.2 游标被驱动降级有些数据库或驱动不支持TYPE_SCROLL_SENSITIVE会静默降级为TYPE_SCROLL_INSENSITIVE甚至TYPE_FORWARD_ONLY。用第 5 节的验证代码确认实际类型不要只看请求参数。6.3 大结果集 OOMTYPE_SCROLL_INSENSITIVE会把所有 fetch 的记录缓存到 JVM。如果结果集有几百万行内存直接爆。解决改用TYPE_FORWARD_ONLYsetFetchSize流式处理或者用数据库端分页。6.4 TYPE_SCROLL_SENSITIVE 看不到 insert这是预期行为。TYPE_SCROLL_SENSITIVE只对 update 可见insert 和 delete 不可见。如果你的业务依赖 insert 可见需要重新查询而不是依赖游标。6.5 并发模式选错导致更新失败CONCUR_UPDATABLE需要驱动和数据库同时支持且查询必须包含主键。如果rs.updateString()报错先检查是否用了只读模式或者查询是否缺少主键列。排障和接入相关的问题可以对照 API Keys 页面和接入文档https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 、https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。如果你在长期写 JDBC 封装或 Agent 辅助编码Coding Plan 更合适https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。我自己的习惯是默认TYPE_FORWARD_ONLY只有确认结果集小且需要来回跳时才换TYPE_SCROLL_INSENSITIVETYPE_SCROLL_SENSITIVE基本不用因为二次查询的开销在多数业务里不划算。每次换游标类型后跑一遍第 5 节的验证代码确认没有被驱动悄悄降级。