全面解构传统持久层框架,拥抱真正的 SQL-First
写在前面
我从一开始就反复强调一句话:
“数据库只认 SQL。”
50 年了,没有任何东西能替代它。所有试图封装 SQL、隐藏 SQL、生成 SQL、翻译 SQL 的框架,最终都逃不开一个结局——你还是要写 SQL,而且你还要多学一层框架的垃圾话。
今天我把这些年对持久层框架的观察、踩坑、思考,一次性摊开来讲。
第一部分:解构 MyBatis——XML 不是优雅,是噪音
MyBatis 号称“SQL-First”,我承认它确实让你写 SQL。但问题在于:你在哪里写?用什么方式写?
答案是:在 XML 里,用一堆标签包着 SQL 写。
1. SQL 和 Java 分离?分明是割裂
他们说“SQL 和 Java 分离更清晰”。但分离不等于解耦,更不等于更好的维护性。
你把 SQL 从 Java 里挪到了 XML,但代价是什么?
- 两个文件之间来回切——改个字段名,Java 改一遍,XML 改一遍
- 四种语言混在一起——Java、XML、OGNL、SQL,你要在同一个文件里处理四种语法
- 无法跳转、无法重构、无法追踪引用——你的 IDE 帮不了你,因为 XML 不是 Java
这叫解耦?这叫耦合Plus,耦合Max。Java 和 SQL 之间没有“分离”,只是从一种耦合变成了更隐蔽、更难维护的耦合。
2. 动态 SQL?本质是低配版字符串拼接
MyBatis 吹得最响的就是“动态 SQL”:
<if test="name != null and name != ''">
AND name = #{name}
</if>
我来问一个问题:这跟你在 Java 里写 if (name != null) { sql.append("AND name = ?"); params.add(name); } 有什么本质区别?
答案是:没有。都是字符串拼接。
唯一的区别是:Java 里写是一行 if,XML 里写是三行标签。Java 里你能用 replace、format、正则、流式处理,XML 里你只能靠那几套标签死撑。
动态 SQL 的灵魂在于“动态”二字,在于逻辑。逻辑用 Java 写是天经地义的,用 XML 写是在自缚手脚。
3. resultMap 的“灵活”是个定时炸弹
<resultMap id="userMap" type="User">
<association property="dept" column="dept_id" select="selectDept"/>
</resultMap>
这个东西看起来很美好:你查一个用户,它自动帮你把部门也带出来。
但实际生产环境没人敢用。因为你不知道它什么时候炸,怎么炸,炸多响。你查 100 条用户,它就发 101 条 SQL;查 1000 条,就发 1001 条。
有经验的 MyBatis 开发者会怎么做?
退回到“1+1 查询”——主表查一次,子表查一次,然后在内存里用 Java 组装。
一旦退回到这一步,MyBatis 的 association 就成了摆设。你付出的是每一次查询都要解析 XML 的代价,换来的只是自动把 ResultSet 转成对象。这个功能 Spring 的 BeanPropertyRowMapper 也能做到,而且不用解析 XML。
那 MyBatis 还剩什么优势?
4. 31 类异常 vs 2 类异常
MyBatis 自己制造了 31 类异常。你花时间学的不是“数据库出了什么问题”,而是“框架的哪一层又炸了”。
你调试的不是 SQL 逻辑,是 XML 标签、是 OGNL 语法、是插件冲突、是缓存配置。你伺候的是框架,不是业务。
Spring JDBC 只有两到三类异常。SimpleDAO 也继承 Spring JDBC,同样是两到三类。你看到的异常就是数据库报的异常,没有框架中间层给你包装一层“框架相关”的废话。
5. 生态繁荣?那是补坑生态
为什么 MyBatis 有那么多插件?PageHelper、MyBatis-Plus、通用 Mapper、MyBatis-Flex……
因为这些功能 MyBatis 自己都不提供。分页不提供,你要装插件;数据权限不提供,你要自己写拦截器;脱敏不提供,你要自己实现 TypeHandler;单表 CRUD 简化不提供,你要用 Plus 或 Flex。
这叫生态繁荣?这叫框架缺了太多东西,全靠别人补。
第二部分:解构 ORM——单表一时爽,联表火葬场
ORM 最大的谎言是:“单表解决 80% 的问题。”
真实的企业开发中,单表查询占比连 10% 都不到。
用户权限、数据统计、业务流程落地、报表聚合……全部都需要多表联查。单表只存在于基础增删改查的测试阶段或极简模块。
1. 单表对象化的价值,只在增删改
- 插入:对象 → 一行记录,天然映射
- 更新:对象 → 一行记录,天然映射
- 删除:按主键删一行,天然映射
这三件事,ORM 做得好,做得省力。但增删改在业务代码中的占比有多少?不到 10%。
2. 查询场景下,ORM 只能躺平
联表查询、聚合查询、子查询、半连接、窗口函数……你一进入查询场景,ORM 就废了。
你写 JPQL、HQL、Criteria、QueryDSL,最后发现它们的能力连 SQL 的 1/3 都发挥不出来。标量子查询写不出来,派生子查询写不出来,窗口函数没有,CTE 没有。
于是 ORM 厂商在文档里写:复杂查询请使用原生 SQL。
这句话翻译一下就是:“我们帮你解决了 10% 的单表增删改,剩下的 90% 请你自己写 SQL。”
3. 关系对象化,根本是错的
ORM 试图把“关系数据库”变成“对象数据库”,把“关系”变成“对象树”。
但数据库的本质是关系,不是对象。两张表 JOIN 之后的结果,既不是“父对象”也不是“子对象”,它就是“两张表的部分字段组合”。你硬要把它变成一个对象树的嵌套结构,就是在扭曲数据的本质。
关系是网,不是树。
第三部分:解构 DSL——翻译带来的只有成本,没有收益
jOOQ 把 DSL 推向极致:全面翻译。每个关键字、每个函数、每个操作符都翻译成 Java 方法。
1. 类型安全?收益极低
jOOQ 的逻辑是:我帮你提前检查字段名拼写错误,这叫类型安全。
但你想想:
- 你写完程序不点一下吗? 点一下不就发现报错了吗?
- 你不自测吗? 自测一下不就发现字段名写错了吗?
- 团队没有测试吗? 测试不就能覆盖吗?
- 如果有人偷偷改数据库字段, 你手写 SQL 不也一样报错吗?
类型安全唯一的收益是“编译时报错比运行时快一步”,但这一步的收益,根本不足以覆盖它带来的成本。
2. 逻辑错误,你永远检查不出来
SELECT * FROM user WHERE age > 18 写成 SELECT * FROM user WHERE age > 80,语法完全正确,类型完全正确,jOOQ 不会报任何错。但业务数据全漏了。
类型安全只覆盖“拼写错误”这 1% 的问题,另外 99% 的逻辑错误和性能问题它一个都帮不上。
3. 成本收益严重倒挂
你写一行 SQL:
SELECT * FROM user WHERE age > 18
jOOQ 要写:
context.selectFrom(USER).where(USER.AGE.gt(18)).fetch()
代码长了两倍,还要学一整套 API,还要生成一堆类,还要维护一个代码生成流程。你付出的一切,只是为了把字段名拼错的风险降到最低——而这个风险本来就不高。
4. DSL 能力上限的排序
jOOQ(上限最高)> MyBatis Flex > MyBatis Plus。
jOOQ 能做“全面翻译”,理论上能覆盖 SQL 全部能力。Flex 覆盖联表场景,留了原生 SQL 后门。Plus 只能覆盖单表查询。
但无论哪一种 DSL,只要它还在“翻译”,就永远不可能比“直写 SQL”更简洁、更透明、更容易维护。因为你学 DSL 的逻辑,本质上是在学一套 SQL 的方言。你付出的每一分精力,都是在为“翻译”买单。
第四部分:SimpleDAO——真正的 SQL-First
前面说了这么多“什么是错的”,那什么是对的?
SimpleDAO 的定位很简单:极致的字符串拼接和参数收集。
1. 三条底线
- 不封装关键字:SELECT、FROM、JOIN、GROUP BY 你手写,框架不碰
- 不封装运算符:=、>=、LIKE、IN、EXISTS 你手写,框架不碰
- 不做语法翻:不解析 SQL、不生成 SQL、不改写 SQL
2. 一套心智模型打到底
单表:
UserCond cond = UserCond.builder().name("张").ageMin(18).build();
userDao.page(cond);
联表(同样的 API):
String sql = "SELECT u.*, d.name dept_name FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id";
userDao.page(sql, cond, UserVO.class);
复杂条件(还是同样的 API):
cond.add("AND EXISTS (SELECT 1 FROM bus_order WHERE user_id = u.id)", flag);
cond.add("AND u.age BETWEEN ? AND ?", ageMin, ageMax);
cond.add("AND (u.status = ? OR u.status = ?)", status1, status2);
从单表到联表到子查询到 CTE 到窗口函数,全是这一套。没有 XML、没有 DSL、没有三套语法切换。SQL 还是 SQL,Java 还是 Java。
3. 你写的是 SQL,框架只帮你拼条件和收集参数
这是 SimpleDAO 和所有传统框架最本质的区别:
| 框架 | 做的事情 |
|---|---|
| MyBatis | 在 XML 里写 SQL,用标签控制动态 |
| ORM | 单表对象化,联表躺平 |
| jOOQ | 把 SQL 翻译成 Java 方法链 |
| SimpleDAO | SQL 就是 SQL,放在 Java 里,框架只帮你拼条件和收集参数 |
4. 没有上限
因为数据库只认 SQL,而你写的就是 SQL。
你的能力上限 = SQL 的能力上限 = 无限。
只要是 SQL 合法的语法——BETWEEN、OR、EXISTS、CASE WHEN、窗口函数、递归 CTE——全都能拼进去。框架不拦着你,框架不限制你。
结语
这些年,我见过太多人在框架里绕弯子:
- 学 XML 标签、学 OGNL 表达式、学 resultMap 嵌套
- 学 QueryWrapper 链式调用、学 Lambda 表达式构建条件
- 学 JPQL、HQL、Criteria、QueryDSL……
- 学 jOOQ 那套从数据库生成代码的流程
最终,你发现所有这些学习曲线,都是为了绕开一个你本来就该会的东西:SQL。
SimpleDAO 不让你绕弯子。它的价值从来不是“功能最多、最智能、最花哨”。它的价值是:
“让你在写 SQL 的时候,少写那些重复的 if 和 params.add()。”
仅此而已。但正是这个“仅此而已”,让 SimpleDAO 成为我从 2017 年一直迭代到今天的项目——因为它没有谎言,没有泡沫,没有承诺它做不到的事情。它是一个勤勤恳恳的 SQL 脚手架。
如果你也受够了 XML 的折磨、OR M 的谎言、DSL 的翻译成本,欢迎试试 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
把时间留给 SQL,而不是框架。

15

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



