There are quite a few different approaches one can take to map enums in entities. I would separate them into three groups:

  1. The ones which can be used with JPA  2.0 or less
    1. Usage of @Enumerated annotation to either save enum as ordinal or its name.
    2. Usage of additional pairs of setter/getter.
    3. Usage of @PostLoad and @PrePersist callbacks.
  2. The ones which can be used with JPA  2.1
    1. Usage of AttributeConverter interface and @Convert annotation.
  3. Vendor specific approach
    1. 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:
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL,
MAY, JUNE, JULY, AUGUST,
SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER,;
}
view raw Month.java hosted with ❤ by GitHub

The mapping in this case will be next:
@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;
}
...
}
view raw Person.java hosted with ❤ by GitHub
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.
@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:
create table Person (
id bigint not null,
favorite_month integer,
first_name varchar(255),
gender varchar(255),
last_name varchar(255),
primary key (id)
)
view raw Person.sql hosted with ❤ by GitHub

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:
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 );
}
}
view raw Grade.java hosted with ❤ by GitHub
and then the entity mapping will look like this:
@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 );
}
}
view raw HomeWork.java hosted with ❤ by GitHub
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:
@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 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.
@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 );
}
}
view raw ClassWork.java hosted with ❤ by GitHub

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

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: 

We need to implement Attribute Converter first:
@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 );
}
}
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:
@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;
....
}
view raw TestExam.java hosted with ❤ by GitHub
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:
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,;
}
}
@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;
...
}
view raw Question.java hosted with ❤ by GitHub

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