面向业务设计持久层框架 Spring Jdbc Ultra
持久层的设计哲学从 MyBatis 的系统性缺陷到 Spring Jdbc Ultra 的范式革命作者注本文不是框架使用教程是一次关于持久层设计范式的深度反思。基于十余年企业级开发实践十余家企业、四五年的生产验证以及一套名为 SimpleDAOSpring 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三种语言耦合动态 SQLJava 更优— 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 的做法SetterGetterpublicclassOrderCondextendsBaseCondition{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 dr0));// 动态子查询add(AND t.id IN (SELECT user_id FROM bus_order WHERE dr0),subQuery);}}2.2 缺陷二Mapper 接口 — 为了接口而接口接口的价值在于多态和抽象。但 MyBatis 的 Mapper 接口永远只对应一个 XML/注解实现方法签名和 SQL 是 1:1 硬绑定增加的是命名空间匹配、方法名同步、IDE 跳转断裂的成本这不是面向接口编程是框架实现细节的外溢。SimpleDAO 的做法直接继承BaseDaoT零代码无伪抽象。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.ordersGROUP BY / HAVING中度动态BaseCondition嵌入 HAVINGSELECT 字段高度稳定手写 SQL框架不干预FROM 表名高度稳定手写 SQL框架不干预三个主类的分工完全对应这个梯度BaseCondition— 负责高度动态部分BaseSql— 负责中度动态部分执行、分页BaseDao— 负责高度稳定部分的自动化单表 CRUD好的这段建议插在3.1 核心抽象SQL 结构的动态梯度之后作为3.2 单表对象化零代码 CRUD3.2 单表对象化零代码 CRUDORM 的理想是单表对象化——把数据库表映射成 Java 对象CRUD 操作像操作对象一样自然。JPA/Hibernate 为此付出了沉重的代价复杂的实体状态管理、HQL、缓存、N1 问题而 MyBatis 连这个理想都没真正实现——它的单表 CRUD 仍然需要写 Mapper 接口和 XML/注解。SimpleDAO 实现了真正的单表对象化而且代价趋近于零RepositorypublicclassUserDaoextendsBaseDaoUser{// 空类获得全部 CRUD 能力}一个空类零代码获得save(User)— 自动填充审计字段、雪花主键、逻辑删除标记update(User)— 非空字段更新自动填充updateTime/updateBydelete(id...)— 自动判断逻辑删除或物理删除findById(id)— 按主键查询list(cond)— 按条件查询列表page(cond)— 按条件分页查询这不是代码生成器生成的模板代码是框架运行时自动推断Table(sys_user)→ 表名Id→ 主键名和类型字段名驼峰→ 自动转下划线createTime/createBy/updateTime/updateBy→ 自动审计dr→ 自动逻辑删除单表是联表的特例不是独立的世界BaseDao的底层就是BaseSqlpublicCextendsBaseConditionPageTpage(booleanshow,finalCc){StringsqlSql.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 XMLiftestname ! 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 11cond.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 11 cond.and()3.6 真动态条件运行时构造不限于 WHEREMyBatis 的动态 SQL是静态模板 条件分支选择。SimpleDAO 的addDynamic()是运行时完全动态生成 SQL 片段// 数据权限 AOP 切面Before(annotation(auth))publicvoidbeforeQuery(JoinPointpoint,DataAuthauth){BaseConditioncond(BaseCondition)point.getArgs()[0];StringuserIdrequest.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);// ✅ 应该只取需要的字段StringsqlSELECT 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.batchUpdateAOP 扩展Spring 的标准切面SimpleDAO 自己做的只有三件事启动时反射解析元数据Table、Id、字段映射并缓存条件拼接BaseCondition的字符串操作SQL 组装BaseDao的单表 SQL 生成4.2 全是收益零成本站在 Spring JDBC 的基准线上SimpleDAO 的每一个增强都是纯收益Spring JDBC 原生SimpleDAO 增强收益手写INSERT/UPDATE/DELETEsave(t)/update(t)/delete(id)零代码单表 CRUD手动处理null字段update()/updateNull()区分语义明确手写WHERE条件BaseCondition.add()类型安全参数化手写COUNTLIMITpage()一行代码智能 COUNT手动设置审计字段自动填充不侵入业务手动处理逻辑删除delete()自动判断统一策略4.3 能力上限 SQL 的上限 Spring 的上限SQL 能力无上限窗口函数、CTE、递归查询、存储过程……只要数据库支持SimpleDAO 就能执行Spring 生态扩展无上限事务、多数据源、缓存、AOP、监控……100% 原生性能 ≈ Spring JDBC启动时反射一次并缓存运行时零反射联表完全不读元数据五、与 MyBatis 的终极对比维度MyBatisSimpleDAO设计哲学面向数据库面向业务SQL 控制权半遮半掩XML 白盒执行黑盒完全白盒动态 SQLXML 标签 OGNLJava 原生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