如何JDBC / Postgres的比较时间戳的时区少java.util.Date?

问题描述 投票:5回答:1

我们有一个具有两个Postgres列,prc_sta_dt和prc_end_dt一个TIMESTAMP WITHOUT TIME ZONE表。我们检查看是否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年12月3日上午9点,并在当天下午3点结束的促销活动。在我们的数据库中的prc_sta_dt和prc_end_dt colums这些行是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点做数据库比较之前?如果是的话,我们的运气了!

我怀疑这会发生,但我只是想确认,所以没有惊喜。

java postgresql jdbc time
1个回答
5
投票

Wrong data type, Date is not a 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值,从而歪曲了什么实际存储后注入时区的用心良苦,但不幸的防功能。

Comparing a moment to values of TIMESTAMP WITHOUT TIME ZONE

至于你的类型TIMESTAMP WITHOUT TIME ZONE列片刻值进行比较,这样做会一般没有意义。

但如果你是清醒和深入的认识日期时间处理,使得这种比较在你的业务逻辑是明智的,让我们开拓上。

Wrong classes

您正在使用糟糕的,可怕的,可怕的日期时间类,被取代年前由java.time类(DateSimpleDateFormat等)。请你帮个忙:停止使用旧日期时间类。仅使用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

proper time zone name的格式指定一个continent/region,如America/MontrealAfrica/Casablanca,或Pacific/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() ;

Half-Open

where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt

该逻辑是容易出现故障。一般来说,在日期时间的最佳实践定义一个跨度的时间使用半开,在刚开始是包容性的,而结局是独占时处理。

因此,使用这样的:

WHERE instant >= start_col AND instant < stop_col ;

对于PreparedStatement,我们将有占位符。

WHERE ? >= start_col AND ? < stop_col ;

在Java方面,如JDBC 4.2,我们可以直接通过getObjectsetObject方法交换与数据库java.time对象。

你也许可以通过根据您的JDBC驱动程序的Instant。对于Instant支持不被JDBC规范所要求的。因此,尝试它,或阅读您的驱动程序的文档。

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 ) ;

Always specify time zone

例如,如果服务器在芝加哥运行,然后将它解释上午10 CST上午10点,然后将其转换成UTC下午4点做数据库比较之前?

程序员不应该依赖于时区(或区域设置的方式)当前被设置为在主机操作系统或JVM的默认。两者都在你的控制。而且两者可以同时运行过程中随时改变!

总是通过传递可选参数,以各种日期,时间的方法指定时区。使这些可选的是,在我看来java.time一个设计缺陷,程序员往往忽略了时区的问题,在他们自己的危险。但是,这是一个令人惊讶的有用和优美的框架,很少有设计缺陷之一。

在我们的代码注意上面我们指定的期望/预期的时区。我们的主机操作系统,我们Postgres数据库连接,我们的JVM的当前默认时区不会改变我们的代码的行为。

Current moment

如果你想在当前时刻使用任何这些:

  • Instant.now() 总是UTC,顾名思义。
  • OffsetDateTime.now( someZoneOffset ) 如被看见在一个特定的偏移,从-UTC的挂钟时间当前时刻。
  • ZonedDateTime.now( someZoneId ) 如看到由居住在一个特定区域内的人使用的挂钟时间当前时刻。

Java 7 and ThreeTen-Backport

如果您使用的是Java 7,那么你有内置的无java.time类。幸运的是,JSR 310和java.time的发明者,斯蒂芬Colebourne对,也带动了ThreeTen-Backport项目生产提供了大部分的java.time功能到Java 6和7的库。

这是在一个单一的java文件一个完整的示例应用程序显示在Java 7中使用回港与H2 Database Engine

Java 7JDBC 4.2是不可用的,所以我们不能直接使用现代类。我们退回到使用java.sql.Timestamp这实际上代表了UTC了一下,但H2存储到TIMESTAMP WITHOUT TIME ZONE服用日期和时间的日作为-IS(使用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 |名称:伽玛|开始:2018年11月23日16:30:00.0 |停止:2018年12月23日16:30:00.0

Java 8 without ThreeTen-Backport

这里是相同的例子,在概念上,但在Java 8或更高版本,我们可以使用内置java.time类没有ThreeTen-反向移植库。

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 |名称:伽玛|开始:2018-11-24T00:30:00Z |停车:2018-12-24T00:30:00Z


About java.time

java.time框架是建立在Java 8和更高版本。这些类取代的麻烦老legacy日期时间类,如java.util.DateCalendar,与SimpleDateFormat

该项目Joda-Time,现在在maintenance mode,建议迁移到java.time类。

要了解更多信息,请参见Oracle Tutorial。和搜索堆栈溢出了很多例子和解释。规格是JSR 310

您可以直接与数据库交换java.time对象。使用符合JDBC driver或更高版本的JDBC 4.2。无需串,没有必要java.sql.*类。

从哪里获取java.time类?

该项目ThreeTen-Extra与java.time其他类延伸。该项目是为将来可能增加的java.time试验场。您可以在这里找到一些有用的类,如IntervalYearWeekYearQuartermore

© www.soinside.com 2019 - 2024. All rights reserved.