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

How to store date-time in UTC into a database using EclipseLink and Joda-Time?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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的属性在模型类中指定如下。

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


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

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

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.

Ok.

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

请参阅setTimestamp()方法的两个重载版本中的以下两个对setTimestampInternal()方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * 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())。

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

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

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

    例如,对于应用程序服务器GlassFish,可以在创建JDBC领域时将它们与服务器内部的JDBC连接池一起设置,也可以使用管理Web GUI工具或直接在domain.xml中设置其他可配置属性。 domain.xml如下所示(使用XA数据源)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <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中配置它们。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    <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,在此类中使用它们各自的setter方法。

    例如,如果直接使用核心JDBC API,或者在Tomcat中使用连接池,则可以直接将它们设置为连接URL(在context.xml中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <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,但在这方面两者都相似。直接从此链接引用。

    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.

    Ok.

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

    Ok.

    顺便说一句,我放弃了EclipseLink的专有转换器,因为JPA 2.1提供了自己的标准转换器,可以根据需要移植到不同的JPA提供程序,而不需要进行任何修改。它现在看起来像下面的java.util.Date也被java.sql.Timestamp替换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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 /远程桌面客户端等)负责根据适当用户的时区转换日期/时间,同时向最终用户显示或呈现日期/时间。为简洁起见,本答案未涉及,并且根据当前问题的性质偏离主题。

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

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

    好。


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

    1
    2
    3
    4
    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()并使用显式转换,如:

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

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

    EDIT 2:

    测试代码

    1
    2
    3
    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。*中这样的implicits相同)。如果从UTC-DateTime对象前后返回的整个转换不起作用,那么我假设你的DateTime-objects输入可能不是真正的UTC。我建议明确检查一下。否则,我们没有足够的信息来说明您的转换代码无效的原因。