~/home of geeks

Java Enumerations persistieren

· 963 Wörter · 5 Minute(n) Lesedauer

hall full of ice figures, sci-fi

Es gibt diverse Strategien, wie man Enumeration-Properties aus Java in der Datenbank persistieren kann. Die Standardvarianten ORDINAL oder STRING haben beide Nachteile. Eine eigene Variante kann diesen Nachteilen entgegenwirken.

Die Ausgangssituation ist einfach: Man hat eine Entityklasse mit einer Enumeration-Property und möchte diese in der Datenbank speichern:

public enum State {
  NEW, WORK_IN_PROGRESS, FINISHED
}
@Entity
public class WorkItem {
  private State state;
  // ...
}

Ordinal und String #

Die ersten Optionen, die einem zur Verfügung stehen sind, die von der Annotation javax.persistence.Enumerated unterstützten Modi EnumType.ORDINAL und EnumType.STRING:

Ordinal #

@Enumerated(EnumType.ORDINAL)
private State state;

Dies speichert eine positive Ganzzahl in der Spalte “state”, welche die (0-basierte) Position in der Enumeration angibt. Für “NEW” wäre das der Wert “0”.

Der Vorteil ist, dass die Spalte wenig Speicherplatz benötigt und als Integerwert sehr effizient indiziert werden kann.

Der Nachteil dieser Variante liegt auf der Hand: Die Einträge in der Datenbank sind intuitiv nicht lesbar und wenn ein Enumerationwert am Anfang der Aufzählung wegfällt, müssen alle Positionen korrigiert werden. Wenn “NEW” wegfällt, steht die “0” fortan für “WORK_IN_PROGRESS”.

String, die Erste #

@Enumerated(EnumType.STRING)
private State state;

Dies speichert den kompletten Enumerationwert als String in der Datenbank. Die Spalte “state” enthält nun den String “NEW”.

Der Vorteil ist, dass man nun in der Datenbank auf einen Blick erkennt, welchen Wert die Spalte tatsächlich hat und repräsentiert. Auch kann ein Enumerationwert dadurch entfernt werden, dass er im Code entfernt und in der Datenbank der Wert umgemappt wird. Alle anderen Enumerationwerte müssen nicht angefasst werden.

Der Nachteil hier ist, dass die Strings recht lang sein können, wenn die Enumerationen sehr geschwätzige Namen haben, wie im Fall “WORK_IN_PROGRESS”. Dies kann insbesondere bei mehreren Enumerationwerten mit gleichem Prefix zu marginal ineffizienteren Indizes führen. Ein weiterer, schwerwiegenderer Nachteil ist es, dass man die Enumerationwerte nicht ohne Datenmigration umbenennen kann. Hat sich der erste Entwickler vertippt (“WORK_IN_POGRESS”), so müssen parallel zum Enumerationwert auch die Werte in der Datenbank angepasst werden.

Custom Mappings #

Die Nachteile beider Varianten lassen sich durch eine eigene Lösung beheben. Hierzu gibt es die Möglichkeit in Hibernate (und auch in JPA) ein eigenes Mapping zwischen Enumerationwerten und Datenbankwerten vorzunehmen.

In beiden Fällen kann man die Enumerationwerte z. B. auf einzelne oder wenige Buchstaben mappen, die wenig Platz benötigen, lesbar sind und unabhängig von der Enumerationschreibweise.

Für das obige Beispiel sollen die Enumerationen auf folgende drei Buchstabenkombinationen gemappt werden: “NEW” für NEW, “WIP” für WORK_IN_PROGRESS und “FIN” für FINISHED.

Hierzu wird zuerst die Enumeration erweitert, sodass die Enumerationwerte selber ihren zwei Buchstaben Code kennen:

public enum State {
  /**
   * State for new, unprocessed items.
   */
  NEW("NEW"),

  /**
   * State for items, which are currently processed.
   */
  WORK_IN_PROGRESS("WIP"),

  /**
   * State for finished items.
   */
  FINISHED("FIN");

  private String code;

  private State(String code) {
    if (code == null) {
      throw new NullPointerException("Code may not be null.");
    }
    this.code = code;
  }

  public String getCode() {
    return code;
  }

  public static State fromCode(String code) {
    if (code == null) {
      throw new NullPointerException("Code may not be null.");
    }
    Status foundState = null;
    for (State aState : State.values()) {
      if (aState.getCode().equals(code)) {
        foundState = aState;
        break;
      }
    }
    if (foundState == null) {
      throw new IllegalArgumentException(
        Strings.format("Code %s is not a valid State. It has to be one of %s.", code, State.values())
      );
    }
    return foundState;
  }
}

Für die Custom Mappings gibt es eine JPA-konforme Lösung und eine Hibernate spezifische.

JPA Attribute Converter #

Der JPA Attribute Converter wird einfach annotiert und damit automatisch eingebunden. Er enthält lediglich zwei Methoden, die befüllt werden müssen:

@Converter(autoApply = true)
public class StateConverter implements AttributeConverter<State, String> {
 
    @Override
    public String convertToDatabaseColumn(State state) {
        return state.getCode();
    }
 
    @Override
    public State convertToEntityAttribute(String codeFromDb) {
        return State.fromCode(codeFromDb);
    }
 
}

Bei dieser Variante muss darauf geachtet werden, dass die gemappte Property KEINE Enumerated-Annotation hat. (Siehe auch JPA 2.1 Attribute Converter – The better way to persist enums von Thorben Janssen)

Hibernate UserType #

Die Hibernate-Variante ist etwas umständlicher. Hier muss ein Interface mit mehreren Methoden implementiert und die Property mit einer entsprechenden Annotation mit dem Type verknüpft werden.

import java.io.Serializable;   
import java.sql.PreparedStatement;   
import java.sql.ResultSet;   
import java.sql.SQLException;   
import java.sql.Types;   
import org.hibernate.HibernateException;   
import org.hibernate.usertype.UserType;   

public class StateUserType implements UserType {   

    private static final int[] SQL_TYPES = {Types.VARCHAR};   
    public int[] sqlTypes() {   
        return SQL_TYPES;   
    }   
   
    public Class returnedClass() {   
        return State.class;   
    }   
   
    public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) throws HibernateException, SQLException {   
        String name = resultSet.getString(names[0]);   
        State result = null;   
        if (!resultSet.wasNull()) {   
            result = State.fromCode(name);   
        }   
        return result;   
    }   
   
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index) throws HibernateException, SQLException {   
        if (null == value) {   
            preparedStatement.setNull(index, Types.VARCHAR);   
        } else {   
            preparedStatement.setString(index, ((State) value).getCode());   
        }   
    }   
   
    public Object deepCopy(Object value) throws HibernateException{   
        return value;   
    }   
   
    public boolean isMutable() {   
        return false;   
    }   
   
    public Object assemble(Serializable cached, Object owner) throws HibernateException {   
         return cached;  
    }   
  
    public Serializable disassemble(Object value) throws HibernateException {   
        return (Serializable) value;   
    }   
   
    public Object replace(Object original, Object target, Object owner) throws HibernateException {   
        return original;   
    }

    public int hashCode(Object x) throws HibernateException {   
        return x.hashCode();   
    }   
    
    public boolean equals(Object x, Object y) throws HibernateException {   
        if (x == y)   
            return true;   
        if (null == x || null == y)   
            return false;   
        return x.equals(y);   
    }   
}

Die Verwendung an einer Property findet via Type-Annotation statt:

@Type(type="myPackage.StateUserType")
private State state;

(Siehe auch UserType for persisting an Enum with a VARCHAR column von Anthony Patricio)

Datenbank Enumerations statt Zeichen #

In seinem Artikel The best way to map an Enum Type with JPA and Hibernate schlägt Vlad Mihalcea vor, die Enumerationwerte statt in Text zu Enumerationwerten in der Datenbank zu mappen.