枚举类在前后端以及数据库的应用

  • Post author:
  • Post category:其他




枚举类在前后端以及数据库的应用

谈到枚举类,你一定知道一个优雅的设计,例如性别这一最常用的枚举类:

public enum Sex implements BaseEnum<Sex, Integer> {
    MALE(1, "男"),
    FEMALE(2, "女");

    private Integer code;
    private String name;

    Sex(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static Sex getEnum(Integer code) {
        for (Sex e : Sex.values()) {
            if (e.getCode().equals(code)) {
                return e;
            }
        }
        return null;
    }
}

这就是所谓优雅的枚举类设计,数据库存储的是code值,即枚举属性编号,前端显示的是name值,即枚举属性名称。

设计很完美,但要想真正实用还缺少很多东西。第一,数据库要想储存code值,得自定义MyBatis枚举类型转换器。

关于MyBatis枚举类型转换器,主要有两种实现方案,第一种,一种枚举类型写一个专用的枚举类型转换器。第二种,所有枚举类型使用统一的枚举类型转换器。闭着眼睛想,你也应该知道选择哪种了吧!不过,下面,我会把两种方式都列出来,还是以性别枚举类为例。



性别枚举类专用转换器

import cn.itcampus.hander.types.Sex;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = Sex.class)
public class SexTypeHandler implements TypeHandler<Sex> {
    @Override
    public void setParameter(PreparedStatement preparedStatement, int i, Sex sex, JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i, sex.getCode());
    }

    @Override
    public Sex getResult(ResultSet resultSet, String s) throws SQLException {
        int code = resultSet.getInt(s);
        return Sex.getEnum(code);
    }

    @Override
    public Sex getResult(ResultSet resultSet, int i) throws SQLException {
        int code = resultSet.getInt(i);
        return Sex.getEnum(code);
    }

    @Override
    public Sex getResult(CallableStatement callableStatement, int i) throws SQLException {
        int code = callableStatement.getInt(i);
        return Sex.getEnum(code);
    }
}

可以看出,专用转换器的设计与实现并不复杂,指定TypeHandler处理的枚举类名,实现四个方法,调用枚举类getCode()、getEnum方法即可实现。学过JDBC底层技术的网友应该知道,preparedStatement.setInt()方法是sql语句预处理时设置预留参数的方法,方法的第一个参数是预留参数在sql语句中的排列序号,第二个参数是预留参数的具体值。resultSet.getInt()则是sql读取成功返回实体对象时,用于获取实体某一个属性的方法,如果传入参数是int类型,表示获取第i个column字段,如果传入的参数是string类型,表示获取名为s的column字段。总之,使用过JDBC的朋友理解MyBatis类型转换器应该很容易。就此打住,对底层源码感兴趣的朋友可以继续深究,此文还是比较偏重应用层面。



统一的枚举类转换器

专用枚举类转换器虽然简单,但是,项目中有多少枚举类就要设计多少这样的枚举转换器,很明显是不合时宜的。接下来,介绍统一枚举转换器的设计与实现。

要想使用统一枚举转换器,就得用一个通用接口统一规范所有枚举类,这是通用类处理思想必要的前提。



枚举类通用接口

public interface BaseEnum<E extends Enum<E>, T> {
    T getCode();
    String getName();
}

对,你没看错,就是这么简单,当然,其实还需要规范枚举类的getEnum方法,但是因为这个方法是静态的,所以通用接口不方便直接约束,只能麻烦开发者费点心思,在所有枚举类中都定义一个getEnum方法,方便统一的枚举类转换器调用。



统一枚举类转换器

import cn.itcampus.hander.types.Education;
import cn.itcampus.hander.types.Sex;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedTypes({Sex.class, Education.class})
public class UniversalEnumHandler<E extends BaseEnum> extends BaseTypeHandler<E> {
    private Class<E> type;

    private E [] enums;

    /**
     * 设置配置文件设置的转换类以及枚举类内容,供其他方法更便捷高效的实现
     * @param type 配置文件中设置的转换类
     */
    public UniversalEnumHandler(Class<E> type) {
        if (type == null)
            throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
        this.enums = type.getEnumConstants();
        if (this.enums == null)
            throw new IllegalArgumentException(type.getSimpleName()
                    + " does not represent an enum type.");
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter,
                                    JdbcType jdbcType) throws SQLException {
        //BaseTypeHandler已经帮我们做了parameter的null判断
        ps.setObject(i,parameter.getCode());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放String类型
        Integer i = Integer.parseInt(rs.getString(columnName));
        if (rs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的value值,定位PersonType子类
            return locateEnumStatus(i);
        }
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex)
            throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放String类型
        Integer i = Integer.parseInt(rs.getString(columnIndex));
        if (rs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的value值
            return locateEnumStatus(i);
        }
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        // 根据数据库存储类型决定获取类型,本例子中数据库中存放String类型
        Integer i = Integer.parseInt(cs.getString(columnIndex));
        if (cs.wasNull()) {
            return null;
        } else {
            // 根据数据库中的value值
            return locateEnumStatus(i);
        }
    }

    /**
     * 枚举类型转换,由于构造函数获取了枚举的子类enums,让遍历更加高效快捷
     * @param value 数据库中存储的自定义value属性
     * @return value对应的枚举类
     */
    private E locateEnumStatus(Integer value) {
        for(E e : enums) {
            if(e.getCode().equals(value)) {
                return e;
            }
        }
        return null;
    }
}

可以看出,统一枚举处理器的设计与实现要复杂一些,但基本是模板式的代码,你可以直接在自己的项目中copy使用。并且,统一枚举处理器其实并不用指定出所有的枚举类,即@MappedTypes({Sex.class, Education.class})其实是多余的,这一点还是很智能的。

好了,有了枚举类型转换器后,你还需要在SpringBoot核心配置文件中稍作配置。

1、如果你使用的是专用枚举转换器,配置如下:

#这一行是固定配置,表示专用枚举转换器统一覆盖mybatis默认转换器
mybatis.configuration.type-handlers-package=mybatis.cn.handler
#这一行是动态配置,指定你的专用枚举转换器所在的包名,因为枚举类有多少个,专用转换器就有多少个,所有转换器统一放在一个包下显然更合适
mybatis.type-handlers-package=cn.itcampus.hander

2、如果你用的是统一枚举转换器,配置如下:

#直接指定统一枚举转换器的全限定类名即可
mybatis.configuration.default-enum-type-handler=cn.itcampus.hander.UniversalEnumHandler

好了,上述就是枚举类在后端的代码设计,可以实现数据库储存code值,后端读取code值转换为相应枚举类的效果。

但是,如果你直接把枚举类转换为JSON对象反馈给前端,你会发现显示的是枚举类属性的英文名,即性别枚举的MALE或FEMALE。

那么,怎样把枚举类的name值反馈给前端呢,即反馈枚举属性的中文名。这其实很简单,只需要在所有枚举类上加上一个注解即可,如下:

@JSONType(serializeEnumAsJavaBean = true)
public enum Sex implements BaseEnum<Sex, Integer> {}

好了,以上就完成了枚举类从数据库code值到后端枚举类,再到前端name值的传输过程,在JSON数据中,code值和name值会以在同一个sex对象属性的方式存在,你可以同时获取它们的值,并选择性使用,非常方便。

最后,说一下前端如何使用code值传递给后端并转换为相应的枚举类。很多人只注意到后端的枚举类怎么显示到前端,却没有注意到前端的枚举参数怎么传输到后端生成枚举类对象。

1、传输枚举类属性英文名,即性别枚举的MALE或FEMALE。后端接到参数后可以自动转换为Sex枚举类对象。

2、传输枚举类属性序列号,对你没听错,是序列号,不是code值,序列号是以0开始的,而code值在枚举类中一般定义从1开始。以性别枚举类为例,当前端传值为0,转换为的枚举属性是MALE,当传值为1,转换为的枚举属性是FEMALE,当传值为2,后端识别错误。



前端例子

其实两种方式都有使用的理由,看你习惯用哪种。接下来举例两种方式在前端使用下拉框时的使用。

1、使用枚举类属性英文名

<select name="sex">
	<option value="">选择性别</option>
	<option value="MALE"></option>
	<option value="FEMALE"></option>
</select>

2、使用枚举类属性序列号

<select name="sex">
	<option value="">选择性别</option>
	<option value="0"></option>
	<option value="1"></option>
</select>



版权声明:本文为weixin_45155235原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。