⚡ 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 语句生成
相关推荐
一直C4 小时前
【数据结构】哈希表+算法复杂度与经典排序查找(C语言)
java·linux·开发语言·数据结构·算法·ubuntu·散列表
paopaokaka_luck4 小时前
基于springboot3+vue3的车间生产管理系统(Echarts图形化分析、BI报表)
java·前端·spring boot·学习·echarts
程序员鱼皮4 小时前
3 大 DeepSeek Harness 进阶玩法,招多个大肥鱼帮我干活!
前端·后端·ai编程
苍何4 小时前
DeepSeek 终于支持多模态了(附实测及接入教程)
后端
KoPa4 小时前
HeySmart:大模型开源网关基座-请求生命周期与钩子引擎
前端·后端
苍何4 小时前
做AI视频还在拆盲盒?手把手教你导演级运镜(附教程)
后端
爱学习的小邓同学4 小时前
Golang语言入门
开发语言·后端·golang
苍何4 小时前
豆包,开始做普通人的 Codex
后端
七牛开发者4 小时前
Coding Agent 如何跑稳长任务?从上下文管理到运行时状态
前端·javascript·后端
Zane19944 小时前
遍历时删元素为什么报错:modCount 这个"计数器"在背后盯着你
java·后端