Реализация общего перечисления JPA AttributeConverter - PullRequest
4 голосов
/ 03 ноября 2019

Проблема, которую я пытаюсь решить

Я пытаюсь реализовать отображение enum для Hibernate. До сих пор я исследовал доступные варианты, и оба значения @Enumerated(EnumType.ORDINAL) и @Enumerated(EnumType.STRING) показались мне неадекватными. @Enumerated(EnumType.ORDINAL), кажется, очень подвержен ошибкам, так как простое переупорядочение констант enum может испортить отображение, и @Enumerated(EnumType.STRING) тоже недостаточно, так как база данных, с которой я работаю, уже полна значений для отображения,и эти значения не соответствуют именам моих констант enum (значения являются строками / целыми числами на иностранных языках).

В настоящее время все эти значения сопоставляются со свойствами String / Integer. В то же время свойства должны разрешать только ограниченный набор значений (представьте себе свойство meetingStatus, допускающее строки: PLANNED, CANCELED и DONE. Или другое свойство, допускающее ограниченный набор значений типа Integer:1, 2, 3, 4, 5).

Моя идея состояла в том, чтобы заменить реализацию перечислениями для повышения безопасности типов кода. Хорошим примером, когда реализация String / Integer может привести к ошибкам, является параметр метода String, представляющий такое значение - с String все идет туда. С другой стороны, наличие типа параметра Enum обеспечивает безопасность во время компиляции.

На данный момент мой лучший подход

Единственное решение, которое, казалось, удовлетворяло моим потребностям, было реализовать пользовательский javax.persistence.AttributeConverter с @Converter аннотация для каждого перечисления. Поскольку моей модели потребовалось бы довольно много перечислений, написание пользовательского конвертера для каждого из них стало казаться действительно безумным. Поэтому я искал общее решение проблемы -> как написать универсальный конвертер для любого типа перечисления. Следующий ответ очень помог здесь: https://stackoverflow.com/a/23564597/7024402. Пример кода в ответе предусматривает несколько общую реализацию, но для каждого перечисления все еще необходим отдельный класс преобразователя. Автор ответа также продолжает:

"Альтернативой может быть определение пользовательской аннотации, исправление поставщика JPA для распознавания этой аннотации. Таким образом, вы можете проверить тип поля при созданииотображая информацию и передавая необходимый тип enum в чисто общий конвертер. "

И это то, что, я думаю, меня заинтересует. К сожалению, я не смогу найти больше информации об этом, и япотребуется немного больше руководств, чтобы понять, что нужно сделать и как он будет работать с этим подходом.

Текущая реализация

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);
    }
}

Этокак мне удалось добиться реализации частично универсального конвертера.

ЦЕЛЬ: избавиться от необходимости создавать новый класс конвертера для каждого перечисления.

1 Ответ

1 голос
/ 09 ноября 2019

К счастью, вы не должны исправлять спящий режим для этого.

  1. Вы можете объявить аннотацию, подобную следующей:
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;
}
Тип пользователя Hibernate, подобный следующему:
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;
   }
}

И затем, вы можете использовать его:
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()
   ...
}

См. Также главу 5.3.3 Расширение Hibernate с помощью пользовательских типов в превосходной книге "Сохранение Javaс гибернацией "Бауэра, короля Грегори .

...