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.
Очень надеюсь, что ты не часто будешь встречать такое в своем коде.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ