MyBatis 入门实战:从“写死 SQL”到灵活 ORM 的成长之路
开篇:缘起

作为一个 Java 后端开发,我经历过一段“原始开发”的阶段。当时刚接触后端系统时,数据层的操作全是通过 JDBC 手写 SQL 拼接完成的,不仅要考虑数据库连接池、事务管理,还要处理各种 ResultSet 的封装逻辑。那段时间,每次看到项目代码里满屏的 try-catch 和 switch-case 就头疼不已。
后来我们团队接手了一个中型项目的重构任务,需要快速搭建稳定的持久层结构。在这个节点上,我第一次正式引入了 MyBatis 作为 ORM 层的解决方案。这个框架在灵活性和性能之间取得了一个不错的平衡点,既没有 Spring Data JPA 那样“重”,又不像纯 JDBC 那么繁琐。
今天,我想基于这次项目经历,谈谈我是如何从“MyBatis 新手”成长为能够驾驭它的人,也希望能给还在使用 JDBC 或者准备入门 MyBatis 的同学一些启发。
问题描述:为什么我们需要 MyBatis?

项目背景
我们当时要重构的是一个企业内部的 CRM 系统,原本的版本由几个外包团队拼接而成,数据访问部分几乎全部是直接拼接 SQL + Map 手动映射结果集的方式。随着业务功能越来越多,维护成本越来越高:
- SQL 分散在各个 DAO 类中,修改困难
- 多个方法调用多个 SQL,事务控制混乱
- 结果集映射易出错,经常有字段名写错导致数据为空
- 查询优化难以统一处理
这种情况下,我们决定对数据访问层进行抽象,并评估了几种方案:
| 技术选型 | 优点 | 缺点 |
|---|---|---|
| 原生 JDBC | 控制力强 | 开发效率低,容易出错 |
| Hibernate/JPA | 完全 ORM,自动生成 SQL | 对复杂查询支持差,SQL 不透明 |
| MyBatis | 自定义 SQL,灵活性高 | 需要手动编写 XML |
权衡之下,最终选择了 MyBatis,因为它允许我们保留 SQL 的精细控制能力,同时又能很好地解耦对象模型与数据库结构。
解决方案:引入 MyBatis 架构设计

整体架构思路
我们在项目中采用了经典的三层架构:
Controller --> Service --> Mapper(MyBatis)
其中,Mapper 层完全由 MyBatis 驱动,而 Service 层负责协调事务并整合多个 Mapper 调用。
为了更好地组织代码,我们还做了几点设计优化:
统一接口命名规范:
public interface UserMapper { User selectById(Long id); List<User> selectAll(); int insert(User user); int update(User user); int deleteById(Long id); }XML 文件集中管理: 把所有的 XML 映射文件统一放在
resources/mapper/下,并按模块划分目录结构,比如用户模块放在resources/mapper/user/UserMapper.xml。动态 SQL 组装: 利用了
<if>、<choose>等标签来简化条件拼接,比如搜索用户的逻辑:<select id="searchUser" resultType="User"> SELECT * FROM users WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="email != null and email != ''"> AND email = #{email} </if> </select>分页封装: 我们封装了通用的 Page
返回结构,并利用拦截器自动注入 count 查询,提升用户体验。 事务管理: 通过 Spring 的注解式事务控制,在 Service 层的方法加上
@Transactional,使得多张表操作可以在同一个事务中完成。
代码实践:一步步带你写第一个 Mapper

第一步:引入依赖
我们在 Spring Boot 项目中引入了如下依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Spring Boot 会自动配置 DataSource 和 SqlSessionFactory,非常方便。
第二步:配置 application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/my_crm?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.crm.model
这样就能让 Spring Boot 自动扫描实体类和 XML 映射文件。
第三步:创建 Model 和 Mapper 接口
public class User {
private Long id;
private String name;
private String email;
// setter & getter...
}
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(User user);
}
如果你不想在每个 Mapper 上加 @Mapper 注解,也可以在启动类上加 @MapperScan:
@SpringBootApplication
@MapperScan("com.example.crm.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
第四步:编写 XML 映射文件
这是更常见的做法,尤其对于复杂的 SQL 来说,XML 更适合管理。
<!-- resources/mapper/user/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.crm.mapper.UserMapper">
<select id="selectAll" resultType="User">
SELECT * FROM users
</select>
<select id="searchUser" resultType="User">
SELECT * FROM users
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</where>
</select>
</mapper>

踩坑经验:这些错误你可能也会犯
在实际开发中,我也踩了不少坑,这里挑几个典型的说说:
1. 事务失效了!
刚开始我们以为只要加上 @Transactional 注解就万事大吉,结果发现事务并没有生效。后来查资料才知道:
- 如果事务方法不是通过 Spring 注入的 Bean 调用的(比如自己 new 实例),事务不生效;
- 同一个类中非事务方法调用事务方法,也不会生效;
- 方法必须为 public,protected/private 也不行。
解决办法很简单:确保事务方法通过注入的 Bean 调用即可。
2. 结果映射错误或字段名映射不上
有时候我们会遇到数据库字段是下划线命名,Java 是驼峰命名的情况,这时候就需要设置自动映射规则。
默认情况下,MyBatis 并不会自动帮你做 user_name -> userName 的映射。我们可以通过以下方式解决:
<resultMap id="userResultMap" type="User">
<id property="id" column="id" />
<result property="userName" column="user_name"/>
<result property="email" column="email"/>
</resultMap>
或者启用自动映射:
mybatis:
configuration:
mapUnderscoreToCamelCase: true
3. 动态 SQL 标签用错了
比如 <where> 写成了 <if>,或者拼接条件的时候忘记加 AND,都会导致 SQL 出错。特别是下面这个例子:
<select id="wrongIf">
SELECT * FROM users WHERE
<if test="name != null">
name = #{name}
</if>
<if test="age != null">
age = #{age}
</if>
</select>
如果 name 是空,生成的 SQL 可能变成:
SELECT * FROM users WHERE age = ?
这会导致语法错误,因为 WHERE 后面没有任何有效条件。因此建议使用 <where> 标签包裹所有条件。
4. 使用 resultMap 时出现循环引用
如果我们不小心在 resultMap 中引入了嵌套的对象映射,比如 User 对象包含 Department,而 Department 又反过来包含 UserList,就容易造成栈溢出或无限递归。
这种情况可以通过延迟加载(lazyLoadingEnabled)配合代理模式来处理,或者在关键地方断开循环。
效果总结:从杂乱到有序的转变

引入 MyBatis 后,我们整个项目的数据访问层清晰了很多:
- 可维护性提升:SQL 统一管理,修改方便;
- 开发效率提升:避免重复写 Resultset 映射;
- 可读性增强:Mapper 接口和 XML 分离,职责明确;
- 事务控制得当:通过 Spring 注解实现细粒度事务控制;
- 扩展性强:后续接入 PageHelper、MyBatis Plus 也非常顺利。
更重要的是,由于 SQL 还是我们自己写的,所以可以放心地做索引优化、执行计划分析等操作,完全掌控数据库层面的性能。
经验分享:给初学者的一些建议
如果你正在学习 MyBatis,我可以给你几个实用建议:
1. 保持 SQL 自主权
不要迷信“全自动 ORM”,尤其是面对复杂业务场景时。很多时候写几句 SQL 比配置一堆关联关系更快、更可控。
2. XML 文件要整洁
即使你可以用注解写 SQL,但对于复杂的查询、更新、批量插入等操作,XML 仍然是最佳选择。记得把 XML 按照模块分类存放。
3. 学好动态 SQL
掌握 <if>、<choose>、<where>、<foreach> 这些标签,它们会让你写出优雅的条件查询和批量操作语句。
4. 配合日志调试
使用 Logback 或 Log4j 输出 MyBatis 生成的 SQL,方便调试和优化。记得打开慢查询日志:
log4j.logger.com.example.crm.mapper=DEBUG
5. 合理使用 PageHelper
分页插件是一个很实用的工具,但注意使用时机和范围,避免影响全局查询。
6. 注意性能边界
虽然 MyBatis 比较轻量,但也别滥用。合理使用缓存、避免 N+1 查询、适当开启批处理,都是提升系统性能的关键点。
结语:MyBatis 是一道通往成熟架构的桥梁
回想起当年那个拿着 JDBC 在 try-catch 里打转的日子,再对比现在用 MyBatis 高效构建稳定系统的体验,真的感触颇深。
MyBatis 可能不是最现代的 ORM 框架,但它确实是一个非常适合中大型项目的技术选型。尤其是在国产化替代的大趋势下,它更是许多政企项目首选的数据访问层解决方案。
如果你也在用 MyBatis,或者正准备开始学习它,希望这篇文章能让你少走些弯路,早点享受它带来的高效与稳定。毕竟,一个好的数据访问层,是一切业务逻辑的根基。
如你有更多疑问或交流,欢迎留言评论,我们一起进步!

评论 0