7. Introduction à JPA et Hibernate#

Nous avons donc un premier aperçu de JDBC, et la démarche logique que nous avons suivie en cherchant à respecter les principes d’indépendance dictés par notre approche MVC nous a mené à « encapsuler » les lignes d’une table relationnelle sous forme d’objet avec d’obtenir une intégration naturelle à l’application. Cela revient à isoler les accès à la base dans la couche modèle, de manière à ce que ces accès deviennent transparents pour le reste de l’application. À ce stade les avantages devraient déjà vous être perceptibles:

  • possibilité de faire évoluer la couche d’accès aux données indépendamment du reste de l’application;

  • possibilité de tester séparément chaque couche, et même la couche modèle sans avoir à se connecter à la base (par création d’entités non persistantes).

Si ce n’est pas encore clair, j’espère que cela le deviendra. Au passage, on peut souligner qu’il est tout à fait possible de développer une application valide sans recourir à ces règles de conception qui peuvent sembler lourdes et peu justifiées. C’est possible mais cela devient de moins en moins facile au fur et à mesure que l’application grossit ainsi que les équipes en charge de la développer.

S1: Concepts et mise en route#

Supports complémentaires :

Il est temps de clarifier cette notion de représentations incompatibles entre la base de données et une application objet. Le constat de cette incompatibilité justifie le développement des outils ORM, et il est bien utile de la maîtriser pour comprendre le but de ces outils.

Relationnel et objet#

Admettons donc que nous avons décidé d’adopter cette approche systématique de séparation en couches, avec tout en bas une couche d’accès aux données basée sur une représentation objet.

Rôle d’un système ORM

Le rôle d’un système ORM est de convertir automatiquement, à la demande, la base de données sous forme d’un graphe d’objet. L’ORM s’appuie pour cela sur une configuration associant les classes du modèle fonctionnel et le schéma de la base de donnés. L’ORM génère des requêtes SQL qui permettent de matérialiser ce graphe ou une partie de ce graphe en fonction des besoins.

Pour bien comprendre cette notion de graphe et en quoi elle diffère de la représentation dans la base relationnelle, voici une petit échantillon de cette dernière.

Table des films#

id

titre

année

idRéalisateur

17

Pulp Fiction

1994

37

54

Le Parrain

1972

64

57

Jackie Brown

1997

37

Table des artistes#

id

nom

prénom

année_naissance

1

Coppola

Sofia

1971

37

Tarantino

Quentin

1963

64

Coppola

Francis

1939

Ce petit exemple illustre bien comment on représente une association en relationnel. C’est un mécanisme de référence par valeur (et pas par adresse), où la clé primaire d’une entité (ligne) dans une table est utilisée comme attribut (dit clé étrangère) d’une autre entité (ligne).

Ici, le réalisateur d’un film est un artiste dans la table Artiste, identifié par une clé nommée id. Pour faire référence à cet artiste dans la table Film, on ajoute un attribut clé étrangère idRealisateur dont la valeur est l’identifiant de l’artiste. Notez dans l’exemple ci-dessus que cet identifiant est 37 pour Pulp Fiction et Jackie Brown, 64 pour Le parrain, avec la correspondance à une et une seule ligne dans la table Artiste.

Voyons maintenant la représentation équivalente dans un langage objet en général (et donc java en particulier). Nous avons des objets, et la capacité à référencer un objet depuis un autre objet (cette fois par un système d’adressage). Par exemple, en supposant que nous avons une classe Artiste et une classe Film en java, le fait qu’un film ait un réalisateur se représente par une référence à un objet Artiste sous forme d’une propriété de la classe Film.

class Film {

  (...)

  Artiste realisateur;

  (...)
}

Et du côté de la classe Artiste, nous pouvons représenter les films réalisés par un artiste par un ensemble.

class Artiste {

  (...)

  Set<Film> filmsDiriges;

  (...)
}

Important

On peut noter dès maintenant qu’une différence importante entre les associations en relationnel et en java est que les premières sont bi-directionnelles. Il est toujours possible par une requête SQL (une jointure) de trouver les films réalisés par un artiste, ou le réalisateur d’un film. En java, le lien peut être représenté de chaque côté de l’association, ou des deux. Par exemple, on pourrait mettre la propriété réalisateur dans la classe Film, mais pas filmsDiriges dans la classe Artiste, ou l’inverse, ou les deux. Cette subtilité est la source de quelques options plus ou moins obscures dans les systèmes ORM, nous y reviendrons.

La base de données, transformée en java, se présentera donc sous la forme d’un graphe d’objet comme celui montré par la figure Le graphe d’objets Java.

_images/graphe-base.png

Figure 1: Le graphe d’objets Java#

Lisez et relisez cette section jusqu’à ce qu’elle soit limpide (et sollicitez vos enseignants si elle ne le devient pas). Les notions présentées ci-dessous sont à la base de tout ce que nous allons voir dans les prochains chapitres.

La couche ORM#

Pour bien faire, il nous faut une approche systématique. On peut par exemple décréter que:

  • on crée une classe pour chaque entité;

  • on équipe cette classe avec des méthodes create(), get(), delete(), search(), …, que l’on implante en SQL/JDBC;

  • on implante la navigation d’une entité à une autre, à l’aide de requête SQL codées dans les classes et exécutées en JDBC.

Il ne serait pas trop difficile (mais pas trop agréable non plus) de faire quelques essais selon cette approche. Par exemple la méthode get(id) est clairement implantée par un SELECT * From <table> WHERE id=:id. Le create() est implanté par un INSERT INTO (), etc. Nous ne le ferons pas pour au moins trois raisons:

  • concevoir nous-mêmes un tel mécanisme est un moyen très sûr de commettre des erreurs de conception qui se payent très cher une fois que des milliers de lignes de code ont été produites et qu’il faut tout revoir;

  • la tâche répétitive de produire des requêtes toujours semblables nous inciterait rapidement à chercher un moyen automatisé;

  • et enfin - et surtout - ce mécanisme dit d’association objet-relationnel (ORM pour Objet-relational mapping) a été mis au point, implanté, et validé depuis de longues années, et nous disposons maintenant d’outils matures pour ne pas avoir à tout inventer/développer nous-mêmes.

Non seulement de tels outils existent, mais de plus ils sont normalisés dans la plate-forme java, sous le nom générique de Java Persistence API ou JPA. JPA est essentiellement une spécification intégrée au JEE qui vise à standardiser la couche d’association entre une base relationnelle et une application Java construite sur des objets (à partir de maintenant nous appelerons cette couche ORM). Le rôle de la couche ORM est illustré par la figure Le mapping d’une base relationnelle en graphe d’objets java.

_images/mapping.png

Figure 2: Le mapping d’une base relationnelle en graphe d’objets java#

Il est important (ou du moins utile) de signaler que cette standardisation vient après l’émergence de plusieurs frameworks qui ont exploré les meilleures pratiques pour la réalisation d’un système ORM. Le plus connu est sans doute Hibernate, que nous allons utiliser dans ce qui suit (voir http://fr.wikipedia.org/wiki/Mapping_objet-relationnel pour une liste). Avec l’apparition de JPA, ces frameworks vont tendre à devenir des implantations particulières de la norme (rappelons que JPA est une spécification visant à la standardisation). Comme JPA est une sorte de facteur commun, il s’avère que chaque framework propose des extensions spécifiques, souvent très utiles. Il existe une implantation de référence de JPA, EclipseLink.

JPA définit une interface dans le package javax.persistence.*. La définition des associations avec la base de données s’appuie essentiellement sur les annotations Java, ce qui évite de produire de longs fichiers de configuration XML qui doivent être maintenus en parallèle aux fichiers de code. On obtient une manière assez légère de définir une sorte de base de données objet virtuelle qui est matérialisée au cours de l’exécution d’une application par des requêtes SQL produites par le framework sous-jacent.

Le choix que j’ai fait dans ce qui suit est d’utiliser Hibernate comme framework de persistance, et de respecter JPA autant que possible pour que vous puissiez passer à un autre framework sans problème (en particulier, les annotations sont utilisées systématiquement).

Dans ce chapitre nous allons effectuer nos premier pas avec JPA/Hibernate, avec pour objectif de construire une première couche ORM pour notre base webscope. L’approche utilisée est assez simple, elle nous servira de base de départ pour découvrir progressivement des concepts plus avancés.

Dernière chose: les ORM s’appuient sur JDBC et nous allons donc retrouver certains aspects déjà connus (et en découvrir d’autres).

Exercice: comprendre la représentation par graphe

Prenez les deux films suivants: Impitoyable et Seven, avec leurs acteurs et metteurs en scène, et construisez le graphe d’objets.

S2: Premiers pas avec Hibernate#

Supports complémentaires :

Installation#

Vous allez installer Hibernate dans votre environnement, en commençant par récupérer la dernière version stable sur le site Hibernate. À l’heure où j’écris, cette version est la 5.4, les exemples connées dans ce qui suit ne devraient pas dépendre, sauf sur des points mineurs, de la version.

_images/hibernate_releasepage.png

Figure 3: La page de téléchargement d’Hibernate (cliquez sur le bouton dans la zone violette)#

Dans le répertoire hibernate obtenu après décompression, vous trouverez une documentation complète et les librairies, dans lib. Elles sont groupées en plusieurs catégories correspondant à autant de sous-répertoires (notez par exemple celui nommé jpa). Pour l’instant nous avons besoin de toutes les librairies dans required: copiez-les, comme d’habitude, dans le répertoire WEB-INF/lib de votre application Web. Le pilote MySQL doit bien entendu rester en place: il est utilisé par Hibernate. Utilisez l’option refresh sous Eclipse sur votre projet (clic bouton droit) pour que ces nouvelles librairies soient identifiées.

Un des objets essentiels dans une application Hibernate est l’objet Session qui est utilisé pour communiquer avec la base de données. Il peut être vu comme une généralisation/extension de l’objet Connection de JDBC. Une session est créée à partir d’un ensemble de paramètres (dont les identifiants de connexion JDBC) contenus dans un fichier de configuration XML nommé hibernate.cfg.xml.

Important

Pour que ce fichier (et toute modification affectant ce fichier) soit automatiquement déployé par Tomcat, il doit être placé dans un répertoire WEB-INF/classes. Créez ce répertoire s’il n’existe pas.

Voici le fichier de configuration minimal avec lequel nous débutons.

<?xml version='1.0' encoding='utf-8'?>

<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
      <session-factory>
            <!-- local connection properties -->
            <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/webscope</property>
            <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
            <property name="hibernate.connection.username">orm</property>
            <property name="hibernate.connection.password">orm</property>
            <property name="hibernate.connection.pool_size">10</property>

            <!-- dialect for MySQL -->
            <property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>

            <property name="hibernate.show_sql">true</property>
            <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
            <property name="cache.use_query_cache">false</property>

      </session-factory>
</hibernate-configuration>

On retrouve les paramètres qui vont permettre à Hibernate d’instancier une connexion à MySQL avec JDBC (mettez vos propres valeurs bien sûr). Par ailleurs chaque système relationnel propose quelques variantes du langage SQL qui vont être prises en compte par Hibernate: on indique le dialecte à utiliser. Finalement, les derniers paramètres ne sont pas indispendables mais vont faciliter notre découverte, notamment avec l’option show_sql qui affiche les requêtes SQL générées dans la console java.

Il peut y avoir plusieurs <session-factory> dans un même fichier de configuration, pour plusieurs bases de données, éventuellement dans plusieurs serveurs différents.

Test de la connexion#

Comme pour JDBC, nous créons

  • un contrôleur Hibernate.java associé à l’URL /hibernate. Il devrait avoir grosso modo la même structure que celui utilisé pour JDBC puisque nous allons reproduire les mêmes actions;

  • une liste d’actions implantés dans une classe de tests nommée TestsHibernate.java;

  • des vues placées dans WebContent/vues/hibernate.

Nous commençons par tester que notre paramétrage est correct. Voici le squelette de TestsHibernate.java.

package modeles;

import org.hibernate.Session;

public class TestsHibernate {

   /**
    * Objet Session de Hibernate
    */
   private Session session;

   /**
    * Constructeur établissant une connexion avec Hibernate
    */
   public TestsHibernate()  {
     Configuration configuration = new Configuration().configure("/hibernate.cfg.xml");
     ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
     SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);
     session = sessionFactory.openSession();
   }

}

Pour créer une connexion avec Hibernate nous passons par une SessionFactory qui repose sur le paramétrage de notre fichier de configuration (ce fichier lui-même est chargé dans un objet Configuration). Si tout se passe bien, on obtient un objet par lequel on peut ouvrir/fermer des sessions. Plusieurs raisons peuvent faire que ça ne marche pas (au moins du premier coup).

  • le fichier de configuration n’est pas trouvé (il doit être dans le CLASSPATH de l’application);

  • ce fichier contient des erreurs;

  • les paramètres de connexion sont faux;

  • et toutes sortes d’erreurs étranges qui peuvent nécessiter le redémarrage de Tomcat..

En cas de problème une exception sera levée avec un message plus ou moins explicite. Testons directement cette connexion. Voici l’action de connexion:

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  TestsHibernate tstHiber = new TestsHibernate();
  String maVue = VUES + "connexion.jsp";
  RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(maVue);
  dispatcher.forward(request, response);
}

Si cela fonctionne du premier coup, bravo, sinon cherchez l’erreur (avec notre aide bienveillante si nécessaire). On y arrive toujours.

Ma première entité#

Dans JPA, on associe une table à une classe Java annotée par @Entity. Chaque ligne de la table devient une entité, instance de cette classe. La classe doit obéir à certaines contraintes qui sont à peu près comparables à celle d’un JavaBean. Les propriétés sont privées, et des accesseurs set et get sont définis pour chaque propriété. L’association d’une propriété à une colonne de la table est indiquée par des annotations.

L’exemple qui suit est plus parlant que de longs discours:

package modeles.webscope;

import javax.persistence.*;

@Entity
public class Pays  {
      @Id
      String code;
      public void setCode(String c) {code = c;}
      public String getCode() {return code ;}

      @Column
      String nom;
      public void setNom(String n) {nom = n;}
      public String getNom() {return nom;}

      @Column
      String langue;
      public void setLangue(String l) {langue = l;}
      public String getLangue() {return langue;}
}

Remarquez l’import du package javax.persistence, et les annotations suivantes:

  • @Entity: indique que les instances de la classe sont persistantes (stockées dans la base);

  • @Id: indique que cette propriété est la clé primaire;

  • @Column: indique que la propriété est associée à une colonne de la table.

Et c’est tout. Bien entendu les annotations peuvent être beaucoup plus complexes. Ici, on s’appuie sur des choix par défaut: le nom de la table est le même que le nom de la classe; idem pour les propriétés, et l’identifiant n’est pas auto-généré.

Cela nous suffit pour un premier essai. Il reste à déclarer dans le fichier de configuration que cette entité persistante est dans notre base:

<session-factory>
     (...)
             <mapping class="modeles.webscope.Pays"/>
    </session-factory>

Hibernate ne connaitra le mapping entre une classe et une table que si elle est déclarée dans la configuration.

Une autre solution pour déclarer une classe persistante est de la charger explicitement dans la configuration.

  public TestsHibernate() {
    Configuration configuration = new Configuration().configure();

    // ICI ON AJOUTE LES CLASSES JPA
    configuration.addAnnotatedClass(Pays.class);
    // FIN DE L'AJOUT DES CLASSES JPA

    ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
            .applySettings(configuration.getProperties()).build();

    SessionFactory sessionFactory = configuration
            .buildSessionFactory(serviceRegistry);

    session = sessionFactory.openSession();
}

À vous de choisir. Notez que l’absence de déclaration d’un mapping entraîne une exception Unknown Entity. La déclaration de l’entité avec l’une des deux méthodes ci-dessus doit régler le problème.

Important

Depuis la version 5, il semble qu’Hibernate ne prenne plus en compte les instructions du document de configuration XML. Il est donc impératif d’ajouter les classes persistantes au moment du chargement de la configuration, comme indiqué ci-dessus.

Insertion d’une entité#

Maintenant définissez une action insertion dans votre contrôleur, avec le code suivant.

TestsHibernate tstHiber = new TestsHibernate();
Pays monPays = new Pays();
monPays.setCode("is");
monPays.setNom("Islande");
monPays.setLangue("islandais");
tstHiber.insertPays(monPays);
maVue = VUES + "insertion.jsp";

Vous voyez que l’on instancie et utilise un objet Pays comme n’importe quel bean. La capacité de l’objet est devenir persistant n’apparaît pas du tout. On peut l’utiliser comme un objet « métier », transient (donc non sauvegardé dans la base). Pour le sauvergarder on appelle la méthode insertPays de notre classe utilitaire, que voici.

session.beginTransaction();
session.save(pays);
session.getTransaction().commit();

Il suffit donc de demander à la session de sauvegarder l’objet (dans le cadre d’une transaction) et le tour est joué. Hibernate se charge de tout: génération de l’ordre SQL correct, exécution de cet ordre, validation. En appliquant ce code vous devriez obtenir dans la console java l’affichage de la requête SQL. Vous pouvez vérifier avec phpMyAdmin que l’insertion s’est bien faite.

Exercice: associer un formulaire de saisie, et une vue

Il s’agit de compléter notre première fonction d’insertion en créant une formlaire pour saisir les paramètres d’un pays. La validation de ce formulaire doit déclencher l’insertion dans la base et afficher une vue donnant les valeurs affichées.

Lecture de données#

Voyons maintenant comment lire des données de la base. Nous avons plusieurs possibilités.

  • Transmettre une requête SQL via Hibernate. C’est la moins portable des solutions car on ne peut pas être sûr à 100% que la syntaxe est compatible d’un SGBD à un autre; il est vrai qu’on ne change pas de SGBD tous les jours.

  • Transmettre une requête HQL. Hibernate propose un langage d’interrogation proche de SQL qui est transcrit ensuite dans le dialecte du SGBD utilisé.

  • Utiliser l’API Criteria. Plus de langage externe (SQL) passé plus ou moins comme une chaîne de caractères: on construit une requête comme un objet. Cette interface a également l’avantage d’être normalisée en JPA.

Note

JPA définit un langage de requête, JPQL (Java Persistence Query Language) qui est un sous-ensemble de HQL. Toute requête JPQL est une requête HQL, l’inverse n’est pas vrai.

Passons à la pratique. Voici la requête qui parcours la table des pays, avec l’API Criteria.

public List<Pays> lecturePays() {
   // Create CriteriaBuilder
   CriteriaBuilder builder = session.getCriteriaBuilder();

   // Create CriteriaQuery
   CriteriaQuery<Pays> criteria = builder.createQuery(Pays.class);
   criteria.from(Pays.class);
   // Execute query
   List<Pays> pays = session.createQuery(criteria).getResultList();

   return pays;
}

La requête était assez simple avec des versions antérieures de Criteria, mais elle reste assez lisible : on crée un constructeur de requêtes, on lui donne une requête avec une classe Java (ici Pays), et l’on récupère la liste des résultats dans une liste après exécution.

Bien sûr pour des requêtes plus complexes, la construction est plus longue. Voici la méthode équivalente en HQL, le langage associé à Hibernate, que nous explorerons en détail dans le chapitre Le langage HQL.

public List<Pays> lecturePaysHQL() {
        Query query = session.createQuery("from Pays");
        return query.list();
}

C’est presque la même chose, mais vous voyez ici que l’on introduit une chaîne de caractères contenant une expression syntaxique qui n’est pas du java (et qui n’est donc pas contrôlée à la compilation).

Exercice: afficher la liste des pays

À vous de jouer: créez l’action, la vue et le modèle pour afficher la liste des pays en testant les deux versions ci-dessus.

Gérer les associations#

Et nous concluons cette introduction avec un des aspects les plus importants du mapping entre la base de données et les objets java: la représentation des associations. Pour l’instant nous nous contentons de l’association (plusieurs à un) entre les films et les pays ( »plusieurs films peuvent avoir été tournés dans un seul pays »). En relationnel, nous avons donc dans la table Film un attribut code_pays qui sert de clé étrangère. Voici comment on représente cette association avec Hibernate.

  package modeles.webscope;

  import javax.persistence.*;

 @Entity
  public class Film {

        @Id
        private Integer id;
        public void setId(Integer i) {id = i;}

        @Column
        String titre;
        public void setTitre(String t) {titre= t;}
        public String getTitre() {return titre;}

        @Column
        Integer annee;
        public void setAnnee(Integer a) {annee = a;}
        public Integer getAnnee() {return annee;}

        @ManyToOne
        @JoinColumn (name="code_pays")
        Pays pays;
        public void setPays(Pays p) {pays = p;}
        public Pays getPays() {return pays;}
}

Cette classe est incomplète: il manque le genre, le réalisateur, etc. Ce qui nous intéresse c’est le lien avec Pays qui est représenté ici:

@ManyToOne
@JoinColumn (name="code_pays")
Pays pays;
public void setPays(Pays p) {pays = p;}
public Pays getPays() {return pays;}

On découvre une nouvelle annotation, @ManyToOne, qui indique à Hibernate que la propriété pays, instance de la classe Pays, encode un des côtés d’une association plusieurs-à-un entre les films et les pays. Pour instancier le pays dans lequel un film a été tourné, Hibernate devra donc exécuter la requête SQL qui, connaissant un film, permet de trouver le pays associé. Cette requête est:

select * from Pays where code = :film.code-pays

La clé étrangère qui permet de trouver le pays est code_pays dans la table Film. C’est ce qu’indique la seconde annotation, @JoinColumn.

Note

N’oubliez pas de modifier votre fichier de configuration pour indiquer que vous avez défini une nouvelle classe « mappée ».

Prenez le temps de bien réfléchir pour vous convaincre que toutes les informations nécessaires à la constitution du lien objet entre une instance de la classe Film et une instance de la classe Pays sont là. Pour le dire autrement: toutes les informations permettant à Hibernate d’engendrer la requête qui précède ont bien été spécifiées par les annotations.

Exercice: afficher la liste des films et leur pays

À vous de jouer: créez une action listeFilms qui affiche la liste des films et le pays où ils ont été tournés. Dans la JSTL, l’affichage du pays ne devrait pas être plus compliqué que:

${film.pays.nom}

Au passage regardez dans la console les requêtes transmises par Hibernate: instructif.

Important

Vous noterez que nous avons défini l’association du côté Film mais pas du côté Pays. Etant donné un objet pays, nous de pouvons pas accéder à la liste des films qui y ont été tournés. L’association est uni-directionnelle. Gardez cela en mémoire mais ne vous grillez pas les neurones dessus: nous allons y revenir.

Vous devriez maintenant pouvoir implanter avec Hibernate l’action qui affiche la liste des films avec leur metteur en scène.

Exercice: afficher la liste des films et leur metteur en scène

Aide: il faut définir la classe Artiste, mappée, et la lier à Film pour représenter l’association. Je vous aide: dans une approche « graphe d’objet », le metteur en scène est un objet propriété de Film.

À l’execution, examinez les requêtes SQL produites par Hibernate et méditez sur ce que cela implique. Prenez également le recul pour apprécier ce qu’apporte Hibernate par rapport à la gestion manuelle que nous avons envisagée dans le chapitre précédent.

Résumé: savoir et retenir#

Les notions suivantes devraient être claires maintenant:

  • une application objet représente les données comme un graphe d’objet, liés par des références;

  • une couche ORM transforme une base relationnelle en graphe d’objets et permet à l’application de naviguer dans ce graphe en suivant les références entre objets;

  • la transformation est effectuée à la volée en fonction des navigations (accès) effectués par l’application; elle repose sur la génération de requêtes SQL;

Hibernate est une implantation de la spécification JPA, et un peu plus que cela.