Mapping enums done right with @Convert in JPA 2.1
Torshovdalen |
enum
as a property of your @Entity
is often very good choice, however JPA prior to 2.1 didn’t handle them very well. It gave you 2+1 choices:@Enumerated(EnumType.ORDINAL)
(default) will mapenum
values usingEnum.ordinal()
. Basically first enumerated value will be mapped to0
in database column, second to1
, etc. This is very compact and works great to the point when you want to modify your enum. Removing or adding value in the middle or rearranging them will totally break existing records. Ouch! To make matters worse, unit and integration tests often work on clean database, so they won’t catch discrepancy in old data.@Enumerated(EnumType.STRING)
is much safer because it stores string representation ofenum
. You can now safely add new values and move them around. However renamingenum
in Java code will still break existing records in DB. Even more important, such representation is very verbose, unnecessarily consuming database resources.- You can also use raw representation (e.g. single
char
orint
) and map it manually back and forth in@PostLoad
/@PrePersist
/@PreUpdate
events. Most flexible and safe from database perspective, but quite ugly.
We will start from sample Spring application developed as part of “Poor man’s CRUD: jqGrid, REST, AJAX, and Spring MVC in one house” article. That version had no persistence, so we will add thin DAO layer on top of Spring Data JPA backed by Eclipselink. Only entity so far is
Book
:@EntityWhere
public class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
private Integer id;
//...
private Cover cover;
//...
}
Cover
is an enum
:public enum Cover {Neither
PAPERBACK, HARDCOVER, DUST_JACKET
}
ORDINAL
nor STRING
is a good choice here. The former because rearranging first three values in any way will break loading of existing records. The latter is too verbose. Here is where custom converters in JPA come into play:import javax.persistence.AttributeConverter;OK, I won’t insult you, my dear reader, explaining this. Converting enum to whatever will be stored in relational database and vice-versa. Theoretically JPA provider should apply converters automatically if they are declared with:
import javax.persistence.Converter;
@Converter
public class CoverConverter implements AttributeConverter<Cover, String> {
@Override
public String convertToDatabaseColumn(Cover attribute) {
switch (attribute) {
case DUST_JACKET:
return "D";
case HARDCOVER:
return "H";
case PAPERBACK:
return "P";
default:
throw new IllegalArgumentException("Unknown" + attribute);
}
}
@Override
public Cover convertToEntityAttribute(String dbData) {
switch (dbData) {
case "D":
return DUST_JACKET;
case "H":
return HARDCOVER;
case "P":
return PAPERBACK;
default:
throw new IllegalArgumentException("Unknown" + dbData);
}
}
}
@Converter(autoApply = true)It didn’t work for me. Moreover declaring them explicitly instead of
@Enumerated
in @Entity
class didn’t work as well:import javax.persistence.Convert;Resulting in exception:
//...
@Convert(converter = CoverConverter.class)
private Cover cover;
Exception Description: The converter class [com.blogspot.nurkiewicz.CoverConverter]Bug or feature, I had to mention converter in
specified on the mapping attribute [cover] from the class [com.blogspot.nurkiewicz.Book] was not found.
Please ensure the converter class name is correct and exists with the persistence unit definition.
orm.xml
:<?xml version="1.0"?>And it flies! I have a freedom of modifying my
<entity-mappings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/orm" version="2.1">
<converter class="com.blogspot.nurkiewicz.CoverConverter"/>
</entity-mappings>
Cover
enum (adding, rearranging, renaming) without affecting existing records.One tip I would like to share with you is related to maintainability. Every time you have a piece of code mapping from or to
enum
, make sure it’s tested properly. And I don’t mean testing every possible existing value manually. I am more after a test making sure that new enum
values are reflected in mapping code. Hint: code below will fail (by throwing IllegalArgumentException
) if you add new enum
value but forget to add mapping code from it:for (Cover cover : Cover.values()) {
new CoverConverter().convertToDatabaseColumn(cover);
}
Custom converters in JPA 2.1 are much more useful than what we saw. If you combine JPA with Scala, you can use
@Converter
to map database columns directly to scala.math.BigDecimal
, scala.Option
or small case class. In Java there will finally be a portable way of mapping Joda time. Last but not least, if you like (very) strongly typed domain, you may wish to have PhoneNumber
class (with isInternational()
, getCountryCode()
and custom validation logic) instead of String
or long
. This small addition in JPA 2.1 will surely improve domain objects quality significantly.If you wish to play a bit with this feature, sample Spring web application is available on GitHub.
Tags: eclipselink, jpa, spring