Enums für die Ewigkeit

EIn schönes Feature der Persistenz unter Java ist das automatische Speichern von Enum-Werten. Wenn nicht weiter spezifiziert, wird der Ordinalwert der Enum Konstante in der Datenbank gespeichert.

public enum Marx {
  GROUCHO, HARPO, CHICO, KARL
}

In unserem Beispiel gäbe es also den Wert 0 für GROUCHO und den Wert 3 für KARL in der Datenbank. Beim Rückmapping wird der Ordinalwert verwendet um wieder zur Konstante zu gelangen. Also würde der Wert 1 in der Datenbank automatisch in die Konstante HARPO umgewandelt.

Was passiert aber nun, wenn ein Kollege an unserer Enum Klasse Änderungen vornimmt? 

Im angenehmsten Fall sind es Ergänzungen zur Enum Klasse. Vielleicht benötigte er noch weitere Konstanten ZEPPO und GUMMO und hat diese hinter KARL eingetragen. Da es sich um neue Konstanten am Ende der Enum Klasse handelt, besitzen sie größere Ordinalwerte, als die bisherigen Konstanten. Dadurch kann es keine Verwechslung geben beim Mapping auf die Datenbank. 

Problematisch wird es, wenn er neue Konstanten zwischen den bisherigen einfügt oder die bestehende Reihenfolge verändert wird. Vielleicht sortiert der Kollege die Konstanten gerne alphabetisch oder nach der Länge ihrer Namen. Damit ist die Katastrophe,  im wahrsten Sinne des Wortes, vorprogrammiert. Nun besitzen im schlimmsten Fall alle Konstanten einen neuen Ordinalwert. Damit ist das Mapping auf die Werte in der Datenbank nicht mehr korrekt.  

Eine recht einfache Möglichkeit sich vor solchem Ungemach zu schützen ist der Einsatz von Unit Tests. Eine Testklasse mit dem sprechenden Namen EnumDatabaseMapping und eine vollständigen Prüfung der Konstanten und ihrer Ordinalwerte, sollte auch den unbekümmerten Entwickler zum Nachdenken bringen. Ein Hinweis im Kommentar der Enum Klasse und in der Testklasse ist auch hilfreich.

public class EnumDataBaseMapping {
  @Test public void criticalMapping() {
    assertEquals(0, GROUCHO.ordinal());
    assertEquals(1, HARPO.ordinal());
...

Es gibt selbstverständlich Programmierer, die auch diese Test anpassen, aber dann sollte man einmal über Änderungen im Team nachdenken. 

Nun gibt es noch die Möglichkeit, das Enum Konstanten umbenannt oder gelöscht werden. Ersteres ist ohne Bedenken zu empfehlen, denn kein Bezeichner ist so gut gewählt, dass er nicht im Laufe der Zeit, durch einen prägnanteren Begriff, ersetzt werden kann. Das Löschen von Konstanten ist nicht ganz so einfach. Eine Konstante aus der Mitte des Enums darf nicht entfernt werden, weil sich dann die Ordinalwerte aller folgender Konstanten verschieben. Wenn das Löschen nicht funktioniert, dann können wir aber die entsprechenden veralteten Konstanten markieren. In unserem Beispiel ist es die Konstante KARL, die mit  @Deprecated  annotiert wird.

public enum Marx {
  GROUCHO, HARPO, CHICO, 
  @Deprecated KARL, 
  ZEPPO, GUMMO
}

Eine, für den Moment, geeignete Lösung, aber wie schaut solch ein Enum nach jahrelanger Verwendung aus? Dutzende, vielleicht sogar Hunderte von Warnungen wegen vieler @Deprecated Annotations überall im Code?  Wäre es nicht doch schöner, wir könnten die unnützen Konstanten ohne großen Aufwand löschen?

Die Lösung bietet uns ein AttributeConverter für unsere Enum Konstanten. Mit solch einer Klasse kann der Entwickler explizit angeben, wie zwischen den Konstanten und der Datenbank Darstellung konvertiert werden soll.

@Converter
public class MarxAttributeConverter implements AttributeConverter<Marx, Integer> {
  private final Map<Marx, Integer> map;
  private final Map<Integer, Marx> invertedMap;

  public MarxAttributeConverter() {
    map = createIndexMap(Marx.class);
    invertedMap = invertMap(map);
  }

  @Override
  public Integer convertToDatabaseColumn(Marx marx) {
    return map.get(marx);
  }

  @Override
  public Marx convertToEntityAttribute(Integer index) {
    return invertedMap.get(index);
  }
}

Der Konverter ist im Grunde recht trivial aufgebaut. Wir schauen in beiden Konvertierungsmethoden in eine Map, ob wir für den entsprechenden Wert einen Gegenpart kennen und liefern diesen zurück. Die invertierte Map invertedMap mit den Integer Schlüsseln generieren wir aus der Map map, indem wir Schlüssel und Wert mit Hilfe der Stream API austauschen. 

private final Map<Integer, Marx> invertMap(Map<Marx, Integer> map) {
  return map.entrySet().stream().collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey()));
}

Da sie recht generisch ist, schreiben wir sie auch gleich so. Dann können wir unseren Konverter auch viel häufiger verwenden.

private final <E extends Enum<E>> Map<Integer, E> invertMap(Map<E, Integer> map) {
  return map.entrySet().stream().collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey()));
}

Jetzt fehlt nur noch die Map, mit der wir das Mapping von einer Enum Konstante auf einen Integer Wert durchführen.

private final <E extends Enum<E>> Map<E, Integer> createIndexMap(Class<E> enumClass) {
  Map<Enum, Integer> map = new EnumMap<>(enumClass);
  int offset = 0;
  for (E key : enumClass.getEnumConstants()) {
    map.put(key, key.ordinal + offset));
  }
  return map;
}

Wir erzeugen für jede Enum Konstante einen Map Eintrag, indem wir die Konstante als Schlüssel und den Ordinalwert und einen Offset als Wert verwenden. Momentan ist der Offset immer 0, da wir aber an unserer ursprünglichen Enum Marx keine Änderungen vorgenommen haben, werden alle Konstanten korrekt auf ihren Ordinalwerte gemappt.

public enum Marx {
  GROUCHO, HARPO, CHICO, 
  @Deprecated KARL, 
  ZEPPO, GUMMO
}

Nun ändern wir unsere Enum und führen dabei eine neue Annotation @Rift ein.

public enum Marx {
  GROUCHO, HARPO, CHICO, 
  @Rift 
  ZEPPO, GUMMO
}

Diese eigene Annotation gibt an, dass hier eine Lücke klafft. Die Anzahl fehlender Werte kann angegeben werden,  als Defaultwert ist 1 definiert.

Da wir die Konstante KARL entfernt haben, berechnet unser Konverter momentan noch die Werte für die folgenden Konstanten falsch. Um diesen Fehler zu beheben ergänzen wir folgende Zeilen in unsere Methode.

Rift rift = enumClass.getField(key.name()).getAnnotation(Rift.class);
offset += rift == null ? 0 : rift.value();

Damit gehen die durch @Rift definierten Lücken in unsere Berechnung ein und wir können zukünftig ungenutzte Konstanten aus unseren Enum entfernen. Da wir unseren Konverter Code sehr allgemein halten konnten, extrahieren wir eine abstrakte Basisklasse und die Definition unsere Konverters verkürzt sich auf folgende Zeilen.

@Converter
public class MarxConverter extends EnumConverter<Marx> {
  public MarxConverter() {
    super(Marx.class);
  }
} 

Vorsicht ist jedoch gegenüber den Kollegen angeraten. Auch für diese Lösung benötigen wir Unit Tests, die uns jederzeit das korrekte Mapping garantieren.