MyBatis入门:一个架构师的实战总结

奇妙代码
2025-06-25 03:24
阅读 268

作为一名Java后端开发老兵,我接触过不少持久层框架,从早期的Hibernate到如今广泛使用的Spring Data JPA、JOOQ等等。但在实际项目中使用频率最高、灵活性最强的,还得是MyBatis。

这篇文章不打算讲太多理论(网上资料已经够多了),我想以自己亲身经历的一个真实项目为例,和大家分享一下MyBatis是如何在实战中帮助我们解决问题的,以及我在使用过程中踩过的坑和一些经验教训。

背景介绍:为什么选了MyBatis?

背景介绍:为什么选了MyBatis?

去年我们团队接手了一个金融行业客户的订单管理系统重构项目。原来的老系统是用Hibernate写的,逻辑复杂、性能差,尤其是数据量大之后慢得不行。更糟的是,很多业务逻辑都封装在实体类里,导致维护成本越来越高。

我们评估过后,决定采用MyBatis作为新的持久层框架,主要出于以下几点考虑:

  • 灵活控制SQL:金融系统的查询条件复杂多变,需要对SQL有更高的可控性;
  • 轻量级且解耦:不像Hibernate那样侵入性强,POJO不需要继承特定类或加注解;
  • 适合微服务架构:MyBatis可以很好地配合Spring Boot进行快速开发,适配未来的服务化拆分;
  • 已有数据库设计成熟:表结构已稳定运行多年,不想为了ORM再去调整表设计。

于是,我们开启了MyBatis之旅。


遇到的挑战:不是所有的“简单”都真的那么简单

遇到的挑战:不是所有的“简单”都真的那么简单

刚开始的时候,我们都觉得用MyBatis应该很简单——毕竟它只是个半自动的ORM框架嘛。但真正开始写的时候才发现,有些地方比想象中麻烦。

1. SQL管理和复用是个头疼的问题

我们在项目初期把所有SQL都写在XML里,但随着表越来越多、语句越来越复杂,XML文件变得越来越庞大。查找修改都很困难,特别是联合查询、动态SQL这些部分,稍不注意就容易出错。

2. 数据库字段与Java对象映射不够直观

虽然我们可以配置resultMap,但如果字段命名风格不统一(比如下划线 vs 驼峰),或者表关联比较深,很容易出现空值或类型转换问题。

3. 分页处理方式五花八门

刚开始大家都是自己拼LIMIT语句,结果测试发现不同数据库分页语法不一样,MySQL和PostgreSQL写法不一致,后期改起来很痛苦。


解决方案:MyBatis + 插件 + 最佳实践

解决方案:MyBatis + 插件 + 最佳实践

面对这些问题,我们并没有退缩,而是逐步建立了一套基于MyBatis的开发规范,并引入了一些插件来提升效率。

1. 使用MyBatis-Plus简化CRUD操作

我们没有完全自己手写每个增删改查,而是采用了社区活跃、功能丰富的扩展框架——MyBatis-Plus。它提供了很多便捷方法,比如:

userMapper.selectList(new QueryWrapper<User>().eq("age", 18));

这极大地提升了开发效率,尤其是在做基础查询时省去了大量样板代码。

不过我们也意识到不能过度依赖MP的自动SQL生成能力,特别是一些复杂的关联查询还是需要手写SQL。

2. 动态SQL管理:XML+SQL片段抽取

针对SQL复用问题,我们采用了以下策略:

  • 每张表一个XML文件,按模块归类;
  • 抽取公共SQL片段,如常用的WHERE条件、JOIN部分;
  • 使用<include>标签提高可读性和复用性;

例如定义一个常用分页:

<sql id="pagination">
    LIMIT ${pageSize} OFFSET ${(pageNum - 1) * pageSize}
</sql>

然后在具体查询中引用:

<select id="queryUsers" resultType="...">
    SELECT * FROM user
    <where>
        <if test="name != null">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
    </where>
    <include refid="pagination"/>
</select>

这样既保持了灵活性,也方便后期维护。

3. 统一字段映射机制

为了避免字段名混乱带来的问题,我们做了两件事:

  • 所有Java实体类默认使用驼峰命名规则;
  • 使用mapUnderscoreToCamelCase = true自动映射数据库的下划线字段;
  • 复杂映射单独配置resultMap

另外,在项目初始化阶段就明确制定了字段命名规范,并由Code Review强制执行,避免“谁想怎么写都行”。


实战中的代码结构设计

实战中的代码结构设计

我们的项目整体采用Spring Boot + MyBatis的结构,大致如下:

src/
├── main/
│   ├── java/
│   │   └── com.example.project/
│   │       ├── controller/     // 控制器
│   │       ├── service/        // 业务逻辑接口
│   │       ├── mapper/         // Mapper接口
│   │       └── model/          // 数据模型(POJO)
│   │
│   └── resources/
│       ├── mapper/             // XML文件放置在此目录
│       └── application.yml     // 配置文件

一个典型的Mapper接口:

public interface UserMapper {
    List<User> queryUsers(@Param("name") String name, 
                          @Param("pageNum") int pageNum,
                          @Param("pageSize") int pageSize);
}

数据流转过程-1

对应的XML:

<select id="queryUsers" resultType="com.example.project.model.User">
    SELECT *
    FROM user
    <where>
        <if test="name != null">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
    </where>
    LIMIT #{pageSize} OFFSET #{pageNum}
</select>

数据流转过程-2

这里有个小技巧:我们在SQL中用了LIMIT #{pageSize} OFFSET #{pageNum},但实际传入的时候要计算偏移量。这点后来通过封装PageUtil工具类统一处理了。


踩过的坑:你永远不知道下一个SQL会不会崩溃

说说我印象最深的几个“坑”,希望大家少走弯路。

1. 动态SQL拼接错误

一开始有个同事写了这样的逻辑:

<if test="ids != null and ids.size() > 0">
    AND id IN (#{ids})
</if>

结果运行时报错说参数无法绑定。后来才意识到,#{ids}其实只能适用于单个值,而如果是集合要用<foreach>循环输出:

<if test="ids != null and ids.size() > 0">
    AND id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</if>

这个坑让我意识到:不要迷信模板写法,要理解底层是怎么处理参数的。

2. 日志打印SQL太难看

默认的日志打印非常丑,很难看清执行的SQL内容。后来我们换成了log4j2加上mybatis-logger,并配置了打印格式,还集成了Druid监控面板,方便实时查看慢查询和执行计划。

3. 分页插件使用不当

之前我们手动拼LIMIT语句,后来引入了PageHelper插件:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

但要注意使用姿势:

PageInfo<User> pageInfo = PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(() -> {
    return userMapper.queryUsers(name);
});

PageHelper一定要紧跟查询语句,否则会影响后续的SQL!


效果总结:性能和体验双提升

经过几个月的打磨和优化,最终效果还是相当不错的:

指标 旧系统 新系统
单个查询平均响应时间 850ms 270ms
系统吞吐量(TPS) 350 980
代码可维护性 差(Hibernate侵入性强) 好(清晰分离SQL与逻辑)
开发效率 中等

尤其是SQL层面的调优变得容易多了。我们可以通过Druid看哪个语句慢、有没有命中索引、有没有全表扫描等问题,这些都是以前用Hibernate时很难做到的。


我的经验建议:别怕手写SQL,关键是你怎么组织它

如果你也在准备用MyBatis,或者已经在用但总觉得差点意思,那我给你几个建议:

✅ 明确职责边界:SQL归SQL,业务归业务

我们经常能看到有人把SQL直接写在Service层,甚至Controller里面。千万别这么干!MyBatis的核心优势就在于能让我们清晰地分离DAO层逻辑。

✅ 合理使用动态SQL,别滥用

MyBatis的动态SQL非常好用,但也容易写出一堆嵌套判断的复杂逻辑。建议拆分为多个独立的<sql>片段,提高可读性。

✅ 封装通用操作,减少重复代码

对于常见的CRUD操作,不妨封装成BaseMapper或BaseService,减少无意义的重复工作。

✅ 引入日志和监控工具

推荐搭配Druid、Prometheus、SkyWalking这一套生态,既能监控SQL执行情况,也能跟踪整个链路性能,排查问题快很多。

✅ 提前规划字段映射规则

建议尽早统一字段命名风格,并在数据库设计和代码模型之间建立一套清晰的映射关系,避免后续频繁调整。


结语:MyBatis不是银弹,但它是值得信赖的武器

说实话,MyBatis并不是万能的。如果你追求全自动的ORM映射,可能更适合选择Spring Data JPA。但如果你更注重对SQL的掌控力、需要高性能查询、并且愿意为灵活性付出一点学习成本,那MyBatis绝对是一个值得长期投入的技术栈。

在这次项目重构中,我不仅重新认识了MyBatis的强大之处,也更加坚定了“技术要落地、架构要实用”的理念。希望这篇来自真实战场的文章,能对你有所帮助。

如有任何问题欢迎留言交流,我们一起成长!

评论 0

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