一、核心概念
JdbcTemplate
是 Spring Framework 提供的一个核心类,用于简化 JDBC 操作,极大地减少了传统 JDBC 编程中大量的样板代码(如获取连接、创建语句、处理结果集、关闭资源、处理异常等)。在 Spring Boot 中,通过 spring-boot-starter-jdbc
起步依赖可以轻松集成和使用。
关键概念
JdbcTemplate
类:- 核心类,提供了执行 SQL 查询、更新、调用存储过程等方法。
- 它封装了
DataSource
的获取、连接管理、异常转换和资源清理。 - 方法命名清晰:
queryXxx
用于查询,update
用于增删改,execute
用于执行任意 SQL。
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 选为默认实现。
异常转换 (Exception Translation):
- JDBC 原生的
SQLException
是检查型异常(Checked Exception),使用繁琐。 JdbcTemplate
会捕获SQLException
并将其转换为 Spring 的运行时异常 (Runtime Exception) 体系,如DataAccessException
的子类(BadSqlGrammarException
,DuplicateKeyException
,CannotGetJdbcConnectionException
等)。- 这使得开发者可以选择捕获特定异常或让其向上抛出,无需在方法签名中声明。
- JDBC 原生的
回调接口 (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
: 允许你直接操作Statement
或Connection
,用于执行复杂的或不支持的操作。
NamedParameterJdbcTemplate
:JdbcTemplate
的增强版,支持使用命名参数(如:name
,:age
)代替?
占位符,使 SQL 更易读和维护。- 特别适合参数较多或需要重复使用同一参数的场景。
自动配置 (Auto-configuration):
- 添加
spring-boot-starter-jdbc
后,Spring Boot 会自动:- 检测并配置
DataSource
(如果spring.datasource.*
配置存在)。 - 创建
JdbcTemplate
Bean 并注入配置好的DataSource
。 - 配置事务管理器 (
DataSourceTransactionManager
)。
- 检测并配置
- 添加
二、操作步骤(非常详细)
步骤 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
管理)。
- 注意: 替换
Gradle (
build.gradle
):dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' runtimeOnly 'mysql:mysql-connector-java' // optional('org.projectlombok:lombok') }
步骤 2:配置数据源
在
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
解决时区问题。
- 重要:
在
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)
创建数据库表 (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 );
创建实体类
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) 层
创建接口
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); // 删除 }
创建实现类
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:启动应用并测试
- 确保数据库
mydb
已创建,users
表已存在。 - 运行 Spring Boot 应用主类。
- 使用工具测试 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
- 创建用户:
三、常见错误
ClassNotFoundException
/No suitable driver found
:- 原因: 数据库驱动 JAR 未正确添加到 classpath。
- 解决: 检查
pom.xml
/build.gradle
依赖是否正确,版本是否兼容,Maven/Gradle 是否成功下载。
CannotGetJdbcConnectionException
:- 原因: 数据库连接失败。可能是 URL 错误、用户名/密码错误、数据库服务未启动、网络问题、防火墙阻止、连接池耗尽。
- 解决: 检查
application.properties
中的url
,username
,password
;确认数据库服务运行;检查网络和防火墙;查看日志中的具体错误信息。
BadSqlGrammarException
:- 原因: SQL 语法错误、表名或列名不存在、参数数量不匹配。
- 解决: 仔细检查 SQL 语句;确认数据库表结构;确保
?
占位符数量与提供的参数数量一致。
IncorrectResultSizeDataAccessException
:- 原因: 使用
queryForObject
时,预期返回一行,但实际返回了零行或多行。 - 解决: 确保查询条件能唯一确定一行(如主键查询);如果可能返回多行,使用
query
方法;如果可能返回零行,用try-catch (EmptyResultDataAccessException)
包裹或使用Optional
处理。
- 原因: 使用
DataIntegrityViolationException
(如DuplicateKeyException
):- 原因: 违反了数据库约束,如主键冲突、唯一索引冲突、外键约束、非空约束。
- 解决: 检查插入/更新的数据是否符合约束;在业务逻辑中提前校验。
InvalidDataAccessApiUsageException
:- 原因: API 使用不当,如对
ResultSet
进行了无效操作(如在next()
之前调用getString
),或参数类型不匹配。 - 解决: 检查
RowMapper
或ResultSetExtractor
的实现逻辑;确保PreparedStatement
参数类型与数据库列类型兼容。
- 原因: API 使用不当,如对
NullPointerException
inRowMapper
:- 原因:
ResultSet
中的列值为NULL
,但尝试将其赋值给基本类型(如int
,long
)或未处理NULL
的包装类型。 - 解决: 使用包装类型(
Integer
,Long
);在RowMapper
中使用rs.getObject("column")
或检查rs.wasNull()
;使用BeanPropertyRowMapper
通常能较好处理NULL
到包装类型的映射。
- 原因:
NamedParameterJdbcTemplate
参数未绑定:- 原因: SQL 中的命名参数(
:param
)在参数 Map 中找不到对应的键。 - 解决: 检查参数 Map 的键名是否与 SQL 中的参数名完全一致(包括
:
)。
- 原因: SQL 中的命名参数(
四、注意事项
JdbcTemplate
是线程安全的: 一个JdbcTemplate
实例可以被多个 DAO 共享,因为它不维护任何会话状态。DataSource
配置是关键: 确保spring.datasource.*
配置正确无误,特别是url
。- 事务管理:
@Transactional
注解默认只对RuntimeException
及其子类回滚。如果方法抛出检查型异常(Checked Exception),事务不会自动回滚,除非显式声明@Transactional(rollbackFor = Exception.class)
。对于写操作(增删改),强烈建议在 Service 层使用@Transactional
。 - 资源管理:
JdbcTemplate
会自动管理Connection
,Statement
,ResultSet
的获取和关闭,你不需要手动关闭它们。 - 异常处理: 理解 Spring 的
DataAccessException
体系,根据需要捕获特定异常进行处理或记录日志。 - SQL 注入: 绝对不要使用字符串拼接来构建 SQL 语句!始终使用
?
占位符(JdbcTemplate
)或命名参数(NamedParameterJdbcTemplate
)来传递参数,这是防止 SQL 注入的根本方法。 RowMapper
vsResultSetExtractor
:- 用
RowMapper
处理结果集中的每一行,JdbcTemplate
会为你遍历ResultSet
并调用mapRow
。 - 用
ResultSetExtractor
处理整个结果集,你需要自己编写遍历ResultSet
的逻辑。
- 用
queryForObject
的陷阱: 它要求结果集恰好一行。返回零行抛EmptyResultDataAccessException
,返回多行抛IncorrectResultSizeDataAccessException
。使用时需谨慎。BeanPropertyRowMapper
的映射规则: 它默认将数据库的下划线命名(user_name
)映射到 Java 的驼峰命名(userName
)。确保列名和属性名遵循此约定,或自定义RowMapper
。- 性能: 对于大量数据的查询,考虑分页(使用
LIMIT
/OFFSET
或数据库特定的分页语法)和流式处理(JdbcTemplate
本身不直接支持,但可通过ConnectionCallback
实现)。
五、使用技巧
使用
BeanPropertyRowMapper
: 当实体类属性名与数据库列名(遵循下划线转驼峰规则)匹配时,避免手写RowMapper
。List<User> users = jdbcTemplate.query("SELECT * FROM users", BeanPropertyRowMapper.newInstance(User.class));
使用
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);
使用
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);
批量操作 (
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);
调用存储过程 (
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");
获取自增主键 (
KeyHolder
): 如save
方法示例所示,使用GeneratedKeyHolder
。queryForList
/queryForObject
的便捷方法:jdbcTemplate.queryForList("SELECT username FROM users", String.class);
// 返回 ListjdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
// 返回单个值
六、最佳实践
- 分层架构: 遵循 Controller -> Service -> Repository 的分层,将数据访问逻辑集中在 Repository 层。
- 使用
@Repository
: 将 DAO 类标记为@Repository
,它不仅是@Component
,还提供了额外的异常翻译支持。 - 构造函数注入: 优先使用构造函数注入
JdbcTemplate
和DataSource
,使代码更易于测试。 @Transactional
在 Service 层: 将事务边界定义在 Service 层的方法上,保证业务操作的原子性。- 防御性编程: 在 Service 层进行参数校验和业务规则校验。
- 使用占位符: 始终使用
?
或命名参数,杜绝 SQL 拼接。 - 选择合适的
RowMapper
: 优先考虑BeanPropertyRowMapper
,复杂映射或性能要求高时手写RowMapper
。 - 处理
NULL
值: 在RowMapper
中正确处理数据库NULL
值。 - 日志记录: 记录关键的数据库操作和异常,便于排查问题。
- 单元测试: 使用
@DataJpaTest
(虽然名字是 JPA,但也支持 JDBC) 或@SpringBootTest
配合@Sql
、TestEntityManager
或JdbcTemplate
本身进行集成测试。使用 H2 内存数据库进行快速测试。
七、性能优化
连接池配置:
- HikariCP: 合理配置
minimum-idle
,maximum-pool-size
,connection-timeout
,idle-timeout
,max-lifetime
。开启预处理语句缓存 (cachePrepStmts=true
,prepStmtCacheSize
,prepStmtCacheSqlLimit
,useServerPrepStmts=true
)。 - 监控: 利用 HikariCP 提供的指标(通过 Micrometer)监控连接池状态(活跃连接、空闲连接、等待线程等)。
- HikariCP: 合理配置
SQL 优化:
- 索引: 为经常查询的列(特别是
WHERE
,JOIN
,ORDER BY
子句中的列)创建合适的索引。 - 避免
SELECT *
: 只查询需要的列。 - 分页: 对大结果集使用分页查询。
- 避免 N+1 查询: 在 Repository 中使用
JOIN
一次性获取关联数据,而不是在循环中多次查询。
- 索引: 为经常查询的列(特别是
批量操作: 对于大量数据的插入、更新或删除,使用
batchUpdate
显著减少网络往返次数。缓存:
- 应用层缓存: 对于读多写少、不常变的数据,使用
@Cacheable
(配合 Redis, Caffeine 等) 缓存查询结果,减少数据库访问。 - 数据库缓存: 利用数据库自身的查询缓存(如果支持且有效)。
- 应用层缓存: 对于读多写少、不常变的数据,使用
PreparedStatement
重用:JdbcTemplate
内部会重用PreparedStatement
(如果连接池和数据库驱动支持预处理语句缓存),确保 SQL 字符串一致。减少对象创建: 在
RowMapper
中避免不必要的对象创建。对于简单查询,考虑返回Map<String, Object>
或基本类型列表。异步处理: 对于耗时较长的数据库操作(如大数据量导出),考虑使用
@Async
在后台线程执行,避免阻塞 Web 请求线程。监控与分析:
- 使用 Spring Boot Actuator 的
/actuator/metrics/jdbc.connections.*
监控连接池。 - 使用
/actuator/metrics/jdbc.*
监控 JDBC 操作的执行时间。 - 使用 APM 工具(如 SkyWalking, Zipkin)追踪 SQL 执行耗时。
- 开启数据库慢查询日志进行分析。
- 使用 Spring Boot Actuator 的
总结: JdbcTemplate
是 Spring Boot 中进行 JDBC 操作的坚实基础,它极大地简化了数据库交互。通过 spring-boot-starter-jdbc
快速集成,利用 DataSource
自动配置和 JdbcTemplate
的便捷方法,结合 RowMapper
处理结果映射,可以高效地完成 CRUD 操作。遵循分层、事务管理、使用占位符、合理配置连接池等最佳实践,并注意常见错误,能够构建出健壮、高效的数据访问层。对于更复杂的场景,NamedParameterJdbcTemplate
和批量操作提供了有力支持。