MyBatis Plus 自动映射枚举原理

使用

自动映射枚举中介绍了 MP 所提供的两种映射枚举方式:

方式一:注解标记

枚举属性使用 @EnumValue 注解,指定枚举值在数据库中存储的实际值。

@Getter
@AllArgsConstructor
public enum GradeEnum {
    PRIMARY(1, "小学"),
    SECONDARY(2, "中学"),
    HIGH(3, "高中");


    @EnumValue // 标记数据库存的值是 code
    private final int code;
    // 其他属性...
}

方式二:实现接口

实现 IEnum 接口,实现 getValue 方法,指定枚举值在数据库中存储的实际值。

@Getter
@AllArgsConstructor
public enum AgeEnum implements IEnum<Integer> {
    ONE(1, "一岁"),
    TWO(2, "二岁"),
    THREE(3, "三岁");

    private final int value;
    private final String desc;

    @Override
    public Integer getValue() {
        return this.value;
    }
}

未声明的枚举将使用 MyBatis 的 defaultEnumTypeHandler 的配置值进行映射。

原理

枚举所对应的 TypeHandler

推荐阅读:MyBatis 枚举映射

MyBatis 在获取 TypeHandler 时,对于枚举,其处理器查找顺序为:

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

关键代码在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;
}

在没有其他处理的情况下,默认最终会由 defaultEnumTypeHandler 处理。而 MyBatis-Plus 设置 defaultEnumTypeHandler 为 CompositeEnumTypeHandler:

public class MybatisConfiguration extends Configuration {
    // ...
    
    /**
     * 初始化调用
     */
    public MybatisConfiguration() {
        super();
        this.mapUnderscoreToCamelCase = true;
        typeHandlerRegistry.setDefaultEnumTypeHandler(CompositeEnumTypeHandler.class);
        languageRegistry.setDefaultDriverClass(MybatisXMLLanguageDriver.class);
    }

    @Override
    public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) {
        if (typeHandler != null) {
            CompositeEnumTypeHandler.setDefaultEnumTypeHandler(typeHandler);
        }
    }
    
    // ...
}

CompositeEnumTypeHandler 和 MybatisEnumTypeHandler

CompositeEnumTypeHandler 是一个代理类,实际处理逻辑会交给内部的 delegate:

public class CompositeEnumTypeHandler<E extends Enum<E>> implements TypeHandler<E> {

    private static final Map<Class<?>, Boolean> MP_ENUM_CACHE = new ConcurrentHashMap<>();
    @Setter
    private static Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
    private final TypeHandler<E> delegate;

    public CompositeEnumTypeHandler(Class<E> enumClassType) {
        if (enumClassType == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        if (CollectionUtils.computeIfAbsent(MP_ENUM_CACHE, enumClassType, MybatisEnumTypeHandler::isMpEnums)) {
            delegate = new MybatisEnumTypeHandler<>(enumClassType);
        } else {
            delegate = getInstance(enumClassType, defaultEnumTypeHandler);
        }
    }

    @Override
    public void setParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        delegate.setParameter(ps, i, parameter, jdbcType);
    }

    //  ...

}

从其构造方法可以看到,当 MybatisEnumTypeHandler::isMpEnums 返回 true 时,会创建 MybatisEnumTypeHandler 对象,否则创建 defaultEnumTypeHandler 对象。

MybatisEnumTypeHandler::isMpEnums 方法会判断枚举是否实现了 IEnum 接口或者存在 @EnumValue 注解:

public static boolean isMpEnums(Class<?> clazz) {
    return clazz != null && clazz.isEnum() && (IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
}

所以最终的自动映射逻辑在 MybatisEnumTypeHandler 中:

public class MybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

    private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private final Class<E> enumClassType;
    private final Class<?> propertyType;
    private final Invoker getInvoker;

    public MybatisEnumTypeHandler(Class<E> enumClassType) {
        if (enumClassType == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.enumClassType = enumClassType;
        MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
        String name = "value";
        if (!IEnum.class.isAssignableFrom(enumClassType)) {
            name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));
        }
        this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
        this.getInvoker = metaClass.getGetInvoker(name);
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
        throws SQLException {
        if (jdbcType == null) {
            ps.setObject(i, this.getValue(parameter));
        } else {
            // see r3589
            ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
        }
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Object value = rs.getObject(columnName, this.propertyType);
        if (null == value || rs.wasNull()) {
            return null;
        }
        return this.valueOf(value);
    }

    private E valueOf(Object value) {
        E[] es = this.enumClassType.getEnumConstants();
        return Arrays.stream(es).filter((e) -> equalsValue(value, getValue(e))).findAny().orElse(null);
    }

    private Object getValue(Object object) {
        try {
            return this.getInvoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }

    // ...
}

其总的逻辑就是:

  1. 在构造方法中,初始化后面所需要用到的属性
  2. 在 setNonNullParameter 方法中,将枚举值转换为对应的数据库字段值
  3. 在 getNullableResult 等方法中,将数据库字段值转换为对应的枚举值

具体实现细节,这里不进行分析。

posted @ 2025-06-04 00:14  Higurashi-kagome  阅读(258)  评论(0)    收藏  举报