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


📋 Deine Position im Kurs

TagThemaStatus
1Filter im Webcontainer✅ Abgeschlossen
2Listener im Webcontainer✅ Abgeschlossen
3Authentifizierung über Datenbank✅ Abgeschlossen
4Container-Managed Security & Jakarta Security API✅ Abgeschlossen
5Custom Tags & Tag Handler (SimpleTag)✅ Abgeschlossen
6Custom Tag Handler mit BodyTagSupport✅ Abgeschlossen
7JPA vs JDBC – Konfiguration & Provider✅ Abgeschlossen
8JPA Relationen (1): @OneToOne & @ManyToOne✅ Abgeschlossen
→ 9JPA Relationen (2): @OneToMany & @ManyToMany👉 DU BIST HIER!
10JSF Ü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 ReferenzCollection von Entities
private User user;private List<Order> orders;
Foreign Key in OrderForeign 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! 🚀


@OneToMany

🟢 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 Orders
  • remove(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!
            }
        }
    }
}

@ManyToMany

🟡 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 Zwischentabelle
  • joinColumns = FK zur EIGENEN Entity (Student)
  • inverseJoinColumns = FK zur ANDEREN Entity (Course)

Warum Set statt List?

  • Sets verhindern Duplikate
  • Bessere Performance bei contains() und remove()
  • 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);
    }
}

 @ManyToMany mit Extra-Daten

🟡 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:

  1. Erstelle eine Tag Entity
  2. Implementiere @ManyToMany zwischen Course und Tag
  3. Ein Course kann mehrere Tags haben (z.B. „Java“, „Web“, „Database“)
  4. Ein Tag kann zu mehreren Courses gehören
  5. Implementiere Helper Methods für bidirektionale Sync
  6. 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:

Tutorials:

Performance:

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

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.