JPA Hibernate中OffsetDateTime字段误判变更触发重复更新问题
问题根因
核心原因是Hibernate对OffsetDateTime类型的脏检查逻辑,没有调用OffsetDateTime原生的equals()方法做时间点等价判断。
Postgres的TIMESTAMP WITH TIME ZONE类型实际存储的是UTC精度的时间戳,本身不保存时区信息,JDBC驱动读取该类型值时,会按照当前连接配置的时区自动转换为对应偏移的OffsetDateTime实例:比如你连接配置的时区是UTC+2,存的UTC时间2022-06-12T06:19:26Z读出来就会变成2022-06-12T08:19:26+02:00,两个值指向完全相同的时间点,仅时区偏移和本地时间表示不同。
Hibernate 5及更早版本对JSR310时间类型的脏检查逻辑,会逐字段比对实体当前值和持久化上下文里存的加载快照值:比对OffsetDateTime时,不会先把两个值归一化到同一时区/转成Instant比较时间点,而是直接比较两个实例的本地日期时间数值、偏移量两个属性,只要任意属性不同,哪怕实际代表同一时间点,也会判定字段发生了变更。
顺带提一句,你贴的验证代码分支逻辑写反了,Objects are equals的日志打在了equals判断为false的else分支里,不过你确认两个时间点等价的结论是对的。
现象匹配验证
你观察到的所有现象都和这个逻辑完全吻合:
- 实体第一次从数据库加载后,Hibernate拍的字段快照里,
createdAt是JDBC返回的2022-06-12T08:19:26+02:00 - 你手动调用setter传入JSON解析得到的
2022-06-12T06:19:26Z,实体当前值和快照的偏移量、本地时间表示都不一致,脏检查直接判定字段有变更,触发UPDATE语句 - 注释掉setter调用时,实体字段值和快照完全一致,Hibernate自然不会判定为脏,也就不会触发不必要的更新
可行解决方案
你可以根据自己的项目情况选任意一种:
- 统一JDBC连接时区为UTC:在Postgres JDBC连接串后添加参数
options=-c timezone=UTC,强制驱动读写TIMESTAMPTZ时统一返回UTC偏移的时间值,和接口传入的Z格式时间完全对齐,从根源避免偏移不一致问题 - 业务层赋值时做偏移归一化:把传入的OffsetDateTime提前转成JDBC连接使用的对应时区偏移值再赋值给实体,保证和快照的字段表示完全一致,示例代码:
OffsetDateTime inputTime = OffsetDateTime.parse(batchProduct.getCreatedAt()); // 替换成你JDBC连接实际使用的时区 OffsetDateTime normalizedTime = inputTime.atZoneSameInstant(ZoneId.of("+02:00")).toOffsetDateTime(); product.setCreatedAt(normalizedTime); - 自定义Hibernate类型映射:给OffsetDateTime字段实现自定义UserType,重写脏检查比对逻辑,比较时先把两个值转成Instant再对比时间点,忽略偏移量差异,不需要业务代码做额外处理
- 升级Hibernate到6.x版本:新版本已经修复了JSR310时间类型的脏检查逻辑,比对OffsetDateTime、ZonedDateTime类型时会自动做时间点归一化,不会因为偏移表示不同误判变更
内容的提问来源于stack exchange,提问作者Levijatanu
相关产品推荐
相关产品推荐

