将java.util.Date转换为带有时区的PostgreSQL时间戳

3

我正在尝试将日期插入到我的PostgreSQL数据库中。以下是我在Java应用程序中解析日期的方式:

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
Date parsedTimeStamp = dateFormat.parse(dateString);

在我的PostgreSQL数据库中,我将其设置为带时区的时间戳。处理和插入它的理想方法是什么?
数据库期望的示例: 2017-09-09 07:15:33.451061+00

实际上,你的数据库需要数据,而不是特定格式的字符串。让你的JDBC驱动程序担心通信数据的技术实现细节。你应该专注于Java对象。 - Basil Bourque
3个回答

6

简而言之

myPreparedStatement.setObject( 
    … , 
    LocalDate.parse( "20170909" , DateTimeFormatter.BASIC_ISO_DATE )  // Parse string into a date-only object, a `LocalDate` object.
             .atStartOfDay( ZoneId.of( "Asia/Kolkata" ) )             // Apply a time zone to determine the first moment of the day on that date, producing a `ZonedDateTime` object.
             .toInstant()                                             // Extract an `Instant` object, a moment always in UTC. This last step is not technically required, as your JDBC driver with Postgres should effectively do this on your behalf. But this line completes the demo conceptually.
) ;

避免使用旧类

其他答案已经过时,使用麻烦的旧日期时间类,这些类现在已被java.time类所取代。

java.time

如果您使用支持JDBC 4.2及更高版本的JDBC驱动程序,您可以与Postgres服务器交换java.time对象。无需使用java.sql日期时间类或可怕的DateCalendar类。

持久化:

Instant instant = Instant.now() ;
myPreparedStatement.setObject( … , instant ) ;

...和检索:

Instant instant = myResultSet( … , Instant.class ) ;

在标准SQL中,TIMESTAMP WITH TIME ZONE是一个时刻,时间线上的特定点。换句话说,它包含日期、时间和时区信息。相比之下,如果只有日期而没有时间和时区信息,你可能想要使用该日期在特定时区的第一时刻。
要将仅包含日期的值转换为时刻,请首先创建一个LocalDate对象。
LocalDate ld = LocalDate.parse( "20170909" , DateTimeFormatter.BASIC_ISO_DATE ) ; 

接下来,调用该LocalDate对象的atStartOfDay方法,并传递一个ZoneId
时区是为了赋予日期意义而必需的。对于任何给定时刻,由于时区的不同,日期在全球范围内会有所变化。例如,在法国巴黎午夜过后几分钟就是新的一天,而在加拿大魁北克省蒙特利尔仍然是“昨天”。

请用continent/region的格式指定一个正确的时区名称,例如America/MontrealAfrica/CasablancaPacific/Auckland。永远不要使用三到四个字母的缩写,例如ESTIST,因为它们不是真正的时区,没有标准化,甚至也不是唯一的!

ZoneId z = ZoneId.of( "America/Montreal" ) ;
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

最后提取一个 Instant
Instant instant = zdt.toInstant() ; 

在此时,您可以像上面讨论的那样将该Instant插入数据库。

对象,而不是字符串

如果您学会了在Java中与数据库交换数据时以对象而不仅仅是字符串的方式思考,您的工作将更加容易。

TIMESTAMP WITH TIME ZONE

顺便提一下,请明确Postgres如何使用数据类型TIMESTAMP WITH TIME ZONE。 SQL标准对此数据类型定义过于简要,而各个数据库在其实现方面也有所不同。以下描述适用于Postgres 8、9和10版本的行为。

使用带有时区或UTC偏移量的任何日期时间值都会使用该时区/偏移量来确定UTC中的值。然后将该UTC值存储在数据库中,分辨率为微秒(而不是旧版Java java.util.DateCalendar类的毫秒,也不是java.time类的纳秒)。进行UTC调整后,区域/偏移信息将被丢弃。如果您关心原始的区域/偏移量,请将其明确地存储在单独的列中。

当从Postgres中检索日期时间值时,它以UTC形式发送。诸如psqlpgAdmin之类的工具可能会混淆地应用默认时区。虽然出于善意,这种特性会产生存储值带有时区的错觉,而实际上并不是这样。相比之下,符合JDBC 4.2及更高版本的JDBC驱动程序将自动处理UTC和时区。通常建议始终检索Instant对象,如上面的代码所示。然后,如果需要,可以像上面的代码一样应用时区来实例化您自己的ZonedDateTimeOffsetDateTime对象。


关于java.time

java.time框架内置于Java 8及以上版本。这些类取代了麻烦的旧遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

要了解更多,请参阅Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310
在哪里获取java.time类?

ThreeTen-Extra 项目通过添加额外的类来扩展 java.time。该项目是 java.time 可能的未来补充的试验场。您可能会在这里找到一些有用的类,例如 Interval, YearWeek, YearQuarter更多


2

在这里使用的SimpleDateFormat将一个字符串翻译为日期,在您的例子中,这必须意味着您使用的格式是"yyyyMMdd",例如20171217(今天)。

要创建用于DB的字符串,您需要采取相反的方法。从日期对象到字符串。

您可以为此创建自己的格式化程序(使用您的示例)。

    Date date = new Date();
    DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss.SSSSSSX");
    String text = formatter.format(date);

您可以在此处阅读有关格式化的更多信息:这里

我得到了以下异常:Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: ClockHourOfAmPm - Killer Beast
我更新了代码。 请注意,第一行获取当前日期(现在)。 - GuyKhmel
但是这个问题特别询问如何解析仅包含日期的字符串,而不是获取当前时间。这个回答没有回答到这个问题。 - Basil Bourque
@BasilBourque,你是正确的,我提供了更通用的方法。再一次,我链接到格式化文档,在那里你可以找到以下示例: LocalDate date = LocalDate.now(); String text = date.format(formatter); LocalDate parsedDate = LocalDate.parse(text, formatter); - GuyKhmel
我需要使用这个变量:yyyy-MM-dd HH:mm:ss.SSSSSSX - 如果有帮助的话。 - adu

0
根据我在JDBC映射类型的Postgres文档中看到的,Postgres期望使用java.sql.Timestamp来映射为带有时区的时间戳类型。如果您正在使用预编译语句进行操作,可以使用以下代码:
java.sql.Timestamp ts = new Timestamp(parsedTimeStamp.getTime());
PreparedStatement stmt;
// define the statement using Postgres SQL with placeholders
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
// now set the timestamp at the particular timezone in the statement
stmt.setTimestamp(index, ts, cal);

GMT替换为您实际想要的时区,并将index替换为时间戳应出现在实际SQL中的占位符的数字索引。

请注意,更近期的Java 8兼容驱动程序应支持使用LocalDateTime以及java.sql.Timestamp


谢谢!你能帮忙将这个翻译成非预编译语句吗?还是不可能? - Killer Beast
@KillerBeast 为什么你不使用语句?我认为你应该使用它们。 - Tim Biegeleisen
返回翻译的文本:目前为止,该项目不使用预处理语句,为了保持统一性,我不能使用“true”。 - Killer Beast
感谢您的所有帮助。我明白您提供的解决方案比被采纳的答案更好,但不幸的是,我只能选择一个答案。甚至不能点赞。因此,我仍然坚持选择另一个答案,因为它更符合我的要求。无论如何,再次感谢! - Killer Beast

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接