Java枚举、JPA和Postgres枚举 - 如何使它们共同工作?

62

我们有一个带有Postgres枚举的Postgres数据库。我们正在将JPA集成到我们的应用程序中。我们还拥有与Postgres枚举相对应的Java枚举。现在最大的问题是如何让JPA理解Java枚举和Postgres枚举。Java方面应该很容易,但我不确定如何处理Postgres方面。


1
我们现在也正在将系统迁移到JPA,但我们仍然将Java枚举映射到varchar列,使用Hibernate时没有遇到任何问题。 - miceuz
5个回答

70

实际上我使用的方法比PGObject和转换器更简单。因为在Postgres中,枚举类型可以很自然地被转换为文本,所以你只需要让它发挥最大作用即可。如果Arjan不介意,我会借用他的情绪示例:

在Postgres中的枚举类型:

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
Java中的类(class)和枚举(enum):
public @Entity class Person {

  public static enum Mood {sad, ok, happy};

  @Enumerated(EnumType.STRING)
  Mood mood;

}

那个@Enumerated标签表示应该使用文本进行枚举的序列化/反序列化。如果没有它,就会使用int,这比任何其他方式都更麻烦。

此时你有两个选择。你可以:

  1. 在连接字符串中添加stringtype=unspecified,如JDBC connection parameters中所解释的那样。这使得Postgres可以猜测右侧的类型并适当地转换所有内容,因为它接收到的是类似于“enum = unknown”的表达式,它已经知道如何处理(将?值提供给左侧类型的反序列化器)。这是首选项,因为它应该可以一次性适用于所有简单的用户定义类型,如枚举。

    jdbc:postgresql://localhost:5432/dbname?stringtype=unspecified
    

或:

  1. 在数据库中创建一个从 varchar 到 enum 的隐式转换。所以在第二种情况下,数据库接收到一些类似于 'enum = varchar' 的赋值或比较,然后在其内部目录中找到一条规则,说它可以通过 varchar 的序列化函数和枚举的反序列化函数来传递右侧的值。这比需要更多步骤;并且在目录中有太多的隐式转换会导致任意查询具有模棱两可的解释,因此要谨慎使用。强制转换的创建方式为:

    CREATE CAST (CHARACTER VARYING as mood) WITH INOUT AS IMPLICIT;

应该只需要这样就可以了。


11
这绝对是最近实现的最简单选项。 - James Fry
1
CREATE CAST解决方案似乎在将枚举用作JPA Repository参数时无法正常工作。例如:Entity findByMyEnum(MyEnum myEnum) - Daniele Repici
1
使用自动 DDL,仍会创建整数/字符变量类型。 - Adrian Elder

31
这涉及到制作多个映射。
首先,Postgres枚举类型在JDBC驱动程序中作为PGObject类型的实例返回。其类型属性具有您的postgres枚举名称,而值属性具有其值。(然而,顺序未存储,因此严格来说它不再是枚举类型,由于这个原因可能完全无用)
无论如何,如果您在Postgres中有以下定义:创建一种类型为情绪的枚举(ENUM),包括 "伤心","还好","快乐" 三种取值。

那么结果集将包含一个类型为“mood”,值为“happy”的PGObject,对于具有此枚举类型并具有值“happy”的行的列。

下一步是编写一些拦截器代码,它坐在JPA从原始结果集读取并在实体上设置值的位置之间。例如,假设您在Java中有以下实体:


公共实体类 Person {
公共 静态 枚举类型 Mood {伤心的,一般般的,开心的}
@Id 长整型 ID; Mood 心情;
}

不幸的是,JPA没有提供一个简单的拦截点来进行从PGObject到Java枚举Mood的转换。然而,大多数JPA供应商都有一些专有支持。例如,Hibernate使用了TypeDef和Type注释(来自Hibernate-annotations.jar)来支持此功能。


@TypeDef(name="myEnumConverter", typeClass=MyEnumConverter.class)
public @Entity class Person {
public static enum Mood {sad, ok, happy}
@Id Long ID; @Type(type="myEnumConverter") 心情 mood;
这段代码是Java中与数据库相关的实体类代码示例,其中使用了一个自定义枚举类型的转换器。在这个示例中,Person类表示数据库中的一个人,其中心情(mood)属性的类型为自定义枚举类型Mood,并使用名为“myEnumConverter”的转换器进行数据类型转换。

这些允许您提供一个UserType实例(来自Hibernate-core.jar),该实例执行实际的转换:


public class MyEnumConverter implements UserType {
private static final int[] SQL_TYPES = new int[]{Types.OTHER};
public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) throws HibernateException, SQLException {
// 从结果集中获取对应枚举值 Object pgObject = resultSet.getObject(X); // X 为包含枚举值的列名
try { // 利用反射获取枚举值的方法并调用 Method valueMethod = pgObject.getClass().getMethod("getValue"); String value = (String)valueMethod.invoke(pgObject);
// 将字符串转化为枚举类型并返回 return Mood.valueOf(value); } catch (Exception e) { e.printStackTrace(); }
return null; }
public int[] sqlTypes() { return SQL_TYPES; }
// 其他方法省略
}
说明:此代码段是一个实现了 Hibernate 框架 UserType 接口的自定义枚举类型转换器,用于将数据库中存储的枚举值转换为 Java 中的枚举类型。具体实现逻辑是从结果集中获取对应的枚举对象,利用反射获取其值并进行转换。

这不是一个完整的工作解决方案,而只是一个快速指向正确方向的提示。


3
我们现在也将系统迁移到 JPA,但是我们仍然将 Java 枚举映射到 varchar 列,而且在使用 Hibernate 时没有出现任何问题。与实际数据库中的枚举相比,varchar 在数据库中看起来很好,但你会错过两个东西:
  • 类型安全,除非你想为每个列添加检查约束或使用 FK 引用一个单独的表,该表将所有 varchar 值作为 PK。
  • 速度。字符串查找和比较更慢,即使进行索引。
因此这是一个权衡。无论是使用 varchars,接受上述限制,还是使用本地 PG 枚举并接受非自动映射。
- Arjan Tijms
有第三种方式,按序数映射,但这并不是真正推荐的方法;这会使代码难以阅读和脆弱;如果您从Java枚举中删除一个常量,所有已经在数据库中的序数可能会变得无效! - Arjan Tijms
1
有没有使用反射而不是转换为PgObject的原因? - Martin
1
为了避免编译时依赖,可以使用@Martin注解。并不是每个项目都在其类路径上拥有驱动程序,例如,驱动程序通常位于服务器的某个/lib文件夹中。如果这对您没有问题,则无需使用反射。 - Arjan Tijms
尽管它不使用序数,但它不会存储整个字符串,它只能固定在63字节以下。枚举值大小为4字节。请查看链接末尾的实现细节(https://www.postgresql.org/docs/current/datatype-enum.html)。 - Darshan K N

4
我已向Hibernate提交了一个带有修补程序的错误报告:HHH-5188
这个修补程序可以让我使用JPA将PostgreSQL枚举类型读取为Java枚举类型。

4
在4.3.5版本中看起来问题变得更糟了。现在我甚至无法将枚举类型持久化到Postgres中,因为Hibernate似乎坚持要将其写成文本或整数。 - Jannik Jochem

1

这对我有用

@org.hibernate.annotations.TypeDef(name = "enum_type", typeClass = PostgreSQLEnumType.class)
public class SomeEntity {

...

@Enumerated(EnumType.STRING)
@Type(type = "enum_type")
private AdType name;

}

并且

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.type.EnumType;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;

public class PostgreSQLEnumType extends EnumType {

@Override
public void nullSafeSet(PreparedStatement ps, Object obj, int index,
                        SharedSessionContractImplementor session) throws HibernateException, SQLException {
    if (obj == null) {
        ps.setNull(index, Types.OTHER);
    } else {
        ps.setObject(index, obj.toString(), Types.OTHER);
    }
 }
}

0

我尝试了上述建议,但没有成功。

唯一使其正常工作的方法是将POSTGRES def定义为TEXT类型,并在实体中使用@Enumerated(STRING)注解。

示例(使用Kotlin):

CREATE TABLE IF NOT EXISTS some_example_enum_table
(
    id                      UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    some_enum               TEXT NOT NULL,
);

示例 Kotlin 类:

enum class SomeEnum { HELLO, WORLD }

@Entity(name = "some_example_enum_table")
data class EnumExampleEntity(
    @Id
    @GeneratedValue
    var id: UUID? = null,

    @Enumerated(EnumType.STRING)
    var some_enum: SomeEnum = SomeEnum.HELLO,
)

然后我的JPA查找实际上起作用了:

@Repository
interface EnumExampleRepository : JpaRepository<EnumExampleEntity, UUID> {
    fun countBySomeEnumNot(status: SomeEnum): Int
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接