JPA使用ZoneOffset

时间:2018-04-25 17:33:54

标签: hibernate datetime jpa java-date jpa-2.2

我有以下实体类:

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

但是,持久化,然后使用JPA 2.2向/从数据库中读取实体会导致信息丢失ZoneOffset startDateTime更改为{{1} (数据库时间戳使用的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因为用户填写了事件的开始时间(事件就像约会)。

要求:

  • 需要能够在JPA-QL中对时间戳进行Instant比较

  • 需要能够检索与持久存储的>, <, >=, <=, ==, !=相同的ZoneOffset

2 个答案:

答案 0 :(得分:8)

//编辑:我更新了答案以反映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。

e.g。

// query does not make any sense, probably.
query.setParameter("createdOnBefore", Instant.now());

这很好用。

使用java.time.Instant代替其他格式

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

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

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

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

使用复合值

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

然而,正如本回答的评论所述,Hibernate可能会做到这一点。

示例实现

我在创建此示例时考虑到了这一原则:打开扩展,关闭以进行修改。在此处阅读有关此原则的更多信息: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);
  }
}

答案 1 :(得分:0)

请勿将Instant存储在数据库中,请使用OffsetDateTime

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

OffsetDateTime附加到UTC /格林威治的偏移量,而Instant不是!

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

对于jpa,请使用

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

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