MyBatis上手踩坑实录:一个前端仔被迫写后端的血泪史
上周五晚上十点半,我还在公司对着IDEA发呆。窗外国贸三期的霓虹灯闪得我眼睛疼,地铁末班车快赶不上了,但手里的这个Java接口还没跑通。作为一个刚入职两周、试用期都还没过的新兵蛋子,我——一个原本只跟CSS动画和React hooks打交道的“纯前端”,居然被临时抓壮丁去给后端项目打补丁。
原因?产品经理在周一例会上拍脑袋说:“我们后台管理页的数据加载太慢了,能不能优化一下?”结果技术负责人老张一转头就指着我说:“小李不是会点Java吗?你先顶两天,把MyBatis这块捋顺了。”
我?会Java?
我只是在简历上写了句“了解Spring Boot基础”啊!但看着团队里其他人都在赶双11大促的紧急需求,我只能硬着头皮点头。
更离谱的是,我平时写前端全靠ChatGPT和Claude续命,连git rebase都要问AI。现在突然要手写SQL映射、配数据源、处理事务……那一刻,我真的想砸电脑。
不过话说回来,既然干了这行,就得支棱起来。于是接下来三天,我一边通勤一小时从回龙观往望京赶(北京早高峰的13号线,懂的都懂),一边疯狂啃MyBatis文档、看B站教程、问Claude“为什么我的Mapper不生效”。今天终于把核心逻辑跑通了,赶紧写篇技术分享,既是给自己复盘,也希望能帮到同样被“前后端不分家”文化毒打的兄弟们。
为什么是MyBatis?而不是JPA或者直接写JDBC?
说实话,刚接触Java持久层时,我一度以为所有公司都用Spring Data JPA——毕竟它“约定大于配置”,写个接口就能自动生成SQL,多省事。但到了我们组才发现,老系统用的是MyBatis,而且是那种XML写法的“传统派”。
问了下老张,他说:“我们业务复杂,很多查询要动态拼接,还要手动优化SQL性能。JPA太黑盒,线上出问题根本没法调。”
这话说得有道理。比如我们有个用户行为分析接口,要根据时间范围、设备类型、地域等多个条件动态过滤,还涉及多表JOIN和分页。用MyBatis的<if>标签和<foreach>,写起来反而更直观可控。
顺便吐槽一句:我们系统居然还在用MySQL 5.6,连窗口函数都不支持。运维大哥说“稳定压倒一切”,我只能默默在MyBatis里手写子查询……
MyBatis核心三件套:配置、Mapper、实体
别被网上那些“十分钟入门MyBatis”的教程骗了。真正上手你会发现,光是把环境配通就能卡住新人半小时。
1. 配置文件:别再用XML了,试试注解+YAML
我们项目用的是Spring Boot + MyBatis,所以直接在application.yml里配:
spring:
datasource:
url: jdbc:mysql://localhost:3306/myapp?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.myapp.entity
configuration:
map-underscore-to-camel-case: true
重点来了:map-underscore-to-camel-case: true 这个必须开!不然数据库字段user_id就映射不到Java对象的userId属性,查出来全是null,debug到怀疑人生。
2. Mapper接口:别忘了@Mapper注解
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectByConditions(@Param("name") String name, @Param("status") Integer status);
}
注意两点:
- 接口上要加
@Mapper,或者在启动类加@MapperScan("com.example.myapp.mapper") - 多参数必须用
@Param标注,否则MyBatis不知道XML里该用#{name}还是#{param1}
3. XML映射文件:动态SQL才是灵魂
<select id="selectByConditions" resultType="User">
SELECT id, user_name, status, created_at
FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND user_name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
这里有个坑:WHERE 1=1虽然不优雅,但能避免动态条件导致的SQL语法错误。当然,你也可以用<where>标签自动处理AND/OR前缀:
<where>
<if test="name != null">...</if>
<if test="status != null">...</if>
</where>
那些让我凌晨三点还在改的坑
坑1:事务失效——Spring AOP的锅
我以为在Service方法上加个@Transactional就完事了。结果测试时发现,即使抛异常,数据还是被插入了!
后来才知道:同一个类中,非事务方法调用事务方法,事务不会生效。因为Spring的事务是基于代理的,内部调用绕过了代理对象。
解决方案:要么把逻辑拆到另一个Service,要么注入自己(@Autowired private UserService self;),然后通过self.xxx()调用。虽然丑,但管用。
坑2:N+1查询——别让MyBatis变成性能杀手
一开始我图省事,在User实体里直接关联了Role列表:
public class User {
private Long id;
private String userName;
private List<Role> roles; // 一对多
}
结果一个selectUsers接口,查100个用户,MyBatis发了101条SQL(1条查用户 + 100条查角色)。页面加载慢得像PPT。
解决办法:
- 简单场景:用
<collection>做JOIN查询,一次搞定 - 复杂场景:放弃懒加载,手动分两次查,用Map聚合
<resultMap id="UserWithRoles" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<collection property="roles" ofType="Role">
<id property="id" column="role_id"/>
<result property="roleName" column="role_name"/>
</collection>
</resultMap>
坑3:分页插件冲突——PageHelper的坑
我们用了PageHelper做物理分页,代码如下:
PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
看似没问题,但如果后面还有别的查询,PageHelper的分页参数会污染下一个SQL!必须确保startPage和查询紧挨着,中间不能有任何其他数据库操作。
更好的做法是用PageHelper.offsetPage()或直接返回PageInfo对象。
面试题高频考点:MyBatis vs 其他框架
最近在准备跳槽面试(没错,试用期就想跑,都是被逼的),整理了几个MyBatis经典面试题:
| 问题 | 回答要点 |
|---|---|
| MyBatis一级缓存和二级缓存的区别? | 一级缓存是SqlSession级别,默认开启;二级缓存是Mapper级别,需手动配置,跨SqlSession共享。但生产环境慎用二级缓存,容易脏读。 |
| #{} 和 ${} 的区别? | #{} 是预编译占位符,防SQL注入;${} 是字符串替换,用于动态表名/列名,但有注入风险。 |
| MyBatis如何防止SQL注入? | 主要靠#{}预编译。但动态表名、ORDER BY字段等场景必须用${},此时需手动校验输入合法性。 |
| MyBatis执行流程? | 加载配置 → 创建SqlSessionFactory → 获取SqlSession → 执行Mapper方法 → 调用Executor → 处理结果映射 |
特别提醒:如果你说“MyBatis比Hibernate好”,面试官可能会追问“好在哪”。别只说“灵活”,要结合业务场景,比如“我们系统需要大量报表查询,SQL优化空间大,MyBatis更适合”。
说好的区块链和Go呢?
我知道你在找关键词。别急,虽然本文主题是MyBatis,但作为互联网民工,谁还没点跨界幻想?
其实我们组最近在调研一个新项目:用区块链存证用户操作日志。听起来高大上,但落地时发现,链上存储成本太高,最终方案是:关键操作哈希值上链,原始数据仍存在MySQL。
这时候MyBatis就派上用场了——我们需要高效地批量插入操作日志,并保证与链上哈希的一致性。于是我在Service层加了分布式事务(Seata),虽然最后因为性能问题砍掉了,但至少让我明白了:再酷的技术,也得向数据库低头。
至于Go?我司新微服务全用Go重写了。老张说:“Java太重,Go启动快、内存小,适合做边缘计算节点。”
我默默看了眼自己写的MyBatis XML,心想:也许下次跳槽,真该学学Go的GORM了。
写在最后:一个前端仔的后端初体验
折腾一周下来,我对MyBatis的态度从“抗拒”变成了“真香”。它不像JPA那样全自动,但给了开发者足够的控制权。尤其是在需要精细优化SQL的场景下,MyBatis的灵活性无可替代。
当然,我也深刻体会到:前后端分离不是万能的。当你的前端组件再炫酷,后端查个列表要5秒,用户体验照样崩盘。所以,哪怕你是纯前端,也该懂点数据库、懂点SQL、懂点ORM原理。
今天上线后,用户反馈“后台快多了”。老张拍拍我肩膀:“不错,下周开始参与核心模块开发。”
我表面微笑,内心OS:救命,我只是想写CSS啊!
但没办法,这就是程序员的成长之路——被需求推着走,被bug逼着学。所幸有ChatGPT帮我写SQL模板,有Claude解释ResultMap原理,还有北京晚高峰的地铁陪我一起emo。
如果你也在试用期挣扎,别怕。每个大佬都曾是菜鸟,每个架构师都曾为一行SQL熬通宵。
共勉。

评论 0