MyBatis基础教程:一个后端开发者的实战入门指南
开篇:为什么选择MyBatis?

记得刚入职互联网公司那会儿,我接手的第一个项目是一个用户中心的后端服务。项目不大,但麻雀虽小五脏俱全,涉及用户注册、登录、权限管理等多个模块,底层数据操作自然少不了。当时团队里用的是MyBatis这个持久层框架,说实话,第一次看到XML里面写SQL语句的时候,我还挺懵的。
之前在学校学过Hibernate和Spring Data JPA,那种“面向对象”风格的ORM用起来确实挺方便,但到了实际项目中才发现,有时候你对数据库的控制要求更高,比如需要优化复杂查询、动态拼接条件、处理多表关联等等。这种时候,JPA的优势反而成了劣势——它隐藏得太深了,有些东西你想改都改不动。
而MyBatis不一样,它就像是一个“半自动”的 ORM 框架,既保留了灵活性,又提供了封装,尤其适合我们这些对性能、细节有追求的后端开发者。
于是,从那时起,我开始正式学习并深入使用MyBatis,也踩了不少坑。今天就结合我的亲身经历,分享一下我在工作中是如何一步步掌握MyBatis,并将其应用到真实项目中的。
问题描述:一次简单的查询却引发的大麻烦

还记得那次任务是写一个用户信息查询接口,支持根据手机号、用户名或者状态过滤。本来我以为就是个简单的REST API,写个Mapper XML配上DAO接口就能搞定的事儿。
结果上线没多久,测试同学反馈说页面加载很慢,而且在某些筛选条件下会出现超时。我一看日志,发现SQL执行时间非常长,甚至出现了全表扫描的情况。再仔细看代码才发现,我写的SQL语句是这样的:
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="phone != null">
AND phone = #{phone}
</if>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
看起来没什么大问题,但是当传入name参数的时候,LIKE的模糊匹配导致索引失效,再加上没有分页,直接查了整张表。这在数据量小的时候没问题,但一旦用户增长上去,系统就扛不住了。
这个问题给了我当头一棒,也让我意识到,MyBatis虽然灵活,但不是万能钥匙。要想用得好,得懂它的门道,还得了解底层数据库是怎么工作的。
解决方案:一步步打造高效稳定的MyBatis应用
为了解决上面的问题,我做了几件事:
第一步:引入分页机制
首先,最直接的办法就是给这个查询加上分页。MyBatis原生不支持分页,但我们可以通过插件来实现,也可以手动写LIMIT语句。
考虑到后续可能会用到统一的分页封装,最后决定采用PageHelper这个常用的分页插件。
在pom.xml中引入:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
然后在Controller里:
@GetMapping("/users")
public PageInfo<User> getUsers(@RequestParam(required = false) String name,
@RequestParam(required = false) String phone,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.findUsers(name, phone, status);
return new PageInfo<>(users);
}
这样,每次查询就不会一次性把所有数据读出来,而是按页返回,性能大幅提升。
第二步:优化模糊查询策略
LIKE模糊查询的问题依然存在。我跟DBA商量了一下,决定对name字段做部分冗余,建立前缀索引。同时修改查询逻辑,只允许左匹配(比如搜索“xiaoming”,而不是“ming”):
<if test="name != null">
AND name LIKE CONCAT(#{name}, '%')
</if>
这样可以让MySQL命中索引,效率提升不少。
第三步:缓存热点数据
为了进一步减少数据库压力,我们还引入了一级缓存和二级缓存。
- 一级缓存:默认开启,同一个SqlSession内有效。
- 二级缓存:跨SqlSession共享,我们在mapper级别加了一个基于Redis的缓存组件,配合Spring Cache做集成。
<!-- 在mapper XML顶部添加 -->
<cache />
搭配Spring Boot的话,只需要配置@EnableCaching,并在方法上加@Cacheable注解即可。
不过这里也有个小坑,记得MyBatis的二级缓存默认是基于namespace级别的,如果你的数据更新频繁,一定要合理设置过期时间和失效策略,否则容易出现缓存不一致的问题。
效果总结:从卡顿到流畅的转变
经过这一系列优化,效果很明显:
- 原本耗时3秒以上的查询,现在基本稳定在50ms以内;
- 数据库CPU和内存占用明显下降;
- 接口响应更加稳定,基本没有超时现象;
- 部分常用查询命中缓存,几乎没有走数据库。
最关键的是,系统的可维护性更强了。有了PageHelper之后,其他同事写类似的接口也能快速上手;有了缓存机制,很多高频查询都能复用已有能力。
经验分享:那些年我踩过的坑和收获
作为一个经历过无数线上事故的老司机,我觉得有必要把我这些年积累的一些经验和建议分享给你们。
1. 别让MyBatis帮你做太多事
很多人喜欢在XML里写特别复杂的SQL,比如嵌套多个子查询、left join一堆表,最后还要动态拼接条件……这不是不能写,但真的不好调试,也不利于后期维护。
建议:
- SQL尽量保持清晰简洁;
- 如果逻辑太复杂,可以考虑拆分成多个接口,或者通过Service层做组合;
- 能不用
和 的地方就别用,实在要用也要注意边界情况。
2. 合理使用连接池,避免资源耗尽
MyBatis本身不负责连接池管理,大多数情况下我们会配合Druid、HikariCP这类连接池工具一起使用。
有一次生产环境突然出现大量数据库连接等待的现象,后来发现是某个定时任务执行时间太久,占用了所有连接,其他请求全都卡死了。
建议:
- 设置合理的最大连接数和最小空闲数;
- 给不同类型的请求(如核心业务和非核心异步任务)分配不同的连接池;
- 监控连接池使用情况,及时预警。
3. 明确区分Query与Update语义
MyBatis不会强制你必须区分查询和写操作,但在实际项目中,我们要明确哪些SQL是用来读取数据,哪些用来写入。尤其是在主从架构下,查询应该尽可能打到从库,写操作则打到主库。
我们采用了ShardingSphere做读写分离,MyBatis的SQL直接发往对应节点。
建议:
- 合理划分接口类型;
- 使用注解或拦截器来标注SQL用途;
- 读多写少的场景优先走从库。
4. 动态SQL要小心,别让它失控
MyBatis最强大的地方在于它的动态SQL功能,比如
但我见过有些人在一个WHERE子句里塞七八个条件判断,根本看不懂是什么逻辑。
建议:
- 尽量让每个查询职责单一;
- 复杂条件可以用Java代码拼接后传入;
- 避免在SQL中做过多的条件判断,保持SQL本身的简洁性。
5. 日志和监控很重要,不要等到出事才想起来
我们一开始没注意SQL日志输出,在排查问题的时候非常被动。后来在application.yml中加了如下配置:
logging:
level:
com.example.mapper: debug
这样可以在控制台看到完整的SQL语句,包括参数绑定情况,排查问题快多了。
另外,还建议接入像SkyWalking或者Prometheus+Grafana这样的监控系统,实时查看SQL执行时间和异常次数,防患于未然。
结语:技术只是工具,思路才是关键
说实话,刚接触MyBatis的时候,我并不觉得它有多神奇。它不像Spring那样高屋建瓴地设计整个生态,也不像Netty那样让你感受到高性能编程的魅力。但正是这种简单而实用的设计理念,让它成为了Java工程师们最爱用的框架之一。
在真实项目中,你会遇到各种各样的问题,比如SQL慢查询、缓存穿透、事务混乱、连接泄漏……这些问题,都不是单纯靠一个框架能解决的,它们需要你有足够的工程经验去判断、去分析、去权衡。
希望这篇结合了我多年工作经验的文章,能帮助你在MyBatis这条路上少走弯路。记住一句话:框架终会变,架构会演进,但解决问题的能力和对技术的热爱,永远不会过时。
如果你也是刚入门的新手,别怕犯错,多实践,多问前辈,相信不久的将来,你也能写出稳定高效的SQL代码。
共勉!

评论 0