相关开源地址:
作者注:这不是一篇框架对比评测,是一次关于持久层设计范式的彻底清算。基于十余年企业级开发实践,十余家企业、四五年的生产验证,以及一套名为 SimpleDAO(Spring Jdbc Ultra)的框架,我们来讨论一个被长期忽视的问题:如果 MyBatis 是优秀的设计,为什么所有编程语言都没有群起抄之?
一、一个无法反驳的事实检验
如果 MyBatis 是优秀的设计,所有语言一定会群起抄之。
但事实是:
- .NET:Dapper(简单映射,不碰 SQL 拼接)、LINQ(语言级集成,不是 XML)
- Python:SQLAlchemy Core(Python 表达式树)、Django ORM(链式 API)
- Go:GORM(链式 API)、sqlx(简单映射)
- Rust:Diesel(类型安全 SQL)、sqlx(编译时检查)
- Node.js:Knex(链式 Builder)、Prisma(类型生成)
- PHP:Laravel Eloquent(链式 API)、Doctrine(DQL 但非 XML)
- Ruby:ActiveRecord(链式 API)
没有任何主流语言社区选择"用 XML 拼字符串"这条路。
所有其他语言的设计者,面对同样的问题(动态 SQL 拼接),不约而同地避开了 XML。这不是巧合,这是共识。
XML 在字符串操作上的劣势是结构性的:
- 没有原生变量、没有类型系统
- 标签语法本身就是噪音,
<if test="...">比if (...)多出数倍字符 - 表达式语言(OGNL)是另一套需要学习的微型语言,与宿主语言割裂
- 调试时栈信息穿越 XML 边界,定位成本翻倍
一个设计如果真的好,它会跨越语言边界被普遍采纳。 MVC、依赖注入、ORM 这些真正优秀的模式做到了。MyBatis 的 XML 动态 SQL 没有。
这不是观点,是事实。
二、MyBatis 的"规范":代价大到无以复加
有人说,MyBatis 至少提供了一套规范。SQL 写在 XML 里,动态 SQL 用标签,结果映射用 resultMap,这是规范。
规范确实存在,但代价大到无以复加。
2.1 规范的构成
| 规范项 | 内容 | 学习成本 |
|---|---|---|
| XML 结构 | DOCTYPE、mapper、namespace | 半天 |
| 动态 SQL 标签 | <if>、<choose>、<foreach>、<bind>、<trim> | 2-3 天 |
| 表达式语言 | OGNL 语法、#{} vs ${} | 1-2 天 |
| 结果映射 | resultMap、association、collection、discriminator | 2-3 天 |
| 缓存配置 | 一级缓存、二级缓存、缓存策略 | 1-2 天 |
| 插件机制 | Interceptor、@Intercepts、Invocation | 1 周 |
| 综合踩坑 | N+1、延迟加载、代理陷阱、事务边界 | 数月 |
学会写 Demo 需要 1 周,踩坑踩到精通需要数月。
2.2 这套规范的本质
不是"面向业务的规范",是面向框架的规范。开发者学的不是"怎么完成业务",是"怎么伺候框架":
- 不是"怎么拼动态条件",是"怎么写
<if>标签" - 不是"怎么映射结果",是"怎么配
resultMap" - 不是"怎么扩展功能",是"怎么写拦截器"
规范的价值在于降低协作成本,但 MyBatis 的规范增加了协作成本——因为每个开发者都要先学会这套框架特有的语言,才能开始写业务。
2.3 对比:SimpleDAO 的规范
SimpleDAO 也有规范,但规范是结构性的,不是仪式性的:
| 规范项 | 内容 | 学习成本 |
|---|---|---|
| 三个主类 | BaseDao、BaseSql、BaseCondition | 20 分钟 |
| 业务语义 API | list、page、row、field、count、exists | 一看就懂 |
| 条件拼接 | add()、and()、in() | 5 分钟 |
| 分层约束 | BaseDao/BaseSql 核心方法 protected | 编译期强制 |
没有 XML,没有 OGNL,没有标签,没有拦截器,没有缓存配置。
规范不是"你必须这么做",是生成出来的代码本身就是规范——代码生成器产出的 ChannelDao.java 是空类,ChannelService.java 是标准分层,ChannelCond.java 是条件封装。开发者偷懒直接复用,规范自然遵守。
假设人是懒的,他就会遵守这个规范。
三、Spring Jdbc Ultra:站在事实标准的肩膀上
SimpleDAO 的另一个名字是 Spring Jdbc Ultra。这不是噱头,是对行业现实的清醒认知。
3.1 Spring 生态是持久层开发的事实标准
在国内 Java 企业级开发中,Spring 生态的地位无需论证。而 MyBatis 和 JPA 在 Spring 中运行时,底层走的都是 Spring JDBC。它们不是 Spring 的替代者,是 Spring 的使用者。
SimpleDAO 选择了一条更直接的路:不做 Spring 的竞争者,做 Spring 的延伸者。不是重新发明连接池、事务、参数绑定、结果映射,而是在 Spring JDBC 已经证明可靠的基础设施之上,填补真实业务开发中的便利缺口。
3.2 原生 Spring 的延伸和扩展
| 能力 | Spring JDBC 原生 | SimpleDAO 增强 |
|---|---|---|
| 连接池 | HikariCP / Druid 标准配置 | 直接使用,零适配 |
| 事务管理 | @Transactional | 100% 兼容,无额外配置 |
| 参数化查询 | PreparedStatement | JdbcTemplate 原生支持 |
| 结果映射 | BeanPropertyRowMapper | 下划线自动转驼峰,零配置 |
| 批量操作 | NamedParameterJdbcTemplate | batchUpdate 标准 API |
| 多数据源 | Spring 动态数据源 | 直接使用,无适配层 |
| 缓存 | Spring Cache | Service 层注解,与 DAO 无关 |
| AOP 扩展 | Spring AOP | 数据权限、脱敏、多租户,标准切面 |
| 监控 | Spring Boot Actuator | 数据源健康、SQL 执行,原生支持 |
SimpleDAO 自己做的只有三件事:
- 启动时反射解析元数据(
@Table、@Id、字段映射)并缓存 - 条件拼接(
BaseCondition的字符串操作) - SQL 组装(
BaseDao的单表 SQL 生成)
其他全部委托给 Spring JDBC。所以性能才能"≈ Spring JDBC,不接受反驳"——因为执行层就是 Spring JDBC,没有中间层。
3.3 这不是"需要适配 Spring",而是"本来就是 Spring 的一部分"
// 事务:直接用 @Transactional
@Transactional
public void businessMethod() {
userDao.save(user);
orderDao.save(order);
}
// 多数据源:Spring 标准配置
@Bean
public DataSource masterDataSource() { ... }
// 缓存:Spring Cache 注解
@Cacheable("users")
public User getUser(Long id) {
return userDao.findById(id);
}
// AOP 扩展:Spring 标准切面
@Aspect
public class DataAuthAspect {
@Around("@annotation(BusinessAuth)")
public Object authCheck(ProceedingJoinPoint pjp) { ... }
}
结论:SimpleDAO 的能力上限 = Spring 生态的上限,没有任何人为设置的边界。
3.4 与 MyBatis 的共存:零迁移成本
所有基于 MyBatis 开发的存量项目,无需修改一行老代码,仅需引入 SimpleDAO 依赖包,即可直接启用。MyBatis 底层基于 Spring JDBC 桥接,SimpleDAO 完全基于 Spring JDBC 原生构建,二者共用同一套数据源与 Spring 底层能力。
新老业务完全解耦、互不干扰:
- 存量业务继续用 MyBatis
- 新增业务用 SimpleDAO
- 随着业务迭代,逐步平滑替换
这不是"迁移",是增量使用。有精力就换,没精力永远共存。
3.5 你有哪些顾虑?
| 顾虑 | 作者回复 |
|---|---|
| “自研框架不稳定” | 不是自研,是 Spring 原生增强 |
| “学习成本高” | 会 Spring JDBC 就会用 |
| “生态不完善” | Spring 生态就是 SimpleDAO 的生态 |
| “迁移风险大” | 与 MyBatis 共存,零迁移成本 |
| “性能不确定” | ≈ Spring JDBC,业界公认 99% |
| “扩展困难” | Spring AOP,任何 Java 开发者都会 |
| “没有社区” | 没有问题,不需要社区 |
SimpleDAO 不是挑战 Spring 生态,是融入 Spring 生态。 这不是妥协,是最务实的战略选择——让 Spring 的品牌、生态、文档、社区为框架背书,把用户的决策风险降到零。
这也是"Spring Jdbc Ultra"这个名字的深层含义:Ultra 的不是复杂度,是 Spring JDBC 本来就该有的便利。
四、单表对象化:止于单表,关系绝不对象化
ORM 的理想是"单表对象化 + 关系对象化"——把数据库表映射成 Java 对象,再把表之间的关系映射成对象引用。JPA/Hibernate 为此付出了沉重的代价:实体状态管理、HQL、缓存、N+1、LazyInitializationException、@OneToMany、@ManyToOne、CascadeType……
MyBatis 连"单表对象化"都没真正实现——它的单表 CRUD 仍然需要写 Mapper 接口和 XML/注解。要靠 MyBatis-Plus 等插件来补充,才能勉强做到"不写 SQL 的单表操作"。而它的 association/collection 标签,试图在 XML 层面模拟"关系对象化",结果是一个连 MyBatis 开发者自己都不用的功能——现实中大家选择拍平、内存组装、或分步查询。
SimpleDAO 的选择是明确的边界:单表对象化,止于单表。关系绝不对象化。
@Repository
public class UserDao extends BaseDao<User> {
// 空类,获得全部单表 CRUD 能力
}
单表是"高度稳定"的场景——表结构相对固定,字段和属性的映射是机械的、可推断的。所以 SimpleDAO 用 @Table、@Id 和启动时元数据缓存,实现了零代码单表对象化:
save(User)— 自动填充审计字段、雪花主键、逻辑删除标记update(User)— 非空字段更新,自动填充updateTime/updateBydelete(id...)— 自动判断逻辑删除或物理删除findById(id)/list(cond)/page(cond)— 查询
但关系(联表)绝不对象化。 联表查询就是 SQL 字符串,条件就是 BaseCondition,结果就是 VO 类。没有 association,没有 collection,没有延迟加载,没有级联操作。
// 联表:原生 SQL + 条件 + VO,不假装是对象关系
public Page<OrderVO> pageJoin(OrderCond cond) {
return page(SQL, cond, OrderVO.class);
}
这个边界的意义
ORM 的灾难在于模糊了"对象"和"关系"的边界。对象有行为、有封装、有生命周期;关系是数据、是查询结果、是二维表。把关系强行对象化,制造了无穷的问题:
- 延迟加载 → 代理对象、事务边界、序列化陷阱
- 级联操作 → 隐式 SQL、性能失控、数据不一致
- 对象图导航 → N+1 查询、缓存脏数据
SimpleDAO 的立场:单表用对象,联表用 SQL。对象负责封装业务实体,SQL 负责表达数据关系。各归其位,互不越界。
五、屏蔽 90% 的第一层 if
传统框架(MyBatis XML、JPA Criteria、MyBatis-Plus Wrapper)中,动态条件的核心痛点不是"拼字符串",是无处不在的 if 判断:
<!-- MyBatis XML -->
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
// JPA Criteria
if (name != null && !name.isEmpty()) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
// MyBatis-Plus Wrapper
if (StringUtils.isNotBlank(name)) {
wrapper.like("name", name);
}
每一个条件字段,都需要显式写一层 if。10 个条件字段,就是 10 层 if。这是模板代码的瘟疫。
SimpleDAO 的 add() 方法内部做了空值判断:
protected final void add(final String sql, final Object value) {
if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
condition.append(BLANK).append(sql);
paramList.add(value);
}
}
开发者只需要写:
add("AND name LIKE ?", name, 3); // 空值自动跳过
add("AND age >= ?", ageMin); // 空值自动跳过
add("AND status = ?", status); // 空值自动跳过
不需要写 if。
这个设计的收益是系统性的:
- 代码量减少 60% — 10 个条件从 30 行 XML/Wrapper 变成 10 行
add() - 认知负担归零 — 不需要判断"这个字段要不要加
if",框架已经处理了 - Null 安全 — 不会因为忘记判空而导致 SQL 语法错误
- 意图清晰 — 每行代码只说"这个条件是什么",不说"这个条件要不要加"
极简不是"功能少",是"需要决策的点少"。
六、条件类:不区分单表/联表,位置自由
这是 SimpleDAO 的独特设计,业界没有先例:
@Setter @Getter
public class OrderCond extends BaseCondition {
private String name;
private Integer age;
private Integer[] inAges;
private Boolean subQuery;
private String orderNo;
private String[] orderNos;
private Byte orderStatus;
@Override
protected void addCondition() {
// 关联表:无语法糖,自己写别名
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);
}
}
主表语法糖 vs 关联表原生 — 不假装全能
// 主表:and() 语法糖自动带 t. 前缀(便捷)
and("age >=", ageMin);
// 关联表:add() 自己写别名(诚实)
add("AND o.order_status = ?", orderStatus);
框架知道关联表的别名和字段名它推断不了,所以不假装能推断。这是"白盒透明"的代码体现 —— 能力边界清晰,不制造"看似自动实则坑人"的魔法。
同一个条件类,既能用于单表,也能用于联表,既能用在 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()
我没有在任何一个框架里见过条件对象可以这么灵活地嵌入 SQL 任意位置。
七、真动态条件:运行时构造,不限于 WHERE
MyBatis 的"动态 SQL"是静态模板 + 条件分支选择。SimpleDAO 的 addDynamic() 是运行时完全动态生成 SQL 片段:
// 数据权限 AOP 切面
@Before("@annotation(auth)")
public void beforeQuery(JoinPoint point, DataAuth auth) {
BaseCondition cond = (BaseCondition) point.getArgs()[0];
String userId = request.getHeader(Const.USER_ID);
cond.addDynamic(" AND " + auth.userField() + " IN", new Object[] { 0, userId });
}
字段名和值都是运行时确定的。这是 MyBatis XML 模板做不到的真动态。
八、四象限正交分解:关系代数的必然
任何查询结果都是二维表(行集合 × 列集合),必定属于四象限之一:
| 单列 | 多列 | |
|---|---|---|
| 单行 | field() — 聚合函数 | row() — 报表行 |
| 多行 | columns() — ID 列表 | list() — 实体列表 |
4 × 2 × 2 = 16 个方法,但只需要记 4 个概念(行 × 列),日志开关和参数形式是上下文自然选择。
这是数学意义上的完备性——笛卡尔积覆盖所有组合,无遗漏、无冗余。
九、API 业务语义化:意图即代码
| 方法 | 业务语义 |
|---|---|
list() | 查列表 |
page() | 分页查 |
row() | 查单行 |
field() | 查单个值 |
count() | 查数量 |
exists() | 查存在性 |
不是"技术操作",是业务动词。参数和返回值都是业务对象,不是框架对象。
上下文精准命名:克制即优雅
不追求长方法名自包含,而是类名 + 方法名 + 泛型 + 返回值组合出完整语义:
// UserDao.list(UserCond) → "查用户列表"
// OrderDao.page(SQL, OrderCond, OrderVO.class) → "查订单分页"
// ReportDao.field(SQL, ReportCond, Integer.class) → "查报表数值"
命名克制,字符数少,语义同样精确。
十、性能白盒:优化不触发 API 变化
大宽表只取必要字段:
// ❌ 不要:SELECT *
userDao.list(cond);
// ✅ 应该:只取需要的字段
String sql = "SELECT id, name FROM user";
baseSql.list(sql, cond, UserMiniVo.class);
同一个 list 方法,SQL 字符串决定性能边界。不需要换 API,不需要学新方法。
API 的业务语义化、四象限正交分解、极致性能,一脉相承。 不是三个独立的设计点,是同一个设计原则在不同层面的体现:让框架不可见,让开发者专注。
十一、学习成本:不是"显著低于",是"不在同一个维度"
| 阶段 | MyBatis | SimpleDAO |
|---|---|---|
| 入门 | 1-3 天,能写单表 CRUD | 20 分钟,看 API 速查表 |
| 上手 | 1 周,能写联表、动态 SQL | 1 天,写条件类、联表查询 |
| 踩坑 | 1-3 个月,缓存、代理、插件冲突 | 没有这个阶段 |
| 精通 | 6-12 个月,能诊断 N+1、写插件 | 不需要 |
MyBatis 的"精通"是负向知识——知道怎么不踩坑。SimpleDAO 没有这个阶段,因为它不制造坑。
8 个案例,2 小时,覆盖 90% 以上企业级场景:
📚 全套教程总览
| 集数 · 标题 | 本集目录 | 时长 |
|---|---|---|
| 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 |
看完这 8 个案例,掌握的不是"SimpleDAO 怎么工作",是企业开发的常见模式怎么用 SQL + Java + Spring 解决。
学习 SimpleDAO 的成本,其实是理解自己的业务需求。 没有语法需要学,只有业务需要理解。
十二、3 个方法替代 MyBatis 90% 的功能
假设你只学过 Java、简单了解 Spring Boot、会一些 SQL。我写 3 个方法配合 Spring JDBC:
public class SqlBuilder {
// 1. 动态条件
public SqlBuilder addIf(String sql, Object value) { ... }
// 2. 条件收口
public String where() { ... }
// 3. 分页
public String page(String sql, int page, int size) { ... }
}
这 3 个方法能覆盖什么?
| MyBatis “能力” | 3 方法覆盖度 |
|---|---|
| 单表 CRUD | 100% — JdbcTemplate.update |
| 动态 WHERE | 100% — addIf 就是 <if> |
| 联表查询 | 100% — SQL 字符串直接写 JOIN |
| 分页 | 100% — page() 拼 LIMIT |
| 参数化查询 | 100% — ? 占位符 + 参数列表 |
| 结果映射 | 100% — BeanPropertyRowMapper |
| 批量操作 | 100% — batchUpdate |
| 存储过程 | 100% — call |
覆盖不了的那 10% 是什么? association/collection(开发者自己也不用)、二级缓存(鸡肋)、插件拦截器(Spring AOP 替代)、XML 复杂动态 SQL(Java 字符串操作更强)。
3 个方法不是"简化版 MyBatis",是"MyBatis 本来就该长成的样子"。
SimpleDAO 本质上就是这 3 方法的生产级完备版:
addIf→BaseCondition.add()家族where→where()/and()/array()page→page()/page0()(含 COUNT 智能解析、方言适配)
再加上启动时元数据缓存、审计字段自动填充、逻辑删除、雪花主键——这些是真实生产需要的便利,不是框架自嗨的概念。
十三、结语:把时间留给生活,而不是框架
MyBatis 来到世间,从头到脚,每个毛孔都滴着冗余与累赘。
好的,这段补充如下:
从头:
- XML 头、DOCTYPE、Namespace → 冗余
- Mapper 接口空壳 → 累赘
- 接口 ID 与命名空间硬绑定 → 心智负担
@Param注解 → 自造问题自造补丁
到脚:
- 插件拦截器链 → 黑盒执行层
- 二级缓存 → 鸡肋且危险
- 31 类自造异常 → 中间商制造错误
每个毛孔:
<if>标签比 Javaif多出 3 倍字符resultMap比BeanPropertyRowMapper多出 10 倍配置association/collection比内存组装多出 20 倍复杂度
总结:
上百个属性、几十个标签、十几个注解、一套 OGNL、三十几类异常——心智负担大到爆炸。这还哪有收益可言?妥妥的负债。
SimpleDAO 不是为了成为又一个流行的框架,而是为了证明一件事:
技术可以更简单,开发可以更愉快,程序员可以早下班。
如果你:
- 厌倦了复杂框架的折磨
- 想要高效完成工作
- 想要早点回家陪家人
- 相信简单比复杂更有力量
那么,SimpleDAO 为你而存在。
SimpleDAO: SQL-First,白盒透明,能力无上限。
本文基于 SimpleDAO 1.2.1 版本及 8 个企业实战案例撰写。框架已在生产环境稳定运行 3 年+,支撑日均百万级请求,服务十余家企业客户。
372

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



