EJB3.1 - ORM - Object-Relational Mapping

Chapitres traités   

Durant cette étude, nous passerons en revue les bases des ORM (Object-Relational Mapping), qui consistent essentiellement à faire correspondre des entités à des tables et des attributs à des colonnes. Nous nous intéresserons ensuite à des associations plus complexes comme les relations, la composition et l'héritage.

Un modèle objet est composé d'objets interagissant ensemble ; or les objets et les bases de données utilisent des moyens différents pour stocker les informations sur ces relations (via des pointeurs ou des clés étrangères). Les bases de données relationnelles ne disposent pas naturellement du concept d'héritage et cette association entre objets et bases n'est par conséquent pas évidente.

Nous irons donc dans les détails et présenterons des exemples qui montrerons comment les attributs, les relations et l'héritage peuvent être traduits d'un modèle objet vers une base de données.

Choix du chapitre Association d'une entité

Comme premier exemple, commençons par l'association la plus simple possible que nous avons élaboré lors de l'étude précédente. Dans le modèle de persistance de JPA, une entité est un objet Java classique (POJO) :

Ceci signifie qu'une entité est déclarée, instanciée et utilisée comme n'importe qu'elle autre classe Java. Une entité possède des attributs (son état) qui peuvent être manipulés au moyen de getters et de setters (notion de propriétés). Dans la base de données relationnelle, chaque attribut est stockée dans une colonne d'une table

Le code source suivant présente l'entité simple Photo.
entité.Photo.java
package entité;

import java.awt.image.BufferedImage;
import java.util.Date;
import javax.persistence.*;

@Entity
@NamedQuery(name="toutes", query="SELECT photo FROM Photo AS photo")
public class Photo { //  implements java.io.Serializable {
    @Id
    private String id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date instant;
    private int largeur;
    private int hauteur;
    private long poids;

    public String getId()            { return id; }
    public int      getHauteur()   { return hauteur;  }
    public Date   getInstant()     { return instant;  }
    public int      getLargeur()   { return largeur;  }
    public long   getPoids()       { return poids;  }

    public Photo() { }

    public Photo(String nom, long poids) {
        id = nom;
        instant = new Date();
        this.poids = poids;
    }

    public void setDimensions(BufferedImage image) {
        largeur = image.getWidth();
        hauteur = image.getHeight();
    }

    @Override
    public String toString() {
        return id+" ("+largeur+", "+hauteur+")";
    }
}

Comme vous pouvez le constater, à part les annotations, cette entité ressemble exactement à n'importe quelle classe Java : elle possède plusieurs attributs (id, instant, largeur, hauteur et poids) de différents types (String, Date, int et long), plusieurs constructeurs dont celui par défaut, des getters pour chaque attribut avec un seul setter. Dans cette exemple là, l'état de l'objet est rapidement renseigné à l'aide du deuxième constructeur et de la méthode setDimensions().

Les annotations permettent d'associer très simplement cette entité à une table dans une base de données.

  1. Tout d'abord, la classe est annotée @javax.persistence.Entity, ce qui permet au fournisseur de persistance de la reconnaître comme une classe persistante et non comme une simple classe POJO.
  2. Puis l'annotation @javax.persistence.Id définit l'identifiant unique de l'objet. JPA étant destiné à associer des objets à des tables relationnelles, les objets doivent posséder un identifiant qui sera associé à une clé primaire.
  3. La plupart des autres attributs (largeur, hauteur et poids) ne sont pas annotés et seront donc stockés dans la table en appliquant une association standard.

Notez que cette entité Photo est une classe qui actuellement n'implémente aucune interface et qui n'hérite d'aucune classe. En fait, pour être une entité, une classe doit impérativement respecter les règles suivantes :

  1. La classe de l'entité doit être annotée par @javax.persistence.Entity.
  2. L'annotation @javax.persistence.Id doit être utilisée pour désigner une clé primaire simple.
  3. La classe de l'entité doit posséder au moins un constructeur par défaut, public ou protégé. Bien entendu, elle peut également avoir d'autres constructeurs.
  4. La classe de l'entité doit être une classe de premier niveau. Une énumération ou une interface ne peut pas être considérée comme une entité.
  5. La classe de l'entité ne peut pas être finale et aucune méthode ou variable d'instance persistante ne peut être finale non plus.
  6. Si une instance d'entité doit être passée par valeur sous forme d'objet détaché (via une interface distante, par exemple), la classe de l'entité doit alors implémenter l'interface Serializable (c'est très souvent le cas).

L'entité Photo respectant ces règles simples, le fournisseur de persistance peut synchroniser les données entre les attributs de l'entité et les colonnes de la table PHOTO. Par conséquent, si l'attribut largeur est modifié par l'application, la colonne LARGEUR le sera également (si l'entité est gérée, si le contexte de transaction est actif, etc.).

L'entité Photo est stockée dans une table PHOTO dont chaque colonne porte le nom de l'attribut correspondant de la classe (l'attribut largeur de type int est associée à une colonne LARGEUR de type INTEGER). Ces règles d'associations par défaut sont un aspect important du principe appelé "convention plutôt que configuration" (ou "configuration par exception").

Configuration par exception

Java EE 5 a introduit l'idée de configuration par exception. Ceci signifie que, sauf mention contraire, le conteneur ou le fournisseur doivent appliquer les règles par défaut. En d'autres termes, fournir une configuration est une exception à la règle. Cette politique permet donc de configurer une application avec un minimum d'effort.

Revenons à l'exemple précédent. Sans annotation, l'entité Photo serait traitée comme n'importe quel POJO et ne serait pas persistante - c'est la règle : sans configuration spéciale, le comportement par défaut s'applique et il consiste évidemment à considérer que la classe Photo est une classe comme les autres.
  1. Comme nous souhaitons modifier ce comportement, nous annotons la classe avec @Entity.
  2. Il en va de même pour l'identifiant : nous avons absolument besoin d'indiquer au fournisseur de persistance que cet attribut doit être associé à une clé primaire, et c'est la raison pour laquelle nous l'annotons avec @Id.

Ce type de décision caractérise bien la politique de configuration par exception : les annotations ne sont pas nécessaires dans le cas général ; elles ne sont utilisées que pour outrepasser une convention. Ceci signifie donc que tous les autres attributs de notre classe (sauf instant) seront associés selon les règles par défaut :

  1. Le nom de l'entité est associé à un nom de table relationnelle (l'entité Photo sera donc associée à une table PHOTO). Si vous désirez l'associer à une autre table, vous devrez utiliser l'annotation @Table, comme nous le découvrirons dans un des chapitres qui suit).
  2. Le nom des attributs sont associés à des noms de colonnes (l'attribut id, ou la méthode getId(), est associé à une colonne ID). Si vous changez ce comportement, vous devrez utiliser l'annotation @Column.
  3. Ce sont les règles JDBC qui s'appliquent pour associer les types primitifs de Java aux types de données de la base. Ainsi :
    • Un String sera associé à un VARCHAR,
    • Un Long à un BIGINT,
    • Un Boolean à un SMALLINT, etc.
    • La taille par défaut d'une colonne associée à un String est de 255 caractères - VARCHAR(255).

Ces règles par défaut peuvent éventuellement varier en fonction du SGBDR : un String est associé avec un VARCHAR avec Derby, mais à un VARCHAR2 avec Oracle ; de la même façon, un Integer est associé à un INTEGER avec Derby, mais à un NUMBER avec Oracle.

La plupart des fournisseurs de persistance, dont EclipseLink, permettent de produire automatiquement la base de données directement à partir des entités. Cette fonctionnalité est tout spécialement pratique lorsque nous sommes en phase de développement car, avec uniquement les règles par défaut, nous pouvons associer très simplement les données en se contentant des annotations @Entity et @Id.

Indépendamment de notre application d'entreprise, il peut arriver que nous souhaitions nous connecter au SGBDR de façon classique, à l'aide notamment de requête SQL. Dans ce cas de figure, pour que les développeurs de base de données classiques puissent s'y retrouver, JPA définit un nombre important d'annotations qui vont permettrent de personnaliser chaque partie de l'association (les noms des tables et des colonnes, les clés primaires, la taille des colonnes, les colonnes NULL ou NOT NULL, etc. ).

 

Choix du chapitreAssociations élémentaires

D'importantes différences existent entre la gestion des données par Java et par un SGBDR. En Java, nous utilisons des classes pour décrire à la fois les attributs qui contiennent les données et les méthodes qui accèdent et manipulent ces données. Lorsqu'une classe est définie, nous pouvons créer autant d'instances que nécessaire. Dans un SGBDR, en revanche, seules les données sont stockées, pas les comportements (exception faite des triggers et des procédures stockées). La procédure de stockage est également totalement différente de la structure des objets puisqu'elle utilise une décomposition en ligne et colonnes.

L'association d'objets Java à une base de données sous-jacente peut donc être simple et se contenter des règles par défaut ; parfois, ces règles peuvent ne pas convenir aux besoin, auquel cas nous sommes obligés de les outrepasser.

Les annotations des associations élémentaires permettent ainsi de remplacer les règles par défaut pour la table, les clés primaires et les colonnes, et de modifier certaines conventions de nommage ou de contenu de colonnes (valeurs non nulle, longueur, etc.).

Tables associées

La convention établit que les noms de l'entité et de la table sont identiques (une entité Photo est associée à une table PHOTO, une entité Livre, à une table LIVRE, etc.). Toutefois, si vous le souhaitez, vous pouvez associer vos attributs à une table différente, voire associer une même entité à plusieurs tables.

@Table

L'annotation @javax.persistence.Table permet de modifier les règles par défaut pour les tables. Vous pouvez, par exemple, indiquer le nom de la table dans laquelle vous voulez stocker vos attributs, le catalogue et le schéma de la base.

Cette annotation possède différents attributs :

  1. name (requis) : définit le nom de la table à utiliser pour le mapping.
  2. catalog (optionnel) : définit le catalog utilisé.
  3. schema (optionnel) : définit le schéma utilisé.
  4. uniqueConstraints (optionnel) : définit les contraintes qui seront placées sur la table. Cet attribut est utilisé lorsque le conteneur génère les tables au déploiement et n'affecte en rien l'exécution même de l'entité.
Le code source suivant montre comment associer la table T_PHOTO à l'entité Photo.
entité.Photo.java
@Entity
@Table(name="t_photo")
public class Photo implements java.io.Serializable {
    @Id
    private String id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date instant;
    private int largeur;
    private int hauteur;
    private long poids;
...
}

Dans l'annotation @Table, le nom de la table est en minuscule (t_photo). Par défaut, la plupart des SGBDR lui feront correspondre un nom en majuscules (c'est notamment le cas de Derby), sauf si vous les configurez pour qu'ils respectent la casse.

@SecondaryTable

Jusqu'à maintenant, nous avons toujours supposé qu'une entité n'était associée qu'à une seule table, également appelée table primaire. Si nous possédons déjà un modèle de données, en revanche, nous voudrons peut-être disséminer les attributs sur plusieurs tables, ou tables secondaires. Cette annotation permet de mettre en place cette configuration.

@SecondaryTable permet d'associer une table secondaire à une entité, alors que @SecondaryTables (avec un "s") en associe plusieurs. Vous pouvez ainsi distribuer les données d'une entité entre les colonnes de la table primaire et celles des tables secondaires en définissant simplement les tables secondaires avec des annotations, puis en précisant pour chaque attribut la table dans laquelle il devra être stocké (à l'aide de l'annotation @Column, que nous décrirons ultérieurement).

Le code source suivant montre comment répartir les attributs d'une entité Adresse entre une table primaire et deux tables secondaires :
Adresse.java
@Entity
@SecondaryTables({
    @SecondaryTable(name="ville"),
    @SecondaryTable(name="pays")
})
public class Adresse implements java.io.Serializable {

    @Id   @GeneratedValue
    private long id;
    private String rue1;
    private String rue2;
    @Column(table="ville")
    private String ville;
    @Column(table="ville")
    private String département;
    @Column(table="ville")
    private int codepostal;
    @Column(table="pays")
    private String pays;
...
}

Par défaut, les attributs de l'entité Adresse seraient associés à la table primaire ADRESSE. L'annotation @SecondaryTables précise qu'il existe deux tables secondaires supplémentaires : VILLE et PAYS. Vous devez ensuite respectivement indiquer dans quelle table secondaire stocker chaque attribut - à l'aide de l'annotation @Column(table="ville") ou @Column(table="pays").

Le résultat d'une telle écriture est ainsi la création de trois tables distinctes se partageant les différents attributs d'une même entité, en ayant toutefois la même clé primaire (afin, bien entendu, de pouvoir les joindre).


Comme vous l'avez sûrement compris, la même entité peut contenir plusieurs annotations. Ainsi, si vous désirez renommer la table primaire, vous pouvez donc ajouter une annotation @Table à celles déjà existantes :

Adresse.java
@Entity
@Table(name="t_adresse")
@SecondaryTables({
    @SecondaryTable(name="ville"),
    @SecondaryTable(name="pays")
})
public class Adresse implements java.io.Serializable {

    @Id   @GeneratedValue
    private long id;
    private String rue1;
    private String rue2;
    @Column(table="ville")
    private String ville;
    @Column(table="ville")
    private String département;
    @Column(table="ville")
    private int codepostal;
    @Column(table="pays")
    private String pays;
...
}

Vous devez être concient de l'impact des tables secondaires sur les performances car, à chaque fois que vous accéder à une entité, le fournisseur de persistance devra accéder à plusieurs tables et les joindre. En revanche, les tables secondaires peuvent être intéressantes si vous avez des attributs de grande taille, comme les BLOB (Binary Large Objects), car vous pourrez les isoler dans une table à part.

Clés primaires

Dans les bases de données relationnelles, une clé primaire identifie chaque ligne d'une table. Cette clé peut être une simple colonne ou un ensemble de colonnes. Les clés primaires doivent évidemment être uniques (et la valeur NULL n'est pas autorisée). Des exemples de clés primaires classiques sont un numéro de compte client, un numéro de téléphone, un numéro de commande et un ISBN.

JPA exige que les entités aient un identifiant associé à une clé primaire qui suivra également les mêmes règles ; identifier de façon unique une entité à l'aide d'un simple attribut ou d'un ensemble d'attributs (clé composée). Une fois affectée, la valeur de la clé primaire d'une entité ne peut plus être modifiée.

A retenir : Un bean entité doit impérativement posséder un attribut dont la valeur est unique, dit identificateur unique ou clé primaire. Ce champ permet de différencier chaque objet entité des autres. Cette clé primaire doit être définie une seule fois dans toute la hiérarchie du bean entité.

@Id et @GeneratedValue

Une clé primaire simple (non composée) doit correspondre à un seul attribut de la classe de l'entité. L'annotation @Id que nous avons déjà rencontrée sert à indiquer une clé simple. L'attribut qui servira de clé doit être de l'un des types suivants :

  1. Les types primitifs de Java : byte, int, short, long, char.
  2. Les classes enveloppes des types primitifs : Byte, Integer, Short, Long, Character.
  3. Tableau de types primitifs ou de classes enveloppes : int[], Integer[], etc.
  4. Chaîne, nombre ou dates : java.lang.String, java.math.BigInteger, java.util.Date, java.sql.Date.

Remarque : en général, les décimaux (nombre à virgule) ne sont pas utilisés en tant que clé primaire. Les entités utilisant ce type pour la clé primaire risquent de ne pas être portables.

Lorsque nous créons une entité, la valeur de cet identifiant peut être produite manuellement par l'application, ou automatiquement par le fournisseur de persistance si nous précisons l'annotation @GeneratedValue. Celle-ci peut accepter l'une des quatre valeurs suivantes :
  1. IDENTITY : Ce type indique au fournisseur de persistance d'assigner la valeur de la clé primaire en utilisant la colonne identité de la base de données. Sous MySQL, par exemple, la clé primaire auto-générée est marquée avec AUTO_INCREMENT.
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
  2. SEQUENCE : Ce type, comme son nom l'indique, oblige le fournisseur de persistance à utiliser une séquence de la base de données. Celle-ci peut être déclarée au niveau de la classe ou au niveau du package grâce à l'annotation @SequenceGenerator et ses attributs :
    • name (requis) : définit un nom unique pour la séquence qui peut être référencée par une ou plusieurs classes (suivant le niveau utilisé pour la déclaration de l'annotation).
    • sequenceName (optionnel) : définit le nom de l'objet séquence de la base de données qui sera utilisé pour récupérer les valeurs des clés primaires liées.
    • initialValue (optionnel) : définit la valeur à laquelle doit démarrer la séquence.
    • allocationSize (optionnel) : définit le nombre utilisé pour l'incrémentation de la séquence lorsque le fournisseur de persistance y accède. Attention, la valeur par défaut est 50.
    @Entity
    @SequenceGenerator(name = "SEQ_USER", sequenceName = "SEQ_USER")
    public class Utilisateur implements Serializable {
       @Id
       @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_USER")
       public int id;

    Ce type de génération est utile lorsque la base de données offre un système natif de séquence et qu'il est conseillé de l'utiliser par le fournisseur de celle-ci.

  3. TABLE : Ce type demande au fournisseur de persistance de stocker le nom de la séquence et sa valeur courante dans une table et d'incrémenter cette valeur à chaque fois qu'une nouvelle instance de l'entité est stockée dans la base.

    Derby, par exemple, crée une table SEQUENCE de deux colonnes : une pour le nom de la séquence (qui est arbitraire) et l'autre pour la valeur (un entier incrémenté automatiquement par Derby).

    De façon annecdotique, nous pouvons pousser plus loin notre investigation en proposant l'annotation @TableGenerator qui permet de préciser les paramètres de création de la table annexe de génération dont les attributs sont les suivants :
    • name : définit un nom pour cette définition de table annexe.
    • table : définit le nom de la table dans la base de données.
    • pkColumnName : spécifie le nom de la colonne qui contient le compteur de clé primaire.
    • pkColumnValue : spécifie la colonne de la clé primaire liée.
    • allocationSize : définit le nombre d'incrémentations effectuées lorsque le fournisseur demande à la table une nouvelle valeur. Cela permet au fournisseur d'utiliser un système de cache afin de ne pas demander une nouvelle valeur à chaque demande d'un nouvel id.
    @Entity
    @TableGenerator(
    	name="C0NTACT_GEN", 
    	table="GENERATEUR_TABLE", 
    	pkColumnName="clé",
    	valueColumnName="haut",
    	pkColumnValue="id",
    	allocationSize=25
    )
    public class Utilisateur implements Serializable {
       @Id
       @GeneratedValue(strategy = GenerationType.TABLE, generator = "CONTACT_GEN")
       public int id;
  4. AUTO : Ce type demande que la génération d'une clé s'effectue automatiquement par la base de données sous-jacente, qui est libre de choisir la technique la plus appropriée. C'est la valeur par défaut de l'annotation @GeneratedValue.

    Ainsi, ce type indique au fournisseur de persistance d'utiliser la meilleure stratégie (entre ENTITY, TABLE, SEQUENCE) suivant la base de données utilisée. Le générateur AUTO est le type préféré pour avoir une application portable.

    Même si l'incrémentation automatique de la clé primaire soulage le développeur, elle doit être utilisée avec parcimonie. En effet, il est préférable d'utiliser un attribut de l'entité plutôt que d'en rajouter un, spécialement pour la clé primaire. Par exemple, l'entité Compte peut contenir une propriété numéroCompte qui se veut unique par la logique bancaire. Cette propriété est alors la meilleure candidate pour la clé primaire.

En l'absence de @GeneratedValue, l'application est responsable de la production des identifiants à l'aide d'un algorithme qui devra renvoyer une valeur unique.

Le code source suivant montre comment obtenir automatiquement un identifiant :

Livre.java
@Entity
public class Livre implements java.io.Serializable {
    @Id   @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String titre;
    private double prix;
    private String description;
    private String isbn;
    private int nombreDePages;
    private boolean illustrations;
...
}

GenerationType.AUTO étant la valeur par défaut de l'annotation, nous aurions pu omettre l'élément strategy. Notez également dans tous nos exemples que l'attribut id est annoté deux fois : avec @Id et avec @GeneratedValue.

Clés primaires composées

Lorsque nous associons des entités, il est conseillé dans la mesure du possible de dédier une seule colonne à la clé primaire. Dans certains cas, toutefois, nous sommes obligés de passer par une clé primaire composée.

Voici un exemple typique ou l'association emprunte correspond à une table de jointure qui relie la table Livre à la table Personne. Dans ce cas là, pour garder le caractère d'unicité, la clé primaire de cette table de jointure doit être composée respectivement des clés étrangères noliv et nopers avec en plus la date de sortie (le livre peut être emprunté plusieurs fois par la même personne) :

CREATE TABLE emprunte 
( 
    nopers integer not null, 
    noliv    integer not null, 
    sortie   date default current_date, 
    retour  date, 
    primary key (noliv, nopers, sortie), 
    foreign key (noliv) references Livre(noliv), 
    foreign key (nopers) references Personne(nopers) 
); 

En réalité, dans la technologie relative à la couche de persistance, la plupart du temps, nous n'avons pas besoin de créer explicitement des entités représentant les tables de jointures. Celles-ci sont fabriquées automatiquement à partir des beans entités représentant les deux tables principales comme ici Livre et Personne. Nous le verrons utltérieurement, c'est au moment de la mise en relation de type @ManyToMany entre l'entité Livre et l'entité Personne que la table de jointure se génère automatiquement.

Toutefois, dans cet exemple précis, cette table de jointure dispose de deux champs importants que sont la date d'emprunt et la date de retour. Ainsi, cette table de jointure, en même temps qu'elle sert de jonction entre les livres et les personnes, sert également à enregistrer des informations importantes. Du coup, il est finalement nécessaire de créer une entité relative afin de bien prendre en compte ces dates de stockage et de restitution. Nous étudierons cette possibilité au travers d'un projet complet de gestion de bibliothèque.

Dans ce type de situation, dans la couche de persistance, nous devons créer spécialement une classe de clé primaire pour représenter la clé primaire composée. Pour cela, nous disposons de deux annotations spécifiques pour cette classe, en fonction de la façon dont nous envisageons de structurer l'entité : @EmbeddedId et @IdClass. Comme nous le découvrirons, le résultat final sera le même. Nous aboutirons au même schéma de base de données, mais cela modifiera légèrement la façon d'interroger l'entité.

Imaginons un projet d'entreprise dans lequel le système prévoit de poster fréquemment des articles sur une page d'accueil pour signaler de nouveaux livres, des titres musicaux ou des artistes. Ces articles doivent possèder un contenu, un titre et, comme ils sont écrits dans des langues différentes, un code langue (EN pour l'anglais, FR pour le français, etc.) La clé primaire des articles pourrait donc être composée du titre et du code de la langue car un article peut être traduit en plusieurs langues tout en gardant son titre initial.

La classe de la clé primaire sera composée des attributs de type String titre et langue. Afin de permettre la gestion des requêtes et des collections internes, les classes des clés primaires doivent :

  1. Redéfinir les méthodes equals() et hashCode() ;
  2. En outre, leurs attributs doivent être de l'un des types déjà mentionnés.
  3. Elles doivent également être publiques et implémenter l'interface Serializable si elles doivent traverser les couches de l'architecture (elles peuvent être gérées dans la couche de peristance et être utilisées dans la couche de présentation, par exemple).
  4. Enfin, comme d'habitude, elles doivent posséder un constructeur par défaut.
@EmbeddedId

Comme nous le verrons plus loin, JPA utilise différentes sortes d'objets intégrés (embedded). Pour faire court, un objet intégré n'a pas d'identité propre (il n'a donc pas de clé primaire) et ses attributs sont stockés dans des colonnes de la table associée à l'entité qui le contient.

Le code source suivant présente la classe NewsId comme une classe intégrable (embeddable). Il s'agit simplement d'un objet intégré (annoté @Embeddable) composé de deux attributs (titre et langue). Cette classe doit :
  1. Redéfinir les méthodes equals() et hashCode() ;
  2. En outre, leurs attributs doivent être de l'un des types déjà mentionnés.
  3. Elles doivent également être publiques et implémenter l'interface Serializable si elles doivent traverser les couches de l'architecture (elles peuvent être gérées dans la couche de peristance et être utilisées dans la couche de présentation, par exemple).
  4. Enfin, comme d'habitude, elles doivent posséder un constructeur par défaut.

Le code source suivant présente la classe NewsId comme une classe intégrable (embeddable). Il s'agit simplement d'un objet intégré (annoté @Embeddable) composé de deux attributs (titre et langue). Cette classe doit avoir un constructeur par défaut, des getters() et redéfinir la méthode equals() et hashCode(). Vous remarquez que la classe n'a pas d'identité par elle-même (aucune annotation @Id : c'est ce qui caractérise un objet intégrable.

entité.NewsId.java
package entité;

import java.io.Serializable;
import javax.persistence.Embeddable;

@Embeddable
public class NewsId implements Serializable {
    private String titre;
    private String langue;

    public NewsId() { }

    public NewsId(String titre, String langue) {
        this.titre = titre;
        this.langue = langue;
    }

    public String getLangue() { return langue; }
    public String getTitre()     { return titre; }

    @Override
    public boolean equals(Object obj) { // généré automatiquement par NetBeans
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final NewsId other = (NewsId) obj;
        if ((this.titre == null) ? (other.titre != null) : !this.titre.equals(other.titre)) {
            return false;
        }
        if ((this.langue == null) ? (other.langue != null) : !this.langue.equals(other.langue)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() { // généré automatiquement par NetBeans
        int hash = 7;
        hash = 89 * hash + (this.titre != null ? this.titre.hashCode() : 0);
        hash = 89 * hash + (this.langue != null ? this.langue.hashCode() : 0);
        return hash;
    }
} 

L'entité News, présentée dans le code source qui suit, doit maintenant intégrer la classe de clé primaire NewsId à l'aide de l'annotation @EmbeddedId. Toutes les annotations @EmbeddedId doivent désigner une classe intégrable annotée par @Embeddable.

entité.News.java
package entité;

import java.io.Serializable;
import javax.persistence.*;

@Entity
public class News implements Serializable {
    @EmbeddedId
    private NewsId id;
    private String contenu;

    public News() { }
    
    public News(String titre, String langue, String contenu) {
        id = new NewsId(titre, langue);
        this.contenu = contenu;
    }
    
    public NewsId getId()        { return id;  }
    public String getContenu() { return contenu;  }
} 

Lors d'une prochaine étude, nous verrons plus précisément comment retrouver les entités à l'aide de leur clé primaire, mais l'exemple ci-dessous présente le principe général : La clé primaire étant une classe avec un constructeur, vous devez d'abord l'instancier avec les valeurs qui forment la clé, puis passer cet objet au gestionnaire d'entités :

session.Gestion.java
package session;

import entité.*;
import javax.ejb.*;
import javax.persistence.*;

@Stateless
@LocalBean
public class Gestion {
    @PersistenceContext
    private EntityManager persistance;
    
    public News recherche(String titre, String langue) {
        return persistance.find(News.class, new NewsId(titre, langue));
    }
...
}
@IdClass

L'autre méthode pour déclarer une clé primaire compsée consiste à utiliser l'annotation @IdClass. Cette approche est différente de la précédente car, ici, chaque attribut de la classe de la clé primaire doit également être déclaré dans la classe entité et annoté avec @Id.

La clé primaire NewsId du code source suivant est maintenant un objet classique qui ne nécessite aucune annotation particulière :
entité.NewsId.java
package entité;

import java.io.Serializable;
import javax.persistence.Embeddable;

public class NewsId implements Serializable {  // la classe de la clé primaire n'est pas annotée
    private String titre;
    private String langue;

    public NewsId() { }

    public NewsId(String titre, String langue) {
        this.titre = titre;
        this.langue = langue;
    }

    public String getLangue() { return langue; }
    public String getTitre()     { return titre; }

    @Override
    public boolean equals(Object obj) { // généré automatiquement par NetBeans
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final NewsId other = (NewsId) obj;
        if ((this.titre == null) ? (other.titre != null) : !this.titre.equals(other.titre)) {
            return false;
        }
        if ((this.langue == null) ? (other.langue != null) : !this.langue.equals(other.langue)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() { // généré automatiquement par NetBeans
        int hash = 7;
        hash = 89 * hash + (this.titre != null ? this.titre.hashCode() : 0);
        hash = 89 * hash + (this.langue != null ? this.langue.hashCode() : 0);
        return hash;
    }
} 

L'entité News doit simplement définir la classe de la clé primaire à l'aide de l'annotation @IdClass et annoter chaque attribut de la clé avec @Id. Pour stocker l'entité News, vous devrez maintenant donner une valeur aux attributs titre et langue.

entité.News.java
package entité;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@IdClass(NewsId.class)
public class News implements Serializable {
    @Id    
    private String titre;
    @Id  
    private String langue;
    private String contenu;

    public News() { }
    
    public News(String titre, String langue, String contenu) {
		   this.titre = titre;
        this.langue = langue;
        this.contenu = contenu;
    }
    
    public NewsId getId()        { return id;  }
    public String getContenu() { return contenu;  }
} 

Les deux approches, @EmbeddedId et @IdClass, donneront la même structure de table. Les attributs de l'entité et de la clé primaire se retrouveront bien dans la même table et la clé primaire sera formée des attributs de la classe clé primaire (titre et langue).

CREATE TABLE emprunte 
( 
    CONTENU varchar(255), 
    TITRE       varchar(255) not null, 
    LANGUE  varchar(255) not null, 
    primary key (TITRE, LANGUE) 
); 

L'approche par @IdClass est plus sujette aux erreurs car vous devez définir chaque attribut de la clé primaire à la fois dans la classe de clé primaire et dans l'entité, en vous assurant d'utiliser les mêmes noms et les mêmes types.

La seule différence visible est la façon dont vous ferez référence à l'entité dans JQPL.

@IdClass : SELECT n.titre FROM News n
@EmbeddedId : SELECT n.newsId.titre FROM News n

Attributs

Une entité doit posséder une clé primaire (simple ou composée) pour être identifiable dans une base de données relationnelle. Elle dispose également de toutes sortes d'attributs qui forment son état, qui doit également être associé à la table. Cet état peut contenir quasiment tous les types Java que vous pourriez vouloir associer :

  1. Les types primitifs de Java : byte, int, short, long, char, float, double.
  2. Les classes enveloppes des types primitifs : Byte, Integer, Short, Long, Character, Float, Double.
  3. Tableaux d'octets ou de caractères : byte[], Byte[], char[], Character[].
  4. Chaîne, nombre ou dates : java.lang.String, java.math.BigInteger, java.math.BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp.
  5. Les types énumérés et types implémentant l'interface Serializable, définis par l'utilisateur.
  6. des collections de type de base et de type intégrable.

Bien sûr, une entité peut également avoir des attributs entités, collections d'entités ou d'instances de classes intégrables. Ceci implique d'introduire des relations entre les entités (que nous étudierons en détail ultérieurement).

Comme nous l'avons vu, en vertu de la configuration par exception, les attributs sont associés selon des règles par défaut. Parfois, cependant, vous aurez besoin d'adapter certaines parties de cette association ; c'est là que les annotations JPA entrent une nouvelle fois en jeu.

@Basic :

La politique de l'API de persistance est de considérer toute propriété comme un champ persistant. Cela signifie qu'il n'est pas nécessaire d'annoter les propriétés pour les désigner persistantes. Le conteneur considère par défaut que la propriété est annotée avec @Basic avec les valeurs par défaut des attributs suivants :

  1. fetch (FetchType.EAGER par défaut) : définit si le contenu de la propriété doit être chargée à la demande (Lazy loading - FetchType.LAZY - paresseusement) ou au moment du chargement de l'entité (FetchType.EAGER - désireux).
  2. optional (true par défaut) : définit si la propriété accepte la valeur null ou non. Cet attribut ne fonctionne pas pour les types primitifs (qui ne peuvent être nuls).

Pour spécifier qu'un attribut ne doit pas être persistant, vous devez l'annoter avec @Transient.
.

Considérons, par exemple, l'entité Piste du code source suivant. Un CD est en effet constitué de plusieurs pistes ayant chacune un titre, une description et un fichier .wav d'une certaine durée. Ce dernier est un BLOB qui peut occuper plusieurs mégaoctets. Lorsque nous accédons à l'entité Piste, nous ne voulons pas charger immédiatement le fichier WAV : nous annotons donc l'attribut avec @Basic(fetch = FetchType.LAZY) pour que ces données ne soient lues dans la base que lorsqu'elles seront vraiment nécessaires (lorsque nous accéderons, par exemple, à l'attribut wav via son getter)
Piste.java
public class Piste {
    @Id    
    private long id;
    private String titre;
    private double durée;
    @Lob  
    @Basic(fetch = FetchType.LAZY)
    private byte[] wav;
    @Basic(optional = true)
    private String description;
...
} 

Notez que l'attribut wav de type byte[] est également annoté @Lob afin que sa valeur soit stockée comme un LOB (Large Object) - les colonnes pouvant stocker ces types de gros objets nécessitent des appels JDBC spéciaux pour être accessibles à partir de Java. Pour en informer le fournisseur, il faut donc ajouter une annotation @Lob à l'association de base.

@Lob :

Ainsi, l'annotation @Lob s'avère utile lorsque vous souhaitez stocker des tableaux de bytes (byte[] ou Byte[]) pour représenter le contenu d'un fichier par exemple :

@Lob
@Basic(fetch=FetchType.LAZY)
public byte[] fichierImage;

L'annotation @Lob s'applique également sur des propriétés de type java.sql.Clob (Character Large Object) ou java.sql.Blob (Binary Large Object). Nous avons volontairement ajouté le mode paressseux. Effectivement, le contenu de cette propriété risque d'être de grande taille.

@Column

L'annotation @javax.persistence.Column définit toutes les propriétés possibles d'une colonne dans la table. Grâce à elle, nous pouvons modifier le nom de la colonne (qui, par défaut, est le même que celui de l'attribut), la taille du champ correspondant, et de nombreuses autres caractéristiques à utiliser pour une propriété persistante, comme notamment si la valeur dans la colonne peut être nulle ou pas.

Voici une description des attributs, tous optionnels, de l'annotation @Column :

  1. name : précise le nom de la colonne liée. Le nom de la propriété est utilisée par défaut.
  2. unique : précise si la propriété est une clé unique ou non (la valeur est unique dans la table).
  3. nullable : précise si la colonne accepte des valeurs nulles ou non.
  4. insertable : précise si la valeur doit être incluse lors de l'exécution de la requête SQL INSERT. La valeur par défaut est true.
  5. updatable : précise si la valeur doit être mise à jour lors de l'exécution de la requête SQL UPDATE. La valeur par défaut est true.
  6. columnDefinition : précise le morceau de code SQL pour la définition de la colonne dans la base de données. C'est avec cet attribut que l'on peut préciser le type SQL de la colonne.
  7. table : précise la table utilisée pour contenir la colonne. La valeur par défaut est la table principale de l'entité. Cet attribut est utilisé lorsqu'un bean entité est mappé sur plusieurs tables.
  8. length : précise la longueur que la base de données doit associer à un champ texte. La longueur par défaut est 255.
  9. precision : précise le nombre maximum de chiffres que la colonne peut contenir. La précision par défaut est définie par la base de données.
  10. scale : précise le nombre fixe de chiffre après le séparateur décimal (en général le point). Cet attribut n'est utilisable que pour des propriétés décimales (float, double, ...). Le nombre de décimales par défaut est définie par la base de données.

Les nombreux paramètres par défaut de l'API de persistance offrent un avantage certain au développeur. Celui-ci peut rapidement tester ses entités. Toutefois, il ne doit pas en rester là, mais utiliser les possiblités exposées ici pour optimiser le mapping entre ses entités et les tables de la base de données.

Pour redéfinir l'association par défaut de l'entité Livre initiale, nous pouvons utiliser de différentes façons l'annotation @Column. Ci-dessous, nous modifions les noms des colonnes associées aux attributs titre et nombreDePages, pour lesquels nous n'autorisons pas les valeurs NULL ; nous précisons également la longueur de la colonne associée à description. et enfin nous proposons un affichage spécifique pour le prix au niveau de la table si nous désirons la consulter extérieurement :
Livre.java
@Entity
public class Livre implements java.io.Serializable {
    @Id   
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(name = "titre_livre", nullable = false, updatable = false)
    private String titre;
    @Column(precision = 5, scale = 2)
    private double prix;
    @Column(length = 2000)
    private String description;
    private String isbn;
    @Column(name = "nombre_pages", nullable = false)
    private int nombreDePages;
    private boolean illustrations;
...
}
Table associée
CREATE TABLE LIVRE 
( 
    ID                           BIGINT not null, 
    TITRE_LIVRE          VARCHAR(255) not null, 
    PRIX                       DOUBLE(5, 2), 
    DESCRIPTION         VARCHAR(2000), 
    ISBN                       VARCHAR(255), 
    NOMBRE_PAGES   INTEGER not null, 
    ILLUSTRATION       SMALLINT, 
    primary key (ID) 
); 
  1. La plupart des éléments de l'annotation @Column influent sur l'association. Si nous fixons à 2000 la longueur de l'attribut description, par exemple, la taille de la colonne correspondante sera également de 2000.
  2. Par défaut updatable et insertable valent true, ce qui signifie que nous pouvons insérer ou modifier n'importe quel attribut dans la base de données. En les positionnant à false, nous demandons au fournisseur de garantir qu'il n'insérera ni ne modifiera les données des colonnes associées à ces attributs lorsque l'entité sera elle-même modifiée.

    Notez que ceci n'implique pas que l'entité ne pourra être modifiée en mémoire - elle pourra l'être mais, en ce cas, elle ne sera plus synchronisée avec la base car l'instruction SQL qui sera produite (INSERT ou UPDATE) ne portera pas sur ces colonnes.

@Temporal

De la même manière, les types java.util.Date ou java.util.Calendar utilisés pour définir des propriétés dites temporelles peuvent être paramétrées pour spécifier le format le plus adéquat à sa mise en persistance, en faisant le choix entre la date, l'heure ou des millisecondes. Ceci peut être précisé grâce à l'annotation @javax.persistence.Temporal qui prend en paramètre en TemporalType (énumération) dont les valeurs sont les suivantes :

  1. DATE : utilisé pour la date uniquement (java.sql.Date),
  2. TIME : utilisé pour l'heure uniquement (java.sql.Time),
  3. TIMESTAMP : utilisé pour les temps plus précis, date + heure à la milliseconde près (java.sql.TimeStamp).
@Temporal(TemporalType.DATE)
private Calendar dateDeNaissance;
L'entité Photo du code source ci-dessous sera associé à la table décrite plus bas. L'attribut instant est associé à une colonne de type TIMESTAMP :
entité.Photo.java
@Entity
public class Photo implements java.io.Serializable {
    @Id
    private String id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date instant;
    private int largeur;
    private int hauteur;
    private long poids;
...
}  
Table associée
CREATE TABLE PHOTO 
( 
    ID                      VARCHAR(255) not null, 
    INSTANT           TIMESTAMP, 
    LARGEUR         INTEGER, 
    HAUTEUR         INTEGER, 
    POIDS               BIGINT, 
    primary key (ID) 
);
@Transient

Avec JPA, tous les attributs d'une classe annotée par @Entity sont sont automatiquement associés à une table. Si vous ne souhaitez pas associer un attribut particulier, utilisez l'annotation @javax.persistence.Transient.

Au travers d'une entité Personnel, nous pouvons avoir besoin éventuellement d'un attribut âge, qui est tributaire d'un calcul en rapport avec la date de naissance, mais qui ne nécessite pas spécialement d'être stocké dans la base de données. Il peut donc être déclaré comme transitoire à l'aide de l'annotation @Transient.
Personnel.java
@Entity
public class Personnel implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    @Temporal(TemporalType.DATE)
    private Date naissance;
    @Transient
    private int âge; // cet attribut n'aura pas de colonne AGE associée
...
}  
Table associée
CREATE TABLE PERSONNEL
( 
    ID                     BIGINT not null, 
    NOM                VARCHAR(255), 
    PRENOM         VARCHAR(255), 
    EMAIL             VARCHAR(255), 
    TELEPHONE    VARCHAR(255), 
    NAISSANCE    DATE,
    primary key (ID) 
);
@Enumerated

Depuis J2SE 5.0, le langage Java apporte un nouveau type de données : les énumérations. Ce type existe depuis plusieurs années au sein des bases de données. Il est enfin possible de les utiliser dans les beans entités. L'énumération permet de spécifier un ensemble de valeurs possibles pour une propriété. Par exemple, le sexe d'un utilisateur ne peut prendre que deux valeurs possibles : Masculin ou Féminin. La solution idéale dans cette situation est bien entendu l'utilisation de l'énumération. La valeur d'une énumération peut être enregistrée soit via une chaîne de caractères soit via un entier. L'annotation @Enumerated prend en paramètre un objet EnumType qui définit la façon de stocker cette valeur. Les valeurs EnumType.STRING ou EnumType.ORDINAL sont utilisées respectivement pour l'enregistrement dans une chaîne de caractères ou dans un entier.

@Entity
public class Utilisateur implements Serializable {
   public enum Sexe {Masculin, Féminin};

   @Enumerated(value=EnumType.STRING)
   public Sexe sexe;
...
}
Les valeurs d'une énumération sont en réalité des constantes auxquelles est implicitement associée un numéro déterminé par leur ordre d'apparition dans l'énumération. Ce numéro ne peut pas être modifié en cours d'exécution mais sert à stocker la valeur du type énuméré dans la base de données. Voici, par exemple, le code source représentant une énumération de type cartes de crédit :
TypeCarteCrédit.java
public enum TypeCarteCrédit  {
   Visa,
   MasterCard,
   AmericanExpress
}  

Les numéros affectés lors de la compilation aux valeurs de ce type énuméré seront respectivement :

Par défaut, les fournisseurs de persistance associerons ce type énuméré à la base de données en supposant que la colonne est de type INTEGER. Le code source suivant montre l'entité CarteCrédit qui utilise l'énumération précédente avec une association par défaut :

CarteCrédit.java
@Entity
public class CarteCrédit {
    @Id
    private String numéro;
    private String dateExpiration;
    private int numéroControl;
    private TypeCarteCrédit typeCarte;
...
}  

Les règles par défaut feront que l'énumération sera associée à une colonne de type entier et tout ira bien. Imaginons maintenant que nous ajoutions une nouvelle constante au début de l'énumération. L'affectation des numéros dépendant de l'ordre d'apparition des constantes, les valeurs déjà stockées dans la base de données ne correspondront plus à l'énumération.

Une meilleure solution consiste donc à stocker le nom de la constante à la place de son numéro d'ordre. C'est ce que nous proposons avec l'annotation @Enumerated avec la valeur STRING (sa valeur par défaut est ORDINAL).

CarteCrédit.java
@Entity
public class CarteCrédit {
    @Id
    private String numéro;
    private String dateExpiration;
    private int numéroControl;
    @Enumerated(value=EnumType.STRING)
    private TypeCarteCrédit typeCarte;
...
}  

Désormais, la colonne TYPECARTE de la table sera de type VARCHAR et une carte Visa sera stockée sous la forme "Visa".

Collections de types de base

Les collections sont très utilisées en Java. Lors de cette étude, nous aurons l'occasion d'étudier les relations entre entités (qui peuvent être des collections d'entités) : essentiellement, ceci signifie qu'une entité contient une collection d'autres entités ou d'objets intégrables. En terme d'association, chaque entité est associée à sa propre table et nous créons alors des références entre les clés primaires et les clés étrangères.

Dans de nombreux cas, il n'est pas toujours nécessaire de créer plusieurs entités alors qu'une seule peut suffire. Comme vous le savez, une entité est une classe Java avec une identité et de nombreux attributs : mais la question qui se pose c'est comment faire pour stocker dans une base de données un attribut qui représente une simple collection de types Java comme les String et/ou des Integer ?

Depuis JPA 2.0, intégré dans Java EE6, il n'est plus nécessaire de créer spécifiquement une classe distincte pour encapsuler ces collections, car nous disposons de nouvelles annotations @ElementCollection et @CollectionTable.

  1. L'annotation @ElementCollection indique qu'un attribut de type java.util.Collection contient les types de base prédéfinis de Java,
  2. tandis que @CollectionTable permet de modifier les détails de la table de la collection - son nom, par exemple. Si cette dernière est omise, le nom de la table sera formé par la concaténation du nom de l'entité conteneur et de celui de l'attribut représentant la collection, séparé par le caractère souligné ("_", appelé aussi underscore).
Reprenons une nouvelle fois l'entité Personnel et modifions l'attribut téléphones qui donnera, comme son nom l'indique, la liste des numéros de téléphones. Nous avons donc besoin de mettre en place une collection de chaînes pour stocker tous ces numéros :
Personnel.java
@Entity
public class Personnel implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private ArrayList<String> téléphones = new ArrayList<String>();
    @Temporal(TemporalType.DATE)
    private Date naissance;
    @Transient
    private int âge;
...
}  

La première démarche consiste à ne placer aucune annotation sur l'attribut téléphones. La base de données considère alors, vu qu'il s'agit d'une collection et qu'il peut y avoir un nombre conséquent de valeurs, qu'il est préférable de prendre le type BLOB au niveau de la colonne représentant ces informations.

En effet, java.util.ArrayList implémente l'interface Serializable et JPA sait associer automatiquement des objets sérializables à des BLOB. En revanche, si vous utilisez une collection java.util.List, vous obtiendrez alors une exception car List n'implémente justement pas Serializable.

Cela fonctionne parfaitement. Le problème de ce choix toutefois, c'est qu'il est alors très difficile de consulter au niveau de la table le contenu d'une telle colonne.

Personnel.java
@Entity
public class Personnel implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    @ElementCollection
    private ArrayList<String> téléphones = new ArrayList<String>();
    @Temporal(TemporalType.DATE)
    private Date naissance;
    @Transient
    private int âge;
...
}  

L'annotation @ElementCollection informe le fournisseur de persistance que l'attribut téléphones est une liste de chaînes qui devra être enregistrée dans une table séparée dont le nom est par défaut PERSONNEL_TELEPHONES avec deux colonnes, d'une part PERSONNEL_ID qui représente la clé primaire de la table PERSONNEL et TELEPHONES qui stocke l'ensemble des numéros.

L'annotation @ElementCollection est un moyen plus élégant et plus pratique de stocker les types primitifs puisque cette fois-ci nous visualisons parfaitement chacun des numéros de téléphone alors que le stockage sous un format binaire opaque aux requêtes les rends inaccessibles.

Personnel.java
@Entity
public class Personnel implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    @ElementCollection(fetch = FetchType.EAGER)
    private ArrayList<String> téléphones = new ArrayList<String>();
    @Temporal(TemporalType.DATE)
    private Date naissance;
    @Transient
    private int âge;
...
}  

Attention, les collections sont gérées de façon paresseuse au niveau de la base de données. Si vous voulez être sûr que vos numéros soient immédiatement enregistrés, il serait alors préférable de stipuler la valeur FetchType.EAGER sur l'attribut fetch de @ElementCollection.

Personnel.java
@Entity
public class Personnel implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "Téléphones")
    @Column(name = "Numéro")
    private ArrayList<String> téléphones = new ArrayList<String>();
    @Temporal(TemporalType.DATE)
    private Date naissance;
    @Transient
    private int âge;
...
}  

En l'absence de @CollectionTable, nous l'avons vu, le nom de la table est PERSONNEL_TELEPHONES. Grâce à cette annotation, il est tout-à-fait possible de choisir explicitement le nom de la table représentant la collection juste en spécifiant l'attribut name. Vous remarquez que j'ai ajouté une annotation @Column supplémentaire afin de renommer également la colonne en Numéro au lieu de Téléphones.

Voici le résultat obtenu en tenant compte de ces modifications. Personnellement, je trouve que les noms proposés par défaut sont assez évocateurs et j'utilise assez rarement cette annotation @CollectionTable.

Association des types de base

Comme les collections, les tables de hachage, communément appelées les cartes, sont très utiles pour le stockage des données. Avec JPA 1.0, nous pouvions pas en faire grand chose en terme d'ORM. Désormais, les cartes peuvent utiliser n'importe quelle combinaison de types de base, d'objets intégrables et d'entités comme clés ou comme valeurs : ceci apporte beaucoup de souplesse. Pour l'instant, nous nous intéressons uniquement aux cartes qui utilisent les types de base.

Lorsqu'une carte emploie des types de base, vous pouvez vous servir des annotations @ElementCollection et @CollectionTable exactement comme nous venons de le voir pour les collections. En ce cas, les données relatives aux cartes sont également stockées dans une table spécifique, la table de collection.

Prenons l'exemple d'un CD contenant un certain nombre de pistes. Une piste peut être considérée comme un titre et une position (la première piste de l'album, la seconde, etc.). Vous pourriez alors utiliser des cartes de pistes utilisant un entier pour représenter la position (la clé) et une chaîne pour représenter le titre (la valeur) :
CD.java
@Entity
public class CD implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    @Lob
    private byte[] jacquette;
    private String description;
    private HashMap<Integer, String> pistes = new HashMap<Integer, String>();
...
}  

Comme précédemment, je propose de ne spécifier aucune annotation particulière sur l'attribut pistes. Là aussi, la colonne représentative PISTES de la table CD est alors de type BLOB :

Là aussi, nous obtenons qu'une seule table qui possède alors toutes les caractéristiques. Cela peut suffire et fonctionne parfaitement. Ceci dit, encore une fois, la colonne représentant les pistes est totalement illisible (mais ce n'est peut-être pas un problème).

CD.java
@Entity
public class CD implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    @Lob
	   private byte[] jacquette;
    private String description;
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "Piste")
    @MapKeyColumn(name = "Position")
    @Column(name = "Titre")
    private HashMap<Integer, String> pistes = new HashMap<Integer, String>();
...
}  
  1. Comme nous l'avons déjà indiqué, l'annotation @ElementCollection permet d'indiquer que les cartes seront stockées dans une table à part. L'annotation optionnelle @CollectionTable, quant à elle, est encore une fois utilisée pour modifier le nom par défaut de la table de collection en PISTE.
  2. La différence avec les collections est que nous introduisons ici une nouvelle annotation, @MapKeyColumn, pour préciser l'association correspondant à la colonne clé de la carte. En son absence, le nom de cette colonne est formé par concaténation du nom de l'attribut qui référence la relation et du suffixe _KEY. Le code source précédent utilise cette annotation pour la renommer POSITION afin qu'elle porte un nom plus lisible.
  3. L'annotation @Column indique que la colonne contenant les valeurs des cartes sera nommée TITRE.

 

Choix du chapitre Objets intégrables (Agrégation et Composition)

Dans le chapitre sur "les clés primaires composées" plus haut dans cette étude, nous avons rapidement vu comment une classe pouvait être intégrée pour servir de clé primaire avec l'annotation @EmbeddedId.

Les objets intégrables sont des objets qui n'ont pas d'identité persistante par eux-mêmes. Une entité peut contenir des collections d'objets intégrables ainsi qu'un simple attribut d'une classe intégrable : dans les deux cas, ils seronts stockés comme faisant partie de l'entité et partageront son identité. Ceci signifie que chaque attribut de l'objet intégré est associé à la table de l'entité.

Il s'agit donc d'une relation de propriété stricte (agrégation forte ou composition). Ainsi, quand l'entité est supprimée, l'objet intégré disparaît également.

Cette composition entre deux classes passe par des annotations.

  1. La classe incluse utilise @Embeddable.
  2. L'entité qui inclut utilise @Embedded.
Prenons l'exemple d'un client possédant un identifiant, un nom, un e-mail et une adresse. Tous ces attributs pourrait se trouver dans une seule entité Client, mais, pour des raisons de modélisation, ils sont répartis en deux classes Client et Adresse. Cette dernière n'ayant pas d'identité propre mais étant simplement une composante de l'état du client, c'est une bonne candidate au status de classe intégrable.
Adresse.java
@Embeddable
public class Adresse {
    private String rue;
    private String ville;
    private int codepostal;
    private String pays;
...
}

Comme vous pouvez le constater à la lecture de ce code source, la classe Adresse est annotée comme étant non pas une entité mais une classe intégrable - l'annotation @Embeddable indique qu'Adresse peut être intégrée dans une entité (ou dans une autre classe intégrable). A l'autre extrémité de la composition, l'entité Client doit utiliser l'annotation @Embedded pour indiquer qu'Adresse est un attribut persistant qui sera stockée comme composante interne et qu'il partage son identité.

Client.java
@Entity
public class Client implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    @Embedded
    private Adresse adresse;
...
}  

Chaque attribut d'Adresse est associé à la table de l'entité Client. Nous disposerons ainsi d'une seule table qui fusionne l'ensemble des attributs des deux classes.


 

Choix du chapitre Relations entre entités

Nous venons d'étudier toutes sortes d'annotations permettant de mapper une classe (ou plusieurs) dans une seule table. Le monde orienté objet regorge aussi de relations entre les classes, agrégation comme nous venons de la voir, associations unidirectionnelles, associations multiples, héritage, etc. Il est possible de rendre persistante cette information de telle sorte qu'une classe peut être liée à une autre dans un modèle relationnel.

Il s'avère qu'une entité ne travaille généralement pas seule mais qu'elle est reliée à d'autres entités. On parle de relations entre entités. Cela correspond, bien entendu, à une base de données relationnelle où les tables sont effectivement en relations les unes avec les autres.

Une relation possède une direction. Elle peut être unidirectionnelle (un objet peut utiliser un autre objet) ou bidirectionnelle (un objet peut interroger un autre objet et vice versa). En Java, nous utilisons le point (.) pour naviguer entre les objets. Ainsi, lorsque nous écrivons, par exemple :
client.getAdresse().getPays();
nous navigons d'un objet de type Client vers un objet de type Adresse puis vers un objet de type Pays.

En UML, une relation :

  1. unidirectionnelle entre deux classes est représentée par une flèche indiquant la direction. Ainsi, par exemple, Classe1 (la source) peut naviguer vers Classe2 (la cible), mais pas l'inverse.

  2. bidirectionnelle n'utilise pas de flèche : Classe1 peut naviguer vers Classe2 et vice versa. En java, ce type de relation est représentée par une classe Classe1 ayant un attribut instance de Classe2 et par une classe Classe2 ayant un attribut instance de Classe1.

  3. possède également une cardinalité. Chaque extrémité peut préciser le nombre d'objets impliqués dans cette relation. Par exemple, une instance de Classe1 est en relation avec zéro ou plusieurs instances de Classe2.



    En UML, une cardinalité est un intervalle compris entre un minimum et un maximum :
    • 0..1 : signifie qu'il existera au minimum zéro objet et au maximum un objet
    • 1 : signifie qu'il n'existera qu'une et une seule instance,
    • 1..* : qu'il existera une ou plusieurs instances,
    • 3..6 : qu'il existera entre trois et six objets.

    En Java, une relation qui représente plusieurs objets utilise les collections de :

    1. java.util.Collection,
    2. java.util.Set,
    3. Java.util.List,
    4. java.util.Map.

    Ces types de conteneur sont en réalités des interfaces. Il faudra bien sur prévoir des conteneurs concrets en correspondance avec l'interface choisie. Ainsi par exemple, nous pourrons créer un conteneur de type ArrayList lorsque nous choisissons l'interface Collection.

    Une relation possède un propriétaire. Dans une relation unidirectionnelle, ce propriétaire est implicite. Dans une relation bidirectionnelle, il faut en revanche l'indiquer explicitement en désignant le côté propriétaire, qui spécifie l'association physique, et le côté opposé (non propriétaire).

Au cours de ce chapitre, nous verrons comment associer des collections d'objets à l'aide des annotations JPA.

Relations dans la base de données relationnelles

Dans le monde relationnel, les choses sont différentes puisque, à proprement parler, une base de données relationnelle est un ensemble de relations (également appelées tables) : tout est modélisé sous forme de table - pour modéliser une relation, vous ne disposez ni de listes, ni d'ensembles, ni de cartes : vous ne possédez que des tables.

Ainsi, une relation entre deux classes Java sera représentée dans la base de données par une référence à une table qui peut être modélisée de deux façons :

  1. Avec une clé étrangère (une colonne de jointure) : A titre d'exemple, supposons qu'un client n'ait qu'une seule adresse, ce qui implique une relation 1-1. En Java, la classe Client aurait donc un attribut Adresse ; dans le monde relationnel, vous pourriez avoir ainsi une table Client pointant vers une table Adresse via la clé étrangère, comme vous le découvrez ci-dessous :


  2. Avec une table de jointure : la seconde méthode, comme vous le voyez ci-dessus, consiste à utiliser une table de jointure. Ainsi, la table Client ne stocke plus la clé étrangère vers Adresse mais utilise une table intermédiaire pour représenter la relation liant ces deux tables. Cette relation est constituée par les clés primaires des deux tables.

    Nous n'utilisons jamais de table de jointure pour représenter une relation 1-1 car cela pourrait avoir des conséquences sur les performances (il faudrait effectivement toujours accéder à la troisième table pour obtenir l'adresse d'un client) ; elles sont généralement réservées aux relations 1-N ou N-M.

    Comme nous le verrons par la suite, JPA utilise ces deux méthodes pour associer les relations entre les objets à une base de données.
    .

Relations entre entités

Revenons maintenat à JPA. La plupart des entités doivent pouvoir référencer ou être en relation avec d'autres entités. JPA permet d'associer ces relations de sorte qu'une entité puisse être liée à une autre dans un modèle relationnel.

Comme pour les annotations d'associations élémentaires, que nous avons déjà étudiées, JPA utilise une configuration par exception pour ces relations : il utilise un mécanisme par défaut pour stocker une relation mais, si cela ne convient pas, vous disposez de plusieurs annotations pour adapter l'association à vos besoins.

Les annotations correpondantes aux cardinalités d'une relation entre deux entités sont les suivantes, en sachant que chacune d'elles peut être utilisée de façon unidirectionnelle ou bidirectionnelle :

Unidirectionnelle et bidirectionnelle

Du point de vue de la modélisation objet, la direction entre les classes est naturelle. Dans une relation unidirectionnelle, un objet de type Classe1 pointe uniquement vers un objet de type Classe2, alors que dans une relation bidirectionnelle, ils se font mutuellement référence.

Cependant, comme le montre l'exemple suivant d'un client et de son adresse, un peu de travail est nécessaire lorsque nous désirons représenter une relation bidirectionnelle dans une base de données.

  1. Dans une relation unidirectionnelle, une entité Client possède un attribut de type Adresse. Cette relation ne va que dans un seul sens : on dit que le client est le propriétaire de la relation. D'un point de vue de la base de données, ceci signifie que la table CLIENT contiendra une clé étrangère (une colonne de jointure) pointant vers la table ADRESSE. Par ailleurs, le propriétaire de la relation peut personnaliser la traduction de cette relation : si vous modifiez le nom de la clé étrangère, par exemple, cette annotation aura lieu dans l'entité Client (le propriétaire).

  2. Cependant, comme nous l'avons mentionné précédemment, les relations peuvent être également bidirectionnelles. Ainsi, pour naviguer entre Adresse et Client, nous devons ajouter également un attribut Client à l'entité Adresse. Notez que les attributs représentant une relation n'apparaissent pas dans les diagrammes UML.

    En termes de Java et d'annotations, ceci revient à posséder deux associations de type 1-1 dans les deux directions opposées. Nous pouvons donc considérer une relation bidirectionnelle comme une paire de relations unidirectionnelles dans les deux sens.

Gestion de la bidirectionnalité

Comment associer une bidirectionnalité à une base de données ? Qui est le propriétaire de cette relation bidirectionnelle ? A qui appartient l'information sur la colonne ou la table de jointure ? Posons-nous ces questions une fois pour toutes. Les cas les plus fréquents restent l'unidirectionnalité que nous traiterons par la suite. Pour une fois, nous commençons par le plus difficile (ce qui est très relatif).

Si les relations unidirectionnelles possèdent un côté propriétaire, les bidirectionnelles ont à la fois un côté propriétaire et un côté opposé, qui doivent impérativement être indiqué explicitement par l'élément mappedBy des annotations @OneToOne, @OneToMany et @ManyToMany. mappedBy identifie ainsi l'attribut priopriétaire de la relation : il est obligatoire pour les relations bidirectionnelles.

Afin d'illustrer tout ceci, comparons ensemble le code Java de sa traduction dans la base de données. Comme vous pouvez le constater dans le codage, les deux entités pointent l'une vers l'autre au moyen des attributs respectifs : Client possède un attribut adresse annoté par @OneToOne et l'entité Adresse possède l'attribut client également annoté. Au niveau de la base de données se trouvent les deux tables respectives CLIENT et ADRESSE. CLIENT est la table propriétaire de la relation car elle contient la clé étrangère vers ADRESSE.


  1. L'entité Adresse utilise mappedBy de son annotation @OneToOne. Ici, mappedBy indique que la colonne de jointure (adresse) est déclarée à l'autre extrémité de la relation.
  2. De son côté, l'entité Client définit la colonne de jointure avec l'annotation @JoinColumn et renomme la clé étrangère en AdresseId.
  3. Client est l'extrémité propriétaire de le relation et, en tant que telle, est la seule à définir l'association de la colonne de jointure.
  4. Adresse est l'extrémité opposée et c'est donc la table de l'entité propriétaire qui contient la clé étrangère (la table CLIENT possède bien une colonne ADRESSEID).

Il existe un élément mappedBy pour les annotations @OneToOne, @OneToMany et @ManyToMany, mais pas pour @ManyToOne.
.

Relation Un à Un unidirectionnelle : @OneToOne

Maintenant que nous connaissons comment gérer la bidirectionnalité, revenons à des cas plus classiques. Une relation 1-1 est utilisée pour lier deux entités uniques indissociables. Par exemple, un corps ne possède qu'un seul coeur, ou une personne n'a qu'une seule carte d'identité. Une relation 1-1 unidirectionnelle entre deux entités a une référence de cardinalité 1 qui ne peut être atteinte que dans une seule direction.

Reprenons l'exemple d'un client et de son adresse en supposant qu'il ne dispose que d'une seule adresse (cardinalité 1). Il faut pouvoir naviguer du client (la source) vers l'adresse (la cible) pour connaître où habite le client. Dans ce cas de figure, nous n'avons pas besoin de faire le trajet inverse (cela suppose que nous n'avons pas besoin de retrouver quel client habite à une adresse donnée).

En Java, ceci signifie que la classe Client possède tout simplement un attribut adresse.
.

Client.java
@Entity
public class Client implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    private Adresse adresse;
...
}  
Adresse.java
@Entity
public class Adresse {
    @Id
    @GeneratedValue
    private long id;
    private String rue;
    private String ville;
    private int codepostal;
    private String pays;
...
}

Comme vous pouvez le constater à la lecture de ces différents codes sources, ces deux entités utilisent un nombre minimal d'annotations - @Entity plus @Id et @GeneratedValue pour la clé primaire, c'est tout ...

Grâce à la configuration par exception, le fournisseur de persistance les associera à deux tables et ajoutera une clé étrangère pour représenter la relation (allant du client à l'adresse). Cette relation 1-1 est déclenchée par le fait qu'Adresse est déclarée comme une entité et qu'elle est incluse dans l'entité Client sous forme d'un attribut. Nous n'avons donc pas besoin d'annotation @OneToOne parce que le comportement par défaut suffit.

Comme vous le savez, si un attribut n'est pas annoté, JPA lui applique les règles d'association par défaut. Ainsi, la colonne de la clé étrangère s'appelera donc ADRESSE_ID, qui est la concaténation du nom de l'attribut (adresse, ici), d'un underscore et du nom de la clé primaire de la table de destination (ici, la colonne ID de la table ADRESSE). Notez également que, dans le langage de définition des données, la colonne ADRESSE_ID peut, par défaut, recevoir des valeurs NULL : par défaut, une relation 1-1 est donc associée à zéro (NULL) ou une valeur :

comme nous l'avons découvert dans la rubrique sur la bidirectionnalité, il existe deux annotations permettant d'adapter l'association d'une relation 1-1.
  1. La première est @OneToOne (car la cardinalité de la relation est un) : elle permet de modifier certains attributs de la relation elle-même, comme la façon dont elle sera parcourue :
    API de l'annotation @OneToOne
    package javax.persistence;
    
    import java.lang.annotation.*;
    
    @Target(value = {ElementType.METHOD, ElementType.FIELD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface OneToOne {
    
        public Class targetEntity() default void.class;
        public CascadeType[] cascade() default {};
        public FetchType fetch() default FetchType.EAGER;
        public boolean optional() default true;
        public String mappedBy() default "";
        public boolean orphanRemoval() default false;
    }
  2. L'autre annotation s'appelle @JoinColumn (son API ressemble beaucoup à celle de @Column). Elle permet de personnaliser la colonne de jointure, c'est-à-dire la clé étrangère du propriétaire de la relation :
    API de l'annotation @JoinColumn
    package javax.persistence;
    
    import java.lang.annotation.*;
    
    @Target(value = {ElementType.METHOD, ElementType.FIELD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface JoinColumn {
    
        public String name() default "";
        public String referencedColumnName() default "";
        public boolean unique() default false;
        public boolean nullable() default true;
        public boolean insertable() default true;
        public boolean updatable() default true;
        public String columnDefinition() default "";
        public String table() default "";
    }

Le codage suivant présente un exemple d'utilisation de ces deux méthodes :

Client.java
@Entity
public class Client implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    @OneToOne(cascade=CascadeType.PERSIST)
@JoinColumn(name="AdresseId", nullable=false) private Adresse adresse; ... }

Le code précédent, grâce à l'annotation @JoinColumn permet de renommer la colonne de la clé étrangère en ADRESSEID et rendre la relation obligatoire en refusant les valeurs NULL. L'annotation @OneToOne quant à elle, demande au fournisseur de persistance d'enregistrer automatiquement l'adresse lorsque nous rendons persistant un client.


Relation Un à Un unidirectionnelle : Mêmes valeurs pour les clés primaires : @PrimaryKeyJoinColumn

Dans certain cas très particulier, il peut être possible de faire en sorte que les deux tables possèdent la même clé primaire pour identifier leurs propres éléments respectifs. Cela permet d'éviter de manipuler une clé étrangère, et donc de s'affranchir d'une colonne supplémentaire. Vous pouvez alors préciser ce choix au travers de l'annotation @PrimaryKeyJoinColumn (jointure par clé primaire).

Revenons un instant sur le projet concernant l'archivage des photos. Il peut être judicieux de rajouter des fonctionnalités et de prévoir des retouches possibles sur un certain nombre de photos tout en conservant les originales. La photo renvoyée au client serait alors la version retouchée dont le traitement serait exécutée juste au moment de la requête. Pour cela, il serait utile de prévoir une persistance qui conserverait les réglages propre à la retouche, comme l'intensité, le contraste, etc.


Photo.java
@Entity
public class Photo implements java.io.Serializable {
    @Id
    private String id;
    private int largeur;
    private int hauteur;
    private long poids;
    @OneToOne(cascade=CascadeType.PERSIST)
@PrimaryKeyJoinColumn private Retouche retouche; ... }
Retouche.java
@Entity
public class Retouche implements java.io.Serializable {
    @Id
    private String id;
    private int intensité;
    private int contraste;
...
}

J'aimerais faire quelques remarques quand à la persistance de deux entités quelque soit la stratégie de l'élaboration de chacune des tables :

  1. Nous devons systématiquement créer deux objets entités séparemment, un pour la Retouche et un pour la Photo.
  2. Dans cet exemple précis, pour ces deux objets, vous devez définir la clé primaire qui doit être identique.
  3. Le plus important encore, c'est que vous devez rendre persistant chacun de ces beans entités (sauf dans le cas d'une stratégie en cascade).

Attention, dans ce genre de situation, vous devez gérer vous-même l'identifiant commun. Vous ne pouvez pas demander une génération automatique de vos clés primaires à l'aide de l'annotation @GeneratedValue.

Voici la constitution des deux tables au niveau de la base de données associées à ces deux entités respectives :


Relation Un à Zéro (valeur optionnelle) : @OneToOne

Une relation Un à Zéro est implémentée de la même manière qu'une relation Un à Un. La seule différence réside dans le fait qu'elle est optionnelle.

En reprenant notre exemple, une photo n'est pas obligée d'avoir une retouche si le cliché est de bonne facture.

Pour transformer une relation Un à Un en une relation Un à Zéro, il suffit d'autoriser la valeur null dans la colonne implémentant la relation. Pour cela, il est nécessaire de positionner l'attribut nullable de l'annotation @JoinColumn à true. En réalité, c'est la valeur par défaut. Nous n'avons donc pas besoin de le spécifier.

Attention, l'annotation @PrimaryKeyJoinColumn ne possède pas un tel attribut, ce qui sous-entends que l'aspect facultatif ne peut pas être prise en compte et que la seule relation possible dans ce cas là, ne peut être qu'une relation de type 1-1. Finalement, si vous désirez intégrer le caractère facultatif d'un élément, vous êtes obligé de passer par une clé étrangère.

Photo.java
@Entity
public class Photo implements java.io.Serializable {
    @Id
    private String id;
    private int largeur;
    private int hauteur;
    private long poids;
    @OneToOne(cascade=CascadeType.PERSIST)
@JoinColumn(nullable=true) private Retouche retouche; ... }
Retouche.java
@Entity
public class Retouche implements java.io.Serializable {
    @Id  @GeneratedValue
    private long id;
    private int intensité;
    private int contraste;
...
}

Attention, dans l'entité Retouche, il est préférable de prendre un identifiant de type long avec une génération automatique.
.

Voici la constitution des deux tables dans la base de données associées à ces deux entités respectives :


Relation Un à Plusieurs unidirectionnelle : @OneToMany ou @ManyToOne

Dans une relation 1-N, l'entité source référence un ensemble d'entités cibles. Une commande, par exemple, est composée d'un ensemble d'articles. cela peut être évoqué au moyen de l'annotation @OneToMany. Inversement, un article pourrait faire référence à la commande dont elle fait partie à l'aide, cette fois-ci, d'une annotation @ManyToOne.

Dans le diagramme UML ci-dessous, l'entité Commande est l'extrémité "One" (la source) de la relation (composition), et l'entité Article est son extrémité "Many" (la cible).

La cardinalité est multiple et la navigation ne se fait que dans le sens Commande vers Article. En Java, cette multiplicité est décrite par les interfaces Collection, List et Set du paquetage java.util.

Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    private List<Article> articles = new ArrayList<Article>();
...
}  
Article.java
@Entity
public class Article implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String intitulé;
    private double prixUnitaire;
    private int quantité;
...
}

Comme vous pouvez le constater à la lecture du code relatif à l'entité Commande, nous n'avons placé aucune annotation particulière sur la collection d'articles. Comme toujours, cela veut dire que nous restons dans une configuration par exception.

  1. Ainsi, lorsqu'une entité possède un attribut qui est une collection d'une autre entité, le système déclenche automatiquement une association de type OneToMany par défaut.
  2. Toujours par défaut, les relations 1-N unidirectionnelles utilisent une table de jointure pour représenter la relation. Cette table est une liste de couples de clés étrangères : une clé fait référence à la table COMMANDE avec le même type que sa clé primaire, l'autre désigne la table ARTICLE. Cette table de jointure s'appelle par défaut COMMANDE_ARTICLE.

Comme précédemment, si vous n'aimez pas le nom de la table de jointure ou celui des clés étrangères, ou si vous désirez associer la relation à une table existante, vous pouvez vous servir des annotations JPA pour redéfinir ces valeurs par défaut.

  1. Le nom d'une colonne de jointure est formée par défaut par la concaténation du nom de l'entité, de l'underscore et du nom de la clé primaire désignée par la clé étrangère (COMMANDE_ID et ARTICLE_ID).
  2. Comme l'annotation @JoinColumn permet de modifier le nom des colonnes de clés étrangères, @JoinTable fait de même pour la table de jointure.
  3. Vous pouvez également utiliser l'annotation @OneToMany, qui, comme @OneToOne, permet de personnaliser la relation elle-même.
    API de l'annotation @JoinTable
    package javax.persistence;
    
    import java.lang.annotation.*;
    
    @Target(value = {ElementType.METHOD, ElementType.FIELD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface JoinTable {
    
        public String name() default "";
        public String catalog() default "";
        public String schema() default "";
        public JoinColumn[ ] joinColumns() default { };
        public JoinColumn[ ] inverseJoinColumns() default { };
        public UniqueConstraint[ ] uniqueConstraints() default  { };
    }

    Dans l'API de l'annotation @JoinTable, vous pouvez remarquer deux attributs de type @JoinColumn : joinColumns et inverseJoinColumns. Ils permettent de différencier l'extrémité propriétaire de la relation et son extrémité opposée. L'extrémité propriétaire de la relation est décrite dans l'élément joinColumns et, dans notre exemple, désigne la table COMMANDE. L'extrémité opposée, la cible de la relation, est précisée par l'élément inverseJoinColumns et désigne la table ARTICLE.

    Commande.java
    @Entity
    public class Commande implements java.io.Serializable {
        @Id
        @GeneratedValue
        private long id;
        @Temporal(TemporalType.TIMESTAMP)
        private Date dateCréation;
        @JoinTable(name="ArticlesCommandés",
    joinColumns=@JoinColumn(name="NuméroCommande"),
    inverseJoinColumns=@JoinColumn(name="Article")) private List<Article> articles = new ArrayList<Article>(); ... }
La règle par défaut pour une relation 1-N unidirectionnelle consiste à utiliser une table de jointure. Il est souvent plus judicieux de faire en sorte d'utiliser des clés étrangères par l'intermédiaire des colonnes de jointure. Pour cela, par exemple, l'entité COMMANDE doit explicitement utiliser une annotation @JoinColumn en lieu et place de l'annotation @JoinTable :
Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @JoinColumn
    private List<Article> articles = new ArrayList<Article>();
...
}  

Le nom d'une colonne de jointure est formée par défaut par la concaténation du nom de l'attribut associé à l'annotation @JoinColumn, de l'underscore et du nom de la clé primaire désignée par la clé étrangère (ARTICLES_ID). Il est tout à fait possible de paramétrer le nom de cette colonne en renseignant l'attribut name de l'annotation @JoinColumn :

Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @JoinColumn(name="NuméroCommande")
    private List<Article> articles = new ArrayList<Article>();
...
}  

Attention, le mode de parcours par défaut dans une relation 1-N est le mode paresseux. Si vous désirez systématiquement avoir à votre possession l'ensemble des articles constituant une commande en spécifiant le mode EAGER dans l'annotation @OneToMany. Comme précédemment, vous pouvez également en profiter pour imposer une persistance automatique de l'ensemble des articles au moment de la persistance de la commande.

Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinColumn(name="NuméroCommande")
    private List<Article> articles = new ArrayList<Article>();
...
}  

Relation Plusieurs à Plusieurs bidirectionnelle : @ManyToMany

Une relation N-M bidirectionnelle intervient lorsqu'un objet source fait référence à plusieurs cibles et qu'une cible fait elle-même référence à plusieurs sources. Un album CD, par exemple, est créé par plusieurs artistes, et un même artiste peut apparaître sur plusieurs albums.

  1. Côté Java, chaque entité contiendra donc une collection d'entités cibles.
  2. En terme de base de données relationnelle, la seule façon de représenter une relation N-M consiste à utiliser, bien entendu, une table de jointure (une colonne de jointure ne peut pas convenir) ; comme nous l'avons évoqué précédemment, il est également nécessaire de définir explicitement le propriétaire d'une relation bidirectionnelle à l'aide de l'élément mappedBy.
En supposant que l'entité Artiste est propriétaire de la relation, ceci implique que CD est l'extrémité opposée et qu'elle doit utiliser l'élément mappedBy de son annotation @ManyToMany. Ici, mappedBy indique au fournisseur de persistance que présentSurCDs est le nom de l'attribut correspondant dans l'entité propriétaire de la relation :
Artiste.java
@Entity
public class Artiste implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    @ManyToMany
    private List<CD> présentSurCDs = new ArrayList<CD>();
...
}  
CD.java
@Entity
public class CD implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    @ManyToMany(mappedBy="présentSurCds")
    private List<Artiste> artistes = new ArrayList<Artiste>();
...
}

La structure de la base de données obtenue est la suivante :

Dans une relation N-M et 1-1 bidirectionnelle, chaque entité peut, en fait, être considérée comme la propriétaire de la relation. Quoi qu'il en soit, l'autre extrémité doit inclure l'élément mappedBy : dans le cas contraire, le fournisseur considérera que les deux extrémités sont propriétaires et traitera cette relation comme deux relations 1-N unidirectionnelles distinctes. Ici, cela produirait donc quatre tables : ARTISTE et CD et deux tables de jointures, ARTISTE_CD et CD_ARTISTE. Vous comprenez bien que nous ne pouvons pas non plus utiliser un élément mappedBy des deux côtés de la relation.

Chargement des relations

Toutes les annotations que nous venons de découvrir (@OneToOne, @OneToMany, @ManyToOne et @ManyToMany) définissent un attribut de chargement qui précise que les objets associés doivent être chargés immédiatement (chargement "glouton") ou plus tard (chargement "paresseux") et qui influe donc sur les performances.

Vous pouvez optimiser les performances en chargeant les données de la base lors de la première lecture de l'entité (glouton) ou uniquement lorsqu'elle est utilisée (paresseux).

Chaque annotation possède une stratégie de chargement par défaut que nous devons connaître et qu'il est souhaitable de pouvoir modifier si cela ne convient pas. Nous pouvons donc changer ce comportement au travers du paramètre fetch qui peut prendre les deux valeurs : LAZY (paresseux) ou EAGER (glouton).

Annotation Stratégie de chargement par défaut
@OneToOne EAGER
@ManyToOne EAGER
@OneToMany LAZY
@ManyToMany LAZY

A titre d'exemple, prenons deux cas extrèmes :

  1. Supposons que nous ayons quatre entités toutes reliées les unes aux autres avec des cardinalités différentes (1-1, 1-N). Dans le premier cas, elles ont toutes des relations "gloutonnes", ce qui signifie que, dès que nous chargeons une entité représentée par la classe Classe1 (par une recherche par l'identifiant ou par une requête spécifique), tous les objets qui en dépendent sont automatiquement chargés en mémoire, ce qui peut avoir certaines répercussions sur votre système.

  2. Dans le scénario opposé, toutes les relations utilisent un chargement paresseux. Lorsque nous chargeons une entité représentée par Classe1, rien d'autre n'est placé en mémoire (sauf les attributs directs de la classe, bien sûr). Il faut explicitement accéder à Classe2 (via la méthode getter correspondante, par exemple) pour que le fournisseur de persistance charge les données à partir de la base, etc. Ainsi, pour manipuler le graphe complet des objets, il est nécessaire d'appeler explicitement chaque entité en particulier.

Ne pensez pas qu'EAGER est le mal et LAZY, le bien.
  1. EAGER placera toutes les données en mémoire à l'aide d'un petit nombre d'accès à la base (le fournisseur de persistance utilisera sûrement des jointures pour extraire ces données).
  2. Avec LAZY, vous ne risquez plus de remplir la mémoire puisque vous contrôlez les objets qui seront chargés, mais vous devrez faire plus d'accès à la base à chaque fois.

Le paramètre fetch est finalement très important car, mal utilisé, il peut pénaliser les performances.
.

Lorsque, par exemple, vous chargez une commande dans votre application, vous avez toujours besoin d'accéder aux différents articles de cette commande. Il est donc plutôt avantageux de changer le mode de chargement par défaut de l'annotation @OneToMany en EAGER.

Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinColumn(name="NuméroCommande")
    private List<Article> articles = new ArrayList<Article>();
...
}  

Tri des relations

Avec les relations 1-N, les entités gèrent des collections d'objets. Du point de vue de Java, ces collections ne sont généralement pas triées et les bases de données relationnelles ne garantissent pas non plus d'ordre sur leurs tables. Si vous désirez obtenir une liste triée, vous devez donc soit trier la collection dans votre programme, soit utiliser une requête JPQL avec une clause Order By. Pour le tri des relations, JPA dispose de mécanismes plus simples reposant sur les annotations.

@OrderBy

L'annotation @OrderBy permet de réaliser un tri dynamique à la volée : les éléments de la collection seront ainsi triés lors de leur récupération à partir de la base de données.

En reprenant l'exemple de l'application d'entreprise sur les commandes, nous pouvons rajouter une entité Client qui possède événtuellement plusieurs commandes qui seraient souhaitables qu'elles apparaissent chronologiquement.

Dans l'entité Client, nous trions la liste des commandes par ordre décroissant des dates de création de chaque commande, en combinant l'annotation @OrderBy avec l'annotation @OneToMany.

Client.java
@Entity
public class Client implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    @OneToMany(fetch=FetchType.EAGER)
@OrderBy("dateCréation desc") private Commande commandes; ... }
Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinColumn(name="NuméroCommande")
    private List<Article> articles = new ArrayList<Article>();
...
}  

L'annotation @OrderBy prend en paramètre les noms des attributs sur lesquels portera le tri (dateCréation, ici) et la méthode (représentée par la chaîne ASC ou DESC, pour signifier, respectivement, un tri croissant ou décroissant). Vous pouvez utiliser plusieurs paires attribut/méthode en les séparant par des virgules : immaginons, par exemple, qu'une entité supérieure ait besoin de trier les clients ; d'abord suivant leurs noms et ensuite suivant leurs prénoms. Voici ce que nous pourrions placer sur un attribut représentant une collection de clients.

@OrderBy("nom asc", "prénom asc")

Cette annotation n'a aucun impact sur l'association dans la base de données - le fournisseur de persistance est simplement informé qu'il doit utiliser une clause order by lorsque la collection est récupérée.

@OrderColumn

JPA 1.0 supportait le tri dynamique avec l'annotation @OrderBy mais ne permettait pas de maintenir un ordre persistant. JPA 2.0 règle ce problème à l'aide d'une nouvelle annotation, @OrderColumn, qui informe le fournisseur de persistance qu'il doit gérer la liste triée à l'aide d'une colonne séparée contenant un index.

L'API de @OrderColumn est semblable à celle de @Column :

API de l'annotation @OrderColumn
package javax.persistence;

import java.lang.annotation.*;

@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface OrderColumn {

    public String name() default "";
    public boolean nullable() default true;
    public boolean insertable() default true;
    public boolean updatable() default true;
    public String columnDefinition() default "";
    public boolean contiguous() default true;
    public int base() default 0;
    public String table() default "";
}
Reprenons l'exemple mise en place précédemment avec la gestion des clients avec leurs commandes en le modifiant légèrement.

Dans l'entité Client, nous pouvons alors annoter la relation avec les commandes avec @OrderColumn afin que cette fois-ci le fournisseur de persistance associe l'entité Client à une table contenant une colonne supplémentaire pour stocker l'ordre.

Client.java
@Entity
public class Client implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String nom;
    private String prénom;
    private String email;
    private String téléphone;
    @OneToMany(fetch=FetchType.EAGER)
@OrderColumn private Commande commandes; ... }
Commande.java
@Entity
public class Commande implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateCréation;
    @OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinColumn(name="NuméroCommande")
    private List<Article> articles = new ArrayList<Article>();
...
}  

L'annotation @OrderColumn prend éventuellement en paramètre le nom de la nouvelle colonne. Si ce nom n'est pas redéfini, cette colonne porte alors le nom formé de la concaténation de l'entité référencée et la chaîne _ORDER (COMMANDES_ORDER, ici). Le type de cette colonne doit absolument être numérique.

Cette annotation a des conséquences sur les performances car le fournisseur de persistance doit maintenant également gérer les modifications de l'index. Il doit maintenir le tri après chaque insertion, suppression ou réordonnacement. Si les données sont insérées au milieu d'une liste triée, le fournisseur devra retrier tout l'index.

Pour réaliser un tri, vous avez donc le choix entre ces deux annotations @OrderColumn et @OrderBy. Pour ma part, je préfère largement cette dernière puisqu'elle prend moins de ressources au niveau de la base de données. Par ailleurs, avec @OrderColumn, vous ne pouvait pas spécifier le critère d'ordonancement. Attention, quelque soit votre choix, ces deux annotations ne peuvent pas être utilisées en même temps.

 

Choix du chapitre L'héritage

L'héritage est une notion essentielle et fondamentale de la programmation objet qui se retrouve dans la quasi-totalité de tout les types d'applications. En programmation orientée objet, les développeurs réutilisent fréquemment le code existant en héritant des attributs et des comportements de classes existantes. Toutefois, la représentation relationnelle d'un héritage d'objets est tout à fait particulier au niveau des tables de la base de données.

Nous venons de voir que les relations entre entités ont des équivalents directs dans les bases de données. Ce n'est pas le cas avec l'héritage car ce concept est totalement inconnu du modèle relationnel. Il impose donc plusieurs contorsions pour être traduit dans un SGBDR.

Pour représenter un modèle hiérarchique dans un modèle relationnel plat, JPA propose alors trois stratégies possibles :

  1. Une seule table unique pour l'ensemble de la hiérarchie des classes. L'ensemble des attributs de toute la hiérarchie des entités est mis à plat et regroupé dans une seule table (il s'agit d'ailleur de la startégie par défaut).
  2. Une table pour chaque classe concrète. Chaque entité concrète de la hiérarchie est associée à une table.
  3. Jointure entre sous-classes. Dans cette approche, chaque entité de la hiérarchie, concrète ou abstraite, est associée à sa propre table. Ainsi, nous obtenons dans ce cas là une séparation des attributs spécifiques de la classe fille par rapport à ceux de la classe parente. Il existe alors, une table pour chaque classe fille, plus une table pour la classe parente. Une jonction est alors nécessaire pour instancier la classe fille.

Le support de la stratégie une table pour chaque classe concrète est encore facultatif avec JPA 2.0. Les applications portables doivent donc l'éviter tant que ce support n'a pas été officiellement déclaré comme obligatoire dans toutes les implémentations.

Tirant parti de la simplicité d'utilisation des annotations, JP 2.0 fournit un support déclaratif pour définir et traduire les hiérérachies d'héritage comprenant des entités concrètes, des entités abstraites, des classes traduites et des classes transitoires.

L'annotation @Inheritance s'applique à une entité racine pour imposer une stratégie d'héritage à cette classe et à ses classes filles. JPA traduit aussi la notion objet de redéfinition qui permet aux attributs de la classe fille d'être redéfinis dans les classes filles.

Stratégies d'héritage

JPA propose donc trois stratégies pour traduire l'héritage. Lorsqu'il existe une hiérarchie d'entités, sa racine est toujours une entité qui peut définir la stratégie d'héritage à l'aide de l'annotation @Inheritance. Si elle ne le fait pas, c'est la stratégie par défaut qui est appliquée, consistant à créer une seule table pout toute la hiérarchie.

Attention : une seule clé primaire doit être définie dans une hiérarchie. Elle est généralement déclarée dans la classe ancêtre afin que toutes les classes filles puissent en bénéficier.

Pour chaque cas, nous étudierons les annotations utilisées et la structure relationnelle engendrée. Ces annotations sont primordiales uniquement pour choisir l'organisation souhaitée au niveau de la base de données. Par contre, lorsque nous utilisons les beans entités, côté codage Java, nous restons dans une écriture et une utilisation tout à fait classique.

Afin d'expliquer chacune de ces stratégies, nous étudierons comment traduire les entités CD et Livre, qui héritent toutes les deux de l'entité Document.

Document est l'entité racine ; elle possède un identifiant unique qui servira de clé primaire pour l'ensemble de la hiérarchie. La clé primaire sera ainsi automatiquement récupérée par le mécanisme d'héritage. Chacune de ses classes filles ajoute également ses propres attributs supplémentaires comme genre pour l'entité Livre et duréeTotale pour l'entité CD.

Stratégie utilisant une seule table

Il s'agit de la stratégie de traduction de l'héritage par défaut, dans laquelle toutes les entités de la hiérarchie sont mappées dans une même et unique table.

Il n'est donc pas nécessaire d'utiliser l'annotation @Inheritance sur l'entité racine, comme le montre le code suivant :
Document.java
@Entity
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
...
}  

Document est la classe parente des entités Livre et CD. Ces entités héritent des attributs de Document ainsi que de la stratégie d'héritage par défaut : elles n'ont donc pas besoin elles-même d'utiliser l'annotation @Inheritance.

Livre.java
@Entity
public class Livre extends Document {
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
}
CD.java
@Entity
public class CD extends Document {
    private double duréeTotale;
    @ElementCollection
    private List<String> pistes = new ArrayList<String>();
...
}

Sans l'héritage, ces trois entités seraient traduites en trois tables distinctes. Avec la stratégie de traduction de l'héritage par une seule table, elles finiront toutes dans la table portant par défaut le nom de la classe racine : DOCUMENT.

Comme vous pouvez le constater, la table DOCUMENT rassemble tous les attributs des entités Document, Livre et CD. Cependant, elle contient une colonne supplémentaire qui n'est liée à aucun des attributs des entités : la colonne discriminante, DTYPE.

La table DOCUMENT sera remplie de livres et de CD. Lorsqu'il accède aux données, le fournisseur de persistance doit savoir à quelle entité appartient chaque ligne afin d'instancier la classe d'objet appropriée (Livre ou CD) : la colonne discriminante est donc là pour préciser explicitement le type de chaque colonne.

  1. Cette stratégie fournit un gain de performance important, car aucune jointure n'est réalisée.
  2. En même temps, comme vous pouvez le constater, la stratégie avec une table possède quelques défauts. Nous voyons, par exemple, que toutes les colonnes ne sont pas nécessairement utiles à toutes les entités (cases violettes). Suivant le cas, nous pouvons obtenir de gros gaspillages. Lorsque nous enregistrons, par exemple, un disque dans la base de données, les colonnes GENRE, NOMBREPAGES et ILLUSTRATION deviennent totalement inutiles.

La colonne discriminante, s'appelle DTYPE, par défaut, elle est de type String (traduit en VARCHAR avec une taille par défaut de 31) et elle contient tout simplement le nom de l'entité (ce qui offre une grande lisibilité au niveau de la table).

Si ce comportement par défaut ne vous convient pas, il est tout-à-fait possible d'utiliser l'annotation @DiscriminatorColumn pour modifier le nom et le type de cette colonne. Les attributs de cette annotation sont alors :

  1. name : nom de la colonne de discrimination qui sera utile pour indiquer le nom de la classe utilisée dans la hiérarchie.
  2. discriminatorType : classe du discriminateur à utiliser via l'énumération DiscriminatorType. Celle-ci possède les attributs suivants :
  3. length : taille de la colonne pour les discriminateurs à base de chaîne de caractères.
  4. columnDefinition : fragment SQL à utiliser pour la déclaration de la colonne (utilisé lors de la génération des tables par le conteneur).
Dans le code suivant, nous renommons la colonne discriminante en DOCUMENT (au lieu donc de DTYPE). Nous proposons également de changer le type de cette colonne pour qu'elle soit de type char en lieu et place du type String. Du coup, chaque entité concrète change également sa valeur discrimante par défaut et propose la lettre "L" pour indiquer qu'il s'agit d'un document de type Livre, et la lettre "C" pour indiquer qu'il s'agit d'un CD :
Document.java
@Entity
@DiscriminatorColumn(name="Document", discriminatorType=DiscriminatorType.CHAR)
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
...
}  

L'entité racine Document définit la colonne discriminante pour toute la hiérarchie à l'aide de l'annotation @DiscriminatorColumn. Pour que cela fonctionne correctement, chaque entité fille doit uniquement redéfinir sa propre valeur discriminante au moyen de l'annotation @DiscriminatorValue.

Livre.java
@Entity
@DiscriminatorValue("L")
public class Livre extends Document {
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
}
CD.java
@Entity
@DiscriminatorValue("C")
public class CD extends Document {
    private double duréeTotale;
    @ElementCollection
    private List<String> pistes = new ArrayList<String>();
...
}

Voici le résultat obtenu par rapport au choix que nous venons de réaliser :

Conclusion sur cette stratégie
  1. Cette stratégie de table unique est donc la stratégie par défaut ; c'est la plus facile à comprendre et elle fonctionne très bien lorsque la hiérérachie est relativement simple et stable. En revanche, comme nous l'avons déjà évoqué, elle possède quelques défauts : notamment, l'ajout de nouvelles entités dans la hiérarchie ou d'attributs dans des entités existantes implique d'ajouter des colonnes à la table, de migrer les données et de modifier les index.
  2. Cette stratégie exige également que les colonnes des entités filles puissent recevoir la valeur NULL : si GENRE de l'entité Livre n'était pas nullable, par exemple, nous serions dans l'incapacité de pouvoir insérer un CD puisque ce dernier ne possède pas cette rubrique.

Pour terminer, il est également possible de mapper cette colonne DOCUMENT sur un attribut (typeDocument) afin de pouvoir contrôler par programme les différents types d'objets enregistrés, à condition toutefois de bien préciser insertable=false et updatable=false :

Document.java
@Entity
@DiscriminatorColumn(name="Document", discriminatorType=DiscriminatorType.CHAR)
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
    @Column(name="Document", insertable=false, updatable=false)
    private char typeDocument;
...
}  
Stratégie par jointure

Dans cette stratégie, chaque entité de la hiérarchie est associée à sa propre table. Ici, la classe ancêtre des entités est représentée par une table, avec, là aussi, une colonne discriminante.

Ainsi, chaque sous-classe est représentée par une table distincte contenant ses propres attributs (non hérités de la classe racine) et une clé primaire qui fait référence à celle de la table racine. Les classes filles n'ont, en revanche, pas de colonne discriminante.

Pour implémenter une stratégie par jointure, nous sommes cette fois-ci obligé d'utiliser l'annotation @Inheritance sur l'entité racine en spécifiant le bon paramètre au niveau de l'attribut.

Le seul attribut de cette annotation est strategy. Il permet de spécifier le type de stratégie à utiliser pour construire la ou les tables à mettre en oeuvre, via l'énumération InheritanceType.

Pour indiquer que nous désirons une stratégie par jointure, nous prenons le paramètre InheritanceType.JOINED dans l'annotation @Inheritance. Par contre, les classes filles n'ont pas besoin d'autre annotation que @Entity (sauf éventuellement l'annotation propre à la valeur du discriminant). A ce sujet, nous pouvons là aussi utiliser l'annotation @DiscriminatorColumn dans l'entité racine afin de personnaliser la colonne discriminante.

Document.java
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="Document", discriminatorType=DiscriminatorType.CHAR)
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
...
}  
Livre.java
@Entity
@DiscriminatorValue("L")
public class Livre extends Document {
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
}
CD.java
@Entity
@DiscriminatorValue("C")
public class CD extends Document {
    private double duréeTotale;
    @ElementCollection
    private List<String> pistes = new ArrayList<String>();
...
}

Du point de vue du développeur, la stratégie par jointure est naturelle car chaque entité, qu'elle soit abstraite ou concrète, sera traduite dans une table distincte.

  1. L'avantage de cette stratégie est d'avoir un modèle relationnel clair. C'est en quelque sorte le modèle idéal pour la base de données (pas de perte mémoire car tous les champs sont utilisés). La stratégie par jointure est également intuitive et proche de ce que nous connaissons du mécanisme de l'héritage. C'est un moyen de fournir un bon support au polymorphisme.
  2. En contre parti, elle a un impact sur les performances des requêtes. En effet, pour recréer une simple instance d'une sous-classe, il faut joindre sa table à celle de la classe racine. Plus la hiérarchie est profonde, plus il faudra de jointures pour recréer l'entité feuille. Dans le cas de hiérarchies importantes (grande profondeur de l'héritage) cela peut entraîner de très mauvaises performances.
Stratégie une table par classe

Dans cette stratégie (une table par classe concrête), chaque entité est traduite dans sa propre table, comme avec la stratégie par jointure. La différence est qu'ici tous les attributs de l'entité racine seront également traduits en colonnes de la table associée à l'entité fille. Cela signifie que toutes les propriétés de la classe (avec celles récupérées par héritage) sont incluses dans la table liée à cette entité. Ici, nous n'avons pas de table partagée, pas de colonne partagée ni de colonne discriminante. La seule exigence est que toutes les tables de la hiérarchie doivent partager la même clé primaire.

Pour spécifier cette stratégie, il faut juste choisir cette fois-ci InheritanceType.TABLE_PER_CLASS dans l'annotation @Inheritance. Encore une fois, les classes filles héritent simplement sans avoir besoin de préciser de spécifications supplémentaires autres que @Entity.
Document.java
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
...
}  
Livre.java
@Entity
public class Livre extends Document {
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
}
CD.java
@Entity
public class CD extends Document {
    private double duréeTotale;
    @ElementCollection
    private List<String> pistes = new ArrayList<String>();
...
}

Voici les tables obtenues. Vous remarquez que LIVRE et CD dupliquent les colonnes ID, TITRE, PRIX, DESCRIPTION et AUTEUR_ID de la table DOCUMENT et que les tables ne sont plus liées entre elles.

  1. Cette stratégie est très performante lorsque nous interrogeons des instances d'une seule entité car nous nous retrouvons dans un scénario comparable à l'utilisation de la stratégie à une seule table - la requête ne porte que sur une seule table.
  2. L'inconvénient est que les requêtes polymorphiques à travers une hiérarchie de classes sont plus coûteuses que les deux autres stratégies. Par exemple, pour rechercher tous les articles, que ce soit des livres ou des CD, il faut alors interroger toutes les tables des sous-classes avec une opération utilisant une UNION, ce qui est coûteux lorsque nous avons beaucoup de données. L'autre inconvénient est la duplication des colonnes dans chacune des tables des beans entités.

Type de classes dans une hiérarchie d'héritage

L'exemple utilisé pour expliquer les stratégies de traduction de l'héritage n'utilise que des entités, mais les entités n'héritent pas que d'entités. Une hiérarchie de classes peut contenir un mélange d'entités, de classes qui ne sont pas des entités (classes transitoires), d'entités abstraites et de superclasses déjà traduites. Hériter de ces différents types de classes a un impact sur la traduction de la hiérarchie.

Entités abstraites

Nous l'avons vu au travers de nos différents exemples, une classe abstraite comme Document peut tout-à-fait être désignée comme une entité. Elle ne diffère d'une entité concrète que parce qu'elle ne peut pas être directement instanciée, mais elle fournit une structure de données que partagerons toutes ses entités filles (Livre et CD) et elle respecte les stratégies de traduction d'héritage.

Du point de vue du fournisseur de persistance, la seule différence se situe du côté de Java, pas dans la correspondnce avec les tables.
.

Non-entités

Les non-entités sont également appelées classes transitoires. Une entité peut hériter d'une non-entité ou peut être étendue par une non-entité. La modélisation objet et l'héritage permettent de partager les états et les comportements ; dans une hiérachie de classes, les non-entités peuvent donc servir à fournir une structure de données commune à leurs entités filles.

L'état d'une superclasse non entité n'est pas persistant car il n'est pas géré par le fournisseur de persistance (n'oubliez pas que la condition pour qu'une classe le soit est la présence de l'annotation @Entity).

Dans le code source ci-dessous, Document devient une simple classe transitoire.
Document.java
public abstract class Document implements java.io.Serializable {
    private String titre;
    private double prix;
    private String description;
    private Auteur auteur;
...
}  

L'entité Livre hérite toujours de Document ; le code Java peut donc accéder aux attributs titre, prix, description et auteur ainsi qu'à toutes les méthodes de Document. Que cette dernière soit concrète ou abstraite n'aura aucune influence sur la traduction finale.

Livre.java
@Entity
public class Livre extends Document {
    @Id
    @GeneratedValue
    private long id;
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
} 

Livre est une entité qui hérite de Document, mais seuls les attributs de Livre seront stockés dans une table. Aucun attribut de Document n'apparaît dans la structure de la table ci-dessous. Pour qu'un Livre soit persistant, vous devez créer une instance de Livre, initialiser les attributs que vous souhaitez (titre, prix, description, etc.), mais seuls ceux de Livre (genre, nombrePages, etc.) seront stockés.

Superclasse "mapped"

JPA définit un type de classe spéciale, appelé superclasse "mapped", qui partage son état, son comportement ainsi que des informations de traduction des entités qui en héritent.

  1. Cependant, les superclasses "mapped" ne sont pas des entités, elles ne sont pas gérées par le fournisseur de persistance, n'ont aucune table qui leur soit associée et ne peuvent pas être interrogées ni faire partie d'une relation.
  2. En revanche, elles peuvent fournir des propriétés de persistance aux entités qui en héritent.
  3. Les superclasses "mapped" ressemblent aux classes intégrables, sauf qu'elles peuvent être utilisées avec l'héritage. Elle sont annotées par @MappedSuperclass.
Dans le code source ci-dessous, la classe racine Document est annotée par @MappedSuperclass, pas par @Entity. Elle définit une stratégie de traduction de l'héritage (JOINED) et annote certains de ses attributs avec @Column. Cependant, les superclasses "mapped" n'étant pas associées à des tables, l'annotation @Table n'est pas autorisée.
Document.java
@MappedSuperclass
@Inheritance(strategy=InheritanceType.JOINED)
public abstract class Document implements java.io.Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Column(length=50, nullable=false)
    private String titre;
    private double prix;
    @Column(length=2000)
    private String description;
...
}

L'entité Livre hérite toujours de Document ; le code Java peut donc accéder aux attributs titre, prix, description et auteur ainsi qu'à toutes les méthodes de Document. Que cette dernière soit concrète ou abstraite n'aura aucune influence sur la traduction finale.

Livre.java
@Entity
public class Livre extends Document {
    private String genre;
    private int nombrePages;
    private boolean illustration;
...
} 

Livre est une entité qui hérite encore de Document. Cette hiérarchie sera traduite en une seule table. Document n'est pas une entité et n'a donc aucune table associée. Les attributs de Document seront traduits en colonnes de la table LIVRE - les superclasses "mapped" partageant également leurs informations de traduction, les annotations @Column de Document seront donc également héritées.