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

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

引言

MyBatis Plus 作为 MyBatis 的增强工具,以其便捷的 CRUD 操作和丰富的功能深受开发者喜爱。但在实际项目中,我们发现 MyBatis Plus 在架构设计上存在一些根本性的问题,这些问题会随着项目的发展逐渐暴露并累积成技术债。

本文将从架构的角度,深入分析 MyBatis Plus 的三个核心设计缺陷。

一、持久层逻辑泄露:违反分层架构原则

1.1 问题的表现

在传统的三层架构中,持久层逻辑应该封装在 DAO 层。但 MyBatis Plus 的典型用法却将持久层逻辑暴露在了 Service 层:

java 复制代码
@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 对比理想的架构

理想的架构应该是:

java 复制代码
// 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 传统三层架构

传统的三层架构有清晰的边界:

markdown 复制代码
┌─────────────────────────────────────────┐
│  Controller 层                          │
│  职责:HTTP 请求处理                    │
└─────────────────────────────────────────┘
              ↓ 调用
┌─────────────────────────────────────────┐
│  Service 层                             │
│  职责:业务逻辑编排、事务控制           │
└─────────────────────────────────────────┘
              ↓ 调用
┌─────────────────────────────────────────┐
│  DAO 层                                 │
│  职责:数据访问、SQL 执行               │
└─────────────────────────────────────────┘

每一层都有明确的职责边界,上层依赖下层,下层不知道上层的存在。

2.2 MyBatis Plus 破坏了边界

使用 MyBatis Plus 后,实际的架构变成了:

markdown 复制代码
┌─────────────────────────────────────────┐
│  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 的路径:

vbnet 复制代码
现状:
┌────────────────────────────────┐
│ 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 的接管成本

sql 复制代码
现状:
┌────────────────────────────────┐
│ 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 的看法和经验。

相关资源

相关推荐
Moment2 小时前
AI全栈入门指南:使用 NestJs 创建第一个后端项目
前端·javascript·后端
希望永不加班2 小时前
SpringBoot 定时任务:@Scheduled 基础与动态定时
java·spring boot·后端·spring
我叫黑大帅2 小时前
如何设计应用层 ACK 来补充 TCP 的不足?
后端·面试·go
AIUCE2 小时前
我给 AI 装了个"秦始皇":11 层架构解决 AI 黑箱问题
后端
SimonKing2 小时前
每天白送4000万Token!这款“龙虾”AI神器,微信就能操控电脑
java·后端·程序员
ego.iblacat2 小时前
Flask 框架
后端·python·flask
鬼先生_sir2 小时前
SpringCloud-openFeign(服务调用)
后端·spring·spring cloud
IT_陈寒2 小时前
Java线程池用完不关闭?小心内存泄漏找上门
前端·人工智能·后端
小江的记录本2 小时前
【JEECG Boot】 《JEECG Boot 数据字典使用教程》(完整版)
java·前端·数据库·spring boot·后端·spring·mybatis