⚡ Spring JDBC 完整体系 · 第 7 讲 · NamedParameterJdbcTemplate 命名参数模板
相关开源地址:
📋 前置知识
| ✅ 你只需要会 | ❌ 你完全不需要会的 |
|---|---|
| Java 基础 + Spring Boot 基本用法 | Spring 自动配置源码细节 |
第 6 讲:JdbcTemplate 的 ? 占位符传参 | 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 讲 | 全链路复盘 + 手搓持久层框架 |
一、开篇 · 上一讲的痛点:记不住 ? 的顺序
第 6 讲我们用 JdbcTemplate 的 ? 占位符完成了增删改查。但有一个问题随着参数增多会越来越明显——顺序记忆。
// 参数少的时候还好
jdbcTemplate.update(
"INSERT INTO sys_user (user_name, birthday, gender) VALUES(?, ?, ?)",
userName, birthday, gender
);
// 参数一多(5个、8个、10个),顺序记忆就非常痛苦
jdbcTemplate.update(
"UPDATE sys_user SET user_name=?, birthday=?, gender=?, phone=?, email=?, address=? WHERE user_id=?",
userName, birthday, gender, phone, email, address, userId
);
问题本质:? 占位符只依赖位置,不携带任何语义信息。开发者必须人工数出 SQL 中 ? 的顺序,再按完全相同的顺序排列参数。数错一位,轻则数据错乱,重则类型转换异常。
这一讲引入 NamedParameterJdbcTemplate——用 :name 命名参数替代 ?,彻底告别顺序记忆。
二、依赖与配置
2.1 pom.xml · 无需新增依赖
NamedParameterJdbcTemplate 与 JdbcTemplate 同属 spring-jdbc 模块:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
已在第 5 讲引入,无需额外添加任何依赖。
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 schema.sql
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
user_id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_name VARCHAR(50),
birthday DATE,
gender TINYINT
);
2.4 User.java · 实体类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long userId;
private String userName;
private LocalDate birthday;
private Byte gender;
}
三、NamedParameterJdbcTemplate 的自动配置
Spring Boot 已预置 NamedParameterJdbcTemplate 的自动配置,直接在 DAO 中注入即可使用,无需手动声明任何 Bean:
@Repository
public class UserDao {
@Autowired
private NamedParameterJdbcTemplate namedJdbc; // 直接注入,开箱即用
}
自动配置原理与第 5 讲的 JdbcTemplateAutoConfiguration 一致——Spring Boot 在 org.springframework.boot.autoconfigure.jdbc 包中预置了 JdbcTemplateAutoConfiguration,它会同时创建 JdbcTemplate 和 NamedParameterJdbcTemplate 两个 Bean,前提是 classpath 中存在对应的类且开发者没有手动覆盖。
四、案例1 · INSERT 命名参数
public void demo01() {
String sql = "INSERT INTO sys_user (user_name, birthday, gender) " +
"VALUES (:userName, :birthday, :gender)";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("userName", "张三")
.addValue("birthday", LocalDate.now())
.addValue("gender", (byte) 1);
namedJdbc.update(sql, params);
}
核心变化:
| 对比项 | JdbcTemplate | NamedParameterJdbcTemplate |
|---|---|---|
| SQL 写法 | VALUES(?, ?, ?) | VALUES(:userName, :birthday, :gender) |
| 参数绑定 | 按 ? 顺序传入 | 通过 MapSqlParameterSource 按名称绑定 |
| 顺序要求 | 必须严格对应 | 无关顺序 |
MapSqlParameterSource 是 Spring 提供的 SqlParameterSource 接口实现类,底层基于 LinkedHashMap 存储键值对。它支持链式调用 .addValue(key, value),每调用一次向内部 Map 存入一个键值对,最终在 NamedParameterJdbcTemplate 执行时被解析为 PreparedStatement 的 ? 参数数组。参数顺序由 NamedParameterJdbcTemplate 在解析 SQL 时根据 :name 的出现顺序自动决定。
参数绑定顺序不再重要,这是
NamedParameterJdbcTemplate与JdbcTemplate最本质的区别。
执行日志:
[DEBUG] Executing prepared SQL update [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-08-03], value class [java.time.LocalDate]
[TRACE] column index 3, parameter value [1], value class [java.lang.Byte]
注意:底层最终执行的仍然是 PreparedStatement 的 ? 占位符。NamedParameterJdbcTemplate 做了两件事:
- 将 SQL 中的
:name替换为? - 根据解析出的名称顺序,从
MapSqlParameterSource中提取对应的值,组装成参数数组
所以命名参数本质上是 编译期的语法糖,运行期仍然是标准的 ? 占位符。
五、案例2 · 自增主键回填
插入数据后,经常需要获取数据库自动生成的主键 ID。NamedParameterJdbcTemplate 配合 KeyHolder 实现主键回填:
public void demo02() {
String sql = "INSERT INTO sys_user (user_name, birthday, gender) " +
"VALUES (:userName, :birthday, :gender)";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("userName", "李四")
.addValue("birthday", LocalDate.now())
.addValue("gender", (byte) 0);
KeyHolder keyHolder = new GeneratedKeyHolder();
namedJdbc.update(sql, params, keyHolder, new String[]{"user_id"});
Long generatedId = keyHolder.getKey().longValue();
log.info("生成的ID:{}", generatedId);
}
关键点:
| 要素 | 说明 |
|---|---|
KeyHolder | Spring 提供的自增主键持有器接口 |
GeneratedKeyHolder | KeyHolder 的标准实现 |
new String[]{"user_id"} | 指定要返回的列名(自增主键字段) |
keyHolder.getKey() | 获取生成的第一个主键值,返回 Number 类型 |
KeyHolder 的底层机制依赖于 JDBC 驱动的 Statement.getGeneratedKeys() 方法。NamedParameterJdbcTemplate 在执行 update(sql, params, keyHolder, keyColumnNames) 时,会调用 PreparedStatement 的 prepareStatement(sql, keyColumnNames) 重载,该重载会向数据库发送“返回指定列”的指令,执行完成后通过 getGeneratedKeys() 获取 ResultSet,并将结果填充到 KeyHolder 中。如果自增主键是 BIGINT 类型,getKey().longValue() 可安全获取数值。
执行日志:
[INFO] 生成的ID:1
六、案例3 · 批量插入
插入多条记录时,逐条发送 SQL 会产生大量网络往返(Round-Trip)。NamedParameterJdbcTemplate 的 batchUpdate 支持一次发送多条记录:
public void demo03() {
String sql = "INSERT INTO sys_user (user_name, birthday, gender) " +
"VALUES (:userName, :birthday, :gender)";
SqlParameterSource[] batchParams = new SqlParameterSource[] {
new MapSqlParameterSource()
.addValue("userName", "王五")
.addValue("birthday", LocalDate.now())
.addValue("gender", (byte) 1),
new MapSqlParameterSource()
.addValue("userName", "赵六")
.addValue("birthday", LocalDate.now().plusDays(1))
.addValue("gender", (byte) 0)
};
namedJdbc.batchUpdate(sql, batchParams);
}
核心要点:
| 要素 | 说明 |
|---|---|
SqlParameterSource[] | 参数源数组,每个元素对应一条记录 |
MapSqlParameterSource | SqlParameterSource 的实现类,基于 Map 存储 |
| 一次发送 | 多条记录打包成一条批量 SQL 发送,减少网络往返 |
batchUpdate 的执行机制是:将 SqlParameterSource[] 中每条记录的参数依次绑定到同一个 SQL 模板上,然后以 JDBC 的 PreparedStatement.addBatch() + executeBatch() 方式一次性提交给数据库。相比单条逐次执行,批量操作在网络 I/O 次数上减少了 N-1 次(N 为记录数),显著提升了写入吞吐量。
执行日志:
[DEBUG] Executing batch update for 2 statements
七、案例4 · 查询 · 命名参数同样适用
NamedParameterJdbcTemplate 不仅支持增删改,查询同样支持命名参数:
public void demo04() {
String sql = "SELECT user_id, user_name, birthday, gender " +
"FROM sys_user WHERE user_id > :minId AND gender = :gender";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("minId", 0L)
.addValue("gender", (byte) 1);
List<User> users = namedJdbc.query(
sql,
params,
new BeanPropertyRowMapper<>(User.class)
);
log.info("users={}", users);
}
八、NamedParameterJdbcTemplate vs JdbcTemplate
| 维度 | JdbcTemplate | NamedParameterJdbcTemplate |
|---|---|---|
| 参数占位符 | ? | :name |
| 参数绑定顺序 | 必须严格对应 | 无关顺序 |
| SQL 可读性 | 参数多时较差 | 清晰,自带语义 |
| 参数绑定方式 | 直接传入或 Object[] | MapSqlParameterSource / Map / SqlParameterSource[] |
| 主键回填 | 支持 KeyHolder | 支持 KeyHolder |
| 批量操作 | batchUpdate(sql, List<Object[]>) | batchUpdate(sql, SqlParameterSource[]) |
| 适用场景 | 参数少(≤3个) | 参数多(≥3个)或需语义化参数名 |
| 依赖 | spring-jdbc | spring-jdbc(同模块) |
选型建议:
JdbcTemplate的?占位符适用于参数较少(通常 ≤ 3 个)的场景,此时顺序记忆成本低,代码更简洁。一旦参数数量增多,或 SQL 结构复杂(如动态条件拼接),NamedParameterJdbcTemplate的语义化参数名能显著提升代码可读性和可维护性。两者可以混用——同一个 DAO 中同时注入两个模板,根据场景灵活选择。
九、三种参数传递方式的演进路线
从第 1 讲的裸 JDBC 到本讲的命名参数,参数传递方式经历了清晰的演进:
| 讲次 | 方式 | 示例 | 特点 | 适用场景 |
|---|---|---|---|---|
| 第 1 讲 | SQL 硬编码 | VALUES('张三', 1) | 值直接写在 SQL 中,参数无法复用 | 数据初始化、种子数据 |
| 第 6 讲 | ? 占位符 | VALUES(?, ?) | SQL 与参数分离,顺序必须对应 | 参数少(≤3个)的通用场景 |
| 第 7 讲 | :name 命名参数 | VALUES(:name, :gender) | 参数名称化,顺序无关 | 参数多或需语义化参数名的场景 |
三种方式各有其适用边界,并非绝对替代关系。开发者在实际编码中应根据参数数量、SQL 复杂度、可维护性需求做出技术判断。
十、本讲核心总结
| 核心要点 | 说明 |
|---|---|
| 无需新增依赖 | NamedParameterJdbcTemplate 同属 spring-jdbc 模块 |
| Spring Boot 自动装配 | 直接 @Autowired 注入即可,无需手动配置 |
| 命名参数语法 | SQL 中用 :name 替代 ?,用 MapSqlParameterSource 绑定值 |
| 参数顺序无关 | 命名参数的核心价值——彻底告别数 ? 的烦恼 |
| 主键回填 | KeyHolder + namedJdbc.update(sql, params, keyHolder, keyColNames) |
| 批量操作 | SqlParameterSource[] + namedJdbc.batchUpdate(sql, batchParams) |
| 查询同样支持 | namedJdbc.query(sql, params, rowMapper) |
本质理解:
NamedParameterJdbcTemplate是JdbcTemplate的 语义化包装。它不替代JdbcTemplate,而是在其基础上增加了命名参数的解析能力。底层执行时,命名参数会被解析为?,最终仍然由PreparedStatement完成预编译和参数绑定。两者是增强关系,而非替代关系。
十一、下集预告
第 8 讲将进入 查询结果四种形态:
单行单列 → queryForObject
单行多列 → queryForObject + RowMapper
多行单列 → queryForList
多行多列 → query + RowMapper

163

被折叠的 条评论
为什么被折叠?



