Persisting Enums in JPA – 在JPA中持久化枚举

最后修改: 2019年 5月 20日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In JPA version 2.0 and below, there’s no convenient way to map Enum values to a database column. Each option has its limitations and drawbacks. These issues can be avoided by using JPA 2.1 features.

在JPA 2.0及以下版本中,没有方便的方法可以将Enum值映射到数据库列。每个选项都有其局限性和缺点。这些问题可以通过使用JPA 2.1的功能来避免。

In this tutorial, we’ll take a look at the different possibilities we have to persist enums in a database using JPA. We’ll also describe their advantages and disadvantages as well as provide simple code examples.

在本教程中,我们将看一下使用JPA在数据库中持久化枚举的不同可能性。我们还将描述它们的优点和缺点,并提供简单的代码示例。

2. Using @Enumerated Annotation

2.使用@Enumerated注释

The most common option to map an enum value to and from its database representation in JPA before 2.1 is to use the @Enumerated annotation. This way, we can instruct a JPA provider to convert an enum to its ordinal or String value.

在2.1之前的JPA中,将一个枚举值映射到其数据库表示方法的最常见选项是使用@Enumerated注解。这样,我们可以指示JPA提供者将一个枚举转换为其序数或String值。

We’ll explore both options in this section.

我们将在本节中探讨这两种选择。

But let’s first create a simple @Entity that we’ll be using throughout this tutorial:

但让我们首先创建一个简单的@Entity,我们将在本教程中一直使用它。

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

    private String title;

    // standard constructors, getters and setters
}

2.1. Mapping Ordinal Value

2.1.顺序值的映射

If we put the @Enumerated(EnumType.ORDINAL) annotation on the enum field, JPA will use the Enum.ordinal() value when persisting a given entity in the database.

如果我们将@Enumerated(EnumType.ORDINAL)注解放在枚举字段上,JPA将在数据库中持久化给定实体时使用Enum.ordinal()值。

Let’s introduce the first enum:

让我们来介绍第一个枚举。

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

Next, let’s add it to the Article class and annotate it with @Enumerated(EnumType.ORDINAL):

接下来,让我们把它添加到Article类中,并用@Enumerated(EnumType.ORDINAL)来注释它。

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

    private String title;

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

Now when persisting an Article entity:

现在,当持久化一个Article实体时。

Article article = new Article();
article.setId(1);
article.setTitle("ordinal title");
article.setStatus(Status.OPEN);

JPA will trigger the following SQL statement:

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]

A problem arises with this kind of mapping when we need to modify our enum. If we add a new value in the middle or rearrange the enum’s order, we’ll break the existing data model.

当我们需要修改我们的枚举时,这种映射就会出现问题。如果我们在中间添加一个新的值或者重新排列枚举的顺序,我们将破坏现有的数据模型。

Such issues might be hard to catch as well as problematic to fix since we would have to update all the database records.

这种问题可能很难被发现,也很难解决,因为我们必须更新所有的数据库记录。

2.2. Mapping String Value

2.2.映射字符串值

Analogously, JPA will use the Enum.name() value when storing an entity if we annotate the enum field with @Enumerated(EnumType.STRING).

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

Let’s create the second enum:

我们来创建第二个枚举。

public enum Type {
    INTERNAL, EXTERNAL;
}

And let’s add it to our Article class and annotate it with @Enumerated(EnumType.STRING):

让我们把它添加到我们的Article类中,并用@Enumerated(EnumType.STRING)来注释它。

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

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;

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

Now when persisting an Article entity:

现在,当持久化一个Article实体时。

Article article = new Article();
article.setId(2);
article.setTitle("string title");
article.setType(Type.EXTERNAL);

JPA will execute the following SQL statement:

JPA将执行以下SQL语句。

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

With @Enumerated(EnumType.STRING), we can safely add new enum values or change our enum’s order. However, renaming an enum value will still break the database data.

通过@Enumerated(EnumType.STRING),我们可以安全地添加新的枚举值或改变枚举的顺序。然而,重命名一个枚举值仍然会破坏数据库数据。

Additionally, even though this data representation is far more readable compared to the @Enumerated(EnumType.ORDINAL) option, it also consumes a lot more space than necessary. This might turn out to be a significant issue when we need to deal with a high volume of data.

此外,尽管与@Enumerated(EnumType.ORDINAL)选项相比,这种数据表示法的可读性要高得多,但它也消耗了很多不必要的空间。当我们需要处理大量的数据时,这可能会成为一个重要的问题。

3. Using @PostLoad and @PrePersist Annotations

3.使用@PostLoad@PrePersist注释

Another option we have to deal with persisting enums in a database is to use standard JPA callback methods. We can map our enums back and forth in the @PostLoad and @PrePersist events.

我们处理数据库中持久化枚举的另一个选择是使用标准的JPA回调方法。我们可以在@PostLoad@PrePersist事件中来回映射我们的枚举。

The idea is to have two attributes in an entity. The first one is mapped to a database value, and the second one is a @Transient field that holds a real enum value. The transient attribute is then used by the business logic code.

这个想法是在一个实体中拥有两个属性。第一个被映射到数据库值,第二个是一个@Transient字段,它持有一个真实的枚举值。然后业务逻辑代码会使用这个暂存属性。

To better understand the concept, let’s create a new enum and use its int value in the mapping logic:

为了更好地理解这个概念,让我们创建一个新的枚举并在映射逻辑中使用其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);
    }
}

We’ve also added the Priority.of() method to make it easy to get a Priority instance based on its int value.

我们还添加了Priority.of()方法,以使我们能够轻松地根据int值获得Priority实例。

Now, to use it in our Article class, we need to add two attributes and implement callback methods:

现在,为了在我们的Article类中使用它,我们需要添加两个属性并实现回调方法。

@Entity
public class Article {

    @Id
    private int id;

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;

    @Enumerated(EnumType.STRING)
    private Type type;

    @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();
        }
    }
}

Now when persisting an Article entity:

现在,当持久化一个Article实体时。

Article article = new Article();
article.setId(3);
article.setTitle("callback title");
article.setPriority(Priority.HIGH);

JPA will trigger the following SQL query:

JPA将触发以下SQL查询。

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

Even though this option gives us more flexibility in choosing the database value’s representation compared to previously described solutions, it’s not ideal. It just doesn’t feel right to have two attributes representing a single enum in the entity. Additionally, if we use this type of mapping, we aren’t able to use enum’s value in JPQL queries. 

尽管与之前描述的解决方案相比,这个选项让我们在选择数据库值的表示方式上有更大的灵活性,但它并不理想。在实体中用两个属性代表一个枚举,感觉不对。另外,如果我们使用这种类型的映射,我们就不能在JPQL查询中使用枚举的值。

4. Using JPA 2.1 @Converter Annotation

4.使用JPA 2.1 @Converter注释

To overcome the limitations of the solutions shown above, the JPA 2.1 release introduced a new standardized API that can be used to convert an entity attribute to a database value and vice versa. All we need to do is to create a new class that implements javax.persistence.AttributeConverter and annotate it with @Converter.

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

Let’s see a practical example.

让我们看看一个实际的例子。

First, we’ll create a new enum:

首先,我们将创建一个新的枚举。

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

    private String code;

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

    public String getCode() {
        return code;
    }
}

We also need to add it to the Article class:

我们还需要将其添加到Article类中。

@Entity
public class Article {

    @Id
    private int id;

    private String title;

    @Enumerated(EnumType.ORDINAL)
    private Status status;

    @Enumerated(EnumType.STRING)
    private Type type;

    @Basic
    private int priorityValue;

    @Transient
    private Priority priority;

    private Category category;
}

Now let’s create a new CategoryConverter:

现在让我们创建一个新的CategoryConverter

@Converter(autoApply = true)
public class CategoryConverter implements AttributeConverter<Category, String> {
 
    @Override
    public String convertToDatabaseColumn(Category category) {
        if (category == null) {
            return null;
        }
        return 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);
    }
}

We’ve set the @Converter‘s value of autoApply to true so that JPA will automatically apply the conversion logic to all mapped attributes of a Category type. Otherwise, we’d have to put the @Converter annotation directly on the entity’s field.

我们将@ConverterautoApply值设置为true,这样JPA就会自动将转换逻辑应用于Category类型的所有映射属性。否则,我们就必须将@Converter注解直接放在实体的字段上。

Let’s now persist an Article entity:

现在让我们坚持一个Article实体。

Article article = new Article();
article.setId(4);
article.setTitle("converted title");
article.setCategory(Category.MUSIC);

Then JPA will execute the following SQL statement:

然后JPA将执行以下SQL语句。

insert 
into
    Article
    (category, priorityValue, status, title, type, id) 
values
    (?, ?, ?, ?, ?, ?)
Converted value on binding : MUSIC -> M
binding parameter [1] as [VARCHAR] - [M]
binding parameter [2] as [INTEGER] - [0]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [converted title]
binding parameter [5] as [VARCHAR] - [null]
binding parameter [6] as [INTEGER] - [4]

As we can see, we can simply set our own rules of converting enums to a corresponding database value if we use the AttributeConverter interface. Moreover, we can safely add new enum values or change the existing ones without breaking the already persisted data.

正如我们所见,如果我们使用AttributeConverter接口,我们可以简单地设置自己的规则,将枚举转换为相应的数据库值。此外,我们可以安全地添加新的枚举值或改变现有的枚举值,而不会破坏已经持续存在的数据。

The overall solution is simple to implement and addresses all the drawbacks of the options presented in the earlier sections.

整个解决方案实施起来很简单,并解决了前面几节中提出的所有选项的缺点。

5. Using Enums in JPQL

5.在JPQL中使用枚举

Let’s now see how easy it is to use enums in the JPQL queries.

现在让我们看看在JPQL查询中使用枚举是多么容易。

To find all Article entities with Category.SPORT category, we need to execute the following statement:

为了找到所有具有Category.SPORT类别的Article实体,我们需要执行以下语句。

String jpql = "select a from Article a where a.category = com.baeldung.jpa.enums.Category.SPORT";

List<Article> articles = em.createQuery(jpql, Article.class).getResultList();

It’s important to note that we need to use a fully qualified enum name in this case.

需要注意的是,在这种情况下,我们需要使用完全合格的枚举名称。

Of course, we’re not limited to static queries.

当然,我们并不局限于静态查询。

It’s perfectly legal to use the named parameters:

使用命名参数是完全合法的。

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

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

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

This example presents a very convenient way to form dynamic queries.

这个例子提出了一个非常方便的方法来形成动态查询。

Additionally, we don’t need to use fully qualified names.

此外,我们不需要使用完全合格的名称。

6. Conclusion

6.结语

In this article, we covered various ways of persisting enum values in a database. We presented our options when using JPA in version 2.0 and below as well as a new API available in JPA 2.1 and above.

在这篇文章中,我们介绍了在数据库中持久化枚举值的各种方法。我们介绍了在2.0及以下版本中使用JPA时的选择,以及在JPA 2.1及以上版本中可用的新API。

It’s worth noting that these aren’t the only possibilities to deal with enums in JPA. Some databases, like PostgreSQL, provide a dedicated column type to store enum values. However, such solutions are outside the scope of this article.

值得注意的是,这些并不是在JPA中处理枚举的唯一可能性。一些数据库,如PostgreSQL,提供了一个专门的列类型来存储枚举值。然而,这样的解决方案不在本文的讨论范围之内。

As a rule of thumb, we should always use the AttributeConverter interface and @Converter annotation if we’re using JPA 2.1 or later.

作为一条经验法则,如果我们使用JPA 2.1或更高版本,我们应该始终使用AttributeConverter接口和@Converter注释。

As usual, all the code examples are available over on our GitHub repository.

像往常一样,所有的代码实例都可以在我们的GitHub仓库上找到。