场景描述
我们在实际场景中经常会遇到需要将枚举值存储到数据库中,或是将从数据库中查询到的值对应到枚举类上的情况。
比如表process
大致定义如下:
-- ----------------------------
-- Table structure for process
-- ----------------------------
DROP TABLE IF EXISTS `process`;
CREATE TABLE `process` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`status` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
对应实体类Process
,大致定义如下:
public class Process{
private int id;
private String name;
private ProcessStatus status;
// 省略 getter setter toString 等
}
其中,枚举类ProcessStatus
,大致定义如下:
public enum ProcessStatus {
RUNNING(100, "running"),
BLOCKED(101, "blocked"),
STOPPED(102, "stopped");
private int code;
private String desc;
ProcessStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
// ...
}
如果此时我们想在存储Process
类时直接将ProcessStatus
对应成某种值,或者在查询时直接将数据库status
字段值对应成为ProcessStatus
类,而不需要用硬编码的方式做更多的转换,我们可以考虑采用 MyBatis 提供的typeHandler
。
MyBatis 内置的枚举处理器
为了处理上述遇到的问题,MyBatis 内置了两种 typeHandler,分别是org.apache.ibatis.type.EnumTypeHandler
和org.apache.ibatis.type.EnumOrdinalTypeHandler
。
EnumTypeHandler
作为默认的枚举 typeHandler,EnumTypeHandler
将使用枚举实例名称来和对应的枚举类之间做转换。
比如process
表有记录:
id | name | status |
---|---|---|
1 | first | RUNNING |
在查询时,Process
类变量status
将自动赋值为ProcessStatus.RUNNING
,添加记录时同理,数据库值将存储为其枚举类实例名称(RUNNING/BLOCKED/STOPPED)。
EnumOrdinalTypeHandler
EnumOrdinalTypeHandler
将使用枚举实例的 ordinal 值(序数值,从0开始)来和枚举类之间做转换。
比如process
有记录:
id | name | status |
---|---|---|
1 | first | 1 |
显式为ProcessStatus
全局指定 typeHandler:
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.foo.ProcessStatus"/>
</typeHandlers>
在查询时,Process
类变量status
将自动赋值为ProcessStatus.BLOCKED
,添加记录时同理,数据库值将存储为其枚举类实例序号(0/1/2)。
混合使用
假如想在一处使用EnumTypeHandler
,另外一处使用EnumOrdinalTypeHandler
,可以在 mapped statement 中单独指定 typeHandler。
<insert id="insert" parameterType="com.foo.Process">
insert into process (id, name, status)
values (#{id}, #{name}, #{status, typeHandler=org.apache.ibatis.type.EnumTypeHandler})
</insert>
或
<resultMap id="processMap" type="com.foo.Process">
<id column="id" property="id"/>
<id column="name" property="name"/>
<id column="status" property="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
</resultMap>
<select id="findById" resultMap="processMap">
select * FROM process WHERE id=#{id}
</select>
自定义枚举处理器
回到我们的场景描述中来,我们需要用枚举实例的 code 值来对应相应的枚举。此时,系统内置的两个枚举处理器便不能很好地完成我们的需求了,所以我们要自定义枚举处理器。
实现方法
枚举处理器也是处理器(typeHandler)的一种,关于自定义处理器的内容,可以参考官方文档。主要操作便是实现org.apache.ibatis.type.TypeHandler
或继承更为方便的org.apache.ibatis.type.BaseTypeHandler
类。
我们选择继承BaseTypeHandler
来完成工作,BaseTypeHandler
需要实现4个方法:
-
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException
用于定义设置参数时,该如何把 Java 类型的参数转换为对应的数据库类型
-
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException
用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的 Java 类型
-
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException
用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的 Java 类型
-
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException
用定义调用存储过程后,如何把数据库类型转换为对应的 Java 类型
由于「枚举」是一个统称,不像具体类型的处理器一样可以使用多种方式来指定匹配的 Java 类型,所以按照官方文档的做法,我们将使用指定泛型的方式来自定义枚举构造器(EnumTypeHanlder
和EnumOrdinalTypeHandler
源代码也是这么实现的),官方文档示例:
//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
private Class<E> type;
public GenericTypeHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
...
}
实现过程
为了更好完成自定义枚举的工作,我们修改一下我们上面定义的枚举ProcessStatus
,使它实现一个通用接口。
public interface BaseEnum {
int getCode();
}
public enum ProcessStatus implements BaseEnum{
RUNNING(100, "running"),
BLOCKED(101, "blocked"),
STOPPED(102, "stopped");
private int code;
private String desc;
ProcessStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
@Override
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
然后再使用一个枚举工作类来完成从枚举 code 值获得枚举实例的工作:
public class EnumUtils {
public static <T extends Enum<?> & BaseEnum> T codeOf(Class<T> enumClass, int code) {
T[] enumConstants = enumClass.getEnumConstants();
for (T t : enumConstants) {
if (t.getCode() == code) {
return t;
}
}
return null;
}
}
万事俱备,接下来完成自定义枚举处理器:
import com.foo.BaseEnum;
import com.foo.EnumUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class EnumCodeTypeHandler<E extends Enum<E> & BaseEnum> extends BaseTypeHandler<E> {
private final Class<E> type;
public EnumCodeTypeHandler(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, E 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 : EnumUtils.codeOf(this.type, code);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return rs.wasNull() ? null : EnumUtils.codeOf(this.type, code);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return cs.wasNull() ? null : EnumUtils.codeOf(this.type, code);
}
}
在 mybatis-config.xml 中注册枚举处理器
<typeHandlers>
<typeHandler handler="com.foo.EnumCodeTypeHandler" javaType="com.foo.ProcessStatus" />
</typeHandlers>
假设数据库process
表有如下记录:
id | name | status |
---|---|---|
1 | first | 101 |
ProcessMapper.java
使用如下查询:
@Select("select * from process where id=#{id}")
public Process findById(int id);
查询结果 status 值将会对应到枚举实例 ProcessStatus.BLOCKED
上。
查询结果Process{id=1, name='first', status=BLOCKED}
设置默认枚举处理器
在 mybatis-config.xml 中为单个枚举注册枚举处理器的方式在需要处理的枚举数量增长时,会带来很多不必要的工作量,根据官方文档,我们可以在 configuration - settings
节点下设置默认枚举处理器,没有特殊指定处理器的枚举都将默认使用这个处理器。
<settings>
<setting name="defaultEnumTypeHandler" value="com.foo.EnumCodeTypeHandler" />
</settings>
说明和参考资料
说明:文中代码测试基于 JDK 8,MyBatis 3.4.5。
参考资料:
如何在MyBatis中优雅的使用枚举,特别感谢这篇清晰明了的文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。