⚡ Spring JDBC 完整体系 · 第 8 讲 · 查询四象限完备
相关开源地址:
📋 前置知识
| ✅ 你只需要会 | ❌ 你完全不需要会的 |
|---|---|
| Java 基础 + Spring Boot 基本用法 | Spring 自动配置源码细节 |
| 第 6-7 讲:JdbcTemplate 增删改 + 命名参数 | MyBatis / Hibernate 任何知识 |
| 基础 SQL(SELECT、聚合函数) | 连接池调优细节 |
📚 全套教程总览(20 讲从零到架构师)
| 篇章 | 涵盖讲次 | 核心目标 |
|---|---|---|
| 开篇筑基 | 第 1-3 讲 | 认知梳理,建立 Spring JDBC 的底层坐标系 |
| API 精讲 | 第 4-7 讲 | 全面掌握 JdbcTemplate + 事务体系 |
| 工具封装 | 第 8-9 讲 | 手搓条件工具 + 代码生成器,从"使用者"变"创造者" |
| 认知升华 | 第 10 讲 | 模板确定性 vs AI 不确定性,建立技术判断力 |
| 高阶扩展 | 第 11-17 讲 | 多数据源、分库分表、分布式事务 |
| 底层观测 | 第 18-19 讲 | SQL 拦截、性能监控、连接池调优 |
| 终极收尾 | 第 20 讲 | 全链路复盘 + 手搓持久层框架 |
一、开篇 · 查询的四象限完备
前两讲我们分别完成了 JdbcTemplate 的增删改操作(第 6 讲)和 NamedParameterJdbcTemplate 的命名参数(第 7 讲)。这一讲回归查询------SELECT 语句的结果形态分析。
无论 SQL 写得多复杂------多表 JOIN、子查询、聚合函数、窗口函数------最终落到 Java 代码里,查询结果只有四种形态。这就是所谓的四象限完备。
具体是哪四种?我们用一张图说清楚:
| 单列 | 多列 | |
|---|---|---|
| 单行 | 单行单列 | 单行多列 |
| 多行 | 多行单列 | 多行多列 |
- 单行单列 :聚合函数(
COUNT、MAX、SUM等)或唯一键查单个字段 - 单行多列:根据主键 ID 查询完整实体
- 多行单列:查某一列的所有值(如所有用户名称列表)
- 多行多列:最常规的列表查询,返回多条完整记录
这一讲的目标很明确:四个方法,四种形态,覆盖所有查询场景,没有遗漏,没有重叠。
二、项目结构与前置代码
2.1 依赖与配置
pom.xml 沿用 spring-boot-starter-jdbc + H2 + Lombok 的标准组合,application.yml 配置数据源并自动执行 schema.sql,logback-spring.xml 开启 JdbcTemplate 的 DEBUG 日志。细节同第 5-7 讲,此处不再赘述。
2.2 schema.sql · 建表与初始数据
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 '性别',
age INT COMMENT '年龄'
);
INSERT INTO sys_user (user_name, birthday, gender, age) VALUES
('郭靖', '2018-07-13', 1, 25),
('洪七公', '2018-07-13', 1, 30),
('王重阳', '2018-07-13', 1, 40),
('周伯通', '2018-07-13', 1, 35),
('李莫愁', '2018-07-13', 0, 28);
2.3 User.java · 实体类
java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long userId; /* 主键 */
private String userName; /* 姓名 */
private LocalDate birthday; /* 生日 */
private Byte gender; /* 性别 */
private Integer age; /* 年龄 */
}
三、demo01 · 单行多列 · findById
场景:根据主键 ID 查询单条记录,返回完整实体对象。
java
@Repository
public class UserDao {
@Autowired
private JdbcTemplate jdbc;
public void demo01() {
String sql = "SELECT user_id, user_name, birthday, gender, age " +
"FROM sys_user WHERE user_id = ?";
User user = jdbc.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1L);
log.info("user={}", user);
}
}
核心 API 拆解:
| 要素 | 说明 |
|---|---|
queryForObject |
期望结果集恰好一行 的方法。多于一行会抛出 IncorrectResultSizeDataAccessException |
BeanPropertyRowMapper |
自动映射下划线字段(user_id)到驼峰属性(userId) |
第三个参数 1L |
? 占位符的实际参数值 |
queryForObject 与 query 的区别在于:queryForObject 底层调用 query 获取 List,然后断言 list.size() == 1,若为 0 抛出 EmptyResultDataAccessException,若大于 1 抛出 IncorrectResultSizeDataAccessException。因此,只有确信结果集只有一行时(如按主键查询),才使用 queryForObject。
log
[INFO] user=User(userId=1, userName=郭靖, birthday=2018-07-13, gender=1, age=25)
四、demo02 · 单行单列 · 聚合统计
场景 :聚合函数------COUNT、SUM、AVG、MAX、MIN,返回单个值。
java
public void demo02() {
String sql = "SELECT COUNT(age) FROM sys_user WHERE user_id > ?";
Long count = jdbc.queryForObject(sql, Long.class, 0L);
log.info("count={}", count);
}
核心 API 拆解:
| 要素 | 说明 |
|---|---|
queryForObject(sql, Class<T>, args...) |
直接返回目标类型的单值,无需 RowMapper |
Long.class |
返回类型。支持 Integer、Long、String、Date 等基础类型/包装类型 |
0L |
? 占位符参数,此处代表查询 user_id > 0 的所有记录 |
COUNT(age) 会忽略 age 为 NULL 的行。如果表中数据为空,COUNT 返回 0,不会触发 EmptyResultDataAccessException。但其他聚合函数如 MAX、MIN 在空表上会返回 NULL,映射为 Long 时需注意 NPE 问题,建议使用 Long.class 而非 long.class。
log
[INFO] count=5
五、demo03 · 多行单列 · 取某一列
场景:只需要某一列的数据,用于校验、去重、构造集合等场景。
java
public void demo03() {
String sql = "SELECT user_name FROM sys_user WHERE user_id > ?";
List<String> names = jdbc.query(sql, new SingleColumnRowMapper<>(String.class), 0L);
log.info("names={}", names);
}
核心 API 拆解:
| 要素 | 说明 |
|---|---|
query |
返回 List<T>,零行时返回空列表(不抛异常) |
SingleColumnRowMapper |
专门处理单列多行结果集,指定列的目标类型 |
String.class |
列值映射到的目标类型 |
SingleColumnRowMapper 的底层逻辑:对 ResultSet 的每一行,只取第一列(column index = 1),然后通过 ConversionService 或直接类型转换将 JDBC 类型映射为指定的 Java 类型。对于字符串列,直接调用 rs.getString(1);对于数值列,调用 rs.getObject(1) 后转换为目标类型。零行时返回空列表,与 queryForObject 不同------后者会抛异常。
log
[INFO] names=[郭靖, 洪七公, 王重阳, 周伯通, 李莫愁]
六、demo04 · 多行多列 · 常规列表
场景:最常规的列表查询,返回多条完整记录。
java
@PostConstruct
public void demo04() {
String sql = "SELECT user_id, user_name, birthday, gender, age " +
"FROM sys_user WHERE user_id > ?";
List<User> users = jdbc.query(sql, new BeanPropertyRowMapper<>(User.class), -1L);
users.forEach(u -> log.info("user={}", u));
}
核心 API 拆解:
| 要素 | 说明 |
|---|---|
@PostConstruct |
Bean 初始化后自动执行,此处用于演示启动时打印数据 |
query |
返回 List<User>,零行时返回空列表 |
BeanPropertyRowMapper |
自动完成下划线转驼峰映射 |
-1L |
参数值,此处用于查询 user_id > -1 即全部记录 |
为什么用 @PostConstruct? 本讲的所有 demo 都是通过这个注解在 Spring 容器启动后自动执行的。这是一种演示技巧,让你无需编写 Controller 或手动调用,启动应用就能看到四种查询形态的输出结果。实际业务中,查询方法通常由 Service 层调用,而非在 DAO 中用 @PostConstruct 驱动。
log
[INFO] user=User(userId=1, userName=郭靖, birthday=2018-07-13, gender=1, age=25)
[INFO] user=User(userId=2, userName=洪七公, birthday=2018-07-13, gender=1, age=30)
[INFO] user=User(userId=3, userName=王重阳, birthday=2018-07-13, gender=1, age=40)
[INFO] user=User(userId=4, userName=周伯通, birthday=2018-07-13, gender=1, age=35)
[INFO] user=User(userId=5, userName=李莫愁, birthday=2018-07-13, gender=0, age=28)
七、四种查询 API 汇总
| 象限 | API | RowMapper | 返回值 | 零行时行为 |
|---|---|---|---|---|
| 单行多列 | queryForObject |
BeanPropertyRowMapper |
单个实体对象 | 抛 EmptyResultDataAccessException |
| 单行单列 | queryForObject |
无需 RowMapper,传 Class<T> |
单个值(Long/String等) |
抛 EmptyResultDataAccessException |
| 多行单列 | query |
SingleColumnRowMapper |
List<T> |
返回空列表 |
| 多行多列 | query |
BeanPropertyRowMapper |
List<Entity> |
返回空列表 |
关键区别:queryForObject vs query
| 行为 | queryForObject |
query |
|---|---|---|
| 零行 | 抛 EmptyResultDataAccessException |
返回空列表 |
| 一行 | 返回该行 | 返回包含一个元素的列表 |
| 多行 | 抛 IncorrectResultSizeDataAccessException |
返回包含多个元素的列表 |
⚠️ 实践中需要注意 :
queryForObject并不能保证"最多一行",它只是"期待一行"。如果 SQL 条件写错导致返回多行(比如按非唯一字段查询),应用会直接抛出异常。因此,queryForObject只适用于按主键或唯一约束查询 的场景。对于其他单行查询,优先用query+ 手动判断list.isEmpty(),更加健壮。
八、本讲核心总结
| 核心要点 | 说明 |
|---|---|
| 四象限完备 | 单行单列、单行多列、多行单列、多行多列,覆盖所有查询场景 |
queryForObject + BeanPropertyRowMapper |
单行多列 → 单个实体对象 |
queryForObject + Class<T> |
单行单列 → 单个值(聚合函数) |
query + SingleColumnRowMapper |
多行单列 → List<T>(某一列) |
query + BeanPropertyRowMapper |
多行多列 → List<Entity>(最常规) |
| 零行处理差异 | queryForObject 抛异常,query 返回空列表 |
九、下集预告
第 9 讲将进入 BaseCondition 条件封装 ------手搓条件类,让动态 SQL 从散装 if 收敛为可复用的语义单元:
text
原生 JDBC 拼接动态 SQL 的痛点:大量 if 判空冗余代码
BaseCondition 设计思路:条件收集 + 参数收集
等值、区间、前后模糊、IN 条件的 API 设计
参数自动收集与 WHERE 语句生成