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

JPA Relationen

📋 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👉 DU BIST HIER!
9JPA Relationen (2): @OneToMany & @ManyToMany🔒 Noch nicht freigeschaltet
10JSF Überblick – Component-Based UI🔒 Noch nicht freigeschaltet

Modul: Java Web Aufbau
Gesamt-Dauer: 10 Arbeitstage
Dein Ziel: Entity-Beziehungen mit @OneToOne und @ManyToOne modellieren


📋 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-7 abgeschlossen (besonders Tag 7!)
  • ✅ persistence.xml konfiguriert
  • ✅ EntityManager Basics verstanden

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 7 für JPA Basics und Entity-Grundlagen.

Setup-Probleme?
Schreib uns: support@java-developer.online


⚡ Das Wichtigste in 30 Sekunden

Heute lernst du:

  • ✅ @OneToOne für 1:1 Beziehungen (User ↔ Profile)
  • ✅ @ManyToOne für N:1 Beziehungen (Orders → User)
  • ✅ @JoinColumn für Foreign Keys konfigurieren
  • ✅ Bidirektionale vs. unidirektionale Relationen
  • ✅ CascadeType verstehen und richtig einsetzen

Am Ende des Tages kannst du: Komplexe Domain-Modelle mit Entity-Beziehungen aufbauen, Foreign Keys kontrollieren und Cascade-Operations sicher nutzen.

Zeit-Investment: ~8 Stunden
Schwierigkeitsgrad: Mittel-Fortgeschritten (Game-Changer!)


👋 Willkommen zu Tag 8!

Hi! 👋

Elyndra hier. Heute wird’s richtig interessant!

Kurzwiederholung: Challenge von Tag 7

Gestern solltest du ein vollständiges User-CRUD mit EntityManager erstellen. Die Lösung:

@Stateless
public class UserService {
    @PersistenceContext
    private EntityManager em;
    
    public void createUser(String username, String email) {
        User user = new User(username, email);
        em.persist(user);
    }
    
    public User findUser(Long id) {
        return em.find(User.class, id);
    }
    
    public void updateEmail(Long id, String newEmail) {
        User user = em.find(User.class, id);
        if (user != null) {
            user.setEmail(newEmail);
        }
    }
    
    public void deleteUser(Long id) {
        User user = em.find(User.class, id);
        if (user != null) {
            em.remove(user);
        }
    }
}

Falls noch nicht gemacht – du findest die vollständige Lösung im GitHub-Projekt.

Was du heute lernst:

Gestern hast du einzelne Entities kennengelernt – heute verbinden wir sie!

Real talk: Entity-Relationen sind der Unterschied zwischen „kann JPA“ und „kann mit JPA arbeiten“. Ohne Relationen hast du nur isolierte Tabellen. Mit Relationen baust du echte Domain-Modelle.

Warum sind Relationen wichtig?

In echten Anwendungen sind Daten verknüpft:

  • Ein User hat ein Profile
  • Eine Order gehört zu einem User
  • Ein Comment gehört zu einem Post

Heute lernst du:

  • @OneToOne – „Ein User hat EIN Profile“
  • @ManyToOne – „Viele Orders gehören zu EINEM User“

Was JPA dir gibt:

  • ✅ Automatische Foreign Keys
  • ✅ Cascading Operations (Update/Delete propagiert)
  • ✅ Lazy/Eager Loading
  • ✅ Bidirektionale Navigation

Keine Sorge:
Relationen wirken komplex, aber wir gehen Schritt für Schritt durch!

Los geht’s! 🚀


🟢 GRUNDLAGEN: @OneToOne verstehen

Was ist eine @OneToOne Beziehung?

Definition: Eine Entity ist mit genau einer anderen Entity verbunden.

Beispiele aus der Praxis:

  • User ↔ Profile (ein User hat EIN Profile)
  • Person ↔ Passport (eine Person hat EINEN Pass)
  • Employee ↔ ParkingSpot (ein Mitarbeiter hat EINEN Parkplatz)

Dein erstes @OneToOne Beispiel

Szenario: User hat ein detailliertes Profile.

User Entity:

package com.javafleet.model;

import jakarta.persistence.*;

@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;
    
    // @OneToOne: Ein User hat EIN Profile
    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "profile_id", referencedColumnName = "id")
    private UserProfile profile;
    
    // Default Constructor
    public User() {}
    
    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }
    
    // 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 UserProfile getProfile() { return profile; }
    public void setProfile(UserProfile profile) { this.profile = profile; }
}

UserProfile Entity:

package com.javafleet.model;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "user_profiles")
public class UserProfile {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @Column(name = "date_of_birth")
    private LocalDate dateOfBirth;
    
    @Column(length = 1000)
    private String bio;
    
    // Default Constructor
    public UserProfile() {}
    
    public UserProfile(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    
    public LocalDate getDateOfBirth() { return dateOfBirth; }
    public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; }
    
    public String getBio() { return bio; }
    public void setBio(String bio) { this.bio = bio; }
}

Was passiert in der Datenbank?

JPA erstellt folgende Struktur:

-- users Tabelle
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL,
    profile_id BIGINT,
    FOREIGN KEY (profile_id) REFERENCES user_profiles(id)
);

-- user_profiles Tabelle
CREATE TABLE user_profiles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    date_of_birth DATE,
    bio VARCHAR(1000)
);

Foreign Key: profile_id in users verweist auf id in user_profiles.

Annotations erklärt

@OneToOne – „Diese Entity hat eine 1:1 Beziehung“
cascade = CascadeType.ALL – „Operationen auf User werden auf Profile propagiert“
orphanRemoval = true – „Wenn Profile von User entfernt wird, lösche es“
@JoinColumn – „Der Foreign Key heißt ‚profile_id'“
referencedColumnName = „id“ – „FK verweist auf ‚id‘ in UserProfile“

Nutzen der @OneToOne Relation

@Stateless
public class UserService {
    
    @PersistenceContext
    private EntityManager em;
    
    // User mit Profile erstellen
    public void createUserWithProfile(String username, String email,
                                      String firstName, String lastName) {
        UserProfile profile = new UserProfile(firstName, lastName);
        
        User user = new User(username, email);
        user.setProfile(profile);  // Profile zuweisen
        
        em.persist(user);  // BEIDE werden gespeichert! (CascadeType.ALL)
    }
    
    // User mit Profile laden
    public User findUserWithProfile(Long userId) {
        return em.find(User.class, userId);
        // Profile wird automatisch geladen!
    }
    
    // Profile aktualisieren
    public void updateProfile(Long userId, String newBio) {
        User user = em.find(User.class, userId);
        if (user != null && user.getProfile() != null) {
            user.getProfile().setBio(newBio);
            // Automatisches UPDATE dank Dirty Checking!
        }
    }
    
    // User mit Profile löschen
    public void deleteUser(Long userId) {
        User user = em.find(User.class, userId);
        if (user != null) {
            em.remove(user);
            // Profile wird automatisch gelöscht! (orphanRemoval = true)
        }
    }
}

Wichtig: Dank CascadeType.ALL musst du Profile NICHT separat persist() – JPA macht das automatisch!


🟢 GRUNDLAGEN: @ManyToOne verstehen

Was ist eine @ManyToOne Beziehung?

Definition: Viele Entities gehören zu einer Entity.

Beispiele aus der Praxis:

  • Orders → User (viele Bestellungen gehören zu einem User)
  • Comments → Post (viele Kommentare gehören zu einem Post)
  • Employees → Department (viele Mitarbeiter gehören zu einer Abteilung)

Dein erstes @ManyToOne Beispiel

Szenario: User hat viele Orders.

Order Entity:

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(nullable = false)
    private BigDecimal totalAmount;
    
    @Column(name = "order_date")
    private LocalDateTime orderDate;
    
    // @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, User user) {
        this.orderNumber = orderNumber;
        this.totalAmount = totalAmount;
        this.user = user;
    }
    
    @PrePersist
    protected void onCreate() {
        orderDate = 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 getOrderDate() { return orderDate; }
    
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }
}

User Entity (erweitert):

@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: Die andere Seite der Beziehung (optional)
    // Wird in Tag 9 erklärt!
    
    // Getters & Setters
    // ...
}

Was passiert in der Datenbank?

-- orders Tabelle
CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_number VARCHAR(255) NOT NULL UNIQUE,
    total_amount DECIMAL(19, 2) NOT NULL,
    order_date DATETIME,
    user_id BIGINT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Foreign Key: user_id in orders verweist auf id in users.

Annotations erklärt

@ManyToOne – „Viele Orders gehören zu einem User“
fetch = FetchType.LAZY – „User wird erst geladen, wenn darauf zugegriffen wird“
@JoinColumn(name = „user_id“) – „Der Foreign Key heißt ‚user_id'“
nullable = false – „Jede Order MUSS einem User gehören“

Nutzen der @ManyToOne Relation

@Stateless
public class OrderService {
    
    @PersistenceContext
    private EntityManager em;
    
    // Order erstellen
    public void createOrder(Long userId, String orderNumber, BigDecimal amount) {
        User user = em.find(User.class, userId);
        if (user == null) {
            throw new IllegalArgumentException("User nicht gefunden!");
        }
        
        Order order = new Order(orderNumber, amount, user);
        em.persist(order);
    }
    
    // Order mit User laden
    public Order findOrderWithUser(Long orderId) {
        Order order = em.find(Order.class, orderId);
        // User ist LAZY - wird erst beim Zugriff geladen:
        if (order != null) {
            String username = order.getUser().getUsername();
            // JETZT wird User aus DB geladen!
        }
        return order;
    }
    
    // Alle Orders eines Users finden
    public List<Order> findOrdersByUser(Long userId) {
        return em.createQuery(
            "SELECT o FROM Order o WHERE o.user.id = :userId ORDER BY o.orderDate DESC",
            Order.class
        )
        .setParameter("userId", userId)
        .getResultList();
    }
    
    // Order löschen
    public void deleteOrder(Long orderId) {
        Order order = em.find(Order.class, orderId);
        if (order != null) {
            em.remove(order);
            // User bleibt erhalten! (kein Cascade von Order → User)
        }
    }
}

🟡 PROFESSIONALS: Fetch Strategies

FetchType.LAZY vs. FetchType.EAGER

Problem: Wann soll JPA verknüpfte Entities laden?

LAZY (Standard für @ManyToOne, @OneToMany, @ManyToMany):

  • Wird geladen, wenn darauf zugegriffen wird
  • Bessere Performance
  • Achtung: LazyInitializationException außerhalb Transaction!

EAGER (Standard für @OneToOne, @ManyToOne optional):

  • Wird sofort beim Laden der Haupt-Entity geladen
  • Einfacher zu nutzen
  • Schlechtere Performance (oft unnötig)

Best Practice: Immer LAZY

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

Warum LAZY?

  1. Performance – lade nur, was du brauchst
  2. Kontrolle – entscheide pro Query
  3. N+1 Problem vermeiden

Wenn du EAGER brauchst: Nutze JOIN FETCH in JPQL:

public Order findOrderWithUser(Long orderId) {
    return em.createQuery(
        "SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.id = :id",
        Order.class
    )
    .setParameter("id", orderId)
    .getSingleResult();
}

🟡 PROFESSIONALS: CascadeType verstehen

Was ist Cascading?

Problem: Wenn ich User lösche, was passiert mit Orders?

Cascading: Operationen auf Parent-Entity werden auf Child-Entity propagiert.

CascadeType Optionen

public enum CascadeType {
    PERSIST,   // persist() propagiert
    MERGE,     // merge() propagiert
    REMOVE,    // remove() propagiert
    REFRESH,   // refresh() propagiert
    DETACH,    // detach() propagiert
    ALL        // Alle Operations propagieren
}

@OneToOne mit CascadeType.ALL

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "profile_id")
private UserProfile profile;

Bedeutet:

  • persist(user) → persist(profile) automatisch
  • remove(user) → remove(profile) automatisch
  • orphanRemoval = true → Profile ohne User wird gelöscht

@ManyToOne – KEIN Cascade!

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

Warum KEIN CascadeType?

Bei remove(order) soll User NICHT gelöscht werden! User ist der „Owner“ – Orders sind abhängig.

Regel:

  • @OneToOne: CascadeType.ALL okay (Profile gehört zu User)
  • @ManyToOne: KEIN Cascade (User ist unabhängig)
  • @OneToMany: Cascade nur bei Composition (später!)

orphanRemoval verstehen

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "profile_id")
private UserProfile profile;

Was ist ein „Orphan“?

Ein Entity ohne Parent.

Szenario:

User user = em.find(User.class, 1L);
user.setProfile(null);  // Profile wird "Waise"
// Beim Commit: Profile wird GELÖSCHT!

Wann nutzen?

  • @OneToOne: Fast immer true
  • @OneToMany: Nur bei „Owned Entities“

🟡 PROFESSIONALS: Bidirektional vs. Unidirektional

Unidirektionale Relation

Was wir bisher hatten:

// Order kennt User
@Entity
public class Order {
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

// User kennt Orders NICHT
@Entity
public class User {
    // Keine @OneToMany!
}

Navigation: Order → User ✅, User → Orders ❌

Bidirektionale Relation

Erweitert:

// Order kennt User
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

// User kennt Orders AUCH
@Entity
public class User {
    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
    private List<Order> orders = new ArrayList<>();
}

Navigation: Order → User ✅, User → Orders ✅

Wichtig: mappedBy = "user" sagt: „Die Relation wird von Order.user verwaltet!“

Wann bidirektional?

Bidirektional nutzen, wenn:

  • Oft von Parent zu Children navigiert wird
  • Convenience-Methoden gewünscht (z.B. user.getOrders())

Unidirektional nutzen, wenn:

  • Nur Child → Parent Navigation benötigt
  • Einfacheres Modell gewünscht

Best Practice:
Start unidirektional – nur bidirektional machen, wenn wirklich benötigt!


🟡 PROFESSIONALS: @JoinColumn Details

Standardverhalten

Ohne @JoinColumn:

@ManyToOne
private User user;

JPA erstellt: user_id (Attributname + „_id“)

Custom Foreign Key Name

@ManyToOne
@JoinColumn(name = "customer_id")
private User user;

DB-Spalte heißt: customer_id

Constraints konfigurieren

@ManyToOne
@JoinColumn(
    name = "user_id",
    nullable = false,           // NOT NULL
    unique = false,             // Nicht unique (Standard)
    foreignKey = @ForeignKey(   // FK-Name für Schema
        name = "fk_order_user"
    )
)
private User user;

Composite Foreign Keys

Für fortgeschrittene Fälle (selten!):

@ManyToOne
@JoinColumns({
    @JoinColumn(name = "user_id", referencedColumnName = "id"),
    @JoinColumn(name = "user_country", referencedColumnName = "country")
})
private User user;

Nur bei Composite Primary Keys nötig!


🔵 BONUS: Vollständiges Beispiel – Order Management

Domain Model

User (1) ←→ (1) UserProfile
User (1) → (N) Order

Entities

User.java:

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;
    
    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "profile_id")
    private UserProfile profile;
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
    private List<Order> orders = new ArrayList<>();
    
    public User() {}
    
    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }
    
    // Convenience Methods
    public void addOrder(Order order) {
        orders.add(order);
        order.setUser(this);
    }
    
    public void removeOrder(Order order) {
        orders.remove(order);
        order.setUser(null);
    }
    
    // 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 UserProfile getProfile() { return profile; }
    public void setProfile(UserProfile profile) { this.profile = profile; }
    
    public List<Order> getOrders() { return orders; }
}

UserProfile.java:

package com.javafleet.model;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "user_profiles")
public class UserProfile {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @Column(name = "date_of_birth")
    private LocalDate dateOfBirth;
    
    @Column(length = 1000)
    private String bio;
    
    @Column(name = "phone_number")
    private String phoneNumber;
    
    public UserProfile() {}
    
    public UserProfile(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    
    public LocalDate getDateOfBirth() { return dateOfBirth; }
    public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; }
    
    public String getBio() { return bio; }
    public void setBio(String bio) { this.bio = bio; }
    
    public String getPhoneNumber() { return phoneNumber; }
    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
}

Order.java:

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(nullable = false)
    private BigDecimal totalAmount;
    
    @Column(name = "order_date")
    private LocalDateTime orderDate;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    public Order() {}
    
    public Order(String orderNumber, BigDecimal totalAmount, User user) {
        this.orderNumber = orderNumber;
        this.totalAmount = totalAmount;
        this.user = user;
        this.status = OrderStatus.PENDING;
    }
    
    @PrePersist
    protected void onCreate() {
        orderDate = 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 getOrderDate() { return orderDate; }
    
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }
}

OrderStatus.java:

package com.javafleet.model;

public enum OrderStatus {
    PENDING,
    CONFIRMED,
    SHIPPED,
    DELIVERED,
    CANCELLED
}

Service mit Relationen

package com.javafleet.service;

import com.javafleet.model.*;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

@Stateless
public class OrderManagementService {
    
    @PersistenceContext
    private EntityManager em;
    
    // User mit Profile erstellen
    public User createUserWithProfile(String username, String email,
                                      String firstName, String lastName,
                                      LocalDate dateOfBirth) {
        UserProfile profile = new UserProfile(firstName, lastName);
        profile.setDateOfBirth(dateOfBirth);
        
        User user = new User(username, email);
        user.setProfile(profile);
        
        em.persist(user);
        return user;
    }
    
    // Order erstellen und User zuordnen
    public Order createOrder(Long userId, String orderNumber, BigDecimal amount) {
        User user = em.find(User.class, userId);
        if (user == null) {
            throw new IllegalArgumentException("User not found: " + userId);
        }
        
        Order order = new Order(orderNumber, amount, user);
        user.addOrder(order);  // Bidirektionale Relation pflegen!
        
        em.persist(order);
        return order;
    }
    
    // User mit allen Orders laden
    public User findUserWithOrders(Long userId) {
        return em.createQuery(
            "SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id",
            User.class
        )
        .setParameter("id", userId)
        .getSingleResult();
    }
    
    // Orders mit User-Info laden (JOIN FETCH)
    public List<Order> findRecentOrders(int limit) {
        return em.createQuery(
            "SELECT o FROM Order o JOIN FETCH o.user " +
            "ORDER BY o.orderDate DESC",
            Order.class
        )
        .setMaxResults(limit)
        .getResultList();
    }
    
    // Order Status aktualisieren
    public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
        Order order = em.find(Order.class, orderId);
        if (order != null) {
            order.setStatus(newStatus);
        }
    }
    
    // User Profile aktualisieren
    public void updateUserProfile(Long userId, String bio, String phone) {
        User user = em.find(User.class, userId);
        if (user != null && user.getProfile() != null) {
            user.getProfile().setBio(bio);
            user.getProfile().setPhoneNumber(phone);
        }
    }
    
    // User mit allen abhängigen Entities löschen
    public void deleteUser(Long userId) {
        User user = em.find(User.class, userId);
        if (user != null) {
            em.remove(user);
            // Profile: gelöscht (orphanRemoval = true)
            // Orders: gelöscht (cascade = CascadeType.REMOVE)
        }
    }
    
    // Einzelne Order löschen
    public void deleteOrder(Long orderId) {
        Order order = em.find(Order.class, orderId);
        if (order != null) {
            User user = order.getUser();
            user.removeOrder(order);  // Bidirektionale Relation pflegen!
            em.remove(order);
        }
    }
    
    // Statistics - Orders pro User
    public Long countOrdersByUser(Long userId) {
        return em.createQuery(
            "SELECT COUNT(o) FROM Order o WHERE o.user.id = :userId",
            Long.class
        )
        .setParameter("userId", userId)
        .getSingleResult();
    }
    
    // Statistics - Total Amount pro User
    public BigDecimal getTotalAmountByUser(Long userId) {
        BigDecimal result = em.createQuery(
            "SELECT SUM(o.totalAmount) FROM Order o WHERE o.user.id = :userId",
            BigDecimal.class
        )
        .setParameter("userId", userId)
        .getSingleResult();
        
        return result != null ? result : BigDecimal.ZERO;
    }
}

Servlet zum Testen

package com.javafleet.web;

import com.javafleet.model.*;
import com.javafleet.service.OrderManagementService;
import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

@WebServlet("/orders")
public class OrderServlet extends HttpServlet {
    
    @Inject
    private OrderManagementService orderService;
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        String action = request.getParameter("action");
        
        if ("create".equals(action)) {
            // Test: User mit Profile erstellen
            User user = orderService.createUserWithProfile(
                "alice", "alice@example.com",
                "Alice", "Johnson",
                LocalDate.of(1990, 5, 15)
            );
            
            out.println("<h2>User erstellt:</h2>");
            out.println("<p>ID: " + user.getId() + "</p>");
            out.println("<p>Username: " + user.getUsername() + "</p>");
            out.println("<p>Name: " + user.getProfile().getFirstName() + 
                       " " + user.getProfile().getLastName() + "</p>");
            
            // Orders erstellen
            Order order1 = orderService.createOrder(
                user.getId(), "ORD-001", new BigDecimal("99.99")
            );
            Order order2 = orderService.createOrder(
                user.getId(), "ORD-002", new BigDecimal("149.50")
            );
            
            out.println("<h2>Orders erstellt:</h2>");
            out.println("<p>Order 1: " + order1.getOrderNumber() + 
                       " - " + order1.getTotalAmount() + "</p>");
            out.println("<p>Order 2: " + order2.getOrderNumber() + 
                       " - " + order2.getTotalAmount() + "</p>");
            
        } else if ("list".equals(action)) {
            // Test: Orders auflisten
            List<Order> orders = orderService.findRecentOrders(10);
            
            out.println("<h2>Recent Orders:</h2>");
            out.println("<table border='1'>");
            out.println("<tr><th>Order#</th><th>User</th><th>Amount</th><th>Status</th></tr>");
            
            for (Order order : orders) {
                out.println("<tr>");
                out.println("<td>" + order.getOrderNumber() + "</td>");
                out.println("<td>" + order.getUser().getUsername() + "</td>");
                out.println("<td>" + order.getTotalAmount() + "</td>");
                out.println("<td>" + order.getStatus() + "</td>");
                out.println("</tr>");
            }
            
            out.println("</table>");
            
        } else {
            out.println("<h2>Order Management Demo</h2>");
            out.println("<p><a href='?action=create'>Create Test Data</a></p>");
            out.println("<p><a href='?action=list'>List Orders</a></p>");
        }
    }
}

💬 Real Talk: Warum Relationen manchmal nerven

Von Elyndra:

Honest talk: Relationen sind cool, aber sie können auch richtig nerven.

3 Dinge, die mich am Anfang frustriert haben:

1. LazyInitializationException

@Transactional
public Order getOrder(Long id) {
    return em.find(Order.class, id);
}

// Später, außerhalb Transaction:
order.getUser().getUsername();  // BOOM! LazyInitializationException

Lösung: JOIN FETCH in Query oder @Transactional breiter setzen.

2. N+1 Problem

List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class)
    .getResultList();

for (Order order : orders) {
    System.out.println(order.getUser().getUsername());
    // Für jede Order: 1 Query! = 1 + N Queries
}

Lösung: JOIN FETCH:

em.createQuery("SELECT o FROM Order o JOIN FETCH o.user", Order.class)

3. Bidirektionale Relationen pflegen

// FALSCH:
order.setUser(user);  // Nur eine Seite!

// RICHTIG:
order.setUser(user);
user.getOrders().add(order);  // Beide Seiten!

Lösung: Helper-Methoden in Entity:

public void addOrder(Order order) {
    orders.add(order);
    order.setUser(this);
}

Bottom line:
Relationen sind mächtig, aber sie haben ihre Quirks. Mit JOIN FETCH, Convenience Methods und Transaction-Scopes kriegst du sie in den Griff!


✅ Checkpoint: Hast du es verstanden?

Zeit, dein Wissen zu testen!

Quiz:

Frage 1: Was ist der Unterschied zwischen @OneToOne und @ManyToOne?

Frage 2: Was macht CascadeType.ALL?

Frage 3: Warum ist FetchType.LAZY besser als FetchType.EAGER?

Frage 4: Was ist orphanRemoval = true und wann nutzt du es?

Frage 5: Was bedeutet mappedBy = "user" in einer bidirektionalen Relation?

Frage 6: Warum sollte @ManyToOne KEIN CascadeType.REMOVE haben?

Frage 7: Wie vermeidest du das N+1 Problem?

Frage 8: Was ist der Unterschied zwischen bidirektional und unidirektional?

Frage 9: Wann nutzt du @JoinColumn?

Frage 10: Was passiert, wenn du eine Order löschst, die zu einem User gehört?


Mini-Challenge:

Aufgabe: Erstelle ein vollständiges Blog-System mit Relationen!

Requirements:

  1. Blog Entity (title, content, createdAt)
  2. Author Entity (username, email, bio)
  3. Comment Entity (text, createdAt)

Relationen:

  • Blog @ManyToOne Author (viele Blogs gehören zu einem Author)
  • Comment @ManyToOne Blog (viele Comments gehören zu einem Blog)
  • Comment @ManyToOne Author (viele Comments gehören zu einem Author)

Service-Methoden:

  1. createBlog(authorId, title, content) – Blog erstellen
  2. addComment(blogId, authorId, text) – Comment hinzufügen
  3. findBlogWithComments(blogId) – Blog mit allen Comments laden
  4. findBlogsByAuthor(authorId) – Alle Blogs eines Authors
  5. deleteComment(commentId) – Comment löschen (Blog bleibt!)

Hinweise:

  • Nutze FetchType.LAZY für alle Relationen
  • Nutze JOIN FETCH für Queries
  • Implementiere @PrePersist für Timestamps
  • KEIN CascadeType.REMOVE bei @ManyToOne!

Lösung:
Die Lösung zu dieser Challenge findest du am Anfang von Tag 9! 🚀

Alternativ: GitHub-Projekt Tag 8 Challenge


Geschafft? 🎉
Dann bist du bereit für die FAQ-Sektion!


❓ Häufig gestellte Fragen

Frage 1: Sollte ich immer bidirektionale Relationen nutzen?

Nein! Start unidirektional (nur Child → Parent).

Bidirektional nur wenn:

  • Du oft von Parent zu Children navigierst
  • Convenience-Methoden gewünscht

Unidirektional Vorteile:

  • Einfacheres Model
  • Weniger Fehlerquellen
  • Besser testbar

Bottom line: YAGNI (You Ain’t Gonna Need It) – bidirektional nur bei echtem Bedarf!


Frage 2: Was ist besser: CascadeType.ALL oder einzelne Types?

Kommt drauf an:

CascadeType.ALL nutzen bei:

  • @OneToOne (Profile gehört zu User)
  • Composition (Child kann ohne Parent nicht existieren)

Einzelne Types nutzen bei:

  • @ManyToOne (mehr Kontrolle)
  • Aggregation (Child kann unabhängig existieren)

Best Practice:

// Composition (Profile gehört zu User)
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)

// Aggregation (Order gehört zu User, aber User ist unabhängig)
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})

Niemals CascadeType.REMOVE von Child → Parent!


Frage 3: Wie handle ich LazyInitializationException?

3 Lösungen:

Option 1: JOIN FETCH in Query

em.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.user WHERE o.id = :id",
    Order.class
)

Option 2: @Transactional breiter setzen

@Transactional
public void processOrder(Long orderId) {
    Order order = em.find(Order.class, orderId);
    String username = order.getUser().getUsername();
    // Alles in einer Transaction!
}

Option 3: FetchType.EAGER (nicht empfohlen)

@ManyToOne(fetch = FetchType.EAGER)

Empfehlung: JOIN FETCH – gibt dir volle Kontrolle!


Frage 4: Wie funktioniert orphanRemoval = true genau?

Beispiel:

@OneToOne(orphanRemoval = true)
@JoinColumn(name = "profile_id")
private UserProfile profile;

Szenario 1: Profile entfernen

User user = em.find(User.class, 1L);
user.setProfile(null);
em.flush();  // Profile wird GELÖSCHT!

Szenario 2: User löschen

User user = em.find(User.class, 1L);
em.remove(user);
// Profile wird automatisch gelöscht (orphanRemoval)

Regel:
„Profile ohne User“ ist ein Orphan → wird gelöscht.

Wann nutzen?

  • @OneToOne: Fast immer true
  • @OneToMany: Nur bei Composition

Frage 5: Warum kein CascadeType.REMOVE bei @ManyToOne?

Problem:

@ManyToOne(cascade = CascadeType.REMOVE)  // GEFÄHRLICH!
private User user;

Was passiert:

em.remove(order);  // Order löschen
// User wird AUCH gelöscht! 😱
// Alle Orders des Users sind plötzlich "verwaist"!

Regel:
Bei @ManyToOne zeigt Child auf Parent. Child löschen soll Parent NICHT löschen!

Richtig:

@ManyToOne(fetch = FetchType.LAZY)  // Kein Cascade!
private User user;

Frage 6: Wie teste ich Entities mit Relationen?

Best Practice: Integration Tests mit H2

@DataJpaTest  // Spring Boot
class OrderRelationTest {
    
    @Autowired
    private EntityManager em;
    
    @Test
    void testCreateOrderWithUser() {
        // Given
        User user = new User("alice", "alice@test.com");
        em.persist(user);
        
        // When
        Order order = new Order("ORD-001", new BigDecimal("99.99"), user);
        em.persist(order);
        em.flush();
        em.clear();  // Persistence Context leeren!
        
        // Then
        Order loaded = em.find(Order.class, order.getId());
        assertNotNull(loaded);
        assertEquals("alice", loaded.getUser().getUsername());
    }
    
    @Test
    void testCascadeDelete() {
        // Given
        User user = new User("bob", "bob@test.com");
        UserProfile profile = new UserProfile("Bob", "Smith");
        user.setProfile(profile);
        em.persist(user);
        em.flush();
        Long userId = user.getId();
        
        // When
        em.remove(user);
        em.flush();
        
        // Then
        assertNull(em.find(User.class, userId));
        assertNull(em.find(UserProfile.class, profile.getId()));
    }
}

Frage 7: Nova fragte: „Lowkey verwirrt – wann @OneToOne, wann @ManyToOne? Same same but different?“

Real talk, Nova! Der Unterschied ist crucial:

@OneToOne:

  • Ein Entity hat GENAU EIN anderes Entity
  • Beispiel: User ↔ Profile (jeder User hat genau 1 Profile)
  • Beispiel: Person ↔ Passport (jede Person hat genau 1 Pass)

@ManyToOne:

  • Viele Entities gehören zu EINEM Entity
  • Beispiel: Orders → User (viele Orders gehören zu einem User)
  • Beispiel: Comments → Post (viele Comments gehören zu einem Post)

Merkregel:
Frag dich: „Kann es mehrere davon geben?“

  • Profile? Nein → @OneToOne
  • Orders? Ja, viele! → @ManyToOne

Vibes Check:
@OneToOne ist „exclusive relationship“ – ein Ding, ein Gegenstück.
@ManyToOne ist „many fans, one idol“ – viele Things zeigen auf ein Ding.

Ngl, das hat bei mir auch gedauert. Practice it!


📚 Quiz-Lösungen

Hier sind die Antworten zum Quiz:


Frage 1: Was ist der Unterschied zwischen @OneToOne und @ManyToOne?

Antwort:

@OneToOne: Eine Entity ist mit genau einer anderen Entity verbunden (1:1).
Beispiel: User ↔ Profile

@ManyToOne: Viele Entities gehören zu einer Entity (N:1).
Beispiel: Orders → User (viele Orders gehören zu einem User)

Kern:
@OneToOne ist exklusiv (ein Gegenstück), @ManyToOne ist eine „many-to-one“ Hierarchie.


Frage 2: Was macht CascadeType.ALL?

Antwort:

Alle Persistence-Operationen auf Parent-Entity werden auf Child-Entity propagiert:

  • persist(parent) → persist(child)
  • merge(parent) → merge(child)
  • remove(parent) → remove(child)
  • refresh(parent) → refresh(child)
  • detach(parent) → detach(child)

Beispiel:

@OneToOne(cascade = CascadeType.ALL)
private UserProfile profile;

em.persist(user);  // Profile wird automatisch persistiert!

Frage 3: Warum ist FetchType.LAZY besser als FetchType.EAGER?

Antwort:

LAZY Vorteile:

  1. Performance – lädt nur, was benötigt wird
  2. Kontrolle – du entscheidest pro Query
  3. N+1 vermeiden – bewusster mit JOIN FETCH

EAGER Nachteile:

  1. Immer geladen, auch wenn unnötig
  2. Performance-Hit bei großen Collections
  3. Kann N+1 Problem verschlimmern

Best Practice: LAZY als Default, JOIN FETCH wo nötig:

em.createQuery("SELECT o FROM Order o JOIN FETCH o.user", Order.class)

Frage 4: Was ist orphanRemoval = true und wann nutzt du es?

Antwort:

orphanRemoval = true: Entities ohne Parent werden automatisch gelöscht.

Beispiel:

@OneToOne(orphanRemoval = true)
private UserProfile profile;

user.setProfile(null);  // Profile wird gelöscht!

Wann nutzen:

  • @OneToOne: Fast immer true (Profile gehört zu User)
  • @OneToMany: Nur bei Composition (Child kann ohne Parent nicht existieren)

Wann NICHT:

  • Aggregation (Child kann unabhängig existieren)

Frage 5: Was bedeutet mappedBy = "user" in einer bidirektionalen Relation?

Antwort:

mappedBy: Definiert die „non-owning“ Seite einer bidirektionalen Relation.

// User (non-owning)
@OneToMany(mappedBy = "user")
private List<Order> orders;

// Order (owning - hat FK!)
@ManyToOne
@JoinColumn(name = "user_id")
private User user;

Bedeutet: „Die Relation wird von Order.user verwaltet!“

Regel:
Nur die Seite OHNE mappedBy hat @JoinColumn und erstellt den Foreign Key.


Frage 6: Warum sollte @ManyToOne KEIN CascadeType.REMOVE haben?**

Antwort:

Problem:

@ManyToOne(cascade = CascadeType.REMOVE)  // GEFÄHRLICH!
private User user;

em.remove(order);
// User wird AUCH gelöscht! 😱

Regel:
Bei @ManyToOne zeigt Child auf Parent. Child löschen soll Parent NICHT löschen!

Logisch:
Wenn du eine Bestellung löschst, soll der User nicht gelöscht werden. Der User ist unabhängig und hat möglicherweise andere Orders!

Richtig:

@ManyToOne(fetch = FetchType.LAZY)  // Kein Cascade von Child → Parent!

Frage 7: Wie vermeidest du das N+1 Problem?

Antwort:

Problem:

List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class)
    .getResultList();  // 1 Query

for (Order order : orders) {
    System.out.println(order.getUser().getUsername());  // N Queries!
}

Lösung: JOIN FETCH

List<Order> orders = em.createQuery(
    "SELECT DISTINCT o FROM Order o JOIN FETCH o.user",
    Order.class
).getResultList();  // Nur 1 Query!

Alternative: Batch Fetching

<property name="hibernate.default_batch_fetch_size" value="10"/>

Frage 8: Was ist der Unterschied zwischen bidirektional und unidirektional?

Antwort:

Unidirektional: Nur eine Seite kennt die andere.

// Order kennt User
@ManyToOne
private User user;

// User kennt Orders NICHT

Bidirektional: Beide Seiten kennen sich.

// Order kennt User
@ManyToOne
@JoinColumn(name = "user_id")
private User user;

// User kennt Orders AUCH
@OneToMany(mappedBy = "user")
private List<Order> orders;

Wann bidirektional?
Nur wenn du wirklich von Parent → Children navigieren musst!


Frage 9: Wann nutzt du @JoinColumn?

Antwort:

Immer wenn:

  1. Du den FK-Namen anpassen willst
  2. Du Constraints konfigurieren willst
  3. Du die „owning side“ explizit machen willst

Beispiel:

@ManyToOne
@JoinColumn(
    name = "customer_id",    // Custom FK name
    nullable = false,        // NOT NULL
    foreignKey = @ForeignKey(name = "fk_order_customer")
)
private User user;

Ohne @JoinColumn:
JPA erstellt automatisch user_id (Attributname + „_id“)


Frage 10: Was passiert, wenn du eine Order löschst, die zu einem User gehört?

Antwort:

Kommt auf die Konfiguration an:

Szenario 1: OHNE Cascade (Standard)

@ManyToOne
private User user;

em.remove(order);
// Order: gelöscht ✅
// User: bleibt erhalten ✅

Szenario 2: MIT CascadeType.REMOVE (falsch!)

@ManyToOne(cascade = CascadeType.REMOVE)  // NIEMALS SO!
private User user;

em.remove(order);
// Order: gelöscht
// User: AUCH gelöscht! 😱 FALSCH!

Regel:
@ManyToOne sollte KEIN CascadeType.REMOVE haben, da Child nicht Parent löschen soll!


🎉 Tag 8 geschafft!

Slay! Du hast es geschafft! 🚀

Das hast du heute gerockt:

  • ✅ @OneToOne für 1:1 Relationen gemeistert
  • ✅ @ManyToOne für N:1 Relationen verstanden
  • ✅ CascadeType und orphanRemoval eingesetzt
  • ✅ LAZY vs. EAGER Fetch Strategies gelernt
  • ✅ N+1 Problem erkannt und gelöst

Von isolierten Entities zu komplexen Domain-Modellen!

Main Character Energy: Unlocked! ✨

Real talk:
Entity-Relationen sind tough, aber du hast den Durchbruch geschafft! Das ist der Skill, der dich von Junior zu Mid-Level hebt.


🚀 Wie geht’s weiter?

Morgen (Tag 9): JPA Relationen (2): @OneToMany & @ManyToMany

Was dich erwartet:

  • @OneToMany für 1:N Relationen (User → Orders)
  • @ManyToMany für M:N Relationen (Students ↔ Courses)
  • Join Tables erstellen und konfigurieren
  • Collection-Management in bidirektionalen Relationen
  • Performance-Optimierung mit Batch Fetching – dein Durchbruch für Production-Ready JPA! 🔥

Brauchst du eine Pause?
Absolut! Relationen sind intensive Kost. Lass es sacken, spiel mit den Entities.

Tipp für heute Abend:
Erweitere das Order-Management-System:

  • Füge eine Category Entity hinzu (Products → Category)
  • Erstelle eine Address Entity (User @OneToOne Address)
  • Teste verschiedene CascadeTypes

Learning by doing! 🔧


🔧 Troubleshooting

Problem: LazyInitializationException

Ursache: LAZY Collection/Entity außerhalb Transaction aufgerufen.

Order order = orderService.findOrder(1L);  // Transaction endet
order.getUser().getUsername();  // BOOM!

Lösung 1: JOIN FETCH

em.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.user WHERE o.id = :id",
    Order.class
)

Lösung 2: @Transactional breiter

@Transactional
public void processOrder(Long id) {
    Order order = em.find(Order.class, id);
    String username = order.getUser().getUsername();  // OK!
}

Problem: mappedBy Element nicht gefunden

Fehler:

Error: Attribute 'user' not found in Order

Ursache: Tippfehler in mappedBy.

// User
@OneToMany(mappedBy = "user")  // MUSS exakt mit Attribut in Order übereinstimmen!
private List<Order> orders;

// Order
@ManyToOne
private User user;  // Attribut heißt "user"

Lösung: Namen müssen genau übereinstimmen!


Problem: Foreign Key Constraint verletzt

Fehler:

Caused by: MySQLIntegrityConstraintViolationException: 
Cannot add or update a child row: a foreign key constraint fails

Ursache: User existiert nicht.

Order order = new Order("ORD-001", amount, null);  // user ist null!
em.persist(order);  // FK user_id kann nicht NULL sein!

Lösung:

User user = em.find(User.class, userId);
if (user == null) {
    throw new IllegalArgumentException("User not found!");
}
Order order = new Order("ORD-001", amount, user);

Problem: Infinite Loop beim JSON serialization

Fehler:

StackOverflowError during JSON serialization

Ursache: Bidirektionale Relation ohne @JsonIgnore.

// User serialisiert Orders → Order serialisiert User → User serialisiert Orders → ...

Lösung: @JsonIgnore

@Entity
public class Order {
    @ManyToOne
    @JsonIgnore  // Verhindert User → Order → User Loop
    private User user;
}

Alternative: DTOs Nutze DTOs für API-Responses statt Entities direkt!


Offizielle Docs:

Tutorials:

Books:

  • „Pro JPA 2“ by Mike Keith (Chapter 4: Relationships)
  • „High-Performance Java Persistence“ by Vlad Mihalcea (must-read!)

Performance:


💬 Feedback?

War Tag 8 zu komplex? Mehr Beispiele gewünscht?

Schreib uns: feedback@java-developer.online


Bis morgen! 👋

Elyndra

elyndra@java-developer.online
Senior Developer bei Java Fleet Systems Consulting


Java Web Aufbau – Tag 8 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.