# Entities

Die Grundlage für Persistenz sind Entities. Dies sind mit @Entity (javax.persistence.Entity) annotierte Klassen, welche die Struktur der Datenbank widerspiegeln – sie entsprechen Tabellen. Der Klassenname wird standardmäßig auch als Tabellenname gemappt – der Anfangsbuchstabe in der Datenbank wird meistens allerdings in Kleinschreibung umgewandelt.

Entities werden als normale Klassen definiert. Die Properties entsprechen dabei den Spalten der Datenbank.

@Entity
class Person

# Properties, @Id

Die Properties der Entity-Klassen entsprechen den Tabellenspalten und werden im Code mit dem entsprechenden Typ angegeben (beispielsweise String für VARCHAR Spalten). Im Normalfall sind die Daten änderbar, also werden die Properties als var deklariert. Der Propertyname wird standardmäßig als Spaltenname gemappt – ggf. mit Umwandlung in Kleinschreibung.

@Entity
class Person(
  var name: String
) {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
  
  var heightInCm: Float? = null
}

Properties ohne Standardwerte werden im Konstruktor definiert, solche mit Initialisierern im Klassenbody. So ist sichergestellt, dass benötigte Properties immer gefüllt werden müssen.

Entities müssen immer eine ID haben, die einzigartig innerhalb der Tabelle ist und ein Objekt dieser somit eineindeutig identifiziert. Im Normalfall (wenn man das Datenmodell neu aufsetzt und nicht ein vorhandenes abbilden muss) ist diese Property vom Typ Long und nicht nullable.

Um zu signalisieren, dass diese Property der Primärschlüssel der Tabelle ist, wird es im Code mit @Id (javax.persistence.Id) markiert und als val (also nicht änderbar) deklariert. Da sie initialisiert werden muss (sonst gibt es Compilefehler), wird hier 0 verwendet. Hibernate ersetzt diesen Wert bei neuen Entities mit dem automatisch generierten Wert. Beim Laden von Entities steht auch der korrekte Wert in der Property.

Die meisten Datenbanken haben Mechanismen um automatisch fortlaufende, eindeutige Werte für den Primärschlüssel einer Tabelle zu generieren. Damit Spring bzw. Hibernate die Spalte passend verdrahten und sich je nach Datenbank um das automatische Zuweisen einer neuen ID kümmern, wird zusätzlich die Annotation @GeneratedValue (javax.persistence.GeneratedValue) benutzt.

TIP

Die JPA (Jakarta (früher Java) Persistence API) kennt mehrere Verfahren, nach denen eine ID automatisch erzeugt werden kann. Falls die angebundene Datenbank Sequenzen unterstützt werden meistens eben jene benutzt (GenerationType.SEQUENCE).

Werden Sequenzen nicht unterstützt (z. B. MySQL, MariaDB), wird dann eine Hilfstabelle erzeugt (GenerationType.TABLE). Das ist bei größeren Projekten oder vielen Neuerzeugungen allerdings der Performance nicht zuträglich.

Da die meisten Datenbanken so etwas wie AUTO_INCREMENT unterstützen, sollte dieses Verfahren gewählt werden (GenerationType.IDENTITY). Die Einsparung der Hilfstabelle macht auch spätere Migrationen einfacher.

# @Column

Der Spaltenname entspricht standardmäßig dem Propertynamen. Möchte man im Code einen anderen Bezeichner verwenden, weil beispielsweise der Spaltenname Zeichen enthält, die für den Bezeichner im Code nicht erlaubt sind (z. B. Minus), kann man das Mapping mittels @Column (javax.persistence.Column) überschreiben:

WARNING

Hibernate liest nicht aus, ob der Typ in Kotlin nullable ist oder nicht. Es legt die Datenbank-Spalten in den meisten Fällen als nullable an. Das bedeutet, dass es zu Diskrepanzen kommen kann, wenn in der Datenbank NULL Werte für non-nullable Properties hinterlegt sind (passiert aber nur, falls die Daten von externer Stelle in die Datenbank geschrieben wurden).

Eine Spalte expizit als non-nullable kennzeichnen, geht auch mit @Column:

@Column(nullable = false)

@Column hat auch noch andere Parameter, wie length und precision.

# @Table

Auch falls sich der Tabellenname vom Klassenname unterscheidet, kann dies angegeben werden. Hierfür gibt es die Annotation @Table (javax.persistence.Table), welche an die Klasse gepackt wird.

Hierüber können auch zusätzliche Indizes angelegt werden. Es können mehrere Indizes je Entity / Tabelle angelegt und optional auch benannt werden (Hibernate erzeugt sonst einen eigenen, kryptischen Namen). Sie können sich auch über mehrere Spalten erstrecken.

Es ist darauf zu achten, dass in columnList der Spaltenname in der Datenbank und nicht der Propertyname im Code verwendet wird, falls diese sich unterscheiden.

Der Index für die @Id Property muss nicht explizit angegeben werden, dieser wird automatisch erstellt. Auch andere Indizes (beispielweise bei Relationen) werden automatisch erstellt.

@Entity
@Table(indexes = [
  Index(columnList = "firstName, lastName"),
  Index(name = "dob_index", columnList = "birth-date")
])
class Person

# Temporale Typen

Es werden die java.time Typen unterstützt:

  • Instant
  • LocalTime
  • LocalDate
  • LocalDateTime
  • OffsetTime
  • OffsetDateTime
  • ZonedDateTime

Der alte Typ java.util.Date sollte nicht verwendet werden.

# Enums

Enums werden standardmäßig anhand ihrer Ordnungszahl (also numerisch) gespeichert. Das wird zum Problem, wenn man im Code zwischendrin neue Enum-Werte einfügt oder diese umsortiert, da sich dann die Ordnungszahlen der Einträge ändern. Etwas besser ist es, statt der Ordnungszahl den Namen des jeweiligen Enum-Wertes in der Datenbank zu speichern. Das wird mittels @Enumerated (javax.persistence.Enumerated) konfiguriert:

  @Enumerated(EnumType.STRING)
  var favoriteColor: Color?

Aber selbst der Name von Enum-Werten kann sich im Laufe der Zeit ändern, sei es wegen Rechtschreibfehlern oder weil man externe Sachen abbildet, die sich geändert haben. Daher empfiehlt es sich den Enum-Werten explizit Zahlen zuzuordnen. Das ist auch generell nützlich, falls es APIs gibt, in denen diese Enums verwendet werden – man hat dann dort auch die Änderungssicherheit. Der Enum-Klasse wird einfach eine neue Property hinzugefügt, welche den eigentlichen Wert enthält. Diese wird dann bei jedem Enum-Wert definiert:

enum class Color(val value: Int) {
  Red(1),
  Green(2),
  Blue(3)
}

Dann fehlt nur noch eine Mini-Klasse, welche zwischen den Enum-Werten und den internen Werten (die in der Datenbank stehen) konvertieren kann. Diese kann als Singleton (object) definiert werden, so ist sie auch an anderer Stelle einfach für Umwandlungen nutzbar. Sie kann auch mit in das enum gepackt werden.

@Converter(autoApply = true)
object ColorConverter : AttributeConverter<Color, Int> {
  override fun convertToDatabaseColumn(attribute: Color?): Int? {
    return attribute?.value
  }

  override fun convertToEntityAttribute(dbData: Int?): Color? {
    return Color.values().find { it.value == dbData }
  }
}

Der Parameter autoApply = true in @Converter (javax.persistence.Converter) bewirkt, dass nicht jede Color Property in jeder Entity extra annotiert werden muss (dort braucht es also keine @Convert oder @Enumerated Markierung), sondern Hibernate automatisch diesen Converter benutzt, um zwischen Datenbank- und Code-Werten umzuwandeln.

# Beispiel

Hier ist ein Beispiel, welches die bisherigen Themen vereint:

@Entity
@Table(indexes = [
  Index(columnList = "date-of-birth")
])
class Person(
  var name: String,

  @Column(name = "date-of-birth", nullable = false)
  var dateOfBirth: LocalDate,
) {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
  
  var favoriteColor: Color? = null

  @Column(nullable = false)
  var created: OffsetDateTime = OffsetDateTime.now()
}

enum class Color(
  val value: Int
) {
  Red(10),
  Green(20),
  Blue(30);

  @Converter(autoApply = true)
  object ColorConverter : AttributeConverter<Color, Int> {
    override fun convertToDatabaseColumn(attribute: Color?): Int? {
      return attribute?.value
    }

    override fun convertToEntityAttribute(dbData: Int?): Color? {
      return Color.values().find { it.value == dbData }
    }
  }
}

# Composite Primary Key

Manchmal setzt sich der Primärschlüssel einer Entity aus mehreren Spalten zusammen. In diesem Fall kann dafür ein Embeddable definiert werden, welches die Properties des Primärschlüssels enthält. Im Entity wird dieses dann als Property deklariert und mit @EmbeddedId annotiert. In der Datenbank werden diese Spalten dann normal in der Tabelle angelegt und mit einem übergreifenden Primary Key Index versehen.

@Embeddable
data class MyCompositeKey(
  @Id
  val partOne: String,

  @Id
  val partTwo: Long
): Serializable

@Entity
class MyEntity(
  @EmbeddedId
  val id: MyCompositeKey,
  
  // ...
)

# Schema

Falls in der verbundenen Datenbank die Tabellen und Spalten der Entities noch nicht existieren, können diese automatisch angelegt werden.

WARNING

Dies erreicht schnell seine Grenzen und es sollte besser eine Datenbankschema-Migrationslösung benutzt werden!

Über die Konfiguration spring.jpa.hibernate.ddl-auto kann man festlegen, ob Hibernate beim Applikationsstart die fehlenden Objekte in der Datenbank anlegt, oder nicht.

Ist die verbundene Datenbank embedded (also H2), ist der Standardwert für diese Konfiguration create-drop (das Schema wird beim Starten erzeugt und beim Beenden wieder gelöscht). Für alle anderen Datenbanktypen ist der Standardwert none, es wird sich also nicht um das Schema gekümmert (was ggf. zu Fehlern führen kann, wenn die Definition im Code nicht zum Datenbankstand passt).

spring.jpa.hibernate.ddl-auto=update

Ich empfehle das explizite Festlegen dieser Konfiguration auf update, falls nur umsetzbare Änderungen vorkommen (für Unit-Tests o. ä. kann natürlich auch ein anderer Wert benutzt werden). Hibernate versucht dann fehlende Sachen anzulegen. Bestehendes wird nicht verändert, auch nicht gelöscht.

TIP

Was geht:

  • Entity hinzufügen
  • Property hinzufügen
  • Index hinzufügen

WARNING

Was nicht geht:

  • Entity oder Property umbenennen
    • 💡 außer es wird mittels @Table oder @Column der alte Name, den es in der Datenbank ja noch gibt, spezifiziert
  • Property-Typ ändern
  • Relationen ändern
  • Sachen entfernen
    • geht schon, führt nur zu Datenmüll, da die entsprechenden Tabellen / Spalten nicht gelöscht werden

Beim nachträglichen Hinzufügen einer @Column(nullable = false) Property füllen z. B. MySQL / MariaDB die schon bestehenden Zeilen mit Standardwerten (Leerstring, Zahl Null, Datum Null, etc.).