MyBatis-Plus动态查询实战:用QueryWrapper优雅处理前端传来的多条件筛选(附分页)
2026/6/13 2:40:52 网站建设 项目流程

MyBatis-Plus动态查询实战:用QueryWrapper优雅处理前端传来的多条件筛选(附分页)

在企业级后台管理系统开发中,动态条件查询是最常见也最考验开发者功底的场景之一。想象这样一个典型需求:HR系统需要支持按部门筛选、按姓名模糊匹配、按入职时间范围查询员工信息,这些条件可能任意组合,甚至包含嵌套逻辑。本文将深入探讨如何利用MyBatis-Plus的QueryWrapper构建灵活、安全的动态查询体系。

1. 动态查询基础架构设计

动态查询的核心在于将前端不确定的查询条件转化为后端可执行的SQL语句。MyBatis-Plus的QueryWrapper提供了比原生MyBatis更优雅的解决方案。我们先看一个基础实现框架:

@GetMapping("/employees") public Page<Employee> 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) { QueryWrapper<Employee> 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(QueryWrapper<Employee> 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条件的情况,比如用户可多选部门:

List<String> selectedDepts = Arrays.asList("HR", "Finance", "IT"); QueryWrapper<Employee> qw = new QueryWrapper<>(); selectedDepts.forEach(dept -> qw.or(w -> w.eq("department_id", dept))); qw.like("name", "张");

生成的SQL:

WHERE ((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);

对应SQL:

WHERE ((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对象已经封装了分页逻辑:

Page<Employee> page = new Page<>(pageNum, pageSize); page.setSearchCount(true); // 是否查询总记录数 IPage<Employee> result = employeeService.page(page, queryWrapper); // 返回结果包含 // result.getRecords() - 当前页数据 // result.getTotal() - 总记录数 // result.getPages() - 总页数

4.2 大数据量分页优化

当数据量超过百万时,传统LIMIT offset, size方式效率低下。可以采用"游标分页":

// 第一页查询 qw.orderByAsc("id").last("LIMIT 100"); List<Employee> 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 Page<Employee> queryEmployeesWithCache(..., int pageNum, int pageSize) { // 查询逻辑 }

5. 安全防护与最佳实践

5.1 SQL注入防护

虽然QueryWrapper已经做了基础防护,但仍需注意:

  • 避免直接拼接SQL片段
  • 复杂动态排序应使用白名单校验:
private static final Set<String> 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, method="query", 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是否符合预期。

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

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

立即咨询