MyBatis-Plus 是 MyBatis 的增强工具,在国内企业级项目中几乎是标配。上一篇讲完了基础 CRUD,这一篇把高频高级用法一次性说清楚。
一、分页查询
MP 的分页插件配置很简单,但配置错了会不生效。
1. 配置分页插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器(设置数据库类型)
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL)
);
return interceptor;
}
}
没有这个配置,分页不会生效,Page 对象会查出所有数据。
2. 使用 Page 对象
// 查第一页,每页 10 条
Page<User> page = new Page<>(1, 10);
// 条件查询 + 分页
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "张");
wrapper.orderByDesc(User::getCreateTime);
// 执行分页查询
Page<User> result = userMapper.selectPage(page, wrapper);
// 从 result 中获取分页信息
System.out.println("总记录数: " + result.getTotal());
System.out.println("总页数: " + result.getPages());
System.out.println("当前页: " + result.getCurrent());
System.out.println("每页大小: " + result.getSize());
System.out.println("是否有下一页: " + result.hasNext());
System.out.println("数据列表: " + result.getRecords());
3. Service 层的分页
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
public Page<User> pageUsers(int pageNum, int pageSize, String keyword) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(keyword), User::getName, keyword);
wrapper.eq(User::getStatus, 1);
wrapper.orderByDesc(User::getCreateTime);
return this.page(page, wrapper);
// 或者 baseMapper.selectPage(page, wrapper)
}
}
4. 分页返回 DTO(自定义结果集)
// 实体类是 User,但只需要部分字段
Page<UserVO> page = new Page<>(1, 10);
Page<UserVO> result = userMapper.selectUserPage(page, "张");
// Mapper.xml
// <select id="selectUserPage" resultType="com.zhang.vo.UserVO">
// SELECT id, username, email, phone FROM user WHERE username LIKE CONCAT('%', #{keyword}, '%')
// </select>
Page 的泛型可以和实体类不一致,泛型仅决定返回的 records 类型。
二、Lambda 条件构造器
MP 最强大的功能之一,SQL 条件不用手写。
常用条件方法
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1); // 等于
wrapper.ne(User::getStatus, 0); // 不等于
wrapper.gt(User::getAge, 18); // 大于
wrapper.ge(User::getAge, 18); // 大于等于
wrapper.lt(User::getAge, 60); // 小于
wrapper.le(User::getAge, 60); // 小于等于
wrapper.like(User::getName, "张"); // 模糊匹配 %张%
wrapper.notLike(User::getName, "测试"); // 不包含
wrapper.likeLeft(User::getName, "张"); // 左模糊 %张
wrapper.likeRight(User::getName, "张"); // 右模糊 张%
wrapper.in(User::getStatus, 1, 2, 3); // IN 查询
wrapper.notIn(User::getStatus, 0); // NOT IN
wrapper.between(User::getCreateTime, start, end); // BETWEEN
wrapper.notBetween(User::getCreateTime, s, e); // NOT BETWEEN
wrapper.isNull(User::getEmail); // IS NULL
wrapper.isNotNull(User::getEmail); // IS NOT NULL
wrapper.orderByAsc(User::getSort); // 升序
wrapper.orderByDesc(User::getCreateTime); // 降序
wrapper.last("LIMIT 1"); // 拼接 SQL 片段
wrapper.exists("SELECT 1 FROM ..."); // EXISTS 子查询
条件拼接(带 if 判断)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 第一个参数是 condition,只有为 true 时才拼接此条件
wrapper.like(StringUtils.isNotBlank(name), User::getName, name);
wrapper.eq(age != null, User::getAge, age);
wrapper.ge(startTime != null, User::getCreateTime, startTime);
wrapper.le(endTime != null, User::getCreateTime, endTime);
这是最常用的写法,参数为空时自动忽略该条件,不用写一堆 if 在外面。
and / or 嵌套
// WHERE age > 18 AND (name LIKE '张%' OR name LIKE '王%')
wrapper.gt(User::getAge, 18);
wrapper.and(w ->
w.like(User::getName, "张")
.or()
.like(User::getName, "王")
);
只查指定字段
// 只查 id、name、email,不查全部字段
wrapper.select(User::getId, User::getName, User::getEmail);
// 或排除某些字段
wrapper.select(User.class, info ->
!info.getColumn().equals("password") // 不查密码
);
三、乐观锁——防并发修改
适合"读多写少"的场景,比如秒杀库存扣减、文章点赞数更新。
1. 配置乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁
return interceptor;
}
2. 实体类加 @Version 注解
@Entity
@TableName("product")
public class Product {
@TableId
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
@Version // 乐观锁版本号
private Integer version;
}
数据库加一个 version 字段,默认值为 0。
3. 更新时自动检测
// 先查询(获取当前 version)
Product product = productMapper.selectById(1L);
System.out.println("当前版本: " + product.getVersion()); // 0
// 修改数据
product.setStock(product.getStock() - 1);
// 更新时 MP 自动拼接 WHERE version = 0
// UPDATE product SET stock = ?, version = version + 1 WHERE id = ? AND version = 0
int rows = productMapper.updateById(product);
if (rows == 0) {
System.out.println("数据已被别人修改,请重试");
}
原理: 更新时 SET version = version + 1,条件是 WHERE version = 旧值。如果别人先改了,版本号变了,当前更新影响行数为 0,说明发生并发冲突。
四、逻辑删除——数据恢复留后路
业务上一般不做物理删除,而是标记删除。
1. 配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: is_deleted # 全局逻辑删除字段
logic-delete-value: 1 # 已删除
logic-not-delete-value: 0 # 未删除
2. 实体类
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
@TableLogic // 逻辑删除注解
private Integer isDeleted;
}
3. 效果
// 执行 delete 时,变成 UPDATE
userMapper.deleteById(1L);
// 实际 SQL: UPDATE user SET is_deleted = 1 WHERE id = 1 AND is_deleted = 0
// 查询时自动拼接条件
userMapper.selectList(null);
// 实际 SQL: SELECT * FROM user WHERE is_deleted = 0
// 如果想查已删除的
userMapper.selectList(new LambdaQueryWrapper<User>()
.eq(User::getIsDeleted, 1));
注意: 逻辑删除会让唯一索引失效——比如用户表用手机号做唯一索引,A 用户注销后(逻辑删除),B 用户注册同手机号会冲突。解决方案:联合唯一索引(phone + is_deleted)。
五、自动填充——createTime / updateTime 不用手动 set
@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());
}
}
实体类上加注解:
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
以后 insert 和 update 时,这两个字段自动填充,不用写冗余代码。
六、批量操作
// 批量插入(JDBC 自动拼接成一条 INSERT 多值语句)
userService.saveBatch(userList); // 默认每次 1000 条
userService.saveBatch(userList, 500); // 自定义批次大小
// 批量更新
userService.updateBatchById(userList);
// 批量删除
userService.removeByIds(Arrays.asList(1L, 2L, 3L));
性能提示: saveBatch 底层是 for 循环每批次提交一次,不是真正的批量 insert,数据量大时建议自己写 XML 的 foreach。
七、实战:一个完整的 Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
/**
* 分页查询用户(带条件)
*/
@Override
public Page<User> queryUserPage(UserQueryDTO dto) {
Page<User> page = new Page<>(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(dto.getName()), User::getName, dto.getName());
wrapper.eq(dto.getStatus() != null, User::getStatus, dto.getStatus());
wrapper.between(dto.getStartTime() != null && dto.getEndTime() != null,
User::getCreateTime, dto.getStartTime(), dto.getEndTime());
wrapper.orderByDesc(User::getCreateTime);
return this.page(page, wrapper);
}
/**
* 更新用户(带乐观锁重试)
*/
@Override
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
public boolean updateWithRetry(User user) {
return this.updateById(user);
}
}
总结
| 功能 | 核心注解/类 | 常见用途 |
|---|---|---|
| 分页 | PaginationInnerInterceptor | 列表查询 |
| 条件构造 | LambdaQueryWrapper | 动态 SQL 查询 |
| 乐观锁 | @Version | 并发更新 |
| 逻辑删除 | @TableLogic | 数据恢复 |
| 自动填充 | MetaObjectHandler | 时间戳、操作人 |
| 批量操作 | saveBatch / updateBatchById | 大批量数据 |
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。

3201

被折叠的 条评论
为什么被折叠?



