JPA使用ZoneOffset存储OffsetDateTime

Chr*_*lli 18 datetime hibernate jpa java-date jpa-2.2

我有以下实体类:

@Entity
public class Event {
    private OffsetDateTime startDateTime;
    // ...
}
Run Code Online (Sandbox Code Playgroud)

然而,持续的,然后从与JPA 2.2结果数据库读取的实体/在信息丢失:所述ZoneOffsetstartDateTime改变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
Run Code Online (Sandbox Code Playgroud)

我需要使用OffsetDateTime:我无法使用ZonedDateTime,因为区域规则发生了变化(而且还会受到此信息丢失的影响).我无法使用LocalDateTime,因为Event世界上任何地方都会发生这种情况,因为ZoneOffset出于准确性原因我需要原件.我无法使用,Instant因为用户填写了事件的开始时间(事件就像约会).

要求:

  • 需要能够>, <, >=, <=, ==, !=在JPA-QL中对时间戳进行比较

  • 需要能够检索相同ZoneOffset的的OffsetDateTime这是坚持

Ben*_*Ben 10

//编辑:我更新了答案以反映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规范.滚动到最后一页,查看包含的时间类型.

如果使用jpql表达式,请确保使用Instant对象并在PDO类中使用Instant.

例如

// query does not make any sense, probably.
query.setParameter("createdOnBefore", Instant.now());
Run Code Online (Sandbox Code Playgroud)

这很好用.

使用java.time.Instant而不是其他格式

无论如何,即使您有ZonedDateTime或OffsetDateTime,从数据库读取的结果也将始终为UTC,因为无论时区如何,数据库都会及时存储.时区实际上只是显示信息(元数据).

因此,我建议改为使用Instant,并仅在需要时将其转换为Zoned或Offset Time类.要恢复给定区域或偏移量的时间,请将区域或偏移量分别存储在其自己的数据库字段中.

JPQL比较将与此解决方案一起使用,只需一直处理瞬间.

PS:我最近和一些春天的人谈过,他们也同意你永远不会坚持除了瞬间.只有瞬间才是特定时间点,然后可以使用元数据进行转换.

使用复合值

根据规范JPA 2.2规范,未提及CompositeValues.这意味着,他们没有将其纳入规范,并且您目前无法将单个字段保留到多个数据库列中.搜索"复合"并仅查看与ID相关的提及.

但是,正如本答案的评论中所提到的,Hibernate可能会做到这一点.

示例实现

我用这个原则创建了这个例子:打开扩展,关闭修改.在此处阅读有关此原则的更多信息:维基百科上的开放/封闭原则.

这意味着,您可以将当前字段保留在数据库中(时间戳),您只需要添加一个额外的列,这不应该受到伤害.

此外,您的实体可以保留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);
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 是这样吗?[JPA 2.2 中的新功能 – Java 8 日期和时间类型](https://vladmihalcea.com/whats-new-in-jpa-2-2-java-8-date-and-time-types/) (2认同)
  • 对我来说,坚持“OffsetDateTime”听起来很简单。我是否遗漏了一些东西(除了它需要一个可以存储日期时间偏移量的数据类型的数据库字段这一事实)? (2认同)

nim*_*o23 5

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

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

OffsetDateTime附加相对于 UTC/格林威治的偏移量,其中Instant并非如此!

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

供jpa使用

@Column(name = "timestamp", columnDefinition = "TIMESTAMP WITH TIME ZONE")
Run Code Online (Sandbox Code Playgroud)

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

  • 如果我们谈论 Postgres,`TIMESTAMP WITH TIME ZONE` 实际上并不存储时区:https://phili.pe/posts/timestamps-and-time-zones-in-postgresql/ 所以如果你坚持使用 db,这实际上存储时区信息,也许这是有道理的......但更通用和清晰的方法是存储上面提到的 Instant 。 (3认同)