MyBatis 基础教程:Java 持久层框架入门
上周五晚上十点半,我正对着公司那套老掉牙的 JPA 代码发呆——不是因为浪漫,是因为又双叒叕一个线上慢 SQL 把数据库 CPU 干到 90%。我们组去年双11期间就因为这个吃过亏,运维兄弟半夜打电话过来语气都快哭了:“哥,你们能不能别用 findAll() 了?”
我是小林,一个刚晋升技术组长、在当前团队混了快两年的 Java 老油条。平时热衷参加各种云原生和 K8s 的线下分享会(主要是为了蹭饭),但最近被领导“委以重任”:重构核心订单模块的持久层。理由很现实——“你简历上不是写了‘精通 ORM 框架’吗?那正好上”。
得,这锅我背了。
为什么选 MyBatis?
其实一开始我想直接上 Go 写个微服务把这块逻辑拆出去(别笑,我们组真有人这么干过)。但现实是:系统耦合太深,测试覆盖率低得可怜,连单元测试都没几行,产品经理还天天催着加新功能。Go 是好,但在这堆祖传 Java 代码里硬塞 Go 服务,怕不是要触发“多语言地狱”成就。
于是我和队友们一合计:不如把 JPA 换成 MyBatis。原因很简单:
- SQL 可控:再也不用猜 Hibernate 到底生成了啥语句
- 学习成本低:XML + 注解,写起来像手写 SQL,新人上手快
- 性能调优方便:慢查询直接看 XML 文件,不用翻半天源码
而且,说真的,现在面试问“MyBatis 原理”的频率可不比问“红黑树”低。我上次帮 HR 筛简历,看到一堆人写“熟悉 MyBatis”,结果连一级缓存和二级缓存的区别都说不清……(此处省略对算法岗同学的羡慕)
实战踩坑:从零配置 MyBatis
我们用的是 Spring Boot 2.7 + MyBatis 3.5。第一步当然是加依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
然后在 application.yml 里配数据源(这里用了 Druid,顺便开了监控):
spring:
datasource:
url: jdbc:mysql://prod-db:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: ********
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
重点来了:map-underscore-to-camel-case 必须开!不然数据库字段 create_time 映射不到 Java 对象的 createTime,你就会像我一样,在凌晨两点对着 null 值怀疑人生。
Mapper 接口 + XML,YYDS
建个实体类:
public class Order {
private Long id;
private String orderNo;
private LocalDateTime createTime;
// getter/setter...
}
再写个 Mapper:
@Mapper
public interface OrderMapper {
List<Order> selectByUserId(@Param("userId") Long userId);
@Select("SELECT * FROM orders WHERE id = #{id}")
Order selectById(Long id);
}
对应的 XML(mapper/OrderMapper.xml):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.OrderMapper">
<select id="selectByUserId" resultType="com.example.model.Order">
SELECT id, order_no, create_time
FROM orders
WHERE user_id = #{userId}
AND status != 'DELETED'
ORDER BY create_time DESC
LIMIT 100
</select>
</mapper>
注意:XML 里我显式写了字段名,而不是 SELECT *。这是血的教训——有一次 DBA 在表里加了个 blob 字段存日志,结果整个接口响应时间从 50ms 飙到 800ms。从此以后,我们组 Code Review 第一条就是:“禁止 SELECT *”。
性能调优:缓存与分页
MyBatis 有一级缓存(SqlSession 级别)和二级缓存(Mapper 级别)。默认一级缓存开启,但 Spring Boot 里每次请求都会新建 SqlSession,所以基本无效。我们开了二级缓存:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
不过要小心:二级缓存只适用于读多写少的场景。我们订单表写操作频繁,最后还是关了,改用 Redis 做业务缓存。
至于分页,千万别自己写 LIMIT #{offset}, #{size}!我们早期这么干,结果遇到深度分页(比如第 10000 页)直接拖垮 DB。后来统一接入了 MyBatis-PageHelper:
PageHelper.startPage(1, 10);
List<Order> orders = orderMapper.selectByUserId(userId);
它会自动重写 SQL 成 SELECT ... LIMIT 10 OFFSET 0,并且支持多种数据库方言。亲测有效,线上慢查询下降 70%。
生产环境避坑指南
- N+1 查询问题:别在循环里调 Mapper 方法!用
<collection>或者提前 JOIN。 - 事务管理:MyBatis 本身不管理事务,靠 Spring 的
@Transactional。但要注意:同一个类内方法调用不会触发事务(代理失效),我因此导致过一次资损,差点被祭天。 - SQL 注入?:MyBatis 的
#{}是预编译,安全;但${}是字符串拼接,危险!我们用 SonarQube 扫描,发现一个${tableName}动态表名,吓得立刻改成白名单校验。
下面是个对比表格,总结 MyBatis 和 JPA 在我们项目中的表现:
| 维度 | JPA (Hibernate) | MyBatis |
|---|---|---|
| SQL 可控性 | 低(自动生成,难优化) | 高(手写,精准调优) |
| 学习曲线 | 陡(注解/继承/懒加载) | 平缓(接近原生 SQL) |
| 性能(复杂查询) | 差(N+1、笛卡尔积) | 优(可控 JOIN) |
| 调试难度 | 高(需看生成 SQL) | 低(直接看 XML) |
| 团队接受度 | 中(新人容易写错) | 高(DBA 也看得懂) |
最后一点感悟
说实话,MyBatis 不是什么新技术,但它胜在“透明”和“可控”。在这个大家都在卷云原生、Service Mesh、eBPF 的时代,有时候回归基础反而更高效。
上周上线后,订单查询 P99 从 320ms 降到 45ms,运维兄弟终于没再半夜打电话。产品经理也难得夸了句:“这次接口真快!”——虽然下一秒就说“那能不能再加个导出 Excel 功能?”
唉,程序员的命运啊。
不过话说回来,如果你正在准备跳槽,MyBatis 这块真得吃透。我面过几个候选人,问“MyBatis 插件怎么实现的?”,90% 回答不上来。其实就四个字:动态代理。底层用 Interceptor 拦截 Executor、StatementHandler 这些组件,和 AOP 思想一模一样。
对了,说到算法——虽然 MyBatis 本身不涉及复杂算法,但理解其缓存淘汰策略(LRU)、分页优化(游标 vs offset)背后的思想,其实和 LeetCode 刷题是相通的。别光刷题不实战,也别光写 CRUD 不思考。
好了,今天就水到这里。下期打算聊聊“如何用 K8s + ArgoCD 实现 MyBatis 配置热更新”(老板说要上云原生,我总得找点活干)。
PS:如果你也在被 ORM 框架折磨,评论区聊聊你的血泪史?或者直接甩简历给我(开玩笑的,HR 说今年 HC 冻结了 😭)

评论 0