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

JPA Relationen (1): @OneToOne & @ManyToOne – Tag 8 von 10
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 (je 8 Stunden)
Dauer heute: 8 Stunden
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 mit Datenbanken!
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?
- 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“?
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:
- 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!
📚 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:
- Performance – lädt nur, was benötigt wird
- Kontrolle – du entscheidest pro Query
- N+1 vermeiden – bewusster mit JOIN FETCH
EAGER Nachteile:
- Immer geladen, auch wenn unnötig
- Performance-Hit bei großen Collections
- 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:
- Du den FK-Namen anpassen willst
- Du Constraints konfigurieren willst
- 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
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?
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

