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