MyBatis入门没那么难,但坑真不少
上周五晚上十点半,我正窝在工位上用Vim敲着一个动画过渡效果的JS逻辑,突然钉钉“叮”了一声——后端小哥甩过来一个链接:“兄弟,帮看看这个SQL查得慢不慢?”点开一看,好家伙,MyBatis的XML里嵌了三层<foreach>,还夹杂着LIKE '%${value}%'。那一刻我真想把键盘砸了:咱前端都快被运营催成陀螺了,你还给我整这种性能炸弹?
说来也巧,最近我在准备跳槽(阿里三年多,双11熬过三轮,是时候换个空气了),刷面试题时发现,MyBatis几乎是Java后端岗的必考项,连一些偏全栈的岗位也会问几句。虽然我主攻前端,但跟后端联调、看日志、查慢SQL这些事,哪次不是亲力亲为?尤其去年双11大促期间,我们一个商品详情页接口因为MyBatis配置不当,缓存穿透直接打崩数据库,CTO半夜拉群@所有人,那场面至今记忆犹新。
所以,与其每次都被后端甩锅说“前端传参不对”,不如自己动手搞明白这玩意儿到底怎么玩。今天这篇,就当我这个Vim党+前端工程师跨界踩坑的实战笔记,给想了解MyBatis的同学铺条路。
为什么是MyBatis?不是JPA,也不是手写JDBC?
先说背景。我们系统早期用的是纯JDBC,每次查个用户信息都要写十几行try-catch,代码又臭又长。后来团队引入MyBatis,主打一个“SQL和代码分离”——SQL写在XML里,Java只管调用接口。好处很明显:
- SQL可读性高,DBA看了直呼内行
- 动态查询(比如运营后台那种“按条件筛选订单”)写起来不痛苦
- 跟Spring Boot集成贼顺,自动装配一配,直接
@Autowired注入
反观JPA(比如Hibernate),虽然“对象关系映射”听起来高大上,但在我们这种高并发、复杂查询多的场景下,生成的SQL往往不够精准,调优成本反而更高。而Go语言那边,很多团队直接用gorm或原生SQL,反倒更灵活——这让我一度怀疑,是不是Java生态太爱封装了?
三步走:从零跑通一个MyBatis查询
别被文档吓到,其实核心就三件事:配置、接口、SQL。
第一步:Maven依赖 + 配置文件
<!-- pom.xml -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
然后在application.yml里配数据源(省略密码等敏感信息):
spring:
datasource:
url: jdbc:mysql://localhost:3306/shop_db?useSSL=false&serverTimezone=UTC
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
注意:别忘了加mybatis.mapper-locations=classpath:mapper/*.xml,不然XML文件找不到,启动直接报错,我第一次就栽这儿了。
第二步:写Mapper接口
// UserMapper.java
public interface UserMapper {
User selectById(Long id);
List<User> selectByStatus(@Param("status") Integer status);
}
这里有个坑:方法参数超过一个时,必须加@Param注解,否则XML里拿不到变量名。别问我怎么知道的,上次联调时我传了个status和page,结果XML里写成#{status}却报“找不到参数status”,差点以为是前端JSON格式错了。
第三步:写XML映射文件
<!-- mapper/UserMapper.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.UserMapper">
<select id="selectById" resultType="com.example.model.User">
SELECT * FROM users WHERE id = #{id}
</select>
<select id="selectByStatus" resultType="com.example.model.User">
SELECT * FROM users
WHERE status = #{status}
ORDER BY created_at DESC
</select>
</mapper>
重点来了:namespace必须和接口全路径一致!大小写、包名一个都不能错。有一次我把UserMapper写成userMapper,IDE没报错,但运行时直接404,排查半小时才发现是XML命名空间写歪了。
运营需求来了?动态SQL救你命
说到运营,他们最爱提这种需求:“我要能按用户ID、手机号、注册时间范围、状态……任意组合筛选!” 如果用JDBC,你得拼字符串,SQL注入风险拉满。而MyBatis的<where> + <if>组合,简直为此而生:
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="userId != null">
AND id = #{userId}
</if>
<if test="phone != null and phone != ''">
AND phone LIKE CONCAT('%', #{phone}, '%')
</if>
<if test="startTime != null">
AND created_at >= #{startTime}
</if>
<!-- 更多条件... -->
</where>
</select>
注意两点:
<where>会自动去掉第一个AND,避免语法错误- 字符串判空别只写
!= null,还得加!= '',否则前端传个空字符串,SQL变成LIKE '%%',全表扫描警告!
性能优化:别让MyBatis拖垮你的双11
作为经历过双11的人,我对“慢SQL”有PTSD。MyBatis本身不慢,但用不好就是灾难。分享几个生产环境血泪经验:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| N+1查询 | 查100个订单,每个订单再查用户,共101次SQL | 用<collection>做关联查询,或改成分页+批量ID查询 |
| 大结果集无分页 | 运营导出全量用户,内存爆了 | 强制加LIMIT,或用RowBounds分页(但慎用,性能差) |
| 模糊查询无索引 | LIKE '%关键词%' 导致全表扫描 |
改用Elasticsearch,或限制只能前缀匹配LIKE '关键词%' |
特别提醒:MyBatis的一级缓存(SqlSession级别)在线程池环境下基本无效,别指望它扛住高并发。真要缓存,老老实实用Redis,或者开启二级缓存(但要注意脏读问题)。
面试题高频考点:MyBatis怎么和Spring整合的?
最近帮朋友模拟面试,发现这个问题几乎必问。简单说:MyBatis-Spring通过MapperFactoryBean把Mapper接口代理成Spring Bean。当你@Autowired UserMapper时,Spring其实注入的是一个动态代理对象,调用方法时会委托给SqlSession执行SQL。
底层原理涉及BeanDefinitionRegistryPostProcessor和ClassPathMapperScanner,但面试时你只要说出“MyBatis-Spring利用Spring的扩展点,自动扫描Mapper接口并注册为Bean”就够了。再深入点,可以聊聊SqlSessionFactoryBean如何创建SqlSessionTemplate——不过这时候面试官可能已经在点头了。
写在最后:技术没有银弹,只有合适与否
说实话,作为一个前端,写这篇MyBatis教程有点“越界”。但在这个前后端边界越来越模糊的时代,懂一点后端,联调时不背锅;懂一点数据库,排查问题不抓瞎。更何况,面试时你能聊清楚MyBatis的缓存机制、动态SQL原理,HR都会觉得你“技术视野广”。
至于要不要学Go?我觉得不用焦虑。Java在企业级应用里还是稳如老狗,MyBatis这套玩法短期内不会淘汰。倒是那些整天喊“抛弃Java”的,可能还没经历过凌晨三点被DBA电话叫醒改SQL的痛。
好了,Vim保存退出,我去改那个动画bug了。希望这篇能帮你少踩几个坑——毕竟,程序员的时间,应该花在创造价值上,而不是重复造轮子,或者背锅。

评论 0