Сегодня любой Java разработчик сходу сможет правильно ответить на вопрос «Как смапить дату и время из колонки таблицы БД на поле в Java классе?». Или нет?
На самом деле, нюансов по ходу решения этой задачи может возникнуть немало.
В новом переводе от команды Spring АйО рассказывается про подробности работы с современным API java.time
, правильный маппинг данных с учётом часовых поясов, устаревших типов java.util.Date
, Calendar
и многое другое.
Базы данных поддерживают различные типы данных для хранения информации о дате и времени. Наиболее используемые из них:
DATE — для сохранения даты без времени,
TIME — для хранения времени без даты,
TIMESTAMP — для хранения информации о дате и времени.
Вы можете маппить все эти типы с помощью JPA и Hibernate.
Однако вам нужно понять, к какому типу Java вы хотите привязать столбец базы данных. Язык Java предоставляет множество классов для представления информации о дате и времени, например:
Типы из API даты и времени (Date and Time API):
Это такие классы, какjava.time.LocalDate, java.time.LocalDateTime, java.time.OffsetTime, java.time.OffsetDateTime, java.time.ZonedDateTime, java.time.Duration
. Современные приложения должны использовать именно их вместо устаревших типов.SQL-специфичные типы:
java.sql.Date
,java.sql.Time
иjava.sql.Timestamp
.А также устаревшие типы:
java.util.Date
иjava.util.Calendar
.
Современные версии стандарта Jakarta Persistence поддерживают большинство из перечисленных типов. Кроме того, Hibernate предоставляет проприетарную поддержку практически для всех остальных.
В этой статье я продемонстрирую различные варианты маппинга:
Мы начнем с маппинга классов из пакета
java.time
, так как они наиболее актуальны для современных приложений.Затем я покажу, как маппить классы из пакета
java.util
, которые до сих пор используются во многих приложениях.И завершим статью разбором маппинга классов из пакета
java.sql
.
Содержание
Маппинг классов
java.time
Работа с
ZonedDateTime
Поддержка
ZonedDateTime
в Hibernate 6Настройка обработки часовых поясов
Типы хранения часовых поясов в Hibernate (
TimeZoneStorageTypes
)Поддержка
ZonedDateTime
в Hibernate 5Маппинг классов
java.util
Маппинг классов
java.sql
Заключение
Маппинг классов java.time
Классы API даты и времени являются наиболее популярным способом представления значений даты и времени. Они разделяют информацию о дате и времени и устраняют недостатки устаревшего класса java.util.Date
.
Начиная с Hibernate 5 и JPA 2.2, вы можете использовать следующие классы в качестве типов атрибутов.
Java Type |
JPA |
Hibernate |
JDBC Type |
|
v2.2 |
v5 |
|
|
v2.2 |
v5 |
|
|
v2.2 |
v5 |
|
|
v2.2 |
v5 |
|
|
v2.2 |
v5 |
|
|
v3.2 |
v5 |
|
|
– |
v5 v6 |
|
|
v3.2 |
v6 |
|
|
– |
v6 |
|
|
– |
v6 |
|
|
– |
v6 |
|
Как видно из таблицы, поддержка API даты и времени постепенно расширялась, и Hibernate поддерживает несколько больше классов, чем JPA. Вы можете легко добавить поддержку дополнительных классов, реализовав AttributeConverter. В одной из предыдущих статей я использовал этот подход для маппинга атрибута типа Duration с помощью JPA.
В зависимости от версии Hibernate следует быть осторожным при использовании ZonedDateTime
. В Hibernate 5 работа с часовыми поясами и маппинг в столбец типа TIMESTAMP
имеет несколько подводных камней. Hibernate 6 предоставляет больше контроля над этим процессом, но вам все равно нужно понимать, как это работает. Подробнее об этом я рассказываю в разделе Работа с ZonedDateTime.
Сначала давайте рассмотрим базовый маппинг сущности и простой тестовый пример с использованием этой сущности.
@Entity
public class MyEntity {
private LocalDate localDate;
private LocalDateTime localDateTime;
private OffsetTime offsetTime;
private OffsetDateTime offsetDateTime;
// Hibernate-specific - not supported by JPA
private Duration duration;
private Instant instant;
private ZonedDateTime zonedDateTime;
...
}
MyEntity e = new MyEntity();
e.setLocalDate(LocalDate.of(2019, 7, 19));
e.setLocalDateTime(LocalDateTime.of(2019, 7, 19, 15, 05, 30));
e.setOffsetTime(OffsetTime.of(15, 05, 30, 0, ZoneOffset.ofHours(+2)));
e.setOffsetDateTime(OffsetDateTime.of(2019, 7, 19, 15, 05, 30, 0, ZoneOffset.ofHours(+2)));
// Hibernate-specific - not supported by JPA
e.setDuration(Duration.ofHours(2));
e.setInstant(Instant.now());
e.setZonedDateTime(ZonedDateTime.of(2019, 7, 18, 15, 05, 30, 0, ZoneId.of("UTC-4")));
em.persist(e);
Классы API даты и времени четко определяют, содержат ли они информацию о дате и/или времени. Поэтому спецификация JPA и все реализующие её фреймворки могут сопоставлять их с соответствующими SQL-типами.
11:52:26,305 DEBUG SQL:94 - insert into MyEntity (duration, instant, localDate, localDateTime, offsetDateTime, offsetTime, sqlDate, sqlTime, sqlTimestamp, utilCalendar, utilDate, zonedDateTime, id) values (?, ?, ?, ?, ?, ?, ?, ?)
11:52:26,306 TRACE BasicBinder:65 - binding parameter [1] as [BIGINT] - [PT2H]
11:52:26,307 TRACE BasicBinder:65 - binding parameter [2] as [TIMESTAMP] - [2019-07-22T09:52:26.284946300Z]
11:52:26,308 TRACE BasicBinder:65 - binding parameter [3] as [DATE] - [2019-07-19]
11:52:26,308 TRACE BasicBinder:65 - binding parameter [4] as [TIMESTAMP] - [2019-07-19T15:05:30]
11:52:26,312 TRACE BasicBinder:65 - binding parameter [5] as [TIMESTAMP] - [2019-07-19T15:05:30+02:00]
11:52:26,313 TRACE BasicBinder:65 - binding parameter [6] as [TIME] - [15:05:30+02:00]
11:52:26,324 TRACE BasicBinder:65 - binding parameter [7] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
11:52:26,324 TRACE BasicBinder:65 - binding parameter [8] as [BIGINT] - [1]
Работа с ZonedDateTime
Объект ZonedDateTime
представляет дату с информацией о времени и часовом поясе, и тип JDBC TIMESTAMP_WITH_TIMEZONE
кажется идеальным вариантом. К сожалению, не все реляционные базы данных поддерживают этот тип, и Hibernate приходится выполнять преобразование типа.
Как уже упоминалось, обработка ZonedDateTime
зависит от версии Hibernate:
Hibernate 6 поддерживает различные варианты маппинга, которые дают полный контроль над обработкой часовых поясов.
Hibernate 5 нормализует часовой пояс объекта
ZonedDateTime
, что может вызывать проблемы.
Поддержка ZonedDateTime в Hibernate 6
В Hibernate 6 был введен enum TimeZoneStorageType
. Он позволяет настроить, как Hibernate будет обрабатывать информацию о часовом поясе, и дает возможность избежать любых нормализаций часовых поясов.
Настройка обработки часовых поясов
Вы можете использовать значения этого enum, чтобы задать обработку часового пояса по умолчанию, установив свойство hibernate.timezone.default_storage
в конфигурации persistence.xml.
<persistence>
<persistence-unit name="my-persistence-unit">
<description>Hibernate example configuration - thorben-janssen.com</description>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.timezone.default_storage" value="NORMALIZE"/>
...
</properties>
</persistence-unit>
</persistence>
Вы также можете аннотировать атрибут ZonedDateTime
с помощью аннотации @TimeZoneStorage
.
@Entity
public class MyEntity {
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private ZonedDateTime zonedDateTime;
...
}
Типы TimeZoneStorageTypes в Hibernate
Вот краткий обзор различных типов TimeZoneStorageTypes
, предоставляемых Hibernate. Более подробное описание каждого типа вы можете найти в этой статье.
TimeZoneStorageType |
Version |
JDBC Type |
|
6.0 |
|
|
6.0 |
Приводит отметку времени к локальной или настроенной временной зоне (так же, как в Hibernate 5). Используется по умолчанию в Hibernate 6.0 и 6.1. |
|
6.0 |
Приводит отметку времени к UTC. |
|
6.0 |
Сохраняет отметку времени и смещение временной зоны в отдельных столбцах. |
|
6.0 |
Зависит от диалекта базы данных:
|
|
6.2 |
Зависит от диалекта базы данных:
|
Поддержка ZonedDateTime в Hibernate 5
Hibernate 5 отображает ZonedDateTime
в SQL-тип TIMESTAMP
без информации о часовом поясе.
Это достигается путем нормализации временной метки. Hibernate преобразует ZonedDateTime
в локальный часовой пояс JVM и сохраняет его в базе данных. При чтении TIMESTAMP
Hibernate добавляет информацию о локальном часовом поясе.
MyEntity e = new MyEntity();
e.setZonedDateTime(ZonedDateTime.of(2019, 7, 18, 15, 05, 30, 0, ZoneId.of("UTC-4")));
em.persist(e);
Hibernate 5 отображает информацию о часовом поясе в логах.
09:57:08,918 DEBUG SQL:92 - insert into MyEntity (zonedDateTime, id) values (?, ?)
09:57:08,959 TRACE BasicBinder:65 - binding parameter [1] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
09:57:08,961 TRACE BasicBinder:65 - binding parameter [2] as [BIGINT] - [1]
Но в базе данных видно, что он преобразовал часовой пояс с UTC-4 в UTC+2, который является моим локальным часовым поясом.
Этот маппинг работает при соблюдении следующих условий:
используется часовой пояс без учета летнего времени,
все экземпляры вашего приложения используют один и тот же часовой пояс,
вы никогда не меняете этот часовой пояс.
Очевидно, что это должно...
Вы можете избежать этих проблем, настроив часовой пояс без учета летнего времени в конфигурации persistence.xml. В таком случае Hibernate 5 будет использовать указанный часовой пояс вместо локального часового пояса вашей JVM. Я рекомендую использовать часовой пояс UTC.
<persistence>
<persistence-unit name="my-persistence-unit">
...
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="hibernate.jdbc.time_zone" value="UTC"/>
...
</properties>
</persistence-unit>
</persistence>
Когда вы повторно запустите тест, вы не увидите никаких изменений в логах.
10:06:41,070 DEBUG SQL:92 - insert into MyEntity (zonedDateTime, id) values (?, ?)
10:06:41,107 TRACE BasicBinder:65 - binding parameter [1] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
10:06:41,108 TRACE BasicBinder:65 - binding parameter [2] as [BIGINT] - [1]
Но если вы посмотрите на таблицу в базе данных, вы увидите, что Hibernate 5 теперь преобразовал ZonedDateTime
в часовой пояс UTC.
Маппинг классов java.util
До выхода Java 8 классы java.util.Date
и java.util.Calendar
были наиболее используемыми для представления дат с и без информации о времени.
Конечно, вы можете маппить оба этих класса с помощью JPA и Hibernate. Однако для маппинга требуется указать несколько дополнительных данных. Нужно определить, хотите ли вы связать java.util.Date
или java.util.Calendar
со столбцом типа DATE
, TIME
или TIMESTAMP
.
Для этого необходимо аннотировать атрибут сущности с помощью аннотации @Temporal и указать значение из enum TemporalType
. Вы можете выбрать одно из следующих значений:
TemporalType.DATE — для маппинга в столбец SQL-типа
DATE
;TemporalType.TIME — для маппинга в столбец SQL-типа
TIME
;TemporalType.TIMESTAMP — для маппинга в столбец SQL-типа
TIMESTAMP
.
В следующем примере кода используется аннотация @Temporal
для маппинга атрибута типа java.util.Date
в столбец TIMESTAMP
и атрибута типа java.util.Calendar
в столбец DATE
.
@Entity
public class MyEntity {
@Temporal(TemporalType.TIMESTAMP)
private Date utilDate;
@Temporal(TemporalType.DATE)
private Calendar utilCalendar;
...
}
Затем вы можете использовать эти атрибуты так же, как и любые другие атрибуты сущности.
MyEntity e = new MyEntity();
e.setUtilDate(new Date(119, 6, 18));
e.setUtilCalendar(new GregorianCalendar(2019, 6, 18));
em.persist(e);
Если вы активируете логирование SQL-запросов, то в логах найдете следующие сообщения.
16:04:07,185 DEBUG SQL:92 - insert into MyEntity (utilCalendar, utilDate, id) values (?, ?, ?)
16:04:07,202 TRACE BasicBinder:65 - binding parameter [8] as [DATE] - [java.util.GregorianCalendar[time=1563400800000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Berlin",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2019,MONTH=6,WEEK_OF_YEAR=29,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=199,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=3600000,DST_OFFSET=3600000]]
16:04:07,207 TRACE BasicBinder:65 - binding parameter [2] as [TIMESTAMP] - [Thu Jul 18 00:00:00 CEST 2019]
16:04:07,208 TRACE BasicBinder:65 - binding parameter [3] as [BIGINT] - [1]
Второе сообщение о привязке объекта GregorianCalendar
может вас удивить. Это довольно сложный способ Hibernate показать, какой объект Calendar
привязывается к параметру типа DATE
. Но не беспокойтесь: если вы посмотрите на базу данных, то увидите, что Hibernate записал дату в столбец типа DATE
.
Маппинг классов java.sql
Маппинг классов java.sql.Date
, java.sql.Time
и java.sql.Timestamp
выполняется просто, так как эти классы соответствуют SQL-типам данных. Это позволяет провайдеру JPA, например Hibernate, автоматически определить маппинг.
Таким образом, без дополнительных аннотаций:
java.sql.Date
отображается в SQL-типDATE
,java.sql.Time
отображается в SQL-типTIME
,java.sql.Timestamp
отображается в SQL-типTIMESTAMP
.
@Entity
public class MyEntity {
private java.sql.Date sqlDate;
private Time sqlTime;
private Timestamp sqlTimestamp;
...
}
Затем вы можете использовать эти атрибуты в своей бизнес-логике для сохранения информации о дате и времени в базе данных.
MyEntity e = new MyEntity();
e.setSqlDate(new java.sql.Date(119, 6, 18));
e.setSqlTime(new Time(15, 05, 30));
e.setSqlTimestamp(new Timestamp(119, 6, 18, 15, 05, 30, 0));
em.persist(e);
А после активации логирования SQL-запросов вы можете увидеть, что Hibernate отображает атрибуты сущности в соответствующие SQL-типы.
06:33:09,139 DEBUG SQL:92 - insert into MyEntity (sqlDate, sqlTime, sqlTimestamp, id) values (?, ?, ?, ?)
06:33:09,147 TRACE BasicBinder:65 - binding parameter [1] as [DATE] - [2019-07-18]
06:33:09,147 TRACE BasicBinder:65 - binding parameter [2] as [TIME] - [15:05:30]
06:33:09,147 TRACE BasicBinder:65 - binding parameter [3] as [TIMESTAMP] - [2019-07-18 15:05:30.0]
06:33:09,154 TRACE BasicBinder:65 - binding parameter [4] as [BIGINT] - [1]
Заключение
JPA и Hibernate могут маппить столбцы базы данных типов DATE
, TIME
и TIMESTAMP
на различные классы Java. Вы можете связать их с:
java.util.Date
иjava.util.Calendar
;java.sql.Date
,java.sql.Time
иjava.sql.Timestamp
;java.time.LocalDate, java.time.LocalDateTime, java.time.OffsetTime, java.time.OffsetDateTime, java.time.ZonedDateTime, java.time.Duration
.
Вам нужно лишь выбрать, какой тип Java использовать в своем коде. Я рекомендую использовать классы из пакета java.time
. Они являются частью API даты и времени, который был представлен в Java 8. Эти классы значительно удобнее в использовании как для маппинга, так и в бизнес-логике.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.