PostgreSQL 枚举和 Java 枚举之间的 Hibernate 映射
Hibernate mapping between PostgreSQL enum and Java enum
背景
- Spring 3.x、JPA 2.0、Hibernate 4.x、Postgresql 9.x。
- 正在处理 Hibernate 映射 class,其中包含我想映射到 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
);
休眠映射 class:
@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();
}
休眠 xml 查询:
<query name="getAllMoves">
<![CDATA[
select move from Move move
where directiontomove = :directionToMove
]]>
</query>
疑难解答
- 通过
id
而不是枚举的查询按预期工作。
没有数据库交互的 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
而不是 XML 中的查询,类似于 Apache's JPA and Enums via @Enumerated documentation 中的 findByRating
示例给出了相同的异常。
- 使用
select * from move where direction = 'LEFT';
在 psql 中查询按预期工作。
- 硬编码
where direction = 'FORWARD'
在查询中 XML 有效。
.setParameter("direction", direction.name())
没有,同.setString()
和.setText()
,异常变为:
Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: movedirection = character varying
尝试解决
自定义 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
映射,如来自与上述相同问题的更高评价但未被接受的答案 所建议,以及:
@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")
})
有无两个第二个参数,看到后
- 尝试像这个答案中那样注释 getter 和 setter 。
- 还没有尝试过
EnumType.ORDINAL
因为我想坚持使用 EnumType.STRING
,它不那么脆弱而且更灵活。
其他说明
JPA 2.1 类型转换器不是必需的,但无论如何都不是一个选项,因为我现在使用的是 JPA 2.0。
HQL
正确使用别名并使用 合格的 属性 名称 是解决方案的第一部分。
<query name="getAllMoves">
<![CDATA[
from Move as move
where move.directionToMove = :direction
]]>
</query>
休眠映射
@Enumerated(EnumType.STRING)
仍然无效,因此需要自定义 UserType
。关键是正确地覆盖 nullSafeSet
就像这个来自网络的答案 and similar implementations 一样。
@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")
}
)
相反,我这样声明 class:
public class PGEnumUserType<E extends Enum<E>> implements UserType
有一个构造函数:
public PGEnumUserType(Class<E> enumClass) {
this.enumClass = enumClass;
}
不幸的是,这意味着任何其他类似映射的枚举 属性 都需要这样的 class:
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
以及它要实现的三个方法
public String objectToSQLString(Object value)
public String toXMLString(Object value)
public String objectToSQLString(Object value)
在我看来没有任何区别,所以我坚持使用 implements UserType
。
- 根据您使用 class 的方式,可能没有必要像两个链接的解决方案那样通过覆盖
nullSafeGet
使其特定于 postgres。
- 如果您愿意放弃 postgres 枚举,您可以创建该列
text
,原始代码无需额外工作即可运行。
如8.7.3. Type Safety of Postgres Docs所述:
If you really need to do something like that, you can either write a custom operator or add explicit casts to your query:
因此,如果您想要一个快速简单的解决方法,请这样做:
<query name="getAllMoves">
<![CDATA[
select move from Move move
where cast(directiontomove as text) = cast(:directionToMove as text)
]]>
</query>
不幸的是,you can't do it simply with two colons:
首先让我说我能够使用 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
的值可能会改变。但是,这将提供更少的代码。
您可以使用 Hibernate Types 依赖项通过 Maven Central 简单地获取这些类型:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
如果您使用以下自定义类型轻松地将 Java 枚举映射到 PostgreSQL 枚举列类型:
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'
)
就是这样,它就像一个魅力。这是 test on GitHub that proves it.
我有另一种持久化转换器的方法:
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)。
按顺序使用 direction.ordinal() 和 Direction.values()[number].
背景
- Spring 3.x、JPA 2.0、Hibernate 4.x、Postgresql 9.x。
- 正在处理 Hibernate 映射 class,其中包含我想映射到 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
);
休眠映射 class:
@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();
}
休眠 xml 查询:
<query name="getAllMoves">
<![CDATA[
select move from Move move
where directiontomove = :directionToMove
]]>
</query>
疑难解答
- 通过
id
而不是枚举的查询按预期工作。 没有数据库交互的 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
而不是 XML 中的查询,类似于 Apache's JPA and Enums via @Enumerated documentation 中的findByRating
示例给出了相同的异常。- 使用
select * from move where direction = 'LEFT';
在 psql 中查询按预期工作。 - 硬编码
where direction = 'FORWARD'
在查询中 XML 有效。 .setParameter("direction", direction.name())
没有,同.setString()
和.setText()
,异常变为:Caused by: org.postgresql.util.PSQLException: ERROR: operator does not exist: movedirection = character varying
尝试解决
自定义
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
映射,如来自与上述相同问题的更高评价但未被接受的答案 所建议,以及:@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") })
有无两个第二个参数,看到后
- 尝试像这个答案中那样注释 getter 和 setter 。
- 还没有尝试过
EnumType.ORDINAL
因为我想坚持使用EnumType.STRING
,它不那么脆弱而且更灵活。
其他说明
JPA 2.1 类型转换器不是必需的,但无论如何都不是一个选项,因为我现在使用的是 JPA 2.0。
HQL
正确使用别名并使用 合格的 属性 名称 是解决方案的第一部分。
<query name="getAllMoves">
<![CDATA[
from Move as move
where move.directionToMove = :direction
]]>
</query>
休眠映射
@Enumerated(EnumType.STRING)
仍然无效,因此需要自定义 UserType
。关键是正确地覆盖 nullSafeSet
就像这个来自网络的答案 and similar implementations 一样。
@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")
}
)
相反,我这样声明 class:
public class PGEnumUserType<E extends Enum<E>> implements UserType
有一个构造函数:
public PGEnumUserType(Class<E> enumClass) {
this.enumClass = enumClass;
}
不幸的是,这意味着任何其他类似映射的枚举 属性 都需要这样的 class:
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
以及它要实现的三个方法public String objectToSQLString(Object value) public String toXMLString(Object value) public String objectToSQLString(Object value)
在我看来没有任何区别,所以我坚持使用
implements UserType
。- 根据您使用 class 的方式,可能没有必要像两个链接的解决方案那样通过覆盖
nullSafeGet
使其特定于 postgres。 - 如果您愿意放弃 postgres 枚举,您可以创建该列
text
,原始代码无需额外工作即可运行。
如8.7.3. Type Safety of Postgres Docs所述:
If you really need to do something like that, you can either write a custom operator or add explicit casts to your query:
因此,如果您想要一个快速简单的解决方法,请这样做:
<query name="getAllMoves">
<![CDATA[
select move from Move move
where cast(directiontomove as text) = cast(:directionToMove as text)
]]>
</query>
不幸的是,you can't do it simply with two colons:
首先让我说我能够使用 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
的值可能会改变。但是,这将提供更少的代码。
您可以使用 Hibernate Types 依赖项通过 Maven Central 简单地获取这些类型:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
如果您使用以下自定义类型轻松地将 Java 枚举映射到 PostgreSQL 枚举列类型:
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'
)
就是这样,它就像一个魅力。这是 test on GitHub that proves it.
我有另一种持久化转换器的方法:
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)。
按顺序使用 direction.ordinal() 和 Direction.values()[number].