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)原生支持枚举类型,但这类实现方式通常与具体数据库绑定,不在本文讨论范围内。