Hibernate 枚举类型映射以及属性转化器的使用

枚举类型的映射

初步了解

对于实体类中字段类型为 Enum 的属性,当其映射为表的列时,需要指定映射的类型,数字 or 字符串

18/2/26 10:50 更:如果不做任何处理的话,从数据库中映射到枚举类,是映射为枚举类对象。也就是像 TRANSFER_GROUP、ONLINE_PROPOSAL 这样。

//@Enumerated 和 @Column 配合使用
// EnumType.STRING 映射成字符串,是把枚举的字面形式。如下面的 TRANSFER_GROUP(0, "调组申请"),映射到数据库是 TRANSFER_GROUP ,不是“调组申请”。如果想要映射为”调组申请“,就要做一个处理,下面会说到
@Column
@Enumerated(EnumType.STRING)
private ProcessType type;


// EnumType.ORDINAL 映射成数字
@Column
@Enumerated(EnumType.ORDINAL)
private ProcessStatus status;
public enum ProcessType implements AbstractEnum {

    TRANSFER_GROUP(0, "调组申请"),

    ONLINE_PROPOSAL(1,"在线提案");

    private int code;
    private String name;

    ProcessType(int code, String name) {
        this.code = code;
        this.name = name;
    }

    @Override
    public int getCode() {
        return code;
    }

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

    @Override
    public String getName() {
        return name;
    }

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


}
public enum ProcessStatus implements AbstractEnum {

    /**
     * 新创建
     */
    CREATE(0, "创建"),

    /**
     * 启动
     */
    START(1, "开始"),

    /**
     * 拒绝
     */
    REJECT(2, "拒绝"),

    /**
     * 结束
     */
    COMPLETE(3, "通过");

    private int code;

    private String name;

    ProcessStatus(int code, String name) {
        this.setCode(code);
        this.setName(name);
    }
    public static ProcessStatus valueOfEnum(String name) {
        ProcessStatus[] types = values();
        for (ProcessStatus type : types) {
            if (type.getName().equals(name)) {
                return type;
            }
        }
        return null;
    }

    @Override
    public int getCode() {
        return this.code;
    }

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

    @Override
    public String getName() {
        return this.name;
    }

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

数据库:
Hibernate 枚举类型映射以及属性转化器的使用_第1张图片

注意到 ProcessType type 类型数据库中存储的是枚举类的字面值,不是 name 值。这还不是要命的,如果查询条件中有枚举类型的映射对应关系可就不是这样了。

深入探讨

再举一个昨天开发遇到的实际问题,一个自评表,数据库中有 int 类型的 level 字段表示自评等级,实体类中该字段类型为枚举。

  1. 自评表实体类

    @Entity
    @Table(name = "s_self_assessment")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Accessors(chain = true)
    public class SelfAssessment {
    
    @Id
    private String id;
    
    private String promoteId;
    
    @Column
    @Enumerated(value = EnumType.ORDINAL)
    private LevelEnum level;
    
    /**
     * level 为 优/良 的自评要素的需要添加描述
     */
    private String description;
    
    /**
     * 此自评要素的分数,优良中差分别对应 7、5、3、1
     */
    private Integer score;
    
    @OneToOne(cascade = {CascadeType.ALL})
    @JoinColumn(name="assessment_factor_id")
    private AssessmentFactor assessmentFactor;
    
    /**
     * 根据 level 指定 score
     */
    public SelfAssessment(String promoteId,LevelEnum level,String description,String assessmentFactorId){
        this.id = IdHelper.nextStringValue();
        this.promoteId = promoteId;
        this.level = level;
        this.description = description;
        assessmentFactor = AssessmentFactor.builder().id(assessmentFactorId).build();
    
        switch (level){
            case LEVEL_EXCELLENT:
                score = 7;
                break;
            case LEVEL_GOOD:
                score = 5;
                break;
            case LEVEL_MIDDLE:
                score = 3;
                break;
            case LEVEL_BAD:
                score = 1;
                break;
        }
    
       }
    }
    
  2. LevelEnum 枚举

    public enum LevelEnum implements AbstractEnum{
       LEVEL_EXCELLENT(1,"优"),LEVEL_GOOD(2,"良"),LEVEL_MIDDLE(3,"中"),LEVEL_BAD(4,"差");
    
        private String name;
    
        private Integer code;
    
        LevelEnum(Integer code,String name){
            this.code = code;
            this.name = name;
        }
    
    
        @Override
        public int getCode() {
            return code;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        public static LevelEnum getLevelEnum(Integer x){
            LevelEnum[] values = values();
    
            for (int i = 0; i < values.length; i++) {
                if (values[i].code.equals(x)){
                    return values[i];
                }
            }
            return null;
        }
    }
    
  3. DAO 层接口

    public interface SelfAssessmentRepository extends JpaRepository<SelfAssessment,String>{
    
    @Query(value = "SELECT id,promote_id,assessment_factor_id,level,score,description FROM s_self_assessment WHERE level = ?1" ,nativeQuery = true)
    List<SelfAssessment> findAllByLevelNative(Integer level);
    
    List<SelfAssessment> findAllByLevel(LevelEnum levelEnum);
    }
    

这么着写,插入、查询都有问题

Hibernate 枚举类型映射以及属性转化器的使用_第2张图片

插入:
Hibernate 枚举类型映射以及属性转化器的使用_第3张图片

这里写图片描述
注意到上面新增自评表的 level 为优秀,数据库中应该存储 1 ,然而数据库中新增记录的 level 字段值却是 0。

查询也是同样的问题,如果查询 level 为优秀的,数据库 level 字段为优秀的记录对应到实体类却是 LEVEL_GOOD

出现这样错误的原因是啥呢?这个问题我几个小时都没有解决,无奈求助于老大——首席架构师。我也是鼓足了勇气,带我的架构师(很荣幸,是一个特别聪明幽默的架构师带我)还没来上班。老大就是老大,在我向人家把问题表述清楚之后,10s 不到的时间就指出了问题所在。特别佩服!其实我问他的不是这个问题,是一个类似的,但是我没有用下面说的方法去解决,而是抖了个机灵,惭愧惭愧~~

如果你和我一样是个新手的话,你好好仔细看看你们公司高开/架构写的枚举类,基本都会有两个属性,一个String 表明含义,一个 int 用于数据库存储。并且仔细看,枚举类中所包含的成员变量都是从 0 开始的。什么意思呢,看我上边 LevelEnum 枚举,LEVEL_EXCELLENT(1,"优"),LEVEL_GOOD(2,"良"),LEVEL_MIDDLE(3,"中"),LEVEL_BAD(4,"差");这个是从 1 开始的,我想让优良可差分别对应1234,如果我让其分别对应0123,想必就没有这篇文章了。这就是问题的关键。

如果你不做其他处理,任由 Hibernate 去映射枚举,且碰巧你打算对应的属性值不是从 0 开始的,那就会出现上面的错误。插入、查询中用到了枚举,Hibernate 不会聪明的按照你臆想的意思去自动给你映射。我这里就是想让优良可差分别对应1234,@Enumerated(EnumType.ORDINAL) 注解的意思是,把枚举类的所有属性从 0 开始依次排序,给你映射到DB中,反过来从DB映射到实体类时也是同样的道理。也就是,如果我插入 level 属性为 LEVEL_EXCELLENT ,Hibernate 会去看 LEVEL_EXCELLENT 在其所属枚举类属性中出现在第几位,第一位就是0,则数据库中插入0。取数据也是。

解决方案

其实在写这篇文章时,我就摸索出了第二个解决方法(注意到上面测试插入的代码中我注释掉了一段,使用这个插入就没有问题),后面仔细看看再写。这里先把第一个方法总结一下,我是参考 Hibernate中枚举Enum类型的映射策略 文章来的,这篇文章写得很好,找了很久才搜到。

AttributeConverter 属性转化器
A class that implements this interface can be used to convert entity attribute state into database column representation and back again.

实现 javax.persistence.AttributeConverter 接口的类可以被用于实体属性和数据库列类型之间相互转化

AttributeConverter 接口有两个方法(X是实体属性,Y是对应的数据库字段类型):

  1. public Y convertToDatabaseColumn (X attribute);用于将实体类转化为数据库字段时,增、改
  2. public X convertToEntityAttribute (Y dbData);将数据库字段转换为实体类属性,查

现在就需要一个类来把实现这个借口,把 LevelEnum 转化掉,注意这个类前要加 @Converter

@Converter
public class LevelEnumConverter implements AttributeConverter<LevelEnum,Integer> {


    /**
     * 转化到数据库,增加、更新时使用
     * @param attribute
     * @return
     */
    @Override
    public Integer convertToDatabaseColumn(LevelEnum attribute) {
        return attribute.getCode();
    }

    /**
     * 从数据库转化到实体类,查询使用
     * @param dbData
     * @return
     */
    @Override
    public LevelEnum convertToEntityAttribute(Integer dbData) {
        return LevelEnum.getLevelEnum(dbData);
    }

}

之前的自评表实体类 SelfAssessment 中,LevelEnum 上面也不能用 @Enumerated(value = EnumType.ORDINAL)注解了

    @Column
    //修改前 @Enumerated(value = EnumType.ORDINAL)
    @Convert(converter = LevelEnumConverter.class)
    private LevelEnum level;

如此进行,大功完成~~

现在再去执行以下查询和插入,结果如下:
之前查询结果是把数据库中 level 为 1 的字段给映射成 LEVEL_GOOD,插入也没问题了
Hibernate 枚举类型映射以及属性转化器的使用_第4张图片

上面那条记录是之前没有使用属性转换器时插入错误的情况

这里写图片描述

写个博客其实不容易,总想把问题表述清楚,而在这个过程中又遇到了种种问题,坚持下去,希望 2018 会有所进步!

你可能感兴趣的:(Spring,Cloud,与,SSM,框架)