MyBatis基础教程:Java持久层框架入门
开篇:为什么我会选择分享这个话题?

在后端开发这条路上,我们总是绕不开数据库操作。从最开始接触JDBC的“痛苦”经历,到后来尝试各种ORM框架,一路走来踩了不少坑。而今天我要和大家分享的,是我用得最多、也最喜欢的一个——MyBatis。
MyBatis不是一个典型的“全自动”ORM框架,但它足够灵活,在性能敏感的场景下尤其有用武之地。它不像Hibernate那样自动帮你处理一切,但正是这种半自动的风格,让我在很多项目中感受到它的魅力和实用性。
这篇文章我会以一个真实的项目案例为背景,讲述我在使用MyBatis过程中遇到的一些典型问题、我的解决思路,以及一些实际开发中的经验总结。如果你刚开始接触MyBatis,或者想深入理解它的核心机制,希望这篇文章能给你带来启发。
项目背景与需求介绍

事情发生在两年前,我参与了一个企业级后台系统重构项目。原系统是一个传统的Spring + JDBC的架构,代码臃肿不说,维护起来更是头痛。由于历史原因,SQL和业务逻辑混合严重,数据结构也不够规范。新项目的目标很明确:
- 提升代码可维护性
- 提高数据访问层性能
- 支持多数据源切换(主要是Oracle和MySQL)
- 后续要接入分库分表方案
当时我们团队技术栈已经确定使用Spring Boot,所以持久层的选择就成了关键点。虽然团队里有人倾向于Hibernate,但我坚持选择了MyBatis,因为它更贴近SQL的特性更适合我们的业务场景,尤其是在性能要求较高的报表模块和高频查询接口上。
于是,我开始了MyBatis的全面引入工作。
初识MyBatis:为什么选择它?

可能有些同学会问:“为什么不直接用JPA或Hibernate?那样写代码不是更简单吗?”
确实,JPA和Hibernate这类全自动ORM省去了大量模板代码,但也有明显的缺点:
- 生成的SQL质量不可控:Hibernate虽然封装得很漂亮,但对于复杂的联表查询和性能优化往往束手无策。
- 学习成本高,调试困难:你不得不花很多时间去了解HQL、Criteria API这些非标准SQL的东西。
- 灵活性受限:有时候你要做的是一些非常定制化的SQL,这时候Hibernate就显得捉襟见肘了。
而MyBatis则不同,它强调的是“SQL映射”,也就是让开发者自己写SQL,框架只负责参数映射和结果集转换。这听起来是不是有点老派?但恰恰是这种设计,让我们可以完全掌控SQL,做到极致优化。
更重要的是,MyBatis和Spring整合非常好,配合注解、动态SQL等功能,既保持了简洁,又不失灵活性。
遇到的第一个挑战:如何优雅地组织MyBatis的配置?

在我刚接手项目的初期,我尝试把所有Mapper XML文件都放到resources目录下,然后通过<mapper>标签逐个引入。但是很快我就发现,随着模块增多,这种方式变得越来越难管理。
举个例子,比如我们有一个订单模块,里面就有OrderMapper.java、OrderMapper.xml、Order.java等多个文件,如果再加几个关联实体类和Service,整个结构就容易混乱。
于是我决定采用如下方式组织代码结构:
src
├── main
│ ├── java
│ │ └── com.example.mapper
│ │ ├── OrderMapper.java
│ │ ├── UserMapper.java
│ │ └── ...
│ │
│ └── resources
│ └── mapper
│ ├── order
│ │ ├── OrderMapper.xml
│ │ └── OrderDetailMapper.xml
│ └── user
│ └── UserMapper.xml
同时,在application.yml中配置扫描路径:
mybatis:
mapper-locations: classpath:mapper/**/*.xml
这样做的好处是显而易见的:
- 按功能模块划分,避免XML文件堆积在一个目录
- 修改和查找对应Mapper更加直观
- 便于后续做代码自动生成工具时定位资源
这算是我第一个小改进吧,也让整个项目结构看起来更清爽。
第二个挑战:如何处理动态SQL?
MyBatis最强大的功能之一就是它的动态SQL能力。但在实际使用过程中,很多人只是用了简单的if判断,却忽略了它真正的威力。
我记得当时有个查询接口,用户可以根据多个条件组合筛选订单数据,传入的参数包括订单状态、时间段、客户姓名、手机号等等。一开始我是这么写的:
<select id="selectOrders" resultType="Order">
SELECT * FROM orders WHERE 1=1
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</if>
<if test="customerName != null">
AND customer_name LIKE CONCAT('%',#{customerName},'%')
</if>
</select>
这段SQL乍一看没问题,但实际上有几个潜在的问题:
WHERE 1=1这种写法在正式场合其实不太推荐,不够优雅- 如果没有任何条件传入,那就会变成
SELECT * FROM orders WHERE 1=1,可能会扫全表,影响性能 - 多个AND之间的拼接容易出错,尤其是在嵌套条件判断的情况下
于是,我查阅官方文档,改成了使用<where>标签包裹动态条件的方式:
<select id="selectOrders" resultType="Order">
SELECT * FROM orders
<where>
<if test="status != null">
status = #{status}
</if>
<if test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</if>
<if test="customerName != null">
AND customer_name LIKE CONCAT('%',#{customerName},'%')
</if>
</where>
</select>
这样一来,即使没有条件传入,也不会出现WHERE关键字错误,同时也避免了手动写WHERE 1=1的尴尬写法。而且MyBatis在内部会智能拼接AND/前缀。
除此之外,我还用到了<set>标签用于更新操作,<choose>进行条件分支判断,以及<foreach>处理IN查询。这些都是MyBatis非常实用的功能。
第三个挑战:MyBatis事务控制与多数据源配置
项目后期我们需要接入MySQL和Oracle双数据源的支持,这就涉及到MyBatis对多数据源的配置和事务管理问题。
早期的时候,我们所有的数据库操作都在一个数据源里。Spring Boot默认只有一个DataSource,所以MyBatis也能很好地配合事务。但一旦引入了第二个数据源,情况就变了。
我最初的思路是在不同的服务类中注入不同的SqlSessionTemplate或@MapperScan扫描不同的包,但这会导致事务无法跨数据源生效。也就是说,如果某个方法需要操作两个数据源的数据,用Spring的@Transactional注解就不起作用了。
最终我们采用了基于AbstractRoutingDataSource的动态数据源方案。大致流程如下:
- 自定义一个ThreadLocal存储当前线程使用的数据源标识符。
- 创建多个目标数据源(MySQL、Oracle等)。
- 构建一个路由数据源,根据当前线程的标识选择具体的数据源。
- 在事务入口处通过切面或者手动设置数据源类型。
关键代码如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
}
然后在Spring配置中:
@Bean
@ConfigurationProperties(prefix = "spring.datasource.oracle")
public DataSource oracleDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource(DataSource oracleDataSource, DataSource mysqlDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("oracle", oracleDataSource);
targetDataSources.put("mysql", mysqlDataSource);
DynamicDataSource ds = new DynamicDataSource();
ds.setTargetDataSources(targetDataSources);
ds.setDefaultTargetDataSource(mysqlDataSource); // 设置默认数据源
return ds;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
注意:这种做法只适用于单体应用,如果是微服务环境下,建议还是通过服务治理来做数据源隔离,因为事务跨库风险极高,维护成本也很高。
踩过的一些坑
坑一:Mapper接口找不到,导致启动失败
这个问题太常见了。通常是因为没有正确配置mybatis.mapper-locations路径,或者没有在启动类加上@MapperScan注解。
解决方案:
- 检查你的
application.yml中配置的mapper路径是否正确; - 启动类上加上
@MapperScan("com.example.mapper"),指定正确的Mapper接口所在包; - 或者在每个Mapper接口上加上
@Mapper注解(不推荐,容易遗漏);
坑二:ResultMap映射出错,字段为空
有时候你会发现,明明数据库中有值,但返回的对象属性却是null。这是因为字段名与Java属性名不匹配。
比如数据库字段是user_name,而你的Java属性是userName,这时候就需要在Mapper XML中显式定义映射关系:
<resultMap id="BaseResultMap" type="User">
<id column="user_id" property="userId"/>
<result column="user_name" property="userName"/>
<result column="email_address" property="emailAddress"/>
</resultMap>
当然,也可以通过开启MyBatis的自动映射功能,比如配置:
mybatis:
mapUnderscoreToCamelCase: true
这样就可以实现user_name → userName的自动转换。
坑三:SQL语句执行慢,日志看不到完整SQL
我们在生产环境上线之后发现某接口响应时间很长,怀疑是SQL的问题,但日志里打印出来的都是类似这样的内容:
==> Preparing: SELECT * FROM orders WHERE status = ?
这显然看不出来真实情况。为了排查性能问题,我开启了MyBatis的日志插件log4j和STDOUT_LOGGING模式:
<!-- mybatis-config.xml -->
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
但这种方式只能输出带?占位符的SQL。想要看到真实执行的SQL,必须结合拦截器或使用Druid内置的SQL监控功能。
这里我强烈推荐引入Druid作为连接池,并开启其内置的SQL监控和慢SQL分析功能,对于生产排障帮助极大。
性能优化小技巧
批量插入优化
我们之前有一个接口需要批量导入用户信息到数据库,每批几万条数据。最开始的做法是一个一个调用INSERT语句,结果可想而知,速度极慢,还容易被打断。
后来改为使用MyBatis的<foreach>结合UNION ALL的方式:
<insert id="batchInsert">
INSERT INTO users (name, email, created_at)
<foreach collection="list" item="item" separator=" UNION ALL ">
(#{item.name}, #{item.email}, #{item.createdAt})
</foreach>
</insert>
但这种方式在Oracle中并不适用,因为Oracle不支持UNION ALL的INSERT语法。
于是我们换成了另一种方式:使用JDBC的BatchUpdate功能,借助MyBatis的SqlSession实现:
public void batchInsert(List<User> userList) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (User user : userList) {
mapper.insert(user);
}
sqlSession.commit();
} finally {
sqlSession.close();
}
}
这样不仅效率高,还能保证事务一致性。
缓存优化
为了缓解数据库压力,我们也启用了MyBatis的一级缓存(默认启用)和二级缓存(需要手动配置)。二级缓存的作用域是同一个Mapper namespace下的所有查询。
配置方式如下:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
不过要注意,使用二级缓存的代价是增加内存占用,并且需要考虑缓存失效的问题。在写多读少的场景下,不建议启用二级缓存。
项目落地后的效果
经过一段时间的重构和优化,项目的整体表现提升明显:
- 接口平均响应时间下降了约37%
- 数据库QPS有所降低,CPU负载稳定
- 代码结构清晰,易于维护和扩展
- 团队成员更容易上手数据库相关开发
特别是在报表查询部分,原本需要2秒才能返回的接口,优化后基本控制在300ms以内。
给读者的一些建议和注意事项
合理使用MyBatis的功能,不要滥用动态SQL。虽然动态SQL强大,但如果过于复杂反而会影响可读性和维护性。
注重SQL编写质量,不要偷懒。即使是MyBatis也要关注索引使用、JOIN顺序等问题。很多时候性能瓶颈不在框架本身。
学会查看执行计划。无论是MySQL的EXPLAIN还是Oracle的Execution Plan,都能帮你快速定位问题。
善用工具。比如Druid、Logback、IDEA的MyBatis插件,甚至是数据库的慢查询日志,都是排查问题的利器。
合理使用缓存。MyBatis的缓存不是银弹,要根据实际情况权衡取舍。
提前规划好模块化结构。良好的项目结构会让你在后续迭代中省去大量重构成本。
写在最后
回顾这一路的学习和实践,我觉得MyBatis带给我的不只是代码上的便利,更多是对数据库交互本质的理解。它教会我,任何框架都不能代替扎实的SQL功底和系统设计能力。
现在回头看那些踩过的坑,其实也都是成长的一部分。技术这条路本就没有捷径,只有不断试错、总结、优化,才能真正掌握一个工具,甚至一套思想。
希望我的这篇分享能让你少走弯路,也能感受到一点来自实战的真实温度。如有不足之处,欢迎留言交流!
附录:常见MyBatis依赖配置示例
<!-- pom.xml 示例 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
作者碎碎念:
其实我一直觉得,好的技术文章不仅要讲清楚原理,更要讲明白它是怎么在现实世界里跑起来的。MyBatis不是一个完美的工具,但它足够成熟、足够灵活,值得我们好好琢磨。希望你在使用MyBatis的过程中,也能写出高效、优雅、可控的SQL代码!

评论 0