MyBatis-Plus分页插件从入门到实战:配置、原理与踩坑指南
2026/9/9 14:57:07 网站建设 项目流程

在 Java 服务端开发这个圈子里,MyBatis-Plus 早就不是一个新鲜词了。我第一次在项目里正式把它接入,是在一个后台管理系统迭代速度极快的阶段,几十张表的单表 CRUD 如果还按原生 MyBatis 的方式写 XML,光维护就能把人累到怀疑人生。后来切到 MyBatis-Plus 之后,基础配置只花了一个下午,分页插件一配,原先那些重复的 limit、offset 代码直接被抹平了。这篇文章不打算从一个纯小白的角度讲“这是什么框架”,而是按我实际落地的顺序,把基础配置、分页插件的原理和实战、以及日常最容易踩的坑从头到尾梳理一遍,希望能给正在项目里引入 MyBatis-Plus 的朋友一些可直接照抄的经验。

这篇文章更适合两类人看。一类是刚把 MyBatis-Plus 引入项目、已经在用基础的 selectById、selectPage 但没搞懂内部原理的开发者;另一类是项目里分页偶尔失效、count 查询结果不对,或者在多数据源场景下被拦截器顺序折磨过的人。我会把配置项、代码写法、原理推断和排查思路放在一起写,既然叫“从入门到实战”,那就既要让你能跑起来,也要让你知道跑起来之后出了问题该从哪里查。

1. 先想清楚:项目里为什么要用 MyBatis-Plus

1.1 原生 MyBatis 最大的痛点是“简单操作不简单”

原生 MyBatis 是一个很优秀的持久层框架,它把 SQL 的执行权和映射逻辑完全交给了开发者,这种方式的优点非常明显:SQL 可控、性能可调、复杂查询游刃有余。但当你面对一张几十个字段的用户表,要写一个按主键查询、按条件分页查询、逻辑删除、批量插入时,那种为了“一个简单的列表页”反复堆 XML 的感觉,干过的人都懂。

MyBatis-Plus 做的事情并不是把 MyBatis 替换掉,而是在 MyBatis 的基础上做增强,官方定位叫“MyBatis 的增强工具,只做增强不做改变”。也就是说,原来那些需要手写 XML 的复杂 SQL 依然可以继续写在 XML 里,而单表的增删改查、分页、条件构造器这类高频操作,直接通过内置 API 就能完成。这个定位很关键,意味着团队如果已经有成熟的 MyBatis 经验,引入 MyBatis-Plus 的学习成本几乎没有,风险也低。

从实际项目收益来看,最明显的变化是 Mapper 层代码量。以前一个模块要写 UserMapper.java、UserMapper.xml,里面铺满 id、name、status 这些字段的 CRUD 语句;现在继承一个 BaseMapper ,基础的 selectList、selectPage、deleteById 全都有了。我自己的项目里,光去掉这些重复 XML 就省了很多时间,而且对新人来说,看代码的门槛也低了不少。

1.2 为什么不用 JPA / Hibernate

很多人会问,既然要提升开发效率,为什么不直接上 Spring Data JPA?JPA 在实体关系映射和对象状态管理上的能力强,设计优雅,但在国内大量业务系统里,SQL 的可控性和团队对该技术的熟悉程度往往比“完全对象化”更重要。MyBatis-Plus 的优势在于它保留了 MyBatis 的 SQL 风格,同时把简单操作自动化,相当于兼顾了开发效率和 SQL 控制力。

举一个很常见的场景:订单查询列表,JPA 要处理 Specification、动态条件拼接,学习曲线不低;而 MyBatis-Plus 的 LambdaQueryWrapper 可以用 Java 代码的方式把查询条件拼出来,生成 SQL 之后仍然完全可控。不需要学习 HQL,不需要理解一级缓存和对象状态,这对大多数业务团队来说更友好。

所以我的建议是:如果你的系统主要是复杂报表、大数据量、各种 join 和子查询,持久层用 MyBatis 加 MyBatis-Plus,不会错;如果你的业务对象关系极其复杂,强依赖对象级联操作,那 JPA 才是更合适的路。选型这事没有绝对的对错,只有是否适合当前团队和场景。

1.3 落地场景:后台管理系统、中台服务、快速迭代项目

我在实际项目里接触到的 MyBatis-Plus 案例,绝大多数集中在后台管理系统、运营平台、中台服务这类“单表 CRUD 多、关联查询相对固定”的业务上。比如用户管理、角色管理、菜单权限、订单列表,这几乎是所有管理系统的标配。这类需求要求开发速度快、代码风格统一、稳定不折腾,MyBatis-Plus 非常契合。

另外一个特别适合的场景是“老项目重构”。很多团队想把老项目从 JDBC 或原生 MyBatis 重构为 Spring Boot 体系,但全量改造成 JPA 风险太大,渐进式地抽一个模块引入 MyBatis-Plus,其他模块保持原样,这种灰度改造方式我实践过,很稳。因为 MyBatis-Plus 没有改变 MyBatis 的执行机制,原来写的 XML 不会失效,只需要把基础 CRUD 逐步替换掉,就能在较短时间内感受到效率提升。

2. 基础配置:把项目一步一步跑起来

2.1 版本选型与环境准备

先聊版本。MyBatis-Plus 目前维护了多个版本线,3.4.x、3.5.x 是最常见的。新项目我建议直接用 3.5.x 的最新稳定版,因为它在 3.4 的基础上完善了多数据源、分页插件的拦截器机制,API 也更统一。Spring Boot 方面,如果是 2.x 项目,用 mybatis-plus-boot-starter 3.5.x 即可;如果是 Spring Boot 3.x,需要注意 JDK 版本和 starter 的适配,MyBatis-Plus 从 3.5.3 之后对 Spring Boot 3 支持得比较成熟。

基础环境大致如下:

  • JDK 8/11/17(Spring Boot 2.x 用 8 或 11,Spring Boot 3.x 用 17 及以上)
  • Maven 3.6 或 Gradle 6.8 以上
  • Spring Boot 2.5+ 或 3.x
  • MySQL 5.7 / 8.0,或者其他支持 SQL 标准分页的数据库

不要在依赖版本上太随意。我见过有项目把 MyBatis-Plus 版本从 3.4 直接跳到 3.5.0,结果 Interceptor 配置方式变化导致启动报错。看一眼官方升级文档,其实也就几分钟的事,能省掉很多排查时间。

2.2 Maven 依赖引入完整示例

在 pom.xml 里引入 mybatis-plus-boot-starter,是最快的方式:

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

如果项目里已经有 mybatis 或 mybatis-spring 的依赖,建议去掉,避免版本冲突。MyBatis-Plus 的 starter 会传入对应版本的 mybatis 和 mybatis-spring,不需要额外声明。数据库驱动和连接池相关依赖保持原有配置即可。

另外,如果项目用了 Lombok,实体类可以少写一堆 getter/setter,我建议配合使用。但有一点要注意:如果你在实体类里手写了带参构造函数,又没有写无参构造函数,MyBatis-Plus 在反射创建对象时会失败,报错信息通常是“There is no getter for property named...”。这个坑很隐蔽,我在项目里遇到过不止一次。

2.3 application.yml 里的核心配置

MyBatis-Plus 的配置项以mybatis-plus开头,基础配置一般包括实体类驼峰映射、逻辑删除、主键策略等。下面是一个我常用的配置模板:

mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这些配置项里,我挑几个重点说:

  • map-underscore-to-camel-case:控制数据库字段下划线user_name与 Java 属性userName的自动映射,一般必须设成 true,不然你的实体类要么被迫命名为user_name,要么每个字段都加@TableField
  • logic-delete-field:全局逻辑删除字段。设置之后,MyBatis-Plus 会在执行 delete 的时候自动转成 update,执行查询的时候自动追加deleted = 0条件。这一点能救很多手滑删数据的案例。
  • id-type:主键策略。assign_id使用雪花算法生成分布式 ID,适合分库分表场景;auto则使用数据库自增。单体小项目可以用auto,微服务或未来的分库分表趋势下建议用assign_id
  • log-impl:配置为StdOutImpl可以在控制台打印 SQL,开发和排查问题阶段很有用,上线前记得去掉或改成不输出,避免日志量过大。

这些配置并不是必须全部写上,但每一行都对应一种真实场景。我不太建议“网上抄一个最简配置就完事”,因为等你的项目开发到中后期,逻辑删除、ID 策略这些如果一开始没规划好,回改成本非常高。

2.4 实体类与 Mapper 层的基本写法

实体类的常规写法如下:

@Data @TableName("user") public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; @TableField("user_name") private String userName; private String email; @TableLogic private Integer deleted; }

这里有几个注解值得注意:

  • @TableName:实体类对应的表名。不加的话,MyBatis-Plus 默认把类名User转成user,如果你的表名和类名不一致,必须显式指定。
  • @TableId:主键。type 可以设置IdType.AUTOIdType.ASSIGN_IDIdType.INPUT等。如果全局已经配置,可以省略。
  • @TableField:字段映射。主要用于两种场景:一是属性名和列名不一致;二是该字段在数据库表中不存在,需要加exist = false,例如一些临时计算的冗余字段。
  • @TableLogic:逻辑删除字段。如果配置了全局logic-delete-field,可以不写这个注解。

Mapper 层接口更简单:

public interface UserMapper extends BaseMapper<User> { }

继承BaseMapper<T>之后,insertdeleteByIdselectByIdselectListupdateByIdselectPage这些通用方法就都有了。想写复杂查询,直接在接口里声明方法,在UserMapper.xml里写对应 SQL 即可,不会和 MyBatis-Plus 的自动方法冲突。

写到这里,基础的 CRUD 能力就已经可用了。但要让分页真正跑起来,还需要配一个拦截器,这就是接下来要讲的分页插件。

3. 分页插件全解析:原理、配置与实战

3.1 手写分页到底麻烦在哪

在聊分页插件之前,先想想没有插件的时候,我们在 MyBatis 里怎么做分页。最原始的方式是在 XML 里手写 limit 相关 SQL,让 Mapper 方法接收offsetlimit两个参数:

<select id="selectPage" resultType="com.example.demo.entity.User"> select * from user where status = #{status} limit #{offset}, #{limit} </select>

这样做有几个问题。

第一,不同数据库的分页方言不同。MySQL 用limit offset, size,Oracle 用rownum或 12c 之后的fetch first,PostgreSQL 用limit offset语法。项目如果后面要切换数据库,所有分页 SQL 都得大改,这种代价很痛苦。

第二,总记录数需要额外写一条select count(*)。如果查询条件很复杂,这条 count SQL 必须和列表 SQL 的 where 条件保持一致,一旦条件更新忘记同步,就会出现“总条数对不上”这类很难排查的问题。

第三,代码侵入性强。业务方法里混入了分页参数、手动计算 total,很难统一规范。

分页插件做的事,就是把这三点一次性解决掉:自动识别数据库方言,自动生成 count 查询,自动改写分页 SQL,并把你传入的 Page 对象中原有的 total、pages 等属性回填。

3.2 分页插件的工作原理

MyBatis-Plus 的分页插件本质是一个 MyBatis 拦截器,它拦截的是Executor的 query 方法。当你调用selectPage(page, wrapper)selectMapsPage时,MyBatis-Plus 会检查参数里有没有IPage类型的对象,如果有,插件就开始工作。

整体流程大概是这样的:

  1. 从参数里取出Page对象,拿到current(当前页)和size(每页条数)。
  2. 根据当前数据库类型,生成一条 count SQL,执行后把结果写入Page.total
  3. 在原 SQL 基础上拼接分页语句,比如 MySQL 就是在结尾追加LIMIT offset,size
  4. 执行分页 SQL,将结果列表写入Page.records
  5. 返回时,业务代码通过Page对象直接获取recordstotalcurrentpages

这里有个很重要的机制是 ThreadLocal。分页参数在拦截器内部通过PageMethod的 ThreadLocal 传递。这也解释了为什么分页插件要求分页参数必须紧跟 SQL 执行链路,一旦你用多线程或者异步方式去执行查询,分页参数就丢了。

另外,分页插件只对IPage类型的参数生效。如果你在自定义方法里传入了两个参数,其中一个是Page,但不是通过 MyBatis-Plus 的selectPage方法调用,而是自己写了一个带分页的 XML SQL,需要在方法签名里把Page参数放在最后,并且参数名能被识别。这个细节我会在后面的常见问题里专门讲。

3.3 配置分页拦截器:这一步不能错

MyBatis-Plus 3.5.x 官方推荐的配置方式是手动注册一个MybatisPlusInterceptor,把PaginationInnerInterceptor添加进去。不要在旧配置里直接注册PaginationInterceptor了,那是老版本的做法,新版本已经移除。

配置代码如下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); // 单页分页条数上限,默认 500 条;设置为 -1 表示不限制 paginationInterceptor.setMaxLimit(500L); // 溢出总页数后是否进行处理,默认 false。设置为 true 时,当前页数超出总页数后返回最后一页 paginationInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }

这里我要特别强调DbType参数。PaginationInnerInterceptor需要知道数据库类型,才能生成正确的方言分页 SQL。你可以在构造函数里指定,也可以在配置文件中通过db-type指定。如果项目里用的是 MySQL,就写DbType.MYSQL;如果是 PostgreSQL、Oracle、SQL Server,对应改成自己的类型。如果配置错了或者没有配置,插件可能无法正确生成分页 SQL,导致分页直接失效。

setMaxLimit是一个容易被忽略但很重要的防线。接口如果被恶意或不规范调用,传入一个极大的size,没有上限限制的话可能直接把数据库拖垮。我一般会设置一个合理的上限,比如 500 或 100,视业务量而定。

拦截器的执行顺序也有讲究。如果你同时使用了多租户插件、乐观锁插件、分页插件,它们都通过addInnerInterceptor添加到同一个MybatisPlusInterceptor里。执行顺序遵循添加顺序,一般分页插件建议放在多租户插件之后,否则分页 SQL 生成时可能没有带上租户条件,造成数据越权。顺序问题在引入了更多拦截器之后会变得很关键。

3.4 分页实战:从 Controller 到 Service 的完整写法

配置完成之后,分页代码其实非常简单。先看一个标准的 Service 层写法:

@Override public Page<UserVO> pageUsers(int current, int size, String name, Integer status) { Page<User> page = new Page<>(current, size); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(name), User::getUserName, name) .eq(status != null, User::getStatus, status) .orderByDesc(User::getCreateTime); Page<User> userPage = userMapper.selectPage(page, wrapper); // 这里可以做一个实体到 VO 的转换,一般用 BeanUtils 或 MapStruct Page<UserVO> userVOPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal()); userVOPage.setRecords(userPage.getRecords().stream().map(...).collect(Collectors.toList())); return userVOPage; }

Controller 层的写法就是接收参数,调 Service,然后封装统一返回结构:

@GetMapping("/users") public Result<Page<UserVO>> listUsers( @RequestParam(defaultValue = "1") int current, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String name, @RequestParam(required = false) Integer status) { return Result.success(userService.pageUsers(current, size, name, status)); }

这里有一个非常常见的坑:Page对象本身继承了ArrayList,说实话它不是一个普通的实体类,在序列化成 JSON 时,如果配置了自定义序列化器或者返回结构比较复杂,可能会有额外字段或者大小不一致的问题。更稳妥的做法是不要把Page直接暴露给前端,而是把它转换成统一的PageResult,只保留recordstotalcurrentsizepages这几个字段。前端拿到的数据干净,后端的返回结构也稳定。

如果你需要自定义分页 SQL,比如多表 join 查询,可以这样写:

public interface UserOrderMapper extends BaseMapper<UserOrder> { IPage<UserOrderVO> selectUserOrderPage(Page<?> page, @Param("userId") Long userId); }

XML 里:

<select id="selectUserOrderPage" resultType="com.example.demo.vo.UserOrderVO"> select uo.id, uo.order_no, u.user_name from user_order uo left join user u on u.id = uo.user_id where uo.user_id = #{userId} </select>

注意,这个自定义方法里的分页 SQL 不需要手写limit,分页插件会负责帮你拼上。方法签名里必须有一个Page类型的参数,通常放在最后。MyBatis-Plus 通过参数类型识别它,然后对当前查询进行分页增强。

IPage的返回类型也值得说一下。selectPage返回的是PageIPage是它的父接口。日常使用中,你可以直接声明返回IPage<T>,只要实现类的recordstotal这些属性被正常填充即可。但要注意,有些代码生成器生成的 Mapper 方法返回List<T>,那不是分页,那是把整表查出来之后再在内存里分页,千万别把这个当成分页用。

3.5 分页插件的高级使用:自定义 count 与优化

默认情况下,分页插件会生成一个SELECT COUNT(*) FROM user WHERE ...的 count SQL。对于简单查询,这个 count 很高效;但遇到多表 join、where 条件里包含子查询时,count 可能也会执行大量 join,性能不一定好。

针对这种情况,MyBatis-Plus 允许你通过自定义count方法优化,但更常用的做法是直接优化查询 SQL 本身。如果 where 条件很轻,count 让它自然执行就行;如果 join 很多,count 有条件提前过滤的话,可以手动让 where 条件尽量精简。

还有一个很多人不知道的优化点:PaginationInnerInterceptor默认会对 count SQL 做自动化改写,比如去掉order by。因为 count 只需要结果数量,排序是没意义的。这个优化是内置的,所以即便你的查询 SQL 里带了order by,count SQL 也不会傻乎乎地跟着排序。

但如果你使用的是自定义countSQL,这个优化就失效了,需要自己保证 count 足够轻量。我个人的习惯是:能用默认 count 就用默认 count,只有确实出现性能瓶颈再手工优化,不要提前做过度设计。

4. 常见问题与排查技巧实录

4.1 分页不生效,查出来全是全表数据

这是用 MyBatis-Plus 分页时被问得最多的问题。现象就是selectPage返回的records是全量数据,total也不对,完全像是没有分页一样。

排查这个问题的第一步是确认拦截器有没有被 Spring 管理。很多人写了MybatisPlusConfig,但忘了加@Configuration,或者类没有被扫描到,导致MybatisPlusInterceptor根本没注册。此时你在日志里看不到任何分页 SQL 打出来,因为插件根本没有参与执行。

第二步是确认分页参数是否真的传进了 MyBatis 的查询链路。比如在自定义 Mapper 方法中,如果你用了@Param注解,但是Page不是放在参数列表中的最后一个,或者没有使用Page类型作为方法参数,分页插件可能识别不到。最保险的做法是:分页方法中尽量只保留一个Page参数,并且让 MyBatis-Plus 的selectPage来调用,不要自己写一个返回Page但是内部用了selectList的方法。

还有一种场景是分页插件的DbType配置错误。比如你配了DbType.SQL_SERVER,但实际连的是 MySQL,数据库方言不匹配,生成的 SQL 无法执行,甚至查询直接报错。配置一定要和实际数据库保持一致。

4.2 total 和 pages 数据不正确

total不正确的常见原因是Page对象没有正确初始化,或者currentsize传了 0。

在 MyBatis-Plus 中,Pagecurrent从 1 开始,size必须大于 0。如果你把current传成 0,有些版本会当成第一页处理,有些版本会算出负数偏移量,导致返回数据不符合预期。我建议在 Controller 层做参数校验,current至少为 1,size至少为 1,并且设置上限。

另一个容易忽略的点是,total的字段类型问题。Page默认的totallong,如果你的前端或返回值包装类型是int,在数据量较大的情况下可能溢出。虽然大多数系统不会真到 21 亿条,但这个细节一旦触发就是线上事故,不如一开始就用long和前端约定。

如果pages算出来和预期不一致,往往是total不正确或者size传了 0 导致的。pages = (total + size - 1) / size,当size = 0时会报错或返回异常值。拦截器内部有判断,但我们在业务里也要避免把size传成 0。

4.3 Mapper 扫描和 XML 扫描配置问题

项目启动的时候报Invalid bound statement (not found),很多人第一反应是 Mapper 接口没有注册或 XML 没找到。这个问题在引入 MyBatis-Plus 之后依然高频出现,排查点有三个:

  1. 启动类或配置类上有没有@MapperScan("com.example.demo.mapper"),如果有,扫描的包路径是否正确。
  2. application.yml里的mapper-locations是否正确,默认值可能是classpath:/mapper/**/*.xml。如果你的 XML 放到resources/mapper下但文件名或路径不匹配,就会找不到。
  3. Mapper 接口的方法名和 XML 里的<select id="xxx">是否一致,以及方法参数、返回类型的resultType是否填写正确。

MyBatis-Plus 的BaseMapper自带的方法不需要 XML,所以这类报错通常只出现在自定义方法上。定位思路就是先确认 XML 能不能被正确加载,再确认方法名和参数绑定是否正确。

4.4 多数据源或复杂场景下分页失效

在引入了多数据源、动态数据源的项目里,分页失效的概率会比单数据源高很多。原因在于,多数据源切换时,如果使用的是不同的SqlSessionFactory,或者MybatisPlusInterceptor的注册时机不对,拦截器可能无法同时作用到所有数据源上。

动态数据源框架如dynamic-datasource-spring-boot-starter通常会自动协调这个关系,但如果你是自己写的多数据源配置,就需要确保每个SqlSessionFactory都注入了同一个MybatisPlusInterceptor。更稳妥的做法是让拦截器 Bean 在 Spring 容器中全局唯一,然后在每个数据源的SqlSessionFactory创建时通过setPlugins注入进去。

还有一个场景是分布式事务、异步线程内分页。分页参数使用 ThreadLocal 传递,一旦你开了异步线程去执行分页查询,分页参数不会自动传递到子线程。解决方案是手动把Page参数传入异步方法,或者使用可以传递 ThreadLocal 的装饰器。不过整体来说,分页查询放到主线程同步执行是最简单的,也是性能可控的方案。

4.5 分页插件与其他插件顺序问题

MyBatis-Plus 3.5.x 把多租户、乐观锁、分页、防全表更新都设计成了InnerInterceptor。多个拦截器同时使用时,顺序直接影响 SQL 改写结果。

举个例子,如果你先添加了分页插件,再添加多租户插件,那么分页 SQL 的优化是先执行分页改写,还是后执行租户条件拼接?实际上,MybatisPlusInterceptor会按照addInnerInterceptor的顺序调用各InnerInterceptor,而多租户插件会在查询 SQL 上追加租户条件,分页插件会追加limit

一般来说,推荐顺序是:多租户插件优先于分页插件,这样分页 SQL 在生成时已经带上了租户条件,count SQL 也能正确统计当前租户的数据范围。乐观锁插件可以放在较后位置,因为它只影响 update 操作,对分页无直接影响。

这个顺序问题如果配反了,最典型的症状就是:分页后查出来的数据正常,但 count 的 total 统计的是所有租户的数据,导致分页总页数偏大,出现跨租户数据泄露隐患。排查时要先看执行 SQL,再检查拦截器顺序。

5. 我的实操心得与推荐配置

聊了这么多原理和排错,最后分享一些我实际项目里沉淀下来的经验和习惯。

第一件事,MyBatis-Plus 的代码生成器值得用,但要用得聪明。我一般会用它生成 entity、mapper、service、controller 的初始代码,然后立刻关掉覆盖模式,避免后续的二次开发被覆盖。生成的实体类里字段注释最好从数据库注释自动同步,这一点对团队协作特别有帮助,不然每次看字段含义都要去翻数据库。

第二件事,DTO/VO 和实体类一定要分开。MyBatis-Plus 的实体类要和数据库表一一对应,查询接口的返回对象单独设计成 VO。这样做的好处是防止前端字段直接暴露数据库字段,也避免因为返回内容变动反复改实体类。最开始为了省事直接在 Controller 返回实体类,后来接口文档一变,改起来非常折磨。

第三件事,分页查询的性能实际上是有上限的。limit offset, size在 offset 非常大的时候会变慢,因为数据库需要扫描并丢弃大量行。MyBatis-Plus 分页插件只是帮我们生成了这条 SQL,它并不能优化深分页本身的性能。如果业务确实需要翻到几千页之后,比如后台导出或大数据量列表,建议改用“游标分页”或“基于最大 ID 分页”的方式,不要硬吃 offset。

第四件事,配置 log 变量在开发期打开,上线前一定关掉。ORM 框架打印 SQL 虽然看着方便,但生产环境的日志量是成倍上涨的。如果线上真的需要排查慢 SQL,配合阿里的 druid 或 MySQL 的 slow query log 更合适,而不是让所有 SQL 都从应用层打出来。

最后分享一个小技巧:如果你用Page对象做条件构造器,记得把排序条件放到wrapper.orderByDesc(...)里,不要放在 SQL 手写字符串里。这样既能保持分页插件对 count SQL 的优化,也能让排序字段在 Java 层可控,避免 SQL 注入。

本来想继续写更多踩坑细节,不过这篇文章的核心内容已经说得差不多了。MyBatis-Plus 的分页插件不是一个复杂的组件,但真正用好它,需要理解它的拦截机制、参数传递方式和多插件协作顺序。配置一次之后,开发效率的提升是实打实的。我自己是在一次项目重构中彻底用顺手的,之后几乎每套系统的持久层都沿用这套方案,也希望这篇文章能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询