从写SQL到优雅持久化:我在项目中与MyBatis的第一次亲密接触

梁红
2025-06-15 15:06
阅读 214

开篇:为什么我会选择讲这个话题?

开篇:为什么我会选择讲这个话题?

2019年,我加入了一个电商中台项目的开发团队。这是我职业生涯中第一次负责数据层设计,项目初期就面临一个现实问题:如何在Java后端优雅、高效地操作数据库。当时我们面临多个ORM框架的选择——Hibernate、JPA、还有相对“轻量级”的MyBatis。

最终我们选择了MyBatis。它不像Hibernate那样“重”,也不像裸写JDBC那样原始,在灵活性和性能之间取得了不错的平衡。这篇文章,就是我想用自己的实战经历告诉你:为什么选MyBatis?怎么用MyBatis写出高质量、可维护的数据访问层?以及我在实际工作中踩过的那些坑。


问题描述:最初的挣扎与困惑

问题描述:最初的挣扎与困惑

我们当时的项目是一个电商平台的商品中心模块,负责商品的基础信息管理、库存更新、上下架状态维护等。虽然功能看似简单,但背后涉及到多张表的联合查询、复杂条件的筛选以及频繁的写操作。

最开始的时候,我尝试用纯JDBC来写DAO层代码。没错,那是一段“不堪回首”的日子:

  • 每次执行完ResultSet都要手动映射成对象;
  • SQL语句嵌在Java代码中难以维护;
  • 参数拼接容易出错,经常因为字符串拼错了导致报错;
  • 连接池处理也得自己写逻辑;
  • 最要命的是,每次加个字段或改个表结构,都要去改一堆代码,测试压力巨大。

这样的方式很快就被产品经理吐槽了:“你们怎么修改一个查询都这么慢?” —— 其实不是我们不快,是写起来太容易出错,不敢随便改啊。


解决方案:引入MyBatis后的第一波提升

解决方案:引入MyBatis后的第一波提升

我们决定试一试MyBatis。一开始我也有些抵触:又得学新框架,还要改现有结构,会不会增加风险?

但用上之后才发现,MyBatis简直是为我们的场景量身定制的。下面我以我们在项目中使用MyBatis的过程为主线,来讲几个关键点。

1. 简单配置,迅速上手

我们的数据库是MySQL,使用Spring Boot作为应用框架。引入MyBatis非常简单:

<!-- pom.xml -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>

然后在application.yml里配一下连接参数:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ecommerce
    username: root
    password: root

不需要任何其他XML配置,Spring Boot会自动扫描Mapper接口并绑定。

2. 映射文件 vs 注解写法

我们一开始就遇到了一个问题:是用XML写SQL好?还是直接用注解写SQL更好?

我们先尝试了注解风格:

@Mapper
public interface ProductMapper {
    @Select("SELECT * FROM product WHERE id = #{id}")
    Product selectById(Long id);

    @Update("UPDATE product SET status = #{status} WHERE id = #{id}")
    void updateStatus(@Param("id") Long id, @Param("status") String status);
}

看起来简洁明了,写起来也方便。但随着SQL越来越复杂,尤其是涉及多表关联、动态SQL时,注解里的SQL字符串开始变得难以维护。

比如这样一段复杂的查询:

SELECT p.id, p.name, p.price, s.stock_num, 
       GROUP_CONCAT(c.category_name) as categories
FROM product p
JOIN stock s ON p.id = s.product_id
LEFT JOIN product_category pc ON p.id = pc.product_id
LEFT JOIN category c ON pc.category_id = c.id
WHERE p.status = 'ON_SALE'
  AND (#{name} IS NULL OR p.name LIKE CONCAT('%', #{name}, '%'))
GROUP BY p.id
ORDER BY p.create_time DESC
LIMIT #{offset}, #{pageSize};

你把它塞到注解里面试试?别说看懂了,写出来都费劲!

于是我们转向了XML方式:

<!-- ProductMapper.xml -->
<select id="searchProducts" resultType="ProductVO">
    SELECT p.id, p.name, p.price, s.stock_num,
           GROUP_CONCAT(c.category_name) as categories
    FROM product p
    JOIN stock s ON p.id = s.product_id
    LEFT JOIN product_category pc ON p.id = pc.product_id
    LEFT JOIN category c ON pc.category_id = c.id
    WHERE p.status = 'ON_SALE'
      <if test="name != null and name.length() > 0">
          AND p.name LIKE CONCAT('%', #{name}, '%')
      </if>
    GROUP BY p.id
    ORDER BY p.create_time DESC
    LIMIT #{offset}, #{pageSize}
</select>

XML方式明显更清晰,而且支持很多高级语法标签,比如 <if><choose><foreach> 等,非常适合做动态SQL。

当然如果你喜欢极简主义,也可以两者结合使用——简单的CRUD用注解,复杂查询用XML。

3. 结果映射与别名优化

刚开始我们也经常遇到“找不到字段映射”的问题。比如上面的例子中有个 categories 字段,但在Java类中是 String[] 类型。

后来我们学会了定义自己的 resultMap

<resultMap id="productDetailResultMap" type="ProductVO">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="price" column="price"/>
    <collection property="categories" ofType="String">
        <discriminator javaType="string" column="categories">
            <case value="" resultType="void"/>
        </discriminator>
    </collection>
</resultMap>

这样就能把逗号分隔的字符串解析成数组。当然你也可以用自定义类型处理器(TypeHandler)来实现更灵活的映射。

不过为了避免麻烦,我们在数据库建模时也开始有意识地让字段命名与Java属性保持一致,减少不必要的映射冲突。


遇到的挑战与进阶实践

挑战一:动态SQL的性能问题

有一次,我们上线了一个搜索接口,发现查询速度特别慢。查看日志发现,是因为在 <if> 条件判断中误用了 null 判断导致索引失效。

例如这段:

<if test="name == null or name == ''">
    AND 1=1
</if>

这种写法其实在某些情况下会让数据库全表扫描!后来我们调整成了:

<if test="name != null and name.length() > 0">
    AND p.name LIKE CONCAT('%', #{name}, '%')
</if>

另外还建议:

  • 在动态SQL中尽量避免使用 OR,尤其不要让 OR 包含空值。
  • 使用 trim, where, set 标签来规范生成的SQL结构,避免拼接错误。

挑战二:批量插入性能差

我们之前有一个需求是要导入供应商商品列表,一次可能有几万条记录。如果每条都用单条INSERT,速度简直不能忍。

MyBatis 提供了批量插入的支持:

<insert id="batchInsertProducts">
    INSERT INTO product (name, price, status)
    VALUES
    <foreach collection="products" item="p" separator=",">
        (#{p.name}, #{p.price}, #{p.status})
    </foreach>
</insert>

同时需要配合 JDBC 的批处理配置:

jdbc:mysql://localhost:3306/ecommerce?rewriteBatchedStatements=true

开启后批量插入效率提升了几十倍,从此再也不怕“一次性上万条”。


架构设计上的思考

作为一个参与过多个项目的老兵,我觉得MyBatis不仅仅是数据访问层的工具,更是系统架构中非常重要的一环

分层设计与接口抽象

我们在项目中一直坚持一个原则:DAO层只暴露接口,具体实现由MyBatis自动完成。好处是后期可以轻松替换底层实现,甚至可以对接其他数据源(如Redis、ES等)。

例如我们定义的接口:

public interface ProductRepository {
    List<ProductVO> searchProducts(String name, int offset, int pageSize);
    ProductVO getProductDetail(Long id);
    boolean updateStatus(Long id, String status);
    int batchInsert(List<Product> products);
}

系统架构设计图-1

MyBatis的Mapper实现这些接口,业务层无需关心细节,只需要调用即可。

数据库设计的影响

MyBatis不会替你做数据库优化。我们在设计数据库时就意识到:

  • 表结构应该合理规范化,但也适当反范式以提高查询效率;
  • 对于高频查询字段一定要加索引;
  • 查询逻辑要尽可能简化,避免过于复杂的JOIN;
  • 使用合适的分页策略,避免OFFSET带来的性能问题(可以用游标分页代替)

性能监控与运维经验

在生产环境中,我们通过以下几个手段来保障数据层稳定性:

  1. SQL慢查询日志 + APM工具

    • MySQL开启慢查询日志,每天定时分析;
    • 使用SkyWalking或Pinpoint监控接口耗时,定位SQL瓶颈;
  2. MyBatis拦截器打日志

    • 写一个Interceptor,在请求前后打印SQL和耗时;
    • 方便定位性能问题,也能用来做审计日志;
  3. 读写分离 + 分库分表预埋

    • 当前还未分库,但我们已经在Service层做了数据源路由的封装;
    • 所有涉及主库写的操作走master,读操作默认走slave;
    • 后期如果分库,只需要改一下路由逻辑即可;

效果总结:MyBatis带来了什么改变?

在项目上线一年后回过头来看,MyBatis带来的收益远超预期:

改变 描述
代码整洁度提升 DAO层不再混杂大量SQL,代码结构更清晰,Review更容易
开发效率加快 特别是对动态SQL的支持,节省了大量拼接字符串的时间
排查问题更快 SQL统一管理在Mapper文件中,便于查找、调试
性能可控性强 不像Hibernate那样隐藏太多细节,MyBatis让我们更贴近数据库,更容易做优化

更重要的是,整个团队形成了一个良好的编码习惯:SQL归SQL,Java归Java,各司其职。


经验分享:给正在学习MyBatis的你几点建议

✅ 建议1:不要死磕MyBatis本身,而是理解它的设计哲学

MyBatis 不是 ORM 框架,它只是一个SQL Mapper。你可以把它当作一个“高级版的JDBC”,而不是“全自动化的ORM”。它最大的优势就在于灵活控制SQL的能力。

所以:

  • 如果你需要极致的灵活性,用MyBatis;
  • 如果你希望全自动管理对象关系,用Hibernate/JPA更好;
  • MyBatis适合对SQL有一定了解,并且愿意花时间优化的人。

✅ 建议2:善用MyBatis插件生态

有很多优秀的MyBatis插件可以让你事半功倍,比如:

  • PageHelper:一键分页神器,一行代码搞定分页;
  • 通用Mapper:提供常用的CRUD操作,省去重复代码;
  • MyBatis Generator(MBG):快速生成POJO+Mapper+XML文件,适合初建项目时使用;
  • MyBatis Plus:如果你想少写点原生SQL,可以考虑它,但要注意它更适合标准场景。

✅ 建议3:掌握动态SQL的基本套路

学会使用如下标签:

  • <if>
  • <choose>/<when>/<otherwise>
  • <where>:帮你自动去除多余的 AND/OR
  • <set>:避免最后一个逗号的问题
  • <foreach>:用于 IN 查询或者批量插入

掌握了这几个标签,几乎能应对80%的动态SQL需求。

✅ 建议4:写好SQL比写好Java更重要

很多人以为MyBatis只是Java层面的技术,其实不然。写不好SQL,用再厉害的框架也没用。

我建议你养成以下习惯:

  • 经常用EXPLAIN查看SQL执行计划;
  • 学习MySQL索引的优化技巧;
  • 了解数据库事务隔离级别和锁机制;
  • 多练习复杂查询,比如窗口函数、临时表的使用。

结语:MyBatis不是终点,而是通往高阶之路的起点

MyBatis只是我们走向高性能、高可用系统的其中一个工具。真正的高手,从来不是靠“会哪个框架”决定的,而是懂得什么时候该用什么技术,怎么组合才能达到效果。

在我参与的后续项目中,MyBatis依然是数据层的基础,我们也在不断探索如何让它更好地服务于分布式架构和微服务体系。比如结合ShardingSphere做分库分表、整合ElasticSearch做联合查询、基于RabbitMQ做异步持久化等等。

如果你刚刚接触MyBatis,不妨从一个小项目开始尝试。相信我,当你真正熟练掌握之后,你会发现它并不“笨”,反而非常聪明地帮助你在灵活和高效之间找到了一个绝佳的平衡点。

一起加油吧,码农兄弟们 🚀

评论 0

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