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 时,对于枚举,其处理器查找顺序为:
- 查找是否有专门针对该类型注册的处理器
- 如果是枚举类型,查找其实现的接口是否有对应的处理器
- 如果都没有,使用默认的枚举处理器
关键代码在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;
}
在没有其他处理的情况下,默认最终会由 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);
}
}
// ...
}
其总的逻辑就是:
- 在构造方法中,初始化后面所需要用到的属性
- 在 setNonNullParameter 方法中,将枚举值转换为对应的数据库字段值
- 在 getNullableResult 等方法中,将数据库字段值转换为对应的枚举值
具体实现细节,这里不进行分析。