MyBatis实战入门:一次真实项目中的持久层重构之旅

半个架构师
2025-06-18 21:42
阅读 233

开篇:为什么选择MyBatis?

开篇:为什么选择MyBatis?

在做后端开发的这些年里,我经历过JDBC手动撸SQL的时代,也尝试过Hibernate、Spring Data JPA等ORM框架。但真正让我觉得“终于找到组织了”的,是MyBatis。

去年我参与了一个中型项目的重构工作,原系统使用的是原生JDBC+DAO模板模式写的数据库操作逻辑。代码量大、维护困难、SQL和Java混杂、没有统一的事务管理,简直是“技术债务”的典范。

面对这些问题,我们决定引入MyBatis来重构整个数据访问层。从0到1搭建的过程中踩了不少坑,也积累了一些宝贵的经验。今天我就以一个实际项目的视角,带大家走一遍MyBatis的基础实践之路。


问题描述:老系统暴露的数据库操作痛点

问题描述:老系统暴露的数据库操作痛点

项目背景

这是一个面向中小企业的在线库存管理系统,用户主要功能包括:

  • 商品信息管理
  • 库存状态跟踪
  • 入库/出库记录查询
  • 报表导出等

项目原有架构采用Maven模块化构建,Spring Boot 2.3 + MySQL 5.7,前端用的Vue。

遇到的挑战

  1. 大量冗余的DAO操作类
    每个实体对应一个DAO类,每个CRUD操作都要重复写JDBC连接、预编译语句、结果集处理等基础代码,代码重复率高。

  2. SQL与业务逻辑耦合严重
    SQL拼接散落在业务层甚至Controller中,难以调试和维护,特别是动态查询条件处理非常复杂。

  3. 缺乏事务控制机制
    多表更新或插入操作时,无法保证ACID特性,容易造成数据不一致。

  4. 性能瓶颈明显
    没有统一的缓存策略,某些高频接口频繁访问数据库导致响应延迟。


解决方案:为什么选择MyBatis?

经过团队内部技术选型讨论,最终选择了MyBatis作为持久层解决方案,主要有以下几个原因:

  • 灵活可控:不像JPA那样封装太深,可以自由掌控SQL。
  • 学习成本低:相比Hibernate来说更易上手,对已有的DAO结构改造更平滑。
  • 性能优越:支持懒加载、关联映射、结果缓存等多种优化手段。
  • 社区活跃:生态丰富,文档完备,问题排查相对容易。

代码实践:从零开始搭建MyBatis核心流程

为了让读者更有代入感,我以商品库存管理这个具体功能为例,演示一下如何使用MyBatis进行数据库操作。

第一步:添加依赖(pom.xml)

<!-- Spring Boot Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

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

<!-- 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

第二步:配置application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/inventory?useSSL=false&serverTimezone=UTC
    username: root
    password: yourpassword
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.inventory.model

第三步:创建实体类

public class Product {
    private Long id;
    private String name;
    private String sku; // SKU编号
    private BigDecimal price;
    private Integer stock; // 库存数量
    // getter/setter...
}

第四步:编写Mapper接口

@Mapper
public interface ProductMapper {
    
    @Select("SELECT * FROM product WHERE id = #{id}")
    Product selectById(Long id);
    
    List<Product> selectByCondition(@Param("name") String name, @Param("lowStock") Integer lowStock);

    @Insert("INSERT INTO product(name, sku, price, stock) VALUES(#{name}, #{sku}, #{price}, #{stock})")
    void insert(Product product);

    @Update("UPDATE product SET stock = #{stock} WHERE id = #{id}")
    void updateStock(Product product);
}

第五步:XML实现复杂查询

比如我们要实现一个模糊搜索+库存预警的动态查询:

<!-- src/main/resources/mapper/ProductMapper.xml -->
<select id="selectByCondition" resultType="Product">
    SELECT * FROM product
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="lowStock != null">
            AND stock <= #{lowStock}
        </if>
    </where>
</select>

第六步:事务控制

如果我们要完成一个复杂的入库流程,涉及多个表的操作:

@Service
public class InventoryService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private InboundRecordMapper inboundRecordMapper;

    @Transactional
    public void addInboundRecordAndIncreaseStock(InboundRecord record) {
        inboundRecordMapper.insert(record);
        productMapper.increaseStock(record.getProductId(), record.getAmount());
    }
}

踩坑经验:那些年我们一起踩过的坑

坑一:XML文件找不到?路径别搞错了!

刚开始集成时,经常报Invalid bound statement not found错误,其实是mapper.xml的位置不对或者未扫描。

解决办法

  • 确保mybatis.mapper-locations指向正确的资源目录
  • Maven项目注意src/main/java下的文件不会被默认打包进resources,最好把XML放在resources下对应的包结构中

例如:

src/
└── main/
    ├── java/
    │   └── com/example/inventory/mapper/
    │       └── ProductMapper.java
    └── resources/
        └── mapper/
            └── ProductMapper.xml

坑二:传参方式混乱,尤其是动态SQL中参数名写错

早期我们在动态SQL中直接写参数名,如${name},结果遇到一些注入问题和拼接错误。

建议做法

  • 所有SQL传参使用#{param}占位符
  • 复杂参数可封装成Map或自定义对象
  • 使用@Param注解明确参数名称,避免歧义

坑三:一对一、一对多映射配置不熟,查出来总是null

刚开始在处理产品详情页面需要展示库存流水记录时,用了join查询返回多个记录,结果只取到了第一条。

后来才知道需要用 <collection> 来处理嵌套集合关系:

<resultMap id="productWithRecords" type="Product">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- 关联出库记录 -->
    <collection property="records" ofType="InventoryRecord">
        <id column="record_id" property="id"/>
        <result column="type" property="type"/>
        <result column="quantity" property="quantity"/>
    </collection>
</resultMap>

这样就能正确映射子列表了。

坑四:事务失效?没加@Transactional或传播行为配置错了

有个批量导入接口,原本以为加了@Transactional就万事大吉,结果插入部分失败后前几条居然提交成功了。

排查过程

  • 方法必须是public,否则Spring事务管理不生效
  • 同一个类内部方法调用会跳过AOP代理
  • 默认的rollbackFor只识别RuntimeException,需要显式声明受检异常也要回滚

正确写法示例:

@Transactional(rollbackFor = Exception.class)
public void batchImport(List<Product> products) throws IOException {
    for (Product p : products) {
        productMapper.insert(p);
    }
}

效果总结:重构后的收益

引入MyBatis之后,我们的持久层发生了以下变化:

方面 改进前 改进后
代码量 平均每个实体需写4~5个DAO方法 Mapper接口极简,大部分通过XML集中管理
查询灵活性 SQL硬编码,难于调整 XML配置清晰,修改SQL不用动Java代码
可维护性 修改字段就得全改一遍 实体+Mapper分离清晰,修改方便
性能优化空间 几乎没有缓存机制 支持二级缓存、懒加载、分页插件等
团队协作 新人难以快速上手 标准化结构,新人一周即可独立开发

服务器部署方案-1

特别是在上线后的一次促销活动中,商品查询接口QPS提升了3倍以上,响应时间稳定在200ms以内,系统表现远超预期。


经验分享:给新手的几点建议

1. 不要一开始就追求高级技巧,先把基本套路跑通

很多文章上来就讲各种ResultMap嵌套、动态SQL、拦截器、插件开发……这些虽然有用,但一开始不要贪多。先把单表查询、新增、更新、带条件查询这几类场景练熟,再去研究复杂关联。

2. XML还是注解?看需求选合适的方式

简单CRUD可以用注解搞定,清爽又直观;但一旦涉及到多表联合查询、动态排序分页、复杂条件拼接,XML就体现出优势了。

我的建议是:小项目用注解,中大型项目用XML + 接口分离结构

3. 学会利用日志看清SQL执行情况

推荐两个实用配置:

  • 显示MyBatis执行的真实SQL(包含替换参数):

    logging:
      level:
        com.example.inventory.mapper: debug
    
  • 使用logbackslf4j绑定日志输出,结合druid监控数据库性能。

4. 提前规划好Mapper命名和SQL结构

在项目初期,就要约定好Mapper接口命名规则、SQL ID命名风格,保持一致性,后期扩展起来才不会乱。

比如:

  • Mapper接口名统一为 XxxMapper
  • XML中SQL ID尽量见名知意,如 selectByName, deleteByStatus, updateStockById

5. 结合Spring Boot使用更爽

Spring Boot提供了非常便利的整合方式,特别是@MapperScan可以自动扫描Mapper接口,避免每个都去手动注册。

另外,结合Spring AOP来做事务管理和日志审计也非常自然。


结语:MyBatis依然是后端开发者的利器

在我这几年的实际项目中,无论是中小型微服务,还是传统企业应用,MyBatis都展现出了极强的适应性和稳定性。它不是最炫酷的框架,但却是最可靠的老伙计。

希望这篇来自实战的文章,能帮助刚接触MyBatis的同学少走弯路,也能让正在犹豫是否重构持久层的开发者们看到一些方向和可能性。

如果你也在用MyBatis,欢迎留言交流你在实际项目中踩过的坑和心得 😊

评论 0

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