MyBatis 基础教程:Java 持久层框架入门

青山不改需求改
2025-12-16 23:36
阅读 1866

上周五晚上十点半,我正对着公司那套老掉牙的 JPA 代码发呆——不是因为浪漫,是因为又双叒叕一个线上慢 SQL 把数据库 CPU 干到 90%。我们组去年双11期间就因为这个吃过亏,运维兄弟半夜打电话过来语气都快哭了:“哥,你们能不能别用 findAll() 了?”

我是小林,一个刚晋升技术组长、在当前团队混了快两年的 Java 老油条。平时热衷参加各种云原生和 K8s 的线下分享会(主要是为了蹭饭),但最近被领导“委以重任”:重构核心订单模块的持久层。理由很现实——“你简历上不是写了‘精通 ORM 框架’吗?那正好上”。

得,这锅我背了。

为什么选 MyBatis?

其实一开始我想直接上 Go 写个微服务把这块逻辑拆出去(别笑,我们组真有人这么干过)。但现实是:系统耦合太深,测试覆盖率低得可怜,连单元测试都没几行,产品经理还天天催着加新功能。Go 是好,但在这堆祖传 Java 代码里硬塞 Go 服务,怕不是要触发“多语言地狱”成就。

于是我和队友们一合计:不如把 JPA 换成 MyBatis。原因很简单:

  • SQL 可控:再也不用猜 Hibernate 到底生成了啥语句
  • 学习成本低:XML + 注解,写起来像手写 SQL,新人上手快
  • 性能调优方便:慢查询直接看 XML 文件,不用翻半天源码

而且,说真的,现在面试问“MyBatis 原理”的频率可不比问“红黑树”低。我上次帮 HR 筛简历,看到一堆人写“熟悉 MyBatis”,结果连一级缓存和二级缓存的区别都说不清……(此处省略对算法岗同学的羡慕)

实战踩坑:从零配置 MyBatis

我们用的是 Spring Boot 2.7 + MyBatis 3.5。第一步当然是加依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>

然后在 application.yml 里配数据源(这里用了 Druid,顺便开了监控):

spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: ********
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true

重点来了:map-underscore-to-camel-case 必须开!不然数据库字段 create_time 映射不到 Java 对象的 createTime,你就会像我一样,在凌晨两点对着 null 值怀疑人生。

Mapper 接口 + XML,YYDS

建个实体类:

public class Order {
    private Long id;
    private String orderNo;
    private LocalDateTime createTime;
    // getter/setter...
}

再写个 Mapper:

@Mapper
public interface OrderMapper {
    List<Order> selectByUserId(@Param("userId") Long userId);
    
    @Select("SELECT * FROM orders WHERE id = #{id}")
    Order selectById(Long id);
}

对应的 XML(mapper/OrderMapper.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.OrderMapper">

    <select id="selectByUserId" resultType="com.example.model.Order">
        SELECT id, order_no, create_time
        FROM orders
        WHERE user_id = #{userId}
          AND status != 'DELETED'
        ORDER BY create_time DESC
        LIMIT 100
    </select>

</mapper>

注意:XML 里我显式写了字段名,而不是 SELECT *。这是血的教训——有一次 DBA 在表里加了个 blob 字段存日志,结果整个接口响应时间从 50ms 飙到 800ms。从此以后,我们组 Code Review 第一条就是:“禁止 SELECT *”。

性能调优:缓存与分页

MyBatis 有一级缓存(SqlSession 级别)和二级缓存(Mapper 级别)。默认一级缓存开启,但 Spring Boot 里每次请求都会新建 SqlSession,所以基本无效。我们开了二级缓存:

<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

不过要小心:二级缓存只适用于读多写少的场景。我们订单表写操作频繁,最后还是关了,改用 Redis 做业务缓存。

至于分页,千万别自己写 LIMIT #{offset}, #{size}!我们早期这么干,结果遇到深度分页(比如第 10000 页)直接拖垮 DB。后来统一接入了 MyBatis-PageHelper

PageHelper.startPage(1, 10);
List<Order> orders = orderMapper.selectByUserId(userId);

它会自动重写 SQL 成 SELECT ... LIMIT 10 OFFSET 0,并且支持多种数据库方言。亲测有效,线上慢查询下降 70%。

生产环境避坑指南

  1. N+1 查询问题:别在循环里调 Mapper 方法!用 <collection> 或者提前 JOIN。
  2. 事务管理:MyBatis 本身不管理事务,靠 Spring 的 @Transactional。但要注意:同一个类内方法调用不会触发事务(代理失效),我因此导致过一次资损,差点被祭天。
  3. SQL 注入?:MyBatis 的 #{} 是预编译,安全;但 ${} 是字符串拼接,危险!我们用 SonarQube 扫描,发现一个 ${tableName} 动态表名,吓得立刻改成白名单校验。

下面是个对比表格,总结 MyBatis 和 JPA 在我们项目中的表现:

维度 JPA (Hibernate) MyBatis
SQL 可控性 低(自动生成,难优化) 高(手写,精准调优)
学习曲线 陡(注解/继承/懒加载) 平缓(接近原生 SQL)
性能(复杂查询) 差(N+1、笛卡尔积) 优(可控 JOIN)
调试难度 高(需看生成 SQL) 低(直接看 XML)
团队接受度 中(新人容易写错) 高(DBA 也看得懂)

最后一点感悟

说实话,MyBatis 不是什么新技术,但它胜在“透明”和“可控”。在这个大家都在卷云原生、Service Mesh、eBPF 的时代,有时候回归基础反而更高效。

上周上线后,订单查询 P99 从 320ms 降到 45ms,运维兄弟终于没再半夜打电话。产品经理也难得夸了句:“这次接口真快!”——虽然下一秒就说“那能不能再加个导出 Excel 功能?”

唉,程序员的命运啊。

不过话说回来,如果你正在准备跳槽,MyBatis 这块真得吃透。我面过几个候选人,问“MyBatis 插件怎么实现的?”,90% 回答不上来。其实就四个字:动态代理。底层用 Interceptor 拦截 ExecutorStatementHandler 这些组件,和 AOP 思想一模一样。

对了,说到算法——虽然 MyBatis 本身不涉及复杂算法,但理解其缓存淘汰策略(LRU)、分页优化(游标 vs offset)背后的思想,其实和 LeetCode 刷题是相通的。别光刷题不实战,也别光写 CRUD 不思考。

好了,今天就水到这里。下期打算聊聊“如何用 K8s + ArgoCD 实现 MyBatis 配置热更新”(老板说要上云原生,我总得找点活干)。

PS:如果你也在被 ORM 框架折磨,评论区聊聊你的血泪史?或者直接甩简历给我(开玩笑的,HR 说今年 HC 冻结了 😭)

评论 0

最热最新
暂无评论
青山不改需求改Lv.1
0
影响力
0
文章
0
粉丝