简体   繁体   English

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

[英]Hibernate mapping between PostgreSQL enum and Java enum

Background背景

  • Spring 3.x, JPA 2.0, Hibernate 4.x, Postgresql 9.x. Spring 3.x、JPA 2.0、Hibernate 4.x、Postgresql 9.x。
  • Working on a Hibernate mapped class with an enum property that I want to map to a Postgresql enum.使用我想映射到 Postgresql 枚举的枚举属性处理 Hibernate 映射类。

Problem问题

Querying with a where clause on the enum column throws an exception.使用枚举列上的 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.

Code (heavily simplified)代码(大大简化)

SQL:查询语句:

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

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

Hibernate mapped class: 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 that calls the query:调用查询的Java:

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

Hibernate xml query:休眠xml查询:

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

Troubleshooting故障排除

  • Querying by id instead of the enum works as expected.id而不是 enum 查询按预期工作。
  • Java without database interaction works fine:没有数据库交互的 Java 工作正常:

     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. createQuery而不是在 XML 中进行查询,类似于Apache 的 JPA 和 Enums 中findByRating示例, 通过 @Enumerated 文档给出了相同的例外。
  • Querying in psql with select * from move where direction = 'LEFT';在 psql 中使用select * from move where direction = 'LEFT'; works as expected.按预期工作。
  • Hardcoding where direction = 'FORWARD' in the query in the XML works.在 XML 中的查询中硬编码where direction = 'FORWARD'有效。
  • .setParameter("direction", direction.name()) does not, same with .setString() and .setText() , exception changes to: .setParameter("direction", direction.name())不,用相同.setString().setText()异常变化为:

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

Attempts at resolution尝试解决

Other notes其他注意事项

A JPA 2.1 Type Converter shouldn't be necessary, but isn't an option regardless, since I'm on JPA 2.0 for now. JPA 2.1 类型转换器不是必需的,但无论如何都不是一个选项,因为我现在使用的是 JPA 2.0。

You can simply get these types via Maven Central using the Hibernate Types dependency:您可以使用Hibernate Types依赖项通过 Maven Central 简单地获取这些类型:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

If you easily map Java Enum to a PostgreSQL Enum column type using the following custom Type:如果您使用以下自定义类型轻松地将 Java Enum 映射到 PostgreSQL Enum 列类型:

public class PostgreSQLEnumType extends org.hibernate.type.EnumType {
     
    public void nullSafeSet(
            PreparedStatement st, 
            Object value, 
            int index, 
            SharedSessionContractImplementor session) 
        throws HibernateException, SQLException {
        if(value == null) {
            st.setNull( index, Types.OTHER );
        }
        else {
            st.setObject( 
                index, 
                value.toString(), 
                Types.OTHER 
            );
        }
    }
}

To use it, you need to annotate the field with the Hibernate @Type annotation as illustrated in the following example:要使用它,您需要使用 Hibernate @Type注释来注释该字段,如下例所示:

@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
}

This mapping assumes you have the post_status_info enum type in PostgreSQL:此映射假设您在 PostgreSQL 中有post_status_info枚举类型:

CREATE TYPE post_status_info AS ENUM (
    'PENDING', 
    'APPROVED', 
    'SPAM'
)

That's it, it works like a charm.就是这样,它就像一个魅力。 Here's a test on GitHub that proves it .这是GitHub 上的一个测试,证明了这一点

HQL高品质

Aliasing correctly and using the qualified property name was the first part of the solution.正确别名和使用限定的属性名称是解决方案的第一部分。

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

Hibernate mapping休眠映射

@Enumerated(EnumType.STRING) still didn't work, so a custom UserType was necessary. @Enumerated(EnumType.STRING)仍然不起作用,因此需要自定义UserType The key was to correctly override nullSafeSet like in this answer https://stackoverflow.com/a/7614642/1090474 and similar implementations from the web.关键是正确覆盖nullSafeSet就像这个答案https://stackoverflow.com/a/7614642/1090474和来自网络的类似实现

@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);
    }
}

Detour车辆改道

implements ParameterizedType wasn't cooperating: implements ParameterizedType不合作:

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

so I wasn't able to annotate the enum property like this:所以我无法像这样注释 enum 属性:

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

Instead, I declared the class like so:相反,我像这样声明了这个类:

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

with a constructor:使用构造函数:

public PGEnumUserType(Class<E> enumClass) {
    this.enumClass = enumClass;
}

which, unfortunately, means any other enum property similarly mapped will need a class like this:不幸的是,这意味着任何其他类似映射的枚举属性都需要一个这样的类:

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

Annotation注解

Annotate the property and you're done.注释属性,你就完成了。

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

Other notes其他注意事项

  • EnhancedUserType and the three methods it wants implemented EnhancedUserType和它想要实现的三个方法

    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 .我看不出有什么区别,所以我坚持使用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.根据您使用该类的方式,通过以两个链接解决方案的方式覆盖nullSafeGet来使其特定于 postgres 可能不是绝对必要的。
  • 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.如果你愿意放弃 postgres 枚举,你可以使列text和原始代码无需额外工作即可工作。

As said in 8.7.3.8.7.3所述Type Safety of Postgres Docs :Postgres 文档的类型安全

If you really need to do something like that, you can either write a custom operator or add explicit casts to your query:如果您真的需要做类似的事情,您可以编写自定义运算符或向查询添加显式转换:

so if you want a quick and simple workaround, do like this:所以如果你想要一个快速简单的解决方法,请这样做:

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

Unfortunately, you can't do it simply with two colons :不幸的是, 你不能简单地用两个冒号来做到这一点

Let me start off saying I was able to do this using Hibernate 4.3.x and Postgres 9.x.首先让我说我可以使用 Hibernate 4.3.x 和 Postgres 9.x 来做到这一点。

I based my solution off something similar to what you did.我的解决方案基于与您所做的类似的事情。 I believe if you combine我相信如果你结合

@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")
})

and this和这个

@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);
  }
}

You should be able to get something along the lines of this, without having to make either above change.您应该能够获得与此类似的东西,而无需进行上述任何更改。

@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")
})

I believe that this works since you're essentially telling Hibernate to map the enum to a type of other ( Types.OTHER == 1111 ).我相信这是有效的,因为您实际上是在告诉 Hibernate 将枚举映射到其他类型( Types.OTHER == 1111 )。 It may be a slightly brittle solution since the value of Types.OTHER could change.这可能是一个稍微脆弱的解决方案,因为Types.OTHER的值可能会改变。 However, this would provide significantly less code overall.但是,这将提供明显更少的代码。

I have another approach with a persistence converter:我有另一种使用持久性转换器的方法:

import javax.persistence.Convert;

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

This is a converter definition:这是一个转换器定义:

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);
    }
}

It does not resolve mapping to psql enum type, but it can simulate @Enumerated(EnumType.STRING) or @Enumerated(EnumType.ORDINAL) in a good way.它不会解析到 psql 枚举类型的映射,但它可以很好地模拟 @Enumerated(EnumType.STRING) 或 @Enumerated(EnumType.ORDINAL)。

For ordinal use direction.ordinal() and Direction.values()[number].对于序数使用 direction.ordinal() 和 Direction.values()[number]。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM