从 JDBC 到 MyBatis:我的 Java 持久层探索之路
引言:为什么我选择了 MyBatis?

作为一名后端开发工程师,我在早期写 Java Web 应用的时候,几乎都是直接使用 JDBC 和原始 SQL。说实话,那段时间写代码的效率很低,数据库操作部分充斥着大量的模板代码,像是 Connection、PreparedStatement、ResultSet 这些玩意儿每天都在和你打交道。
有一次我们公司要上线一个新项目,时间紧任务重,技术选型由我来主导一部分。当时就在考虑持久层框架的选择。Hibernate 听说挺强大,但之前尝试过一次,总觉得它太“重”了,而且学习曲线陡峭。ORM 的好处是显而易见,但也容易让开发者脱离对 SQL 的掌控。于是最终决定采用 MyBatis —— 它轻量级、灵活性强,SQL 写在 XML 或注解中也更容易管理和优化。
这篇文章就来聊聊我是怎么一步步了解并熟练使用 MyBatis 的,中间踩过的坑,以及一些实际工作中的经验总结。
背景介绍与挑战:从零开始搭建持久层


我们的项目是一个典型的电商后台系统,需要处理订单、商品管理、用户中心等核心模块。其中订单模块涉及复杂的多表关联查询,数据量比较大,性能要求也比较高。初期为了快速开发,先用 Spring Boot 快速搭起基础架子,再引入 MyBatis 做数据访问层。
最初的想法很朴素:数据库操作应该和业务逻辑分离,同时又能灵活控制 SQL 的执行过程。但现实远比想象复杂。
比如:
- 我该如何优雅地组织 SQL?
- Mapper 接口如何定义?
- 结果集映射该怎么搞?
- 动态 SQL 怎么写?万一写错了怎么办?
- 多环境配置怎么区分(开发/测试/生产)?
这些问题让我在刚上手的时候吃了不少亏。
解决方案:MyBatis 是如何帮我们破局的?
技术选型对比:为什么不是 Hibernate 或 JPA?
我们团队内部讨论时其实也有同事推荐过 JPA,甚至有更激进的建议上 Spring Data JPA + Hibernate。
但我们最终没选择它们,是因为:
- Hibernate 封装得太多,有时候一句简单的查询会生成非常复杂的 SQL,调试起来困难;
- 在一些高并发场景下,我们需要更精细地控制数据库行为,比如分页、连接池、缓存机制;
- 对于一些老系统的表结构不够规范,JPA 需要做很多额外的适配;
- 团队整体对于 SQL 编写和调优比较熟悉,不想被 ORM 所限制。
相比之下,MyBatis 提供了良好的折中:既支持原生 SQL,又有一定的封装能力,可以让我们专注于数据库和业务之间的桥梁建设。
代码实践:从零构建你的第一个 MyBatis 项目
第一步:Spring Boot 整合 MyBatis
我们在 Spring Boot 项目里通过 Maven 引入依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
配置文件 application.yml 中添加数据库信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce?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.demo.model
第二步:编写模型类和 Mapper 接口
以订单实体为例,Order 类如下:
public class Order {
private Long id;
private String orderNo;
private BigDecimal amount;
private String status;
// getter & setter ...
}
Mapper 接口定义如下:
@Mapper
public interface OrderMapper {
List<Order> findOrdersByStatus(@Param("status") String status);
Order selectById(Long id);
int insert(Order order);
}
对应的 OrderMapper.xml 放在 resources/mapper/order 目录下:
<mapper namespace="com.example.demo.mapper.OrderMapper">
<select id="findOrdersByStatus" resultType="Order">
SELECT * FROM orders WHERE status = #{status}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders(order_no, amount, status)
VALUES(#{orderNo}, #{amount}, #{status})
</insert>
</mapper>
第三步:Service 层调用 Mapper
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public List<Order> getActiveOrders() {
return orderMapper.findOrdersByStatus("paid");
}
}
到这一步,我们就完成了最基本的数据库读写流程。
踩坑经验:那些年我们一起走过的弯路
坑 1:动态 SQL 不小心写错,查不出数据
曾经在一个批量查询接口中,我用 <foreach> 拼接 in 查询:
<select id="findOrdersByIds" resultType="Order">
SELECT * FROM orders
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>
结果始终查不到数据。后来发现是因为传入的参数名不一致,或者集合为空导致 SQL 语法错误。MyBatis 不会自动校验这些,只能靠单元测试或日志排查。
解决方案:
- 使用 MyBatis 日志插件输出完整 SQL。
- 增加参数非空判断。
- 如果可能为空,可以用
<if test="ids != null and ids.size() > 0">包裹整个 IN 条件。
坑 2:Map 映射字段出错,找不到对应属性
有时候我们会临时返回 Map 类型结果,比如:
List<Map<String, Object>> list = orderMapper.getSalesReport();
这时候如果数据库字段和 Map key 名称不一致,会导致取值失败,或者出现空值。
解决方法:
- 可以在 SQL 中使用别名保持一致;
- 或者用 ResultMap 显式定义映射关系。
坑 3:MyBatis 二级缓存开启不当造成脏数据
我们曾试图开启 MyBatis 的二级缓存提升性能,结果在某些更新操作后,读出来的还是旧数据,引发线上问题。
后来发现原因是我们没有正确配置刷新时机。MyBatis 默认是根据 Mapper 的 namespace 来做缓存,一旦有任意一个更新操作发生,该命名空间下的所有缓存都会失效。
因此我们最后决定:仅针对读多写少的静态表开启二级缓存,且配置合理的 TTL 时间。
实战效果:MyBatis 带来的收益
项目上线后,在高并发场景下表现良好:
| 模块 | 响应时间(平均) | 并发处理数 | 稳定性 |
|---|---|---|---|
| 订单查询 | 80ms | 500+ QPS | ✅ |
| 用户下单 | 60ms | 300+ QPS | ✅ |
| 数据统计报表 | 120ms | 200+ QPS | ✅ |

得益于 MyBatis 的灵活性,我们能针对不同模块定制 SQL,优化索引和执行计划,避免了 ORM 带来的性能陷阱。
经验分享:给正在入门 MyBatis 的你
1. SQL 应该写在哪里?
这个问题其实一直都有争议。我个人倾向于把 SQL 放在 XML 文件中,尤其是对于稍复杂的查询语句。这样一是方便维护,二是便于统一管理。
如果你喜欢简洁风格,也可以使用注解方式,适合 CRUD 简单的场景:
@Select("SELECT * FROM orders WHERE id = #{id}")
Order selectById(Long id);
但对于 join 查询或多条件拼接的情况,XML 更友好。
2. 结果映射尽量明确
无论是基本类型还是 POJO,MyBatis 都提供了自动映射的能力,但在实际工作中,我还是推荐手动编写 <resultMap>。虽然一开始费点时间,但后期维护起来会更加清晰。
3. 善用日志调试工具
推荐大家引入 MyBatis-Log4j2 或者集成 MyBatis Plus 自带的日志插件,方便查看真实发出的 SQL,排查慢查询。
4. 结合现代技术栈使用更高效
MyBatis 本身功能已经足够强大,但结合当前流行技术栈使用会更舒服:
- MyBatis Plus:增强增删改功能,自动生成 CRUD 方法,节省时间;
- PageHelper:支持物理分页;
- Dynamic-Datasource:实现多数据源切换(比如读写分离);
- Lombok:简化 POJO 类的 getter/setter 编写;
- Druid/Alibaba Druid:强大的数据库连接池和监控平台。
5. 生产环境注意事项
- SQL 要尽量避免全表扫描,合理使用索引;
- Mapper 文件不要过多嵌套层级,否则维护成本很高;
- 建议设置慢查询日志阈值,定期分析执行效率差的 SQL;
- 关键业务表做好备份和归档策略;
- 不要轻易在高并发接口中使用大量 left join,容易导致锁争用。
结语:MyBatis 是值得掌握的基础技能
回顾这段 MyBatis 的学习和实战旅程,虽然过程中不断踩坑,但也收获了很多宝贵的经验。它的“半自动”理念正好契合了我对数据库操作的理解——既要有控制力,又要保持灵活性。
在如今这个微服务盛行、高并发常态化的时代,掌握像 MyBatis 这样灵活高效的持久层框架,是每一位 Java 开发者的必备技能。
希望这篇文章能帮助你少走一点弯路,早点享受 MyBatis 带来的自由与高效。如果有任何问题,欢迎留言交流!
文章作者:一名热爱编码、追求极致体验的全栈开发者
本文首发于个人博客,转载请联系授权

评论 0