MyBatis基础教程:Java持久层框架入门 —— 一个前技术总监的实战复盘

可爱鹿
2025-12-13 16:30
阅读 210

上周五晚上,我正瘫在沙发上刷 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

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