Современное состояние дел со временем

Со времен, когда придумали JDBC и стандартизировали его интерфейсы, прошло лет 20, и за это время много чего изменилось.

Во-первых, мир стал глобальным и теперь один сервер может обслуживать пользователей со всего мира. Скорость интернета порешала. Поэтому в SQL был добавлен еще один тип данных для работы со временем. Теперь типы выглядят вот так:

  • DATE – хранит дату: год, месяц, день.
  • TIME – хранит время: часы, минуты, секунды.
  • TIMESTAMP – хранит конкретный момент времени: дата, время и миллисекунды.
  • TIMESTAMP WITH TIME ZONE – TIMESTAMP и временная зона (имя зоны или смещение).

Во-вторых, в Java появился DateTime API для глобальной работы со временем. В нем появились такие классы:

  • Дата и время:
    • LocalDate
    • LocalTime
  • Точное мгновение:
    • java.time.Instant
    • java.time.LocalDateTime
    • java.time.OffsetDateTime
    • java.time.ZonedDateTime
  • Время с часовым поясом:
    • java.time.OffsetDateTime
    • java.time.ZonedDateTime

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

Например, я хочу получить из базы все задания на сегодня. У SQL-сервера есть функция CURDATE() для этого дела. Только вот сервер находится в США, а я – в Японии. И хотелось бы чтобы он мне вернул все записи за “мое сегодня”, а не “его сегодня”.

В общем, SQL-сервер тоже должен уметь по-умному работать с клиентами в разных временных зонах.

Современные проблемы требуют современных решений

В принципе, новые типы из Java DateTime API и типы из SQL можно удобно сопоставлять. Для того чтобы представить тип DATE в Java нужно использовать класс java.time.LocalDate из JDK 8 DateTime API.

Тип TIME из базы данных можно представить двумя типами из Java: java.time.LocalTime и java.time.OffsetTime. Тоже ничего сложного.

Конкретный момент времени, представленный типом TIMESTAMP в базе, в Java можно представить 4 типами:

  • java.time.Instant
  • java.time.LocalDateTime
  • java.time.OffsetDateTime
  • java.time.ZonedDateTime

Ну, и наконец, TIMESTAMP WITH TIME ZONE можно представить двумя типами:

  • java.time.OffsetDateTime
  • java.time.ZonedDateTime

Так как ты уже знаком с DateTime API, то запомнить это дело тебе труда не составит :)

Запишу в виде таблицы, так будет проще:

SQL TYPE Java Type
DATE java.time.LocalDate
TIME java.time.LocalTime
java.time.OffsetTime
TIMESTAMP java.time.Instant
java.time.LocalDateTime
java.time.OffsetDateTime
java.time.ZonedDateTime
TIMESTAMP WITH TIME ZONE java.time.OffsetDateTime
java.time.ZonedDateTime

Получение даты

У меня для тебя хорошая новость. Первая за долгое время. Мы можем обойти ограничение метода getDate(), который возвращает тип java.sql Date.

Дело в том, что у объекта ResultSet есть еще один интересный метод — getObject(). Этот метод принимает два параметра: колонку и тип, и возвращает значение колонки, преобразованное к заданному типу. Общий вид метода такой:


ИмяКласса имя = getObject(column, ИмяКласса);

И если вы хотите преобразовать тип DATE к типу java.time.LocalDate, то нужно написать что-то типа:


  LocalDate localDate = results.getObject(4, LocalDate.class);

А любой TIMESTAMP вообще можно преобразовать к куче типов:


  java.time.Instant instant = results.getObject(9, java.time.Instant.class);
  java.time.LocalDateTime local = results.getObject(9, java.time. LocalDateTime.class);
  java.time.OffsetDateTime offset = results.getObject(9, java.time. OffsetDateTime.class);
  java.time.ZonedDateTime zoned = results.getObject(9, java.time. ZonedDateTime.class);

Важно! Этот код не будет работать если у вас устаревший MySQL JDBC Driver. Обрати внимание на версию “mysql-connector-java”, прописанную в твоем pom.xml, или добавленную в Libraries в настройках проекта.

Кстати, таким же способом можно обойти и невозможность хранить null у примитивных типов. Если колонка таблицы имеет тип INT, то есть пара способов получить из нее null. Смотри пример ниже:


  Integer id1 = results.getObject(8, Integer.class);    	 //так будет работать
  Integer id2 = results.getObject(8, int.class);                 //так тоже будет работать
  int id3 = results.getObject(8,  Integer.class);            	//метод вернет null, JVM кинет NPE
  int id4 = results.getObject(8,  int.class);                    	//метод вернет null, JVM кинет NPE

Настройка часового пояса в MySQL

С MySQL тоже произошло много всего интересного. Как ты знаешь, при создании соединения с MySQL к нему можно добавлять различные параметры:
mysql://localhost:3306/db_scheme?имя=значение&имя=значение

Так вот, для работы с временными зонами в MySQL добавили три параметра. Эти параметры ты можешь передавать, когда устанавливаешь соединение с сервером.

Ниже я приведу таблицу с ними:

Параметр Значения Значение по умолчанию
connectionTimeZone LOCAL | SERVER | user-zone SERVER
forceConnectionTimeZoneToSession true | false true
preserveInstants true | false false

С помощью параметра connectionTimeZone мы выбираем временную зону (часовой пояс), в котором будут выполняться все запросы. С точки зрения клиента, сервер работает в указанной временной зоне.

Параметр forceConnectionTimeZoneToSession заставляет игнорировать переменную time_zone сессии и заменить ее на connectionTimeZone.

И наконец параметр preserveInstants управляет преобразование точного-момента-времени между JVM timeZone и connectionTimeZone.

Самые частые конфигурации такие:

  • connectionTimeZone=LOCAL & forceConnectionTimeZoneToSession=false — соответствует старому MySQL JDBC драйверу версии 5.1 с параметром useLegacyDatetimeCode=true.

  • connectionTimeZone=LOCAL & forceConnectionTimeZoneToSession=true — новый режим, обеспечивающий наиболее естественный способ обработки значений даты и времени.

  • connectionTimeZone=SERVER & preserveInstants=true — соответствует старому MySQL JDBC драйверу версии 5.1 с параметром useLegacyDatetimeCode=false.

  • connectionTimeZone=user_defined & preserveInstants=true — помогает преодолеть ситуацию, когда часовой пояс сервера не может быть распознан коннектором, потому что он установлен как общая аббревиатура, такая как CET/CEST.

Да, даты — тема интересная и проблем с ними много. Как говорится: это конечно страшно, но и я не сыкло! :)

undefined
1
Задача
Модуль 4. Работа с БД, 7 уровень, 4 лекция
Недоступна
Получение даты
В методе main создай подключение к БД с помощью метода getConnection(String, String, String) класса DriverManager. Используй URL "jdbc:mysql://localhost:3306/test", пользователя "root" и такой же пароль. Получи Statement используя метод createStatement без параметров.