4.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 для их представления в базе данных.

4.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 сам решит на какую колонку и тип должно быть замаплено данное поле.

4.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 Установка своей временной зоны

Когда ты будешь работать с сохранением дат в базу данных, то столкнешься с тем, что есть аж 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();

4.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.

Очень надеюсь, что ты не часто будешь встречать такое в своем коде.