日常java开发中经常有这种需求,用0或者1这些代码(不局限于数字)来表示某种状态。比如用0表示女性,用1来表示男性。而且写入数据库可能是一个标识,从数据库读取又还原为具体的说明。而且一般情况下为了更好理解或者消除魔法值,通常的处理方案是定义一个枚举:
有些枚举是这样定义的
public enum GenderType{ FEMALE,MALE,UNKNOWN }
那么通常很多人会这么入库(java伪代码)
if(GenderType.MALE){ // 写入 1 }else if(GenderType.FEMALE){ // 写入 0 }else{ //也可能是泰国回来的 那就 2 }
读取的时候要么同样按照上面的再反向处理一次或者使用数据库sql语法 case when
来直接写入DTO
CASE gender WHEN 1 THEN '男' WHEN 0 THEN '女' ELSE '未知' END
这种处理方式看起来不是很优雅。而且多了很多的判断和处理逻辑,和我们的业务并不是非常相关。所以我们可以选择更好的处理方式。
如果你ORM框架用的是Mybatis。那么将很容易通过TypeHandler<T>接口解决这个问题。
public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
源码分析:
我们发现TypeHandler有一个实现类EnumOrdinalTypeHandler。字面意思是可以通过枚举的序号来处理类型。
@Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.ordinal()); }
我们先不考虑setNull的情况。通过此方法我们发现确实存入的是枚举的顺序值(顺序从0开始),拿上面的例子来说 如果是GenderType.FEMALE是0,如果是GenderType.MALE是1,但是当GenderType.UNKNOWN时存入的是3。取的时候也是自然反向处理为具体的GenderType枚举。
我们还发现有另外一个枚举类型处理器。它的set方法是这样的:
@Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { ps.setString(i, parameter.name()); } else { ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589 } }
我们不考虑jdbcType问题发现都是将Enum.name()的值写入数据库。拿上面的例子来说 如果是GenderType.FEMALE是FEMALE,如果是GenderType.MALE是MALE,但是当GenderType.UNKNOWN时存入的是UNKNOWN。读库是通过Enum.valueOf(Class<T> enumType,String name)来进行反转操作。
如果说我们的枚举类型或者说我们使用其他方式来处理类别转换怎么办?当然Mybatis不会帮你干这么具体的事情。需要你自己来实现了。我们还拿枚举作为例子,然后模仿上面的两种TypeHandler。 还是拿开始的例子来说通常我个人比较喜欢这么定义枚举:
public enum GenderTypeEnum { /** * female. */ FEMALE(0, "女"), /** * male. */ MALE(1,"男"), /** * unknown. */ UNKNOWN(2, "未知"); private int value; private String description; GenderType(int value, String description) { this.value = value; this.description = description; } public int value() { return this.value; } public String description() { return this.description; } }
通过继承BaseTypeHandler实现该抽象类的3个钩子方法就行了:
@MappedTypes({GenderTypeEnum.class}) @MappedJdbcTypes({JdbcType.INTEGER}) public class GenderTypeEnumTypeHandler extends BaseTypeHandler<GenderTypeEnum> { @Override public void setNonNullParameter(PreparedStatement ps, int i, GenderTypeEnum parameter, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { ps.setInt(i, parameter.value()); } else { // see r3589 ps.setObject(i, parameter.value(), jdbcType.TYPE_CODE); } } @Override public GenderTypeEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { return getGenderType(rs.getInt(columnName)); } @Override public GenderTypeEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return getGenderType(rs.getInt(columnIndex)); } @Override public GenderTypeEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return getGenderType(cs.getInt(columnIndex)); } private GenderTypeEnum getGenderType(int value) { Class<GenderTypeEnum> genderTypeClass = GenderTypeEnum.class; return Arrays.stream(genderTypeClass.getEnumConstants()) .filter(genderType -> genderType.value() == value) .findFirst().orElse(GenderTypeEnum.UNKNOWN); } }
TypeHandler 实现写好了,那么如何让其发挥作用呢?我们接着往下走。
TypeHandler作用是javaType和jdbcType相互转换。所以在声明一个TypeHandler的时候一定要明确该TypeHandler处理的这两种类型。这是必须要明确的原则。MyBatis不会通过窥探数据库元信息来决定使用哪种JDBC类型,所以你必须在参数和结果映射中指明何种类型的字段,使其能够绑定到正确的类型处理器上。MyBatis直到语句被执行时才清楚数据类型。 通过上述例子中的@MappedJdbcTypes和@MappedTypes来进行绑定类型转换关系,也可以通过xml的typeHandler元素中的jdbcType或者javaType来指定。如果同时指定,xml的优先级要高。 注意有可能你会覆盖内置的TypeHandler。所以自定义时一定要去了解Mybatis提供的一些默认处理器。避免对其他业务的影响。所以使用自定义TypeHandler很重要的一个原则就是一定要声明JavaType和JdbcType.上面这些虽然比较生涩但是对于使用好TypeHandler非常重要。接下来我们来讲讲具体的配置。
我们这里只讲xml中的配置:
<resultMap id="StudentMap" type="cn.felord.mybatis.entity.Student"> <id column="student_id" property="studentId"/> <result column="student_name" property="studentName"/> <result column="gender" property="genderType" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/> <result column="age" property="age"/> </resultMap>
<insert id="saveStu"> insert into student (student_name, gender, age) values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER,typeHandler=cn.felord.mybatis.type.GenderTypeEnumTypeHandler}, #{age}) </insert>
如果注册了别名都可以使用别名。上面的好处就是不用在TypeHandlerRegistry中进行注册。
在配置中声明注册TypeHandler,然后Mybatis根据两种类型会自动匹配。所以这里还是要强调2.5中的核心要点。
<typeHandlers> <typeHandler jdbcType="JdbcType枚举存在的枚举" javaType="typeAliases的别名或者全限定类名" handler="类全限定名"/> <package name="指定所有typeHandler所在的包的包名"/> </typeHandlers>
如果你注册了TypeHandler。在Mapper.xml中只需要声明jdbcType和javaType,无需再声明具体的typeHandler。Mybatis会自动通过jdbcType、javaType来映射到具体注册的TypeHandler上去 。就像下面的例子
<insert id="saveAutomaticStu"> insert into student (student_name, gender, age) values (#{studentName}, #{genderType,javaType=cn.felord.mybatis.enums.GenderTypeEnum,jdbcType=INTEGER}, #{age}) </insert>
今天我们学习了mybatis开发中如何通过使用类型处理器进行类型的转换处理,如何处理枚举,如何自定义处理器并使用它。相信对你在java开发过程中会有很大的帮助。相关的代码在我的码云仓库中: https://gitee.com/felord/mybatis-test.git