There are quite a few different approaches one can take to map enums in entities. I would separate them into three groups:
- The ones which can be used with JPA 2.0 or less
- Usage of @Enumerated annotation to either save enum as ordinal or its name.
- Usage of additional pairs of setter/getter.
- Usage of @PostLoad and @PrePersist callbacks.
- The ones which can be used with JPA 2.1
- Usage of AttributeConverter interface and @Convert annotation.
- Vendor specific approach
- Implementing custom user type in Hibernate.
Classic ways to map enums
So how actually enum can be used as an entity property?
@Enumerated(EnumType.ORDINAL) - the default approach
If you have an enum field in your entity the easiest and the default way of mapping it is using @Enumerated annotation on that field (or on the corresponding property (getter method) if you prefer using getters for entity mapping purposes). Using this approach on my opinion is suitable only in cases when mapped enum is very stable (will not be changed) and has a natural order of constants in it. If this is not true about the mapped enum, then you should consider other mapping options to save yourself from data consistency issues. To show this case let's consider next entity
Where Month is a next enum:
The mapping in this case will be next:
As EnumType.ORDINAL is a default option for @Enumerated it can be omitted. Month is a good example when ordinal can be used - as most likely there will be no new months in the nearest future and the order shouldn't change as well.
As EnumType.ORDINAL is a default option for @Enumerated it can be omitted. Month is a good example when ordinal can be used - as most likely there will be no new months in the nearest future and the order shouldn't change as well.
@Enumerated(EnumType.STRING) - not far away from the default
As you might have guessed this approach is not dependent on the order of enum constants anymore, but on their names. If you put @Enumerated(EnumType.STRING) on your enum field it'll be persisted using your enum value name (Enum#name()). To show this case let's add Gender enum and update our Person entity by adding gender to it.
Now let's write a test for person entity and check how the generated schema will look like. Here's a test that creates one Person instance and persists it to H2 database. After that we are querying for the attributes of the person that we just saved to see that enums were mapped as we expect them to.
So we have two enums Month - mapped as ordinal, and Gender mapped as string running the test we'll see that the SQL to generate the schema is next:
Now let's write a test for person entity and check how the generated schema will look like. Here's a test that creates one Person instance and persists it to H2 database. After that we are querying for the attributes of the person that we just saved to see that enums were mapped as we expect them to.
So we have two enums Month - mapped as ordinal, and Gender mapped as string running the test we'll see that the SQL to generate the schema is next:
Hacky setter/getter
But what should we do if we have a long enum constants names or more important - what if we want to actually have some other representation of enum in the database and not its name? One of the options that we have is to add protected setter/getter methods for our enum property and mark the getter to be persisted, while the actual property will be marked as @Transient and will not be stored. To show this case let's introduce a Grade enum and HomeWork entity.
As you can see this Grade enum is more complicated than just a label. And we would like charGrade to be stored in the database. To be able to use these "hacky"setters/getters we would also need to be able to find a Grade by its charGrade representation so that's why we will add Grade#byChar() static method to it:
and then the entity mapping will look like this:
So we marked the actual property with @Transient to hide it from persistence and also made the additional setter/getter protected as they are not for external users but just for persistence.
To make sure that everything works fine let's run next test:
Using
This approach looks a bit safer (as we do not expose any setters/getters) than the previous one. But still it's not perfect. The idea behind it is to have two fields one for application use and one for database. Synchronization of the fields is performed using @PostLoad and @PrePersist callbacks. Before saving an entity we are setting a value from the application field to database one, and after loading an entity we are updating the application value based on the database one. To show this case let's use the same Grade enum with ClassWork entity.
As we can see this approach requires additional field and two callbacks methods which are cluttering the entity. So even though this is better than the previous approaches it's still not ideal. So what can we do? Well JPA 2.1 to the rescue!
One of the things introduced in JPA 2.1 revision was Attribute Converter. How can we benefit from it for enum mapping? It's actually pretty easy, as all we need to do is - implement an AttributeConverter interface and put a @Convert annotation on the enum field. Let's do so using the same Grade enum and TestExam entity:
As you can see this Grade enum is more complicated than just a label. And we would like charGrade to be stored in the database. To be able to use these "hacky"setters/getters we would also need to be able to find a Grade by its charGrade representation so that's why we will add Grade#byChar() static method to it:
and then the entity mapping will look like this:
So we marked the actual property with @Transient to hide it from persistence and also made the additional setter/getter protected as they are not for external users but just for persistence.
To make sure that everything works fine let's run next test:
Using combination of @PostLoad and @PrePersist
As we can see this approach requires additional field and two callbacks methods which are cluttering the entity. So even though this is better than the previous approaches it's still not ideal. So what can we do? Well JPA 2.1 to the rescue!
Right ways to map enums
Using combination of @Convert and AttributeConverter
We need to implement Attribute Converter first:
As we can see it's a very simple interface with two methods, one is converting a value to be persisted and another converts retrieved value from database to the one which can be used by the application. Having converter we can proceed to mapping our entity, which is pretty easy:
As one can see the code with Attribute Converter is much more cleaner and classes are more cohesive now. Converting responsibilities are taken by Attribute Converter and entity is only responsible for mappings and has nothing unrelated (like for example additional properties, fields and callbacks as in previous examples).
Bonus: Using Hibernate's custom user type
There can be cases when enum has a couple of fields and you would like to store some of those, or even maybe all of them, but you don't want use an entity but an enum. What we can do in such situation is either use the @PostLoad and @PrePersist callbacks approach and add the fields that we want to store to the entity. Another way is to serialize our enum to JSON and persist it. Some databases like Postgresql and MySQL allows JSONs stored inside as a specific type and has some JSON functions for querying. To show this case let's use a Question entity and QuestionKind enum:And the code for them is next:
To actually be able to do this kind of mapping we first need to implement AbstractTypeDescriptor and AbstractSingleColumnStandardBasicType. Having these we can register them within Hibernate types and use a @Type(type = "com.blogspot.that_java_guy.converters.type.QuestionKindType") on our enum field. A type parameter can either be a fully qualified name of custom type or its name returned by AbstractSingleColumnStandardBasicType#getName(). Here's a possible way to implement a custom type.
As I've mentioned before - I'd like to store an enum as a JSON in the database. But it's also completely fine to use the same approach and for example map our enum to its id or some other field.
All the code samples provided in this post are available on github. You'll also find tests for each case there. Feel free to check them out and play with them. And if you find a bug or would like to suggest any improvements - pull requests are welcomed ;).
To be continued...
COMMENTS