MyBatis 枚举映射

内置实现

MyBatis 对枚举类型提供了两种默认的 TypeHandler 实现:

  1. EnumTypeHandler:使用枚举的 name() 值进行映射[1]
  2. 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:

  1. 使用@MappedTypes注解指定处理LabelValue接口
  2. 构造函数接收枚举类型参数,用于初始化枚举映射
  3. 使用 Map 缓存枚举值和枚举实例的对应关系
  4. 使用枚举的 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 版本对枚举映射做了增强:

  1. 支持枚举接口的 TypeHandler
  2. 允许配置默认的枚举 TypeHandler
  3. 支持类型继承时的 TypeHandler 传递

关键实现在TypeHandlerRegistrygetJdbcHandlerMap方法:

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;
}

可以看到,对于枚举,其处理器查找顺序为:

  1. 查找是否有专门针对该类型注册的处理器
  2. 如果是枚举类型,查找其实现的接口是否有对应的处理器
  3. 如果都没有,使用默认的枚举处理器

这种机制让我们可以:

  1. 为具体的枚举类型配置专门的处理器
  2. 为一类枚举配置统一的处理器(通过接口)
  3. 修改所有枚举的默认处理方式(参见 settings 中的 defaultEnumTypeHandler 配置)

实际上,MyBatis-Plus 提供了 MybatisEnumTypeHandler 来处理枚举类型,枚举只需要实现 IEnum 接口,即可完成映射。


  1. 意思是会使用枚举的 name() 值保存到数据库,从数据库查询数据时,会根据 name() 值创建对应的枚举 ↩︎

posted @ 2025-03-26 23:33  Higurashi-kagome  阅读(408)  评论(0)    收藏  举报