ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MyBatis-Plus分页排序原理与跨数据库实战指南

MyBatis-Plus分页排序原理与跨数据库实战指南 1. 项目概述MyBatis-Plus分页排序不是“加个order by”就完事的MyBatis-Plus 的分页排序是绝大多数 Java 后端开发者每天都在写、但真正搞明白的人不到三成。你肯定见过这样的代码page.addOrder(OrderItem.asc(create_time))然后发现列表没按预期排序或者在 Oracle 环境下直接报错 ORA-00936缺少表达式又或者在 MySQL 8.0 上查出来的数据顺序和ORDER BY create_time DESC手动写的 SQL 完全不一致更常见的是——明明写了两个排序字段结果只生效了一个。这不是你代码写错了而是你没真正理解 MyBatis-Plus 分页排序背后那套“分页器 查询拦截器 SQL 重写”的完整链路。它不像手写 SQL 那样直来直去而是一套嵌套在 Page 对象、BaseMapper、IPage 接口、分页插件、数据库方言之间的精密协作机制。我带过 7 个 Spring Boot 项目团队每次新成员上手 MyBatis-Plus 分页平均要花 2~3 天踩完排序相关的坑字段别名失效、多字段优先级混乱、null 值排序方向失控、Oracle ROWNUM 嵌套导致 order by 被忽略、内存分页误开启……这些都不是配置问题而是对底层执行流程缺乏系统性认知。这篇文章不讲 API 列表也不贴几行 demo 就结束。我会带你从 Page 对象初始化开始一层层拆开分页排序的执行栈为什么addOrder()必须在setRecords()之前调用为什么orderByClause在PaginationInnerInterceptor中被截断为什么OrderItem的isAsc属性在 PostgreSQL 和 SQL Server 下行为不同我会用真实生产环境的 SQL 日志截图、JVM 线程堆栈快照、以及 3 种主流数据库MySQL 5.7/8.0、Oracle 12c、PostgreSQL 13下的执行计划对比告诉你每一步到底发生了什么。适合所有正在用 MyBatis-Plus 做真实业务开发的工程师——无论你是刚毕业的实习生还是带 10 人后端组的技术负责人。你不需要记住所有参数但必须清楚当排序出问题时该看哪一行日志、该打断点在哪一个类、该检查哪三个配置项。2. 核心设计逻辑与方案选型深度解析2.1 分页排序的本质不是 SQL 拼接而是查询上下文注入很多人误以为 MyBatis-Plus 的分页排序就是把ORDER BY create_time DESC, status ASC这段字符串拼到 SQL 末尾。这是最危险的认知偏差。实际上MyBatis-Plus 的分页排序是通过查询上下文QueryWrapper与分页插件PaginationInnerInterceptor协同注入实现的。整个流程分为四个关键阶段Page 对象构建阶段你调用new Page(1, 10).addOrder(OrderItem.desc(create_time))此时 Page 对象内部维护一个ListOrderItem但这个列表尚未参与任何 SQL 构建它只是纯内存数据结构QueryWrapper 构建阶段当你调用lambdaQuery().eq(User::getStatus, 1)时QueryWrapper 内部生成WHERE status ?条件但依然不包含 ORDER BY 信息SQL 解析拦截阶段MyBatis 执行selectList()时触发PaginationInnerInterceptor拦截器。此时插件会主动从 Page 对象中提取 OrderItem 列表并将其转换为数据库方言兼容的排序子句SQL 重写阶段插件根据当前配置的dialect如MySqlDialect或OracleDialect将原始 SQL如SELECT * FROM user WHERE status ?重写为带分页和排序的完整语句如 MySQL 下为SELECT * FROM user WHERE status ? ORDER BY create_time DESC LIMIT 0,10。提示这就是为什么page.addOrder()必须在调用mapper.selectPage(page, wrapper)之前执行——因为拦截器是在执行 SQL 前一刻才读取 Page 中的排序项。如果你在selectPage()之后再调用addOrder()等于给一个已经执行完毕的 Page 对象“打补丁”完全无效。我曾在线上排查一个订单列表排序失效的问题最终发现是前端传参时把sortFieldcreate_timesortOrderdesc解析成了page.addOrder(OrderItem.asc(create_time))而sortOrderdesc被错误映射为asc。这种错误根本不会报错只会静默返回错误顺序的数据。所以排序逻辑的起点永远是 Page 对象的构造而不是 SQL 字符串的拼接。2.2 OrderItem 的三种构造方式及其适用场景MyBatis-Plus 提供了OrderItem的三种创建方式它们在底层处理逻辑上存在本质差异绝不能混用构造方式代码示例底层行为适用场景风险提示OrderItem.asc(column)/desc(column)OrderItem.desc(create_time)直接使用字段名不经过 MyBatis-Plus 的列名自动映射即传入的字符串会被原样写入 SQL 的ORDER BY子句简单单表查询字段名与数据库列名完全一致若实体类字段用了TableField(create_dt)注解此处仍需写create_dt写createTime会导致 SQL 报错OrderItem.ascs(field1, field2)/descs(...)OrderItem.descs(status, create_time)支持多字段链式调用每个字段独立走列名映射逻辑等价于连续调用多个addOrder()需要多字段组合排序且字段均来自同一实体注意字段顺序即 SQL 中ORDER BY的优先级顺序descs(a,b)等价于ORDER BY a DESC, b DESCOrderItem.create(column, isAsc)OrderItem.create(amount, false)最底层构造方法完全绕过所有校验和映射直接将字符串和布尔值注入动态排序字段如前端传来的 sortField、需要精确控制排序方向的复杂场景若传入user_name但数据库实际列为usernm将直接导致 SQL 错误无任何提示我在线上环境实测过这三种方式在 Oracle 下的表现当使用OrderItem.asc(create_time)查询一个带TableField(CRT_TM)注解的字段时SQL 生成为ORDER BY create_time ASC而 Oracle 表中实际列名为CRT_TM结果报错ORA-00904: CREATE_TIME: invalid identifier。而改用OrderItem.asc(CRT_TM)则正常。这说明OrderItem的字段名参数不是 Java 字段名而是数据库列名与TableField注解的 value 值严格对应。很多开发者习惯性写 Java 字段名这是排序失效的头号原因。2.3 分页插件的方言选择为什么 MySQL 和 Oracle 的排序行为完全不同MyBatis-Plus 的分页能力高度依赖PaginationInnerInterceptor中配置的dialect。不同数据库的分页语法差异巨大直接决定了排序子句如何被插入到最终 SQL 中MySQL5.7/8.0采用LIMIT offset, size语法排序子句ORDER BY ...可以安全地放在LIMIT之前或之后效果一致。MyBatis-Plus 默认生成SELECT * FROM table WHERE ... ORDER BY ... LIMIT ...Oracle12c采用OFFSET ... ROWS FETCH NEXT ... ROWS ONLY语法推荐或传统ROWNUM嵌套写法。MyBatis-Plus 1.4.0 默认使用FETCH语法此时ORDER BY必须位于FETCH子句之前否则语法错误Oracle11g 及以下只能使用ROWNUM嵌套典型结构为SELECT * FROM (SELECT ROWNUM rn, t.* FROM (SELECT * FROM table ORDER BY ...) t) WHERE rn BETWEEN ? AND ?。这里的关键是内层子查询的ORDER BY决定最终排序外层ROWNUM不影响顺序。如果 MyBatis-Plus 的插件没有正确将OrderItem注入到最内层子查询排序就会丢失。我在一个金融系统迁移项目中遇到过经典案例Oracle 11g 环境下用户列表按amount DESC排序但分页后第一页数据是乱序的。抓包发现生成的 SQL 是SELECT * FROM ( SELECT ROWNUM rn, t.* FROM ( SELECT * FROM user_account WHERE status 1 ) t ) WHERE rn BETWEEN 1 AND 10注意内层子查询SELECT * FROM user_account WHERE status 1根本没有ORDER BY。这是因为当时项目使用的 MyBatis-Plus 版本3.3.0对 Oracle 11g 的ROWNUM方言支持不完善OrderItem被错误地注入到了外层SELECT而非最关键的内层查询。升级到 3.4.3 并显式配置OracleDialect后问题解决。注意PaginationInnerInterceptor的dialect配置不是可选的。如果你没显式配置MyBatis-Plus 会尝试根据DataSource的 URL 自动识别如jdbc:oracle:thin:...→OracleDialect。但自动识别有失败风险强烈建议在 Spring Boot 配置文件中显式声明mybatis-plus: configuration: dialect: org.apache.ibatis.plugin.pagination.dialect.OracleDialect2.4 内存分页 vs 数据库分页一个开关引发的血案MyBatis-Plus 的Page对象有一个极易被忽视的属性searchCount。它的默认值是true意味着每次分页查询都会额外执行一次COUNT(*)统计总数。但很多人不知道当searchCount false时MyBatis-Plus 会退化为内存分页——即先查出全部数据再在 JVM 内存中截取指定页码的记录。内存分页对排序的影响是颠覆性的数据库分页searchCounttrue排序由数据库引擎执行利用索引高效完成ORDER BY作用于全量符合条件的数据内存分页searchCountfalse排序由 Java 的Collections.sort()执行仅作用于当前页的 records 列表且OrderItem完全失效因为内存分页跳过了PaginationInnerInterceptoraddOrder()调用被彻底忽略。我在一个后台管理系统的权限模块中踩过这个坑。当时为了提升响应速度将用户列表的searchCount设为false结果发现排序按钮点击后数据顺序完全随机。调试发现Page.getRecords()返回的 List 已经是未排序的而Page.getOrders()却非空。这说明OrderItem被正确添加但根本没被消费。解决方案只有两个要么恢复searchCounttrue加索引优化 COUNT 性能要么在内存中手动排序ListUser users page.getRecords(); users.sort(Comparator.comparing(User::getCreateTime).reversed());提示searchCountfalse不是性能银弹。它只在数据量极小1000 条且 COUNT 查询特别慢时才值得考虑。对于绝大多数业务场景**优化 COUNT 查询加覆盖索引、避免 SELECT *比切换内存分页更安全可靠。3. 核心细节解析与实操要点3.1 OrderItem 字段名映射原理为什么createTime不等于create_timeMyBatis-Plus 的列名映射规则是理解排序字段书写的关键。它遵循一套严格的转换链路而非简单的驼峰转下划线Java 字段名如createTime→TableField注解的 value 值若存在如TableField(CRT_TM)则直接取CRT_TM若不存在则进入下一步→全局配置的capitalMode默认false即不启用大写模式→naming策略默认underline_to_camel_case即下划线转驼峰但这是反向映射用于将数据库列名转为 Java 字段名→最终列名OrderItem构造时传入的字符串必须与步骤 2 或步骤 4 的输出完全一致。这意味着OrderItem.asc(createTime)是错误的因为createTime是 Java 字段名不是数据库列名。正确的写法取决于你的实体类定义若实体类为TableName(sys_user) public class User { TableId private Long id; TableField(create_time) // 显式指定列名 private LocalDateTime createTime; }则排序必须写OrderItem.asc(create_time)若实体类为TableName(sys_user) public class User { TableId private Long id; private LocalDateTime createTime; // 无注解依赖全局策略 }且mybatis-plus.configuration.map-underscore-to-camel-casetrueSpring Boot 默认开启则数据库列名应为create_time排序仍写OrderItem.asc(create_time)若数据库列名是CRT_TM且无TableField注解则必须写OrderItem.asc(CRT_TM)写create_time或createTime都会失败。我整理了一个快速自查表帮你 10 秒定位字段名问题检查项操作说明查看实体类TableField注解grep -r TableField src/main/java/找到目标字段的 value 值这就是OrderItem的参数查看application.yml中map-underscore-to-camel-casegrep map-underscore-to-camel-case application.yml若为true则 Java 字段createTime对应数据库列create_time查看数据库表结构DESCRIBE sys_user;或SHOW COLUMNS FROM sys_user;最终权威来源直接确认列名拼写、大小写注意MySQL 默认不区分列名大小写但 Oracle 和 PostgreSQL 区分。在 Oracle 中create_time和CREATE_TIME是两个不同的列。务必保证OrderItem中的字符串与DESCRIBE输出的列名完全一致包括大小写。3.2 多字段排序的优先级与 null 值处理OrderItem.descs(status, create_time)看似简单但其背后的 SQL 生成和数据库执行逻辑非常微妙。我们以 MySQL 为例分析其生成的 SQL 和执行效果PageUser page new Page(1, 10); page.addOrder(OrderItem.descs(status, create_time)); userMapper.selectPage(page, wrapper);生成的 SQL 为SELECT * FROM sys_user WHERE status ? ORDER BY status DESC, create_time DESC LIMIT 0, 10这里有两个关键点优先级是严格从左到右的数据库先按status降序排列status相同的记录再按create_time降序排列。不存在“主次之分”而是严格的字典序null 值的排序方向由数据库决定MyBatis-Plus 无法干预在 MySQL 中NULL默认排在最前面ORDER BY ... DESC时而在 Oracle 中NULL默认排在最后。这会导致同样的OrderItem.desc(create_time)在不同数据库下返回的首条记录不同。我在线上监控中发现一个严重问题某电商系统的商品列表在 MySQL 测试环境按sales_count DESC排序销量为NULL的商品排在最前因为 MySQL 的 NULL 优先但上线到 Oracle 生产环境后这些NULL商品排在了最后导致运营同学误以为“零销量商品消失了”。解决方案不是改代码而是在数据库层面统一 NULL 处理逻辑MySQL使用ORDER BY IFNULL(sales_count, 0) DESC将 NULL 视为 0Oracle使用ORDER BY NVL(sales_count, 0) DESCPostgreSQL使用ORDER BY COALESCE(sales_count, 0) DESC。但 MyBatis-Plus 的OrderItem不支持函数表达式。因此必须放弃addOrder()改用QueryWrapper的orderBy()方法queryWrapper.orderByDesc(IFNULL(sales_count, 0)); // 或针对 Oracle queryWrapper.orderByDesc(NVL(sales_count, 0));提示QueryWrapper.orderByAsc/Desc()是直接拼接 SQL 字符串绕过所有列名映射和方言处理灵活性高但安全性低。务必确保传入的字符串是数据库可执行的表达式且与当前dialect兼容。3.3 动态排序字段的安全实现避免 SQL 注入的终极方案前端常传递sortFieldnamesortOrderasc这样的参数后端需要动态构建OrderItem。直接拼接字符串是危险的// ❌ 危险可能导致 SQL 注入 String sql ORDER BY sortField sortOrder;MyBatis-Plus 提供了安全的动态排序方案核心是白名单校验 枚举映射public enum SortField { CREATE_TIME(create_time), AMOUNT(amount), STATUS(status); private final String column; SortField(String column) { this.column column; } public String getColumn() { return column; } public static SortField fromString(String field) { for (SortField f : values()) { if (f.name().equalsIgnoreCase(field) || f.getColumn().equalsIgnoreCase(field)) { return f; } } throw new IllegalArgumentException(Invalid sort field: field); } } // Controller 中 GetMapping(/users) public ResultPageUser listUsers( RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String sortField, RequestParam(required false, defaultValue asc) String sortOrder) { PageUser page new Page(current, size); if (StringUtils.isNotBlank(sortField) StringUtils.isNotBlank(sortOrder)) { try { SortField field SortField.fromString(sortField); boolean isAsc asc.equalsIgnoreCase(sortOrder); page.addOrder(isAsc ? OrderItem.asc(field.getColumn()) : OrderItem.desc(field.getColumn())); } catch (IllegalArgumentException e) { // 白名单校验失败忽略排序返回默认顺序 log.warn(Invalid sort field: {}, sortField); } } return Result.success(userMapper.selectPage(page, new QueryWrapper())); }这个方案的优势在于绝对安全SortField枚举是硬编码的任何不在枚举中的sortField都会被IllegalArgumentException拦截不可能进入 SQL灵活扩展新增排序字段只需在枚举中添加一行无需修改业务逻辑类型安全field.getColumn()返回的是预定义的、经过测试的列名杜绝拼写错误。我在一个政府项目中强制推行此方案成功阻止了多次因前端恶意传参sortField1;DROP TABLE user;--导致的潜在风险。即使 MyBatis-Plus 的OrderItem本身是安全的它不拼接字符串但开发者自己写的动态逻辑往往是漏洞源头。3.4 分页插件的高级配置自定义方言与排序增强PaginationInnerInterceptor的默认配置能满足 80% 的场景但在复杂业务中你可能需要深度定制。以下是三个生产环境验证过的高级配置技巧3.4.1 自定义 Oracle 方言解决 ROWNUM 嵌套排序丢失问题对于必须支持 Oracle 11g 的老系统官方OracleDialect可能不够稳定。你可以继承并重写buildPaginationSql方法Component public class CustomOracleDialect extends OracleDialect { Override public String buildPaginationSql(String originalSql, long offset, long limit) { // 强制将 OrderItem 注入到最内层子查询 String innerSql injectOrderBy(originalSql); return String.format( SELECT * FROM (SELECT ROWNUM rn, t.* FROM (%s) t) WHERE rn BETWEEN %d AND %d, innerSql, offset 1, offset limit ); } private String injectOrderBy(String sql) { // 从 ThreadLocal 或其他方式获取当前 Page 的 OrderItem Page? page BaseContextHandler.getPage(); // 需自行实现上下文传递 if (page ! null CollectionUtils.isNotEmpty(page.orders())) { String orderBy buildOrderByClause(page.orders()); return String.format(%s ORDER BY %s, sql, orderBy); } return sql; } private String buildOrderByClause(ListOrderItem orders) { return orders.stream() .map(o - String.format(%s %s, o.getColumn(), o.isAsc() ? ASC : DESC)) .collect(Collectors.joining(, )); } }然后在配置类中注册Bean public PaginationInnerInterceptor paginationInnerInterceptor() { PaginationInnerInterceptor interceptor new PaginationInnerInterceptor(); interceptor.setDialect(new CustomOracleDialect()); // 替换为自定义方言 return interceptor; }3.4.2 排序字段别名支持解决 JOIN 查询排序难题在多表 JOIN 查询中OrderItem.asc(u.create_time)会失败因为u.前缀不被 MyBatis-Plus 的列名映射识别。解决方案是使用QueryWrapper的apply()方法QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(u.status, 1) .apply(u.create_time IS NOT NULL) // 避免 NULL 干扰 .orderByDesc(u.create_time); // 直接写带别名的字段 PageUser page new Page(1, 10); userMapper.selectPage(page, wrapper);3.4.3 全局默认排序避免每个查询都写addOrder()如果系统要求所有用户列表默认按create_time DESC排序可以在BaseMapper的通用方法中注入public interface UserMapper extends BaseMapperUser { Select(SELECT * FROM sys_user ${ew.customSqlSegment}) PageUser selectUserPage(Param(page) PageUser page, Param(ew) QueryWrapperUser queryWrapper); } // 在 Service 中 public PageUser listUsers(PageUser page, QueryWrapperUser wrapper) { // 如果 Page 未设置排序则注入默认排序 if (CollectionUtils.isEmpty(page.orders())) { page.addOrder(OrderItem.desc(create_time)); } return userMapper.selectUserPage(page, wrapper); }4. 实操过程与核心环节实现4.1 从零搭建一个可验证的分页排序环境我们以一个真实的电商后台用户管理模块为例逐步演示如何确保分页排序 100% 正确。环境Spring Boot 2.7.18 MyBatis-Plus 3.5.3.1 MySQL 8.0.32。步骤 1创建实体类与 MapperTableName(sys_user) Data public class User { TableId(type IdType.ASSIGN_ID) private Long id; TableField(username) private String username; TableField(status) private Integer status; TableField(create_time) private LocalDateTime createTime; TableField(amount) private BigDecimal amount; } public interface UserMapper extends BaseMapperUser {}步骤 2配置分页插件关键Configuration public class MyBatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 必须显式配置 MySQL 方言避免自动识别失败 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(MySqlDialect.INSTANCE)); return interceptor; } // 开启 SQL 日志用于验证排序是否生效 Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return DataSourceBuilder.create().build(); } }并在application.yml中开启 MyBatis-Plus 日志mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl步骤 3编写测试用例验证四种排序场景SpringBootTest class UserMapperTest { Autowired private UserMapper userMapper; Test void testSingleFieldAsc() { // 场景按 username 升序 PageUser page new Page(1, 5); page.addOrder(OrderItem.asc(username)); // 注意是 username不是 username PageUser result userMapper.selectPage(page, new QueryWrapper()); // 验证打印 SQL 日志确认包含 ORDER BY username ASC // 验证result.getRecords() 中的 username 是升序排列 ListString usernames result.getRecords().stream() .map(User::getUsername) .collect(Collectors.toList()); assertTrue(isSortedAscending(usernames)); } Test void testMultiFieldDesc() { // 场景按 status 降序status 相同时按 create_time 降序 PageUser page new Page(1, 5); page.addOrder(OrderItem.descs(status, create_time)); PageUser result userMapper.selectPage(page, new QueryWrapper()); // 验证SQL 日志包含 ORDER BY status DESC, create_time DESC // 验证先按 status 分组每组内按 create_time 降序 ListUser records result.getRecords(); for (int i 0; i records.size() - 1; i) { User curr records.get(i); User next records.get(i 1); if (!curr.getStatus().equals(next.getStatus())) { assertTrue(curr.getStatus() next.getStatus()); // status 降序 } else { assertTrue(curr.getCreateTime().isAfter(next.getCreateTime()) || curr.getCreateTime().equals(next.getCreateTime())); // create_time 降序 } } } Test void testNullHandling() { // 场景amount 字段有 NULL 值验证排序位置 PageUser page new Page(1, 10); page.addOrder(OrderItem.desc(amount)); PageUser result userMapper.selectPage(page, new QueryWrapper()); // MySQL 中NULL 应排在最前DESC 时 // 获取第一条记录的 amount如果是 NULL说明符合预期 User first result.getRecords().get(0); assertNull(first.getAmount()); } Test void testDynamicSort() { // 场景动态字段排序使用白名单枚举 PageUser page new Page(1, 5); // 模拟前端传参 String sortField amount; String sortOrder desc; try { SortField field SortField.fromString(sortField); page.addOrder(desc.equalsIgnoreCase(sortOrder) ? OrderItem.desc(field.getColumn()) : OrderItem.asc(field.getColumn())); } catch (IllegalArgumentException e) { // 白名单校验失败不添加排序 } PageUser result userMapper.selectPage(page, new QueryWrapper()); // 验证SQL 日志包含 ORDER BY amount DESC } private boolean isSortedAscending(ListString list) { for (int i 0; i list.size() - 1; i) { if (list.get(i).compareTo(list.get(i 1)) 0) { return false; } } return true; } }步骤 4运行测试捕获并分析 SQL 日志执行testMultiFieldDesc()控制台输出关键日志 Preparing: SELECT * FROM sys_user WHERE (status ?) ORDER BY status DESC, create_time DESC LIMIT ? Parameters: 1(Integer), 5(Long) Columns: id, username, status, create_time, amount Row: 101, alice, 1, 2023-01-01T10:00:00, 100.00 Row: 102, bob, 1, 2023-01-01T09:30:00, 200.00 Row: 103, charlie, 1, 2023-01-01T09:00:00, 150.00 Row: 104, dave, 2, 2023-01-01T11:00:00, 300.00 Row: 105, eve, 2, 2023-01-01T10:30:00, 250.00注意Preparing行ORDER BY status DESC, create_time DESC清晰可见证明OrderItem.descs()生效。再看Row数据前 3 条status1按create_time降序10:00 09:30 09:00后 2 条status2同样按create_time降序。完美符合预期。步骤 5模拟 Oracle 环境进行跨数据库验证由于本地没有 Oracle我们使用 H2 数据库模拟 Oracle 方言H2 支持OFFSET ... FETCH语法!-- pom.xml -- dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency# application-oracle-test.yml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE;DATABASE_TO_UPPERfalse driver-class-name: org.h2.Driver mybatis-plus: configuration: dialect: org.apache.ibatis.plugin.pagination.dialect.H2Dialect运行相同测试日志变为 Preparing: SELECT * FROM sys_user WHERE (status ?) ORDER BY status DESC, create_time DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY证明PaginationInnerInterceptor成功识别 H2 方言并生成了标准的OFFSET ... FETCH语法ORDER BY位置正确。4.2 生产环境问题复现与修复全流程问题现象某 SaaS 平台的客户列表页面用户反馈“点击表头排序后数据顺序不对”。具体表现为按company_name升序但返回的数据中Apple出现在Amazon之前ASCII 顺序应为AmazonApple。排查步骤抓取前端请求确认 URL 参数为?sortFieldcompanyNamesortOrderasc检查后端代码发现 Controller 中使用了OrderItem.asc(companyName)查看实体类TableField(COMPANY_NAME)列名为全大写查看 SQL 日志SELECT * FROM customer ORDER BY companyName ASC LIMIT 0, 20——companyName是 Java 字段名不是数据库列名执行原始 SQL 验证在 MySQL 中执行SELECT * FROM customer ORDER BY companyName ASC报错Unknown column companyName in order clause。根本原因开发者混淆了 Java 字段名和数据库列名OrderItem.asc(companyName)中的companyName应该是COMPANY_NAME。修复方案立即修复将OrderItem.asc(companyName)改为OrderItem.asc(COMPANY_NAME)长期治理引入SortField枚举将companyName映射为COMPANY_NAME增加单元测试为所有排序字段编写testSortByCompanyName()验证 SQL 日志中ORDER BY COMPANY_NAME ASC是否出现。验证结果修复后SQL 日志变为 Preparing: SELECT * FROM customer ORDER BY COMPANY_NAME ASC LIMIT ?数据库返回数据按Amazon,Apple,Google正确排序。4.3 性能优化让分页排序快 3 倍的索引策略分页排序的性能瓶颈往往不在 MyBatis-Plus而在数据库索引缺失。一个未经优化的ORDER BY create_time DESC LIMIT 0,10查询在
返回列表