Niveau : | Elémentaire |
La quasi-totalité des applications de gestion traitent des données dans des volumes plus ou moins importants. Dès que ce volume devient assez important, les données sont stockées dans une base de données.
Il existe plusieurs types de base de données
La seconde catégorie est historiquement la plus répandue mais aussi une des moins compatibles avec la programmation orientée objet.
Ce chapitre contient plusieurs sections :
La correspondance des données entre le modèle relationnel et le modèle objet doit faire face à plusieurs problèmes :
La persistance des objets en Java présente de surcroît quelques inconvénients supplémentaires :
La première approche pour faire une correspondance entre ces deux modèles a été d'utiliser l'API JDBC fournie en standard avec le JDK. Cependant cette approche présente plusieurs inconvénients majeurs :
Tous ces facteurs réduisent la productivité mais aussi les possibilités d'évolutions et de maintenance. De plus, une grande partie de ce travail peut être automatisée.
Face à ce constat, différentes solutions sont apparues :
Le mapping Objet/Relationnel (mapping O/R) consiste à réaliser la correspondance entre le modèle de données relationnel et le modèle objets de la façon la plus facile possible.
Un outil de mapping O/R doit cependant proposer un certain nombre de fonctionnalités parmi lesquelles :
Les solutions de mapping sont donc riches en fonctionnalités ce qui peut rendre leur mise en oeuvre plus ou moins complexe. Cette complexité est cependant différente d'un développement de toute pièce avec JDBC.
Les solutions de mapping O/R permettent de réduire la quantité de code à produire mais impliquent une partie configuration (généralement sous la forme d'un ou plusieurs fichiers XML ou d'annotations pour les solutions reposant sur Java 5).
Depuis quelques années, les principales solutions mettent en oeuvre des POJO (Plain Old Java Object).
L'héritage est un des concepts clé de la programmation orientée objet mais il ne possède pas d'équivalent dans le modèle relationnel.
Le modèle objet supporte des relations de type « est un » ou « possède un ».
Le modèle relationnel supporte seulement les relations de types « possède un »
Il existe cependant plusieurs stratégies standard pour permettre de mapper une hiérarchie de classes dans le modèle relationnel. Chacune de ces stratégies présente des inconvénients.
Le choix de la stratégie doit tenir de deux points particuliers :
Le changement de la stratégie de mapping impose une migration plus ou moins lourde des données.
Dans la stratégie une table par hiérarchie de classes (Table Per Hierarchy : TPH), une seule table est utilisée pour mapper l'ensemble de la hiérarchie de classes.
Cette table contient toutes les propriétés de chaque classe de la hiérarchie. Les propriétés qui n'appartiennent pas à la classe précisée par le discriminant ont pour valeur null. Toutes les colonnes de la table correspondant à une de ces propriétés doivent être nullable. Si la hiérarchie comporte de nombreuses classes ou si le nombre de leurs propriétés est important alors la table est composée de toutes ces propriétés dont la plupart seront null.
Il est important que toutes les propriétés des classes qui sont mappées aient toutes un nom de colonne unique.
Cette table requiert une colonne technique supplémentaire qui sert de discriminant en identifiant la classe concernée pour les données de la ligne. La colonne qui sert de discriminant permet de préciser selon sa valeur à quelle classe correspondent les données de la ligne : elle permet à Hibernate de déterminer le type de la classe qu'il devra instancier lors de la lecture de la ligne dans la base de données.
Il n'est pas possible de définir le type d'une classe fille comme clé étrangère sur l'identifiant de la table puisque toutes les classes sont mappées dans une seule table.
Avantages |
Inconvénients |
Facile à implémenter Les performances de cette stratégie sont bonnes grâce à une seule instruction select permettant de retrouver les données (aucune jointure ni sous-select ne sont requis) Une seule table à gérer |
Chaque changement d'une propriété dans une des classes nécessite de modifier la colonne correspondante dans la table (ajout, suppression, modification). Elle n'est donc pas adaptée si le graphe d'objets est modifié fréquemment. La contrainte NOT NULL ne peut pas être utilisée sur toutes les colonnes qui correspondent à une propriété dans les classes filles La table n'est pas normalisée : elle ne répond pas à la troisième forme normale |
Si des requêtes sont faites sur les types des classes filles, il peut être intéressant de définir un index sur la colonne qui sert de discriminant.
La stratégie une table par sous-classe (Table Per Subclass : TPS) propose de convertir la relation "est un" du modèle objet en une relation « possède un » du modèle relationnel en utilisant des clés étrangères. Toutes les sous-classes et la mère si elle est abstraite sont mappées sur leurs propres tables.
Les tables des sous-classes possèdent une clé étrangère qui permet de faire référence à la clé primaire de la table de la classe mère mettant en oeuvre une relation de type 1-1. La clé primaire de la table de la classe mère est utilisée comme clé primaire et clé étrangère dans les tables des classes filles. Hibernate utilise alors des jointures ouvertes dans les requêtes SQL pour obtenir les données.
Cette stratégie de mapping est celle qui est la plus proche du modèle objet. Chaque classe abstraite ou concrète est mappée sur une table dans la base de données. Seule la colonne servant d'identifiant est partagée par toutes les tables : elle permet de faire les jointures requises entre les tables.
Avantages |
Inconvénients |
Le modèle relationnel est normalisé et l'intégrité des données peut être maintenue Les changements dans la base de données sont simples lorsqu'une modification a lieu dans une classe |
Les performances se dégradent lorsque la hiérarchie de classes grossit car le nombre de jointures entre tables augmente dans les requêtes |
Dans la stratégie une table par classe concrète (Table Per Concret Class), chaque classe concrète de la hiérarchie possède sa propre table. Les colonnes des classes mères sont dupliquées dans les tables des classes fille. Les classes abstraites ne possèdent pas de table.
Chaque table de la hiérarchie possède des identifiants dédiés : il n'y a pas d'identifiants dupliqués dans ces tables. Ceci permet de garantir l'unicité des identifiants lors de requêtes polymorphiques.
Il n'y a pas de relation entre les tables de la hiérarchie. Il n'est pas nécessaire d'avoir un champ de type discriminant puisque chaque classe possède sa propre table.
Avantages |
Inconvénients |
C'est la stratégie de mapping la plus simple à implémenter |
Les tables ne sont pas normalisées Les colonnes correspondant à des données de classes mères abstraites sont dupliquées dans les tables des classes filles. La modification d'une colonne (ajout / modification / suppression) de la table de la classe mère doit donc être reportée dans toutes les tables des classes filles Le support du polymorphisme est délicat car il nécessite plusieurs requêtes ou l'utilisation d'une clause UNION. Pour effectuer une requête sur le type de la classe mère, il sera nécessaire de faire des requêtes sur chacune des tables Les performances se dégradent si la hiérarchie comporte de nombreuses classes : les requêtes polymorphiques sont coûteuses car elles requièrent l'utilisation d'unions sur les tables de la hiérarchie Il n'est pas possible d'utiliser la colonne identifiant comme clé étrangère car elle est répartie sur plusieurs tables Il n'est pas possible de définir des contraintes d'unicité sur des colonnes d'une classe mère car elles sont dupliquées sur toutes les tables Il n'est pas possible d'utiliser une valeur auto-générée pour les colonnes identifiants car chaque ligne dans toutes les classes de la hiérarchie doivent avoir un identifiant unique |
Cette stratégie n'est pas fréquemment utilisée.
Comme pour tout choix d'une solution, des critères standard doivent entrer en ligne de compte (prix, complexité de prise en main, performance, maturité, pérennité, support, ...)
Dans le cas d'une solution de mapping O/R, il faut aussi prendre en compte des critères plus spécifiques à ce type de technologie.
La solution doit proposer des fonctionnalités de base :
La prise en compte des performances et des optimisations proposées par la solution est très importante :
Il est aussi nécessaire de tenir compte des outils proposés par la solution pour faciliter sa mise en oeuvre. Ces outils peuvent par exemple :
Certaines fonctionnalités avancées peuvent être utiles voire requises en fonction des besoins :
De nombreuses difficultés peuvent survenir lors de la mise en oeuvre d'un outil de mapping O/R
Dans une architecture en couches, il est important de prévoir une couche dédiée aux accès aux données.
Il est assez fréquent dans cette couche de parler de la notion de CRUD qui représente un ensemble des 4 opérations de bases réalisables sur des données.
Il est aussi de bon usage de mettre en oeuvre le design pattern DAO (Data Access Object) proposé par Sun.
La partie du code responsable de l'accès aux données dans une application multiniveau doit être encapsulée dans une couche dédiée aux interactions avec la base de données de l'architecture généralement appelée couche de persistance. Celle-ci permet notamment :
La couche métier qui va utiliser la couche de persistance reste indépendante du code dédié à l'accès à la base de données. Ainsi la couche métier ne contient aucune requête SQL, ni code de connexion ou d'accès à la base de données. La couche métier utilise les classes de la couche persistance qui encapsulent ces traitements. Ainsi la couche métier manipule uniquement des objets pour les accès à la base de données.
Le choix des API ou des outils dépend du contexte : certaines solutions ne sont utilisables qu'avec la plate-forme Enterprise Edition (exemple : les EJB) ou sont utilisables indifféremment avec les plates-formes Standard et Enterprise Edition.
L'utilisation d'une API standard permet de garantir la pérennité et de choisir l'implémentation à mettre en oeuvre.
Les solutions open source et commerciales ont les avantages et inconvénients inhérents à leur typologie respective.
L'acronyme CRUD (Create, Read, Update and Delete) désigne les quatre opérations réalisables sur des données (création, lecture, mise à jour et suppression).
Exemple : une interface qui propose des opérations de type CRUD pour un objet de type Entite |
public interface EntiteCrud {
public Entite obtenir(Integer id);
public void creer(Entite entite);
public void modifier(Entite entite);
public Collection obtenirTous();
public void supprimer(Entite entite);
}
DAO est l'acronyme de Data Access Object. C'est un modèle de conception qui propose de découpler l'accès à une source de données.
L'accès aux données dépend fortement de la source de données. Par exemple, l'utilisation d'une base de données est spécifique pour chaque fournisseur. Même si SQL et JDBC assurent une partie de l'indépendance vis-à-vis de la base de données utilisée, certaines contraintes imposent une mise en oeuvre spécifique de certaines fonctionnalités.
Par exemple, la gestion des champs de type identifiants est proposée selon diverses formes par les bases de données : champ auto-incrémenté, identity, séquence, ...
Le motif de conception DAO proposé dans le blue print de Sun propose de séparer les traitements d'accès physique à une source de données de leur utilisation dans les objets métiers. Cette séparation permet de modifier une source de données sans avoir à modifier les traitements qui l'utilisent.
Le DAO peut aussi proposer un mécanisme pour rendre l'accès aux bases de données indépendant de la base de données utilisée et même rendre celle-ci paramétrable.
Les classes métier utilisent le DAO par son interface et sont donc indépendantes de son implémentation. Si cette implémentation change (par exemple un changement de base de données), seul l'implémentation du DAO est modifiée mais les classes qui l'utilisent via son interface ne sont pas impactées.
Le DAO définit donc une interface qui va exposer les fonctionnalités utilisables. Ces fonctionnalités doivent être indépendantes de l'implémentation sous-jacente. Par exemple, aucune méthode ne doit avoir de requêtes SQL en paramètre. Pour les même raisons, le DAO doit proposer sa propre hiérarchie d'exceptions.
Une implémentation concrète de cette interface doit être proposée. Cette implémentation peut être plus ou moins complexe en fonction de critères de simplicité ou de flexibilité.
Fréquemment les DAO ne mettent pas en oeuvre certaines fonctionnalités comme la mise en oeuvre d'un cache ou la gestion des accès concurrents.
Différentes solutions peuvent être utilisées pour la persistance des objets en Java :
La plupart de ces solutions offre de surcroît un choix plus ou moins important d'implémentations.
Les différentes évolutions de Java ont apporté plusieurs solutions pour assurer la persistance des données vers une base de données.
JDBC est l'acronyme de Java DataBase Connectivity. C'est l'API standard pour permettre un accès à une base de données. Son but est de permettre de coder des accès à une base de données en laissant le code le plus indépendant de la base de données utilisée.
C'est une spécification qui définit des interfaces pour se connecter et interagir avec la base de données (exécution de requêtes ou de procédures stockées, parcours des résultats des requêtes de sélection, ...)
L'implémentation de ces spécifications est fournie par des tiers, et en particulier les fournisseurs de base de données, sous la forme de Driver.
La mise en oeuvre de JDBC est détaillée dans le chapitre JDBC
JDO est l'acronyme de Java Data Object : le but de cette API est de rendre transparente la persistance d'un objet. Elle repose sur l'enrichissement du bytecode à la compilation.
Cette API a été spécifiée sous la JSR-012
Il existe plusieurs implémentations dont :
La mise en oeuvre de JDO est détaillée dans le chapitre JDO
La version 2 de JDO a été diffusée en mars 2006.
Il existe plusieurs implémentations dont :
Les EJB (Enterprise Java Bean) proposent des beans de type Entités pour assurer la persistance des objets.
Les EJB de type Entité peuvent être de deux types :
Les EJB bénéficient des services proposés par le conteneur cependant cela les rend dépendants de ce conteneur pour l'exécution : ils sont difficilement utilisables en dehors du conteneur (par exemple pour les tester).
Il existe de nombreuses implémentations puisque chaque serveur d'applications certifié J2EE doit implémenter les EJB ce qui inclut entre autres JBoss de RedHat, JonAS, Geronimo d'Apache, GlassFish de Sun/Oracle, Websphere d'IBM, Weblogic de BEA, ...
Les Entity Beans CMP 2.x ont été déclarés pruned dans Java EE 6.
JPA (Java Persistence API) est issu des travaux de la JSR-220 concernant la version 3.0 des EJB : elle remplace d'ailleurs les EJB Entités version 2. C'est une synthèse standardisée des meilleurs outils du sujet (Hibernate, Toplink/EclipseLink, ...)
L'API repose sur
JPA peut être utilisé avec Java EE dans un serveur d'applications mais aussi avec Java SE (avec quelques fonctionnalités proposées par le conteneur en moins).
JPA est une spécification : il est nécessaire d'utiliser une implémentation pour la mettre en oeuvre. L'implémentation de référence est la partie open source d'Oracle Toplink : Toplink essential. La version 3.2 d'Hibernate implémente aussi JPA.
JPA ne peut être utilisé qu'avec des bases de données relationnelles.
La version 3.0 des EJB utilise JPA pour la persistance des données.
La mise en oeuvre de JPA est détaillée dans le chapitre JPA
Pour palier certaines faiblesses des API standards, la communauté open source a développé de nombreux frameworks concernant la persistance de données dont le plus utilisé est Hibernate. Cette section va rapidement présenter quelques-uns d'entre-eux.
Le site officiel du projet est à l'url : http://ibatis.apache.org/
Le site officiel du projet est à l'url : http://mybatis.org/
Hibernate est le framework open source de mapping O/R le plus populaire. Cette popularité est liée à la richesse des fonctionnalités proposées et à ses performances.
Hibernate propose son propre langage d'interrogation HQL et a largement inspiré les concepteurs de l'API JPA. Hibernate est un projet open source de mapping O/R qui fait référence en la matière car il possède plusieurs avantages :
Le site officiel du projet est à l'url : http://hibernate.org/
L'utilisation d'Hibernate est détaillée dans le chapitre Hibernate
Le site officiel du projet est à l'url : http://eclipse.org/eclipselink/
EclipseLink est un projet open source de la fondation Eclipse qui propose un framework extensible permettant de se connecter à une source de données : base de données (JPA), Moxy pour XML (JAXB), Java Connector Architecture (JCA), Service Data Object (SDO).
EclipseLink est l'implémentation de référence de JPA 2.0.
Castor permet de mapper des données relationnelles ou XML avec des objets Java.
Castor propose une solution riche mais qui n'implémente aucun standard.
Le site officiel du projet est à l'url : http://castor.codehaus.org/
Torque est un framework initialement développé pour le projet Jakarta Turbine : il est développé depuis sous la forme d'un projet autonome.
Torque se compose d'un générateur qui va générer automatiquement les classes requises pour accéder à la base de données et d'un environnement d'exécution qui va permettre la mise en oeuvre des classes générées.
Torque utilise un fichier XML contenant une description de la base de données pour générer des classes permettant des opérations sur la base de données grâce à des outils dédiés. Ces classes reposent sur un environnement d'exécution (runtime) fourni par Torque.
Le site officiel du projet est à l'url http://db.apache.org/torque/
TopLink a été racheté par Oracle et intégré dans ses solutions J2EE.
Le site officiel du produit est à l'url : http://www.oracle.com/technetwork/middleware/toplink/overview/index.phpl
La partie qui compose la base de TopLink est en open source.
Le site officiel du projet est à l'url http://db.apache.org/ojb/
Le site officiel du projet est à l'url http://cayenne.apache.org/
Cayenne est distribué avec un outil de modélisation nommé CayenneModeler.
L'utilisation de procédures stockées peut apporter des améliorations notamment en termes de performance de certains traitements.
Cependant, les procédures stockées possèdent plusieurs inconvénients :