Как JDBC / Postgres сравнивает java.util.Date без часового пояса с меткой времени? - PullRequest
0 голосов
/ 03 декабря 2018

У нас есть таблица 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 у нас есть рекламные акции, которые начинаются в 9 утра 3/3/2018 и заканчиваются в 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:00 (2018-12-03T10: 00: 00) или будет относиться к этому времени как к часовому поясу, на котором работает сервер, и затем преобразовать его в UTC?

Например, если сервер работает в Чикаго, то будет ли он интерпретировать 10:00 как 10:00 CST, а затем преобразовать его в 4:00 UTC перед выполнением сравнения в базе данных?Если так, то нам не повезло!

Я сомневаюсь, что это произойдет, но я просто хочу убедиться, что никаких сюрпризов.

1 Ответ

0 голосов
/ 04 декабря 2018

Неверный тип данных, Date не является датой

Объект java.util.Date представляет момент в UTC, конкретную точку на временной шкале.Таким образом, он представляет собой комбинацию даты, времени дня и нулевого смещения от UTC (для самого UTC).Среди многих неудачных вариантов дизайна в этом ужасном классе есть его вводящее в заблуждение имя, которое сбило с толку бесчисленное количество Java-программистов.

TIMESTAMP WITHOUT TIME ZONE

Если вас волнуют моменты, тогда столбец вашей базы данных должен не быть типа TIMESTAMP WITHOUT TIME ZONE.Этот тип данных представляет дату и время суток без какой-либо концепции часового пояса или смещения от UTC.Таким образом, по определению, этот тип не может представлять момент, является не точкой на временной шкале.Этот тип следует использовать только в том случае, если вы имеете в виду дату со временем везде или везде .

Примеры:

  • «Рождество начинается после инсульта в полночь в начале 25 декабря 2018 года», где Рождество в Кирибати наступает первым, позже - в Индии и Африкедаже позже.
  • «Меморандум всей компании: каждый из наших заводов в Дели, Дюссельдорфе и Детройте будет закрыт на один час рано в 16:00 21 января», где 16:00 на каждом заводе - это три разных момента, каждый по несколько часовКроме.

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.

Укажите часовой пояс, в котором вы хотите настроить Instant момент в UTC для сравнения.Например, если ваша база данных была создана кем-то, кто не понимал правильную обработку даты и времени, и использовал столбец TIMESTAMP WITHOUT TIME ZONE для хранения значений даты и времени, которые были взяты из настенного времени Квебека, тоиспользуйте часовой пояс America/Montreal.

Укажите правильное имя часового пояса в формате continent/region, например 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, та же точка на временной шкале, но рассматриваемая с другим временем настенных часов.

Чтобы вбить квадратный колышек в круглое отверстие , давайте преобразуем этот ZonedDateTime объект вLocalDateTime объекта, тем самым удаляя информацию о часовом поясе и оставляя только значение даты-времени-дня.

LocalDateTime ldt = zdt.toLocalDateTime() ;

Half-Open

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

Эта логикасклонен к провалу.Как правило, лучшая практика в обработке даты и времени при определении промежутка времени для использования Half-Open, где начало включительно , а окончание эксклюзивно .

Так что используйте это:

WHERE instant >= start_col AND instant < stop_col ;

ДляPreparedStatement, у нас будут заполнители.

WHERE ? >= start_col AND ? < stop_col ;

На стороне Java, начиная с JDBC 4.2, мы можем напрямую обмениваться объектами java.time с базой данных через getObject иsetObject методы.

Вы можете быть в состоянии передать Instant в зависимости от вашего драйвера JDBC.Поддержка Instant не требуется в спецификации JDBC.Попробуйте или прочтите документацию для своего драйвера.

myPreparedStatement.setObject( 1 , instant ) ;
myPreparedStatement.setObject( 2 , instant ) ;

Если Instant не поддерживается, преобразуйте из Instant в OffsetDateTime значение UTC.Поддержка OffsetDateTime - , требуемая спецификацией.

myPreparedStatement.setObject( 1 , instant.atOffset( ZoneOffset.UTC ) ) ;
myPreparedStatement.setObject( 2 , instant.atOffset( ZoneOffset.UTC ) ) ;

Извлечение.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

Всегда указывать часовой пояс

Например, если сервер работает в Чикаго, то будет ли он интерпретировать 10:00 как 10:00 CST, а затем преобразовать его в 4:00 UTC перед выполнением сравнения в базе данных?

Программист должен никогда зависеть от часового пояса (или, кстати, локали), который в настоящее время установлен по умолчанию в хост-ОС или 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 Стивен Коулборн также руководил проектом ThreeTen-Backport для создания библиотекипредоставление большинства функций java.time для Java 6 и 7.

Вот полный пример приложения в одном файле .java, показывающий использование back-port в 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

Пиктограмма события: 3 |имя: Гамма |начало: 2018-11-23 16: 30: 00.0 |останов: 2018-12-23 16: 30: 00.0

Java 8 без ThreeTen-Backport

И вот тот же пример, концептуально, но вJava 8 или более поздней версии, где мы можем использовать классы java.time , встроенные без библиотеки ThreeTen-Backport .

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:00 |остановка: 2018-12-24T00: 30: 00Z


О java.time

java.time *Фреймворк 1244 * встроен в Java 8 и более поздние версии.Эти классы вытесняют старые классные устаревшие классы даты и времени, такие как java.util.Date, Calendar и & SimpleDateFormat.

Проект Joda-Time , теперь в режиме обслуживания , рекомендует выполнить переход на классы java.time .

Чтобы узнать больше, см. Oracle Tutorial .И поиск переполнения стека для многих примеров и объяснений.Спецификация: JSR 310 .

Вы можете обмениваться java.time объектами непосредственно с вашей базой данных.Используйте драйвер JDBC , совместимый с JDBC 4.2 или более поздней версии.Нет необходимости в строках, нет необходимости в java.sql.* классах.

Где получить классы java.time?

  • Java SE 8 , Java SE 9 , Java SE 10, Java SE 11 и более поздние версии - часть стандартного Java API с связанной реализацией.
    • Java 9 добавляет некоторые незначительные функции и исправления.
  • Java SE 6 и JavaSE 7
    • Большинство функций java.time перенесены в Java 6 & 7 в ThreeTen-Backport .
  • Android
    • Более поздние версии реализации связки Android java.time классы.
    • Для более ранних версий Android (<26) проект <a href="https://github.com/JakeWharton/ThreeTenABP" rel="nofollow noreferrer"> ThreeTenABP адаптируется ThreeTen-Backport (упомянуто выше).См. Как использовать ThreeTenABP… .

ThreeTen-Extra Проект расширяет java.time дополнительными классами.Этот проект является полигоном для возможных будущих дополнений к java.time.Здесь вы можете найти несколько полезных классов, таких как Interval, YearWeek, YearQuarter и more .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...