1. @Column als Vertrag
Wenn man das erste Entity zu schreiben anfängt, denkt man sehr gerne so: „Na ja, Feld String code ist doch sowieso ein String. Warum soll ich irgendwas angeben?" Der Gedanke ist normal, besonders wenn du gerade @Entity und @Id bezwungen hast und das Gefühl hast, dir Tee mit Keksen verdient zu haben. Aber @Column ist nicht für die Schönheit da, sondern um die Regeln der Datenspeicherung festzulegen: wie das Feld in der Tabelle heißt, ob es NULL sein darf, ob es eindeutig sein muss, wie lang ein String sein soll und mit welcher Genauigkeit Zahlen wie Preise gespeichert werden.
Es ist wichtig, den Sinn von @Column zu verstehen: Das ist kein „Befehl an Hibernate", sondern dein Vertrag zwischen Java-Code und SQL-Schema. Auch wenn du gerade automatische Schema-Generierung verwendest, wird die Datenbank in der Realität trotzdem nach diesen Regeln leben. Wenn der Vertrag unklar ist, kommt die Unklarheit auch in die Daten.
Mini-Beispiel: Nicht alles muss annotiert werden, aber Schlüsselfelder lohnt es sich normalerweise zu annotieren.
import jakarta.persistence.Column;
public class Category {
// Schlüsselfeld der Domäne: Pflichtfeld, Eindeutigkeit und maximale Länge festlegen
@Column(nullable = false, unique = true, length = 50)
private String code;
}
Hier haben wir in einer einzigen Zeile viel mehr über das Feld gesagt als „das ist ein String": Es ist ein Pflichtfeld, eindeutig und hat eine sinnvolle Längenbeschränkung.
2. Spaltenname: @Column(name = ...)
Namen sind eine eigene Disziplin. In Java schreibt man normalerweise camelCase, in SQL trifft man häufiger snake_case an, und dann gibt es noch Legacy-Schemata mit Namen wie CAT_CD — das ist kein Kätzchen, sondern „category code". In der JPA-Welt gibt es Standardregeln, die versuchen, den Spaltennamen aus dem Feldnamen abzuleiten. Manchmal passt alles perfekt, und manchmal schaut man auf den SQL-Log und denkt: „Ist das wirklich meine Anwendung und nicht eine fremde?"
Der Parameter name in @Column ist ein Weg zu sagen: „Hör auf zu raten, hier ist der genaue Name." Und das ist nicht nur aus Schönheitsgründen nützlich, sondern auch als Schutz vor Refactoring: Du kannst ein Java-Feld umbenennen, aber den Spaltennamen behalten, und das Datenbankschema beginnt sich nicht „spontan zu ändern".
Einfaches Beispiel, wenn Java-Name und SQL-Name voneinander abweichen:
import jakarta.persistence.Column;
public class Category {
// Datenbankname explizit festlegen: Refactoring des Felds in Java soll die Spalte nicht "versehentlich" umbenennen
@Column(name = "category_code", nullable = false, unique = true, length = 50)
private String code;
}
Wann name nötig ist und wann Standardwerte reichen
Am Anfang neigt man leicht zu Extremen: Entweder „alles annotieren" oder „nichts annotieren". Beide Ansätze führen schnell zu Schmerz, nur auf unterschiedliche Weise. Wenn man nichts festlegt, wird man zum Gefangenen der Naming Strategy und zufälliger Umbenennungen. Wenn man alles ohne Sinn annotiert, beginnt der Code wie eine Abschlussarbeit über Annotationen auszusehen.
Die gesunde Logik lautet: Wenn ein Feld ein wichtiger Teil des Vertrags ist (Kategoriecode, Artikel-SKU, Preis, Status), sollte man die Spalte lieber explizit benennen oder zumindest explizit Einschränkungen setzen. Für sekundäre Felder, die nicht an Eindeutigkeit beteiligt sind, keine strengen Größen haben und für den Vertrag nicht kritisch sind, reichen manchmal die Standardwerte. Aber sobald man sich denkt „dieses Feld ist sehr wichtig", ist das der Moment für @Column(...).
Kleines Beispiel, wo der Name auf dem Standard bleiben kann, aber Einschränkungen dennoch festgelegt werden:
import jakarta.persistence.Column;
public class Category {
// Der Spaltenname kann auf dem Standard bleiben, aber Länge und Pflichtfeld besser vertraglich festlegen
@Column(nullable = false, length = 100)
private String name;
}
Expliziter Name und Schutz vor SQL-Schlüsselwörtern
Es gibt auch einen rein menschlichen Faktor: Wir benennen Felder so, wie es uns bequem ist, aber SQL liebt „reservierte Wörter". Eine Spalte order oder user zu nennen ist fast so, als ob man eine Bananenschale auf der Treppe liegen lässt: Irgendjemand wird darüber ausrutschen. Wenn man im Voraus @Column(name = "customer_order") oder @Table(name = "customer_order") festlegt, ist die Wahrscheinlichkeit eines Kollisionsgeschäfts mit der SQL-Syntax deutlich geringer.
Wir bauen gerade kein komplexes Schema und wählen keine ideale Namenskonvention auf FAANG-Niveau, aber der Gedanke ist einfach: Ein Name in SQL ist Teil des Vertrags, und name = "..." ermöglicht es, diesen Vertrag explizit statt implizit zu machen.
3. nullable: Pflichtfeld und NULL
nullable scheint elementar: nullable = false — Pflichtfeld, nullable = true — kein Pflichtfeld. Aber genau hier beginnen die ersten echten Probleme. In Java bedeutet null „kein Wert vorhanden", und in SQL ist NULL ein eigener Zustand einer Spalte. Die Welten ähneln sich, sind aber nicht identisch: Java kann es stillschweigend erlauben, einen Wert zu vergessen, während die Datenbank ehrlich einen Insert verweigert und sagt: „Du hast versprochen, dass hier immer ein Wert ist."
Deshalb ist @Column(nullable = false) ein Weg festzulegen, dass das Feld nicht nur „wünschenswert", sondern ein Pflichtbestandteil der Zeile ist. Und wenn du versuchst, eine Entität ohne diesen Wert zu speichern, bekommst du einen Fehler — und das ist gut: Besser das Problem sofort sehen, als monatelang fehlerhafte Daten zu speichern.
Beispiel aus unserer Domäne: Der Kategoriecode muss immer vorhanden sein.
import jakarta.persistence.Column;
public class Category {
// Wenn das Feld dem fachlichen Sinn nach ausgefüllt sein muss — soll es NOT NULL in der DB sein
@Column(nullable = false, unique = true, length = 50)
private String code;
}
Wie NULL in die Datenbank gelangt
Ein häufiges Einsteiger-Szenario: Du hast ein Objekt new Category() erstellt, ein paar Felder befüllt, und eines vergessen. In Java sieht das nicht wie ein Verbrechen aus — der Compiler schweigt. Aber beim Speichern in der Datenbank stellt sich heraus, dass „vergessen zu befüllen" und „ich möchte NULL speichern" für die Datenbank fast dasselbe sind.
Wenn die Spalte „NOT NULL" ist, verweigert die Datenbank den Insert. Wenn die Spalte NULL erlaubt, schreibt die Datenbank NULL, und du wirst dich später wundern, warum im Kategorienbericht plötzlich eine Kategorie ohne Namen erscheint. Und ja, das passiert normalerweise am Freitagabend.
Kleines DDL-Beispiel, das aus nullable = false entstehen kann:
create table category (
id bigint not null,
code varchar(50) not null, -- Code ist Pflichtfeld: ohne ihn ist die Zeile "ungültig" laut Vertrag
name varchar(100) not null -- Name ist ebenfalls Pflichtfeld: das ist NOT NULL auf Schema-Ebene
);
Auch wenn DDL noch nicht „vertraut" klingt, ist das Schlüsselwort hier sofort zu sehen: not null. Das ist die Pflichtfeld-Eigenschaft.
Nuance bei Java-Typen: Primitive und null
Hier erscheint eine „kleine Tücke" von Java, die man im Hinterkopf behalten sollte. Wenn ein Feld vom Typ boolean ist, kann es per Definition kein null sein. Es hat immer einen Wert: standardmäßig false. Aber Boolean kann bereits null sein. Dasselbe gilt für int/Integer, long/Long und so weiter.
Das hängt direkt mit nullable zusammen. Wenn du möchtest, dass eine Spalte NULL sein kann, ist es logisch, einen Typ zu wählen, der das erlaubt: Boolean, Integer. Wenn das Feld ein Pflichtfeld sein soll, kann ein primitiver Typ praktisch sein — aber nur wenn der Standardwert (false, 0) für die Domäne wirklich sinnvoll ist.
Wir machen diese Vorlesung nicht zu einer eigenen Lektion über die Wahl von Java-Typen, aber es ist wichtig, den Zusammenhang zu sehen: nullable ist nicht nur eine SQL-Angelegenheit, sondern auch eine Frage, was du wirklich im Java-Feld speichern kannst.
4. unique: Eindeutigkeit als Regel
unique = true wird von Einsteigern oft so aufgefasst: „Na, soll es halt so sein, schlimmer wird's nicht." In der Praxis wird es schlimmer, wenn man Eindeutigkeit gedankenlos aktiviert: Man kann leicht völlig legitime Datenszenarien verbieten. Aber wenn Eindeutigkeit wirklich eine Geschäftsregel ist — zum Beispiel für die sku eines Produkts oder den code einer Kategorie —, dann macht unique die Datenbank zu einem zuverlässigen Wächter: Duplikate kommen nicht durch, auch wenn jemand im Code einen Fehler gemacht hat.
Für unseren Mini-Shop ist das sehr natürlich: Eine Kategorie hat einen Code, und der soll sich nicht wiederholen. Ein Produkt hat eine sku, und die soll sich auch nicht wiederholen. Wenn man Duplikate zulässt, kann man später nicht mehr verstehen, „welches genau das Produkt" ist, das man verkauft, egal wie sehr man fragt.
Beispiel:
import jakarta.persistence.Column;
public class Product {
// SKU ist die Produktkennung für die Außenwelt, Duplikate sind nicht zulässig
@Column(nullable = false, unique = true, length = 64)
private String sku;
}
Was in der Datenbank bei unique = true passiert
Es ist wichtig, das pragmatisch zu verstehen: unique = true ist keine „Prüfung in Java", sondern eine Bitte, eine Unique Constraint auf Datenbankebene zu erstellen, wenn das Schema generiert wird. In der SQL-Welt sieht das normalerweise wie eine UNIQUE-Einschränkung aus, und oft wird dabei automatisch ein Index erstellt, damit die Eindeutigkeitsprüfung schnell ist.
Ungefähres DDL-Fragment:
create table product (
id bigint not null,
sku varchar(64) not null,
constraint uk_product_sku unique (sku) -- die DB lässt keine zwei gleichen SKUs durch
);
Und hier beginnt echtes Engineering: Die Datenbank lässt kein zweites Produkt mit derselben sku einfügen. Ja, du bekommst einen Fehler. Aber dieser Fehler ist kein Übel, sondern ein Signal: Du hast einen Dateninvarianten verletzt.
unique = true — nur für eine Spalte
Eine weitere Nuance, die man kennen sollte, um sich nicht zu wundern. @Column(unique = true) eignet sich gut, wenn eine Spalte eindeutig sein soll. Wenn die Geschäftsregel lautet „eindeutig ist ein Wertepaar" — zum Beispiel die Kombination aus country + documentNumber —, ist das schon eine andere Form der Einschränkung und wird anders gelöst.
Im Rahmen unserer ersten Entitäten brauchen wir das nicht, deshalb bleiben wir beim Einfachen und Klaren: eindeutiger code und eindeutige sku. Aber die Grenze ist wichtig zu kennen: unique = true ist kein universeller Knopf für alle Arten von Eindeutigkeit.
5. length: String-Größen
Der Parameter length erscheint so lange als Kleinigkeit, bis man in der Datenbank einen „Kategoriecode" mit 10.000 Zeichen sieht oder einen „Produktnamen", in den jemand versehentlich einen ganzen Wikipedia-Artikel eingefügt hat. Ja, klingt wie ein Witz, aber so etwas passiert wirklich, wenn man Größen nicht einschränkt und den Speicher-Vertrag nicht festlegt.
Standardmäßig bekommen String-Spalten oft varchar(255). Das ist ein historisch beliebter Wert, aber er muss nicht mit deiner Domäne übereinstimmen. Ein Kategoriecode ist normalerweise kurz: 20–50 Zeichen. Ein Produktname — sagen wir bis 120. Eine Kategoriebeschreibung kann länger sein, aber immer noch in einem vernünftigen Rahmen.
Beispiel:
import jakarta.persistence.Column;
public class Category {
// Kurzer „Code" bedeutet kurze Spalte: Obergrenze explizit festlegen
@Column(nullable = false, unique = true, length = 50)
private String code;
// Name ist länger als Code, aber auch mit vernünftiger Obergrenze
@Column(nullable = false, length = 100)
private String name;
}
length als Teil der Vereinbarung
Man könnte denken: „Na ja, die Länge einschränken — das ist doch wegen der Performance." In der Realität, besonders in PostgreSQL, geht die Länge von varchar meistens nicht um Geschwindigkeit, sondern um Datendisziplin. Du sagst der Datenbank und dem Team: „Dieses Feld ist für einen kurzen Code. Wenn du versuchst, dort den Roman von Tolstoi hineinzustopfen — machst du etwas falsch."
Und es gibt noch einen praktischen Bonus: Wenn man das Tabellenschema oder das generierte DDL anschaut, liest sich varchar(50) sofort als „Code", und varchar(500) als „Beschreibung". Das macht das Schema verständlicher, auch ohne Dokumentation. Ja, klingt langweilig, aber in einem echten Projekt bedeutet langweilig normalerweise „zuverlässig".
Lange Texte: nicht einfach length = 5000 aus Gewohnheit
Wenn der Text wirklich groß ist — zum Beispiel eine mehrseitige Produktbeschreibung —, werden normalerweise andere Speicheransätze verwendet. In JPA gibt es dafür eigene Mechanismen, die sich etwas von normalem varchar unterscheiden. Aber für unseren grundlegenden Start brauchen wir das nicht: Wir bauen kleine, verständliche Entitäten, und für description reichen uns rund 500 Zeichen.
Beispiel:
import jakarta.persistence.Column;
public class Category {
// Beschreibung ist optional: nullable ist standardmäßig true, aber die Größe trotzdem einschränken
@Column(length = 500)
private String description;
}
Das ist ein typischer vernünftiger Kompromiss: Das Feld kann leer sein, und wenn es befüllt ist — wird es kein endloses Textlager.
6. precision und scale: Zahlen und Geld
Wenn Strings der Ort sind, wo man den Vertrag am häufigsten vergisst, dann sind Zahlen der Ort, wo man sich am häufigsten selbst täuscht. Besonders Geld. Ein Einsteiger möchte oft den Preis als double speichern, weil „das doch eine Zahl mit Komma ist". Und dann sieht man 19.99999999997 und bemerkt, dass der Computer kreativ sein kann.
Für Geld verwendet man in Java normalerweise BigDecimal, und in der Datenbank einen Typ wie numeric/decimal. Und hier kommen precision und scale ins Spiel. precision ist die Gesamtzahl der Ziffern, scale ist die Anzahl der Nachkommastellen. Für normale Preise ist die Kombination 12,2 eine häufige und verständliche Wahl: bis zu 10 Stellen vor dem Komma und 2 danach.
Beispiel für das Preis-Mapping:
import jakarta.persistence.Column;
import java.math.BigDecimal;
public class Product {
// Geld vorhersehbar speichern: 2 Nachkommastellen — Teil des Speicher-Vertrags
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal price;
}
numeric(12,2) in der Praxis
Stell dir den Preis 9999999999.99 vor. Das sind 10 Stellen vor dem Komma und 2 danach — insgesamt 12 Stellen. Er passt in numeric(12,2). Aber 99999999999.99 hat bereits 11 Stellen vor dem Komma — insgesamt 13 Stellen — und passt nicht mehr.
Das ist keine Mathematik um der Mathematik willen. Das ist ein Weg, vorab zu vereinbaren, welche Preise in deiner Domäne zulässig sind. In einem Mini-Shop verkaufen wir keine Planeten, deshalb erscheint precision = 12 vernünftig.
Ungefähres DDL-Fragment:
price numeric(12, 2) not null
BigDecimal: String vs. double
Das ist schon fast Java Core, aber zu nützlich, um es zu verschweigen. BigDecimal kann aus einem String und aus einem double erstellt werden. Aus einem String — vorhersehbar. Aus einem double — manchmal mit Überraschungen, weil double die Zahl im binären Format speichert und nicht alle Dezimalbrüche dort genau darstellbar sind.
import java.math.BigDecimal;
// Korrekt für Geld: BigDecimal aus String erstellen, ohne binäre "Reste"
BigDecimal ok = new BigDecimal("49.90");
// Gefährlich für Geld: aus double können unerwartete Dezimal-Artefakte entstehen
BigDecimal weird = new BigDecimal(49.90);
System.out.println(ok); // 49.90
System.out.println(weird); // 49.899999999999998578914... (ungefähr)
Der Sinn liegt nicht darin, sich einen „magischen String" zu merken, sondern darin, dass Geld Genauigkeit liebt. Und precision/scale in @Column machen die Genauigkeit zu einem Teil des SQL-Vertrags, damit du nicht versehentlich anfängst, die „Produktkosten" mit 17 Nachkommastellen zu speichern.
7. @Column im generierten SQL
Jetzt kommt der angenehmste Moment: Wir haben die Parameter von @Column bereits mit verständlichen SQL-Konzepten verknüpft (NOT NULL, UNIQUE, varchar(n), numeric(p,s)). Aber das alles bleibt Theorie, bis man es im generierten SQL gesehen hat. Hibernate und JPA sind keine Gedankenleser: Sie lesen nicht deine Gedanken über das Schema. Sie lesen das Mapping und generieren daraus SQL — entweder DDL zum Erstellen von Tabellen, wenn automatische Generierung aktiviert ist, oder DML zum Einfügen und Aktualisieren von Daten.
Deshalb ist es nützlich, sich einen einfachen Gedanken anzugewöhnen: @Column schreibt man nicht, damit „eine Annotation vorhanden ist", sondern damit man später den SQL-Log aufmachen und sehen kann, was wirklich passiert. Das ist wie bei einem Rezept: Man kann lange lesen, oder man kocht einmal.
Kleines Schema dessen, was konzeptuell passiert:
flowchart TD
A["Java-Feld
private String code"] --> B["@Column(nullable=false, unique=true, length=50)"]
B --> C["SQL-Spalte
code varchar(50) not null"]
C --> D["Einschränkung
unique(code)"]
Und ein DDL-Beispiel, das beim Start der Anwendung in den Logs erscheinen kann:
create table category (
id bigint not null,
code varchar(50) not null,
name varchar(100) not null,
description varchar(500),
active boolean not null,
primary key (id),
constraint uk_category_code unique (code)
);
Ja, die tatsächliche Ausgabe hängt von den Schema-Generierungseinstellungen, der Naming Strategy und der Hibernate-Version ab. Aber der Sinn bleibt eisern: @Column ist der Ort, wo du direkt auf solche Ausdrücke wie not null, unique und Typengrößen Einfluss nimmst.
8. Typische Fehler beim Umgang mit @Column
Fehler mit @Column sind unangenehm, weil sie wie Kleinigkeiten aussehen, aber entweder zur Laufzeit beim Speichern von Daten detonieren oder — schlimmer — still fehlerhafte Daten erzeugen. Einsteiger leiden darunter besonders oft, weil im Code alles ordentlich aussehen kann: Die Java-Klasse kompiliert, die Anwendung startet, und das Problem taucht erst beim ersten echten Szenario auf. Deshalb ist es besser, im Voraus zu wissen, wo diese Stolpersteine liegen.
Fehler Nr. 1: @Column als dekorative Annotation betrachten und für wichtige Felder nichts festlegen.
Wenn code, sku oder price ohne explizite Einschränkungen bleiben, sagt man im Grunde: „Mal sehen, wie's kommt." Dann stellt sich heraus, dass in der Datenbank varchar(255) für den Code ist, NULL dort, wo Werte sein müssen, und unerwartete Duplikate. Das ist kein „Hibernate-Fehler", sondern das Fehlen eines Vertrags.
Fehler Nr. 2: unique = true „auf Vorrat" setzen.
Eindeutigkeit ist kein Stil, sondern eine Geschäftsregel. Wenn man den name einer Kategorie als eindeutig markiert und dann zwei Kategorien „Books" in verschiedenen Sprachen oder Kontexten haben möchte, wird die Datenbank zu Recht sagen „geht nicht". Wenn die Regel nicht eiserhartig ist, hat Eindeutigkeit im Schema nichts zu suchen.
Fehler Nr. 3: nullable = false vergessen und sich dann über NULL in Pflichtfeldern wundern.
Wenn ein Feld dem Sinn nach Pflichtfeld ist, man das aber nicht im Vertrag festlegt, speichert die Datenbank „keinen Wert" als normalen Zustand. Und dann schreibt man überall im Lesecode defensiven Code: if (name == null) .... Das ist so, als ob man einen Regenschirm kauft, nachdem man schon bis auf die Haut durchnässt ist.
Fehler Nr. 4: precision und scale für BigDecimal nicht setzen, besonders für Geld.
Ohne explizites precision/scale riskiert man unvorhersehbare Zahlenspeicherung: Mal Rundung, mal zusätzliche Stellen, mal verschiedene Typen in verschiedenen Datenbanken. Geld und Mengen erfordern Disziplin. Wenn man einen Preis speichert, erkläre sein Format als Teil des Vertrags.
Fehler Nr. 5: Strings dort „auf Standard" lassen, wo die Größe offensichtlich ist.
Ein Kategoriecode sollte fast nie varchar(255) sein. Eine Produkt-SKU auch nicht. Wenn die Größe bekannt ist, macht length die Daten ordentlicher und das Schema verständlicher. Außerdem hilft es, Fehler früher zu erkennen: Wenn die Anwendung versucht, einen zu langen Code einzutragen, erfährt man es sofort und nicht erst nach einer Woche.
GO TO FULL VERSION