Oracle/JDBC: получение значения TIMESTAMP WITH TIME ZONE в формате ISO 8601

Многое было сказано (и написано на SO) на части предмета, но не на всеобъемлющем, полном пути, поэтому у нас может быть одно "окончательное, охватывающее все" решение для всех, кто будет использовать.

У меня есть БД Oracle, где я храню дату + время + часовой пояс глобальных событий, поэтому оригинальная TZ должна быть сохранена и доставлена ​​на клиентскую сторону по запросу. В идеале, он может хорошо работать с использованием стандартного формата ISO 8601 "T", который может быть хорошо сохранен в Oracle с использованием типа столбца "TIMESTAMP WITH TIME ZONE" ( "TSTZ" ).

Что-то вроде '2013-01-02T03: 04: 05.060708 + 09: 00'

Все, что мне нужно сделать, это получить вышеуказанное значение из БД и отправить его клиенту без каких-либо манипуляций.

Проблема в том, что Java не имеет поддержки ISO 8601 (или любого другого типа даты + времени + nano + tz), и ситуация еще хуже, потому что драйвер JDBC Oracle (ojdbc6.jar) имеет еще меньшую поддержку TSTZ ( в отличие от Oracle DB, где он хорошо поддерживается).

В частности, здесь я не должен или не могу делать:

  • Любое отображение из TSTZ в java Date, Time, Timestamp (например, через вызовы JDBC getTimestamp()) не будет работать, потому что я теряю TZ.
  • Драйвер Oracle JDBC не предоставляет какой-либо метод для сопоставления TSTZ с java-объектом Calendar (это может быть решение, но его там нет)
  • JDBC getString() может работать, но драйвер Oracle JDBC возвращает строку в формате "2013-01-02 03: 04: 05.060708 +9: 00", которая не соответствует стандарту ISO 8601 (нет "T" ), отсутствие заднего 0 в TZ и т.д.). Более того, этот формат жестко закодирован (!) Внутри реализации драйвера JDBC Oracle, который также игнорирует настройки локали JVM и настройки форматирования сеанса Oracle (т.е. Игнорирует переменную сеанса NLS_TIMESTAMP_TZ_FORMAT).
  • JDBC getObject() или getTIMESTAMPTZ() возвращают объект Oracle TIMESTAMPTZ, который практически бесполезен, поскольку он не имеет никакого преобразования в Календарь (только дата, время и временная метка), поэтому снова мы теряем информацию TZ.

Итак, вот варианты, с которыми мне осталось:

  • Используйте JDBC getString() и стройте-манипулируйте им, чтобы исправить и сделать ISO 8601 совместимым. Это легко сделать, но есть опасность умереть, если Oracle изменит внутреннее жестко закодированное getString() форматирование. Кроме того, если посмотреть на исходный код getString(), похоже, что использование getString() также приведет к некоторому снижению производительности.

  • Используйте Oracle DB toString для преобразования: "SELECT TO_CHAR (tstz...) EVENT_TIME...". Это прекрасно работает, но имеет 2 основных недостатка:

    • Каждый SELECT теперь должен включать TO_CHAR-вызов, который является головной болью для запоминания и записи
    • Каждый SELECT теперь должен добавить столбец EVENT_TIME "псевдоним" (необходимо, например, для сериализации результата для Json автоматически)
      .
  • Используйте класс Java TIMESTAMPTZ Java и извлеките соответствующее значение вручную из его внутренней (документированной) структуры массива байтов (т.е. реализуйте мой собственный метод toString(), который Oracle забыл реализовать там). Это опасно, если Oracle изменяет внутреннюю структуру (маловероятно) и требует относительно сложной функции для ее реализации и поддержки.

  • Я надеюсь, что там 4-й, отличный вариант, но из просмотра по всему Интернету и SO - я не вижу ничего.

Идеи? Мнения?

UPDATE

Много идей было дано ниже, но похоже, что нет правильного способа сделать это. Лично я считаю, что использование метода # 1 является самым коротким и наиболее читаемым способом (и поддерживает достойную производительность, не теряя субмиллисекунд или SQL-запросы).

Вот что я в конечном итоге решил использовать:

String iso = rs.getString(col).replaceFirst(" ", "T");

Спасибо за хорошие ответы всем,
Б.

Ответ 1

Поскольку, похоже, нет волшебного способа сделать это правильно, самым простым и самым коротким методом будет # 1. В частности, это все необходимое код:

// convert Oracle hard-coded: '2013-01-02 03:04:05.060708 +9:00'
// to properly formatted ISO 8601: '2013-01-02T03:04:05.060708 +9:00'
String iso = rs.getString(col).replaceFirst(" ", "T"); 

кажется, что просто добавить "Т" достаточно, хотя перфекционист, вероятно, добавит больше косметики (конечно, регулярное выражение может быть оптимизировано), например: rs.getString(col).replaceFirst( "," T"). replaceAll ( "," ").replaceFirst(" \ + ([0-9]) \: "," + 0 $1:");

В.

Ответ 2

Небольшое улучшение до # 2:

CREATE OR REPLACE PACKAGE FORMAT AS
  FUNCTION TZ(T TIMESTAMP WITH TIME ZONE) RETURN VARCHAR2;
END;
/
CREATE OR REPLACE PACKAGE BODY FORMAT AS
  FUNCTION TZ(T TIMESTAMP WITH TIME ZONE) RETURN VARCHAR2
  AS
  BEGIN
    RETURN TO_CHAR(T,'YYYYMMDD"T"HH24:MI:SS.FFTZHTZM');
  END;
END;
/

В SQL это будет:

SELECT FORMAT.TZ(tstz) EVENT_TIME ...

Это более читаемо. Если вам когда-либо понадобится изменить его, это 1 место.
Недостатком является дополнительный вызов функции.

Ответ 3

JDBC getObject() или getTIMESTAMPTZ() возвращают объект Oracle TIMESTAMPTZ, который практически бесполезен, поскольку он не имеет никакого преобразования в Календарь (только дата, время и временная метка), поэтому снова мы теряем информацию TZ.

Это была бы моя рекомендация как единственный надежный способ получить информацию, которую вы ищете.

Если вы находитесь на Java SE 8 и имеете ojdbc8, вы можете использовать getObject(int, OffsetDateTime.class). Помните, что при использовании getObject(int, ZonedDateTime.class) на вас может повлиять ошибка 25792016.

Использовать Java TIMESTAMPTZ класс Java и извлекать соответствующее значение вручную из его внутренней (документированной) структуры массива байтов (т.е. реализовать собственный метод toString(), который Oracle забыл реализовать там). Это рискованно, если Oracle изменяет внутреннюю структуру (маловероятно) и требует относительно сложной функции для реализации и поддержки.

Это то, что мы в конечном итоге пошли с до тех пор, пока в драйвере Oracle JDBC не будет доступна поддержка JSR-310 без ошибок. Мы определили, что это единственный надежный способ получить нужную нам информацию.

Ответ 4

вам нужны два значения: time utc в миллисе с 1970 года и смещение временной зоны fom utc.
Поэтому сохраните их как пару и переместите их как пару.

class DateWithTimeZone {
long timestampUtcMillis;
// offset in seconds
int tzOffsetUtcSec;
}

A Дата - это пара чисел. Это не строка. Поэтому интерфейс машины не должен содержать дату, представленную строкой iso, хотя это удобно для отладки. Если даже java не может разобрать эту дату, как вы думаете, что могут делать ваши клиенты?

Если вы создаете интерфейс для своих клиентов, подумайте, как они могут его разобрать. И заранее напишите код, который показывает это.

Ответ 5

Это непроверено, но похоже, что это должен быть работоспособный подход. Я не уверен в разборе имени TZ, но просто рассматриваю две части объекта TZTZ, поскольку отдельные входы в Календарь, похоже, должны были идти.

Я не уверен, вернет ли longValue() значение в local или GMT/UCT. Если это не GMT, вы должны иметь возможность загружать календарь в формате UTC и запрашивать его для календаря, преобразованного в локальный TZ.

public Calendar toCalendar(oracle.sql.TIMESTAMPTZ myOracleTime) throws SQLException {
    byte[] bytes = myOracleTime.getBytes();
    String tzId = "GMT" + ArrayUtils.subarray(bytes, ArrayUtils.lastIndexOf(bytes, (byte) ' '), bytes.length);
    TimeZone tz = TimeZone.getTimeZone(tzId);
    Calendar cal = Calendar.getInstance(tz);
    cal.setTimeInMillis(myOracleTime.longValue());
    return cal;
}

Ответ 6

Вы действительно заботитесь о субмиллисекундной точности? Если не конвертировать из UTC миллисекунды + временное смещение в нужную строку, это однострочный с использованием joda-time:

    int offsetMillis = rs.getInt(1);
    Date date = rs.getTimestamp(2);

    String iso8601String =
            ISODateTimeFormat
                    .dateTime()
                    .withZone(DateTimeZone.forOffsetMillis(offsetMillis))
                    .print(date.getTime());

Печать, например (текущее время в +9: 00):

2013-07-18T13:05:36.551+09:00

Что касается базы данных: два столбца, один для смещения, один для даты. Столбец даты может быть фактическим типом даты (таким образом, в любом случае, многие независимые от часового пояса, доступны функции даты db). Для запросов, зависящих от часовой пояс (например, упомянутая глобальная ежечасная гистограмма), возможно, представление может отображать столбцы: local_hour_of_day, local_minute_of_hour и т.д.

Вероятно, это нужно сделать, если не существует типа данных TSTZ, что, учитывая низкую поддержку Oralce, практически подходит для практических целей. Кто хочет использовать специальные функции Oracle в любом случае!: -)

Ответ 7

Решение с оракулом - SELECT SYSTIMESTAMP FROM DUAL