-
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)。
// 综合查询条件类
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)
-
引入依赖:
<dependency>
<groupId>com.bj58.zhuanzhuan.kf</groupId>
<artifactId>dao-contract</artifactId>
</dependency>
-
定义实体
// 售后单据
public class AssOrderEntity {
private Long id; // 售后单ID
// 其他字段...
}
-
定义接口:继承 BaseDao,无需编写方法实现
// 售后单表的 CRUD 接口
public interface IAssOrderDao extends BaseDao<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_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
-
编写实现:继承 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 问题导致的线上故障。
如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享
因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享
·END·
相关阅读:
-
一张图看懂微服务架构路线
-
基于Spring Cloud的微服务架构分析 -
微服务等于Spring Cloud?了解微服务架构和框架
-
如何构建基于 DDD 领域驱动的微服务? -
微服务架构实施原理详解
-
微服务的简介和技术栈 -
微服务场景下的数据一致性解决方案 -
设计一个容错的微服务架构
作者:孟建国,转转履约中台研发工程师,主要负责售后业务
来源:转转技术
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
我们都是架构师!

关注架构师(JiaGouX),添加“星标”
获取每天技术干货,一起成为牛逼架构师
技术群请加若飞:1321113940 进架构师群
投稿、合作、版权等邮箱:admin@137x.com

