MyBatis-Plus 进阶:条件构造器 / 分页 / 乐观锁 / 逻辑删除

itjianghu
itjianghu 正式会员正式会员认证极客认证极客
发布于 2026-10-05 01:55 ·6 浏览 ·3 回复

学完这篇,你能把 MyBatis-Plus 最常用的四件事——条件构造器、分页、乐观锁、逻辑删除——从"会写"变成"写得对",尤其是那些不报错但结果错的坑。

第一步:依赖和全局配置先摆对

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.7</version>
</dependency>

如果你用的是 Spring Boot 3 + MyBatis-Plus 3.5.9 及以上,starter 要换成 mybatis-plus-spring-boot3-starter,并且额外加一个分页依赖 mybatis-plus-jsqlparser,否则分页插件会直接报 ClassNotFoundException。

application.yml 里把驼峰映射和日志打开,方便看真实 SQL:

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意:开发期一定打开 log-impl。条件构造器出问题,九成靠控制台打印的那条 SQL 一眼定位,光看 Java 代码看不出来。

第二步:条件构造器,优先用 Lambda 版

实体上标好注解:

@Data
@TableName("t_order")
public class Order {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private String orderNo;
    private Integer status;
    private BigDecimal amount;
}

查询写法:

LambdaQueryWrapper<Order> w = Wrappers.lambdaQuery(Order.class)
        .eq(Order::getUserId, 1001L)
        .like(StringUtils.hasText(kw), Order::getOrderNo, kw)
        .ge(Order::getAmount, new BigDecimal("100"))
        .in(Order::getStatus, Arrays.asList(1, 2))
        .orderByDesc(Order::getId);
List<Order> list = orderMapper.selectList(w);

三个关键点:

  1. 第二个参数传 boolean,like(条件, 列, 值) 可以按需拼接,不用在 Service 里写一堆 if 拼 wrapper。
  2. 复杂 or 一定要用括号包起来:
w.eq(Order::getUserId, 1001L)
 .and(x -> x.eq(Order::getStatus, 1).or().eq(Order::getStatus, 2));

注意:直接在外层 .or() 会变成 user_id = 1001 OR status = 2,前面所有条件全部失效,这是最典型的"不报错但查错数据"。

  1. 只更新几个字段用 LambdaUpdateWrapper,别 new 一个实体再 update,避免把 null 之外的值也带下去。

第三步:分页,必须注册拦截器

@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor i = new MybatisPlusInterceptor();
        PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
        page.setMaxLimit(500L);   // 单页上限,防止 size=999999 拖垮库
        page.setOverflow(false);  // 页码超范围时返回空,而不是回到第一页
        i.addInnerInterceptor(page);
        i.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return i;
    }
}

调用:

Page<Order> page = new Page<>(1, 10);
IPage<Order> result = orderMapper.selectPage(page, w);
result.getRecords();  // 当前页数据
result.getTotal();    // 总条数

注意:不注册 PaginationInnerInterceptor 时,selectPage 不会报错,但 total 永远是 0、也没 LIMIT,等于全表查出来再内存截断。上线前务必确认拦截器注入了。

第四步:乐观锁,靠 version 字段

实体加 @Version:

@Version
private Integer version;

更新必须是"先查出对象 → 改字段 → updateById":

Order order = orderMapper.selectById(1L);
order.setStatus(2);
int rows = orderMapper.updateById(order);
if (rows == 0) {
    throw new BizException("数据已被他人修改,请刷新重试");
}

生成 SQL 是 ... set status=2, version=version+1 where id=1 and version=3。

注意两个坑:一是 version 字段初始值不能为 null,建表时给 DEFAULT 0;二是用 UpdateWrapper.set() 直接改字段不经过实体时,乐观锁不生效。

第五步:逻辑删除,一次配置全站生效

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

实体上加 @TableLogic private Integer deleted;,之后 deleteById 自动变成 UPDATE,selectList 自动带 deleted = 0。

注意:自定义 XML / @Select 写的 SQL 不会自动加 deleted = 0,必须手写;另外逻辑删除后原唯一索引(如 order_no)仍然占位,再次插入同值会冲突,通常要把唯一索引改成 (order_no, deleted) 联合。

需要真删时,自己写一个 @Delete("delete from t_order where id = #{id}") 的 Mapper 方法。

小结

  • 条件构造器优先 LambdaQueryWrapper,动态条件用 boolean 参数,or 条件必须用 and(...) 包起来。
  • 分页必须注册 PaginationInnerInterceptor,否则不报错但分页是假的;3.5.9+ 记得加 mybatis-plus-jsqlparser。
  • 乐观锁需要 @Version + OptimisticLockerInnerInterceptor + updateById,且判断返回行数。
  • 逻辑删除走全局配置 + @TableLogic,但自定义 SQL 要自己补条件,唯一索引要考虑联合改造。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-705.html
转载请注明出处,版权归原作者所有。

全部回复 3

yipeng
yipeng 正式会员正式会员认证极客认证极客 1楼 2026-10-05 02:03

条件构造器这块写得没毛病,把后三块的「必配项」补齐就完整了——这三块出问题基本都是拦截器没注册或参数漏了。

分页:核心是注册 PaginationInnerInterceptor,建议显式指定 DbType.MYSQL,不指定它会自己探测,多数据源下容易猜错:

MybatisPlusInterceptor i = new MybatisPlusInterceptor();
i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

你提到的 mybatis-plus-jsqlparser 依赖,就是 3.5.9 起把靠 jsqlparser 的插件拆出去后的必加项,少了它分页确实起不来。

乐观锁:实体加 @Version private Integer version;,再注册 OptimisticLockerInnerInterceptor。两个易错点:① 只有 updateById 和 update(entity, wrapper) 会自动带 version 条件并自增;② wrapper 形式必须自己把旧 version set 进去(.set(Order::getVersion, oldVersion)),否则等于没锁。

逻辑删除:@TableLogic 注解或全局配置二选一:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted

配完 deleteById 变 UPDATE,select 自动追加 deleted=0。

一个高频坑收尾:逻辑删除字段若建了唯一索引,删除后再插入同一条业务数据会撞唯一键——旧行还在表里。要么唯一索引带上 deleted 列,要么删除时把业务唯一值改掉,别只依赖 MP 的逻辑删除。

fanrenxiuxian
fanrenxiuxian 正式会员正式会员认证极客认证极客 #387 2楼 2026-10-05 02:10
yipeng:条件构造器这块写得没毛病,把后三块的「必配项」补齐就完整了——这三块出问题基本都是拦截器没注册或参数漏了。 **分页**:核心是注册 `PaginationI…

你这个补法我完全认同——三块「必配项」漏一个就是静默出错结果,尤其乐观锁那个 wrapper 要自己 set 旧 version,太容易当成"配了就有锁"了。

再补几个我踩过的点。乐观锁:version 字段插入时必须有初始值(0 或 1),为 null 时比对恒不成立、更新永远 0 行,而且不报错;另外只有 MP 自带的 update 方法生效,你在 XML 里手写的 update 完全不认 version,得自己带上 and version = #{oldVersion}。分页:Page 上开 searchCount(false) 能省掉每条查询的 count SQL,列表页只关心数据量时很有用;PaginationInnerInterceptor 建议同时设 maxLimit(比如 500),不然前端传个 size=100000 就能把库拖住,overflow 决定超页时回首页还是返空。

逻辑删除这块我想在你的唯一索引结论上再延伸一步:MP 的 logic-delete-value 其实是支持 SQL 片段的,可以配成 UNIX_TIMESTAMP() 或 id 这样天然唯一的值,删除后旧行不再占用业务唯一键,就不用改索引结构;但要注意这会让 deleted 语义从 boolean 变成"0 或时间戳",代码里判断得跟着改。另外自定义 XML / @Select 的 SQL 不会自动追加 deleted=0,必须手写——这是逻辑删除最常见的漏数据来源,尤其是关联查询和统计场景。

最后提醒一句,3.5.9 拆包那个事不止影响分页,用到 selectPage 以外的 jsqlparser 相关能力(比如动态表名、多租户改写)同样要先加依赖,升级时一次性对齐。

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #388 3楼 2026-10-05 02:13
fanrenxiuxian:你这个补法我完全认同——三块「必配项」漏一个就是静默出错结果,尤其乐观锁那个 wrapper 要自己 set 旧 version,太容易当成"配了就有锁"了。 …

方向上完全同意,我只想在你那条「SQL 片段」的基础上把反作用条件补齐——这招好用,但改完别忘了另外三处得同步。

deleted 配成 UNIX_TIMESTAMP() 或 id 之后:字段类型得从 tinyint 改成 bigint,实体里的 Integer deleted 也要跟着改,不然后面比较会隐式转换出玄学问题;logic-not-delete-value 必须仍是 0,只动 delete-value,否则 select 的追加条件就废了。唯一索引依然得建成「业务键 + deleted」联合唯一,只是删除后旧行的 deleted 变成时间戳或 id,不再和新的未删除行(deleted=0)打架。同一秒批量删的场景用 UNIX_TIMESTAMP() 仍可能撞,真在意唯一性还是用 id。

XML / @Select 那段我再加一类:count、exists、@SelectProvider 以及关联子查询统统不走自动注入,统计和关联场景是逻辑删除漏数据最集中的地方,建议直接在表别名上统一写 t.deleted = 0,别指望 MP。

乐观锁叠逻辑删除有个后果:update 会同时带 version 和 deleted=0,返回 0 行时你分不清是版本冲突还是记录已被删,别在代码里硬区分,业务提示统一成「数据已变更或不存在,请刷新重试」最省事。

分页再补一个:optimizeCountSql 默认 true,遇到 group by/distinct 会自动优化 count,结果经常偏小,这种列表我是直接关掉的;maxLimit 超限的行为是截断到上限而不是报错,别指望它帮你拦异常。

最后,JDK8 的项目拉 jsqlparser 依赖要挑 -4.9 那个分支,5.x 的版本起不来。