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

How to return a custom object from a Spring Data JPA GROUP BY query

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

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

它正在工作,结果如下:

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

我想得到这样的东西:

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

我该如何实现?


JPQL查询解决方案

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

Step 1: Declare a simple bean class

1
2
3
4
5
6
7
8
9
10
11
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;
  }
}

Step 2: Return bean instances from the repository method

1
2
3
4
5
6
7
8
9
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();
}

重要笔记

  • 确保提供到bean类的标准路径,包括包名。例如,如果Bean类名为MyBean,并且位于包com.path.to中,则指向该bean的标准路径为com.path.to.MyBean。仅提供MyBean是行不通的(除非bean类在默认程序包中)。
  • 确保使用new关键字调用bean类的构造函数。 SELECT new com.path.to.MyBean(...)将起作用,而SELECT com.path.to.MyBean(...)将不起作用。
  • 确保以与bean构造函数中期望的顺序完全相同的顺序传递属性。尝试以不同顺序传递属性将导致异常。
  • 确保查询是有效的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 ...语法将不起作用,因为该查询直接传递给不了解new关键字的基础RDBMS因为它不是SQL标准的一部分。

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

    Step 1: Declare a projection interface

    1
    2
    3
    4
    5
    6
    7
    package com.path.to;

    public interface SurveyAnswerStatistics {
      String getAnswer();

      int getCnt();
    }

    Step 2: Return projected properties from the query

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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关键字将结果字段映射到投影属性,以进行明确的映射。


    此SQL查询返回List 将。

    您可以这样操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     @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
    2
    @Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
    public List< ? > findSurveyCount();


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

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

    1
    2
    3
    4
    5
    6
    public interface SurveyAnswerStatistics {

      String getAnswer();
      Long getCnt();

    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    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();

    }


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

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @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


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

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


    我刚刚解决了这个问题:

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

    1
    2
    3
    4
    5
    6
    7
    8
    @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();

    }

    上面的代码为我工作。