《MyBatis-Plus 排序的“隐形冲突”:Page OrderItem、Wrapper.orderBy 与 Mapper.xml 写死 ORDER BY 的优先级裁决与源码剖析》
- 现象:三条排序路径,谁说了算?
- 源码定位
- PaginationInnerInterceptor.concatOrderBy 与 addOrderByElements
- 实践建议:不要在三个地方同时写排序
- 总结
现象:三条排序路径,谁说了算?
在 MyBatis-Plus 的分页查询中,排序条件可能从三个完全不同的地方进入最终 SQL:
**路径一:Page 对象的 ascs和descs或者OrderItem(3.5+) **
// MybatisPlus 3.5+版本Page<User>page=newPage<>(1,10);page.addOrder(OrderItem.desc("create_time"));路径二:Wrapper 的 orderByAsc/Desc
LambdaQueryWrapper<User>wrapper=Wrappers.lambdaQuery();wrapper.orderByDesc(User::getCreaTime);路径三:Mapper.xml 中写死的 ORDER BY
<select id="selectPageList"resultType="User">SELECT*FROMuserWHEREdeleted=0ORDERBYcreate_timeDESC</select>一个常被观察到的现象是:Page 中的 OrderItem 和 Mapper.xml 中的写死 ORDER BY 可以“共存”,最终 SQL 里两者的排序都会出现;但如果在 Wrapper 上调用了 orderByAsc/Desc,而 Mapper.xml 里又硬编码了 ORDER BY,却可能抛出 SQL 语法异常。要理解这种差异,必须看清分页插件在底层到底把谁拼在了谁前面。
优先级结论:Page OrderItem > Wrapper.orderBy > Mapper.xml 写死 ORDER BY
在 MyBatis-Plus 的 PaginationInnerInterceptor 介入后,三条排序来源的实际效果遵循以下优先级:
Page 的 OrderItem 优先级最高,它会被主动拼接到原有 SQL 的 ORDER BY 之前,相当于“前置排序”,从而在效果上覆盖 Wrapper 和 Mapper.xml 中的排序。这也是一些资料直接表述“通过 Page 对象设置排序,优先级最高”的原因。
Wrapper 的 orderBy 次之。当 Wrapper 通过 ${ew.customSqlSegment} 或类似机制注入 SQL 时,它的 ORDER BY 片段会进入“原有 SQL”的排序部分,因此会被 Page 的 OrderItem 前置覆盖。
Mapper.xml 中写死的 ORDER BY 优先级最低。它天然存在于原始 SQL 中,任何后置拼接的排序都会对它产生覆盖或冲突效应。
源码定位
PaginationInnerInterceptor.concatOrderBy 与 addOrderByElements
分页插件的排序拼接逻辑,核心落在 com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor 中。
在分页查询执行前,如果 Page 对象的 orders 集合不为空,插件会调用 concatOrderBy(buildSql, orders) 将排序条件注入 SQL。该方法内部使用 JSqlParser 解析原始 SQL,取出已有的 OrderByElements,再调用 addOrderByElements(orderList, orderByElements) 完成合并。
addOrderByElements 的源码逻辑决定了最终的优先级顺序。它先从 Page 的 OrderItem 列表生成一组 additionalOrderBy,然后:
if(CollectionUtils.isEmpty(orderByElements)){returnadditionalOrderBy;}additionalOrderBy.addAll(orderByElements);returnadditionalOrderBy;也就是说,Page 的 OrderItem 被放在了原有 OrderByElements 之前。源码中的注释也印证了这一点:“优化排序,比如:默认 order by id 前端传了 name 排序,设置为 order by name,id”。这正是 Page 的 OrderItem 能够“覆盖”原有排序的机制根源:它不是删除原有排序,而是把自己的排序插到最前面,让原有排序降级为兜底排序。
至于 Wrapper 与 Mapper.xml 的冲突,问题不在于 concatOrderBy,而在于 Wrapper 的 ORDER BY 如何进入 SQL。当 Wrapper 通过 **${ew.customSqlSegment} **注入时,如果 Mapper.xml 中已经手写了 ORDER BY,两者在 SQL 层面并列出现,形成 ORDER BY … ORDER BY … 的非法结构,数据库自然报错。这不是优先级谁高谁低的问题,而是 SQL 语法不允许两个顶层 ORDER BY 子句同时存在。
实践建议:不要在三个地方同时写排序
分页查询统一使用 Page 的 OrderItem 管理排序。 PaginationInnerInterceptor 对 Page.orders 的处理路径最清晰,排序会稳定地前置到最终 SQL 中,且不会与 Wrapper 或 XML 的排序结构产生语法层面的不可预期行为。
如果 Mapper.xml 中已经有写死的 ORDER BY,不要再在 Wrapper 中追加 orderBy。 二者共存的风险不是“谁覆盖谁”,而是可能直接产生非法 SQL。要么把排序完全移到 Page 中,要么把 Mapper.xml 中的 ORDER BY 移除,只保留一个排序入口。
如果 Mapper.xml 中已经有写死的 ORDER BY,不要再在 Wrapper 中追加 orderBy。 例如:Mapper.xml 中写 ORDER BY id DESC 作为默认兜底,Page.addOrder 在前端传入排序字段时动态注入,最终效果是“动态排序优先、默认排序兜底”。如果改用 Wrapper 做动态排序,就会与 Mapper.xml 的 ORDER BY 正面相撞。
排序来源越多,SQL 的可预期性越低。在 MyBatis-Plus 的分页体系中,Page 的 OrderItem 是分页插件“亲儿子”级别的排序入口,把排序权集中到这里,比在 Wrapper 和 XML 之间猜测覆盖关系要可靠得多。
总结
Page的orderItem优先级最高,会覆盖Wrapper,Page的orderItem会排在Mapper.xml写死的ORDER BY之前。