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

📋 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Filter im Webcontainer | ✅ Abgeschlossen |
| 2 | Listener im Webcontainer | ✅ Abgeschlossen |
| 3 | Authentifizierung über Datenbank | ✅ Abgeschlossen |
| 4 | Container-Managed Security & Jakarta Security API | ✅ Abgeschlossen |
| 5 | Custom Tags & Tag Handler (SimpleTag) | ✅ Abgeschlossen |
| 6 | Custom Tag Handler mit BodyTagSupport | ✅ Abgeschlossen |
| 7 | JPA vs JDBC – Konfiguration & Provider | ✅ Abgeschlossen |
| → 8 | JPA Relationen (1): @OneToOne & @ManyToOne | 👉 DU BIST HIER! |
| 9 | JPA Relationen (2): @OneToMany & @ManyToMany | 🔒 Noch nicht freigeschaltet |
| 10 | JSF Ü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?
- Performance – lade nur, was du brauchst
- Kontrolle – entscheide pro Query
- 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)automatischremove(user)→remove(profile)automatischorphanRemoval = 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:
- Blog Entity (title, content, createdAt)
- Author Entity (username, email, bio)
- 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:
createBlog(authorId, title, content)– Blog erstellenaddComment(blogId, authorId, text)– Comment hinzufügenfindBlogWithComments(blogId)– Blog mit allen Comments ladenfindBlogsByAuthor(authorId)– Alle Blogs eines AuthorsdeleteComment(commentId)– Comment löschen (Blog bleibt!)
Hinweise:
- Nutze
FetchType.LAZYfü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
| Projekt | Fü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
CategoryEntity hinzu (Products → Category) - Erstelle eine
AddressEntity (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!
📚 Resources & Links
Offizielle Docs:
- Jakarta Persistence: https://jakarta.ee/specifications/persistence/
- JPA Relationships: https://docs.oracle.com/javaee/7/tutorial/persistence-intro004.htm
Tutorials:
- Baeldung JPA Relationships: https://www.baeldung.com/jpa-relationships
- Thorben Janssen Associations: https://thorben-janssen.com/ultimate-guide-association-mappings-jpa-hibernate/
Books:
- „Pro JPA 2“ by Mike Keith (Chapter 4: Relationships)
- „High-Performance Java Persistence“ by Vlad Mihalcea (must-read!)
Performance:
- Vlad Mihalcea’s Blog: https://vladmihalcea.com/
💬 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

