本节摘要:原生 JDBC 的代码里,真正有信息量的只有 SQL 与结果映射那几行,其余全是取连接、建语句、关资源的仪式性代码。JdbcTemplate 用模板方法把这些仪式收走。本节盘点痛点清单、给出查询与更新的完整写法,并讨论什么时候它比 ORM 更合适。
不用模板时,一次查询要写这么多:
Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = dataSource.getConnection(); ps = conn.prepareStatement("SELECT id, name FROM users WHERE id = ?"); ps.setLong(1, userId); rs = ps.executeQuery(); if (rs.next()) { return new User(rs.getLong("id"), rs.getString("name")); } return null; } finally { // 三段关闭,顺序不能错 if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); }
十四行里只有两行是业务信息(SQL 与字段映射)。更糟的是隐患:异常路径漏关连接,连接池慢慢耗尽,应用在流量高峰假死。模板要消化的是四类样板:资源管理、异常转译、语句执行、结果映射。
@Repository public class JdbcUserRepository { private final JdbcTemplate jdbc; public JdbcUserRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; } public User findById(Long id) { return jdbc.queryForObject( "SELECT id, name FROM users WHERE id = ?", (rs, i) -> new User(rs.getLong("id"), rs.getString("name")), id); } public int insert(User user) { return jdbc.update("INSERT INTO users VALUES (?, ?)", user.id(), user.name()); } }
连接的取用与归还、语句的创建与关闭、结果集遍历、受检异常到运行时异常的转译,全部进了模板。你留下的每行代码都携带业务信息。异常转译值得一提:各家数据库的错误码千差万别,模板把它们统一翻译成层次清晰的异常体系,比如唯一键冲突统一表现为重复键异常——捕获方不必再关心底下是哪家数据库。

模板不是过渡方案,它在几类场景里反而是首选:报表类复杂 SQL,ORM 的Criteria 写出来又长又难调;批量更新用模板的批处理接口一条语句反复执行,吞吐远胜逐条 save;团队 SQL 功底好、希望对语句有完全掌控时,显式 SQL 比生成的更可审计。反过来,对象关系复杂、增删改查为主的应用,第 5.2 节的方案省力得多。
💡 连接池与模板是搭档:模板只管借还,池子大小决定并发上限。压测前先确认池配置与容器线程池的比例,二者失配是隐性吞吐瓶颈的常见来源。
背景:订单月度报表用 ORM 的条件构造器写了两百多行,生成七张表连接的语句,性能与可维护性双差。操作:迁回 JdbcTemplate。第一步,把 DBA 调优过的 SQL 原样搬进模板方法,参数用占位符;第二步,写一个行映射器把列装配进报表视图对象:
public List<MonthlyReportRow> monthly(int year, int month) { return jdbc.query( """ SELECT u.city, COUNT(o.id) AS cnt, SUM(o.amount) AS total FROM orders o JOIN users u ON o.user_id = u.id WHERE YEAR(o.created_at) = ? AND MONTH(o.created_at) = ? GROUP BY u.city ORDER BY total DESC """, (rs, i) -> new MonthlyReportRow( rs.getString("city"), rs.getLong("cnt"), rs.getBigDecimal("total")), year, month); }
结果:代码从两百行缩到三十行,语句与 DBA 给的调优版本逐字一致,执行计划可复核。解读:迁移的本质是把"描述转换成 SQL"的职责从代码移回 SQL 本身,复杂分析查询里这是更短的路径。变式:再补一个批处理接口做月度数据归档,一次传入整月主键列表批量更新状态,吞吐相比逐条更新高一个数量级——这也是 8.4 节批处理思想的微缩预演。
经验补充:模板方法抛出的异常已统一转译,捕获重复键异常做幂等重试是常见组合:
try { jdbc.update("INSERT INTO users VALUES (?, ?)", id, name); } catch (DuplicateKeyException e) { // 转译后的统一异常 log.info("用户已存在,按幂等处理跳过:{}", id); }
最后补一个命名参数的写法提示:占位符的问号在参数多时可读性差,模板同样支持冒号命名的参数风格,SQL 与参数按名字对应,改顺序不会错位。模板这条"最短路径"的要义是:框架收走仪式,你只留下意图——SQL 与映射,两样都不能省也都不必多。