相关开源地址

  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

作者注:这不是一篇框架对比评测,是一次关于持久层设计范式的彻底清算。基于十余年企业级开发实践,十余家企业、四五年的生产验证,以及一套名为 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 结构 DOCTYPEmappernamespace 半天
动态 SQL 标签 <if><choose><foreach><bind><trim> 2-3 天
表达式语言 OGNL 语法、#{} vs ${} 1-2 天
结果映射 resultMapassociationcollectiondiscriminator 2-3 天
缓存配置 一级缓存、二级缓存、缓存策略 1-2 天
插件机制 Interceptor@InterceptsInvocation 1 周
综合踩坑 N+1、延迟加载、代理陷阱、事务边界 数月

学会写 Demo 需要 1 周,踩坑踩到精通需要数月。

2.2 这套规范的本质

不是"面向业务的规范",是面向框架的规范。开发者学的不是"怎么完成业务",是"怎么伺候框架":

  • 不是"怎么拼动态条件",是"怎么写 <if> 标签"
  • 不是"怎么映射结果",是"怎么配 resultMap"
  • 不是"怎么扩展功能",是"怎么写拦截器"

规范的价值在于降低协作成本,但 MyBatis 的规范增加了协作成本——因为每个开发者都要先学会这套框架特有的语言,才能开始写业务。

2.3 对比:SimpleDAO 的规范

SimpleDAO 也有规范,但规范是结构性的,不是仪式性的

规范项 内容 学习成本
三个主类 BaseDaoBaseSqlBaseCondition 20 分钟
业务语义 API listpagerowfieldcountexists 一看就懂
条件拼接 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 自己做的只有三件事:

  1. 启动时反射解析元数据(@Table@Id、字段映射)并缓存
  2. 条件拼接(BaseCondition 的字符串操作)
  3. 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@ManyToOneCascadeType……

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/updateBy
  • delete(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 方法的生产级完备版

  • addIfBaseCondition.add() 家族
  • wherewhere() / and() / array()
  • pagepage() / page0()(含 COUNT 智能解析、方言适配)

再加上启动时元数据缓存、审计字段自动填充、逻辑删除、雪花主键——这些是真实生产需要的便利,不是框架自嗨的概念。


十三、结语:把时间留给生活,而不是框架

MyBatis 来到世间,从头到脚,每个毛孔都滴着冗余与累赘。

好的,这段补充如下:


从头:

  • XML 头、DOCTYPE、Namespace → 冗余
  • Mapper 接口空壳 → 累赘
  • 接口 ID 与命名空间硬绑定 → 心智负担
  • @Param 注解 → 自造问题自造补丁

到脚:

  • 插件拦截器链 → 黑盒执行层
  • 二级缓存 → 鸡肋且危险
  • 31 类自造异常 → 中间商制造错误

每个毛孔:

  • <if> 标签比 Java if 多出 3 倍字符
  • resultMapBeanPropertyRowMapper 多出 10 倍配置
  • association/collection 比内存组装多出 20 倍复杂度

总结:
上百个属性、几十个标签、十几个注解、一套 OGNL、三十几类异常——心智负担大到爆炸。这还哪有收益可言?妥妥的负债


SimpleDAO 不是为了成为又一个流行的框架,而是为了证明一件事:

技术可以更简单,开发可以更愉快,程序员可以早下班。

如果你:

  • 厌倦了复杂框架的折磨
  • 想要高效完成工作
  • 想要早点回家陪家人
  • 相信简单比复杂更有力量

那么,SimpleDAO 为你而存在。

SimpleDAO: SQL-First,白盒透明,能力无上限。


本文基于 SimpleDAO 1.2.1 版本及 8 个企业实战案例撰写。框架已在生产环境稳定运行 3 年+,支撑日均百万级请求,服务十余家企业客户。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐