MyBatis基础教程:Java持久层框架入门
周五下午三点,阳光正好,咖啡刚续上第二杯。我戴着耳机,一边听着陈奕迅的《陀飞轮》,一边在 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 级别(需手动配置,慎用,容易脏读) |
在项目中,我们定了几条铁律:
- 禁止在 XML 里写
${},除非是动态表名(且必须白名单校验)。 - 所有查询必须指定字段,禁用
SELECT *。 - 复杂逻辑用 ResultMap,别硬塞进 resultType。
- 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 <= #{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