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极强,savefindById非常方便
✅ 对象关系映射@OneToMany@ManyToOne,适合简单主子表
❌ 复杂 SQLJPQL 能力有限,标量子查询、派生表、窗口函数几乎不可用
❌ 性能不可控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% 白盒可控
❌ 单表对象化无,saveupdatedelete全部手写 SQL
❌ 条件拼接手写WHERE拼字符串,?占位符顺序要人工对齐
❌ 分页手写LIMIT/ROWNUM/OFFSET FETCH,换数据库要重写
❌ 审计字段手写createTimeupdateTime赋值
❌ 日志需要自己配 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_EXTRACTGROUP_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每次手动 setsave时自动注入
updateTime/updateBy每次手动 setupdate时自动注入
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 = 1delete(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 FETCH4 个 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_nameuserName

痛点 13:批量操作的性能优化

传统方案Spring JDBC Ultra
逐条插入 1000 条数据saveBatch(list)生成真正的批量 INSERT
JPA 的saveAll是逐条 insertMySQL 支持replaceBatch批量 Upsert

痛点 14:分布式主键生成

传统方案Spring JDBC Ultra
数据库自增 ID 分库分表不可用@Id("snow")雪花算法
UUID 太长影响索引性能worker-iddata-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/JPAMyBatisSpring JDBCSimpleDAO
单表 CRUD 自动化✅ 强❌ 弱❌ 无
联表 SQL 自由度❌ 受限✅ 全自由✅ 全自由全自由
SQL 组织方式HQL/JPQLXML 标签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 的设计哲学,可以用三个“不封装”来概括:

  1. 不封装 SQL 的内容:你不写 HQL、不写 JPQL、不写 XML 标签,你直接写 SQL。SQL 是 4GL(第四代语言),是数据库的母语,不需要被“翻译”成任何中间语言。

  2. 不封装 SQL 的能力:标量子查询、半连接、派生表、窗口函数、CTE、数据库专有函数——你随便写。框架不做任何“能力阉割”。

  3. 不封装 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 的竞争对手”。

它是对“关系型数据库应该如何被访问”这个问题的终极回答。

这个回答不依赖于某一门语言,不依赖于某个特定版本,不依赖于某家公司的商业策略。它只依赖于三个永恒的事实:

  1. SQL 是集合论和关系代数的编程语言(4GL)
  2. Java 是图论和对象引用的编程语言(3GL)
  3. 这两者之间没有完美映射,但可以有一座足够薄的桥

SimpleDAO 就是这座桥。

它不假装自己能消除 3GL 和 4GL 之间的语义鸿沟(那是 ORM 的幻觉),它只是在这条鸿沟上架了一座足够薄的桥——让你在桥的这边用 Java 组织参数,在桥的那边用 SQL 表达业务,两边各司其职,互不干扰。

写在最后

如果你也受够了:

  • MyBatis 的 XML 标签地狱
  • JPA 的 N+1 查询陷阱和 HQL 语法限制
  • 手写 RowMapper 的机械重复
  • 调试时手动替换 20 个?的痛苦
  • 数据权限、多租户等扩展需求不得不扒源码

欢迎来试试Spring JDBC Ultra(开源项目名:SimpleDAO)。

它不是这个时代最流行的框架,但它是这个时代最诚实的框架。因为它从不假装自己能做到做不到的事,也从不阻拦开发者去做应该做的事。

把时间留给业务,而不是框架。

相关开源地址

  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

:本文是Spring JDBC Ultra的落地实践篇。关于支撑这套框架的底层理论体系——SQL-First 范式,我已单独写了一篇完整的理论文章,从 3GL(Java)与 4GL(SQL)之间的代际差、关系代数的动态性梯度,到 ORM 为何注定失败的数学原因,做了系统性剖析。

👉 SQL-First 范式:持久层设计的终极思想(附理论+落地实战)