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

175

我正在开发一个使用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" }
]

我该怎样实现这个目标?
10个回答

374

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

第二步:从存储库方法返回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的查询。由于new和com.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();
}

第二步:从查询中返回预测属性。
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
10
Bean类必须使用完全限定名,也就是包括完整的包名。例如:com.domain.dto.SurveyAnswerReport - manish
你能确认在这种情况下扩展JPARepository的类的名称吗?它叫做SurveyRepository还是SurveyAnswerStatisticsRepository?谢谢。 - emeraldjava
3
当我尝试从我的 JpaRepository 返回自定义类型时,出现了 java.lang.IllegalArgumentException: PersistentEntity must not be null! 的错误提示。这是我漏掉了某些配置吗? - marioosh
1
在使用本地查询时,异常显示为:嵌套异常是java.lang.IllegalArgumentException: Not a managed type: class ... 为什么会发生这种情况? - Mikheil Zhghenti
显示剩余15条评论

28
这个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
谢谢...我更喜欢这个解决方案,而不是接受答案中的本地查询解决方案,以避免一长串的投影接口。 - sarath
这仅在比较以上答案时有效。 - SagitSri

23

我知道这是一个古老的问题,并且已经有了答案,但这里还有另一种方法:

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

1
我喜欢你的答案,因为它不强制我创建一个新的类或接口。它对我很有用。 - Yuri Hassle Araújo
运行良好,但我更喜欢在泛型中使用 Map 而不是 ?,因为 Map 可以让我们将它们作为键(0)和值(1)进行访问。 - sam

6

定义一个自定义的POJO类,比如SurveyQueryAnalytics,并将查询返回值存储在您的自定义POJO类中。

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

2
解决方案更好。或者使用官方文档中的投影技术 - Ninja

4

我不喜欢在查询字符串中使用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
需要使用Lombok。刚刚在代码中添加了一个脚注。 - dwe
对我来说它不起作用。构造函数没有被调用。对我来说,只有基于接口的投影或使用 @Query 中的 new 的基于类的投影才有效。如果基于类而不使用 new(使用此构造函数 HashMap<String, Object>)将会很好。但是我得到了 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] - mkczyk
似乎Spring无法将Map对象注入到构造函数中,因为它们是不同的类型。仓库返回类型不是Map<String, Object>,因此无法调用适当的转换器。找到下面的帖子可能会有所帮助:https://www.bytestree.com/spring/spring-data-jpa-projections-5-ways-return-custom-object/ - Manoel Stilpen

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

}

上述代码对我有用。

2
我使用自定义DTO(接口)将本地查询映射到对象 - 这是最灵活的方法,并且可以保持重构的安全性。
我遇到的问题是,令人惊讶的是,接口中字段的顺序和查询中的列的顺序很重要。我通过按字母顺序排列接口getter,然后以相同的方式排序查询中的列来解决了这个问题。

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


1

我刚刚解决了这个问题:

  • 基于类的投影不能与查询本地化(@Query(value = "SELECT ...", nativeQuery = true))一起使用,因此我建议使用接口定义自定义DTO。
  • 在使用DTO之前,应该验证查询是否语法正确。

0
    //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();`

虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - Pradeepal Sudeshana

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接