从架构视角看 MyBatis Plus 的设计缺陷
引言
MyBatis Plus 作为 MyBatis 的增强工具,以其便捷的 CRUD 操作和丰富的功能深受开发者喜爱。但在实际项目中,我们发现 MyBatis Plus 在架构设计上存在一些根本性的问题,这些问题会随着项目的发展逐渐暴露并累积成技术债。
本文将从架构的角度,深入分析 MyBatis Plus 的三个核心设计缺陷。
一、持久层逻辑泄露:违反分层架构原则
1.1 问题的表现
在传统的三层架构中,持久层逻辑应该封装在 DAO 层。但 MyBatis Plus 的典型用法却将持久层逻辑暴露在了 Service 层:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public List<Order> searchOrders(OrderQueryDTO query) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
if (query.getStatus() != null) {
wrapper.eq(Order::getStatus, query.getStatus());
}
if (query.getStartTime() != null) {
wrapper.ge(Order::getCreateTime, query.getStartTime());
}
if (query.getMinAmount() != null) {
wrapper.ge(Order::getTotalAmount, query.getMinAmount());
}
// ...(省略 30+ 行类似代码)
wrapper.orderByDesc(Order::getCreateTime);
return orderMapper.selectList(wrapper);
}
}
1.2 架构视角的分析
从架构的角度看,这段代码违反了几个基本原则:
1. 单一职责原则(SRP)
Service 层同时承担了两个职责:
- 业务逻辑编排
- 查询条件构建(持久层关注点)
2. 依赖倒置原则(DIP)
Service 层直接依赖了:
- 数据库表结构(通过
Order::getStatus等字段引用) - MyBatis Plus 的 API(
LambdaQueryWrapper)
理想情况下,Service 应该依赖抽象(DAO 接口),而不是具体实现细节。
3. 开闭原则(OCP)
当需要修改查询逻辑时(比如添加新的筛选条件),必须修改 Service 层代码。这违反了"对修改封闭"的原则。
1.3 对比理想的架构
理想的架构应该是:
// Service 层:纯业务逻辑
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
public List<Order> searchOrders(OrderQuery query) {
// 业务逻辑...
return orderDao.findList(query);
}
}
// DAO 层:封装数据访问
public interface OrderDao extends BaseDao<Order> {
List<Order> findList(OrderQuery query);
}
这样的架构有几个优点:
- 关注点分离:Service 只关心业务,DAO 只关心数据访问
- 易于测试:Service 可以独立测试,mock DAO 即可
- 易于维护:查询逻辑的修改只影响 DAO 层
二、架构边界被破坏:层次混乱
2.1 传统三层架构
传统的三层架构有清晰的边界:
┌─────────────────────────────────────────┐
│ Controller 层 │
│ 职责:HTTP 请求处理 │
└─────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────┐
│ Service 层 │
│ 职责:业务逻辑编排、事务控制 │
└─────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────┐
│ DAO 层 │
│ 职责:数据访问、SQL 执行 │
└─────────────────────────────────────────┘
每一层都有明确的职责边界,上层依赖下层,下层不知道上层的存在。
2.2 MyBatis Plus 破坏了边界
使用 MyBatis Plus 后,实际的架构变成了:
┌─────────────────────────────────────────┐
│ Service 层 │
│ ├─ 业务逻辑编排 │
│ ├─ 事务控制 │
│ ├─ Wrapper 构建 ← 持久层关注点! │
│ ├─ 字段名引用 ← 持久层关注点! │
│ └─ 条件拼装 ← 持久层关注点! │
└─────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────┐
│ Mapper 接口 │
│ 职责:提供基础 CRUD 方法 │
│ (几乎沦为工具类) │
└─────────────────────────────────────────┘
持久层的逻辑泄露到了 Service 层,DAO 层几乎沦为摆设。
2.3 架构腐化的连锁反应
边界的破坏会导致一系列问题:
1. 代码复用困难
类似的 Wrapper 构建逻辑散落在各个 Service 方法中,无法复用。
2. 测试变得复杂
Service 层的单元测试需要:
- Mock Mapper
- 验证 Wrapper 的正确性
- 实际上变成了集成测试
3. 重构风险高
查询逻辑的修改需要改 Service 代码,影响面大,风险高。
4. 新人理解困难
新人需要同时理解业务逻辑和查询逻辑,学习曲线陡峭。
三、接管成本高:演进困难
3.1 性能优化的困境
在实际项目中,随着数据量的增长和业务的复杂化,经常需要优化 SQL:
- 添加 JOIN 查询关联数据
- 使用子查询优化性能
- 添加索引提示
- 使用数据库特有的优化语法
这时候,MyBatis Plus 的 Wrapper API 就不够用了,必须手写 SQL。
3.2 接管路径的复杂性
从 Wrapper 迁移到手写 SQL 的路径:
现状:
┌────────────────────────────────┐
│ Service 层 │
│ ├─ 50 行 Wrapper 构建代码 │
│ └─ orderMapper.selectList() │
└────────────────────────────────┘
需要优化:添加 JOIN 查询
↓
Step 1: 在 Mapper.xml 写 SQL
<select id="selectOrdersWithDetails">
SELECT o.*, u.name, s.name
FROM order o
LEFT JOIN user u ON o.user_id = u.id
LEFT JOIN shop s ON o.shop_id = s.id
WHERE ...
</select>
Step 2: 修改 Service 层代码(!)
public List<Order> searchOrders(OrderQueryDTO query) {
// 删除 50 行 Wrapper 代码
return orderMapper.selectOrdersWithDetails(query);
}
Step 3: 如果这个方法被多处调用
需要检查所有调用方
进行回归测试
问题:
- Service 层代码需要修改
- 如果有多个类似的查询,都要改
- 改动风险大,测试成本高
3.3 对比其他方案
手写 MyBatis 的接管成本:
现状:
┌────────────────────────────────┐
│ Service 层 │
│ └─ orderDao.findList(query) │
└────────────────────────────────┘
↓
┌────────────────────────────────┐
│ DAO 层 │
│ └─ Mapper.xml: 简单 SQL │
└────────────────────────────────┘
需要优化:
↓
Step 1: 修改 Mapper.xml 的 SQL(添加 JOIN)
<select id="findList">
SELECT o.*, u.name, s.name
FROM order o
LEFT JOIN user u ON o.user_id = u.id
WHERE ...
</select>
Step 2: 完成!
Service 层代码不需要改
调用方不需要改
接管成本:零!
这就是为什么从长期看,手写 MyBatis 更容易维护。
四、从设计模式角度的反思
4.1 MyBatis Plus 违反了哪些原则?
| 原则 | 违反情况 | 后果 |
|---|---|---|
| 单一职责原则(SRP) | Service 混杂业务和查询逻辑 | 职责不清,难以维护 |
| 开闭原则(OCP) | 修改查询逻辑要改 Service | 修改风险高 |
| 依赖倒置原则(DIP) | Service 直接依赖表结构 | 耦合度高,重构困难 |
| 接口隔离原则(ISP) | Mapper 接口过于泛化 | DAO 层职责不明确 |
4.2 为什么会这样设计?
MyBatis Plus 的设计初衷是:降低简单 CRUD 的开发成本。
这个目标本身没错,但实现方式有问题:
- 为了便利性,牺牲了架构清晰性
- 为了快速开发,破坏了分层边界
- 为了减少配置,引入了运行时生成
这是典型的"过早优化"陷阱。
五、总结与思考
5.1 MyBatis Plus 的本质问题
从架构视角看,MyBatis Plus 的核心问题是:
为了提升开发效率,将持久层逻辑暴露在了业务层,破坏了分层架构的边界。
这带来了三个后果:
- 持久层逻辑泄露:Service 层混杂业务和数据访问
- 架构边界破坏:职责不清,代码难以维护
- 接管成本高:性能优化困难,重构风险大
5.2 适用场景
MyBatis Plus 并非一无是处,它适合:
- 短期项目(< 6 个月)
- 简单的 CRUD 应用
- 对架构质量要求不高的项目
但对于需要长期维护的项目,需要谨慎评估。
5.3 下一篇预告
你可能会想:"那 JPA 呢?它也提供了便利性,是不是更好?"
JPA 确实在某些方面做得更好,但它有自己的 4 个致命问题。
下一篇:《JPA 的 4 个致命问题》
系列文章
- 本文:从架构视角看 MyBatis Plus 的设计缺陷
- 下篇:JPA 的 4 个致命问题
- 第三篇:MyBatis 为什么更适合长期项目
- 第四篇:ORM 本质问题与技术选型哲学
- 第五篇:MyBatisGX 的设计哲学与实践
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!
欢迎在评论区分享你对 MyBatis Plus 的看法和经验。
相关资源:
- MyBatisGX GitHub: github.com/cris-xue/my…
- 在线文档: www.mybatisgx.com