学习 MyBatis 到底有哪些"收益"?
前言 你MyBatis能做到吗?
@Override
protected void addCondition() {
add("AND d.code = ?", deptCode); // 等值
add("AND u.age >= ?", ageMin); // 范围下限
add("AND u.age <= ?", ageMax); // 范围上限
add("AND u.user_id IN ", userIds); // 集合
add("AND d.name LIKE ?", deptNameLike, 3); // 模糊
add("AND u.dept_id IN (SELECT id FROM dept WHERE code = 10)", subQuery); // 子查询
add("AND u.dr = 0"); // SQL片段(固定)
}
一、两个故事
先说两个真实场景。是每个后端团队里每天都在发生的事。
故事一:分页工具类
有两组人。一组用 Spring JDBC,一组用 MyBatis。
Spring JDBC 组
团队负责人把新来的实习生叫过来:“来,你帮我写一个全局通用的分页工具类。不能从外面引插件,自己写。只适配咱们现在用的这一种数据库。给你一个小时。写完了能用,上线一周不抛锚,直接转正。”
这个实习生会写吗?
能。概率很高。
因为这件事在 Spring JDBC 的语境下,就是一个纯 Java 问题。拿到 SQL 字符串,拼上 LIMIT ? OFFSET ?,再执行一次 COUNT(*),两次查询,完事。一个有点基础的人,搜搜资料,一个小时写出来完全可能。逻辑全在明面上——字符串拼接、两次 JDBC 调用、参数传递。Code Review 的时候负责人一眼就能看明白在干什么。
上线一周抛锚的概率?有,但不高。 可能 SQL 带了分号结尾拼 LIMIT 会报错,可能子查询里 ORDER BY 在某些版本会炸。但这些都是边界 case,测试阶段大概率就暴露了。而且因为是白盒代码,出了问题你打开一看就知道哪行炸了。
实习生转正的概率?很大。
MyBatis 组
负责人把工作两年的小王叫过来:“小王,你也有两年多经验了,算是有点后端开发经验了吧。给你一天时间,写一个 MyBatis 全局通用的分页。写好了发 2000 块奖金。当然了,这是没有大语言模型的年代。”
小王能写出来吗?
极低概率。可能不到一成。
因为他要面对的不是"写代码"的问题,是"理解 MyBatis 内部架构"的考试。他需要搞懂 Interceptor 接口、@Signature 注解、Invocation 里怎么拿 MappedStatement、BoundSql、ParameterHandler。他要从 BoundSql 里取出原始 SQL,判断数据库方言,改写 SQL 后塞回去——而"塞回去"这一步,得靠反射改私有字段,或者自己 new 一个 BoundSql。他还要考虑怎么触发 count 查询、分页参数从哪来(ThreadLocal?)、插件链的执行顺序、多个插件叠加时他的分页在第几层……
这些不是"会不会写 Java"的问题。是"有没有读过 MyBatis 源码"的问题。
一个工作两年的后端,没专门研究过 MyBatis 源码——他连从哪下手都不知道。
上线出 bug 的概率?极高。九成以上。 就算他"写出来"了(能跑),几乎必然踩坑:嵌套 resultMap 加分页、存储过程被拦截、多 Executor 类型没考虑到、插件顺序冲突、延迟加载加分页会话关闭……每一个都是生产环境才会暴露的雷。
这两个场景,需求一模一样,时间给得 Spring JDBC 组更短(一小时 vs 一天),奖金给得 Spring JDBC 组更少(转正 vs 2000 块)。但结果完全反过来。
这不是人的差距。这是框架给不给你下手的地方的差距。
故事二:屏蔽判空 if
上周那个实习生表现不错,留用了。Spring JDBC 组要继续优化。负责人说:“拼动态条件的时候,每次都手写一个 if 判空,太难看了。能不能把它屏蔽掉?当然,正常的业务逻辑该写还写,只是这个判空 if 别让我每次都写。”
实习生,一天时间。写不出来走人。
他能写出来吗?
大概率能。
因为这本质上就是写一个条件拼接的辅助类。纯 Java,没有任何框架黑盒。一个 SqlWhere 工具类,内部维护一个 StringBuilder 和一个 hasWhere 标记,where(condition, value, appender) 方法里包一层判空,不为空才拼。用法直观,逻辑透明。实习生可能写得不够优雅,但核心就是一层薄封装——在他已有的 Java 知识范围内完全能搞定。
上线出 bug 的概率?低。 最多忘了处理 hasWhere 导致多一个 AND,一跑就炸,测试阶段就修了。
MyBatis 组
上次的教训让负责人换了一个人——这次找了个有 10 年后端经验、号称 MyBatis 专家的老哥。需求一样:把 XML 里那个 <if test="xxx != null"> 屏蔽掉,自动处理判空。
给一周时间。写出来奖金 2 万。写不出来拎包走人。
这位专家能写出来吗?
极低概率。可能两成都不到。
因为问题不在 Java 能力,在 MyBatis 的架构天花板。
他有几条路:
路线 A:写自定义标签。 比如 <notNull property="name">。走不通。MyBatis 的 XML 标签体系是硬编码在 XMLMapperBuilder 里的,没有 Tag Library 机制,没有可插拔的标签注册接口。标签名不认识?解析器直接抛异常。想加新标签?改框架源码。
路线 B:写 LanguageDriver。 MyBatis 确实留了这个扩展点。但你要么自己发明一套模板语法(等于在 MyBatis 上面又搭一层 DSL),要么在 XML 里嵌 Groovy 或 Velocity 模板——那 XML 里就不是 MyBatis 标签了,是模板引擎。代价巨大,维护噩梦。
路线 C:用 @SelectProvider 注解。 把 SQL 挪回 Java 里用 SQL 类拼。但这等于放弃了 XML,而且比 Spring JDBC 还绕——多了一层 Provider 方法的间接。
路线 D:写 Plugin 拦截改写 SQL。 跟分页一样挖地道。SQL 在 BoundSql 里已经是解析完的字符串了,你想在解析阶段处理标签逻辑——拦截不到那个时机。
所以一个 10 年专家面对的现实是:MyBatis 的 XML 标签体系是封闭的。 你想在它上面做语法糖?门都没有。除非你改源码。
这不是能力问题。这是架构天花板问题。10 年经验让他更清楚"这条路走不通",而不是让他能变出花来。
两个故事合在一起,说明了一件事:
Spring JDBC 的扩展性在 Java 本身——任何人都能用,实习生能写、专家能写、你也能写。
MyBatis 的扩展性被锁死在 XML 里——XML 不是编程语言,没有函数、没有变量、没有抽象机制。想扩展标签?改框架源码。想在不改源码的前提下"优雅扩展"?不存在的。
所谓 MyBatis 社区的"生态繁荣"——PageHelper、MyBatis-Plus、通用 Mapper——全都是从后门挖地道挖出来的。这个我们后面细说。
二、代码对决:1+1 查询场景
故事讲完了。有人会说:“你这是极端场景,日常 CRUD 哪有这么多扩展需求?平时不就是写个查询吗?”
好。那我们就比日常。不比扩展,不比插件,就比最普通的业务查询代码量。
场景
A 表 30 个字段,B 表 20 个字段。A 只取 id 和 user_name 两个字段,B 只取 user_id、order_id、amount 三个字段。
先查 A,拿到 ID 列表后用 IN 查 B,然后在内存里组装成带订单列表的用户对象。
这是业内公认最平衡的查询方式——1+1 两次查询 + 内存流式拼装。不用 JOIN 拉冗余数据,不用让数据库做应用层该做的事。
Spring JDBC 方案
@Repository
public class UserOrderDao {
private final NamedParameterJdbcTemplate jdbc;
public UserOrderDao(NamedParameterJdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public List<UserBrief> users(List<Long> ids) {
List<UserBrief> users = jdbc.query(
"SELECT id, user_name FROM user WHERE id IN (:ids)",
Map.of("ids", ids),
new BeanPropertyRowMapper<>(UserBrief.class));
List<OrderBrief> orders = jdbc.query(
"SELECT user_id, order_id, amount FROM orders WHERE user_id IN (:ids)",
Map.of("ids", ids),
new BeanPropertyRowMapper<>(OrderBrief.class));
Map<Long, List<OrderBrief>> orderMap = orders.stream()
.collect(Collectors.groupingBy(OrderBrief::getUserId));
users.forEach(u -> u.setOrders(orderMap.getOrDefault(u.getId(), List.of())));
return users;
}
}
VO 类就是普通的 Java 类,两三个属性,getter/setter。BeanPropertyRowMapper 一行搞定映射,下划线自动转驼峰。IN 查询用 :ids 自动展开,不需要手写占位符拼接。
MyBatis 方案
Mapper 接口:
List<UserBrief> selectUsersByIds(@Param("ids") List<Long> ids);
List<OrderBrief> selectOrdersByUserIds(@Param("ids") List<Long> ids);
XML(完整文件,一个字不少):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserOrderMapper">
<select id="selectUsersByIds" resultType="com.example.vo.UserBrief">
SELECT id, user_name FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="selectOrdersByUserIds" resultType="com.example.vo.OrderBrief">
SELECT user_id, order_id, amount FROM orders WHERE user_id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper>
Service 层的内存拼装代码跟 Spring JDBC 一模一样,不重复了。
字符数硬比
| 部分 | Spring JDBC | MyBatis |
|---|---|---|
| SQL 定义 | Java 字符串,~260 字符 | XML 本体 + foreach,~300 字符 |
| 映射 | new BeanPropertyRowMapper<>(X.class) 一行 | resultType="X" 一行 |
| 框架固定税 | 0 | XML 声明 + DOCTYPE + namespace + 全限定类名:~360 字符 |
| 总有效字符数 | ~600 | ~820 |
Spring JDBC 少了 27%。而且少的不是业务逻辑——少的全是框架税。那 360 个字符跟你的业务零关系,每次都在那碍眼。你说"复制粘贴就行"——好,复制粘贴不丢人,但它占着屏幕、参与搜索结果、出现在 Git diff 里、增加认知噪音。
还有人说 MyBatis 的 resultType 比 BeanPropertyRowMapper 方便?两边都是一行。MyBatis 的 <foreach> 比 :ids 方便?:ids 是框架自动展开,零额外代码;<foreach> 是你手写四个属性加一对括号。谁更简洁,数字不会骗人。
三、XML 的扩展性真相
前面两个故事已经点到了,这里展开说。
JSP/JSTL 都能自定义标签,MyBatis 不能
JSP 当年有 Tag Library 机制——你写个 SimpleTagSupport 子类,配个 .tld 文件,新标签就能在页面上用。任何人都能扩展,框架从第一天就给使用者留了口子。标签的本质是 Java 类,可编译、可调试、可单测。
MyBatis 的 XML 标签解析在 XMLMapperBuilder 里,是硬编码的。没有 Tag Library,没有可插拔注册接口,没有扩展点。你想加一个 <notNull>?解析器不认识这个字符串,直接抛异常。
JSP 是 2000 年代初的技术。MyBatis 是 2010 年的框架。隔了十年,标签可扩展性——退步了。这不是时代局限,是设计者根本没把这个当设计目标。他设计的是一套封闭的 DSL,不是开放的标签平台。
社区的"扩展"全是挖地道
| 所谓"扩展" | 实际在干什么 |
|---|---|
| PageHelper | 拦截 Executor.query,反射改 BoundSql 私有字段 |
| MyBatis-Plus | 重写整个 SQL 注入器,基本架空 XML 体系另起炉灶 |
| 通用 Mapper(tk) | 运行时反射实体类,动态生成 SQL 塞进执行链 |
| 多租户插件 | 拦截 StatementHandler,字符串替换 SQL |
| 数据权限插件 | 同上 |
这些"扩展"的共同特征:
- 没有一个走的是框架官方预留的干净扩展点——全是拦截、反射、改私有字段、字符串替换
- 全都在跟 MyBatis 的内部对象搏斗——
BoundSql、MappedStatement、Configuration不是公开 API,是内部实现,随时可能变 - 升级 MyBatis 版本就提心吊胆——因为你在用的"扩展"依赖的是内部细节,不是公开契约
- 每个插件作者都在赌——赌框架内部不改、赌自己的反射不会在下一个版本炸
这不是生态繁荣。这是框架自己制造了坑,然后让开发者写插件来填坑。你在为框架的设计缺陷买单。
PageHelper 为什么存在?因为 MyBatis 不给你在 XML 里写分页的标准方式。MyBatis-Plus 为什么存在?因为 MyBatis 不给你单表 CRUD 的零配置方案。通用 Mapper 为什么存在?因为 MyBatis 不给你通用 Mapper 的扩展机制。
每一个"生态繁荣"的项目,背后都是 MyBatis 的一个设计缺口。
四、工程化维度全量审判
下面这十个维度,是我跟一位朋友反复辩论后确定的工程化评估框架。不是"好不好用"这种主观感受,是硬指标。
1. 高效性(开发效率)
MyBatis:改一条查询 = 改 XML + 改 Mapper 接口,跨两个文件操作。
Spring JDBC:在 DAO 方法里改 SQL 字符串 + 参数,一个文件搞定。
MyBatis 胜在"有代码生成器"——但那是工具的效率,不是框架的效率。
判定:Spring JDBC 胜。
2. 简洁性
字符数实锤:同样 1+1 查询,MyBatis ~820 字符 vs Spring JDBC ~600 字符。MyBatis 多出来的全是框架税。
判定:Spring JDBC 胜。
3. 可用性(开箱即用)
MyBatis:引依赖 + 配 SqlSessionFactory + 配 MapperScan + 写 XML。
Spring JDBC:引依赖 + 一个 JdbcTemplate Bean。
新人第一天写查询:MyBatis 要学 XML 语法、标签、resultMap、namespace 规则;Spring JDBC 会写 SQL + 会写 Java 方法就行。
判定:Spring JDBC 胜。
4. 易用性
两边写 IN 查询都能用,动态 SQL 两边都是面条。但 MyBatis 每次改查询要跨文件(接口 + XML),Spring JDBC 只在一个文件里改。
判定:平手(但两边都不优雅)。
5. 心智负担
这是最容易被人忽略但最致命的维度。
MyBatis 的 <if test="">、<foreach>、OGNL 表达式——这是一门新的微型语言。你得单独记它的语法规则:test 里不能用 && 得用 and,字符串比较得单引号套双引号,集合判空用 list.size() > 0 而不是 !list.isEmpty()……你花的时间不是"学会怎么拼 SQL",是"学会怎么用 MyBatis 的语法去拼 SQL"。
Spring JDBC 拼动态 SQL 用的就是 if/else、StringBuilder、Stream、Optional——这些东西你本来就懂,不需要学。
判定:Spring JDBC 胜。
MyBatis 做的是**“语法转移”,不是"抽象简化"**。它把用 Java 拼变成了用 XML 标签拼,工作量没少,认知负担反而多了——因为你脑子里得同时维护两套东西的映射关系。
6. 学习成本
MyBatis:SqlSession、Mapper、resultMap、动态 SQL 标签、OGNL、TypeHandler、插件机制、缓存配置——上百个属性、几十个标签、十几个注解。
Spring JDBC:JdbcTemplate API、RowMapper 接口。会用 query() 和 update() 就覆盖 90% 场景。
判定:Spring JDBC 胜。
7. 场景适配性
前面说了,动态 SQL 两边都不咋地。但关键区别是:
- Spring JDBC 的不优雅,你能自己写工具改善(实习生那个 SqlWhere 就是例子)
- MyBatis 的不优雅,你永远改善不了——因为 XML 标签体系是封闭的
判定:Spring JDBC 胜。
8. 生态完备性(底层基建)
注意,这里说的是底层基建生态,不是补坑的插件生态。
连接池?两边都是 HikariCP。事务?两边都是 Spring 的 PlatformTransactionManager。监控?两边都是 Spring Boot Actuator。
但区别在于:Spring JDBC 是 Spring 生态的原生层,事务、AOP、Boot、Actuator 是同一套设计语言;MyBatis 是外来户,靠 mybatis-spring 桥接,桥接本身有损耗、有版本对齐问题、有配置冲突风险。
判定:Spring JDBC 胜。
9. 可扩展性
Spring JDBC:想扩展什么就写什么 Java 类,不需要任何人批准,不需要改框架源码。
MyBatis:XML 层面零扩展能力;Java 层面留了几个后门(Interceptor、LanguageDriver),但全是让你"挖地道绕开 XML"用的。
判定:Spring JDBC 胜。
这不是"程度差异",是**“有没有”**的差异。
10. 业务代码浓度 / 框架存在感
Spring JDBC:你写一行代码,90% 以上是在表达业务逻辑。
MyBatis:你写一行,大概 40% 是框架语法(标签、namespace、OGNL),60% 是业务。
好的持久层框架应该让你感觉不到它的存在——Spring JDBC 做到了,MyBatis 没有。
判定:Spring JDBC 胜。
总判决
| 维度 | 判定 |
|---|---|
| 高效性 | Spring JDBC |
| 简洁性 | Spring JDBC |
| 可用性 | Spring JDBC |
| 易用性 | 平手 |
| 心智负担 | Spring JDBC |
| 学习成本 | Spring JDBC |
| 场景适配性 | Spring JDBC |
| 生态完备性(底层) | Spring JDBC |
| 可扩展性 | Spring JDBC |
| 业务代码浓度 / 框架存在感 | Spring JDBC |
10 个维度,10 胜 1 平 0 负。 连那个"平手"还是"两边都不优雅"的意思,不是 MyBatis 有什么拿得出手的东西。
五、为什么它还是行业标配?
既然全方位被碾压,为什么 MyBatis 还是国内 Java 后端的"行业标配"?
这是社会学问题,不是技术问题。
历史惯性
SSM 时代(Spring + Spring MVC + MyBatis),MyBatis 是"比 Hibernate 轻量"的妥协产物。那个年代 Hibernate 的复杂度把大家吓怕了,MyBatis 说"我就是 SQL 映射",大家如释重负。
但这个"轻量"是相对于 Hibernate 而言的——放到今天跟 Spring JDBC 比,它一点也不轻。
简历驱动
培训班批量生产 MyBatis 使用者,面试必问"MyBatis 的一二级缓存"“#{} 和 ${} 的区别”“插件原理”。会 MyBatis 成了"Java 后端"的入场券。你用 Spring JDBC,面试官可能觉得你"没用过主流框架"。
阿里系背书
早期淘宝用 iBatis(MyBatis 前身),技术博客、开源项目、内部实践大量流出,"阿里都在用"成了最强背书。但阿里当时的技术选型有它的历史上下文——你今天的新项目不是淘宝,不需要为那个上下文买单。
逆向工程
代码生成器让"写 XML"看起来不费劲——表一建,插件一跑,Mapper + XML + Entity 全出来。但维护的时候呢?生成的 XML 几百行,改一个字段要追三层继承,没人敢删只敢加。生成器解决的是"写"的问题,没解决"读"和"改"的问题。
技术选型应该是工程决策,不是历史包袱的继承。
当你用工程化标准去量的时候,答案很清楚——MyBatis 没有给你任何 Spring JDBC 给不了的东西,但它收了你一堆税。
六、总结
回到标题那个问题:学习 MyBatis 到底有哪些"收益"?
从纯技术角度看,跟 Spring JDBC 对比:
| 争议点 | 真相 |
|---|---|
| SQL 集中管理? | Spring JDBC 的 DAO 类一样集中,而且不用跨文件跳转 |
| 自动映射? | BeanPropertyRowMapper 一行等价 |
| 命名参数? | NamedParameterJdbcTemplate 的 :ids 一样好用 |
| 动态 SQL? | 两边都是面条,MyBatis 还多了一层标签语法要学 |
| SQL 复用? | Java 常量 + 方法抽取,比 XML 的 <sql> 更灵活 |
| 二级缓存? | 多实例部署直接废掉,必须关 |
| 分页? | 自己写工具类实习生就能搞定,不需要引插件 |
| 扩展性? | XML 是封闭的,所谓扩展全是挖地道 |
诚实的答案是:收益不是零,是彻底的为负。
多出来的所有东西——XML、标签、插件、拦截器、嵌套映射——全是成本,没有对应的 Spring JDBC 给不出的能力。
如果你正在启动一个新项目,我的建议很简单:用 Spring JDBC。
如果你在维护一个老项目,满屏 MyBatis XML——至少你现在知道,那些让你不舒服的东西,不是你的问题,是框架的问题。
写这篇文章的过程,就是一次自我验证的过程。每一个"MyBatis 有收益"的论点,在故事、代码、数据和工程化维度面前,一个一个倒下。如果你觉得哪里还有争议,欢迎讨论——但请带上代码和字符数,不要只带感觉。
如果你也在用 MyBatis 写复杂 SQL,且已经被 XML 的
<if>搞得心烦意乱——试试这个方案。代码已开源,用了就知道什么叫"白盒透明"。
相关开源地址:

519

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



