如何使用EclipseLink和Joda-Time将UTC日期时间存储到数据库中?

时间:2014-03-14 20:30:50

标签: mysql jpa eclipselink jodatime utc

我一直在摸索以下EclipseLink Joda-Time转换器很长一段时间,以便将UTC中的日期时间存储到MySQL数据库中,但根本没有成功。

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);
    }
}

objectValue方法的convertObjectValueToDataValue()参数是instanceOf DateTime,已根据UTC区域。因此,我避免使用.withZone(DateTimeZone.UTC)

客户端上已经有一个单独的转换器,它将日期时间的字符串表示形式转换为UTC中的org.joda.time.DateTime,然后再发送给EJB。)

((DateTime) objectValue).toDate()方法的return语句中的convertObjectValueToDataValue()始终采用应位于UTC区域的系统本地时区。

无论如何,应根据UTC区域将日期时间插入MySQL。

最佳/理想的解决方案是,如果它处理Joda的日期时间,类似于Hibernate


修改

作为示例的org.joda.time.DateTime类型的属性在模型类中指定如下。

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

2 个答案:

答案 0 :(得分:2)

Date在Java中与时区无关。它始终需要UTC(默认情况下始终为),但当Date / Timestamp通过JDBC驱动程序传递到数据库时,它会根据JVM时区解释日期/时间,默认为系统时间反过来区域(本机操作系统区域)。

因此,除非明确强制MySQL JDBC驱动程序使用UTC区域或JVM本身设置为使用该区域,否则它不会使用UTC将Date / Timestamp存储到目标数据库中虽然MySQL本身被配置为使用default_time_zone='+00:00'中的my.inimy.cnf部分中的[mysqld]来使用UTC。像Oracle这样的某些数据库可能会支持带时区的时间戳,这可能是我不熟悉的例外情况(未经测试,因为我目前没有这种环境)。

void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException

  

将指定参数设置为给定的java.sql.Timestamp值,   使用给定的Calendar对象。驱动程序使用Calendar对象   构造一个SQL TIMESTAMP值,然后驱动程序发送给它   数据库。使用Calendar对象,驱动程序可以计算   时间戳考虑到自定义时区。 如果没有Calendar个对象   如果指定,则驱动程序使用默认时区,即时区   运行应用程序的虚拟机

通过检查MySQL JDBC驱动程序实现的setTimestampInternal()方法的调用,可以进一步澄清这一点。

setTimestampInternal()方法的两个重载版本中查看对setTimestamp()方法的以下two次调用。

/**
 * 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());
}

如果使用Calendar方法未指定PreparedStatement#setTimestamp()实例,则将使用默认时区(this.connection.getDefaultTimeZone())。

在由连接/ JNDI支持的应用程序服务器/ Servlet容器中使用连接池时,访问或操作数据源,如

需要强制MySQL JDBC驱动程序使用我们感兴趣的所需时区(UTC),需要通过连接URL的查询字符串提供以下两个参数。

我不熟悉MySQL JDBC驱动程序的历史,但在相对较旧版本的MySQL驱动程序中,可能不需要此参数useLegacyDatetimeCode。因此,在这种情况下,可能需要调整自己。

例如,对于应用程序服务器GlassFish,可以在创建JDBC领域时设置它们,同时使用管理Web GUI工具或{{1直接。 domain.xml如下所示(使用XA数据源)。

domain.xml

对于WildFly,可以使用CLI命令或使用管理Web GUI工具(使用XA数据源)在<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> 中配置它们。

standalone-xx.yy.xml

同样的事情适用于非XA数据源。在这种情况下,它们可以直接附加到连接URL本身。

在这两种情况下,这些所有提到的属性都将被设置为JDBC驱动程序中提到的类<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> ,在这个类中使用它们各自的setter方法。

例如,如果直接使用核心JDBC API,或者在Tomcat中使用连接池,则可以直接将它们设置为连接URL(com.mysql.jdbc.jdbc2.optional.MysqlXADataSource

context.xml

其他:

如果目标数据库服务器在DST敏感区域上运行且夏令时(DST)未关闭,则会导致问题。更好地配置数据库服务器也使用不受DST(如UTC或GMT)影响的标准时区。 UTC通常优于GMT,但在这方面两者都相似。直接从this link引用。

  

如果你真的更喜欢使用当地时区,我建议至少   关闭夏令时,因为日期不明确   你的数据库可能是一场真正的噩梦。

     

例如,如果您正在构建电话服务并且正在使用   您要求的数据库服务器上的夏令时   麻烦:没有办法告诉客户是否打电话   来自&#34; 2008-10-26 02:30:00&#34;到&#34; 2008-10-26 02:35:00&#34;实际上叫   5分钟或1小时5分钟(假设夏令时)   发生在10月26日凌晨3点)!

顺便说一句,我删除了EclipseLink的专有转换器since JPA 2.1 provides its own standard converter,可以根据需要将其移植到不同的JPA提供程序,而不需要进行任何修改。现在看起来如下<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> 也被java.util.Date替换。

java.sql.Timestamp

完全由相关应用程序客户端(Servlet / JSP / JSF /远程桌面客户端等)负责在显示或显示日期时根据适当的用户时区转换日期/时间/最终用户的时间,为简洁起见未在本答复中涵盖,并且根据当前问题的性质偏离主题。

转换器中的那些空检查也是不需要的,因为它也只是相关应用程序客户端的责任,除非某些字段是可选的。

现在一切都很顺利。欢迎任何其他建议/建议。任何对我的无知的批评都是最受欢迎的。

答案 1 :(得分:1)

我没有得到问题,尤其是转换为java.util.Date会使用系统时区的声明。以下测试显示了不同且正确的行为:

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

当然,如果你打印日期变量d,那么它的toString() - 方法使用系统时区,但对象jodad都代表同一时刻你可以自UTC区域中的UNIX纪元以来的毫秒表示。

例如System.out.println(d);在我的时区生成此字符串:

  

Fri Mar 14 01:00:00 CET 2014

但这不是结果的内部状态,也不会存储在数据库中,所以不要混淆或担心。顺便说一句,您需要将结果转换为java.sql.Date或java.sql.Timestamp,具体取决于数据库中的列类型。

修改

要确定UTC,您应该更改其他方法convertDataValueToObjectValue()并使用如下的显式转换:

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

否则(假设反向方法总是按照你所说的UTC中的DateTime-对象)你可能会得到不对称(我现在不知道JodaTime在没有DateTimeZone参数的构造函数中做了什么 - 不是这样的记录完好?)。

修改-2:

测试代码

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

清楚地表明没有第二个DateTimeZone参数的DateTime构造函数隐式使用系统时区(我不喜欢在Joda或java.util。*中这样的含义相同)。如果从UTC-DateTime对象前后返回的整个转换不起作用,那么我假设你的DateTime-objects输入可能不是真正的UTC。我建议明确检查一下。否则,我们没有足够的信息来说明您的转换代码无效的原因。