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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public enum Marx {
GROUCHO, HARPO, CHICO, KARL
}
public enum Marx { GROUCHO, HARPO, CHICO, KARL }
public enum Marx {
  GROUCHO, HARPO, CHICO, KARL
}

In unserem Beispiel gäbe es also den Wert

0
0 für
GROUCHO
GROUCHO und den Wert
3
3 für
KARL
KARL in der Datenbank. Beim Rückmapping wird der Ordinalwert verwendet um wieder zur Konstante zu gelangen. Also würde der Wert
1
1 in der Datenbank automatisch in die Konstante
HARPO
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
ZEPPO und
GUMMO
GUMMO und hat diese hinter
KARL
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
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class EnumDataBaseMapping {
@Test
public void criticalMapping() {
assertEquals(0, GROUCHO.ordinal());
assertEquals(1, HARPO.ordinal());
...
public class EnumDataBaseMapping { @Test public void criticalMapping() { assertEquals(0, GROUCHO.ordinal()); assertEquals(1, HARPO.ordinal()); ...
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
KARL, die mit 
@Deprecated
@Deprecated  annotiert wird.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public enum Marx {
GROUCHO, HARPO, CHICO,
@Deprecated KARL,
ZEPPO, GUMMO
}
public enum Marx { GROUCHO, HARPO, CHICO, @Deprecated KARL, ZEPPO, GUMMO }
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
@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
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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);
}
}
@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); } }
@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
invertedMap mit den Integer Schlüsseln generieren wir aus der Map
map
map, indem wir Schlüssel und Wert mit Hilfe der Stream API austauschen. 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private final Map<Integer, Marx> invertMap(Map<Marx, Integer> map) {
return map.entrySet().stream().collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey()));
}
private final Map<Integer, Marx> invertMap(Map<Marx, Integer> map) { return map.entrySet().stream().collect(Collectors.toMap(e -> e.getValue(), e -> e.getKey())); }
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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()));
}
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())); }
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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;
}
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; }
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
Marx keine Änderungen vorgenommen haben, werden alle Konstanten korrekt auf ihren Ordinalwerte gemappt.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public enum Marx {
GROUCHO, HARPO, CHICO,
@Deprecated KARL,
ZEPPO, GUMMO
}
public enum Marx { GROUCHO, HARPO, CHICO, @Deprecated KARL, ZEPPO, GUMMO }
public enum Marx {
  GROUCHO, HARPO, CHICO, 
  @Deprecated KARL, 
  ZEPPO, GUMMO
}

Nun ändern wir unsere Enum und führen dabei eine neue Annotation

@Rift
@Rift ein.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public enum Marx {
GROUCHO, HARPO, CHICO,
@Rift
ZEPPO, GUMMO
}
public enum Marx { GROUCHO, HARPO, CHICO, @Rift ZEPPO, GUMMO }
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
1 definiert.

Da wir die Konstante

KARL
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Rift rift = enumClass.getField(key.name()).getAnnotation(Rift.class);
offset += rift == null ? 0 : rift.value();
Rift rift = enumClass.getField(key.name()).getAnnotation(Rift.class); offset += rift == null ? 0 : rift.value();
Rift rift = enumClass.getField(key.name()).getAnnotation(Rift.class);
offset += rift == null ? 0 : rift.value();

Damit gehen die durch

@Rift
@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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Converter
public class MarxConverter extends EnumConverter<Marx> {
public MarxConverter() {
super(Marx.class);
}
}
@Converter public class MarxConverter extends EnumConverter<Marx> { public MarxConverter() { super(Marx.class); } }
@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.