MyBatis上手踩坑实录:一个前端仔被迫写后端的血泪史

Rust练习生
2026-01-03 06:29
阅读 1858

上周五晚上十点半,我还在公司对着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

最热最新
暂无评论
Rust练习生Lv.1
0
影响力
0
文章
0
粉丝