Wenn du in Java mit Datenbanken arbeitest, liegt ein Gedanke ziemlich nahe: Eine Klasse entspricht einer Tabelle und ein Objekt entspricht einer Zeile dieser Tabelle. Auf den ersten Blick passt das erstaunlich gut. Eine Tabelle CUSTOMER besitzt Spalten wie ID, NAME und EMAIL, während eine Klasse Customer Felder wie id, name und email besitzt.
Damit kann man arbeiten. Genau deshalb ist diese Vorstellung so verführerisch.
Das Problem ist nur: Sie ist fachlich nicht richtig.
Eine Tabelle und ein Objekt beschreiben Daten auf völlig unterschiedliche Weise. Eine relationale Datenbank organisiert Informationen in Tabellen, Zeilen, Spalten und Beziehungen zwischen diesen Tabellen. Objektorientierter Code organisiert dagegen Zustand und Verhalten in Objekten, die miteinander interagieren.
Natürlich können beide Welten miteinander verbunden werden. JPA und andere ORM-Techniken machen genau das. Aber Mapping bedeutet nicht, dass beide Modelle identisch sind.
Wer diesen Unterschied früh versteht, schreibt später deutlich besseren objektorientierten Code.
Eine Tabellenzeile speichert Daten - ein Objekt modelliert etwas
Nehmen wir zunächst ein bewusst einfaches Beispiel. In einer Datenbank könnte eine Kundentabelle so aussehen:
CUSTOMER
ID | NAME | EMAIL
1 | Anna Beispiel | anna@example.org
2 | Max Muster | max@example.org
Wenn du diese Tabelle anschaust, erkennst du sofort ihre Aufgabe. Sie speichert Werte. Zu einer bestimmten ID gehören ein Name und eine E-Mail-Adresse.
Die naheliegende Java-Klasse dazu könnte so aussehen:
public class Customer {
private Long id;
private String name;
private String email;
}
Das ist zunächst nicht falsch. Es ist nur noch kaum objektorientiert.
Diese Klasse besitzt zwar die Form eines Objekts, verhält sich aber praktisch wie eine Tabellenzeile. Sie ist lediglich ein Container für Werte.
Ein Objekt kann aber mehr ausdrücken.
Angenommen, die E-Mail-Adresse eines Kunden darf nicht beliebig geändert werden. Eine leere Adresse soll beispielsweise nicht erlaubt sein.
Dann könnte das Objekt selbst dafür sorgen:
public class Customer {
private final Long id;
private String name;
private String email;
public Customer(Long id, String name, String email) {
this.id = id;
this.name = name;
this.changeEmail(email);
}
public void changeEmail(String email) {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("E-Mail darf nicht leer sein.");
}
this.email = email;
}
}
Damit verändert sich die Bedeutung der Klasse.
Der Customer besitzt jetzt nicht nur Daten. Er kontrolliert auch, wie sein Zustand verändert werden darf. Die Regel zur E-Mail-Adresse gehört zum Kundenobjekt, weil sie Teil seiner fachlichen Regeln ist.
Eine Datenbankzeile kennt dagegen keine Methode changeEmail(). Sie kennt nur den gespeicherten Wert.
Natürlich kann auch die Datenbank Regeln durch Constraints erzwingen. Ein NOT NULL Constraint kann beispielsweise verhindern, dass überhaupt kein Wert gespeichert wird. Das ist sinnvoll und sollte nicht durch Objektorientierung ersetzt werden.
Die Aufgabe ist aber eine andere.
Die Datenbank schützt die Konsistenz der gespeicherten Daten. Das Objekt modelliert die fachliche Bedeutung und das Verhalten innerhalb deiner Anwendung.
Beziehungen zeigen den Unterschied noch deutlicher
Interessanter wird es, sobald mehrere Dinge zusammengehören.
Ein Kunde kann beispielsweise mehrere Bestellungen besitzen. Im relationalen Modell werden dafür normalerweise zwei Tabellen verwendet:
CUSTOMER
ID | NAME
1 | Anna Beispiel
ORDERS
ID | CUSTOMER_ID | TOTAL
101 | 1 | 49.90
102 | 1 | 17.50
Die Beziehung entsteht über CUSTOMER_ID.
Das relationale Modell sagt also im Grunde: In dieser Zeile steht eine ID, die auf eine andere Zeile verweist.
Im Java-Code musst du diese technische Struktur nicht nachbauen.
public class Customer {
private final Long id;
private final String name;
private final List<Order> orders = new ArrayList<>();
public Customer(Long id, String name) {
this.id = id;
this.name = name;
}
public void addOrder(Order order) {
this.orders.add(order);
}
public BigDecimal calculateTotalOrderValue() {
return this.orders.stream()
.map(Order::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Eine Bestellung könnte entsprechend aussehen:
public class Order {
private final Long id;
private final BigDecimal total;
public Order(Long id, BigDecimal total) {
this.id = id;
this.total = total;
}
public BigDecimal getTotal() {
return this.total;
}
}
Jetzt gibt es im Customer keine customerId, über die irgendwie eine Beziehung hergestellt werden muss. Das Objekt besitzt direkt eine Liste von Order-Objekten.
Genau hier werden die unterschiedlichen Denkmodelle sichtbar.
In der Datenbank denkst du in Fremdschlüsseln.
Im Objektmodell denkst du in Objektreferenzen.
Die Datenbank braucht CUSTOMER_ID, weil sie Beziehungen zwischen Zeilen herstellen muss. Dein Java-Code braucht diese ID für die fachliche Modellierung der Beziehung dagegen nicht. Dort kann ein Customer direkt mit einem Order arbeiten.
Wenn JPA ins Spiel kommt, verbindet es diese beiden Darstellungen miteinander:
@Entity
public class Customer {
@Id
private Long id;
private String name;
@OneToMany
private List<Order> orders = new ArrayList<>();
}
Auf Java-Seite arbeitest du mit einer List<Order>. In der Datenbank wird diese Beziehung trotzdem über Tabellen und Schlüssel abgebildet.
Genau darin besteht ein großer Teil der Arbeit eines ORM wie JPA: Es übersetzt zwischen zwei unterschiedlichen Modellen.
Wären Objekte tatsächlich einfach Tabellen, müsste es diese Übersetzung gar nicht geben.
Dein Objektmodell muss nicht wie dein Datenbankschema aussehen
Besonders problematisch wird die Vorstellung "Klasse gleich Tabelle", wenn du anfängst, dein gesamtes Java-Modell nach der Datenbank auszurichten.
Angenommen, du speicherst die Adresse eines Kunden in mehreren Spalten:
CUSTOMER
ID
NAME
STREET
ZIP_CODE
CITY
Dann könntest du diese Struktur direkt übernehmen:
public class Customer {
private Long id;
private String name;
private String street;
private String zipCode;
private String city;
}
Technisch funktioniert das.
Aus objektorientierter Sicht kann eine Adresse aber ein eigenes Konzept sein:
public class Address {
private final String street;
private final String zipCode;
private final String city;
public Address(String street, String zipCode, String city) {
this.street = street;
this.zipCode = zipCode;
this.city = city;
}
}
Der Kunde enthält dann nicht drei lose zusammengehörende Strings, sondern eine Adresse:
public class Customer {
private final Long id;
private String name;
private Address address;
public Customer(Long id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
public void changeAddress(Address address) {
if (address == null) {
throw new IllegalArgumentException("Adresse darf nicht null sein.");
}
this.address = address;
}
}
In der Datenbank können die drei Werte trotzdem weiterhin in derselben CUSTOMER-Tabelle liegen.
Mit JPA lässt sich ein solches Objekt beispielsweise als @Embeddable modellieren:
@Embeddable
public class Address {
private String street;
private String zipCode;
private String city;
}
und anschließend in der Entity verwenden:
@Embedded
private Address address;
Eine eigene Java-Klasse benötigt also nicht zwangsläufig eine eigene Datenbanktabelle.
Umgekehrt muss auch nicht jede Tabelle automatisch eine fachlich relevante Java-Klasse darstellen. Manche Tabellen existieren beispielsweise hauptsächlich zur technischen Abbildung von Beziehungen oder für andere Persistenzanforderungen.
Das Datenbankschema beantwortet die Frage: Wie sollen diese Daten relational gespeichert werden?
Das Objektmodell beantwortet eine andere Frage: Welche fachlichen Konzepte gibt es in meiner Anwendung und wie verhalten sie sich?
Diese Fragen hängen zusammen, sind aber nicht identisch.
Wenn du dein Objektmodell ausschließlich aus Tabellen ableitest, entstehen deshalb schnell Klassen mit vielen Feldern, Gettern und Settern, aber kaum echtem Verhalten. Der Java-Code wird dann zu einer objektorientiert aussehenden Darstellung des Datenbankschemas.
Java kann deutlich mehr.
Ein gutes Objekt darf Regeln durchsetzen. Es darf Berechnungen durchführen. Es darf verhindern, dass ungültige Zustände entstehen. Es darf andere Objekte enthalten und mit ihnen zusammenarbeiten.
Die Datenbank bleibt trotzdem wichtig. Sie sorgt dafür, dass Daten dauerhaft, konsistent und effizient gespeichert werden können.
Du solltest nur nicht verlangen, dass beide Modelle gleich aussehen.
Fazit
"Eine Klasse ist eine Tabelle und ein Objekt ist eine Tabellenzeile" kann als erste Vorstellung beim Einstieg in JPA hilfreich wirken. Als dauerhaftes Verständnis von Objektorientierung führt sie aber in die falsche Richtung.
Eine Tabelle organisiert gespeicherte Daten. Ein Objekt modelliert einen Zustand zusammen mit dem Verhalten und den Regeln, die zu diesem Zustand gehören.
Eine relationale Datenbank stellt Beziehungen mit Schlüsseln her. Java stellt Beziehungen zwischen Objekten mit Referenzen her.
Eine Tabelle kann mehrere Werte enthalten, die im Objektmodell sinnvoll zu einem eigenen Objekt wie Address zusammengefasst werden. Gleichzeitig kann ein Objekt Verhalten besitzen, für das es in einer Datenbank überhaupt keine direkte Entsprechung gibt.
JPA verbindet diese Welten miteinander. Genau deshalb sprechen wir von Object Relational Mapping.
Wenn du also das nächste Mal eine neue Klasse modellierst, sollte die erste Frage nicht lauten: "Wie sieht die Tabelle dazu aus?"
Die bessere Frage lautet: "Was stellt dieses Objekt in meiner Anwendung dar und was kann es tun?"
