简体   繁体   English

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

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

I'm developing a Spring Boot application with Spring Data JPA.我正在使用 Spring Data JPA 开发 Spring Boot 应用程序。 I'm using a custom JPQL query to group by some field and get the count.我正在使用自定义 JPQL 查询按某个字段分组并获取计数。 Following is my repository method.以下是我的存储库方法。

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

It's working and result is obtained as follows:它正在工作,结果如下:

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

I would like to get something like this:我想得到这样的东西:

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

How can I achieve this?我怎样才能做到这一点?

Solution for JPQL queries JPQL查询解决方案

This is supported for JPQL queries within the JPA specification . JPA 规范中的JPQL 查询支持此功能。

Step 1 : Declare a simple bean class第 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;
  }
}

Step 2 : Return bean instances from the repository method第 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();
}

Important notes重要笔记

  1. Make sure to provide the fully-qualified path to the bean class, including the package name.确保提供 bean 类的完全限定路径,包括包名称。 For example, if the bean class is called MyBean and it is in package com.path.to , the fully-qualified path to the bean will be com.path.to.MyBean .例如,如果 bean 类名为MyBean并且它在包com.path.to ,则 bean 的完全限定路径将是com.path.to.MyBean Simply providing MyBean will not work (unless the bean class is in the default package).简单地提供MyBean行不通的(除非 bean 类在默认包中)。
  2. Make sure to call the bean class constructor using the new keyword.确保使用new关键字调用 bean 类构造函数。 SELECT new com.path.to.MyBean(...) will work, whereas SELECT com.path.to.MyBean(...) will not. SELECT new com.path.to.MyBean(...)会起作用,而SELECT com.path.to.MyBean(...)不会。
  3. Make sure to pass attributes in exactly the same order as that expected in the bean constructor.确保以与 bean 构造函数中预期的完全相同的顺序传递属性。 Attempting to pass attributes in a different order will lead to an exception.尝试以不同的顺序传递属性将导致异常。
  4. Make sure the query is a valid JPA query, that is, it is not a native query.确保查询是有效的 JPA 查询,即它不是本机查询。 @Query("SELECT ...") , or @Query(value = "SELECT ...") , or @Query(value = "SELECT ...", nativeQuery = false) will work, whereas @Query(value = "SELECT ...", nativeQuery = true) will not work. @Query("SELECT ...")@Query(value = "SELECT ...")@Query(value = "SELECT ...", nativeQuery = false)将起作用,而@Query(value = "SELECT ...", nativeQuery = true)将不起作用。 This is because native queries are passed without modifications to the JPA provider, and are executed against the underlying RDBMS as such.这是因为本机查询在没有修改的情况下传递给 JPA 提供程序,并且是针对底层 RDBMS 执行的。 Since new and com.path.to.MyBean are not valid SQL keywords, the RDBMS then throws an exception.由于newcom.path.to.MyBean不是有效的 SQL 关键字,因此 RDBMS 会抛出异常。

Solution for native queries本机查询的解决方案

As noted above, the new ... syntax is a JPA-supported mechanism and works with all JPA providers.如上所述, new ...语法是 JPA 支持的机制,适用于所有 JPA 提供程序。 However, if the query itself is not a JPA query, that is, it is a native query, the new ... syntax will not work as the query is passed on directly to the underlying RDBMS, which does not understand the new keyword since it is not part of the SQL standard.但是,如果查询本身是不是一个JPA查询,也就是说,它是一种天然的查询时, new ...语法不会工作作为查询直接传递到底层的RDBMS,这并不了解new ,因为关键字它不是 SQL 标准的一部分。

In situations like these, bean classes need to be replaced with Spring Data Projection interfaces.在这种情况下,需要用Spring Data Projection接口替换 bean 类。

Step 1 : Declare a projection interface第一步:声明一个投影接口

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Step 2 : Return projected properties from the query第 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();
}

Use the SQL AS keyword to map result fields to projection properties for unambiguous mapping.使用 SQL AS关键字将结果字段映射到投影属性以进行明确映射。

This SQL query return List< Object[] > would.此 SQL 查询将返回 List<Object[]>。

You can do it this way:你可以这样做:

 @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;
     }
 }

I know this is an old question and it has already been answered, but here's another approach:我知道这是一个老问题,已经有人回答了,但这是另一种方法:

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

Using interfaces you can get simpler code.使用接口可以获得更简单的代码。 No need to create and manually call constructors无需创建和手动调用构造函数

Step 1 : Declare interface with the required fields:第 1 步:声明具有必填字段的接口:

public interface SurveyAnswerStatistics {

    String getAnswer();
    Long getCnt();

}

Step 2 : Select columns with same name as getter in interface and return intefrace from repository method:第 2 步:在接口中选择与 getter 同名的列并从存储库方法返回接口:

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();

}

define a custom pojo class say sureveyQueryAnalytics and store the query returned value in your custom pojo class定义一个自定义 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();

I do not like java type names in query strings and handle it with a specific constructor.我不喜欢查询字符串中的 java 类型名称并使用特定的构造函数处理它。 Spring JPA implicitly calls constructor with query result in HashMap parameter: 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();

Code needs Lombok for resolving @Getter代码需要 Lombok 来解​​析 @Getter

I used custom DTO (interface) to map a native query to - the most flexible approach and refactoring-safe.我使用自定义 DTO(接口)将本机查询映射到 - 最灵活的方法和重构安全。

The problem I had with this - that surprisingly, the order of fields in the interface and the columns in the query matters.我遇到的问题 - 令人惊讶的是,界面中的字段顺序和查询中的列很重要。 I got it working by ordering interface getters alphabetically and then ordering the columns in the query the same way.我通过按字母顺序对接口 getter 进行排序,然后以相同的方式对查询中的列进行排序来使其工作。

@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();

}

The above code worked for me.上面的代码对我有用。

Get data with column name and its values (in key-value pair) using JDBC:使用 JDBC 获取列名及其值(在键值对中)的数据:

/*Template class with a basic set of JDBC operations, allowing the use
  of named parameters rather than traditional '?' placeholders.
 
  This class delegates to a wrapped {@link #getJdbcOperations() JdbcTemplate}
  once the substitution from named parameters to JDBC style '?' placeholders is
  done at execution time. It also allows for expanding a {@link java.util.List}
  of values to the appropriate number of placeholders.
 
  The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is
  exposed to allow for convenient access to the traditional
  {@link org.springframework.jdbc.core.JdbcTemplate} methods.*/


@Autowired
protected  NamedParameterJdbcTemplate jdbc;


@GetMapping("/showDataUsingQuery/{Query}")
    public List<Map<String,Object>> ShowColumNameAndValue(@PathVariable("Query")String Query) throws SQLException {

      /* MapSqlParameterSource class is intended for passing in a simple Map of parameter values
        to the methods of the {@link NamedParameterJdbcTemplate} class*/

       MapSqlParameterSource msp = new MapSqlParameterSource();

       // this query used for show column name and columnvalues....
        List<Map<String,Object>> css = jdbc.queryForList(Query,msp);

        return css;
    }

I just solved this problem :我刚刚解决了这个问题:

  • Class-based Projections doesn't work with query native( @Query(value = "SELECT ...", nativeQuery = true )) so I recommend to define custom DTO using interface .基于类的投影不适用于查询 native( @Query(value = "SELECT ...", nativeQuery = true )) 所以我建议使用 interface 定义自定义 DTO。
  • Before using DTO should verify the query syntatically correct or not在使用 DTO 之前应该验证查询的语法是否正确
    //in Service      
      `
                public List<DevicesPerCustomer> findDevicesPerCustomer() {
                    LOGGER.info(TAG_NAME + " :: inside findDevicesPerCustomer : ");
                    List<Object[]> list = iDeviceRegistrationRepo.findDevicesPerCustomer();
                    List<DevicesPerCustomer> out = new ArrayList<>();
                    if (list != null && !list.isEmpty()) {
                        DevicesPerCustomer mDevicesPerCustomer = null;
                        for (Object[] object : list) {
                            mDevicesPerCustomer = new DevicesPerCustomer();
mDevicesPerCustomer.setCustomerId(object[0].toString());
                            mDevicesPerCustomer.setCount(Integer.parseInt(object[1].toString()));
                            
                            out.add(mDevicesPerCustomer);
                        }
                    }
            
                    return out;
                }`
        
    //In Repo
        `   @Query(value = "SELECT d.customerId,count(*) FROM senseer.DEVICE_REGISTRATION d  where d.customerId is not null group by d.customerId", nativeQuery=true)
            List<Object[]> findDevicesPerCustomer();`

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM