MyBatis基础教程:从零开始掌握Java持久层框架
引言:我为什么决定写这篇文章

作为一名在一线互联网公司做后端开发的 Java 程序员,我在日常工作中经常需要和数据库打交道。从最开始用 JDBC 手写 SQL 到后来接触 Hibernate,再到现在几乎所有的项目都使用 MyBatis,这个过程其实也踩了不少坑。
去年我们团队负责一个用户中心重构项目,其中涉及到大量的数据库操作,包括用户信息、权限管理、操作日志等模块。当时为了提升开发效率和代码可维护性,决定采用 MyBatis 作为 ORM 框架。但在实际落地过程中,遇到了不少问题,比如:
- 明明写了 SQL,却总是查不到数据
- 多表关联时结果集映射出错
- 动态 SQL 写法不够优雅,后期维护困难
- 性能优化无从下手,SQL 执行慢不知道哪儿出了问题
这些问题让我意识到,虽然大家都会说“我会用 MyBatis”,但要真正用好它,尤其是理解它的底层机制、写出高可用、高性能的代码,还需要下一番功夫。
今天就结合我的实战经验,来分享一下 MyBatis 的入门实践,以及我在真实项目中踩过的那些坑和对应的解决方案。
项目背景:用户中心重构中的挑战

我们当时的项目目标是将原有单体架构中的用户系统拆分成一个独立服务,并提供 RESTful 接口供其他业务线调用。老系统的数据库结构复杂,存在大量冗余字段,新项目采用 MySQL 分库设计,同时希望提升查询效率和接口响应速度。
面对这样的需求,我们选择了 MyBatis,因为它灵活性强、控制粒度细、适合做精细化 SQL 调优,比 Hibernate 更贴近原生 SQL 的风格,在高并发场景下更容易排查性能瓶颈。
然而刚上手没多久,团队里的小伙伴就开始频繁提问:
- XML 中 resultType 和 resultMap 有什么区别?
- SQL 参数传递怎么处理更安全?
- 一对一、一对多的结果集映射应该怎么写?
- 动态 SQL 有没有统一规范?
这些问题促使我整理了一套内部文档,这篇内容也是基于那次沉淀出来的经验和教训。
MyBatis 的工作流程与核心概念
先简单说说 MyBatis 是个什么东西。官方说法是:
MyBatis 是支持定制化 SQL、存储过程以及高级映射的持久层框架。不同于全 ORM 框架如 Hibernate,MyBatis 更倾向于半自动化,允许开发者直接编写 SQL,从而获得更高的控制能力。
那它到底是怎么工作的呢?以一次典型的查询为例:
- 你写了一个 Mapper 接口(比如 UserMapper)
- 在 XML 文件里定义了对应的 SQL(或使用注解)
- Spring Boot 启动时会通过扫描自动注册这些 Mapper
- 当调用
userMapper.selectById(1)方法时,MyBatis 会:- 定位到对应 SQL
- 处理参数注入
- 执行 SQL 查询
- 将结果集映射成 Java 对象
- 返回给调用方
所以整个 MyBatis 的核心可以归纳为三个关键词:
- SQL 编写:你可以完全掌控 SQL,而不是被框架生成
- 参数绑定:占位符处理、参数类型转换
- 结果映射:把 ResultSet 变成对象
下面我们就通过具体的例子来一步步说明。
实战演练:MyBatis 基础使用示例
项目结构概览
我们的服务基于 Spring Boot + MyBatis 构建,大致目录如下:
src/
├── main/
│ ├── java
│ │ └── com.example.usercenter.mapper
│ │ └── UserMapper.java
│ │ └── com.example.usercenter.model
│ │ └── User.java
│ │ └── ...
│ ├── resources
│ └── mapper
│ └── UserMapper.xml
Spring Boot 配置文件中开启了 MyBatis 的自动扫描:
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.usercenter.model
这样在启动时,Spring Boot 会自动加载所有 XML 映射文件,并与对应的 Mapper 接口绑定。
最简单的 CRUD 示例
Mapper 接口定义
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
}
XML 中定义 SQL
<!-- src/resources/mapper/UserMapper.xml -->
<mapper namespace="com.example.usercenter.mapper.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectAll" resultType="User">
SELECT * FROM user
</select>
<insert id="insert">
INSERT INTO user (username, email, created_at)
VALUES (#{username}, #{email}, #{createdAt})
</insert>
<update id="update">
UPDATE user
SET username = #{username}, email = #{email}
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
这里的几个关键点:
<select>的resultType指定返回类型,注意必须和@Data注解一起使用(Lombok)#{xxx}是预编译占位符,避免 SQL 注入- 插入语句不需要指定 resultType,因为不会返回实体对象
看起来很简单对吧?但实际上在生产环境下,远不止这么点内容。接下来咱们看看真正的“坑”在哪。
踩坑记录一:结果集映射问题
有一次我们在做一个复杂的查询——需要联合 user 表和 user_role 表,获取某个用户的所有角色信息。
SQL 写好了,也能在客户端跑出来,但 Java 返回的对象一直为空。
最终发现问题出在 resultMap 配置上。
我们最初的写法是:
<select id="selectUserWithRoles" resultType="User">
SELECT u.id, u.username, r.role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
WHERE u.id = #{id}
</select>
但 role_name 这个字段并不能自动映射到 User 类里面去,因为 User 对象中并没有这个字段。
这时候就需要自定义 resultMap:
<resultMap id="UserWithRolesResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<collection property="roles" ofType="Role">
<id column="role_id" property="id"/>
<result column="role_name" property="name"/>
</collection>
</resultMap>
<select id="selectUserWithRoles" resultMap="UserWithRolesResultMap">
SELECT u.id, u.username, r.id as role_id, r.role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
WHERE u.id = #{id}
</select>
这里需要注意几个点:
<collection>表示这是一个集合类型的属性,适合一对多关系- 字段别名尽量清晰,特别是涉及多表联查的时候
- 如果使用 Lombok,记得加上
@Accessors(chain = true)或者写 setter 方法
踩坑记录二:动态 SQL 的正确姿势
动态 SQL 是 MyBatis 的一大优势,但我们曾经因为写法不规范导致代码难以维护。
举个例子,我们需要根据条件动态筛选用户列表:
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<if test="username != null">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
<if test="minCreatedAt != null">
AND created_at >= #{minCreatedAt}
</if>
<if test="maxCreatedAt != null">
AND created_at <= #{maxCreatedAt}
</if>
</where>
</select>
这段代码用了 <where> 和 <if> 来构造条件。但如果字段多了之后,逻辑容易混乱。
我们后来的做法是在 Java 层构造查询条件对象,然后传入 Map,或者使用 @Param 注解明确参数名称:
List<User> searchUsers(@Param("params") UserSearchCriteria criteria);
XML 中改成:
<if test="params.username != null">
这样后续扩展起来更清晰,也方便封装通用逻辑。
踩坑记录三:事务与批量插入性能
我们有一次要做批量导入用户数据,刚开始想当然地写了个 for 循环执行 insert 语句。结果测试发现,每次插入 1000 条数据竟然要十几秒!
于是研究了一下 MyBatis 的批量操作机制,最终改成了以下方式:
@Insert({
"<script>",
"INSERT INTO user (username, email, created_at) VALUES",
"<foreach collection='users' item='user' separator=','>",
"(#{user.username}, #{user.email}, #{user.createdAt})",
"</foreach>",
"</script>"
})
int batchInsert(@Param("users") List<User> users);

配合开启事务:
@Transactional
public void importUsers(List<User> userList) {
userMapper.batchInsert(userList);
}
这样一次性提交大大减少了网络往返次数,效率提升了几十倍。
经验总结:我眼中的 MyBatis 最佳实践
经过多个项目的洗礼,我觉得用好 MyBatis 需要注意以下几个方面:
1. SQL 分离与复用
- 使用
<sql>标签提取公共部分(如字段列表) - 把常用查询封装成视图或通用方法
- 使用
<include>提高可维护性
2. 数据库设计与字段命名保持一致
- 数据库字段建议小写+下划线命名(如 user_name)
- Java 对象字段遵循驼峰命名(userName)
- MyBatis 默认自动映射驼峰和下划线组合,无需额外配置
3. 合理使用连接查询和延迟加载
- 能用一条 SQL 查清楚的不要拆成多次调用
- 对于大数据量场景慎用嵌套查询,优先考虑在业务层手动组装
- 延迟加载设置合理作用域,防止 N+1 查询问题
4. 性能监控与 SQL 日志记录
- 在开发环境启用
log4j2输出完整 SQL,方便调试 - 生产环境可通过慢查询日志 + APM 工具(如 SkyWalking)定位性能瓶颈
- 关键业务 SQL 加索引、定期做 explain 分析
结语:MyBatis 并不难,难的是如何用得专业
写到这里,回想起当初那个只会拷贝模板 XML 的自己,真的感慨万千。
现在的我依旧每天在和 MyBatis 打交道,但已经能够游刃有余地处理各种查询、优化执行计划、分析慢查询日志,甚至可以参与数据库迁移和分库方案的设计。
如果你刚刚接触 MyBatis,我希望这篇分享能帮你少走一些弯路;如果你已经在用它很久,也欢迎留言交流你的实践经验。
技术不是万能的,但我始终相信,把一件看似简单的事情做到极致,就是专业。
愿我们一起成长。 🙌

评论 0