JDBC / Postgres如何将无时区的java.util.Date与时间戳进行比较?

时间:2018-12-03 19:05:33

标签: java postgresql jdbc time

我们有一个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.02018-12-03 15:00:00.0

问题:当JDBC / Postgres接受我们的searchDate并将其与这些时间戳进行比较时,它将接受给定的上午10点搜索日期(2018-12-03T10: 00:00)还是将这段时间视为服务器正在运行的时区之下,然后将其转换为UTC?

例如,如果服务器在芝加哥运行,那么它将在数据库中进行比较之前将上午10点解释为CST上午10点,然后将其转换为UTC下午4点吗?如果是这样,那我们就不走运了!

我怀疑这种情况是否会发生,但我只是想确保没有意外。

1 个答案:

答案 0 :(得分:5)

错误的数据类型,Date不是日期

java.util.Date对象代表UTC中的时刻,是时间轴上的特定点。因此,它表示日期,一天中的时间以及与UTC的偏移量为零(对于UTC本身)的组合。在这个可怕的类中许多糟糕的设计选择中,有一个令人误解的名称,它使无数的Java程序员感到困惑。

TIMESTAMP WITHOUT TIME ZONE

如果您关心时刻,那么您的数据库列应TIMESTAMP WITHOUT TIME ZONE类型。该数据类型表示日期和时间,没有任何时区或UTC偏移量的概念。因此,根据定义,该类型不能表示时刻,不是时间轴上的一点。仅当您指的是带有日期的时间任何地方任何地方时,才应使用此类型。

示例:

  • “圣诞节始于2018年12月25日午夜之后”,Kiribati中的圣诞节首先出现,印度随后出现,非洲甚至更晚。
  • “公司范围内的备忘录:我们在德里,杜塞尔多夫和底特律的每个工厂都将于1月21日16:00提前关闭一个小时”,其中每个工厂的下午4点是三个不同的时刻,每个时刻间隔几个小时。

TIMESTAMP WITH TIME ZONE

在跟踪特定的特定时刻(时间轴上的单个点)时,请使用类型为TIMESTAMP WITH TIME ZONE的列。在Postgres中,这些值存储在UTC中。使用输入提交的任何时区或偏移量信息都可用于调整为UTC,然后将时区/偏移量信息丢弃。

注意::某些工具可能具有很好的意图,但不幸的是,它在检索UTC中的值后注入时区会产生反作用,从而错误地表示了实际存储的内容。

将时刻与TIMESTAMP WITHOUT TIME ZONE的值进行比较

关于将时刻与类型为TIMESTAMP WITHOUT TIME ZONE的列中的值进行比较,这样做通常毫无意义

但是,如果您对日期时间处理头脑清醒并受过教育,并且在您的业务逻辑中进行这种比较是明智的,那就继续吧。

错误的班级

您使用的是糟糕,糟糕,糟糕的日期时间类(DateSimpleDateFormat等),这些类是几年前被 java.time 类取代的。帮自己一个忙:停止使用旧的日期时间类。仅使用 java.time

如果稍加java.util.Date,请使用添加到旧类中的新方法进行转换。特别是java.util.DateInstant取代。

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/MontrealAfrica/CasablancaPacific/Auckland。切勿使用2-4个字母的缩写,例如ESTIST,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

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开始,我们可以通过getObjectsetObject方法与数据库直接交换 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()
    根据定义始终使用UTC。
  • OffsetDateTime.now( someZoneOffset )
    当前时刻,以特定于UTC的偏移量的挂钟时间显示。
  • ZonedDateTime.now( someZoneId )
    居住在特定地区的人们使用挂钟时间显示的当前时刻。

Java 7和 ThreeTen-Backport

如果您使用的是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

没有 ThreeTen-Backport

的Java 8

从概念上讲,这是相同的示例,但是在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.time框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

目前位于Joda-Timemaintenance 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中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore