前言
在 Java 后端开发中,MyBatis 是非常常见的持久层框架。
不过实际开发中,我们经常会写大量重复的单表 CRUD,例如:
insert deleteById updateById selectById selectList这些代码在不同项目中往往都差不多。
MyBatis-Plus,简称 MP,就是在 MyBatis 基础上进行增强的工具。
它的核心理念是:
只做增强,不做改变。
也就是说,简单 CRUD 可以交给 MyBatis-Plus,复杂 SQL 依然可以继续使用 MyBatis XML。
简单理解:
MyBatis-Plus = MyBatis + 通用 CRUD + 条件构造器 + 分页 + 逻辑删除 + 自动填充 + 常用插件下面主要介绍日常开发中最常使用的一些功能。
一、基本使用
在 Spring Boot 项目中引入 MyBatis-Plus 后,假设有一张用户表:
CREATETABLEuser(idBIGINTPRIMARYKEY,usernameVARCHAR(50),ageINT,statusINT,deletedINTDEFAULT0,versionINTDEFAULT1,create_timeDATETIME,update_timeDATETIME);对应实体类:
@Data@TableName("user")publicclassUser{@TableIdprivateLongid;privateStringusername;privateIntegerage;privateIntegerstatus;privateIntegerdeleted;privateIntegerversion;privateLocalDateTimecreateTime;privateLocalDateTimeupdateTime;}常见注解包括:
@TableName 表名映射 @TableId 主键 @TableField 字段映射 @TableLogic 逻辑删除 @Version 乐观锁二、BaseMapper:简化 CRUD
创建 Mapper:
@MapperpublicinterfaceUserMapperextendsBaseMapper<User>{}只需要继承:
BaseMapper<User>就可以直接使用常见 CRUD。
新增
Useruser=newUser();user.setUsername("张三");user.setAge(20);userMapper.insert(user);根据 ID 查询
Useruser=userMapper.selectById(1L);修改
Useruser=newUser();user.setId(1L);user.setUsername("李四");userMapper.updateById(user);删除
userMapper.deleteById(1L);查询全部
List<User>users=userMapper.selectList(null);这也是 MyBatis-Plus 最直观的优势:
很多简单 SQL 不需要重复手写。
三、IService
实际项目一般会按照:
Controller ↓ Service ↓ Mapper ↓ Database这样的结构开发。
Service 可以继承:
publicinterfaceUserServiceextendsIService<User>{}实现类:
@ServicepublicclassUserServiceImplextendsServiceImpl<UserMapper,User>implementsUserService{}之后可以直接使用:
userService.save(user);userService.getById(1L);userService.list();userService.removeById(1L);userService.updateById(user);还有:
saveBatch()saveOrUpdate()page()count()不过复杂业务还是建议定义语义明确的方法,例如:
freezeUser();cancelOrder();refundOrder();而不是所有业务都直接调用通用 CRUD。
四、QueryWrapper 条件查询
MyBatis-Plus 中非常常用的一个功能就是 Wrapper。
例如查询:
年龄大于等于 18,并且状态正常的用户。
可以写:
QueryWrapper<User>wrapper=newQueryWrapper<>();wrapper.ge("age",18).eq("status",1);List<User>list=userMapper.selectList(wrapper);常见条件:
eq = ne != gt > ge >= lt < le <= like LIKE in IN between BETWEEN还可以:
orderByAsc();orderByDesc();进行排序。
五、更推荐 LambdaQueryWrapper
普通 QueryWrapper 有一个问题:
wrapper.eq("username","张三");字段名是字符串。
如果以后字段发生变化,IDE 重构不一定能够发现这里的问题。
因此实际项目中更推荐:
LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUsername,"张三").ge(User::getAge,18).eq(User::getStatus,1);List<User>list=userMapper.selectList(wrapper);相比:
"username"这种写法:
User::getUsername更加安全,也更方便重构。
所以我一般更推荐:
LambdaQueryWrapper LambdaUpdateWrapper六、动态查询
Wrapper 很适合开发搜索接口。
例如前端传:
Stringusername;Integerstatus;Integerage;可以写:
LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.like(StringUtils.hasText(username),User::getUsername,username).eq(status!=null,User::getStatus,status).ge(age!=null,User::getAge,age);当条件不满足时,对应 SQL 条件不会被拼接。
相比大量:
if(...){wrapper.eq(...);}代码会更加简洁。
七、LambdaUpdateWrapper
除了查询,Wrapper 也可以用于更新。
例如:
禁用 id 为 1 的用户。
LambdaUpdateWrapper<User>wrapper=newLambdaUpdateWrapper<>();wrapper.eq(User::getId,1L).set(User::getStatus,0);userMapper.update(null,wrapper);最终 SQL 类似:
UPDATEuserSETstatus=0WHEREid=1;对于只修改少数字段的情况,这种方式比较方便。
八、分页查询
后台管理系统通常都会使用分页。
配置:
@ConfigurationpublicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}}查询:
Page<User>page=newPage<>(1,10);Page<User>result=userMapper.selectPage(page,null);其中:
1 当前页 10 每页条数分页结果可以获取:
result.getRecords();result.getTotal();result.getCurrent();result.getSize();result.getPages();如果带条件:
LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getStatus,1).orderByDesc(User::getCreateTime);Page<User>result=userMapper.selectPage(newPage<>(1,10),wrapper);九、逻辑删除
实际项目中,有些数据不能真正删除。
例如用户、文章、订单等数据可能需要保留历史记录。
这时可以使用逻辑删除。
数据库字段:
deleted 0 = 正常 1 = 已删除实体中:
@TableLogicprivateIntegerdeleted;执行:
userMapper.deleteById(1L);表面上是删除,实际 SQL 类似:
UPDATEuserSETdeleted=1WHEREid=1;之后正常查询时,会自动过滤已经逻辑删除的数据。
需要注意:
逻辑删除和业务状态不是一回事。
例如订单:
待支付 已支付 已取消 已完成应该使用status表示,而不是使用deleted。
十、自动填充
很多数据库表都有:
create_time update_time如果每次手动赋值会比较麻烦。
可以使用 MyBatis-Plus 自动填充。
实体:
@TableField(fill=FieldFill.INSERT)privateLocalDateTimecreateTime;@TableField(fill=FieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;然后实现:
@ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{@OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,"createTime",LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());}@OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,"updateTime",LocalDateTime.class,LocalDateTime.now());}}除了时间,还可以用于:
createUser updateUser等公共字段。
十一、乐观锁
假设两个用户同时修改同一条数据。
用户 A 查询到:
version = 1用户 B 查询时也是:
version = 1如果两个人同时提交修改,可能发生数据覆盖。
MyBatis-Plus 可以使用乐观锁。
实体:
@VersionprivateIntegerversion;配置:
interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());更新时 SQL 思路类似:
UPDATEuserSETusername='新的名字',version=2WHEREid=1ANDversion=1;如果其他线程已经把版本改成:
version = 2那么:
version = 1这个条件就无法匹配,从而发现并发冲突。
十二、MybatisPlusInterceptor
前面的分页、乐观锁,其实都属于 MyBatis-Plus 的插件能力。
核心入口是:
MybatisPlusInterceptor例如:
MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor());interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());除了这些,还可以实现:
多租户 动态表名 数据权限 防止全表更新和删除等功能。
因此 MyBatis-Plus 不只是简单地帮我们少写 CRUD,也提供了很多数据库访问层的公共能力。
十三、BaseMapper 为什么没有实现类?
我们只写:
publicinterfaceUserMapperextendsBaseMapper<User>{}却没有:
UserMapperImpl为什么可以执行?
这里可以简单理解为两个部分。
1. MyBatis 动态代理
MyBatis 会在运行时为 Mapper 接口创建代理对象。
所以:
userMapper.selectById(1L);实际上调用的是 MyBatis 生成的代理对象。
2. MyBatis-Plus SQL Injector
MyBatis-Plus 在启动时会把:
insert deleteById updateById selectById selectList这些通用 CRUD 注册进去。
整体流程可以简单理解为:
UserMapper ↓ BaseMapper ↓ MyBatis-Plus 注册通用 SQL ↓ MyBatis Mapper 动态代理 ↓ Executor ↓ JDBC ↓ 数据库所以 MyBatis-Plus 底层依然建立在 MyBatis 之上。
十四、复杂 SQL 应该怎么办?
使用 MyBatis-Plus 之后,一个常见误区是:
是不是以后都不用写 SQL 了?
答案当然不是。
例如:
SELECT*FROMuserWHEREstatus=1;这种简单查询非常适合 Wrapper。
但是如果涉及:
多表 JOIN GROUP BY 复杂聚合 子查询 统计报表 窗口函数如果强行使用 Wrapper 拼接,可读性可能反而下降。
这时候直接使用:
Mapper + XML通常更加清晰。
所以我个人更推荐:
简单 CRUD ↓ BaseMapper / IService 动态条件 ↓ LambdaQueryWrapper 动态更新 ↓ LambdaUpdateWrapper 复杂 SQL ↓ Mapper XML十五、实际开发中的几个注意点
1. 尽量使用 Lambda Wrapper
少写:
wrapper.eq("username",username);推荐:
wrapper.eq(User::getUsername,username);可以减少字段名写错的问题。
2. 不要直接相信前端传来的 SQL 字段
例如:
wrapper.orderByAsc(sortField);如果sortField完全由前端控制,就存在安全风险。
更合理的方式是做字段白名单:
username createTime id只允许指定字段参与排序。
3. 不要滥用 saveOrUpdate
saveOrUpdate(user);确实方便。
但是很多业务应该明确区分:
新增 修改例如:
createUser();updateUser();业务语义会更加清晰。
4. Wrapper 太复杂时直接写 SQL
如果 Wrapper 已经写成:
.and(...).or(...).nested(...).exists(...)并且自己都很难一眼看懂最终 SQL,那么通常直接写 XML 会更加合适。
工具是为了降低复杂度,而不是增加复杂度。
十六、总结
MyBatis-Plus 本质上是对 MyBatis 的增强。
它比较适合解决:
CRUD 分页 动态查询 逻辑删除 自动填充 乐观锁这些大量重复出现的开发需求。
常用学习路线可以概括为:
BaseMapper ↓ IService ↓ LambdaQueryWrapper ↓ LambdaUpdateWrapper ↓ 分页 ↓ 逻辑删除 ↓ 自动填充 ↓ 乐观锁 ↓ MybatisPlusInterceptor但 MyBatis-Plus 并不是为了完全取代 SQL。
比较合理的使用方式应该是:
简单 CRUD 交给 MyBatis-Plus 复杂 SQL 交给 MyBatis 业务逻辑 放在 Service 数据一致性 依靠数据库设计和事务我认为 MyBatis-Plus 最大的价值不是:
让我们以后不用写 SQL。
而是:
让我们不用再重复写那些没有必要手写的 SQL。
理解这一点之后,MyBatis-Plus 才真正算是用对了。