1. 原生用法痛点
直接在业务代码中拼接 SQL 聚合函数,字段名容易写错且不便于复用:
bash
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.select("SUM(price) as totalPrice"); // 字段名字符串,无法类型检查
Map<String, Object> map = orderMapper.selectMaps(wrapper).get(0);
BigDecimal total = (BigDecimal) map.get("totalPrice");
2. 封装目标
- 利用 Lambda 表达式 自动解析字段名,避免字符串硬编码。
- 统一封装在
BaseService中,支持 求和、平均值、最大值、最小值。 - 完全兼容默认列名(驼峰转下划线)和
@TableField自定义列名。
3. 核心难点:可靠获取数据库列名
- 默认规则:
属性名 → 驼峰转下划线(如orderPrice → order_price)。 - 若存在
@TableField("total_amount"),需要拿到注解中的值。 - 解决方案 :通过
TableInfoHelper.getTableInfo(entityClass)获取 MP 启动时解析好的元数据,field.getColumn()就是最终的数据库列名(已包含注解、全局前缀、关键字转义等)。
工具方法(完全兼容)
bash
public class MpLambdaUtils {
public static <T> String getColumnName(Class<T> entityClass, SFunction<T, ?> func) {
String propertyName = PropertyNamer.methodToProperty(
LambdaUtils.extract(func).getImplMethodName()
);
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
if (tableInfo != null) {
return tableInfo.getFieldList().stream()
.filter(f -> f.getProperty().equals(propertyName))
.findFirst()
.map(f -> f.getColumn())
.orElse(propertyName); // 极端情况降级
}
return StringUtils.camelToUnderline(propertyName);
}
}
4. BaseService 通用封装
bash
public abstract class BaseService<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> {
@Override
public <R> R sum(SFunction<T, ?> column, QueryWrapper<T> queryWrapper, Class<R> resultType) {
return aggregate(column, "SUM", queryWrapper, resultType);
}
@Override
public <R> R avg(SFunction<T, ?> column, QueryWrapper<T> queryWrapper, Class<R> resultType) {
return aggregate(column, "AVG", queryWrapper, resultType);
}
@Override
public <R> R max(SFunction<T, ?> column, QueryWrapper<T> queryWrapper, Class<R> resultType) {
return aggregate(column, "MAX", queryWrapper, resultType);
}
@Override
public <R> R min(SFunction<T, ?> column, QueryWrapper<T> queryWrapper, Class<R> resultType) {
return aggregate(column, "MIN", queryWrapper, resultType);
}
private <R> R aggregate(SFunction<T, ?> column, String funcName, QueryWrapper<T> queryWrapper, Class<R> resultType) {
String columnName = MpLambdaUtils.getColumnName(entityClass, column);
queryWrapper = Optional.ofNullable(queryWrapper).orElse(Wrappers.emptyWrapper());
queryWrapper.select(funcName + "(" + columnName + ") as res");
Map<String, Object> map = getMap(queryWrapper);
if (map == null || map.get("res") == null) {
return null;
}
if (resultType == BigDecimal.class) {
return (R) new BigDecimal(map.get("res").toString());
} else if (resultType == Long.class) {
return (R) Long.valueOf(map.get("res").toString());
} else if (resultType == Double.class) {
return (R) Double.valueOf(map.get("res").toString());
} else if (resultType == Integer.class) {
return (R) Integer.valueOf(map.get("res").toString());
}
// 可按需增加其他类型
throw new IllegalArgumentException("不支持的返回类型: " + resultType);
}
}
5. 使用示例
bash
@Service
public class OrderServiceImpl extends BaseService<OrderMapper, Order> implements OrderService { }
// 无条件下求和
BigDecimal total = orderService.getSum(Order::getAmount, null);
// 带条件求平均
LambdaQueryWrapper<Order> wrapper = Wrappers.<Order>lambdaQuery()
.eq(Order::getUserId, 1)
.ge(Order::getCreateTime, LocalDate.now().minusDays(7));
BigDecimal avg = orderService.getAvg(Order::getAmount, wrapper);
实体无论是否使用 @TableField 都能正确解析:
bash
@TableField("total_amount")
private BigDecimal amount; // SQL 会正确拼接 SUM(total_amount)
6. 关键点总结
| 要点 | 说明 |
|---|---|
| 列名获取 | 始终从 TableInfoHelper 拿,兼容所有场景 |
| Lambda 类型安全 | SFunction 确保字段引用不会拼错 |
| 空值处理 | 聚合结果为 null 时可返回 0 或 null,按需调整 |
| 性能注意 | 确保聚合字段有索引;分库分表时会路由所有表 |
| 多字段聚合 | 可一次 select 多个函数,返回 Map 后拆解 |
| group by | 同样支持,只需在 wrapper 中设置 group 条件 |
7. 扩展建议
- 可封装泛型返回
Long、Double等,通过额外参数指定类型。 - 可设计
StatsResult对象一次性返回多个统计值。 - 对于复杂统计,仍可结合 XML 自定义 SQL,本封装适合轻量级聚合需求。
一句话总结:利用 MP 的元数据 + Lambda 表达式,可以安全、统一地封装字段数学计算,彻底告别字符串拼字段名,并天然兼容各类列名映射。