web-dev-qa-db-fra.com

Comment retourner un objet personnalisé à partir d'une requête Spring Data JPA GROUP BY

Je développe une application Spring Boot avec Spring Data JPA. J'utilise une requête JPQL personnalisée pour regrouper certains champs et obtenir le nombre. Voici ma méthode de référentiel.

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

Cela fonctionne et le résultat est obtenu comme suit:

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

J'aimerais avoir quelque chose comme ça:

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

Comment puis-je atteindre cet objectif?

73
Pranav C Balan

Solution pour les requêtes JPQL

Ceci est pris en charge pour les requêtes JPQL au sein de spécification JPA .

Étape 1 : Déclarez une classe de beans simple

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

Étape 2 : Renvoyez les instances de bean à partir de la méthode de référentiel.

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

Remarques importantes

  1. Assurez-vous de fournir le chemin complet de la classe de bean, y compris le nom du paquet. Par exemple, si la classe de bean est appelée MyBean et qu'elle se trouve dans le package com.path.to, le chemin qualifié complet menant au bean sera com.path.to.MyBean. Fournir simplement MyBean ne fonctionnera pas (sauf si la classe de bean est dans le paquet par défaut).
  2. Assurez-vous d'appeler le constructeur de la classe de haricot à l'aide du mot clé new. SELECT new com.path.to.MyBean(...) fonctionnera, alors que SELECT com.path.to.MyBean(...) ne fonctionnera pas.
  3. Assurez-vous de transmettre les attributs dans le même ordre que celui attendu dans le constructeur de bean. Tenter de transmettre des attributs dans un ordre différent entraînera une exception.
  4. Assurez-vous que la requête est une requête JPA valide, c'est-à-dire qu'il ne s'agit pas d'une requête native. @Query("SELECT ...") ou @Query(value = "SELECT ...") ou @Query(value = "SELECT ...", nativeQuery = false) fonctionnera, alors que @Query(value = "SELECT ...", nativeQuery = true) ne fonctionnera pas. En effet, les requêtes natives sont transmises sans modification au fournisseur JPA et sont exécutées sur le SGBDR sous-jacent en tant que tel. Puisque new et com.path.to.MyBean ne sont pas des mots-clés SQL valides, le SGBDR lève alors une exception.

Solution pour les requêtes natives

Comme indiqué ci-dessus, la syntaxe new ... est un mécanisme pris en charge par JPA et fonctionne avec tous les fournisseurs JPA. Toutefois, si la requête elle-même n’est pas une requête JPA, c’est-à-dire qu’elle est une requête native, la syntaxe new ... ne fonctionnera pas car la requête est transmise directement au SGBDR sous-jacent, qui ne comprend pas le new mot-clé puisqu'il ne fait pas partie du standard SQL.

Dans de telles situations, les classes de beans doivent être remplacées par des interfaces Spring Data Projection .

Étape 1 : Déclarez une interface de projection

package com.path.to;

public interface SurveyAnswerStatistics {
  String getAnswer();

  int getCnt();
}

Étape 2 : Renvoyer les propriétés projetées à partir de la requête.

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

Utilisez le mot clé SQL AS pour mapper les champs de résultat sur les propriétés de projection pour un mappage non ambigu.

180
manish

Cette requête SQL renvoie List <Object []>.

Vous pouvez le faire de cette façon:

 @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;
     }
 }
16
ozgur

Je sais que c'est une vieille question à laquelle il a déjà été répondu, mais voici une autre approche:

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

En utilisant des interfaces, vous pouvez obtenir un code plus simple. Pas besoin de créer et d'appeler manuellement des constructeurs

Étape 1: Déclarez intefrace avec les champs obligatoires:

public interface SurveyAnswerStatistics {

  String getAnswer();
  Long getCnt();

}

Étape 2: Sélectionnez les colonnes portant le même nom que getter dans l'interface et renvoyez intefrace à partir de la méthode de référentiel:

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

}
7
Nick Savenia

définir une classe pojo personnalisée, par exemple sureveyQueryAnalytics, et stocker la valeur renvoyée par la requête dans votre classe pojo personnalisée

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

Je n'aime pas les noms de type Java dans les chaînes de requête et les gère avec un constructeur spécifique. Spring JPA appelle implicitement le constructeur avec le résultat de la requête dans le paramètre 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();

Le code a besoin de Lombok pour résoudre @Getter

2
dwe

Je viens de résoudre ce problème:

  • Les projections basées sur des classes ne fonctionnent pas avec la requête native (@Query(value = "SELECT ...", nativeQuery = true)). Il est donc recommandé de définir un DTO personnalisé à l'aide d'une interface.
  • Avant d'utiliser DTO, vous devez vérifier si la requête est correcte du point de vue de la synchronisation ou non.
0
Yosra ADDALI