1、MyBatis-Plus 简介:不止是增强,更是重构

MyBatis-Plus 可以理解为「MyBatis + 瑞士军刀皮肤 + 防删库保险栓」—— 它在保留 MyBatis 原生特性的基础上,通过 "零侵入" 设计实现了单表操作的极简开发。

其核心价值体现在三方面:

  • 瑞士军刀般的便捷性:将 XML 配置的 "青铜时代" 升级为 Lambda 表达式的 "赛博坦时代",用极简代码实现复杂操作;

  • 保险栓级的安全性:通过拦截器让 "delete from table" 这类危险操作成为不可能,从源头避免删库风险;

  • 零侵入的兼容性:无需修改现有 MyBatis 代码,老项目可平滑迁移,既保留原生 SQL 灵活性,又获得增强功能。

2、核心功能:单表操作的 "全能工具箱"

MyBatis-Plus 的核心功能是围绕单表的 CRUD 展开的,覆盖从基础操作到高级查询的全场景,所有功能均通过 BaseMapper 接口暴露,无需手动实现。

2.1 基础 CRUD:单表操作 "零代码" 实现

BaseMapper 中封装了单表所有基础操作,无需手写 SQL 就能满足 90% 的业务需求。关键方法整理如下:

操作类型 方法示例 作用 注意事项

新增

int insert(T entity)

插入一条记录,返回影响行数

自动忽略 entity 中 null 值的属性

新增 / 更新

boolean insertOrUpdate(T entity)

若记录存在则更新,否则插入

依赖主键查询(建议查主库避免主从延迟)

删除

int deleteById(Serializable id)

根据主键删除单条记录

物理删除,数据恢复需 DBA 协助

删除

int deleteByIds(Collection<?> idList)

批量删除主键对应的记录

需手动控制 idList 大小,防止数据库负载过高

更新

int updateById(T entity)

根据主键更新记录

主键必须非空,忽略 null 值属性(避免 WHERE id=NULL)

查询

T selectById(Serializable id)

根据主键查询单条记录

-

查询

List<T> selectList(Wrapper<T> queryWrapper)

根据条件批量查询

queryWrapper 为 null 时会全表扫描,需谨慎

2.2 条件构造器:复杂查询 "优雅编码"

条件构造器是 MyBatis-Plus 的 "灵魂",支持用面向对象的方式构建 SQL 条件,避免字符串拼接的坑。核心实现有 4 种:

  • QueryWrapper:基础条件构造器,通过字符串指定字段(如 eq("name", "张三"));

  • LambdaQueryWrapper:基于 Lambda 表达式的构造器(如 eq(User::getName, "张三")),推荐优先使用;

  • UpdateWrapper/LambdaUpdateWrapper:用于构建更新条件,支持动态设置 set 值。

为什么优先用 LambdaQueryWrapper?

对比传统写法的优势一目了然:

// ❌ 不推荐:字段拼写错误编译不报错,字段变更易遗漏
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "张三").gt("age", 18);

// ✅ 推荐:编译期检查字段有效性,重构自动更新引用
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getName, "张三").gt(User::getAge, 18);

优势体现在:

  • 防止字段拼写错误导致的 SQL 异常(编译期校验);

  • 自动校验值类型,避免因类型不匹配导致索引失效;

  • 提高代码可读性,字段含义一目了然。

2.3 丰富的插件集合:功能扩展的 "万能接口"

MyBatis-Plus 通过 MybatisPlusInterceptor 实现插件机制,可拦截 SQL 执行过程并增强功能,核心插件如下:

插件名称 作用 关键说明
PaginationInnerInterceptor

自动分页

必须配置,否则会内存分页(不拼接 limit)

BlockAttackInnerInterceptor

防止全表更新 / 删除

拦截不带 WHERE 条件的 update/delete,避免误操作

OptimisticLockerInnerInterceptor

乐观锁

通过版本号字段解决并发更新冲突

TenantLineInnerInterceptor

多租户

自动为 SQL 添加租户 ID 条件,隔离数据

IllegalSQLInnerInterceptor

SQL 性能规范

拦截不符合规范的 SQL(如 SELECT *)

3、使用建议:写出高效、安全的 MP 代码

3.1 优先使用 Lambda 条件构造器

再强调一次:别用字符串拼条件!LambdaQueryWrapper 能在编译期帮你挡住 "字段不存在"、"类型不匹配" 等一堆坑,重构时还能自动更新引用 —— 相当于给代码加了 "自动纠错"buff。

3.2 条件构造器 NULL 值处理:减少冗余代码

当查询条件含 null 值时,传统写法需用 if 判断避免无效条件,MP 支持 "条件性添加",一行代码搞定:

// ❌ 不推荐:大量if判断
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
    wrapper.eq(User::getName, name);
}
if (age != null) {
    wrapper.eq(User::getAge, age);
}

// ✅ 推荐:条件性添加,减少冗余
wrapper.eq(StringUtils.isNotBlank(name), User::getName, name)
       .eq(Objects.nonNull(age), User::getAge, age);

优势:减少 if 判断、避免无效查询条件。

3.3 尽量明确 select 字段:提升查询效率

默认情况下,selectList 会查询所有字段(select *),指定查询字段可利用索引覆盖、减少数据传输:

// ❌ 不推荐:全表字段查询,浪费资源
List<User> users1 = userMapper.selectList(lambdaQueryWrapper);

// ✅ 推荐:只查询需要的字段
lambdaQueryWrapper.select(User::getId, User::getName, User::getAge);
List<User> users2 = userMapper.selectList(lambdaQueryWrapper);

优势体现在:

  • 利用索引覆盖,避免回表查询,提升 SQL 效率;

  • 减少数据传输和序列化开销,降低数据库压力;

  • 节省内存,尤其对大表查询效果明显。

4、开发实战:从 "能用" 到 "用好" 的进阶(重点)

在实际项目中,尤其是微服务架构下,需要将dao层作为单独的服务对外提供原子能力,此时必须解决 QueryWrapper 在 RPC 场景的痛点,同时实现高效接入、功能扩展及安全保障。

4.1 基础能力:微服务下的查询条件封装

4.1.1 痛点与解决方案

在微服务中,若将 DAO 层封装为独立服务,直接暴露 QueryWrapper 存在 3 大问题:

  • QueryWrapper 结构复杂,序列化 / 反序列化耗时;

  • 逻辑层需引入 mybatis-plus-core 依赖,易导致 Jar 包冲突;

  • 原子层升级 MP 版本时,所有调用方需同步升级,维护成本高。

解决方案:自定义 QueryCondition 替代 QueryWrapper,作为 RPC 入参,兼顾灵活性与轻量性。

4.1.2 QueryCondition 设计

QueryCondition 整合查询、排序、分页及字段选择能力,结构如下:

  • 查询条件集合(queryFieldList):封装 WHERE 子句的条件,包含字段名、匹配规则(如等于、模糊查询)、拼接方式(AND/OR);

  • 排序字段集合(orderFieldList):定义排序字段及排序方式(ASC/DESC);

  • 返回字段集合(selectFieldList):指定查询结果需返回的字段,避免 select *

  • 分页参数:页码(pageNum)和每页条数(pageSize)。

// 综合查询条件类
public class QueryCondition {
    private List<String> selectFieldList; // 返回字段
    private List<QueryField> queryFieldList; // 查询条件
    private List<OrderField> orderFieldList; // 排序字段
    private int pageNum;  // 页码
    private int pageSize; // 每页条数
}
4.1.3 类型安全的构建工具:QueryConditionBuilder

为简化 QueryCondition 的创建,设计 QueryConditionBuilder 工具类,通过 Lambda 表达式实现类型安全构建:

public class QueryConditionBuilder<T> {
    private List<SelectFieldBuilder<T>> selectFieldBuilderList; // 返回字段构建
    private List<QueryFieldBuilder<T>> queryFieldBuilderList; // 查询条件构建
    private List<OrderFieldBuilder<T>> orderFieldBuilderList; // 排序字段构建
}

使用示例

@Test
public void selectByQuery() {
    QueryConditionBuilder<User> builder = QueryConditionBuilder.builder();
    QueryCondition condition = builder
        .select(User::getId, User::getName) // 选择返回字段
        .eq(User::getStatus, 1) // 相等查询
        .like(User::getName, "张") // 模糊查询
        .in(User::getType, Arrays.asList(1, 2, 3)) // 集合查询
        .orderByDesc(User::getCreateTime) // 排序
        .pageNum(1) // 页码
        .pageSize(5) // 每页条数
        .build(); // 构建时校验数据类型,不匹配则抛异常
}
4.1.4 接口与实现设计
  • 接口定义:在 dao-contract 中定义通用接口 BaseDao,对外暴露单表操作的CRUD能力

public interface BaseDao<P extends Serializable, T> {
    @Master
    T insert(T entity, Option... option); // 新增
    @Slave
    List<T> selectListByQuery(QueryCondition queryCondition, Option... option); // 条件查询

    // 其他方法...
}
  • 实现设计:在 dao-service 通过抽象类 AbstractBaseDaoImpl 继承 MyBatis-Plus 的 ServiceImpl,并实现 BaseDao 接口,在抽象类中将自定义的 QueryCondition 转换成 LambdaQueryWrapper 再调用 ServiceImpl 中的方法实现 BaseDao 中对外暴漏的所有方法:

public abstract class AbstractBaseDaoImpl<P extends Serializable, T, M extends BaseMapper<T>> 
        extends ServiceImpl<M, T> implements BaseDao<P, T> {
            
            @Override
            public T insert(T entity, Option... option) {
                // 适配自定义配置项
                this.beforeOption(option);
                try {
                    if (Objects.nonNull(entity)) {
                        super.save(entity);
                    }
                    return entity;
                } finally {
                    // 回滚自定义配置项
                    this.afterOption(option);
                }
            }
               
            @Override
            public List<T> selectListByQuery(QueryCondition queryCondition, Option... option) {
                if (Objects.isNull(queryCondition)) {
                    return new ArrayList<>();
                }
                this.beforeOption(option);
                try {
                    final Wrapper<T> queryWrapper = this.queryCondition2QueryWrapper(queryCondition);
                    return super.list(queryWrapper);
                } finally {
                    this.afterOption(option);
                }
            }

            // ...... 其他方法的实现
}
4.1.5 整体架构

采用分层架构实现高内聚低耦合:

kf_scaffold/
├── dao-contract/    # 接口定义层:暴露对外RPC接口
├── dao-service/     # 服务实现层:实现接口,依赖MyBatis-Plus
├── dao-plugin/      # 插件扩展层:自定义插件(如全表拦截)
├── dao-spring-boot-starter/  # 自动配置层:封装Starter,简化接入
└── dao-demo/        # 使用示例层:提供接入示例

4.2 快速接入:3 步实现单表 CRUD 接口

以售后单表(AssOrder)为例,快速搭建对外暴露的 CRUD 服务:

4.2.1 接口层工程(ass-dao-contract)
  1. 引入依赖

<dependency>
    <groupId>com.bj58.zhuanzhuan.kf</groupId>
    <artifactId>dao-contract</artifactId>
</dependency>
  1. 定义实体

// 售后单据
public class AssOrderEntity {
    private Long id; // 售后单ID
    // 其他字段...
}
  1. 定义接口:继承 BaseDao,无需编写方法实现

// 售后单表的 CRUD 接口
public interface IAssOrderDao extends BaseDao<Long, AssOrderEntity> {
}
4.2.2 服务层工程(ass-dao-service)
  1. 引入依赖

<dependency>
    <groupId>com.bj58.zhuanzhuan.kf</groupId>
    <artifactId>dao-spring-boot-starter</artifactId>
</dependency>
  1. 配置数据源:区分主从库,实现读写分离

kf:
  dao:
    data-source:
      master:
        url: jdbc:mysql://localhost:3306/master_db
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://localhost:3306/slave_db
        driver-class-name: com.mysql.cj.jdbc.Driver
  1. 编写实现:继承 AbstractBaseDaoImpl,无需手动实现方法

// 售后单表的 CRUD 接口实现
public class AssOrderDao extends AbstractBaseDaoImpl<Long, AssOrderEntity, AssOrderMapper> 
        implements IAssOrderDao {
}

效果:通过上述步骤,无需编写 SQL,即可对外提供 AssOrder 表的 CRUD 接口,支持通过 QueryCondition 进行条件查询、排序、分页等操作。

4.3 丰富 BaseMapper:扩展自定义通用方法

BaseMapper 的默认方法若不满足需求(如按实体属性统计数量),可按以下步骤扩展:

4.3.1 自定义 Mapper 接口

定义 MyMapper 继承 BaseMapper,添加自定义方法:

public interface MyMapper<T> extends BaseMapper<T> {
    // 按实体属性拼接AND条件统计数量
    int countByEntity(T entity);
}
4.3.2 注入方法实现

通过 AbstractMethod 构建 SQL 模板,实现 countByEntity 的逻辑:

public class CountByEntityMethod extends AbstractMethod {
    private static final String SQL_TEMPLATE = "<script>%s SELECT COUNT(%s) FROM %s %s %s\n</script>";
    
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql = String.format(SQL_TEMPLATE, 
            sqlFirst(),                // 前置SQL
            selectColumns(tableInfo, true),  // 计数字段
            tableInfo.getTableName(),  // 表名
            sqlWhereEntityWrapper(true, tableInfo),  // WHERE条件(基于实体属性)
            sqlComment());             // 注释
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return addSelectMappedStatementForOther(mapperClass, "countByEntity", sqlSource, Integer.class);
    }
}
4.3.3 注册自定义方法

通过 SqlInjector 将自定义方法注入 MyBatis-Plus:

// 自定义注入器
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        List<AbstractMethod> methods = super.getMethodList(mapperClass, tableInfo);
        methods.add(new CountByEntityMethod()); // 添加自定义方法
        return methods;
    }
}

// 配置注入器,将自定义注入器放入spring环境中
@Configuration
publicclass MybatisPlusConfig {
    @Bean
    public MySqlInjector customSqlInjector() {
        return new MySqlInjector();
    }
}
4.3.4 使用扩展方法
// 定义 UserMapper 继承自定义的 MyMapper
@Mapper
public interface UserMapper extends MyMapper<User> {
}

// 调用示例
@Test
public void testCountByEntity() {
    User user = new User();
    user.setName("张三"); // 按姓名统计
    int count = userMapper.countByEntity(user); 
    System.out.println("符合条件的用户数:" + count);
}

4.4 内置插件:增强系统安全性

为避免生产环境中的误操作,在dao-plugin工程中定义了两个插件:

4.4.1 全表扫描拦截(FullTableScanInterceptor)
  • 功能:拦截无查询条件的 SQL(如 select * from user),防止全表扫描导致的性能问题;

  • 场景:当 QueryCondition 未设置查询条件时,自动拦截并抛异常。

4.4.2 全表更新拦截(BlockFullTableOperationInterceptor)
  • 功能:拦截无更新条件的 SQL(如 update user set status=0),防止全表更新;

  • 价值:避免因条件构造器错误导致的批量数据修改,从源头降低风险。

5、总结:为什么 MyBatis-Plus 值得用?

MyBatis-Plus 的核心价值在于:用最小的改造成本,实现 DAO 层开发效率的质的飞跃

  • 对开发者:减少 90% 的 CRUD 代码,用 Lambda 替代字符串拼接,从 "写 SQL" 转向 "拼条件";

  • 对系统:通过插件机制增强安全性(防删库、SQL 规范)和可扩展性(分页、多租户);

  • 对团队:降低新人上手成本,统一 DAO 层编码规范,减少因 SQL 问题导致的线上故障。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐