Java Web Aufbau – Tag 9 von 10
Von Elyndra Valen, Senior Developer bei Java Fleet Systems Consulting

📋 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Filter im Webcontainer | ✅ Abgeschlossen |
| 2 | Listener im Webcontainer | ✅ Abgeschlossen |
| 3 | Authentifizierung über Datenbank | ✅ Abgeschlossen |
| 4 | Container-Managed Security & Jakarta Security API | ✅ Abgeschlossen |
| 5 | Custom Tags & Tag Handler (SimpleTag) | ✅ Abgeschlossen |
| 6 | Custom Tag Handler mit BodyTagSupport | ✅ Abgeschlossen |
| 7 | JPA vs JDBC – Konfiguration & Provider | ✅ Abgeschlossen |
| 8 | JPA Relationen (1): @OneToOne & @ManyToOne | ✅ Abgeschlossen |
| → 9 | JPA Relationen (2): @OneToMany & @ManyToMany | 👉 DU BIST HIER! |
| 10 | JSF Überblick – Component-Based UI | 🔒 Noch nicht freigeschaltet |
Modul: Java Web Aufbau
Gesamt-Dauer: 10 Arbeitstage
Dein Ziel: Collection-Relationen mit @OneToMany und @ManyToMany meistern
📋 Voraussetzungen für diesen Tag
Du brauchst:
- ✅ JDK 21 LTS installiert
- ✅ Apache NetBeans 22 (oder neuer)
- ✅ Payara Server 6.x konfiguriert
- ✅ MySQL oder PostgreSQL Datenbank läuft
- ✅ Tag 1-8 abgeschlossen (besonders Tag 7 & 8!)
- ✅ @OneToOne und @ManyToOne verstanden
- ✅ CascadeType und FetchType Basics
Datenbank-Setup:
Falls noch nicht geschehen:
docker run --name mysql-jpa -e MYSQL_ROOT_PASSWORD=secret \ -e MYSQL_DATABASE=jpadb -p 3306:3306 -d mysql:8
Tag verpasst?
Kein Problem! Spring zurück zu Tag 8 für @OneToOne und @ManyToOne Basics.
Setup-Probleme?
Schreib uns: elyndra.valen@java-developer.online
⚡ Das Wichtigste in 30 Sekunden
Heute lernst du:
- ✅ @OneToMany für 1:N Beziehungen (User → Orders)
- ✅ @ManyToMany für M:N Beziehungen (Students ↔ Courses)
- ✅ Join Tables erstellen und konfigurieren
- ✅ Bidirektionale Collection-Relationen managen
- ✅ Performance-Optimierung mit Batch Fetching
Am Ende des Tages kannst du: Komplexe Domain-Modelle mit Collections aufbauen, Join Tables kontrollieren und das gefürchtete N+1 Problem bei Collections lösen.
Schwierigkeitsgrad: Fortgeschritten (aber du schaffst das!)
👋 Willkommen zu Tag 9!
Hi! 👋
Elyndra hier. Vorletzter Tag – du bist fast am Ziel!
Kurzwiederholung: Challenge von Tag 8
Gestern solltest du ein Order-Management-System mit @ManyToOne erweitern. Die Lösung:
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private BigDecimal totalAmount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
// Constructors, Getters, Setters...
}
Falls noch nicht gemacht – du findest die vollständige Lösung im GitHub-Projekt.
Was du heute lernst:
Gestern hast du gelernt, wie eine Entity auf eine andere zeigt (@ManyToOne). Heute drehen wir den Spieß um: Eine Entity hat eine Collection von anderen Entities!
Real talk: @OneToMany und @ManyToMany sind die mächtigsten, aber auch die trickreichsten JPA-Features. Hier passieren die meisten Performance-Probleme. Aber keine Sorge – ich zeige dir, wie du sie vermeidest!
Der Unterschied zu gestern:
| Gestern (Tag 8) | Heute (Tag 9) |
|---|---|
| Order → User (N:1) | User → Orders (1:N) |
| Einzelne Referenz | Collection von Entities |
private User user; | private List<Order> orders; |
| Foreign Key in Order | Foreign Key in Order (gleich!) |
Was JPA dir gibt:
- ✅ Automatische Collection-Verwaltung
- ✅ Join Tables für M:N Relationen
- ✅ Lazy Loading für Collections
- ✅ Cascade Operations auf Collections
Los geht’s! 🚀

🟢 GRUNDLAGEN: @OneToMany verstehen
Was ist eine @OneToMany Beziehung?
Definition: Eine Entity hat eine Collection von anderen Entities.
Beispiele aus der Praxis:
- User → Orders (ein User hat VIELE Bestellungen)
- Post → Comments (ein Post hat VIELE Kommentare)
- Department → Employees (eine Abteilung hat VIELE Mitarbeiter)
- Category → Products (eine Kategorie hat VIELE Produkte)
@OneToMany ist die Gegenseite von @ManyToOne!
Wichtig zu verstehen:
@OneToMany und @ManyToOne sind ZWEI SEITEN derselben Medaille!
User (1) ←────────────→ Order (N)
↑ ↑
│ │
@OneToMany @ManyToOne
List<Order> orders User user
Die Foreign Key liegt IMMER auf der „Many“-Seite!
In der Datenbank:
-- orders Tabelle hat den Foreign Key!
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_number VARCHAR(255),
user_id BIGINT, -- FK zu users!
FOREIGN KEY (user_id) REFERENCES users(id)
);
Dein erstes @OneToMany Beispiel
Szenario: User hat viele Orders.
User Entity (die „One“-Seite):
package com.javafleet.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String email;
// @OneToMany: Ein User hat VIELE Orders
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// Default Constructor
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
}
// === HELPER METHODS für bidirektionale Synchronisation ===
public void addOrder(Order order) {
orders.add(order);
order.setUser(this); // Bidirektionale Sync!
}
public void removeOrder(Order order) {
orders.remove(order);
order.setUser(null); // Bidirektionale Sync!
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public List<Order> getOrders() { return orders; }
public void setOrders(List<Order> orders) { this.orders = orders; }
}
Order Entity (die „Many“-Seite):
package com.javafleet.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", nullable = false, unique = true)
private String orderNumber;
@Column(name = "total_amount", precision = 10, scale = 2)
private BigDecimal totalAmount;
@Column(name = "created_at")
private LocalDateTime createdAt;
// @ManyToOne: Viele Orders gehören zu EINEM User
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
// Default Constructor
public Order() {}
public Order(String orderNumber, BigDecimal totalAmount) {
this.orderNumber = orderNumber;
this.totalAmount = totalAmount;
}
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public LocalDateTime getCreatedAt() { return createdAt; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
Annotations erklärt
@OneToMany(mappedBy = „user“)
mappedBy= „Diese Seite ist NICHT der Owner“- Der Wert
"user"= Name des Attributs in Order-Klasse - Keine @JoinColumn hier! Der FK ist in Order.
cascade = CascadeType.ALL
- Operationen auf User werden auf Orders propagiert
persist(user)→ persistiert auch alle Ordersremove(user)→ löscht auch alle Orders
orphanRemoval = true
- Wenn Order aus Collection entfernt wird → DELETE in DB
user.getOrders().remove(order)→ Order wird gelöscht!
fetch = FetchType.LAZY (Default bei @OneToMany)
- Orders werden NICHT sofort geladen
- Erst bei
user.getOrders()→ Extra Query
🟢 GRUNDLAGEN: Bidirektionale Synchronisation
Das Problem: Inkonsistenter Zustand
FALSCH – führt zu Problemen:
// Nur eine Seite setzen
Order order = new Order("ORD-001", new BigDecimal("99.99"));
order.setUser(user); // Order kennt User
// ABER: User kennt Order NICHT!
em.persist(order); // Funktioniert, aber...
user.getOrders(); // Enthält order NICHT! Inkonsistent!
Die Lösung: Helper Methods
RICHTIG – immer beide Seiten synchron halten:
// In User-Klasse
public void addOrder(Order order) {
orders.add(order); // User kennt Order
order.setUser(this); // Order kennt User
}
public void removeOrder(Order order) {
orders.remove(order); // User vergisst Order
order.setUser(null); // Order vergisst User
}
Verwendung:
User user = em.find(User.class, 1L);
Order order = new Order("ORD-001", new BigDecimal("99.99"));
// Eine Methode, beide Seiten synchron!
user.addOrder(order);
em.persist(user); // Order wird mit-persistiert (CascadeType.ALL)
Best Practice: Setter schützen
// In User-Klasse - setOrders() einschränken
public void setOrders(List<Order> orders) {
// Verhindere direktes Ersetzen!
throw new UnsupportedOperationException(
"Use addOrder() and removeOrder() instead!");
}
// Oder: Defensive Copy
public List<Order> getOrders() {
return Collections.unmodifiableList(orders);
}
🟢 GRUNDLAGEN: @OneToMany nutzen
User mit Orders erstellen
@Stateless
public class UserService {
@PersistenceContext
private EntityManager em;
// User mit mehreren Orders erstellen
public void createUserWithOrders() {
User user = new User("maxmustermann", "max@example.com");
Order order1 = new Order("ORD-001", new BigDecimal("99.99"));
Order order2 = new Order("ORD-002", new BigDecimal("149.99"));
Order order3 = new Order("ORD-003", new BigDecimal("29.99"));
// Helper Methods nutzen!
user.addOrder(order1);
user.addOrder(order2);
user.addOrder(order3);
em.persist(user); // Alle 3 Orders werden mit-persistiert!
}
// User mit allen Orders laden
public User findUserWithOrders(Long userId) {
return em.createQuery(
"SELECT DISTINCT u FROM User u " +
"LEFT JOIN FETCH u.orders " +
"WHERE u.id = :id",
User.class
)
.setParameter("id", userId)
.getSingleResult();
}
// Order zu existierendem User hinzufügen
public void addOrderToUser(Long userId, String orderNumber, BigDecimal amount) {
User user = em.find(User.class, userId);
if (user != null) {
Order order = new Order(orderNumber, amount);
user.addOrder(order);
// Kein persist() nötig - CascadeType.ALL!
}
}
// Order von User entfernen
public void removeOrderFromUser(Long userId, Long orderId) {
User user = em.find(User.class, userId);
if (user != null) {
Order orderToRemove = user.getOrders().stream()
.filter(o -> o.getId().equals(orderId))
.findFirst()
.orElse(null);
if (orderToRemove != null) {
user.removeOrder(orderToRemove);
// orphanRemoval = true → Order wird gelöscht!
}
}
}
}

🟡 PROFESSIONALS: @ManyToMany verstehen
Was ist eine @ManyToMany Beziehung?
Definition: Viele Entities sind mit vielen anderen Entities verbunden.
Beispiele aus der Praxis:
- Students ↔ Courses (ein Student belegt VIELE Kurse, ein Kurs hat VIELE Studenten)
- Products ↔ Tags (ein Produkt hat VIELE Tags, ein Tag hat VIELE Produkte)
- Users ↔ Roles (ein User hat VIELE Rollen, eine Rolle hat VIELE User)
- Authors ↔ Books (ein Autor schreibt VIELE Bücher, ein Buch hat VIELE Autoren)
@ManyToMany braucht eine Join Table!
Das Problem:
In relationalen Datenbanken kann eine Spalte nur EINEN Wert haben. Wie speicherst du „Student 1 hat Kurse A, B, C“?
Die Lösung: Join Table (Zwischentabelle)
-- students Tabelle
CREATE TABLE students (
id BIGINT PRIMARY KEY,
name VARCHAR(255)
);
-- courses Tabelle
CREATE TABLE courses (
id BIGINT PRIMARY KEY,
title VARCHAR(255)
);
-- JOIN TABLE: student_courses
CREATE TABLE student_courses (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
Die Join Table enthält nur Foreign Keys – keine eigenen Daten!
Dein erstes @ManyToMany Beispiel
Szenario: Students und Courses.
Student Entity:
package com.javafleet.model;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
// @ManyToMany: Ein Student hat VIELE Courses
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_courses", // Name der Join Table
joinColumns = @JoinColumn(name = "student_id"), // FK zu Student
inverseJoinColumns = @JoinColumn(name = "course_id") // FK zu Course
)
private Set<Course> courses = new HashSet<>();
// Default Constructor
public Student() {}
public Student(String name, String email) {
this.name = name;
this.email = email;
}
// === HELPER METHODS ===
public void enrollInCourse(Course course) {
courses.add(course);
course.getStudents().add(this); // Bidirektionale Sync!
}
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this); // Bidirektionale Sync!
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Set<Course> getCourses() { return courses; }
public void setCourses(Set<Course> courses) { this.courses = courses; }
// equals() und hashCode() für Set-Verwendung!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return id != null && id.equals(student.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
Course Entity:
package com.javafleet.model;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(length = 1000)
private String description;
private int credits;
// @ManyToMany: Ein Course hat VIELE Students (inverse Seite)
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// Default Constructor
public Course() {}
public Course(String title, int credits) {
this.title = title;
this.credits = credits;
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public int getCredits() { return credits; }
public void setCredits(int credits) { this.credits = credits; }
public Set<Student> getStudents() { return students; }
public void setStudents(Set<Student> students) { this.students = students; }
// equals() und hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Course)) return false;
Course course = (Course) o;
return id != null && id.equals(course.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
Annotations erklärt
@ManyToMany
- Definiert M:N Beziehung
- EINE Seite ist Owner (hat @JoinTable)
- ANDERE Seite hat
mappedBy
@JoinTable
name= Name der ZwischentabellejoinColumns= FK zur EIGENEN Entity (Student)inverseJoinColumns= FK zur ANDEREN Entity (Course)
Warum Set statt List?
- Sets verhindern Duplikate
- Bessere Performance bei
contains()undremove() - WICHTIG: equals() und hashCode() implementieren!
Warum KEIN CascadeType.REMOVE?
- Student löschen sollte Course NICHT löschen!
- Course hat andere Studenten
- Nur PERSIST und MERGE sind sinnvoll
🟡 PROFESSIONALS: @ManyToMany nutzen
Students und Courses verwalten
@Stateless
public class EnrollmentService {
@PersistenceContext
private EntityManager em;
// Student in Course einschreiben
public void enrollStudent(Long studentId, Long courseId) {
Student student = em.find(Student.class, studentId);
Course course = em.find(Course.class, courseId);
if (student != null && course != null) {
student.enrollInCourse(course);
// Automatisch in Join Table: INSERT INTO student_courses
}
}
// Student aus Course austragen
public void dropStudent(Long studentId, Long courseId) {
Student student = em.find(Student.class, studentId);
Course course = em.find(Course.class, courseId);
if (student != null && course != null) {
student.dropCourse(course);
// Automatisch aus Join Table: DELETE FROM student_courses
}
}
// Alle Courses eines Students laden
public Student findStudentWithCourses(Long studentId) {
return em.createQuery(
"SELECT DISTINCT s FROM Student s " +
"LEFT JOIN FETCH s.courses " +
"WHERE s.id = :id",
Student.class
)
.setParameter("id", studentId)
.getSingleResult();
}
// Alle Students eines Courses laden
public Course findCourseWithStudents(Long courseId) {
return em.createQuery(
"SELECT DISTINCT c FROM Course c " +
"LEFT JOIN FETCH c.students " +
"WHERE c.id = :id",
Course.class
)
.setParameter("id", courseId)
.getSingleResult();
}
// Neuen Student mit Courses erstellen
public void createStudentWithCourses(String name, String email,
List<Long> courseIds) {
Student student = new Student(name, email);
for (Long courseId : courseIds) {
Course course = em.find(Course.class, courseId);
if (course != null) {
student.enrollInCourse(course);
}
}
em.persist(student);
}
}

🟡 PROFESSIONALS: @ManyToMany mit Extra-Daten
Das Problem: Join Table braucht mehr als nur IDs
Szenario: Du willst speichern, WANN ein Student sich eingeschrieben hat und welche NOTE er hat.
-- Join Table mit Extra-Spalten
CREATE TABLE enrollments (
student_id BIGINT,
course_id BIGINT,
enrolled_at TIMESTAMP, -- Extra!
grade VARCHAR(2), -- Extra!
PRIMARY KEY (student_id, course_id)
);
Problem: @ManyToMany unterstützt KEINE Extra-Spalten in der Join Table!
Die Lösung: @ManyToMany aufteilen in 2x @ManyToOne
Enrollment Entity (die neue „Join Entity“):
package com.javafleet.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "enrollments")
public class Enrollment {
@EmbeddedId
private EnrollmentId id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("studentId")
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("courseId")
@JoinColumn(name = "course_id")
private Course course;
@Column(name = "enrolled_at")
private LocalDateTime enrolledAt;
private String grade;
// Default Constructor
public Enrollment() {}
public Enrollment(Student student, Course course) {
this.student = student;
this.course = course;
this.id = new EnrollmentId(student.getId(), course.getId());
this.enrolledAt = LocalDateTime.now();
}
// Getters & Setters...
}
Composite Key:
package com.javafleet.model;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
public class EnrollmentId implements Serializable {
private Long studentId;
private Long courseId;
public EnrollmentId() {}
public EnrollmentId(Long studentId, Long courseId) {
this.studentId = studentId;
this.courseId = courseId;
}
// Getters & Setters
public Long getStudentId() { return studentId; }
public void setStudentId(Long studentId) { this.studentId = studentId; }
public Long getCourseId() { return courseId; }
public void setCourseId(Long courseId) { this.courseId = courseId; }
// equals() und hashCode() PFLICHT für Composite Keys!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof EnrollmentId)) return false;
EnrollmentId that = (EnrollmentId) o;
return Objects.equals(studentId, that.studentId) &&
Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
Student Entity (angepasst):
@Entity
public class Student {
// ... andere Felder
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Enrollment> enrollments = new HashSet<>();
public void enrollInCourse(Course course) {
Enrollment enrollment = new Enrollment(this, course);
enrollments.add(enrollment);
course.getEnrollments().add(enrollment);
}
}
🟡 PROFESSIONALS: Das N+1 Problem bei Collections
Das Problem verstehen
// 1 Query für alle Users
List<User> users = em.createQuery("SELECT u FROM User u", User.class)
.getResultList();
// N Queries für Orders (LAZY Loading!)
for (User user : users) {
System.out.println(user.getUsername() + ": " +
user.getOrders().size() + " orders");
// ↑ Jeder Zugriff = 1 Query!
}
Ergebnis bei 100 Users:
- 1 Query für Users
- 100 Queries für Orders
- = 101 Queries! 😱
Lösung 1: JOIN FETCH
List<User> users = em.createQuery(
"SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders",
User.class
).getResultList();
// Jetzt nur 1 Query!
for (User user : users) {
System.out.println(user.getUsername() + ": " +
user.getOrders().size() + " orders");
}
DISTINCT ist wichtig! Ohne DISTINCT bekommst du Duplikate durch den JOIN.
Lösung 2: Batch Fetching
In persistence.xml:
<property name="hibernate.default_batch_fetch_size" value="25"/>
Effekt:
- Statt 100 Queries → 4 Queries (100/25 = 4 Batches)
- JPA lädt 25 Collections auf einmal
Lösung 3: Entity Graph
@Entity
@NamedEntityGraph(
name = "User.withOrders",
attributeNodes = @NamedAttributeNode("orders")
)
public class User { ... }
// Verwendung:
EntityGraph<?> graph = em.getEntityGraph("User.withOrders");
User user = em.find(User.class, 1L,
Map.of("jakarta.persistence.fetchgraph", graph));
🔵 BONUS: Performance Best Practices
Set vs. List bei @OneToMany
List (ArrayList):
@OneToMany(mappedBy = "user") private List<Order> orders = new ArrayList<>();
- Erlaubt Duplikate
- Reihenfolge bleibt erhalten
- Bei
remove(): Alle Elemente werden geladen!
Set (HashSet):
@OneToMany(mappedBy = "user") private Set<Order> orders = new HashSet<>();
- Keine Duplikate
- Keine garantierte Reihenfolge
- Bei
remove(): Nur das Element wird geprüft - Bessere Performance!
Empfehlung: Set für @OneToMany und @ManyToMany!
Extra Lazy Loading (Hibernate-spezifisch)
@OneToMany(mappedBy = "user")
@org.hibernate.annotations.LazyCollection(
org.hibernate.annotations.LazyCollectionOption.EXTRA
)
private Set<Order> orders = new HashSet<>();
Effekt:
orders.size()→SELECT COUNT(*) ...(keine vollständige Collection laden!)orders.contains(order)→SELECT ... WHERE id = ?
🔵 BONUS: Häufige Fehler vermeiden
Fehler 1: CascadeType.ALL bei @ManyToMany
// ❌ FALSCH! @ManyToMany(cascade = CascadeType.ALL) private Set<Course> courses; // Student löschen → ALLE Courses werden gelöscht! 😱
Richtig:
// ✅ RICHTIG
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Course> courses;
Fehler 2: Bidirektionale Sync vergessen
// ❌ FALSCH - nur eine Seite gesetzt student.getCourses().add(course); // course.getStudents() enthält student NICHT! // ✅ RICHTIG - Helper Method nutzen student.enrollInCourse(course);
Fehler 3: equals()/hashCode() nicht implementiert
// ❌ FALSCH - Default equals() bei Set
Set<Course> courses = new HashSet<>();
// contains(), remove() funktionieren nicht richtig!
// ✅ RICHTIG - equals()/hashCode() implementieren
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Course)) return false;
Course course = (Course) o;
return id != null && id.equals(course.id);
}
📝 Mini-Challenge
Aufgabe: Erweitere das Student-Course-System!
Requirements:
- Erstelle eine
TagEntity - Implementiere @ManyToMany zwischen
CourseundTag - Ein Course kann mehrere Tags haben (z.B. „Java“, „Web“, „Database“)
- Ein Tag kann zu mehreren Courses gehören
- Implementiere Helper Methods für bidirektionale Sync
- Schreibe einen Service mit:
addTagToCourse(Long courseId, String tagName)findCoursesByTag(String tagName)findTagsByCourse(Long courseId)
Bonus:
- Verhindere doppelte Tags (gleicher Name)
- Implementiere
removeTagFromCourse()
Lösung:
Findest du am Anfang von Tag 10! 🚀
❓ Häufig gestellte Fragen
Frage 1: Wann @OneToMany vs. @ManyToMany?
@OneToMany: „Gehört zu“ Beziehung
- Order gehört zu User (User „besitzt“ Orders)
- Wenn User gelöscht wird, werden Orders auch gelöscht
@ManyToMany: „Verbunden mit“ Beziehung
- Student ist verbunden mit Course (keiner „besitzt“ den anderen)
- Wenn Student gelöscht wird, bleibt Course bestehen
Frage 2: Muss ich immer bidirektional mappen?
Nein! Bidirektional nur wenn du von BEIDEN Seiten navigieren musst.
Unidirektional @ManyToMany:
// Student kennt Courses @ManyToMany @JoinTable(...) private Set<Course> courses; // Course kennt Students NICHT (kein mappedBy)
Vorteil: Weniger Code, weniger Sync-Aufwand.
Frage 3: Warum Set statt List für @ManyToMany?
List-Problem:
// Mit List: Hibernate löscht ALLE und fügt neu ein! student.getCourses().remove(course); // DELETE FROM student_courses WHERE student_id = ? // INSERT INTO student_courses ... (alle anderen)
Set-Lösung:
// Mit Set: Nur der eine Eintrag wird gelöscht student.getCourses().remove(course); // DELETE FROM student_courses WHERE student_id = ? AND course_id = ?
Frage 4: Wie handle ich Ordering bei @OneToMany?
Mit @OrderBy:
@OneToMany(mappedBy = "user")
@OrderBy("createdAt DESC")
private List<Order> orders;
Mit @OrderColumn (eigene Spalte):
@OneToMany(mappedBy = "user") @OrderColumn(name = "order_index") private List<Order> orders;
Frage 5: Kann ich @ManyToMany mit @JoinTable anpassen?
Ja! Alle Details konfigurierbar:
@ManyToMany
@JoinTable(
name = "course_enrollments",
joinColumns = @JoinColumn(
name = "student_fk",
referencedColumnName = "id",
foreignKey = @ForeignKey(name = "fk_enrollment_student")
),
inverseJoinColumns = @JoinColumn(
name = "course_fk",
referencedColumnName = "id",
foreignKey = @ForeignKey(name = "fk_enrollment_course")
),
uniqueConstraints = @UniqueConstraint(
columnNames = {"student_fk", "course_fk"}
)
)
private Set<Course> courses;
Frage 6: Was ist der Unterschied zwischen orphanRemoval und CascadeType.REMOVE?
CascadeType.REMOVE:
- Löscht Children wenn Parent gelöscht wird
em.remove(user)→ alle Orders gelöscht
orphanRemoval:
- Löscht Children wenn sie aus Collection entfernt werden
user.getOrders().remove(order)→ Order gelöscht- Zusätzlich zu CascadeType.REMOVE!
Empfehlung bei @OneToMany:
@OneToMany(mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true)
Frage 7: Bernd meinte, „Collection-Relationen sind Performance-Killer“. Hat er recht?
Lowkey ja – WENN du sie falsch nutzt! 😄
Performance-Killer:
- FetchType.EAGER bei großen Collections
- N+1 Problem ignorieren
- Alle Daten laden statt Paginierung
Performance-Helden:
- FetchType.LAZY + JOIN FETCH bei Bedarf
- Batch Fetching für Listen-Views
- DTOs für Read-Only Queries
- Extra Lazy für size()/contains()
Real talk: Collection-Relationen sind mächtig, aber du musst wissen, wann welche Query ausgeführt wird. Mit den richtigen Patterns (JOIN FETCH, Batch Fetching, Entity Graphs) sind sie genauso performant wie Raw SQL!
✅ Checkpoint: Hast du Tag 9 geschafft?
Kontrolliere deine Erfolge:
🟢 Grundlagen (PFLICHT):
- [ ] @OneToMany Beziehung verstanden und implementiert
- [ ] Bidirektionale Synchronisation mit Helper Methods
- [ ] mappedBy vs. @JoinColumn Unterschied klar
- [ ] orphanRemoval Effekt verstanden
🟡 Professionals (EMPFOHLEN):
- [ ] @ManyToMany mit @JoinTable implementiert
- [ ] Join Entity für Extra-Spalten erstellt
- [ ] N+1 Problem erkannt und mit JOIN FETCH gelöst
- [ ] Set statt List für Collections verwendet
🔵 Bonus (OPTIONAL):
- [ ] Batch Fetching konfiguriert
- [ ] Entity Graph ausprobiert
- [ ] Extra Lazy Loading verstanden
✅ Alle Grundlagen-Häkchen gesetzt?
Glückwunsch! Du bist bereit für Tag 10! 🎉
❌ Nicht alles funktioniert?
Kein Problem! Schau nochmal in die Troubleshooting-Sektion.
🎉 Tag 9 geschafft!
Slay! Du hast es geschafft! 🚀
Das hast du heute gerockt:
- ✅ @OneToMany für 1:N Relationen gemeistert
- ✅ @ManyToMany mit Join Tables verstanden
- ✅ Bidirektionale Sync mit Helper Methods
- ✅ N+1 Problem erkannt und gelöst
- ✅ Performance-Patterns gelernt
Von einfachen Entities zu komplexen Domain-Modellen!
Main Character Energy: Unlocked! ✨
Real talk:
Collection-Relationen sind der letzte große JPA-Boss. Du hast ihn besiegt! Das ist Enterprise-Level Wissen.
🚀 Wie geht’s weiter?
Morgen (Tag 10): JSF Überblick – Component-Based UI
Was dich erwartet:
- JSF-Lifecycle verstehen
- Facelets und Managed Beans
- Component-basierte UIs bauen
- Das große Finale des Java Web Aufbau Kurses! 🎉
Brauchst du eine Pause?
Absolut! Collection-Relationen sind intensive Kost. Lass es sacken.
Tipp für heute Abend:
Arbeite die Mini-Challenge durch:
- Tag-Entity für Courses
- @ManyToMany in beide Richtungen
- Helper Methods implementieren
Learning by doing! 🔧
🔧 Troubleshooting
Problem: LazyInitializationException bei Collection
Ursache: Collection wird außerhalb der Transaction geladen.
User user = userService.findUser(1L); // Transaction endet user.getOrders().size(); // BOOM! LazyInitializationException
Lösung 1: JOIN FETCH
em.createQuery(
"SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id",
User.class
)
Lösung 2: @Transactional breiter
@Transactional
public void processUserOrders(Long userId) {
User user = em.find(User.class, userId);
user.getOrders().forEach(o -> System.out.println(o.getOrderNumber()));
}
Problem: Duplikate bei JOIN FETCH
Fehler:
List<User> users = em.createQuery(
"SELECT u FROM User u LEFT JOIN FETCH u.orders",
User.class
).getResultList();
// User mit 3 Orders erscheint 3x in der Liste!
Lösung: DISTINCT
"SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders"
Problem: MultipleBagFetchException
Fehler:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
Ursache: Zwei List-Collections mit JOIN FETCH.
// ❌ FALSCH "SELECT u FROM User u JOIN FETCH u.orders JOIN FETCH u.comments" // Beide sind List → Exception!
Lösung 1: Set statt List
@OneToMany(mappedBy = "user") private Set<Order> orders; // Set statt List!
Lösung 2: Zwei Queries
// Query 1: User mit Orders
User user = em.createQuery(
"SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id",
User.class
).setParameter("id", userId).getSingleResult();
// Query 2: Comments nachladen
em.createQuery(
"SELECT u FROM User u LEFT JOIN FETCH u.comments WHERE u = :user",
User.class
).setParameter("user", user).getSingleResult();
Problem: StackOverflowError bei toString()
Ursache: Bidirektionale Relation in toString().
// User.toString() ruft orders.toString() auf // Order.toString() ruft user.toString() auf // → Endlosschleife!
Lösung: Keine Relationen in toString()
@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "'}";
// KEINE orders hier!
}
Problem: ConcurrentModificationException
Fehler:
for (Order order : user.getOrders()) {
if (order.isCancelled()) {
user.getOrders().remove(order); // BOOM!
}
}
Lösung: Iterator oder removeIf()
// Mit removeIf (Java 8+)
user.getOrders().removeIf(Order::isCancelled);
// Oder mit Iterator
Iterator<Order> it = user.getOrders().iterator();
while (it.hasNext()) {
if (it.next().isCancelled()) {
it.remove();
}
}
📚 Resources & Links
Offizielle Docs:
- Jakarta Persistence: https://jakarta.ee/specifications/persistence/
- JPA Collections: https://docs.oracle.com/javaee/7/tutorial/persistence-intro004.htm
Tutorials:
- Baeldung @OneToMany: https://www.baeldung.com/hibernate-one-to-many
- Baeldung @ManyToMany: https://www.baeldung.com/jpa-many-to-many
- Thorben Janssen Collections: https://thorben-janssen.com/best-practices-for-many-to-many-associations-with-hibernate-and-jpa/
Performance:
- Vlad Mihalcea N+1: https://vladmihalcea.com/n-plus-1-query-problem/
- Vlad Mihalcea Batch Fetching: https://vladmihalcea.com/hibernate-batch-fetching/
Books:
- „Pro JPA 2“ by Mike Keith (Chapter 4: Relationships)
- „High-Performance Java Persistence“ by Vlad Mihalcea
💬 Feedback?
War Tag 9 zu komplex? Mehr Beispiele gewünscht?
Schreib uns: feedback@java-developer.online
Bis morgen – zum großen Finale! 👋
Elyndra
elyndra.valen@java-developer.online
Senior Developer bei Java Fleet Systems Consulting
Java Web Aufbau – Tag 9 von 10
© 2025 Java Fleet Systems Consulting
Website: java-developer.online

