MyBatis-Plus实战:用selectMaps和selectObjs搞定复杂报表查询与数据导出
MyBatis-Plus实战:用selectMaps和selectObjs搞定复杂报表查询与数据导出
在企业级应用开发中,报表查询与数据导出是后台管理系统中最常见的需求之一。想象这样一个场景:产品经理突然要求开发一个用户数据导出功能,需要包含用户基础信息、所属部门名称(来自关联表)、最近30天订单数(聚合统计)等混合字段。传统做法可能需要编写复杂SQL或多次查询组装数据,而MyBatis-Plus的selectMaps和selectObjs方法却能优雅地解决这类问题。
1. 理解核心查询方法的应用场景
1.1 selectMaps:灵活的结果集映射
selectMaps方法返回List<Map<String, Object>>类型,这种设计在以下场景中特别有价值:
- 非标准化字段查询:当需要查询的字段不属于任何实体类时
- 动态字段选择:前端可配置需要返回的字段组合
- 聚合统计场景:包含AVG、COUNT、SUM等聚合函数的查询
// 典型selectMaps查询示例 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("id", "username", "department.name as deptName") .leftJoin("department", "user.dept_id = department.id") .eq("user.status", 1); List<Map<String, Object>> result = userMapper.selectMaps(wrapper);1.2 selectObjs:轻量级单列查询
selectObjs专注于获取单列数据,其特点包括:
- 高性能:只返回查询的第一个字段,减少数据传输量
- 批量ID处理:适合获取ID列表用于后续批量操作
- 内存优化:相比完整对象查询,内存占用更低
// 获取所有活跃用户ID QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("id").eq("status", 1); List<Object> userIds = userMapper.selectObjs(wrapper);2. 构建复杂报表查询方案
2.1 多表关联查询实现
假设我们需要查询用户信息及其部门名称:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("user.*", "department.name as deptName") .leftJoin("department", "user.dept_id = department.id") .apply("DATE(user.create_time) >= {0}", "2023-01-01"); List<Map<String, Object>> userList = userMapper.selectMaps(wrapper);注意:联表查询时建议明确指定字段别名,避免字段名冲突
2.2 聚合统计字段处理
对于需要统计的字段,可以直接在select中嵌入SQL函数:
wrapper.select("user.id", "user.username", "COUNT(order.id) as order_count", "SUM(order.amount) as total_amount") .leftJoin("order", "user.id = order.user_id") .groupBy("user.id");2.3 动态字段控制技巧
通过参数控制返回字段,实现灵活的报表配置:
public List<Map<String, Object>> exportUsers(List<String> columns) { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select(columns.toArray(new String[0])) .eq("status", 1); return userMapper.selectMaps(wrapper); }3. 高效数据导出实战
3.1 与EasyExcel集成方案
将查询结果直接转换为Excel导出:
// 准备表头映射 List<ExcelHeader> headers = Arrays.asList( new ExcelHeader("username", "用户名"), new ExcelHeader("deptName", "部门"), new ExcelHeader("orderCount", "订单数") ); // 查询数据 List<Map<String, Object>> data = getReportData(); // 使用EasyExcel导出 EasyExcel.write(response.getOutputStream()) .head(ExcelUtil.buildHead(headers)) .sheet("用户报表") .doWrite(ExcelUtil.buildData(data, headers));3.2 大数据量分页导出
对于大量数据,采用分页查询避免内存溢出:
public void exportLargeData(HttpServletResponse response) { int pageSize = 1000; int current = 1; try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) { while (true) { Page<User> page = new Page<>(current, pageSize); List<Map<String, Object>> data = queryPageData(page); if (data.isEmpty()) break; WriteSheet writeSheet = EasyExcel.writerSheet("第" + current + "页").build(); excelWriter.write(data, writeSheet); current++; } } }3.3 性能优化关键点
| 优化方向 | 具体措施 | 效果预估 |
|---|---|---|
| 查询优化 | 只select必要字段 | 减少30%-50%查询时间 |
| 内存优化 | 使用分页查询 | 避免OOM风险 |
| IO优化 | 使用流式导出 | 降低内存占用70%+ |
| 并发优化 | 多线程分页查询 | 提升吞吐量2-3倍 |
4. 避坑指南与最佳实践
4.1 常见问题解决方案
问题1:字段类型转换异常
当使用selectMaps时,数据库返回的值可能不是预期的Java类型。解决方案:
// 安全获取Map中的值 public <T> T getValue(Map<String, Object> map, String key, Class<T> type) { Object value = map.get(key); if (value == null) return null; if (type.isInstance(value)) { return type.cast(value); } // 类型转换逻辑... }问题2:联表查询性能低下
- 确保关联字段有索引
- 避免多对多关联
- 考虑使用
@TableField(exist=false)+单独查询方式
4.2 安全注意事项
- SQL注入防护:
- 永远不要拼接SQL参数
- 使用
apply()方法时确保参数安全
// 不安全的写法 wrapper.apply("create_time > '" + userInput + "'"); // 安全的写法 wrapper.apply("create_time > {0}", userInput);- 数据权限控制:
- 在Wrapper中自动添加权限过滤条件
- 使用MyBatis-Plus的Tenant特性
4.3 扩展应用场景
场景1:动态看板数据
// 获取各类统计指标 public Map<String, Object> getDashboardData() { Map<String, Object> result = new HashMap<>(); // 用户总数 result.put("userCount", userMapper.selectCount(null)); // 按状态统计 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("status", "COUNT(*) as count") .groupBy("status"); result.put("statusStats", userMapper.selectMaps(wrapper)); return result; }场景2:数据缓存预热
// 使用selectObjs获取需要缓存的ID列表 public void preheatCache() { List<Object> activeUserIds = userMapper.selectObjs( new QueryWrapper<User>().select("id").eq("status", 1)); activeUserIds.parallelStream().forEach(id -> { userCache.put((Long)id, getUserDetail((Long)id)); }); }在实际项目中,我发现合理组合使用selectMaps和selectObjs可以解决80%以上的复杂报表需求。特别是在处理动态字段、多表关联和聚合统计时,相比传统方案能减少50%以上的代码量。一个实用的技巧是将常用的查询模式封装成工具方法,比如buildReportQuery()方法可以统一处理字段选择、条件过滤和排序逻辑。
