JPA存储枚举类型的几种处理方式
存储枚举类型到数据库可以分为以下三种需求:
- 以整型存储,这个一般是按枚举定义的次序排列,即枚举的
ordinal
。 - 存储为枚举的名称
- 自定义存储枚举的内容,如code等等。
在JPA 2.0
以及之前的版本,一般使用的是@Enumerated
来转换枚举类型,支持按枚举的ordinal
和枚举名称来存储,不支持自定义枚举内存存储。
JPA2.1
引入Converter,就可以按需要定义转换枚举类型的存储。
一、使用@Enumerated注解
先讲解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;
}
缺点
以枚举名称存储在数据库需要注意的问题是:尽量不要修改枚举的名称,否则需要同步更新相关记录。
二、使用@PostLoad 和****@PrePersist 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中添加:
- @PostLoad注释的方法
toPriorityTransient()
,JPA加载实体时,用于把数据库的值映射为枚举类型。 - @PrePersist注释的方法
toPriorityPersistent()
,用于JPA保持实体时把priority的枚举类型转换为数值。
缺点
这样可以自定义保存的枚举值,但是缺点在代码上不是很优雅,一个字段需要有两个属性来表示。
三、使用JPA 2.1 @Converter注解
为了克服上述解决方案的局限性,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的方法要优雅,并且能实现自定义的灵活的转换。
四、总结
总结下:
- 如果只是简单的映射为枚举的ordinal或者名称,推荐使用@Enumerated注释。
- 要更灵活自定义的转换,推荐使用JPA2.1的@Converter
- 因为枚举的Ordinal映射,如果修改了枚举的次序,或者增删枚举值,可能会导致难以发现的问题,除非确定不会修改,否则不推荐使用。