Spring JDBC Ultra:凭什么敢说自己是 MyBatis 终结者?
📚 8 集实战教程(从入门到精通)
| 集数 · 标题 | 本集目录 | 时长 |
|---|---|---|
| 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 破局 | Filter + AOP链路 · extendCondition钩子 · 最小路径演示 | 约 7 min |
| 08 · 脱敏 + 审计扩展 · 框架不设限 | 字段脱敏(VO getter)· 审计重写 · 逻辑删除调整 | 约 7 min |
写在前面:一个“异类”的诞生
我写持久层框架开源之后经常收到一个问题:
“你这框架跟 MyBatis 有什么区别?跟 JPA 有什么区别?”
我的回答是:它们都在做“映射”,我在做“连接”。它们面向数据库设计,我面向业务设计。
这个区别,看似很小,实则是一条分水岭。
我从 8 个 Demo 案例、2 组生产案例、16 个痛点场景一路验证下来,逐步构建起一套完整的理论体系——SQL-First 范式。今天这篇文章,就是把这套范式和它的实现框架Spring JDBC Ultra(开源项目名:SimpleDAO),完整地介绍给你。
一、先看现实:三大主流框架,各有各的“死穴”
在 Spring JDBC Ultra 出现之前,Java 持久层生态由三大主流把持。我们心平气和地看看它们各自的优劣势:
1. Hibernate / JPA
| 维度 | 评价 |
|---|---|
| ✅ 单表 CRUD | 极强,save、findById非常方便 |
| ✅ 对象关系映射 | 有@OneToMany、@ManyToOne,适合简单主子表 |
| ❌ 复杂 SQL | JPQL 能力有限,标量子查询、派生表、窗口函数几乎不可用 |
| ❌ 性能不可控 | N+1 问题、懒加载陷阱、SQL 生成黑盒 |
一句话总结:单表是王者,复杂查询是青铜。
2. MyBatis
| 维度 | 评价 |
|---|---|
| ✅ SQL 自由度 | 原生 SQL 随便写,不受 JPQL 限制 |
| ❌ 组织方式 | 被 XML 绑架,SQL 被标签切割成碎片,维护成本高 |
| ❌ 动态条件 | <if>、<foreach>标签地狱,OGNL 表达式黑盒 |
| ❌ 扩展能力 | 拦截器体系复杂,数据权限、多租户等扩展要扒源码 |
一句话总结:SQL 是自由的,但被 XML 套上了枷锁。
你说得对,我重新琢磨了一下——“无框架级拦截器,但可以用 Spring AOP”,这恰恰是 Spring JDBC 的白盒优势,不是短板。
修正后的对比表:
3. Spring JDBC
| 维度 | 评价 |
|---|---|
| ✅ SQL 自由度 | 极致的自由,想写什么写什么 |
| ✅ 白盒 | 执行过程完全透明,没有任何黑盒拦截器 |
| ✅ 结果映射 | 自动映射(BeanPropertyRowMapper),支持下划线转驼峰 |
| ✅ 扩展能力 | 无框架级拦截器,但可以利用Spring AOP做数据权限、多租户等横切逻辑,100% 白盒可控 |
| ❌ 单表对象化 | 无,save、update、delete全部手写 SQL |
| ❌ 条件拼接 | 手写WHERE拼字符串,?占位符顺序要人工对齐 |
| ❌ 分页 | 手写LIMIT/ROWNUM/OFFSET FETCH,换数据库要重写 |
| ❌ 审计字段 | 手写createTime、updateTime赋值 |
| ❌ 日志 | 需要自己配 Logback 打印占位符 SQL |
一句话总结:白盒透明,AOP 扩展无上限,但条件拼接、分页、审计、日志全要手写,繁琐。
- MyBatis 的拦截器:黑盒,你要学它那套
Interceptor接口、Invocation对象、@Intercepts注解,还容易跟别的插件冲突,属于“框架强加的扩展机制”。 - Spring JDBC 的扩展:没有内置拦截器,但你可以用Spring AOP做任何事——
@Around切 Service 层或 DAO 层,纯原生 Spring 语法,不需要理解任何 MyBatis 内部结构。
“无内置拦截器”不是缺点,是设计选择——把扩展能力交还给 Spring 生态最原生的 AOP,这才是白盒的极致体现。
这三个框架,各自解决了某个方面的问题,又各自在另一个方面留下了巨大的坑。开发者常年在这三者之间反复横跳,始终没有一个方案能“一杆清台”。
二、一个大胆的尝试:把三者的优点集于一身
于是我开始思考:能不能做一个框架,把三者的优点全部继承,把三者的缺点全部剔除?
- 继承 Hibernate 的单表对象化——但绝不搞
@OneToMany那种对象嵌套。 - 继承 MyBatis 的SQL 全自由——但绝不把 SQL 塞进 XML。
- 继承 Spring JDBC 的纯白盒透明——但把参数传递和结果映射自动化。
这就是Spring JDBC Ultra(开源项目名:SimpleDAO)的起点。
它不是“第四个选项”,它是前三个的“完全体”。
三、核心设计:三大主类,解决 90% 的痛点
1.BaseDao—— 单表 CRUD 的“零代码”实现
@RepositorypublicclassUserDaoextendsBaseDao<User>{// 空类,继承即获得所有 CRUD 能力}一个空类,你就拥有了:
save(T)/saveBatch(List<T>)update(T)/updateNull(T)delete(id...)/delete(Cond)findById(id)/findOne(Cond)list(Cond)/page(Cond)/count(Cond)/exists(Cond)
注解驱动:
@Data@Table("sys_user")publicclassUser{@Id// 默认雪花算法,也支持 AUTO / UUID / CUSTOMprivateLongid;privateStringname;privateIntegerage;privateLocalDateTimecreateTime;// save 时自动填充privateLongcreateBy;// save 时自动填充privateLocalDateTimeupdateTime;// update 时自动填充privateLongupdateBy;// update 时自动填充privateBytedr;// 逻辑删除字段}就这么简单。没有 XML,没有@PrePersist,没有拦截器配置。
2.BaseSql—— 联表查询的“无限自由”
单表用BaseDao,联表用BaseSql。API 完全一致:
@RepositorypublicclassOrderDaoextendsBaseDao<Order>{privatestaticfinalStringJOIN_SQL=""" SELECT o.*, u.name user_name, u.phone user_phone FROM bus_order o LEFT JOIN sys_user u ON o.user_id = u.id """;publicPage<OrderVO>pageJoin(OrderCondcond){returnpage(JOIN_SQL,cond,OrderVO.class);}}支持所有 SQL 特性:
- 标量子查询:
SELECT o.*, (SELECT COUNT(1) FROM items WHERE order_id = o.id) AS cnt FROM orders o - 半连接:
WHERE EXISTS (SELECT 1 FROM items WHERE order_id = o.id) - 派生表:
JOIN (SELECT user_id, COUNT(1) cnt FROM orders GROUP BY user_id) stats ON stats.user_id = u.id - 窗口函数、CTE、
UNION、数据库专有函数(JSON_EXTRACT、GROUP_CONCAT等)
框架不解析 SQL,所以以上全部支持。你能写出来的 SQL,框架就能映射出来。
3.BaseCondition—— 条件拼接的“语义单元”
MyBatis 的动态 SQL 靠 XML 标签,Spring JDBC Ultra 靠 Java 代码:
@Getter@SetterpublicclassUserCondextendsBaseCondition{privateStringname;privateIntegerageMin;privateIntegerageMax;privateBytestatus;privateObject[]ids;@OverrideprotectedvoidaddCondition(){and("name LIKE",name,3);// 3=前后模糊and("age >=",ageMin);and("age <=",ageMax);and("status =",status);in("id",ids);// 关联表条件直接用 addadd("AND u.dept_id = ?",deptId);add("AND u.role IN ",roleIds);// IN 条件自动展开// 带逻辑开关的条件add("AND u.id IN (SELECT user_id FROM orders WHERE status = 1)",hasOrder);}}关键洞察:这不是“动态条件”,这是“静态条件 + 动态参数”。真正的动态条件(运行时决定列名)用addDynamic+ AOP 实现(见后文)。
四、16 个痛点,逐个拿下
下面我把日常开发中最常遇到的 16 个痛点,以及 Spring JDBC Ultra 的解法,逐一列出来。
痛点 1:单表 CRUD 样板代码
| 传统方案 | Spring JDBC Ultra |
|---|---|
| 每张表手写 insert/update/delete/select | 继承BaseDao<T>,空类获得全部能力 |
| 改一个字段改 5 处代码 | 只改实体类 |
痛点 2:审计字段手工填充
| 传统方案 | Spring JDBC Ultra |
|---|---|
createTime/createBy每次手动 set | save时自动注入 |
updateTime/updateBy每次手动 set | update时自动注入 |
MyBatis 要写拦截器,JPA 要写@PrePersist | 零配置,实体类定义字段即可 |
// 你只需要在实体类里定义这些字段privateLocalDateTimecreateTime;privateLongcreateBy;privateLocalDateTimeupdateTime;privateLongupdateBy;// 剩下的框架自动完成痛点 3:逻辑删除标准化缺失
| 传统方案 | Spring JDBC Ultra |
|---|---|
有的表用del_flag,有的用is_deleted | 统一配置simple-dao.logic-delete.field: dr |
手写update t set dr = 1 | delete(id)自动变成逻辑删除 |
痛点 4:SQL 日志信息黑洞
| 传统方案 | Spring JDBC Ultra |
|---|---|
MyBatis 打印WHERE name = ?,参数另起一行 | 打印完整 SQL:WHERE name = '张三' |
调试要手动替换 20 个? | 复制日志直接贴到 Navicat 执行 |
| 日志要么全开要么全关 | 方法级控制:list(true, cond)打印,list(false, cond)不打印 |
这是我最得意的功能之一——Sql.fill(sql, params)把占位符全部替换成真实值,日志即调试工具。
痛点 5:动态条件拼接的“标签地狱”
| 传统方案 | Spring JDBC Ultra |
|---|---|
MyBatis XML 里<if>嵌套<foreach>,200 行起步 | Java 代码里直接if+and(),一行一个条件 |
| 改条件要改 XML,容易漏闭合标签 | IDE 重构、高亮、跳转全支持 |
痛点 6:联表查询的对象映射灾难
| 传统方案 | Spring JDBC Ultra |
|---|---|
JPA 的@OneToMany导致 N+1 | 直接写 SQL,list(SQL, cond, VO.class) |
MyBatis 的resultMap写 100 行 XML | 列名和 VO 字段名匹配即可,零配置 |
| 12 表联查基本不可维护 | 12 表联查,SQL 文本块直接写 |
痛点 7:标量子查询 / 半连接 / 派生表
| 传统方案 | Spring JDBC Ultra |
|---|---|
| JPQL 完全不支持 | SQL 文本块直接写 |
JPA 只能退回nativeQuery = true | 框架不做任何限制 |
| MyBatis 能写但 SQL 被 XML 切碎 | 完整 SQL 保留在 Java 里 |
Stringsql=""" SELECT o.*, (SELECT COUNT(1) FROM order_item WHERE order_id = o.id) AS item_count, (SELECT SUM(amount) FROM payment WHERE order_id = o.id) AS paid_amount FROM orders o WHERE EXISTS (SELECT 1 FROM order_item WHERE order_id = o.id AND price > 1000) """;// 这个 SQL 在 JPA 里写不出来,在 Spring JDBC Ultra 里直接跑痛点 8:数据库专有函数
| 传统方案 | Spring JDBC Ultra |
|---|---|
JPA 用FUNCTION('JSON_EXTRACT', ...) | 直接写JSON_EXTRACT |
MyBatis 用${}有注入风险 | 直接写,参数部分依然走?占位符 |
痛点 9:分页方言差异
| 传统方案 | Spring JDBC Ultra |
|---|---|
MySQL 用LIMIT,Oracle 用ROWNUM,SQL Server 用OFFSET FETCH | 4 个 Dialect 类自动适配 |
| 手写分页,换数据库重写 SQL | 自动检测数据库类型,零配置 |
痛点 10:数据权限 / 多租户
| 传统方案 | Spring JDBC Ultra |
|---|---|
| MyBatis 拦截器解析 SQL,风险高 | AOP +addDynamic,注入条件片段 |
JPA@Filter黑盒操作 | 列名由运行时决定,值走预编译 |
@Aspect@ComponentpublicclassDataAuthAspect{@Around("@annotation(dataAuth)")publicObjectinjectAuth(ProceedingJoinPointpjp,DataAuthdataAuth){BaseConditioncond=(BaseCondition)pjp.getArgs()[0];StringuserId=getCurrentUserId();// 真正的动态条件:列名由运行时决定cond.addDynamic(" AND "+dataAuth.userField()+" = ?",userId);returnpjp.proceed();}}痛点 11:多条件类参数合并
| 传统方案 | Spring JDBC Ultra |
|---|---|
| 多个子查询不同条件,要揉进一个 DTO | 每个子查询用独立的 Cond 类 |
XML 里用<if>判断来源,极易混乱 | mergeParams(cond1, cond2, cond3)自动合并 |
Stringsql=""" SELECT ... FROM (子查询1 WHERE条件A) a LEFT JOIN (子查询2 WHERE条件B) b """;returnlist(sql,VO.class,mergeParams(condA,condB));痛点 12:结果集映射的重复劳动
| 传统方案 | Spring JDBC Ultra |
|---|---|
RowMapper手写rs.getString("name") | BeanPropertyRowMapper自动映射 |
换字段就要改RowMapper | 列名和 VO 字段名匹配即可 |
| 下划线转驼峰要手动处理 | 自动转换:user_name→userName |
痛点 13:批量操作的性能优化
| 传统方案 | Spring JDBC Ultra |
|---|---|
| 逐条插入 1000 条数据 | saveBatch(list)生成真正的批量 INSERT |
JPA 的saveAll是逐条 insert | MySQL 支持replaceBatch批量 Upsert |
痛点 14:分布式主键生成
| 传统方案 | Spring JDBC Ultra |
|---|---|
| 数据库自增 ID 分库分表不可用 | @Id("snow")雪花算法 |
| UUID 太长影响索引性能 | worker-id和data-center-id支持集群配置 |
| 雪花算法要自己实现 | 一行注解解决 |
痛点 15:SQL 注入
| 传统方案 | Spring JDBC Ultra |
|---|---|
MyBatis 的${}是 SQL 注入高发区 | 所有值传递强制走?占位符 |
JPA 的nativeQuery同样存在拼接风险 | SqlSecurityChecker检查动态 SQL 片段 |
痛点 16:跨语言复刻
| 传统方案 | Spring JDBC Ultra |
|---|---|
| Hibernate 只在 Java 生态 | 已复刻到 8 种语言(Java、C#、Python、Go、Rust、PHP、Node.js、C++) |
| 换语言要重学一套 ORM | 范式跟着 SQL 走,跨语言知识复用 |
五、三方对比:SimpleDAO 如何“集大成”
把三者的优劣势和 SimpleDAO 的定位放在一起对比,一目了然:
| 能力维度 | Hibernate/JPA | MyBatis | Spring JDBC | SimpleDAO |
|---|---|---|---|---|
| 单表 CRUD 自动化 | ✅ 强 | ❌ 弱 | ❌ 无 | ✅强 |
| 联表 SQL 自由度 | ❌ 受限 | ✅ 全自由 | ✅ 全自由 | ✅全自由 |
| SQL 组织方式 | HQL/JPQL | XML 标签 | Java 字符串(需手动拼接) | ✅Java 文本块 + 条件类 |
| 动态条件表达 | Criteria API(冗长) | <if>/<foreach>(标签地狱) | 手写WHERE拼字符串 | ✅Java +add语义单元 |
| 参数传递 | 自动(JPQL 占位符) | 自动(#{}) | 自动(JdbcTemplate可变参数 / 命名参数) | ✅自动 + 顺序精准 |
| 结果映射 | 自动(含对象嵌套) | 自动(resultMap或列名匹配) | 自动(BeanPropertyRowMapper) | ✅自动平铺映射 |
| 日志 | 占位符 SQL + 参数分离 | 占位符 SQL + 参数分离 | 占位符 SQL + 参数分离 | ✅完整带参 SQL |
| 执行透明度 | 黑盒(SQL 生成不可见) | 灰盒(SQL 可见,但拦截器改写) | 白盒(SQL 即执行 SQL) | ✅纯白盒 |
| 扩展能力 | 监听器(受限) | 拦截器(复杂) | Spring AOP(原生白盒) | ✅Spring AOP + 内置扩展点 |
| 数据库专有函数 | ❌ 需FUNCTION包装 | ✅ 可用(${}有注入风险) | ✅ 可用 | ✅直接写,无限制 |
| 复杂子查询/派生表 | ❌ JPQL 不支持 | ✅ 可用(SQL 被 XML 切碎) | ✅ 可用 | ✅直接写,无限制 |
六、三个字总结:不封装
Spring JDBC Ultra 的设计哲学,可以用三个“不封装”来概括:
不封装 SQL 的内容:你不写 HQL、不写 JPQL、不写 XML 标签,你直接写 SQL。SQL 是 4GL(第四代语言),是数据库的母语,不需要被“翻译”成任何中间语言。
不封装 SQL 的能力:标量子查询、半连接、派生表、窗口函数、CTE、数据库专有函数——你随便写。框架不做任何“能力阉割”。
不封装 SQL 的结果:
ResultSet映射到 VO 是唯一的封装,且是“平铺映射”。复杂对象嵌套是业务表达的范畴,在 Service 层用 Java 集合做内存组装,绝不把树形结构强塞给 SQL。
七、核心结论:面向业务设计,而非面向数据库设计
所有已知的持久层框架,都是面向数据库设计的。Spring JDBC Ultra 是唯一一个面向业务设计的。
- Hibernate/JPA:先定义
@Entity、@OneToMany,再让业务逻辑适配这个模型。 - MyBatis:先写 Mapper 接口 + XML,再让 SQL 适配 XML 的标签语法。
- Spring JDBC Ultra:业务需要什么数据形状,你就写什么 SQL;SQL 怎么写,框架就怎么帮你传参和映射。
这就是 SQL-First 范式的核心:不是“少写 SQL”,而是让 SQL 回归它本来的位置——作为业务表达的直接载体。框架不定义业务规则,业务规则由开发者的 SQL 和 Java 代码定义。
八、为什么说它是“元模型”?
SimpleDAO 不是“另一个 ORM”,不是“MyBatis 的平替”,不是“JPA 的竞争对手”。
它是对“关系型数据库应该如何被访问”这个问题的终极回答。
这个回答不依赖于某一门语言,不依赖于某个特定版本,不依赖于某家公司的商业策略。它只依赖于三个永恒的事实:
- SQL 是集合论和关系代数的编程语言(4GL)
- Java 是图论和对象引用的编程语言(3GL)
- 这两者之间没有完美映射,但可以有一座足够薄的桥
SimpleDAO 就是这座桥。
它不假装自己能消除 3GL 和 4GL 之间的语义鸿沟(那是 ORM 的幻觉),它只是在这条鸿沟上架了一座足够薄的桥——让你在桥的这边用 Java 组织参数,在桥的那边用 SQL 表达业务,两边各司其职,互不干扰。
写在最后
如果你也受够了:
- MyBatis 的 XML 标签地狱
- JPA 的 N+1 查询陷阱和 HQL 语法限制
- 手写 RowMapper 的机械重复
- 调试时手动替换 20 个
?的痛苦 - 数据权限、多租户等扩展需求不得不扒源码
欢迎来试试Spring JDBC Ultra(开源项目名:SimpleDAO)。
它不是这个时代最流行的框架,但它是这个时代最诚实的框架。因为它从不假装自己能做到做不到的事,也从不阻拦开发者去做应该做的事。
把时间留给业务,而不是框架。
相关开源地址:
- 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
- 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
- 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
- 实战案例:https://gitee.com/gao_zhenzhong/simple-dao-demo
附:本文是Spring JDBC Ultra的落地实践篇。关于支撑这套框架的底层理论体系——SQL-First 范式,我已单独写了一篇完整的理论文章,从 3GL(Java)与 4GL(SQL)之间的代际差、关系代数的动态性梯度,到 ORM 为何注定失败的数学原因,做了系统性剖析。
👉 SQL-First 范式:持久层设计的终极思想(附理论+落地实战)