MyBatis基础教程:Java持久层框架入门
上周五晚上十点半,我正窝在沙发里用 VSCode 撸 Rust,突然钉钉“叮”一声——产品经理发来消息:“亲,在吗?下周三前要上线一个新模块,得支持用户积分明细查询,接口 QPS 要撑住 2000。”
我盯着那条消息,手里的咖啡都凉了。又来? 上周不是刚搞完双11大促的库存扣减优化?算了,打工人命苦,只能认。
但说实话,这次需求其实挺简单:查数据库、分页返回、加点条件过滤。问题在于,我们系统是典型的 Java 后端架构,用的 Spring Boot + MyBatis。而我……最近沉迷 Rust,对 Java 的手感有点生疏了。再加上团队里新来了几个应届生,天天问我“MyBatis 是啥?为啥不用 JPA 或者直接写 JDBC?”——行吧,那就借这个机会,好好捋一捋 MyBatis 这个“老古董”,顺便写篇教程,也算给新人铺个路,也给自己复习一把。
为什么是 MyBatis?而不是 Hibernate、JPA,或者直接上 Python?
先别急着喷。我知道现在很多人吹 Python 多香,Django ORM 几行代码搞定 CRUD,连 SQL 都不用写。但现实是:我们公司核心交易系统是 Java 写的,DB 是 MySQL 5.7,线上跑了三年多,每天处理几千万笔订单。 这种场景下,换语言?等于自杀。
至于 JPA/Hibernate?确实优雅,对象关系映射(ORM)全自动,但“全自动”往往意味着“不可控”。比如一个 @OneToMany 注解,不小心就触发 N+1 查询,线上慢查询日志爆了你都不知道咋回事。而 MyBatis 呢?它不替你做决定,SQL 你写,结果怎么映射你也控制——透明、可控、性能可预测。对我们这种高并发、低延迟要求的系统来说,这太重要了。
再说回求职。最近帮 HR 筛简历,发现不少应届生只会在 Spring Boot 项目里用 JPA 快速生成 CRUD 接口,问底层怎么优化、如何避免 SQL 注入、分页怎么实现,一脸懵。反倒是那些能手写 MyBatis Mapper、知道 #{} 和 ${} 区别的候选人,面试通过率高得多。企业要的是能扛生产环境的人,不是只会搭脚手架的“玩具工程师”。
实战:从零搭建一个 MyBatis 项目(基于 Spring Boot)
咱们不玩虚的,直接上手。假设你要做一个用户积分流水查询接口,表结构大概是这样:
CREATE TABLE `user_points_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`points` INT NOT NULL,
`reason` VARCHAR(100) NOT NULL COMMENT '变动原因',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`)
);
第一步:依赖引入(Maven)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version> <!-- 注意版本兼容性! -->
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
💡 踩坑提示:MyBatis-Spring-Boot-Starter 的版本和 Spring Boot 版本强相关!我们之前有个同事用 Spring Boot 3.x 配了 2.x 的 starter,启动直接报
ClassNotFoundException,折腾到凌晨两点。血泪教训!
第二步:配置数据源(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml # XML 文件位置
type-aliases-package: com.yourcompany.model # 实体类别名包
configuration:
map-underscore-to-camel-case: true # 数据库下划线自动转 Java 驼峰
这里重点说一下 map-underscore-to-camel-case。我们 DB 字段习惯用 snake_case(比如 user_id),而 Java 对象用 camelCase(userId)。开了这个配置,MyBatis 会自动映射,省得你每个字段写 @Results。
第三步:写实体类和 Mapper 接口
// UserPointsLog.java
public class UserPointsLog {
private Long id;
private Long userId;
private Integer points;
private String reason;
private LocalDateTime createdAt;
// getter/setter 略
}
// UserPointsLogMapper.java
@Mapper
public interface UserPointsLogMapper {
List<UserPointsLog> selectByUserId(@Param("userId") Long userId,
@Param("offset") int offset,
@Param("limit") int limit);
int countByUserId(@Param("userId") Long userId);
}
注意两点:
- 用
@Mapper注解标记接口,Spring Boot 自动扫描。 - 参数用
@Param显式命名,XML 里才能用#{userId}引用。否则多个参数会报错!
第四步:写 XML 映射文件(关键!)
<!-- resources/mapper/UserPointsLogMapper.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.yourcompany.mapper.UserPointsLogMapper">
<select id="selectByUserId" resultType="UserPointsLog">
SELECT id, user_id, points, reason, created_at
FROM user_points_log
WHERE user_id = #{userId}
ORDER BY created_at DESC
LIMIT #{offset}, #{limit}
</select>
<select id="countByUserId" resultType="int">
SELECT COUNT(1)
FROM user_points_log
WHERE user_id = #{userId}
</select>
</mapper>
看到没?SQL 就是原生 SQL,清晰、高效、无黑盒。而且你可以随时在 SQL 里加索引提示、强制走某个索引,甚至写复杂 JOIN——这才是生产环境需要的自由度。
避坑指南:那些年我踩过的 MyBatis 坑
1. ${} vs #{} —— SQL 注入的生死线
新手最爱犯的错:为了“动态表名”或“动态排序字段”,直接用 ${} 拼接:
<!-- 千万别这么干! -->
ORDER BY ${sortField} ${sortOrder}
如果 sortField 来自前端传参,黑客传个 1; DROP TABLE user_points_log--,你的表就没了。#{} 会预编译,${} 是纯字符串替换! 安全做法:后端白名单校验字段名,或者用枚举限制。
2. 分页别自己手写 OFFSET/LIMIT
上面例子用了 LIMIT #{offset}, #{limit},但在大数据量下(比如 offset=100000),MySQL 会扫描前 10 万行再丢弃,性能极差。我们线上就因此在双11被报警轰炸过。
正确姿势:用 MyBatis-Plus 或 PageHelper 插件,它们底层用“记录上次最大 ID”的方式实现分页(游标分页),效率 O(1)。
3. 事务管理别忘加 @Transactional
有一次我写了个积分扣除逻辑,先查余额,再插入流水,最后更新用户总积分。结果没加事务,某次网络抖动导致只插入了流水没更新余额——用户凭空多了积分!测试小姐姐提 bug 时我脸都绿了。
@Service
public class PointsService {
@Transactional(rollbackFor = Exception.class)
public void deductPoints(Long userId, int points, String reason) {
// 1. 查当前积分
// 2. 插入流水
// 3. 更新总积分
}
}
记住:涉及多表写操作,必须加事务!
性能调优:MyBatis 在生产环境的实战经验
| 优化项 | 默认行为 | 生产建议 |
|---|---|---|
| 一级缓存 | 开启(SqlSession 级) | 一般不用管,但注意循环内不要复用 SqlSession |
| 二级缓存 | 关闭 | 谨慎开启!分布式环境下容易脏读,建议用 Redis 替代 |
| 批量插入 | 单条 INSERT | 用 foreach + ExecutorType.BATCH,速度提升 10 倍+ |
| 日志输出 | 关闭 | 开发开 log4j.logger.com.yourcompany.mapper=DEBUG,生产务必关 |
特别说下批量插入。比如我们要初始化 10 万条用户数据:
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchInsert(List<UserPointsLog> logs) {
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserPointsLogMapper mapper = session.getMapper(UserPointsLogMapper.class);
for (UserPointsLog log : logs) {
mapper.insert(log);
}
session.commit(); // 一次性提交
}
}
配合 XML 里的 <insert>,效率吊打循环单条插入。
最后聊聊:MyBatis 还值得学吗?
前几天和组里一个想跳槽的同事聊天,他说:“现在都流行 Go/Rust 写微服务了,Java 是不是快凉了?” 我笑了笑,给他看了我们监控大盘——QPS 峰值 12 万,P99 延迟 45ms,GC 暂停平均 3ms。这套系统跑在 Java + MyBatis + MySQL 上,稳如老狗。
技术没有银弹,只有合适不合适。如果你目标是进大厂做后端,Java 生态依然是主流。而 MyBatis 作为国内使用最广的持久层框架(阿里系、腾讯系、字节都在用),掌握它不仅能让你写出高性能、可维护的代码,更是面试时的一块硬通货。
至于我?写完这篇教程,Rust 的学习计划又得推迟了。不过没关系,反正产品经理明天又要改需求了(笑)。
附:VSCode 插件推荐(MyBatis 开发必备)
MyBatisX:XML 和 Mapper 接口互相跳转Rainbow Brackets:括号彩虹色,看嵌套 SQL 不眼花Error Lens:实时显示编译错误,比 IDEA 还快啊对了,如果你也在远程办公,记得每小时起来活动下——我上周坐太久,腰疼到差点叫救护车。程序员,保命要紧啊!

评论 0