MyBatis-Plus不是垃圾,是你没把它用好

乐云一
  • 笔记
  • note
About 2100 wordsAbout 7 min

MyBatis-Plus不是垃圾,是你没把它用好

前阵子在某个技术论坛闲逛,看到一个帖子标题大概是"MyBatis-Plus就是一坨屎"。

点进去一看,楼主的抱怨大概集中在这几点:

  • "SQL都封装好了,出了问题都不知道怎么改"
  • "Wrapper套Wrapper,复杂查询写出来跟屎山一样"
  • "Service层一堆方法,找个想要的比找对象还难"
  • "跟JPA比起来就是原始社会"

底下跟着一堆人附和,什么"早就弃了"、"还是JPA优雅"之类的。

我默默关掉了帖子,喝了口咖啡,心想:说得好像JPA你就用明白了一样。

说实在的,作为一个从JPA转到MyBatis-Plus,并且基于它封装了一套自己的DAO层架构的人,我觉得有必要聊聊我对这个框架的理解。

不是为了吹它,而是想说清楚一件事:框架没有垃圾的,只有没用对的。

MyBatis-Plus到底给了我们什么

在开始聊之前,先搞清楚MyBatis-Plus(以下简称MP)到底提供了什么。

说白了就三样东西:

1. BaseMapper

继承它,你就有了17个CRUD方法。selectByIdinsertupdateByIddeleteById...不用写一行SQL。

2. Wrapper系列

LambdaQueryWrapperQueryWrapperUpdateWrapper...用来构建动态查询条件。

3. ServiceImpl

继承它,你又多了一堆批量操作、链式查询的方法。

就这?就这。

很多人抱怨MP不好用,本质上就是只用了这三样东西的皮毛,然后发现复杂场景搞不定,就开始骂框架。

但MP真正强大的地方在于:它是一个很好的基础层,你可以在上面搭建适合自己团队的架构。

Service调DAO的正确姿势

先聊聊我理解的Service→DAO层的调用关系。

很多人用MP的时候,Controller直接调ServiceImpl,ServiceImpl里面直接写Wrapper。这在简单项目里没问题,但项目一复杂,代码就开始腐烂了。

为什么?因为查询逻辑散落在各个Service里,没有统一规范,每个人写出来的查询风格都不一样。

我的做法是,在Service和Mapper之间,再加一层Repository。结构是:

Controller → Service → Repository → Mapper

Repository层负责:

  1. 封装通用的增删改查逻辑
  2. 统一对象转换规则
  3. 统一逻辑删除处理
  4. 提供统一的查询接口

这样一来,Service层只关心业务逻辑,不需要操心查询条件怎么拼、DO怎么转DTO这些事。

我的BaseRepository架构

说干就干,我基于MP封装了一套BaseRepository。开源在这里:leyuna-baseRepositoryopen in new window

核心设计思路

整个架构的泛型定义为 BaseRepository<M extends BaseMapper<DO>, DO>,其中:

  • M:MyBatis-Plus的Mapper接口
  • DO:数据库实体对象

继承关系是:

ServiceImpl<M, DO>            ← MP提供的基类
    └── BaseCommon<M, DO>     ← 我封装的通用方法
        └── BaseRepository<M, DO>  ← 最终的基类

接口层面:

public interface IBaseRepository<DO>
    extends IService<DO>, IOperationService<DO>, IQueryService<DO> {
}

IOperationService管增删改,IQueryService管查询,IQueryPageService管分页。

关键能力

1. 自动对象转换

最常用的场景是什么?前端传过来的DTO对象,要转成DO对象才能入库;查出来的DO对象,要转成VO对象才能返回给前端。

我在BaseCommon里通过反射自动处理这个转换:

protected DO castToDO(Object o) {
    DO d = (DO) do_Class.newInstance();
    if (null != o) {
        BeanUtil.copyProperties(o, d);
    }
    return d;
}

这样调用的时候,你不需要手动做对象拷贝:

// 直接传DTO,框架自动转DO
userRepo.insertOrUpdate(userDTO);

2. 条件对象查询

这是我封装的核心功能。不需要手写Wrapper,直接传一个对象进去,框架自动根据非空字段生成查询条件:

// 用DTO对象查询,非空字段自动作为条件
List<User> users = userRepo.selectByCon(queryDTO);

// 查询并转换成VO
List<UserVO> vos = userRepo.selectByCon(queryDTO, UserVO.class);

// 查单条
UserVO vo = userRepo.selectOne(queryDTO, UserVO.class);

底层原理很简单——通过反射拿到对象的非空字段,设置到MP的LambdaQueryWrapper的entity属性上,MP会自动根据entity的非空字段做等值查询。

当然也支持手动传Wrapper做更精细的控制:

List<UserVO> vos = userRepo.selectByCon(queryDTO, UserVO.class, wrapper -> {
    wrapper.gt(User::getAge, 18)
           .orderByDesc(User::getCreateTime);
});

3. 逻辑删除自动处理

逻辑删除这东西,说大不大说小不小,但是每次查询都要记得加isDeleted = 0的条件,忘一次就是一次线上事故。

我在BaseCommon里加了一个deletedToFalse方法:

protected void deletedToFalse(Object con) {
    Field deletedField = aClass.getDeclaredField("isDeleted");
    Object value = deletedField.get(con);
    if (value == null) {
        deletedField.set(con, 0);  // 默认查未删除的
    }
}

每次查询前自动调用,确保不传isDeleted条件时默认查未删除的数据。

4. BaseRepository2 — MapStruct版本

如果你需要更高级的对象转换(比如字段名不一样、需要做类型转换),我还提供了BaseRepository2

它支持传入一个MapStruct的Converter接口,框架在构造时自动扫描Converter里的方法,建立Map<Class, Method>的转换映射表。查询结果出来后,自动走对应的转换方法。

// 定义Converter
@Mapper
public interface UserConverter {
    UserVO toVO(UserDO user);
    List<UserVO> toVOList(List<UserDO> users);
}

// 继承BaseRepository2,传入Converter接口
public class UserRepository
    extends BaseRepository2<UserMapper, UserDO, UserConverter> {
}

调用时:

// 查出来的DO自动转成VO
List<UserVO> vos = userRepo.selectByCon(queryDTO, UserVO.class);

框架内部会自动找到UserConverter.toVOList方法来执行转换。

使用效果

整套架构用下来的效果就是——Service层非常干净

一个典型的Service方法:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepo;

    // 新增/修改
    public boolean save(UserDTO dto) {
        return userRepo.insertOrUpdate(dto);
    }

    // 条件查询
    public List<UserVO> list(UserQueryDTO query) {
        return userRepo.selectByCon(query, UserVO.class);
    }

    // ID查询
    public UserVO getById(Long id) {
        return userRepo.selectById(id, UserVO.class);
    }

    // 删除
    public boolean delete(Long id) {
        return userRepo.deleteById(id);
    }
}

没有一行Wrapper,没有一行对象拷贝,没有一行SQL。但是底层做的事情一样不少。

回到那个帖子

说了这么多,回到开头那个"MyBatis-Plus就是一坨屎"的帖子。

楼主抱怨的那些问题,真的存在吗?存在。但这些问题本质上是:

  • "SQL都封装好了,出了问题都不知道怎么改" → 你没有理解MP的执行机制,也不知道怎么开SQL日志
  • "Wrapper套Wrapper,复杂查询写出来跟屎山一样" → 你没有在DAO层做封装,把查询逻辑到处乱写
  • "Service层一堆方法,找个想要的比找对象还难" → 你直接用了ServiceImpl,没有做自己的Repository抽象
  • "跟JPA比起来就是原始社会" → 你用JPA也未必能玩明白

觉得MyBatis-Plus垃圾的人,和觉得JPA垃圾的人,本质上是同一类人——没有深入理解框架,没有根据实际场景做架构设计,拿来就用,用不好就骂。

MP给我最大的好处就是可控性。我知道每一条SQL是怎么来的,我知道每一个查询条件是怎么拼的,我知道出了问题该去哪里排查。在这个基础上,我再封装出自己的Repository层,让团队用起来简单、规范、高效。

这不比无脑骂框架强多了?

总结

MyBatis-Plus不是什么银弹,也不是什么垃圾。它就是一个工具,一个提供了基础CRUD能力和动态查询构建的工具。

你能用它做出什么样的架构,完全取决于你对它的理解和你的架构设计能力。

就像我封装的这套BaseRepository,说到底也就是在MP的基础上做了对象转换、条件查询、逻辑删除这三件事的统一封装。但就是这三件事,让整个团队的Service层代码量少了一半,规范统一了,维护也轻松了。

所以与其在网上跟人吵架JPA好还是MP好,不如想想怎么把你选的那个框架用好。

工具在手里,作品好坏看的是人。

Last update:
Contributors: LeYunone
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.14.7