The best way to map an Enum Type with JPA and Hibernate

(Last Updated On: January 16, 2018)

Introduction

Recently, I got a very good question on Twitter, and I decided that the best way to answer it is with a new article.

In this article, we are going to see how various ways to map an Enum using JPA and Hibernate.

Domain Model

Considering we have the following entity:

The Post entity has a status attribute which is an Enum, represented by the PostStatus type. In JPA, Enum types must be marked with the @Enumerated annotation, which can take an EnumType defining if the Java Enumeration is mapped to either a String or an Integer column type.

Mapping Enum to a String column type

To map the Enum to a String database column type, you need to specify the EnumType.STRING value when using the @Enumerated annotation.

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Enumerated(EnumType.STRING)
    @Column(length = 8)
    private PostStatus status;

    //Getters and setters omitted for brevity
}

Assuming we are using PostgreSQL, on the database side, it is expected to have the following table:

CREATE TABLE post (
    id INT8 NOT NULL, 
    title VARCHAR(255), 
    status VARCHAR(8), 
    PRIMARY KEY (id)
)

Now, when inserting a Post entity:

Post post = new Post();
post.setId( 1L );
post.setTitle( "High-Performance Java Persistence" );
post.setStatus( PostStatus.PENDING );

entityManager.persist( post );

Hibernate generates the following SQL INSERT statement:

INSERT INTO post (
    status, 
    title, 
    id
) 
VALUES (
    'PENDING', 
    'High-Performance Java Persistence', 
    1
)

As expected, the String representation of the Java Enum was used to populate the associated database column value.

However, as readable as this might be for the developer, this column takes way more space than necessary. In this case, the status column takes 8 bytes. If we were to store 100 million post records, the status column alone will occupy 800 MB.

Mapping Enum to an Integer column type

As previously explained, you can also use an Integer column type. The Post entity mapping becomes as follows:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Enumerated
    @Column(columnDefinition = "smallint")
    private PostStatus status;

    //Getters and setters omitted for brevity
}

Notice that the @Enumerated column does not need to take the ORDINAL EnumType value since that’s used by default. We are also using the smallint integer column type since it is improbable that we will ever need more than 2 bytes to store all values for the Post Enum type.

As expected, on the database side, the following table will be used:

CREATE TABLE post (
    id INT8 NOT NULL, 
    title VARCHAR(255), 
    status SMALLINT, 
    PRIMARY KEY (id)
)

Now, when inserting the same Post entity, Hibernate generates the following SQL INSERT statement:

INSERT INTO post (
    status, 
    title, 
    id
) 
VALUES (
    0, 
    'High-Performance Java Persistence', 
    1
)

The Enum ordinal value, which is 0 for PostStatus.PENDING, is used to represent the Java Enum type.

Now, this is much more efficient, but it less expressive. So, how can we have both performance and readability?

Actually, it’s pretty simple. Just create a post_status_info as follows:

CREATE TABLE post_status_info (
    id SMALLINT NOT NULL, 
    description VARCHAR(255), 
    name VARCHAR(255), 
    PRIMARY KEY (id)
)

Now, you can populate to describe all PostStatu Enum values:

INSERT INTO post_status_info (
    description, 
    name, 
    id
) 
VALUES (
    'Posts waiting to be approved by the admin', 
    'PENDING', 
    0
)

INSERT INTO post_status_info (
    description, 
    name, 
    id
) 
VALUES (
    'Posts approved by the admin', 
    'APPROVED', 
    1
)

INSERT INTO post_status_info (
    description, 
    name, 
    id
) 
VALUES (
    'Posts rejected as spam', 
    'SPAM', 
    2
)

Now you can get the description by joining the post_status_info table, as illustrated by the following example:

Tuple tuple = (Tuple) entityManager
.createNativeQuery(
        "SELECT " +
        "    p.id, " +
        "    p.title, " +
        "    p.status, " +
        "    psi.name, " +
        "    psi.description " +
        "FROM post p " +
        "INNER JOIN post_status_info psi ON p.status = psi.id " +
        "WHERE p.id = :postId", Tuple.class
)
.setParameter( "postId", 1L )
.getSingleResult();

assertEquals(
    "PENDING", 
    tuple.get( "name" )
);

assertEquals(
    "Posts waiting to be approved by the admin", 
    tuple.get( "description" )
);

Cool, right?

Mapping a Java Enum to a database-specific Enumarated column type

Since we have been using PostgreSQL, let’s use the dedicated Enumared column type.

First, we need to create a new PostgreSQL enum type to mirror the Java PostStatus enumeration:

CREATE TYPE post_status_info AS ENUM (
    'PENDING', 
    'APPROVED', 
    'SPAM'
)

Now, to map this custom PostgreSQL type we can no longer use the default Hibernate-specific org.hibernate.type.EnumType because PostgreSQL expects an Object type, not a VARCHAR or an INT.

Luckily, we can easily create a custom Type by extending org.hibernate.type.EnumType:

public class PostgreSQLEnumType extends org.hibernate.type.EnumType {

    public void nullSafeSet(
            PreparedStatement st, 
            Object value, 
            int index, 
            SharedSessionContractImplementor session) 
        throws HibernateException, SQLException {
        if(value == null) {
            st.setNull( index, Types.OTHER );
        }
        else {
            st.setObject( 
                index, 
                value.toString(), 
                Types.OTHER 
            );
        }
    }
}

You don't have to create all these types manually. You can simply get them via Maven Central using the following dependency:

1
2
3
4
5
<dependency>
    <groupId>com.vladmihalceagroupId>
    <artifactId>hibernate-types-52artifactId>
    <version>${hibernate-types.version}version>
dependency>

For more info, check out the hibernate-types open-source project.

That’s it. Now, we can use the PostgreSQLEnumType as follows:

@Entity(name = "Post")
@Table(name = "post")
@TypeDef(
    name = "pgsql_enum",
    typeClass = PostgreSQLEnumType.class
)
public static class Post {

    @Id
    private Long id;

    private String title;

    @Enumerated(EnumType.STRING)
    @Column(columnDefinition = "post_status_info")
    @Type( type = "pgsql_enum" )
    private PostStatus status;

    //Getters and setters omitted for brevity
}

When inserting the Post entity, our JDBC statement interceptor will see the enum as a String, although in the database a dedicated Enum type is going to be used:

INSERT INTO post (
    status, 
    title, 
    id
) 
VALUES (
    'PENDING', 
    'High-Performance Java Persistence', 
    1
)

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As I explained in my book, the choice of database types can have a significant impact on application performance, and Enum types are no different.

Although you might be tempted to use the STRING EnumType, in reality, this is the least efficient option. So, if you have hundreds of millions of entries, it’s better to use either the ORDINAL EnumType or the database-specific type. PostgreSQL uses 4 bytes for an Enumerated type, therefore the ORDINAL which can use a SMALLINT is the most compact option. Although the MySQL Enum takes either 1 or 2 bytes, the MySQL Enum type has many downsides.

However, there are downsides to using ORDINAL as well. In case you need to reorder enum values or add/remove/rename entries, the STRING or the database-specific type will require a minimum amount of effort. On the other hand, removing the first element from ann Enum will require to shift all entries in case you are using ORDINAL.

Therefore, it’s all about trade-offs, so choose wisely.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

20 thoughts on “The best way to map an Enum Type with JPA and Hibernate

  1. Thanks for this article.

    I did notice, however, that the enum ordinal starts at 0, while the post_status_info table starts at 1, so won’t match with the join.

    I guess, however, it isn’t a big deal to do an INSERT from 0, however, as it’s not auto increment or anything.

Leave a Reply to Ivan Cancel reply

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