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