枚舉類型的映射
初步瞭解
對於實體類中字段類型爲 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 字段表示自評等級,實體類中該字段類型爲枚舉。
-
自評表實體類
@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; } } }
-
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; } }
-
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是對應的數據庫字段類型):
- public Y convertToDatabaseColumn (X attribute);用於將實體類轉化爲數據庫字段時,增、改
- 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 會有所進步!