MyBatis-Plus动态查询实战用QueryWrapper优雅处理前端传来的多条件筛选附分页在企业级后台管理系统开发中动态条件查询是最常见也最考验开发者功底的场景之一。想象这样一个典型需求HR系统需要支持按部门筛选、按姓名模糊匹配、按入职时间范围查询员工信息这些条件可能任意组合甚至包含嵌套逻辑。本文将深入探讨如何利用MyBatis-Plus的QueryWrapper构建灵活、安全的动态查询体系。1. 动态查询基础架构设计动态查询的核心在于将前端不确定的查询条件转化为后端可执行的SQL语句。MyBatis-Plus的QueryWrapper提供了比原生MyBatis更优雅的解决方案。我们先看一个基础实现框架GetMapping(/employees) public PageEmployee queryEmployees( RequestParam(required false) String deptId, RequestParam(required false) String nameKeyword, RequestParam(required false) LocalDate hireDateStart, RequestParam(required false) LocalDate hireDateEnd, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { QueryWrapperEmployee queryWrapper new QueryWrapper(); // 条件构建将在这里实现 buildQueryConditions(queryWrapper, deptId, nameKeyword, hireDateStart, hireDateEnd); return employeeService.page(new Page(pageNum, pageSize), queryWrapper); }关键设计要点所有查询参数都应设置为required false表示条件可选使用QueryWrapper而非原生MyBatis的Example因其链式调用更直观分页参数设置默认值避免空指针异常2. 条件构建的三种典型模式2.1 基础条件拼接最简单的AND条件连接适用于字段间的与关系private void buildQueryConditions(QueryWrapperEmployee qw, String deptId, String nameKeyword, LocalDate hireDateStart, LocalDate hireDateEnd) { if (StringUtils.isNotBlank(deptId)) { qw.eq(department_id, deptId); } if (StringUtils.isNotBlank(nameKeyword)) { qw.like(name, nameKeyword); } if (hireDateStart ! null hireDateEnd ! null) { qw.between(hire_date, hireDateStart, hireDateEnd); } else if (hireDateStart ! null) { qw.ge(hire_date, hireDateStart); } else if (hireDateEnd ! null) { qw.le(hire_date, hireDateEnd); } }注意字符串判断使用StringUtils.isNotBlank而非!null可以同时排除空字符串和纯空格情况2.2 嵌套OR条件处理当需要实现部门A且(姓名包含张或入职时间在2023年)这类复杂逻辑时qw.eq(department_id, A) .and(wrapper - wrapper .like(name, 张) .or() .between(hire_date, LocalDate.of(2023, 1, 1), LocalDate.of(2023, 12, 31)));对应的SQL输出WHERE (department_id A AND (name LIKE %张% OR hire_date BETWEEN 2023-01-01 AND 2023-12-31))2.3 动态OR条件组对于需要动态构建OR条件的情况比如用户可多选部门ListString selectedDepts Arrays.asList(HR, Finance, IT); QueryWrapperEmployee qw new QueryWrapper(); selectedDepts.forEach(dept - qw.or(w - w.eq(department_id, dept))); qw.like(name, 张);生成的SQLWHERE ((department_id HR) OR (department_id Finance) OR (department_id IT)) AND (name LIKE %张%)3. 高级条件组合技巧3.1 条件优先级控制使用nested方法可以精确控制条件分组qw.nested(nested - nested .eq(status, 1) .or() .gt(salary, 10000)) .lt(age, 30);对应SQLWHERE ((status 1 OR salary 10000) AND age 30)3.2 动态字段选择配合前端需要的字段控制避免查询不必要的数据String[] fields {id, name, department}; qw.select(fields) .eq(status, 1);3.3 条件判断优化推荐使用Java 8的Optional简化判空逻辑Optional.ofNullable(deptId).ifPresent(id - qw.eq(department_id, id)); Optional.ofNullable(nameKeyword) .filter(StringUtils::isNotBlank) .ifPresent(name - qw.like(name, name));4. 分页查询性能优化4.1 基础分页实现MyBatis-Plus的Page对象已经封装了分页逻辑PageEmployee page new Page(pageNum, pageSize); page.setSearchCount(true); // 是否查询总记录数 IPageEmployee result employeeService.page(page, queryWrapper); // 返回结果包含 // result.getRecords() - 当前页数据 // result.getTotal() - 总记录数 // result.getPages() - 总页数4.2 大数据量分页优化当数据量超过百万时传统LIMIT offset, size方式效率低下。可以采用游标分页// 第一页查询 qw.orderByAsc(id).last(LIMIT 100); ListEmployee firstPage employeeService.list(qw); // 获取最后一条记录的ID Long lastId firstPage.get(firstPage.size()-1).getId(); // 下一页查询 qw.gt(id, lastId).last(LIMIT 100);4.3 分页缓存策略对于相对静态的数据可引入缓存减少数据库压力Cacheable(value employeePage, key #root.methodName : #pageNum : #pageSize : #deptId) public PageEmployee queryEmployeesWithCache(..., int pageNum, int pageSize) { // 查询逻辑 }5. 安全防护与最佳实践5.1 SQL注入防护虽然QueryWrapper已经做了基础防护但仍需注意避免直接拼接SQL片段复杂动态排序应使用白名单校验private static final SetString ALLOWED_SORT_FIELDS Set.of(name, hire_date, salary); public void validateSortField(String field) { if (!ALLOWED_SORT_FIELDS.contains(field)) { throw new IllegalArgumentException(Invalid sort field); } }5.2 查询性能监控添加拦截器监控慢查询Intercepts(Signature(type StatementHandler.class, methodquery, args{Statement.class, ResultHandler.class})) public class SlowQueryInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); long time System.currentTimeMillis() - start; if (time 1000) { // 超过1秒视为慢查询 MappedStatement ms (MappedStatement) invocation.getArgs()[0]; log.warn(Slow query detected: {} took {}ms, ms.getId(), time); } return result; } }5.3 日志调试技巧开启MyBatis-Plus的SQL日志时建议配置格式化输出mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-sql: true sql-comment: false对于复杂查询可以临时获取完整SQL用于调试String sql queryWrapper.getSqlSegment(); log.debug(Generated SQL: {}, sql);在实际项目中我们发现最常出现的问题往往不是技术实现而是条件组合的逻辑错误。建议为复杂查询编写单元测试验证各种条件组合的输出SQL是否符合预期。