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:
- Each enum constant is associated with a specific string value, which serves as its persistent representation.
- The
getValue()
method (inherited fromValueEnum<String>
) ensures that this value can be retrieved when needed. - The inner
Converter
class extendsValueEnumConverter
, which handles mapping between the enum and its stored database value. - Lombok annotations (
@Getter
and@RequiredArgsConstructor
) are used to eliminate boilerplate code by automatically generating getters and the required constructor.
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.