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:
E extends Enum<E> & ValueEnum<T>
der zu konvertierende Objekttyp, also ein Enum, das das InterfaceValueEnum
implementiertT
der Zieltyp nach der Konvertierung, sodass unser Ansatz generisch bleibt.
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:
- Statt der in der Anwendung definierten Enum-Namen speichern wir mithilfe des
ValueEnumConverter
allgemeingültige Teilenummern in der Datenbank. - Der
ValueEnumConverter
wird als innere Klasse definiert – warum das notwendig ist, erklären wir im nächsten Abschnitt. - Die Lombok-Annotationen (
@Getter
und@RequiredArgsConstructor
) reduzieren Boilerplate-Code, indem sie automatisch Getter und den erforderlichen Konstruktor generieren.
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!