web-dev-qa-db-ja.com

PostgreSQL列挙型とJava列挙型の間のHibernateマッピング

バックグラウンド

  • Spring 3.x、JPA 2.0、Hibernate 4.x、Postgresql9.x。
  • Postgresqlのenumにマップするenumプロパティを持つHibernateマップクラスで作業します。

問題

Enum列の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>
_

トラブルシューティング

  • Enumの代わりに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;
    }
    _
  • ApacheのJPAおよび@Enumerated documentationを介したEnumscreateQueryの例と同様に、XMLでクエリを作成する代わりにfindByRatingを使用すると、同じ例外が発生しました。
  • _select * from move where direction = 'LEFT';_を使用したpsqlでのクエリは期待どおりに機能します。
  • XMLのクエリでの_where direction = 'FORWARD'_のハードコーディングは機能します。
  • .setParameter("direction", direction.name()).setString()および.setText()とは異なり、例外が次のように変更されます。

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

解決の試み

  • この受け入れられた回答で提案されているカスタムUserTypehttps://stackoverflow.com/a/1594020/1090474 とともに:

    _@Column(name = "direction", nullable = false)
    @Enumerated(EnumType.STRING) // tried with and without this line
    @Type(type = "full.path.to.HibernateMoveDirectionUserType")
    private Direction directionToMove;
    _
  • 評価が高いが受け入れられていない回答 https://stackoverflow.com/a/1604286/1090474 と同じ質問からの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")
        })
    _

    https://stackoverflow.com/a/13241410/1090474 を見た後の2つの2番目のパラメーターの有無

  • この回答のようにゲッターとセッターに注釈を付けてみました https://stackoverflow.com/a/20252215/1090474
  • _EnumType.ORDINAL_を試したことはありません。脆弱性が低く、柔軟性の高い_EnumType.STRING_を使い続けたいからです。

その他の注意事項

今のところJPA 2.0を使用しているので、JPA 2.1タイプコンバーターは必要ではありませんが、オプションではありません。

29
Kenny Linsky

HQL

正しいエイリアスと修飾されたプロパティ名の使用がソリューションの最初の部分でした。

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

Hibernateマッピング

@Enumerated(EnumType.STRING)はまだ機能しなかったため、カスタムUserTypeが必要でした。重要なのは、この回答のようにnullSafeSetを正しくオーバーライドすることでした https://stackoverflow.com/a/7614642/1090474 および similarimplementations ウェブから。

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

そのため、次のようにenumプロパティに注釈を付けることができませんでした。

_@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とそれが実装したい3つのメソッド

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

    目に見える違いはなかったので、_implements UserType_のままにしました。

  • クラスの使用方法によっては、2つのリンクされたソリューションのようにnullSafeGetをオーバーライドして、クラスをpostgres固有にする必要がない場合もあります。
  • Postgres列挙型をあきらめたい場合は、列をtextにすると、元のコードは余分な作業なしで機能します。
9
Kenny Linsky

以下のすべてのHibernateタイプを手動で作成する必要はありません。次の依存関係を使用して、Maven Centralから単純に取得できます。

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

詳しくは hibernate-typesオープンソースプロジェクト をご覧ください。

私が この記事で説明 のように、簡単に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のテスト です。

30
Vlad Mihalcea

8.7.3。Postgresドキュメントのタイプセーフ で述べたように:

本当にそのようなことをする必要がある場合は、カスタム演算子を作成するか、クエリに明示的なキャストを追加できます。

したがって、迅速かつ簡単な回避策が必要な場合は、次のようにします。

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

残念ながら、 2つのコロンだけでは実行できません

1
bdshadow

永続化コンバーターを使用した別のアプローチがあります:

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<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]を使用します。

0
Marko Novakovic

まず、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")
})

列挙型を他の型(Types.OTHER == 1111)にマップするように本質的にHibernateに指示しているので、これはうまくいくと思います。 Types.OTHERの値が変更される可能性があるため、これはやや脆弱なソリューションである可能性があります。ただし、これによりコード全体が大幅に少なくなります。

0
lew