До настоящего момента наше обсуждение глобализации было сосредоточено исключительно на строках. Тем не менее проблемы с датой и временем порой создают в ходе локализации ничуть не меньше проблем. Пользователи, работающие с вашей базой данных и веб-сервером, могут находиться на другом конце света, но им все равно нужна точная информация, относящаяся к их часовому поясу, а дата и время должны иметь знакомую структуру.
Основные проблемы, усложняющие работу с датой и временем:
- В мире существует много разных часовых поясов.
- В одних регионах действует летнее время, в других его нет.
- В некоторых локальных контекстах используются разные календари.
- В мире не существует общепринятых правил форматирования даты/времени.
Типы данных временных меток
До выхода Oracle9i
работа с датой и временем была достаточно тривиальной. В распоряжении разработчика был тип DATE
и функция TO_DATE
(читайте эту и эту статью). Из-за ограниченных возможностей типа DATE
разработка глобальных приложений становилась делом утомительным и рутинным. Введение любых поправок на часовой пояс требовало ручных вычислений. К сожалению, если ваше приложение должно работать в Oracle8i
и более ранних версиях, этот вариант остается единственным.
А в распоряжении пользователей Oracle9i
и последующих версий оказываются все выдающиеся возможности типов данных TIMESTAMP
и INTERVAL
. Если вы еще не читали эти статьи, я приведу краткую сводку, но рекомендую вернуться и прочитать ее для более подробного понимания темы.
Следующий пример демонстрирует типы данных timestamp
, timestamp
with
time zone
и timestamp with local time zone
в действии:
DECLARE
v_date_timestamp TIMESTAMP ( 3 ) := SYSDATE;
v_date_timestamp_tz TIMESTAMP ( 3 ) WITH TIME ZONE := SYSDATE;
v_date_timestamp_ltz TIMESTAMP ( 3 ) WITH LOCAL TIME ZONE := SYSDATE;
BEGIN
DBMS_OUTPUT.put_line ('TIMESTAMP: ' || v_date_timestamp);
DBMS_OUTPUT.put_line ('TIMESTAMP WITH TIME ZONE: ' || v_date_timestamp_tz);
DBMS_OUTPUT.put_line ( 'TIMESTAMP WITH LOCAL TIME ZONE: '
|| v_date_timestamp_ltz
);
END;
Пример возвращает следующие результаты:
TIMESTAMP: 08-JAN-05 07.28.39.000 PM
TIMESTAMP WITH TIME ZONE: 08-JAN-05 07.28.39.000 PM −07:00
TIMESTAMP WITH LOCAL TIME ZONE: 08-JAN-05 07.28.39.000 PM
Данные timezone
и timezone with local timestamp
идентичны, потому что время базы данных принадлежит тому же локальному контексту, что и время сеанса. Значение TIMESTAMP
WITH
TIMEZONE
показывает, что пользователь находится в часовом поясе Монтаны. Если бы подключение к базе данных в Колорадо осуществлялось из Калифорнии, то результат был бы немного другим:
TIMESTAMP: 08-JAN-05 07.28.39.000 PM
TIMESTAMP WITH TIME ZONE: 08-JAN-05 07.28.39.000 PM −07:00
TIMESTAMP WITH LOCAL TIME ZONE: 08-JAN-05 06.28.39.000 PM
Значение timestamp with local timezone
использовало часовой пояс сеанса (смещение -08:00), а значение было автоматически преобразовано.
Форматирование даты и времени
Одна из проблем локализации связана с форматированием даты и времени. Например, в Японии принят формат yyyy/MM/dd hh:mi:ssxff AM
, тогда как в США распространен формат dd-MON-yyyy hh:mi:ssxff AM
.
Подобные проблемы обычно решаются включением списка форматных масок в таблицу локальных контекстов. При подключении пользователя по его локальному контексту устанавливается правильный формат даты/времени для его региона.
В схему glln
включены таблицы USERS
и LOCALE
, объединяемые по значению locale_id
. Рассмотрим несколько примеров использования функций даты/времени (см. главу 10) и форматных масок из таблицы glln.locale
.
Столбец registration_date
в таблице относится к типу данных TIMESTAMP
WITH
TIME
ZONE
. Функция TO_CHAR
и передача маски для локального контекста каждого пользователя обеспечивают отображение данных в правильном формате.
FUNCTION date_format_func
RETURN sys_refcursor
IS
v_date sys_refcursor;
BEGIN
OPEN v_date
FOR
SELECT locale.locale_desc "Locale Description",
TO_CHAR (users.registration_date,
locale.DATE_FORMAT
) "Registration Date"
FROM users, locale
WHERE users.locale_id = locale.locale_id;
RETURN v_date;
END date_format_func;
Выполнение функции:
variable v_format refcursor
CALL date_format_func() INTO :v_format;
PRINT v_format
Результат выглядит так:
Locale Description Registration Date
----------------------- ------------------
English 01-JAN-2005 11:34:21.000000 AM US/MOUNTAIN
Japanese 2005/01/01 11:34:21.000000 AM JAPAN
German 01 January 05 11:34:21.000000 AM EUROPE/WARSAW
В каждом из трех локальных контекстов назначена своя маска формата даты. Этот метод позволяет каждому пользователю работать с датой в формате, соответствующем локальному контексту из его профиля. Если теперь добавить условие NLS_DATE_LANGUAGE
, то при выводе даты и времени будет использоваться соответствующий язык из локального контекста. Давайте проверим правильность вывода на наших таблицах:
FUNCTION date_format_lang_func
RETURN sys_refcursor
IS
v_date sys_refcursor;
BEGIN
OPEN v_date
FOR
SELECT locale.locale_desc "Locale Description",
TO_CHAR (users.registration_date,
locale.DATE_FORMAT,
'NLS_DATE_LANGUAGE= ' || locale_desc
) "Registration Date"
FROM users, locale
WHERE users.locale_id = locale.locale_id;
RETURN v_date;
END date_format_lang_func;
При выполнении функции:
variable v_format refcursor
CALL date_format_lang_func() INTO :v_format;
PRINT v_format
мы получаем следующий результат:
Locale Description Registration Date
----------------------- ------------------
English 01-JAN-2005 11:34:21.000000 AM US/MOUNTAIN
Japanese 2005/01/01 11:34:21.000000 午前 JAPAN
German 01 Januar 05 11:34:21.000000 AM EUROPE/WARSAW
Данные в таблице USERS
не изменились, но время выводится в формате конкретного локального контекста. Значение NLS_DATE_LANGUAGE
адаптируется для каждой территории, так что для японского локального контекста обозначение полудня (AM
) выводится на японском, а название месяца в немецком локальном контексте выводится на немецком. Функцию можно усовершенствовать так, чтобы в выходных данных учитывался часовой пояс сеанса, — для этого значение преобразуется либо к типу данных TIMESTAMP
WITH TIME ZONE
, либо к локальному часовому поясу сеанса TIMESTAMP WITH LOCAL TIME ZONE
. Для этого мы воспользуемся функцией CAST
(см. главу 7), изменяющей тип данных значения, хранящегося в таблице:
FUNCTION date_ltz_lang_func
RETURN sys_refcursor
IS
v_date sys_refcursor;
BEGIN
OPEN v_date
FOR
SELECT locale.locale_desc,
TO_CHAR
(CAST
(users.registration_date AS TIMESTAMP WITH LOCAL TIME ZONE
),
locale.DATE_FORMAT,
'NLS_DATE_LANGUAGE= ' || locale_desc
) "Registration Date"
FROM users, locale
WHERE users.locale_id = locale.locale_id;
RETURN v_date;
END date_ltz_lang_func;
Выполнение функции:
variable v_format refcursor
CALL date_ltz_lang_func() INTO :v_format;
PRINT v_format
Данные выводятся в следующем формате:
Locale Description Registration Date
----------------------- ------------------
English 01-JAN-2005 11:34:21.000000 AM −07:00
Japanese 2004/12/31 07:34:21.000000 午後 −07:00
German 01 Januar 05 03:34:21.000000 AM −07:00
Обратите внимание на следующие моменты:
- Язык даты/времени преобразуется к локальному контексту.
- Форматирование привязано к локальному контексту.
- Я использую
CAST
для преобразования значений, хранящихся в форматеTIMESTAMP WITH TIMEZONE
, вTIMESTAMP WITH LOCAL TIMEZONE
. - Время выводится относительно часового пояса сеанса (-07:00 в нашем примере).
Во многих примерах, приводившихся ранее, часовой пояс выводился в виде смещения UTC
. Такой формат не всегда оказывается самым понятным для пользователя. Oracle
ведет список названий регионов и сокращений, которые можно подставлять с изменением маски форматирования. Я вставил три записи, используя имена регионов вместо смещения UTC
. Чтобы получить полный список часовых поясов, обратитесь с запросом к представлению V$TIMEZONE_NAMES
. Другие примеры использования имен регионов встречаются в командах INSERT
для таблицы USERS
схемы g11n
.
Необходимо упомянуть еще один параметр NLS
, относящийся к дате/времени. При сохранении времени в таблице из нашего примера используется григорианский календарь, определяемый значением параметра NLS_CALENDAR
. Однако не все локальные контексты используют тот же календарь, и смена базового календаря не компенсируется никаким форматированием. Параметр NLS_CALENDAR
позволяет переключиться с григорианского календаря на другие стандартные календари — скажем, на японский имперский календарь:
ALTER SESSION SET NLS_CALENDAR = 'JAPANESE IMPERIAL';
ALTER SESSION SET NLS_DATE_FORMAT = 'E RR-MM-DD';
После изменения конфигурации сеанса выполняется следующая команда SELECT
:
SELECT sysdate
FROM dual;
Она выводит измененное значение SYSDATE
:
SYSDATE
---------
H 17-02-08
Значения по умолчанию определяются настройками NLS
. Если в системе имеется основной локальный контекст, с которым вы работаете, то задать параметры NLS
для базы данных намного проще, чем явно указывать их в приложении. С другой стороны, в приложениях, в которых настройки NLS
должны изменяться динамически, рекомендуется включить их в состав настроек пользователя/локального контекста и сохранить их в приложении.