Using ENUM’s with JPA but without the evil ordinal()

Kris Geusebroek

The ordinal of an Enum is used together with JPA to set the database value of an Enum type field of an entity. Since i find the use of the ordinal dangerous in case of future changes i was searching for an alternative way of populating my database field while still using the Enum in my application code.

The first obvious solution might be using the EnumType.STRING option of the @Enumerated annotation. In my opinion this could be usable when the database can be refactored to hold those string representations.

The case i ran into was a defined database field of NUMBER called status with the possible values of 0,100 and 200 meaning INITIAL, ACTIVE and INACTIVE. Using the default conversion with the below mentioned enum would mean that the values 0, 1 and 2 where used to insert into the database.

public enum Status {
  INITIAL, ACTIVE, INACTIVE;
}

and using the enum in the entity class

private Status status

And what if someone decides to add an extra status called CHECKED with a value of 10?

Should i create an Enum like this to be able to use the default?

public enum Status {
  INITIAL, UNUSED_1, …, UNUSED_9, CHECKED, UNUSED_11 .. UNUSED_99, ACTIVE, DO_YOU_GET_THE_FEELING?;
}

So i looked further and found that with EclipseLink (the JPA provider we use) it’s possible to define a Converter and use that in your Entities.  So let’s try.

First we need an Enum which contains the correct value inside: (See also a previous blog on Enums)

public enum Status {
  INITIAL(0), CHECKED(10), ACTIVE(100), INACTIVE(200);

  private final byte status;

  private Status(int value) {
    status = (byte) value;
  }

  public int getValue() {
    return status;
  }
}

Next we need a Converter:

public class StatusEnumConverter implements Converter {
  public Object convertDataValueToObjectValue(Object data, Session session) {
    //TODO implement converting database value to Enum
    return null;
  }

  public Object convertObjectValueToDataValue(Object data, Session session) {
    if (data instanceof Status) {
      return BigDecimal.valueOf(((Status) data).getValue());
    }
    return null;
  }

  public void initialize(DatabaseMapping dbMap, Session session) {
    // No need for database mapping
  }

  public boolean isMutable() {
    return false;
  }
}

That’s one way traffic now. How do we get form a value to the correct item of the enum?

Looking on the internet i found the Enum Inversion problem which supplied me with a solution for this. Also i wanted the StatusEnumConverter to be more generic so the converter could be easily reused. I create a ReverseEnumMap class to aid to the problem of getting the right Enum value and a convertable interface to generify (is that a word?) the Converter. The end result looks like this:

public class ReverseEnumMap & Convertable> {
  private Map map = new HashMap();

  public ReverseEnumMap(Class valueType) {
    for (V v : valueType.getEnumConstants()) {
      map.put(v.convert(), v);
    }
  }

  public V get(Object obj) {
    return map.get(obj);
  }
}

public interface Convertable & Convertable> {
  Object convert();
  E getFromValue(Object value);
}

public enum Status implements Convertable {
  INITIAL(0), CHECKED(10), ACTIVE(100), INACTIVE(200);

  private static ReverseEnumMap map = new ReverseEnumMap(Status.class);

  private final byte status;

  private Status(int value) {
    status = (byte) value;
  }

  public Object convert() {
    return BigDecimal.valueOf(status);
  }

  public Status getFromValue(Object obj) {
    return map.get(obj);
  }
}

public abstract class AbstractConverter implements Converter {

  public abstract Convertable getConvertableEnum();

  public Object convertDataValueToObjectValue(Object data, Session session) {
    if (data == null) {
      return getConvertableEnum();
    }
    Convertable convertableEnum = getConvertableEnum().getFromValue(data);
    if (convertableEnum == null) {
      throw new IllegalArgumentException(
          "Data not with a value suitable got [" + data.getClass() +" : "+data
          + "] expected a valid value of ["
          + getConvertableEnum().getClass() + "]");
    } else {
      return convertableEnum;
    }
  }

  public Object convertObjectValueToDataValue(Object data, Session session) {
    if (data == null) {
      return getConvertableEnum().convert();
    }
    if (data instanceof Convertable) {
      return ((Convertable) data).convert();
    }
    throw new IllegalArgumentException("Data not of correct type got ["
      + data.getClass() + "] expected [Convertable]");
  }
  //left out the other methods for readibility
}

public class StatusConverter extends AbstractConverter {

  private static final long serialVersionUID = 6209909216228257358L;

  @Override
  public Convertable getConvertableEnum() {
    return Status.INITIAL;
  }
}

And this is how you will use the converter in your entity class. Annotating the status field. Note that you only have to define the @Converter annotation once to be able to use the @Convert in other entities also!

@Converter(name="status", converterClass=com.xebia.enum.converters.StatusConverter.class)
@Convert("status")
private Status status;

So with this i think we have a pretty generic solution to avoid using ordinal with enums and JPA.

Comments (5)

  1. Anthony - Reply

    December 24, 2009 at 3:32 am

    Do you have the full code for this? I'm a bit confused with the use of Status.NOT_YET_ACTIVE which has not been defined in your Enum.

    I'm trying a similar approach but when getConvertableEnum is called, is the value of the Enum relevant?

  2. Kris - Reply

    December 28, 2009 at 9:11 am

    Hi Anthony,

    The method getConvertableEnum is just a way to specify a default value of the Enum to return when there is no data in the database column. So i used NOT_ACTIVE_YET as the default.

    I agree it's a bit confusing that this Enum value is not mentioned earlier. So i will change it into return Status.INITIAL for clarity.

    Kris

  3. Bhaskar - Reply

    January 30, 2012 at 7:06 am

    How sorting will be working with this implementation??

    • Kris Geusebroek - Reply

      January 30, 2012 at 9:55 am

      Hi Bhaskar,

      In java there are two way to sort some object, the first is to implement the Comparable interface. This is already done with the Enum class. Only bad thing about this is that the compareTo method is declared final so you cannot change the default way of sorting.

      The second option you have is to write a Comparator.
      something like:

      class StatusComparator implements Comparator
      {
      public int compare(Status o1, Status o2)
      {
      return o1.getValue() - o2.getValue();
      }
      }

  4. Eduardo Frazão - Reply

    August 31, 2013 at 12:26 am

    Hi Kris. Nice work!!

    Only as a tip: If you dont need final implementations to return a default Enum for null database values, you could use the void initialize(DatabaseMapping dbMap, Session session) to get the Enum class of the mapped field (mapping.getAttributeClassification()). In this way, you can iterate over enum constants, and as your enum is a common type, returning a know database id method, you can compare this to grab the correct ENUM for the field, without need to create a final enum converter implementatio for each Enum of your domain!

    Something like:
    private Object getEnumById(int id) {
    for(DomainEnum e : typeClass.getEnumConstants()) {
    if(e.databaseId() == id) {
    return e;
    }
    }
    throws some exception from wrong enum id value. (domain enum is a interface with a databaseId() method)
    }

Add a Comment