How to map Java Enum to custom values with JPA and Hibernate

Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?

What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?

Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.

So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!

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:

Post table storing Java Enum with custom values

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 uses the EnumType to determine whether the Enum name or the ordinal is used to persist the Enum in the underlying database column.

JPA provides the AttributeConverter abstraction to help us deal with situations when we want to control how a given Basic Type is persisted in the database table column.

To achieve our goal of using a custom ordinal value, we are going to use the CustomOrdinalEnumConverter from the Hypersistence Utils project, which looks like this:

public abstract class CustomOrdinalEnumConverter<T> 
        implements AttributeConverter<T, Integer> {
⠀⠀
    private Map<Integer, T> customOrdinalValueToEnumMap = new HashMap<>();
⠀
    /**
     * Initialization constructor taking the Java Enum to manage.
     *
     * @param enumType Java Enum type to manage
     */
    public CustomOrdinalEnumConverter(Class<T> enumType) {
        T[] enumValues = ReflectionUtils.invokeStaticMethod(
            ReflectionUtils.getMethod(enumType, "values")
        );
        for (T enumValue : enumValues) {
            Integer customOrdinalValue = convertToDatabaseColumn(enumValue);
            customOrdinalValueToEnumMap.put(customOrdinalValue, enumValue);
        }
    }
⠀
    /**
     * {@inheritDoc}
     */
    @Override
    public T convertToEntityAttribute(Integer ordinalValue) {
        return customOrdinalValueToEnumMap.get(ordinalValue);
    }
}

Now, we need to create the PostStatusConverter that extends the CustomOrdinalEnumConverter base class and implements the convertToDatabaseColumn method:

@Converter
public class PostStatusConverter 
        extends CustomOrdinalEnumConverter<PostStatus> {
⠀
    public PostStatusConverter() {
        super(PostStatus.class);
    }
⠀
    @Override
    public Integer convertToDatabaseColumn(PostStatus enumValue) {
        return enumValue.getStatusCode();
    }
}

Next, we need to instruct Hibernate to use the PostStatusConverter for our PostStatus entity attribute using the @Convert annotation:

@Entity(name = "Post")
@Table(name = "post")
public class Post {
⠀
    @Id
    private Integer id;
⠀
    @Column(length = 250)
    private String title;
⠀
    @Column(columnDefinition = "NUMERIC(3)")
    @Convert(converter = PostStatusConverter.class)
    private PostStatus status;
}

That’s it!

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.

Seize the deal! 33% discount. Seize the deal! 33% discount. Seize the deal! 33% discount.

Conclusion

If you want to use a custom ordinal value when persisting and fetching a given Enum value, JPA allows you to use a custom AttributeConverter 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 using the 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 AttributeConverter instance.

Transactions and Concurrency Control eBook

2 Comments on “How to map Java Enum to custom values with JPA and Hibernate

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.