zoukankan      html  css  js  c++  java
  • 解决JPA的枚举局限性

    解决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)

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

     

     

      

  • 相关阅读:
    隐藏QQ全部图标,隐藏QQ全部信息
    发放腾讯微博邀请,先到先得、
    关于“5005: 优化字节代码时发生未知错误。”的处理办法
    端口
    xmldocument
    MasterPage
    asp.net ajax
    mysqladmin 设置用户名初始密码报错you need the SUPER privilege for this operation
    实践SSH通道链接国外服务器访问受限网站
    转载 实践与分享:Windows 7怎么获取TrustedInstaller权限【图文教程】
  • 原文地址:https://www.cnblogs.com/xiaoq/p/7885775.html
Copyright © 2011-2022 走看看