Generic enum JPA AttributeConverter implementation

╄→гoц情女王★ 提交于 2021-02-18 07:39:28

问题


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.

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

  1. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!