在Java实体中应该使用Instant、DateTime还是LocalDateTime?

huangapple go评论68阅读模式
英文:

Should I use Instant or DateTime or LocalDateTime in Java entities?

问题

在我的Java(使用Spring Boot和Spring Data JPA)应用程序中,我通常使用Instant。另一方面,我想要使用最适合时间值的数据类型。

1. 要精确保留时间戳,我应该选择哪种数据类型(我不确定Instant是否是最佳选项)?

2. 对于通常情况下只需要日期和时间的情况(据我所知,旧库已过时,但不确定应该使用哪个库)。

我还考虑时区,但不确定是否使用带有UTC的LocalDateTime可以解决我的问题。

将不确定的部分用括号括起来,其他部分保持不变。

英文:

In my Java (with Spring Boot and Spring Data JPA) applications, I generally use Instant. On the other hand, I would like to use the most proper data type for time values.

Could you please clarify me about these issues? What data type should I prefer for keeping date and time when:

1. To keep time precisely as timestamp (I am not sure if Instant is the best option)?

2. For normal cases when I just need date and time (as far as I know, the old library was obsolete, but not sure which library should I use).

I also consider the TimeZone, but not sure if using LocalDateTime with UTC solves my problem.

Any help would be appreciated.

答案1

得分: 4

1 rzwitserloot 的答案是正确且明智的。此外,以下是各种类型的摘要。有关更多信息,请参见我的答案 中类似问题的回答。

  1. 精确保持时间戳 (我不确定 Instant 是否是最佳选择)?

如果您想精确跟踪某一时刻,时间轴上的特定点:

  • Instant
    一个瞬间,以零时分秒的偏移量为基准。这个类是 java.time 框架的基本构建块。
  • OffsetDateTime
    一个瞬间,以特定的偏移量为基准,相对于协调世界时 (UTC) 的小时、分钟、秒数。这个类是 java.time 框架的基本构建块。
  • ZonedDateTime
    一个瞬间,以特定的时区为基准。时区是一个命名的历史,用于描述某个地区的居民根据其政治家所决定的过去、现在和未来的偏移量变化。

如果您只想跟踪日期和时间,而不需要偏移量或时区的上下文,请使用 LocalDateTime。这个类不表示一个瞬间,也不是时间轴上的一个点。

  1. 对于通常情况,当我只需要日期和时间

如果您绝对确定只需要日期和时间,但不需要偏移量或时区的上下文,请使用 LocalDateTime

使用带有 UTC 的 LocalDateTime

这是矛盾的,毫无意义。LocalDateTime 类没有 UTC 的概念,也没有偏移量或时区的概念。

Spring Data JPA

JDBC 4.2+ 规范将 SQL 标准数据类型 映射到 Java 类。

  • TIMESTAMP WITH TIME ZONE 列映射到 Java 中的 OffsetDateTime
  • TIMESTAMP WITHOUT TIME ZONE 列映射到 Java 中的 LocalDateTime
  • DATE 列映射到 LocalDate
  • TIME WITHOUT TIME ZONE 列映射到 LocalTime

SQL 标准还提到了 TIME WITH TIME ZONE,但这种类型是没有意义的(仅仅是思考一下就可以明白)。据我所知,SQL 委员会从未解释过他们的想法。如果您 必须 使用这种类型,Java 定义了 ZoneOffset 类来匹配。

请注意,JDBC 将任何 SQL 类型映射到 InstantZonedDateTime。您可以轻松地转换为/从映射类型 OffsetDateTime

Instant instant = myOffsetDateTime.toInstant();
OffsetDateTime myOffsetDateTime = instant.atOffset(ZoneOffset.UTC);

还有:

ZonedDateTime zdt = myOffsetDateTime.atZoneSameInstant(myZoneId);
OffsetDateTime odt = zdt.toOffsetDateTime();  // 该时区在那一刻使用的偏移量。
OffsetDateTime odt = zdt.toInstant().atOffset(ZoneOffset.UTC);  // 零小时分钟秒的偏移量从 UTC。

我也考虑时区

TimeZone 类是可怕的旧日期时间类的一部分,早些年被现代的 java.time 类替代。现在应使用 ZoneIdZoneOffset 替代。

英文:

The Answer by rzwitserloot is correct and wise. In addition, here is a summary of the various types. For more info, see my Answer on a similar Question.

>1. To keep time precisely as timestamp (I am not sure if Instant is the best option)?

If you want to track a moment, a specific point on the timeline:

  • Instant<br />A moment as seen with an offset-from-UTC of zero hours-minutes-seconds. This class is the basic building-block of the java.time framework.
  • OffsetDateTime<br />A moment as seen with a particular offset, some number of hours-minutes-seconds ahead of, or behind, the temporal meridian of UTC.
  • ZonedDateTime<br />A moment as seen with a particular time zone. A time zone is a named history of the past, present, and future changes to the offset used by the people of a particular region, as decided by their politicians.

If you want to track just the date and time-of-day, without the context of an offset or time zone, use LocalDateTime. This class does not represent a moment, is not a point on the timeline.

>2. For normal cases when I just need date and time

If you are absolutely sure that you want only a date with time-of-day, but do not need the context of an offset or time zone, use LocalDateTime.

> using LocalDateTime with UTC

That is a contradiction, and makes no sense. A LocalDateTime class has no concept of UTC, nor any concept of offset-from-UTC or time zone.

> Spring Data JPA

The JDBC 4.2+ specification maps SQL standard data types to Java classes.

  • TIMESTAMP WITH TIME ZONE columns map to OffsetDateTime in Java.
  • TIMESTAMP WITHOUT TIME ZONE columns map to LocalDateTime in Java.
  • DATE columns map to LocalDate.
  • TIME WITHOUT TIME ZONE columns map to LocalTime.

The SQL standard also mentions TIME WITH TIME ZONE, but this type is meaningless (just think about it!). The SQL committee has never explained what they had in mind, as far as I know. If you must use this type, Java defines the ZoneOffset class to match.

Note that JDBC does not map any SQL types to Instant nor ZonedDateTime. You can easily convert to/from the mapped type OffsetDateTime.

Instant instant = myOffsetDateTime.toInstant() ;
OffsetDateTime myOffsetDateTime = instant.atOffset( ZoneOffset.UTC ) ;

… and:

ZonedDateTime zdt = myOffsetDateTime.atZoneSameInstant( myZoneId ) ;
OffsetDateTime odt = zdt.toOffsetDateTime() ;  // The offset in use at that moment in that zone.
OffsetDateTime odt = zdt.toInstant().atOffset( ZoneOffset.UTC ) ;  // Offset of zero hours-minutes-seconds from UTC.

>I also consider the TimeZone

The TimeZone class is part of the terrible legacy date-time classes that were years ago supplanted by the modern java.time classes. Replaced by ZoneId and ZoneOffset.

答案2

得分: 3

以下是已翻译的内容:

Let's assume we need to cover the full span of date and time concerns. If there is a certain concern you don't have, that either collapses various types into 'well then they are interchangeable' or simply means you don't need to use a certain part of the API. The point is, you need to understand what these types represent, and once you know that, you know which one to apply. Because even if various different java.time types all technically do what you want, code is more flexible and a lot simpler to read if the types you use represent the things you want them to. For the same reason String[] student = new String[] {"Joe McPringle", "56"}; is perhaps mechanically a way to represent a student's name and age, but things are just a lot simpler if you write a class Student { String name; int age; } and use that instead.

Local alarm clock

Imagine you want to wake up at 07:00 in the morning. Not because you have an appointment, you just like to be a fairly early riser.

So you set your alarm for 07:00 in the morning, go to sleep, and your alarm promptly goes off at 7. So far, so good. However, you then hop in a plane and fly from Amsterdam to New York. (it is 6 hours earlier in New York). You then go to sleep again. Should the alarm go off at 01:00 at night, or at 07:00 in the morning?

Both answers are correct. The question is, how do you 'store' that alarm, and to answer that question, you need to figure out what the alarm is attempting to represent.

If the intent is '07:00, wherever I might be at the time the alarm is supposed to go off', the correct data storage mechanism is java.time.LocalDateTime, which stores time in human terms (years, months, days, hours, minutes, and seconds) and not in computery terms (we'll get there later), and does not include a time zone at all. If the alarm is supposed to go off every day, then you don't want that either, as LDT stores date and time, hence the name, you'd use LocalTime instead.

That's because you wanted to store the concept of 'the alarm should go off at 7 o'clock' and nothing more than that. You had no intention of saying: "The alarm should go off when people in Amsterdam would agree it is currently 07:00", nor did you have the intent of saying: "When the universe arrives at this exact moment in time, sound the alarm". You had the intent of saying: "When it is 07:00 wherever you are now, sound the alarm", so store that, which is a LocalTime.

The same principle applies to LocalDate: It stores a year/month/day tuple with no notion of where.

This does draw some perhaps wonky conclusions: Given a LocalDateTime object, it is not possible to ask how long it'll take until that LDT arrives. It is also not possible for any given moment in time to be compared to an LDT, because these things are apples and oranges. The notion 'Feb 18th, 2023, 7 in the morning on the dot' isn't a singular time. After all, in New York that 'moment' occurs a full 6 hours earlier than it would in Amsterdam. You can only compare 2 LocalDateTimes.

Instead, you would have to first 'place' your LDT somewhere, by converting it to one of the other types (ZonedDateTime or even Instant) by asking the java.time API: Okay, I want this particular LDT in a certain time zone.

Hence, if you are writing your alarm app, you would have to take the stored alarm (a LocalTime object), convert it to an Instant (which is what the nature of 'what time is it now, i.e. System.currentTimeMillis()' works on), by saying: That LocalTime, on the current day in the current local timezone, as an instant, and THEN comparing those two results.

Human appointments

Imagine that, just before jetting off to New York, you made an appointment at your local (in Amsterdam) barber. Their agenda was kinda busy so the appointment was set for June 20th, 2025, at 11:00.

If you stay in New York for a few years, the correct time for your calendar to remind you that you have an appointment with your barber is certainly not at 10:00 on June 20th, 2025 in New York. You'd have missed the appointment by then. Instead, your phone should chirp at you that you have an hour left to get to your barber's (a bit tricky, from New York, sure) at 04:00 in the middle of the night.

It sure sounds like we can say that the barber's appointment is a specific instant in time. However, this is not correct. The EU has already adopted legislation, agreed upon by all member states, that all EU countries shall abolish daylight savings time. However, this law does not provide a deadline, and crucially, does not provide a time zone that each EU member state needs to pick. The Netherlands is therefore going to change time zones at some point. They will likely choose to stick either to permanent summer time (in which case they'd be at UTC+2 permanently, vs. their current situation where they are at UTC+2 in summer and UTC+1 in winter, with, notably, different dates when the switch happens vs. New York!), or stay on winter time, i.e. UTC+1 forever.

Let's say they choose to stick to winter time forever.

The day the gavel slams down in the Dutch parliament building enshrining into law that the Dutch will no longer advance the clocks in March is the day your appointment shifts by one hour. After all, your barber is not going to go into their appointment book and shift all appointments by an hour. No, your appointment will remain on June 20th, 2025, at 11:00. If you have a running clock ticking down the seconds until your barber appointment, when that gavel comes down it should jump by 3600 seconds.

This belies the point: That barber appointment truly is not a singular moment in time. It's a human/political agreement that your appointment is when Amsterdam universally agrees it is currently June 20th, 2025, 11:00 – and who knows when that moment will actually occur; it depends on political choices.

So, you cannot 'solve' this by storing an instant in time, and it shows how the concept 'instant in time' and 'year/month/day hour:minute:second in a certain timezone' are not quite interchangeable.

The correct data type for this concept is a ZonedDateTime. This represents a date time in human terms: year/month/day hour:second:minute, and the timezone. It doesn't shortcut by storing a moment in time in epoch millis or some such. If the gavel comes down and your JDK updates its timezone definitions, asking "how

英文:

Let's assume we need to cover the full span of date and time concerns. If there is a certain concern you don't have, that either collapses various types into 'well then they are interchangible' or simply means you don't need to use a certain part of the API. The point is, you need to understand what these types represent, and once you know that, you know which one to apply. Because even if various different java.time types all technically do what you want, code is more flexible and a lot simpler to read if the types you use represent the things you want them to. For the same reason String[] student = new String[] {&quot;Joe McPringle&quot;, &quot;56&quot;}; is perhaps mechanically a way to represent a student's name and age, but things are just a lot simpler if you write a class Student { String name; int age; } and use that instead.

Local alarm clock

Imagine you want to wake up at 07:00 in the morning. Not because you have an appointment, you just like to be a fairly early riser.

So you set your alarm for 07:00 in the morning, go to sleep, and your alarm promptly goes off at 7. So far, so good. However, you then hop in a plane and fly from Amsterdam to New York. (it is 6 hours earlier in new york). You then go to sleep again. Should the alarm go off at 01:00 at night, or at 07:00 in the morning?

Both answers are correct. The question is, how do you 'store' that alarm, and to answer that question, you need to figure out what the alarm is attempting to represent.

If the intent is '07:00, whereever I might be at the time the alarm is supposed to go off', the correct data storage mechanism is java.time.LocalDateTime, which stores time in human terms (years, months, days, hours, minutes, and seconds) and not in computery terms (we'll get there later), and does not include a time zone at all. If the alarm is supposed to go off every day, then you don't want that either, as LDT stores date and time, hence the name, you'd use LocalTime instead.

That's because you wanted to store the concept of 'the alarm should go off at 7 o'clock' and nothing more than that. You had no intention of saying: "The alarm should go off when people in Amsterdam would agree it is currently 07:00", nor did you have the intent of saying: "When the universe arrives at this exact moment in time, sound the alarm". You had the intent of saying: "When it is 07:00 where-ever you are now, sound the alarm", so store that, which is a LocalTime.

The same principle applies to LocalDate: It stores a year/month/day tuple with no notion of where.

This does draw some perhaps wonky conclusions: Given a LocalDateTime object, it is not possible to ask how long it'll take until that LDT arrives. It is also not possible for any given moment in time to be compared to an LDT, because these things are apples and oranges. The notion 'Feb 18th, 2023, 7 in the morning on the dot' isn't a singular time. After all, in New York that 'moment' occurs a full 6 hours earlier than it would in Amsterdam. You can only compare 2 LocalDateTimes.

Instead, you would have to first 'place' your LDT somewhere, by converting it to one of the other types (ZonedDateTime or even Instant) by asking the java.time API: Okay, I want this particular LDT in a certain time zone.

Hence, if you are writing your alarm app, you would have to take the stored alarm (a LocalTime object), convert it to an Instant (which is what the nature of 'what time is it now, i.e. System.currentTimeMillis()' works on), by saying: That LocalTime, on the current day in the current local timezone, as an instant, and THEN comparing those two results.

Human appointments

Imagine that, just before jetting off to New York, you made an appointment at your local (in Amsterdam) barber. Their agenda was kinda busy so the appointment was set for June 20th, 2025, at 11:00.

If you stay in New York for a few years, the correct time for your calendar to remind you that you have an appointment with your barber's in an hour is certainly not at 10:00 on june 20th 2025 in New York. You'd have missed the appointment by then. Instead, your phone should chirp at you that you have an hour left to get to your barber's (a bit tricky, from New York, sure) at 04:00 in the middle of the night.

It sure sounds like we can say that the barber's appointment is a specific instant in time. However, this is not correct. The EU has already adopted legislation, agreed upon by all member states, that all EU countries shall abolish daylight savings time. However, this law does not provide a deadline, and crucially, does not provide a time zone that each EU member state needs to pick. The Netherlands is therefore going to change time zones at some point. They will likely choose to stick either to permanent summer time (in which case they'd be at UTC+2 permanently, vs. their current situation where they are at UTC+2 in summer and UTC+1 in winter, with, notably, different dates when the switch happens vs. New York!), or stay on winter time, i.e. UTC+1 forever.

Let's say they choose to stick to winter time forever.

The day the gavel slams down in the dutch parliament building enshrining into law that the dutch will no longer advance the clocks in march is the day your appointment shifts by one hour. After all, your barber is not going to go into their appointment book and shift all appointments by an hour. No, your appointment will remain on June 20th, 2025, at 11:00. If you have a running clock ticking down the seconds until your barber appointment, when that gavel comes down it should jump by 3600 seconds.

This belies the point: That barber appointment truly is not a singular moment in time. It's a human/political agreement that your appointment is when Amsterdam universally agrees it is currently June 20th, 2025, 11:00 – and who knows when that moment will actually occur; it depends on political choices.

So, you cannot 'solve' this by storing an instant in time, and it shows how the concept 'instant in time' and 'year/month/day hour:minute:second in a certain timezone' are not quite interchangible.

The correct data type for this concept is a ZonedDateTime. This represents a date time in human terms: year/month/day hour:second:minute, and the timezone. It doesn't shortcut by storing a moment in time in epochmillis or some such. If the gavel comes down and your JDK updates its timezone definitions, asking "how many seconds until my appointment" will correctly shift by 3600 seconds, which is what you want.

Because this is for appointments and it doesn't make sense to store just the time of an appointment but not the date, there is no such thing as a ZonedDate or a ZonedTime. Unlike the first thing which comes in 3 flavours (LocalDateTime, LocalDate, and LocalTime), there's only ZonedDateTime.

The universe/log time

Imagine you are writing a computer system that logs that an event occurred.

That event, naturally, has a timestamp associated with it. Turns out that due to severe political upheaval, the laws of the land decide that retrospectively the country has been in a different timezone than what you thought when the event occurred. Applying the same logic as the barber's case (where the actual moment in time jumps by 3600 seconds when the gavel comes down) is incorrect. The timestamp represents a moment in time when a thing happened, not an appointment in a ledger. It should not jump by 3600.

Timezone really has no purpose here. The point of storing 'timestamp' for a log event is so you know when it happened, it doesn't matter where it happened (or if it does, that is fundamentally a separate notion).

The correct data type for this is java.time.Instant. An instant doesn't even know about time zones at all, and isn't a human concept. This is 'computery time' - stored as millis since an agreed upon epoch (midnight, UTC, 1970, new years), no timezone information is necessary or sane here. Naturally there is no time-only or date-only variant, this thing doesn't even really know what 'date' is - some fancypants human concept that computery time is not concerned with in the slightest.

Conversions

You can trivially go from a ZonedDateTime to an Instant. There's a no-args method that does it. But note:

  1. Create a ZonedDateTime.
  2. Store it someplace.
  3. Convert it to an Instant, store that too.
  4. Update your JDK and get new time zone info
  5. Load the ZDT.
  6. Convert it to an Instant a second time.
  7. Compare the 2 ZDTs and the 2 instants.

Results in different results: The 2 instants would not be the same, but the ZDTs are the same. The ZDT represents the appointment line in the barber's book (which never changed - 2025 june 20th, 11:00), the instant represents the moment in time that you are supposed to show up which did change.

If you store your barber's appointment as a java.time.Instant object, you will be an hour late to your barber's appointment. That's why it's important to store things as what they are. A barber's appointment is a ZonedDateTime. storing it as anything else would be wrong.

Conversions are rarely truly simple. There is no one way to convert one thing to another - you need to think of what these things represent, what the conversion implies, and then follow suit.

Example: You are writing a logging system. The backend parts store log events into a database of some sort, and the frontend parts read this database and show the log events to an admin user for review. Because the admin user is a human being, they want to see it in terms they understand, say, the time and date according to UTC (it's a programmer, they tend to like that sort of thing).

The logging system's storage should be storing the Instant concept: Epoch millis, and without timezone because that is irrelevant.

The frontend should read these as Instant (it is always a bad idea to do silent conversions!) - then consider how to render this to the user, figure out that the user wants these as local-to-UTC, and thus you would then on the fly, for each event to be printed to screen, convert the Instant to a ZonedDateTime in the zone the user wants, and from there to a LocalDateTime which you then render (because the user probably does not want to see UTC on every line, their screen estate is limited).

It would be incorrect to store the timestamps as UTC ZonedDateTimes, and even more wrong to store them as LocalDateTimes derived by asking for the current LocalDT in UTC as the event happens and then storing that. Mechanically all these things would work but the data types are all wrong. And that will complicate matters. Imagine the user actually wants to see the log event in Europe/Amsterdam time.

A note about timezones

The world is more complicated than a handful of timezones. For example, almost all of mainland europe is currently 'CET' (Central European Time), but some think that refers to european winter time (UTC+1), some thing that refers to the current state in central europe: UTC+1 in winter, UTC+2 in summer. (There's also CEST, Central European Summer Time, which means UTC+2 and isn't ambiguous). When EU countries start applying the new law to get rid of daylight savings, its likely e.g. The Netherlands on the west edge of the CET zone picks a different time than Poland on the eastern edge. Hence, 'all of central europe' is far too broad. 3-letter acronyms also are by no means unique. Various countries use 'EST' to mean 'eastern standard time', it's not just the eastern USA for example.

Hence, the only proper way to represent timezone names is using strings like Europe/Amsterdam or Asia/Singapore. If you need to render these as 09:00 PST for residents of the west coast of the USA, that's a rendering issue, so, write a rendering method that turns America/Los_Angeles into PST, which is an issue of localization, and has nothing to do with time.

huangapple
  • 本文由 发表于 2023年2月18日 17:53:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/75492508.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定