MyBatis-Plus 字段数学计算封装

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. 扩展建议

  • 可封装泛型返回 LongDouble 等,通过额外参数指定类型。
  • 可设计 StatsResult 对象一次性返回多个统计值。
  • 对于复杂统计,仍可结合 XML 自定义 SQL,本封装适合轻量级聚合需求。

一句话总结:利用 MP 的元数据 + Lambda 表达式,可以安全、统一地封装字段数学计算,彻底告别字符串拼字段名,并天然兼容各类列名映射。

相关推荐
bandaoyu13 小时前
【AMD】HDP(Host Data Path)是什么
java·后端·spring
用户21816970493013 小时前
golang socket(一) TCP协议 简单的socket服务器和客户端
后端
ZengLiangYi13 小时前
MCP 协议从零实现:手写最简 MCP Server
前端·javascript·后端
yspwf13 小时前
Node.js 本地下载并使用 Hugging Face 中文向量模型:以 bge-base-zh-v1.5 为例
javascript·后端
Huyuejia13 小时前
cli介绍
后端
一 乐13 小时前
个人博客系统|基于Springboot的个人博客系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·个人博客系统
Nyarlathotep011313 小时前
自动内存管理(3):HotSpot中垃圾收集的实现
jvm·后端
神奇小汤圆13 小时前
一行代码干翻 Java 反射?EggG 流式反射调用让反射优雅到不可思议
后端