MyBatis 基础教程:Java 持久层框架入门
引言:从手写 JDBC 到 MyBatis 的蜕变

我第一次接触持久层开发的时候,还是靠纯手动写 JDBC 来操作数据库的。那个时候每天都在拼接 SQL 字符串、处理结果集映射、关闭连接池……每次修改表结构都要改好几处代码。有一天晚上加班改完一个字段映射后,程序跑不起来了,排查了整整两个小时才发现少了个空格。
那之后我就痛下决心,必须得找一种更优雅的方式来和数据库打交道。后来公司项目引入了 MyBatis,从此彻底告别了那段“SQL 手工时代”。
这篇文章想结合我的实际工作经历,来聊聊我是如何一步步掌握 MyBatis 这个 Java 持久层利器的,包括我在使用过程中踩过哪些坑,又是怎么绕出来的。
项目背景:一个 CMS 系统的重构

去年我们团队要对内部使用的 CMS 内容管理系统进行架构升级,原来的系统用的是纯 DAO + JDBC,代码冗长不说,还特别容易出错。比如最简单的一个文章详情页接口,就需要手动写一堆 ResultSet 转换逻辑,一旦数据库字段变了,很容易漏掉某个字段没更新导致报错。
我们决定在新版本中引入 MyBatis,希望通过 ORM 的方式来提高开发效率,并且统一数据访问层的编码规范。
遇到的挑战:不仅仅是技术问题
刚开始上手的时候,确实遇到不少问题:
- 查询语句总是写不对 SQL 映射
- 复杂对象(嵌套结构)转换失败
- 动态 SQL 不知道该怎么用
- 数据库连接老是超时,不知道配置哪里错了
最尴尬的一次是我们在一个统计查询的接口里写了多表关联,结果由于 resultType 类型配错了,导致返回的数据被强制转成了错误类型,测试同学反馈说数字变成 null,查了半天才定位到是映射的问题。
解决方案:MyBatis 的核心实践
1. 先解决基础问题:SQL 映射和实体类绑定
我们采用的是 XML 方式定义 Mapper,主要考虑到后期维护方便,尤其是复杂的 SQL 和动态 SQL 可读性更好。
举个例子,我们有张 article 表,对应实体类 Article.java:
public class Article {
private Long id;
private String title;
private String content;
private LocalDateTime publishTime;
// getter/setter 省略
}
对应的 XML Mapper 就像这样:
<mapper namespace="com.example.cms.mapper.ArticleMapper">
<select id="getById" resultType="com.example.cms.model.Article">
SELECT id, title, content, publish_time
FROM article
WHERE id = #{id}
</select>
</mapper>
一开始我也纠结用 resultType 还是 resultMap,不过大多数情况下简单的实体可以直接用 resultType,如果字段名和属性名不一样,可以配合 @Results 注解或者自定义 resultMap。
小贴士:如果你的数据库字段命名习惯是下划线分隔,而 Java 对象是驼峰命名,建议开启 mapUnderscoreToCamelCase 设置,省去很多麻烦!
2. 复杂对象映射怎么办?
后来遇到了一个需求,需要把文章的分类信息一并返回。我们设计了 Category 实体类,并希望返回的 Article 对象中包含一个 Category 属性。
这个时候我们就不能用 resultType 了,需要用 resultMap 定义复杂的映射关系:
<resultMap id="articleWithCategoryResultMap" type="Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<result property="publishTime" column="publish_time"/>
<association property="category" javaType="Category">
<id property="id" column="category_id"/>
<result property="name" column="category_name"/>
</association>
</resultMap>
<select id="getWithCategory" resultMap="articleWithCategoryResultMap">
SELECT a.id, a.title, a.content, a.publish_time,
c.id AS category_id, c.name AS category_name
FROM article a
LEFT JOIN category c ON a.category_id = c.id
WHERE a.id = #{id}
</select>
这一块算是 MyBatis 的精华之一。通过 <association> 我们实现了对象的嵌套映射,不需要再自己手动 set 对象属性。
3. 动态 SQL 是真香预警
在写搜索功能的时候,我们需要根据多个条件组合查询文章,比如按标题模糊匹配、时间范围筛选等。
如果没有 MyBatis 提供的动态 SQL,那就只能自己拼字符串了。但用了 <if>、<where> 这些标签之后,真的舒服了不少。
下面是一个动态查询的示例:
<select id="search" parameterType="map" resultType="Article">
SELECT * FROM article
<where>
<if test="title != null and title != ''">
AND title LIKE CONCAT('%', #{title}, '%')
</if>
<if test="startTime != null">
AND publish_time >= #{startTime}
</if>
<if test="endTime != null">
AND publish_time <= #{endTime}
</if>
</where>
</select>
是不是比自己用 if else 拼字符串清晰多了?特别是当条件变得更多时,这种优势会更明显。
4. 配置文件别瞎改,小心连不上数据库
之前有个同事在 application.yml 中误配了连接池参数,结果服务启动没问题,一跑起来就报 connection closed。
后来查了一下他的配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/cms?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 5
minimum-idle: 1
idle-timeout: 10000
max-lifetime: 1800000
connection-test-query: SELECT 1
看起来没毛病?但其实他少了一个非常关键的配置项:
mybatis:
configuration:
mapUnderscoreToCamelCase: true
而且他还忘记在 mapper XML 中添加命名空间扫描路径。这就导致 Spring Boot 启动后一直找不到 mapper 文件,接口调用时直接抛 NullPointer。
所以建议大家在整合 Spring Boot 使用 MyBatis 的时候一定要注意以下几个关键配置点:
- MyBatis 的
mapperLocations - 数据源配置正确无误
- 如果用了二级缓存或插件,记得加载配置
- 酌情设置日志输出,便于排查 SQL 错误
踩过的坑与应对经验
坑 1:动态 SQL 参数为空时 SQL 报错
有时候我们会传一些可选参数进去,但如果不小心没加判断,就会导致 SQL 出现多余的 AND 或者 OR。
解决方案:永远使用 <where> 标签包裹动态条件,它会自动处理多余的 AND/OR 问题。
坑 2:结果集映射错位,导致属性赋值混乱
这个坑我在做联表查询的时候踩过不止一次。比如 LEFT JOIN 查询返回的字段包含了两个 id,导致 Java 对象赋值错乱。
解决方案:
- 给重复字段起别名(AS)
- 在 resultMap 中明确指定 column 名
- 如果用注解方式,也可以通过 @Column 注解来标记列名
坑 3:PageHelper 分页插件失效
我们在做文章列表接口时用了 PageHelper 插件来做分页查询,但发现有时候分页生效,有时候无效。
根本原因:PageHelper 的分页只对紧跟它的第一个查询有效。如果中间夹了其他 SQL 调用,或者用了异步方法,就可能失效。
修复方式:
- 确保 PageHelper.startPage() 跟着你要分页的查询语句
- 查看当前 PageHelper 的版本是否与 MyBatis 版本兼容
- 如果有复杂查询,考虑自己实现逻辑分页或使用数据库的窗口函数
总结:MyBatis 带来的改变与收益
用了 MyBatis 之后,我们整个项目的 DAO 层代码减少了将近 40%,并且维护成本明显降低。原来一个 CRUD 接口要写上百行的 DAO 代码,现在只需要几个 XML 配置和接口定义即可搞定。
更重要的是提升了系统的健壮性和可扩展性。当我们需要调整数据库结构或添加新字段时,只需要修改对应的 resultMap 或 SQL,就能快速完成适配。
当然,MyBatis 不是银弹,它适合那些需要灵活控制 SQL 的场景。对于追求全自动 ORM 的项目,也可以选择 JPA;但在我们这个 CMS 场景下,MyBatis 更符合业务特点。
给初学者的几点建议
如果你刚接触 MyBatis,以下几点可能会帮你少走弯路:
- 不要急着学高级特性:先理解 SQL 映射的基本机制,搞清楚什么是
#{}和${}的区别 - 善用日志输出 SQL:把日志级别设为 DEBUG,能清楚看到最终执行的 SQL,避免拼接错误
- 多练手写动态 SQL:比如条件查询、批量插入这些常用场景,动手写一遍记忆更深
- 了解底层原理有助于排错:MyBatis 本质上封装了 JDBC,明白它是怎么做的,才能在出问题时快速定位
- 合理使用连接池:推荐使用 HikariCP,速度快又稳定,注意设置最大连接数防止数据库被打爆
结尾感悟:技术的进化,从工具开始
回想起以前每天为了改一个字段就要翻好几个 DAO 类的日子,现在想想真是不堪回首。有了 MyBatis,我才真正体会到什么是“面向对象+SQL”的自由度和效率。
虽然现在也有 QueryDSL、JPA Criteria 等各种进阶玩法,但在日常工作中,我还是最喜欢用 MyBatis 来处理那些“需要一点灵活性”的数据库交互场景。
如果你还在用原生 JDBC 或者手写 SQL 拼接,不妨试试 MyBatis。也许就是这一步小小的尝试,会成为你编程习惯转变的重要契机。
如有收获,欢迎留言交流。一起进步,才是成长的意义。

评论 0