hibernate - JPA 使用 ZoneOffset 存储 OffsetDateTime

标签 hibernate datetime jpa date jpa-2.2

我有以下实体类:

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

但是,使用 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

我需要使用 OffsetDateTime : 我不能使用 ZonedDateTime ,因为区域规则发生了变化(并且无论如何也会遭受这种信息丢失的影响)。我不能使用 LocalDateTime , 因为 Event发生在世界任何地方,我需要原始 ZoneOffset出于准确性原因,从它发生的时间开始。我无法使用 Instant因为用户填写了一个事件的开始时间(这个事件就像一个约会)。

要求:
  • 需要能够执行>, <, >=, <=, ==, != JPA-QL 中时间戳的比较
  • 需要能够检索到相同的ZoneOffsetOffsetDateTime 起被持久化了
  • 最佳答案

    //编辑:我更新了答案以反射(reflect) 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使这项工作的界面。

    看:

  • https://stuetzpunkt.wordpress.com/2015/03/15/persisting-localdate-with-jpa-2-1/
  • http://www.adam-bien.com/roller/abien/entry/new_java_8_date_and

  • 当心 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 可能能够做到这一点。

    示例实现

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

    关于hibernate - JPA 使用 ZoneOffset 存储 OffsetDateTime,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50028241/

    相关文章:

    spring - Spring数据存储库不会以多对多关系保存数据

    java - 使用 PostgreSQL 和 Tomcat 的简单 Web 应用程序

    mysql - 将 MySQL 格式的 (Date_format,Case) 替换为 ms-access 格式的 (Format,IIF)

    java - 在 hibernate 中一对多单向添加更多对象到现有列表中

    javascript - 如何更改 JavaScript 中日期选择的语言?

    输入日期的VBA解释

    java - 在 oracle Spring/JPA 中对分组行本身进行排序

    java - Hibernate @OneToOne 不映射 id

    java - Hibernate:我如何加入自己的表格?

    java - 我无法在 Hibernate 5.1.0 中获取 SessionFactory。我的代码在获取 SessionFactory 对象时中断行