对于数据字典型字段,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)
是否是最佳实现我不知道,但完美知足我项目对枚举的要求,并代码足够精简就好了