Spring JPA/Hibernate如何返回嵌套自定义类的分组统计结果
Spring Data JPA 本身确实不支持直接将聚合查询结果直接映射为Map<String, Map<String, Stats>>这类嵌套结构,但有3种成熟的落地实现方案,完全可以避免使用List<Object[]>这类无类型的返回值。
方案1:接口投影 + Repository默认方法转换(零额外依赖,新手首选)
这是最轻量的实现方式,不需要自定义Repository实现类,不需要引入第三方依赖,所有逻辑都在JPA Repository接口内完成,编译期可做类型校验。
- 首先定义统计值封装类
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 }
- 修正原有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类型结果后再做分组转换,逻辑更直观:
- 定义自定义Repository接口:
public interface CustomStudentRepository { Map<String, Map<String, Stats>> getStatsByDepartmentAndSubDepartment(); }
- 写实现类,通过
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) ) ) )); } }
- 让原
StudentRepository继承自定义接口,即可直接调用该方法:
public interface StudentRepository extends JpaRepository<Student, Long>, CustomStudentRepository { // 原有其他方法保留 }
方案3:引入JPA增强工具(适合复杂统计场景多的项目)
如果项目中存在大量嵌套DTO、聚合查询的映射需求,可以引入JPA增强类工具,这类工具支持直接在查询定义阶段声明嵌套对象映射规则,不需要手动写stream转换逻辑。对于当前这个简单需求来说这类方案偏重,按需选择即可。
注意事项
- 聚合函数
avg()返回的数值类型默认是Double,不要定义为Integer,否则会出现类型转换异常 - 使用构造器投影时,必须写POJO类的全限定名,构造器参数顺序、类型必须和查询字段完全匹配
- 如果使用H2数据库做测试,注意和生产数据库的聚合函数返回类型保持一致,避免出现本地测试正常、线上报错的问题
内容的提问来源于stack exchange,提问作者Ian McGrath
相关产品推荐
相关产品推荐

