Oracle для профессионалов Том Кайт DiaSoft 2003 торгово-издательский дом Москва Х Санкт-Петербург Х Киев УДК 681.3. 06(075) Б Б К 32.973.2 К 91 КАЙТ ТОМ К 91 Oracle для профессионалов. Пер. с ...
-- [ Страница 23 ] --Пакет UTL_FILE Стандартный пакет UTL_FILE позволяет читать и создавать текстовые файлы в файловой системе сервера в среде PL/SQL. Здесь существенны следующие ключевые слова. Х Текстовые файлы. Пакет UTL_FILE позволяет читать и создавать простые текстовые файлы. В частности, его нельзя использовать для чтения или создания двоичных файлов. Специальные символы, содержащиеся в двоичных данных, приводят к некорректной работе пакета UTL_FILE. Х В файловой системе сервера. Пакет UTL_FILE позволяет читать и записывать файлы только в файловой системе сервера баз данных. Он не позволяет читать или записывать в файловую систему компьютера, на котором работает клиент, если последний не подключен локально к серверу. Пакет UTL_FILE подходит для создания отчетов и сброса данных из базы в текстовые файлы, а также для чтения и загрузки данных. В главе 9 первой части книги представлен полный пример использования пакета UTL_FILE для создания текстового файла в формате, упрощающем загрузку. Пакет UTL_FILE также помогает при отладке. В главе 21, посвященной средствам тщательного контроля доступа, представлен пакет DEBUG. Этот пакет интенсивно использует средства пакета UTL_FILE для записи сообщений в файловую систему. Пакет UTL_FILE Ч очень полезен, но, используя его, нужно помнить об определенных ограничениях. В противном случае результаты могут оказаться некорректными (причем, выявиться это может не при тестировании, а при внедрении) и вызовут разочарование.
Приложение А Я рассмотрю ряд проблем, часто встречающихся при использовании пакета UTL_FILE, в том числе: Х установку параметра инициализации UTL_FILE_DIR;
Х доступ к дискам других компьютеров (сетевым дискам) в среде Windows (проблем с этим в среде Unix нет);
Х обработку исключительных ситуаций, возникающих при использовании пакета UTL_FILE;
Х применение пакета UTL_FILE для периодического пересоздания статических Web-страниц;
Х печально известное ограничение Ч 1023 байта;
Х получение списка файлов каталога, чтобы можно было обработать все находящиеся в нем файлы.
Параметр инициализации UTL_FILE_DIR Это наиболее существенная особенность использования пакета UTL_FILE, который всегда работает от имени пользователя Ч владельца СУБД Oracle, Ч ввод-вывод выполняет выделенный или разделяемый сервер, всегда работающий от имени пользователя oracle. С учетом того, что работа с файлами выполняется от имени пользователя oracle, а пользователь oracle может читать и записывать файлы данных, файлы конфигурации и т.п., Ч не стоит разрешать доступ с помощью средств UTL_FILE ко всем каталогам. В файле параметров инициализации необходимость явно перечислять каталоги, к которым необходим доступ на запись, Ч это средство защиты, а не излишняя сложность. Если бы пакет UTL_FILE позволял записывать файлы в доступных пользователю oracle каталогах, то любой пользователь с помощью функции UTL_FILE.FOPEN смог бы переписать файлы данных системы. Это, конечно же, недопустимо. Поэтому администратор базы данных должен явно открывать доступ к конкретным каталогам. Нельзя разрешить доступ к корневому каталогу поддерева, позволив тем самым обращаться ко всем каталогам этого поддерева, Ч надо явно перечислить все каталоги, в которых предполагается чтение и изменение файлов с помощью пакета UTL_FILE. Следует помнить, что изменять параметр инициализации в процессе работы сервера нельзя. Для добавления или удаления каталога необходимо перезапускать экземпляр. Параметр инициализации UTL_FILE_DIR можно задавать одним из следующих способов: u t l _ f i l e _ d i r = (c:\temp,c:\temp2) или utl_file_dir = c:\temp utl_file_dir = c:\temp Другими словами, можно либо задавать список каталогов через запятую, в круглых скобках, либо перечислять каталоги по одному в последовательных строках. Главное здесь Ч "в последовательных строках". Если файл параметров инициализации (init.ora) завершается следующими строками:
Пакет UTL_FILE utl_file_dir = c:\temp timed_statistics=true utl_file_dir = c:\temp2 учтена будет только последняя запись Ч UTL_FILE_DIR. Первый каталог будет проигнорирован. Это может сбивать с толку, поскольку никаких предупреждающих сообщений или записей в файле alert.log, свидетельствующих об игнорировании записи UTL_FILE_DIR, не появляется. Все записи UTL_FILE_DIR в файле параметров инициализации должны идти подряд. Хочу предупредить об одной особенности использования параметра инициализации на платформе Windows. Если добавить к значению параметра UTL_FILE_DIR завершающую обратную косую черту (\), например, так: utl_file_dir = c:\temp\ utl_file_dir = c:\temp2 при запуске сервера будет выдано следующее сообщение об ошибке: SVRMGR> Startup LRM-00116: syntax error at 'c:\terrputl_file_' following '=' LRM-00113: error when processing file -> 'C:\oracle\admin\tkyte816\pfile\init.ora' ORA-01078: failure in processing system parameters Дело в том, что символ \ является управляющим, если стоит последним в строке файла параметров инициализации. Он позволяет продолжить длинную запись на следующей строке. Чтобы избежать такой конкатенации строк, нужно просто указывать две обратных косых подряд: utl_file_dir = c:\temp\\ utl_file_dir = c:\oracle И последняя особенность этого параметра инициализации. Если в файле параметров инициализации использована завершающая обратная косая черта в имени каталога, необходимо указывать завершающую обратную косую и в вызовах функции fopen. Если обратная косая не указана в файле параметров инициализации, не нужно ее задавать и в вызовах функции fopen. Параметр функции fopen, задающий имя каталога, должен буквально совпадать с одним из значений, заданных в файле параметров инициализации.
Обращение к сетевым дискам в Windows Эта задача часто вызывает затруднения, особенно у тех, кто привык работать в ОС Unix. В ОС Unix смонтированное устройство (например, смонтированный клиентом NFS удаленный диск) немедленно становится доступным всем пользователям, независимо от того, в каком сеансе оно смонтировано. У каждого пользователя могут быть свои права доступа к нему, но смонтированный диск Ч это атрибут системы, а не конкретного сеанса. В среде Windows все не так. На сервере может работать несколько пользовательских сеансов, каждый со своим набором доступных "дисков". Может оказаться, что, зарегистрировавшись, я обнаружу сетевой ресурс Ч диск D:, физически находящийся на дру Приложение А гой машине. Но это не означает, что процесс, работающий на этой машине, сможет увидеть этот диск. Вот тут и возникают проблемы. Многие пользователи регистрируются на сервере и видят диск D:. Они включают в файл параметров конфигурации запись UTL_FILE_dir = d:\reports Ч каталог, в котором предполагается создание отчетов с помощью средств пакета UTL_FILE. При выполнении, однако, они получают сообщение об ошибке: ERROR at line 1: ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: at "SYS.UTL_FILE", line 98 ORA-06512: at "SYS.UTL_FILE", line 157 С помощью обработчика исключительных ситуаций (ниже представлен код, который я обычно использую) это сообщение можно сделать более информативным, например: ERROR at line 1: ORA-20001: INVALID_PATH: File location or filename was invalid. ORA-06512: at "TKYTE.CSV", line 51 ORA-06512: at line 2 Итак, с точки зрения пользователя каталог D:\reports вполне допустим. Его можно найти с помощью программы Проводник (Explorer). Можно открыть окно командной строки DOS и обнаружить в нем этот каталог. Только СУБД Oracle его не видит. Причина в том, что при запуске системы диска D: нет и, более того, учетная запись, от имени которой работает сервер Oracle, по умолчанию вообще не может обращаться к сетевым ресурсам. Пытайтесь сколько угодно, монтируйте диск любым способом Ч сервер Oracle его не увидит. При создании экземпляра Oracle службы, поддерживающие его, настраиваются для регистрации в системе от имени учетной записи SYSTEM (как системные), а эта учетная запись имеет ограниченные привилегии, и домены Windows NT ей недоступны. Чтобы обращаться к другой машине, работающей под управлением Windows NT, служба OracleServiceXXXX должна зарегистрироваться в соответствующем домене Windows NT от имени пользователя, имеющего доступ к диску, который предполагается использовать с помощью пакета UTL_FILE. Чтобы изменить стандартные параметры регистрации для служб Oracle, выберите (в ОС Windows NT): Control Panel | Services | OracleServiceXXXX | Startup | Log On As;
(где XXXX Ч имя экземпляра) B OC Windows 2000 следует выбрать: Control Panel | Administrative Tools | Services | OracleServiceXXXX | Properties | Log On Tab;
(где XXXX Ч имя экземпляра) Выберите переключатель This Account и введите соответствующую информацию о регистрации в домене. После настройки служб для работы от имени пользователя с соответствующими привилегиями, параметр UTL_FILE_DIR можно задать одним из двух способов. Х С помощью сопоставленного диска. Чтобы использовать сопоставленный удаленному ресурсу диск, необходимо, чтобы пользователь, от имени которого запуска Пакет UTL_FILE ется служба, настроил диск, указанный в значении параметра UTL_FILE_DIR, и был зарегистрирован на сервере при использовании пакета UTL_FILE. Х С помощью универсальных соглашений по именованию. Использование универсальных соглашений по именованию (Universal Naming Conventions Ч UNC) предпочтительнее сопоставления дисков, поскольку не требует регистрации пользователя. При этом значение параметра инициализации UTL_FILE_DIR задается в виде \\<имя машины >\<имя общего ресурса >\<путь>>. Естественно, сервер Oracle после изменения свойств сетевой службы необходимо перезапустить.
Обработка исключительных ситуаций При возникновении ошибки пакет UTL_FILE возбуждает исключительную ситуацию. К сожалению, он возбуждает исключительные ситуации, задаваемые пользователем, Ч они определены в спецификации пакета. Если эти исключительные ситуации не перехвачены по имени, выдаются совершенно бесполезные сообщения об ошибках: ERROR at line 1: ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: at "SYS.UTL_FILE", line 98 ORA-06512: at "SYS.UTL_FILE", line 157 О самой ошибке в этом сообщении ничего не сказано. Для решения этой проблемы надо включить обращения к пакету UTL_FILE в блок обработки исключительных ситуаций, где каждая из них перехватывается по имени. Я предпочитаю преобразовывать исключительные ситуации в вызовы процедуры RAISE_APPLICATION_ERROR. Это позволяет задать код ошибки ORA- и выдать более информативное сообщение об ошибке. В предыдущем примере мы использовали этот прием для преобразования представленного выше сообщения об ошибке в следующее:
ORA-20001: INVALID_PATH: File location or filename was invalid.
Это сообщение намного полезнее. Я всегда использую блок обработки исключительных ситуаций следующего вида:* exception when utl_file.invalid_path then raise_application_error(-20001, Ч ошибка в имени файла 'INVALID_PATH: File location or filename was invalid.');
when utl_file.invalid_mode then raise_application_error(-20002, Ч недопустимое значение режима -> открытия файла 'INVALID_MODE: The open_mode parameter in FOPEN was invalid.');
when utl_file.invalid_filehandle then raise_application_error(-20002, Ч недопустимый дескриптор файла 'INVALID_FILEHANDLE: The file handle was invalid.');
* Исходные сообщения об ошибках приводятся на английском, а в строке с кодом ошибки добавлен комментарий на русском языке с описанием причины ее возникновения. - Прим. научн. ред.
Приложение А when utl_file.invalid_operation then raise_application_error(-20003, Ч файл нельзя открыть или -> обработать в указанном режиме 'INVALID_OPERATION: The file could not be opened or operated on as requested.');
when utl_file.read_error then raise_application_error(-20004, Ч в ходе чтения произошла ошибка -> операционной системы 'READ_ERROR: An operating system error occurred during the read operation.');
when utl_file.write_error then raise_application_error(-20005, Ч в ходе записи произошла ошибка -> операционной системы 'WRITE_ERROR: An operating system error occurred during the write operation.');
when utl_file.internal_error then raise_application_error(-20006, Ч неизвестная ошибка в PL/SQL 'INTERNAL_ERROR: An unspecified error in PL/SQL.');
end;
Я записал этот блок в отдельный небольшой файл и добавляю в каждую подпрограмму, использующую пакет UTL_FILE, чтобы автоматически перехватывать и "переименовывать" исключительные ситуации.
Как сбросить Web-страницу на диск?
Этот вопрос задают так часто, что я решил дать ответ здесь. Предполагается, что используется Oracle WebDB, Oracle Portal или другие процедуры на базе средств Web Toolkit (пакетов htp). Хотелось бы не генерировать динамически отчет, который можно получить с помощью этих средств, отдельно для каждого пользователя, а периодически, один раз в X часов или минут, создавать статический файл с отчетом. Именно так я и генерирую начальную страницу на своем сайте. Вместо того чтобы генерировать ее динамически для каждого из тысяч поступающих за этот период обращений, раз в 5 минут я генерирую статическую начальную страницу заново на основе динамических данных. Это существенно сокращает ресурсы, необходимые для поддержки сайта. Такой прием я применяю для популярных динамических станиц, базовые данные которых меняются сравнительно редко. Ниже представлена универсальная процедура, которую я для этого использую: create or replace procedure dump_page(p_dir in varchar2, p_fname in varchar2) is l_thePage htp.htbuf_arr;
l_output utl_file.file_type;
l_lines number default 99999999;
begin l_output := utl_file.fopen(p_dir, p_fname, 'w', 32000);
owa.get_page(l_thePage, l_lines);
for i in 1.. l_lines loop Пакет UTL_FILE utl_file.put(l_output, l_thePage(i));
end loop;
utl_file.fclose (l_output) ;
end dump_page;
/ Все очень просто. Необходимо открыть файл, получить HTML-страницу, выдать каждую ее строку в файл и закрыть его. Если вызвать эту процедуру после WebDB-npoцедуры, она сохранит результат работы WebDB-процедуры в заданном файле. Единственная проблема при использовании этого подхода состоит в том, что WebDBпроцедура выполняется непосредственно, а не через Web. Если в коде WebDB-процедуры используется среда CGI, эта процедура не выполнится, поскольку среда не настроена. Для решения этой проблемы достаточно использовать небольшой фрагмент кода для настройки среды: declare nm owa.vc_arr;
vl owa.vc_arr;
begin nm(l) := 'SERVER_PORT';
vl(l) := '80';
owa.init_cgi_env(nm.count, nm, vl);
Ч здесь выполните необходимую webdb-процедуру dump_page('directory', 'filename');
end;
/ Например, если WebDB-процедура проверяет, запущена ли она с порта 80 на сервере, надо создать для нее соответствующую среду. В этом блоке надо задать и все остальные переменные среды, существенные для приложения. Теперь осталось только перечитать раздел, посвященный пакету DBMS_JOB, и обеспечить периодическое выполнение этого блока кода.
Ограничение длины строки Ч 1023 байт Когда-то у пакета UTL_FILE было ограничение длины записываемой в файл строки: не более 1023 байт. В противном случае возбуждалась исключительная ситуация, и процедура пакета UTL_FILE не выполнялась. К счастью, в версии Oracle 8.0.5 добавлена новая версия функции FOPEN, позволяющая при открытии файла задавать максимальнуюдлину строки вплоть до 32 Кбайт. 32 Кбайт Ч максимальный размер переменной в PL/SQL, и такой длины в большинстве случаев хватает. К сожалению, в документации эта перегруженная версия функции FOPEN описана через несколько страниц после исходной функции. Поэтому многие пользователи об этой возможности и не подозревают. Я по-прежнему получаю много вопросов об этом, хотя сейчас используются версии, начиная с 8.1.7. Пользователя не замечают перегруженную версию функции FOPEN;
они сталкиваются с ограничением и ищут способы обойти его. Ответ простой, но, чтобы найти его, надо прочитать описание всех функций пакета UTL_FILE!
Приложение А Для решения этой проблемы необходимо использовать пакет UTL_FILE так, как это делалось в представленной выше процедуре DUMP_PAGE. Четвертый параметр вызова функции UTL_FILE.FOPEN задает максимальную длину строки текста, которую предполагается использовать. Я допускал использование строк длиной до 32000 байт.
Чтение содержимого каталога Этой возможности в пакете UTL_FILE не хватает. Часто необходимо создать периодически выполняющееся задание для просмотра каталога в поисках новых файлов и обработки этих файлов, например загрузки их содержимого в базу данных. К сожалению, стандартного способа прочитать список файлов в каталоге в языке PL/SQL нет. Можно, однако, реализовать эту возможность с помощью небольшого фрагмента кода на языке Java. Следующий пример демонстрирует, как это сделать. Сначала я создам пользователя с минимальным набором привилегий, необходимых для выполнения действий по загрузке данных и получения списка файлов в каталоге /tmp. Если необходимо читать содержимое других каталогов, придется добавить соответствующие вызовы dbmsJava.grant_permission (подробнее о них см. в главе 19) или заменить /tmp на *, что позволит получить список файлов любого каталога. SQL> connect system/manager system@DEV816> drop user dirlist cascade;
User dropped. system@DEV816> grant create session, create table, create procedure 2 to dirlist identified by dirlist;
Grant succeeded. system@DEV816> begin 2 dbms_java.grant_permission 3 ('DIRLIST', 4 'java.io.FilePermission', 5 '/tmp', 6 'read');
7 end;
8/ PL/SQL procedure successfully completed. Затем, подключившись от имени пользователя DirList, мы создаем глобальную временную таблицу в его схеме (для хранения списка файлов каталога). Так, через временную таблицу, мы сможем получить результаты выполнения хранимой процедуры на языке Java в вызывающей среде. Можно использовать для этого и другие способы (строки, массивы и т.п.). SQL> connect dirlist/dirlist Connected. dirlist@DEV816> create global temporary table DIR_LIST 2 (filename varchar2(255)) 3 on commit delete rows 4/ Table created.
Пакет UTL_FILE Теперь создадим хранимую процедуру на языке Java для получения списка файлов в указанном каталоге. Чтобы упростить программирование, я использую средства препроцессора SQLJ Ч это позволяет избежать написания большого количества вызовов JDBC: dirlist@DEV816> create or replace 2 and compile java source named "DirList" 3 as 4 import java.io.*;
5 import java.sql.*;
6 7 public>
13 String[] list = path.list();
14 String element;
15 16 for (int i = 0;
i < list.length;
i++) 17 { 18 element = list[i];
19 #sql { INSERT INTO DIR_LIST (FILENAME) 20 VALUES (:element) };
21 } 22 } 23 24 } 25 / Java created. Затем необходимо создать процедуру сопоставления, связывающую языки PL/SQL и Java. Она достаточно проста: dirlisteDEV816> create or replace 2 procedure get_dir_list(p_directory in varchar2) 3 as language java 4 name 'DirList.getList(java.lang.String)';
5/ Procedure created.
Теперь можно использовать процедуру get_dir_list:
dirlist@DEV816> exec get_dir_list('\tmp');
PL/SQL procedure successfully completed. dirlist@DEV816> select * from dir_list where rownum < 5;
FILENAME -----------------lost+found.rpc_door ps_data.pcmcia Приложение А Вот и все. В соответствующей временной таблице теперь можно получить список файлов каталога. К данным таблицы можно применять фильтры, например LIKE, или сортировать результаты.
Резюме Пакет UTL_FILE Ч замечательная утилита, которая пригодится во многих приложениях. В этом разделе мы рассмотрели как настроить сервер для использования средств пакета UTL_FILE, и описали особенности его работы. Мы рассмотрели наиболее часто возникающие проблемы при использовании пакета UTL_FILE, в частности обращение к сетевым дискам в среде Windows, ограничение длины строки 1023 байтами, и обработку исключительных ситуаций. Для каждой из этих проблем были представлены решения. Мы также изучили ряд утилит, которые можно создать с помощью пакета UTL_FILE, в частности процедуру UNLOADER, описанную в главе 9, средства чтения списка файлов каталога и сброса Web-страницы на диск.
Пакет UTL_HTTP В этом разделе мы рассмотрим, когда и как использовать пакет UTL_HTTP. Кроме того, я хочу представить новую расширенную версию пакета UTL_HTTP, созданную на основе типа SocketType, который рассматривается в разделе, посвященном пакету UTL_TCP. Его производительность сравнима с обеспечиваемой стандартным пакетом UTL_HTTP, а возможности Ч намного шире. Стандартный пакет UTL_HTTP, поставляемый вместе с сервером, реализует весьма упрощенный подход. Он содержит две функции. Х UTL_HTTP.REQUEST: возвращает до 2000 первых байт содержимого с заданным адресом URL. Х UTL_HTTP.REQUEST_PIECES: возвращает PL/SQL-таблицу элементов типа VARCHAR2(2000). Если конкатенировать последовательно все элементы, будет получено содержимое соответствующей страницы.
В пакете UTL_HTTP, однако, не хватает многих возможностей. Х Нельзя проверить заголовки HTTP. Это не позволяет выдавать сообщения об ошибках. Нельзя, например, различить ошибки доступа Not Found и Unauthorized. Х Нельзя пересылать информацию на Web-сервер с помощью метода POST. Можно использовать только метод GET. Кроме того, не поддерживается метод HEAD протокола HTTP. Х С помощью пакета UTL_HTTP нельзя получать двоичные данные.
Приложение А Х Интерфейс запроса страницы по частям (REQUEST_PIECES) неочевиден Ч намного проще было бы использовать данные типа CLOB или BLOB для возврата данных в виде "потока" (что обеспечило бы заодно и доступ к двоичным данным). Х Пакет не поддерживает "ключики" (cookies). Х Пакет не поддерживает даже простейшую аутентификацию. Х В пакете нет методов кодирования адреса URL. Одна из возможностей, которые пакет UTL_HTTP поддерживает, Ч это использование протокола SSL. C помощью диспетчера Oracle Wallet можно выполнять запросы по протоколу HTTPS (HTTPS Ч это реализация протокола HTTP поверх SSL). Я продемонстрирую использование пакета UTL_HTTP для доступа по протоколу SSL, но соответствующие возможности в пакете HTTP_PKG реализовывать не будем. Полный текст пакета HTTP_PKG вследствие большого размера в этом разделе не приводится;
он доступен на сайте издательства Wrox по адресу Возможности пакета UTL_HTTP Рассмотрим сначала функциональные возможности пакета UTL_HTTP, поскольку предполагается реализовать аналогичные в пакете HTTP_PKG. Простейший вариант использования средств пакета UTL_HTTP представлен далее. В этом примере myserver Ч имя моего Web-сервера. Вы, разумеется, должны выполнить этот пример, обращаясь в Webсерверу, который вам доступен: ops$tkyte@DEV816> select utl_http.request(' from dual;
UTL_HTTP.REQUEST('