Mybatis:实体字段使用枚举写入数据库及序列化时自动添加说明字段

334 阅读3分钟

你有没有遇到过这种情况:数据库字段注释没有注释或者没有及时更新,而代码里的字段使用Integer、String、Long类型也没有注释。好一点的情况是对该字段定义了常量类或枚举比如下面这样,但是实体仍然使用Integer类型

public interface SomeState{
    /**
    * 初始化状态
    */
    int INIT_STATE = 1;
    /**
    * 结束状态
    */
    int FIN_STATE = 2;
}
@Getter
public enum SomeStateEnum{
    INIT_STATE(1,"初始化状态"),
    FIN_STATE(2,"结束状态"),;
    private final String name;
    private final Integer code;
    SomeStateEnum(Integer code,String name){
        this.code = code;
        this.name = name;
    }
}
@Table(name="order")
public class Order{
    @Column("state")
    private Integer state;
}
order.setState(SomeState.INIT_STATE);
order.setState(SomeStateEnum.INIT_STATE.getCode());

这样定义常量类或接口或枚举,可以把字段含义聚集在一起,但是如果使用了枚举还要再多调一个 getCode 方法。 最重要一个潜在问题是由于没有对 state 做类型限制,在set的时候仍然可以直接传入一个数字,甚至由于手滑传入一个非法状态,比如

order.setState(11);

枚举字段怎么插入到数据库

怎么对类型做限制呢,那就直接在实体类字段声明时使用枚举类型,比如

public class Order{
    private SomeStateEnum state;
}

order.setState(SomeStateEnum.INIT_STATE);

但是这样又引入一个新问题,这样的字段是无法直接存到数据库的。 这就需要使用 mybatis 提供的类型转换器 TypeHandler 了。 我们可以定义一个SomeStateEnumTypeHandler

@MappedTypes(SomeStateEnum.class)
public class SomeStateEnumTypeHandler<SomeStateEnum> extends BaseTypeHandler<SomeStateEnum>{

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, SomeStateEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode());
    }
}

正常情况下系统里是存在很多这样的枚举定义的,对每一个枚举都定义一个 TypeHandler 是不现实的。所以还是得定义一个更加通用的 TypeHandler 才行,那些需要转换的枚举都实现一个统一接口,而且实现这样的接口后,我们可以再接口定义一个通用 getByCode 的方法,这样我们就不用每个枚举里面都写一个 getByCode 方法了。

public interface IntCodeEnum {

    Integer getCode();

    String getName();

    static <T> T getByCode(Class<T> clazz, Integer code) {
        if (code == null) {
            return null;
        }
        if (clazz == null) {
            throw new RuntimeException("clazz为空");
        }
        if (clazz.getSuperclass() != Enum.class) {
            throw new RuntimeException(clazz.getName() + "不是枚举");
        }
        if (Arrays.stream(clazz.getInterfaces()).noneMatch(i -> i == IntCodeEnum.class)) {
            throw new RuntimeException(clazz.getName() + "需要实现IntCodeEnum");
        }

        for (T enumConstant : clazz.getEnumConstants()) {
            IntCodeEnum anEnum = (IntCodeEnum) enumConstant;
            if (anEnum.getCode().equals(code)) {
                return enumConstant;
            }
        }
        throw new RuntimeException(clazz.getName() + "不存在code=" + code);
    }

}

实现了上面的接口后,我们就可以写一个更加通用的TypeHandler

@MappedTypes(IntCodeEnum.class)
public class IntCodeEnumTypeHandler<E extends IntCodeEnum> extends BaseTypeHandler<E> {
    private Class<E> type;
    private Map<Integer, E> enumMap;
    public IntCodeEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        E[] enums = type.getEnumConstants();
        //配置到 <typeHandler> 初始化时,这里的 type 只是一个接口,并不是枚举,所以要特殊判断
        //下面除了第一个 setNonNullParameter 赋值不需要 enumMap,其他 3 个都需要,
        if (enums != null) {
            this.enumMap = new HashMap<>(enums.length);
            for (E anEnum : enums) {
                this.enumMap.put(anEnum.getCode(), anEnum);
            }
        }
    }

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

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return getE(rs.getInt(columnName), rs.wasNull());
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return getE(rs.getInt(columnIndex), rs.wasNull());
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return getE(cs.getInt(columnIndex), cs.wasNull());
    }

    private E getE(int code, boolean wasNull) {
        if (wasNull) {
            return null;
        }
        try {
            return enumMap.get(code);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert " + code + " to " + type.getSimpleName() + " .", ex);
        }

    }

}

这个TypeHandler 需要让mybatis感知到,把这个类放到 com.xxx.typehandler 包下面,然后需要配置

mybatis.type-handlers-package=com.xxx.typehandler

这样就可以实现实体类型中定义了枚举类型字段仍然可以正常插入数据库了。

枚举字段的序列化与反序列化

在返回给前端数据时,可能之前都不会定义枚举类型字段,因为不做特殊处理的话是没法实现我们的需求的,因为他会返回"INIT_STATE"、"FIN_STATE"这样的字符串。而如果我们手动set值得话又太麻烦。 前端传数据时含有枚举字段也需要反序列化为对应的枚举。

下面借助 jackson 的序列化和反序列化器就可以让我们定义枚举字段后,自动设置相应的值,而且可以你的业务需求往里面增加枚举描述字段比如 自动添加 stateName 字段。

@Configuration
@SuppressWarnings({"rawtypes"})
public class EnumJacksonConfig {


    /**
     * 处理 IntCodeEnum 子类的序列化
     */
    @JsonComponent
    public static class Serializer extends JsonSerializer<IntCodeEnum> {
        @Override
        public void serialize(IntCodeEnum intCodeEnum, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            JsonStreamContext outputContext = jsonGenerator.getOutputContext();
            String currentName = outputContext.getCurrentName();
            jsonGenerator.writeNumber(intCodeEnum.getCode());
            jsonGenerator.writeObjectField(currentName + "Name", intCodeEnum.getName());
            // jsonGenerator.writeString(intCodeEnum.getName());
            // jsonGenerator.writeObjectField(currentName + "Code", intCodeEnum.getCode());
        }
    }

    /**
     * RequestBody
     * 处理枚举类型的反序列化,特殊处理 IntCodeEnum
     */
    @JsonComponent
    public static class EnumDeserializer
            extends JsonDeserializer<Enum> implements ContextualDeserializer {

        @Override
        public Enum deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException {
            //不使用当前的类进行反序列化
            return null;
        }

        @Override
        public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
                throws JsonMappingException {
            //获取参数类型
            final Class<?> enumClass = ctxt.getContextualType().getRawClass();
            //如果继承的 IntCodeEnum 接口,使用 IntCodeEnum 反序列化器
            if (IntCodeEnum.class.isAssignableFrom(enumClass)) {
                return new IntCodeEnumDeserializer(enumClass);
            } else {
                //这里返回的默认的枚举策略
                return new DefaultEnumDeserializer(enumClass);
            }
        }
    }

    /**
     * IntCodeEnum 子类枚举的反序列化
     */
    public static class IntCodeEnumDeserializer extends JsonDeserializer<IntCodeEnum> {
        private Class<?> enumClass;

        public IntCodeEnumDeserializer(Class<?> enumClass) {
            this.enumClass = enumClass;
        }

        @Override
        public IntCodeEnum deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            //获取序列化值
            int intValue;
            if (p.hasToken(JsonToken.VALUE_STRING)) {
                String valueAsString = p.getValueAsString();
                if (StringUtils.isEmpty(valueAsString)){
                    return null;
                }
                try {
                    intValue = Integer.parseInt(valueAsString);
                } catch (NumberFormatException e) {
                    throw new RuntimeException(enumClass.getSimpleName() + "的code无法解析");
                }
            }else {
                intValue = p.getIntValue();
            }

            //遍历返回枚举值
//          enumClass.va
            for (Object enumConstant : enumClass.getEnumConstants()) {
                if (((IntCodeEnum) enumConstant).getCode().equals(intValue)) {
                    return (IntCodeEnum) enumConstant;
                }
            }
            //可以根据逻辑返回 null 或者抛出非法参数异常
            return null;
        }
    }

    /**
     * 默认枚举反序列化
     */
    public static class DefaultEnumDeserializer extends JsonDeserializer<Enum> {
        private Class<?> enumClass;

        public DefaultEnumDeserializer(Class<?> enumClass) {
            this.enumClass = enumClass;
        }

        @Override
        public Enum deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException {
            JsonToken token = p.getCurrentToken();
            //枚举可能是用的 name() 方式,也就是字符串
            if (token == JsonToken.VALUE_STRING) {
                String value = p.getValueAsString();
                for (Object enumConstant : enumClass.getEnumConstants()) {
                    if (((Enum) enumConstant).name().equals(value)) {
                        return (Enum) enumConstant;
                    }
                }
            }
            //也可能使用的 ordinal() 数值方式
            else if (token == JsonToken.VALUE_NUMBER_INT) {
                int intValue = p.getIntValue();
                for (Object enumConstant : enumClass.getEnumConstants()) {
                    if (((Enum) enumConstant).ordinal() == intValue) {
                        return (Enum) enumConstant;
                    }
                }
            }
            //可以根据逻辑返回 null 或者抛出非法参数异常
            return null;
        }
    }

}