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

背景

  • Spring 3.x,JPA 2.0,Hibernate 4.x,Postgresql 9.x.
  • 使用我要映射到Postgresql枚举的枚举属性处理Hibernate映射类。

问题

在枚举列上使用where子句进行查询会引发exception。

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 getMoves(Direction directionToMove) { return (List) sessionFactory.getCurrentSession() .getNamedQuery("getAllMoves") .setParameter("directionToMove", directionToMove) .list(); } 

Hibernate xml查询:

    

故障排除

  • 通过id而不是enum查询按预期工作。
  • 没有数据库交互的Java工作正常:

     public List getMoves(Direction directionToMove) { List moves = new ArrayList(); Move move1 = new Move(); move1.setDirection(directionToMove); moves.add(move1); return moves; } 
  • createQuery而不是XML中的查询,类似于Apache的JPA中的findByRating示例和Enums通过@Enumerated文档给出了相同的exception。
  • 使用select * from move where direction = 'LEFT';查询psql select * from move where direction = 'LEFT'; 按预期工作。
  • 在XML中查询中的where direction = 'FORWARD'硬编码工作原理。
  • .setParameter("direction", direction.name())不与.setString().setString()相同,exception更改为:

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

试图解决

  • 自己接受的答案https://stackoverflow.com/a/1594020/1090474建议的自定义UserType以及:

     @Column(name = "direction", nullable = false) @Enumerated(EnumType.STRING) // tried with and without this line @Type(type = "full.path.to.HibernateMoveDirectionUserType") private Direction directionToMove; 
  • 使用Hibernate的EnumType映射,如同上面提到的更高评级但未被接受的答案https://stackoverflow.com/a/1604286/1090474 ,以及:

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

    在看到https://stackoverflow.com/a/13241410/1090474之后,有和没有两个第二个参数

  • 尝试在这个答案https://stackoverflow.com/a/20252215/1090474中注释getter和setter。
  • 没有尝试过EnumType.ORDINAL因为我想坚持使用EnumType.STRING ,它不那么脆弱且更灵活。

其他说明

JPA 2.1类型转换器不是必需的,但不管怎样都不是选项,因为我现在正在使用JPA 2.0。

您不必手动创建以下所有Hibernate类型。 您可以使用以下依赖项通过Maven Central获取它们:

  com.vladmihalcea hibernate-types-52 ${hibernate-types.version}  

有关更多信息,请查看hibernate类型的开源项目 。

正如我在本文中所解释的 ,如果您使用以下自定义类型轻松地将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 ); } } } 

要使用它,您需要使用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 } 

此映射假设您在PostgreSQL中具有post_status_info枚举类型:

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

就是这样,它就像一个魅力。 这是对GitHub的测试certificate了这一点 。

HQL

正确别名并使用限定属性名称是解决方案的第一部分。

    

Hibernate映射

@Enumerated(EnumType.STRING)仍然无效,因此需要自定义UserType 。 关键是要正确覆盖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); } } 

车辆改道

implements 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> implements UserType 

使用构造函数:

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

遗憾的是,这意味着任何其他类似映射的枚举属性都需要这样的类:

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

注解

注释财产,你就完成了。

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

其他说明

  • EnhancedUserType以及它想要实现的三种方法

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

    我没看到任何不同,所以我坚持使用implements UserType

  • 根据您使用该类的方式,可能没有必要通过以两个链接解决方案的方式覆盖nullSafeGet来使其特定于postgres。
  • 如果您愿意放弃postgres枚举,您可以使列text和原始代码无需额外工作。

如8.7.3所述。 Postgres文档的类型安全性 :

如果您确实需要执行类似的操作,可以编写自定义运算符或向查询添加显式强制转换:

因此,如果您想要快速简单的解决方法,请执行以下操作:

    

不幸的是, 你不能只用两个冒号做到这一点 :

让我开始说我能够使用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的值可能会改变。 但是,这将提供明显更少的代码。

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

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

这是转换器定义:

 import javax.persistence.Converter; @Converter public class DirectionConverter implements AttributeConverter { @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)。

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