MyBatis 枚举映射
内置实现
MyBatis 对枚举类型提供了两种默认的 TypeHandler 实现:
EnumTypeHandler
:使用枚举的 name() 值进行映射[1]EnumOrdinalTypeHandler
:使用枚举的 ordinal() 值进行映射
默认情况下使用EnumTypeHandler
。
自定义枚举映射
有时候我们希望用枚举中的其他字段来做映射,这时就需要自定义 TypeHandler。下面通过一个例子来说明。
1. 定义接口
首先定义一个通用的枚举接口:
public interface LabelValue {
String getLabel();
Integer getValue();
}
2. 实现枚举
让枚举类实现该接口:
public enum Color implements LabelValue {
RED("红色", 1),
GREEN("绿色", 2),
BLUE("蓝色", 3);
private final String label;
private final Integer value;
Color(String label, Integer value) {
this.label = label;
this.value = value;
}
@Override
public String getLabel() {
return label;
}
@Override
public Integer getValue() {
return value;
}
}
3. 自定义 TypeHandler
然后自定义 TypeHandler 来处理 LabelValue。在那之前我们先来看EnumTypeHandler
的实现,之后参考其代码实现自定义:
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private final Class<E> type;
public EnumTypeHandler(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 {
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String s = cs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
}
注意到EnumTypeHandler
的构造方法,会接收需要处理的枚举类型作为参数。setNonNullParameter
方法,会使用枚举的 name() 值保存到数据库。而从数据库查询数据时(各getNullableResult
方法),会根据 name() 值创建对应的枚举。
我们可以参考EnumTypeHandler
实现自定义的类型处理器来处理 LabelValue 类型:
@MappedTypes(LabelValue.class)
public class LabelValueTypeHandler<E extends LabelValue> extends BaseTypeHandler<E> {
private Class<E> type;
// 记录枚举值和枚举的映射关系
private Map<Integer, E> enumMap;
public LabelValueTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
E[] enums = type.getEnumConstants();
if (enums != null) {
// 将枚举值和枚举类型存入 enumMap,方便快速取值
this.enumMap = new HashMap<>(enums.length);
for (E e : enums) {
enumMap.put(e.getValue(), e);
}
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter.getValue());
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
int value = rs.getInt(columnName);
if (rs.wasNull()) {
return null;
}
return getEnum(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int value = rs.getInt(columnIndex);
if (rs.wasNull()) {
return null;
}
return getEnum(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int value = cs.getInt(columnIndex);
if (cs.wasNull()) {
return null;
}
return getEnum(value);
}
private E getEnum(int value) {
try {
// 从 enumMap 中取出对应的枚举实例
return enumMap.get(value);
} catch (Exception ex) {
throw new IllegalArgumentException(
"Cannot convert " + value + " to " + type.getSimpleName() + " by value.", ex);
}
}
}
可以看到这个 TypeHandler:
- 使用
@MappedTypes
注解指定处理LabelValue
接口 - 构造函数接收枚举类型参数,用于初始化枚举映射
- 使用 Map 缓存枚举值和枚举实例的对应关系
- 使用枚举的 getValue() 方法的返回值保存到数据库,从数据库查询数据时,会根据 getValue() 的返回值创建对应的枚举
4. 配置 TypeHandler
在 MyBatis 配置文件中注册 TypeHandler:
<typeHandlers>
<typeHandler handler="com.example.LabelValueTypeHandler"
javaType="com.example.LabelValue"
jdbcType="INTEGER"/>
</typeHandlers>
5. 使用效果
配置完成后:
- 保存数据时,会将枚举的 getValue() 方法的返回值(如 RED.getValue() = 1)保存到数据库
- 查询数据时,会根据数据库的值找到对应的枚举实例(如 getValue() = 1 映射为 Color.RED)
工作原理
MyBatis 3.4.5 版本对枚举映射做了增强:
- 支持枚举接口的 TypeHandler
- 允许配置默认的枚举 TypeHandler
- 支持类型继承时的 TypeHandler 传递
关键实现在TypeHandlerRegistry
的getJdbcHandlerMap
方法:
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
// 获取已注册的处理器
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
return null;
}
if (jdbcHandlerMap == null && type instanceof Class) {
Class<?> clazz = (Class<?>) type;
// 处理枚举类型
if (clazz.isEnum()) {
// 查找枚举接口的处理器
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
if (jdbcHandlerMap == null) {
// 使用默认枚举处理器
register(clazz, getInstance(clazz, defaultEnumTypeHandler));
return TYPE_HANDLER_MAP.get(clazz);
}
} else {
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
可以看到,对于枚举,其处理器查找顺序为:
- 查找是否有专门针对该类型注册的处理器
- 如果是枚举类型,查找其实现的接口是否有对应的处理器
- 如果都没有,使用默认的枚举处理器
这种机制让我们可以:
- 为具体的枚举类型配置专门的处理器
- 为一类枚举配置统一的处理器(通过接口)
- 修改所有枚举的默认处理方式(参见 settings 中的 defaultEnumTypeHandler 配置)
实际上,MyBatis-Plus 提供了 MybatisEnumTypeHandler 来处理枚举类型,枚举只需要实现 IEnum 接口,即可完成映射。
意思是会使用枚举的 name() 值保存到数据库,从数据库查询数据时,会根据 name() 值创建对应的枚举 ↩︎