ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MyBatisPlus实战:分页失效排查与单页500条限制突破

MyBatisPlus实战:分页失效排查与单页500条限制突破 开门见山说个事现在Java后端做持久层你要是还在用纯粹的MyBatis手写SQLCRUD接口一个表写一堆XML人效真的跟不上。MyBatisPlus在MyBatis基础上把单表CRUD、条件构造器、分页插件、代码生成器全给封装好了项目里彻底告别重复的增删改查模板代码。这篇文章我把自己在实际项目中用MyBatisPlus这几年踩过的坑、调优过的配置、解决的问题包括最近特别多人问的“分页失效”和“单页500条限制”一次性理清楚。不管你是在老项目里引入MP还是新项目选型这篇都值得看完再动手。1. 为什么团队都在迁移到 MyBatisPlus1.1 它到底解决了什么痛点先别急着看代码想清楚一个问题我们以前用MyBatis写单表操作最烦的是什么是每个实体类对应一套Mapper接口加一套XML里面全是insert、delete、update、selectById这种几乎一模一样的SQL。表一多XML文件几十个改个字段名要全局搜替换眼都快花了。MyBatisPlus做的事情很简单单表CRUD不用写SQLBaseMapper把常用的方法全给你预置好了你继承一下接口就有十几个现成方法可以用。条件查询不用拼字符串用LambdaQueryWrapper构造查询条件类型安全、可读性好、不怕字段名拼错。分页不用再手写LIMIT配一个分页插件传个Page对象就能拿到总数和当前页数据。代码生成器一键把实体、Mapper、Service、Controller全部生成出来新模块落地速度可以说是碾压手写。为什么在众多增强框架里MP脱颖而出它没有重新发明轮子BaseMapper底层还是MyBatis那一套SQL还是你自己能控制的。MP更像是一个“脚手架”把重复劳动包下来复杂查询你随时可以退回原生SQL灵活性和便捷性兼顾。这个定位非常讨巧团队上手成本极低从一个老MyBatis项目迁移过来几乎不需要培训。1.2 适合什么项目什么情况下别用它从项目类型来看MP最适合业务系统、管理后台、中台服务这类以单表CRUD为主、查询条件比较灵活的项目。如果一个项目80%以上的数据库操作是单表增删改查MP能帮你省掉大量的模板代码。尤其是新项目快速迭代的阶段代码生成器加MP一个模块从建表到接口跑通可能就半天时间。但是别神话它。如果你的系统是报表分析型项目SQL动不动就是三四张表join加子查询加窗口函数这种场景MP帮不上太大忙复杂SQL还是得老老实实写在XML里。还有一种是极端追求SQL性能优化的团队每一句SQL都要手工调执行计划MP自动生成的SQL虽然不差但可能会让你觉得不够“精准”。我个人的建议是MP作为基础CRUD层没问题复杂查询继续用XML两者完全可以共存这也是MP官方推荐的使用方式。2. 核心功能的落地姿势CRUD、条件构造器和分页2.1 内置CRUD接口怎么用才不踩坑引入MP之后实体类上记得加几个关键注解否则会有一些隐藏问题。TableName指定表名如果表名和实体名不一致不指定就会报错TableId指定主键字段主键如果叫id而实体里叫userId不指定也会出问题TableField指定非主键字段名特别要注意字段名是keyword、order、desc这种数据库关键字时必须用TableField标注。还有事务版本的实体最好加Version实现乐观锁逻辑删除字段加TableLogic。继承BaseMapper就能直接用这些方法public interface UserMapper extends BaseMapperUser { }selectById、deleteById、insert、updateById、selectBatchIds这些方法都有。但有个点很多人会忽略updateById这个方法默认只会更新非NULL字段。如果你想把某个字段更新为NULL直接传一个null进去这个方法不会帮你更新。解决办法是使用LambdaUpdateWrapper显式调用set方法userMapper.update(null, new LambdaUpdateWrapperUser() .eq(User::getId, 1001) .set(User::getRemark, null));这个坑我当年踩过线上有个需求要把备注清空结果updateById传null字段进去静默失败数据完全没变排查了很久。所以记住updateById适合整行覆盖更新局部字段更新特别是置空操作一定要走UpdateWrapper。2.2 条件构造器的正确打开方式条件构造器是MP的灵魂所在。QueryWrapper、LambdaQueryWrapper、UpdateWrapper、LambdaUpdateWrapper这四件套要搞清楚各自的使用场景。QueryWrapper用法是列名写字符串好处是动态性高坏处是字段名写错编译器不报错运行时报错LambdaQueryWrapper直接传实体方法的引用User::getName编译期就能发现问题。能写Lambda就用Lambda代码重构的时候好处尤其明显。动态条件判断是日常开发最常用的功能比如前端传过来的筛选条件可能为空我们以前要么写一堆if判断拼接SQL要么用MyBatis的 标签。MP支持condition参数写法非常精炼LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), User::getName, name) .eq(StringUtils.hasText(status), User::getStatus, status) .ge(ageMin ! null, User::getAge, ageMin);第一个参数是boolean为false时这个条件自动忽略整个查询条件链看起来非常干净。eq、ne、like、between、in、orderByDesc这些方法都支持condition重载。还有个要注意的点like查询在MySQL里走的是%关键字%这种模糊匹配如果数据量大LIKE %xxx这个前缀%会导致索引失效这是SQL优化问题不是MP问题但使用时要心里有数。2.3 分页插件必须这样配才对分页是MP的招牌功能但很多人配错。先看标准配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个配置是必须的少了它分页就不生效这一个问题能解释网上90%的“MP分页失效”求助帖。DbType一定要指定准确因为MP要根据数据库类型生成不同的分页方言。接着在Mapper里这样写PageUser page new Page(current, size); PageUser result userMapper.selectPage(page, new LambdaQueryWrapperUser().eq(User::getStatus, 1)); ListUser records result.getRecords(); long total result.getTotal();Page对象有两个关键点。第一Page构造方法第一个参数是页码从1开始第二个是每页大小。第二个点MP的分页查询是执行两条SQL一条带LIMIT查数据一条不带LIMIT查COUNT所以在分页SQL里别再去掉旧的count查询插件会自动处理。total是long类型前端如果按int接收数据量大的时候会溢出这个注意一下。3. 分页失效5个必查原因和现场还原3.1 原因一插件没配等于白写我刚才说的那句“分页插件必须配”真不是危言耸听。很多项目是半路接的MP只引入了依赖把Mapper改成继承BaseMapper然后就开开心心写Page查询了结果list结果出来一数条数不对发现limit完全没生效。翻配置文件翻代码排查了半天最后发现MybatisPlusConfig这个类根本不存在。这个问题的本质是PaginationInnerInterceptor是一个拦截器它要注册到MyBatis的拦截器链里才会对SQL做改写。你没配置SQL就不会被改写分页自然不生效。我自己给团队做代码审查的时候第一件事就是看有没有这个配置类。如果是Spring Boot项目确保配置类被扫描到别把配置类写在Spring扫描路径之外。3.2 原因二多表联查时count翻车分页插件在遇到多表关联的SQL时自动生成的count SQL偶尔会有问题。比如SQL里有distinct有group by有复杂的子查询插件生成的count SQL可能是错误的报语法错误或者count结果不对。分页插件看到count失败可能就不执行分页SQL了直接返回全量数据表现出来就是分页失效。解决办法其实很简单手动控制count查询。MP的Page对象可以设置一个AnsiSqlParserInfo或者直接传入分页SQL但更常见的做法是自定义一个count SQL。如果你的mapper方法是用Select注解写分页SQL的可以这样Select(select * from blog b left join author a on b.author_id a.id where b.title like #{name}) IPageBlogVO selectBlogPage(Page? page, String name);Page参数只要放在方法参数里分页插件会自动拦截改写这条SQL。如果count不对或者性能差你可以单独写一个count查询方法或者在XML里定义两条SQL语句一条查询列表一条查询总数然后把总数通过自定义方案传回去。实战里大部分联表分页问题只要保证主表是驱动表、条件字段有索引自动count还是能扛得住的。3.3 原因三自定义SQL与Wrapper参数脱节这个是新手的重灾区。我见过有人这样写Mapper方法ListUser selectUserPage(PageUser page, Param(name) String name);然后在XML里select idselectUserPage resultType... select * from user where name like concat(%, #{name}, %) /select分页插件要生效Page必须是第一个参数吗其实不是必须的但IPage参数必须被识别到。关键坑在于如果你在Service里创建了Page对象但没传给Mapper方法或者Mapper方法的Page参数没配上插件无法识别分页意图就不会做分页。还有一点如果你的自定义SQL里参数是用Param(ew)传进来的Wrapper那么在XML里要写${ew.customSqlSegment}才能把条件拼接进去。写漏了条件丢失看起来也是“分页没生效”因为数据量变多了。正确姿势是IPageUser selectUserPage(PageUser page, Param(Constants.WRAPPER) WrapperUser wrapper);XML里select idselectUserPage resultType... select * from user ${ew.customSqlSegment} /select3.4 原因四物理分页SQL和插件叠buff这个问题隐蔽性很高。有的项目之前没有分页插件开发人员在SQL里手写了LIMITselect idselectPage resultType... select * from user limit #{offset}, #{size} /select引入MP之后这段SQL又被分页插件拦截改写结果生成了limit limit这种畸形SQL或者插件改写后参数错乱直接报语法错误。我当时遇到一个现场pageSize传10结果返回了20条数据追查下去发现就是原生limit和分页插件互相叠加了。排查方法在XML里搜一下有没有手写limit#{offset}这种关键字。有的话要么删掉原生limit全交给插件处理要么这个查询不走Page参数直接用List接收保留原生SQL。记住一个原则同一句SQL里物理分页只能有一个实现方式别混用。3.5 原因五返回类型与IPage使用姿势错误分页方法可以返回IPage也可以返回List但两种方式分页插件的处理逻辑有区别。如果你用List接收返回结果Page对象还是照样有效但插件的分页SQL只负责查询数据总条数不会自动查。很多人用List接收后发现total里没有值或者压根没生成count查询就以为是分页失效。这里给大家一个统一的规范分页查询方法的返回值一律用IPage。这样MP会自动执行count同时把records封装进IPage对象里total、current、size全都有了。用List接收等于自己把count信息丢掉了还容易出现“看起来分页没生效”的误会。如果在自定义DTO查询里不想创建实体类也可以用IPageMapString, Object没毛病。3.6 排查思路总结把分页失效问题整理成了一张速查表遇到问题直接对照排查比从头查快很多现象优先级排查方向SQL没有LIMIT高分页插件是否注册、能否被扫描到返回条数对不上高手写limit与插件叠加count结果错误总条数total为0中返回类型是否用了List没用IPageSQL语法错误中count SQL重写失败检查复杂SQL条件丢失中自定义SQL里${ew.customSqlSegment}是否拼接多数据源场景中每个数据源都要注册分页插件4. 单页500条限制新版本分页插件的一个大坑4.1 这个限制是从哪来的最近很多群友问项目明明配置了分页为什么pageSize传600就报错了说超过单页最大限制。这个问题从MyBatisPlus 3.5.9版本开始出现分页插件里新增了一个MaxLimitHandler机制默认单页查询最大行数限制为500条。这个设计本意是防止有人恶意查询超大分页比如pageSize传100万直接把数据库拖垮。之前没有限制的年代分页插件对pageSize大小没有任何约束传多少就limit多少。加这个默认限制后pageSize超过500直接抛异常。很多老项目一升级MP版本就突然冒出这个报错就是这个原因。4.2 典型报错现场报错信息长什么样大家先认一下Error querying database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: single page maximum limit is 500或者类似“single page maximum limit is xxx”的信息。看到这个关键字第一反应就是命中MaxLimitHandler了。这里我要先给一个判断标准你的业务真的需要单页超过500条吗如果是前端表格展示、分页器翻页单页500条已经属于比较大的容量了。但如果是内部系统导出数据、接口对接批次拉取或者某些管理后台的“查看全部”功能500条限制确实会让业务卡住。4.3 解决办法一InterceptorIgnore快逃MP提供了一个注解可以在Mapper方法上跳过部分拦截器处理。针对分页可以在方法上标注InterceptorIgnore(maxLimit 0) IPageOrder selectOrderPage(PageOrder page, Param(Constants.WRAPPER) WrapperOrder queryWrapper);maxLimit 0表示这条SQL不参与单页限制拦截pageSize想传多少传多少。这个注解的原理是只对该方法生效其他方法仍然保留默认限制。个人建议在确实需要超大分页的接口上单独使用不要全局放开。因为全局放开等于把这个安全机制架空了万一有人恶意传超大分页数据库压力会直接爆炸。4.4 解决办法二自定义MaxLimitHandler如果你用的版本已经支持自定义的它可以在配置分页插件时传入自定义限制值PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L);这里setMaxLimit(500L)设置的是单页最大限制注意默认就是500。如果你觉得默认值不够调大到1000或者2000都可以接受。但要考虑性能MySQL的limit偏移量一大全表扫描的代价是很高的。比如pageSize2000currentPage10实际数据库要扫描offset18000行再返回2000行这性能能好到哪里去所以更好的姿势不是无限调大限制而是业务上避免超大分页。4.5 解决办法三全局配置与新旧版本差异全局配置方式是通过配置类设置PaginationInnerInterceptor的maxLimit相当于所有分页查询都受影响。但是这里有个版本差异要注意MyBatisPlus 3.5.9以前的版本PaginationInnerInterceptor没有maxLimit这个概念3.5.9及以后版本才默认限制500。所以如果你没升级版本项目分页一直都好好的同样代码升级到3.5.9突然就报单页500限制错误多半是你之前pageSize传过很大数值的接口在报警。升级之后想全局关闭限制最简单的处理是做一个自定义限制的Handler把限制值调成一个业务上不可能到达的巨大值比如Long.MAX_VALUE。但强烈不建议这么做。更好的做法是先审计一遍所有接口的pageSize调用把不合理的分页参数修正再针对导出、批量拉取这种确有大分页需求的接口单独InterceptorIgnore。这套组合拳下来既能保证系统稳定又不受默认限制影响。5. 真实项目的其他高频配置和优化经验5.1 逻辑删除和填充器的坑逻辑删除是标配实体类字段加TableLogic删除操作自动变成update查询自动追加deleted0。但有几个坑得注意。第一逻辑删除后唯一索引字段容易冲突比如用户名是唯一索引用户删了记录还在下次注册同名用户就冲突。解决办法一般是把逻辑删除字段纳入联合唯一索引或者用时间戳处理。第二3.5.9之前版本逻辑删除字段如果没在全局配置里指定logic-delete-field每张表都要在实体类上注解漏掉一张表删除就变物理删除了这个相当危险。填充器是另一个容易被忽略的功能。TableField(fill FieldFill.INSERT)配合MetaObjectHandler可以自动填充createTime、updateTime、creator这些字段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()); } }这里要特别注意strictInsertFill和strictUpdateFill的用法字段类型必须写正确否则填充不生效。一旦填充分配好新建和修改数据的接口再也不用手动set时间字段了减少漏填的几率。5.2 多租户插件和分页的联动如果你的项目要做SaaS多租户隔离TenantLineInnerInterceptor会自动在SQL后面拼上tenant_id条件。这个插件和分页插件一起配合要注意插件的注册顺序。TenantLineInnerInterceptor要在PaginationInnerInterceptor之前注册因为SQL先加好租户条件再走分页改写这样才能保证count统计也是租户隔离后的数据。如果某些表不需要租户隔离比如字典表、全局配置表要单独配置TenantLineHandler忽略这些表。现实中我遇到过一个坑联表查询里只给主表加了租户条件从表没加导致租户A查到了租户B的数据。这其实是SQL层面条件遗漏问题使用多租户插件时要仔细检查生成的SQL是否在所有业务表上都加了租户条件。在代码里想办法打印实际执行SQL多看一眼能省掉线上事故。5.3 查询性能优化的几个实测技巧MP生成SQL的能力没问题但性能还是得靠使用者本身把控。这里分享几个实测过的优化方向。第一个是避免SELECT *。BaseMapper的selectById、selectList默认查询所有字段但很多场景只需要两三个字段全字段查询会浪费IO和带宽。可以用select方法指定字段。LambdaQueryWrapper里用select指定列或者写自定义SQL都有助于减少不必要的数据传输。第二个是超大查询避免IN特别长的集合。比如一个批处理任务查询几千个idIN条件里塞几千个参数数据库执行计划很容易出问题。建议分批处理每批500个id效率高很多。第三个是用好数据库索引。eq、like前模糊匹配这类检索条件如果没有对应索引数据量大起来MP再方便也是白搭。建议直接利用执行计划EXPLAIN分析一下MP生成的SQL确认走的索引是不是最合理的。5.4 代码生成器新模块开发提速的关键最后说下代码生成器。MyBatisPlus的AutoGenerator可以基于数据库表生成实体、Mapper、Service、ServiceImpl、Controller、XML全套文件。很多人觉得生成器会生成冗余代码但实际用起来尤其是在中后台管理系统里生成的Controller和Service能直接跑通一套基础CRUD再改改查询条件和业务逻辑就行新手也能快速上手。新版本配置代码稍微多一点但核心逻辑不变。强调一个使用体会生成完之后务必要自己再检查一遍实体类字段和数据库字段的映射。生成器是根据表结构反向生成的如果表里字段注释写得不全生成的注释也没啥参考价值还是得自己补上业务含义。一个字段一个坑字段注释写清楚后面接手的同事会感谢你。6. 常见问题速查表做一个速查表把上面所有经验浓缩到这里方便随时查阅。问题原因解决方案分页完全没生效没有LIMIT缺少分页插件配置添加MybatisPlusInterceptor注册PaginationInnerInterceptor分页条数不对手写LIMIT与插件叠加删除原生limit保留插件分页total为0方法返回类型用了List改成返回IPagecount SQL报错复杂SQL导致自动count失败手动指定count或改写SQL结构单页超过500条报错3.5.9版本默认500限制InterceptorIgnore(maxLimit0)或调大maxLimit局部字段更新为NULL失败updateById忽略null字段改用LambdaUpdateWrapper的set方法逻辑删除字段失效实体未加TableLogic补上注解并检查全局配置多租户数据串号从表未加租户条件检查各表的租户拦截配置查询全字段性能差默认SELECT *使用select指定字段优化索引特别提示分页是数据库访问的高频动作一旦出现性能问题优先打开慢SQL日志定位具体SQL。MP生成的SQL看起来简单但join多、条件多时同样可能执行计划崩坏不能用“框架帮我搞定”的心态忽视SQL本身的调优。做后端开发这些年我越发觉得框架只是工具真正决定系统质量的还是使用者的理解深度。MyBatisPlus给我最大的帮助不是省了多少行代码而是把重复劳动压缩到极致让我把精力放在真正的业务逻辑和SQL性能优化上。也希望这篇稿子能帮你在分页、限制、逻辑删除、多租户这些关键点上少走弯路直接抄作业。
返回列表