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

Posted

技术标签:

【中文标题】如何从 Spring Data JPA GROUP BY 查询中返回自定义对象【英文标题】:How to return a custom object from a Spring Data JPA GROUP BY query 【发布时间】:2016-07-19 14:12:09 【问题描述】:

我正在使用 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" 
]

我怎样才能做到这一点?

【问题讨论】:

【参考方案1】:

JPQL 查询解决方案

JPA specification 中的 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();

重要提示

    确保提供 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 ... 语法将不起作用,因为查询直接传递到底层 RDBMS,它不理解 new 关键字因为它不是 SQL 标准的一部分。

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

第一步:声明一个投影接口

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 关键字将结果字段映射到投影属性以进行明确映射。

【讨论】:

它不工作,触发错误: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.......... 您的输出中的 SurveyAnswerReport 是什么。我假设你用你自己的类SurveyAnswerReport 替换了SurveyAnswerStatistics 。您需要指定完全限定的类名。 bean 类必须是完全限定的,即包含完整的包名。类似com.domain.dto.SurveyAnswerReport 当我尝试从JpaRepository 返回自定义类型时,我得到了'java.lang.IllegalArgumentException:PersistentEntity 不能为空!`?我错过了一些配置吗? 虽然使用本机查询异常说:嵌套异常是 java.lang.IllegalArgumentException: Not a managed type: class ... 为什么会发生这种情况?【参考方案2】:

此 SQL 查询将返回 List

你可以这样做:

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

【讨论】:

感谢您对此问题的回复。它清脆清晰 @manish 谢谢你拯救了我的睡眠,你的方法就像一个魅力!!!!!!! 谢谢...我更喜欢这个解决方案而不是接受的答案本机查询解决方案,以避免一长串投影接口。【参考方案3】:

我知道这是一个老问题,并且已经得到解答,但这里有另一种方法:

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

【讨论】:

我喜欢你的回答,因为它不会强迫我创建一个新的类或接口。它对我有用。 工作正常,但我更喜欢在泛型中使用 Map 而不是 ?,因为 Map 会让我们以键 (0) 和值 (1) 的形式访问它们【参考方案4】:

定义一个自定义 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();

【讨论】:

解决方案更好。或者使用the projection in the official document.【参考方案5】:

我不喜欢查询字符串中的 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 在运行代码之前显示错误,因为它不适用于对象类型 需要龙目岛。刚刚在代码中添加了一个脚注。 它对我不起作用。构造函数没有被调用。对我来说,仅适用于基于接口的投影或基于类的投影,new in @Query。如果没有new(使用此构造函数HashMap&lt;String, Object&gt;)的基于类的工作会很棒。但我得到org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [package.MyClass]【参考方案6】:

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

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

【讨论】:

【参考方案7】:
@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();


上面的代码对我有用。

【讨论】:

【参考方案8】:

使用 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;
    

【讨论】:

【参考方案9】:

我刚刚解决了这个问题:

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

【讨论】:

【参考方案10】:
    //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();`

【讨论】:

虽然此代码可能会回答问题,但提供有关此代码为何和/或如何回答问题的额外上下文可提高其长期价值。

以上是关于如何从 Spring Data JPA GROUP BY 查询中返回自定义对象的主要内容,如果未能解决你的问题,请参考以下文章

从控制台应用程序使用带有休眠功能的spring-data-jpa时如何延迟加载收集

Spring整合Hibernate实现Spring Data JPA (简单使用)

Spring Data JPA 将原生查询结果映射到非实体 POJO

Spring Data JPA:查询ManyToMany,如何从映射类中获取数据?

如何使用Spring数据jpa @Query注释直接获取学生列表

如何使用 Spring Data JPA Repository 从 2 个表中查询?