Architecture n tiers(Java EE) Partie 1: Mapping Objet Relationnel Chapitre 4: Hibernate
Généralités Outil ORM ou Cadre (Framework) de persistance libre (open source) gérant la persistance des objets Java/JavaEE en base de données relationnelle [Wikipédia,Pat05] Version 3.2.x : implémentation du standard de persistance EJB 3.0 Java Persistence API (JPA) Possibilité d’être utilisé aussi bien dans un développement client lourd, que dans un environnement web léger de type Apache Tomcat ou dans un environnement J2EE complet [Wikipédia] Code SQL généré à l’exécution via des informations fournies dans un document de correspondance (mapping) XML ou des annotations
Différents modules Hibernate Core : API native implémentant les services de base pour la persistance Méta-données au format XML Langage HQL et interface pour écrire des requêtes Hibernate Annotations : Remplacement des fichiers XML par des annotations JDK 5.0 implémentant les annotations du standard JPA + annotations spécifiques à Hibernate
Objectifs d’Hibernate Que permet de faire Hibernate ? Hibernate s’occupe du transfert des classes Java dans les tables de la base de données (et des types de données Java dans les types de données SQL) Quel avantage est apporté par Hibernate ? Il réduit de manière significative le temps de développement qui aurait été autrement perdu dans une manipulation manuelle des données via SQL et JDBC.
Architecture du noyau Hibernate Fichier de configuration Version XML : hibernate.cfg.xml permettant un paramétrage plus fin Configuration par programmation
Architecture du noyau Hibernate
Architecture du noyau Hibernate SessionFactory (org.hibernate.SessionFactory) : Cache immuable (threadsafe) des correspondances (mappings) vers une (et une seule) base de données Coûteuse à construire car implique l’analyse des fichiers de Configuration Construite à partir d’un objet Configuration Session (org.hibernate.Session) : Objet mono-threadé, à durée de vie courte, représentant une conversation entre l'application et l'entrepôt de persistance Encapsule une connexion JDBC Contient un cache des objets persistants
Architecture du noyau Hibernate
Hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver </property> <property name="hibernate.connection.password">root</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/orm <property name="hibernate.connection.username">root</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect
Hibernate.cfg.xml(suite) <property name="hbm2ddl.auto">update</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property> <property name="current_session_context_class">thread</property> <mapping resource="contexte/Employer.hbm.xml" /> </session-factory> </hibernate-configuration>
Configuration d’Hibernate Déclaration du type de document utilisé par l’analyseur syntaxique (parseur) XML pour valider le document de configuration d’après la DTD de configuration d’Hibernate : <?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN " "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> Paramètres de configuration nécessaires pour la connexion JDBC : <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver </property> <property name="hibernate.connection.password">root</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/orm <property name="hibernate.connection.username">root</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect
Configuration d’hibernate Activation de la génération automatique des schémas de base de données - directement dans la base de données : <property name="hbm2ddl.auto">create</property> Autre valeur update Fichier de configuration (fichier de mapping) des classes persistantes : <mapping resource="contexte/Employer.hbm.xml"/>
Configuration par programmation Configuration cfg = new Configuration() .addClass(Person.class) .addClass(Event.class) .setProperty(Environment.HBM2DDL_AUTO, "create"); cfg.setProperty("hibernate.show_sql", "true"); cfg.setProperty("hibernate.connection.driver_class", "com.mysql.jdbc.Driver ") .setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect") .setProperty("hibernate.connection.url", "jdbc:mysql://localhost:3306/orm") .setProperty("hibernate.connection.username", "root") .setProperty("hibernate.connection.password", "root") .setProperty("hibernate.order_updates", "true"); factory = cfg.buildSessionFactory();
Environnement Hibernate Bibliothèques Hibernate Core (en plus de hibernate3.jar) : antlr.jar ANTLR (Another Tool for Language Recognition) - Indispensable à l’exécution commons-collections.jar Bibliothèques du projet Apache Jakarta Commons pour manipuler les collections - Indispensable à l’exécution Jta.jar API JTA strandard – requis pour les applications s’exécutant en dehors d’un serveur d’application dom4j.jar Analyseur Syntaxique de configuration XML et de mapping - Indispensable à l’exécution log4j jar log4j.Mécanisme de log sous-jacent pouvant être utilisé par Commons Logging - Optionnel
Environnement Hibernate Fichiers nécessaires avec Hibernate Core : hibernate.cfg.xml : fichier de configuration globale contenant Les paramètres de connexion à la base de données (pilote, login, mot de passe, url, etc.) Le dialecte SQL de la base de données La gestion de pool de connexions Le niveau de détails des traces etc. Pour chaque classe persistante : ClassePersistante.java : Implémentation POJO (Plain Old Java Objects) de la classe ClassePersistante.hbm.xml : Fichier XML de correspondance (mapping) ClassePersistanteHome.java : Implémentation du DAO (Data Access Object) pour l’isolation avec la couche de persistance – Optionnel
Classes persistantes Objets persistants implémentés sous la forme de POJO Pas d’implémentation de certaines interfaces d’héritage de classes particulières Des règles à respecter : Implémenter un constructeur sans paramètre (pouvant être privé mais de préférence accessible par le paquetage) Fournir une propriété d'identifiant (optionnel mais fortement recommandé) Implémenter des modificateurs (mutateurs - setter) et accesseurs(getter) pour chaque champ persistant
La classe Employer package contexte; public class Employer implements java.io.Serializable{ private int id; private String nom; private String prenom; private float salaire; public Employer() {} public Employer(String nom, String prenom, Float salaire) { this.nom = nom; this.prenom = prenom; this.salaire = salaire; }
La classe Employer public int getNo() { return no;} public void setNo(int no) { this.no = no;} public String getNom() { return nom;} public void setNom(String nom) { this.nom = nom;} public String getPrenom() { return prenom;} public void setPrenom(String prenom) { this.prenom = prenom;} public Float getSalaire() { return salaire;} public void setSalaire(Float salaire) { this.salaire = salaire;} }
Fichier de correspondance Hibernate a besoin de savoir comment charger et stocker des objets d’une classe persistante. Le fichier de mapping indique à Hibernate à quelle table dans la base de données il doit accéder, et quelles colonnes de cette table il devra utiliser. Le fichier de mapping doit être stocké dans le même répertoire que le fichier source.
Employer.hbm.xml <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="contexte"> <class name="Employer" table="employee"> <id name="no" type="int"> <column name="no" /> <generator class="increment" /> </id> <property name="nom" type="java.lang.String"> <column name="nom" /> </property>
Employer.hbm.xml <property name="prenom" type="java.lang.String"> <column name="prenom" /> </property> <property name="salaire" type="float"> <column name="salaire" /> </class> </hibernate-mapping>
Pour manipuler les objets persistantes Ouverture d’une Session Hibernate [ Débuter une transaction] . débute une transaction ] – fortement conseillé Appliquer les opérations de Session pour interagir avec l’environnement de persistance [Valider ( commit() ) la transaction en cours] Synchroniser avec la base de données (flush) et fermer la session
Manipuler les objets persistantes SessionFactory sf= new Configuration().configure().buildSessionFactory(); sf.openSession(); Transaction tx = null; Session session=sf.getCurrentSession(); try {tx = session.beginTransaction(); Employer c1 = new Employer("Romdhani", "Mohamed",100f); session.save(c1); session.flush(); tx.commit(); } catch (Exception e) { if (tx != null) { tx.rollback(); sf.close();}}
Diagramme d’états des objets d’une classe persistante * Méthodes JPA - implémentées dans Hibernate EntityManager mais pas dans Hibernate Core
Contexte de persistance Session associée à un contexte de persistance : Vérification des modifications et synchronisation avec la base de données (automatic dirty checking) Garantie de l’identité Java Extension du contexte de persistance à une conversation (unité de travail longue)
Contexte de persistance Utilisation du cache : Pour vérifier les mises à jour Pour améliorer les performances en diminuant l’interaction avec la base de données Pour éviter les conflits de représentation (pas deux objets correspondants au même nuplet) Possibilité de gérer le cache (détachement d’objets et nettoyage du contexte de persistance)
Contexte de persistance Opérations Session Récupérer une instance persistante Rendre une instance persistante Rendre persistantes les modifications apportées à une instance persistante Rendre persistantes les modifications apportées à une instance détachée Ré-attacher une instance détachée Détacher une instance persistante Supprimer une instance persistante Rafraîchir une instance Détecter automatiquement un état
Contexte de persistance Récupérer une instance persistante dans Hibernate Core : session.load(Class cl, serializable id) ⇒ Levée d’une exception irrécupérable s'il n'y a pas de ligne correspondante dans la base de données session .get(Class cl, serializable id) ⇒ null si pas de nuplet correspondant dans la base
Contexte de persistance Pour ces deux méthodes : Génération d’un ordre SQL un SELECT sur la base pour récupérer l’entité à partir de son identificateur Possibilité de charger dans une instance nouvellement créée ou dans une instance existante Possibilité de transformer le SELECT en SELECT ... FOR UPDATE en utilisant LockMode.UPGRADE en 3ème paramètre Possibilité de récupérer une instance aussi par les API Query, Criteria, SQLQuery
Contexte de persistance session.save(objet) Pour rendre persistante une instance temporaire Génération d’une commande INSERT uniquement exécutée au moment du lancement de la méthode session.commit() session.persist(objet) session.merge(objet) Fusionne une instance détachée avec une instante persistante (existante ou chargée depuis la base) Effectue un SELECT avant pour déterminer s’il faut faire INSERT ou UPDATE
Contexte de persistance Rendre persistante les modifications apportées à une instance persistante : ⇒ Pas de méthode particulière (automatic dirty checking) Tout modification d’une instance persistante transactionnelle (objet chargé, sauvegardé, créé ou requêté par la Session) est rendu persistant par la méthode flush() Surveillance (dirty checking) de toutes les instances persistantes par la session Instance persistante modifiée = instance sale (dirty) Synchronisation avec la base définitive une fois la transaction sous-jacente validée
Contexte de persistance Rendre persistante les modifications apportées à une instance détachée : Pas de surveillance possible des instances détachées ⇒ nécessité de ré-attacher les instances en rendant persistant les modifications apportées session.merge(objet) Effectue un SELECT avant l’UPDATE pour récupérer les données dans la base et les fusionner avec les modifications apportées Retourne l’instance persistante correspondante session.update(objet) Force la mise à jour (UPDATE) de l’objet dans la base Lève une exception si une instance de même identificateur existe dans la Session
Contexte de persistance Détacher une instance persistante : Plus de surveillance de l’instance par la Session : ⇒Plus aucune modification rendue persistante de manière transparente Trois moyens de détacher une instance : En fermant la session : session.close() En vidant la session : session.clear() En détachant une instance particulière: session.evict(objet)
Contexte de persistance Rendre un objet transient : ⇒ Extraction définitive de l’entité correspondante dans la base de données session.delete(objet) : Enregistrement correspondant plus présent dans la base Instance toujours présente dans la JVM tant que l’objet est référencé – instance transiente
Correspondance des associations Partie la plus complexe dont dépend les performances de l’application Balises de correspondance des collections : <set>, <list>; <map>, <bag>, <array> … Tags de correspondance des cardinalités/multiplicités : <one-to-one>, <many-to-one>, <many-to-many> Si correspondance d’une collection : utilisation d’une balise de collection contenant un tag de cardinalité Si correspondance d’une association uni ou bidirectionnelle vers une entité : utilisation des balises de cardinalité
Correspondance des associations <many-to-one> et <one-to-one> : associations uni ou bi-directionnelle vers une entité name : nom de la propriété column : nom de la clé étrangère – possibilité de balise <column> class : nom de la classe associée – optionnel par défaut le type de la propriété déterminé par réflexion cascade : Propagation des opérations de l'objet père vers les objets associés – optionnel + d’autres – cf. doc Hibernate
Many-to-one Exemple d’association uni-directionnelle : public class Enseignant { private int id ; private String nom; private float salaire; private Departement departement; Enseignant Departement 1 *
Many-to-one <class name="Enseignant" table="enseignant"> <id name="id" type="int"> <column name="id"/> <generator class="native"/> </id> <property name="nom" type="string"><column name="nom"/> </property> <property name="salaire" type="float"><column name="salaire"/> <many-to-one name="departement" class="persistence.Departement" > <column name="numero" not-null="true" /> </many-to-one> </class>
Many-to-one Pour chaque opération basique de session: persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() . On lui correspond un style de cascade nommé: create, merge, save-update, delete, lock, refresh, evict, replicate. <one-to-one name="person“ cascade="persist"/> combinaison de cascade <many-to-one name="person" cascade="persist,delete,lock"/> Appliqué touts les types de casacade <many-to-one name="person" cascade=“all"/>
One-to-one La méthode la plus simple est d’utiliser est de traiter une association one-to-one comme une association many-to-one on ajoutant la contrainte unique à true Enseignant Adresse 1 1
One-to-one <hibernate-mapping package="persistence"> <class name="Adresse" table="adresse"> <id name="numero" type="int"> <column name="numero"/> </id> <property name="rue" type="string"><column name="rue"/> </property> <property name="ville" type="string"><column name="ville"/> </class></hibernate-mapping> package persistence; public class Adresse { private int numero; private String Rue; private String Ville; //Getters et setters …}
One-to-One Classe Enseignant public class Enseignant{ ... private Adresse adresse; } Enseignant.hbm.xml <class name="Enseignant" table="enseignant"> <many-to-one name="adresse" class="persistence.Adresse" cascade="all" unique="true"> <column name="num" not-null="true" /> </many-to-one>
One-to-One On a mappé une association one-to-one unidirectionnelle d’Enseignant vers Adresse Si on voulez transformer cette association en bidirectionnelle on devra ajouter un attribut enseignant dans la classe adresse Enseignant Adresse 1 1
One-to-One Classe Adresse public class Adresse { private int numero; private String Rue; private String Ville; private Enseignant ens; //Getters et Setters Adresse.hbm.xml <one-to-one name="ens" class="Enseignant" property-ref="adresse" />
One-to-One Une deuxième méthode de mapper une association One-to-one est de faire partager les deux objets(un Enseignant et une Adresse) la même clé primaire. Ici si on va choisir que l’enseignant est l’objet fort(master) et sa clé est généré automatiquement donc un objet adresse doit partager la clé de l’enseignant comme clé étrangère. Chaque objet Adresse aura la même valeur de la clé de l’enseignant.
One-to-One Adresse.hbm.xml Enseignant.hbm.xml <id name="numero" column="ID"> <generator class="foreign"> <param name="property">ens</param> </generator> </id> …. <one-to-one name="ens" class="Enseignant" cascade="all" constrained="true"/> Enseignant.hbm.xml <one-to-one name="adresse" class="persistence.Adresse" cascade="save-update,delete" />
Correspondance des associations Implémentations des collections Set, List et Map propres à Hibernate Balise propre à chaque type d’interface : <set>, <list>, <map> etc. name : nom de la propriété contenant la collection table : nom de la relation contenant la collection - optionnel par défaut nom de la propriété - non utilisé pour les associations one-to-many lazy : pour désactiver l'initialisation tardive - par défaut = true inverse : Pour définir la collection comme extrémité inverse de l'association bidirectionnelle.
One-to-Many private Set<Enseignant> enseignants; <set name="enseignants" inverse="true" cascade="all" > <key column="id" /> <one-to-many class="Enseignant"/> </set>
Many-to-Many On va ajouter à notre projet la classe Module. Un enseignant peut enseigner plusieurs module et un module peut être enseigner par plusieurs enseignant Enseignant private Set<Module> ens_mod; <set name="ens_mod" table="ens_module" cascade="all" > <key column="id_ens" /> <many-to-many class="Module" column="id_mod" /> </set>
Many-to-Many Module public class Module { private int codeModule; private String nom_module; private float coef; private Set<Enseignant> ens_mod; …. } <set name="ens_mod" table="ens_module" cascade="all" > <key column="id_mod" /> <many-to-many class="Enseignant" column="id_ens"/> </set>
Many-to-Many Ne pas oublier de gérer les deux extrémités des associations En relationnel, contrairement au Java, réalisation d’une seule opération : mise à jour ou initialisation de la clé étrangère En Java : deux opérations – une à chaque extrémité de l’association Ne pas oublier de spécifier inverse= "true" à l’une des extrémité dans le fichier de correspondance pour éviter la création de deux ordres SQL Penser à regrouper les méthodes d’instanciation au sein d’une méthode métier « de cohérence »
Héritage Hibernate supporte les trois stratégies d'héritage de base : Une table par hiérarchie de classe (table per class hierarchy) Une table par classe fille (table per subclass) Une table par classe concrète (table per concrete class) Hibernate supporte en plus une quatrième stratégie, légèrement différente, qui supporte le polymorphisme : le polymorphisme implicite (Il ne sera pas étudié en cours)
Héritage Une table par hiérarchie de classe (table per class hierarchy) 1 table <class name="Article" table="ARTICLE_PERCLASSHIERARCHY"> <id name="idArticle" type="long" column=“ARTICLE_ID"> <generator class="native" /> </id> <discriminator column="ARTICLE_TYPE" type="string" /> <property name="prixArticle" column="prixArticle" /> <subclass name="Stylo" discriminator-value="STYLO"> <property name="couleur" column="COULEUR" /> </subclass> <subclass name="Ram" discriminator-value="RAM"> <property name="capacite" type="int" column="CAPACITE" /> </class>
Héritage Une table par hiérarchie de classe (table per class hierarchy) <class name="Article" table="ARTICLE_PERCLASSHIERARCHY"> <id name="idArticle" type="long" column="PAYMENT_ID"> <generator class="native" /> </id> <discriminator column="ARTICLE_TYPE" type="string" /> <property name="prixArticle" column="prixArticle" /> <subclass name="Stylo" discriminator-value="STYLO"> <property name="couleur" column="COULEUR" /> </subclass> <subclass name="Ram" discriminator-value="RAM"> <property name="capacite" type="int" column="CAPACITE" /> </class>
Héritage Une table par classe fille (table per subclass ) 3 tables <class name="Article" table="ARTICLE_PERCONCRETECLASS"> <id name="id" type="long" column=“ARTICLE_ID"> <generator class="native" /> </id> <property name="prixArticle" column="prixArticle" /> <joined-subclass name="Stylo" table="STYLO_PERCONCRETE"> <key column="ARTICLE_id" /> <property name="couleur" column="COULEUR" /> </joined-subclass> <joined-subclass name="Ram" table="RAM_PERCONCRETE"> <property name="capacite" type="int" column="CAPACITE" /> </class>
Héritage Une table par classe concrète (table per concrete class) 2 tables <class name="Ram" table="RAM_ARTICLE"> <id name="idArticle" type="long" column="ARTICLE_ID"> <generator class="native" /> </id> <property name="capacite" type="int" column="CAPACITE" /> <property name="prixArticle" column="prixArticle" /> </class> <class name="Stylo" table="STYLO_ARTICLE"> <property name="couleur" column="COULEUR" />
Héritage Inconvénient majeur de cette solution est qu’un stylo et un ramette peuvent avoir la même valeur de clé primaire. Pour résoudre ce problème il faudra utiliser ce code qui ne marche pas sous MYSQL <class name=“Article" > <id name="id" type="long" column="ID_Article"> <generator class="sequence"/> </id> <property name=“prixArticle" column=“prixArticle" /> <union-subclass name=“Stylo" table=“Stylo_Article"> <property name=“couleur" type="string" column=“couleur" /> </union-subclass> <union-subclass name=“Ram" table=“Ram_Article"> <property name=“capacite" column=“capacite" /> </class>
Héritage <class name="Article" abstract="true"> La solution avec le SGBD Mysql est: <class name="Article" abstract="true"> <id name="idArticle" type="long" column="ID_Article"> <generator class="increment" /> </id> <property name="prixArticle" column="prixArticle" /> <union-subclass name="Stylo" table="Stylo_Article"> <property name="couleur" type="string" column="couleur " /> </union-subclass> <union-subclass name="Ram" table="Ram_Article"> <property name="capacite" column="capacite" /> </class>
Requête HQL "Langage de requêtes orientées objet" ou encapsulation du SQL selon une logique orientée objet Requêtes HQL (et SQL natives) représentées avec une instance de org.hibernate.Query Obtention d’une Query en utilisant la Session courante : session.createQuery (string) Clauses : from, select, where Invocation de la méthode list() ⇒ retour du résultat sous forme de liste Query req = session.createQuery("from Module"); List modules=req.list();
Requête HQL Interface Query fournit deux méthodes pour récupérer un sous ensemble du résultat. Très utile pour la pagination. Query req = session.createQuery("from Module"); req.setFirstResult(20);// premier enregistrement(commence par 0) req.setMaxResults(10);// nombre d’enregistrement List modules=req.list(); Avec HQL on peut ajouter des paramètres à la requête Query req = session.createQuery("from Module where id=?"); Req.setInt(0,100);
Requête HQL Autre méthode Query req = session.createQuery("from Module where id=:id_m"); Req.setInt(id_m,100); List modules=req.list(); from : Clause suivie d’un nom de classe et non de la relation de BD : from Module Utilisation des alias Query req = session.createQuery("from Module as m where m.id=:id_m");
Requête HQL join : Pour exécuter des jointures (inner join) Select e from Enseignant e join e.department Departement where Department.nom = “Etude” Where: Equivalent à celle de SQL, on peut utiliser and, or et not from Enseignant e where e.nom like ‘%M%’ and e.salaires between 100 and 200; Utilisation de null from Enseignant e where e.department is not null
Requête SQL native Pour utiliser des requêtes optimisées et tirer partie des spécificités du SGBD utilisé Requêtes natives du noyau session.createSQLQuery("SELECT * FROM PERSON").list(); Retourne une liste d’Object[] avec des valeurs scalaires pour chaque colonne de la table PERSON (i.e. retourne une table comme pour les requêtes classiques JDBC) session.createSQLQuery("SELECT * FROM PERSON") .addEntity(Person.class); Retourne une liste d’objets de la classe Person