问题 在编码过程中,经常会遇到用某个数值来表示某种状态、类型或者阶段的情况,比如有这样一个枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public enum ComputerState { OPEN(10 ), CLOSE(11 ), OFF_LINE(12 ), FAULT(200 ), UNKNOWN(255 ); private int code; ComputerState(int code) { this .code = code; } }
通常我们希望将表示状态的数值存入数据库,即 ComputerState.OPEN 存入数据库取值为 10。
探索 首先,我们先看看 MyBatis 是否能够满足我们的需求。 MyBatis 内置了两个枚举转换器分别是:org.apache.ibatis.type.EnumTypeHandler
和 org.apache.ibatis.type.EnumOrdinalTypeHandler
。
EnumTypeHandler 这是默认的枚举转换器,该转换器将枚举实例转换为实例名称的字符串,即将 ComputerState.OPEN
转换 OPEN
。
EnumOrdinalTypeHandler 顾名思义这个转换器将枚举实例的 ordinal 属性作为取值,即 ComputerState.OPEN
转换为 0,ComputerState.CLOSE
转换为 1。 使用它的方式是在 MyBatis 配置文件中定义:
1 <typeHandlers > <typeHandler handler ="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType ="com.example.entity.enums.ComputerState" /> </typeHandlers >
以上的两种转换器都不能满足我们的需求,所以看起来要自己编写一个转换器了。
方案 MyBatis 提供了 org.apache.ibatis.type.BaseTypeHandler
类用于我们自己扩展类型转换器,上面的 EnumTypeHandler
和 EnumOrdinalTypeHandler
也都实现了这个接口。
1. 定义接口 我们需要一个接口来确定某部分枚举类的行为。如下:
1 2 3 public interface BaseCodeEnum { int getCode () ; }
该接口只有一个返回编码的方法,返回值将被存入数据库。
2. 改造枚举 就拿上面的 ComputerState
来实现 BaseCodeEnum
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public enum ComputerState implements BaseCodeEnum { OPEN(10 ), CLOSE(11 ), OFF_LINE(12 ), FAULT(200 ), UNKNOWN(255 ); private int code; ComputerState(int code) { this .code = code; } @Override public int getCode () { return this .code; } }
3. 编写一个转换工具类 现在我们能顺利的将枚举转换为某个数值了,还需要一个工具将数值转换为枚举实例。
1 2 3 4 5 6 7 8 9 10 11 12 public class CodeEnumUtil { public static <E extends Enum <?> & BaseCodeEnum> E codeOf (Class<E> enumClass, int code) { E[] enumConstants = enumClass.getEnumConstants(); for (E e : enumConstants) { if (e.getCode() == code) return e; } return null ; } }
4. 自定义类型转换器 准备工作做的差不多了,是时候开始编写转换器了。BaseTypeHandler<T>
一共需要实现 4 个方法:
void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
用于定义设置参数时,该如何把 Java 类型的参数转换为对应的数据库类型
T getNullableResult(ResultSet rs, String columnName)
用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的 Java 类型
T getNullableResult(ResultSt rs, int columnIndex)
用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的 Java 类型
T getNullableResult(CallableStatement cs, int columnIndex)
用定义调用存储过程后,如何把数据库类型转换为对应的 Java 类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class CodeEnumTypeHandler <E extends Enum <?> & BaseCodeEnum> extends BaseTypeHandler { 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 CodeEnumUtil.codeOf(type, code); } catch (Exception ex) { throw new IllegalArgumentException ("Cannot convert " + code + " to " + type.getSimpleName() + " by code value." , ex); } } }
5. 使用 接下来需要指定哪个类使用我们自己编写转换器进行转换,在 MyBatis 配置文件中配置如下:
1 <typeHandlers > <typeHandler handler ="com.example.typeHandler.CodeEnumTypeHandler" javaType ="com.example.entity.enums.ComputerState" /> </typeHandlers >
搞定! 经测试 ComputerState.OPEN
被转换为 10,ComputerState.UNKNOWN
被转换为 255,达到了预期的效果。
6. 优化 在第 5 步时,我们在 MyBatis 中添加 typeHandler 用于指定哪些类使用我们自定义的转换器,一旦系统中的枚举类多了起来,MyBatis 的配置文件维护起来会变得非常麻烦,也容易出错。如何解决呢? 在 Spring 中我们可以使用 JavaConfig 方式来干预 SqlSessionFactory 的创建过程,来完成转换器的指定。
思路
再写一个能自动匹配转换行为的转换器
通过 sqlSessionFactory.getConfiguration().getTypeHandlerRegistry()
取得类型转换器注册器
再使用 typeHandlerRegistry.setDefaultEnumTypeHandler (Class<? extends TypeHandler> typeHandler) 将第一步的转换器注册成为默认的
首先,我们需要一个能确定转换行为的转换器: AutoEnumTypeHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class AutoEnumTypeHandler <E extends Enum > extends BaseTypeHandler { private BaseTypeHandler typeHandler = null ; public AutoEnumTypeHandler (Class<E> type) { if (type == null ) { throw new IllegalArgumentException ("Type argument cannot be null" ); } if (BaseCodeEnum.class.isAssignableFrom(type)){ typeHandler = new CodeEnumTypeHandler (type); }else { typeHandler = new EnumTypeHandler <>(type); } } @Override public void setNonNullParameter (PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { typeHandler.setNonNullParameter(ps,i, parameter,jdbcType); } @Override public E getNullableResult (ResultSet rs, String columnName) throws SQLException { return (E) typeHandler.getNullableResult(rs,columnName); } @Override public E getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return (E) typeHandler.getNullableResult(rs,columnIndex); } @Override public E getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return (E) typeHandler.getNullableResult(cs,columnIndex); } }
接下来,我们需要干预 SqlSessionFactory
的创建过程,将刚刚的转换器指定为默认的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Configuration @ConfigurationProperties(prefix = "mybatis") public class MyBatisConfig { private String configLocation; private String mapperLocations; @Bean public SqlSessionFactory sqlSessionFactory ( DataSource dataSource, JSONArrayHandler jsonArrayHandler, JSONObjectHandler jsonObjectHandler) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean (); factory.setDataSource(dataSource); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver (); factory.setConfigLocation(resolver.getResource(configLocation)); factory.setMapperLocations(resolver.getResources(mapperLocations)); SqlSessionFactory sqlSessionFactory = factory.getObject(); TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry(); typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class); return sqlSessionFactory; } }
搞定! 这样一来,如果枚举实现了 BaseCodeEnum
接口就使用我们自定义的 CodeEnumTypeHandler
,如果没有实现 BaseCodeEnum
接口就使用默认的。再也不用写 MyBatis 的配置文件了!