在老式的设计中往往将数据表的代码设计为外键,与代码表(即数据字典、枚举)的主键关联。这样有助于防止在数据库中插入(或更新)非法的代码值。但现在这种设计方法已经被抛弃——在《Java开发手册(嵩山版)》的数据库SQL语句部分,第6条是一个强制规定不得使用外键与级联,一切外键概念必须在应用层解决。其原因在于外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

在后端的应用层处理代码值的最佳办法就是应用枚举类型。它可以在极大提高代码可读性的同时防止插入(或更新)非法代码值。

3.1 定义枚举类型

在创建用户表的时候设计了一个性别字段:

  1. gender tinyint default 2 not null comment '性别'

现在我们为他创建一个枚举类型(我们可以把所有和定义枚举类型相关的代码都放到一个Java的Package把发布成一个Jar文件,这样可以在多个项目中复用):

  1. package com.longser.utils.enums;
  2. import com.fasterxml.jackson.annotation.JsonCreator;
  3. import com.fasterxml.jackson.annotation.JsonValue;
  4. /**
  5. * @author David
  6. */
  7. public enum Gender {
  8. //无关或未填
  9. NONE(0),
  10. //性别男,代码值 1
  11. MALE(1),
  12. //性别女,代码值 2
  13. FEMALE(2),
  14. ;
  15. private final int code;
  16. Gender(int code) {
  17. this.code = code;
  18. }
  19. @JsonValue
  20. public int getCode() {
  21. return this.code;
  22. }
  23. @JsonCreator
  24. public static Gender fromCode(int code) {
  25. Gender[] values = Gender.values();
  26. for (int i = 0; i < values.length; i++) {
  27. if (values[i].code == code) {
  28. return values[i];
  29. }
  30. }
  31. return null;
  32. }
  33. }

对于上面的代码不做过多解释,具体请自行阅读。需要说明的是,枚举类不能继承自其他类,因此尽管不同枚举类定义中用相近的代码,但不能通过继承统一的基类来简化。

3.2 定义枚举处理器

MyBatis 从一开始就自带了两个用于枚举的类型处理器 EnumTypeHandlerEnumOrdinalTypeHandler,这两个枚举类型处理器可以用于最简单情况下的枚举类型:

  • EnumTypeHandler
    这个类型处理器是 MyBatis 中枚举类型的默认处理器,它的作用是将枚举的名字和枚举类型对应起来。对于 Gender 枚举来说,存数据库时会使用 “MALE” 或者 “FEMALE” 字符串存储,从数据库取值时,会将字符串转换为对应的枚举。这显然是不能接受的
  • EnumOrdinalTypeHandler
    这是另一个枚举类型处理器,它的作用是将枚举的索引和枚举类型对应起来。对于 Gender 枚举来说,存数据库时会使用枚举对应的索引值1(MALE) 或者 索引值2(FEMALE) 存储,从数据库取值时,会将整型索引值转换为对应的枚举。

显然,默认的EnumTypeHandler处理器不符合我们的要求,因为通常标准的数据库设计都是存储枚举对应的索引而不是字符串名称,所以在mybatis-config.xml需要按照下面的方式进行配置:

  1. <typeHandlers>
  2. <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
  3. javaType="com.longser.utils.enums.Gender"/>
  4. </typeHandlers>

这个配置告诉 MyBatis : 枚举类 Gender 要使用 EnumOrdinalTypeHandler 来处理。

<typeHandler>还有有一个属性jdbcType,但除非 javaType 对应的多个 jdbcType 使用不同的类型处理器,否则不要设置 **jdbcType**。或者在配置 jdbcType 的同时,通过额外增加一个不配置jdbcType的来作为默认的处理器。

3.3 在注解映射中应用枚举类型

3.3.1 修改实体类定义

增加类属性 gender

  1. package com.longser.union.cloud.data.model;
  2. +import com.longser.utils.enums.Gender;
  3. public class UserEntry {
  4. private Long id;
  5. private String userName;
  6. private String nickName;
  7. private String mobile;
  8. private String password;
  9. + private Gender gender;

增加3个方法

  1. public UserEntry(String userName, String mobile, Gender gender) {
  2. super();
  3. this.userName = userName;
  4. this.mobile = mobile;
  5. this.gender = gender;
  6. }
  7. public Gender getGender() {
  8. return gender;
  9. }
  10. public void setGender(Gender gender) {
  11. this.gender = gender;
  12. }

把 toString() 方法改成如下内容

  1. public String toString() {
  2. return "userName " + this.userName + ", nickName " + this.nickName + ", mobile " + this.mobile + ", gender " + this.gender;
  3. }

3.3.2 在结果中应用枚举

在结果中应用枚举的时候,主要是要做好结果集映射。

给 UserMapper 中的 getAll() 方法的结果定义中增加性别属性:

  1. @Select("SELECT * FROM user")
  2. @Results({
  3. @Result(property = "mobile", column = "mobile"),
  4. @Result(property = "userName", column = "user_name"),
  5. @Result(property = "nickName", column = "nick_name"),
  6. + @Result(property = "gender", column = "gender",
  7. + javaType = Gender.class),
  8. })
  9. List<UserEntryUser> getAll();

在@Resule的参数中,可以通过显式地定义类型处理器来覆盖默认的设置,如 typeHandler = EnumOrdinalTypeHandler.class

运行测试方法
image.png
结果输出为FEMALE
image.png

3.3.3 在数据插入中应用枚举

在插入的时候使用枚举也很简单,可以直接使用。

首先为Mapper类定义一个新的方法:

  1. @Insert("INSERT INTO user(user_name, mobile, gender) VALUES(#{userName}, #{mobile}, #{gender})")
  2. void insert3(UserEntry userEntry);

然后定义一个新的测试用方法。
在传入枚举参数的时候,下面的代码实用了两种方法:

  • 直接指定枚举值,即Gender.MALE和Gender.FEMALE
  • 根据索引值获得枚举值,即Gender.fromCode(1)

    1. @Test
    2. public void testInsert3() {
    3. userMapper.insert3(new UserEntry("aa1", "13701111234", Gender.MALE));
    4. userMapper.insert3(new UserEntry("bb1", "18601234567", Gender.fromCode(1)));
    5. userMapper.insert3(new UserEntry("cc1", "18801885678", Gender.FEMALE));
    6. assertEquals(4, userMapper.getAll().size());
    7. List<UserEntry> users = userMapper.getAll();
    8. for (UserEntry current : users) {
    9. String s = current.toString();
    10. System.out.println(current.getId() + ": " + s);
    11. if (current.getId() > 1) {
    12. userMapper.deleteById(current.getId());
    13. }
    14. }
    15. assertEquals(1, userMapper.getAll().size());
    16. }

    下面是运行测试的结果
    image.png

    3.4 在XML映射中应用枚举类型

    在XML映射中应用枚举类型也很简单,下面我们只展示配置和代码,不再给出测试运行结果。

    3.4.1 在结果中应用枚举

    同样的,只需要修改结果集映射:

    1. <resultMap id="BaseResultMap" type="com.longser.union.cloud.data.model.UserEntry" >
    2. <id column="id" property="id" jdbcType="BIGINT" />
    3. <result column="user_name" property="userName" javaType="string" />
    4. <result column="nick_name" property="nickName" javaType="string" />
    5. <result column="mobile" property="mobile" javaType="string" />
    6. <result column="gender" property="gender" javaType="com.longser.utils.enums.Gender" />
    7. </resultMap>
    8. <sql id="Base_Column_List" >
    9. id, user_name, nick_name, mobile, gender
    10. </sql>

    3.4.2 在数据插入中应用枚举

    在XML映射文件中增加定义:

    1. <insert id="insert3" useGeneratedKeys="true" keyProperty="id"
    2. parameterType="com.longser.union.cloud.data.model.UserEntry" >
    3. INSERT INTO
    4. user
    5. (user_name,mobile,gender)
    6. VALUES
    7. (#{userName}, #{mobile}, #{gender})
    8. </insert>

    在Mapper接口类中增加方法:

    1. Integer insert3(UserEntry userEntry);

    在单元测试中写对应的测试方法:

    1. @Test
    2. public void testInsert3() {
    3. userMapperXml.insert3(new UserEntry("aa1", "13701111234", Gender.MALE));
    4. userMapperXml.insert3(new UserEntry("bb1", "18601234567", Gender.fromCode(1)));
    5. userMapperXml.insert3(new UserEntry("cc1", "18801885678", Gender.FEMALE));
    6. assertEquals(4, userMapperXml.getAll().size());
    7. List<UserEntry> users = userMapperXml.getAll();
    8. for (UserEntry current : users) {
    9. String s = current.toString();
    10. System.out.println(current.getId() + ": " + s);
    11. if (current.getId() > 1) {
    12. userMapperXml.deleteById(current.getId());
    13. }
    14. }
    15. assertEquals(1, userMapperXml.getAll().size());
    16. }

    本节的代码逻辑和上一节完全一样,这里就不做过多的解释,请自行修改后查看测试结果。

    3.5 一次配置多个枚举类

    3.5.1 问题的提出

    在2.4.1的数据表定义中,我们还定义了一个应该用枚举类型来表示的数字字段,最高学位:

    1. degree tinyint default 0 null comment '最高学位'

    毫无疑问,我应该是为它也设计一个枚举类(如com.longser.utils.enums.Degree)。使用的时候,可以如3.2中那样,为这个新的枚举类也配置一个处理器定义

    1. <typeHandlers>
    2. <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
    3. javaType="com.longser.utils.enums.Degree"/>
    4. </typeHandlers>

    很显然,如果一个应用系统中有很多的枚举类,这样做起来就太繁琐了,而且大大降低了可维护性。在当前版本的MyBatis中,我们其实有多种手段来尽可能的减少枚举需要的配置。这里我仅讨论如何自定义统一的枚举类型处理器。

    3.5.1 增加新的枚举类

    在继续讨论之前,我们先增加一个新的枚举类 ```java package com.longser.utils.enums;

import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue;

/**

  • @author David */ public enum Degree { //无 NONE(0), //学士 BACHELOR(1), //硕士 MASTER(2), //博士 DOCTOR(3), ;

    private final int code;

    Degree(int code) {

    1. this.code = code;

    }

    @JsonValue public int getCode() {

    1. return this.code;

    }

    @JsonCreator public static Degree fromCode(int code) {

    1. Degree[] values = Degree.values();
    2. for (int i = 0; i < values.length; i++) {
    3. if (values[i].code == code) {
    4. return values[i];
    5. }
    6. }
    7. return null;

    } } ```

    3.5.2 自定义统一的枚举类型处理器

MyBatis允许我们同时定义多个自己的枚举类型处理(尽管多数时候一个就够了)。然后我们把自己所有的类型处理器都放在同一个包名下(可以在不同 jar 包),然后通过配置告知MyBatis。

下面是我们定义的统一的枚举类型处理器:

  1. package com.longser.utils.enums;
  2. import org.apache.ibatis.type.EnumOrdinalTypeHandler;
  3. import org.apache.ibatis.type.MappedTypes;
  4. @MappedTypes({Gender.class, Degree.class})
  5. public class TypeHandler<E extends Enum<E>> extends EnumOrdinalTypeHandler<E> {
  6. public TypeHandler(Class<E> type) {
  7. super(type);
  8. }
  9. }

如代码所示,为了能在类上增加 @MappedTypes注解,这里简单继承了 EnumOrdinalTypeHandler 类,并且多个多个枚举可以一起配置到 @MappedTypes 注解中。

3.5.3 定义类型处理器包路径

在 mybatis-config.xml 按如下配置:

  1. - <typeHandlers>
  2. - <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
  3. - javaType="com.longser.utils.enums.Gender"/>
  4. - </typeHandlers>
  5. + <typeHandlers>
  6. + <package name="com.longser.utils.enums" />
  7. + </typeHandlers>

新配置可以让 MyBatis 固定扫描这个包,然后处理其中符合要求的类型处理器。

3.5.4 应用新的的枚举类

接下来的应用和之前的应用没有任何区别。下面我仅以注解方法中的查询为例。

为实体类增加属性:

  1. public class UserEntry {
  2. private Long id;
  3. private String userName;
  4. private String nickName;
  5. private String mobile;
  6. private Gender gender;
  7. + private Degree degree;

修改 toString() 方法

  1. public String toString() {
  2. return "userName " + this.userName + ", nickName " + this.nickName +
  3. ", mobile " + this.mobile + ", gender " + this.gender
  4. + ", degree " + this.degree;
  5. }

在结果集映射中增加内容

  1. @Select("SELECT * FROM user")
  2. @Results({
  3. @Result(property = "mobile", column = "mobile"),
  4. @Result(property = "userName", column = "user_name"),
  5. @Result(property = "nickName", column = "nick_name"),
  6. @Result(property = "gender", column = "gender",
  7. javaType = Gender.class),
  8. + @Result(property = "degree", column = "degree",
  9. + javaType = Degree.class),

直接运行测试
image.png
把数据库中的id为1的数据的degree字段值改为1,再次运行查询测试:
image.png

3.6 为枚举类型定义全局转换器

前文讨论的各种关于枚举类型的应用和转换都是在后端代码和数据库存储之间发生的。当后端代码接收从前端传递过来的数据时,我们同样希望“男性”用代码1而不是字符串MALE来表示,但默认的情况下,前端传过来的1不会自动转换成Gender.MALE,所以我们需要定义一个专门用于枚举类型转换的全局转换器。

3.6.1 定义枚举公共接口

定义如下的公共接口

  1. package com.longser.utils.enums;
  2. public interface BaseEnum {
  3. Integer getCode();
  4. }

3.6.2 枚举类型实现接口

修改枚举类型的定义

  1. -public enum Gender {
  2. +public enum Gender implements BaseEnum{
  1. -public enum Degree {
  2. +public enum Degree implements BaseEnum{
  1. + @Override
  2. @JsonValue
  3. - public int getCode() {
  4. + public Integer getCode() {
  5. return this.code;
  6. }

3.6.3 定义2个公共转换器

数字到枚举类型的转换器

  1. package com.longser.utils.enums.converter;
  2. import com.longser.utils.enums.BaseEnum;
  3. import org.springframework.core.convert.converter.Converter;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. import java.util.Objects;
  7. public class IntegerToEnumConverter<T extends BaseEnum> implements Converter<Integer, T> {
  8. private final Map<Integer, T> enumMap = new HashMap<>();
  9. public IntegerToEnumConverter(Class<T> enumType) {
  10. T[] enums = enumType.getEnumConstants();
  11. for (T e : enums) {
  12. enumMap.put(e.getCode(), e);
  13. }
  14. }
  15. @Override
  16. public T convert(Integer source) {
  17. T t = enumMap.get(source);
  18. if (Objects.isNull(t)) {
  19. throw new IllegalArgumentException("无法匹配对应的枚举类型");
  20. }
  21. return t;
  22. }
  23. }

字符串(一般是用字符串形式表示的数字)到枚举类型的转换器

  1. package com.longser.utils.enums.converter;
  2. import com.longser.utils.enums.BaseEnum;
  3. import org.springframework.core.convert.converter.Converter;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. import java.util.Objects;
  7. public class StringToEnumConverter<T extends BaseEnum> implements Converter<String, T> {
  8. private final Map<String, T> enumMap = new HashMap<>();
  9. public StringToEnumConverter(Class<T> enumType) {
  10. T[] enums = enumType.getEnumConstants();
  11. for (T e : enums) {
  12. enumMap.put(e.getCode().toString(), e);
  13. }
  14. }
  15. @Override
  16. public T convert(String source) {
  17. T t = enumMap.get(source);
  18. if (Objects.isNull(t)) {
  19. throw new IllegalArgumentException("无法匹配对应的枚举类型");
  20. }
  21. return t;
  22. }
  23. }

3.6.4 定义2个转换器工厂

下面是前文两个转换器对应的工厂

  1. package com.longser.utils.enums.converter;
  2. import com.longser.utils.enums.BaseEnum;
  3. import org.springframework.core.convert.converter.Converter;
  4. import org.springframework.core.convert.converter.ConverterFactory;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. @SuppressWarnings("unchecked")
  8. public class IntegerCodeToEnumConverterFactory implements ConverterFactory<Integer, BaseEnum> {
  9. private static final Map<Class<? extends BaseEnum>, Converter<Integer, ? extends BaseEnum>> CONVERTERS = new HashMap<>();
  10. /**
  11. * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现
  12. *
  13. * @param targetType 转换后的类型
  14. * @return 返回一个转化器
  15. */
  16. @Override
  17. public <T extends BaseEnum> Converter<Integer, T> getConverter(Class<T> targetType) {
  18. Converter<Integer, T> converter = (Converter<Integer, T>)CONVERTERS.get(targetType);
  19. if (converter == null) {
  20. converter = new IntegerToEnumConverter<>(targetType);
  21. CONVERTERS.put(targetType, converter);
  22. }
  23. return converter;
  24. }
  25. }
  1. package com.longser.utils.enums.converter;
  2. import com.longser.utils.enums.BaseEnum;
  3. import org.springframework.core.convert.converter.Converter;
  4. import org.springframework.core.convert.converter.ConverterFactory;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. @SuppressWarnings("unchecked")
  8. public class StringCodeToEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
  9. private static final Map<Class<? extends BaseEnum>, Converter<String, ? extends BaseEnum>> CONVERTERS = new HashMap<>();
  10. /**
  11. * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现
  12. *
  13. * @param targetType 转换后的类型
  14. * @return 返回一个转化器
  15. */
  16. @Override
  17. public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
  18. Converter<String, T> converter = (Converter<String, T>)CONVERTERS.get(targetType);
  19. if (converter == null) {
  20. converter = new StringToEnumConverter<>(targetType);
  21. CONVERTERS.put(targetType, converter);
  22. }
  23. return converter;
  24. }
  25. }

3.6.5 注册类型转换器工厂

如前文代码所说,全局类型转换器都定义在包com.longser.utils.enums之下,所以他们可以和各种枚举类型定义一起打包成Jar文件在公司或项目组范围内统一维护与使用。在实际使用的时候,只需要在使用了@Configuration注解的配置类中注册需要的转换器工厂即可。示例代码如下:

  1. package com.longser.union.cloud.config;
  2. import com.longser.utils.enums.converter.IntegerCodeToEnumConverterFactory;
  3. import com.longser.utils.enums.converter.StringCodeToEnumConverterFactory;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.format.FormatterRegistry;
  6. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  7. @Configuration
  8. public class WebMvcConfig implements WebMvcConfigurer {
  9. @Override
  10. public void addFormatters(FormatterRegistry registry) {
  11. registry.addConverterFactory(new IntegerCodeToEnumConverterFactory());
  12. registry.addConverterFactory(new StringCodeToEnumConverterFactory());
  13. }
  14. }

完成本节的所有工作,以后再增加新的枚举类,只要它实现了 BaseEnum 接口,就会被自动注册它的转换器。

3.6.7 测试转换器效果

现在执行写好的单元测试代码,你应该得到相同的结果。

3.7 总结

在后端开发中使用枚举类型会并不会增加很多的代码工作量,但大极大地提高了代码的可读性,能够大幅度地减少犯错的可能。尤其业务逻辑需要根据不同枚举值而执行不同的处理规则时,枚举类型的好处更为明显。此外,建立公司统一的枚举包也是项目开发标准化的一个主要组成部分。

除了自定义枚举处理器以外,MyBatis还支持自动定义类型处理器接口,能够进一步简化使用多个枚举类时的工作。

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。