从 JDBC 到 MyBatis:我的 Java 持久层探索之路

一只会写码的猫
2025-06-16 08:47
阅读 392

引言:为什么我选择了 MyBatis?

引言:为什么我选择了 MyBatis?

作为一名后端开发工程师,我在早期写 Java Web 应用的时候,几乎都是直接使用 JDBC 和原始 SQL。说实话,那段时间写代码的效率很低,数据库操作部分充斥着大量的模板代码,像是 Connection、PreparedStatement、ResultSet 这些玩意儿每天都在和你打交道。

有一次我们公司要上线一个新项目,时间紧任务重,技术选型由我来主导一部分。当时就在考虑持久层框架的选择。Hibernate 听说挺强大,但之前尝试过一次,总觉得它太“重”了,而且学习曲线陡峭。ORM 的好处是显而易见,但也容易让开发者脱离对 SQL 的掌控。于是最终决定采用 MyBatis —— 它轻量级、灵活性强,SQL 写在 XML 或注解中也更容易管理和优化。

这篇文章就来聊聊我是怎么一步步了解并熟练使用 MyBatis 的,中间踩过的坑,以及一些实际工作中的经验总结。


背景介绍与挑战:从零开始搭建持久层

缓存策略对比-1

背景介绍与挑战:从零开始搭建持久层

我们的项目是一个典型的电商后台系统,需要处理订单、商品管理、用户中心等核心模块。其中订单模块涉及复杂的多表关联查询,数据量比较大,性能要求也比较高。初期为了快速开发,先用 Spring Boot 快速搭起基础架子,再引入 MyBatis 做数据访问层。

最初的想法很朴素:数据库操作应该和业务逻辑分离,同时又能灵活控制 SQL 的执行过程。但现实远比想象复杂。

比如:

  • 我该如何优雅地组织 SQL?
  • Mapper 接口如何定义?
  • 结果集映射该怎么搞?
  • 动态 SQL 怎么写?万一写错了怎么办?
  • 多环境配置怎么区分(开发/测试/生产)?

这些问题让我在刚上手的时候吃了不少亏。


解决方案:MyBatis 是如何帮我们破局的?

技术选型对比:为什么不是 Hibernate 或 JPA?

我们团队内部讨论时其实也有同事推荐过 JPA,甚至有更激进的建议上 Spring Data JPA + Hibernate。

但我们最终没选择它们,是因为:

  1. Hibernate 封装得太多,有时候一句简单的查询会生成非常复杂的 SQL,调试起来困难;
  2. 在一些高并发场景下,我们需要更精细地控制数据库行为,比如分页、连接池、缓存机制;
  3. 对于一些老系统的表结构不够规范,JPA 需要做很多额外的适配;
  4. 团队整体对于 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

API接口文档-2

得益于 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝