Cách trả về một đối tượng tùy chỉnh từ truy vấn Spring Data JPA GROUP BY


115

Tôi đang phát triển một ứng dụng Spring Boot với Spring Data JPA. Tôi đang sử dụng truy vấn JPQL tùy chỉnh để nhóm theo một số trường và nhận số lượng. Sau đây là phương pháp lưu trữ của tôi.

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

Nó hoạt động và kết quả thu được như sau:

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

Tôi muốn nhận được một cái gì đó như thế này:

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

Làm thế nào tôi có thể đạt được điều này?

Câu trả lời:


249

Giải pháp cho các truy vấn JPQL

Điều này được hỗ trợ cho các truy vấn JPQL trong đặc tả JPA .

Bước 1 : Khai báo một lớp bean đơn giản

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

Bước 2 : Trả về các thể hiện bean từ phương thức kho lưu trữ

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

Ghi chú quan trọng

  1. Đảm bảo cung cấp đường dẫn đủ điều kiện đến lớp bean, bao gồm cả tên gói. Ví dụ, nếu lớp bean được gọi MyBeanvà nó nằm trong gói com.path.to, thì đường dẫn đủ điều kiện đến bean sẽ là com.path.to.MyBean. Việc cung cấp đơn giản MyBeansẽ không hoạt động (trừ khi lớp bean nằm trong gói mặc định).
  2. Đảm bảo gọi hàm tạo lớp bean bằng newtừ khóa. SELECT new com.path.to.MyBean(...)sẽ hoạt động, trong khi SELECT com.path.to.MyBean(...)sẽ không.
  3. Đảm bảo truyền các thuộc tính theo đúng thứ tự như mong đợi trong hàm tạo bean. Cố gắng chuyển các thuộc tính theo một thứ tự khác sẽ dẫn đến một ngoại lệ.
  4. Đảm bảo rằng truy vấn là một truy vấn JPA hợp lệ, nghĩa là, nó không phải là một truy vấn gốc. @Query("SELECT ...")hoặc @Query(value = "SELECT ..."), hoặc @Query(value = "SELECT ...", nativeQuery = false)sẽ hoạt động, ngược lại @Query(value = "SELECT ...", nativeQuery = true)sẽ không hoạt động. Điều này là do các truy vấn gốc được chuyển mà không có sửa đổi đối với nhà cung cấp JPA và được thực thi dựa trên RDBMS cơ bản như vậy. Vì newcom.path.to.MyBeankhông phải là các từ khóa SQL hợp lệ, RDBMS sau đó ném một ngoại lệ.

Giải pháp cho các truy vấn gốc

Như đã lưu ý ở trên, new ...cú pháp là một cơ chế được JPA hỗ trợ và hoạt động với tất cả các nhà cung cấp JPA. Tuy nhiên, nếu bản thân truy vấn không phải là truy vấn JPA, có nghĩa là, nó là truy vấn gốc, thì new ...cú pháp sẽ không hoạt động vì truy vấn được chuyển trực tiếp đến RDBMS bên dưới, không hiểu newtừ khóa vì nó không phải là một phần của tiêu chuẩn SQL.

Trong những tình huống như thế này, các lớp bean cần được thay thế bằng các giao diện Spring Data Projection .

Bước 1 : Khai báo giao diện chiếu

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Bước 2 : Trả về các thuộc tính dự kiến ​​từ truy vấn

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

Sử dụng AStừ khóa SQL để ánh xạ các trường kết quả thành các thuộc tính chiếu để ánh xạ rõ ràng.


1
Nó không làm việc, sa thải lỗi: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

Đây là gì SurveyAnswerReport trong đầu ra của bạn. Tôi giả sử bạn đã thay thế SurveyAnswerStatistics bằng lớp của riêng bạn SurveyAnswerReport. Bạn cần chỉ định tên lớp đủ điều kiện.
Bunti,

8
Lớp bean phải đủ tiêu chuẩn, nghĩa là bao gồm tên gói đầy đủ. Một cái gì đó giống như com.domain.dto.SurveyAnswerReport.
manish,

2
Tôi nhận được 'java.lang.IllegalArgumentException: PersentlyEntity không được rỗng!' Khi tôi thử trả về kiểu tùy chỉnh từ của tôi JpaRepository? Có phải một số cấu hình tôi đã bỏ qua?
marioosh

1
Trong khi sử dụng ngoại lệ truy vấn gốc cho biết: ngoại lệ lồng nhau là java.lang.IllegalArgumentException: Không phải là loại được quản lý: lớp ... Tại sao điều này nên được thực hiện?
Mikheil Zhghenti

20

Truy vấn SQL này trả về Danh sách <Đối tượng []> sẽ.

Bạn có thể làm theo cách này:

 @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
cảm ơn vì câu trả lời của bạn cho câu hỏi này. Đó là sắc nét và rõ ràng
Dheeraj R

@manish Cảm ơn bạn đã cứu giấc ngủ đêm của tôi, phương pháp của bạn hoạt động như một sự quyến rũ !!!!!!!
Vineel

15

Tôi biết đây là một câu hỏi cũ và nó đã được trả lời, nhưng đây là một cách tiếp cận khác:

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

Tôi thích câu trả lời của bạn vì nó không buộc tôi phải tạo một lớp hoặc giao diện mới. Nó đã làm việc cho tôi.
Yuri Hassle Araújo

Hoạt động tốt nhưng tôi thích sử dụng Bản đồ ở dạng chung thay vì ?, vì Bản đồ sẽ cho phép chúng tôi truy cập chúng dưới dạng khóa (0) và giá trị (1)
Samim Aftab Ahmed

10

Sử dụng giao diện bạn có thể nhận được mã đơn giản hơn. Không cần tạo và gọi các hàm tạo theo cách thủ công

Bước 1 : Khai báo Intefrace với các trường bắt buộc:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Bước 2 : Chọn các cột có cùng tên với getter trong giao diện và trả về intefrace từ phương thức kho lưu trữ:

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

}

Rất tiếc, các phép chiếu không thể được sử dụng làm đối tượng DTO từ góc nhìn GUI. Nếu bạn muốn sử dụng lại DTO để gửi biểu mẫu, bạn sẽ không thể. Bạn vẫn cần một bean thông thường riêng biệt với getters / setters. Vì vậy, nó không phải là một giải pháp tốt.
gen b.

Ngoài ra, còn thiếu Lớp Khảo sát
Mikheil Zhghenti

6

xác định một lớp pojo tùy chỉnh nói sureveyQueryAnalytics và lưu trữ giá trị trả về truy vấn trong lớp pojo tùy chỉnh của bạn

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

1
Giải pháp tốt hơn là sử dụng phép chiếu trong tài liệu chính thức.
Ninja

3

Tôi không thích tên kiểu java trong chuỗi truy vấn và xử lý nó bằng một hàm tạo cụ thể. Spring JPA ngầm gọi hàm tạo với kết quả truy vấn trong tham số 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();

Mã cần Lombok để giải quyết @Getter


@Getter đang hiển thị một lỗi trước khi chạy các mã như không của nó đối với các loại đối tượng
user666

Lombok là cần thiết. Chỉ cần thêm một chú thích cuối trang vào mã.
DWE

1

Tôi vừa giải quyết vấn đề này:

  • Phép chiếu dựa trên lớp không hoạt động với truy vấn native ( @Query(value = "SELECT ...", nativeQuery = true)), vì vậy tôi khuyên bạn nên xác định DTO tùy chỉnh bằng cách sử dụng giao diện.
  • Trước khi sử dụng DTO, nên xác minh theo cú pháp của truy vấn có đúng hay không

1

Tôi đã sử dụng DTO (giao diện) tùy chỉnh để ánh xạ một truy vấn gốc tới - cách tiếp cận linh hoạt nhất và tái cấu trúc an toàn.

Vấn đề tôi gặp phải với điều này - điều đáng ngạc nhiên là thứ tự của các trường trong giao diện và các cột trong truy vấn lại quan trọng. Tôi đã làm cho nó hoạt động bằng cách sắp xếp các bộ nhận giao diện theo thứ tự bảng chữ cái và sau đó sắp xếp các cột trong truy vấn theo cùng một cách.


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

}

Đoạn mã trên đã làm việc cho tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.