Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я покажу вам, как лучше всего сопоставлять типы столбцов даты и метки времени при использовании JPA и Hibernate.
Хотя существует множество вариантов отображения столбцов даты и метки времени на стороне Java, как вы скоро увидите, не все из них подходят.
Столбцы базы данных с датами и метками времени
Предположим, что у нас есть таблица user_account
, в которой хранится дата подписки пользователя, и таблица post
со столбцом published_on
, в котором хранится значение метки времени, когда сообщение было опубликовано.
Столбцы даты и времени MySQL
Если мы используем MySQL, тип столбца subscribed_on
в таблице user_account
может быть ДАТА
, и тип столбца published_on
в таблице post
может быть ДАТА-ВРЕМЯ
.
MySQL также предлагает МЕТКУ ВРЕМЕНИ
столбец для хранения информации о дате и времени. Однако, поскольку максимальное значение столбца МЕТКА ВРЕМЕНИ
равно 2038-01-09 03:14:07
, обычно лучше использовать DATETIME
вместо этого.
PostgreSQL Столбцы даты и метки времени
Если мы используем PostgreSQL, тип столбца subscribed_on
в таблице user_account
может быть ДАТА
и тип столбца published_on
в таблице post
могут быть ОТМЕТКОЙ ВРЕМЕНИ
.
PostgreSQL также предлагает ВРЕМЕННУЮ МЕТКУ С ЧАСОВЫМ ПОЯСОМ
столбец для хранения информации о дате и времени. Столбец МЕТКА ВРЕМЕНИ С ЧАСОВЫМ ПОЯСОМ
преобразует указанное значение метки времени в UTC на основе текущего ЧАСОВОЙ ПОЯС
настройка.
Далее мы увидим, какие у нас есть опции для сопоставления типов столбцов даты и метки времени в качестве атрибутов сущности JPA или Hibernate.
Сохранение метки времени в формате UTC
Как я объяснил в этой статье , очень хорошая идея хранить значения меток времени в формате UTC.
Во-первых, вам необходимо настроить сервер базы данных для использования часового пояса UTC. Например, в PostgreSQL вы можете сделать это, указав следующую настройку в файле postgresql.conf
:
timezone = 'UTC'
В MySQL вы можете установить это в my.cnf
(например, Linux) или my.ini
(например, файлы конфигурации Windows):
default_time_zone='+00:00'
Для MySQL 8 убедитесь, что вы используете драйвер 8.0.20 Connector/J или новее, так как он исправляет Ошибка преобразования ДАТЫ .
Во-вторых, вам нужно установить hibernate.jdbc.time_zone
Свойство Hibernate имеет значение UTC
.
Это можно сделать с помощью persistence.xml
файл конфигурации:
Или через файл Spring Boot application.properties
:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Без предоставления этого свойства драйвер JDBC может преобразовать предоставленные значения временных меток из часового пояса JVM в часовой пояс базы данных.
Хранение столбцов данных и меток времени с помощью JPA и гибернации
Чтобы отобразить столбец даты, у нас есть следующие опции:
java.sql. Дата
java.util. Дата
ЛокалЬная дата
Чтобы отобразить столбец метки времени, мы можем использовать один из следующих типов Java:
java.sql. Отметка времени
java.util. Дата
Местное время
Смещение времени
Дата в зоне
Далее мы проанализируем все эти варианты и рассмотрим преимущества и недостатки сопоставления атрибутов каждой сущности.
Сопоставление даты и метки времени с помощью java.sql. Дата и java.sql. Отметка времени
JDBC предлагает java.sql. Дата
и java.sql. Метка времени
для сопоставления данных и столбцов меток времени, поэтому мы можем сопоставить subscribed_on
и published_on
столбцы, использующие следующие сопоставления объектов JPA и Hibernate:
@Column(name = "subscribed_on") private java.sql.Date subscribedOn; @Column(name = "published_on") private java.sql.Timestamp publishedOn;
Учитывая, что у нас есть следующие полезные методы:
private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private java.sql.Date parseDate(String date) { try { return new Date(DATE_FORMAT.parse(date).getTime()); } catch (ParseException e) { throw new IllegalArgumentException(e); } } private java.sql.Timestamp parseTimestamp(String timestamp) { try { return new Timestamp(DATE_TIME_FORMAT.parse(timestamp).getTime()); } catch (ParseException e) { throw new IllegalArgumentException(e); } }
При сохранении следующей Учетной записи пользователя
и Должность
организации:
UserAccount user = new UserAccount() .setId(1L) .setFirstName("Vlad") .setLastName("Mihalcea") .setSubscribedOn( parseDate("2013-09-29") ); Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy(user) .setPublishedOn( parseTimestamp("2020-05-01 12:30:00") ); entityManager.persist(user); entityManager.persist(post);
Hibernate генерирует соответствующие инструкции SQL INSERT:
INSERT INTO user_account ( first_name, last_name, subscribed_on, id ) VALUES ( 'Vlad', 'Mihalcea', '2013-09-29', 1 ) INSERT INTO post ( user_account_id, published_on, title, id ) VALUES ( 1, '2020-05-01 12:30:00', 'High-Performance Java Persistence', 1 )
И, при извлечении всего из базы данных, мы можем видеть, что Дата
и Метка времени
значения-это именно те, которые мы сохранили:
Post post = entityManager.find( Post.class, 1L ); assertEquals( parseTimestamp("2020-05-01 12:30:00"), post.getPublishedOn() ); UserAccount userAccount = post.getCreatedBy(); assertEquals( parseDate("2013-09-29"), userAccount.getSubscribedOn() );
Хотя сопоставление является простым, большинство приложений не хотят привязывать свои сущности JPA к классам API JDBC. Итак, давайте посмотрим, какие у нас есть другие варианты.
Сопоставление даты и метки времени с помощью java.util. Дата
Вместо использования JDBC Дата
и Метка времени
классы, мы можем использовать java.util. Дата
для обоих Дата
и Время
типы столбцов. Чтобы различать эти два типа столбцов, JPA предлагает аннотацию @Temporal
, как показано на следующем сопоставлении атрибутов сущности JPA:
@Column(name = "subscribed_on") @Temporal(TemporalType.DATE) private java.util.Date subscribedOn; @Column(name = "published_on") @Temporal(TemporalType.TIMESTAMP) private java.util.Date publishedOn;
Утилита Парседат
и parseTimestamp
методы на этот раз не требуют дополнительной упаковки:
private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private java.util.Date parseDate(String date) { try { return DATE_FORMAT.parse(date); } catch (ParseException e) { throw new IllegalArgumentException(e); } } private java.util.Date parseTimestamp(String timestamp) { try { return DATE_TIME_FORMAT.parse(timestamp); } catch (ParseException e) { throw new IllegalArgumentException(e); } }
Сохранение и извлечение Учетной записи пользователя
и Сообщение
сущности не меняются, поэтому оно не будет повторяться для краткости.
Преимущество использования java.util. Дата
заключается в том, что сущности уровня доступа к данным больше не связаны с классами API JDBC. Недостатком является то, что нам необходимо предоставить аннотацию @Temporal
, чтобы сообщить поставщику JPA о соответствующем типе столбца базы данных.
Сопоставление даты и метки времени с помощью LocalDate и LocalDateTime
Как объясняется в этой статье , JPA 2.2 добавляет поддержку API даты и времени Java 8.
Таким образом, мы можем сопоставить subscribed_on
с Локальной датой
и столбец published_on
в Местное время So, we can map the
subscribed_on
@Column(name = "subscribed_on") private LocalDate subscribedOn; @Column(name = "published_on") private LocalDateTime publishedOn;
По сравнению с java.util. Date
или его аналоги JDBC, API Java Date/Time намного проще в использовании и не требует никаких утилитарных методов, так как предоставляемые заводские методы очень просты в использовании:
UserAccount user = new UserAccount() .setId(1L) .setFirstName("Vlad") .setLastName("Mihalcea") .setSubscribedOn( LocalDate.of( 2013, 9, 29 ) ); Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy(user) .setPublishedOn( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ) ); entityManager.persist(user); entityManager.persist(post);
Запросы SQL-вставки идентичны тем, которые были представлены ранее. И сущности также правильно извлекаются из базы данных:
Post post = entityManager.find( Post.class, 1L ); assertEquals( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ), post.getPublishedOn() ); UserAccount userAccount = post.getCreatedBy(); assertEquals( LocalDate.of( 2013, 9, 29 ), userAccount.getSubscribedOn() );
Сопоставление метки времени с использованием OffsetDateTime
Вы также можете использовать Java 8 OffsetDateTime
для отображения столбца published_on
:
@Column(name = "published_on") private OffsetDateTime publishedOn;
Однако при сохранении объекта Post
:
Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy(user) .setPublishedOn( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ).atOffset(ZoneOffset.UTC) ); entityManager.persist(post);
Мы видим, что Hibernate преобразовал метку времени в соответствии с нашим местным часовым поясом:
INSERT INTO post ( user_account_id, published_on, title, id ) VALUES ( 1, '2020-05-01 15:30:00.0', 'High-Performance Java Persistence', 1 )
Это связано с тем, что дескриптор Java OffsetDateTime
обертывает предоставленный Смещение времени
в Временная метка
, вот так:
return (X) Timestamp.from( offsetDateTime.toInstant() );
И при чтении его из базы данных он преобразует его в местный часовой пояс:
return OffsetDateTime.ofInstant( ts.toInstant(), ZoneId.systemDefault() );
Таким образом, это работает только в том случае, если используется системный часовой пояс:
Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy(user) .setPublishedOn( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ).atOffset( ZoneOffset.systemDefault() .getRules() .getOffset(LocalDateTime.now()) ) );
При сохранении атрибута OffsetDateTime
сущности Hibernate не сохраняет смещение отдельно, поэтому используется локальное смещение. По этой причине OffsetDateTime
не очень полезен, и его можно заменить на LocalDateTime
вместо этого.
Если вам понравилась эта статья, держу пари, вам также понравятся моя Книга и Видеокурсы.
Отображение метки времени с помощью ZonedDateTime
Вы также можете использовать Java 8 ZonedDateTime
для отображения столбца published_on
:
@Column(name = "published_on") private ZonedDateTime publishedOn;
Однако, точно так же, как это было в случае Смещение времени
, только системный часовой пояс работает при сохранении и извлечении атрибутов сущности.
Итак, вот как вам нужно сохранить Дата в зоне So, this is how you need to persist the
ZonedDateTime
Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy(user) .setPublishedOn( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ).atZone(ZoneId.systemDefault()) );
Для того, чтобы иметь возможность извлечь его из базы данных:
assertEquals( LocalDateTime.of( 2020, 5, 1, 12, 30, 0 ).atZone(ZoneId.systemDefault()), post.getPublishedOn() );
При сохранении атрибута ZonedDateTime
сущности Hibernate не сохраняет часовой пояс отдельно, поэтому используется локальное смещение. По этой причине ZonedDateTime
не очень полезен, и его можно заменить на LocalDateTime
вместо этого.
Вывод
При использовании JPA и гибернации java.util. Дата
, а также Местное состояние
и Местное время
являются лучшими вариантами отображения столбцов данных и меток времени.