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

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“?

Der Begriff Orphan (verwaistes Objekt) wird in der objekt-relationalen Abbildung (ORM) – insbesondere bei JPA / Hibernate – verwendet, um einen sehr konkreten Zustand zu beschreiben.

Ein Orphan ist eine Entity, die keinen Parent mehr besitzt, obwohl sie logisch zu einem Parent gehören müsste.

Damit ist nicht gemeint, dass das Objekt technisch ungültig ist, sondern dass seine Beziehungskonsistenz verletzt wurde.


Grundidee in einfachen Worten

In einem Parent–Child-Modell gilt:

  • Der Parent ist die fachlich führende Entity
  • Das Child existiert nur im Kontext dieses Parents

Wird das Child aus dieser Beziehung entfernt, existiert es allein weiter – und wird damit zum Orphan.


Klassisches Beispiel (JPA)

Fachliches Modell

  • Order (Parent)
  • OrderItem (Child)

Eine Bestellung besteht aus Bestellpositionen.
Eine Bestellposition ergibt ohne Bestellung keinen Sinn.

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!


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!


Jakarta Web Aufbau - Tag 8

Tag 8 Quiz

Frage 1 von 10

Was beschreibt eine @OneToOne-Relation in JPA?

Frage 2 von 10

Was beschreibt eine @ManyToOne-Relation in JPA?

Frage 3 von 10

Wo wird bei @OneToOne standardmäßig der Foreign Key gespeichert?

Frage 4 von 10

Was bewirkt das Attribut mappedBy bei @OneToOne?

Frage 5 von 10

Welche Annotation wird verwendet, um den Foreign Key-Namen bei @ManyToOne anzupassen?

Frage 6 von 10

Was ist der Unterschied zwischen der owning side und der inverse side einer Relation?

Frage 7 von 10

Was passiert bei einem @ManyToOne ohne explizites @JoinColumn?

Frage 8 von 10

Was bewirkt cascade = CascadeType.PERSIST bei einer Relation?

Frage 9 von 10

Wann sollte fetch = FetchType.LAZY bei @OneToOne verwendet werden?

Frage 10 von 10

Was ist ein typisches Beispiel für eine @ManyToOne-Relation?

ProjektFür wen?Download
tag08-jpa-ralation.zip🟢 Einsteiger⬇️ Download

🎉 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?


Bis morgen! 👋

Elyndra

elyndra.valen@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.