告别原生SQL:MyBatis-Plus QueryWrapper的selectMaps()实战指南
在Java后端开发中,数据统计和报表功能几乎是每个系统都绕不开的需求。传统做法往往是编写冗长的原生SQL,然后在DAO层进行结果映射。这种模式不仅代码臃肿,维护困难,还容易因为字符串拼接导致SQL注入风险。今天我要分享的是MyBatis-Plus中一个被严重低估的特性——selectMaps()方法,它能让你用面向对象的方式处理复杂统计查询,彻底告别原生SQL的烦恼。
1. 为什么需要selectMaps()?
想象一下这样的场景:产品经理要求你开发一个用户画像统计功能,需要按性别分组统计用户数量,并且要将数据库中的数字编码(如1代表男,0代表女)转换为前端可读的文字标签。传统做法可能会这样写:
@Select("SELECT sex, COUNT(id) AS num FROM user GROUP BY sex") List<Map<String, Object>> countUsersBySex();这种方式有几个明显痛点:
- SQL以字符串形式硬编码在注解或XML中
- 结果集需要手动处理类型转换
- 修改查询条件时需要同步修改多个地方的SQL
而selectMaps()配合QueryWrapper的链式调用,可以完美解决这些问题。它最核心的价值在于:
- 类型安全:通过Lambda表达式引用实体属性,避免字段名拼写错误
- 可组合性:查询条件可以动态拼接,复用性极强
- 直接映射:返回的
List<Map>结构天然适合前端展示
2. selectMaps()基础用法
让我们从一个最简单的例子开始。假设要统计不同状态下的订单数量:
LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery(); wrapper.select("status", "COUNT(id) AS count") .groupBy(Order::getStatus); List<Map<String, Object>> result = orderMapper.selectMaps(wrapper);这段代码等效于SQL:
SELECT status, COUNT(id) AS count FROM order GROUP BY status返回的数据结构是这样的:
[ {"status": 1, "count": 152}, {"status": 2, "count": 87}, {"status": 3, "count": 43} ]几个关键点需要注意:
select()方法可以直接传入SQL片段,用于指定查询字段- 聚合函数如COUNT、SUM等需要指定别名(AS语法)
- 返回的Map中,key是查询字段名或别名,value是数据库原始值
3. 处理复杂统计场景
3.1 条件判断与字段转换
实际业务中经常需要在查询时做条件判断。比如用户性别在数据库存的是数字,但需要在前端显示为文字:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.select( "sex", "COUNT(id) AS num", "CASE WHEN sex = 1 THEN '男' WHEN sex = 0 THEN '女' ELSE '未知' END AS sexLabel" ).groupBy(User::getSex); List<Map<String, Object>> result = userMapper.selectMaps(wrapper);等效SQL:
SELECT sex, COUNT(id) AS num, CASE WHEN sex = 1 THEN '男' WHEN sex = 0 THEN '女' ELSE '未知' END AS sexLabel FROM user GROUP BY sex提示:CASE WHEN语句在统计报表中非常实用,可以用来实现数据分类、状态映射等复杂逻辑。
3.2 多维度分组统计
对于需要按多个字段分组的场景,比如统计每个部门下不同职级的员工数量:
LambdaQueryWrapper<Employee> wrapper = Wrappers.lambdaQuery(); wrapper.select( "department_id", "job_level", "COUNT(id) AS headcount" ).groupBy( Employee::getDepartmentId, Employee::getJobLevel ); List<Map<String, Object>> result = employeeMapper.selectMaps(wrapper);返回结果示例:
[ {"department_id": 101, "job_level": 3, "headcount": 12}, {"department_id": 101, "job_level": 4, "headcount": 5}, {"department_id": 102, "job_level": 3, "headcount": 8} ]3.3 时间维度统计
处理时间数据是统计分析的常见需求。比如要统计每日新增用户数:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.select( "DATE(create_time) AS date", "COUNT(id) AS new_users" ).groupBy("DATE(create_time)") .orderByAsc("DATE(create_time)"); List<Map<String, Object>> result = userMapper.selectMaps(wrapper);这里用到了MySQL的DATE函数来提取日期部分,其他数据库也有类似的函数,如Oracle的TRUNC、PostgreSQL的DATE_TRUNC等。
4. 高级技巧与性能优化
4.1 动态字段选择
有时我们需要根据条件动态选择查询字段。QueryWrapper的select方法支持Lambda表达式,可以优雅地实现这一点:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); boolean needDetail = true; // 从参数获取 if (needDetail) { wrapper.select( User::getId, User::getName, User::getAge ); } else { wrapper.select( User::getId, User::getName ); }4.2 分页统计查询
对于大数据量的统计,分页是必须的。MyBatis-Plus的分页接口同样支持selectMaps:
LambdaQueryWrapper<Order> wrapper = Wrappers.lambdaQuery(); wrapper.select( "product_id", "SUM(amount) AS total_amount" ).groupBy(Order::getProductId); Page<Map<String, Object>> page = new Page<>(1, 10); IPage<Map<String, Object>> result = orderMapper.selectMapsPage(page, wrapper);返回的IPage对象包含分页信息和数据列表,非常适合前后端分离的场景。
4.3 使用SQL函数
QueryWrapper的apply方法允许在条件中使用数据库函数。比如IP地址范围查询:
wrapper.apply("INET_ATON(ip_address) BETWEEN INET_ATON({0}) AND INET_ATON({1})", startIp, endIp);5. 实战案例:电商数据看板
让我们通过一个完整的电商数据统计案例,展示selectMaps()在实际项目中的应用。假设需要开发一个数据看板,包含以下指标:
- 当日订单总数和总金额
- 按支付方式分组的订单数
- 热销商品TOP 10
// 当日订单统计 LambdaQueryWrapper<Order> dailyWrapper = Wrappers.lambdaQuery(); dailyWrapper.select( "COUNT(id) AS order_count", "SUM(amount) AS total_amount" ).apply("DATE(create_time) = CURRENT_DATE"); Map<String, Object> dailyStats = orderMapper.selectMaps(dailyWrapper).get(0); // 支付方式分布 LambdaQueryWrapper<Order> paymentWrapper = Wrappers.lambdaQuery(); paymentWrapper.select( "payment_method", "COUNT(id) AS count" ).groupBy(Order::getPaymentMethod); List<Map<String, Object>> paymentStats = orderMapper.selectMaps(paymentWrapper); // 热销商品 LambdaQueryWrapper<OrderItem> productWrapper = Wrappers.lambdaQuery(); productWrapper.select( "product_id", "product_name", "SUM(quantity) AS sales_volume" ).groupBy(OrderItem::getProductId) .orderByDesc("sales_volume") .last("LIMIT 10"); List<Map<String, Object>> hotProducts = orderItemMapper.selectMaps(productWrapper);这个案例展示了如何用selectMaps()快速实现典型的数据看板需求,所有查询都通过链式调用完成,没有一行原生SQL。