MyBatis基础教程:Java持久层框架入门 —— 一个前技术总监的实战复盘
上周五晚上,我正瘫在沙发上刷 Hacker News,突然收到前同事的消息:“老哥,你之前写的那个 MyBatis 封装层,还能开源不?” 我愣了一下——这都离职半年了,居然还有人惦记着那套代码?但转念一想,也难怪。毕竟当年双11大促前夜,我们团队就是靠它扛住了每秒三万的订单写入,没让数据库崩掉。
如今我已经在家远程创业,白天撸 Go 写后端服务,晚上用 JavaScript 调动画效果(是的,前端交互依然是我的白月光),但 Java 和 MyBatis 这对“老搭档”,始终是我职业生涯里绕不开的一段执念。今天就借这个机会,把 MyBatis 的入门要点、踩过的坑、以及我在大厂带团队时总结的最佳实践,一次性吐个干净。
声明一下:我不是 AI,也不是什么布道师。我只是个刚从“996修罗场”逃出来的前技术总监,现在每天和猫抢键盘、跟投资人扯需求。这篇文章,纯粹是出于“别让后来人再踩我踩过的屎”。
为啥不用 JPA 或 Hibernate?偏偏选 MyBatis?
先说个真实故事。三年前,我还在某电商公司当后端 Leader,产品狗(哦不,产品经理)突然提了个需求:“我们要做动态表单,字段数量和类型都不固定。” 当时团队用的是 Spring Data JPA,结果为了支持这个需求,硬生生把实体类搞成了 Map<String, Object> 的地狱。上线后三天,数据库慢查询直接干爆监控大盘。
那一刻我就意识到:全自动 ORM 虽香,但在复杂业务面前,往往不如半自动的灵活可控。
MyBatis 的核心哲学就一句话:SQL 是你的亲儿子,不是框架的附属品。它不会替你生成 SQL,而是让你自己写,然后帮你参数绑定、结果映射。这种“手动挡”的模式,在高并发、复杂查询、分库分表等场景下,反而更稳、更可优化。
而且,对比 Go 生态里常见的 GORM,或者 JavaScript 世界的 TypeORM,MyBatis 的学习曲线其实很平缓——只要你懂 SQL,就能上手。
MyBatis 入门三板斧:配置、Mapper、实体
第一步:Maven 引入 + 配置文件
<!-- pom.xml -->
<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"
"https://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/test?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>
⚠️ 注意:生产环境千万别把密码写死!我们当年就因为这个被安全团队约谈过三次。后来改用 Vault + 动态 Token,运维小哥差点给我送锦旗。
第二步:写 Mapper XML
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<select id="findById" resultType="com.example.model.User">
SELECT id, name, email FROM users WHERE id = #{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>
</mapper>
这里有个最佳实践:useGeneratedKeys="true" + keyProperty="id" 能自动回填自增主键,避免再查一次。很多新人不知道这点,硬生生多了一次数据库 round-trip。
第三步:Java 调用
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findById(1L);
System.out.println(user.getName());
}
看起来很简单?但线上事故往往就藏在这种“简单”里。
那些年,我和 MyBatis 踩过的坑
坑 1:N+1 查询问题(比产品经理的需求还烦人)
假设你有一个 Order 表,关联 User。如果在 XML 里这么写:
<select id="listOrders" resultType="Order">
SELECT * FROM orders
</select>
然后在 Java 里遍历订单,每个都去查用户:
for (Order order : orders) {
User user = userMapper.findById(order.getUserId()); // N+1!
}
结果就是:100 个订单 → 101 次 SQL 查询。数据库 CPU 直接飙到 90%,运维冲进办公室拍桌子:“谁又在压测生产库?”
解决方案:用 <resultMap> 做关联查询,或者干脆在 Service 层批量查用户 ID 再 Map 批量填充。后者更推荐,因为 SQL 更清晰,也方便分页。
坑 2:缓存失效与脏读
MyBatis 有两级缓存:一级(SqlSession 级)和二级(Mapper 级)。很多人以为开了二级缓存就万事大吉,结果在集群环境下,A 节点更新了数据,B 节点还在读旧缓存——用户看到“订单已支付”,实际上钱没到账。
血泪教训:除非是只读配置表,否则慎用二级缓存。我们后来全关了,用 Redis 自己控制缓存策略,反而更稳。
坑 3:动态 SQL 写成“意大利面条”
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="email != null">AND email = #{email}</if>
</where>
看着还行?但如果条件有 20 个,XML 文件能写到 200 行,连 IDE 折叠都救不了。而且 CONCAT 在 MySQL 里性能一般,不如用 LIKE + 参数预处理。
建议:复杂动态查询,考虑用 MyBatis-Plus 或 QueryDSL,或者干脆在 Java 里拼 WHERE 条件(配合 StringBuilder),至少能单元测试。
性能调优:从本地开发到生产部署
| 优化项 | 开发环境 | 生产环境 |
|---|---|---|
| 连接池 | HikariCP 默认 | 最大连接数 ≤ DB 实例 max_connections * 0.7 |
| SQL 日志 | 打印完整 SQL | 关闭,用 APM 工具(如 SkyWalking) |
| 事务管理 | 手动 commit | Spring @Transactional + 合理传播行为 |
| 批量操作 | 单条 insert | ExecutorType.BATCH + 分批提交 |
特别提醒:别在循环里开 SqlSession!我见过实习生这么写,结果连接池被耗尽,整个服务雪崩。正确的做法是:一个业务方法一个 Session,批量操作用 session.flushStatements() 控制刷盘节奏。
和 Go / JavaScript 的对比视角
作为一个现在同时写 Go 和 JS 的人,我经常思考不同语言生态的 ORM 设计哲学:
- Go (GORM):倾向于“约定大于配置”,但复杂查询仍需手写 SQL。MyBatis 更像它的“显式版”。
- JavaScript (TypeORM/Prisma):追求类型安全和链式调用,但运行时性能损耗较大。MyBatis 则更贴近数据库,几乎没有抽象税。
有趣的是,无论哪种语言,最终大家都会回归到“可控的 SQL”这一原点。这也解释了为什么 MyBatis 在国内 Java 圈经久不衰——我们太需要对数据库的绝对掌控权了。
最后几句掏心窝子的话
MyBatis 不是银弹,但它是一个尊重开发者智力的框架。它不替你做决定,而是给你工具,让你写出高性能、可维护的 SQL。
如果你正在学 MyBatis,别只看官方文档。去 GitHub 找几个 star 高的开源项目(比如 Jeecg-Boot、RuoYi),看看人家怎么组织 Mapper、怎么处理分页、怎么集成 Spring Boot。代码是最好的老师。
至于我?现在创业项目里,后端用 Go 写微服务,前端用 Vue + GSAP 做交互动画。但每当看到有人还在为 ORM 框架争论不休,我总会想起那个双11凌晨三点的办公室——满地泡面盒,屏幕蓝光闪烁,而 MyBatis 的日志里,一条条 INSERT 正安静地写入数据库,稳如老狗。
也许,这就是技术人的浪漫吧。
P.S. 如果你也在创业或准备跳槽,欢迎来 GitHub 找我聊。顺便,我家猫最近学会了按键盘,说不定哪天就给我 PR 一段 JavaScript 动画代码 😅

评论 0