Cara mengembalikan objek khusus dari kueri JPA GROUP BY Spring Data

115

Saya sedang mengembangkan aplikasi Spring Boot dengan Spring Data JPA. Saya menggunakan kueri JPQL kustom untuk mengelompokkan menurut beberapa bidang dan menghitungnya. Berikut adalah metode repositori saya.

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

Ini bekerja dan hasilnya diperoleh sebagai berikut:

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

Saya ingin mendapatkan sesuatu seperti ini:

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

Bagaimana saya bisa mencapai ini?

Pranav C Balan
sumber

Jawaban:

249

Solusi untuk kueri JPQL

Ini didukung untuk kueri JPQL dalam spesifikasi JPA .

Langkah 1 : Deklarasikan kelas kacang sederhana

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

Langkah 2 : Kembalikan instance kacang dari metode repositori

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

Catatan penting

  1. Pastikan untuk memberikan jalur yang sepenuhnya memenuhi syarat ke kelas kacang, termasuk nama paket. Misalnya, jika kelas kacang dipanggil MyBeandan berada dalam paket com.path.to, jalur yang sepenuhnya memenuhi syarat ke kacang akan menjadi com.path.to.MyBean. Hanya menyediakan MyBeantidak akan berhasil (kecuali kelas kacang ada dalam paket default).
  2. Pastikan untuk memanggil konstruktor kelas kacang menggunakan newkata kunci. SELECT new com.path.to.MyBean(...)akan bekerja, sedangkan SELECT com.path.to.MyBean(...)tidak.
  3. Pastikan untuk meneruskan atribut dengan urutan yang persis sama seperti yang diharapkan dalam pembuat kacang. Mencoba meneruskan atribut dalam urutan yang berbeda akan menyebabkan pengecualian.
  4. Pastikan kueri tersebut adalah kueri JPA yang valid, yang bukan kueri asli. @Query("SELECT ..."), atau @Query(value = "SELECT ..."), atau @Query(value = "SELECT ...", nativeQuery = false)akan berhasil, sedangkan @Query(value = "SELECT ...", nativeQuery = true)tidak akan berhasil. Ini karena kueri native diteruskan tanpa modifikasi ke penyedia JPA, dan dijalankan terhadap RDBMS yang mendasarinya. Karena newdan com.path.to.MyBeanbukan kata kunci SQL yang valid, RDBMS kemudian melontarkan pengecualian.

Solusi untuk kueri asli

Seperti disebutkan di atas, new ...sintaksis adalah mekanisme yang didukung JPA dan berfungsi dengan semua penyedia JPA. Namun, jika kueri itu sendiri bukan kueri JPA, yaitu kueri asli, new ...sintaksis tidak akan berfungsi karena kueri diteruskan langsung ke RDBMS yang mendasari, yang tidak memahami newkata kunci karena bukan bagian dari standar SQL.

Dalam situasi seperti ini, kelas kacang perlu diganti dengan antarmuka Proyeksi Data Musim Semi .

Langkah 1 : Deklarasikan antarmuka proyeksi

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Langkah 2 : Kembalikan properti yang diproyeksikan dari kueri

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

Gunakan ASkata kunci SQL untuk memetakan kolom hasil ke properti proyeksi untuk pemetaan yang tidak ambigu.

manish
sumber
1
Tidak berfungsi, kesalahan tembak: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
Apa ini SurveyAnswerReport dalam keluaran Anda. Saya menganggap Anda diganti SurveyAnswerStatistics dengan kelas Anda sendiri SurveyAnswerReport. Anda perlu menentukan nama kelas yang sepenuhnya memenuhi syarat.
Bunti
8
Kelas kacang harus sepenuhnya memenuhi syarat, yaitu menyertakan nama paket lengkap. Sesuatu seperti com.domain.dto.SurveyAnswerReport.
manish
2
Saya mendapat 'java.lang.IllegalArgumentException: PersistentEntity tidak boleh null! `Ketika saya mencoba mengembalikan tipe kustom dari my JpaRepository? Apakah ada konfigurasi yang terlewat?
marioosh
1
Saat menggunakan pengecualian kueri asli mengatakan: pengecualian bersarang adalah java.lang.IllegalArgumentException: Bukan tipe terkelola: kelas ... Mengapa ini harus terjadi?
Mikheil Zhghenti
20

Kueri SQL ini akan mengembalikan Daftar <Object []>.

Anda bisa melakukannya dengan cara ini:

 @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;
     }
 }
Ozgur
sumber
1
terima kasih atas tanggapan Anda atas pertanyaan ini. Itu tajam dan jelas
Dheeraj R
@manish Terima kasih Anda telah menyelamatkan tidur malam saya, metode Anda bekerja seperti pesona !!!!!!!
Vineel
15

Saya tahu ini adalah pertanyaan lama dan sudah dijawab, tetapi inilah pendekatan lain:

@Query("select new map(count(v) as cnt, v.answer) from Survey v group by v.answer")
public List<?> findSurveyCount();
rena
sumber
Saya suka jawaban Anda karena tidak memaksa saya untuk membuat kelas atau antarmuka baru. Itu berhasil untuk saya.
Yuri Hassle Araújo
Bekerja dengan baik tetapi saya lebih suka penggunaan Map dalam obat generik daripada?, Karena Peta akan membiarkan kita mengaksesnya sebagai kunci (0) dan nilai (1)
Samim Aftab Ahmed
10

Menggunakan antarmuka Anda bisa mendapatkan kode yang lebih sederhana. Tidak perlu membuat dan memanggil konstruktor secara manual

Langkah 1 : Deklarasikan intefrace dengan field yang diperlukan:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Langkah 2 : Pilih kolom dengan nama yang sama dengan getter di antarmuka dan kembalikan intefrace dari metode repositori:

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

}
Nick Savenia
sumber
Sayangnya Proyeksi tidak dapat digunakan sebagai objek DTO dari perspektif GUI. Jika Anda ingin menggunakan kembali DTO untuk pengiriman formulir, Anda tidak dapat melakukannya. Anda masih membutuhkan kacang biasa yang terpisah dengan getter / setter. Jadi ini bukan solusi yang bagus.
gen b.
Juga, ada Kelas Survei yang hilang
Mikheil Zhghenti
6

tentukan kelas pojo khusus, katakan sureveyQueryAnalytics dan simpan kueri yang mengembalikan nilai di kelas pojo khusus Anda

@Query(value = "select new com.xxx.xxx.class.SureveyQueryAnalytics(s.answer, count(sv)) from Survey s group by s.answer")
List<SureveyQueryAnalytics> calculateSurveyCount();
TanvirChowdhury
sumber
1
Solusinya lebih baik, atau gunakan proyeksi di dokumen resmi.
Ninja
3

Saya tidak suka nama tipe java dalam string kueri dan menanganinya dengan konstruktor tertentu. Spring JPA secara implisit memanggil konstruktor dengan hasil kueri dalam parameter 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();

Kode membutuhkan Lombok untuk menyelesaikan @Getter

dwe
sumber
@ Getter menunjukkan kesalahan sebelum menjalankan kode karena ini bukan untuk tipe objek
pengguna666
Lombok dibutuhkan. Baru saja menambahkan catatan kaki ke kode.
dwe
1

Saya baru saja memecahkan masalah ini:

  • Proyeksi berbasis kelas tidak bekerja dengan query native ( @Query(value = "SELECT ...", nativeQuery = true)) jadi saya sarankan untuk mendefinisikan DTO kustom menggunakan antarmuka.
  • Sebelum menggunakan DTO, Anda harus memverifikasi kueri yang secara sintatis benar atau tidak
Yosra ADDALI
sumber
1

Saya menggunakan DTO (antarmuka) khusus untuk memetakan kueri asli ke - pendekatan yang paling fleksibel dan aman untuk refactoring.

Masalah yang saya hadapi dengan ini - yang mengejutkan, urutan bidang di antarmuka dan kolom dalam kueri penting. Saya membuatnya berfungsi dengan memesan getter antarmuka berdasarkan abjad dan kemudian mengurutkan kolom dalam kueri dengan cara yang sama.

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

}

Kode di atas berhasil untuk saya.

Senthuran
sumber