MyBatis 基础教程:一个老码农的持久层血泪史
早上 8 点,我坐在北京出租屋的小书桌前,窗外是五环外一如既往的早高峰喧嚣。泡好咖啡、打开 IDEA,脑子里还在想着昨天晚上那个 MyBatis 的 #{} 和 ${} 到底有没有被同事搞混——毕竟这玩意儿线上出过一次 SQL 注入事故,我们组全员被叫去复盘到凌晨两点,产品经理还在钉钉上问“这个能不能明天上线”。
我是那种典型的“35 岁还在写代码”的老程序员。坐标帝都,通勤一小时,家里有娃,不敢裸辞。技术栈从 SSH(别笑,是真的)一路折腾到现在 Spring Boot + MyBatis,中间还短暂迷恋过 Go,但最后发现老板要的是稳定交付,不是炫技。
今天这篇文,不是为了卷面试题,而是想给刚入门 MyBatis 的兄弟们避个坑——毕竟你要是连 resultMap 都配不明白,面试官一句“说说 MyBatis 缓存机制”就能让你当场社死。
为啥又捡起 MyBatis?因为老板不让用 JPA
去年双11前,我们接了个新项目,要求高并发读写、低延迟响应。架构师拍板用 MySQL + Redis + MyBatis。我内心其实是拒绝的——Spring Data JPa 多香啊,findByUserIdAndStatus() 直接生成 SQL,省心省力。
但运维老大一句话怼回来:“上次 JPA 的 N+1 查询把数据库 CPU 干到 90%,你还敢用?”
行吧,MyBatis 就 MyBatis。至少它SQL 写在哪、怎么跑,我心里有数。不像某些 ORM,黑盒得像薛定谔的猫,你永远不知道它下一秒吐出的是高效查询还是全表扫描。
第一次写 Mapper,差点被 #{} 搞崩
新手最容易栽的坑,就是参数绑定。看这段代码:
<select id="getUserByName" resultType="User">
SELECT * FROM users WHERE name = '${name}'
</select>
如果你这么写,恭喜你,SQL 注入漏洞已就位。上周五晚上加班改 Bug,测试同学甩过来一个 case:输入用户名 ' OR '1'='1,结果返回了全库用户……我当时真的想砸键盘。
正确姿势是:
<select id="getUserByName" resultType="User">
SELECT * FROM users WHERE name = #{name}
</select>
#{} 会预编译,${} 是直接拼接字符串。记住:除非你 100% 信任输入(比如枚举值),否则别碰 ${}。我在 GitHub 上翻过不少开源项目,连一些 star 几千的 repo 都在这栽过跟头。
resultMap:MyBatis 的灵魂,也是噩梦
MyBatis 最强大的地方,其实是它的映射能力。比如你的数据库字段是 user_name,Java 实体类是 userName,这时候就得靠 resultMap:
<resultMap id="UserMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="getAllUsers" resultMap="UserMap">
SELECT id, user_name, create_time FROM users
</select>
刚开始我觉得这玩意儿太啰嗦,不如 JPA 自动映射香。但后来遇到复杂场景——比如多表关联、嵌套对象、动态字段——才发现 resultMap 是救命稻草。
举个例子:用户订单列表,既要查用户信息,又要查最近一笔订单。用 MyBatis 的 <association> 和 <collection>,配合 LEFT JOIN,一条 SQL 搞定。而 JPA 可能得发 N+1 条查询,或者手写 DTO 转换,累死。
缓存机制:别让“优化”变成“事故”
MyBatis 有一级缓存(SqlSession 级)和二级缓存(Mapper 级)。听起来很美好,对吧?但生产环境慎用二级缓存!
去年我们有个配置中心模块,用了 MyBatis 二级缓存。结果某次发布后,改了数据库配置,前端却一直读旧数据。排查半天才发现,缓存没刷新。后来干脆关了,用 Redis 手动控制缓存策略,反而更可控。
如果你非要用二级缓存,记住三点:
- 只用于读多写少、数据变更不敏感的场景(比如字典表)
- 必须实现
Serializable - 配合
cache-eviction策略,别让它无限膨胀
性能调优:别只顾着 CRUD
在我们团队,每次提测前都要过一遍 MyBatis 性能 checklist:
| 项目 | 推荐做法 | 反面教材 |
|---|---|---|
| SQL 日志 | 开启 log4j.logger.org.apache.ibatis=DEBUG |
上线后关日志,结果慢查询无从查起 |
| 批量操作 | 用 <foreach> 或 ExecutorType.BATCH |
循环里单条 insert,1000 条记录跑 30 秒 |
| 分页 | 用 PageHelper 或手写 LIMIT |
先查全量再内存分页,OOM 预警拉满 |
| 动态 SQL | <if>, <choose> 组合使用 |
拼接字符串判断条件,维护成本爆炸 |
特别是批量插入,别再干这种事了:
for (User user : users) {
userMapper.insert(user);
}
正确的姿势是:
<insert id="batchInsert">
INSERT INTO users (name, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email})
</foreach>
</insert>
或者用 SqlSession 的 ExecutorType.BATCH 模式,性能提升 10 倍不是梦。
面试题高频考点,别等面试才临时抱佛脚
最近帮公司面试几个 Java 岗,MyBatis 相关问题几乎必问。整理几个高频题:
MyBatis 和 Hibernate 有什么区别?
别答“一个轻量一个重量”,要说:MyBatis 把 SQL 控制权交给开发者,适合复杂查询;Hibernate 抽象程度高,适合快速开发但调试困难。#{}和${}的区别?
上面讲过了,但面试官喜欢追问:“什么时候必须用${}?” 答:动态表名、列名(如分表分库)、ORDER BY 字段——但一定要做白名单校验!MyBatis 如何防止 SQL 注入?
核心是预编译 + 参数绑定,但也要补充:输入校验、最小权限原则、定期 SQL 审计。
这些题 GitHub 上搜 “MyBatis interview” 能找到一堆答案,但真正理解原理的人不多。建议你动手写个 demo,故意制造注入,看 MyBatis 日志怎么处理的。
最后一点真心话
MyBatis 不是银弹,但它给了你对 SQL 的掌控感。在这个微服务、云原生、Serverless 满天飞的时代,很多人觉得写 SQL 是“底层细节”,应该被屏蔽。但现实是:线上慢查询、死锁、连接池耗尽,最终还得 DBA 和后端一起背锅。
所以,别小看 CRUD。能把 MyBatis 用明白,比盲目追 Go、Rust 更重要——毕竟,老板要的是系统稳定,不是你用了多少新语言(虽然我也偷偷在学 Go,但只敢用来写脚本 😅)。
如果你刚入门,建议直接上手 mybatis-spring-boot-starter,官方示例清晰,社区活跃。遇到问题先看 GitHub Issues,八成有人踩过同样的坑。
好了,咖啡喝完了,该去挤地铁了。希望这篇带点烟火气的教程,能帮你少熬两个夜。毕竟,35 岁的我,只想准点下班陪娃睡觉。

评论 0