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。
遇到的挑战
大量冗余的DAO操作类
每个实体对应一个DAO类,每个CRUD操作都要重复写JDBC连接、预编译语句、结果集处理等基础代码,代码重复率高。SQL与业务逻辑耦合严重
SQL拼接散落在业务层甚至Controller中,难以调试和维护,特别是动态查询条件处理非常复杂。缺乏事务控制机制
多表更新或插入操作时,无法保证ACID特性,容易造成数据不一致。性能瓶颈明显
没有统一的缓存策略,某些高频接口频繁访问数据库导致响应延迟。
解决方案:为什么选择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分离清晰,修改方便 |
| 性能优化空间 | 几乎没有缓存机制 | 支持二级缓存、懒加载、分页插件等 |
| 团队协作 | 新人难以快速上手 | 标准化结构,新人一周即可独立开发 |

特别是在上线后的一次促销活动中,商品查询接口QPS提升了3倍以上,响应时间稳定在200ms以内,系统表现远超预期。
经验分享:给新手的几点建议
1. 不要一开始就追求高级技巧,先把基本套路跑通
很多文章上来就讲各种ResultMap嵌套、动态SQL、拦截器、插件开发……这些虽然有用,但一开始不要贪多。先把单表查询、新增、更新、带条件查询这几类场景练熟,再去研究复杂关联。
2. XML还是注解?看需求选合适的方式
简单CRUD可以用注解搞定,清爽又直观;但一旦涉及到多表联合查询、动态排序分页、复杂条件拼接,XML就体现出优势了。
我的建议是:小项目用注解,中大型项目用XML + 接口分离结构。
3. 学会利用日志看清SQL执行情况
推荐两个实用配置:
显示MyBatis执行的真实SQL(包含替换参数):
logging: level: com.example.inventory.mapper: debug使用
logback或slf4j绑定日志输出,结合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