Leitfaden für JPA mit Hibernate – Relationship Mapping

Einführung

In diesem Artikel tauchen wir in das Relationship Mapping mit JPA und Hibernate in Java ein.

Die Java Persistence API (JPA) ist der Persistenzstandard des Java-Ökosystems. Sie ermöglicht es uns, unser Domänenmodell direkt auf die Datenbankstruktur abzubilden und gibt uns dann die Flexibilität, Objekte in unserem Code zu manipulieren – anstatt uns mit umständlichen JDBC-Komponenten wie Connection, ResultSet usw. herumzuschlagen.

Wir werden eine umfassende Anleitung zur Verwendung von JPA mit Hibernate als Anbieter erstellen. In diesem Artikel werden wir uns mit Relationship Mappings beschäftigen.

  • Leitfaden zu JPA mit Hibernate – Basic Mapping
  • Leitfaden zu JPA mit Hibernate – Relationships Mapping (Sie sind hier)
  • Leitfaden zu JPA mit Hibernate: Inheritance Mapping (demnächst!)
  • Leitfaden zu JPA mit Hibernate – Querying (demnächst!)

Unser Beispiel

Bevor wir beginnen, wollen wir uns an das Beispiel erinnern, das wir im vorherigen Teil dieser Serie verwendet haben. Die Idee war, das Modell einer Schule mit Schülern abzubilden, die Kurse besuchen, die von Lehrern gegeben werden.

So sieht dieses Modell aus:

Wie wir sehen können, gibt es ein paar Klassen mit bestimmten Eigenschaften. Diese Klassen haben Beziehungen untereinander. Am Ende dieses Artikels werden wir all diese Klassen auf Datenbanktabellen abgebildet haben und ihre Beziehungen beibehalten.

Außerdem werden wir in der Lage sein, sie als Objekte abzurufen und zu bearbeiten, ohne die Mühe von JDBC.

Beziehungen

Zunächst definieren wir eine Beziehung. Wenn wir unser Klassendiagramm betrachten, können wir einige Beziehungen sehen:

Lehrer und Kurse – Studenten und Kurse – Kurse und Kursmaterialien.

Es gibt auch Verbindungen zwischen Studenten und Adressen, aber sie werden nicht als Beziehungen betrachtet. Das liegt daran, dass ein Address keine Entität ist (d.h. es wird nicht auf eine eigene Tabelle abgebildet). Soweit es JPA betrifft, handelt es sich also nicht um eine Beziehung.

Es gibt einige Arten von Beziehungen:

  • Eine-zu-Viele
  • Viele-zu-Eine
  • Eine-zu-Eine
  • Viele-zu-Viele

Lassen Sie uns diese Beziehungen eine nach der anderen angehen.

Eine-zu-Viele/Viele-zu-Eine

Beginnen wir mit den Beziehungen Eins-zu-Viele und Viele-zu-Eins, die eng miteinander verwandt sind. Man könnte sagen, dass sie die entgegengesetzten Seiten derselben Medaille sind.

Was ist eine One-to-Many-Beziehung?

Wie der Name schon sagt, handelt es sich um eine Beziehung, die eine Entität mit vielen anderen Entitäten verbindet.

In unserem Beispiel wäre das eine Teacher und ihre Courses. Ein Lehrer kann mehrere Kurse geben, aber ein Kurs wird nur von einem Lehrer gegeben (das ist die Many-to-One-Perspektive – viele Kurse zu einem Lehrer).

Ein anderes Beispiel könnte in den sozialen Medien sein – ein Foto kann viele Kommentare haben, aber jeder dieser Kommentare gehört zu diesem einen Foto.

Bevor wir in die Details der Zuordnung dieser Beziehung eintauchen, lassen Sie uns unsere Entitäten erstellen:

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

Die Felder der Klasse Teacher sollten nun eine Liste von Kursen enthalten. Da wir diese Beziehung in einer Datenbank abbilden möchten, die keine Liste von Entitäten innerhalb einer anderen Entität enthalten kann, werden wir sie mit einer @OneToMany-Annotation versehen:

@OneToManyprivate List<Course> courses;

Wir haben hier einen List als Feldtyp verwendet, aber wir hätten auch einen Set oder einen Map nehmen können (obwohl dieser etwas mehr Konfiguration erfordert).

Wie bildet JPA diese Beziehung in der Datenbank ab? Im Allgemeinen müssen wir für diese Art von Beziehung einen Fremdschlüssel in einer Tabelle verwenden.

JPA tut dies für uns, wenn wir angeben, wie die Beziehung behandelt werden soll. Dies geschieht über die @JoinColumn-Annotation:

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

Durch die Verwendung dieser Annotation wird JPA mitgeteilt, dass die Tabelle COURSE eine Fremdschlüsselspalte TEACHER_ID haben muss, die auf die Spalte ID der Tabelle TEACHER verweist.

Lassen Sie uns einige Daten zu diesen Tabellen hinzufügen:

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');

Und nun lassen Sie uns überprüfen, ob die Beziehung wie erwartet funktioniert:

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");

Wir können sehen, dass die Kurse des Lehrers automatisch erfasst werden, wenn wir die Teacher-Instanz abrufen.

Wenn Sie mit dem Testen in Java nicht vertraut sind, könnte Sie die Lektüre von Unit Testing in Java mit JUnit 5 interessieren!

Eigene Seite und Bidirektionalität

Im vorherigen Beispiel wird die Klasse Teacher als die eigene Seite der One-To-Many-Beziehung bezeichnet. Das liegt daran, dass sie die Verknüpfungsspalte zwischen den beiden Tabellen definiert.

Die Klasse Course wird in dieser Beziehung als referenzierende Seite bezeichnet.

Wir hätten Course zur besitzenden Seite der Beziehung machen können, indem wir stattdessen das Feld Teacher mit @ManyToOne in der Klasse Course abgebildet hätten:

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

Es besteht jetzt keine Notwendigkeit, eine Liste von Kursen in der Klasse Teacher zu haben. Die Beziehung hätte auf die umgekehrte Weise funktioniert:

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");

Diesmal haben wir die @ManyToOne-Annotation verwendet, genau wie bei @OneToMany.

Hinweis: Es ist eine gute Praxis, die besitzende Seite einer Beziehung in die Klasse/Tabelle zu legen, in der der Fremdschlüssel gehalten wird.

In unserem Fall ist diese zweite Version des Codes also besser. Aber was ist, wenn wir immer noch wollen, dass unsere Teacher-Klasse Zugriff auf ihre Course-Liste bietet?

Das können wir erreichen, indem wir eine bidirektionale Beziehung definieren:

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

Wir behalten unsere @ManyToOne-Zuordnung auf die Course-Entität bei. Allerdings bilden wir auch eine Liste von Courses auf die Teacher-Entität ab.

Was hier wichtig ist, ist die Verwendung des mappedBy-Flags in der @OneToMany-Annotation auf der referenzierenden Seite.

Ohne dieses Flag hätten wir keine bidirektionale Beziehung. Wir würden zwei einseitige Beziehungen haben. Beide Entitäten würden Fremdschlüssel für die andere Entität abbilden.

Damit teilen wir JPA mit, dass das Feld bereits von einer anderen Entität abgebildet wird. Es wird durch das teacher-Feld der Course-Entität gemappt.

Eager vs Lazy Loading

Eine weitere erwähnenswerte Sache ist Eager und Lazy Loading. Wenn alle Beziehungen abgebildet sind, ist es ratsam, den Speicher der Software nicht durch zu viele Entitäten zu belasten, wenn dies nicht notwendig ist.

Stellen Sie sich vor, dass Course ein schweres Objekt ist, und wir laden alle Teacher Objekte aus der Datenbank für eine Operation. Wir brauchen die Kurse für diesen Vorgang nicht abzurufen oder zu verwenden, aber sie werden trotzdem zusammen mit den Teacher-Objekten geladen.

Das kann für die Leistung der Anwendung verheerend sein. Technisch lässt sich das Problem lösen, indem man das Data Transfer Object Design Pattern verwendet und Teacher-Informationen ohne die Kurse abruft.

Das kann jedoch ein massiver Overkill sein, wenn alles, was wir durch das Pattern gewinnen, darin besteht, die Kurse auszuschließen.

Dankenswerterweise hat JPA vorausgedacht und dafür gesorgt, dass One-to-Many-Beziehungen standardmäßig verzögert geladen werden.

Das bedeutet, dass die Beziehung nicht sofort geladen wird, sondern nur, wenn sie tatsächlich benötigt wird.

In unserem Beispiel würde das bedeuten, dass die Kurse erst dann aus der Datenbank geholt werden, wenn wir die Methode Teacher#courses aufrufen.

Im Gegensatz dazu sind Many-to-One-Beziehungen standardmäßig eager, d.h. die Beziehung wird gleichzeitig mit der Entität geladen.

Wir können diese Eigenschaften ändern, indem wir das fetch-Argument beider Annotationen setzen:

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

Das würde die ursprüngliche Funktionsweise umkehren. Die Kurse würden eifrig geladen werden, sobald wir ein Teacher-Objekt laden. Im Gegensatz dazu würde teacher nicht geladen werden, wenn wir courses abrufen, wenn es zu diesem Zeitpunkt nicht benötigt wird.

Optionalität

Lassen Sie uns nun über Optionalität sprechen.

Eine Beziehung kann optional oder obligatorisch sein.

Betrachten wir die One-to-Many-Seite – sie ist immer optional, und wir können nichts dagegen tun. Die Many-to-One-Seite hingegen bietet uns die Möglichkeit, sie obligatorisch zu machen.

Standardmäßig ist die Beziehung optional, was bedeutet, dass wir ein Course speichern können, ohne ihm einen Lehrer zuzuweisen:

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

Nun wollen wir diese Beziehung obligatorisch machen. Dazu verwenden wir das optional-Argument der @ManyToOne-Anmerkung und setzen es auf false (standardmäßig ist es true):

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

Damit können wir einen Kurs nicht mehr speichern, ohne ihm einen Lehrer zuzuweisen:

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

Wenn wir ihm jedoch einen Lehrer zuweisen, funktioniert er wieder einwandfrei:

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

Nun, so scheint es zumindest. Wenn wir den Code ausgeführt hätten, wäre eine Ausnahme ausgelöst worden:

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

Warum ist das so? Wir haben ein gültiges Teacher-Objekt in das Course-Objekt gesetzt, das wir zu persistieren versuchen. Allerdings haben wir das Teacher-Objekt nicht persistiert, bevor wir versucht haben, das Course-Objekt zu persistieren.

Das Teacher-Objekt ist also keine verwaltete Entität. Bringen wir das in Ordnung und versuchen es noch einmal:

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();

Durch die Ausführung dieses Codes werden beide Entitäten persistiert und die Beziehung zwischen ihnen bleibt erhalten.

Kaskadierende Operationen

Wir hätten jedoch auch etwas anderes tun können – wir hätten kaskadieren können und somit die Persistenz des Teacher-Objekts propagieren können, wenn wir das Course-Objekt persistieren.

Das macht mehr Sinn und funktioniert so, wie wir es erwarten, wie im ersten Beispiel, das eine Ausnahme auslöste.

Um dies zu tun, ändern wir das cascade-Flag der Annotation:

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

Auf diese Weise weiß Hibernate, dass das benötigte Objekt auch in dieser Beziehung persistiert werden muss.

Es gibt mehrere Arten von Kaskadenoperationen: PERSIST, MERGE, REMOVE, REFRESH, DETACH und ALL (die alle vorhergehenden kombiniert).

Wir können das Kaskadenargument auch auf die One-to-Many-Seite der Beziehung setzen, so dass Operationen auch von Lehrern auf ihre Kurse kaskadiert werden.

Ein-zu-Eins

Nachdem wir nun die Grundlagen des Relationship Mappings in JPA durch One-to-Many/Many-to-One-Beziehungen und deren Einstellungen geschaffen haben, können wir zu One-to-One-Beziehungen übergehen.

Anstatt eine Beziehung zwischen einer Entität auf der einen Seite und einer Reihe von Entitäten auf der anderen Seite zu haben, werden wir diesmal maximal eine Entität auf jeder Seite haben.

Das ist zum Beispiel die Beziehung zwischen einer Course und ihrer CourseMaterial. Lassen Sie uns zuerst CourseMaterial abbilden, was wir noch nicht getan haben:

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

Die Anmerkung für die Abbildung einer einzelnen Entität auf eine einzelne andere Entität ist, wenig überraschend, @OneToOne.

Bevor wir sie in unserem Modell einrichten, lassen Sie uns daran denken, dass eine Beziehung eine besitzende Seite hat – vorzugsweise die Seite, die den Fremdschlüssel in der Datenbank hält.

In unserem Beispiel wäre das CourseMaterial, da es Sinn macht, dass es auf Course verweist (obwohl wir es auch andersherum machen könnten):

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

Es hat keinen Sinn, Material zu haben, wenn es nicht auch einen Kurs gibt, der es umfasst. Deshalb ist die Beziehung nicht optional in dieser Richtung.

Apropos Richtung: Machen wir die Beziehung bidirektional, so dass wir auf das Material eines Kurses zugreifen können, wenn dieser einen hat. In der Klasse Course fügen wir hinzu:

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

Hier teilen wir Hibernate mit, dass das Material innerhalb einer Course bereits durch das course-Feld der CourseMaterial-Entität abgebildet wird.

Außerdem gibt es hier kein optional-Attribut, da es standardmäßig true ist, und wir könnten uns einen Kurs ohne Material vorstellen (von einem sehr faulen Lehrer).

Zusätzlich dazu, dass die Beziehung bidirektional ist, könnten wir auch kaskadierende Operationen hinzufügen oder Entitäten eifrig oder träge laden lassen.

Many-to-Many

Nun, zu guter Letzt: Many-to-Many-Beziehungen. Diese haben wir uns für das Ende aufgehoben, weil sie etwas mehr Arbeit erfordern als die vorherigen.

Effektiv beinhaltet eine Many-to-Many-Beziehung in einer Datenbank eine mittlere Tabelle, die auf die beiden anderen Tabellen verweist.

Glücklicherweise übernimmt JPA die meiste Arbeit für uns, wir müssen nur ein paar Anmerkungen machen, und es erledigt den Rest für uns.

In unserem Beispiel ist die Many-to-Many-Beziehung diejenige zwischen Student– und Course-Instanzen, da ein Student mehrere Kurse besuchen kann und ein Kurs von mehreren Studenten besucht werden kann.

Um eine Many-to-Many-Beziehung abzubilden, verwenden wir die Annotation @ManyToMany. Dieses Mal verwenden wir jedoch auch eine @JoinTable-Anmerkung, um die Tabelle einzurichten, die die Beziehung darstellt:

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

Schauen Sie sich nun an, was hier passiert. Die Anmerkung benötigt ein paar Parameter. Zunächst einmal müssen wir der Tabelle einen Namen geben. Wir haben uns für STUDENTS_COURSES entschieden.

Danach müssen wir Hibernate mitteilen, welche Spalten verbunden werden sollen, um STUDENTS_COURSES zu füllen. Der erste Parameter joinColumns legt fest, wie die Join-Spalte (Fremdschlüssel) der besitzenden Seite der Beziehung in der Tabelle konfiguriert werden soll. In diesem Fall ist die besitzende Seite eine Course.

Der Parameter inverseJoinColumns tut das Gleiche, aber für die referenzierende Seite (Student).

Lassen Sie uns einen Datensatz mit Studenten und Kursen einrichten:

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);

Natürlich wird dies nicht ohne weiteres funktionieren. Wir müssen eine Methode hinzufügen, mit der wir Studenten zu einem Kurs hinzufügen können. Ändern wir die Klasse Course ein wenig:

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

Jetzt können wir unseren Datensatz vervollständigen:

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);

Nachdem dieser Code ausgeführt wurde, bleiben unsere Instanzen Course, Teacher und Student sowie ihre Beziehungen bestehen. Rufen wir zum Beispiel einen Studenten aus einem persistierten Kurs ab und prüfen, ob alles in Ordnung ist:

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

Natürlich können wir die Beziehung immer noch als bidirektional abbilden, so wie wir es bei den vorherigen Beziehungen getan haben.

Wir können auch Operationen kaskadieren und festlegen, ob Entitäten lazily oder eagerly geladen werden sollen (Many-to-Many-Beziehungen sind standardmäßig lazy).

Abschluss

Damit ist dieser Artikel über Beziehungen von abgebildeten Entitäten mit JPA abgeschlossen. Wir haben Many-to-One, One-to-Many, Many-to-Many und One-to-One Beziehungen behandelt. Außerdem haben wir uns mit kaskadierenden Operationen, Bidirektionalität, Optionalität und eager/lazy loading fetch-types beschäftigt.

Den Code für diese Serie finden Sie auf GitHub.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.