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.
Дуже сподіваюся, що ти не часто зустрічатимеш таке у своєму коді.