从抵触AI写代码到真香:MyBatis基础教程,一个Vim党在杭州的持久层入门血泪史
“以前看到同事用Copilot自动生成DAO层代码,我嗤之以鼻:‘这不就是把XML换成人话?’
直到上个月被一个紧急需求逼到凌晨三点,才明白——能少写一行重复代码,都是对发际线的尊重。”
大家好,我是老K,坐标杭州未来科技城,一个常年窝在Vim里敲:%s/old/new/g的Java后端。虽然公司(某厂字头大厂)早就在推AI辅助编码,但我一直觉得“真正的程序员就该手撸SQL”。直到去年双11压测时,因为手写JDBC连接池配置漏了maxWait参数,导致订单服务雪崩——那一刻,我站在工位前盯着满屏的Too many connections日志,突然悟了:框架不是偷懒,是避免重复踩坑。
最近在折腾Rust的同时,也重新审视了Java生态里那些“老古董”技术。MyBatis就是其中之一。别看它年纪大,但在杭州这边(尤其是阿里系、网易系),面试官问“你用过哪些ORM框架”时,如果你只说Hibernate,HR可能连简历都懒得筛。求职市场上,MyBatis几乎是Java岗的默认选项——毕竟,谁不想招个能直接优化SQL的人呢?
今天这篇,就带大家从零搞懂MyBatis的核心逻辑。不讲虚的,全是我在项目里实打实用过的东西,包括那些让我想砸键盘的坑。
起因:产品经理又改需求了
事情要从上周五说起。我们组在做一个内部运营后台,原本用Spring Data JPA跑得好好的。结果产品经理周五下午4:58在群里@我:“能不能加个模糊搜索+分页+按状态过滤的功能?明天上线!”
我当时内心OS:你当数据库是你家后花园啊?随便翻土播种?但转念一想,JPA动态查询写起来确实反人类——Specification嵌套三层,调试时断点都不知道打哪。更别说生成的SQL又臭又长,DBA看了直摇头。
于是拍板:切MyBatis。原因很简单:
- 动态SQL写起来像写原生SQL,产品经理改需求?改个
<if>标签就行 - 性能可控,不会出现N+1查询这种经典背锅场景
- 公司老系统全用MyBatis,新员工入职第一天就要看
Mapper.xml
MyBatis核心思想:SQL与代码解耦
很多人误以为MyBatis只是“把SQL塞进XML”,其实它的精髓在于 “你掌控SQL,框架处理映射”。对比一下传统JDBC和MyBatis的流程:
| 步骤 | 原生JDBC | MyBatis |
|---|---|---|
| 获取连接 | 手动管理DataSource | 框架自动注入SqlSession |
| 执行SQL | PreparedStatement硬编码 |
XML/注解定义SQL模板 |
| 结果映射 | 手动resultSet.getString("xxx") |
自动映射到POJO |
| 异常处理 | try-catch嵌套地狱 | 统一异常转换 |
举个最简单的例子。假设我们要查用户信息:
// User.java (POJO)
public class User {
private Long id;
private String name;
private String email;
// getter/setter省略
}
在MyBatis里,你只需要两步:
- 定义Mapper接口
- 写SQL映射文件
// UserMapper.java
public interface UserMapper {
User selectById(Long id);
}
<!-- UserMapper.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.mapper.UserMapper">
<select id="selectById" resultType="com.example.model.User">
SELECT id, name, email
FROM users
WHERE id = #{id}
</select>
</mapper>
注意那个#{id}——这就是MyBatis防SQL注入的关键。它会预编译成?占位符,比JDBC的String.format安全一万倍。曾经有个实习生直接拼接WHERE id = ${id},结果测试环境被注入删库,还好是测试库...
配置踩坑:别让环境变量背锅
MyBatis本身轻量,但和Spring Boot集成时,配置项多得让人眼花。我之前就栽在一个低级错误上:
# application.yml
mybatis:
mapper-locations: classpath*:mapper/*.xml # 注意是classpath*,不是classpath!
type-aliases-package: com.example.model
configuration:
map-underscore-to-camel-case: true # 数据库下划线字段自动转驼峰
重点说说classpath*这个星号。如果你的mapper文件放在多个jar包里(比如common模块),必须加*,否则启动时会报Invalid bound statement (not found)。这错误信息简直反人类——明明XML路径没错,却提示找不到方法。
还有一次,线上环境突然所有查询变慢。排查半天发现是运维把mybatis.configuration.cache-enabled设成了true,而我们的表更新频繁,缓存反而成了性能杀手。生产环境关缓存,除非你确定数据几乎不变。
动态SQL实战:产品经理的“小需求”
回到开头那个紧急需求。用MyBatis实现动态查询有多爽?
<select id="searchUsers" resultType="User">
SELECT id, name, email, status
FROM users
<where>
<if test="keyword != null and keyword != ''">
AND (name LIKE CONCAT('%', #{keyword}, '%')
OR email LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>
注意<where>标签的智能处理:它会自动去掉第一个AND,避免语法错误。要是手写JDBC,光这个就得写一堆StringBuilder判断。
分页部分我用的是LIMIT #{offset}, #{size},但要注意——MyBatis本身不提供分页插件!这里我故意没用PageHelper,因为:
- 大表深度分页性能差(
OFFSET 100000会扫描10万行) - 我们产品要求“无限滚动”,改用游标分页(
WHERE id > lastId)
说到产品,不得不吐槽:他们总以为“加个搜索框”是五分钟的事,殊不知要考虑索引设计、ES同步、敏感词过滤... 下次再有人跟我说“简单改个需求”,我就把DBA拉进群。
性能调优:别让N+1查询毁了你
MyBatis最大的陷阱是什么?关联查询的N+1问题。
假设我们查订单,同时要加载每个订单的用户信息:
// Order.java
public class Order {
private Long id;
private Long userId;
private User user; // 关联对象
}
如果这样写:
<resultMap id="OrderWithUser" type="Order">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
</resultMap>
<select id="selectOrdersWithUser" resultMap="OrderWithUser">
SELECT o.id, o.user_id, u.name as user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
</select>
这没问题,一次查询搞定。但如果偷懒写成:
// 错误示范!
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
order.setUser(userMapper.selectById(order.getUserId())); // 每次循环都查DB!
}
100个订单就产生101次SQL查询!线上CPU直接飙到90%。我在压测报告里看到这代码时,差点把键盘扔出窗外。
正确做法要么用JOIN,要么用MyBatis的@SelectProvider手写批量查询。记住:任何循环里调DAO的操作,都是定时炸弹。
为什么Go程序员也在看MyBatis?
可能有人问:现在不是流行Go吗?为啥还学Java的ORM?
两点真相:
- 杭州大厂后端主力仍是Java。阿里系P7以下基本要求Java,网易游戏后端也是Java为主。跳槽时MyBatis是硬通货。
- Go的ORM(如GORM)设计思路和MyBatis一脉相承。比如GORM的
Scopes动态查询,本质就是MyBatis的<if>标签。理解MyBatis,等于掌握ORM通用范式。
我最近研究Rust的Diesel ORM,发现它也在解决同样的问题:如何平衡SQL控制力与开发效率。语言会变,但持久层的核心矛盾永远存在。
总结:框架是工具,人才是核心
从抵触到真香,我对MyBatis的态度转变其实反映了程序员的成长:
- 新人追求“炫技”,觉得手写JDBC才够硬核
- 老手追求“稳定”,知道框架帮你避开了多少坑
MyBatis不是银弹,但它给了你可控的抽象。你可以:
- 在XML里写复杂SQL(DBA友好)
- 用注解快速开发简单CRUD
- 通过插件扩展(比如SQL打印、分页)
最后分享个心得:不要为了用框架而用框架。我们组有个原则——简单单表操作用Spring Data JPA,复杂查询上MyBatis。技术选型要看场景,不是站队。
对了,上周那个需求最终按时上线了。产品经理请我喝了杯瑞幸,说“下次还找你”。我笑着点头,心里默念:下次记得提前一周提需求,不然我就用Hibernate给你生成100个left join。
附:MyBatis vs Hibernate 关键对比(杭州互联网公司视角)
| 维度 | MyBatis | Hibernate |
|---|---|---|
| 学习曲线 | 低(会SQL就会用) | 高(需理解Session、缓存机制) |
| SQL控制力 | 完全掌控,可优化到极致 | 黑盒,复杂查询难优化 |
| 动态查询 | XML标签直观 | Criteria API反人类 |
| 求职热度 | 阿里/网易/拼多多标配 | 外企、传统企业较多 |
| 适合场景 | 互联网高并发、复杂查询 | 快速原型、简单CRUD |
本文代码已脱敏,但血泪教训真实。欢迎杭州同僚来茶水间battle Vim vs VS Code——不过先说好,我的
.vimrc可是配了LSP的,Copilot?拿来吧你!

评论 0