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


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 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.

Transactions and Concurrency Control eBook



Source link

Leave a Comment