I have been fumbling with the following EclipseLink Joda-Time converter for a long time to store date-time in UTC into MySQL database with no success at all.

import java.util.Date;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.sessions.Session;
import org.joda.time.DateTime;

public final class JodaDateTimeConverter implements Converter {

    private static final long serialVersionUID = 1L;

    @Override
    public Object convertObjectValueToDataValue(Object objectValue, Session session) {
        //Code to convert org.joda.time.DateTime to java.util.Date in UTC.
        //Currently dealing with the following line
        //that always uses the system local time zone which is incorrect.
        //It should be in the UTC zone.
        return objectValue instanceof DateTime ? ((DateTime) objectValue).toDate() : null;
    }

    @Override
    public Object convertDataValueToObjectValue(Object dataValue, Session session) {
        return dataValue instanceof Date ? new DateTime((Date) dataValue) : null;
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public void initialize(DatabaseMapping databaseMapping, Session session) {
        databaseMapping.getField().setType(java.util.Date.class);
    }
}

The objectValue parameter of the convertObjectValueToDataValue() method is an instanceOf DateTime which is already according to UTC zone. Therefore, I have avoided .withZone(DateTimeZone.UTC).

There is already a separate converter on the client that converts a String representation of date-time to org.joda.time.DateTime in UTC before sending it to EJBs).

This ((DateTime) objectValue).toDate() in the return statement of the convertObjectValueToDataValue() method always takes the system local time zone that should be in the UTC zone.

Anyway date-time should be inserted into MySQL according to the UTC zone.

The best / ideal solution would be if it handles date-time of Joda similar to Hibernate


EDIT:

A property of type org.joda.time.DateTime as an example is designated in a model class as follows.

@Column(name = "discount_start_date", columnDefinition = "DATETIME")
@Converter(name = "dateTimeConverter", converterClass = JodaDateTimeConverter.class)
@Convert("dateTimeConverter")
private DateTime discountStartDate; //Getter and setter.    
有帮助吗?

解决方案 2

I don't get the question and especially the statement that conversion to java.util.Date would use the system time zone. Following test shows a different and correct behaviour:

DateTime joda = new DateTime(2014, 3, 14, 0, 0, DateTimeZone.UTC);
Date d = joda.toDate();
System.out.println(joda.getMillis()); // 1394755200000
System.out.println(d.getTime()); // 1394755200000

Of course, if you print the Date-variable d, then its toString()-method uses the system time zone, but the objects joda and d both represent the same instant as you can see in the representation of milliseconds since UNIX epoch in UTC-zone.

For example System.out.println(d); produces this string in my time zone:

Fri Mar 14 01:00:00 CET 2014

But that is not the internal state of the result and will not be stored in database, so don`t be confused or worry about. By the way, you will need to convert the result to either java.sql.Date or java.sql.Timestamp depending on the column type in your database.

EDIT:

To be sure about UTC you should change your other method convertDataValueToObjectValue() and use an explicit conversion like:

new DateTime((Date) dataValue, DateTimeZone.UTC)

Otherwise (assuming that the reverse method has always DateTime-objects in UTC as you said) you might get an asymmetry (I don't know at this moment what JodaTime does here in constructor without DateTimeZone-argument - not so well documented?).

EDIT-2:

The test code

DateTime reverse = new DateTime(d);
System.out.println(reverse); // 2014-03-14T01:00:00.000+01:00
System.out.println(reverse.getZone()); // Europe/Berlin

clearly shows that the DateTime-constructor without a second DateTimeZone-argument implicitly uses the system time zone (I don't like such implicits equal in Joda or in java.util.*). If the whole conversion forth and back from and to UTC-DateTime-objects does not work then I assume your input of DateTime-objects is maybe not really in UTC. I recommend to check this explicitly. Otherwise we have not enough informations about why your conversion code does not work.

其他提示

Date is timezone agnostic in Java. It always takes UTC (by default and always) but when Date / Timestamp is passed through a JDBC driver to a database, it interprets date / time according to the JVM time zone which defaults to the system time zone in turn (the native operating system zone).

Therefore, unless the MySQL JDBC driver was explicitly forced to use the UTC zone or JVM itself is set to use that zone, it would not store Date / Timestamp into the target database using UTC even though MySQL itself were to be configured to use UTC using default_time_zone='+00:00' in my.ini or my.cnf in the [mysqld] section. Some databases like Oracle may support time stamp with time zone and it may be an exception which I am not familiar with (untested as I do not have that environment at present).

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.

This can further be clarified by checking the invocation of setTimestampInternal() method of the MySQL JDBC driver implementation.

See the following two calls to the setTimestampInternal() method from within the two overloaded versions of the setTimestamp() method.

/**
 * Set a parameter to a java.sql.Timestamp value. The driver converts this
 * to a SQL TIMESTAMP value when it sends it to the database.
 *
 * @param parameterIndex the first parameter is 1...
 * @param x the parameter value
 *
 * @throws SQLException if a database access error occurs
 */
public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
    setTimestampInternal(parameterIndex, x, this.connection.getDefaultTimeZone());
}

/**
 * Set a parameter to a java.sql.Timestamp value. The driver converts this
 * to a SQL TIMESTAMP value when it sends it to the database.
 *
 * @param parameterIndex the first parameter is 1, the second is 2, ...
 * @param x the parameter value
 * @param cal the calendar specifying the timezone to use
 *
 * @throws SQLException if a database-access error occurs.
 */
public void setTimestamp(int parameterIndex, java.sql.Timestamp x,Calendar cal) throws SQLException {
    setTimestampInternal(parameterIndex, x, cal.getTimeZone());
}

When no Calendar instance is specified with the PreparedStatement#setTimestamp() method, the default time zone will be used (this.connection.getDefaultTimeZone()).


While using a connection pool in application servers / Servlet containers backed by a connection / JNDI accessing or operating upon datasources like,

the MySQL JDBC driver needs to be forced to use the desired time zone of our interest (UTC), the following two parameters need to be supplied through the query string of the connection URL.

I am not familiar with the history of MySQL JDBC drivers but in relatively older versions of MySQL drivers, this parameter useLegacyDatetimeCode may not be needed. Thus, one may require to adjust oneself in that case.

In case of application servers, GlassFish, for example, they can be set while creating a JDBC realm along with a JDBC connection pool inside the server itself along with other configurable properties either using the admin web GUI tool or in domain.xml directly. domain.xml looks like the following (using an XA datasource).

<jdbc-connection-pool datasource-classname="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"
                      name="jdbc_pool"
                      res-type="javax.sql.XADataSource">

  <property name="password" value="password"></property>
  <property name="databaseName" value="database_name"></property>
  <property name="serverName" value="localhost"></property>
  <property name="user" value="root"></property>
  <property name="portNumber" value="3306"></property>
  <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
  <property name="characterEncoding" value="UTF-8"></property>
  <property name="useUnicode" value="true"></property>
  <property name="characterSetResults" value="UTF-8"></property>
  <!-- The following two of our interest -->
  <property name="serverTimezone" value="UTC"></property>
  <property name="useLegacyDatetimeCode" value="false"></property>
</jdbc-connection-pool>

<jdbc-resource pool-name="jdbc_pool" 
               description="description"
               jndi-name="jdbc/pool">
</jdbc-resource>

In case of WildFly, they can be configured in standalone-xx.yy.xml using CLI commands or using the admin web GUI tool (using an XA datasource).

<xa-datasource jndi-name="java:jboss/datasources/datasource_name"
               pool-name="pool_name"
               enabled="true"
               use-ccm="true">

    <xa-datasource-property name="DatabaseName">database_name</xa-datasource-property>
    <xa-datasource-property name="ServerName">localhost</xa-datasource-property>
    <xa-datasource-property name="PortNumber">3306</xa-datasource-property>
    <xa-datasource-property name="UseUnicode">true</xa-datasource-property>
    <xa-datasource-property name="CharacterEncoding">UTF-8</xa-datasource-property>
    <!-- The following two of our interest -->
    <xa-datasource-property name="UseLegacyDatetimeCode">false</xa-datasource-property>
    <xa-datasource-property name="ServerTimezone">UTC</xa-datasource-property>

    <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
    <driver>mysql</driver>
    <transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>

    <xa-pool>
        <min-pool-size>5</min-pool-size>
        <max-pool-size>15</max-pool-size>
    </xa-pool>

    <security>
        <user-name>root</user-name>
        <password>password</password>
    </security>

    <validation>
        <valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"/>
        <background-validation>true</background-validation>
        <exception-sorter class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"/>
    </validation>

    <statement>
        <share-prepared-statements>true</share-prepared-statements>
    </statement>
</xa-datasource>

<drivers>
    <driver name="mysql" module="com.mysql">
        <driver-class>com.mysql.jdbc.Driver</driver-class>
    </driver>
</drivers>

The same thing is applicable to non-XA datasources. They can directly be appended to the connection URL itself in that case.

These all mentioned properties will be set to the mentioned class available in the JDBC driver namely com.mysql.jdbc.jdbc2.optional.MysqlXADataSource using their respective setter methods in this class in both the cases.

In case of using the core JDBC API directly, or connection pooling in Tomcat, for example, they can be set directly to the connection URL (in context.xml)

<Context antiJARLocking="true" path="/path">
    <Resource name="jdbc/pool" 
              auth="Container"
              type="javax.sql.DataSource"
              maxActive="100"
              maxIdle="30"
              maxWait="10000"
              username="root"
              password="password"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://localhost:3306/database_name?useEncoding=true&amp;characterEncoding=UTF-8&amp;useLegacyDatetimeCode=false&amp;serverTimezone=UTC"/>
</Context>

Additional :

If the target database server is running on a DST-sensible zone and Daylight Saving time (DST) is not turned off, it will cause problems. Better configure the database server also to use a standard time zone which is not affected by DST like UTC or GMT. UTC is usually preferred over GMT but both are similar in this regard. Quoting directly from this link.

If you really prefer to use a local timezone, I recommend at least turning off Daylight Saving Time, because having ambiguous dates in your database can be a real nightmare.

For example, if you are building a telephony service and you are using Daylight Saving Time on your database server then you are asking for trouble: there will be no way to tell whether a customer who called from "2008-10-26 02:30:00" to "2008-10-26 02:35:00" actually called for 5 minutes or for 1 hour and 5 minutes (supposing Daylight Saving occurred on Oct. 26th at 3am)!

By the way, I dropped the proprietary converter of EclipseLink, since JPA 2.1 provides its own standard converter which can be ported to a different JPA provider as and when required without a little or no modifications at all. It now looks like the following in which java.util.Date was also replaced by java.sql.Timestamp.

import java.sql.Timestamp;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

@Converter(autoApply = true)
public final class JodaDateTimeConverter implements AttributeConverter<DateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(DateTime dateTime) {
        return dateTime == null ? null : new Timestamp(dateTime.withZone(DateTimeZone.UTC).getMillis());
    }

    @Override
    public DateTime convertToEntityAttribute(Timestamp timestamp) {
        return timestamp == null ? null : new DateTime(timestamp, DateTimeZone.UTC);
    }
}

It is then solely the responsibility of the associated application client(s) (Servlets / JSP / JSF / remote desktop clients etc) to convert date / time according to an appropriate user's time zone while displaying or presenting date / time to end-users which is not covered in this answer for brevity and is off-topic based on the nature of the current question.

Those null checks in the converter are also not needed as it is also solely the responsibility of the associated application client(s) unless some fields are optional.

Everything goes fine now. Any other suggestions / recommendations are welcome. Criticism to any of ignorant of mine is most welcome.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top