⚡ Spring JDBC 完整体系 · 第 8 讲 · 查询四象限完备

⚡ Spring JDBC 完整体系 · 第 8 讲 · 查询四象限完备

相关开源地址

  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 自动配置源码细节
第 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 代码里,查询结果只有四种形态。这就是所谓的四象限完备

具体是哪四种?我们用一张图说清楚:

单列 多列
单行 单行单列 单行多列
多行 多行单列 多行多列
  • 单行单列 :聚合函数(COUNTMAXSUM 等)或唯一键查单个字段
  • 单行多列:根据主键 ID 查询完整实体
  • 多行单列:查某一列的所有值(如所有用户名称列表)
  • 多行多列:最常规的列表查询,返回多条完整记录

这一讲的目标很明确:四个方法,四种形态,覆盖所有查询场景,没有遗漏,没有重叠。

二、项目结构与前置代码

2.1 依赖与配置

pom.xml 沿用 spring-boot-starter-jdbc + H2 + Lombok 的标准组合,application.yml 配置数据源并自动执行 schema.sqllogback-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 ? 占位符的实际参数值

queryForObjectquery 的区别在于: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 · 单行单列 · 聚合统计

场景 :聚合函数------COUNTSUMAVGMAXMIN,返回单个值。

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 返回类型。支持 IntegerLongStringDate 等基础类型/包装类型
0L ? 占位符参数,此处代表查询 user_id > 0 的所有记录

COUNT(age) 会忽略 ageNULL 的行。如果表中数据为空,COUNT 返回 0,不会触发 EmptyResultDataAccessException。但其他聚合函数如 MAXMIN 在空表上会返回 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 语句生成
相关推荐
情可轻 秦任之4 小时前
ASP.NET页面优化,性能提升8倍的方法
后端·asp.net
雾喔4 小时前
算法练习7
java·数据结构·算法
王中阳Go4 小时前
用TRAE Work批量优化学员简历,原来2天的活现在2小时就干完了
后端·面试·agent
Zane19944 小时前
同一个 list 为什么能被两个 for 循环同时遍历?一文讲透可迭代对象与迭代器的真相
后端·python
他们叫我秃子4 小时前
前端开发转 Go 全栈(四):代码写在前面,却要最后执行?我终于搞懂了 defer
前端·后端·go
掉鱼的猫4 小时前
跳出 Fatjar 的束缚:Solon 插件的体外扩展(E-Spi)与热插拔(H-Spi)
java
Bill FANG5 小时前
从原生Java NIO到Netty重构:充电桩Socket服务端高性能改造实战
java·重构·nio
2601_955759415 小时前
Claude API 成本审计:该看哪些指标,怎么分析更靠谱
java
带刺的坐椅5 小时前
跳出 Fatjar 的束缚:Solon 插件的体外扩展(E-Spi)与热插拔(H-Spi)
java·jar·solon·plugin·fatjar
Csvn5 小时前
📊 SQL 入门 Day 14:聚合窗口函数 — 让 SUM / AVG 也能"滑动"起来
后端·sql