MyBatis基础教程:Java持久层框架入门 —— 一个斜杠程序员的血泪实战总结

邓磊·
2025-12-12 22:28
阅读 694

上周五晚上十点半,我一边戴着 AirPods 听周杰伦的《稻香》,一边盯着 IntelliJ IDEA 里那堆红得发紫的 SQL 报错信息,心里只有一个念头:产品经理能不能别在周五下午五点突然加需求?

没错,这就是我——一个坐标上海、租住在公司步行10分钟范围内的斜杠程序员。主业是后端开发,副业接外包(最近帮一家创业公司搞了个小商城),顺带在 B 站录点技术视频赚点奶茶钱。我的日常就是:白天改 Bug,晚上写代码,周末可能还在和 Go 语言较劲(别问,问就是想转云原生,结果发现 Java 还是香)。

今天这篇文章,其实源于一个真实的“事故”。上个月,我接了个外包项目,客户是个做智能健身镜的初创团队。他们原来用的是纯 JDBC 操作数据库,代码写得跟意大利面条一样,连个基本的分页查询都要复制粘贴三次。更离谱的是,前端同事每次改个字段,后端就得改五六个 DAO 方法,测试提了 20 个 bug,运维差点把我服务器权限给禁了。

最后被逼无奈,我决定把整个数据访问层重构成 MyBatis。结果这一折腾,不仅性能提升了 40%,连产品经理都跑来夸我“这次接口文档写得人话多了”。

所以,这篇不是教科书式的“Hello World”,而是一个踩过坑、熬过夜、被线上告警吵醒过的斜杠程序员,关于 MyBatis 的最佳实践总结。我会尽量避开那些官方文档里都能抄到的内容,重点讲怎么在真实产品中用好它


为什么不用 JPA 或 Hibernate?先说清楚立场

很多刚入行的朋友一听说 ORM,第一反应就是 Spring Data JPA。确实,JPA 对于 CRUD 场景很爽,但一旦涉及复杂查询、多表关联、动态条件,或者需要精细控制 SQL 性能时,JPA 就开始“抽象泄漏”了。

我们那个健身镜项目就吃过亏:早期用 JPA 写了个“用户最近7天运动记录聚合查询”,结果生成的 SQL 嵌套了四层子查询,数据库 CPU 直接飙到 90%。DBA 跑来质问我:“你这是要干掉我们的 MySQL 实例吗?”

而 MyBatis 的核心哲学很简单:SQL 是你写的,你负责。它不替你生成 SQL,而是帮你管理 SQL 和 Java 对象之间的映射。这种“半自动”的设计,反而在产品级系统中更有优势——尤其是当你需要:

  • 手动优化慢查询
  • 复用已有存储过程
  • 支持多数据源(比如读写分离)
  • 需要和前端联调接口字段(而不是被 ORM 绑死)

顺便吐槽一句:有些团队迷信“全自动 ORM”,结果上线后发现一条简单查询拖垮了整个服务。算法再牛,也扛不住烂 SQL 啊兄弟们!


MyBatis 入门:别被配置劝退

我知道很多人第一次看 MyBatis 配置文件就被吓跑了:mybatis-config.xmlMapper.xmlSqlSessionFactory……一堆 XML,感觉回到了 2005 年。

但其实,如果你用 Spring Boot,90% 的配置都可以自动化。我现在的项目基本这么搞:

# application.yml
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.product.domain
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

然后在启动类加上 @MapperScan("com.example.product.mapper"),搞定。别手写 SqlSessionFactoryBean,那是上个时代的事了。

真正要花心思的是 Mapper 接口和 XML 文件。举个例子,我们要查用户运动记录:

// UserRecordMapper.java
public interface UserRecordMapper {
    List<UserRecord> selectRecentRecords(@Param("userId") Long userId, @Param("days") int days);
}

对应的 XML:

<!-- UserRecordMapper.xml -->
<select id="selectRecentRecords" resultType="User'))
  SELECT id, user_id, duration, calories, created_at
  FROM user_records
  WHERE user_id = #{userId}
    AND created_at >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
  ORDER BY created_at DESC
</select>

注意几个细节:

  • 用了 map-underscore-to-camel-case,所以数据库字段 user_id 自动映射到 Java 的 userId
  • #{} 是预编译参数,防 SQL 注入;${} 是字符串替换,慎用!
  • 别在 XML 里写业务逻辑,那是前端该干的事(开玩笑的,但真有人这么干)

动态 SQL:MyBatis 最香的功能

产品需求永远在变。上周产品经理说“只要最近7天的”,这周他说“要支持自定义时间段,还要能按运动类型筛选”。

这时候,MyBatis 的 <if><choose><foreach> 就派上用场了:

<select id="selectRecordsByCondition" resultType="UserRecord">
  SELECT id, user_id, duration, calories, created_at, sport_type
  FROM user_records
  WHERE user_id = #{userId}
  <if test="startDate != null">
    AND created_at >= #{startDate}
  </if>
  <if test="endDate != null">
    AND created_at &lt;= #{endDate}
  </if>
  <if test="sportTypes != null and sportTypes.size() > 0">
    AND sport_type IN
    <foreach item="type" collection="sportTypes" open="(" separator="," close=")">
      #{type}
    </foreach>
  </if>
  ORDER BY created_at DESC
</select>

这段代码在线上跑了三个月,零故障。关键是:所有条件都是可选的,SQL 只拼接非空参数。

对比一下如果用 JPA 写,你可能得搞个 Specification 或者写一堆 if-else 构建 CriteriaQuery,代码又臭又长。而 MyBatis 让 SQL 保持可读性,这对后期维护太重要了。


性能与架构:别只顾着 CRUD

作为关注架构设计的程序员,我必须强调:MyBatis 不是万能的,用不好照样拖垮系统

1. N+1 查询问题

新手最容易犯的错。比如查用户列表,然后每个用户再去查他的记录:

List<User> users = userMapper.selectAll();
for (User u : users) {
    u.setRecords(recordMapper.selectByUserId(u.getId())); // 每次都查一次 DB!
}

100 个用户 = 101 次查询。线上直接炸。

解决方案

  • <collection> 做关联查询(但要注意笛卡尔积)
  • 或者分两步:先查用户 ID 列表,再批量查记录,最后内存合并
<resultMap id="UserWithRecords" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <collection property="records" ofType="UserRecord"
              select="selectRecordsByUserId" column="id"/>
</resultMap>

但注意:这种方式在大数据量下可能更慢,因为每条主记录都会触发一次子查询。建议在低频、小数据场景使用。

2. 二级缓存?谨慎开启!

MyBatis 有二级缓存,但默认是关闭的。我在外包项目里试过开,结果因为多个服务实例共享 Redis 缓存,导致数据不一致,被测试追着骂了一天。

生产环境建议

  • 单机应用:可用,但注意缓存粒度
  • 分布式系统:别用 MyBatis 自带缓存,自己用 Redis + 缓存穿透/击穿防护更可控

3. 分页别手写 OFFSET

很多人写分页直接:

SELECT * FROM table LIMIT #{offset}, #{size}

当 offset 很大时(比如第 10000 页),MySQL 要扫描前 10000 行,性能极差。

正确做法:用游标分页(cursor-based pagination),基于时间戳或自增 ID:

SELECT * FROM user_records
WHERE id > #{lastId}
ORDER BY id
LIMIT #{size}

前端传 lastId,后端返回下一页的起始 ID。这才是产品级分页该有的样子。


跨语言协作:别忘了 Go 和前端

虽然 MyBatis 是 Java 的,但在微服务架构里,你的服务可能要和 Go 写的服务通信,或者给前端提供 API。

给前端的建议

  • 字段命名统一用驼峰(靠 map-underscore-to-camel-case
  • 时间字段统一用 ISO8601 格式(2023-10-01T08:00:00Z
  • 敏感字段(如密码)别查出来,哪怕你“只是临时用一下”

和 Go 服务交互

我们有个推荐算法服务是用 Go 写的,它会调用我的 Java 服务获取用户行为数据。为了保证一致性:

  • 定义清晰的 DTO(Data Transfer Object)
  • 用 OpenAPI/Swagger 生成客户端
  • 数据库字段变更时,先改接口契约,再改实现

有一次我偷偷改了个字段名没通知 Go 团队,结果他们的推荐模型跑出一堆 NaN,算法工程师差点冲到我们工位来打人。


配置对比:MyBatis vs 其他方案

方案 优点 缺点 适用场景
MyBatis SQL 可控、性能透明、学习曲线平缓 需手写 SQL、XML 略繁琐 中大型产品、复杂查询、性能敏感系统
Spring Data JPA 开发快、CRUD 极简 复杂查询难优化、黑盒感强 快速原型、简单后台、MVP 产品
JDBC Template 零依赖、极致控制 重复代码多、无对象映射 超轻量级工具、遗留系统改造
Go GORM Go 生态友好、链式调用 类型安全弱、调试困难 Go 微服务内部

注:别问我为什么列 Go GORM,因为上周我刚用它写了 side project,结果被 nil pointer error 折磨到凌晨三点……


最后几句真心话

MyBatis 不是什么高深技术,但它代表了一种对 SQL 的尊重。在这个“全自动 ORM 才是现代化”的风气下,能手动写 SQL、敢对 DBA 说“这条语句我优化过”的后端,才是靠谱的产品守护者。

我现在接外包,第一件事就是看对方有没有用 MyBatis(或者至少没乱用 JPA)。因为这意味着代码大概率是可维护、可监控、可优化的。

当然,技术只是工具。真正决定产品成败的,是你愿不愿意为每一行 SQL 负责

好了,音乐停了,《稻香》放完了。我得去改产品经理刚提的新需求了——“能不能在用户运动时实时推送鼓励语?” …… 我现在就想对自己说一句:加油,打工人!


P.S. 如果你在用 MyBatis,记得打开日志(log-impl 那行),亲眼看看生成的 SQL。别信 ORM,信 EXPLAIN。

评论 0

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