在 Spring 的数据访问体系中,JdbcTemplate 是 JDBC 核心模块中最常用的工具类。它封装了传统 JDBC 中繁琐的资源管理、异常处理和样板代码,让开发者可以将注意力集中在 SQL 本身和结果映射上,而不再纠结于 Connection、Statement、ResultSet 的手动获取与关闭。本节将从基础查询、更新操作到批量处理,逐一展示 JdbcTemplate 的实用模式。
13.2.1 JdbcTemplate 的初始化
要在项目中使用 JdbcTemplate,通常只需要配置一个数据源,然后通过 Spring 的自动装配或手动创建即可。
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/app_db");
config.setUsername("app");
config.setPassword("secret");
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
在 Spring Boot 项目中,只需引入 spring-boot-starter-jdbc 并在配置文件中提供数据库连接信息,Spring Boot 会自动配置好 JdbcTemplate,之后直接注入即可使用:
@Autowired
private JdbcTemplate jdbcTemplate;
13.2.2 查询数据
JdbcTemplate 提供了一系列 query 方法,满足从简单查询到复杂结果映射的各种需求。
1. 查询单行单列
queryForObject 可以直接获取某个单一值,比如统计计数:
String sql = "SELECT COUNT(*) FROM orders WHERE user_id = ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, 1001L);
这里第三个参数 1001L 会替换 SQL 中的占位符 ?。返回值的类型由第二个参数指定,支持基本类型的包装类和 String 等。
2. 查询单行记录,映射为对象
当需要将一行记录映射为一个 Java 对象时,可以使用 RowMapper 接口。RowMapper 负责将 ResultSet 的当前行转换成一个对象。
String sql = "SELECT id, user_id, amount, create_time FROM orders WHERE id = ?";
Order order = jdbcTemplate.queryForObject(sql, new RowMapper<Order>() {
@Override
public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
Order o = new Order();
o.setId(rs.getLong("id"));
o.setUserId(rs.getLong("user_id"));
o.setAmount(rs.getBigDecimal("amount"));
o.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
return o;
}
}, 2001L);
Java 8 之后,RowMapper 可以简化为 Lambda 表达式,更加清爽:
Order order = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
Order o = new Order();
o.setId(rs.getLong("id"));
o.setUserId(rs.getLong("user_id"));
o.setAmount(rs.getBigDecimal("amount"));
o.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
return o;
}, 2001L);
如果查询结果为空,queryForObject 会抛出 EmptyResultDataAccessException,需要根据业务决定是捕获处理还是允许异常向上传播。
3. 查询多行记录
query 方法返回一个 List,每一行都会被 RowMapper 处理:
String sql = "SELECT id, user_id, amount, create_time FROM orders WHERE user_id = ?";
List<Order> orders = jdbcTemplate.query(sql, (rs, rowNum) -> {
Order o = new Order();
o.setId(rs.getLong("id"));
o.setUserId(rs.getLong("user_id"));
o.setAmount(rs.getBigDecimal("amount"));
o.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
return o;
}, 1001L);
查询的结果集为空时,返回的是空列表而非 null,这在批量处理时减少了判空烦恼。
4. 使用 BeanPropertyRowMapper 自动映射
在某些简单场景下,如果 Java 对象的属性名与数据库列名完全一致(不区分大小写),可以使用 BeanPropertyRowMapper 完成自动映射,省去手写 RowMapper。
String sql = "SELECT id, user_id, amount, create_time FROM orders WHERE user_id = ?";
List<Order> orders = jdbcTemplate.query(sql,
new BeanPropertyRowMapper<>(Order.class), 1001L);
但要注意,这种方式依赖标准的 JavaBean 命名规范,且无法处理列名与属性名不一致的情况(比如数据库列是 user_id,而 Java 属性是 userId,默认不会自动转换)。自 Spring 3.0 起,可以在 BeanPropertyRowMapper 中自定义映射关系或开启下划线转驼峰的匹配策略。
13.2.3 插入、更新与删除
不返回结果的 DML 语句,主要使用 update 方法。它返回的是受影响的记录数。
// 插入
String insertSql = "INSERT INTO orders (user_id, amount, create_time) VALUES (?, ?, ?)";
int rows = jdbcTemplate.update(insertSql, 1001L, new BigDecimal("199.99"), LocalDateTime.now());
// 更新
String updateSql = "UPDATE orders SET amount = ? WHERE id = ?";
int updated = jdbcTemplate.update(updateSql, new BigDecimal("249.99"), 2001L);
// 删除
String deleteSql = "DELETE FROM orders WHERE id = ?";
int deleted = jdbcTemplate.update(deleteSql, 2001L);
对于需要返回自增主键的插入操作,可以使用 PreparedStatementCreator 配合 KeyHolder:
String sql = "INSERT INTO orders (user_id, amount, create_time) VALUES (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setLong(1, 1001L);
ps.setBigDecimal(2, new BigDecimal("199.99"));
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
return ps;
}, keyHolder);
Long newId = keyHolder.getKey().longValue();
System.out.println("新插入记录的主键:" + newId);
13.2.4 批量操作
当需要一次性执行大量类似的 SQL 语句时,批量操作可以显著提升性能,减少数据库的往返次数。
1. 批量更新(单条 SQL 多组参数)
batchUpdate 方法接收一条 SQL 和一个 List,List 中的每个元素代表一组参数(通常用 Object[]),框架内部会使用 PreparedStatement 的 addBatch 和 executeBatch 完成批量提交。
String sql = "INSERT INTO orders (user_id, amount, create_time) VALUES (?, ?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
batchArgs.add(new Object[]{1001L, new BigDecimal("50.00"), LocalDateTime.now()});
batchArgs.add(new Object[]{1002L, new BigDecimal("120.00"), LocalDateTime.now()});
batchArgs.add(new Object[]{1003L, new BigDecimal("75.00"), LocalDateTime.now()});
int[] affectedRows = jdbcTemplate.batchUpdate(sql, batchArgs);
返回的 int[] 数组记录了每组参数执行后影响的行数。如果希望更精细的控制(比如回滚或继续处理特定失败),可以将 SQL 和 String 列表的形式换为 PreparedStatement 回调。
2. 批量处理多个不同的 SQL
当需要在一个批次中执行多条完全不同的 SQL(例如混合更新和删除),可以使用 batchUpdate 的重载版本,传入多个 SQL 字符串:
String[] sqls = new String[]{
"UPDATE orders SET amount = 100 WHERE id = 1",
"DELETE FROM orders WHERE id = 2",
"INSERT INTO order_log (msg) VALUES ('batch done')"
};
int[] results = jdbcTemplate.batchUpdate(sqls);
但这种用法不支持参数化,在实际业务中较少使用,更常见的仍是参数化的批量插入/更新。
3. 大批量数据的分批处理
如果一次批量操作的数据量极大(例如数十万条),一次性提交可能导致内存过高或数据库事务时间过长。这时可以结合业务场景将数据分片,分批次提交:
public void batchInsert(List<Order> orders, int batchSize) {
String sql = "INSERT INTO orders (user_id, amount, create_time) VALUES (?, ?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
for (Order order : orders) {
batchArgs.add(new Object[]{
order.getUserId(), order.getAmount(), order.getCreateTime()
});
if (batchArgs.size() % batchSize == 0) {
jdbcTemplate.batchUpdate(sql, batchArgs);
batchArgs.clear();
}
}
// 处理剩余不足一批的数据
if (!batchArgs.isEmpty()) {
jdbcTemplate.batchUpdate(sql, batchArgs);
}
}
通过控制 batchSize,可以在吞吐量和资源消耗之间找到平衡。
13.2.5 调用存储过程与函数
JdbcTemplate 还支持调用存储过程和函数,通过 call 方法或使用 SimpleJdbcCall 可以更便捷地操作。
简单调用无返回参数的存储过程:
jdbcTemplate.update("CALL clean_expired_orders(?)", 30);
// 或者使用 call
jdbcTemplate.call(conn -> {
CallableStatement cs = conn.prepareCall("{call clean_expired_orders(?)}");
cs.setInt(1, 30);
return cs;
}, Collections.emptyList());
对于有输出参数的存储过程,推荐使用 SimpleJdbcCall,它提供了更清晰、链式的 API。
13.2.6 异常处理与数据访问层的最佳实践
JdbcTemplate 会将 JDBC 中的受检异常 SQLException 转换为 Spring 的非受检异常体系 DataAccessException。这意味着开发者可以灵活选择是否捕获异常,避免代码中充斥大量 try-catch。
常见的具体异常有:
DataIntegrityViolationException:唯一键或外键约束冲突。DuplicateKeyException:主键或唯一索引重复。BadSqlGrammarException:SQL 语法错误。EmptyResultDataAccessException:期望返回至少一行结果却无数据。
在数据访问层的实现中,通常将 JdbcTemplate 封装到一个 Repository 类中,对外暴露清晰的方法:
@Repository
public class OrderRepository {
private final JdbcTemplate jdbcTemplate;
public OrderRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Order findById(Long id) {
String sql = "SELECT * FROM orders WHERE id = ?";
return jdbcTemplate.queryForObject(sql, orderRowMapper(), id);
}
public int save(Order order) {
String sql = "INSERT INTO orders (user_id, amount, create_time) VALUES (?, ?, ?)";
return jdbcTemplate.update(sql, order.getUserId(), order.getAmount(), order.getCreateTime());
}
private RowMapper<Order> orderRowMapper() {
return (rs, rowNum) -> {
Order o = new Order();
o.setId(rs.getLong("id"));
o.setUserId(rs.getLong("user_id"));
o.setAmount(rs.getBigDecimal("amount"));
o.setCreateTime(rs.getTimestamp("create_time").toLocalDateTime());
return o;
};
}
}
13.2.7 总结
JdbcTemplate 的价值在于用最少的代码完成 JDBC 操作,却又不隐藏细节。它保留了 SQL 的完全控制力,同时消除了连接泄漏、资源忘记关闭的风险。在实际项目中,即使引用了 ORM 框架,也常常在复杂报表、批量处理等场景中使用 JdbcTemplate 作为补充工具。
需要注意的是,JdbcTemplate 本身是线程安全的,可以作为单例在应用中使用。但仍要遵守 JDBC 事务边界的管理,通常与 Spring 的声明式事务结合,保证数据的一致性。