枚举类型(enum)是一种方便的数据类型,允许我们指定一个常量列表,对象字段或数据库列可以设置为该列表中的值。
枚举的美妙之处在于我们可以通过提供人类可读格式的枚举常量来确保数据完整性。因此,Java和PostgreSQL原生支持这种数据类型并不令人惊讶。
但是,Java和PostgreSQL枚举之间的转换并不是开箱即用的。JDBC API不将枚举识别为一个独特的数据类型,这使得如何处理转换的决策留给了JDBC驱动程序。通常,驱动程序对此无能为力 —— 这是一个先有鸡还是先有蛋的问题。
有许多解决方案可以帮助您在Java和PostgreSQL枚举之间进行映射,但大多数解决方案都是ORM或JDBC特定的。这意味着适用于Spring Data的建议可能不适用于Quarkus,反之亦然。
在本文中,我将回顾处理Java和PostgreSQL枚举转换的通用方法。此方法适用于普通的JDBC API和流行的ORM框架,如Spring Data、Hibernate、Quarkus和Micronaut。此外,它还被构建在PostgreSQL上的数据库支持,包括Amazon Aurora、Google AlloyDB和YugabyteDB。
创建Java实体对象和枚举
假设我们有一个代表披萨订单的Java实体对象:
public class PizzaOrder {
private Integer id;
private OrderStatus status;
private Timestamp orderTime;
// getters and setters are omitted
}
该对象的status字段是一个如下定义的枚举类型:
public enum OrderStatus {
Ordered,
Baking,
Delivering,
YummyInMyTummy
}
当我们在线订购披萨时,应用程序将状态设置为Ordered。一旦厨师开始处理我们的订单,状态就会变为Baking。一旦披萨刚出炉,就会有人取走并送到我们的门口 - 状态随后更新为Delivering。最后,状态被设置为YummyInMyTummy,意味着我们享受了披萨(希望如此!)
创建数据库表和枚举
为了在PostgreSQL中持久化披萨订单,让我们创建以下与我们的PizzaOrder实体类映射的表:
CREATE TABLE pizza_order (
id int PRIMARY KEY,
status order_status NOT NULL,
order_time timestamp NOT NULL DEFAULT now()
);
该表带有一个名为order_status的自定义类型。该类型是如下定义的枚举:
CREATE TYPE order_status AS ENUM(
'Ordered',
'Baking',
'Delivering',
'YummyInMyTummy');
该类型定义与 Java 对应的常量(状态)类似。
解决转换问题
如果我们使用psql(或其他SQL工具)连接到PostgreSQL并执行以下INSERT语句,它将成功执行:
insert into pizza_order (id, status, order_time)
values (1, 'Ordered', now());
该语句很好地接受文本表示形式的订单状态(枚举数据类型)Ordered。
看到这一点后,我们可能会想以这种格式将 Java 枚举值发送到 PostgreSQL String。如果我们直接使用JDBC APIPreparedStatement ,可以如下所示:
PreparedStatement statement = conn
.prepareStatement("INSERT INTO pizza_order (id, status, order_time) VALUES(?,?,?)");
statement.setInt(1, 1);
statement.setString(2, OrderStatus.Ordered.toString());
statement.setTimestamp(3, Timestamp.from(Instant.now()));
statement.executeUpdate();
但是,该语句将出现以下异常:
org.postgresql.util.PSQLException: ERROR: column "status" is of type order_status but expression is of type character varying
Hint: You will need to rewrite or cast the expression.
Position: 60
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2675)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2365)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:355)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:490)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:408)
尽管 PostgreSQL 在INSERT/UPDATE通过 psql 会话直接执行语句时接受枚举文本表示,但它不支持varchar(由 Java 传递)和我们的枚举类型之间的转换。
对于普通 JDBC API 修复此问题的一种方法是将 Java 枚举保留为以下类型的对象java.sql.Types.OTHER:
PreparedStatement statement = conn
.prepareStatement("INSERT INTO pizza_order (id, status, order_time) VALUES(?,?,?)");
statement.setInt(1, 1);
statement.setObject(2, OrderStatus.Ordered, java.sql.Types.OTHER);
statement.setTimestamp(3, Timestamp.from(Instant.now()));
statement.executeUpdate();
但是,正如我之前所说,这种方法并不通用。虽然它适用于普通的 JDBC API,但如果您使用Spring Data、Quarkus或其他ORM,则需要寻找其他解决方案。
数据库级别的类型转换
数据库提供了通用的解决方案。
PostgreSQL 支持强制转换运算符,可以自动执行两种数据类型之间的转换。
因此,在我们的例子中,我们需要做的就是创建以下运算符:
CREATE CAST (varchar AS order_status) WITH INOUT AS IMPLICIT;
创建的运算符将在varchar类型(由 JDBC 驱动程序传递)和我们的数据库级order_status枚举类型之间进行映射。该WITH INOUT AS IMPLICIT子句确保对于使用该order_status类型的所有语句,强制转换将透明且自动地发生。
使用纯 JDBC API 进行测试
在 PostgreSQL 中创建该强制转换运算符后,早期的 JDBC 代码片段会毫无问题地插入一个订单:
PreparedStatement statement = conn
.prepareStatement("INSERT INTO pizza_order (id, status, order_time) VALUES(?,?,?)");
statement.setInt(1, 1);
statement.setString(2, OrderStatus.Ordered.toString());
statement.setTimestamp(3, Timestamp.from(Instant.now()));
statement.executeUpdate();
我们所需要做的就是将 Java 枚举值作为 a 传递String,驱动程序会将其以表示形式发送到 PostgreSQL varchar,该表示形式会自动将该varchar值转换为order_status类型。
如果您从数据库读回订单,那么您可以轻松地从一个值重建 Java 级枚举String:
PreparedStatement statement = conn.prepareStatement("SELECT id, status, order_time " +
"FROM pizza_order WHERE id = ?");
statement.setInt(1, 1);
ResultSet resultSet = statement.executeQuery();
resultSet.next();
PizzaOrder order = new PizzaOrder();
order.setId(resultSet.getInt(1));
order.setStatus(OrderStatus.valueOf(resultSet.getString(2)));
order.setOrderTime(resultSet.getTimestamp(3));
使用 Spring Data 进行测试
接下来,让我们使用 Spring Data 验证基于强制转换运算符的方法。如今,您可能会直接使用 ORM 而不是 JDBC API。
首先,我们需要PizzaOrder用一些 JPA 和 Hibernate 注释来标记我们的实体类:
@Entity
public class PizzaOrder {
@Id
private Integer id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@CreationTimestamp
private Timestamp orderTime;
// getters and setters are omitted
}
指示@Enumerated(EnumType.STRING)JPA 实现(通常是 Hibernate)将枚举值作为 a 传递String给驱动程序。
其次,我们PizzaOrderRepository使用 Spring Data API 创建并保存实体对象:
// The repository interface
public interface PizzaOrderRepository extends JpaRepository<PizzaOrder, Integer> {
}
// The service class
@Service
public class PizzaOrderService {
@Autowired
PizzaOrderRepository repo;
@Transactional
public void addNewOrder(Integer id) {
PizzaOrder order = new PizzaOrder();
order.setId(id);
order.setStatus(OrderStatus.Ordered);
repo.save(order);
}
...
// Somewhere in the source code
pizzaService.addNewOrder(1);
}
当pizzaService.addNewOrder(1)我们的源代码中的某个位置调用该方法时,订单将被创建并成功保存到数据库中。Java 和 PostgreSQL 枚举之间的转换不会出现任何问题。
最后,如果我们需要从数据库读回订单,我们可以使用该JpaRepository.findById(ID id)方法,该方法根据其表示重新创建 Java 枚举String:
PizzaOrder order = repo.findById(orderId).get();
System.out.println("Order status: " + order.getStatus());
使用 Quarkus 进行测试
Quarkus 怎么样,它可能是您的#1 ORM?只要 Quarkus 支持 Hibernate 作为 JPA 实现,与 Spring Data 就没有显着差异。
首先,我们PizzaOrder用 JPA 和 Hibernate 注解来注解我们的实体类:
@Entity(name = "pizza_order")
public class PizzaOrder {
@Id
private Integer id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@CreationTimestamp
@Column(name = "order_time")
private Timestamp orderTime;
// getters and setters are omitted
}
其次,我们介绍PizzaOrderService一下使用EntityManager实例进行数据库请求:
@ApplicationScoped
public class PizzaOrderService {
@Inject
EntityManager entityManager;
@Transactional
public void addNewOrder(Integer id) {
PizzaOrder order = new PizzaOrder();
order.setId(id);
order.setStatus(OrderStatus.Ordered);
entityManager.persist(order);
}
...
// Somewhere in the source code
pizzaService.addNewOrder(1);
当我们调用pizzaService.addNewOrder(1)应用程序逻辑中的某处时,Quarkus 将成功保留顺序,并且 PostgreSQL 将负责 Java 和 PostgreSQL 枚举转换。
最后,要从数据库读回订单,我们可以使用以下方法EntityManager将结果集中的数据映射到PizzaOrder实体类(包括枚举字段):
PizzaOrder order = entityManager.find(PizzaOrder.class, 1);
System.out.println("Order status: " + order.getStatus());
使用 Micronaut 进行测试
好吧,好吧, Micronaut怎么样?我喜欢这个框架,你可能也会喜欢它。
数据库端强制转换运算符对于 Micronaut 来说也是一个完美的解决方案。为了让事情有所不同,我们不会为 Micronaut 使用 Hibernate。相反,我们将通过使用该模块来依赖 Micronaut 自己的功能micronaut-data-jdbc:
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-jdbc</artifactId>
</dependency>
// other dependencies
首先,我们对PizzaOrder实体进行注释:
@MappedEntity
public class PizzaOrder {
@Id
private Integer id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private Timestamp orderTime;
// getters and setters are omitted
}
接下来,定义PizzaRepository:
@JdbcRepository(dialect = Dialect.POSTGRES)
public interface PizzaRepository extends CrudRepository<PizzaOrder, Integer> {
}
然后,通过在应用程序逻辑中的某个位置调用以下代码片段,将披萨订单存储在数据库中:
PizzaOrder order = new PizzaOrder();
order.setId(1);
order.setStatus(OrderStatus.Ordered);
order.setOrderTime(Timestamp.from(Instant.now()));
repository.save(order);
与Spring Data 和 Quarkus 一样,Micronaut将对象持久保存到 PostgreSQL,让数据库处理 Java 和 PostgreSQL 枚举类型之间的转换没有任何问题。
最后,每当我们需要从数据库读回订单时,我们可以使用以下 JPA API:
PizzaOrder order = repository.findById(id).get();
System.out.println("Order status: " + order.getStatus());
findById(ID id)方法从数据库检索记录并重新创建PizzaOrder实体,包括enum类型的PizzaOrder.status字段。
总结
如今,你很可能会在应用程序逻辑中使用Java枚举,并因此需要将它们持久化到一个PostgreSQL数据库。你可以使用特定于ORM的解决方案进行Java和PostgreSQL枚举之间的转换,或者你可以利用基于PostgreSQL的转换操作符的通用方法。
基于转换操作符的方法适用于所有ORM,包括Spring Data、Hibernate、Quarkus和Micronaut,以及受欢迎的PostgreSQL兼容数据库,如Amazon Aurora、Google AlloyDB和YugabyteDB。
作者:Denis Magda
更多内容请关注公号【云原生数据库】
squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。