MyBatis 基础教程:一个老码农的持久层血泪史

代码不眠人
2025-12-18 10:07
阅读 2129

早上 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 手动控制缓存策略,反而更可控。

如果你非要用二级缓存,记住三点:

  1. 只用于读多写少、数据变更不敏感的场景(比如字典表)
  2. 必须实现 Serializable
  3. 配合 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>

或者用 SqlSessionExecutorType.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

最热最新
暂无评论
代码不眠人Lv.1
0
影响力
0
文章
0
粉丝