JPA Хранение OffsetDateTime с ZoneOffset - PullRequest
0 голосов
/ 25 апреля 2018

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

@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 Ответ

0 голосов
/ 26 апреля 2018

// Редактировать: я обновил ответ, чтобы отразить различия между 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: Я недавно разговаривал с некоторыми парнями из 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);
  }
}
...