如何使用JPA和Hibernate映射PostgreSQL enum

18
我正在尝试将一个名为transmission_result的PostgreSQL自定义类型映射到Hibernate/JPA POJO。 PostgreSQL自定义类型更或多或少是一个字符串值的enum类型。
我创建了一个名为PGEnumUserType的自定义EnumUserType,以及一个表示PostgreSQL枚举值的enum类。 当我对真实数据库运行此代码时,会收到以下错误:
'ERROR: column "status" is of type transmission_result but expression is of type
character varying 
  Hint: You will need to rewrite or cast the expression.
  Position: 135 '

看到这个问题,我意识到我需要将我的SqlTypes更改为Types.OTHER。但是这样做会导致我的集成测试(使用内存中的HyperSQL数据库)出现以下错误信息:

'Caused by: java.sql.SQLException: Table not found in statement
[select enrollment0_."id" as id1_47_0_,
 enrollment0_."tpa_approval_id" as tpa2_47_0_,
 enrollment0_."tpa_status_code" as tpa3_47_0_,
 enrollment0_."status_message" as status4_47_0_,
 enrollment0_."approval_id" as approval5_47_0_,
 enrollment0_."transmission_date" as transmis6_47_0_,
 enrollment0_."status" as status7_47_0_,
 enrollment0_."transmitter" as transmit8_47_0_
 from "transmissions" enrollment0_ where enrollment0_."id"=?]'

我不确定为什么更改sqlType会导致此错误。希望得到任何帮助。

JPA / Hibernate实体:

@Entity
@Access(javax.persistence.AccessType.PROPERTY)
@Table(name="transmissions")
public class EnrollmentCycleTransmission {

// elements of enum status column
private static final String ACCEPTED_TRANSMISSION = "accepted";
private static final String REJECTED_TRANSMISSION = "rejected";
private static final String DUPLICATE_TRANSMISSION = "duplicate";
private static final String EXCEPTION_TRANSMISSION = "exception";
private static final String RETRY_TRANSMISSION = "retry";

private Long transmissionID;
private Long approvalID;
private Long transmitterID;
private TransmissionStatusType transmissionStatus;
private Date transmissionDate;
private String TPAApprovalID;
private String TPAStatusCode;
private String TPAStatusMessage;


@Column(name = "id")
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getTransmissionID() {
    return transmissionID;
}

public void setTransmissionID(Long transmissionID) {
    this.transmissionID = transmissionID;
}

@Column(name = "approval_id")
public Long getApprovalID() {
    return approvalID;
}

public void setApprovalID(Long approvalID) {
    this.approvalID = approvalID;
}

@Column(name = "transmitter")
public Long getTransmitterID() {
    return transmitterID;
}

public void setTransmitterID(Long transmitterID) {
    this.transmitterID = transmitterID;
}

@Column(name = "status")
@Type(type = "org.fuwt.model.PGEnumUserType" , parameters ={@org.hibernate.annotations.Parameter(name = "enumClassName",value = "org.fuwt.model.enrollment.TransmissionStatusType")} )
public TransmissionStatusType getTransmissionStatus() {
    return this.transmissionStatus ;
}

public void setTransmissionStatus(TransmissionStatusType transmissionStatus) {
    this.transmissionStatus = transmissionStatus;
}

@Column(name = "transmission_date")
public Date getTransmissionDate() {
    return transmissionDate;
}

public void setTransmissionDate(Date transmissionDate) {
    this.transmissionDate = transmissionDate;
}

@Column(name = "tpa_approval_id")
public String getTPAApprovalID() {
    return TPAApprovalID;
}

public void setTPAApprovalID(String TPAApprovalID) {
    this.TPAApprovalID = TPAApprovalID;
}

@Column(name = "tpa_status_code")
public String getTPAStatusCode() {
    return TPAStatusCode;
}

public void setTPAStatusCode(String TPAStatusCode) {
    this.TPAStatusCode = TPAStatusCode;
}

@Column(name = "status_message")
public String getTPAStatusMessage() {
    return TPAStatusMessage;
}

public void setTPAStatusMessage(String TPAStatusMessage) {
    this.TPAStatusMessage = TPAStatusMessage;
}
}

自定义EnumUserType:

public class PGEnumUserType implements UserType, ParameterizedType {

private Class<Enum> enumClass;

public PGEnumUserType(){
    super();
}

public void setParameterValues(Properties parameters) {
    String enumClassName = parameters.getProperty("enumClassName");
    try {
        enumClass = (Class<Enum>) Class.forName(enumClassName);
    } catch (ClassNotFoundException e) {
        throw new HibernateException("Enum class not found ", e);
    }

}

public int[] sqlTypes() {
    return new int[] {Types.VARCHAR};
}

public Class returnedClass() {
    return enumClass;
}

public boolean equals(Object x, Object y) throws HibernateException {
    return x==y;
}

public int hashCode(Object x) throws HibernateException {
    return x.hashCode();
}

public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
    String name = rs.getString(names[0]);
    return rs.wasNull() ? null: Enum.valueOf(enumClass,name);
}

public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
    if (value == null) {
        st.setNull(index, Types.VARCHAR);
    }
    else {
        st.setString(index,((Enum) value).name());
    }
}

public Object deepCopy(Object value) throws HibernateException {
    return value;
}

public boolean isMutable() {
    return false;  //To change body of implemented methods use File | Settings | File Templates.
}

public Serializable disassemble(Object value) throws HibernateException {
    return (Enum) value;
}

public Object assemble(Serializable cached, Object owner) throws HibernateException {
    return cached;
}

public Object replace(Object original, Object target, Object owner) throws HibernateException {
    return original;
}

public Object fromXMLString(String xmlValue) {
    return Enum.valueOf(enumClass, xmlValue);
}

public String objectToSQLString(Object value) {
    return '\'' + ( (Enum) value ).name() + '\'';
}

public String toXMLString(Object value) {
    return ( (Enum) value ).name();
}
}

枚举类:

public enum TransmissionStatusType {
accepted,
rejected,
duplicate,
exception,
retry}

也可能是由于未将枚举类型转换为 varchar 引起的! - Gaurav
7个回答

23
如果您在PostgreSQL中有以下的post_status_info枚举类型:
CREATE TYPE post_status_info AS ENUM (
    'PENDING', 
    'APPROVED', 
    'SPAM'
)

你可以使用PostgreSQLEnumTypeHypersistence Utils项目中轻松地将Java Enum映射到PostgreSQL Enum列类型。
要使用它,你需要在字段上使用Hibernate的@Type注解,如下面的示例所示:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    private Long id;
 
    private String title;
 
    @Enumerated(EnumType.STRING)
    @Column(columnDefinition = "post_status_info")
    @Type(PostgreSQLEnumType.class)
    private PostStatus status;
 
    //Getters and setters omitted for brevity
}

就是这样!它像魔法一样运行。
这里有一个在GitHub上证明它的测试

1
哇!非常感谢您的贡献,这真是救了我的一天!(GitHub示例完美) - Camille
我很高兴能够帮助。 - Vlad Mihalcea
org.hibernate.type.EnumType在Hibernate 6.2中已被弃用。有什么替代的方法吗? - Ali Behzadian Nejad
@AliBehzadianNejad PostgreSQLEnumType来自Hypersistence Utils的枚举类型并未被弃用,因此如果您使用它,当Hibernate移除EnumType时,您的应用程序不会受到影响,因为PostgreSQLEnumType将切换到Hibernate ORM所需的任何内容。 - Vlad Mihalcea
在最新的Hibernate中没有@TypeDef。如果您更新您的答案,将会非常有帮助。谢谢。@vlad-mihalcea - Ali Behzadian Nejad
@AliBehzadianNejad 完成了。现在已经专门更新为Hypersistence Utils。 - Vlad Mihalcea

15

我找到答案了。在nullSafeSet函数中,我需要使用setObject而不是setString,并传入Types.OTHER作为java.sql.type,以让jdbc知道这是一个postgresql类型。

public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
    if (value == null) {
        st.setNull(index, Types.VARCHAR);
    }
    else {
//            previously used setString, but this causes postgresql to bark about incompatible types.
//           now using setObject passing in the java type for the postgres enum object
//            st.setString(index,((Enum) value).name());
        st.setObject(index,((Enum) value), Types.OTHER);
    }
}

7
以下内容可能有助于Postgres将字符串自动转换为您的SQL枚举类型,这样您就可以使用@Enumerated(STRING)而不需要@Type
CREATE CAST (character varying as post_status_type) WITH INOUT AS IMPLICIT;

3
一个快速的解决方案将是:
jdbc:postgresql://localhost:5432/postgres?stringtype=unspecified

?stringtype=unspecified 是答案


3

由于在Hibernate 6中TypeDef已经消失,因此我们需要对每个受影响的属性进行注释,我发现使用

@ColumnTransformer(write="?::transmission_result ")

强制类型转换生效,无需任何 Hibernate usertype 类。


1

build.gradle.kts

dependencies {
    api("javax.persistence", "javax.persistence-api", "2.2")
    api("org.hibernate",  "hibernate-core",  "5.4.21.Final")
}

在 Kotlin 中,使用 EnumType<Enum<*>>() 创建通用扩展非常重要。

PostgreSQLEnumType.kt

import org.hibernate.type.EnumType
import java.sql.Types

class PostgreSQLEnumType : EnumType<Enum<*>>() {

    @Throws(HibernateException::class, SQLException::class)
    override fun nullSafeSet(
            st: PreparedStatement,
            value: Any,
            index: Int,
            session: SharedSessionContractImplementor) {
        st.setObject(
                index,
                value.toString(),
                Types.OTHER
        )
    }
}

Custom.kt

import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
import javax.persistence.*

@Entity
@Table(name = "custom")
@TypeDef(name = "pgsql_enum", typeClass = PostgreSQLEnumType::class)
data class Custom(
        @Id @GeneratedValue @Column(name = "id")
        val id: Int,
    
        @Enumerated(EnumType.STRING) @Column(name = "status_custom") @Type(type = "pgsql_enum")
        val statusCustom: StatusCustom
)

enum class StatusCustom {
    FIRST, SECOND
}

一个更简单的选择是在连接URL中添加参数,以便不会丢失枚举数据类型。这是Arthur's answer中的第一个选项,但我不建议使用。我认为将数据类型映射到后端服务器和数据库之间的责任正是后端的责任。

<property name="connection.url">jdbc:postgresql://localhost:5432/yourdatabase?stringtype=unspecified</property>

来源



0

我通过创建自定义的 Hibernate Basic Type 解决了这个问题。

Hibernate 文档的参考

版本

  1. Hibernate: 6.1.7.Final
  2. SpringBoot: 3.0.3
  3. PostgreSql: 15

日志

如果您想要记录自定义类,请在 application.properties 文件中编写这些内容。

logging.level.**.**.**.GenderType=trace

学生实体 public class Student extends BaseEntity {

@Size(max = 30)
@NotNull
@Column(name = "student_id", nullable = false, length = 30)
private String studentId;

@Size(max = 40)
@NotNull
@Column(name = "name", nullable = false, length = 40)
private String name;

@NotNull
@Column(name = "major_id", nullable = false)
@ToString.Exclude
private Long majorId;

@Column(name = "gender", columnDefinition = "gender")
@Type(GenderType.class)
@Enumerated(EnumType.STRING)
private Gender gender;

@NotNull
@Column(name = "grade", nullable = false)
private Integer grade;

@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "activity_id")
private Long activityId;}

性别类型

public class GenderType implements UserType<Gender> {
public static final GenderType INSTANCE = new GenderType();

private static final Logger log = Logger.getLogger(GenderType.class);
@Override
public int getSqlType() {
    return Types.OTHER;
}

@Override
public Class<Gender> returnedClass() {
    return Gender.class;
}

@Override
public boolean equals(Gender x, Gender y) {
    return x.getCode()
            .equals(y.getCode());
}

@Override
public int hashCode(Gender x) {
    return Objects.hashCode(x);
}

@Override
public Gender nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session,
                          Object owner) throws SQLException {
    String columnValue = (String) rs.getObject(position);
    if (rs.wasNull()) {
        columnValue = null;
    }
    log.debugv("Result set column {0} value is {1}", position, columnValue);
    return Gender.fromCode(columnValue);
}

@Override
public void nullSafeSet(PreparedStatement st, Gender value, int index,
                        SharedSessionContractImplementor session) throws SQLException {
    if (value == null) {
        log.debugv("Binding null to parameter {0} ",index);
        st.setNull(index, Types.OTHER);
    }
    else {
        log.debugv("binding parameter [{1}] as [gender] - [{0}] ", value.getCode(), index);
        st.setObject(index, value, Types.OTHER);
    }
}

@Override
public Gender deepCopy(Gender value) {
    return value == null ? null :
           Gender.fromCode(value.getCode());
}

@Override
public boolean isMutable() {
    return true;
}

@Override
public Serializable disassemble(Gender value) {
    return deepCopy(value);
}

@Override
public Gender assemble(Serializable cached, Object owner) {
    return deepCopy((Gender) cached);
}

@Override
public Gender replace(Gender detached, Gender managed, Object owner) {
    return deepCopy(detached);
}}

性别

public enum Gender {

MALE("MALE"),

FEMALE("FEMALE"),

UNKNOWN("UNKNOWN");

private final String code;

Gender(String gender) {
    code = gender;
}


public String getCode() {
    return code;
}

public static Gender fromCode(String gender) {
    if (gender == null) {
        return null;
    }
    return switch (gender) {
        case "MALE", "男" -> Gender.MALE;
        case "FEMALE", "女" -> Gender.FEMALE;
        default -> throw new IllegalArgumentException("Invaild input value");
    };
}}

学生服务

    public Stream<Student> findStudent(@Nullable Integer grade, @Nullable String gender,
                                   @Nullable Long majorId, @Nullable Long activityId) {

    return studentRepo.findAll((Specification<Student>) (root, query, criteriaBuilder) -> {
        List<Predicate> predicates = new ArrayList<>();
        if (grade != null) {
            predicates.add(criteriaBuilder.equal(root.get("grade"), grade));
        }

        if (gender != null) {
            predicates.add(criteriaBuilder.equal(root.get("gender"), Gender.fromCode(gender)));
        }

        return query.where(predicates.toArray(new Predicate[predicates.size()]))
                    .getRestriction();
    }).stream();

}


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