MyBatis-Plus 高级用法实战——分页、条件构造器、乐观锁、逻辑删除

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;

以后 insertupdate 时,这两个字段自动填充,不用写冗余代码。

六、批量操作

// 批量插入(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/爬虫 实战干货,不让你白来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值