MyBatis基础教程:Java持久层框架入门 —— 一个斜杠程序员的血泪实战总结
上周五晚上十点半,我一边戴着 AirPods 听周杰伦的《稻香》,一边盯着 IntelliJ IDEA 里那堆红得发紫的 SQL 报错信息,心里只有一个念头:产品经理能不能别在周五下午五点突然加需求?
没错,这就是我——一个坐标上海、租住在公司步行10分钟范围内的斜杠程序员。主业是后端开发,副业接外包(最近帮一家创业公司搞了个小商城),顺带在 B 站录点技术视频赚点奶茶钱。我的日常就是:白天改 Bug,晚上写代码,周末可能还在和 Go 语言较劲(别问,问就是想转云原生,结果发现 Java 还是香)。
今天这篇文章,其实源于一个真实的“事故”。上个月,我接了个外包项目,客户是个做智能健身镜的初创团队。他们原来用的是纯 JDBC 操作数据库,代码写得跟意大利面条一样,连个基本的分页查询都要复制粘贴三次。更离谱的是,前端同事每次改个字段,后端就得改五六个 DAO 方法,测试提了 20 个 bug,运维差点把我服务器权限给禁了。
最后被逼无奈,我决定把整个数据访问层重构成 MyBatis。结果这一折腾,不仅性能提升了 40%,连产品经理都跑来夸我“这次接口文档写得人话多了”。
所以,这篇不是教科书式的“Hello World”,而是一个踩过坑、熬过夜、被线上告警吵醒过的斜杠程序员,关于 MyBatis 的最佳实践总结。我会尽量避开那些官方文档里都能抄到的内容,重点讲怎么在真实产品中用好它。
为什么不用 JPA 或 Hibernate?先说清楚立场
很多刚入行的朋友一听说 ORM,第一反应就是 Spring Data JPA。确实,JPA 对于 CRUD 场景很爽,但一旦涉及复杂查询、多表关联、动态条件,或者需要精细控制 SQL 性能时,JPA 就开始“抽象泄漏”了。
我们那个健身镜项目就吃过亏:早期用 JPA 写了个“用户最近7天运动记录聚合查询”,结果生成的 SQL 嵌套了四层子查询,数据库 CPU 直接飙到 90%。DBA 跑来质问我:“你这是要干掉我们的 MySQL 实例吗?”
而 MyBatis 的核心哲学很简单:SQL 是你写的,你负责。它不替你生成 SQL,而是帮你管理 SQL 和 Java 对象之间的映射。这种“半自动”的设计,反而在产品级系统中更有优势——尤其是当你需要:
- 手动优化慢查询
- 复用已有存储过程
- 支持多数据源(比如读写分离)
- 需要和前端联调接口字段(而不是被 ORM 绑死)
顺便吐槽一句:有些团队迷信“全自动 ORM”,结果上线后发现一条简单查询拖垮了整个服务。算法再牛,也扛不住烂 SQL 啊兄弟们!
MyBatis 入门:别被配置劝退
我知道很多人第一次看 MyBatis 配置文件就被吓跑了:mybatis-config.xml、Mapper.xml、SqlSessionFactory……一堆 XML,感觉回到了 2005 年。
但其实,如果你用 Spring Boot,90% 的配置都可以自动化。我现在的项目基本这么搞:
# application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.product.domain
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
然后在启动类加上 @MapperScan("com.example.product.mapper"),搞定。别手写 SqlSessionFactoryBean,那是上个时代的事了。
真正要花心思的是 Mapper 接口和 XML 文件。举个例子,我们要查用户运动记录:
// UserRecordMapper.java
public interface UserRecordMapper {
List<UserRecord> selectRecentRecords(@Param("userId") Long userId, @Param("days") int days);
}
对应的 XML:
<!-- UserRecordMapper.xml -->
<select id="selectRecentRecords" resultType="User'))
SELECT id, user_id, duration, calories, created_at
FROM user_records
WHERE user_id = #{userId}
AND created_at >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
ORDER BY created_at DESC
</select>
注意几个细节:
- 用了
map-underscore-to-camel-case,所以数据库字段user_id自动映射到 Java 的userId #{}是预编译参数,防 SQL 注入;${}是字符串替换,慎用!- 别在 XML 里写业务逻辑,那是前端该干的事(开玩笑的,但真有人这么干)
动态 SQL:MyBatis 最香的功能
产品需求永远在变。上周产品经理说“只要最近7天的”,这周他说“要支持自定义时间段,还要能按运动类型筛选”。
这时候,MyBatis 的 <if>、<choose>、<foreach> 就派上用场了:
<select id="selectRecordsByCondition" resultType="UserRecord">
SELECT id, user_id, duration, calories, created_at, sport_type
FROM user_records
WHERE user_id = #{userId}
<if test="startDate != null">
AND created_at >= #{startDate}
</if>
<if test="endDate != null">
AND created_at <= #{endDate}
</if>
<if test="sportTypes != null and sportTypes.size() > 0">
AND sport_type IN
<foreach item="type" collection="sportTypes" open="(" separator="," close=")">
#{type}
</foreach>
</if>
ORDER BY created_at DESC
</select>
这段代码在线上跑了三个月,零故障。关键是:所有条件都是可选的,SQL 只拼接非空参数。
对比一下如果用 JPA 写,你可能得搞个 Specification 或者写一堆 if-else 构建 CriteriaQuery,代码又臭又长。而 MyBatis 让 SQL 保持可读性,这对后期维护太重要了。
性能与架构:别只顾着 CRUD
作为关注架构设计的程序员,我必须强调:MyBatis 不是万能的,用不好照样拖垮系统。
1. N+1 查询问题
新手最容易犯的错。比如查用户列表,然后每个用户再去查他的记录:
List<User> users = userMapper.selectAll();
for (User u : users) {
u.setRecords(recordMapper.selectByUserId(u.getId())); // 每次都查一次 DB!
}
100 个用户 = 101 次查询。线上直接炸。
解决方案:
- 用
<collection>做关联查询(但要注意笛卡尔积) - 或者分两步:先查用户 ID 列表,再批量查记录,最后内存合并
<resultMap id="UserWithRecords" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="records" ofType="UserRecord"
select="selectRecordsByUserId" column="id"/>
</resultMap>
但注意:这种方式在大数据量下可能更慢,因为每条主记录都会触发一次子查询。建议在低频、小数据场景使用。
2. 二级缓存?谨慎开启!
MyBatis 有二级缓存,但默认是关闭的。我在外包项目里试过开,结果因为多个服务实例共享 Redis 缓存,导致数据不一致,被测试追着骂了一天。
生产环境建议:
- 单机应用:可用,但注意缓存粒度
- 分布式系统:别用 MyBatis 自带缓存,自己用 Redis + 缓存穿透/击穿防护更可控
3. 分页别手写 OFFSET
很多人写分页直接:
SELECT * FROM table LIMIT #{offset}, #{size}
当 offset 很大时(比如第 10000 页),MySQL 要扫描前 10000 行,性能极差。
正确做法:用游标分页(cursor-based pagination),基于时间戳或自增 ID:
SELECT * FROM user_records
WHERE id > #{lastId}
ORDER BY id
LIMIT #{size}
前端传 lastId,后端返回下一页的起始 ID。这才是产品级分页该有的样子。
跨语言协作:别忘了 Go 和前端
虽然 MyBatis 是 Java 的,但在微服务架构里,你的服务可能要和 Go 写的服务通信,或者给前端提供 API。
给前端的建议
- 字段命名统一用驼峰(靠
map-underscore-to-camel-case) - 时间字段统一用 ISO8601 格式(
2023-10-01T08:00:00Z) - 敏感字段(如密码)别查出来,哪怕你“只是临时用一下”
和 Go 服务交互
我们有个推荐算法服务是用 Go 写的,它会调用我的 Java 服务获取用户行为数据。为了保证一致性:
- 定义清晰的 DTO(Data Transfer Object)
- 用 OpenAPI/Swagger 生成客户端
- 数据库字段变更时,先改接口契约,再改实现
有一次我偷偷改了个字段名没通知 Go 团队,结果他们的推荐模型跑出一堆 NaN,算法工程师差点冲到我们工位来打人。
配置对比:MyBatis vs 其他方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MyBatis | SQL 可控、性能透明、学习曲线平缓 | 需手写 SQL、XML 略繁琐 | 中大型产品、复杂查询、性能敏感系统 |
| Spring Data JPA | 开发快、CRUD 极简 | 复杂查询难优化、黑盒感强 | 快速原型、简单后台、MVP 产品 |
| JDBC Template | 零依赖、极致控制 | 重复代码多、无对象映射 | 超轻量级工具、遗留系统改造 |
| Go GORM | Go 生态友好、链式调用 | 类型安全弱、调试困难 | Go 微服务内部 |
注:别问我为什么列 Go GORM,因为上周我刚用它写了 side project,结果被 nil pointer error 折磨到凌晨三点……
最后几句真心话
MyBatis 不是什么高深技术,但它代表了一种对 SQL 的尊重。在这个“全自动 ORM 才是现代化”的风气下,能手动写 SQL、敢对 DBA 说“这条语句我优化过”的后端,才是靠谱的产品守护者。
我现在接外包,第一件事就是看对方有没有用 MyBatis(或者至少没乱用 JPA)。因为这意味着代码大概率是可维护、可监控、可优化的。
当然,技术只是工具。真正决定产品成败的,是你愿不愿意为每一行 SQL 负责。
好了,音乐停了,《稻香》放完了。我得去改产品经理刚提的新需求了——“能不能在用户运动时实时推送鼓励语?” …… 我现在就想对自己说一句:加油,打工人!
P.S. 如果你在用 MyBatis,记得打开日志(log-impl 那行),亲眼看看生成的 SQL。别信 ORM,信 EXPLAIN。

评论 0