1. Сучасний стан справ з часом

З часів, коли вигадали 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-сервер теж повинен вміти по-розумному працювати з клієнтами в різних часових зонах.

2. Сучасні проблеми потребують сучасних рішень

В принципі, нові типи з 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

3. Отримання дати

У мене для тебе гарна новина. Перша за довгий час. Ми можемо обійти обмеження методу 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

4. Налаштування часового поясу в 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.

Так, дати — тема цікава, і проблем із ними багато. Як то кажуть: сміливий наскок — половина-спасіння.