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

ktm*_*124 5 java postgresql time jdbc

我们有一个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);
Run Code Online (Sandbox Code Playgroud)

在我们的Postgres表格中,我们的促销活动于2014年12月3日上午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点解释为上午10点CST,然后将其转换为下午4点UTC,然后再在数据库中进行比较吗?如果是这样,那么我们运气不好!

我怀疑这会发生,但我只是想确保没有意外.

Bas*_*que 5

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

一个java.util.Date对象表示一个时刻在UTC,在时间轴上的特定点.因此,它表示日期,时间和UTC的偏移量(对于UTC本身)的组合.在这个糟糕的类中,许多糟糕的设计选择中有一个误导性的名称,这使得无数的Java程序员感到困惑.

TIMESTAMP WITHOUT TIME ZONE

如果您关心时刻,那么您的数据库列应该是类型TIMESTAMP WITHOUT TIME ZONE.该数据类型表示日期和时间,没有任何时区概念或从UTC偏移.因此,根据定义,该类型不能代表片刻,不是时间轴上的一个点.当你的意思是一个日期与时间这种类型的,才应使用在任何地方任何地方.

例子:

  • "圣诞节开始于2018年12月25日午夜的中午之后",基里巴斯的圣诞节首先出现,印度晚些时候,非洲甚至更晚.
  • "全公司备忘录:我们在德里,杜塞尔多夫和底特律的每家工厂都将在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,这样做通常没有意义.

但是如果你对日期时间处理有着清醒的头脑和教育,并且在你的业务逻辑中进行这种比较是明智的,那就让我们开始吧.

错误的课程

您正在使用糟糕的,可怕的,可怕的日期时间类(Date,SimpleDateFormat,等)被取代年前由java.time类.帮自己一个忙:停止使用遗留日期时间类.仅使用java.time.

如果给定片刻java.util.Date,则使用添加到旧类中的新方法进行转换.特别java.util.Date是被替换为Instant.

Instant instant = myJavaUtilDate.toInstant() ;  // Convert from legacy class to modern class.
Run Code Online (Sandbox Code Playgroud)

指定要以InstantUTC格式调整时刻的时区以进行比较.例如,如果您的数据库是由不了解正确的日期时间处理的人构建的,并且一直使用该TIMESTAMP WITHOUT TIME ZONE列来存储从魁北克的挂钟时间中获取的日期时间值,那么请使用时间区域America/Montreal.

指定适当的时区名称,格式continent/region,如America/Montreal,Africa/CasablancaPacific/Auckland.切勿使用2-4字母缩写,例如ESTIST因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!).

ZoneId z = ZoneId.of( "America/Montreal" ) ;
Run Code Online (Sandbox Code Playgroud)

将该区域应用于我们Instant以获取ZonedDateTime对象.

ZonedDateTime zdt = instant.atZone( z ) ;
Run Code Online (Sandbox Code Playgroud)

我们生成的ZonedDateTime对象表示与对象相同的时刻Instant,时间轴上的相同点,但使用不同的挂钟时间查看.

为了将方形钉子敲入圆孔,让我们将该ZonedDateTime对象转换为LocalDateTime对象,从而剥离时区信息并仅留下具有时间日期值的日期.

LocalDateTime ldt = zdt.toLocalDateTime() ;
Run Code Online (Sandbox Code Playgroud)

半开

where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt
Run Code Online (Sandbox Code Playgroud)

这种逻辑很容易失败.通常,在定义使用半开的时间跨度时,日期时间处理的最佳实践,其中开头是包含的,而结尾是独占的.

所以使用这个:

WHERE instant >= start_col AND instant < stop_col ;
Run Code Online (Sandbox Code Playgroud)

对于a PreparedStatement,我们会有占位符.

WHERE ? >= start_col AND ? < stop_col ;
Run Code Online (Sandbox Code Playgroud)

On the Java side, as of JDBC 4.2 we can directly exchange java.time objects with the database via getObject and setObject methods.

You might be able to pass an Instant depending on your JDBC driver. Support for Instant is not required by the JDBC spec. So try it, or read the doc for your driver.

myPreparedStatement.setObject( 1 , instant ) ;
myPreparedStatement.setObject( 2 , instant ) ;
Run Code Online (Sandbox Code Playgroud)

If Instant is not supported, convert from Instant to an OffsetDateTime set to UTC. Support for OffsetDateTime is required by the spec.

myPreparedStatement.setObject( 1 , instant.atOffset( ZoneOffset.UTC ) ) ;
myPreparedStatement.setObject( 2 , instant.atOffset( ZoneOffset.UTC ) ) ;
Run Code Online (Sandbox Code Playgroud)

Retrieval.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Run Code Online (Sandbox Code Playgroud)

Always specify time zone

For example, if the server is running in Chicago, then will it interpret 10 am as 10am CST and then convert that to 4pm UTC before doing the comparison in the database?

A programmer should never depend on the time zone (or locale, by the way) currently set as the default on the host OS or JVM. Both are out of your control. And both can change at any moment during runtime!

Always specify the time zone by passing the optional argument to various date-time methods. Making those optional was a design flaw in java.time in my opinion, as programmers all too often ignore the issue of time zone, at their own peril. But that is one of very few design flaws in an amazingly useful and elegant framework.

Notice in our code above we specified the desired/expected time zone. The current default time zone of our host OS, our Postgres database connection, and our JVM will not alter the behavior of our code.

Current moment

If you want the current moment use any of these:

  • Instant.now()
    Always in UTC, by definition.
  • OffsetDateTime.now( someZoneOffset )
    Current moment as seen in the wall-clock time of a particular offset-from-UTC.
  • ZonedDateTime.now( someZoneId )
    Current moment as seen in the wall-clock time used by the people living in a particular region.

Java 7 and ThreeTen-Backport

If you are using Java 7, then you have no java.time classes built-in. Fortunately, the inventor of JSR 310 and java.time, Stephen Colebourne, also led the ThreeTen-Backport project to produce a library providing most of the java.time functionality to Java 6 & 7.

Here is a complete example app in a single .java file showing the use of back-port in Java 7 with the H2 Database Engine.

In Java 7, JDBC 4.2 is not available, so we cannot directly use the modern classes. We fall back to using java.sql.Timestamp which actually represents a moment in UTC, but which H2 stores into a column of TIMESTAMP WITHOUT TIME ZONE taking the date and the time-of-day as-is (using the wall-clock time of UTC) while ignoring the UTC aspect. I have not tried this in Postgres, but I expect you will see the same behavior.

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

When run.

instant.toString(): 2018-12-04T05:06:02.573Z

Event pkey: 3 | name: Gamma | start: 2018-11-23 16:30:00.0 | stop: 2018-12-23 16:30:00.0

Java 8 without ThreeTen-Backport

And here is that same example, conceptually, but in Java 8 or later where we can use the java.time classes built-in without the ThreeTen-Backport library.

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

When run.

instant.toString(): 2018-12-04T05:10:54.635Z

Event pkey: 3 | name: Gamma | start: 2018-11-24T00:30:00Z | stop: 2018-12-24T00:30:00Z


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.