Persisting Enums in Java: Beyond ORDINAL and STRING

In Java, enums are typically persisted in a database using either their ordinal values or names (strings). While this approach is straightforward, it may not always be the best choice. Enum names are tied to the application’s code and can be prone to changes, while ordinal values are even more fragile, as modifying the enum order can break data consistency. A more flexible and database-friendly approach is needed when dealing with identifiers that don’t naturally align with Java’s enum conventions or when aiming to decouple the database structure from internal application logic.

To address this, an AttributeConverter can be used to persist a custom "value" field associated with each enum constant. This approach involves introducing an additional interface, ValueEnum, which allows defining and storing meaningful values while keeping the application code clean and maintainable.

public interface ValueEnum<T> {
  T getValue();
}

The ValueEnumConverter class is a generic AttributeConverter. It maps the enum value suitable for storage in the database to the appropriate enum constant when reading back.

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;

@Converter(autoApply = true)
public class ValueEnumConverter<E extends Enum<E> & ValueEnum<T>, T>
    implements AttributeConverter<E, T> {
  
  private final Class<E> clazz;

  public ValueEnumConverter() {
    var superclass = getClass().getGenericSuperclass();
    var typeArgs = ((ParameterizedType) superclass).getActualTypeArguments();
    this.clazz = (Class<E>) typeArgs[0];
  }

  @Override
  public T convertToDatabaseColumn(E value) {
    if (value == null) return null;
    return value.getValue();
  }

  @Override
  public E convertToEntityAttribute(T dbValue) {
    if (dbValue == null) return null;
    return Arrays.stream(clazz.getEnumConstants())
        .filter(e -> e.getValue().equals(dbValue))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Unknown enum value: " + dbValue));
  }
}

Example Use Case:

To demonstrate how ValueEnum works in practice, let's define an enum for spaceship parts. Each part has a custom string value that will be stored in the database instead of the enum's name.

@Getter
@RequiredArgsConstructor
public enum SpaceshipPartEnum implements ValueEnum<String> {
  THRUSTER("01-TW3719"),
  HYPERDRIVE("0E1-HD9834"),
  SHIELD_GENERATOR("SG1552");

  private final String value;

  public static class Converter extends ValueEnumConverter<SpaceshipPartEnum, String> {}
}

In this example:

Why Use an Inner Converter Class?

If we instantiated ValueEnumConverter directly without subclassing, the generic types (SpaceshipPartEnum and String) would be erased at runtime due to Java’s type erasure. This is because ValueEnumConverter determines the enum type using reflection in its default constructor:

public ValueEnumConverter() {
    var superclass = getClass().getGenericSuperclass();
    var typeArgs = ((ParameterizedType) superclass).getActualTypeArguments();
    this.clazz = (Class<E>) typeArgs[0];
}

Since JPA does not allow passing constructor arguments to converters, we cannot explicitly specify the type. Instead, by defining a concrete subclass (SpaceshipPartEnum.Converter), we ensure that getGenericSuperclass() correctly retrieves the parameterized type, allowing the converter to determine the enum class at runtime.

Applying the Converter

To persist the enum in an entity, we annotate the field with @Convert:

@NotNull
@Convert(converter = SpaceshipPartEnum.Converter.class)
private SpaceshipPartEnum spaceshipPart;

This tells JPA to use our ValueEnumConverter subclass to store and retrieve the SpaceshipPartEnum values correctly.

By adopting this approach to persisting enums in Java, you ensure that your application's domain logic remains decoupled from the database schema. Instead of relying on fragile ordinal or name-based persistence, this method allows enums to store custom values while maintaining type safety and flexibility.