面向业务设计持久层框架 Spring Jdbc Ultra
持久层的设计哲学:从 MyBatis 的系统性缺陷到 Spring Jdbc Ultra 的范式革命
作者注:本文不是框架使用教程,是一次关于持久层设计范式的深度反思。基于十余年企业级开发实践,十余家企业、四五年的生产验证,以及一套名为 SimpleDAO(Spring Jdbc Ultra)的框架,我们来讨论一个被长期忽视的问题:持久层框架到底应该面向数据库设计,还是面向业务设计?
一、问题的提出:MyBatis 为什么流行?
MyBatis 是国内 Java 生态中最流行的持久层框架之一。但流行不等于正确。我们需要追问:它的流行是技术选择的结果,还是认知失调、历史包袱和惯性使然?
1.1 MyBatis 的"优势"说辞
互联网上为 MyBatis 辩护的常见论点:
- SQL 与代码分离,便于维护
- 动态 SQL 强大灵活
- 结果自动映射
- 缓存机制
- 插件扩展机制
- Mapper 接口,IDE 有代码提示
- XML 语法高亮
- 批量操作更优雅
- 存储过程调用支持更好
- 与 Spring 集成后事务管理更方便
1.2 逐一检验:站在 Spring JDBC 的参照系上
| 说辞 | 检验结论 |
|---|---|
| SQL 与代码分离 | 伪优势— 分离的是文件,不是关注点。XML 里 1/3 表达式、1/3 标签、1/3 SQL,三种语言耦合 |
| 动态 SQL | Java 更优— Java 字符串操作能力远强于 XML 标签 |
| 自动映射 | Spring JDBC 更优—BeanPropertyRowMapper零配置,MyBatis 的resultMap是手工配置假装自动 |
| 缓存机制 | 鸡肋— 用户第一件事就是关闭缓存 |
| 插件扩展 | Spring AOP 更优— 白盒扩展 vs 黑盒拦截器 |
| Mapper 接口 | 伪抽象— 无多实现场景,纯为框架代理机制服务 |
| XML 语法高亮 | 无关— Java Text Block 同样有 SQL 高亮 |
| 批量操作 | Spring JDBC 更直接 |
| 存储过程 | 平手 |
| Spring 事务 | 平手— 两者都是 Spring 事务 |
结论:站在 Spring JDBC 的参照系上,MyBatis 没有任何站得住脚的优势。
二、MyBatis 的系统性缺陷
MyBatis 不是"有一点毛病",是全身都是毛病。这些毛病不是设计失误,是范式层面的错误。
2.1 缺陷一:XML 拼字符串 — 用残缺的标签语言做 Java 原生就擅长的事
“你撕开 XML 那层皮,里面到底是什么?不还是一坨低级的字符串拼接吗?”
Java 里有replace、format、正则、流式处理,能把字符串玩出花来。XML 有什么?XML 连个像样的循环和字符串截取都写得费劲。
动态 SQL 的灵魂在于"逻辑",不在于"标签"。为什么换个文件,用一堆又臭又长的标签去干 Java 一行代码搞定的事,就叫高级了?
SimpleDAO 的做法:
@Setter@GetterpublicclassOrderCondextendsBaseCondition{privateStringname;privateIntegerage;privateInteger[]inAges;privateBooleansubQuery;privateStringorderNo;privateString[]orderNos;privateByteorderStatus;@OverrideprotectedvoidaddCondition(){// 关联表:无语法糖,自己写别名add("AND o.order_status = ?",orderStatus);add("AND o.order_no LIKE ?",orderNo,3);add("AND o.order_no IN ",orderNos);// IN 条件(主表同样适用)add("AND t.age IN ",inAges);// 静态子查询add("AND t.id IN (SELECT user_id FROM bus_order WHERE dr=0)");// 动态子查询add("AND t.id IN (SELECT user_id FROM bus_order WHERE dr=0)",subQuery);}}2.2 缺陷二:Mapper 接口 — 为了接口而接口
接口的价值在于多态和抽象。但 MyBatis 的 Mapper 接口:
- 永远只对应一个 XML/注解实现
- 方法签名和 SQL 是 1:1 硬绑定
- 增加的是命名空间匹配、方法名同步、IDE 跳转断裂的成本
这不是"面向接口编程",是框架实现细节的外溢。
SimpleDAO 的做法:直接继承BaseDao<T>,零代码,无伪抽象。
2.3 缺陷三:SQL 编写白盒,SQL 执行黑盒
MyBatis 把 SQL 编写设计成白盒(你能看到 XML),但 SQL 执行设计成黑盒(拦截器、代理链、缓存层)。
想加个数据权限?写拦截器。想加个脱敏?写拦截器。那拦截器是普通人能搞定的吗?80% 的程序员写 20 行代码,连业务的边都摸不着,全在跟框架内部对象搏斗。
SimpleDAO 的做法:全是白盒。AOP 直接拦截参数和返回值,10 分钟搞定。
2.4 缺陷四:补坑生态 — 受害者联盟
“传统框架生态繁荣?那是补坑生态!”
为什么 Spring JDBC 没人讨论?因为它没坑,白盒到底。传统框架呢?SQL 编写白盒,SQL 执行黑盒。它自己制造了坑,然后让开发者写插件来填坑。你在为框架的设计缺陷买单。
SimpleDAO 的做法:无坑,无需生态。
2.5 缺陷五:单表思维 — 背离 RDBMS 的本质
RDBMS,第一个单词就是Relation(关系)。企业级开发中,单表场景占比极低,越庞大的系统比例越小。MyBatis-Plus 所谓的"单表解决 80% 业务",是行业最大的谎言之一。
真实的企业开发,核心是多表联动。MyBatis-Plus 的单表 Wrapper,到了联表场景完全失能,只能退回 XML。
SimpleDAO 的做法:联表是常态,单表是特例。BaseDao只是BaseSql的预配置,底层完全统一。
2.6 缺陷六:31 类自造异常 — 中间商制造错误
Spring JDBC 只有 2-3 类异常。MyBatis 有 31 类。你花时间学的不是"数据库出了什么问题",而是"框架的哪一层又炸了"。
SimpleDAO 的做法:基于 Spring JDBC,异常就是数据库真实的异常,没有中间商赚差价,更没有中间商制造错误。
2.7 缺陷七:七层执行链路 — 性能损耗
业界公认的性能排名:原生 JDBC 100%,Spring JDBC 99%,MyBatis 95%。那 5% 差在哪?就差在那多出来的四五层抽象、动态代理、反射调用上。
SimpleDAO 的做法:三层到底,直通数据库,性能 ≈ Spring JDBC。
三、SimpleDAO 的设计哲学
SimpleDAO 不是"另一个 ORM",是对 ORM 范式的根本否定。它的设计哲学可以概括为一句话:
面向业务设计,而非面向数据库设计。
3.1 核心抽象:SQL 结构的动态梯度
SimpleDAO 对 SQL 结构做了关键抽象:不同位置的动态程度不同。
| SQL 位置 | 动态程度 | SimpleDAO 设计 |
|---|---|---|
| WHERE | 高度动态 | BaseCondition— 核心抽象 |
| 分页 (LIMIT/OFFSET) | 高度动态 | page()/page0()— 自动处理 |
| JOIN / ON | 中度动态 | BaseCondition嵌入任意位置 |
| ORDER BY | 中度动态 | BaseCondition.orders |
| GROUP BY / HAVING | 中度动态 | BaseCondition嵌入 HAVING |
| SELECT 字段 | 高度稳定 | 手写 SQL,框架不干预 |
| FROM 表名 | 高度稳定 | 手写 SQL,框架不干预 |
三个主类的分工完全对应这个梯度:
BaseCondition— 负责"高度动态"部分BaseSql— 负责"中度动态"部分(执行、分页)BaseDao— 负责"高度稳定"部分的自动化(单表 CRUD)
好的,这段建议插在"3.1 核心抽象:SQL 结构的动态梯度"之后,作为"3.2 单表对象化:零代码 CRUD":
3.2 单表对象化:零代码 CRUD
ORM 的理想是"单表对象化"——把数据库表映射成 Java 对象,CRUD 操作像操作对象一样自然。JPA/Hibernate 为此付出了沉重的代价(复杂的实体状态管理、HQL、缓存、N+1 问题),而 MyBatis 连这个理想都没真正实现——它的单表 CRUD 仍然需要写 Mapper 接口和 XML/注解。
SimpleDAO 实现了真正的单表对象化,而且代价趋近于零:
@RepositorypublicclassUserDaoextendsBaseDao<User>{// 空类,获得全部 CRUD 能力}一个空类,零代码,获得:
save(User)— 自动填充审计字段、雪花主键、逻辑删除标记update(User)— 非空字段更新,自动填充updateTime/updateBydelete(id...)— 自动判断逻辑删除或物理删除findById(id)— 按主键查询list(cond)— 按条件查询列表page(cond)— 按条件分页查询
这不是"代码生成器生成的模板代码",是框架运行时自动推断:
@Table("sys_user")→ 表名@Id→ 主键名和类型- 字段名(驼峰)→ 自动转下划线
createTime/createBy/updateTime/updateBy→ 自动审计dr→ 自动逻辑删除
单表是联表的特例,不是独立的世界
BaseDao的底层就是BaseSql:
public<CextendsBaseCondition>Page<T>page(booleanshow,finalCc){Stringsql=Sql.builder().select().fields(fields).from().table(table).as().sql();returnpage(show,sql,c,clazz);}自动生成单表 SQL,然后调用BaseSql.page()。单表和联表共享同一套执行机制,没有"单表用一套 API、联表退回另一套"的认知割裂。
零代码不是魔法,是元数据缓存
启动时反射解析一次实体类的@Table、@Id、字段列表,缓存到BaseDao实例中。运行时零反射,性能无损。
这是"单表对象化"的正确实现方式:编译期零侵入(只有注解),运行期零反射(启动时缓存),开发期零代码(空类继承)。
3.3 四象限正交分解:关系代数的必然
任何查询结果都是二维表(行集合 × 列集合),必定属于四象限之一:
| 单列 | 多列 | |
|---|---|---|
| 单行 | field()— 聚合函数 | row()— 报表行 |
| 多行 | columns()— ID 列表 | list()— 实体列表 |
4 × 2 × 2 = 16 个方法,但只需要记4 个概念(行 × 列),日志开关和参数形式是上下文自然选择。
这是数学意义上的完备性——笛卡尔积覆盖所有组合,无遗漏、无冗余。
3.4 条件类:极简的动态条件构造
SimpleDAO 屏蔽了占比 90%+ 的第一层if。
传统框架(MyBatis XML、JPA Criteria、MyBatis-Plus Wrapper)中,动态条件的核心痛点不是"拼字符串",是无处不在的if判断:
// MyBatis XML<iftest="name != null and name != ''">ANDnameLIKECONCAT('%',#{name},'%')</if>// JPA Criteriaif(name!=null&&!name.isEmpty()){predicates.add(cb.like(root.get("name"),"%"+name+"%"));}// MyBatis-Plus Wrapperif(StringUtils.isNotBlank(name)){wrapper.like("name",name);}每一个条件字段,都需要显式写一层if。10 个条件字段,就是 10 层if。这是模板代码的瘟疫。
SimpleDAO 的add()方法内部做了空值判断:
protectedfinalvoidadd(finalStringsql,finalObjectvalue){if(Objects.nonNull(value)&&StringUtils.hasText(value.toString())){condition.append(BLANK).append(sql);paramList.add(value);}}开发者只需要写:
add("AND t.name LIKE ?",name,3);// 空值自动跳过add("AND t.age >= ?",ageMin);// 空值自动跳过add("AND t.status = ?",status);// 空值自动跳过不需要写if。
这个设计的收益是系统性的:
- 代码量减少 60%— 10 个条件从 30 行 XML/Wrapper 变成 10 行
add() - 认知负担归零— 不需要判断"这个字段要不要加
if",框架已经处理了 - Null 安全— 不会因为忘记判空而导致 SQL 语法错误
- 意图清晰— 每行代码只说"这个条件是什么",不说"这个条件要不要加"
这 90%+ 的if不是"被优化掉了",是被框架吸收了。开发者只关注那 10% 真正需要业务逻辑控制的动态条件,其余全部交给框架的默认行为。
极简不是"功能少",是"需要决策的点少"。
3.5 条件类:不区分单表/联表,位置自由
这是 SimpleDAO 的独特设计,业界没有先例:
// 同一个条件类,既能用于单表,也能用于联表// 既能用在 WHERE 后,也能用在 ON 后、HAVING 后、甚至子查询里// WHERE 后"SELECT ... FROM a WHERE 1=1"+cond.where()// ON 后"SELECT ... FROM a JOIN b ON a.id = b.a_id "+cond.and()// 子查询里"SELECT ... FROM a WHERE id IN (SELECT id FROM b WHERE "+cond.where()+")"// HAVING 后"SELECT ... GROUP BY ... HAVING 1=1 "+cond.and()3.6 真动态条件:运行时构造,不限于 WHERE
MyBatis 的"动态 SQL"是静态模板 + 条件分支选择。SimpleDAO 的addDynamic()是运行时完全动态生成 SQL 片段:
// 数据权限 AOP 切面@Before("@annotation(auth)")publicvoidbeforeQuery(JoinPointpoint,DataAuthauth){BaseConditioncond=(BaseCondition)point.getArgs()[0];StringuserId=request.getHeader(Const.USER_ID);cond.addDynamic(" AND "+auth.userField()+" IN",newObject[]{0,userId});}字段名和值都是运行时确定的,这是 MyBatis XML 模板做不到的真动态。
3.7 API 业务语义化:意图即代码
| 方法 | 业务语义 |
|---|---|
list() | 查列表 |
page() | 分页查 |
row() | 查单行 |
field() | 查单个值 |
count() | 查数量 |
exists() | 查存在性 |
不是"技术操作",是业务动词。参数和返回值都是业务对象,不是框架对象。
3.8 上下文精准命名:克制即优雅
不追求长方法名自包含,而是类名 + 方法名 + 泛型 + 返回值组合出完整语义:
// UserDao.list(UserCond) → "查用户列表"// OrderDao.page(SQL, OrderCond, OrderVO.class) → "查订单分页"// ReportDao.field(SQL, ReportCond, Integer.class) → "查报表数值"命名克制,字符数少,语义同样精确。
3.9 性能白盒:优化不触发 API 变化
大宽表只取必要字段:
// ❌ 不要:SELECT *userDao.list(cond);// ✅ 应该:只取需要的字段Stringsql="SELECT id, name FROM user";baseSql.list(sql,cond,UserMiniVo.class);同一个list方法,SQL 字符串决定性能边界。不需要换 API,不需要学新方法。
四、SimpleDAO 与 Spring JDBC 的关系
4.1 “Spring Jdbc Ultra” 的定位
SimpleDAO 不是"又一个框架",是Spring JDBC 的原生功能延伸与增强。
- 连接池?Spring 的
- 事务?Spring 的
@Transactional - 参数化查询?
JdbcTemplate/NamedParameterJdbcTemplate - 结果映射?
BeanPropertyRowMapper - 批量操作?
NamedParameterJdbcTemplate.batchUpdate - AOP 扩展?Spring 的标准切面
SimpleDAO 自己做的只有三件事:
- 启动时反射解析元数据(
@Table、@Id、字段映射)并缓存 - 条件拼接(
BaseCondition的字符串操作) - SQL 组装(
BaseDao的单表 SQL 生成)
4.2 全是收益,零成本
站在 Spring JDBC 的基准线上,SimpleDAO 的每一个增强都是纯收益:
| Spring JDBC 原生 | SimpleDAO 增强 | 收益 |
|---|---|---|
手写INSERT/UPDATE/DELETE | save(t)/update(t)/delete(id) | 零代码单表 CRUD |
手动处理null字段 | update()/updateNull()区分 | 语义明确 |
手写WHERE条件 | BaseCondition.add() | 类型安全,参数化 |
手写COUNT+LIMIT | page() | 一行代码,智能 COUNT |
| 手动设置审计字段 | 自动填充 | 不侵入业务 |
| 手动处理逻辑删除 | delete()自动判断 | 统一策略 |
4.3 能力上限 = SQL 的上限 = Spring 的上限
- SQL 能力无上限:窗口函数、CTE、递归查询、存储过程……只要数据库支持,SimpleDAO 就能执行
- Spring 生态扩展无上限:事务、多数据源、缓存、AOP、监控……100% 原生
- 性能 ≈ Spring JDBC:启动时反射一次并缓存,运行时零反射,联表完全不读元数据
五、与 MyBatis 的终极对比
| 维度 | MyBatis | SimpleDAO |
|---|---|---|
| 设计哲学 | 面向数据库 | 面向业务 |
| SQL 控制权 | 半遮半掩(XML 白盒,执行黑盒) | 完全白盒 |
| 动态 SQL | XML 标签 + OGNL | Java 原生if |
| 联表查询 | 标签地狱,SQL 切碎 | 完整 SQL 直写 |
| 结果映射 | resultMap,写两遍 | BeanPropertyRowMapper,零配置 |
| 扩展机制 | 拦截器,高门槛 | AOP,零门槛 |
| 学习成本 | 数周到数月(含踩坑) | 2 小时(8 个案例) |
| 代码量 | 基准 | 1/3 ~ 1/4 |
| 性能 | 95% | 99% |
| 异常体系 | 31 类自造异常 | 仅数据库真实异常 |
| 执行链路 | 七层 | 三层 |
| 生态本质 | 受害者联盟,补坑生态 | 无坑,无需生态 |
六、结语:把时间留给生活,而不是框架
SimpleDAO 不是为了成为又一个流行的框架,而是为了证明一件事:
技术可以更简单,开发可以更愉快,程序员可以早下班。
如果你:
- 厌倦了复杂框架的折磨
- 想要高效完成工作
- 想要早点回家陪家人
- 相信简单比复杂更有力量
那么,SimpleDAO 为你而存在。
SimpleDAO: SQL-First,白盒透明,能力无上限。
本文基于 SimpleDAO 1.2.1 版本及 8 个企业实战案例撰写。框架已在生产环境稳定运行 3 年+,支撑日均百万级请求,服务十余家企业客户。
相关开源地址:
- 核心框架源码: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