是否可以为JPA编写一个通用的枚举转换器?

i1icjdpr  于 2023-03-03  发布在  其他
关注(0)|答案(7)|浏览(228)

我想为JPA写一个转换器,把任何枚举都存储为大写。我们遇到的一些枚举还没有遵循只使用大写字母的约定,所以直到它们被重构之前,我仍然存储未来的值。
目前为止我得到的是:

package student;

public enum StudentState {

    Started,
    Mentoring,
    Repeating,
    STUPID,
    GENIUS;
}

我希望将"已开始"存储为"已开始"等。

package student;

import jpa.EnumUppercaseConverter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "STUDENTS")
public class Student implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mId;

    @Column(name = "LAST_NAME", length = 35)
    private String mLastName;

    @Column(name = "FIRST_NAME", nullable = false, length = 35)
    private String mFirstName;

    @Column(name = "BIRTH_DATE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date mBirthDate;

    @Column(name = "STUDENT_STATE")
    @Enumerated(EnumType.STRING)
    @Convert(converter = EnumUppercaseConverter.class)
    private StudentState studentState;

}

转换器当前如下所示:

package jpa;

import javax.persistence.AttributeConverter;
import java.util.EnumSet;

public class EnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {

    private Class<E> enumClass;

    @Override
    public String convertToDatabaseColumn(E e) {
        return e.name().toUpperCase();
    }

    @Override
    public E convertToEntityAttribute(String s) {
        // which enum is it?
        for (E en : EnumSet.allOf(enumClass)) {
            if (en.name().equalsIgnoreCase(s)) {
                return en;
            }
        }
        return null;
    }

}

不起作用的是我不知道enumClass在运行时是什么。2而且我也找不到一种方法来把这个信息传递给@Converter注解中的转换器。
那么有没有办法给转换器添加参数或者做点小手脚呢?或者有没有别的办法呢?
我使用的是Eclipse链接2.4.2
谢谢!

w7t8yxp5

w7t8yxp51#

基于@scottb解决方案,我做了这个,在hib4.3上测试:(没有休眠类,应该可以在JPA上运行)
接口枚举必须实现:

public interface PersistableEnum<T> {
    public T getValue();
}

基本抽象转换器:

@Converter
public abstract class AbstractEnumConverter<T extends Enum<T> & PersistableEnum<E>, E> implements AttributeConverter<T, E> {
    private final Class<T> clazz;

    public AbstractEnumConverter(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public E convertToDatabaseColumn(T attribute) {
        return attribute != null ? attribute.getValue() : null;
    }

    @Override
    public T convertToEntityAttribute(E dbData) {
        T[] enums = clazz.getEnumConstants();

        for (T e : enums) {
            if (e.getValue().equals(dbData)) {
                return e;
            }
        }

        throw new UnsupportedOperationException();
    }
}

必须为每个枚举创建一个转换器类,我发现在枚举内部创建静态类更容易:(JPA/Hibernate可以只提供枚举的接口,哦,好吧...)

public enum IndOrientation implements PersistableEnum<String> {
    LANDSCAPE("L"), PORTRAIT("P");

    private final String value;

    @Override
    public String getValue() {
        return value;
    }

    private IndOrientation(String value) {
        this.value= value;
    }

    public static class Converter extends AbstractEnumConverter<IndOrientation, String> {
        public Converter() {
            super(IndOrientation.class);
        }
    }
}

带注解的Map示例:

...
@Convert(converter = IndOrientation.Converter.class)
private IndOrientation indOrientation;
...

通过一些更改,您可以创建一个IntegerEnum接口并将其泛化。

whlutmcx

whlutmcx2#

你需要做的是写一个泛型基类,然后为每个你想要持久化的枚举类型扩展它,然后在@Converter注解中使用扩展的类型:

public abstract class GenericEnumUppercaseConverter<E extends Enum<E>> implements AttributeConverter<E, String> {
    ...
}

public FooConverter
    extends GenericEnumUppercaseConverter<Foo> 
    implements AttributeConverter<Foo, String> // See Bug HHH-8854
{
    public FooConverter() {
        super(Foo.class);
    }
}

其中Foo是要处理的枚举。
另一种方法是定义一个自定义注解,修补JPA提供程序以识别该注解,这样,您就可以在构建Map信息时检查字段类型,并将必要的枚举类型提供给一个纯泛型转换器。
相关:

x6yk4ghg

x6yk4ghg3#

这个答案已经过修改,以利用Java 8中的default接口方法。
设施点的组件数量(下面列举)仍然是四个,但是所需的样板文件数量要少得多。以前的AbstractEnumConverter类已经被一个名为JpaEnumConverter的接口所取代,该接口现在扩展了JPAx 1 m3n1x接口。此外,每个占位符JPAx 1 m4n1x类现在只需要实现一个抽象方法,该方法返回枚举的Class<E>对象(甚至更少的样板)。
此解决方案与其他解决方案类似,并且还利用了JPA 2.1中引入的JPA Converter工具。由于Java 8中的泛型类型没有具体化,因此似乎没有一种简单的方法可以避免为您希望能够转换为数据库格式或从数据库格式转换的每个Java枚举编写单独的占位符类。
不过,您可以将编写枚举转换器类的过程简化为纯样板。此解决方案的组件包括:

  1. Encodable接口;枚举类的协定,该协定为每个枚举常量赠款对String令牌的访问权限。该协定仅编写一次,并由所有要通过JPA持久化的枚举类实现。此接口还包含一个静态工厂方法,用于为其匹配令牌取回枚举常量。
  2. JpaEnumConverter接口;提供了用于将标记转换为枚举常量或从枚举常量转换标记的公共代码。这也只编写了一次,并由项目中的所有占位符@Converter类实现。
    1.项目中的每个Java枚举类都实现Encodable接口。
    1.每个JPA占位符@Converter类都实现JpaEnumConverter接口。
    Encodable接口很简单,包含一个静态工厂方法forToken(),用于获取枚举常量:
public interface Encodable{

    String token();

    public static <E extends Enum<E> & Encodable> E forToken(Class<E> cls, String tok) {
        final String t = tok.trim();
        return Stream.of(cls.getEnumConstants())
                .filter(e -> e.token().equalsIgnoreCase(t))
                .findAny()
                .orElseThrow(() -> new IllegalArgumentException("Unknown token '" +
                        tok + "' for enum " + cls.getName()));
    }
}

JpaEnumConverter接口是一个通用接口,也很简单。它扩展了JPA 2.1 AttributeConverter接口,并实现了用于在实体和数据库之间来回转换的方法。这些方法随后由每个JPA @Converter类继承。每个占位符类必须实现的唯一抽象方法是返回枚举的Class<E>对象的方法。

public interface JpaEnumConverter<E extends Enum<E> & Encodable>
            extends AttributeConverter<E, String> {
    
    public abstract Class<E> getEnumClass();

    @Override
    public default String convertToDatabaseColumn(E attribute) {
        return (attribute == null)
            ? null
            : attribute.token();
    }

    @Override
    public default E convertToEntityAttribute(String dbData) {
        return (dbData == null)
            ? null
            : Encodeable.forToken(getEnumClass(), dbData);
    }
}

下面显示了一个具体的枚举类示例,现在可以使用JPA 2.1 Converter工具将其持久化到数据库中(请注意,它实现了Encodable,并且每个枚举常量的令牌都定义为私有字段):

public enum GenderCode implements Encodable{
    
    MALE   ("M"), 
    FEMALE ("F"), 
    OTHER  ("O");
    
    final String e_token;

    GenderCode(String v) {
        this.e_token = v;
    }

    @Override
    public String token() {    // the only abstract method of Encodable
        return this.e_token;
    }
}

每个占位符JPA 2.1 @Converter类的样板现在看起来像下面的代码。注意,每个这样的转换器都需要实现JpaEnumConverter并提供getEnumClass()的实现......仅此而已!JPA AttributeConverter接口方法的实现是继承的。

@Converter
public class GenderCodeConverter 
                 implements JpaEnumConverter<GenderCode> {

    @Override
    public Class<GenderCode> getEnumClass() {    // sole abstract method
        return GenderCode.class;  
    }
}

这些占位符@Converter类可以很容易地嵌套为其关联枚举类的static成员类。

qnakjoqk

qnakjoqk4#

以上的解决方案都很好,我在这里补充一下。
我还添加了以下内容,以便在编写转换器类实现接口时强制执行。当你忘记jpa开始使用默认机制时,这些机制实际上是模糊的解决方案(特别是当Map到某个数值时,我总是这样做)。
接口类如下所示:

public interface PersistedEnum<E extends Enum<E> & PersistedEnum<E>> {
  int getCode();
  Class<? extends PersistedEnumConverter<E>> getConverterClass();
}

PersistedEnumConverter类似于之前的帖子。但是,当实现这个接口时,您必须处理getConverterClass实现,除了强制提供特定的转换器类之外,它完全无用。
下面是一个示例实现:

public enum Status implements PersistedEnum<Status> {
  ...

  @javax.persistence.Converter(autoApply = true)
  static class Converter extends PersistedEnumConverter<Status> {
      public Converter() {
          super(Status.class);
      }
  }

  @Override
  public Class<? extends PersistedEnumConverter<Status>> getConverterClass() {
      return Converter.class;
  }

  ...
}

我在数据库中所做的总是为每个枚举创建一个伴随表,每个枚举值对应一行

create table e_status
    (
       id    int
           constraint pk_status primary key,
       label varchar(100)
    );

  insert into e_status
  values (0, 'Status1');
  insert into e_status
  values (1, 'Status2');
  insert into e_status
  values (5, 'Status3');

并且在使用枚举类型的任何地方设置一个fk约束。这样就可以保证使用正确的枚举值。我特别在这里设置了值0,1和5来显示它是多么的灵活,并且仍然是可靠的。

create table using_table
   (
        ...
    status         int          not null
        constraint using_table_status_fk references e_status,
        ...
   );
46scxncf

46scxncf5#

我发现了一种不用java.lang.Class、默认方法或反射就能做到这一点的方法。我是通过使用一个Function来做到的,该Function是通过方法引用从枚举传递给构造函数中的Convertor的。另外,来自枚举的Convertos应该是私有的,不需要它们在外部。
1.枚举为了持久化而应实现的接口

public interface PersistableEnum<T> {
            
 /** A mapping from an enum value to a type T (usually a String, Integer etc).*/
 T getCode();
            
}

1.抽象转换器将使用函数来覆盖convertToEntityAttribute转换

@Converter
public abstract class AbstractEnumConverter<E extends Enum<E> & PersistableEnum<T>, T> implements AttributeConverter<E, T> {

 private Function<T, E> fromCodeToEnum;

 protected AbstractEnumConverter(Function<T, E> fromCodeToEnum) {
   this.fromCodeToEnum = fromCodeToEnum;
 }

 @Override
 public T convertToDatabaseColumn(E persistableEnum) {
   return persistableEnum == null ? null : persistableEnum.getCode();
 }

 @Override
 public E convertToEntityAttribute(T code) {
   return code == null ? null : fromCodeToEnum.apply(code);
 }

}

1.枚举将实现接口(我使用lombok作为getter),并通过使用接收Function的构造函数创建转换后的,我使用方法引用传递ofCode。我更喜欢这样,而不是使用java.lang.Class或使用反射,我在枚举中有更多的自由。

@Getter 
public enum CarType implements PersistableEnum<String> {

    DACIA("dacia"),
    FORD("ford"),
    BMW("bmw");
    
    public static CarType ofCode(String code) {
      return Arrays.stream(values())
                .filter(carType -> carType.code.equalsIgnoreCase(code))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Invalid car type code."));
      }
    
    private final String code;
    
    CarType(String code) {
      this.code = code;
    }
       
    @Converter(autoApply = true)
    private static class CarTypeConverter extends AbstractEnumConverter<CarType, String> {
      protected CarTypeConverter () {
        super(CarType::ofCode);
      }
    }    
}

4.在实体中,你只需要使用枚举类型,它会保存它的String代码。

@Column(name = "CAR_TYPE")
  private CarType workflowType;
ufj5ltwl

ufj5ltwl6#

如果你不介意反思的话,这是可行的。

abstract class EnumTypeConverter<EnumType,ValueType> implements AttributeConverter<EnumType, ValueType> {

    private EnumType[] values

    @Override
    ValueType convertToDatabaseColumn(EnumType enumInstance) {
        return enumInstance ? enumInstance.getProperty(getValueColumnName()) : null
    }

    @Override
    EnumType convertToEntityAttribute(ValueType dbData) {

        if(dbData == null){
            return null
        }

        EnumType[] values = getValues()
        EnumType rtn = values.find {
            it.getProperty(getValueColumnName()).equals(dbData)
        }
        if(!rtn) {
            throw new IllegalArgumentException("Unknown ${values.first().class.name} value: ${dbData}")
        }
        rtn
    }

    private EnumType[] getValues() {
        if(values == null){
            Class cls = getTypeParameterType(getClass(), EnumTypeConverter.class, 0)
            Method m = cls.getMethod("values")
            values = m.invoke(null) as EnumType[]
        }
        values
    }

    abstract String getValueColumnName()

    // https://stackoverflow.com/a/59205754/3307720
    private static Class<?> getTypeParameterType(Class<?> subClass, Class<?> superClass, int typeParameterIndex) {
        return getTypeVariableType(subClass, superClass.getTypeParameters()[typeParameterIndex])
    }

    private static Class<?> getTypeVariableType(Class<?> subClass, TypeVariable<?> typeVariable) {
        Map<TypeVariable<?>, Type> subMap = new HashMap<>()
        Class<?> superClass
        while ((superClass = subClass.getSuperclass()) != null) {

            Map<TypeVariable<?>, Type> superMap = new HashMap<>()
            Type superGeneric = subClass.getGenericSuperclass()
            if (superGeneric instanceof ParameterizedType) {

                TypeVariable<?>[] typeParams = superClass.getTypeParameters()
                Type[] actualTypeArgs = ((ParameterizedType) superGeneric).getActualTypeArguments()

                for (int i = 0; i < typeParams.length; i++) {
                    Type actualType = actualTypeArgs[i]
                    if (actualType instanceof TypeVariable) {
                        actualType = subMap.get(actualType)
                    }
                    if (typeVariable == typeParams[i]) return (Class<?>) actualType
                    superMap.put(typeParams[i], actualType)
                }
            }
            subClass = superClass
            subMap = superMap
        }
        return null
    }
}

然后在实体类中:

enum Type {
        ATYPE("A"), ANOTHER_TYPE("B")
        final String name

        private Type(String nm) {
            name = nm
        }
    }

...

@Column
Type type

...

@Converter(autoApply = true)
    static class TypeConverter extends EnumTypeConverter<Type,String> {
        String getValueColumnName(){
            "name"
        }
}

这是用groovy编写的,因此需要针对Java做一些调整。

gk7wooem

gk7wooem7#

对于那些在Kotlin工作的人,这里有一个抽象转换器的例子:

enum class MyEnum(override val serializedAs: Int) : SerializableEnum {
    A(0),
    B(1),
    C(2),
}

@Converter(autoApply = true)
class MyEnumConverter : AbstractEnumConverter<MyEnum>(MyEnum::class)
interface SerializableEnum {
    val serializedAs: Int
}

abstract class AbstractEnumConverter<TEnum>(enumType: KClass<TEnum>) : AttributeConverter<TEnum, Int> where TEnum : SerializableEnum {

    var fromSerialized = enumType.java.enumConstants.associateBy { it.serializedAs }

    init {
        if (fromSerialized.size != enumType.java.enumConstants.size) {
            throw IllegalStateException("Serializable enum $enumType must have unique `serializedAs` values.")
        }
    }

    override fun convertToDatabaseColumn(enum: TEnum?) = enum?.serializedAs

    override fun convertToEntityAttribute(enum: Int?) = enum?.let { fromSerialized[it] }
}

相关问题