JPA存储枚举类型的几种处理方式

百科知识2025-04-261

存储枚举类型到数据库可以分为以下三种需求:

  1. 以整型存储,这个一般是按枚举定义的次序排列,即枚举的ordinal。
  2. 存储为枚举的名称
  3. 自定义存储枚举的内容,如code等等。

在JPA 2.0以及之前的版本,一般使用的是@Enumerated来转换枚举类型,支持按枚举的ordinal和枚举名称来存储,不支持自定义枚举内存存储。

JPA2.1引入Converter,就可以按需要定义转换枚举类型的存储。

i>一、使用@Enumerated注解</i

先讲解JPA2.0以及之前版本的@Enumerated和@EnumType实现存储ordinal和枚举名称的方式。

后面以User为示例:

@Entity
public class User {
@Id
private int id;

private String name;

// 构造函数, getters和setters方法

}

映射枚举类型为ordinal值

将@Enumerated(EnumType.ORDINAL) 注释放在枚举字段上,JPA 将实体存储到数据库时,会使用 Enum.ordinal() 值。

没有添加@Enumerated,或者没有指定EnumType.ORDINAL,默认情况下,JPA也是使用Enum.ordinal() 值。

如给User添加Status枚举:

public enum Status {
NORMAL, DISABLED;
}

添加后如下:

@Entity
public class User{
@Id
private int id;

private String name;

@Enumerated(EnumType.ORDINAL)
private Status status;

}

存储枚举ordinal缺点

以枚举的ordinal存储,有一个很大的缺点就是:当需要修改枚举时,就会出现映射的问题。 如果在中间添加一个新值或重新排列枚举的顺序,将破坏现有的数据的ordinal值。 这些问题可能很难发现,也很难修复,必须更新所有数据库记录。

映射枚举类型为名称

使用 @Enumerated(EnumType.STRING) 注释枚举字段,JPA 将在存储实体时使用 Enum.name() 值。

给User添加Role枚举:

public enum Role {
NORMAL,VIP,ADMIN;
}

添加后User:

@Entity
public class User{
@Id
private int id;

private String name;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Role role;

}

缺点

以枚举名称存储在数据库需要注意的问题是:尽量不要修改枚举的名称,否则需要同步更新相关记录。

二、使用i>@PostLoad</ii>@PrePersist</i Annotations

另一个选择是使用JPA的回调方法。在@PostLoad 和@PrePersist 事件中来回映射的枚举。 这个做法需要在实体上定义两个属性。 一个映射到数据库值,另一个添加@Transient 字段为枚举值。业务逻辑代码使用transient枚举属性。 

如在User中添加Priority枚举属性,映射逻辑中使用它的int值:

public enum Priority {
LOW(100), MEDIUM(200), HIGH(300);

private int priority;

private Priority(int priority) {
    this.priority = priority;
}

public int getPriority() {
    return priority;
}

public static Priority of(int priority) {
    return Stream.of(Priority.values())
            .filter(p -> p.getPriority() == priority)
            .findFirst()
            .orElseThrow(IllegalArgumentException::new);
}

}

在User中使用如下:

@Entity
public class User {
@Id
private int id;

private String name;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Role role;

@Basic
private int priorityValue;

@Transient
private Priority priority;

@PostLoad
void toPriorityTransient() {
    if (priorityValue > 0) {
        this.priority = Priority.of(priorityValue);
    }
}

@PrePersist
void toPriorityPersistent() {
    if (priority != null) {
        this.priorityValue = priority.getPriority();
    }
}

}

User中添加:

  1. @PostLoad注释的方法toPriorityTransient(),JPA加载实体时,用于把数据库的值映射为枚举类型。
  2. @PrePersist注释的方法toPriorityPersistent(),用于JPA保持实体时把priority的枚举类型转换为数值。

缺点

这样可以自定义保存的枚举值,但是缺点在代码上不是很优雅,一个字段需要有两个属性来表示。

三、使用JPA 2.1 i>@Converter注解</i

为了克服上述解决方案的局限性,JPA 2.1 版本引入了一个新的API,可用于将实体属性转换为数据库值,反之亦然。 我们需要做的就是创建一个实现 javax.persistence.AttributeConverter 的新类,并用@Converter 对其进行注解。

当然@Converter不仅用于枚举类型的转换,还可以用于其他类型的转换,这里是以枚举为例。

这里以上面的priority枚举为例,创建Priority的转换器。

@Converter(autoApply = true)
public class PriorityConverter implements AttributeConverter<Priority, Integer> {

@Override
public Integer convertToDatabaseColumn(Priority priority) {
    if (priority == null) {
        return null;
    }
    return priority.getPriority();
}

@Override
public Priority convertToEntityAttribute(Integer priorityValue) {
    if (priorityValue == null) {
        return null;
    }

    return Priority.of(priorityValue);
}

}

修改后的User即为:

@Entity
public class User {
@Id
private int id;

private String name;

@Enumerated(EnumType.ORDINAL)
private Status status;

@Enumerated(EnumType.STRING)
private Role role;

@Convert(converter = PriorityConverter .class)
private Priority priority;

}

这样代码就相对使用@PostLoad和@PrePersist的方法要优雅,并且能实现自定义的灵活的转换。

四、总结

总结下:

  1. 如果只是简单的映射为枚举的ordinal或者名称,推荐使用@Enumerated注释。
  2. 要更灵活自定义的转换,推荐使用JPA2.1的@Converter
  3. 因为枚举的Ordinal映射,如果修改了枚举的次序,或者增删枚举值,可能会导致难以发现的问题,除非确定不会修改,否则不推荐使用。