我们有一个Postgres表,该表具有两个TIMESTAMP WITHOUT TIME ZONE
列,即prc_sta_dt和prc_end_dt。我们检查一下java.util.Date
是否介于开始日期和结束日期之间。
这里有一些Java代码,虽然经过简化,但很重要。
// This format expects a String such as 2018-12-03T10:00:00
// With a date and a time, but no time zone
String timestamp = "2018-12-03T10:00:00";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date searchDate = formatter.parse(timestamp);
// Here's the Postgres query
String query = "select promotion_cd from promotions " +
"where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt";
Map<String, Object> map = new HashMap<String, Object>();
map.put("srch_dt", searchDate);
List<Promotion> promotions = jdbcTemplate.query(query, map, promotionMapper);
在我们的Postgres表中,促销活动从2018年3月12日上午9点开始,到同一天下午3点结束。我们数据库中这些行的prc_sta_dt和prc_end_dt列为2018-12-03 09:00:00.0
和2018-12-03 15:00:00.0
问题:当JDBC / Postgres接受我们的searchDate
并将其与这些时间戳进行比较时,它将接受给定的上午10点搜索日期(2018-12-03T10: 00:00)还是将这段时间视为服务器正在运行的时区之下,然后将其转换为UTC?
例如,如果服务器在芝加哥运行,那么它将在数据库中进行比较之前将上午10点解释为CST上午10点,然后将其转换为UTC下午4点吗?如果是这样,那我们就不走运了!
我怀疑这种情况是否会发生,但我只是想确保没有意外。
答案 0 :(得分:5)
Date
不是日期 java.util.Date
对象代表UTC中的时刻,是时间轴上的特定点。因此,它表示日期,一天中的时间以及与UTC的偏移量为零(对于UTC本身)的组合。在这个可怕的类中许多糟糕的设计选择中,有一个令人误解的名称,它使无数的Java程序员感到困惑。
TIMESTAMP WITHOUT TIME ZONE
如果您关心时刻,那么您的数据库列应不为TIMESTAMP WITHOUT TIME ZONE
类型。该数据类型表示日期和时间,没有任何时区或UTC偏移量的概念。因此,根据定义,该类型不能表示时刻,不是时间轴上的一点。仅当您指的是带有日期的时间任何地方或任何地方时,才应使用此类型。
示例:
TIMESTAMP WITH TIME ZONE
在跟踪特定的特定时刻(时间轴上的单个点)时,请使用类型为TIMESTAMP WITH TIME ZONE
的列。在Postgres中,这些值存储在UTC中。使用输入提交的任何时区或偏移量信息都可用于调整为UTC,然后将时区/偏移量信息丢弃。
注意::某些工具可能具有很好的意图,但不幸的是,它在检索UTC中的值后注入时区会产生反作用,从而错误地表示了实际存储的内容。
TIMESTAMP WITHOUT TIME ZONE
的值进行比较关于将时刻与类型为TIMESTAMP WITHOUT TIME ZONE
的列中的值进行比较,这样做通常毫无意义。
但是,如果您对日期时间处理头脑清醒并受过教育,并且在您的业务逻辑中进行这种比较是明智的,那就继续吧。
您使用的是糟糕,糟糕,糟糕的日期时间类(Date
,SimpleDateFormat
等),这些类是几年前被 java.time 类取代的。帮自己一个忙:停止使用旧的日期时间类。仅使用 java.time 。
如果稍加java.util.Date
,请使用添加到旧类中的新方法进行转换。特别是java.util.Date
被Instant
取代。
Instant instant = myJavaUtilDate.toInstant() ; // Convert from legacy class to modern class.
指定您要在UTC中调整Instant
时刻以进行比较的时区。例如,如果您的数据库是由不了解正确的日期时间处理方式的人建立的,并且一直在使用TIMESTAMP WITHOUT TIME ZONE
列来存储从魁北克的壁钟时间获取的日期-时间值,然后使用时区America/Montreal
。
以continent/region
的格式指定proper time zone name,例如America/Montreal
,Africa/Casablanca
或Pacific/Auckland
。切勿使用2-4个字母的缩写,例如EST
或IST
,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。
ZoneId z = ZoneId.of( "America/Montreal" ) ;
将该区域应用于我们的Instant
以获取一个ZonedDateTime
对象。
ZonedDateTime zdt = instant.atZone( z ) ;
我们得到的ZonedDateTime
对象表示与Instant
对象相同的时刻,时间轴上的相同点,但是使用不同的挂钟时间查看。
要锤击square-peg into a round-hole,让我们将该ZonedDateTime
对象转换为LocalDateTime
对象,从而剥离时区信息,仅保留带日期的日期值。
LocalDateTime ldt = zdt.toLocalDateTime() ;
where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt
这种逻辑很容易失败。通常,定义时间跨度以使用Half-Open时,日期时间处理的最佳做法是,开始时间为 inclusive ,结束时间为 exclusive 。
所以使用这个:
WHERE instant >= start_col AND instant < stop_col ;
对于PreparedStatement
,我们将使用占位符。
WHERE ? >= start_col AND ? < stop_col ;
在Java方面,从JDBC 4.2开始,我们可以通过getObject
和setObject
方法与数据库直接交换 java.time 对象。
您可能可以根据您的JDBC驱动程序传递Instant
。 JDBC规范不需要支持Instant
。因此,请尝试一下,或阅读驱动程序的文档。
myPreparedStatement.setObject( 1 , instant ) ;
myPreparedStatement.setObject( 2 , instant ) ;
如果不支持Instant
,请从Instant
转换为设置为UTC的OffsetDateTime
。规范要求支持OffsetDateTime
。
myPreparedStatement.setObject( 1 , instant.atOffset( ZoneOffset.UTC ) ) ;
myPreparedStatement.setObject( 2 , instant.atOffset( ZoneOffset.UTC ) ) ;
检索。
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
例如,如果服务器在芝加哥运行,那么它将在数据库中进行比较之前将上午10点解释为CST上午10点,然后将其转换为UTC下午4点吗?
程序员应从不依赖于当前在主机OS或JVM上设置为默认值的时区(或语言环境)。两者都无法控制。两者都可以在运行时随时更改 !
始终通过将可选参数传递给各种日期时间方法来指定时区。在我看来,将这些设置为可选是 java.time 的一个设计缺陷,因为程序员常常无视自己的时区问题。但这是一个非常有用且优雅的框架中极少数设计缺陷之一。
在上面的代码中,我们指定了期望/期望的时区。主机操作系统,Postgres数据库连接和JVM的当前默认时区不会改变代码的行为。
如果您需要当前时刻,请使用以下任意一项:
Instant.now()
OffsetDateTime.now( someZoneOffset )
ZonedDateTime.now( someZoneId )
如果您使用的是Java 7,则没有内置的 java.time 类。幸运的是,JSR 310和 java.time 的发明者Stephen Colebourne也带领ThreeTen-Backport项目创建了一个提供大部分 java.time 的库。 em> Java 6和7的功能。
这是一个完整的示例应用程序,位于单个.java文件中,显示了Java 7中使用H2 Database Engine的反向端口的用法。
在Java 7中,JDBC 4.2不可用,因此我们不能直接使用现代类。我们回过头来使用java.sql.Timestamp
,它实际上代表UTC的一个时刻,但H2会按原样的日期和时间存储到TIMESTAMP WITHOUT TIME ZONE
的列中(使用挂钟) UTC时间),而忽略了UTC方面。我没有在Postgres中尝试过此方法,但是我希望您会看到相同的行为。
package com.basilbourque.example;
import java.sql.*;
import org.threeten.bp.*;
public class App {
static final public String databaseConnectionString = "jdbc:h2:mem:localdatetime_example;DB_CLOSE_DELAY=-1"; // The `DB_CLOSE_DELAY=-1` keeps the in-memory database around for multiple connections.
public static void main ( String[] args ) {
App app = new App();
app.doIt();
}
private void doIt () {
System.out.println( "Bonjour tout le monde!" );
// java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
// System.out.println( ts );
this.makeDatabase();
java.util.Date d = new java.util.Date(); // Capture the current moment using terrible old date-time class that is now legacy, supplanted years ago by the class `java.time.Instant`.
this.fetchRowsContainingMoment( d );
}
private void makeDatabase () {
try {
Class.forName( "org.h2.Driver" );
} catch ( ClassNotFoundException e ) {
e.printStackTrace();
}
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ; // The `mem` means “In-Memory”, as in “Not persisted to disk”, good for a demo.
Statement stmt = conn.createStatement() ;
) {
String sql = "CREATE TABLE event_ ( \n" +
" pkey_ IDENTITY NOT NULL PRIMARY KEY , \n" +
" name_ VARCHAR NOT NULL , \n" +
" start_ TIMESTAMP WITHOUT TIME ZONE NOT NULL , \n" +
" stop_ TIMESTAMP WITHOUT TIME ZONE NOT NULL \n" +
");";
stmt.execute( sql );
// Insert row.
sql = "INSERT INTO event_ ( name_ , start_ , stop_ ) VALUES ( ? , ? , ? ) ;";
try (
PreparedStatement preparedStatement = conn.prepareStatement( sql ) ;
) {
preparedStatement.setObject( 1 , "Alpha" );
// We have to “fake it until we make it”, using a `java.sql.Timestamp` with its value in UTC while pretending it is not in a zone or offset.
// The legacy date-time classes lack a way to represent a date with time-of-day without any time zone or offset-from-UTC.
// The legacy classes have no counterpart to `TIMESTAMP WITHOUT TIME ZONE` in SQL, and have no counterpart to `java.time.LocalDateTime` in Java.
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 2 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Beta" );
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 4 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 5 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Gamma" );
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 11 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 12 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
private void fetchRowsContainingMoment ( java.util.Date moment ) {
// Immediately convert the legacy class `java.util.Date` to a modern `java.time.Instant`.
Instant instant = DateTimeUtils.toInstant( moment );
System.out.println( "instant.toString(): " + instant );
String sql = "SELECT * FROM event_ WHERE ? >= start_ AND ? < stop_ ORDER BY start_ ;";
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ;
PreparedStatement pstmt = conn.prepareStatement( sql ) ;
) {
java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( instant );
pstmt.setTimestamp( 1 , ts );
pstmt.setTimestamp( 2 , ts );
try ( ResultSet rs = pstmt.executeQuery() ; ) {
while ( rs.next() ) {
//Retrieve by column name
Integer pkey = rs.getInt( "pkey_" );
String name = rs.getString( "name_" );
java.sql.Timestamp start = rs.getTimestamp( "start_" );
java.sql.Timestamp stop = rs.getTimestamp( "stop_" );
// Instantiate a `Course` object for this data.
System.out.println( "Event pkey: " + pkey + " | name: " + name + " | start: " + start + " | stop: " + stop );
}
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
}
运行时。
instant.toString():2018-12-04T05:06:02.573Z
事件pkey:3 |名称:Gamma |开始:2018-11-23 16:30:00.0 |停止:2018-12-23 16:30:00.0
从概念上讲,这是相同的示例,但是在Java 8或更高版本中,我们可以使用没有 ThreeTen-Backport 库的内置 java.time 类。
package com.basilbourque.example;
import java.sql.*;
import java.time.*;
public class App {
static final public String databaseConnectionString = "jdbc:h2:mem:localdatetime_example;DB_CLOSE_DELAY=-1"; // The `DB_CLOSE_DELAY=-1` keeps the in-memory database around for multiple connections.
public static void main ( String[] args ) {
App app = new App();
app.doIt();
}
private void doIt ( ) {
System.out.println( "Bonjour tout le monde!" );
this.makeDatabase();
java.util.Date d = new java.util.Date(); // Capture the current moment using terrible old date-time class that is now legacy, supplanted years ago by the class `java.time.Instant`.
this.fetchRowsContainingMoment( d );
}
private void makeDatabase ( ) {
try {
Class.forName( "org.h2.Driver" );
} catch ( ClassNotFoundException e ) {
e.printStackTrace();
}
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ; // The `mem` means “In-Memory”, as in “Not persisted to disk”, good for a demo.
Statement stmt = conn.createStatement() ;
) {
String sql = "CREATE TABLE event_ ( \n" +
" pkey_ IDENTITY NOT NULL PRIMARY KEY , \n" +
" name_ VARCHAR NOT NULL , \n" +
" start_ TIMESTAMP WITHOUT TIME ZONE NOT NULL , \n" +
" stop_ TIMESTAMP WITHOUT TIME ZONE NOT NULL \n" +
");";
stmt.execute( sql );
// Insert row.
sql = "INSERT INTO event_ ( name_ , start_ , stop_ ) VALUES ( ? , ? , ? ) ;";
try (
PreparedStatement preparedStatement = conn.prepareStatement( sql ) ;
) {
preparedStatement.setObject( 1 , "Alpha" );
// We have to “fake it until we make it”, using a `java.sql.Timestamp` with its value in UTC while pretending it is not in a zone or offset.
// The legacy date-time classes lack a way to represent a date with time-of-day without any time zone or offset-from-UTC.
// The legacy classes have no counterpart to `TIMESTAMP WITHOUT TIME ZONE` in SQL, and have no counterpart to `java.time.LocalDateTime` in Java.
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
;
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 2 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Beta" );
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 4 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 5 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Gamma" );
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 11 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 12 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
private void fetchRowsContainingMoment ( java.util.Date moment ) {
// Immediately convert the legacy class `java.util.Date` to a modern `java.time.Instant`.
Instant instant = moment.toInstant();
System.out.println( "instant.toString(): " + instant );
String sql = "SELECT * FROM event_ WHERE ? >= start_ AND ? < stop_ ORDER BY start_ ;";
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ;
PreparedStatement pstmt = conn.prepareStatement( sql ) ;
) {
pstmt.setObject( 1 , instant );
pstmt.setObject( 2 , instant );
try ( ResultSet rs = pstmt.executeQuery() ; ) {
while ( rs.next() ) {
//Retrieve by column name
Integer pkey = rs.getInt( "pkey_" );
String name = rs.getString( "name_" );
Instant start = rs.getObject( "start_" , OffsetDateTime.class ).toInstant();
Instant stop = rs.getObject( "stop_" , OffsetDateTime.class ).toInstant();
// Instantiate a `Course` object for this data.
System.out.println( "Event pkey: " + pkey + " | name: " + name + " | start: " + start + " | stop: " + stop );
}
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
}
运行时。
instant.toString():2018-12-04T05:10:54.635Z
事件pkey:3 |名称:Gamma |开始:2018-11-24T00:30:00Z |停止:2018-12-24T00:30:00Z
java.time框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.Date
,Calendar
和SimpleDateFormat
。
目前位于Joda-Time的maintenance mode项目建议迁移到java.time类。
要了解更多信息,请参见Oracle Tutorial。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310。
您可以直接与数据库交换 java.time 对象。使用符合JDBC driver或更高版本的JDBC 4.2。不需要字符串,不需要java.sql.*
类。
在哪里获取java.time类?
ThreeTen-Extra项目使用其他类扩展了java.time。该项目为将来可能在java.time中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如Interval
,YearWeek
,YearQuarter
和more。