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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum Month { | |
JANUARY, FEBRUARY, MARCH, APRIL, | |
MAY, JUNE, JULY, AUGUST, | |
SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER,; | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
public class Person { | |
@Id | |
@GeneratedValue | |
private Long id; | |
@Column(name = "first_name") | |
private String firstName; | |
@Column(name = "last_name") | |
private String lastName; | |
@Enumerated(EnumType.ORDINAL) | |
@Column(name = "favorite_month") | |
private Month favoriteMonth; | |
public Person() { | |
} | |
public Person(String firstName, String lastName, Month favoriteMonth) { | |
this.firstName = firstName; | |
this.lastName = lastName; | |
this.favoriteMonth = favoriteMonth; | |
} | |
... | |
} |
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void testEnum() { | |
Person person = new Person( "John", "Doe", Gender.MALE, Month.AUGUST ); | |
entityManager.persist( person ); | |
entityManager.flush(); | |
Assertions.assertNotNull( entityManager.find( Person.class, person.getId() ), "entity not found" ); | |
Object[] resultRow = (Object[]) entityManager | |
.createNativeQuery( "SELECT p.first_name, p.last_name, p.gender, p.favorite_month FROM person AS p WHERE p.id = :id" ) | |
.setParameter( "id", person.getId() ) | |
.getSingleResult(); | |
Assertions.assertEquals( "John", resultRow[0], "first name should match" ); | |
Assertions.assertEquals( "Doe", resultRow[1], "last name should match" ); | |
Assertions.assertEquals( Gender.MALE.name(), resultRow[2], "gender should match" ); | |
Assertions.assertEquals( Month.AUGUST.ordinal(), resultRow[3], "month should match" ); | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create table Person ( | |
id bigint not null, | |
favorite_month integer, | |
first_name varchar(255), | |
gender varchar(255), | |
last_name varchar(255), | |
primary key (id) | |
) |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum Grade { | |
GRADE_A( 'A', 100, 91 ), | |
GRADE_B( 'B', 90, 81 ), | |
GRADE_C( 'C', 80, 71 ), | |
GRADE_D( 'D', 70, 61 ), | |
GRADE_E( 'E', 60, 51 ), | |
GRADE_F( 'F', 50, 0 ),; | |
public final Character charGrade; | |
public final int maxPoints; | |
public final int minPoints; | |
Grade(Character charGrade, int maxPoints, int minPoints) { | |
this.charGrade = charGrade; | |
this.maxPoints = maxPoints; | |
this.minPoints = minPoints; | |
} | |
public static Grade byChar(Character charGrade) { | |
return Stream.of( values() ) | |
.filter( grade -> grade.charGrade.equals( charGrade ) ) | |
.findAny() | |
.orElse( null ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Table(name = "home_work") | |
public class HomeWork { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String subject; | |
@Transient | |
private Grade grade; | |
@ManyToOne(cascade = CascadeType.PERSIST) | |
private Person student; | |
.... | |
@Column(name = "grade") | |
@Access(AccessType.PROPERTY) | |
protected Character getDbGrade() { | |
return grade != null ? grade.charGrade : null; | |
} | |
protected void setDbGrade(Character grade) { | |
this.grade = Grade.byChar( grade ); | |
} | |
} |
To make sure that everything works fine let's run next test:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void testEnum() { | |
HomeWork homeWork = new HomeWork( | |
"Learn JPA enum mappings", | |
Grade.GRADE_B, | |
new Person( "Angie", "Doe", Gender.FEMALE, Month.APRIL ) | |
); | |
entityManager.persist( homeWork ); | |
entityManager.flush(); | |
Object[] result = (Object[]) entityManager.createNativeQuery( "SELECT h.subject, h.grade FROM home_work AS h WHERE h.id = :id" ) | |
.setParameter( "id", homeWork.getId() ) | |
.getSingleResult(); | |
Assertions.assertEquals( "Learn JPA enum mappings", result[0] ); | |
Assertions.assertEquals( Grade.GRADE_B.charGrade.toString(), result[1] ); | |
} |
Using combination of @PostLoad and @PrePersist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Table(name = "class_work") | |
public class ClassWork { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String subject; | |
@Transient | |
private Grade grade; | |
@Column(name = "grade") | |
private Character dbGrade; | |
@ManyToOne(cascade = CascadeType.PERSIST) | |
private Person student; | |
... | |
@PrePersist | |
private void prePersist() { | |
this.dbGrade = grade == null ? null : grade.charGrade; | |
} | |
@PostLoad | |
private void postLoad() { | |
this.grade = Grade.byChar( dbGrade ); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Converter | |
public class GradeConverter implements AttributeConverter<Grade, Character> { | |
@Override | |
public Character convertToDatabaseColumn(Grade attribute) { | |
return attribute.charGrade; | |
} | |
@Override | |
public Grade convertToEntityAttribute(Character dbData) { | |
return Grade.byChar( dbData ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Table(name = "test_exam") | |
public class TestExam { | |
@Id | |
@GeneratedValue | |
private Long id; | |
@Column(name = "number_of_passed_questions") | |
private int numberOfPassedQuestions; | |
@Convert(converter = GradeConverter.class) | |
@Column(name = "exam_grade") | |
private Grade examGrade; | |
@OneToOne(cascade = CascadeType.PERSIST) | |
private Person student; | |
@ManyToMany(cascade = CascadeType.PERSIST) | |
private List<Question> questions; | |
.... | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum QuestionKind { | |
MULTIPLE_CHOICE_YES_NO( 1, "multiple choice of yes/no options", AnswerKind.BOOLEAN, true ), | |
SINGLE_YES_NO( 2, "simple yes/no question", AnswerKind.BOOLEAN, false ), | |
FREE_TEXT( 3, "a long answer in a free text form", AnswerKind.TEXT, false ), | |
CALCULATION( 4, "a result of performed calculus operations", AnswerKind.NUMBER, false ), | |
MULTIPLE_CALCULATION( 5, "multiple results of performed calculus operations", AnswerKind.NUMBER, true ),; | |
private int id; | |
private String questionTypeDescription; | |
private AnswerKind answerKind; | |
private boolean multipleAnswersPossible; | |
... | |
public static QuestionKind byId(int id) { | |
return Stream.of( values() ) | |
.filter( kind -> kind.id == id ) | |
.findAny() | |
.orElse( null ); | |
} | |
public enum AnswerKind { | |
TEXT, NUMBER, BOOLEAN, CHARACTER,; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
public class Question { | |
@Id | |
@GeneratedValue | |
private Long id; | |
private String question; | |
@Column(name = "kind", length = 1024) | |
@Type(type = "com.blogspot.that_java_guy.converters.type.QuestionKindType") | |
private QuestionKind questionKind; | |
... | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class QuestionKindType extends AbstractSingleColumnStandardBasicType<QuestionKind> { | |
public static final QuestionKindType INSTANCE = new QuestionKindType(); | |
public QuestionKindType() { | |
super( VarcharTypeDescriptor.INSTANCE, QuestionKindJavaTypeDescriptor.INSTANCE ); | |
} | |
@Override | |
public String getName() { | |
return "questionKind"; | |
} | |
@Override | |
protected boolean registerUnderJavaType() { | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class QuestionKindJavaTypeDescriptor extends AbstractTypeDescriptor<QuestionKind> { | |
private static GsonBuilder builder = new GsonBuilder() | |
.registerTypeAdapter( QuestionKind.class, new CustomJsonSerializer() ); | |
public static final QuestionKindJavaTypeDescriptor INSTANCE = new QuestionKindJavaTypeDescriptor(); | |
protected QuestionKindJavaTypeDescriptor() { | |
super( QuestionKind.class ); | |
} | |
@Override | |
public String toString(QuestionKind value) { | |
return builder.create().toJson( value ); | |
} | |
@Override | |
public QuestionKind fromString(String string) { | |
if ( string == null ) { | |
return null; | |
} | |
Map parsedJson = builder.create().fromJson( string, Map.class ); | |
return QuestionKind.byId( (int) parsedJson.get( "id" ) ); | |
} | |
@Override | |
public <X> X unwrap(QuestionKind value, Class<X> type, WrapperOptions options) { | |
return StringTypeDescriptor.INSTANCE.unwrap( | |
toString( value ), | |
type, | |
options | |
); | |
} | |
@Override | |
public <X> QuestionKind wrap(X value, WrapperOptions options) { | |
return fromString( StringTypeDescriptor.INSTANCE.wrap( value, options ) ); | |
} | |
... | |
} |
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