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

时间:2022-06-01 12:59:49

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

  1. 以整型存储,这个一般是按枚举定义的次序排列,即枚举的ordinal
  2. 存储为枚举的名称
  3. 自定义存储枚举的内容,如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中添加:

  1. @PostLoad注释的方法toPriorityTransient(),JPA加载实体时,用于把数据库的值映射为枚举类型。
  2. @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的方法要优雅,并且能实现自定义的灵活的转换。

四、总结

总结下:

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