ARTICLE DETAIL

资讯详情

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

JDBC三大执行方法区别:execute系列选型指南

JDBC三大执行方法区别:execute系列选型指南 1. 三个方法到底差在哪从一次跑批报错说起上周帮同事排查一个跑批任务日志里就一行java.sql.SQLException: Can not issue data manipulation statements with executeQuery()任务卡在凌晨三点白天的对账报表直接空了。翻代码一看写的人在通用的执行工具类里图省事所有 SQL 一律走executeQuery()平时跑 SELECT 相安无事那天上线了一个清理历史数据的分支SQL 变成了DELETE FROM ... WHERE ...当场翻车。这个坑几乎每个写 JDBC 的人都踩过一次或者将要踩一次而它的根源就是对execute()、executeQuery()、executeUpdate()这三个方法的边界认识模糊。先把结论摆桌面上这三个方法都属于java.sql.Statement接口PreparedStatement和CallableStatement都继承自它所以你在哪一层调用都见得到它们。区别不在于谁快谁慢而在于契约——每个方法对能执行什么 SQL和返回什么结果都有明确规定越界就抛异常或者拿到一个你压根没预期的东西。execute()万能入口什么 SQL 都能塞返回值是boolean告诉你第一个结果是不是结果集。executeQuery()只认会返回结果集的语句通常是 SELECT返回一个绝不为 null的ResultSet。executeUpdate()面向 DML 和 DDL返回int表示受影响行数或者 DDL 情况下的 0。这三个方法适合的人其实很广刚学 JDBC 在写第一个 CRUD 工具类的新手、维护老系统存留Statement代码的中间件同学、写 Flink JDBC 连接器或数据同步组件的工程师、甚至只是想在 DataGrip 或 JMeter 里手动跑条 SQL 的测试同学。区别看着简单但真到动态 SQL 不知道是查还是改一次提交多条语句流式读大表这些场景选错一个方法就是几小时的排查成本。接下来我不打算照着 Javadoc 念一遍而是按为什么这么设计 → 底层怎么走 → 怎么选 → 动手写 → 出问题怎么查这条线把这三个方法的来龙去脉和实操细节讲透包括几个只在生产环境才会暴露的坑。2. 方法签名背后的设计意图2.1 execute() 返回 boolean 到底在表达什么boolean execute(String sql) throws SQLException这个签名第一次看很容易懵SQL 执行完你给我一个 true/false 是什么意思答案是——它在回答一个问题这条语句产生的第一个结果是一个ResultSet吗返回true说明第一个结果是结果集你需要接着调getResultSet()把它取出来返回false说明第一个结果是更新计数或者压根没有结果你要调getUpdateCount()拿行数。这个设计是为了兼容一条语句可能产生多个结果的情况。JDBC 规范允许驱动支持一次执行多条语句比如 MySQL 连接串打开allowMultiQueriestrue后写SELECT ...; UPDATE ...也允许存储过程返回多个结果集。这种时候execute()配合下面这组方法才能完整遍历boolean hasResultSet stmt.execute(sql); while (true) { if (hasResultSet) { try (ResultSet rs stmt.getResultSet()) { // 处理结果集 } } else { int count stmt.getUpdateCount(); if (count -1) { break; // 没有更多结果了 } // 处理更新计数 } hasResultSet stmt.getMoreResults(); }这里有个反直觉的细节getUpdateCount()在当前结果不是更新计数或者已经没有结果时返回-1而一条 DDL比如CREATE TABLE执行成功也可能返回0。0 和 -1 完全是两码事0 是确实执行了影响 0 行-1 是这里没有更新计数给你。我见过有人用if (count 0)判断执行成功结果 DDL 全部被判定为失败这种 bug 特别隐蔽。那为什么不让execute()直接返回ResultSet呢因为如果返回ResultSet遇到UPDATE语句就得返回null调用方必须做空判断而且多结果场景下也表达不了先结果集后行数这种顺序。用布尔值 后续 getter 的组合虽然啰嗦但语义上是完备的。2.2 executeQuery() 的契约只认返回结果集的语句ResultSet executeQuery(String sql) throws SQLException签名干净利落文档里明确写着返回的 ResultSet 永远不会是 null。这句话很重要。很多新手习惯写ResultSet rs stmt.executeQuery(sql); if (rs ! null) { ... }这个空判断纯属多余。executeQuery()如果没有结果返回的是一个打开但没有任何行的结果集rs.next()直接返回 false而不是给你一个 null。真正需要判断的是next()的返回值。契约的另一半是你只能传会返回单个 ResultSet 的 SQL。传INSERT、UPDATE、DELETE、CREATE会怎样规范说驱动应该抛SQLException。实际表现上主流驱动都遵从这个约定只是报错信息各不相同数据库驱动报错信息大意触发场景MySQLCan not issue data manipulation statements with executeQuery()传 UPDATE/DELETEPostgreSQLA result was expected but none was returned传 DML 或 DDLOracle通常抛出 ORA-00900 或驱动层异常传非查询语句SQL ServerThe statement did not return a result set传 DML 或 DDL注意 MySQL 的报错文案里只提了 data manipulation但实际CREATE TABLE也一样会报。有些老版本驱动对某些语句存在宽容行为比如把 DDL 当成无结果查询放过去这种不一致性恰恰说明不能依赖驱动的默认行为代码里该区分就得区分。还有一个容易忽略的点executeQuery()返回的 ResultSet它的生命周期和 Statement 绑定。关掉 StatementResultSet 就失效同一 Statement 再次执行前一个 ResultSet 也会被隐式关闭。所以别写出用一个 Statement 连续查两次把两个 ResultSet 都留着慢慢读这种代码第二个查询一执行第一个结果集就废了。2.3 executeUpdate() 的返回值到底在数什么int executeUpdate(String sql) throws SQLException返回值语义分两类对INSERT、UPDATE、DELETE这类 DML返回受影响的行数。对CREATE TABLE、DROP TABLE这类不返回任何内容的 DDL返回0。受影响行数这个词是有讲究的。以 MySQL 为例默认情况下UPDATE返回的是实际被改变的行数如果新值和旧值完全相同这行不算被改变。但如果你在连接串里加了useAffectedRowstrue或者某些驱动的对应参数返回的就变成匹配到的行数。同一个 UPDATE 语句两种配置下返回值可能从 3 变成 10这在做数据校验或者幂等判断时会造成误判。所以如果你的逻辑依赖这个数字务必先确认驱动的行为配置。还有一个坑是UPDATE ... WHERE id ?影响 0 行。这时候返回 0但语句本身是成功执行的只是没有任何行匹配。很多人把它当成失败抛出自定义异常导致业务误报。判断执行是否成功要看有没有抛SQLException而不是看返回值大小。至于超过Integer.MAX_VALUE的场景——比如一次DELETE干掉二十多亿行理论上存在实践中很少JDBC 4.2 补了executeLargeUpdate()返回long。如果你的系统有超大表清理任务用这个方法更稳妥。3. JDBC 驱动内部是怎么分发这三种调用的3.1 三种调用最终走的是同一条路很多人以为execute()是个更重量级的方法性能比另外两个差。实际上在绝大多数驱动实现里这三个方法的差别只在参数校验和结果包装上真正的网络往返和协议交互是同一套。拿 MySQL Connector/J 举例ClientPreparedStatement.executeQuery()内部会做几件事校验 SQL 类型、设置结果集类型、然后走executeInternal()。而execute()也是走executeInternal()区别只是返回的时候判断第一个结果是什么类型。所以用 executeQuery 比 execute 快这种说法是没有依据的微基准测试里那点差异基本落在噪声范围内。真正影响性能的是另外三件事是否使用了 PreparedStatement预编译 参数绑定避免 SQL 注入还能复用执行计划fetchSize 的设置决定是一次性把结果拉回内存还是分批取是否开启了批量模式addBatchexecuteBatch把多条语句合并成一次往返我以前做过一个对比实验同一个 10 万行的查询用execute()和executeQuery()分别跑一百次平均耗时差异在 2% 以内完全被 JIT 和网络抖动淹没。但把fetchSize从默认值改成流式读取内存占用从 1.2G 降到 60M 左右差距是数量级的。所以选型的时候别在这三个方法之间纠结性能把精力放在上面三件事上。3.2 PreparedStatement 的方法继承关系PreparedStatement继承Statement所以它同样有execute()、executeQuery()、executeUpdate()三个方法而且都不带 SQL 参数——因为 SQL 在构造PreparedStatement的时候已经传进去了String sql UPDATE account SET balance ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, new BigDecimal(100.00)); ps.setLong(2, 1001L); int rows ps.executeUpdate(); // 注意这里不传 SQL }CallableStatement又继承PreparedStatement用于调用存储过程。它多了一个execute()的变体语义返回 true 表示结果是 ResultSetfalse 表示结果是更新计数或输出参数。调用存储过程时因为输出参数要用registerOutParameter()先注册所以通常直接用execute()或executeQuery()配合getXXX()方法读输出参数。这里有个实际工作中很常见的困惑为什么PreparedStatement的方法没有 SQL 参数但很多工具类还是写成void execute(String sql)的形式那是因为工具类内部把 SQL 传给了Statement而不是PreparedStatement。如果你在做通用 DAO 封装一定要把两种角色分清楚Statement是语句由参数传入PreparedStatement是语句在创建时确定。3.3 一次 execute 调用在数据库侧发生了什么从应用层往下看一次executeQuery()的完整链路大致是应用层把 SQL或预编译语句 ID 参数交给驱动。驱动通过 Socket 或者数据库专有协议把请求发出去同时带上结果集类型、并发模式、fetchSize 等元信息。数据库解析、优化、执行把结果集的首批数据回传。驱动把回传的数据包装成ResultSet对象返回给应用。executeUpdate()的链路少了第 2 步里的结果集元信息数据库执行完直接回一个受影响行数。execute()则要额外告诉驱动我不确定会返回什么你按通用情况处理驱动返回的时候会带一个结果类型标记。这个差别在某些数据库的协议实现里会有一点开销比如 PostgreSQL 在 simple query 和 extended query 两种协议模式下的处理不同。但这点开销相对于一次网络往返来说微不足道。反倒是如果你的 SQL 文本很长、参数很多用PreparedStatement省下来的解析时间远比方法选择重要。4. 选型决策什么场景该用哪个方法4.1 常规 CRUD 的推荐写法日常业务代码里绝大多数情况应该这样选场景推荐方法返回处理单表查询、联表查询executeQuery()遍历 ResultSet单条 INSERTexecuteUpdate()校验返回值或忽略带条件的 UPDATE/DELETEexecuteUpdate()判断是否为 0 行并决定是否报错建表、建索引等 DDLexecuteUpdate()通常忽略返回值动态拼接、类型未知execute()用 getResultSet/getUpdateCount 分支存储过程调用execute()或executeQuery()结合输出参数读取批量写入addBatch()executeBatch()取 int 数组逐个检查这张表我建议直接贴到团队的编码规范里。它背后的逻辑很简单能确定语句类型就用确定的方法确定不了才用 execute()。用确定的方法有两个好处一是代码意图清晰review 的时候一眼就知道这条 SQL 是查还是改二是驱动可以在早期做参数校验出错的第一现场更靠近问题代码而不是等到取值的时候才发现类型不对。顺便说一个反模式有人为了统一写法封一个ListMapString,Object query(String sql)内部一律用execute()然后根据返回值判断。这种写法看起来灵活实际上把所有类型信息都丢掉了调用方拿到一个ListMap根本不知道这是查询结果还是某个 UPDATE 影响的行数被包装成了一行。我接手过一个这样的系统排查一个数据不落库的问题花了两天最后发现是execute()返回 false 时工具类静默返回了空列表写了日志但被淹没了。4.2 动态 SQL 与多结果集场景真正需要execute()的场合不多但确实有第一类SQL 由外部输入决定。比如你写一个轻量级的 SQL 执行器用户在前端文本框里填任意 SQL你没法预判是查还是改。这时候execute()是唯一正确的选择配合getMoreResults()循环把所有结果都消费掉避免驱动里残留未读的数据。第二类一次提交多条语句。MySQL 的allowMultiQueriestrue、SQL Server 的多语句批处理都属于这一类。执行stmt.execute(SELECT ...; SELECT ...)之后第一个结果集用getResultSet()拿处理完调getMoreResults()返回 true 就再取一个返回 false 且getUpdateCount()为 -1 就结束了中间可能会穿插更新计数。第三类调用返回多个结果集的存储过程。有些老系统把查主表 查明细表压在一个存储过程里返回两个结果集这时候也只能用execute()逐个取。不过这种设计在新系统里已经很少见了更推荐拆成两次独立查询。注意使用多结果集时务必完整消费所有结果再关闭 Statement。有些驱动在还有未读结果的情况下提前关闭会抛异常或者导致连接无法回池。稳妥的做法是写一个循环一直取到getMoreResults()返回 false 且getUpdateCount()返回 -1。4.3 批量处理与流式查询的特殊处理这两类场景虽然不直接涉及三方法的选择但会放大方法选错带来的后果单独拎出来讲。批量写入的正确姿势是String sql INSERT INTO order_detail(order_id, sku, qty) VALUES (?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (OrderDetail d : list) { ps.setLong(1, d.getOrderId()); ps.setString(2, d.getSku()); ps.setInt(3, d.getQty()); ps.addBatch(); if (count % 1000 0) { ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch(); conn.commit(); }executeBatch()返回int[]每个元素对应一条语句的受影响行数。这里有个驱动差异MySQL 需要连接串加rewriteBatchedStatementstrue才能真正把多条 INSERT 合并成一条多值 INSERT否则它只是省了网络往返性能提升有限。批大小也不是越大越好我一般从 500 开始试1000 到 5000 之间找一个和内存、事务日志匹配的值超过 1 万容易在数据库侧造成长事务和锁等待。流式查询的目标是不要让驱动一次性把整张表拉进 JVM 内存。各家的开启方式不一样数据库开启方式注意事项MySQLstmt.setFetchSize(Integer.MIN_VALUE)会独占连接直到结果集读完PostgreSQLconn.setAutoCommit(false)stmt.setFetchSize(1000)不关自动提交fetchSize 不生效Oraclestmt.setFetchSize(1000)默认 10调大减少往返SQL Serverstmt.setFetchSize(1000)配合selectMethodcursor更稳流式读的时候用executeQuery()拿到的 ResultSet 一定要尽快读完或者主动关闭。如果中途 break 跳出循环不读了MySQL 上这条连接在驱动层面还处于结果未读完的状态直接归还连接池可能污染后续使用。所以要么读完要么在 finally 里显式rs.close()把剩余数据丢弃。5. 动手实操一套能跑通的对比代码5.1 建表与准备数据为了让对比有实感我先建一张简单表。用 MySQL 8 举例其他库把类型微调一下即可CREATE TABLE account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner VARCHAR(64) NOT NULL, balance DECIMAL(18, 2) NOT NULL DEFAULT 0.00, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); INSERT INTO account(owner, balance) VALUES (alice, 1000.00), (bob, 500.00), (carol, 0.00);连接获取部分我抽成一个方法后面复用private static Connection getConnection() throws SQLException { return DriverManager.getConnection( jdbc:mysql://127.0.0.1:3306/demo?useSSLfalseserverTimezoneAsia/Shanghai, demo, demo); }提示生产代码不要用 DriverManager 裸连交给连接池HikariCP、Druid 之类管理。DriverManager 每次都要建立物理连接而且没有连接有效性检测长时间空闲后第一次访问经常拿到已经断掉的连接。5.2 三种方法各跑一遍先看executeQuery()的标准用法public static void queryWithExecuteQuery() throws SQLException { String sql SELECT id, owner, balance FROM account WHERE balance ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, new BigDecimal(100.00)); ps.setFetchSize(100); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { System.out.printf(id%d owner%s balance%s%n, rs.getLong(id), rs.getString(owner), rs.getBigDecimal(balance)); } } } }再看executeUpdate()的两种典型场景public static void updateWithExecuteUpdate() throws SQLException { // DML返回受影响行数 String sql UPDATE account SET balance balance ? WHERE owner ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, new BigDecimal(100.00)); ps.setString(2, alice); int rows ps.executeUpdate(); if (rows 0) { // 注意不是失败只是没有匹配的行 System.out.println(没有匹配的账户请检查 owner 是否正确); } else { System.out.println(更新行数 rows); } } } public static void ddlWithExecuteUpdate() throws SQLException { // DDL返回 0但执行是成功的 String sql CREATE TABLE IF NOT EXISTS login_log (id BIGINT PRIMARY KEY AUTO_INCREMENT, uid BIGINT, ts TIMESTAMP); try (Connection conn getConnection(); Statement stmt conn.createStatement()) { int r stmt.executeUpdate(sql); System.out.println(DDL 返回 r); // 输出 0 } }最后用execute()处理不知道是什么语句的情况public static void executeUnknown(String sql) throws SQLException { try (Connection conn getConnection(); Statement stmt conn.createStatement()) { boolean isResultSet stmt.execute(sql); int resultIndex 0; while (true) { if (isResultSet) { try (ResultSet rs stmt.getResultSet()) { ResultSetMetaData md rs.getMetaData(); int cols md.getColumnCount(); System.out.println(--- 结果集 (resultIndex) 列数 cols); while (rs.next()) { StringBuilder sb new StringBuilder(); for (int i 1; i cols; i) { sb.append(md.getColumnLabel(i)).append().append(rs.getObject(i)).append( ); } System.out.println(sb); } } } else { int count stmt.getUpdateCount(); if (count -1) { break; } System.out.println(更新计数 count); } isResultSet stmt.getMoreResults(); } } }这段代码里最关键的是count -1这个终止条件。我特意在上一节强调过DDL 也可能返回 0所以不能用count 0来判断结束只有 -1 才代表没有更多结果了。5.3 用 JMeter 和 DataGrip 手动验证不写代码也想验证这三个方法的差异可以借工具。JMeter 的 JDBC Request 里有Query Type下拉框选项包括Select Statement、Update Statement、Prepared Select Statement、Prepared Update Statement、Callable Statement、Commit、Rollback、AutoCommit(false)等。它内部的实现就是按这个下拉值去调对应的方法选Select Statement走executeQuery()选Update Statement走executeUpdate()。如果故意把DELETE语句配成Select Statement就会看到我在开头说的那个报错。这也是排查接口压测时更新不生效这类问题的快捷方式——先看 Query Type 有没有选对。DataGrip 这边SQL 编辑器里执行语句时它会先做一次语句类型解析来决定用哪个执行路径。如果遇到某个 GoldenDB 或 GaussDB 这类兼容协议的数据源偶尔会出现解析判断偏差表现为明明是 SELECT 却报没有结果集或者UPDATE 执行完不显示行数。这时候可以在数据源的高级选项里调整prepareThreshold、preferQueryMode之类的驱动参数绕过驱动的预编译路径退回到简单查询模式。我在一个 GaussDB 项目上就遇到过查询结果集元数据缺失的问题把preferQueryMode从extended改成simple之后恢复正常代价是失去了服务端预编译的收益只在调试环境用。至于 JMeter 里把 JDBC 查询结果作为下一个接口参数的常规做法是在 JDBC Request 后面挂一个JDBC Request的Variable Names配置把某列映射成变量后续用${var_1}引用。这里有个容易踩的坑如果查询返回多行变量会变成var_1、var_1_1、var_1_2这样的形式var_1还额外存了一个行数。很多人以为${var_1}是第一行的值结果发现拿到的是行数排查半天。建议配合Debug Sampler先看一眼实际生成的变量名。6. 常见问题与排查技巧实录6.1 高频报错速查表下面这张表是我这些年积攒下来的基本覆盖了这三个方法的绝大多数报错场景报错信息大意根本原因解决方式Can not issue data manipulation statements with executeQuery()用 executeQuery 跑了 DML换成 executeUpdate 或 executeCan not issue SELECT via executeUpdate()用 executeUpdate 跑了查询换成 executeQueryThe statement did not return a result set同上SQL Server 版本同上ResultSet 已关闭 / Operation not allowed after ResultSet closedStatement 被复用或关闭ResultSet 失效用完再关 Statement别共用更新成功但返回 0 被误判为失败没有匹配行或 DDL 返回 0用是否抛异常判断成功多结果集只读到第一个没调用 getMoreResults写 while 循环完整消费大表查询 OOM未开启流式读取结果全部驻留内存设置 fetchSize流式读取中途 break 后连接异常结果集未读完就归还连接finally 里显式关闭 ResultSet这张表里的第二条我特别想展开说一句executeUpdate()传 SELECT 的情况比想象中多尤其是那些从别的数据库迁移过来的代码某些老驱动对 SELECT 走 executeUpdate 是容忍的迁移到新驱动或者换库之后就炸了。这类问题在跨库迁移项目里属于高发项建议在迁移前做一次全量代码扫描把所有executeUpdate(后面跟的 SQL 类型梳理一遍。6.2 连接器与工具链上的坑如果你的项目里用了 Flink 的 JDBC 连接器那么这套连接器内部对三个方法的使用是封装好的你通常不需要直接接触。但异常排查的时候理解它的行为会有帮助。Flink JDBC Sink 在写入时走的是executeBatch或者executeUpdate并在flush的时候提交事务。常见的异常有几类方言Dialect不匹配导致生成的 SQL 语法错误表现为 SQL 语法报错主键冲突导致整批回滚日志里只看到一条 Duplicate entry驱动类名或 JDBC URL 写错比如把某一类国产数据库的驱动类名写成通用形式连接直接失败。排这类问题的顺序建议是先确认 URL 和驱动 jar 版本匹配再确认方言是否选对最后看批大小和写入模式是否与目标库的承受能力匹配。国产数据库这边也是类似。GaussDB、神通这类数据库都有各自的 JDBC 驱动 jar从官网下载后要确认驱动类名和 URL 前缀不要照抄 MySQL 的写法。有些驱动对executeQuery()返回空结果集的处理不完全一致可能出现getMetaData()返回 null 的情况如果代码里直接用元数据做列名映射会 NPE。稳妥的写法是先判断rs.getMetaData() ! null再取列信息或者干脆用列序号取值。还有一个跨库通用的问题数据库连接串里的参数会改变方法的行为。MySQL 的useAffectedRows、allowMultiQueries、rewriteBatchedStatementsPostgreSQL 的preferQueryModeOracle 的defaultRowPrefetch每一个都可能让同一段 JDBC 代码在不同环境下表现不同。我现在的习惯是把连接串参数当成代码的一部分进版本管理并且在 README 里写清楚每个参数为什么加避免后来人以为是历史遗留随手删掉。6.3 资源释放与事务边界的注意事项最后讲几个和这三个方法配套使用、但经常被忽略的细节。第一ResultSet、Statement、Connection 的关闭顺序是反的。先关 ResultSet再关 Statement最后关 Connection。用 try-with-resources 的嵌套写法天然满足这个顺序手写finally容易出错。我见过把顺序写反的代码ResultSet 在 Statement 之后关闭在 MySQL 上大多时候没问题但在连接池场景下偶发Communications link failure。第二自动提交模式下每条 DML 都是一个独立事务。如果你批量执行一千条executeUpdate()即使它们逻辑上属于同一批业务操作也是提交了一千次。想让它们在一个事务里必须setAutoCommit(false)全部执行成功后commit()出错时rollback()。这里还要注意连接池的一个常见陷阱autoCommitfalse的连接如果忘记提交或回滚就归还了连接池把连接还给你的时候可能带着未完成的事务直接导致后续操作读到脏数据甚至锁等待。靠谱的连接池都会在归还时重置 autoCommit但依赖这个不如自己写对。第三DML 之后不要立刻 close 又立刻查同一张表。在部分数据库的默认隔离级别下比如可重复读同一个连接上先 UPDATE 再 SELECT看到的是自己的修改但如果你是 UPDATE 之后 close 连接换一条连接去查就需要等事务真正提交可见。这个差异在批处理和实时查询混跑的系统里经常造成数据写入成功但查询查不到的假象。判断方法很简单看这两次操作是不是同一个连接、同一个事务。第四DDL 在很多数据库里是隐式提交的。你在一个事务里先INSERT再CREATE TABLE后者会把你前面未提交的 INSERT 一起提交掉。这跟executeUpdate()本身无关但凡是执行 DDL 的地方都要提防。做数据库变更脚本的时候建议每条 DDL 独立执行、独立记录不要和业务 DML 混在一个事务里。我在实际项目里养成的习惯是DAO 层每个方法只做一件事查询方法返回领域对象列表更新方法返回受影响行数绝不在一个方法里同时干这两种事。看似多写了几个方法但半年后回头看代码的时候能省下大量这条 SQL 到底干嘛的推理时间。这个习惯的源头就是当年那个凌晨三点的executeQuery()报错——工具类为了通用把所有 SQL 都往一个口子上塞结果把类型信息全丢掉了。后来我把那个工具类拆成了query、update、execute三个入口调用方必须显式选择报错量直接降了下来。对于还在用裸Statement拼 SQL 的老项目我给一个低成本的改造思路先不改业务逻辑只在数据访问层加一层薄封装把三个方法的调用点按 SQL 类型分类统计出来看看哪些地方存在类型和调用不匹配的风险。扫描的结果往往能直接定位到几个隐藏很久的定时任务和后台管理功能这些地方平时没人点一旦触发就是生产事故。
返回列表