解決JPA的枚舉局限性 - 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)

是不是最佳實現我不知道,但完美滿足我項目對枚舉的要求,並代碼足夠精簡就行了

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM