问题
Problem I am trying to solve
I am trying to implement enum mapping for Hibernate. So far I have researched available options, and both the @Enumerated(EnumType.ORDINAL)
and @Enumerated(EnumType.STRING)
seemed inadequate for my needs. The @Enumerated(EnumType.ORDINAL)
seems to be very error-prone, as a mere reordering of enum constants can mess the mapping up, and the @Enumerated(EnumType.STRING)
does not suffice too, as the database I work with is already full of values to be mapped, and these values are not what I would like my enum constants to be named like (the values are foreign language strings / integers).
Currently, all these values are being mapped to String / Integer properties. At the same time the properties should only allow for a restricted sets of values (imagine meetingStatus
property allowing for Strings: PLANNED
, CANCELED
, and DONE
. Or another property allowing for a restricted set of Integer values: 1
, 2
, 3
, 4
, 5
).
My idea was to replace the implementation with enums to improve the type safety of the code. A good example where the String / Integer implementation could cause errors is String method parameter representing such value - with String, anything goes there. Having an Enum parameter type on the other hand introduces compile time safety.
My best approach so far
The only solution that seemed to fulfill my needs was to implement custom javax.persistence.AttributeConverter
with @Converter
annotation for every enum. As my model would require quite a few enums, writing custom converter for each of them started to seem like a madness really quickly. So I searched for a generic solution to the problem -> how to write a generic converter for any type of enum. The following answer was of big help here: https://stackoverflow.com/a/23564597/7024402. The code example in the answer provides for somewhat generic implementation, yet for every enum there is still a separate converter class needed. The author of the answer also continues:
"The alternative would be to define a custom annotation, patch the JPA provider to recognize this annotation. That way, you could examine the field type as you build the mapping information and feed the necessary enum type into a purely generic converter."
And that's what I think I would be interested in. I could, unfortunately, not find any more information about that, and I would need a little more guidance to understand what needs to be done and how would it work with this approach.
Current Implementation
public interface PersistableEnum<T> {
T getValue();
}
public enum IntegerEnum implements PersistableEnum<Integer> {
ONE(1),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6);
private int value;
IntegerEnum(int value) {
this.value = value;
}
@Override
public Integer getValue() {
return value;
}
}
public abstract class PersistableEnumConverter<E extends PersistableEnum<T>, T> implements AttributeConverter<E, T> {
private Class<E> enumType;
public PersistableEnumConverter(Class<E> enumType) {
this.enumType = enumType;
}
@Override
public T convertToDatabaseColumn(E attribute) {
return attribute.getValue();
}
@Override
public E convertToEntityAttribute(T dbData) {
for (E enumConstant : enumType.getEnumConstants()) {
if (enumConstant.getValue().equals(dbData)) {
return enumConstant;
}
}
throw new EnumConversionException(enumType, dbData);
}
}
@Converter
public class IntegerEnumConverter extends PersistableEnumConverter<IntegerEnum, Integer> {
public IntegerEnumConverter() {
super(IntegerEnum.class);
}
}
This is how I was able to achieve the partially generic converter implementation.
GOAL: Getting rid of the need to create new converter class for every enum.
回答1:
Luckily, you should not patch the hibernate for this.
- You can declare an annotation like the following:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.sql.Types;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface EnumConverter
{
Class<? extends PersistableEnum<?>> enumClass() default IntegerEnum.class;
int sqlType() default Types.INTEGER;
}
- A hibernate user type like the following:
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.DynamicParameterizedType;
import org.hibernate.usertype.UserType;
public class PersistableEnumType implements UserType, DynamicParameterizedType
{
private int sqlType;
private Class<? extends PersistableEnum<?>> clazz;
@Override
public void setParameterValues(Properties parameters)
{
ParameterType reader = (ParameterType) parameters.get(PARAMETER_TYPE);
EnumConverter converter = getEnumConverter(reader);
sqlType = converter.sqlType();
clazz = converter.enumClass();
}
private EnumConverter getEnumConverter(ParameterType reader)
{
for (Annotation annotation : reader.getAnnotationsMethod()){
if (annotation instanceof EnumConverter) {
return (EnumConverter) annotation;
}
}
throw new IllegalStateException("The PersistableEnumType should be used with @EnumConverter annotation.");
}
@Override
public int[] sqlTypes()
{
return new int[] {sqlType};
}
@Override
public Class<?> returnedClass()
{
return clazz;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException
{
return Objects.equals(x, y);
}
@Override
public int hashCode(Object x) throws HibernateException
{
return Objects.hashCode(x);
}
@Override
public Object nullSafeGet(ResultSet rs,
String[] names,
SharedSessionContractImplementor session,
Object owner) throws HibernateException, SQLException
{
Object val = null;
if (sqlType == Types.INTEGER) val = rs.getInt(names[0]);
if (sqlType == Types.VARCHAR) val = rs.getString(names[0]);
if (rs.wasNull()) return null;
for (PersistableEnum<?> pEnum : clazz.getEnumConstants())
{
if (Objects.equals(pEnum.getValue(), val)) return pEnum;
}
throw new IllegalArgumentException("Can not convert " + val + " to enum " + clazz.getName());
}
@Override
public void nullSafeSet(PreparedStatement st,
Object value,
int index,
SharedSessionContractImplementor session) throws HibernateException, SQLException
{
if (value == null) {
st.setNull(index, sqlType);
}
else {
PersistableEnum<?> pEnum = (PersistableEnum<?>) value;
if (sqlType == Types.INTEGER) st.setInt(index, (Integer) pEnum.getValue());
if (sqlType == Types.VARCHAR) st.setString(index, (String) pEnum.getValue());
}
}
@Override
public Object deepCopy(Object value) throws HibernateException
{
return value;
}
@Override
public boolean isMutable()
{
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException
{
return Objects.toString(value);
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException
{
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException
{
return original;
}
}
- And then, you can use it:
import org.hibernate.annotations.Type;
@Entity
@Table(name="TST_DATA")
public class TestData
{
...
@EnumConverter(enumClass = IntegerEnum.class, sqlType = Types.INTEGER)
@Type(type = "com.example.converter.PersistableEnumType")
@Column(name="INT_VAL")
public IntegerEnum getIntValue()
...
@EnumConverter(enumClass = StringEnum.class, sqlType = Types.VARCHAR)
@Type(type = "com.example.converter.PersistableEnumType")
@Column(name="STR_VAL")
public StringEnum getStrValue()
...
}
See also the chapter 5.3.3 Extending Hibernate with UserTypes at the excellent book "Java Persistence with Hibernate" by Bauer, King, Gregory.
回答2:
Simplifying:
import com.pismo.apirest.mvc.enums.OperationType;
import com.pismo.apirest.mvc.enums.support.PersistableEnum;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@SuppressWarnings("unused")
public interface EnumsConverters {
@RequiredArgsConstructor
abstract class AbstractPersistableEnumConverter<E extends Enum<E> & PersistableEnum<I>, I> implements AttributeConverter<E, I> {
private final E[] enumConstants;
public AbstractPersistableEnumConverter(@NonNull Class<E> enumType) {
enumConstants = enumType.getEnumConstants();
}
@Override
public I convertToDatabaseColumn(E attribute) {
return Objects.isNull(attribute) ? null : attribute.getId();
}
@Override
public E convertToEntityAttribute(I dbData) {
return fromId(dbData, enumConstants);
}
public E fromId(I idValue) {
return fromId(idValue, enumConstants);
}
public static <E extends Enum<E> & PersistableEnum<I>, I> E fromId(I idValue, E[] enumConstants) {
return Objects.isNull(idValue) ? null : Stream.of(enumConstants)
.filter(e -> e.getId().equals(idValue))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(
String.format("Does not exist %s with ID: %s", enumConstants[0].getClass().getSimpleName(), idValue)));
}
}
@Converter(autoApply = true)
class OperationTypeConverter extends AbstractPersistableEnumConverter<OperationType, Integer> {
public OperationTypeConverter() {
super(OperationType.class);
}
}
}
来源:https://stackoverflow.com/questions/58681902/generic-enum-jpa-attributeconverter-implementation