JPA 2.2存储OffsetDateTime时区偏移丢失问题咨询
我之前也碰到过这个问题!JPA 2.2对OffsetDateTime的默认处理确实会丢失时区偏移信息——本质是它默认把时间转换为UTC时区,存储到不带时区的数据库时间字段中,读取时直接用UTC偏移量还原OffsetDateTime,导致原始的时区信息没了。
就像你描述的场景:
Event e = new Event(); e.setStartDateTime(OffsetDateTime.parse("2018-01-02T09:00-05:00")); System.out.println(e.getStartDateTime().getHour()); // 输出9 System.out.println(e.getStartDateTime().getOffset()); // 输出-05:00 // 持久化再读取 entityManager.persist(e); entityManager.flush(); entityManager.clear(); Event fetched = entityManager.find(Event.class, e.getId()); System.out.println(fetched.getStartDateTime().getHour()); // 变成14(UTC时间) System.out.println(fetched.getStartDateTime().getOffset()); // 变成Z(UTC偏移)
这里有几个实用的方案,你可以根据自己的数据库环境和需求选择:
1. 使用数据库的带时区时间类型
如果你的数据库支持带时区的时间字段(比如PostgreSQL的TIMESTAMPTZ、MySQL 8.0+的TIMESTAMP WITH TIME ZONE),直接给字段指定对应的数据库类型即可:
@Entity public class Event { @Column(columnDefinition = "TIMESTAMPTZ") // PostgreSQL示例,MySQL替换为TIMESTAMP WITH TIME ZONE private OffsetDateTime startDateTime; // ...其他字段和方法 }
这种方式最省心,数据库会完整存储时区偏移信息,读取时直接还原原始的OffsetDateTime。
2. 自定义属性转换器(AttributeConverter)
如果没法修改数据库字段类型,或者需要兼容现有数据结构,可以写一个自定义转换器,把OffsetDateTime序列化为带时区的ISO-8601字符串存储:
@Converter(autoApply = true) // autoApply=true会自动应用到所有OffsetDateTime字段 public class OffsetDateTimeConverter implements AttributeConverter<OffsetDateTime, String> { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; @Override public String convertToDatabaseColumn(OffsetDateTime offsetDateTime) { return offsetDateTime != null ? offsetDateTime.format(FORMATTER) : null; } @Override public OffsetDateTime convertToEntityAttribute(String dbData) { return dbData != null ? OffsetDateTime.parse(dbData, FORMATTER) : null; } }
然后实体类可以不用额外注解(如果设置了autoApply=true),或者手动指定:
@Entity public class Event { @Convert(converter = OffsetDateTimeConverter.class) private OffsetDateTime startDateTime; // ... }
存储的字符串会是2018-01-02T09:00:00-05:00这种格式,读取时能完整解析出原始时区偏移。
3. 拆分时间和时区为单独字段
另一种思路是把OffsetDateTime拆成Instant(时间戳)和ZoneOffset(时区偏移)两个字段存储,然后通过自定义getter/setter封装成OffsetDateTime:
@Entity public class Event { private Instant startInstant; private ZoneOffset startOffset; // 对外暴露的OffsetDateTime getter/setter public OffsetDateTime getStartDateTime() { return startInstant != null && startOffset != null ? OffsetDateTime.ofInstant(startInstant, startOffset) : null; } public void setStartDateTime(OffsetDateTime offsetDateTime) { if (offsetDateTime != null) { this.startInstant = offsetDateTime.toInstant(); this.startOffset = offsetDateTime.getOffset(); } else { this.startInstant = null; this.startOffset = null; } } // ...其他字段和方法 }
这种方式也能完美保留时区信息,而且不需要修改数据库字段类型(Instant对应TIMESTAMP,ZoneOffset可以存为字符串或整数)。
用上面任意一种方案,再跑你的测试代码,读取后的OffsetDateTime都会和原始值一致:
Event fetched = entityManager.find(Event.class, e.getId()); System.out.println(fetched.getStartDateTime().getHour()); // 输出9 System.out.println(fetched.getStartDateTime().getOffset()); // 输出-05:00
内容的提问来源于stack exchange,提问作者Christopher Chianelli

