news 2026/10/20 5:36:17

JDBC中Statement与PreparedStatement的差异、预编译原理与实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDBC中Statement与PreparedStatement的差异、预编译原理与实战避坑指南

初看“Connection.createStatement”和“Statement/PreparedStatement”这个话题,不少人会觉得老掉牙——JDBC是Java后端最基础的东西,但凡写过数据库代码的人都用过。但我在这些年里,经历过的两个故障让我对这两个类的理解发生了彻底的改变:一次是凌晨被电话叫醒,说线上订单号重复入库;另一次是某同学在群里问“为什么我的SQL执行一次比一次慢,日志打出来都是编译时间”。这两个问题,最后都回到了Statement和PreparedStatement本身的机制上。

所以这个看似简单的标题,其实藏着不少值得掰开揉碎讲清楚的东西——连接层到底做了什么、预编译缓存在哪一层生效、批量操作的真正代价、以及那些网上很少被提到的边界情况。这篇文章就是把我自己踩过的坑、查过的源码、实测过的心得整理出来,希望对正在写JDBC代码的你有所帮助,尤其是那些已经能跑通基本CURD、但还想更进一步理解背后机制的同学。

1. 从一次线上事故说起:Statement和PreparedStatement的真实差别

1.1 那个凌晨的SQL拼串事故

那次事故的场景我至今记得很清楚:某提醒服务每天凌晨要向一批用户发消息,当时负责这块的同事为了快速上线,直接用Statement拼了一个很大的动态SQL。本来以为只是数据量大了变慢,结果用户开始收到重复消息。排查后发现,之前逻辑是每次先delete某张状态表再插入新记录,但因为SQL超长后触发了某种异常,导致delete没有执行成功,而insert还在继续,最终状态表里留下了大量旧数据——顺序乱了,重复消息就出来了。

这里面的真正元凶,就是对Statement性质的误判。用Statement的时候,SQL是作为字符串整体传给数据库的,每执行一次,数据库都要重新解析、生成执行计划。而更关键的是,一旦SQL里面拼了外部输入(比如用户ID、时间范围),这些输入被直接嵌入SQL文本,产生的不只是性能隐患,还有语义不可控的风险。那次事故中,就是因为拼接出的SQL包含了一个未清理干净的分支条件,实际执行的语句和预期完全不匹配,导致delete被逻辑跳过。

1.2 拼字符串到底错在了哪里

很多人觉得拼SQL只是不优雅,不觉得是技术债。但说一个最直接的例子:

String sql = "SELECT * FROM t_order WHERE user_id = " + userId;

这段代码在userId是一个普通数字时,看起来一切正常。但如果userId来自一个不受信任的入口,比如某人提交了一个1 OR 1=1,拼接出来的SQL就变成了:

SELECT * FROM t_order WHERE user_id = 1 OR 1=1

全表数据就都暴露了。这就是SQL注入最经典的形态。你可能会说“我们的入参都是数字,做了校验”,可现实里,很多业务字段是字符串、日期、甚至JSON片段,你永远无法保证每个入口都做了严格的转义和校验。

PreparedStatement处理这个问题的思路完全不同:SQL骨架和参数是分开传输的,数据库在编译阶段就把?当作参数占位符,之后再用实际值去填充。这句话有两层意思:一是SQL结构在编译时就被固定了,用户输入永远只是“数据”,而不可能变成“指令”;二是同一个SQL模板只需要编译一次,后续重复执行时直接复用执行计划,省掉了重复解析的开销。

1.3 用一张表看清Statement和PreparedStatement的差异

对比维度StatementPreparedStatement
SQL编译每次执行都完整解析、编译首次编译后缓存执行计划,重复执行直接复用
参数传递拼接进SQL文本,靠转义自求多福通过占位符传参,驱动自动处理类型转换
SQL注入风险高,取决于开发者是否做了严格的过滤、转义低,参数不会改变SQL语义
批量执行需逐条发送SQL文本,网络开销大支持预编译批处理,可显著减少通信与解析成本
可读性与维护性长字符串拼接,读起来费劲模板与参数分离,代码结构清晰
类型安全全部变成字符串,强制转换容易出错驱动会按参数类型做相应处理,减少转型问题

注意:PreparedStatement也不是绝对保险,比如错误地使用字符串拼接来构造SQL模板本身,或者在某些驱动版本中参数类型与数据库列类型不匹配导致隐式转换,依然可能带来意外。但相比Statement,它把风险降到了很低的量级。

2. 说透Connection.createStatement背后的设计逻辑

2.1 拿到Statement之前,连接层做了什么

用JDBC执行一条SQL,第一步永远是取得Connection。而Connection.createStatement()这个调用,很多人没有仔细想过它的开销。它并不是简单地“取一个现成的对象”,而是经过了一系列步骤:

首先是连接池获取连接。如果用的是HikariCP这类池化组件,这一步大多数时候是复用已有连接,开销很低。但如果没有连接池,每次new一个连接都要经过TCP握手、认证、会话初始化等流程,这个成本可能达到几十甚至上百毫秒,比执行一条简单SQL还贵。

然后是Driver层的处理。调用createStatement时,驱动会返回一个Statement对象,但它本质上是和这条连接绑定的。驱动内部可能还会懒加载一些元数据、设置会话变量、准备结果集游标等。不同驱动实现各有差异,MySQL Connector/J和PostgreSQL JDBC Driver的处理就不完全一样,但共同点是:这个环节有开销,而且连接的状态会影响创建Statement的成本。

再说说自动提交。默认情况下,JDBC连接是自动提交模式(autoCommit = true),也就是说,你每执行一条SQL,数据库都会立即提交事务。这个机制在单条SQL场景下很简单,但在一个业务方法里执行多条UPDATE时,就会产生多次事务提交,每一次提交都要刷写日志、释放锁,性能损耗非常明显。所以早年的实践往往都会在执行多条语句之前,先调用connection.setAutoCommit(false),最后统一commit。

2.2 Statement的三种基础操作

拿到Statement之后,最常用的三个方法就是executeQuery、executeUpdate和execute。

executeQuery用于执行SELECT,返回的是ResultSet,这个对象承载查询结果。注意:ResultSet并不是一次性把所有数据拉到内存,而是一般通过流式方式从数据库逐行读取(不同驱动实现细节不同)。这意味着如果查询返回了海量数据但你没有及时关闭ResultSet,连接和数据库端的游标资源就会被长期占用。

executeUpdate用于INSERT、UPDATE、DELETE等DML操作,返回的是受影响的行数。很多人忽略的细节是,某些数据库(甚至某些版本的驱动)对executeUpdate返回的“行数”语义有歧义,尤其是在批量操作时,返回的是每批的行数还是累计行数,需要实测确认。

execute是一个通用方法,SELECT和DML都能执行。如果SQL是SELECT,返回值为true,需要通过getResultSet拿结果集;如果是DML,返回值为false,需要靠getUpdateCount获取受影响行数。这个方法的定位在于SQL类型不确定的场景,比如动态生成的工具类。

还有一个容易被忽略的点——所有Statement对象在数据库层面最终都会对应一个游标或执行句柄。如果创建了Statement但对结果集只处理了一半就丢到一边,既没有关闭Statement,也没有关闭ResultSet,长此以往,连接池里的连接会被这些“半死状态”占满,新请求拿不到连接,业务就挂了。

2.3 ResultSet游标和数据类型转换

ResultSet的游标机制,恰恰解释了为什么那句“数据集从数据库取到本地”的说法不准确。默认情况下,Type为TYPE_FORWARD_ONLY,Concurrency为CONCUR_READ_ONLY,也就是只能向前移动、只读。这已经覆盖了绝大多数业务场景。

但在某些特殊场景下,比如你需要在一个事务里先查询再更新某一行,很多人会下意识想用ResultSet去updateRow。这时就要确认驱动的ResultSet到底是否支持可滚动、可更新。MySQL的驱动默认是不支持的,强制使用很容易抛异常。更合理的做法是查出主键后另起一条UPDATE语句,而不是试图在ResultSet上直接改。

数据类型转换也是坑。数据库里的DECIMAL、BIGINT、DATETIME,通过getString获取时驱动会做转换,但有些转换会导致精度损失,尤其是BIGINT超过Long.MAX_VALUE时,或者DATETIME在不同时区下的显示值不一样。通常我建议:按列的真实类型去取数据,能取Long就不取String,能用LocalDateTime就不硬拼字符串。

3. 为什么生产环境几乎见不到裸Statement

3.1 预编译的执行路径

PreparedStatement的好处,官网文档和各类教程都说了很多,但我想从执行路径的角度讲一下。在MySQL Connector/J中,PreparedStatement的执行过程大概是这样的:客户端把SQL模板和参数通过二进制协议发送到服务端;服务端先解析SQL并生成执行计划;将执行计划缓存起来;之后如果相同的SQL模板再次到达,就可以直接使用缓存的执行计划,而不用再解析一遍。

但这里有一个隐藏关键点:并不是所有“看起来一样”的SQL都能命中缓存。SQL文本必须完全一致,包括大小写、空格、换行。比如:

SELECT * FROM t_user WHERE id = ?

和

SELECT * FROM t_user WHERE id=? -- 注意少了空格,或者是全大写

在大部分数据库里被认为是不同的SQL,所以缓存就失效了。很多人喜欢用工具自动格式化SQL,在开发环境看着舒服,但如果在代码里通过模板引擎拼接SQL,格式不统一,就会导致预编译形同虚设。

更隐蔽的问题是:MySQL默认对PreparedStatement的预处理是“客户端模拟”的。MySQL Connector/J里面有一个参数useServerPrepStmts,默认是false。也就是说,如果不显式开启,PreparedStatement的占位符是在客户端完成的,SQL并没有真正发送到服务端做预编译。这种情况下,防注入的能力依然有效,但性能收益会打折扣。生产环境中,如果希望真正利用服务端预编译,通常需要:

jdbc:mysql://localhost:3306/test?useServerPrepStmts=true&cachePrepStmts=true

开启cachePrepStmts后,驱动会在客户端缓存PreparedStatement句柄,避免重复请求服务端创建预编译语句。这个参数的影响在我实测的项目里,语句重复执行上百次时,整体耗时能降低20%以上,尤其是复杂SQL。但也不是越大越好,假如每个业务方法都构造自己独有的SQL文本,缓存里会积累大量无用的预编译语句,反而增加内存压力。

3.2 性能差异的定量感受

很多同学问:用Statement重复插入1000条记录,和用PreparedStatement批量插入,差距到底有多大?我做过一个简单的对比测试:本机数据库,插入1000条数据,每条数据10个字段左右。

用Statement逐条拼接执行,耗时大约800毫秒到1秒;用PreparedStatement逐条执行,耗时大约600到700毫秒;用PreparedStatement的批量模式(addBatch + executeBatch),耗时能压缩到100毫秒左右。

为什么批量模式这么快?因为减少了网络往返次数。逐条插入时,每条数据都要发送一个完整的请求,1000条就是1000次往返;批量模式可以把1000条数据的参数一次性打包发送(当然驱动内部会按阈值分批),网络通信次数大幅减少。还有一个原因是,每条SQL的编译工作只做一次,后续执行计划复用。

这个测试说明,PreparedStatement并不只是安全性的选择,更是性能和资源利用的选择。尤其是你的业务中频繁执行结构相同、参数不同的SQL时,预编译的收益相当可观。

3.3 用PreparedStatement的正确姿势

用法上,和我见过很多半路出家的代码对比,有几个容易被忽略的细节:

占位符是1-based——?的下标从1开始,不是0。这个错误藏在代码里非常隐蔽,因为大多数时候你只在JDBC代码里和下标打交道,一旦列多、条件长、动态SQL复杂,越界和错位几乎是必然。我的建议是:动态条件多时,先把条件和参数分别存入List,再按顺序绑定。

参数类型要用setObject还是setString—— 新手习惯setString(1, value),但对于日期、数字、Boolean这种字段,setString会让数据库做一次隐式类型转换,可能无法命中索引。正确的做法是按Java类型选择setInt、setLong、setTimestamp、setBigDecimal等方法。如果字段类型确实不确定,可以用setObject,驱动会按照Java对象的类型去映射。

不要在一次执行中反复设置同一参数—— 很多人会在循环里重新调用setString,其实这是允许的,但要注意有些驱动会检查参数是否变更,频繁变更本身也会增加额外的状态同步开销。更合理的做法是:循环内部尽量只修改必要的参数,或者直接复用已经绑定好的参数。

用完就关,别靠GC—— Statement、ResultSet、Connection这三个对象的关闭顺序也很讲究。最标准的做法是在try-with-resources中逆序声明:先声明Connection,再声明PreparedStatement,最后声明ResultSet。这样当try块结束时,关闭顺序正好是ResultSet → Statement → Connection,不会出现“连接忘了关”的问题。

4. 实操:用PreparedStatement完成一整套数据库操作

4.1 基础增删改查的代码骨架

直接给出一套常见的基础代码,注释里写清楚每一步的含义:

public List<UserDO> listUsers(Date startTime, Date endTime) { String sql = "SELECT id, name, created_at FROM t_user WHERE created_at BETWEEN ? AND ?"; List<UserDO> users = new ArrayList<>(); try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 占位符从1开始,第一个?对应startTime,第二个?对应endTime ps.setTimestamp(1, new Timestamp(startTime.getTime())); ps.setTimestamp(2, new Timestamp(endTime.getTime())); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UserDO user = new UserDO(); user.setId(rs.getLong("id")); user.setName(rs.getString("name")); user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); users.add(user); } } } catch (SQLException e) { // 生产环境应替换为更精细的日志和异常转换 log.error("query users failed, sql={}, startTime={}, endTime={}", sql, startTime, endTime, e); throw new RuntimeException("query user failed", e); } return users; }

这段代码核心的几个点:占位符和set方法一一对应;try-with-resources保证了连接、语句、结果集都被正确关闭;使用setTimestamp来绑定时间字段,避免字符串拼接引发格式问题。看似简单,但在真实项目中这些细节决定了代码是能稳健运行还是三天两头出bug。

4.2 批量插入的正确写法

批量插入是最能体现PreparedStatement价值的场景。在订单、日志、行为流水等数据量较大的写入场景中,批量插入几乎是必修课。一个标准的写法如下:

public void batchInsertOrders(List<OrderDO> orders) { String sql = "INSERT INTO t_order (order_no, user_id, amount, status, created_at) VALUES (?, ?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 关闭自动提交,便于统一事务管理 conn.setAutoCommit(false); int batchSize = 0; for (OrderDO order : orders) { ps.setString(1, order.getOrderNo()); ps.setLong(2, order.getUserId()); ps.setBigDecimal(3, order.getAmount()); ps.setString(4, order.getStatus()); ps.setTimestamp(5, Timestamp.valueOf(order.getCreatedAt())); ps.addBatch(); batchSize++; // 每500条提交一次,避免单次批处理过大导致内存占用过高 if (batchSize % 500 == 0) { ps.executeBatch(); } } // 提交剩余不足500条的批次 ps.executeBatch(); conn.commit(); } catch (SQLException e) { // 出错时回滚,避免部分数据残留 log.error("batch insert orders failed", e); try (Connection conn = dataSource.getConnection()) { conn.rollback(); } catch (SQLException ex) { log.error("rollback failed", ex); } throw new RuntimeException("batch insert orders failed", e); } }

这里有几个心得。分批提交的阈值,我一般设置为200到500条。太大的话,单次executeBatch会占用大量内存保存参数数据;太小的话,又没法有效减少网络往返。另一个坑是:如果某一条数据在批处理中执行失败,数据库的行为取决于QO和驱动实现,有的会直接中断整个批次,有的会继续执行后面的,还有的会抛出BatchUpdateException并携带成功数。所以捕获异常时,最好把BatchUpdateException的getUpdateCounts打出来看看,明确哪些成功、哪些失败。

4.3 获取自增主键

插入一条记录后,经常需要立即拿到自增ID,用Statement.RETURN_GENERATED_KEYS的写法非常方便。PreparedStatement同样支持:

String sql = "INSERT INTO t_biz (biz_name) VALUES (?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, bizName); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { Long id = rs.getLong(1); return id; } } }

但要注意,getGeneratedKeys的有效性和数据库/驱动强相关。MySQL是支持的,PostgreSQL也能用,但某些老版本的驱动或某些中间件可能拿到空结果。如果项目本身是双数据库兼容,我建议用规范的SQL查询方式回查一遍主键,而不是完全依赖这个功能。另外,如果插入语句里指定了主键值,那getGeneratedKeys大概率返回空,这是常见误解。

4.4 动态查询条件组合

动态SQL在业务系统中太常见了,但很多人还是抱着“拼接字符串”的思维去写。PreparedStatement的动态SQL要稍微绕一下,但思路很明确:先用List收集条件片段和参数,再一次性生成SQL。

public List<UserDO> search(String name, Integer minAge, Integer maxAge) { StringBuilder sql = new StringBuilder("SELECT id, name, age FROM t_user WHERE 1 = 1"); List<Object> params = new ArrayList<>(); if (StringUtils.hasText(name)) { sql.append(" AND name LIKE ?"); params.add("%" + name + "%"); } if (minAge != null) { sql.append(" AND age >= ?"); params.add(minAge); } if (maxAge != null) { sql.append(" AND age <= ?"); params.add(maxAge); } try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } try (ResultSet rs = ps.executeQuery()) { // 处理结果... } } }

这里的核心是“参数和条件同步收集”,这样就避免了在拼接SQL时还要重新数占位符的尴尬。条件一多,这种写法尤其省心。注意LIKE语句中,%和_是放在参数里拼接的,不能在SQL模板里直接写,否则占位符会被通配符干扰,查询结果和预期完全不一样。

5. 常见坑与排查实录

5.1 连接泄漏——我见过最普遍的事故

连接泄漏是所有使用Connection的程序员的噩梦,它的表现不是“立刻报错”,而是“运行几天后突然大量超时”。我见到过的典型场景:某同学在方法里获取了Connection,但忘记在finally里关闭,因为平时流量小看不出问题,一旦流量上来,连接池的可用连接被占满,新请求全部排队,最终触发了雪崩效应。

排查方法很简单:开启连接池的泄漏检测。HikariCP可以配置leakDetectionThreshold,比如设为10000毫秒,连接从池中借出超过这个时间没有归还,日志里就会打印一段带堆栈的警告,那一段堆栈能直接定位到是哪一行代码借了连接没还。这个方法救过我好几次,强烈建议线上开启,代价是微乎其微的性能开销。

另一个防止连接泄漏的实践是:避免在try块里手动close,而是使用try-with-resources。但要说清楚,try-with-resources只能保证“声明在try括号里的资源”被关闭,如果你在方法内部先获取了Connection,然后又传给另一个方法去执行SQL,关闭的责任就要被清晰定义,否则很容易出现“我关了你还在用”的异常,或者反过来“你没关你也不管”的泄漏。

5.2 where in 参数占位符失效

很多人在SQL里写WHERE id IN (?),然后试图用setString把一组ID传进去,结果发现只匹配到一条数据,甚至报错。原因很简单:JDBC的占位符是“一个占位符对应一个值”,并不是“一个占位符对应一个列表”。

我一般推荐两种方案。第一种是动态生成占位符串:

List<Long> ids = Arrays.asList(1L, 2L, 3L); String placeholders = ids.stream().map(id -> "?").collect(Collectors.joining(",")); String sql = "SELECT * FROM t_user WHERE id IN (" + placeholders + ")";

这种方式直白且兼容性好。注意IN子句的列表不宜过长,否则不仅SQL文本变大,数据库执行计划也可能退化。超过1000个ID时,我一般会分批查询或者改用临时表关联。

第二种方案是对接一些高级特性,比如PostgreSQL的数组类型、MySQL的JSON_CONTAINS,但前提是你的数据库版本和驱动都支持,而且不能为了用特性而牺牲可移植性。

5.3 大结果集吃内存

ResultSet默认是一次性把数据拉回客户端吗?其实不一定。MySQL驱动在处理大结果集时有useCursorFetch参数。默认情况下,MySQL是边读边取的,但如果你执行了一条返回十万行的查询,又不想全部加载到内存,可以考虑设置statement.setFetchSize(Integer.MIN_VALUE),这会启用流式读取,逐行从服务端取数,代价是读取期间不能执行其他语句(某些版本lock了连接)。

很多做报表导出、大批量数据同步的同学,在这块踩坑最多。明明查了几万行数据就把内存打满,其实就是没有显式设置fetchSize,驱动可能是把结果一次性拉到了客户端。解决方式要先确认驱动行为,再去设fetchSize,不要想当然。

5.4 事务超时与自动提交误区

事务和Statement的关系,很多人理解是“事务就是多条SQL放在一起”。但一个容易忽视的点是:在JDBC里,事务边界由Connection控制,而不是由Statement控制。如果在一个方法里打开事务,先执行了INSERT,中途抛了异常,但你只关闭了Statement没有回滚,这个事务可能一直挂着,占着数据库连接和锁资源。

更糟糕的误操作是:把setAutoCommit(false)和commit写进了循环里。每调一次commit,事务就结束了,后续SQL自动开启新事务。如果业务上需要“全部成功或全部失败”,这种写法是灾难。我见过一个定时任务就是因为循环里commit,导致前面一批数据处理成功、后面失败时无法回滚,最终产生了数据不一致。

建议把事务管理放到独立的层,比如Spring的@Transactional,避免自己在业务代码里散落地调用setAutoCommit和commit,这样不仅清晰,也容易被框架的统一机制兜底。

5.5 预编译缓存被撑爆

服务端预编译虽然好,但如果你在SQL模板里拼接了很多不同的字符串值(比如把表名、字段名、排序字段也拼进去),那缓存里会有成千上万个不同的SQL模板,内存和解析开销都上来了。这种情况下,你不仅没享受到预编译的复用收益,反而比Statement更糟。

我的一个习惯是:表名、字段名、排序字段这些“SQL结构”部分,最好通过白名单的方式去映射,不要直接用外部参数拼接。比如前端传一个orderBy字段,代码里就做一个Map映射,只允许白名单里的值进入SQL,其他值一律用默认排序。这样既安全,又保证了SQL模板的数量可控。

5.6 一个容易被忽略的参数绑定顺序问题

之前说过占位符从1开始,这里再补充一个实战细节。在动态SQL中,如果你先检查A条件,再检查B条件,那么参数绑定的顺序也必须严格按照条件出现的顺序来。代码里第一次写时,A条件在前面,B条件在后面;后来有人调整了条件的先后顺序,但忘了调整参数List的添加顺序,结果就是查询结果完全错乱,而且不会报错。这种bug极其隐蔽,排查时最好的办法是在日志里把最终SQL和绑定的参数一起打出来,肉眼对一遍。

6. 一些长期实践下来的心得体会

6.1 建议的代码组织方式

围绕Statement和PreparedStatement,我建议项目里往这几个方向去建设:

  • 统一SQL管理:SQL不要散落在业务代码里,尽量收拢到DAO层的常量或XML中,方便审计和优化。
  • 统一异常处理:JDBC抛出的SQLException信息很笼统,建议包装成带SQL、参数、错误码的领域异常,方便定位。
  • 统一参数绑定:如果项目里没有用ORM框架,可以封装一个简单的JdbcHelper,内部统一管理占位符绑定和资源关闭,避免每个方法都重复写try-with-resources模板。
  • 适当引入ORM:当项目复杂到一定程度,纯手写JDBC的维护成本会非常高,这时选用成熟的ORM框架(比如MyBatis或Spring JdbcTemplate)并不丢人,但底层原理仍然是今天讲到的这些内容。

6.2 需要避开的几个“反模式”

  • 用Statement拼接用户输入,除了教学Demo,我建议生产代码里完全杜绝。
  • 为了图省事统一setString,导致索引失效。该setInt就setInt,该setTimestamp就setTimestamp。
  • 在一个方法里创建多个Statement对象。比如先查一次,再插入一次,再查一次,你完全可以用同一个Statement执行多条SQL(当然不同预处理语句除外),没必要每条SQL都创建新对象,白白增加驱动和数据库的开销。
  • 企图修改ResultSet里的值来更新数据库。这个在多数驱动里是不支持的,即便支持,也容易引发并发问题,不是一个好的实践。
  • 忽视数据库和驱动的版本差异。同一个功能,在不同版本的MySQL驱动上行为可能不同,比如getGeneratedKeys、服务端预编译、大结果集流式读取,升级驱动后要重点回归。

6.3 写在最后的一点体会

我个人在实际项目里的感受是,Connection.createStatement这行代码本身没有任何问题,它不是洪水猛兽,但它只是一个起点。真正让你少加班的,是你对Statement机制的理解深度——你清楚参数如何绑定、执行计划如何缓存、连接和事务的边界在哪里,你才能在遇到诡异的线上故障时,迅速判断问题是出在SQL本身、连接状态、还是驱动行为上。这些年修复过的很多看似神秘的数据库问题,追根究底,大部分都绕不开今天聊的这几个细节。希望这篇总结能帮你少走一些弯路,也欢迎你在自己的项目里实测一下批量模式的性能提升,实践出来的结论,比任何文档都可靠。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:35:51

Grok Bot 从零搭建实战:环境准备、安装配置与群组接入保姆教程

1. 从零理解 Grok Bot&#xff1a;它到底是什么&#xff0c;能帮你做什么Grok Bot 这个词最近在技术社区里出现的频率越来越高&#xff0c;但很多人第一次听到的时候会有点懵——它到底是一个聊天机器人&#xff0c;还是一个自动化工具&#xff0c;还是某种群组助手&#xff1f…

作者头像 李华
网站建设 2026/10/20 5:35:14

JSON/XML格式化与比对CLI工具:本地化、语义感知、CI/CD就绪

简介&#xff1a;这是一款面向前端开发者、测试工程师及API接口调试人员的轻量级Web端格式化与比对工具集&#xff0c;聚焦JSON/XML数据的校验、美化与差异分析痛点。资源包含7个文件&#xff08;2个HTML主页面、1个CSS样式表、1个JS核心逻辑脚本、1个PNG图标、1个README说明文…

作者头像 李华
网站建设 2026/10/20 5:35:13

3DGS端侧重建:手机拍一圈就能生成3D模型

拍一圈&#xff0c;真的能出3D模型——看到鸿蒙7这个“3DGS端侧重建”特性视频时&#xff0c;我第一反应是&#xff1a;等了这么久&#xff0c;终于有人把3D Gaussian Splatting真刀真枪搬进了手机本地。以前聊3D重建&#xff0c;要么是实验室里跑几天几夜的离线渲染&#xff0…

作者头像 李华
网站建设 2026/10/20 5:35:07

CAD2024企业级静默安装与许可部署实战指南

简介&#xff1a;本资源是一份面向机械设计初学者与工程师的CAD2024&#xff08;机械版&#xff09;安装实践指南&#xff0c;聚焦解决零基础用户在Windows系统下部署专业CAD工具时常见的环境适配、权限配置与路径选择等实操难题。压缩包为5KB的ZIP格式&#xff0c;共含3个核心…

作者头像 李华
网站建设 2026/10/20 5:34:25

DeepSite V2:从Prompt生成可运行前端项目的本地AI工程编译器

简介&#xff1a;DeepSite V2是一款面向前端开发者、AI初学者及快速原型设计者的开源AI建站工具&#xff0c;基于DeepSeek大模型实现自然语言到可运行网页的端到端生成&#xff0c;显著降低网页开发门槛。资源包仅3个核心文件&#xff08;1个HTML主页面、1个.inscode配置文件、…

作者头像 李华
网站建设 2026/10/20 5:34:23

手写Python协同过滤:在线购物系统商品推荐实战全解

1. 没有商品-用户矩阵&#xff0c;协同过滤就无法落地&#xff1a;先理清数据底座做在线购物系统的商品推荐&#xff0c;很多人第一反应是去套一个算法库&#xff0c;跑通别人给的示例数据&#xff0c;再把模型塞进 Web 服务里。真上线后才发现&#xff0c;推荐结果要么是空的&…

作者头像 李华