JPA Хранение OffsetDateTime с ZoneOffset
У меня есть следующий класс сущности:
@Entity
public class Event {
private OffsetDateTime startDateTime;
// ...
}
Однако сохранение и последующее чтение объекта в / из базы данных с помощью JPA 2.2 приводит к потере информации: ZoneOffset
из startDateTime
изменения в UTC
(ZoneOffset
используется меткой времени базы данных). Например:
Event e = new Event();
e.setStartDateTime(OffsetDateTime.parse("2018-01-02T09:00-05:00"));
e.getStartDateTime().getHour(); // 9
e.getStartDateTime().getOffset(); // ZoneOffset.of("-05:00")
// ...
entityManager.persist(e); // Stored startDateTime as timestamp 2018-01-02T14:00Z
Event other = entityManager.find(Event.class, e.getId());
other.getStartDateTime().getHour(); // 14 - DIFFERENT
other.getStartDateTime().getOffset(); // ZoneOffset.of("+00:00") - DIFFERENT
Мне нужно использовать OffsetDateTime
: Я не могу использовать ZonedDateTime
потому что правила зоны изменяются (и это также страдает от этой потери информации в любом случае). Я не могу использовать LocalDateTime
, так как Event
происходит в любой точке мира, и мне нужен оригинал ZoneOffset
с момента, когда это произошло по причинам точности. Я не могу использовать Instant
потому что пользователь заполняет время начала события (событие похоже на встречу).
Требования:
Надо уметь выполнять
>, <, >=, <=, ==, !=
сравнения по меткам времени в JPA-QLДолжен быть в состоянии получить то же самое
ZoneOffset
по состоянию наOffsetDateTime
это было сохранено
1 ответ
// Редактировать: я обновил ответ, чтобы отразить различия между версиями JPA 2.1 и 2.2.
// Редактировать 2: Добавлена ссылка на спецификацию JPA 2.2
Проблема с JPA 2.1
JPA v2.1 не знает о типах java 8 и попытается привести указанное значение в соответствие. Для LocalDateTime, Instant и OffsetDateTime он будет использовать метод toString() и сохранит соответствующую строку в целевом поле.
При этом вы должны указать JPA, как преобразовать ваше значение в соответствующий тип базы данных, например java.sql.Date
или же java.sql.Timestamp
,
Внедрить и зарегистрировать AttributeConverter
интерфейс, чтобы сделать эту работу.
Увидеть:
- https://stuetzpunkt.wordpress.com/2015/03/15/persisting-localdate-with-jpa-2-1/
- http://www.adam-bien.com/roller/abien/entry/new_java_8_date_and
Остерегайтесь ошибочной реализации Adam Bien: LocalDate должен быть сначала зонирован.
Использование JPA 2.2
Только не создавайте атрибут конвертеров. Они уже включены.
// Обновление 2:
Вы можете увидеть это в спецификации здесь: JPA 2.2 spec. Прокрутите до самой последней страницы, чтобы увидеть, что типы времени включены.
Если вы используете выражения jpql, обязательно используйте объект Instant, а также используйте Instant в ваших классах PDO.
например
// query does not make any sense, probably.
query.setParameter("createdOnBefore", Instant.now());
Это работает просто отлично.
С помощью java.time.Instant
вместо других форматов
В любом случае, даже если у вас есть ZonedDateTime или OffsetDateTime, результат, считанный из базы данных, всегда будет в формате UTC, поскольку база данных хранит момент времени независимо от часового пояса. Часовой пояс на самом деле просто отображать информацию (метаданные).
Поэтому я рекомендую использовать Instant
вместо этого и конвертируйте его в классы Zoned или Offset Time только при необходимости. Чтобы восстановить время в данной зоне или смещении, сохраните зону или смещение отдельно в своем поле базы данных.
Сравнения JPQL будут работать с этим решением, просто продолжайте работать с моментами все время.
PS: я недавно разговаривал с некоторыми парнями из Spring, и они также согласились, что вы никогда не сохраните ничего, кроме Instant. Только момент является конкретным моментом времени, который затем может быть преобразован с использованием метаданных.
Использование составного значения
Согласно спецификации JPA 2.2, CompositeValues не упоминаются. Это означает, что они не вошли в спецификацию, и вы не можете сохранить одно поле в нескольких столбцах базы данных в настоящее время. Ищите "Композит" и смотрите только упоминания, связанные с идентификаторами.
Как бы то ни было, Hibernate может быть способен сделать это, как упоминалось в комментариях к этому ответу.
Пример реализации
Я создал этот пример с учетом этого принципа: открыт для расширения, закрыт для модификации. Подробнее об этом принципе читайте здесь: Открытый / закрытый принцип в Википедии.
Это означает, что вы можете сохранить свои текущие поля в базе данных (временная метка), и вам нужно только добавить дополнительный столбец, который не должен повредить.
Кроме того, ваша сущность может сохранять установщики и получатели OffsetDateTime. Внутренняя структура не должна беспокоить абонентов. Это означает, что это предложение не должно навредить вашему API.
Реализация может выглядеть так:
@Entity
public class UserPdo {
@Column(name = "created_on")
private Instant createdOn;
@Column(name = "display_offset")
private int offset;
public void setCreatedOn(final Instant newInstant) {
this.createdOn = newInstant;
this.offset = 0;
}
public void setCreatedOn(final OffsetDateTime dt) {
this.createdOn = dt.toInstant();
this.offset = dt.getOffset().getTotalSeconds();
}
// derived display value
public OffsetDateTime getCreatedOnOnOffset() {
ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(this.offset);
return this.createdOn.atOffset(zoneOffset);
}
}
Не хранить Instant
в базе данных используйте OffsetDateTime
.
Всегда храните UTC в базе данных.
OffsetDateTime
добавляет смещение от UTC/ Гринвича, которое Instant
не!
И нет необходимости добавлять два столбца, если db поддерживает "TIMESTAMP WITH TIME ZONE".
Для использования jpa
@Column(name = "timestamp", columnDefinition = "TIMESTAMP WITH TIME ZONE")
С OffsetDateTime
легко конвертировать в пользователей LocalDateTime
впоследствии, потому что вы знаете смещение от UTC, полученное OffsetDateTime
.