如何从Spring Data JPA GROUP BY查询返回自定义对象


115

我正在使用Spring Data JPA开发Spring Boot应用程序。我正在使用自定义JPQL查询来按某个字段分组并获取计数。以下是我的存储库方法。

@Query(value = "select count(v) as cnt, v.answer from Survey v group by v.answer")
public List<?> findSurveyCount();

它正在工作,结果如下:

[
  [1, "a1"],
  [2, "a2"]
]

我想得到这样的东西:

[
  { "cnt":1, "answer":"a1" },
  { "cnt":2, "answer":"a2" }
]

我该如何实现?

Answers:


249

JPQL查询解决方案

JPA规范中的 JPQL查询支持此功能。

步骤1:声明一个简单的bean类

package com.path.to;

public class SurveyAnswerStatistics {
  private String answer;
  private Long   cnt;

  public SurveyAnswerStatistics(String answer, Long cnt) {
    this.answer = answer;
    this.count  = cnt;
  }
}

步骤2:从存储库方法返回bean实例

public interface SurveyRepository extends CrudRepository<Survey, Long> {
    @Query("SELECT " +
           "    new com.path.to.SurveyAnswerStatistics(v.answer, COUNT(v)) " +
           "FROM " +
           "    Survey v " +
           "GROUP BY " +
           "    v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();
}

重要笔记

  1. 确保提供到bean类的标准路径,包括包名。例如,如果bean类被调用MyBean并且在包中com.path.to,则指向bean的标准路径将为com.path.to.MyBean。仅提供MyBean将不起作用(除非bean类在默认程序包中)。
  2. 确保使用new关键字调用bean类的构造函数。SELECT new com.path.to.MyBean(...)会工作,而SELECT com.path.to.MyBean(...)不会。
  3. 确保以与bean构造函数中期望的顺序完全相同的顺序传递属性。尝试以不同顺序传递属性将导致异常。
  4. 确保查询是有效的JPA查询,也就是说,它不是本机查询。@Query("SELECT ..."),或@Query(value = "SELECT ..."),或@Query(value = "SELECT ...", nativeQuery = false)将起作用,而@Query(value = "SELECT ...", nativeQuery = true)将不起作用。这是因为本机查询无需修改就传递给JPA提供程序,并照此针对基础RDBMS执行。由于newcom.path.to.MyBean是无效的SQL关键字,因此RDBMS会引发异常。

本机查询解决方案

如上所述,该new ...语法是JPA支持的机制,并且可与所有JPA提供程序一起使用。但是,如果查询本身不是JPA查询,即它是本机查询,则new ...语法将不起作用,因为该查询直接传递给基础RDBMS,该RDBMS不理解该new关键字,因为它不是该关键字的一部分。 SQL标准。

在这种情况下,需要用Spring Data Projection接口替换bean类。

步骤1:声明投影界面

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

步骤2:从查询中返回投影的属性

public interface SurveyRepository extends CrudRepository<Survey, Long> {
    @Query(nativeQuery = true, value =
           "SELECT " +
           "    v.answer AS answer, COUNT(v) AS cnt " +
           "FROM " +
           "    Survey v " +
           "GROUP BY " +
           "    v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();
}

使用SQL AS关键字将结果字段映射到投影属性,以进行明确的映射。


1
它不起作用,触发错误:Caused by: java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate class [SurveyAnswerReport] [select new SurveyAnswerReport(v.answer,count(v.id)) from com.furniturepool.domain.Survey v group by v.answer] at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1750) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677) at org.hibernate.jpa.spi.AbstractEnti..........
Pranav C Balan

SurveyAnswerReport 您的输出中这是什么。我假设您已替换SurveyAnswerStatistics 为自己的课程SurveyAnswerReport。您需要指定完全限定的类名称。
邦蒂

8
Bean类必须完全合格,即包括完整的程序包名称。有点像com.domain.dto.SurveyAnswerReport
manish

2
当我尝试从我的返回自定义类型时,我得到了'java.lang.IllegalArgumentException:PersistentEntity一定不能为null!' JpaRepository。我错过了一些配置吗?
marioosh

1
在使用本机查询异常时说:嵌套异常是java.lang.IllegalArgumentException:不是托管类型:class ...为什么要对此进行隐藏?
Mikheil Zhghenti,

20

该SQL查询返回List <Object []>将。

您可以这样操作:

 @RestController
 @RequestMapping("/survey")
 public class SurveyController {

   @Autowired
   private SurveyRepository surveyRepository;

     @RequestMapping(value = "/find", method =  RequestMethod.GET)
     public Map<Long,String> findSurvey(){
       List<Object[]> result = surveyRepository.findSurveyCount();
       Map<Long,String> map = null;
       if(result != null && !result.isEmpty()){
          map = new HashMap<Long,String>();
          for (Object[] object : result) {
            map.put(((Long)object[0]),object[1]);
          }
       }
     return map;
     }
 }

1
感谢您对这个问题的回答。它清脆清晰
Dheeraj R

@manish谢谢您保存了我一夜的睡眠,您的方法像个魅力一样发挥了作用!!!!
Vineel

15

我知道这是一个老问题,并且已经回答了,但是这是另一种方法:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();

我喜欢您的回答,因为它不会强迫我创建新的类或接口。它为我工作。
Yuri HassleAraújo,

?工作正常,但我更喜欢地图的使用在仿制药代替,如地图将让我们进入他们的关键(0)和价值(1)
SAMIM阿夫塔卜·艾哈迈德

10

使用接口,您可以获得更简单的代码。无需创建和手动调用构造函数

第1步:使用必填字段声明界面:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

步骤2:在接口中选择与getter名称相同的列,并从存储库方法返回intefrace:

public interface SurveyRepository extends CrudRepository<Survey, Long> {

    @Query("select v.answer as answer, count(v) as cnt " +
           "from Survey v " +
           "group by v.answer")
    List<SurveyAnswerStatistics> findSurveyCount();

}

不幸的是,从GUI角度来看,投影不能用作DTO对象。如果您想重复使用DTO进行表单提交,则将无法使用。您仍然需要带有getter / setter的单独的常规bean。因此,这不是一个好的解决方案。
基因b。

此外,缺少调查类
Mikheil Zhghenti

6

定义一个自定义pojo类,例如sureveyQueryAnalytics并将查询返回的值存储在自定义pojo类中

@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();

1
解决方案更好。或使用官方文件中的投影。
忍者'18

3

我不喜欢查询字符串中的Java类型名称并使用特定的构造函数来处理它。Spring JPA在HashMap参数中隐式调用具有查询结果的构造函数:

@Getter
public class SurveyAnswerStatistics {
  public static final String PROP_ANSWER = "answer";
  public static final String PROP_CNT = "cnt";

  private String answer;
  private Long   cnt;

  public SurveyAnswerStatistics(HashMap<String, Object> values) {
    this.answer = (String) values.get(PROP_ANSWER);
    this.count  = (Long) values.get(PROP_CNT);
  }
}

@Query("SELECT v.answer as "+PROP_ANSWER+", count(v) as "+PROP_CNT+" FROM  Survey v GROUP BY v.answer")
List<SurveyAnswerStatistics> findSurveyCount();

代码需要Lombok来解决@Getter


@Getter在运行代码之前显示错误,因为它不是针对对象类型的
user666

龙目岛是必需的。刚刚在代码中添加了脚注。
dwe

1

我刚刚解决了这个问题:

  • 基于类的投影不适用于本机查询(@Query(value = "SELECT ...", nativeQuery = true)),因此我建议使用interface定义自定义DTO。
  • 在使用DTO之前,应在语法上验证查询是否正确

1

我使用自定义DTO(接口)将本机查询映射到-最灵活的方法和重构安全的方法。

我遇到的问题-令人惊讶的是,界面中字段的顺序和查询中的列很重要。我通过按字母顺序对接口吸气剂进行排序,然后以相同的方式对查询中的列进行排序来使其工作。


0
@Repository
public interface ExpenseRepo extends JpaRepository<Expense,Long> {
    List<Expense> findByCategoryId(Long categoryId);

    @Query(value = "select category.name,SUM(expense.amount) from expense JOIN category ON expense.category_id=category.id GROUP BY expense.category_id",nativeQuery = true)
    List<?> getAmountByCategory();

}

上面的代码为我工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.