web-dev-qa-db-fra.com

Mappage Hibernate entre PostgreSQL enum et Java enum

Contexte

  • Spring 3.x, JPA 2.0, Hibernate 4.x, Postgresql 9.x.
  • Travailler sur une classe mappée Hibernate avec une propriété enum que je veux mapper sur une enum Postgresql.

Problème

L'interrogation avec une clause where dans la colonne enum lève une 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.

Code (fortement simplifié)

SQL:

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

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

Classe mappée 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 qui appelle la requête:

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

Requête XML Hibernate:

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

Dépannage

  • L'interrogation par id au lieu de l'énumération fonctionne comme prévu.
  • Java sans interaction avec la base de données fonctionne bien:

    public List<Move> getMoves(Direction directionToMove) {
        List<Move> moves = new ArrayList<>();
        Move move1 = new Move();
        move1.setDirection(directionToMove);
        moves.add(move1);
        return moves;
    }
    
  • createQuery au lieu d'avoir la requête en XML, similaire à l'exemple findByRating dans JPA d'Apache et Enums via @Enumerated documentation a donné la même exception.
  • L'interrogation dans psql avec select * from move where direction = 'LEFT'; fonctionne comme prévu.
  • Hardcoding where direction = 'FORWARD' dans la requête dans les travaux XML.
  • .setParameter("direction", direction.name()) ne fonctionne pas, comme avec .setString() et .setText(), les exceptions se modifient comme suit:

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

Tentatives de résolution

  • Personnalisé UserType comme suggéré par cette réponse acceptée https://stackoverflow.com/a/1594020/1090474 avec:

    @Column(name = "direction", nullable = false)
    @Enumerated(EnumType.STRING) // tried with and without this line
    @Type(type = "full.path.to.HibernateMoveDirectionUserType")
    private Direction directionToMove;
    
  • Mappage avec EnumType d'Hibernate comme suggéré par une réponse mieux notée mais non acceptée https://stackoverflow.com/a/1604286/1090474 de la même question que ci-dessus, avec:

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

    Avec et sans les deux seconds paramètres, après avoir vu https://stackoverflow.com/a/13241410/1090474

  • Essayé d'annoter le getter et le setter comme dans cette réponse https://stackoverflow.com/a/20252215/1090474 .
  • Je n'ai pas essayé EnumType.ORDINAL parce que je veux m'en tenir à EnumType.STRING, qui est moins fragile et plus flexible.

Autres notes

Un convertisseur de type JPA 2.1 ne devrait pas être nécessaire, mais ce n'est pas une option, car je suis pour l'instant sur JPA 2.0.

20
Kenny Linsky

HQL

L'aliasing correct et l'utilisation du nom de propriété qualifié constituaient la première partie de la solution.

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

Mappage Hibernate

@Enumerated(EnumType.STRING) ne fonctionnait toujours pas, donc une UserType personnalisée était nécessaire. La clé consistait à remplacer correctement nullSafeSet comme dans cette réponse https://stackoverflow.com/a/7614642/1090474 et similaireimplémentations à partir du Web.

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

Deviation

implements ParameterizedType n'a pas coopéré:

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

donc je n'ai pas pu annoter la propriété enum comme ceci:

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

Au lieu de cela, j'ai déclaré la classe comme suit:

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

avec un constructeur:

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

ce qui, malheureusement, signifie que toute autre propriété enum mappée de la même manière aura besoin d'une classe comme celle-ci:

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

Annotation

Annotez la propriété et vous avez terminé.

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

Autres notes

  • EnhancedUserType et les trois méthodes qu'il souhaite implémenter

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

    je n'ai vu aucune différence, je me suis donc retrouvé avec implements UserType.

  • En fonction de votre utilisation de la classe, il peut ne pas être strictement nécessaire de la rendre spécifique à postgres en surchargeant nullSafeGet de la même manière que les deux solutions liées.
  • Si vous êtes prêt à abandonner l'énumération postgres, vous pouvez créer la colonne text et le code d'origine fonctionnera sans travail supplémentaire.
8
Kenny Linsky

Il n'est pas nécessaire de créer manuellement tous les types Hibernate suivants. Vous pouvez simplement les obtenir via Maven Central en utilisant la dépendance suivante:

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

Pour plus d'informations, consultez le projet open-source hibernate-types .

Comme _ expliqué dans cet article , si vous mappez facilement Java Enum à un type de colonne PostgreSQL Enum à l'aide du type personnalisé suivant:

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

Pour l'utiliser, vous devez annoter le champ avec l'annotation Hibernate @Type, comme illustré dans l'exemple suivant:

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

Ce mappage suppose que vous avez le type post_status_info enum dans PostgreSQL:

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

Ça y est, ça marche à merveille. Voici un test sur GitHub qui le prouve .

17
Vlad Mihalcea

Comme indiqué dans 8.7.3. Type Sécurité des documents Postgres :

Si vous avez vraiment besoin de faire quelque chose comme ça, vous pouvez écrire un opérateur personnalisé ou ajouter des conversions explicites à votre requête:

donc si vous voulez une solution de contournement rapide et simple, procédez comme suit:

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

Malheureusement, vous ne pouvez pas le faire simplement avec deux colons

1
bdshadow

J'ai une autre approche avec un convertisseur de persistance:

import javax.persistence.Convert;

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

Ceci est une définition de convertisseur:

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

Il ne résout pas le mappage vers le type psum enum, mais il peut simuler correctement @Enumerated (EnumType.STRING) ou @Enumerated (EnumType.ORDINAL).

Pour ordinal, utilisez direction.ordinal () et Direction.values ​​() [nombre].

0
Marko Novakovic

Permettez-moi de commencer en disant que j'ai pu le faire avec Hibernate 4.3.x et Postgres 9.x.

J'ai basé ma solution sur quelque chose de similaire à ce que vous avez fait. Je crois que si vous combinez

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

et ça

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

Vous devriez pouvoir obtenir quelque chose dans le même ordre, sans avoir à apporter les modifications ci-dessus.

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

Je pense que cela fonctionne puisque vous demandez essentiellement à Hibernate de mapper l'énum sur un type d'autre (Types.OTHER == 1111). Il peut s’agir d’une solution légèrement fragile, car la valeur de Types.OTHER pourrait changer. Cependant, cela fournirait beaucoup moins de code dans l'ensemble.

0
lew