【问题标题】:How to get UTC timestamps from JDBC+postgreSql timestamp?如何从 JDBC+postgreSql 时间戳获取 UTC 时间戳?
【发布时间】:2014-11-21 18:08:22
【问题描述】:

我在 PostgreSQL 中创建了一个像这样的表:

create table myTable (
    dateAdded timestamp(0) without time zone null default (current_timestamp at time zone 'UTC');
)

我选择“无时区”,因为我知道我的应用程序使用的所有时间戳始终 UTC。就我获得的文档而言,与“带时间戳”的唯一区别是我可以提供其他时区的值,然后将其转换为 UTC。但是我想避免这种自动转换,因为如果我知道我的值是 UTC,它们几乎不会有任何好处。

当我在测试表中添加新记录并使用 pgAdmin 查看表的内容时,我可以看到插入日期已正确保存为 UTC 格式。

但是,当我尝试使用 JDBC 选择值时,该值会减去 2 小时。我位于 UTC+2,所以看起来 JDBC 假定表中的日期不是 UTC 时间戳,而是 UTC+2 时间戳,并尝试转换为 UTC。

一些谷歌搜索显示 JDBC 标准规定了与当前时区之间的转换,但这可以通过向 getTimestamp/setTimestamp 调用提供 Calander 来防止。然而,提供日历并没有任何区别。这是我的 MyBatis/Jodatime 转换器:

@MappedTypes(DateTime.class)
public class DateTimeTypeHandler extends BaseTypeHandler<DateTime> {
    private static final Calendar UTC_CALENDAR = Calendar.getInstance(DateTimeZone.UTC.toTimeZone());

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
            DateTime parameter, JdbcType jdbcType) throws SQLException {
        ps.setTimestamp(i, new Timestamp(parameter.getMillis()), UTC_CALENDAR);
    }

    @Override
    public DateTime getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        return fromSQLTimestamp(rs.getTimestamp(columnName, UTC_CALENDAR));
    }
    /* further get methods with pretty much same content as above */

    private static DateTime fromSQLTimestamp(final Timestamp ts) {
        if (ts == null) {
            return null;
        }
        return new DateTime(ts.getTime(), DateTimeZone.UTC);
    }
}

从 JDBC+PostgreSQL 时间戳源获取 UTC 时间戳的正确方法是什么?

【问题讨论】:

  • 你有没有考虑过使用新的java 8 time api?,以前的dateTime类也有这种问题。也许使用 java 8 中的 Instant 类而不是 DateTime。

标签: java postgresql datetime jdbc


【解决方案1】:

解决方案

将 UTC 设置为 JVM -Duser.timezone=UTC 的默认时区或将整个操作系统设置为 UTC。

背景

在 Postgres 中,TIMESTAMPTIMESTAMP WITH TIMEZONE 的存储方式相同 - 自 Postgres 纪元 (2000-01-01) 以来的秒数。主要区别在于 Postgres 在保存时间戳值(例如 2004-10-19 10:23:54+02)时所做的事情:

  • 没有 TZ,+02 就被剥离了
  • 使用 TZ 执行 -02 校正以使其成为 UTC

现在有趣的是 JDBC 驱动程序加载值时:

  • 没有 TZ,存储的值由用户的 (JVM / OS) TZ 移动
  • TZ 的值被认为是 UTC

在这两种情况下,您最终都会得到带有用户默认 TZ 的 java.sql.Timestamp 对象。

时区

没有 TZ 的时间戳非常有限。如果您有两个系统连接到您的数据库,它们都具有不同的 TZ,那么它们会以不同的方式解释时间戳。因此,我强烈建议您使用TIMESTAMP WITH TIMEZONE


更新

你可以tell JDBC通过ResultSet#getTimestamp(String, Calendar)读取时间戳时应该使用什么样的TZ。摘自 JavaDoc:

如果底层数据库不存储时区信息,此方法使用给定的日历为时间戳构造适当的毫秒值。

【讨论】:

  • 当然是一个有效的答案,但需要具有系统访问权限。我更喜欢一种解决方案,它允许我发布一个可以部署在服务器上的战争文件,而不会出现此类服务器配置问题。
  • 更新了答案,提供了如何使 JDBC 驱动程序使用不同的 TZ 的信息。您在问题中提到了类似的内容,但您可能只是使用了不同的方法。
  • 使用-Duser.timezone=UTC 技巧,DB 查询参数现在以 UTC 格式发送(很好),但结果仍然是时间转移到本地时区。更改客户端系统时钟是我目前的工作。是否有 JDBC 参数或 JVM 参数来禁用此转换? (即类似于您的getTimestamp() 解决方案,但不是Java 代码。我只能访问已编译的JAR(PyCharm,它使用JDBC)。
  • 时移发生在哪里?您的意思是在使用-Duser.timezone=UTC 运行时,直接从JDBC 中获得的ResultSet 时间不正确?
【解决方案2】:

Pavel 提出的解决方案(添加 jvm 参数 '-Duser.timezone=UTC')肯定是最好的选择,如果您没有系统访问权限,这并不总是可行的。

我发现的唯一方法是将查询中的时间戳转换为epoch,并将其读取为long

SELECT  extract(epoch from my_timestamp_colum at time zone 'utc')::bigint * 1000 
            AS my_timestamp_as_epoc
FROM    my_table

然后用

在纯 JDBC 中读取它
ResultSet rs = ...
long myTimestampAsEpoc = rs.getLong("my_timestamp_as_epoc");
Date myTimestamp = new Date(myTimestampAsEpoc);

使用 iBatis/MyBatis,您需要将列处理为 Long,然后手动将其转换为 Date/Timestamp。不方便和丑陋。

PostgreSQL 中的反向操作可以通过:

SELECT TIMESTAMP WITHOUT TIME ZONE 'epoch' + 
       1421855729000 * INTERVAL '1 millisecond'

也就是说,JDBC 规范并没有说返回的时间戳应该或不应该将时间戳转移到用户时区;但是,由于您将表列定义为“没有时区的时间戳”,我希望没有时间变化,但记录的纪元只是包装在 java.sql.Timestamp 中。在我看来,这是 PostgreSQL JDBC 驱动程序中的一个错误。那么,作为这个级别的问题,如果没有系统访问权限,可能没有什么可以做的了。

【讨论】:

  • JDBC 规范(或:API 文档)要求 - 默认情况下 - 在 JVM 的当前时区中考虑时间。如果您想使用另一个时区,那么您可以在正确的时区提供Calendar。由于这是问题中所做的,因此看起来这是实际的错误。
  • 您能否指出 JDBC 规范在哪里将 JVM 时区声明为默认值?昨天搜了一下,没找到。
  • setTimestamp(int parameterIndex, Timestamp x):它根本没有提到任何日历或时区。事实上,此方法使用 null 日历委托给 setTimestamp(int parameterIndex, Timestamp x, Calendar cal),这是规范未强制要求的实现细节。
  • 强调我的:"如果没有指定日历对象,驱动使用默认时区,也就是虚拟机的时区运行应用程序。” JDBC 的问题在于您需要兼顾规范和 API 文档,并记住信息并不总是重复的。没有Calendar 的set/getTimestamp 就是这里提到的"If no Calendar object is specified"
【解决方案3】:

有一些针对 Postgres JDBC 驱动程序的技巧

https://jdbc.postgresql.org/documentation/head/java8-date-time.html

所以在阅读时你可以这样做

    Instant utc =resultSet.getObject("dateAdded",LocalDateTime.class).toInstant(ZoneOffset.UTC);

如果使用Hikari等连接池,还可以通过设置connectionInitSql=set time zone 'UTC'来指定每个连接使用的时区

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-05-02
    • 2015-01-08
    • 2014-03-31
    • 2013-03-20
    • 2017-07-04
    • 2012-11-11
    • 2011-05-21
    • 2011-12-24
    相关资源
    最近更新 更多