You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring JPA/Hibernate如何返回嵌套自定义类的分组统计结果

Spring Data JPA 本身确实不支持直接将聚合查询结果直接映射为Map<String, Map<String, Stats>>这类嵌套结构,但有3种成熟的落地实现方案,完全可以避免使用List<Object[]>这类无类型的返回值。

方案1:接口投影 + Repository默认方法转换(零额外依赖,新手首选)

这是最轻量的实现方式,不需要自定义Repository实现类,不需要引入第三方依赖,所有逻辑都在JPA Repository接口内完成,编译期可做类型校验。

  1. 首先定义统计值封装类Stats,这是普通POJO,不需要加任何JPA注解:
// 注意替换成你自己的项目包路径
public class Stats {
    private Integer minMarks;
    private Integer maxMarks;
    private Double avgMarks;

    // 全参构造,参数顺序、类型必须和后续JPQL中统计字段的顺序、类型完全匹配
    public Stats(Integer minMarks, Integer maxMarks, Double avgMarks) {
        this.minMarks = minMarks;
        this.maxMarks = maxMarks;
        this.avgMarks = avgMarks;
    }

    // 补全所有字段的getter、setter
}
  1. 修正原有Repository代码(注意原代码的两个错误:Repository必须是接口而非类、JPQL中groupby是拼写错误,要写为group by),定义扁平结构的投影接口,再通过默认方法完成嵌套Map的转换:
public interface StudentRepository extends JpaRepository<Student, Long> {
    // 注意new Stats后要写类的全限定名,所有查询字段加别名和投影接口的getter对应
    @Query("select s.department as department, s.subDepartment as subDepartment, " +
            "new com.yourpackage.Stats(min(s.marks), max(s.marks), avg(s.marks)) as stats " +
            "from Student s group by s.department, s.subDepartment")
    List<FlatStat> getFlatStats();

    // 中间扁平投影接口,承接单条查询结果的三个字段
    interface FlatStat {
        String getDepartment();
        String getSubDepartment();
        Stats getStats();
    }

    // 对外暴露的方法直接返回目标嵌套Map结构,调用方直接调用该方法即可
    default Map<String, Map<String, Stats>> getNestedStats() {
        return getFlatStats().stream()
                .collect(Collectors.groupingBy(
                        FlatStat::getDepartment,
                        Collectors.toMap(
                                FlatStat::getSubDepartment,
                                FlatStat::getStats
                        )
                ));
    }
}

方案2:自定义Repository实现 + JPA Tuple结果转换

如果不想写中间投影接口,可以自定义Repository实现类,注入EntityManager直接执行JPQL,拿到JPA原生的Tuple类型结果后再做分组转换,逻辑更直观:

  1. 定义自定义Repository接口:
public interface CustomStudentRepository {
    Map<String, Map<String, Stats>> getStatsByDepartmentAndSubDepartment();
}
  1. 写实现类,通过EntityManager查询后做结果转换:
@Repository
public class CustomStudentRepositoryImpl implements CustomStudentRepository {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Map<String, Map<String, Stats>> getStatsByDepartmentAndSubDepartment() {
        return entityManager.createQuery(
                "select s.department as department, s.subDepartment as subDepartment, " +
                        "min(s.marks) as minMarks, max(s.marks) as maxMarks, avg(s.marks) as avgMarks " +
                        "from Student s group by s.department, s.subDepartment",
                Tuple.class
        ).getResultList()
        .stream()
        .collect(Collectors.groupingBy(
                tuple -> tuple.get("department", String.class),
                Collectors.toMap(
                        tuple -> tuple.get("subDepartment", String.class),
                        tuple -> new Stats(
                                tuple.get("minMarks", Integer.class),
                                tuple.get("maxMarks", Integer.class),
                                tuple.get("avgMarks", Double.class)
                        )
                )
        ));
    }
}
  1. 让原StudentRepository继承自定义接口,即可直接调用该方法:
public interface StudentRepository extends JpaRepository<Student, Long>, CustomStudentRepository {
    // 原有其他方法保留
}

方案3:引入JPA增强工具(适合复杂统计场景多的项目)

如果项目中存在大量嵌套DTO、聚合查询的映射需求,可以引入JPA增强类工具,这类工具支持直接在查询定义阶段声明嵌套对象映射规则,不需要手动写stream转换逻辑。对于当前这个简单需求来说这类方案偏重,按需选择即可。

注意事项

  • 聚合函数avg()返回的数值类型默认是Double,不要定义为Integer,否则会出现类型转换异常
  • 使用构造器投影时,必须写POJO类的全限定名,构造器参数顺序、类型必须和查询字段完全匹配
  • 如果使用H2数据库做测试,注意和生产数据库的聚合函数返回类型保持一致,避免出现本地测试正常、线上报错的问题

内容的提问来源于stack exchange,提问作者Ian McGrath

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 07:01:02