Несколько дней назад я столкнулся с проблемой при работе с атрибутом LocalDateTime в JPA. В этом посте я попытаюсь создать пример проблемы, чтобы объяснить проблему, а также решение, которое я использовал.
Рассмотрим следующую сущность, которая моделирует сотрудника определенной компании:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
@Entity@Getter@Setterpublic class Employee { @Id @GeneratedValue private Long id; private String name; private String department; private LocalDateTime joiningDate;} |
Я использовал Spring Data JPA, поэтому создал следующий репозиторий —
|
1
2
3
4
5
|
@Repositorypublic interface EmployeeRepository extends JpaRepository<Employee, Long> {} |
Я хотел найти всех сотрудников, которые присоединились к компании на определенную дату. Для этого я расширил свой репозиторий из
JpaSpecificationExecutor —
|
1
2
3
4
5
6
|
@Repositorypublic interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {} |
и написал запрос, как показано ниже —
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@SpringBootTest@RunWith(SpringRunner.class)@Transactionalpublic class EmployeeRepositoryIT { @Autowired private EmployeeRepository employeeRepository; @Test public void findingEmployees_joiningDateIsZeroHour_found() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime joiningDate = LocalDateTime.parse("2014-04-01 00:00:00", formatter); Employee employee = new Employee(); employee.setName("Test Employee"); employee.setDepartment("Test Department"); employee.setJoiningDate(joiningDate); employeeRepository.save(employee); // Query to find employees List<Employee> employees = employeeRepository.findAll((root, query, cb) -> cb.and( cb.greaterThanOrEqualTo(root.get(Employee_.joiningDate), joiningDate), cb.lessThan(root.get(Employee_.joiningDate), joiningDate.plusDays(1))) ); assertThat(employees).hasSize(1); }} |
Вышеуказанный тест прошел без проблем. Однако следующий тест не удался (который должен был пройти) —
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Testpublic void findingEmployees_joiningDateIsNotZeroHour_found() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime joiningDate = LocalDateTime.parse("2014-04-01 08:00:00", formatter); LocalDateTime zeroHour = LocalDateTime.parse("2014-04-01 00:00:00", formatter); Employee employee = new Employee(); employee.setName("Test Employee"); employee.setDepartment("Test Department"); employee.setJoiningDate(joiningDate); employeeRepository.save(employee); List<Employee> employees = employeeRepository.findAll((root, query, cb) -> cb.and( cb.greaterThanOrEqualTo(root.get(Employee_.joiningDate), zeroHour), cb.lessThan(root.get(Employee_.joiningDate), zeroHour.plusDays(1)) ) ); assertThat(employees).hasSize(1);} |
Единственное, что отличается от предыдущего теста, это то, что в предыдущем тесте я использовал нулевой час в качестве даты присоединения, а здесь я использовал 8 часов утра. Сначала это показалось мне странным. Испытания, казалось, проходили всякий раз, когда дата присоединения сотрудника была установлена на ноль часов в день, но терпели неудачу, когда она была установлена в любое другое время.
Чтобы исследовать проблему, я включил запись в спящий режим, чтобы увидеть фактический запрос и значения, отправляемые в базу данных, и заметил что-то подобное в журнале —
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
2017-03-05 22:26:20.804 DEBUG 8098 --- [ main] org.hibernate.SQL:selectemployee0_.id as id1_0_,employee0_.department as departme2_0_,employee0_.joining_date as joining_3_0_,employee0_.name as name4_0_fromemployee employee0_whereemployee0_.joining_date>=?and employee0_.joining_dateHibernate:selectemployee0_.id as id1_0_,employee0_.department as departme2_0_,employee0_.joining_date as joining_3_0_,employee0_.name as name4_0_fromemployee employee0_whereemployee0_.joining_date>=?and employee0_.joining_date2017-03-05 22:26:20.806 TRACE 8098 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARBINARY] - [2014-04-01T00:00]2017-03-05 22:26:20.807 TRACE 8098 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARBINARY] - [2014-04-02T00:00] |
Было очевидно, что JPA НЕ рассматривал атрибут joiningDate как дату или время, а как тип VARBINARY . Вот почему сравнение с фактической датой провалилось.
На мой взгляд, это не очень хороший дизайн. Вместо того, чтобы выдавать что-то вроде UnsupportedAttributeException или чего-то еще, он молча пытался преобразовать значение во что-то другое, и, таким образом, провал сравнения случайным образом (ну, не совсем случайным). Ошибки такого типа трудно найти в приложении, если у вас нет сильного набора автоматических тестов, что, к счастью, было моим случаем.
Вернемся к проблеме сейчас. Причина, по которой JPA не удалось соответствующим образом преобразовать LocalDateTime, была очень проста. Последняя версия спецификации JPA (то есть 2.1) была выпущена до Java 8, и в результате она не может обрабатывать новый API даты и времени.
Чтобы решить эту проблему, я создал собственную реализацию конвертера, которая преобразует LocalDateTime в
java.sql. Timestamp перед сохранением его в базу данных, и наоборот. Это решило проблему —
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Converter(autoApply = true)public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Timestamp> { @Override public Timestamp convertToDatabaseColumn(LocalDateTime localDateTime) { return Optional.ofNullable(localDateTime) .map(Timestamp::valueOf) .orElse(null); } @Override public LocalDateTime convertToEntityAttribute(Timestamp timestamp) { return Optional.ofNullable(timestamp) .map(Timestamp::toLocalDateTime) .orElse(null); }} |
Приведенный выше конвертер будет автоматически применяться всякий раз, когда я пытаюсь сохранить атрибут LocalDateTime. Я мог также явно пометить атрибуты, которые я хотел преобразовать явно, используя
javax.persistence.Convert annotation —
|
1
2
|
@Convert(converter = LocalDateTimeConverter.class)private LocalDateTime joiningDate; |
Полный код доступен на Github .
| Ссылка: | Работа с Java LocalDateTime в JPA от нашего партнера по JCG Саима Ахмеда в блоге Codesod . |