线上排查问题时,我见过太多次类似的场景:一段跑了大半年的定时任务突然开始报Can not issue data manipulation statements with executeQuery(),值班同学第一反应是数据库出问题了,翻日志翻监控折腾两小时,最后发现是有人把一条UPDATE语句塞进了executeQuery()里。也见过反过来,写了个跑批脚本用executeUpdate()去执行SELECT,结果明明有数据却一直拿不到结果集,以为连接池配置错了。这类问题的根因都指向同一件事——JDBC 的execute()、executeQuery()、executeUpdate()这三个方法,看着是入门第一课的东西,实际用起来踩坑率极高,越是写框架、写连接器、写压测脚本的人越容易在上面翻车。
这篇文章就把这三个方法彻底拆开讲。从接口设计意图、返回值语义、驱动层面的实现差异,一直讲到流式读取、批量操作、Flink JDBC 连接器、Jmeter JDBC Request、DataGrip 这些真实工具里的调用方式。适合已经写过 JDBC 但总觉得"大概知道、说不清楚"的开发者,也适合正在排查连接器异常、批处理返回值异常、驱动兼容性问题的同学。看完至少能明确一件事:这三个方法不是"随便挑一个能跑就行"的等价物,它们各自有明确的适用边界,选错的代价从报错到数据错乱都有可能。
1. 三个方法到底在解决什么问题
1.1 从 Statement 接口的方法签名说起
JDBC 规范里,Statement接口对外的执行入口其实就三个核心方法,签名非常干净:
ResultSet executeQuery(String sql) throws SQLException; int executeUpdate(String sql) throws SQLException; boolean execute(String sql) throws SQLException;这三个签名放在一起看,能读出设计者的意图:它们按"预期结果类型"把 SQL 分成了三类,然后为每一类给出一个语义最贴切的返回值类型。
- 我只期待一个结果集,那就给我
ResultSet,方法叫executeQuery; - 我只期待一个影响行数,那就给我
int,方法叫executeUpdate; - 我不确定会返回什么,可能两者都有,那就给我一个
boolean告诉我"第一个结果是结果集还是更新计数",剩下的我自己去取,方法叫execute。
这个设计的本质是把"调用方对自己要执行的语句的确定性"显式表达出来。调用方说"我确定这是查询",驱动就可以放心地走查询路径、准备结果集相关的元数据;调用方说"我确定这是更新",驱动就走更新路径、去拿 affected rows。这个"确定性"不是可选项,它会直接影响驱动内部的分支逻辑。
对比一下PreparedStatement就更清楚了:prepareStatement(sql)时驱动已经知道 SQL 长什么样,但它仍然保留了executeQuery、executeUpdate、execute三个方法,而不是合并成一个。原因是预编译只解决了"参数怎么传",没解决"结果长什么样"。SQL 的静态文本和运行时的结果形态是两件事。
注意 很多人以为用了
PreparedStatement之后三个方法就等价了,这是个常见误解。预编译和结果类型判断完全是两个维度的事情,选错方法照样报错。
1.2 返回值背后的取舍逻辑
先看executeQuery。它的返回值是ResultSet,语义非常直白:一定有一个结果集。但它的隐含语义同样重要——它不返回任何关于影响行数的信息。这就意味着如果你拿它去执行UPDATE,即使某些驱动"宽容"地让你跑通了,你也拿不到"改了几行"这个关键数据,做幂等校验、做数据比对时会直接失明。
再看executeUpdate。它返回int,这个int的含义在不同的语句类型下是不一样的:对INSERT、UPDATE、DELETE这类 DML,它是受影响的行数;对CREATE TABLE、DROP TABLE、ALTER TABLE这类 DDL,规范规定它返回0。所以"返回 0"不等于"没执行成功",这是个必须记住的点。
最后看execute。它返回boolean,很多人第一眼会理解成"成功返回 true、失败返回 false"。这是本篇最需要纠正的误解。这个boolean的真实语义是:true表示第一个结果是ResultSet,false表示第一个结果是更新计数(或者没有结果)。它和"语句执行是否成功"没有任何关系——执行失败会直接抛SQLException,根本轮不到返回值来表达。
boolean hasResultSet = stmt.execute(sql); if (hasResultSet) { ResultSet rs = stmt.getResultSet(); // 处理结果集 } else { int count = stmt.getUpdateCount(); // 处理影响行数 }上面这段是execute()的最简用法。但真正的坑在于:execute()可能产生多个结果,比如调用一个存储过程,它先返回一个结果集,再返回一个更新计数,又返回一个结果集。这时候必须用getMoreResults()循环遍历,后面的章节会展开。
1.3 为什么不是"一个方法走天下"
既然execute()什么都能干,为什么不干脆只留它一个?我总结了几个实打实的理由,都是踩过坑才明白的。
第一是类型安全。executeQuery返回ResultSet是编译期就确定的,你不用做任何类型判断和强制转换。用execute()的话,每个调用点都得写一遍if (hasResultSet) ... else ...,代码噪音大,而且一旦漏判就会拿到null或者意外的计数。
第二是驱动可以做针对性优化。不同数据库对"查询"和"更新"的处理路径差异很大。以 MySQL 为例,查询路径涉及结果集元数据的构建、可滚动/可更新的判定、fetch 策略的选择;更新路径则完全不碰这些,直接拿 affected rows。如果只有一个入口,驱动就得在运行时先猜后判,反而增加开销。这也是为什么某些驱动会明确拒绝"用executeQuery执行 DML"——它不是不能兼容,而是主动选择不兼容,逼你用对方法。
第三是流式读取的开关挂在特定路径上。后面讲大数据量查询时会说,setFetchSize的流式效果在某些驱动上只对查询路径生效。如果你用execute()去执行查询,然后忘了走getResultSet()分支,游标行为可能和预期完全不一样。
第四是框架层要靠方法名做语义映射。Jmeter 的 JDBC Request、Flink 的 JDBC 连接器、各种 ORM 的底层,都是通过"调用哪个方法"来判断这条 SQL 的意图的。你在上层看到的是一个个枚举值,底层就是这三个方法。
2. 核心差异拆解:返回值、适用语句与执行路径
2.1 executeQuery():只认单结果集
executeQuery的适用面其实比很多人以为的窄。它只应该用来执行返回单个结果集的语句,典型代表是SELECT。除此之外,一些数据库的SHOW、DESC、EXPLAIN也走这条路,因为它们返回的就是结果集形态的数据。
它明确不适合的场景有:
INSERT/UPDATE/DELETE:没有结果集,用它是错的;CREATE/DROP/ALTER:更不合适,DDL 没有任何结果集;- 带
RETURNING子句的 DML(PostgreSQL、部分国产库支持):这个情况比较微妙,语句本质上既是 DML 又返回结果集,需要用execute()或executeQuery()配合特定驱动支持,不能想当然。
用错时的表现因驱动而异。MySQL Connector/J 会直接抛:
java.sql.SQLException: Can not issue data manipulation statements with executeQuery().PostgreSQL 的 JDBC 驱动则通常抛A result was returned when none was expected.或者干脆让你拿到一个空的结果集。"抛异常"其实是最好的情况,因为它让你立刻就发现了错误;最怕的是某些驱动"默默接受",让你以为跑通了,实际上什么都没更新。
还有一个细节值得说:executeQuery返回的ResultSet在同一个Statement上再次执行时会被自动关闭。所以如果你需要把结果集传出去异步处理,要么先读完,要么换一个新的Statement。
2.2 executeUpdate():DML 和 DDL 的计数官
executeUpdate覆盖的场景比executeQuery宽,因为它同时接受 DML 和 DDL。
对 DML,返回的是受影响行数。这里有三个容易混淆的点:
第一,"受影响"到底是"匹配到"还是"真正改变"?以 MySQL 为例,Connector/J 默认返回的是匹配到的行数(found rows),而不是真正发生数据变化(changed rows)的行数。也就是说,一条UPDATE t SET a = 1 WHERE a = 1,即使所有行的值都没变,也可能返回匹配的行数。如果需要"真正改变"的语义,要在连接串里加useAffectedRows=true。这个差异在做数据同步、幂等判断时非常致命,很多人对不上账就是栽在这里。
第二,DDL 返回 0。规范就是这么定的,不要拿 0 去判断执行成功与否。执行失败会抛异常,返回 0 只是说明"这类语句没有行数概念"。
第三,某些语句天然返回 0。比如UPDATE的WHERE条件没有匹配到任何行,返回 0 是正确的;再比如某些驱动对含大对象列的更新无法准确计数,也会返回 0。
executeUpdate最大的坑是"不接受结果集"。MySQL 驱动会抛:
java.sql.SQLException: Can not issue SELECT via executeUpdate() or executeLargeUpdate().PostgreSQL 驱动抛A result was returned when none was expected.。这两个报错文案你大概率会在日志里见过。
2.3 execute():万能但有代价
execute的能力最全,代价也最大——它把"判断结果类型"的责任转嫁给了调用方。
它真正不可替代的场景有几个:
- 调用存储过程,而且不确定返回值形态(可能是结果集、可能是计数、可能都有);
- 一次提交多条语句,比如某些数据库允许用分号拼接多条 SQL,结果可能是混合的;
- 带
RETURNING的 DML,需要同时拿到结果集和更新计数; - DDL 与 DML 混合批处理,虽然这种用法不推荐。
用execute()的正确姿势是配合getResultSet()、getUpdateCount()、getMoreResults()三个方法做完整遍历:
boolean hasResultSet = stmt.execute(sql); while (true) { if (hasResultSet) { try (ResultSet rs = stmt.getResultSet()) { // 消费结果集 } } else { int updateCount = stmt.getUpdateCount(); if (updateCount == -1) { break; // 没有更多结果了 } // 记录或处理影响行数 } hasResultSet = stmt.getMoreResults(); }这段循环里有几个关键约定,不记住就会写出死循环或者漏读结果:
getUpdateCount()返回-1,表示当前结果不是更新计数,或者已经没有更多结果了;getMoreResults()通常会自动关闭当前的ResultSet,所以不要在外面继续持有它;- 循环的退出条件是"既没有结果集,更新计数又是 -1",缺一不可。
2.4 一张对照表理清边界
| 方法 | 返回类型 | 适用语句 | 返回值含义 | 典型误用 |
|---|---|---|---|---|
executeQuery | ResultSet | SELECT、SHOW、DESC | 查询结果集 | 拿去跑UPDATE,直接抛异常 |
executeUpdate | int | INSERT/UPDATE/DELETE/DDL | DML 为受影响行数,DDL 为 0 | 拿去跑SELECT,直接抛异常 |
execute | boolean | 存储过程、多结果、RETURNING | true表示首个结果是结果集 | 误以为是"成功标志",不做多结果遍历 |
executeBatch | int[] | 批量 DML | 每条语句的影响行数数组 | 忽略-2/-3特殊值 |
executeLargeUpdate | long | 超大行数 DML | 受影响行数(long) | 在支持 long 的场景仍用 int 造成溢出 |
这张表建议直接贴到自己的代码规范文档里。选方法的判断逻辑就一句话:先想清楚这条 SQL 会不会返回结果集,再想清楚我需要的是结果集还是行数,两个问题答完,方法自然就确定了。
3. 参数细节与执行路径:返回值到底从哪来
3.1 JDBC 规范对返回值的硬性约定
JDBC 规范对这部分的描述其实相当明确,但很多人没读过原文,只靠试。几条关键约定:
executeUpdate对 DML 返回受影响行数,对 DDL 返回 0;execute返回的boolean表示"第一个结果是否为ResultSet";getUpdateCount()在没有更多结果时返回-1;- 批量执行时,
Statement.SUCCESS_NO_INFO(值为-2)表示"执行成功但行数未知",Statement.EXECUTE_FAILED(值为-3)表示"该条执行失败"。
-2和-3这两个常量是批量操作排查的核心。很多连接器、同步工具在批量写入后报"行数对不上"或者"部分数据丢失",排查到最后就是这两个值被当成了正常行数处理。看到负数的行数,几乎可以确定是这两个常量之一。
3.2 影响行数在不同驱动里的计算路径
"受影响行数"这个数从哪来?答案是:由数据库服务端返回,驱动只做透传和归一化。但不同数据库、不同驱动对"受影响"的翻译口径不一样。
以 MySQL 为例,UPDATE语句执行后服务端返回的其实是三个数:匹配行数、更新行数、警告数。Connector/J 默认取的是匹配行数,除非显式配置useAffectedRows=true才取更新行数。这个差异在数据同步场景下会导致"目标端和源端计数不一致"这类很难定位的问题。
Oracle 的executeUpdate返回的是真实被修改的行数,语义相对稳定。PostgreSQL 也是类似。国产数据库和分布式数据库在这一块的行为差异更大,有的严格遵循规范,有的在负载均衡模式下会因为分片路由导致计数口径变化。在跨库迁移或者换驱动的时候,建议先写一个最小用例验证executeUpdate的返回值语义,再动生产代码。
3.3 execute() 的多结果遍历为什么必须写成循环
前面给的遍历模板,很多人第一次看会觉得多余——"直接用getResultSet不就行了"。问题在于execute()的结果数量是不可预知的。
举个具体例子,某些数据库的存储过程会先返回一个"操作摘要"结果集,再返回一个"明细"结果集。如果你只调一次getResultSet(),你拿到的是第一个摘要,明细就丢了。更隐蔽的是,有些过程的结果顺序在不同版本、不同参数下会变化,硬编码"只读第一个"的代码在升级后就会静默出错。
遍历循环还有一个细节:getMoreResults()有一个重载版本接受int参数,可以传Statement.CLOSE_CURRENT_RESULT、Statement.KEEP_CURRENT_RESULT、Statement.CLOSE_ALL_RESULTS。默认行为是关闭当前结果集。如果你确实需要同时持有多个结果集(比如做跨结果集比对),得显式传KEEP_CURRENT_RESULT,但要小心驱动和数据库是否真的支持多结果集同时打开——这一点在很多实现里是有限制的。
3.4 批量接口是另一条独立的路径
addBatch/executeBatch和上面三个方法不是一回事。executeBatch()返回int[],数组长度等于addBatch的次数,每个元素是那条语句的影响行数。
这里有几个容易忽略的点:
clearBatch()必须记得调,尤其在循环复用同一个Statement的场景,否则批次会越滚越大;- 批量里的语句类型必须一致,混着 DML 和查询语句会导致某些驱动直接拒绝;
- 批次太大或太小都影响性能,通常要结合数据库的
max_allowed_packet、网络往返开销一起调; - 返回数组里的
-2、-3一定要判,不能直接求和。
很多上层框架,比如 Flink 的 JDBC 连接器,内部就是靠executeBatch做批量写入的。你在配置里看到的batchSize参数,最终就映射到这一层。
4. 实操实录:从建表到批量写入的完整链路
4.1 环境准备与最小验证集
要验证三个方法的行为,其实不需要复杂的项目。一张最简单的表就够:
CREATE TABLE account ( id BIGINT PRIMARY KEY, name VARCHAR(64), balance DECIMAL(18, 2), updated_at TIMESTAMP );驱动层面,MySQL 用com.mysql.cj.jdbc.Driver,PostgreSQL 用org.postgresql.Driver,国产库和分布式数据库各有自己的驱动类名,加载方式完全一致,都是Class.forName或直接走 SPI 自动加载。换库时最容易出问题的地方恰恰是驱动匹配,版本不匹配时会报cannot load jdbc driver这类错误,或者在获取连接时抛No suitable driver found。遇到这类问题,先确认三件事:驱动 jar 是否在 classpath、驱动类名是否正确、连接串前缀是否和驱动匹配。
连接建立之后,建议第一件事就是打印conn.getMetaData()里的数据库名和驱动版本,把环境信息固化到日志里。这个习惯在排查"同一份代码在两套环境表现不同"的问题时能省下大量时间。
4.2 三种方法的实操对比与输出
下面这段代码把三个方法放在一起跑一遍,注意每一段的输出差异:
// 场景一:查询,用 executeQuery try (PreparedStatement ps = conn.prepareStatement( "SELECT id, name, balance FROM account WHERE balance > ?")) { ps.setBigDecimal(1, new BigDecimal("1000")); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getLong("id") + " / " + rs.getString("name")); } } } // 场景二:更新,用 executeUpdate try (PreparedStatement ps = conn.prepareStatement( "UPDATE account SET balance = balance - ? WHERE id = ?")) { ps.setBigDecimal(1, new BigDecimal("100")); ps.setLong(2, 1001L); int affected = ps.executeUpdate(); System.out.println("受影响行数 = " + affected); // 0 或 1 } // 场景三:不确定返回什么,用 execute try (PreparedStatement ps = conn.prepareStatement( "CALL some_procedure(?)")) { ps.setLong(1, 1001L); boolean hasResultSet = ps.execute(); int resultIndex = 0; while (true) { if (hasResultSet) { try (ResultSet rs = ps.getResultSet()) { System.out.println("第 " + (++resultIndex) + " 个结果集"); while (rs.next()) { // 消费 } } } else { int count = ps.getUpdateCount(); if (count == -1) break; System.out.println("更新计数 = " + count); } hasResultSet = ps.getMoreResults(); } }实测下来,场景二里affected返回 0 有两种可能:一是WHERE没匹配到行,二是这条UPDATE没改变任何值(在默认useAffectedRows=false的情况下其实还是匹配行数,具体要看驱动和配置)。判断的时候一定要区分"没匹配"和"匹配了但没变"。
场景三有几个实操细节:CALL语句在不同数据库上的行为差异很大,MySQL 想拿到过程内的结果集需要驱动支持,PostgreSQL 更常见的是用SELECT * FROM func(...)的形式调用函数。写跨库兼容代码时,能用函数就别用过程,因为过程的返回结构太不统一。
4.3 大数据量下的流式读取
这是三个方法里差异最容易被忽略、后果最严重的一块。默认情况下,很多驱动会把整个结果集一次性拉到客户端内存里,然后再交给你遍历。数据量小的时候没感觉,一旦单表几百万行、字段里还有大文本,OutOfMemoryError就来了。
要让executeQuery走真正的流式(游标)读取,不同数据库的设置方式不一样:
MySQL:需要stmt.setFetchSize(Integer.MIN_VALUE)配合正向、只读的结果集,或者连接串加useCursorFetch=true再设置一个正整数 fetchSize。注意Integer.MIN_VALUE这个"魔法值"是 Connector/J 的特有约定,看起来很不优雅,但确实是官方推荐的流式开关。
PostgreSQL:必须先关掉自动提交,conn.setAutoCommit(false),然后stmt.setFetchSize(1000)之类。如果 autocommit 是 true,fetchSize 会被忽略,这是 PostgreSQL 流式失效最常见的原因,没有之一。
Oracle:setFetchSize默认值是 10,通常调到 100 到 1000 之间比较合适,太小会增加网络往返,太大就失去流式的意义。
// PostgreSQL 流式读取的最小正确写法 conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM big_table")) { ps.setFetchSize(1000); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 逐行处理 } } } conn.commit();这套配置直接决定了数据同步任务、导出任务能不能跑完。我做数据迁移时踩过的最深的坑就是:小表测试全绿,一上大数据量就 OOM,排查半天发现是 autocommit 没关,fetchSize 形同虚设。
提示 流式读取期间,同一个连接上通常不能再执行其他语句,否则游标可能被中断。做流式导出时,建议单独开一个连接专用,别和业务查询共用。
4.4 上游框架是怎么调用这三个方法的
理解了底层,再看上层框架的配置项就会豁然开朗。
Flink JDBC 连接器:它的JdbcExecutionOptions里有batchSize、batchIntervalMs、maxRetries三个关键参数,底层走的是addBatch+executeBatch。遇到批量写入报错、重试行为不符合预期、或者写入行数和源端对不上时,大概率要往executeBatch的返回值处理上找原因。连接器抛出的"连接不可用""批处理失败"这类异常,根因往往在驱动版本和数据库的批量语义差异上,而不是连接池本身。
Jmeter 的 JDBC Request:这个组件把方法选择做成了显式枚举,Query Type 下拉里能看到Select Statement、Update Statement、Callable Statement、Prepared Select Statement、Prepared Update Statement,以及Commit、Rollback、AutoCommit(false)这些控制项。选Select Statement底层就是executeQuery,选Update Statement就是executeUpdate,选Callable Statement就是execute。做参数化压测时,如果 Query Type 选错,表现就是 Variables names 里拿不到值,或者一直报结果集相关的错。做"把 JDBC 查询结果作为下一个接口参数"这种串联时,务必用Select Statement,并且正确填写 Result variable name 和 Variable names,前者拿整个结果集对象,后者把列按顺序映射成变量。
DataGrip 这类数据库客户端:它不需要你选方法,因为它会解析 SQL 的首个关键字自动判断,SELECT走查询路径,UPDATE走更新路径。但这也意味着,如果你在客户端里手写了一条奇怪的语句被误判,表现就会和程序里不一致。用客户端验证行为时,记住它和你的代码走的不是同一条判断逻辑。
5. 常见问题与排查技巧实录
5.1 典型报错速查表
| 报错信息关键词 | 触发原因 | 处理思路 |
|---|---|---|
Can not issue data manipulation statements with executeQuery() | 用executeQuery执行了 DML | 换成executeUpdate或execute |
Can not issue SELECT via executeUpdate() | 用executeUpdate执行了查询 | 换成executeQuery |
A result was returned when none was expected | 同上,PostgreSQL 驱动文案 | 检查方法选择,别被文案误导 |
No suitable driver found | 驱动未加载或连接串前缀不匹配 | 检查 jar、类名、URL 前缀 |
cannot load jdbc driver | 驱动 jar 缺失或版本冲突 | 清理重复 jar,确认版本矩阵 |
getUpdateCount一直返回 -1 | 当前结果是结果集,或已无更多结果 | 用getMoreResults循环遍历 |
批量返回-2 | SUCCESS_NO_INFO,成功但行数未知 | 不要按行数求和 |
批量返回-3 | EXECUTE_FAILED,该条失败 | 定位具体批次索引排查 |
Operation not allowed after ResultSet closed | 结果集被后续执行关闭 | 读完再执行下一条,或换 Statement |
Could not execute JDBC batch update | 批量语句类型混杂或参数不匹配 | 拆分批次,统一语句类型 |
这张表里的前两条,我建议直接抄进团队的代码评审清单。它们出现频率极高,而且发现成本极低,一个静态检查就能拦住。
5.2 不同驱动与国产库的行为差异
换库、换驱动是这类问题的重灾区。同样的代码在 MySQL 上跑得好好的,换到某个分布式库上就可能出现:
executeUpdate在负载均衡模式下返回的计数口径变化,因为请求可能被路由到不同分片;- 某些驱动对
execute()的多结果支持不完整,getMoreResults()直接返回 false; - 驱动版本和新版本数据库不匹配,报驱动加载失败或协议不兼容;
setFetchSize被静默忽略,流式读取退化成全量加载。
应对策略很朴素但很有效:每次换库或升级驱动,先跑一套最小验证用例,覆盖"查询返回行数""更新返回计数""DDL 是否返回 0""批量返回值""流式读取是否生效"这五个点,把结果记录下来作为基线。以后再出问题,对比基线就能快速定位是环境变了还是代码变了。
5.3 我总结的六步排查法
遇到这三个方法相关的问题,按下面的顺序走,基本能覆盖九成以上场景:
- 先看报错文案里的方法名。
executeQuery、executeUpdate这些关键词会直接出现在异常信息里,这是最快的线索。 - 确认 SQL 类型。把出问题的 SQL 拿出来单独看首个关键字,是查询、是 DML、还是调用语句。
- 确认方法选择。对照前面的表格,看方法是否匹配 SQL 类型。
- 确认返回值语义。如果没报错但结果不对,重点看
executeUpdate返回的是匹配行数还是改变行数,execute是否遍历完了所有结果。 - 确认驱动和版本。换过驱动、升过库、换过连接串的,优先怀疑这一层。
- 最小用例复现。把问题剥离成十几行代码,一张测试表,能复现就说明找到了根因,不能复现就往连接池、事务、并发方向找。
这个顺序的价值在于:它把"最容易验证、成本最低"的检查放在最前面。我见过太多人跳过前四步,直接从"是不是数据库挂了"开始查,结果绕了一大圈回到原地。
5.4 几条踩坑换来的实操心得
第一,永远不要用executeQuery去执行 DML,哪怕某个驱动的某个版本"看起来能跑"。这种依赖未定义行为的代码,升级驱动那天就是它爆炸的日子。
第二,executeUpdate返回 0 时,先别急着判失败。分清是 DDL、是没匹配到行、还是驱动不支持计数。加日志的时候把 SQL 类型和返回计数一起打出来,比只打一个数字有用得多。
第三,用execute()就必须写完整的结果遍历循环,不要偷懒只取一次结果。存储过程的返回结构是会变的,硬编码假设迟早出问题。
第四,大数据量查询先确认流式是否真的生效。一个简单的验证方法:在遍历到第一行时打印内存占用,如果内存已经涨了一大截,说明是全量加载,fetchSize 没起作用。这一步花两分钟,能省下一次 OOM 事故。
第五,批量的返回值一定要处理负数。-2和-3不是行数,是状态码。写一个小的工具方法把它们翻译成可读状态,比在业务代码里到处判断要清爽得多。
第六,代码里加一层薄封装是值得的。不是为了炫技,而是把"根据 SQL 类型选方法"这个判断集中到一处,用一次静态检查替代无数次人工评审。封装层还能统一处理返回值语义、统一打日志、统一做流式配置,长期看收益远大于那几十行代码的成本。
我在实际项目里最后收敛出来的做法是:查询统一走queryForList,更新统一走updateAndReturnCount,只有确实需要多结果或调用过程时才暴露execute。这样三个方法的使用边界在代码里是清晰可见的,新人接手时也不容易选错。踩过几次坑之后你会发现,这三个方法本身没什么难的,难的是把它们当成"必须明确选择"的工具,而不是"随便挑一个能跑就行"的替代品。