有没有可能编写一个通用的枚举转换器用于JPA?

45

我想编写一个 JPA 转换器,将任何枚举值存储为大写字母。我们遇到的一些枚举值尚未遵循只使用大写字母的约定,因此在它们进行重构之前,我仍会存储未来的值。

到目前为止,我的工作成果如下:

package student;

public enum StudentState {

    Started,
    Mentoring,
    Repeating,
    STUPID,
    GENIUS;
}

我希望“Started”被存储为“STARTED”,以此类推。

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

}

不能行的是我不知道运行时的枚举类是什么。我无法想出一种方法将这个信息传递给@Converter注释中的转换器。

那么有没有一种方法可以向转换器添加参数或者有什么其他的方法呢?

我正在使用EclipseLink 2.4.2

谢谢!


4
请注意,这很可能是脆弱的,特别是因为枚举可以拥有值 AVALUEAValue,而这在法律上是完全合法的。 - chrylis -cautiouslyoptimistic-
是的,那是真的,但我认为那是完全禁止的:D - wemu
7个回答

45

基于@scottb的解决方案,我做了这个,已经测试过hibernate 4.3:(没有hibernate类,应该可以在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);
        }
    }
}

带注释的映射示例:

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

通过一些更改,您可以创建一个IntegerEnum接口并进行泛型处理。


1
嗨Sérgio,感谢你的贡献。我已经实现了你的解决方案,并从AbstractEnumConverter中删除了@Convert,因为它会抛出有关ParameterizedType的异常。之后一切正常。谢谢。 - Marco Blos
1
不客气。我目前的代码中也实现了相同的枚举。请检查您是否输入了错误的名称(@Convert用于映射属性,@Converter用于类)。 - ChRoNoN
6
在使用SpringData时,你需要从AbstractEnumConverter中移除@Converter注解,因为Spring会自动尝试将转换器注册到Hibernate配置中,而AbstractEnumConverter没有默认的无参构造函数,所以这不起作用。否则,一切都能正常运行。 - Julien Kronegg
3
好的解决方案。我从AbstractEnumConverter中删除了@Converter注释,从IndOrientation字段中删除了@Convert(converter = IndOrientation.Converter.class)注释,代替这些操作,我在具体实现的Converter上添加了@Converter(autoApply = true)注释。 - Tuom
我也实现了这个解决方案,但采用了@Tuom的变体,因为在将EAR部署到Jboss7上时出现了持久化单元错误。 - Leib
显示剩余2条评论

25
您需要做的是编写一个通用的基类,然后为每个要持久化的枚举类型扩展该基类。 然后在@Converter注释中使用扩展类型:

您需要做的是编写一个通用的基类,然后为每个要持久化的枚举类型扩展该基类。 然后在@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提供程序以识别此注释。这样,您可以在构建映射信息时检查字段类型,并将必要的枚举类型馈入纯通用转换器中。

相关:


但是,枚举不能扩展另一个类?https://dev59.com/G2Up5IYBdhLWcg3wLlVV#15450935 - pioto
@pioto:在这种情况下,那并不重要。我已经添加了一个如何做到这一点的示例。 - Aaron Digulla
啊哈!我以为你在谈论枚举的抽象类,而不是转换器...这看起来很棒,我会试一试。谢谢! - pioto
1
这个很好用,谢谢。但是有一个问题:至少在Hibernate中,你需要在具体的FooConverter类的声明中添加implements AttributeConverter <Foo, String>,感谢这个bug:https://hibernate.atlassian.net/browse/HHH-8854 - pioto

10

这个答案已经修改以利用Java 8中的默认接口方法。

设施的组件数量(下面枚举)仍为四,但所需的样板代码要少得多。原来的AbstractEnumConverter类已被名为JpaEnumConverter的接口取代,该接口现在扩展了JPA AttributeConverter接口。此外,每个占位符JPA @Converter类现在只需要实现一个返回枚举的Class<E>对象的单个抽象方法(样板代码更少)。

这个解决方案与其他方案类似,也利用了JPA 2.1中引入的JPA Converter功能。由于Java 8中的通用类型未实体化,因此似乎没有简单的方法可以避免为要能够转换为/从数据库格式的每个Java枚举编写单独的占位符类。

但是,您可以将编写枚举转换器类的过程减少到纯样板代码。此解决方案的组成部分包括:

  1. Encodable接口;授予枚举类对每个枚举常量的String标记访问权限的合同。这只需编写一次,并由要通过JPA持久化的所有枚举类实现。此接口还包含一个静态工厂方法,用于获取其匹配标记的枚举常量。
  2. JpaEnumConverter接口;提供将标记转换为/从枚举常量的通用代码。这也只需编写一次,并由项目中所有占位符@Converter类实现。
  3. 项目中的每个Java枚举类都实现了Encodable接口。
  4. 每个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转换器将其持久化到数据库中(请注意它实现了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成员类中。


0

如果您不介意使用反射,这个方法可以实现。感谢另一个SO答案的贡献。

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://dev59.com/vXA75IYBdhLWcg3wPmZ8#59205754
    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中需要进行一些调整。


0
我找到了一种方法来实现这个,而不使用java.lang.Class、默认方法或反射。我通过在构造函数中将一个函数(Function)传递给转换器(Convertor),并使用方法引用来实现这一点。此外,枚举类中的转换器应为私有的,在外部没有必要使用它们。
  1. 枚举类应该实现的接口,以便进行持久化
public interface PersistableEnum<T> {
            
 /** A mapping from an enum value to a type T (usually a String, Integer etc).*/
 T getCode();
            
}

抽象转换器将使用函数来覆盖 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. 在实体中,您只需使用枚举类型,它将保存其字符串代码。

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

0
以上解决方案非常好。我在这里添加了一些小内容。
我还添加了以下内容,以强制实现接口编写转换器类。当您忘记时,jpa会开始使用默认机制,这些机制是非常模糊的解决方案(特别是当映射到某些数字值时,我总是这样做)。
接口类看起来像这样:
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');

并且从枚举类型的任何使用处添加一个外键约束。这样就可以始终保证正确的枚举值的使用。我特别在这里放置了值0、1和5,以展示它有多么灵活,但仍然坚固。

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

我不明白,如果getConverterClass对你所实现的内容完全没有用处,在这段代码中就没有任何意义! - Marcus Voltolim
1
该函数从未被调用,但是通过将其放在接口中,它强制实现者提供特定的伴随转换器类。JPA应该提供一个一次性转换器类,当枚举实现特定接口时总是执行转换,但据我所知它没有提供。因此,您必须为每个PersistedEnum具体实现重复提供伴随转换器类。 - marcel

0

对于那些使用 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] }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接