Spring Boot JPA Instant/ZonedDateTime数据库Map

6tdlim6h  于 12个月前  发布在  Spring
关注(0)|答案(2)|浏览(229)

我遇到了一个java.time类和它们到DB类型的Map的问题。我想存储Instant类型,但它的行为很不直观。
简单的项目示例:https://gitlab.com/Gobanit/jpa-time-example
我有基本的实体类:

@Entity
@Getter
@Setter
public class AbstractEntity {
    @Id
    @GeneratedValue
    private Long id;
//    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
    private Instant createdAt;
//    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
    private ZonedDateTime createdAtZoned;
    private LocalDateTime createdAtLocal;
    private Instant modifiedAt;

    @PrePersist
    protected void prePersist() {
        createdAt = Instant.now();
        modifiedAt = createdAt;
        createdAtZoned = createdAt.atZone(ZoneId.systemDefault());
        createdAtLocal = createdAtZoned.toLocalDateTime();
        System.out.println(String.format("Created: instant=%s, zoned=%s, local=%s", createdAt, createdAtZoned, createdAtLocal));
    }

    @PreUpdate
    protected void preUpdate() {
        modifiedAt = Instant.now();
    }
}

字符串
现在,我将创建并持久化实体:

@Transactional
    @GetMapping("create")
    public AbstractEntity createNew() {
        System.out.println("create");
        System.out.println("ZoneId=" + ZoneId.systemDefault());
        var e = new AbstractEntity();
        em.persist(e);
        return e;
    }


Java中的数据与预期一致-系统输出如下:

ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:13:55.624902400Z, zoned=2021-12-29T15:13:55.624902400+01:00[Europe/Bratislava], local=2021-12-29T15:13:55.624902400


然而,数据库中的数据不是。它们都存储为相同的值。所有列的默认类型都设置为TIMESTAMP。并且它们都存储为应用程序JVM时区中的LocalDateTime。
x1c 0d1x的数据
我一点也不喜欢这样,这些类型之间没有区别,值的“含义”取决于应用程序的区域。如果我在不知道哪个应用程序写入了数据以及它的区域是什么的情况下查看数据库,我就不能说日期时间意味着什么。更糟糕的是,如果我用不同的区域运行应用程序,它会假设值是在它的时区写入的,而事实并非如此。
有些人说,时间数据应该存储在UTC中(我部分同意),他们建议设置以下属性,以便在通过JDBC存储在DB中之前将值转换为UTC区域:

spring.jpa.properties.hibernate.jdbc.time_zone: UTC


然而,这会导致更糟糕的行为,因为它不仅适用于Instant/ZonedDateTime,而且适用于LocalDateTime,这在我看来在逻辑上是错误的。LocalDateTime值根本不应该受到任何时区转换的影响,这就是为什么它是LocalDateTime。
系统输出:

ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:15:13.614799900Z, zoned=2021-12-29T15:15:13.614799900+01:00[Europe/Bratislava], local=2021-12-29T15:15:13.614799900


DB结果:



我希望其中之一会发生:
1.默认情况下,将Instant和ZonedDateTimeMap到TIMESTAMP_WITH_TIMEZONE。此外,Instant类型应始终使用UTC zone序列化-就像打印到字符串或使用Jackson序列化时一样。(理想)
1.将Instant和ZonedDateTimeMap到TIMESTAMP,但在序列化时将其转换为UTC,并在序列化时将其转换回JVM时区。(不理想,但足够好)
我可以通过取消下面一行的注解来显式地覆盖默认Map。

// @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")


这很好,只有Instant没有转换为UTC,而是采用JVM区域,就像ZonedDateTime一样。从技术上讲,这完全没有问题,我看不到任何不一致的结果,因为这一点,但我个人更喜欢他们在UTC,所以他们总是统一的。第二个问题是,我需要为每个字段显式配置它。

ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:19:14.600583400Z, zoned=2021-12-29T15:19:14.600583400+01:00[Europe/Bratislava], local=2021-12-29T15:19:14.600583400



最后,我有几个问题,我真的很想有人回答我:)**
1.有没有办法得到理想的解决方案(第一个提到的)?
1.有没有办法至少全局配置类型Map?
1.你能看到这种方法的任何风险吗?
1.是否有一个更好或更标准化的方法,我不知道?什么是你的工作策略与时间?
谢谢你,谢谢

ntjbwcob

ntjbwcob1#

我不能谈论JPA,但我可以解释一些JDBC问题。
我想存储Instant类型
JDBC规范没有MapInstant

  • 对于类似于SQL标准类型TIMESTAMP WITHOUT TIME ZONE的列,请使用Java类LocalDateTime
  • 对于类似于SQL标准类型TIMESTAMP WITH TIME ZONE的列,使用Java类OffsetDateTime

在JDBC中,使用setObjectgetObject在JDBC 4.2及更高版本中将数据时间值交换为 java.time 值。

myPreparedStatement.setObject( … , myOffsetDateTime ) ;

字符串
某些数据库(如Postgres)会自动将传入数据调整为UTC,即0小时-分钟-秒的偏移量。对于跨数据库的可移植代码,您可能希望在Java代码中进行调整。

OffsetDateTime myOdt = otherOdt.withOffsetSameInstant( ZoneOffset.UTC ) ;


将当前时刻捕捉为OffsetDateTime

OffsetDateTime myOdt = OffsetDateTime.now( ZoneOffset.UTC ) ;


如果您手头有一个Instant,请转换为OffsetDateTime,以便通过JDBC存储在数据库中。

OffsetDateTime myOdt = myInstant.atOffset( ZoneOffset.UTC ) ;


不需要在数据库中存储分区或未分区的值。您可以在数据库检索后轻松地在Java代码中调整时区,就像您为其他本地化需求所做的那样。

ZoneId z = ZoneId.of( "America/Edmonton" ) ;
ZonedDateTime zdt = myOdt.atZoneSameInstant( z ) ;


在处理时刻(时间轴上的特定点)时,我通常看不到使用LocalDateTime的好处。该类故意缺少时区或偏移量的上下文。此类对象只包含日期和一天中的时间,因此本质上是模糊的。您可以从OffsetDateTime转换为LocalDateTime,但会剥离重要信息,而不会获得任何回报。
它们都像应用程序JVM时区中的LocalDateTime一样存储。
这是一个自相矛盾的术语。LocalDateTime没有时区或偏移。
所有这些列的默认类型都设置为TIMESTAMP。
列的数据类型没有默认值。您可以在创建列时指定数据类型。插入行时数据类型不会更改。只有在发出ALTER TABLE命令时,数据类型才能更改。
另外,我们不知道你用TIMESTAMP这个词是什么意思,因为你忽略了提到你的特定数据库。如上所述,SQL标准使用四个词来表示两种非常不同的时间戳类型。
将Instant和ZonedDateTimeMap到TIMESTAMP,但在序列化时将其转换为UTC,
不需要,JDBC不需要InstantZonedDateTime的任何此类Map。
您需要在JDBC调用之外的Java代码中执行此类转换。
唯一的例外是,您的特定JDBC驱动程序可能会选择超出JDBC规范的要求。如果驱动程序的创建者认为合适,则可以自由处理InstantZonedDateTime类型。但是要注意,您编写的任何此类代码可能无法在其他JDBC驱动程序之间移植。
并返回到JVM时区。
我相信,如果作为一名程序员/DBA/SysAdmin,您在UTC(偏移量为零)中进行大部分思考、日志记录、调试、数据存储和数据交换,您会发现这项工作要容易得多。
转换为Instant是调整到UTC的一种简单方法。

Instant instant = odt.toInstant() ;
Instant instant = zdt.toInstant() ;


提示:一般来说,编写代码时 * 不要 * 依赖于JVM、数据库会话或主机操作系统的默认时区。这些默认值不受程序员的控制。这些默认值可以在运行时的任何时刻更改。因此,只有在需要时才使用默认值,例如本地化到用户的移动终端的默认区域,以便在用户界面中显示。并使您的默认使用显式,总是为各种调用指定可选的zone/offset参数。
提示:记录插入或更新行的时刻通常最好留给数据库。使用触发器,数据库服务器捕获当前时刻并将其分配给字段。然后,您可以进行Java应用程序之外的操作,例如批量数据加载。

qnakjoqk

qnakjoqk2#

如果你想存储一个瞬间的时间,你应该用java.time.Instant来操作它。你总是可以用java.time.ZonedDateTime来显示它,然后你必须决定如何存储它:

  • 作为某种日期时间列(取决于您的DBMS)
  • 只要精确到秒
  • 只要有毫秒精度

如果你用datetime列存储它,你应该使用java.sql.Timestampjava.time.OffsetTime的转换器。
我真的更喜欢把它存储在一个长列中,因为它是便携式的,你知道它到底意味着什么。
以下是转换器:

@Converter
public class InstantAsSeconds implements AttributeConverter<Instant, Long> {

    @Override
    public Long convertToDatabaseColumn(Instant attribute) {
        return attribute == null ? null : attribute.getEpochSecond();
    }

    @Override
    public Instant convertToEntityAttribute(Long dbData) {
        return dbData == null ? null : Instant.ofEpochSecond(dbData);
    }
}

@Converter
public class InstantAsMilliseconds implements AttributeConverter<Instant, Long> {

    @Override
    public Long convertToDatabaseColumn(Instant attribute) {
        return attribute == null ? null : attribute.toEpochMilli();
    }

    @Override
    public Instant convertToEntityAttribute(Long dbData) {
        return dbData == null ? null : Instant.ofEpochMilli(dbData);
    }
}

@Converter
public class InstantAsTimestamp implements AttributeConverter<Instant, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(Instant attribute) {
        return attribute == null ? null : Timestamp.from(attribute);
    }

    @Override
    public Instant convertToEntityAttribute(Timestamp dbData) {
        return dbData == null ? null : dbData.toInstant();
    }
}

@Converter
public class InstantAsOffsetTime implements AttributeConverter<Instant, OffsetDateTime> {

    @Override
    public OffsetDateTime convertToDatabaseColumn(Instant attribute) {
        return attribute == null ? null : attribute.atOffset(ZoneOffset.UTC);
    }

    @Override
    public Instant convertToEntityAttribute(OffsetDateTime dbData) {
        return dbData == null ? null : dbData.toInstant();
    }
}

字符串

相关问题