⚡ Spring JDBC 完整体系 · 第 6 讲 · JdbcTemplate API 精讲(上)
⚡ Spring JDBC 完整体系 · 第 6 讲 · JdbcTemplate API 精讲(上)
相关开源地址:
- 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
- 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
- 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
- 实战案例(本集源码):https://gitee.com/gao_zhenzhong/simple-dao-demo
📋 前置知识
| ✅ 你只需要会 | ❌ 你完全不需要会的 |
|---|---|
| Java 基础 + Spring Boot 基本用法 | Spring Boot 自动配置源码细节 |
| 第 5 讲:Spring Boot Starter JDBC 自动配置 | MyBatis / Hibernate 任何知识 |
| 基础 SQL(INSERT、UPDATE、DELETE、SELECT) | 连接池调优细节 |
📚 全套教程总览(20 讲从零到架构师)
| 篇章 | 涵盖讲次 | 核心目标 |
|---|---|---|
| 开篇筑基 | 第 1-3 讲 | 认知梳理,建立 Spring JDBC 的底层坐标系 |
| API 精讲 | 第 4-7 讲 | 全面掌握 JdbcTemplate + 事务体系 |
| 工具封装 | 第 8-9 讲 | 手搓条件工具 + 代码生成器,从"使用者"变"创造者" |
| 认知升华 | 第 10 讲 | 模板确定性 vs AI 不确定性,建立技术判断力 |
| 高阶扩展 | 第 11-17 讲 | 多数据源、分库分表、分布式事务 |
| 底层观测 | 第 18-19 讲 | SQL 拦截、性能监控、连接池调优 |
| 终极收尾 | 第 20 讲 | 全链路复盘 + 手搓持久层框架 |
一、开篇 · API 精讲正式起航
经过前 5 讲的铺垫,我们已经完成了从裸 JDBC → Spring 容器 → Spring JDBC 最小案例 → 连接池 + 事务管理器 → Spring Boot Starter 自动配置的完整演进。从这一讲开始,我们将正式进入 JdbcTemplate API 的全面精讲。
如果说前 5 讲是在搭建"开发环境",那从第 6 讲开始,就是真正的"使用兵器"阶段。本讲的目标很简单:把 JdbcTemplate 的增删改查 API 全部过一遍,让你看到原生 JDBC 那种冗长的
PreparedStatement+ResultSet循环,在 JdbcTemplate 里是如何被一行代码替代的。
二、项目结构与前置代码
2.1 Maven 依赖 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
第 5 讲已经详细拆解过
spring-boot-starter-jdbc的依赖聚合和自动配置机制,此处不再赘述,直接复用。
2.2 配置文件 application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
sql:
init:
schema-locations: classpath:schema.sql
mode: always
2.3 日志配置 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%p][%F:%L]:%m%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.jdbc.core.JdbcTemplate" level="debug" />
<logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="trace" />
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
JdbcTemplate的 DEBUG 日志用于观察 SQL 执行语句,StatementCreatorUtils的 TRACE 日志用于查看参数绑定细节,是调试 SQL 的标配组合。
2.4 建表脚本 schema.sql
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
user_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
user_name VARCHAR(50) COMMENT '姓名',
birthday DATE COMMENT '生日',
gender TINYINT COMMENT '性别'
);
2.5 Spring Boot 启动类 Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
三、POJO 实体类 User.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long userId; /* 主键 */
private String userName; /* 姓名 */
private LocalDate birthday; /* 生日 */
private Byte gender; /* 性别 */
}
⚠️ 注意字段命名:数据库字段是
user_id、user_name,实体类是userId、userName。BeanPropertyRowMapper会自动完成下划线转驼峰映射,无需手动配置。
四、核心认知 · Spring JDBC 的设计立场
在进入具体 API 之前,先建立一个关键认知:
Spring JDBC 不区分单表与联表。数据库能写什么 SQL,Spring JDBC 就能执行什么 SQL。
具体表现为三条原则:
| 原则 | 说明 |
|---|---|
| 不区分单表 / 联表 |
FROM 后面跟一张表还是十张表,JdbcTemplate 一视同仁 |
只认 ? 占位符 + 参数数组 |
参数传递只依赖占位符顺序,与 SQL 结构无关 |
| 增删改查皆可连表 |
UPDATE、DELETE 同样支持多表关联语法 |
单表只是联表的一种特殊情况。这种设计立场决定了 Spring JDBC 的定位——它不是 ORM 框架,而是一个 SQL 执行器,把 SQL 的完全控制权交给开发者。
五、辅助查询方法 query()
每个 demo 操作完成后,都需要一个查询方法来验证当前表数据:
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void query() {
String sql = "SELECT user_id, user_name, birthday, gender FROM sys_user WHERE user_id > ?";
List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), 0);
log.info("users={}", users);
}
}
BeanPropertyRowMapper 映射规则:
| 数据库字段 | 实体属性 | 映射方式 |
|---|---|---|
user_id |
userId |
下划线转驼峰 |
user_name |
userName |
下划线转驼峰 |
birthday |
birthday |
同名直接映射 |
gender |
gender |
同名直接映射 |
无需手动配置映射关系,也无需逐行 rs.getXxx() 取值,一行代码完成全部结果集映射。
六、demo01 · INSERT 硬编码
public void demo01() {
String sql = "INSERT INTO sys_user (user_name, birthday, gender) VALUES('张三', '2026-01-30', 1)";
jdbcTemplate.update(sql);
query();
}
特点:SQL 中直接写死值,参数无法复用,适用场景极其有限——仅适合一次性数据初始化。
[INFO] users=[User(userId=1, userName=张三, birthday=2026-01-30, gender=1)]
七、demo02 · INSERT 占位符 ?
public void demo02() {
User user = User.builder()
.userName("张三")
.birthday(LocalDate.now())
.gender((byte) 1)
.build();
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
user.getUserName(), user.getBirthday(), user.getGender()
);
query();
}
特点:SQL 与参数分离,预编译防止 SQL 注入,这是生产环境最推荐的写法。
[DEBUG] INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)
[TRACE] column index 1, parameter value [张三], value class [java.lang.String]
[TRACE] column index 2, parameter value [2026-07-27], value class [java.time.LocalDate]
[TRACE] column index 3, parameter value [1], value class [java.lang.Byte]
[INFO] users=[User(userId=1, userName=张三, birthday=2026-07-27, gender=1)]
八、demo03 · INSERT 数组传参
public void demo03() {
User user = User.builder()
.userName("张三")
.birthday(LocalDate.now())
.gender((byte) 1)
.build();
Object[] params = {user.getUserName(), user.getBirthday(), user.getGender()};
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
params
);
query();
}
特点:demo02 的数组版,参数统一管理,适合参数较多的场景。
[INFO] users=[User(userId=1, userName=张三, birthday=2026-07-27, gender=1)]
九、demo04 · UPDATE
public void demo04() {
// 先插入张三
User user = User.builder()
.userName("张三")
.birthday(LocalDate.now())
.gender((byte) 1)
.build();
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
user.getUserName(), user.getBirthday(), user.getGender()
);
// 再更新为李四
Object[] params = {"李四", LocalDate.now().plusDays(2), (byte) 0, 1L};
jdbcTemplate.update(
"UPDATE sys_user SET user_name = ?, birthday = ?, gender = ? WHERE user_id = ?",
params
);
query();
}
特点:先插入再更新,演示 UPDATE 操作。参数顺序必须与 SQL 中 ? 严格一一对应。
[INFO] users=[User(userId=1, userName=张三, birthday=2026-07-27, gender=1)]
[INFO] users=[User(userId=1, userName=李四, birthday=2026-07-29, gender=0)]
十、demo05 · DELETE
public void demo05() {
// 插入张三
Object[] params1 = {"张三", LocalDate.now(), (byte) 1};
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
params1
);
// 插入李四
Object[] params2 = {"李四", LocalDate.now().plusDays(2), (byte) 0};
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
params2
);
// 按 ID 批量删除
jdbcTemplate.update(
"DELETE FROM sys_user WHERE user_id IN (?, ?)",
1L, 2L
);
query();
}
特点:先插入两条数据,再按 ID 批量删除,覆盖完整生命周期。
[INFO] users=[User(userId=1, userName=张三, birthday=2026-07-27, gender=1),
User(userId=2, userName=李四, birthday=2026-07-29, gender=0)]
[INFO] users=[]
十一、五种 Demo 汇总对照
| Demo | 操作 | 参数传递方式 | 核心要点 |
|---|---|---|---|
| demo01 | INSERT | 硬编码 | 值直接写在 SQL 中,不推荐 |
| demo02 | INSERT | 占位符 ?
|
SQL 与参数分离,防注入,推荐 |
| demo03 | INSERT |
Object[] 数组 |
批量/多参场景适用 |
| demo04 | UPDATE | 占位符 ?
|
参数顺序必须与 SQL 中 ? 严格对应 |
| demo05 | DELETE | 占位符 ?
|
IN 查询同样支持占位符 |
十二、本讲核心总结
| 核心要点 | 说明 |
|---|---|
所有增删改都用 update() |
INSERT、UPDATE、DELETE 统一入口,无二义性 |
| 参数传递三种方式 | 硬编码 → 占位符 → 数组,推荐占位符 + 数组 |
结果映射用 BeanPropertyRowMapper |
一行搞定,下划线自动转驼峰 |
| 参数顺序敏感 | 参数数组顺序必须与 SQL 中 ? 一一对应 |
| Spring JDBC 不区分单表/联表 | 数据库能写的 SQL,JdbcTemplate 都能执行 |
💡 最佳实践:生产中杜绝硬编码传参,统一使用
?占位符 + 参数列表或Object[]数组。这不仅是安全性问题(防 SQL 注入),更是可维护性问题(SQL 与参数分离,逻辑清晰)。
十三、下集预告
第 7 讲将进入 NamedParameterJdbcTemplate(命名参数模板)——
告别 `?` 顺序记忆,用 `:name` 语义化传参
SQL 可读性大幅提升
批量操作与命名参数的结合
多参数场景下的最佳实践