Standardise Oracle date across clients in Java Resultset - java

In my Java code, I'm selecting/fetching an Oracle DATE column using ResultSet and getTimestamp() method, and converting this to time-in-milli-seconds. The problem is the time-in-milli-seconds varies across the machines I'm running it on. For example, if the actual timestamp is 1511213580 ms, on machine1 this is 1511262180 ms and on machine2 it is 1511233380 ms.
I've read this post Is java.sql.Timestamp timezone specific? and understand that Timestamp uses the machine's equivalent timezone to store this data.
My question is, how do I standardise the timestamps to display/read the same across clients? I do not have access to the code that persists the timestamps. On the machines that run this fetch-from-db program, I'm using additional shell scripts to compare the time-in-milli-seconds with the current time from the machine.
Here are my code snippets:
Timestamp timestamp = rset.getTimestamp(i);
if (timestamp != null)
timeInSeconds = timestamp.getTime()/1000;

tl;dr
myResultSet.getObject( … , Instant.class )
.isBefore( Instant.now() )
Details
As has been discussed many times on Stack Overflow, you should:
Store your date-time values in UTC.
Never depend on the JVM’s current default time zone.
Avoid the terrible legacy date-time classes such as Date, Calendar, and the java.sql.* classes.
Use only the java.time rather than the legacy date-time classes they supplant.
Use a driver compliant with JDBC 4.2 or later to retrieve java.time objects.
Avoid tracking time as a count-from-epoch, using objects instead.
I’m not an Oracle user, but it appears that DATE is a legacy type, lacks any concept of time zone. I suggest using the standard TIMESTAMP WITH TIME ZONE instead.
An Instant is a moment on the timeline, always in UTC, with a resolution of nanoseconds.
Instant instant = myResultSet.getObject( … , Instant.class ) ;
Boolean isPast = instant.isBefore( Instant.now() ) ;
While I advise against tracking time as a count from epoch, if you insist, you can extract a count of milliseconds since the epoch of 1970-01-01T00:00:00Z. Beware of data loss as you are truncating any microseconds or nanoseconds that may be present.
long millis = instant.toEpochMilli() ;
About java.sql.Timestamp, these objects are always in UTC. This confusing legacy class, badly designed as a hack, is now legacy and should be avoided. Replaced by java.time.Instant.
The problem is the time-in-milli-seconds varies across the machines I'm running it on. For example, if the actual timestamp is 1511213580 ms, on machine1 this is 1511262180 ms and on machine2 it is 1511233380 ms.
That makes no sense as java.sql.Timestamp will contain the same count from epoch in UTC across machines. I suspect the clocks of those alternate machines have been set to the wrong time, perhaps intentionally as a misguided attempt to handle time zone adjustment.
In that case you have a mess on your hands. The only workaround is to test each machine, calculate the delta between true UTC time and that particular machine’s incorrect clock. Then add or subtract that amount when obtaining data from that machine. Obviously risky as you never know when that confused sysadmin will attempt another adjustment hack.
The real solution is to: keep all servers in UTC and set to true accurate clock time, handle date-time values with java.time classes, and store moments in a column of type TIMESTAMP WITH TIME ZONE.

Related

How to elegantly convert from MSSQL Datetime2 to java.time.Instant

I have a simple spring boot REST API application, using plain jdbc to fetch data from a MSSQL DB. I am trying to figure out how best to retrieve a DATETIME2 column from the DB (which stores no timezone info), and serialize it as a UTC timestamp (and treat it as such in general in code).
My DB server timezone is set to UTC. I know that everything stored to this column is stored as UTC and I cannot change the column type unfortunately. It's a bit of a legacy DB, so all the dates will need to fetch will have this same problem, hence looking for a clean neat and tidy solution.
Ideally in my Java app, I would ideally like all my "date" fields to be of type java.time.Instant, since it is easy to handle and will serialize to json looking something like "someDate": "2022-05-30T15:04:06.559896Z".
The options as I see them are:
Use a custom RowMapper to do something like myModel.setDate(rs.getTimestamp("Date").toLocalDateTime().toInstant(ZoneOffset.UTC));, but this just seems verbose. I suppose I could tuck it away in some utility class static function?
Use LocalDateTime everywhere and do myModel.setDate(rs.getTimestamp("Date").toLocalDateTime()). But then Jackson will serialize it without timezone information.
Set the whole app timezone to UTC on startup. But this could be changed by other code, and from what I read is generally a bad idea.
Caveat: I am not a user of Spring.
moment versus not-a-moment
You need to get clear on one fundamental issue with date-time handling: moment versus not-a-moment.
By “moment” I mean a specific point on the timeline. Without even thinking about time zones and such, we all know that time flows forward, one moment at a time. Each moment is simultaneous for everyone around the world (sticking with Newtonian time, ignoring Einstein Relativity here 😉). To track a moment in Java, use Instant, OffsetDateTime, or ZonedDateTime. These are three different ways to represent a specific point on the timeline.
By “not-a-moment” I mean a date with a time-of-day, but lacking the context of a time zone or offset-from-UTC. If I were to say to you, “Call me at noon tomorrow” without the context of a time zone, you would have no way of knowing if you should call at noon time in Tokyo Japan, noon time in Toulouse France, or noon time in Toledo Ohio US — three very different moments, several hours apart. For not-a-moment, use LocalDateTime.
So never mix LocalDateTime with the other three classes, Instant, OffsetDateTime, or ZonedDateTime. You would be mixing your apples with your oranges.
You said:
I would ideally like all my "date" fields to be of type java.time.Instant
Yes, I would agree on generally using Instant as the member field on any Java object tracking a moment. This is generally a good idea — but only for moments. For not-a-moment, as discussed above, you should use LocalDateTime instead.
TIMESTAMP WITH TIME ZONE
Another issue, Instant was not mapped in JDBC 4.2 and later. Some JDBC drivers may optionally handle an Instant object, but doing so is not required.
So convert your Instant to a OffsetDateTime. The OffsetDateTime class is mapped in JDBC to a database column of a type akin to the SQL-standard type TIMESTAMP WITH TIME ZONE.
OffsetDateTime odt = instant.atOffset( Offset.UTC ) ;
Writing to database.
myPreparedStatement.setObject( … , odt ) ; // Pass your `OffsetDateTime` object.
Retrieval.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
TIMESTAMP WITHOUT TIME ZONE
For database columns of a type akin to the SQL-standard type TIMESTAMP WITHOUT TIME ZONE, use LocalDateTime class.
Writing to database.
myPreparedStatement.setObject( … , ldt ) ; // Pass your `LocalDateTime` object.
Retrieval.
LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;
Specify time zone
You said:
My DB server timezone is set to UTC.
That should be irrelevant. Always write your Java code in such as way as to not rely on the JVM’s current default time zone, the host OS’ current default time zone, or the database’s current default time zone. All of those lay outside your control as a programmer.
Specify your desired/expected time zone explicitly.
Retrieve a moment from the database, and adjust into a desired time zone.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = odt.atZoneSameInstant( z ) ;
Generate text localized to the user's preferred locale.
Locale locale = Locale.JAPAN ;
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG ).withLocale( locale ) ;
String output = zdt.format( f ) ;
DATETIME2 in MS SQL Server
The type DATETIME2 type in MS SQL Server stores a date with time-of-day, but lacks the context of a time zone or offset-from-UTC.
That is exactly the wrong type to use for storing a moment. As discussed above, that type is akin to the SQL standard type TIMESTAMP WITHOUT TIME ZONE, and maps to the Java class LocalDateTime.
You seem to understand that fact given your comment:
I know that everything stored to this column is stored as UTC and I cannot change the column type unfortunately. It's a bit of a legacy DB …
Let me point out that you do not know the values in that column are intended to represent a moment as seen with an offset of zero. You can expect that, and hope so. But without using the protection of the database’s type system, you cannot be certain. Every user, every DBA, and every SysAdmin must have always been aware of this unfortunate scenario, and must have always done the right thing. You’ll need lots of luck with that.
I must mention that the ideal solution is to refactor your database, to correct this wrong choice of data type for that column. But I understand this could be a burdensome and challenging fix.
So given this unfortunate scenario without a fix being feasible, what to do?
Options 1, 2, & 3 you listed
Option 1
Regarding your option # 1, yes that makes sense to me. Except two things:
I would change the name of your model method to be more precise: setInstant. Or use a descriptive business name such as setInstantWhenContractGoesIntoEffect.
Never use the awful legacy date-time classes in Java such as Timestamp. Change this:
myModel.setDate(rs.getTimestamp("Date").toLocalDateTime().toInstant(ZoneOffset.UTC));
… to:
myModel
.setInstantWhenContractGoesIntoEffect
(
resultSet
.getObject( "Date" , LocalDateTime.class ) // Returns a `LocalDateTime` object.
.toInstant( ZoneOffset.UTC ) // Returns an `Instant` object.
)
;
Option 2
As for your option # 2, I am not quite sure what you have in mind. But my impression is that would be the wrong way to go. I believe the best approach, for long-term maintenance without "technical debt", and for avoiding confusing and mishaps, is to “tell the truth”. Do not pretend to have a zebra when you actually have donkey. So:
On the database side, be clear and explicit that you have a date with time but lack the context of an offset. Add lots of documentation to explain that this is based on a faulty design, and that we are intend to store moments as seen in UTC.
On the app side, the Java side, deal only with Instant, OffsetDateTime, and ZonedDateTime objects, because within the data model we are representing moments. So use classes that represent a moment. So no use of LocalDateTime where you really mean a specific point on the timeline.
Obviously, there is some kind of a dividing line between your database side and your app side. Crossing that line is where you must convert between your Java type for a moment and your database type faking it as a moment. Where you draw that line, that transition zone, is up to you.
Option 3
As for your option # 3, yes that would be a very bad idea.
Setting such a default is not reliable. Any SysAdmin, or even an unfortunate OS update, could change the OS’s current default time zone. Like wise for the database’s current default time zone. And likewise for the JVM’s current default time zone.
So you end up three default time zones that could be changing, with each affecting various parts of your environment. And changing the current default time zone in any of those places immediately affects all other software depending on that default, not just your particular app.
As mentioned above, I recommend just the opposite: Code without any reliance on default time zones anywhere.
The one place for accessing a default time zone is maybe for presentation to the user. But even then, if the context in crucial, you must confirm the desired/expected time zone with the user. And where you do make use of a current default time zone, do so explicitly rather than implicitly. That is, make explicit calls such as ZoneId.getSystemDefault() rather than using omitted optional arguments.
I'm not sure I see a problem.
Instant values are UTC by definition, and java.sql.Timestamps have no zone other than the one implied by the database setting. You know the database is strictly UTC. This is lucky for you since it eliminates one error-prone conversion. Then, reading java.sql.Timestamps and keeping them as Instants at runtime is trivial, given java.sql.Timestamp#toInstant(). DON'T convert through LocalDateTime.
This has nothing to do with setting any "default" timezone in your application. Design and write your code so that internally (i.e. runtime memory and database) you deal ONLY with UTC (i.e. instants). The only point at which you should convert instants to anything local is at external interface points... i.e.
when outputting date/time values, either for human consumption or for other software that expects a specific timezone.
when reading date/time values from the user or another program (for which you will need to know any implied zone if it's not explicit)
Leave your "default" timezone as whatever is given to you by your environment. Then, no matter where your code is running, it will produce meaningful local dates/times.
Establish a strict rule that you deal only with UTC internally. This will make reasoning about your code MUCH simpler in the long run.
I guess the only real stumbling block is realizing that things depending on local conditions, such as day boundaries, have to be done in the local zone... but write your code to "think" UTC internally.

What Offset is used to unmarshal an OffsetDateTime from a Postgres timestampTZ?

I did read timestamps-and-time-zones-in-postgresql and understood that a timestampTZ is stored as a UTC-timestamp with any timezone/offset converted away and lost.
So, when loading a JPA/Hibernate Entity with an OffsetDateTime-field, bound to such a timestampTZ field, where does the Offset come from?
Is it always converted into the JDBC-Connection's Timezone?
This is a kind of information-truncation where we actually lose the original timezone-information, thus we are set back to whatever timezone the JDBC-connection is bound to and thus, we are required to store the timezone-information additionally if we'd needed that?
If all of the above holds, wouldn't it be much clearer/precise to use Instant instead of OffsetBigTime, which represents an UTC-point-in-time exactly like timestampTZ is doing?
Then I would have to at least apply the "proper timezone" explicitly in code and not have it applied "magically" by some DB-connection...
when loading an JPA/Hibernate Entity with a OffsetDateTime-field, bound to such an timestampTZ field, where does the Offset come from
While I do not use JPA or Hibernate (I use straight JDBC), I would expect that you receive an OffsetDateTime where the offset is zero hours-minutes-seconds ahead/behind UTC. We might refer to this as “at UTC” for short.
You can see for yourself. Retrieve an OffsetDateTime object, and call toString. If the resulting text shows +00:00 or Z at the end, hat means an offset of zero.
we are required to store the timezone-information additionally if we'd needed that?
Yes, if you care about the time zone or offset submitted to the database, you must save that information in a second column yourself with your own extra programming. By default, Postgres uses the submitted zone or offset info to adjust into UTC, then discards that zone or offset info.
I expect most business apps do not care what the original zone or offset was. Keep in mind that the moment, the point on the timeline, is not changed. Only the wall-clock time appears different. Postgres is like a person in Iceland (where UTC is their year-round permanent time zone) receiving a call from someone in Tokyo or Montréal. If both persons look up at the clock on their walls, the person in Tokyo sees a time of day several hours ahead of the Postgres person in Iceland. The Montréal person sees a time of day on the clock hanging on heir own wall to be hours behind that of the Postgres person in Iceland.
When you retrieve that OffsetDateTime object with an offset of zero, you can easily adjust into any time zone you desire.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
ZoneId z = ZoneId.of( "Asia/Tokyo" ) ;
ZonedDateTime zdt = odt.atZoneSameInstant( z ) ;
wouldn't it be much more clearer/precise to use Instant
Yes!!
For whatever reasons, the folks controlling the JDBC API made the odd choice in JDBC 4.2 to require support for OffsetDateTime but not require support for Instant or ZonedDateTime. These other two classes are used much more often in most apps I imagine. So the logic of their choice escapes me.
Your JDBC driver may support Instant or ZonedDateTime. Just try it and see. The JDBC 4.2 API does not forbid such support; the API makes no mention of those types. Therefore support of Instant or ZonedDateTime is optional.
Then I would have to at least apply the "proper timezone" explicitly in code and not have it applied "magically" by some db-connection...
If you are retrieving java.time objects through JDBC 4.2 compliant drivers, I would be very surprised to see them applying a zone or offset to retrieved values. I expect you will only receive OffsetDateTime objects with an offset of zero. But I do not recall his behavior being mandated in the specification one way or the other. So always test the behavior of your particular JDBC driver.
Beware that retrieving values as text, or using other middleware tools such as PgAdmin, may well inject some default zone or offset. While well-intentioned, I consider this an anti-feature, creating the illusion of a particular zone having been saved in the database when it was not in fact.
What Timezone is used to unmarshal an OffsetDateTime from a Postgres timestampTZ?
Firstly, know that an offset is merely a number of hours, minutes, and seconds ahead or behind the prime meridian. A time zone is much more. A time zone is a history of the past, present, and future changes to the offset used by the people of a particular region.
So your title’s wording is contradictory. There is no time zone involved with an OffsetDateTime, thus the name. For a time zone, use the ZonedDateTime class.

how to convert a java timestamp to a string respecting the timezone with nanosecond resolution

I want to convert a timestamp to a string given a timezone argument (in Java). From the code I'm looking at internally the timestamp has nanosecond precision (or at least one can convert it to nanoseconds and I don't care since the output format I want to generate isn't that precise).
The DateFormat allows an S specifier and will apply a TimeZone, but it seems to do only 3 digits (milliseconds?) precision. Using toString gives 6 digits of precision within the seconds (microseconds?) but doesn't seem to take a TimeZone argument.
I need something that does both, allows a TimeZone specifier and gives 6 digits of precision within a second.
What to use?
I am assuming you mean a java.sql.Timestamp that you are getting from an SQL database where it was a timestamp either with or without time zone.
Use java.time
First, you don’t need that. The Timestamp class is poorly designed and long outdated. Instead prefer to get from your database:
If your database column is a timestamp with time zone, which it should be, then get an OffsetDateTime (with some JDBC drivers an Instant works too).
If your database column is timestamp without time zone (not recommended), then get a LocalDateTime. The problem with this is that a LocalDateTime is a date and time without time zone, so not a unique point in time, so unsuited for a timestamp.
The mentioned types are all from java.time, the modern Java date and time API, and they all have nanosecond precision.
Example:
OffsetDateTime odt = yourResultSet.getObject("my_timestamp_col", OffsetDateTime.class);
System.out.println(odt);
Example output:
2018-11-29T22:34:56.123456789Z
The trailing Z in the output means Zulu time zone, UTC or offset zero.
You wanted to apply a given time zone. So for example:
ZonedDateTime zdt = odt.atZoneSameInstant(ZoneId.of("America/Guyana"));
System.out.println(zdt);
2018-11-29T18:34:56.123456789-04:00[America/Guyana]
Getting a type from java.time from the result set using ResultSet.getObject requires JDBC 4.2 (or a modern JPA implementation or what you use for data access). Most of us have that.
Handling a Timestamp from a legacy API
In case you either haven’t got JDBC 4.2 yet or you are getting the Timestamp from a legacy API that you can’t afford to change just now:
ZonedDateTime zdt = yourTimestamp.toInstant()
.atZone(ZoneId.of("America/Guyana"));
If you have specific requirements for your string output, use a DateTimeFormatter to format your ZonedDateTime. This is described in many places, just search.
Link
Oracle tutorial: Date Time explaining how to use java.time.

Converting Date from ZoneDateTime gives local times instead of ZonedTime

I am trying to convert the ZonedDateTime to a Date. Looks like in the conversion, it looses the time zone and gets my local time.
System.out.println("zoneDate1::::::::::"+ZonedDateTime.now(ZoneId.of("America/Chicago")));
System.out.println("zoneDate1:Date::::::::::"+Date.from(ZonedDateTime.now(ZoneId.of("America/Chicago")).toInstant()));
The above outputs as below:
zoneDate1::::::::::2016-04-15T17:35:06.357-05:00[America/Chicago]
zoneDate1:Date::::::::::Fri Apr 15 18:35:06 EDT 2016
Is this because this is a Date type? How would i go about doing this kind of conversion and conserve the zoned time?
What is the problem? What did you expect? I see no misbehavior.
Your java.time type (ZonedDateTime) is assigned a time zone of America/Chicago.
Your JVM apparently has an assigned time zone related to east coast of North America, the clue being the EDT value seen in string output. The toString method on java.util.Date applies your JVM’s current default time zone when generating its textual representation of the date-time value. Poorly designed, this behavior is trying to be helpful but is actually confusing because you cannot actually get or set this time zone on the java.util.Date object.
At any rate, the east coast of North America (such as America/New_York time zone) is an hour ahead of America/Chicago. So you are seeing 17:xx:xx time for Chicago and 18:xx:xx for Eastern Daylight Saving Time. These values are correct.
You should call java.util.TimeZone.getDefault() when investigating the behavior of the old date-time classes.
java.time
The bigger problem is that you are even using these old date-time classes such as java.util.Date/.Calendar. They are poorly designed, confusing, and troublesome. Avoid these old classes altogether. They have been supplanted in Java 8 and later by the java.time framework.
Also, avoid using 3-4 letter zone abbreviations like EDT. These are neither standardized nor unique. Use proper time zone names in continent/region format.
Instant
To capture the current date-time in java.time, just use Instant. This class captures a moment on the timeline in UTC. Do most of your work in UTC. No need for time zones unless expected by your user when displayed in the user interface.
Instant now = Instant.now();
Database
To send to your database, first make sure you have defined the column in the table as something along the line of the SQL standard TIMESTAMP WITH TIME ZONE. By the way, support for date-time types various among databases with some doing a much better job than others.
Hopefully JDBC drivers will be updated someday to directly handle the java.time types. Until then, we must convert into a java.sql type when transferring data to/from a database. The old java.sql classes have new methods to facilitate these conversions.
java.sql.Timestamp
For a date-time value like Instant, we need the java.sql.Timestamp class and its from( Instant ) method.
java.sql.Timestamp ts = java.sql.Timestamp.from( now );
Avoid working in java.sql.Timestamp as it is part of the old poorly-designed mess that is the early Java date-time classes. Use them only for database transfer, then shift into java.time immediately.
Instant instant = ts.toInstant();
So simple, no time zones or offset-from-UTC involved. The Instant, java.sql.Timestamp, and database storage are all in UTC.
ZonedDateTime
When you do need to shift into some locality’s wall-clock time, apply a time zone.
ZoneId zoneId = ZoneId.of( "America/Chicago" ); // Or "America/New_York" and so on.
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );
Huh? Date doesn't have time zones so, this is probably why it's failing. Maybe this is what you're looking for:
Date.from(java.time.ZonedDateTime.now().toInstant());
If your database allows you to store the timestamp along with the timezone, you should go ahead and save it as a timestamp.
If not, I would recommend that you store the date-time as per your timezone (or GMT). Add an additional column in the table to hold the value of the user's timezone.
When you fetch the value from the database, you can convert it to the user's timezone. Avoid storing just the date.

Timestamps and time zone conversions in Java and MySQL

I'm developing a Java application with a MySQL database on a server located in a different time zone from mine, and I am trying to decide between using DATETIME or TIMESTAMP on my database.
After reading questions like Should I use field 'datetime' or 'timestamp'?, and the MySQL documentation, I decided TIMESTAMP was better for me as it converts values to UTC for storage, and back to the current time zone for retrieval.
Also, as user Jesper explains in this thread, java.util.Date objects are internally only a UTC timestamp (i.e. number of milliseconds since the Epoch), and when you do a toString() it is displayed according to your current time zone.
For me, that looks like a good practice: storing datetimes as UTC timestamps, and then displaying them according to the current time zone.
I was about to do it like that, but then I found this from the Java documentation for Prepared Statements and got very confused:
void setTimestamp(int parameterIndex,
Timestamp x,
Calendar cal)
throws SQLException
Sets the designated parameter to the given java.sql.Timestamp value,
using the given Calendar object. The driver uses the Calendar object
to construct an SQL TIMESTAMP value, which the driver then sends to
the database. With a Calendar object, the driver can calculate the
timestamp taking into account a custom timezone. If no Calendar object
is specified, the driver uses the default timezone, which is that of
the virtual machine running the application.
Before this, I thought timestamps were by convention always in UTC. Why on earth would anyone want a localized timestamp instead of a localized representation of it? Wouldn't that be very confusing for everyone?
How do these conversions work? If Java takes an UTC timestamp and converts it to an arbitrary time zone, how can it tell MySQL in which timezone it is?
Won't MySQL assume that this timestamp is in UTC and then retrieve an incorrect localized value?
Date-Time Handling Is A Mess
The first paragraph in the answer by Teo is quite insightful and correct: Date-time handling in Java is a mess. Ditto for all other languages & development environments that I know of. Date-time work is difficult and tricky, especially error-prone and frustrating because we think it of date-time intuitively. But "intuitively" does not cut it when it comes to data types, databases, serialization, localization, adjusting across time zones, and all the other formalities that come with computer programming.
Unfortunately, the computer industry basically chose to ignore this problem of date-time work. Just as Unicode took too long to be invented given the obvious need, so too has the industry kicked the can down the road on solving the problem of date-time handling.
Do Not Rely On Count-Since-Epoch
But I must disagree with its conclusion. Working with a count-since-epoch is not the best solution. Using count-since-epoch is inherently confusing and error-prone and incompatible.
Humans cannot read a long number and decipher that as a date-time. So verifying data and debugging becomes complicated, to say the least.
What "count" would you use? The milliseconds used by java.util.Date and by Joda-Time? The microseconds used by Postgres, MySQL, and other databases? The nanoseconds used by the new java.time package in Java 8?
Which epoch would you use? The Unix epoch of the beginning of 1970 in UTC is common, but far from singular. Almost two dozen epochs have been used by various computer systems.
We create numeric data types for doing math rather than using bits. We create string classes to handle the nitty-gritty details of handling text rather than bare octets. So too we should create data-types and classes to handle date-time values.
The early Java teams (and IBM & Taligent before them) made an attempt with the java.util.Date and java.util.Calendar and related classes. Unfortunately, the attempt was inadequate. While date-time is inherently confusing, these classes have added even more confusion.
Joda-Time
As far as I know, the Joda-Time project was the first project to take on date-time in a thorough, competent, and successful manner. Even so, the creators of Joda-Time were not entirely satisfied. They went on to create the java.time package in Java 8, and extend that work with the threeten-extra project. Joda-Time and java.time share similar concepts but are distinct, each having some advantages.
Database Problems
Specifically, the java.util.Date & .Calendar classes lack date-only values without time-of-day and time zone. And they lack time-only values without date and time zone. Before Java 8, the Java team added the hacks known as the java.sql.Date and java.sql.Time classes which is a date-time value masquerading as a date-only. Both Joda-Time and java.time rectify that by offering LocalDate and LocalTime classes.
Another specific problem is that java.util.Date has a resolution of milliseconds, but databases frequently use microseconds or nanoseconds. In an ill-advised attempt to bridge this disparity, the early Java team created another hack, the java.sql.Timestamp class. While technically a java.util.Date subclass, it also tracks the fractional seconds to nanosecond resolution. So when converting in and out of this type you may losing or gaining the finer fractional seconds granularity without being conscious of that fact. So that might mean that values you expect to be equal are not.
Another source of confusion is the SQL data type, TIMESTAMP WITH TIME ZONE. That name is a misnomer as the time zone info is not stored. Think of the name as TIMESTAMP WITH RESPECT FOR TIME ZONE as any passed time zone offset info is used in converting the date-time value to UTC.
The java.time package with its nanosecond resolution has some specific features to better communicate date-time data with a database.
I could write much more, but such information can be gleaned from searching StackOverflow for words such as joda, java.time, sql timestamp, and JDBC.
Example using Joda-Time with JDBC with Postgres. Joda-Time uses immutable objects for thread-safety. So rather than alter an instance ("mutate"), we create a fresh instance based on the values of the original.
String sql = "SELECT now();";
…
java.sql.Timestamp now = myResultSet.getTimestamp( 1 );
DateTime dateTimeUtc = new DateTime( now , DateTimeZone.UTC );
DateTime dateTimeMontréal = dateTimeUtc.withZone( DateTimeZone.forID( "America/Montreal" ) );
Focus On UTC
Before this, I thought timestamps were by convention always in UTC. Why on earth would anyone want a localized timestamp instead of a localized representation of it? Wouldn't that be very confusing for everyone?
Indeed. The SQL standard defines a TIMESTAMP WITHOUT TIME ZONE which ignores and strips away any included time zone data. I cannot imagine the usefulness of that. This Postgres expert, David E. Wheeler, says as much in recommending always using TIMESTAMP WITH TIME ZONE. Wheeler cites one narrow technical exception (partitioning) and even then says to convert all the values to UTC yourself before saving to the database.
The best practice is to work and store data in UTC while adjusting to localized time zones for presentation to the user. There may be times when you want to remember the original date-time data in its localized time zone; if so, save that value in addition to converting to UTC.
Guidelines
The first steps to better date-time handling are avoiding java.util.Date & .Calendar, using Joda-Time and/or java.time, focusing on UTC, and learning the behavior of your specific JDBC driver and your specific database (databases vary widely in their date-time handling despite the SQL standard).
MySQL
Caveat: I don’t use MySQL (I'm a Postgres kind of guy).
According to the version 8 documentation, the two types DATETIME and TIMESTAMP differ in that the first one lacks any concept of time zone or offset-from-UTC. The second one uses any indication of time zone or offset-from-UTC accompanying an input to adjust that value to UTC, then stores it, and discards the zone/offset info.
So these two types seem to be akin to the standard SQL types:
MySQL DATETIME ≈ SQL-standard TIMESTAMP WITHOUT TIME ZONE
MySQL TIMESTAMP ≈ SQL-standard TIMESTAMP WITH TIME ZONE
For MySQL DATETIME, use the Java class LocalDateTime. That class, like that data type, purposely lacks any concept of time zone or offset-from-UTC. Use this type and class for either:
When you mean any zone or all zones, such as “Christmas starts on first moment of December 25, 2018”. That translates to different moments in different places as a new day dawns earlier in the east than in the west.
When scheduling appointments or events far enough out in the future that politicians may change the offset of the time zone, for which politicians around the world have shown a proclivity. In this usage, you must at runtime apply a time zone to dynamically calculate, but not store, a moment for display on a calendar. That way, a 15:00 dental appointment in 8 months remains at 15:00 even if politicians redefine the clock to be minutes/hours ahead or behind.
For MySQL TIMESTAMP, use the Java class Instant, as shown above. Use this type and class for moments, specific point on the timeline.
JDBC 4.2
As of JDBC 4.2 and later, we can directly exchange java.time objects with the database. Use getObject & setObject methods.
myPreparedStatement.setObject( … , Instant.now() ) ;
Retrieval.
Instant instant = myResultSet.getObject( … , Instant.class ) ;
The JDBC 4.2 specification requires a driver to support OffsetDateTime but strangely does not require support for the more common types Instant and ZonedDateTime. But converting between types is quite easy.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Instant instant = odt.toInstant() ;
You can then adjust that UTC value in Instant to a specific time zone for presentation to a user.
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;
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.
Your question is spot on a problem which i think is huge these days. Both DB (via SQL) and server side itself (via programming languages such as Java) offer a compendium of ways of dealing with date and time. I would qualify the status-quo as highly non-standardized and a bit chaotic (personal opinion :)
My answer is partial but i'll explain why.
You're correct, Java's Date (and Calendar) store time as milliseconds since the Unix Epoch (which is great). It happens not only in Java but in other programming languages as well. In my opinion the perfect time-keeping architecture emerges naturally from this: the Unix Epoch is January 1st, 1970, midnight, UTC. Therefore if you choose to store time as milliseconds since the Unix Epoch you have a lot of benefits:
architecture clarity: server side works with UTC, client side shows the time through its local timezone
database simplicity: you store a number (milliseconds) rather than complex data structures like DateTimes
programming efficiency: in most programming languages you have date/time objects capable of taking milliseconds since Epoch when constructed (which as you said, allows for automatic conversion to client-side timezone)
I find code and architecture is much simpler and more flexible when using this approach. I stopped trying to understand things like DateTime (or Timestamp) and only deal with them when i have to fix legacy code.

Categories

Resources