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:
- Есть ли способ обхода/решения для поддержки полей даты 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) вместо этого измените свой тип атрибута на "Объект", чтобы все типы параметров, которые вы могли пройти, перейдут в ваш конвертер.