枚举类在前后端以及数据库的应用
谈到枚举类,你一定知道一个优雅的设计,例如性别这一最常用的枚举类:
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>