MyBatis基础教程:Java持久层框架入门
上周五晚上十点半,我坐在公司工位上,盯着屏幕上那个熟悉的 org.apache.ibatis.exceptions.PersistenceException 报错,差点把键盘砸了。这已经是本周第三次因为 MyBatis 配置问题导致接口 500 了。产品经理还在群里@我说“这个需求明天上线啊”,运维兄弟在 Slack 上发了个“服务器 CPU 又飙了”的表情包,而我,一个自诩“手写 SQL 才是真男人”的老派 Java 程序员,居然被一个 ORM 框架整得怀疑人生。
说来惭愧,我在成都一家中型互联网公司做后端开发,平时主要搞云原生和 K8s 这套,数据库交互基本靠自己拼 SQL,觉得这样才够“可控”。直到去年双11前,团队接了个大项目,领导拍板说:“咱们这次用 MyBatis,别再手写 JDBC 了,效率太低。” 我心里一万个不情愿——毕竟,手写代码是我最后的倔强。但为了不拖团队后腿(以及保住饭碗),我硬着头皮啃起了 MyBatis 文档。
结果你猜怎么着?用熟了之后,发现这玩意儿还真香!今天这篇MyBatis基础教程,就是想给和我一样“保守派”但又不得不拥抱新工具的 Java 开发者们,提供一份接地气的入门指南。别怕,咱不整那些花里胡哨的理论,直接上干货。
为啥非得用 MyBatis?
先说说我之前的做法:每个 DAO 类里都是一堆 PreparedStatement、ResultSet,连个分页都要自己算 offset 和 limit。有一次线上事故,就是因为手写的 SQL 拼接漏了个空格,导致 WHERE 条件失效,全表扫描直接把数据库干挂了。DBA 老哥找我喝茶时的眼神,我现在还记得。
MyBatis 的核心价值,不是“ORM 全自动”,而是“SQL 与 Java 解耦 + 半自动映射”。它让你依然能写原生 SQL(这点对我这种老古董太友好了),但不用再操心参数注入、结果集映射这些脏活累活。而且,它和 Spring Boot 集成起来几乎零成本——我们团队现在所有微服务都跑在 K8s 上,每个 Pod 启动时加载 MyBatis 配置也就几毫秒,性能完全没压力。
快速上手:三步搞定 CRUD
第一步:加依赖(别忘了版本号)
我们用的是 Spring Boot 2.7 + MyBatis 3.5.x,pom.xml 里加上:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
📌 吐槽一下:Maven 中央仓库有时候抽风,建议本地搭个 Nexus 缓存,不然 CI/CD 流水线卡在下载依赖上,运维又要念经了。
第二步:写配置文件(application.yml)
spring:
datasource:
url: jdbc:mysql://your-db-host:3306/myapp?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml # XML 映射文件位置
type-aliases-package: com.example.demo.model # 实体类别名
configuration:
map-underscore-to-camel-case: true # 数据库下划线字段自动转驼峰
这里重点说下 map-underscore-to-camel-case:我们数据库命名习惯是 user_id、created_at,而 Java 实体类是 userId、createdAt。开启这个选项后,MyBatis 会自动映射,省得你手动写 resultMap —— 刚开始我没开,结果每个字段都要配一次,差点放弃。
第三步:写 Mapper 接口 + XML
假设有个 User 表:
CREATE TABLE user (
id BIGINT PRIMARY KEY,
user_name VARCHAR(50),
email VARCHAR(100),
created_at DATETIME
);
对应的 Java 实体类(Lombok 大法好):
@Data
public class User {
private Long id;
private String userName;
private String email;
private LocalDateTime createdAt;
}
然后创建 Mapper 接口:
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int delete(Long id);
}
重点来了:XML 文件怎么写?
<!-- resources/mapper/UserMapper.xml -->
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT * FROM user ORDER BY created_at DESC
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(user_name, email, created_at)
VALUES(#{userName}, #{email}, #{createdAt})
</insert>
<update id="update">
UPDATE user
SET user_name = #{userName}, email = #{email}
WHERE id = #{id}
</update>
<delete id="delete">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
看到没?SQL 还是你自己写的,MyBatis 只负责传参和映射。像 #{userName} 这种写法,底层会自动用 PreparedStatement 防止 SQL 注入,比手拼字符串安全一万倍。
踩过的坑 & 生产经验
坑1:空值更新问题
有次我调 update 方法,只改了 email,结果 userName 被设成 null 了!原因很简单:XML 里的 UPDATE 语句是全字段更新。解决方案是用 <set> 标签动态拼接:
<update id="update">
UPDATE user
<set>
<if test="userName != null">user_name = #{userName},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
💡 生产建议:所有 UPDATE 操作务必用动态 SQL,否则前端传个空对象过来,你的数据就没了。
坑2:N+1 查询
曾经有个接口要查订单列表+用户信息,我一开始写了两个查询:先查订单,再循环查用户。结果 QPS 一高,数据库连接池直接爆了。后来改用 <resultMap> + <association> 一次性 JOIN 查询:
<resultMap id="OrderWithUser" type="Order">
<id property="id" column="order_id"/>
<result property="amount" column="amount"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
</association>
</resultMap>
<select id="selectOrdersWithUsers" resultMap="OrderWithUser">
SELECT o.id AS order_id, o.amount, u.id AS user_id, u.user_name
FROM orders o
LEFT JOIN user u ON o.user_id = u.id
</select>
性能从 200ms 降到 20ms,运维终于没再给我发“CPU 飙升”警告了。
坑3:事务管理
MyBatis 本身不管理事务,得靠 Spring。记得在 Service 层方法上加 @Transactional:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 扣款 + 加款
}
}
否则你以为的原子操作,其实根本不是原子的——别问我怎么知道的 😭
性能 & 架构考量
在 K8s 环境下,我们对 MyBatis 做了几个关键优化:
| 优化项 | 配置方式 | 效果 |
|---|---|---|
| 连接池 | 使用 HikariCP(Spring Boot 默认) | 减少连接创建开销 |
| 二级缓存 | <cache/> 标签(谨慎使用) |
高频读场景减少 DB 压力 |
| 分页插件 | 引入 PageHelper | 避免手写 LIMIT OFFSET |
特别提醒:二级缓存在分布式环境下慎用!我们曾经在多副本 Pod 中开启全局缓存,结果数据一致性乱成一锅粥。现在只对极少数只读配置表用本地缓存(@CacheNamespaceRef),其他一律走 DB。
另外,所有 SQL 都必须经过 慢查询日志监控。我们在 MySQL 开启了 slow_query_log,配合 Prometheus + Grafana 做告警。一旦某个 MyBatis 查询超过 100ms,就会触发企业微信通知——这招治好了团队里“随便写 SQL”的坏习惯。
写在最后:保守派也能拥抱变化
说实话,现在我还是会在复杂报表查询时手写原生 SQL,但日常 CRUD 已经完全交给 MyBatis 了。它没让我失去对 SQL 的控制权,反而帮我避开了无数低级错误。上周我们新上线的服务,QPS 稳稳 1500+,数据库 CPU 使用率不到 30%,连运维都说“这次部署很丝滑”。
如果你也和我一样,是个“手写代码至上”的老顽固,不妨给 MyBatis 一个机会。它不是来取代你的,而是来帮你把精力集中在业务逻辑上,而不是重复造轮子。
毕竟,在成都这种节奏舒服的城市,谁不想早点下班去吃火锅呢?代码写得优雅一点,Bug 少一点,生活才能更巴适。
附:学习资源推荐
- 官方文档:https://mybatis.org/mybatis-3/zh/index.html (中文版很全)
- GitHub 示例:搜索 “mybatis-spring-boot-sample”
- 本地调试技巧:开启
logging.level.com.example.mapper=debug,能看到实际执行的 SQL
共勉。下次技术分享会上,我打算讲讲 “MyBatis + K8s 下的弹性伸缩实践”,欢迎来成都找我喝茶(或者火锅)聊技术!

评论 0