JPA 2.1: Знакомство с API Java 8 Дата/Время

Я хочу добавить поддержку для Java 8 Date/Time API (JSR-310) в моем приложении с поддержкой JPA.

Ясно, что JPA 2.1 не поддерживает API-интерфейс Java 8 Date/Time.
В качестве обходного пути наиболее распространенным советом является использование AttributeConverter.

В моем существующем приложении я изменил свои сущности, чтобы использовать типы LocalDate/LocalDateTime для полей сопоставления столбцов и добавить для них устаревшие setter/getters для java.util.Date.
Я создал соответствующие классы AttributeConverter.

Мое приложение теперь терпит неудачу при использовании Query.setParameter() с экземплярами java.util.Date (оно работало до перехода на новый API).
Похоже, JPA ожидает новых типов дат и не конвертирует их на лету.

Я ожидал, что если передать аргумент в setParameter() типа, для которого был зарегистрирован AttributeConverter, он будет автоматически преобразован конвертером. Но это, похоже, не так, по крайней мере, не используя EclipseLink 2.6.2:

java.lang.IllegalArgumentException: You have attempted to set a value of type class java.util.Date for parameter closeDate with expected type of class java.time.LocalDate from query string SELECT obj FROM [...]
    at org.eclipse.persistence.internal.jpa.QueryImpl.setParameterInternal(QueryImpl.java:937) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
    at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:593) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
    at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:1) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
  [...]


Вопросы

  • Является ли это поведение ожидаемым? Я что-то пропустил?
  • Существует ли способ использовать новые типы дат как поля без нарушения существующего кода?
  • Как вы применили с переходом на новый API Date/Time в JPA?


ОБНОВЛЕНИЕ:

Однако, похоже, что по крайней мере с использованием EclipseLink нестандартные типы, для которых существует AttributeConverter, не полностью поддерживаются:

В рамках запросов JPQL ни один тип поля или преобразованный тип базы данных не могут использоваться в качестве параметра. При использовании преобразованного типа базы данных происходит описанное выше исключение. При использовании фактического типа поля (например, LocalDate) он напрямую передается драйверу jdbc, который не знает этого типа:

Caused by: java.sql.SQLException: Invalid column type
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectCritical(OraclePreparedStatement.java:10495) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:9974) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:10799) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObject(OraclePreparedStatement.java:10776) 
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.setObject(OraclePreparedStatementWrapper.java:241)
    at org.eclipse.persistence.internal.databaseaccess.DatabasePlatform.setParameterValueInDatabaseCall(DatabasePlatform.java:2506)

Я бы ожидал, что EclipseLink преобразует тип поля в тип java.sql с помощью AttributeConverter. (см. также этот отчет об ошибке: https://bugs.eclipse.org/bugs/show_bug.cgi?id=494999)

Это приводит нас к самому важному вопросу № 4:

  1. Есть ли способ обхода/решения для поддержки полей даты java 8 с использованием EclipseLink, , включая возможность использования параметров запроса в таком поле?

ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ

Ответ 1

Некоторое время назад я преобразовал веб-приложение Java EE 7 из Java 7 в Java 8 и заменил java.util.Date на сущности с помощью LocalDate и LocalDateTime.

  • Да, это поведение ожидается, потому что AttributeConverter преобразует только тип, используемый для полей сущности и типа базы данных (т.е. java.sql.Date и т.д.); он не конвертирует между типом поля сущности и java.util.Date, используемым в параметре запроса.
  • Насколько я знаю, нет, нет способа продолжить использование java.util.Date в существующем коде, после ввода типов java.time в объекты JPA.
  • Помимо создания необходимых реализаций AttributeConverter, я изменил все вхождения java.util.Date на соответствующие типы java.time не только на сущности, но и на запросы JPA-QL и в бизнес-методах.

Для пункта 2, конечно, вы можете пойти каким-то образом, используя методы утилиты и getters/seters, которые конвертируются между java.util и java.time, но это не будет идти полным ходом. Что еще более важно, я не совсем понимаю смысл введения типов java.time в атрибуты сущности JPA, если вы не хотите конвертировать оставшийся код, который использует эти атрибутов. После работы по конверсии, которую я сделал в этом приложении Java EE, не было никакого использования java.util.Date слева (хотя мне также приходилось создавать конвертеры для JSF).

Ответ 2

С таким количеством ошибок в самом провайдере я не думаю, что у вас есть выбор, но использовать java.util.Date на уровне отображения и датах java 8 на уровне API.

Предполагая, что вы пишете служебный класс для преобразования в/из java.util дат под названием DateUtils, вы можете определить свои сопоставления следующим образом:

@Entity
public class MyEntity {

  @Column("DATE")
  private Date date; // java.util.Date

  public void setDate(LocalDateTime date) {
    this.date = DateUtils.convertToDate(date);
  }

  public LocalDateTime getDate() {
    return DateUtils.convertFromDate(date);
  }
}

Затем для фильтрации по date в JPQL:

public List<MyEntity> readByDateGreaterThan(LocalDateTime date) {
  Query query = em.createQuery("select e from MyEntity e where e.date > :date");
  query.setParameter("date", DateTuils.convertToDate(date));
  return query.getResultList();
}

Итак, даты java.util будут использоваться внутри сущностей и DAO (репозиториев) внутри, тогда как API, открытый сущностями и DAO, будет принимать/возвращать даты java 8, тем самым позволяя остальной части приложения работать с датами java 8 только.

Ответ 3

У меня есть следующая настройка:

  • EclipseLink v2.6.2
  • h2 База данных v1.4.191
  • Java 8

Класс сущности выглядит следующим образом:

@Entity
public class MeasuringPoint extends BaseEntity {

    @Column(nullable = false)
    private LocalDateTime when;

    public void setWhen(LocalDateTime when) {
        this.when = when;
    }

    public LocalDateTime getWhen() {
        return when;
    }

}

Необходимым конвертером для JPA 2.1 является:

@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(LocalDateTime attribute) {
        return attribute == null ? null : Timestamp.valueOf(attribute);
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Timestamp dbData) {
        return dbData == null ? null : dbData.toLocalDateTime();
    }

}

Теперь я могу сделать запрос

List<?> result = em.createQuery(
         "SELECT p FROM MeasuringPoint p WHERE p.when = :custDate")
         .setParameter("custDate", LocalDateTime.now())
         .getResultList();

и он работает как шарм. Результат содержит ожидаемые объекты. Преобразование в TIMESTAMP выполняется автоматически. Когда у вас есть запросы с использованием API критериев, просмотрите этот ответ, в котором показано, как использовать LocalDateTime в запросах API критериев.

Интересно, почему он не работает с вашим кодом. Возможно, драйвер H2 JDBC поддерживает то, что не делает ваш Oracle.

Ответ 4

AttributeConverter работает как сконструированный, так как конвертер предназначен для обработки назад и вперед между типом объекта и типом базы данных. Проверка проверяет, что тип параметра не соответствует типу внутри объекта - только потому, что ваш преобразователь атрибутов может обрабатывать его, не означает, что он соответствует контракту типов конвертеров атрибутов. JPA только говорит, что он перейдет через конвертер, прежде чем перейти в базу данных, и в этом случае он не переходит в базу данных.

Если вам не нравятся предложения Rogério, вы можете 1) измените код EclipseLink, чтобы облегчить проверку, чтобы он мог перейти в ваш конвертер или 2) вместо этого измените свой тип атрибута на "Объект", чтобы все типы параметров, которые вы могли пройти, перейдут в ваш конвертер.