3

This is my first question on this forum, please be patient with me.

Oracle says "ZonedDateTime is a date and time with a fully qualified time zone. This can resolve an offset at any point in time. The rule of thumb is that if you want to represent a date and time without relying on the context of a specific server, you should use ZonedDateTime." which is exactly what I want to do because the application is dealing with interactions globally, but MySQL only seems to save DATETIME as TIMESTAMP, but it evidently saves this as UTC so that it can be converted to any time zone. The servers that we will be running on are going to be running in several time zones, and we will not know which one will be running where as the cloud provider will be dynamically moving them based on demand and maintenance.

So, maintaining the date/time/zone in this application seems to be perfectly fitted to the new ZonedDateTime construct, but I have gotten myself confused repeatedly in trying to keep everything straight between legacy Date which PrimeFaces and other component code still delivers and MySQL which wants to deal in Timestamps which will eventually age out in 2038 anyway.

We do not want to use any external date libraries like Joda or Apache.

My question is fairly straight-forward, but the answer appears to be elusive for me and the nuances seem to be many: What are the best practices for saving the java ZonedDateTime to the MySQL database, reading it back in so that work can be performed globally by users via java Instant calculations which look right to the local user, and will be correct no matter the location of either the Glassfish server or the MySQL server which might each be in different time zones from one another and from day to day?

FreedomRings
  • 181
  • 1
  • 9

1 Answers1

0

I think the way this has shaken out us this: MySQL saves the date as UTC when it saves as a Timestamp so as long as I do that, it does not matter where MySQL lives.

Glassfish can tell you where it lives by inquiring the server, but it can also set a property for the home office which gives you a base of operations that is consistent wherever the server lives. You can do that in the web.xml

<context-param>
    <param-name>GLASSFISH_HOME_TIME_ZONE</param-name>
    <param-value>America/New_York</param-value>
</context-param>

The data bean needs to do most of the work so that it is consistent with the data across all of the data uses. Problems with the component libraries that are either not updated to the ZonedDateTime or only partially updated will generally call the data with a getter, so using overloading should allow the component libraries to find the specific method it prefers. I created a data bean that looks something like this:

public class DataBean {

private final ZoneId GLASSFISH_HOME_TIME_ZONE = ZoneId.of(FacesContext.getCurrentInstance().getExternalContext().getInitParameter( "GLASSFISH_HOME_TIME_ZONE"));
private ZonedDateTime dateToUseInGlassfish = null;

public DataBean (
    Timestamp dateFromMySQL) 
{
    if ( dateFromMySQL == null ) {
        this.dateToUseInGlassfish = null;
    } else {
        this.dateToUseInGlassfish = LocalDateTime.ofInstant(dateFromMySQL.toInstant(), GLASSFISH_HOME_TIME_ZONE ).atZone( GLASSFISH_HOME_TIME_ZONE );
    }
}

/** Formatter for Date/Time */
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy ' at ' h:mm a z");

/** Formatter for Date only */
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");

/** Get the date string formatted with date and time */
public String getDateToUseInGlassfishDateTimeFormatted() {
    if ( dateToUseInGlassfish == null ) { return null; }
    String formattedDate = dateTimeFormatter.format( dateToUseInGlassfish );
    return formattedDate;
}

/** Get the date string formatted with date only */
public String getgetDateToUseInGlassfishDateFormatted() {
    if ( dateToUseInGlassfish == null) { return null; }
    String formattedDate = dateFormatter.format( dateToUseInGlassfish );
    return formattedDate;
}

/** Get the date ZDT formatted (for calculations) */
public ZonedDateTime getgetDateToUseInGlassfish() {
    return dateToUseInGlassfish;
}

/** Get the date as Date (for component libraries that automatically fetch then throw up with ZDT) */
public Date getDateToUseInGlassfishDate() {
    if ( dateToUseInGlassfish == null) { return null; }
    return Date.from( dateToUseInGlassfish.toInstant());
}

/** Set the date from ZDT (results from calculations stored in bean) */
public void setDateToUseInGlassfish( ZonedDateTime dateToUseInGlassfish ) {
    this.dateToUseInGlassfish = dateToUseInGlassfish;
}

/** Set the date from Date with an automatic convert to ZDT */
public void setDateToUseInGlassfish( Date dateToUseInGlassfish ) {
    if (dateToUseInGlassfish == null) {
        this.dateToUseInGlassfish = null;
    } else {
        this.dateToUseInGlassfish = LocalDateTime.ofInstant( Instant.ofEpochMilli( dateToUseInGlassfish.getTime()), GLASSFISH_HOME_TIME_ZONE ).atZone( GLASSFISH_HOME_TIME_ZONE );
    }
}

Getting the date as a Timestamp from MySQL is getting it as a UTC point in time and it looks something like this :

ResultSet resultSet = preparedSelectQuoteSql.executeQuery()) {
    while (resultSet.next()) {
        quoteBean = new QuoteBean(
            resultSet.getTimestamp("MySQLDateColumn")
        );
    }
}

Getting it inserted/updated into MySQL from the ZonedDateTime into a timestamp that MySQL will automatically convert to UTC so that we can let MySQL live anywhere we want it to live and read back the same Instant in time:

if ( insertValue instanceof ZonedDateTime ) {
    if ( insertValue != null ) {
        Timestamp convertedDate = Timestamp.from( ((ZonedDateTime) insertValue).toInstant() );
        preparedStatement.setTimestamp( paramNumber, convertedDate );
    } else {
        preparedStatement.setNull ( paramNumber, Types.TIMESTAMP );
    }
}

I think that this works, but I WELCOME criticism.

FreedomRings
  • 181
  • 1
  • 9