MyBatis入门:从被ORM折磨到写出可读SQL的佛系之路

朱勇
2026-02-14 07:07
阅读 2446

上周五晚上十点,我正戴着耳机听Lo-fi beats,一边摸鱼刷GitHub,一边改一个“简单”的数据查询接口。产品经理说:“这个需求很简单,就加个状态筛选嘛。”结果一查,用的是公司老项目里那套手写JDBC模板——ConnectionPreparedStatementResultSet轮番上阵,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管理,导致事务失效。线上更新一半数据,另一半没更新,差点被用户投诉

解决方案:一定要用SqlSessionTemplateMapperScannerConfigurer,让Spring接管事务。


Aider 和 Windsurf:我的开发加速器

最近我在用两个工具提升效率,顺便安利一下:

  • Aider:一个AI编程助手,支持直接修改代码库。我让它帮我生成MyBatis的Mapper XML模板,几秒钟搞定,比手写快多了。

    aider --model gpt-4o mybatis-mapper.xml
    
  • Windsurf:一个国产的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

最热最新
暂无评论
朱勇Lv.1
0
影响力
0
文章
0
粉丝