In some cases, it may be useful to map a collection of enums to a single column in the database table. This can be achieved using a custom Hibernate type, which will map a collection of enums to a varchar type.
Keep in mind that this idea is probably only good if there are only a few items in a collection, their number is limited and enum fields are rarely or never going to change.
For example, we may have an enum which represents the user’s permissions:
public enum Permission {
CREATE, EDIT, DELETE, BLOCK, MUTE
}
Instead of using a List or some other collection type, Java has a specialized Set implementation for use with enum types, which is much more efficient – EnumSet.
First, we need to implement a descriptor that represents a Java type, here EnumSet, and does the conversion from EnumSet to a String (unwrap method) and from a String (wrap method). We’ll use enum’s ordinals and create a comma-separated String.
public class EnumSetTypeDescriptor extends AbstractTypeDescriptor<EnumSet> {
private static final String SEPARATOR = ",";
private Object[] enumConstants;
protected EnumSetTypeDescriptor(Class enumClass) {
super(EnumSet.class);
this.enumConstants = enumClass.getEnumConstants();
}
@Override
public String toString(EnumSet value) {
return (String) value.stream()
.map(Enum.class::cast)
.map(v -> Integer.toString(((Enum) v).ordinal()))
.collect(Collectors.joining(SEPARATOR));
}
@Override
public EnumSet fromString(String string) {
if (StringUtils.isEmpty(string))
return null;
List<Enum> list = Arrays.stream(StringUtils.split(string, SEPARATOR))
.map(ordinal -> enumConstants[Integer.parseInt(ordinal)])
.map(Enum.class::cast)
.collect(Collectors.toList());
if (list.isEmpty())
return null;
return EnumSet.copyOf(list);
}
@Override
public int extractHashCode(EnumSet value) {
return Objects.hashCode(value);
}
@Override
public <X> X unwrap(EnumSet value, Class<X> type, WrapperOptions options) {
if (value == null)
return null;
if (EnumSet.class.isAssignableFrom(type))
return (X) value;
if (String.class.isAssignableFrom(type))
return (X) toString(value);
throw unknownUnwrap(type);
}
@Override
public <X> EnumSet wrap(X value, WrapperOptions options) {
if (value == null)
return null;
if (EnumSet.class.isInstance(value))
return (EnumSet) value;
if (String.class.isInstance(value))
return fromString((String) value);
throw unknownWrap(value.getClass());
}
}
StringUtils is a class from Apache Commons Lang library.
Next, we need to implement a BasicType, and since we would be using a single column to store the value, there is a specific implementation – AbstractSingleColumnStandardBasicType.
Also, we would like from this Hibernate custom type to support any enum type, so we need to create a parameterized type by also implementing DynamicParameterizedType.
public class EnumSetType extends AbstractSingleColumnStandardBasicType<EnumSet> implements DynamicParameterizedType {
public EnumSetType() {
super(VarcharTypeDescriptor.INSTANCE, null);
}
@Override
public String getName() {
return "enum-set";
}
@Override
public String[] getRegistrationKeys() {
return new String[] { getName(), "EnumSet", EnumSet.class.getName() };
}
@Override
public void setParameterValues(Properties parameters) {
String enumClassName = (String) parameters.get("enumClass");
try {
setJavaTypeDescriptor(new EnumSetTypeDescriptor(Class.forName(enumClassName)));
}
catch (ClassNotFoundException e) {}
}
}
To make use of this custom type, we must declare it in an entity using the @TypeDef annotation, annotate the field with a @Type annotation and pass the fully qualified enum class name as a parameter.
@Entity
@Table(name = "user")
@TypeDef(typeClass = EnumSetType.class)
public class User {
// ...
@Column(length = 10)
@Type(type = "enum-set", parameters = {
@org.hibernate.annotations.Parameter(name = "enumClass", value = "xyz.joshefin.example.domain.constant.Permission")
})
private EnumSet<Permission> permissions;
// ...
}
For some other custom types, check out a great library Hibernate Types created by Vlad Mihalcea.
2 comments
After a few days of looking for the best solution to store the list of enums in one column in DB, I ended up with this article. I just want to say thank you, this is the solution that I was looking for. 🙂
Just a note for those who struggled with enum which is inner class – you have to use “$” to point to inner class:
@org.hibernate.annotations.Type(
type = “com.thevegcat.app.enums.EnumSetType”,
parameters = {
@org.hibernate.annotations.Parameter(
name = “enumClass”,
value = “com.thevegcat.app.entities.tag.Tag$Visibility”
)
}
)