⚡ SQL‑First 范式 · Java 版
🎬 SimpleDAO 企业实战教程 · 第 06 集 · mergeParams 多组条件合并
相关开源地址:
📋 前置知识
| ✅ 你只需要会 | ❌ 你完全不需要会的 |
|---|---|
| Java 基础(类、接口、注解、泛型) | MyBatis / Hibernate 任何知识 |
| 基础 SQL(SELECT、JOIN、WHERE、子查询) | XML 配置、动态标签、插件开发 |
| Spring Boot 基础配置(数据源配置、启动类) | Spring Boot 自动配置原理、条件注解 |
| 会用 IDE 运行 Maven 项目 | Maven 高级配置、父子工程 |
📚 全套教程总览(1 小时从零到生产落地)
| 集数 · 标题 | 本集目录 | 时长 |
|---|---|---|
| 01 · 单表 CRUD + 审计 + 逻辑删除 | 实体注解 · 空 DAO · 保存审计 · ID查询 · 分页 · 逻辑删除 | 约 6 min |
| 02 · 联表查询 + 分页 | 联表 SQL · VO定义 · 条件类 · page调用 · 高性能COUNT | 约 4 min |
| 03 · 条件进阶:IN + 子查询 | IN自动展开 · 子查询拼接 · add vs and · 三种动态边界 | 约 6 min |
| 04 · 多表联查 + 复杂条件 | 行锁 · updateNull · 重复性校验 · 三表联查透传 · 时间范围 | 约 6 min |
| 05 · 报表聚合:GROUP BY + 聚合函数 | 三表JOIN+聚合 · 条件类复用 · 独立判空 · 日志控制到方法 | 约 6 min |
| 06 · mergeParams 多组条件合并(本集) | 多条件类定义 · SQL多位置嵌入 · mergeParams合并 · 条件跨位置复用 | 约 5 min |
| 07 · 多租户 + 数据权限 · AOP 破局 | 传统痛点 · 构造器 add 租户ID · 数据权限 · 对比 MyBatis 插件 | 约 7 min |
| 08 · 脱敏 + 审计扩展 · 框架不设限 | 字段脱敏(VO getter)· 审计重写 · 逻辑删除调整 | 约 7 min |
🚀 项目快速上手
本集案例依旧内置 H2 内存库,无需安装任何外部数据库,克隆项目直接启动即可运行。本集重点展示多组条件合并 —— 当报表需要同时接收时间范围、业务筛选、数据权限等多组条件,且这些条件需要嵌入 SQL 的不同位置时,SimpleDAO 如何做到清晰又灵活。
完整项目层级结构
demo06_mergeParams/
├── pom.xml
└── src/main/
├── java/example/
│ ├── DemoApplication.java // 启动类,内置全套测试逻辑
│ └── report/
│ ├── TimeCond.java // 时间条件类
│ ├── BizCond.java // 业务筛选条件类
│ ├── ValidCond.java // 有效数据条件类
│ ├── ReportDao.java // 报表 DAO(核心:mergeParams)
│ └── ReportVo.java // 报表结果 VO
└── resources/
├── application.yml // 极简数据源配置
└── schema.sql // 三表建表 + 测试数据
1. Maven 依赖 pom.xml
说明:仅依赖 Spring JDBC + SimpleDAO 核心包,依赖体积缩减至 1/3。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.simple</groupId>
<artifactId>simple-dao</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2. 配置文件 application.yml
说明:无框架专属复杂配置,仅标准 Spring 数据源。simple-dao.show-sql: false 全局关闭 SQL 日志。
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
simple-dao.show-sql: false
3. 建表脚本 + 测试数据 schema.sql
说明:包含用户表 sys_user、订单表 bus_order、商品表 bus_goods,一套典型的一对多对多关联。测试数据已预置,开箱即用。
-- 1. 用户表(3条)
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
age INT NOT NULL,
email VARCHAR(50),
dr TINYINT DEFAULT 0 COMMENT '删除标记 0-未删 1-已删'
);
INSERT INTO sys_user (id, name, age, email) VALUES
(1, '张三', 25, 'zhangsan@test.com'),
(2, '李四', 30, 'lisi@test.com'),
(3, '王五', 28, 'wangwu@test.com');
-- 2. 订单表(4条)
DROP TABLE IF EXISTS bus_order;
CREATE TABLE bus_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '关联sys_user.id',
order_no VARCHAR(30) NOT NULL COMMENT '订单号',
create_time DATETIME NOT NULL COMMENT '创建时间',
create_by BIGINT NOT NULL COMMENT '创建人',
update_time DATETIME NOT NULL COMMENT '修改时间',
update_by BIGINT NOT NULL COMMENT '修改人',
dr TINYINT DEFAULT 0 COMMENT '删除标记 0-未删 1-已删',
FOREIGN KEY (user_id) REFERENCES sys_user(id)
);
INSERT INTO bus_order (id, user_id, order_no, create_time, create_by, update_time, update_by) VALUES
(1, 1, 'ORD2026001', '2026-02-01 10:00:00', 1, '2026-02-01 10:00:00', 1),
(2, 1, 'ORD2026002', '2026-02-02 14:30:00', 1, '2026-02-02 14:30:00', 1),
(3, 2, 'ORD2026003', '2026-02-03 09:15:00', 2, '2026-02-03 09:15:00', 2),
(4, 3, 'ORD2026004', '2026-02-04 16:20:00', 3, '2026-02-04 16:20:00', 3);
-- 3. 商品表(3条)
DROP TABLE IF EXISTS bus_goods;
CREATE TABLE bus_goods (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL COMMENT '关联bus_order.id',
goods_name VARCHAR(50) NOT NULL COMMENT '商品名',
price DECIMAL(10,2) NOT NULL COMMENT '商品价格',
dr TINYINT DEFAULT 0 COMMENT '删除标记 0-未删 1-已删',
FOREIGN KEY (order_id) REFERENCES bus_order(id)
);
INSERT INTO bus_goods (id, order_id, goods_name, price) VALUES
(1, 1, '手机', 1999.00),
(2, 2, '耳机', 199.00),
(3, 3, '键盘', 299.00);
🔧 核心业务代码演示
第一层:实体类 Goods.java
说明:@Table 绑定表名,@Id 标记主键。本集重点在多组条件合并,实体类快速过。
@Setter
@Getter
@Table("bus_goods")
public class Goods {
@Id
private Long id;
private Long orderId;
private String goodsName;
private BigDecimal price;
private Byte dr;
}
第二层:报表 VO ReportVo.java
说明:按商品分组统计的结果,包含商品名、订单数、商品数、总金额、均价。
@Data
public class ReportVo {
private String goodsName; // 商品名
private Integer orderCount; // 订单数
private Integer goodsCount; // 商品数
private BigDecimal totalAmount;// 总金额
private BigDecimal avgPrice; // 均价
}
第三层:条件类(本集核心:多条件类各司其职)
说明:我们定义了三个独立的条件类,各司其职、互不干扰。每个条件类只关心自己那部分规则,通过继承 BaseCondition 并在 addCondition() 中定义自己的条件逻辑。
TimeCond.java —— 时间范围条件
@Setter
@Getter
public class TimeCond extends BaseCondition {
private LocalDateTime orderTimeStart; // 开始时间
private LocalDateTime orderTimeEnd; // 结束时间
@Override
protected void addCondition() {
add("AND create_time >= ?", orderTimeStart);
add("AND create_time <= ?", orderTimeEnd);
}
}
BizCond.java —— 业务筛选条件
@Setter
@Getter
public class BizCond extends BaseCondition {
private String goodsName; // 商品名(模糊)
private Double priceMin; // 最低价格
private Double priceMax; // 最高价格
@Override
protected void addCondition() {
add("AND t.price >= ?", priceMin);
add("AND t.price <= ?", priceMax);
add("AND t.goods_name LIKE ?", goodsName, 3);
}
}
ValidCond.java —— 有效数据条件
@Setter
@Getter
public class ValidCond extends BaseCondition {
private String userName; // 用户名(模糊)
@Override
protected void addCondition() {
add("AND name LIKE ?", userName, 3);
add("AND dr = 0"); // 固定过滤
}
}
第四层:DAO 层 ReportDao.java(核心:mergeParams)
说明:需求是「按商品分组统计,但筛选条件分别来自三张表,且要嵌入 SQL 的不同位置」:
- 时间条件 → 嵌入订单子查询(
timeCond.and()) - 业务条件 → 作用于主查询 WHERE(
bizCond.and()) - 有效用户条件 → 嵌入用户子查询(
validCond.where())
三个条件片段被精确地放置在了它们该去的地方。最后通过 mergeParams() 按顺序合并参数列表。
@Repository
public class ReportDao extends BaseDao<Goods> {
/**
* 多条件合并报表统计
* 条件分别嵌入 SQL 的 3 个不同位置
*/
public List<ReportVo> reportGoodsByMerge(TimeCond timeCond, BizCond bizCond, ValidCond validCond) {
String sql =
"SELECT t.goods_name, COUNT(o.id) order_count, COUNT(t.id) goods_count, " +
"SUM(t.price) total_amount, AVG(t.price) avg_price FROM bus_goods t " +
"JOIN (SELECT id, user_id FROM bus_order WHERE dr=0 " + timeCond.and() + ") o ON t.order_id = o.id " +
"WHERE t.dr = 0 " + bizCond.and() +
"AND o.user_id IN (SELECT id FROM sys_user " + validCond.where() + ") GROUP BY t.goods_name";
// 按顺序合并三个条件类的参数数组
return list(sql, ReportVo.class, BaseCondition.mergeParams(timeCond, bizCond, validCond));
}
}
📌 mergeParams 两大核心作用
| 作用 | 说明 | 示例 |
|---|---|---|
| 合并参数 | 按传入顺序把多个条件类的参数列表合并成一个完整数组,永不乱序 | mergeParams(timeCond, bizCond, validCond) |
| 条件复用 | 同一个条件类可在 SQL 不同位置多次出现,参数同样可以重复传入 | 下文的 UNION ALL 示例 |
📌 条件复用典型场景:UNION ALL
当你在 UNION ALL 中需要对多个独立查询应用完全相同的时间条件时,mergeParams 配合条件复用可以避免重复构造:
-- 纯 SQL:两段查询共用同一个时间范围
SELECT '收入' AS type, SUM(amount) FROM t_income WHERE create_time >= ? AND create_time <= ?
UNION ALL
SELECT '退费' AS type, SUM(amount) FROM t_refund WHERE create_time >= ? AND create_time <= ?
// Java 代码:同一个 dateCond 在 SQL 中出现两次,mergeParams 也传两次
String sql = "SELECT '收入' AS type, SUM(amount) FROM t_income WHERE 1=1 " + dateCond.and() +
" UNION ALL " +
"SELECT '退费' AS type, SUM(amount) FROM t_refund WHERE 1=1 " + dateCond.and();
return list(sql, IncomeVO.class, BaseCondition.mergeParams(dateCond, dateCond));
要点:条件类没有被绑定在某个固定的 SQL 片段上,它是一个完全独立的、可携带的参数单元。你可以在任何需要它的地方调用
.and()或.where(),然后由mergeParams按指定顺序统一调度这些单元产生的参数。将来要拿掉或新增某个条件,只需修改一处,其他位置完全不用动。
📝 业务调用与运行日志
场景一:全条件组合
调用代码:传入时间范围 + 业务筛选(商品名模糊 + 价格下限)+ 有效用户(用户名模糊)。
LocalDateTime start = LocalDateTime.of(2026, 2, 1, 0, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 2, 4, 23, 59, 59);
TimeCond timeCond = TimeCond.builder().orderTimeStart(start).orderTimeEnd(end).build();
BizCond bizCond = BizCond.builder().goodsName("手").priceMin(1000.0).build();
ValidCond validCond = ValidCond.builder().userName("张").build();
reportDao.reportGoodsByMerge(timeCond, bizCond, validCond)
.forEach(i -> log.info("结果:{}", i));
运行日志:
[INFO] SELECT t.goods_name, COUNT(o.id) order_count, COUNT(t.id) goods_count,
SUM(t.price) total_amount, AVG(t.price) avg_price
FROM bus_goods t
JOIN (SELECT id, user_id FROM bus_order WHERE dr=0
AND create_time >= '2026-02-01 00:00:00'
AND create_time <= '2026-02-04 23:59:59') o ON t.order_id = o.id
WHERE t.dr = 0
AND t.price >= 1000.0
AND t.goods_name LIKE '%手%'
AND o.user_id IN (SELECT id FROM sys_user WHERE name LIKE '%张%' AND dr=0)
GROUP BY t.goods_name
[INFO] 结果:ReportVo(goodsName=手机, orderCount=1, goodsCount=1, totalAmount=1999.00, avgPrice=1999.00)
场景二:缺时间条件
调用代码:传入空的 TimeCond,时间条件被自动忽略。
reportDao.reportGoodsByMerge(new TimeCond(), bizCond, validCond)
.forEach(i -> log.info("结果:{}", i));
运行日志:
[INFO] SELECT t.goods_name, COUNT(o.id) order_count, COUNT(t.id) goods_count,
SUM(t.price) total_amount, AVG(t.price) avg_price
FROM bus_goods t
JOIN (SELECT id, user_id FROM bus_order WHERE dr=0) o ON t.order_id = o.id
WHERE t.dr = 0
AND t.price >= 1000.0
AND t.goods_name LIKE '%手%'
AND o.user_id IN (SELECT id FROM sys_user WHERE name LIKE '%张%' AND dr=0)
GROUP BY t.goods_name
[INFO] 结果:ReportVo(goodsName=手机, orderCount=1, goodsCount=1, totalAmount=1999.00, avgPrice=1999.00)
关键观察:日志中的 SQL 完全没有时间范围条件 —— 因为
TimeCond为空对象,add()方法检测到值为null自动忽略。你不需要写任何if (timeCond != null)判断。
📌 本集核心总结
-
多条件类各司其职:按业务维度拆分条件类(时间、业务、权限),每个类只管理自己的规则,职责清晰,互不干扰。
-
SQL 多位置嵌入:同一个查询中,不同条件可以嵌入 SQL 的不同位置(主查询 WHERE、子查询、JOIN 子句等),通过
.where()和.and()精准控制。 -
mergeParams合并参数:静态方法BaseCondition.mergeParams(cond1, cond2, ...)按顺序合并多个条件类的参数数组,顺序完全由你控制,永不乱序。 -
条件跨位置复用:同一个条件类可在 SQL 不同位置多次出现,
mergeParams同样支持重复传入,特别适合 UNION ALL 等场景。 -
零 XML,SQL 即所见:全程无 XML、无标签、无 OGNL。你手写的 SQL 片段 + 条件类生成的片段 = 最终执行的 SQL,完全透明。
-
条件类 = 可组合的积木:条件类没有被绑定在某个固定的 SQL 片段上,它是完全独立的、可携带的参数单元。将来要拿掉或新增某个条件,只需修改一处,其他位置完全不用动。
下一集,我们将进入企业级开发的另一个硬核战场 —— 多租户 + 数据权限 · AOP 破局,看 SimpleDAO 如何用 Spring 原生 AOP 优雅解决传统 MyBatis 拦截器的诸多痛点,敬请期待!
 mergeParams 多组条件合并&spm=1001.2101.3001.5002&articleId=162395793&d=1&t=3&u=77675f982bfd4580825dacf76e5d7b6c)
484

被折叠的 条评论
为什么被折叠?



