Guide de JPA avec Hibernate – Mappage des relations

Introduction

Dans cet article, nous allons plonger dans le mappage des relations avec JPA et Hibernate en Java.

L’API de persistance Java (JPA) est la norme de persistance de l’écosystème Java. Elle nous permet de mapper notre modèle de domaine directement sur la structure de la base de données et nous donne ensuite la flexibilité de manipuler des objets dans notre code – au lieu de s’embêter avec des composants JDBC encombrants comme Connection, ResultSet, etc.

Nous allons faire un guide complet pour utiliser JPA avec Hibernate comme son fournisseur. Dans cet article, nous allons couvrir les mappings de relations.

  • Guide de JPA avec Hibernate – Mapping de base
  • Guide de JPA avec Hibernate – Mapping de relations (vous êtes ici)
  • Guide de JPA avec Hibernate : Mapping d’héritage (bientôt !)
  • Guide de JPA avec Hibernate – Interrogation (bientôt !)

Notre exemple

Avant de commencer, rappelons l’exemple que nous avons utilisé dans la partie précédente de cette série. L’idée était de cartographier le modèle d’une école avec des étudiants qui suivent des cours donnés par des enseignants.

Voici à quoi ressemble ce modèle :

Comme nous pouvons le voir, il y a quelques classes avec certaines propriétés. Ces classes ont des relations entre elles. À la fin de cet article, nous aurons mappé toutes ces classes à des tables de base de données, en persistant leurs relations.

De plus, nous serons en mesure de les récupérer et de les manipuler en tant qu’objets, sans les tracas de JDBC.

Relations

D’abord, définissons une relation. Si nous regardons notre diagramme de classes, nous pouvons voir quelques relations :

Professeurs et cours – étudiants et cours – cours et matériel de cours.

Il existe également des connexions entre les étudiants et les adresses, mais elles ne sont pas considérées comme des relations. C’est parce qu’un Address n’est pas une entité (c’est-à-dire qu’il n’est pas mappé à une table qui lui est propre). Donc, en ce qui concerne JPA, ce n’est pas une relation.

Il existe quelques types de relations :

  • Un-à-plusieurs
  • Many-to-One
  • Un-à-un
  • Many-to-Many

Attachons ces relations une par une.

Un-à-plusieurs/Many-to-One

Nous allons commencer par les relations Un-à-plusieurs et Many-to-One, qui sont étroitement liées. Vous pourriez aller de l’avant et dire qu’elles sont les côtés opposés de la même pièce de monnaie.

Qu’est-ce qu’une relation de un à plusieurs ?

Comme son nom l’indique, c’est une relation qui relie une entité à plusieurs autres entités.

Dans notre exemple, ce serait un Teacher et leur Courses. Un professeur peut donner plusieurs cours, mais un cours n’est donné que par un seul professeur (c’est la perspective Many-to-One – plusieurs cours pour un seul professeur).

Un autre exemple pourrait être sur les médias sociaux – une photo peut avoir de nombreux commentaires, mais chacun de ces commentaires appartient à cette seule photo.

Avant de plonger dans les détails de la façon de mapper cette relation, créons nos entités :

@Entitypublic class Teacher { private String firstName; private String lastName;}@Entitypublic class Course { private String title;}

Maintenant, les champs de la classe Teacher devraient inclure une liste de cours. Puisque nous aimerions cartographier cette relation dans une base de données, qui ne peut pas inclure une liste d’entités dans une autre entité – nous l’annoterons avec une annotation @OneToMany:

@OneToManyprivate List<Course> courses;

Nous avons utilisé un List comme type de champ ici, mais nous aurions pu opter pour un Set ou un Map (bien que celui-ci nécessite un peu plus de configuration).

Comment JPA reflète-t-il cette relation dans la base de données ? Généralement, pour ce type de relation, nous devons utiliser une clé étrangère dans une table.

JPA le fait pour nous, compte tenu de notre entrée sur la façon dont il doit gérer la relation. Cela se fait via l’annotation @JoinColumn:

@OneToMany@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private List<Course> courses;

L’utilisation de cette annotation indiquera à JPA que la table COURSE doit avoir une colonne de clé étrangère TEACHER_ID qui fait référence à la colonne ID de la table TEACHER.

Ajoutons quelques données à ces tables:

insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane');insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');

Et maintenant vérifions si la relation fonctionne comme prévu:

Teacher foundTeacher = entityManager.find(Teacher.class, 1L);assertThat(foundTeacher.id()).isEqualTo(1L);assertThat(foundTeacher.lastName()).isEqualTo("Doe");assertThat(foundTeacher.firstName()).isEqualTo("Jane");assertThat(foundTeacher.courses()) .extracting(Course::title) .containsExactly("Java 101", "SQL 101", "JPA 101");

Nous pouvons voir que les cours du professeur sont rassemblés automatiquement, lorsque nous récupérons l’instance Teacher.

Si vous n’êtes pas familier avec les tests en Java, vous pourriez être intéressé par la lecture de Test unitaire en Java avec JUnit 5 !

Côté propriétaire et bidirectionnalité

Dans l’exemple précédent, la classe Teacher est appelée le côté propriétaire de la relation Un-To-Many. C’est parce qu’elle définit la colonne de jointure entre les deux tables.

La classe Course est appelée le côté référent dans cette relation.

Nous aurions pu faire de Course le côté propriétaire de la relation en mappant le champ Teacher avec @ManyToOne dans la classe Course à la place :

@ManyToOne@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

Il n’est pas nécessaire d’avoir une liste de cours dans la classe Teacher maintenant. La relation aurait fonctionné dans le sens inverse:

Course foundCourse = entityManager.find(Course.class, 1L);assertThat(foundCourse.id()).isEqualTo(1L);assertThat(foundCourse.title()).isEqualTo("Java 101");assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");

Cette fois, nous avons utilisé l’annotation @ManyToOne, de la même manière que nous avons utilisé @OneToMany.

Note : C’est une bonne pratique de mettre le côté propriétaire d’une relation dans la classe/table où la clé étrangère sera tenue.

Donc, dans notre cas, cette deuxième version du code est meilleure. Mais, que faire si nous voulons toujours que notre classe Teacher offre un accès à sa liste Course?

Nous pouvons le faire en définissant une relation bidirectionnelle:

@Entitypublic class Teacher { // ... @OneToMany(mappedBy = "teacher") private List<Course> courses;}@Entitypublic class Course { // ... @ManyToOne @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") private Teacher teacher;}

Nous conservons notre mappage @ManyToOne sur l’entité Course. Cependant, nous mappons également une liste de Coursesur l’entité Teacher.

Ce qu’il est important de noter ici, c’est l’utilisation du drapeau mappedBy dans l’annotation @OneToMany du côté du référencement.

Sans cela, nous n’aurions pas de relation bidirectionnelle. Nous aurions deux relations à sens unique. Les deux entités mapperaient des clés étrangères pour l’autre entité.

Avec elle, nous disons à JPA que le champ est déjà mappé par une autre entité. Il est mappé par le champ teacher de l’entité Course.

Eager vs Lazy Loading

Une autre chose qui vaut la peine d’être notée est l’eager et la lazy loading. Avec toutes nos relations mappées, il est sage d’éviter d’impacter la mémoire du logiciel en y mettant trop d’entités si cela n’est pas nécessaire.

Imaginez que Course est un objet lourd, et que nous chargeons tous les objets Teacher de la base de données pour une certaine opération. Nous n’avons pas besoin de récupérer ou d’utiliser les cours pour cette opération, mais ils sont toujours chargés à côté des objets Teacher.

Cela peut être dévastateur pour les performances de l’application. Techniquement, cela peut être résolu en utilisant le patron de conception des objets de transfert de données et en récupérant les informations Teacher sans les cours.

Cependant, cela peut être une surenchère massive si tout ce que nous gagnons avec le patron est d’exclure les cours.

Heureusement, JPA a pensé à l’avance et a fait en sorte que les relations Un à Plusieurs se chargent paresseusement par défaut.

Cela signifie que la relation ne sera pas chargée tout de suite, mais seulement quand et si elle est réellement nécessaire.

Dans notre exemple, cela signifierait que jusqu’à ce que nous fassions appel à la méthode Teacher#courses, les cours ne sont pas récupérés de la base de données.

En revanche, les relations Many-to-One sont eager par défaut, ce qui signifie que la relation est chargée en même temps que l’entité.

Nous pouvons modifier ces caractéristiques en paramétrant l’argument fetch des deux annotations :

@OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER)private List<Course> courses;@ManyToOne(fetch = FetchType.LAZY)private Teacher teacher;

Cela inverserait le fonctionnement initial. Les cours seraient chargés avec empressement, dès que l’on charge un objet Teacher. Par contre, le teacher ne serait pas chargé lorsque nous allons chercher courses s’il n’est pas nécessaire à ce moment-là.

Optionnalité

Maintenant, parlons de l’optionnalité.

Une relation peut être optionnelle ou obligatoire.

Envisageons le côté Un-à-plusieurs – il est toujours optionnel, et nous ne pouvons rien y faire. Le côté Many-to-One, en revanche, nous offre la possibilité de la rendre obligatoire.

Par défaut, la relation est facultative, ce qui signifie que nous pouvons enregistrer un Course sans lui attribuer un enseignant:

Course course = new Course("C# 101");entityManager.persist(course);

Maintenant, rendons cette relation obligatoire. Pour ce faire, nous allons utiliser l’argument optional de l’annotation @ManyToOne et le définir à false (il est true par défaut):

@ManyToOne(optional = false)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

Donc, nous ne pouvons plus enregistrer un cours sans lui attribuer un enseignant:

Course course = new Course("C# 101");assertThrows(Exception.class, () -> entityManager.persist(course));

Mais si nous lui attribuons un enseignant, cela fonctionne à nouveau parfaitement:

Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);

En tout cas, il semblerait que ce soit le cas. Si nous avions exécuté le code, une exception aurait été levée:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

Pourquoi ? Nous avons défini un objet Teacher valide dans l’objet Course que nous essayons de persister. Cependant, nous n’avons pas persisté l’objet Teacher avant d’essayer de persister l’objet Course.

Donc, l’objet Teacher n’est pas une entité gérée. Corrigeons cela et essayons à nouveau :

Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");entityManager.persist(teacher);Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);entityManager.flush();

L’exécution de ce code fera persister les deux entités et persistera la relation entre elles.

Opérations en cascade

Cependant, nous aurions pu faire autre chose – nous aurions pu faire des opérations en cascade, et ainsi propager la persistance de l’objet Teacher lorsque nous persistons l’objet Course.

Cela a plus de sens et fonctionne comme nous l’attendons comme dans le premier exemple qui a jeté une exception.

Pour ce faire, nous allons modifier le drapeau cascade de l’annotation :

@ManyToOne(optional = false, cascade = CascadeType.PERSIST)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

De cette façon, Hibernate sait qu’il faut persister l’objet nécessaire dans cette relation également.

Il existe plusieurs types d’opérations en cascade : PERSIST, MERGE, REMOVE, REFRESH, DETACH, et ALL (qui combine toutes les précédentes).

Nous pouvons également mettre l’argument de cascade du côté One-to-Many de la relation, de sorte que les opérations sont également cascadées des enseignants vers leurs cours.

One-to-One

Maintenant que nous avons mis en place les bases du mappage de relations dans JPA à travers les relations One-to-Many/Many-to-One et leurs paramètres, nous pouvons passer aux relations One-to-One.

Cette fois, au lieu d’avoir une relation entre une entité d’un côté et un tas d’entités de l’autre, nous aurons au maximum une entité de chaque côté.

C’est, par exemple, la relation entre un Course et son CourseMaterial. Commençons par mapper CourseMaterial, ce que nous n’avons pas encore fait :

@Entitypublic class CourseMaterial { @Id private Long id; private String url;}

L’annotation pour mapper une seule entité à une seule autre entité est, sans surprise, @OneToOne.

Avant de la mettre en place dans notre modèle, rappelons qu’une relation a un côté propriétaire – de préférence le côté qui détiendra la clé étrangère dans la base de données.

Dans notre exemple, ce serait CourseMaterial car il est logique qu’il fasse référence à un Course (bien que nous puissions aller dans l’autre sens):

@OneToOne(optional = false)@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")private Course course;

Il n’y a aucun intérêt à avoir du matériel sans un cours pour l’englober. C’est pourquoi la relation n’est pas optional dans cette direction.

En parlant de direction, rendons la relation bidirectionnelle, afin que nous puissions accéder au matériel d’un cours s’il en a un. Dans la classe Course, ajoutons :

@OneToOne(mappedBy = "course")private CourseMaterial material;

Ici, nous disons à Hibernate que le matériel dans un Course est déjà cartographié par le champ course de l’entité CourseMaterial.

De plus, il n’y a pas d’attribut optional ici car il est true par défaut, et nous pourrions imaginer un cours sans matériel (d’un professeur très paresseux).

En plus de rendre la relation bidirectionnelle, nous pourrions également ajouter des opérations en cascade ou faire en sorte que les entités se chargent avec empressement ou paresseusement.

Many-to-Many

Enfin, le dernier mais non le moindre : Les relations Many-to-Many. Nous les avons gardées pour la fin parce qu’elles nécessitent un peu plus de travail que les précédentes.

Effectivement, dans une base de données, une relation Many-to-Many implique une table centrale référençant les deux autres tables.

Heureusement pour nous, JPA fait la plupart du travail, nous devons juste jeter quelques annotations là-bas, et il gère le reste pour nous.

Donc, pour notre exemple, la relation Many-to-Many sera celle entre les instances Student et Course car un étudiant peut suivre plusieurs cours, et un cours peut être suivi par plusieurs étudiants.

Pour mapper une relation Many-to-Many, nous utiliserons l’annotation @ManyToMany. Cependant, cette fois-ci, nous utiliserons également une annotation @JoinTable pour configurer la table qui représente la relation :

@ManyToMany@JoinTable( name = "STUDENTS_COURSES", joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"), inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID"))private List<Student> students;

Maintenant, revoyez ce qui se passe ici. L’annotation prend quelques paramètres. Tout d’abord, nous devons donner un nom à la table. Nous l’avons choisi pour être STUDENTS_COURSES.

Après cela, nous devrons indiquer à Hibernate les colonnes à joindre afin de peupler STUDENTS_COURSES. Le premier paramètre, joinColumns définit comment configurer la colonne de jointure (clé étrangère) du côté propriétaire de la relation dans la table. Dans ce cas, le côté propriétaire est un Course.

D’autre part, le paramètre inverseJoinColumns fait la même chose, mais pour le côté référencement (Student).

Configurons un ensemble de données avec des étudiants et des cours:

Student johnDoe = new Student();johnDoe.setFirstName("John");johnDoe.setLastName("Doe");johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));johnDoe.setGender(MALE);johnDoe.setWantsNewsletter(true);johnDoe.setAddress(new Address("Baker Street", "221B", "London"));entityManager.persist(johnDoe);Student willDoe = new Student();willDoe.setFirstName("Will");willDoe.setLastName("Doe");willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));willDoe.setGender(MALE);willDoe.setWantsNewsletter(false);willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));entityManager.persist(willDoe);Teacher teacher = new Teacher();teacher.setFirstName("Jane");teacher.setLastName("Doe");entityManager.persist(teacher);Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);entityManager.persist(sqlCourse);

Bien sûr, cela ne fonctionnera pas d’emblée. Nous devrons ajouter une méthode qui nous permettra d’ajouter des étudiants à un cours. Modifions un peu la classe Course:

public class Course { private List<Student> students = new ArrayList<>(); public void addStudent(Student student) { this.students.add(student); }}

Maintenant, nous pouvons compléter notre ensemble de données:

Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);javaCourse.addStudent(johnDoe);javaCourse.addStudent(willDoe);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);sqlCourse.addStudent(johnDoe);entityManager.persist(sqlCourse);

Une fois ce code exécuté, il fera persister nos instances Course, Teacher et Student ainsi que leurs relations. Par exemple, récupérons un étudiant d’un cours persistant et vérifions si tout va bien :

Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);assertThat(courseWithMultipleStudents).isNotNull();assertThat(courseWithMultipleStudents.students()) .hasSize(2) .extracting(Student::firstName) .containsExactly("John", "Will");

Bien sûr, nous pouvons toujours mapper la relation comme bidirectionnelle de la même manière que nous l’avons fait pour les relations précédentes.

Nous pouvons également effectuer des opérations en cascade ainsi que définir si les entités doivent se charger paresseusement ou avidement (les relations Many-to-Many sont paresseuses par défaut).

Conclusion

C’est la conclusion de cet article sur les relations d’entités mappées avec JPA. Nous avons couvert les relations Many-to-One, One-to-Many, Many-to-Many et One-to-One. En outre, nous avons exploré les opérations en cascade, la bidirectionnalité, l’optionnalité et les types de fetch à chargement rapide/lazy.

Le code de cette série peut être trouvé sur GitHub.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.