วิธีส่งคืนออบเจ็กต์ที่กำหนดเองจากแบบสอบถาม Spring Data JPA GROUP BY


115

ฉันกำลังพัฒนาแอปพลิเคชัน Spring Boot ด้วย Spring Data JPA ฉันใช้แบบสอบถาม 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" }
]

ฉันจะบรรลุเป้าหมายนี้ได้อย่างไร?

คำตอบ:


252

โซลูชันสำหรับแบบสอบถาม JPQL

นี้ได้รับการสนับสนุนสำหรับการค้นหา JPQL ภายในข้อกำหนด JPA

ขั้นตอนที่ 1 : ประกาศคลาสถั่วง่ายๆ

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

ขั้นตอนที่ 2 : ส่งคืนอินสแตนซ์ 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();
}

หมายเหตุสำคัญ

  1. ตรวจสอบให้แน่ใจว่าได้ระบุพา ธ แบบเต็มไปยังคลาส bean รวมถึงชื่อแพ็กเกจ ตัวอย่างเช่นถ้าระดับถั่วที่เรียกว่าMyBeanและมันก็เป็นในแพคเกจเส้นทางที่มีคุณสมบัติครบถ้วนที่จะถั่วจะเป็นcom.path.to com.path.to.MyBeanการให้เพียงอย่างเดียวMyBeanจะไม่ทำงาน (เว้นแต่คลาส bean จะอยู่ในแพ็คเกจเริ่มต้น)
  2. ตรวจสอบให้แน่ใจว่าได้เรียกตัวสร้างคลาส bean โดยใช้newคีย์เวิร์ด SELECT new com.path.to.MyBean(...)จะทำงานในขณะที่SELECT com.path.to.MyBean(...)จะไม่
  3. ตรวจสอบให้แน่ใจว่าได้ส่งแอตทริบิวต์ในลำดับเดียวกันกับที่คาดไว้ในตัวสร้าง bean การพยายามส่งผ่านแอตทริบิวต์ในลำดับที่แตกต่างกันจะนำไปสู่ข้อยกเว้น
  4. ตรวจสอบให้แน่ใจว่าแบบสอบถามเป็นแบบสอบถาม 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 ที่อยู่ข้างใต้ซึ่งไม่เข้าใจnewคำหลักเนื่องจากไม่ได้เป็นส่วนหนึ่งของ มาตรฐาน SQL

ในสถานการณ์เช่นนี้คลาส bean จำเป็นต้องถูกแทนที่ด้วยอินเทอร์เฟซSpring Data Projection

ขั้นตอนที่ 1 : ประกาศอินเทอร์เฟซการฉายภาพ

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

ขั้นตอนที่ 2 : ส่งคืนคุณสมบัติที่คาดการณ์ไว้จากแบบสอบถาม

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

ใช้ASคีย์เวิร์ดSQL เพื่อแม็พฟิลด์ผลลัพธ์กับคุณสมบัติการฉายสำหรับการแม็พที่ไม่คลุมเครือ


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

นี่คืออะไรSurveyAnswerReport ในผลลัพธ์ของคุณ ผมถือว่าคุณแทนที่กับชั้นเรียนของคุณเองSurveyAnswerStatistics SurveyAnswerReportคุณต้องระบุชื่อคลาสแบบเต็ม
Bunti

8
คลาส bean ต้องมีคุณสมบัติครบถ้วนนั่นคือรวมชื่อแพ็กเกจแบบเต็ม บางอย่างเช่นcom.domain.dto.SurveyAnswerReport.
manish

2
ฉันได้รับ 'java.lang.IllegalArgumentException: PersistentEntity ต้องไม่เป็น null!' เมื่อฉันลองส่งคืนประเภทที่กำหนดเองจากของฉันJpaRepository? ฉันพลาดการกำหนดค่าบางอย่างหรือไม่
marioosh

1
ในขณะที่ใช้ข้อยกเว้นการสืบค้นแบบเนทีฟกล่าวว่า: ข้อยกเว้นที่ซ้อนกันคือ java.lang.IllegalArgumentException: ไม่ใช่ประเภทที่มีการจัดการ: class ... เหตุใดจึงควรเกิดขึ้น
Mikheil Zhghenti

20

แบบสอบถาม SQL นี้ส่งคืนรายการ <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

15

ฉันรู้ว่านี่เป็นคำถามเก่าและมีคำตอบแล้ว แต่นี่เป็นอีกแนวทางหนึ่ง:

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

ฉันชอบคำตอบของคุณเพราะไม่ได้บังคับให้ฉันต้องสร้างคลาสหรืออินเทอร์เฟซใหม่ มันได้ผลสำหรับฉัน
Yuri Hassle Araújo

ใช้งานได้ดี แต่ฉันชอบการใช้แผนที่ในรูปแบบทั่วไปแทนที่จะเป็น? เนื่องจากแผนที่จะช่วยให้เราเข้าถึงเป็นคีย์ (0) และค่า (1)
Samim Aftab Ahmed

10

การใช้อินเทอร์เฟซคุณจะได้รับโค้ดที่ง่ายขึ้น ไม่จำเป็นต้องสร้างและเรียกตัวสร้างด้วยตนเอง

ขั้นตอนที่ 1 : ประกาศ Intefrace ด้วยฟิลด์บังคับ:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

ขั้นตอนที่ 2 : เลือกคอลัมน์ที่มีชื่อเดียวกันกับ getter ในอินเทอร์เฟซและส่งคืน intefrace จากวิธีการเก็บ:

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

}

ขออภัยไม่สามารถใช้ Projections เป็นวัตถุ DTO จากมุมมอง GUI หากคุณต้องการใช้ DTO ซ้ำในการส่งแบบฟอร์มคุณจะไม่สามารถทำได้ คุณยังต้องการถั่วธรรมดาแยกต่างหากกับ getters / setters ดังนั้นจึงไม่ใช่ทางออกที่ดี
ยีนข.

นอกจากนี้ยังไม่มี Survey Class
Mikheil Zhghenti

6

กำหนดคลาส pojo ที่กำหนดเองพูดว่า sureveyQueryAnalytics และเก็บค่าที่ส่งคืนแบบสอบถามในคลาส pojo ที่คุณกำหนดเอง

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

1
วิธีแก้ปัญหาจะดีกว่าหรือใช้การฉายภาพในเอกสารอย่างเป็นทางการ
Ninja

3

ฉันไม่ชอบชื่อประเภท 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

ลอมบอกเป็นสิ่งจำเป็น เพิ่งเพิ่มเชิงอรรถในรหัส
dwe

1

ฉันเพิ่งแก้ไขปัญหานี้:

  • การคาดการณ์ตามคลาสใช้ไม่ได้กับคิวรีเนทีฟ ( @Query(value = "SELECT ...", nativeQuery = true)) ดังนั้นฉันขอแนะนำให้กำหนด DTO แบบกำหนดเองโดยใช้อินเทอร์เฟซ
  • ก่อนใช้ DTO ควรตรวจสอบคิวรีว่าถูกต้องตามหลักไวยากรณ์หรือไม่

1

ฉันใช้ DTO (อินเทอร์เฟซ) ที่กำหนดเองเพื่อแมปแบบสอบถามเนทีฟกับ - วิธีการที่ยืดหยุ่นที่สุดและการปรับโครงสร้างใหม่ - ปลอดภัย

ปัญหาที่ฉันมีกับสิ่งนี้ - น่าแปลกใจที่ลำดับของฟิลด์ในอินเทอร์เฟซและคอลัมน์ในแบบสอบถามมีความสำคัญ ฉันทำให้มันใช้งานได้โดยสั่งให้อินเทอร์เฟซ getters ตามตัวอักษรแล้วเรียงลำดับคอลัมน์ในแบบสอบถามในลักษณะเดียวกัน


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

}

รหัสด้านบนใช้ได้ผลสำหรับฉัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.