MyBatis基础教程:从“写死SQL”到优雅的持久层实践
引言:为什么选择MyBatis?

在我刚入行那会儿,公司的一个老项目还在用JDBC直接拼接SQL语句。你可以想象一下那个场景——一个DAO类里充斥着PreparedStatement和各种setString()、setInt()的调用,维护起来就像修一条年久失修的老路,每次改动都提心吊胆。
后来我们尝试用了Hibernate,结果又陷入了一个完全不同的泥潭:生成的SQL复杂难控,关联查询多的时候性能奇差,开发调试困难重重。直到我接触到了MyBatis,才算真正理解什么叫“掌控与便捷的平衡”。
这篇文章就想从我的亲身经历出发,带大家从头了解MyBatis这个Java生态中非常实用的持久层框架,并结合实际项目中的经验,聊聊怎么用它写出既稳定又高效的代码。
项目背景与挑战:一次数据迁移任务的“噩梦”

去年我参与了一个老旧系统的重构项目,目标是从一套单体架构系统迁移到微服务架构下,其中数据迁移是一个关键环节。我们面对的是多个Oracle数据库表,共计上千万条历史数据需要清洗、转换并导入MySQL新库。
在初期,团队是采用传统DAO+JDBC的方式写的迁移逻辑,效果惨不忍睹:
- SQL硬编码在Java类中难以复用
- 多个字段映射手动处理,出错率高
- 查询复杂后难以维护
- 分页、动态条件查询要自己封装一堆if判断
整个迁移过程像是一次又一次“人肉ORM”,效率低不说,还总出bug。老板看不下去了,说:“要么引入点能干活的ORM工具,要么咱们改手写SQL算了。”
于是,我们开始调研轻量级ORM框架,并最终选择了MyBatis。
解决方案:MyBatis如何让我们的持久层“脱胎换骨”?

1. 架构设计层面的选择
MyBatis不是全自动化ORM(比如Hibernate那种),而是所谓的“半自动ORM”。这意味着:
- 你需要自己写SQL,但可以通过配置实现映射(ResultMap)
- 可以灵活控制查询语句,避免生成“笨重”的SQL语句
- 更容易排查和优化性能瓶颈,适合对性能有要求的场景
这正好符合我们这次项目的需求:我们不想把所有业务逻辑交给框架来处理,但也不愿意回到手动拼接SQL的时代。MyBatis就像是给了我们一双“魔法手套”,既保留了SQL书写的自由度,又能大幅提升开发效率。
2. 数据库设计的配合优化
我们为数据迁移设计了一个新的中间状态数据库,专门用于缓存过渡数据。使用MyBatis后,我们可以非常方便地通过XML文件或注解方式定义SQL语句,并通过接口契约统一访问入口。
例如,一个用户基本信息的读取操作就变成这样:
public interface UserMapper {
@Select("SELECT id, name, email FROM user_info WHERE status = #{status}")
List<User> selectByStatus(int status);
}
简洁明了,不需要繁琐的JDBC处理流程。
快速入门实战:搭建你的第一个MyBatis项目
1. 创建Maven项目结构

如果你是Spring Boot环境,添加依赖如下即可:
<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>
<version>8.0.33</version>
</dependency>
2. 配置数据源(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.model
3. 编写实体类 + Mapper接口
public class Product {
private Long id;
private String name;
private Double price;
// getter/setter 略
}
@Mapper
public interface ProductMapper {
Product selectById(Long id);
}
4. 写SQL映射(XML版)
在resources/mapper目录下创建ProductMapper.xml:
<mapper namespace="com.example.mapper.ProductMapper">
<select id="selectById" resultType="com.example.model.Product">
SELECT * FROM product WHERE id = #{id}
</select>
</mapper>
这就是最基础的MyBatis应用了。是不是比纯JDBC简单多了?接下来我们会深入一些更高级的用法。
踩坑实录:那些年我和MyBatis一起走过的弯路
1. XML里SQL写错了,查不出来还不报错!
有一次我在XML里写了个字段名拼错了,比如把 user_name 写成了 user_nmae,MyBatis竟然没报错,只是返回了null值。这让我找了半天才发现问题。
教训总结:
- 在开发时一定要开启日志打印SQL语句(log4j或logback)
- 使用
resultType的时候,字段必须严格匹配 - 推荐初期使用
resultMap来显式映射字段
2. 动态SQL使用不当导致查询慢
我们在做分页查询时,一开始写了类似这样的SQL:
<select id="searchProducts" resultType="Product">
SELECT * FROM product
<where>
<if test="name != null and name.length() > 0">
AND name LIKE '%${name}%'
</if>
</where>
</select>
虽然功能正常了,但在数据量大时发现很慢。原来 ${} 是字符串替换,无法预编译,极易引发SQL注入和性能问题。
优化建议:
- 使用
#{}占位符 - 模糊查询改写成:
AND name LIKE CONCAT('%', #{name}, '%')
或者使用MyBatis的 <bind> 标签预处理参数。
3. 批量插入效率太低
我们刚开始做数据导入时,是循环调用单条插入的方法,结果几十万条数据跑了两个小时还没完。
后来改成批量插入,使用MyBatis的foreach语法:
<insert id="batchInsert">
INSERT INTO product (name, price) VALUES
<foreach collection="products" item="product" separator=",">
(#{product.name}, #{product.price})
</foreach>
</insert>
配合JDBC URL增加参数:
rewriteBatchedStatements=true
一下子插入速度提升了十倍不止。
效果对比:用MyBatis前 vs 后
| 指标 | 原JDBC方式 | MyBatis方式 |
|---|---|---|
| 开发效率 | 慢,重复劳动多 | 提升50%以上 |
| 维护难度 | 高,耦合严重 | 明显降低 |
| 性能表现 | 差异不大(合理使用) | 更好控制 |
| 错误率 | 高 | 显著下降 |
最直观的变化是:原本一周的工作,现在两三天就能搞定;更重要的是,团队协作也顺畅了很多,因为每个人都知道去哪里找对应的SQL,不用再翻几十个Java类来找一句拼接的INSERT语句。
实战经验分享:给初学者的一些建议

✅ 小贴士一:学会“动静结合”
MyBatis的最大优势就是动态SQL。比如:
<if>:条件判断<choose>/<when>:分支逻辑<foreach>:循环处理集合<set>:更新部分字段
这些标签用好了,可以让你的SQL更清晰,也可以省掉大量冗余Java代码。
✅ 小贴士二:合理使用ResultMap
如果你的字段名与列名差异较大,或者涉及联合查询映射多个对象,建议使用<resultMap>来显式定义映射规则:
<resultMap id="userWithRoleMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<association property="role" javaType="Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
</association>
</resultMap>
✅ 小贴士三:关注日志输出与事务管理
别等到上线才去加日志,开发阶段就应该打开SQL日志输出:
在logback.xml中加入:
<logger name="com.example.mapper" level="DEBUG"/>
另外,对于需要保证一致性的操作,务必用@Transactional包裹多个Mapper调用,否则可能出现脏数据。
当前趋势下的思考:MyBatis还有未来吗?
很多人可能会问:现在很多Spring Data JPA、Querydsl甚至Kotlin的Exposed也出来了,MyBatis是不是该被淘汰了?
我的观点是:否!
- 灵活性:相比JPA来说,MyBatis更适合需要深度定制SQL的场景
- 性能控制:你可以精准控制每条SQL的执行路径,便于分析性能瓶颈
- 学习成本低:相比Hibernate的庞大体系,MyBatis更容易上手,适合中小型项目快速迭代
当然,在大型项目中也可以考虑混合使用,比如:
- 对于CRUD简单的模块使用Spring Data JPA
- 对性能敏感或查询复杂的模块使用MyBatis
结语:技术没有银弹,只有适配场景的解
回顾这一年多的MyBatis使用历程,我最大的感悟是:工具只是手段,解决问题才是核心。你可能今天用MyBatis,明天用Hibernate,后天换成JOOQ,只要你理解背后的思想,都能写出高质量的代码。
希望这篇来自一线实战的经验文章,能帮你少走弯路,快速掌握MyBatis的基础和进阶技巧。
如果你也在用MyBatis,欢迎留言交流你踩过的坑,我们一起成长!

评论 0