Статьи

Что еще сложнее, чем даты и часовые пояса?

Недавно в списке рассылки jOOQ состоялась интересная дискуссия о том, что на данный момент jOOQ не имеет встроенной поддержки типов данных TIMESTAMP WITH TIME ZONE .

Никто не сказал, что дата, время и часовые пояса просты! Здесь есть забавная часть, которую я рекомендую прочитать: программисты Falsehoods верят во время

А когда этого недостаточно, читайте также: больше программистов лжи верят во время

Мне лично нравится то, что программисты ошибочно полагают, что «время Unix — это количество секунд с 1 января 1970 года» … время Unix не может представить високосные секунды;)

Вернуться к JDBC

Вот интересный ответ Stack Overflow от Mark Rotteveel , разработчика Jaybird (драйвер JDBC для Firebird): является ли java.sql.Timestamp часовым поясом?

Объяснение Марка можно наблюдать следующим образом (я использую PostgreSQL здесь):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Connection c = getConnection();
Calendar utc = Calendar.getInstance(
    TimeZone.getTimeZone("UTC"));
 
try (PreparedStatement ps = c.prepareStatement(
    "select"
  + "  ?::timestamp,"
  + "  ?::timestamp,"
  + "  ?::timestamp with time zone,"
  + "  ?::timestamp with time zone"
)) {
 
    ps.setTimestamp(1, new Timestamp(0));
    ps.setTimestamp(2, new Timestamp(0), utc);
    ps.setTimestamp(3, new Timestamp(0));
    ps.setTimestamp(4, new Timestamp(0), utc);
 
    try (ResultSet rs = ps.executeQuery()) {
        rs.next();
 
        System.out.println(rs.getTimestamp(1)
                 + " / " + rs.getTimestamp(1).getTime());
        System.out.println(rs.getTimestamp(2, utc)
                 + " / " + rs.getTimestamp(2, utc).getTime());
        System.out.println(rs.getTimestamp(3)
                 + " / " + rs.getTimestamp(3).getTime());
        System.out.println(rs.getTimestamp(4, utc)
                 + " / " + rs.getTimestamp(4, utc).getTime());
    }
}

Вышеупомянутая программа использует все варианты использования часовых поясов и не использует часовые пояса в Java и в БД, и выходные данные всегда одинаковы:

1
2
3
4
1970-01-01 01:00:00.0 / 0
1970-01-01 01:00:00.0 / 0
1970-01-01 01:00:00.0 / 0
1970-01-01 01:00:00.0 / 0

Как видите, в каждом случае метка времени UTC 0 была правильно сохранена и извлечена из базы данных. Моя собственная локаль — Швейцария, то есть CET / CEST, которая была UTC + 1 в эпоху, что и является результатом вывода Timestamp.toString() .

Все становится интересным, когда вы используете литералы меток времени, как в SQL, так и / или в Java. Если вы замените переменные связывания такими:

1
2
3
4
5
6
Timestamp almostEpoch = Timestamp.valueOf("1970-01-01 00:00:00");
 
ps.setTimestamp(1, almostEpoch);
ps.setTimestamp(2, almostEpoch, utc);
ps.setTimestamp(3, almostEpoch);
ps.setTimestamp(4, almostEpoch, utc);

Это то, что я получаю на своей машине, снова в CET / CEST

1
2
3
4
1970-01-01 00:00:00.0 / -3600000
1970-01-01 00:00:00.0 / -3600000
1970-01-01 00:00:00.0 / -3600000
1970-01-01 00:00:00.0 / -3600000

Т.е. не эпоха, а литерал метки времени, который я отправил на сервер. Заметьте, что четыре комбинации привязки / выборки всегда производят одну и ту же метку времени.

Давайте посмотрим, что произойдет, если при записи сеанса в базу данных используется другой часовой пояс (предположим, вы находитесь в PST), чем при извлечении сеанса из базы данных (я снова использую CET или UTC). Я запускаю эту программу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Calendar utc = Calendar.getInstance(
    TimeZone.getTimeZone("UTC"));
 
Calendar pst = Calendar.getInstance(
    TimeZone.getTimeZone("PST"));
 
try (PreparedStatement ps = c.prepareStatement(
    "select"
  + "  ?::timestamp,"
  + "  ?::timestamp,"
  + "  ?::timestamp with time zone,"
  + "  ?::timestamp with time zone"
)) {
 
    ps.setTimestamp(1, new Timestamp(0), pst);
    ps.setTimestamp(2, new Timestamp(0), pst);
    ps.setTimestamp(3, new Timestamp(0), pst);
    ps.setTimestamp(4, new Timestamp(0), pst);
 
    try (ResultSet rs = ps.executeQuery()) {
        rs.next();
 
        System.out.println(rs.getTimestamp(1)
                 + " / " + rs.getTimestamp(1).getTime());
        System.out.println(rs.getTimestamp(2, utc)
                 + " / " + rs.getTimestamp(2, utc).getTime());
        System.out.println(rs.getTimestamp(3)
                 + " / " + rs.getTimestamp(3).getTime());
        System.out.println(rs.getTimestamp(4, utc)
                 + " / " + rs.getTimestamp(4, utc).getTime());
    }
}

Это дает такой вывод:

1
2
3
4
1969-12-31 16:00:00.0 / -32400000
1969-12-31 17:00:00.0 / -28800000
1970-01-01 01:00:00.0 / 0
1970-01-01 01:00:00.0 / 0

Первой меткой времени была Epoch, сохраненная как PST (16:00), затем информация о часовом поясе была удалена базой данных, которая превратила Epoch в местное время, которое вы имели в Epoch (-28800 секунд / -8h), и эта информация действительно хранится

Теперь, когда я выбираю это время из CET своего часового пояса, я все еще хочу узнать местное время (16:00). Но в моем часовом поясе это уже не -28800 секунд, а -32400 секунд (-9ч). Причудливый достаточно?

Дела идут наоборот, когда я извлекаю сохраненное местное время (16:00), но я заставляю извлечение происходить в UTC, что приведет к полученной вами отметке времени, первоначально в PST (-28800) секунд). Но при печати этой метки времени (-28800 секунд) в моем CET часового пояса это будет 17:00.

Когда мы используем тип данных TIMESTAMP WITH TIME ZONE в базе данных, часовой пояс поддерживается (PST), и когда я получаю значение Timestamp, независимо от того, используется ли CET или UTC, я все равно получаю Epoch, которая была безопасно сохранена в база данных, распечатанная как 01:00 в CET.

Уф.

TL; DR:

При использовании jOOQ , если для вас важна правильная временная метка UTC, используйте TIMESTAMP WITH TIMEZONE, но вам придется реализовать свой собственный тип данных Binding , потому что jOOQ в настоящее время не поддерживает этот тип данных. Как только вы используете свой собственный тип данных Binding, вы также можете использовать API времени Java 8, который лучше представляет эти различные типы, чем java.sql.Timestamp + уродливый Календарь.

Если для вас имеет значение местное время или вы не работаете в разных часовых поясах, вы можете использовать TIMESTAMP и поле <Timestamp> jOOQ.

К счастью, если вы похожи на меня, вы работаете в очень маленькой стране с одним часовым поясом, где большинство локальных программ просто не сталкиваются с этой проблемой.