从 CRUD 到 MyBatis 入门:一个后端工程师的真实踩坑之路
记得我刚入行那会儿,还在实习公司做了一个简单的用户管理系统。项目用的是 Spring Boot + MySQL,最开始数据访问层直接写 JDBC,后来被导师一顿批评:“小伙子,你这是想把代码写成数据库连接工厂吗?”他推荐我使用 MyBatis,从此我就和这个“半自动化 ORM 框架”结下了不解之缘。
今天我想结合自己这几年在多个实际项目中使用 MyBatis 的经验,给大家讲一讲这个 Java 持久层框架的基础内容,以及我在真实开发过程中遇到的一些问题、踩过的坑,还有我的解决思路。希望能帮你在学习 MyBatis 的路上少走点弯路。
背景介绍:为什么选择 MyBatis?

我们先不说技术细节,先来聊一聊为什么要使用 MyBatis 这个持久层框架。在我参与的第一个正式项目里,是一个订单系统的重构工程,系统需要对接大量的订单查询、插入、更新操作。项目前期用的是 Hibernate,结果上线一段时间后发现性能瓶颈非常严重,尤其是在批量插入和复杂查询场景下,生成的 SQL 十分臃肿,维护成本极高。
当时团队讨论后决定换用 MyBatis,理由很现实也很朴素:
- 灵活性高:SQL 完全由开发者控制,适合各种复杂的业务逻辑。
- 轻量级设计:不依赖于特定数据库特性,可移植性强。
- 与原生 SQL 接近:更适合有 DBA 背景的团队协作(比如我们当时的 DBA 爷爷就特别喜欢看我们写的 SQL)。
我遇到的第一个坑:MyBatis 的映射配置写起来太痛苦!

项目初期,我们采用了最原始的 XML 配置方式来写 SQL 映射。刚开始还觉得挺清晰的,但随着表结构增多,每个实体类都要配一个 Mapper.xml 文件,再加上 ResultMap 写得眼花缭乱,整个项目目录变得异常臃肿。
举个例子,比如我们有一个 Order 表,字段很多,还有嵌套的用户信息、商品信息,这时候 ResultMap 就得手动绑定每一列:
<resultMap id="orderWithUserResultMap" type="Order">
<id property="orderId" column="order_id"/>
<result property="totalAmount" column="total_amount"/>
<association property="user" javaType="User">
<id property="userId" column="user_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
说实话,每次看到这玩意我都觉得头大 😵。更头疼的是,一旦数据库字段名变了,不仅要改 SQL,还要去 ResultMap 里一个一个对应调整。
解决方案:注解+XML混用,适当封装通用逻辑
后来我们做了几个优化:
对于简单查询使用注解:
@Select("SELECT * FROM orders WHERE user_id = #{userId}") List<Order> findByUserId(Long userId);清晰又方便,不用写一堆 XML。
对复杂查询保留 XML:像涉及多表关联、动态 SQL 这种情况,还是用 XML 更直观。
统一定义 BaseMapper 和通用 ResultMap: 我们抽象了一个
BaseEntity类,所有的 Entity 继承它并实现一个接口,然后通过泛型的方式,在通用的 XML 中复用一些基础字段的映射。这样减少了大量重复代码。引入 Lombok:减少 Getter/Setter 冗余,简化 Bean 定义。
动态 SQL 的坑:IF ELSE 写多了容易懵逼

还记得有一次我们要实现一个条件搜索订单的功能,要根据状态、时间范围、用户ID等多个参数进行筛选。
刚开始我们是这么写的:
<select id="searchOrders" resultType="Order">
SELECT * FROM orders
WHERE 1=1
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</if>
...
</select>
看起来没问题吧?其实埋了不少隐患。
出现的问题:
当 startTime 或 endTime 为 null 时,整个条件失效。因为
<if>的判断只有两个都非空才执行。拼接出来的 SQL 可读性差,测试调试麻烦。
有时候忘了加
AND,或者误用了OR,导致条件错误。
最终解决方案:使用 <where> 标签 + <choose>/<when> 分支处理
我们后来优化成了这样:
<select id="searchOrders" resultType="Order">
SELECT * FROM orders
<where>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
</where>
</select>
这里重点是 <where> 标签会自动去掉开头多余的 AND 或 OR,再也不用担心条件拼错了。
更复杂的场景还可以配合 <choose>/<when>:
<choose>
<when test="id != null">
AND order_id = #{id}
</when>
<otherwise>
AND user_id = #{userId}
</otherwise>
</choose>
缓存的问题:本地缓存和二级缓存别乱用!
在某个电商项目中,我们为了提升商品详情页的访问速度,开启了 MyBatis 的二级缓存功能,结果上线之后出了大问题:用户看到的数据不是最新的,特别是库存信息出现了延迟更新的情况。
原因分析:
- MyBatis 的二级缓存默认是基于 Mapper 的,也就是说同一个 Mapper 下的所有方法共享一个缓存空间。
- 没有设置合理的过期策略或刷新机制。
- 在分布式环境下,各节点的缓存无法同步,导致数据不一致。
最终处理方式:
- 关闭 MyBatis 自带的二级缓存。
- 改为接入 Redis 缓存,统一管理缓存生命周期。
- 对于关键业务数据,采用主动清除缓存或 TTL 控制。
如果你确实需要用到二级缓存,建议注意几点:
- 只用于读多写少的静态数据,比如字典表、分类信息等。
- 启用前评估是否需要跨应用共享缓存。
- 合理设置 flushInterval,避免内存溢出。
数据库事务与批量操作:MyBatis 支持得挺好的,但你自己得懂

有一回我们做导入订单的批量插入功能,几十万条数据一次导入,一开始是单条插入,效率感人,每秒只能插入几十条。
后来我们改成批量插入:
<insert id="batchInsert">
INSERT INTO orders (order_sn, user_id, total_amount)
VALUES
<foreach collection="orders" item="order" separator=",">
(#{order.orderSn}, #{order.userId}, #{order.totalAmount})
</foreach>
</insert>
但上线后发现一个问题:如果某一条数据插入失败了,整个语句都会报错,之前的也插不进去。这对于部分错误的数据来说简直是灾难!
解决办法:
- 使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 处理冲突。
- 开启 事务控制,将整个批量插入放在一个事务里,并且加入 try-catch 处理异常,按需回滚或记录错误日志。
- 拆分大批量数据为小批次插入,例如每 500 条为一组,可以降低失败影响范围。
此外,MyBatis 默认是手动提交事务的,所以要注意:
SqlSession session = sqlSessionFactory.openSession();
try {
OrderMapper mapper = session.getMapper(OrderMapper.class);
mapper.batchInsert(orders);
session.commit(); // 记得 commit!
} catch (Exception e) {
session.rollback(); // 出错 rollback!
} finally {
session.close();
}
性能调优技巧分享
作为一个经历过线上服务压力测试的人,我想给你一些实战级别的建议:
1. 尽量不要使用 SELECT *
哪怕是为了省事也不要用,尤其是当你查的是一张大表的时候。指定字段不仅减少网络传输,还能让索引命中率更高。
SELECT order_id, user_id, create_time FROM orders WHERE ...
2. 分页慎用 OFFSET
OFFSET 在大数据量分页时会导致性能急剧下降,应该结合 ID 索引来实现滚动分页。
3. 开启慢查询日志
MyBatis 提供了 log4j 集成插件,可以在开发阶段监控到底层执行的 SQL,帮助你排查低效查询。
4. 使用 Druid 监控 SQL 执行情况
Druid 是阿里巴巴开源的一个数据库连接池组件,同时也有非常强大的 SQL 监控能力。可以帮你定位热点 SQL、统计执行耗时,甚至发现未释放连接等问题。
结合 Spring Boot 的最佳实践
如果你正在使用 Spring Boot,集成 MyBatis 也非常方便。常见的做法如下:
引入依赖:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>配置 Mapper 包扫描路径:
mybatis: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.example.model使用
@MapperScan注解扫描 Mapper 接口。
这种方式极大简化了传统 Spring 整合 MyBatis 的配置,推荐所有新手使用!
心得体会与建议
回顾这些年用 MyBatis 的过程,我觉得有几个原则是必须坚持的:
- SQL 是你的朋友,不要完全交给框架处理。很多时候,一条写得漂亮的 SQL 比 N 层封装更能解决问题。
- 保持良好的命名习惯。无论是表字段、Java 实体属性,还是 Mapper 方法,命名规范会让维护成本大幅降低。
- 善用日志工具。开发阶段务必打开 SQL 输出,方便快速定位问题。
- 重视数据库索引设计。MyBatis 写得好不如数据库快,索引是性能的第一道防线。
- 多跟 DBA 同事交流。他们的经验会让你写出更高效的 SQL。
结语:MyBatis 不是最好的,但最适合你才是关键
MyBatis 并不是一个完美无瑕的框架,它要求开发者有一定的 SQL 功底。但对于追求性能、重视数据层面的系统来说,它依然是目前市面上最灵活、最可控的持久层框架之一。
这篇文章我尽量用真实案例和踩坑经历来分享 MyBatis 的入门知识,希望对你有所帮助。如果你也在使用 MyBatis,欢迎留言一起交流踩坑心得,说不定我们还会遇到同样的问题呢 😄。
记住一句话:不要迷信框架,要理解本质。无论未来是否继续使用 MyBatis,理解其背后的原理,才是真正值得你带走的东西。

评论 0