Spring JDBC 完整体系 · 第 10 集 | 手搓 BaseCondition
相关开源地址(与主体系一致):
📋 前置知识
| 你只需要会 | 你完全不需要会 |
|---|---|
| Java 基础 + Spring Boot 基本用法 | Spring 自动配置原理 |
| 基础 SQL(SELECT、WHERE) | MyBatis / Hibernate 任何知识 |
第 9 集的 list 泛型方法 |
设计模式理论 |
上一集我们封装了 list 方法,用泛型解决了结果映射的重复代码问题。但动态条件的拼装依然散落在各处------每个 DAO 方法里都有一堆 if 判空和 params.add()。本集我们正式手搓 BaseCondition,把动态条件从散装 if 收敛为可复用的语义单元。
一、项目依赖 · 延续第 9 集环境
本集代码基于第 9 集的 Spring Boot 4.1+ 环境,依赖完全一致:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>[4.1.0,4.99.999)</version>
<relativePath/>
</parent>
<dependencies>
<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>
</dependencies>
application.yml 与第 9 集相同,使用 H2 内存库,启动时执行 schema.sql:
yaml
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
建表脚本沿用 sys_user + sys_dept,此处不再重复粘贴,可参考第 9 集或源码仓库。
二、回顾 · list 方法留下的悬念
第 9 集我们封装了通用的 list 方法:
java
protected <T> List<T> list(String sql, Class<T> clazz, Object... obj) {
return jdbc.query(sql, new BeanPropertyRowMapper<>(clazz), obj);
}
这个方法很漂亮地解决了结果映射的重复代码,但调用时如果遇到动态条件,依然要手动拼装:
java
// 动态条件还是散装的
StringBuilder sql = new StringBuilder("SELECT * FROM sys_user WHERE 1=1");
List<Object> params = new ArrayList<>();
if (name != null && !name.isEmpty()) {
sql.append(" AND user_name LIKE ?");
params.add("%" + name + "%");
}
if (age != null) {
sql.append(" AND age >= ?");
params.add(age);
}
List<User> users = list(sql.toString(), User.class, params.toArray());
问题核心 :list 方法需要两样东西------完整的 SQL 字符串和参数数组。动态条件意味着 ? 的数量和参数值都是不确定的,我们必须设计一个条件载体,它能够:
- 按需拼装 SQL 片段
- 同步收集对应的参数
- 最终输出完整的条件子句和参数数组
三、list 方法的参数结构分析
在动手之前,先看清 list 方法的三个参数:
java
protected <T> List<T> list(String sql, Class<T> clazz, Object... obj)
| 参数 | 类型 | 作用 |
|---|---|---|
sql |
String |
SQL 模板,包含 ? 占位符 |
clazz |
Class<T> |
返回实体类型,作为类型令牌 |
obj |
Object... |
可变参数,按顺序填充 ? |
关键约束 :obj 是定长数组 。SQL 里有几个 ?,就必须传入对应数量的参数,不能多也不能少。
看一条联表查询的 SQL:
sql
SELECT u.user_name, u.gender, u.age, d.dept_name, d.dept_code
FROM sys_user u
JOIN sys_dept d ON u.dept_id = d.dept_id
WHERE u.user_name LIKE ? AND d.dept_code = ?
WHERE 部分有两个条件,必须传 2 个参数。但业务中 user_name 可能为空,此时只需要 1 个参数甚至 0 个。定长数组无法动态伸缩,这就是为什么需要一个独立的条件载体来处理动态拼装。
四、条件载体必须满足的约定
为了让 list 方法支持动态条件,条件载体必须具备三种能力:
| 能力 | 对应方法 | 用途 |
|---|---|---|
| 拼装条件 | addCondition() |
编写具体的业务条件逻辑 |
| 输出拼接好的条件 | where() |
给 list 方法提供完整的 WHERE 子句 |
| 输出参数数组 | array() |
给 list 方法提供按顺序排列的参数 |
这三个方法协同工作,最终将动态条件转换为 list 方法能够接受的 sql + obj 形式。
五、五步推导 · 从 POJO 到 BaseCondition
下面我们用五步推导,展示 BaseCondition 是如何被实际问题一步步逼出来的。
📌 第①步:条件参数类 ------ 纯粹的 POJO
最简单的形态,只是一个承载查询参数的普通 Java 对象:
java
@Setter
@Getter
public class UserCond {
private String name;
private Integer age;
}
问题 :只有数据,没有行为。拼装条件的逻辑全部甩给调用方,每个 DAO 方法都要写一堆 if 判空。
📌 第②步:把拼装逻辑塞进条件类
把 StringBuilder 和 List<Object> 搬进条件类内部,让条件自己负责拼装:
java
@Setter
@Getter
public class UserCond {
private String name;
private Integer age;
protected List<Object> paramList = new ArrayList<>();
protected StringBuilder condition = new StringBuilder();
protected void addCondition() {
if (name != null) {
condition.append(" AND u.user_name LIKE ?");
paramList.add("%" + name + "%");
}
if (age != null) {
condition.append(" AND u.age >= ?");
paramList.add(age);
}
}
public String where() {
addCondition();
return condition.toString().replaceFirst("AND", "WHERE");
}
public String and() {
addCondition();
return condition.toString();
}
public Object[] array() {
return paramList.toArray();
}
}
调用方现在可以这样用:
java
UserCond cond = new UserCond();
cond.setName("郭靖");
cond.setAge(20);
String sql = "SELECT * FROM sys_user u WHERE 1=1" + cond.and();
List<User> users = list(sql, User.class, cond.array());
进步 :动态条件有了专属载体,拼装逻辑内聚在条件类中。
问题:每个条件字段都要手动写判空、拼接、收集参数,三个动作重复劳动。
📌 第③步:抽取 add 方法 ------ 判空 + 拼接 + 收参 三合一
封装一个 add 工具方法,把判空、拼接、收集参数合并为一行:
java
protected final void add(final String sql, final Object value) {
if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
condition.append(" ").append(sql);
paramList.add(value);
}
}
addCondition 瞬间变得清爽:
java
protected void addCondition() {
add("AND u.user_name LIKE ?", "%" + name + "%");
add("AND u.age >= ?", age);
}
进步 :每个条件一行代码,判空逻辑被统一封装。
问题 :paramList、condition、add、where、and、array 这些基础设施在每个条件类里完全一样,重复定义。
📌 第④步:明确划分「专用」和「共用」
观察每个条件类的结构,可以清晰地分为两部分:
java
// ===== 专用部分(每个类不同) =====
private String name;
private Integer age;
protected void addCondition() {
add("AND u.user_name LIKE ?", "%" + name + "%");
add("AND u.age >= ?", age);
}
// ===== 共用部分(每个类完全相同) =====
protected List<Object> paramList = new ArrayList<>();
protected StringBuilder condition = new StringBuilder();
protected final void add(String sql, Object value) { ... }
public String where() { ... }
public String and() { ... }
public Object[] array() { ... }
专用部分与具体业务相关,共用部分与业务无关。共用部分应该属于基类。
📌 第⑤步:抽取 BaseCondition ------ 模板方法模式
把共用部分抽到抽象基类 BaseCondition 中:
java
package com.gzz.common;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class BaseCondition {
protected List<Object> paramList = new ArrayList<>();
protected StringBuilder condition = new StringBuilder();
/** 模板方法:子类实现具体条件拼装 */
protected abstract void addCondition();
/** 拼装条件(保留 AND 前缀) */
protected final void add(final String sql, final Object value) {
if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
condition.append(" ").append(sql);
paramList.add(value);
}
}
/** 重置并返回 AND 条件串(用于拼接在 WHERE 1=1 后面) */
public String and() {
condition.setLength(0);
paramList.clear();
addCondition();
return condition.toString();
}
/** 重置并返回 WHERE 条件串(替换首个 AND 为 WHERE) */
public String where() {
return and().replaceFirst("AND", "WHERE");
}
/** 返回参数数组 */
public Object[] array() {
return paramList.toArray();
}
}
子类现在只写业务条件:
java
@Setter
@Getter
public class UserCond extends BaseCondition {
private String name;
private Integer age;
@Override
protected void addCondition() {
add("AND u.user_name LIKE ?", "%" + name + "%");
add("AND u.age >= ?", age);
}
}
调用方式:
java
UserCond cond = new UserCond();
cond.setName("郭靖");
String sql = "SELECT * FROM sys_user u WHERE 1=1";
List<User> users = list(sql + cond.and(), User.class, cond.array());
或直接使用 cond.where() 替换 WHERE 1=1 的写法:
java
String sql = "SELECT * FROM sys_user u";
List<User> users = list(sql + cond.where(), User.class, cond.array());
这就是模板方法模式 :父类定义了算法的骨架(and → addCondition → 返回结果),子类填充具体的业务细节(写哪些条件)。子类只关心"加什么条件",不关心"怎么重置、怎么清空、怎么输出"。
六、add 方法的多种重载
实际业务中,条件类型远不止"等于"。BaseCondition 中通常会提供多个 add 重载,覆盖常见场景:
java
// 1. 等值查询
protected final void add(String sql, Object value) {
if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
condition.append(" ").append(sql);
paramList.add(value);
}
}
// 2. IN 查询
protected final void addIn(String sql, Object... values) {
if (StringUtils.hasText(sql) && !ObjectUtils.isEmpty(values)) {
condition.append(" ").append(sql).append(in(values));
paramList.addAll(Arrays.asList(values));
}
}
// 3. 模糊查询(通过 site 控制 % 位置)
protected final void add(String sql, String value, int site) {
if (Objects.nonNull(sql) && StringUtils.hasText(value)) {
condition.append(" ").append(sql);
switch (site) {
case 1 -> paramList.add("%" + value); // 左模糊
case 2 -> paramList.add(value + "%"); // 右模糊
case 3 -> paramList.add("%" + value + "%"); // 全模糊
}
}
}
// 4. 纯 SQL 片段(无参数,如 IS NULL)
protected final void add(String sql, Boolean logic) {
if (Objects.nonNull(logic) && logic && StringUtils.hasText(sql)) {
condition.append(" ").append(sql);
}
}
有了这些重载,子类几乎覆盖所有业务条件类型,无需自己处理判空和参数收集:
java
@Override
protected void addCondition() {
add("AND u.user_id > ?", userId); // > 查询
add("AND u.user_name LIKE ?", userName, 3); // 全模糊
addIn("AND u.dept_id IN", deptIds); // IN 查询
add("AND u.deleted IS NULL", true); // 无参条件
}
七、为什么叫「模板方法模式」?
很多初学者觉得"模板方法"听起来很高深,实际上就是我们刚才做的事情:
| 角色 | 在本案例中 |
|---|---|
| 抽象基类 | BaseCondition,定义了 and() / where() / array() 的调用流程 |
| 抽象方法 | addCondition(),由子类实现 |
| 具体子类 | UserCond、OrderCond,各自实现 addCondition() 填充业务条件 |
| 模板方法 | and() 方法:清空 → 调用 addCondition() → 返回结果 |
父类掌控何时 调用、如何 重置状态;子类掌控添加哪些条件 。这就是模板方法模式的核心:父类定义骨架,子类填充细节。
八、本集总结 · 每步都是被问题逼出来的
| 步骤 | 做了什么 | 驱动力 |
|---|---|---|
| ① | 纯粹的 POJO 条件类 | 承载查询参数 |
| ② | 把拼装逻辑塞进条件类 | 拼装逻辑必须有载体 |
| ③ | 抽取 add 三合一方法 |
每个字段都在重复判空和拼接 |
| ④ | 明确划分"专用"和"共用" | 基础设施在每个类里完全一样 |
| ⑤ | 抽离 BaseCondition 基类 |
共用部分与具体业务无关,必须复用 |
一句话总结 :BaseCondition 的诞生不是一次突发灵感,而是被每一阶段的实际问题倒逼出来的必然结果。从最简单的 POJO 出发,一步步解决重复判空、重复基础设施、重复代码的问题,最终收敛为一个可复用的条件基类。
九、下一讲预告
第 11 集我们将把 BaseCondition 整合进 BaseDao 的 list 方法,完成单表条件查询的完整闭环。届时 DAO 层将彻底告别手动拼 SQL,做到:
java
UserCond cond = new UserCond();
cond.setUserName("郭靖");
List<User> users = userDao.list(cond); // 一行搞定
下期见!
附:本集完整源码结构(关键文件)
com.gzz.common.BaseCondition(条件基类)com.gzz.sys.user.UserCond(用户条件子类)com.gzz.common.BaseDao(含list方法)- 调用演示见
com.gzz.sys.user.UserService
完整代码可参考文首开源仓库 simple-dao-demo 中的 16-spirng-jdbc-obj 模块。