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?

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.


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

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

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

}

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

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


@ 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

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.


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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.