⚡Spring JDBC 完整体系 · 第 10 集 · 手搓 `BaseCondition`

Spring JDBC 完整体系 · 第 10 集 | 手搓 BaseCondition

相关开源地址(与主体系一致):

  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 自动配置原理
基础 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 字符串和参数数组。动态条件意味着 ? 的数量和参数值都是不确定的,我们必须设计一个条件载体,它能够:

  1. 按需拼装 SQL 片段
  2. 同步收集对应的参数
  3. 最终输出完整的条件子句和参数数组

三、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 判空。


📌 第②步:把拼装逻辑塞进条件类

StringBuilderList<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);
}

进步 :每个条件一行代码,判空逻辑被统一封装。

问题paramListconditionaddwhereandarray 这些基础设施在每个条件类里完全一样,重复定义。


📌 第④步:明确划分「专用」和「共用」

观察每个条件类的结构,可以清晰地分为两部分:

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());

这就是模板方法模式 :父类定义了算法的骨架(andaddCondition → 返回结果),子类填充具体的业务细节(写哪些条件)。子类只关心"加什么条件",不关心"怎么重置、怎么清空、怎么输出"。


六、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(),由子类实现
具体子类 UserCondOrderCond,各自实现 addCondition() 填充业务条件
模板方法 and() 方法:清空 → 调用 addCondition() → 返回结果

父类掌控何时 调用、如何 重置状态;子类掌控添加哪些条件 。这就是模板方法模式的核心:父类定义骨架,子类填充细节


八、本集总结 · 每步都是被问题逼出来的

步骤 做了什么 驱动力
纯粹的 POJO 条件类 承载查询参数
把拼装逻辑塞进条件类 拼装逻辑必须有载体
抽取 add 三合一方法 每个字段都在重复判空和拼接
明确划分"专用"和"共用" 基础设施在每个类里完全一样
抽离 BaseCondition 基类 共用部分与具体业务无关,必须复用

一句话总结BaseCondition 的诞生不是一次突发灵感,而是被每一阶段的实际问题倒逼出来的必然结果。从最简单的 POJO 出发,一步步解决重复判空、重复基础设施、重复代码的问题,最终收敛为一个可复用的条件基类。


九、下一讲预告

第 11 集我们将把 BaseCondition 整合进 BaseDaolist 方法,完成单表条件查询的完整闭环。届时 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 模块。

相关推荐
珍珠先生1 小时前
第9章 JDK 11 新特性:var 与便捷新 API
java
2601_962074681 小时前
Spring Boot的无缝衔接:深入解析与实践
数据库·spring boot·后端
hoho_121 小时前
麒麟V10升级nginx最新版本到1.31.4
java·服务器·nginx
冬夜戏雪1 小时前
笔试的Scanner in Scanner out
java·开发语言
南城以南溫暖如初1471 小时前
社交裂变商城系统设计与实战开发指南
java·spring boot·mysql·vue·mybatis
Zane1994121 小时前
HashSet、TreeSet、LinkedHashSet:底层都是“套壳“
java·开发语言
xiaoqiMikko1 小时前
Spring 官方给 5.3 线开了三个修复版,Maven Central 上一个都没有
java·spring boot
聪明蛋子哟1 小时前
多智能体协作的三种模式:从群聊到分层,如何设计可扩展的Agent系统
后端·python·flask
南城以南溫暖如初1472 小时前
分享返现商城系统开发实战:技术架构与运营经验指南
java·spring boot·mysql·架构·vue·mybatis