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 интерфейс, чтобы сделать эту работу.

Увидеть:

Остерегайтесь ошибочной реализации 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.

Другие вопросы по тегам