Persistenz von Enums in Java: Die bessere Alternative zu ORDINAL und STRING

In Java werden Enums in Datenbanken typischerweise entweder durch ihre ordinalen Werte oder ihre Namen (Strings) gespeichert. Dieser Ansatz ist zwar einfach, aber nicht immer ideal. Enum-Namen sind direkt an den Anwendungscode gebunden und können sich ändern, während ordinale Werte noch problematischer sind – eine Änderung der Enum-Reihenfolge kann die Datenkonsistenz gefährden. Ein flexiblerer und datenbankfreundlicherer Ansatz ist erforderlich, insbesondere wenn man mit Konstanten arbeitet, die nicht den Java-Enum-Konventionen entsprechen, oder wenn man die Datenbankstruktur von der internen Anwendungslogik entkoppeln möchte.

Um dieses Problem zu lösen, kann ein AttributeConverter verwendet werden, der ein benutzerdefiniertes value-Feld persistiert, das jeder Enum-Konstante zugeordnet ist.

Damit dieser Ansatz generisch funktioniert, definieren wir das Interface ValueEnum. So müssen wir den AttributeConverter nicht für jede einzelne Enum-Klasse separat implementieren.

public interface ValueEnum<T> {
  T getValue();
}

Damit können wir unseren ValueEnumConverter implementieren. Ein AttributeConverter erwartet zwei generische Parameter: den Typ des zu konvertierenden Objekts und den Typ des Werts, der nach der Konvertierung resultiert.

In unserem Fall ist es:

Beim Persistieren wandelt ValueEnumConverter die Enum-Konstante in ihren zugehörigen Wert um, der in der Datenbank gespeichert wird. Beim Laden aus der Datenbank stellt er die ursprüngliche Enum-Konstante anhand dieses Werts wieder her.

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;

@Converter(autoApply = true)
public class ValueEnumConverter<E extends Enum<E> & ValueEnum<T>, T>
    implements AttributeConverter<E, T> {
  
  private final Class<E> clazz;

  public ValueEnumConverter() {
    var superclass = getClass().getGenericSuperclass();
    var typeArgs = ((ParameterizedType) superclass).getActualTypeArguments();
    this.clazz = (Class<E>) typeArgs[0];
  }

  @Override
  public T convertToDatabaseColumn(E value) {
    if (value == null) return null;
    return value.getValue();
  }

  @Override
  public E convertToEntityAttribute(T dbValue) {
    if (dbValue == null) return null;
    return Arrays.stream(clazz.getEnumConstants())
        .filter(e -> e.getValue().equals(dbValue))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Unbekannter Enum-Wert: " + dbValue));
  }
}

Beispielanwendung:

Um zu demonstrieren, wie ValueEnum zusammen mit dem ValueEnumConverter funktioniert, definieren wir ein Enum für Raumschiff-Teile. Jede Enum-Konstante besitzt einen benutzerdefinierten String-Wert, der in der Datenbank anstelle des Enum-Namens gespeichert wird.

@Getter
@RequiredArgsConstructor
public enum SpaceshipPartEnum implements ValueEnum<String> {
  THRUSTER("01-TW3719"),
  HYPERDRIVE("0E1-HD9834"),
  SHIELD_GENERATOR("SG1552");

  private final String value;

  public static class Converter extends ValueEnumConverter<SpaceshipPartEnum, String> {}
}

In diesem Beispiel sehen wir:

Warum eine innere Converter-Klasse verwenden?

Ein Converter wird typischerweise zusammen mit der Annotation @Convert verwedet. Würden wir hier direkt den ValueEnumConverter verwenden, so würde über Type Erasure zur Laufzeit die Information über die generischen Typen verloren gehen. Diese Information benötigen wir aber direkt im Konstruktor des ValueEnumConverter:

public ValueEnumConverter() {
    var superclass = getClass().getGenericSuperclass();
    var typeArgs = ((ParameterizedType) superclass).getActualTypeArguments();
    this.clazz = (Class<E>) typeArgs[0];
}

Da JPA nicht zulässt, dass Konstruktorargumente an Converter übergeben werden, können wir den Typ nicht explizit angeben. Stattdessen gewährleisten wir durch die Definition einer konkreten Unterklasse (SpaceshipPartEnum.Converter), dass getGenericSuperclass() den parametrisierten Typ korrekt abruft, was dem Converter ermöglicht, die Enum-Klasse zur Laufzeit zu bestimmen.

Anwendung des Converters

Wie schon im vorherigen Abschnitt angedeutet, können wir den ValueEnumConverter direkt über die @Convert-Annotation einbinden:

@NotNull
@Convert(converter = SpaceshipPartEnum.Converter.class)
private SpaceshipPartEnum spaceshipPart;

Mit dieser Lösung zur Persistierung von Enums in Java bleibt die Anwendungslogik sauber von der Datenbankstruktur getrennt – und das ganz ohne böse Überraschungen bei Enum-Änderungen. Viel Spaß beim fröhlichen Konvertieren!