如何从 Spring Data JPA GROUP BY 查询中 return 自定义对象
How to return a custom object from a Spring Data JPA GROUP BY query
我正在使用 Spring Data JPA 开发 Spring 启动应用程序。我正在使用自定义 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" }
]
我怎样才能做到这一点?
这个 SQL 查询 return 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;
}
}
JPQL 查询的解决方案
中的 JPQL 查询支持此功能
Step 1: Declare a simple bean class
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
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 class 的完全限定路径,包括包名称。例如,如果 bean class 被称为
MyBean
并且它在包 com.path.to
中,则该 bean 的完全限定路径将为 com.path.to.MyBean
。简单地提供 MyBean
是行不通的(除非 bean class 在默认包中)。
- 确保使用
new
关键字调用 bean class 构造函数。 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 ...
语法将不起作用,因为查询被直接传递到不理解 new
关键字,因为它不是 SQL 标准的一部分。
在这种情况下,bean classes 需要替换为 Spring Data Projection 接口。
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
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
关键字将结果字段映射到投影属性以进行明确映射。
定义自定义 pojo class sureveyQueryAnalytics 并将查询返回值存储在自定义 pojo 中 class
@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();
我知道这是一个老问题,已经有人回答了,但这里有另一种方法:
@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
我不喜欢 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
我刚刚解决了这个问题:
- Class-based 投影不适用于查询 native(
@Query(value = "SELECT ...", nativeQuery = true
)) 因此我建议使用 interface 定义自定义 DTO。
- 在使用 DTO 之前应该验证查询在语法上是否正确
我使用自定义 DTO(接口)将本机查询映射到 - 最灵活的方法和重构安全。
我遇到的问题 - 令人惊讶的是,界面中字段的顺序和查询中的列很重要。我通过按字母顺序对接口 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();
}
以上代码对我有用。
使用 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;
}
//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 开发 Spring 启动应用程序。我正在使用自定义 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" }
]
我怎样才能做到这一点?
这个 SQL 查询 return 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;
}
}
JPQL 查询的解决方案
中的 JPQL 查询支持此功能Step 1: Declare a simple bean class
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
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 class 的完全限定路径,包括包名称。例如,如果 bean class 被称为
MyBean
并且它在包com.path.to
中,则该 bean 的完全限定路径将为com.path.to.MyBean
。简单地提供MyBean
是行不通的(除非 bean class 在默认包中)。 - 确保使用
new
关键字调用 bean class 构造函数。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 ...
语法将不起作用,因为查询被直接传递到不理解 new
关键字,因为它不是 SQL 标准的一部分。
在这种情况下,bean classes 需要替换为 Spring Data Projection 接口。
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
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
关键字将结果字段映射到投影属性以进行明确映射。
定义自定义 pojo class sureveyQueryAnalytics 并将查询返回值存储在自定义 pojo 中 class
@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();
我知道这是一个老问题,已经有人回答了,但这里有另一种方法:
@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
我不喜欢 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
我刚刚解决了这个问题:
- Class-based 投影不适用于查询 native(
@Query(value = "SELECT ...", nativeQuery = true
)) 因此我建议使用 interface 定义自定义 DTO。 - 在使用 DTO 之前应该验证查询在语法上是否正确
我使用自定义 DTO(接口)将本机查询映射到 - 最灵活的方法和重构安全。
我遇到的问题 - 令人惊讶的是,界面中字段的顺序和查询中的列很重要。我通过按字母顺序对接口 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();
}
以上代码对我有用。
使用 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;
}
//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();`