泛型枚举JPA AttributeConverter实现



我试图解决的问题

我正在尝试为Hibernate实现枚举映射。到目前为止,我已经研究了可用的选项,@Enumerated(EnumType.ORDINAL)@Enumerated(EnumType.STRING)似乎都不足以满足我的需求。@Enumerated(EnumType.ORDINAL)似乎非常容易出错,因为仅仅对枚举常量进行重新排序就可能会打乱映射,而@Enumerated(EnumType.STRING)也不够,因为我使用的数据库中已经充满了要映射的值,而且这些值不是我希望枚举常量命名的样子(这些值是外语字符串/整数(。

目前,所有这些值都映射到String/Integer属性。同时,属性应该只允许有限的值集(想象一下meetingStatus属性允许Strings:PLANNEDCANCELEDDONE。或者另一个属性允许有限的整数值集:12345(。

我的想法是用枚举替换实现,以提高代码的类型安全性。String/Integer实现可能导致错误的一个很好的例子是表示这样的值的String方法参数——使用String,任何东西都会出现。另一方面,拥有Enum参数类型会引入编译时安全性。

我迄今为止最好的方法

唯一能满足我需求的解决方案是为每个枚举实现带有@Converter注释的自定义javax.persistence.AttributeConverter。由于我的模型需要相当多的枚举,为每个枚举编写自定义转换器似乎很快就变得疯狂了。因此,我搜索了该问题的通用解决方案->如何为任何类型的枚举编写通用转换器。以下答案对这里有很大帮助:https://stackoverflow.com/a/23564597/7024402.答案中的代码示例提供了一些通用的实现,但对于每个枚举,仍然需要一个单独的转换器类。答案的作者还继续说道:

";另一种选择是定义一个自定义注释,修补JPA提供程序以识别此注释。这样,您就可以在构建映射信息时检查字段类型,并将必要的枚举类型输入到纯泛型转换器中">

这就是我想我会感兴趣的。不幸的是,我找不到更多关于这方面的信息,我需要更多的指导来了解需要做什么,以及如何使用这种方法。

当前实施

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

这就是我实现部分通用转换器的方式。

目标:不再需要为每个枚举创建新的转换器类

幸运的是,您不应该为此修补hibernate。

  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;
}
  1. 休眠用户类型如下:
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. 然后,您可以使用它:
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()
...
}

另请参阅Bauer,King,Gregory的优秀著作《Java Persistence with Hibernate》中的章节《用用户类型扩展Hibernate》。

简化:

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

我尝试了1000次创建相同的东西。为每个枚举动态生成转换器-这不是问题,但它们将具有相同的类。主要问题:org.hibernate.boot.internal.MetadataBuilderImpl#applyAttributeConverter(java.lang.Class<?扩展javax.persistence.AttributeConverter>,布尔值(

如果转换器已经注册,我们得到了异常。

public void addAttributeConverterInfo(AttributeConverterInfo info) {
if ( this.attributeConverterInfoMap == null ) {
this.attributeConverterInfoMap = new HashMap<>();
}
final Object old = this.attributeConverterInfoMap.put( info.getConverterClass(), info );
if ( old != null ) {
throw new AssertionFailure(
String.format(
"AttributeConverter class [%s] registered multiple times",
info.getConverterClass()
)
);
}
}

也许我们可以更改org.hibernate.boot.internal.BootstrapContextImpl,但我确信它会创建太复杂和不灵活的代码。

最新更新