Dans ce TP, nous allons reprendre le code du premier TP et le compléter pour gérer un autre moyen de persistance des objets via JPA au lieu de JDBC.
Les IDE comme Eclipse, IntelliJ ou Netbeans ont la capacité de générer automatiquement des POJO à partir de tables SQL. Le principe est que chaque table correspondra à une classe et que les colonnes d'une table auront un attribut de même nom et de type équivalent dans la classe associée. L'avantage majeur est que cette génération gère directement les jointures entre les tables en les transformant en attributs dans les classes.
Malheureusement la fonctionnalité "JPA Entities from Tables" d'Eclipse fonctionne mal, tout comme celle d'IntelliJ. Vous pouvez néanmoins suivre le tutoriel sur la génération des Entités JPA sous Netbeans (disponible sur Moodle). Sinon, si cela ne fonctionne pas, vous trouverez dans l'archive data.zip, le package "data" qui contient les 3 POJO Sport, Discipline et Sportif automatiquement générés par Netbeans à partir de la base SQL du TP1.
Rajoutez ce package avec ses classes dans le code de votre projet du TP1.
Pour pouvoir utiliser les POJO JPA, il faut convertir votre projet Java du premier TP en un projet JPA. Pour cela, dans le navigateur du projet, sélectionnez le projet, faites un clic droit -> Configure -> Convert to JPA projet .... Vérifiez que la case JPA est cochée. Cliquez deux fois sur Next puis dans la fenêtre "JPA Facet", dans la liste Type de "JPA implementation", choisissez Disable Library Configuration puis cliquez sur Finish.
Le résultat principal de cette action est la création d'un fichier nommé "persistence.xml" dans un répertoire nouvellement créé nommé "META_INF" dans les sources du projet. C'est le fichier de configuration de JPA.
Editez le contenu de ce fichier pour y placer le contenu suivant :
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="SportsPU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>data.Sport</class>
<class>data.Discipline</class>
<class>data.Sportif</class>
<properties>
<property name="javax.persistence.jdbc.url" value="URL"/>
<property name="javax.persistence.jdbc.user" value="LOGIN"/>
<property name="javax.persistence.jdbc.password" value="MDP"/>
<property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver"/>
<property name="hibernate.show_sql" value="true" />
</properties>
</persistence-unit>
</persistence>
Le contenu de ce fichier de persistance définit, dans l'ordre :
@Entity
.
Si vous voulez avoir un projet le plus possible portable comme nous le
faisons depuis le début avec les DAO génériques, il faut éviter de
mettre des configurations spécifiques au moteur JPA, ici
Hibernate. Normalement, votre code doit fonctionner de la même façon
avec un autre moteur, par exemple EclipseLink.
Néanmoins, cela peut être utile pour gérer des optimisations de
performance précises. Hibernate propose beaucoup de configurations en
ce sens (voir les documentations spécialisées à ce sujet).
Pour vérifier que tout fonctionne au niveau de JPA et des mappings avec la BDD, créez une classe "TestJPA" à la racine de votre projet Java qui contient le code suivant :
import data.Sport;
import data.Discipline;
import javax.persistence.*;
public class TestJPA {
public static void main(String argv[]) throws Exception {
// charge le gestionnaire d'entités lié à l'unité de persistance "SportsPU"
EntityManagerFactory emf = Persistence.createEntityManagerFactory("SportsPU");
EntityManager em = emf.createEntityManager();
System.out.println("PU chargée");
// récupère le sport d'identifiant 1, affiche son intitulé et ses disciplines
int cle = 1;
Sport sport = em.find(Sport.class, cle);
System.out.println("Le sport " + cle + " est " + sport.getIntitule());
for (Discipline disc : sport.getDisciplineSet())
System.out.println(" -> " + disc.getIntitule());
}
}
Exécutez ce code, vérifiez que les valeurs affichées sont bien celles qui sont dans votre BDD et étudiez les requêtes SQL réalisées par Hibernate. On constate qu'Hibernate fait une première requête pour récupérer le sport puis ensuite fait une deuxième requête pour récupérer les disciplines du sport (mais seulement une fois qu'on a appelé le getter pour récupérer les disciplines du sport) :
PU chargée
Hibernate: select sport0_.code_sport as code_spo1_2_0_, sport0_.intitule as intitule2_2_0_ from sport sport0_ where sport0_.code_sport=?
Le sport 1 est athlétisme
Hibernate: select discipline0_.code_sport as code_spo3_0_0_, discipline0_.code_discipline as code_dis1_0_0_, discipline0_.code_discipline as code_dis1_0_1_, discipline0_.code_sport as code_spo3_0_1_, discipline0_.intitule as intitule2_0_1_ from discipline discipline0_ where discipline0_.code_sport=?
-> 100 mètres
-> 200 mètres
-> saut en hauteur
-> saut en longueur
-> marathon
Le code des POJO générés fonctionne parfaitement et évite de faire des requêtes SQL à la main. Le gain en termes de temps de développement et d'optimisation est donc important (n'oubliez pas que par défaut, JPA charge les références de type collection d'objets à la demande et de manière transparente comme on vient de le voir dans l'exemple ci-dessus).
Néanmoins, il y a deux problèmes :
data.Discipline
, il y a un attribut de
type Sport
qui s'appelle "codeSport". On préférera
"sport" tout court mais Netbeans avait simplement repris le nom de la
clé étrangère comme nom d'attribut. De même, il y a un
atttribut Set<Sportif> sportifSet
que l'on pourrait
préférer être simplement nommé "sportifs".TestDAO
continue de fonctionner sans modification, il faut que
l'implémentation JPA se base sur ces POJO là et non ceux du package
"data".Pour gérer ces problèmes :
TestJPA
pour utiliser les POJO
du package "donnees" (changez les imports et d'éventuels appels de
getters). Vérifiez que cela fonctionne comme avant.DAO_JPA_Sport
,
DAO_JPA_Discipline
et DAO_JPA_Sportif
héritant de la classe abstraite DAO
.DAO_JPA<D>
Sports_JPA_DAO_Factory
héritant de la classe
abstraite SportsDAOFactory
. Modifiez la fabrique de
fabriques AbstractDAOFactory
pour gérer le cas de
JPA.TestDAO
et modifiez
uniquement la première ligne en :SportsDAOFactory factory =
AbstractDAOFactory.getDAOFactory(PersistenceKind.JPA);
Adresse
qui contiendra les 3 champs d'adresse d'un sportif (rue, ville et code
postal). Modifiez la classe Sportif
pour utiliser un
attribut de type Adresse
et l'intégrer avec une
annotation @Embedded.Vous n'avez sans doute pas fait attention, mais dans les logs de
lancement de vos projets Java sous Eclipse, vous voyez passer ceci
:
WARN: HHH10001002: Using Hibernate built-in
connection pool (not for production use!)
Hibernate vous informe qu'il ne faut pas l'utiliser en production (dans un "vrai" projet) ou du moins, pas avec son gestionnaire de connexions par défaut. Il est vrai que ce gestionnaire pose un gros problème : à chaque requête SQL, il ouvre une nouvelle connexion avec le serveur qu'il ne referme pas. Il est possible que ce mode de fonctionnement aboutisse à paralyser voire à planter le serveur SQL.
Il existe deux solutions à ce problème. La première est d'utiliser un autre moteur de persistance JPA qu'Hibernate. L'autre principal moteur qui existe est Eclipse Link dont le gestionnaire de connexions n'a pas les défauts de celui d'Hibernate. L'autre solution, comme nous avons déjà configuré notre projet pour fonctionner avec Hibernate, est d'utiliser avec Hibernate un autre gestionnaire de connexions comme par exemple c3p0.
Pour ajouter c3p0 à votre projet Java, rajoutez les dépendances Maven suivantes dans le fichier "pom.xml" (ou rajoutez à la main les fichiers .jar de l'archive jars-c3p0.zip si Maven ne fonctionne pas sur votre Eclipse) :
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>6.1.7.Final</version>
</dependency>
Ensuite, on peut configurer le gestionnaire de connexion de c3p0, par exemple, en ajoutant ces lignes dans le fichier "persistence.xml" :
<property name="hibernate.connection.provider_class" value="org.hibernate.connection.C3P0ConnectionProvider" />
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="10" />
<property name="hibernate.c3p0.timeout" value="500" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="300" />
En quelques mots, les valeurs de ces paramètres indiquent que :
Nous avons vu comment configurer le gestionnaire de connexions
pour optimiser la relation avec le serveur SQL mais il faut tout de
même ne pas faire n'importe quoi dans son code. Typiquement, ces 2
lignes de code ne doivent être exécutées qu'une seule fois dans un
projet Java :
EntityManagerFactory emf = Persistence.createEntityManagerFactory("MaPU");
EntityManager em = emf.createEntityManager();
La fabrique de gestionnaire d'entités ne doit pas être instanciée plusieurs fois car chaque instance ouvre des connexions avec le serveur SQL. Comme personne ne se soucie en général de fermer les fabriques ouvertes, au bout d'un moment, le système peut saturer et on ne peut plus communiquer avec le serveur.
Le gestionnaire d'entités doit être lui aussi instancié en un seul exemplaire. Techniquement, on peut en avoir autant qu'on veut dans un programme Java mais 1) il n'y a pas de raisons d'en avoir plusieurs si un seul suffit et 2) les POJO sont rattachés à un gestionnaire et on peut se retrouver avec des incohérences si deux objets Java interdépendants sont gérés par des gestionnaires distincts. Donc sauf cas particuliers, un seul gestionnaire d'entités à la fois suffit.
Utilisez un design pattern singleton pour instancier en un seul exemplaire un gestionnaire d'entités. Inspirez vous de celui de la classe SQLConnection du code fourni qui réalise cela pour avoir une connexion SQL unique dans le contexte de JDBC.
Dans ce cours, nous voyons une introduction à JPA, principalement par l'exemple et par l'usage : les classes des POJO sont générées automatiquement sans avoir besoin de modifier beaucoup de choses à part un peu de nommage et ensuite JPA gère lui-même les requêtes SQL et les jointures permettant d'associer les objets entre eux. En cas d'accès un peu complexe aux données, on utilisera des requêtes en JPQL sur les structures des classes Java à la place de faire du SQL sur les tables.
En termes de performances, JPA gère déjà un certain nombre de choses comme la configuration en lazy loading sur les jointures renvoyant des collections d'objets. Ainsi, comme nous l'avons vu, quand on charge un objet de type sport, on ne charge pas tout de suite toutes ses disciplines, cela sera seulement fait quand on appellera le getter sur la collection des disciplines. A ce moment là, JPA exécutera de manière transparente une requête SQL pour charger les objets Java de la collection soit au moment où on en aura besoin.
Si cette transparence est confortable d'un point de vue programmation (on n'a pas besoin de savoir comment ni quand les objets sont récupérés à partir de la base SQL), dans le cadre de développements avec de gros accès en base de données, il faudra tout de même comprendre précisément comment fonctionne JPA en arrière-plan pour éviter de gros problèmes de performance, comme le "N + 1 query problem". Sans rentrer dans les détails, l'idée est que quand on réalise une requête qui par exemple renvoie une collection de 50 objets Java que l'on parcourra ensuite par un itérateur, au lieu d'avoir une seule requête SQL qui charge le contenu de ces 50 objets en une seule fois, on pourra avoir une nouvelle requête SQL pour accèder au prochain objet de la collection (d'où le "N + 1") et donc 50 requêtes au total. La détection de ces problèmes de performance peut être complexe car d'un point de vue Java tout fonctionne correctement. Il faut regarder précisément les logs des requêtes SQL exécutées voire utiliser des outils d'analyse du code JPA pour repérer les problèmes. Mais tout cela sort du cadre de l'introduction à JPA de ce cours.
Eric Cariou, dernière modification : 08/01/24