如何在 Mybatis 中优雅地使用枚举
前言
Mybatis 遇到字段为枚举时无法解析成我们想要的数据
MyBatis 内置枚举转换器
org.apache.ibatis.type.EnumTypeHandler
和 org.apache.ibatis.type.EnumOrdinalTypeHandler
EnumTypeHandler
Mybatis 中默认的枚举转换器,获取枚举中的 name
属性
EnumOrdinalTypeHandler
获取枚举中 ordinal
属性,相当于索引,从1开始
为了贴合业务开发,我们需要创建一个新的枚举转换器
1、枚举接口
枚举通用行为接口,此处的 value
属性为存储于数据库中的值
public interface BaseEnum<T extends Serializable> {
/**
* 枚举数据库存储值
*/
T getValue();
}
让需要的枚举实现该接口,此方式不够灵活,只能使用存取value
属性
@Getter
@AllArgsConstructor
public enum GenderEnum implements BaseEnum<Integer> {
MALE(1, "男"),
FEMALE(2, "女"),
;
@JsonValue
private Integer value;
private String desc;
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static GenderEnum getByValue(int value) {
for (GenderEnum genderEnum : values()) {
if (genderEnum.getValue() == value) {
return genderEnum;
}
}
return null;
}
}
2、枚举注解
/**
* 支持普通枚举类字段, 只用在enum类的字段上
* 当实体类的属性是普通枚举,且是其中一个字段,使用该注解来标注枚举类里的那个属性对应字段
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface EnumValue {
}
注解可以标记到想要的任何一个属性上
@Getter
@AllArgsConstructor
public enum GenderEnum {
MALE(1, "男"),
FEMALE(2, "女"),
;
@JsonValue
@EnumValue
private int value;
private String desc;
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static GenderEnum getByValue(int value) {
for (GenderEnum genderEnum : values()) {
if (genderEnum.getValue() == value) {
return genderEnum;
}
}
return null;
}
}
3、自定义枚举属性转换器
MybatisEnumTypeHandler
用于处理实现了BaseEnum
的或者使用了@EnumValue
的枚举
/**
* 自定义枚举属性转换器
**/
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 (!BaseEnum.class.isAssignableFrom(enumClassType)) {
name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));
}
this.propertyType = PrimitiveUtil.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
this.getInvoker = metaClass.getGetInvoker(name);
}
/**
* 查找标记标记EnumValue字段
*
* @param clazz class
* @return EnumValue字段
*/
public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
if (clazz != null && clazz.isEnum()) {
String className = clazz.getName();
return Optional.ofNullable(TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(className, key -> {
Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
return fieldOptional.map(Field::getName).orElse(null);
}));
}
return Optional.empty();
}
private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst();
}
@SuppressWarnings("Duplicates")
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
if (jdbcType == null) {
ps.setObject(i, this.getValue(parameter));
} else {
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);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object value = rs.getObject(columnIndex, this.propertyType);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object value = cs.getObject(columnIndex, this.propertyType);
if (null == value && cs.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);
}
/**
* 值比较
*
* @param sourceValue 数据库字段值
* @param targetValue 当前枚举属性值
* @return 是否匹配
*/
protected boolean equalsValue(Object sourceValue, Object targetValue) {
String sValue = String.valueOf(sourceValue).trim();
String tValue = String.valueOf(targetValue).trim();
if (sourceValue instanceof Number && targetValue instanceof Number
&& new BigDecimal(sValue).compareTo(new BigDecimal(tValue)) == 0) {
return true;
}
return Objects.equals(sValue, tValue);
}
private Object getValue(Object object) {
try {
return this.getInvoker.invoke(object, new Object[0]);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e.getMessage());
}
}
}
工具类PrimitiveUtil
/**
* 包装类型转换
**/
public final class PrimitiveUtil {
private static final Map<Class<?>, Class<?>> PRIMITIVE_TYPE_TO_WRAPPER_MAP = new IdentityHashMap<>(8);
static {
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(boolean.class, Boolean.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(byte.class, Byte.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(char.class, Character.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(double.class, Double.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(float.class, Float.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(int.class, Integer.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(long.class, Long.class);
PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(short.class, Short.class);
}
public static Class<?> resolvePrimitiveIfNecessary(Class<?> clazz) {
return (clazz.isPrimitive() && clazz != void.class ? PRIMITIVE_TYPE_TO_WRAPPER_MAP.get(clazz) : clazz);
}
}
4、将MybatisEnumTypeHandler
注册成默认的枚举属性转换器
mybatis:
configuration:
# 自定义枚举属性转换器
default-enum-type-handler: com.xxx.handler.MybatisEnumTypeHandler
----淡定从容,宁静致远----