Using ORMs like JPA makes it easy to map your entity fields to the database. You can easily map a String, an Enum or a List just using annotations to specify how you want your data to be stored. Sometimes, however, you want your data to be saved in a very specific way. In that case is when you use Attribute Converters.
In this article I want to show you how I implemented an Attribute Converter to map a Set of Enums to a single database column.
We have the following Java Enum:
public enum MusicGenre {
ROCK, METAL, POP
}
Which we are going to map to an Entity field as a Set, just like this:
private Set<MusicGenre> genres;
We can use different approaches to store this data, for example using a @ElementCollection annotation.
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
private Set<MusicGenre> genres;
But this will generate us a new table (if we use DDL generation) with the Enum value and the related entity. I want this data to be stored as a single column for my use case, so I created a custom attribute converter so I can map any Set of Enums to a String Database column.
We want the converter to be reusable, so we will implement it using Java Generics. Also, we want to store it as a String in the Database, so we need to use a character as a separator. Finally, when we retrieve the data from the Database, we are going to create an EnumSet instead of a simple HashSet, because it has better performance. Here is the class using Java 8 Streams:
public abstract class GenericEnumSetConverter<E extends Enum<E>> implements AttributeConverter<Set<E>, String> {
private static final String SPLIT_CHAR = ";";
private final Class<E> clazz;
protected GenericEnumSetConverter(Class<E> clazz) {
this.clazz = clazz;
}
@Override
public String convertToDatabaseColumn(Set<E> values) {
if (values.isEmpty()) {
return null;
}
return values.stream()
.map(Enum::name)
.collect(Collectors.joining(SPLIT_CHAR));
}
@Override
public Set<E> convertToEntityAttribute(String string) {
if (StringUtils.isBlank(string)) {
return EnumSet.noneOf(clazz);
}
List<E> values = Stream.of(string.split(SPLIT_CHAR))
.map(e -> Enum.valueOf(clazz, e))
.collect(Collectors.toList());
return Sets.newEnumSet(values, clazz);
}
}
We can’t use this as a Converter yet, we need to create the concrete implementation for our Enum type (notice we need to also implements AttributeConverter to make it work).
@Converter
public class MusicGenreSetConverter extends GenericEnumSetConverter<MusicGenre>
implements AttributeConverter<Set<MusicGenre>, String> {
public MusicGenreSetConverter() {
super(MusicGenre.class);
}
}
Then we can annotate our field with this Converter
@Convert(converter = MusicGenreSetConverter.class)
private Set<MusicGenre> genres;
And that’s it. Our Enum set will be stored in our database as a String, using semicolons as a separator.
This approach has many advantages (we don’t need a new table for this mapping, for example), but it also has its disadvantages. If we want to query something using this field, is going to be slightly more difficult because we will need to take into account how our data is being stored in this column. Also, if we use libraries like QueryDSL to write queries, we may have runtime errors because of this custom converter.