Подтвердить что ты не робот

JPA Хранение OffsetDateTime с ZoneOffset

У меня есть следующий класс сущностей:

@Entity
public class Event {
    private OffsetDateTime startDateTime;
    // ...
}

Однако сохранение и последующее чтение объекта в/из базы данных с помощью JPA 2.2 приводит к потере информации: ZoneOffset of 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 который был сохранен

4b9b3361

Ответ 1

//Edit: я обновил ответ, чтобы отразить различия между 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 чтобы сделать эту работу.

Видеть:

Остерегайтесь ошибочной реализации Адама Биена: сначала нужно сначала зонировать 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: Недавно я поговорил с некоторыми ребятами из Весны, и они также согласились, что вы никогда не сохраняете ничего, кроме Мгновенного. Только мгновение - это конкретный момент времени, который затем может быть преобразован с использованием метаданных.

Использование составного значения

Согласно спецификации spec JPA 2.2, CompositeValues не упоминаются. Это означает, что они не вошли в спецификацию, и вы не можете перенести одно поле в несколько столбцов базы данных в это время. Найдите "Композитный" и увидите только упоминания, связанные с идентификаторами.

Howverver, Hibernate мог бы это сделать, как упоминалось в этом комментарии к этому ответу.

Пример реализации

Я создал этот пример с учетом этого принципа: Open для расширения, закрытого для модификации. Подробнее об этом принципе читайте здесь: Открытый/Закрытый принцип в Википедии.

Это означает, что вы можете сохранить текущие поля в базе данных (timestamp), и вам нужно добавить дополнительный столбец, который не должен повредить.

Кроме того, ваша организация может хранить сеттеры и получатели OffsetDateTime. Внутренняя структура не должна касаться вызывающих абонентов. Это означает, что это предложение не должно повредить вашему апи.

Реализация может выглядеть так:

@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);
  }
}