Автор оригинала: Vlad Mihalcea.
Работа с часовыми поясами всегда сложна . Как правило, это намного проще, если все значения даты/времени хранятся в формате UTC и, при необходимости, имеют дело только с преобразованиями часовых поясов в пользовательском интерфейсе.
В этой статье будет продемонстрировано, как вы можете выполнить эту задачу с помощью JDBC и свойства конфигурации awesome hibernate.jdbc.time_zone
.
Для наших тестов мы будем использовать следующую сущность Book
, которая предоставляет свойство java.sql.Timestamp
для обозначения даты/времени создания книги:
@Entity @Table(name = "book") public class Book { @Id private Long id; private String title; @Column(name = "created_by") private String createdBy; @Column(name = "created_on") private Timestamp createdOn; //Getters and setters omitted for brevity }
Предполагая, что мы хотим сохранить следующую книгу:
Book book = new Book(); book.setId(1L); book.setTitle("High-Performance Java Persistence"); book.setCreatedBy("Vlad Mihalcea"); book.setCreatedOn( new Timestamp( ZonedDateTime.of(2016, 8, 25, 11, 23, 46, 0, ZoneId.of("UTC") ).toInstant().toEpochMilli() ) ); assertEquals( 1472124226000L, book.getCreatedOn().getTime() ); entityManager.persist(book);
Мы явно устанавливаем атрибут CreatedOn
в метку времени UTC (GMT), миллисекунды эпохи которой (например, 1472124226000) указывают на ожидаемую дату/время (например, Чт, 25 августа 2016 11:23:46 GMT).
Чтобы увидеть, есть ли какой-либо дрейф часового пояса, мы установим часовой пояс модульных тестов на США/Гавайи :
TimeZone.setDefault(TimeZone.getTimeZone("US/Hawaii"));
PostgreSQL
При выполнении вышеупомянутой сущности persist в PostgreSQL Hibernate генерирует следующую инструкцию INSERT:
INSERT INTO book ( created_by, created_on, title, id ) VALUES ( 'Vlad Mihalcea', '2016-08-25 01:23:46.0', 'High-Performance Java Persistence', 1 )
Оператор INSERT
ожидается с java.sql.Timestamp
, как и его базовый класс (например, java.util.Дата
), печатает базовую метку времени в миллисекундах в соответствии с местным часовым поясом, которым в нашем случае является США/Гавайи .
Однако при извлечении метки времени из базы данных мы видим, что метка времени была фактически сохранена в соответствии с местным часовым поясом, а не относительно UTC:
Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { try (Statement st = connection.createStatement()) { try (ResultSet rs = st.executeQuery( "SELECT TO_CHAR(created_on, 'YYYY-MM-DD HH24:MI:SS') " + "FROM book")) { while (rs.next()) { String timestamp = rs.getString(1); assertEquals( "2016-08-25 01:23:46", timestamp ); } } } });
Если мы загрузим объект обратно в режим гибернации, мы увидим, что Метка времени
значение в миллисекундах вообще не изменилось:
Book book = entityManager.find(Book.class, 1L); assertEquals(1472124226000L, book.getCreatedOn().getTime());
MySQL
То же самое относится и к MySQL:
INSERT INTO book ( created_by, created_on, title, id ) VALUES ( 'Vlad Mihalcea', '2016-08-25 01:23:46.0', 'High-Performance Java Persistence', 1 )
Единственное отличие заключается в том, что сейчас мы используем функцию DATE_FORMAT
MySQL, но результат такой же, как и при использовании PostgreSQL:
Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { try (Statement st = connection.createStatement()) { try (ResultSet rs = st.executeQuery( "SELECT DATE_FORMAT(created_on, '%Y-%m-%d %H:%i:%s') " + "FROM book")) { while (rs.next()) { String timestamp = rs.getString(1); assertEquals( "2016-08-25 01:23:46", timestamp ); } } } });
И, если мы загрузим объект в режим гибернации, мы увидим, что значение Метка времени
было переведено обратно в локальный часовой пояс JVM, поэтому значение в миллисекундах эпохи такое же, как при сохранении объекта:
Book book = entityManager.find(Book.class, 1L); assertEquals(1472124226000L, book.getCreatedOn().getTime());
Что только что произошло? Почему значение метки времени на 10 часов раньше начального значения, назначенного для этого столбца?
Проблема заключается в JDBC, а не в спящем режиме. По умолчанию Тип метки времени
Класс Hibernate установит переменную привязки PreparedStatement
timestamp следующим образом:
st.setTimestamp( index, timestamp );
И чтение его из набора результатов
выполняется следующим образом:
rs.getTimestamp( name )
Однако оба этих метода JDBC имеют перегруженную версию, для которой требуется Календарь
для установки явного часового пояса:
Подготовленное утверждение#Метка времени(int parameterIndex, Метка времени x, Календарь cal)
Набор результатов#Отметка времени(int ColumnIndex, Calendar cal)
В PreparedStatement
|/Метка времени Javadoc указано, что:
С помощью объекта календаря драйвер может рассчитать метку времени с учетом пользовательского часового пояса. Если объект календаря не указан, драйвер использует часовой пояс по умолчанию, который соответствует виртуальной машине, на которой запущено приложение.
В то время как для метода ResultSet
getTimestamp
говорится, что:
Этот метод использует данный календарь для построения соответствующего значения миллисекунды для метки времени, если базовая база данных не хранит информацию о часовом поясе.
Итак, поскольку мы не указали явный часовой пояс, драйвер JDBC будет предполагать, что Метка времени
миллисекунды эпохи должны обрабатываться в соответствии с местным часовым поясом, которым в нашем случае является США/Гавайи .
Одним из исправлений было бы изменить часовой пояс JVM по умолчанию на UTC, но мы этого не хотим, так как пользовательский интерфейс должен отображаться в соответствии с нашим местным США/Гавайями часовым поясом.
В Hibernate 5.2.3 я решил эту проблему с помощью проблемы HHH-11396 Jira, которая теперь позволяет передавать Часовой пояс
в дескрипторах SQL TimestampType
и TimeType
, чтобы вместо этого использовались вышеупомянутые перегруженные методы JDBC (с использованием часового пояса Календаря
).
Таким образом, нам не нужно менять часовой пояс JVM, но мы можем указать Hibernate использовать любой часовой пояс, который мы пожелаем, для уровня доступа к данным.
Установка свойства hibernate.jdbc.time_zone с помощью JPA
Вы можете установить это свойство в своем JPA persistence.xml
файл конфигурации, подобный этому:
Установка свойства hibernate.jdbc.time_zone с помощью Spring Boot
Для весенней загрузки вы можете задать это свойство в файле конфигурации application.properties
:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Вы также должны настроить базу данных на использование UTC по умолчанию. Ознакомьтесь с этой статьей для получения более подробной информации.
PostgreSQL
При запуске нашего тестового примера на PostgreSQL после установки этого свойства конфигурации мы видим, что метка времени больше не смещается в другой часовой пояс:
Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { try (Statement st = connection.createStatement()) { try (ResultSet rs = st.executeQuery( "SELECT TO_CHAR(created_on, 'YYYY-MM-DD HH24:MI:SS') " + "FROM book")) { while (rs.next()) { String timestamp = rs.getString(1); assertEquals( "2016-08-25 11:23:46", timestamp ); } } } });
Даже если мы загрузим объект обратно в режим гибернации, мы увидим, что Метка времени
указывает на один и тот же миллисекундный снимок эпохи:
Book book = entityManager.find(Book.class, 1L); assertEquals(1472124226000L, book.getCreatedOn().getTime());
MySQL
Если вы используете MySQL JDBC Connector/J до версии 8, вам необходимо установить для свойства useLegacyDatetimeCode
connection значение false
, так как в противном случае hibernate.jdbc.time_zone
не действует.
Драйвер MySQL 8 Connector/J удалил свойство useLegacyDatetimeCode
конфигурации, поэтому вам не нужно ничего устанавливать.
Теперь, если мы выполним следующий запрос SELECT, мы получим ожидаемую метку времени.
Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { try (Statement st = connection.createStatement()) { try (ResultSet rs = st.executeQuery( "SELECT DATE_FORMAT(created_on, '%Y-%m-%d %H:%i:%s') " + "FROM book")) { while (rs.next()) { String timestamp = rs.getString(1); assertEquals( "2016-08-25 11:23:46", timestamp ); } } } });
И то же самое происходит, когда мы загружаем объект в режим гибернации:
Book book = entityManager.find(Book.class, 1L); assertEquals(1472124226000L, book.getCreatedOn().getTime());
Работа с часовыми поясами всегда сложна, даже при работе с часовыми поясами UTC или GMT. К счастью, свойство конфигурации hibernate.jdbc.time_zone
Hibernate является отличным дополнением.
При работе с MySQL не забудьте установить useLegacyDatetimeCode
в false
, так как в противном случае устаревшая обработка даты/времени , в которой будет использоваться часовой пояс сервера базы данных, просто оставит отметку времени как есть.
Даже если вы предоставите устаревшие свойства useTimezone
или useJDBCCompliantTimezoneShift
подключения, вы не получите ожидаемого результата, поэтому рекомендуется использовать новую логику обработки даты/времени, поэтому лучше установите для свойства MySQL useLegacyDatetimeCode
значение false
.