人人都会AI编程

13.2 JdbcTemplate 基础使用与批量操作

更新时间:2026-07-10

在 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[]),框架内部会使用 PreparedStatementaddBatchexecuteBatch 完成批量提交。

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 的声明式事务结合,保证数据的一致性。