vendredi 16 mars 2012

JPA, Hibernate et requête constructeur




La spec JPA prévoit la possibilité de construire des POJO (non entité) à l'intérieur de requêtes JPQL (4.8.2 de la JSR317), ce qui s'avérer pratique pour requêter et construire des DTO en une étape. Je ne m'en prive donc pas :




TypedQuery<hoteldto> query = 
em.createQuery("select new org.blep.poc.hcq.HotelDto(h) from Hotel h",
HotelDto.class);




Et phénomène surprenant, en parcourant la liste de résultats, je m'aperçois que les entités identifiées dans une première requête et sont remontées unitairement :




Hibernate: /* select  new org.blep.poc.hcq.HotelDto(h) from Hotel h */
   select hotel0_.id as col_0_0_ from Hotel hotel0_
Hibernate: /* load org.blep.poc.hcq.Hotel */
  select hotel0_.id as id0_0_, hotel0_.address as address0_0_, hotel0_.city as city0_0_, hotel0_.name as name0_0_, hotel0_.price as price0_0_, hotel0_.state as state0_0_ from Hotel hotel0_ where hotel0_.id=?




Je me retrouve donc avec (nombre d'entités selectionnées + 1) requêtes soumises à la BDD (une requête de surface - shallow query- et les requêtes de chargement), ce qui peut vite devenir un frein pour les performances.



La doc Hibernate est plus que succincte sur le sujet des "constructor expressions", en gros c'est supporté mais il n'y a pas plus d'info qui explique ce comportement. Si la doc n'aide pas, il reste les retours d'expériences des autres utilisateurs dans les forums ou les blogs et en dernier lieu les bugs...  En fouinant un peu je trouve 
https://hibernate.onjira.com/browse/HHH-544 qui décrit parfaitement mon problème et Sir GK Himself répond en 2005:



 To be clear, all "select new" queries are considered "shallow" by the parser.
La date, statut de la JIRA et la réponse ne permettent pas d'envisager la négociation sur le sujet ni aucun tuning d'ailleurs pour influer sur le comportement! Il ne reste qu'une seule autre voie, la recherche des solutions de contournement...



La première tentée est l'utilisation de l'API Criteria, je ne suis pas fan mais si ça marche, comme le code reste inscrit dans JPA, pourquoi pas:






CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<HotelDto> cq = criteriaBuilder.createQuery(HotelDto.class);
Root<Hotel> from = cq.from(Hotel.class);
cq.select(criteriaBuilder.construct(HotelDto.class, from));
TypedQuery<HotelDto> query = em.createQuery(cq);






Rien que de le coder j'ai mal... mais en plus la sortie SQL montre que le Criteria est interprété pour générer du JPQL (tiens d'ailleurs ça me donne une raison de plus de ne pas aimer cette API!):






Hibernate: /* select new org.blep.poc.hcq.HotelDto(generatedAlias0) from Hotel as generatedAlias0 */ select hotel0_.id as col_0_0_ from Hotel hotel0_


Donc aucun intérêt puisque le comportement n'évolue pas.






La mort dans l'âme je me résouds à aller au delà de JPA et à m'adresser directement à Hibernate et il existe effectivement un moyen de combler le besoin mais pas dans le cadre d'un constructeur:






Query query = em.createQuery("select h as hotel from Hotel h");
org.hibernate.Query unwrapped = query.unwrap(org.hibernate.Query.class);
unwrapped.setResultTransformer(Transformers.aliasToBean(HotelDto.class));







Je sens d'ici les picotement dans les yeux de mon lecteur (un, c'est toute mon ambition en terme d'auditoire pour le moment!), toutefois les données sont obtenues en un seul SQL. Au rayon des contraintes, le DTO n'est pas peuplé par le constructeur mais par mutateur, ce qui implique qu'il doit y avoir un constructeur sans argument et que le mutateur est lié à l'alias dans le JPQL (ie le DTO doit présenter la méthode setHotel(Hotel). Le fait que le DTO puisse être construit sans argument ouvre la possibilité d'avoir un objet inconsistant, ce n'est pas souhaitable mais tellement fréquent... 






Possibilité suivante, utiliser un implémentation propre de Collection<HotelDto> encapsulant une Collection<Hotel> comme déléguée (code disponible sur
https://github.com/bleporini/JPA-Hibernate-shallow-constructor-query/blob/master/src/main/java/org/blep/poc/hcq/HotelDtoCollection.java). Du coup le code devient:





HotelDtoCollection hotels = new HotelDtoCollection(em.createQuery("select h from Hotel h", Hotel.class).getResultList());


C'est correct, ne nécessite pas de révéler une implémentation sous-jacente mais clairement ça fait pas mal de code boiler plate... Ca serait pas mal de trouver quelquechose de plus élégant.



Donc voici la voie basée sur Google Guava:





List<Hotel> resultList = em.createQuery("select h from Hotel h", Hotel.class).getResultList();
List<HotelDto> dtos = Lists.transform(resultList, new Function<Hotel, HotelDto>() {
    public HotelDto apply(@Nullable Hotel input) {
        return new HotelDto(input);
    }
});








C'est une possibilité aussi pertinente que celle de la collection déléguée mais nettement plus concise, la doc stipule que la liste résultante est peuplée tardivement, c'est parfait!



Côté performances, pour une requête ramenant 100 entités d'une base H2 embarquée, la solution du transformateur Hibernate prend 23ms, la collection déléguée 19ms et Guava 19ms sur mon poste.