xiaoq

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

解决JPA的枚举局限性

对于数据字典型字段,java的枚举比起Integer好处多多,比如

1、限定值,只能赋值枚举的那几个实例,不能像Integer随便输,保存和查询的时候特别有用

2、含义明确,使用时不需要去查数据字典

3、显示值跟存储值直接映射,不需要手动转换,比如1在页面上显示为启用,0显示禁用,枚举定义好可以直接显示

4、基于enum可以添加一些拓展方法

 

我的项目使用spring boot JPA(hibernate实现),支持@Enumerated的annotation来标注字段类型为枚举,如:

@Enumerated(EnumType.ORDINAL)
@Column(name = "STATUS")
private StatusEnum status;

 

Enumerated提供了两种持久化枚举的方式,EnumType.ORDINAL和EnumType.STRING,但都有很大的局限性,让人很难选择,经常不能满足需求

EnumType.ORDINAL:按枚举的顺序保存数字

有一些我项目不能容忍的局限性,比如

1、顺序性 - java枚举的顺序从0开始递增,没法自己指定,我有些枚举并不是从0开始的,或者不是+1递增的,比如一些行业的标准代码。

2、旧数据可能不兼容,比如-1代表删除,映射不了

3、不健壮 - 项目那么多人开发,保不准一个猪队友往枚举中间加了一个值,那完了,数据库里的记录就要对不上了。数据错误没有异常,发现和排查比较困难

EnumType.STRING:保存枚举的值,也就是toString()的值

同样有局限性:

1、String类型,数据库定义的是int,即使override toString方法返回数字的String,JPA也保存不了

2、同样不适用旧数据,旧数据是int

3、不能改名,改了后数据库的记录映射不了

 

我对枚举需求其实很简单,1是保存int型,2是值可以自己指定,可惜默认的那两种都实现不了。

没办法,只能考虑在保存和取出的时候自己转换了,然后很容易就找到实体转换器AttributeConverter,可以自定义保存好取出时的数据转换,Yeah!(似乎)完美解决问题!

实现如下:

定义枚举

public enum StatusEnum {
    Deleted(-1, "删除"),
    Inactive(0, "禁用"),
    Active(1, "启用");

    private Integer value;

    private String display;

    private StatusEnum(int value, String display) {
        this.value = value;
        this.display = display;
    }

    //显示名
    public String getDisplay() {
        return display;
    }

    //保存值
    public Integer getValue() {
        return value;
    }

    //获取枚举实例
    public static StatusEnum fromValue(Integer value) {
        for (StatusEnum statusEnum : StatusEnum.values()) {
            if (Objects.equals(value, statusEnum.getValue())) {
                return statusEnum;
            }
        }
        throw new IllegalArgumentException();
    }
}
 

 创建Convert,很简单,就是枚举跟枚举值的转换

public class EnumConvert implements AttributeConverter<StatusEnum, Integer> {
    @Override
    public Integer convertToDatabaseColumn(StatusEnum attribute) {
        return attribute.getValue();
    }

    @Override
    public StatusEnum convertToEntityAttribute(Integer dbData) {
        return StatusEnum.fromValue(dbData);
    }
}

 网上说class上加上@Converter(autoApply = true),JPA能自动识别类型并转换,然而我用spring boot跑unit test实验了并不起作用,使用还是把@Converter加在实体字段上

    @Convert(converter = EnumConvert.class)
    @Column(name = "STATUS")
    private StatusEnum status;

嗯,测试结果正常,很好!

 

等等,,我有20个左右的枚举,难道我要建20个转换器??咱程序猿怎么能干这种搬砖的活呢?必须简化!

我试试用泛型,先定义一个枚举的接口

public interface IBaseDbEnum {
    /**
     * 用于显示的枚举名
     *
     * @return
     */
    String getDisplay();

    /**
     * 存储到数据库的枚举值
     *
     * @return
     */
    Integer getValue();

    //按枚举的value获取枚举实例
    static <T extends IBaseDbEnum> T fromValue(Class<T> enumType, Integer value) {
        for (T object : enumType.getEnumConstants()) {
            if (Objects.equals(value, object.getValue())) {
                return object;
            }
        }
        throw new IllegalArgumentException("No enum value " + value + " of " + enumType.getCanonicalName());
    }
}

然后Convert改为泛型

public class EnumConvert<T extends IBaseDbEnum> implements AttributeConverter<T, Integer> {
    @Override
    public Integer convertToDatabaseColumn(T attribute) {
        return attribute.getValue();
    }

    @Override
    public T convertToEntityAttribute(Integer dbData) {
        //先随便写,测试一下
        return (T) StatusEnum.Active;
    }
}

可是到这犯难了,实体的@Convert怎么写呢?converter参数要求class类型,@Convert(converter = EnumConvert<StatusEnum>.class)这种写法不能通过啊,不传入泛型参数,又没办法吧数据库的int转换为具体枚举,这不还是要写20多个转换器?继承泛型的基类转换器只是减少了一部分代码而已,还是不能接受。

Convert方式走不通,然后考虑其他方式,干脆把枚举当做一个自定义类型,不用局限于枚举身上,只要能实现保存和映射就足够了。

创建自定义的UserType - DbEnumType,完整代码如下:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.DynamicParameterizedType;
import org.hibernate.usertype.UserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;
import java.util.Properties;

/**
 * 数据库枚举类型映射
 * 枚举保存到数据库的是枚举的.getValue()的值,为Integer类型,数据库返回对象时需要把Integer转换枚举
 * Create by XiaoQ on 2017-11-22.
 */
public class DbEnumType implements UserType, DynamicParameterizedType {

    private Class enumClass;
    private static final int[] SQL_TYPES = new int[]{Types.INTEGER};

    @Override
    public void setParameterValues(Properties parameters) {
        final ParameterType reader = (ParameterType) parameters.get(PARAMETER_TYPE);
        if (reader != null) {
            enumClass = reader.getReturnedClass().asSubclass(Enum.class);
        }
    }

    //枚举存储int值
    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    @Override
    public Class returnedClass() {
        return enumClass;
    }

    //是否相等,不相等会触发JPA update操作
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == null && y == null) {
            return true;
        }
        if ((x == null && y != null) || (x != null && y == null)) {
            return false;
        }
        return x.equals(y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x == null ? 0 : x.hashCode();
    }

    //返回枚举
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        String value = rs.getString(names[0]);
        if (value == null) {
            return null;
        }
        for (Object object : enumClass.getEnumConstants()) {
            if (Objects.equals(Integer.parseInt(value), ((IBaseDbEnum) object).getValue())) {
                return object;
            }
        }
        throw new RuntimeException(String.format("Unknown name value [%s] for enum class [%s]", value, enumClass.getName()));
    }

    //保存枚举值
    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        if (value == null) {
            st.setNull(index, SQL_TYPES[0]);
        } else if (value instanceof Integer) {
            st.setInt(index, (Integer) value);
        } else {
            st.setInt(index, ((IBaseDbEnum) value).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 (Serializable) 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;
    }
}

然后在实体对象上加上@Type

@Type(type = "你的包名.DbEnumType")

 

修改Idea的Generate POJOs脚本,自动为枚举类型加上@Type,重新生成一遍实体类,跑unit test,颇费!(perfect)

是不是最佳实现我不知道,但完美满足我项目对枚举的要求,并代码足够精简就行了

 

 

  

 

posted on 2017-11-23 16:58  xiaoqhuang  阅读(11418)  评论(1编辑  收藏  举报