Java 枚举、JPA 和 PostgreSQL 枚举

1、简介

本文将带你了解 Java 枚举、JPA 和 PostgreSQL 枚举的概念,以及如何将它们结合使用,在 Java 枚举和 PostgreSQL 枚举之间创建无缝映射。

2、Java 枚举

Java 枚举(Enum)是一种特殊类型的类,用于表示一组固定数量的常量。枚举用于定义一组具有底层类型(如字符串或整数)的命名值。当我们需要定义一组在应用中具有特定含义的命名值时,枚举非常有用。

下面是一个 Java 枚举的示例:

public enum OrderStatus {
    PENDING, IN_PROGRESS, COMPLETED, CANCELED
}

在本例中,OrderStatus 枚举定义了四个常量。这些常量可以在我们的应用中用来表示订单的状态。

3、使用 @Enumerated 注解

在 JPA 中使用 Java 枚举时,需要用 @Enumerated 来注解枚举字段,以指定如何将枚举值存储到数据库中。

首先,定义一个名为 CustomerOrder 的实体类,并用 @Entity 进行注解,以标记其用于 JPA 持久化:

@Entity
public class CustomerOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Enumerated() 
    private OrderStatus status;

    // 其他的字段和方法省略
}

默认情况下,JPA 将枚举值存储为整数(Integer),代表枚举常量的序号位置(ordinal())。例如,上文中的 OrderStatus 枚举值 PENDINGIN_PROGRESSCOMPLETEDCANCELED,默认行为将分别把它们存储为整数 0123。由此产生的数据库表将有一个小 int 类型的状态列,其值为 03

create table customer_order (
    id bigserial not null,
    status smallint check (status between 0 and 3),
    primary key (id)
);

但是,如果我们在 Java 代码中更改枚举常量的顺序,这种默认行为可能会导致问题。例如,如果我们调换 IN_PROGRESSPENDING 的顺序,数据库值仍将是 0123,但它们将不再与更新后的枚举顺序相匹配。这会导致应用出现不一致和错误。

为了避免这个问题,可以使用 EnumType.STRING 将枚举值以字符串形式存储在数据库中。这种方法可确保枚举值以人类可读的格式存储,而且可以更改枚举常量的顺序,而不会影响数据库值:

@Enumerated(EnumType.STRING)
private OrderStatus status;

@Enumerated(EnumType.STRING) 指示 JPA 在数据库中存储 OrderStatus 枚举值的字符串表示(如 "PENDING"),而不是顺序位置(整数索引)。生成的数据库表将有一个 varchar 类型的 status 列,该列可以保存枚举中定义的特定字符串值:

create table customer_order (
    id bigint not null,
    status varchar(16) check (status in ('PENDING','IN_PROGRESS', 'COMPLETED', 'CANCELLED')),
    primary key (id)
);

4、Java 枚举映射到 PostgreSQL 枚举

由于处理和功能上的差异,将 Java 枚举映射到 PostgreSQL 枚举可能有点棘手。即使使用 EnumType.STRING,JPA 仍然不知道如何将 Java 枚举映射到 PostgreSQL 枚举。

为了演示这个问题,创建一个 PostgreSQL 枚举类型:

CREATE TYPE order_status AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELED');

接着,创建一个使用 PostgreSQL 枚举类型的表:

CREATE TABLE customer_order (
    id BIGINT NOT NULL,
    status order_status,
    PRIMARY KEY (id)
);

我们更新了 status 列,使用 PostgreSQL 枚举类型 order_status。现在,尝试向表中插入一些数据:

CustomerOrder order = new CustomerOrder();
order.setStatus(OrderStatus.PENDING);
session.save(order);

尝试插入数据时,会出现异常:

org.hibernate.exception.SQLGrammarException: could not execute statement 
  [ERROR: column "status" is of type order_status but expression is of type character varying

出现 SQLGrammarException 是因为 JPA 不知道如何将 Java 枚举 OrderStatus 映射到 PostgreSQL 枚举 order_status

5、使用 @Type 注解

在 Hibernate 5 中,我们可以利用 Hypersistence Utils 库来处理这个问题。该库提供了更多类型,包括对 PostgreSQL 枚举的支持。

首先,需要在 pom.xml 中添加 Hypersistence Utils 依赖:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>3.7.0</version>
</dependency>

Hypersistence Utils 库包含一个 PostgreSQLEnumType 类,用于处理 Java 枚举和 PostgreSQL 枚举类型之间的转换。我们将使用该类作为自定义 Type Handler。

接下来,我们可以使用 @Type 来注解枚举字段,并在实体类中定义 Type Handler:

@Entity
@TypeDef(
    name = "pgsql_enum",
    typeClass = PostgreSQLEnumType.class
)
public class CustomerOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(columnDefinition = "order_status")
    @Type(type = "pgsql_enum")
    private OrderStatus status;

    // 其他字段和方法省略
}

通过如上步骤,我们就能在 Hibernate 5 中有效地将 PostgreSQL 枚举类型映射为 Java 枚举。

6、使用 PostgreSQLEnumJdbcType

在 Hibernate 6 中,我们可以直接在枚举字段上使用 @JdbcType 注解来指定自定义 JDBC Type Handler。该注解允许我们定义一个自定义的 JdbcType 类,以处理 Java 枚举类型和相应 JDBC 类型之间的映射。

CustomerOrder 实体中使用 @JdbcType,如下:

@Enumerated(EnumType.STRING)
@JdbcType(type = PostgreSQLEnumJdbcType.class)
private OrderStatus status;

如上,指定 PostgreSQLEnumJdbcType 类作为自定义 Type Handler。该类是 Hibernate 6 的一部分,负责处理 Java 枚举字符串和 PostgreSQL 枚举类型之间的转换。

当持久化一个 OrderStatus 对象时(例如 order.setStatus(OrderStatus.PENDING)),Hibernate 首先将枚举值转换为字符串表示("PENDING")。然后,PostgreSQLEnumJdbcType 类将字符串值("PENDING")转换为适合 PostgreSQL 枚举类型的格式。然后将转换后的值传递给数据库,存储在 order_status 列中。

7、原生查询插入枚举值

使用原生查询将数据插入 PostgreSQL 表时,插入的数据类型必须与列类型相匹配。对于枚举类型的列,PostgreSQL 希望值是枚举类型的,而不仅仅是普通字符串。

试着执行不带转换的原生查询:

String sql = "INSERT INTO customer_order (status) VALUES (:status)";
Query query = session.createNativeQuery(sql);
query.setParameter("status", OrderStatus.COMPLETED); // 用字符串表示枚举

错误信息如下:

org.postgresql.util.PSQLException: ERROR: column "status" is of type order_status but expression is of type character varying

出现这个错误的原因是,PostgreSQL 希望值是 order_status 类型,但接收到的却是 character varying。为了解决这个问题,可以在原生查询中明确地将值转换为枚举类型:

String sql = "INSERT INTO customer_order (status) VALUES (CAST(:status AS order_status))";
Query query = session.createNativeQuery(sql);
query.setParameter("status", OrderStatus.COMPLETED);

SQL 语句中的 CAST(:status AS order_status) 部分确保字符串值 "COMPLETED" 在 PostgreSQL 中被明确转换为 order_status 枚举类型。

8、总结

本文介绍了如何使用 JPA 在 Java 枚举和 PostgreSQL 枚举之间进行映射,通过使用 PostgreSQLEnumJdbcType 可以确保 Java 枚举和 PostgreSQL 枚举之间的无缝集成。


Ref:https://www.baeldung.com/java-enums-jpa-postgresql