解决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> {@Overridepublic Integer convertToDatabaseColumn(StatusEnum attribute) {return attribute.getValue();}@Overridepublic 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> {@Overridepublic Integer convertToDatabaseColumn(T attribute) {return attribute.getValue();}@Overridepublic 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};@Overridepublic void setParameterValues(Properties parameters) {final ParameterType reader = (ParameterType) parameters.get(PARAMETER_TYPE);if (reader != null) {enumClass = reader.getReturnedClass().asSubclass(Enum.class);}}//枚舉存儲int值@Overridepublic int[] sqlTypes() {return SQL_TYPES;}@Overridepublic Class returnedClass() {return enumClass;}//是否相等,不相等會觸發JPA update操作@Overridepublic 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);}@Overridepublic int hashCode(Object x) throws HibernateException {return x == null ? 0 : x.hashCode();}//返回枚舉@Overridepublic 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()));}//保存枚舉值@Overridepublic 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());}}@Overridepublic Object deepCopy(Object value) throws HibernateException {return value;}@Overridepublic boolean isMutable() {return false;}@Overridepublic Serializable disassemble(Object value) throws HibernateException {return (Serializable) value;}@Overridepublic Object assemble(Serializable cached, Object owner) throws HibernateException {return cached;}@Overridepublic Object replace(Object original, Object target, Object owner) throws HibernateException {return original;} }然后在實體對象上加上@Type
@Type(type = "你的包名.DbEnumType")
修改Idea的Generate POJOs腳本,自動為枚舉類型加上@Type,重新生成一遍實體類,跑unit test,頗費!(perfect)
是不是最佳實現我不知道,但完美滿足我項目對枚舉的要求,并代碼足夠精簡就行了
總結
以上是生活随笔為你收集整理的解决JPA的枚举局限性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL中information_sc
- 下一篇: 掌控谈话~校准问题