對於數據字典型字段,java的枚舉比起Integer好處多多,好比java
一、限定值,只能賦值枚舉的那幾個實例,不能像Integer隨便輸,保存和查詢的時候特別有用spring
二、含義明確,使用時不須要去查數據字典sql
三、顯示值跟存儲值直接映射,不須要手動轉換,好比1在頁面上顯示爲啓用,0顯示禁用,枚舉定義好能夠直接顯示數據庫
四、基於enum能夠添加一些拓展方法sass
個人項目使用spring boot JPA(hibernate實現),支持@Enumerated的annotation來標註字段類型爲枚舉,如:session
@Enumerated(EnumType.ORDINAL) @Column(name = "STATUS") private StatusEnum status;
Enumerated提供了兩種持久化枚舉的方式,EnumType.ORDINAL和EnumType.STRING,但都有很大的侷限性,讓人很難選擇,常常不能知足需求ide
EnumType.ORDINAL:按枚舉的順序保存數字測試
有一些我項目不能容忍的侷限性,好比this
一、順序性 - java枚舉的順序從0開始遞增,無法本身指定,我有些枚舉並非從0開始的,或者不是+1遞增的,好比一些行業的標準代碼。spa
二、舊數據可能不兼容,好比-1表明刪除,映射不了
三、不健壯 - 項目那麼多人開發,保不許一個豬隊友往枚舉中間加了一個值,那完了,數據庫裏的記錄就要對不上了。數據錯誤沒有異常,發現和排查比較困難
EnumType.STRING:保存枚舉的值,也就是toString()的值
一樣有侷限性:
一、String類型,數據庫定義的是int,即便override toString方法返回數字的String,JPA也保存不了
二、一樣不適用舊數據,舊數據是int
三、不能更名,改了後數據庫的記錄映射不了
我對枚舉需求其實很簡單,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)
是否是最佳實現我不知道,但完美知足我項目對枚舉的要求,並代碼足夠精簡就好了