Persistieren von Enums mit JPA

“One man’s constant is another man’s variable.”

Alan J. Perlis

Die Java Enums sind für jeden Software Entwickler ein Segen, verhindern sie doch an vielen Stellen feherhafte Zuweisungen von Konstanten. Erstaunlicherweise werden sie in ihrer Natur als Konstanten nicht immer akzeptiert.

Schon in mehreren Projekten durfte ich einige seltsame Arten erleben, wie mit Enums verfahren wurde. Insbesondere bei der Speicherung von Enums passiert Wunderliches.

Enum sind Klassen, die ein festes Set von Instanzen bereitstellen, die sich durch ihren Namen und ihrem Ordinalwert identifizieren lassen.

enum Role { JUNIOR, EXPERT, SENIOR, PRINCIPAL }

Im obigen Enum gint es die Konstanten JUNIOR, EXPERT, SENIOR und PRINCIPAL mit den entsprechenden Namen und den Ordinalwerten von 0 bis 3.

Da der Name der Konstante in einer Datenbankspalte sprechender ist als der Ordinalwert, hat es sich in manchen Kreisen eingebürgert, den Namen der Konstanten zu speichern.

In der Theorie ist es natürlich egal, ob man den Namen oder den Ordinalwert speichert. Denn beide gewährleisten es prinzipiell, auf den Enum-Wert zu schließen. In der Realität sieht es natürlich etwas anders aus.

Die Konstanten sind in Enum Klasse über ein Array ansprechbar. Um nun anhand des Namens auf den richtigen Eintrag zu kommen, muss das Array durchsucht werden.

Role byName(String name) {
  return Arrays.stream(values).map(r -> r.name().equals(name)).findFirst()
    .orElseThrow(() ->new NoSuchElementException());
}

Der Ordinalwert der Konstante entspricht ihrer Position im Array. Der Zugriff auf die Konstante ist tivial.

Role byOrdinal(int ordinal) {
  return values()[ordinal];
}

Obwohl die Speicherung der Ordinalwerte platzsparender und die Herleitung der Konstante aus ihr effizienter ist, als den Namen zu bemühen, existiert ein Schwachpunkt in ihrer Verwendung.

Wie schon im Beitrag Enums für die Ewigkeit angesprochen, darf die bestehende Reihenfolge der Enums nicht verändert werden. Dadurch würde ein gespeicherter Ordinalwert bei der späteren Verwendung auf eine falsche Konstante gemapped. Neue Enum Konstanten dürfen also nur angefügt werden.

Bei der Verwendung der Namen darf ein Entwickler keine bestehenden Namen im Zuge eines Refactorings ändern. Dann könnten gespeicherte Namen nicht mehr gemapped werden.

In vielen Projekten wird zwischen datenbankspezifischen Entity Klassen und DTO Klassen unterschieden und zwischen beiden gemappt.

Eine Enum als Attribute einer Entity kann mit JPA sehr einfach gespeichert werden.

@Entity
public class Employee {
    @Id private int id;

    @Enumerated(EnumType.ORDINAL)
    private Role role;
}

Durch die Annotation @Enumerated(EnumType.ORDINAL) wird die Enum mit ihrem Ordinalwert gespeichert. Mit @Enumerated(EnumType.STRING) wird ihrem Namen. Dennoch findet man immer noch Entitäten, die ein Integer oder String Attribute enthalten und diese auf das Enum Attribute im DTO mappen lassen.

Ein anderes Kuriosum ist die Verwendung zweier Enum Klassen. Eine Enum Klasse bildet das Attribute im Entity ab und die andere Enum Klasse bildet das Attribut im DTO ab.

enum RoleDto { JUNIOR, EXPERT, SENIOR, PRINCIPAL }
class EmployeeDto { private RoleDto role; }

enum Role { JUNIOR, EXPERT, SENIOR, PRINCIPAL }
class Emploee { private Role role; }

Bei jedem Mapping zwischen Entity und DTO wird also auch zwischen zwei identischen Konstanten gemapped. Dies ist unnötig und eine fehleranfällige Redundanz, die gepflegt werden muss.

Manchmal enthält das DTO ein Attribute, dass kein Enum ist, sondern eine Collection eines Enum.

public class EmployeeDto {
    private Set<Role> roles;
}

Neben der Lösung mit einer @OneToMany Beziehung von Employee zu einer neuen RoleEntity, existiert eine schönere Variante, bei der Entity und DTO strukturell nicht voneinander divergieren.

@Entity
public class Employee {
    @Id private int id;

    @ElementCollection(targetClass = Role.class)
    @JoinTable(name = "enum", joinColumns = @JoinColumn(name = "id"))
    @Column(name = "role", nullable = false)
    @Enumerated(EnumType.ORDINAL)
    private Set<Role> roles = new HashSet<>();
}

Die Annotation @ElementCollection ist ein JPA Feature um u.a. Relationen zu simplen Typen zu realisieren. Die Rollen sind keine Entities für sich, sondern sind nur als Attribute der umgebenen Entiy sinnvoll. Ohne diese Entity sind sie bedeutungslos.

Die Verwendung der Enum in der Entity stellt auch sicher, dass jede Rolle tatsächlich nur ein einziges mal zugeordnet wird. Bei einer extra zu erstellenden RoleEntity muss dafür expliziet Sorge getragen werden.

Die Verwendung von Enums ist mit vielen Code Smells versehen. Wer diese Konstanten falsch behandelt, muss sich ändern.