Blog

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

28 Aug, 2009

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<v extends Enum & 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<e extends Enum & 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.

guest
6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Anthony
Anthony
12 years ago

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?

Kris
12 years ago

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

Bhaskar
Bhaskar
10 years ago

How sorting will be working with this implementation??

Eduardo Frazão
Eduardo Frazão
8 years ago

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)
}

Vince
Vince
7 years ago

Your ReverseEnumMap class and Convertable interface are not escaping HTML correctly. The angle brackets should be ampersand lt; and ampersand rt;

Explore related posts