一个基于 Spring JDBC 的持久层方案,让代码量减少 60%
一、先给你看三个对比------如果你写过 MyBatis,你会懂
对比一:动态条件拼接
场景: 根据用户名模糊查询、年龄区间、部门ID、ID集合的 IN 查询。
MyBatis 写法(XML):
xml
<if test="name != null and name != ''">
AND user_name LIKE concat('%', #{name}, '%')
</if>
<if test="ageMin != null">
AND age >= #{ageMin}
</if>
<if test="ageMax != null">
AND age <= #{ageMax}
</if>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
<if test="ids != null and ids.size > 0">
AND user_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
~15 行,每个条件写两遍(if 判断 + SQL 片段),而且你必须保证参数名、字段名、SQL 片段三者一致,改一个地方要同步改三个位置。
SimpleDAO 写法(Java,BaseCondition 子类):
java
@Override
protected void addCondition() {
and("name LIKE", name, 3); // 一行,自动处理空值
and("age >=", ageMin); // 一行
and("age <=", ageMax); // 一行
and("dept_id =", deptId); // 一行
in("user_id", ids); // 一行,自动展开数组
}
5 行,每个条件一行,没有冗余判断,没有标签闭合,没有参数名重复声明。
对比二:IN 条件
场景: 按一组 ID 查询用户。
MyBatis 写法:
xml
<if test="ids != null and ids.size() > 0">
AND user_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
6 行(if 开始、SQL 片段、foreach 开始、循环体、foreach 结束、if 结束)
SimpleDAO 写法:
java
in("user_id", ids);
1 行
对比三:子查询 + 关联表条件 + 逻辑开关
场景: 查询订单,条件包括订单状态、金额下限、用户姓名、用户手机号,并且根据业务开关决定是否只查"有有效订单"的用户。
SimpleDAO 写法(OrderCond):
java
@Override
protected void addCondition() {
// 主表条件
and("order_status =", status);
and("total_amount >=", amountMin);
// 关联表条件(用户表)------直接写 SQL 片段
add("AND u.name LIKE ?", userName, 3);
add("AND u.phone = ?", userPhone);
// 动态子查询------一行搞定,逻辑开关控制
add("AND u.id IN (SELECT user_id FROM bus_order WHERE dr = 0)", subQueryEnabled);
}
关键点: add 方法不区分"主表条件"、"关联表条件"、"子查询条件"。它只做两件事:拼接字符串、收集参数。你写什么 SQL 片段,它就拼什么,它不解析、不拦截、不转换。
同样的 add 方法,既能拼主表条件,也能拼关联表条件,还能拼子查询------因为我们的条件类不关心 SQL 片段长什么样,它只关心两件事:这个片段要不要拼进去、对应的参数放哪个位置。
二、设计哲学:为什么可以这样写
2.1 起点:80 行代码解决核心痛点
这套体系的起点,是一个 80 行的 Sql 工具类。核心思想极其朴素:
动态条件拼接 = 字符串拼接 + 参数收集
你只需要一个 StringBuilder 存 SQL 片段,一个 List<Object> 存参数值。每次 add 方法同时操作这两个结构,确保"片段"和"参数"永远一一对应。
这个工具类不需要 XML,不需要 OGNL,不需要 Mapper 接口。它只是一个纯 Java 的"拼接器"。但它解决的是 MyBatis 用户每天最痛苦的事------动态条件拼接。
2.2 三层架构:各司其职,互不侵入
| 层级 | 职责 | 核心类 |
|---|---|---|
| 执行层 | 发 SQL、收结果 | BaseSql |
| 对象化层 | 单表 CRUD、实体映射 | BaseDao |
| 拼接层 | 动态条件、参数收集 | BaseCondition |
关键设计: BaseCondition 既是 SQL 片段的拼接器,也是参数的收集器。add 方法同时干了这两件事,所以永远不会出现"条件跟参数对不上"的问题。
BaseSql 不知道什么是实体、什么是条件------它只负责把 SQL 和参数发出去。BaseDao 不知道什么是联表、什么是子查询------它只负责管理单表的 CRUD。BaseCondition 不知道什么是事务、什么是数据源------它只管拼字符串和收参数。
三层之间通过"参数数组"和"SQL 字符串"连接,没有任何一层依赖另一层的内部状态。
2.3 双不封装:保持对 SQL 的完全控制力
不封装关键字和运算符
我们不提供 .eq()、.gt()、.like() 这类链式 API。你直接写 AND age >= ?。
原因: SQL 语法是稳定的、完整的、经过数十年验证的,不需要 Java 再封装一层。
代价: 封装等于穷举,穷举必然遗漏。每当遇到 BETWEEN、REGEXP、MATCH AGAINST、JSON_CONTAINS,封装好的 API 就会露出缺口,开发者被迫退回到"拼字符串"。那为什么不从一开始就保持"拼字符串"的权利?
不封装 Spring JDBC 异常
所有异常都来自 JdbcTemplate,只有 DataAccessException 这一套体系。
原因: Spring 的异常体系已经足够完善,不需要再包装一层 PersistenceException。
代价: 你不需要学第二套异常类型,不需要在 MyBatis 的 31 类异常和 Spring 的异常之间来回翻译。
"双不封装"带来的副作用: BaseCondition 不区分单表、联表、子查询、半连接、SQL 片段。因为它只做最纯的"拼接 + 参数收集",它根本不管前面的 SQL 是什么结构。
2.4 双重复用:一次封装,处处可用
BaseCondition 在 BaseDao 和 BaseSql 两层复用
BaseDao用条件类生成单表的WHERE和SETBaseSql用条件类对联表、报表、子查询生成WHERE- 同一个条件类,单表联表都能用
BaseDao 复用 BaseSql 的执行能力
BaseDao只负责"生成 SQL 的静态部分"(SELECT、INSERT、UPDATE、DELETE)BaseSql只负责"把 SQL 和参数发出去"- 两者通过条件类连接,互不依赖
2.5 五大独创
独创 1:单表、联表同一套 API 族
用户从单表过渡到联表时,心智负担为零。你只需要改 SQL 字符串,其他不变。
独创 2:单表、联表同一套条件类
BaseCondition 不区分单表、联表、子查询、半连接、SQL 片段。因为它只做最纯的"拼接 + 参数收集"。
独创 3:屏蔽条件中的第一层 if
add 方法内部自动判断 value != null && value != ""。开发者不用写 if(userName != null),直接写条件。减少 90% 的样板代码。
独创 4:参数合并(mergeParams)
多个条件类各自独立构造,最后一行合并参数数组。解决报表/复杂查询场景下的参数顺序耦合问题。
独创 5:正交完备的 API(基于关系代数)
四个核心方法:field(单行单列)、row(单行多列)、columns(多行单列)、list(多行多列)。
基于关系代数的闭包性质设计------两个关系的运算结果还是一个关系,在 Java 里表现为这四个象限。不是拍脑袋穷举出来的。
三、实战案例:哲学不是空谈
案例一:单表 CRUD(零代码继承)
java
@Data
@Table("sys_user")
public class User {
@Id
private Long id;
private String name;
private Integer age;
// 审计字段自动填充:createTime、createBy、updateTime、updateBy
private Byte dr; // 逻辑删除,自动处理
}
@Repository
public class UserDao extends BaseDao<User> {
// 空类即可获得:save、update、delete、list、page、findById 等全部 CRUD 能力
}
DAO 层零代码。 继承空类就能获得完整 CRUD 能力。
案例二:联表查询(同样的 API)
java
@Repository
public class OrderDao extends BaseDao<Order> {
private final static String SQL = """
SELECT t.*, u.name user_name, u.phone user_phone
FROM bus_order t
LEFT JOIN sys_user u ON t.user_id = u.id
""";
public Page<OrderVO> pageJoin(OrderCond cond) {
return page(SQL, cond, OrderVO.class); // 同样的 page 方法
}
}
从单表到联表,只是改了一个 SQL 字符串,其他代码完全不变。
案例三:mergeParams------多条件合并
java
TimeCond timeCond = TimeCond.builder().orderTimeStart(start).orderTimeEnd(end).build();
BizCond bizCond = BizCond.builder().goodsName("手机").priceMin(1000.0).build();
ValidCond validCond = ValidCond.builder().userName("张").build();
String sql = """
SELECT t.goods_name, COUNT(o.id) order_count
FROM t_goods t
JOIN (SELECT id, user_id FROM t_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 t_user """ + validCond.where() + """)
GROUP BY t.goods_name
""";
// 一行合并所有参数,按 SQL 中 ? 出现的顺序精准匹配
list(sql, ReportVo.class, mergeParams(timeCond, bizCond, validCond));
三个条件类各自独立构造,在 SQL 不同位置嵌入,最后一行合并参数。
案例四:数据权限------AOP + extendCondition
java
@Aspect
@Component
public class DataAuthAspect {
@Before("@annotation(auth)")
public void beforeQuery(JoinPoint point, DataAuth auth) {
BaseCondition cond = (BaseCondition) point.getArgs()[0];
String userId = request.getHeader("X-User-Id");
// 一行注入权限条件
cond.setExtendCondition(" AND " + auth.userField() + " IN (" + userId + ",0)");
}
}
@Repository
public class NoticeDao extends BaseDao<Notice> {
@DataAuth(userField = "t.receiver")
public Page<NoticeVO> pageJoin(NoticeCond cond) {
return page(SQL, cond, NoticeVO.class);
}
}
MyBatis 用户要写 Interceptor 去扒 BoundSql,风险极高。你只需要 setExtendCondition,像喝水一样简单。
四、与 MyBatis 的核心对比
| 维度 | MyBatis | SimpleDAO |
|---|---|---|
| 动态条件 | XML 标签,一行变三行 | add 方法,一行解决 |
| IN 条件 | 6 行(if + foreach + 闭合) | 1 行 |
| 关联表条件 | 需在 XML 中混写 | 同一套 add API |
| 子查询 | XML 标签嵌套,难以维护 | add("AND ... IN (SELECT ...)", flag) 一行 |
| 数据权限 | 拦截器扒 BoundSql,风险高 | AOP + setExtendCondition |
| 脱敏 | 写 TypeHandler 或手动 | AOP 代理 Service 方法 |
| 多条件合并 | 参数顺序在 XML 中耦合 | mergeParams 一行合并 |
| 异常体系 | 31 类自造异常 | 仅 DataAccessException |
| 学习成本 | XML + OGNL + 标签 + 拦截器 | 懂 SQL + Spring JDBC 即可 |
五、框架与工具链
核心框架
https://gitee.com/gao_zhenzhong/simple-dao
核心就三个类:BaseDao、BaseSql、BaseCondition,总共 ~300 行核心逻辑。没有任何黑盒拦截器,没有任何 XML 解析开销,没有任何 OGNL 表达式引擎。
管理系统完整底座(RBAC 权限、鉴权、前后端脚手架)
https://gitee.com/gao_zhenzhong/simple-dao-starter
包含完整的用户、角色、权限、菜单管理,前后端分离脚手架。开箱即用,基于 SimpleDAO 构建,所有数据访问层天然享受 SimpleDAO 的一切优势。
代码生成器
https://gitee.com/gao_zhenzhong/simple-dao-coder
一键生成 Entity、Cond、Dao、Service、Controller、VO、前端页面模板(Vue + Element)。生成的代码是标准 Java,不是黑盒魔法,你可以自由修改、Code Review、对接任意技术栈。
实战案例仓库
https://gitee.com/gao_zhenzhong/simple-dao-demo
包含 8 个可运行案例:单表 CRUD、联表分页、子查询、报表聚合、多条件合并、数据权限、脱敏。内置 H2 内存数据库,开箱即跑。
六、总结
SimpleDAO 不造新轮子,它只是把 Spring JDBC 的潜力释放到了极致------让你用最少的代码、最简的心智、最透明的 SQL,完成企业级开发。
我们为自己写的,是一套能让自己少加班、早回家的工具。如果你也受够了 MyBatis 的 XML 和 OGNL,欢迎来试试。
能力上限 = SQL 表达的上限 = Spring 生态的上限。没有任何人为设置的边界。
- 代码量: 减少 60%-80%
- 学习成本: 会 SQL +Java 即可
- 调试体验: 复制 SQL 到 Navicat 就能跑
- 性能: ≈ Spring JDBC,无中间层损耗
- 迁移成本: 零------可与 MyBatis 无缝共存
把时间留给生活,而不是框架。