【问题标题】:JPA Storing OffsetDateTime with ZoneOffsetJPA 使用 ZoneOffset 存储 OffsetDateTime
【发布时间】:2018-10-06 06:47:17
【问题描述】:

我有以下实体类:

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

但是,使用 JPA 2.2 将实体持久化并从数据库读取实体会导致信息丢失startDateTimeZoneOffset 更改为 UTC(@987654325 @ 由数据库时间戳使用)。例如:

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 中的时间戳执行 >, <, >=, <=, ==, != 比较

  • 需要能够检索与持久化的OffsetDateTime 相同的ZoneOffset

【问题讨论】:

  • 我认为这是一个很好的问题,因为文档没有说明很多关于使用 JPA 持久化这些类的内容,并且 EE7 中包含的 JPA 不支持开箱即用地持久化这些类。除此之外,“时间点”和“元信息”的区别可能会令人困惑。这就是为什么我赞成这个问题并指出我的回答中的差异。干杯!
  • @GeoffreyDeSmet 我再次更新了答案,并解释了为什么如果您之前编写过 PDO 代码,添加字段应该不是问题。另外,我添加了规范链接。
  • 感谢 Ben - 非常出色的工作 - 但我们有一个硬性要求,即我们的 Event 类(这是一个人类事件,而不是系统事件)有一个字段 OffsetDateTime startOffsetDateTime end。非简单的 getter 对我们来说不是一个可行的解决方法。例如,jackson 注释也在字段级别。
  • 您的OffsetDateTime 最终保存在哪里?什么数据类型的数据库列?

标签: hibernate datetime jpa date jpa-2.2


【解决方案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.Datejava.sql.Timestamp

实现并注册AttributeConverter 接口以使其工作。

见:

注意 Adam Bien 的错误实现:LocalDate 需要先进行分区。

使用 JPA 2.2

只是不要创建属性转换器。它们已经包含在内。

// 更新 2:

您可以在此处的规格中看到这一点:JPA 2.2 spec。滚动到最后一页以查看是否包含时间类型。

如果您使用 jpql 表达式,请务必使用 Instant 对象,并在您的 PDO 类中使用 Instant。

例如

// 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 spec,未提及 CompositeValues。这意味着,他们没有将其纳入规范,此时您不能将单个字段持久保存到多个数据库列中。搜索“复合”并仅查看与 ID 相关的提及。

但是,Hibernate 可能能够做到这一点,如本答案的 cmets 中所述。

示例实现

我创建这个示例时考虑到了这个原则:对扩展开放,对修改关闭。在此处阅读有关此原则的更多信息:Open/Closed Principle on Wikipedia

这意味着,您可以将当前字段保留在数据库中(时间戳),并且您只需要添加一个额外的列,这不会有什么坏处。

此外,您的实体可以保留 OffsetDateTime 的 setter 和 getter。调用者不应该关心内部结构。这意味着,这个提议根本不应该伤害你的 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);
  }
}

【讨论】:

  • 对我来说,坚持OffsetDateTime 听起来很简单。我是否遗漏了一些东西(除了它需要一个数据类型的数据库字段,它可以存储日期时间的偏移量)?
  • JPQL 比较将不起作用,因为比较是在数据库中的数据上完成的,并且没有一对一的映射可以保留 ZoneOffset 并且所有比较都有效。可以使用的最好的是映射(x) -> [UTC timestamp of x][ZoneOffset of x],它适用于< >,但不适用于<= >= != ==。有一个有效的一对二映射(分别存储 Timestamp 和 ZoneOffset(这就是 @Ben 所说的)),但没有办法用 AttributeConverter 做到这一点(我知道)
  • @Ben 更新您的答案以包括您需要单独存储 ZoneOffset 以及示例新模型的事实,我会将其标记为正确答案(尽管我希望有一天JPA 将允许多对一转换)。
  • @GeoffreyDeSmet 我不知道您是否可以将单个属性转换为两个数据库列。无论如何,我的建议不会修改您的模型,它只会扩展您的模型。您可以保留现有的 getter 和 setter(开闭原则)。如果你不能添加另一个字段,据我所知,你会被卡住。根据我知道的规范,JPA 无法将单个属性转换为两列。
【解决方案2】:

不要在数据库中存储Instant,使用OffsetDateTime

始终将 UTC 存储在数据库中。

OffsetDateTime 附加了与 UTC/格林威治的偏移量,Instant 没有!

如果 db 支持“TIMESTAMP WITH TIME ZONE”,则无需添加两列。

供jpa使用

@Column(name = "timestamp", columnDefinition = "TIMESTAMP WITH TIME ZONE")

使用OffsetDateTime 之后很容易转换为用户LocalDateTime,因为您知道OffsetDateTime 获得的UTC 偏移量。

【讨论】:

  • 如果我们谈论 Postgres,TIMESTAMP WITH TIME ZONE 实际上并不存储时区:phili.pe/posts/timestamps-and-time-zones-in-postgresql 所以如果你坚持使用真正存储时区信息的 db,也许它是有道理的......但更通用明确的方法是如上所述存储 Instant。
猜你喜欢
  • 2021-02-02
  • 2015-05-26
  • 2018-10-19
  • 1970-01-01
  • 2011-03-24
  • 2016-11-10
  • 1970-01-01
  • 2020-08-22
  • 2021-03-15
相关资源
最近更新 更多