Что меня всю жизнь огорчало в Java, так это работа с датами и временем. Ниже типичный пример того, как дату из базы данных преобразовать в строку определенного формата.
Листинг 1 Ничего сложного - всего две строки кода. Однако, кажется странным, что сам тип java.sql.Date не содержит в себе метода преобразования в строку согласно заданному формату. Это значит, что программист должен помнить, что для преобразования даты в строку нужно использовать класс SimpleDateFormat.
Примечательны и две первые строки из Листинга 1. Чтобы получить тестовое значение, нам пришлось манипулировать классом Calendar. И это тоже надо помнить. А еще надо помнить, что месяцы в конструкторе Calendar начинаются с 0. Ну мы же программисты, в конце концов, а не обычные люди - нам в кайф считать месяцы с 0!
Но это только начало боли. Известно, что в Java два типа Date. Один в пакете java.util, второй - в java.sql. Из-за этого часто приходится писать полное имя типа, чтобы понимать, какой из них мы используем. Чем же они отличаются? А тем, что java.util.Date содержит дату и время, а java.sql.Date - дату без времени. А если в базе данных нужно сохранить дату со временем? О, для этого у нас есть java.sql.Timestamp! Логично, не правда ли? Ну и для полноты картины нужно упомянуть тип java.sql.Time, который должен использоваться для времени без даты. Следует понимать, что все упомянутые типы из пакета java.sql наследуются от java.util.Date.
Но и это еще не всё. Главная проблема в том, что java.util.Date - это момент времени в часовом поясе UTC. И до Java 8 в ядре не было типов, предназначенных для локальных дат и времени. К примеру, дата рождения - это локальная дата. Никого ведь не интересует, в каком часовом поясе вы родились! И программисты были вынуждены использовать тип Date и его вариации для хранения локальных дат. И собирали по этому поводу одни и те же грабли.
Что за грабли? Вот записали мы в базу данных дату рождения 1970-05-02. А база данных находится в Новосибирске, в часовом поясе UTC+7. И это значит, что в базе лежит значение 1970-05-02 00:00:00 UTC+7. Теперь представим, что это значение интерпретирует приложение в Москве. Так вот, для него дата внезапно станет 1970-05-01, потому что когда в Новосибирске была полночь 1970-05-02, в Москве было 1970-05-01 21:00:00! Лично я и мои коллеги неоднократно нарывались на эту или подобные ошибки. Потому что это ведь тоже надо постоянно помнить. И это та вещь, которая маниакально норовит спрятаться в памяти.
Вообще, сейчас мне кажется странным, почему не прижилось хранение в качестве локальных дат в строках в ISO-формате. Их и сортировать можно, и сравнивать. Понятно, что в этом случае приходится руками делать проверку достоверности значения, но это не большая плата за отсутствие упомянутых граблей.
Ладно, это дело прошлое. В Java 8 появился пакет java.time, который закрывает эту зияющую дыру в языке. Теперь в нашем распоряжении есть типы для хранения локальных дат и времени: LocalDateTime, LocalDate и LocalTime. Они - замечательные. Следующий код доставляет эстетическое наслаждение!
Листинг 2 Правда, замечательно? Однако, радость быстро проходит, когда обнаруживаешь, что тот же Hibernate не поддерживают эти типы. Восторга как не бывало! На этом можно пожать плечами и продолжать использовать старые недобрые классы java.sql. Как многие, кстати, и делают. А можно копнуть чуть глубже и обнаружить, что новые типы-таки можно использовать в хранимых классах. Для этого нужно подключить вместо библиотеки hibernate-core альтернативную библиотеку hibernate-java8. Ниже фрагменты соответствующих конфигураций для Maven и Gradle в случае приложения на Spring Boot.
Листинг 3
Листинг 4
Вторая проблема заключается в том, что конкретная база данных может не поддерживать локальных дат (например, Oracle). Является ли это проблемой? Допустим, сохраняем мы в поле базы данных типа DATE локальную дату 1970-05-02. База данных - в Новосибирске. Соответственно, там будет значение 1970-05-02 00:00:00 UTC+7. И пока приложение работает тоже по новосибирскому времени, проблем не будет: записал в базу 1970-05-02, прочел из базы - тоже 1970-05-02. Всё хорошо! На самом деле, я ни разу не видел, чтобы база данных и серверное приложение, работающее с ней, работали в разных часовых поясах. Но вот на разных хостах - это как правило. И если возникает смена часового пояса, чем любят заниматься власти, то можно и словить проблему. Это если админы решат менять часовой пояс на горячую, и какое-то время база данных и серверное приложение будут работать в разных часовых поясах.
К сожалению, поддержка типов java.time в JDBC-драйверах не первоклассная. Под капотом, как можно догадаться, происходит конвертация в типы java.sql и обратно. Класс ResultSet не имеет методов getLocalDate, getLocalDateTime и т.д. Приходится использовать вместо них универсальный getObject.
К слову, если у нас REST-приложение опять же на Spring Boot 1.x, то придется заменить библиотеку jackson-datatype на jackson-datatype-jsr310. Ниже соответствующие фрагменты для Maven и Gradle.
Листинг 5
Листинг 6 Придется также задавать формат даты в аннотации к полю класса модели DTO. Иначе дата предстанет в неожиданном виде.
Листинг 7 Кстати, пакет java.time содержит замены классам java.sql.Timestamp и java.sql.Time: OffsetDateTime и OffsetTime соответственно. Использовать старые типы теперь нет смысла. В пакете java.time есть абсолютно всё необходимое для работы с датой и временем. Хотя и там не обошлось без странностей.
И в заключение позволю себе дать совет. Следует разделять временные типы на абсолютные и локальные. И если тип локальный, то он должен быть везде представлен как таковой. Например, день рождения должен передаваться в клиентское приложение без часового пояса. И уж точно не в unix-time.
Листинг 1 Ничего сложного - всего две строки кода. Однако, кажется странным, что сам тип java.sql.Date не содержит в себе метода преобразования в строку согласно заданному формату. Это значит, что программист должен помнить, что для преобразования даты в строку нужно использовать класс SimpleDateFormat.
Примечательны и две первые строки из Листинга 1. Чтобы получить тестовое значение, нам пришлось манипулировать классом Calendar. И это тоже надо помнить. А еще надо помнить, что месяцы в конструкторе Calendar начинаются с 0. Ну мы же программисты, в конце концов, а не обычные люди - нам в кайф считать месяцы с 0!
Но это только начало боли. Известно, что в Java два типа Date. Один в пакете java.util, второй - в java.sql. Из-за этого часто приходится писать полное имя типа, чтобы понимать, какой из них мы используем. Чем же они отличаются? А тем, что java.util.Date содержит дату и время, а java.sql.Date - дату без времени. А если в базе данных нужно сохранить дату со временем? О, для этого у нас есть java.sql.Timestamp! Логично, не правда ли? Ну и для полноты картины нужно упомянуть тип java.sql.Time, который должен использоваться для времени без даты. Следует понимать, что все упомянутые типы из пакета java.sql наследуются от java.util.Date.
Но и это еще не всё. Главная проблема в том, что java.util.Date - это момент времени в часовом поясе UTC. И до Java 8 в ядре не было типов, предназначенных для локальных дат и времени. К примеру, дата рождения - это локальная дата. Никого ведь не интересует, в каком часовом поясе вы родились! И программисты были вынуждены использовать тип Date и его вариации для хранения локальных дат. И собирали по этому поводу одни и те же грабли.
Что за грабли? Вот записали мы в базу данных дату рождения 1970-05-02. А база данных находится в Новосибирске, в часовом поясе UTC+7. И это значит, что в базе лежит значение 1970-05-02 00:00:00 UTC+7. Теперь представим, что это значение интерпретирует приложение в Москве. Так вот, для него дата внезапно станет 1970-05-01, потому что когда в Новосибирске была полночь 1970-05-02, в Москве было 1970-05-01 21:00:00! Лично я и мои коллеги неоднократно нарывались на эту или подобные ошибки. Потому что это ведь тоже надо постоянно помнить. И это та вещь, которая маниакально норовит спрятаться в памяти.
Вообще, сейчас мне кажется странным, почему не прижилось хранение в качестве локальных дат в строках в ISO-формате. Их и сортировать можно, и сравнивать. Понятно, что в этом случае приходится руками делать проверку достоверности значения, но это не большая плата за отсутствие упомянутых граблей.
Ладно, это дело прошлое. В Java 8 появился пакет java.time, который закрывает эту зияющую дыру в языке. Теперь в нашем распоряжении есть типы для хранения локальных дат и времени: LocalDateTime, LocalDate и LocalTime. Они - замечательные. Следующий код доставляет эстетическое наслаждение!
Листинг 2 Правда, замечательно? Однако, радость быстро проходит, когда обнаруживаешь, что тот же Hibernate не поддерживают эти типы. Восторга как не бывало! На этом можно пожать плечами и продолжать использовать старые недобрые классы java.sql. Как многие, кстати, и делают. А можно копнуть чуть глубже и обнаружить, что новые типы-таки можно использовать в хранимых классах. Для этого нужно подключить вместо библиотеки hibernate-core альтернативную библиотеку hibernate-java8. Ниже фрагменты соответствующих конфигураций для Maven и Gradle в случае приложения на Spring Boot.
Листинг 3
Листинг 4
Вторая проблема заключается в том, что конкретная база данных может не поддерживать локальных дат (например, Oracle). Является ли это проблемой? Допустим, сохраняем мы в поле базы данных типа DATE локальную дату 1970-05-02. База данных - в Новосибирске. Соответственно, там будет значение 1970-05-02 00:00:00 UTC+7. И пока приложение работает тоже по новосибирскому времени, проблем не будет: записал в базу 1970-05-02, прочел из базы - тоже 1970-05-02. Всё хорошо! На самом деле, я ни разу не видел, чтобы база данных и серверное приложение, работающее с ней, работали в разных часовых поясах. Но вот на разных хостах - это как правило. И если возникает смена часового пояса, чем любят заниматься власти, то можно и словить проблему. Это если админы решат менять часовой пояс на горячую, и какое-то время база данных и серверное приложение будут работать в разных часовых поясах.
К сожалению, поддержка типов java.time в JDBC-драйверах не первоклассная. Под капотом, как можно догадаться, происходит конвертация в типы java.sql и обратно. Класс ResultSet не имеет методов getLocalDate, getLocalDateTime и т.д. Приходится использовать вместо них универсальный getObject.
К слову, если у нас REST-приложение опять же на Spring Boot 1.x, то придется заменить библиотеку jackson-datatype на jackson-datatype-jsr310. Ниже соответствующие фрагменты для Maven и Gradle.
Листинг 5
Листинг 6 Придется также задавать формат даты в аннотации к полю класса модели DTO. Иначе дата предстанет в неожиданном виде.
Листинг 7 Кстати, пакет java.time содержит замены классам java.sql.Timestamp и java.sql.Time: OffsetDateTime и OffsetTime соответственно. Использовать старые типы теперь нет смысла. В пакете java.time есть абсолютно всё необходимое для работы с датой и временем. Хотя и там не обошлось без странностей.
И в заключение позволю себе дать совет. Следует разделять временные типы на абсолютные и локальные. И если тип локальный, то он должен быть везде представлен как таковой. Например, день рождения должен передаваться в клиентское приложение без часового пояса. И уж точно не в unix-time.
Комментарии
Отправить комментарий