关于 MyBatis 和 Hibernate 这两个 Java ORM 框架中枚举映射的二三事
关于数据库字段与实体类中枚举的映射也算不上花活了。在业务开发中还是比较有必要的。
比如一个实体类里有个 state
字段, 里面包含了几种状态如: 初始化、进行中、已完成; 像这种类似于一个对象的状态都是可以被枚举出来的, 同时也可能携带一定的业务逻辑与状态之间切换的规则。
此时如果直接使用数字类型/字符串类型来作为实体类属性, 则不可避免的在进行业务逻辑操作时需要做额外的操作。最好的实现当然是直接定义成枚举, 然后根据枚举内部携带的 code 来映射到数据库之中。
因为枚举是可以继承接口的, 所以一般都会定义一个接口来表示这个枚举支持和指定值之间的转换,具体的枚举均实现此接口表明自身支持转换即可:
public interface BaseCodeEnum { int getCode(); static <E extends Enum<E> & BaseCodeEnum> E forCode(Class<E> cls, int code) { return Stream.of(cls.getEnumConstants()) .filter(e -> e.getCode() == code) .findFirst() .orElse(null); } }
实体类中既然已经有特定的属性是枚举类型了, 那就必须得在ORM映射上做点文章。 不然人家都不知道怎么转。
MyBatis:
这里需要点名表扬 MyBatis, 他提供了全局的类型映射处理器,我们只需要很简单的配置即可实现想要的功能。
Mabatis 有个类 org.apache.ibatis.type.BaseTypeHandler
专门用于提供类型的转化, 可以配置应用到全局
我们可以像这样创建一个类继承他:
@MappedTypes(BaseCodeEnum.class) public class CodeEnumTypeHandler<E extends Enum<E> & BaseCodeEnum> extends BaseTypeHandler<BaseCodeEnum> { private Class<E> type; public CodeEnumTypeHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument cannot be null"); } this.type = type; } @Override public void setNonNullParameter(PreparedStatement ps, int i, BaseCodeEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { int code = rs.getInt(columnName); return rs.wasNull() ? null : codeOf(code); } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int code = rs.getInt(columnIndex); return rs.wasNull() ? null : codeOf(code); } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int code = cs.getInt(columnIndex); return cs.wasNull() ? null : codeOf(code); } private E codeOf(int code) { try { return BaseCodeEnum.forCode(type, code); } catch (Exception ex) { throw new IllegalArgumentException("Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex); } } }
然后在配置 Mybatis 的 SqlSessionFactory 时, 设置好类型转化器所在的包就OK了。
这是在Spring下的一个简单示例:
@Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factory.setMapperLocations(resolver.getResources(mapperLocations)); factory.setTypeHandlersPackage(CodeEnumTypeHandler.class.getPackageName()); return factory.getObject(); }
Hibernate:
相对于 Mybatis 在类型转换的的简易实用而言, Hibernate 就不大行了, 我找了挺久,对于枚举映射这点始终没找到一个可以直接应用在全局的妥善的解决方法。
而 Hibernate 支持的所谓通过 @Enumerated
注解在属性上支持枚举映射的方式,仅支持枚举name和枚举的ordinal方式映射,这种映射模型很脆弱,稍微有点改动就是一堆脏数据。
StackOverFlow 里也有老哥在问枚举转换相关的问题, 但是我看了一下 给出的解决方法并没有可以直接应用到全局之中的。
https://stackoverflow.com/questions/16140282/jpa-enumerated-types-mapping-best-approach
那就确实没啥办法了, 只能说用一个相对其余的方式而言稍微优雅一点点的机制来实现需要的功能。
根据Hibernate本身自带的转换机制, 也可以继承 javax.persistence.AttributeConverter
从而实现对指定类型的转换。
就中规中矩的来使用这个机制的话又不可避免的会遇到类膨胀的问题, 造成项目里多出一堆转化类, 而转化类里也是一堆看起来大体相同的代码, 这样就很丑陋。
而我的解决方法就是在结构设计上尽量优化了,将 枚举-code 之间的映射关系核心转化逻辑提取到公有域之中,尽量的减少垃圾代码。
这是我写的抽象枚举转化器:
/** * 抽象的枚举转换器 * 只要是 BaseCodeEnum 此接口的实现类,只需继承此类则可以实现 enum/code 的转换 * 一般是用于 {@link javax.persistence.Convert,javax.persistence.Column} 所标识的枚举 * 在 持久化/RS反序列化 时进行转化用 * * @param <E> */ public abstract class AbstractBaseCodeEnumConverter<E extends Enum<E> & BaseCodeEnum> implements AttributeConverter<E, Integer> { private Class<E> codeEnumClass; public AbstractBaseCodeEnumConverter(Class<E> codeEnumClass) { this.codeEnumClass = codeEnumClass; } @Override public Integer convertToDatabaseColumn(E attr) { return (attr == null) ? null : attr.getCode(); } @Override public E convertToEntityAttribute(Integer dbData) { return (dbData == null) ? null : BaseCodeEnum.forCode(this.codeEnumClass, dbData); } }
然后在需要转化的枚举之中, 建立一个公共的静态内部类,比如这样:
/** * 删除标志 * 表示一张表内某条数据的删除与否 * 一般用于做软删除相关操作/判定时使用 */ public enum DeletedStatusEnum implements BaseCodeEnum { UN_DELETE(0), DELETED(1); private int value; DeletedStatusEnum(int value) { this.value = value; } public int getValue() { return value; } public boolean valueIs(int value) { return Objects.equals(this.value, value); } @Override public int getCode() { return getValue(); } public static class Convert extends AbstractBaseCodeEnumConverter<DeletedStatusEnum> { public Convert() { super(DeletedStatusEnum.class); } } }
使用这样子的结构,即可在实体类中使用 @Convert
注解,指定字段映射时使用的转化类; 而转化器本身是枚举的内部类, 尽量在枚举这一层面上维护了单一职责原则。 虽说不是最优解,但使用起来感觉还是可以的。
@Data @MappedSuperclass public class VirtualDeleteSupportEntity extends BaseEntity { @Convert(converter = DeletedStatusEnum.Convert.class) @Column(name = "DELETED", length = 2, nullable = false, columnDefinition = "tinyint") private DeletedStatusEnum deleted; public boolean hasDeleted() { return DeletedStatusEnum.DELETED.equals(deleted); } }
如果不去用一些花活的话,Hibernate 一般也就只能做成这样的效果了, 如果条件支持的话,去改造一下Hibernate的源码也不是不行,一个枚举转换而已,涉及的地方估计也不会很多。
在这一点上 Hibernate 比起 MyBatis 而言确实不够方便 ┓( ´∀` )┏