Отображение гибернации между перечислением PostgreSQL и перечислением Java

Фон

  • Spring 3.x, JPA 2.0, Hibernate 4.x, Postgresql 9.x.
  • Работа над сопоставленным классом Hibernate с свойством enum, которое я хочу сопоставить с перечислением 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
);

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

Запрос xibernate xml:

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

Поиск проблемы

  • Запрос по id вместо enum работает, как ожидалось.
  • 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, аналогично примеру findByRating в Apache JPA и Enums через @Enumerated документацию, дает то же исключение.
  • Запросить в psql с помощью select * from move where direction = 'LEFT'; работает, как ожидалось.
  • Hardcoding, 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 как предлагается в этом принятом ответе qaru.site/info/316702/... а также:

    @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 как было предложено более высоким, но не принятым ответом qaru.site/info/316702/... по тому же вопросу, что и выше, а также:

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

    С двумя и без двух параметров после просмотра qaru.site/info/355864/...

  • Пробовал аннотировать геттер и сеттер, как в этом ответе qaru.site/info/355866/....
  • Не пробовал EnumType.ORDINAL потому что я хочу придерживаться EnumType.STRING, который менее хрупкий и более гибкий.

Другие примечания

Конвертер типа JPA 2.1 не должен быть необходимым, но это не вариант, поскольку я сейчас на JPA 2.0.

Ответы

Ответ 1

HQL

Правильное использование псевдонима и использование имени квалифицированной собственности было первой частью решения.

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

Отображение гибернации

@Enumerated(EnumType.STRING) все еще не работает, поэтому необходим пользовательский UserType. Ключ должен был правильно переопределить nullSafeSet как в этом ответе fooobar.com/questions/355863/... и аналогичный реализации из Интернета.

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

который, к сожалению, означает, что любое другое свойство enum, аналогично отображаемое, будет нуждаться в таком классе:

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.

  • В зависимости от того, как вы используете класс, это может быть не обязательно, чтобы сделать его postgres-специфичным, переопределив nullSafeGet в том, как это делали два связанных решения.
  • Если вы готовы отказаться от перечисления postgres, вы можете сделать столбец text, а исходный код будет работать без дополнительной работы.

Ответ 2

Вам не нужно создавать все следующие типы гибернации вручную. Вы можете просто получить их через Maven Central, используя следующую зависимость:

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

Для получения дополнительной информации ознакомьтесь с проектом open-source типа 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 тип перечисления post_status_info:

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

Что это, он работает как шарм. Вот тест на GitHub, который это доказывает.

Ответ 3

Как сказано в 8.7.3. Тип Безопасность документов Postgres:

Если вам действительно нужно сделать что-то подобное, вы можете либо написать пользовательский оператор, либо добавить явные приведения к вашему запросу:

поэтому, если вам нужно быстрое и простое обходное решение, выполните следующие действия:

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

К сожалению, вы не можете сделать это просто с двумя двоеточиями:

Ответ 4

Позвольте мне сказать, что я смог сделать это, используя 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 может измениться. Однако это обеспечит значительно меньше кода.

Ответ 5

У меня есть другой подход с конвертером постоянства:

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].