web-dev-qa-db-de.com

Hibernate-Zuordnung zwischen PostgreSQL-Enum und Java-Enum

Hintergrund

  • Spring 3.x, JPA 2.0, Hibernate 4.x, Postgresql 9.x.
  • Arbeiten an einer Hibernate-Klasse mit einer Enumeneigenschaft, die ich einer Postgresql-Enumenzuordnen möchte.

Problem

Beim Abfragen mit einer where-Klausel in der Enumerationsspalte wird eine Ausnahme ausgelöst.

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 (stark vereinfacht)

SQL:

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

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

Hibernate zugeordnete Klasse:

@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, das die Abfrage aufruft:

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

XML-Abfrage im Ruhezustand:

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

Fehlerbehebung

  • Die Abfrage mit id anstelle der Aufzählung funktioniert wie erwartet.
  • Java ohne Datenbankinteraktion funktioniert gut:

    public List<Move> getMoves(Direction directionToMove) {
        List<Move> moves = new ArrayList<>();
        Move move1 = new Move();
        move1.setDirection(directionToMove);
        moves.add(move1);
        return moves;
    }
    
  • createQuery Anstatt die Abfrage in XML zu haben, ähnelte das _findByRating-Beispiel in Apaches JPA und Enums über @Enumerated documentation der gleichen Ausnahme.
  • Die Abfrage in psql mit select * from move where direction = 'LEFT'; funktioniert wie erwartet.
  • Hardcoding where direction = 'FORWARD' in der Abfrage in den XML-Werken.
  • .setParameter("direction", direction.name()) ändert sich nicht wie bei .setString() und .setText():

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

Versuche der Lösung

  • Benutzerdefinierte UserType wie in dieser akzeptierten Antwort vorgeschlagen https://stackoverflow.com/a/1594020/1090474 zusammen mit:

    @Column(name = "direction", nullable = false)
    @Enumerated(EnumType.STRING) // tried with and without this line
    @Type(type = "full.path.to.HibernateMoveDirectionUserType")
    private Direction directionToMove;
    
  • Mapping mit EnumType von Hibernate, wie von einer höher bewerteten, aber nicht akzeptierten Antwort https://stackoverflow.com/a/1604286/1090474 aus der gleichen Frage wie oben angegeben, zusammen mit:

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

    Mit und ohne die beiden zweiten Parameter, nachdem Sie https://stackoverflow.com/a/13241410/1090474 gesehen haben. _

  • Versuchte, den Getter und Setter wie in dieser Antwort zu kommentieren https://stackoverflow.com/a/20252215/1090474 .
  • Ich habe EnumType.ORDINAL nicht ausprobiert, da ich mit EnumType.STRING bleiben möchte, das weniger spröde und flexibler ist.

Weitere Hinweise

Ein JPA 2.1 Type Converter sollte nicht notwendig sein, ist aber keine Option, da ich jetzt auf JPA 2.0 bin.

20
Kenny Linsky

HQL

Richtiges Aliasing und Verwenden des qualifizierten Eigenschaftsnamens war der erste Teil der Lösung.

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

Hibernate-Mapping

@Enumerated(EnumType.STRING) hat immer noch nicht funktioniert, daher war eine benutzerdefinierte UserType erforderlich. Der Schlüssel war, nullSafeSet wie in dieser Antwort https://stackoverflow.com/a/7614642/1090474 und ähnlicheImplementierungen aus dem Web korrekt zu überschreiben.

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

Umleitung

implements ParameterizedType kooperierte nicht:

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

so konnte ich die Enumeneigenschaft nicht wie folgt kommentieren:

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

Stattdessen erklärte ich die Klasse so:

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

mit einem Konstruktor:

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

was leider bedeutet, dass eine andere, ähnlich zugeordnete Enumeneigenschaft eine Klasse wie diese benötigt:

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

Anmerkung

Beschriften Sie die Eigenschaft und Sie sind fertig.

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

Weitere Hinweise

  • EnhancedUserType und die drei Methoden, die implementiert werden sollen

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

    ich machte keinen Unterschied, ich konnte sehen, also blieb ich bei implements UserType.

  • Je nachdem, wie Sie die Klasse verwenden, ist es möglicherweise nicht unbedingt erforderlich, sie postgres-spezifisch zu machen, indem Sie nullSafeGet wie die beiden verknüpften Lösungen überschreiben.
  • Wenn Sie bereit sind, das Postgres-Enum aufzugeben, können Sie die Spalte text festlegen, und der ursprüngliche Code funktioniert ohne zusätzlichen Aufwand.
8
Kenny Linsky

Sie müssen nicht alle folgenden Hibernate-Typen manuell erstellen. Sie können sie einfach über Maven Central mit folgender Abhängigkeit erhalten:

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

Weitere Informationen finden Sie im Open-Source-Projekt hibernate-types .

Wie ich in diesem Artikel erklärt , wenn Sie Java Enum ganz einfach einem PostgreSQL Enum-Spaltentyp zuordnen, verwenden Sie den folgenden benutzerdefinierten Typ:

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

Um es zu verwenden, müssen Sie das Feld mit der Annotation Hibernate @Type versehen, wie im folgenden Beispiel dargestellt:

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

Dieses Mapping setzt voraus, dass Sie in PostgreSQL den Enammentyp post_status_info haben:

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

Das ist es, es funktioniert wie ein Zauber. Hier ist ein Test auf GitHub, der es beweist .

17
Vlad Mihalcea

Wie in 8.7.3 erwähnt. Typensicherheit von Postgres-Dokumenten :

Wenn Sie so etwas wirklich tun müssen, können Sie entweder einen benutzerdefinierten Operator schreiben oder der Abfrage explizite Umsetzungen hinzufügen:

wenn Sie also eine schnelle und einfache Problemumgehung wünschen, machen Sie Folgendes:

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

Sie können es leider nicht einfach mit zwei Doppelpunkten machen

1
bdshadow

Ich habe einen anderen Ansatz mit einem Persistenz-Konverter:

import javax.persistence.Convert;

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

Dies ist eine Konverterdefinition:

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

Die Zuordnung zum psql-Enummentyp wird nicht aufgelöst, es kann jedoch @Enumerated (EnumType.STRING) oder @Enumerated (EnumType.ORDINAL) auf eine gute Weise simuliert werden.

Verwenden Sie für die Ordinalzahl direction.ordinal () und Direction.values ​​() [Anzahl].

0
Marko Novakovic

Lassen Sie mich zu Beginn sagen, dass ich dies mit Hibernate 4.3.x und Postgres 9.x tun konnte.

Ich habe meine Lösung auf etwas Ähnlichem basiert, was Sie getan haben. Ich glaube, wenn du kombinierst

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

und das

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

Sie sollten in der Lage sein, etwas in diese Richtung zu bekommen, ohne die oben genannten Änderungen vornehmen zu müssen.

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

Ich glaube, dass dies funktioniert, da Sie Hibernate im Wesentlichen sagen, dass das Enum einem anderen Typ zugeordnet werden soll (Types.OTHER == 1111). Es kann eine etwas spröde Lösung sein, da sich der Wert von Types.THER ändern kann. Dies würde jedoch insgesamt deutlich weniger Code liefern.

0
lew