Автор оригинала: 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. Дата , а также Местное состояние и Местное время являются лучшими вариантами отображения столбцов данных и меток времени.