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.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<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

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

  1. I saw that you are creating a post_info table for improving readability, so does that mean I have to create a new description table for every enum type, if I am using ordinal value in the database?

    1. Check out the Relationship module of my High-Performance Java Persistence video course. Not only that you’ll find the answer to your question, but you are going to learn other useful tricks as well.

  2. Hello Vlad,

    I love your blog. It’s so insipiring when JPAing.

    I just wanted to contribute in some way. I developed some project in which I took the ordinal and enum-replicated-table solution. I’m very happy to see that for you that is the correct solution too. But what I was afraid of is that some developer could change the order in which the enum values are defined (he could may be add a new value and decide to reorder them alhpabetically or any other kind of twistted thinking that we developers have) and that would mess things up so much…

    I had two options, I could add an id field to the enum and implement an AttributeConverter (propbably the best solution) but it was in a legacy environment with Java EE 5, so no AttributeConverter at hand in that JPA version.

    So I ended up creating a unit tests for each enum, so that the tests will fail if someone happens to reorder the enum values:

    @Test
    public void testEnumOrder() {
    assertEquals(0, PostStatus.PENDING.ordinal());
    assertEquals(1, PostStatus.APPROVED.ordinal());
    assertEquals(2, PostStatus.SPAM.ordinal());
    }

    Which of both solutions do you like more? AttributeConverter or Test?

    Thank you.

  3. I was using a custom version of the PostgreSQLEnumType to support a Java Enum that looks something like this:
    https://gist.github.com/shawnjohnson/d93804882cee20dc22759fdaf1ca0358

    After upgrading to Hibernate 5.2 (Spring Boot 1.5), I am seeing the following error:

    o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 42804
    o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: column “bookType” is of type book_type but expression is of type character varying
    Hint: You will need to rewrite or cast the expression.
    Position: 1018

    This was working previously to the upgrade. Any idea of any differences from Hibernate 5.0 to 5.2 that I might need to look into?

    1. I guess another thing I was doing differently before the upgrade to 5.2, was actually not using your hibernate-types for this (it’s awesome for all other custom types!). I reverted my changes back to using Hibernate 5.0 and my Entity is annotated this way (https://gist.github.com/shawnjohnson/0ad86da3393acccc87eaf87be5f999d3), which seems to work okay. I’m also using Spring Boot 2.0 and JPA, but maybe Hibernate 5.2 no longer looks for or allows the @Type(type=”org.fake.StringEnumType”) style type references, without a separate explicit registration?

      1. I think I resolved this issue by removing my previous custom StringEnumType and simply the PostgreSQLEnumType as-is, as documented above. It seems some additional support for writing other types of enums was added in this later version of Hibernate?

  4. I’m interested in mapping the table post_status_info into an enum, like PENDING(0, “Posts waiting to be approved by the admin”), but is there a way I can map this enum in the Post class instead of using query?

    1. The only way to do that is if you create a custom Hibernate Type. However, what’s the benefit to justify all the work for doing that?

  5. I know that you can use some tricks to dynamically charge the enum. I thought this was the purpose.
    Anyway. Thanks for the article!

  6. So, you need to define the enum in Java also?
    In here : @Enumerated(EnumType.STRING)
    @Column(columnDefinition = “post_status_info”)
    @Type( type = “pgsql_enum” )
    private PostStatus status;
    What is the use of post_status_info if you need to define also the java enum ?

    1. The post_status_info is the database column, The Java Enum is the entity property. Otherwise, how would you want to interact with the database rows? That’s the purpos eof using an ORM tool.

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.