从架构视角看 MyBatis Plus 的设计缺陷

24 阅读6分钟

从架构视角看 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 的核心问题是:

为了提升开发效率,将持久层逻辑暴露在了业务层,破坏了分层架构的边界。

这带来了三个后果:

  1. 持久层逻辑泄露:Service 层混杂业务和数据访问
  2. 架构边界破坏:职责不清,代码难以维护
  3. 接管成本高:性能优化困难,重构风险大

5.2 适用场景

MyBatis Plus 并非一无是处,它适合:

  • 短期项目(< 6 个月)
  • 简单的 CRUD 应用
  • 对架构质量要求不高的项目

但对于需要长期维护的项目,需要谨慎评估。

5.3 下一篇预告

你可能会想:"那 JPA 呢?它也提供了便利性,是不是更好?"

JPA 确实在某些方面做得更好,但它有自己的 4 个致命问题。

下一篇:《JPA 的 4 个致命问题》


系列文章

  • 本文:从架构视角看 MyBatis Plus 的设计缺陷
  • 下篇:JPA 的 4 个致命问题
  • 第三篇:MyBatis 为什么更适合长期项目
  • 第四篇:ORM 本质问题与技术选型哲学
  • 第五篇:MyBatisGX 的设计哲学与实践

如果这篇文章对你有帮助,欢迎点赞、收藏、关注

欢迎在评论区分享你对 MyBatis Plus 的看法和经验。

相关资源