JPA et les jointures
Par Cédric Tabin le mercredi 24.02.2010, 08:00 - Java - Lien permanent
La Java Persistence API (JPA) est la spécification faisant partie de Java EE 5 (JEE5) définissant un langage SQL (le JPQL) sensé être portable. Toutefois je suis tombé sur une importante limitation de la spécification JPA elle-même : il n'est pas possible de mettre des conditions sur des jointures externes...
Afin de mieux comprendre le problème, je vais utiliser un exemple simple portant sur deux tables. La première table (Person) contient une liste de personnes avec les données suivantes (l'en-tête est le nom de la colonne) :
personId | namePerson | loginPerson |
---|---|---|
1 | Cédric | cedric1337 |
2 | Raphaël | raphael32 |
3 | Jacques | jacques1024 |
4 | John | john256 |
Chaque personne peut avoir plusieurs adresses (ou alors aucune). Elles sont stockées dans la table Address :
addressId | personId | npaAddress | cityAddress |
---|---|---|---|
1 | 1 | 3960 | Sierre |
2 | 1 | 1018 | Lausanne |
3 | 2 | 3960 | Sierre |
4 | 4 | 1018 | Lausanne |
5 | 4 | 1951 | Sion |
6 | 4 | 3960 | Sierre |
On peut constater que Jacques ne possède aucune adresse alors que John en a quatre. Il y a deux autres détails importants pour la suite :
- toutes les personnes ont une adresse 3960 Sierre (sauf Jacques)
- Raphaël n'a pas d'adresse 1018 Lausanne
L'idée est maintenant la suivante : ressortir à chaque fois toutes les personnes, mais avec une contrainte sur l'adresse pour ceux qui en ont. D'aucun diront que c'est une bête jointure externe et que la requête suivante fonctionne :
SELECT p FROM Person p LEFT JOIN p.addresses a WHERE (a.npa='3960' OR a.npa IS NULL)
Ce qui donne comme résultat :
1 -> Cédric (cedric1337) with 2 addresses. 2 -> Raphaël (raphael32) with 1 addresses. 3 -> Jacques (jacques1024) with 0 addresses. 4 -> John (john256) with 3 addresses.
Au premier abord, ce résultat semble correct, puisque toutes les personnes sont bien la, y compris Jacques ! Maintenant, essayons la même chose pour la ville de Lausanne (il suffit de changer le NPA) :
SELECT p FROM Person p LEFT JOIN p.addresses a WHERE (a.npa='1018' OR a.npa IS NULL)
La sortie est la suivante :
1 -> Cédric (cedric1337) with 2 addresses. 3 -> Jacques (jacques1024) with 0 addresses. 4 -> John (john256) with 3 addresses.
D'un coup, la ligne Raphaël disparait de la liste des résultats. Et c'est tout à fait normal car aucune donnée ne correspond à la contrainte requise qui peut se traduire par 'soit il y a une adresse avec le NPA 1018, soit il n'y a aucune adresse pour la personne'. Hors pour Raphaël, il y a une adresse mais qui ne correspond à aucun critère voulu !
Dès lors, comment contourner le problème ? Les manias du SQL auront tout se suite remarqué que la contrainte de jointure (donc le NPA) est au mauvais endroit. Une requête standard, ressemblerait à ceci :
SELECT * FROM Person p LEFT JOIN Address a ON (a.personId = p.personId AND a.npaAddress='1018')
Le fait est que la spécification JPA ne supporte pas cette syntaxe ! Il est impossible selon cette dernière de mettre des conditions de jointures comme dans la requête ci-dessus... Et si l'on veut éviter d'utiliser des requêtes natives, cela pose évidemment un gros souci !
Malgré tout, il existe une solution digne de ce nom : utiliser Hibernate comme moteur de persistence ! Ce dernier est non seulement compatible JPA mais supporte quelques fonctionnalités supplémentaires. Ce fut dur à trouver, mais dans ce topic se trouve la réponse : le mot-clé WITH !
Du coup, il la requête JPQL devient :
SELECT p FROM Person p LEFT JOIN p.addresses a WITH (a.npa='1018' OR a.npa IS NULL)
Et la sortie est correcte :
1 -> Cédric (cedric1337) with 2 addresses. 2 -> Raphaël (raphael32) with 1 addresses. 3 -> Jacques (jacques1024) with 0 addresses. 4 -> John (john256) with 3 addresses.
Toutefois cette solution force l'application à utiliser Hibernate (au lieu d'avoir le choix entre TopLink, EclipseLink ou OpenJPA pour ne citer que les plus connus). Il faut évidemment faire attention à la portabilité : si cette fonctionnalité n'a pas été incluse dans JPA, c'est parce qu'elle n'est probablement pas supportée par toutes les base de données !
J'ai mis en annexe un projet NetBeans 6.8 / GlassFish v3 qui illustre les étapes que j'ai décrites ci-dessus. Je remercie également Michel Ganguin pour ses recherches !
Commentaires
Salut...
François
Hello,
Oui cela fonctionne, il faut juste créer l'entité correspondante dans le code.
@++