Hibernate 枚舉類型映射以及屬性轉化器的使用

枚舉類型的映射

初步瞭解

對於實體類中字段類型爲 Enum 的屬性,當其映射爲表的列時,需要指定映射的類型,數字 or 字符串

18/2/26 10:50 更:如果不做任何處理的話,從數據庫中映射到枚舉類,是映射爲枚舉類對象。也就是像 TRANSFER_GROUP、ONLINE_PROPOSAL 這樣。

//@Enumerated 和 @Column 配合使用
// EnumType.STRING 映射成字符串,是把枚舉的字面形式。如下面的 TRANSFER_GROUP(0, "調組申請"),映射到數據庫是 TRANSFER_GROUP ,不是“調組申請”。如果想要映射爲”調組申請“,就要做一個處理,下面會說到
@Column
@Enumerated(EnumType.STRING)
private ProcessType type;


// EnumType.ORDINAL 映射成數字
@Column
@Enumerated(EnumType.ORDINAL)
private ProcessStatus status;
public enum ProcessType implements AbstractEnum {

    TRANSFER_GROUP(0, "調組申請"),

    ONLINE_PROPOSAL(1,"在線提案");

    private int code;
    private String name;

    ProcessType(int code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


}
public enum ProcessStatus implements AbstractEnum {

    /**
     * 新創建
     */
    CREATE(0, "創建"),

    /**
     * 啓動
     */
    START(1, "開始"),

    /**
     * 拒絕
     */
    REJECT(2, "拒絕"),

    /**
     * 結束
     */
    COMPLETE(3, "通過");

    private int code;

    private String name;

    ProcessStatus(int code, String name) {
        this.setCode(code);
        this.setName(name);
    }
    public static ProcessStatus valueOfEnum(String name) {
        ProcessStatus[] types = values();
        for (ProcessStatus type : types) {
            if (type.getName().equals(name)) {
                return type;
            }
        }
        return null;
    }

    @Override
    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

數據庫:
這裏寫圖片描述

注意到 ProcessType type 類型數據庫中存儲的是枚舉類的字面值,不是 name 值。這還不是要命的,如果查詢條件中有枚舉類型的映射對應關係可就不是這樣了。

深入探討

再舉一個昨天開發遇到的實際問題,一個自評表,數據庫中有 int 類型的 level 字段表示自評等級,實體類中該字段類型爲枚舉。

  1. 自評表實體類

    @Entity
    @Table(name = "s_self_assessment")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Accessors(chain = true)
    public class SelfAssessment {
    
    @Id
    private String id;
    
    private String promoteId;
    
    @Column
    @Enumerated(value = EnumType.ORDINAL)
    private LevelEnum level;
    
    /**
     * level 爲 優/良 的自評要素的需要添加描述
     */
    private String description;
    
    /**
     * 此自評要素的分數,優良中差分別對應 7、5、3、1
     */
    private Integer score;
    
    @OneToOne(cascade = {CascadeType.ALL})
    @JoinColumn(name="assessment_factor_id")
    private AssessmentFactor assessmentFactor;
    
    /**
     * 根據 level 指定 score
     */
    public SelfAssessment(String promoteId,LevelEnum level,String description,String assessmentFactorId){
        this.id = IdHelper.nextStringValue();
        this.promoteId = promoteId;
        this.level = level;
        this.description = description;
        assessmentFactor = AssessmentFactor.builder().id(assessmentFactorId).build();
    
        switch (level){
            case LEVEL_EXCELLENT:
                score = 7;
                break;
            case LEVEL_GOOD:
                score = 5;
                break;
            case LEVEL_MIDDLE:
                score = 3;
                break;
            case LEVEL_BAD:
                score = 1;
                break;
        }
    
       }
    }
    
  2. LevelEnum 枚舉

    public enum LevelEnum implements AbstractEnum{
       LEVEL_EXCELLENT(1,"優"),LEVEL_GOOD(2,"良"),LEVEL_MIDDLE(3,"中"),LEVEL_BAD(4,"差");
    
        private String name;
    
        private Integer code;
    
        LevelEnum(Integer code,String name){
            this.code = code;
            this.name = name;
        }
    
    
        @Override
        public int getCode() {
            return code;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        public static LevelEnum getLevelEnum(Integer x){
            LevelEnum[] values = values();
    
            for (int i = 0; i < values.length; i++) {
                if (values[i].code.equals(x)){
                    return values[i];
                }
            }
            return null;
        }
    }
    
  3. DAO 層接口

    public interface SelfAssessmentRepository extends JpaRepository<SelfAssessment,String>{
    
    @Query(value = "SELECT id,promote_id,assessment_factor_id,level,score,description FROM s_self_assessment WHERE level = ?1" ,nativeQuery = true)
    List<SelfAssessment> findAllByLevelNative(Integer level);
    
    List<SelfAssessment> findAllByLevel(LevelEnum levelEnum);
    }
    

這麼着寫,插入、查詢都有問題

這裏寫圖片描述

插入:
這裏寫圖片描述

這裏寫圖片描述
注意到上面新增自評表的 level 爲優秀,數據庫中應該存儲 1 ,然而數據庫中新增記錄的 level 字段值卻是 0。

查詢也是同樣的問題,如果查詢 level 爲優秀的,數據庫 level 字段爲優秀的記錄對應到實體類卻是 LEVEL_GOOD

出現這樣錯誤的原因是啥呢?這個問題我幾個小時都沒有解決,無奈求助於老大——首席架構師。我也是鼓足了勇氣,帶我的架構師(很榮幸,是一個特別聰明幽默的架構師帶我)還沒來上班。老大就是老大,在我向人家把問題表述清楚之後,10s 不到的時間就指出了問題所在。特別佩服!其實我問他的不是這個問題,是一個類似的,但是我沒有用下面說的方法去解決,而是抖了個機靈,慚愧慚愧~~

如果你和我一樣是個新手的話,你好好仔細看看你們公司高開/架構寫的枚舉類,基本都會有兩個屬性,一個String 表明含義,一個 int 用於數據庫存儲。並且仔細看,枚舉類中所包含的成員變量都是從 0 開始的。什麼意思呢,看我上邊 LevelEnum 枚舉,LEVEL_EXCELLENT(1,"優"),LEVEL_GOOD(2,"良"),LEVEL_MIDDLE(3,"中"),LEVEL_BAD(4,"差");這個是從 1 開始的,我想讓優良可差分別對應1234,如果我讓其分別對應0123,想必就沒有這篇文章了。這就是問題的關鍵。

如果你不做其他處理,任由 Hibernate 去映射枚舉,且碰巧你打算對應的屬性值不是從 0 開始的,那就會出現上面的錯誤。插入、查詢中用到了枚舉,Hibernate 不會聰明的按照你臆想的意思去自動給你映射。我這裏就是想讓優良可差分別對應1234,@Enumerated(EnumType.ORDINAL) 註解的意思是,把枚舉類的所有屬性從 0 開始依次排序,給你映射到DB中,反過來從DB映射到實體類時也是同樣的道理。也就是,如果我插入 level 屬性爲 LEVEL_EXCELLENT ,Hibernate 會去看 LEVEL_EXCELLENT 在其所屬枚舉類屬性中出現在第幾位,第一位就是0,則數據庫中插入0。取數據也是。

解決方案

其實在寫這篇文章時,我就摸索出了第二個解決方法(注意到上面測試插入的代碼中我註釋掉了一段,使用這個插入就沒有問題),後面仔細看看再寫。這裏先把第一個方法總結一下,我是參考 Hibernate中枚舉Enum類型的映射策略 文章來的,這篇文章寫得很好,找了很久才搜到。

AttributeConverter 屬性轉化器
A class that implements this interface can be used to convert entity attribute state into database column representation and back again.

實現 javax.persistence.AttributeConverter 接口的類可以被用於實體屬性和數據庫列類型之間相互轉化

AttributeConverter<X,Y> 接口有兩個方法(X是實體屬性,Y是對應的數據庫字段類型):

  1. public Y convertToDatabaseColumn (X attribute);用於將實體類轉化爲數據庫字段時,增、改
  2. public X convertToEntityAttribute (Y dbData);將數據庫字段轉換爲實體類屬性,查

現在就需要一個類來把實現這個藉口,把 LevelEnum 轉化掉,注意這個類前要加 @Converter

@Converter
public class LevelEnumConverter implements AttributeConverter<LevelEnum,Integer> {


    /**
     * 轉化到數據庫,增加、更新時使用
     * @param attribute
     * @return
     */
    @Override
    public Integer convertToDatabaseColumn(LevelEnum attribute) {
        return attribute.getCode();
    }

    /**
     * 從數據庫轉化到實體類,查詢使用
     * @param dbData
     * @return
     */
    @Override
    public LevelEnum convertToEntityAttribute(Integer dbData) {
        return LevelEnum.getLevelEnum(dbData);
    }

}

之前的自評表實體類 SelfAssessment 中,LevelEnum 上面也不能用 @Enumerated(value = EnumType.ORDINAL)註解了

    @Column
    //修改前 @Enumerated(value = EnumType.ORDINAL)
    @Convert(converter = LevelEnumConverter.class)
    private LevelEnum level;

如此進行,大功完成~~

現在再去執行以下查詢和插入,結果如下:
之前查詢結果是把數據庫中 level 爲 1 的字段給映射成 LEVEL_GOOD,插入也沒問題了
這裏寫圖片描述

上面那條記錄是之前沒有使用屬性轉換器時插入錯誤的情況

這裏寫圖片描述

寫個博客其實不容易,總想把問題表述清楚,而在這個過程中又遇到了種種問題,堅持下去,希望 2018 會有所進步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章