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


115

Je développe une application Spring Boot avec Spring Data JPA. J'utilise une requête JPQL personnalisée pour grouper par un champ et obtenir le nombre. Voici ma méthode de dépôt.

@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 obtenir quelque chose comme ceci:

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

Comment puis-je atteindre cet objectif?

Réponses:


249

Solution pour les requêtes JPQL

Ceci est pris en charge pour les requêtes JPQL dans la spécification JPA .

Étape 1 : Déclarez une classe de bean 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 : renvoyer les instances de bean à partir de la méthode du 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();
}

Notes IMPORTANTES

  1. Assurez-vous de fournir le chemin d'accès complet à la classe du bean, y compris le nom du package. Par exemple, si la classe du bean est appelée MyBeanet qu'elle est dans le package com.path.to, le chemin d'accès complet au bean sera com.path.to.MyBean. Le simple fait de fournir MyBeanne fonctionnera pas (sauf si la classe du bean est dans le package par défaut).
  2. Assurez-vous d'appeler le constructeur de classe bean en utilisant le newmot - clé. SELECT new com.path.to.MyBean(...)fonctionnera, alors que SELECT com.path.to.MyBean(...)non.
  3. Assurez-vous de passer les attributs exactement dans le même ordre que celui attendu dans le constructeur du bean. Tenter de transmettre les 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. Cela est dû au fait que les requêtes natives sont transmises sans modifications au fournisseur JPA et sont exécutées sur le SGBDR sous-jacent en tant que tel. Puisque newet com.path.to.MyBeanne 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 new ...syntaxe est un mécanisme pris en charge par JPA et fonctionne avec tous les fournisseurs JPA. Cependant, si la requête elle-même n'est pas une requête JPA, c'est-à-dire une requête native, la new ...syntaxe ne fonctionnera pas car la requête est transmise directement au SGBDR sous-jacent, qui ne comprend pas lenew mot clé car il ne fait pas partie de la norme SQL.

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

Étape 1 : déclarer 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 ASmot clé SQL pour mapper les champs de résultat aux propriétés de projection pour un mappage sans ambiguïté.


1
Cela ne fonctionne pas, erreur de tir: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

Qu'est-ce que c'est SurveyAnswerReport dans votre sortie. Je suppose que vous avez remplacé SurveyAnswerStatistics par votre propre classe SurveyAnswerReport. Vous devez spécifier le nom complet de la classe.
Bunti

8
La classe de bean doit être pleinement qualifiée, c'est-à-dire inclure le nom complet du package. Quelque chose comme com.domain.dto.SurveyAnswerReport.
manish

2
J'ai 'java.lang.IllegalArgumentException: PersistentEntity ne doit pas être nul!' Quand j'essaye de renvoyer le type personnalisé de mon JpaRepository? Une configuration me manque-t-elle?
marioosh

1
Lors de l'utilisation de l'exception de requête native, il est dit: l'exception imbriquée est java.lang.IllegalArgumentException: Pas un type géré: classe ... Pourquoi cela devrait-il se produire?
Mikheil Zhghenti le

20

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

1
merci pour votre réponse à cette question. C'était net et clair
Dheeraj R

@manish Merci d'avoir sauvé ma nuit de sommeil, votre méthode a fonctionné comme un charme !!!!!!!
Vineel

15

Je sais que c'est une vieille question à laquelle on a déjà 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();

J'aime votre réponse car elle ne m'oblige pas à créer une nouvelle classe ou une nouvelle interface. Cela a fonctionné pour moi.
Yuri Hassle Araújo

Fonctionne bien mais je préfère l'utilisation de Map dans les génériques au lieu de?,
Samim Aftab Ahmed

10

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 l'intégrale 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 l'interface à partir de la méthode du 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();

}

Malheureusement, les projections ne peuvent pas être utilisées comme objets DTO du point de vue de l'interface graphique. Si vous vouliez réutiliser les DTO pour la soumission de formulaires, vous ne seriez pas en mesure de le faire. Vous auriez toujours besoin d'un haricot régulier séparé avec des getters / setters. Ce n'est donc pas une bonne solution.
gène b.

De plus, il manque une classe d'enquête
Mikheil Zhghenti le

6

définir une classe pojo personnalisée, dire 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();

1
La solution est meilleure, ou utilisez la projection dans le document officiel.
Ninja

3

Je n'aime pas les noms de type java dans les chaînes de requête et je 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


@Getter affiche une erreur avant d'exécuter le code car ce n'est pas pour le type d'objet
user666

Lombok est nécessaire. Je viens d'ajouter une note de bas de page au code.
dwe

1

Je viens de résoudre ce problème:

  • Les projections basées sur les classes ne fonctionnent pas avec query native ( @Query(value = "SELECT ...", nativeQuery = true)), je recommande donc de définir un DTO personnalisé à l'aide de l'interface.
  • Avant d'utiliser DTO, vérifiez que la requête est syntatiquement correcte ou non

1

J'ai utilisé un DTO (interface) personnalisé pour mapper une requête native vers - l'approche la plus flexible et la refactorisation sûre.

Le problème que j'ai eu avec cela - que, étonnamment, l'ordre des champs dans l'interface et les colonnes dans la requête est important. Je l'ai fait fonctionner en ordonnant les getters d'interface par ordre alphabétique, puis en ordonnant les colonnes de la requête de la même manière.


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

}

Le code ci-dessus a fonctionné pour moi.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.