19 avril 2011

JPA, et après ...

Ceux qui suivent ce blog connaissent déjà Fonzie, mon petit hack pour éliminer l'écriture de toute la couche persistance d'une application JavaEE dans le cas des requêtes simples, qui constituent d'après le projet sur lequel il a été utilisé plus de 90% des cas d'utilisation.

Avec une API puissante, JPA nous permet de construire des requêtes et du code d'accès à la base pour quasiment tous les cas de figure (bien sûr il reste quelques exceptions, sinon le groupe de travail JPA 2.1 serait au chômage). Seulement, cette API est parfois surdimensionnée pour des requêtes simples, de la forme select * from Truc where bibi = 'foc'

Fonzie était inspiré par Grails (GORM), mais n'est pas, loin s'en faut, la seule tentative de simplifier le codage des accès à la base de données. Il y avait déjà Hades, présenté il y a deux ans à Devoxx, qui permet de déclarer un Repository sous forme d'interface et de laisser le framework analyser les noms de méthodes pour en déduire les requêtes. Il y a depuis peu Spring-Data et son module JPA qui reprend la même idée avec la force de frappe de SpringSource/VMWare.

C'est bien, mais certains (j'en suis) regretterons que tous ces facilitateurs ne valident pas les requêtes définies par les programmeurs. Il faut attendre l'exécution pour que le framework nous indique qu'une méthode ne peut être interprétée. Un refactoring de trop et hop, tout plante.

Pour répondre à cette attente il y a donc, depuis JPA 2.0, l'API Criteria. Contrairement à Hibernate Criteria, celle-ci est totalement type-safe et valide la construction des requêtes dès la compilation. C'est donc une très bonne chose a priori. Sauf que ... Criteria s'impose de couvrir 100% des capacités de JPQL, et aboutit à une syntaxe extrêmement lourde et très peu lisible; jugez plutôt, pour la même requête pourtant triviale :

CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<truc> query = cb.createQuery(Truc.class);

        Root<truc> truc = query.from(Truc.class);
        query.where(cb.eq( truc.<string>get( Truc_.bibi ), "foc" ));

        TypedQuery<truc> tq = em.createQuery(query);

L'informatique semble avoir horreur du vide, aussi un framework se propose de combler ce manque de lisibilité. L'idée est de générer (avec un annotation processor Java6) un métamodel qui soit spécifique aux requêtes QueryDSL (les classes Q*), et pas juste le métamodel "neutre" défini par JPA (mais qui par contre pourait servir à d'autres frameworks ... si l'API était sortie du package javax.persistence). QueryDSL, qui d'ailleurs peut accéder à bien plus que JPA, propose de construire des requêtes avec une excellente lisibilité, et 100% type-safe :

import static QTruc.truc;

   public List<Truc> getBibiFoc() {
        return from( truc )
              .where( truc.bibi.eq( "foc" ) )
              .list( truc );
   }

C'est pas beau ça ? Aux parenthèses près, on se croirait sous .Net en train de coder avec LINQ !
Alors, évidemment, il est probable qu'en creusant un peu queryDSL ne supporte pas 100% de JPQL, et ne permette pas de faire un LEFT OUTER JOIN sur une Map d' Embeddable, mais en même temps, le jour où j'aurais vraiment besoin de ça, ce n'est pas de coder le Repository qui exécute cette requête qui va me coûter mais plutôt de comprendre comment on a pu pondre un modèle pareil...

10 commentaires:

Unknown a dit…

Sympa la syntaxe! Pas trop pénible à gérer les imports static pour accéder aux champs?

Nicolas De Loof a dit…

l'import static c'est mon interprétation. La doc suggère de déclarer des variables locales initialisées par QEntity.entity.

D'ailleurs, si tu fait un repository pour chaque Entity majeur à laquelle ton appli accède (ce qui est généralement constaté) tu n'aura que 2,3 imports de ce genre maxi

Unknown a dit…

Oui très "fluent" comme DSL. Tant qu'à générer du code, c'est à ça qu'aurait du ressembler la criteria API de JPA2.

Jocelyn LECOMTE a dit…

Merci d'attirer notre attention sur ce projet !
Ca a l'air vraiment sympa, et un tour sur le site montre qu'on peut aussi requêter du NoSql, Lucene, ...
Et j'espère que les quelques trous qui peuvent exister maintenant seront gommés petit à petit !

Timo Westkämper a dit…

Merci beaucoup pour le article. ;)

Ollie a dit…

Hi,

as you seem to like both Spring Data JPA and Querydsl you'll be probably happy to hear that the latest M2 release of Spring Data JPA allows executing Querydsl predicates on a repository.

Cheers,
Ollie

- Spring Data JPA project lead

Nicolas De Loof a dit…

@Timo you're welcome
@Ollie I also noticed that, this is a nice option to get the best of both ! I'd like to see in spring-data a compile-time checker (APT?) to ensure the convention-based method names can be safely translated into queries, just my 2 cents ;)

Timo Westkämper a dit…

Left outer join with Map Embeddables should be possible :

<P> Q leftJoin(MapExpression<?&, P> target, Path<P> alias);

Or have you run into trouble with it?

Nicolas De Loof a dit…

@Timo I even didn't know it was a real use case ;) Could be interesting to read carefully the JPA spec and create a feature matrix that check how QueryDSL could be a partial or complete alternative to Criteria API.

Timo Westkämper a dit…

Here is a JPA 2 Criteria / Querydsl comparison