JPA 使用 ZoneOffset 存储 OffsetDateTime
JPA Storing OffsetDateTime with ZoneOffset
我有以下实体 class:
@Entity
public class Event {
private OffsetDateTime startDateTime;
// ...
}
但是,使用 JPA 2.2 持久化然后读取实体 to/from 数据库会导致信息丢失:ZoneOffset
of [=13] =] 更改为 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
中对时间戳执行 >, <, >=, <=, ==, !=
比较
需要能够检索与 OffsetDateTime
相同的 ZoneOffset
已被持久化
// 编辑:我更新了答案以反映 JPA 2.1 版和 2.2 版之间的差异。
// 编辑 2:添加了 JPA 2.2 规范 link
JPA 2.1 的问题
JPA v2.1 不知道 java 8 种类型,并将尝试将提供的值字符串化。对于 LocalDateTime、Instant 和 OffsetDateTime,它将使用 toString() 方法并将相应的字符串保存到目标字段。
也就是说,您必须告诉 JPA 如何将您的值转换为相应的数据库类型,例如 java.sql.Date
或 java.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 需要首先被 Zoned。
使用 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。这意味着,他们没有将其纳入规范,此时您不能将单个字段持久化到多个数据库列中。搜索 "Composite" 并仅查看与 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);
}
}
不要在数据库中存储 Instant
,使用 OffsetDateTime
。
始终将 UTC 存储在数据库中。
OffsetDateTime
附加了 UTC/Greenwich 的偏移量,而 Instant
没有!
如果数据库支持 "TIMESTAMP WITH TIME ZONE",则无需添加两列。
用于 jpa
@Column(name = "timestamp", columnDefinition = "TIMESTAMP WITH TIME ZONE")
使用 OffsetDateTime
之后很容易转换为用户 LocalDateTime
,因为您知道 OffsetDateTime
.
获得的 UTC 偏移量
我有以下实体 class:
@Entity
public class Event {
private OffsetDateTime startDateTime;
// ...
}
但是,使用 JPA 2.2 持久化然后读取实体 to/from 数据库会导致信息丢失:ZoneOffset
of [=13] =] 更改为 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
中对时间戳执行 需要能够检索与
OffsetDateTime
相同的ZoneOffset
已被持久化
>, <, >=, <=, ==, !=
比较
// 编辑:我更新了答案以反映 JPA 2.1 版和 2.2 版之间的差异。
// 编辑 2:添加了 JPA 2.2 规范 link
JPA 2.1 的问题
JPA v2.1 不知道 java 8 种类型,并将尝试将提供的值字符串化。对于 LocalDateTime、Instant 和 OffsetDateTime,它将使用 toString() 方法并将相应的字符串保存到目标字段。
也就是说,您必须告诉 JPA 如何将您的值转换为相应的数据库类型,例如 java.sql.Date
或 java.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 需要首先被 Zoned。
使用 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。这意味着,他们没有将其纳入规范,此时您不能将单个字段持久化到多个数据库列中。搜索 "Composite" 并仅查看与 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);
}
}
不要在数据库中存储 Instant
,使用 OffsetDateTime
。
始终将 UTC 存储在数据库中。
OffsetDateTime
附加了 UTC/Greenwich 的偏移量,而 Instant
没有!
如果数据库支持 "TIMESTAMP WITH TIME ZONE",则无需添加两列。
用于 jpa
@Column(name = "timestamp", columnDefinition = "TIMESTAMP WITH TIME ZONE")
使用 OffsetDateTime
之后很容易转换为用户 LocalDateTime
,因为您知道 OffsetDateTime
.