Introduction
In this article, we are going to see how we can map Java Enum to custom values when using JPA and Hibernate.
While Hibernate provides several options to save Enum values, having the option to customize this mechanism is even better, as it will allow you to better deal with legacy applications or use cases that require you to reorder the Java Enum values.
Domain Model
Let’s consider we have the following post
table:
The status
column stores a numerical value associated with a given PostStatus
Enum value, but that value is not the typical ordinal
value of a Java Enum Object.
The PostStatus
Java Enum looks as follows:
public enum PostStatus { PENDING(100), APPROVED(10), SPAM(50), REQUIRES_MODERATOR_INTERVENTION(1); ⠀ private final int statusCode; ⠀ PostStatus(int statusCode) { this.statusCode = statusCode; } ⠀ public int getStatusCode() { return statusCode; } }
What we want to do is store the custom statusCode
of the PostStatus
Enum instead of typical Java Enum ordinal
or name
values.
How to map Java Enum to custom values with JPA and Hibernate
By default, Hibernate 6 uses the EnumJavaType
to map Java Enum entity attributes to a table column that stores either the name
or the ordinal
of that particular Java Enum.
However, since we want to use a custom mapping strategy, we need to extend the default EnumJavaType
like this:
public abstract class CustomOrdinalValueEnumJavaType<T extends Enum<T>> extends EnumJavaType<T> { ⠀ private Map<T, Integer> enumToCustomOrdinalValueMap = new TreeMap<>(); private Map<Integer, T> customOrdinalValueToEnumMap = new TreeMap<>(); ⠀ public CustomOrdinalValueEnumJavaType(Class<T> type) { super(type); ⠀ T[] enumValues = ReflectionUtils.invokeStaticMethod( ReflectionUtils.getMethod(type, "values") ); ⠀ for(T enumValue : enumValues) { Integer customOrdinalValue = getCustomOrdinalValue(enumValue); enumToCustomOrdinalValueMap.put(enumValue, customOrdinalValue); customOrdinalValueToEnumMap.put(customOrdinalValue, enumValue); } } ⠀ protected abstract Integer getCustomOrdinalValue(T enumValue); ⠀ public Byte toByte(T domainForm) { return domainForm != null ? enumToCustomOrdinalValueMap.get(domainForm).byteValue() : null ; } ⠀ public Short toShort(T domainForm) { return domainForm != null ? enumToCustomOrdinalValueMap.get(domainForm).shortValue() : null ; } ⠀ public Integer toInteger(T domainForm) { return domainForm != null ? enumToCustomOrdinalValueMap.get(domainForm) : null ; } ⠀ public Long toLong(T domainForm) { return domainForm != null ? enumToCustomOrdinalValueMap.get(domainForm).longValue() : null ; } ⠀ public T fromByte(Byte byteValue) { return byteValue != null ? customOrdinalValueToEnumMap.get(byteValue.intValue()) : null ; } }
The CustomOrdinalValueEnumJavaType
utility will help us extract that custom value using the toByte
, toShort
, toInteger
, and toLong
methods and retrieve the Java Enum object based on the custom ordinal value via the fromByte
method.
To define the custom ordinal value for the PostStatus
Enum Object, we are going to create the PostStatusJavaType
that extends the CustomOrdinalValueEnumJavaType
base class and implements the getCustomOrdinalValue
method:
public class PostStatusJavaType extends CustomOrdinalValueEnumJavaType<PostStatus> { public PostStatusJavaType() { super(PostStatus.class); } ⠀ @Override protected Integer getCustomOrdinalValue(PostStatus postStatus) { return postStatus.getStatusCode(); } }
The PostStatusJavaType
will be used to instruct Hibernate how to handle the PostStatus
attribute of our Post
entity.
In Hibernate 6, we can use the @JavaType
annotation to provide the custom PostStatusJavaType
that manages the PostStatus
Enum, as illustrated by the following example:
@Table(name = "post") public static class Post { ⠀ @Id private Integer id; ⠀ @Column(length = 250) private String title; ⠀ @Column(columnDefinition = "NUMERIC(3)") @JavaType(PostStatusJavaType.class) private PostStatus status; }
Testing Time
When persisting the following Post
entities:
entityManager.persist( new Post() .setId(1) .setTitle("To be moderated") .setStatus( PostStatus.REQUIRES_MODERATOR_INTERVENTION ) ); entityManager.persist( new Post() .setId(2) .setTitle("Pending") .setStatus( PostStatus.PENDING ) ); entityManager.persist( new Post() .setId(3) .setTitle("Approved") .setStatus( PostStatus.APPROVED ) ); entityManager.persist( new Post() .setId(4) .setTitle("Spam post") .setStatus( PostStatus.SPAM ) );
Hibernate generates the following SQL INSERT statements:
INSERT INTO post ( status, title, id ) VALUES ( 1, 'To be moderated', 1 ) INSERT INTO post ( status, title, id ) VALUES ( 100, 'Pending', 2 ) INSERT INTO post ( status, title, id ) VALUES ( 10, 'Approved', 3 ) INSERT INTO post ( status, title, id ) VALUES ( 50, 'Spam post', 4 )
And when fetching the newly persisted Post
entities, we can see that the status
attributes are fetched correctly:
assertEquals( PostStatus.REQUIRES_MODERATOR_INTERVENTION, entityManager.find(Post.class, 1).getStatus() ); assertEquals( PostStatus.PENDING, entityManager.find(Post.class, 2).getStatus() ); assertEquals( PostStatus.APPROVED, entityManager.find(Post.class, 3).getStatus() ); assertEquals( PostStatus.SPAM, entityManager.find(Post.class, 4).getStatus() );
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
If you want to use a custom ordinal value when persisting and fetching a given Enum value, Hibernate allows you to extend the default EnumJavaType
and provide your own mapping logic.
This mechanism is useful when dealing with legacy applications or if you need to reorder the Enum values. For example, if your application has been previously default ordinal values that got persisted in the database, reordering the Enum values will break the application without either updating the existing Enum column values in the post
table or using a custom EnumJavaType
instance.
Related