CodeGym /Kurse /Spring Data JPA /@OneToMany und mappedBy

@OneToMany und mappedBy

Spring Data JPA
Level 8, Lektion 1
Verfügbar

1. Rolle von @OneToMany neben @ManyToOne

Wenn Product seine Category bereits über @ManyToOne kennt, entsteht bei Anfängern fast immer ein sehr menschlicher Wunsch: „Okay, das Produkt kennt seine Kategorie. Kann man das umgekehrt machen – die Kategorie nehmen und eine Liste ihrer Produkte erhalten?“. Dieser Wunsch kommt nicht von Faulheit, sondern von normalem objektorientierten Denken: In Java sind wir es gewohnt, dass wenn etwas „Kinder“ hat, dies wie eine Kollektion aussieht.

Stellen Sie sich einen Katalogservice vor. Sie haben die Kategorie Coffee geladen und möchten alle Produkte in dieser Kategorie auf dem Bildschirm (oder zumindest im Log) anzeigen. Aus Domänensicht ist das eine natürliche Frage: Die Kategorie als Verzeichnis „enthält“ eine Reihe von Produkten und ist nicht einfach eine einzelne Zeile in der Tabelle.

Natürlich kann man auch ohne Kollektion in der Entität leben. Sie können immer eine Abfrage wie „Gib alle Produkte, deren category_id = ? ist“ durchführen und eine Liste erhalten. Aber dann verzichten Sie bewusst auf einen Teil des Komforts des Objektmodells: Sie haben ein Objekt Category, das „sinngemäß“ mit Produkten verbunden ist, aber im Code wird das nicht ausgedrückt. Genau das ermöglicht @OneToMany: die Navigation vom Elternteil zu den Kindern.

Und hier ist es wichtig, nicht in die Erwartungsfalle zu tappen. @OneToMany ist kein magisches „Erstelle eine zweite Verbindung in der Datenbank“. Es ist eher eine Möglichkeit, dem ORM zu sagen: „Ich möchte in meinem Java-Objekt eine Kollektion haben, die der bereits bestehenden Verbindung über den Fremdschlüssel entspricht“. Die Schlüsselwörter in diesem Satz sind bereits bestehenden.

2. Wie die Verbindung gespeichert wird: ein FK und Abfragen

Aus Sicht der Datenbank ist die Kollektion products innerhalb der Kategorie keine separate Spalte und kein separater Speichermechanismus. In der Tabelle category gibt es kein Feld products, und das ist normal: Das relationale Modell speichert keine „Listen von Verweisen“ innerhalb einer Zeile. Es speichert Zeilen, Schlüssel und Beziehungen über Schlüssel.

Die Verbindung „Kategorie – Produkte“ in unserem mini-shop wird fast immer so aussehen: Die Tabelle product enthält den Fremdschlüssel category_id, der auf category(id) verweist. Das heißt, das „Kind“ speichert die Referenz auf den „Elternteil“. Ungefähr so (vereinfacht):

erDiagram
  CATEGORY ||--o{ PRODUCT : contains

  CATEGORY {
    bigint id PK
    varchar code
    varchar name
  }

  PRODUCT {
    bigint id PK
    varchar sku
    varchar name
    bigint category_id FK
  }

Wenn Sie die Produkte einer Kategorie abrufen möchten, „holt“ die Datenbank sie nicht aus einer Spalte, sondern führt eine Abfrage aus. Die einfachste Variante sieht so aus:

-- Produkte einer bestimmten Kategorie abrufen (Beispiel: id = 10)
SELECT p.id, p.sku, p.name
FROM product p
-- category_id – Fremdschlüssel auf category(id)
WHERE p.category_id = 10;

Und wenn Sie Kategorien zusammen mit ihren Produkten anzeigen möchten (z. B. für einen Admin-Bildschirm), kommen Sie leicht zu einem JOIN:

-- JOIN gibt mehrere Zeilen zurück (eine pro Produkt in der Kategorie)
SELECT c.id AS category_id, c.name AS category_name,
       p.id AS product_id,  p.name AS product_name
FROM category c
JOIN product p ON p.category_id = c.id
WHERE c.id = 10;

Hier ist ein wichtiger Gedanke, den man im Kopf behalten sollte, bevor man irgendwelche Annotationen verwendet. In der Datenbank gibt es nur einen Fremdschlüssel (auf der Seite von product). Und die „Liste der Produkte einer Kategorie“ ist eine abgeleitete Darstellung, die durch eine Abfrage erhalten wird.

Wenn wir also @OneToMany in JPA hinzufügen, erstellen wir keine „zweite Verbindung“, sondern fügen eine praktische Navigation im Objektmodell hinzu, die auf demselben FK basiert. Und damit Hibernate dies versteht, muss man ihm explizit erklären: „Diese Kollektion ist ein Spiegelbild der Beziehung, die bereits auf der Produktseite beschrieben wurde“. Genau das macht mappedBy.

3. Bidirektionales Mapping: @ManyToOne + @OneToMany

Wenn wir die Navigation in beide Richtungen wünschen, haben wir eine bidirektionale Beziehung (bidirectional association). Dabei gibt es zwei Felder:

  1. Product.category – Referenz auf den Elternteil (@ManyToOne)
  2. Category.products – Kollektion der Kinder (@OneToMany)

Aber physisch in der Datenbank gibt es, wie gesagt, immer noch nur einen FK.

In unserem Projekt sieht die minimale Version so aus. Zunächst die „Kind“-Seite – die hatten wir bereits in der letzten Vorlesung, aber ich zeige sie in maximal kompakter Form, damit das Feld sichtbar ist, auf das wir über mappedBy verweisen werden.

import jakarta.persistence.*;

@Entity // JPA-Entität (wird normalerweise auf die Tabelle product abgebildet)
public class Product {

    @ManyToOne // Viele Produkte können auf eine Kategorie verweisen
    @JoinColumn(name = "category_id") // FK-Spalte in der Tabelle product
    private Category category;
}

Jetzt fügen wir die „Eltern“-Kollektion hinzu. Und hier kommt der entscheidende Punkt – mappedBy.

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity // JPA-Entität (wird normalerweise auf die Tabelle category abgebildet)
public class Category {

    @OneToMany(mappedBy = "category") // "category" – Feldname in Product, nicht Spaltenname
    private List<Product> products = new ArrayList<>(); // Initialisieren, um NPE zu vermeiden
}

Der Ausdruck mappedBy = "category" bedeutet: „Diese Kollektion products wird auf das Feld category auf der anderen Seite der Beziehung abgebildet“.

Beachten Sie zwei sehr wichtige Details, die überraschend oft verwechselt werden:

Erstens: mappedBy gibt den Namen des Java-Felds an, nicht den Spaltennamen in der Tabelle. Also category, nicht category_id.

Zweitens: mappedBy hat nichts direkt mit SQL zu tun. Es ist eine Anweisung auf ORM-Ebene: „Versuche nicht, einen separaten Speichermechanismus für diese Kollektion zu erstellen. Schau dir das Feld Product.category an – es definiert bereits die Beziehung“.

Das gleiche Muster gilt auch für unsere Bestellungen. Die Bestellposition hat eine Referenz auf die Bestellung (@ManyToOne), und die Bestellung hat eine Kollektion von Positionen (@OneToMany(mappedBy = "customerOrder")).

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity // Bestellung als Entität (Elternteil)
public class CustomerOrder {

    @OneToMany(mappedBy = "customerOrder") // "customerOrder" – Feld in OrderItem
    private List<OrderItem> items = new ArrayList<>(); // Leere Liste statt null
}

Und die „Kind“-Seite:

import jakarta.persistence.*;

@Entity // Position der Bestellung (Kind), hier wird der FK auf die Bestellung gespeichert
public class OrderItem {

    @ManyToOne // Viele Positionen können zu einer Bestellung gehören
    @JoinColumn(name = "customer_order_id") // FK-Spalte in der Tabelle order_item
    private CustomerOrder customerOrder;
}

Wenn Sie ein Gefühl der Symmetrie bekommen, wird es viel beruhigender: „Das Kind speichert die Referenz auf den Elternteil“, „Der Elternteil kann eine Kollektion von Kindern haben“, und mappedBy verbindet diese beiden Felder zu einer Beziehung.

4. mappedBy: Feldname, nicht Spaltenname

mappedBy ist der Parameter, der mehr Nervenzellen zerstört als eine Kaffeemaschine im Büro am Montagmorgen. Der Grund ist einfach: In den Köpfen von Anfängern vermischen sich drei Ebenen: Java-Felder, SQL-Spalten und „wie JPA das speichert“.

Vereinfachen wir es auf eine fast alltägliche Regel. In mappedBy schreiben Sie den Namen des Felds in der Kind-Entität, das auf den Elternteil verweist. Wenn Sie sich unsicher sind, wo dieses Feld ist, machen Sie eine ganz einfache Sache: Öffnen Sie die Kind-Klasse und suchen Sie nach dem @ManyToOne auf den Elternteil. Der Feldname daneben ist genau das mappedBy.

Zum Beispiel in Product:

@ManyToOne // Kind-Seite: Hier lebt die „echte“ Referenz auf den Elternteil
private Category category; // Dieser Feldname muss in mappedBy angegeben werden

Also in Category wird es sein:

@OneToMany(mappedBy = "category") // mappedBy = Name des Java-Felds in Product
private List<Product> products;

Analog in OrderItem:

@ManyToOne // Referenz auf die Bestellung wird in der Bestellposition gespeichert
private CustomerOrder customerOrder; // Dieser Feldname kommt in mappedBy

Also in CustomerOrder wird es sein:

@OneToMany(mappedBy = "customerOrder") // mappedBy = Name des Java-Felds in OrderItem
private List<OrderItem> items;

Jetzt ein wichtiges Anti-Beispiel, das so gut wie immer vorkommt (mindestens einmal bei jedem Studenten – und das ist normal). Oft wird so geschrieben:

@OneToMany(mappedBy = "category_id") // ❌ falsch: das ist ein Spaltenname, kein Feldname
private List<Product> products;

Warum ist das falsch? Weil "category_id" ein Spaltenname (in der SQL-Welt) ist, aber JPA erwartet den Namen des Felds (in der Java-Welt). Hibernate wird buchstäblich versuchen, in Product ein Feld namens category_id zu finden (das es nicht gibt), und Sie erhalten einen Mapping-Fehler beim Start der Anwendung.

Eine weitere typische Verwechslung ist, den Klassennamen zu schreiben:

@OneToMany(mappedBy = "Category") // ❌ falsch: das ist kein Feldname im Kind
private List<Product> products;

Auch das ist falsch. mappedBy bedeutet nicht „wer ist der Elternteil“, sondern „über welches Feld speichert das Kind die Referenz auf den Elternteil“.

Wenn Sie sich das einfache Mantra merken – „mappedBy = Name des Felds im Kind“ – verschwinden die meisten Probleme und das Leben wird verdächtig angenehm.

5. Ohne mappedBy: Join-Tabelle und „zweite Verbindung“

Der beste Weg, mappedBy zu verstehen, ist zu sehen, was passiert, wenn es fehlt. Denn ohne es denkt das ORM (bzw. das JPA-Modell): „Oh, wir haben @ManyToOne in Product und auch @OneToMany in Category. Also … sind das zwei separate Beziehungen! Wir müssen die zweite irgendwie speichern“.

Und eine @OneToMany ohne mappedBy „über den FK im Kind“ kann JPA standardmäßig nicht speichern, weil diese @OneToMany dann den Fremdschlüssel in einer anderen Tabelle verwalten müsste. Der häufigste Default, den das ORM wählt, ist eine Join-Tabelle, also eine zusätzliche Verknüpfungstabelle.

Vereinfacht könnte das so aussehen:

erDiagram
  CATEGORY ||--o{ CATEGORY_PRODUCTS : links
  PRODUCT ||--o{ CATEGORY_PRODUCTS : links

  CATEGORY {
    bigint id PK
    varchar code
    varchar name
  }

  PRODUCT {
    bigint id PK
    varchar sku
    varchar name
  }

  CATEGORY_PRODUCTS {
    bigint category_id FK
    bigint product_id FK
  }

Das heißt, statt eines Fremdschlüssels product.category_id taucht plötzlich eine Verknüpfungstabelle auf. Und die haben Sie nicht verlangt. Und in Ihrer Domäne brauchen Sie sie nicht. Sie haben eine Eins-zu-Viele-Beziehung, keine Viele-zu-Viele-Beziehung.

Deshalb ist mappedBy keine „Kosmetik für die Schönheit“, sondern eine wichtige Anweisung: „Dieses Feld ist ein Spiegel – erstelle keinen zweiten Speichermechanismus“. Mit menschlichen Worten: mappedBy spart Ihnen eine überflüssige Tabelle, überflüssige Komplexität und überflüssige Fragen „Was zum Teufel ist das?“

Hier reicht es, eine praktische Schlussfolgerung festzuhalten: Eine Kollektion mit mappedBy beschreibt die Rückwärtsnavigation und erstellt keine zweite Möglichkeit, die Beziehung zu speichern. Der FK lebt weiterhin auf der Kind-Seite, die Kollektion ist also nicht der Ort, um die Beziehung zu setzen, sondern ihr Spiegelbild.

6. Kollektionen in Entitäten: Deklaration und Fallstricke

Wenn Sie List<Product> products zu Category hinzufügen, verwenden Sie im Wesentlichen eine normale Java-Kollektion innerhalb der Entität. Und dann kommen die altbekannten Probleme ins Spiel: null, plötzliche NullPointerException, versehentliches Ersetzen der Liste und „Warum funktioniert alles in einem Test und fällt dann in einem anderen aus“.

Die nützlichste Regel für Anfänger-JPA-Code klingt langweilig, spart aber Nerven: Initialisieren Sie die Kollektion sofort. Selbst wenn die Kategorie aktuell keine Produkte hat, sollte es eine leere Kollektion sein, kein null.

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity // Kategorie als Eltern-Entität
public class Category {

    @OneToMany(mappedBy = "category") // Rückseite der Beziehung (siehe Product.category)
    private List<Product> products = new ArrayList<>(); // Leere Liste statt null
}

Das erspart eine Menge „Schutzcode“ der Art:

if (category.getProducts() != null) { ... }

Die zweite Feinheit ist der Feldtyp. In der Regel schreibt man das Interface List, nicht die konkrete Implementierung ArrayList. Und das ist kein Snobismus, sondern praktisch: Hibernate kann die Kollektion durch eine eigene interne Implementierung ersetzen (z. B. für Lazy-Loading und Änderungsverfolgung). Wenn Sie einen konkreten Typ festlegen, schränken Sie das ORM (und sich selbst) ein.

Die dritte Feinheit: Machen Sie die Kollektion in der Entität nicht final. In einem normalen Java-Modell ist final gut – weniger Überraschungen. Aber in der ORM-Welt kann das problematisch sein, weil der Provider die Kollektion durch eine „intelligente“ Kollektion ersetzen möchte. final verhindert das. Bei Value-Objekten kann man über Unveränderlichkeit diskutieren, aber Kollektionen von Beziehungen sind nicht der Bereich, in dem man mit strengen Einschränkungen beginnen sollte.

Und schließlich ein wichtiger Punkt zum „Ersetzen der gesamten Kollektion“. Manchmal wird ein Setter geschrieben:

public void setProducts(List<Product> products) {
    this.products = products;
}

Formal funktioniert das, aber methodisch (und häufig auch praktisch) ist es gefährlich. Indem Sie die gesamte Liste ersetzen, zerstören Sie leicht die internen Erwartungen des ORM und verlieren die Kontrolle darüber, welche Elemente entfernt, welche hinzugefügt wurden und was das für die Beziehung bedeutet. Es ist viel gesünder, die Kollektion als „lebenden Container“ zu behandeln und ihren Inhalt sorgfältig zu verwalten. Für den Moment reicht es zu merken: Die Kollektion ist Teil der Beziehung, kein zufälliges Listenfeld, daher sollte sie nicht gedankenlos vollständig ersetzt werden.

7. Minikarte des Projekts: Beziehungen und Felder

Um die Idee zu festigen, ist es hilfreich, nicht abstrakte „Parent/Child“ im Kopf zu haben, sondern die konkreten Beziehungen unseres mini-shop. Wir haben im Wesentlichen zwei „Lern-Schaufenster“, an denen wir unser Gehirn trainieren werden:

  1. Category ↔ Product
  2. CustomerOrder ↔ OrderItem

Sie ähneln sich in der Form (Eins-zu-Viele), haben aber unterschiedliche Bedeutungen in der Fachdomäne. Heute besprechen wir noch keine Kaskaden und Lebenszyklen, aber es ist wichtig zu sehen: Die Mechanik von mappedBy ist dieselbe.

Eine kleine Tabelle, die hilft, nichts zu verwechseln:

Beziehung Feld des Kindes (FK-Seite) Annotation Feld des Elternteils (Kollektion) Annotation
Category ↔ Product Product.category @ManyToOne Category.products @OneToMany(mappedBy = "category")
CustomerOrder ↔ OrderItem OrderItem.customerOrder @ManyToOne CustomerOrder.items @OneToMany(mappedBy = "customerOrder")

Wenn Sie es noch anschaulicher möchten, können Sie auch dieses Schema im Kopf behalten:

flowchart TD
    Category["Category (parent)"]
    Product["Product (child)"]
    CustomerOrder["CustomerOrder (parent)"]
    OrderItem["OrderItem (child)"]

    Product -- "FK: category_id" --> Category
    OrderItem -- "FK: customer_order_id" --> CustomerOrder

    Category -. "products (mappedBy: category)" .-> Product
    CustomerOrder -. "items (mappedBy: customerOrder)" .-> OrderItem

Die gestrichelten Linien zeigen bewusst die „Rückwärtsnavigation“. Sie existiert im Objektmodell, basiert aber physisch auf dem FK im Kind.

Und genau das vergisst man leicht, wenn man nur auf den Java-Code schaut: Die Kollektion im Elternteil sieht aus wie „Ich habe hier eine Liste von Kindern“, aber hinter dieser Liste steckt, dass die Kinder den FK speichern. Die Kollektion ist im Wesentlichen eine bequeme Fassade für die Abfrage „Gib mir alle Kinder, die auf mich verweisen“.

8. Typische Fehler bei der Arbeit mit @OneToMany

Fehler mit @OneToMany und mappedBy entstehen oft nicht, weil der Student „unaufmerksam“ ist, sondern weil das Gehirn versucht, zwei Welten gleichzeitig zu vereinen: die objektorientierte (mit Referenzen und Kollektionen) und die relationale (mit Fremdschlüsseln und JOINs). Das ist normal. Aber es gibt ein paar Fallstricke, auf die man besser im Klassenzimmer tritt als in einem kommerziellen Projekt in der Produktion.

Fehler Nr. 1: In mappedBy den Spaltennamen statt des Feldnamen schreiben.
Das ist der häufigste Patzer. Sie sehen in der Datenbank category_id und möchten logischerweise mappedBy = "category_id" schreiben. Aber JPA sucht ein Feld in der Java-Klasse des Kindes, nicht eine Spalte. Richtig ist mappedBy = "category", weil das Feld in Product category heißt. Wenn man sich die Regel merkt „mappedBy = Name des Felds auf der @ManyToOne-Seite“, ist man fast immer gerettet.

Fehler Nr. 2: mappedBy vergessen und versehentlich eine Join-Tabelle erhalten.
Wenn mappedBy fehlt, nimmt das ORM an, dass @OneToMany eine eigenständige Beziehung ist, die irgendwo gespeichert werden muss. Sehr oft ist das Ergebnis eine überflüssige Verknüpfungstabelle. Es sieht so aus, als ob alles „läuft“, die Anwendung startet, aber das Schema bläht sich auf und führt ein Eigenleben. In einem Lernprojekt ist das besonders tückisch: Sie können das Problem lange übersehen und sich dann wundern, warum eine seltsame Tabelle in der Datenbank aufgetaucht ist.

Fehler Nr. 3: Die Kollektion null lassen und im unpassendsten Moment eine NullPointerException erhalten.
Wenn products oder items nicht initialisiert sind, führt jeder Aufruf von category.getProducts().add(...) zu einem „Bumm“ auf der Stelle. Und dieser Fehler passiert immer dann, wenn Sie schon müde sind und sicher sind, dass „hier ist ja alles klar“. Die Initialisierung mit new ArrayList<>() direkt im Feld ist eine kleine Angewohnheit, die viel Zeit spart.

Fehler Nr. 4: Denken, dass @OneToMany einen zweiten FK erstellt oder die „Beziehung selbstständig schreibt“.
Intuitiv möchte man glauben, dass wenn Category ein products hat, das Hinzufügen eines Produkts zu dieser Liste automatisch den richtigen FK setzt. In der Realität ist es etwas komplizierter: Die Beziehung wird auf der Kind-Seite gespeichert, und dort lebt die „echte“ Referenz. Die Kollektion dient der Navigation, aber der FK lebt weiterhin im Kind. Daher endet die Erwartung „es reicht, in die Liste einzufügen, den Rest errät JPA schon“ fast immer mit einer unangenehmen Überraschung.

Fehler Nr. 5: Versuchen, das Modell zu „vereinfachen“ und statt der Beziehung nur categoryId in Product zu speichern.
Manchmal scheint die Objektreferenz ein überflüssiger Luxus zu sein, und es ist einfacher, einen Long categoryId zu halten. Aber dann verlieren Sie die eigentliche Idee des ORM-Modells: Sie drücken die Beziehung nicht mehr als Teil der Domäne aus und verwandeln die Entität in eine halb-SQL-DTO. In unserem Kurs lernen wir bewusst, Beziehungen als Referenzen auf Entitäten zu modellieren, nicht als eine Ansammlung von „IDs“, denn das ist die Grundlage für das weitere Verständnis des JPA-Verhaltens.

1
Aufgabe
Spring Data JPA, Level 8, Lektion 1
Gesperrt
Produktsammlung der Kategorie über `@OneToMany(mappedBy = "category")`
Produktsammlung der Kategorie über `@OneToMany(mappedBy = "category")`
1
Aufgabe
Spring Data JPA, Level 8, Lektion 1
Gesperrt
Bestellpositionen ueber `@OneToMany(mappedBy = "customerOrder")`
Bestellpositionen ueber `@OneToMany(mappedBy = "customerOrder")`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION