1. 概述

在 JPA 2.0 及更早版本中,并没有一种便捷的方式来将 Java 枚举(Enum)值映射到数据库列中。每种实现方式都有其局限性和使用上的不便。这些问题在 JPA 2.1 中得到了改善,新增的 @Converter 注解提供了更灵活的解决方案。而在 JPA 3.1 中,并没有对枚举持久化部分引入重大变更。

本文将介绍在 JPA 中持久化枚举的各种方式,包括它们的优缺点以及使用示例代码。适用于有经验的 Java 开发者参考。

2. 使用 @Enumerated 注解

这是 JPA 2.1 之前最常用的映射方式,通过 @Enumerated 注解可以指定 JPA 使用枚举的序号(ordinal)或名称(name)作为数据库存储值。

我们以一个简单的实体类 Article 为例:

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

    private String title;

    // 标准构造函数、getter 和 setter
}

2.1. 映射序号值(Ordinal)

使用 @Enumerated(EnumType.ORDINAL),JPA 会将枚举值映射为其在枚举类中的序号(从 0 开始)。

定义枚举:

public enum Status {
    OPEN, REVIEW, APPROVED, REJECTED;
}

Article 类中使用:

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

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;
}

保存实体时,JPA 会生成类似如下 SQL:

insert into Article (status, title, id) values (?, ?, ?)
binding parameter [1] as [INTEGER] - [0]
binding parameter [2] as [VARCHAR] - ["ordinal title"]
binding parameter [3] as [INTEGER] - [1]

⚠️ 问题:如果修改枚举顺序(如插入新值),会导致数据库中已有数据含义错乱,存在数据一致性风险

2.2. 映射字符串值(String)

使用 @Enumerated(EnumType.STRING),JPA 会将枚举值映射为其名称(即 name() 方法返回值)。

定义枚举:

public enum Type {
    INTERNAL, EXTERNAL;
}

Article 类中使用:

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

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;

    @Enumerated(EnumType.STRING)
    private Type type;
}

保存实体时 SQL 示例:

insert into Article (status, title, type, id) values (?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [0]
binding parameter [2] as [VARCHAR] - ["string title"]
binding parameter [3] as [VARCHAR] - ["EXTERNAL"]
binding parameter [4] as [INTEGER] - [2]

优点:可以安全地在枚举中添加新值或调整顺序
缺点:若重命名枚举常量,数据库中旧数据将无法匹配,仍需迁移;且存储空间占用较大

3. 使用 @PostLoad@PrePersist 回调

另一种方式是通过 JPA 的生命周期回调方法手动控制枚举与数据库值的转换。

实现思路:

  • 使用一个持久化字段(如 int)来保存数据库值
  • 使用一个 @Transient 字段来保存枚举对象
  • @PostLoad 中将数据库值转为枚举
  • @PrePersist 中将枚举转为数据库值

定义枚举:

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

    private int priority;

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

    public int getPriority() {
        return priority;
    }

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

Article 中使用:

@Entity
public class Article {

    @Id
    private int id;

    private String title;

    @Basic
    private int priorityValue;

    @Transient
    private Priority priority;

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

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

保存实体时 SQL 示例:

insert into Article (priorityValue, title, id) values (?, ?, ?)
binding parameter [1] as [INTEGER] - [300]
binding parameter [2] as [VARCHAR] - ["callback title"]
binding parameter [3] as [INTEGER] - [3]

优点:可灵活定义映射逻辑
缺点:需要维护两个字段,结构不清晰;无法在 JPQL 查询中直接使用枚举值

4. 使用 JPA 2.1+ 的 @Converter 注解

这是目前最推荐的方式,尤其适用于 JPA 2.1 及以上版本。

通过实现 AttributeConverter 接口并配合 @Converter 注解,我们可以自定义任意枚举与数据库值之间的转换逻辑。

定义枚举:

public enum Category {
    SPORT("S"), MUSIC("M"), TECHNOLOGY("T");

    private String code;

    Category(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

Article 中使用:

@Entity
public class Article {

    @Id
    private int id;

    private String title;

    private Category category;
}

创建转换器类:

@Converter(autoApply = true)
public class CategoryConverter implements AttributeConverter<Category, String> {

    @Override
    public String convertToDatabaseColumn(Category category) {
        return category == null ? null : category.getCode();
    }

    @Override
    public Category convertToEntityAttribute(String code) {
        if (code == null) return null;

        return Stream.of(Category.values())
                     .filter(c -> c.getCode().equals(code))
                     .findFirst()
                     .orElseThrow(IllegalArgumentException::new);
    }
}

保存实体时 SQL 示例:

insert into Article (category, title, id) values (?, ?, ?)
binding parameter [1] as [VARCHAR] - ["M"]
binding parameter [2] as [VARCHAR] - ["converted title"]
binding parameter [3] as [INTEGER] - [4]

优点

  • 转换逻辑清晰可控
  • 不影响实体结构
  • 支持动态添加或修改枚举值
  • 可自动应用(autoApply)
  • 支持在 JPQL 中使用枚举

5. 在 JPQL 查询中使用枚举

使用 @Converter 后,可以直接在 JPQL 查询中使用枚举值。

查询示例:

String jpql = "select a from Article a where a.category = com.example.enums.Category.SPORT";
List<Article> articles = entityManager.createQuery(jpql, Article.class).getResultList();

注意:必须使用全限定类名

也可以使用参数化查询:

String jpql = "select a from Article a where a.category = :category";

TypedQuery<Article> query = entityManager.createQuery(jpql, Article.class);
query.setParameter("category", Category.TECHNOLOGY);

List<Article> articles = query.getResultList();

优点:支持动态查询,无需全限定名

6. 总结

方式 适用 JPA 版本 优点 缺点
@Enumerated(EnumType.ORDINAL) <= 2.0 简单易用 枚举顺序变更破坏数据一致性
@Enumerated(EnumType.STRING) <= 2.0 可安全增删 枚举重命名破坏数据一致性,存储空间大
@PostLoad + @PrePersist <= 2.0 自定义映射 实体结构复杂,无法用于 JPQL
@Converter + AttributeConverter >= 2.1 最推荐方案 仅限 JPA 2.1+

推荐做法如果使用 JPA 2.1 及以上版本,务必使用 @Converter 方式实现枚举持久化。它结构清晰、扩展性强、支持 JPQL 查询,是目前最优雅的解决方案。

📌 额外提示:某些数据库(如 PostgreSQL)原生支持枚举类型,但这类实现方式通常与具体数据库绑定,不在本文讨论范围内。


原始标题:Persisting Enums in JPA | Baeldung