1. Екскурс в історію

Завдання щодо збереження Java-об'єктів до бази даних було актуальним чи не відразу після створення мови Java. У той час у мові Java був лише один тип даних — Date, який зберігав час за стандартом UNIX-time: як кількість мілісекунд, що минули з 1970 року.

Ну а в базах даних на той час були вже різні типи даних для дат. Як мінімум, були окремі типи для дати, часу і дата+час:

  • DATE
  • TIME
  • TIMESTAMP

Тому творці мови Java додали до нього спеціальний пакет — java.sql, який містив класи:

  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp

Мапити такі класи — суцільне задоволення:


@Entity
public class TemporalValues {
 
@Basic
    private java.sql.Date sqlDate;
 
@Basic
    private java.sql.Time sqlTime;
 
    @Basic
    private java.sql.Timestamp sqlTimestamp;
}

Але оскільки програмістам раніше доводилося працювати з класом java.util.Date, то до Hibernate додали спеціальну анотацію @Temporal, щоб керувати мапінгом типу Date.

Приклад:


// Якщо анотація відсутня, то в базі буде тип TIMESTAMP
Date dateAsTimestamp;
 
@Temporal(TemporalType.DATE) // буде замаплений на тип DATE
Date dateAsDate;
 
@Temporal(TemporalType.TIME) // буде замаплений на тип TIME
Date dateAsTime;

Для типу java.util.Calendar та типу java.util.Date за замовчуванням використовується тип TIMESTAMP для їх представлення в базі даних.

2. Новий час

Нині з мапінгом все набагато простіше і краще. Всі бази даних підтримують 4 типи даних для роботи з часом:

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

Для того, щоб представити тип 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, запам'ятати цю справу тобі не складе :)

Мапити їх — суцільне задоволення:


@Basic
private java.time.LocalDate localDate;
 
@Basic
private java.time.LocalTime localTime;
 
@Basic
private java.time.OffsetTime offsetTime;
 
@Basic
private java.time.Instant instant;
 
@Basic
private java.time.LocalDateTime localDateTime;
 
@Basic
private java.time.OffsetDateTime offsetDateTime;
 
@Basic
private java.time.ZonedDateTime zonedDateTime;

Анотація @Basic означає, що поле потрібно обробити автоматично: Hibernate сам вирішить, на яку колонку і тип потрібно замапити це поле.

3. Робота з часовими зонами

Якщо часова зона є частиною дати, то зберігати їх у базі просто — просто, як звичайну дату:


@Basic
private java.time.OffsetDateTime offsetDateTime;
 
@Basic
private java.time.ZonedDateTime zonedDateTime;

Але якщо ти хочеш зберігати часові зони окремо від дати:


@Basic
private java.time.TimeZone timeZone;
 
@Basic
private java.time.ZoneOffset zonedOffset;

То Hibernate за замовчуванням зберігатиме їх у типі VARCHAR. Що, власне, логічно, тому що TimeZone зазвичай має рядкове ім'я типу "UTC+3" або "Cairo".

4. Встановлення тимчасової зони

Коли ти працюватимеш зі збереженням дат до бази даних, то зіткнешся з тим, що є аж 4 місця, де можна вказати поточну тимчасову зону:

  • Операційна система сервера;
  • СУБД;
  • Java-програма;
  • Hibernate.

Якщо в СУБД не вказано часову зону (TimeZone), вона візьме її з налаштувань операційної системи. Це може бути незручно, оскільки резервні СУБД часто розміщують в інших датацентрах, які мають свою часову зону.

Тому майже у всіх СУБД адміни встановлюють єдину зону, щоб дані можна було легко переносити з одного сервера на інший.

Схожа ситуація і з Java-програмою. Вона також може запускатися на різних серверах у різних датацентрах, тому зазвичай для неї вказують часову зону явно.


java -Duser.timezone=UTC ...

Або під час роботи програми:


TimeZone.setDefault(TimeZone.getTimeZone( "UTC"));

І, звісно, Hibernate дозволяє встановити свою часову зону явно.

По-перше, її можна вказати під час конфігурування SessionFactory:


settings.put(
    AvailableSettings.JDBC_TIME_ZONE,
    TimeZone.getTimeZone("UTC")
);

По-друге, часову зону можна вказати для конкретної сесії:


Session session = sessionFactory()
    .withOptions()
    .jdbcTimeZone(TimeZone.getTimeZone("UTC"))
    .openSession();

5. Анотація @TimeZoneStorage

Часто буває ситуація, що програмісти почали проєктувати базу з розрахунку роботи в одній країні (і одній тимчасовій зоні), а потім через кілька років їм довелося додати підтримку роботи в різних часових зонах.

Тому вони просто додали до бази окрему колонку для зберігання часової зони. Це така часта ситуація, що в Hibernate додали спеціальну анотацію, яка дозволяє зберігати TimeZone конкретної дати в окремій колонці.

Приклад:


@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_offset_offset")
@Column(name = "birthday_offset")
private OffsetDateTime offsetDateTimeColumn;
 
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_zoned_offset")
@Column(name = "birthday_zoned")
private ZonedDateTime zonedDateTimeColumn;

Це костиль. Але є йому і виправдання: він з'явився ще за часів, коли DateTime API ще не було. А в класі java.util.Date не можна було зберігати TimeZone.

Дуже сподіваюся, що ти не часто зустрічатимеш таке у своєму коді.