Hibernate PostgreSQL枚举和Java枚举之间的映射

46

背景

  • 使用Spring 3.x、JPA 2.0、Hibernate 4.x和Postgresql 9.x。
  • 正在处理一个具有枚举属性的Hibernate映射类,我想将其映射到Postgresql枚举。

问题

在枚举列上使用where子句查询会抛出异常。

org.hibernate.exception.SQLGrammarException: could not extract ResultSet
... 
Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: movedirection = bytea
  Hint: No operator matches the given name and argument type(s). You might need to add explicit type casts.

代码(大幅简化)

SQL:

create type movedirection as enum (
    'FORWARD', 'LEFT'
);

CREATE TABLE move
(
    id serial NOT NULL PRIMARY KEY,
    directiontomove movedirection NOT NULL
);

Hibernate映射类:

@Entity
@Table(name = "move")
public class Move {

    public enum Direction {
        FORWARD, LEFT;
    }

    @Id
    @Column(name = "id")
    @GeneratedValue(generator = "sequenceGenerator", strategy=GenerationType.SEQUENCE)
    @SequenceGenerator(name = "sequenceGenerator", sequenceName = "move_id_seq")
    private long id;

    @Column(name = "directiontomove", nullable = false)
    @Enumerated(EnumType.STRING)
    private Direction directionToMove;
    ...
    // getters and setters
}

调用查询的Java代码:

public List<Move> getMoves(Direction directionToMove) {
    return (List<Direction>) sessionFactory.getCurrentSession()
            .getNamedQuery("getAllMoves")
            .setParameter("directionToMove", directionToMove)
            .list();
}

Hibernate XML查询:
<query name="getAllMoves">
    <![CDATA[
        select move from Move move
        where directiontomove = :directionToMove
    ]]>
</query>

故障排除

  • Querying by id instead of the enum works as expected.
  • Java without database interaction works fine:

    public List<Move> getMoves(Direction directionToMove) {
        List<Move> moves = new ArrayList<>();
        Move move1 = new Move();
        move1.setDirection(directionToMove);
        moves.add(move1);
        return moves;
    }
    
  • createQuery instead of having the query in XML, similar to the findByRating example in Apache's JPA and Enums via @Enumerated documentation gave the same exception.
  • Querying in psql with select * from move where direction = 'LEFT'; works as expected.
  • Hardcoding where direction = 'FORWARD' in the query in the XML works.
  • .setParameter("direction", direction.name()) does not, same with .setString() and .setText(), exception changes to:

    Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: movedirection = character varying
    

解决方案尝试

  • Custom UserType as suggested by this accepted answer https://dev59.com/knI-5IYBdhLWcg3w9thw#1594020 along with:

    @Column(name = "direction", nullable = false)
    @Enumerated(EnumType.STRING) // tried with and without this line
    @Type(type = "full.path.to.HibernateMoveDirectionUserType")
    private Direction directionToMove;
    
  • Mapping with Hibernate's EnumType as suggested by a higher rated but not accepted answer https://dev59.com/knI-5IYBdhLWcg3w9thw#1604286 from the same question as above, along with:

    @Type(type = "org.hibernate.type.EnumType",
        parameters = {
                @Parameter(name  = "enumClass", value = "full.path.to.Move$Direction"),
                @Parameter(name = "type", value = "12"),
                @Parameter(name = "useNamed", value = "true")
        })
    

    With and without the two second parameters, after seeing https://dev59.com/eWrWa4cB1Zd3GeqP8C88#13241410

  • Tried annotating the getter and setter like in this answer https://dev59.com/SXnZa4cB1Zd3GeqPsqRH#20252215.
  • Haven't tried EnumType.ORDINAL because I want to stick with EnumType.STRING, which is less brittle and more flexible.

其他注意事项

JPA 2.1类型转换器不是必需的,但由于我现在使用的是JPA 2.0,因此无法选择。


12
这是一个非常清晰明了的问题。我希望更多的问题能够清晰地陈述问题、展示相关代码,并展示解决问题的尝试。做得好。 - Todd
截至2017年2月14日,@cslotty的链接已失效。 - bretmattingly
1
我支持Todd的评论,并且还会添加一个有用的链接到Medium,这是我从中获得解决方案的地方:https://prateek-ashtikar512.medium.com/how-to-map-java-enum-to-postgresql-enum-type-fcb3f81a7c42。在那里,我不需要添加Hypersistence Util,只需要一个package-info.java文件。 - jmizv
6个回答

66
你可以通过 Maven Central 使用 Hypersistence Util 依赖项轻松获取这些类型。
<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

接下来,您需要使用Hibernate的@Type注解对字段进行注释,如下例所示。

Hibernate 6

如果您正在使用Hibernate 6,可以按照以下方式进行映射:

@Entity(name = "Post")
@Table(name = "post")
public static 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上的示例,展示了如何与Hibernate 6.2一起使用。
Hibernate 5
如果您正在使用Hibernate 5,可以按照以下方式进行映射:
@Entity(name = "Post")
@Table(name = "post")
@TypeDef(
    name = "pgsql_enum",
    typeClass = PostgreSQLEnumType.class
)
public static class Post {
 
    @Id
    private Long id;
 
    private String title;
 
    @Enumerated(EnumType.STRING)
    @Column(columnDefinition = "post_status_info")
    @Type(type = "pgsql_enum")
    private PostStatus status;
 
    //Getters and setters omitted for brevity
}

这是一个在GitHub上的示例,展示了它如何与Hibernate 5.6一起工作。
这个映射假设你在PostgreSQL中有一个名为post_status_info的枚举类型。
CREATE TYPE post_status_info AS ENUM (
    'PENDING', 
    'APPROVED', 
    'SPAM'
)

就这样。

3
就像你所说的那样,运行得非常好!应该有更多的赞。 - leventunver
1
太棒了,应该被接受为最佳答案,非常有效!已点赞。 - Kevin Orriss
@VladMihalcea:我对你的库有点困惑。因为我不确定你的库是否支持Postgress枚举类型。所以,如果我将该库添加到我的项目中,我是否需要代码?混淆的原因是,在你的文章中,你解释了如何设置它,但仍然让我对功能产生疑问。 - LeO
1
是的,它支持。这个答案展示了如何编写一个支持PostgreSQL Enum的类型,而且正是hibernate-types库所做的。现在,你可以使用我编写的类型,也可以自己编写。就是这么简单。 - Vlad Mihalcea
org.hibernate.type.EnumType已被弃用并标记为将被移除。 - undefined
显示剩余6条评论

9

HQL

正确设置别名并使用限定的属性名称是解决问题的第一步。

<query name="getAllMoves">
    <![CDATA[
        from Move as move
        where move.directionToMove = :direction
    ]]>
</query>

Hibernate映射

@Enumerated(EnumType.STRING) 仍然无法工作,因此需要使用自定义的 UserType。关键是正确地覆盖 nullSafeSet,就像这个答案中所述https://dev59.com/sWsz5IYBdhLWcg3w3r0J#7614642和来自网络上的类似的实现问题解决

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

绕路

实现ParameterizedType并不合作:

org.hibernate.MappingException: type is not parameterized: full.path.to.PGEnumUserType

所以我不能像这样注释枚举属性:

@Type(type = "full.path.to.PGEnumUserType",
        parameters = {
                @Parameter(name = "enumClass", value = "full.path.to.Move$Direction")
        }
)

我改为这样声明类:

public class PGEnumUserType<E extends Enum<E>> implements UserType

使用构造函数:
public PGEnumUserType(Class<E> enumClass) {
    this.enumClass = enumClass;
}

不幸的是,这意味着任何其它枚举属性同样需要像这样的一个类:

public class HibernateDirectionUserType extends PGEnumUserType<Direction> {
    public HibernateDirectionUserType() {
        super(Direction.class);
    }
}

注释

只需为属性添加注释即可完成。

@Column(name = "directiontomove", nullable = false)
@Type(type = "full.path.to.HibernateDirectionUserType")
private Direction directionToMove;

其他注意事项

  • EnhancedUserType and the three methods it wants implemented

    public String objectToSQLString(Object value)
    public String toXMLString(Object value)
    public String objectToSQLString(Object value)
    

    didn't make any difference I could see, so I stuck with implements UserType.

  • Depending on how you're using the class, it might not be strictly necessary to make it postgres-specific by overriding nullSafeGet in the way the two linked solutions did.
  • If you're willing to give up the postgres enum, you can make the column text and the original code will work without extra work.

那么,最终的代码是什么?我看到了很多摘录,但它们在我的脑海中并没有形成一个稳固的解决方案。我需要一个解决方案,而不是一堆零散的改进、大量的超链接、我不想跟进的想法和思路。 - danissimo

1

正如Postgres文档中的8.7.3类型安全所述:

如果你真的需要做这样的事情,你可以编写自定义操作符或在查询中添加显式转换:

因此,如果你想要一个快速简单的解决方法,请按照以下步骤进行:

<query name="getAllMoves">
<![CDATA[
    select move from Move move
    where cast(directiontomove as text) = cast(:directionToMove as text)
]]>
</query>

很遗憾,你不能仅用两个冒号来完成此操作


1

在 Hibernate 6.3 版本之前

@Enumerated(EnumType.STRING)
@ColumnTransformer(write = "?::yours_enum_type")
private YoursEnumType enumType;

Hibernate 6.3

看起来从6.3.0版本开始,它已经被原生支持了。 https://hibernate.atlassian.net/browse/HHH-16125


0

让我先说一下,我能够使用Hibernate 4.3.x和Postgres 9.x完成这个任务。

我的解决方案是基于你所做的类似的东西。我相信如果你结合

@Type(type = "org.hibernate.type.EnumType",
parameters = {
        @Parameter(name  = "enumClass", value = "full.path.to.Move$Direction"),
        @Parameter(name = "type", value = "12"),
        @Parameter(name = "useNamed", value = "true")
})

和这个

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

你应该能够得到类似这样的东西,而不必进行上述任何更改。

@Type(type = "org.hibernate.type.EnumType",
parameters = {
        @Parameter(name  = "enumClass", value = "full.path.to.Move$Direction"),
        @Parameter(name = "type", value = "1111"),
        @Parameter(name = "useNamed", value = "true")
})

我相信这个方法是可行的,因为你实际上是在告诉Hibernate将枚举映射到其他类型(Types.OTHER == 1111)。这可能是一个稍微脆弱的解决方案,因为Types.OTHER的值可能会改变。但是,这将大大减少整体代码量。


0

我有另一种使用持久化转换器的方法:

import javax.persistence.Column;
import javax.persistence.Convert;

@Column(name = "direction", nullable = false)
@Convert(converter = DirectionConverter.class)
private Direction directionToMove;

这是一个转换器的定义:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class DirectionConverter implements AttributeConverter<Direction, String> {
    @Override
    public String convertToDatabaseColumn(Direction direction) {
        return direction.name();
    }

    @Override
    public Direction convertToEntityAttribute(String string) {
        return Diretion.valueOf(string);
    }
}

它不能解决映射到psql枚举类型的问题,但可以以良好的方式模拟@Enumerated(EnumType.STRING)或@Enumerated(EnumType.ORDINAL)。

对于ordinal,请使用direction.ordinal()和Direction.values()[number]。


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