# Relationen

Meistens werden zwar einfache Datentypen wie String, Long oder temporale Typen benötigt, es gibt aber natürlich auch Fälle, wo Einfach- oder Mehrfachverknüpfungen zwischen den Entities bestehen.

TIP

Relationen sollten immer bidirektional definiert werden, auch wenn die eine Richtung nicht benutzt wird. Das ist nötig, damit Hibernate die entsprechenden Verknüpfungen aufräumen (NULLen) kann, sobald eine Entity gelöscht wird. Das kann es nur, wenn es über alle Stellen Bescheid weiß, an denen das Entity verwendet wird, und wie diese zusammenhängen.

# FetchType, Lazy

Da die Daten hinter Relationen nicht direkt mit in der Tabelle des Entities, sondern in anderen Tabellen liegen, bedingt der Zugriff auf die Daten im Code, dass mehrere Abfragen in der Datenbank ausgeführt werden müssen, um alle Daten einzusammeln. Das führt bei Relationen, hinter denen mehrere Datensätze stecken können, schnell zu Performanceproblemen.

Aus diesem Grund kann man den sog. FetchMode bei jeder Relationen angeben. Dieser kann entweder EAGER oder LAZY sein. Wird ein Entity aus der Datenbank geladen, werden nicht nur die simplen Properties wie String, Long, etc. geladen, sondern auch direkt alle Daten von Relationen, die mit FetchMode = EAGER konfiguriert sind.

Im Gegensatz dazu werden die Daten bei LAZY Relationen erst geladen, sobald zur Laufzeit im Code darauf zugegriffen wird. Das erfolgt transparent im Hintergrund ohne weiteres Zutun. Im Code greift man ganz normal auf eine List o. ä. zu.

Der Standard für @*ToMany Relationen ist LAZY. Das sollte auch so belassen werden, damit nicht unnötige Queries gemacht und Daten geladen werden. Bei @*ToOne Relationen ist der Standard EAGER.

WARNING

Da nicht nur das eigentliche Entity, sondern bei EAGER Relationen auch die Entities dahinter geladen werden, welche evtl. selbst wieder EAGER Relationen haben usw., empfiehlt es sich auch die @*ToOne Relationen manuell auf LAZY zu setzen, um unnötiges Laden zu vermeiden. Für den Code macht das keinen Unterschied, da ja beim Zugriff auf die LAZY Properties transparent im Hintergrund die Daten geladen werden.

Damit man sich nicht merken muss, bei welchen Relationen LAZY explizit anzugeben ist, kann man es einfach bei allen @*To* Relationen angeben.

WARNING

Es gibt noch eine andere, wichtige Sache zu beachten. JPA / Hibernate lassen Datenabfragen in Datenbank-Sessions / Transaktionen ablaufen. Da im Falle von LAZY die Daten aber erst irgendwann im Hintergrund geladen werden gibt es zu diesem Zeitpunkt evtl. die Datenbank-Session / Transaktion nicht mehr – das führt dann zu Exceptions.

Eine einfache Möglichkeit ist, Hibernate mitzuteilen, dass es beim Laden von LAZY Daten keine Datenbank-Session / Transaktion benötigt. Das geht mit folgender Konfiguration:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Das ist zwar auch nicht die feine Art, löst das Problem aber auf einfache Weise und geht im Normalfall nicht sehr auf die Performance. Dieser Artikel (opens new window) geht nochmal auf die Hintergründe und mögliche Lösungen ein. Man könnte auch entsprechende Methoden mit @Transactional versehen, darauf gehe ich aber hier nicht ein.

# Cascade

Normalerweise muss jede Entity-Instanz manuell gespeichert oder gelöscht werden. Will man beispielsweise einen BlogPost mit mehreren Comments anlegen, so müsste man alle Comments einzeln zusätzlich zum BlogPost anlegen. Genauso beim Löschen: es müssten zusätzlich zum BlogPost alle verknüpften Comments gelöscht werden.

Bei solchen Parent-Child-Relationen (@OneToOne und @OneToMany), also bei denen die Unterelemente nicht standalone existieren, sondern nur, wenn es einen Parent-Datensatz gibt, können Parent-Operationen wie Speichern und Löschen automatisch auch auf die Child-Datensätze angewandt werden.

Dazu setzt man einfach bei der entsprechenden Annotation die cascade Eigenschaft. Diese kann verschiedene Werte annehmen. Hier die wichtigsten:

  • bei ALL werden alle Operationen weitergereicht
  • bei PERSIST wird das Neuanlegen und Speichern durchgereicht
  • bei REMOVE wird das Löschen durchgereicht

TIP

Bei Parent-Child-Relationen (@OneTo*), bei denen die Child-Datensätze nicht standalone existieren, also nur in Verbindung mit ihrem Parent existieren, empfehle ich bei der Relation cascade = [CascadeType.ALL] und orphanRemoval = true anzugeben.

# @OneToOne

Eine 1:0-1 (optional = true, Property-Typ nullable) oder 1:1 (optional = false, Property-Typ nicht nullable) Beziehung. Es kann noch orphanRemoval = true angegeben werden, das führt beim Entfernen der Verbindung zum Child-Datensatz zum Löschen des Child-Datensatzes und ist nützlich um Datenmüll zu vermeiden.

Beispiel: Ein User hat immer genau einen UserSettings Datensatz zugeordnet.

@Entity
class User(
  var username: String,

  @OneToOne(optional = false, cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
  var settings: UserSettings
) {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
}

@Entity
class UserSettings(
  var darkTheme: Boolean
) {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
}

TIP

In UserSettings ist im Beispiel keine Rückverknüpfung definiert, da nicht nötig. Sie würde hier auch eher stören, weil man sie auch immer füllen müsste.

Für Fälle, wo es einer Rückverknüpfung bedarf, kann in der verknüpften Entity ebenfalls eine Property @OneToOne eingebaut werden. In der Annotation wird dann noch der Parameter mappedBy auf den Feldnamen der Vorwärtsverknüpfung gesetzt.

# @ManyToMany

Eine n:m Beziehung. Hier erstellt Hibernate automatisch eine Hilfstabelle, um die Verknüpfungen abzubilden. Gibt es die Verknüpfung auf der Gegenseite auch, so werden beide mit @JoinTable konfiguriert.

Beispiel: Ein Benutzer (User) kann mehreren Projekten (Project) zugeordnet sein. In einem Projekt können aber auch mehrere Benutzer sein.

@Entity
class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0

  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "users_projects",
    joinColumns = [JoinColumn(name = "user_id")],
    inverseJoinColumns = [JoinColumn(name = "project_id")])
  var projects: MutableList<Project> = mutableListOf()
}

@Entity
class Project {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0

  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "users_projects",
    joinColumns = [JoinColumn(name = "project_id")],
    inverseJoinColumns = [JoinColumn(name = "user_id")])
  var users: MutableList<User> = mutableListOf()
}

# @OneToMany

Eine 1:n Beziehung. Der Property-Typ muss eine Collection wie List oder Set sein. Auch hier kann orphanRemoval = true verwendet werden, falls die Kind-Datensätze gelöscht werden sollen, sobald sie nicht mehr in der Collection enthalten sind.

Bei @OneToMany wird in der Entity-Tabelle keine Spalte angelegt. Die Zuordnung erfolgt ja über das zugehörige Feld in der anderen Entity-Tabelle.

Beispiel: Eine Rechnung (Invoice) enthält mehrere Positionen (InvoiceItem).

@Entity
class Invoice {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
  
  @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "invoice_id")
  var items: MutableList<InvoiceItem> = mutableListOf()
}

Die Angabe von @JoinColumn hier ist wichtig, damit diese Spalte in InvoiceItem angelegt wird. Ansonsten wird eine extra Hilfstabelle erstellt, was ja aber nicht wirklich nötig ist, da das Verknüpfungsfeld direkt im InvoiceItem enthalten sein kann (somit entfällt auch ein JOIN, was die Performance steigert).

# @ManyToOne

Eine n:1 Beziehung, also das Gegenstück zu @OneToMany. Hier kann ebenfalls optional verwendet werden, falls die n Datensätze auch standalone, also ohne Verknüpfung zu einem 1Datensatz, existieren dürfen.

Diese Relationen werden eigentlich nur benutzt, um eine Rückverknüpfung zu der Entity mit @OneToMany zu haben (falls diese Rückverknüpfung nicht benötigt wird, braucht keine @ManyToOne Property definiert zu werden, da Hibernate dank @JoinColumn schon ein Feld anlegt – oder eine Hilfstabelle ohne diese Annotation). Damit die Rückverknüpfung funktioniert, muss zwingend auch hier @JoinColumn mit demselben Namen wie bei @OneToMany sowie optional = true angegeben werden.

Die Rückverknüpfung wird als lateinit var deklariert, weil nur so eine non-nullable Property definiert werden kann, die nicht direkt initialisiert werden muss (das übernimmt dann Hibernate). Leider kann man so den schreibenden Zugriff auf diese Property von außerhalb nicht verhindern.

Beispiel: Mehrere Rechnungspositionen (InvoiceItem) sind einer Rechnung (Invoice) zugeordnet.

@Entity
class InvoiceItem {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
  
  @ManyToOne(fetch = FetchType.LAZY, optional = true)
  @JoinColumn(name = "invoice_id")
  lateinit var invoice: Invoice
}

# @Embeddable, @Embedded

Gibt es Properties, die man in derselben Form bei mehreren Entities verwendet, kann man diese in einer @Embeddable data class zusammenfassen. Ein @Embeddable hat im Gegensatz zu einem @Entity keine @Id Property.

In den Entities wird dann nur eine mit @Embedded annotierte Property vom Typ dieser Klasse verwendet. Beispiel: Eine zeitliche Periode, die ein Start und ein Ende haben kann, wird im Projekt für die Planungsphase verwendet.

@Entity
class Project(
  @Embedded
  val planningTime: Period
) {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  val id: Long = 0
}

@Embeddable
data class Period(
  @Column(nullable = true)
  var start: OffsetDateTime? = null,

  @Column(nullable = true)
  var end: OffsetDateTime? = null
)

Für Period wird keine extra Datenbanktabelle angelegt. In der Tabelle von Project sind stattdessen die Felder start und end direkt enthalten.

WARNING

Ein @Embeddable Typ kann so nur einmal je Entity verwendet werden, da sich sonst die Spaltennamen doppeln würden.

Ebenfalls ist Vorsicht vor gleichen Propertynamen unterschiedlicher @Embeddables geboten. Wenn in einer Entity diese Typen gleichzeitig verwendet werden, würden sich auch dort die Spaltennamen überschneiden.

Falls unbedingt nötig, gibt es aber auch Möglichkeiten die jeweiligen Namen zu überschreiben.

# @ElementCollection

Die @*To* Relationen mappen immer Entities. Möchte man aber nur simple Typen wie String, Long oder @Embeddables zuordnen, kann @ElementCollection benutzt werden:

@ElementCollection
var luckyNumbers: MutableList<Int> = mutableListOf()

@ElementCollection
var periods: MutableList<Period> = mutableListOf()

Solche Relationen verhalten sich wie @OneToMany mit cascade = [CascadeType.ALL] und orphanRemoval = true. Die Unterobjekte landen in Hilfstabellen und werden automatisch angelegt, geändert und gelöscht.

# @OrderBy, @OrderColumn

Bei @*ToMany Relationen kann mittels @OrderBy angegeben werden, in welcher Reihenfolge die Elemente aus der Datenbank in die Collection geladen werden sollen. Als Parameter dieser Annotation gibt man die Property der anderen Entity an, nach der sortiert werden soll. Man kann auch mehrere Properties kommagetrennt angeben und jeweils ASCund DESC benutzen.

@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, orphanRemoval = true)
@OrderBy("amount DESC")
var items: List<InvoiceItem>

WARNING

Da das Sortieren in der Datenbank erfolgt, geht das evtl. auf die Performance, falls es keine passenden Indizes gibt.

Falls es sich um eine Parent-Child-Relation handelt und die Child-Elemente immer so sortiert sein sollen, wie sie zur Laufzeit in der Collection stehen, kann @OrderColumn beuntzt werden.

Diese Annotation legt automatisch eine zusätzliche Sortierspalte (mit passendem Index) in der Hilfstabelle an, in die automatisch die Sortierung als aufsteigende Zahlen hinterlegt wird. Anhand dieser wird dann beim Auslesen auch automatisch sortiert.

# Maps

Auch Maps können persistiert werden. Hibernate legt dafür automatisch Hilfstabellen an. Die Map-Keys sind für gewöhnlich simple Werte, die Values können sowohl simple Werte als auch Entitites sein. Für Entity-Values wird die Relation mit @OneToMany (dasselbe Entity darf dann nur einmal als Value in der Map auftauchen) oder @ManyToManybenutzt. Bei simplen Values nimmt man @ElementCollection.

@ElementCollection
var map1: MutableMap<String, String> = mutableMapOf()

@ManyToMany
var map2: MutableMap<Long, User> = mutableMapOf()