对于实体类中字段类型为 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;
}
}
注意到 ProcessType type 类型数据库中存储的是枚举类的字面值,不是 name 值。这还不是要命的,如果查询条件中有枚举类型的映射对应关系可就不是这样了。
再举一个昨天开发遇到的实际问题,一个自评表,数据库中有 int 类型的 level 字段表示自评等级,实体类中该字段类型为枚举。
自评表实体类
@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;
}
}
}
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;
}
}
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);
}
这么着写,插入、查询都有问题
注意到上面新增自评表的 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
现在就需要一个类来把实现这个借口,把 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,插入也没问题了
上面那条记录是之前没有使用属性转换器时插入错误的情况
写个博客其实不容易,总想把问题表述清楚,而在这个过程中又遇到了种种问题,坚持下去,希望 2018 会有所进步!