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) :

personIdnamePersonloginPerson
1Cédriccedric1337
2Raphaëlraphael32
3Jacquesjacques1024
4Johnjohn256


Chaque personne peut avoir plusieurs adresses (ou alors aucune). Elles sont stockées dans la table Address :

addressIdpersonIdnpaAddresscityAddress
113960Sierre
211018Lausanne
323960Sierre
441018Lausanne
541951Sion
643960Sierre


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 !