⚡ Spring JDBC 完整体系 · 第 6 讲 · JdbcTemplate API 精讲(上)

⚡ Spring JDBC 完整体系 · 第 6 讲 · JdbcTemplate API 精讲(上)

相关开源地址

  1. 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
  2. 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
  3. 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
  4. 实战案例(本集源码):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_iduser_name,实体类是 userIduserNameBeanPropertyRowMapper 会自动完成下划线转驼峰映射,无需手动配置。

四、核心认知 · Spring JDBC 的设计立场

在进入具体 API 之前,先建立一个关键认知:

Spring JDBC 不区分单表与联表。数据库能写什么 SQL,Spring JDBC 就能执行什么 SQL。

具体表现为三条原则:

原则 说明
不区分单表 / 联表 FROM 后面跟一张表还是十张表,JdbcTemplate 一视同仁
只认 ? 占位符 + 参数数组 参数传递只依赖占位符顺序,与 SQL 结构无关
增删改查皆可连表 UPDATEDELETE 同样支持多表关联语法

单表只是联表的一种特殊情况。这种设计立场决定了 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 可读性大幅提升
批量操作与命名参数的结合
多参数场景下的最佳实践
© 版权声明

相关文章