Dealing with DateTime and Timezones with Java / Po

2019-07-10 05:57发布

问题:

I am creating an application that let's a user create an event with a date. For example - Birthday, 25.09.2018. (The user is asked only for the date). I want to store these event objects in my Postgres DB.

At the end of that date (it means at 23:59:59 on 25.09.2018) I want my application to set a flag on the event expired = true.

My initial idea is to run a Scheduler every full hour, check the records for events and if the current server time is greater that the stored event time, set the flag. My first question would therefore be: Is the scheduler idea the best way to do it? Or is it maybe a bit poor with performance and there are actually better ways to do it? The reason why I want to run it every hour is because of the different timezones. There always is 23:59:59 somewhere.

The other questions are more focused on storing and checking the date information itself. How do I deal with timezones? Say a user from the USA adds a Birthday event on a given date and my server is located in Europe. How do I store it in PostgresDB and how do I perform the check in the scheduler? I need to store the timezone in the DB somehow and then in the sscheduler check if in that given timezone it is that date at 23:59:59 for the given server time.

Please help, I am really confused with this. An example would be amazing! Thanks!

回答1:

The Answer by Albe looks correct to me. Here are some further thoughts of mine.

I want my application to set a flag on the event expired = true.

As Albe mentions, there is no need to record this fact of expiration. Simply query by the date to find records before a date (expired) or after a date (not expired).

If performance is a concern, test to verify. If it is an issue, consider indexing the date column.

How do I deal with timezones?

That is the trick. I'll be brief here, as this has been handled many times already here and on the sister site, https://dba.stackexchange.com.

First, get clear if your business requirements need a granularity of date or a moment. A date is imprecise as you intuit. For any given moment, the date varies around the globe by time zone. A few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

If you care about moments, use a column of a type akin to the SQL-standard TIMESTAMP WITH TIME ZONE. Understand that the SQL standard barely touches on the subject of date-time handling. So behavior in different database systems varies widely. In Postgres, a value in that column is always in UTC. Any time zone or offset-from-UTC info provided with an input is used to adjust into UTC, then discarded. So if you care about the original zone/offset, you must manually store that in an additional column.

When retrieved, the value from that column is always in UTC. However the access tool you are using may inject its opinion about how to present that value. Some tools will dynamically apply a time zone. I find this behavior to be an unfortunate design choice. Though well-intentioned that behavior creates the illusion of that zone being part of the stored data.

In Java, use only the modern java.time classes, not the terribly troublesome and flawed old date-time classes bundled with the earliest versions of Java.

As of JDBC 4.2, you can directly exchange java.time objects with your database using PreparedStatement::setObject and ResultSet::getObject.

my server is located in Europe

The location of your server, and the current default time zone of the server’s OS and JVM, should be irrelevant to your programming and database work. Never depend on those default settings as they are out of your control and can be changed at any moment during runtime.

Instead, always specify your desired/expected time zone explicitly. Generally, most of your work should be in UTC. Adjust into a time zone only when required by business rules or for presentation to the user.

LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

Adjust back into UTC by extracting an Instant. An Instant is what you should be exchanging with your database.

Instant instant = zdt.toInstant() ;

Adjust back into a zone.

ZonedDateTime zdtAuckland = instant.atZone( ZoneId.of( "Pacific/Auckland" ) ;  // Same moment, same point on the timeline, different wall-clock time.

For querying on moments, search Stack Overflow to learn about the Half-Open approach where a span-of-time is defined where the beginning is inclusive while the ending is exclusive. This means you do not use the SQL operator BETWEEN. For example, see this.

Tip: Search on these class names and concepts using a search engine with a site:stackoverflow.com criterion. The search feature built into Stack Overflow is unfortunately skewed towards the Questions while ignoring the Answers. For example: https://duckduckgo.com/?q=site%3Astackoverflow.com+%2B%22Half-Open%22+%2Bjava&t=osx&ia=web


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

  • Java SE 8, Java SE 9, Java SE 10, and later
    • Built-in.
    • Part of the standard Java API with a bundled implementation.
    • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android bundle implementations of the java.time classes.
    • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.



回答2:

You'll have to store the user's time zone if you want to be able to expire data by time zone. My advice is not to change the row when it expires, but rather calculate at query time if it is expired or not:

SELECT e.event_date::timestamp
       AT TIME ZONE u.timezone
       <= current_timestamp - INTERVAL '1 day' AS expired
FROM event e JOIN user u ON ...