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

写码不秃头
2025-12-16 19:56
阅读 362

周五下午三点,阳光正好,咖啡刚续上第二杯。我戴着耳机,一边听着陈奕迅的《陀飞轮》,一边在 VS Code 里敲着 Rust 的 match 表达式——别误会,这可不是摸鱼,是我们团队最近搞的“新技术预研”任务。作为国企程序员,双休不加班是基本人权(虽然去年双11前被临时拉去支援电商系统,但那是特例!),所以现在这种节奏刚刚好。

但今天这篇博客不是讲 Rust 的(虽然真香),而是回炉重造——聊聊 MyBatis。为啥?因为上周部门新来了个实习生,写了个 Spring Boot + JPA 的 Demo,结果被老张(我们组的数据库老炮)一句“你这 N+1 查询问题线上炸了谁背锅?”直接问懵了。于是领导拍板:下周所有后端统一用 MyBatis,JPA 先放一放。

我心想:行吧,反正咱 Java 老兵,MyBatis 虽然有点年头,但稳如老狗。而且——说真的,面试题里十有八九会问它。


为啥又是 MyBatis?

先说背景。我们现在的项目是个内部审批系统,用户量不大(也就几万日活),但数据一致性要求极高。之前用的是 Spring Data JPA,代码是少,但遇到复杂查询就抓瞎。比如一个“待办事项列表”,要关联用户、部门、流程节点、历史记录……JPA 的 @Query 写到后面自己都看不懂,性能还差得一批。

有一次测试环境压测,一个接口响应时间飙到 3 秒+,查了半天发现是懒加载触发了一堆子查询。运维小哥在群里@我:“兄弟,数据库 CPU 都干到 90% 了,是不是你写的 SQL 在裸奔?”我当时真想砸键盘。

后来翻了下生产日志,发现很多地方其实只需要 SELECT 几个字段,但 JPA 默认把整个实体都拉出来了。典型的“我要一碗面,你给我一头牛”。

于是,我们决定切回 MyBatis。理由很简单:

  • SQL 自主可控:写什么查什么,绝不拖泥带水。
  • 性能可预测:没有 ORM 黑盒,执行计划心里有数。
  • 和 DBA 友好协作:他们看 SQL 直接就能优化,不用猜你在用什么 JPQL。

快速上手:从“Hello World”到项目集成

先别急着看源码,咱们直接上手。假设你有个 Spring Boot 项目(我们用的 2.7.x),加个依赖就行:

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

然后配个 application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/approval_db?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.approval.entity
  configuration:
    map-underscore-to-camel-case: true

注意那个 map-underscore-to-camel-case: true,这是 MyBatis 的神配置!数据库字段 user_name 自动映射到 Java 的 userName,省了你一堆 @Results 注解。

接下来,写个实体类:

public class TodoItem {
    private Long id;
    private String title;
    private String assignee;
    private LocalDateTime createTime;
    // getter/setter 省略
}

再写个 Mapper 接口:

@Mapper
public interface TodoItemMapper {
    List<TodoItem> selectAll();
    TodoItem selectById(Long id);
    int insert(TodoItem item);
}

最后,写 XML 映射文件 mapper/TodoItemMapper.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.approval.mapper.TodoItemMapper">

    <select id="selectAll" resultType="TodoItem">
        SELECT id, title, assignee, create_time FROM todo_item
    </select>

    <select id="selectById" resultType="TodoItem" parameterType="long">
        SELECT id, title, assignee, create_time FROM todo_item WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="TodoItem" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO todo_item(title, assignee, create_time)
        VALUES (#{title}, #{assignee}, #{createTime})
    </insert>

</mapper>

搞定!启动项目,调用 todoItemMapper.selectAll(),数据哗哗出来。这时候你可能会想:这也太简单了吧?没错,MyBatis 的哲学就是——让你专注 SQL,其他都是浮云


避坑指南:那些年踩过的雷

当然,理想很丰满,现实很骨感。我在集成过程中也踩了几个经典坑:

坑1:XML 文件没被扫描到

报错:Invalid bound statement (not found): com.example...

原因:Spring Boot 默认不会自动加载 mapper/*.xml,除非你显式配置 mybatis.mapper-locations。或者,你可以把 XML 放在和 Mapper 接口同目录下,再加个 Maven resource 配置:

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
    </resources>
</build>

坑2:事务失效

你以为加了 @Transactional 就万事大吉?错!MyBatis 的 Mapper 是通过 JDK 动态代理生成的,如果方法不是 public,或者在同一个类里 self-invocation(比如 A 方法调 B 方法,B 有 @Transactional),事务就无效。

解决办法:确保事务方法是 public,并且由 Spring 容器调用(别自己 new 对象)。

坑3:批量插入慢成狗

一开始我用循环调 insert,1000 条数据跑了 30 秒。后来改成 foreach 批量插入:

<insert id="batchInsert">
    INSERT INTO todo_item(title, assignee, create_time) VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.title}, #{item.assignee}, #{item.createTime})
    </foreach>
</insert>

配合 MySQL 的 rewriteBatchedStatements=true 参数,速度直接干到 1 秒内。DBA 看了直呼内行。


面试题高频考点 & 项目最佳实践

说到面试,MyBatis 的题真不少。结合我们项目的实战经验,总结几个重点:

问题 回答要点
MyBatis 和 Hibernate/JPA 的区别? MyBatis 是 SQL Mapping,JPA 是 ORM;前者灵活可控,后者开发快但黑盒多
#{} ${} 有什么区别? #{} 是预编译(防 SQL 注入),${} 是字符串替换(慎用!)
如何实现分页? 推荐 PageHelper 插件,或者手写 LIMIT(注意 offset 性能问题)
一级缓存和二级缓存? 一级缓存是 SqlSession 级别(默认开启),二级是 Mapper 级别(需手动配置,慎用,容易脏读)

在项目中,我们定了几条铁律:

  1. 禁止在 XML 里写 ${},除非是动态表名(且必须白名单校验)。
  2. 所有查询必须指定字段,禁用 SELECT *
  3. 复杂逻辑用 ResultMap,别硬塞进 resultType。
  4. SQL 写在 XML 里,别用注解(除非超简单查询),方便 DBA 审核。

举个例子,我们有个“多条件组合查询”需求,产品经理要求支持按状态、创建人、时间段筛选。用 MyBatis 的 <where><if> 标签,清爽又安全:

<select id="search" resultType="TodoItem">
    SELECT id, title, assignee, create_time
    FROM todo_item
    <where>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="assignee != null and assignee != ''">
            AND assignee = #{assignee}
        </if>
        <if test="startTime != null">
            AND create_time >= #{startTime}
        </if>
        <if test="endTime != null">
            AND create_time &lt;= #{endTime}
        </if>
    </where>
</select>

上线后,这个接口 QPS 稳定在 500+,P99 延迟 < 50ms。测试妹子跑来夸我:“这次终于没让我半夜爬起来提 bug 了!” —— 这就是 MyBatis 给我的安全感。


最后一点碎碎念

其实写这篇博客的时候,我还在想:现在都 2024 年了,还有必要学 MyBatis 吗?毕竟 Spring Data、JOOQ、甚至纯 JDBC + Records 都挺火。

但转念一想——技术没有新旧,只有合适不合适。在我们这种对 SQL 有强控制需求、DBA 参与度高的国企环境,MyBatis 就是那个“稳”字的代名词。而且你看,阿里的 MyBatis-Plus、腾讯的 TBase,底层不还是 MyBatis?

再说,跳槽面试官问起“你怎么理解 ORM”,你能说“我只会 JPA 自动生成 SQL”吗?显然不能。掌握 MyBatis,等于掌握了和数据库对话的主动权。

好了,音乐快播完了,Rust 的练习也该继续了。希望这篇带点烟火气的教程,能帮你少走点弯路。毕竟——程序员的时间,不该浪费在猜 ORM 到底干了啥上面

(完)

P.S. 如果你们公司也在用 MyBatis,欢迎留言交流避坑经验。要是你们产品经理也喜欢半夜改需求……咱们抱头痛哭一下 😅

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝