Persistenz in Java: Neues seit Hibernate ORM 6

Seite 2: Verbesserte Mandantenfähigkeit

Inhaltsverzeichnis

Die Möglichkeit, mit einem gemeinsamen Backend mehrere voneinander getrennte Mandanten zu bedienen, ist für viele Anwendungen relevant. In Version 5 war Hibernate ORM nur dazu in der Lage, wenn die Daten der Mandanten in voneinander getrennten Datenbanken oder Datenbankschemata vorlagen. Seit Version 6 ist der als "partitioned data" bezeichnete Einsatz einer zusätzlichen Tabellenspalte zum Zuordnen der Mandanten möglich.

Hierzu muss man die Entitäten um eine zusätzliche, mit @TenantId annotierte Eigenschaft erweitern.

@Entity
public class Book {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  private String title;

  @TenantId
  private String tenant;

  ...
}

Um die Mandanten-ID zu erhalten, erwartet Hibernate wie bereits in Version 5 eine Implementierung von CurrentTenantIdentifierResolver. Gängige Implementierungen dieses Interfaces fragen die Mandanten-ID von der Authentifizierungskomponente ab und stellen sie Hibernate zur Verfügung. In der Vergangenheit erlaubte Hibernate ausschließlich Strings als Mandanten-IDs, aber seit Version 6.4 auch andere Eigenschaftstypen. Üblicherweise verwenden Anwendungen die Typen String, Integer oder Long.

Hibernate setzt die ID automatisch beim Persistieren einer neuen Entität und erweitert die ausgeführten Abfragen, um die Ergebnisse auf den aktuell aktiven Mandaten einzuschränken.

em.createQuery("SELECT b FROM Book b " + 
               "WHERE b.title = :title", Book.class)
  .setParameter("title", "My Book")
  .getResultList();

09:00:15,976 DEBUG [org.hibernate.SQL] - 
    select
        b1_0.id,
        b1_0.tenant,
        b1_0.title,
        b1_0.version 
    from
        Book b1_0 
    where
        b1_0.tenant = ? 
        and b1_0.title=?

Eine häufige Anforderung von Enterprise-Anwendungen sind Soft Deletes, um die Datensätze nicht zu löschen, sondern zu deaktivieren. Die Daten sind auf Anwenderseite nicht mehr sichtbar, stehen aber für weitere Auswertungen oder zur späteren Nachvollziehbarkeit weiterhin zur Verfügung.

In der Vergangenheit musste man dazu manuell einen Filter einsetzen und die Löschoperation überschreiben.

Seit der Version 6.4 bietet Hibernate die @SoftDelete-Annotation.

@Entity
@SoftDelete
public class Book { ... }

Für eine damit annotierte Entität speichert Hibernate den aktuellen Status des Datensatzes in einer zusätzlichen Datenbankspalte mit dem Typ Boolean. Die optionale Eigenschaft columnName der @SoftDelete-Annotation legt den Spaltennamen fest. Fehlt der Name, verwendet Hibernate die Spalte deleted.

Um inaktive Datensätze automatisch auszublenden, erweitert Hibernate alle Datenbankabfragen um eine zusätzliche Prüfung der Spalte deleted.

Book book = em.createQuery("SELECT b FROM Book b " + 
                           "WHERE b.title = :title", 
                           Book.class)
              .setParameter("title", "My Book")
              .getSingleResult();

09:15:13,799 DEBUG [org.hibernate.SQL] - 
    select
        b1_0.id,
        b1_0.title,
        b1_0.version 
    from
        Book b1_0 
    where
        b1_0.title=? 
        and b1_0.deleted=false

Solange der Datensatz aktiv ist, enthält deleted den Wert false. Beim Löschen der Entität führt Hibernate die Operation SQL UPDATE statt SQL DELETE aus und setzt den Wert von deleted auf true.

09:15:13,804 DEBUG [org.hibernate.SQL] - 
    update
        Book 
    set
        deleted=true 
    where
        id=? 
        and deleted=false 
        and version=?

Die Annotation @SoftDelete bietet zusätzliche Konfigurationsmöglichkeiten, um Hibernates Implementierung an Tabellenschemata anzupassen.

Intern basiert die Soft-Delete-Implementierung auf einem Boolean, der den aktuellen Status des jeweiligen Datensatzes abbildet. Wie Hibernate diesen Boolean auswertet, legt der Enum-Wert SoftDeleteType fest. Er ist Teil der Eigenschaft strategy von @SoftDelete und kann ACTIVE oder DELETED sein.

Im Standardfall verwendet Hibernate SoftDeleteType.DELETED. Dabei signalisiert true, dass der Datensatz deaktiviert ist. Bei der Strategie SoftDeleteType.ACTIVE bedeutet true hingegen, dass der Datensatz aktiv ist.

Wer den Status des Datensatzes nicht als Boolean in der Datenbank speichern möchte, kann das Vorgehen durch die Referenz eines AttributeConverter anpassen. Dabei handelt es sich um ein einfaches Interface mit zwei Methoden, die die Umwandlung zwischen dem Typ der Entitätseigenschaft und der Datenbankspalte implementieren.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE, 
            columnName = "status", 
            converter = SoftDeleteConverter.class)
public class Book { ... }
Java Flight Recorder Events

Seit Version 6.4 kann Hibernate ORM unterschiedliche interne Ereignisse als JFR-Events (Java Flight Recorder) ausgeben. Dazu gehören unter anderem das Ausführen von JDBC Statements, das Öffnen und Schließen einer Session, das Ausführen von Flush-Operationen und die Interaktionen mit dem 2nd Level Cache.

Um das Erzeugen von JFR-Events zu aktivieren, muss man die Komponente hibernate-jfr als Abhängigkeit zum Projekt hinzufügen und die Anwendung mit dem Flag --XX:StartFlightRecording:filename=<Dateiname>.jfr starten.

<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jfr</artifactId>
  <version>${hibernate.version}</version>
</dependency>

Hibernate erzeugt daraufhin während der Ausführung der Anwendung eine Java-Flight-Recorder-Datei, die sich beispielsweise mit JDK Mission Control einlesen und auswerten lässt.

Das Tool JDK Mission Control hilft beim Auswerten der JFR-Datei (Abb. 1).

(Bild: Screenshot (Thorben Janssen))