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


相关开源地址

  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 天
结果映射resultMapassociationcollectiondiscriminator2-3 天
缓存配置一级缓存、二级缓存、缓存策略1-2 天
插件机制Interceptor@InterceptsInvocation1 周
综合踩坑N+1、延迟加载、代理陷阱、事务边界数月

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

2.2 这套规范的本质

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

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

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

2.3 对比:SimpleDAO 的规范

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

规范项内容学习成本
三个主类BaseDaoBaseSqlBaseCondition20 分钟
业务语义 APIlistpagerowfieldcountexists一看就懂
条件拼接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 标准配置直接使用,零适配
事务管理@Transactional100% 兼容,无额外配置
参数化查询PreparedStatementJdbcTemplate 原生支持
结果映射BeanPropertyRowMapper下划线自动转驼峰,零配置
批量操作NamedParameterJdbcTemplatebatchUpdate 标准 API
多数据源Spring 动态数据源直接使用,无适配层
缓存Spring CacheService 层注解,与 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 的业务语义化、四象限正交分解、极致性能,一脉相承。 不是三个独立的设计点,是同一个设计原则在不同层面的体现:让框架不可见,让开发者专注。


十一、学习成本:不是"显著低于",是"不在同一个维度"

阶段MyBatisSimpleDAO
入门1-3 天,能写单表 CRUD20 分钟,看 API 速查表
上手1 周,能写联表、动态 SQL1 天,写条件类、联表查询
踩坑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 方法覆盖度
单表 CRUD100% — JdbcTemplate.update
动态 WHERE100% — 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 年+,支撑日均百万级请求,服务十余家企业客户。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值