一、核心概念

JdbcTemplate 是 Spring Framework 提供的一个核心类,用于简化 JDBC 操作,极大地减少了传统 JDBC 编程中大量的样板代码(如获取连接、创建语句、处理结果集、关闭资源、处理异常等)。在 Spring Boot 中,通过 spring-boot-starter-jdbc 起步依赖可以轻松集成和使用。

关键概念

  1. JdbcTemplate 类:

    • 核心类,提供了执行 SQL 查询、更新、调用存储过程等方法。
    • 它封装了 DataSource 的获取、连接管理、异常转换和资源清理。
    • 方法命名清晰:queryXxx 用于查询,update 用于增删改,execute 用于执行任意 SQL。
  2. DataSource

    • JdbcTemplate 依赖于一个 javax.sql.DataSource 实例来获取数据库连接。
    • Spring Boot 会根据 application.properties/application.yml 中的配置(如 spring.datasource.url, spring.datasource.username, spring.datasource.password)自动配置一个 DataSource Bean。
    • 常用的 DataSource 实现有 HikariCP (默认)、Tomcat JDBC Pool、Commons DBCP2 等,HikariCP 因其高性能和低延迟被 Spring Boot 选为默认实现。
  3. 异常转换 (Exception Translation):

    • JDBC 原生的 SQLException 是检查型异常(Checked Exception),使用繁琐。
    • JdbcTemplate 会捕获 SQLException 并将其转换为 Spring 的运行时异常 (Runtime Exception) 体系,如 DataAccessException 的子类(BadSqlGrammarException, DuplicateKeyException, CannotGetJdbcConnectionException 等)。
    • 这使得开发者可以选择捕获特定异常或让其向上抛出,无需在方法签名中声明。
  4. 回调接口 (Callback Interfaces):

    • JdbcTemplate 大量使用模板方法模式和回调。
    • RowMapper<T>: 将 ResultSet 的每一行映射到一个 Java 对象。你需要实现这个接口的 mapRow(ResultSet rs, int rowNum) 方法。
    • ResultSetExtractor<T>: 用于处理整个 ResultSet,适用于返回复杂结果(如多行多列聚合)或需要自定义遍历逻辑的场景。实现 extractData(ResultSet rs) 方法。
    • PreparedStatementSetter: 用于设置 PreparedStatement 的参数。通常用 lambda 表达式或 new ArgPreparedStatementSetter(args) 实现。
    • StatementCallback / ConnectionCallback: 允许你直接操作 StatementConnection,用于执行复杂的或不支持的操作。
  5. NamedParameterJdbcTemplate

    • JdbcTemplate 的增强版,支持使用命名参数(如 :name, :age)代替 ? 占位符,使 SQL 更易读和维护。
    • 特别适合参数较多或需要重复使用同一参数的场景。
  6. 自动配置 (Auto-configuration):

    • 添加 spring-boot-starter-jdbc 后,Spring Boot 会自动:
      • 检测并配置 DataSource (如果 spring.datasource.* 配置存在)。
      • 创建 JdbcTemplate Bean 并注入配置好的 DataSource
      • 配置事务管理器 (DataSourceTransactionManager)。

二、操作步骤(非常详细)

步骤 1:添加依赖

  1. Maven (pom.xml):

    <dependencies>
        <!-- Spring Boot JDBC Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    
        <!-- 数据库驱动 (以 MySQL 8.x 为例) -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope> <!-- 通常 runtime 足够 -->
        </dependency>
    
        <!-- (可选) 如果需要使用 NamedParameterJdbcTemplate -->
        <!-- 它通常包含在 starter-jdbc 中,但有时需要显式添加 -->
        <!-- <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency> -->
    
        <!-- (可选) Lombok 简化 POJO 代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    • 注意: 替换 <version> 为你使用的 Spring Boot 版本(通常由 spring-boot-starter-parent 管理)。
  2. Gradle (build.gradle):

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        runtimeOnly 'mysql:mysql-connector-java'
        // optional('org.projectlombok:lombok')
    }
    

步骤 2:配置数据源

  1. application.properties 中配置:

    # 数据库连接 URL (MySQL 8.x)
    spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
    # 数据库用户名
    spring.datasource.username=myuser
    # 数据库密码
    spring.datasource.password=mypassword
    # (可选) 指定驱动类名 (Spring Boot 通常能自动推断)
    # spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    # (可选) HikariCP 连接池配置 (常用)
    # 最小空闲连接数
    spring.datasource.hikari.minimum-idle=5
    # 最大连接数
    spring.datasource.hikari.maximum-pool-size=20
    # 连接超时 (毫秒)
    spring.datasource.hikari.connection-timeout=20000
    # 空闲连接超时 (毫秒)
    spring.datasource.hikari.idle-timeout=300000
    # 连接最大存活时间 (毫秒)
    spring.datasource.hikari.max-lifetime=1200000
    # 测试连接的 SQL
    spring.datasource.hikari.data-source-properties.cachePrepStmts=true
    spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250
    spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
    spring.datasource.hikari.data-source-properties.useServerPrepStmts=true
    
    • 重要: useSSL=false 仅用于开发环境(如果数据库不支持 SSL 或证书问题)。生产环境应配置正确的 SSL。serverTimezone=UTC 解决时区问题。
  2. application.yml 中配置 (更清晰):

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
        username: myuser
        password: mypassword
        # driver-class-name: com.mysql.cj.jdbc.Driver
        hikari:
          minimum-idle: 5
          maximum-pool-size: 20
          connection-timeout: 20000
          idle-timeout: 300000
          max-lifetime: 1200000
          data-source-properties:
            cachePrepStmts: true
            prepStmtCacheSize: 250
            prepStmtCacheSqlLimit: 2048
            useServerPrepStmts: true
    

步骤 3:创建数据库表和实体类 (POJO)

  1. 创建数据库表 (MySQL):

    CREATE DATABASE IF NOT EXISTS mydb;
    USE mydb;
    
    CREATE TABLE IF NOT EXISTS users (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(50) UNIQUE NOT NULL,
        email VARCHAR(100) NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
  2. 创建实体类 User.java (src/main/java/com/example/demo/entity/User.java):

    package com.example.demo.entity;
    
    import lombok.Data; // 如果使用 Lombok
    
    // @Data // Lombok: 自动生成 getter, setter, toString, equals, hashCode
    public class User {
        private Long id;
        private String username;
        private String email;
        private java.sql.Timestamp createdAt;
    
        // 如果不使用 Lombok,手动添加 getter 和 setter
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public java.sql.Timestamp getCreatedAt() {
            return createdAt;
        }
    
        public void setCreatedAt(java.sql.Timestamp createdAt) {
            this.createdAt = createdAt;
        }
    
        // toString, equals, hashCode (可选)
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", email='" + email + '\'' +
                    ", createdAt=" + createdAt +
                    '}';
        }
    }
    

步骤 4:创建 Repository (DAO) 层

  1. 创建接口 UserRepository.java (src/main/java/com/example/demo/repository/UserRepository.java):

    package com.example.demo.repository;
    
    import com.example.demo.entity.User;
    import java.util.List;
    import java.util.Optional;
    
    public interface UserRepository {
        User save(User user); // 创建或更新
        Optional<User> findById(Long id);
        List<User> findAll();
        List<User> findByUsernameContaining(String username); // 模糊查询
        int update(User user); // 更新
        int deleteById(Long id); // 删除
    }
    
  2. 创建实现类 UserRepositoryImpl.java (src/main/java/com/example/demo/repository/UserRepositoryImpl.java):

    package com.example.demo.repository;
    
    import com.example.demo.entity.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.BeanPropertyRowMapper; // 自动映射列到属性
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    
    @Repository // 标记为 Spring Bean, 并参与组件扫描
    public class UserRepositoryImpl implements UserRepository {
    
        private final JdbcTemplate jdbcTemplate;
        private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; // 可选,用于命名参数
    
        // 构造函数注入 (推荐)
        @Autowired
        public UserRepositoryImpl(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
            this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
        }
    
        // RowMapper 实现 (内部类或 Lambda)
        private static final RowMapper<User> USER_ROW_MAPPER = new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User user = new User();
                user.setId(rs.getLong("id"));
                user.setUsername(rs.getString("username"));
                user.setEmail(rs.getString("email"));
                user.setCreatedAt(rs.getTimestamp("created_at"));
                return user;
            }
        };
    
        // 或者使用 Lambda (更简洁)
        // private static final RowMapper<User> USER_ROW_MAPPER = (rs, rowNum) -> {
        //     User user = new User();
        //     user.setId(rs.getLong("id"));
        //     user.setUsername(rs.getString("username"));
        //     user.setEmail(rs.getString("email"));
        //     user.setCreatedAt(rs.getTimestamp("created_at"));
        //     return user;
        // };
    
        // 或者使用 Spring 的 BeanPropertyRowMapper (如果列名和属性名匹配,且类型兼容)
        // private static final BeanPropertyRowMapper<User> USER_ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class);
        // 注意:数据库列名通常是下划线分隔 (created_at),Java 属性是驼峰 (createdAt)。
        // BeanPropertyRowMapper 默认支持这种映射 (created_at -> createdAt)。
    
        @Override
        @Transactional // 对于写操作,建议加上事务注解
        public User save(User user) {
            if (user.getId() == null) {
                // 插入新用户
                String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
                // 使用 KeyHolder 获取自增主键
                KeyHolder keyHolder = new GeneratedKeyHolder();
                jdbcTemplate.update(connection -> {
                    PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                    ps.setString(1, user.getUsername());
                    ps.setString(2, user.getEmail());
                    return ps;
                }, keyHolder);
    
                // 设置生成的 ID
                user.setId(keyHolder.getKey().longValue());
            } else {
                // 更新现有用户
                String sql = "UPDATE users SET username = ?, email = ? WHERE id = ?";
                int updated = jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), user.getId());
                if (updated == 0) {
                    throw new RuntimeException("User not found for update with id: " + user.getId());
                }
            }
            return user;
        }
    
        @Override
        public Optional<User> findById(Long id) {
            String sql = "SELECT id, username, email, created_at FROM users WHERE id = ?";
            try {
                User user = jdbcTemplate.queryForObject(sql, USER_ROW_MAPPER, id);
                return Optional.of(user);
            } catch (EmptyResultDataAccessException e) {
                return Optional.empty(); // 查询不到结果
            }
        }
    
        @Override
        public List<User> findAll() {
            String sql = "SELECT id, username, email, created_at FROM users";
            return jdbcTemplate.query(sql, USER_ROW_MAPPER);
            // 或者使用 BeanPropertyRowMapper
            // return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        }
    
        @Override
        public List<User> findByUsernameContaining(String username) {
            // 使用 ? 占位符
            String sql = "SELECT id, username, email, created_at FROM users WHERE username LIKE ?";
            // 注意:LIKE 需要通配符 %,这里在参数中添加
            return jdbcTemplate.query(sql, USER_ROW_MAPPER, "%" + username + "%");
        }
    
        // 演示 NamedParameterJdbcTemplate
        public List<User> findByUsernameContainingNamed(String username) {
            String sql = "SELECT id, username, email, created_at FROM users WHERE username LIKE :username";
            Map<String, Object> params = new HashMap<>();
            params.put("username", "%" + username + "%");
            return namedParameterJdbcTemplate.query(sql, params, USER_ROW_MAPPER);
        }
    
        @Override
        @Transactional
        public int update(User user) {
            String sql = "UPDATE users SET username = ?, email = ? WHERE id = ?";
            int updated = jdbcTemplate.update(sql, user.getUsername(), user.getEmail(), user.getId());
            if (updated == 0) {
                throw new RuntimeException("User not found for update with id: " + user.getId());
            }
            return updated;
        }
    
        @Override
        @Transactional
        public int deleteById(Long id) {
            String sql = "DELETE FROM users WHERE id = ?";
            return jdbcTemplate.update(sql, id);
        }
    }
    

步骤 5:创建 Service 层(可选但推荐)

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true) // 读操作,只读事务
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    @Transactional(readOnly = true)
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @Transactional // 写操作
    public User createUser(User user) {
        // 可以添加业务逻辑校验
        if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
            throw new IllegalArgumentException("Username cannot be empty");
        }
        return userRepository.save(user);
    }

    @Transactional
    public User updateUser(User user) {
        return userRepository.save(user); // save 方法内部会判断是 insert 还是 update
    }

    @Transactional
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

步骤 6:创建 Controller 层

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        Optional<User> user = userService.getUserById(id);
        return user.map(ResponseEntity::ok)
                   .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        User savedUser = userService.createUser(user);
        return ResponseEntity.ok(savedUser);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id); // 确保 ID 正确
        User updatedUser = userService.updateUser(user);
        return ResponseEntity.ok(updatedUser);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

步骤 7:启动应用并测试

  1. 确保数据库 mydb 已创建,users 表已存在。
  2. 运行 Spring Boot 应用主类。
  3. 使用工具测试 API:
    • 创建用户: POST http://localhost:8080/api/users
      {
          "username": "john_doe",
          "email": "john@example.com"
      }
      
    • 获取所有用户: GET http://localhost:8080/api/users
    • 获取单个用户: GET http://localhost:8080/api/users/1
    • 更新用户: PUT http://localhost:8080/api/users/1
      {
          "id": 1,
          "username": "john_updated",
          "email": "john.updated@example.com"
      }
      
    • 删除用户: DELETE http://localhost:8080/api/users/1

三、常见错误

  1. ClassNotFoundException / No suitable driver found:

    • 原因: 数据库驱动 JAR 未正确添加到 classpath。
    • 解决: 检查 pom.xml/build.gradle 依赖是否正确,版本是否兼容,Maven/Gradle 是否成功下载。
  2. CannotGetJdbcConnectionException:

    • 原因: 数据库连接失败。可能是 URL 错误、用户名/密码错误、数据库服务未启动、网络问题、防火墙阻止、连接池耗尽。
    • 解决: 检查 application.properties 中的 url, username, password;确认数据库服务运行;检查网络和防火墙;查看日志中的具体错误信息。
  3. BadSqlGrammarException:

    • 原因: SQL 语法错误、表名或列名不存在、参数数量不匹配。
    • 解决: 仔细检查 SQL 语句;确认数据库表结构;确保 ? 占位符数量与提供的参数数量一致。
  4. IncorrectResultSizeDataAccessException:

    • 原因: 使用 queryForObject 时,预期返回一行,但实际返回了零行或多行。
    • 解决: 确保查询条件能唯一确定一行(如主键查询);如果可能返回多行,使用 query 方法;如果可能返回零行,用 try-catch (EmptyResultDataAccessException) 包裹或使用 Optional 处理。
  5. DataIntegrityViolationException (如 DuplicateKeyException):

    • 原因: 违反了数据库约束,如主键冲突、唯一索引冲突、外键约束、非空约束。
    • 解决: 检查插入/更新的数据是否符合约束;在业务逻辑中提前校验。
  6. InvalidDataAccessApiUsageException:

    • 原因: API 使用不当,如对 ResultSet 进行了无效操作(如在 next() 之前调用 getString),或参数类型不匹配。
    • 解决: 检查 RowMapperResultSetExtractor 的实现逻辑;确保 PreparedStatement 参数类型与数据库列类型兼容。
  7. NullPointerException in RowMapper:

    • 原因: ResultSet 中的列值为 NULL,但尝试将其赋值给基本类型(如 int, long)或未处理 NULL 的包装类型。
    • 解决: 使用包装类型(Integer, Long);在 RowMapper 中使用 rs.getObject("column") 或检查 rs.wasNull();使用 BeanPropertyRowMapper 通常能较好处理 NULL 到包装类型的映射。
  8. NamedParameterJdbcTemplate 参数未绑定:

    • 原因: SQL 中的命名参数(:param)在参数 Map 中找不到对应的键。
    • 解决: 检查参数 Map 的键名是否与 SQL 中的参数名完全一致(包括 :)。

四、注意事项

  1. JdbcTemplate 是线程安全的: 一个 JdbcTemplate 实例可以被多个 DAO 共享,因为它不维护任何会话状态。
  2. DataSource 配置是关键: 确保 spring.datasource.* 配置正确无误,特别是 url
  3. 事务管理: @Transactional 注解默认只对 RuntimeException 及其子类回滚。如果方法抛出检查型异常(Checked Exception),事务不会自动回滚,除非显式声明 @Transactional(rollbackFor = Exception.class)。对于写操作(增删改),强烈建议在 Service 层使用 @Transactional
  4. 资源管理: JdbcTemplate 会自动管理 Connection, Statement, ResultSet 的获取和关闭,你不需要手动关闭它们
  5. 异常处理: 理解 Spring 的 DataAccessException 体系,根据需要捕获特定异常进行处理或记录日志。
  6. SQL 注入: 绝对不要使用字符串拼接来构建 SQL 语句!始终使用 ? 占位符(JdbcTemplate)或命名参数(NamedParameterJdbcTemplate)来传递参数,这是防止 SQL 注入的根本方法。
  7. RowMapper vs ResultSetExtractor
    • RowMapper 处理结果集中的每一行JdbcTemplate 会为你遍历 ResultSet 并调用 mapRow
    • ResultSetExtractor 处理整个结果集,你需要自己编写遍历 ResultSet 的逻辑。
  8. queryForObject 的陷阱: 它要求结果集恰好一行。返回零行抛 EmptyResultDataAccessException,返回多行抛 IncorrectResultSizeDataAccessException。使用时需谨慎。
  9. BeanPropertyRowMapper 的映射规则: 它默认将数据库的下划线命名(user_name)映射到 Java 的驼峰命名(userName)。确保列名和属性名遵循此约定,或自定义 RowMapper
  10. 性能: 对于大量数据的查询,考虑分页(使用 LIMIT/OFFSET 或数据库特定的分页语法)和流式处理(JdbcTemplate 本身不直接支持,但可通过 ConnectionCallback 实现)。

五、使用技巧

  1. 使用 BeanPropertyRowMapper 当实体类属性名与数据库列名(遵循下划线转驼峰规则)匹配时,避免手写 RowMapper

    List<User> users = jdbcTemplate.query("SELECT * FROM users", 
                                         BeanPropertyRowMapper.newInstance(User.class));
    
  2. 使用 NamedParameterJdbcTemplate 当 SQL 复杂或参数多时,可读性更好。

    String sql = "UPDATE users SET username = :username, email = :email WHERE id = :id";
    Map<String, Object> params = Map.of("username", user.getUsername(), 
                                        "email", user.getEmail(), 
                                        "id", user.getId());
    namedParameterJdbcTemplate.update(sql, params);
    
  3. 使用 MapSqlParameterSource 构建命名参数更方便。

    String sql = "INSERT INTO users (username, email) VALUES (:username, :email)";
    SqlParameterSource paramSource = new MapSqlParameterSource()
        .addValue("username", user.getUsername())
        .addValue("email", user.getEmail());
    namedParameterJdbcTemplate.update(sql, paramSource);
    
  4. 批量操作 (batchUpdate):

    • JdbcTemplate.batchUpdate(String sql, BatchPreparedStatementSetter setter):
      String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
      jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
          @Override
          public void setValues(PreparedStatement ps, int i) throws SQLException {
              User user = users.get(i);
              ps.setString(1, user.getUsername());
              ps.setString(2, user.getEmail());
          }
          @Override
          public int getBatchSize() {
              return users.size();
          }
      });
      
    • NamedParameterJdbcTemplate.batchUpdate(String sql, SqlParameterSource[] batchArgs):
      String sql = "INSERT INTO users (username, email) VALUES (:username, :email)";
      SqlParameterSource[] batchArgs = users.stream()
          .map(user -> new MapSqlParameterSource("username", user.getUsername())
                         .addValue("email", user.getEmail()))
          .toArray(SqlParameterSource[]::new);
      namedParameterJdbcTemplate.batchUpdate(sql, batchArgs);
      
  5. 调用存储过程 (CallableStatementCreator):

    String call = "{call get_user_stats(?, ?)}"; // 假设过程有两个参数
    Map<String, Object> result = jdbcTemplate.call(
        new CallableStatementCreator() {
            @Override
            public CallableStatement createCallableStatement(Connection con) throws SQLException {
                CallableStatement cs = con.prepareCall(call);
                cs.setLong(1, userId);
                cs.registerOutParameter(2, Types.INTEGER); // 注册输出参数
                return cs;
            }
        },
        Collections.singletonList("status") // 输出参数名列表
    );
    Integer status = (Integer) result.get("status");
    
  6. 获取自增主键 (KeyHolder):save 方法示例所示,使用 GeneratedKeyHolder

  7. queryForList / queryForObject 的便捷方法:

    • jdbcTemplate.queryForList("SELECT username FROM users", String.class); // 返回 List
    • jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class); // 返回单个值

六、最佳实践

  1. 分层架构: 遵循 Controller -> Service -> Repository 的分层,将数据访问逻辑集中在 Repository 层。
  2. 使用 @Repository 将 DAO 类标记为 @Repository,它不仅是 @Component,还提供了额外的异常翻译支持。
  3. 构造函数注入: 优先使用构造函数注入 JdbcTemplateDataSource,使代码更易于测试。
  4. @Transactional 在 Service 层: 将事务边界定义在 Service 层的方法上,保证业务操作的原子性。
  5. 防御性编程: 在 Service 层进行参数校验和业务规则校验。
  6. 使用占位符: 始终使用 ? 或命名参数,杜绝 SQL 拼接。
  7. 选择合适的 RowMapper 优先考虑 BeanPropertyRowMapper,复杂映射或性能要求高时手写 RowMapper
  8. 处理 NULL 值:RowMapper 中正确处理数据库 NULL 值。
  9. 日志记录: 记录关键的数据库操作和异常,便于排查问题。
  10. 单元测试: 使用 @DataJpaTest (虽然名字是 JPA,但也支持 JDBC) 或 @SpringBootTest 配合 @SqlTestEntityManagerJdbcTemplate 本身进行集成测试。使用 H2 内存数据库进行快速测试。

七、性能优化

  1. 连接池配置:

    • HikariCP: 合理配置 minimum-idle, maximum-pool-size, connection-timeout, idle-timeout, max-lifetime。开启预处理语句缓存 (cachePrepStmts=true, prepStmtCacheSize, prepStmtCacheSqlLimit, useServerPrepStmts=true)。
    • 监控: 利用 HikariCP 提供的指标(通过 Micrometer)监控连接池状态(活跃连接、空闲连接、等待线程等)。
  2. SQL 优化:

    • 索引: 为经常查询的列(特别是 WHERE, JOIN, ORDER BY 子句中的列)创建合适的索引。
    • 避免 SELECT *: 只查询需要的列。
    • 分页: 对大结果集使用分页查询。
    • 避免 N+1 查询: 在 Repository 中使用 JOIN 一次性获取关联数据,而不是在循环中多次查询。
  3. 批量操作: 对于大量数据的插入、更新或删除,使用 batchUpdate 显著减少网络往返次数。

  4. 缓存:

    • 应用层缓存: 对于读多写少、不常变的数据,使用 @Cacheable (配合 Redis, Caffeine 等) 缓存查询结果,减少数据库访问。
    • 数据库缓存: 利用数据库自身的查询缓存(如果支持且有效)。
  5. PreparedStatement 重用: JdbcTemplate 内部会重用 PreparedStatement(如果连接池和数据库驱动支持预处理语句缓存),确保 SQL 字符串一致。

  6. 减少对象创建:RowMapper 中避免不必要的对象创建。对于简单查询,考虑返回 Map<String, Object> 或基本类型列表。

  7. 异步处理: 对于耗时较长的数据库操作(如大数据量导出),考虑使用 @Async 在后台线程执行,避免阻塞 Web 请求线程。

  8. 监控与分析:

    • 使用 Spring Boot Actuator 的 /actuator/metrics/jdbc.connections.* 监控连接池。
    • 使用 /actuator/metrics/jdbc.* 监控 JDBC 操作的执行时间。
    • 使用 APM 工具(如 SkyWalking, Zipkin)追踪 SQL 执行耗时。
    • 开启数据库慢查询日志进行分析。

总结: JdbcTemplate 是 Spring Boot 中进行 JDBC 操作的坚实基础,它极大地简化了数据库交互。通过 spring-boot-starter-jdbc 快速集成,利用 DataSource 自动配置和 JdbcTemplate 的便捷方法,结合 RowMapper 处理结果映射,可以高效地完成 CRUD 操作。遵循分层、事务管理、使用占位符、合理配置连接池等最佳实践,并注意常见错误,能够构建出健壮、高效的数据访问层。对于更复杂的场景,NamedParameterJdbcTemplate 和批量操作提供了有力支持。