MyBatis入门:从被ORM折磨到写出可读SQL的佛系之路
上周五晚上十点,我正戴着耳机听Lo-fi beats,一边摸鱼刷GitHub,一边改一个“简单”的数据查询接口。产品经理说:“这个需求很简单,就加个状态筛选嘛。”结果一查,用的是公司老项目里那套手写JDBC模板——Connection、PreparedStatement、ResultSet轮番上阵,try-catch-finally嵌套三层,连注释都写着“勿动,线上跑着”。
那一刻,我真想把键盘扔进黄浦江。
但作为佛系打工人,砸电脑是不行的,毕竟下个月房租还没交。于是,我默默打开IDEA,新建了一个模块,决定用MyBatis重写这个接口。不是为了卷,纯粹是为了让自己下班前能安心听完整首歌。
为什么又是MyBatis?
其实我们团队去年双11前就讨论过要不要换ORM框架。有人推JPA,说“约定优于配置”;有人站Spring Data JDBC,说“轻量又干净”;还有人偷偷用Kotlin Exposed,结果被运维大哥骂“线上日志看不懂”。
最后技术总监拍板:“老项目不动,新模块用MyBatis,至少SQL可控。”
这句话我深有体会。之前有个同事用JPA自动生成SQL,结果一个findAll()愣是查了整张用户表,还JOIN了五个关联表,数据库CPU直接飙到90%。DBA在群里@他:“你这是要送我们去ICU?”
而MyBatis呢?它不替你写SQL,它只是帮你把SQL和Java对象“温柔地”粘在一起。你写什么,它就执行什么——像极了我这种只想躺平但又怕背锅的程序员。
MyBatis的核心哲学:SQL是你的,不是它的
很多人初学MyBatis,总以为它是个“全自动”ORM,结果发现还得自己写SQL,直呼“被骗了”。但恰恰相反,这正是MyBatis最佛系的地方:它不替你做决定,只提供工具。
比如,你想查一个用户:
<!-- UserMapper.xml -->
<select id="selectUserById" resultType="com.example.User">
SELECT id, name, email, created_at
FROM users
WHERE id = #{id}
</select>
就这么简单。没有魔法,没有隐式JOIN,没有“智能”缓存(除非你开)。你写的每一行SQL,都是你对数据库的直接对话。
小声BB:其实我特别喜欢这种“透明感”。至少出问题时,我能一眼看出是SQL慢,还是索引没建,而不是在JPA的EntityGraph里迷失自我。
快速上手:三步搞定一个查询
第一步:加依赖
Maven项目加个依赖就行(别用Gradle,我们组运维说“看不懂build.gradle”):
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
第二步:写配置
创建mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
第三步:写Mapper接口 + XML
// UserMapper.java
public interface UserMapper {
User selectUserById(Long id);
}
配合上面那段XML,启动时MyBatis会自动代理这个接口。不用写实现类,不用new对象,甚至连@Autowired都不用(当然Spring集成另说)。
和Spring Boot集成:这才是生产环境的样子
说实话,纯MyBatis在真实项目中很少单独用。我们公司所有新服务都基于Spring Boot,所以一般用mybatis-spring-boot-starter。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
然后在application.yml里配:
mybatis:
mapper-locations: classpath:mappers/*.xml
type-aliases-package: com.example.model
configuration:
map-underscore-to-camel-case: true
这样,user_name字段就能自动映射到Java的userName属性,省得写@Results。
踩坑实录:那些让我想砸电脑的瞬间
坑1:#{} 和 ${} 的区别
刚学时,我把#{id}写成${id},结果被SQL注入了。测试同学发了个id=1; DROP TABLE users--,我本地数据库直接没了。当时真的想辞职回老家种田。
记住:
#{}是预编译参数,安全${}是字符串替换,危险(除非你100%信任输入,比如动态表名)
坑2:ResultMap 写错字段名
有一次我把email写成e_mail,结果返回的User对象里email是null。查了两小时,最后发现是XML里拼错了。MyBatis默认不会报错,只会默默给你null。
后来我养成了习惯:所有字段显式写出来,哪怕和Java属性名一样。
<resultMap id="UserResultMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<result property="createdAt" column="created_at"/>
</resultMap>
坑3:事务没生效
在Service层加了@Transactional,但MyBatis的SqlSession没交给Spring管理,导致事务失效。线上更新一半数据,另一半没更新,差点被用户投诉。
解决方案:一定要用SqlSessionTemplate或MapperScannerConfigurer,让Spring接管事务。
Aider 和 Windsurf:我的开发加速器
最近我在用两个工具提升效率,顺便安利一下:
Aider:一个AI编程助手,支持直接修改代码库。我让它帮我生成MyBatis的Mapper XML模板,几秒钟搞定,比手写快多了。
aider --model gpt-4o mybatis-mapper.xmlWindsurf:一个国产的IDE插件,能可视化SQL执行计划。我把MyBatis生成的SQL粘进去,它直接告诉我“这里没走索引”,“这个JOIN可以优化”。再也不用求DBA看慢查询日志了。
这两个工具让我在“摸鱼”和“交付”之间找到了完美平衡——写代码更快,开会更少,下班更早。
性能与可维护性:佛系程序员的底线
虽然我主张“能跑就行”,但在持久层,有些底线不能破:
| 事项 | 建议做法 | 佛系但不摆烂 |
|---|---|---|
| SQL可读性 | 每个字段单独一行,加注释 | SELECT id, name → ❌;SELECT id, \n name → ✅ |
| 动态SQL | 用<if>、<choose>,避免拼接 |
拼String → ❌;<where><if> → ✅ |
| 批量操作 | 用<foreach>,控制批次大小 |
一次insert 10万条 → ❌;分批1000条 → ✅ |
| 缓存 | 谨慎使用二级缓存 | 默认开 → ❌;明确标注useCache=false → ✅ |
特别是动态SQL,MyBatis的<trim>、<set>简直是神器。比如更新用户信息:
<update id="updateUser">
UPDATE users
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
<set>会自动去掉末尾的逗号,再也不用写StringUtils.join()了。
结语:写SQL,也是一种修行
现在,那个“简单”的筛选接口已经上线一周,零报警,零慢查询。我甚至在代码里加了注释:“本接口由MyBatis驱动,SQL经过Windsurf优化,作者:一个不想加班的上海租房程序员”。
有时候我在想,为什么MyBatis能活这么多年?不是因为它多先进,而是它尊重开发者对SQL的掌控权。在这个“全自动”、“智能化”满天飞的时代,能让你亲手写SQL的框架,反而成了一种奢侈。
就像我每天下班后,戴上耳机,听着音乐,慢慢写一段清晰的SQL——不为KPI,不为晋升,只为明天还能准时打卡,回家吃一碗热腾腾的葱油拌面。
技术可以卷,但生活不必。
MyBatis,yyds。

评论 0