К основному контенту

Как работать с датами и временем в Java

Что меня всю жизнь огорчало в 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.

Комментарии

Популярные сообщения из этого блога

Велостанок Tacx Vortex Smart и внешний мощемер

Весной 2019-го я установил на свой шоссейный велосипед мощемер Stages Shimano 105 в виде левого шатуна. Согласно плану, он должен был помочь мне в прохождении полной дистанции триатлона в Сочи осенью того же года. В этом состязании важно сохранить свежие ноги после 180-километровой шоссейной гонки для того, чтобы следом пробежать 42 км. Идея состояла в том, чтобы рассчитать правильную мощность педалирования и строго придерживаться ее во время гонки. Чтобы контролировать мощность, надо на нее где-то смотреть. Можно выводить показание на спортивные часы. Однако, у меня возникли сильные сомнения, что заряда моих Garmin Forerunner 230 хватит на всю гонку. Поэтому я заблаговременно приобрел велокомпьютер Wahoo Elemnt Bolt. Говорят, один из лучших в своем классе. Tacx Vortex Smart - станок умный, что очевидно из названия. Соответственно, в нем уже есть мощемер. Интуитивно было понятно, что внешний мощемер показывает мощность, которая отличается от мощности станка. А это значит, что и на...

Лёгкий пар в "Здрава"

Сходили семьёй в русскую  баню "Здрава" . 3 года я искал в Новосибирске и окрестностях, а также на Алтае, настоящую русскую баню. Ходил в разные бани, включая общественные "Фёдоровские" и "Сандуны". Всё не то. И вот нашел! Спасибо  Людмиле Богачевой, моему давневу другу,  за наводку! Баня находится в Бердске, в поселке Вега. Понравилось в описании на сайте, что в бане не принято пить, курить и заниматься прочими безобразиями. День выбрали будний, благо работаю по гибкому графику. В будни дешевле. Общение с Женей, хозяйкой бани (язык не поворачивается назвать ее администратором), началось заблаговременно и оставило приятное впечатление. Обрисовал, что в баню хотим пойти с 5-месячной крохой, что для неё это будет первый заход. Изначально планировали в прошлый четверг, но в связи с морозами Женя убедила перенести на неделю - беспокоилась, что дочке будет не совсем комфортно в комнате отдыха в экстремальный холод снаружи.  Встретил нас Костя, хозяин бани. Всё ...

Belovo Olympic Triathlon 2019

11 месяцев назад в Белово у меня был первый триатлонный старт (отчёт  здесь ). В этом году старт перенесли с июля на июнь. И теперь это был этап Кубка России - звучит гордо и волнительно. Старт хорошо вписался в мой график подготовки - ровно 4 недели до половинки в Астане/Нур-Султане. В терминах "Библии триатлета" старт в Белово был отнесен мною к категории C, без подводки. К соревнованию я подошел в не лучшем состоянии: травма спины (падение на велосипеде), больной бок (падение на теннисном корте), крепатура в ногах (силовая и вело- тренировки во вторник), периодические судороги ног и насморк с болью в горле (кондиционер на работе). Впрочем, всё ерунда, кроме пчёл. Да и пчёлы - ерунда. Но их много! Предварительно удалось дважды поплавать в гидраче. Вода в Беловском водохранилище еще холодная. Ну и большой плюс в том, что успел накататься на улице - это добавило уверенности на велоэтапе. На неделе перед стартом отдал велосипед на ТО в Райд Сервис . Нужно было попра...