MyBatis-Plus 深度指南:从基础到实战,让 DAO 层开发效率起飞
1、MyBatis-Plus 简介:不止是增强,更是重构 2、核心功能:单表操作的 "全能工具箱" 2.1 基础 CRUD:单表操作 "零代码" 实现 2.2 条件构造器:复杂查询 "优雅编码" 2.3 丰富的插件集合:功能扩展的 "万能接口" 3、使用建议:写出高效、安全的 MP 代码 3.1 优先使用 Lambda 条件构造器 3.2 条件构造器 NULL 值处理:减少冗余代码 3.3 尽量明确 select 字段:提升查询效率 4、开发实战:从 "能用" 到 "用好" 的进阶(重点) 4.1 基础能力:微服务下的查询条件封装 4.2 快速接入:3 步实现单表 CRUD 接口 4.3 丰富 BaseMapper:扩展自定义通用方法 4.4 内置插件:增强系统安全性 5、总结:为什么 MyBatis-Plus 值得用?
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) | |||
boolean insertOrUpdate(T entity) | |||
int deleteById(Serializable id) | |||
int deleteByIds(Collection<?> idList) | |||
int updateById(T entity) | |||
T selectById(Serializable id) | |||
List<T> selectList(Wrapper<T> queryWrapper) |
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 | ||
BlockAttackInnerInterceptor | ||
OptimisticLockerInnerInterceptor | ||
TenantLineInnerInterceptor | ||
IllegalSQLInnerInterceptor |
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)。
// 综合查询条件类publicclassQueryCondition{private List<String> selectFieldList; // 返回字段private List<QueryField> queryFieldList; // 查询条件private List<OrderField> orderFieldList; // 排序字段privateint pageNum; // 页码privateint pageSize; // 每页条数}4.1.3 类型安全的构建工具:QueryConditionBuilder
为简化 QueryCondition 的创建,设计 QueryConditionBuilder 工具类,通过 Lambda 表达式实现类型安全构建:
publicclassQueryConditionBuilder<T> {private List<SelectFieldBuilder<T>> selectFieldBuilderList; // 返回字段构建private List<QueryFieldBuilder<T>> queryFieldBuilderList; // 查询条件构建private List<OrderFieldBuilder<T>> orderFieldBuilderList; // 排序字段构建}使用示例:
@TestpublicvoidselectByQuery(){ 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能力
publicinterfaceBaseDao<PextendsSerializable, T> {@MasterT insert(T entity, Option... option); // 新增@SlaveList<T> selectListByQuery(QueryCondition queryCondition, Option... option); // 条件查询// 其他方法...}实现设计:在 dao-service通过抽象类AbstractBaseDaoImpl继承 MyBatis-Plus 的ServiceImpl,并实现BaseDao接口,在抽象类中将自定义的QueryCondition转换成LambdaQueryWrapper再调用ServiceImpl中的方法实现BaseDao中对外暴漏的所有方法:
publicabstract classAbstractBaseDaoImpl<PextendsSerializable, T, MextendsBaseMapper<T>> extendsServiceImpl<M, T> implementsBaseDao<P, T> {@Overridepublic T insert(T entity, Option... option){// 适配自定义配置项this.beforeOption(option);try {if (Objects.nonNull(entity)) {super.save(entity); }return entity; } finally {// 回滚自定义配置项this.afterOption(option); } }@Overridepublic 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)
引入依赖:
<dependency><groupId>com.bj58.zhuanzhuan.kf</groupId><artifactId>dao-contract</artifactId></dependency>定义实体
// 售后单据publicclassAssOrderEntity{private Long id; // 售后单ID// 其他字段...}定义接口:继承 BaseDao,无需编写方法实现
// 售后单表的 CRUD 接口publicinterfaceIAssOrderDaoextendsBaseDao<Long, AssOrderEntity> {}4.2.2 服务层工程(ass-dao-service)
引入依赖
<dependency><groupId>com.bj58.zhuanzhuan.kf</groupId><artifactId>dao-spring-boot-starter</artifactId></dependency>配置数据源:区分主从库,实现读写分离
kf:dao:data-source:master:url:jdbc:mysql://localhost:3306/master_dbdriver-class-name:com.mysql.cj.jdbc.Driverslave:url:jdbc:mysql://localhost:3306/slave_dbdriver-class-name:com.mysql.cj.jdbc.Driver编写实现:继承 AbstractBaseDaoImpl,无需手动实现方法
// 售后单表的 CRUD 接口实现publicclassAssOrderDaoextendsAbstractBaseDaoImpl<Long, AssOrderEntity, AssOrderMapper> implementsIAssOrderDao{}效果:通过上述步骤,无需编写 SQL,即可对外提供 AssOrder 表的 CRUD 接口,支持通过 QueryCondition 进行条件查询、排序、分页等操作。
4.3 丰富 BaseMapper:扩展自定义通用方法
BaseMapper 的默认方法若不满足需求(如按实体属性统计数量),可按以下步骤扩展:
4.3.1 自定义 Mapper 接口
定义 MyMapper 继承 BaseMapper,添加自定义方法:
publicinterfaceMyMapper<T> extendsBaseMapper<T> {// 按实体属性拼接AND条件统计数量intcountByEntity(T entity);}4.3.2 注入方法实现
通过 AbstractMethod 构建 SQL 模板,实现 countByEntity 的逻辑:
publicclassCountByEntityMethodextendsAbstractMethod{private static final String SQL_TEMPLATE = "<script>%s SELECT COUNT(%s) FROM %s %s %s\n</script>";@Overridepublic 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 classMySqlInjectorextendsDefaultSqlInjector{@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo){ List<AbstractMethod> methods = super.getMethodList(mapperClass, tableInfo); methods.add(new CountByEntityMethod()); // 添加自定义方法return methods; }}// 配置注入器,将自定义注入器放入spring环境中@ConfigurationpublicclassMybatisPlusConfig{@Beanpublic MySqlInjector customSqlInjector(){return new MySqlInjector(); }}4.3.4 使用扩展方法
// 定义 UserMapper 继承自定义的 MyMapper@MapperpublicinterfaceUserMapperextendsMyMapper<User> {}// 调用示例@TestpublicvoidtestCountByEntity(){ 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 问题导致的线上故障。
关于作者
孟建国,转转履约中台研发工程师,主要负责售后业务
想了解更多转转公司的业务实践,欢迎点击关注下方公众号: