Рубрики
Без рубрики

Как решить проблему с оператором PostgreSQL :: cast с JPA и гибернацией

Узнайте, как использовать оператор PostgreSQL :: cast при написании запроса сущности JPQL с помощью JPA и спящего режима либо путем экранирования, либо с помощью функции CAST.

Автор оригинала: Vlad Mihalcea.

Вступление

Отвечая на вопросы на форуме Hibernate, я наткнулся на следующий вопрос, касающийся операции приведения PostgreSQL :: , используемой в JPA и запросе сущности Hibernate|/.

Поскольку это очень интересный вариант использования, я решил превратить ответ в специальный пост в блоге.

Модель предметной области

Учитывая, что у нас есть следующая организация:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @Column(name = "created_on")
    private LocalDateTime createdOn;

    //Getters and setters omitted for brevity
}

Как я объяснил в этой статье , Hibernate поддерживает типы даты и времени Java 1.8 начиная с версии 5.1. Теперь, когда они были включены в JPA 2.2, каждый поставщик JPA должен начать их поддерживать.

Теперь давайте сохраним следующие три Сообщения сущности:

Post part1 = new Post();
part1.setTitle("High-Performance Java Persistence, Part 1");
part1.setCreatedOn(
    LocalDateTime.now().with(
        TemporalAdjusters.previous(DayOfWeek.MONDAY)
    )
);
entityManager.persist(part1);

Post part2 = new Post();
part2.setTitle("High-Performance Java Persistence, Part 2");
part2.setCreatedOn(
    LocalDateTime.now().with(
        TemporalAdjusters.previous(DayOfWeek.TUESDAY)
    )
);
entityManager.persist(part2);

Post part3 = new Post();
part3.setTitle("High-Performance Java Persistence, Part 3");
part3.setCreatedOn(
    LocalDateTime.now().with(
        TemporalAdjusters.previous(DayOfWeek.THURSDAY)
    )
);
entityManager.persist(part3);

Первый Пост создается в понедельник, второй-во вторник, а последний-в четверг.

Проблема

Теперь мы хотели бы выполнить запрос, чтобы получить все объекты Post , соответствующие тому же дню недели, который мы указываем с помощью параметра запроса.

Для этого мы используем собственный SQL-запрос, потому что он подобен волшебной палочке .

Если вы работаете на PostgreSQL, вы можете использовать функцию date_part и привести параметр с помощью :: оператора преобразования типов .

List posts = entityManager.createNativeQuery(
    "SELECT * " +
    "FROM post " +
    "WHERE " +
    "   date_part('dow', created_on) = " +
    "   date_part('dow', :datetime::date)", Post.class)
.setParameter("datetime", Timestamp.valueOf(
    LocalDateTime.now().with(
        TemporalAdjusters.next(DayOfWeek.MONDAY)))
    )
.getResultList();

Однако при выполнении приведенного выше запроса Hibernate выдает следующее исключение:

java.lang.IllegalArgumentException: Unknown parameter name : datetime
    at org.hibernate.query.internal.QueryParameterBindingsImpl.getBinding(QueryParameterBindingsImpl.java:208)
    at org.hibernate.query.internal.AbstractProducedQuery.setParameter(AbstractProducedQuery.java:486)
    at org.hibernate.query.internal.NativeQueryImpl.setParameter(NativeQueryImpl.java:586)

Это связано с тем, что оператор приведения типа :: конфликтует с синтаксисом именованных параметров JPA : .

Итак, как мы можем это исправить?

Есть два способа решить эту проблему. Мы можем либо избежать символа : с помощью \ , либо использовать функцию CAST .

Побег из: персонажа

Если мы экранируем каждый из символов : с помощью \ , так что :: станет \:\: , то все будет работать просто отлично:

List posts = entityManager.createNativeQuery(
    "SELECT * " +
    "FROM post " +
    "WHERE " +
    "   date_part('dow', created_on) = " +
    "   date_part('dow', :datetime\\:\\:date)", Post.class)
.setParameter("datetime", Timestamp.valueOf(
    LocalDateTime.now().with(
        TemporalAdjusters.next(DayOfWeek.MONDAY)))
    )
.getResultList();

assertEquals(1, posts.size());
assertEquals(
    "High-Performance Java Persistence, Part 1", 
    posts.get(0).getTitle()
);

Использование функции ПРИВЕДЕНИЯ

Другой вариант устранения этой проблемы-использование функции PostgreSQL CAST .

List posts = entityManager.createNativeQuery(
    "SELECT * " +
    "FROM post " +
    "WHERE " +
    "   date_part('dow', created_on) = " +
    "   date_part('dow', cast(:datetime AS date))", Post.class)
.setParameter("datetime", Timestamp.valueOf(
    LocalDateTime.now().with(
        TemporalAdjusters.next(DayOfWeek.MONDAY)))
    )
.getResultList();

assertEquals(1, posts.size());
assertEquals(
    "High-Performance Java Persistence, Part 1", 
    posts.get(0).getTitle()
);

И это работает так, как и ожидалось.

Преобразование из строки

В качестве альтернативы мы также могли бы преобразовать значение из строки . Учтите, что мы получаем параметр запроса в виде Строки :

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(
    "dd-MM-YYYY"
);

String dateString = dateTimeFormatter.format(
    LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))
);

На этот раз вместо простого вызова функции ПРИВЕДЕНИЯ нам нужно использовать функцию TO_TIMESTAMP:

List posts = entityManager.createNativeQuery(
    "SELECT * " +
    "FROM post " +
    "WHERE " +
    "   date_part('dow', created_on) = " +
    "   date_part('dow', to_timestamp(:datetime, 'dd-MM-YYYY'))", Post.class)
.setParameter("datetime", dateString)
.getResultList();

assertEquals(1, posts.size());
assertEquals(
    "High-Performance Java Persistence, Part 1", 
    posts.get(0).getTitle()
);

И это работает как заклинание.

Вывод

Собственные SQL-запросы очень мощны, и именно поэтому вы должны их использовать. Однако несколько специфичных для базы данных операторов, таких как :: оператор преобразования типов, могут мешать синтаксису именованных параметров, специфичных для JPA.

Исправить эту проблему на самом деле очень просто, так как вы можете заменить рассматриваемый оператор эквивалентным вызовом функции SQL.