【问题标题】:How to store date-time in UTC into a database using EclipseLink and Joda-Time?如何使用 EclipseLink 和 Joda-Time 将 UTC 中的日期时间存储到数据库中?
【发布时间】:2014-04-20 08:16:30
【问题描述】:

我长期以来一直在摸索以下 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);
    }
}

convertObjectValueToDataValue() 方法的objectValue 参数是一个instanceOf DateTime,它已经根据UTC 时区。因此,我避开了.withZone(DateTimeZone.UTC)

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

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

无论如何日期时间都应该根据UTC时区插入MySQL。

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


编辑:

作为示例,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.    

【问题讨论】:

  • 您是否尝试过使用new DateTime(((Date) dataValue).getTime(), TimeZone.getDefault()) 构建您的DateTime 对象?
  • 字符串到日期时间的转换是通过这一行进行的,DateTimeFormat.forPattern("dd-MMM-yyyy hh:mm:ss aa").parseDateTime(StringRepresentationOfDate).withZone(DateTimeZone.UTC) 在一个单独的转换器中。 new DateTime(((Date) dataValue).getTime(), TimeZone.getDefault()) 能有所作为吗?
  • 啊,我明白了...您必须通过Calendar 更改Date 时区。这就是我讨厌 Java 的日期 API 的原因……幸运的是,Java 8 有一个好主意来挽救 Joda Time!
  • @Tobb :我已经编辑了帖子以显示映射。

标签: mysql jpa eclipselink jodatime utc


【解决方案1】:

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

因此,除非 MySQL JDBC 驱动程序被显式强制使用 UTC 区域或 JVM 本身设置为使用该区域,否则即使 MySQL 本身是配置为使用 UTC 在 my.ini 中使用 default_time_zone='+00:00' 或在 [mysqld] 部分中使用 my.cnf。像 Oracle 这样的一些数据库可能支持带时区的时间戳,这可能是我不熟悉的一个例外(未经测试,因为我目前没有那个环境)。

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

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

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

请参阅setTimestamp() 方法的两个重载版本中对setTimestampInternal() 方法的以下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());
}

当没有使用PreparedStatement#setTimestamp() 方法指定Calendar 实例时,将使用默认时区(this.connection.getDefaultTimeZone())。


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

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

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

在应用服务器的情况下,例如 GlassFish,可以在创建 JDBC 领域以及服务器本身内部的 JDBC 连接池以及其他可配置属性时进行设置,使用管理 Web GUI 工具或在domain.xml直接地。 domain.xml 如下所示(使用 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>

如果是 WildFly,可以使用 CLI 命令或使用管理 Web GUI 工具(使用 XA 数据源)在 standalone-xx.yy.xml 中配置它们。

<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>

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

在这两种情况下,所有提到的属性都将设置为 JDBC 驱动程序中可用的提到的类,即com.mysql.jdbc.jdbc2.optional.MysqlXADataSource,使用它们各自在此类中的设置方法。

如果直接使用核心 JDBC API,或者 Tomcat 中的连接池,可以直接设置为连接 URL(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>

补充:

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

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

例如,如果您正在构建电话服务并且正在使用 您的数据库服务器上的夏令时,然后您要求 麻烦:将没有办法判断一个客户是否打电话 从 "2008-10-26 02:30:00" 到 "2008-10-26 02:35:00" 实际调用 5 分钟或 1 小时 5 分钟(假设夏令时 发生在 10 月 26 日凌晨 3 点)!

顺便说一句,我放弃了 EclipseLink 的专有转换器since JPA 2.1 provides its own standard converter,它可以在需要时移植到不同的 JPA 提供程序,而无需进行少量修改或根本不需要修改。现在看起来像下面这样,其中java.util.Date 也被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);
    }
}

然后,相关的应用程序客户端(Servlet / JSP / JSF / 远程桌面客户端等)全权负责根据适当的用户时区转换日期/时间,同时显示或显示结束日期/时间- 为简洁起见,此答案未涵盖且根据当前问题的性质偏离主题的用户。

也不需要转换器中的这些空检查,因为它也完全由关联的应用程序客户端负责,除非某些字段是可选的。

现在一切正常。欢迎任何其他建议/建议。欢迎批评我的任何无知者。

【讨论】:

    【解决方案2】:

    我不明白这个问题,尤其是转换为 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); 在我的时区生成这个字符串:

    2014 年 3 月 14 日星期五 01:00:00 CET

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

    编辑:

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

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

    否则(假设反向方法始终如您所说的 UTC 中的 DateTime 对象)您可能会得到不对称(我现在不知道 JodaTime 在没有 DateTimeZone 参数的构造函数中做了什么 - 没有很好的文档记录?)。

    EDIT-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-objects 的整个转换不起作用,那么我假设您输入的 DateTime-objects 可能不是真正的 UTC。我建议明确检查这一点。否则,我们没有足够的信息说明您的转换代码为何不起作用。

    【讨论】:

    • 例如,如果我输入了02-Oct-2013 11:34:26 AM 之类的日期,则要插入 MySQL 的日期时间(DateTime 列)将类似于 UTC 中的 02-10-2013 06:04:26,而在 @ 中正确发生987654321@。不能在这里做吗?
    • 通过我的测试代码的第一行,我已经预料到你所有的 DateTime 对象都是 UTC 的声明。如果您的 DateTime 对象的来源是字符串形式的本地时间戳(如您在评论中所示),那么当然会有时区转换,但我也说 DateTime#toDate() 不应该在 DateTime- 的情况下进行区域转换对象已采用 UTC。
    • 根据上一条评论中的例子,DateTime从String转换后在UTC是2013-10-02T06:04:26.000Z,但是当使用.toDate()时,它显示2013-10-02 11:34:26.0不应该发生。
    • @Tiny 已编辑我的答案。您应该检查其他代码位置,看看它是否适合您。
    • 我已更改该构造函数以接受额外的 DateTimeZone 参数,这没有任何区别。这已经持续了五个多月了 :) 我已经厌倦了检查应用程序的其他部分。我希望问题不会在应用程序的其他地方持续存在。
    猜你喜欢
    • 1970-01-01
    • 2013-10-22
    • 2011-04-17
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 2021-01-31
    • 2013-01-14
    • 1970-01-01
    相关资源
    最近更新 更多