Mybatis通用枚舉 Enum TypeHandler

介紹

Mybatis 內置提供了兩種枚舉TypeHandler,EnumTypeHandler和EnumOrdinalTypeHandler

  • EnumTypeHandler
    默認的枚舉TypeHandler,入庫的值為枚舉的name

  • EnumOrdinalTypeHandler
    入庫的值為枚舉的位置

但是現實處理的時候,我們Enum定義時可能是需要存儲code,上面兩種都不符合我們的需求,這時候就需要自定義TypeHandler了。

public enum GenderEnum {
 
    UNKNOWN(0),
    MALE(1),
    FEMALE(2);
 
    @JsonValue
    @Getter
    private int code;
 
    GenderEnum(int code) {
        this.code = code;
    }
 
    private static final Map<Integer, GenderEnum> VALUES = new HashMap<>();
 
    static {
        for (final GenderEnum gender : GenderEnum.values()) {
            GenderEnum.VALUES.put(gender.getCode(), gender);
        }
    }
 
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public GenderEnum of(int code) {
        return VALUES.get(code);
    }
}

實現方案

通常可以為每個Enum類配置一個TypeHandler,但是這種比較繁瑣,這里通過注解配合Mybatis的默認EnumTypeHander配置實現通用枚舉TypeHander。(代碼來自Mybatis-Plus,做了一些小改動)

  1. 定義一個注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface EnumValue {
 
}
  1. 實現通用的枚舉TypeHandler
    在mybatis-plus提供的TypeHandler上做了簡單修改,會取枚舉添加@EnumValue注解的屬性值,如果未發現注解,使用枚舉的name。
@Slf4j
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;
 
    private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_TYPE_MAP = new IdentityHashMap<>(8);
    private static final Map<Class<?>, Class<?>> PRIMITIVE_TYPE_TO_WRAPPER_MAP = new IdentityHashMap<>(8);
 
    static {
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Boolean.class, boolean.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Byte.class, byte.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Character.class, char.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Double.class, double.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Float.class, float.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Integer.class, int.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Long.class, long.class);
        PRIMITIVE_WRAPPER_TYPE_MAP.put(Short.class, short.class);
        for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_WRAPPER_TYPE_MAP.entrySet()) {
            PRIMITIVE_TYPE_TO_WRAPPER_MAP.put(entry.getValue(), entry.getKey());
        }
    }
 
    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);
        Optional<String> name = findEnumValueFieldName(this.enumClassType);
        if (name.isPresent()) {
            this.propertyType = resolvePrimitiveIfNecessary(metaClass.getGetterType(name.get()));
            this.getInvoker = metaClass.getGetInvoker(name.get());
        } else {
            log.info(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName()));
            this.propertyType = String.class;
            this.getInvoker = null;
        }
    }
 
    /**
     * 查找標記標記EnumValue字段
     *
     * @param clazz class
     * @return EnumValue字段
     * @since 3.3.1
     */
    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();
    }
 
    private static Class<?> resolvePrimitiveIfNecessary(Class<?> clazz) {
        return (clazz.isPrimitive() && clazz != void.class ? PRIMITIVE_TYPE_TO_WRAPPER_MAP.get(clazz) : clazz);
    }
 
    @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 {
            // 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);
    }
 
    @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 是否匹配
     * @since 3.3.0
     */
    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);
    }
     
    /**
     * 取值,如果有@EnumValue注解,會取該屬性的值,否則取枚舉的name
     */
    private Object getValue(E object) {
        if (this.getInvoker == null) {
            return object.name();
        }
        try {
            return this.getInvoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }
}

使用說明

  1. 配置
mybatis:
    configuration:
        default-enum-type-handler: com.example.mybatis.typeHandler.MybatisEnumTypeHandler
  1. 在對應的Enum屬性上添加@EnumValue注解
public enum GenderEnum {
 
    UNKNOWN(0),
    MALE(1),
    FEMALE(2);
 
    @JsonValue
    @EnumValue
    @Getter
    private int code;
 
    GenderEnum(int code) {
        this.code = code;
    }
 
    private static final Map<Integer, GenderEnum> VALUES = new HashMap<>();
 
    static {
        for (final GenderEnum gender : GenderEnum.values()) {
            GenderEnum.VALUES.put(gender.getCode(), gender);
        }
    }
 
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public GenderEnum of(int code) {
        return VALUES.get(code);
    }
}

配合框架使用

  1. tk通用Mapper使用
    tk mapper會默認忽略Enum的屬性字段。
    相關源碼:
for (EntityField field : fields) {
    //如果啟用了簡單類型,就做簡單類型校驗,如果不是簡單類型,直接跳過
    //3.5.0 如果啟用了枚舉作為簡單類型,就不會自動忽略枚舉類型
    //4.0 如果標記了 Column 或 ColumnType 注解,也不忽略
    if (config.isUseSimpleType()
            && !field.isAnnotationPresent(Column.class)
            && !field.isAnnotationPresent(ColumnType.class)
            && !(SimpleTypeUtil.isSimpleType(field.getJavaType())
            ||
            (config.isEnumAsSimpleType() && Enum.class.isAssignableFrom(field.getJavaType())))) {
        continue;
    }
    processField(entityTable, field, config, style);
}

有兩種方式處理:

  • 第一種:3.5.0 以上可以配置枚舉處理
mapper:
     enum-as-simple-type: true
  • 第二種:4.0以上對應屬性上添加@Column或者@ColumnType注解
@Data
@Table(name = "user")
public class User implements Serializable {
    private static final long serialVersionUID = -53635849389000664L;
    /**
     * 主鍵ID
     */
    @Id
    private Long id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private Integer age;
    /**
     * 郵箱
     */
    private String email;
 
    /**
     * 性別
     */
    @Column
    private GenderEnum gender;
 
}
  1. MybatisPlus使用
    mybatis-plus內置了Enum的支持,可以直接配置Enum的目錄。
mybatis-plus:
    type-enums-package: com.example.enums
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容