4. Mise en relation d'entités

4.1. Introduction

Tout comme en SQL, on peut définir quatre types de relations entre entités JPA :
  • relation 1:1 : annotée par @OneToOne ;
  • relation n:1 : annotée par @ManyToOne ;
  • relation 1:p : annotée par @OneToMany ;
  • relation n:p : annotée par @ManyToMany.
Bien qu'exprimées entre des classes Java, les relations sont définies entre des entités JPA. Il n'est donc pas légal d'annoter une relation qui pointerait vers une classe qui ne serait pas une entité.

4.2. Relations unidirectionnelles et bidirectionnelles

Les relations entre entités, telles que définies en JPA peuvent être unidirectionnelles ou bidirectionnelles. Dans ce second cas, l'une des deux entités doit être maître et l'autre esclave. Dans le cas des relations 1:1 et n:p, on peut choisir le côté maître comme on le souhaite. Dans le cas des relations 1:p et n:1, l'entité du côté 1 est l'entité esclave.

4.3. Relation 1:1

Prenons comme exemple un modèle simple qui comporte des Commune et des Maire. Chaque commune possède un maire, et un maire ne peut être maire que d'une seule commune. Nous avons donc bien une relation 1:1 entre les communes et leurs maires. Supposons que dans notre exemple, ce sont les communes qui tiennent la relation entre les communes et les maires.

4.3.1. Cas unidirectionnel

Une première façon d'écrire notre modèle est la suivante :

Exemple 7. Relation 1:1 unidirectionnelle

//
 // Entité Maire
 //

 @Entity
 public  class Maire  implements Serializable {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @Column(length=40)
     private String nom ;

     // suite de la classe
}


 //
 // Entité Commune
 //

 @Entity
 public  class Commune  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @Column(length=40)
     private String nom ;

     @OneToOne
     private Maire maire ;
	
     // suite de la classe
}

La relation que nous venons d'écrire est unidirectionnelle, en ce sens que l'on peut connaître le maire d'une commune, mais rien dans la classe Maire ne nous permet de connaître la commune dont il est maire. Ce cas est le plus simple à traiter : il suffit d'annoter la relation maire avec @OneToOne. En base, les choses se passent assez simplement : une colonne maire_id va être créée dans la table Commune. Cette colonne sera une clé étrangère référençant la colonne id de la table Maire. On peut imposer le nom de cette colonne en ajoutant l'annotation @JoinColumn(name="...") sur la relation maire. L'annotation @OneToOne permet de préciser deux comportements importants dans la gestion des relations en JPA : le comportement cascade et le comportement fetch , que nous verrons à la fin de cette partie.

4.3.2. Cas bidirectionnel

L'entité esclave doit préciser un champ retour par une annotation @OneToOne et un attribut mappedBy, qui doit référencer le champ qui porte la relation côté maître. Créons par exemple un champ retour dans notre classe Maire. L'attribut mappedBy est défini sur l'entité esclave de la relation.

Exemple 8. Relation 1:1 bidirectionnelle

//
 // Entité Maire
 //

 @Entity
 public  class Maire  implements Serializable {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @Column(length=40)
     private String nom ;

     @OneToOne(mappedBy="maire")  // référence la relation dans la classe Commune
     private Commune commune ;
    
     // suite de la classe
}

Définir une relation bidirectionnelle en JPA permet de créer le jeu de clés étrangères qui permet de garantir l'intégrité référentielle de la base de données. Il appartient au code Java de fixer la valeur du champ retour afin de garantir la cohérence du modèle objet. Une relation bidirectionnelle ne se comporte pas comme deux relations unidirectionnelles. Effectivement, une relation unidirectionnelle est caractérisée par une colonne de jointure sur la table maître, et une clé étrangère de cette colonne vers la clé primaire de l'entité en relation. Deux relations unidirectionnelles créeront donc deux colonnes de jointure, dans chacune des deux tables en relation. Une relation bidirectionnelle ne crée pas cette deuxième colonne de la table de jointure, de la table esclave vers la table maître. Lorsque l'on veut lire la relation retour, à partir de l'esclave, une requête est lancée sur la base, en utilisant le caractère bidirectionnel de la relation.
Schéma pour une relation 1:1 unidirectionnelle

Figure 9. Schéma pour une relation 1:1 unidirectionnelle


4.4. Relation 1:p

Une relation 1:p est caractérisée par un champ de type Collection dans la classe maître. La classe esclave ne porte pas de relation retour. Cette relation peut être spécifiée soit par l'annotation @OneToMany ou @ManyToMany. Elle peut être unidirectionnelle ou bidirectionnelle. Dans ce second cas, le côté maître est obligatoirement le côté qui tient la relation monovaluée.

4.4.1. Cas unidirectionnel

Dans ces deux cas, JPA crée une table de jointure entre les deux tables associées aux deux entités. Cette table de jointure porte une clé étrangère vers la clé primaire de la première table, et une clé étrangère vers la clé primaire de la deuxième table. Voyons ceci sur un exemple.

Exemple 9. Relation 1:p unidirectionnelle

//
 // Entité Marin
 // 

 @Entity
 public  class Marin  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     // reste de la classe
}

 // 
 // Entité Bateau
 // 
 @Entity
 public  class Bateau  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @OneToMany
     private Collection<Marin> marins ;

     // reste de la classe
}

Voici le schéma créé par EclipseLink.
Schéma pour une relation 1:p unidirectionnelle

Figure 10. Schéma pour une relation 1:p unidirectionnelle


On peut ici se poser une question : pourquoi la spécification JPA prévoit-elle la création d'une table de jointure alors que la relation est de type 1:p ? La réponse est simple : parce qu'aucune information n'existe sur le champ retour, et que JPA n'a donc aucune information sur la colonne de jointure à utiliser dans la table destination. Dans le cas 1:p bidirectionnel, nous verrons que JPA ne crée pas cette table de jointure. Nous avons écrit que l'on pouvait utiliser l'annotation @OneToMany ou @ManyToMany pour spécifier cette relation. Cela dit, il faut noter que les deux annotations ne sont pas équivalente. Dans le premier cas, JPA ajoute une contrainte d'unicité sur la clé étrangère de la table de jointure vers la table Marin. C'est bien ce que nous voulons ici : un même marin ne peut pas appartenir à plusieurs équipages à la fois. Il faut toutefois bien avoir présent à l'esprit cette contrainte : une relation de type 1:p est parfois une relation @ManyToMany.

4.4.2. Cas bidirectionnel

Une relation 1:p bidirectionnelle doit correspondre à une relation p:1 dans la classe destination de la relation. Comme pour le cas des relations 1:1, le caractère bidirectionnel d'une relation 1:p est marqué en définissant l'attribut mappedBy sur la relation. JPA nous pose une contrainte ici : l'attribut mappedBy est défini pour l'annotation @OneToMany, mais pas pour l'annotation @ManyToOne. Or, comme nous l'avons vu dans le cas de l'annotation @OneToOne, mappedBy doit être précisé sur le côté esclave d'une relation. Dans le cas d'une relation 1:p bidirectionnelle, JPA ne nous laisse donc pas le choix de l'entité maître et de l'entité esclave.

Exemple 10. Relation 1:p bidirectionnelle

//
 // Entité Marin
 // 

 @Entity
 public  class Marin  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;
    
     @ManyToOne
     private Bateau bateau ;

     // reste de la classe
}

 // 
 // Entité Bateau
 // 
 @Entity
 public  class Bateau  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @OneToMany(mappedBy="bateau")
     private Collection<Marin> marins ;

     // reste de la classe
}

Examinons le schéma de base généré par JPA.
Schéma pour une relation 1:p bidirectionnelle

Figure 11. Schéma pour une relation 1:p bidirectionnelle


JPA a créé une colonne de jointure dans la table de l'entité cible (ici Marin), de façon à établir la jointure. Il a de plus créé une clé étrangère vers la clé primaire de la table Bateau. Comme on le voit, JPA n'a plus besoin dans ce cas d'une table de jointure pour coder cette relation. On retombe donc dans le cas nominal d'une relation 1:p avec colonne de jointure dans la table cible de la relation.

4.5. Relation p:1

Tout d'abord, notons que le cas bidirectionnel a déjà été traité, puisqu'une relation bidirectionnelle 1:p en JPA est identique à une relation p:1 bidirectionnelle. Voyons donc le cas unidirectionnel sur un exemple.

Exemple 11. Relation p:1 unidirectionnelle

//
 // Entité Marin
 //

 @Entity
 public  class Marin  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @ManyToOne
     private Bateau bateau ;
    
     // reste de la classe
}


 //
 // Entité Bateau
 //
 @Entity
 public  class Bateau  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     // reste de la classe
}

Cette fois-ci notre classe Marin possède un champ bateau, sans que cette classe n'ait accès à la liste de son équipage.
Schéma pour une relation p:1 unidirectionnelle

Figure 12. Schéma pour une relation p:1 unidirectionnelle


Comme on le voit, JPA a créé une colonne de jointure dans la table Marin, et une clé primaire qui référence la clé primaire de la table Bateau.

4.6. Relation n:p

Une relation n:p est une relation multivaluée des deux côtés de la relation. La façon classique d'enregistrer ce modèle en base consiste à créer une table de jointure, et c'est ce que fait JPA.

4.6.1. Cas unidirectionnel

Voyons ce cas sur un exemple.

Exemple 12. Relation n:p unidirectionnelle

//
 // Entité Musicien
 //

 @Entity
 public  class Musicien  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @ManyToMany
     private Collection<Instrument> instruments ;
    
     // reste de la classe
}

 //
 // Entité Instrument
 //
 @Entity
 public  class Instrument  implements Serializable {
    
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;
    
     // reste de la classe
}

JPA génère le schéma suivant pour ce jeu de classes. Une table de jointure est créé entre les deux tables qui portent les entités. Cette table référence les deux clés primaires des deux entités au travers de clés étrangères. Le schéma généré est donc ici tout à fait classique.
Schéma pour une relation n:p unidirectionnelle

Figure 13. Schéma pour une relation n:p unidirectionnelle


4.6.2. Cas bidirectionnel

Dans le cas bidirectionnel, l'entité cible porte également une relation @ManyToMany vers l'entité maître. Cette relation doit comporter un attribut mappedBy, qui indique le nom de la relation correspondante dans l'entité maître.

Exemple 13. Relation n:p bidirectionnelle

//
 // Entité Musicien
 //

 @Entity
 public  class Musicien  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @ManyToMany
     private Collection<Instrument> instruments ;
    
     // reste de la classe
}

 //
 // Entité Instrument
 //
 @Entity
 public  class Instrument  implements Serializable {
    
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;
    
     @ManyToMany(mappedBy="instruments")
     private Collection<Musicien> musiciens ;
    
     // reste de la classe
}

Dans ce deuxième cas, la structure de tables générée est la même. Notons encore une fois que c'est la présence de l'attribut mappedBy qui crée le caractère bidirectionnel de la relation. Si l'on ne le met pas, alors JPA créera une seconde table de jointure.

4.7. Comportement cascade

Comme nous l'avons vu, l' entity manager permet de mener à bien cinq opérations sur une entité : DETACH, MERGE, PERSIST, REMOVE, REFRESH. Le comportement cascade consiste à spécifier ce qui se passe pour une entité en relation d'une entité père (que cette relation soit monovaluée ou multivaluée), lorsque cette entité père subit une des opérations définies ci-dessus. Prenons l'exemple suivant.

Exemple 14. Comportement cascade sur une relation @OneToOne

@Entity
 public  class Commune  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @OneToOne(cascade={CascadeType.PERSIST, CascadeType.REMOVE})
     private Maire maire ;
    
     // reste de la classe
}

Le comportement cascade est précisé par l'attribut cascade, disponible sur les annotations : @OneToOne, @OneToMany et @ManyToMany. La valeur de cet attribut est une énumération de type CascadeType. En plus des valeurs DETACH, MERGE, PERSIST, REMOVE, REFRESH, cette énumération définit la valeur ALL, qui correspond à toutes les valeurs à la fois. Remarquons bien que l'annotation @ManyToOne ne définit pas cet attribut.

4.8. Effacement des entités orphelines

JPA 2.0 apporte une amélioration très intéressante sur JPA 1.0 : la détection d'entités orphelines. Dans de nombreux cas, l'existence d'une entité n'a de sens que si elle est en relation d'une autre entité (cas des compositions de l'UML). Effacer cette entité père doit alors entraîner l'effacement de cette entité en relation. Dans la plupart des cas, le comportement cascade suffit à traiter ce cas. Mais il arrive que des effacements d'entité soient nécessaires sans qu'il y ait eu d'appel à la méthode remove() de l' entity manager . Dans l'exemple de nos communes et de nos maires, effacer une commune entraînera l'effacement du maire. Mais l'appel à commune.setMaire(null) doit aussi entraîner l'effacement de ce maire, dans la mesure où cette entité sera orpheline. On dispose pour cela d'un attribut défini sur @OneToOne et @OneToMany : orphanRemoval. Le fait de mettre cet attribut à true activera la détection d'entités orphelines, et leur effacement automatique.

Exemple 15. Effacement des orphelins sur une relation @OneToOne

@Entity
 public  class Commune  implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;

     @OneToOne(cascade={CascadeType.PERSIST, CascadeType.REMOVE}
              orphanRemoval=true)
     private Maire maire ;
    
     // reste de la classe
}

JPA & EJB
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction
1. Objet du mapping objet / relationnel
2. Un peu d'histoire
Un premier exemple
1. Introduction
2. Création de l'environnement technique
2.1. Introduction
2.2. Création de la base Derby
2.3. Création du projet NetBeans et d'une première entité
2.4. Structure d'un projet persistant
2.5. Une première classe persistante
2.6. Un premier fichier persistence.xml
3. Utilisation de ce premier exemple
3.1. Écriture du code d'utilisation
3.2. Exécution de notre premier exemple
3.3. Modification de la class Marin
3.4. Opérations CRUD
Mettre un jeu de classes en base
1. Introduction
2. Définition d'une entité JPA
2.1. Écriture de l'entité
2.2. Annotation de l'entité
2.3. Annotations des champs
2.4. Exemple d'utilisation
3. Opérations sur les entités
3.1. Introduction
3.2. Opération PERSIST
3.3. Opération REMOVE
3.4. Opération REFRESH
3.5. Opération DETACH
3.6. Opération MERGE
4. Mise en relation d'entités
4.1. Introduction
4.2. Relations unidirectionnelles et bidirectionnelles
4.3. Relation 1:1
4.4. Relation 1:p
4.5. Relation p:1
4.6. Relation n:p
4.7. Comportement cascade
4.8. Effacement des entités orphelines
5. Charger des entités et leurs relations
6. Objets inclus
6.1. Introduction
6.2. Déclaration d'un objet inclus
6.3. Utilisation d'objets inclus
6.4. Cas où l'objet inclus est nul
6.5. Renommer les colonnes incluses
6.6. Collections d'objets inclus
L'API Collection en base
1. Introduction
2. Enregistrer une collection d'entités
2.1. Enregistrement d'une collection simple
2.2. Enregistrement d'un Set
2.3. Enregistrement d'une List
3. Enregistrer une collection de types de base
4. Enregistrement d'une Map
4.1. Table de hachage de type (type de base, entité)
4.2. Cas où la clé est un champ de la valeur
4.3. Cas d'une table (entité, entité)
Héritage
1. Introduction
2. Enregistrement d'une hiérarchie de classes
2.1. Entité et super-classe non enregistrée
2.2. Position du problème
2.3. Trois façons de faire
3. Stratégie SINGLE_TABLE
3.1. Fonctionnement
3.2. Mise en place
3.3. Limitations
4. Stratégie JOINED
4.1. Fonctionnement
4.2. Mise en place
4.3. Limitations
5. Stratégie TABLE_PER_CLASS
5.1. Fonctionnement
5.2. Mise en place
5.3. Limitations
Requêtes
1. Introduction
2. Un premier exemple
2.1. Écriture d'une première requête
2.2. Exécution d'une première requête
2.3. Exécution d'une première requête d'agrégation
3. Définition de requêtes
3.1. Requêtes dynamiques
3.2. Requêtes paramétrées
3.3. Requêtes nommées
3.4. Requêtes natives
4. Exécution, analyse du résultat
4.1. Exécution d'une requête dynamique
4.2. Exécution d'une requête nommée
4.3. Analyse du résultat
4.4. Cas des résultats de grande taille
4.5. Remarques
5. Clause From
5.1. Définition des entités
5.2. Jointures dans la clause From
5.3. Remarque finale sur les jointures en JPQL
6. Clause Where
6.1. Variables et chemins dans une clause where
6.2. Expressions conditionnelles et opérateurs
6.3. Requêtes imbriquées
6.4. Opérateurs any, all et some
6.5. Expressions fonctionnelles
7. Clauses Group By et Having
8. Opérations Update et Delete
EJB
1. Introduction
2. Un premier exemple
2.1. Introduction
2.2. Installation dans Glassfish à l'aide de Netbeans
2.3. Création d'un premier EJB
2.4. Déploiement de notre premier EJB
2.5. Création d'un client
3. Mise en oeuvre du pattern session facade
3.1. Introduction
3.2. Modèle objet
3.3. Définition de l'unité de persistance
3.4. Assemblage de notre application
3.5. Assemblage et déploiement
3.6. Utilisation du client
4. Opération de persistance en façade
4.1. Introduction
4.2. Enrichissement du service
4.3. Création de la méthode findMarinById(long)
4.4. Ajout de la méthode findAllMarins()
4.5. Utilisation dans un code client
5. Types d'EJB
5.1. Introduction
5.2. Qu'est-ce qu'un EJB ?
5.3. Écriture d'un EJB session
5.4. Qu'est-ce qu'une méthode métier ?
5.5. EJB avec ou sans état
5.6. Gestion des transactions
5.7. Restrictions
6. Cycle de vie d'un EJB
6.1. Cas des EJB sans état
6.2. Cas des EJB avec état
6.3. Injection de dépendances
7. Transaction gérée par l'EJB
7.1. Introduction
7.2. Déclaration du mode transactionnel
7.3. Gestion de la transaction
7.4. Fonctionnement de la transaction
7.5. Cas des EJB avec état
8. Transaction gérée par le serveur
8.1. Introduction
8.2. Déclaration du mode transactionnel
8.3. Gestion de la transaction
8.4. Fonctionnement de la transaction
8.5. Remarques
9. Intercepteurs
9.1. Introduction
9.2. Aperçu général
9.3. Cycle de vie d'un intercepteur
9.4. Object InvocationContext
9.5. Interception d'un EJB ou d'une méthode métier
9.6. Exemple de mise en œuvre d'un intercepteur