Oracle для профессионалов Книга 2 Расширение возможностей и защита Третье издание, переработанное и дополненное Том Кайт
торгово-нздательскнй дом
DiaSoft
Москва • Санкт-Петербург • Киев 2005
УДК 681.3. 06(075) ББК 32.973.2 К 62 Кайт Том К 62 Oracle для профессионалов. Книга 2. Расширение возможностей и защита. Третье издание, переработанное и дополненное / Том Кайт. ; Пер. с англ. — СПб. : ООО «ДиаСофтЮП», 2005. - 8 1 6 с. ISBN 5-93772-159-4 Выход в свет в конце прошлого года этой книги издательства Wrox стал эпохальным событием: впервые доходчиво и исчерпывающе раскрыты основные особенности архитектуры СУБД Oracle, принципиально отличающие ее от других популярных систем управления базами данных. Причем подробно описаны и проиллюстрированы множеством примеров именно те возможности, средства и особенности Oracle, которые обеспечивают разработку эффективных приложений для этой СУБД и ее успешную эксплуатацию. Автор книги, Том Кайт, давно работает с СУБД Oracle, создает приложения и администрирует базы данных. Многие годы он профессионально занимается решением проблем, возникающих при использовании СУБД Oracle у администраторов и разработчиков по всему миру. На специализированном сайте корпорации Oracle (http://asktom.oracle.com) Том Кайт отвечает на десятки вопросов в день. Он не только делится знаниями, но и умело подталкивает читателя к самостоятельным экспериментам. Следуя по указанному им пути, становишься Профессионалом. Если вы приступаете к изучению СУБД Oracle, — начните с этой книги. Если вы опытный разработчик приложений или администратор баз данных Oracle, — прочтите ее и проверьте, достаточно ли глубоко вы знаете эту СУБД и умеете ли использовать ее возможности. Вы найдете в книге десятки советов, описаний приемов и методов решения задач, о которых никогда не подозревали. ББК 32.973.2 Authorized translation from the English language edition, entitled Expert One-on-One Oracle, 1st Edit on by Kyte, Thomas, published by Pearson Education, Inc, publishing as Wrox Press Ltd, Copyright © 2002 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Russian language edition published by DiaSoft Publishing. Copyright © 2005 Лицензия предоставлена издательством Wrox Press Ltd. Все права зарезервированы, включая право на полное или частичное воспроизведение в какой бы то ни было форме. Материал, изложенный в данной книге многократно проверен. Но, поскольку вероятность технич гских ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги. Все торговые знаки, упомянутые в настоящем издании, зарегистрированы. Случайное неправильное использование или пропуск торгового знака или названия его законного владельца не должно рассматриваться как нарушение прав собственности. ISBN 966-7992-24-1 (рус.) ISBN 5-93772-159-4 (англ.)
© Перевод на русский язык. ООО «ДиаСофтЮП», 2005 © Wrox Press Ltd, 2002 © Оформление. ООО «ДиаСофтЮП», 2005 Гигиеническое заключение № 77.99.6.953.П.438.2.99 от 04.02.1999
Предисловие научного редактора Уважаемые читатели! Это вторая часть моего перевода замечательной книги Тома Кайта "Expert One-onOne Oracle", опубликованной издательством Wrox Press Ltd. в конце 2001 года. В нее вошли главы 12-23 и приложения. Я буду следить за дальнейшей судьбой книги и связанных с ней материалов и регулярно публиковать соответствующую информацию на своем сайте по адресу http://ln.com.ua/~openxs/projects/oracle/expert.html. С удовольствием отвечу на ваши вопросы, связанные с переводом и использованием СУБД Oracle. В. Кравчук, OpenXS Initiative http://ln.com.ua/~openxs mailto:
[email protected] 2 апреля 2005 года
'
Оглавление Предисловие научного редактора Об авторе Благодарности
Глава 12. Аналитические функции
5 14 15
17
Пример Как работают аналитические функции Синтаксис Функции Конструкция секционирования Конструкция упорядочения Конструкция окна Окна диапазона Окна строк ..., Задание окон Функции Примеры Запрос первых N Запрос с транспонированием Доступ к строкам вокруг текущей строки Проблемы Аналитические функции в PL/SQL Аналитические функции в конструкции WHERE Значения NULL и сортировка Производительность
18 21 21 23 23 24 26 28 31 33 36 39 39 50 57 61 61 63 63 65
Резюме
66
Глава 13. Материализованные представления
69
Предыстория Что необходимо для выполнения примеров Пример Назначение материализованных представлений Как работать с материализованными представлениями Подготовка Внутренняя реализация Переписывание запроса Как гарантировать использование представлений Ограничения целостности Измерения Пакет DBMS_OLAP Оценка размера Проверка достоверности измерений Рекомендация создания материализованных представлений Проблемы
70 71 72 78 79 79 80 81 83 83 88 97 97 99 101 103
Оглавление
Материализованные представления не предназначены для систем OLTP Целостность запросов при переписывании Резюме
Глава 14. Секционирование Использование секционирования Повышение доступности данных Упрощение администрирования Повышение производительности операторов DML и запросов Как выполняется секционирование Схемы секционирования таблиц Секционирование индексов Локально секционированные индексы Глобально секционированные индексы Резюме
Глава 15. Автономные транзакции Пример Когда использовать автономные транзакции? Аудит, записи которого не могут быть отменены Метод, позволяющий избежать ошибки изменяющейся таблицы Выполнение операторов DDL в триггерах Запись в базу данных Строгий аудит Когда среда позволяет выполнять только операторы SELECT Разработка модульного кода Как работают автономные транзакции Выполнение транзакции Область действия Переменные пакетов Установки/параметры сеанса Изменения в базе данных Блокировки Завершение автономной транзакции Точки сохранения Проблемы Невозможность использования в распределенных транзакциях Только в среде PL/SQL Откатывается вся транзакция Временные таблицы уровня транзакции Изменяющиеся таблицы Ошибки, которые могут произойти Резюме
Глава 16. Динамический SQL Сравнение динамического и статического SQL Когда использовать динамический SQL? Использование динамического SQL Пакет DBMS_SQL
103 104 104
107 108 108 110 111 114 114 119 120 128 138
141 142 144 144 147 148 154 154 158 162 163 163 165 165 166 167 170 171 172 174 174 174 174 176 178 180 181
183 184 186 188 188
8
Оглавление
Встроенный динамический SQL Сравнение пакета DBMS_SQL и встроенного динамического SQL Связываемые переменные Количество столбцов выходных данных на этапе компиляции не известно Многократное выполнение одного и того же оператора Проблемы Нарушение цепочки зависимостей "Хрупкость" кода Сложность настройки Резюме
Глава 17. interMedia Краткий исторический экскурс Использование компонента interMedia Text Поиск текста Управление разнородными документами Индексирование текста из различных источников данных Компонент interMedia Text —часть базы данных Oracle Смысловой анализ Поиск в приложениях XML Как работает компонент interMedia Text Индексирование с помощью interMedia Text Оператор ABOUT Поиск в разделах Проблемы Компонент interMedia Text — это НЕ система документооборота Синхронизация индекса Индексирование информации вне базы данных Службы обработки документов Индекс-каталог Возможные ошибки Устаревший индекс Ошибки внешней процедуры Дальнейшее развитие Резюме
Глава 18. Внешние процедуры на языке С Когда используются внешние процедуры? Как реализована поддержка внешних процедур? Конфигурирование сервера Проверка программы extproc Проверка среды сервера Проверка процесса прослушивания Первая проверка Компиляция кода extproc.с Настройка учетной записи SCOTT/TIGER Создание библиотеки demolib Установка и запуск
195 200 200 205 213 223 223 224 224 225
227 228 229 229 232 232 235 236 238 239 242 245 246 252 252 253 254 254 255 257 257 258 259 259
261 262 264 265 268 268 270 270 271 272 272 273
Оглавление Наша первая внешняя процедура Оболочка Код на языке С Создание внешней процедуры Установка и запуск Внешняя процедура для сброса большого объекта в файл (LOBJO) Спецификация пакета LOBJO Код Рго*С для пакета LOBJO - Создание внешней процедуры Установка и использование пакета LOBJO Возможные ошибки Резюме
Глава 19. Хранимые процедуры на языке Java Когда используются хранимые процедуры на языке Java? Как работают внешние процедуры на языке Java Передача данных Полезные примеры Генерация списка файлов каталога Выполнение команды ОС Получение времени с точностью до миллисекунд Возможные ошибки ORA-29549 Java Session State Cleared Ошибки прав доступа ORA-29531 no method X in class Y Резюме
Глава 20. Использование объектно-реляционных средств В каких случаях используются объектно-реляционные средства Как работают объектно-реляционные средства Добавление новых типов данных в систему Использование типов для расширения возможностей языка PL/SQL Создание нового типа данных PL/SQL Уникальные приемы использования наборов SELECT * из PL/SQL-функции Множественная выборка данных в записи Вставка записей Объектно-реляционные представления Необходимые типы Объектно-реляционное представление Резюме
Глава 21. Детальный контроль доступа Пример Когда использовать это средство? Простота сопровождения Контроль доступа выполняется на сервере Упрощение разработки приложений Эволюционная разработка приложений
274 275 286 310 314 315 316 318 322 324 329 336
339 340 341 346 356 356 358 361 362 362 363 363 364
367 368 369 369 383 383 394 395 398 399 400 400 401 413
417 418 419 419 420 421 421
10
Оглавление
Отказ от совместно используемых учетных записей Поддержка совместно используемых учетных записей Предоставление доступа к приложению как к службе Как реализованы средства детального контроля доступа Пример 1: Реализация правил защиты Пример 2: Использование контекстов приложений Проблемы Целостность ссылок Скрытый канал Удаление строк Кеширование курсоров Экспортирование/Импортирование Проблемы экспорта Проблемы импорта Отладка Ошибки, которые могут произойти Резюме
421 421 422 423 425 429 447 447 448 449 452 458 459 461 462 462 467
Глава 22. Многоуровневая аутентификация
469
Когда использовать многоуровневую аутентификацию? Механизм многоуровневой аутентификации Предоставление привилегии Аудит промежуточных учетных записей Проблемы Резюме
470 473 481 482 483 485
Глава 23. Права вызывающего и создателя
487
Пример Когда использовать права вызывающего Разработка универсальных утилит Приложения, работающие со словарем данных Универсальные объектные типы Реализация собственных средств контроля доступа Когда использовать права создателя Производительность и масштабируемость Защита Как работают процедуры с правами вызывающего Права создателя Компиляция процедуры с правами создателя Права вызывающего Разрешение ссылок и передача привилегий Компиляция процедуры с правами вызывающего Использование объектов-шаблонов Проблемы Права вызывающего и использование разделяемого пула Производительность Более надежный код для обработки ошибок Побочные эффекты использования SELECT *
488 491 491 495 498 498 501 501 502 503 503 505 508 509 514 515 519 519 522 524 526
Оглавление
Помните о "скрытых" столбцах Java и права вызывающего Возможные ошибки Резюме
Приложение А. Основные стандартные пакеты Когда используются стандартные пакеты О стандартных пакетах
Пакеты DBMS_ALERT и DBMS.PIPE Когда использовать сигналы и каналы Настройка Пакет DBMS_ALERT Одновременные сигналы нескольких сеансов Неоднократная передача сигнала в сеансе Передача многочисленных сигналов несколькими сеансами до вызова процедуры ожидания Пакет DBMS_PIPE Серверы каналов или внешние процедуры? Пример в сети Internet Резюме
Пакет DBMS_APPLICATION_INFO Использование информации о клиенте Использование представления V$SESSION_LONGOPS Резюме
Пакет DBMS.JAVA Функции LONGNAME и SHORTNAME Установка опций компилятора Процедура SET_OUTPUT Процедуры loadjava и dropjava Процедуры управления правами Резюме
Пакет DBMS_JOB Однократное выполнение задания Текущие задания Нетривиальное планирование Контроль заданий и обнаружение ошибок Резюме
Пакет DBMSJ.OB Как загружать большие объекты? Функция substr Оператор SELECT FOR UPDATE в языке Java Преобразования Преобразование типа BLOB в VARCHAR2 и обратно Преобразование данных типа LONG/LONG RAW в большой объект
11 527 529 534 534
537 538 539
541 542 542 543 545 546 547 548 551 553 553
554 556 557 563
564 564 565 569 569 570 572
573 577 581 584 586 588
589 590 591 591 593 594 598
12
Оглавление
Пример множественного однократного преобразования типа Оперативное преобразование типа данных Запись значений объекта типа BLOB/CLOB на диск Выдача большого объекта на Web-странице с помощью PL/SQL Резюме
Пакет DBMSJ.OCK Резюме
Пакет DBMSJ.OGMNR Обзор Этап 1: создание словаря данных Этап 2: использование средств LogMiner Опции и использование Определение с помощью LogMiner, когда Использование области PGA Ограничения пакетов LogMiner Объектные типы Oracle Перемещенные или расщепленные строки Другие ограничения Представление V$LOGMNR_CONTENTS Резюме
Пакет DBMS_OBFUSCATION_TOOLKIT Пакет-оболочка Проблемы Управление ключами Генерация и хранение ключей в клиентском приложении Хранение ключей в той же базе данных Хранение ключей в файловой системе сервера базы данных Резюме
Пакет DBMS_OUTPUT Как работает пакет DBMS_OUTPUT Пакет DBMSJDUTPUT в других средах Обход ограничений Использование небольшой функции-оболочки или другого пакета Создание аналога пакета DBMS_OUTPUT Резюме
Пакет DBMS_PROFILER Проблемы Резюме
Пакет DBMSJJTILITY Процедура COMPILE_SCHEMA Процедура ANALYZE_SCHEMA Применение процедуры ANALYZESCHEMA к изменяющейся схеме Процедура ANALYZEJ3CHEMA анализирует не все
600 603 606 606 608
609 613
614 616 617 620 626 629 631 632 632 635 638 638 642
643 645 660 662 662 663 664 665
666 667 672 676 676 677 683
684 694 695
696 696 701 702 703
Оглавление
Процедура ANALYZE_DATABASE Функция FORMAT_ERROR_STACK Функция FORMAT_CALL_STACK Функция GET_TIME Функция GET_PARAMETER__VALUE Процедура NAME_RESOLVE Процедура NAME_TOKENIZE Процедуры COMMA_TO_TABLE, TABLE_TO_COMMA Процедура DB_VERSION и функция PORT_STRING Функция GET_HASH_VALUE Резюме
Пакет UTL_FILE Параметр инициализации UTL_FILE_DIR Обращение к сетевым дискам в Windows Обработка исключительных ситуаций Как сбросить Web-страницу на диск? Ограничение длины строки — 1023 байта Чтение содержимого каталога Резюме
Пакет UTL_HTTP Возможности пакета UTLJHTTP Добавление поддержки протокола SSL в пакете UTL_HTTP Реальное использование пакета UTL_HTTP Улучшенная версия пакета UTLJHTTP Резюме
13 704 704 706 709 710 711 714 717 719 719 724
725 726 727 729 730 731 732 734
735 736 738 745 748 758
Пакет UTL_RAW
759
Пакет UTL_SMTP и отправка электронной почты
762
UTL_SMTP — расширенный пример использования Загрузка и использование интерфейса JavaMail Резюме
Пакет UTL_TCP Тип SocketType Резюме
Предметный указатель
762 767 776
777 778 791
792
Об авторе
Меня зовут Том Кайт. Я работаю в корпорации Oracle со времени версии 7.0.9 Саля тех, кто не измеряет время версиями Oracle, уточню — с 1993 года). Однако я работал с СУБД Oracle, начиная с версии 5.1.5с (однопользовательская версия для DOS стоимостью 99 долларов на дискетах емкостью 360 Кбайт). До корпорации Oracle я более шести лет работал системным интегратором, занимаясь построением крупномасштабных гетерогенных баз данных и приложений (в основном для правительственных и оборонных учреждений). В этот период я много работал с СУБД Oracle, а точнее, помогал пользователям баз данных Oracle. Я работал непосредственно с клиентами на этапе создания спецификаций и построения систем, но чаще помогал перестраивать или настраивать системы ("настройка" обычно сводилась к переделке). Кроме того, я — именно тот Том, который ведет рубрику "AskTom" в журнале Oracle Magazine, отвечая на вопросы читателей о сервере и инструментальных средствах Oracle. Обычно на сайте http://asktom.oracle.com я получаю за день от 30 до 40 вопросов и отвечаю на них. Раз в два месяца я публикую подборку лучших вопросов с ответами в журнале (ответы на все вопросы доступны в Web и, естественно, хранятся в базе данных Oracle). В общем, я занимаюсь тем, что помогаю людям успешно эксплуатировать СУБД Oracle. Да, а в свободное время я разрабатываю приложения и программное обеспечение в самой корпорации Oracle. В этой книге описано то, чем я занимаюсь ежедневно. Предлагаемый в ней материал посвящен темам и вопросам, с которыми пользователи сталкиваются постоянно. Все проблемы рассматриваются с позиции "если я использую это, то делаю так...". Моя книга — итог многолетнего опыта использования СУБД Oracle в тысячах различных ситуаций.
Благодарности
\
j
Благодарности Я хотел бы поблагодарить многих людей, помогавших мне создать эту книгу. В корпорации Oracle я работаю с лучшими и наиболее яркими людьми из тех, кого мне удалось узнать, и они все так или иначе помогли мне. В частности, я благодарю Джоэла Калмана (Joel Kallman) за помощь в создании раздела книги, посвященного технологии interMedia. В ходе работы над сайтом AskTom мне не раз пришлось обращаться к Джоэлу за помощью в этой области — он именно тот человек, к которому стоит обратиться, если речь идет об interMedia и соответствующих технологиях. Я также благодарен Дэвиду Ноксу (David Knox) за помощь в создании примеров работы с протоколом SSL в разделе, посвященном пакету UTL_HTTP. Если бы не его знания и желание поделиться ими со мной, этого раздела просто не было бы. Наконец, я хочу поблагодарить всех, с кем работаю, за поддержку в испытании писательским трудом. Мне понадобилось намного больше времени и энергии, чем я мог себе представить, и я благодарен им за понимание моих проблем. В особенности, хочу поблагодарить Тима Хёхста (Tim Hoechst) и Майка Хичва (Mike Hichwa), которых я знаю по совместной работе уже почти 10 лет. Их постоянные вопросы и требования помогли мне разобраться с вещами, которыми я лично никогда и не подумал бы заниматься. Хочу также поблагодарить пользователей программного обеспечения Oracle, задающих так много хороших вопросов. Если бы не они, мне и в голову не пришло бы написать эту книгу. Большая часть представленной здесь информации является прямым результатом чьего-нибудь вопроса "как" или "почему". Наконец, и это самое главное, я благодарен за неизменную поддержку моей семье. Когда в тысячный раз слышишь: "Папа, ну почему ты все еще пишешь эту книгу?", то понимаешь, что кому-то нужен. Я просто не представляю, как бы я закончил эту книгу без постоянной поддержки моей жены Лори, сына Алана и дочери Мэган. — Том Кайт
Редакция выражает свою искреннюю признательность Кузьменко Юрию Анатольевичу за неоценимый вклад в издание "Oracle для профессионалов. Книга 2. Расширение возможностей и защита". Его поддержка и активное участие позволили довести этот проект до успешного завершения и сделала возможным появление книги такой, какой вы ее держите в руках. — Издательство ООО "ТИД "ДС"
s-Ш*»*
lePrograra ,.sional0racleP .-gprof sssionalO leProgrammlngPi lO/'acleProgra1 slonalOracle Professional, ammingProfe^ «НЕ IMP
Sm I
I
W
*»*| 5
&** *t* (it ..;
! О Г* ri f" 1 • P P I" • ,••"•• ?."•<
«i
f*.:
>
•
•••
?"•
\%
:
*""' г о f P s ч i o h»
11*1
il
.
•
.
:
f
Аналитические функции •
SQL — очень мощный язык и лишь очень немногие запросы в нем нельзя создать. По опыту знаю, что можно придумать хитрый SQL-запрос для получения ответа практически на любой вопрос относительно любых данных. Однако производительность некоторых из этих запросов крайне низкая, да и придумать их непросто. Ряд запросов, которые сложно сформулировать на обычном языке SQL, весьма типичны: •
Подсчет промежуточной суммы. Показать суммарную зарплату сотрудников отдела построчно, чтобы в каждой строке выдавалась сумма зарплат всех сотрудников вплоть до указанного.
Q Подсчет процентов в группе. Показать, какой процент от общей зарплаты по отделу составляет зарплата каждого сотрудника. Берем его зарплату и делим на сумму зарплат по отделу. • Запросы первых N. Найти N сотрудников с наибольшими зарплатами или N наиболее продаваемых товаров по регионам. О Подсчет скользящего среднего. Получить среднее значение по текущей и предыдущим N строкам. Q Выполнение ранжирующих запросов. Показать относительный ранг зарплаты сотрудника среди других сотрудников того же отдела. Аналитические функции, появившиеся в версии Oracle 8.1.6, создавались для решения именно этих задач. Они расширяют язык SQL так, что подобные операции не только проще записываются, но и быстрее выполняются по сравнению с использованием чистого языка SQL. Эти расширения сейчас изучаются комитетом ANSI SQL с целью включения в спецификацию языка SQL.
18
Глава 12
Мы начнем эту главу с примера, демонстрирующего возможности аналитических функций. После этого будет представлен полный синтаксис и описание всех функций, а также ряд практических примеров выполнения перечисленных выше операций. Как обычно, в конце будут рассмотрены потенциальные проблемы использования аналитических функций.
Пример Простой пример подсчета промежуточной суммы зарплат по отделам с описанием того, что же в действительности происходит, позволят получить начальное представление о принципах использования аналитических функций: tkyte@TKYTE816> break on deptno skip 1 tkyte@TKYTE816> s e l e c t ename, deptno, s a l , 2 sum(sal) over 3 (order by deptno, ename) running total, 4 sum(sal) over (partition by deptno 5 6 order by ename) department total, 7 row number() over 8 (partition by deptno 9 order by ename) seq 10 from emp • 11 order by deptno, ename 12 / ENAME
DEPTNO
SAL RUNNING TOTAL
DEPARTMENT TOTAL
EEQ
CLARK KING MILLER
10
2450 5000 1300
2450 7450 8750
2450 7450 8750
1 2 3
ADAMS FORD JONES SCOTT SMITH
20
1100 3000 2975 3000 800
9850 12850 15825 18825 19625
1100 4100 7075 10075 10875
1 2 3 4 5
ALLEN BLAKE JAMES MARTIN TURNER WARD
30
1600 2850 950 1250 1500 1250
21225 24075 25025 26275 27775 29025
1600 4450 5400 6650 8150 9400
1 2 3 4 5 6
14 rows selected. В представленном выше коде удалось получить значение RUNNING_TOTAL для запроса в целом. Это было сделано по всему упорядоченному результирующему множеству с помощью конструкции SUM(SAL) OVER (ORDER BY DEPTNO, ENAME). Также удалось подсчитать промежуточные суммы по отделам, сбрасывая их в ноль при переходе к следующему отделу. Этого удалось добиться благодаря конструкции PARTITION BY
Аналитические функции
J^ у
DEPTNO в SUM(SAL) — в запросе была указана конструкция, задающая условие разбиения данных на группы. Для последовательной нумерации строк в каждой группе, в соответствии с критериями упорядочения, использовалась функция ROW_NUMBER() (для выдачи этого номера строки был добавлен столбец SEQ). В результате видно, что SCOTT — четвертый по списку сотрудник в отделе 20 при упорядочении по фамилии (ENAME). Функция ROW_NUMBER() используется и во многих других ситуациях, например для транспонирования или преобразования результирующих множеств (как будет описано далее). Этот новый набор функциональных возможностей содержит много замечательного. Он открывает абсолютно новые перспективы работы с данными. Можно избавиться от большого объема процедурного кода и сложных (или неэффективных) запросов, требующих много времени на разработку, и получить при этом желаемый результат. Чтобы почувствовать, насколько эффективными могут быть аналитические функции по сравнению с "чисто реляционными способами", давайте оценим производительность в случае 1000 строк, а не 14. При этом сравним производительность двух запросов: с новыми аналитическими функциями и на основе "старых" реляционных методов. Следующая пара операторов позволит создать аналог таблицы SCOTT.EMP с тремя столбцами — ENAME, DEPTNO и SAL — и индексом (единственным необходимым в данном примере). Я буду выбирать данные по столбцам DEPTNO и ENAME: tkyte@TKYTE816> create t a b l e t 2 as 3 select object_name ename, 4 mod(object_id, 50) deptno, 5 object_id sal 6 from all_objects 7 where rownum <« 1000 8 / Table c r e a t e d . tkyte@TKYTE816> c r e a t e index t_idx on t(deptno,ename); Index c r e a t e d . Повторим запрос, но к новой таблице, задав установку AUTOTRACE TRACEONLY, чтобы увидеть, сколько и чего пришлось делать (для этого необходимо наличие роли PLUSTRACE): tkyte@TKYTE816> set autotrace traceonly tkyte@TKYTE816> select ename, deptno, sal, 2 sum(sal) over 3 (order by deptno, ename) running_total, 4 sum (sal) over 5 (partition by deptno 6 order by ename) department_total, 7 row_number() over 8 (partition by deptno 9 order by ename) seq 10 from t emp 11 order by deptno, ename
20
Глава 12 12
/
1000 rows selected. Elapsed: 00:00:00.61 Execution Plan 0 1 2 3
0 1 2
SELECT STATEMENT Optimizer=CHOOSE WINDOW (BUFFER) TABLE ACCESS (BY INDEX ROWID) OF 'T' INDEX (FULL SCAN) OF 'T_IDX' (NON-UNIQUE)
Statistics 0 2 292 66 0 :L06978 7750 68 0 1 1000
recursive calls db block gets consistent gets physical reads redo size bytes sent via SQL*Net to client bytes received via SQL*Net from client SQL*Net roundtrips to/from client sorts (memory) sorts (disk) rows processed
Итак, потребовалось 0,61 секунды и 294 логические операции ввода-вывода. Теперь выполним эквивалентный запрос, но используя только "стандартные" возможности языка SQL: tkyte@TKYTE816> s e l e c t ename, deptno, s a l , 2 (select sum(sal) 3 from t e2 4 where e2.deptno < emp.deptno 5 or (e2.deptno = emp.deptno and e2.ename <= emp.ename )) 6 running_total, 7 (select sum(sal) 8 from t e3 9 where e3.deptno = emp.deptno 10 and e3.ename <= emp.ename) 11 department_total, 12 (select count(ename) 13 from t e3 14 where e3.deptno = emp.deptno 15 and e3.ename <= emp.ename) 16 seq 17 from t emp 18 order by deptno, ename 19
/
1000 rows selected. Elapsed: 00:00:06.89
Аналитические функции
2 I
Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE TABLE ACCESS (BY INDEX ROWID) OF 'T' INDEX (FULL SCAN) OF 'T_IDX' (NON-UNIQUE)
Statistics 0 0 665490 0 0 106978 7750 68 0 0 1000
•
recursive calls db block gets c o n s i s t e n t gets physical reads redo s i z e bytes sent v i a SQL*Net t o c l i e n t bytes received v i a SQL*Net from c l i e n t SQL*Net roundtrips to/from c l i e n t s o r t s (memory) s o r t s (disk) rows processed
tkyte@TKYTE816> s e t autotrace off Оба запроса дали одинаковые результаты, но производительность отличается существенно. Время выполнения больше во много раз, а количество логических операций ввода-вывода увеличилось на несколько порядков. Аналитические функции обработали результирующее множество, использовав намного меньше ресурсов и, соответственно, быстрее. Более того, если рассмотреть синтаксис аналитических функций, оказывается, что записывать с их помощью запросы намного проще, чем на стандартном языке SQL. Чтобы почувствовать разницу, сравните текст двух представленных выше запросов.
Как работают аналитические функции В первой части этого раздела будут представлены подробности синтаксиса и определены термины. После этого мы перейдем непосредственно к примерам. Я продемонстрирую многие из 26 новых функций (не все, потому что при этом некоторые примеры повторялись бы). Аналитические функции используют общий синтаксис и предоставляют специфические возможности, создававшиеся для нужд технических дисциплин, незнакомых большинству разработчиков. Поняв принцип написания аналитических функций — как секционировать данные, как задавать окна данных и так далее, — использовать эти функции будет очень легко.
Синтаксис Синтаксис вызова аналитической функции на вид весьма прост, но эта простота может быть обманчивой. Все начинается с такой конструкции: ИМЯ_ФУНКЦИИ(<аргумент>,< а р г у м е н т > , . . . ) OVER (<конструкция_секционирования> <конструкция_упорядочения> <конструкция_окна>)
22
Глава 12
Вызов аналитической функции может содержать до четырех частей: аргументы, конструкция секционирования, конструкция упорядочения и конструкция, задающая окно. В представленном выше примере: 4 5 6
sum (sal) over (partition by deptno order by ename) department_total,
Q SUM — имя функции. •
(SAL) — аргумент аналитической функции. Аналитические функции принимают от нуля до трех аргументов. В качестве аргументов передаются выражения, т.е. вполне можно было бы использовать SUM(SAL+COMM).
• OVER — ключевое слово, идентифицирующее эту функцию как аналитическую. В противном случае синтаксический анализатор не мог бы отличить функцию агрегирования SUM() от аналитической функции SUM(). Конструкция после ключевого слова OVER описывает срез данных, "по которому" будет вычисляться аналитическая функция. • PARTITION BY DEPTNO — необязательная конструкция секционирования. Если конструкция секционирования не задана, все результирующее множество считается одной большой секцией. Это используется для разбиения результирующего множества на группы, так что аналитическая функция применяется к группам, а не ко всему результирующему множеству. В первом примере главы, когда конструкция секционирования не указывалась, функция SUM по столбцу SAL вычислялась для всего результирующего множества. Секционируя результирующее множество по столбцу DEPTNO, мы вычисляли SUM по столбцу SAL для каждого отдела (DEPTNO), сбрасывая промежуточную сумму для каждой группы. Q ORDER BY ENAME — необязательная конструкция ORDER BY; для некоторых функций она обязательна, для других — нет. Функции, зависящие от упорядочения данных, например LAG и LEAD, которые позволяют обратиться к предыдущим и следующим строкам в результирующем множестве, требуют обязательного указания конструкции ORDER BY. Другие функции, например AVG, не требуют. Эта конструкция обязательна, если используется любая функция работы с окном (подробнее см. далее в разделе "Конструкция окна"). Конструкция ORDER BY определяет, как упорядочиваются данные в группах при вычислении аналитической функции. В нашем случае упорядочивать по DEPTNO и ENAME не нужно, потому что по столбцу DEPTNO выполнялось секционирование, т.е. неявно предполагается, что столбцы, по которым выполняется секционирование, по определению входят в ключ сортировки (конструкция ORDER BY применяется к каждой секции поочередно). •
Конструкция окна в данном примере отсутствует. Именно ее синтаксис иногда кажется сложным. Подробно возможные способы задания конструкции окна будут рассмотрены ниже.
Теперь более детально рассмотрим каждую из четырех частей вызова аналитической функции, чтобы понять, как их можно задавать.
Аналитические функции
23
Функции Сервер Oracle предлагает 26 аналитических функций. Они разбиваются на четыре основных класса по возможностям. Первый класс образуют различные функции ранжирования, позволяющие строить запросы типа "первых N". Мы уже использовали одну функцию этого класса, ROW_NUMBER, при генерации столбца SEQ в предыдущем примере. Она ранжировала сотрудников в отделах по фамилии (ENAME). Точно так же их можно было бы ранжировать по зарплате (SALARY) или любому другому атрибуту. Второй класс образуют оконные функции, позволяющие вычислять разнообразные агрегаты. В первом примере этой главы была показана такая функция — мы вычисляли SUM(SAL) по разным группам. Вместо функции SUM можно было использовать и другие функции агрегирования, например COUNT, AVG, MIN, МАХ и т.д. К третьему классу относятся различные итоговые функции. Они очень похожи на оконные, поэтому имеют те же имена: SUM, MIN, MAX и т.д. Тогда как оконные функции используются для работы с окнами данных, как промежуточная сумма в предыдущем примере, итоговые функции работают со всеми строками секции или группы. Например, если бы в первоначальном запросе использовались обращения: sum(sal) over () t o t a l _ s a l a r y , sum(sal) over ( p a r t i t i o n by deptno)
total_salary_for_department
мы бы получили общие суммы по группам, а не промежуточные. Ключевое отличие итоговой функции от оконной — отсутствие конструкции ORDER BY в операторе OVER. При отсутствии конструкции ORDER BY функция применяется к каждой строке группы. При наличии конструкции ORDER BY функция применяется к окну (подробнее об этом в разделе, описывающем конструкцию окна). Есть также функции LAG и LEAD, позволяющие получать значения из предыдущих или следующих строк результирующего множества. Это помогает избежать самосоединения данных. Например, если в таблице записаны даты визитов пациентов к врачу и необходимо вычислить время между визитами для каждого их них, очень пригодится функция LAG. Можно просто секционировать данные по пациентам и отсортировать их по дате. После этого функция LAG легко сможет вернуть данные предыдущей записи для пациента. Останется вычесть из одной даты другую. До появления аналитических функций для получения этих данных приходилось организовывать сложное соединение таблицы с ней же самой. Наконец, есть большой класс статистических функций, таких как VAR_POP, VAR_SAMP, STDEV_POP, набор функций линейной регрессии и т.п. Эти функции позволяют вычислять значения статистических показателей для любой неупорядоченной секции. В конце раздела, посвященного синтаксису, представлена таблица с кратким объяснением назначения всех аналитических функций.
Конструкция секционирования Конструкция PARTITION BY логически разбивает результирующее множество на N групп по критериям, задаваемым выражениями секционирования. Слова "секция" и "группа" в этой главе и в документации Oracle используются как синонимы. Аналити-
24
Глава 12
ческие функции применяются к каждой группе независимо, — для каждой новой группы они сбрасываются. Например, ранее при демонстрации функции, вычисляющей промежуточную сумму зарплат, секционирование выполнялось по столбцу DEPTNO. Когда значение в столбце DEPTNO в результирующем множестве изменялось, происходил сброс промежуточной суммы в ноль, и суммирование начиналось заново. Если не указать конструкцию секционирования, все результирующее множество считается одной группой. Во первом примере мы использовали функцию SUM(SAL) без конструкции секционирования, чтобы получить промежуточные суммы для всего результирующего множества. Интересно отметить, что каждая аналитическая функция в запросе может иметь уникальную конструкцию секционирования; фактически уже в простейшем примере в начале главы это и было сделано. Для столбца RUNNING_TOTAL конструкция секционирования не была задана, поэтому целевой группой было все результирующее множество. Для столбца DEPARTMENTAL_TOTAL результирующее множество секционируется по отделам, что позволило вычислять промежуточные суммы для каждого из них. Синтаксис конструкции секционирования прост и очень похож на синтаксис конструкции GROUP BY в обычных SQL-запросах: PARTITION BY выражение [, выражение] [, выражение]
Конструкция упорядочения Конструкция ORDER BY задает критерий сортировки данных в каждой группе (в каждой секции). Это, несомненно, влияет на результат выполнения любой аналитической функции. При наличии (или отсутствии) конструкции ORDER BY аналитические функции вычисляются по-другому. В качестве примера рассмотрим, что происходит при использовании функции AVG() с конструкцией ORDER BY и без оной: scott@TKYTE816> s e l e c t ename, s a l , avg(sal) over () 2 from emp; 3 / ENAME SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER
SAL AVG(SAL)OVER() 800.00 1600.00 1250.00 2975.00 1250.00 2850.00 2450.00 3000.00 5000.00 1500.00 1100.00 950.00 3000.00 1300.00
14 rows selected.
2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21 2073.21
Аналитические функции
25
scott@TKYTE816> select ename, sal, avg(sal) over (ORDER BY ENAME) 2 from emp 3 order by ename .4 / ENAME ADAMS ALLEN BLAKE CLARK FORD JAMES JONES KING MARTIN MILLER SCOTT SMITH TURNER WARD
SAL AVG(SAL)OVER(ORDERBYENAME) 1100.00 1600.00 2850.00 2450.00 3000.00 950.00 2975.00 5000.00 1250.00 1300.00 3000.00 800.00 1500.00 1250.00
1100.00 1350.00 1850.00 2000.00 2200.00 1991.67 2132.14 2490.63 2352.78 2247.50 2315.91 2189.58 2136.54 2073.21
14 rows selected. В отсутствие конструкции ORDER BY среднее значение вычисляется по всей группе, и одно и то же значение выдается для каждой строки (функция используется как итоговая). Когда функция AVG() используется с конструкцией ORDER BY, среднее значение в каждой строке является средним по текущей и всем предыдущим строкам (функция используется как оконная). Например, средняя зарплата для пользователя ALLEN в результатах выполнения запроса с конструкцией ORDER BY — 1350 (среднее для значений 1100 и 1600). Немного забегая вперед, в следующий раздел, посвященный конструкции окна, можно сказать, что наличие конструкции ORDER BY в вызове аналитической функции добавляет стандартную конструкцию окна — RANGE UNBOUNDED PRECEDING. Это означает, что для вычисления используется набор из всех предыдущих и текущей строки в текущей секции. При отсутствии ORDER BY стандартным окном является вся секция. Чтобы реально почувствовать, как все это работает, рекомендую применить одну и ту же аналитическую функцию при двух различных конструкциях ORDER BY. В первом примере текущая сумма вычисляется для всей таблицы ЕМР с использованием конструкции ORDER BY DEPTNO, ENAME. При этом текущая сумма вычисляется для всех строк, причем, порядок их просмотра определяется конструкцией ORDER BY. Если изменить порядок указания столбцов в этой конструкции на противоположный или вообще сортировать по другим столбцам, получаемые текущие суммы будут существенно отличаться; общая сумма в последней строке совпадет, но все промежуточные значения будут другими. Например:
Z0
Глава 12 ops$tkyte@DEV816> select ename, deptno, 2 sum(sal) over (order by ename, deptno) sum_ename_deptno, 3 sum(sal) over (order by deptno, ename) sum_deptno ename 4 from emp 5 order by ename, deptno 6 / ENAME
IDEPTNO SUM_ENAME DEPTNO SUM_DEPTNO_ENAME
ADAMS ALLEN BLAKE CLARK FORD JAMES JONES KING MARTIN MILLER SCOTT SMITH TURNER WARD
20 30 30 10 20 30 20 10 30 10 20 20 30 30
1100 2700 5550 8000 11000 11950 14925 19925 21175 22475 25475 26275 27775 29025
9850 21225 24075 2450 12850 25025 15825 7450 26275 8750 18825 19625 27775 29025
14 rows selected. Оба столбца SUM(SAL) одинаково корректны; один из них содержит SUM(SAL) при упорядочении по столбцу DEPTNO, а потом — по ENAME, а другой — при упорядочении по столбцу ENAME, а потом — по DEPTNO. Поскольку результирующее множество упорядочено по (ENAME, DEPTNO), значения SUM(SAL), вычислявшиеся именно в этом порядке, кажутся более корректными, но общая сумма совпадает: 29025. Конструкция ORDER BY в аналитических функциях имеет следующий синтаксис: ORDER BY выражение [ASCIDESC] [NULLS FIRST|NULLS LAST]
Она совпадает с конструкцией ORDER BY для запроса, но будет упорядочивать строки только в пределах секций и может не совпадать с конструкцией ORDER BY для запроса в целом (или любой другой секции). Конструкции NULLS FIRST и NULLS LAST впервые появились в версии Oracle 8.I.6. Они позволяют указать, где при упорядочении должны быть значения NULL — в начале или в конце. В случае сортировки по убыванию (DESC), особенно в аналитических функциях, эта новая возможность принципиально важна. Почему — описано в разделе "Проблемы" в конце главы.
Конструкция окна Синтаксис этой конструкции на первый взгляд кажется сложным из-за используемых ключевых слов. Конструкция вида RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, задающая стандартное окно при использовании конструкции ORDER BY, не похожа на те, что постоянно используются разработчиками. Синтаксис конструкции окна достаточно сложен для описания. Вместо попыток перерисовать синтаксические схемы, представленные в руководстве OracleSi SQL Reference Manual, я перечислю все варианты конструкции окна и опишу, какой набор данных в пределах груп-
Аналитические функции
27
пы задает соответствующий вариант. Сначала, однако, давайте разберемся, что вообще позволяет сделать конструкция окна. Конструкция окна позволяет задать перемещающееся или жестко привязанное окно (набор) данных в пределах группы, с которым будет работать аналитическая функция. Например, конструкция диапазона RANGE UNBOUNDED PRECEDING означает: "применять аналитическую функцию к каждой строке данной группы, с первой по текущую". Стандартным является жестко привязанное окно, начинающееся с первой строки группы и продолжающееся до текущей. Если используется следующая аналитическая функция. SUM(sal) OVER (PARTITION BY deptno ORDER BY ename ROWS 2 PRECEDING) d e p a r t m e n t _ t o t a l 2 , то будет создано перемещающееся окно в группе, и сумма зарплат будет вычисляться по столбцу SAL текущей и двух предыдущих строк в этой группе. Если необходимо создать отчет, показывающий сумму зарплат текущего и двух предыдущих сотрудников отдела, соответствующий сценарий может выглядеть так: scott@TKYTE816> break on deptno scott@TKYTE816> s e l e c t deptno, ename, s a l , 2 sum(sal) over 3 (partition by deptno 4 order by ename 5 rows 2 preceding) sliding total 6 from emp 7 order by deptno, ename 8 / DEPTNO ENAME
SAL SLIDING TOTAL
10 CLARK
2450 5000 1300
2450 7450 8750
20 ADAMS FORD JONES SCOTT SMITH
1100 3000 2975 3000 800
1100 4100 7075 8975 6775
30 ALLEN BLAKE JAMES MARTIN TURNER WARD
1600 2850 950 1250 1500 1250
1600 4450 5400 5050 3700 4000
14 rows selected.
Нас интересует эта часть запроса: 2 3
sum(sal) over (partition by deptno
28
Глава 12 4 5
order by ename rows 2 preceding) sliding_total
Конструкция секционирования приводит к вычислению SUM(SAL) по отделам, независимо от других групп (значение SUM(SAL) сбрасывается при изменении номера отдела). Конструкция ORDER BY ENAME приводит к сортировке данных в каждом отделе по столбцу ENAME; это позволяет с помощью конструкции окна, rows 2 preceding, при суммировании зарплат обращаться к двум предыдущим строкам в соответствии с заданным порядком сортировки. Например, значение в столбце SLIDINGJTOTAL для сотрудника SMITH — 6775, что равно сумме значений 800, 3000 и 2975. Это сумма зарплат в строке для SMITH и двух предыдущих строках окна. Можно создавать окна по двум критериям: по диапазону (RANGE) значений данных или по смещению (ROWS) относительно текущей строки. Конструкция RANGE уже встречалась ранее, RANGE UNBOUNDED PRECEDING например. Она требует брать все строки вплоть до текущей, в соответствии с порядком, задаваемым конструкцией ORDER BY. Следует помнить, что для использования окон необходимо задавать конструкцию ORDER BY. Сейчас мы рассмотрим задание окон с помощью конструкций ROWS и RANGE, а затем другие способы задания окон.
Окна диапазона Окна диапазона объединяют строки в соответствии с заданным порядком. Если в запросе сказано, например, "range 5 preceding", то будет сгенерировано перемещающееся окно, включающее предыдущие строки группы, отстоящие от текущей строки не более чем на 5 строк. Диапазон можно задавать в виде числового выражения или выражения, значением которого является дата. Применять конструкцию RANGE с другими типами данных нельзя. Если имеется таблица ЕМР со столбцом HIREDATE типа даты и задана аналитическая функция count(*) over (order by h i r e d a t e asc range 100 preceding) она найдет все предыдущие строки фрагмента, значение которых в столбце HIREDATE лежит в пределах 100 дней от значения HIREDATE текущей строки. В этом случае, поскольку данные сортируются по возрастанию (ASC), значения в окне будут включать все строки текущей группы, у которых значение в столбце HIREDATE меньше значения HIREDATE текущей строки, но не более чем на 100 дней. Если использовать функцию count(*) over (order by hiredate desc range 100 preceding) и сортировать фрагмент по убыванию (DESC), базовая логика работы останется той же, но, поскольку группа отсортирована иначе, в окно попадет другой набор строк. В рассматриваемом случае функция найдет все строки, предшествующие текущей, где значение в поле HIREDATE больше значения HIREDATE в текущей строке, но не более чем на 100 дней. Пример поможет это прояснить. Я буду использовать запрос с аналитической функцией FIRST_VALUE. Эта функция возвращает вычисленное значение для первой строки окна. Так мы легко сможем понять, где начинается окно:
Аналитические функции
2.У
scott@TKYTE816> s e l e c t ename, s a l , h i r e d a t e , hiredate-100 window_top, 2 first_value(ename) 3 over (order by hiredate asc 4 range 100 preceding) ename_prec, 5 first_value(hiredate) 6 over (order by hiredate asc 7 range 100 preceding) hiredate_prec 8 from emp 9 order by hiredate asc 10 / ENAME
SAL HIREDATE
SMITH ALLEN WARD JONES BLAKE CLARK TURNER MARTIN KING FORD JAMES MILLER SCOTT ADAMS
800 17-DEC-80 1600 20-FEB-81 1250 22-FEB-81 2975 02-APR-81 2850 01-MAY-81 2450 09-JUN-81 1500 08-SEP-81 1250 28-SEP-81 5000 17-NOV-81 3000 03-DEC-81 950 03-DEC-81 1300 23-JAN-82 3000 09-DEC-82 1100 12-JAN-83
WINDOWJTOP ENAME_PREC HIREDATE_ 08-SEP-80 SMITH 12-NOV-80 SMITH 14-NOV-80 SMITH 23-DEC-80 ALLEN 21-JAN-81 ALLEN 01-MAR-81 JONES 31-MAY-81 CLARK 20-JUN-81 TURNER 09-AUG-81 TURNER 25-AUG-81 TURNER 25-AUG-81 TURNER 15-OCT-81 KING 31-AUG-82 SCOTT 04-OCT-82 SCOTT
17-DEC-80 17-DEC-80 17-DEC-80 20-FEB-81 20-FEB-81 02-APR-81 09-JUN-81 08-SEP-81 08-SEP-81 08-SEP-81 08-SEP-81 17-NOV-81 09-DEC-82 09-DEC-82
14 rows selected. Мы упорядочили одну секцию по критерию HIREDATE ASC. При этом использовалась аналитическая функция FIRST_VALUE для поиска первого значения ENAME и первого значения HIREDATE в соответствующем окне. Посмотрев на строку данных для сотрудника CLARK, можно обнаружить, что для него значение в столбце HIREDATE — 9 июня 1981 года, 09-JUN-81, а дата за 100 дней до этой соответствует 1 марта 1981 года, 01-MAR-81. Для удобства эта дата помещена в столбец WINDOW_TOP. Аналитическая функция затем вычисляется для всех строк отсортированной секции, предшествующих строке для сотрудника CLARK и имеющих значение в столбце HIREDATE в диапазоне с 01-MAR-81 по 09-JUN-81. Первое значение ENAME для этого окна — JONES. Это имя и выдает аналитическая функция в столбце ENAME_PREC. Упорядочив данные по критерию HIREDATE DESC, мы получим: scott@TKYTE816> select ename, sal, hiredate, hiredate+100 windowtop, 2 first_value(ename) 3 over (order by hiredate desc 4 range 100 preceding) ename_prec, 5 first_value(hiredate) 6 over (order by hiredate desc 7 range 100 preceding) hiredate_prec 8 from emp 9 order by hiredate desc 10 /
30
e
Глава 12
ENAME
SAL HIREDATE
ADAMS SCOTT MILLER FORD JAMES KING MARTIN TURNER CLARK BLAKE JONES WARD ALLEN SMITH
1100 12-JAN-83 3000 09-DEC-82 1300 23-JAN-82 3000 03-DEC-81 950 03-DEC-81 5000 17-NOV-81 1250 28-SEP-81 1500 08-SEP-81 2450 09-JUN-81 2850 01-МАУ-81 2975 02-APR-81 1250 22-FEB-81 1600 20-FEB-81 800 17-DEC-80
WINDOWTOP ENAME_PREC HIREDATE_ 22-APR-83 19-MAR-83 03-MAY-82 13-MAR-82 13-MAR-82 25-FEB-82 06-JAN-82 17-DEC-81 17-SEP-81 09-AUG-81 ll-JUL-81 02-JUN-81 31-MAY-81 27-MAR-81
ADAMS ADAMS MILLER MILLER MILLER MILLER FORD FORD TURNER CLARK CLARK BLAKE BLAKE WARD
12-JAN-83 12-JAN-83 23-JAN-82 23-JAN-82 23-JAN-82 23-JAN-82 03-DEC-81 03-DEC-81 08-SEP-81 09-JUN-81 09-JUN-81 01-MAY-81 01-MAY-81 22-FEB-81
14 rows selected. Если снова обратиться к строке сотрудника CLARK, окажется, что выбрано другое окно, поскольку данные секции отсортированы по-иному. Окно для строки CLARK по условию RANGE 100 PRECEDING теперь доходит до строки TURNER, поскольку значение HIREDATE для TURNER — последняя дата среди значений HIREDATE в строках, предшествующих строке CLARK, отличающаяся не более чем на 100 дней. Иногда достаточно сложно понять, какие значения будут входить в диапазон. Я считаю использование функции FIRST_VALUE удобным методом, помогающим увидеть диапазоны окна и проверить, корректно ли установлены параметры. Теперь, представив диапазоны окон, мы используем их для вычисления чего-то более существенного. Пусть необходимо выбрать зарплату каждого сотрудника и среднюю зарплату всех принятых на работу в течение 100 предыдущих дней, а также среднюю зарплату всех принятых на работу в течение 100 следующих дней. Соответствующий запрос будет выглядеть так: scott@TKYTE816> s e l e c t ename, hiredate, s a l , 2 avg(sal) 3 over (order by hiredate asc range 100 preceding) 4 avg_sal_100_days_before, 5 avg(sal) 6 over (order by hiredate desc range 100 preceding) 7 avg_sal_100_days_after 8 from emp 9 order by 8 / ENAME
HIREDATE
SMITH ALLEN WARD JONES BLAKE CLARK TURNER
17-DEC-80 20-FEB-81 22-FEB-81 02-APR-81 01-MAY-81 09-JUN-81 08-SEP-81
SAL AVG SAL 100 DAYS BEFORE AVG SAL 100 DAYS AFTER 800.00 1600.00 1250.00 2975.00 2850.00 2450.00 1500.00
800.00 1200.00 1216.67 1941.67 2168.75 2758.33 1975.00
1216.67 2168.75 2358.33 2758.33 2650.00 1975.00 2340.00
Аналитические функции
MARTIN KING JAMES FORD MILLER SCOTT ADAMS
28-SEP-81 17-NOV-81 03-DEC-81 03-DEC-81 23-JAN-82 09-DEC-82 12-JAN-83
1250.00 5000.00 950.00 3000.00 1300.00 3000.00 1100.00
1375.00 2583.33 2340.00 2340.00 2562.50 3000.00 2050.00
31 2550.00 2562.50 1750.00 1750.00 1300.00 2050.00 1100.00
14 rows selected. Если теперь снова обратиться к строке для сотрудника CLARK, то, поскольку мы уже понимаем, какое окно в группе будет с ней связано, легко убедиться, что средняя зарплата (2758,33) равна (2450+2850+2975)/3. Это средняя зарплата для строки CLARK и строк, предшествующих CLARK (это строки для сотрудников JONES и BLAKE) при упорядочении данных по возрастанию. С другой стороны, средняя зарплата 1975,00 равна (2450+1500)/2. Это средняя зарплата для строки CLARK и строк, предшествующих CLARK при упорядочении данных по убыванию. С помощью этого запроса можно одновременно вычислить среднюю зарплату для сотрудников, принятых на работу за 100 дней до и за 100 дней после сотрудника CLARK. Окна диапазона можно задавать только по данным типа NUMBER или DATE, поскольку нельзя добавить или вычесть N единиц из значения типа VARCHAR2. Еще одно ограничение для таких окон состоит в том, что в конструкции ORDER BY может быть только один столбец — диапазоны по природе своей одномерны. Нельзя задать диапазон в N-мерном пространстве.
Окна строк Окна срок задаются в физических единицах, строках. Перепишем вступительный пример из предыдущего раздела, задав окно строк: count (*) over (order by x ROWS 5 preceding) Это окно будет включать до 6 строк: текущую и пять предыдущих (порядок определяется конструкцией ORDER BY). Для окон по строкам нет ограничений, присущих окнам по диапазону; данные могут быть любого типа и упорядочивать можно по любому количеству столбцов. Вот пример, сходный с рассмотренным ранее: scott@TKYTE816> s e l e c t ename, s a l , h i r e d a t e , 2 first_value(ename) 3 over (order by hiredate asc 4 rows 5 preceding) ename_j?rec, 5 first_value(hiredate) 6 over (order by hiredate asc 7 rows 5 preceding) hiredatejprec 8 from emp 9 order by hiredate asc 10 / ENAME SMITH ALLEN WARD
SAL HIREDATE ENAME PREC HIREDATE 800 .00 17-DEC-80 SMITH 1600 .00 20-FEB-81 SMITH 1250 .00 22-FEB-81 SMITH
17-DEC-80 17-DEC-80 17-DEC-80
32
Глава 12
2975 .00 JONES 2850 .00 BLAKE 2450 .00 CLARK TURNER 1500.00 MARTIN 1250.00 5000.00 KING 950.00 JAMES 3000.00 FORD 1300.00 MILLER SCOTT 3000.00 ADAMS 1100.00 14 rows selected.
02-APR-81 01-MAY-81 09-JUN-81 08-SEP-81 28-SEP-81 17-NOV-81 03-DEC-81 03-DEC-81 23-JAN-82 09-DEC-82 12-JAN-83
SMITH SMITH SMITH ALLEN WARD JONES BLAKE CLARK TURNER MARTIN KING
17-DEC-80 17-DEC-80 17-DEC-80 20-FEB-81 22-FEB-81 02-APR-81 01-MAY-81 09-JUN-81 08-SEP-81 28-SEP-81 17-NOV-81
Взглянув на строку для сотрудника CLARK, можно увидеть, что первой в окне ROWS 5 PRECEDING следует строка для сотрудника SMITH — она просто пятая перец строкой для CLARK в соответствии с заданным порядком. Строка для сотрудника SMITH будет первой в окне и для всех предыдущих строк (для BLAKE, JONES и т.д.). Дело в том, что строка для SMITH — первая в данной группе (она будет первой и для самой себя). При сортировке группы по возрастанию окна изменяются: scott@TKYTE816> s e l e c t ename, s a l , h i r e d a t e , 2 first value(ename) 3 over (order by hiredate desc 4 rows 5 preceding) ename prec' f 5 first value(hiredate) 6 over (order by hiredate desc 7 rows 5 preceding) hiredate prec 8 from emp 9 order by hiredate desc 10 / ENAME ADAMS SCOTT MILLER JAMES FORD KING MARTIN TURNER CLARK BLAKE JONES WARD ALLEN SMITH
SAL HIREDATE 1100.00 3000.00 1300.00 950.00 3000.00 5000.00 1250.00 1500.00 2450.00 2850.00 2975.00 1250.00 1600.00 800.00
12-JAN-83 09-DEC-82 23-JAN-82 03-DEC-81 03-DEC-81 17-NOV-81 28-SEP-81 08-SEP-81 09-JUN-81 01-MAY-81 02-APR-81 22-FEB-81 20-FEB-81 17-DEC-80
ENAME_PREC HIREDATE_ ADAMS ADAMS ADAMS ADAMS ADAMS ADAMS SCOTT MILLER JAMES FORD KING MARTIN TURNER CLARK
12-JAN-83 12-JAN-83 12-JAN-83 12-JAN-83 12-JAN-83 12-JAN-83 09-DEC-82 23-JAN-82 03-DEC-81 03-DEC-81 17-NOV-81 28-SEP-81 08-SEP-81 09-JUN-81
14 rows selected. Теперь первое значение для набора из 5 строк, предшествующих в группе строке для сотрудника CLARK, — строка для сотрудника JAMES. Теперь можно вычислить сред-
Аналитические функции
33
нюю зарплату для указанного сотрудника и пяти принятых на работу до него и после него: scott@TKYTE816> select ename, hiredate, sal, 2 avg(sal) 3 over (order by hiredate asc rows 5 preceding) avg_5_before, count(*) 4 over (order by hiredate asc rows 5 preceding) obs_before, 5 avg(sal) 6 over (order by hiredate desc rows 5 preceding) avg_5_after, 7 8 count(*) over (order by hiredate desc rows 5 preceding) obs_after 9 10 from emp 11 order by hiredate 12 / ENAME
HIREDATE
SMITH ALLEN WARD JONES BLAKE CLARK TURNER MARTIN KING JAMES FORD MILLER SCOTT ADAMS
17-DEC-80 20-FEB-81 22-FEB-81 02-APR-81 01-MAY-81 09-JUN-81 08-SEP-81 28-SEP-81 17-NOV-81 03-DEC-81 03-DEC-81 23-JAN-82 09-DEC-82 12-JAN-83
SAL AVG 5 BEFORE OBS BEFORE AVG 5 AFTER OBS AFTER 800.00 1600.00 1250.00 2975.00 2850.00 2450.00 1500.00 1250.00 5000.00 950.00 3000.00 1300.00 3000.00 1100.00
800.00 1200.00 1216.67 1656.25 1895.00 1987.50 2104.17 2045.83 2670.83 2333.33 2358.33 2166.67 2416.67 2391.67
1.00 2.00 3.00 4.00 5.00 6.00 6.00 6.00 6.00 6.00 6.00 6.00 6.00 6.00
1987.50 2104.17 2045.83 2670.83 2675.00 2358.33 2166.67 2416.67 2391.67 1587.50 1870.00 1800.00 2050.00 1100.00
6.00 6.00 6.00 6.00 6.00 6.00 6.00 6.00 6.00 4.00 5.00 3.00 2.00 1.00
14 rows selected. Обратите внимание, что я выбирал также значение COUNT(*). Это позволяет понять, по какому количеству строк было вычислено среднее значение. Можно явно увидеть, что для вычисления средней зарплаты сотрудников, принятых до сотрудника ALLEN, использовалось только 2 записи, а для вычисления средней зарплаты сотрудников, нанятых после него, — 6. В том месте группы, где находится строка для сотрудника ALLEN, есть только 1 предыдущая запись, а при вычислении аналитической функции используются все имеющиеся строки.
Задание окон Теперь, понимая различие между окнами диапазонов и окнами строк, можно изучать способы задания окон. В простейшем случае, окно задается с помощью одной из трех следующих взаимоисключающих конструкций. a
UNBOUNDED PRECEDING. Окно начинается с первой строки текущей группы и заканчивается текущей обрабатываемой строкой.
2 Зак. 244
34
Глава 12
Q CURRENT ROW. Окно начинается (и заканчивается) текущей строкой. а
Числовое_выражение PRECEDING. Окно начинается со строки за числовое_выражение строк до текущей, если оно задается по строкам, или со строки, меньшей по значению столбца, упомянутого в конструкции ORDER BY, не более чем на числовое_выражение, если оно задается по диапазону.
Окно CURRENT ROW в простейшем виде, вероятно, никогда не используется, поскольку ограничивает применение аналитической функции одной текущей строкой, а для этого аналитические функции не нужны. В более сложном случае для окна задается также конструкция BETWEEN. В ней CURRENT ROW можно указывать в качестве начальной или конечной строки окна. Начальную и конечную строку окна в конструкции BETWEEN можно задавать с использованием любой из перечисленных выше конструкций и еще одной, дополнительной: • Числовое_выражение FOLLOWING. Окно заканчивается (или начинается) со строки, через числовое_выражение строк после текущей, если оно задается по строкам, или со строки, большей по значению столбца, упомянутого в конструкции ORDER BY, не более чем на числовое_выражение, если оно задается по диапазону. Рассмотрим ряд примеров такого задания окон: scott@TKYTE816> s e l e c t deptno, ename, h i r e d a t e , 2 count(*) over (partition by deptno 3 order by hiredate nulls first 4 range 100 preceding) cnt range, 5 count(*) over (partition by deptno 6 order by hiredate nulls first 7 rows 2 preceding) cnt rows 8 from emp 9 where deptno in (10, 20) 10 order by deptno, hiredate 11 / DEPTNO ENAME
HIREDATE
CNT_RANGE
CNT_ROWS
10 CLARK KING MILLER
09-JUN-81 17-NOV-81 23-JAN-82
1 1 2
1 2 3
20 SMITH JONES FORD SCOTT ADAMS
17-DEC-80 02-APR-81 03-DEC-81 09-DEC-82 12-JAN-83
1 1 1 1 2
1 2 3 3 3
8 rows selected. Как видите, окно RANGE 100 PRECEDING содержит только строки текущей секции, предшествующие текущей строке, и те, значение которых HIREDATE находится в диапазоне HIREDATE-100 и HIREDATE относительно текущей. В данном случае таких строк всегда 1 или 2, т.е. интервал между приемом людей на работу обычно превы-
Аналитические функции
35
шает 100 дней (за исключением двух случаев). Окно ROWS 2 PRECEDING, однако, содержит от 1 до 3 строк (это определяется тем, как далеко текущая строка находится от начала группы). Для первой строки группы имеем значение 1 (предыдущих строк нет). Для следующей строки в группе таких строк 2. Наконец, для третьей и далее строк значение COUNT(*) остается постоянным, поскольку мы считаем только текущую строку и две предыдущие. Теперь рассмотрим использование конструкции BETWEEN. Все заданные до сих пор окна заканчивались текущей строкой и возвращались по результирующему множеству в поисках дополнительной информации. Можно задать окно так, что обрабатываемая строка не будет последней, а окажется где-то в середине окна. Например: scott@TKYTE816> s e l e c t ename, h i r e d a t e , 2 first_value(ename) over 3 (order by hiredate asc 4 range between 100 preceding and 100 following), 5 last_value(ename) over 6 (order by hiredate asc 7 range between 100 preceding and 100 following) 8 from emp 9 order by hiredate asc 10 / ENAME
HIREDATE FIRST_VALU LAST_VALUE
SMITH ALLEN WARD JONES BLAKE CLARK TURNER MARTIN KING FORD JAMES MILLER SCOTT ADAMS
17-DEC-80 SMITH 20-FEB-81 SMITH 22-FEB-81 SMITH 02-APR-81 ALLEN 01-MAY-81 ALLEN 09-JUN-81 JONES 08-SEP-81 CLARK 28-SEP-81 TURNER 17-NOV-81 TURNER 03-DEC-81 TURNER 03-DEC-81 TURNER 23-JAN-82 KING 09-DEC-82 SCOTT 12-JAN-83 SCOTT
WARD BLAKE BLAKE CLARK CLARK TURNER JAMES JAMES MILLER MILLER MILLER MILLER ADAMS ADAMS
14 rows selected. Обратившись снова к строке для сотрудника CLARK, можно убедиться, что окно начинается со строки для JONES и продолжается до строки для сотрудника TURNER. Теперь в окно входят строки для тех, кто принят на работу за 100 дней до и (а не или, как прежде) после текущего сотрудника. Итак, теперь мы хорошо знаем синтаксис четырех компонентов вызова аналитической функции. Это: •
имя функции;
•
конструкция секционирования, используемая для разбиения результирующего множества на независимые группы;
36
Глава 12
Q конструкция ORDER BY, сортирующая данные в группе для оконных функций; •
конструкция окна, задающая набор строк, к которым применяется аналитическая функция.
ИМЯ_ФУНКЦИИ(<аргумент>, <аргумент>, . . . ) OVER (<конструкция секционирования> <конструкция упорядочений <конструкция окна>) Рассмотрим кратко предлагаемые функции.
Функции Сервер предлагает более 26 аналитических функций. Имена некоторых из них совпадают с именами функций агрегирования, например AVG и SUM. Другие, с новыми именами, обеспечивают новые возможности. В этом разделе будут перечислены имеющиеся функции и кратко описано их назначение. Аналитическая функция AVG([DISTINCT выражение)
ALL]
CORR (выражение, выражение)
Назначение Используется для вычисления среднего значения выражения в пределах группы и окна. Для поиска среднего после удаления дублирующихся значений можно указывать ключевое слово DISTINCT. Выдает коэффициент корреляции для пары выражений, возвращающих числовые значения. Это сокращение для выражения: COVAR_POP(выражение!., выражение2) / STDDEV_POP(выражение!.) * STDDEV_POP(выражение2) ) . В статистическом смысле, корреляция — это степень связи между переменными. Связь между переменными означает, что значение одной переменной можно в определенной степени предсказать по значению друой. Коэффициент корреляции представляет степень корреляции в виде числа в диапазоне от -1 (высокая обратная корреляция) до 1 (высокая корреляция). Значение 0 соответствует отсутствию корреляции.
COUNT([DISTINCT] [*] [выражение])
Эта функция считает строки в группах. Если указать * или любую константу, кроме NULL, функция count будет считать все строки. Если указать выражение, функция count будет считать строки, для которых выражение имеет значение не NULL. Можно задавать модификатор DISTINCT, чтобы считать строки в группах после удаления дублирующихся строк.
СО\/АР_РОР(выражение, выражение)
Возвращает ковариацию генеральной совокупности (population covariance) пары выражений с числовыми значениями.
COVAR_SAMP (выражение, выражение)
Возвращает выборочную ковариацию (sample covariance) пары выражений с числовыми значениями.
Аналитические функции
37
Аналитическая функция
Назначение
CUME DIST
Вычисляет относительную позицию строки в группе. Функция CUME_DIST всегда возвращает число большее О и меньше или равное 1. Это число представляет "позицию" строки в группе из N строк. В группе из трех строк, например, возвращаются следующие значения кумулятивного распределения: 1/3, 2/3 и 3/3.
DENSE _RANK
Эта функция вычисляет отностельный ранг каждой возвращаемой запросом строки по отношению к другим строкам, основываясь на значениях выражений в конструкции ORDER BY. Данные в группе сортируются в соответствии с конструкцией ORDER BY, а затем каждой строке поочередно присваивается числовой ранг, начиная с 1. Ранг увеличивается при каждом изменении значений выражений, входящих в конструкцию ORDER BY. Строки с одинаковыми значениями получают один и тот же ранг (при этом сравнении значения NULL считаются одинаковыми). Возвращаемый этой функцией "плотный" ранг дает ранговые значения без промежутков. Сравните с представленной далее функцией RANK.
FIRST VALUE
Возвращает первое значение в группе.
Функция LAG дает доступ к другим строкам результирующего множества, избавляя от необходимости <смещение>, Стандартное значение>) выполнять самосоединения. Она позволяет работать с курсором как с массивом. Можно ссылаться на строки, предшествующие текущей строке в группе. О том, как обращаться к следующим строкам в группе, см. в описании функции LEAD. Смещение — это положительное целое число со стандартным значением 1 (предыдущая строка). Стандартное значение возвращается, если индекс выходит за пределы окна (для первой строки группы будет возвращено стандартное значение). LAST VALUE
Возвращает последнее значение в группе.
LEAD(выpaжeниe, Функция LEAD противоположна функции LAG. Если <смещение>, функция LAG дает доступ к предшествующим строкам <стандартное значение>) группы, то функция LEAD позволяет обращаться к строкам, следующим за текущей. Смещение - это положительное целое число со стандартным значением 1 (следующая строка). Стандартное значение возвращается, если индекс выходит за пределы окна (для последней строки группы будет возвращено стандартное значение). МАХ( выражение)
Находит максимальное значение выражения в пределах окна в группе.
ММ(выражение)
Находит минимальное значение выражения в пределах окна в группе.
38
Глава 12
Аналитическая функция
Назначение
МТ11.Е(выражение)
Делит группу на фрагменты по значению выражения. Например, если выражение = 4, то каждой строке в группе присваивается число от 1 до 4 в соответствии с фрагментом, в которую она попадает. Если в группе 20 строк, первые 5 получат значение 1, следующие 5 значение 2 и т.д. Если количество строк в группе не делится на значение выражения без остатка, строки распределяются так, что ни в одном фрагменте количество строк не превосходит минимальное количество в других фрагментах более чем на 1, причем дополнительные строки будут в группах с меньшими номера фрагмента. Например, если снова выражение = 4, а количество строк = 21, в первом фрагменте будет б строк, во втором и последующих — 5.
PERCENT RANK
Аналогична функции CUME_DIST (кумулятивное распределение). Вычисляет ранг строки в группе минус 1, деленный на количество обрабатываемых строк минус 1. Эта функция всегда возвращает значения в диапазоне от 0 до 1 включительно.
RANK
Эта функция вычисляет относительный ранг каждой строки, возвращаемой запросом, на основе значений выражений, входящих в конструкцию ORDER BY. Данные в группе сортируются в соответствии с конструкцией ORDER BY, а затем каждой строке поочередно присваивается числовой ранг, начиная с 1. Строки с одинаковыми значениями выражений, входящих в конструкцию ORDER BY, получают одинаковый ранг, но если две строки получат одинаковый ранг, следующее значение ранга пропускается. Если две строки получили ранг 1, строки с рангом 2 не будет; следующая строка в группе получит ранг 3. В этом отличие от функции DENSE_RANK, которая не пропускает значений.
RATIO_TO_REPORT (выражение)
Эта функция вычисляет значение выражение / (зит(выражение)) по строкам группы. Это дает процент, который составляет значение текущей строки по отношению к 5ит(выражение)
REGR_xxxxxxx ЭТИ функции линейной регрессии применяют стандартную (выражение, выражение) линейную регрессию по методу наименьших квадратов к паре выражений. Предлагается 9 различных функций регрессии. ROW NUMBER
Возвращает смещение строки по отношению к началу упорядоченной группы. Может использоваться для последовательной нумерации строк, упорядоченных по определенным критериям.
STDDEV( выражение)
Вычисляет стандартное (среднеквадратичное) отклонение (standard deviation) текущей строки го отношению к группе.
Аналитические функции Аналитическая функция
39
Назначение
STDDEV_POP (выражение)
Эта функция вычисляет стандартное отклонение генеральной совокупности (population standard deviation) и возвращает квадратный корень из дисперсии генеральной совокупности (population variance). Она возвращает значение, совпадающее с квадратным корнем из результата функции VAR_POP.
STDDEV_SAMP (выражение)
Эта функция вычисляет накопленное стандартное отклонение выборки (cumulative sample standard deviation) и возвращает квадратный корень выборочной дисперсии (sample variance). Она возвращает значение, совпадающее с квадратным корнем из результата функции VAR_SAMP.
5иМ(выражение)
Вычисляет общую сумму значений выражения для группы.
VAR_POP( выражение)
Эта функция возвращает дисперсию генеральной совокупности для набора числовых значений (значения NULL игнорируются). Функция VAR_POP вычисляет значение: (SUM(выражение*выражение) — SUM(выражение)*SUM(выражение) / COUNT(выражение)) / COUNT(выражение)
VAR_SAMP(выpaжeниe)
Эта функция возвращает выборочную дисперсию для набора числовых значений (значения NULL игнорируются). Она вычисляет значение: (SUM(выражение*выражение) — SUM(выражение)*SUM(выражение) / COUNT(выражение)) / (COUNT(выражение) - 1)
VARIANCE(выpaжeниe)
Возвращает дисперсию для выражения. Сервер Oracle вычисляет дисперсию как: •
0, если количество строк в группе = 1;
Q
VAR_SAMP, если количество строк в группе > 1.
Примеры Теперь можно переходить к самой интересной части — возможностям, предоставляемым аналитическими функциями. Приводимые примеры не демонстрируют все возможности, а лишь дают начальное представление.
Запрос первых N Мне часто задают вопрос: "Как получить первых N записей набора полей?". До появления аналитических функций ответить на такой вопрос было очень трудно. С запросами первых N записей, однако, бывают трудности связанные в основном с формулировкой задачи. Это надо учитывать при проектировании отчетов. Рассмотрим следующее, вполне разумное на первый взгляд требование: получить для каждого отдела трех наиболее высокооплачиваемых специалистов по продажам. Однако эта задача неоднозначна из-за возможного повторения значений: в отделе может быть четыре человека с одинаково огромной зарплатой, и что тогда делать?
40
Глава 12
Я могу предложить как минимум три одинаково разумных интерпретации этого требования, причем каждой интерпретации может соответствовать и не три записи! Требование можно интерпретировать так. •
Выдать список специалистов по продажам, имеющих одну из трех максимальных зарплат. Другими словами, найти все различные значения зарплаты, отсортировать, выбрать три наибольших, и вернуть всех сотрудников, зарплата которых совпадает с одним из этих трех значений.
Q Выдать до трех человек с максимальными зарплатами. Если четыре человека имеют одинаковую максимальную зарплату, в ответ не должно выдаваться ни одной строки. Если два сотрудника имеют максимальную зарплату и два — следующую по значению, ответ будет предполагать две строки (два сотрудника с максимальной зарплатой). Q Отсортировать специалистов по продажам по убыванию зарплат. Вернуть первые три строки. Если в отделе менее трех специалистов по продажам, в результате будет менее трех записей. После дополнительных вопросов и уточнений оказывается, что некоторым необходима первая интерпретация; другим — вторая или третья. Давайте рассмотрим, как с помощью аналитических функций сформулировать все три запроса, и как это делалось без них. Для этих примеров используем таблицу SCOTT.EMP. Сначала реализуем запрос "Выдать список специалистов по продажам в каждом отделе, имеющих одну из трех максимальных зарплат": scott@TKYTE816> select * ю,, ename, 2 from (select deptno, ename, sal, sal, dense rank() 3 4 over (partition by deptno 5 order by sal desc) 6 dr from emp) 7 where dr <= 3 8 order by deptno, sal (iesc 9 / DEPTNO ENAME
SAL
DR
10 KING CLARK MILLER
5000 2450 1300
1 2 3
20 SCOTT FORD JONES ADAMS
3000 3000 2975 1100
1 1 2 3
30 BLAKE ALLEN TURNER
2850 1600 1500
1 2 3
10 rows selected.
Аналитические функции
41
Здесь для получения трех максимальных зарплат была использована функция DENSE_RANK(). Мы присвоили записям непрерывные ранговые значения по столбцу sal и отсортировали результат по убыванию. Если обратиться к описанию функций, оказывается, что при непрерывном ранжировании значения ранга не пропускаются и две строки с одинаковыми значениями получают одинаковый ранг. Поэтому после построения результирующего множества в виде подставляемого представления, можно просто выбирать все строки с "плотным" рангом не более трех. В результате для каждого отдела будут получены все сотрудники с одной из трех максимальных зарплат в отделе. Для сравнения выберем функцию RANK и сравним, что происходит при обнаружении дублирующихся значений: scott@TKYTE816> s e l e c t deptno, ename, s a l , 2 dense_rank() 3 over (partition by deptno 4 order by sal desc) dr, 5 rank() 6 over (partition by deptno 7 order by sal desc) r 8 from emp 9 order by deptno, sal desc 10 / DEPTNO ENAME
SAL
DR
10 KING CLARK MILLER
5000 2450 1300
1 2 3
1 2 3
20 SCOTT FORD JONES ADAMS SMITH
3000 3000 2975 1100 800
1 1 2 3 4
1 1 3
30 BLAKE ALLEN TURNER WARD MARTIN JAMES
2850 1600 1500 1250 1250 950
1 2 3 4 4 5
1 2 3 4 4 6
4 5
14 rows selected. Если бы использовалась функция RANK, сотрудник ADAMS (получивший ранг 4) не вошел бы в результирующее множество, но он — один из сотрудников отдела 20, получивших одну из трех максимальных зарплат, так что в результат он попадать должен. В данном случае использование функции RANK вместо DENSE_RANK привело бы к неправильному ответу на поставленный вопрос. Наконец, пришлось использовать вложенное представление и задать псевдоним DR для результатов аналитической функции dense_rank(). Дело в том, что нельзя использовать аналитические функции в конструкциях WHERE или HAVING непосредственно, так что пришлось выбрать результат в представление, а затем отфильтровать, оставив
42
Глава 12
только необходимые строки. Использование подставляемого представления с условием — типичная конструкция для многих примеров в этой главе. Теперь вернемся к запросу "Выдать не более трех человек с максимальными зарплатами по каждому отделу": scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 count(*) over (partition by deptno 4 order by sal desc 5 range unbounded preceding) 6 cnt from emp) 7 where cnt <= 3 8 order by deptno, sal desc 9 / DEPTNO ENAME
SAL
CNT
10 KING CLARK MILLER
5000 2450 1300
1 2 3
20 SCOTT FORD JONES
3000 3000 2975
2 2 3
30 BLAKE ALLEN TURNER
2850 1600 1500
1 2 3
9 rows selected. Этот запрос немного нетривиален. Мы подсчитываем все записи в окне, предшествующие текущей, при сортировке по зарплате. Диапазон RANGE UNBOUNDED PRECEDING задает окно, включающее все записи, зарплата в которых больше или равна зарплате в текущей записи, поскольку сортировка выполнена по убыванию (DESC). Подсчитывая всех сотрудников с такой же или более высокой зарплатой, можно выбирать только строки, в которых значение этого количества (CNT), меньше или равно 3. Обратите внимание, что в отделе 20 для сотрудников SCOTT и FORD возвращается значение 2. Оба они получили наибольшую зарплату в отделе, так что попадают в окно друг для друга. Интересно отметить небольшое отличие, которое дает следующий запрос: scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 count(*) over (partition by deptno 4 order by sal desc, ename 5 range unbounded preceding) 6 cnt from emp) 7 where cnt <= 3 8 order by deptno, sal desc 9 / DEPTNO ENAME 10 KING
SAL
CNT
5000
1
Аналитические функции CLARK MILLER
2450 1300
2 3
20 FORD SCOTT JONES
3000 3000 2975
1 2
30 BLAKE ALLEN TURNER
2850 1600 1500
1 2 3
43
9 rows selected. Обратите внимание, как добавление столбца в конструкцию ORDER BY повлияло на окно. Ранее сотрудники FORD и SCOTT оба имели в столбце CNT значение 2. Причина в том, что окно строилось исключительно по столбцу зарплаты. Более избирательное окно дает другие результаты функции COUNT. Я привел этот пример, чтобы подчеркнуть, что функция окна зависит от обеих конструкций, ORDER BY и RANGE. Если секция сортировалась только по зарплате, строка для сотрудника FORD предшествовала строке для SCOTT, когда строка для SCOTT была текущей, а строка для SCOTT, в свою очередь, предшествовала строке для FORD, когда та была текущей. Только при сортировке по столбцам SAL и ENAME можно однозначно упорядочить строки для сотрудников SCOTT и FORD по отношению друг к другу. Чтобы убедиться, что этот подход, предусматривающий использование функции COUNT, позволяет возвращать не более трех записей, давайте изменим данные так, чтобы максимальная зарплата была у большего числа сотрудников отдела: scott@TKYTE816> update emp set sal = 99 where deptno = 30; 6 rows updated. scott@TKYTE816> s e l e c t * 2 from ( s e l e c t deptno, 3 count(*) over 4 5 6 cnt from emp) 7 where cnt <= 3 8 order by deptno, s a l DEPTNO ENAME
ename, s a l , ( p a r t i t i o n by deptno order by s a l desc range unbounded preceding) desc SAL
10 KING CLARK MILLER
5000 2450 1300
20 SCOTT FORD JONES
3000 3000 2975
CNT
6 rows selected. Теперь строк для отдела 30 в отчете нет, поскольку 6 сотрудников этого отдела имеют одинаковую зарплату. В поле CNT для всех них находится значение 6, которое никак не меньше или равно 3.
44
Глава 12
Перейдем теперь к последнему запросу: "Отсортировать специалистов по продажам по убыванию зарплат и вернуть первые три строки". Это легко сделать с помощью функции ROW_NUMBER(): scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 row_number() over (partition by deptno 4 order by sal desc) 5 rn from emp) 6 where rn <= 3 7 / DEPTNO ENAME
SAL
RN
10 KING CLARK MILLER
5000 2450 1300
1 2 3
20 SCOTT FORD JONES
3000 3000 2975
1 2 3
99 99 99
1 2 3
30 ALLEN BLAKE MARTIN 9 rows selected.
При выполнении запроса каждая секция сортируется по убыванию значений зарплат, после чего по мере обработки каждой строке секции присваивается последовательный номер. После этого с помощью конструкции WHERE мы получаем только первые три строки каждой секции. В примере с транспонированием результирующего множества мы используем такой же прием для преобразования строк в столбцы. Следует отметить, однако, что для отдела DEPTNO=30 возвращаются в определенном смысле случайные строки. Если помните, информация в отделе 30 была изменена так, что все 6 сотрудников получили значение 99 в столбце зарплаты. Можно в некоторой степени управлять тем, какие три записи будут возвращаться, с помощью конструкции ORDER BY. Например, можно использовать конструкцию ORDER BY SAL DESC, ENAME для получения упорядоченной по фамилии информации о наиболее высокооплачиваемых сотрудниках, если несколько из них имеют одинаковую зарплату. Интересно отметить, что с помощью функции ROW_NUMBER можно получать произвольную секцию данных из группы строк. Это может пригодиться в среде, не поддерживающей информацию о состоянии, когда надо выдавать данные постранично. Например, если необходимо выдавать данные из таблицы ЕМР, отсортированные по столбцу ENAME, группами по пять строк, можно использовать запрос следующего вида: scott@TKYTE816> s e l e c t ename, h i r e d a t e , s a l 2 from ( s e l e c t ename, h i r e d a t e , s a l , 3 row_number() over (order by ename) 4 rn from emp) 5 where rn between 5 and 10 6 order by rn 7 /
Аналитические функции ENAME
HIREDATE
FORD JAMES JONES KING MARTIN MILLER
03-DEC-81 03-DEC-81 02-APR-81 17-NOV-81 28-SEP-81 23-JAN-82
45
SAL 3000 950. 2975 5000 1250 1300
6 rows selected.
И напоследок, чтобы продемонстрировать всю мощь аналитических функций, сравним запросы с аналитическими функциями с такими же запросами, где эти функции не используются. Для сравнения я создал таблицу Т, являющуюся увеличенной во всех смыслах разновидностью таблицы ЕМР: scott@TKYTE816> create table t 2 as 3 select object_name ename, 4 mod(object_id,50) deptno, 5 object_id sal 6 from all_objects 7 where rownum О 1000 8 / Table created. scott@TKYTE816> create index t_idx on t(deptno,sal desc); Index created. scott@TKYTE816> analyze table t 2 compute statistics 3 for table 4 for all indexed columns 5 for all indexes 6 / Table analyzed. Мы создали индекс по этой таблице, позволяющий ответить на запросы, которые мы будем к ней выполнять. Теперь сравним тексты и производительность запросов с аналитическими функциями и без них. Для сравнения производительности я использовал установки SQLJTRACE, TIMED_STATISTICS и утилиту TKPROF. Подробнее об этих средствах и интерпретации результатов см. в главе 10, посвященной стратегиям и средствам настройки производительности: scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 dense_rank() over (partition by deptno 4 order by sal desc) 5 dr from t) 6 where dr <= 3 7 order by deptno, sal desc 8 /
46
Глава 12 count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 2 11
0.00 0.00 0.01
0.00 0.00 0.07
0 0 7
0 0 10
0 0 17
0
0 150
total
14
0.01
0.07
10
17
150
call
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows
Row Source Operation
150 VIEW 364 WINDOW SORT PUSHED RANK 1000 TABLE ACCESS FULL T ************************************ scott@TKYTE816> select deptno, ename, sal 2 from t el 3 where sal in (select sal from (select distinct sal , deptno 4 from t e3 5 6 order by deptno, sal desc) e2 7 where e2.deptno = el.deptno 8 and rownum <= 3) 9 order by deptno, sal desc 10 call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 1 11
0. 00 0. 00 0. 80
0 .00 0 .00 0 .80
0 0 0
0 0 10010
0 0 12012
0 0 150
total
13
0.80
0.80
0
10010
12012
150
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows 150 150 1001 1000 3700 2850 2850 20654 20654
Row Source Operation SORT ORDER BY FILTER TABLE ACCESS FULL T FILTER COUNT STOPKEY VIEW SORT ORDER BY STOPKEY SORT UNIQUE TABLE ACCESS FULL T
Аналитические функции
4У
Оба представленных выше запроса возвращают данные о трех сотрудниках, которые получают наибольшие зарплаты. Запрос, использующий аналитические функции, позволяет получить эти сведения без особых усилий: для этого требуется 0,01 секунды процессорного времени и 27 логических операций ввода-вывода. А реляционный запрос требует выполнения более 22000 логических операций ввода-вывода, и на это уходит 0,80 секунды процессорного времени. Для выполнения запроса, не использующего аналитические функции, необходимо выполнять подзапрос для каждой строки в таблице Т, чтобы найти три наибольших зарплаты в данном отделе. Этот запрос не только медленнее выполняется, но и сложен для написания. Его производительность можно повысить с помощью подсказок, но при этом он стал бы еще менее понятным и более "хрупким". (Как любой запрос, использующий подсказки: подсказка — это всего лишь предложение, и оптимизатор может его проигнорировать). Запрос с аналитическими функциями, определенно, выигрывает как по производительности, так и по простоте. Теперь переходим ко второму вопросу: выдать до трех человек с максимальными зарплатами. scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 count(*) over (partition by deptno 4 order by sal desc 5 range unbounded preceding) 6 cnt from t) 7 where cnt <= 3 8 order by deptno, sal desc 9 / call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 2 11
0.01 0.00 0.02
0.01 0.00 0.12
0 0 15
0 0 10
0 0 17
0 0 150
total
14
0.03
0.13
15
10
17
150
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows
Row Source Operation
150 VIEW 1000 WINDOW SORT 1000 TABLE ACCESS FULL T ************************************* scott@TKYTE816> select deptno, ename, sal 2 from t el 3 where (select count(*) 4 from t e2 5 where e2.deptno = el.deptno 6 and e2.sal >= el.sal) <= 3
48
Глава 12 7 8
order by deptno, sal desc /
call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 1 11
0.01 0.00 0.60
0 .01 0 .00 0 .66
0 0 0
0 0 4010
0 0 4012
0 0 150
total
13
0.61
0.67
0
4010
4012
150
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows 150 150 1001 2000 10827
Row Source O p e r a t i o n SORT ORDER BY FILTER TABLE ACCESS FULL T SORT AGGREGATE INDEX FAST FULL SCAN ( o b j e c t i d 27867)
И на этот раз результаты говорят сами за себя: 0,03 секунды процессорного времени и 27 логических операций ввода-вывода по сравнению с 0,61 секунды процессорного времени и более чем 8000 логических операций ввода-вывода. Снова явное преимущество имеет запрос, использующий аналитические функции. Причина и на этот раз в том, что при отсутствии аналитических функций для каждой строки базовой таблицы приходится выполнять коррелированный подзапрос. Этот подзапрос подсчитывает количество записей в том же отделе для сотрудников с такой же или более высокой зарплатой. Выбираются только записи, для которых это количество меньше или равно 3. Результаты запросов одинаковы, но используемые во время выполнения ресурсы принципиально различны. Составить оба запроса одинаково несложно, но производительность при использовании аналитических функций несравнимо выше. Наконец, необходимо получить для каждого отдела первых три сотрудника с наибольшими зарплатами. Результаты следующие: scott@TKYTE816> s e l e c t deptno, ename, s a l 2 from t el 3 where (select count(*) 4 from t e2 5 where e2.deptno = el.deptno 6 and e2.sal >= el.sal ) <=3 7 order by deptno, sal desc 8 / call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 2 11
0.00 0.00 0.00
0.00 0.00 0.12
0 0 14
0 0 10
0 0 17
0 0 150
total
14
0.00
0.12
14
10
17
150
Аналитические функции
49
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows
Row Source Operation
150 VIEW 1000 WINDOW SORT 1000 TABLE ACCESS FULL T ************************************* scott@TKYTE816> select deptno, ename, sal 2 from t el 3 where (select count(*) 4 from t e2 5 where e2.deptno = el.deptno 6 and e2.sal >= el.sal 7 and (e2.sal > el.sal OR e2.rowid > el.rowid)) < 3 8 order by deptno, sal desc 9 / call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 1 11
0.00 0.00 0.88
0.00 0.00 0.88
0 0 0
0 0 4010
0 0 4012
0 0 150
total
13
0.88
0.88
0
4010
4012
150
Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 54 Rows 150 150 1001 2000 9827
Row Source Operation SORT ORDER BY FILTER TABLE ACCESS FULL T SORT AGGREGATE INDEX FAST FULL SCAN ( o b j e c t i d 27867)
И на этот раз производительность запросов несравнима. Версия с аналитической функцией во много раз превосходит по производительности реляционный запрос. Кроме того, версию с использованием аналитических функций в данном случае написать намного проще. Созданный коррелированный подзапрос весьма сложен. Необходимо подсчитать количество сотрудников в отделе, у которых зарплата больше или равна зарплате в текущей записи. Кроме того, если зарплата не больше зарплаты в текущей записи (совпадает с ней), такую запись можно учитывать, только если значение ROWID (или любого столбца с уникальными значениями) больше, чем в текущей записи. Это гарантирует, что строки не будут считаться дважды, а каждый раз будет выбираться другой набор строк.
50
Глава 12
Во всех случаях оказалось, что аналитические функции не только упрощают написание сложных запросов, но и могут существенно повысить производительность. Они позволяют делать то, что не стоит запрашивать с помощью "чистого" языка SQL из-за неэффективности выполнения.
Запрос с транспонированием При запросе с транспонированием (опорный запрос — pivot query) берутся данные вида: Cl
C2
СЗ
al al al
Ы Ы Ы
xl х2 хЗ
и выдаются в следующем виде: С1
С2
С3(1)
С3(2) С3(3)
al
Ы
xl
х2
хЗ
Этот запрос преобразует строки в столбцы. Например, можно выдать должности сотрудников отдела в виде столбцов: DEPTNO
JOB I 10 CLERK 20 ANALYST 30 CLERK
JOB 2
JOB 3
MANAGER ANALYST MANAGER
PRESIDENT CLERK SALESMAN
а не в виде строк: DEPTNO
JOB 10 10 10 20 20 20 30 30 30
CLERK MANAGER PRESIDENT ANALYST CLERK MANAGER CLERK MANAGER SALESMAN
Я представлю два примера запросов с транспонированием. Первый — разновидность описанного выше запроса трех сотрудников с максимальными зарплатами. Второй пример показывает, как транспонировать любое результирующее множество, и дает шаблон необходимых для этого действий. Предположим, необходимо выдать фамилии сотрудников отдела с тремя наибольшими зарплатами в виде столбцов. Запрос должен возвращать ровно одну строку для каждого отдела, причем в строке должно быть 4 столбца: номер отдела (DEPTNO), фамилия сотрудника с наибольшей зарплатой в отделе, фамилия сотрудника со следующей
Аналитические функции
по величине зарплатой и т.д. С помощью новых аналитических функций это сделать просто (а до их появления — практически невозможно): ops$tkyte@DEV816> s e l e c t deptno, 2 max(decode(seq,l,ename,null)) highest_paid, 3 max(decode(seq,2,ename,null)) second_highest, 4 max(decode(seq,3,ename,null)) t h i r d _ h i g h e s t 5 from (SELECT deptno, ename, 6 row_number() OVER 7 (PARTITION BY deptno 8 ORDER BY s a l desc NULLS LAST) seq 9 FROM emp) 10 where seq <= 3 11 group by deptno 12 / DEPTNO HIGHEST PA SECOND HIG THIRD HIGH 10 KING
CLARK
MILLER
20 SCOTT 30 BLAKE
FORD ALLEN
JONES TURNER
Мы создали внутреннее результирующее множество, где сотрудники отделов пронумерованы по убыванию зарплат. Функция decode во внешнем запросе оставляет только строки со значениями номеров 1, 2 или 3 и присваивает взятые из них фамилии соответствующему столбцу. Конструкция GROUP BY позволяет избавиться от лишних строк и получить сжатый результат. Возможно, понять, что я имею в виду, проще, если сначала посмотреть на результирующее множество запроса без конструкций GROUP BY и МАХ: scott@TKYTE816> s e l e c t deptno, 2 decode(seq,1,ename,null) highest_paid, 3 decode(seq,2,ename,null) second_highest, 4 decode(seq,3,ename,null) t h i r d _ h i g h e s t 5 from ( s e l e c t deptno, ename, 6 row_number() over 7 ( p a r t i t i o n by deptno 8 order by s a l desc n u l l s l a s t ) 9 seq from emp) 10 where seq <= 3 11 / DEPTNO HIGHEST PA SECOND HIG THIRD HIGH 10 KING 10 10
CLARK MILLER
20 SCOTT
20 20
30 ALLEN 30
FORD
JONES BLAKE
52
Глава 12
30
MARTIN
9 rows s e l e c t e d . Функция агрегирования MAX будет применяться конструкцией группировки GROUP BY по столбцу DEPTNO. Значение в столбце HIGHEST_PAID для отдела только в одной строке будет непустым — в остальных строках этот столбец всегда будет иметь значение NULL. Функция МАХ будет выбирать только строку с непустым значением. Поэтому сочетание группирования и функции МАХ позволит, убрав значения NULL, "свернуть" результирующее множество и получить желаемый результат. Если есть таблица Т со столбцами С1 и С2 и необходимо получить результат вида: С1
С2(1)
С2(2)
C2(N),
где столбец С1 должен присутствовать во всех строках (значения выдаются по направлению к концу страницы), а столбец С2 должен быть транспонирован так, чтобы он представлялся в виде строк (значения С2 выдаются по направлению к концу строки, они становятся столбцами, а не строками), надо создать такой запрос: Select cl, max(decode(rn,1,с2,null)) с2_1, max(decode(rn,2,с2,null)) c2 2,
— max(decode(rn,N,c2,null)) c2_N from ( s e l e c t c l , c2, row_number() over ( p a r t i t i o n by Cl order by <столбцы>) rn from T <условие>) group by Cl В представленном выше примере в качестве С1 использовался столбец DEPTNO, a в качестве С2 — ENAME. Поскольку упорядочение выполнялось по критерию SAL DESC, первые три полученные строки соответствовали трем наиболее высокооплачиваемым сотрудникам соответствующего отдела (напоминаю: если максимальные зарплаты получало четыре человека, одного из них мы теряем). Второй пример: транспонировать результирующее множество. Рассмотрим более общий случай, когда опорный (отсюда и второе название запроса — опорный) столбец, С1, и транспонируемый столбец, С2, представляют собой наборы столбцов. Решение очень похоже на то, что представлено выше. Предположим, необходимо для каждого отдела и должности выдать фамилии и зарплаты сотрудников. При этом в отчете фамилии и соответствующие зарплаты должны выдаваться в строке, как столбцы. Кроме того, в строке сотрудников надо упорядочивать слева направо по возрастанию зарплат. Для решения этой проблемы необходимо выполнить следующее: scott@TKYTE816> s e l e c t max(count(*)) from emp group by deptno, j o b ; MAX(COUNT(*)) 4
В результате мы получаем количество столбцов. Теперь можно создавать запрос:
Аналитические функции
53
scott@TKYTE816> select deptno, job, max(decode(rn, 1, ename , null)) ename_l, 2 з max(decode(rn, 1, sal, null)) sal 1, 4 max(decode(rn, 2, ename , null)) ename 2, 5 max(decode(rn, 2, sal, null)) sal_2, 6 max(decode(rn, 3, ename , null)) ename_3, 7 max(decode(rn, 3, sal, null)) sal 3, max(decode(rn, 4, ename , null)) ename 4, 8 9 max(decode(rn, 4, sal, null)) sal 4 10 from (select deptno', job,ename, sal, 11 row number( ) over (partition by deptno, job 12 order by sal, ename) rn from emp) 13 14 group by deptno, job 15 / DEPTNO JOB
ENAME_ LSALJL ENAME_ 2 SAL_2 ENAME 3 SAL_3 ENAME 4 SAL_4
10 CLERK MILLER 10 MANAGER CLARK 10 PRESIDENT KING
1300 2450 5000
20 ANALYST 20 CLERK 20 MANAGER
FORD SMITH JONES
3000 SCOTT 800 ADAMS 2975
30 CLERK 30 MANAGER 30 SALESMAN
JAMES BLAKE ALLEN
99 99 99 MARTIN
3000 1100
99 TURNER
99 WARD
99
9 rows selected.
Ранее в этой главе м ы установили значение зарплаты 99 для сотрудников отдела 30. Для транспонирования произвольного результирующего множества можно пойти еще дальше. Если имеется набор столбцов С1, С2, СЗ, ... C N и значения столбцов С 1 ... С х должны выдаваться во всех строках, а значения столбцов Сх+1 ... C N — в виде столбцов каждой строки, запрос будет иметь такой синтаксис: Select Cl, C2, ... СХ, max(decode(rn,l,C{X+l},null)) max(decode(rn,2,C{X+l},null))
сх+1_1,...max(decode(rn,l,CN,null)) CN_1 cx+l_2,...max(decode(rn,l,CN,null)) CN_2
max(decode(rn,N,c{X+l},null)) cx+l_N,...max(decode(rn,l,CN,null)) CN_N from ( s e l e c t Cl, C2, . . . CN row_number() over ( p a r t i t i o n by Cl, C2, . . . CX order by <столбцы>) rn from T <условие>) group by Cl, C2, . . . CX В предыдущем примере в качестве Cl использовался столбец DEPTNO, в качестве С2 — JOB, СЗ представлял столбец ENAME, a C4 — SAL. Для создания подобного запроса надо знать максимальное количество строк, которое может быть в фрагменте. Оно определяет количество генерируемых столбцов. В SQL
54
Глава 12
необходимо знать количество выбираемых столбцов, т.к. в противном случае мы не сможем транспонировать результирующее множество. Таким образом, можно привести еще более общий пример создания запроса с транспонированием. Если заранее, до выполнения, общее количество столбцов не известно, придется использовать динамический SQL, чтобы справиться с переменным списком выбора в операторе SELECT. Для демонстрации этого можно написать PL/SQL-процедуру; в результате мы получим универсальную процедуру для транспонирования любого результирующего множества. Эта процедура (я поместил ее в пакет) будет иметь следующую спецификацию: scott@TKYTE816> c r e a t e or replace package my_pkg 2 as 3 type refcursor is ref cursor; 4 type array is table of varchar2(30); 5 procedure pivot(p_max_cols in number default NULL, 6 p_max_cols_query in varchar2 default NULL, 7 p_query in varchar2, 8 p_anchor in array, 9 p_pivot in array, 10 p_cursor in out refcursor); 12 end; Package created. Необходимо задать значения для параметра P_MAX_COLS или для параметра P_MAX_COLS_QUERY. Для создания SQL-оператора необходимо знать количество столбцов в запросе, и эти параметры позволят создать запрос с соответствующим количеством столбцов. Задавать этим параметрам надо значение, полученное в результате выполнения такого запроса: scott@TKYTE816> s e l e c t max(count(*)) from emp group by deptno, job; Он возвращает количество различных значений в строках, которые мы хотим транспонировать. Можно либо получить это количество с помощью подобного запроса, либо просто ввести его, если оно заранее известно. Параметр P_QUERY — это запрос, собирающий данные. Для представленного выше примера можно передать следующий запрос: 10 11 12 13
from (select deptno, job, ename, sal, row_number() over (partition by deptno, job order by sal, ename) rn from emp)
Следующие два параметра — массивы имен столбцов. Параметр P_ANCHOR указывает, значения каких столбцов остаются в строках, а параметр P_PIVOT перечисляет столбцы, значения которых выносятся в строки. В рассмотренном ранее примере 1 1 P_ANCHOR = ('DEPTNO , "JOB ), a P_PIVOT = CENAME','SAL'). Отвлечемся ненадолго от нашей темы и рассмотрим, как может выглядеть вызов процедуры транспонирования результирующего множества: scott@TKYTE816> v a r i a b l e x refcursor scott@TKYTE816> s e t a u t o p r i n t on
Аналитические функции
55
scott@TKYTE816> begin 2 my_pkg.pivot 3 (p_max_cols_query => 'select max(count(*)) from emp 4 group by deptno,job', 5 p_query => 'select deptno, job, enarne, sal, б row_number() over (partition by deptno, job 7 order by sal, ename) 8 rn from emp a', 9 p_anchor => my_j?kg. array (' DEPTNO', 1 JOB'), 10 11 p_pivot => myjpkg.array('ENAME', 1 SAL'), 12 p_cursor => :x) ; 13 end; PL/SQL procedure successfully completed. DEPTNO JOB 10 10 10 20 20 20 30 30 30
CLERK MANAGER PRESIDENT ANALYST CLERK MANAGER CLERK MANAGER SALESMAN
ENAME MILLER CLARK KING FORD SMITH JONES JAMES BLAKE ALLEN
SAL 1 ENAME 2 SAL 2 ENAME 3 SAL 3 ENAME 4 SAL 4 1300 2450 5000 3000 SCOTT 800 ADAMS 2975 99 99 99 MARTIN
3000 1100
99 TURNER
99 WARD
99
9 rows selected. Как видите, запрос динамически переписан на базе разработанного универсального шаблона. Реализация тела пакета достаточно проста: scott@TKYTE816> create or replace package body myjpkg 2 as 3 4 procedure pivot(p_max_cols in number default null, p_max_cols_query in varchar2 default null, 5 p_query in varchar2, 6 _anchor in array, 7 r_" p_pivot in array, 8 p cursor in out refcursor) 9 10 11 l_max_cols number; 12 l_query long; 13 l_cnames array; 14 begin 15 — определяем количество столбцов, которые надо возвращать 16 — мы либо ЗНАЕМ его, либо получаем запрос, с помощью которого его ^ можно узнать 17 if (p_max_cols is not null) 18 then 19 l_max_cols := p_max_cols; 20 elsif (p_max_cols_query is not null)
56
Глава 12 21 22 23 24 25 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
then execute immediate p_max_cols_query into l_max_cols; else raise_application_error(-20001, 'He могу определить максимальное количество столбцов'); end if;
— Теперь создаем запрос, который позволяет ответит на ••• поставленный вопрос. . . — Начинаем со столбцов Cl, C2, ... СХ: l_query := 'select '; for i in 1 .. p_anchor.count loop l_query := l_query || p_anchor(i) || ','; end loop; — Теперь добавляем транспонируемые столбцы С{х+1}... CN: — Ф о р м а т : "max(decode(rn,1,C(X+1),null)) сх+1_1" for i in 1 .. l_max_cols loop for j in 1 .. p_pivot.count loop l_query := l_query || 'max(decode(rn, '||i||', '|| pjpivot(j)|| p_j>ivot(j) | end loop; end loop; —
Теперь просто добавляем исходный запрос
l_query := rtrim(l_query, ', ') | | ' from ( ' | | p_query | | ') group by ', —
а затем — столбцы, по которым надо группировать...
for i in 1 .. p_anchor.count loop l_query : = l_query || p_anchor(i) || ','; end loop; l_query := rtrim(l_query, ', ' ) ; —
и возвращаем курсор для результирующего множества
execute immediate 'alter session set cursor_sharing=force'; open p_cursor for l_query; execute immediate 'alter session set cursor_sharing=exact'; end;
Аналитические функции
70 71 end; 72 / Package body created. Понадобилось несколько строковых функций для перезаписи запроса и динамическое открытие курсорной переменной (REF CURSOR). Поскольку вполне вероятно, что в условии запроса есть константы, мы включаем опцию cursor_sharing перед анализом запроса, чтобы принудительно использовались связываемые переменные, а затем отключаем ее. (Подробнее об этом см. в главе 10, посвященной настройке производительности). В результате получаем полностью проанализированный запрос, готовый для извлечения данных через курсорную переменную.
Доступ к строкам вокруг текущей строки Часто необходимо обращаться к данным не только в текущей строке, но и в ближайших предыдущих или последующих. Предположим, необходимо создать отчет, в котором по отделам были бы представлены все сотрудники, причем, для каждого сотрудника выдана дата его приема на работу, за сколько дней до этой даты последний раз принимали сотрудника на работу, и через сколько дней после этого приняли на работу следующего сотрудника. Написание подобного запроса с помощью "чистого" языка SQL — чрезвычайно сложная задача. Более того, производительность полученного запроса вызывает сомнения. В прошлом я либо пытался применять прием "select из select", либо писал PL/SQL-функцию, которая по данным из текущей строки находила предыдущую и следующую строки данных. Это работало, но очень много времени уходило на разработку запроса (приходилось писать больше кода); кроме того, расходовалось большое количество ресурсов при его выполнении. С помощью аналитических функций это делается быстро и эффективно. Соответствующий запрос будет выглядеть так: scott@TKYTE816> s e l e c t deptno, ename, h i r e d a t e , 2 lag(hiredate, 1, null) over (partition by deptno 3 order by hiredate, ename) last hire, 4 hiredate — lag(hiredate, 1, null) over (partition by deptno 5 6 order by hiredate, ename) days last, 7 lead(hiredate, 1, null) 8 over (partition by deptno 9 order by hiredate, ename) next hire, 10 leadfhiredate, 1, null) 11 over (partition by deptno 12 order by hiredate, ename) — hiredate days next 13 from emp 14 order by deptno, hiredate 15 / DEPTNO ENAME 10 CLARK KING
HIREDATE
LAST_HIRE
09-JUN-81 17-NOV-81 09-JUN-81
DAYS_LAST NEXT_HIRE DAYS_NEXT 17-NOV-81 161 23-JftN-82
161 67
JO
Глава 12 MILLER
23-JAN-82 17-NOV-81
67
20 SMITH JONES FORD SCOTT ADAMS
17-DEC-80 02-APR-81 03-DEC-81 09-DEC-82 12-JAN-83
17-DEC-80 02-APR-81 03-DEC-81 09-DEC-82
02-APR-81 106 03-DEC-81 245 09-DEC-82 371 12-JAN-83 34
106 245 371 34
30 ALLEN WARD BLAKE TURNER MARTIN JAMES
20-FEB-81 22-FEB-81 01-MAY-81 08-SEP-81 28-SEP-81 03-DEC-81
20-FEB-81 22-FEB-81 01-MAY-81 08-SEP-81 28-SEP-81
2 68 130 20 66
22-FEB-81 01-MAY-81 08-SEP-81 28-SEP-81 03-DEC-81
2 68 130 20 66
14 rows selected. Функции LEAD и LAG можно рассматривать как способы индексации в пределах группы. С помощью этих функций можно обратиться к любой отдельной строке. Обратите внимание: в представленных выше результатах запись для сотрудника KING включает данные (выделены полужирным) из предыдущей строки (LAST_HIRE) и последующей (NEXT_HIRE). Можно получить поля предыдущих или последующих записей в упорядоченной секции. Прежде чем подробно рассматривать функции LAG и LEAD, я хотел бы сравнить этот запрос с аналогичным по результатам запросом, в котором не используются аналитические функции. Для этого я создам необходимые индексы по таблице, чтобы максимально быстро получать ответ: scott@TKYTE816> c r e a t e t a b l e t 2 as 3 select object_name ename, 4 created hiredate, 5 mod(object_id,50) deptno 6 from all_objects 7 / Table created. scott@TKYTE816> alter table t modify deptno not null; Table altered. scott@TKYTE816> create index t_idx on t(deptno,hiredate,ename) 2 / Index created. scott@TKYTE816> analyze table t 2 compute statistics 3 for table 4 for all indexes 5 for all indexed columns 6 / Table analyzed.
59
Аналитические функции
Я даже добавил в индекс столбец E N A M E , чтобы при выполнении запроса достаточно было обращения только к индексу, без обращения к таблице по значению R O W I D . Запрос с аналитической функцией демонстрирует следующую производительность: scott@TKYTE816> select deptno, ename, hiredate, 2 lag(hiredate, 1, null) over (partition by deptno 3 order by hiredate, ename) last_hire, 4 hiredate — lag(hiredate, 1, null) 5 over (partition by deptno 6 order by hiredate, ename) days_last, 7 lead(hiredate, 1, null) 8 over (partition by deptno 9 order by hiredate, ename) next_hire, 10 lead(hiredate, 1, null) 11 over (partition by deptno 12 order by hiredate, ename) — hiredate days_next 13 from emp 14 order by deptno, hiredate 15 / call
count
cpu
elapsed
disk
query
current
rows
Parse Execute2 Fetch
1 0.00 1313
0.01 0.00 0.72
0 .01 0 1 .57
0 0 142
0 0 133
0 0 2
0 19675
total
1316
0.73
1.58
142
133
2
19675
Misses in library cache during parse: 0 Optimizer goal: FIRST_ROWS Parsing user id: 54 Rows 19675 19675
Row Source Operation WINDOW BUFFER INDEX FULL SCAN (object id 27899)
Сравним с эквивалентным запросом, где аналитические функции не используются: scott@TKYTE816> select deptno, ename, hiredate, 2 hiredate-(select max(hiredate) 3 from t e2 4 where e2.deptno = el.deptno 5 and e2.hiredate < el.hiredate) last_hire, 6 hiredate-(select max(hiredate) 7 from t e2 8 where e2.deptno - el.deptno 9 and e2.hiredate < el.hiredate) days_last, 10 (select min(hiredate) 11 from t e3 12 where e3.deptno = el.deptno 13 and e3.hiredate > el.hiredate) nextjhire, 14 (select min(hiredate) 15 from t e3 16 where e3.deptno = el.deptno
60
Глава 12 17 and e3.hiredate > el.hiredate) — hiredate days_next 18 from t el 19 order by deptno, hiredate 20 / call
count
cpu
elapsed
disk
query
current
Parse Execute Fetch
1 1 1313
0.01 0.00 2.48
0 .01 0 .00 2 .69
0 0 0
0 0 141851
0 0 0
0 0 19675
total
1315
2.49
2.70
0
141851
0
19675
Misses in library cache during parse: 0 Optimizer goal: FIRST_ROWS Parsing user id: 54 Rows 19675
Row Source Operation INDEX FULL SCAN (object i d 27899)
Производительность этих двух запросов существенно отличается. Сравните: 135 логических операций ввода-вывода и 141000; 0,73 секунды процессорного времени и 2,49. Запрос с аналитической функцией и в этом случае оказался намного эффективнее, Учтите также сложность текста запросов. Мне кажется, запрос с помощью функций LAG и LEAD не только проще написать, но и понять впоследствии, что выбирается. Прием "select из select" — хороший трюк, но такой код сложнее придумать, а при чтении полученного запроса часто очень трудно понять, что он выбирает. Чтобы восстановить логику второго запроса, придется намного больше думать. Теперь давайте более детально рассмотрим функции LAG и LEAD. Эти функции принимают три аргумента: lag(Argl, Arg2, Arg3) • Argl — выражение, которое надо вернуть на основе другой строки. • Arg2 — смещение требуемой строки в группе относительно текущей. Смещение задается как положительное целое число. В случае функции LAG берется соответствующая смещению предыдущая строка, а в случае функции LEAD — следующая. Этот аргумент имеет стандартное значение 1. О Arg3 — возвращаемое значение в том случае, если смещение, заданное аргументом Arg2, выводит за границу группы. Например, первая строка в каждой группе не имеет предыдущей, так что значение функции LAG(..., 1) для этой строки определить нельзя. Можно возвращать стандартное значение NULL или указать значение явно. Следует учитывать, что окна для функций LAG и LEAD не используются — можно задавать конструкции PARTITION BY и ORDER BY, но не ROWS или RANGE. Итак, в нашем примере: 4 5 6
hiredate — lag(hiredate, I, null) over (partition by deptno order by hiredate, ename) days_last,
Аналитические функции
Q \
функция LAG использовалась для поиска предыдущей строки, поскольку в качестве второго параметра передавалось значение 1 (если предыдущей записи нет, возвращается значение NULL). Мы секционировали данные по столбцу DEPTNO, так что каждый отдел просматривается независимо от остальных. Полученную секцию мы упорядочили по значению столбца HIREDATE, так что вызов LAG(HIREDATE, I, NULL) возвращает максимальное значение HIREDATE, меньшее соответствующего значения в текущей строке.
Проблемы С аналитическими функциями у меня почти не было проблем. Они позволяют получать ответ на абсолютно новые запросы намного эффективнее, чем до их появления. Если разобраться в синтаксисе, открываются безграничные возможности. Не часто бывает, когда результат можно получить практически даром, но с аналитическими функциями все обстоит, похоже, именно так. Следует, однако, помнить о четырех потенциальных проблемах.
Аналитические функции в PL/SQL При попытке использования аналитических функций в коде на языке PL/SQL могут возникать ошибки. Если взять простой запрос и поместить его в PL/SQL-блок: scott@TKYTE816> v a r i a b l e x refcursor scott@TKYTE816> s e t a u t o p r i n t on scott@TKYTE816> begin 2 open :x for 3 select mgr, ename, 4 row_number() over (partition by mgr 5 order by ename) 6 rn from emp; 7 end; 8 / row_number() over (partition by mgr ERROR at line 5: ORA-06550: line 5, column 31: PLS-00103: Encountered the symbol "(" when expecting one of the following: , from into bulk синтаксический анализатор PL/SQL его не воспримет. Анализатор SQL-операторов, используемый в PL/SQL, еще не понимает (в версиях Oracle 8i — прим. научн. ред.) синтаксис вызова аналитических функций. Сталкиваясь с подобными проблемами (есть и другие конструкции, не воспринимаемые синтаксическим анализатором PL/SQL), я использую динамически открываемую курсорную переменную. Реализация показанного выше запроса в этом случае может выглядеть так: scott@TKYTE816> v a r i a b l e x refcursor scott@TKYTE816> s e t a u t o p r i n t on
62
Глава 12 scott@TKYTE816> begin 2 open :x for 3 'select mgr, ename, 4 row_number() over (partition by mgr 5 order by ename) 6 rn from emp'; 7 end; 8
/
PL/SQL procedure successfully completed. MGR ENAME 7566 7566 7698 7698 7698 7698 7698 7782 7788 7839 7839 7839 7902
FORD SCOTT ALLEN JAMES MARTIN TURNER WARD MILLER ADAMS BLAKE CLARK JONES SMITH KING
RN 1 2 1 2 3 4 5 1 1 1 2 3 1 1
14 rows selected. Мы "обманули" синтаксический анализатор PL/SQL, не разрешая ему анализировать конструкции, которые он не понимает, в данном случае — вызов функции ROW_NUMBER(). Для этого достаточно использовать динамически открываемые курсорные переменные. После открытия они работают аналогично обычным курсорам: из них извлекаются данные, потом курсорные переменные закрываются и т.д., но PL/SQLмашина не пытается анализировать операторы ни во время компиляции, ни при выполнении, поэтому можно использовать новые синтаксические конструкции языка SQL. Можно также создать представление на базе запроса с аналитическими функциями, а затем обращаться в PL/SQL-блоке к этому представлению. Например: scott@TKYTE816> create or replace view 2 emp view 3 as 4 select mgr, ename, 5 row number( ) over (partition by mgr 6 order by ename) rn 7 from emp 8 / View created. scott@TKYTE816> begin 2 open :x for 3 select mgr, ename, rn
Аналитические функции 4 5
from emp_view; end;
6
/
63
PL/SQL procedure successfully completed. MGR ENAME
RN
7566 FORD 7566 SCOTT
1 2
Аналитические функции в конструкции WHERE Следует учитывать, что аналитические функции применяются по ходу выполнения запроса почти в самом конце (после них обрабатывается только окончательная конструкция ORDER BY). Это означает, что аналитические функции нельзя непосредственно использовать в условиях (т.е. применять в конструкциях WHERE и HAVING). Если необходимо включать данные в результирующее множество на основе результатов аналитической функции, придется использовать вложенное представление. Аналитические функции могут использоваться только в списке выбора или в конструкции ORDER BY запроса. В этой главе приводилось много примеров использования подставляемых представлений, в частности в разделе, посвященном выбору первых N строк. Например, чтобы найти группу сотрудников каждого отдела с тремя наибольшими зарплатами, мы выполняли следующий запрос: scott@TKYTE816> select * 2 from (select deptno, ename, sal, 3 dense_rank() over (partition by deptno 4 order by sal desc) dr 5 from emp) 6 where dr <= 3 7 order by deptno, sal desc 8
/
Поскольку функцию DENSE_RANK нельзя использовать в конструкции where непосредственно, приходится скрывать ее в подставляемом представлении под псевдонимом DR, чтобы в дальнейшем можно было использовать столбец DR в условии для получения необходимых строк. Такой прием часто используется при работе с аналитическими функциями.
Значения NULL и сортировка Значения NULL могут влиять на результат работы аналитических функций, особенно при использовании сортировки по убыванию. По умолчанию значения NULL считаются больше любых других значений. Рассмотрим следующий пример: scott@TKYTE816> s e l e c t ename, comm from emp order by comm desc; ENAME SMITH JONES
COMM
64
Глава 12
CLARK BLAKE SCOTT KING JAMES MILLER FORD ADAMS MARTIN WARD ALLEN TURNER
1400
500 300 0
14 rows selected.
Выбрав первые N строк, получим: scott@TKYTE816> select ename, comm, dr 2 from (select ename, comm, 3 dense_rank() over (order by comm desc) 4 dr from emp) 5 where dr <= 3 6 order by comm 8 / ENAME
COMM
DR
1400 500
1 1 1 1 1 1 1 1 1 1 2 3
SMITH JONES CLARK BLAKE SCOTT KING JAMES MILLER FORD ADAMS MARTIN WARD 12 rows selected.
Хотя формально это верно, но вряд ли соответствует желаемому результату. Значения NULL либо вообще не должны учитываться, либо интерпретироваться как "наименьшие" в данном случае. Поэтому надо либо исключить значения NULL из рассмотрения, добавив условие where comm is not null: scott@TKYTE816> select ename, comm, dr 2 from (select ename, comm, 3 dense_rank() over (order by comm desc) 4 dr from emp 5 where comm is not null) 6 where dr <= 3 7 order by comm desc 8 /
Аналитические функции ENAME
COMM
DR
MARTIN WARD ALLEN
1400 500 300
1 2 3
65
либо использовать NULLS LAST в конструкции O R D E R BY: scott@TKYTE816> select ename, comm, dr 2 from (select ename, comm, 3 dense_rank() over (order by comm desc nulls last) 4 dr from emp 5 where comm is not null) 6 where dr <= 3 7 order by comm desc 8 / ENAME
COMM
DR
MARTIN WARD ALLEN
1400 500 300
1 2 3
Следует помнить, что NULLS LAST можно указывать и в обычных конструкциях ORDER BY, а не только при вызове аналитических функций.
Производительность До сих пор все, что удалось узнать об аналитических функциях, свидетельствует о них как об универсальном средстве повышения производительности. Однако при неправильном использовании они могут отрицательно повлиять на производительность. При использовании этих функций надо опасаться видимой легкости, с которой они позволяют сортировать и фильтровать множества немыслимыми в стандартном языке SQL способами. Каждый вызов аналитической функции в списке выбора оператора SELECT может использовать свои секции, окна и порядок сортировки. Если они несовместимы (не являются подмножествами друг друга), может выполняться огромный объем сортировки и фильтрования. Например, ранее мы выполняли следующий запрос: ops$tkyte@DEV816> s e l e c t ename, deptno, 2 sum(sal) over (order by ename, deptno) sum_ename_deptno, 3 sum(sal) over (order by deptno, ename) sum_deptno_ename 4 from emp 5 order by ename, deptno 6 / В этом запросе имеются три конструкции ORDER BY, то есть может потребоваться три сортировки. Две сортировки можно объединить, поскольку они выполняются по одним и тем же столбцам, но третью — придется выполнять отдельно. Это — не повод для беспокойства или отказа от использования аналитических функций. Просто надо это учитывать. С помощью аналитических функций можно так же легко написать запрос, использующий все ресурсы компьютера, как и запросы, элегантно и эффективно решающие сложные задачи.
3 Зак. 244
66
Глава 12
Резюме В этой главе мы подробно рассмотрели синтаксис и возможности аналитических функций. Было показано, с какой легкостью они позволяют решать типичные задачи вроде вычисления частичных сумм, транспонирования результирующих множеств, доступа из текущей строки к "соседним" строкам и т.д. Аналитические функции дают широкий спектр потенциальных возможностей для запросов.
.-* зс1ePrpgr# :опаГ
U
•
Материал изованнь ie представления Материализованные представления — средство повышения производительности для хранилищ данных и систем поддержки принятия решений, которое многократно ускоряет выполнение запросов, обращающихся к большому количеству (сотням тысяч или миллионам) записей. Говоря упрощенно, они позволяют за секунды (и даже доли секунд) выполнять запросы к терабайтам данных. Это достигается за счет прозрачного использования заранее вычисленных итоговых данных и результатов соединений таблиц. Предварительно вычисленные итоговые данные обычно имеют очень небольшой объем по сравнению с исходными данными. Предположим, в компании имеется база данных продаж, в которую загружены сведения о миллионах заказов, и необходимо проанализировать продажи по регионам (весьма типичный запрос). Будут просмотрены все записи, данные — агрегированы по регионам с выполнением необходимых вычислений. С помощью материализованного представления можно сохранить итоговые данные продаж по регионам и обеспечить автоматическую поддержку этих данных системой. При наличии десяти регионов продаж итоговые данные будут состоять из десяти записей, так что мы будем обращаться не к миллиону фактических записей, а только к десяти. Более того, при выполнении несколько измененного запроса, например об объеме продаж по определенному региону, ответ на него тоже можно получить по этому материализованному представлению. В этой главе мы разберемся, что такое материализованные представления, каковы их возможности и, самое главное, как они устроены. Я покажу, как обеспечить использование созданного материализованного представления всеми запросами, для которых оно позволяет получить ответ (иногда очевидно, что сервер Oracle мог бы использовать
/О
Глава 13
материализованное представление, но не делает этого из-за отсутствия важной информации). В частности, будет: Q рассмотрен пример, демонстрирующий возможности материализованных представлений и позволяющий решить, пригодятся ли они вам; О описаны параметры и привилегии, которые необходимо установить для использования материализованных представлений; •
показано на примерах использование измерений (dimensions) и ограничений целостности, позволяющих серверу определить, когда для ответа на запрос оправданно применение материализованного представления;
Q описано использование пакета DBMS_OLAP для анализа представлений; •
и в завершение описаны две проблемы, которые надо учитывать при использовании материализованных представлений.
Предыстория Управление таблицами итогов — еще одна возможность материализованного представления — уже некоторое время использовалось в инструментальных средствах типа Oracle Discoverer (средство построения произвольных запросов и отчетов). С помощью Discoverer администратор создает в базе данных таблицы итогов. Затем это инструментальное средство анализировало запросы, прежде чем отправлять их на сервер Oracle. Если имеется таблица итогов, позволяющая более эффективно ответить на запрос, Discoverer переписывает запрос так, чтобы он обращался к таблицам итогов, а не к базовым, заданным в исходном запросе, после чего отправляет запрос серверу Oracle. Это было замечательно, пока для выполнения запросов использовалось именно это средство. Если аналогичный запрос выполнялся из среды SQL*Plus или поступал от клиента по протоколу JDBC, то перезапись запроса не происходила (не могла происходить). Более того, синхронизация между исходными и итоговыми данными не могла выполняться автоматически, поскольку это инструментальное средство не входило в состав сервера. Начиная с версии 7.0, сервер Oracle уже поддерживал возможность, аналогичную таблицам итогов, — моментальные снимки (snapshot). Первоначально эта возможность создавалась для поддержки репликации, но я лично использовал ее для сохранения ответов на большие запросы. Я создавал моментальные снимки, не использующие связь базы данных для репликации данных из одной базы в другую, а просто вычисляющие итоговые значения или выполняющие соединения для часто используемых данных. Это было здорово, но без возможности переписать запросы использование этого приема было ограниченным. Приложение должно было "знать" о существовании таблиц итогов и использовать их, а это усложняло его создание и поддержку. При добавлении новой таблицы итогов мне приходилось находить и переписывать код, в котором можно было ее использовать. В версию Oracle 8.1.5 (Enterprise и Personal Edition) из инструментальных средств типа Discoverer была перенесена возможность переписывания запросов, предусмотрены механизмы автоматического обновления и планирования моментальных снимков (что позволило сделать таблицы итогов "самоподдерживаемыми"), и все это объединено с воз-
/ \
Материализованные представления
можностью оптимизатора находить лучший план из многих альтернативных. В результате получились материализованные представления. Все эти возможности, сосредоточенные в сервере, теперь позволяли любому приложению использовать преимущества автоматического переписывания запросов, независимо от способа доступа к базе данных: из утилиты SQL*Plus, из приложения Oracle Forms, через протоколы JDBC и ODBC, из программ Pro*C, OCI или из средства сторонних производителей. Любой сервер Oracle 8i масштаба предприятия позволяет управлять таблицами итогов. Кроме того, поскольку все происходит в базе данных, таблицы итогов легко синхронизировать с исходными (по крайней мере сервер всегда "знает", когда они не синхронизированы, и может не использовать "устаревшие" таблицы итогов (этим управляет пользователь)). Поскольку эти функциональные возможности включены непосредственно в сервер, любое приложение, способное обратиться к СУБД Oracle, может воспользоваться ими. Тот же подход лежит в основе реализации средств детального контроля доступа (Fine Grained Access Control, FGAC, см. раздел об открытости
в главе 1 и главу
21,
посвященную этим средствам). Чем "ближе" к данным применяются функции, тем большее количество инструментальных средств сможет ими воспользоваться.
Если
поместить средства защиты вне базы данных, например в приложении, воспользоваться ими смогут только пользователи этого приложения (и то, если обращаются к данным исключительно через приложение).
Что необходимо для выполнения примеров Для выполнения примеров, представленных в этой главе, необходима редакция Personal или Enterprise Edition сервера версии Oracle 8.1.5 и выше. Соответствующие функциональные возможности не поддерживаются в редакции Standard. Учетная запись, от имени которой будут выполняться примеры, должна иметь следующие привилегии: Q
GRANT CREATE SESSION
Q
GRANT CREATE TABLE
•
GRANT CREATE MATERIALIZED VIEW
•
GRANT QUERY REWRITE
Первые три привилегии можно предоставить роли, которая, в свою очередь, предоставлена учетной записи. Привилегия QUERY REWRITE должна быть предоставлена непосредственно. Кроме того, необходим доступ к табличному пространству, 30—50 Мбайт которого свободно. Наконец, чтобы воспользоваться средствами переписывания запросов, необходимо использовать оптимизатор, основанный на стоимости (Cost-Based Optimizer — СВО). Если СВО не используется, запрос не может быть переписан. В наших примерах остав-
72
Глава 13
лена стандартная цель оптимизации, CHOOSE; чтобы воспользоваться возможностями переписывания запросов, достаточно проанализировать таблицы.
Пример Продемонстрирую на простом примере, что может дать материализованное представление. Вы увидите, насколько сокращается время выполнения запроса за счет добавления итоговых данных. Запрос к большой таблице будет переписан сервером в запрос к гораздо меньшей таблице, причем без потери точности результата. Начнем с создания большой таблицы, содержащей список владельцев и принадлежащих им объектов. Таблица строится на основе представления ALL_OBJECTS словаря данных: tkyte@TKYTE816> create table my all objects 2 nologging 3 as 4 select * from all_ objects 5 union all 6 select * from all_ objects 7 union all 8 select * from all objects 9 / Table created. tkyte@TKYTE816> insert /*+ APPEND */ into my_all_objects 2 select * from my_all_objects; 65742 rows created. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> insert /*+ APPEND */ into my_all_objects 2 select * from my_all_objects; 131484 rows created. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> analyze table my_all_objects compute statistics; Table analyzed. Моя система поддерживает язык Java (Java option), поэтому в таблице MY_ALL_OB.JECTS после выполнения указанных действий оказалось около 250000 строк. Для получения такого же результата вам, возможно, придется выполнить UNION ALL и INSERT большее количество раз. Теперь выполним запрос к этой таблице, показывающий количество объектов у каждого пользователя. Первоначально для этого понадобится полный просмотр громадной таблицы: tkyte@TKYTE816> s e t autotrace on tkyte@TKYTE816> s e t timing on tkyte@TKYTE816> s e l e c t owner, count(*) from my_all_objects group by owner;
Материализованные представления
73
COUNT(*)
OWNER
А В CTXSYS DBSNMP DEMO DEMO11 DEMO_DDL MDSYS MVJJSER ORDPLUGINS ORDSYS OUR_TYPES OUTLN PERFSTAT PUBLIC SCHEDULER SCOTT SEAPARK SYS SYSTEM TESTING TKYTE TTS_USER
36 24 2220 48 60 36 108 2112 60 312 2472 12 60 636 117972 36 84 36 135648 624 276 12 48
TYPES
36
24 rows selected. Elapsed: 00:00:03.35 tkyte@TKYTE816> set timing off tkyte@TKYTE816> set autotrace traceonly tkyte@TKYTE816> select owner, count(*) from my_all_objects group by owner; 24 rows selected. Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=2525 Card=24 Bytes=120) SORT (GROUP BY) (Cost=2525 Card=24 Bytes=120) TABLE ACCESS (FULL) OF 'MY ALL OBJECTS' (Cost=547 Card=262968
Statistics 0 recursive calls 27 db block gets 3608 consistent gets 3516 physical reads 0 redo size 1483 bytes sent via SQL*Net to client 535 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 1 sorts (memory)
74
Глава 13 О 24
sorts (disk) rows processed
Для получения результатов агрегирования необходимо просмотреть более 250000 записей в более чем 3600 блоках. К сожалению, в нашей системе этот запрос необходимо выполнять часто, по нескольку десятков раз в день. Приходится сканировать почти 30 Мбайт данных. Создав материализованное представление, можно избежать многократного подсчета по исходной таблице. Ниже описано, что для этого нужно сделать. Операторы GRANT и ALTER более детально будут рассмотрены в разделе "Как работать с материализованными представлениями". Кроме указанных ниже привилегий может понадобиться также привилегия CREATE MATERIALIZED VIEW (в зависимости от того, какие роли предоставлены и действуют для соответствующей учетной записи): tkyte@TKYTE816> grant query rewrite to tkyte; Grant succeeded. tkyte@TKYTE816> a l t e r session set query_rewrite_enabled=true; Session altered. tkyte@TKYTE816> alter session set query_rewrite_integrity=enforced; Session altered. tkyte@TKYTE816> create materialized view my_all_objects_aggs 2 build immediate 3 refresh on commit 4 enable query rewrite 5 as 6 select owner, count(*) 7 from my_all_objects 8 group by owner 9 / Materialized view created. tkyte@TKYTE816> analyze table my_all_objects_aggs compute s t a t i s t i c s ; Table analyzed. По сути, мы заранее вычислили количество объектов и задали итоговую информацию в виде материализованного представления. Мы потребовали немедленно псстроить и наполнить данными это представление. Обратите внимание, что были также заданы конструкции REFRESH ON COMMIT и ENABLE QUERY REWRITE (вскоре они будут рассмотрены подробнее). Также обратите внимание, что, хотя создано материализованное представление, анализируется таблица. При создании материализованного представления создается настоящая таблица, и ее можно индексировать, анализировать и т.д. Давайте посмотрим представление в действии, выполнив еще раз запрос, использовавшийся при создании представления: tkyte@TKYTE816> s e t timing on tkyte@TKYTE816> s e l e c t owner, count(*) 2 from my_all_objects 3 group by owner;
Материализованные представления OWNER
ID
COUNT(*)
A В
36 24
TYPES
36
24 rows selected. Elapsed: 00:00:00.10 tkyte@TKYTE816> set timing off tkyte@TKYTE816> set autotrace traceonly tkyte@TKYTE816> select owner, count(*) 2 from my_all_objects 3 group by owner; 24 rows selected. Execution Plan 0 1
0
SELECT STATEMENT Optimizer=CHOOSE (Cost-1 Card=24 Bytes=216) TABLE ACCESS (FULL) OF 'MY_ALL_OBJECTS_AGGS' (Cost-1 Card=Valve)
Statistics 0 recursive calls 12 db block gets 7 consistent gets 0 physical reads 0 redo size 1483 bytes sent via SQL*Net to client 535 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 0 sorts (memory) 0 s o r t s (disk) 24 rows processed tkyte@TKYTE816> set autotrace
off
Вместо более чем 3600 consistent gets (логических операций ввода-вывода) использовано всего 12. Физического ввода-вывода на этот раз вообще не было — данные взяты из кеша. Теперь буферный кеш будет значительно эффективнее, так как кешировать надо намного меньше данных. Раньше кеширование рабочего множества даже не начиналось, но теперь все рабочее множество помещается в кеше. Обратите внимание, что план выполнения запроса предусматривает полный просмотр таблицы MY_ALL_OBJECTS_AGGS, хотя запрос выполнялся к исходной таблице MY_ALL_OBJECTS. При получении запроса SELECT OWNER, COUNT(*)... сервер автоматически направляет его к соответствующему материализованному представлению. Давайте пойдем дальше: добавим новую строку в таблицу MY_ALL_OBJECTS и зафиксируем изменение: tkyte@TKYTE816> i n s e r t i n t o my_all_objects 2 (owner, object_name, object_type, object_id)
76
Глава 13 3
values
4
('New Owner', 'New Name', 'New Type 1 ,
1111111);
1 row created. tkyte@TKYTE816> commit; Commit complete. Теперь выполним аналогичный запрос, но обратимся только к вновь вставленной строке: tkyte@TKYTE816> s e t t i m i n g on tkyte@TKYTE816> s e l e c t owner, c o u n t ( * ) 2 from my_all_objects 3 where owner = 'New Owner' 4 group by owner; OWNER
COUNT(*)
New Owner
1
Elapsed: 00:00:00.01 tkyte@TKYTE816> set timing off tkyte@TKYTE816> set autotrace traceonly tkyte@TKYTE816> select owner, count(*) 2 from my_all_objects 3 where owner = 'New Owner' 4 group by owner; Execution Plan 0 1
0
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=l Bytes=9) TABLE ACCESS (FULL) OF 'MY_ALL_OBJECTS_AGGS' (Cost=l Card=Valve)
Statistics 0 recursive calls 12 db block gets б consistent gets 0 physical reads 0 redo size 430 bytes sent via SQL*Net to client 424 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed tkyte@TKYTE816> s e t autotrace
off
Анализ показывает, что новая строка была найдена при просмотре материализованного представления. Присутствие в исходном определении представления конструкции REFRESH ON COMMIT заставляет сервер Oracle обеспечивать синхронизацию между представлением и исходными данными — при изменении исходных данных изменяется
Материализованные представления
/ /
и представление. Такую синхронизацию нельзя обеспечить для всех материализованных представлений, но в случае однотабличного итогового представления (как наше) или только соединений, без агрегирования, это возможно. Теперь еще один, последний запрос: tkyte@TKYTE816> s e t t i m i n g on tkyte@TKYTE816> s e l e c t c o u n t ( * ) 2 from my_all_objects 3 where owner = 'New Owner'; COUNT(*)
Elapsed: 00:00:00.00 tkyte@TKYTE816> set timing off tkyte@TKYTE816> set autotrace traceonly tkyte@TKYTE816> select count(*) 2 from my_all_objects 3 where owner = 'New Owner'; Execution Plan 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=l Bytes=9) 1 0 SORT (AGGREGATE) 2 1 TABLE ACCESS (FULL) OF 'MY_ALL_OBJECTS_AGGS' (Cost=l ** Card=Valve) Statistics 0 recursive calls 12 db block gets 5 consistent gets 0 physical reads 0 redo size 367 bytes sent via SQL*Net to client 424 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed tkyte@TKYTE816> set autotrace
off
Как видите, сервер Oracle может использовать представление даже для запроса. Конструкции GROUP BY в нашем запросе не было, но сервер "понял", что материализованное представление все равно можно использовать. Это и является чудом использования материализованных представлений. Пользователи могут не знать о существовании таблиц итогов, сервер сам разберется, что ответ уже существует, если включена возмож-
78
Глава 13
ность переписывания запроса, и автоматически перепишет запрос так, чтобы использовать соответствующее материализованное представление. Это позволяет непосредственно повлиять на работу приложений, не изменяя в них ни одного запроса.
Назначение материализованных представлений Его можно сформулировать коротко: повышение производительности. Получив (однажды) ответы на сложные вопросы, можно существенно снизить нагрузку на сервер. При этом: • Уменьшается количество физических чтений. Приходится просматривать меньше данных. Q Уменьшается количество записей. Не нужно так часто сортировать/агрегировать данные. Q Уменьшается нагрузка на процессор. Не придется постоянно вычислять агрегаты и функции от данных, поскольку это уже сделано. Q Существенно сокращается время ответа. При использовании итоговых данных запросы выполняются значительно быстрее по сравнению с запросами к исходным данным. Все зависит от объема действий, которых можно избежать при использовании материализованного представления, но ускорение на несколько порядков вполне возможно. При использовании материализованных представлений увеличивается потребность только в одном ресурсе — дисковом пространстве. Необходимо дополнительное место для хранения материализованных представлений, но за счет этого можно получить много преимуществ. Материализованные представления больше подходят для сред, где данные только читаются (пусть даже интенсивно). Они не предназначены для использования в среде интенсивной обработки транзакций. Они требуют дополнительных затрат ресурсов при изменении базовых таблиц для учета этих изменений. При использовании опции REFRESH ON COMMIT возникают проблемы одновременного доступа. Вернемся к рассмотренному выше примеру с итоговыми данными. При вставке строки в базовую таблицу (или удалении) необходимо изменить одну из 24 строк в таблице итогов, чтобы количество было актуальным. Это означает, что одновременно фиксировать транзакции сможет не более 24 пользователей (если, конечно, они затрагивают объекты разных владельцев). Однако это не мешает использовать материализованные представления в среде ООТ. Например, если периодически полностью обновлять представления (в периоды минимальной нагрузки), при изменении данных ресурсы дополнительно расходоваться не будут, как не будет и проблем с одновременным доступом. Это позволит создавать отчеты на основе, например, вчерашних данных, не обращаясь к активно изменяющимся при обработке транзакций данным.
Материализованные представления
/у
Как работать с материализованными представлениями Сначала работа с материализованными представлениями может показаться сложной. Иногда материализованное представление содержит ответ на определенный вопрос, но сервер Oracle почему-то его не использует. Если достаточно глубоко покопаться, можно понять, почему. Сервер Oracle — всего лишь программа, и она может работать только с явно предоставленной информацией. Чем больше метаданных предоставлено, чем больше сведений о базовых данных передано серверу Oracle, тем лучше. Эти фрагменты информации (ограничения NOT NULL, первичные ключи, внешние ключи и т.д.) — слишком тривиальные вещи, чтобы задумываться о них в среде хранилищ данных. Предоставляемые этими ключами и ограничениями метаданные дают оптимизатору больше информации, а значит, — больше шансов. Ключи и ограничения, подобные представленным выше, не только обеспечивают целостность данных, но и добавляют в словарь данных информацию о данных, которую можно использовать при переписывании запросов (отсюда и название — метаданные). Подробнее об этом см. в разделе "Ограничения целостности". В следующих разделах мы рассмотрим, что необходимо сделать для использования материализованных представлений, представим ряд примеров и покажем, как после введения дополнительной информации, дополнительных метаданных в базу увеличивается частота использования материализованных представлений.
Подготовка Для использования материализованных представлений обязательно надо установить один параметр инициализации, COMPATIBLE. Параметр COMPATIBLE должен иметь значение 8.1.0 или больше, чтобы переписывание запросов вообще применялось. Если этот параметр не будет иметь соответствующего значения, модуль переписывания запросов не будет вызываться. Есть еще два связанных с использованием материализованных представлений параметра, которые можно устанавливать либо на уровне системы (в файле параметров инициализации, INIT.ORA), либо на уровне сеанса (с помощью оператора ALTER SESSION). •
QUERY_REWRITE_ENABLED. Если этот параметр не имеет значения TRUE, запрос не переписывается. Стандартное значение — FALSE.
Q QUERY_REWRITE_INTEGRITY. Этот параметр управляет тем, как сервер Oracle переписывает запросы. Он может иметь одно из трех значений. •
ENFORCED. Запросы будут переписываться с помощью ограничений и правил, применяемых и гарантируемых сервером Oracle. Имеются механизмы, с помощью которых можно сообщить серверу Oracle о других косвенных взаимосвязях, и это позволит переписать больше запросов; но поскольку сервер Oracle не обеспечивает эти взаимосвязи, он не будет использовать подобные сведения на этом уровне целостности.
80
Глава 13
•
TRUSTED. Запросы будут переписываться на основе ограничений, обеспечиваемых сервером Oracle, а также всех взаимосвязей данных, о которых сообщили серверу, даже если их выполнение сервером не гарантируется. Так, в начальном примере можно вручную создать физическую таблицу MY_ALL_OBJECTS_AGGS с помощью распараллеливаемого и нерегистрируемого в журнале повторного выполнения оператора CREATE TABLE AS SELECT (для ускорения построения таблицы итогов). Затем можно создать материализованное представление, использующее эту созданную заранее таблицу, а не создавать ее заново. Если необходимо, чтобы сервер Oracle использовал эту созданную заранее таблицу при последующем переписывании запросов, необходимо задать параметру QUERY_REWRITE_INTEGRITY значение TRUSTED. Надо, чтобы сервер Oracle "поверил", что мы предоставили корректные данные в заранее созданной таблице (сам сервер Oracle корректность этих данных не обеспечивает).
•
STALE_TOLERATED. Запросы будут переписываться для использования материализованных представлений, даже если серверу Oracle известно, что содержащиеся в представлении данные устарели (не синхронизированы с исходными). Это может пригодиться в среде, где таблицы итогов обновляются периодически, а не при фиксации изменений, и где небольшая рассинхронизация приемлема.
В представленном выше примере использовались операторы ALTER SESSION, обеспечивающие применение фокуса с переписыванием запроса. Поскольку в примере использовались только объекты и связи, поддерживаемые сервером Oracle, целостность запроса при перезаписи можно установить максимальной: ENFORCED. Также необходимо получить привилегию QUERY REWRITE. Но учетной записи, от имени которой я работаю, предоставлена роль DBA, среди привилегий которой есть и QUERY REWRITE, так зачем же явно предоставлять эту привилегию самому себе? Причина в том, что нельзя создать скомпилированные хранимые объекты, будь то материализованные представления, хранимые процедуры или триггеры, имея привилегии роли (роли DBA в данном случае). Полное описание особенностей использования ролей при работе со скомпилированными хранимыми объектами дано в главе 23. Если создается материализованное представление при включенном параметре QUERY_REWRITE_ENABLED, но системная привилегия QUERY REWRITE явно не предоставлена, будет получено следующее сообщение об ошибке: create materialized view my_all_objects_aggs *
ERROR at line 1:
ORA-01031: insufficient privileges
Внутренняя реализация Итак, теперь, научившись создавать материализованные представления и убедившись, что они используются, разберемся, что будет предпринимать сервер Oracle для переписывания запросов? Обычно, когда параметр QUERY_REWRITE_ENABLED имеет значение FALSE, сервер Oracle анализирует полученный оператор SQL и оптимизирует его.
Материализованные представления
Q \
При включенном переписывании запросов сервер Oracle добавляет в этот процесс дополнительный шаг. После анализа сервер попытается переписать запрос так, чтобы он обращался к тому или иному материализованному представлению вместо указанной в нем таблицы. Если переписать запрос можно, полученный в результате запрос (или запросы) анализируется и оптимизируется вместе с исходным запросом. Из полученного набора выбирается план выполнения с наименьшей стоимостью. Если запрос переписать не удается, исходный проанализированный запрос просто оптимизируется и выполняется, как обычно.
Переписывание запроса При включенном переписывании запроса сервер Oracle будет пытаться переписать запрос так, чтобы он обращался к материализованному представлению, в следующих случаях.
Полное совпадение текста Сервер ищет полное совпадение строк запроса с текстами определяющих запросов, хранящихся в словаре данных для материализованных представлений. В рассмотренном ранее примере именно этот метод использовал сервер Oracle для первого запроса, при выполнении которого использовалось материализованное представление. При этом используется более "дружественный" (гибкий) алгоритм, чем при поиске в разделяемом пуле (требующем побайтового совпадения), поскольку пробелы, регистр символов и другие особенности форматирования игнорируются.
Частичное совпадение текста Начиная с конструкции FROM, оптимизатор сравнивает оставшийся текст с текстом запроса, определяющего материализованное представление. В результате допускаются расхождения в списке выбора. Если необходимые данные можно получить из материализованного представления (т.е. по нему можно найти значения всех выражений, указанных в списке выбора), сервер Oracle перепишет запрос с использованием этого материализованного представления. Запрос SELECT LOWER(OWNER) FROM MY_ALL_OBJECTS GROUP BY OWNER; — пример частичного совпадения текста.
Общие методы переписывания запроса Они обеспечивают использование материализованного представления, даже если оно содержит часть необходимых данных, больше данных или данные, которые могут быть преобразованы к нужному виду. Оптимизатор сравнивает определение материализованного представления с отдельными компонентами запроса (SELECT, FROM, WHERE, GROUP BY) в поисках соответствия. При этом сервер Oracle проверяет для этих компонентов следующее. • Достаточность данных. Можно ли получить нужные данные из этого материализованного представления? Если в списке выбора есть столбец X, отсутствующий в материализованном представлении, и его нельзя получить, выполняя соединение с этим представлением, то сервер Oracle не будет переписывать запрос так, чтобы он обращался к этому представлению. Например, запрос SELECT
82
Глава 13
DISTINCT OWNER FROM MY_ALL_OBJECTS при наличии созданного в примере материализованного представления может быть переписан, поскольку столбец OWNER доступен. Запрос же SELECT DISTINCT OBJECT_TYPE FROM MY_ALL_OBJECTS по материализованному представлению выполнить нельзя, поскольку в нем недостаточно данных. Q Совместимость по соединениям. Можно ли получить результат соединения, требуемого исходным запросом, из материализованного представления. Совместимость по соединениям можно продемонстрировать с помощью таблицы MY_ALL_OBJECTS и следующих таблиц: tkyte@TKYTE816> create t a b l e t l (owner varchar2(30), flag c h a r ( l ) ) ; Table created. tkyte@TKYTE816> create table t2 (object_type varchar2(30), flag c h a r ( l ) ) ; Table created. Следующий запрос совместим по соединению с материализованным представлением, поэтому он будет переписан так, чтобы обращаться к материализованному представлению: tkyte@TKYTE816> s e l e c t a.owner, count(*), b.owner 2 from my_all_objects a, t l b 3 where a.owner = b.owner 4 and b . f l a g i s not n u l l 5 group by a.owner, b.owner 6 / Сервер может выяснить, что при использовании материализованного представления вместо исходной таблицы будет получен тот же ответ. Следующий запрос, однако, хотя и похож, но не совместим по соединению: tkyte@TKYTE816> select a.owner, count(*), b.object_type 2 from my_all_objects a, t2 b 3 where a.object_type = b.object_type 4 and b.flag is not null 5 group by a.owner, b.object_type 6 / Столбец OBJECT_TYPE в наше материализованное представление не входит, поэтому сервер Oracle не может переписать запрос так, чтобы он обращался к этому представлению.
Совместимость конструкций
группировки
Она требуется, если и материализованное представление, и запрос содержат конструкцию GROUP BY. Если материализованное представление сгруппировано на необходимом или более высоком уровне детализации, запрос будет переписан с использованием материализованного представления. Запрос SELECT COUNT(*) FROM MY_ALL_OBJECTS GROUP BY 1, если выполнять его после представленного выше примера, представляет тот самый случай, когда материализованное представление сгруп-
Материализованные представления
оЗ
пировано на более высоком уровне детализации, чем необходимо. Сервер может переписать этот запрос с использованием материализованного представления, хотя критерии фуппировки в запросе и материализованном представлении не совпадают.
Совместимость конструкций агрегирования Такая совместимость требуется, если и запрос, и материализованное представление содержат функции агрегирования. Она гарантирует, что материализованное представление обеспечит данные для необходимых агрегатов. В некоторых случаях возможны очень интересные варианты переписывания. Например, сервер распознает, что AVG(X) — то же самое, что и SUM(X)/COUNT(X), так что запрос, требующий выбора AVG(X), может быть выполнен по материализованному представлению, содержащему значения SUM и COUNT. Во многих случаях простое применение описанных выше правил позволит серверу Oracle переписать запрос так, чтобы он обращался к материализованному представлению. В других случаях (как будет показано далее) серверу потребуется помощь администратора. Надо предоставить серверу дополнительную информацию, чтобы он смог использовать для ответа на запрос материализованное представление.
Как гарантировать использование представлений В этом разделе мы научимся это делать — сначала с помощью ограничений, помогающих использовать переписывание запроса, а потом с помощью измерений (dimensions), являющихся средством описания сложных взаимосвязей — иерархий данных.
Ограничения целостности Меня часто спрашивали: "Почему надо использовать первичный ключ? Почему просто не создать уникальный индекс?". Можно, конечно, создать и просто индекс, но ведь факт использования первичного ключа говорит намного больше, чем просто создание уникального индекса. То же самое можно сказать об использовании внешних ключей, ограничений NOT NULL и других. Они не только защищают данные от нежелательных изменений, но и добавляют информацию о данных в словарь данных. На основе этой дополнительной информации сервер Oracle сможет чаще и в более сложных случаях переписывать запрос. Рассмотрим следующий небольшой пример. Скопируем таблицы ЕМР и DEPT из схемы пользователя SCOTT и создадим материализованное представление, соединяющее эти две таблицы. Это материализованное представление отличается от использованного в первом примере тем, что для него задана конструкция REFRESH ON DEMAND. Это означает, что для учета изменений в исходных данных это представление надо обновлять вручную: tkyte@TKYTE816> create table emp as select * from scott.emp;
Table created. tkyte@TKYTE816> create table dept as select * from scott.dept;
84
Глава 13
Table
created.
tkyte@TKYTE816> Session
query_rewrite_enabled=true;
alter session set
query_rewrite_integrity=enforced;
altered.
tkyte@TKYTE816> Session
alter session set
altered.
tkyte@TKYTE816> create materialized view emp_dept 2 build immediate 3 refresh on demand 4 enable query rewrite 5 as 6 select dept.deptno, dept.dname, count (*) 7 from emp, dept 8 where emp.deptno = dept.deptno 9 group by dept.deptno, dept.dname 10 / Materialized view tkyte@TKYTE816> Session
created.
alter session set
optimizer_goal=all_rows;
altered.
Поскольку базовые таблицы и полученное материализованное представление — очень небольшие, мы с помощью оператора ALTER SESSION принудительно потребуем использовать оптимизатор, основанный на стоимости, а не проанализируем таблицы, как обычно. Если сервер Oracle "узнает", насколько малы эти таблицы, он не будет выполнять некоторые из желательных для нас оптимизаций. При использовании стандартной статистической информации, оптимизатор будет работать так, будто таблицы достаточно большие. В данном случае мы предоставили серверу Oracle мало информации. Ему не известно, как соотносятся таблицы ЕМР и DEPT, какие столбцы являются первичными ключами и т.д. Теперь выполним запрос и посмотрим, что произойдет: tkyte@TKYTE816> s e t autotrace on tkyte@TKYTE816> s e l e c t count(*) from emp; COUNT(*)
14 Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l) SORT (AGGREGATE) TABLE ACCESS (FULL) OF 'EMP1 (Cost=l Card=82)
Запрос был выполнен к базовой таблице ЕМР. Теперь мы с вами знаем, что значение COUNT(*) намного эффективнее (особенно при большом количестве отделов и сотрудников в них) может быть получено из материализованного представления. В нем есть вся необходимая информация для подсчета количества сотрудников. Мы знаем об этом потому, что учитываем сведения о данных, неизвестные серверу Oracle:
Материализованные представления
•
Столбец DEPTNO — первичный ключ таблицы DEPT. Это означает, что каждая строка в таблице ЕМР соответствует не более чем одной строке в таблице DEPT.
•
Столбец DEPTNO в таблице ЕМР — внешний ключ по столбцу DEPTNO таблицы DEPT. Если значение столбца DEPTNO в строке таблицы ЕМР непустое, она будет соединена со строкой в таблице DEPT (ни одна строка таблицы ЕМР с непустым значением при соединении потеряна не будет).
• Для столбца DEPTNO в таблице ЕМР задано требование NOT NULL. В сочетании с требованием внешнего ключа это означает, что ни одна строка таблицы ЕМР не будет потеряна. Эти три факта в совокупности означают, что при соединении таблиц ЕМР и DEPT каждая строка таблицы ЕМР будет входить в результирующее множество только один раз. Поскольку серверу Oracle об этом не сообщалось, он не смог использовать материализованное представление. Давайте же сообщим серверу все это: tkyte@TKYTE816> a l t e r t a b l e dept 2
add c o n s t r a i n t dept_pk primary key(deptno);
Table a l t e r e d . tkyte@TKYTE816> a l t e r t a b l e emp 2 add c o n s t r a i n t emp_fk_dept 3
foreign key(deptno) references dept(deptno);
Table a l t e r e d . tkyte@TKYTE816> a l t e r t a b l e emp modify deptno not n u l l ; Table a l t e r e d . tkyte@TKYTE816> s e t a u t o t r a c e on tkyte@TKYTE816> s e l e c t count(*) from emp; COUNT(*) 14 Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l Bytes=13) SORT (AGGREGATE) TABLE ACCESS (FULL) OF 'EMP_DEPT' (Cost=l Card=82 Bytes=1066)
Теперь сервер Oracle может переписать запрос с использованием материализованного представления EMP_DEPT. Каждый раз, когда известно, что сервер мог бы использовать материализованное представление, но не использует (и проверено, что вообще использование материализованных представлений в сеансе возможно), более детально изучите данные и спросите себя: "Какую информацию я не предоставил серверу Oracle?". В девяти случаях из десяти обнаружится фрагмент метаданных, при добавлении которого сервер Oracle будет переписывать запрос. Итак, что же произойдет в реальном хранилище данных, где в представленных таблицах будут десятки миллионов записей? Дополнительные затраты ресурсов на проверку выполнения ограничений целостности нежелательны — в программе первичной об-
86
Глава 13
работки данных это уже сделано, не так ли? В данном случае можно создать непроверяемое ограничение, которое используется для информирования сервера о взаимосвязи, но сервером не проверяется. Давайте рассмотрим предыдущий пример еще раз, но теперь сымитируем загрузку данных в существующее хранилище (хранилище представлено предыдущим примером). Удалим ограничения, загрузим данные, обновим материализованные представления и снова добавим ограничения. Начнем с удаления ограничений: tkyte@TKYTE816> a l t e r table emp drop constraint emp_fk_dept; Table altered. tkyte@TKYTE816> a l t e r table dept drop constraint dept_pk; Table altered. tkyte@TKYTE816> a l t e r table emp modify deptno null; Table altered. Теперь, чтобы сымитировать загрузку, я вставлю новую строку (для демонстрационных целей этого вполне достаточно) в таблицу ЕМР. Затем мы обновим материачизованное представление и сообщим серверу Oracle, что его можно считать актуальным (FRESH): tkyte@TKYTE816> i n s e r t i n t o emp (empno,deptno) values ( 1 , 1 ) ; 1 row created. tkyte@TKYTE816> exec dbms_mview.refresh('EMP_DEPT'); PL/SQL procedure successfully completed. tkyte@TKYTE816> alter materialized view emp_dept consider fresh; Materialized view altered. Теперь сообщаем серверу о взаимосвязи таблиц ЕМР и DEPT: tkyte@TKYTE816> a l t e r t a b l e dept 2 add c o n s t r a i n t dept_pk primary key(deptno) 3 rely enable NOVALIDATE 4 / Table altered. tkyte@TKYTE816> alter table emp 2 add constraint emp_fk_dept 3 foreign key(deptno) references dept(deptno) 4 rely enable NOVALIDATE 5 / Table altered. tkyte@TKYTE816> alter table emp modify deptno not null NOVALIDATE; Table altered. Итак, мы сообщили серверу Oracle, что имеется, как и прежде, внешний ключ в таблице ЕМР, ссылающийся на таблицу DEPT. Однако, поскольку перед загрузкой в хранилище данные уже обрабатывались, мы сообщаем серверу, что проверять выполнение
Материализованные представления
Q /
ограничений не надо. Опция NOVALIDATE позволяет избежать проверки загруженных данных, а опция RELY требует, чтобы сервер рассматривал данные как целостные. По сути, мы сообщили серверу о необходимости считать, что при соединении таблиц ЕМР и DEPT по столбцу DEPTNO каждая строка в таблице ЕМР обязательно попадет в результат, причем не более одного раза. Фактически мы "обманули" сервер, вставив в таблицу ЕМР строку, для которой нет соответствующей строки в таблице DEPT. Теперь все готово для выполнения запроса: tkyte@TKYTE816> a l t e r session set Session
query_rewrite_integrity=enforced;
altered.
tkyte@TKYTE816> s e l e c t count(*) from emp; COUNT(*) 15 Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l) SORT (AGGREGATE) TABLE ACCESS (FULL) OF 'EMP' (Cost=l Card=164)
Поскольку установлено значение параметра QUERY_REWRITE_INTEGRrrY=ENFORCED, сервер Oracle не переписал запрос с использованием материализованного представления. Необходимо понизить уровень целостности запроса. Надо, чтобы сервер Oracle нам "поверил": tkyte@TKYTE816> a l t e r session set query_rewrite Session
integrity=trusted;
altered.
tkyte@TKYTE816> s e l e c t count(*) from emp; COUNT(*)
14 Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=ALL_ROWS (Cost=l Card=l Bytes=13) SORT (AGGREGATE) TABLE ACCESS (FULL) OF 'EMP_DEPT' (Cost=l Card=82 Bytes=1066)
В этом случае сервер Oracle переписал запрос, но побочным эффектом оказалось то, что вновь вставленные строки не учтены. Возвращается "ошибочный" ответ, поскольку "факт" сохранения каждой строки таблицы ЕМР в результатах соединения с таблицей DEPT при загруженных в таблицу данных — уже не факт. При обновлении материализованного представления вновь добавленная строка ЕМР в него не попала. Данные, которым сервер Oracle по нашему требованию доверял, оказались ненадежными. В результате мы приходим к двум важным умозаключениям: Q можно очень эффективно использовать материализованные представления в больших хранилищах данных, без необходимости выполнять множество дополнительных и зачастую избыточных проверок данных;
88
Глава 13
• но, лучше лишний раз перепроверить согласованность данных, если вы требуете от сервера Oracle им доверять.
Измерения Использование измерений — еще один метод предоставления дополнительной информации серверу Oracle. Предположим, имеется таблица исходных данных с датами транзакций и идентификаторами клиентов. По дате транзакции в другой таблице можно найти детальную информацию о том, к какому месяцу относится транзакция, к какому кварталу финансового года и т.д. Теперь предположим, что создано материатизованное представление для хранения агрегированной информации о продажах по месяцам. Может ли сервер Oracle использовать это представление, выполняя запрос о продажах за квартал или год? Да, мы знаем, что по дате транзакции можно получить месяц, по месяцу — квартал, по кварталу — год, так что — да, может. Серверу Oracle (пока) об этой взаимосвязи не известно, поэтому использовать представление он не будет. С помощью объекта базы данных DIMENSION (измерение) можно сообщить серверу Oracle эти сведения о данных, чтобы он использовал их для переписывания большего количества запросов. Измерение декларирует отношение главный/подчиненный между парами столбцов. С его помощью можно указать серверу Oracle что, в строке таблицы значение столбца MONTH определяет значение, которое окажется в столбце QTR, столбец QTR определяет значение, которое окажется в столбце YEAR и т.д. Используя измерение, можно создать материализованное представление, содержащее менее подробные сведения, чем исходные записи (например, итоговые данные по месяцам). Этот уровень агрегирования может оказаться более детальным, чем требуется в запросе (в запросе, скажем, требуются данные по кварталам), но сервер Oracle разберется, что для получения ответа можно использовать материализованное представление. Вот простой пример. Создадим таблицу SALES для хранения даты транзакции, идентификатора клиента и общей суммы продаж. В этой таблице будет около 350000 строк. Другая таблица, TIME_HIERARCHY, будет содержать соответствие даты транзакции месяцу, кварталу и году. При соединении этих двух таблиц можно получить агрегированные данные по месяцам, кварталам, годам и т.д. Аналогично, если имеется таблица, сопоставляющая идентификатор клиента с почтовым индексом, а почтовые индексы — с регионом, можно легко соединить эту таблицу с таблицей SALES для агрегирования данных по почтовому индексу или региону. В обычной базе данных (без материализованных представлений и других специфических структур) эти действия можно выполнить, но это потребует много времени. Для каждой строки данных продаж придется выполнять чтение по индексу справочной таблицы (соединение вложенным циклом, NESTED LOOP JOIN) для преобразования даты транзакции или идентификатора клиента в другое значение и последующего группирования результатов по этому значению. Вот тут и пригодится материализованное представление. Можно хранить итоговые данные по продажам, агрегированные, скажем, помесячно по датам транзакции и по почтовым индексам клиентов. Теперь обобщение данных поквартально или по регионам может выполняться очень быстро. Начнем с создания таблицы SALES и загрузки в нее случайных тестовых данных, сгенерированных на основе представления ALL_OBJECTS.
Материализованные представления
tkyte@TKYTE816> create table sales 2
(trans_date date, cust_id int, sales_amount number);
Table created. tkyte@TKYTE816> insert /*+ APPEND */ into sales 2 select trunc(sysdate,'year')+mod(rownum,366) TRANS_DATE, 3 mod(rownum,100) CUST_ID, 4 abs(dbms_random.random)/100 SALES_AMOUNT 5 from all_objects 6
/
21921 rows created. tkyte@TKYTE816> commit; Commit complete. Эта исходная информация будет представлять данные за год. Я задаю столбец TRANS_DATE как первый день года плюс число от 1 до 365. Значение CUST_ID — число от 0 до 99. Общая сумма продаж — некоторое сравнительно большое число (год выдался хороший). В моем представлении ALL_OBJECTS содержится около 22000 строк, так что после четырех вставок, каждая из которых удваивает размер таблицы, мы получим около 350000 записей. Я использую подсказку / * + APPEND */, чтобы избежать генерации большого объема данных в журнал повторного выполнения: tkyte@TKYTE816> begin 2 for i in 1 .. 4 3 loop 4 insert /*+ APPEND */ into sales 5 select trans_date, cust_id, abs(dbms_random.random)/100 6 from sales; 7 commit; 8 end loop; 9 end; 10 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select count(*) from sales; COUNT(*) 350736 Теперь необходимо создать таблицу TIME_HIERARCHY, "округляющую" дату до месяца, года, квартала и т.д.: tkyte@TKYTE816> c r e a t e t a b l e time_hierarchy 2 (day primary key, mmyyyy, mon_yyyy, qtr_yyyy, yyyy) 3 organization index 4 as 5 select distinct 6 trans_date DAY, cast (to_char(trans_date,'mmyyyy1) as number) MMYYYY,
90
Глава 13 8 to_char(trans_date,'mon-yyyy') MON_YYYY, 9 'Q' I I c e i l ( to_char(trans_date,'mm 1 )/3) | I ' FY' 10 | | to_char(trans_date,'yyyy') QTR_YYYY, 11 cast( to_char( trans_date, 'yyyy' ) as number ) YYYY 12 from sales 13 / Table created. В данном случае все просто. Мы сгенерировали столбцы: Q MMYYYY — месяц и год; •
MON_YYYY — то же, но с сокращенным названием месяца;
•
QTR_YYYY — квартал и год;
•
YYYY — год.
Однако вычисления, необходимые для создания подобной таблицы, могут быть намного сложнее. Например, кварталы финансового года вычислить не так легко, как и границы финансового года. Как правило, его границы не соответствуют календарному году. Теперь создадим материализованное представление SALES_MV. Оно суммирует исходные продажи за месяц. Можно ожидать, что в полученном материализованном представлении будет примерно 1/30 общего количества строк таблицы SALES, если данные были равномерно распределены: tkyte@TKYTE816> analyze table sales compute s t a t i s t i c s ; Table analyzed. tkyte@TKYTE816> analyze table time_hierarchy compute s t a t i s t i c s ; Table analyzed. tkyte@TKYTE816> create materialized view sales_mv 2 build immediate 3 refresh on demand 4 enable query rewrite 5 as 6 select sales.cust_id, sum(sales.sales_amount) sales_amount, 7 time_hierarchy.mmyyyy 8 from sales, time_hierarchy 9 where sales.trans_date = time_hierarchy.day 10 group by sales.cust_id, time_hierarchy.mmyyyy 11 / Materialized view created. tk.yte@TKYTE816> set autotrace on tkyte@TKYTE816> select time_hierarchy.mmyyyy, sum(sales_amount) 2 from sales, time_hierarchy 3 where sales.trans_date = time_hierarchy.day 4 group by time_hierarchy.mmyyyy 5 /
Материализованные представления
у \
MMYYYY SUM(SALES AMOUNT) 12001 12002 22001 32001 42001 52001 62001 72001 82001 92001 102001 112001 122001
3.2177Е+11 1.0200Е+10 2.8848Е+11 3.1944Е+11 3.1012Е+11 3.2066Е+11 3.0794Е+11 3.1796Е+11 3.2176Е+11 3.0859Е+11 3.1868Е+11 3.0763Е+11 3.1305Е+11
13 rows s e l e c t e d . Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=327 Bytes=850VALVE) SORT (GROUP BY) (Cost=4 Card=327 Bytes=8502) TABLE ACCESS (FULL) OF 'SALES_MV' (Cost=l Card=327 Bytes
Пока все отлично: сервер Oracle переписал запрос так, что используется представление SALES_MV. Однако посмотрим, что произойдет при выполнении запроса, требующего более высокого уровня агрегирования: tkyte@TKYTE816> set timing on tkyte@TKYTE816> s e t autotrace on tkyte@TKYTE816> s e l e c t time_hierarchy.qtr_yyyy, sum(sales_amount) 2 from sales, time_hierarchy 3 where sales.trans_date = time_hierarchy.day 4 group by time_hierarchy.qtr_yyyy 5 / QTR YYYY Ql Ql Q2 Q3 Q4
SUM(SALES AMOUNT)
FY2001 FY2002 FY2001 FY2001 FY2001
9.2969E+11 1.0200E+10 9.3872E+11 9.4832E+11 9.3936E+11
Elapsed: 00:00:05.58 Execution Plan 0 1 2 3 4
0 1 2 2
SELECT STATEMENT Optimizer=CHOOSE (Cost=8289 Card=5 Bytes=14) SORT (GROUP BY) (Cost=8289 Card=5 Bytes=145) NESTED LOOPS (Cost=169 Card=350736 Bytes=10171344) TABLE ACCESS (FULL) OF 'SALES' (Cost-169 Card-350736 В INDEX (UNIQUE SCAN) OF 'SYS IOT TOP 30180' (UNIQUE)
92
Глава 13 Statistics О 15 351853
recursive calls db block gets consistent gets
Как видите, сервер Oracle не знает того, что знаем мы. Он еще не знает, что мог бы использовать материализованное представление для ответа на данный запрос, поэтому использует исходную таблицу SALES, проделывая огромный объем работы для получения ответа. То же самое получится и при запросе обобщенных данных за финансовый год. С помощью объекта DIMENSION проинформируем сервер Oracle о том, что материализованное представление позволяет получить ответ и на эти запросы. Сначала создадим объект DIMENSION: tkyte@TKYTE816> create dimension time_hierarchy_dim 2 level day i s time_hierarchy.day 3 level mmyyyy i s time_hierarchy.mmyyyy 4 level qtr_yyyy i s time_hierarchy.qtr_yyyy 5 level yyyy i s time_hierarchy.yyyy 6 hierarchy time_rollup 7 ( 8 day child of 9 mmyyyy child of 10 qtr_yyyy child of 11 yyyy 12 ) 13 attribute mmyyyy 14 determines mon_yyyy; Dimension created. Этот оператор сообщает серверу Oracle, что столбец DAY таблицы TIME_HIERARCHY определяет значение столбца MMYYYY, который, в свою очередь, определяет значение столбца QTR_YYYY. Наконец, значение столбца QTR_YYYY определяет значение столбца YYYY. Также утверждается, что столбцы MMYYYY и MON_YYYY — синонимы, между ними есть однозначное соответствие. Так что, когда сервер Oracle обнаруживает в запросе столбец MON_YYYY, он обрабатывает запрос так же, как при использовании столбца MMYYYY. Теперь, когда серверу Oracle известна взаимосвязь данных, выполнение запроса существенно ускоряется: tkyte@TKYTE816> s e t autotrace on tkyte@TKYTE816> s e l e c t time_hierarchy.qtr_yyyy, sum(sales_amount) 2 from sales, time_hierarchy 3 where sales.trans_date = time_hierarchy.day 4 group by time_hierarchy.qtr_yyyy 5 / QTR_YYYY Ql FY2001 Ql FY2002
SUM(SALES_AMOUNT) 9.2969E+11 1.0200E+10
Материализованные представления
Q2 FY2001 Q3 FY2001 Q4 FY2001
93
9.3872E+11 9.4832E+11 9.3936E+11
Elapsed: 00:00:00.20 Execution Plan 0 1 2 3 4 5 6
0 1 2 3 4 2
SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=5 Bytes=195) SORT (GROUP BY) (Cost=7 Card=5 Bytes=195) HASH JOIN (Cost=6 Card=150 Bytes=5850) VIEW (Cost=4 Card=46 Bytes=598) SORT (UNIQUE) (Cost=4 Card=46 Bytes=598) INDEX (FAST FULL SCAN) OF 'SYS_IOT_TOP_30180' (UNI TABLE ACCESS (FULL) OF 'SALES_MV' (Cost=l Card=327 Byt
Statistics 0 16 12
recursive calls db block gets consistent gets
Мы сократили количество логических чтений с 350000 до 12 — не так уж плохо. Если выполнить этот пример, различие будет заметно. Для выполнения первого запроса потребовалось некоторое время (около шести секунд), а вот ответ на второй оказался на экране раньше, чем я отпустил клавишу Enter (через две сотых доли секунды). Для одной базовой исходной таблицы можно задавать сколько угодно иерархий с помощью DIMENSION. Давайте свяжем с каждым клиентом в таблице продаж атрибуты ZIP_CODE (почтовый индекс) и REGION (регион): tkyte@TKYTE816> create table custoraer_hierarchy 2 (cust_id primary key, zip_code, region) 3 organization index 4 as 5 select cust_id, 6 mod(rownum, 6) I I to__char(mod( rownum, 1000 7 mod(rownum, 6) region 8 from (select distinct cust id from sales) 9 /
'fmOOOO1) zip code,
Table created. tkyte@TKYTE816> analyze table customer_hierarchy compute s t a t i s t i c s ; Table analyzed. Теперь пересоздадим материализованное представление так, чтобы значения SALES_AMOUNT группировались по столбцам ZIP_CODE и MMYYYY: tkyte@TKYTE816> drop materialized view sales_mv; Materialized view dropped. tkyte@TKYTE816> create materialized view sales_mv 2 build immediate 3 refresh on demand
94
Глава 13 4 enable query rewrite 5 as 6 select customer_hierarchy.zip_code, 7 time_hierarchy.mmyyyy, 8 sum(sales.sales_amount) sales_amount 9 from sales, time_hierarchy, customer_hierarchy 10 where sales.trans_date = time_hierarchy.day 11 and sales.cust_id = customer_hierarchy.cust_id 12 group by customer_hierarchy.zip_code, time_hierarchy.mmyyyy 13 /
Materialized view created. Выполнив запрос, который выдает данные по продажам, сгруппированные по столбцам ZIP_CODE и MMYYYY, можно убедиться, что для их выполнения используется это материализованное представление: tkyte@TKYTE816> s e t autotrace tkyte@TKYTE816> s e l e c t customer_hierarchy.zip_code, 2 time_hierarchy.mmyyyy, 3 sum(sales.sales_amount) sales_amount 4 from sales, time_hierarchy, customer_hierarchy 5 where sales.trans_date = time_hierarchy.day 6 and sales.cust_id = customer_hierarchy.cust_id 7 group by customer_hierarchy.zip_code, time_hierarchy.mmyyyy 8
/
1250 rows selected. Execution Plan 0 1
0
SELECT STATEMENT Optimizer=CHOOSE (Cost-l Card=409 Bytes=2Ci4 TABLE ACCESS (FULL) OF 'SALES_MV' (Cost-l Card=409 Bytes=2
Statistics 28 12 120
recursive calls db block gets consistent gets
Однако если запросить информацию на другом уровне агрегирования (обобщая MMYYYY до YYYY и ZIP_CODE до REGION), окажется, что сервер не счел возможным использовать материализованное представление: tkyte@TKYTE816> s e l e c t customer_hierarchy.region, 2 time_hierarchy.yyyy, 3 sum(sales.sales_amount) sales_amount 4 from sales, time_hierarchy, customer_hierarchy 5 where sales.trans_date = time_hierarchy.day 6 and sales.cust_id = customer_hierarchy.cust_id 7 group by customer_hierarchy.region, time_hierarchy.yyyy 8 / 9 rows selected.
Материализованные представления Execution Plan 0 1 2 3 4 5 6
0 1 2 3 3 2
SELECT STATEMENT Optimizer=CHOOSE (Cost=8289 Card=9 Bytes=26 SORT (GROUP BY) (Cost=8289 Card=9 Bytes=261) NESTED LOOPS (Cost=169 Card=350736 Bytes=10171344) NESTED LOOPS (Cost=169 Card=350736 Bytes=6663984) TABLE ACCESS (FULL) OF 'SALES' (Cost=169 Card=350736 INDEX (UNIQUE SCAN) OF 'SYS_IOT_TOP_30185' (UNIQUE) INDEX (UNIQUE SCAN) OF 'SYS_IOT_TOP_30180' (UNIQUE)
Statistics 0 recursive calls 15 db block gets 702589 consistent gets Сервер учел имеющееся измерение по времени, но у него отсутствует информация о том, как соотносятся столбцы CUST_ID, ZIP_CODE и REGION в таблице CUSTOMER_HIERARCHY. Чтобы исправить это, пересоздадим измерение так, чтобы оно включало две иерархии: одну для таблицы TIME_HIERARCHY, а другую — для CUSTOMER_HIERARCHY: tkyte@TKYTE816> drop dimension time_hierarchy_dim 2 / Dimension dropped. tkyte@TKYTE816> create dimension sales_dimension 2 level cust_id i s customer_hierarchy.cust_id 3 level zip_code is customer_hierarchy.zip_code 4 level region is customer_hierarchy.region 5 level day is time_hierarchy.day 6 l e v e l mmyyyy is time_hierarchy.mmyyyy 7 level qtr_yyyy is time_hierarchy.qtr_yyyy 8 level yyyy i s time_hierarchy.yyyy 9 hierarchy cust_rollup 10 ( 11 cust_id child of 12 zip_code child of 13 region 14 ) 15 hierarchy time_rollup 16 ( 17 day child of 18 mmyyyy child of 19 qtr_yyyy child of 20 yyyy 21 ) 22 attribute mmyyyy 23 determines mon_yyyy; Dimension created.
96
Глава 13
Мы удалили исходную иерархию по времени и создали новое, более информативное измерение, описывающее все существенные взаимосвязи. Теперь сервер Oracle "поймет", что по созданному представлению SALES_MV можно ответить на многие другие запросы. Например, если еще раз запросить "регионы по годам": tkyte@TKYTE816> select customer_hierarchy.region, 2 time_hierarchy.yyyy, 3 sum(sales.sales_amount) sales_amount 4 from sales, time_hierarchy, customer_hierarchy 5 where sales.trans_date = time_hierarchy.day 6 and sales.cust_id = customer_hierarchy.cust_id 7 group by customer_hierarchy.region, time_hierarchy.yyyy REGION
YYYY SALES AMOUNT
2001 2002 2001 2001 2002 2001 2001 2002 2001
0 0 1 2 2 3 4 4 5
5.9598E+11 3123737106 6.3789E+11 6.3903E+11 3538489159 6.4069E+11 6.3885E+11 3537548948 6.0365E+11
9 rows selected. Execution Plan 0 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 3 2 8 9
SELECT STATEMENT Optimizer=CHOOSE (Cost=ll Card=9 Bytes=57 5) SORT (GROUP BY) (Cost=ll Card=9 Bytes=576) HASH JOIN (Cost=9 Card=78 Bytes=4992) HASH JOIN (Cost=6 Card=78 Bytes=4446) VIEW (Cost=3 Card=19 Bytes=133) SORT (UNIQUE) (Cost=3 Card=19 Bytes=133) INDEX (FAST FULL SCAN) OF 'SYS_IOT_TOP_30180' (U TABLE ACCESS (FULL) OF 'SALES_MV' (Cost=l Card=409 В VIEW (Cost=3 Card=100 Bytes=700) SORT (UNIQUE) (Cost=3 Card=100 Bytes=700) INDEX (FULL SCAN) OF 'SYS_IOT_TOP_30185' (UNIQUE)
Statistics 0 16 14
recursive calls db block gets consistent gets
Оказывается, что сервер Oracle смог использовать обе иерархии измерения и выполнил запрос к материализованному представлению. Благодаря созданным измерениям он выполнил простой поиск для преобразования значения столбца CUST_ID в REGION (поскольку значение CUST_ID определяет значение ZIP_CODE, а оно, в свою очередь,
Материализованные представления
у /
определяет REGION), значения столбца MMYYYY — в QTR_YYYY и ответил на запрос почти моментально. Здесь нам удалось сократить количество операций логического ввода-вывода с более чем 700000 до 16. Если учесть, что таблица SALES со временем будет расти, а размер представления SALES_MV будет увеличиваться намного медленнее (примерно 180 записей в месяц), запрос будет очень хорошо масштабироваться.
Пакет DBMS OLAP Последним фрагментом головоломки, которую представляют собой материализованные представления, является пакет DBMS_OLAP. Этот пакет используется для: •
оценки размера материализованного представления в строках и байтах;
•
проверки корректности объектов-измерений, с учетом заданных отношений первичного/внешнего ключа;
•
получения рекомендаций о создании дополнительных материализованных представлений и поиска лишних, которые надо удалить, с учетом их реального использования и структуры или только структуры;
•
оценки использования материализованного представления с помощью предоставляемых процедур, которые информируют о фактической полезности имеющихся материализованных представлений независимо от того, использовались они или нет.
К сожалению, процедуры оценки полезности выходят за рамки тем, которые я могу раскрыть в одной главе. Для использования этих процедур необходимо настроить утилиту Oracle Trace и средства Enterprise Manager Performance Pack, но остальные три процедуры мы рассмотрим. Чтобы использовать пакет DBMS_OLAP, необходимо настроить использование внешних процедур, поскольку большая часть кода пакета DBMS_OLAP хранится в библиотеке, написанной на языке С. Если выдается сообщение об ошибке следующего вида, выполните инструкции по настройке, представленные в главе 18: ERROR a t l i n e
ORA-28575: ORA-06512: ORA-06512: ORA-06512:
1:
unable to open RPC connection to external procedure agent at "SYS.DBMS_SUMADV", line 6 at "SYS.DBMS_SUMMARY", line 559 at line 1
Оценка размера Процедура ESTIMATE_SUMMARY_SIZE информирует о предположительном количестве строк и размере в байтах материализованного представления. Поскольку ретроспективный анализ дает наилучшие результаты, можно оценить эти значения с помощью пакета DBMS_OLAP, а затем сравнить с реальными значениями. Для запуска процедуры необходимо убедиться, что в схеме установлена таблица PLAN_TABLE. Соответствующий оператор CREATE TABLE можно найти в файле [ORACLE_HOME]/rdbms/admin/utlxplan.sql на сервере. При выполнении этого сцена-
4 Зак. 244
98
Глава 13
рия автоматически будет создана таблица PLAN_TABLE. Эта таблица используется при выполнении оператора EXPLAIN PLAN, результаты работы которого, в свою очередь, используются пакетом DBMS_OLAP для оценки размера материализованного представления. При наличии этой таблицы можно использовать встроенную процедуру ESTIMATE_SUMMARY_SIZE для оценки количества строк/байтов в материализованном представлении, которое предполагается создать. Я начну с удаления статистической информации (DELETE STATISTICS) о материализованном представлении SALES_MV. Обычно пакету DBMS_OLAP недоступно материализованное представление, размер которого оценивается, поэтому нам придется этот размер скрыть (в противном случае пакет DBMS_OLAP получит точный ответ по словарю данных): tkyte@TKYTE816> analyze t a b l e sales_mv DELETE s t a t i s t i c s ; Table analyzed. tkyte@TKYTE816> declare 2 3 4
5 6 7 8 9 10 11 12 13 14 15 16 17 18
num__rows number; num_bytes number; begin
dbms_olap.estimate_suiranary_size ('SALES_MV_ESTIMATE' , ' s e l e c t customer_hierarchy.zip_code, time_hierarchy.mmyyyy, sum(sales.sales_amount) sales_amount from sales, time_hierarchy, customer_hierarchy where sales.trans_date = time_hierarchy.day and sales.cust_id = customer_hierarchy.cust_id group by customer_hierarchy.zip_code, time_hierarchy.mmyyyy', num_rows, num_bytes); dbms_output.put_line(num_rows || ' rows'); dbms_output.put_line(num_bytes || ' bytes');
19 end; 20 / 409 rows 36401 bytes PL/SQL procedure successfully completed. Первый параметр процедуры — имя плана, под которым его надо запомнить в таблице планов. Это имя не имеет особого значения, но его надо задать в операторе DELETE FROM PLANJTABLE WHERE STATEMENTJD = 'SALES_MV_ESTIMATE' по завершении эксперимента. Второй параметр — запрос, который будет использоваться для создания материализованного представления. Пакет DBMS_OLAP проанализирует этот запрос на основе статистической информации о базовых таблицах, чтобы оценить размер этого объекта. Остальные два параметра предназначены для передачи результатов работы процедуры пакета DBMS_OLAP — предполагаемого количества строк и байтов. Они получили значения 409 и 36401, соответственно. Теперь давайте подсчитаем реальные значения:
Материализованные представления tkyte@TKYTE816> analyze table salesjnv COMPUTE statistics; Table analyzed. tkyte@TKYTE816> select count(*) from salesjw; COUNT(*) 1250 tkyte@TKYTE816> select blocks * 8 * 1024 2 from user_tables 3 where table name = 'SALES MV' 4 / BLOCKS*8*1024 40960 Итак, процедура ESTIMATE_SUMMARY_SIZE дала хороший результат при оценке размера таблицы, но недооценила количество строк. Обычно с оценками так и происходит: какие-то параметры оцениваются верно, а какие-то — нет. Я бы использовал эту процедуру для грубой оценки предполагаемого размера объекта.
Проверка достоверности измерений Эта процедура проверяет достоверность иерархий, входящих в указанное измерение. Так, в рассмотренном ранее примере она проверит, действительно ли значение CUST_ID определяет значение ZIP_CODE, а то, в свою очередь, определяет значение столбца REGION. Чтобы увидеть соответствующую процедуру в действии, создадим пример недостоверного измерения. Начнем с таблицы, содержащей строку для каждого дня года с атрибутами день, месяц и год: tkyte@TKYTE816> create t a b l e time_rollup 2 (day date, 3 mon number, 4 year number 5 ) 6 / Table created. tkyte@TKYTE816> i n s e r t i n t o time_rollup 2 s e l e c t dt, to_char(dt,'mm'), to_char(dt,'yyyy') 3 from (select trunc(sysdate,'year')+rownum-l dt 4 from all_objects where rownum < 366) 5 / 365 rows created. Итак, мы создали развернутую информацию по дате, аналогично предыдущему примеру. На этот раз, однако, я не включил год в атрибут, представляющий месяц, — только две цифры, представляющие порядковый номер месяца в году. Если добавить в эту таблицу еще одну строку:
100
Глава 13
tkyte@TKYTE816> insert into time_rollup values 2 (add_months(s ysdate,12), 3 to_char(add_months(sysdate,12),'mm'), 4 to_char(add_months(sysdate,12), 'yyyy')) ; 1 row created. проблема станет очевидной. Мы будем утверждать, что значение DAY определяет значение MON, a MON, в свою очередь, определяет значение YEAR, но в данном случае это неверно. Одно и то же значение месяца будет в таблице для двух разных годов. Пакет DBMS_OLAP позволяет проверить достоверность; при этом ошибка будет выявлена. Сначала создаем измерение: tkyte@TKYTE816> create dimension time_rollup_dim 2 level day i s time_rollup.day 3 level mon is time_rollup.mon 4 level year is time_rollup.year 5 hierarchy time_rollup 6 ( 7 day child of mon child of year 8 ) 9 / Dimension created. А затем проверяем его достоверность: tkyte@TKYTE816> exec dbms_olap.validate_dimension('time_rollup_dim', user, false, false); PL/SQL procedure successfully completed. Кажется, что все в порядке, но надо проверить таблицу, автоматически созданную и заполненную в процессе работы с данными: tkyte@TKYTE816> select * from mview$_exceptions; OWNER TABLE NAME
DIMENSION NAME
RELATIONSHI BAD ROWID
TKYTE TIME_ROLLUP TKYTE TIME_ROLLUP TKYTE TIME ROLLUP
TIME_ROLLUP_DIM CHILD OF TIME_ROLLUP_DIM CHILD OF TIME ROLLUP DIM CHILD OF
AAAGkxAAGAAAAcKAA7 AAAGkxAAGAAAAcKAA8 AAAGkxAAGAAAAcKAA9
32 rows selected. Если просмотреть строки, на которые указывает представление MVIEW$_EXCEFTIONS, окажется, что это строки за март (я выполнял этот пример в марте). А именно: tkyte@TKYTE816> s e l e c t * from time_rollup 2 where rowid in (select bad_rowid from mview$_exceptions); DAY
01-MAR-01 02-MAR-01
MON
3 3
YEAR
2001 2001
Материализованные представления 03-MAR-01 04-MAR-01
3 3
2001 2001
30-MAR-01 31-MAR-01 26-MAR-02
3 3 3
2001 2001 2002
1 0 1
32 rows selected. Теперь проблема ясна: значение MON не определяет однозначно YEAR — измерение недостоверно. Его использование небезопасно, поскольку будет получен неверный ответ. Рекомендуется проверять достоверность измерений после их изменения, чтобы гарантировать целостность результатов, получаемых из материализованных представлений благодаря наличию этих измерений.
Рекомендация создания материализованных представлений Один из наиболее интересных вариантов использования пакета DBMS_OLAP — определение того, какие материализованные представления имеет смысл создавать. Процедура RECOMMEND делает именно это. Имеется две версии этой процедуры. •
Процедура RECOMMEND_MV анализирует структуру таблицы, внешние ключи, материализованные представления, всю соответствующую статистическую информацию, а затем выдает список рекомендаций в порядке убывания приоритетов.
•
Процедура RECOMMEND_MV_W идет еще дальше. Если используется утилита Oracle Trace и Enterprise Manager Performance Packs, процедура анализирует запросы, обрабатываемые системой, и рекомендует создание материализованных представлений на основе информации о реальной работе.
В качестве простого примера, оценим с помощью пакета DBMS_OLAP существующую "таблицу фактов" SALES. Таблица фактов (таблица основной информации) — это таблица в схеме "звезда", содержащая фактические данные. Использованная ранее таблица SALES — это таблица фактов. В таблице фактов обычно есть два типа столбцов: столбцы, содержащие факты (например, столбец SALES_AMOUNTв нашей таблице SALES), и столбцы, являющиеся внешними ключами к таблицам измерений (например, к таблице TRANS_DATE для нашей таблицы SALES).
Давайте посмотрим, что порекомендует пакет DBMS_OLAP. Сначала необходимо создать внешние ключи. Процедуры RECOMMEND не будут анализировать объекты DIMENSION при выработке рекомендаций — для определения связей между таблицами им необходимы внешние ключи:
102
Глава 13
tkyte@TKYTE816> alter table sales add constraint t_fk_time 2 foreign key( trans_date) references time_hierarchy 3 / Table altered. tkyte@TKYTE816> alter table sales add constraint t_fk_cust 2 foreign key( cust_id) references customer_hierarchy 3 / Table altered. После этого можно анализировать нашу таблицу фактов, SALES: tkyte@TKYTE816> exec dbms_olap.recommend_mv('SALES',
10000000000, ' ' ) ;
PL/SQL procedure successfully completed. Мы попросили процедуру RECOMMEND_MV: •
проанализировать таблицу SALES;
Q учесть, что для материализованных представлений места предостаточно (мы просто передали очень большое значение в качестве ограничения); •
не оставлять без необходимости существующих материализованных представлений (мы передали " в качестве списка имен сохраняемых представлений).
Теперь можно либо непосредственно обращаться к заполненным этой процедурой таблицам, либо, что удобнее, использовать простую процедуру для выдачи их содержимого. Для установки этой процедуры и построения отчета надо выполнить: tkyte@TKYTE816> @С:\oracle\RDBMS\demo\sadvdemo Package c r e a t e d . Package body c r e a t e d . Package c r e a t e d . Package body c r e a t e d . tkyte@TKYTE816> e x e c demo_sumadv.prettyprint_recommendations Recommendation Number = 1 Recommended A c t i o n i s CREATE new summary: SELECT CUSTOMER_HIERARCHY.CUST_ID, CUSTOMER_HIERARCHY.ZIP_CODE, CUSTOMER_HIERARCHY.REGION , COUNT(*), SUM(SALES.SALES_AMOUNT), COUNT(SALES.SALES_AMOUNT) FROM TKYTE.SALES, TKYTE.CUSTOMER_HIERARCHY WHERE SALES.CUST_ID = CUSTOMER_HIERARCHY.CUST_ID GROUP BY CUSTOMER_HIERARCHY.CUST_ID, CUSTOMER_HIERARCHY.ZIP_CODE, CUSTOMER_HIERARCHY.REGION Storage in bytes is 2100 Percent performance gain i s 43.2371266138567 Benefit-to-cost r a t i o is .0205891079113603 Recommendation Number = 2 ... PL/SQL procedure successfully completed.
Материализованные представления
1U3
Процедура пакета DBMS_OLAP учла существующие измерения и материализованные представления и выдала рекомендации по созданию дополнительных материализованных представлений, которые могут оказаться полезными с учетом имеющихся у сервера метаданных (первичных ключей, внешних ключей и измерений). При использовании утилиты Oracle Trace можно пойти в процессе выработки рекомендаций чуть дальше. Утилита Oracle Trace позволяет перехватывать фактические запросы к данным, принимаемые сервером, и записывать подробную информацию о них. Эта информация будет использоваться пакетом DBMS_OLAP для выработки еще более точных рекомендаций, основанных не только на потенциальных возможностях, но и на реальных запросах, выполняемых к данным. При этом не рекомендуется создание материализованных представлений, которые теоретически могли бы, но не будут использоваться при таком потоке запросов. Рекомендоваться будут только те материализованные представления, которые будут использованы для выполнения запросов, реально поступающих в систему.
Проб При использовании материализованных представлений надо учитывать ряд соображений. Мы кратко поговорим о них в этом разделе.
Материализованные представления не предназначены для систем OLTP Как уже упоминалось, поддержка материализованных представлений требует дополнительных затрат ресурсов при выполнении отдельных транзакций и, если представления созданы с опцией REFRESH ON COMMIT, приводит к конфликтам. Дополнительные затраты ресурсов связаны с необходимостью учета изменений, выполненных транзакцией, — эти изменения будут либо сохраняться в качестве данных состояния сеанса, либо регистрироваться в таблицах. В системах, интенсивно обрабатывающих транзакции, такие дополнительные затраты ресурсов нежелательны. Проблема одновременного доступа возникает для материализованных представлений с опцией REFRESH ON COMMIT из-за того, что многие записи в исходной таблице фактов ссылаются на одну строку в таблице итогов. Изменение любой из многих тысяч записей, которые могут существовать, приводит к необходимости изменить одну строку в таблице итогов. Это, естественно, мешает одновременному доступу при интенсивных изменениях. Это не исключает использования материализованных представлений, в частности представлений, обновляемых по требованию (REFRESH ON DEMAND), в среде OLTP при полном обновлении. Полное обновление не требует дополнительных затрат ресурсов на отслеживание изменений на уровне транзакций. Вместо этого в определенный момент времени выполняется запрос, определяющий материализованное представление, и его результаты просто заменяют существующее материализованное представление. Поскольку делается это по требованию (или периодически), такое обновление можно выполнять в периоды низкой загруженности сервера. Полученное материализованное представление особенно пригодится для создания отчетов: данные OLTP можно каж-
104
Глава 13
дую ночь преобразовывать с помощью SQL-операторов в форму, упрощающую и ускоряющую выполнение запросов. На следующий день оперативные отчеты по результатам вчерашней работы выполняются максимально быстро, не мешая при этом обработке транзакций.
Целостность запросов при переписывании Как было описано ранее, есть три режима обеспечения целостности. О ENFORCED. Будет использовать материализованное представление, только если при этом невозможно получение некорректных или устаревших данных. Q TRUSTED. Сервер Oracle будет использовать материализованное представление, даже если некоторые ограничения, на которые он при этом полагается, им не проверяются и не обеспечиваются. Эта ситуация типична в среде хранилища данных, где многие ограничения целостности соблюдены, но не поддерживаются сервером Oracle. О STALE_TOLERATED. Сервер Oracle будет использовать материализованное представление, даже если "знает", что данные, по которым оно построено, изменились. Эта ситуация типична для среды создания отчетов, вроде описанной выше. Надо понимать последствия использования каждого из этих режимов. Режим ENFORCED дает правильные ответы всегда — за счет отказа от использования некоторых материализованных представлений, которые могли бы ускорить выполнение запроса. В режиме TRUSTED, если окажется, что условие, которому сервер Oracle попросили доверять, в действительности не выполняется, могут выдаваться результаты, отличные от получаемых по исходным данным. Такая ситуация была рассмотрена н примере с материализованным представлением EMP_DEPT. Режим STALEJTOLERATED следует использовать в системах создания отчетов, где вполне допустимо получить устаревшее значение. Если требуется актуальная информация с точностью до минуты, режим STALE TOLERATED использовать нельзя.
Резюме Материализованные представления — мощное средство повышения производительности хранилищ данных и систем поддержки принятия решений. Одно материализованное представление может использоваться многими взаимосвязанными запросами. Самое главное, что его использование полностью прозрачно для приложения и пользователя. Не нужно сообщать пользователям, какие таблицы итогов поддерживаются, — об этом информируется сервер Oracle с помощью ограничений целостности ссылок и измерений. Все остальное сервер сделает автоматически. Материализованные представления — естественное развитие и объединение различных возможностей сервера и средств поддержки принятия решений. Средства поддержки таблиц итогов Oracle Discoverer (и других подобных программ) уже не ограничены только этой средой. Теперь любой клиент, от замечательного SQL*Plus до разработанного вами приложения и готовых средств создания отчетов, может использовать уже где-то хранящийся ответ на его запрос.
Материализованные представления
Добавьте к этому возможности пакета DBMS_OLAP. Он не только позволяет оценить, сколько дополнительно места на диске понадобится для поддержки материализованного представления, но и может следить за использованием существующих представлений. На основе этой информации пакет рекомендует удалить одни и создать другие представления — вплоть до выдачи текста запроса, который имеет смысл для этого использовать. В конечном итоге, материализованные представления в среде, где данные только читаются или интенсивно читаются, несомненно, оправдают выделенное для них дополнительное место на диске, сократив время выполнения и экономя ресурсы, необходимые для фактического выполнения запросов.
Секционирование Возможность секционирования, то есть разбиения таблицы или индекса на несколько меньших, проще управляемых частей, впервые появилась в сервере Oracle версии 8.0. Логически для обращающегося к базе данных приложения есть только одна таблица или индекс. Физически же эта таблица или индекс могут состоять из многих десятков секций. Каждая секция — самостоятельный объект, с которым можно работать отдельно или как с частью большего объекта. Секционирование разрабатывалось для упрощения управления очень большими таблицами и индексами за счет применения подхода "разделяй и властвуй". Предположим, в базе данных имеется индекс размером 10 Гбайт. Если необходимо перестроить этот несекционированный индекс, придется перестраивать весь индекс в один прием. Хотя сервер способен перестраивать такие индексы динамически, объем ресурсов, необходимых для полного пересоздания всего индекса размером 10 Гбайт, огромен. Потребуется еще не менее 10 Гбайт свободного пространства для хранения обоих экземпляров индекса, необходима временная таблица журнала транзакций для записи изменений, сделанных в базовой таблице за время пересоздания индекса, и т.д. С другой стороны, если индекс разбит на десять секций размером 1 Гбайт, можно пересоздавать каждую секцию индекса отдельно, по одной. При этом потребуется только 10 процентов свободного пространства, которое понадобилось бы при пересоздании несекционированного индекса. Пересоздание индекса пройдет намного быстрее (возможно, раз в десять), намного уменьшится объем выполненных транзакциями изменений, которые придется учесть в новом индексе, и т.д.
108
Глава 14
Короче, секционирование может сделать устрашающие по поглощению ресурсов, а иногда даже невозможные в большой базе данных операции настолько же простыми, как в маленькой.
Использование секционирования Для использования секционирования имеются три причины: Q повышение доступности данных; •
упрощение администрирования;
Q повышение производительности запросов и операторов DML.
Повышение доступности данных Повышение доступности данных достигается за счет того, что секции являются независимыми сущностями. Доступность (или недоступность) одной секции объекта не означает, что весь объект доступен (или недоступен). Оптимизатор учтет реализованную схему секционирования и удалит из плана выполнения запроса секции, данные из которых не запрашиваются. Если недоступна одна секция большого объекта и запрос не обращается к данным в этой секции, сервер Oracle успешно выполнит этот запрос. Давайте создадим секционированную по хеш-функции таблицу из двух секций, хранящихся в разных табличных пространствах, и вставим в нее данные. Для каждой вставляемой в таблицу строки значение в столбце EMPNO хешируется с целью определения секции (а значит, и табличного пространства, в данном случае), в который следует помещать данные. Затем, дополнив имя таблицы именем секции, просмотрим содержимое каждой секции: tkyte@TKYTE816> CREATE TABLE emp 2 (empno int, 3 ename varchar2(20) 4 ) 5 PARTITION BY HASH (empno) 6 (partition part_l tablespace pi, 7 partition part_2 tablespace p2 8 ) 9 / Table created. tkyte@TKYTE816> insert into emp select empno, ename from scott.emp 2 / 14 rows created. tkyte@TKYTE816> select * from emp partition(part_l); EMPNO ENAME 7369 7499 7654 7698
SMITH ALLEN MARTIN BLAKE
Секционирование
109
7782 CLARK 7839 KING 7876 ADAMS 7934 MILLER 8 rows s e l e c t e d . tkyte@TKYTE816> s e l e c t * from emp p a r t i t i o n ( p a r t 2 ) ; EMPNO ENAME 7521 WARD 7566 JONES 7788 SCOTT 7844 TURNER 7900 JAMES 7902 FORD 6 rows selected. Теперь сделаем часть данных недоступными, отключив одно из используемых табличных пространств, и выполним запрос, затрагивающий обе секции, чтобы показать невозможность его выполнения. Вы увидите, что запрос, не обращающийся к отключенному табличному пространству, работает как обычно — сервер Oracle не обращается к отключенному табличному пространству. В этом примере я использовал связываемую переменную, чтобы продемонстрировать, что, даже если в момент оптимизации запроса сервер не знает, к какой секции будут обращаться, он все равно пропустит ненужную секцию: tkyte@TKYTE816> a l t e r tablespace p i
offline;
Tablespace a l t e r e d . tkyte@TKYTE816> s e l e c t * from emp 2 / s e l e c t * from emp ERROR a t l i n e 1: ORA-00376: file 4 cannot be read at this time ORA-01110: data file 4: 'C:\ORACLE\ORADATA\TKYTE816\P1.DBF' tkyte@TKYTE816> variable n number tkyte@TKYTE816> exec :n := 7844 PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from emp where empno = :n 2 / EMPNO ENAME 7844 TURNER Как видите, мы отключили одно из табличных пространств, сымитировав сбой диска. В результате, если потребуется обратиться ко всей таблице, естественно, ничего не получится. Однако обратиться к данным, находящимся в доступной секции, можно. Когда оптимизатор может удалить секции из плана выполнения запроса, он это делает.
по
Глава 14
Этот факт увеличивает доступность данных для приложений, использующих в запросах ключ, по которому выполнено секционирование. Секции повышают доступность также благодаря сокращению времени простоя. Например, если таблица размером 100 Гбайт разбита на 50 секций размером 2 Гбайта, восстановление в случае ошибок выполняется в 50 раз быстрее. Если одна из секций размером 2 Гбайта повреждена, для ее восстановления нужно намного меньше времени, чем для восстановления таблицы размером 100 Гбайт. Таким образом, доступность повышается по двум направлениям: многие пользователи могут вообще не заметить, что данные были недоступны, благодаря тому, что сбойная секция пропускается, а время простоя при сбое сокращается вследствие существенно меньшего объема работы, необходимой для восстановления.
Упрощение администрирования Упрощение администрирования связано с тем, что операции с маленькими объектами выполнять гораздо проще, быстрее и при этом требуется меньше ресурсов, чем в случае больших объектов. Например, если оказалось, что 50 процентов строк в таблице фрагментированы (подробнее о фрагментации и переносе строк см. в главе 6, посвященной таблицам), и необходимо это исправить, секционирование таблицы очень пригодится. Чтобы исключить фрагментацию строк, как правило, пересоздается объект, в данном случае — таблица. Если таблица размером 100 Гбайт, придется выполнять эту операцию одним большим "куском", последовательно, с помощью оператора ALTER TABLE MOVE. Если же эта таблица разбита на 25 секций размером 4 Гбайта, можно перестраивать секции по одной. Более того, если это делается в период минимальной загруженности сервера, можно даже выполнять операторы ALTER TABLE MOVE параллельно в отдельных сеансах, что позволяет сократить время пересоздания. Практически все, что делается с несекционированным объектом, можно сделать и с частью секционированного объекта. Еще один фактор, который необходимо учитывать при оценке влияния секционирования на администрирование, — использование смещающегося окна данных в хранилищах данных и при архивировании. Во многих случаях необходимо сохранять доступными последние (по времени создания) N групп данных. Например, необходимо предоставлять данные за последние 12 месяцев или пять лет. При отсутствии секционирования это обычно связано с множественной вставкой новых данных, а затем — множественным удалением устаревших. Для этого необходимо выполнить множество операторов DML, сгенерировать множество данных повторного выполнения и отката. При использовании секционирования можно: • загрузить в отдельную таблицу данные за новый месяц (или год, или любой другой период); Q полностью проиндексировать таблицу (эти шаги можно сделать вообще в другом экземпляре, а результаты перенести в текущую базу данных); Q добавить ее в конец секционированной таблицы; • удалить самую старую секцию с другого конца секционированной таблицы.
Секционирование
Итак, теперь легко поддерживать очень большие объекты, содержащие хронологическую информацию. Устаревшие данные просто удаляются из секционированной таблицы, если они не нужны, или помещаются в архив. Новые данные можно загрузить в отдельную таблицу, чтобы не мешать доступу к секционированной таблице во время загрузки, индексирования и т.п. Полный пример использования смещающегося окна будет представлен далее.
Повышение производительности операторов DML и запросов Еще одним преимуществом секционирования является повышение производительности запросов и операторов DML. Мы рассмотрим преимущества секционирования для этих двух категорий операторов отдельно. Повышение производительности операторов DML связано с потенциальной возможностью распараллеливания. При распараллеливании операторов DML сервер Oracle использует несколько потоков или процессов для выполнения операторов INSERT, UPDATE или DELETE. На многопроцессорной машине с большой пропускной способностью ввода-вывода потенциальное ускорение для операторов DML, выполняющих множественные изменения, может быть весьма большим. В отличие от параллельных запросов (обработки несколькими процессами/потоками оператора SELECT), для распараллеливания операторов DML требуется секционирование (есть специальный случай параллельной непосредственной вставки, задаваемой с помощью подсказки /*+ APPEND */, когда секционирование не требуется). Если таблицы не секционированы, распараллелить операторы DML не удастся. Сервер Oracle присваивает каждому объекту максимальную степень распараллеливания в зависимости от количества составляющих его секций. Не стоит ожидать от распараллеливания операторов DML ускорения работы приложений оперативной обработки транзакций (OLTP). В этом отношении есть много заблуждений. Я много раз слышал: "Параллельно выполняемые операции должны давать результаты быстрее, чем при последовательном выполнении". Это не всегда верно. При параллельном выполнении некоторых операций требуется во много раз больше времени, чем при последовательном. На организацию параллельного выполнения расходуются определенные ресурсы, необходима дополнительная координация. Более того, распараллеливание вообще не стоит использовать в среде интенсивной оперативной обработки транзакций — в этом просто нет смысла. Распараллеливание операций предназначено исключительно для максимального использования ресурсов системы. При параллельном выполнении один пользователь может единолично использовать все диски, процессоры и всю свободную память машины. В среде хранилища данных, где данных много, а пользователей мало, именно это и требуется. В системе OLTP (большое количество пользователей, выполняющих короткие, быстрые транзакции) предоставление пользователю возможности полностью использовать ресурсы машины отрицательно скажется на масштабируемости. В этом есть кажущееся противоречие: мы используем распараллеливание запроса для ускорения работы с большими объемами данных, как же это решение может быть не-
112
Глава 14
масштабируемым? Применительно к системе OLTP, однако, это утверждение абсолютно верно. Параллельные запросы плохо масштабируются при увеличении количества пользователей, одновременно их выполняющих. Параллельные запросы позволяют в одном пользовательском сеансе выполнять столько же действий, как в ста одновременных сеансах. Однако в системе OLTP нежелательно, чтобы один пользователь выполнял столько же действий, как сто. Распараллеливание операторов DML полезно в среде больших хранилищ данных как средство множественного изменения больших объемов данных. Распараллеленный оператор DML выполняется сервером во многом аналогично параллельному запросу: каждая секция используется как отдельный экземпляр базы данных. Каждая секция изменяется отдельным потоком в отдельной транзакции (поэтому при изменении возможно использование отдельного сегмента отката), а когда все изменения закончатся, происходит нечто подобное быстрой двухфазной фиксации отдельных, независимых транзакций. В связи с такой архитектурой при распараллеливании операторов DML возникает ряд ограничений. Например, в ходе параллельного выполнения оператора DML не поддерживаются триггеры. Это, по-моему, разумное ограничение, поскольку выполнение триггеров при изменении обычно существенно увеличивает использование ресурсов, а распараллеливание выполняется для максимального ускорения — это несовместимые действия. Кроме того, при распараллеливании операторов DML не поддерживается ряд декларативных ограничений целостности ссылок, поскольку каждая секция обрабатывается в отдельной транзакции, по сути — в отдельном сеансе. Не поддерживается, например, целостность ссылок объекта на самого себя. Представьте себе проблемы блокирования и взаимные блокировки, которые могли бы возникнуть при проверке таких огрпничений. С точки зрения производительности секционирование обеспечивает две специализированные операции. • Игнорирование секции. Некоторые секции данных не просматриваются при обработке запроса. Пример игнорирования секции был представлен ранее, когда мы отключили одно из табличных пространств и смогли, тем не менее, выполнить запрос. Отключенное табличное пространство при выполнении запроса было пропущено. • Параллельное выполнение операций. Распараллеливать можно соединения по секциям, если объекты секционированы по ключам соединения, или просмотр индексов. Как и в случае распараллеливания операторов DML, не стоит ожидать от секционирования существенного повышения производительности в среде OLTP. Игнорирование секции эффективно при полном просмотре больших объектов. Пропуская секцию, можно избежать просмотра больших частей объекта. Именно за счет этого может повышаться производительность. В среде OLTP, однако, большие объекты полностью не просматриваются (если просматриваются — это серьезная ошибка проектирования). Даже если секционировать индексы, ускорение при просмотре меньшей части индекса будет незначительным (или его вообще не будет). Если часть запросов использует индекс и при поиске можно пропустить только одну секцию индекса, то запросы после такого секционирования могут выполняться медленнее, поскольку вместо одного большого индекса
Секционирование
\
j
теперь надо просматривать 5,10 или 20 маленьких. Более детально мы изучим это позже, при рассмотрении типов секционирования индексов. Имеется возможность повысить эффективность работы системы OLTP с помощью секционирования, например, повышая степень параллелизма за счет уменьшения количества конфликтов. Секционирование можно использовать для распределения изменений одной таблицы по нескольким физическим секциям. Вместо использования одного сегмента таблицы и одного сегмента индекса можно создать 20 секций таблицы и 20 секций индекса. Результат будет таким же, как при наличии 20 таблиц вместо одной, — конфликтов при изменениях этого разделяемого ресурса станет меньше. Что касается распараллеливания запросов, то оно, как я уже подчеркивал ранее, в среде OLTP нежелательно. Распараллеливание операций имеет смысл оставить администратору базы данных для пересоздания и создания индексов, анализа таблиц и т.п. Для запросов в системе OLTP обычно характерен очень быстрый доступом к данным по индексу, так что секционирование не слишком ускорит этот доступ. Это не означает, что следует вообще отказаться от использования секционирования в среде OLTP, просто не стоит ожидать существенного повышения производительности только за счет секционирования объектов. Приложения в данном случае не могут использовать преимущества, которые могло бы дать секционирование. В среде же хранилища данных/систем поддержки принятия решений (DSS) секционирование не только существенно упрощает администрирование, но и позволяет ускорить работу. Предположим, имеется большая таблица, к которой надо выполнить запрос, анализирующий продажи по кварталам. Для каждого квартала имеются сотни тысяч записей, а общее количество записей измеряется миллионами. Итак, необходимо запросить сравнительно небольшой срез общего набора данных, но индексировать данные по кварталам нет смысла. Такой индекс будет ссылаться на сотни тысяч записей для каждого квартала, и просмотр индекса по диапазону при этом будет выполняться чрезвычайно медленно (подробнее об этом см. в главе 7, посвященной индексам). Итак, для выполнения большого количества запросов приходится полностью просматривать таблицу, но при этом просматриваются миллионы записей, большинство из которых не связаны с выполняемым запросом. Используя соответствующую схему секционирования, можно разделить данные по кварталам таким образом, что при запросе данных за конкретный квартал полностью просматривать придется только одну секцию. Это лучшее из всех возможных решений. Кроме того, в среде хранилищ данных/систем поддержки принятия решений часто используется распараллеливание запросов. Здесь операции типа параллельного просмотра индексов по диапазону или параллельный, быстрый полный просмотр индекса не только имеют смысл, но и дают преимущества. Мы хотим максимально использовать все имеющиеся ресурсы, и распараллеливание запросов позволяет этого добиться. Так что, в подобной среде секционирование с большой долей вероятности ускорит выполнение запросов. Упорядочив преимущества секционирования по степени важности, получим: 1. Увеличение доступности данных — хорошо для всех типов систем. 2. Упрощение администрирования больших объектов базы данных за счет замены их несколькими меньшими — хорошо для всех типов систем.
114
Глава 14
3. Повышение производительности некоторых операторов DML и запросов — достигается, в основном, в среде больших хранилищ данных. 4. Уменьшение количества конфликтов в системах OLTP с большим количеством вставок (например, в таблицу записей аудита) за счет их распределения по нескольким отдельным секциям (распределение "горячей точки" по нескольким дискам).
Как выполняется секционирование В этом разделе будут рассмотрены схемы секционирования, предлагаемые сервером Oracle 8i. Имеется три схемы секционирования для таблиц и две — для индексов. В рамках двух схем секционирования индексов можно выделить несколько классов секционированных индексов. Мы рассмотрим преимущества и отличительные особенности каждого класса, а также разберемся, какие схемы секционирования следует применять для различных типов приложений.
Схемы секционирования таблиц В настоящее время сервер Oracle поддерживает три способа секционирования таблиц. •
Секционирование по диапазону. Можно указать диапазоны значений данных, строки для которых должны храниться вместе. Например, все данные за январь 2001 года будут храниться в секции 1, все данные за февраль 2001 года — в секции 2 и т.д. Это, вероятно, самый популярный способ секционирования в Oracle 8i.
•
Хеш-секционирование. Такая схема уже использовалась в первом примере этой главы. К значению одного или нескольких столбцов применяется хеш-функция, определяющая секцию, в которую помещается строка.
•
Составное секционирование. Это сочетание секционирования по диапазону и хешсекционирования. Можно сначала применить разбиение по диапазону значений данных, а затем выбрать в пределах диапазона секциию на основе значения хешфункции.
Следующий код и схемы наглядно демонстрируют применение этих способов секционирования. Кроме того, операторы CREATE TABLE структурированы так, чтобы можно было понять синтаксис создания секционированной таблицы. Сначала рассмотрим таблицу, секционированную по диапазону: tkyte@TKYTE816> CREATE TABLE range_example 2 (range_key_column date, 3 data varchar2(20) 4 ) 5 PARTITION BY RANGE (range_key_column) 6 (PARTITION part_l VALUES LESS THAN 7 (to_date('01-jan-1995','dd-mon-yyyy')), 8 PARTITION part_2 VALUES LESS THAN 9 (to_date('01-jan-1996','dd-mon-yyyy')) 10 )
Секционирование 11
^ j
/
Table created. Следующая схема показывает, что сервер Oracle проверяет столбец RANGE_KEY_COLUMN и в зависимости от его значения вставляет строку в одну из секций: inser t into range_example (range_k ey_column, data) values (to_date('01-jan-1994', 'dd-mon-yyyy'), 'application data');
inser t into range_example (range_k ey_column, data) values (to_dateC01-mar-1995', 'dd-mon-yyyy'), 'application data');
Секция_1
Секция_2
Интересно, что произойдет, если изменится значение столбца, определяющего, в какую секцию попадает строка. При этом надо учитывать два случая. •
Изменение не перемещает строку в другую секцию; строка принадлежит той же секции. Такое изменение возможно всегда.
•
Изменение вызывает перемещение строки в другую секцию. Такое изменение поддерживается, только если для таблицы включен перенос строк, иначе выдается сообщение об ошибке.
Эту особенность легко продемонстрировать. Вставим строку в секцию PART_1 созданной ранее таблицы. Затем изменим значение столбца RANGE_KEY_COLUMN так, что строка останется в секции PART_1, — это изменение будет успешно выполнено. Далее изменим значение столбца RANGE_KEY_COLUMN так, чтобы переместить строку в секции PART_2. При этом будет выдано сообщение об ошибке, поскольку перенос строк явно включен не был. Наконец, изменим таблицу, разрешив перемещение строк, и продемонстрируем последствия этого изменения: tkyte@TKYTE816> insert into range_example 2 values (to_date('Ol-jan-1994', 'dd-mon-yyyy'),
1
'application data );
1 row created. tkyte@TKYTE816> update range_example 2 set range_key_column = range_key_column+l 3 / 1 row updated. Как и ожидалось, изменение успешно выполнено, ведь строка осталась в секции PART_1. Затем посмотрим, что произойдет, если изменение вызывает перемещение строки:
116
Глава 14
tkyte@TKYTE816> update range_example 2 set range_key_column = range_key_column+366 3 / update range_exarrtple * ERROR a t l i n e 1: ORA-14402: updating p a r t i t i o n key column would cause a p a r t i t i o n change Сразу же выдается сообщение об ошибке. В Oracle 8.1.5 и более новых версиях можно включить поддержку переноса строк для таблицы, что позволит перемещать строку из одного фрагмента в другой. В версиях Oracle 8.0 это было невозможно; приходилось удалять строку и повторно вставлять ее с измененными значениями. Следует, однако, помнить о побочном эффекте перемещения строк. Это — один из двух случаев, когда идентификатор строки (ROWID) изменяется при изменении данных (другой — изменение первичного ключа таблицы, организованной по индексу; универсальный идентификатор для этой строки тоже изменится): tkyte@TKYTE816> s e l e c t rowid from range_example 2 / ROWID AAAHeRAAGAAAAAKAAA tkyte@TKYTE816> alter table range_example enable row movement 2 / Table altered. tkyte@TKYTE816> update range_example 2 set range_key_column = range_key_column+366 3 / 1 row updated. tkyte@TKYTE816> select rowid from range_example 2 / ROWID AAAHeSAAGAAAABKAAA Итак, если учитывать изменение идентификатора строки при изменении значения ключа секционирования, включение перемещения строк позволит изменять эти ключи. Следующий пример демонстрирует хеш-секционирование таблицы. В этом случае сервер Oracle будет применять хеш-функцию к ключу секционирования для определения того, в какую из N секций надо поместить данные. Для наиболее равномерного распределения рекомендуется в качестве значения N использовать степени двойки (2, 4, 8, 16 и т.д.). Хеш-секционирование предназначено для равномерного распределения данных по нескольким устройствам (дискам). В качестве хеш-ключа для таблицы необходимо выбирать столбец или набор столбцов с как можно большим количеством уникальных значений — это обеспечивает равномерное распределение значений. Если выбрать столбец, имеющий всего четыре значения, и использовать две секции,,, в результате хеширования все строки могут оказаться в одной секции, что делает секционирование бессмысленным.
Секционирование
117
Создадим таблицу, распределенную по хеш-функции на две секции: tkyte@TKYTE816> CREATE 2 (hash_key_column 3 data 4 5 PARTITION BY HASH 6 (partition part_l partition part_2 7 8 9
TABLE hash_example date, varchar2(20) (hash_key_colurnn) tablespace pi, tablespace p2
Table created. Следующая схема показывает, что сервер Oracle применит хеш-функцию к столбцу HASH_KEY_COLUMN и, в зависимости от ее значения, вставит строку в одну из двух секций: inser t into hash_example (hash_k ey_column, data) values (to_date('01-jan-1994', 'dd-mon-yyyy'), 'application data'); Hash (01-jan-1994) = part_2
inser t into hash_example (hash_k ey_column, data) values {to_date('01-mar-1995', 'dd-mon-yyyy'), 'application data'); Hash (01-mar-1995) = part _1
Теперь рассмотрим пример смешанного секционирования, когда строки секционируются и по диапазону, и по хеш-функции. Здесь секционирование по диапазону будет выполняться для одного набора столбцов, а хеш-секционирование — для другого. Вполне допустимо использовать одни и те же столбцы в обоих условиях секционирования: tkyte@TKYTE816> CREATE TABLE composite_example 2 (range_key_column date, 3 hash_key_column int, 4 data varchar2(20) 5 ) 6 PARTITION BY RANGE (range_key_column) 7 subpartition by hash(hash_key_column) subpartitions 2 8 ( 9 PARTITION part_l 10 VALUES LESS THAN(to_date('01-jan-1995','dd-mon-yyyy')) 11 (subpartition part_l_sub_l, 12 subpartition part_l_sub_2 13 ), 14 PARTITION part 2
118 15 16 17 18 19 20
Глава 14
VALUES LESS THAN(to_date('01-jan-1996','dd-mon-yyyy')) (subpartition part_2_sub_l, s u b p a r t i t i o n part_2_sub_2
Table created. При смешанном секционировании сервер Oracle сначала применяет правила секционирования по диапазону, чтобы понять, к какому диапазону относятся данные, а затем — хеш-функцию, которая и определяет, в какую физическую секцию попадет строка: inser t into composite_example values (to_date('01-jan-19941, vdd-mon-yyyy'), 123, 'application data');
inser t into composite_example values (to_date('03-mar-1995', 'dd-mon-yyyy'), 456, 'application data');
'Hash (123) = sub_2
Hash (456) = sub _1
1
Секция_1
Sub_1 - Подсекция!,
Sub_2 - Подсекция J>
Секционирование по диапазону используется, когда данные логически разделяются по значениям. Классический пример — данные, привязанные к периоду времени. Секционирование по кварталам, по финансовым годам, по месяцам. Секционирование по диапазону во многих случаях позволяет пропускать секции, в том числе для условий строгого равенства и условий, задающих диапазоны: меньше, больше, в указанных пределах и т.д. Хеш-секционирование подходит для данных, в которых не удается выделить естественные диапазоны значений, подходящие для секционирования. Предположим, необходимо загрузить в таблицу данные переписи населения — в них может и не быть атрибута, по которому имеет смысл разделять данные на диапазоны. Однако хотелось бы воспользоваться преимуществами, которые предоставляет секционирование с точки зрения администрирования, производительности и доступности данных. Можно выбрать набор столбцов с уникальными значениями, по которым выполнять хеширование. Это позволит равномерно распределить данные по любому количеству секций. Игнорирование секции для объектов, разделенных по хеш-функции, возможно только для условий строгого равенства или IN (значение1, значение2,...), но не для условий, задающих диапазоны значений. Составное секционирование подходит, когда данные логически поделены на диапазоны, но получающиеся в результате секции — слишком большие, чтобы ими можно было
Секционирование
\у
эффективно управлять. Можно разделить данные по диапазону, а затем разбить каждую секцию на несколько подсекций по хеш-функции. Это позволит распределить операции ввода-вывода по нескольким дискам для каждой большой секции. Кроме того, теперь можно пропускать секции на трех уровнях. Если запрос выполняется по ключу, использованному при секционировании по диапазону, сервер может пропустить все секции, не отвечающие критериям запроса. Если в запросе будет указан еще и ключ хеширования, сервер сможет пропустить другие подсекции в соответствующем диапазоне. Если в запросе используется только ключ хеш-функции (а ключ, по которому выполнено секционирование по диапазону, не используется), сервер будет обращаться только к соответствующим подсекциям в каждом диапазоне. Рекомендуется преимущественно использовать секционирование по диапазону, если данные вообще имеет смысл разбивать на диапазоны по каким-то атрибутам. Хеш-секционирование имеет множество преимуществ, но она не так эффективна с точки зрения возможностей игнорирования секций. Использовать хеш-секционирование в пределах секций по диапазону рекомендуется, когда соответствующие диапазонам секции слишком велики для эффективного управления, или в тех случаях, когда желательно распараллеливать операторы DML или иметь возможность параллельного просмотра индексов для отдельной секции.
Секционирование индексов Индексы, как и таблицы, можно секционировать. Существует два способа секционирования индексов. •
Можно секционировать индекс по тем же критериям, что и базовую таблицу. Такие индексы называют локально секционированными. Для каждой секции таблицы будет создана соответствующая секция индекса, индексирующая только эту секцию таблицы. Все записи в данной секции индекса ссылаются на одну секцию таблицы, а все строки секции таблицы представлены в одной секции индекса.
•
Можно секционировать индекс по диапазону. Такие индексы называют глобально секционированными. Индекс секционируется по диапазону, и одна секция индекса может ссылаться на любые (хоть все) секции базовой таблицы.
Следующие схемы показывают различие между локально и глобально секционированными индексами. секция индекса А
секция индекса В
секция индекса А
секция индекса В
исекция таблицы А
секция таблицы В
локально секционированный индекс
секция таблицы А
—- к -
1
секция таблицы В глобально секционированный индекс
секция таблицы С
120
Глава 14
Следует помнить, что количество секций глобально секционированного индекса может не совпадать с количеством секций таблицы. Поскольку глобально секционированные индексы можно секционировать только по диапазону, при секционировании индекса по хеш-функции или в случае составного секционирования придется использовать локально секционированные индексы. Локально секционированный индекс использует такую же схему секционирования, как и базовая таблица.
Локально секционированные индексы Практика показывает, что чаще всего используются локально секционированные индексы. Дело в том, что секционирование, как правило, используется в среде хранилищ данных. В системах OLTP более типичны глобально секционированные индексы. Локально секционированные индексы наиболее подходят для хранилищ данных. Они обеспечивают большую доступность данных (меньшее время простоя), поскольку проблемы доступности, скорее всего, будут связаны с одним диапазоном или хеш-секцией данных. Вследствие того что глобально секционированный индекс ссылается на несколько секций, для определенных запросов секции могут стать недоступными. Локально секционированные индексы обеспечивают большую гибкость при сопровождении секции. Если администратор базы данных решит перенести секцию таблицы, изменять придется только соответствующую секцию локально секционированного индекса. Если индекс секционирован глобально, изменять придется все его секции. То же самое и в случае "смещающегося окна" данных, когда старые данные удаляются, а новые — добавляются в таблицу в виде отдельной секции. При этом нет необходимости изменять локально секционированные индексы, а вот все глобально секционированные придется изменить. В некоторых случаях сервер Oracle может учитывать факт локального секционироиания индекса аналогично таблице, и вырабатывать на основе этого факта оптимальные планы выполнения запросов. При использовании глобально секционированных индексов такой взаимосвязи секций индекса и таблицы нет. Локально секционированные индексы также помогают при восстановлении состояния секций на определенный момент времени. Если необходимо восстановить состояние одной секции на более ранний момент времени, чем всю остальную таблицу, все локально секционированные индексы можно восстановить на этот же момент. Все глобально секционированные индексы для такого объекта придется пересоздавать. Выделяется два типа локально секционированных индексов. • Локально секционированные индексы с префиксом. Это индексы, в которых ключи секционирования являются начальными ключами индекса. Например, если таблица секционирована по диапазону значений столбца TIMESTAMP, в списке столбцов ключа локально секционированного индекса с префиксом по этой таблице столбец TIMESTAMP будет первым. Q Локально секционированные индексы без префикса. Это индексы, в которых ключи секционирования не являются начальными ключами индекса. Такой индекс может содержать или не содержать столбцы ключа секционирования. Оба типа индексов обеспечивают игнорирование секции, оба могут поддерживать уникальность (если индекс без префикса включает ключ секционирования) и т.д. Зап-
Секционирование
121
рос, использующий локально секционированный индекс с префиксом, всегда позволяет пропускать секцию, а запрос, использующий локально секционированный индекс без префикса, может и не позволить это сделать. Вот почему утверждается, что локально секционированные индексы без префикса "медленнее"; они не гарантируют игнорирование секций (хотя его и поддерживают). Кроме того, как будет продемонстрировано ниже, при выполнении некоторых операций оптимизатор будет обрабатывать локально секционированные индексы без префикса не так, как индексы с префиксом. В документации Oracle подчеркивается, что: локально секционированные индексы с префиксом обеспечивают более высокую производительность, чем индексы без префикса, потому что уменьшают количество проверяемых оптимизатором индексов Понимать это надо так: локально секционированные индексы обеспечивают более высокую производительность для_ЗАПРОСОВ, ссылающихся на весь входящий в них ключ секционирования, по сравнению с ЗАПРОСАМИ, не ссылающимися на ключ секционирования Локально секционированные индексы с префиксом, использующиеся для начального доступа к таблице в запросе, не имеют существенных преимуществ по сравнению с индексами без префикса. Я имею в виду, что, если выполнение запроса может начаться с просмотра индекса (SCAN AN INDEX), особой разницы между индексами с префиксом и без префикса нет. Позже, когда будет рассматриваться использование секционированных индексов в соединениях, вы увидите разницу между индексами с префиксом и без префикса. Для запроса, выполнение которого начинается с доступа по индексу, все зависит от условия, использованного в запросе. Продемонстрирую это на маленьком примере. Создадим таблицу PARTITIONED_TABLE и локально секционированный индекс с префиксом LOCAL_PREFIXED по ней. Кроме того, добавим локально секционированный индекс без префикса LOCALJVONPREFIXED: tkyte@TKYTE816> CREATE TABLE p a r t i t i o n e d _ t a b l e 2 (a int, 3 b int 4 ) 5 PARTITION BY RANGE (a) 6 ( 7 PARTITION part_l VALUES LESS THAN(2), 8 PARTITION part_2 VALUES LESS THAN(3) 9 ) 10 / Table created. tkyte@TKYTE816> create index local_prefixed on partitioned_table (a,b) ^local; Index created.
122
Глава 14
tkyte@TKYTE816> create index local_nonprefixed on partitioned_table (b)
*• local; Index created. Теперь вставим данные в одну секцию и пометим секции индексов как не используемые (UNUSABLE): tkyte@TKYTE816> insert into partitioned_table values (1, 1 ) ; 1 row created. tkyte@TKYTE816> alter index local_prefixed modify partition part_2 unusable; Index altered. tkyte@TKYTE816> alter index local_nonprefixed modify partition part_2 ^unusable; Index altered. Пометка этих секций индекса как UNUSABLE предотвращает доступ к ним сервера Oracle. Все будет точно так же, как если бы произошел сбой носителя, — секции недоступны. Теперь выполним запросы к таблице, чтобы разобраться, какие секции индексов потребуются для разных запросов: tkyte@TKYTE816> s e t autotrace on explain tkyte@TKYTE816> s e l e c t * from p a r t i t i o n e d _ t a b l e where a = 1 and b = 1;
Execution Plan 0 1
0
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=l Bytes=26) INDEX (RANGE SCAN) OF 'LOCAL_PREFIXED' (NON-UNIQUE) (Cost=l
Итак, запрос, использующий индекс LOCAL_PREFIX, успешно выполнен. Оптимизатор смог исключить секцию PART_2 индекса LOCAL_PREFIX из рассмотрения, поскольку в запросе задано условие А=1. Нам помогло игнорирование секции. Для второго запроса: tkyte@TKYTE816> s e l e c t * from p a r t i t i o n e d _ t a b l e where b = 1; ERROR: ORA-01502: index •TKYTE.LOCAL_NONPREFIXED' or partition of such index is in unusable state no rows selected Execution Plan 0 1 2 3
0 1 2
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=2 Bytes=52) PARTITION RANGE (ALL) TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'PARTITIONEDJTABLE' INDEX (RANGE SCAN) OF 'LOCAL_NONPREFIXED' (NON-UNIQUE)
Оптимизатор не смог исключить из рассмотрения секцию PART_2 индекса OCAL_NONPREFIXED. С этим и связана проблема производительности при исполь-
Секционирование
123
зовании локально секционированных индексов без префикса. Они используются и для запросов, не включающих ключ секционирования, в отличие от индексов с префиксом. Дело не в том, что индексы с префиксом лучше, просто они используются запросами, обеспечивающими возможность игнорирования секций. Если удалить индекс LOCAL_PREFIXED и еще раз выполнить исходный, успешно выполненный запрос: tkyte@TKYTE816> s e l e c t * from p a r t i t i o n e d _ t a b l e where a = 1 and b = 1; А
В
1
1
E x e c u t i o n Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=l Bytes=26) TABLE ACCESS (BY LOCAL INDEX.ROWID) OF 'PARTITIONEDJTABLE' INDEX (RANGE SCAN) OF 'LOCAL_NONPREFIXED' (NON-UNIQUE) (Cost=l
Этот результат может показаться удивительным. Почти такой же план выполнения, как и в случае неудавшегося запроса, но на этот раз все работает. Причина в том, что оптимизатор может пропускать секции даже для локально секционированных индексов без префикса (в этом плане нет шага PARTITION RANGE(ALL)). Если к представленной выше таблице часто выполняются запросы вида: select . . . from partitioned_table where a = :a and b = :b; select . . . from partitioned_table where b = :b; имеет смысл создать локально секционированный индекс без префикса по столбцам (Ь,а); он пригодится для обоих представленных выше запросов. Локально секционированный индекс с префиксом по столбцам (а,Ь) пригодится только для первого запроса. Однако при использовании секционированных индексов в соединениях результаты могут быть другими. В представленных выше примерах сервер Oracle по условию запроса мог во время оптимизации определить, можно или нельзя пропустить секции. Это было понятно по условию в конструкции WHERE (даже если в нем использовались связываемые переменные). Когда доступ по индексу используется в качестве начального, основного метода доступа, особой разницы между локально секционированными индексами с префиксом и без префикса нет. При соединении с локально секционированным индексом, однако, все меняется. Рассмотрим следующую таблицу, секционированную по диапазону: tkyte@TKYTE816> CREATE TABLE range_example 2 (range_key_column date, 3 x int, 4 data varchar2(20) 5 ) 6 PARTITION BY RANGE (range_key_column) 7 (PARTITION part_l VALUES LESS THAN 8 (to_date('01-jan-1995','dd-mon-yyyy')), 9 PARTITION part_2 VALUES LESS THAN 10 (to_date('01-jan-1996','dd-mon-yyyy')) 11 )
124
Глава 14
12
/
Table created. tkyte@TKYTE816> alter table range_example 2 add constraint range_example_j?k 3 primary key (range_key_column,x) 4 using index local 5 / Table altered. tkyte@TKYTE816> insert into range_example values (to_date('Ol-jan-1994'). 1, **' xxx' ) ; 1 row created. tkyte@TKYTE816> i n s e r t into range_example values (to_date('Ol-jan-1995'), 2, *•*' xxx') ; 1 row created. Сначала по таблице создан локально секционированный индекс с префиксом для первичного ключа. Чтобы увидеть разницу между индексами с префиксом и без префикса, необходимо создать еще одну таблицу. Используем эту таблицу в качестве иедущей в запросе к таблице RANGE_EXAMPLE. Таблица TEST просто используется в качестве ведущей в запросе, который будет выполнять соединение вложенными циклами с таблицей RANGE_EXAMPLE: tkyte@TKYTE816> c r e a t e t a b l e t e s t (pk, range_key_column, x, 2 constraint test_pk primary key(pk)) 3 as 4 select rownum, range_key_column, x from range_example 5 / Table created. tkyte@TKYTE816> set autotrace on explain tkyte@TKYTE816> select * from test, range_example 2 where test.pk = 1 3 and test.range_key_column = range_example.range_key_column 4 and test.x - range example.x 5 / PK RANGE_KEY 1
01-JAN-94
X RANGE_KEY
X DATA
1 01-JAN-94
1 xxx
Execution Plan 0 1 2 3 4 5 6
0 1 2 1 4 5
SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=l Bytes=69) NESTED LOOPS (Cost=2 Card=l Bytes=69) TABLE ACCESS (BY INDEX ROWID) OF 'TEST' (Cost=l Card=l INDEX (RANGE SCAN) OF 'TEST_PK' (UNIQUE) (Cost-1 Card=l) PARTITION RANGE (ITERATOR) TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'RANGE_EXAMPLE' INDEX (UNIQUE SCAN) OF 'RANGE EXAMPLE PK' (UNIQUE)
Секционирование
125
Представленный выше план будет обрабатываться следующим образом. 1. С помощью индекса TEST_PK находим строки в таблице TEST, соответствующие условию test.pk = 1. 2. Обращаемся к таблице TEST для выборки значений остальных столбцов TEST — range_key_column и х. 3. По выбранным на предыдущем шаге значениям с помощью RANGE_EXAMPLE_PK находим единственную соответствующую секцию со строками таблицы RANGE_EXAMPLE. 4. Обращаемся к одной секции таблицы RANGE_EXAMPLE для выбора значений столбца данных. Это кажется достаточно очевидным, но давайте посмотрим, что произойдет при изменении порядка следования столбцов range_key_column и х и превращении индекса в индекс без префикса: tkyte@TKYTE816> a l t e r t a b l e range_example 2 drop constraint range_example_pk 3 / Table altered. tkyte@TKYTE816> alter table range_example 2 add constraint range_example_pk 3 primary key (x, range_key_column) 4 using index local 5 / Table altered. tkyte@TKYTE816> select * from test, range_example 2 where test.pk = 1 3 and test.range_key_column = range_example.range_key_column 4 and test.x = range_example.x PK RANGE_KEY
X RANGE_KEY
X DATA
1 01-JAN-94
1 01-JAN-94
1 xxx
Execution Plan 0 1 2 3 4 5
0 1 2 1 4
SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=l Bytes=69) NESTED LOOPS (Cost=2 Card=l Bytes=69) TABLE ACCESS (BY INDEX ROWID) OF 'TEST' (Cost=l Card=l INDEX (RANGE SCAN) OF 'TEST_PK' (UNIQUE) (Cost=l Card=l) PARTITION RANGE (ITERATOR) TABLE ACCESS (FULL) OF 'RANGE_EXAMPLE' (Cost=l Card=164
Неожиданно оказывается, что с точки зрения сервера Oracle этот новый индекс слишком неэффективен. Это один из случаев, когда использование индекса с префиксом гораздо предпочтительнее.
126
Глава 14
Итак, не стоит бояться локально секционированных индексов без префикса или считать их причиной снижения производительности. Если имеется много запросов, которые могут использовать индекс без префикса, как было показано выше, имеет смысл его создать. Главное — проверить, есть ли в запросах условия, позволяющие использовать игнорирование секции индекса. При использовании локально секционированных индексов с префиксом это гарантируется. При использовании индексов без префикса — нет. Следует также учитывать, как именно используется индекс: при использовании этих двух типов индексов на первом шаге выполнения запроса особых различий нет. Если же индекс используется для выполнения соединения, как в предыдущем примере, индексы с префиксом имеют преимущество. Если можно использовать локально секционированный индекс, создавайте именно его.
Локально секционированные индексы и уникальность Для поддержки уникальности, задаваемой ограничениями целостности UNIQUE или PRIMARY KEY, ключ секционирования индекса должен входить в соответствующее требование. По-моему, это самая главная особенность локально секционированных индексов. Сервер Oracle обеспечивает уникальность только в пределах секции индекса, но не среди нескольких секций. Это означает, например, что нельзя секционировать данные по диапазону значений столбца TIMESTAMP и обеспечить поддержку первичного ключа по столбцу ID с помощью локально секционированного индекса. Сервер Oracle для обеспечения уникальности создаст один глобальный индекс. Например, если выполнить следующий оператор CREATE TABLE в схеме, где нет объектов (чтобы можно было легко понять, какие объекты созданы, просто запросив все сегменты данного пользователя), окажется: tkyte@TKYTE816> CREATE TABLE p a r t i t i o n e d 2 (timestamp date, 3 id int primary key 4 ) 5 PARTITION BY RANGE (timestamp) 6 ( 7 PARTITION part_l VALUES LESS THAN 8 (to_date('01-jan-2000','dd-mon-yyyy')), 9 PARTITION part_2 VALUES LESS THAN 10 (to_date('01-jan-2001','dd-mon-yyyy')) 11 ) 12 / Table created. tkyte@TKYTE816> select segment_name, partition_name, segment_type 2 from user_segments; SEGMENT_NAME
PARTITION_NAME
SEGMENTJTYPE
PARTITIONED PARTITIONED SYS_C003582
PART_2 PART_1
TABLE PARTITION TABLE PARTITION INDEX
Индекс SYS_C003582 — несекционированный, он и не мог таким оказаться. Это означает, что вы теряете ценные для хранилищ данных свойства секционирования. Обыч-
Секционирование
127
ные действия с секциями в хранилищах данных больше нельзя будет выполнять независимо. Так, при добавлении новой секции данных, придется пересоздавать весь глобальный индекс, а также изменять все действия с секциями. Сравните с таблицей, имеющей только локально секционированные индексы — их не нужно пересоздавать, если только не затрагивается соответствующая секция. Если попытаться обмануть сервер Oracle, воспользовавшись тем, что ограничение первичного ключа может обеспечиваться и неуникальным индексом, окажется, что это тоже не помогает: tkyte@TKYTE816> CREATE TABLE p a r t i t i o n e d 2 (timestamp date, 3 id int 4 ) 5 PARTITION BY RANGE (timestamp) 6 ( 7 PARTITION part_l VALUES LESS THAN 8 (to_date('01-jan-2000','dd-mon-yyyy')), 9 PARTITION part_2 VALUES LESS THAN 10 (to_date('01-jan-2001','dd-mon-yyyy')) 11 ) 12 / Table created. tkyte@TKYTE816> create index partitioned_index 2 on partitioned(id) 3 LOCAL 4 / Index created. tkyte@TKYTE816> select segment_name, partition_name, segment_type 2 from user_segments; SEGMENT_NAME
PARTITION_NAME
SEGMENTJTYPE
PARTITIONED PARTITIONED PARTITIONED_ INDEX PARTITIONED_ INDEX
PART_2 PART_1 PART_2 PART_1
TABLE TABLE INDEX INDEX
PARTITION PARTITION PARTITION PARTITION
tkyte@TKYTE816> alter table partitioned 2 add constraint partitioned_pk 3 primary key(id) 4 / alter table partitioned * ERROR at line 1: ORA-01408: such column l i s t already indexed Здесь сервер Oracle попытался создать глобальный индекс по столбцу ID, но обнаружил, что не может этого сделать, потому что индекс уже существует. Представленные выше операторы сработали бы, если бы был создан несекционированный индекс (сервер Oracle просто использовал бы его для выполнения требования).
128
Глава 14
Если ключ секционирования не входит в условие, обеспечить уникальность нельзя по двум причинам. Во-первых, если бы сервер Oracle это разрешал, то были бы сведены на нет преимущества секционирования. Доступность и масштабируемость были бы потеряны, поскольку требовался бы просмотр всех секций и доступ к ним при любой вставке или изменении данных. Чем больше секций, тем менее доступны данные. Чем больше секций в таблице, тем больше секций индекса приходится просматривать и тем менее масштабируемой становится схема секционирования. То есть секционирование ухудшит оба показателя. Кроме того, серверу Oracle пришлось бы последовательно выполнять вставки и изменения этой таблицы на уровне транзакций. Дело в том, что при добавлении строки со значением ID=1 в секцию PART_1, серверу пришлось бы предотвращать вставку строки со значением Ш = 1 другими сеансами в секцию PART_2. Единственный способ добиться этого — запретить изменять секцию PART_2, поскольку другого метода заблокировать эту секцию просто нет. В системе OLTP для обеспечения целостности данных ограничения уникальности должны обеспечиваться системой (сервером Oracle). Это означает, что логическая модель данных будет влиять на физическую. Необходимость поддерживать уникальность либо будет определять схему секционирования, диктуя выбор ключей секционирования, либо наличие этих требований приведет к необходимости использования глобально секционированных индексов. Рассмотрим глобально секционированные индексы более подробно.
Глобально секционированные индексы Глобально секционированные индексы разбиваются на секции не так, как базовая таблица. Таблица может быть разбита по значению столбца TIMESTAMP на десять секций, а глобально секционированный индекс по этой таблице может быть разбит на пять секций по значению столбца REGION. В отличие от локально секционированных, есть только один класс глобально секционированных индексов — с префиксом. Глобально секционированные индексы, ключ которых не начинается с ключа секционирования, не поддерживаются. Продолжая предыдущий пример, ниже я представлю простой вариант использования глобально секционированного индекса. Вы убедитесь, что глобально секционированный индекс обеспечивает уникальность первичного ключа, так что можно использовать секционированные индексы, обеспечивающие уникальность, но не включающие ключ секционирования базовой таблицы. В следующем примере создается таблица, секционированная по столбцу TIMESTAMP, индекс которой секционирован по столбцу ID: tkyte@TKYTE816> CREATE TABLE p a r t i t i o n e d 2 (timestamp date, 3 id int 4 ) 5 PARTITION BY RANGE (timestamp) 6 ( 7 PARTITION part_l VALUES LESS THAN 8 (to_date('01-jan-2000','dd-mon-yyyy')), 9 PARTITION part_2 VALUES LESS THAN
Секционирование 10 11
)
12
/
\/,у
(to_date('01-jan-2001','dd-mon-yyyy'))
Table created. tkyte@TKYTE816> create index partitioned_index 2 on partitioned(id) 3 GLOBAL 4 partition by range(id) 5 ( 6 partition part_l values less than(lOOO), 7 partition part_2 values less than (MAXVALUE) о
)
9
/
Index created. Обратите внимание на использование в этом индексе значения MAXVALUE. Значение MAXVALUE можно использовать в таблицах или индексах, секционированных по диапазону. Оно представляет максимально возможное значение. До сих пор в примерах использовались жесткие верхние границы диапазонов (значения, меньшие чем Определенное значение>). Однако для глобально секционированного индекса требуется, чтобы секция с наибольшими значениями (последняя секция) имела верхний предел MAXVALUE. Это гарантирует, что все строки базовой таблицы можно будет проиндексировать. Добавим для таблицы первичный ключ: tkyte@TKYTE816> a l t e r t a b l e p a r t i t i o n e d add c o n s t r a i n t 2 partitioned_pk 3 primary key(id) 4 / Table altered. Пока еще не очевидно, что сервер Oracle использует для поддержки первичного ключа созданный индекс. Доказать это можно с помощью "волшебного" запроса к словарю данных. Этот запрос необходимо выполнять от имени учетной записи, имеющей привилегию SELECT на базовые таблицы словаря данных или привилегию SELECT ANY TABLE: tkyte@TKYTE816> s e l e c t t.name table_name 2 , u.name owner 3 , с name constraint name 4 , l. name mdex_name 5 , decode(bitandfi.flags, 4), 4, 'Yes', 6 decode(i.name, c.name, 'Possibly', 'No')) generated 7 from sys.cdef$ cd 8 , sys.con$ с 9 , sys.obj$ t in
w*
•
10 , sys.ob]$ l 11 , sys.user$ u 12 where cd.type# between 2 and 3 13 and cd.con# = c.con# 14 and cd.obj# = t.obj# 5 Зак. 244
130
Глава 14
15 and 16 and 17 and 18 /
cd.enabled = i.obj# с owner # = u.userf c.owner# = uid
TABLE_NAME
OWNER CONSTRAINT_NAME INDEX_NAME
PARTITIONED
TKYTE PARTITIONED_PK
GENERATE
PARTITIONED_INDEX No
Запрос показывает, какой индекс использовался для поддержки данного ограничения, и пытается "угадать", сгенерировано ли имя индекса автоматически или задано явно. В данном случае он показывает, что для поддержки первичного ключа используется только что созданный индекс PARTITIONED_INDEX (имя которого не было сгенерировано автоматически). Чтобы показать, что сервер Oracle не позволит создать глобальный индекс без префикса, достаточно попробовать: tkyte@TKYTE816> create index partitioned_index2 2 on partitioned(timestamp,id) 3 GLOBAL 4 partition by range(id) 5 ( 6 partition part_l values less than(lOOO), 7 partition part_2 values less than (MAXVALUE) 8 ) 9 / partition by range(id) * ERROR at line 4: ORA-14038: GLOBAL p a r t i t i o n e d index must be prefixed Сообщение об ошибке весьма красноречиво. Глобально секционированный индекс должен быть с префиксом. Итак, когда же надо использовать глобально секционированный индекс? Рассмотрим два типа систем — хранилища данных и системы OLTP — и выясним, когда эти индексы могут пригодиться.
Хранилища данных и глобально секционированные индексы Я считаю, что эти две вещи несовместимы. Хранилища данных предполагают определенные особенности; добавляются и удаляются большие объемы данных, высока вероятность сбоя на каком-нибудь из дисков и т.д. В любом хранилище данных, использующем перемещающееся окно, лучше избегать использования глобально секционированных индексов. Вот пример того, что я имею в виду под перемещающимся окном, и как на его использование влияет глобально секционированный индекс: tkyte@TKYTE816> CREATE TABLE p a r t i t i o n e d 2 (timestamp date, 3 id int 4 ) 5 PARTITION BY RANGE (timestamp) 6 (
Секционирование
131
7 PARTITION fy_1999 VALUES LESS THAN 8 (to_date('01-jan-2000','dd-mon-yyyy')), 9 PARTITION fy_2000 VALUES LESS THAN 10 (to_date('01-jan-2001','dd-mon-yyyy')), 11 PARTITION the_rest VALUES LESS THAN 12 (maxvalue) 13 ) 14 / Table created. tkyte@TKYTE816> insert into partitioned partition(fy_1999) 2 select to_date('31-dec-1999')-mod(rownum,360), object_id 3 from all_objects 4 / 21914 rows created. tkyte@TKYTE816> insert into partitioned partition(fy_2000) 2 select to_date('31-dec-2000')-mod(rovmum,360), object_id 3 from all_objects 4 / 21914 rows created. tkyte@TKYTE816> create index partitioned_idx_local 2 on partitioned(id) 3 LOCAL Index created. tkyte@TKYTE816> create index partitioned_idx_global 2 on partitioned(timestamp) 3 GLOBAL 4 / Index created. Итак, мы создали таблицу "хранилища данных". Данные секционированы по финансовому году; оперативно доступны данные за последние два года. По этой таблице создано два секционированных индекса; один — как LOCAL, а другой — как GLOBAL. Обратите внимание, что я оставил пустую секцию, THE_REST, в конце таблицы. Это поможет быстро добавлять новые данные. Предположим, очередной финансовый год закончился, и необходимо сделать следующее. 1. Удалить данные за самый давний финансовый год. Эти данные не теряются, они просто считаются устаревшими и архивируются. 2. Добавить данные за последний финансовый год. Для их загрузки, преобразования, индексирования и т.д. потребуется определенное время. Хотелось бы, чтобы эти действия не влияли на доступность остальных данных. Итак, можно выполнить следующее: tkyte@TKYTE816> create table fy_1999 (timestamp date, id Table created.
int);
1 JL
Глава 14
tkyte@TKYTE816> create index fy_1999_idx on fy_1999(id) 2 / Index created. tkyte@TKYTE816> create table fy_2001 (timestamp date, id int); Table created. tkyte@TKYTE816> insert into fy_2001 2 select to_date('31-dec-2001')-mod(rownum,360), object_id 3 from all_objects 4 / 21922 rows created. tkyte@TKYTE816> create index fy_2001_idx on fy_2001(id) nologging 2 / Index created. Здесь я создал новую пустую таблицу-"оболочку" и индекс для самых старых данных. Преобразуем текущую полную секцию в пустую и создадим "полную" таблицу с данными секции FY_1999. Кроме того, я заранее выполнил все необходимые действия по подготовке данных для секции FY_2001. Речь идет о проверке достоверности данных, преобразовании и любых других сложных действиях по их подготовке. Теперь все готово для изменения "актуальных" данных: tkyte@TKYTE816> a l t e r t a b l e p a r t i t i o n e d 2 exchange partition fy_1999 3 with table fy_1999 4 including indexes 5 without validation 6 / Table altered. tkyte@TKYTE816> alter table partitioned 2 drop partition fy 1999 3 / ~ Table altered. Вот и все, что необходимо сделать для удаления "устаревших" данных. Мы превратили секцию в отдельную целую таблицу, а пустую таблицу — в секцию. Это было просто изменение словаря данных, никаких больших объемов ввода-вывода. Теперь можно экспортировать эту таблицу (возможно, с помощью переносимого табличного пространства) из базы данных с целью архивирования. При необходимости ее очень легко присоединить снова. Теперь добавим новые данные: tkyte@TKYTE816> a l t e r t a b l e p a r t i t i o n e d 2 split partition the_rest 3 at (to_date('01-jan-2002','dd-mon-yyyy')) 4 into (partition fy_2001, partition the_rest) 5 / Table altered.
Секционирование
133
tkyte@TKYTE816> a l t e r table partitioned 2 exchange partition fy_2001 3 with table fy_2001 4 including indexes 5 without validation 6 / Table altered. Изменение словаря данных было выполнено моментально. Отделение пустой секции требует очень мало времени, поскольку данных там никогда не было и не будет. Вот зачем я поместил пустую секцию в конец таблицы, — чтобы упросить отделение. Затем вновь созданная пустая секция заменяется всей таблицей, а таблица — пустой секцией. Новые данные оперативно доступны. Однако, если посмотреть на индексы, оказывается: tkyte@TKYTE816> s e l e c t index_name, s t a t u s from user_indexes; INDEX NAME —
STATUS
FY_1999_IDX FY_2001_IDX PARTITIONED_IDX_GLOBAL PARTITIONED_IDX_LOCAL
VALID VALID UNUSABLE N/A
Глобально секционированный индекс после этих действий, несомненно, недоступен. Поскольку каждая секция индекса может указывать на секцию таблицы, и мы удалили одну секцию, и добавили другую, индекс некорректен. В нем есть записи, ссылающиеся на удаленную секцию. В нем нет записей, ссылающихся на добавленную секцию. Запрос, использующий этот индекс, не будет выполнен: tkyte@TKYTE816> s e l e c t count(*) 2 from p a r t i t i o n e d 3 where timestamp between sysdate-50 and sysdate; s e l e c t count(*) ERROR a t l i n e 1: ORA-01502: index 'TKYTE.PARTITIONED_IDX_GLOBAL' or p a r t i t i o n of such index i s in unusable s t a t e Можно установить параметр SKIP_UNUSABLE_INDEXES=TRUE, но тогда мы теряем повышение производительности, которое обеспечивал индекс (это работает в Oracle 8.1.5 и более поздних версиях; до этого оператор SELECT все равно пытался бы использовать индекс, помеченный как UNUSABLE). Этот индекс надо пересоздать, чтобы снова обеспечить возможность использования данных. Процесс перемещения окна, который до сих пор происходил без задержек, теперь будет выполняться очень долго, пока не будет пересоздан глобальный индекс. Придется просмотреть все данные и полностью пересоздать индекс по данным таблицы. Если таблица имеет размер в сотни гигабайт, для этого потребуются существенные ресурсы. Любая операция с секцией таблицы сделает невозможным использование глобально секционированного индекса. Если необходимо перенести секцию на другой диск, все глобально секционированные индексы надо пересоздавать (для локально секционирован-
134
Глава14
ных индексов достаточно пересоздать только соответствующие секции). Если окажется, что необходимо разделить секцию на две меньших, все глобально секционированные индексы придется пересоздавать (для локально секционированных индексов достаточно пересоздать только соответствующие пары секций). И так далее. Поэтому надо избегать использования глобально секционированных индексов в среде хранилища данных. Их использование может негативно повлиять на многие действия.
Системы OLTP и глобально секционированные индексы Система OLTP характеризуется частым выполнением множества небольших транзакций, читающих и изменяющих данные. Обычно беспокоиться о поддержке перемещающихся окон данных не приходится. Прежде всего необходимо обеспечить быстрый доступ к строкам. Целостность данных — жизненно важна. Доступность данных также имеет большое значение. Глобально секционированные индексы имеет смысл использовать в системах OLTP. Данные таблицы могут быть секционированы только по одному ключу, по одному набору столбцов. Однако могут понадобиться различные способы доступа к данным. Можно секционировать данные в таблице EMPLOYEE по местонахождению офиса. Однако при этом необходимо также обеспечить быстрый доступ к данным по следующим столбцам. Q DEPARTMENT. Отделы географически разнесены, и однозначного соответствия между отделом и его местонахождением нет. Q EMPLOYEE_ID. Хотя идентификатор сотрудника определяет его местонахождение, не хотелось бы искать данные по EMPLOYEE_ID и LOCATION, поскольку при этом не удастся обеспечить игнорирование секций индекса. Кроме того, значения столбца EMPLOYEE_ID сами по себе должны быть уникальны. • JOBJTITLE. Необходимо обращаться к данным таблицы EMPLOYEE по многим различным ключам, из разных частей приложения, причем, скорость доступа является основным требованием. В хранилище данных мы просто использовали бы локально секционированные индексы по перечисленным выше ключам и параллельный просмотр диапазонов по индексам для быстрого доступа к данным. Там не нужно было бы использовать игнорирование секций, но в системе OLTP, однако, это необходимо. Распараллеливание запроса в таких системах неприемлемо — надо предоставить соответствующие индексы. Поэтому необходимо использовать глобально секционированные индексы по некоторым полям. Итак, необходимо достичь следующих целей: Q быстрый доступ; Q целостность данных; О доступность данных. В системе OLTP этого позволяют добиться глобально секционированные индексы, поскольку характеристики этой системы существенно отличаются от хранилища данных. Не будут использоваться перемещающиеся окна, не придется делить секции (разве что в период запланированного простоя), не нужно переносить данные из одного таблич-
Секционирование
135
ного пространства в другое и т.д. Действия, типичные для хранилищ данных, обычно не выполняются в системе оперативной обработки транзакций. Вот небольшой пример, показывающий, как добиться трех перечисленных выше целей с помощью глобально секционированных индексов. Я собираюсь использовать простые глобальные индексы из одной секции, но результаты будут такими же и для глобально секционированных индексов (разве что возрастет доступность и управляемость при добавлении секции): tkyte@TKYTE816> c r e a t e t a b l e emp 2 (EMPNO NUMBER(4) NOT NULL, 3 ENAME VARCHAR2(10), 4 JOB VARCHAR2(9), 5 MGR NUMBER(4), 6 HIREDATE DATE, 7 SAL NUMBER(7,2), 8 COMM NUMBER(7,2), 9 DEPTNO NUMBER(2) NOT NULL, 10 LOC VARCHAR2(13) NOT NULL 11 ) 12 partition by range(loc) 13 ( 14 partition pi values less than('C') tablespace 15 partition p2 values less than('D') tablespace 16 partition p3 values less than('N') tablespace 17 partition p4 values less than('Z') tablespace 18 ) 19 /
pi, p2, p3, p4
Table created. tkyte@TKYTE816> alter table emp add constraint emp_pk 2 primary key(empno) 3 / Table altered. tkyte@TKYTE816> create index emp_job_idx on emp(job) 2 GLOBAL 3 / Index created. tkyte@TKYTE816> create index emp_dept_idx on emp(deptno) 2 GLOBAL 3 / Index created. tkyte@TKYTE816> insert into emp 2 select e.*, d.loc 3 from scott.emp e, scott.dept d 4 where e.deptno = d.deptno 5 / 14 rows created.
136
Глава 14
Итак, м ы начинаем с таблицы, секционированной по местонахождению, L O C . в соответствии с нашими правилами. Существует глобальный уникальный индекс по столбцу E M P N O как побочный эффект выполнения оператора A L T E R T A B L E A D D CONSTRAINT. Это показывает, что можно обеспечить целостность данных. Кроме того, мы добавили еще два глобальных индекса по столбцам D E P T N O и J O B для ускорения доступа к строкам по этим атрибутам. Теперь добавим в таблицу немного данных и посмотрим, что оказалось в каждой из секций: tkyte@TKYTE816> select empno,job,loc from emp partition(pi); no rows selected tkyte@TKYTE816> select empno,job,loc from emp partition(p2); EMPNO
JOB
7900 CLERK 7844 SALESMAN 7698 MANAGER 7654 SALESMAN 7521 SALESMAN 7499 SALESMAN 6 rows selected.
LOC CHICAGO CHICAGO CHICAGO CHICAGO CHICAGO CHICAGO
tkyte@TKYTE816> select empno,job,loc from emp partition(p3), EMPNO 7902 7876 7788 7566 7369
JOB
LOC
ANALYST CLERK ANALYST MANAGER CLERK
DALLAS DALLAS DALLAS DALLAS DALLAS
tkyte@TKYTE816> select empno,job,loc from emp partition(p4); EMPNO
JOB
LOC
7934 CLERK NEW YORK 7839 PRESIDENT NEW YORK 7782 MANAGER NEW YORK Этот пример показывает распределение данных по секциям в соответствии с местонахождением сотрудника. Теперь можно выполнить несколько запросов для оценки производительности: tkyte@TKYTE816> select empno,job,loc from emp where empno - 7782; EMPNO JOB 7782 MANAGER
LOC NEW YORK
Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=4 Bytes=108) TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=l Card INDEX (RANGE SCAN) OF 'EMP PK' (UNIQUE) (Cost=l Card=4)
Секционирование tkyte@TKYTE816> EMPNO
7900 7876 7369 7934
1 3 /
s e l e c t e m p n o , j o b , l o c f r o m emp w h e r e j o b = ' C L E R K ' ;
JOB
LOC
CLERK CLERK CLERK CLERK
CHICAGO DALLAS DALLAS NEW YORK
Execution Plan 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=4 Bytes=108) TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=l Card INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE) (Cost=l
Созданные индексы используются для обеспечения высокоскоростного доступа к данным в системе OLTP. Если бы они были секционированы, то должны были бы включать префикс, что позволило бы игнорировать секции индекса; но и так масштабируемость вполне приемлема. Наконец, давайте разберемся с доступностью данных. Документация Oracle утверждает, что глобально секционированные индексы обеспечивают "меньшую доступность" данных, чем локально секционированные. Не могу полностью согласиться с этой безоговорочной характеристикой. Я уверен, что в системе OLTP они обеспечивают такую же степень доступности, как и локально секционированные. Рассмотрим пример: tkyte@TKYTE816> a l t e r tablespace p i Tablespace
offline;
altered.
tkyte@TKYTE816> a l t e r tablespace p2 o f f l i n e ; Tablespace a l t e r e d . tkyte@TKYTE816> a l t e r tablespace p3 o f f l i n e ; Tablespace a l t e r e d . tkyte@TKYTE816> s e l e c t empno,job,loc from emp where empno = 7782; EMPNO JOB
7782 MANAGER
LOC
NEW YORK
Execution Plan 0 SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=4 Bytes=108) 1 0 TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=l Card 2 1 INDEX (RANGE SCAN) OF 'EMP_PK' (UNIQUE) (Cost=l Card=4) Итак, хотя большая часть базовых данных таблицы недоступна, мы можем добраться до любого компонента данных по индексу. Если только необходимое значение EMPNO находится в доступном табличном пространстве, глобальный индекс используется. С другой стороны, если бы нас угораздило использовать в этом случае "обеспечивающий высокую доступность данных" локально секционированный индекс, это могло бы помешать доступу к данным! Это побочный эффект того, что секционирование выполнено по столбцу LOC, а в запросе задано условие по столбцу EMPNO; нам пришлось бы об-
138
Глава 14
ратиться к каждой секции локально секционированного индекса, и при попытке обратиться к недоступной секции произошла бы ошибка. Другие типы запросов не будут (и не могут) нормально выполняться в такой ситуации: tkyte@TKYTE816> s e l e c t empno,job,loc from emp where job = 'CLERK'; s e l e c t empno,job,loc from emp where job m 'CLERK' * ERROR a t l i n e 1: ORA-00376: file 13 cannot be read at this time ORA-01110: data file 13: 'C:\ORACLE\ORADATA\TKYTE816\P2.DBF' Данные о клерках разбросаны по всем секциям; то, что три табличных пространства недоступны, не будет учтено. Это неизбежно, если только данные не секционированы по столбцу JOB, но тогда проблемы возникли бы с запросами, обращающимися к данным по значению столбца LOC. Каждый раз, когда необходимо обращаться к данным по нескольким различным ключам, возникают подобные проблемы. Сервер Oracle предоставит данные, если только это вообще возможно. Учтите, однако, что если ответ на запрос можно было бы получить по индексу, избежав доступа к таблице TABLE ACCESS BY ROWID, фактор недоступности данных не был бы столь критичным: tkyte@TKYTE816> s e l e c t c o u n t ( * ) from emp where j o b = 'CLERK'; COUNT(*) 4 Execution P l a n 0 1 2
0 1
SELECT STATEMENT Optimizer=CHOOSE (Cost=l Card=l Bytes=6) SORT (AGGREGATE) INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE) (Cost-1
Поскольку серверу Oracle в этом случае таблица не нужна, недоступность большинства секций не скажется на выполнении запроса. Поскольку такая оптимизация (ответ на запрос с помощью одного только индекса) типична для систем OLTP, многие приложения "не заметят" недоступности данных. Осталось только как можно быстрее обеспечить доступность этих данных (путем восстановления и синхронизации).
Резюме Секционирование особенно полезно как средство повышения масштабируемости при увеличении размеров больших объектов в базе данных. Повышение же масштабируемости положительно сказывается на производительности, доступности данных и упрощает администрирование. Все три последствия крайне важны для разных категорий пользователей. Для администратора базы данных имеет значение возможность эффективного управления. Владельцев системы интересует доступность данных. Простой — это потеря денег, и все, что сокращает простой (или минимизирует его влияние), повышает отдачу от системы. Пользователей системы интересует производительность — медленно работающие системы никто не любит.
Секционирование
Мы также выяснили, что в системе OLTP секционирование может и не повысить производительность, особенно при неправильной реализации. Секционирование повышает производительность выполнения тех классов запросов, которые нехарактерны для систем OLTP. Это важно понимать, поскольку многие считают секционирование средством "безусловного повышения производительности". Это не означает, что секционирование не надо использовать в системах OLTP — она обеспечивает в этой среде другие, менее заметные преимущества. Сокращается время простоев. Производительность остается удовлетворительной (секционирование при правильном применении не замедлит работу). Упрощается управление системой, вследствие чего повышается производительность, поскольку некоторые действия по сопровождению администратор базы данных выполняет чаще — они ведь выполняются быстрее. Мы изучили различные схемы секционирования таблиц, предлагаемые сервером: по диапазону, по хеш-функции и смешанное секционирование, и обсудили, для каких случаев каждое из них больше всего подходит. Существенное внимание было уделено секционированию индексов, оценке различий между индексами с префиксом и без префикса, локально и глобально секционированными. Оказалось, что глобально секционированные индексы не подходят для большинства хранилищ данных, но в системе OLTP именно они используются чаще всего. Предоставляемые СУБД Oracle возможности секционирования постоянно развиваются, причем, на следующие версии запланированы существенные улучшения. Со временем, вследствие увеличения размеров баз данных и сложности приложений, секционирование будет, как мне кажется, использоваться еще более широко. Сеть Internet и присущие ей аппетиты в отношении баз данных приводят к созданию все больших подборок данных, а секционирование является естественным средством, позволяющим справиться с возникающими при этом проблемами.
| О У р n f
sionalOracjeV, inqProfessionalO •"ammingProfess fProgramraingl s siоn a10 га ale 1
, .ingj
I
Автономные транзакции
Автономные транзакции позволяют создать новую транзакцию в пределах текущей, так что можно фиксировать или откатывать ее изменения независимо от родительской транзакции. Они позволяют приостановить текущую транзакцию, начать новую, выполнить ряд действий, зафиксировать их или откатить, не влияя на состояние текущей транзакции. Автономные транзакции предлагают новый метод управления транзакциями в языке PL/SQL и могут использоваться: •
в анонимных блоках верхнего уровня;
•
в локальных, отдельных или входящих в пакеты процедурах и функциях;
Q в методах объектных типов; •
в триггерах базы данных.
Для выполнения примеров, приводимых в этой главе, необходим сервер Oracle версии 8.1.5 или выше. Подходит любая редакция — Standard, Enterprise или Personal, поскольку эта возможность поддерживается во всех редакциях. В этой главе мы: •
выясним, для чего используются автономные транзакции, включая реализацию аудита, записи которого нельзя откатить, предотвращение возникновения ошибок изменяющихся таблиц, запись в базу данных из оператора SELECT и повышение модульности кода;
Q рассмотрим, как работают автономные транзакции; изучим управление транзакциями и область действия транзакций, а также то, как завершать автономную транзакцию и устанавливать точки сохранения;
142
Глава 15
Q обсудим проблемы и ошибки, которых надо остерегаться при использовании автономных транзакций в приложениях.
Пример Чтобы показать возможности автономных транзакций, я начну с простого примера, демонстрирующего последствия их выполнения. Создадим простую таблицу для сохранения сообщений, а также две процедуры: обычную и оформленную как автономная транзакция. Процедуры будут изменять созданную таблицу. С помощью этих объектов я продемонстрирую, какие изменения остаются (фиксируются) в базе данных в различных ситуациях: tkyte@TKYTE816> create table t (msg varchar2(25)); Table created. tkyte@TKYTE816> create or replace procedure Autonomous_Insert 2 as 3 pragma autonomous_transaction; 4 begin 5 insert into t values ('Autonomous Insert'); 6 commit; 7 end; 8 / Procedure created. tkyte@TKYTE816> create or replace procedure NonAutonomous_Insert 2 as 3 begin 4 insert into t values ('NonAutonomous Insert'); 5 commit; 6 end; Procedure created. Процедуры просто вставляют свои имена в таблицу сообщений и фиксируют результат. Обратите внимание на использование PRAGMA AUTONOMOUSJTRANSACTION. Эта директива указывает серверу, что процедура должна выполняться как новая автономная транзакция, независимо от родительской транзакции. Теперь рассмотрим поведение обычной, не автономной транзакции в анонимном блоке PL/SQL: tkyte@TKYTE816> begin 2 i n s e r t i n t o t values ('Anonymous Block'); 3 NonAutonomous_Insert; 4 rollback; 5 end; 6 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t;
Автономные транзакции
143
MSG
Anonymous Block NonAutonomous Insert Как видите, изменения, выполненные анонимным блоком (вставка строки), были зафиксированы процедурой NonAutonomous_Insert. Зафиксированы обе строки данных, и оператору rollback оказалось нечего откатывать. Сравните это с поведением хранимой процедуры, оформленной как автономная транзакция: tkyte@TKYTE816> delete from t ; 2 rows deleted. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> begin 2 i n s e r t i n t o t values ('Anonymous 3 Autonomous_Insert; 4 rollback; 5 end;
Block');
PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; MSG Autonomous Insert В данном случае остаются только изменения, выполненные и зафиксированные в автономной транзакции. Оператор INSERT, выполненный в анонимном блоке, откатывается оператором ROLLBACK в строке 4. Оператор COMMIT в процедуре — автономной транзакции не влияет на родительскую транзакцию, начатую в анонимном блоке. Этот простой пример показывает суть автономных транзакций и их возможности. При отсутствии автономных транзакций оператор COMMIT в процедуре NonAutonomous_Insert зафиксировал не только выполненные в ней изменения (результат выполнения оператора INSERT),но и все остальные еще не зафиксированные изменения, выполненные в сеансе (например, вставку строки Anonymous Block, выполненную в автономном блоке). Оператор отката ничего не отменил, поскольку при вызове процедуры были зафиксированы обе вставки. В случае применения автономных транзакций все меняется. Изменения, выполненные в процедуре, объявленной как AUTONOMOUSJTRANSACTION, зафиксированы, однако изменения, выполненные вне автономной транзакции, отменены. Сервер Oracle для решения своих внутренних задач поддерживает автономные транзакции уже довольно давно. Мы постоянно их видим в форме рекурсивных SQLоператоров. Например, при выборе из последовательности, не находящейся в кеше, автоматически выполняется рекурсивная транзакция, увеличивающая значение последовательности в таблице SYS.SEQ$. Изменение значения последовательности немедленно фиксируется и видимо для других транзакций, но обратившаяся к последователь-
144
Глава 15
ности транзакция при этом не фиксируется. Кроме того, при откате транзакции увеличенное значение последовательности остается — оно не откатывается вместе с транзакцией, поскольку уже зафиксировано. Управление пространством, аудит и другие внутренние действия сервера также выполняются рекурсивно. Просто эта возможность не предлагалась для широкого использования. Теперь, увидев действие автономной транзакции, давайте разберемся, для чего такие транзакции можно использовать.
Когда использовать автономные транзакции? В этом разделе мы рассмотрим ряд ситуаций, когда могут пригодиться автономные транзакции.
Аудит, записи которого не могут быть отменены В прошлом разработчики приложений часто задавали вопрос: "Как зарегистрировать в журнале аудита попытку изменить защищенную информацию?". Они хотели не только предотвратить попытку изменения, но и сохранить информацию о такой попытке. В прошлом многие (безуспешно) пытались применять для этого триггеры. Триггер срабатывает при выполнении изменения; определяет, что пользователь изменяет данные, которые изменять не должен; создает запись в журнале аудита и завершает изменение как неудавшееся (вызывая откат). К сожалению, если изменение не выполняется, откатывается и запись в журнале аудита — неделимость операторов означает "все или ничего". Теперь, с помощью автономных транзакций, можно безопасно сохранить запись о попытке выполнения несанкционированного изменения и откатить само изменение, что позволяет уведомить пользователя: "Вы не имеете права изменять эти данные, и мы зафиксировали вашу попытку их изменить". Интересно отметить, что встроенное средство AUDIT сервера Oracle уже давно позволяло регистрировать с помощью автономных транзакций безуспешные попытки изменить информацию. Наличие такой возможности у разработчика позволяет ему создавать собственные, более гибкие системы проверки. Вот небольшой пример. Скопируем таблицу ЕМР из схемы пользователя SCOTT и создадим таблицу для журнала аудита (audit trail table), куда будем записывать кто попытался изменить таблицу ЕМР и когда была сделана эта попытка, а также что именно пытались изменить. Для автоматического сохранения этой информации для таблицы ЕМР будет создан триггер, оформленный как автономная транзакция: tkyte@TKYTE816> c r e a t e t a b l e emp 2 as 3 select * from scott.emp; Table created. tkyte@TKYTE816> grant all on emp to scott;
Автономные транзакции
Grant succeeded. tkyte@TKYTE816> create table audit_tab 2 (username varchar2(30) default user, 3 timestamp date default sysdate, 4 msg varchar2(4000) 5 ) 6 / Table created. Затем необходимо создать триггер для проверки изменений таблицы ЕМР. Обратите внимание на использование автономной транзакции. Этот триггер предотвращает изменение записи о сотруднике любым пользователем, если этот сотрудник не является его подчиненным. Запрос с конструкцией CONNECT BY реализует поиск по всей иерархии подчиненных текущего пользователя. Запрос будет проверять, принадлежит ли запись, которую пользователь пытается изменить, одному из его подчиненных: tkyte@TKYTE816> create or replace t r i g g e r EMP_AUDIT 2 before update on emp 3 for each row 4 declare 5 pragma autonomous_transaction; . 6 1 cnt number; . — 7 begxn 8 9 s e l e c t count(*) i n t o l_cnt 10 from dual 11 where EXISTS (select null 12 from emp 13 where empno = :new.empno 14 start with mgr = (select empno 15 from emp 16 where ename = USER) 17 connect by prior empno = mgr); 18 19 if ( l_cnt = 0 ) 20 then 21 insert into audit_tab (msg) 22 values ('Attempt to update 1 11 :new.empno) 23 commit; 24 25 raise_application_error(-20001, 'Access Denied'); 26 end if; 27 end; 28 / Trigger created. Итак, мы создали таблицу ЕМР, имеющую иерархическую структуру (задаваемую рекурсивным отношением EMPNO/MGR). Имеется также таблица AUDIT_TAB, в которую будут записываться неудавшиеся попытки изменить информацию. Создан триггер, позволяющий изменять запись о сотруднике только его руководителю или руководителю руководителя (и так далее).
146
Глава 15
В этом триггере надо обратить внимание на следующее. Q В определении триггера указана прагма AUTONOMOUSJTRANSACTION Весь триггер выполняется как автономная транзакция по отношению к текущей. Вкратце объясню понятие "прагма" (pragma). Прагма — это директива компилятору — способ потребовать от компилятора выполнения определенной опции компиляции. Есть и другие прагмы, описание которых можно найти в руководстве PL/SQL User's Guide and Reference. •
Триггер по таблице ЕМР читает данные из таблицы ЕМР в запросе. Подробнее о том, почему это существенно, — чуть ниже.
Q Триггер фиксирует транзакцию. Раньше это было невозможно — триггеры никогда не фиксировали изменения. На самом деле триггер не фиксирует изменения, вызвавшие его срабатывание. Он фиксирует только изменения, сделанные им самим (добавленную запись аудита). Давайте теперь рассмотрим, как все это работает: tkyte@TKYTE816> update emp s e t s a l = sal*10; update emp s e t s a l = sal*10 * ERROR a t l i n e 1: ORA-20001: Access Denied ORA-06512: a t "TKYTE.EMP_AUDIT", l i n e 22 ORA-04088: e r r o r during execution of t r i g g e r 'TKYTE.EMP_AUDIT' tkyte@TKYTE816> column msg format a30 word_wrapped tkyte@TKYTE816> s e l e c t * from audit_tab; USERNAME
TIMESTAMP MSG
TKYTE
15-APR-01 Attempt to update 7369
Итак, триггер среагировал на попытку изменения и предотвратил его, записав при этом информацию о попытке в таблицу аудита (обратите внимание, как с помощью значений по умолчанию, заданных после ключевого слова DEFAULT в операторе CREATE TABLE, в запись автоматически добавлены значения встроенных функций USER и SYSDATE). Зарегистрируемся от имени пользователя, который имеет право выполнять изменения, и попробуем выполнить ряд операторов: tkyte@TKYTE816> connect s c o t t / t i g e r scott@TKYTE816> update tkyte.emp set sal = sal*1.05 where ename = 'ADAMS'; 1 row updated. scott@TKYTE816> update tkyte.emp set sal = sal*1.05 where ename = 'SCOTT'; update tkyte.emp set sal = sal*1.05 where ename = 'SCOTT' * ERROR at line 1: ORA-20001: Access Denied ORA-06512: at "TKYTE.EMP_AUDIT", line 22 ORA-04088: error during execution of trigger 'TKYTE.EMP AUDIT1
Автономные транзакции
147
В стандартной таблице ЕМР сотрудник ADAMS подчиняется сотруднику SCOTT, поэтому первый оператор UPDATE выполняется успешно. Второе изменение (пользователь SCOTT пытается поднять самому себе зарплату) не выполняется, поскольку SCOTT не является своим руководителем. Если снова зарегистрироваться от имени пользователя, которому принадлежит таблица AUDIT_TAB, можно увидеть следующее: scott@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> s e l e c t * from audit_tab; USERNAME
TIMESTAMP MSG
TKYTE SCOTT
15-APR-01 Attempt t o u p d a t e 7369 15-APR-01 Attempt t o u p d a t e 7788
Попытка изменения данных пользователем SCOTT зарегистрирована в этой таблице. Осталось разобраться, почему так важно, что триггер по таблице ЕМР читает данные из таблицы ЕМР? Это рассматривается в следующем разделе.
Метод, позволяющий избежать ошибки изменяющейся таблицы Ошибка изменяющейся таблицы (mutating table error) может возникнуть по нескольким причинам. Чаще всего она возникает при попытке читать данные из таблицы, в ответ на изменение которой сработал триггер. В представленном выше примере мы явно читали данные из таблицы, изменение которой вызвало срабатывание триггера. Закомментируем две строки в тексте триггера и попытаемся использовать его следующим образом: tkyte@TKYTE816> c r e a t e or replace t r i g g e r EMP_AUDIT 2 before update on emp 3 for each row 4 declare 5 — pragma autonomous_transaction; 6 1 cnt number; — 7 begin g
9 10 11 12 13 14 15 16 17 18 19 20 21 22
select count(*) into l_cnt from dual where EXISTS (select null from emp where empno = :new.empno start with mgr = (select empno from emp where ename = USER) connect by prior empno = mgr); if ( l_cnt = 0 ) then insert into audit_tab (msg) values ('Attempt to update ' || :new.empno);
14о
Глава 15
24 25 raise_application_error(-20001, 26 end if; 27 end; 28 /
1
'Access Denied );
tkyte@TKYTE816> update emp set sal = sal*10; update emp set sal = sal*10 * ERROR at line 1: ORA-04091: table ТЮТЕ.EMP i s mutating, trigger/function may not see i t ORA-06512: a t "TKYTE.EMP_AUDIT", l i n e 6 ORA-04088: e r r o r during execution of t r i g g e r 'TKYTE.EMP_AUDIT' Без использования автономных транзакций представленный выше триггер написать сложно, даже если он всего лишь пытается проверить, имеет ли право пользователь изменять данную строку (и не пытается зарегистрировать эту попытку). До появления прагмы AUTONOMOUS_TRANSACTION для этого необходимо было создать пакет и три триггера. Это не значит, что во избежание ошибок изменяющейся таблицы во всех случаях следует использовать автономные транзакции — их надо использовать осторожно и четко представлять себе, как обрабатываются транзакции. В разделе "Проблемы" я объясню это подробнее. Ошибка изменяющейся таблицы призвана защитить целостность данных, и очень важно понимать, почему она возникает. Не обманывайте себя: автономные транзакции не устраняют ошибку изменяющейся таблицы в триггерах раз и навсегда!
Выполнение операторов DDL в триггерах Часто задают вопрос: "Как создать объект базы данных при вставке строки в такуюто таблицу?". При этом спрашивают о разных объектах. Иногда хотят создать пользователя базы данных при вставке строки в таблицу, иногда — таблицу или последовательность. Поскольку при выполнении операторов DDL непосредственно перед самим оператором и сразу после него выполняется фиксация транзакции (или фиксация и откат, если при выполнении оператора DDL произошла ошибка), выполнять эти операторы в триггере было невозможно. Автономные транзакции теперь позволяют это делать. Раньше приходилось использовать пакет DBMS_JOB для выполнения задания с соответствующими операторами DDL после фиксации транзакции. Это решение по-прежнему возможно, и почти всегда оно является корректным и оптимальным. Использование подпрограмм пакета DBMS_JOB для выполнения оператора DDL в виде отдельного задания хорошо тем, что позволяет включить операторы DDL в транзакцию. Если триггер поставил задание на выполнение, и это задание создало учетную запись пользователя, при откате родительской транзакции поставленное в очередь задание по созданию учетной записи пользователя тоже будет отменено. Строка, представляющая задание, будет "удалена". Не останется ни записи в таблице, ни учетной записи в системе. Если в этом случае использовать автономные транзакции, учетная запись в базе данных будет создана, а записи в таблице не будет. Недостатком подхода на базе пакета DBMS_JOB является неизбежное небольшое отставание между моментом фиксации транзакции и запуском задания. Учетная запись пользователя будет создана вскоре пос-
Автономные транзакции
ле фиксации, но не сразу. В зависимости от требований, можно использовать тот или иной метод. Повторю еще раз, что почти в любом случае можно найти аргументы в пользу пакета DBMS_JOB. В качестве примера выполнения операторов DDL в триггере рассмотрим ситуацию, когда необходимо создавать учетную запись пользователя базы данных при вставке строки в таблицу и удалять эту запись при удалении соответствующей строки. В представленном ниже примере я буду использовать свой способ, позволяющий избежать ситуаций, когда учетная запись пользователя создана, а строки в таблице нет, или строка в таблице осталась, а учетная запись пользователя удалена. Этот способ основан на использовании триггера INSTEAD OF для представления таблицы APPLICATION_USERS_TBL. Триггеры INSTEAD OF — удобное средство, позволяющее задать действия, выполняемые при изменении строк представления вместо стандартных действий сервера Oracle. В главе 20, посвященной использованию объектно-реляционных средств, будет продемонстрировано, как с помощью триггеров INSTEAD OF обеспечить изменение сложных представлений, которые сервер Oracle обычно изменять не позволяет. Мы будем использовать эти триггеры для создания учетной записи пользователя и вставки строки в реальную таблицу (или удаления учетной записи пользователя и соответствующей строки таблицы). Этот метод гарантирует, что либо создана учетная запись и строка вставлена, либо ни то, ни другое не выполнено. Если бы триггер был создан только по самой таблице, мы не могли бы этого гарантировать, а вот представление поможет нам связать эти два события. Вместо вставки и удаления данных из реальной физической таблицы все приложения будут вставлять и удалять данные из представления. Для представления будет создан триггер INSTEAD OF, так что все изменения можно будет выполнить последовательно, в процедурном коде. Это позволит гарантировать, что если строка существует в реальной таблице, то и учетная запись пользователя тоже создана. Если строка удалена из реальной таблицы, то удалена и учетная запись. Как этого добиться, лучше всего продемонстрирует пример. Я буду объяснять существенные детали, как только мы до них доберемся. Начнем с создания схемы, в которой будут храниться объекты приложения: tkyte@TKYTE816> create user demo_ddl i d e n t i f i e d by demo_ddl; User created. tkyte@TKYTE816> grant connect, resource t o demo_ddl with admin option; Grant succeeded. tkyte@TKYTE816> grant create user t o demo_ddl; Grant succeeded. tkyte@TKYTE816> grant drop user t o demo_ddl; Grant succeeded. tkyte@TKYTE816> connect demo_ddl/demo_ddl demo_ddl@TKYTE816> Итак, мы только что создали учетную запись пользователя и хотим, чтобы этот пользователь мог предоставлять привилегии CONNECT и RESOURCE другим пользователям. (Привилегии CONNECT и RESOURCE использованы для простоты. Используйте те привилегии, которые необходимо.) Для того чтобы предоставлять эти привилегии дру-
150
Глава 15
гим, сам он должен иметь привилегии CONNECT и RESOURCE с опцией WITH ADMIN OPTION. Кроме того, поскольку предполагается создание и удаление учетных записей в триггере, надо предоставить соответствующие привилегии CREATE и DROP непосредственно, как показано выше. Эти привилегии должны быть предоставлены пользователю непосредственно, а не через роль, поскольку триггеры всегда выполняются с правами создателя, а в этом режиме роли не учитываются (подробнее об этом см. в главе 23). Теперь создадим таблицу приложения, в которой будет храниться информация о пользователях. Для этой таблицы мы создадим триггер для событий BEFORE INSERT или DELETE. Этот триггер будет гарантировать, что ни один пользователь (включая владельца) не сможет вставить или удалить данные из этой таблицы непосредственно. Нам надо, чтобы все вставки/удаления выполнялись через представление и чтобы при этом обязательно выполнялись операторы DDL. В представленном ниже коде MY_CALLER — небольшая функция, которую я часто использую (совместно с подпрограммой WHO_CALLED_ME). Код этих подпрограмм можно найти в приложении "Основные стандартные пакеты" в конце книги, в разделе, посвященном пакету DBMS_UTILITY. Эта функция просто возвращает имена процедур/функций/триггеров, вызвавших ее. Если MY_CALLER вызвана не из триггера по представлениям (который еще надо создать), выполнение этой операции запрещено. demo_ddl@TKYTE816> c r e a t e t a b l e a p p l i c a t i o n _ u s e r s _ t b l 2 (uname varchar2(30) primary key, 3 pw varchar2(30), 4 role_to_grant varchar2(4000) 5 ); Table created. demo_ddl@TKYTE816> create or replace trigger application_users_tbl_bid 2 before insert or delete on application_users_tbl 3 begin 4 if (my_caller not in ('DEMO_DDL.APPLICATION_USERS_IOI', 5 'DEMOJDDL.APPLICATION_USERS_IOD')) 6 then 7 raise_application_error(-20001, 'Cannot insert/delete directly'); 8 end if; 9 end; 10 / Trigger created. Создадим представление с триггерами INSTEAD OF, которые и будут выполнять необходимые действия. А для представления создадим триггер INSTEAD OF INSERT, позволяющий создавать учетные записи. Создадим также для представления триггер INSTEAD OF DELETE. Он будет вызывать выполнение оператора DROP USER. Можно расширить пример, воспользовавшись триггерами INSTEAD OF UPDATE, которые позволяют добавлять роли и изменять пароли с помощью простых операторов UPDATE. Срабатывая, триггер INSTEAD OF INSERT выполняет два оператора: Q оператор, аналогичный GRANT CONNECT, RESOURCE TO SOME_USERNAME IDENTIFIED BY SOME_PASSWORD;
Автономные транзакции
151
Q оператор вставки INSERT в созданную ранее таблицу APPLICATION_USERS_TBL. Причина использования оператора GRANT вместо последовательности CREATE USER, а затем уже GRANT в том, что при этом операторы COMMIT, CREATE USER, GRANT и COMMIT выполняются за один шаг. Причем, если этот единственный оператор (предоставления привилегий) завершится неудачно, не придется удалять учетную запись пользователя вручную. Оператор CREATE USER может выполниться успешно, а при выполнении GRANT может произойти сбой. Ошибки, возникающие при выполнении оператора GRANT, все равно надо перехватывать, чтобы удалить только что вставленную строку. Поскольку операторы INSERT и GRANT выполняются для каждой вставляемой в представление строки, можно с уверенностью утверждать: если строка существует в реальной таблице, значит, соответствующая учетная запись успешно создана, а если нет строки, то нет и учетной записи. Потенциальная возможность сбоя все же остается, полностью избавиться от нее нельзя. Если после вставки строки в таблицу APPLICATION_USERS_TBL оператор GRANT не срабатывает, а удалить только что вставленную строку невозможно (из-за сбоя системы или недоступности табличного пространства, содержащего таблицу APPLICATION_USERS_TBL и т.п.), мы получим рассогласование. Не забывайте, что оператор GRANT на самом деле представляет собой тройку операторов COMMIT/GRANT/COMMIT, как и все операторы DDL, поэтому перед сбоем оператора GRANT результат оператора INSERT уже зафиксирован. Временной промежуток, когда это может случиться, однако, очень мал, чтобы можно было без опасений пользоваться этим методом. Теперь создадим представление и описанные выше триггеры: demo_ddl@TKYTE816> create or replace view 2 application_users 3 as 4 select * from application_users_tbl 5 / View created. demo_ddl@TKYTE816> create or replace trigger application_users_IOI 2 instead of insert on application_users 3 declare 4 pragma autonomous transaction; 5 begin 6 insert into application_users_tbl 7 (uname, pw, role_to_grant) 8 values 9 {upper(:new.uname), :new.pw, :new.role_to_grant); 11 12 13 14 15
begin execute immediate 'grant ' || :new.role_to_grant || ' to ' |I :new.uname || ' identified by ' I I rnew.pw;
152L
Глава 15
16 exception 17 when others then 18 delete from application users tbl 19 where uname = upper(:new.uname); 20 commit; 21 raise; 22 end; 23 end; 24 / Trigger created. Итак, триггер INSTEAD OF INSERT по этой таблице сначала вставляет строку в таблицу APPLICATION_USERS_TBL. Затем он выполняет оператор GRANT для создания учетной записи пользователя. Оператор GRANT фактически представляет собой тройку COMMIT/GRANT/COMMIT, так что после его выполнения строка в таблице APPLICATION_USER_TBL зафиксирована. Если оператор GRANT успешно выполнен, значит, автономная транзакция зафиксирована, и работа триггера завершается. Если же оператор GRANT не сработал (потому что учетная запись пользователя уже существует, имя пользователя — недопустимое и т.д.), мы перехватываем ошибку, явно удаляем вставленную строку и фиксируем удаление. Затем мы снова возбуждаем исключительную ситуацию. В данном случае мы выполняем оператор INSERT, а затем — оператор DDL, поскольку отменить INSERT намного проще, чем отменить создание учетной записи пользователя (для отмены сделанного предпочтительнее выполнять оператор DELETE, а не DROP). В конечном итоге триггер гарантирует, что либо вставлена строка в таблицу APPLICATION_USERS_TBL и создана соответствующая учетная запись пользователя, либо ни одно из этих действий не выполнено. Теперь перейдем к триггеру INSTEAD OF DELETE, удаляющему строку и учетную запись пользователя: demo_ddl@TKYTE816> create or replace trigger application_users_IOD 2 instead of delete on application_users 3 declare 4 pragma autonomous_transaction; 5 begin 6 execute immediate 'drop user ' I| :old.uname; 7 delete from appllcation_users_tbl 8 where uname = :old.uname; 9 commit; 10 end; 11 / Trigger created. Я умышленно изменил порядок выполнения действий. В этом триггере сначала выполняется оператор DDL, а потом — оператор DML, а раньше было наоборот. Причина снова связана с простотой восстановления в случае ошибки. Если оператор DROP USER не срабатывает, отменять ничего не нужно. Вероятность сбоя при выполнении оператора DELETE нулевая. Нет никаких ограничений целостности, которые могли бы помешать удалению строки. При большой вероятности неудачного выполнения оператора
Автономные транзакции
1J 3
DELETE из-за имеющихся ограничений целостности ссылок, порядок выполнения действий можно изменить (по аналогии с триггером INSTEAD OF INSERT). Теперь протестируем решение: вставим запись о пользователе в представление, проверим, создана ли учетная запись пользователя, и, наконец, удалим учетную запись пользователя. demo_ddl@TKYTE816> s e l e c t * from a l l _ u s e r s where username = 'NEWJJSER'; no rows s e l e c t e d demo_ddl@TKYTE816> i n s e r t into application_users values 2 ('new_user', 'pw', 'connect, r e s o u r c e 1 ) ; 1 row created. demo_ddl@TKYTE816> select * from all_users where username = 'NEW_USER'; USERNAME NEWJJSER
USER_ID CREATED 235 15-APR-01
demo_ddl@TKYTE816> delete from application_users where uname = 'NEWJJSER'; 1 row deleted. demo_ddl@TKYTE816> select * from all_users where username = 'NEWJJSER'; no rows selected (Полученное вами при выполнении этого примера значение USER_ID скорее всего будет отличаться от 235. Не удивляйтесь — это вполне объяснимо.) Наконец, убедимся, что нельзя удалять и вставлять данные непосредственно в "реальную" таблицу. demo_ddl@TKYTE816> insert into application_users_tbl values 2 ('new_user', 'pw', 'connect, resource'); insert into application_users_tbl values ERROR at line 1: ORA-20001: Cannot insert/delete directly ORA-06512: at "DEMO DDL.APPLICATION USERS TBL BID", line 5 — — — — ORA-04088: error during execution of trigger ^'DEMO_DDL.APPLICATION_USERS_TBL_BID' demo_ddl@TKYTE816> delete from application_users_tbl; delete from application_users_tbl * ERROR at line 1: ORA-20001: Cannot insert/delete directly ORA-06512: at "DEMO DDL.APPLICATION USERS TBL BID", line 5 — — _ — ORA-04088: e r r o r during execution of t r i g g e r *•»' DEMO_DDL. APPLICATION_USERS_TBL_BID' Вот и все. Триггеры обеспечивают добавление и удаление учетных записей при вставке и удалении строк из таблицы базы данных. С помощью триггеров INSTEAD OF можно обеспечить безопасность данной операции за счет выполнения компенсирующих транзакций, гарантируя при необходимости синхронность изменения таблиц приложения и
154
Глава 15
выполнения операторов DDL. Можно пойти еще дальше и создать триггеры на события базы данных, срабатывающие при удалении учетной записи пользователя с помощью оператора DROP, чтобы исключить удаление учетной записи без изменения представления.
Запись в базу данных В сервере Oracle 7.1 впервые появилась возможность расширять набор встроенных функций SQL с помощью функций, реализованных на языке PL/SQL. Это очень мощная возможность, особенно теперь, когда эти функции можно писать не только на языке PL/SQL, но и на Java или С. В прошлом функции, вызываемые в операторах SQL, не должны были изменять состояние базы данных (Write No Database State — WNDS). Если функция выполняла операторы INSERT, UPDATE, DELETE, CREATE, ALTER, COMMIT и т.д. или вызывала процедуру или функцию, выполняющую подобные действия, ее нельзя было использовать в SQL-операторах. С помощью автономных транзакций мы теперь можем изменять состояние базы данных в функциях, вызываемых в SQL-операторах. Это требуется не так уж часто: • строгий аудит; необходимо знать, какие данные видел каждый из пользователей, или надо записать идентификатор каждой записи, запрошенной у системы; •
средство создания отчетов позволяет выполнять только SQL-операторы SELECT; абсолютно необходимо по ходу построения отчета вызывать хранимую процедуру, выполняющую ряд вставок (например, заполняющую таблицу параметров для другого отчета).
Давайте рассмотрим, как решить эти проблемы.
Строгий аудит Я знаю ряд правительственных учреждений, где из соображений конфиденциальности необходимо регистрировать, кто видел различные части записи. Например, налоговая служба накапливает детальные данные о том, сколько вы заработали, что вам принадлежит и т.п. Когда кто-то запрашивает данные для того или иного лица и видит эту конфиденциальную информацию, необходимо зарегистрировать это действие в журнале аудита. По этому журналу со временем можно будет понять, не получают ли сотрудники записи, которые не имеют права получать, или ретроспективно определить, кто обращался к соответствующим записям в случае публикаций в прессе или других утечек информации. С помощью автономных транзакций и представлений можно реализовать такую проверку ненавязчиво и абсолютно прозрачно для пользователей, независимо от используемых ими инструментальных средств. Они не смогут обойти эту систему аудита, и при этом она не будет им мешать. При этом, естественно, для выполнения запросов понадобятся дополнительные ресурсы, но это вполне подходит для ситуаций, когда записи выбираются по одной, а не сотнями или тысячами. С учетом этих ограничений реализация получается достаточно простой. Используя таблицу ЕМР в качестве шаблона, можно реализовать аудит по столбцам HIREDATE, SALARY и COMMISSION, и когда кто-либо просматривает, например, данные о зарплате (SALARY), мы будем знать, кто чх просматривал и какие именно записи увидел. Начнем с создания таблицы для жур-
Автономные транзакции
155
нала аудита обращений к таблице ЕМР, которую мы скопировали из схемы пользователя SCOTT ранее в этой главе: tkyte@TKYTE816> create table audit_trail 2 (username varchar2(30), 3 pk number, 4 attribute varchar2(30), 5 dataum varchar2(255), 6 timestamp date 7 ) 8 /
•
Table created. Затем создадим ряд перегруженных функций в пакете, реализующем аудит. Каждая из этих функций принимает в качестве аргумента значение первичного ключа выбираемой строки, а также значение и имя столбца. Перегруженные функции используются, чтобы даты сохранялись как даты, а числа — как числа, что позволяет преобразовать их в стандартный формат (в строку) для хранения в созданном выше столбце DATAUM: tkyte@TKYTE816> create or replace package audit_trail_pkg 2 as 3 function record(p_pk in number, 4 p_attr in varchar2, 5 p_dataum in number) return number; 6 function record(p_pk in number, 7 p_attr in varchar2, 8 p_dataum in varchar2) return varchar2; 9 function record(p_pk in number, 10 p_attr in varchar2, 11 p_dataum in date) return date; 12 end; 13 / Package created. Итак, теперь все готово для реализации тела пакета. Каждая из объявленных выше функций RECORD вызывает внутреннюю процедуру LOG. Процедура LOG выполняется как автономная транзакция, вставляющая и фиксирующая запись в таблицу аудита. Обратите внимание, в частности, на то, как представленная ниже функция RECORD, возвращающая данные типа DATE автоматически преобразует дату в строку с сохранением времени: tkyte@TKYTE816> create or replace package body audit_trail_pkg 2 as 3 4 procedure log(p_pk in number, 5 p_attr in varchar2, 6 p_dataum in varchar2) 7 as 8 pragma autonomous_transaction; 9 begin 10 insert into audit trail values
156 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
Глава 15 (user, p_pk, p attr, p dataum, sysdate); commit; end; function record(p pk in number, p attr in varchar2, p dataum in number) return number is begin log(p_pk, p attr, p dataum); return p dataum; end; function record(p pk in number, p attr in varchar2, p_dataum in varchar2) return varchar2 is begin log(pjpk, p_attr, p_dataum); return p dataum; end; function record(p_j?k in number, p attr in varchar2, p dataum in date) return date is begin log(p pk, p attr, to_char(p dataum,'dd-mon-yyyy hh24:mi:ss')); return p dataum; end; end; /
Package body created. tkyte@TKYTE816> create or replace view emp_v 2 as 3 select empno , ename, job,mgr, 4 audit_trail_pkg.record(empno, 'sal', sal) sal, 5 audit_trail_pkg.record(empno, 'com', comm) comm, 6 audit_trail_pkg.record(empno, 'hiredate', hiredate) hiredate, 7 deptno 8 from emp 9 / View created. Мы создали представление, возвращающее три столбца — HIREDATE, SAL и COMM — через PL/SQL-функцию. PL/SQL-функция записывает, кто, что и когда просматривал. Это представление подходит для непосредственных поисковых запросов вида:
Автономные транзакци и tkyte@TKYTE816> s e l e c t empno, ename, h i r e d a t e , 2 from emp_v where ename = 'KING'; EMPNO ENAME 7839 KING tkyte@TKYTE816> tkyte@TKYTE816> tkyte@TKYTE816> tkyte@TKYTE816>
HIREDATE
SAL
17-NOV-81 column column column column
5000
sal,
coiran,
1 5 7
job
COMM JOB PRESIDENT
username format a8 pk format 9999 a t t r i b u t e format a8 dataum format a20
tkyte@TKYTE816> s e l e c t * from a u d i t _ t r a i l ; USERNAME TKYTE TKYTE TKYTE
PK ATTRIBUT DATAUM
TIMESTAMP
7839 hiredate 17-nov-1981 00:00:00 15-APR-01 7839 s a l 5000 15-APR-01 7839 comm 15-APR-01
tkyte@TKYTE816> s e l e c t empno,
ename from emp_v w h e r e ename »
'BLAKE';
EMPNO ENAME 7698 BLAKE tkyte@TKYTE816> s e l e c t USERNAME TKYTE TKYTE TKYTE
* from
audit_trail;
PK ATTRIBUT DATAUM
TIMESTAMP
7839 h i r e d a t e 1 7 - n o v - 1 9 8 1 7839 s a l 5000 7839 comm
00:00:00
15-APR-01 15-APR-01 15-APR-01
Как видно по этим результатам, пользователь TKYTE просматривал столбцы HIREDATE, SAL и COMM в указанный день. По второму запросу информация из этих столбцов не получена, поэтому дополнительные записи в журнал аудита не внесены. Подобное представление, как уже было сказано, подходит для простых справочных запросов, потому что в некоторых случаях оно регистрирует "лишние" попытки доступа. Бывают случаи, когда данные представления показывают, что кто-то просматривал фрагмент информации, тогда как фактически он его не видел. Он был отброшен в дальнейшем в сложном запросе или агрегирован в определенное значение, не имеющее отношения к данному лицу. Следующий пример показывает, что при агрегировании или использовании столбца в конструкции WHERE в журнал аудита вносится запись о том, что столбец просмотрен. Начнем с очистки таблицы журнала аудита, чтобы происходящее стало очевидным: tkyteQTKYTE816> d e l e t e from
audit_trail;
3 rows d e l e t e d . tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> s e l e c t avg(sal)
from emp_v;
158
Глава 15
AVG(SAL) 2077.14286 tkyte@TKYTE816> select * from audit_trail; USERNAME
PK ATTRIBUT DATAUM
TIMESTAMP
TKYTE
7499 sal
1600
15-APR-01
TKYTE
7934 sal
1300
15-APR-01
14 rows selected. tkyte@TKYTE816> select ename from emp_v where sal >= 5000; ENAME KING tkyte@TKYTE816> s e l e c t * from a u d i t _ t r a i l ; USERNAME
PK ATTRIBUT DATAUM
TIMESTAMP
TKYTE
7499 s a l
1600
15-APR-01
TKYTE
7934 s a l
1300
15-APR-01
28 rows selected. При выполнении запроса с агрегированием зарегистрирован просмотр каждого значения зарплаты, которое было просмотрено для получения среднего, AVG(SAL). Запрос WHERE SAL >= 5000 записал каждую зарплату, которая просматривалась для получения ответа. Для таких случаев хороших решений нет, разве что не использовать представления в такого рода запросах. Для получения значения AVG(SAL) можно использовать представление, включающее столбец SAL и, возможно, другие данные. Запрос должен выполняться к представлению, не связывающему значение в столбце SAL с определенным лицом. Оно позволит просматривать зарплаты, но не узнавать, кто их получает. Проверить условие SAL > = 5000, не записав каждую зарплату, сложно. Я бы использовал хранимую процедуру, возвращающую курсорную переменную, REF CURSOR. В этой хранимой процедуре можно было бы обращаться к таблице ЕМР и проверять условия по столбцу SAL, но выбирать любую информацию, за исключением столбца SAL. Пользователь не узнает, сколько именно получил тот или иной человек, он узнает только, что зарплата превышала определенное значение. Представлением EMP_V можно будет пользоваться только при необходимости получить одновременно и личную информацию (значения столбцов EMPNO и ENAME), и зарплату (SAL). Выполнение операторов DML в SQL в данном случае специфично, и делать это надо осторожно.
Когда среда позволяет выполнять только операторы SELECT Это действительно удобное использование автономных транзакций в SQL-операторах. Во многих случаях используются инструментальные средства, позволяющие выпол-
Автономные транзакции
\ j y
нять только операторы SELECT или даже простые операторы типа INSERT, но на самом деле необходимо вызывать хранимые процедуры, а этого подобные средства не позволяют. Автономные же транзакции позволяют вызывать любую хранимую процедуру или функцию с помощью SQL-оператора SELECT. Предположим, создана хранимая процедура, вставляющая ряд значений в таблицу для того, чтобы на их основе ограничить набор строк, выдаваемых последующим запросом в том же сеансе. Если значений в этой таблице нет, отчет создать нельзя. Работать приходится в среде, не позволяющей выполнять хранимые процедуры, а только обычные SQL-операторы. Как же поступить, если выполнить эту процедуру необходимо. Следующий пример демонстрирует возможное решение: tkyte@TKYTE816> c r e a t e t a b l e report_j?arm_table 2 (sess±on_id number, 3 argl number, 4 arg2 date 5 ) Table created. tkyte@TKYTE816> create or replace 2 procedure set_up_report(p_argl in number, p_arg2 in date) 3 as 4 begin 5 delete from report_parm_table 6 where session_id = sys_context('userenv','sessionid'); 8 9 10 11 12 13
insert into report_parm_table (session_id, argl, arg2) values (sys_context('userenv','sessionid'), p_argl, p_arg2); end; /
Procedure created. Итак, имеется хранимая процедура, изменяющая состояние базы данных; она входит в уже существующую систему. Мы хотим вызывать ее из SQL-оператора SELECT, поскольку это единственно доступный способ. Процедуру SET_UP_REPORT необходимо "обернуть" в небольшую PL/SQL-функцию, поскольку в SQL-операторах можно вызывать только функции. Кроме того, такая "обертка" нужна, чтобы можно было задать прагму AUTONOMOUSJTRANSACTION: tkyte@TKYTE816> c r e a t e or replace 2 function set_up_report_F(p_argl in number, p_arg2 in date) 3 r e t u r n number 4 as 5 pragma autonomous_transaction; 6 begin 7 set_up_report(p_argl, p_arg2); 8 coinmit;
160
Глава 15
9 return 1; 10 exception 11 when others then 12 rollback; 13 return 0; 14 end; 15 / Function c r e a t e d . tkyte@TKYTE816> s e l e c t set_up_report_F(1, sysdate) from dual 2 / SET UP REPORT F(l,SYSDATE)
tkyte@TKYTE816> s e l e c t * from report_parm_table 2 Интересно посмотреть, что произойдет, если попытаться вызвать эту функцию в SQLоператоре, не объявив ее как автономную транзакцию. Если перекомпилировать представленную выше функцию без прагмы, при выполнении оператора будут получены следующие сообщения: tkyte@TKYTE816> s e l e c t set_up_report_F(l, sysdate) from dual 2 / s e l e c t set_up_report_F(l, sysdate) from dual ERROR a t l i n e 1: ORA-14552: cannot perform a DDL, commit or rollback i n s i d e a query or DML ORA-06512: a t "TKYTE.SET_UP_REPORT_F", l i n e 10 ORA-14551: cannot perform a DML operation inside a query ORA-06512: a t l i n e 1 Именно этого и позволяет избежать автономная транзакция. Итак, мы создали функцию, которую можно вызывать из SQL-операторов и которая вставляет строку в базу данных. Важно помнить, что эта функция должна обязательно зафиксировать (или откатить) транзакцию до завершения работы — в противном случае будет получено сообщение об ошибке ORA-06519 (подробнее см. далее в разделе "Возможные сообщения об ошибках"). Кроме того, функция обязательно должна возвращать значение. Моя функция возвращает 1 в случае успешного выполнения и 0 — в случае неудачи. Кроме того, надо учитывать, что функция может иметь только параметры, передаваемые в режиме IN, — никаких параметров IN/OUT или OUT. Дело в том, что SQL не позволяет задавать параметры с этими режимами передачи. Я хочу рассказать о проблемах при использовании описанного подхода. Обычно я описываю проблемы в конце главы, но в данном случае проблемы непосредственно связаны с изменением базы данных операторами SELECT. У таких изменений могут быть опасные побочные эффекты, связанные со способом оптимизации и выполнения запросов. Представленный выше пример был достаточно безопасен. Таблица DUAL — однострочная, мы выбирали значение функции, и вызываться функция будет только один раз. Не было никаких соединений, предикатов, сортировок и побочных эффектов. Она
Автономные транзакции
161
должна работать надежно. Иногда функция вызывается меньшее или большее количество раз, чем предполагалось. Чтобы продемонстрировать это, я прибегну к несколько надуманному примеру. Используем простую таблицу COUNTER, которую автономная транзакция будет обновлять при каждом выполнении. Таким образом, мы сможем выполнять запросы и видеть, сколько раз вызывалась функция: tkyte@TKYTE816> c r e a t e t a b l e counter (x i n t ) ; Table created. tkyte@TKYTE816> i n s e r t i n t o counter values (0); 1 row created. tkyte@TKYTE816> c r e a t e or replace function f r e t u r n number 2 as 3 pragma autonomous_transaction; 4 begin 5 update counter set x = x+1; 6 commit; 7 return 1; 8 end; 9 / Function created. Итак, мы создали таблицу COUNTER и функцию. При каждом вызове функции F значение X будет увеличиваться на 1. Давайте попробуем: tkyte@TKYTE816> s e l e c t count(*) 2 from ( s e l e c t f from emp) 3 / COUNT(*) 14 tkyte@TKYTE816> select * from counter; X 0 Как видите, функция ни разу не вызывалась, хотя должна была вызываться 14 раз. Чтобы продемонстрировать, что функция F работает, выполним следующий оператор: tkyte@TKYTE816> s e l e c t count(*) 2 from ( s e l e c t f from emp union s e l e c t f from emp) 3 / COUNT(*) 1 : * from counter; X 28
6 Зак. 244
162
Глава 15
Именно этого мы и ожидали. Функция F была вызвана 28 раз (14 из них — для запросов в операторе UNION). Поскольку при выполнении оператора UNION в качестве побочного эффекта происходит сортировка для поиска неповторяющихся значений (SORT DISTINCT), функция COUNT(*) по объединению дает 1 (и это правильно), а функция, как и ожидалось, вызывалась 28 раз. Слегка изменим запрос: tkyte@TKYTE816> update counter set x = 0; 1 row updated. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> s e l e c t f from emp union ALL s e l e c t f from emp 2 / F 1 1
28 rows selected. tkyte@TKYTE816> select * from counter; X 32 Запрос вернул 28 строк, но наша функция была вызвана 32 раза! Помните о побочных эффектах. Сервер Oracle не гарантирует, что функция вообще будет вызвана (вс помните первый пример) или будет вызвана определенное количество раз (последний пример). Будьте особенно осторожны при использовании таких изменяющих базу данных функций в SQL-операторах, если используется другая таблица, кроме DUAL, выполняются соединения, сортировки и т.п. Результаты могут оказаться неожиданными.
Разработка модульного кода Автономные транзакции также позволяют повысить модульность кода. Традиционно создается набор пакетов, выполняющих некоторые действия. Эти действия представляют собой ряд изменений в базе данных, которые, в зависимости от результатов, фиксируются или откатываются. Все это замечательно, если надо заботиться только о своих процедурах. В крупномасштабном приложении, однако, ваш простой пакет будет далеко не единственным компонентом. Скорее всего, он будет лишь небольшой частью общей картины. В типичной процедуре фиксируются не только выполненные в ней изменения, но и все не зафиксированные изменения, выполненные в сеансе до вызова этой процедуры. При фиксации постоянными делаются все эти изменения. Проблема в том, что выполнявшиеся до вызова процедуры изменения могут быть не завершены. Поэтому выполнение оператора COMMIT может привести к ошибке в вызывающей процедуре. Она уже не сможет откатить выполненные изменения в случае сбоя, даже "не подозревая" об этом.
Автономные транзакции
163
С помощью автономных транзакций теперь можно создавать самодостаточные подпрограммы, выполняющие транзакции, которые не влияют на состояние вызывающих транзакций. Это может быть весьма полезно для многих типов подпрограмм, в частности, реализующих аудит, журнализацию и другие служебные функции. Так можно создавать код, который безопасно вызывать из различных сред, не влияя деструктивно на функционирование этих сред. Использование автономных транзакций для аудита, журнализации и других служебных функций — вполне оправдано. Лично я считаю фиксацию транзакций в хранимых процедурах обоснованной только в перечисленных случаях. Я считаю, что фиксировать транзакции должно только клиентское приложение. Необходимо быть осторожным и использовать автономные транзакции правильно. Если код должен вызываться как логическая часть более масштабной транзакции (например, если создан пакет ADDRESS_UPDATE для системы учета кадров), то оформлять его в виде автономной транзакции нельзя. Во внешней среде необходимо обеспечить возможность вызова этого пакета и других соответствующих пакетов, а затем зафиксировать (или отменить) все изменения в целом. Поэтому при использовании автономной транзакции контроль из вызывающей среды невозможен, как и построение больших транзакций из меньших составных частей. Кроме того, автономная транзакция не видит незафиксированных изменений, выполненных в вызывающей транзакции. Подробнее это описано в разделе "Как работают автономные транзакции". Это означает, что для автономной транзакции незафиксированные изменения остальной кадровой информации будут невидимы. Для корректного использования автономных транзакций необходимо четко представлять все возможные варианты использования создаваемого кода.
Как работают автономные транзакции В этом разделе я опишу, как работают автономные транзакции и чего от них можно ожидать. Мы изучим последовательность действий при выполнении автономных транзакций. Мы также рассмотрим, как автономные транзакции влияют на область действия различных элементов — переменных пакетов, параметров сеансов, параметров базы данных и блокировок. Мы поговорим о том, как правильно завершать автономную транзакцию, и о точках сохранения.
Выполнение транзакции Выполнение автономной транзакции начинается с ключевого слова BEGIN и завершается ключевым словом END. To есть, при наличии следующего блока кода: declare pragma autonomous_transaction; X number default func; begin
(1) (2)
end;
(3)
автономная транзакция начинается со строки (2), а не (1). Она начинается с первого выполняемого оператора. Если FUNC — функция, выполняющая изменения в базе данных, — эти изменения не являются частью автономной транзакции. Они — часть роди-
164
Глава 15
тельской транзакции. Кроме того, порядок следования элементов в разделе DECLARE блока не имеет значения: конструкция PRAGMA может быть как первой, так и последней. Весь раздел DECLARE блока является частью родительской транзакции, а не автономной. Следующий пример поможет это прояснить: tkyte@TKYTE816> c r e a t e t a b l e t (msg varchar2(50)); Table created. tkyte@TKYTE816> c r e a t e or replace function func r e t u r n number 2 as 3 begin 4 insert into t values 5 ('Строка вставлена функцией FUNC'); 6 return 0; •7 end; 8 / Function created. Итак, имеется функция, изменяющая базу данных. Давайте теперь вызовем эту функцию в разделе DECLARE блока, оформленного как автономная транзакция: tkyte@TKYTE816> declare 2 х number default func; 3 pragma autonomous_transaction; 4 begin 5 insert into t values 6 ('Строка вставлена анонимным блоком'); 7 commit; 8 end; 9 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; MSG Строка вставлена функцией FUNC Строка вставлена анонимным блоком Пока что обе строки есть. Однако одна из строк еще не зафиксирована. В этом можно убедиться, выполнив откат: tkyte@TKYTE816> rollback; Rollback complete. tkyte@TKYTE816> s e l e c t * from t ; MSG Строка вставлена анонимным блоком Как видите, после выполнения анонимного блока кажется, что обе строки вставлены в таблицу Т и зафиксированы. Это, однако, ошибочное представление. Строка, вставленная функцией, на самом деле еще не зафиксирована. Она — часть родительской, еще
Автономные транзакции
10 5
не завершенной транзакции. Выполняя откат, мы убеждаемся, что она исчезает, а вот строка, вставленная в автономной транзакции, остается. Итак, автономная транзакция начинается с первого за конструкцией PRAGMA ключевого слова BEGIN и действует в пределах соответствующего блока. Любые функции или процедуры, которые вызываются в автономной транзакции, триггеры, срабатывание которых она вызывает, и т.д. являются частью этой автономной транзакции, и выполненные ими изменения будут зафиксированы или отменены вместе с ней. Автономные транзакции могут быть вложенными — в автономной транзакции можно начать новую автономную транзакцию. Вложенные автономные транзакции обрабатываются точно так же, как родительская, — они начинаются с первого ключевого слова BEGIN, действуют вплоть до соответствующего ключевого слова END и полностью независимы от родительской транзакции. Единственное ограничение на глубину вложенности автономных транзакций задается параметром инициализации TRANSACTIONS, который определяет, сколько одновременных транзакций может поддерживать сервер. Обычно это значение равно количеству сеансов (SESSIONS), умноженному на 1,1; если планируется интенсивно использовать автономные транзакции, значение этого параметра должно быть увеличено.
Область действия Под областью действия подразумевается возможность получать значения различных элементов базы данных. В данном случае нас интересуют четыре элемента. Рассмотрим поочередно области действия: Q переменных пакетов; Q установок/параметров сеанса; •
изменений в базе данных;
•
блокировок.
Переменные пакетов Автономная транзакция создает новый контекст транзакции, но не новый сеанс. Поэтому любые переменные, находящиеся в области действия (доступные) родительской и автономной транзакции, будут в них идентичны, поскольку присвоение значений переменным не входит в транзакцию (нельзя вернуть переменной PL/SQL прежнее значение). Поэтому автономная транзакция может не только читать переменные состояния родительской транзакции, но и изменять их, и эти изменения будут видимы в родительской транзакции. Это означает, что поскольку изменения значений переменных не фиксируются и не откатываются, эти изменения выпадают из области действия автономных транзакций и происходят так же, как и при отсутствии автономных транзакций. Чтобы продемонстрировать это на простом примере, я создам пакет с глобальной переменной. Родительская транзакция (наш сеанс) будет устанавливать этой переменной определенное значение, а в автономной транзакции оно будет изменяться. Это изменение скажется на родительской транзакции:
166
Глава 15
tkyte@TKYTE816> create or replace package global_yariables 2 as 3 x number; 4 end; 5 / Package created. tkyte@TKYTE816> begin 2 global_variables.x : = 5; 3 end; 4 / PL/SQL procedure successfully completed. tkyte@TKYTE816> declare 2 pragma autonomous_transaction; 3 begin 4 global_variables. x :«• 10; 5 commit; 6 end; 7 / PL/SQL procedure successfully completed. tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> exec dbms_output.put_line(global_variables.x); 10 PL/SQL procedure successfully
completed.
Это изменение глобальной переменной автономной транзакцией останется в силе независимо от конечного результата (фиксации или отката) автономной транзакции.
Установки/параметры сеанса Опять-таки, поскольку автономные транзакции создают новую транзакцию, а не новый сеанс, состояние сеанса в родительской транзакции будет таким же, как и в порожденной. Обе транзакции выполняются в одном сеансе, хотя и отдельно. Сеанс организуется при подключении приложения к базе данных. При выполнении автономной транзакции повторное подключение не выполняется — используется то же подключение и тот же сеанс. Поэтому любые изменения на уровне сеанса, выполненные в родительской транзакции, будут видимы в порожденной и, более того, если в порожденной транзакции выполняются изменения на уровне сеанса с помощью оператора ALTER SESSION, они повлияют и на родительскую транзакцию. Следует отметить, что оператор SET TRANSACTION, по определению работающий на уровне транзакции, влияет только на транзакцию, в которой выполнен. Так что, например, если в автономной транзакции выполнен оператор SET TRANSACTION USE ROLLBACK SEGMENT, то соответствующий сегмент отката будет задан только для автономной, но не для родительской транзакции. Оператор SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, выполненный в автономной транзакции, влияет только на эту транзакцию, а вот при выполнении оператора ALTER SESSION SET ISOLATION_LEVEL=SERIALIZABLE изменится и уровень изолированности следующей транзакции на уровне родительской.
Автономные транзакции
167
Кроме того, родительская транзакция, работающая в режиме READ ONLY, может вызвать автономную транзакцию, изменяющую базу данных. Автономная транзакция в этом случае может изменять данные.
Изменения в базе данных Теперь переходим к самому интересному — изменениям в базе данных. Здесь происходящее несколько затуманивается. Изменения в базе данных, выполненные, но еще не зафиксированные родительской транзакцией, невидимы в автономных транзакциях. Изменения, выполненные и зафиксированные в родительской транзакции, всегда видимы порожденным транзакциям. Изменения, выполненные в автономной транзакции, могут быть как видимы, так и невидимы в родительской транзакции — это зависит от уровня ее изолированности. Однако вот в чем неясность происходящего. Я вполне четко заявил, что изменения, выполненные в родительской транзакции, невидимы для порожденной, но это еще не все. Для курсора, отрытого в порожденной автономной транзакции, эти незафиксированные изменения невидимы. Но вот курсор, открытый в родительской транзакции, при выборе из него данных в порожденной, позволяет получить эти измененные данные. Следующий пример демонстрирует, о чем идет речь. Создадим новую таблицу ЕМР (для прежней мы создали всевозможные средства аудита), а затем напишем пакет, который ее изменяет и выдает содержимое. В этом пакете мы создадим глобальный курсор, выбирающий данные из таблицы ЕМР. В пакете будет одна процедура, оформленная как автономная транзакция. Она выбирает данные из курсора и выдает результаты. Сначала эта процедура проверяет, открыт ли курсор, и, если — нет, открывает его. Это позволит продемонстрировать разницу в получаемых результатах, в зависимости от того, где был открыт курсор. Результирующее множество курсора всегда согласовано на момент его открытия с учетом того, в какой транзакции он был открыт: tkyte@TKYTE816> drop t a b l e emp; Table dropped. tkyte@TKYTE816> create t a b l e emp as s e l e c t * from scott.emp; Table created. tkyte@TKYTE816> c r e a t e or replace package my_pkg 2 as 3 4 procedure run; 5 6 end; 7 / Package created. tkyte@TKYTE816> create or replace package body my_pkg 2 as 4 5 6
cursor global_cursor is select ename from emp;
168 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
Глава 15
procedure show_results is pragma autonomous_transaction; l_ename emp.ename%type; begin if (global_cursor%isopen) then dbms_output.put_line('Еще НЕ открытый курсор'); else dbms_output.put_line('Уже открытый'); open global_cursor; end if; loop fetch global_cursor into l_ename; exit when global cursor%notfound; dbms_output .put_line (l_ename) ; end loop; close global_cursor; end;
procedure run is begin update emp set ename = 'x'; open global_cursor; show_results; show_results; rollback; end;
44 / Package body created. tkyte@TKYTE816> exec my_pkg.run Еще НЕ открытый курсор x X Уже открытый SMITH MILLER PL/SQL procedure successfully completed.
Автономные транзакции
Когда курсор открыт в родительской транзакции, в автономной транзакции можно получить незафиксированные строки — все значения х. Курсор, отрытый в автономной транзакции, с таким же успехом можно было открыть и в другом сеансе — для него эти незафиксированные данные недоступны. Мы видим данные в том состоянии, в каком они были до изменения. Итак, этот пример показывает, как автономная транзакция будет реагировать на незафиксированные изменения в родительской транзакции при выполнении операторов SELECT. А будут ли в родительской транзакции видны изменения, произошедшие в автономной транзакции? Это будет зависеть от уровня изолированности родительской транзакции. При использовании стандартного уровня изолированности, READ COMMITTED, родительская транзакция сможет увидеть эти изменения. При использовании уровня изолированности SERIALIZABLE эти изменения не будут видны, хотя они выполнены в том же сеансе. Например: tkyte@TKYTE816> create t a b l e t (msg Table
varchar2(4000));
created.
tkyte@TKYTE816> create or replace procedure auto_proc 2 as 3 pragma autonomous_transaction; 4 begin 5 insert into t values ('A row for you'); 6 commit; 7 end; 8 / Procedure created. tkyte@TKYTE816> create or replace 2 procedure proc(read_committed in boolean) 3 as 4 begin 5 if (read_committed) then 6 set transaction isolation level read committed; 7 else 8 set transaction isolation level serializable; end if; 11 12 13 14 15 16 17 18 19 end; 20 /
auto_proc; dbms_output.put_line(' ' ); for x in (select * from t) loop dbms_output.put_line(x.msg); end loop; dbms_output.put_line (,' '); commit;
Procedure created.
170
Глава 15
tkyte@TKYTE816> exec proc(TRUE) A row for you PL/SQL procedure successfully completed. tkyte@TKYTE816> delete from t; 1 row deleted. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> exec proc(FALSE)
PL/SQL procedure successfully completed. Как видите, при выполнении процедуры в режиме READ COMMITTED зафиксированные изменения видны. При выполнении в режиме SERIALIZABLE изменения не видны. Дело в том, что изменения, выполненные в автономной транзакции, выполнены в другой транзакции, а уровень изолированности SERIALIZABLE требует учитывать только изменения, выполненные в данной транзакции (при этом уровне изолированности все обстоит так, как если бы транзакция была единственной — изменения, выполненные в других транзакциях, не видны).
Блокировки В предыдущем разделе мы разобрались, что происходит при попытке чтения в порожденной автономной транзакции зафиксированных и незафиксированных изменений, выполненных родительской транзакцией, а также при чтении в родительской транзакции изменений, выполненных порожденной транзакцией. Теперь посмотрим, какие при этом устанавливаются блокировки. Поскольку родительская и порожденная — это две абсолютно разные транзакции, они никак не могут совместно использовать блокировки. Если родительская транзакция заблокировала ресурс, который требуется заблокировать также порожденной автономной транзакции, произойдет взаимное блокирование в сеансе. Следующий пример демонстрирует эту проблему: tkyte@TKYTE816> c r e a t e or replace procedure child 2 as 3 pragma autonomous_transaction; 4 l_ename emp.ename%type; 5 begin 6 select ename into l_ename 7 from emp 8 where ename = 'KING' 9 FOR UPDATE; 10 commit; 11 end; 12 /
Автономные транзакции
1/1
Procedure created. tkyte@TKYTE816> create or replace procedure parent 2 3 4 5 6 7 8 9 10 11 12
as l_ename emp.ename%type; begin select ename into l_ename from emp where ename = 'KING FOR UPDATE; child; commit;
1
end; /
Procedure created. tkyte@TKYTE816> exec parent BEGIN parent; END; * ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource ORA-06512: a t "TKYTE.CHILD", l i n e 6 ORA-06512: a t "TKYTE.PARENT", l i n e 9 ORA-06512: a t l i n e 1 Необходимо проявлять осторожность и не допускать взаимного блокирования родительской и порожденной транзакций. Порожденная транзакция в этом случае всегда "проигрывает", и соответствующий ее оператор откатывается.
Завершение автономной транзакции Для завершения автономной транзакции необходимо выполнять операторы COMMIT или ROLLBACK, или оператор DDL, который автоматически фиксирует транзакцию. Сама автономная транзакция начинается автоматически при выполнении изменения в базе данных, блокировании ресурсов или выполнении оператора управления транзакцией, такого как SET TRANSACTION или SAVEPOINT. Автономная транзакция должна быть явно завершена, прежде чем управление вернется в родительскую транзакцию (иначе выдается сообщение об ошибке). Отката до точки сохранения (ROLLBACK TO SAVEPOINT) недостаточно, даже если в результате незафиксированных изменений не остается, поскольку при этом не завершается транзакция. Если автономная транзакция завершается нормально (а не путем распространения исключительной ситуации) и в ней не выполнен оператор COMMIT или ROLLBACK, выдается следующее сообщение об ошибке: tkyte@TKYTE816> create or replace procedure c h i l d 2 as 3 pragma autonomous_transaction; 4 l_ename emp.ename%type; 5 begin 6 select ename into l_ename 7 from emp
172
Глава 15
8 9 10 end; 11 /
where ename = 'KING' FOR UPDATE;
Procedure created. tkyte@TKYTE816> exec child BEGIN child; END; * ERROR at line 1: ORA-06519: a c t i v e autonomous transaction detected and r o l l e d back ORA-06512: a t "TKYTE.CHILD", l i n e 6 ORA-06512: a t l i n e 1 Так что в случае автономной транзакции следует не только избегать взаимных блокировок с родительской, но и позаботиться о ее "чистом" завершении (чтобы предотвратить откат всех изменений).
Точки сохранения В главе 4, посвященной транзакциям, я описывал точки сохранения и их влияние на выполняемые приложением транзакции. Точки сохранения действуют только в пределах текущей транзакции. Это означает, что нельзя откатить автономную транзакцию до точки сохранения, установленной в транзакции вызывающей подпрограммы. Этой точки сохранения нет в среде текущей автономной транзакции. Давайте посмотрим, что получится, если попытаться выполнить такой откат: tkyte@TKYTE816> c r e a t e or replace procedure c h i l d 2 as 3 pragma autonomous_transaction; 4 l_ename emp.ename%type; 5 begin 6 7 update emp set ename = 'y' where ename = 'BLAKE'; 8 rollback to Parent_Savepoint; 9 commit; 10 end; 11 / Procedure created. tkyte@TKYTE816> create or replace procedure parent 2 as 3 l_ename emp.ename%type; 4 begin 5 savepoint Parent_Savepoint; 6 update emp set ename = 'x' where ename = 'KING'; 7 8 child; 9 rollback; 10 end; 11 /
Автономные транзакции
1 / 3
Procedure created. tkyte@TKYTE816> exec parent BEGIN parent; END; * ERROR a t l i n e 1: ORA-01086: savepoint 'PARENT_SAVEPOINT' never e s t a b l i s h e d ORA-06512: a t "TKYTE.CHILD", l i n e 8 ORA-06512: a t "TKYTE.PARENT", l i n e 8 ORA-06512: a t l i n e 1 Для автономной транзакции эта точка сохранения никогда не устанавливалась. Если удалить признак автономной транзакции из представленной выше процедуры child и повторно выполнить процедуру parent, все успешно сработает. Автономная транзакция не может изменять состояние родительской транзакции. Это не означает, что в автономной транзакции нельзя использовать точки сохранения. Можно. Нужно только устанавливать собственные точки сохранения. Например, следующий код демонстрирует, что установленная в порожденной транзакции точка сохранения работает. Одно изменение, выполненное до точки сохранения, осталось, а другое — отменено, как и предполагалось: tkyte@TKYTE816> c r e a t e or replace procedure c h i l d 2 as 3 pragma autonomous_transaction; 4 1 ename emp.ename%type; 5 begin 6 7 update emp set ename = 'у' where ename = 'BLAKE'; 8 savepoint child_savepoint; 9 update emp set ename = 'z' where ename = 'SMITH'; 10 rollback to child_savepoint; 11 commit; 12 end; 13 / Procedure created. tkyte@TKYTE816> create or replace procedure parent 2 as 3 1 ename emp.ename%type; 4 begin " 5 savepoint Parent_Savepoint; 6 update emp set ename = 'x' where ename = 'KING1; 7 8 child; 9 commit; 10 end; 11 / Procedure created. tkyte@TKYTE816> select ename 2 from emp 3 where ename in ('x', 'y\ ' z \ 'BLAKE', 'SMITH', 'KING');
174
Глава 15
ENAME SMITH BLAKE KING tkyte@TKYTE816> exec parent PL/SQL procedure successfully completed. tkyte@TKYTE816> select ename 2 from emp 3 where ename in ('x', 'y', ' z \ 'BLAKE', 'SMITH', 'KING'); ENAME SMITH У
Проблемы При использовании автономных транзакций имеется ряд нюансов, которые надо учитывать. В этом разделе мы рассмотрим их поочередно: какие возможности недоступны в автономных транзакциях, в каких средах автономные транзакции можно использовать, особенности, с которыми можно столкнуться при их использовании и другие подобные проблемы.
Невозможность использования в распределенных транзакциях В текущих версиях (по крайней мере до Oracle 8.1.7) не допускается использование автономных транзакций в распределенной транзакции. Четкого сообщения об ошибке при этом не выдается. Во многих (но не во всех) случаях возникает внутренняя ошибка. В будущем планируется обеспечить надежную поддержку использования автономных транзакций в распределенных. Пока же, если используются связи базы данных, об автономных транзакциях лучше забыть.
Только в среде PL/SQL Автономные транзакции доступны только в среде PL/SQL. Их можно перенести в Java и другие языки, вызвав соответствующую подпрограмму из блока PL/SQL, оформленного как автономная транзакция. Поэтому, если необходимо создать хранимую процедуру на языке Java, работающую как автономная транзакция, создают хранимую процедуру на PL/SQL, оформленную в виде автономной транзакции, и вызывают Java-процедуру из нее.
Откатывается вся транзакция Если автономная транзакция завершается из-за ошибки, вследствие неперехваченной и необработанной исключительной ситуации, откатывается вся транзакция, а не
Автономные транзакции
175
только оператор, при выполнении которого произошла ошибка. Это означает, что при выполнении автономной транзакции вы получаете "все или ничего". Либо все изменения успешно фиксируются, либо возникает необработанная исключительная ситуация, и все незафиксированные изменения пропадают. Обратите внимание: незафиксированные изменения. В коде, оформленном как автономная транзакция, фиксация может выполняться многократно, и откатываются только незафиксированные изменения. Обычно, если при вызове процедуры возникает исключительная ситуация, которая перехватывается и обрабатывается в вызывающем коде, незафиксированные изменения остаются, но не в случае автономной транзакции. Например: tkyte@TKYTE816> c r e a t e t a b l e t (msg varchar2(25)); Table created. tkyte@TKYTE816> c r e a t e or replace procedure autc_proc 2 as 3 pragma AUTONOMOUSJTRANSACTION; 4 x number; 5 begin 6 insert into t values ('AutoProc'); 7 x := 'a'; — При выполнении этого оператора произойдет ошибка 8 commit; 9 end; 10 / Procedure created. tkyte@TKYTE816> create or replace procedure Regular_Proc 2 as 3 x number; 4 begin 5 insert into t values ('RegularProc'); 6 x := 'a'; — При выполнении этого оператора произойдет ошибка 7 commit; 8 end; 9 / Procedure created. tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> begin 2 insert into t values ('Anonymous'); 3 auto proc; 4 exception 5 when others then 6 dbms_output.put_line('Перехвачена ошибка:'); 7 dbms_output.put_line(sqlerrm); 8 commit; 9 end; 10 / Перехвачена ошибка: ORA-06502: PL/SQL: numeric or value error: character to number conversion terror
176
Глава 15
PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; MSG Anonymous
Сохранились только данные, вставленные в анонимном блоке. Сравните это с поведением "обычного" блока: tkyte@TKYTE816> delete from t; 1 row deleted. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> begin 2 insert into t values ('Anonymous'); 3 regular_proc; 4 exception 5 when others then 6 dbms_output.put_line('Перехвачена ошибка:'); 7 dbms_output.put_line(sqlerrm); 8 commit; 9 end; 10 / Перехвачена ошибка: ORA-06502: PL/SQL: numeric or value error: character to number conversion terror PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; MSG Anonymous RegularProc В данном случае, поскольку ошибка перехвачена и обработана, в таблице остались строки, вставленные как анонимным блоком, так и завершившейся ошибкой процедурой. Это означает, что нельзя просто добавить прагму AUTONOMOUSJTRANSACTION в существующие хранимые процедуры в надежде, что они будут работать как прежде. Могут появиться существенные отличия.
Временные таблицы уровня транзакции При использовании временных (GLOBAL TEMPORARY) таблиц необходимо учитывать, что временные таблицы уровня транзакции нельзя одновременно использовать в нескольких транзакциях в одном сеансе. Временные таблицы управляются на уровне сеанса и при создании их на уровне транзакции (с удалением всех строк при фиксации) могут использоваться только в родительской или только в порожденной транзакции, но не в обеих. Так, следующий пример показывает, что автономная транзакция, пытающа-
Автономные транзакции
1/ /
яся читать или изменять временную таблицу уровня транзакции, уже использующуюся в сеансе, не срабатывает: tkyte@TKYTE816> c r e a t e global temporary t a b l e temp 2 (x int) 3 on commit delete rows 4 / Table created. tkyte@TKYTE816> create or replace procedure auto_procl 2 as 3 pragma autonomous_transaction; 4 begin 5 insert into temp values (1); 6 commit; 7 end; 8 / Procedure created. tkyte@TKYTE816> create or replace procedure auto_j?roc2 2 as 3 pragma autonomous_transaction; 4 begin 5 for x in (select * from temp) 6 loop 7 null; 8 end loop; 9 commit; 10 end; 11 / Procedure created. tkyte@TKYTE816> insert into temp values (2); 1 row created. tkyte@TKYTE816> exec auto_procl; BEGIN auto_procl; END; ERROR at line 1: ORA-14450: attempt to access a transactional temp table already in use ORA-06512: at "TKYTE.AUTO_PROC1", line 5 ORA-06512: at line 1 tkyte@TKYTE816> exec auto_proc2; BEGIN auto_proc2; END; * ERROR at line 1: ORA-14450: attempt to access a transactional temp table already in use ORA-06512: at "TKYTE.AUTO_PROC2", line 5 ORA-06512: at line 1
178
Глава 15
Именно это сообщение об ошибке вы и получите при попытке использовать одну и ту же временную таблицу в обеих транзакциях. Следует отметить, что это происходит только с одновременными транзакциями в одном сеансе. Несколько одновременно выполняющихся транзакций, если только каждая из них выполняется в отдельном сеансе, могут обращаться к временным таблицам уровня транзакции.
Изменяющиеся таблицы Казалось бы, автономные транзакции позволяют решить все проблемы изменяющихся таблиц. Эти решения, однако, могут стать началом новых логических проблем. Предположим, необходимо обеспечить выполнение правила, по которому максимальная зарплата сотрудника не может более чем вдвое превышать среднюю зарплату сотрудников соответствующего отдела. Можно начать с процедуры и триггера примерно следующего вида: tkyte@TKYTE816> create or replace 2 procedure sal_check(p_deptno in number) 3 is 4 avg_sal number; 5 max_sal number; 6 begin 7 select avg(sal), max(sal) 8 into avg_sal, max_sal 9 from emp 10 where deptno = p deptno; 11 12 if (max_sal/2 > avg_sal) 13 then 14 raise_application_error(-20001,'Rule violated'); 15 end if; 16 end; 17 / Procedure created. tkyte@TKYTE816> create or replace trigger sal_trigger 2 after insert or update or delete on emp 3 for each row 4 begin 5 if (inserting or updating) then 6 sal_check(:new.deptno); 7 end if; 8 9 if (updating or deleting) then 10 sal_check(:old.deptno); 11 end if; 12 end; 13 / Trigger created. tkyte@TKYTE816> tkyte@TKYTE816> update emp set sal = sal*l.l;
Автономные транзакции
\ /у
update emp set sal = sal*l.l ERROR at line 1: ORA-04091: table TKYTE.EMP is mutating, trigger/function may not see it ORA-06512: at "TKYTE.SAL_CHECK", line 6 ORA-06512: at "TKYTE.SALJTRIGGER", line 3 ORA-04088: error during execution of trigger 'TKYTE.SAL_TRIGGER' He слишком удачно. Мы сразу же столкнулись с ошибкой изменяющейся таблицы, поскольку нельзя читать таблицу в процессе ее изменения. Сразу приходит в голову мысль: раз таблица изменяется, надо использовать автономную транзакцию. Это и делается: tkyte@TKYTE816> create or replace 2 procedure sal_check(p_deptno in number) 3 is 4 pragma autonomous_transaction; 5 avg_sal number; 6 max_sal number; 7 begin Procedure created. Кажется, что проблема решена: tkyte@TKYTE816> update emp set sal = sal*l.l; 14 rows updated. tkyte@TKYTE816> commit; Commit complete. При более детальном рассмотрении, однако, оказывается, что эта идея принципиально ошибочна. В ходе тестирования обнаруживается, что велика вероятность следующего: tkyte@TKYTE816> update emp s e t s a l = 99999.99 where ename = 'WARD'; 1 row updated. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> exec sal_check(30); BEGIN sal_check(30); END; ERROR a t l i n e 1: ORA-20001: Rule v i o l a t e d ORA-06512: a t "TKYTE.SAL_CHECK", l i n e 14 ORA-06512: a t l i n e 1 Я изменил запись служащего WARD, установив ему очень большую зарплату; WARD работает в отделе 30, и его зарплата теперь намного превышает среднюю зарплату по этому отделу. Триггер этого не выявил, но постфактум, выполнив тот же код, что вы-
180
Глава 15
полняет триггер, мы обнаруживаем нарушение правила. Почему? Потому что в автономной транзакции невидимы выполняемые нами изменения. Поэтому такое увеличение зарплаты и проходит: процедура проверяет таблицу по состоянию до начала этого изменения! С нарушением столкнется следующий невезучий пользователь (как мы продемонстрировали, искусственно вызвав процедуру SAL_CHECK). При любом использовании автономной транзакции во избежание проблемы изменяющейся таблицы убедитесь, что вы поступаете правильно. В разделе "Аудит, записи которого не могут быть отменены" я использовал автономную транзакцию "безопасным" способом. Логика работы триггера не нарушается из-за того, что таблица в нем видна в состоянии до начала транзакции. В представленном выше примере триггер от этого существенно пострадал. Необходимо быть особенно внимательным и проверять корректность каждого триггера, оформленного как автономная транзакция.
Ошибки, которые могут произойти При работе с автономными транзакциями может произойти несколько ошибок. Для полноты изложения соответствующие сообщения представлены1 и прокомментированы ниже, но большинство них мы уже встречали в примерах.
ORA-06519: выполнен откат назад для незавершенной автономной транзакции I/ * Причина: Перед выходом из автономного PL/SQL-блока все начатые в нем // автономные транзакции должны быть завершены (зафиксированы // или отменены). В противном случае активная автономная // транзакция неявно откатывается и выдается это сообщение об // ошибке. // * Действие: Убедитесь, что перед выходом из автономного PL/SQL-блока // все активные автономные транзакции явно зафиксированы или // отменены. Это сообщение об ошибке выдается каждый раз при выходе из автономной транзакции, если перед этим не позаботились о ее явной фиксации или откате. При этом автономная транзакция откатывается, а исключительная ситуация распространяется в среду, откуда автономная транзакция была вызвана. Чтобы избавиться от этой проблемы, следует всегда обеспечивать фиксацию или откат транзакции для всех вариантов завершения PL/SQL-блока, оформленного как автономная транзакция. Это сообщение всегда связано с логической ошибкой в коде.
ORA-14450: попытка доступа к уже используемой временной таблице транзакций I/ * Причина: Выполнена попытка доступа к временной таблице уровня // транзакции, данные в которую поместила другая транзакция, // одновременно выполняющаяся в том же сеансе. ' В этом разделе текст сообщения об ошибке приведен так, как он выдается СУБД Oracle версии 8.1.6.0.0 при установке русского языка для сообщений. Обратите внимание на несоответствие терминологии, предлагаемой компанией Oracle. В примерах оставлены сообщения на английском языке. - Прим. научн. ред.
Автономные транзакции // * Действие: // //
l o l
Не пытайтесь обращаться к временной таблице, пока одновременно выполняющаяся транзакция не будет зафиксирована или отменена.
Как было продемонстрировано ранее, глобальная временная таблица, созданная с опцией ON COMMIT DELETE ROWS, может использоваться только одной транзакцией в сеансе. Необходимо не допускать использования одной временной таблицы в родительской и порожденной транзакции.
ORA-00060: взаимная блокировка при ожидании ресурса I/ * Причина: Произошла взаимная блокировка транзакций при ожидании // ресурса. // * Действие: Определите по трассировочному файлу, какие транзакции и // ресурсы стали причиной взаимной блокировки. При // необходимости повторите транзакцию. Эта ошибка не связана непосредственно с использованием автономных транзакций, но я представил ее здесь потому, что при использовании автономных транзакций повышается вероятность ее возникновения. Поскольку родительская транзакция приостанавливается на время выполнения порожденной транзакции, и не может продолжить работу до ее завершения, взаимная блокировка, не возникающая при одновременной работе двух сеансов, возникает при использовании автономной транзакции. Она произойдет при попытке изменить одни и те же данные в двух отдельных транзакциях одного сеанса. Необходимо проверять, не пытается ли порожденная транзакция заблокировать ресурсы, уже заблокированные родительской транзакцией.
Резюме В этой главе мы детально изучили возможности автономных транзакций. Вы увидели, как их можно использовать для создания более модульного и безопасного кода. Вы научились выполнять с их помощью невозможные до этого действия (например, выполнять операторы DDL в триггере или вызывать в операторе SELECT хранимую функцию независимо от того, изменяет ли она базу данных). Я объяснил, почему неразумно предполагать, что функция, вызываемая в SQL-операторе, будет выполнена определенное количество раз, и поэтому надо быть особенно осторожным при изменении базы данных в таких функциях. Вы узнали, как с помощью автономных транзакций избежать проблемы изменяющейся таблицы, и что они могут привести к ошибочному результату при некорректном использовании для решения этой проблемы. Автономные транзакции — мощное средство, которое сервер Oracle использует уже многие годы для выполнения рекурсивных SQL-операторов. Теперь их можно использовать и в приложениях. Прежде чем использовать это средство, надо хорошо понимать, как выполняются транзакции, когда они начинаются и когда заканчиваются, поскольку могут возникать различные побочные эффекты. Например, сеанс может заблокировать сам себя, родительская транзакция может видеть или не видеть результаты порожденной автономной, порожденная автономная транзакция не видит незафиксированные результаты родительской и т.д.
„,
•
ft'
•
•'
:
•
.
. '
.•
'
'
'
••'
'*
Динамический SQL Обычно при разработке программ все используемые в них SQL-операторы явно записываются в исходном коде. Такой вариант использования SQL-операторов обычно называют статический SQL. Многие полезные программы, однако, до момента запуска не "знают", какие именно SQL-операторы будут выполняться. Именно так и появляется динамический SQL — программа при запуске выполняет SQL-операторы, неизвестные во время компиляции. Возможно, программа генерирует запросы по ходу работы на основе введенных пользователем условий; возможно, это специализированная программа загрузки данных. Утилита SQL*Plus — прекрасный пример такого рода программы, как и любое другое средство выполнения произвольных запросов или генерации отчетов. Утилита SQL*Plus позволяет выполнить любой SQL-оператор и показать результаты его выполнения 1 хотя при ее компиляции операторы, которые выполняет пользователь, определенно не были известны. В этой главе мы обсудим, когда возникает необходимость использовать динамический SQL в программах и когда его имеет смысл применять. Мы сосредоточимся на использовании динамического SQL в программах на языке PL/SQL, поскольку именно в этой среде большинство разработчиков и используют динамический SQL в предварительно компилируемом формате. Поскольку использование динамического SQL — единственный способ выполнить SQL-операторы в программах на языке Java через интерфейс JDBC (выполнить динамический SQL в среде прекомпилятора SQLJ можно только через интерфейс JDBC) и на языке С при использовании библиотеки OCI, не имеет смысла обсуждать эти среды в данном контексте. В этих средах есть только динамический SQL; статический SQL вообще не поддерживается, так что там просто нет выбора. Мы же в данной главе:
184
Глава 16
Q рассмотрим последствия использования динамического или статического SQL; Q разберемся, как использовать динамический SQL в программах с помощью средств стандартного пакета DBMS_SQL; Q изучим возможности встроенного (native) динамического SQL; а
рассмотрим ряд проблем, с которыми можно столкнуться при использовании динамического SQL в приложениях, в частности нарушение цепочки зависимостей, уязвимость кода и трудности при его настройке.
Для выполнения всех примеров динамического SQL в этой главе необходим сервер Oracle 8.1.5 или более новых версий. Встроенный динамический SQL появился именно в этой версии и является одной из важнейших возможностей всех последующих версий. Для выполнения большинства примеров, в которых используются средства пакета DBMS_SQL, достаточно сервера Oracle версии 7.1 или более новых версий (правда, функции, обрабатывающие массивы, появились в пакете DBMS_SQL в версии 8.0).
Сравнение динамического и статического SQL Использование динамического SQL — естественная возможность работать с базой данных через функциональный интерфейс, такой как ODBC, JDBC и OCI. Статический SQL обычно принято использовать в средах с предварительной компиляцией кода, таких как Pro*C, SQLJ и PL/SQL (я не оговорился: компилятор PL/SQL можно рассматривать как прекомпилятор). При работе через функциональный интерфейс поддерживается только динамический SQL. Программист создает запрос в виде строки, а затем эта строка анализируется, связываются входящие в нее переменные, запрос выполняется, при необходимости выбираются строки из результирующего множества через курсор и, наконец, соответствующий курсор закрывается. В среде статического SQL эти действия выполняются автоматически. Для сравнения создадим две выполняющие одинаковые действия PL/SQL-процедуры: одну с — использованием динамического SQL, a вторую — с использованием статического. Вот версия на основе динамического SQL: scott@TKYTE816> c r e a t e or replace procedure DynEmpProc(p_job in varchar2) 2 as 3 type refcursor is ref cursor; 4 5 — При использовании динамического SQL необходимо 6 — создать хост-переменные и выделить ресурсы. 7 l_cursor refcursor; 8 l_ename emp.ename%type; 9 begin 10 11 — Начинаем с анализа запроса 12 open l_cursor for 13 'select ename 14 from emp 15 where job = :x' USING in p_job;
Динамический SQL 16 17 18 19 20 21 22 23
185
loop — и явно ВЫБИРАЕМ данные через курсор. fetch l_cursor into l_ename; — Необходимо самостоятельно обрабатывать ошибки — и делать выборку exit when l_cursor%notfound;
25 dbms_output.put_line(l_ename); 26 end loop; 27 28 — He забываем освободить ресурсы 29 close l_cursor; 30 exception 31 when others then 32 — а также перехватить и обработать все ошибки, 33 — чтобы не допустить утечки ресурсов 34 — при возникновении ошибок. 35 if (l_cursor%isopen) 36 then 37 close 1 cursor; 38 end if; 39 RAISE; 40 er end; 41 / Procedure created. А вот что мы имеем в случае статического SQL: scott@TKYTE816> create or replace procedure StaticEmpProc(p_job in varchar2) 2 as 3 begin 4 for x in (select ename from emp where job = p_job) 5 loop 6 dbms_output.put_line(x.ename); 7 end loop; 8 end; 9 / Procedure created. Эти две процедуры делают то же самое: scott@TKYTE816> set serveroutput on size 1000000 scott@TKYTE816> exec DynEmpProc('CLERK') SMITH ADAMS JAMES MILLER PL/SQL procedure successfully completed. scott@TKYTE816> exec StaticEmpProc('CLERK')
186
Глава 16
SMITH ADAMS JAMES MILLER
PL/SQL p r o c e d u r e s u c c e s s f u l l y
completed.
Понятно, однако, что версия с динамическим SQL требует от разработчика написания гораздо большего объема кода. По опыту знаю: статический SQL обеспечивает более высокую производительность труда программиста при написании кода (приложения разрабатываются быстрее), но динамический SQL обеспечивает большую гибкость при выполнении (программа в ходе работы может делать то, что не внесено в ее код явно). Кроме того, статический SQL (особенно в среде PL/SQL) будет выполняться намного эффективнее, чем динамический. Используя статический SQL, PL/SQL-машина при обработке одной строки интерпретируемого кода может сделать то, на что потребуется пять или шесть строк интерпретируемого кода с динамическим SQL. Поэтому я использую статический SQL где только возможно и применяю динамический, только если подругому задачу решить нельзя. Оба они эффективны, ни один не имеет принципиальных преимуществ перед другим, и оба имеют свои специфические возможности и средства повышения производительности.
Когда использовать динамический SQL? Многие задачи требуют использования динамического SQL в PL/SQL. Вот лишь некоторые из них. •
Разработка обобщенных процедур, выполняющих стандартные действия вроде выгрузки данных в файлы. В главе 9 был представлен пример такой процедуры.
•
Разработка универсальных процедур загрузки данных в не известные заранее таблицы. Мы рассмотрим использование динамического SQL для загрузки данных в таблицу.
• Динамический вызов других PL/SQL-процедур во время выполнения. Эта тема затрагивается в главе 23. Здесь мы рассмотрим ее более детально. •
Генерация условий (например, конструкции WHERE) в процессе работы на основе введенных пользователем данных. Это, пожалуй, основная причина использования динамического SQL большинством разработчиков. Я покажу в этой главе, как это надо (и как не надо!) делать.
О Выполнение операторов DDL. Поскольку PL/SQL не разрешает включать статические операторы DDL в код приложения, остается использовать динамический SQL. Это позволит выполнять операторы, начинающиеся с ключевых слов CREATE, ALTER, GRANT, DROP и т.п. Решаться перечисленные задачи будут с помощью двух средств языка PL/SQL. Сначала мы рассмотрим использование стандартного пакета DBMS_SQL. Этот пакет существует уже достаточно давно, он появился в версии 7.1. Пакет обеспечивает процедурный метод выполнения динамического SQL, аналогичный использованию
Динамический SQL
187
функциональных интерфейсов (таких как JDBC или ODBC). Затем поговорим о встроенном динамическим SQL (который реализуется в PL/SQL оператором EXECUTE IMMEDIATE). Это декларативный способ выполнения динамического SQL в языке PL/SQL и в большинстве случаев он синтаксически намного проще, чем использование пакета DBMS_SQL; кроме того, он обеспечивает более высокую производительность. Учтите, что многие подпрограммы пакета DBMS_SQL по-прежнему являются жизненно важными и активно используются в PL/SQL. Мы сравним два метода и попытаемся четко сформулировать, когда имеет смысл использовать каждый из них. Как только стало понятно, что необходимо использовать динамический SQL (статический SQL — лучший выбор в большинстве случаев), придется выбирать реализацию на основе пакета DBMS_SQL или встроенного динамического SQL. Пакет DBMS_SQL необходимо использовать в следующих случаях. •
Если заранее не известно количество или типы столбцов, с которыми придется работать. Пакет DBMS_SQL включает процедуры для описания результирующего множества. Встроенный динамический SQL не позволяет получить такое описание. При использовании встроенного динамического SQL необходимо знать характеристики результирующего множества при компиляции, если результаты необходимо обрабатывать в PL/SQL.
•
Если заранее не известно количество или типы связываемых переменных, с которыми придется работать. Пакет DBMS_SQL по ходу выполнения позволяет привязать с помощью процедур входные переменные к операторам. Встроенный динамический SQL требует учета количества и типов связываемых переменных на этапе компиляции (я приведу интересный способ решения этой проблемы).
•
Когда необходимо выбирать или вставлять тысячи строк и можно использовать обработку массивов. Пакет DBMS_SQL поддерживает обработку массивов — возможность выбрать N строк за раз, одним вызовом. Встроенный динамический SQL обычно не позволяет этого сделать, но это ограничение можно обойти, как будет показано далее.
•
Если в сеансе многократно выполняется один и тот же оператор. Пакет DBMS_SQL позволяет один раз разобрать оператор, а затем выполнять его многократно. При использовании встроенного динамического SQL частичный разбор будет осуществляться при каждом выполнении. В главе 10 было показано, почему такие дополнительные повторные разборы нежелательны.
Встроенный динамический SQL имеет смысл использовать в следующих случаях. •
Когда количество и типы столбцов, с которыми придется работать, заранее известны.
•
Когда заранее известно количество и типы связываемых переменных. (Можно также использовать контексты приложений, чтобы с помошью более простого встроенного динамического SQL выполнять операторы с заранее неизвестным количеством или типами связываемых переменных.)
•
Когда необходимо выполнять операторы DDL.
188
Глава 16
D Если динамически формируемые операторы будут выполняться лишь несколько раз (оптимальный вариант — однократно).
Использование динамического SQL Я рассмотрю основные шаги при использовании как стандартного пакета DBMS_SQL, так и возможностей встроенного динамического SQL.
Пакет DBMS_SQL DBMS_SQL — это стандартный встроенный пакет, поставляемый вместе с сервером. Стандартно он устанавливается в схеме пользователя SYS, а привилегия для его выполнения предоставляется роли PUBLIC. Это означает, что не должно быть никаких проблем с доступом к нему или созданием хранимых объектов, ссылающихся на его процедуры, — никаких дополнительных или специальных привилегий для этого предоставлять не надо. Одним из положительных свойств пакета является доступность соответствующей документации. Если при использовании DBMS_SQL необходимо вспомнить ту или иную особенность, можно просто выполнить следующий сценарий: scott@TKYTE816> s e t pagesize 30 scott@TKYTE816> s e t pause on scott@TKYTE816> prompt He забудьте нажать ENTER, чтобы получить результат Не забудьте нажать ENTER, чтобы получить результат scott@TKYTE816> s e l e c t t e x t 2 from all_source 3 where name = 'DBMS_SQL' 4 and type = 'PACKAGE' 5 order by line 6 / TEXT package dbms sql is —
OVERVIEW
This package provides a means to use dynamic SQL to access the ^database.
--
RULES AND LIMITATIONS
Если необходимо узнать возможности или просмотреть примеры, используйте этот прием для всех стандартных пакетов DBMS_ или VTL_. Пакет DBMS_SQL реализует процедурный подход к использованию динамического SQL. Этот подход сходен с тем, который используется в других языках (например,
Динамический SQL
189
при программировании на Java с использованием JDBC или на С с использованием библиотеки OCI) В общем случае, процесс, использующий пакет DBMS_SQL, будет иметь следующую структуру. •
Вызов OPEN_CURSOR для получения дескриптора курсора.
•
Вызов PARSE для анализа оператора. Один и тот же дескриптор курсора можно использовать для обработки нескольких операторов. В каждый момент времени, однако, обрабатывается только один оператор.
•
Вызов BIND_VARIABLE или BIND_ARRAY для передачи входных данных оператору.
•
Если обрабатывается запрос (оператор SELECT), необходимо вызвать процедуру DEFINE_COLUMN или DEFINE_ARRAY, чтобы указать серверу Oracle, как передавать результаты (как массивы или как скалярные величины и какой тип данных при этом использовать).
•
Вызов EXECUTE для выполнения оператора.
•
Если выполняется запрос, необходимо вызвать FETCH_ROWS для выборки данных. Для получения данных по порядковому месту в списке выбора используется вызов COLUMN_VALUE.
•
Если же выполняется блок кода PL/SQL или оператор DML с конструкцией RETURNING, можно вызвать процедуру VARIABLE_VALUE для получения результатов (параметров типа OUT) из блока по имени.
•
Вызов CLOSE_CURSOR.
В следующем псевдокоде продемонстрирована последовательность шагов для динамического выполнения запроса: 1) Открыть курсор 2) Проанализировать оператор 3) При необходимости получить описание оператора, чтобы выяснить количество и типы возвращаемых столбцов 4) Выполнить цикл по i по связываемым переменным (входным) Связать i-ую входную переменную с оператором 5) Выполнить цикл по i по возвращаемым столбцам Определить i-ый столбец, сообщив серверу Oracle тип переменной, в которую будут выбираться данные 6) Выполнить оператор 7) Выполнять цикл пока удается выбрать строку 8) Выполнить цикл по i по возвращаемым столбцам Получить значение 1-го столбца строки с помощью column_value Конец цикла по строкам 9) Закрыть курсор Для выполнения PL/SQL-блока или оператора DML используется следующий псевдокод: 1) Открыть курсор 2) Проанализировать оператор 3) Выполнить цикл по i по связываемым переменным (входным и выходным)
190
Глава 16
Связать i-ую переменную с оператором 4) Выполнить оператор 5) Выполнить цикл по i по выходным связываемым переменным Получить значение i-й выходной переменной с помощью variable_value 6) Закрыть курсор Наконец, при выполнении операторов DDL (в которых нельзя использовать связываемые переменные), PL/SQL-блоков или операторов DML, в которых нет связываемых переменных, представленный выше алгоритм упрощается (хотя, для этого типа операторов я всегда предпочитаю использовать не пакет DBMS_SQL, а встроенный динамический SQL): 1) 2) 3) 4)
Открыть курсор Проанализировать оператор Выполнить оператор Закрыть курсор
Рассмотрим пример использования пакета DBMS_SQL для выполнения запроса, подсчитывающего количество строк в таблице базы данных, к которой пользователь имеет доступ: scott@TKYTE816> c r e a t e or replace 2 function get_row_cnts(p_tname in varchar2) r e t u r n number 3 as 4 l_theCursor integer; 5 l_columnValue number default NULL; 6 l_status integer; 7 begin 8 9 — Шаг 1, открыть курсор. 10 l_theCursor := dbms_sql.open_cursor; Мы начинаем блок с обработчиком исключительных ситуаций. Если по ходу работы этого блока возникает ошибка, необходимо закрыть только что открытый курсор в обработчике исключительных ситуаций, чтобы предотвратить "утечку курсоров", когда дескриптор курсора теряется при распространении исключительной ситуации за пределы функции. 11 12 13 14 15 16 17
begin — Шаг 2, проанализировать запрос. dbms_sql.parse(c => l_theCursor, statement => 'select count(*) from ' || language_flag «•> dbms_sql. native );
Обратите внимание, что параметр Ianguage_flag получает значение одной из констант пакета DBMS_SQL, NATIVE. Это вызывает анализ запроса по правилам сервера, выполняющего код. Можно также задать значения DBMS_SQL.V6 или DBMS_SQL.V7. Я всегда использую значение NATIVE.
Динамический SQL
\у\
Шаги 3 и 4 из представленного ранее псевдокода нам не нужны, поскольку результаты известны и никаких связываемых переменных в этом примере нет. 18 19 20 21 22
— Шаг 5, убедиться, что запрос возвращает данные типа NUMBER. dbms_sql.define_column (с => l_theCursor, p o s i t i o n => 1, column => l_columnValue);
Процедура DEFINE_COLUMN — перегруженная, так что компилятор сам определяет, когда надо вызывать версию для типа NUMBER, а когда — для DATE или VARCHAR. 23 24 25
— Шаг 6, выполнить оператор. l_status := dbms_sql.execute(l_theCursor);
Если бы выполнялся оператор DML, переменная L_STATUS получила бы значение, равное количеству возвращенных строк. Для оператора SELECT возвращаемое значение несущественно. 26 — Шаг 7, выбрать строки. 27 if (dbms sql.fetch rows(с => 1 theCursor) > 0) 28 then 29 — Шаг 8, получить значения из очередной строки. 30 dbms_sql.column_value(c => l_theCursor, 31 position => 1, 32 value => l_columnValue); 33 end if; 34 35 — Шаг 9, закрыть курсор. 36 dbms_sql.close_cursor(с => l_theCursor); 37 return l_columnValue; 38 exception 39 when others then 40 dbms_output.put_line('===> ' || sqlerrm); 41 dbms_sql.close_cursor(с => l_theCursor); 42 RAISE; 43 end; 44 end; 45 / Function created. scott@TKYTE816> set serveroutput on scott@TKYTE816> begin 2 dbms_output.put_line('Emp has this many rows ' || 3 get_row_cnts('emp')); 4 end; 5 / Emp has this many rows 14 PL/SQL procedure successfully completed. scott@TKYTE816> begin 2 dbms_output.put_line('Not a table has this many rows ' ||
192
Глава 16
3 get_row_cnts('NOT_A_TABLE')); 4 end; 5 / ===> ORA-00942: t a b l e or view does not e x i s t begin * ERROR a t l i n e 1: ORA-00942: t a b l e or view does not e x i s t ORA-06512: a t "SCOTT.GET_ROW_CNTS", l i n e 60 ORA-06512: a t l i n e 2 Рассмотренный пример начинается созданием курсора с помощью вызова DBMS_SQL.OPEN_CURSOR. Следует отметить, что это специфический курсор DBMS_SQL — его нельзя передать для выборки данных в приложение на Visual Basic или использовать в качестве PL/SQL-курсора. Для выборки данных с помощью этого курсора необходимо использовать подпрограммы пакета DBMS_SQL. Затем мы проанализировали запрос SELECT COUNT(*) FROM TABLE, где значение TABLE передается при вызове во время выполнения — оно просто конкатенируется со строкой запроса. Приходится "вклеивать" имя таблицы в запрос, поскольку связываемые переменные нельзя использовать в качестве идентификатора (имени таблицы или имени столбца, например). После анализа запроса мы вызвали DBMS_SQL.DEFINE_COLUMN, чтобы указать, что первый (и единственный в данном случае) столбец в списке SELECT должен интерпретироваться при выборке как тип NUMBER. To, что мы хотим выбирать данные именно этого типа, явно не указано — процедура DBMS_SQL.DEFINE_COLUMN перегружена и имеет несколько версий для данных типа VARCHAR, NUMBER, DATE, BLOB, CLOB и так далее. Тип возвращаемого значения определяется по типу переменной, в которую он помещается. Поскольку переменная L_COLUMNVALUE в рассмотренном примере имеет тип NUMBER, вызывается версия процедуры DEFINE_COLUMN для чисел. Затем мы вызываем DBMS_SQL.EXECUTE. Если бы выполнялся оператор INSERT, UPDATE или DELETE, функция EXECUTE вернула бы количество затронутых строк. В случае запроса возвращаемое значение функции не определено, и его можно проигнорировать. После выполнения оператора вызывается функция DBMS_SQL.FETCH_ROWS. Функция FETCH_ROWS возвращает количество фактически выбранных строк. В нашем случае, поскольку связывались скалярные переменные (не массивы), функция FETCH_ROWS будет возвращать 1 до тех пор, пока не исчерпаются данные, — тогда она вернет 0. При извлечении каждой строки мы вызываем DBMS_SQL.COLUMN_VALUE для каждого столбца в списке выбора, чтобы получить его значение. Наконец, мы завершаем выполнение функции, закрывая курсор с помощью вызова DBMS_SQL.CLOSE_CURSOR. Теперь рассмотрим, как использовать пакет DBMS_SQL для обработки динамически формируемых параметризованных PL/SQL-блоков или операторов DML. Я часто использую такое динамическое формирование, например, при загрузке данных из файлов операционной системы с помощью пакета UTL_FILE (он позволяет читать текстовые файлы в PL/SQL). Пример подобного рода утилиты был представлен в главе 9. Там мы использовали пакет DBMS_SQL для динамического построения операторов INSERT, в которых количество столбцов становится известным только при выполнении и меня-
Динамический SQL
ется от вызова к вызову. Нельзя использовать встроенный динамический SQL для загрузки в таблицу произвольного количества столбцов, поскольку для этого уже на этапе компиляции необходимо точно знать количество связываемых переменных. Следующий пример создан специально, чтобы показать особенности использования подпрограмм пакета DBMS_SQL при работе с блоками PL/SQL и операторами DML (это пример проще реализовать с помощью встроенного динамического SQL, поскольку в этом случае количество связываемых переменных извест?ю во время компиляции): scott@TKYTE816> c r e a t e or replace 2 function update_row(p_owner 3 p_newDname 4 p_newLoc 5 p_deptno 6 p_rowid 7 return number 8
i n varchar2, in varchar2, i n varchar2, in varchar2, out varchar2)
is
9 l_theCursor integer; 10 l_columnValue number default NULL; 11 l_status integer; 12 l_update long; 13 begin 14 l_update := 'update ' || p_owner || '.dept 15 set dname = :bvl, loc = :bv2 16 where deptno = to_number(:pk) 17 returning rowid into :out'; 18 19 — Шаг 1, открыть курсор. 20 l_theCursor := dbms_sql.open_cursor; 21 Начнем вложенный блок с обработчиком исключительных ситуаций. Если в этом блоке кода возникнет ошибка, необходимо обработать ее как можно ближе к месту возникновения и закрыть курсор, чтобы избежать "утечки курсоров", когда дескриптор открытого курсора просто теряется при распространении ошибки за пределы подпрограммы. 22 23 24 25 26 27 28 29 30 31 32 33 34 35 -r 36
7 Заг. 244
begin — Шаг 2, проанализировать запрос. dbms_sql.parse(c => l_theCursor, statement => l_update, language_flag => dbms_sql.native); — Шаг З, связать все входные и выходные переменные. dbms_sql.bind_variable(c => l_theCursor, name => ' :bvl •, value => p_newDname); dbms_sql.bind_variable(c => l_theCursor, name => ':bv2', value => p_newLoc); dbms sql.bind variable(c => 1 theCursor, — — — > , name => ':pk',
194 37 38 39 40 41 42
Глава 16 value => p_deptno); dbms_sql.bind_variable(с => l_theCursor, name => ':out', value => p_rowid, out_value_size => 4000);
Учтите, что, хотя возвращаемые переменные передаются как параметры в режиме OUT, необходимо связать их перед выполнением. Необходимо также указать наибольший размер ожидаемого результата (OUT_VALUE_SIZE), чтобы сервер Oracle выделил под него соответствующее пространство. 43 — Шаг 4: выполнить оператор. Поскольку это оператор DML, 44 — в переменной L_STATUS окажется количество измененных строк. . 45 — Именно это значение мы и возвращаем. 46 47 l_status := dbms_sql.execute(l_theCursor); 48 49 — Шаг 5: выбрать OUT-переменные из результатов выполнения. 50 dbms_sql.variable_yalue(c => l_theCursor, 51 name => ':out', 52 value => p_rowid); 53 54 — Шаг 6: закрыть курсор. 55 dbms_sql.close_cursor(с => l_theCursor); 56 return l_columnValue; 57 exception 58 when dup_val_on_index then 59 dbms_output. put_line (' = > ' II sqlerrm) ; 60 dbms_sql.close_cursor(c => l_theCursor); 61 RAISE; 62 end; 63 end; 64 / Function created. scott@TKYTE816> set serveroutput on scott@TKYTE816> declare 2 l_rowid varchar(50); 3 l_rows number; 4 begin 5 l_rows := update_row('SCOTT', 'CONSULTING', 'WASHINGTON', '10', l_rowid); 6 7 dbms_output.put_line('Updated ' || l_rows || ' rows'); 8 dbms_output.put_line('its rowid was ' II l_rowid); 9 end; 10 / Updated 1 rows its rowid was AAAGnuAAFAAAAESAAA PL/SQL procedure successfully completed.
I
Динамический SQL
195
Итак, я продемонстрировал особенности использования пакета DBMS_SQL для выполнения блока кода с передачей входных данных и выборкой результатов. Повторю еще раз: представленный выше блок кода лучше реализовать с помощью встроенного динамического SQL (чуть ниже мы так и сделаем). Подпрограммы пакета DBMS_SQL в нем применялись в основном для демонстрации использования соответствующего функционального интерфейса. В других главах книги, в частности в главе 9, посвященной загрузке данных, продемонстрировано, почему пакет DBMS_SQL по-прежнему широко используется. В главе 9 рассмотрен код программ загрузки и выгрузки данных на PL/SQL. В них средства DBMS_SQL используются в полном объеме, позволяя обрабатывать неизвестное количество столбцов любого типа как во входных данных (для операторов INSERT), так и в результатах (для операторов SELECT). Мы рассмотрели примерно 75 процентов функциональных возможностей пакета DBMS_SQL. Чуть позже, многократно выполняя один и тот же динамически сформированный оператор, мы рассмотрим взаимодействие с помощью массивов и сравним использование пакета DBMS_SQL и встроенного динамического SQL. Пока, однако, мы завершим обзор пакета DBMS_SQL. Полный список входящих в него подпрограмм и описание их входных/выходных параметров можно найти в руководстве Oracle8i Supplied PL/SQL Packages Reference, где отдельно рассмотрена каждая подпрограмма.
Встроенный динамический SQL Встроенный динамический SQL впервые появился в Oracle 8i. Он позволяет декларативно выполнять динамический SQL в среде PL/SQL. Большинство действий можно выполнить с помощью одного оператора, EXECUTE IMMEDIATE, а остальные — с помощью оператора OPEN FOR. Оператор EXECUTE IMMEDIATE имеет следующий синтаксис: EXECUTE IMMEDIATE 'оператор' [INTO {переменная!., переменная2, . . . nepeMeHHanN | запись}] [USING [IN | OUT I IN OUT] связываемая_переменная1, . . . связываемая_переменнаяЫ] [{RETURNING I RETURN} INTO результат1 [, . . . , р е з у л ь т а т ы ] . . . ] ; где: •
оператор — любой оператор SQL или PL/SQL-блок;
Q переменная1, переменная2, ... nepeMeHHanN или запись — переменные PL/SQL, в которые необходимо выбрать данные (столбцы одной строки результатов оператора SELECT); •
связываемая_переменная1, ... связываемая_перемсннаяК — набор переменных PL/SQL, используемых для передачи входных данных/результатов;
•
результат!, ... pe3}MibTaTN — набор PL/SQL-переменных, используемых для размещения результатов, возвращаемых конструкцией RETURN оператора DML.
В следующем примере код для функций GET_ROW_CNTS и UPDATE_ROW, которые мы ранее реализовали с помощью средств пакета DBMS_SQL, переписан с использованием оператора EXECUTE IMMEDIATE. Начнем с функции GET_ROW_CNTS:
196
Глава 16
scott@TKYTE816> create or replace 2 function get_row_cnts(p_tname in varchar2) return number 3 as 4 l_cnt number; 5 begin 6 execute immediate 7 'select count(*) from ' I I p_tname 8 into l_cnt; 9 10 return l_cnt; 11 end; 12 / Function created. scott@TKYTE816> set serveroutput on scott@TKYTE816> exec dbms_output.put_line(get_row_cnts('emp')); 14 PL/SQL procedure successfully completed. Использовав оператор SELECT...INTO... в качестве аргумента для EXECUTE IMMEDIATE, мы существенно уменьшили объем кода. Девять процедурных шагов, необходимых при использовании пакета DBMS_SQL, превратились в один шаг в случае встроенного динамического SQL. He всегда удается свести все к одному шагу — иногда необходимо три, как будет показано ниже, — но общая идея понятна. Встроенный динамический SQL в данном случае обеспечивает более высокую производительность при написании кода (последствия его использования с точки зрения производительности мы рассмотрим чуть позже). Также бросается в глаза отсутствие раздела EXCEPTION — обработка исключительных ситуаций не нужна, поскольку все происходит неявно. Нет курсора, который необходимо закрывать, ничего не нужно освобождать. Сервер Oracle все делает сам. Теперь реализуем с помощью встроенного динамического SQL функцию UPDATE_ROW: scott@TKYTE816> create or replace 2 function update_row(p owner 3 p_newDname 4 p newLoc 5 p deptno 6 p_rowid 7 return number Q О
9 10 11 12 13 14 15 16 17
in varchar2, in varchar2, in varchar2, in varchar2, out varchar2)
•i о
IS
begin execute immediate 'update ' lip owner || '.dept set dname = :bvl, loc = :bv2 where deptno • to number(:pk) returning rowid into :out' using p newDname, p newLoc , p deptno returning into p rowid;
Динамический SQL
197
18 return sql%rowcount; 19 end; 20 / Function created. scott@TKYTE816> set serveroutput on scott@TKYTE816> declare 2 l_rowid varchar(50); 3 l_rows number; 4 begin 5 l_rows := update_row('SCOTT1, 'CONSULTING', 6 'WASHINGTON', 4 0 ' , l_rowid); 7 8 dbms_output.put_line('Updated ' || l_rows || ' rows'); 9 dbms_output.put_line('its rowid was ' || l_rowid); 10 end; 11 / Updated 1 rows i t s rowid was AAAGnuAAFAAAAESAAA PL/SQL procedure successfully completed. Снова код существенно сократился — один шаг вместо шести; такой код проще читать и сопровождать. В этих двух случаях встроенный динамический SQL, несомненно, превосходит средства пакета DBMS_SQL. Помимо оператора EXECUTE IMMEDIATE встроенный динамический SQL поддерживает динамическую обработку курсорных переменных, REF CURSOR. Курсорные переменные достаточно давно поддерживаются сервером Oracle (с версии 7.2). Первоначально они позволяли открыть (OPEN) запрос (результирующее множество) в хранимой процедуре и передать ссылку на него клиенту. С их помощью хранимые процедуры возвращают результирующие множества клиентам при использовании VB, протоколов JDBC и ODBC или библиотеки OCI. Позднее, в версии 7.3, поддержка курсорных переменных была расширена, так что в PL/SQL появилась возможность использовать их не только в операторе OPEN, но и в операторе FETCH (в качестве клиента могла использоваться другая подпрограмма на PL/SQL). Это позволило подпрограмме на PL/SQL принимать результирующее множество в качестве входных данных и обрабатывать его. Таким образом, впервые стало возможно централизовать общую обработку результатов запросов: одна подпрограмма может выбирать данные из нескольких различных запросов (результирующих множеств). До появления версии Oracle 8i, однако, курсорные переменные по сути были исключительно статические. На этапе компиляции (при создании хранимой процедуры) надо было точно знать, какой SQL-запрос будет выполняться. Это было весьма существенное ограничение, поскольку не позволяло динамически изменять условия запроса, запрашивать другую таблицу и т.п. Начиная с Oracle 8i встроенный динамический SQL позволяет динамически открывать для запроса курсорную переменную. При этом используется следующий синтаксис: OPEN курсорная_переменная FOR ' s e l e c t . . . " USING связываемая_переменная1, связываемая_переменная2, . . . ;
198
Глава 16
Итак, с помощью курсорных переменных и динамического SQL можно реализовать обобщенную процедуру запроса таблицы в зависимости от входных данных и возвращения результирующего множества клиенту для дальнейшей обработки: scott@TKYTE816> c r e a t e or replace package my_pkg 2 as 3 type refcursor_Type is ref cursor; 4 5 procedure get_emps(p_ename in varchar2 default NULL, 6 p_deptno in varchar2 default NULL, 7 p_cursor in out refcursor_type); 8 end; 9 / Package created. scott@TKYTE816> create or replace package body my_pkg 2 as 3 procedure get_emps(p_ename in varchar2 default NULL, 4 p_deptno in varchar2 default NULL, 5 p_cursor in out refcursor_type) 6 is 7 l_query long; 8 l_bind varchar2(30) ; 9 begin 10 l_query := 'select deptno, ename, job from emp'; 11 12 if (p_ename is not NULL) 13 then 14 l_query := l_query || ' where ename like :x'; 15 l_bind := '%' || upper(p_ename) || '%'; 16 elsif (p_deptno is not NULL) 17 then 18 l_query := l_query || ' where deptno = to_number(:x) '; 19 l_bind := p_deptno; 20 else 21 raise_application_error(-20001,'Missing search criteria'); 22 end if; 23 24 open p cursor for 1 query using 1 bind; 25 end; 26 end; 27 / Package body created. scott@TKYTE816> variable С refcursor scott@TKYTE816> set autoprint on 1 scott@TKYTE816> exec my_pkg.get_emps(p_ename => 'a , p_cursor => :C) PL/SQL procedure successfully completed.
Динамический SQL
DEPTNO ENAME 20 30 30 10 30 30 30
ADAMS ALLEN BLAKE CLARK JAMES MARTIN WARD
JOB CLERK SALESMAN MANAGER MANAGER CLERK SALESMAN SALESMAN
7 rows selected. scott@TKYTE816> exec my_pkg.get_emps(p_deptno=> '10', p_cursor => :C) PL/SQL procedure successfully completed. DEPTNO ENAME 10 CLARK 10 KING 10 MILLER
JOB MANAGER PRESIDENT CLERK
Если созданный динамически запрос возвращает более одной строки, надо использовать представленный выше метод, а не оператор EXECUTE IMMEDIATE. Итак, по сравнению с представленными выше подпрограммами пакета DBMS_SQL, использование операторов EXECUTE IMMEDIATE и OPEN FOR существенно упрощает написание программ. Значит ли это, что пакет DBMS_SQL больше использовать не придется? Определенно, — не значит. Представленные выше примеры показывают, насколько простым может быть использование динамического SQL, если количество связываемых переменных известно во время компиляции. Если бы мы этого не знали, то не смогли бы использовать оператор EXECUTE IMMEDIATE так просто, как в представленных примерах. Для этого оператора количество связываемых переменных надо знать заранее. Пакет DBMS_SQL в этом отношении обеспечивает большую гибкость. Помимо количества связываемых переменных необходимо знать еще и столбцы, входящие в результат выполнения SQL-оператора SELECT. Если количество и типы этих столбцов неизвестны, использовать оператор EXECUTE IMMEDIATE тоже не удастся. Можно будет использовать оператор OPEN FOR, если клиент, получающий курсорную переменную, не является другой подпрограммой на языке PL/SQL. Оператор EXECUTE IMMEDIATE обеспечит более высокую производительность по сравнению с пакетом DBMS_SQL для всех операторов, анализируемых и выполняемых однократно (все наши примеры пока были именно такими). Для выполнения подпрограмм пакета DBMS_SQL в этом отношении требуется больше ресурсов, потому что нужно вызвать пять или шесть процедур там, где достаточно одного выполнения оператора EXECUTE IMMEDIATE. Однако пакет DBMS_SQL обеспечивает более высокую производительность, если его процедуры используются для многократного выполнения одного и того же проанализированного оператора. Оператор EXECUTE IMMEDIATE не позволяет "повторно использовать" проанализированные операторы. Он всегда разбирает оператор, и расходы ресурсов на повторные выполнения этой операции со временем превышают расхо-
200
Глава 16
ды на дополнительные вызовы процедур. Особое значение это приобретает в многопользовательской среде. Наконец, операторы EXECUTE IMMEDIATE и OPEN не позволяют обрабатывать массивы так же просто, как подпрограммы пакета DBMS_SQL и, как будет продемонстрировано, одно это может принципиально повлиять на производительность.
Сравнение пакета DBMSSQL и встроенного динамического SQL Рассмотрев способы реализации подпрограмм с помощью пакета DBMS_SQL и встроенного динамического SQL, поговорим о том, когда следует использовать каждый из способов. Решение зависит от следующих факторов. •
Известно ли на этапе компиляции, какие связываемые переменные придется использовать? Если — нет, надо выбрать пакет DBMS_SQL.
Q Известны ли на этапе компиляции все выходные данные? Если — нет, нужно отдать предпочтение пакету DBMS_SQL. •
Надо ли использовать курсорную переменную для возврата результирующего множества из хранимой процедуры? Если — да, придется использовать оператор OPEN FOR.
О Будет ли формируемый динамически оператор выполняться в сеансе один раз или многократно. Если один и тот же динамически формируемый оператор выполняется несколько раз, пакет DBMS_SQL обеспечит более высокую производительность. •
Надо ли использовать обработку массивов для динамически формируемых операторов.
Три из этих факторов мы рассмотрим ниже (на самом деле — даже четыре, поскольку многократное выполнение оператора мы рассмотрим как в случае обработки массивов, так и без оной).
Связываемые переменные Связываемые переменные существенно влияют на производительность системы. Если они не используются, производительность недопустимо низка. Метод автоматической подстановки связываемых переменных (auto binding) путем установки соответствующего значения параметра CURSOR_SHARING был рассмотрен в главе 10. Это несколько улучшает ситуацию, но приводит к избыточному расходованию ресурсов, поскольку сервер вынужден переписывать запрос и удалять информацию, существенную для оптимизатора, которую можно было бы оставить при явном включении связываемых переменных в код. Предположим, необходимо создать процедуру, динамически создающую запрос на основе введенных пользователем данных. Запрос всегда будет возвращать однотипные результаты (тот же список выбора), но конструкция WHERE будет меняться в зависимости от входных данных. Из соображений производительности необходимо использо-
Динамический SQL
20 1
вать связываемые переменные. Как это сделать с помощью встроенного динамического SQL и средств пакета DBMS_SQL? Чтобы представить методы, начну со спецификации подпрограммы. Разрабатываемая процедура будет иметь следующий вид: scott@TKYTE816> c r e a t e or replace package dyn_demo 2 as 3 type array is table of varchar2(2000); 4 6 /* 7 * DO_QUERY будет динамически запрашивать таблицу emp 8 * и обрабатывать результаты. Ее можно вызвать 9 * следующим образом: 10 * 11 * dyn_demo.do_query( array('ename', 'j o b ' ) , 12 a r r a y C l i k e ' , ' =' ) , 13 array('%A%\ 'CLERK')); 14 * 15 * для выполнения запроса: 16 * 17 * select * from emp where ename like '%A%' and job = 'CLERK' 18 19 * например. 20 */ 21 procedure do_query(p_cnames in array, 22 p_operators in array, 23 p_values in array); 24 25 end; 26 / Package created. Вполне естественно реализовать ее с помощью DBMS_SQL — для таких ситуаций и создавался этот пакет. Можно организовать цикл по массивам столбцов и значений и построить конструкцию WHERE. Затем проанализировать запрос и выполнить еще один цикл по массивам для связывания значений переменных. После этого выполнить оператор, выбрать строки и обработать их. Это можно записать следующим образом: scott@TKYTE816> c r e a t e or replace package body dyn_demo 2 as 3 4 /* 5 * Реализация динамического запроса с неизвестными 6 * связываемыми переменными средствами DBMS SQL 7 */ 8 g_cursor int default dbms sql.open cursor; 9 10 11 procedure do query(p cnames in array, 12 p_operators in array, 13 p values in array)
202
Глава 16
14 is 15 l_query long; 16 l_sep varchar2(20) default ' where '; 17 l_comma varchar2(l) default " ; 18 l_status int; 19 l_colValue varchar2(4000); 20 begin 21 /* 22 * Это наш постоянный список выбора — мы всегда 23 * выбираем эти три столбца. Изменяются 24 * условия выбора. 25 */ 26 l_query := 'select ename, empno, job from emp'; 27 28 /* 29 * Мы строим условие, сначала 30 * помещая в запрос конструкцию: 31 32 * cname operator :bvX 33 * 34 */ 35 for i in 1 .. p_cnames.count loop 36 l_query := l_query || l_sep |I p_cnames(i) || 37 p_operators(i) II 38 ':bv' || i; 39 l_sep := ' and '; 40 end loop; 41 42 / 43 Теперь можно анализировать запрос 44 */ 45 dbms_sql.parse(g cursor, 1 query, dbms sql.native); 46 47 /* 48 * и определять столбцы результата. Все три столбца 49 * выбираются в переменные типа VARCHAR2. 50 */ 51 for i in 1 .. 3 loop 52 dbms_sql.define_column(g_cursor, i, l_colValue, 4000); 53 end loop; 54 55 /* 56 * Теперь можно связать входные переменные запроса 57 */ 58 for i in 1 .. p_cnames.count loop 59 dbms_sql.bind_yariable(g_cursor, ':b i, p_values(i), 4000); 60 end loop; 61 62 /* 63 * и выполнить его. Так формируется результирующее множество 64 */
Динамический SQL
203
65 l _ s t a t u s : = dbms_sql.execute(g_cursor); 66 67 /* 68 * Теперь проходим в цикле по строкам и выдаем результаты. 69 */ 70 while (dbms_sql.fetch_rows(g_cursor) > 0) 71 loop 72 l_coirana :- " ; 73 for i in 1 .. 3 loop 74 dbms_sql.column_value(g_cursor, i, l_colValue); 75 dbms_output.put(l_comma II l_colValue); 76 l_comma := ','; 77 end loop; 78 dbms_output.new_line; 79 end loop; 80 end; 81 82 end dyn_demo; 83 / Package body created. scott@TKYTE816> set serveroutput on scott@TKYTE816> begin 2 dyn_demo.do_query(dyn_demo.array('ename', 'j ob'), 3 dyn_demo.array('like', '='), 4 dyn_demo.array('%A%', 'CLERK')); 5 end; 6 / ADAMS,7876,CLERK JAMES,7900,CLERK PL/SQL procedure successfully completed. Как видите, все просто и в рамках действий, предусмотренных для использования пакета DBMS_SQL. Теперь реализуем то же самое с помощью встроенного динамического SQL. Здесь мы сталкиваемся с проблемой. Для динамического выполнения запроса со связываемыми переменными во встроенном динамическом SQL используется следующий синтаксис: OPEN курсорная_переменная FOR ' s e l e c t . . . ' USING переменная1, переиенная2, переменная3,
...;
Проблема в том, что на этапе компиляции мы не знаем размера списка USING — будет ли в нем одна переменная, две или вообще ни одной? Поэтому необходимо параметризировать запрос, но использовать обычные связываемые переменные нельзя. Можно, однако, использовать средство, предназначавшееся совсем для других целей. В главе 21, при изучении средств детального контроля доступа, мы рассмотрим контекст приложения (application context) и его использование. Контекст приложения, по сути, позволяет поместить в пространство имен (namespace) пару переменная/значение. К этой паре переменная/значение можно обращаться в SQL-операторах с помощью встроенной функции SYS_CONTEXT. Контекст приложения, таким образом, можно исполь-
204
Глава 16
зовать для параметризации запроса, помещая связываемые значения в пространство имени и выбирая их в запросе с помощью встроенной функции SYS_CONTEXT. Итак, вместо запроса следующего вида: select from where and
ename, empno, job emp ename like :bvl job = :bv2;
создаем такой запрос: select from where and
ename, empno, job emp ename like SYS_CONTEXT('namespace','ename') job = SYS_CONTEXT('namespace1,'job');
Код, реализующий этот метод, может выглядеть так: scott@TKYTE816> REM Пользователь SCOTT должен иметь привилегию CREATE ANY ^CONTEXT scott@TKYTE816> REM или роль с такой привилегий, иначе код не сработает scott@TKYTE816> create or replace context bv_context using dyn_demo 2 / Context created. scott@TKYTE816> create or replace package body dyn_demo 2 as 3 4 procedure do_query(p_cnames in array, 5 p_operators in array, 6 p_values in array) 7 is 8 type re is ref cursor; 10 l_query long; 11 l_sep varchar2(20) default ' where ',12 l_cursor re; 13 l_ename emp.ename%type; 14 l_empno emp.empno%type; 15 l_job emp.job%type; 16 begin 17 /* 18 * Это наш постоянный список выбора — мы всегда 19 * выбираем эти три столбца. Изменяются 20 * условия выбора. 21 */ 22 l_query := 'select ename, empno, job from emp'; 23 24 for i in 1 .. p_cnames.count loop 25 l_query := l_query || l_sep || 26 p_cnames(i) II ' ' || 27 p_operators(i) II ' ' || 28 'sys_context(''BV_CONTEXT'', ''' ||
Динамический SQL
2 0 5
r-j с п я т п ^ ч (i ^ It ' ' '1'; 29 30 1 sep := ' and '; 31 dbms session.set context('bv context', 32 p cnames(i), 33 p values(i)); 34 end loop; 35 36 open 1 cursor for 1 query; 37 loop 38 fetch 1 cursor into 1 ename, 1 empno, 1 job; exit when 1 cursor%notfound; 39 40 dbms output.put line(l ename ||', '|| 1 empno ]|','|| 1 job); 41 end loop; 42 close 1 cursor; 43 end; 44 45 end dyn demo; 46
Package body created. scott@TKYTE816> set serveroutput on scott@TKYTE816> begin 2 dyn_demo.do_query( dyn_demo.array('ename', 'j ob'), 3 dyn_demo.array('like', ' ='), 4 dyn_demo.array('%A%', 'CLERK')); 5 end; 6 / ADAMS,7876,CLERK JAMES,7900,CLERK PL/SQL procedure successfully completed. Так что, с точки зрения использования связываемых переменных, все гораздо сложнее, чем в случае пакета DBMSJSQL, — необходим хитрый прием. После того, как вы поймете суть этого приема, вполне можно использовать встроенный динамический SQL вместо средств пакета DBMS_SQL, если только запрос выдает фиксированное количество результатов и используется контекст приложения. Чтобы эффективно решать с помощью встроенного динамического SQL подобного рода задачи, необходимо создать и использовать контекст приложения. В конечном итоге оказывается, что представленный выше пример с курсорными переменными при реализации с помощью встроенного динамического SQL работает быстрее. В случае простых запросов, когда временем обработки самого запроса можно пренебречь, встроенный динамический SQL обеспечивает скорость выборки данных почти вдвое выше, чем пакет DBMS_SQL.
Количество столбцов выходных данных на этапе компиляции не известно Здесь все понятно: если клиент, выбирающий и обрабатывающий данные, создается на PL/SQL, необходимо использовать пакет DBMS_SQL. Если клиент, выбирающий и обрабатывающий данные, — приложение на процедурном языке программирования, ис-
206
Глава 16
пользующее интерфейсы ODBC, JDBC, OCI и т.п., необходимо использовать встроенный динамический SQL. Рассмотрим ситуацию, когда, получая запрос во время выполнения, мы не знаем, сколько столбцов входит в список выбора. Необходимо определить это в коде PL/SQL. Оказывается, встроенный динамический SQL использовать нельзя, поскольку придется включить в код оператор вида: FETCH курсор INTO переменная!., переменная2, переменнаяЗ, . . . ; но сделать этого нельзя, потому что до момента выполнения не известно, сколько переменных надо в него поместить. Это один из случаев, когда придется использовать средства пакета DBMS_SQL, поскольку он позволяет применять следующие конструкции: 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
while (dbms_sql.fetch_rows(l_theCursor) > 0) loop /* Строим длинную строку результатов, — это эффективнее, чем * вызывать DBMS_OUTPUT.PUT_LINE в цикле. */ l_cnt := l_crit+l; l_line := l_cnt; /* Шаг 8 — получить и обработать данные столбцов. */ for i in 1 . . 1 colCnt loop dbms sql .column value(1 theCursor, i, 1 columnValue); 1 line := 1 line II',' I| 1 columnValue; end loop, /* Теперь выдаем строку. */ dbms_output.put_line(l_line); end loop;
Можно проходить по столбцам в цикле, как если бы они представляли собой массив. Представленная выше конструкция взята из следующего фрагмента кода: scott@TKYTE816> create or replace 2 procedure dump_query(p_query in varchar2) 3 is 4 l_columnValue varchar2(4000); 5 l_status integer; 6 l_colCnt number default 0; 7 l_cnt number default 0; 8 l_line long; 9 10 /* Мы будем использовать эту таблицу, чтобы узнать, 11 * сколько столбцов придется выбирать, чтобы определить их, 12 * а затем выбрать их значения. 13 */ 14 1 descTbl dbms sql.desc tab; 15 " 16 17 /* Шаг 1: открыть курсор. */ 18 l_theCursor integer default dbms_sql.open_cursor; 19 begin 20
Динамический SQL 2 0 7 21 /* Шаг 2: проанализировать запрос, чтобы можно было получить *• описание его результатов. */ 22 dbms_sql.parse(l_theCursor, p_query, dbms_sql.native); 23 24 /* Шаг З: получаем описание результатов запроса. */ 25 dbms_sql.describe_columns(l_theCursor, l_colCnt, l_descTbl); 26 27 /* Шаг 4 в этом примере не используется, потому что связывать *•* ничего не нужно. 28 * Шаг 5: необходимо определить каждый столбец, сообщить серверу, 29 * что и куда мы будем выбирать. В данном случае все данные 30 * будут выбираться в одну локальную переменную типа * varchar2(4000). 31 */ 32 for i in 1 .. l_colCnt 33 loop 34 dbms_sql.define_column(l_theCursor, i, l_columnValue, 4000); 35 end loop; 36 37 /* Шаг 6: выполнить оператор. */ 38 l_status := dbms_sql.execute(l_theCursor); 39 40 /* Шаг 7: выбрать все строки. */ 41 while (dbms_sql.fetch_rows(l_theCursor) > 0) 42 loop 43 /* Строим длинную строку результатов — это эффективнее, чем 44 * вызывать DBMS_OUTPUT.PUT_LINE в цикле. 45 */ 46 l_cnt := l_cnt+l; 47 l_line := l_cnt; 48 /* Шаг 8: получаем и обрабатываем данные столбцов. */ 49 for i in 1 .. l_colCnt loop 50 dbms_sql.column_value(l_theCursor, i, l_columnValue); 51 l_line := l_line || ',' || l_columnValue; 52 end loop; 53 54 /* Теперь выдаем строку. */ 55 dbms_output.put_line (l_line) ; 56 end loop; 57 58 /* Step 9: закрываем курсор, чтобы освободить ресурсы. */ 59 dbms_sql.close_cursor(l_theCursor); 60 exception 61 when others then 62 dbms_sql.close_cursor(l_theCursor); 63 raise; 64 end dump_query; 65 / Procedure created.
Из этого следует, что пакет DBMS_SQL позволяет с помощью процедуры DBMS_SQL.DESCRIBE_COLUMNS получить количество, имена и типы данных стол-
208
Глава 16
бцов в запросе. В качестве примера ее использования рассмотрим обобщенную процедуру сброса результатов запроса в файл операционной системы. Она отличается от SQL-Unloader, рассмотренной в главе 9 при создании средств выгрузки данных. В этом примере данные сбрасываются в файл в виде записей фиксированной длины, в которых столбцы всегда начинаются с одной и той же позиции. Для этого анализируются результаты вызова DBMS_SQL.DESCRIBE_COLUMNS, в которых помимо количества выбираемых столбцов можно найти и их максимальный размер. Прежде чем рассмотреть пример полностью, давайте подробней разберемся с процедурой DESCRIBE_COLUMNS. После анализа запроса с помощью этой процедуры можно обратиться к серверу за информацией о том, что можно ожидать при выборке данных по этому запросу. Эта процедура создает массив записей с информацией об именах, типах данных, максимальном размере столбцов и т.п. Вот пример использования процедуры DESCRIBE_COLUMNS. Выдаются данные, возвращаемые ею для запроса; благодаря этому можно узнать, какая информация доступна: scott@TKYTE816> c r e a t e or replace 2 procedure desc_query(p_query in varchar2) 3 is 4 l__columnValue varchar2(4000); 5 l_status integer; 6 l_colCnt number default 0; 7 l_cnt number default 0; 8 l_line long; 9 10 /* Мы используем эту таблицу, чтобы узнать, какие данные *•* выбирает запрос 11 */ 12 l_descTbl dbms_sql.desc_tab; 13 14 15 /* Шаг 1: открыть курсор. */ 16 l_theCursor integer default dbms_sql.open_cursor; 17 begin 18 19 /* Шаг 2: проанализировать входной запрос, чтобы можно было ^ описать его результаты. */ 20 dbms_sql.parse(l_theCursor, p_query, dbms_sql.native); 21 22 /* Шаг З: описываем результаты запроса. 23 * Переменная L_COLCNT будет содержать количество выбранных в 24 * запросе столбцов. Оно будет равно L_DESCTBL.COUNT; 25 * эта переменная содержит избыточную информацию. Переменная 26 * L_DESCTBL содержит полезные сведения о выбранных столбцах. 27 */ 28 29 dbms_sql.describe_columns(c => l_theCursor, 30 col_cnt => l_colCnt, 31 desc_t => l_descTbl); 32
Динамический SQL 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
209
for i in 1 . . l_colCnt loop dbms_output.put_line ('Column Type ' II l_descTbl(i).col_type); dbms_output.put_line ('Max Length ' | | l_descTbl(i) .col_max_len) ; dbms_output. put_l ine ('Name ' || l_descTbl (i) . col_name) ; dbms_output.put_line ('Name Length ' II l_descTbl(i) .col_name_len) ; dbms_output.put_line ('ObjColumn Schema Name.' II l_descTbl(i).col_schema_name); dbms_output.put_line ('Schema Name Length....' |I l_descTbl(i).col_schema_name_len) ; dbms_output.put_line ('Precision ' | | l_descTbl(i) . col_j?recision) ; dbms_output.put_line ('Scale ' II l_descTbl(i) .col_scale) ; dbms_output.put_line ('Charsetid ' II l_descTbl(i) .col_Charsetid) ; dbms_output. put_line ( 'Charset Form ' II l_descTbl(i) . col_charsetf orm) ; if (l_desctbl(i).col_null_ok) then dbms_output.put_line( 'Nullable У ); else dbms_output.put_line( 'Nullable N') ; end if; dbms_output .put_line (' ') ; end loop; /* Шаг 9: закрыть курсор и освободить ресурсы. */ dbms_sql.close_cursor(l_theCursor); exception when others then dbms_sql.close_cursor(l_theCursor); raise; end desc_query;
Procedure created. scott@TKYTE816> set serveroutput on scott@TKYTE816> exec desc_query('select rowid, ename from emp'); Column Type 11 Max Length 16 Name ROWID Name Length 5 ObjColumn Schema Name. Schema Name Length....0 Precision 0 Scale 0 Charsetid 0
210
Глава 16
Charset Form Nullable
0 Y
Column Type 1 Max Length 10 Name ENAME Name Length 5 ObjColumn Schema Name. Schema Name Length.... 0 Precision 0 Scale 0 Charsetid 31 Charset Form 1 Nullable Y PL/SQL procedure successfully completed. К сожалению, значение COLUMN TYPE — число, а не имя типа данных, так что если не знать, что значение 11 соответствует типу ROWID, а значение 1 — типу VARCHAR2, расшифровать эти результаты не удастся. В руководстве Oracle Call Interface Programmer's Guide представлен полный список внутренних кодов типов данных и соответствующих имен типов. Этот список воспроизведен ниже. Имя типа данных
Код
VARCHAR2, NVARCHAR2
1
NUMBER
2
LONG
8
ROWID
11
DATE
12
RAW
23
LONG RAW
24
CHAR, NCHAR
96
Пользовательский тип (объектный тип, VARRAY, вложенная таблица)
108
REF
111
CLOB, NCLOB
112
BLOB
113
BFILE
114
UROWID
208
Теперь мы готовы рассмотреть всю подпрофамму, которая может принять практически любой запрос и сбросить результаты его выполнения в файл операционной системы (предполагается, что пакет UTL_FILE настроен; эта настройка подробно описана в приложении А):
Динамический SQL
21
scott@TKYTE816> create or replace 2 function dump_fixed_width(p_query in varchar2, 3 p_dir in varchar2, 4 p_filename in varchar2) 5 return number 6 is 7 l_output utl_file.file_type; 8 l_theCursor integer default dbms_sql.open_cursor; 9 l_columnValue varchar2(4000); 10 l_status integer; 11 l__colCnt number default 0; 12 l_cnt number default 0; 13 l_line long; 14 l_descTbl dbms_sql.desc_tab; 15 l_dateformat nls_session_parameters.value%type; 16 begin 17 select value into l_dateformat 18 from nls_session_parameters 19 where parameter = 'NLS_DATE_FORMAT'; 20 21 /* Используем формат даты, включающий время. */ 22 execute immediate 23 'alter session set nls_date_format=''dd-mon-yyyy hh24:mi:ss'' '; 24 l_output := utl_file.fopen(p_dir, p_filename, 'w1, 32000); 25 26 /* Анализируем входной запрос, чтобы можно было получить его ^ описание. */ 27 dbms_sql.parse(l_theCursor, p_query, dbms_sql.native); 28 29 /* Теперь получаем описание результатов запроса. */ 30 dbms_sql.describe_columns(l_theCursor, l_colCnt, l_descTbl); 31 32 /* Необходимо определить каждый столбец и указать серверу, 33 * что и куда мы будем выбирать. В данном случае, все данные 34 * будут выбираться в одну переменную типа varchar2(4000). 35 * 36 * Мы также определим максимальный размер каждого столбца. Это 37 * делается для того, чтобы при выдаче данных каждое поле 38 * начиналось и заканчивалось в одной и той же позиции в каждой w записи. 39 */ 40 for i in 1 .. l_colCnt loop 41 dbms_sql.define_column(l_theCursor, i, l_columnValue, 4000); 42 43 if (l_descTbl(i).col_type = 2) /* тип number */ 44 then 45 L_descTbl(i).col_max_len := l_descTbl(i).col_precision+2; 46 e l s i f (l_descTbl(i).col_type = 12) /* тип date */ 47 then 48 /* длина заданного выше формата даты */ 49 l_descTbl(i).col_max_len := 20;
212
Глава 16
50 end if; 51 end loop; 52 53 l_status := dbms_sql.execute(l_theCursor); 54 55 while (dbms_sql.fetch_rows(l_theCursor) > 0) 56 loop 57 /* Строим большую строку результата. Это более эффективно, 58 * чем вызывать процедуру UTL__FILE.PUT в цикле. 59 */ 60 l_line := null; 61 for i in 1 .. l_colCnt loop 62 dbms_sql.column_value(l_theCursor, i, l_columnValue); 63 l_line := l_line || 64 rpad(nvl(l_columnValue,' ' ) , 65 l_descTbl(i).col_max_len); 66 end loop; 67 68 /* Теперь выдаем строку в файл и увеличиваем значение ^* счетчика. */ 69 utl_file.put_line(l_output, l_line); 70 l_cnt := l_cnt+l; 71 end loop; 72 73 /* Освобождаем ресурсы. */ 74 dbms_sql.close_cursor(l_theCursor); 75 utl_file.fclose(l_output); 76 77 /* Восстанавливаем формат даты ... и завершаем работу. */ 78 execute immediate 79 'alter session set nls_date_format=''' || l_dateformat II '"' '; 80 return l_cnt; 81 exception 82 when others then 83 dbms_sql.close_cursor(l_theCursor); 84 execute immediate 85 ' a l t e r session set nls_date_f ormat=' " 11 | l_datef ormat | | ' ' ' ' ; 86 87 end dump_fixed_width; 88
/
Function created. Итак, эта функция использует подпрограмму DBMS_SQL.DESCRIBE_COLUMNS для поиска количества столбцов и их типов данных. Я изменил некоторые значения максимальных размеров, чтобы учесть используемый формат даты, а также десятичную запятую и знак в числах. Представленная выше подпрограмма не может выгрузить данные типа LONG, LONG RAW, CLOB и BLOB. Ее легко изменить для поддержки данных типа CLOB и даже LONG. Придется специальным образом выполнять связывание переменных этих типов, а также использовать пакет DBMS_CLOB для выборки данных типа CLOB и подпрограмму DBMS_SQL.COLUMN_VALUE_LONG — для данных типа
Динамический SQL
213
LONG. Следует заметить, что добиться этого с помощью встроенного динамического SQL невозможно — его нельзя использовать, если список выбора в PL/SQL не известен.
Многократное выполнение одного и того же оператора В данном случае придется выбирать между средствами пакета DBMS_SQL и встроенным динамическим SQL. За счет большего объема и сложности кода можно достичь более высокой производительности. Чтобы продемонстрировать это, я создам подпрограмму, динамически вставляющую в таблицу большое количество строк. В ней используется динамический SQL, поскольку до начала выполнения имя таблицы, куда будут вставляться данные, неизвестно. Для сравнения создадим четыре аналогичных подпрограммы: Подпрограмма
Назначение
DBMSSQL_ARRAY
Использует обработку массивов в PL/SQL для множественной вставки строк
NATIVE_DYNAMIC_ARRAY
Использует эмуляцию обработки массивов с помощью таблиц объектного типа
DBMSSQL_NOARRAY
Выполняет построчную обработку при вставке строк
NAT!VE__DYNAMIC_NOARRAY
Выполняет построчную обработку при вставке строк
Первый метод (используемый в подпрограмме DBMSSQL_ARRAY) наиболее масштабируем и обеспечивает наибольшую производительность. В моих тестах на различных платформах первый и второй методы были очень близки по результатам в однопользовательской среде: если на машине не работают другие пользователи, они более-менее сопоставимы. На некоторых платформах встроенный динамический SQL работал немного быстрее, на других — пакет DBMS_SQL. В многопользовательской среде, однако, из-за повторного полного анализа запроса при каждом выполнении во встроенном динамическом SQL, подход с использованием обработки массивов средствами пакета DBMS_SQL обеспечивал лучшую масштабируемость. При этом не нужно было выполнять частичный разбор запроса при каждом выполнении. Необходимо также учесть, что для эмуляции обработки массивов во встроенном динамическом SQL пришлось применить трюк. Так что код в обоих случаях оказался достаточно сложным. Обычно код, где используется встроенный динамический SQL, намного проще, чем код с вызовами DBMS_SQL, но не в этом случае. Единственный определенный вывод, который можно сделать, — третий и четвертый методы намного медленнее первых двух. Следующие результаты были получены на платформе Solaris для одного пользователя, но результаты на платформе Windows были аналогичными. Выполните тесты на своей платформе, чтобы получить наиболее достоверные результаты. scott@TKYTE816> create or replace type vcArray as table of varchar2(400) 2 / Type created. scott@TKYTE816> create or replace type dtArray as table of date 2 /
214
Глава 16
Type created. scott@TKYTE816> create or replace type nmArray as table of number 2 / Type created.
Эти типы необходимы для эмуляции обработки массивов с помощью встроенного динамического SQL. Массивы именно этих типов мы и будем использовать (во встроенном динамическом SQL вообще нельзя использовать PL/SQL-таблицы). Теперь представим спецификацию пакета, который будет использоваться для тестов: scott@TKYTE816> create or replace package load_data 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
as procedure dbmssql_array(p_tname in varchar2, p_arraysize in number default 100, p_rows in number default 500); procedure dbmssql_noarray(p_tname p_rows
in varchar2, in number default 500);
procedure native_dynamic_noarray(p_tname p_rows
in varchar2, in number default 500);
procedure native_dynamic_array(p_tname in varchar2, p_arraysize in number default 100, p_rows in number default 500); end load_data; /
Package created.
Каждая из представленных выше процедур будет динамически вставлять строки в таблицу, заданную параметром P_TNAME. Количество вставляемых строк определяет параметр P_ROWS; при использовании обработки массивов их размер задается параметром P_ARRAYSIZE. Теперь переходим к реализации: scott@TKYTE816> create or replace package body load_data 2 as 3 4 procedure dbmssql_array(p_tname in varchar2, 5 p_arraysize in number default 100, 6 p_rows in number default 500) 7 is 8 l_stmt long; 9 l_theCursor integer; 10 l_status number; 11 l_coll dbms_sql.number_table; 12 I_col2 dbms_sql.date_table; 13 I_col3 dbms_sql.varchar2_table; 14 l_cnt number default 0; 15 begin
Динамический SQL 16 17
1 stmt := 'insert into ' || p tname || 1 ql (a, b, c) values (:a, :b,
215
:c)';
1Р
хо 1 theCursor := dbms_sql.open_cursor; 19 dbms sql.parse(l theCursor, 1 stmt, dbms sql.native); 20 21 22 * Здесь мы будем формировать данные. После формирования 23 * ARRAYSIZE строк, мы вставляем их все сразу. В конце 24 * цикла, если еще остались строки, мы их тоже вставляем. 25 */ 26 for i in 1 .. р rows 27 loop 28 l_cnt := l_cnt+l; 29 l_coll(l_cnt) := i; 30 I_col2(l cnt) := sysdate+i; 1 col3(l cnt) := to char(i); 31 32 33 if (1 cnt = p arraysize) 34 then 35 dbms sql.bind array(l theCursor, ':a', 1 coll, 1, 1 _cnt); dbms sql.bind array(l theCursor, ':b', 1 col2, 1, 1 cnt); 36 37 dbms sql.bind array(l theCursor, ':c', 1 col3, 1> !. cnt); 38 l_status := dbms_sql.execute(l_theCursor); 39 1 cnt := 0; 40 end if; 41 end loop; 42 if (l_cnt > 0) 43 then 44 dbms_sql.bind array(l_theCursor, ':a', 1 coll, 1, 1 _cnt); 45 dbms sql.bind array(l theCursor, ':b', 1 col2, 1, 1 _cnt) ; 46 dbms_sql.bind array(l theCursor, ':c', 1 col3, !' !..cnt); 47 1 status := dbms sql.execute(1 theCursor); 48 end if; 49 dbms sql.close cursor(1 theCursor); 50 end; 51
7*
Итак, в этой подпрограмме используются средства пакета DBMS_SQL для вставки массива из N строк с помощью одной операции. Мы используем перегруженную подпрограмму BIND_VARIABLE, позволяющую пересылать PL/SQL-таблицу соответствующего типа с загружаемыми данными. Мы также указываем границы массива, чтобы сервер Oracle "знал", где начинается и заканчивается блок данных в переданной PL/SQLтаблице. В данном случае всегда следует начинать с индекса 1 и заканчивать индексом L_CNT. Обратите внимание, что для имени таблицы в операторе INSERT задан псевдоним (коррелирующее имя) Q1. Я сделал это для того, чтобы при анализе производительности с помощью утилиты TKPROF можно было определить, какие операторы INSERT использовались той или иной подпрограммой. Вообще, код получается довольно простым. Теперь представим реализацию на базе пакета DBMS_SQL без обработки массивов:
216 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
Глава 16 procedure dbmssql_noarray(p_tname in varchar2, p_rows in number default 500) is l_stmt long; l_theCursor integer; l_status number; begin l_stmt := 'insert into ' I| p_tname || ' q2 (a, b, c) values (:a, :b, : c ) ' ; l_theCursor := dbms_sql.open_cursor; dbms_sql.parse(l_theCursor, l_stmt, dbms_sql.native); /* * Здесь мы будем формировать данные. * Каждая строка вставляется отдельным оператором * в цикле. */ for i in 1 .. p_rows loop dbms_sql.bind_variable(l_theCursor, ':a', i ) ; dbms_sql.bind_variable(l_theCursor, ':b', sysdate+i); dbms_sql.bind_variable(l_theCursor, ':c', to_char(i)); l_status := dbms_sql.execute(l_theCursor); end loop; dbms_sql.close_cursor (l_theCursor) ; end;
Эта подпрограмма отличается от предыдущей только тем, что не формируются массивы. Если вы пишете код, подобный этому, советую прибегнуть к обработке массивов. Как вскоре будет показано, это может существенно повысить производительность приложения. Теперь переходим к подпрограмме, использующей встроенный динамический SQL: 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
procedure native_dynamic_noarray(p_tname in varchar2, p_rows in number default 500) is begin /* * Здесь мы формируем строку и вставляем ее. * Что может быть проще для написания и выполнения... */ for i in 1 .. p_rows loop execute immediate 'insert into ' I I p_tname || ' q3 (a, b, c) values (:a, :b, :c)' using i, sysdate+i, to_char(i); end loop; end;
Динамический SQL
217
В этой подпрограмме массивы не обрабатываются. Простенькая программа; ее легко создать, но вот производительность будет очень низкой из-за постоянно выполняемых разборов оператора. Наконец, пример эмуляции вставки массивов с помощью встроенного динамического SQL: 96 procedure native_dynamic_array(p_tname in varchar2, 97 p_arraysize in number default 100, 98 p_rows in number default 500) 99 is 100 l_stmt long; 101 l_theCursor integer; 102 l_status number; 103 l_coll nmArray := nmArrayO; 104 I_col2 dtArray := dtArrayO; 105 I_col3 vcArray := vcArrayO; 106 l_cnt number := 0; 107 begin 108 /* 109 * Здесь мы будем формировать данные. После формирования 110 * ARRAYSIZE строк мы вставляем их все сразу. В конце цикла, 111 * если еще остались строки, мы их тоже вставляем. 112 */ 113 l_coll.extend(p_arraysize) ; 114 1_со12.extend(p_arraysize); 115 1_со13.extend(p_arraysize); 116 for i in 1 .. p_rows 117 loop 118 l_cnt := l_cnt+l; 119 l_coll(l_cnt) := i; 120 I_col2(l_cnt) := sysdate+i; 121 l_col3(l_cnt) := to_char(i); 122 123 if (l_cnt = p_arraysize) 124 then 125 execute immediate 126 'begin 127 forall i in 1 .. :n 128 insert into ' || p_tname || ' 129 q4 (a, b, c) values (:a(i), :b(i), :c(i)); 130 end;' 131 USING l_cnt, l_coll, I_col2, I_col3; 132 l_cnt := 0; 133 end if; 134 end loop; 135 if (l_cnt > 0) 136 then 137 execute immediate 138 'begin 139 forall i in 1 .. :n 140 insert into ' || p_tname || ' 141 q4 (a, b, c) values (:a(i), :b(i), :c(i));
2,1 О 142 143 144 145 146 147 148
Глава 16 end;' USING l_cnt, l _ c o l l , I_col2, I_col3; end
if;
end; end load_data; /
Package body created. Как видите, тут все немного запутано. Наша подпрограмма создает код, который будет динамически выполняться. В этом динамически формируемом коде используется оператор FORALL для множественной вставки строк из массивов. Поскольку в операторе EXECUTE IMMEDIATE можно использовать только типы данных SQL, пришлось заранее создать в базе данных соответствующие типы. После этого необходимо динамически выполнять оператор: begin f o r a l l i i n 1 .. :n insert into t (a,b,c) end;
values
(:a(I),
:b(I),
:c(I));
подставляя в него количество вставляемых строк и три массива значений. Как будет показано ниже, обработка массивов ускоряет вставку во много раз. Необходимо решить, стоит ли это ускорение того, чтобы отказаться от простоты написания подпрограммы с помощью встроенного динамического SQL при отсутствии массивов. Конечно, трудно что-то противопоставить одной строке кода! Если речь идет о программе одноразового использования, для которой производительность несущественна, я бы выбрал самый простой способ. Если речь идет о многократно используемой подпрограмме, которую будут использовать достаточно долго, я бы выбрал пакет DBMS_SQL, если скорость работы имеет значение и количество связываемых переменных заранее не известно, и — встроенный динамический SQL, если производительность приемлема, а количество связываемых переменных хорошо известно. Наконец, не стоит забывать о результатах, представленных в главе 10, — там было показано, что желательно сокращать количество частичных разборов. Пакет DBMS_ SQL позволяет это сделать, а встроенный динамический SQL — нет. Необходимо хорошо представлять себе, что именно надо сделать, и выбирать соответствующий подход. Если пишется программа загрузки данных, которую запускают раз в день, и при этом запросы анализируются всего несколько сотен раз, встроенный динамический SQL прекрасно подходит. С другой стороны, если пишется подпрограмма, использующая один и тот же динамический SQL-оператор десятки раз и выполняемая одновременно десятками пользователей, имеет смысл использовать средства пакета DBMS_SQL, чтобы можно было проанализировать запрос один раз, а затем только выполнять. Я выполнил представленные ранее подпрограммы с помощью следующего тестового кода (помните, мы работаем в однопользовательской системе!): create t a b l e t (a i n t , b date, с varchar2(15)); a l t e r session set sql_trace=true; truncate table t ;
Динамический SQL
2>\j7
exec load_data.dbmssql_array('t', 50, 10000); truncate table t ; exec load_data.native_dynamic_array('t', 50, 10000); truncate table t ; exec load_data.dbmssql_noarray('t', 10000) truncate table t ; exec load_data.native_dynamic_noarray('t', 10000) В отчете TKPROF можно обнаружить следующее: BEGIN load data.dbmssql a r r a y C t ' , 50, 10000); END; call
count
cpu
elapsed
disk
query
Parse Execute Fetch
1 0.01 1 2 . 5 8 0 0.00
0.00 2.83 0.00
0 0 0
0 0 0
0 0 0
0 1 0
total
2
2.83
0
0
0
1
2.59
current
rows
BEGIN load_data.native_dynamic_array('t', 50, 10000); END; call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 1 0
0.00 2.39 0.00
0.00 2.63 0.00
0 0 0
0 0 0
0 0 0
0 1 0
total
2
2.39
2.63
0
0
0
1
Общие профили выполнения очень близки: 2,59 и 2,30 секунд процессорного времени. Различие — в деталях. Если вы обратили внимание, в представленном ранее коде я сделал каждый оператор вставки уникальным, добавив псевдонимы таблиц Q l , Q2, Q3 и Q4. Благодаря этому можно определить, сколько раз анализировался каждый оператор. В подпрограмме на основе пакета DBMS_SQL и массивов использовался псевдоним Q1, а в подпрофамме со встроенным динамическим SQL — псевдоним Q4. Получены следующие результаты: i n s e r t i n t o t q l (a, b, c) values (:a, call Parse
:b, :c)
count
cpu
elapsed
disk
query
current
rows
1
0.00
0.01
0
0
0
0
и: begin f o r a l l i in 1 . . :n i n s e r t i n t o t q4 (a, b, c) values ( : a ( i ) , end; call Parse
:b(i),
count
cpu
elapsed
disk
query
200
0.10
0.07
0
0
:c(i));
current 0
rows 0
220
Глава 16
INSERT INTO T Q4 (A,B,C) VALUES ( : Ы , :Ь2,:ЬЗ) call Parse
count 200
cpu 0.07
elapsed
disk
query
current
rows
0.04
0
0
0
0
Как видите, подпрограмме, использующей средства пакета DBMS_SQL, хватило всего одного разбора, а вот при использовании встроенного динамического SQL анализировать операторы пришлось 400 раз. В загруженной системе, где одновременно работает множество пользователей, это может существенно снизить производительность. Поскольку избежать избыточных разборов можно и соответствующий код с использованием средств пакета DBMS_SQL не намного сложнее, я считаю оптимальным при решении подобного рода задач использовать DBMS_SQL. Хотя код и сложнее, но для обеспечения более высокой масштабируемости я бы использовал именно его. Если сравнить результаты процедур, не обрабатывающих массивы, оказывается, что они существенно хуже: BEGIN load_data.dbmssql_noarray('t\
10000); END;
call
disk
query
current
count
cpu
elapsed
Parse Execute Fetch
1 1 0
0.00 7.66 0.00
0.00 7.68 0.00
0 0 0
0 0 0
0 0 0
total
2
7.66
7.68
0
0
0
BEGIN load_data.native_dynamic_noarray('t', call
count
cpu
elapsed
Parse Execute Fetch
1 1 0
0.00 6.15 0.00
0.00 6.25 0.00
total
2
6.15
6.25
0 1 0
10000); END;
disk
query
current
rcws
0 0 0
0 0 0
0 0 0
0 1 0
0
0
0
1
Несомненно, имеет смысл использовать встроенный динамический SQL. Но и без массивов я все равно использовал бы средства пакета DBMS_SQL. И вот почему: i n s e r t into t q2 (a, b, c) values (:a, :b, :c) call Parse
count
cpu
elapsed
disk
query
1
0.00
0.00
0
0
current
rows 0
i n s e r t i n t o t q3 (a, b , c) values (:a, :b, :c) call
count
Parse
10000
cpu 1.87
elapsed
disk
1.84
0
query
current
rows
Оказывается, что при использовании встроенного динамического SQL пришлось выполнить 10000 частичных разборов, и лишь один — при использовании пакета DBMS_SQL. В многопользовательской среде реализация на основе пакета DBMS_ SQL окажется намного более масштабируемой.
Динамический SQL
2 2 1
Аналогичные результаты можно получить и при обработке множества строк, выдаваемых по динамически формируемому запросу. Обычно данные можно выбирать из курсорных переменных массивами, но только из строго типизированных. Это такие курсорные переменные, структура которых известна на этапе компиляции. Встроенный динамический SQL поддерживает использование только слабо типизированных курсорных переменных и поэтому не поддерживает множественную выборку, BULK COLLECT. Если попытаться выполнить оператор BULK COLLECT для динамически открытой курсорной переменной, будет получено сообщение об ошибке: ORA-01001: Invalid Cursor Вот сравнение двух процедур, выбирающих и подсчитывающих все строки из представления ALL_OBJECTS. Процедура, использующая средства пакета DBMS_SQL и обрабатывающая массивы, работает почти вдвое быстрее: scott@TKYTE816> create or replace procedure native_dynamic_select 2 as 3 type re is ref cursor; 4 l_cursor re; 5 l_oname varchar2(255); 6 l_cnt number := 0; 7 l_start number default dbms_utility.get_time; 8 begin 9 open l_cursor for 'select object_name from all_objects'; 10 11 loop 12 fetch l_cursor into l_oname; 13 exit when l_cursor%notfound; 14 l_cnt := l_cnt+l; 15 end loop; 16 17 close l_cursor; 18 dbms_output.put_line(L_cnt || ' rows processed'); 19 dbms_output. put_line 20 (round((dbms_utility.get_time-l_start)/100, 2) || ' seconds'); 21 exception 22 when others then 23 if (l_cursor%lsopen) 24 then 25 close l_cursor; 26 end if; 27 raise; 28 end; 29 / Procedure created. scott@TKYTE816> create or replace procedure dbms_sql_select 2 as 3 l_theCursor integer default dbms_sql.open_cursor; 4 l_columnValue dbms_sql.varchar2_table; 5 l_status integer; 6 1 cnt number := 0;
222 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Глава 16 l_start begin
number default dbms_utility.get_time;
dbms_sql.parse(l_theCursor, 'select object_name from all_objects', dbms_sql.native); dbms_sql.define_array(l_theCursor, 1, l_columnValue, 100, 1 ) ; l_status := dbms_sql.execute(l_theCursor); loop l_status := dbms_sql.fetch_rows(l_theCursor); dbms_sql.column_value(l_theCursor,1,l_columnValue); l_cnt := l_status+l_cnt; exit when l_status <> 100; end loop; dbms_sql.close_cursor(l_theCursor); dbms_output.put_line(L_cnt || ' rows processed'); dbms_output.put_line (round((dbms_utility.get_time-l_start)/100, 2 ) || ' seconds'); exception when others then dbms_sql.close_cursor(l_theCursor); raise; end; /
Procedure created. scott@TKYTE816> set serveroutput on scott@TKYTE816> exec native_dynamic_select 19695 rows processed 1.85 seconds PL/SQL procedure successfully completed. scott@TKYTE816> exec native_dynamic_select 19695 rows processed 1.86 seconds PL/SQL procedure successfully completed. scott@TKYTE816> exec dbms_sql_select 19695 rows processed 1.03 seconds PL/SQL procedure successfully completed. scott@TKYTE816> exec dbms_sql_select 19695 rows processed 1.07 seconds PL/SQL procedure successfully completed.
Снова приходится выбирать между производительностью и простотой кода. Для обработки массивов средствами пакета D B M S _ S Q L необходимо написать намного боль-
Динамический SQL 2 2 3 ше кода, чем при использовании встроенного динамического SQL, но производительность при этом существенно возрастает.
Проблемы Как и в случае любого средства, при использовании динамического SQL есть ряд нюансов, которые необходимо учитывать. В этом разделе мы рассмотрим их последовательно. При использовании динамического SQL в хранимых процедурах возникает три основных проблемы: •
нарушается цепочка зависимостей;
Q код становится более хрупким ; •
настройка, обеспечивающая предсказуемое время выполнения, существенно усложняется.
Нарушение цепочки зависимостей Обычно при компиляции процедуры в базе данных все объекты, на которые она ссылается, а также все объекты, ссылающиеся на нее, регистрируются в словаре данных. Например, я создам функцию: ops$tkyte@DEV816> c r e a t e o r replace function count_emp r e t u r n number 2 as 3 4
l_cnt number; begin
5 6 7 end; 8 /
select count(*) into l_cnt from emp; return 1 cnt;
Function created. ops$tkyte@DEV816> select referenced_name, referenced_type 2 from user_dependencies 3 where name = 'COUNT_EMP' 4 5
a n d t y p e = 'FUNCTION' /
REFERENCED_NAME
REFERENCED^
STANDARD SYS_STUB_FOR_PURITY_ANALYSIS EMP 3 rows selected.
PACKAGE PACKAGE TABLE
Сравним это с тем, что зарегистрировано при последнем создании использующей встроенный динамический SQL функции GET_ROW_CNTS; ops$tkyte@DEV816> s e l e c t referenced_name, 2 from user_dependencies 3 where name = 'GET ROW CNTS'
referenced_type
224
Глава 16 4 5
a n d t y p e = 'FUNCTION 1 /
REFERENCED_NAME STANDARD SYS_STUB_FOR_PURITY_ANALYSIS 2 rows selected.
REFERENCEDJT PACKAGE PACKAGE
Для функции, статически ссылающейся на таблицу ЕМР, эта ссылка зарегистрирована в таблице зависимостей. Для функции же с динамическим SQL — нет, поскольку она не зависит от таблицы ЕМР. В данном случае это вообще не проблема, поскольку использование динамического SQL дает другое существенное преимущество — возможность определить количество строк в таблице. Ранее, однако, мы иногда использовали динамический SQL без особой нужды, и эта нарушенная цепочка зависимостей — очень плохой побочный эффект. Необходимо знать, на какие объекты ссылаются процедуры и где они используются. При использовании динамического SQL такие взаимосвязи не отслеживаются.
"Хрупкость" кода При использовании только статического SQL можно быть уверенным, что если уж программа успешно скомпилирована, во встроенных в нее SQL-операторах нет синтаксических ошибок, т.к. при компиляции все проверяется. При использовании динамического SQL его корректность будет определена только на этапе выполнения. Более того, поскольку SQL-операторы создаются динамически, необходимо проверять возможные ветвления кода, чтобы убедиться в корректности генерируемого SQL-кода. Если при определенных входных данных генерируется корректный SQL-оператор, это еще не означает, что все будет работать при любых входных данных. Это верно для любого кода, но использование динамического SQL делает код несколько уязвимым. Динамический SQL позволяет сделать многое, что по-другому сделать нельзя, но по возможности надо использовать статический SQL. Он выполняется быстрее, требует меньше ресурсов и менее уязвим.
Сложность настройки Это неочевидно, но приложение, динамически создающее запросы, настраивать сложно. Обычно можно получить полный список запросов, используемых приложением, выделить те из них, которые влияют на производительность, и бесконечно их настраивать. Если набор выполняемых приложением запросов не известен до того, как приложение начнет работать, нельзя точно узнать, какой будет его производительность. Предположим, создается хранимая процедура, динамически строящая запрос на основе введенных пользователем в Web-форме данных. Если не протестировать все возможные запросы, которые могут быть сгенерированы этой процедурой, нельзя понять, все ли необходимые индексы созданы и можно ли вообще считать настройку системы законченной. Даже при небольшом количестве столбцов (скажем, пяти) могут быть заданы десятки условий. Это не означает, что следует отказаться от использования динамичес-
Динамический SQL
225
кого SQL, но будьте готовы к такого рода проблемам, если выполняются запросы, о возможности генерирования которых системой вы даже не задумывались.
Резюме В этой главе мы детально изучили использование динамического SQL в хранимых процедурах; рассмотрели различия между его реализацией средствами встроенного динамического SQL и пакета DBMS_SQL; выяснили, когда использовать тот или иной подход. Оба подхода имеют свои преимущества и назначение. Динамический SQL позволяет создавать процедуры, реализовать которые иначе просто невозможно, — универсальные утилиты выгрузки и загрузки данных и т.п. На сайте издательства Apress вы можете найти программы, использующие динамический SQL, утилиту для загрузки файлов dBASE III в базу данных Oracle с помощью PL/SQL, сценарий для печати результатов выполнения запросов в SQL*Plus по столбцам (об этом мы поговорим подробно в главе 23), программы для транспонирования результирующих множеств и многое другое.
. 244
eP г о g j .
i a f iL»sionaI0 ( •• rnmingHrof ess,
lOracleProgra Professional I. eProgr
МШ
i •1.
шшшишшв
interMedia
interMedia — это набор средств, тесно интегрированных в СУБД Oracle и обеспечивающих загрузку в базу данных и безопасное управление разнородной информацией (rich content), а также доступ к ней в приложениях. Подобная информация широко используется в большинстве современных Web-приложений и включает текст, данные, изображения, аудио- и видеофайлы. Эта глава посвящена одному из моих любимых компонентов interMedia: interMedia Text. Я считаю, что технология interMedia Text обычно используется мало. Это происходит из-за недостаточного понимания ее сути и возможностей. Большинству специалистов известны только общие сведения о возможностях interMedia; еще они знают, как обеспечить поддержку работы с текстом для своих таблиц. При ближайшем же рассмотрении оказывается, что interMedia Text — замечательное и уникальное средство СУБД Oracle. После краткого обзора истории interMedia мы: Q обсудим использование компонента interMedia Text, в частности, для поиска текста, индексирования данных из множества различных источников и поиска в приложениях XML; • кратко рассмотрим, как реализованы соответствующие возможности в СУБД; Q рассмотрим ряд особенностей компонента interMedia Text: индексирование, использование оператора ABOUT и поиска в разделах.
2,2,0
Глава 17
Краткий исторический экскурс В ходе разработки большого проекта в 1992 году я впервые столкнулся с компонентом interMedia Text, или, как он тогда назывался, SQLTextRetrieval. В это время одной из моих задач была интеграция множества различных СУБД для создания большой распределенной сети баз данных. Одна из этих СУБД была настолько "закрытой", насколько это вообще возможно. Она не обеспечивала SQL-интерфейс для управления базой данных и доступа к текстовым данным. Мы должны были создать для нее SQL-интерфейс. Примерно в середине работы над проектом наш консультант по технологиям Oracle предоставил информацию о следующем поколении программного продукта Oracle SQLTextRetrieval, которое должно было называться TextServer3. Одно из преимуществ TextServer3 состояло в высокой степени оптимизации для работы в клиент/серверной среде. Кроме того, в составе TextServer3 предлагался несколько заумный интерфейс на языке С, но теперь я, по крайней мере, мог хранить все текстовые данные в базе данных Oracle и иметь при этом возможность обращаться к другим данным в той же базе данных с помощью SQL-операторов. Мне это понравилось. В 1996 году корпорация Oracle выпустила следующее поколение продукта TextServer под названием ConText Option, которое существенно отличалось от предыдущих версий. Не надо было больше хранить тексты и управлять ими через функциональный интерфейс на языке С или в среде Forms. Все можно было сделать в SQL. Компонент ConText Option предоставил множество PL/SQL-процедур и пакетов, позволяющих сохранять текст, создавать индексы, выполнять запросы, выполнять операции сопровождения для индексов и т.п., и для этого больше не требовалось писать ни одной строки кода на языке С. Среди многих преимуществ ConText Option, по моему мнению, два были наиболее существенными. Первое, и самое главное — ConText Option перестал быть лишь слабо интегрированным, периферийным компонентом СУБД. Он поставлялся в составе СУБД Oracle7 как отдельно лицензируемый необязательный компонент СУБД и был интегрирован в Oracle7. Второе преимущество состояло в том, что компонент ConText Option не только выполнял стандартную процедуру поиска текста, но и поддерживал лингвистический анализ текстов и документов, что позволило разработчикам создавать системы, "читающие между строк" и реально учитывающие общий смысл текстов. Не забывайте также, что все это было доступно через SQL, т.е. существенно упрощало использование этих развитых средств. Одним из существенных усовершенствований СУБД Oracle8i является стройная система расширения возможностей. На основе поддерживаемых служб разработчики получили средства создания специализированных, сложных типов данных, а также возможность организовывать собственные базовые службы СУБД для поддержки этих типов данных. В рамках этой системы расширения можно создавать новые типы индексов, использовать специализированные методы сбора статистической информации, а также интегрировать в СУБД Oracle специализированные функции оценки стоимости и избирательности методов доступа. На основе этой информации оптимизатор запросов может разумно и эффективно обращаться к новым типам данных. Создававшая ConText Option команда разработчиков оценила значимость этой системы расширения и занялась со-
interMedia
JLI/У
зданием современного продукта, interMedia Text, который впервые появился в составе Oracle8i в 1999 году.
Использование компонента interMedia Text Компонент interMedia Text можно использовать в приложениях для многих целей, в том числе: •
Поиск текста. Компонент interMedia Text позволяет быстро создать приложение, обеспечивающее эффективный поиск в текстовых данных.
•
Управление разнородными документами. Компонент interMedia Text позволяет создать приложение, обеспечивающее поиск по документам в различных форматах, в том числе в текстовых файлах, файлах Microsoft Word, Lotus 1-2-3 и Microsoft Excel.
•
Индексирование текста из различных источников данных. Компонент interMedia Text позволяет создать приложение, управляющее текстовыми данными не только в базе данных Oracle, но и в файловой системе и в сети Internet.
•
Создание приложений, "читающих между строк". Помимо обеспечения эффективного поиска слов и фраз, компонент interMedia Text позволяет построить "базу знаний" с краткими резюме по каждому документу или проклассифицировать документы по описываемым в них понятиям, а не просто по содержащимся словам.
•
Поиск в приложениях XML. Компонент interMedia Text предоставляет разработчикам приложений все необходимые средства для создания систем, не только запрашивающих содержимое XML-документов, но и позволяющих выполнять запросы к определенной структуре в XML-документе.
Наличие всех этих функциональных возможностей в СУБД Oracle позволяет при работе с текстовыми данными в полном объеме использовать присущую ей масштабируемость и защиту данных.
Поиск текста Разумеется, есть много способов поиска текста в базе данных Oracle и без использования компонента interMedia. В следующем примере мы создадим простую таблицу, вставим несколько строк, а затем воспользуемся стандартной функцией INSTR и оператором LIKE для поиска по текстовому столбцу таблицы: SQL> 2 3 4 5
create table mytext (id number primary key, thetext varchar2(4000) ) /
Table created. SQL> insert into mytext 2 values(1, 'The headquarters of Oracle Corporation is ' I|
230
Глава 17 3
'in Redwood Shores, California.');
1 row created. SQL> insert into mytext 2
values(2, 'Oracle has many training centers around the world. ' ) ;
1 row created. SQL> commit; Commit complete. SQI> select id 2 from mytext 3 where instr(thetext, 'Oracle') > 0; ID 1 2 SQL> select id 2 from mytext 3 where thetext like '%Oracle%'; ID 1 2 С помощью встроенной функции SQL INSTR можно искать вхождения подстроки в строке. С помощью оператора LIKE можно также искать строки, соответствующие шаблону. Во многих случаях функция INSTR или оператор LIKE идеально подходят для решения поставленной задачи, и все прочие средства просто избыточны, особенно при поиске в сравнительно небольших таблицах. Однако эти методы поиска текста обычно требуют полного просмотра таблицы и огромных ресурсов. Более того, функциональные возможности такого поиска весьма ограничены. Они не помогут, например, если необходимо создать приложение, поддерживающее следующие запросы: •
найти все строки, содержащие слово "Oracle" рядом со словом "Corporation" так, что их разделяет не более двух слов;
Q найти все строки, содержащие слово "Oracle" или слово "California", отсортировав результаты по релевантности; •
найти все строки со словами, имеющими корень "train" (например, trained, training, trains);
•
выполнить поиск строки в библиотеке документов независимо от регистра символов.
Эти запросы — лишь малая часть того, что нельзя сделать традиционными средствами, но легко делается с помощью компонента interMedia Text. Чтобы продемонстрировать, насколько легко interMedia позволяет отвечать на приведенные выше запросы, необходимо сначала создать индекс interMedia Text по нашему текстовому столбцу:
interMedia
231
Чтобы использовать PL/SQL-пакеты компонента interMedia Text, пользователю должна быть предоставлена роль СТХАРР. SQL> 2 3 4
create index mytext_idx on mytext(thetext) indextype is CTXSYS.CONTEXT /
Index created. Создав индекс нового типа, CTXSYS.CONTEXT, мы обеспечили возможность эффективного поиска текста для существующей таблицы. Теперь можно использовать множество операторов, поддерживаемых компонентом interMedia Text, для сложной обработки текстовых данных. Следующие примеры демонстрируют использование оператора CONTAINS для ответа на четыре представленных ранее запроса (не обращайте пока внимания на особенности синтаксиса SQL-операторов, поскольку он будет подробно рассмотрен далее): SQL> s e l e c t id 2 from mytext 3 where contains(thetext, 'near((Oracle,Corporation),10)') > 0; ID j
SQL> select score(l), id 2 from mytext 3 where contains(thetext, 'oracle or California', 1) > 0 4 order by score(l) desc 5 / SCORE(1)
ID
4 3
1 2
SQL> select id 2 from mytext 3 where contains(thetext, '$train') > 0; ID 2 Помимо функциональных возможностей индексы interMedia Text превосходят традиционные реляционные методы поиска текста в столбце и по производительности. Этот абсолютно новый тип индекса в базе данных Oracle предоставляет ценную информацию об индексированных данных, которую оптимизатор может использовать при создании плана выполнения запроса. Поэтому ядро СУБД Oracle получает оптимальный способ доступа к данным столбца, проиндексированным с помощью компонента interMedia Text.
232 Глава 17 Управление разнородными документами Помимо возможности индексировать текстовые столбцы в базе данных компонент interMedia Text включает набор фильтров документов для множества форматов. Компонент interMedia Text будет автоматически обрабатывать документы Microsoft Word 2000 для Windows, Microsoft Word 98 для Macintosh, электронные таблицы Lotus 1-2-3, документы в формате Adobe Acrobat PDF и даже файлы презентаций Microsoft PowerPoint. Всего в составе компонента interMedia Text поставляется более 150 фильтров для разных типов файлов и документов. Наличие тех или иных фильтров зависит от версии Oracle 8i. Например, версии Oracle 8.1.5 и Oracle 8.1.6 были выпущены раньше, чем появился формат Microsoft Word 2000 для Windows. Поэтому в составе компонента interMedia Text версии 8.1.5 или 8.1.6 нет фильтра для этого типа документов, а в составе interMedia Text в Oracle 8.1.7 — уже есть.
Технология фильтрования, включенная в состав interMedia Text, получена по лицензии от корпорации Inso Corporation и с точки зрения точности и эффективности я считаю ее лучшей из имеющихся на рынке. Список текущих поддерживаемых форматов файлов представлен в приложении к руководству Oracle8i interMedia Text Reference. На момент написания этой книги фильтры Inso не были доступны на платформе Linux, и перенос их на эту платформу не планировался, что очень печально. Это не означает, что компонент interMedia Text нельзя использовать на платформе Linux, но если используется версия Oracle 8i для Linux, придется либо ограничиться текстовыми и HTML-документами, либо создавать так называемые пользовательские фильтры, объекты USER_FILTER.
Индексирование текста из различных источников данных Компонент interMedia Text обеспечивает не только хранение файлов в базе данных. Источник данных (datastore object) interMedia Text позволяет указать, где именно должен храниться текст или данные. Источник данных обеспечивает необходимую для индекса interMedia Text информацию о том, где находятся данные. Эту информацию можно задать только при создании индекса. Как было показано в предыдущем примере, данные для индекса interMedia Text могут поступать непосредственно из базы данных — храниться в столбце таблицы. Этот источник данных, DIRECT_DATASTORE, является стандартным, если явно не указан другой источник. Столбец может быть типа CHAR, VARCHAR, VARCHAR2, BLOB, CLOB или BFILE. Можно создать индекс interMedia Text и по столбцу типа LONG или LONG RAW, но с момента выхода версии Oracle 8 эти типы считаются устаревшими, и их не стоит использовать во вновь создаваемых приложениях. Еще один полезный тип источника данных для текста, хранящегося в столбцах таблиц базы данных, — DETAIL_DATASTORE. Отношение главный/подчиненный часто
interMedia
233
встречается в приложениях. Это отношение задает связь между строкой в главной (родительской) таблице и нулем или более строк в подчиненной таблице и реализуется с помощью ограничения внешнего ключа в подчиненной таблице, ссылающегося на главную. Счет-фактура — хороший пример отношения главный/подчиненный: обычно одному счету-фактуре соответствует ноль или более строк в подчиненной таблице, описывающей купленные товары. Источник данных типа DETAIL_DATASTORE позволяет разработчику учесть эту логическую взаимосвязь таблиц. Давайте рассмотрим пример. Необходимо создать такую структуру из главной и подчиненной таблицы, чтобы запрос с помощью средств interMedia Text обращался к главной таблице, но фактически данные для interMedia Text брались бы из подчиненной таблицы. Создадим сначала главную и подчиненную таблицы и наполним их данными: SQL> 2 3 4 5 6
c r e a t e table purchase_order (id number primary key, description varchar2(100), line__item_body char (1) ) /
Table created. SQL> 2 3 4 5 6
create table line_item (po_id number, po_sequence number, llne_item_detail varchar2(1000) ) /
Table created. SQL> insert into purchase_order (id, description) 2 v, values(1, 'Many Office Items'; 3 / 1 row created. SQL> insert into line_item(po_id, po_sequence, line_item_detail) 2 valuesd, 1, 'Paperclips to be used for many reports') 3 / 1 row created. SQL> insert into line_item(po_id, po_sequence, line_item_detail) 2 values(1, 2, 'Some more Oracle letterhead') 3 / 1 row created. SQL> insert into line_item(po_id, po_sequence, line_item_detail) 2 values(1, 3, 'Optical mouse') 3 / 1 row created. SQL> commit; Commit complete. 5te.
234
Глава 17
Обратите внимание: столбец LINE_ITEM_BODY п 0 сути "фиктивный" — он существует, чтобы можно было создать индекс interMedia Text по главной таблице. Я никогда не буду вставлять в него данные. Прежде чем создавать индекс, необходимо задать параметры interMedia Text так, чтобы при создании индекса были найдены индексируемые данные: SQL> begin 2 ctx_ddl.create_preference('po_pref', 'DETAIL_DATASTORE'); 3 ctx_ddl.set_attribute('po_pref', 'detail_table', 'line_item'); 4 ctx_ddl.set_attribute('po_pref', 'detail_key', 'po_id'); 5 ctx_ddl.set_attribute('po_pref', 'detail_lineno', 'po_sequence'); 6 ctx_ddl. set_attribute (' pojoref', ' detail_text', ' line_item_detail') ; 7 end; 8 / PL/SQL procedure successfully completed. Сначала мы создаем пользовательский параметр PO_PREF. Это источник данных типа DETAIL_DATASTORE, в котором будет храниться вся необходимая информация для поиска данных в подчиненной таблице. В следующих строках мы задаем имя подчиненной таблицы, ключ, по которому выполняется соединение с главной таблицей, порядок следования строк и, наконец, индексируемый столбец. Теперь создадим индекс и проверим его в работе: SQL> 2 3 4
c r e a t e i n d e x po_index on p u r c h a s e _ o r d e r ( l i n e _ i t e m _ b o d y ) indextype is ctxsys.context parameters('datastore po_pref) /
Index created. SQL> select id 2 from purchase_order 3 where contains(line_item_body, 'Oracle') > 0 4 / ID
Хотя индекс создается по столбцу LINE_ITEM_BODY, при создании можно задать и столбец главной таблицы DESCRIPTION. Помните, однако, что любые изменения этого столбца (этот столбец — не фиктивный) вызовут переиндексацию строки главной таблицы и связанных с ней строк подчиненной таблицы. Компонент interMedia Text поддерживает также внешние источники данных, в частности файлы, не входящие в базу данных, а также унифицированные указатели ресурсов (Uniform Resource Locators — URLs). Во многих производственных средах файлы обычно хранятся в доступной по сети общей файловой системе. Необязательно хранить тексты и документы приложения в базе данных Oracle. Можно создать источник данных типа FILE_DATASTORE — это позволит серверу Oracle управлять только текстовым индексом и не заниматься хранением и защитой файлов. При использовании источника данных FILE_DATASTORE не надо хранить текст документа в столбце. Необходимо хранить ссылку на файл в файловой системе, по которой можно обратить-
interMedia
23 J
ся к файлу на сервере. Так что, даже если используется, например, Windows-клиент, а сервер Oracle работает на вашей любимой разновидности ОС UNIX, ссылка на файл должна задаваться по правилам файловой системы ОС UNIX, например /export/home/ tkyte/MyWordDoc.doc. Учтите, что этот способ доступа к внешним файлам никак не связан с альтернативным способом доступа из базы данных Oracle с помощью данных типа BFILE. Еще один тип источника данных, внешнего по отношению к базе данных, — URL_DATASTORE. Он очень похож на источник данных FILE_DATASTORE, но вместо ссылки на файл в файловой системе в столбце таблицы хранится URL. В момент индексации строки компонент interMedia Text фактически прочитает данные по протоколу HTTP. Но и в этом случае сервер Oracle не хранит и не управляет этими данными. Индекс создается на основе профильтрованного содержимого потока данных HTTP, a сами выбранные по URL данные не сохраняются. Протокол FTP тоже поддерживается при использовании источника данных URLDATASTORE, так что interMedia Text позволяет индексировать также файлы, доступные для сервера баз данных по FTP. При использовании версии Oracle 8.1.7 или более новой можно также встраивать имя пользователя и пароль для FTP непосредственно в строку URL (например, ftp:// uid:
[email protected]/tmp/test.doc). Некоторые думают, что источник данных URL_DATASTORE пригоден только для создания поискового робота (кстати, этой возможностью поиска в Web он в исходном виде не обладает). Это неверно. Мои коллеги создали очень большую распределенную систему баз данных с возможностью доступа из сети Internet. Она должна была обеспечить единый универсальный интерфейс поиска текстовых данных во всех задействованных базах. Для этого можно было создать систему индексов interMedia Text по таблицам в каждой базе данных, а затем выполнять декомпозицию запроса на множество распределенных запросов к этим базам данных. Однако они не пошли по пути, ведущему к неоптимальной производительности, а решили выделить один сервер для поддержки компонента interMedia Text и создали все индексы с помощью источника данных URL_DATASTORE. В этой системе удаленные базы данных отвечали за вставку адресов URL для новых или изменившихся документов в базу данных, обеспечивающую поиск. Таким образом, при каждом создании нового или изменении существующего документа серверу, обеспечивающему индексацию документов, передается URL для получения содержания этого документа. Индексирующей машине не приходится искать новые и измененные документы: предоставляющие их серверы просто уведомляют ее о новых поступлениях. При этом не только не нужен распределенный запрос, но и создается централизованный источник информации, что упрощает администрирование.
Компонент interMedia Text - часть базы данных Oracle Одной из наиболее существенных причин для использования компонента interMedia Text вместо решений на базе файловой системы является то, что этот компонент входит в базу данных Oracle. Во-первых, в базе данных Oracle поддерживаются транзакции, а в файловой системе — нет. Целостность данных не нарушается, а свойства ACID реляционной базы данных распространяются и на компонент interMedia Text.
236
Глава 17
Свойства ACID (неделимость, согласованность, изолированность и сохраняемость) представлены в главе 4 (в первой части книги — прим. научн. ред./
Во-вторых, в Oracle для работы с базой данных предлагается язык SQL, и компонент interMedia Text полностью доступен в SQL-операторах. Это позволяет при разработке приложений использовать множество инструментальных средств, "понимающих" язык SQL. При желании (хотя я этого и не рекомендую) можно создать приложение, использующее возможности компонента interMedia Text в электронных таблицах Microsoft Excel, подключаясь к базе данных Oracle через интерфейс ODBC. Поскольку в течение своей карьеры я часто выполнял функции администратора базы данных, меня очень радует тот факт, что все данные будут содержаться в базе данных Oracle, и при ее резервном копировании будет копироваться также приложение и все его данные. При необходимости можно будет восстановить приложение и его данные по состоянию на любой момент времени. Если используется решение на базе файловой системы, придется проверять, создана ли резервная копия базы данных и соответствующей файловой системы, и надеяться, что в момент копирования они были согласованы. При использовании компонента interMedia Text для индексирования информации, содержащейся вне базы данных Oracle, однако, необходимо немного изменить стратегию резервного копирования. Если используются источники данных URL_DATASTORE или FILE_DATASTORE, компонент interMedia Text поддерживает только ссылки на документы, но не сами документы. Документы эти со временем устаревают, удаляются или оказываются недоступными по другим причинам, и это может отрицательно сказаться на работе приложения. Кроме того, при резервном копировании базы данных Oracle уже не происходит полное резервное копирование приложения. Необходимо придумать отдельную стратегию резервного копирования для документов, хранящихся вне базы данных Oracle.
Смысловой анализ Обратитесь к своей любимой поисковой системе, введите часто встречающееся в сети Internet слово, например 'database', и ждите в ответ огромного количества результатов поиска. Индексирование текстов — очень мощное средство, которое можно использовать во многих приложениях. Но его бывает недостаточно. Это особенно верно для очень больших объемов данных, анализировать которые пользователю сложно. Компонент interMedia Text включает интегрированные средства, позволяющие преобразовать все эти данные в полезную информацию. В компонент interMedia Text интегрирована расширяемая база знаний, которая используется в ходе индексирования и анализа текста и обеспечивает возможность лингвистического анализа. Можно не только искать текст, но и анализировать его смысл. Так что при создании индекса interMedia Text можно дополнительно сгенерировать список тем документов. Это позволяет создавать приложения, например, для анализа документов и классификации их по темам, а не по содержащимся словам или фразам. Когда возможность тематической классификации впервые появилась в базе данных Oracle, я придумал простой тест, чтобы оценить в ее возможности. Я загрузил в таблицу базы данных Oracle около тысячи коротких новостей из различных компьютерных жур-
interMedia
237
налов. Затем создал индекс interMedia Text по столбцу, использовавшемуся для хранения текста статей, и сгенерировал список тем для каждой статьи. Выполнив поиск документов, посвященных теме "database", я обнаружил, что в их числе оказались статьи, не содержащие слова "database" и тем не менее отнесенные компонентом interMedia Text к теме "база данных" (database). Сначала я подумал, что это — ошибка в компоненте interMedia Text, но, разобравшись, понял, что обладаю потрясающей возможностью — находить в базе данных текст по смыслу. Речь не идет о статистическом анализе или хитроумном способе подсчета вхождений слов — это именно лингвистический анализ текста. Продемонстрирую эти возможности на примере: SQL> 2 3 4 5
create table mydocs (id number primary key, thetext varchar2(4000) ) /
Table created. SQL> 2 3 4 5 6
create table mythemes (query_id number, theme varchar2(2000), weight number ) /
Table created. SQL> 2 3 4 5
insert into mydocs(id, thetext) values(1, 'Go to your favorite Web search engine, type in a frequently occurring word on the Internet like ''database'', and wait for the plethora of search results to return.'
1 row created. SQL> commit; Commit complete. SQL> create index my_idx on mydocs(thetext) indextype is ctxsys.context; Index created. SQL> begin 2 ctx_doc.themes(index_name => 'my_idx', 3 textkey => '1', 4 restab => 'mythemes' 5 ); /П ' PL/SQL procedure successfully completed. SQL> select theme, weight from mythemes order by weight desc;
•
238
Глава 17
THEME
occurrences search engines Internet result returns databases searches favoritism type plethora frequency words
WEIGHT
12 12
12 11 11 11 11 11 10 10 6 5 4 3 3
12 rows selected.
PL/SQL-блок берет таблицу, на которую ссылается индекс MY_IDX, находит строку со значением key = 1 и выбирает проиндексированные данные. Затем эти данные обрабатываются тематическим анализатором. Анализатор генерирует список затронутых в документе тем, присваивая им "вес" (например, статья про банковскую деятельность может касаться тем "деньги", "кредит" и т.п.). Затем информация об этих темах помещается в таблицу MYTHEMES. Если проделать это для всех данных в приложении, пользователи смогут искать не только строки, содержащие определенное слово, но и строки, наиболее близкие по смыслу к определенному тексту. Учтите, что если предоставить компоненту interMedia Text больше данных для анализа, сгенерированный список тем может оказаться намного точнее, чем для рассмотренного простого предложения. Учтите также, что я создал столбец ID как первичный ключ. Для компонента interMedia Text в Oracle 8i 8.1.6 и более ранних версиях необходимо наличие первичного ключа для таблицы, прежде чем по ней можно будет создавать индекс interMedia Text. В Oracle 8i 8.1.7 и последующих версиях компонент interMedia Text больше не требует наличия первичного ключа при создании индекса.
Поиск в приложениях XML У меня часто спрашивают, как обеспечить эффективный поиск в документе со встроенной разметкой, например, на языке HTML или XML. К счастью, компонент interMedia Text позволяет очень просо решить эту задачу — за счет использования объектов, называемых разделами. Это решение легко использовать, сочетая возможности анализа XML (XML parsing) и задания разделов в источнике данных URL_DATASTORE. Если XML соответствует декларируемым целям, т.е. является средством взаимодействия разнородных систем, то разработчик приложения с помощью компонента interMedia Text может легко создать оперативную базу знаний с возможностями поиска данных из различных систем. Полный пример индексирования XML-документов представлен далее в этой главе.
interMedia
2 3 9
Как работает компонент interMedia Text В этом разделе описано, как реализован компонент interMedia Text и что дает его использование. Как уже упоминалось, компонент interMedia Text создан с использованием стандартного механизма расширения Oracle. С помощью соответствующих средств команда разработчиков компонента interMedia Text смогла добавить в СУБД Oracle специфический тип индекса для текста. Внимательней присмотревшись к используемым объектам базы данных, можно "приподнять занавес" и получить представление о том, как реализован этот компонент. Объекты базы данных, составляющие компонент interMedia Text, всегда принадлежат пользователю CTXSYS: SQL> connect ctxsys/ctxsys Connected. SQL> s e l e c t indextype_name, implementation_name 2 from user_indextypes; INDEXTYPE_NAME
IMPLEMENTATION_NAME
CONTEXT CTXCAT
TEXTINDEXMETHODS CATINDEXMETHODS
Как видите, в схеме, которой принадлежит компонент interMedia Text, имеется два типа индексов. Один из индексов, CONTEXT, знаком большинству пользователей компонента interMedia Text. Второй индекс, СТХСАТ, — это индекс для каталога, обеспечивающий подмножество возможностей, доступных при использовании индекса CONTEXT. Индекс для каталога, появившийся в версии Oracle 8.1.7, идеально подходит для текстовых данных, представляющих собой небольшие фрагменты текста. SQL> select library_name, file_spec,
dynamic from user_libraries;
LIBRARY_NAME
FILE_SPEC
D
DR$LIB DR$LIBX
O:\Oracle\Ora81\Bin\oractxx8.dll
N Y
Как видите, с компонентом interMedia Text связаны две библиотеки. DR$LIB не является динамически компонуемой и представляет собой библиотеку проверенного кода в самой СУБД Oracle. DR$LIBX — это разделяемая, динамически компонуемая библиотека, зависящая от соответствующей операционной системы. Поскольку этот запрос был выполнен к базе данных, работающей в среде Windows, имя файла, содержащего эту разделяемую библиотеку, отражает особенности Windows. Если выполнить такой же запрос в среде UNIX, результат будет другим. Эти библиотеки специально предназначены для компонента interMedia Text. Они содержат набор методов, позволяющих ядру СУБД Oracle обрабатывать соответствующие индексы interMedia Text. SQL> s e l e c t operator_name, number_of_binds from user_operators; OPERATOR NAME CATSEARCH
NUMBER OF BINDS 2
240
Глава 17
CONTAINS SCORE
В рамках механизма расширения можно также создавать уникальные объекты базы данных — операторы. Оператор используется индексом; с каждым оператором ассоциируется ряд связываний (bindings). Во многом аналогично языку PL/SQL, где можно определять функции с одинаковыми именами, но с различными типами параметров (сигнатурой), механизм расширения позволяет определить оператор, соответствующий различным пользовательским методам, в зависимости от сигнатуры использования. SQL> s e l e c t d i s t i n c t 2 type_name;
method_name, type_name from u s e r _ m e t h o d _ p a r a m s
METHOD NAME
TYPE NAME
ODCIGETINTERFACES ODCIINDEXALTER ODCIINDEXCREATE ODCIINDEXDELETE ODCIINDEXDROP ODCIINDEXGETMETADATA ODCIINDEXINSERT ODCIINDEXTRUNCATE ODCIINDEXUPDATE ODCIINDEXUTILCLEANUP ODCIINDEXUTILGETTABLENAMES RANK ODCIGETINTERFACES ODCIINDEXALTER ODCIINDEXCREATE ODCIINDEXDROP ODCIINDEXGETMETADATA ODCIINDEXTRUNCATE ODCIINDEXUTILCLEANUP ODCIINDEXUTILGETTABLENAMES ODCIGETINTERFACES
CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CATINDEXMETHODS CTX_FEEDBACK_ITEM_TYPE TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTINDEXMETHODS TEXTOPTSTATS
order
by
21 rows selected.
После просмотра этих результатов становится понятным, как разработчик может использовать механизм расширения в базе данных Oracle. С каждым типом ассоциированы наборы поименованных методов, которые механизм расширения различает по уникальным именам. Например, методы, связанные с поддержкой индекса — ODCIIndexInsert, ODCIIndexUpdate и ODCIIndexDelete, вызываются СУБД Oracle при создании, изменении или удалении данных, связанных с индексом. Таким образом, при необходимости вставки новой строки в индекс interMedia Text ядро Oracle вызывает метод, ассоциированный с ODCIIndexInsert. Это специально созданная подпрограмма, выполняющая необходимые операции с индексом interMedia Text, а затем уведомляющая СУБД Oracle о завершении обработки. Ознакомившись с основами реализации компонента interMedia Text, давайте рассмотрим некоторые из объектов базы данных, связываемые с этим специализированным индексом interMedia Text при его создании в базе данных.
interMedia
241
SQL> select table_name 2 from user_tables 3 where table_name like '%MYTEXT%\TABLE NAME MYTEXT SQL> 2 3 4
create index mytext_idx on mytext(thetext) indextype is ctxsys.context /
Index created. SQL> select table_name 2 from user_tables 3 where table_name like '%MYTEXT%'; TABLE NAME DR$MYTEXT_IDX$I DR$MYTEXT_IDX$K DR$MYTEXT_IDX$N DR$MYTEXT_IDX$R MYTEXT Мы начали сеанс SQL*Plus с запроса к представлению USER_TABLES, выбирающего все имена таблиц, содержащие подстроку MYTEXT. После создания таблицы MYTEXT и индекса interMedia Text по столбцу этой таблицы оказывается, что имя пяти таблиц, включая исходную, содержит эту подстроку. Таким образом, при создании индекса interMedia Text автоматически создается еще четыре таблицы. Имена этих таблиц всегда будут иметь префикс DR$, за которым следует имя созданного индекса и один из суффиксов — $1, $К, $N или $R. Соответствующие таблицы всегда создаются в той же схеме, что и индекс interMedia Text. Давайте подробнее рассмотрим их структуру. SQL> desc dr$mytext_idx$i; Name TOKENJTEXT TOKENJTYPE TOKEN_FIRST TOKEN_LAST TOKEN COUNT TOKEN_INFO QL> desc dr$mytext idx$k; Name DOCID TEXTKEY
Null?
Type
NOT NOT NOT NOT NOT
VARCHAR2(64) NUMBER(3) NUMBER(IO) NUMBER(IO) NUMBER(IO) BLOB
NULL NULL NULL NULL NULL
Null?
Type
NUMBER(38) NOT NULL ROWID
242
Глава 17
SQL> d e s c Name
dr$mytext_idx$n;
Null?
Type
NLT_DOCID
NOT NULL NUMBER(38)
NLT~MARK
NOT NULL CHAR(l)
SQL> d e s c Name
dr$mytext_idx$r;
ROW_NO DATA
Null?
Type NUMBER(3) BLOB
Для каждого индекса interMedia Text создается набор таблиц с подобной структурой. Таблица лексем, DR$MYTEXT_IDX$I, — это основная таблица индекса interMedia Text. Эта таблица используется для хранения каждой проиндексированной лексемы и битовой карты с установленными битами для всех документов, содержащих эту лексему. В этой таблице хранится и другая двоичная информация для оценки близости лексем в тексте. Обратите внимание, что я умышленно использую термин "лексема" в этом абзаце, поскольку компонент interMedia Text позволяет индексировать тексты на языках с иероглифической письменностью, включая китайский, японский и корейский. Было бы некорректно говорить об использовании таблицы DR$I для индексирования "слов". Таблицы DR$K и DR$R по сути поддерживают соответствие между идентификаторами строк (ROWID) и идентификаторами документов. Последняя таблица, DR$N, или "таблица отсутствующих строк", используется для поддержки списка удаленных документов/строк. При удалении строки из таблицы, по которой создан индекс interMedia Text, физическое удаление информации об этой строке из индекса interMedia Text откладывается. В этой служебной таблице записываются идентификаторы документов из удаленных строк для последующего удаления при следующей перестройке или оптимизации индекса. Учтите также, что таблицы DR$K и DR$N создаются как организованные по индексу. Обращения к этим таблицам в коде компонента interMedia Text обычно затрагивают оба столбца таблиц. Для повышения эффективности и сокращения объема ввода-вывода эти таблицы организуются по индексу. Подводя итоги этого раздела, хочу подчеркнуть, что, хотя и интересно разобраться, как компонент interMedia Text реализован с помощью механизма расширения Oracle, это вовсе не обязательно для эффективного использования interMedia Text. Многие разработчики создавали весьма сложные приложения с использованием компонента interMedia Text, ничего не зная о назначении создаваемых таблиц.
Индексирование с помощью interMedia Text Используя простую таблицу, созданную в предыдущем разделе, давайте по шагам пройдем процесс вставки текста, чтобы увидеть момент фактического выполнения соответствующих изменений компонентом interMedia Text: SQL> delete from mytext; 2 rows deleted.
interMedia
243
SQL> insert into mytext(id, thetext) 2
values(1, 'interMedia Text is quite simple to use');
1 row created. SQL> insert into mytext(id, thetext) 2
values(2, 'interMedia Text is powerful, yet easy to learn');
1 row created. SQL> commit; Commit complete. Итак, можно ли сейчас по запросу поиска Text получить обе строки таблицы? Может быть. Если индекс interMedia Text не синхронизирован, то выполненные изменения в нем еще не учтены. Синхронизация индекса означает выполнение всех ожидающих учета изменений. Как же определить, есть ли в индексе interMedia Text изменения, ожидающие учета? SQL> s e l e c t pnd_index_name, p n d _ r o w i d from
ctx_user_pending;
PND INDEX NAME
PND ROWID
MYTEXT_IDX MYTEXT_IDX
AAAGF1AABAAAIV0AAA AAAGF1AABAAAIV0AAB
Выполняя запрос к представлению CTX_USER_PENDING, можно определить, что ожидают изменения две строки индекса interMedia Text с именем MYTEXT_IDX. Представление CTX_USER_PENDING создано по принадлежащей пользователю CTXSYS таблице DRSPENDING. При любой вставке строки в таблицу MYTEXT в таблицу DRSPENDING будет вставляться строка для индекса MYTEXTJDX interMedia Text. Обе вставки выполняются в одной физической транзакции, поэтому, если транзакция, вставившая строку в таблицу MYTEXT, будет отменена, произойдет также отмена вставки в таблицу DRSPENDING. Есть три различных способа синхронизации индекса interMedia Text. Эта синхронизация может выполняться в различных условиях и по разным причинам. Позже я скажу о том, когда предпочтительнее использовать тот или иной метод. Простейший метод синхронизации индекса — запуск программы ctxsrv. Эта программа работает аналогично демонам в ОС UNIX. Программа запускается, работает в фоновом режиме и время от времени автоматически синхронизирует индекс. Этот метод рекомендуется использовать при работе с небольшим количеством (до 10000) строк, каждая из которых содержит небольшой объем текста. Другой метод синхронизации индекса — выполнение оператора ALTER INDEX. Можно организовать очередь изменений, ожидающих выполнения, а затем построить и выполнить пакет действий по синхронизации индекса. Во многих случаях это лучший метод синхронизации индекса, обеспечивающий минимальную фрагментацию. Для синхронизации индекса используется следующий оператор: a l t e r index [схема.]индекс rebuild [online] parameters ('sync [memory объем__памяти] ') Имеет смысл пересоздавать индекс в оперативном режиме (online), чтобы он оставался доступным в процессе синхронизации. Кроме того, можно задать объем исполь-
244
Глава 17
зуемой при этом памяти. Чем больше памяти выделено процессу синхронизации, тем большим может быть пакет индексируемых изменений и тем меньше окажется в итоге индекс interMedia Text. Хотя многие и сочтут третий метод синхронизации индекса одноразовым, я настаиваю, что простое пересоздание индекса тоже является методом синхронизации. При выполнении оператора CREATE INDEX для создания индекса типа CONTEXT будет создан индекс и проиндексированы все данные столбцов, по которым он создается. При этом часто действия выполняются циклически: в таблице есть данные, мы создаем индекс, а затем добавляем новые строки. Поскольку изменения, связанные с добавлением новых строк, не выполняются до момента синхронизации индекса, многие приходят к выводу, что единственный способ поддержать актуальность индекса — удалить и создать его заново! Индекс действительно синхронизируется, но такой метод крайне неэффективен, и я не рекомендую его использовать. Семантика языка SQL принципиально не позволяет двум пользователям одновременно выполнять оператор ALTER INDEX REBUILD для одного и того же индекса, но ничто не мешает пересоздавать или синхронизировать одновременно несколько индексов interMedia Text. Продолжая пример, синхронизируем индекс: SQL> a l t e r index mytext_idx rebuild online parameters('sync memory 20M'); Index a l t e r e d . SQL> s e l e c t pnd_index_name, pnd_rowid from
ctx_user_pending;
no rows selected Теперь индекс синхронизирован, и можно выполнять запрос, использующий его: SQL> select id 2 from mytext 3 4
where contains(thetext, 'easy') > 0
/ ID
Просмотрим данные в одной из служебных таблиц, созданных автоматически при создании индекса interMedia Text: SQL> select token_text, token_type from dr$mytext_idx$i; TOKEN_TEXT
TOKENJTYPE
EASY INTERMEDIA LEARN POWERFUL QUITE SIMPLE TEXT USE YET
0 0 0 0 0 0 0 0 0
interMedia interMedia Text learning TOKEN TEXT
245 1 1
TOKEN TYPE
powerfulness simplicity Выполняя запрос к таблице DR$I, соответствующей индексу MYTEXT_IDX, мы можем оценить, какая часть информации обработана компонентом interMedia Text при синхронизации индекса. Во-первых, обратите внимание, что многие значения в столбце TOKEN_TEXT целиком состоят из прописных букв. Это реальные слова из текста, переведенные в верхний регистр. При необходимости можно потребовать от компонента interMedia Text создавать индекс по словам с учетом регистра при выполнении оператора CREATE INDEX. Обратите также внимание, что некоторые лексемы, для которых в столбце TOKEN_TYPE находится значение 1, хранятся в смешанном регистре. Что еще важнее, ни в одной из строк таблицы MYTEXT нет слов "simplicity" и "learning". Так откуда же взялись эти данные? При разбиении лексическим анализатором блока текста на английском языке на лексемы стандартным действием является добавление в индекс информации о тематике текста. Таким образом, каждая строка со значением TOKENJTYPE =1 — это тема, сгенерированная компонентом interMedia Text в процессе лингвистического анализа. Наконец, нельзя не заметить отсутствия некоторых слов в этой таблице. Слова is и to не значатся среди лексем в таблице индекса, хотя и входили в исходные данные проиндексированной таблицы. Они являются стоп-словами (stopwords) и не включаются в индекс как лишняя информация. Эти слова часто многократно встречаются в большинстве текстов на английском и по сути являются "шумом". Слова is, to и около 120 других входят в стандартный список стоп-слов (stoplist) для английского языка, который и используется по умолчанию при создании индекса. Корпорация Oracle включила в состав компонента interMedia Text стандартные списки стоп-слов более чем для 40 языков. Помните, что список стоп-слов использовать не обязательно, можно создавать и использовать собственные специализированные списки. Хочу завершить этот раздел предупреждением. Хотя весьма интересно разобраться, как именно устроен компонент interMedia Text, особенно посмотреть, какие лексемы генерируются при создании индекса, не пытайтесь создавать другие объекты базы данных, использующие внутренние структуры индекса. В частности, не создавайте представления для таблицы DR$MYTEXT_IDX$I или триггеры по таблице DR$MYTEXT_IDX$K. Структура реализации может измениться и скорее всего изменится уже в следующих версиях компонента.
Оператор ABOUT С появлением оператора ABOUT в Oracle стало намного проще выполнять тематический анализ в запросах, да и точность результатов, выдаваемых по таким запросам, существенно увеличилась. Для текстов на английском языке оператор ABOUT обеспе-
246
Глава 17
чивает поиск всех строк, соответствующих нормализованному представлению искомого понятия. Как я уже говорил, по умолчанию для текстов на английском языке информация о тематике включается в индекс. Эта информация о тематике текста в индексе будет использоваться для поиска других строк, в которых затрагиваются близкие понятия. Если почему-либо было решено не генерировать информацию о тематике текста при создании индекса, оператор ABOUT будет выполнять простой поиск соответствующих лексем. SQI> select id from mytext where contains(thetext, 'about(databases)') > 0; no rows s e l e c t e d Как и ожидалось, в таблице нет строк, посвященных понятию "databases". SQL> s e l e c t i d from mytext where c o n t a i n s ( t h e t e x t , ' a b o u t ( s i m p l y ) ' ) > 0;
™ I Есть одна строка, связанная с понятием "просто" (simply). Если точнее, есть одна строка, содержащая понятия-синонимы нормализованной версии слова simply. Чтобы показать это, выполним: SQL> s e l e c t id from mytext where c o n t a i n s ( t h e t e x t , ' s i m p l y ' ) > 0; no rows selected При удалении оператора ABOUT из запроса ни одна строка не возвращается — в столбец thetext не входит слово simply. Имеется, однако, одна строка, в которой используемые понятия соответствуют нормализованному корню слова simply. Связанное со словом понятие — не то же самое, что лингвистический корень слова. Оператор получения основы (stem operator — $) компонента interMedia Text позволяет искать инфлекционные (inflectional) или производные формы слова. Таким образом, поиск по основе слова health может дать в результате документы, содержащие слово healthy. Поиск синонимов понятия health (здоровье) с помощью оператора ABOUT может также вернуть документы, содержащие слово wellness. Оператор ABOUT очень легко включить в приложения для использования всей мощи средств генерации тематической информации и лингвистического анализа. Оператор ABOUT позволяет обеспечить в приложении не только поиск введенных пользователем слов, но и поиск связанных с ними понятий. Это действительно мощное средство.
Поиск в разделах Последняя тема, которую я хочу подробно рассмотреть, — поиск в разделах. Разделы обеспечивают избирательный доступ запроса к документу и могут существенно повысить точность запросов. Раздел может представлять собой не что иное, как заданную разработчиком последовательность символов, отмечающую начало и конец логической единицы в документе. Популярность стандартных языков разметки, таких как HTML и XML, позволяет продемонстрировать всю мощь средств поиска в разделах, предлагаемых компонентом interMedia Text.
interMedia
24/
Типичный документ содержит общеупотребительные логические элементы, образующие его структуру. У большинства документов есть название, может быть заголовок, может быть шаблонная информация, основной текст, содержание, предметный указатель, приложения и т.д. Все это — логические единицы, образующие структуру документа. В качестве примера ситуации, когда необходим поиск в разделах документов, рассмотрим гипотетическое хранилище документов для Министерства обороны. Может понадобиться найти в хранилище документы, содержащие фразу "Ракета Hellfire". Но может быть еще важнее найти документы, содержащие фразу "Ракета Hellfire" в заголовке документа или, например, в предметном указателе. Компонент interMedia Text позволяет разработчику приложения задать последовательность символов, выделяющую эти логические разделы структуры документа. Кроме того, компонент interMedia Text поддерживает поиск текста в заданных таким образом логических разделах. В компоненте interMedia Text для задания логических единиц или группировки текста в документе используется понятие "разделы". Набор идентификаторов разделов образует группу разделов, и именно группу разделов можно указать при создании индекса interMedia Text. Язык разметки гипертекста (Hypertext Markup Language — HTML) тоже первоначально создавался как способ организации структуры документа, но быстро стал языком, частично описывающим структуру, а частично — внешний вид документа. Тем не менее в состав interMedia Text стандартно входят компоненты, позволяющие создать раздел логической структуры документа из каждого тега разметки, содержащегося в документе. Аналогично, поддержка языка XML тоже встроена в компонент interMedia Text, начиная с версии Oracle 8.I.6. Для XML-документа можно легко (при желании — автоматически) задать разделы, соответствующие каждому заданному в нем XML-элементу. Давайте сначала рассмотрим следующий пример документа на языке HTML: SQL> c r e a t e t a b l e my_html_docs 2 (id number primary key, 3 html_text varchar2(4000)) 4 / Table created. SQL> 2 3 4 5 6 7
insert into my_html_docs(id, html_text) values(1, '
Oracle Technology This is about the wonderful marvels of 8i and 9i ') /
1 row created. SQL> commit; Commit complete. SQL> create index my_html_idx on my_html_docs(html_text) 2 indextype is ctxsys.context
248
Глава 17
3
/
Index created. Теперь можно искать строку по проиндексированному в HTML-документе слову. SQL> s e l e c t id from my_html_docs 2 where contains(html_text, 'Oracle') > 0 3 / ID
SQL> select id from my_html_docs 2 where contains(html_text, 'html') > 0 3 / ID
Легко создать запрос для поиска всех строк, содержащих слово Oracle, но в полученном решении очевидны два недостатка. Во-первых, элементы разметки индексировать не надо, поскольку они встречаются постоянно и во всех документах и не являются частью содержимого документа. Во-вторых, мы, конечно, можем искать слова в HTMLдокументе, но без учета структурных элементов, в которых они содержатся. Мы знаем, что где-то в тексте документа есть строка, содержащая слово Oracle, но это может быть заголовок, основной текст, колонтитул и т.п. Предположим, в приложении необходимо обеспечить поиск в заголовках HTMLдокументов. Создадим для этого группу разделов с тегом TITLE, а затем удалим и заново создадим индекс: SQL> b e g i n 2 ctx_ddl.create_section_group('my_section_group', 'BASIC_SECTION_GROUP'); 3 ctx_ddl.add_field_section( 4 group_name => 'my_section_group', 5 section_name => 'Title', 6 tag => 'title', 7 visible => FALSE); 8 end; 9
/
PL/SQL procedure successfully completed. SQL> drop index my_html_idx; Index dropped. SQL> create index my_html_idx on my_html_docs(html_text) 2 indextype is ctxsys.context 3 parameters('section group my_section_group') 4 / Index created. Мы создали новую группу разделов, MY_SECTION_GROUP, и добавили в нее раздел с именем Title. Обратите внимание, что раздел соответствует тегу title и будет неви-
interMedia
249
дим. Если раздел помечен как видимый, текст между соответствующими тегами считается частью документа. Если же раздел помечен как невидимый, текст между начальным и конечным тегами рассматривается отдельно от документа и будет доступен только при поиске в соответствующем разделе. Как и большинство современных языков разметки (например, XML, HTML, WML), начальный тег в interMedia Text начинается символом < и заканчивается символом >. Конечный тег начинается с последовательности символов и заканчивается символом >. SQL> select id 2 from my_html_docs 3 where contains(html_text, 'Oracle') > 0 4 / no rows selected Запрос, прежде возвращавший одну строку, теперь строк не возвращает, а мы ведь всего лишь задали группу разделов для индекса interMedia Text. Вспомните, что раздел Title был задан как невидимый, поэтому текст в тегах title рассматривается как подчиненный документ. SQL> select id 2 from my_html_docs 3 where contains(html_text, 'Oracle within title') > 0 4 / ID
Теперь можно выполнить запрос, выполняющий поиск только в разделах title всех документов. Если же попытаться искать текст самого тега, окажется, что компонент interMedia Text тег не проиндексировал: SQL> select id 2 from my_html_docs 3 where contains( html_text, 'title' ) > 0 4 / no rows selected Хотя ранее я задал собственный тип группы разделов на основе группы разделов BASIC_SECTION_GROUP, в состав компонента interMedia входят также заранее заданные группы разделов со стандартными системными установками для языков HTML и XML (HTML_SECTION_GROUP и XML_SECTION_GROUP). Использование такой группы разделов не определяет автоматически разделы для всех возможных элементов HTML и XML. Это надо делать самому. Однако при использовании указанных групп разделов компонент interMedia Text сможет корректно преобразовать размеченный документ в обычный текст. Попробуем применить соответствующую заданную группу в рассмотренном примере: SQL> drop index my_html_idx; Index dropped.
250
Глава 17
SQL> 2 3 4
create index my_html_idx on my_html_docs(html_text) indextype is ctxsys.context parameters('section group ctxsys.html_section_group') /
Index created. SQL> select id 2 from my_html_docs 3 where contains(html_text, 'html') > 0 4 / no rows selected
Оказывается, задав системные установки, соответствующие группе разделов HTML_SECTION_GROUP, мы избежали индексирования строк, являющихся тегами разметки языка HTML. Это не только повышает точность запросов к документам, но и сокращает общий размер индекса interMedia Text. Предположим, необходимо найти слово title во всех хранящихся HTML-документах. Если не использовать для индекса interMedia Text группу HTML_SECTION_GROUP, в ответ на этот запрос могут быть выданы все HTML-документы, содержащие раздел title (речь идет о части HTML-документа, между тегами
и ). Игнорируя теги и ограничиваясь исключительно содержимым HTML-документов, можно существенно повысить точность поиска. Рассмотрение обработки XML-документов начнем с примера. Предположим, необходимо управлять подборкой XML-документов и обеспечить интерфейс для запросов к структурным элементам этих документов. Чтобы усложнить задачу, предположим, что не все собранные XML-документы соответствуют одному и тому же определению структуры, задаваемому определением типа документа (Document Type Definition — DTD). По аналогии с предыдущим примером можно подумать, что необходимо определить все элементы XML-документов, в которых может потребоваться поиск, а затем задать раздел interMedia Text для каждого из этих элементов. К счастью, компонент interMedia Text включает средства автоматического создания и индексирования разделов по имеющимся в документе тегам. Появившаяся в компоненте interMedia Text версии Oracle 8.1.6, группа разделов AUTO_SECTION_GROUP работает аналогично группе разделов XML_SECTION_GROUP, но снимает с разработчика приложений необходимость заранее определять все разделы. Группа разделов AUTO_SECTION_GROUP требует от компонента interMedia Text автоматически создать разделы для всех непустых тегов в документе. Хотя разработчик явно может сопоставить тегу любое имя раздела, при такой автоматической генерации имена разделов будут совпадать с соответствующими тегами. SQL> c r e a t e t a b l e my_xml_docs 2 3 4 5
(id number primary key, xmldoc varchar2(4000) ) /
Table created. SQL> insert into my_xml_docs(id, xmldoc) 2 values(1,
interMedia 3 4 5 6 7 8 9 10 11 12
251
'
Team Meeting <start_date>31-MAR-200K/start_date> <start_tirne>1100 <notes>Review projects for Ql Joel Tom ')
13 / 1 row created. SQL> commit; Commit complete. SQL> create index my_xml_idx on my_xml_docs(xmldoc) 2 indextype is ctxsys.context 3 parameters('section group ctxsys.auto section group') 4 / ~ ~ Index created. Таким образом, без всяких дополнительных действий со стороны разработчика приложения компонент interMedia Text автоматически создал разделы для всех тегов, содержащихся в индексируемых XML-документах. Так что, если необходимо найти документы, содержащие слово projects в элементе note, достаточно выполнить следующий оператор: SQL> select id 2 from my_xml_docs 3 where contains(xmldoc, 'projects within notes') > 0 / ID
В процессе автоматического создания разделов созданы специальные разделы зон (zone section). В предыдущих примерах определения разделов создавались исключительно разделы полей (field section). В отличие от разделов полей, разделы зон могут перекрываться и быть вложенными. Поскольку при использовании группы разделов AUTO_SECTION_GROUP компонент interMedia Text создает разделы зон для всех непустых тегов, можно выполнять запросы вида: SQL> select id 2 from my_xml_docs 3 where contains(xmldoc, 'projects within appointment') > 0 4 / ID 1
252
Глава 17
SQL> select id 2 from my_xml_docs 3 where contains(xmldoc, 'Joel within attendees') > 0 4 / ID
Раздел, указанный в предыдущих запросах, не содержит искомых терминов явно: они вложены в структурные элементы этого раздела. Использование разделов зон и автоматического создания разделов позволяет управлять областью поиска в XML-документе, расширяя или сужая ее в соответствии с необходимостью. Используя группы разделов, можно потребовать от компонента interMedia Text проиндексировать атрибуты тегов. При использовании группы разделов AUTO_SECTION_GROUP значения атрибутов разделов автоматически выбираются и индексируются. Итак, если необходимо найти все персональные задания, другими словами, XMLдокументы, содержащие строку personal как значение атрибута type тега appointment, можно выполнить следующий запрос: SQL> select id 2 from my_xml_docs 3 where contains(xmldoc, 'personal within appointment@type') > 0 4 / ID
Как видите, задание и индексирование разделов — очень мощное средство компонента interMedia Text. Следует, однако, помнить, что группу AUTO_SECTION_GROUP можно использовать не всегда. Хотя и есть возможность потребовать от компонента interMedia Text при автоматическом создании разделов не индексировать определенные теги, в конечном итоге в разделы может быть выделено и проиндексировано слишком много элементов документа, что "загрязняет" индекс. Общий размер индекса чрезвычайно увеличивается, что может резко снизить производительность поиска. Автоматическое создание разделов — мощное средство, но его надо использовать осторожно.
Проблемы При использовании компонента interMedia Text следует учитывать возможность возникновения ряда проблем. Не все они очевидны или возникают достаточно часто, поэтому я опишу наиболее типичные, с которыми мне приходилось сталкиваться.
Компонент interMedia Text - это НЕ система документооборота Это не проблема, скорее следствие неверного представления о назначении компонента interMedia Text. Я слышал, как при упоминании компонента interMedia Text клиенты и сотрудники корпорации Oracle называли его системой документооборота (document
interMedia
253
management solution). Без сомнения, interMedia Text не является системой документооборота. Документооборот — отдельная полноценная наука. Система документооборота предлагает набор средств, поддерживающих весь жизненный цикл документов. Она должна обеспечивать регистрацию входящих и исходящих документов, их логическую структуризацию, хранение нескольких версий документа и списков контроля доступа, интерфейс для поиска текста, а также возможность публикации документов. Компонент interMedia Text, конечно, не является системой документооборота. Он может использоваться в полной системе документооборота и обеспечивать многие из необходимых функций. Корпорация Oracle тесно интегрировала средства interMedia Text в состав системы Oracle Internet File System, которую можно использовать для управления содержимым сайтов (content management), и которая обеспечивает базовые функции системы документооборота.
Синхронизация индекса Часто необходимо создать систему на базе средств interMedia Text и выполнять в фоновом режиме процесс ctxsrv для периодической синхронизации индекса interMedia Text, причем синхронизация должна происходить в реальном времени. Одна из проблем, которые могут возникнуть при многократной и частой синхронизации индекса interMedia Text для большого набора документов, связана с его разрастанием и фрагментацией. Нет простого правила, позволяющего определить, когда следует периодически синхронизировать индексы большими пакетами, а когда лучше синхронизировать их с помощью процесса ctxsrv сразу после фиксации изменений в документах. Во многом это зависит от сути приложения, частоты изменения текста документов, общего количества и размера документов. В качестве примера можно привести мой Web-сайт AskTom. На этом сайте пользователи Oracle задают технические вопросы о программных продуктах Oracle. Вопросы просматриваются, на них даются подробные (я надеюсь) ответы, и эти ответы публикуются. Опубликованные вопросы и ответы вставляются в таблицу, проиндексированную с помощью индекса interMedia Text. На Web-сайте AskTom есть страница поиска по этой таблице опубликованных вопросов и ответов. Общее количество строк в этой системе сравнительно невелико (менее 10000 на момент написания этой главы). Изменения в системе практически никогда не происходят; после публикации вопросов и ответов они почти никогда не изменяются и не удаляются. Каждый день вставляется обычно не более 25 строк, и вставки эти выполняются в течение всего дня. Мы выполняем синхронизацию индекса с помощью процесса ctxsrv, работающего в фоновом режиме, что идеально подходит для данной системы, предназначенной прежде всего для поиска в условиях небольшого количества вставок и изменений. Если же предполагается загрузка в таблицу по миллиону документов в неделю, не стоит индексировать их с помощью процесса ctxsrv. В этом случае имеет смысл синхронизировать индекс interMedia Text пакетами максимально большого размера, при котором еще не требуется откачка страниц памяти на диск. Выстроив запросы индексирова-
254
Глава 17
ния в очередь и выполняя их большими пакетами, можно получить в результате более компактный индекс. Независимо от выбранного метода синхронизации следует периодически оптимизировать индексы interMedia Text с помощью оператора ALTER INDEX REBUILD. Процесс оптимизации позволит получить не только более компактный индекс, но и очистит его от информации, оставшейся после прежних логических удалений.
Индексирование информации вне базы данных Компонент interMedia Text не требует помещать текстовые данные в базу данных. С его помощью можно индексировать данные, содержащиеся в документах в файловой системе сервера или даже доступных извне по адресу URL. Когда данные находятся в базе данных Oracle, все изменения в них автоматически обрабатываются компонентом interMedia Text. Когда же источник данных находится вне базы данных, синхронизацию индекса с изменившимися внешними данными должен обеспечить разработчик приложения. Изменить отдельную строку проще всего, изменив один из столбцов, по которому создан индекс interMedia Text. Например, если бы я использовал следующую таблицу и индекс для поддержки списка проиндексированных адресов URL: SQL> create table my_urls 2 (id number primary key, 3 theurl varchar2(4000) 4 )
I Table created. SQL> create index my__url_idx on my_urls(theurl) 2 indextype is ctxsys.context 3 parameters('datastore ctxsys.url_datastore') 4
/
Index created. Я мог бы "обновить" индекс для конкретной строки, выполнив: SQL> update my_urls 2 set theurl = theurl 3 where id = 1 4 / 0 rows updated.
Службы обработки документов Под "службами обработки документов" я подразумеваю набор средств компонента interMedia Text для преобразования документа в текстовый вид или формат HTML, с возможным выделением слов, найденных в документе в результате поиска. Многие ошибочно считают, что компонент interMedia Text сохраняет всю необходимую информацию для полного воссоздания документа. Это ошибочное представление приводит к выводу, что после индексирования исходный текст документа можно уда-
interMedia
255
лить. Это верно, если придется только выполнять запросы к проиндексированной информации, но не верно, если предполагается поддержка тех или иных служб обработки документов. Например, если создать индекс interMedia Text с помощью URL_DATASTORE и попытаться сгенерировать HTML-представление найденной строки с помощью процедуры CTX_DOC.FILTER, ее вызов завершится сообщением об ошибке, если документ с соответствующим адресом URL недоступен. Компоненту interMedia Text для выполнения этого действия необходим доступ к исходному документу. Это относится также к файлам, хранящимся в файловой системе вне базы данных и проиндексированным как источник данных FILE_DATASTORE.
Индекс-каталог Во многих ситуациях индекс interMedia Text предоставляет намного больше возможностей, чем требуется приложению. Использование индекса interMedia Text, кроме того, требует выполнения определенных действий по сопровождению, обеспечивающих его оптимизацию, синхронизацию и т.д. Для поддержки приложений, которым не нужны все функциональные возможности индекса interMedia Text, в версии компонента interMedia Text в СУБД Oracle 8.1.7 появился новый тип индекса — индекс-каталог, или, сокращенно, ctxcat. Обычно большая часть текстовых данных не хранится в базе данных в виде большого набора документов. Во многих приложениях баз данных текст обычно неформатирован, состоит из небольших фрагментов и не разбит на логические разделы, а размер текстовых фрагментов настолько мал, что их качественный лингвистический анализ невозможен. Кроме того, такого рода приложения баз данных часто запрашивают текстовые данные по условиям на другие столбцы., Рассмотрим, например, базу данных, в которой регистрируются поступившие сообщения о проблемах и предлагаемые способы решения этих проблем. В соответствующей таблице может использоваться, скажем, 80символьное поле произвольного текста — тема (суть проблемы) и большое текстовое поле с описанием проблемы и способов ее решения. Кроме того, в таблице могут быть и другие столбцы со структурированной информацией, например датой поступления сообщения о проблеме, кодом аналитика, который над ней работает, кодом программного продукта, с которым связана проблема, и т.п. Мы имеем сочетание текста (по определению не являющегося документом) и структурированных данных. К этой таблице будут часто выполняться запросы типа "найти все проблемы, связанные с СУБД (программный продукт) версии 8.1.6 (еще один атрибут), где упоминается ошибка ORA-01555 в поле темы (текстовый поиск)". Именно для таких приложений и был создан индекскаталог. Как и можно было ожидать от "урезанной" версии полного индекса, индекс ctxcat имеет ряд ограничений. Поддерживаемые этим индексом операторы запросов являются подмножеством операторов "полного" индекса interMedia Text. Данные, которые индексируются с помощью индекса ctxcat, должны содержаться в базе данных Oracle в текстовом виде. Более того, индекс ctxcat не допускает использование нескольких языков в одном индексе. Однако даже с учетом этих ограничений индекс-каталог обеспечивает отличную производительность для многих приложений.
256
Глава 17
Одна из приятных особенностей индекса-каталога — его не надо поддерживать. Операторы DML применяются к этому индексу в рамках транзакции. Поэтому не нужно периодически синхронизировать индекс или запускать процесс ctxsrv для синхронизации в фоновом режиме. Еще одна серьезная причина использовать индекс-каталог связанна с присущей ему поддержкой структурированных запросов. Разработчик приложений может создавать наборы индексирования (index sets), которые позволяют с помощью индекса-каталога эффективно поддерживать как текстовый поиск, так и запросы к другим структурированным данным. Набор индексирования позволяет компоненту interMedia сохранять в индексе структурированную реляционную информацию вместе с индексируемым текстом. Это позволяет компоненту interMedia одновременно использовать поиск текста и поиск структурированной информации для нахождения документов, удовлетворяющих специфическим критериям. Рассмотрим небольшой пример: SQL> create table mynews 2 (id number primary key, 3 date_created date, 4 news_text varchar2(4000)) 5 / Table created. SQL> insert into mynews 2 values(1, '01-JAN-1990', 'Oracle is doing well') 3 / 1 row created. SQL> insert into mynews 2 values(2, '01-JAN-2001', 'I am looking forward to 9i') 3 / 1 row created. SQL> commit; Commit complete. SQL> begin 2 ctx_ddl.create_index_set('news_index_set'); 3 ctx_ddl.add_index('news_index_set', 'date_created'); 4 end; 5 / SQL> create index news_idx on mynews(news_text) 2 indextype is ctxsys.ctxcat 3 parameters('index set news_index_set') 4 / Index created. Обратите внимание, что для создания индекса-каталога указан тип индекса CTXSYS.CTXCAT. Кроме того, я создал набор индексирования NEWS_INDEX_.SET и добавил в него столбец DATE_CREATED. Это позволит компоненту interMedia Text эффективно обрабатывать запросы, содержащие условия для обоих столбцов: NEWS_TEXT и DATE CREATED.
interMedia
257
SQL> select id 2 from mynews 3 where catsearch(news_text, 'Oracle', null) > 0 4 and date created < sysdate 5 / ~ ID I SQL> select id 2 from mynews 3 where catsearch(news_text, 'Oracle', 'date_created < sysdate1) > 0 4
/ ID _
Здесь мы видим оба метода поиска строк, содержащих слово Oracle в тексте сообщения, дата внесения которого, DATE_CREATED, предшествует текущему дню. Первый запрос сначала использует interMedia для поиска всех строк, которые могут удовлетворять запросу, а затем просматривает их в поисках тех, у которых значение DATE_CREATED меньше, чем SYSDATE. Второй, более эффективный запрос, будет использовать индекс interMedia, включающий столбец DATE_CREATED, для поиска только тех строк, которые одновременно удовлетворяют критерию поиска текста и условию DATE_CREATED < SYSDATE. При поиске по индексу-каталогу вместо оператора CONTAINS используется оператор CATSEARCH. Поскольку ранее был создан набор индексирования, содержащий столбец DATE_CREATED, стало возможным задать структурированное условие запроса непосредственно в операторе CATSEARCH. С помощью этой информации сервер Oracle может очень эффективно проверить оба условия запроса. На условия, которые можно указывать в операторе CATSEARCH, налагается ряд ограничений, в частности поддерживаются только логические операторы AND, OR и NOT, но для множества приложений этот индекс не только подходит, но и является оптимальным.
Возможные ошибки Общаясь с разработчиками приложений и клиентами, использующими компонент interMedia Text, я постоянно слышу об одном и том же небольшом наборе проблем. Эти проблемы рассматриваются в данном разделе.
Устаревший индекс Часто ко мне обращались с вопросом, почему часть информации проиндексирована, но недавно добавленные строки индексом не учитываются. Чаще всего причина этого в том, что индекс interMedia Text не синхронизирован. Выполнив запрос к представлению CTX_USER_PENDING, легко определить, есть ли отложенные изменения, ожи-
9
Чаг 244
258
Глава 17
дающие индексирования. Если есть, синхронизируйте индекс с помощью одного из описанных ранее методов. Еще одна типичная причина отсутствия информации в индексе связана с ошибками, особенно в процессе фильтрования. При попытке индексирования документов с помощью фильтров Inso, если формат документа не поддерживается, возникают ошибки, и соответствующий документ не индексируется. В представлении CTX_USER_INDEX_ERRORS можно найти достаточно информации, чтобы выяснить причины возникновения проблем при индексировании.
Ошибки внешней процедуры В версии компонента interMedia Text, поставлявшейся в составе СУБД Oracle 8.1.5 и 8.1.6, фильтрование текста выполнялось с помощью фильтров Inso, подключаемых через внешние процедуры. Внешние процедуры — это написанные на языке С функции, хранящиеся в разделяемой библиотеке и вызываемые из PL/SQL. Эти внешние процедуры работают в отдельном адресном пространстве, а не в адресном пространстве сервера Oracle. Если необходимо фильтровать документы с помощью компонента interMedia Text, но поддержка внешних процедур не сконфигурирована на сервере надлежащим образом, возможно получение одного или нескольких сообщений об ошибках*: ORA-29855: e r r o r occurred in the execution of ODCIINDEXCREATE routine ORA-29855: возникла ошибка при выполнении программы ODCIINDEXCREATE ORA-20000: interMedia t e x t e r r o r : DRG-50704: Net8 l i s t e n e r i s not running or cannot s t a r t external procedures ORA-28575: unable t o open RPC connection t o external procedure agent ORA-28575: невозможно открыть соединение RPC с агентом внешней процедуры Исчерпывающую информацию о конфигурировании сервера для поддержки внешних процедур можно найти в руководстве Net8 Administrator's Guide, но вот очень простой и быстрый способ проверить, работают ли внешние процедуры. Организовав с сервера базы данных (или из окна командной строки, если сервер работает на платформе Microsoft Windows) сеанс telnet, выполните команду tnsping extproc_connection_data. Если в результате вы не получите ответ ОК, как показано ниже: oracle8i@cmh:/> tnsping extproc_connection_data TNS Ping U t i l i t y for S o l a r i s : Version 8.1.7.0.0 - Production on 30-MAR2001 13:46:59 (c) Copyright 1997 Oracle Corporation.
All r i g h t s reserved.
Attempting t o contact (ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC)) OK (100 msec) значит, поддержка внешних процедур сконфигурирована неправильно. * Представлены также тексты сообщений об ошибках, выдаваемые на русском языке сервером Oracle 8.1.6.0.0. Прим. научн. ред.
interMedia
2JУ
Внешние процедуры не нужны для фильтрования с помощью interMedia Text в версии Oracle 8.I.7. Процесс фильтрования теперь поддерживается самим сервером.
Дальнейшее развитие С выходом Oracle 9i название компонента interMedia Text снова изменилось — теперь соответствующий компонент называется Oracle Text. Все функциональные возможности, которые были в версиях interMedia Text для Oracle 8i, будут поддерживаться и в Oracle Text, но в версию Oracle 9i добавлен ряд новых полезных возможностей. Одной из наиболее желательных возможностей для компонента interMedia Text была автоматическая классификация документов по содержанию. Компонент interMedia Text давал разработчику приложений все необходимые средства для тематического анализа и создания резюме документов, на основе которых можно было их классифицировать. В Oracle Text возможность классификации документов просто встроена. Она помогает создавать системы, позволяющие определить, каким запросам будет соответствовать документ. В компоненте Oracle Text также расширена встроенная поддержка XML-документов. Кроме собственно содержимого, в XML-документ входят структурированные метаданные. Между элементами XML-документа имеются неявные взаимосвязи, и для выражения этих взаимосвязей можно использовать структурированные метаданные. Спецификация XPath, рекомендованная консорциумом W3C (http://www.w3.org/TR/xpath), предлагает способ получения элементов XML-документа на основе содержимого и относительной структуры этих элементов. Компонент Oracle Text включает новый оператор ХРАТН, позволяющий создавать SQL-запросы на основе структурных элементов документа. Это лишь некоторые из новых возможностей, появившихся в компоненте interMedia Text/Oracle Text с выходом сервера версии Oracle 9i.
Резюме В этой главе мы рассмотрели богатый набор функциональных возможностей компонента interMedia Text, а также способы их использования в разнообразных приложениях. Хотя в этой главе описаны многие особенности и средства interMedia Text, гораздо больше осталось за рамками обсуждения. Можно использовать тезаурус, задавать специализированный лексический анализатор, генерировать HTML-представление для всех документов, независимо от их исходного формата, сохранять запросы для дальнейшего использования и даже создавать собственные списки стоп-слов. Компонент interMedia Text — обширная тема для обсуждения, его невозможно описать в одной главе. Наряду с богатством возможностей компонент interMedia Text отличается простой использования и понятностью. После прочтения этой главы у вас должно сложиться четкое понимание того, как реализован компонент interMedia Text и как его использовать в приложениях.
ionalDracleP пь. ngProf esslonal©! -*•"' • ,»hi!tiIngProfess ^Programming? vracleProgra sionalOracle ProfessiOi 'arnraingProf e^ ""•""""• igrsi'':;., i n g
LOracieProgn - r, in
]
.]
Внешние процедуры на языке С Вначале в качестве языка программирования сервера Oracle и клиентских приложений, работающих на самом сервере Oracle, использовался исключительно PL/SQL. В версии Oracle 8.0 появилась возможность создавать хранимые процедуры на других языках. Эта возможность — поддержка внешних процедур — распространяется на хранимые процедуры на языке С (и все процедуры, которые можно вызвать из языка С) и на языке Java. В этой главе мы сконцентрируемся исключительно на языке С, а следующая глава посвящена использованию Java. В этой главе внешние процедуры рассматриваются с точки зрения архитектуры и показано, как они были реализованы разработчиками ядра Oracle. Кроме того, мы разберемся, как сконфигурировать сервер для поддержки внешних процедур и как должен конфигурироваться сервер с учетом защиты. Я продемонстрирую, как написать внешнюю процедуру с помощью прекомпилятора Oracle Pro*C. Эта внешняя процедура будет использоваться для записи содержимого любого большого объекта в файловую систему сервера. Однако прежде чем перейти к этому примеру, рассмотрим базовый пример, демонстрирующий, как передавать значения основных типов данных из языка PL/SQL в функции на языке С и получать результаты. Этот базовый пример позволит также создать шаблон для быстрой разработки всех последующих внешних процедур на языке С. Вы узнаете, как создавать код на языке С с учетом возможности использования его для выполнения сервером Oracle. Мы также рассмотрим реализацию SQL-оболочки для внешней процедуры. Я расскажу, как обеспечить возможность ее вызова из языков SQL и PL/SQL и как программировать оболочку, чтобы ее легко было использовать тем, кому нужен соответствующий код. Наконец, мы рассмотрим преимущества и недостатки вне-
262
Глава 18
шних процедур, а также различные ошибки сервера (ошибки ORA-XXXX), которые могут возникнуть при их использовании. В примере на языке Рго*С будет создаваться недостающее средство сервера. В составе сервера Oracle поставляется пакет DBMS_LOB для работы с большими объектами. В этом пакете есть процедура loadfromfile, позволяющая читать в большой объект базы данных содержимое любого файла в файловой системе сервера. Однако в этом пакете нет функции writetofile, которая могла бы записывать в файловую систему содержимое большого объекта, а такая потребность часто возникает. Мы решим эту задачу, создав собственный пакет LOB_IO. Этот пакет позволит записывать любой большой объект типа BLOB, CLOB или BFILE в отдельный файл вне базы данных (так что для объектов типа BFILE мы по сути создадим команду копирования, поскольку исходный объект BFILE уже находится вне базы данных).
Когда используются внешние процедуры? Один язык или одна среда не может обеспечить средства и функции на все случаи жизни. У каждого языка есть недостатки: не все можното сделать с его помощью, недостает возможностей или средств, о которых разработчики не подумали. При разработке программ на языке С мне иногда приходится программировать на ассемблере. При программировании приложений для Oracle на Java иногда удобно использовать код на языке PL/SQL. Суть в том, что не нужно всегда использовать язык "низкого уровня" — иногда необходимо переходить на более высокий уровень. Внешние процедуры можно считать переходом на более "низкоуровневый" язык. Они обычно используются для интеграции существующего кода на языке С в виде библиотек функций (например, DLL — динамически компонуемых библиотек в Windows, созданных сторонним производителем, которые необходимо вызывать с сервера) или для расширения функциональных возможностей существующих пакетов, как в нашем случае. Именно эту технологию используют разработчики сервера Oracle для расширения его возможностей. Например, мы уже рассмотрели, как эта возможность использовалась в компоненте interMedia (в предыдущей главе) и в пакете DBMS_OLAP (в главе 13, посвященной материализованным представлениям). Первая хранимая процедура, которую я написал, представляла собой реализацию простого клиента TCP/IP. С ее помощью в версии сервера 8.0.3 я получил возможность в PL/SQL открывать сокет TCP/IP для подключения к существующему серверу и обмена сообщениями с ним. Я мог подключаться к серверу дискуссионных групп по протоколу Net News Transport Protocol (NNTP), к серверу электронной почты по протоколам Internet Message Access Protocol (IMAP), Simple Mail Transfer Protocol (SMTP) или Post Office Protocol (POP), к Web-серверу и т.д. "Научив" язык PL/SQL использовать сокеты, я открыл широкий спектр новых возможностей. Теперь я мог: •
посылать сообщение электронной почты из триггера с помощью протокола SMTP;
•
включать сообщения электронной почты в базу данных с помощью протокола POP;
Внешние процедуры на языке С
263
Q индексировать сообщения дискуссионных групп с помощью компонента interMedia Text и протокола NNTP; •
обращаться к любой доступной сетевой службе.
Я стал использовать сервер Oracle также в качестве клиента прочих серверов. После включения полученных от них данных в свою базу я мог выполнять с ними множество действий (индексировать, выполнять поиск, представлять в другом виде и т.д.). Со временем это средство начали использовать настолько часто, что теперь оно стало интегрированной возможностью сервера. Начиная с версии 8.1.6 сервера Oracle, все возможности, которые обеспечивал простой клиент TCP/IP, теперь реализуются в пакете UTLJTCP. С тех пор я написал еще несколько внешних процедур. Одни — для получения времени с помощью системных часов с большей точностью, чем возвращает встроенная функция SYSDATE, другие — для выполнения команд операционной системы, определения часового пояса системы или для получения списка файлов в указанном каталоге. Последней мной написана функция для записи во внешний файл содержимого любого большого объекта: символьного (Character LOB — CLOB), двоичного (Binary LOB — BLOB) или хранящегося во внешнем файле (BFILE). Полезным побочным эффектом созданного при этом пакета является обеспечение для двоичных файлов возможностей, предоставляемых пакетом UTL_FILE (пакет UTL_FILE не позволяет создавать двоичные файлы). Поскольку сервер поддерживает временные большие объекты (Temporary LOB) и обеспечивает возможность записи (WRITE) во временный, большой объект, этот новый пакет, который мы собираемся реализовать, даст возможность записывать из PL/SQL любой двоичный файл. Итак, этот пакет позволит: •
экспортировать любой большой объект во внешний файл на сервере;
•
записывать на сервере двоичный файл практически любого размера и с любыми данными (аналогично пакету UTL_FILE, который работает с текстовыми данными, но работу с произвольными двоичными данными не поддерживает).
Из всего сказаного ясно, что причины использования внешних процедур могут быть многочисленны и разнообразны. Обычно они используются для: Q реализации отсутствующих функциональных возможностей; •
интегрирования существующего кода, который проверяет корректность данных;
Q ускорения обработки; скомпилированный код на языке С всегда будет выполнять операции, требующие большого объема вычислений, быстрее, чем при реализации их на интерпретируемых языках PL/SQL или Java. Как обычно, решение использовать что-то вроде хранимых процедур требует определенных издержек. Дополнительные издержки связаны с разработкой кода на языке С, что, как мне кажется, сложнее, чем разработка на PL/SQL. Приходится также жертвовать переносимостью или даже идти на потенциальную невозможность переноса кода. Если разработана DLL-библиотека для Windows, нет гарантии, что написанный исходный код можно будет использовать на UNIX-машине, и наоборот. Я считаю, что внешнюю процедуру надо использовать лишь тогда, когда невозможно решить задачу с помощью языка PL/SQL.
264
Глава 18
Как реализована поддержка внешних процедур? Внешние процедуры выполняются процессом, физически отделенным от процессов сервера. Это сделано из соображений защиты. Хотя технически можно динамически загружать DLL-библиотеку (в Windows) или файл .so (Shared Object code — разделяемый объектный код, скажем, в Solaris) во время выполнения и в существующих процессах сервера, при этом сервер будет подвергаться неоправданному риску. Код библиотеки будет иметь доступ к тому же пространству памяти, что и процессы сервера, в том числе и к системной глобальной области Oracle (SGA). В результате этот посторонний код может случайно повредить базовые структуры данных СУБД, что приведет к потере данных или сбою экземпляра. Чтобы избежать этого, внешние процедуры выполняются отдельными процессами, не использующими разделяемые области памяти сервера. В большинстве случаев отдельный процесс будет работать от имени пользователя, не являющегося владельцем программного обеспечения Oracle. Причина та же, что и в случае выполнения внешних процедур отдельным процессом — безопасность. Пусть, например, мы собираемся создать внешнюю процедуру, которая сможет записывать файлы на диск (как это делает процедура, рассматриваемая далее). Предположим, сервер работает в среде UNIX, и внешняя процедура выполняется от имени владельца программного обеспечения Oracle. Пользователь вызывает новую функцию и "просит" ее записать объект BLOB в файл /dOl/oracle/data/system.dbf. Поскольку этот код выполняется от имени пользователя — владельца программного обеспечения Oracle, этот вызов сработает и непреднамеренно запишет содержимое какого-то большого двоичного объекта вместо системного табличного пространства. Мы можем этого даже и не заметить до момента остановки и перезапуска сервера (много дней спустя). Если бы внешняя процедура выполнялась от имени менее привилегированного пользователя, это не могло бы случиться (этот пользователь не имел бы права на запись файла system.dbf). Поэтому в разделе, посвященном конфигурированию сервера для поддержки внешних процедур, мы рассмотрим, как настроить безопасный процесс прослушивания EXTPROC (EXTernal PROCedure), работающий от имени другой учетной записи ОС. Причины такой настройки примерно те же, что и в случае запуска Web-серверов от имени пользователя nobody в UNIX или от имени учетной записи с минимальными привилегиями в Windows. Итак, при вызове внешней процедуры сервер Oracle будет автоматически создавать процесс ОС под названием EXTPROC. Для этого он связывается с процессом прослушивания Net8 (Net8 listener). Процесс прослушивания Net8 будет автоматически создавать процесс EXTPROC точно так же, как он порождает выделенные или разделяемые серверы. В среде Windows NT это можно увидеть с помощью утилиты tlist из набора NT Resource Toolkit, выдающей дерево процессов и подпроцессов. Например, я запустил сеанс, из которого обратился к внешней процедуре, а затем выполнил команду tlist -t и получил следующее: C:\bin>tlist -t System Process (0) System (8) smss.exe (140) csrss.exe (164)
Внешние процедуры на языке С
265
winlogon.exe (160) services.exe (212) svchost.exe (384) SPOOLSV.EXE (412) svchost.exe (444) regsvc.exe (512) s t i s v c . e x e (600) ORACLE.EXE (1024) ORADIM.EXE (1264) TNSLSNR.EXE (1188) EXTPROC.EXE (972)
lsass.exe
(224)
Это показывает, что процесс TNSLSNR.EXE является родительским для процесса EXTPROC.EXE. Процесс EXTPROC и процесс сервера теперь могут взаимодействовать. Что еще важнее, процесс EXTPROC может динамически загружать пользовательские DLL-библиотеки (или файлы .so/.sl/.a в ОС UNIX). Архитектурно это выглядит следующим образом:
', DLL-библиотека !
Происходит следующее. 1. Пользовать подключается к СУБД. При этом либо запускается процесс выделенного сервера, либо используется один из разделяемых серверных процессов. 2. Пользователь вызывает внешнюю процедуру. Поскольку это первый вызов, серверный процесс связывается с процессом TNSLISTENER (процессом прослушивания Net8). 3. Процесс прослушивания Net8 запускает (или находит в пуле запущенных свободный) процесс выполнения внешних процедур для сеанса. Этот процесс загружает запрошенную DLL-библиотеку (или файл .so/.sl/.a в ОС UNIX). 4. Теперь можно взаимодействовать с процессом выполнения внешних процедур, который будет обеспечивать обмен данными между языками SQL и С.
Конфигурирование сервера Сейчас я опишу настройку реквизитов, которую необходимо провести, чтобы обеспечить выполнение внешних процедур. Для этого придется настраивать файлы
266
Глава 18
LISTENER.ORA и TNSNAMES.ORA на сервере, а не на клиентской машине. После полной установки эти файлы должны быть автоматически сконфигурированы для поддержки служб внешних процедур (EXTPROC). В этом случае конфигурационный файл LISTENER.ORA будет иметь примерно такой вид: # LISTENER.ORA Network Configuration File: # C:\oracle\network\admin\LISTENER.ORA # Generated by Oracle configuration tools. LISTENER = (DESCRIPTION_LIST = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL - TCP)(HOST = tkyte-del)(PORT = 1521)) ) (ADDRESS_LIST = (ADDRESS - (PROTOCOL = IPC)(KEY = EXTPROC1))
SID_LIST_LISTENER = (SID_LIST = (SID_DESC = (SID_NAME = PLSExtProc) (ORACLE_HOME = C:\oracle) (PROGRAM = extproc) ) (SID_DESC = (GLOBAL_DBNAME = t k y t e 8 1 6 ) (ORACLE_HOME = C : \ o r a c l e ) (SID_NAME = t k y t e 8 1 6 )
Следующие установки в файле процесса прослушивания существенны для использования внешних процедур. Q (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1)). Задает IPC-адрес. Запомните значение KEY. Это значение может быть произвольным, просто запомните его. В некоторых системах регистр символов в значении KEY существенен, что тоже необходимо учитывать. Q (SID_DESC = (SID_NAME = PLSExtProc,). Запомните значение SID_NAME, PLSExtProc или что-то подобное. По умолчанию это значение SID будет равно PLSExtProc. Файл LISTENER.ORA можно сконфигурировать и вручную с помощью простого текстового редактора или с помощью программы Net8 Assistant. Настоятельно рекомендуется использовать программу Net8 Assistant, поскольку минимальная ошибка в конфигурационном файле, например незакрытая круглая скобка, сделает его бесполезным. При использовании Net8 Assistant следуйте процедуре, описанной в системе оператив-
Внешние процедуры на языке С
267
ной справки в разделе NetAssistantHelp, Local, Listeners, How To..., и Configure External Procedures for the Listener. После изменения файла LISTENER.ORA не забудьте остановить и запустить процесс прослушивания с помощью команд Isnrctl stop и lsnrctl start из командной строки. Следующий конфигурационный файл — TNSNAMES.ORA. Этот файл должен находиться в каталоге, который сервер будет использовать при разрешении имен. Обычно файл TNSNAMES.ORA находится на клиенте и используется для поиска сервера. В данном случае сервер сам должен найти службу по имени. Файл TNSNAMES.ORA будет иметь примерно такой вид: # TNSNAMES.ORA Network Configuration # File:С:\oracle\network\admin\TNSNAMES.ORA # Generated by Oracle configuration t o o l s . EXTPROC_CONNECTION_DATA =
(DESCRIPTION = (ADDRESS_LIST (ADDRESS - (PROTOCOL = IPC)(KEY = EXTPROC1)) (CONNECT_DATA (SID = PLSExtProc) (PRESENTATION = RO)
)
В этом конфигурационном файле существенно следующее. •
EXTPROC_CONNECTION_DATA. Имя службы, которую будет искать сервер. Это имя использовать обязательно. Далее будет рассмотрена проблема, связанная с установкой параметра names.default_domain в конфигурационном файле SQLNET.ORA.
•
(ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1)). Этот параметр должен быть таким же, как в файле LISTENER.ORA. В частности, значения компонента KEY = должны совпадать.
•
(CONNECT_DATA =(SID = PLSExtProc). Значение после SID = должно соот-
ветствовать значению SID в конструкции (SID_DESC = (SID_NAME = PLSExtProc) в файле LISTENER.ORA. С именем EXTPROC_CONNECTION_DATA связана следующая проблема. Если в файле SQLNET.ORA задан стандартный домен, он должен быть включен в запись TNSNAMES. Так что, если в файле SQLNET.ORA есть следующая установка: names.default_domain = world необходимо задать в файле TNSNAMES.ORA значение EXTPROC_CONNECTION_DATA.world, а не просто EXTPROC_CONNECTION_DATA. Любые ошибки в представленных выше конфигурационных файлах почти наверняка приведут к выдаче сообщения об ошибке ORA-28575, представленного ниже: declare *
268
Глава 18
ERROR a t l i n e 1: ORA-28575: unable to open RPC connection to external procedure agent ORA-06512: a t "USERNAME.PROCEDURE_NAME", l i n e 0 ORA-06512: a t l i n e 5
При получении этого сообщения об ошибке имеет смысл проверить следующее: • доступна ли программа extproc и является ли она выполняемой; Q правильно ли настроена среда сервера для использования внешних процедур; • правильно ли сконфигурирован процесс прослушивания. Сейчас мы детально рассмотрим каждый из этих шагов. Предполагается, что вы уже получили сообщение об ошибке ORA-28575 по какой-то причине.
Проверка программы extproc Если после конфигурирования поддержки внешних процедур вы получаете сообщение об ошибке ORA-28575, прежде всего необходимо проверить наличие программы extproc и возможность ее выполнения. Это легко сделать из командной строки — как в Windows, так и в UNIX. Это надо делать, зарегистрировавшись как пользователь, от имени которого будет запускаться процесс прослушивания (поскольку именно этот процесс и будет выполнять программу extproc), чтобы убедиться, что этот пользователь имеет все необходимые права. Затем нужно выполнить следующую команду: С:\oracle\BIN>.\extproc.ехе Oracle Corporation SATURDAY AUG 05 2000 14:57:19.851 Heterogeneous Agent based on the following module(s): — External Procedure Module C:\oracle\BIN> Результат работы должен быть подобен представленному выше. Обратите внимание, что я выполнял команду из каталога [ORACLE_HOME]\bin, поскольку именно в нем и находится программа extproc.exe. Если выполнить эту программу не удается, значит, установка выполнена некорректно или необходимо исправить конфигурацию на уровне операционной системы.
Проверка среды сервера В среде сервера нужно проверить несколько установок. Прежде всего проверьте, используется ли соответствующий файл TNSNAMES.ORA и правильно ли он сконфигурирован. Например, на моей UNIX-машине с помощью программы truss я получил следующую информацию: $ setenv TNS_ADMIN /tmp $ t r u s s sqlplus /@ora8i.us.oracle.com
|& grep TNSNAMES
access("/export/home/tkyte/.TNSNAMES.ORA", access("/tmp/TNSNAMES.ORA", 0)
0)
Err#2 ENOENT Err#2 ENOENT
Внешние процедуры на языке С
2лУу
access("/var/opt/oracle/TNSNAMES.ORA", 0) Err#2 ENOENT access("/export/home/oracle8i/network/admin/TNSNAMES.0RA", 0) = 0 Итак, сервер Oracle искал файл TNSNAMES.ORA: • в моем начальном каталоге; •
•
в каталоге, задаваемом переменной среды TNS_ADMIN;
• в каталоге /var/opt/oracle; •
и, наконец, в каталоге $ORACLE_HOME/network/admin/.
Типичная ошибка: файл TNSNAMES.ORA
конфигурируется в каталоге
[ORACLE_HOME]/network/admin, но установлена переменная среды TNS_ADMIN, которая заставляет сервер Oracle искать файл TNSNAMES.ORA в другом месте. Поэтому проверьте, используется ли соответствующий файл TNSNAMES.ORA (чтобы добиться однозначности, просто установите переменной среды TNS_ADMIN требуемое значение перед запуском сервера; это гарантирует, что сервер Oracle использует именно тот экземпляр файла, который необходимо). Убедившись, что используется соответствующий файл TNSNAMES.ORA, надо поискать ошибки конфигурирования в этом файле. Следует проверить в соответствии с представленными выше примерами, настроены ли компоненты (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1)) и (CONNECT_DATA =(SID = PLSExtProc)) корректно. Сравните их с содержимым файла LISTENER.ORA. Если для установки использовалась программа Net8 Assistant, заботиться о соответствии круглых скобок не придется. Если файл редактируется вручную, будьте очень внимательны. Одна незакрытая или стоящая не на своем месте круглая скобка не позволит использовать соответствующую запись. Проверив корректность этих компонентов, обратите внимание на имя записи в файле TNSNAMES.ORA. Это должна быть запись EXTPROC_CONNECTION_DATA. Никакое другое имя не подходит (к имени записи может быть только добавлено имя домена). Проверьте правильность написания имени. Одновременно сверьтесь с конфигурационным файлом SQLNET.ORA. Сервер Oracle ищет файл SQLNET.ORA так же, как и файл TNSNAMES.ORA. Учтите, что он необязательно находится в том же каталоге, что и файл TNSNAMES.ORA, — он может быть и в других местах. Если установлен любой из параметров: U names.directory_path Q names.default domain необходимо убедиться, что файл TNSNAMES.ORA им соответствует. Если параметр names.default_domain имеет непустое значение, например (WORLD), необходимо проверить, указан ли этот домен в соответствующей записи файла TNSNAMES.ORA. Вместо EXTPROC_CONNECTION_DATA, в качестве имени записи в этом случае должно быть указано EXTPROC_CONNECTION_DATA.WORLD. Если установлен параметр names.directory_path, необходимо убедиться, что он содержит значение TNSNAMES. Если параметр names.directory_path имеет, например, значение (HOSTNAME,ONAMES), то протокол Net8 будет использовать имена хостов (Host
270
Глава 18
Naming Method) при обработке строки подключения EXTPROC_CONNECTION_DATA, a если найти параметры подключения таким образом не удастся, — обратится к серверу Oracle Names Server. Поскольку ни один из этих методов не позволит найти запись EXTPROC_CONNECTION_DATA, подключение не будет установлено, и вызов extproc завершится неудачно. Просто добавьте в этот список слово TNSNAMES, чтобы сервер Oracle мог найти запись EXTPROC_CONNECTION_DATA в локальном файле TNSNAMES.ORA.
Проверка процесса прослушивания Проблемы с прослушиванием похожи на проблемы с установкой среды для сервера. При проверке конфигурации процесса прослушивания обратите внимание на следующее: • используется ли соответствующий конфигурационный файл LISTENER.ORA? • правильно ли настроен этот файл? Снова, как и при проверке среды сервера, необходимо убедиться, что для прослушивания используется соответствующая среда, позволяющая ему найти требуемый файл LISTENER.ORA. Возникают те же проблемы, связанные с поиском конфигурационных файлов в разных местах. В случае сомнений, какой именно набор конфигурационных файлов используется, следует установить соответствующее значение переменной среды TNS_ADMIN перед запуском процесса прослушивания. Это гарантирует использование соответствующих конфигурационных файлов. Убедившись, что используются нужные конфигурационные файлы, необходимо проверить содержимое файла LISTENER.ORA в соответствии с представленной ранее информацией. После этого проверка из среды сервера доступности службы extproc_connection_data (с добавлением стандартного домена) с помощью программы tnsping должна завершиться успешно. Например, у меня используется стандартный домен us.oracle.com, и проверка доступности дает следующий результат: С:\oracle\network\ADMIN>tnsping
extproc_connection_data.us.oracle.com
TNS Ping U t i l i t y for 32-bit Windows: Version 8.1.6.0.0 - Production on 06-AUG-2000 09:34:32 (c) Copyright 1997 Oracle Corporation.
All r i g h t s reserved.
Attempting t o contact (ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC1)) OK (40 msec) Это подтверждает, что среда сервера базы данных и процесса прослушивания сконфигурирована правильно. Не должно быть никаких сообщений об ошибке ORA-28575: unable to open RPC connection to external procedure agent (ORA-28575: невозможно открыть соединение RPC с агентом внешней процедуры).
Первая проверка Рекомендуется проверить правильность выполнения внешних процедур с помощью демонстрационной программы. Для этого имеются две причины.
Внешние процедуры на языке С
2,/х
Q Служба поддержки Oracle поможет настроить/сконфигурировать демонстрационную программу. Если служба поддержки работает со знакомым примером, то сможет решить любые проблемы намного быстрее. •
Предлагаемая демонстрационная программа демонстрирует правильный подход к компиляции и компоновке на данной платформе.
Демонстрационная программа находится в каталоге [ORACLE_HOME]/plsql/demo во всех версиях Oracle 8i. Шаги, которые необходимо выполнить для создания демонстрационной программы, описаны в следующих разделах.
Компиляция кода extproc.c Сначала необходимо скомпилировать код extproc.c в DLL-библиотеку или файл .so/ .sl/.a. Чтобы сделать это в Windows, достаточно перейти в каталог ORACLE_HOME\ plsql\demo и набрать make (в этом каталоге корпорация Oracle поставляет файл make.bat): С:\oracle\plsql\demo>make Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 10.00.5270 for 80x86 Copyright (C) Microsoft Corp 1984-1995. All r i g h t s reserved. extproc.с Microsoft (R) 32-Bit Incremental Linker Version 3.00.5270 Copyright (C) Microsoft Corp 1992-1995. All r i g h t s reserved. /out:extproc.dll /dll /implib:extproc.lib /debug ..\..\oci\lib\msvc\oci.lib msvcrt.lib /nod:libcmt /DLL /EXPORT:UpdateSalary /EXPORT:PercentComm /EXPORT:PercentComm ByRef /EXPORT:EmpExp /EXPORT:CheckEmpName /EXPORT:LobDemo extproc.obj С:\oracle\plsql\demo> В ОС UNIX делается практически то же самое, но для компиляции необходимо ввести другую команду. Вот как она должна выглядеть: $ make -£ demo_plsql.mk extproc.so /usr/ccs/bin/make -f /export/home/ora816/rdbrns/demo/demo_rdbms.mk extproc_callback SHARED_LIBNAME=extproc.so OBJS="extproc.o" После завершения работы команды будет получен файл с расширением .dll в Windows или с расширением .so/.sl/.a в ОС UNIX; расширение зависит от платформы. Например, в ОС Solaris используется расширение .so, а в ОС HP/UX — .si.
272 Глава 18 Настройка учетной записи SCOTT/TIGER Чтобы эта демонстрационная программа работала правильно, необходимо создать демонстрационную учетную запись SCOTT/TIGER. Если в базе данных нет учетной записи SCOTT/TIGER, ее можно создать с помощью оператора: SQL> grant connect, resource t o s c o t t i d e n t i f i e d by t i g e r ; При этом создается пользователь SCOTT, которому предоставляются привилегии подключения и создания объектов (таблиц, пакетов и т.п.). Имеет смысл задать для этого пользователя другое стандартное табличное пространство вместо SYSTEM, а также явно задать временное табличное пространство. SQL> a l t e r user s c o t t default tablespace t o o l s temporary tablespace temp; При наличии учетной записи SCOTT/TIGER, прежде чем продолжать, необходимо предоставить ей еще одну дополнительную привилегию. Пользователю SCOTT необходима привилегия CREATE LIBRARY. Она позволит ему выполнить оператор CREATE LIBRARY, необходимый для использования внешних процедур. К этому оператору мы еще вернемся. Поскольку привилегия эта — весьма мощная, имеет смысл отобрать ее у пользователя SCOTT после выполнения примера. Для предоставления привилегии необходимо выполнить следующий оператор: SQL> grant c r e a t e l i b r a r y t o SCOTT; подключившись как один из пользователей, имеющих привилегию CREATE LIBRARY с опцией ADMIN (например, как пользователь SYSTEM или любой другой пользователь, которому предоставлена роль DBA). Наконец, необходимо убедиться, что в схеме SCOTT созданы и наполнены данными демонстрационные таблицы EMP/DEPT. Это можно проверить следующим образом: SQL> s e l e c t count(*) from emp; COUNT(*) 14
SQL> s e l e c t c o u n t ( * ) from d e p t ; COUNT(*)
Если эти таблицы не существуют или они не заполнены, их можно пересоздать, выполнив сценарии demodrop.sql (для удаления таблиц) и demobld.sql (для их создания и наполнения данными). Эти сценарии находятся в каталоге [ORACLE_HOME]\ sqlplus\demo и должны выполняться в среде SQL*Plus при подключении от имени пользователя SCOTT.
Создание библиотеки demolib Следующий шаг — создание объекта-библиотеки в базе данных Oracle. Этот объект просто сопоставляет имя библиотеки (любое имя длиной до 30 символов) с физическим
Внешние процедуры на языке С
2 7 3
файлом операционной системы. Этим файлом операционной системы является созданный нами скомпилированный двоичный файл. Пользователь, выполняющий оператор CREATE LIBRARY, должен обладать привилегией CREATE LIBRARY, предоставленной ему непосредственно или через роль. Эта привилегия считается достаточно мощной и должна предоставляться только тем, кто пользуется доверием. Она позволяет пользователям выполнять С-код на сервере с правами той учетной записи, от имени которой работает служба extproc. Это одна из причин, почему необходимо конфигурировать службу extproc так, чтобы она работала не от имени пользователя — владельца программного обеспечения Oracle (чтобы избежать случайного или намеренного переписывания, например, табличного пространства SYSTEM). Для выполнения этого шага необходимо ввести в среде SQL*Plus команды: SQL> connect s c o t t / t i g e r Connected. SQL> create or replace library demolib as 2 'с:\oracle\plsql\demo\extproc.dll'; 3 / Library created. Имя DEMOLIB выбрано для библиотеки разработчиками демонстрационной программы; имя DEMOLIB использовать обязательно. Имя файла, c:\oracle\plsql\demo\ extproc.dll, может отличаться (я создавал пример непосредственно в демонстрационном каталоге в ORACLE_HOME). У вас может отличаться и значение ORACLE_HOME; демонстрационную программу можно создавать вообще в другом каталоге. В операторе создания библиотеки необходимо указать фактическое местонахождение файла extproc.dll, созданного на первом шаге.
Установка и запуск Последний шаг демонстрации — установка PL/SQL-кода, создающего соответствующие подпрограммы для функций в библиотеке demolib. Нам интересен не столько их исходный код, сколько результат их выполнения. Цель демонстрации — протестировать работу внешних процедур. Как их создавать и подключать, мы рассмотрим далее. Теперь просто выполняем команды: SQL> connect scott/tiger Connected. SQL> @extproc перейдя предварительно в каталог [ORACLE_HOME]\plsql\demo. Вы должны получить примерно следующее: SQL> @extproc Package created. No errors. Package body created. No errors. ENAME : ALLEN JOB : SALESMAN
274
Глава 18
1600 SALARY 300 COMMISSION Percent Commission : 18.75 MARTIN ENAME JOB SALESMAN SALARY 1250 COMMISSION 1400 Percent Commission : 112 Return value from CheckEmpName : 0 old_ename value on return : ANIL ENAME : 7369 HIREDATE : 17-DEC-80 Employee Experience Test Passed. ***************************************
PL/SQL procedure successfully completed. ... (здесь будут и другие информационные сообщения)... Это показывает, что внешние процедуры правильно сконфигурированы и могут использоваться в системе. Первая процедура выполняет многие из функций созданной нами библиотеки extproc.dll. Поэтому можно сделать вывод, что все сконфигурировано правильно. Если система сконфигурирована неправильно, вы скорее всего получите следующее: SQL> Sextproc Package created. No errors. Package body created. No errors. BEGIN demopack.demo_procedure; END; * ERROR at line 1:
ORA-28575: unable to open RPC connection to external procedure agent ORA-06512: a t "SCOTT.DEMOPACK", l i n e 61 ORA-06512: a t "SCOTT.DEMOPACK", l i n e 103 ORA-06512: a t l i n e 1
Это означает, что надо перечитать представленный ранее раздел "Конфигурирование сервера" и выполнить все описанные в нем проверки.
Наша первая внешняя процедура Предполагая, что среда разработки настроена, как описано выше, и готова к использованию внешних процедур, попробуем создать первую собственную внешнюю процедуру. В этом примере мы просто будем передавать переменные различных типов (строки, числа, даты, массивы и т.д.) и рассмотрим, как будет выглядеть соответствующий код на языке С для получения этих значений. Внешняя процедура будет обрабатывать некоторые из этих значений, изменяя значения параметров, переданных в режиме OUT или IN/OUT, в зависимости от значений других параметров, переданных в режиме IN или IN/OUT.
Внешние процедуры на языке С
2 7 5
Я продемонстрирую свой способ сопоставления для этих переменных, поскольку есть много различных вариантов сопоставления и полезных приемов. Я покажу метод, который предпочитаю использовать, несмотря на некоторую избыточность, потому что он обеспечивает максимум информации во время выполнения. Кроме того, я представлю шаблон, по которому я создаю хранимые процедуры. Этот шаблон реализует многие конструкции, необходимые в любом реальном приложении: •
Управление состоянием. Внешние процедуры могут "потерять" информацию о состоянии (текущие значения статических или глобальных переменных). Это связано с реализованным в EXTPROC механизмом кеширования. Поэтому необходимо использовать механизм определения и сохранения состояния в программах на языке С.
•
Механизмы трассировки. Внешние процедуры выполняются на сервере отдельно от других процессов. Хотя на некоторых платформах эти процедуры можно отлаживать с помощью обычного отладчика, это весьма сложно; если же ошибки возникают только при использовании внешней процедуры одновременно многими пользователями, то просто невозможно. Необходимо средство генерации трассировочных файлов по требованию, "начиная с этого момента". Эти файлы аналогичны трассировочным файлам, которые сервер Oracle генерирует при выполнении оператора alter session set sql_trace = true; цель их создания — записать данные времени выполнения в текстовый файл для отладки/настройки.
•
Использование параметров. Необходимо средство параметризации внешних процедур, чтобы можно было управлять их работой извне, с помощью файла параметров, аналогично тому, как файл init.ora используется для управления сервером.
Q Общая обработка ошибок. Необходимо простое средство выдачи вразумительных сообщений об ошибках пользователю.
Оболочка Я собираюсь начать с PL/SQL-прототипа. Будут представлены спецификации подпрограмм на PL/SQL, которые планируется реализовать. В этом примере я собираюсь реализовать набор процедур, принимающих параметр в режиме IN и параметр в режиме OUT (или IN/OUT). Мы напишем такую процедуру для каждого существенного (часто используемого) типа данных. На примере этих процедур я продемонстрирую, как правильно передавать входные данные и получать результаты каждого из этих типов. Кроме того, я хочу создать ряд функций, возвращающих результаты некоторых из этих типов. Мне кажется, наиболее существенны следующие типы: Q строки (размером вплоть до максимально поддерживаемого языком PL/SQL, 32 Кбайта); а
числа (данные типа NUMBER, с любым масштабом и точностью);
•
даты (DATE);
Q целые числа (данные типа BINARY_INTEGER);
276
Глава 18
• данные булева типа (BOOLEAN); • данные типа R A W (размером до 32 Кбайт); Q большие объекты (для всех данных размером >32 Кбайт); • массивы строк; Q массивы чисел; Q массивы дат. Для этого необходимо сначала создать несколько типов наборов. Они будут представлять массивы строк, чисел и дат: tkyte@TKYTE816> create or replace type numArray as table of number 2 / Type created. tkyte@TKYTE816> create or replace type dateArray as table of date 2 / Type created. tkyte@TKYTE816> create or replace type strArray as table of varchar2(255) 2 / Type created. Теперь можно создавать спецификацию пакета. Она представляет собой набор перегруженных процедур для тестирования передачи параметров. Каждая процедура имеет параметры, передаваемые в режиме IN и OUT, за исключением версии для данных типа CLOB, в которой параметр передается в режиме IN/OUT. Клиент должен инициализировать параметр LOB IN OUT, а внешняя процедура заполнит этот объект: tkyte@TKYTE816> create or replace package demo_passing_pkg 2 as 3 procedure pass(p_in in number, p_out out number); 4 5 procedure pass(p_in in date, p_out out date); 6 7 procedure pass(p_in in varchar2, p_out out varchar2); 8 9 procedure pass(p_in in boolean, p_out out boolean); 10 11 procedure pass(p_in in CLOB, p_out in out CLOB); 12 13 procedure pass(p_in in numArray, p_out out numArray); 14 15 procedure pass(p_in in dateArray, p__out out dateArray); 16 17 procedure pass(p_in in strArray, p_out out strArray); Нельзя использовать перегрузку для процедур, использующих параметры типа RAW и INT, поскольку вызов PASS(RAW, RAW) будет совпадать по сигнатуре с PASS(VARCHAR2,VARCHAR2), a PASS(INTJNT) - с PASS(NUMBER,NUMBER). Поэтому для этих двух типов данных я в виде исключения создам процедуры со специальными именами:
Внешние процедуры на языке С 19 20 21 22
2 7 7
procedure pass_raw(p_in in RAW, p_out out RAW); procedure pass_int(p_in p_out
i n binary_integer, out binary_integer);
Наконец, добавим несколько функций, возвращающих значения, чтобы продемонстрировать, как они реализуются. Создадим по функции для каждого из основных скалярных типов данных: 25 function r e t u r n number return number; 26 27 function return_date return date; 28 29 function return_string return varchar2; 30 31 end demo_passing_pkg; 32 / Package created. Операторы CREATE TYPE позволяют создать необходимые типы массивов. С их помощью мы определили новые SQL-типы; numArray — вложенная таблица чисел, dateArray — вложенная таблица дат и strArray — вложенная таблица строк типа VARCHAR2(255). Мы создали также спецификацию пакета, который собираемся реализовать, так что можно приступать к реализации. Я представлю ее шаг за шагом. Начнем с создания библиотеки: tkyte@TKYTE816> c r e a t e or replace l i b r a r y 2 as 3 'C:\demo_passing\extproc.dll' 4 / Library created.
demoPassing
Этот оператор, как и в рассмотренном ранее примере, где проверялась настройка учетной записи SCOTT/TIGER, просто определяет для сервера Oracle место хранения библиотеки demoPassing; в данном случае она находится в файле C:\demo_passing\ extproc.dll. Тот факт, что эта DLL-библиотека еще не создана, не имеет значения. Объектбиблиотека необходим для компиляции тела PL/SQL-пакета, который мы собираемся создавать. Библиотеку extproc.dll мы создадим чуть позже. Оператор создания библиотеки можно успешно выполнить даже при ее отсутствии. Теперь переходим к телу пакета: tkyte@TKYTE816> c r e a t e or replace package body demo_passing_j?kg 2 as 3 4 procedure pass(p in in number, 5 p out out number) 6 as 7 language С 8 name "pass number" 9 library demoPassing 10 with context 11 parameters (
278 12 13 14 15 16
Глава 18 CONTEXT, p_in OCINumber, p_in INDICATOR short, p_out OCINumber, p_out INDICATOR short);
Итак, на первый взгляд, все начинается как обычно: оператор CREATE OR REPLACE PACKAGE и тело процедуры PROCEDURE Pass( ... ) as ..., но затем появляется отличие от обычной хранимой процедуры. Мы создаем спецификацию вызова, а не PL/SQLкод. Спецификация вызова — это метод сопоставления PL/SQL-типов встроенным типам данных языка, на котором создается внешняя процедура. Например, выше выполнено сопоставление параметра p_in number типу данных OCINumber языка С. Ниже представлено построчное описание того, что делается в данном случае. Q Строка 7: language С. Задаем язык. Создаются внешние процедуры на языке С, хотя можно их создавать и на языке Java (но этому посвящена следующая глава). Q Строка 8: name "pass_number". Задаем имя функции на языке С, которую будем вызывать из библиотеки demoPassing. Для сохранения регистра символов необходимо использовать идентификатор в кавычках (поскольку для языка С регистр символов имеет значение). Обычно все идентификаторы в Oracle переводятся в верхний регистр, но если взять их в двойные кавычки, регистр символов при записи в базу данных будет сохранен. Это имя должно точно совпадать с именем С-функции, вплоть до регистра символов. Q Строка 9: library demoPassing. Указываем имя библиотеки, которая будет содержать соответствующий код. Это имя совпадает с именем, заданным в представленном ранее операторе CREATE LIBRARY. Q Строка 10: with context. Хотя это и не обязательно, я всегда передаю контекст. Этот контекст необходим для выдачи осмысленных сообщений об ошибках и использования функций OCI или Рго*С. •
Строка 11: parameters. Начало списка параметров. Мы явно задаем последовательность и типы передаваемых параметров. Это, как и передавать контекст, делать не обязательно, но я всегда стараюсь описывать параметры явно. Вместо использования стандартных соглашений и периодического их "угадывания" я явно сообщаю серверу Oracle, какие значения и в каком порядке ожидаю получить.
• Строка 12: CONTEXT. Это ключевое слово, стоящее в списке параметров первым, требует от сервера Oracle передать в качестве первого параметр типа OCIExtProcContext *. Это ключевое слово можно задавать в любом месте списка параметров, но обычно его указывают первым. OCIExtProcContext — тип данных, определяемый функциональным интерфейсом OCI и представляющий информацию о сеансе на сервере. • Строка 13: p_in OCINumber. Я сообщаю серверу Oracle, что следующий параметр в моей С-функции должен быть типа OCINumber. В данном случае он будет передаваться как OCINumber *, указатель на данные типа OCINumber. Таблицу соответствий типов данных см. далее.
Внешние процедуры на языке С •
2,7 У
Строка 14: p_in INDICATOR short. Я сообщаю серверу Oracle, что следующий параметр в С-функции — индикаторная переменная типа short, которая позволит определить, имеет ли параметр p_in значение NULL. Хотя это необязательно, я всегда передаю индикаторную переменную вместе с каждым параметром. Если этого не делать, нельзя будет определить во внешней процедуре, передано ли значение NULL, или вернуть из нее значение NULL.
• Строка 15: p_out OCINumber. Я сообщаю серверу Oracle, что следующий после индикаторной процедуры параметр функции тоже имеет тип OCINumber. В данном случае он будет передаваться как OCINumber *. Таблицу соответствий типов данных см. далее. • Строка 16: p_out INDICATOR short. Я сообщаю серверу Oracle, что следующий параметр — индикаторная переменная для параметра p_out, и она должна быть типа short. Поскольку параметр p_out передается в режиме OUT, эта индикаторная переменная позволит мне сообщить вызывающему, имеет ли параметр, переданный в режиме OUT, значение NULL. Поэтому этот параметр передается как данные типа short * (указатель), чтобы можно было не только читать значение, но и устанавливать его. С учетом этого, давайте рассмотрим прототип С-функции, соответствующий только что созданной PL/SQL-процедуре. Этот прототип должен иметь следующий вид: 18 19 20 21 22 23 24 25
—void pass_number — ( — OCIExtProcContext — OCINumber — short — OCINumber — short -- );
* * * *
/* /* /* /* /*
1 2 3 4 5
: контекст */ : P_IN */ : P_IN (индикатор) */ : P_OUT */ : Р OUT(индикатор) */
Закончим обзор создаваемого тела PL/SQL-пакета, сопоставляющего остальные процедуры/функции. Вот процедура, передающая и принимающая даты: они будут сопоставлены типу данных OCIDate языка С, предоставляемому функциональным интерфейсом OCI: 27 28 29 30 31 32 33 34 35 36 37 38 39 40
procedure passfp in in date, p__out out date) as language С name "pass date" library demoPassing with context parameters (CONTEXT, p in OCIDate, p in INDICATOR short, p out OCIDate, p out INDICATOR short); — void pass date / — OCIExtProcContext *, /* OCIDate *, /* short , /* OCIDate *, /*
1 2 3 4
: : : :
контекст */ P_IN */ P IN (индикатор) P_OUT */
280
Глава 18
41 42
5 : Р OUT (индикатор) */
short
Давайте рассмотрим, как передавать и принимать данные типа varchar2 — а этом случае сервер будет сопоставлять строки типу данных char * — указателю на строку символов. 45 46 47 48 49 50 51 52 53 54 55 56 57 53 59 60 61
procedure pass(p_in in varchar2, p_out out varchar2) as language С name "pass_str" library demoPassing with context parameters (CONTEXT, p_in STRING, p_in INDICATOR short, p_out STRING, p_out INDICATOR short, p_out MAXLEN i n t ) ; —
void pass str / — \ — OCIExtProcContext char — short char short int
* , /* * , /* , /* * , /* * , /* * /*
1 2 3 4 5 6
контекст */ P IN */ P IN (индикатор) Ч P_OUT */ P OUT (индикатор) */ P OUT (максимальная длина) */
В этом коде мы впервые столкнулись с использованием параметра MAXLEN. Он требует от сервера Oracle передать внешней процедуре максимальный размер передаваемого в режиме OUT параметра p_out. Поскольку мы возвращаем строку, важно знать ее максимально возможную длину, чтобы предотвратить перезапись буфера. Для всех строковых типов, передаваемых в режиме OUT, я настоятельно рекомендую использовать параметр MAXLEN. Теперь давайте рассмотрим, как передавать тин PL/SQL BOOLEAN, которому будет сопоставляться тип int языка С: 64 65 66 67 68 64 70 71 72 73 74 75 76 77 78 79
procedure pass(p_in in boolean, p_out out boolean) as language С name "pass_bool" l i b r a r y demoPassing with context parameters (CONTEXT, p_in i n t , p_in INDICATOR short, p_out i n t , p_out INDICATOR s h o r t ) ; — void pass bool —
OCIExtProcContext * , int , short , int * , short *
/* /* /* /* /*
1 2 3 4 5
: : : : :
контекст */ P_IN */ P IN (индикатор) */ P_OUT */ P OUT (индикатор) */
Рассмотрим пример для типа данных CLOB. Мы передаем тип данных PL/SQL CLOB как тип данных OCILobLocator языка С. Обратите внимание, что в этом случае если
Внешние процедуры на языке С
28 1
параметр передается в режиме OUT, функция должна принимать указатель на указатель. Это позволяет в функции на языке С изменять не только содержимое, на которое указывает локатор большого объекта, но и сам этот локатор, т.е. при необходимости сослаться на другой большой объект: 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
procedure: pass(p in in clob, P.out in out clob) as language С name "pass clob" library demoPassing with context parameters (CONTEXT, p in OCILobLocator, p in INDICATOR short, p out OCILobLocator, p out INDICATOR short); —
void pass clob ( — OCIExtProcContext *, — OCILobLocator *, — short , — OCILobLocator **, — short * );
/* /* /* /* /*
1 2 3 4 5
: : : : :
контекст */ P_IN */ P_IN (индикатор) */ P_OUT */ P_OUT (индикатор) */
Затем следуют три процедуры, передающие и принимающие массивы, данные типа наборов в Oracle. Поскольку сопоставления типов во всех трех случаях очень похожи, рассмотрим все их одновременно. С-функции для этих трех процедур имеют абсолютно одинаковые прототипы — каждая получает данные типа OCIColl, независимо от передаваемого типа набора: 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
procedure pass(p_in in numArray, p_out out numArray) as language С name "pass_numArray" library demoPassing with context parameters (CONTEXT, p_in OCIColl, p_in INDICATOR short, p out OCIColl, p out INDICATOR short); — void pass_numArray — ( — OCIExtProcContext *, — OCIColl *, ' — short , — OCIColl **, — short * — );
/* /* /* /* /*
1 2 3 4 5
: : : : :
контекст */ P_IN */ P_IN (индикатор) */ P_OUT */ P_OUT (индикатор) */
procedure pass(p_in in dateArray, p_out out dateArray) as language С name "pass_dateArray" library demoPassing with context parameters (CONTEXT, p_in OCIColl, p_in INDICATOR short, p_out OCIColl, p out INDICATOR short);
282 124 125 126 127 128 129 130 131
Глава 18
procedure pass(p_in in strArray, p_out out strArray) as language С name "pass_strArray" library demoPassing with context parameters (CONTEXT, p_in OCIColl, p_in INDICATOR short, p_out OCIColl, p_out INDICATOR short);
Далее следует процедура, передающая и принимающая данные типа RAW. В данном случае используется как атрибут MAXLEN (который уже использовался в представленной выше процедуре с параметрами типа VARCHAR2), так и атрибут LENGTH. Необходимо передавать длину данных типа RAW, поскольку они содержат двоичную информацию, включая нули, поэтому фактическую длину строки в программе на языке С определить невозможно, и сервер Oracle не сможет понять, какого объема данные возвращаются. Для данных типа RAW оба атрибута, LENGTH и MAXLEN, принципиально важны. Атрибут LENGTH передавать обязательно, a MAXLEN — желательно. 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
procedure pass_raw(p_in in raw, p_out out raw) as language С name "pass_raw " library demoPassing with context parameters (CONTEXT, p_in RAW, p_in INDICATOR short, p_in LENGTH int, p_out RAW, p_out INDICATOR short, p_out MAXLEN int, P_out LENGTH int); — void pass_long_raw — ( контекст */ — OCIExtProcContext *, /* 1 — unsigned char *, /* 2 P_IN */ — short , /* 3 P_IN (индикатор) */ — int , /* 4 P_IN (длина) */ — unsigned char *, /* 5 P_OUT */ — short *, /* 6 P_OUT (индикатор) */ — int *, /* 7 P_OUT (максимальная длина) */ — int * /* 8 P OUT (длина) */ — );
Далее идет процедура, принимающая и передающая в функцию языка С данные PL/SQL типа BINARY_INTEGER. В этом случае тип BINARY_INTEGER сопоставляется встроенному типу данных int языка С: 154 155 156 157 158 159 160 161 162 163
procedure pass_int(p_in in binary_integer, p_out out binary_integer) as language С name "pass_int" library demoPassing with context parameters (CONTEXT, p_in int, p_in INDICATOR short, p_out int, p_out INDICATOR short); — void pass_int — (
Внешние процедуры на языке С
164 165 166 167 168 169
— — — — — —
OCIExtProcContext *, int , short , int *, short * );
/* /* /* /* /*
1 2 3 4 5
: : : : :
контекст */ P_IN */ P_IN (индикатор) */ P_OUT */ P_OUT (индикатор) */
Ниже представлены оболочки для трех функций, возвращающих числа, даты и строки. В них используется новое ключевое слово RETURN. При сопоставлении с функцией необходимо использовать ключевое слово RETURN в качестве последнего параметра в списке параметров. При этом задается тип возвращаемого значения, а не формального параметра функции. Ниже я буду указывать три параметра в SQL-оболочке и только два из них в используемом прототипе функции на языке С. Параметр RETURN OCINumber задает тип данных, которые будут возвращаться в соответствии с прототипом функции OCINumber *return_number. Я включил индикаторную переменную даже для возвращаемого значения, чтобы можно было при необходимости вернуть значение NULL. Если не включить в список параметров индикаторную переменную, вернуть значение NULL будет невозможно. Как было показано в примере для строк, можно также возвращать атрибут LENGTH, но не MAXLEN, поскольку этот атрибут можно задавать только для параметров, передаваемых в режиме OUT, память для которых выделяет сервер Oracle. При возврате значений, поскольку за выделение памяти отвечает разработчик, атрибут MAXLEN не имеет смысла. 173 174 175 176 177 178 179 180 181 182 183 184 185 ... 186 187 188 189 190 191 192 193 194 195 196 197 198 199
function r e t u r n number return number language С name "return_nuiriber" l i b r a r y demoPassing with context parameters (CONTEXT, RETURN INDICATOR s h o r t , RETURN OCINumber); — OCINumber *return_number — ( — OCIExtProcContext *, /* — short * /* );
1 : контекст */ 2 : RETURN (индикатор) */
function return date return date — as language С name "return_date" library demoPassing with context parameters (CONTEXT, RETURN INDICATOR short, RETURN OCIDate); — OCIDate *return_date — ( — OCIExtProcContext *, — short * — );
/* /*
1 : контекст */ 2 : RETURN (индикатор) */
function return_string return varchar2 as language С name "return_string" library demoPassing
284 200 201 202 203 204 205 206 207 208 209 210 211
Глава 18 with c o n t e x t parameters (CONTEXT, RETURN INDICATOR short, RETURN LENGTH i n t , RETURN STRING); — char "return string / — \ — *, — OCIExtProcContext short *, int *
/* /* /*
1 : контекст */ 2 : RETURN (индикатор) 3 : RETURN (длина) */
end demo_passing_pkg;
Package body c r e a t e d . В последней функции я использую как атрибут LENGTH, так и индикаторную переменную. Так я могу передать серверу Oracle длину возвращаемой строки. Итак, мы имеем: •
спецификацию пакета, определяющую его основные функции;
•
набор новых типов массивов SQL;
•
объект-библиотеку в базе данных, задающую соответствие для еще не созданной библиотеки extproc.dll;
•
тело пакета, представляющее собой спецификации SQL для функций на языке С. Эти спецификации сообщают серверу Oracle, какие данные (индикаторные переменные, параметры, контексты и т.д.) и как (на уровне типа данных) передавать внешней процедуре.
Теперь, после рассмотрения примера, имеет смысл представить таблицы соответствия типов данных. Одна таблица показывает, какие внешние типы данных можно использовать для SQL-типа Х. Затем для выбранного внешнего типа, показано, какой тип данных языка С будет фактически использоваться. Таблицы соответствия типов данных можно найти в руководстве Oracle Application Developers Guide — Fundamentals; здесь они представлены просто для справки. Учтите, что внешний тип данных — это не тип, используемый в языке С (и не тип, используемый в языке SQL или PL/SQL). Для определения фактического типа данных, который должен использоваться в языке С, обратитесь ко второй таблице. Тип данных SQL или PL/SQL Внешний тип данных
Стандартный тип
BINARY INTEGER, BOOLEAN, PLSJNTEGER
[unsigned]char, [unsigned]short, [unsignedjint, [unsigned]long, sh>1, sb2, sb4, ub1, ub2, ub4, size t
int
NATURAL, NATURALN, POSITIVE, POSITIVEN, SIGNTYPE
unsigned int [unsigned]char, [unsigned]short, [unsigned]int, [unsigned]long, sb1, sb2, sb4, ub1, ub2, ub4, size_t
FLOAT, REAL
Float
float
DOUBLE PRECISION
Double
double
Внешние процедуры на языке С
Тип данных SQL или PL/SQL Внешний тип данных
285
Стандартный тип
CHAR, CHARACTER, LONG, NCHAR, NVARCHAR2, ROWID, VARCHAR2, VARCHAR
string, ocistring
string
LONG RAW, RAW
raw, ociraw
raw
BFILE, BLOB, CLOB, NCLOB
ociloblocator
ociloblocator
NUMBER, DEC, DECIMAL, INT, INTEGER, NUMERIC, SMALLINT
[unsignedjchar, [unsigned]short, [unsigned]int, [unsigned]long, sb1, sb2, sb4, ubi, ub2, ub4, size t, ocinumber
ocinumber
DATE
Ocidate
ocidate
Абстрактные типы данных (АТД)
Dvoid
dvoid
Наборы (вложенные таблицы, массивы VARRAY)
Ocicoll
ocicoll
•
Итак, в таблице приводятся внешние типы данных, соответствующие типу данных SQL или PL/SQL. Я рекомендую использовать стандартные типы, поскольку с ними проще всего работать в функциях на языке С. Внешний тип данных очень похож на тип данных языка С, но надо пойти на шаг дальше. Поскольку любой тип данных SQL или PL/SQL может задаваться для параметра, передаваемого в режиме in, in out, out или определять тип возвращаемого значения функции, при определении фактического типа данных языка С необходимо дополнительное уточнение. В общем случае параметры, которые возвращаются или передаются в режиме in, передаются по значению, а параметры в режимах in out, out или передаваемые по ссылке явно, передаются через указатели, по ссылке. В следующей таблице показано, какой тип данных в С надо использовать для соответствующего внешнего типа данных и режима передачи параметра: Внешний тип данных
Тип в языке С для параметров IN и возвращаемых значений
Тип в языке С для параметров IN OUT, OUT и при передаче по ссылке
[unsigned] char
[unsigned] char
[unsigned] char *
[unsigned] short
[unsigned] short
[unsigned] short *
[unsigned] int
[unsigned] int
[unsigned] int *
[unsigned] long
[unsigned] long
[unsigned] long *
size t
size t
size_t *
sb1
sb1
sb1 *
sb2
sb2
sb2 *
sb4
sb4
sb4 *
ub1
ub1
ub1 *
ub2
ub2
ub2 *
ub4
ub4
ub4 *
288
Глава 18 typedef struct myCtx OCIExtProcContext * ctx; OCIEnv * OCISvcCtx * OCIError *
envhp; svchp; errhp;
/* Контекст, передаваемый внешним /* процедурам */ /* Дескриптор среды OCI */ /* Дескриптор службы OCI */ /* Дескриптор ошибки OCI */
int char *
curr_lineno; curr_filename;
ubl char char
debugf_flag; debugf_path[255]; debugf_filename[50];
/* добавьте сюда необходимые переменные состояния... */ myCtxStruct; Затем в шаблоне исходного кода следует функция debugf — процедура трассировки. Это С-функция, работающая аналогично стандартной функции fprintf и даже принимающая любое количество аргументов (обратите внимание на троеточие в списке аргументов). Первый ее аргумент — контекст — описанная выше структура, предоставляющая информацию о состоянии. Я всегда предполагаю, что указатель на состояние имеет имя myCtx (в макросе для функции debugf это предположение используется). Функция debugf демонстрирует кое-что новое. В ней представлена большая часть функций библиотеки OCI для работы с файлами, которые очень похожи на семейство функций fopen/fread/ fwrite/fclose языка С. Код функции debugf, который вызывается, только если установлен флаг myCtx->debugf_flag, открывает файл, формирует сообщение, записывает его и закрывает файл. Этот код может служить примером того, как используется контекст. Контекст содержит информацию о состоянии сеанса и важные переменные, например структуры OCIEnv и OCIError, необходимые для всех вызовов функций OCI. Показано, как изменять состояние, манипулируя переменными в структуре контекста (как это делает макрос debugf). Макрос debugf позволяет "проигнорировать" обращения к реализующей трассировку функции _debugf(). Дело в том, что если либо контекст rayCtx, либо флаг myCtx->debugf_flag не установлены, состояние контекста никогда не изменяется, и функция _debugf() никогда не вызывается. Это означает, что можно оставить отладочные операторы в производственном коде, поскольку их наличие не повлияет на производительность в долгосрочной перспективе (пока флаг debugf_flag имеет значение false). void _debugf(myCtxStruct
* myCtx, char * fmt, . . . )
va_list ap; OCIFileObject * fp; time_t theTime = time(NULL); char msg[8192]; ub4 bytes; if (OCIFileOpen(myCtx->envhp, myCtx->errhp, &fp, myCtx->debugf_filename, myCtx->debugf_path,
Внешние процедуры на языке С
289
OCI_FILE_WRITE_ONLY, OCI_FILE_APPEND | OCI_FILE_CREATE, OCI_FILE_TEXT) != OCI_SUCCESS) return; strftime(msg, sizeof(msg), "%y%m%d %H%M%S GMT ", gmtime{stheTime)); OCIFileWrite(myCtx->envhp, myCtx->errhp, fp, msg, strlen(msg), Sbytes); va_start(ap,fmt); vsprintf(msg, fmt, a p ) ; va_end(ap); strcat(msg,"\n"); OCIFileWrite(myCtx->envhp, myCtx->errhp, fp, msg, strlen(msg), Sbytes); OCIFileClose(myCtx->envhp, myCtx->errhp, f p ) ; )
Следующий фрагмент кода представляет интерфейсный макрос для функции debugf. Этот макрос использовать удобнее, чем функцию _debugf. Вместо обязательной передачи значений _LINE_, _FILE_ при каждом вызове, достаточно написать: debugf( myCtx, "This i s some format %s", some_string
);
и этот макрос автоматически установит соответствующие значения в контексте, а затем вызовет функцию _debugf. v o i d d e b u g f ( m y C t x S t r u c t * myCtx, c h a r * fmt, # d e f i n e debugf \ i f ((myCtx!=NULL) && (myCtx->debugf_flag)) \
...);
myCtx->curr_lineno = LINE^, \ myCtx->curr_filename = FILE , \ _debugf После этого в шаблоне следует утилита raise_application_error для обработки ошибок. Это имя, конечно, знакомо каждому разработчику, использовавшему язык PL/SQL. raise_application_error — встроенная функция PL/SQL, возбуждающая исключительную ситуацию в случае ошибки. Эта функция у нас имеет такое же назначение. Если внешняя процедура вызывает эту функцию раньше, чем завершает работу, возвращаемые внешней процедурой значения игнорируются, а вместо этого в вызывающей среде возбуждается исключительная ситуация. Это позволяет обрабатывать ошибки, возникающие во внешней процедуре, точно так же, как и в любой PL/SQL-процедуре. static int raise_application_error(myCtxStruct * myCtx, int errCode, char * errMsg, ...) < char msg[8192]; va_list ap; va_start(ap,errMsg); vsprintf(msg, errMsg, a p ) ; va_end(ap); debugf(myCtx, "raise application error( %d, %s ) " , errCode, m s g ) ; if (OCIExtProcRaiseExcpWithMsg(myCtx->ctx,errCode,msg,0) = OCIEXTPROC ERROR)
Ш 4« 244
290
Глава 18 debugf(myCtx,
"He удалось возбудить исключительную ситуацию");
return - 1 ; Дальше следует еще одна функция обработки ошибок, lastOciError. Эта функция получает контекст текущего сеанса и, используя его структуру OCIError, извлекает текст сообщения о последней произошедшей ошибке OCI. Этот текст выбирается в память, выделенную с помощью вызова OCIExtProcAllocCalIMemory(). Любая область памяти, выделенная этой функцией, будет автоматически освобождена при выходе из внешней процедуры. Эта функция чаще всего используется в обращении к функции raise_application_error после неудачного вызова одной из функций OCI. Благодаря ей можно узнать причину возникновения ошибки OCI. s t a t i c char * lastOciError(myCtxStruct * myCtx) sb4 char
errcode; * errbuf = (char*)OCIExtProcAllocCallMemory(myCtx->ctx,
256);
strcpy(errbuf, "unable t o r e t r i e v e message\n"); OCIErrorGet(myCtx->errhp, 1, NULL, Serrcode, errbuf, 255, OCI_HTYPE_ERROR); e r r b u f [ s t r l e n ( e r r b u f ) - l ] = 0; r e t u r n errbuf; Теперь переходим к "основной" функции в шаблоне для создания внешних процедур; речь идет о функции init. Она отвечает за формирование и получение информации о состоянии и обработку параметров, установленных в файле инициализации. Это слишком большая функция, чтобы описывать ее полностью, но достаточно простая, если разобраться с используемыми вызовами функций библиотеки OCI. Функция init создает структуру myCtxStruct и вызвает необходимые функции инициализации OCI. Вначале функция получает дескрипторы среды OCI. Она делает это одним из двух способов. Если используются только средства OCI (без прекомпилятора Рго*С), достаточно просто вызвать OCIExtProcGetEnv с передачей контекста внешней процедуры. Эта функция OCI автоматически получает все необходимые дескрипторы. Если используется и библиотека OCI, и прекомпилятор Рго*С, применяется конструкция EXEC SQL REGISTER CONNECT :ctx. Она выполняет настройку на уровне Рго*С. При этом все равно необходимо получить дескрипторы среды OCI, но для этого придется использовать вызовы библиотеки Pro*C: SQLEnvGet, SQLSvcCtxGet. Уберите комментарий с используемого метода и закомментируйте другой метод. /* это надо включать только для внешних процедур, использующих средства Рго*С!! •define SQLCA_INIT EXEC SQL INCLUDE s q l c a ; _
_
_
_
_
_
_
_
_
_
.
_
_
__——___.«•_«•._
static myCtxStruct * init(OCIExtProcContext * ctx) ubl
false - 0;
_
_
_
_
_
_
*
/
Внешние процедуры на языке С
myCtxStruct OCIEnv OCISvcCtx OCIError ub4
*myCtx = NULL; *envhp; *svchp; *errhp; key = 1;
if (OCIExtProcGetEnv( ctx, Senvhp, Ssvchp, Serrhp ) !- OCI_SUCCESS) OCIExtProcRaiseExcpWithMsg(ctx,20000, "не удалось подключиться к OCT.", Obreturn NULL; } замените представленный выше вызов OCIExtProcGetEnv() следующий при использовании Рго*С
/*
EXEC SQL REGISTER CONNECT USING :ctx; if (sqlca.sqlcode < 0) { OCIExtProcRaiseExcpWithMsg(ctx,20000,sqlca.sqlerrm.sqlerzmc,70); return NULL; if ((SQLEnvGet(0, Senvhp) != OCIJSUCCESS) II (OCIHandleAlloc(envhp, (dvoid**)&errhp, OCI_HTYPE_ERROR,0,0) != OCI_SUCCESS) II (SQLSvcCtxGet(0, NULL, 0, Ssvchp) != OCI_SUCCESS)) { OCIExtProcRaiseExcpWithMsg(ctx,20000,"не удалось получить среду ; return NULL; */ Получив среду OCI, необходимо вызвать функцию OCIContextGetValue() для получения контекста. Эта функция принимает среду OCI и ключ и пытается получить указатель. Ключ в данном случае — 64-битовое число. Можно сохранять сколько угодно контекстов, но в этот раз мы будем использовать один. if
(OCIContextGetValue(envhp, errhp, (ubl*)&key, sizeof(key), (dvoid**)&myCtx) != OCI_SUCCESS)
{
OCIExtProcRaiseExcpWithMsg(ctx,20000, "не удалось получить контекст OCI",0); r e t u r n NULL; При получении указателя NULL (это происходит, если контекст еще не установлен), мы выделяем достаточный объем памяти для контекста и сохраняем его в контексте. Для выделения блока памяти, который остается действительным на все время существования процесса, вызывается функция OCIMemoryAllocate. После того как память выделена, она сохраняется в контексте с помощью вызова функции OCIContextSetValue. Эта функция на время сеанса сопоставляет указатель (который не будет изменяться) выбранному ключу. Следующий вызов функции OCIContextGetValue с тем же ключом в пределах того же сеанса позволит получить этот указатель.
292
Глава 18 if (myCtx — NULL) { if (OCIMemoryAlloc(envhp, errhp, (dvoid**)SmyCtx, OCI_DURATION_PROCESS, sizeof(myCtxStruct), OCI_MEMORY_CLEARED) != OCI_SUCCESS) { OCIExtProcRaiseExcpWithMsg(ctx,20000, "не удалось получить память ОС1",0); return NULL; } myCtx->ctx = ctx; myCtx->envhp = envhp; myCtx->svchp = svchp; myCtx->errhp = errhp; if ( OCIContextSetValue(envhp, errhp, OCI_DURATION_SESSION, (ubl*)&key, sizeof(key), myCtx ) != OCI_SUCCESS)
f raise_application_error(myCtx, 20000, "%s", lastOciError(myCtx)); return NULL; } Продолжаем, поскольку получение указателя NULL означает, что параметры еще не обработаны. Мы обработаем их в следующем блоке кода. Для обработки файлов, правила построения которых аналогичны файлу параметров инициализации Oracle, используются предоставляемые библиотекой ОС1 функции управления параметрами. Файл параметров инициализации описан в главе 2. Я обычно использую этот файл для управления выдачей трассировочной и отладочной информации и задания стандартных значений для других переменных, используемых в программе. Файл параметров инициализации для данного примера может выглядеть следующим образом: debugf = t r u e debugf_filename = extproc2.log debugf_path = /tmp/ Он включает выдачу трассировочной информации (debugf = true) в файл /tmp/ extproc2.log. Можно добавить в этот файл дополнительные параметры и изменить код функции init так, чтобы можно было читать их и устанавливать соответствующие значения в контексте сеанса. Процесс чтения и обработки файла параметров инициализации состоит из следующих шагов. 1. Вызов функции OCIExtractlnit для инициализации библиотеки обработки параметров. 2. Вызов функции OCIExtractSetNumKeys для передачи библиотеке OCI количества запрашиваемых имен. Оно должно совпадать с количеством параметров в файле параметров. 3. Вызов функции OCIExtractSetKey столько раз, сколько было указано в вызове функции OCIExtractSetNumKeys().
Внешние процедуры на языке С
2,у 3
4. Вызов функции OCIExtractFromFile для обработки файла параметров. 5. Вызов функции OCIExtractTo<™n данных> для поочередного получения значений параметров. 6. Вызов функции OCIExtractTerm для завершения работы библиотеки обработки параметров и освобождения выделенных ей системных ресурсов. if
( ( O C I E x t r a c t l n i t ( e n v h p , e r r h p ) != OCI_SUCCESS) | | (OCIExtractSetNumKeys(envhp, e r r h p , 3) ! = OCI_SUCCESS) | I (OCIExtractSetKey(envhp, e r r h p , "debugf", OCI_EXTRACT_TYPE_BOOLEAN, 0, S f a l s e , NULL, NULL) ! = OCI_SUCCESS) I | (OCIExtractSetKey( envhp, e r r h p , " d e b u g f _ f i l e n a m e " , OCI_EXTRACT_TYPE_STRING, 0, " e x t p r o c . l o g " , NULL, NULL) != OCI_SUCCESS) I I (OCIExtractSetKey( envhp, e r r h p , "debugf_j?ath", OCI_EXTRACT_TYPE_STRING, 0, " " , NULL, NULL) != OCI_SUCCESS) | | (OCIExtractFromFile(envhp, e r r h p , 0, INI_FILE_NAME) != OCI_SUCCESS) I I (OCIExtractToBool(envhp, e r r h p , "debugf", 0, &myCtx->debugf_flag) != OCI_SUCCESS) | I (OCIExtractToStr(envhp, e r r h p , "debugf _filename", 0, myCtx->debugf_filename, sizeof(myCtx->debugf_filename)) != OCI_SUCCESS) | | (OCIExtractToStr(envhp, e r r h p , "debugf _ p a t h " , 0, myCtx->debugf _path, sizeof(myCtx->debugf_path)) != OCI_SUCCESS) | | (OCIExtractTerm(envhp, errhp) != OCI SUCCESS)) raise_application_error(myCtx, 20000, "%s", lastOciError(myCtx)); r e t u r n NULL;
) Далее следует блок кода, который будет выполняться при втором и последующих вызовах функции init в сеансе. Поскольку функция OCIContextGetValue для второго и последующих вызовов возвращает контекст, мы просто устанавливаем на него соответствующие указатели в структуре: else myCtx->ctx myCtx->envhp myCtx->svchp myCtx->errhp
= = = =
ctx; envhp; svchp; errhp;
294
Глава 18
Последнее действие в функции init — вызов функции OCIFilelnit. Она инициализирует набор функций OCI для работы с файлами, чтобы можно было открывать файлы операционной системы для чтения/записи. Можно было бы использовать и стандартные функции fopen, fclose, fread и fwrite языка С. Использованный подход позволяет сделать внешнюю процедуру более переносимой и обеспечить единообразную обработку ошибок на различных платформах. В функцию init можно добавить и другие вызовы. Например, если предполагается использование функций OCIFormat* (аналогичных функции vsprintf языка С), можно добавить в функцию инициализации вызов OCIFormatlnit. He забудьте при этом добавить соответствующий вызов OCIFormatTerm в представленную ниже функцию term. if
(OCIFilelnit(myCtx->envhp, myCtx->errhp) != OCI_SUCCESS)
{
raise_application_error(myCtx, r e t u r n NULL;
20000, "%s", lastOciError(myCtx));
}
r e t u r n myCtx; }
Теперь перейдем к упомянутой функции term. Это функция завершения и очистки; ее нужно вызывать после каждого успешного вызова функции init. Это должна быть последняя функция, вызываемая перед возвратом управления из внешней процедуры на языке С в SQL: s t a t i c void term(myCtxStruct * myCtx) {
OCIFileTerm(myCtx->envhp, myCtx->errhp); }
Создание шаблона закончено. Я использую один и тот же шаблон исходного кода для всех своих проектов по созданию внешних процедур (с небольшими изменениями, если используется только библиотека OCI, а не сочетание Рго*С и вызовов OCI). Такой шаблон экономит большое количество времени и обеспечивает множество функциональных возможностей. Теперь начнем добавлять специфический код. Сразу же после общих компонентов я перечисляю все коды ошибок, которые будет возвращать функция, начиная с 20001. Перечислив их в самом начале, можно задать соответствующие исключительные ситуации с помощью конструкций pragma exception_init в коде на языке PL/SQL. Это позволит перехватывать в программах на PL/SQL исключительные ситуации с определенными именами, а не проверять коды ошибок. Я не буду демонстрировать это в данном примере, но в следующем примере с использованием прекомпилятора Рго*С — продемонстрирую. Коды ошибок должны быть в диапазоне от 20000 до 20999, поскольку именно эти коды ошибок выделяются сервером Oracle для приложений; остальные коды ошибок используются самим сервером. •define •define •define •define •define
ERROR_OCI_ERROR ERROR_STR_TOO_SMALL ERROR_RAW_TOO_SMALL ERROR_CLOB_NULL ERROR ARRAY NULL
20001 20002 20003 20004 20005
Внешние процедуры на языке С
Переходим к первой специфической функции. Это реализация процедуры pass_number, заданной в представленном ранее PL/SQL-пакете. Она принимает из PL/SQL параметр типа NUMBER в режиме IN и устанавливает параметр типа NUMBER, переданный в режиме OUT. Функция выполняет следующие действия. •
Использует внутренний тип данных Oracle OCINumber с помощью соответствующих функций. В данном случае тип данных Oracle NUMBER преобразуется в тип данных double языка С с помощью встроенной функции OCINumberToReal. Можно преобразовать данные типа NUMBER в строку с помощью функции OCINumberToText или в тип данных int языка С с помощью функции OCINumberToInt. В библиотеке OCI имеется почти 50 числовых функций для выполнения различных операций с внутренним типом данных. Описание всех имеющихся функций можно найти в руководстве Oracle Call Interface Programmer's Guide.
•
Обрабатывает данные типа double. В данном случае мы просто меняем знак числа: если число было положительным, мы делаем его отрицательным, и наоборот.
•
Устанавливает параметру типа NUMBER, переданному в режиме OUT, полученное значение с измененным знаком, и завершает работу.
Перед каждой функцией, вызываемой из языка PL/SQL, указывается макрос, обеспечивающий ее переносимость. Этот макрос "экспортирует" функцию. Это необходимо только на платформе Windows, а в ОС UNIX — нет. Я обычно включаю этот макрос независимо от платформы, для которой создается внешняя процедура, поскольку мне часто приходится переносить библиотеки внешних процедур из Windows в UNIX, и наоборот. Постоянное включение макроса упрощает перенос. Встроенные в код комментарии объясняют его назначение по ходу дела: #ifdef WIN__NT _declspec (dllexport) #endif void pass_number (OCIExtProcContext * ctx
/* контекст */,
OCINumber * short
p inum p inum i
/* OCINumber V, /* INDICATOR short
OCINumber * short *
p onum p onum i
/* OCINumber */, /* INDICATOR short V ]
V,
double l_inum; myCtxStruct*myCtx; До выполнения любых действий необходимо получить контекст сеанса. При этом будет получена среда OCI, значения параметров и т.д. Этот вызов будет выполняться первым во всех внешних процедурах: i f ((myCtx = i n i t ( ctx )) == NULL) r e t u r n ; debugf(myCtx, "Входим в функцию Pass Number"); Обратимся к первому параметру. Мы передали его как данные типа OCINumber. Теперь к нему можно применять множество функций OCINumber*. В данном случае
296
Глава 18
данные типа NUMBER преобразуются в данные типа double языка С с помощью функции OCINumberToReal. Так же просто преобразовать их в данные типа int, long, float или в форматированную строку. Сначала необходимо убедиться, что передано не пустое значение типа NUMBER; если — да, оно обрабатывается, если же — нет, вызывается функция term(). Если удалось успешно получить первый параметр, мы изменяем его знак, после чего создаем на основе полученного значения данные типа OCINumber с помощью функции OCINumberFromReal. Если это получилось, устанавливаем в индикаторе пустого значения p_onum_I признак NOTNULL, чтобы в вызывающей среде можно было понять, что возвращается непустое значение. Обработка закончена. Вызываем функцию term для освобождения ресурсов и возвращаем управление: if
(p_inum_i == OCI_IND_NOTNULL)
{
if
(OCINumberToReal(myCtx->errhp, p_inum, sizeof(l_inum), ! = OCI_SUCCESS)
&l_inum)
{
raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); else debugf(myCtx, "Первый параметр: %g", l_inum); l_inum = -l_inum; if (OCINumberFromReal(myCtx->errhp, &l_inum, sizeof(l_inum), p_onum) != 0CI_SUCC3SS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); } else *p_onum_i = OCI_IND_NOTNULL; debugf(myCtx, "Устанавливаем параметр OUT равным %g, а индикаторную ^переменную — равной NOTNULL", 1 inum);
term(myCtx); } Вот и все. Наша первая внешняя процедура использует все вспомогательные функции: raise_application_error, lastOciError, init, term и debugf. При тестировании этой процедуры мы проверим результаты вызова функции debugf. Они подтвердят, что функция делает именно то, что и предполагалось (представляя собой удобное средство для отладки).
Внешние процедуры на языке С
Обратите внимание, как я позаботился о возвращении управления из данной функции только в одном месте. Если возврат управления происходит в нескольких местах, не забудьте в каждом из них вызывать функцию term(myCtx). Теперь переходим к остальным функциям. Следующая функция обрабатывает даты, переданные как параметры в режиме IN и OUT. Мы будем: Q принимать параметр типа DATE в режиме IN; •
форматировать его как строку с помощью соответствующих функций библиотеки OCI для трассировки (для работы с данными типа DATE предлагается около 16 функций OCI);
Q добавлять один месяц к дате с помощью соответствующей функции OCI; •
присваивать новую дату параметру, переданному в режиме OUT;
Q преобразовывать только что присвоенную дату в строку и выдавать ее; Q наконец, вызывать функцию term и возвращать управление. #ifdef WIN_NT _declspec (dllexport) #endif void pass_date (OCIExtProcContext * ctx
)
OCIDate * short OCIDate * short *
p_idate p_idate_i p_odate p_odate_i
/* CONTEXT */, /* /* /* /*
OCIDATE */, INDICATOR short */, OCIDATE */, INDICATOR short */
{
char buffer[255]; ub4 buff_len; char * fmt = "dd-mon-yyyy hh24:mi:ss"; myCtxStruct * myCtx; if ((myCtx = init( ctx )) — NULL) return; debugf(myCtx, "Входим в функцию Pass Date"); if (p_idate_i =
OCI_IND_NOTNULL)
buff_len = sizeof(buffer); if (OCIDateToText(myCtx->errhp, p_idate, fmt, strlen(fmt), NULL, -1, &buff_len, buffer) != OCI_SUCCESS) raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); } else debugf(myCtx, "Входной параметр типа date имел значение ' % . * s ' " , buff_len, b u f f e r ) ; if
(OCIDateAddMonths(myCtx->errhp, p _ i d a t e , 1, p_odate) != OCI SUCCESS)
298
Глава 18 raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); else *p_odate_i = OCI_IND_NOTNULL; buff_len = sizeof(buffer); if (OCIDateToText(myCtx->errhp, p_odate, fmt, strlen(fmt), NULL, -1, &buff_len, buffer) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); } else { debugf(myCtx, "Выходной параметр типа date получил значение '%.*s'", buff_len, buffer);
} term(myCtx);}
Теперь разберемся, что необходимо для приема и передачи строк. Работать со строками несколько проще, чем с данными типа NUMBER и DATE, поскольку они передаются просто как строки ASCII, завершаемые нулем. Для всех строк, передаваемых в режиме OUT, будет использоваться параметр MAXLEN. Параметр MAXLEN задает для возвращаемой строки максимальный размер буфера, который может изменяться при каждом вызове. Дело в том, что буфер предоставляет вызывающая среда, и при каждом вызове может передаваться в режиме OUT другой параметр другого размера. В результате внешняя процедура сможет учесть размер и предотвратить переполнение буфера. Она проинформирует вызывающую среду о том, что предоставлен слишком маленький буфер, и сообщит, какого размера он должен быть. #ifdef WIN_NT _declspec (dllexport) #endif void p a s s _ s t r (OCIExtPrOcContext * ctx
char * short char * short * int *
myCtxStruct
p p p p p
* myCtx;
istr istr i ostr ostr_i ostr ml
/ * CONTEXT * / ,
/* /* /* /* /*
STRING */, INDICATOR short */, STRING */, INDICATOR short MAXLEN int */
Внешние процедуры на языке С
if ((myCtx = init(ctx)) — NULL) return; debugf(myCtx, "Входим в функцию Pass Str"); if (p_istr_i == OCI_IND_NOTNULL) ( int l_istr_l = strlen(p_istr); if (*p_ostr_ml > l_istr_l) {
strcpy(p_ostr, p _ i s t r ) ; strupr(p_ostr); *p_ostr_i = OCI_IND_NOTNULL; else {
raise_application_error(myCtx, ERROR_STR_TOO_SMALL, "выходной буфер размером %d байт должен быть длиной не '-•менее %d байт", *p_ostr_ml, l _ i s t r _ l + l ) ;
j
term(myCtx);
На примере следующей функции я продемонстрирую использование типа binary_integer. Тип binary_integer в PL/SQL представляет 32-битовое целое число со знаком. Передать его проще всего. Значения этого типа передаются естественным для программиста на языке С образом. Эта функция просто проверит входное значение и присвоит его (умноженное на 10) выходному параметру: #ifdef WINJJT _declspec (dllexport) #endif void pass_int (OCIExtProcContext * ctx
/* CONTEXT */,
int short
p_iINT p_iINT_i
/* int */, /* INDICATOR short */,
int * short *
p_oINT p_oINT_i
/* int */, /* INDICATOR short */
) ( myCtxStruct * myCtx; if ((myCtx = init(ctx)) == NULL ) return; debugf(myCtx, "Входим в функцию Pass Int"); if (p_iINT_i == OCI_IND_NOTNULL) < debugf (myCtx, "Первый параметр типа INT имеет значение %d", p_iINT); *p_oINT = p_iINT*10; *p_oINT_i = OCI_IND_NOTNULL; debugf(myCtx, "Устанавливаем выходному параметру типа INT ^значение %d", *p_oINT);
300
Глава 18
term(myCtx) ; }
Теперь рассмотрим передачу параметров PL/SQL типа BOOLEAN. Тип BOOLEAN языка PL/SQL в данном случае сопоставляется типу int языка С. Значение 1 представляет истину, а значение 0 — ложь, как и следовало ожидать. Эта функция просто проверяет входной параметр (не пуст ли он) и устанавливает выходной параметр равным отрицанию входного. И в этом случае, поскольку обеспечивается простое сопоставление со встроенным типом языка С, реализовать эту функцию очень легко. Для обмена данными не надо использовать дескрипторы среды или вызовы функций. Функция просто устанавливает выходной параметр равным отрицанию входного: tifdef WIN_NT declspec (dllexport) tendif void pass bool (OCIExtProcContext * ctx
/* CONTEXT */,
int short
p ibool p ibool_i
/* int */, /* INDICATOR short */,
int * short *
p obool p obool _ _ i
/* int */, /* INDICATOR short */)
myCtxStruct * myCtx; if ((myCtx = init(ctx)) — NULL) return; debugf(myCtx, "Входим в функцию Pass Boolean"); if (p_ibool_i == OCI_IND_NOTNULL) { *p_obool = !p_ibool; *p_obool__i = OCI_IND_NOTNULL; } term(myCtx); > Теперь займемся передачей параметров типа RAW. Поскольку переменные типа VARCHAR2 в PL/SQL могут иметь длину не более 32 Кбайт, мы всегда будем использовать более простой для взаимодействия внешний тип данных RAW. Он соответствует в языке С типу данных unsigned char *, который представляет собой указатель на байты данных. Работая с данными типа RAW, мы всегда будем получать атрибут LENGTH. Это обязательно, поскольку нет другого способа определить, какое количество данных надо обрабатывать. Мы также всегда будем получать атрибут MAXLEN для всех параметров, переданных в режиме OUT и имеющих переменную длину, чтобы избежать потенциальной перезаписи буфера. Этот атрибут, хотя технически и не обязателен, слишком важен, чтобы его не использовать. Данная функция просто копирует входной буфер в выходной: #ifdef WIN_NT _declspec (dllexport)
Внешние процедуры на языке С fendif void pass_raw (OCIExtProcContext * ctx unsigned char short int
p_iraw p_iraw_i p_iraw_l
unsigned char * p_oraw short * p_oraw_i p_oraw_ml int p oraw 1 int
3 0 1
/* CONTEXT */, /* RAW */, /* INDICATOR short */, /* LENGHT INT */, /* RAW */, /* INDICATOR short */, /* MAXLEN int */, /* LENGTH int */
myCtxStruct * myCtx; if ((myCtx = init(ctx)) == NULL ) return; debugf(myCtx, "Входим в функцию Pass long raw"); if (p_iraw_i == OCI_IND_NOTNULL) { if (p_iraw_l <= *p_oraw_ml) { memcpy(p_oraw, p_iraw, p_iraw_l); *p_oraw_l = p_iraw_l; *p_oraw_i = OCI_IND_NOTNULL; else raise_application_error(myCtx, ERROR_RAW_TOO_SMALL, "Буфер размером %d байт должен быть размером не менее %d байт", *p_oraw_ml, p_iraw_l);
else { *p_oraw_i = OCI_IND_NULL; *p oraw 1 = 0; } term(myCtx);
I Последняя функция, обрабатывающая ск&тарные данные, работаете большими объектами. Работать с большими объектами ничуть не сложнее, чем с данными типа DATE или NUMBER. Имеется несколько функций библиотеки OCI, позволяющих читать и записывать большие объекты, копировать и сравнивать их, и т.д. В этом примере используются функции для определения длины и последующего копирования большого входного объекта в выходной. Эта функция требует, чтобы в вызывающей среде был проинициализирован большой объект, передаваемый в режиме OUT (либо путем выбора локатора LOB из существующей строки таблицы, либо с помощью подпрограммы dbmsjob.createtemporary). Хотя я демонстрирую работу только с большим объектом тина CLOB, реализации для объектов типа BLOB и BFILE будут очень похожи: для всех трех
302
Глава 18
типов используется тип данных OCILobLocator. Подробнее о функциях, работающих с данными типа OCILobLocator, можно прочитать в руководстве Oracle Call Interface Programmer's Guide. Приведенная функция просто копирует входной объект типа CLOB в выходной. #ifdef WIN_NT _declspec (dllexport) #endif void pass_clob (OCIExtProcContext * ctx OCILobLocator * p_iCLOB short p_iCLOB_i OCILobLocator * * p_oCLOB short * p_oCLOB_i
/* CONTEXT */, /* /* /* /*
OCILOBLOCATOR */, INDICATOR short */, OCILOBLOCATOR */, INDICATOR short */
ub4 lob_length; myCtxStruct * myCtx; if ((myCtx - init(ctx)) == NULL ) return; debugf(myCtx, "Входим в функцию Pass Clob" ) ; if (p_iCLOB_i — OCI_IND_NOTNULL && *p_oCLOB_i — OCI_IND_NOTNULL) ( debugf(myCtx, "оба больших объекта — NOT NULL"); if (OCILobGetLength(myCtx->svchp, myCtx->errhp, p_iCLOB, &lob_length) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); else { debugf(myCtx, "Длина входного большого объекта была %d", lob_length) ; if (OCILobCopy(myCtx->svchp, myCtx->errhp, *p_oCLOB, p_iCLOB, lob_length, 1, 1) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); } else {
debugf(myCtx, "Мы скопировали большой объект!");
else raise_application_error(myCtx, ERROR_CLOB_NULL, "%s %s объект типа clob был пустым (NULL)" (p_iCLOB_i — OCI_IND_NULL)?"входной":"",
Внешние процедуры на языке С
303
(*p_oCLOB_i= OCI_IND_NULL) ?"выходной" : " " ) ; } term(myCtx); } Следующие три функции демонстрируют, как передавать и принимать массивы данных во внешней процедуре. Если помните, мы создали несколько табличных типов в SQL: numArray, dateArray и strArray. Эти типы будут использоваться для демонстрации. Функции будут показывать, сколько элементов передано в массиве, выдавать их значения и наполнять этими элементами массив, переданный в режиме OUT. В этих функциях, работающих с массивами, мы будем использовать набор функций OCICoIl*. Для работы с наборами (массивами) можно использовать около 15 функций, позволяющих выполнять итерацию по элементам, получать или устанавливать значения элементов и т.п. Ниже использованы следующие наиболее типичные функции: •
OCIColISize, для получения количества элементов в массиве;
•
OCICoIlGetElem, для получения i-ro элемента массива;
Q OCICollAppend, для добавления элемента в конец массива. Полный список имеющихся функций можно найти в руководстве Oracle Call Interface Programmer's Guide. Начнем с массива чисел. Эта функция будет проходить по всем элементам входного набора, выдавать их значения и присваивать соответствующим элементам выходного набора: #ifdef WIN_NT declspec (dllexport) #endif void pass numArray (OCIExtProcContext * OCIColl * short OCIColl ** short *
ctx p in p in i p out p out i
/* /* /* /* /*
CONTEXT */, OCICOL */, INDICATOR short */, OCICOL */, INDICATOR short */
ub4 arraySize; double tmp_dbl; boolean exists; OCINumber *ocinum; int i; myCtxStruct * myCtx; if ((myCtx = init(ctx)) = NULL) return; debugf(myCtx, "Входим в функцию Pass numArray"); if (p_in_i == OCI_IND_NULL) { raise_application_error(myCtx, ERROR_ARRAY_NULL, "Входной массив — NULL");
304
Глава 18 else if (OCICollSize(myCtx->envhp, myCtx->errhp, p_in, SarraySize) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx));
else
debugf(myCtx, "Входной массив состоит из %d элементов", arraySize); f o r ( i = 0 ; i < arraySize; i if (OCICollGetElem(myCtx->envhp, myCtx->errhp, p_in, i, Sexists, (dvoid*)&ocinum, 0) ! = OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break; } if (OCINumberToRealf myCtx->errhp, ocinum, sizeof(tmp_dbl), &tmp_dbl) != OCI_SUCCESS) < raise_application_error(myCtx,ERROR_OCI_ERROR,"%s", lastOciError(myCtx)); break;
J debugf(myCtx, "p_in[%d] = %g", i, tmp_dbl); if (OCICollAppend(myCtx->envhp, myCtx->errhp, ocinum, 0, *p_out) != OCI_SUCCESS ) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break; } debugf(myCtx, "Элемент добавлен в конец другого массива"); ) *p_out_i = OCI_IND_NOTNULL; term(myCtx); }
Следующие две функции — для массивов строк и дат. Они очень похожи на представленную выше функцию, работающую с массивами чисел, поскольку все три функции работают с данными типа OCIColl *. Пример для типа данных strArray интересен тем, что в нем впервые используется новый тип данных библиотеки OCI — OCIString (это не то же самое, что тип char *). При использовании типа данных OCIString необходимо работать со ссылками на ссылки. Для строк и дат мы будем выполнять те же действия, что и в представленном ранее примере для чисел: #ifdef WIN_NT _declspec (dllexport)
Внешние процедуры на языке С #endif void pass_strArray (OCIExtProcContext * ctx OCIColl * p_in short p_in_i OCIColl ** p_out short * p_out_i
305
/* CONTEXT */ t /* OCICOL */ t /* INDICATOR short */, /* OCICOL */ /* INDICATOR short */
ub4 arraySize; boolean exists; OCIString * * ocistring; int i; text *txt; myCtxStruct * myCtx; if ((myCtx = init(ctx)) == NULL) return; debugf( myCtx, "Входим в функцию Pass strArray"); if (p_in_i == OCI_IND_NULL) { raise_application_error(myCtx, ERROR_ARRAY_NULL, "Входной массив — NULL"); else if (OCICollSize( myCtx->envhp, myCtx->errhp, p_in, SarraySize) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); } else debugf(myCtx, "Входной массив состоит из %d элементов", arraySize); f o r ( i = 0 ; i < arraySize; i++) { if (OCICollGetElem(myCtx->envhp, myCtx->errhp, p_in, i, sexists, (dvoid*)&ocistring, 0) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break; } txt - OCIStringPtr(myCtx->envhp, *ocistring); debugf( myCtx, "p_in[%d] - '%s', size = %d, exists = %d", i, txt, OCIStringSize(myCtx->envhp,*ocistring), exists); if (OCICollAppend(myCtx->envhp,myCtx->errhp, *ocistring, 0, *p_out) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break;
306
Глава 18 debugf(myCtx, "Элемент добавлен в конец другого массива"); } *p_out_i - OCI_IND_NOTNULL; ) term(myCtx);
#ifdef WIN_NT declspec (dllexport) #endif void pass_dateArray (OCIExtProcContext * ctx
/* CONTEXT */,
OCIColl * short
p in p in_i
/* OCICOL */, /* INDICATOR £
OCIColl ** short *
p_out p out i
/* OCICOL */, /* INDICATOR :
ub4 arraySize; boolean exists; OCIDate * ocidate; int i; char * fmt = "Day, Month YYYY hh24:mi:ss"; ub4 buff_len; char buffer[255]; myCtxStruct * m y C t x ; if ((myCtx = init(ctx)) == NULL) return; debugf(myCtx, "Входим в функцию Pass dateArray"); if (p_in_i == OCI_IND_NULL) { raise_application_error(myCtx, ERROR_ARRAY_NULL, "Входной массив — NULL"); } else if (OCICollSize(myCtx->envhp, myCtx->errhp, p_in, SarraySize) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); else debugf (myCtx, "Входной массив состоит из %d элементов", arraySize); for(i = 0 ; i < arraySize; i if (OCICollGetElem( myCtx->envhp, myCtx->errhp, p_in, i, Sexists, (dvoid*)Socidate, 0) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx));
Внешние процедуры на языке С
307
break; }
buff_len = sizeof(buffer); if (OCIDateToText(myCtx->errhp, ocidate, fmt, strlen(fmt), NULL, -1, Sbuff len, buffer) != OCI SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break; } debugf(myCtx, "p_in[%d] = %.*s", i, buff_len, buffer); if (OCICollAppend(myCtx->envhp,myCtx->errhp, ocidate, 0, *p_out ) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); break; } debugf(myCtx, "Элемент добавлен в конец другого массива"); } *p_OUt_i = OCI_IND_NOTNULL; } term(myCtx); }
В завершение рассмотрим функции, непосредственно возвращающие значения. Они выглядят немного необычно, поскольку в PL/SQL используются функции без параметров, возвращающие значения, но соответствующие функции на языке С должны принимать ряд параметров. Другими словами, простейшей функции PL/SQL без параметров будет соответствовать С-функция с формальными параметрами. Эти формальные параметры будут использоваться внешней процедурой для передачи серверу Oracle следующих данных: Q индикаторной переменной, показывающей, вернула ли функция значение NULL; •
текущей длины данных строкового типа или типа
RAW.
С этими параметрами мы уже встречались, просто странно, что они передаются функции. # i f d e f WIN NT _declspec (dllexport) #endif OCINumber * return_number (OCIExtProcContext * ctx, . . . . short * return i) double our_number = 123.456; OCINumber * return_value; myCtxStruct * myCtx; *return i - OCI IND NULL;
308
Глава 18 i f ((myCtx = i n i t ( c t x ) ) — NULL) return NULL; debugf(myCtx, "Входим в функцию, возвращающую Number");
Здесь необходимо выделить память для возвращаемого числа. Нельзя использовать стековую переменную, поскольку при возврате значения она выходит из области действия. При выделении памяти с помощью функции malloc произойдет утечка памяти. Использовать статическую переменную тоже нельзя, поскольку из-за кеширования внешних процедур другой сеанс может изменить значение, на которое мы сослались, после его возврата (но до того, как сервер Oracle его скопирует). Единственно корректный способ — выделить память следующим образом: return_value • (OCINumber *)OCIExtProcAllocCallMemory(ctx, if(return_value == NULL) raise_application_error(myCtx,
sizeof(OCINumber));
ERROR_OCI_ERROR,"%s", "не хватает памяти");
else if
(OCINumberFromReal(myCtx->errhp, &our_nuiriber, sizeof(our_number), return_value) != OCI_SUCCESS) raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx));
*return_i = OCI_IND_NOTNULL; }
term(myCtx); return return_value; Возврат даты очень похож на возврат числа. Возникают те же проблемы с памятью. Выделяется память для возвращаемого значения типа DATE, в нее записывается значение, устанавливается значение индикаторной переменной и возвращается результат: #ifdef WIN_NT _declspec (dllexport) #endif OCIDate * return_date (OCIExtProcContext * ctx, short * return_i) {
OCIDate * return_value; myCtxStruct * myCtx; if ((myCtx = init(ctx)) == NULL) return NULL; debugf(myCtx, "Входим в функцию, возвращающую данные типа Date"); return_value = (OCIDate *)OCIExtProcAllocCallMemory(ctx, sizeof(OCIDate)); if (return_value — NULL)
Внешние процедуры на языке С
raise_application_error(myCtx, ERROR_OCI_ERROR, "%s", "не хватает памяти"); else *return_i = OCI_IND_NULL; if (OCIDateSysDate(myCtx->errhp, return_value) != OCI_SUCCESS) { raise_application_error(myCtx,ERROR_OCI_ERROR, "%s",lastOciError(myCtx)); •return i = ОСI IND NOTNULL; — — — term(myCtx); return return_value; При возврате строковых данных (VARCHAR) будут использоваться два параметра: индикаторная переменная и поле LENGTH. В этом случае, как и для параметра, переданного в режиме OUT, задается поле LENGTH, чтобы в вызывающей среде была известна длина возвращенной строки. Представленные выше соображения во многом применимы и при возврате строк: выделяется память, устанавливается индикаторная переменная, задается и возвращается значение: •ifdef WIN_NT _declspec (dllexport) #endif char * return_string (OCIExtProcContext * short * int *
ctx, tx, return i , return_l)
char * data_we_want_to_return = "Hello World!"; char * return value; — myCtxStruct * myCtx; if ((myCtx = init(ctx)) == NULL) return NULL; debugf(myCtx, "Входим в функцию, возвращающую строку " ) ; return_value = (char *)OCIExtProcAllocCallMemory(ctx, strlen(data_we_want_to_return)+1); if(return_value —> NULL) raise_application_error(myCtx, ERROR_OCI_ERROR, "%s", "не хватает памяти"); else {
*return_i = OCI_IND_NULL; strcpy(return_value, data_we_want_to_return); •return 1 = strlen(return value);
310
Глава 18 * r e t u r n _ i = OCI_IND_NOTNULL; } term(myCtx); r e t u r n return_value;
}
Мы рассмотрели код на языке С, необходимый для демонстрации способов передачи всех основных типов данных в режиме IN и IN/OUT, а также возврата данных соответствующих типов из функций. Было представлено также множество функций библиотеки OCI, использующихся во внешних процедурах, в частности функции для создания и получения контекста с целью поддержки информации о состоянии, функции для обработки файлов параметров, создания и записи файлов ОС. Не продемонстрировано следующее. •
Передача и получение данных сложных объектных типов во внешних процедурах. Делается это примерно так же, как и в примерах с массивами (поскольку там передавались и принимались данные простых объектных типов). Для работы с входными и выходными данными объектных типов используются предоставляемые библиотекой OCI средства работы с компонентами объекта.
•
Возврат всех необходимых типов из функций. Я представил только возврат строк, дат и чисел. Возврат данных остальных типов выполняется аналогично (несколько проще для данных типа int, поскольку при этом не надо выделять память).
Сейчас мы рассмотрим файлы управления проектом, make-файлы, которые можно использовать для создания внешних процедур в среде ОС UNIX или Windows.
Создание внешней процедуры Давайте сначала рассмотрим универсальный make-файл для Windows: CPU=i386 MSDEV
= c:\msdev
(1)
ORACLE_HOME = c : \ o r a c l e
(2)
! i n c l u d e <$(MSDEV)\include\win32.mak>
(3)
TGTDLL OBJS
(4) (5)
= extproc.dll - extproc.obj
NTUSER32LIBS
= $(MSDEV)\lib\user32.lib \ $(MSDEV)\lib\msvcrt.lib \ $(MSDEV)\lib\oldnames.lib \ $(MSDEV)\lib\kernel32.lib \ $(MSDEV)\lib\advapi32.lib
(6)
SQLLIB
= $(ORACLE_HOME)\precomp\lib\msvc\orasql8.1ib $(ORACLE_HOME)\oci\lib\msvc\oci.lib
\
(7)
INCLS
= -1$(MSDEV)Unclude \ -I$(ORACLE_HOME)\oci\include \
(8)
CFLAGS
= $(INCLS) -DWIN32 -DWIN_NT -D_DLL
(9)
Внешние процедуры на языке С all:
$(TGTDLL)
clean: e r a s e *.obj
3 1 1
(10) (11) * . l i b *.exp
$(TGTDLL): $(OBJS) $ ( l i n k ) -DLL $ ( d l l f l a g s ) \ /NODEFAULTLIB:LIBC.LIB -out:$(TGTDLL) \ $(OBJS) \ $(NTUSER32LIBS) \ $(SQLLIB)
(12)
Выделенные полужирным числа в круглых скобках не являются частью файла управления проектом, они указаны лишь для возможности ссылки на них в дальнейшем. 1. Это каталог, в котором у меня установлен компилятор языка С. Я использую компилятор Microsoft Visual C/C++, который поддерживается в среде Windows. Значение этой переменной я использую позже в make-файле, когда необходимо сослаться на этот каталог. 2. Мой каталог ORACLE_HOME. Он используется при поиске включаемых файлов для ОС1/Рго*С и стандартных библиотек Oracle. 3. Я включаю стандартный шаблон make-файла Microsoft. Он задает переменные $(link) и $(dllflags), которые могут быть разными для различных версий компилятора. 4. Переменная TGTDLL задает имя создаваемой DLL-библиотеки. 5. Переменная OBJS задает список объектных файлов, используемых при создании библиотеки. Если распределить код по нескольким файлам, в списке будет указано несколько объектных файлов. В нашем несложном примере используется только один объектный файл. 6. Переменная NTUSER32LIBS содержит список стандартных системных библиотек, с которыми выполняется компоновка. 7. Переменная SQLLIB содержит список необходимых библиотек Oracle. В данном примере я компоную библиотеки как Рго*С, так и OCI, хотя используются только библиотеки OCI. Но от включения библиотек Рго*С вреда не будет. 8. Переменная INCLS содержит список каталогов, в которых находятся необходимые включаемые файлы. В данном случае мне необходимы системные заголовочные файлы, а также заголовочные файлы сервера Oracle и текущий рабочий каталог. 9. Переменная CFLAGS — стандартный макрос языка С, используемый компилятором. Я определяю макрос -DWIN_NT, для условной компиляции кода, предназначенного для NT (например, _declspec(dllexport)). 10. Цель all: по умолчанию будет создавать DLL-библиотеку. 11. Цель clean: требует удалить временные файлы, созданные в ходе компиляции. 12. Цель TGTDLL требует выполнить команду, создающую DLL-библиотеку. Она скомпилирует и скомпонует весь необходимый код.
312
Глава 18
Как разработчик я постоянно использую этот make-файл. Обычно я изменяю только строку (4) — имя библиотеки и строку (5) — список объектных файлов. Остальные компоненты make-файла изменять после первоначального конфигурирования не придется. Выполнив команду nmake, мы увидим примерно следующее: C:\Documents and SettingsXThomas Kyte\Desktop\extproc\demo_passing>nmake Microsoft Copyright cl '--DWIN_NT Microsoft ^80x86 Copyright
(R) Program Maintenance U t i l i t y Version 1.60.5270 (c) Microsoft Corp 1988-1995. All r i g h t s reserved. -Ic:\msdev\include - I c : \ o r a c l e \ o c i \ i n c l u d e - I . -DWIN32 -D_DLL /c extproc.c (R) 32-bit C/C++ Optimizing Compiler Version 10.00.5270 for (C) Microsoft Corp 1984-1995. All r i g h t s reserved.
extproc.с link -DLL /NODEFAULTLIB:LIBC.LIB -out:extproc.dll extproc.obj с:\msdev\lib\user32.lib с:\msdev\lib\msvcrt.lib с:\msdev\lib\oldnames.lib с:\msdev\lib\kernel32.lib с:\msdev\lib\adv api32.lib с:\oracle\precomp\lib\msvc\orasql8.lib с:\oracle\oci\lib\msvc\oci.lib Microsoft (R) 32-Bit Incremental Linker Version 3.00.5270 Copyright (C) Microsoft Corp 1992-1995. All rights reserved. Creating library extproc.lib and object extproc.exp Библиотека extproc.dll создана и готова для использования. Теперь давайте перенесем ее в среду О С U N I X с помощью следующего файла управления проектом: MAKEFILE= $(ORACLE_HOME)/rdbms/demo/demo_rdbms.mk
(1)
INCLUDE= -I$(ORACLE_HOME)/rdbms/demo \ -1$(ORACLE_HOME)/rdbms/public \ -I$(ORACLE_HOME)/plsql/public \ -1$(ORACLE_HOME)/network/public
(2)
TGTDLL= extproc.so OBJS = extproc.о
(3) (4)
all: $(TGTDLL)
(5)
clean: rm *.o
(6)
$(TGTDLL): $(OBJS) $(MAKE) -f $(MAKEFILE) e x t p r o c _ c a l l b a c k \ SHARED_LIBNAME=$(TGTDLL) OBJS=$(OBJS)
(7)
CC=cc CFLAGS= -g - I . $(INCLUDE) -Wall
(8) (9)
И в этом случае выделенные полужирным шрифтом числа в круглых скобках не являются частью make-файла, а указаны лишь для возможности ссылок на них в дальнейшем.
Внешние процедуры на языке С
313
1. Имя/местонахождение стандартного make-файла Oracle. Я буду использовать этот файл для безошибочной компиляции и компоновки с необходимыми для данной платформы и версии Oracle библиотеками. Поскольку набор этих библиотек существенно отличается для каждого релиза, версии и платформы, я настоятельно рекомендую использовать этот make-файл. 2. Список каталогов для поиска включаемых файлов. Здесь я перечислил каталоги Oracle. 3. Имя создаваемой библиотеки. 4. Список файлов, образующих эту библиотеку. 5. Стандартная цель, которая будет создаваться. 6. Цель для удаления временных файлов, созданных в ходе сборки проекта. 7. Фактическая цель проекта. При ее построении для создания библиотеки внешних процедур используется стандартный make-файл, поставляемый корпорацией Oracle. Это снимает все проблемы с именами и местонахождением библиотек. 8. Имя компилятора языка С, который предполагается использовать. 9. Стандартный набор опций, которые необходимо передать компилятору языка С. С учетом того, как был написан код, перенос закончен. Осталось выполнить команду make и получить примерно такой результат: $ make ее -g - I . -I/export/home/ora816/rdbms/demo -I/export/home/ora816/rdbms/ public -I/export/home/ora816/plsql/publ±c -I/export/home/ora816/network/ public -Wall -c extproc.c -o extproc.o make -f /export/home/ora816/rdbms/demo/demo_rdbms.mk extproc_callback \ SHARED_LIBNAME=extproc.so OBJS=extproc.о makefl]: Entering directory Yaria-export/home/tkyte/src/demo_j?assing' Id -G -L/export/home/ora816/lib -R/export/home/ora816/lib -o extproc.so extproc.o -lclntsh 'sed -e 's/-ljava//g' /export/home/ora816/lib/ ldflags' -Insgr8 -Inzjs8 -In8 -Inl8 -Inro8 ~sed -e 's/-ljava//g' / export/home/ora816/lib/ldflags' -Insgr8 -Inzjs8 -In8 -Inl8 -Iclient8 -Ivsn8 -Iwtc8 -Icommon8 -Igeneric8 -Iwtc8 -lmm -Inls8 -Icore8 -Inls8 N Icore8 -Inls8 'sed -e 's/-ljava//g' /export/home/ora816/lib/ldflags -Insgr8 -Inzjs8 -In8 -Inl8 -Inro8 'sed -e 's/-ljava//g' /export/home/ ora816/lib/ldflags' -Insgr8 -Inzjs8 -In8 -Inl8 -Iclient8 -Ivsn8 Iwtc8 -Icommon8 -Igeneric8 -Itrace8 -Inls8 -Icore8 -Inls8 -Icore8 Inls8 -Iclient8 -Ivsn8 -Iwtc8 -Icommon8 -Igeneric8 -Inls8 -Icore8 Inls8 -Icore8 -Inls8 'cat /export/home/ora816/lib/sysliblist* "if [ -f /usr/lib/libsched.so ] ; then echo -lsched ; else true; fi' -R/export/ home/ora816/lib -laio -Iposix4 -lkstat -lm -lthread \ /export/home/ora816/lib/libpls8.a makefl]: Leaving directory Varia-export/home/tkyte/src/demo_j?assing' В результате, м ы получили файл библиотеки extproc.so для О С Solaris.
314
Глава 18
Установка и запуск Теперь, при наличии спецификации вызова, объекта-библиотеки, соответствующих типов данных, спецификации и тела пакета demo_passing в файле extproc.sql, а также библиотеки extproc.dll (или extproc.so), все готово для установки нашего демонстрационного примера в базе данных. Для этого мы выполним команду ©extproc.sql, а затем — ряд анонимных блоков, чтобы проверить работу внешних процедур. Необходимо настроить оператор CREATE LIBRARY так, чтобы он указывал на созданную .dll- или .soбиблиотеку: c r e a t e or replace l i b r a r y demoPassing as 'С:\<местонахождение DLL-6H6mroTeKH>\extproc.dll'; но остальная часть должна компилироваться без изменений. Итак, после запуска сценария extproc.sql можно проверить работу внешних процедур следующим образом: SQL> declare 2 l_input number; 3 l_output number; 4 begin 5 dbms_output.put_line('Передаем Number'); 6 7 dbms_output.put_line('Сначала проверяем передачу пустых *•* значений') ; 8 demo_passing_pkg.pass(l_input, l_output); 9 dbms_output.put_line('l_input = '||l_input|| *•» ' l_output - ' | |l_output); 10 11 l_input :- 123; 12 dbms_output.put_line ('Теперь проверяем передачу непустых *•* значений') ; 13 dbms_output.put_line('Предполагается, что в результате будет * -123'); 14 demo_passing_pkg.pass(l_input, l_output); 15 dbms_output.put_line('l_input = '|Il_input||' l_output = *• '||l_output); 16 end; 17 / Передаем Number Сначала проверяем передачу пустых значений l_input - l_output = Теперь проверяем передачу непустых значений Предполагается, что в результате будет -123 l_input = 123 l_output - -123 PL/SQL procedure successfully completed.
Тексты сообщений в тестовом примере переведены на русский язык. Если вы будете использовать соответствующие коды с сайта издательства (http://www.apress.com), сообщения будут выдаваться на английском языке. Прим. научн. ред.
Внешние процедуры на языке С
315
У меня есть простой анонимный блок для поочередной проверки каждой из созданных процедур и функций. Я не буду приводить здесь результат выполнения каждой команды. В код демонстрационного примера на сайте издательства включен сценарий test_all.sql, выполняющий каждую процедуру и функцию и выдающий результат, подобный представленному выше. После установки можно выполнить его, чтобы увидеть в работе каждую из внешних процедур. Теперь, если вы помните код на языке С, там был ряд вызовов debugf. Если после выполнения представленного выше PL/SQL-блока просмотреть файл ext_proc.log во временном каталоге, можно увидеть результаты вызовов debugf. Они будут иметь следующий вид: 000809 000809 000809 000809 000809 000809 123, а
185056 GMT ( extproc.c,176) Входим в функцию Pass Number 185056 GMT ( extproc.c,183) Получена среда Oci 185056 GMT ( extproc.c,176) Входим в функцию Pass Number 185056 GMT ( extproc.c,183) Получена среда Oci 185056 GMT ( extproc.c,209) Первый параметр: 123 185056 GMT ( extproc.c,230) Устанавливаем параметр OUT равным индикаторную переменную — равной NOTNULL
Это показывает, что 9 августа 2000 года (000809) в 6:50:56 вечера (185056) по Гринвичу (GMT) из строки 176 исходного кода в файле extproc.c был выполнен вызов debugf с сообщением "Входим в функцию Pass Number". Далее записаны все остальные выполненные вызовы debugf. Как видите, при отладке очень удобно использовать трассировочный файл, запись информации в который можно включать и отключать при необходимости. Поскольку внешние процедуры выполняются на сервере, отладка их может оказаться очень сложным делом. Хотя возможность использовать обычный отладчик не исключена, на практике к ней прибегают очень редко.
Внешняя процедура для сброса большого объекта в файл (LOBJO) В Oracle 8.0 появился ряд новых типов данных: •
CLOB — Character Large Object (символьный большой объект);
•
BLOB — Binary Large Object (двоичный большой объект);
Q BFILE — Binary FILE (двоичный файл). Типы данных CLOB и BLOB позволяют сохранить в базе неструктурированные данные размером до 4 Гбайт. С помощью данных типа BFILE можно читать файлы ОС, находящиеся в файловой системе сервера. В составе сервера Oracle поставляется пакет DBMS_LOB, содержащий много утилит для работы с большими объектами. Он включает даже функцию loadfromfile для загрузки большого объекта из существующего файла ОС. Но в составе сервера Oracle, однако, не поставляется функция для записи большого объекта в файл ОС. Во многих случаях для данных типа CLOB можно использовать средства пакета UTL_FILE, но для данных типа BLOB это решение абсолютно не подходит. Сейчас мы займемся реализацией функции для записи в файл данных типа CLOB и BLOB в виде внешней процедуры на языке С, использующей средства прекомпилятора Рго*С.
316
Глава 18
Спецификация пакета LOBJO Снова начнем с оператора CREATE LIBRARY, затем определим спецификацию пакета, потом — тело пакета, задающее соответствующие подпрограммы для внешних Сфункций, и, наконец, реализуем эти функции на языке С с помощью прекомпилятора Рго*С. Создаем библиотеку в базе данных: tkyte@TKYTE816> c r e a t e or replace l i b r a r y lobToFile_lib 2 as 'C:\extproc\lobtofile\extproc.dll' 3 / Library created. Теперь переходим к спецификации создаваемого пакета. Она начинается с трех перегруженных функций для записи большого объекта в файл на сервере. Они вызываются одинаково и возвращают количество байтов, записанных на диск. После спецификаций перечислены исключительные ситуации, которые могут возбуждаться по ходу работы этих функций. tkyte@TKYTE816> c r e a t e or replace package lob_io 2 as 3 4 function write(p_path in varchar2, 5 p_filename in varchar2, p_lob in blob) 6 return binary_integer; 7 8 function write(p_path in varchar2, 9 p_filename in varchar2, p_lob in clob) 10 return binary_integer; 11 12 function write(p_path in varchar2, 13 p_filename in varchar2, p_lob in bfile) 14 return binary_integer; 15 16 IO_ERROR exception; 17 pragma exception_init(IO_ERROR, -20001); 18 19 CONNECT_ERROR exception; 20 pragma exception_init(CONNECT_ERROR, -20002); 21 22 INVALID_LOB exception; 23 pragma exception_init(INVALID_LOB, -20003); 24 25 INVALID_FILENAME exception; 26 pragma exception_init(INVALID_FILENAME, -20004); 27 28 OPEN_FILE_ERROR exception; 29 pragma exception_init(OPEN_FILE_ERROR, -20005); 30 31 LOB_READ_ERROR exception; 32 pragma exception_init(LOB_READ_ERROR, -20006); 33
Внешние процедуры на языке С
3 1У
34 end; 35 / Package c r e a t e d . Здесь мы каждому коду ошибки (эти коды ошибки определены с помощью директив #define ERROR_ в начале файла с исходным текстом внешних процедур) сопоставляем имя исключительной ситуации PL/SQL. Это удобное дополнение, позволяющее пользователю пакета перехватывать исключительные ситуации с определенными именами следующим образом: exception when lob io.IO ERROR then when l o b io.CONNECT ERROR t h e n
или при желании получать вместо исключительных ситуаций коды ошибок и тексты сообщений об ошибках: exception when others then if (sqlcode = -20001 ) then — (это была ошибка ввода-вывода) ... elsif ( sqlcode = -20002 ) then — (это была ошибка подключения) ... и так далее Даже не просматривая соответствующий код на языке С, легко понять, какие ошибки могут возникнуть во внешней процедуре Теперь переходим к телу пакета. В нем каждой из представленных выше спецификаций PL/SQL-функций сопоставляется С-функция из библиотеки lobToFile: tkyte@TKYTE816> create or replace package body lob_io 2 as 3 4 function write(p_path in varchar2, p_filename in varchar2, p_lob in ^ blob) 5 return binary_integer 6 as 7 language С name "lobToFile" library lobtofile_lib 8 with context parameters (CONTEXT, 9 p_path STRING, p_path INDICATOR short, 10 p_filename STRING, p_filename INDICATOR short, 11 P_lob OCILOBLOCATOR, p_lob INDICATOR short, 12 RETURN INDICATOR short); 13 14 15 function write(p_path in varchar2, p_filename in varchar2, p_lob in "•• clob) 16 return binary integer 17 as 18 language С name "lobToFile" library lobtofile_lib 19 with context parameters (CONTEXT, 20 p__path STRING, p_j>ath INDICATOR short.
318
Глава 18
21 р filename STRING, p_filename INDICATOR short, 22 p~lob OCILOBLOCATOR, p_lob INDICATOR short, 23 RETURN INDICATOR short); 24 25 26 function write(p_path in varchar2, p_filename in varchar2, p_lob in *• bfile) 27 return binary_integer 28 as 29 language С name "lobToFile" library lobtofile_lib 30 with context parameters (CONTEXT, 31 p_path STRING, p_path INDICATOR short, 32 p_filename STRING, p_filename INDICATOR short, 33 p_lob OCILOBLOCATOR, p_lob INDICATOR short, 34 RETURN INDICATOR short); 35 36 end lob_io; 37 / Package body created. Интересно отметить, что все три функции сопоставляются одной и той же внешней С-функции. Я не писал отдельные функции для данных типа CLOB, BLOB и BFILE. Поскольку любой большой объект передается как данные типа OCILOBLOCATOR, все их можно обрабатывать одной и той же функцией. Как обычно, я передаю индикаторную переменную для каждого формального параметра и для возвращаемого значения. Хотя это и не обязательно, но очень рекомендуется.
Код Рго*С для пакета LOBJO Теперь рассмотрим код для прекомпилятора Рго*С, реализующий библиотеку lobtofile_lib. Я не буду комментировать универсальный код, рассмотренный в первом примере, чтобы сократить текст (функции debugf, raise_application_error, ociLastError, term и init — такие же, за исключением того, что в функции init в приложениях на Рго*С используется конструкция EXEC SQL REGISTER CONNECT), и перейду сразу к коду специфическому. Следует отметить, что представленный далее код должен идти после представленного ранее "шаблонного" кода, и что в шаблоне необходимо убрать комментарии с разделов, связанных с подключением в Рго*С. Начнем с определения всех ошибок, о которых будет выдаваться сообщение. Этот набор кодов ошибок должен в точности соответствовать кодам ошибок для исключительных ситуаций, заданных в спецификации PL/SQL-пакета. Гарантировать это соответствие невозможно; это всего лишь договоренность, которой я привык следовать, но следовать ей, определенно, стоит. #define #define •define •define •define •define
ERROR_FWRITE ERROR_REGISTER_CONNECT ERROR_BLOB_IS_NULL ERROR_FILENAME_IS_NULL ERROR_OPEN_FILE ERROR LOB READ
20001 20002 20003 20004 20005 20006
Внешние процедуры на языке С
319
Дальше идет внутренняя функция, непосредственно из PL/SQL недоступная, которая будет использоваться основной функцией lobToFile для записи данных в файл. Она также подсчитывает количество байтов, записанных в файл: s t a t i c int writeToFile(myCtxStruct * OCIFileObject char * int int * ( bytesWritten; ub4
myCtx, output, buff, bytes, totalWritten) totalWritten)
debugf(myCtx, "Записываем %d байтов в файл", bytes); if (OCIFileWrite(myCtx->envhp, myCtx->errhp, output, buff, bytes, SbytesWritten) != OCI_SUCCESS) return raise_application_error (myCtx, ERROR_FWRITE, "Error writing to file lastOciError(myCtx)); } if (bytesWritten != bytes) { return raise_application_error (myCtx, ERROR_FWRITE, "Ошибка записи в файл %d байт, записано только %d байт", bytes, bytesWritten); *totalWritten += bytesWritten; return 0; ) Первый параметр этой функции — контекст сеанса. Этот контекст должен передаваться всем вызываемым функциям, чтобы можно было использовать такие утилиты, как raise_application_error. Следующий параметр — выходной файл, в который будут записываться данные. Для выполнения ввода-вывода используются переносимые функции OCIFile. Предполагается, что перед вызовом функции writeToFile соответствующий файл уже открыт. Далее идут указатели на записываемый буфер и количество байтов в буфере. Последней передается переменная-счетчик, в которой хранится общее количество записанных байтов. Теперь переходим к основной (и последней) функции. Эта функция выполняет все необходимые действия; она принимает локатор большого объекта (независимо от типа объекта — BLOB, CLOB или BFILE) и записывает его содержимое в указанный файл: #ifdef WIN_NT _declspec (dllexport) #endif int lobToFile(OCIExtProcContext * ctx, char * path,
320
Глава 18 short char * short OCIBlobLocator * short short *
path i, filename, filename i, blob, blob_i, return indicator)
Следующая часть кода задает структуру, в которую мы будем выбирать данные. Она содержит начальное поле размера, в байтах, а затем — пространство для данных размером 64 Кбайта. Мы будем выбирать данные из большого объекта порциями по 64 Кбайта и записывать их на диск. Затем определяются необходимые локальные переменные: typedef s t r u c t long_varraw ub4 len; t e x t buf[65536]; } long_varraw; EXEC SQL ТУРЕ longjvarraw IS LONG VARRAW(65536); long_varraw ub4
data; amt;
/* в эту структуру мы будем выбирать данные */ /* здесь будет храниться количество выбранных */ /* байтов */ ub4 buffsize = s i z e o f ( d a t a . b u f ) ; /* это количество байтов */ /* мы запрашиваем */ int offset = 1 ; /* с какой позиции большого объекта мы */ /* читаем данные */ OCIFileObject* output = NULL; /* файл, в который выполняется запись */ int bytesWritten = 0 ; /* сколько байтов всего ЗАПИСАНО */ myCtxStruct * myCtx; *return_indicator = OCI_IND_NULL; i f ((myCtx=init(ctx)) = NULL) return 0; Начнем с проверки индикаторов пустых значений. Если установлена любая из индикаторных переменных, необходимо вернуть сообщение об ошибке. Это показывает, почему важно всегда передавать индикаторные переменные во внешние процедуры на языке С. Никогда нельзя быть уверенным, что пользователь случайно не передал пустое значение. Попытка без предварительной проверки обратиться к файлу с указанным именем или к большому объекту, которые окажутся пустыми, может закончиться катастрофически (внешняя процедура закончится неудачно), поскольку параметры окажутся не проинициализированными. if (blob i == OCI IND NULL) raise_application_error (myCtx, ERROR_BLOB_IS_NULL, "Функции lobToFile передан пустой большой объект; ^недопустимый аргумент"); } else if (filename_i — OCI_IND_NULL I| path_i — OCI_IND_NULL)
Внешние процедуры на языке С
3 2 1
{
raise_application_error (myCtx, ERROR_FILENAME_IS_NULL, "Функции lobToFile передано пустое шля файла или каталога; ^недопустимый аргумент"); }
Теперь откроем файл. М ы открываем его на запись в двоичном режиме. М ы хотим просто сбросить байты из базы данных в файл. else if (OCIFileOpen(myCtx->envhp, myCtx->errhp, soutput, filename, path, OCI_FILE_WRITE_ONLY, OCI_FILE_CREATE, OCI_FILE_BIN) ! = OCI_SUCCESS) { raise_application_error(myCtx, ERROR_OPEN_FILE, "Ошибка открытия файла '%s'", lastOciError(myCtx)); else { debugf(myCtx, "lobToFile(filename => '%s%s', lob => %X)", path, filename, blob); Теперь мы будем читать большой объект с помощью средств Рго*С методом без опроса (non-polling). Это важно, поскольку "опрашивать" большой объект во внешней процедуре нельзя. Таким образом, мы никогда не запросим больше данных, чем можем получить в одном вызове (non-polling). Мы начинаем со смещения 1 (с первого байта) и будем читать по BUFSIZE байтов за раз (64 Кбайта в данном случае). Каждый раз увеличивая смещение на прочитанное количество байтов, мы выйдем из цикла только когда считано будет столько байтов, сколько запрошено — это будет означать, что прочитан весь большой объект. for( offset = 1, amt = buffsize; amt == buffsize; offset += amt ) {
debugf(myCtx, "Попытка прочитать %d байт из большого объекта " , amt); EXEC SQL LOB READ :amt FROM :blob AT :offset INTO :data WITH LENGTH :buffsize; Проверяйте все возможные ошибки — при их возникновении выдавайте собственные сообщения об ошибках в стек ошибок среды PL/SQL. Обратите внимание, как мы освобождаем все использованные ресурсы (открытый файл) перед завершением работы. Это важно. По возможности, надо предотвращать
11 3». 244
322
Глава 18
утечку ресурсов. Для этого м ы возвращаем управление только в одном месте (ниже) и перед этим вызываем функцию term: if (sqlca.sqlcode < 0) break; if (writeToFilefmyCtx, output, data.buf, amt, SbytesWritten)) break;
Осталось закрыть файл и вернуть управление: if (output != NULL) debugf(myCtx, "Закончили запись и закрываем файл"); OCIFileClose(myCtx->envhp, myCtx->errhp, output); *return_indicator = OCI_IND_NOTNULL; debugf(myCtx, "Возвращаем значение %d как количество прочитанных байтов", bytesWritten); term(myCtx); return bytesWritten; >
Создание внешней процедуры Процесс создания библиотеки lobtoflle почти совпадает с рассмотренным ранее для библиотеки demo_passing. Универсальный файл управления проектом (make-файл) использовался как в среде Windows, так и в ОС UNIX с минимальными изменениями. В Windows мы используем: CPU=i386 MSDEV = c:\msdev ORACLE_HOME = c:\oracle !include <$(MSDEV)\include\win32.mak> TGTDLL = extproc.dll OBJS = lobtofile.obj NTUSER32LIBS
= $(MSDEV)\lib\user32.lib \ $(MSDEV)\lib\msvcrt.lib \ $(MSDEV)\lib\oldnames.lib \ $(MSDEV)\lib\kernel32.lib \ $(MSDEV)\lib\advapi32.lib
SQLLIB
= $(ORACLE_HOME)\precomp\lib\msvc\orasql8.1ib $(ORACLE_HOME)\oci\lib\msvc\oci.lib
INCLS
= -1$(MSDEV)\include \ -I$(ORACLE_HOME)\oci\include \ -I.
\
Внешние процедуры на языке С CFLAGS
3 2 3
= $(INCLS) -DWIN32 -DWIN_NT -D_DLL
a l l : $(TGTDLL) clean: erase *.obj *.lib *.exp lobtofile.с $(TGTDLL): $(OBJS) $(link) -DLL $(dllflags) \ /NODEFAULTLIB:LIBC.LIB -out:$(TGTDLL) \ $(OBJS) \ $(NTUSER32LIBS) \ $(SQLLIB) \ l o b t o f i l e . с : lobtofile.pc proc \ include=$(ORACLE_H0ME)\network\public \ include-$(ORACLE_HOME)\proc\lib \ include=$(ORACLE_H0ME)\rdbms\demo \ include=$(OBACLE_H0ME)\oci\include \ include=$(MSDEV) \include \ lines=yes \ parse»full \ iname=lobtofile. pc Изменения выделены полужирным шрифтом. Было изменено имя компонуемого объектного файла и добавлено правило для автоматического преобразования lobtofile.pc в Iobtofile.c. Вызванному прекомпилятору Рго*С мы сообщаем, где находятся заголовочные файлы (INCLUDE=), что номера строк следует сохранить в полученном .с-файле (Iines=yes), что требуется проанализировать код на языке С (parse=full) и что имя преобразуемого файла — lobtofile.pc (iname=). Теперь осталось выполнить команду nmake, и DLL-библиотека будет создана. В ОС UNIX make-файл имеет следующий вид: MAKEFILE= $(ORACLE_HOME)/rdbms/demo/demo_rdbms.mk INCLUDE= -I$(ORACLE_HOME)/rdbms/demo \ -I$(ORACLE_HOME)/rdbms/public \ -I$(ORACLE_HOME)/plsql/public \ -1$(ORACLE_HOME)/network/public TGTDLL= extproc.so OBJS = lobtofile.о all: $(TGTDLL) clean:
Iobtofile.c: lobtofile.pc proc \ include=$(ORACLE_HOME)/network/public \ include=$(ORACLE_HOME)/proc/lib \ include»$(ORACLE_H0ME)/rdbms/demo \ include=$(ORACLE_HOME)/rdbms/public \
324
Глава 18 lines=yes \ iname=lobtofile.pc
extproc.so: lobtofile.c lobtofile.o $(MAKE) -f $(MAKEFILE) extproc_callback \ SHARED_LIBNAME=extproc.so OBJS="lobtofile.o" CC=cc CFLAGS= -g -I. $(INCLUDE) Для ОС UNIX мы сделали такие же изменения, как и в среде Windows. Мы просто добавили команду для вызова прекомпилятора Рго*С и изменили имя компонуемого объектного файла. Набираем команду make и получаем соответствующий .so-файл. Теперь все готово для его проверки и использования.
Установка и использование пакета LOBJO Осталось только выполнить операторы CREATE LIBRARY, CREATE PACKAGE и CREATE PACKAGE BODY. При этом пакет LOB_IO будет установлен в базе данных. Для его тестирования мы используем пару анонимных PL/SQL-блоков. Первый блок будет проверять средства выявления и обработки ошибок. Вызовем внешнюю процедуру и намеренно передадим ей некорректные входные данные, неверные имена каталогов и т.п. Вот этот блок с комментариями, описывающими, что мы должны получить на каждом шаге: SQL> REM д л я NT
SQL> REM задайте РАТН=с:\temp\ SQL> REM задайте CMD=fc /b SQL> REM для UNIX SQL> задайте PATH=/tmp/ SQL> задайте CMD="diff -s" SQL> drop table demo; Table dropped. SQL> create table demo(theBlob blob, theClob clob); Table created. SQL> DOO D0O DOO D0O D0O DOO SQL> SQL> 2 3 4 5 6 7
/* * В следующем блоке проверяюеся все условия возникновения выявленных * нами ошибок. Не проверяются ошибки IO_ERROR (для этого надо, чтобы * на диске не осталось свободного места или запись была невозможна * по другой подобной причине) и CONNECT_ERROR (это не должно * случиться *никогда*) */ declare l_blob blob; l_bytes number; begin /* * Пытаемся передать большой объект NULL
Внешние процедуры на языке С
325
8 */ 9 begin 10 l_bytes := lob_io.write('&PATH', 'test.dat1, l_blob); 11 exception 12 when lob_io.INVALID_LOB then 13 dbms_output.put_line('недопустимый аргумент перехвачен, *-» как и ожидалось') ; 14 dbms_output.put_line(rpad('-',70,'-')); 15 end; 16 17 /* 18 * Теперь попытаемся передать реальный большой объект и имя ••» файла NULL 19 */ 20 begin 21 insert into demo (theBlob) values(empty_blob()) 22 returning theBlob into l_blob; 23 24 l_bytes := lob_io.write(NULL, NULL, l_blob); 25 exception 26 when lob_io.INVALID_FILENAME then 27 dbms_output.put_line('недопустимый аргумент снова *"• перехвачен, как и ожидалось') ; 28 dbms_output.put_line(rpad('-',70,'-')); 29 end; 30 31 /* 32 * Теперь попытаемся передать существующий большой объект, но *•* несуществующий каталог 33 */ 34 begin 35 l_bytes := lob_io.write('/nonexistent/directory', 'x.dat', "* l_blob); 36 exception 37 when lob_io.OPEN_FILE_ERROR then 38 dbms_output.put_line('перехватили ошибку открытия файла, *• как и ожидалось' ) ; 39 dbms_output.put_line(sqlerrm); 40 dbms_output.put_line(rpad('-',70,'-')); 41 end; 42 43 /* 44 * Теперь запишем объект, чтобы проверить работу функции 45 */ 46 l_bytes := lob_io.write('&РАТН', 'l.daf, l_blob); 47 dbms_output.put_line('Успешно записали ' || l_bytes || *•» ' байтов' ) ; 48 dbms_output.put_line(rpad('-',70,'-')); 49 50 rollback; 51
326
Глава 18
52 /* 53 * Теперь у нас есть непустой большой объект, НО ыы выполнили 54 * откат, так что локатор большого объекта стал недействительный. 55 * Давайте посмотрим, что выдаст внешняя процедура в данной *• случае... 56 */ 57 begin 58 l_bytes :- lob_io.write('SPATH', '1.dat', l_blob); 59 exception 60 when lob_io.LOB_READ_ERROR then 61 dbms_output.put_line('перехватили ошибку чтения большого ** объекта, как и ожидалось'); 62 dbms_output.put_line (sqlerrm); 63 dbms_output.put_line(rpad('-',70,'-')); 64 end; 65 end; 66 / old 10: l_bytes := lob_io.write('SPATH', ' t e s t . d a t ' , l_blob); new 10: l_bytes := l o b _ i o . w r i t e ( ' / t m p / ' , ' t e s t . d a t ' , l j b l o b ) ; old 46: l_bytes := lob_io.write С&РАТН', ' l . d a t ' , l_blob); new 46: l_bytes := l o b _ i o . w r i t e ( ' / t m p / ' , ' l . d a t ' , l_blob); old 58: l_bytes := lob_io.write('&PATH', ' l . d a t ' , l_blob); new 58: l_bytes := l o b _ i o . w r i t e ( ' / t m p / ' , ' l . d a t ' , l_blob); недопустимый аргумент перехвачен, как и ожидалось недопустимый аргумент снова перехвачен, как и ожидалось перехватили ошибку открытия файла, как и ожидалось ORA-20005: Error opening f i l e 'ORA-30152: F i l e does not e x i s t ' Успешно записали 0 байт PL/SQL procedure successfully completed. Как видите, все произошло именно так, как и ожидалось. Мы намеренно сделали несколько ошибок и получили соответствующие сообщения. Теперь используем пакет по прямому назначению. Для этого теста я создал объект-каталог в базе данных, соответствующий моему временному каталогу (/tmp в ОС UNIX, C:\temp\ в Windows). Объект-каталог используется при работе с данными типа BFILE, позволяя читать файлы в указанном каталоге. В файловую систему ОС (/tmp или C:\temp\) я помещу тестовый файл something.big. Это достаточно большой файл для проверки внешней процедуры. Его содержимое не имеет значения. Мы загрузим этот файл в столбец типа CLOB, затем — в столбец типа BLOB и, наконец, во временный большой объект. Затем запишем каждый из этих больших объектов в отдельный файл с помощью созданной внешней процедуры. В завершение с помощью утилит ОС (diff в UNIX и FC в Windows) сравним сгенерированные файлы с исходным: SQL> create or replace directory my_files as '&PATH.'; old 1: create or replace directory my_files as '&PATH.' new 1: create or replace directory my files as '/tmp/'
Внешние процедуры на языке С
3 2 7
Directory created. SQL> SQL> declare 2 l_blob blob; 3 l_clob clob; 4 l_bfile bfile; 5 begin 6 insert into demo 7 values (empty_blob(), empty_clob()) 8 returning theBlob, theClob into l_blob, l_clob; 9 10 l_bfile := bfilename ('MY_FILES', 'something.big'); 11 12 dbms_lob.fileopen(l_bfile); 13 14 dbms_lob.loadfromfile(l_blob, l_bfile, 15 dbms_lob.getlength(l_bfile)); 16 17 dbms_lob.loadfromfile(l_clob, l_bfile, 18 dbms lob.getlength(1 bfile)); 19 " 20 dbms_lob.fileclose(ljbfile); 21 commit; 22 end; 23 / PL/SQL procedure successfully completed. Итак, мы загрузили файл something.big в базу данных (сначала — как данные типа BLOB, затем — как данные типа CLOB). Теперь снова запишем содержимое этих больших объектов во внешние файлы: SQL> declare 2 l_bytes number; 3 l_bfile bfile; 4 begin 5 for x in (select theBlob from demo) 6 loop 7 l_bytes := lob_io.write('&PATH','blob.dat', x.theBlob); 8 dbms_output.put_line('Записали ' || l_bytes ||' байтов *-* данных типа blob'); 9 end loop; 10 11 for x in (select theClob from demo) 12 loop 13 ljsytes := lob_io.writeC&PATH','clob.dat1, x.theclob); 14 dbms_output.put_line('Записали ' || l_bytes ||' байтов ^» данных типа clob') ; 15 end loop; 16 17 l_bfile := bfilename ('MY_FILES', 'something.big'); 18 dbms_lob.fileopen(l_bfile); 19 1 bytes := lob io.write('SPATH1,'bfile.dat', l_bfile);
328
Глава 18
20 dbms_output.put_line('Записали -> типа b f i l e ' ) ;
21 22 enc 23 / 7: old new 7: old 13: new 13: old 19: new 19: Записали Записали Записали
dbms l o b . f i l e c l o s e ( l
I| l_bytes
|I ' байтов данных
bfile);
1_ bytes : = lob IO.write( '&PATH', 'blob.dat 1 , x.theBlob); 1~ bytes := lob" io.write( '/trap/1, 'blob.dat', x.theBlob); l"bytes := lob] io.write( '&PATH', 'clob.dat', x.theclob); l"bytes := lob" io.write( 1 /tmp/', 'clob.dat', x.theclob); l_bytes := lob_io.write('&PATH','bfile.dat', l_bfile); l_bytes := lob_io.write( ' / t m p / ' , ' b f i l e . d a t ' , l_bfile); 1107317 байт данных типа blob 1107317 байт данных типа clob 1107317 байт данных типа b f i l e
PL/SQL procedure successfully completed. Это показывает, что мы успешно вызвали внешнюю процедуру и трижды записали файл. Каждый раз размер файла был одним и тем же (как и ожидалось). Теперь создадим временный большой объект, скопируем в него файл и запишем объект в другой файл, чтобы убедиться, что можно работать и с временными большими объектами: SQL> declare 2 l_tmpblob blob; 3 l_blob blob; 4 l_bytes number; 5 begin б select theBlob into l_blob from demo; 7 8 dbms_lob.createtemporary(l_tmpblob,TRUE); 9 10 dbms_lob.copy(l_tmpblob,l_blob,dbms_lob.getlength(l_blob),1,1); 11 12 l_bytes := lob_io.write('&PATH','tempblob.dat', l_tmpblob); 13 dbms_output.put_line('Записали ' || l_bytes II байтов временного ** большого объекта'); 14 15 DBMS_LOB.FREETEMPORARY(l_tmpblob); 16 END; 17 / old 12: l _ b y t e s := l o b _ i o . w r i t e ( ' & P A T H ' , ' t e m p b l o b . d a t ' , l _ t m p b l o b ) ; new 12: l _ b y t e s := l o b _ i o . w r i t e ( ' / t m p / 1 , ' t e m p b l o b . d a t ' , l _ t m p b l o b ) ; Записали 1107317 байтов временного большого объекта PL/SQL p r o c e d u r e s u c c e s s f u l l y
completed.
Таким образом, запись прошла успешно и, к счастью, записано точно такое же количество байтов. Последний шаг — проверить с помощью утилит ОС, что записанные файлы идентичны загруженному файлу: SQL> host &CMD &РАТН.something.big sPATH.blob.dat F i l e s /tmp/something.big and /tmp/blob.dat are i d e n t i c a l SQL> host &CMD &PATH.something.big SPATH.clob.dat F i l e s /tmp/something.big and /tmp/clob.dat are i d e n t i c a l
Внешние процедуры на языке С
329
SQL> host SCMD &PATH.something.big SPATH.bfile.dat Files /tmp/something.big and /tmp/bfile.dat are identical SQL> host &CMD &PATH.something.big &PATH.tempblob.dat Files /tmp/something.big and /tmp/tempblob.dat are identical М ы проверили работу нового пакета LOB_IO.
Возможные ошибки Ниже представлен список типичных ошибок, которые могут возникнуть при использовании внешних процедур. Некоторые из них мы уже обсуждали, например ошибку, возникающую при неправильном конфигурировании процесса прослушивания или файла TNSNAMES.ORA. Но многие ошибки еще не рассмотрены. Мы займемся ими сейчас: я расскажу, когда они могут возникать и что сделать, чтобы их исправить. Все эти сообщения об ошибках описаны также в руководстве Oracle 8i Error Messages Manual.*
ORA-28575 "невозможно открыть соединение RPC с агентом внешней процедуры" 28575, 00000, "unable to open RPC connection to external procedure agent" // * Причина: инициализация подключения по сети к агенту внешних // процедур не прошла успешно. Это может быть связано с // проблемами сети, неправильной конфигурацией процесса // прослушивания или некорректным кодом переноса. // * Действие: проверить конфигурацию процесса прослушивания в файлах // LISTENER.ORA и TNSNAMES.ORA или проверить сервер Oracle // Names. Эта ошибка почти всегда свидетельствует об ошибках конфигурации в файлах T N S N A M E S . O R A или LISTENER.ORA. Возможные причины ее возникновения уже рассматривались ранее, в разделе "Конфигурирование сервера".
ORA-28576 "потеряно соединение RPC с агентом внешней процедуры" 28576, 00000, "lost RPC connection to external procedure agent" // * Причина: произошла фатальная ошибка сетевого соединения, в агенте // внешних процедур или в вызванной внешней процедуре после // успешной организации взаимодействия. // * Действие: сначала проверьте вызываемый код внешней процедуры, // поскольку наиболее вероятной причиной получения этого // сообщения об ошибке является аварийное завершение // вызванной С-функции. Если — нет, проверьте сеть. Устраните // проблему, если она обнаружена. Если все компоненты // функционируют нормально, но проблема остается, она может // быть связана с внутренней логической ошибкой в коде // передачи RPC (вызова удаленной процедуры). Свяжитесь с // представителем службы поддержки. * Текст сообщения об ошибке сначала приведен так, как он выдается СУБД Oracle версии 8.0.5.0.0 при установке русского языка для сообщений. В примерах и описаниях оставлены сообщения на английском языке. - Прим. научн. ред.
330
Глава 18
Это сообщение об ошибке при обращении к внешней процедуре почти наверняка связано с ошибкой в разработанном вами коде. Эта ошибка возникает, когда "исчезает" внешний процесс. Это происходит в случае "слета" программы. Например, я добавил строку: char * return string (OCIExtProcContext * ctx, return_ i/ short * int * return 1) * r e t u r n _ i = OCI_IND_NOTNULL; *(char*)NULL - 1/ return return_yalue; }
в конце текста функции return_string. После перекомпиляции обнаружилось следующее:
[email protected]> exec dbms_output.put_line( demo_passing_pkg.return_string) BEGIN dbms_output.put_line(demo_passing_pkg.return_string); END; * ERROR a t l i n e 1: ORA-28576: l o s t RPC connection t o external procedure agent Это сообщение об ошибке будет выдаваться до тех пор, пока вы не устраните ошибку в исходном коде.
ORA-28577 "аргумент %s внешней процедуры %s имеет неподдерживаемый тип данных %s" 28577, 00000, '-•datatype % s " // * Причина: // // * Действие: //
"argument %s of external procedure %s has unsupported при передаче аргументов внешней процедуры агенту обнаружен тип данных, не поддерживаемый системой. найдите в документации список поддерживаемых типов данных аргументов внешних процедур.
Эта ошибка возникает при попытке передать из PL/SQL во внешнюю процедуру тип данных, не поддерживаемый данным интерфейсом. В частности, это относится к PL/SQL-таблицам. Если в примере demojassing объявить тип пшпАггау в спецификации пакета: type пшпАггау i s t a b l e of number index by binary_integer; procedure pass(p_in in numArray, p_out out numArray); а не как тип вложенной таблицы SQL, во время выполнения произойдет следующее: 1 2 3 4 5
declare l_input demo_passing_pkg.numArray; l_output demo_passing_pkg.пшпАггау; begin demo_pas s ing_pkg.pas s ( l _ i n p u t , l _ o u t p u t ) ;
Внешние процедуры на языке С
.3.3 1
6* end; SQL> / declare ERROR at line 1: ORA-28577: argument 2 of external procedure pass_numArray has unsupported datatype ORA-06512: a t "OPS$TKYTE.DEMO_PASSING_PKG", ORA-06512: a t l i n e 5
line 0
Причина в том, что передача PL/SQL-таблиц не поддерживается (можно передавать наборы, тип которых создан в базе данных, но не PL/SQL-таблицы).
ORA-28578 "ошибка протокола во время вызова внешней процедуры" 28578, 00000, "protocol error during callback from an external procedure" // * Причина: произошла внутренняя ошибка протокола при попытке // выполнить обращение (callback) к серверу Oracle из // созданной пользователем внешней процедуры. // * Действие: свяжитесь со службой поддержки Oracle. К счастью, я никогда не получал ни этого, ни приведенного ниже сообщения об ошибке. Оно свидетельствует о внутренней ошибке сервера Oracle. Единственное, что можно сделать, получив это сообщение об ошибке, — попытаться воспроизвести его с помощью небольшого тестового кода и сообщить о проблеме службе поддержки Oracle.
ORA-28579 "сетевая ошибка во время вызова от агента внешней процедуры" ORA-28579 "network error during callback from external procedure agent" // * Причина: произошла внутренняя ошибка сети при попытке выполнить // обращение (callback) к серверу Oracle из созданной // пользователем внешней процедуры. // * Действие: свяжитесь со службой поддержки Oracle.
ORA-28580 "рекурсивные внешние процедуры не поддерживаются" ORA-28580 "recursive external procedures are not supported" // * Причина: обращение из внешней процедуры привело к вызову другой // внешней процедуры. // * Действие: проверьте, не вызывает ли выполняемый при обращении к // серверу SQL-код (непосредственно или косвенно, например // при срабатывании триггера) другую внешнюю процедуру, // PL/SQL-процедуру, вызывающую внешние процедуры и т . д . Эта ошибка возникает при обращении из внешней процедуры к серверу, когда вызванная процедура обращается к другой внешней процедуре. Другими словами, внешняя процедура не может прямо или косвенно вызывать другую внешнюю процедуру. Это можно продемонстрировать, изменив .рс-файл для нашего пакета LOB_IO. Я добавил в этот файл следующее: {int x; exec sql execute begin
332
Глава 18
:х := demo_passing_pkg.return_nurnber; end; end-exec; if (sqlca.sqlcode < 0) { return raise_application_error (ctx, 20000, "Error:\n%.70s", sqlca.sqlerrm.sqlerrmc);
сразу после вызова REGISTER CONNECT. Теперь при попытке выполнения основной функции пакета lob_io мы получим:
[email protected]> declare x clob; у number; begin у :•> l o b _ i o . w r i t e ( ' x ' , x ) ; end; 2 / declare x clob; у number; begin у := lob_io.write('x', x ) ; end; * ERROR at line 1: ORA-20000: Error: ORA-28580: recursive external procedures are not supported ORA-06512: ORA-06512: a t "OPS$TKYTE.LOB_IO", l i n e 0 ORA-06512: a t l i n e 1 Единственное решение — никогда не вызывать из внешней процедуры другую внешнюю процедуру.
ORA-28582 "прямое соединение с этим агентом не разрешено" $ оегг ога 28582 28582, 00000, "a direct connection to this agent is not allowed" // * Причина: пользователь или инструментальное средство пытается // непосредственно подключиться к агенту внешних процедур или // к агенту гетерогенных служб (Heterogeneous Services), // например: // "SVRMGR> CONNECT SCOTT/TIGER@NETWORK_ALIAS". // Такие подключения не разрешены. //* Действие: при выполнении оператора CONNECT проверьте, не ссылается // ли связь базы данных или псевдоним на агента гетерогенных // служб или агента внешних процедур.
Это сообщение может быть выдано только после попытки подключиться к базе данных, если случайно указано имя службы, сконфигурированной для подключения к службе внешних процедур. ORA-06520 "PL/SQL: ошибка загрузки внешней библиотеки" $ оегг ога 6520 06520, 00000, "PL/SQL: Error loading external library" // * Причина: выявлена ошибка, связанная с попыткой динамической // загрузки внешней библиотеки в PL/SQL.
Внешние процедуры на языке С
333
// * Действие: другие сообщения об ошибках (если они есть) подскажут // причину выдачи этого сообщения. После этого сообщения об ошибке должно быть выдано специфическое сообщение об ошибке ОС. Чтобы продемонстрировать эту ошибку, я сделал следующее: $ ср l o b t o f i l e . p c extproc.so То есть скопировал исходный код поверх .so-файла, что, определенно, должно вызвать проблемы. Теперь при попытке вызвать внешнюю процедуру я получаю: declare x clob; у number; begin у := l o b _ i o . w r i t e ( ' х ' , х ) ; end; * ERROR a t l i n e 1: ORA-06520: PL/SQL: Error loading e x t e r n a l l i b r a r y ORA-06522: l d . s o . l : extprocPLSExtProc: f a t a l : /export/home/tkyte/src/ l o b t o f i l e / e x t p r o c . s o : unknown f i l e type ORA-06512: a t "OPS$TKYTE.LOB_IO", l i n e 0 ORA-06512: at l i n e 1 Итак, как видите, среди сообщений об ошибках есть сообщение ОС, свидетельствующее о неизвестном типе файла, что и поможет выявить причину возникновения ошибки (в данном случае все просто: при просмотре файла extproc.so оказывается, что он содержит исходный код на языке С).
ORA-06521 "PL/SQL: ошибка отображения функции" $ оегг ога 6521 06521, 00000, "PL/SQL: Error mapping function" // * Причина: ошибка возникла при попытке динамического сопоставления с // указанной функцией в PL/SQL. // * Действие: другие сообщения об ошибках (если они есть) подскажут // причину выдачи этого сообщения. Эта ошибка обычно возникает по одной из следующих причин: •
в имени внешней процедуры в оболочке PL/SQL или в коде на языке С сделана ошибка;
Q
разработчик забыл экспортировать функцию в Windows (_declspec( dllexport)).
Для демонстрации этой ошибки я изменил исходный код в файле lobtofile.pc следую щ и м образом: #ifdef WIN_NT _declspec (dllexport) fendif int xlobToFile(OCIExtProcContext * ctx, char * filename, Я добавил х к имени файла. Теперь при попытке выполнения мы получаем: declare x clob; у number; begin у := lob_io.write('х', х ) ; end; ERROR at line 1: ORA-06521: PL/SQL: Error mapping function
334
Глава 18
ORA-06522: ld.so.l: extprocPLSExtProc: fatal: lobToFile: can't find symbol ORA-06512: a t "OPS$TKYTE.LOB_IO", l i n e 0 ORA-06512: a t l i n e 1 Это показывает, что причина ошибки — can't find symbol, т.е. нет соответствия между именем, указанным в PL/SQL-коде, и именем функции во внешней библиотеке. Либо вкралась опечатка, либо имя функции не экспортировано (в среде Windows).
ORA-06523 "превышено максимальное количество аргументов" $ оегг ога 6523 06523, 00000, "Maximum number of arguments exceeded" // * Причина: имеется ограничение на количество аргументов, которые можно // передавать внешней функции. // * Действие: в документации для своей версии сервера и платформы // проверьте, как вычисляется максимальное количество // аргументов.
Это сообщение об ошибке можно получить в случае слишком большого списка параметров. Во внешние процедуры обычно можно передавать до 128 параметров (меньше, если передаются числа двойной точности, double, поскольку они занимают 8 байт, а не 4). При получении этого сообщения об ошибке, если действительно необходимо передавать столько параметров, проще всего обойти это ограничение с помощью набора. Например, следующий фрагмент: 1 declare 2 l_input strArray := strArrayO; 3 l_output strArray := strArray(); 4 begin 5 dbms_output.put_line('Pass strArray'); 6 for i in 1 .. 1000 loop 7 l_input.extend; 8 l_input(i) := 'Element ' || i; 9 end loop; 10 demo_passing_j3kg.pass(l_input, l_output); 11 dbms_output.put_line('l_input.count = ' II l_input.count || 12 ' l_output.count = ' II l_output.count); 13 for i in 1 .. l_input.count loop 14 i f (l_input(i) != l_output(i)) then 15 raise program_error; 16 end if; 17 end loop; 18* end; SQL> / Pass strArray l_input.count = 1000 l_output.count = 1 0 0 0 PL/SQL procedure successfully completed.
показывает, что с помощью набора я могу передавать внешней процедуре 1000 строк — во много раз превысив ограничение на количество параметров.
Внешние процедуры на языке С
335
ORA-06525 "неправильная длина для данных типа CHAR или RAW" 06525, 00000, "Length Mismatch for CHAR or RAW data" // * Причина: значение, указанное в переменной, задающей длину строки, // недопустимо. Эта ошибка может произойти, если в PL/SQL // переменная типа RAW указана в качестве параметра, // передаваемого в режиме IN OUT, OUT или в качестве // возвращаемого значения, но соответствующая переменная, // задающая длину, не передана. Эта ошибка может также // возникать при несоответствии заданного в переменной // значения длины фактической длине данных типа orlvstr или // orlraw. // // * Действие: исправьте код внешней процедуры и правильно задайте //
переменную, определяющую
длину.
Это сообщение об ошибке, если вы следуете моим принципам передачи и возврата параметров, может произойти только в случае возвращения из функции данных типа RAW и строки. Решение проблемы очень простое: надо правильно задать длину. Для пустого параметра типа RAW, переданного в режиме OUT, необходимо установить длину О, как делалалось в представленных выше примерах. Для непустого параметра типа RAW, переданного в режиме OUT, длина должна быть меньше или равна атрибуту MAXLEN. Аналогично, длина возвращаемой строки тоже должна устанавливаться правильно: меньше чем MAXLEN, но, поскольку память для строки выделяет внешняя процедура, значения MAXLEN она не получает, поэтому атрибут LENGTH должен быть меньше или равен 32760 (максимальное значение, которое может быть обработано в PL/SQL).
ORA-06526 "невозможно загрузить библиотеку PL/SQL" $ oerr ora 6526 06526, 00000, "Unable to load PL/SQL l i b r a r y " // * Причина: PL/SQL не смог загрузить библиотеку, на которую ссылается //
конструкция EXTERNAL. Это серьезная ошибка, которая обычно
//
не возникает.
II*
Действие: сообщите о проблеме службе поддержки.
Это внутренняя ошибка. Сообщение о ней выдаваться не должно, но если уж оно получено, возможны два варианта. Во-первых, это сообщение об ошибке может сопровождаться другим, более детальным сообщением. Например: ERROR a t l i n e 1: ORA-6526: Unable to load PL/SQL l i b r a r y ORA-4030: out of process memory when trying to a l l o c a t e 65036 bytes (callheap,KQL tmpbuf) Второе сообщение самоочевидно: не хватает памяти. Необходимо сократить объем используемой сеансом памяти. Если же сопровождающее ошибку ORA-6526 сообщение не позволяет понять причину ошибки, обращайтесь в службу поддержки.
336
Глава 18
ORA-06527 "ошибка SQLLIB внешней прецедуры: %s" $ oerr ora 6527 06527, 00000, "External procedure SQLLIB error: %s" // * Причина: при выполнении внешней процедуры, написанной с помощью // Рго*С, возникла ошибка в библиотеке sqllib. // // * Действие: тест сообщения позволит понять, какая именно ошибка // произошла в библиотеке SQLLIB. Обратитесь к руководству // Oracle Error Messages and Codes, где можно найти полное // описание причин ошибки, и выполните соответствующие // действия. С этой ошибкой все понятно. Более детальная информация о причинах будет представлена в сообщении.
Резюме В этой главе мы рассмотрели тонкости использования внешних процедур: • поддержку информации о состоянии с помощью контекстов; •
использование независимых от ОС функций для работы с -файлами;
•
параметризацию кода внешней процедуры с помощью внешних файлов параметров;
• оснащение кода средствами отладки (с помощью макроса debugf), позволяющими найти причину проблем; • приемы написания безопасного кода (всегда передавать контекст, всегда передавать индикаторы значений NULL и т.д.); Q как использовать универсальный шаблон для быстрой разработки внешних процедур с широкими функциональными возможностями; Q различия между внешними процедурами, использующими исключительно библиотеку OCI, и процедурами, использующими средства прекомпилятора Рго*С; • как сопоставлять, принимать и передавать основные типы данных PL/SQL во внешние функции на языке С; • как передавать и принимать наборы данных. Имея представленные выше универсальный шаблон и файлы управления проекгом, вы получили все необходимое для написания внешней процедуры от начала до конца за несколько минут. Самое сложное — сопоставление типов данных, но по представленным в этой главе таблицам это легко сделать. Они вам подскажут, какой тип использовать для передачи данных. Далее следуйте представленным в примерах принципам передачи параметров (всегда передавать контекст, всегда передавать атрибут MAXLEN для строк и данных типа RAW, всегда передавать индикаторные переменные и т.д.). Это обеспечит создание эффективных внешних процедур в кратчайшие строки.
;ionalOracle : gProfessionalO rwrnrningProf ess ** ^Programming? -'acleProgra s i о n я 10 г* э г ] ° J™* ? S #
£ * %#
• t
r . • • ' • РВД ^tfV t * «"4 •
W
^45^ &«P
ammlngProfe ePrograiimln iOpacxsPPOQГ. :V, f Я,».K | V П
I *,: ! « 5 <э ,i U f I
lePrc I
t
.
••
-
Хранимые процедуры на языке Java
В сервере Oracle 8.1.5 впервые появилась возможность использовать для реализации хранимых процедур язык Java. Для 99 процентов задач всегда хватало возможностей языка PL/SQL, и его по-прежнему можно использовать. В Oracle 8.0 ранее появилась возможность реализовать процедуры на языке С (см. главу 18). Хранимые процедуры на языке Java (еще один вид внешних процедур) — естественное расширение этой возможности, позволяющее использовать язык Java в тех случаях, когда раньше приходилось программировать на С или C++. Если необходимо разработать хранимую процедуру, теперь есть как минимум три возможности: использовать язык PL/SQL, Java или С. Я перечислил их в порядке предпочтения. Большую часть обработки в базе данных можно выполнить на PL/SQL. Если что-то нельзя сделать с помощью PL/SQL (в основном это касается интерфейсов с ОС), вступает в игру язык Java. Язык С используется при наличии уже созданного кода на С или в тех случаях, когда нельзя решить задачу средствами Java. Эта глава не раскрывает основы Java, интерфейса JDBC или программирования с помощью SQLJ. Предполагается, что читатель хоть немного знаком с языком Java и сможет разобраться в небольших фрагментах Java-кода. Предполагается также общее знание интерфейса JDBC и прекомпилятора SQLJ, хотя при наличии минимального опыта использования Java вы легко сможете понять фрагменты кода, связанные с JDBC и SQLJ.
340
Глава 19
Когда используются хранимые процедуры на языке Java? Внешние процедуры на языке Java отличаются от процедур на С тем, что, как и программные единицы PL/SQL, они выполняются встроенной виртуальной Java-машиной (JVM) сервера Oracle, непосредственно в адресном пространстве сервера. Чтобы использовать внешние процедуры на языке С, необходимо сконфигурировать процесс прослушивания, настроить файл TNSNAMES.ORA и запустить отдельный процесс. При использовании языка Java все это не нужно, поскольку как интерпретируемый язык он считается "безопасным" (как и PL/SQL). Нельзя создать Java-функцию, переписывающую часть области SGA. Это и хорошо, и плохо, как выяснится по ходу обсуждения. Тот факт, что работа происходит в одном адресном пространстве, обеспечивает более быстрое взаимодействие между кодом на Java и сервером, в частности происходит меньше переключений контекста между процессами на уровне ОС. С другой стороны, однако, Java-код всегда работает с правами "владельца ПО Oracle", поэтому хранимая процедура на Java (при наличии соответствующих привилегий) может переписать файл параметров инициализации сервера, INIT.ORA (или другие, еще более важные файлы, например файлы данных). Лично я постоянно использую небольшие фрагменты Java-кода для реализации того, что невозможно сделать с помощью PL/SQL. Например, в приложении А, посвященном основным стандартным пакетам, я демонстрирую, как я реализовал пакет для работы с сокетами TCP/IP при помощи Java. Я создавал его для версии Oracle 8.1.5, до появления пакета UTLJTCP (который тоже написан на языке Java), и предпочитаю использовать его до сих пор. Я также использую средства языка Java для передачи сообщений электронной почты с сервера. И для этих целей уже существует стандартный пакет, UTL_SMTP (тоже реализованный на языке Java), позволяющий отправлять простые сообщения, но непосредственное использование языка Java открывает множество других возможностей, включая передачу (и получение) сообщений электронной почты с вложениями. Я интенсивно использую пакет UTL_FILE для чтения и записи файлов в PL/SQL. Одна из возможностей, которых не хватает пакету UTL_FILE, — получение списка файлов в каталоге. С помощью языка PL/SQL его получить нельзя, а на Java — элементарно. Иногда необходимо выполнить команду ОС или программу из среды сервера. В этом случае язык PL/SQL тоже не поможет, a Java позволит легко решить задачу. Изредка мне необходимо узнать часовой пояс, установленный на сервере. В PL/SQL его получить нельзя, а вот с помощью Java — можно (эту возможность мы рассмотрим в приложении А при изучении стандартного пакета UTL_TCP). Надо измерять время с точностью до миллисекунд? В Oracle 8i с помощью Java это можно сделать. Если постоянно необходимо подключаться к СУБД DB2 для выполнения запросов, это можно сделать с помощью шлюза (Transparent Gateway) для СУБД DB2. Это позволит без ограничений выполнять соединения таблиц в разнородных базах данных, распределенные транзакции, прозрачную двухэтапную фиксацию и использовать много
Хранимые процедуры на языке Java
других возможностей. Но если необходимо выполнить запрос или изменение в базе данных DB2 и все перечисленные потрясающие возможности не нужны, достаточно загрузить в базу данных драйверы JDBC для DB2, написанные на языке Java, и воспользоваться ими (естественно, это применимо не только для СУБД DB2). По сути, любой из миллионов имеющихся не интерактивных (не обладающих пользовательским интерфейсом) фрагментов Java-кода можно загрузить в базу данных Oracle и использовать. Вот почему фрагменты Java-кода постоянно встречаются в приложениях. Я предпочитаю использовать язык Java, только когда это удобно и необходимо. Я по-прежнему считаю PL/SQL подходящим средством для создания подавляющего большинства хранимых процедур. Написав одну-две строки PL/SQL-кода, можно получить тот же результат, что и в случае многострочной программы на Java/JDBC. Препроцессор SQLJ уменьшает объем необходимого кода, но выдаваемый им код по производительности уступает сочетанию языков PL/SQL и SQL. Производительность кода на PL/SQL при взаимодействии с SQL выше, чем для сочетания Java/JDBC, как и можно было предположить. Язык PL/SQL проектировался как расширение SQL, и они очень тесно интегрированы. Большинство типов данных языка PL/SQL — это стандартные типы данных SQL, а все типы данных SQL включены в PL/SQL. Между этими языками нет несоответствия типов. Доступ к SQL из кода на Java выполняется средствами функционального интерфейса, добавленного к языку. Каждый тип данных SQL необходимо преобразовать в некий тип данных Java, и, наоборот, все SQL-операторы выполняются процедурно, т.е. между этими языками нет тесной связи. Итак, если выполняется обработка данных в базе, надо использовать язык PL/SQL. Если надо на время выйти за пределы базы данных (например, чтобы отправить сообщение по электронной почте), лучшим средством для этого является язык Java. Если необходимо выполнить поиск в сообщениях электронной почты, хранящихся в базе данных, используйте язык PL/SQL. Если же необходимо загрузить сообщения электронной почты в базу данных, используйте Java.
Как работают внешние процедуры на языке Java Оказывается, внешние процедуры на языке Java (термин "внешняя процедура" в данном случае является синонимом "хранимой процедуры") создавать значительно проще, чем на языке С. Например, в предыдущей главе, посвященной созданию внешних процедур на языке С, пришлось решать следующие проблемы. • Управление состоянием. Внешние процедуры могут потерять информацию о состоянии (текущие значения статических или глобальных переменных). Это связано с используемым механизмом кеширования динамически подключаемых библиотек. Поэтому необходим механизм определения и сохранения состояния в программах на языке С. • Механизмы трассировки. Внешние процедуры выполняются на сервере как отдельный процесс. Хотя на некоторых платформах эти процедуры можно отлаживать с помощью обычного отладчика, это весьма сложно и, если ошибки возни-
342
Глава 19 кают только при одновременном использовании внешней процедуры большим количеством пользователей, просто невозможно. Необходимо средство генерации трассировочных файлов по требованию, "начиная с этого момента".
Q Использование параметров. Необходимо средство параметризации внешних процедур, чтобы можно было управлять их работой извне с помощью файла параметров, аналогично тому, как файл init.ora используется для управления сервером. Q Общая обработка ошибок. Необходимо простое средство выдачи пользователю вразумительных сообщений об ошибках. При использовании языка Java оказывается, что управление состоянием, трассировка и общая обработка ошибок уже не является проблемой. Для сохранения информации о состоянии достаточно объявить переменные в создаваемых Java-классах. Для обеспечения простейшей трассировки можно использовать вызовы System.out.println. Общую обработку ошибок можно выполнять с помощью функции RAISE_APPLICATION_ERROR языка PL/SQL. Все это продемонстрировано в следующем коде: tkyte@TKYTE816> create or replace and compile 2 Java source named "demo" 3 as 4 import j ava.sql.SQLException; 6 public class demo extends Object 7 { 8 9 static int counter = 0; 10 11 public static int IncrementCounter() throws SQLException 12 { 13 System.out.println("Входим в функцию IncrementCounter, » counter == "+counter); 14 if (++counter >= 3) 15 { 16 System.out.println("Ошибка! counter="+counter); 17 #sql { 18 begin raise_application error(-20001, * 'Слишком много вызовов•); end; 19 }; 20 } 21 System.out.println("Выходим из функции IncrementCounter, • counter == "+counter); 22 return counter; 23 } 24 } 25 / Java created. Состояние поддерживается с помощью статической переменной counter. Наша простая демонстрационная программа будет увеличивать счетчик при каждом вызове, а
Хранимые процедуры на языке Java
3 4 3
начиная с третьего и при последующих вызовах, будет автоматически выдавать сообщение об ошибке. Обратите внимание, что для создания небольших фрагментов кода вроде этого можно использовать утилиту SQL*Plus, непосредственно загружающую Java-код в базу данных, автоматически компилируя его в байт-код и запоминая в соответствующих структурах. Ни внешний компилятор, ни средства разработки JDK при этом не нужны — достаточно SQL-оператора CREATE OR REPLACE. Именно так я и предпочитаю создавать хранимые процедуры на языке Java. Это упрощает их установку на любой платформе. Не нужно запрашивать имя пользователя и пароль, как при использовании команды LOADJAVA (это утилита командной строки для загрузки исходного кода, классов Java или jar-файлов в базу данных). Не надо думать о каталогах для поиска классов (classpath) и других подобных нюансах. В приложении А мы рассмотрим утилиту LOADJAVA и пакет DBMS_JAVA, обеспечивающий интерфейс к программе LOADJAVA. Этот метод (с использованием оператора CREATE OR REPLACE) загрузки небольших Java-функций в базу данных особенно хорошо подходит для тех, кто только начинает осваивать технологии Java. Вместо установки JDBC-драйверов, среды разработки JDK, настройки списка каталогов для поиска классов можно просто компилировать код в базе данных, точно так же, как при создании программных единиц PL/SQL. Сообщения об ошибках компиляции выдаются точно так же, как и при использовании языка PL/SQL, например: tkyte@TKYTE816> create or replace and compile 2 Java source named "demo2" 3 as 4 5 public class demo2 extends Object 6 { 7 8 public static int my_routine() 9
{
10 11 12 13 } 14 } 15 /
System.out.println("Входим в функцию my_routine"); r e t u r n counter;
Warning: Java created with compilation e r r o r s . tkyte@TKYTE816> show e r r o r s Java source "demo2" Errors for JAVA SOURCE demo2: LINE/COL ERROR 0/0 0/0
demo2:8: Undefined v a r i a b l e : Info: 1 e r r o r s
counter
Это показывает, что функция my_routine, определенная в строке 8, обращается к необъявленной переменной. Не приходится выискивать ошибку в коде, поскольку получено информативное сообщение о ней. Я не раз убеждался, что многократных оши-
344
Глава 19
бок при настройке JDBC/JDK/CLASSPATH можно легко избежать, загрузив за пару секунд код с помощью этого простого подхода. Вернемся теперь к работающему примеру. Хочу обратить ваше внимание на еще одну важную деталь в созданном выше классе. Метод, вызываемый из языка SQL, IncrementCounter, объявлен как статический. Он обязательно должен быть статическим. (Хотя не все должно быть статическим: при реализации статического метода можно использовать обычные методы). Для языка SQL необходим хотя бы один метод, который можно вызвать, не передавая неявно данные экземпляра с помощью скрытого параметра, вот почему нужен статический метод. Теперь, после загрузки небольшого Java-класса, необходимо создать для него спецификацию вызова в языке PL/SQL. Эта процедура очень похожа на ту, что была описана в главе 18 для внешних процедур на языке С, когда мы сопоставляли типы данных С типам данных SQL. Именно это мы и сделаем сейчас; только на этот раз будут сопоставляться типы данных языка Java типам данных SQL: tkyte@TKYTE816> create or replace 2 function java_counter r e t u r n number 3 as 4 language Java 5 name 'demo.IncrementCounter() return integer'; 6 / Function created. Теперь можно вызывать эту функцию: tkyte@TKYTE816> s e t serveroutput on tkyte@TKYTE816> exec dbms_output.put_line(java_counter); 1 PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dbms_output.put_line(java_counter); 2 PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dbms_output.put_line(java_counter); BEGIN dbms_output.put_line(java_counter); END; * ERROR at line 1: ORA-29532: Java call terminated by uncaught Java exception: oracle.j dbc.driver.OracleSQLException: ORA-20001: Слишком иного вызовов ORA-06512: at line 1 ORA-06512: at "TKYTE.JAVA_COUNTER", line 0 ORA-06512: at line 1 Как видите, информация о состоянии поддерживается автоматически, о чем свидетельствует увеличение счетчика с 1 до 2 и 3. Об ошибках сообщать тоже достаточно легко, но куда попадают результаты обращения к System.out.println? По умолчанию они попадают в трассировочный файл. При наличии доступа к представлениям VSPROCESS, VSSESSION и VSPARAMETER можно определить имя трассировочного файла в конфигурации выделенного сервера следующим образом (этот пример предназначен для
Хранимые процедуры на языке Java
345
Windows — для ОС UNIX он будет аналогичным, но полученное имя файла будет другим): tkyte@TKYTE816>select c.valuel|'\ORA'||to_char(a.spid,'fmOOOOO 1 )||•.trc' 2 from v$process a, v$session b , v$parameter с 3 where a.addr • b.paddr 4 and b.audsid = u s e r e n v ( ' s e s s i o n i d ' ) 5 and с name = 'user_dump_dest' 6 / C.VALUEI|'\ORA'||TO_CHAR(A.SPID,'FMOOOOO')||'.TRC С:\oracle\admin\tkyte816\udump\ORA01236.trc tkyte@TKYTE816> edit C:\oracle\admin\tkyte816\udump\ORA01236.trc
В этом файле можно обнаружить следующее: Dump file C:\oracle\admin\tkyte816\udump\ORA01236.TRC Tue Mar 27 11:15:48 2001 ORACLE V8.1.6.0.0 - Production vsnsta=0 vsnsql=e vsnxtr=3 Windows 2000 Version 5.0 , CPU type 586 Oracle8i Enterprise Edition Release 8.1.6.0.0 — Production With the Partitioning option JServer Release 8.1.6.0.0 — Production Windows 2000 Version 5.0 , CPU type 586 Instance name: tkyte816 Redo thread mounted by this instance: 1 Oracle process number: 12 Windows thread id: 1236, image: ORACLE.EXE *** 2001-03-27 11:15:48.820 *** SESSION ID:(8.11) 2001-03-27 11:15:48.810 Входим в функцию IncrementCounter, counter = 0 Выходим из функции IncrementCounter, counter = 1 Входим в функцию IncrementCounter, counter = 1 Выходим из функции IncrementCounter, counter = 2 Входим в функцию IncrementCounter, counter = 2 Ошибка! counter=3 oracle.jdbc.driver.OracleSQLException: ORA-20001: Слишком много вызовов ORA-06512: at line 1 Я также мог бы использовать средства пакета DBMS_JAVA для перенаправления этих результатов на экран утилиты SQL*Plus, чтобы избежать поиска соответствующего трассировочного файла при отладке функции. В этой главе периодически упоминается пакет DBMS_JAVA, но полное его описание будет представлено в соответствующем разделе приложения А. Из этого небольшого примера понятно, что, по сравнению с созданием внешних процедур на языке С, создавать хранимые процедуры на Java — просто. Не нужно специально настраивать сервер — только инсталлировать Java в базу данных. Не нужен внешний компилятор. Многие средства, которые в случае языка С пришлось создавать самим, мы получаем от сервера автоматически. Это на самом деле просто.
346
Глава 19
Я не описывал пока конфигурирование Java-кода с помощью файла параметров. Причина в том, что Java содержит встроенные средства для этого в виде класса java.util.Properties. Достаточно использовать метод load этого класса для загрузки ранее сохраненного набора свойств либо из большого объекта в таблице базы данных, либо из файла ОС, — что больше подходит. Далее я представлю несколько полезных примеров хранимых процедур на языке Java, в частности, упоминавшихся ранее в разделе "Когда используются хранимые процедуры на языке Java?". Но до этого я хочу переписать представленный в главе 18 пакет DEMO_PASSING_PKG на языке Java вместо С, чтобы продемонстрировать, как передавать и принимать основные типы данных SQL во внешних процедурах на языке Java.
Передача данных В этом примере я собираюсь создать ряд процедур с параметром, передаваемым в режиме IN, и параметром, передаваемым в режиме OUT (или IN OUT). Мы напишем по процедуре для каждого из интересующих нас типов данных (наиболее часто используемых). При этом будет продемонстрирован правильный способ передачи входных данных и получения результатов каждого типа. Кроме того, я создам несколько функций и покажу, как возвращать данные некоторых из этих типов. Меня при работе с Java интересуют следующие типы: •
строки (размером до 32 Кбайт);
Q числа (произвольного масштаба и точности); Q даты; целые числа (включая данные типа binary_integer);
•
• данные типа RAW (размером до 32 Кбайт); Q большие объекты (для любых данных размером более 32 Кбайт); •
массивы строк;
• массивы чисел; •
Q массивы дат. Этот список несколько отличается от аналогичного списка для внешних процедур на языке С. В частности, в нем не указан тип данных BOOLEAN. Дело в том, что пока нет соответствия между типом данных PL/SQL BOOLEAN и типами данных языка Java. Нельзя передавать данные типа BOOLEAN как параметры внешним процедурам, написанным на языке Java. С помощью объектно-реляционных расширений можно создавать типы данных любой сложности. Для создания таких типов данных я рекомендую использовать поставляемое корпорацией Oracle Java-средство JPublisher. Оно автоматически создает Javaклассы, соответствующие объектным типам. Подробнее о JPublisher можно почитать в руководстве Oracle8i JPublisher User's Guide, которое входит в набор документации, предлагаемой корпорацией Oracle. Как и в случае внешних процедур на языке С, мы не будем углубляться в особенности использования объектных типов в хранимых процедурах на • ограничившись только простыми наборами данных скалярных типов.
Хранимые процедуры на языке Java
347
Java-класс будет создан для тех же целей, что и представленная в предыдущей главе динамически подключаемая библиотека на языке С. Начнем с SQL-операторов для создания трех типов наборов — они совпадают с использовавшимися в примере для языка С в предыдущей главе: tkyte@TKYTE816> create or replace type numArray as t a b l e of number; Type created. tkyte@TKYTE816> create or replace type dateArray as t a b l e of d a t e ; Type created. tkyte@TKYTE816> create or replace type strArray as table of varchar2(255); Type created. Теперь рассмотрим спецификацию PL/SQL-пакета для этого примера. Она будет состоять из набора перегруженных процедур и функций для проверки приема и передачи параметров в хранимых процедурах на языке Java. Каждая подпрограмма имеет параметр, предаваемый в режиме IN, и параметр, передаваемый в режиме OUT, что позволяет продемонстрировать передачу данных в Java-код и возвращение результатов. Первая процедура передает числовые данные. Данные Oracle типа NUMBER будут передаваться как Java-тип BigDecimal. Их можно принимать и как данные типа int, и как строки и как другие типы, но при этом возможна потеря точности. Данные типа BigDecimal могут без проблем принять любое значение типа NUMBER от сервера Oracle. Обратите внимание, что параметр, передаваемый в режиме OUT, на уровне Java принимается как массив данных типа BigDecimal. Так будет для всех параметров, передаваемых Java в режиме OUT. Для изменения параметра, переданного Java, нужно передавать "массив" параметров (в этом массиве будет только один элемент) и изменять соответствующий элемент массива. Далее, при описании кода на языке Java, вы увидите, как это сказывается на исходном коде. tkyte@TKYTE816> create or replace package demo_passing_pkg 2 as 3 procedure pass(p_in in number, p_out out number) 4 as 5 language j ava 6 name 'demo_passing_pkg.pass(Java.math.BigDecimal, 7 j ava.math.BigDecimal[])' Даты Oracle сопоставляются типу данных Timestamp. И в этом случае можно было бы сопоставить датам множество других типов, например String, но во избежание потери информации при неявных преобразованиях я выбрал тип Timestamp, который позволяет сохранить все данные, содержащиеся в объектах Oracle типа DATE. 9 10 11 12 13
procedure pass(p_in in date, p_out out date) as language java name 'demo_jpassing_pkg. pass (Java. sql. Timestamp, Java.sql.Timestamp[])';
Строки типа VARCHAR2 передаются очень просто — как данные типа java.lang.String.
348 14 15 16 17 18 19
Глава 19
procedure pass(p_in in varchar2, p_out out varchar2) as language Java name 'demo_passing_pkg.pass(Java.lang.String, java.lang.String[])';
Для данных типа CLOB мы используем предоставляемый Oracle Java-тип oracle.sql.CLOB. С помощью этого типа мы сможем получить входной и выходной потоки данных, используемые для чтения и записи данных типа CLOB. 20 21 22 23 24 25
procedure pass(p_in in CLOB, p_out in out CLOB) as language Java name 'demo_passing_pkg.pass(oracle.sql.CLOB, oracle.sql.CLOB[])';
Теперь перейдем к наборам: вы видели, что, независимо от типа фактически передаваемого набора, используется один и тот же предоставляемый Oracle тип. Вот почему в данном случае Java-функции не являются перегруженными, как все предыдущие (пока что все вызываемые Java-функции назывались demo_passing_pkg.pass). Поскольку все типы наборов передаются как один и тот же тип Java, перегрузку имен использовать нельзя — необходимо называть функцию в соответствии с реально передаваемым типом данных: 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
procedure pass(p_in in numArray, p_out out numArray) as language Java name 'demo_passing_pkg.pass_num_array(oracle.sql.ARRAY, oracle.sql.ARRAYf])'; procedure pass(p_in in dateArray, p_out out dateArray) as language Java name 'demo_passing_pkg.pass_date_array(oracle.sql.ARRAY, oracle.sql.ARRAY[ ] ) '; procedure pass(p_in in strArray, p_out out strArray) as language Java name 'demo_passing_pkg.pass_str_array(oracle.sql.ARRAY, oracle.sql.ARRAY[])';
Следующие две процедуры демонстрируют сопоставление для типов RAW и INT. SQLтип RAW будет сопоставляться встроенному типу byte языка Java. Для целых чисел будет использоваться встроенный тип данных int языка Java: 44 45 46 47
procedure pass_raw(p_in in RAW, p_out out RAW) as language Java
Хранимые процедуры на языке Java 48 49 50 51 52 53 54
name 'demo_passing_pkg.pass(byte[], b y t e [ ] [ ] ) ' ; procedure p a s s _ i n t ( p _ i n i n number, p_out out number) as language Java name 'demo_passing_pkg.pass_int(int, i n t [ ] ) ' ;
Наконец, для полноты изложения я продемонстрирую использование функций для возвращения данных простых скалярных типов: 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
function return_number return number as language Java name 'demo_passing_pkg.return_num() return java.math.BigDecimal' ; function return_date return date as language Java name 'demo_passing_pkg.return_date() return java.sql.Timestamp'; function return_string return varchar2 as language Java name 'demo_passing_pkg.return_string() return Java.lang.String'; end demo_passing_pkg; /
Package created. Эта спецификация пакета практически совпадает (за исключением процедур для данных типа BOOLEAN) с той, что использовалась для внешних процедур на языке С. В этом примере я поместил уровень связывания непосредственно в спецификацию, чтобы не пришлось писать избыточное тело пакета (все функции написаны на языке Java). Рассмотрим Java-код, реализующий использованные выше функции. Начнем с определения Java-класса demo_passing_pkg: tkyte@TKYTE816> set define
off
tkyte@TKYTE816> create or replace and compile 2 Java source named "detno_passing_pkg" 3 as 4 import j a v a . i o . * ; 5 import j a v a . s q l . * ; 6 import j ava.math.*; 7 import o r a c l e . s q l . * ; 8 import o r a c l e . j d b c . d r i v e r . * ; 9 10 public c l a s s demo_jpassing_pkg extends Object 11 {
350
Глава 19
В первом из представленных далее методов демонстрируется единственно возможный способ передачи параметров в режиме OUT функции на Java; фактически мы передаем массив из одного элемента. При изменении значения в массиве изменяется параметр, переданный в режиме OUT. Вот почему все эти методы в качестве второго параметра принимают массив. Значение p_out[0] можно изменять, и оно будет передано методом в вызывающую среду. Изменения значения p_in в вызывающую среду не передаются. Интересная особенность данного метода — отсутствие индикаторной переменной. Язык Java поддерживает понятие неопределенного объекта (null) в объектных типах, как и языки SQL и PL/SQL. Он, однако, не поддерживает трехзначную логику, как SQL; операции X IS NOT NULL нет — можно только непосредственно сравнивать объект с null. He перепутайте и не пытайтесь писать условия вида p_in О NULL в PL/SQL-коде, поскольку они не будут работать корректно. 12 public static void pass(Java.math.BigDecimal p_in, 13 java.math.BigDecimal[] p_out) 14 { 15 if (p_in != null) 16 { 17 System.out.println 18 ("Первый параметр " + p__in. toString ()); 19 20 p_out[0] = p_in.negate() ; 21 22 System.out.println 23 ("Устанавливаем параметр out равным " + p_out[0].toString()); 24 } 25 } Следующий метод работает с типом данных Oracle DATE. Он совпадает с представленным выше, за исключением того, что используются методы класса Timestamp для обработки даты. Наша задача — добавить к переданной дате один месяц: 26 27 public static void pass(Java.sql.Timestamp p_in, 28 java.sql.Timestamp[] p_out) 29 { 30 if (p_in != null) 31 { 32 System.out.println 33 ("Первый параметр " + p_in.toString()); 34 35 p_out[0] = p_in; 36 37 if (p_out[0].getMonthO < 11) 38 p_out[0].setMonth(p_out[0].getMonth()+1); 39 else 40 { 41 p_out[0].setMonth(0); 42 P_ ou t 10].setYear(p_out[0].getYear 0 + 1 ) ; 43 )
Хранимые процедуры на языке Java 44 45 46 47
351
System.out.println ("Устанавливаем параметр out равным " + p_out[0].toString()); } }
Теперь переходим к самому простому из типов данных — String, который соответствует строковым типам SQL. Если вспомнить версию на языке С, с шестью формальными параметрами, индикаторными переменными, атрибутами strlen, функциями strcpy и т.п., то по сравнению с ней эта реализация тривиальна: 48 49 public static void pass(Java.lang.String p_in, 50 Java.lang.String[] p_out) 51 { 52 if (p_in != null) 53 { 54 System.out.println 55 ("Первый параметр " + p_in.toString()); 56 57 p_out 10] = p_in.toUpperCase(); 58 59 System.out.println 60 ("Устанавливаем параметр out равным " + p_out[0].toString()); 61 } 62 } В методе для данных типа CLOB придется выполнить ряд дополнительных действий. Для того чтобы показать, как принимать и возвращать большие объекты, здесь выполняется копирование. Вы видите, что для изменения/чтения содержимого большого объекта используются стандартные потоки чтения/записи языка Java. В этом примере is — входной поток, a os — выходной. Метод копирует данные фрагментами по 8 Кбайт. Выполняется цикл чтения и записи, пока не закончатся считываемые данные: 63 64 public static void pass(oracle.sql.CLOB p_in, 65 oracle.sql.CLOB[] p_out) 66 throws SQLException, IOException 67 { 68 if (p_in !- null && p_out[0] != null) 69 { 70 System.out.println 71 ("Первый параметр " + p_in.length()); 72 System.out.println 73 ("Первый параметр '" + 74 p_in.getSubString(l,80) + " ' " ) ; 75 76 Reader is = p_in.getCharacterStream(); 77 Writer os = p_out[0].getCharacterOutputStream(); 78 79 char buffer[] = new char[8192]; 80 int length; 81
352
Глава 19
82 83 84 85 86 87 88 89 90 91 92 }
while((length=is.read(buffer,0,8192)) os.write(buffer,0,length);
!=-l)
is.closeO; os.closeO; System.out.println ("Устанавливаем параметр out равным " + p_out[0].getSubString(l,80)); }
Следующий метод — приватный (внутренний). Он выдает метаданные о переданном ему объекте типа oracle.sql.ARRAY. Для каждого из передаваемых Java трех типов массивов будет вызываться этот метод, информирующий о том, какого размера и типа массив передан: 93 94 private static void show_array_infо(oracle.sql.ARRAY p_in) 95 throws SQLException 96 { 97 System.out.println("Тип массива "+ 98 p_in.getSQLTypeName()); 99 System.out.println("Код типа массива " + 100 p_in.getBaseType()); 101 System.out.println("Длина массива "+ 102 p_in.length()); 103 } Теперь рассмотрим методы для обработки этих массивов. Использовать массивы несложно, если разобраться, как получать из них данные и изменять их. Получить данные очень просто; метод getArray() возвращает базовый массив данных. Приведя возвращаемое методом getArray() значение к нужному типу, мы получим Java-массив этого типа. Поместить данные в такой массив немного сложнее. Необходимо сначала получить дескриптор (метаданные) массива, а затем создать новый объект-массив с этим дескриптором и соответствующими значениями. Следующий набор методов продемонстрирует это для каждого из использованных типов массивов. Обратите внимание, что тексты методов практически совпадают, за исключением фактических обращений к массивам данных Java. Эти методы выдают метаданные для типа oracle.sql.ARRAY, выдают содержимое массива и копируют входной массив в выходной: 104 105 public static void pass_num_array(oracle.sql.ARRAY p_in, 106 oracle.sql.ARRAY[] p_out) 107 throws SQLException 108 { 109 show_array_infо(p_in); 110 j ava. math. BigDecimal [ ] values = (BigDecimal [ ]) p_in. getArray t,) ; 111 112 for(int i = 0; i < p_in.length(); i++) 113 System.out.println("p_in["+i+"] - " + values[i].toString());
Хранимые процедуры на языке Java
353
114 115 Connection conn = new OracleDriver().defaultConnection(); 116 ArrayDescriptor descriptor « 117 ArrayDescriptor.createDescriptor
150
151 152 153 154 155 156 157 }
Connection conn = new OracleDriver().defaultConnection(); ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor(p_in.getSQLTypeName(), conn); p_out[0] = new ARRAY(descriptor, conn, values);
Передача данных типа RAW ничем не отличается от передачи строк. С этим типом данных работать очень легко: 158 159 public static void pass(byte[] p_in, byte[][] p_out) 160 {
12 Зак. 244
354 161 162 163
Глава 19 i f (p_in ! - null) p_out[0] = p_in; }
Передача целых чисел — проблематична, я вообще не рекомендую их передавать. Нет способа передать значение NULL — соответствующий тип данных int относится к базовым типам данных языка Java. Эти данные не являются объектами и поэтому не могут быть неопределенными. Поскольку индикаторные переменные не поддерживаются, то при необходимости обработать неопределенные значения придется передавать отдельный параметр, а в PL/SQL-коде — проверять соответствующий флаг, чтобы определить, не возвращено ли неопределенное значение. Соответствующий метод представлен здесь для полноты, но лучше вообще не использовать данные целого типа, особенно как параметры, передаваемые в режиме IN, — Java-метод не сможет определить, что значение не нужно читать, поскольку неопределенные значения не поддерживаются. 164 165 public static void pass_int(int p_in, int[] p_out) 166 { 167 System.out.println 168 ("Входной параметр " + p_in); 169 170 p_out[0] = p_in; 171 172 System.out.println 173 ("Выходной параметр " + p_out[0]); 174 } Наконец, перейдем к функциям. Если помните, на языке С написать их было непросто. Необходимо было выделять память, обрабатывать неопределенные значения, явно преобразовывать типы данных С в типы данных Oracle и т.д. Каждая С-функция при этом состояла как минимум из десятка строк кода. В случае же Java достаточно добавить оператор return: 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
public static String return_string() { return "Hello World"; } public static java.sql.Timestamp return_date() { return new java.sql.Timestamp(O); } public static Java.math.BigDecimal return_num() {
return new Java.math.BigDecimal("44.3543"); } } /
Хранимые процедуры на языке Java
355
Java created tkyte@TKYTE816> set define on Запрограммировать функцию на Java гораздо проще, чем на языке С, благодаря тому, что Java выполняет много действий автоматически, "за кадром". Для обеспечения аналогичной функциональности на языке С потребовалось около 1000 строк кода. Выделение памяти, которое требует столько внимания при программировании на С, в случае Java — не проблема. В случае ошибки возбуждается исключительная ситуация. Индикаторные переменные, с которыми надо было возиться в языке С, вообще не нужны в Java. Проблема возникает при передаче типов данных, соответствующих не объектным типам Java, но, как я уже говорил, не следует их использовать, если может потребоваться передать неопределенные значения. Поскольку все компоненты созданы, можно вызывать подпрограммы. Например: tkyte@TKYTE816> set serveroutput on s i z e 1000000 tkyte@TKYTE816> exec dbms_java.set_output(1000000) tkyte@TKYTE816> declare 2 l _ i n strArray := s t r A r r a y O ; 3 l_out strArray := strArray(); 4 begin 5 for i xn 1 .. 5 loop 6 1 in.extend; 7 1 in(i) := 'Элемент ' 11 i; 8 end loop; 9 10 demo passing pkg.pass(l in, 1 out) 11 for i in 1 .. 1 out.count loop 12 dbms output.put line('l out(' 1 1 i 1 1 ) = 13 end loop; 14 end, 15 / Тип массива SECOND.STRARRAY Код типа массива 12 Длина массива 5 p_in[0] == Элемент 1 р in[l] == Элемент 2 Р in[2] •= Элемент 3 p_in[3] == Элемент 4 p_in[4] == Элемент 5 1 out(l) = Элемент 1 1 out(2) = Элемент 2 l_out(3) = Элемент 3 l_out(4) = Элемент 4 l_out(5) = Элемент 5
out(i));
PL/SQL procedure successfully completed. Первые восемь строк результата были сгенерированы Java-методом, а последние пять — PL/SQL-кодом. Значит, мы передали массив из PL/SQL в Java и получили его обратно. С помощью Java-метода мы скопировали входной массив в выходной после распечатки метаданных и значений элементов массива.
356
Глава 19
Полезные примеры Я свято верю, что, если задачу можно решить с помощью одного SQL-оператора, это надо делать. Никогда не используйте, например, цикл FOR по курсору, если достаточно выполнить оператор UPDATE. Если задачу нельзя решить в SQL, попытайтесь решить ее в PL/SQL. Никогда не пишите внешнюю процедуру на языке Java или С, разве что задачу нельзя решить в PL/SQL или реализация на языке С существенно повышает производительность. Если по техническим причинам задачу нельзя решить с помощью PL/SQL, попробуйте решить ее на языке Java. Однако использование Java требует дополнительных ресурсов — памяти, процессорного времени и времени на первоначальный запуск виртуальной машины JVM. Использование PL/SQL также требует дополнительных ресурсов, но они уже выделены, ничего дополнительно запускать не надо. Тем не менее ряд задач нельзя решить с помощью языка PL/SQL, а при использовании Java они решаются элементарно. Ниже представлены полезные фрагменты Java-кода, используемые мной в повседневной практике. Это, конечно, не исчерпывающий список, а лишь вершина айсберга. В приложении А примеры использования языка Java в Oracle рассмотрены более широко.
Генерация списка файлов каталога Пакет UTL_FILE, который мы уже несколько раз использовали по ходу изложения, хорошо справляется с чтением и записью текстовых файлов. Очень часто, однако, необходимо обработать все файлы в указанном каталоге. Этого пакет не позволяет сделать. Для получения списков файлов каталога нет встроенных методов ни в SQL, ни в PL/SQL. На Java его очень легко получить. Вот как это делается: tkyte@TKYTE816> create global temporary t a b l e DIR_LIST 2 (filename varchar2(255)) 3 on commit delete rows 4 / Table created. В этой реализации я решил использовать для возвращения результатов из хранимой процедуры на Java временную таблицу. Я считаю этот метод наиболее удобным, потому что он позволяет в дальнейшем легко сортировать список и выбирать файлы с нужными именами. Необходим следующий фрагмент Java-кода: tkyte@TKYTE816> c r e a t e or replace 2 and compile Java source named "DirList" 3 as 4 import j a v a . i o . * ; 5 import j ava.sql.*; 6 7 public class DirList 8 { 9 public static void getList(String directory) 10 throws SQLException 11 {
Хранимые процедуры на языке Java 12 13 14 15 16 17 18 19 20 21 22 } 23 24 } 25 /
35У
File path = new File(directory); String[] list = path.list(); String element; for(int i = 0; i < list.length; i++) { element = listfi]; #sql { INSERT INTO DIR_LIST (FILENAME) VALUES (:element) }; }
Java created. Я решил использовать SQLJ, чтобы сократить программу. Подключение к базе данных уже выполнено, поэтому реализация с помощью интерфейса JDBC потребовала лишь нескольких дополнительных строк кода. Но с помощью препроцессора SQLJ выполнять SQL-операторы в Java так же просто, как и в PL/SQL. Теперь, конечно же, необходимо создать спецификацию вызова: tkyte@TKYTE816> 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. Прежде чем запускать эту процедуру, следует учесть еще один нюанс. Необходимо предоставить процедуре право делать то, что она должна — читать список файлов каталога. В данном случае я обладаю правами администратора базы данных, поэтому могу предоставить соответствующие привилегии сам себе, но обычно приходится обращаться с соответствующим запросом к администратору. Если помните, во введении к этой главе я писал: "... Java-код всегда работает с правами владельца ПО Oracle, поэтому хранимая процедура на Java при предоставлении соответствующих привилегий может переписать файл параметров инициализации сервера, INIT. ORA (или другие, еще более важные файлы, например файлы данных)." Сервер Oracle защищается от этого следующим образом: для выполнения небезопасных действий необходимо явно получить соответствующую привилегию. Попытавшись использовать эту процедуру до получения необходимых привилегий, мы получим следующее сообщение об ошибке: tkyte@TKYTE816> exec g e t _ d i r _ l i s t ( ' с : \ t e m p ' ) ; BEGIN g e t _ d i r _ l i s t ( ' с : \ t e m p ' ) ; END; ERROR a t l i n e 1: •
358
Глава 19
ORA-29532: Java c a l l terminated by uncaught Java exception: j ava.security.AccessControlException: the Permission (java.io.FilePermission c:\temp read) has not been granted by dbms java.grant_permission t o SchemaProtectionDomain(TKYTE|PolicyTableProxy(TKYTE)) ORA-06512: a t "TKYTE.GET_DIR_LIST", l i n e 0 ORA-06512: a t l i n e 1 Поэтому предоставим себе право получать список файлов в соответствующем каталоге: tkyte@TKYTE816> begin 2 dbms_java.grant_permission 3 (USER, 4 'java.io.FilePermission', 5 'c:\temp', 6 'read'); 7 end; 8
/
PL/SQL procedure successfully completed. И можно выполнять процедуру: tkyte@TKYTE816> exec get_dir_list('c:\temp'); PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from dir_list where rownum < 5; FILENAME a.sql abc.dat activation activation8i.zip Соответствующие права доступа определяются спецификацией Java2 Standard Edition (J2SE) и подробно описаны на странице http://java.sun.eom/j2se/l.3/docs/api/java/ security/Permission.html. В приложении А мы подробно рассмотрим пакет DBMS_JAVA и его использование. Есть еще один нюанс, который необходимо учитывать. Oracle 8.1.6 — первая версия СУБД Oracle, поддерживающая систему прав доступа, задаваемую спецификацией J2SE. В Oracle 8.1.5 для этого приходилось использовать роли. К сожалению, роль была ровно одна: JAVASYSPRTV. Ее использование будет подобно предоставлению роли администратора базы данных каждому пользователю только потому, что ему необходимо создать представление, — это слишком мощная роль для выполнения такого простого действия. При наличии роли JAVASYSPRIV можно делать все, что угодно. Будьте осторожны при использовании этой роли в версии 8.1.5 и постарайтесь перейти на следующие версии, где принята более избирательная модель привилегий.
Выполнение команды ОС Если бы я получал десятицентовую монету всякий раз, отвечая на вопрос о том, как выполнить команду ОС! До появления поддержки языка Java в СУБД, это действитель-
Хранимые процедуры на языке Java
.35,7
но было сложно. Теперь же это почти тривиально. Есть, вероятно, сотни способов сделать это, но следующий фрагмент кода работает вполне удовлетворительно: tkyte@TKYTE816> create or replace and compile 2 Java source named " U t i l " 3 as 4 import j a v a . i o . * ; 5 import j a v a . l a n g . * ; 6 7 public class Util extends Object 8 { 9 10 public static int RunThis(String[] args) 11 { 12 Runtime rt = Runtime.getRuntime(); 13 int re = -1; 14 15 try 16 { 17 Process p = rt.exec(args[0]); 18 19 int bufSize = 4096; 20 BufferedlnputStream bis = 21 new BufferedInputStream(p.getInputStream(), bufSize); 22 int len; 23 byte buffer[] = new byte[bufSize]; 24 25 // Выдаем то, что получено программой 26 while ((len = bis.read(buffer, 0, bufSize)) != -1) 27 System.out.write(buffer, 0, len); 28 29 re = p.waitFor(); 30 } 31 catch (Exception e) 32 { 33 e.printStackTrace(); 34 re = -1; 35 } 36 finally 37 { 38 return re; 39 } 40 } 41 } 42 / Java created. Он позволяет выполнить любую программу и получить ее результаты либо в трассировочном файле на сервере, либо, при использовании средств пакета DBMS_JAVA, в буфере пакета DBMS_OUTPUT. Это, однако, весьма мощное средство — при наличии соответствующих привилегий с его помощью можно выполнять любую команду от име-
360
Глава 19
ни пользователя — владельца ПО Oracle. В данном случае я хочу иметь возможность получить список процессов с помощью утилиты /usr/bin/ps в ОС UNIX и \bin\tlist.exe в Windows. Для этого мне необходимы две привилегии: tkyte@TKYTE816> BEGIN 2 dbms_java.grant_permission 3 (USER, 4 'java.io.FilePermission1, 5 — '/usr/bin/ps', — для UNIX 6 c:\bin\tlist.exe', — для WINDOWS 7 'execute'); 8 9 dbms_java.grant_permission 10 (USER, 11 'java.lang.RuntimePermission', 12 '*', 13 'writeFileDescriptor'); 14 end; 15 / PL/SQL procedure successfully completed. В вашей системе может отсутствовать утилита tlist.exe. Она входит в состав набора инструментальных средств Windows Resource Toolkit и доступна не во всех Windowsсистемах. Этот пример просто показывает, что можно сделать, — отсутствие доступа к tlist.exe не помешает демонстрации. Этот метод можно использовать дня выполнения любой программы. Учтите, однако, что нужно быть внимательным, предоставляя права на выполнение программ с помощью пакета DBMS_JAVA. Например, предоставление права на выполнение программы c:\winnt\system32\cmd.exe фактически означает разрешение выполнять ВСЕ программы, что очень опасно. Первый вызов dbms_java.grant_permission позволяет запускать одну конкретную программу. При желании можно рискнуть и указать вместо имени программы символ *. Это позволит выполнять любые программы. Я не думаю, однако, что это разумно; явно перечисляйте полные имена программ, в надежности которых вы уверены. Вторая привилегия позволяет генерировать результаты во время выполнения. Здесь придется использовать метасимвол *, поскольку я не знаю точно, куда именно будут выдаваться результаты (в стандартный выходной поток, stdout, например, или куда-нибудь еще). Теперь необходимо создать уровень связывания: tkyte@TKYTE816> c r e a t e or replace 2 function RUN__CMD(p_cmd in varchar2) return number 3 as 4 language Java 5 name 'Util.RunThis(java.lang.String[]) return integer'; 6 / Function created. tkyte@TKYTE816> create or replace procedure rc( 2 as
Хранимые процедуры на языке Java
3 4 5 6 7 8 9 10 11
361
х number; begin х := run_cmd(p_cmd); i f (x <> 0) then r a i s e program_error; end if; end; /
Procedure created. Здесь я создал еще один уровень абстракции выше функции связывания, чтобы можно было выполнять программу как процедуру. Давайте посмотрим, как это работает: tkyte@TKYTE816> set serveroutput on size 1000000 tkyte@TKYTE816> exec dbms_java.set_output(1000000) PL/SQL procedure successfully completed. tkyte@TKYTE816> exec rc('C:\WINNT\system32\cmd.exe /c dir') Volume in drive С has no label. Volume Serial Number is F455-B3C3 Directory of C:\oracle\DATABASE 05/07/2001 10:13a
05/07/2001 10:13a 11/04/2000 06:28p ARCHIVE 11/04/2000 06:37p 47 inittkyte816 .ora 11/04/2000 06:28p 31,744 ORADBA.EXE 05/07/2001 09:07p 1,581 oradim.log 05/10/2001 07:47p 2,560 pwdtkyte816.ora 05/06/2001 08:43p 3,584 pwdtkyte816.ora.hold 01/26/2001 11:31a 3,584 pwdtkyte816.XXX 04/19/2001 09:34a 21,309 sqlnet.log 05/07/2001 10:13a 2,424 test.sql 01/30/2001 02:10p 348,444 xml.tar 9 File(s) 415, 277 bytes 3 Dir(s) 13,600,501, 760 bytes free PL/SQL procedure successfully completed. М ы получили список файлов каталога ОС.
Получение времени с точностью до миллисекунд Примеры становятся все меньше, короче и выполняются быстрее. Это я и хочу подчеркнуть. С помощью небольших фрагментов Java-кода, примененных в соответствующих местах, можно существенно расширить функциональные возможности. В Oracle 9i эта функция станет избыточной, поскольку эта версия поддерживает временные отметки с точностью менее секунды. Но при необходимости такая точность измерения времени достижима и в предыдущих версиях: tkyte@TKYTE816> c r e a t e or replace Java source 2 named "MyTimestamp" 3 as
362
Глава 19
4 import java.lang.String; 5 import java.sql.Timestamp; 6 7 public class MyTimestamp 8
{
9 public static String getl'imestamp () 10 { 11 return (new 12 Timestamp(System.currentTimeMillis())).toString(); 13 } 14 }; 15 / Java created. tkyte@TKYTE816> create or replace function my_timestamp return varchar2 2 as language Java 3 name 'MyTimestamp.getTimestamp() return Java.lang.String'; 4 / ••
Function created. tkyte@TKYTE816> select my_timestamp, 2 to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') from dual 3 / MYJTIMESTAMP
TO_CHAR(SYSDATE, 'YY
2001-03-27 19:15:59.688
2001-03-27 19:15:59
Возможные ошибки Большинство сообщений об ошибках, которые вы получите при использовании хранимых процедур на Java, связаны с компиляцией кода и несоответствием типов параметров. Некоторые из наиболее типичных сообщений об ошибках рассмотрены ниже.
ORA-29549 Java Session State Cleared По ходу разработки можно столкнуться с сообщениями следующего вида: s e l e c t my_timestamp, to_char(sysdate,'yyyy-mm-dd
hh24:mi:ss') from dual
ERROR a t l i n e 1: ORA-29549: c l a s s TKYTE.MyTimestamp has changed, Java session s t a t e cleared Это означает, что использованный в сеансе класс был перекомпилирован (скорее всего — вами же). Вся связанная с этим классом информация о состоянии потеряна. Достаточно повторно выполнить оператор, при выполнении которого было выдано это сообщение, и информация о состоянии обновится. По этой причине следует избегать повторной загрузки Java-классов в действующей производственной системе. После этого использующий Java-класс сеанс при обращении к нему получит такое сообщение об ошибке.
Хранимые процедуры на языке Java
363
Ошибки прав доступа Мы уже знакомы с таким сообщением: ERROR a t l i n e 1: ORA-29532: Java call terminated by uncaught Java exception: Java.security.AccessControlException: the Permission (java.io.FilePermlssion c:\temp read) has not been granted by dbms_java.grant_permission to SchemaProtectionDomain(TKYTE|PolicyTableProxy(TKYTE)) ORA-06512: at "TKYTE.GET_DIR_LIST", line 0 ORA-06512: at line 1 •
К счастью, в тексте сообщения об ошибке явно указано, какие привилегии необходимо получить, чтобы вызов был успешным. Обладающий соответствующими привилегиями пользователь должен предоставить вам эти привилегии с помощью процедуры GRANT PERMISSION пакета DBMS JAVA.
ORA-29531 no method X in class Y Если в рассмотренном ранее примере RunThis изменить спецификацию вызова следующим образом: tkyte@TKYTE816> create or replace 2 function RUN_CMD(p_cmd in varchar2) return number 3 as 4 language j ava 5 name 'Util.RunThis(String[]) return integer'; 7 / Function created. будет выдано сообщение об ошибке ORA-29531. Обратите внимание, что в списке параметров функции Util.RunThis, я указал тип данных String, а не java.Iang.String. tkyte@TKYTE816> exec rc('c:\winnt\system32\cmd.exe /с d i r ' ) Java.lang.NullPointerException at oracle.aurora.util. JRIExtensions.getMaximallySpecificMethod(JRIExtensions.j ava) a t oracle.aurora.util.JRIExtensions.getMaximallySpecificMethod(JRIExtensions.j ava) BEGIN RC('c:\winnt\system32\cmd.exe /c d i r ' ) ; END; * ERROR a t l i n e ORA-29531: no ORA-06512: a t ORA-06512: a t ORA-06512: a t
1: method RunThis i n c l a s s U t i l "TKYTE.RUN_CMD", l i n e 0 "TKYTE.RC", l i n e 5 line 1
Дело в том, что для успешного сопоставления типов данных должны указываться полные (fully qualified) имена типов. Хотя класс java.lang неявно импортируется в Javaпрограммах, он не импортируется на уровне языка SQL. Получив это сообщение об ошибке, необходимо проверить сопоставление типов данных и убедиться, что используются полные имена типов данных Java и что они в точности совпадают с именами
364
Глава 19
имеющихся типов данных. Соответствующий Java-метод определяется по сигнатуре, а сигнатура создается на основе используемых типов данных. Минимальное различие в типах входных данных, результатов или регистре символов в имени приведет к несовпадению сигнатур, и сервер Oracle не найдет соответствующий код.
Резюме В этой главе вы узнали, как реализовать хранимые процедуры на языке Java. Это не означает, что весь существующий код на языке PL/SQL необходимо переписать в виде хранимых процедур на Java. Но если, программируя на PL/SQL, вы столкнетесь с неразрешимой проблемой, что обычно происходит при необходимости выйти за пределы базы данных и обеспечить взаимодействие с операционной системой, попробуйте решить задачу с помощью языка Java. Благодаря полученным в этой главе сведениям вы сможете передать основные типы данных SQL, в том числе массивы, с уровня PL/SQL на уровень Java-методов и получить результаты. Я представил несколько полезных фрагментов Java-кода, которые можно использовать непосредственно; обратившись к документации по языку Java, вы обнаружите десятки других фрагментов, незаменимых при разработке приложений. При осторожном использовании, программирование на Java может существенно расширить возможности разработки приложений.
acieProgra
lOfaclefi, ft > -• rofessianalOi irsmingProf ess' ,
л ;
•
raclcProgra ianalOracle" raf:;riingflr
; •
: f'f
•4
i
1
Использование объектнореляционных средств Начиная с версии Oracle 8, в базах данных сервера Oracle могут использоваться объектно-реляционные средства. Выходя за пределы стандартных скалярных типов NUMBER, DATE и строк символов, объектно-реляционные средства Oracle позволяют расширить набор поддерживаемых типов данных. Можно создавать собственные типы данных, включающие: Q атрибуты, каждый из которых может быть скалярной величиной или набором (массивом) других объектных/скалярных типов; •
методы для работы с данными этого типа;
•
статические методы;
О необязательный метод сравнения, используемый для сортировки и сравнения данных. Затем этот новый тип можно использовать для создания таблиц, столбцов таблиц, представлений или для расширения возможностей языков SQL и PL/SQL. Вновь созданный пользовательский тип данных можно использовать точно так же, как и базовый тип данных DATE. В этой главе я продемонстрирую, как использовать объектно-реляционные средства сервера Oracle. Будет также показано, как их не следует использовать. Я буду описывать компоненты этой технологии последовательно. Однако эту главу нельзя считать полным обзором всех возможностей объектно-реляционных средств Oracle. Этому посвящено 200-страничное руководство Oracle Application Developer's Guide — Object-Relational Features. Цель данной главы — показать, когда и как использовать эти возможности.
368
Глава 20
Объектно-реляционные средства Oracle можно использовать во многих языках При программировании на Java — через интерфейс JDBC, на Visual Basic — с помощью компонентов 0 0 4 0 (Oracle Objects for Ole). При программировании с помощью библиотеки 0С1 (Oracle Call Interface), языка PL/SQL и прекомпилятора Рго*С очень легко использовать соответствующие функциональные возможности. Корпорация Oracle предоставляет различные инструментальные средства, упрощающие использование объектно-реляционных возможностей сервера в этих языках. Например, при программировании на Java/JDBC можно использовать Oracle JPublisher — утилиту, автоматически генерирующую Java-классы, представляющие объектные типы базы данных, наборы и PL/SQL-пакеты (это генератор кода, который позволяет сопоставлять сложные типы SQL типам языка Java). Библиотека OCI поддерживает на стороне клиента встроенный кеш объектов, используемый для эффективного управления и обработки объектов. Прекомпилятор Рго*С включает средство ОТТ (Object Type Translator — транслятор объектных типов) для генерации структур (struct) языка C/C++, соответствующих объектным типам. Я не буду касаться использования этих языков и средств — все это детально описано в документации сервера Oracle. Все внимание будет сосредоточено на создании объектных типов в базе данных.
В каких случаях используются объектнореляционные средства Я использую объектно-реляционные средства сервера Oracle преимущественно для естественного расширения возможностей языка PL/SQL. Объектный тип — прекрасный способ добавить в PL/SQL новые функциональные возможности аналогично тому, как классы позволяют сделать это в C++ или Java. В следующем разделе мы рассмотрим соответствующий пример. Объектные типы можно также использовать для стандартизации. Я могу создать новый тип, скажем, ADDRESS_TYPE, который инкапсулирует определение адреса или отдельных компонентов, из которых состоит адрес. Можно даже добавить служебные функции (методы) для этого типа, которые, например, возвращают адрес в формате, подходящем для распечатки на почтовых наклейках. Теперь при создании таблицы, в которой должны содержаться данные об адресе, можно просто указать столбец типа ADDRESS_TYPE. Атрибуты адреса при этом будут добавлены в таблицу автоматически. Пример такого использования тоже будет рассмотрен. Объектные типы можно использовать для объектно-реляционного представления чисто реляционных, по сути, данных. Т.е. можно взять пару таблиц EMP/DEPT и построить объектно-реляционное их представление, в котором каждая строка таблицы DEPT будет содержать набор объектов ЕМР. Не соединяя таблицы ЕМР и DEPT, я смогу обратиться к объектному представлению DEPT и получить информацию из таблиц DEPT и ЕМР в одной строке. В следующем разделе мы рассмотрим и этот пример. Объектные типы можно также использовать для создания объектных таблиц. Преимущества и недостатки объектных таблиц рассматривались в главе 6. Объектные таблицы содержат множество скрытых столбцов; при использовании этих таблиц возникают побочные эффекты, и происходят различные "чудеса". Кроме того, обычно (для
Использование объектно-реляционных средств
369
множества различных целей) необходимо строго реляционное представление данных (в частности, для утилит и средств создания отчетов, которые "не понимают" объектные типы). Именно поэтому объектные таблицы я стараюсь не использовать. Я использую объектные представления реляционных данных, что в конечном итоге дает те же возможности, что и объектные таблицы. Однако при этом я контролирую все аспекты физического хранения данных. Поэтому тему объектных таблиц я здесь подробно рассматривать не буду.
Как работают объектно-реляционные средства В этом разделе мы рассмотрим использование объектно-реляционных средств Oracle для решения следующих задач: Q расширение набора стандартных типов данных в системе; •
естественное расширение возможностей языка PL/SQL;
•
создание объектно-реляционных представлений реляционных, по сути, данных.
Добавление новых типов данных в систему Начнем с простого: типа данных ADDRESS_TYPE. Рассмотрим синтаксис соответствующих конструкций, их возможности, побочные эффекты, с которыми можно столкнуться, и т.п. Для начала создадим простой тип: tkyte@TKYTE816> create or replace type AddressJType 2 as object 3 (street_addrl varchar2(25), varchar2(25), 4 street addr2 5 city varchar2(30), varchar2(2), 6 state number 7 zip code 8 ) 9 Type created. Это простейшая разновидность оператора CREATE TYPE. По ходу работы над примером мы добавим в него дополнительные конструкции. Этот тип состоит только из заданных скалярных типов, не имеет методов, специфических функций сравнения — ничего "выдающегося". Зато его можно сразу же использовать в таблицах и в PL/SQLкоде: tkyte@TKYTE816> create t a b l e people 2 (name varchar2(10), 3 home_address address_type, 4 work_address address_type 5 )
370 6
Глава 20 /
Table created. tkyte@TKYTE816> declare 2 l_home_address address_type; 3 l_work_address address_type; 4 begin 5 l_home_address :" Address_Type('123 Main Street', null, 6 'Reston', 'VA', 45678); 7 l_work_address := Address_Type('1 Oracle Way', null, 8 'Redwood', 'CA\ 23456); 9 10 insert into people 11 (name, home_address, work_address) 12 values 13 . ('TomKyte', l_home_address, l_work_address); 14 end; 15 / PL/SQL procedure s u c c e s s f u l l y
completed.
tkyte@TKYTE816> s e l e c t * from people; NAME Tom Kyte
HOME ADDRESS(STREET WORK ADDRESS(STREET Z Z ____! Г ADDRESSJTYPE('123 Ma ADDRESSJTYPE('1 Orac i n S t r e e t 1 , NULL, 'R l e Way', NULL, 'Redw e s t o n ' , 'VA', 45678) o o d ' , 'CA', 23456)
Как видите, использовать этот новый тип в операторе CREATE TABLE так же легко, как, например, тип NUMBER. Кроме того, объявлять переменные типа ADDRESS_TYPE в PL/SQL тоже просто: в языке PL/SQL новые типы данных мсжно использовать сразу же. Новые функциональные возможности использованы в приведенном PL/SQL-коде в строках с 5 по 8. Здесь вызывается конструктор объекта нового типа. Стандартный конструктор типа позволяет задать значения для всех атрибутов объектного типа. По умолчанию создается только один стандартный конструктор, при вызове которого надо указать значения для всех атрибутов типа. В разделе "Использование типов для расширения возможностей PL/SQL" мы рассмотрим, как создавать специфические конструкторы с помощью статических методов-функций. Созданные переменные типа ADDRESS_TYPE после инициализации можно использовать в качестве связываемых переменных в SQL-операторах, как было показано выше. Достаточно просто вставить значения столбцов NAME, HOME_ADDRESS и WORK_ADDRESS. Несложный SQL-запрос позволяет получить эти данные. С помощью SQL можно обращаться не только к столбцу HOME_ADDRESS, но и к каждому из компонентов HOME_ADDRESS. Например: tkyte@TKYTE816> s e l e c t name, home_address.state, work_address.state 2 from people 3 / select name, home_address.state, work_address.state ERROR at line 1: ORA-00904: invalid column name
Использование объектно-реляционных средств
J / 1
tkyte@TKYTE816> select name, P.home_address.state, P.work_address.state 2 from people P 3 / NAME
HOME_ADDRESS.STATE WORK_ADDRESS.STATE
Tom Kyte
VA
CA
Я продемонстрировал неправильный и правильный способ. Первый пример — это то, что обычно пишут разработчики. Запрос, конечно, не работает. Чтобы обратиться к компонентам объектного типа, необходимо использовать коррелирующее имя, как сделано во втором запросе. В нем я задал для таблицы PEOPLE псевдоним Р (можно использовать любой допустимый идентификатор, включая слово PEOPLE). Если возникает необходимость сослаться на отдельные компоненты адреса, я использую псевдоним. Как же выглядит в действительности таблица PEOPLE? To, что показывает сервер Oracle, весьма отличается от того, что используется на самом деле, как можно догадаться, прочитав главу 6 и изучив примеры с вложенной или объектной таблицей: tkyte@TKYTE816> d e s c p e o p l e Name
Null?
Type
NAME VARCHAR2(10) HOME_ADDRESS ADDRESSJTYPE WORK_ADDRESS ADDRESSJTYPE tkyte@TKYTE816> select name, length 2 from sys.col$ 3 where obj# = (select object_id 4 from user objects 5 where object name = 'PEOPLE') 6 / NAME NAME HOME_ADDRESS SYS_NC00003$ SYS_NC00004$ SYS_NC00005$ SYS_NC00006$ SYS NC00007$ WORK_ADDRESS SYS_NC00009$ SYS NC00010$ SYS_NC00011$ SYS_NC00012$ SYS NC00013$
LENGTH 10 1 25 25 30 2 22 1 25 25 30 2 22
13 rows selected. Сервер Oracle сообщает, что в таблице — три столбца, но в реальном словаре данных их, однако, — тринадцать. В нем можно обнаружить скрытые скалярные столбцы. Хотя все не так очевидно и используются скрытые столбцы, применять скалярные объектные типы (без вложенных таблиц) подобным образом несложно. С такого рода неоче-
372
Глава 20
видностью вполне можно смириться. Если использовать опцию SET DESCRIBE утилиты SQL*Plus, можно заставить эту утилиту показывать всю структуру объектного типа: tkyte@TKYTE816> s e t d e s c r i b e d e p t h tkyte@TKYTE816> d e s c p e o p l e
Name
all
Null?
NAME HOME_ADDRESS STREET_ADDR1 STREET_ADDR2 CITY STATE ZIP_CODE WORK_ADDRESS STREET_ADDR1 STREET_ADDR2 CITY STATE ZIPCODE
Type
VARCHAR2(10) ADDRESSJTYPE VARCHAR2(25) VARCHAR2(25) VARCHAR2(30) VARCHAR2(2) NUMBER ADDRESS_TYPE VARCHAR2(25) VARCHAR2(25) VARCHAR2(30) VARCHAR2(2) NUMBER
Это очень удобно для определения поддерживаемых атрибутов. Теперь давайте немного усложним тип ADDRESS_TYPE: добавим функцию выдачи адреса в удобном формате в виде одного поля. Для этого можно добавить в тело типа соответствующую метод-функцию: tkyte@TKYTE816> a l t e r type AddressJType 2 REPLACE 3 as object varchar2(25), 4 (street_addrl street_addr2 varchar2(25), 5 city varchar2(30), 6 state varchar2(2), 7 zip_code number, 8 member function toString return varchar2 9 10 11 Type altered. tkyte@TKYTE816> create or replace type body Address_Type 2 as 3 member function toString return varchar2 4 is 5 begin 6 if (street_addr2 is not NULL) 7 then 8 return street_addrl || chr(10) || 9 street_addr2 || chr(10) || 10 city II ', ' || state I I ' ' I I zip_code; 11 else 12 return street_addrl || chr(10) || 13 city || ', ' || state || ' ' I I zip code;
Использование объектно-реляционных средств
14 15 16 17
3
/
J
end if; end; end; /
Type body created. tkyte@TKYTE816> select name, p.home_address.toString() 2 from people P 3 / NAME P.HOME_ADDRESS.TOSTRING() Tom Kyte 123 Main Street Reston, VA 45678 Вот и первый пример метода объекта. Каждый метод вызывается с неявным параметром SELF. Можно добавить этот префикс к атрибутам STREET_ADDR1, STREET ADDR2 и т.д.: SELF.street_addrl | | chr(10)
| | SELF.street_addr2
...
но он и так добавляется неявно. Тут вы вполне резонно можете заметить: "Ведь все это можно сделать с помощью реляционной таблицы и PL/SQL-пакета". Это действительно так. Однако использование объектного типа с методами, как показано выше, дает определенные преимущества. Q
Обеспечивается более совершенный механизм инкапсуляции. Тип ADDRESS_TYPE инкапсулирует и поддерживает адрес, со всеми его атрибутами и функциональными возможностями.
•
Методы более тесно привязываются к специфическим данным. Это очень важный момент. Если используются скалярные столбцы и PL/SQL-функция, форматирующая их для вывода адреса на печать, эту функцию можно вызвать с любыми данными. Можно передать значение столбца EMPLOYEE_NUMBER в качестве почтового индекса, фамилию — вместо названия улицы и т.д. Привязывая метод к атрибутам, мы гарантируем, что метод TOSTRING может работать только с данными адреса. Пользователи, вызывающие этот метод, не должны задумываться о передаче соответствующих данных — они "уже здесь".
Однако объектный тип имеет один недостаток: в Oracle 8i он мало поддается изменениям. С помощью оператора ALTER можно добавлять новые методы, но нельзя ни удалить существующие, ни добавить дополнительные атрибуты после создания таблицы, использующей этот тип (да и удалить добавленные методы невозможно). Единственное, что можно делать, — это добавлять методы или изменять их (тело типа). Другими словами, развитие схемы при использовании объектных типов затруднено. Если со временем окажется, что для объекта ADDRESS_TYPE необходим еще один атрибут, придется пересоздавать объекты, в которые этот тип встроен. Это не относится к объектным типам, которые не используются для столбцов таблиц базы данных или в операторах
374
Глава 20
CREATE TABLE OF TYPE. Другими словами, если объектные типы используются исключительно в объектных представлениях или для расширения возможностей PL/SQL (как описано в следующих разделах), этой проблемой можно пренебречь. С объектными типами также связаны специфические методы MAP и ORDER. Они используются при сортировке, сравнении или группировке экземпляров объектных типов. Если у объектного типа нет функции MAP или ORDER, при попытке выполнения этих операций вы получите следующее сообщение об ошибке: tkyte@TKYTE816> s e l e c t * from people order by home_address; s e l e c t * from people order by home_address * ERROR a t l i n e 1: ORA-22950: cannot ORDER objects without MAP or ORDER method tkyte@TKYTE816> select * from people where home_address > work_addres:3; select * from people where home_address > work_address ERROR at line 1: ORA-22950: cannot ORDER objects without MAP or ORDER method tkyte@TKYTE816> s e l e c t * from people where home_address = work_addres;3 ; no rows s e l e c t e d Упорядочивать данные объектного типа, использовать их при поиске "больших" или "меньших" значений будет невозможно. Значения можно будет сравнивать только на равенство. При этом сервер Oracle выполняет сравнение по всем атрибутам, чтобы узнать, не совпадают ли они. Чтобы можно было выполнять все остальные операции, необходимо добавить метод MAP или метод ORDER (объектный тип может иметь либо метод MAP, либо метод ORDER, но не оба одновременно). Метод MAP — это функция, работающая с одним экземпляром объекта и возвращающая значение одного из скалярных типов, которое сервер Oracle будет использовать для сравнения с другими однотипными объектами. Например, если объектный тип представляет точку на плоскости с координатами X и Y, функция MAP может возвращать квадратный корень из (X*X+Y*Y) — расстояние от начала координат. Метод ORDER принимает два экземпляра объекта — SELF и объект для сравнения с SELF. Метод ORDER возвращает 1, если SELF больше этого объекта, -1, если SELF меньше другого объекта или 0, если объекты равны. Метод MAP предпочтительнее, поскольку работает намного быстрее и даже может вызываться в параллельных запросах (метод ORDER нельзя использовать при распараллеливании). Метод MAP достаточно вызвать для экземпляра объекта один раз, и после этого сервер Oracle может использовать это значение при сортировке. Метод ORDER при сортировке большого множества, возможно, придется вызывать сотни или тысячи раз с одними и теми же данными. Для созданного ранее типа ADDRESS_TYPE я продемонстрирую оба метода. Сначала — метод ORDER: tkyte@TKYTE816> a l t e r type AddressJType 2 REPLACE 3 as object 4 (street_addrl varchar2(25), 5 street addr2 varchar2(25),
Использование объектно-реляционных средств 6 7 8 9 10 11 12 13
375
city varchar2(30), state varchar2(2), zip_code number, member function toString return varchar2, order member function order_function(compare2 in Address_type) return number ) /
Type altered. tkyte@TKYTE816> create or replace type body Address_Type 2 as 3 member function toString return varchar2 4 is 5 begin 6 if (street_addr2 is not NULL) 7 then 8 return street_addrl |I chr(10) || 9 street_addr2 || chr(10) || 10 city || ', ' || state II ' ' II zip_code; 11 else 12 return street_addrl |I chr(10) || 13 city || ', ' || state I I ' ' I I zip_code; 14 end if; 15 end; 16 17 order member function order_function(compare2 in Address_type) 18 return number 19 is 20 begin 21 if (nvl(self.zip_code,-99999) о nvl(compare2.zip_code,-99999)) 22 then 23 return sign(nvl(self.zip_code,-99999) 24 - nvl(compare2.zip_code,-99999)); 25 end if; 26 if (nvl(self.city,chr(0)) > nvl(compare2.city,chr(0))) 27 then 28 return 1; 29 elsif (nvl(self.city,chr(0)) < nvl(compare2.city,chr(0))) 30 then 31 return -1; 32 end if; 33 if (nvl(self.street_addrl,chr(0)) > 34 nvl(compare2.street_addr1,chr(0))) 35 then 36 return 1; 37 elsif (nvl(self.street_addrl,chr(0)) < 38 nvl(compare2.street_addr1,chr(0))) 39 then 40 return - 1 ; 41 end if; 42 if (nvl(self.street addr2,chr(0)) >
376
Глава 20
43 nvl(compare2.street_addr2,chr(0))) 44 then 45 return 1; 46 elsif (nvl(self.street_addr2,chr(0)) < 47 nvl(compare2.street_addr2, chr(0))) 48 then 49 return - 1 ; 50 end if; 51 return 0; 52 end; 53 end; 54 / Type body created. Этот метод сравнивает два адреса по следующему алгоритму. 1. Если значение почтового индекса (ZIP_CODE) у объекта SELF меньше, чем у объекта COMPARE2, вернуть -1, а если больше — 1. 2. Если значение города (CITY) у объекта SELF меньше, чем у объекта COMPARE2, вернуть -1, а если больше — 1. 3. Если значение первого компонента адреса (STREET_ADDR1) у объекта SELF меньше, чем у объекта COMPARE2, вернуть -1, а если больше — 1. 4. Если значение второго компонента адреса (STREET_ADDR2) у объекта SELF меньше, чем у объекта COMPARE2, вернуть -1, а если больше — 1. 5. Иначе вернуть 0 (адреса совпадают). Как видите, при сравнении приходится постоянно проверять, не переданы ли значения NULL, и т.п. В результате метод получился достаточно большим и сложным. Он, несомненно, неэффективен. Задумав написать метод ORDER, попробуйте использовать вместо него метод MAP. Представленное выше сравнение лучше переписать в виде метода MAP. Учтите, что если вы уже изменили тип, добавив в него представленный выше метод ORDER, придется удалить таблицу, зависящую от этого типа, удалить сам тип и создать все заново. Методы нельзя удалять — их можно только добавлять с помощью оператора ALTER TYPE, а нам надо избавиться от существующего метода ORDER. Полный пример должен был бы включать операторы DROP TABLE PEOPLE, DROP TYPE ADDRESSJTYPE и CREATE TYPE и лишь затем — следующий оператор ALTER TYPE: tkyte@TKYTE816> a l t e r type AddressJType 2 REPLACE 3 as object 4 (street_addrl varchar2(25), 5 street_addr2 varchar2(25), 6 city varchar2(30), 7 state varchar2(2), 8 zip_code number, 9 member function toString return varchar2, 10 map member function mapping_function return varchar2 11 )
Использование объектно-реляционных средств 12
3 7 7
/
Type altered. tkyte@TKYTE816> create or replace type body Address_Type 2 as 3 member function toString return varchar2 4 is 5 begin 6 if (street_addr2 is not NULL) 7 then return street addrl || chr(10) || 8 9 street addr2 || chr(10) || 10 city || ', ' || state I I ' 1 || zip code; 11 else 12 return street addrl || chr(10) || 13 city || ', ' || state I I 1 ' II zip code; 14 end if; 15 end; 16 17 map member function mapping function return varchar2 18 is begin 19 return to char(nvl(zip code,0), 'fmOOOOO ') II 20 21 lpad(nvl(city,' ' ) , 30) || 22 lpad(nvl(street addrl,' ' ) , 25) | I 23 lpad(nvl(street_addr2,' ' ) , 2 5 ) ; 24 end; 25 end; 26 / Type body created. Возвращая строку фиксированной длины, содержащую значение ZIP_CODE, затем — CITY и поля STREET_ADDR, можно переложить задачу сравнений и сортировки на сервер Oracle. Прежде чем переходить к другим вариантам использования объектных типов (я больше всего люблю использовать их для расширения возможностей языка PL/SQL), хочу представить еще один тип наборов — VARRAY. В главе 6 мы рассматривали вложенные таблицы и их реализацию. Было показано, что они реализуются в виде пары родительской и дочерней таблиц, со скрытым суррогатным ключом в родительской таблице и столбцом NESTED_TABLE_ID в порожденной. Массив VARRAY во многом похож на вложенную таблицу, но реализуется абсолютно иначе. Массив VARRAY (или вложенная таблица) используется для хранения массива данных, связанных с одной строкой. Например, если необходимо хранить в таблице PEOPLE дополнительные таблицы (скажем, массив прежних адресов проживания, начиная с самого старого), можно сделать следующее: tkyte@TKYTE816> c r e a t e or replace type Address_Array_Type 2 as varray(25) of Address_Type 3 / Type created.
378
Глава 20
tkyte@TKYTE816> alter table people add previous_addresses Address_Array_Type 2 / Table altered. tkyte@TKYTE816> set describe depth all tkyte@TKYTE816> desc people Name Null? NAME HOME_ADDRESS STREET_ADDR1 STREET_ADDR2 CITY STATE ZIP_CODE
Type VARCHAR2(10) ADDRESS_TYPE VARCHAR2(25) VARCHAR2(25) VARCHAR2(30) VARCHAR2(2) NUMBER
METHOD MEMBER FUNCTION TOSTRING RETURNS VARCHAR2 METHOD MAP MEMBER FUNCTION MAPPING_FUNCTION RETURNS VARCHAR2 WORK_ADDRESS ADDRESSJTYPE STREET_ADDR1 VARCHAR2(25) STREET_ADDR2 VARCHAR2(25) CITY VARCHAR2(30) STATE VARCHAR2(2) ZIP_CODE NUMBER METHOD MEMBER FUNCTION TOSTRING RETURNS VARCHAR2 METHOD MAP MEMBER FUNCTION MAPPING_FUNCTION RETURNS VARCHAR2 PREVIOUS_ADDRESSES ADDRESS_ARRAY_TYPE STREET_ADDR1 VARCHAR2(25) STREET_ADDR2 VARCHAR2(25) CITY VARCHAR2(30) STATE VARCHAR2(2) ZIP_CODE NUMBER METHOD MEMBER FUNCTION TOSTRING RETURNS VARCHAR2 METHOD MAP MEMBER FUNCTION MAPPING_FUNCTION RETURNS VARCHAR2 Итак, теперь в нашей таблице можно хранить до 25 предыдущих адресов. Вопрос в том, что при этом происходит "за кадром"? Обратившись к словарю данных, можно увидеть следующее:
Использование объектно-реляционных средств
tkyte@TKYTE816> s e l e c t name, length 2 from sys.col$ 3 where obj# = (select object id 4 from user objects 5 where object name = 6 / NAME NAME HOME_ADDRESS SYS_NC00003$ SYS_NC00004$ SYS_NC00005$ SYS NC00006$ SYS NC00007$ WORK_ADDRESS SYS_NC00009$ SYS NC00010$ SYS NC00011$ SYS_NC00012$ SYS NC00013$ PREVIOUS_ADDRESSES
1
37У
PEOPLE')
LENGTH 10 1 25 25 30 2 22 1 25 25 30 2 22 2940
14 rows s e l e c t e d . Сервер Oracle добавил столбец размером 2940 байтов для реализации массива VARRAY. Данные массива VARRAY будут храниться тут же, в самой строке. При этом возникает интересный вопрос: что произойдет, если размер массива станет больше 4000 байтов (это максимальный размер структурированного столбца, поддерживаемый сервером Oracle)? Если удалить столбец и пересоздать его как VARRAY(50), произойдет следующее: tkyte@TKYTE816> a l t e r t a b l e people drop column 2 /
previous_addresses
Table a l t e r e d . tkyte@TKYTE816> c r e a t e or replace type Address_Array_Type 2 as varray(50) of Address_Type 3 / Type created. tkyte@TKYTE816> alter table people add previous_addresses Address_Array_Type 2 / Table altered. tkyte@TKYTE816> select object_type, object_name, 2 decode(status,'INVALID','*','') status, 3 tablespace_name 4 from user_objects a, user_segments b 5 where a.object_name = b.segment_name (+)
380
Глава 20
6 order by object_type, 7 / OBJECT TYPE OBJECT NAME
object_name S TABLESPACE NAME
LOB
SYS LOB0000026301C00014$$
DATA
TABLE
PEOPLE
DATA
TYPE
ADDRESS_ARRAY_TYPE ADDRESSJTYPE
TYPE BODY
ADDRESS TYPE
tkyte@TKYTE816> select name, length 2 from sys.col$ select ob object_id 3 where ob^# = (select ;]< from user user_objects 4 5 where obj name = 'PEOPLE') 6 / NAME NAME HOME_ADDRESS SYS_NC00003$ SYS_NC00004$ SYS_NC00005$ SYS NC00006$ SYS_NC00007$ WORK_ADDRESS SYS NC00009$ SYS_NC00010$ SYS_NC00011$ SYS_NC00012$ SYS_NC00013$ PREVIOUS ADDRESSES
LENGTH 10 1 25 25 30 2 22 1 25 25 30 2 22 3852
14 rows selected. Как видите, теперь сервер Oracle автоматически создал большой объект. Если объем данных в массиве VARRAY не превышает примерно 4000 байтов, данные хранятся вместе со строкой (inline). Если же объем данных больше, массив VARRAY выносится из строки в сегмент большого объекта (как и любой большой объект). Массивы VARRAY либо хранятся в строке как столбец типа RAW, либо (при достаточно большом объеме) как большой объект. Дополнительных ресурсов для поддержки данных типа VARRAY (по сравнению с вложенной таблицей) надо очень мало, что делает массив VARRAY привлекательным методом хранения повторяющихся данных. Поиск по массиву VARRAY можно реализовать, преобразовав его данные в таблицу, что сделает его не менее гибким, чем вложенные таблицы: tkyte@TKYTE816> update people 2 s e t previous_addresses = Address_Array_Type( 3 Address_Type('312 Johnston Dr', n u l l , 4 'Bethlehem', 'PA', 18017),
Использование объектно-реляционных средств
З о 1
AddressJType('513 Zulema St', 'Apartment #3', 'Pittsburg', 'PA', 18123), Address_Type('840 South Frederick St 1 , null, 'Alexandria', 'VA\ 20654));
5 6 7
8 1 row updated.
tkyte@TKYTE816> select name, prev.city, prev.state, prev.zip_code 2 from people p, table(p.previous_addresses) prev 3 where prev.state = ' PA'; NAME
CITY
ST
ZIP CODE
Tom Kyte Tom Kyte
Bethlehem Pittsburg
PA PA
18017 18123
Существенное различие состоит в том, что при реализации с помощью вложенной таблицы можно создать индекс по столбцу STATE вложенной таблицы, и оптимизатор этот индекс использовал бы. В данном случае столбец STATE проиндексировать нельзя. Итак, основные отличия вложенных таблиц от массивов переменной длины (VARRAY) представлены в следующей таблице. Вложенная таблица
VARRAY
Элементы "массива" не упорядочены. Данные из набора могут возвращаться совсем не в том порядке, в каком они туда вставлялись.
VARRAY — настоящие массивы. Данные после вставки остаются упорядоченными. В рассмотренном ранее примере данные добавлялись в конец массива. Это означает, что самый старый адрес идет в массиве первым, а последний по времени адрес находится в конце массива. При использовании вложенной таблицы для упорядочения адресов по давности потребуется дополнительный атрибут.
Вложенные таблицы физически хранятся в виде пары родительской и дочерней таблиц с суррогатными ключами.
Массивы VARRAY хранятся как столбец типа RAW или как большой объект. При этом для обеспечения их работы требуются минимальные дополнительные ресурсы.
Вложенные таблицы не имеют ограничения на количество хранящихся элементов.
Для массивов VARRAY при создании типа задается ограничение на количество хранящихся элементов.
Вложенные таблицы можно изменять (добавлять/изменять/удалять в них данные) с помощью языка SQL.
Массивы VARRAY необходимо изменять процедурно. Нельзя выполнять операторы вида: INSERT INTO TABLE (SELECT P.PREVIOUS_ADDRESSES FROM PEOPLE P) VALUES ... как для вложенной таблицы. Для добавления адреса придется использовать процедурный код (см. пример далее).
382
Глава 20
Вложенная таблица
VARRAY
Для получения данных строки при использовании вложенных таблиц необходимо выполнять реляционное соединение. В случае небольших наборов это повлечет чрезмерное использование ресурсов.
Для получения данных массива VARRAY соединение выполнять не нужно. В случае небольших наборов данные хранятся в самой строке; если же наборы большие в сегменте большого объекта. При обращении к элементам массива VARRAY требуется меньше ресурсов, чем при обращении к вложенной таблице. При изменении же данных массива VARRAY ресурсов требуется больше, чем при изменении вложенной таблицы, поскольку заменять приходится весь массив, а не один элемент.
В представленной выше таблице указано, что массив VARRAY нельзя изменять с помощью SQL-операторов с конструкцией TABLE; его надо обрабатывать процедурно. Для изменения столбцов типа VARRAY лучше всего написать хранимую процедуру. Ее код может быть примерно таким: tkyte@TKYTE816> declare 2 l_prev_addresses address_Array_Type; 3 begin 4 select p.previous_addresses into l_prev_addresses 5 from people p 6 where p.name = 'Tom Kyte'; 7 8 l_prev_addresses.extend; 9 l_prev_addresses(l_j?rev_addresses.count) := 10 Address_Type(423 Main S t r e e t 1 , n u l l , 11 'Reston', 'VA\ 45678); 12 13 update people 14 set previous_addresses = l_prev_addresses 15 where name = 'Tom Kyte'; 16 end; 17 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select name, prev.city, prev.state, prev.zip_code 2 from people p, table(p.previous_addresses) prev 3 / NAME
CITY
ST
ZIP CODE
Tom Tom Tom Tom
Bethlehem Pittsburg Alexandria Reston
PA PA VA VA
18017 18123 20654 45678
Kyte Kyte Kyte Kyte
Мы рассмотрели преимущества и недостатки использования расширенных типов данных Oracle в таблицах базы данных. Вы должны решить, стоит ли ради возможности
Использование объектно-реляционных средств
3 8 3
создавать новые типы данных с однозначно реализованными методами обработки и использовать их в определениях столбцов, жертвовать возможностью развивать эти типы со временем (добавлять или удалять атрибуты).* Мы также сравнили использование массивов VARRAY и вложенных таблиц как способов физического хранения данных. Было показано, что массивы VARRAY больше подходят для хранения ограниченного набора упорядоченных элементов, чем вложенные таблицы. Использовать массивы VARRAY очень удобно для хранения списка элементов, которому не требуется отдельная таблица. Избирательное использование новых типов существенно улучшает систему и ее структуру. Использование объектных типов Oracle в качестве типов столбцов таблиц (но не создание объектных таблиц, как было продемонстрировано в главе 6) позволяет обеспечить стандартизацию и вызов процедур (методов) с семантически правильными параметрами. Огорчает только невозможность существенно развивать тип данных схемы после того, как создана хотя бы одна таблица, в которой этот тип используется.
Использование типов для расширения возможностей языка PL/SQL Именно в этом объектно-реляционные средства Oracle максимально преуспели. Язык PL/SQL — очень гибкий и мощный, как доказывает уже то, что механизм расширенной репликации (Advanced Replication) был написан полностью на PL/SQL еще в версии Oracle 7.1.6. Приложения из набора Oracle Applications (Human Resources — управление персоналом, Financial Applications — бухгалтерский учет, CRM applications — управление клиента и т.д.) разработаны в основном на PL/SQL. Хотя это и замечательный язык программирования, встречаются ситуации, когда его базовые возможности требуется расширить (как и в случае языков Java, С, C++ или любых других языков программирования). Это можно сделать с помощью объектных типов. Они добавляют новые функциональные возможности в PL/SQL, как классы — в языках Java или C++. В этом разделе я продемонстрирую, как использовать объектные типы для упрощения программирования на PL/SQL. Будет создан тип данных File на основе средств пакета UTL_FILE. UTL_FILE — это стандартный пакет, поставляемый в составе сервера Oracle и позволяющий выполнять в PL/SQL операции ввода-вывода (чтение и запись) текстовых данных в файлы на сервере. Он обеспечивает функциональный интерфейс, аналогичный семейству f-функций языка С (fopen, fclose, fread, fwrite и т.д.). Функциональные возможности пакета UTL_FILE будут инкапсулированы в простой в использовании объектный тип.
Создание нового типа данных PL/SQL Пакет UTL_FILE возвращает записи PL/SQL (данные типа RECORD). Это несколько усложняет работу, но проблему можно решить. Усложнение связано с тем, что объектный тип SQL может содержать только SQL-типы, но не типы данных PL/SQL. Поэтому 'Хочу отметить, что в версии Oracle 9/ ситуация принципиально меняется, поскольку появляется возможность развивать систему типов за счет наследования. - Прим. научн. ред.
384
Глава 20
нельзя создать объектный тип, содержащий атрибут типа записи PL/SQL, но нам это необходимо, чтобы инкапсулировать функциональные возможности существующего пакета. Чтобы решить эту проблему, придется создать вместе с типом небольшой PL/SQLпакет. Начнем со спецификации типа — прототипа того, что мы планируем создать: tkyte@TKYTE816> create or replace type FileType 2 as object 3 (g_file_name varchar2(255), 4 g_j>ath varchar2 (255), 5 g_file_hdl number, 6 7 static function open(p_path in varchar2, 8 p_file_name in varchar2, 9 p_mode in varchar2 default 'r', 10 p_maxlinesize in number default 32765) 11 return FileType, 12 13 member function isOpen return boolean, 14 member procedure close, 15 member function get_line return varchar2, 16 member procedure put(p_text in varchar2), 17 member procedure new_line(p_lines in number default 1 ) , 18 member procedure put_line(p_text in varchar2), 19 member procedure putf(p_fmt in varchar2, 20 p_argl in varchar2 default null, 21 p_arg2 in varchar2 default null, 22 p_arg3 in varchar2 default null, 23 p_arg4 in varchar2 default null, 24 p_arg5 in varchar2 default null), 25 member procedure flush, 26 27 static procedure write_io(p_file in number, 28 p_operation in varchar2, 29 p_j5arml in varchar2 default null, 30 pjparm2 in varchar2 default null, 31 p_parm3 in varchar2 default null, 32 p_parm4 in varchar2 default null, 33 p_parm5 in varchar2 default null, 34 p_parm6 in varchar2 default null) 35 ) 36 / Type created. Эта спецификация очень похожа на спецификацию пакета UTL_FILE (если вы не знакомы с пакетом UTL_FILE, можете прочитать о нем в приложении А). Он обеспечивает практически те же функциональные возможности, что и пакет UTL_FILE, просто в более удобном (как мне кажется) виде. Помните, при рассмотрении создания типа ADDRESS_TYPE я говорил, что каждый объектный тип имеет один стандартный конструктор и в этом конструкторе надо задать значения для всех атрибутов типа. Пользо-
Использование объектно-реляционных средств
Зо5
вательский код этот стандартный конструктор не выполняет. Другими словами, он может использоваться только для установки атрибутов объектного типа. Это не слишком удобно. Статическая функция OPEN в представленном выше типе будет использоваться для демонстрации создания собственных, куда более полезных (и сложных), конструкторов для типов. Обратите внимание, что функция OPEN — часть объектного типа FILETYPE сама возвращает данные типа FILETYPE. Она выполняет необходимую настройку и возвращает полностью инициализированный объект. Именно для этого в основном используются статические методы-функции в объектных типах: с их помощью создают сложные конструкторы объектов. Статические функции и процедуры в объектном типе отличаются от остальных процедур и функций тем, что не получают неявного параметра SELF. Эти функции похожи на функции или процедуры пакета. Они пригодятся для реализации общих утилит, вызываемых другими методами, но не требующих доступа к данным экземпляра (атрибутам объекта). Процедура WRITE_IO в представленном выше объектном типе — пример такого рода утилиты. Я использую ее для обращения к пакету UTL_FILE, связанного с записью в файл, так что не приходится каждый раз повторять 14-строчный блок обработки исключительных ситуаций. Обратите внимание, что в этом объектном типе нет ссылок на тип данных UTL_FILE.FILE_TYPE, поскольку атрибуты объектного типа могут быть только SQLтипов. Эту запись необходимо сохранить в другом месте. Для этого я собираюсь использовать PL/SQL-пакет следующего вида: tkyte@TKYTE816> create or replace package FileType_pkg 2 as 3 type utl_fileArrayType is table of utl_file.file_type 4 index by binary_integer; 5 6 g_files utl_fileArrayType; V 8 g_invalid_path_msg constant varchar2(131) default 9 'INVALID PATH: Недопустимое местонахождение или имя файла.'; 10 11 g_invalid_mode_msg constant varchar2(131) default 12 'INVALID_MODE: Недопустимый параметр open_mode %s в вызове FOPEN.'; 13 14 g_invalid_filehandle_msg constant varchar2(131) default 15 'INVALID_FILEHANDLE: Недопустимый дескриптор файла.'; 16 17 g_invalid_operation_msg constant varchar2(131) default 18 'INVALID_OPERATION: Файл нельзя открыть или обработать так, '|I 19 'как запрошено.'; 20 21 g_read_error_msg constant varchar2(131) default 22 'READ_ERROR: В ходе операции чтения произошла ошибка '|| 23 'операционной системы.'; 24 25 g_write_error_msg constant varchar2(131) default 26 'WRITE_ERROR: В ходе операции записи произошла ошибка '|| 27 'операционной системы.'; 28
13 Заг. 244
386 29 30 31 32 33 34 35 36
Глава 20 g_internal_error_msg constant varchar2(131) default 1 'INTERNAL_ERROR: Неопределенная ошибка в PL/SQL. ; g_invalid_maxlinesize_msg constant varchar2(131) default 'INYALID_MAXLINESIZE: Указанный максимальный размер строки %d — 'II 'слишком велик или слишком мал'; end; /
Package created.
Этот пакет будет использоваться для хранения записей типа UTL_FILE.FILE_TYPE в процессе выполнения. Каждый экземпляр объектного типа (переменная) FILE_TYPE будет выделять себе пустой "слот" в представленном выше массиве G_FILES. Это показывает, как создавать "приватные" данные в объектных типах Oracle. Реальные данные времени выполнения будут храниться в переменной пакета G_FILES, а в объектном типе — только дескриптор (индекс в массиве). В текущей реализации объектов в Oracle все данные объектного типа — общедоступны. Невозможно создать скрытый атрибут типа, недоступный для пользователей. Например, в случае представленного выше типа FILE_TYPE вполне можно обратиться к переменной экземпляра G_FILE_NAME непосредственно. Если это нежелательно, необходимо скрыть эту переменную в PL/SQLпакете так же, как мы скрыли там тип PL/SQL-записи. Никто не сможет обратиться данным в PL/SQL-пакете, не получив привилегию EXECUTE для этого пакета, поэтому данные защищены. Этот пакет также используется для хранения ряда констант. Объектные типы не поддерживают неизменяемые данные, поэтому пакет представляет собой удачное место для их хранения. Я предпочитаю называть пакет, поддерживающий тип подобным образом, так, чтобы в его имя входило имя типа. Поскольку мы создали тип FILETYPE, для его поддержки создан пакет FILETYPE_PKG. Теперь можно переходить к телу типа FILETYPE. Оно будет содержать все представленные ранее методы, статические функции и процедуры. Ниже приведен код с комментариями. tkyte@TKYTE816> c r e a t e or replace type body FileType 2 as 3
4 static function open(p_path in varchar2, 5 p_file_name in varchar2, 6 p_mode in varchar2 default 'r', 7 p_maxlinesize in number default 32765) 8 return FileType 9 is 10 l_file_hdl number; 11 l_utl_file_dir varchar2(1024); 12 begin 13 l_file_hdl := nvl(fileType_pkg.g_files.last, 0)+l; 14 15 filetype_pkg.g_files(l_file_hdl) := 16 utl_file.fopen(p_path, p_file_name, p_mode, p_maxlinesize);
Использование объектно-реляционных средств
3 8 У
17 18
return fileType(p_file_name,
p_path, l_file_hdl);
Представленная выше часть статической функции OPEN отвечает за поиск свободного слота в приватных данных (скрытых в пакете filetype j k g ) . Для этого она добавляет единицу к значению атрибута LAST PL/SQL-таблицы. Если таблица пуста, LAST имеет значение NULL, поэтому его значение передается функции NVL, и первое выделяемое значение будет иметь индекс 1. Следующее — 2 и так далее. Функция CLOSE будет удалять записи при закрытии файла, так что место в массиве при открытии и закрытии файлов будет использоваться повторно. Остальная часть функции очень проста; она открывает указанный файл и возвращает готовый к использованию, полностью проинициализированный экземпляр объекта FILETYPE. Далее в функции FILETYPE.OPEN идет блок обработки исключительных ситуаций, позволяющий перехватить и обработать все ошибки, которые могут возникнуть при выполнении функции UTLJFTLE.FOPEN: 19 exception 20 when utl_file.invalid_path then 21 begin 22 execute immediate 'select value 23 from v$parameter 24 where name = ''utl_file_dir''' 25 into l_utl_file_dir; 26 exception 27 when others then 28 l_utl_file_dir := p_path; 29 end; 30 if (instr( l_utl_file_dir|Г,', p_path II', 1 ) = 0 ) 31 then 32 raise_application_error 33 (-20001, 'Каталог ' | | p_j>ath | | 34 ' не входит в список каталогов utl_file_dir "' И 35 l_utl_f ile_dir | | " " ) ; 36 else 37 raise_application_error 38 (-20001,fileType_pkg.g_invalid_path_msg); 39 end if; 40 when utl_file.invalid_mode then 41 raise_application_error 42 (-2 0002,replace(fileType_pkg.g_invalid_mode_msg,'%s',p_mode)); 43 when utl_file.invalid_operation then 44 raise_application_error 45 (-20003,fileType_pkg.g_invalid_operation_msg); 46 when utl_file.internal_error then 47 raise_application_error 48 (-20006,fileType_pkg.g_internal_error_msg); 49 when utl_file.invalid_maxlinesize then 50 raise_application_error 51 (-20007, replace(fileType_pkg.g_invalid_maxlinesize_msg, 52 '%d',p_maxlinesize)); 53 end;
388
Глава 20
Блок обработки исключительных ситуаций создан для перехвата и повторного возбуждения исключительных ситуаций UTL_FILE более удобным образом, чем это делает пакет UTL_FILE. Вместо получения в вызывающей подпрограмме обычного сообщения об ошибке (SQLERRM) USER DEFINED EXCEPTION, мы получим нечто более осмысленное, вроде: Недопустимый режим открытия файла. Кроме того, для исключительной ситуации INVALID_PATH, которая возбуждается в том случае, когда файл нельзя открыть из-за неверного имени файла или каталога, выполняются дополнительные проверки, и причина ошибки устанавливается более точно. Если владелец этого типа имеет привилегию SELECT на представление SYS.V_$PARAMETER, мы выбираем из него значение параметра инициализации UTL_FILE_DIR и проверяем, можно ли использовать тот каталог, который мы пытаемся использовать. Если нельзя, выдается соответствующее сообщение. Из всех ошибок, происходящих в процессе работы с пакетом UTL_FILE, эта, несомненно, самая "популярная". Выдавая такое точное сообщение об ошибке, мы сэкономим многие часы отладки для начинающих пользователей пакета UTL_FILE. Продолжая рассмотрение, переходим к методу Open: 55 member function isOpen return boolean 56 is 57 begin 58 return utl_file.is_open(filetype_pkg.g_files(g_file_hdl)); 59 end; Это просто оболочка для существующей функции UTL_FILE.IS_OPEN. Поскольку эта функция пакета UTL_FILE никогда не возбуждает исключительных ситуаций, ее реализация очень проста. Далее идет более сложный метод GET_LINE: 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
member function get_line return varchar2 is l_buffer varchar2(32765); begin utl_file.get_line(filetype_pkg.g_files(g_file_hdl), l_buffer); return l_buffer; exception when utl_file.invalid_filehandle then raise_application_error (-20002,fileType_pkg.g_invalid_filehandle_msg); when utl_file.invalid_operation then raise_application_error (-20003,fileType_j?kg.g_invalid_operation_msg); when u t l _ f i l e . r e a d _ e r r o r then raise_application_error (-20004,fileType_pkg.g_read_error_msg); when utl_file.internal_error then raise_application_error (-20006,fileType_pkg.g_internal_error_msg); end;
В нем используется локальная переменная типа VARCHAR2(32765), — переменная максимально возможного в PL/SQL размера и одновременно самая длинная строка,
Использование объектно-реляционных средств
которую фактически позволяет прочитать пакет UTL_FILE. Как и в представленном ранее методе OPEN, мы перехватываем и обрабатываем исключительные ситуации, которые возбуждаются подпрограммой UTL_FILE.GET_LINE, и преобразуем их в вызовы RAISE_APPLICATION_ERROR. Это позволяет выдавать информативные сообщения об ошибках в функции GET_LINE (для удобства использования GET_LINE реализована как функция, а не как процедура). Теперь переходим к другой статической процедуре — WRITE_IO. Процедура WRITE_IO используется единственно для того, чтобы избежать написания одних и тех же обработчиков исключительных ситуаций шесть раз, для каждой из подпрограмм, связанных с записью, поскольку все они возбуждают одни и те же исключительные ситуации. Эта функция, добавленная исключительно для удобства программирования, просто вызывает одну из шести функций пакета UTL_FILE и обрабатывает все возможные ошибки: 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
static procedure write_io(p_file in number, p_operation in varchar2, p_parml in varchar2 default null, p_parm2 in varchar2 default null, p_parm3 in varchar2 default null, p_parm4 in varchar2 default null, p_parm5 in varchar2 default null, p_parm6 in varchar2 default null) is l_file utl_file.file_type default filetype_pkg.g_files(p_file); begin if (p_operation='close') then utl_file.fclose(l_file); elsif (p_operation='puf) then utl_f ile.put(l_file,p_parml); elsif (p_operation='new_line') then utl_file.new_line(l_file,p_parml); elsif (p_operation='put_line') then utl_file.put_line(l_file, p_parml), elsif (p_operation='flush') then utl_file.fflush(l_file); elsif (p_operation='putf') then utl_file.putf(I_file,p_parml,p_parm2, 5, p_parm3,p_parm4,p_parm5, p_parm6); else raise program_error; end if; exception when utl_file.invalid_filehandle then raise_application_error (-20002,fileType_pkg.g_invalid_filehandle_msg)• when utl_file.invalid_operation then raise_application_error (-20003,fileType_pkg.g_invalid_operation_msg); when utl file.write error then
390
Глава 20
117 raise_application_error 118 (-20005,fileType_pkg.g_write_error_msg); 119 when utl_file.internal_error then 120 raise_application_error 121 (-20006,fileType_pkg.g_internal_error_msg); 122 end; Остальные методы вызывают метод WRITE_IO с соответствующими параметрами: 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
member procedure close is begin fileType.write_io(g_file_hdl, 'close'); filetype_pkg.g_files.delete(g_file_hdl) ; end; member procedure put(p_text in varchar2) is begin fileType.write_io(g_file_hdl, 'put',p_text); end; member procedure new_line(p_lines in number default 1) is begin fileType.write_io(g_file_hdl, 'new_line',p_lines); end; member procedure put_line(p_text in varchar2) is begin fileType.write_io(g_file_hdl, 'put_line', p_text); end; member procedure putf (p_fmt in varchar2, p_argl in varchar2 default null, p_arg2 in varchar2 default null, p_arg3 in varchar2 default null, p_arg4 in varchar2 default null, p_arg5 in varchar2 default null) is begin fileType.write_io (g_file_hdl, 'putf, p_fmt, p_argl, p_arg2, p_arg3, p_arg4, p_arg5); end; member procedure flush is begin fileType.write_io(g_filejhdl, 'flush'); end; end;
Использование объектно-реляционных средств
167
3 9 1
/
Type body created. В представленном коде перехватываются все исключительные ситуации пакета UTL_FILE и возбуждаются другие исключительные ситуации с помощью процедуры RAISE_APPLICATION_ERROR. Это основная причина инкапсуляции средств пакета UTL_FILE в объектный тип. Пакет UTL_FILE возбуждает исключительные ситуации с описанием USER-DEFINED EXCEPTION. Эти исключительные ситуации определены разработчиками пакета UTL_FILE, и при их возбуждении сервер Oracle выдает сообщение: "USER-DEFINED EXCEPTION". Оно не слишком информативно и не помогает разобраться в причинах ошибки. Я предпочитаю использовать процедуру RAISE_APPLICATION_ERROR, которая позволяет задать значения встроенных функций SQLCODE и SQLERRM, возвращаемые клиенту. Чтобы увидеть, как это может повлиять на отладку, достаточно рассмотреть следующий небольшой пример, демонстрирующий, какого рода сообщения об ошибках можно получить от пакета UTL_FILE и объектного типа FILETYPE: tkyte@TKYTE816> declare 2 f utl_file.file_type := utl_file. fopen('с:\temp\bogus', 1 3 foo.txt', 'w'); 4 begin 5 utl file.fclose(f); 6 end; 7 / declare ERROR at line ORA-06510: PL/SQL: unhandled user-definedexception ORA-06512: at "SYS.UTL_FILE", line 98 ORA-06512: at "SYS.UTL_FILE", line 157 ORA-06512: at line 2 tkyte@TKYTE816> declare 2 f fileType := fileType.open('с:\temp\bogus', •foo..txt', 'w'); 3 4 begin 5 f..close; 6 end; 7 / declare * ERROR a t line 1: ORA-20001: The path c:\temp\bogus i s not in the utl_file_dir path "c:\teinp, c:\oracle" ORA-06512: a t "TKYTE.FILETYPE", line 54 ORA-06512: a t line 2 Нетрудно понять, с помощью какого сообщения проще определить причину ошибки. Второе сообщение об ошибке (при наличии у владельца типа доступа к представлению V$PARAMETER) очень точно объясняет причину ошибки: использован недопус-
392
Глава 20
тимый каталог, не указанный в параметре инициализации UTL_FILE_DIR. Даже при отсутствии доступа к представлению VSPARAMETER будет выдано следующее сообщение: * ERROR a t l i n e 1: ORA-20001: INVALID_PATH: F i l e location or filename was i n v a l i d . ORA-06512: a t "TKYTE.FILETYPE", l i n e 59 ORA-06512: a t l i n e 2 которое все равно лучше, чем краткое user-defined exception. В этом типе также следует обратить внимание на возможность установки предпочтительных стандартных значений параметров подпрограмм. Например, до версии Oracle 8.0.5 пакет UTL_FILE имел ограничение на максимальную длину строки — 1023 байт. Если попытаться выдать более длинную строку, пакет UTL_FILE возбуждает исключительную ситуацию. По умолчанию точно так же происходит и в Oracle 8i. Если явно не указать максимальный размер строки при вызове UTL_FILE.FOPEN, ограничение 1023 байт остается. Я лично предпочитаю по умолчанию устанавливать максимальную длину строки равной 32 Кбайт. В начале кода я также задал стандартный режим открытия файла — на чтение ('R'). Поскольку в 90 процентах случаев я использую пакет UTL_FILE для чтения файла, такое стандартное значение для меня имеет смысл. Теперь давайте проверим работу объектного типа и рассмотрим использование всех функций и процедур. Сначала создадим файл (предполагается, что мы работаем в Windows NT, существует каталог c:\temp и параметр инициализации UTL_FILE_DIR содержит c:\temp) и запишем в него определенные данные. Затем мы закроем этот файл, сохранив данные. Это продемонстрирует возможности записи типа FILETYPE: tkyte@TKYTE816> declare 2 f fileType := fileType.open('c:\temp', 'foo.txt', 'w'); 3 begin 4 if (f.isOpen) 5 then 6 dbms_output.put_line('Файл открыт'); 7 end if; 8 9 for i in 1 .. 10 loop 10 f.put(i || ' , ' ) ; 11 end loop; 12 f.put_line(ll); 13 14 f.new_line(5); 15 for i in 1 .. 5 16 loop 17 f.put_line('строка ' || i); 18 end loop; 19 20 f.putf('%s % s \ 'Hello', 'World'); 21 22 f.flush; 23
Использование объектно-реляционных средств 24 f.close; 25 end; 26 / Файл открыт PL/SQL procedure successfully completed. Далее продемонстрировано чтение файла с помощью объекта типа FILETYPE. Откроем только что записанный файл и убедимся, что прочитаны именно те данные, которые в нем содаржатся: tkyte@TKYTE816> declare 2 f fileType := fileType.open('с:\temp', ' f o o . t x t ' ) ; 3 begin 4 if (f.isOpen) 5 then 6 dbms output.put line('Файл открыт'); 7 end if; ' 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
dbms_output.put_line ('строка 1: (должна быть 1,2,...,11)' || f.get_line); for i in 2 .. 6 loop dbms_output.put_line ('строка ' | | i | | ': (должна быть пустой)' || f.get_line); end loop; for i in 7 .. 11 loop dbms_output.put_line ('строка ' || to_char(i+l) || ': (должна быть строка N ) ' || f.get_line); end loop; dbms_output.put_line ('строка 12: (должна быть Hello World)' || f.get_line);
on
28 begin 29 dbms_output.put_line(f.get_line); 30 dbms_output.put_line('в предыдущем операторе должна *"* произойти ошибка' ) ; 31 exception 32 when NO_DATA_FOUND then 33 dbms_output.put_line('получили no data found, как и w ожидалось'); 34 end; 35 f lose36 end37 / Файл открыт строка 1:.(должна быть 1,2,...,11)1,2,3,4,5,6,7,8,9,10,11
394
Глава 20
строка 2: (должна быть пустой) строка 3: (должна быть пустой) строка 4: (должна быть пустой) строка 5: (должна быть пустой) строка 6: (должна быть пустой) строка 8: (должна быть строка N)строка 1 строка 9: (должна быть строка N)строка 2 строка 10: (должна быть строка N)строка 3 строка 11: (должна быть строка N)строка 4 строка 12: (должна быть строка N)строка 5 строка 12: (должна быть Hello World)Hello World получили no data found, как и ожидалось PL/SQL procedure successfully completed. Мы инкапсулировали пакет UTL_FILE в объектный тип Oracle. Получился замечательный интерфейс к стандартному пакету, работающий именно так, как нам хотелось. В терминах объектного программирования, мы создали класс UTL_FILE, включив в него методы, которые работают так, как нужно нам, а не так, как придумали разработчики Oracle. Мы не переписывали пакет UTL_FILE, а только создали для него другой интерфейс. Это хороший прием программирования, позволяющий легко решать проблемы, связанные с изменением реализации пакета UTL_FILE или появлением ошибки в новой версии. Все эти проблемы можно решить, изменив тело типа, а не сотни или тысячи непосредственно зависящих от пакета приложений. Например, в одной из версий пакета UTL_FILE нельзя было открыть несуществующий файл в режиме А (на добавление); при этом файл не создавался, хотя и должен был. Для решения проблемы достаточно было написать следующий код: begin f i l e _ s t a t := u t l _ f i l e . f o p e n ( f i l e _ d i r , f i l e _ n a m e , ' a ' ) ; exception — если файл не существует, fopen не сработает — из-за ошибки режима ' а ' : 371510 when u t l _ f i l e . i n v a l i d _ o p e r a t i o n then — все остальные исключительные ситуации распространяются — во внешний блок, как обычно f i l e _ s t a t := u t l _ f i l e . f o p e n ( f i l e _ d i r , f i l e _ n a m e , ' w ' ) ; end; Если файлы открываются на добавление в 100 подпрограммах, придется делать много исправлений. Если же использован дополнительный уровень интерфейса, достаточно внести изменение только в одном месте.
Уникальные приемы использования наборов Еще один вариант применения объектных типов в PL/SQL связан с использованием наборов и их возможностей взаимодействия с языками SQL и PL/SQL. Наборы позволяют сделать три вещи в SQL и PL/SQL, о реализации которых часто спрашивают разработчики.
Использование объектно-реляционных средств
•
Как выбрать данные (SELECT *) из PL/SQL-функции?* Можно написать PL/SQLфункцию и обращаться с запросами к ней, а не к таблице базы данных.
Q Как выбрать данные в массив записей? PL/SQL изначально позволяет выполнять выборку данных BULK COLLECT (выбирать по нескольку строк за раз) в PL/SQLтаблицу. К сожалению, это можно делать только в PL/SQL-таблицы с элементами скалярных типов. Я не могу сделать следующее: s e l e c t c l , c2 BULK COLLECT INTO record_type from T поэтому приходится писать: s e l e c t c l , c2 BULK COLLECT INTO t a b l e l , table2 from T С помощью наборов можно обеспечить выборку данных в записи. •
Как вставить запись? Вместо вставки по столбцам, я могу вставить в таблицу одну запись.
SELECT * из PL/SQL-функции Чтобы продемонстрировать эту возможность, давайте вернемся к проблеме использования связываемых переменных (это моя любимая тема). Часто мне приходится слышать утверждения, что необходимо выполнить запрос вида: s e l e c t * from t where c in (:bind_variable) где BIND_VARIABLE представляет собой список значений. Другими словами, переменная BIND_VARIABLE получает, допустим, значение ' 1 , 2, 3' и необходимо, чтобы представленный выше запрос выполнялся как: s e l e c t * from t where с in (1, 2, 3) и возвращал строки, в которых с = 1, 2 или 3; но при таком значении связываемой переменной фактически выполняется запрос: s e l e c t * from t where c in ( ' 1 , 2 , 3 ' ) Он возвращает строки, в которых с = '1,2,3' — значение столбца с равно одной такой строке. Такие запросы часто порождаются пользовательскими интерфейсами, в которых пользователь может отметить флажками одно или несколько (любое количество) возможных значений из списка. Чтобы не создавать уникальные запросы для каждого сочетания флажков (мы знаем, насколько это плохо), необходим метод связывания произвольного количества элементов в списке. Ну, поскольку можно выполнять SELECT * из PL/SQL-функции, решение есть. Сейчас я его продемонстрирую: tkyte@TKYTE816> c r e a t e or replace type myTableType 2 as t a b l e of number; 3 / Type created. Созданный таким образом тип данных и будет возвращать PL/SQL-функция. Этот тип обязательно должен быть задан на уровне SQL с помощью оператора CREATE TYPE. * Другая формулировка той же проблемы: как возвращать результирующие множества из хранимых функций? Прим. научн. ред.
396
Глава 20
Нельзя использовать тип, заданный в PL/SQL-пакете; конечная цель — выбирать данные с помощью SQL-операторов, поэтому необходим тип данных SQL. Этот пример также показывает, что я не являюсь принципиальным противником использования вложенных таблиц. Именно этот тип набора оптимален при программировании на PL/SQL, если приходится решать подобного рода задачи. При использовании массива VARRAY придется искусственно ограничить количество элементов. Размер же вложенной таблицы ограничен только объемом памяти, доступной в системе. tkyte@TKYTE816> create or replace 2 function s t r 2 t b l ( p _ s t r in varchar2) return myTableType 3 as 4 l_str long default p_str |I ', '; 5 l_n number; 6 l_data myTableType := myTabletype(); 7 begin 8 loop 9 l_n := instr(l_str, ' , ' ) ; 10 exit when (nvl(l_n,0) = 0); 11 l_data.extend; 12 l_data(l_data.count) := 13 ltrim(rtrim(substr(l_str,l,l_n-l))); 14 l_str := substr(l_str, l_n+l); 15 end loop; 16 return l_data; 17 end; 18 / Function created. Итак, создана PL/SQL-функция, принимающая строку со списком значений через запятую и преобразующая ее в SQL-тип MYTABLETYPE. Осталось только найти способ выбрать эти данные с помощью SQL-оператора. Это легко сделать с помощью оператора TABLE и преобразования типа, CAST: tkyte@TKYTE816> v a r i a b l e bind_variable varchar2(30) tkyte@TKYTE816> exec :bind_variable := ' 1 , 3 , 5 , 7 , 9 9 ' PL/SQL procedure successfully completed. BIND VARIABLE 1,3,5,7,99 tkyte@TKYTE816> select * 2 from TABLE (cast(str2tbl(:bind_variable) as myTableType)) 3 / COLUMN__VALUE 1 3
5 7 99
Использование объектно-реляционных средств
j y /
Теперь легко использовать эту конструкцию в подзапросе IN: tkyte@TKYTE816> select * 2 from a l l users 3 where user_id in 4 (select * 5 from TABLE (cast(str2tbl(:bind_variable) as myTableType)) 6 ) 7 / USERNAME SYSTEM
USER_ID CREATED 5 04-NOV-OO
Этот прием можно использовать во многих случаях. Можно применить к значению PL/SQL-переменной типа набора операцию ORDER BY, можно возвращать клиенту наборы данных, сгенерированные PL/SQL-функцией, можно задавать конструкции WHERE для выбора определенных значений PL/SQL-переменных и т.д. Если пойти чуть дальше, таким способом можно возвращать полные результирующие множества с несколькими столбцами. Например: tkyte@TKYTE816> create type myRecordType as object 2 (seq int, 3 a int, 4 b varchar2(10), 5 с date 6 ) 7 / Type created. tkyte@TKYTE816> create table t (x int, у varchar2(10), z date); Table created. tkyte@TKYTE816> create or replace type myTableType 2 as table of myRecordType 3 / Type created. tkyte@TKYTE816> create or replace function my_function return myTableType 2 is 3 l_data myTableType; 4 begin 5 l_data := myTableType(); 6 7 for i in 1..5 8 loop 9 l_data.extend; 10 l_data(i) := myRecordType(i, i, 'row ' || i, sysdate+i); 11 end loop; 12 return l_data; 13 end; 14 / Function created.
398
Глава 20
tkyte@TKYTE816> select * 2 from TABLE (cast(my_function() as mytableType)) 3 where с > sysdate+1 4 order by seq desc 5 / SEQ 5 4 3 2
А В 5 4 3 2
row row row row
С 5 4 3 2
29-MAR-01 28-MAR-01 27-MAR-01 26-MAR-01
Множественная выборка данных в записи Итак, мы рассмотрели, как использовать наборы для выборки данных из PL/SQLфункции. Теперь разберемся, как с их помощью обеспечить множественную выборку данных в аналог PL/SQL-записей. Выполнить множественную выборку в массив реальных PL/SQL-записей нельзя, но можно выбрать данные во вложенную таблицу SQL. Для этого потребуется два объектных типа: скалярный тип, представляющий запись, и вложенная таблица с записями этого типа. Например: tkyte@TKYTE816> c r e a t e type myScalarType 2 as object 3 (username varchar2(30), 4 user_id number, 5 created date 6 ) 7 / Type created. tkyte@TKYTE816> create type myTableType as table of myScalarType 2 / Type created. Теперь все готово для выборки данных в переменную типа MYTABLETYPE следующим образом: tkyte@TKYTE816> declare 2 l_users myTableType; 3 begin 4 select cast(multiset(select username, user_id, created 5 from all_users 6 order by username) 7 as myTableType) 8 into l_users 9 from dual; 10 11 dbms_output.put_line('Retrieved '|l l_users.count || ' rows'); 12 end; 13 / Retrieved 25 rows PL/SQL procedure successfully completed.
Использование объектно-реляционных средств
Запрос к представлению ALL_USERS можно заменить любым запросом, выбирающим строку типа VARCHAR2(30), число и дату. Запрос может быть сколь угодно сложным, включать соединения и т.п. Фокус в том, что результаты этого подзапроса преобразуются в объектный тип. Затем можно выбрать все результирующее множество в локальную переменную с помощью стандартного оператора SELECT ... INTO.
Вставка записей Зная, что можно выполнять операторы SELECT * FROM НАБОР, где НАБОР — это либо локальная переменная, либо PL/SQL-функция, возвращающая вложенную таблицу, нетрудно придумать, как выполнить аналогичным образом оператор INSERT. Необходимо задать переменную типа вложенной таблицы и заполнить ее записями, которые требуется вставить. Следующий пример демонстрирует, как будет выглядеть вставка одной строки: tkyte@TKYTE816> create t a b l e t as s e l e c t * from a l l users where 1=0; — Table created. tkyte@TKYTE816> declare 2 l_users myTableType := 3 myTableType(myScalarType('torn', 1, sysdate)); 4 begin 5 insert into t 6 select * from TABLE (cast(l_users as myTableType)); 7 end; 8 / tkyte@TKYTE816> select * from t; USERNAME torn .
USER_ID CREATED 1 24-MAR-01
При работе с многостолбцовой таблицей этот прием может пригодиться. Итак, в этом разделе мы рассмотрели использование объектных типов Oracle для расширения возможностей языка PL/SQL аналогично тому, как для этих целей используются классы в языках Java или C++. Описаны также интересные варианты использования вложенных таблиц. Возможность выполнять оператор SELECT * из PL/SQL-функции открывает заманчивые перспективы. Списки значений произвольной длины для конструкции IN — только начало. Возможности поистине безграничны. Можно написать небольшую функцию, использующую средства пакета UTL_FILE для чтения файла ОС, разбиения прочитанных строк на поля по запятым и возвращения результирующего множества, построенного по содержимому обычного файла, для вставки в другую таблицу или соединения с существующей таблицей. Такое использование объектных типов дает новую жизнь хорошо зарекомендовавшему себя языку программирования. Создав самостоятельно один-два типа, вы найдете применение этой методике во множестве приложений. Это логический способ объединения данных и функций для работы с ними, что является одной из основных целей объектно-ориентированного программирования. Предвидя протесты, я не называю это
400
Глава 20
чисто объектно-ориентированным программированием на PL/SQL, но это, определенно, очень близкая к нему методика.
Объектно-реляционные представления Это весьма мощное средство для тех, кто хочет работать с объектно-реляционными расширениями, но должен обеспечивать для множества приложений реляционное представление данных. Можно использовать стандартный механизм VIEW для создания объектов на основе реляционных таблиц. Не нужно создавать объектные таблицы, со всеми их мистическими столбцами — можно создать объектное представление стандартных таблиц (скорее всего уже существующих). Подобные представления обеспечат возможности, аналогичные объектной таблице того же типа, но без дополнительных затрат ресурсов на поддержку скрытых ключей, суррогатных ключей и пр. В этом разделе мы используем таблицы ЕМР и DEPT для создания представления данных по отделам. Это очень похоже на пример создания вложенной таблицы в главе 6, где был создан тип EMP_TAB_TYPE как вложенная таблица записей типа EMP_TYPE, а таблица DEPT содержала столбец — вложенную таблицу этого типа. Здесь мы еще раз смоделируем типы EMP_TYPE и EMP_TAB_TYPE, но создадим еще и объектный тип DEPT_TYPE, а также представление этого типа. Интересно отметить, что использование объектных представлений позволяет взять лучшее из двух миров (реляционного и объектно-реляционного). Например, можно создать приложение, в котором должно использоваться представление данных по отделам. В представлении указаны данные по отделам, а информация о сотрудниках отдела естественным образом представляется как набор, один из атрибутов отдела. Другому приложению необходимо другое представление тех же данных. Например, службе охраны на проходной необходим доступ к данным по сотрудникам. Отдел в данном случае — лишь атрибут записи о сотруднике, в то время как для другого приложения список сотрудников является атрибутом отдела. В этом — сила реляционной модели: она позволяет одновременно поддерживать несколько представлений данных. Объектная модель не обеспечивает поддержку нескольких различных представлений одних и тех же данных так же просто (если вообще обеспечивает) или эффективно. Используя несколько различных объектных представлений реляционных данных, можно обеспечить потребности всех приложений.
Необходимые типы Использованные в этом примере типы позаимствованы из главы 6, с добавлением типа DEPT_TYPE. Вот как они создаются: scott@TKYTE816> create or replace type emp type 2 as object 3 (empno number(4), 4 ename varchar2(10), 5 job varchar2(9), 6 mgr number(4), 7 hiredate date, 8 sal number(7, 2),
Использование объектно-реляционных средств
9 10 11
comm ); /
4 0 1
number(7, 2)
Туре created. scott@TKYTE816> c r e a t e or replace type emp_tab_type 2 as t a b l e of emp_type 3 / Type created. scott@TKYTE816> create or replace type dept_type 2 as object 3 (deptno number(2), 4 dname varchar2(14), 5 loc varchar2(13), 6 emps emp_tab_type 7 ) 8 / Type created. Отдел моделируется как объект с такими атрибутами: номер отдела (deptno), название (dname), местонахождение (loc) и список сотрудников (emps).
Объектно-реляционное представление По представленным выше определениям типов легко получить данные для этого представления по существующим реляционным данным. Вот как создается представление: scott@TKYTE816> c r e a t e or replace view dept_or 2 of dept_type 3 with object identifier(deptno) 4 as 5 select deptno, dname, loc, 6 cast (multiset ( 7 select empno, ename, job, mgr, hiredate, sal, comm 8 from emp 9 where emp.deptno = dept.deptno) 10 as emp_tab_type ) 11 from dept 12 / View created. Назначение конструкций CAST и MULTISET вам уже знакомо — с их помощью коррелированный подзапрос преобразуется в набор типа вложенной таблицы. Для каждой строки в таблице DEPT мы запрашиваем всех сотрудников соответствующего отдела. С помощью конструкции WITH OBJECT IDENTIFIER серверу Oracle можно сообщить, какой столбец (или столбцы) однозначно идентифицируют строку в представлении. Это позволяет серверу автоматически создать ссылку на объект (object reference), что и обеспечивает возможности работы с этим представлением как с объектной таблицей.
402
Глава 20
Созданное представление можно использовать: scott@TKYTE816> s e l e c t dname, d.emps 2 from dept_or d 3 / DNAME
EMPS(EMPNO, ENAME, JOB, MGR, HIREDATE, S
ACCOUNTING
EMP_TAB_TYPE(EMPJTYPE(7782, 'CLARK', 'MANAGER1, 7839, '09-JUN-81', 2450, NULL), EMP_TYPE(7839, 'KING', 'PRESIDENT', NULL, 47-NOV-81', 5000, NULL), EMP_TYPE(7934, 'MILLER', 'CLERK1, 7782, '23-JAN-82', 1300, NULL))
RESEARCH
EMP_TAB_TYPE(EMPJTYPE(7369, 'SMITH', 'CLERK', 7902, '17-DEC-80', 800, NULL), EMP_TYPE(7566, 'JONES', 'MANAGER', 7839, '02-APR-81', 2975, NULL), EMP_TYPE(7788, 'SCOTT1, 'ANALYST', 7566, '09-DEC-82', 3000, NULL), EMP_TYPE(7876, 'ADAMS', 'CLERK', 7788, '12-JAN-83', 1100, NULL), EMP_TYPE(7902, 'FORD', 'ANALYST', 7566, '03-DEC-81', 3000, NULL))
SALES
EMP_TAB_TYPE(EMP_TYPE(7499, 'ALLEN', 'SALESMAN', 7698, '20-FEB-81', 1600, 300), EMP_TYPE(7521, 'WARD', 'SALESMAN1, 7698, '22-FEB-81', 1250, 500), EMP_TYPE(7654, 'MARTIN', 'SALESMAN', 7698, '28-SEP-811, 1250, 1400), EMP_TYPE(7698, 'BLAKE', 'MANAGER', 7839, '01-MAY-81', 2850, NULL), EMP_TYPE(7844, 'TURNER1, 'SALESMAN1, 7698, '08-SEP-811, 1500, 0), EMP_TYPE(7900, 'JAMES1, 1 'CLERK', 7698, '03-DEC-81 , 950, NULL))
OPERATIONS
EMP_TAB__TYPE ()
4 rows selected. scott@TKYTE816> select deptno, dname, loc, count(*) 2 from dept_or d, table (d.emps) 3 group by deptno, dname, loc 4 / DEPTNO DNAME 10 ACCOUNTING 20 RESEARCH 30 SALES
LOC
COUNT(*)
NEW YORK DALLAS CHICAGO
3 rows selected. Итак, у нас есть реляционные таблицы и объектно-реляционное представление. Пользователю трудно определить, где — представление, а где — таблицы. Все возмож-
Использование объектно-реляционных средств
403
ности объектной таблицы доступны: из нее можно выбрать ссылки на объекты, вложенная таблица создана и т.д. Преимущество такой реализации в том, что мы явно указываем, как соединять таблицы ЕМР и DEPT, используя естественное отношение — главный/подчиненный. Итак, создано объектно-реляционное представление, позволяющее запрашивать данные. Но изменять его данные пока нельзя: scott@TKYTE816> update TABLE ( s e l e c t p.emps 2 from dept_or p 3 where deptno = 2 0 ) 4 set ename = lower(ename) 5 / set ename = lower(ename) * ERROR at line 4: ORA-25015: cannot perform DML on this nested table ale view view column column scott@TKYTE816> declare 2 1 emps emp tab type; 3 begin 4 select p.emps into l_emps 5 from dept or p — 6 where deptno = 10; 8 for i in 1 .. l_emps.count 9 loop 10 l_emps(i).ename := lower(l_emps(i).ename); 11 end loop; 12 13 update dept_or 14 set emps - l_emps 15 where deptno = 10; 16 end; 17 / declare * ERROR at line 1: ORA-01733: virtual column not allowed here ORA-06512: at line 13 Необходимо заставить представление "обновляться". У нас реализовано сложное сопоставление реляционных данных объектно-реляционным (на самом деле оно может иметь любую степень сложности). Так как же заставить представление "обновляться"? Сервер Oracle обеспечивает для этого соответствующий механизм — триггеры INSTEAD OF. Можно реализовать алгоритм, который должен выполняться сервером Oracle вместо (INSTEAD OF) стандартного при изменении содержимого представления. Чтобы продемонстрировать это, давайте обеспечим возможность изменения показанного ранее представления. Сервер Oracle позволяет создать триггер INSTEAD OF по представлению DEPT_OR и по любому типу вложенной таблицы, входящей в это представление. Создав триггер
404
Глава 20
по столбцам вложенной таблицы, можно изменять столбец вложенной таблицы так, будто это обычная таблица. Соответствующий триггер может иметь следующий вид: scott@TKYTE816> c r e a t e or replace t r i g g e r EMPS_IO_UPDATE 2 instead of UPDATE on nested t a b l e emps of dept_or 3 begin 4 i f (:new.empno = :old.empno) 5 then 6 update emp 7 s e t ename = :new.ename, job = :new.job, mgr = :new.mgr, 8 h i r e d a t e = :new.hiredate, s a l = :new.sal, *"• comm = :new.comm 9 where empno = :old.empno; 10 else 11 raise_application_error(-20001,'Значение столбца empno ** изменять нельзя'); 12 end if; 13 end; 14 / Trigger created. Как видите, триггер будет срабатывать при изменении (INSTEAD OF UPDATE) столбца типа вложенной таблицы, EMPS, представления DEPT_OR. Он будет срабатывать для каждой изменяемой строки вложенной таблицы и имеет доступ к значениям до и после изменения (:OLD и :NEW), как и "обычный" триггер. В данном случае понятно, что надо делать, — изменить строку таблицы ЕМР с соответствующим значением EMPNO, задав ее столбцам новые значения. В этом триггере я принудительно запрещаю изменять первичный ключ (мы используем объектно-реляционные средства, но это не значит, что можно нарушать основные принципы проектирования реляционных баз данных). Теперь, выполним следующие операторы: scott@TKYTE816> update TABLE (select p.emps 2 from dept_or p 3 where deptno = 2 0 ) 4 set ename = lower(ename) 5 / 5 rows updated. scott@TKYTE816> select ename from emp where deptno - 20; ENAME smith jones scott adams ford scott@TKYTE816> select ename 2 from TABLE(select p.emps 3 from dept_or p 4 where deptno = 20);
Использование объектно-реляционных средств
40 J
ENAME smith jones scott adams ford Как видите, изменение вложенной таблицы успешно преобразовано в изменение реляционной таблицы, как и ожидалось. Так же легко написать триггеры на события INSERT и DELETE, поскольку UPDATE — самый сложный случай. Так что на этом и остановимся. Если сейчас выполнить следующее изменение: scott@TKYTE816> declare 2 1 emps emp tab type; 3 begin 4 select p.emps into l_emps 5 from dept_or p 6 where deptno = 10; 7 8 for i in 1 .. l_emps.count 9 loop 10 l_emps(i).ename := lower(l_emps(i).ename); 11 end loop; 12 13 update dept_or 14 set emps - l_emps 15 where deptno = 10; 16 end; declare * ERROR a t l i n e 1: ORA-01732: data manipulation operation not legal on t h i s view ORA-06512: a t l i n e 13 то окажется, что при выполнении выдается сообщение об ошибке. Странно. Не должен ли сработать созданный ранее триггер? На самом деле — нет. Только изменения вложенной таблицы, связанные с извлечением ее данных, вызовут срабатывание триггера. Триггер срабатывает, только если с вложенной таблицей работают как с обычной таблицей. Мы же не выполняем операцию над большим количеством данных вложенной таблицы, а только изменяем столбец представления DEPT_OR. Чтобы поддержать работу подобного кода (и изменений других скалярных столбцов представления DEPT_OR), необходимо создать триггер INSTEAD OF для представления DEPT_OR. Этот триггер будет обрабатывать значения :OLD.EMPS и :NEW.EMPS как множества следующим образом. 1. Удалять из таблицы ЕМР все записи, значение EMPNO которых было в наборе :OLD, но отсутствует в наборе :NEW. Для этого прекрасно подходит реляционный оператор MINUS.
406
Глава 20
2. зменять в таблице ЕМР каждую запись, значение столбца EMPNO которой входит во множество значений EMPNO, соответствующие :NEW-3anncH которых отличаются от :О1Л)-записей. Это множество легко найти с помощью оператора MINUS. 3. Вставлять в таблицу ЕМР все записи :NEW, значение :NEW.EMPNO которых не находится во множестве значений EMPNO rOLD-записи. Вот как это реализуется: scott@TKYTE816> create or replace t r i g g e r DEPT_OR_IO_UPDATE 2 instead of update on dept_or 3 begin 4 i f (:new.deptno = :old.deptno) 5 then 6 i f updating('DNAME') or updating('LOC') 7 then 8 update dept 9 set dname = :new.dname, loc = rnew.loc 10 where deptno = :new.deptno; 11 end if; 12 13 if (updating('EMPS')) 14 then 15 delete from emp 16 where empno in 17 (select empno 18 from TABLE(cast(:old.emps as emp_tab_type)) 19 MINUS 20 select empno 21 from TABLE(cast(:new.emps as emp_tab_type)) 22 ); 23 dbms_output.put_line('удалено .' || sql%rowcount); Первый оператор MINUS возвращает множество значений EMPNO, которые были в наборе :OLD, но отсутствуют в наборе :NEW. Эти записи надо удалить из таблицы ЕМР, поскольку в наборе их больше нет. Изменим те записи набора, которые были обновлены: 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
update emp E set (deptno, ename, job, mgr, hiredate, sal, coiran) = (select :new.deptno, ename, job, mgr, hiredate, sal, comm from TABLE(cast(:new.emps as emp_tab_type)) T where T.empno = E.empno ) where empno in (select empno from (select * from TABLE(cast(:new.emps as emp_tab_type)) MINUS select *
Использование объектно-реляционных средств 39 40 41 42
4U7
from TABLE(cast (: old.eitps as emp_tab_type)) ) >; dbms_output.put_line('изменено ' || sql%rowcount);
Этот оператор MINUS возвращает все значения из :NEW, кроме совпадающих со значениями из :OLD; это дает множество измененных записей. Оно используется в подзапросе для получения множества значений EMPNO, соответствующие записи для которых в таблице ЕМР надо изменить, после чего эти значения определяются с помощью коррелированного подзапроса. Наконец, добавляем новые записи: 43 44 insert xnto emp 45 (deptno, empno, ename, job, mgr, hiredate, sal, comm) 46 select :new.deptno,empno,ename,job,mgr,hiredate,sal,coiran 47 from (select * 48 from TABLE(cast(:new.emps as emp_tab_type)) 49 where empno in 50 (select empno 51 from TABLE(cast(:new.emps as ^ emp_tab_type)) 52 MINUS 53 select empno 54 from TABLE(cast(: emps as *• emp_tab_type)) 55 ) 56 ); 57 dbms_output.put_line('вставлено ' И sql%rowcount); 58 else 59 dbms_output.put_line('Обработка вложенной таблицы ** пропущена'); 60 end if; 61 else 62 raise_application_error(-20001,'значение deptno изменять *•* нельзя'); 63 end if; 64 end; 65 / Trigger created. Оператор MINUS генерирует множество значений EMPNO из набора :NEW, отсутствующих в наборе :OLD; оно представляет список строк, которые надо добавить в таблицу ЕМР. Триггер кажется огромным, но на самом деле он простой. Вначале он проверяет, не изменены ли скалярные столбцы представления DEPT_OR. Если — да, выполняются соответствующие изменения в таблице DEPT. Затем, если был изменен столбец вложенной таблицы (заменены все ее значения), эти изменения вносятся в таблицу ЕМР. Чтобы внести все необходимые изменения, надо: 1. удалить из таблицы ЕМР записи, которые были удалены из столбца вложенной таблицы EMPS;
408
Глава 20
2. изменить в таблице ЕМР те записи, значения которых были изменены в столбце вложенной таблицы EMPS; 3. вставить в таблицу ЕМР записи, добавленные в столбец вложенной таблицы EMPS. К счастью, SQL-оператор MINUS и возможность преобразовать столбец типа вложенной таблицы в реальную таблицу упрощает реализацию триггера. Теперь мы можем обрабатывать данные так: scott@TKYTE816> 2 l_emps 3 begin 4 select 5 from 6 where
declare emp_tab_type; p.emps into 1 emps dept_or p deptno = 10;
7
8 for i in 1 .. l_emps.count 9 loop 10 l_emps(i).ename := lower(l_emps(i).ename); 11 end loop; 12 13 update dept_or 14 set emps = l_emps 15 where deptno = 10; 16 end; 17 / удалено 0 изменено З вставлено О PL/SQL procedure successfully completed. scott@TKYTE816> declare 2 l_emps emp_tab_type; 3 begin 4 select p.emps into l_emps 5 _ from dept_or p 6 where deptno = 10; 7 8 9 for i in 1 .. l_emps.count 10 loop 11 if (l_emps(i).ename = 'miller') 12 then 13 l_emps.delete(i); 14 else 15 l_emps(i).ename := initcap(l_emps(i).ename); 16 end if; 17 end loop; 18 19 l_emps.extend; 20 l_emps(l_emps. count) :21 emp type(1234, 'Tom', 'Boss',
Использование объектно-реляционных средств
22 23 24 25 26
40,7
null, sysdate , 1000,, 500); update dept or set emps - 1 emps where deptno = 10;
27 end • 28 / удалено 1 изменено 2 вставлено 1 PL/SQL procedure successfully completed. scott@TKYTE816> update dept_or set dname - initcap(dname); Обработка вложенной таблицы пропущена Обработка вложенной таблицы пропущена Обработка вложенной таблицы пропущена Обработка вложенной таблицы пропущена 4 rows updated. scott@TKYTE816> commit; Commit complete.
Триггер преобразует наши действия с экземпляром объекта в соответствующие изменения базовых реляционных таблиц. Возможность работать с реляционными данными через объектно-реляционные представления позволяет максимально использовать преимущества как реляционной, так и объектно-реляционной модели. Реляционная модель сильна тем, что с ее помощью можно ответить практически на любой вопрос о базовых данных. Рассматриваются ли данные по отделам (надо запросить информацию об отделе и его сотрудниках) или по сотрудникам (надо задать номер сотрудника и получить информацию об отделе, в котором он работает), всегда есть возможность выполнить соответствующий запрос. Реляционные таблицы можно использовать непосредственно или сгенерировать модель на основе объектного типа — все необходимые данные будут объединены и представлены в удобном для использования виде. Рассмотрим результаты следующих запросов: scott0TKYTE816> s e l e c t * from dept_or where deptno = 10; DEPTNO DNAME HIREDATE, S 10 A c c o u n t i n g
LOC
NEW YORK
EMPS(EMPNO, ENAME, JOB, MGR,
EMP_TAB_TYPE(EMPJTYPE(7782, ' C l a r k ' , 'MANAGER', 7 8 3 9 , ' 0 9 - J U N - 8 1 ' , 2 4 5 0 , NULL), EMP_TYPE(7839, ' K i n g ' , 'PRESIDENT 1 , NULL, ' 1 7 - N O V - 8 1 ' , 5 0 0 0 , 1 NULL), EMP__TYPE(1234, ' T o m , ' B o s s ' , 1 NULL, ' 2 5 - M A R - 0 1 , 1 0 0 0 , 5 0 0 ) ) scott@TKYTE816> s e l e c t d e p t . * , empno, ename, j o b , mgr, h i r e d a t e , s a l , comm 2 from emp, d e p t 3 where emp.deptno = dept.deptno 4 and dept.deptno = 1 0 5 /
410
Глава 20
DEPTNO DNAME 10 Accounting 10 Accounting 10 Accounting
LOC NEW YORK NEW YORK NEW YORK
EMPNO ENAME JOB
MGR
HIREDATE
7782 Clark MANAGER 7839 09-JUN-81 7839 King PRESIDENT 17-NOV-81 1234 Tom Boss 25-MAR-01
SAL 2450 5000 1000
com
500
Они возвращают похожие данные. Первый запрос в сжатом виде выдает всю информацию об отделе в виде одной строки. Он может возвращать несколько вложенных таблиц, для чего в классическом языке SQL пришлось бы выполнить несколько запросов. На сервере можно выполнить много действий, формируя ответ и возвращая его в виде одной строки. Когда работа выполняется в среде, где лишних обменов данными по сети следует избегать (из-за большого времени ожидания), это дает существенные преимущества. Не говоря уже о том, что один простой оператор SELECT * FROM T может выполнить действия нескольких SQL-операторов. Учтите также, что в объектном представлении нет повторяющихся столбцов данных. Значения столбцов DEPTNO, DNAME и LOC не повторяются для каждого сотрудника; они возвращаются только один раз, что для многих приложений более удобно. Второй запрос требует от разработчика более глубокого знания данных, и эту особенность следует учитывать. Надо знать, как соединять данные; если же соединяется много таблиц, возможно, потребуются дополнительные запросы, результаты которых придется обрабатывать самостоятельно. Поясню. Предположим, в модели необходимо учесть бюджет отдела на финансовый год. Он хранится в следующей реляционной таблице: scott@TKYTE816> create t a b l e dept_fy_budget 2 (deptno number(2) references dept, 3 fy date, 4 5 6 7
amount number, constraint dept_fy_budget_pk primary key(deptno,fy) ) /
Table created.
В этой таблице хранятся данные о бюджете отделов за этот и несколько предыдущих финансовых лет. Для работы приложения необходимо представление данных по отделам, включающее все скалярные данные (название, местонахождение). Кроме того, необходима информация о сотрудниках (столбец типа EMP_TAB_TYPE), а также данные о бюджете за последние финансовые годы. Для получения этих данных по реляционной модели разработчику приложения придется выполнять следующие операторы: scott@TKYTE816> select dept.' empno, ename, job, mgr, hiredate, sal, comm 2 from emp, dept 3 where emp.deptno = dept.deptno 4 and dept.deptno = 1 0 5 / DEPTNO DNAME 10 Accounting 10 Accounting 10 Accounting
LOC NEW YORK NEW YORK NEW YORK
EMPNO ENAME JOB
MGR
HIREDATE
7782 Clark MANAGER 7839 09-JUN-81 7839 King PRESIDENT 17-NOV-81 1234 Tom Boss 25-MAR-01
SAL
COMM
2450 5000 1000
500
Использование объектно-реляционных средств
411
3 rows selected. scott@TKYTE816> select fy, amount 2 from dept_fy_budget 3 where deptno = 1 0 4 / FY
AMOUNT
01-JAN-99 01-JAN-00 01-JAN-01
500 750 1000
3 rows selected. Нельзя написать один реляционный запрос, выдающий все эти данные. Можно использовать ряд расширений Oracle (функцию CURSOR в SQL) для возврата строк, каждая из которых содержит результирующее множество: scott@TKYTE816> s e l e c t 2 dept.deptno, dept.dname, 3 c u r s o r ( s e l e c t empno from emp where deptno = dept.deptno), 4 c u r s o r ( s e l e c t fy, amount from dept_fy_budget where deptno = dept.deptno) 5 from dept 6 where deptno = 1 0 7 / DEPTNO DNAME
CURSOR(SELECTEMPNOFR CURSOR(SELECTFY,AMOU
10 ACCOUNTING
CURSOR STATEMENT : 3 CURSOR STATEMENT
CURSOR STATEMENT : EMPNO 7782 7839 7934 3 rows selected. CURSOR STATEMENT : 4 FY
AMOUNT
01-JAN-99 01-JAN-00 01-JAN-01
500 750 1000
3 rows selected. 1 row selected.
В данном случае была выбрана одна строка, вернувшая клиенту еще два курсора. Клиентское приложение выбрало данные из этих двух курсоров и выдало результаты. Это прекрасно работает, но требует знания особенностей базовых данных и способов •
412
Глава 20
их объединения (как написать коррелированные подзапросы для генерации курсоров). Эту модель можно реализовать с помощь объектно-реляционных расширений, пересоздав представление следующим образом: scott@TKYTE816> c r e a t e or replace type dept_budget_type 2 as object 3 (fy date, 4 amount number 5 ) 6 / Type created. scott@TKYTE816> create or replace type dept_budget_tab_type 2 as table of dept_budget_type 3 / Type created. scott@TKYTE816> create or replace type dept_type 2 as object 3 (deptno number(2), 4 dname varchar2(14), 5 loc varchar2(13), 6 emps emp_tab_type, 7 budget dept_budget_tab_type 8 ) 9 Type created. scott@TKYTE816> create or replace view dept_or 2 of dept_type 3 with object identifier(deptno) 4 as 5 select deptno, dname, loc, 6 cast (multiset ( select empno, ename, job, mgr, hiredate, sal, comm 8 from emp 9 where emp.deptno = dept.deptno) 10 as emp_tab_type) emps, 11 cast (multiset ( 12 select fy, amount 13 from dept_fy_budget 14 where dept_fy_budget.deptno = dept.deptno) 15 as dept_budget_tab_type) budget 16 from dept 17 / View created. Теперь учтите, что представленные выше действия выполняются один раз, и все сложности от приложения скрыты. В приложении можно просто написать: scott@TKYTE816> s e l e c t * from dept_or where deptno = 10 2 /
Использование объектно-реляционных средств DEPTNO DNAME 10 A c c o u n t i n g
LOC
413
EMPS(EMPNO, ENAME, J BUDGET(FY, AMOUNT)
NEW YORK EMP_TAB_TYPE(EMP_TYP DEPT_BUDGET_TAB_TYPE
E(7782, ' C l a r k ' , 'MANAGER', 7839, 1 09-JUN-81', 2450, NULL), EMP_TYPE(7839, ' K i n g 1 , 'PRESIDENT', NULL, 47-NOV-81', 5000, NULL), EMP_TYPE(1234, 'Tom', ' B o s s ' , NULL, '25-MAR-01', 1000, 500))
(DEPT_BUDGET_TYPE('0 l-JAN-99', 5 0 0 ) , DEPT_BUDGET_TYPE('01 -JAN-00', 7 5 0 ) , DEPT_BUDGET_TYPE('01 -JAN-01', 1000))
1 row s e l e c t e d . Снова мы получаем одну строку, один экземпляр объекта, представляющего данные в нужном виде. Это весьма удобно. Сложность базовой физической модели скрыта, и легко понять, как создать графический интерфейс для представления этих данных пользователю. При программировании на языке Java (с помощью интерфейса JDBC), Visual Basic (с помощью объектов 0 0 4 0 — Oracle Objects for Ole), PL/SQL, использовании библиотеки OCI (Oracle Call Interface) и прекомпилятора Рго*С объектно-реляционные расширения легко применить. Использовать реляционную модель становится все труд•нее по мере добавления сложных отношений один ко многим. При использовании объектно-реляционной модели все несколько проще. Придется, конечно, изменить триггеры INSTEAD OF для поддержки изменения базовых реляционных данных, так что приведенный пример неполон, но идею вы, надеюсь, уловили.
Резюме В этой главе мы изучили основные способы использования объектных типов и расширений сервера Oracle. Из четырех возможных способов мы детально рассмотрели три. Мы рассмотрели использование объектных типов для расширения стандартного набора типов системы. С помощью нового типа ADDRESS_TYPE мы смогли не только задать общую систему именования и использования адресов, но и обеспечить специализированные методы и средства работы с ними. Мы также рассмотрели использование объектно-реляционных расширений для естественного развития возможностей языка PL/SQL. Мы взяли стандартный пакет и реализовали для него интерфейс на уровне объектного типа. Это позволяет защититься от изменений в реализации стандартного пакета, а также обеспечивает более "объектноориентированный" стиль программирования на PL/SQL, подобный использованию классов в языках C++ или Java. Кроме того, было показано, как, используя наборы, можно выбирать данные из PL/SQL-функции с помощью оператора SELECT. Одной этой возможности достаточно, чтобы оправдать использование объектно-реляционных расширений.
414
Глава 20
Наконец, мы изучили, как использовать эти средства для создания объектно-реляционных представлений реляционных, по сути, данных. Как оказалось, можно легко создать специализированные объектные представления реляционных данных для любого количеств различных приложений. Основное преимущество этого подхода состоит в том, что в приложении можно применять практически тривиальные SQL-операторы. Не нужно выполнять соединения и делать несколько запросов, чтобы получить все необходимые данные. Все необходимое возвращается при выборке всего одной строки. Четвертый способ использования, создание объектных таблиц по хранимому типу данных, был рассмотрен в главе 6, посвященной таблицам. Поскольку объектные таблицы по "внешним" свойствам аналогичны объектным представлениям (или скорее, наоборот — объектные представления аналогичны объектным таблицам), их использование тоже, фактически, рассмотрено. Я предпочитаю не использовать объектные таблицы. По многим описанным ранее причинам, мне больше нравятся объектные представления реляционных таблиц. Основная причина в том, что, в конечном итоге, почти всегда необходимо поддерживать реляционное представление данных, как обеспечивающее различные способы использования данных в приложениях. Объектно-реляционные представления прекрасно подходят для моделирования специфических представлений данных в приложениях.
adeProgra onalOra
•
Детальный контроль доступа •
Детальный контроль доступа (Fine Grained Access Control — FGAC) в Oracle 8i позволяет во время выполнения динамически добавлять условие (конструкцию WHERE) ко всем запросам, обращенным к таблице или представлению базы данных. Теперь можно процедурно изменять запрос во время выполнения, другими словами, динамически создавать представление. Можно проверить, кто выполнял запрос, с какого терминала и когда (например, по времени суток) он выполнялся, а затем создать условие на основе этой информации. С помощью контекстов приложений можно безопасно добавлять в среду информацию (например, роль пользователя в отношении приложения) и обращаться к этой информации в процедуре или условии. В различных публикациях средства FGAC описываются под различными названиями. Обычно используются следующие: • детальный контроль доступа (Fine Grained Access Control); •
виртуальная приватная база данных (Virtual Private Database — VPD);
Q защита на уровне строк (Row Level Security), или пакет DBMS_RLS (этот PL/SQLпакет реализует соответствующие возможности). Чтобы выполнять представленные в этой главе примеры, необходим сервер Oracle версии 8.1.5 или выше. Кроме того, средства детального контроля доступа доступны только в редакциях Enterprise и Personal Edition; в Standard Edition эти примеры работать не будут. В этой главе мы рассмотрим следующее. Q Преимущества использования средств детального контроля доступа — простота сопровождения, реализация этих средств на сервере, возможность развития приложений и упрощение их разработки и т.д. 14 Зак. 244
418
Глава 21
• Два примера в разделе "Как реализованы средства детального контроля доступа", демонстрирующие применение правил защиты (security policies) и контекстов приложений. •
Проблемы и нюансы, которые необходимо учитывать, в частности особенности функционирования средств детального контроля доступа при наличии ограничений целостности ссылок, кеширование курсоров, особенности экспортирования и импортирования данных и тонкости отладки.
а
Ошибки, с которыми можно столкнуться при реализации детального контроля доступа в приложениях.
Пример Предположим, существуют правила защиты, определяющие, какие строки могут просматривать различные группы пользователей. Правила защиты позволяют разработать условие проверки, учитывающее, кто зарегистрирован и какую роль он имеет в системе. Средства детального контроля доступа позволяют переписать простой запрос SELECT * FROM EMP следующим образом: Пользователь зарегистрирован как.
Запрос переписывается так...
Комментарии
Сотрудник
select * from (select * from emp where ename = USER)
Рядовые сотрудники могут просматривать только собственные записи.
Руководитель подразделения
select * from (select * from emp where mgr = (select empno from emp
Руководители подразделений могут просматривать свои записи и записи сотрудников своего подразделения.
where ename = USER) or ename = USER Сотрудники отдела кадров.
select * Сотрудники отдела кадров from (select * могут видеть все записи в данном подразделении. В from emp этом примере представлен where deptno = SYS_CONTEXT('OurApp', 'ptno') способ получения значений ) переменных из контекста приложения с помощью встроенной функции SYS_CONTEXT().
Детальный контроль доступа
4 1 У
Когда использовать это средство? В этом разделе рассмотрены причины и варианты использования средств детального контроля доступа.
Простота сопровождения Средства детального контроля доступа позволяют с помощью одной таблицы и одной хранимой процедуры справиться с задачей, для решения которой могло бы понадобиться несколько представлений или триггеров, или большой объем специализированной обработки в приложениях. Подход с использованием нескольких представлений достаточно типичен. Разработчики приложений создают несколько учетных записей в базе данных, например EMPLOYEE, MANAGER, HR_REP, и устанавливают в каждой из соответствующих схем полный набор представлений, выбирающих только необходимые данные. Для рассматриваемого примера в каждой схеме создается отдельное представление ЕМР со специализированным условием выбора данных для соответствующей группы пользователей. Для управления тем, что конечные пользователи могут просматривать, создавать, изменять и удалять, придется создавать до четырех различных представлений для таблицы ЕМР — для операторов SELECT, INSERT, UPDATE и DELETE. Это быстро приводит к запредельному увеличению количества объектов базы данных — каждый раз при добавлении новой группы пользователей придется создавать и поддерживать новый набор представлений. Если правила защиты изменятся (например, если необходимо разрешить руководителям просматривать записи не только непосредственных подчиненных, но и подчиненных следующего уровня), придется пересоздать представление в базе данных, делая недействительными все объекты, которые на него ссылаются. Такой подход приводит не только к увеличению количества представлений в базе данных, но и требует, чтобы пользователи регистрировались от имени нескольких совместно используемых учетных записей, что осложняет контроль работы пользователей. Кроме того, этот подход требует дублирования значительного объема кода в базе данных. При наличии хранимой процедуры, работающей с таблицей ЕМР, придется устанавливать ее в каждой из задействованных схем. Это относится и ко многим другим объектам (триггерам, функциям, пакетам и т.д.). Теперь при внесении изменений в программное обеспечение придется каждый раз изменять N схем, чтобы все пользователи выполняли один и тот же код. Еще один подход связан с использованием, кроме представлений, триггеров базы данных. Вместо создания отдельных представлений для операторов SELECT, INSERT, UPDATE и DELETE, для построчного просмотра выполняемых пользователем изменений используется триггер, принимающий или отвергающий эти изменения. Эта реализация не только приводит к созданию большого количества дополнительных представлений, но и влечет расходы ресурсов на поддержку срабатывания триггера (иногда весьма сложного) для каждой изменяемой строки. Наконец, можно всю защиту реализовать в приложении, будь то клиентское приложение в случае архитектуры клиент-сервер или сервер приложений. Приложение будет учи-
420
Глава 21
тывать, кто к нему обращается, и выполнять соответствующий запрос. Приложение по сути реализует собственный механизм детального контроля доступа. Серьезным недостатком такого подхода (и вообще любого подхода, использующего для доступа к данным специфические средства приложения) является то, что данные в базе могут использоваться только соответствующим приложением. Нельзя использовать никакие средства создания запросов, средства генерации отчетов и т.п., поскольку данные не защищены, если доступ к ним идет не через приложение. Когда защита встроена в приложение, усложняется развитие приложения и добавление новых интерфейсов, т.е. снижается полезность данных. Средства детального контроля доступа позволяют справиться с этими трудностями и избежать потери функциональных возможностей с помощью всего двух объектов — исходной таблицы (или представления) и пакета (или функции) базы данных. Пакет можно изменить в любой момент, разработав новые правила защиты. Вместо поддержки десятков представлений, реализующих правила защиты для объекта, всю соответствующую информацию можно задавать в одном месте.
Контроль доступа выполняется на сервере С учетом сложности создания и поддержки большого количества представлений разработчики часто реализуют алгоритмы защиты в самом приложении, как было описано выше. Приложение анализирует, кто зарегистрировался и что он запрашивает, а затем отправляет на сервер соответствующий запрос. Это позволяет защитить данные только при доступе к ним через приложение, а вероятность неавторизованного доступа к данным увеличивается, поскольку для этого достаточно подключиться к базе данных с помощью любого инструментального средства, кроме приложения, и выполнить запрос. При детальном контроле доступа алгоритмы защиты, определяющие, какие данные может "видеть" пользователь, помещаются в базу данных. При этом гарантируется защита данных, независимо от используемого средства доступа к ним. Потребность в таких средствах вполне объяснима. В начале и середине 1990-х годов преимущественно использовалась модель клиент-сервер (а еще раньше нормой считалось централизованное выполнение приложений). Большинство клиент-серверных приложений (и практически все централизованные) включали алгоритмы, проверяющие уровень доступа к приложению. Сегодня очень модно использовать серверы приложений и размещать на них все прикладные алгоритмы. По мере переноса клиент-серверных приложений на новую архитектуру разработчики начали переносить алгоритмы защиты с клиентской части и встраивать их в серверы приложений. Это привело к двойной реализации алгоритмов защиты (некоторые клиент-серверные приложения продолжают использоваться), так что теперь поддерживать и отлаживать эти алгоритмы надо в двух местах. Ситуация станет еще хуже при появлении следующей парадигмы программирования. Что случится, когда серверы приложений выйдут из моды? Что делать, если пользователям необходимо инструментальное средство сторонних производителей, обращающееся к данным непосредственно? Если вся защита сосредоточена на промежуточном сервере приложений, это становится невозможным. Если же защита реализуется сервером баз данных, вы готовы к применению любой технологии, как существующей, так и еще не придуманной.
Детальный контроль доступа
4 2 1
Упрощение разработки приложений Средства детального контроля доступа позволяют отделить алгоритмы защиты от других алгоритмов работы приложения. Разработчик приложения может заняться прикладными алгоритмами, а не алгоритмами безопасного доступа к данным. Поскольку детальный контроль доступа выполняется полностью на сервере баз данных, эти алгоритмы немедленно наследуются всеми приложениями. Раньше разработчикам приходилось включать алгоритмы защиты в приложения, что усложняло разработку и дальнейшее сопровождение приложений. Если приложения должны обеспечивать защиту доступа к данным, а доступ к одним и тем же данным выполняется во многих компонентах приложения, изменение правил защиты повлияет на десятки модулей приложения. При использовании средств детального контроля доступа все соответствующие модули автоматически наследуют новые правила доступа без каких-либо изменений в коде.
Эволюционная разработка приложений Во многих средах правила защиты изначально строго не сформулированы и со временем могут меняться. При слиянии компаний или ужесточении правил доступа к данным, или появлении новых законов о невмешательстве в частную жизнь правила защиты придется изменять. Если средства контроля доступа реализованы как можно ближе к данным, эти изменения легко учесть при минимальном изменении приложений и инструментальных средств. Новые алгоритмы защиты реализуются в одном месте, а все приложения и средства доступа к базе данных автоматически наследуют новые алгоритмы.
Отказ от совместно используемых учетных записей При использовании средств детального контроля доступа каждый пользователь может регистрироваться от имени уникальной учетной записи. Это позволяет выполнять полный учет и проверку действий на уровне пользователей. В прошлом многие разработчики приложений, столкнувшись с необходимостью обеспечивать разное представление данных для различных пользователей, создавали совместно используемые учетные записи. Например, все сотрудники для доступа к кадровой информации использовали учетную запись EMPLOYEE; все руководители — учетную запись MANAGER и т.д. Это не позволяло контролировать действия на уровне отдельных пользователей. Невозможно было понять, регистрировался ли пользователь ТКУТЕ — в системе работало несколько сеансов от имени учетной записи EMPLOYEE (кто бы из сотрудников ни подключался). При желании можно использовать средства детального контроля доступа вместе с такими совместно используемыми учетными записями. Однако эти средства позволяют избежать необходимости создания и использования таких учетных записей.
Поддержка совместно используемых учетных записей Это дополнение к предыдущему разделу. Средства детального контроля доступа не требуют обязательного использования для каждого пользователя отдельной учетной за-
422
Глава 21
писи; они только позволяют это сделать. С помощью контекста приложения можно использовать средства детального контроля доступа и в "однопользовательской" среде, например, используемой при организации пула подключений сервера приложений. Некоторые пулы подключений требуют при регистрации использования одной учетной записи базы данных. Средства детального контроля доступа прекрасно работают и в таких средах.
Предоставление доступа к приложению как к службе Средства детального контроля доступа позволяют поставщику прикладных служб (Application Service Provider — ASP), не изменяя существующее приложение, предоставить к нему как к службе доступ множеству клиентов. Предположим, имеется приложение по учету кадров, доступ к средствам которого хотелось бы за деньги предоставлять клиентам по сети Internet. Поскольку клиентов предполагается много и все они требуют гарантий конфиденциальности данных, необходимо придумать способ защиты данных одного клиента от доступа других. Возможны следующие варианты: • установить, сконфигурировать и поддерживать отдельные экземпляры базы данных для каждого клиента; •
переписать все используемые приложением хранимые процедуры так, чтобы они работали с правами вызывающего (это будет описано в главе 23), и создать для каждого клиента отдельную схему;
Q использовать один экземпляр базы данных и одну схему со средствами детального контроля доступа. Первый вариант крайне нежелателен. Неизбежные расходы ресурсов на поддержку отдельного экземпляра базы данных для каждого клиента, имеющего всего десяток сотрудников, не позволяют реально его использовать. Для крупных клиентов с сотнями или тысячами пользователей он вполне оправдан. Для массы же мелких клиентов, каждый из которых добавляет пять-шесть конечных пользователей, создавать отдельную базу данных слишком расточительно. Второй вариант потенциально требует переписать приложение. Целью является создание для каждого клиента отдельной схемы со своим набором таблиц. Любую хранимую процедуру надо создавать так, чтобы она работала с таблицами, доступными только для текущего зарегистрированного пользователя (клиента). Обычно хранимым процедурам доступны те же объекты, что и создателю процедуры — надо только убедиться, что используются подпрограммы, работающие с правами вызывающего и что в приложении нигде явно не указаны схемы. Например, нельзя использовать оператор SELECT * FROM SCOTT.EMP - только SELECT * FROM EMP. Это касается не только PL/SQL-процедур. Для любого внешнего кода на языках Java или Visual Basic тоже должны соблюдаться эти правила (и не использоваться имена схем). Поэтому второй вариант также нежелателен, да еще и связан с необходимостью поддержки многих сотен пользовательских схем.
Детальный контроль доступа
423
Третий вариант — использование средств детального контроля доступа — наиболее безболезненный и простой. Можно, например, добавить в каждую таблицу, требующую защиты, столбец с идентификатором организации. Для поддержки значений в этом столбце надо использовать триггер (чтобы не пришлось изменять приложение). Триггер будет брать соответствующее значение из контекста приложения, устанавливаемого в системном триггере ON LOGON. Правила защиты будут задавать условие выбора строк только соответствующей организации. При этом можно ограничивать доступ к данным не только по идентификатору клиента, но и по любым другим необходимым условиям. В нашей системе кадрового учета добавлять можно не только условие WHERE COMPANY = ЗНАЧЕНИЕ, но и дополнительные условия, в зависимости от того, работает ли с системой рядовой сотрудник, руководитель или сотрудник одела кадров. Можно пойти еще дальше и добавить условия секционирования, чтобы физически отделить данные крупных клиентов с целью обеспечения надежности хранения и высокой доступности.
Как реализованы средства детального контроля доступа Средства детального контроля доступа в Oracle 8i реализуются с помощью двух конструкций. Q Контекст приложения. Это пространство имен с соответствующим набором пар атрибут/значение. Например, в контексте OurApp можно обращаться к переменным DeptNo, Mgr и т.д. Контекст приложения всегда привязан к PL/SQL-пакету. Этот пакет — единственный метод установки значений в контексте. Чтобы установить значение атрибута DeptNo в нашем контексте OurApp, необходимо обратиться к соответствующему пакету, связанному с контекстом OurApp. Этот пакет может корректно устанавливать значения в контексте OurApp (вы сами его написали, поэтому и считается, что он будет правильно устанавливать значения в контексте). Это предотвращает установку значений в контексте приложений злонамеренными пользователями с целью получения несанкционированного доступа к информации. Читать значения в контексте приложения может кто угодно, но устанавливать их может только один пакет. •
Правила защиты. Это созданная разработчиком функция, возвращающая условие для динамического фильтрования данных при выполнении запроса. Эту функцию можно привязывать к определенной таблице или представлению, и вызываться она может для всех или только для некоторых операторов, обращающихся к таблице. Это означает, что можно задать одни правила для оператора SELECT, другие — для INSERT и третьи — для операторов UPDATE и DELETE. Обычно в этой функции используются значения атрибутов в контексте приложений для создания соответствующего условия (например, проверяется, кто зарегистрировался и что он пытается сделать и создается соответствующее подмножество строк для работы). Следует помнить, что для пользователя SYS (или INTERNAL) правила защиты никогда не применяются (соответствующие функции просто никогда не вызываются); эти пользователи всегда могут читать и изменять все данные.
424
Глава 21
Также следует упомянуть средства сервера Oracle 8i, расширяющие возможности средств детального контроля доступа. •
Функция SYS_CONTEXT. Эта функция используется в языках SQL и PL/SQL для доступа к значениям в контексте приложения. Подробное описание этой функции и список стандартных значений в автоматически устанавливаемом сервером Oracle контексте USERENV можно найти в руководстве Oracle SQL Reference. Из этого контекста можно получить имя пользователя, организовавшего сеанс, IPадрес клиента и другую полезную информацию.
Q Триггеры на событие регистрации в базе данных. Они позволяют выполнять любой код при регистрации пользователя в базе данных. Это очень удобно для настройки первоначального, стандартного контекста приложения. а
Пакет DBMS_RLS. Этот пакет обеспечивает функциональный интерфейс для добавления, удаления, изменения, включения и отключения правил защиты. Соответствующие подпрограммы можно вызвать из любого языка программирования или среды, из которых можно подключиться к СУБД Oracle.
Чтобы использовать средства детального контроля доступа, разработчику, помимо стандартных ролей CONNECT и RESOURCE (или соответствующих им привилегий), необходимы следующие привилегии. Q EXECUTE_CATALOG_ROLE. Эта роль позволяет разработчику выполнять подпрограммы пакета DBMS_RLS. Достаточно также, подключившись как SYS, предоставить пользователю привилегию на выполнение пакета DBMS_RLS. Q CREATE ANY CONTEXT. Эта привилегия позволяет разработчику создавать контексты приложений. Контекст приложения создается с помощью SQL-оператора: SQL> c r e a t e or replace context OurApp using Our_Context Pkg; Здесь OurApp — имя контекста, a Our_Context_Pkg — PL/SQL-пакет, которому разрешается устанавливать значения атрибутов контекста. При реализации средств детального контроля доступа контексты приложений существенны по двум причинам. •
Они обеспечивают надежный способ установки значений переменных в пространстве имен. Устанавливать значения в контексте можно только с помощью PL/SQLпакета, связанного с этим контекстом. Это гарантирует целостность значений в контексте. Поскольку контекст используется для ограничения или обеспечения доступа к данным, необходимо гарантировать целостность значений в контексте.
• Ссылки на значения атрибутов контекста в SQL-запросе обрабатываются как связываемые переменные. Например, если установить значение атрибута DeptNo в контексте OurApp и реализовать правило защиты, возвращающее конструкцию WHERE deptno = SYS_CONTEXT('OurApp',1DeptNo1), соответствующий оператор не будет в разделяемом пуле уникальным, поскольку обращение к функции SYS_CONTEXT аналогично deptno = :Ы. В сеансах можно будет задавать различные значения атрибута Deptno, но все они будут повторно использовать одни и те же оптимизированные планы запросов.
Детальный контроль доступа
425
Пример 1: Реализация правил защиты Чтобы продемонстрировать возможности средств детального контроля доступа, будут реализованы очень простые правила защиты. Зададим следующие правила. Q Если текущий пользователь является владельцем таблицы (OWNER), он может обращаться ко всем ее строкам. •
В противном случае пользователь может обращаться только к строкам, у которых значение в столбце OWNER совпадает с его именем.
•
Кроме того, добавлять можно только строки, в столбце OWNER которых указано имя обращающегося пользователя. Попытки добавить строку с другим значением в этом столбце отвергаются.
Для реализации этих правил необходимо создать следующую PL/SQL-функцию: tkyte@TKYTE816> c r e a t e or replace 2 function security_policy_function(p_schema 3 p_object i n varchar2) 4 r e t u r n varchar2 5 as 6 begin 7 if (user = p_schema) then 8 return ' ' ; 9 else 10 return 'owner = USER'; 11 end if; 12 end; 13 /
i n varchar2,
Function created. Этот пример показывает общую структуру функции, реализующей правила защиты. Эта функция всегда возвращает значение типа VARCHAR2. Возвращаемое значение представляет собой условие, которое будет добавлено к запросу. Фактически это условие будет добавляться к таблице или представлению, к которым применяется это правило защиты, с помощью вложенного представления (inline view): Запрос:
SELECT * FROM T
Будет переписан к а к : SELECT * FROM (SELECT * FROM T WHERE owner - USER) или: SELECT * FROM (SELECT * FROM T) Кроме того, все функции, реализующие правила защиты, должны принимать два параметра в режиме IN: имя схемы, которой принадлежит объект, и имя объекта, к которому применяется функция. Их значения можно при необходимости использовать в функции, реализующей правила защиты. Итак, в нашем примере условие owner = USER будет динамически добавляться ко всем запросам к таблице, с которой связана эта функция, эффективно ограничивая множество строк, доступных пользователю. Пустое условие будет возвращаться только в том случае, если текущий зарегистрированный пользователь является владельцем таблицы. Вернуть пустое условие — то же самое, что вернуть условие 1=1 или True. Воз-
426
Глава 21
врат значения Null равносилен возвращению пустого условия. В представленном выше примере вместо пустой строки можно было с тем же результатом возвращать Null. Чтобы связать функцию с таблицей, используется рассматриваемая далее PL/SQLпроцедура DBMS_RLS.ADD_POLICY. В нашем примере имеется следующая таблица, а сеанс выполняется от имени пользователя TKYTE: tkyte@TKYTE816> c r e a t e t a b l e data_table 2 (some_data varchar2(60), 3 OWNER varchar2(30) default USER 4 ) 5
/
Table created. tkyte@TKYTE816> grant all on data_table to publicGrant succeeded. tkyte@TKYTE816> create public synonym data_table for data_table; Synonym created. tkyte@TKYTE816> insert into data_table (some_data) values ('Некие данные'); 1 row created. tkyte@TKYTE816> insert into data_table (some_data, owner) 2
values ('Некие данные, принадлежащие пользователю SCOTT', 'SCOTT');
1 row created. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> select * from data_table; SOME_DATA
OWNER
Некие данные Данные, принадлежащие пользователю SCOTT
TKYTE SCOTT
Теперь привяжем написанную ранее функцию защиты к этой таблице с помощью следующего обращения к пакету D B M S _ R L S : tkyte@TKYTE816> begin 2 dbms_rls.add_policy 3 (object_schema => 4 object_name => 5 policy_name => 6 function_schema => 7 policy_function => 8 statement_types => 9 update_check => 10 enable => И ); 12 end; 13 /
'TKYTE', 'data_table', 'MY_POLICY', 'TKYTE', 'security_policy_function', 'select, insert, update, delete', TRUE, TRUE
PL/SQL procedure successfully completed.
Детальный контроль доступа
4 2 7
Процедура ADD_POLICY — одна из ключевых процедур пакета DBMS_RLS. Именно она позволяет добавить правило защиты для таблицы. Мы передали процедуре следующие параметры. •
OBJECT_SCHEMA. Имя владельца таблицы или представления. Если оставить стандартное значение Null, оно будет интерпретироваться как имя текущего зарегистрированного пользователя. Для полноты рассмотренного выше примера я передал имя пользователя.
•
OBJECT_NAME. Имя таблицы или представления, для которого добавляется правило.
•
POLICY_NAME. Уникальное имя для этого правила. Это имя используется в дальнейшем для включения, отключения, изменения или удаления правила.
•
FUNCTION_SCHEMA. Имя владельца функции, возвращающей условие. Оно обрабатывается аналогично параметру OBJECT_SCHEMA. Если оставлено стандартное значение Null, используется имя текущего зарегистрированного пользователя.
•
POLICYJFUNCTION. Имя функции, возвращающей условие.
•
Q STATEMENT_TYPES. Список типов операторов, к которым применяется правило. Может представлять собой любое сочетание INSERT, UPDATE, SELECT и DELETE, перечисленных через запятую. Стандартное значение — все четыре оператора. Для наглядности я задал список явно. •
UPDATE_CHECK. Этот параметр влияет только на обработку операторов INSERT и UPDATE. Если параметр имеет значение True (стандартное значение — False), будет выполняться проверка, доступны ли вставленные или измененные данные текущему пользователю в соответствии с заданным условием. Другими словами, если задано значение True, нельзя вставить данные, которые нельзя будет выбрать из таблицы в соответствии с возвращаемым функцией условием.
•
ENABLE. Задает, включено это правило или нет. Стандартное значение — True.
Теперь, после вызова процедуры ADD_POLICY, ко всем операторам DML, применяемым к таблице DATA_TABLE, будет добавляться условие, возвращаемое функцией SECURITY_POLICY_FUNCTION, независимо от того, из какой среды поступил оператор DML. Другими словами, независимо от приложения, обращающегося к данным. Чтобы увидеть это в действии, выполним: tkyte@TKYTE816> connect system/manager system@TKYTE816> s e l e c t * from data_table; no rows s e l e c t e d system@TKYTE816> connect s c o t t / t i g e r scott@TKYTE816> s e l e c t * from data_table; SOME_DATA
OWNER
Данные, принадлежащие пользователю SCOTT
SCOTT
428
Глава 21
Итак, этот пример показывает, что строки фильтруются — пользователь SYSTEM не получает из этой таблицы никаких данных. Причина в том, что условие WHERE OWNER = USER не выполняется ни для одной из существующих строк данных. При регистрации от имени пользователя SCOTT, однако, можно получить единственную строку, принадлежащую пользователю SCOTT. Продолжим пример и попытаемся применить к таблице ряд операторов DML: sys@TKYTE816> connect s c o t t / t i g e r scott@TKYTE816> i n s e r t i n t o data_table (some_data) 2 values ('Новые данные'); 1 row created. scott@TKYTE816> insert into data_table (some_data, owner) 2 values ('Новые данные, принадлежащие пользователю SYS', 'SYS') 3 / insert into data_table ( some_data, owner ) * ERROR at line 1: ORA-28115: policy with check option violation scott@TKYTE816> s e l e c t * from d a t a _ t a b l e ; SOME_DATA
OWNER
Данные, принадлежащие п о л ь з о в а т е л ю SCOTT Новые д а н н ы е
SCOTT SCOTT
Можно создавать данные, которые будут доступны, но если они недоступны, возвращается сообщение об ошибке ORA-28115, поскольку при добавлении правила в вызове процедуры dbms_rls.add_policy было передано значение: 9 •• •
update_check
=> TRUE);
Это аналогично созданию представления с конструкцией CHECK OPTION. Разрешается создавать только те данные, которые можно потом выбрать. По умолчанию можно создавать данные, не выбираемые в соответствии с правилами защиты. Теперь, в соответствии с реализованным правилом защиты, владелец таблицы видит все строки и имеет возможность создавать любую строку. Чтобы убедиться в этом, регистрируемся как пользователь TKYTE и пытаемся выполнить следующие действия: scott@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> i n s e r t i n t o data_table (some_data, owner) ' 2 values ('Новые данные, принадлежащие пользователю SYS', 'SYS') 3 / 1 row c r e a t e d . tkyte@TKYTE816> select * from data_table 2 /
Детальный контроль доступа SOME_DATA
4 2 9
OWNER
Некие данные TKYTE Данные, принадлежащие пользователю SCOTT SCOTT Новые данные SCOTT Новые данные, принадлежащие пользователю SYS SYS Итак, пример показывает, что на пользователя TKYTE правило защиты не распространяется. Интересно отметить, что в случае регистрации от имени пользователя SYS наблюдается следующая особенность: tkyte@TKYTE816> c o n n e c t Connected.
sys/change_on_install
sys@TKYTE816> s e l e c t * from d a t a _ t a b l e ; SOME_DATA
OWNER
Некие данные TKYTE Данные, принадлежащие пользователю SCOTT SCOTT Новые данные SCOTT Новые данные, принадлежащие пользователю SYS SYS Правила защиты не применяются для специального пользователя SYS (а также при регистрации от имени INTERNAL или как SYSDBA). Это сделано специально. Учетные записи с привилегиями SYSDBA предназначены для решения задач администрирования и соответствующим пользователям доступны все данные. Это особенно важно учитывать при экспортировании данных. Если только экспортирование не выполняется с привилегиями SYSDBA, применяются правила защиты. При использовании учетной записи без привилегий SYSDBA и обычном экспорте вы получите не все данные!
Пример 2: Использование контекстов приложений В этом примере мы реализуем правила защиты информации о сотрудниках (для отдела кадров крупной компании). Будем использовать простые таблицы ЕМР и DEPT, принадлежащие пользователю SCOTT, и добавим таблицу с информацией о сотрудниках, которые отвечают за кадровые вопросы в том или ином отделе. При этом необходимо реализовать следующие правила защиты. Руководитель отдела может: •
просматривать свою собственную запись, а также записи для всех своих подчиненных;
Q изменять записи своих непосредственных подчиненных. Рядовой сотрудник может: Q просматривать свою собственную запись. Ответственный за кадровые вопросы в отделе может: •
читать все записи для отдела (предполагается, что ответственный за кадровые вопросы в определенный момент времени работает только в одном отделе);
430 •
Глава 21 изменять все записи для отдела;
Q вставлять записи для соответствующего отдела; Q удалять записи для соответствующего отдела. Как было сказано, приложение будет использовать копии существующих таблиц ЕМР и DEPT из схемы пользователя SCOTT и дополнительную таблицу, HR_REPS, позволяющую назначить ответственного за кадровые вопросы в отделе. При регистрации желательно автоматически назначать пользователю соответствующую роль в приложении. Так что, например, при регистрации ответственного за кадровые вопросы он сразу бы получал соответствующую роль в приложении. Для начала необходимо создать в базе данных ряд учетных записей. Эти учетные записи создаются для владельца приложения и пользователей. В данном случае владелец приложения — пользователь TKYTE, и соответствующая схема будет содержать копии таблиц ЕМР и DEPT из демонстрационной схемы SCOTT. Пользователи именуются так же, как сотрудники в таблице ЕМР (KING, BLAKE и т.д.). Для создания и настройки всех этих учетных записей использовался следующий сценарий. Сначала удаляем и пересоздаем пользователя TKYTE, предоставляя ему роли CONNECT и RESOURCE: sys@TKYTE816> drop user tkyte cascade; User dropped. sys@TKYTE816> c r e a t e user tkyte i d e n t i f i e d by tkyte 2 default tablespace data 3
temporary tablespace temp;
User created. sys@TKYTE816> grant connect, resource to tkyte; Grant succeeded. Теперь предоставим пользователю минимальные привилегии, необходимые для организации детального контроля доступа. Вместо привилегии EXECUTE ON DBMS_RLS можно предоставить роль EXECUTE_CATALOG: sys@TKYTE816> grant execute on dbms_rls t o t k y t e ; Grant succeeded. sys@TKYTE816> grant c r e a t e any context t o t k y t e ; Grant succeeded. Следующая привилегия необходима для создания триггера на событие регистрации в базе данных, который надо будет создать в дальнейшем: sys@TKYTE816> grant administer database t r i g g e r t o t k y t e ; Grant succeeded. Теперь создадим учетные записи сотрудников и руководителей для пользователей приложения. Для каждого сотрудника в таблице ЕМР (кроме SCOTT) будет создана учетная запись, имя которой совпадает со значением в столбце ENAME. Во многих базах данных учетная запись SCOTT уже существует:
Детальный контроль доступа
43 1
sys@TKYTE816> begin 2 for x in (select ename 3 from scott.emp where ename <> 'SCOTT') 4 loop 5 execute immediate 'grant connect to ' || x.ename || 6 ' identified by ' || x.ename; 7 end loop; 8 end; 9
/
PL/SQL procedure successfully completed. sys@TKYTE816> connect scott/tiger scott@TKYTE816> grant select on emp to tkyte; Grant succeeded. scott@TKYTE816> grant select on dept to tkyte; Grant succeeded.
Приложением используется следующая простая схема. Сначала копируются таблицы Е М Р и D E P T из схемы пользователя SCOTT. Для этих таблиц м ы также добавим декларативные ограничения целостности ссылок: scott@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> create table dept as select * from scott.dept; Table created. tkyte@TKYTE816> alter table dept add constraint dept_pk primary key(deptno); Table altered. tkyte@TKYTE816> create table emp_base_table as select * from scott.emp; Table created. tkyte@TKYTE816> alter table emp_base_table add constraint 2 empjpk primary key(empno); Table altered. tkyte@TKYTE816> alter table emp_base_table add constraint emp_fk_to_dept 2 foreign key (deptno) references dept(deptno); Table altered. Теперь добавим несколько индексов и ограничений. Создадим индексы, которые будут использоваться функциями контекста приложения для повышения их производительности. Мы должны иметь возможность быстро определять, является ли данный пользователь руководителем отдела: tkyte@TKYTE816> c r e a t e index emp_mgr_deptno_idx on emp_base_table(mgr); Index created. Необходимо также быстро получать по имени пользователя значение EMPNO и обеспечить уникальность имен пользователей в приложении:
432
Глава 21
tkyte@TKYTE816> alter table emp_base_table 2 add constraint 3 emp_ename_unique unique(ename); Table altered.
Теперь создадим представление ЕМР для таблицы EMP_BASE_TABLE. Правила защиты будут задаваться для этого представления, а приложение будет обращаться к нему для получения, вставки, изменения и удаления данных. Зачем используется представление, я объясню чуть позже: tkyte@TKYTE816> c r e a t e view emp as s e l e c t * from emp_base_table; View created. Теперь создадим таблицу для хранения информации о сотрудниках, ответственных за кадровые вопросы в отделах. Для этого используем таблицу, организованную по индексу. Поскольку к таблице будет выполняться единственный запрос, SELECT * FROM HR_REPS WHERE USERNAME = :X AND DEPTNO = :Y, таблица традиционной структуры нам не нужна: tkyte@TKYTE816> c r e a t e t a b l e hr_reps 2 3 4 5 6
(username varchar2(30), deptno number, primary key(username,deptno) ) organization index;
Table created.
Теперь зададим ответственных за кадровые вопросы в отделах: tkyte@TKYTE816> i n s e r t i n t o hr_reps values ('KING', 10); 1 row created. tkyte@TKYTE816> insert into hr_reps values ('KING', 2 0 ) ; 1 row created. tkyte@TKYTE816> insert into hr_reps values ('KING1, 3 0 ) ; 1 row created. tkyte8TKYTE816> insert into hr_reps values ('BLAKE', 1 0 ) ; 1 row created. tkyte@TKYTE816> insert into hr_reps values ('BLAKE', 2 0 ) ; 1 row created. tkyte@TKYTE816> commit; Commit complete.
Теперь, когда таблицы приложения, ЕМР, DEPT и HR_REPS, созданы, создадим процедуру, которая позволит устанавливать контекст приложения. Он будет содержать три атрибута: номер (EMPNO) текущего зарегистрированного пользователя, имя пользователя (USERNAME) и роль пользователя в приложении (ЕМР, MGR или HR_REP). При создании конструкции WHERE для выполняемых пользователем операторов фун-
Детальный контроль доступа
4 33
кция, динамически формирующая условие, будет использовать роль, хранящуюся в контексте приложения. Для определения роли используется информация из таблиц EMP_BASE_TABLE и HR_REPS. Вот вам и ответ на вопрос, зачем создавать таблицу EMP_BASE_TABLE и отдельное представление ЕМР как SELECT * FROM EMP_BASE_TABLE. Для этого имеются две причины: • данные в таблице сотрудников используются для реализации правил защиты; • данные из этой таблицы считываются при попытке задать контекст приложения. Для чтения данных о сотрудниках необходимо настроить контекст приложения, но для настройки контекста приложения необходимо получить данные о сотрудниках. Это проблема определения, что первично: курица или яйцо? Мы создадим представление (ЕМР), к которому будут обращаться все приложения, и обеспечим защиту этого представления. Исходная таблица EMP_BASE_TABLE будет использоваться функцией, реализующей правила защиты. По таблице EMP_BASE_TABLE можно определить, кто является руководителем отдела и кто у него в подчинении. Приложения и пользователи никогда не будут обращаться к таблице EMP_BASE_TABLE — только функция, реализующая правила защиты. Для этого мы не будем предоставлять другим пользователям прав доступа к базовой таблице; при этом невозможность работы с ней обеспечит сервер. В этом примере мы выбрали автоматическую установку контекста приложения при регистрации. Это стандартная процедура, если ее можно использовать, для автоматической настройки контекста приложения. Иногда этого недостаточно. Если при регистрации не хватает информации для определения того, каким должен быть контекст, придется установить его атрибуты вручную с помощью вызова соответствующей процедуры. Это часто приходится делать при использовании сервера приложений, подключающего всех пользователей к базе данных через одну совместно используемую учетную запись. Сервер приложений вызовет процедуру базы данных, передав ей имя "реального" пользователя для правильной настройки контекста. Ниже представлена написанная нами процедура для установки значений атрибутов контекста. Мы знаем все особенности ее работы, поэтому можем ей доверять. Она позволяет реализовать необходимые правила защиты, устанавливая в контексте только соответствующее имя пользователя, название роли и номер сотрудника. В дальнейшем при обращении к этим значениям можно быть уверенными в том, что они заданы правильно и безопасно. Процедура будет автоматически выполняться триггером ON LOGON. В такой реализации процедура позволяет поддерживать трехуровневые приложения, использующие пул подключений и всего одну учетную запись в базе данных. Необходимо предоставить право на выполнение этой процедуры учетной записи, используемой пулом подключений, а затем на сервере приложений выполнять эту процедуру, причем не используя стандартное значение — имя текущего пользователя, подключившегося к базе данных, а передавая имя пользователя как параметр. tkyte@TKYTE816> create or replace 2 procedure set_app_role(p_username in varchar2 3 default sys_context('userenv','session_user')) 4 as
434
Глава 21
5 l_empno number; 6 l_cnt number; 7 l_ctx varchar2(255) default 'Hr_App_Ctx'; 8 begin 9 dbms_session.set_context(l_ctx, 'UserName', p_username); 10 begin 11 select empno into l_empno 12 from emp_base_table 13 where ename = p_username; 14 dbms_session.set_context(l_ctx, 'Empno 1 , l_empno); 15 exception 16 when NO_DATA_FOUND then 17 — Пользователя нет в таблице emp — это, должно быть, •• ответственный за кадры. 18 NULL; 19 end; 20 21 22 — Сначала посмотрим, не является ли этот пользователь •* ответственным за кадры; если нет 23 — вдруг это руководитель; в противном случае устанавливаем •» пользователю роль ЕМР. 24 25 select count(*) into l_cnt 26 from dual 27 where exists 28 (select NULL 29 from hr_reps 30 where username = p_username 31 ); 32 33 if (l_cnt о 0) 34 then 35 dbms_session.set_context(l_ctx, 'RoleName', 'HR_REP'); 36 else 37 — Проверим, не является ли пользователь руководителем. 38 — Если нет, он получает роль ЕМР. 39 40 select count(*) into l_cnt 41 from dual 42 where exists 43 (select NULL 44 from emp_base_table 45 where mgr = to_number(sys_context(l_ctx,'Empno')) 46 ); 47 if (l_cnt О 0) 48 then 49 dbms_session.set_context(l_ctx, 'RoleName', 'MGR'); 50 else 51 — Роль ЕМР может получить каждый. 52 dbms_session.set_context(l_ctx, 'RoleName', 'EMP'); 53 end if;
Детальный контроль доступа 54 55 56
end
435
if;
end; /
Procedure created. Создадим контекст приложения. Контекст будет иметь имя HR_APP_CTX (совпадающее с именем предыдущей процедуры). Создавая контекст, обратите внимание, как он привязывается к только что созданной процедуре, — только она сможет устанавливать значения атрибутов в этом контексте: tkyte@TKYTE816> c r e a t e or replace context Hr_App_Ctx using SET_APP_ROLE 2 / Context c r e a t e d . Чтобы автоматизировать настройку контекста, используем триггер базы данных на событие регистрации, в котором будет вызываться процедура, устанавливающая значения контекста: tkyte@TKYTE816> create or replace t r i g g e r APP_L0GON_TRIGGER 2 a f t e r logon on database 3 begin 4 set_app_role; 5 end; 6 / Trigger created. Итак, мы создали процедуру, задающую роль для текущего зарегистрированного пользователя. Эта процедура будет вызываться не более одного раза за сеанс, гарантируя, что атрибут RoleName однократно получает значение при регистрации, которое затем не изменяется. Поскольку в зависимости от значения RoleName функция, реализующая правила защиты, будет возвращать различные значения, в версиях Oracle 8.1.5 и 8.1.6 нельзя разрешать пользователям изменять свою роль после установки. В противном случае возникнет потенциальная проблема с кешированными курсорами и "старыми" условиями (см. описание соответствующей проблемы в разделе "Проблемы" далее в этой главе; в версии 8.1.7 проблема в основном решена). Кроме того, мы находим значение EMPNO для текущего пользователя. Это дает нам два преимущества. •
Возможность проверить, является ли пользователь сотрудником. Получение сообщения об ошибке NO_DATA_FOUND позволяет судить, что подключившийся пользователь не является сотрудником. Поскольку его атрибут EMPNO не получает значения, этот пользователь не увидит данных, если только не является ответственным за кадры.
• Часто используемое значение помещается в контекст приложения. Теперь можно быстро обратиться к таблице ЕМР по значению EMPNO для текущего пользователя, что мы и будем делать далее в функции, формирующей условия. Затем мы создали объект (контекст приложения) и связали его с созданной ранее процедурой SET_APP_ROLE. В результате только эта процедура может устанавливать значения в данном контексте. Вот почему контекст приложения можно безопасно использовать и доверять получаемым результатам. Мы точно знаем, какой фрагмент кода может
436
Глава 21
устанавливать значения в контексте, и мы уверены, что они устанавливаются правильно (ведь мы же сами написали эту процедуру). Следующий пример показывает, что произойдет при попытке установить значения в контексте из другой процедуры: tkyte@TKYTE816> begin 2 dbms_session.set_context('Hr_App_Ctx', 3 'RoleName1, 'MGR'); 4 end; 5 / begin * ERROR a t l i n e 1: ORA-01031: i n s u f f i c i e n t p r i v i l e g e s ORA-06512: a t "SYS.DBMS_SESSION", l i n e 58 ORA-06512: a t l i n e 2 Чтобы проверить логику работы процедуры, попытаемся использовать ее от имени различных пользователей и посмотрим, какие роли мы можем устанавливать и какие значения устанавливаются в контексте. Начнем с пользователя SMITH. Это рядовой сотрудник. Он никем не руководит и не отвечает за кадры. Для получения значений, установленных в контексте, воспользуемся общедоступным представлением SESSION_CONTEXT: tkyte@TKYTE816> connect smith/smith smith@TKYTE816> smith@TKYTE816> smith@TKYTE816> smith@TKYTE816>
column column column select
namespace format alO a t t r i b u t e format alO value format alO * from s e s s i o n _ c o n t e x t ;
NAMESPACE ATTRIBUTE VALUE HR_APP_CTX ROLENAME EMP HR_APP_CTX USERNAME HR_APP__CTX EMPNO
SMITH 7369
Как видите, все работает, как и предполагалось. Пользователь SMITH успешно получил соответствующие значения атрибутов USERNAME, EMPNO и ROLENAME в контексте HR_APP_CTX. Подключившись от имени другого пользователя, мы видим, как работает процедура, попутно используя другой способ проверки значений в контексте приложения: smith@TKYTE816> connect blake/blake blake8TKYTE816> declare 2 l_AppCtx dbms_session.AppCtxTabTyp; 3 1 size number; — 4 begin 5 dbms_session.list_context(l_AppCtx, l_size); 6 for i in 1 .. l_size loop 7 dbms_output.put(l_AppCtx(i).namespace II ' . ' ) ; 8 dbms_output.put(l_AppCtx(i).attribute || ' = ' ) ; 9 dbms_output.put_line(l_AppCtx(i).value); 10 end loop; 11 end;
Детальный контроль доступа
43 /
12 / HR_APP_CTX.ROLENAME = HR_REP HR_APP_CTX.USERNAME = BLAKE HR_APP_CTX.EMPNO = 7698 PL/SQL p r o c e d u r e s u c c e s s f u l l y completed. На этот раз мы зарегистрировались как пользователь BLAKE, который является руководителем отдела 30 и ответственным за кадры в отделах 10 и 30. После регистрации видно, что контекст установлен правильно: для пользователя установлена роль HR_REP, имя пользователя и номер сотрудника. При этом показано, как получить пары атрибут/ значение из контекста сеанса с помощью процедуры DMBS_SESSION.LIST_CONTEXT. Этот пакет общедоступен, поэтому все пользователи смогут проверять значения атрибутов своего контекста с помощью этого метода, в дополнение к рассмотренному ранее представлению SESSION_CONTEXT. Убедившись, что контекст сеанса устанавливается так, как предполагалось, можно переходить к созданию функций, реализующих правила защиты. Эти функции будут вызываться сервером во время выполнения для динамического добавления условия к операторам. Такие динамически формируемые условия ограничивают множество данных, которые пользователь может читать или записывать. Мы создадим отдельные функции для операторов SELECT, операторов UPDATE и операторов INSERT/DELETE. Дело в том, что каждый из этих операторов может обращаться к различным подмножествам строк. Выбирать можно больше данных, чем изменять (например, сотрудник может просматривать свою запись, но не может ее менять). Только специально назначенные пользователи могут вставлять и удалять строки, поэтому условия для этих операторов тоже отличаются: blake@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> create or replace package hr_predicate_pkg 2 as 3 function select_function(p_schema in varchar2, 4 p_object in varchar2) return varchar2; 5 6 function update_function(p_schema in varchar2, 7 p_object in varchar2) return varchar2; 8 9 function insert_delete_function(p_schema in varchar2, 10 p_object in varchar2) return varchar2; 11 end; 12 / Package created. Ниже представлена реализация пакета HR_PREDICATE_PKG. Начнем с глобальных переменных: tkyte@TKYTE816> create or replace package body hr_predicate_pkg 2 as 3 4 g_app_ctx constant varchar2(30) default 'Hr_App_Ctx'; 5
438
Глава 21 6 7 8 9
g_sel_pred varchar2(1024) default NULL; g_upd_pred varchar2(1024) default NULL; g_ins_del_j3red varchar2(1024) default NULL;
Константа G_APP_CTX содержит имя контекста приложения. Если когда-либо потребуется переименовать контекст, можно изменить значение этой, используемой в остальном коде константы. Если придется это имя менять, достаточно будет поменять значение константы в одном месте и перекомпилировать тело пакета. Остальные три глобальные переменные будут содержать условия. Этот пример создавался для версии Oracle 8.I.6. В этой версии есть проблема, связанная с кешированием курсоров и средствами детального контроля доступа (подробнее см. в разделе "Проблемы" далее). Начиная с версии Oracle 8.1.7, этот прием программирования использовать необязательно. В данном случае реализуется правило, запрещающее менять роль после регистрации. Однажды сгенерированные в сеансе условия возвращаются для всех запросов. Мы не будем заново генерировать их для каждого запроса, так что любые изменения роли не повлияют на результат, пока пользователь не завершит сеанс и не зарегистрируется снова (или не сбросит состояние сеанса с помощью вызова DBMS_SESSION.RESET_PACKAGE). Теперь перейдем к первой из генерирующих условие функций. Она генерирует условие для операторов SELECT, которые обращаются к представлению ЕМР. Обратите внимание, что она просто устанавливает значение глобальной переменной пакета G_SEL_PRED (Global SELect PREDicate — глобальное условие для SELECT) в зависимости от значения атрибута контекста RoIeName. Если атрибут контекста не установлен, функция возбуждает исключительную ситуацию, которая приводит к неудачному завершению запроса: 10
11 function select_function(p_schema in varchar2, 12 p_object in varchar2) return varchar2 13 is 14 begin 15 16 if (g_sel_j3red is NULL) 17 then 18 if (sys_context(g_app_ctx, 'RoIeName') = 'EMP') 19 then 20 g_sel_pred:= 21 'empno=sys_context('''|Ig_app_ctx||''',''EmpNo " )'; 22 elsif (sys_context(g_app_ctx, 'RoIeName') = 'MGR') 23 then 24 g_sel_pred := 25 'empno in (select empno 26 from emp_base_table 27 start with empno = 28 sys_context(' " ||g_app_ctx||''',''EmpNo'') 29 connect by prior empno = mgr)'; 30 31 elsif (sys_context(g_app_ctx, 'RoIeName1) - 'HR_REP') 32 then
Детальный контроль доступа
33 g_sel_j>red := 'deptno in 34 ( s e l e c t deptno 35 from hr_reps 36 where username = 37 sys c o n t e x t ( ' ' ' | | g app c t x | Г " , ' ' U s e r N a m e ' ' ) ) ' ; 38 " " 39 else 40 raise_application_error(-20005, 'Роль не установлена'); 41 end if; 42 end if; 43 44 return g_sel_pred; 45 end; 46 Теперь перейдем к функции, возвращающей условие для операторов изменения. Алгоритм ее очень похож на алгоритм предыдущей функции, но условие возвращается другое. Обратите на использование условия 1=0, например, если атрибут RoleName имеет значение ЕМР. Рядовые сотрудники ничего изменять не могут. Руководители могут изменять записи своих подчиненных (но не собственную запись). Ответственные за кадры могут изменять записи всех сотрудников отдела, которым они занимаются: 47 function update_function(p_schema in varchar2, 48 p_object in varchar2) return varchar2 49 is 50 begin 51 if (g_upd_j3red is NULL) 52 then 53 if (sys_context(g_app_ctx, 'RoleName') = 'EMP') 54 then 55 g_upd_pred := '1=0'; 56 57 elsif (sys_context(g_app_ctx, 'RoleName') = 'MGR') 58 then 59 g_upd_j?red : = 60 ' empno in (select empno 61 from emp_base_table 62 where mgr = 63 sys_context('''||g_app_ctx|| 64 ' ", "EmpNo"))'; 65 66 elsif (sys_context(g_app_ctx, 'RoleName') = 'HR_REP') 67 then 68 g_upd_pred : = 'deptno in 69 (select deptno 70 from hr_reps 71 where username = 72 sys_context ('"II g_app_ctx | |' " , " UserName ''))'; 73 74 else 75 raise_application_error(-20005, 'Роль не установлена'); 76 end if;
440
Глава 21
77 end if; 78 79 r e t u r n g_upd_pred; 80 end; Наконец, рассмотрим функцию для операторов INSERT и DELETE. В этом случае условие 1=0 возвращается для пользователей с ролями ЕМР и MGR: никто из них не может создавать и удалять записи — это прерогатива ответственных за кадры (пользователей с ролью HR_REPS): 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
function insert_delete_function(p_schema in varchar2, p_object in varchar2) return varchar2 is begin if (g_ins_del_j?red is NULL) then if (sys_context(g_app_ctx, 'RoleName' ) in ( ' E M P \ 'MGR 1 )) then g_ins_del_pred := '1=0'; elsif (sys_context(g_app_ctx, 'RoleName') = 'HR_REP') then g_ins_del_pred := 'deptno in (select deptno from hr_reps where username = sys_context('''I Ig_app_ctx|I'•',''UserName''))'; else raise_application_error(-20005, 'Роль не установлена'); end if; end if; return g_ins_del_pred; end; end; /
Package body created. До появления средств детального контроля доступа обеспечить применение этих грех условий при работе с одной таблицей можно было только за счет использования многочисленных представлений — по одному для операторов SELECT, UPDATE и INSERT/ DELETE для каждой роли. Средства детального контроля доступа позволяют создать всего одно представление с динамически формируемыми условиями. Последний шаг в решении задачи — связывание условий с каждой операцией DML и представлением ЕМР. Это делается следующим образом: tkyte@TKYTE816> begin 2 dbms_rls.add_policy 3 (object_name => 'ЕМР', 4 policy_name => 'HR_APP_SELECT_POLICY', 5 policy_function => 'HR PREDICATE PKG.SELECT FUNCTION',
Детальный контроль доступа
4 41
6 statement_types => ' s e l e c t ' ) ; 7 end; 8
/
PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 dbms rls.add policy =•> (object name 3 => 4 policy name 5 policy function => 6 statement types 7 update check 8 end; •
-
>
'EMP1, 'HR APP_UPDATE_POLICY', 'HR_PREDICATE_ PKG.UPDATE_FUNCTION', 'update', TRUE);
PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 dbms rls.add policy 3 (object name => 4 policy name => 5 policy function => 6 statement types => 7 update check => 8 end; 9
'EMP1, 'HR_APP_INSERT_DELETE_POLICY' , 1 HR_PREDICATE_PKG.INSERT_DELETE_FUNCTION', 'insert, delete', TRUE);
PL/SQL procedure successfully completed. Итак, с каждом из операторов DML мы связали функцию, возвращающую условие. Когда пользователь обращается с запросом к представлению ЕМР, используется условие, генерируемое функцией HR_PREDICATE_PKG.SELECT_FUNCTION. При изменении данных будет вызвана функция UPDATE_FUNCTION этого пакета и т.д. Теперь протестируем приложение. Мы создадим пакет HR_APP. Этот пакет будет выступать в роли приложения. В нем есть подпрограммы для: •
выборки данных (процедура listEmps);
Q изменения данных (процедура updateSal); Q удаления данных (процедура deleteAII); •
вставки новых данных (процедура insertNew).
Мы будем регистрироваться от имени различных пользователей, с разными ролями и контролировать работу приложения. В результате будет продемонстрировано, как работают средства детального контроля доступа. D U I 1,11 сцификация
прилижснии. tkyte@TKYTE816> create or replace package hr_app 2 as 3 procedure listEmps; 4 5 procedure updateSal; 6
442 7 8 9 10 11
Глава 21 procedure deleteAll; procedure insertNew(p_deptno in number); end; /
Package created.
Теперь перейдем к телу пакета. Этот пример несколько надуманный, поскольку процедура, выполняющая UPDATE, пытается изменить все возможные строки, задав всем одно и то же значение. Это сделано для того, чтобы можно было точно увидеть, какие строки затрагиваются. Другие процедуры, по сути, похожи — они сообщают, что сделали и сколько строк обработано: tkyte@TKYTE816> c r e a t e or replace package body hr_app 2 as 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
procedure listEmps as l_cnt number default 0; begin dbms_output.put_line (rpad('ename',10) || rpad('sal', 6 ) II ' ' II rpad('dname',10) II rpad('mgr',5) II ' ' II rpad('dno',3)); for x in (select ename, sal, dname, mgr, emp.deptno from emp, dept where emp.deptno = dept.deptno) loop dbms_output.put_line(rpad(nvl(x.ename,'(null)'),10) || to_char(x.sal, '9,999') II ' ' И rpad(x.dname,10) II to_char(x.mgr,'9999') II ' ' || to_char(x.deptno,'99')); l_cnt := l_cnt + 1; end loop; dbms_output.put_line(l_cnt || ' строк(и) выбрано'); end;
procedure updateSal is begin update emp set sal = 9999; dbms_output.put_line(sql%rowcount end;
|| ' строк(и) изменено');
procedure deleteAll is begin delete from emp where empno <> sys_context('Hr_app_Ctx','EMPNO'); dbms_output.put_line(sql%rowcount || ' строк(и) удалено');
Детальный контроль доступа
443
39 end; 40 41 procedure insertNew(p_deptno in number) 42 as 43 begin 44 insert into emp (empno, deptno, sal) values (123, p_deptno, 1111); 45 end; 46 47 end hr app; 48 / Package body created. tkyte@TKYTE816> grant execute on hr app to public 2 / Grant succeeded. Вот и все наше "приложение". Процедура listEmps выдает все записи, доступные в представлении ЕМР. Процедура updateSal изменяет все записи, которые разрешено изменять. Процедура deleteAll удаляет все записи, которые разрешено удалять, за исключением записи текущего пользователя. Процедура insertNew пытается создать новую запись о сотруднике указанного отдела. Это приложение просто проверяет выполнение всех возможных операторов DML с представлением ЕМР (как я уже писал, приложение это весьма надуманное). Теперь, регистрируясь от имени различных пользователей, проверим работу приложения. Сначала регистрируемся и просматриваем значения атрибутов в контексте приложения: tkyte@TKYTE816> connect adams/adams adams@TKYTE816> adams@TKYTE816> adams@TKYTE816> adams@TKYTE816> NAMESPACE
column column column select
ATTRIBUTE
HR_APP_CTX ROLENAME HR_APP_CTX USERNAME HR_APP_CTX EMPNO
namespace format alO a t t r i b u t e format alO value format alO * from s e s s i o n _ c o n t e x t ; VALUE
EMP ADAMS 7876
adams@TKYTE816> s e t s e r v e r o u t p u t on И т а к , поскольку м ы зарегистрировались к а к о б ы ч н ы й сотрудник, п р о ц е д у р а listEmps д о л ж н а выдать т о л ь к о одну з а п и с ь — д л я этого сотрудника: adams@TKYTE816> e x e c t k y t e . h r _ a p p . l i s t E m p s ename sal dname mgr d n o ADAMS 1 , 1 0 0 RESEARCH 7788 20 1 с т р о к ( и ) выбрано PL/SQL procedure successfully completed. Поскольку мы выступаем в качестве обычного сотрудника, права изменять и удалять записи у нас быть не должно. Проверим:
444
Глава 21
adams@TKYTE816> exec tkyte.hr_app.updateSal О строк(и) изменено PL/SQL procedure successfully completed. adams@TKYTE816> exec
tkyte.hr_app.deleteAll
О строк(и) удалено PL/SQL procedure successfully completed. Наконец, проверим возможность выполнения оператора INSERT. В данном случае сервер выдаст сообщение об ошибке. В нашем примере для операторов UPDATE и DELETE ничего подобного не случилось. Попытки выполнить UPDATE или DELETE завершились успешно, потому что пользователю просто не дали данных, которые можно было бы изменить или удалить. При попытке вставки, однако, строка создается, нарушает правила защиты, и удаляется. В этом случае сервер выдает сообщение об ошибке: adams@TKYTE816> exec tkyte.hr_app.insertNew(20); BEGIN tkyte.hr_app.insertNew(20); END; * ERROR a t l i n e 1: ORA-28115: policy with check option violation ORA-06512: at "TKYTE.HR_APP", line 36 ORA-06512: at line 1 Итак, мы убедились, что пользователь может просматривать только собственную запись. Попытки изменять данные любым способом, удалять и вставлять строки завершаются неудачно. Именно это и предполагалось, и обеспечивается автоматически. Приложение, пакет HR_APP, не содержит кода, обеспечивающего реализацию этих правил. Все они автоматически реализуются сервером, с момента начала и до завершения сеанса, независимо от того, какая среда или инструментальное средство использовано для подключения. Теперь зарегистрируемся в качестве руководителя и посмотрим, что получится. Для начала снова получим значения атрибутов контекста, а затем — список сотрудников, записи которых доступны: adams@TKYTE816> @connect
jones/jones
jones@TKYTE816> s e t s e r v e r o u t p u t on jones@TKYTE816>
s e l e c t * from
NAMESPACE ATTRIBUTE
session_context;
VALUE
HR_APP_CTX ROLENAME MGR HR_APP_CTX USERNAME JONES HR_APP_CTX EMPNO 7566 jones@TKYTE816> exec t k y t e . h r _ a p p . l i s t E m p s ename sal dname mgr dno SMITH 800 RESEARCH 7902 20 JONES 2,975 RESEARCH 7839 20 SCOTT 9,999 RESEARCH 7566 20
Детальный контроль доступа ADAMS 1 , 1 0 0 RESEARCH FORD 3 , 0 0 0 RESEARCH 5 строк(и) выбрано
7788 7566
445
20 20
PL/SQL procedure successfully completed. Как видите, на этот раз мы получили из представления ЕМР несколько записей. Получены записи для всех сотрудников отдела 20 (пользователь JONES является его руководителем, в соответствии с информацией в представлении ЕМР). Теперь выполним процедуру изменения (updateSal) и проверим, какие изменения сделаны: jones@TKYTE816> exec tkyte.hr_app.updateSal 2 rows updated PL/SQL procedure successfully completed. jones@TKYTE816> exec tkyte.hr_app.listEmps ename sal dname mgr dno SMITH 7902 20 800 RESEARCH 7839 20 JONES 2,975 RESEARCH SCOTT 7566 20 9,999 RESEARCH ADAMS 7788 20 1,100 RESEARCH FORD 1SEARCH 9,999 RESEARCH 7566 20 5 строк(и) выбрано Предполагалось, что изменять можно только записи непосредственных подчиненных. Изменение затронуло только две записи для непосредственных подчиненных пользователя JONES. Теперь попытаемся выполнить удаление и вставку. Поскольку пользователь получил роль MGR, а не HR_REP, мы не сможем удалять записи, а при выполнении оператора INSERT будет получено сообщение об ошибке: jones@TKYTE816> exec 0 строк(и) удалено
tkyte.hr_app.deleteAll
PL/SQL procedure successfully completed. jones@TKYTE816> exec tkyte.hr_app.insertNew(20) BEGIN tkyte.hr_app.insertNew(20); END; * ERROR a t l i n e 1: ORA-28115: policy with check option violation ORA-06512: at "TKYTE.HR_APP", line 44 ORA-06512: at line 1 Итак, руководитель может следующее. Q Просматривать не только свои собственные данные. Руководитель получает информацию обо всех своих подчиненных. •
Изменять данные. В частности, можно изменять только записи для непосредственных подчиненных, что и требовалось.
Q Как и требовалось, ничего удалять и вставлять руководитель не может. Теперь зарегистрируемся в качестве ответственного за кадры (пользователь с ролью HR_REP) и проверим, что позволяет сделать приложение от имени этой роли. Снова
446
Глава 21
начнем с выдачи информации о контексте приложения и списка доступных для чтения строк. На этот раз выдается вся таблица ЕМР, поскольку пользователь KING имеет доступ к информации по всем трем отделам: jones@TKYTE816> c o n n e c t k i n g / k i n g king@TKYTE816> s e l e c t * from s e s s i o n _ c o n t e x t ; NAMESPACE ATTRIBUTE VALUE HR_APP_CTX ROLENAME HR_APP_CTX USERNAME HR_APP_CTX EMPNO
HR_REP KING 7839
king@TKYTE816> exec t k y t e . h r a p p . l i s t E m p s ename sal dname mgr dno CLARK 2,450 ACCOUNTING 7839 10 KING 5,000 ACCOUNTING 10 MILLER 1,300 ACCOUNTING 7782 10 SMITH 800 RESEARCH 7902 20 JONES 2,975 RESEARCH 7839 20 SCOTT 7566 20 9,999 RESEARCH ADAMS 1,100 RESEARCH 7788 20 FORD 9,999 RESEARCH 7566 20 ALLEN 1,600 SALES 7698 30 WARD 1,250 SALES 7698 30 MARTIN 1,250 SALES 7698 30 BLAKE 2,850 SALES 7839 30 TURNER 1,500 SALES 7698 30 JAMES 950 SALES 7698 30 14 строк(и) выбрано PL/SQL procedure successfully completed. Теперь выполним изменение и посмотрим, какие данные можно изменить. В данном случае изменены все строки: king@TKYTE816> exec tkyte.hr_app.updateSal 14 строк(и) изменено PL/SQL procedure successfully completed. king@TKYTE816> exec tkyte.hr_app.listEmps ename sal dname mgr dno CLARK 9,999 ACCOUNTING 7839 10 KING 9,999 ACCOUNTING 10 MILLER 9,999 ACCOUNTING 7782 10 SMITH 9,999 RESEARCH 7902 20 JONES 9,999 RESEARCH 7839 20 SCOTT 9,999 RESEARCH 7566 20 ADAMS 9,999 RESEARCH 7788 20 FORD 9,999 RESEARCH 7566 20 ALLEN 9,999 SALES 7698 30 WARD 9,999 SALES 7698 30 MARTIN 9,999 SALES 7698 30 BLAKE 9,999 SALES 7839 30
Детальный контроль доступа TURNER 9,999 SALES JAMES 9,999 SALES 14 с т р о к ( и ) выбрано
7698 7698
447
30 30
PL/SQL procedure successfully completed. Значение 9,999 в столбце SAL доказывает, что изменены все строки таблицы. Теперь попробуем выполнить удаление. Процедура DeleteAH создавалась так, чтобы она не могла удалять запись текущего зарегистрированного пользователя ни при каких условиях: king@TKYTE816> exec 13 строк(и) удалено
tkyte.hr_app.deleteAll
PL/SQL procedure successfully completed. Впервые мы смогли удалить записи. Теперь попробуем создать новую запись: king@TKYTE816> exec t k y t e . h r _ a p p . i n s e r t N e w ( 2 0 ) PL/SQL p r o c e d u r e s u c c e s s f u l l y completed. king@TKYTE816> exec t k y t e . h r _ a p p . l i s t E m p s ename sal dname mgr dno KING 9,999 ACCOUNTING 10 (null) 1,111 RESEARCH 20 2 строк(и) выбрано PL/SQL procedure successfully completed. Понятно, что в данном случае это получится, поскольку применяются правила защиты для роли HRJREP. Мы завершили тестирование всех трех ролей. Все требования выполнены, данные защищены, причем защита эта реализована прозрачно для приложений.
Проблемы Как и в случае любого средства, при использовании возможностей детального контроля доступа следует учитывать ряд нюансов. В этом разделе мы попытаемся их рассмотреть.
Целостность ссылок При взаимодействии со средствами обеспечения целостности ссылок средства детального контроля доступа могут давать неожиданные для разработчиков результаты. Все, мне кажется, зависит от предположений о возможном взаимодействии. Лично я не вполне уверен, что при их взаимодействии должно происходить. Оказывается, средства обеспечения целостности ссылок обходят защиту, устанавливаемую средствами детального контроля доступа. С их помощью я могу читать данные таблицы, удалять и изменять их, хотя непосредственно выполнять операторы SELECT, DELETE и INSERT для этой таблицы нельзя. Именно это и предусматривалось разработчиками СУБД и должно быть учтено при проектировании, если предполагается использовать средства детального контроля доступа.
448
Глава 21
Рассмотрим следующие возможности. Q Определение значений данных, которые должны быть недоступны. Это называется скрытым каналом (covert channel). Я не могу запросить данные непосредственно. Однако, я могу доказать существование (или отсутствие) определенных значений данных в таблице, используя внешний ключ. Q Удаление данных из таблицы при наличии ограничения целостности ссылок с конструкцией ON DELETE CASCADE. •
Изменение данных в таблице при наличии ограничения целостности ссылок с конструкцией ON DELETE SET NULL.
Мы рассмотрим все три возможности на несколько надуманном примере двух таблиц — Р (главная) и С (подчиненная): tkyte@TKYTE816> create table p (x int primary key); Table created. tkyte@TKYTE816> create table с (x int references p on delete cascade); Table created.
Скрытый канал Скрытым каналом в данном случае называется возможность определять наличие или отсутствие значений первичного ключа в таблице Р путем вставки строки в таблицу С и анализа результатов. Таким способом можно определить, есть ли в таблице Р строка с соответствующим значением первичного ключа. Начнем с функции, которая всегда возвращает условие, являющееся ложным для строки: tkyte@TKYTE816> create or replace function pred_function 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 4 as 5 begin 6 return '1=0'; 7 end; 8 / Function created. И используем эту функцию для ограничения доступа с помощью оператора SELECT к таблице Р: tkyte@TKYTE816> begin 2 dbms_rls.add__policy 1 3 (object_name => 'P , 4 policy_name => 'P_POLICY', 5 policy_function => 'pred_function', 6 statement_types => 'select'); 7 end; 8 / PL/SQL procedure successfully completed.
Детальный контроль доступа
4 4 9
Теперь мы по-прежнему можем вставлять значения в таблицу Р (а также изменять и удалять в ней данные), но не можем ничего выбрать из этой таблицы. Начнем с вставки строки в таблицу Р: tkyte@TKYTE816> i n s e r t i n t o p values (1); 1 row created. tkyte@TKYTE816> s e l e c t * from p; no rows s e l e c t e d Условие не позволяет выбрать эту строку, но можно проверить ее наличие, просто вставив строку в таблицу С: tkyte@TKYTE816> i n s e r t i n t o с v a l u e s ( 1 ) ; 1 row c r e a t e d . tkyte@TKYTE816> i n s e r t i n t o с v a l u e s ( 2 ) ; i n s e r t i n t o с v a l u e s (2) * ERROR a t l i n e 1: ORA-02291: integrity constraint (TKYTE.SYS_C003873) violated - parent key not found Итак, мы теперь знаем, что значение 1 содержится в таблице Р, а значения 2 в ней нет, потому что в таблицу С удалось вставить строку со значением 1 и не удалось — со значением 2. Средства обеспечения целостности ссылок могут читать данные, несмотря на установленные средствами детального контроля доступа правила защиты. Это может стать сюрпризом для приложения, например, средства, генерирующего запросы на основе информации из словаря данных. Если запросить данные из таблицы С, все необходимые строки возвращаются. При попытке же соединения таблиц Р и С будет получено пустое результирующее множество. Следует также отметить, что подобный скрытый канал получения данных из подчиненной таблицы возможен и для главной таблицы. Если бы аналогичное правило защиты было установлено не для таблицы Р, а для С и для нее не была бы задана конструкция ON DELETE CASCADE (другими словами, требовалось бы только наличие соответствующего значения первичного ключа), можно было бы определить, какие значения столбца X есть в таблице С, удаляя строки из таблицы Р. Если в подчиненной таблице С есть строки с соответствующим значением, попытка удаления строки из таблицы Р приведет к выдаче сообщения об ошибке, а если — нет, удаление пройдет успешно, хотя выбирать строки из таблицы С с помощью SELECT и нельзя.
Удаление строк Это можно сделать при наличии конструкции ON DELETE CASCADE в ограничении целостности. Если удалить правило для таблицы Р и задать эту же функцию в качестве правила защиты для удаления из таблицы С следующим образом: tkyte@TKYTE816> begin 2 dbmS_rls.drop_policy 3 ('TKYTE', ' P ' , 'P POLICY');
15 Зак 244
450
Глава 21
4 end; 5 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 dbms_rls.add_policy 3 (object_name => ' C , 4 policy_name => 'C_POLICY', 5 policy_function => 'pred_function', 6 statement_types => 'DELETE'); 7 end; 8
/
PL/SQL procedure successfully completed. то окажется, что нельзя удалить ни одной строки из таблицы С с помощью SQL-оператора: tkyte@TKYTE816> delete from С; О rows deleted. Установленное правило защиты не позволяет это сделать. Наличие строки в таблице С (в результате вставки в предыдущем примере) легко проверить: tkyte@TKYTE816> select * from С; X 1 Простое удаление строки в главной таблице: tkyte@TKYTE816> delete from Р; 1 row deleted. снова позволяет обойти правило защиты, задаваемое средствами детального контроля доступа — соответствующая строка из таблицы С тоже автоматически удаляется: tkyte@TKYTE816> s e l e c t * from С; no rows selected
Изменение строк Аналогичная ситуация при удалении строк из главной таблицы возникает и при использовании в ограничении целостности конструкции ON DELETE SET NULL. Немного изменим пример, чтобы, благодаря ограничению целостности ссылок, можно было изменять строки в таблице С, которые нельзя изменять SQL-операторами. Начнем с пересоздания таблицы С, задав для внешнего ключа конструкцию ON DELETE SET NULL. tkyte@TKYTE816> drop table c; Table dropped. tkyte@TKYTE816> create table с (х int references p on delete set n u l l ) ; Table created.
Детальный контроль доступа
45 1
tkyte@TKYTE816> i n s e r t i n t o p v a l u e s ( 1 ) ; 1 row c r e a t e d . tkyte@TKYTE816> i n s e r t i n t o с v a l u e s ( 1 ) ; 1 row c r e a t e d . Теперь зададим ту же функцию, что и в предыдущих примерах, в качестве правила защиты для изменения данных в таблице С, и установим флагу UPDATE_CHECK значение TRUE. Это не позволит изменять строки: tkyte@TKYTE816> begin 2 dbms_rls.add_policy 3 (object_name => ' С , 4 policy_name => 'C_POLICY', 5 policy_function => 'pred_function', 6 statement_types => 'UPDATE', 7 update_check => TRUE); 8 end; 9
/
PL/SQL procedure successfully completed. tkyte@TKYTE816> update с set x = NULL; 0 rows updated. tkyte@TKYTE816> select * from c; v
1 Итак, с помощью SQL-операторов строки в таблице С изменять нельзя. Однако удаление строк из таблицы Р показывает следующее: tkyte@TKYTE816> delete from p ; 1 row deleted. tkyte@TKYTE816> s e l e c t * from c; X Итак, обходным путем можно изменить данные в таблице С. Есть и другой способ продемонстрировать это, но данные в таблицах придется восстановить: tkyte@TKYTE816> d e l e t e from с; 1 row deleted. tkyte@TKYTE816> insert into p values (1); 1 row created. tkyte@TKYTE816> insert into с values (1); 1 row created. Теперь перепишем функцию так, чтобы можно было изменять строки в таблице С, задавая им любые значения, кроме Null:
452
Глава 21
tkyte@TKYTE816> create or replace function pred_function 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 4 as 5 begin 6 return 'x is not n u l l ' ; 7 end; 8 / Function created. tkyte@TKYTE816> update с set x = NULL; update с set x = NULL ERROR at line 1: ORA-28115: policy with check option violation
Это изменение завершилось неудачно, поскольку условие X IS NOT NULL не будет выполняться после изменения. Если теперь снова удалить строку из таблицы Р: tkyte@TKYTE816> delete from p; 1 row deleted. tkyte@TKYTE816> select * from c; X
строка в таблице С получит значение, которое нельзя задать с помощью SQL-оператора непосредственно.
Кеширование курсоров Одна из важных особенностей реализации функции, задающей правила защиты, из первого примера данной главы состоит в том, что в ходе сеанса эта функция возвращает одно и то же условие. Это принципиально важно. Если еще раз рассмотреть алгоритм этой функции: 5 as 6 begin 7 if (user = p_schema) then 8 return ''; 9 else 10 return 'owner = USER'; 11 end if; 12 end;
оказывается, что она возвращает либо пустое условие, либо условие owner = USER. Но в течение сеанса она постоянно возвращает одно и то же условие. Невозможно получить условие owner = USER, а затем в том же сеансе получить пустое условие. Чтобы понять, почему это принципиально важно для корректного использования средств детального контроля доступа в приложении, надо разобраться, когда условие связыва-
Детальный контроль доступа
4 5 3
ется с запросом и как это происходит при использовании различных сред: PL/SQL, Pro*C, OCI, JDBC, ODBC и т.д. Предположим, имеется следующая функция, возвращающая условие: SQL> create or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 5 begin 6 7 8 9 10 11 12 end; 13 /
if (sys_context('myctx', 'x') is not null) then return 'x > 0'; else return '1=0'; end if;
Function created. Она реализует такой алгоритм: если в контексте установлен атрибут х, возвращается условие х > 0; если же в контексте атрибут х не установлен, возвращается условие 1=0. Если создать таблицу Т, поместить в нее данные и добавить правила и контекст следующим образом: SQL> create table t (x int); Table created. SQL> insert into t values (1234); 1 row created. SQL> begin 2 dbms_rls.add_policy 3 (object_schema => user, 4 object_name => 'T', 5 policy_name => 'T_POLICY', 6 function_schema => user, 7 policy_function => 'rls_examp', 8 statement_types => 'select'); 9 end; 10 / PL/SQL procedure successfully completed. SQL> create or replace procedure set_ctx(p_val in varchar2) 2 as 3 begin 4 dbms_session.set_context('myctx', 'x', p_val); 5 end; 6 / Procedure created. SQ1> create or replace context myctx using set_ctx; Context created.
454
Глава 21
Предполагается, что в случае установки контекста мы должны получить одну строку. Если же контекст не установлен, мы ни одной строки получить не должны. Если проверить это в среде SQL*Plus с помощью простых SQL-операторов, именно так и окажется: SQL> exec set_ctx(null); PL/SQL procedure successfully completed. SQL> select * from t ; no rows selected SQL> exec set_ctx(1); PL/SQL procedure successfully completed. SQL> select * from t ; X 1234 Итак, казалось бы, все в порядке. Динамически формируемое условие применяется так, как предполагалось. Фактически же, если использовать язык PL/SQL (или Рго*С, или правильно написанное приложение, использующее интерфейсы OCI/JDBC/OOBC, да и многие другие среды), оказывается, что это не так. Создадим, например, небольшую PL/SQL-процедуру: SQL> c r e a t e or replace procedure dump_t 2 (some_input in number default NULL) 3 as 4 begin 5 dbms_output.put_line 6 (i*** результат выполнения оператора SELECT * FROM T'); 7 8 for x in (select * from t) loop 9 dbms_output.put_line(x. x); 10 end loop; 11 12 if (some_input is not null) 13 then 14 dbms_output.put_line 15 (>*** результат выполнения другого оператора SELECT ** * FROM T') ; 16 17 for x in (select * from t) loop 18 dbms_output.put_line(x. x); 19 end loop; 20 end iff; 21 end; 22 / Procedure created. • Эта процедура выполняет оператор SELECT * FROM T один раз, если входные данные не переданы, и два раза, если переданы какие-либо входные данные. Выполним эту
Детальный контроль доступа
45 5
процедуру и посмотрим результаты. Выполнение процедуры начнем, установив в контексте значение Null (поэтому будет применяться условие 1=0, другими словами, не будет возвращена ни одна строка): SQL> set serveroutput on SQL> exec set_ctx(NULL) PL/SQL procedure successfully completed. SQL> exec dump_t *** Результат выполнения оператора SELECT * FROM T PL/SQL procedure successfully completed. Как и ожидалось, данные не получены. Теперь установим значение в контексте так, чтобы возвращалось условие х > 0. Затем вызовем процедуру DUMP_T так, чтобы она выполняла оба запроса. В версиях Oracle 8.1.5 и 8.1.6 при этом произойдет следующее: SQL> exec set_ctx(1) PL/SQL procedure successfully completed. SQL> exec dump_t(0) *** Результат выполнения оператора SELECT * FROM T *** Результат выполнения другого оператора SELECT * FROM T 1234 PL/SQL procedure successfully completed. Первый запрос, первоначально выполненный при значении Null в контексте, по-прежнему не возвращает данных. Его курсор был сохранен в кеше и повторно не анализировался. При выполнении процедуры со значением Null атрибута х в контексте, получаем предполагаемые результаты (потому что это было первое выполнение данной процедуры в сеансе). Устанавливаем атрибуту х непустое значение и получаем неоднозначные результаты. Первый оператор SELECT * FROM T в процедуре по-прежнему не возвращает ни одной строки — он, видимо, продолжает использовать условие 1=0. Второй запрос (который первый раз мы не выполняли) возвращает, как и предполагалось, правильные результаты. Он использует условие х > 0, как и было задумано. Почему первый оператор SELECT в этой процедуре не использует условие, которое мы предполагали? Это связано с оптимизацией, называемой кешированием курсора. Язык PL/SQL и многие другие среды выполнения не "закрывают" курсор, когда этого требует разработчик. Представленный пример можно легко воспроизвести в Рго*С, если оставить опции прекомпилятора release_cursor стандартное значение NO. Если тот же код обработать с опцией release_cursor=YES, программа Рго*С будет работать аналогично запросам в среде SQL*Plus. Условие, используемое пакетом DBMS_RLS, связывается с запросом на стадии анализа. Первый запрос SELECT * FROM T анализируется при первом выполнении хранимой процедуры, когда фактически возвращалось условие 1=0. PL/SQL-машина автоматически кеширует проанализированный курсор. При втором выполнении хранимой процедуры PL/SQL-машина повторно использует проанализированный курсор первого запроса SELECT * FROM Т. Этот проанализированный курсор
456
Глава 21
включает условие 1=0. Функция, возвращающая условие, на этот раз вообще не вызывалась. Поскольку мы передали процедуре входные данные, PL/SQL-машина выполнила и второй запрос. Для этого запроса, однако, еще нет открытого и проанализированного курсора, так что при выполнении он был проанализирован, с учетом непустого атрибута в контексте. Со вторым запросом SELECT * FROM T было связано условие х>0. Это и вызвало двусмысленность. Поскольку мы не можем управлять кешированием курсора, возвращения в одном сеансе различных условий функцией, реализующей правила защиты, надо избегать любой ценой. В противном случае возможны плохо воспроизводимые и сложные для отладки ошибки в приложении. Ранее, в примере приложения для работы с информацией о сотрудниках, было продемонстрировано, как реализовать функцию, которая возвращает в ходе сеанса не более одного условия. Это гарантирует следующее. •
Согласованность результатов запросов с точки зрения средств детального контроля доступа.
Q Неизменность условий защиты по ходу сеанса. Если они изменяются, возможны странные и непредсказуемые результаты. Q Зависимость правил защиты от пользователя, выполняющего операторы, но не от среды, в которой он работает. В версиях Oracle 8.1.7 и выше результат будет следующим: tkyte@dev817> exec dump_t(0) *** Результат выполнения оператора SELECT * FROM T 1234 *** Результат выполнения другого оператора SELECT * FROM T 1234 PL/SQL procedure successfully completed. Во избежание описанных проблем сервер Oracle версий 8.1.7 и выше повторно анализирует запрос при изменении контекста сеанса, если с запросом связаны правила защиты. Подчеркиваю: при изменении контекста сеанса. Если для создания условия не используется контекст сеанса, снова возникает проблема, связанная с кешированием курсора. Рассмотрим систему, в которой условия хранятся как данные в таблице базы данных. Такая система реализует правила защиты на основе таблицы. Если при этом содержимое таблицы правил изменится, вызывая изменение условия, мы столкнемся в версии 8.1.7 с теми же проблемами, что и в 8.1.6, и более ранних версиях. Если изменить предыдущий пример так, чтобы использовалась таблица базы данных: tkyte@TKYTE816> create table policy_rules_table 2 (predicate_jpiece varchar2(255) 3 ); Table created. tkyte@TKYTE816> insert into policy_rules_table values ('x > 0' ); 1 row created. и изменить функцию, реализующую правила защиты так, чтобы она использовала таблицу:
Детальный контроль доступа
4 5 7
tkyte@TKYTE816> create or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 4 as 5 l_predicate_piece varchar2(255); 6 begin 7 select predicate_piece into l_predicate_piece 8 from policy_rules_table; 9
10 return l_predicate_piece; 11 end; 12 / Function created. то можно ожидать следующих результатов выполнения процедуры DUMP_T (при изменении условия после выполнения DUMP_T без параметров, но до выполнения ее с параметром): tkyte@DEV817> exec dump_t *** Результат выполнения оператора SELECT * FROM T 1234 PL/SQL procedure successfully completed. tkyte@DEV817> update policy_rules_table set predicatejoiece = '1=0'; 1 row updated. tkyte@DEV817> exec dump_t(0) *** Результат выполнения оператора SELECT * FROM T 1234 *** Результат выполнения другого оператора SELECT * FROM T PL/SQL procedure successfully completed. Обратите внимание, что при первом выполнении использовалось условие х>0, и запрос возвращал одну строку из таблицы Т. После выполнения этой процедуры мы изменили условие (это изменение можно было сделать из другого сеанса — его, например, мог сделать администратор). При втором выполнении процедуры DUMP_T — с параметром, требующим выполнить после первого запроса второй, оказывается, что в первом запросе по-прежнему используется старое условие, х>0, тогда как во втором запросе — второе условие, 1=0, только что помещенное в таблицу POLICY_RULES. Необходимо учитывать последствия кеширования курсора, даже в версиях 8.1.7 и выше, если только в правилах защиты наряду с таблицей не используется контекст приложения. Хочу подчеркнуть, что изменять значение SYS_CONTEXT по ходу работы приложения вполне допустимо. Эти изменения будут учтены и использованы при следующем выполнении запроса. Поскольку значения атрибутов контекста передаются как связываемые переменные, они вычисляются на этапе выполнения запроса, а не на этапе анализа, так что константы на этапе анализа не используются. Только текст условия не должен меняться по ходу работы приложения. Вот небольшой пример, демонстрирующий это. Завершим сеанс и снова зарегистрируемся (чтобы очистить кеш курсоров в сеансе), а затем изменим реализацию функции RLS_EXAMP. Затем выполним те же действия, что и раньше:
458
Глава 21
tkyte@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> create or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 4 as 5 begin 6 r e t u r n 'x > s y s _ c o n t e x t ( ' ' m y c t x ' ' , ' ' x ' ' ) ' ; 7 end; Function created. tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> exec set_ctx(NULL) PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dump_t *** Результат выполнения оператора SELECT * FROM T PL/SQL procedure successfully completed. tkyte@TKYTE816> exec set_ctx(l) PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dump_t(0) *** Результат выполнения оператора SELECT * FROM T 1234 *** Результат выполнения другого оператора SELECT * FROM T 1234 PL/SQL procedure successfully completed. На этот раз оба запроса вернули одинаковые результаты. Это связано с тем, что они используют одинаковую конструкцию WHERE и в условии динамически обращаются к значению атрибута в контексте приложения. Следует упомянуть, что бывают случаи, когда изменение условия по ходу сеанса может оказаться необходимым. Для этого приходится специальным образом программировать приложения, обращающиеся к объектам, правила защиты которых допускают изменение условий по ходу сеанса. Например, в PL/SQL придется использовать в приложении исключительно динамический SQL, чтобы избежать кеширования курсоров. При использовании этого способа поддержки динамических условий следует помнить, что результаты будут зависеть от того, как написано клиентское приложение, так что подобным образом универсальные правила защиты реализовать не удастся. Мы не будем рассматривать этот способ использования средств пакета DBMS_RLS, а сосредоточимся на его исходном назначении — защите данных от несанкционированного доступа.
Экспортирование/Импортирование Эту проблему мы уже упоминали. Необходимо быть внимательным при использовании утилиты ЕХР для экспортирования данных и утилиты I M P для их импортирования. Поскольку при этом возникают разные проблемы, они будут рассмотрены по от-
Детальный контроль доступа
4 5 у
дельности. Чтобы продемонстрировать проблемы, придется несколько расширить предыдущий пример, изменив правила защиты T_POLICY. На этот раз правила будут применяться не только для операторов SELECT, но и для операторов INSERT: tkyte@TKYTE816> begin 2 dbms_rls.drop_policy('TKYTE', 'T' 3 end; 4 / PL/SQL procedure successfully completed.
' Т _POLICY
)'
tkyte@TKYTE816> begin 2 dbms_rls.add_policy 3 (object_name => 'T', 4 policy_name => 'T_POLICY', 5 policy_function => 'rls_examp', 6 statement_types => 'select, insert' 7 update_check => TRUE); 8 end; 9 / PL/SQL procedure successfully completed. После этого мы получим следующий эффект: tkyte@TKYTE816> delete from t; 1 row deleted. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> exec set_ctx(null); PL/SQL procedure successfully completed. tkyte@TKYTE816> insert into t values (1); insert into t values (1) * ERROR at line 1: ORA-28115: policy with check option violation tkyte@TKYTE816> exec set_ctx(0) ; PL/SQL procedure successfully completed. tkyte@TKYTE816> insert into t values (1); 1 row created. Итак, теперь контекст необходимо устанавливать не только для чтения, но и для вставки данных.
Проблемы экспорта Стандартно утилита ЕХР работает в режиме "обычного" (conventional path) экспорта. Для чтения данных она использует SQL-операторы. Используя утилиту ЕХР для получения данных таблицы Т из базы данных, получим следующий результат (учтите, что в таблице Т имеется одна строка — результат выполнения оператора INSERT):
460
Глава 21
C:\fgac>exp userid=tkyte/tkyte tables=t Export: Release 8.1.6.0.0 - Production on-Mon Apr 16 16:29:25 2001 (c) Copyright 1999 Oracle Corporation. All rights reserved. Connected to: 0racle8i Enterprise Edition Release 8.1.6.0.0 — Production With the Partitioning option JServer Release 8.1.6.0.0 — Production Export done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set About to export specified tables via Conventional Path ... EXP-00079: Data in table "T" is protected. Conventional path may only be exporting partial table. . . exporting table T 0 rows exported Export terminated successfully with warnings.
Обратите внимание, как утилита ЕХР "великодушно" сообщила, что таблица может быть экспортирована только частично, поскольку используется обычный способ экспортирования. Для решения этой проблемы при экспортировании надо подключаться как пользователь SYS (или от имени любого другого пользователя с ролью SYSDBA). Для пользователя SYS средства детального контроля доступа не действуют: C:\fgac>exp userid=sys/manager tables=tkyte.t Export: Release 8.1.6.0.0 - Production on Mon Apr 16 16:35:21 2001 (c) Copyright 1999 Oracle Corporation. All rights reserved. Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 — Production With the Partitioning option JServer Release 8.1.6.0.0 - Production Export done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set About to export specified tables via Conventional Path ... Current user changed to TKYTE . . exporting table T 1 rows exported Export terminated successfully without warnings.
Можно также использовать процедуру D B M S _ R L S . E N A B L E _ P O L I C Y для временного отключения правил защиты и повторного их включения после экспортирования. Хотя так делать нежелательно, поскольку на время экспорта таблицы остаются незащищенными. В некоторых версиях Oracle 8.1.5 при непосредственном экспорте по ошибке обходятся средства детального контроля доступа. Другими словами, если указать опцию direct=true, будут экспортированы все данные. На этот способ полагаться не стоит, поскольку в следующих версиях эта ошибка была исправлена. В новых версиях вы получите: About to export specified tables via Direct Path ... EXP-00080: Data in table "T" is protected. Using conventional mode. EXP-00079: Data in table "T" is protected. Conventional path may only...
Детальный контроль доступа
Утилита ЕХР будет автоматически экспортировать защищенные таблицы в обычном режиме.
Проблемы импорта Эти проблемы возникают только в том случае, когда установлены правила защиты для операторов вставки и опция UPDATE_CHECK имеет значение True. В этом случае утилита IMP не будет вставлять строки, не удовлетворяющие условию, возвращаемому соответствующей функцией. Именно так и произойдет в рассмотренном ранее примере. Если не установить контекст, ни одна строка вставлена не будет (по умолчанию значение в контексте — Null). Поэтому, если попытаться импортировать экспортированные данные: C:\fgac>imp userid=tkyte/tkyte
full=y ignore=y
Import: Release 8.1.6.0.0 - Production on Mon Apr 16 16:37:33 2001 (c) Copyright 1999 Oracle Corporation.
All r i g h t s reserved.
Connected t o : 0racle8i Enterprise Edition Release 8.1.6.0.0 — Production With the Partitioning option JServer Release 8.1.6.0.0 — Production Export f i l e created by EXPORT:V08.01.06 via conventional path Warning: the objects were exported by SYS, not by you import done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set . importing SYS's objects into TKYTE . . importing table "T" IMP-00058: ORACLE error 28115 encountered ORA-28115: policy with check option violation IMP-00017: following statement f a i l e d with ORACLE e r r o r 28101: "BEGIN DBMS_RLS.ADD_POLICY('TKYTE', 1 T ' , 'T_POLICY','TKYTE', 'RLS_EXAMP','SE" "LECT,INSERT',TRUE,TRUE); END;" IMP-00003: ORACLE e r r o r 28101 encountered ORA-28101: policy already e x i s t s ORA-06512: a t "SYS.DBMS_RLS", l i n e 0 ORA-06512: a t l i n e 1 Import terminated successfully with warnings. строки вставлены не будут. Проблему можно решить, подключившись от имени пользователя SYS или пользователя с ролью SYSDBA: C:\fgac>imp userid=sys/manager full=y ignore=y Import: Release 8.1.6.0.0 - Production on Mon Apr 16 16:40:56 2001 (c) Copyright 1999 Oracle Corporation. All rights reserved. Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 — Production With the Partitioning option JServer Release 8.1.6.0.0 — Production
462
Глава 21
Export f i l e created by EXPORT:V08.01.06 via conventional path import done i n WE8ISO8859P1 character s e t and WE8ISO8859P1 NCHAR character s e t . importing SYS's objects i n t o SYS . importing TKYTE's objects i n t o TKYTE . . importing t a b l e "T" 1 rows imported Можно также с помощью процедуры DBMS_RLS.ENABLE_POLICY временно отключить правила и включить их после импортирования. Как и в случае экспорта, это не желательно, поскольку в процессе импортирования таблица не защищена.
Отладка При написании функций, возвращающих условия, я часто использую пакет средств отладки, debug. Этот пакет, созданный сотрудником корпорации Oracle Кристофером Беком (Christopher Beck), позволяет включить в код операторы отладочной печати. Он позволяет свободно вставлять в код операторы вида: create function foo ... as begin debug.f('Входим в процедуру foo')I i f (some_condition) then l_predicate := ' x = l ' ; end if; debug. f {' Возвращаем условие ' ' %s ' ' ' , l_j?redicate) ; r e t u r n l_predicate; end; Итак, процедура debug.f работает аналогично С-функции printf и реализована с помощью средств пакета UTL_FILE. Она создает управляемые программистом файлы трассировки на сервере базы данных. Файлы трассировки содержат результаты отладочной печати, благодаря которым можно понять, как выполняется код. Поскольку ядро сервера вызывает код реализующих правила защиты функций в фоновом режиме, отладка их затруднена. Традиционные средства вроде пакета DBMS_OUTPUT и отладчика PL/SQL тут не особенно помогут. При наличии файлов трассировки можно сэкономить очень много времени. Среди сценариев, которые можно загрузить с сайта издательства Wrox, находится и пакет debug с комментариями по его настройке и использованию. Этот пакет особенно полезен при поиске проблем в функциях, реализующих правила защиты, и я настоятельно рекомендую использовать его или другой подобный пакет. При отсутствии подобных средств трассировки практически невозможно разобраться, что работает неправильно.
Ошибки, которые могут произойти По ходу реализации рассмотренного ранее приложения я столкнулся с многочисленными ошибками, и мне пришлось заниматься его отладкой. Поскольку средства детального контроля доступа работают на сервере, поиск причин ошибки и отладка приложе-
Детальный контроль доступа
463
ния очень затруднена. Изучив следующие разделы, вы сможете находить и успешно устранять причины ошибок*.
ORA-28110: пакет или функция <имя функции> методики имеет ошибку Эта ошибка свидетельствует о том, что в пакете или функции, реализующей правила защиты, имеется ошибка, и перекомпиляция ее невозможна. Если выполнить в среде SQL*Plus команду SHOW ERRORS FUNCTION <ИМЯ ФУНКЦИИ> или SHOW ERRORS PACKAGE BODY <ИМЯ ПАКЕТА>, можно получить соответствующие сообщения об ошибках. Эта ошибка может быть связана с тем, что: Q один из объектов, на который ссылается функция, удален или стал недействительным; О в компилируемом коде есть синтаксическая ошибка или его по какой-то причине нельзя скомпилировать. Наиболее типичная причина этой ошибки состоит в том, что условие, которое возвращает функция, реализующая правила защиты для таблицы, содержит ошибку. Рассмотрим функцию, использованную в предыдущих примерах: tkyte@TKYTE816> create or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 r e t u r n varchar2 4 as 5 begin 6 this is an error 7 return 'x > sys_context(''myctx'',''x'')'; 8 end; 9 / Warning: Function created with compilation errors. Предположим, при компиляции мы не обратили внимания, что функция не откомпилирована, как положено. Мы предполагаем, что функция скомпилирована успешно, и что ее можно выполнять. Теперь, попытавшись выполнить запрос к таблице Т, мы получим: tkyte@TKYTE816> exec set_ctx(0); PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; select * from t * ERROR at line 1: ORA-28110: policy function or package TKYTE.RLS_EXAMP has error * Тексты сообщений об ошибках в названиях подразделов здесь представлены так, как их выдает сервер Oracle 8.1.6 при установке русского в качестве языка сообщений. В примерах кода оставлены сообщения об ошибках на английском. Обратите внимание на расхождения в терминологии: "методика" вместо "правила". - П р и м . научн. ред.
464
Глава 21
Итак, это сообщение свидетельствует о наличии ошибки, а именно, об ошибке в функции TKYTE.RLS_EXAMP (ее не удалось успешно скомпилировать). Для того чтобы находить подобные проблемы прежде, чем они возникнут, пригодится следующий запрос: tkyte@TKYTE816> column pf_owner format alO tkyte@TKYTE816> column package format alO tkyte@TKYTE816> column function format alO tkyte@TKYTE816> s e l e c t pf_owner, package, function 2 from user_policies a 3 where exists (select null 4 from all_objects 5 where owner = pf_owner 6 and object_type in ('FUNCTION', 'PACKAGE', 7 'PACKAGE BODY') 8 and status = 'INVALID' 9 and object_name in (a.package, a.function) 10 ) 11 / PFJOWNER
PACKAGE
TKYTE
FUNCTION RLS_EXAMP
Этот запрос выдает список всех недействительных функций, реализующих правила защиты. Пока что он подтверждает то, что мы и так знаем, — что функция TKYTE.RLS_EXAMP скомпилирована с ошибками. Решить эту проблему несложно. Выполняем: tkyte@TKYTE816> show e r r o r s function Errors for FUNCTION RLS_EXAMP:
rls_examp
LINE/COL ERROR 6/10
FLS-00103: Encountered the symbol "AN" when expecting one of the following: := • ( @ % ;
Исправляем ошибку в строке 6 с текстом this is an error, и сообщение ORA-28110 выдаваться не будет.
ORA-28112: сбой при выполнении функции методики Сообщение об ошибке ORA-28112: failed to execute policy function выдается при выполнении оператора SELECT или оператора DML для таблицы, относительно которой заданы правила защиты, если в реализующей эти правила функции (а не в возвращаемом ею условии) произошла ошибка. Это означает, что при выполнении функции возбуждена исключительная ситуация, не обработанная в самой функции и полученная ядром сервера. При возникновении ошибки ORA-28112 в каталоге, заданном параметром инициализации USER_DUMP_DEST, будет генерироваться файл трассировки. В этом файле не будет сообщения об ошибке ORA-28112, но будет фраза Policy function execution error. Зададим для функции следующий алгоритм (продолжаем предыдущий пример):
Детальный контроль доступа
4 6 5
tkyte@TKYTE816> create or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 return varchar2 4 as 5 l_uid number; 6 begin 7 select user_id 8 into l_uid 9 from all_users 10 where username = 'ИМЯ_НЕСУЩЕСТВУЮЩЕГО_ПОЛЬЗОВААТЕЛЯ'; 11 12 return 'x > sys_context(''myctx'',''x'')'; 13 end; 14 / Function created. Эта функция специально написана так, чтобы возбуждалась и не обрабатывалась исключительная ситуация NO_DATA_FOUND. Любопытно посмотреть, что произойдет, если исключительная ситуация распространится на уровень ядра сервера. Инициируем вызов этой функции: tkyte@TKYTE816> exec set_ctx(0); PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from t; select * from t ERROR at line 1: ORA-28112: failed to execute policy function Это означает, что функция, реализующая правила защиты, существует и синтаксически корректна, но при ее выполнении возбуждается исключительная ситуация. При возникновении этой ошибки создается файл трассировки. Если посмотреть содержимое каталога, задаваемого параметром инициализации USER_DUMP_DEST и найти этот файл трассировки, в самом начале можно обнаружить следующее: ... *** SESSION ID:(8.405) 2001-04-16 17:03:00.193 *** 2001-04-16 17:03:00.193 Policy function execution error: Logon user : TKYTE Table or View : TKYTE.T Policy name : T_POLICY Policy function: TKYTE.RLS_EXAMP ORA-01403: no data found ORA-06512: at "TKYTE.RLS_EXAMP", line 7 ORA-06512: at line 1 Эта информация позволяет определить, в каком месте функции произошла ошибка. Явно сказано про строку 7, содержащую оператор SELECT ... INTO, а также указано, что в этой строке возбуждена исключительная ситуация NO DATA FOUND.
466
Глава 21
ORA-28113: ошибка в предикате методики Сообщение об ошибке ORA-28113: policy predicate has error выдается при выполнении оператора SELECT или оператора DML для таблицы, относительно которой заданы правила защиты, если соответствующая функция возвращает синтаксически или семантически неверное условие. Это условие при добавлении к исходному запросу дает синтаксически неправильный SQL-оператор. При возникновении ошибки ORA-28113 в каталоге, заданном параметром инициализации USER_DUMP_DEST, генерируется файл трассировки. В нем будет сообщение об ошибке ORA-28113 и информация о текущем сеансе и ошибочном условии. Пусть, например, функция реализована так, как показано ниже. Она возвращает условие, сравнивающее значение в столбце X с несуществующим столбцом таблицы: tkyte@TKYTE816> c r e a t e or replace function rls_examp 2 (p_schema in varchar2, p_object in varchar2) 3 r e t u r n varchar2 4 as 5 begin 6 r e t u r n 'x = несуществующий_столбец'; 7 end; 8 / Function created. Так что запрос вида: s e l e c t * from t будет переписан как: s e l e c t * from ( select * from t where x = несуществующий столбец) Очевидно, поскольку в таблице Т такого столбца нет, этот запрос выполнить нельзя. tkyte@TKYTE816> s e l e c t * from t ; s e l e c t * from t ERROR a t l i n e 1: ORA-28113: policy predicate has e r r o r Функция успешно вернула условие, но при добавлении этого условия к запросу произошла ошибка. В конце текста соответствующего файла трассировки на сервере мы обнаружим: *** SESSION I D : ( 8 . 4 0 9 ) 2001-04-16 1 7 : 0 8 : 1 0 . 6 6 9 *** 2001-04-16 1 7 : 0 8 : 1 0 . 6 6 9 E r r o r i n f o r m a t i o n f o r ORA-28113: Logon u s e r : TKYTE Table o r View : TKYTE.T P o l i c y name : T_POLICY P o l i c y f u n c t i o n : TKYTE.RLS_EXAMP RLS p r e d i c a t e :
Детальный контроль доступа
4 6 7
х = несуществующий_столбец ORA-00904: i n v a l i d column name Этой информации достаточно для решения проблемы (изменения условия, которое привело к выдаче сообщения об ошибке), поскольку, помимо ошибочного условия, имеется сообщение об ошибке в этом условии.
ORA-28106: вводимое значение для аргумента #2 неверно Если имя атрибута не является допустимым идентификатором Oracle, сообщение об ошибке можно получить при вызове процедуры DBMS_SESSION.SET_CONTEXT. Атрибуты в контексте приложения должны именоваться с соответствии с соглашениями, принятыми в Oracle (точно так же, как имена столбцов таблиц или переменных PL/SQL). Единственное решение — изменить имя атрибута. Нельзя, например, использовать атрибут контекста с именем SELECT, придется переименовать его.
Резюме В этой главе были подробно рассмотрены средства детального контроля доступа. Есть много аргументов за использование этих средств и лишь несколько — против. В общемто, найти аргументы против использования этих средств непросто. Мы рассмотрели, как средства детального контроля доступа: Q Упрощают разработку приложений. Они отделяют управление доступом от приложения, приближая его к данным. •
Гарантируют постоянную защиту данных. Независимо от того, какое средство использовано для доступа к данным, правила защиты применяются неукоснительно, и обойти их нельзя.
•
Позволяют изменять правила защиты без изменения клиентских приложений.
•
Упрощают управление объектами базы данных. Их использование позволяет уменьшить количество объектов базы данных, необходимых для поддержки приложения.
Эти средства обеспечивают отличную производительность. Производительность фактически зависит от производительности алгоритмов и SQL-операторов, реализующих правила защиты. Если возвращаемое условие не позволяет оптимизатору выработать эффективный план выполнения, это никак не связано со средствами детального контроля доступа, а исключительно с настройкой SQL-оператора. Использование контекста приложения позволяет воспользоваться всеми преимуществами разделяемых SQL-операторов и уменьшить количество создаваемых для базы данных объектов. Средства детального контроля доступа не снижают производительность в большей степени, чем любой другой способ выполнения тех же проверок. Было также показано, что эти средства могут создавать определенные проблемы при отладке, поскольку детальный контроль доступа выполняется в фоновом режиме и обычные средства, например, отладчик или пакет DBMS_OUTPUT, не помогают. Пакеты, вроде упомянутого в разделе "Ошибки, которые могут возникнуть" пакета debug, упрощают трассировку и отладку приложений, использующих средства детального контроля доступа.
еРгодгй" rtalOr
Многоуровневая аутентификация
Многоуровневой (n-tier), или промежуточной, аутентификацией называется регистрация программного обеспечения промежуточного уровня от своего имени для выполнения в базе данных каких-либо действий по поручению пользователя. Это позволяет создавать промежуточные приложения, использующие собственную схему аутентификации, например, с помощью сертификатов Х509 или другого процесса однократной регистрации и регистрироваться от имени пользователя, не зная его пароля в базе данных. Хотя регистрация выполнена не от имени пользователя, а от имени промежуточного ПО, для базы данных он зарегистрирован. В этой главе мы рассмотрим многоуровневую аутентификацию и использование этой новой возможности Oracle 8i в приложениях. В частности: Q будут представлены возможности и рассмотрено их использование в приложениях; •
разработана программа на основе средств библиотеки OCI, которая позволит использовать промежуточную аутентификацию для регистрации в базе данных;
Q описан оператор ALTER USER, позволяющий использовать промежуточную аутентификацию в базе данных; • рассмотрена возможность отслеживания действий, выполняемых от имени промежуточной учетной записи.
470
Глава 22
В версии Oracle 8i многоуровневую аутентификацию можно использовать только в программах на основе библиотеки Oracle Call Interface (OCI), написанных на языках С или C++. В Oracle 9i многоуровневая аутентификация будет поддерживаться также интерфейсом JDBC, что существенно расширит ее возможности.
Когда использовать многоуровневую аутентификацию? Во времена централизованных и клиент-серверных систем аутентификация была простой. Клиент (приложение) запрашивал у пользователя реквизиты (имя пользователя и пароль) и передавал их серверу базы данных. Сервер проверял эти реквизиты и, если все было правильно, клиент подключался к серверу: \
Scott/Tiger
База данных
Приложение
Теперь у нас есть Web и, как следствие, многоуровневая архитектура, когда, например, клиент (браузер) предоставляет свои реквизиты промежуточному серверу приложений, на котором выполняются компоненты JavaServer Page (JSP), обращающиеся к CORBA-объекту, который, в свою очередь, взаимодействует с базой данных. Реквизиты, передаваемые ПО промежуточного уровня, могут совпадать или не совпадать с именем пользователя и паролем, использовавшимся во времена клиент-серверной архитектуры, — это могут быть реквизиты, разрешающие доступ к службе каталогов (directory service), чтобы ПО промежуточного уровня могло выяснить, кто подключается и какими привилегиями доступа. Это могут быть реквизиты в виде сертификата Х.509, включающего идентификационную информацию и привилегии. В любом случае ПО промежуточного уровня необязательно использует эти реквизиты для регистрации пользователя в базе данных. Клиент больше не взаимодействует с базой данных непосредственно; между ними есть один, два или более промежуточных уровней. Можно, правда, заставить пользователя передавать регистрационное имя и пароль компонентам JSP, которые будут передавать их CORBA-объекту, а тот — серверу базы данных, но это не позволит использовать другие технологии и механизмы аутентификации, в особенности механизмы однократной регистрации. Рассмотрим следующий пример — достаточно типичное современное Web-приложение:
Многоуровневая аутентификация
47 1
2
Web-браузер БД метасвязей
Клиент представляет собой Web-браузер, отображающий HTML-страницы и обращающийся к Web-серверу/серверу приложений по протоколу HTTP (1). Само приложение находится на Web-сервере/сервере приложений, например, в виде Java-сервлета, модуля сервера Apache и т.д. На представленной выше схеме промежуточный сервер использует службу каталогов (directory service), доступную по протоколу LDAP, которой и передаются предоставленные пользователем реквизиты (2). Служба каталогов используется как средство аутентификации сеанса браузера. Если аутентификация прошла успешно, сервер приложений получает соответствующее уведомление (3) и, наконец, подключается к одной из нескольких баз данных (4) для получения данных и обработки транзакций. "Реквизиты", передаваемые из браузера серверу приложений на шаге (1), могут быть разного вида — имя пользователя/пароль, ключ с сервера однократной регистрации, некий цифровой сертификат — все, что угодно. Имя пользователя базы данных и его пароль, как правило, не передается. Проблема, конечно же, в том, что серверу приложений необходимо имя пользователя базы данных и пароль для аутентификации пользователя в используемой базе данных. Более того, комбинации имя пользователя/пароль в каждом случае будут разными. В рассмотренном выше примере имеется четыре базы данных: •
база данных ошибок, в которой надо регистрироваться, скажем, как TKYTE;
Q база данных затрат, в которой надо регистрироваться как TKYTE_US; Q база данных планировщика, в которой надо регистрироваться как WEBSTKYTE; •
и так далее...
Подумайте: сколько у вас имен пользователей и паролей в разных базах данных? Я могу вспомнить не меньше 15. Более того, хотя имена пользователей в базах данных я никогда не меняю, пароли изменяются достаточно часто. Правда, хорошо было бы аутен-
472
Глава 22
тифицироваться один раз — на сервере приложений — и обеспечить доступ сервера приложений ко всем необходимым базам данных от нашего имени (или по нашему заданию), не передавая пароли для каждой базы данных? Именно это и обеспечивает многоуровневая аутентификация. На уровне базы данных для этого достаточно задать опцию подключения. В Oracle 8i оператор ALTER USER был изменен и поддерживает конструкцию GRANT CONNECT THROUGH (подробнее она рассматривается далее, в разделе "Предоставление привилегии"). Рассмотрим доступ к базе данных затрат в представленном ранее приложении:
ТомКайт/ пароль__ТомаКайта
j Сервер_приложений/ пароль как ТомКайт_иЭ
БД затрат
JBK'.
Web-браузер
Сервер приложений
Служба каталогов содержит информацию сопоставления, связывающую пользователя TomKyte с клиентом базы данных TKYTE_US. После успешного получения этой информации сервер приложений (промежуточный сервер) может подключаться к базе данных со своими реквизитами от имени клиента базы данных, TKYTE_US. Серверу приложений при этом пароль пользователя TKYTE_US знать не надо. Чтобы это можно было сделать, администратор базы данных затрат должен предоставить учетной записи APP_SERVER право подключаться от имени клиента: a l t e r user tkyte_us grant connect through app_server Сервер приложений будет работать в базе данных от имени и с привилегиями пользователя TKYTE_US, как если бы пользователь TKYTE_US зарегистрировался в базе данных непосредственно. Таким образом, сервер Oracle 8i расширяет модель защиты настолько, что сервер приложений может безопасно работать от имени клиента, не требуя от него пароля соответствующей учетной записи в базе данных и не запрашивая многочисленные привилегии для доступа к объектам или процедурам, которые ему непосредственно не нужны. Кроме того, система аудита также была расширена и включает действия, выполняемые сервером приложений от имени клиента. Другими словами, мы можем узнать, выполнил ли сервер приложений определенное действие от имени клиента (подробнее об этом см. в разделе "Аудит промежуточных учетных записей").
Многоуровневая аутентификация
4 / 3
Теперь перейдем к обсуждению реализации этих возможностей. Как упоминалось во введении, это средство в настоящее время* доступно только для программ на языках С и C++, использующих средства Oracle Call Interface.
Механизм многоуровневой аутентификации В этом разделе одна из стандартных демонстрационных программ OCI, реализующая MHHH-SQL*P1US, будет изменена для использования промежуточной аутентификации при подключении к базе данных. В результате получится небольшое, интерактивное средство выполнения SQL-операторов, которое позволит выяснить, как работает многоуровневая аутентификация и какие побочные эффекты при этом возникают. В качестве приза вы получите инструментальное средство, которое при наличии соответствующих привилегий позволит зарегистрироваться от имени другого пользователя и выполнять действия, регистрируемые системой аудита как ваши. Это средство можно, например, использовать для предоставления привилегии SELECT на таблицу другого пользователя, не зная его пароля. В демонстрационную программу cdemo2.c (которая находится в каталоге [ORACLE_HOME]\rdbms\demo) придется добавить только другую процедуру регистрации. После этого мы сможем зарегистрироваться как пользователь SCOTT, используя аутентификацию операционной системой, и предоставить роль CONNECT: С:\> cdemo2 / scott CONNECT Можно также, например, зарегистрироваться, указав имя пользователя и пароль в удаленной по отношению к учетной записи SCOTT базе данных, и предоставить роли RESOURCE и PLUSTRACE: С:\> cdemo2 user/pass@database s c o t t RESOURCE,PLUSTRACE Для того чтобы показать, как регистрироваться с помощью механизма многоуровневой аутентификации, придется создать С-функцию — эту часть программы мы рассмотрим детально. Остальная же часть приложения — обычный OCI-код, ничем не отличающийся от любой программы, использующей библиотеку OCI. В начало кода включен стандартный заголовочный файл oci.h, находящийся в каталоге ORACLE_HOME]\rdbms\demo. Этот файл содержит необходимые прототипы функций и макросов для всех OCI-программ. Затем объявляются локальные переменные для подпрограммы регистрации. Используются обычные дескрипторы подключений ОС1, но обратите внимание на два дескриптора OCISession: один — для учетной записи, реквизиты которой будут передаваться (от имени которой выполняется регистрация), а второй — для учетной записи, от имени которой мы будем работать. Назначение остальных локальных переменных понятно из имен — они содержат имя пользователя, пароль, имя базы данных и все роли, которые мы хотим предоставить: •include void checkerr(OCIError * errhp, sword status); * в Oracle Si - Прим. научн. ред.
474
Глава 22
Lda_Def connect8i(int argc, char * argv[]) t ОСIEnv OCIServer OCIError
* envi ronment_handle; *data_server_handle; *error_handle;
OCISvcCtx *application_server_service_handle; OCISession *first_client_session_handle; OCISession *application_server_session_handle; char char char char char char
int
*username; •password; •database; temp[255] ; role buffer[1024]; •roles[255]; nroles;
Проверим допустимость переданных функции аргументов командной строки. Если передано не четыре аргумента, значит, переданной информации недостаточно, т.е. просто выдается сообщение о правильном использовании, и работа завершается. В противном случае мы анализируем (с помощью стандартной С-функции strtok) переданные аргументы. Поскольку вызов strtok — деструктивный (он изменяет переданную функции строку), перед анализом аргументов мы копируем их в локальные переменные: if
(argc != 4)
{
printf ("usage: %s proxy_user/proxy_jpass real_account_name rolel, . . .\n", argv[0]); printf(" proxy_user/proxy_pass can just be A n " ) ; printf(" real_account_name is what you want to connect to\n"); exit(1); }
strcpy(temp, a r g v f l ] ) ; username = strtok(temp, " / " ) ; password = strtok(NULL, "@"); database = strtok(NULL, " " ) ; strcpy( role_buffer, argv[3] ) ; for (nroles = 0, r o l e s [ n r o l e s ] = s t r t o k ( r o l e _ b u f f e r , " , " ) ; r o l e s [ n r o l e s ] != NULL; nroles++, r o l e s f n r o l e s ] - strtok(NULL,",")); Теперь выполняем общую инициализацию и выделение контекстов. Это стандартные действия для всех OCI-программ: OCIInitialize(OCI_DEFAULT, NULL, NULL, NULL, NULL); OCIEnvInit(&environment handle, OCI DEFAULT, 0, NULL); OCIHandleAlloc((dvoid *) environment_handle, (dvoid **) &error_handle, OCI_HTYPE_ERROR, 0, NULL); Затем выделяем и инициализируем контексты сервера и службы, используемые "сервером приложений". В данном случае сервером приложений является демонстрацион-
Многоуровневая аутентификация
4 7 5
ная программа cdemo2 — реализация SQL*Plus в миниатюре. Этот код выполняет подключение к серверу, но не начинает сеанс: checkerr(error_handle, OCIHandleAlloc(environment_handle, (dvoid **)&data_server_handle, OCI_HTYPE_SERVER, 0, NULL) ); checkerr(error_handle, OCIHandleAlloc((dvoid *) environment_handle, (dvoid **) &application_server_service_handle, OCI_HTYPE_SVCCTX, 0, NULL) ); checkerr(error_handle, OCIServerAttach(data_server_handle, error_handle, ( t e x t *)database?database:"", s t r l e n ( d a t a b a s e ? d a t a b a s e : " " ) , 0) ); checkerr(error_handle, OCIAttrSet((dvoid *) application_server_service_handle, OCI_HTYPE_SVCCTX, (dvoid *) data_server_handle, (ub4) 0, OCI_ATTR_SERVER, error_handle) ); Теперь можно инициализировать, а затем аутентифицировать дескриптор сеанса сервера приложений. В данном случае используется либо внешняя аутентификация, либо имя пользователя/пароль: checkerr(error_handle, OCIHandleAlloc((dvoid *) environment_handle, (dvoid **)&application_server_session_handle, (Ub4) OCI_HTYPE_SESSION, (size_t) 0, (dvoid **) 0) ); Инициализировав дескриптор сеанса, мы должны передать информацию для аутентификации. Можно разрешить либо аутентификацию операционной системой (которая не потребует от сервера приложений передавать имена пользователей и пароли на сервер), либо стандартную аутентификацию по имени пользователя и паролю. Вот код для аутентификации по имени пользователя и паролю: if
(username ! = NULL && password != NULL && *username && *password) { checkerr (error__handle, OCIAttrSet((dvoid *) application_server_session_handle,
476
Глава 22
(ub4) OCI_HTYPE_SESSION, (dvoid *) username, (ub4) strlen ((char *) usernarre), (ub4) OCI_ATTR_USERNAME, error_handle) ); checkerr( error_handle, OCIAttrSet((dvoid *) application_server_session_handle, (ub4) OCI_HTYPE_SESSION, (dvoid *) password, (ub4) strlen((char *)password), (ub4) OCI_ATTR_PASSWORD, error_handle) ); checkerr(error_handle, OCISessionBegin (application_server_service_handle, error_handle, application_server_session_handle, OCI_CRED_RDBMS, (ub4) OCI_DEFAULT)
А это — для выполнения аутентификации операционной системой: else {
checkerr(error_handle, OCISessionBegin(application_server_service_handle, error_handle, application_server_session_handle, OCI_CRED_EXT, OCI_DEFAULT) ); }
Теперь все готово для инициализации сеанса от имени клиента (пользователя, который регистрируется на сервере приложений, и от имени которого нам доверяют выполнять действия). Сначала инициализируем сеанс: checkerr(error_handle, OCIHandleAlloc((dvoid *) environment_handle, (dvoid **)&first_client_session_handle, (ub4) OCI_HTYPE_SESSION, (size_t) 0, (dvoid **) 0) ); Потом устанавливаем имя пользователя, от имени которого этот сеанс будет работать: checkerr(error_handle, OCIAttrSet((dvoid *) first_client_session_handle, (ub4) OCI_HTYPE_SESSION, (dvoid *) argv[2],
Многоуровневая аутентификация
(ub4) strlen(argv[2]), OCI_ATTR USERNAME, error_handle) ); Затем добавляем список ролей, которые необходимо включить для данного сеанса, — если пропустить этот вызов, будут включены все стандартные роли соответствующего пользователя: . . . . ,.; checkerr( error_handle, OCIAttrSet((dvoid *) first_client_session_handle, (ub4) OCI_HTYPE_SESSION, (dvoid *) roles, (ub4) nroles, OCI_ATTR_INITIAL_CLIENT_ROLES, error_handle) ),' Теперь все готово для начала реального сеанса. Сначала мы свяжем клиентский сеанс (от имени которого должны выполняться действия в базе данных) с сеансом сервера приложений (промежуточной учетной записью): checkerr(error_handle, OCIAttrSet((dvoid *) first_client_session_handle, (ub4) OCI_HTYPE_SESSION, (dvoid *) application_server_session_handle, (ub4) 0, OCI ATTR PROXY CREDENTIALS, error_handle) '' checkerr(error_handle, OCIAttrSet((dvoid *)application_server_service_handle, (ub4) OCI_HTYPE_SVCCTX, (dvoid *)first_client_session_handle, (ub4)0, (ub4 )OCI_ATTR_SESSION, error_handle) ); А затем начнем сеанс: checkerr(error_handle, OCISessionBegin(application_server_service_handle, error_handle, first_client_session_handle, OCI_CRED__PROXY, OCI DEFAULT) );
_
Теперь, поскольку это OCI-программа версии 7 (cdemo2.c — программа версии 7), необходимо преобразовать регистрационные данные Oracle 8i в форму, которую можно будет использовать. Здесь будет преобразована информация о подключении версии 8 в OCI LDA (Login Data Area — область данных регистрации) версии 7 и возвращен результат:
478
Глава 22
checkerr(error_handle, OCISvcCtxToLda(application_server_service_handle, error_handle, &lda ) ); return Ida; }
Последняя часть кода — функция checkerr, которую мы многократно использовали. Эта функция проверяет, что коды возврата OCI-функций свидетельствуют об успешном выполнении — в противном случае она выдает сообщение об ошибке и завершает работу программы: void checkerr(OCIError
* errhp, sword s t a t u s )
{
text sb4
errbuf[512]; errcode » 0;
switch (status) { case OCI_SUCCESS: break; case OCI_SUCCESS_WITH_INFO: (void) printf("Ошибка - OCI_SUCCESS_WITH_INFO\n"); break; case OCI_NEED_DATA: (void) printf("Ошибка - OCI_NEED_DATA\n"); break; case OCI_NO_DATA: (void) printf("Ошибка - OCI_NODATA\n"); break; case OCI_ERROR: (void) OCIErrorGet((dvoid *)errhp, (ub4) 1, (text *) NULL, Serrcode, errbuf, (ub4) sizeof(errbuf), OCI_HTYPE_ERROR); (void) printf("Ошибка - %.*s\n", 512, errbuf); exit(l); break; case OCI_INVALID_HANDLE: (void) printf("Ошибка - OCI_INVALID_HANDLE\n"); break; case OCI_STILL_EXECUTING: (void) printf("Ошибка - OCI_STILL_EXECUTING\n"); break; case OCI_CONTINUE: (void) printf("Ошибка - OCI_CONTINUE\n"); break; default: break;
Теперь осталось изменить файл cdemo2.c и включить в него необходимый код. Существующий код этой демонстрационной программы имеет вид:
Многоуровневая аутентификация
47,7
s t a t i c sword numwidth = 8; main() sword c o l , errno, n, ncols; t e x t *cp; /* Подключаемся к ORACLE. */ i f (connect__user ()) exit(-l); Изменение очень несложное — добавить код, выделенный
полужирным:
static sword numwidth = 8; Lda_Def connect8i(int argc, char * argv[]); main(int argc, char * argv[]) sword col, errno, n, ncols; text *cp; /* Подключаемся к ORACLE. */ /* if (connect_user()) exit(-l); */ Ida = connect8i( argc, argv ) ; Затем надо добавить весь представленный ранее код (функции connectSi и checkerr) в конец файла исходного кода. Сохраняем файл и компилируем. В ОС UNIX для компиляции надо выполнить следующую команду: $ make -f $ORACLE_HOME/rdbms/demo/demo_rdbms.mk cdemo2 В среде Windows NT я использовал следующий файл управления проектом makefile: CPU=i386 WORK_DIR
= Л
!include <\msdev\include\win32.mak> OBJDIR - $(WORK_DIR)\ EXEDIR = $(WORK_DIR)\ *•* . ехе-модули ORACLE_HOME = \oracle TARGET
# # каталог, в который будут помещаться все
- $(EXEDIR)cdemo2.exe
SAMPLEOBJS
- cdemo2.obj
LOCAL_DEFINE = -DWIN_NT SYSLIBS = \msdev\lib\msvcrt.lib \ \msdev\lib\oldnames.lib \ \msdev\lib\kernel32.1ib \
480
Глава 22 \msdev\lib\advapi32.1ib \msdev\lib\wsock32.lib
\
NTUSER32LIBS
SQLLIB
= \msdev\lib\user32.lib \ \msdev\lib\advapi32.lib \ \msdev\lib\libc.lib = $(ORACLE_HOME)\oci\lib\msvc\oci.lib
INCLS
= -I\msdev\include \ -1$(ORACLE_HOME)\oci\include CFLAGS = $(cdebug) $(cflags) $(INCLS) $(LOCAL_DEFINE) LINKOPT = /nologo /subsystem:console /machine:1386 /nodefaultlib $(TARGET): $(SAMPLEOBJS) $(SQLLIB) $(link) $(LINKOPT) \ -out:$(TARGET) $(SAMPLEOBJS) \ $(NTUSER32LIBS) \ $(SYSLIBS) \ $(SQLLIB) А затем просто выполнял для компиляции команду шпаке: с:\oracle\rdbms\demo>nmake Теперь можно проверять работу программы: c:\oracle\rdbms\demo>cdemo2 tkyte/tkyte s c o t t connect,resource Ошибка — ORA-28150: proxy not authorized t o connect as c l i e n t Итак, пока еще не работает, но до успешного выполнения осталось совсем немного. Необходимо предоставить промежуточной учетной записи (TKYTE) право подключаться от имени клиента базы данных (SCOTT). Регистрируемся в SQL*Plus и выполняем: sys@TKYTE816> a l t e r user s c o t t grant connect through t k y t e ; User a l t e r e d . Подробно этот новый оператор со всеми опциями мы рассмотрим немного позже. Пока нам просто надо убедиться, что программа работает. Обратите внимание на выделенное полужирным приглашение — я работаю не в среде SQL*Plus. Это работает демонстрационная программа cdemo2, очень похожая на утилиту SQL*Plus: c:\oracle\rdbms\demo>cdemo2 tkyte/tkyte s c o t t connect,resource OCISQL> SELECT user, 2
substr(sys_context('userenv','proxy_user'),1,30)
FROM d u a l ;
USER SCOTT
SUBSTR(SYS_CONTEXT('USERENV','PRO TKYTE
1 row processed. OCISQL> select * from session_roles; ROLE CONNECT RESOURCE
Многоуровневая аутентификация
48 1
2 rows processed. OCISQL> select distinct authentication_type from v$session_cormect_info 2 where sid = (select sid from v$mystat where rownum =1); AUTHENTICATION TYPE PROXY 1 row processed. OCISQL> exit С:\oracle\RDBMS\demo> Вот и все. Мы успешно зарегистрировались от имени пользователя SCOTT, не зная его пароля. Кроме того, мы увидели, как можно проверить, что мы работаем через промежуточную учетную запись, сравнивая значение USER с атрибутом PROXY_USER, возвращаемым функцией SYS_CONTEXT, или просматривая информацию сеанса в представлении V$SESSION_CONNECT_INFO. Кроме того, явно видно, что включены роли CONNECT и RESOURCE. Если подключиться следующим образом: с:\oracle\rdbms\demo>cdemo2 tkyte/tkyte s c o t t connect OCISQL> s e l e c t * from
session_roles;
ROLE CONNECT
1 row processed. Видно, что включена роль CONNECT, — мы контролируем, какие роли включает "сервер приложений".
Предоставление привилегии Соответствующий оператор ALTER USER имеет следующий базовый синтаксис: Alter user <иыя пользователя> grant connect through <промежуточный пользовательХ,промежуточный п о л ь з о в а т е л е . . . Это дает возможность пользователям, перечисленным в списке промежуточных пользователей, подключаться от имени указанного после ALTER USER пользователя. По умолчанию для этих пользователей будут устанавливаться все роли данного пользователя. Другая разновидность этого оператора: A l t e r user <имя пользователя> grant connect through <промежуточный пользователе WITH NONE; позволяет промежуточной учетной записи подключаться от имени указанного пользователя, но только с ее базовыми привилегиями — роли включаться не будут. Кроме того, можно использовать: Alter user <имя пользователя> grant connect through <промежуточный пользователь> ROLE имя_роли,имя_роли,... или:
16 Зм. 244
482
Глава 22
Alter user <имя пользователя> grant connect through <промежуточный пользователе ROLE ALL EXCEPT имя_роли,имя_роли,... Два представленных выше оператора дают промежуточной учетной записи возможность подключаться в качестве пользователя, но при этом включены будут только определенные роли. Необязательно давать учетной записи сервера приложений все привилегии — достаточно предоставить роли, необходимые для выполнения его функций. По умолчанию сервер Oracle пытается включить все стандартные роли пользователя и роли PUBLIC. Вполне допустимо разрешить серверу приложений использовать только роль HR данного пользователя, и никакие другие прикладные роли этого пользователя. Разумеется, можно и отобрать соответствующую привилегию: Alter user <mm пользователя> REVOKE connect through <промежуточный пользовательХ,промежуточный пользователь^... Есть административное представление, PROXY_USERS, которое можно использовать для получения информации обо всех промежуточных учетных записях. После выполнения оператора ALTER user SCOTT GRANT CONNECT through tkyte в представлении PROXY_USERS будет: TKYTE@TKYTE816> s e l e c t * from proxy_users; PROXY
CLIENT
TKYTE
SCOTT
ROLE
FLAGS PROXY MAY ACTIVATE ALL CLIENT ROLES
Аудит промежуточных учетных записей Вот новый синтаксис оператора AUDIT для работы с промежуточными учетными записями: AUDIT <действие> BY <проиежуточный пользователь;*, <промежуточный п о л ь з о в а т е л е . . . ON BEHALF OF <клиент>, <клиент>..; или: AUDIT <действие> BY <проыежуточный пользователь:», <проыежуточный пользователь> ON BEHALF OF ANY; Новая часть — BY <промежуточный пользователь^.. ON BEHALF OF. Она позволяет явно проверять действия, выполняемые указанными промежуточными пользователями от имени некоторых или всех учетных записей. Предположим, администратор включил аудит, установив параметр инициализации AUDIT_TRAIL=TRUE и перезапустив экземпляр. Тогда можно использовать: sys@TKYTE816> audit connect by tkyte on behalf of s c o t t ; Audit succeeded. Теперь, если использовать для подключения измененную программу cdemo2.c: С:\oracle\RDBMS\demo>cdemo2 tkyte/tkyte s c o t t connect OCISQL> e x i t В таблице DBA_AUDIT_TRAIL я обнаружу следующее:
Многоуровневая аутентификация OS_USERNAME USERNAME USERHOST TERMINAL TIMESTAMP OWNER OBJ_NAME ACTION ACTION_NAME NEW_OWNER NEW_NAME OBJ_PRIVILEGE SYS_PRIVILEGE ADMIN_OPTION GRANTEE AUDIT_OPTION SES_ACTIONS LOGOFF_TIME LOGOFF_LREAD LOGOFF_PREAD LOGOFF_LWRITE LOGOFF_DLOCK COMMENTJTEXT SESSIONID ENTRYID STATEMENTID RETURNCODE PRIV USED
483
Thomas?Kyte SCOTT TKYTE-DELL 08-may-2001 19:19:29 101 LOGOFF
08-may-2001 19:19:30 23 0 б 0 Authenticated by: PROXY: TKYTE 8234 1 1 0 CREATE SESSION
Интересно отметить, что при подключении пользователя SCOTT или пользователя TKYTE с помощью утилиты SQL*Plus записи аудита не создаются. Аудит строго ограничен конструкцией: connect by tkyte on behalf of s c o t t ; При необходимости можно регистрировать подключение пользователей TKYTE или SCOTT, просто в данном случае я решил этого не делать. Этот пример демонстрирует, что средства многоуровневой аутентификации не противоречат учету действий в базе данных (можно определить, что именно пользователь SCOTT выполнил определенное действие), позволяя также определить, когда действие от имени пользователя SCOTT выполнил сервер приложений.
Проблемы Обычно многоуровневая аутентификация работает вполне предсказуемо. Если подключиться следующим образом: С:\oracle\RDBMS\demo>cdemo2 t k y t e / t k y t e s c o t t connect Это будет равносильно непосредственной регистрации пользователя SCOTT. Средства обеспечения работы с правами вызывающего и правами создателя (см. главу 23) функционируют так, как если бы зарегистрировался пользователь SCOTT. Средства деталь-
484
Глава 22
ного контроля доступа (см. главу 21) функционируют так, как если бы зарегистрировался пользователь SCOTT. Триггеры на событие регистрации пользователя SCOTT срабатывают. И так далее. Я не нашел ни одной возможности, которая была бы недоступна при использовании многоуровневой аутентификации. Есть, однако, одна деталь реализации, которая может привести к проблемам. При использовании многоуровневой аутентификации сервер будет автоматически включать набор ролей. Если вы используете атрибут OCI_ATTR_INITIAL_CLIENT_ROLES, как в представленном выше коде, то ожидаете, что в набор войдут только явно заданные роли. Однако всегда включаются также все роли, предоставленные роли PUBLIC. Например, если предоставить роль PLUSTRACE роли PUBLIC (роль PLUSTRACE позволяла использовать установку AUTOTRACE, которую мы постоянно применяли по ходу изложения в среде SQL*Plus для оценки производительности): sys@TKYTE816> grant p l u s t r a c e t o publicGrant succeeded. Теперь при подключении с помощью нашей утилиты MHHH-SQL*P1US: c:\oracle\rdbms\demo>cdemo2 t k y t e / t k y t e s c o t t connect OCISQL> s e l e c t * from
session_roles;
ROLE
CONNECT PLUSTRACE 2 rows processed. оказывается, что кроме роли CONNECT включена также роль PLUSTRACE. Сначала это не кажется опасным. Однако, если с помощью оператора ALTER USER явно потребовать предоставлять пользователю только строго ограниченный набор ролей: sys@TKYTE816> a l t e r user s c o t t grant connect through tkyte with r o l e *• CONNECT; User a l t e r e d . окажется, что: c:\oracle\rdbms\demo>cdemo2 tkyte/tkyte scott connect Ошибка — ORA-28156: Proxy user 'TKYTE1 not authorized to set role 1 'PLUSTRACE for client 'SCOTT' пользователю TKYTE не разрешено включать эту роль при подключении от имени пользователя SCOTT. Решить эту проблему можно только так: 1. не предоставлять ролей роли PUBLIC; 2. всегда добавлять соответствующие роли к списку ролей в операторе ALTER USER. Например, если выполнить: sys@TKYTE816> a l t e r user s c o t t grant connect through tkyte with r o l e 2 connect, p l u s t r a c e ; User a l t e r e d .
Многоуровневая аутентификация
485
следующая команда сработает, как и предполагалось: c:\oracle\rdbms\demo>cdemo2 t k y t e / t k y t e s c o t t connect OCISQL> s e l e c t * from session_roles; ROLE CONNECT PLUSTRACE
Резюме В этой главе мы изучили возможности многоуровневой, или промежуточной, аутентификации, которые доступны при программировании с использованием библиотеки OCI. Многоуровневая аутентификация позволяет серверу приложений промежуточного уровня действовать в качестве пользующегося доверием агента в базе данных от имени известного приложению клиента. Мы рассмотрели, как сервер Oracle позволяет ограничить набор ролей, доступных промежуточной учетной записи сервера приложений, чтобы от имени промежуточной учетной записи можно было выполнять только ограниченный набор действий, необходимых приложению. Далее мы рассмотрели систему аудита, которая теперь поддерживает использование многоуровневой аутентификации. Можно регистрировать действия, выполняемые промежуточными учетными записями от имени любого указанного пользователя или от имени всех пользователей. При этом всегда легко определить, когда данный пользователь выполнил действие через промежуточную учетную запись, а когда — непосредственно. Изменив одну демонстрационную программу Oracle, мы получили простую среду, напоминающую SQL*Plus, для тестирования возможностей многоуровневой аутентификации. Эта среда идеально подходит для тестирования различных особенностей многоуровневой аутентификации и позволяет изучать их в интерактивном режиме.
шщш
'
Права вызывающего и создателя Для начала представим ряд определений, чтобы гарантировать однозначное понимание терминов вызывающий и создатель: Q Создатель. Пользователь, создавший скомпилированный хранимый объект и владеющий им. (Говорят также, что объект находится в схеме пользователя.) К скомпилированным хранимым объектам относятся пакеты, процедуры, функции, триггеры и представления. • Вызывающий. Пользователь, с привилегиями которого работает текущий сеанс. Это может быть текущий зарегистрированный пользователь, но может быть и другой пользователь. До версии Oracle 8i ми создателя объекта. шение имен. Другими владельцу (создателю)
все скомпилированные хранимые объекты выполнялись с праваПо отношению к соответствующей схеме происходило и разресловами, набор привилегий, непосредственно предоставленных объекта использовался при компиляции для определения того:
•
к каким объектам (таблицам и т.д.) фактически обращаться;
•
есть ли у создателя необходимые для доступа к этим объектам привилегии.
Это статическое связывание, выполняемое на этапе компиляции, распространяется так далеко, что учитываются только непосредственно предоставленные создателю привилегии (другими словами, в ходе компиляции и выполнения хранимой процедуры роли не учитываются). Кроме того, при выполнении процедуры с правами создателя, она будет работать с базовым набором привилегий создателя, а не вызвавшего процедуру.
488
Глава 23
Начиная с Oracle 8i появилась возможность выполнять процедуры с правами вызывающего, что позволяет создавать процедуры, функции и пакеты, выполняющиеся с набором привилегий вызывающего, а не создателя. В этой главе мы рассмотрим: •
когда следует использовать процедуры с правами вызывающего, в том числе, для создания приложений, работающих со словарем данных, универсальных объектных типов и реализации собственных средств контроля доступа;
•
когда следует использовать процедуры с правами создателя, с учетом их масштабируемости по сравнению с процедурами, работающими с правами вызывающего, а также возможностей по реализации защиты данных;
•
как работают хранимые процедуры этих двух типов;
•
проблемы, которые необходимо учитывать при выполнении процедур с правами вызывающего, в частности использование разделяемого пула, производительность процедур, необходимость расширенных средств обработки ошибок в коде, а также использование языка Java для реализации процедур, работающих с правами вызывающего.
Пример Возможность выполнять код с правами вызывающего позволяет, например, создать хранимую процедуру, работающую с набором привилегий пользователя, который ее выполняет. В результате хранимая процедура может работать правильно и корректно для одного пользователя (который имеет доступ ко всем необходимым объектам), но не работать для другого (у которого такого доступа нет). Причина состоит в том, что доступ к базовым объектам проверяется не во время компиляции, а во время выполнения (правда, создатель должен иметь доступ к соответствующим объектам или хотя бы к объектам с такими же именами, чтобы PL/SQL-код вообще можно было скомпилировать). Подобный доступ во время выполнения осуществляется с учетом привилегий и ролей текущего пользователя/схемы. Следует отметить, что работа с правами вызывающего не поддерживается для представлений и триггеров. Представления и триггеры создаются и работают только с правами создателя. Возможность работы с правами вызывающего легко реализовать и проверить, поскольку для этого необходимо добавить к процедуре или пакету всего одну строку. Рассмотрим, например, следующую процедуру, выдающую значения атрибутов контекста: Q CURRENT_USER. Имя пользователя, с привилегиями которого работает сеанс. Q SESSION_USER. Имя пользователя, зарегистрировавшегося и первоначально создавшего этот сеанс. Это значение в течение сеанса неизменно. •
CURRENT_SCHEMA. Имя стандартной схемы, которая будет использоваться при разрешении неуточненных ссылок на объекты.
Для создания процедуры, работающей с правами создателя, необходим следующий код: tkyte@TKYTE816> c r e a t e or replace procedure definer_proc 2 as
Права вызывающего и создателя
3 begin 4 for x in 5 (select sys_context('userenv', 'current_user') current_user, 6 sys_context('userenv', 'session_user') session_user, 7 sys_context('userenv', 'current_schema') current_schema 8 from dual) 9 loop 10 dbms_output.put_line('Current User: ' || x.current_user); 11 dbms_output.put_line('Session User: ' || x.session_user); 12 dbms_output.put_line('Current Schema: ' || x.current_senema) ; 13 end loop; 14 end; 15 / Procedure created. tkyte@TKYTE816> grant execute on definerjproc to scott; Grant succeeded. Для создания такой же процедуры, работающей с правами вызывающего, код надо немного изменить: tkyte@TKYTE816> c r e a t e or replace procedure invoker_proc 2 AOTHID CURRENT USER — 3 as 4 begin 5 for x in 6 (select sys_context('userenv', 'current_user') current_user, 7 sys_context('userenv', 'session_user') session_user, 8 sys_context('userenv', 'current_schema') current_senema 9 from dual) 10 loop 11 dbms_output.put__line('Current User: ' || x.current_user); 12 dbms_output.put_line('Session User: • || x.session_user); 13 dbms_output.put_line( 'Current Schema: ' || x.current_schema); * 14 end loop; 15 end; 16 / Procedure created. tkyte@TKYTE816> grant execute on invoker_proc to scott; Grant succeeded. Вот и все; добавили одну строку, и процедура теперь будет выполняться с привилегиями и в пространстве имен вызывающего пользователя, а не создателя. Чтобы глубже понять, что это означает, выполним представленные выше процедуры и сравним выдаваемые ими результаты. Сначала процедура, работающая с правами создателя: tkyte@TKYTE816> connect s c o t t / t i g e r scott@TKYTE816> Current User: Session User: Current Schema:
exec tkyte.definer_j?roc TKYTE SCOTT TKYTE
PL/SQL procedure successfully completed.
490
Глава 23
Внутри процедуры, работающей с правами создателя, текущий пользователь и схема, с привилегиями которой работает сеанс — TKYTE. Пользователь, зарегистрировавшийся и начавший сеанс — SCOTT. Это значение в течение сеанса не меняется. При этом все неуточненные ссылки будут разрешаться в схеме TKYTE (например, запрос SELECT * FROM T будет разрешаться как SELECT * FROM TKYTE.T). Процедура, работающая с правами вызывающего, дает совсем другие результаты: scott@TKYTE816> Current User: Session User: Current Schema:
exec tkyte.invoker_proc SCOTT SCOTT SCOTT
PL/SQL procedure successfully completed. Текущий пользователь — SCOTT, а не TKYTE. Текущий пользователь в такой процедуре совпадает с пользователем, непосредственно выполняющим эту процедуру. Пользователь, зарегистрировавшийся и начавший сеанс — SCOTT, как и ожидалось. Текущая схема, однако, — тоже SCOTT, поэтому запрос SELECT * FROM T будет выполняться как SELECT * FROM SCOTT.T. Это показывает фундаментальное отличие процедур, работающих с правами вызывающего: процедура работает с правами пользователя, который ее вызвал. Кроме того, текущая схема также зависит от вызывающего. При выполнении этой процедуры разными пользователями она может обращаться к различным объектам. Интересно, как повлияет на эти процедуры явное изменение текущей схемы: scott@TKYTE816> a l t e r session s e t current_senema = system; Session a l t e r e d . scott@TKYTE816> Current User: Session User: Current Schema:
exec tkyte.definer_proc TKYTE SCOTT TKYTE
PL/SQL procedure successfully completed. scott@TKYTE816> Current User: Session User: Current Schema:
exec tkyte.invoker_j?roc SCOTT SCOTT SYSTEM
PL/SQL procedure successfully completed. Как видите, результаты выполнения процедуры с правами создателя не изменились. В таких процедурах текущий пользователь и текущая схема "статичны". Они жестко задаются при компиляции и не меняются в дальнейшем при изменении текущей среды. Процедура, работающая с правами вызывающего, более динамична. Текущий пользователь устанавливается во время выполнения, а текущая схема может меняться для каждого вызова, даже в пределах одного сеанса. Это очень мощное средство (при правильном и уместном использовании). Оно позволяет реализовать для хранимых процедур и пакетов PL/SQL поведение, более свойственное приложениям, написанным на Рго*С. Приложение, использующее Рго*С (или интерфейсы ODBC, JDBC — в общем, любое клиентское приложение на обычных про-
Права вызывающего и создателя
49 1
цедурных языках программирования), выполняется с привилегиями текущего зарегистрированного пользователя (вызывающего) и разрешением имен в его схеме. Теперь можно писать на PL/SQL код, который ранее приходилось писать на обычных языках программирования вне базы данных.
Когда использовать права вызывающего В этом разделе мы рассмотрим различные причины и случаи, когда может потребоваться выполнение с правами вызывающего. Мы сконцентрируемся на правах вызывающего, поскольку это новая возможность, пока еще являющаяся исключением. Хранимые процедуры ранее всегда выполнялись сервером Oracle с правами создателя. Необходимость работать с правами вызывающего чаще всего возникает, когда универсальный фрагмент кода создается одним пользователем, а используется — множеством других. Разработчик не имеет доступа к объектам, к которым будут иметь доступ пользователи. Именно привилегии пользователя будут определять, к каким объектам этот код может обращаться. Другая потенциальная причина использования прав вызывающего — необходимость создать набор процедур, централизованно выбирающих данные из нескольких различных схем. При использовании процедур, работающих с правами создателя, как было показано, привилегии и схема, относительно которой выполняется разрешение имен, — статичны и определяются во время компиляции. При каждом выполнении процедура, работающая с правами создателя, обращается к одному и тому же набору объектов (если, конечно, не используется динамическое формирование SQLоператоров). Работа с правами вызывающего позволяет создать процедуру, способную обращаться к аналогичным структурам в различных схемах, — в зависимости от того, кто ее вызвал. Давайте рассмотрим ряд типичных случаев, когда используются процедуры с правами вызывающего.
Разработка универсальных утилит Пусть создается хранимая процедура, использующая динамический SQL для выполнения запроса и выдачи результатов в виде файла со значениями через запятую. Если не работать с правами вызывающего, эта процедура будет универсальной и полезной всем только при выполнении одного из следующих условий. •
Создатель процедуры должен иметь возможность читать любой объект в базе данных. Например, обладать привилегией SELECT ANY TABLE. В противном случае при запуске этой процедуры для представления данных таблицы в виде текстового файла произойдет ошибка, потому что создатель не имеет необходимой привилегии SELECT для этой таблицы. Надо выполнять эту процедуру с правами вызывающего, а не создателя.
•
Каждый пользователь должен иметь исходный код и устанавливать его копию в своей схеме. Это нежелательно по очевидным причинам — сопровождение превратится в кошмар. Если будет обнаружена ошибка в исходном коде или вследствие обновления версии придется изменять код, обновлять придется десятки
492
Глава 23 копий. Кроме того, эта скопированная процедура все равно не сможет обращаться к объектам, доступным пользователю через роль.
Обычно именно второй вариант чаще всего применяется для разработки универсального кода. Этот подход неидеален, но более "безопасен" с точки зрения защиты данных. Используя работу с правами вызывающего, можно создать процедуру один раз, предоставить права на ее выполнение многим пользователям, и они будут использовать ее со своим набором привилегий и с разрешением имен в своих схемах. Давайте рассмотрим небольшой пример. Мне часто приходится просматривать в среде SQL*Plus слишком "широкие" таблицы, имеющие много столбцов. Если просто выполнить SELECT * FROM Т для такой таблицы, утилита SQL*Plus будет переносить данные на следующую строку по правому краю окна терминала. Например: tkyte@DEV816> s e l e c t * from dba_tablespaces where rownum = 1; TABLESPACE_NAME MAX_EXTENTS
INITIAL_EXTENT NEXT_EXTENT MIN_EXTENTS
PCT_INCREASE MINJEXTLEN STATUS
CONTENTS
LOGGING
EXTENT MAN ALLOCATIO PLU SYSTEM 505 DICTIONARY USER
50 NO
16384 16384 0 ONLINE PERMANENT LOGGING
1
Полученные данные читать очень неудобно. Вот если бы получать результаты в следующем виде: tkyte@DEV816> exec p r i n t _ t a b l e ( ' s e l e c t * from dba_tablespaces where ** rownum = 1' ) ; TABLESPACE_NAME INITIAL_EXTENT NEXT_EXTENT MIN_EXTENTS MAX_EXTENTS PCT_INCREASE MIN_EXTLEN STATUS CONTENTS LOGGING EXTENT_MANAGEMENT ALLOCATIONJFYPE PLUGGED IN
SYSTEM 16384 16384 1 505 50 0 ONLINE PERMANENT LOGGING DICTIONARY USER NO
PL/SQL procedure successfully completed.
Да, так гораздо лучше! Легко увидеть значение каждого столбца. Увидев результаты использования моей процедуры PRINT_TABLE, все хотят получить ее. Вместо того чтобы давать код, я предлагаю использовать мою, поскольку она создана с конструкцией AUTHID CURRENT_USER. Мне не нужен доступ к чужим таблицам. Эта процедура сможет обращаться к ним (даже к тем, которые доступны через роль, — процедуры,
Права вызывающего и создателя
493
работающие с правами создателя, не могут этого делать в принципе). Давайте рассмотрим код и разберемся, как он устроен. Начнем с создания служебной учетной записи для хранения этого универсального кода, а также учетной записи, которую мы будем использовать для проверки защиты: tkyte@TKYTE816> grant connect to another_user identified by another_user; Grant succeeded. tkyte@TKYTE816> create user utils_acct identified by utils_acct; User created. tkyte@TKYTE816> grant create session, create procedure to utils_acct; Grant succeeded. Я создал пользователя с очень ограниченными привилегиями. Их достаточно для того, чтобы зарегистрироваться и создать процедуру. Теперь я создам процедуру в этой схеме: tkyte@TKYTE816> utils_acct/utils_acct utils_acct@TKYTE816> create or replace 2 procedure print_table(p_query in varchar2) 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
AUTHID CURRENTJJSER is l_theCursor integer default dbms_sql.open_cursor; l_columnValue varchar2(4000); l_status integer; l_de s cTbl dbms_s ql.desc_tab; l_colCnt number; begin dbms_sql.parse(l_theCursor, p_query, dbms_sql.native); dbms_sql.describe_columns(l_theCursor, l_colCnt, l_descTbl); for i in 1 .. l_colCnt loop dbms_sql.define_column(l_theCursor, i, l_colurnnValue, 4000); end loop; l_status := dbms_sql.execute(l_theCursor); while (dbms_sql.fetch_rows(l_theCursor) > 0) loop for i in 1 .. l_colCnt loop dbms_sql.column_value(l_theCursor, i, l_columnValue); dbms_output.put_line(rpad(l_descTbl(i).col_name, 30) | | ' : • | | l_columnValue); end loop; dbms_output.put_line(' ') ; end loop; exception when others then dbms_sql.close_cursor(l_theCursor); RAISE; end;
494 34
Глава 23 /
Procedure created. utils_acct@TKYTE816> grant execute on print_table to publicGrant succeeded. Теперь я пойду на шаг дальше: сделаю так, чтобы от имени учетной записи UTILS_ACCT зарегистрироваться вообще было невозможно. Это предотвратит подбор пользователем пароля учетной записи UTILS_ACCT и размещение "троянского" кода под видом процедуры PRINT_TABLE. Конечно, администратор базы данных с соответствующими привилегиями сможет снова активизировать эту учетную запись и зарегистрироваться от имени UTILS_ACCT — этого предотвратить нельзя: utils_acct@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> revoke create session, create procedure 2 from u t i l s _ a c c t ; Revoke succeeded. Итак, имеется учетная запись, в схеме которой хранится код, но она по сути заблокирована, поскольку больше не имеет привилегии CREATE SESSION. При регистрации от имени пользователя SCOTT оказывается, что мы не только по-прежнему можем использовать эту процедуру (хотя учетная запись UTILS_ACCT лишена всех привилегий), но и обращаться к своим таблицам. Убедимся, что другие пользователи не могут использовать эту процедуру для доступа к нашим таблицам (если только они не сцелают это непосредственно, с помощью запроса), т.е. продемонстрируем, что процедура работает с привилегиями вызывающего: scott@TKYTE816> exec u t i l s _ a c c t . p r i n t _ t a b l e ( ' s e l e c t DEPTNO
:
DNAME LOC
: ACCOUNTING : NEW YORK
* from s c o t t . d e p t ' )
10
PL/SQL procedure successfully completed. Это показывает, что пользователь SCOTT может использовать процедуру, и она имеет доступ к объектам в схеме пользователя SCOTT. Однако при выполнении от имени пользователя ANOTHER_USER обнаруживается следующее: scott@TKYTE816> connect another_user/another_user another_user@TKYTE816>
desc s c o t t . d e p t
ERROR:
ORA-04043: object scott.dept does not exist another_user@TKYTE816> set serverout on another_user@TKYTE816> exec utils_acct.print_table('select * from *•• scott.dept'); BEGIN utils_acct.print_table('select * from scott.dept'); END; * ERROR at line 1: ORA-00942: table or view does not exist
Права вызывающего и создателя
4;/5
ORA-06512: a t "UTILS_ACCT.PRINT_TABLE", l i n e 31 ORA-06512: a t l i n e 1 Пользователь, не имеющий доступа к таблицам пользователя SCOTT, не сможет использовать процедуру для получения доступа к ним. Для полноты эксперимента снова зарегистрируемся как SCOTT и предоставим пользователю ANOTHER__USER соответствующую привилегию: another_user@TKYTE816> connect scott/tiger scott@TKYTE816> grant select on dept to another_user; Grant succeeded. scott@TKYTE816> connect another_user/another_user another_user@TKYTE816> exec utils_acct.print_table('select * from scott.dept'); DEPTNO
:
10
DNAME
:
ACCOUNTING
LOC
: NEW YORK
PL/SQL procedure successfully completed. Это демонстрирует практическое использование прав вызывающего в универсальных приложениях.
Приложения, работающие со словарем данных Разработчикам всегда хотелось создать процедуры, выдающие информацию из словаря данных в более удобном виде, чем можно получить с помощью простого оператора SELECT, или, например, средства получения операторов DDL. С помощью процедур, работающих с правами создателя, сделать это было очень сложно. Если используются представления USER_* (например, USER_TABLES), будет выдаваться информация об объектах, принадлежащих создателю процедуры, а не вызывающему. Дело в том, что в условиях всех представлений USER_* и ALL_* есть конструкция: where o.owner# = userenv('SCHEMAID') Функция USERENV('SCHEMAID') возвращает идентификатор пользователя для схемы, в которой выполняется процедура. В хранимой процедуре с правами создателя (т.е. в стандартной хранимой процедуре) это значение постоянно — это всегда будет идентификатор пользователя, в схеме которого создана процедура. Это означает, что если кто-то напишет процедуру, обращающуюся к словарю данных, эта процедура будет видеть его объекты, а не объекты пользователя, выполняющего запрос. Более того, в хранимой процедуре роли не используются (мы рассмотрим эту проблему чуть позже), так что, если доступ к таблице в схеме другого пользователя получен через роль, в хранимой процедуре эта таблица будет недоступна. Когда-то единственным решением было создание хранимой процедуры, обращающейся к представлениям DBA_* (после получения непосредственных привилегий для этого) и реализующей собственный механизм защиты, который гарантировал получение пользователями только той информации,
496
Глава 23
которая доступна им в представлениях ALL_* или USER_*. Это более чем нежелательно, поскольку приходится писать большой объем кода, предоставлять права доступа ко всем представлениям DBA_*; кроме того, при малейшей невнимательности процедура позволит получить несанкционированный доступ к объектам. Здесь поможет процедура, работающая с правами вызывающего. Теперь можно не только создать хранимую процедуру, обращающуюся к представлениям ALL_* и USER_*, — это можно делать от имени текущего зарегистрированного пользователя, с его привилегиями и даже ролями. Мы продемонстрируем это, реализовав "усовершенствованную" команду DESCRIBE. Это будет минимальная по возможностям реализация — разобравшись, как она работает, вы сможете добавить любые возможности: tkyte@TKYTE816> c r e a t e or replace 2 procedure desc_table(p_tname in varchar2) 3 AUTHID CURRENT USER 4 as 5 begin 6 dbms_output.put_line('Типы данных для таблицы ' || p_tname); 7 dbms_output.new_line ; 8 9 dbms_output.put_line(rpad('Имя столбца',31) || 10 rpad('Тип данных',20) |l 11 rpad('Длина',11) || 12 'Nullable'); 13 dbms_output.put_line(rpad('-',30, '-') II ' ' II 14 rpad('-',19,'-') II ' ' II 15 rpad('-',10,'-') || ' ' II 16 ' '); 17 for x in 18 (select column_name, 19 data_type, 20 substr( 21 decode(data_type, 22 'NUMBER', decode(data_j>recision, NULL, NULL, 23 ' C M data_precision I Г , ' | I data_scale II')'), 24 data_length),l,ll) data_length, 25 decode(nullable,'Y','null','not null') nullable 26 from user_tab_columns 27 where table_name = upper(p_tname) 28 order by column_id) 29 loop 30 dbms_output.put_line(rpad(x.column_name,31) II 31 rpad(x.data_type,20) || 32 rpad(x.data_length,ll) || 33 x.nullable); 34 end loop; 35 36 dbms_output.put_line(chr(10) I| chr(10) || 37 'Индексы по ' I I p_tname); 38 39 for z in 40 (select a.index_name, a.uniqueness
Права вызывающего и создателя
4 9 7
41 from user_indexes a 42 where a.table_name = upper(p_tname) 43 and ±ndex_type = 'NORMAL') 44 loop 45 dbms_output.put(rpad(z.index_name,31) II 46 z.uniqueness); 47 for у in 48 (select decode(column_position,1,'(',', ' ) | | 49 column_name column_name 50 from user_ind_columns b 51 where b.index_name = z.index_name 52 order by column_position) 53 loop • 54 dbms_output.put(у.column_name); 55 end loop; 56 dbms_output.put_line(')' |I chr(10)); 57 end loop; 58 59 end; 60 / Procedure created. tkyte@TKYTE816> grant execute on desc_table to public 2 / Grant succeeded. Эта процедура интенсивно обращается к представлениям USER_INDEXES и USER_IND_COLUMNS. При работе с правами создателя (без конструкции AUTHID CURRENT_USER) эта процедура сможет выдать информацию только для одного пользователя (и всегда — для одного и того же). Однако при использовании прав вызывающего процедура будет выполняться от имени и с привилегиями пользователя, зарегистрировавшегося во время выполнения. Так что, хотя процедура и принадлежит пользователю TKYTE, ее можно выполнять от имени пользователя SCOTT и получать результат, подобный следующему: tkyte@TKYTE816> connect s c o t t / t i g e r scott@TKYTE816> s e t serveroutput on format wrapped scott@TKYTE816> exec tkyte.desc_table('emp 1 ) Типы данных для таблицы emp Имя столбца
Тип данных
Длина
Nullable
EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
NUMBER VARCHAR2 VARCHAR2 NUMBER DATE NUMBER NUMBER NUMBER
(4,0) (4,0) 10 9 (4,0) 7 (7,2) (7,2) (2,0)
not null not null null null null null null null
498
Глава 23
Индексы по emp ЕМР_РК
UNIQUE(EMPNO)
PL/SQL p r o c e d u r e s u c c e s s f u l l y
completed.
Универсальные объектные типы Идея в данном случае та же, что и в предыдущем, но результаты могут быть более общезначимыми. С помощью средств Oracle 8, позволяющих создавать собственные объектные типы со специфическими методами обработки данных, можно создавать методы, работающие с набором привилегий текущего зарегистрированного пользователя. То есть, создав универсальные, обобщенные типы, их устанавливают в базе данных один раз и разрешают всем использовать. Без возможности работать с правами вызывающего владелец объектного типа должен был иметь очень мощные привилегии (как было описано ранее) или надо было устанавливать этот объектный тип в каждой схеме, где его предполагалось использовать. Именно с правами вызывающего всегда работали объектные типы, стандартно поставляемые в составе сервера Oracle (например, типы ORDSYS.*, используемые для поддержки компонентов interMedia), что позволяло устанавливать их в базе данных только один раз, а использовать — любому пользователю со своими привилегиями. Это имеет значение, потому что объектные типы ORDSYS выполняют чтение и запись в таблицы базы данных. Набор таблиц, к которым они обращаются, полностью зависит от того, кто именно выполняет соответствующие методы. Именно это свойство позволяет обеспечить универсальность и общедоступность этих типов. Они устанавливаются в схеме ORDSYS, но пользователь ORDSYS не имеет доступа к таблицам, с которыми они фактически работают. Теперь, используя Oracle 8i, разработчики приложений могут создавать такие же типы.
Реализация собственных средств контроля доступа В Oracle 8i появилась возможность детального контроля доступа (Fine Grained Access Control — FGAC), благодаря чему можно реализовать правила защиты, предотвращающие несанкционированный доступ к данным. Обычно для этого в каждую таблицу добавляется столбец, например COMPANY. Значения в этом столбце автоматически формируются триггером, а в каждый запрос включается условие WHERE COMPANY = SYS_CONTEXT (...), ограничивающее набор доступных пользователю строк только теми, доступ к которым авторизован (подробнее об этом см. в главе 21). Можно также создавать отдельную схему (набор таблиц) для каждой компании. Другими словами, для каждой компании устанавливается и наполняется данными свой набор таблиц. При этом в принципе невозможен доступ одного пользователя к данным другого, поскольку данные эти физически хранятся в другой таблице. Это — вполне уместный подход, имеющий преимущества (и недостатки) по сравнению со средствами детального контроля доступа. Проблема, однако, в том, что хотелось бы поддерживать один набор хранимого кода для всех пользователей. Кэширования же в разделяемом пуле
Права вызывающего и создателя
£\у у
десятка копий одного и того же большого PL/SQL-пакета желательно избежать. Не хотелось бы изменять десяток экземпляров одного и того же кода, если в нем будет найдена ошибка. Не хотелось бы, чтобы пользователи выполняли потенциально разные версии одного кода. Работа с правами вызывающего идеально поддерживает эту модель защиты (несколько наборов таблиц и один экземпляр кода). Имея возможность работать с правами вызывающего, можно написать одну хранимую процедуру, обращающуюся к таблицам с правами доступа текущего пользователя и разрешением имен в его схеме. Как было продемонстрировано в примере с процедурой PRINT_TABLE, это можно сделать как с помощью динамического, так и статического SQL. Рассмотрим следующий пример. Установим таблицы EMP/DEPT в схеме SCOTT и в моей схеме TKYTE. Третий пользователь будет создавать приложение, использующее таблицы ЕМР и DEPT для создания отчета; он не будет иметь доступа к таблицам ЕМР и DEPT ни в схеме SCOTT, ни в схеме TKYTE (его таблицы созданы для тестирования). Вы увидите, что процедура, выполненная пользователем SCOTT, будет выдавать данные из схемы SCOTT; когда же ее выполнит пользователь TKYTE, будут использованы таблицы последнего: tkyte@TKYTE816> connect scott/tiger scott@TKYTE816> grant select on emp to publicGrant succeeded. scott@TKYTE816> grant select on dept to publicGrant succeeded. scott@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> create table dept as select * from scott.dept; Table created. tkyte@TKYTE816> create table emp as select * from scott.emp; Table created. tkyte@TKYTE816> insert into emp select * from emp; 14 rows created. tkyte@TKYTE816> create user application identified by pw 2 default tablespace users quota unlimited on users; User created. tkyte@TKYTE816> grant create session, create table, 2 create procedure to application; Grant succeeded. tkyte@TKYTE816> connect application/pw application@TKYTE816> create table emp as select * from scott.emp where 1=0; Table created. application@TKYTE816> create table dept as 2 select * from scott.dept where 1=0; Table created.
500
Глава 23
Итак, имеются три пользователя, у каждого из которых собственная пара таблиц EMP/DEPT. Данные же в этих таблицах существенно отличаются. У пользователя SCOTT — обычный набор данных ЕМР, у пользователя TKYTE данных в два раза больше, а у пользователя APPLICATION эти таблицы пусты. Теперь создадим приложение: application@TKYTE816> c r e a t e or replace procedure emp_dept_rpt 2 AUTHID CURRENT_USER 3 as 4 begin 5 dbms_output.put_line('Зарплаты и количество сотрудников по *•* отделам') ; 6 dbms_output.put_line(chr(9)||'Отдел Зарплата Количество'); 7 dbms_output. put_line (chr (9) I Г •) ; 8 for x in (select dept.deptno, sum(sal) sal, count(*) cnt 9 from emp, dept • 10 where dept.deptno = emp.deptno 11 group by dept.deptno) 12 loop 13 dbms_output.put_line(chr(9) II 14 to_char(x.deptno,'99999') II ' ' II 15 to_char(x.sal,'99,999') II ' ' || 16 to_char(x.cnt,'99,999')); 17 end loop; 18 dbms_output. put_line (' ') ; 19 end; 20 / Procedure created. application@TKYTE816> grant execute on emp_dept_rpt to public 2 / Grant succeeded. application@TKYTE816> set serveroutput on format wrapped application@TKYTE816> exec emp_dept_rpt; Зарплаты и количество сотрудников по отделам Отдел Зарплата Количество
PL/SQL procedure successfully completed.
Когда процедуру выполняет пользователь APPLICATION, таблицы пусты, как и ожидалось. При выполнении же этого приложения пользователями S C O T T и TKYTE: tkyte@TKYTE816> connect scott/tiger scott@TKYTE816> set serveroutput on format wrapped scott@TKYTE816> exec application.emp_dept_rpt Зарплаты и количество сотрудников по отделам Отдел Зарплата Количество
10 20 30
8, 750
ю,875
9, 400
3 5 6
Права вызывающего и создателя
PL/SQL procedure successfully completed. scott@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> set serveroutput on format wrapped tkyte@TKYTE816> exec application.emp_dept_rpt Зарплаты и количество сотрудников по отделам Отдел Зарплата Количество 10 20 30
17, 500 21, 750 18, 800
6 10 12
PL/SQL procedure successfully completed. Как видите, процедура действительно обращается к разным таблицам в разных схемах. Тем не менее, как будет показано в разделе "Проблемы", надо позаботиться о синхронизации этих схем. Не только должны существовать таблицы с соответствующими именами, но типы данных, порядок и количество столбцов в них при использовании статического SQL тоже должны совпадать.
Когда использовать права создателя Работа с правами создателя продолжает оставаться доминирующим методом использования скомпилированных хранимых объектов. Для этого имеются две основные причины. •
Производительность. База данных, в которой используются процедуры с правами создателя, существенно лучше масштабируется и обеспечивает более высокую производительность, чем база данных, использующая процедуры с правами вызывающего.
Q Защита. Процедуры с правами создателя имеют такие особенности с точки зрения защиты, которые делают их применение единственно верным выбором почти во всех случаях.
Производительность и масштабируемость Процедура, работающая с правами создателя, — замечательная вещь с точки зрения защиты и производительности. В разделе "Как работают процедуры с правами вызывающего" будет показано, что, благодаря статическому связыванию на этапе компиляции, можно существенно повысить эффективность во время выполнения. Все проверки защиты, зависимостей и т.п. выполняются один раз, при компиляции. Для процедуры, работающей с правами вызывающего, большая часть этих действий будет делаться во время выполнения. Более того, может потребоваться многократно выполнять эти действия в одном сеансе, после выполнения оператора ALTER SESSION или SET ROLE. Любое изменение среды выполнения приводит к изменению поведения процедуры с правами вызывающего. Процедура же с правами создателя от этих изменений не зависит. Кроме того, как будет показано далее в разделе "Проблемы", процедура, работающая с правами вызывающего, более интенсивно использует разделяемый пул, чем ана-
502
Глава 23
логичная процедура, работающая с правами создателя. Поскольку среда выполнения для процедуры с правами создателя статична, все выполняемые ею статические SQL-операторы могут совместно использоваться в разделяемом пуле. Как было показано в других главах этой книги, необходимо заботиться о правильном использовании разделяемого пула (использовать связываемые переменные, избегать излишних разборов и т.д.). Использование процедур с правами создателя гарантирует максимально эффективное использование разделяемого пула. Процедуры же с правами вызывающего могут приводить к неэффективному использованию разделяемого пула. Вместо того чтобы один запрос, SELECT * FROM T, при использовании в процедуре означал бы одно и то же для всех пользователей, он может иметь для пользователей разный смысл. В разделяемом пуле будет больше различных SQL-операторов. Использование прав создателя обеспечивает более эффективное использование разделяемого пула.
Защита Используя права создателя, можно создать процедуру, безопасно и корректно обрабатывающую определенный набор объектов базы данных. Затем можно предоставить другим пользователям возможность выполнять эту процедуру с помощью оператора GRANT EXECUTE ON <процедура> ТО <пользователь>/риЬПс/<роль>. Эти пользователи смогут запускать процедуру для чтения/записи таблиц (способом, предусмотренным в коде этой процедуры), но никаким другим способом читать или записывать данные в наши таблицы они не могут. Другими словами, мы только что создали надежный процесс изменения или чтения объектов безопасным образом и теперь можем предоставлять пользователям право на это, не опасаясь, что они смогут каким-либо другим способом прочитать или изменить эти объекты. Они не смогут вставлять записи в таблицу сотрудников с помощью утилиты SQL*Plus. Это можно будет делать только с помощью хранимой процедуры, реализующей все необходимые проверки. Этот способ работы существенно влияет на разработку приложения и предоставление прав на использование ваших данных. Больше не придется выполнять операторы GRANT INSERT для таблицы, как это делалось для клиент-серверного приложения, непосредственно выполняющего SQL-операторы INSERT. Вместо этого придется выполнить оператор GRANT EXECUTE для процедуры, которая может проверять и оценивать данные и их защищенность. Беспокоиться о целостности данных при этом тоже больше не нужно (процедура точно задает, что и как необходимо делать, а других способов работы с данными просто нет). Сравните это с работой типичных клиент-серверных и даже многих 3-уровневых приложений. В клиент-серверном приложении операторы INSERT, UPDATE, DELETE и т.п. включены непосредственно в код клиентского приложения. Для работы этого приложения, пользователю необходимо предоставить привилегии INSERT, UPDATE и DELETE непосредственно для базовых таблиц. Теперь весь мир имеет доступ к базовым таблицам через любой интерфейс, способный взаимодействовать с СУБД Oracle. При использовании процедуры с правами создателя такой проблемы нет. Единственный способ изменения таблиц — с помощью надежной процедуры, которой вполне можно доверять. Это очень важное свойство.
Права вызывающего и создателя
5U3
Часто разработчики спрашивают: "Как сделать так, чтобы только мое приложение, myapp.exe, могло выполнять действие X в базе данных?". Например, надо, чтобы это приложение могло выполнять вставку в таблицу, но другие приложения этого сделать не могли. Единственно безопасный способ сделать это — поместить алгоритмы работы с данными приложения myapp.exe в базу данных, и никаких операторов INSERT, UPDATE, DELETE или SELECT в клиентском приложении. Разместив приложение непосредственно в базе данных, устранив необходимость выполнять вставку (или любые другие операции с таблицей) в клиентском приложении, вы добьетесь того, чтобы только одно приложение могло обращаться к данным. Размещая алгоритмы работы с базой данных приложения в ней самой, мы делаем из приложения просто еще одни уровень абстракции. Не имеет значения, как вызывается приложение (его компонент, работающий с базой данных) — из SQL*Plus, из графического интерфейса или из другого, еще нереализованного интерфейса, — в базе данных работает одно и то же приложение.
Как работают процедуры с правами вызывающего Именно тут возможно непонимание: когда и какие привилегии действуют. Прежде чем приступить к рассмотрению функционирования процедур с правами вызывающего, рассмотрим, как работают процедуры с правами создателя. Разобравшись в этом, мы рассмотрим, чем отличается работа процедур с правами вызывающего при различных условиях вызова.
Права создателя При использовании прав создателя хранимая процедура компилируется с учетом привилегий, непосредственно предоставленных пользователю, "владеющему" процедурой. Под "непосредственно предоставленными привилегиями" подразумеваются все объектные и системные привилегии, предоставленные пользователю или роли PUBLIC, но не другим ролям, которые предоставлены пользователю или роли PUBLIC. Короче, для процедур с правами создателя роли не учитываются и не используются, ни во время компиляции, ни при выполнении. Процедура компилируется только с учетом непосредственных привилегий. Это описано в руководстве Oracle Application Developer's Guide так:
Привилегии, необходимые для создания процедур и функций При создании отдельной процедуры или функции, спецификации или тела пакета должны выполняться следующие требования. Необходимо наличие системной привилегии CREATE PROCEDURE (для создания процедуры или пакета в своей схеме) или системной привилегии CREATE ANY PROCEDURE (для создания процедуры или пакета в другой пользовательской схеме). Внимание: Для успешной компиляции процедуры или пакета требуются следующие дополнительные привилегии: Q владелец процедуры или пакета должен явно получить необходимые объектные привилегии для всех объектов, на которые есть ссылки в коде;
506
Глава 23
роль PUBLIC выполнять UPDATE T непосредственно, без использования какихлибо ролей. О Устанавливается и поддерживается зависимость процедуры от объектов, на которые она ссылается. Если процедура выполняет оператор SELECT FROM T, регистрируется зависимость процедуры от таблицы Т. Если, например, создается процедура Р, пытающаяся выполнить оператор SELECT * FROM Т, компилятор сначала преобразует Т в полностью уточненное имя. Имя Т в базе данных неоднозначно — таких таблиц представлений может быть несколько. Чтобы выяснить, какую именно таблицу Т использовать, сервер Oracle применяет правила определения области действия. Вместо синонимов подставляются соответствующие базовые объекты, причем для каждого объекта указывается имя соответствующей схемы. Это разрешение имен выполняется для текущего зарегистрированного пользователя (создателя). Другими словами, сервер ищет объект Т у данного пользователя и использует его (при этом используются приватные синонимы пользователя), затем сервер ищет Т среди общедоступных синонимов и т.д. Определив объект, на который ссылается имя Т, сервер Oracle определяет, возможен ли доступ к этому объекту в нужном режиме. В данном случае, если объект Т принадлежит создателю процедуры или создатель непосредственно получил привилегию SELECT на объект Т (или эта привилегия была предоставлена роли PUBLIC), процедура будет скомпилирована. Если создатель процедуры не имеет доступа к объекту по имени Т благодаря непосредственно предоставленной привилегии, процедура Р не будет скомпилирована. Таким образом, когда объект (хранимая процедура, ссылающаяся на Т) компилируется, сервер Oracle выполняет все эти проверки. Если они "пройдены", сервер Oracle компилирует процедуру, сохраняет двоичный код процедуры и устанавливает зависимости между этой процедурой и объектом Т. Эта зависимость используется для проверки действительности процедуры в дальнейшем, если что-то произошедшее с объектом Т может потребовать перекомпиляции процедуры. Например, если позже мы выполним REVOKE SELECT ON T и отберем эту привилегию у владельца процедуры, сервер Oracle пометит все хранимые процедуры этого пользователя, зависящие от объекта Т и ссылающиеся на Т, как недействительные (INVALID). Если мы добавим столбец с помощью оператора ALTER T ADD ..., сервер Oracle сделает недействительными все зависящие от объекта Т процедуры. Это приведет к их автоматической перекомпиляции при следующем вызове. Интересно разобраться не только в том, что сохраняется, но и что не сохраняется при компиляции объекта. Сервер Oracle не сохраняет информацию о привилегии, необходимой для получения доступа к объекту Т. Мы знаем только, что процедура Р зависит от Т. Мы не знаем, почему получен доступ к объекту Т: Q потому что создателю процедуры была предоставлена соответствующая привилегия (GRANT SELECT ON T TO USER); а
потому что привилегия была предоставлена роли PUBLIC (GRANT SELECT ON T TO PUBLIC);
•
потому что пользователь имеет привилегию SELECT ANY TABLE.
Права вызывающего и создателя
507
Знать, что не сохраняется при компиляции, интересно потому, что, если одна из этих привилегий будет отобрана, процедура Р станет недействительной. Если при компиляции процедуры у пользователя были все три привилегии, лишение пользователя любой из них приведет к пометке процедуры как недействительной и принудительной перекомпиляции перед следующим выполнением. Теперь, когда процедура скомпилирована и хранится в базе данных, а все зависимости учтены, можно выполнять процедуру, точно зная, что представляет собой объект Т, и будучи уверенными, что он доступен. Если что-то способное повлиять на доступность объекта Т произойдет с самим объектом Т или с набором базовых привилегий создателя процедуры, процедура станет недействительной и ее придется перекомпилировать.
Права создателя и роли Теперь нам предстоит разобраться, почему при компиляции и выполнении хранимых процедур с правами создателя роли не учитываются. Сервер Oracle не хранит информацию о том, почему мы можем обращаться к объекту Т, — только то, что мы это можем. Любое изменение привилегий, которое может сделать невозможным обращение к объекту Т, приведет к пометке процедуры как недействительной и ее перекомпиляции. Если роли не учитываются, такое изменение привилегий может произойти только при выполнении операторов REVOKE SELECT ANY TABLE или REVOKE SELECT ON T для пользователя-создателя или роли PUBLIC. Если роли учитываются, набор операторов, которые могут сделать процедуру недействительной, существенно расширяется. Представьте на минуту, что роли позволяют обращаться к объектам из хранимых процедур. Тогда при изменении любой из ролей, лишении ее привилегии или роли (ведь роли можно предоставлять ролям), мы рискуем сделать недействительными множество процедур (даже тех, которые не использовали привилегии измененной роли). Рассмотрим результат лишения роли системной привилегии. Это будет аналогично лишению роли PUBLIC мощной системной привилегии (не делайте этого, просто представьте, а если уж хотите сделать, то в тестовой базе данных). Если роли PUBLIC была предоставлена привилегия SELECT ANY TABLE, в результате лишения ее этой привилегии будут помечены как недействительные практически все процедуры в базе данных. Если процедуры зависят от ролей, любая процедура в базе данных затрагивается даже наименьшими изменениями прав доступа. Поскольку одно из основных преимуществ хранимых процедур — однократная компиляция при многократном выполнении, это крайне негативно повлияет на производительность. Учтите также следующие особенности ролей. •
Роли могут быть нестандартными. Если я создам нестандартную роль, включу ее и скомпилирую процедуру, работа которой зависит от привилегий этой роли, по завершении сеанса у меня этой роли больше не будет. Станет ли при этом процедура недействительной? Почему? А почему — нет? Я легко могу обосновать оба варианта.
•
Роли могут быть защищены паролями. Если изменен пароль роли, надо ли перекомпилировать все объекты, которым эта роль может понадобиться? Мне эта роль может быть предоставлена, но, не зная ее нового пароля, я не смогу ею восполь-
508
Глава 23
зоваться. Будут ли по-прежнему доступны соответствующие привилегии? Почему — да или почему — нет? Есть аргументы и за, и против. Подведем итоги по использованию ролей в процедурах, работающих с правами создателя. Q Вы можете работать с тысячами или десятками тысяч пользователей. Они не создают хранимые объекты. Для управления всеми этими пользователям необходимы роли. Именно для этого и создавались роли. Q Вы можете использовать намного меньше схем приложений (в них и находятся хранимые объекты). Нужно точно знать, какие для них необходимы привилегии и почему. С точки зрения защиты это называется концепцией минимальных привилегий. Надо явно указать, какие привилегии нужны и зачем. Если унаследовано множество привилегий от ролей, добиться минимальности привилегий практически невозможно. При явном задании привилегий не возникают проблемы, поскольку количество схем приложений невелико (но количество их пользователей огромно).
•
Наличие непосредственной взаимосвязи между создателем и процедурой позволяет сделать базу данных намного эффективней. Мы перекомпилируем объекты только в случае необходимости. Это существенно повышает эффективность их работы.
Права вызывающего Между процедурами с правами вызывающего и процедурами с правами создателя (и анонимными блоками PL/SQL) есть существенное отличие с точки зрения использования привилегий и разрешения ссылок на объекты. Что касается выполнения SQL-операторов, процедуры с правами вызывающего подобны анонимному блоку PL/SQL, но PL/SQL-операторы выполняются в них так же, как в процедурах с правами создателя. Кроме того, роли могут учитываться в процедуре с правами вызывающего, в зависимости от того, как к ней обращаются (в отличие от процедуры с правами создателя, которая игнорирует роли при доступе к объектам). Рассмотрим две части процедур, работающих с правами вызывающего: •
"SQL-части" — все операторы SELECT, INSERT, UPDATE, DELETE и все операторы, динамически выполняемые с помощью DBMS_SQL или EXECUTE IMMEDIATE (включая динамически выполняемый PL/SQL-код);
• "PL/SQL-части" — статические ссылки на объектные типы в объявлениях переменных, вызовы хранимых процедур, пакетов, функций и т.п. В процедурах с правами вызывающего обработка этих "частей" очень отличается. Имена в "SQL-части" разрешаются при компиляции (для определения структур данных и т.п.) и еще раз — при выполнении. Именно это позволяет хранимой процедуре с запросом SELECT * FROM EMP обращаться к другим таблицам ЕМР при выполнении другими пользователями. Однако "PL/SQL-части" при компиляции связываются статически, как
Права вызывающего и создателя
509
и в процедуре с правами создателя. Поэтому, если в процедуре с правами вызывающего имеется следующий код: AUTHID CURRENT_USER as begin for x in (select * from T) loop proc(x.cl); end loop; то ссылки на Т будут разрешаться во время выполнения (как и во время компиляции, чтобы понять, что означает SELECT *) динамически, что позволяет использовать разные объекты Т для каждого пользователя. Ссылка на процедуру PROC, однако, будет разрешена только при компиляции, поэтому процедура будет статически связана с одной процедурой PROC. Пользователю, вызывающему эту процедуру, не нужна привилегия EXECUTE ON PROC, но вот привилегия SELECT для объекта Т нужна. Не хочется вас запутывать, но если необходимо разрешать вызов PROC при выполнении, есть механизм и для этого. Можно написать следующий код: AUTHID CURRENTJJSER
as begin
for x in (select * from T) loop execute immediate 'begin proc(:x); end;' USING x.cl; end loop; В этом случае ссылка на процедуру PROC будут разрешаться на основе набора привилегий вызывающего, и этим вызывающим (или соответствующим ролям, если действуют роли) необходимо предоставить привилегию EXECUTE.
Разрешение ссылок и передача привилегий Давайте рассмотрим, как проверяются привилегии в процедуре с правами вызывающего. Для этого придется рассмотреть различные среды, или стеки вызовов, в которых может вызываться процедура: а
непосредственный вызов пользователем;
•
вызов из процедуры с правами создателя;
Q вызов из другой процедуры с правами вызывающего; Q вызов из SQL-оператора; а
вызов из представления, ссылающегося на процедуру с правами вызывающего;
Q вызов из триггера. Для одной и той же процедуры результат в каждой из перечисленных сред может отличаться. В каждом из этих случаев процедура с правами вызывающего может обращаться при выполнении к другим таблицам и объектам базы данных.
510
Глава 23
Начнем с изучения того, как связываются объекты и какие привилегии доступны в процедуре с правами вызывающего при выполнении в каждой из перечисленных сред. Случаи представления и триггера будем считать одинаковыми, поскольку там процедура может работать только с правами создателя. Кроме того, поскольку статические объекты PL/SQL всегда, во всех средах разрешаются во время выполнения, мы их рассматривать не будем. Они всегда разрешаются в схеме и с привилегиями создателя. Текущему зарегистрированному пользователю не нужен доступ к объекту PL/SQL, на который делается ссылка. В следующей таблице описано предполагаемое поведение в каждой из сред: Среда
SQL-объекты и динамически вызываемый PL/SQL
Роли действуют ?
Непосредственный вызов пользователем. Например: SQL> exec p;
Ссылки на эти объекты разрешаются в стандартной схеме и со стандартными привилегиями текущего пользователя. Неуточненные ссылки на объекты будут разрешаться в их схеме. Все объекты должны быть доступны текущему зарегистрированному пользователю. Если процедура выполняет SELECT из Т, текущий пользователь также должен иметь привилегию SELECT для объекта Т (полученную либо непосредственно, либо через роль).
Да. Все роли, включенные до выполнения процедуры, действуют и в процедуре. Они будут использоваться для разрешения или запрещения доступа ко всем SQL-объектам и динамически вызываемому PL/SQL-коду.
Вызов из процедуры с правами создателя (Р1), где Р2 процедура с правами вызывающего. Например: procedure p i is begin Р2; end;
Этот вызов разрешается в схеме создателя, в схеме вызывающей процедуры. Неуточненные имена объектов будут разрешаться в этой схеме, схеме вызывающей процедуры, а не в схеме текущего зарегистрированного пользователя и не в схеме, где была создана процедура с правами вызывающего. В нашем примере, владелец Р1 всегда будет "вызывающим" внутри процедуры Р2.
Нет. Роли не учитываются, поскольку вызвана процедура с правами создателя. В момент входа в процедуру с правами создателя все роли отключаются и не учитываются до выхода из этой процедуры.
Вызов из другой процедуры с правами вызывающего.
Аналогично непосредственному вызову пользователем.
Да. Точно так же, как и при вызове пользователем.
Вызов из SQL-оператора.
Аналогично непосредственному вызову пользователем.
Да. Точно так же, как и при вызове пользователем.
Права вызывающего и создателя
Среда
SQL-объекты и динамически вызываемый PL/SQL
Вызов из Аналогично вызову из процедуры представления с правами создателя. или триггера, ссылающегося на процедуру с правами вызывающего.
5 1
Роли действуют? Нет. Точно так же, как и при вызове из процедуры с правами создателя.
Как видите, среда выполнения может существенно влиять на выполнение процедуры с правами вызывающего. Одна и та же хранимая процедура на PL/SQL при непосредственном вызове и при вызове из другой хранимой процедуры может обращаться к различным наборам объектов, даже при регистрации от имени одного и того же пользователя. Чтобы продемонстрировать это, я создам процедуру, показывающую, какие роли активны во время выполнения и обращающуюся к таблице за данными, свидетельствующими о "владельце" таблицы. Мы сделаем это для каждого из представленных выше случаев, за исключением вызова процедуры с правами вызывающего из другой процедуры с правами вызывающего, поскольку это ничем не отличается от ее непосредственного вызова. Начнем с создания двух учетных записей, которые будут использоваться для демонстрации: tkyte@TKYTE816> drop user a cascade; User dropped. tkyte@TKYTE816> drop user b cascade; User dropped. tkyte@TKYTE816> c r e a t e user a i d e n t i f i e d by a default tablespace data temporary tablespace temp; User created. tkyte@TKYTE816> grant connect, resource to a; Grant succeeded. tkyte@TKYTE816> c r e a t e user b i d e n t i f i e d by b default tablespace data temporary tablespace temp; User c r e a t e d . tkyte@TKYTE816> grant connect, resource t o b; Grant succeeded. Итак, созданы два пользователя, А и В, и каждому из них предоставлены две роли, CONNECT и RESOURCE. Пользователь А создаст процедуру с правами вызывающего, а также процедуру с правами создателя и представление, из которых будет вызываться процедура с правами вызывающего. При каждом выполнении процедура будет выдавать количество действующих ролей, имя текущего пользователя (с набором привилегий какой схемы она работает), имя текущей схемы и, наконец, какая таблица используется запросом. Начнем с создания таблицы, определенно принадлежащей пользователю А:
512
Глава 23
tkyte@TKYTE816> connect a/a a@TKYTE816> create table t (x varchar2(255)); Table created. a@TKYTE816> insert into t values ('Таблица пользователя A ' ) ; 1 row created. Затем пользователь А создает функцию с правами вызывающего, процедуру с правами создателя и представление: a@TKYTE816> c r e a t e function Invoker_rights_function r e t u r n varchar2 2 AUTHID CURRENTJJSER 3 as 4 l_data varchar2(4000) ; 5 begin 6 dbms_output.put_line('Я — функция с правами вызывающего, "•• принадлежащая А 1 ) ; 7 select 'current_user=' I I 8 sys_context('userenv', 'current_user') II 9 ' current_schema=' || 10 sys_context('userenv', 'current_schema') || 11 ' active roles=' I I cnt || 12 ' data from T=' || t.x 13 into l_data 14 from (select count(*) cnt from session_roles), t; 15 16 return l_data; 17 end; 18 / Function created. a@TKYTE816> grant execute on Invoker_rights_function to public; Grant succeeded. a@TKYTE816> create procedure Definer_rights_procedure 2 as 3 l_data varchar2(4000); 4 begin 5 dbms_output.put_line('Я — процедура с правами создателя, ""• принадлежащая А') ; 6 select 'current_user=' || 7 sys_context('userenv1, 'current_user') || 8 ' current_schema=' || 9 sys_context('userenv1, 'current_schema') II 10 ' active roles=' || cnt || 11 ' data from T=' | | t.x 12 into l_data 13 from (select count(*) cnt from session_roles), t; 14 15 dbms_output.put_line(l_data); 16 dbms_output.put_line ** ('Теперь вызываем функцию с правами вызывающего...'); 17 dbms_output.put line(Invoker rights_function);
Права вызывающего и создателя 18 19
513
end; /
Procedure created. a@TKYTE816> grant execute on Definer_rights_procedure to publicGrant succeeded. a@TKYTE816> create view V 2 as 3 select invoker_rights_function from dual 4 / View created. a@TKYTE816> grant select on v to public 2 / Grant succeeded. Зарегистрируемся как пользователь В, создадим таблицу Т с идентифицирующей пользователя строкой и выполним созданную ранее функцию: a@TKYTE816> connect b/b b@TKYTE816> create t a b l e t (x varchar2(255)); Table c r e a t e d . b@TKYTE816> i n s e r t i n t o t values ('Таблица пользователя В ' ) ; 1 row created. b@TKYTE816> exec dbms_output.put_line(a.Invoker_rights_function) Я — функция с правами вызывающего, принадлежащая А current_user=B current_schema=B a c t i v e roles=3 data from Т=Таблица ••* пользователя В PL/SQL procedure successfully completed. Итак, мы видим: когда пользователь В непосредственно вызывает функцию с правами вызывающего, принадлежащую пользователю А, во время ее выполнения используются привилегии пользователя В (current_user=B). Далее, поскольку current_schema — тоже пользователь В, запрос выбирает данные из таблицы В.Т, а не из А.Т. Это доказывает строка data from Т=Таблица пользователя В в представленных выше результатах. Наконец, мы видим, что при выполнении запроса в сеансе активны три роли (третья роль — PLUSTRACE, необходимая для использования AUTOTRACE; в моей базе данных она предоставлена роли PUBLIC). Посмотрим, что произойдет при вызове через процедуру с правами создателя: b@TKYTE816> exec a.Definer_rights_procedure Я — процедура с правами создателя, принадлежащая А current_user=A current_schema=A a c t i v e roles=0 data from Т=Таблица • ' • пользователя А Теперь вызываем функцию с правами вызывающего... Я — функция с правами вызывающего, принадлежащая А current_user=A current_schema=A a c t i v e roles=0 data from Т=Таблица *•* пользователя А PL/SQL procedure successfully completed. 17 Зак 244
514
Глава 23
Вы видите, что процедура с правами создателя выполняется с привилегиями пользователя А, кроме ролей (active roles=0). Процедура с правами создателя жестко связана с таблицей А.Т и не обращается к таблице В.Т. Важнее всего то, что происходит при вызове функции с правами вызывающего из процедуры с правами создателя. Обратите внимание, что на этот раз вызывающий — пользователь А, а не В. Вызывающий определяется текущей схемой в момент вызова процедуры с правами вызывающего. Функция больше не выполняется от имени В, как в предыдущем случае, — теперь она выполняется от имени пользователя А. Поскольку current_user и current_schema теперь задают пользователя А, функция с правами вызывающего обращается к таблице пользователя А. Еще один важный факт: на этот раз роли в функции с правами вызывающего не действуют. При входе в процедуру с правами создателя роли отключаются и остаются отключенными до выхода из этой процедуры. Теперь рассмотрим последствия вызова функции с правами вызывающего из SQLоператора: b@TKYTE816> s e l e c t a.invoker_rights_function from d u a l ; INVOKER_RIGHTS_FUNCTION current_user=B current_schema=B a c t i v e roles=3 data from Т=Таблица w пользователя В b@TKYTE816> s e l e c t * from a . v ; INVOKER_RIGHTS_FUNCTION current_user=A current_schema=A a c t i v e roles=0 data from Т=Таблица *•* пользователя А Как видите, вызов процедуры с правами вызывающего непосредственно из SQL-оператора (как в нашем случае, когда мы выбирали значение функции из таблицы DUAL), ничем не отличается от непосредственного вызова. Более того, вызов функции из представления, как во втором запросе, показывает, что она ведет себя так же, как при вызове из процедуры с правами создателя, поскольку представления всегда сохраняются с правами создателя.
Компиляция процедуры с правами вызывающего А сейчас разберемся, что происходит при компиляции и сохранении в базе данных процедуры с правами вызывающего. Может показаться странным, но происходит в точности то же, что и при компиляции процедуры с правами создателя. Выполняются следующие шаги: •
Проверяется существование всех объектов, к которым процедура обращается статически (всех, к которым она не обращается с помощью динамического SQL). Имена разрешаются с помощью стандартных правил области действия по отношению к владельцу процедуры. Роли не учитываются.
Q Проверяется, все ли объекты доступны в нужном режиме. Например, если выполняется оператор UPDATE T, сервер Oracle проверит, может ли создатель или роль PUBLIC выполнять UPDATE T непосредственно, не используя ролей.
Права вызывающего и создателя •
515
Устанавливается и поддерживается зависимость процедуры от объектов, на которые она ссылается. Если процедура выполняет оператор SELECT FROM T, регистрируется зависимость процедуры от таблицы Т.
Это означает, что подпрограмма с правами вызывающего при компиляции обрабатывается точно так же, как подпрограмма с правами создателя. Многих это сбивает с толку. Они слышали, что процедуры с правами вызывающего используют роли, и это так. Но (повторяю) они не используются в ходе компиляции. Это означает, что пользователь, компилирующий хранимую процедуру, ее владелец, должен иметь непосредственный доступ ко всем статически используемым таблицам. Вспомните пример из раздела "Права создателя", где было показано, что можно успешно выполнить SELECT COUNT(*) FROM EMP в SQL и в анонимном блоке PL/SQL, но такой же оператор в хранимой процедуре приводит к ошибке компиляции. То же самое произойдет и в подпрограмме с правами вызывающего. Правила, сформулированные в разделе Privileges Required to Create Procedures and Functions руководства Oracle 8i Application Developer's Guide
остаются в силе: все равно необходим непосредственный доступ к базовым объектам. Причина — в механизме зависимостей, используемом сервером Oracle. Если действие, выполняемое в базе данных (например, оператор REVOKE), делает процедуру с правами создателя недействительной, аналогичная процедура с правами вызывающего тоже становится недействительной. Различие между процедурами с правами вызывающего и создателя наблюдается только при выполнении. С точки зрения зависимостей, пометки процедур как недействительных и привилегий, необходимых владельцу процедуры, никаких различий нет. Эту проблему можно обойти, и для большинства процедур с правами вызывающего проблема эта вообще не актуальна. Однако из-за нее иногда приходится создавать объекты-шаблоны. В следующем разделе мы рассмотрим, что такое объекты-шаблоны и как их использовать, чтобы избежать необходимости предоставления непосредственных привилегий.
Использование объектов-шаблонов Теперь, зная, что при компиляции процедура с правами вызывающего не отличается от процедуры с правами создателя, можно понять, почему необходим непосредственный доступ ко всем объектам. При разработке процедур с правами вызывающего, в которых предполагается использование ролей, создателю необходимы непосредственные привилегии, а не роли. Их получение может оказаться невозможным по любой причине (достаточно, чтобы кто-то решил: "Привилегии select на эту таблицу я не дам"), и придется искать решение. Тут пригодятся объекты-шаблоны. Объект-шаблон — это объект, к которому пользователь-создатель имеет непосредственный доступ и по структуре совпадающий с объектом, к которому предполагается обращаться при выполнении. Его можно рассматривать как конструкцию struct языка С, Java-класс, PL/SQL-запись или структуру данных. Он создается для того, чтобы сервер знал количество и типы столбцов, и другие свойства объекта. Рассмотри это на примере. Предположим, необходимо создать процедуру, обращающуюся к представлению DBA_USERS и выдающую в удобном формате оператор CREATE USER для любого существующего пользователя. Можно попытаться создать эту процедуру, например, так:
516
Глава 23
tkyte@TKYTE816> create or replace 2 procedure show_user_info(p_username in varchar2) 3 AUTHID CURRENT_USER 4 as 5 l_rec dba_users%rowtype; 6 begin 7 select * 8 into l_rec 9 from dba_users 10 where username = upper(p_username); 11 12 dbms_output.put_line('create user ' || p_username); 13 if (l_rec.password = 'EXTERNAL1) then 14 dbms_output.put_line(' identified externally'); 15 else 16 dbms_output.put_line 17 (' identified by values " ' II l_rec.password I| " " ); 18 end if; 19 dbms_output.put_line 20 (' temporary tablespace ' || l_rec.temporary_tablespace || 21 ' default tablespace ' || l_rec.default_tablespace |I 22 ' profile ' M l_rec.profile); 23 exception 24 when no_data_found then 25 dbms_output.put_line('*** Нет такого пользователя: ' || ^» p_username); 26 end; 27 / Warning: Procedure created with compilation e r r o r s . tkyte@TKYTE816> show e r r Errors for PROCEDURE SHOW_USER_INFO: LINE/COL ERROR 4/13 4/13 6/5 8/12 12/5 12/10
PLS-00201: i d e n t i f i e r 'SYS.DBAJ7SERS' must be declared PL/SQL: Item ignored PL/SQL: SQL Statement ignored PLS-00201: i d e n t i f i e r 'SYS.DBA_USERS' must be declared PL/SQL: Statement ignored PLS-00320: the declaration of the type of t h i s expression incomplete or malformed
18/5 19/35
PL/SQL: Statement ignored PLS-00320: the declaration of the type of t h i s expression incomplete or malformed
is
is
Эта процедура не компилируется, не потому, что не существует объект SYS.DBA_USERS, а потому, что обращаться к DBA_USERS мы можем только благодаря предоставленной роли, а в ходе компиляции хранимой процедуры роли не используются. Так что же сделать, чтобы эта процедура скомпилировалась? Для этого можно создать собственную таблицу DBA_USERS. Это позволит успешно скомпилировать процедуру. Однако по-
Права вызывающего и создателя
5 1 7
скольку это не "реальная" таблица DBA_USERS, желаемые результаты при выполнении не будут получены, пока мы не выполним процедуру от имени другого пользователя, который может обращаться к реальному представлению DBA_USERS: tkyte@TKYTE816> c r e a t e t a b l e dba_users 2 as 3 s e l e c t * from SYS.dba_users where 1=0; Table created. tkyte@TKYTE816> a l t e r procedure show_user_info
compile;
Procedure a l t e r e d . tkyte@TKYTE816> exec show_user_info(USER); *** Нет такого пользователя TKYTE PL/SQL procedure successfully completed. tkyte@TKYTE816>
connect system/manager
system@TKYTE816> exec tkyte.show_user_infо('TKYTE') c r e a t e user TKYTE i d e n t i f i e d by values '698F1E51F530CA57' temporary tablespace TEMP default tablespace DATA p r o f i l e DEFAULT PL/SQL procedure successfully completed. Теперь мы получили процедуру, которая, при вызове любым пользователем, кроме создателя, обращается к "правильному" представлению DBA_USERS (если вызывающий не имеет права обращаться к DBA_USERS, он получит сообщение о том, что таблица или представление не существует). Если же процедуру выполняет создатель, он получает сообщение "Нет такого пользователя", поскольку у него объект-шаблон DBA_USERS пустой. Все остальные пользователи, однако, получают ожидаемые результаты. Во многих случаях это вполне приемлемо. Например, когда предполагается работа одного и того же кода с разными таблицами. В данном случае, однако, хотелось бы, чтобы эта процедура всегда работала с одним представлением, DBA_USERS. Итак, возвращаемся к тому, как обеспечить работу этой процедуры для всех пользователей, включая создателя? Надо использовать объект-шаблон другого типа. Создадим таблицу, структурно совпадающую с представлением DBA_USERS, но с другим именем, скажем, DBA_USERS_TEMPLATE. Используем эту таблицу для определения типа записи, в которую выбираются данные. После этого мы сможем динамически обращаться к представлению DBA_USERS во всех случаях: system@TKYTE816> connect tkyte/tkyte tkyte@TKYTE816> drop t a b l e dba_users; Table dropped. tkyte@TKYTE816> c r e a t e t a b l e dba_users_TEMPLATE 2 as 3 select * from SYS.dba_users where 1=0; Table created. tkyte@TKYTE816> create or replace 2 procedure show_user_info(p_username in varchar2)
518
Глава 23
3 AUTHID CURRENTJJSER 4 as 5 type re is ref cursor; 6 7 l_rec dba_users_TEMPLATE%rowtype; 8 l_cursor re; 9 begin 10 open l_cursor for 11 'select * 12 from dba_users 13 where username = :x' 14 USING upper(p_username); 15 16 fetch l_cursor into l_rec; 17 if (l_cursor%found) then 18 19 dbms_output.put_line('create user ' || p_username); 20 if (l_rec.password = 'EXTERNAL') then 21 dbms_output.put_line(' identified externally'); 22 else 23 dbms_output.put_line 24 (' identified by values ' " || l_rec.password II " " ) ; 25 end if; 26 dbms_output.put_line 27 (' temporary tablespace ' || l_rec.temporary_tablespace || 28 ' default tablespace ' I I l_rec.default_tablespace || 29 ' profile ' || l_rec.profile); 30 else 31 dbms_output.put_line( '*** Нет такого пользователя: ' II *"• p_username ) ; 32 " end if; 33 close l_cursor; 34 end; 35 / Procedure c r e a t e d . tkyte@TKYTE816> exec show_user_info(USER); c r e a t e user TKYTE i d e n t i f i e d by values '698F1E51F530CA57' temporary tablespace TEMP default tablespace DATA p r o f i l e DEFAULT PL/SQL procedure successfully completed. Итак, в данном случае мы использовали таблицу DBA_USERS_TEMPLATE только для того, чтобы упростить создание типа записи, в которую будут выбираться данные. Можно было бы получить описание представления DBA_USERS и создать тип записи со всеми соответствующими полями, но мне больше нравится, когда работу за меня делает сервер. После перехода на новую версию Oracle достаточно будет просто пересоздать таблицу-шаблон и процедура перекомпилируется автоматически, при этом все новые/дополнительные столбцы или изменения типов данных будут автоматически учтены.
Права вызывающего и создателя
Ь 1j /
Проблемы Как и при использовании любого средства, в работе процедур с правами вызывающего есть ряд нюансов, которые необходимо учитывать. В этом разделе мы попытаемся рассмотреть некоторые из них.
Права вызывающего и использование разделяемого пула При использовании прав вызывающего для обеспечения доступа одной процедуры к данным в различных схемах, в зависимости от того, кто ее вызывает, следует помнить, что это достигается за счет менее эффективного использования разделяемого пула. При использовании процедур с правами создателя для каждого запроса в процедуре в разделяемом пуле будет не более одного экземпляра соответствующего SQL-оператора. Процедуры с правами создателя максимально эффективно применяют возможности совместного использования SQL-операторов (почему это принципиально важно, см. в главе 10). Процедуры с правами вызывающего подобную эффективность могут не обеспечивать. Это не хорошо и не плохо. Это просто надо учитывать при задании размера разделяемого пула. При использовании процедур с правами вызывающего мы будем использовать разделяемый пул аналогично тому, как это делает клиент-серверное приложение, использующее интерфейс ODBC или JDBC и непосредственно посылающее серверу операторы DML. Каждый пользователь будет выполнять запрос с одним и тем же текстом, но запросы эти могут различаться. Так что, хотя все выполняют SELECT * FROM Т, но поскольку таблицы Т у всех пользователей разные, для каждого пользователя будет создаваться и помещаться в разделяемый пул отдельный план запроса и другая соответствующая информация. Это необходимо, поскольку каждый раз используется другая таблица Т, с другими правами и планами доступа. Влияние на разделяемый пул легко продемонстрировать на примере. Я создал в одной схеме следующие объекты: tkyte@TKYTE816> create t a b l e t (x i n t ) ; Table created. tkyte@TKYTE816> create t a b l e t2 (x i n t ) ; Table created. tkyte@TKYTE816> c r e a t e public synonym T for T; Synonym created. tkyte@TKYTE816> create or replace procedure dr_proc 2 as 3 1 cnt number; — 4 begin 5 select count(*) into l_cnt from t DEMO_DR; 6 end; 7 / Procedure created.
520
Глава 23
tkyte@TKYTE816> create or replace procedure ir_procl 2 authid current_user 3 as 4 l_cnt number; 5 begin 6 select count(*) into l_cnt from t DEMO_IR_1; 7 end; 8 / Procedure created. tkyte@TKYTE816> create or replace procedure ir_proc2 2 authid current_user 3 as 4 l_cnt number; 5 begin 6 select count(*) into l_cnt from tkyte.t DEMO_IR_2; 7 end; 8 / Procedure created. tkyte@TKYTE816> create or replace procedure ir_proc3 2 authid current_user 3 as 4 l_cnt number; 5 begin 6 select count(*) into l_cnt from t2 DEMO_IR_3; 7 end; 8
/
Procedure created. tkyte@TKYTE816> grant select on t to publicGrant succeeded. tkyte@TKYTE816> grant execute on drjproc to publicGrant succeeded. tkyte@TKYTE816> grant execute on ir_procl to publicGrant succeeded. tkyte@TKYTE816> grant execute on ir_proc2 to publicGrant succeeded. tkyte@TKYTE816> grant execute on irj?roc3 to publicGrant succeeded. Мы создали две таблицы, Т и Т2. Существует также общедоступный синоним Т для таблицы TKYTE.T. Все четыре процедуры обращаются либо к таблице Т, либо к таблице Т2. Для процедуры с правами создателя, статически связываемой при компиляции, уточнять имя таблицы именем схемы не надо. Процедура с правами вызывающего, IR_PROC1, будет обращаться к таблице Т через общедоступный синоним. Вторая процедура, IR_PROC2, будет использовать полностью уточненную ссылку, а третья процедура, IR_PROC3, будет обращаться к Т2 без уточнения схемы. Обратите внимание, что
Права вызывающего и создателя
Э JL I
общедоступного синонима для таблицы Т2 нет: я намеренно сделал, чтобы процедура IR_PROC3 при выполнении обращалась к разным таблицам Т2. Затем я создал десять пользователей с помощью следующего сценария: tkyte@TKYTE816> begin 2 10 loop for i in 1 3 begin cascade' 4 execute immediate 'drop user u' Mill 5 exception б when others then null; 7 end; 8 execute immediate 'create user u ' | | i II identified by pw'; 9 execute immediate 'grant create session, create table to u ' I | i ; 10 execute immediate 'alter user u' I I i I I ' default tablespace data quota unlimited on data'; 11 12 end loop; 13 end; 14 / PL/SQL procedure successfully completed. и для каждого пользователя мы выполняем: create table t2 (x int); exec tkyte.dr_proc exec tkyte.ir_procl exec tkyte.ir_proc2 exec tkyte.ir_proc3 Необходимо зарегистрироваться как очередной пользователь, создать таблицу Т2, а затем выполнить четыре интересующих нас процедуры. После того как это сделано от имени всех десяти пользователей, можно исследовать содержимое разделяемого пула с помощью представления V$SQLAREA, используя представленную ранее в этой главе процедуру PRINT_TABLE: tkyte@TKYTE816> s e t serveroutput on s i z e 1000000 tkyte@TKYTE816> begin 2 p r i n t _ t a b l e ( ' s e l e c t sql_text, sharable_mem, version_count, loaded_versions, p a r s e _ c a l l s , optimizer_mode 3 from v$sqlarea 4 5 where sql_text l i k e ''% DEMO\ R%'' escape ' ' \ ' ' 6 and lower(sql_text) not l i k e ''%v$sqlarea%'' ' ) ; 7 end; / SELECT COUNT(*)
SQLJTEXT DEMO_IR_2 SHARABLE_MEM VERSION_COUNT LOADEDJ/ERSIONS PARSE_CALLS OPTIMIZER_MODE
4450 1 1 10 CHOOSE
SQLJTEXT SHARABLE MEM
SELECT COUNT(*) 4246
FROM OPS$TKYTE.T
FROM T DEMO DR
522
Глава 23
VERSION_COUNT LOADED_VERSIONS PARSE_CALLS OPTIMIZER_MODE
1 I 10 CHOOSE
SQL_TEXT SHARABLE_MEM VERSION_COUNT LOADED_VERSIONS PARSE_CALLS OPTIMIZER_MODE
SELECT COUNT(*) 4212 1 1 10 CHOOSE
SQLJTEXT SHARABLE_MEM VERSION_COUNT LOADEDJVERSIONS PARSE_CALLS OPTIMIZER MODE
SELECT COUNT(*) FROM T2 DEMO IR 3 31941 10 10 10 MDLTIPLE CHILDREN PRESENT
FROM T DEMO IR 1
PL/SQL procedure successfully completed. Хотя SQL-оператор во всех случаях один и тот же — SELECT COUNT(*) FROM T2 DEMO_IR_3, — в разделяемом пуле для него есть десять разных экземпляров кода Каждому пользователю необходим собственный оптимизированный план выполнения, поскольку запрос ссылается на разные объекты. В тех случаях, когда базовые объекты совпадали и привилегий хватало, планы выполнения SQL-операторов использовались совместно, как и ожидалось. Итак, если с помощью прав вызывающего вы собираетесь использовать один экземпляр кода для доступа к нескольким различным схемам, необходимо увеличить разделяемый пул, чтобы он вмещал все планы выполнения запросов. Так мы подходим к следующей проблеме.
Производительность При использовании процедур с правами вызывающего, как вы уже знаете, каждому пользователю может потребоваться отдельный специфический план выполнения запроса. На построение этих дополнительных планов могут потребоваться существенные ресурсы. Анализ запроса — одно из наиболее интенсивно нагружающих процессор действий сервера. "Стоимость" анализа уникальных запросов, который возможен при использовании подпрограмм с правами вызывающего, можно продемонстрировать с помощью утилиты TKPROF, показывающей продолжительность анализа операторов. Для выполнения следующего примера необходима привилегия ALTER SYSTEM: tkyte@TKYTE816> alter system flush shared_pool; System altered. tkyte@TKYTE816> a l t e r system set timed_statistics=true; System altered. tkyte@TKYTE816> alter session set sql_trace=true;
Права вызывающего и создателя
523
Session altered. tkyte@TKYTE816> declare 2 type re is ref cursor; 3 l_cursor re; 4 begin for i in 1 .. 500 loop 5 open l_cursor for 'select * from all_objects t' | 6 close l_cursor; 7 end loop; 8 end; 9 10
i;
PL/SQL procedure successfully completed. При этом был выполнен анализ 500 уникальных операторов (в них используются уникальные псевдонимы таблицы). Ситуация аналогична использованию одной процедуры с правами вызывающего 500 пользователями в 500 различных схемах. В итоговом отчете утилиты TKPROF для этого сеанса можно найти следующее: OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS call
count
cpu
Parse Execute Fetch
1148 1229 1013
17 .95 0 .29 0 .14
total
3390
18.38
elapsed
disk
query
current
18 .03 0 .25 0 .17
0 0 0
55 0 2176
15 0 0
0 0 888
18.45
0
2231
15
888
Misses in library cache during parse: 536 504 user SQL statements in session. 648 internal SQL statements in session. 1152 SQL statements in session. 0 statements EXPLAINed in this session.
Теперь выполним блок, не анализирующий уникальный оператор 500 раз: tkyte@TKYTE816> alter system flush shared_pool; System altered. tkyte@TKYTE816> alter system set timed_statistics=true; System altered. tkyte@TKYTE816> alter session set sql_trace=true; Session altered. tkyte@TKYTE816> declare 2 type re is ref cursor; 3 l_cursor re; 4 begin 5 for i in 1 .. 500 loop 6 open 1 cursor for 'select 7 close 1 cursor;
from all_obj ects t';
524
Глава 23
8 9 10
end loop; end; /
PL/SQL procedure successfully completed. и в отчете TKPROF увидим: . OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS call Parse Execute Fetch
count
cpu
elapsed
disk
614 671 358
0 .74 0 .09 0 .08
0.53 0.31 0.04
1 0 8
total 1643 0.91 0.88 9 Misses in l i b r a r y cache during parse: 22 504 114 618 0
query c u r r e n t
rows
55 0 830
9 0 0
0 0 272
885
9
272
user SQL statements in session. i n t e r n a l SQL statements in session. SQL statements in session. statements EXPLAINed in t h i s session.
Разница огромна. Для анализа 500 уникальных операторов (эмулирующих поведение процедуры с правами вызывающего, которая обращается при каждом вызове к другой таблице) требуется 17,95 секунд процессорного времени. Для анализа же 500 одинаковых операторов (эмулирующих использование стандартной процедуры с правами создателя) понадобилось 0,74 секунды процессорного времени. В 24 раза меньше! Конечно, это надо учитывать. Когда SQL-операторы не используются повторно, система может тратить больше времени на анализ запросов, чем на их фактическое выполнение. Причины этого были рассмотрены в главе 10, посвященной стратегиям и средствам настройки производительности. Там я продемонстрировал необходимость использования связываемых переменных для повторного использования планов запросов. Однако это не повод отказываться от использования процедур с правами вызывающего. Используйте их, но помните о последствиях.
Более надежный код для обработки ошибок При создании хранимой процедуры со следующим кодом: begin for x in (select pk from t) loop update у set с = c+0.5 where d = x.pk; end loop; end; ... вполне можно быть уверенным, что при отсутствии синтаксических и семантических ошибок (компилируется успешно) она будет работать. При использовании процедур с
Права вызывающего и создателя
525
правами создателя это верно. Я точно знаю, что объекты (таблицы или представления) Т и Y существуют, что Т доступен для чтения, a Y можно изменять. При использовании процедуры с правами вызывающего, ни в чем нельзя быть уверенным. Существует ли объект Т, и если — да, то имеется ли в нем столбец с именем РК? И имею ли я для него привилегию SELECT? А если имею, то не через роль ли она получена? Ведь тогда при вызове процедуры из подпрограммы с правами создателя, она не сработает, хотя при непосредственном вызове будет работать прекрасно. Существует ли объект Y? И так далее. Другими словами, все условия, которые раньше можно было считать гарантированно выполненными, вызывают сомнению в процедурах с правами вызывающего. Так что, хотя процедуры с правами вызывающего и открывают новые возможности программирования, в некотором отношении они его усложняют. При использовании представленного выше кода надо готовиться к обработке множества вполне вероятных случаев: _ •
объекта Т нет;
•
объект Т есть, но нет необходимых для доступа к нему привилегий;
• •
объект Т есть, но в нем нет столбца РК; объект Т существует и имеет столбец РК, но тип данных столбца отличается от использованного при компиляции; Q все то же в отношении объекта Y. Поскольку изменение объекта Y происходит только при получении определенных данных из Т, мы можем многократно успешно выполнить эту процедуру, но однажды, когда в Т будут помещены данные, процедура не сработает. Мы никогда не могли обратиться к объекту Y, но процедура не сработала потому, что мы впервые "попытались". Ошибка во фрагменте кода произойдет только тогда, когда он выполнится. Для получения "надежной" процедуры, перехватывающей все возможные ошибки, необходим примерно такой код: c r e a t e or replace procedure P authid current_user as no_such_table exception; pragma exception_init(no_such_table,-942); insufficient_privs exception; pragma exception_init(insufficient_privs,-1031); invalid_column_name exception; pragma exception_init(invalid_column_name,-904); inconsistent_datatypes exception; pragma exception_init(inconsistent_datatypes,-932); begin for x in (select pk from t) loop update у set с = c+0.5 where d = x.pk; end loop; exception when NO SUCH TABLE then
526
Глава 23
dbms_output.put_line('Перехвачена ошибка: when INSUFFICIENT_PRIVS then dbms_output.put_line('Перехвачена ошибка: when INVALID_COLUMN_NAME then dbms_output.put_line('Перехвачена ошибка: when INCONSISTENT_DATATYPES then dbms_output.put_line('Перехвачена ошибка: — ... (дальше идет множество других обработчиков end;
' || sqlerrm); ' || sqlerrm); ' || sqlerrm); ' M sqlerrm); ошибок)...
Побочные эффекты использования SELECT * Использование конструкции SELECT * в PL/SQL-процедуре с правами вызывающего, обращающейся к разным таблицам при вызове разными пользователями, может быть очень опасно. При этом данные могут быть получены "поврежденными" или в другом порядке. Причина в том, что запись, в которую выполняется выборка данных, настраивается при компиляции, а не при выполнении. Поэтому список столбцов для PL/SQL-объектов (записей) вместо * формируется при компиляции, а данные получаются при выполнении запроса. Если в другой схеме имеется объект с тем же именем, но с другим порядком столбцов и к нему обращаются из процедуры с правами вызывающего с помощью оператора SELECT *, возникает именно такой побочный эффект: tkyte@TKYTE816> c r e a t e t a b l e t (msg varchar2(25), c l i n t , c2 i n t ) ; Table created. tkyte@TKYTE816> i n s e r t i n t o t values ( ' c l = l , c2=2', 1, 2 ) ; 1 row created. tkyte@TKYTE816> create or replace procedure P 2 authid current_user 3 as 4 begin 5 for x in (select * from t) loop 6 dbms_output,.put line('msg= ' 11 x.msg); dbms output .put line('Cl = ' 11 x.cl); 7 dbms output,,put_line('C2 = ' 11 x.c2); 8 9 end loop; 10 end; 11 / Procedure created. tkyte@TKYTE816> exec p msg= cl=l, c2=2 Cl = 1 C2 = 2 PL/SQL procedure successfully completed. tkyte@TKYTE816> grant execute on P to ul; Grant succeeded.
Права вызывающего и создателя
5 2 7
Итак, мы создали процедуру, показывающую содержимое таблицы Т. Значение которое она выдает в столбце MSG, я использую, чтобы продемонстрировать предполагаемый ответ. Кроме того, она выдает значения столбцов С1 и С2. Все просто и понятно. Теперь давайте посмотрим, что произойдет, если выполнить процедуру от имени другого пользователя, со своей собственной таблицей Т: tkyte@TKYTE816> gconnect ul/pw ul@TKYTE816> drop t a b l e t ; Table dropped. ul@TKYTE816> create t a b l e t (msg varchar2(25), c2 i n t , c l i n t ) ; Table created. ul@TKYTE816> i n s e r t i n t o t values C c l - 2 , c 2 = l \ 1, 2 ) ; 1 row created. Обратите внимание, что при создании таблицы я изменил порядок столбцов С1 и С2. Здесь я предполагаю, что С1 = 2 и С2 = 1. При выполнении процедуры, однако, получаем следующее: ul@TKYTE816> exec t k y t e . p msg= cl=2, c2=l Cl = 1 C2 = 2 PL/SQL procedure successfully completed. He совсем так, как ожидалось, но если вдуматься глубже... При компиляции была автоматически создана неявная запись X. Запись X — это просто структура данных с тремя элементами — MSG VARCHAR2, Cl NUMBER и С2 NUMBER. Когда список столбцов SELECT * формировался на этапе анализа запроса от имени пользователя TKYTE, были получены столбцы MSG, С1 и С2 (именно в таком порядке). При выполнении процедуры пользователем U1 были, однако, получены столбцы MSG, С2 и С1. Поскольку все типы данных совпадают с типами полей неявно созданной записи X, мы не получили сообщения об ошибке INCONSISTENT DATATYPE (это тоже могло произойти, если бы типы данных оказались несовместимыми, с точностью до неявного преобразования). Данные были успешно выбраны, но значение столбца С2 помещено в поле записи С1. Это вполне предсказуемый побочный эффект и еще одна причина не использовать операторы SELECT * в производственном коде.
Помните о "скрытых" столбцах Это очень похоже на представленную ранее проблему SELECT *. Она тоже связана с тем, как компилируется PL/SQL-процедура с правами вызывающего и как разрешаются имена и ссылки на объекты. Рассмотрим оператор UPDATE, который при выполнении непосредственно в среде SQL*Plus даст другой результат, чем при выполнении в подпрограмме с правами вызывающего. Он работает "правильно" в обеих средах, просто — по-разному. Когда PL/SQL-код компилируется в базе данных, каждый статический SQL-оператор анализируется и уточняются все идентификаторы. Эти идентификаторы могут быть
528
Глава 23
именами столбцов или именами PL/SQL-переменных (связываемых переменных). Если это имена столбцов, они оставляются в запросе без изменений. Если же это имена переменных PL/SQL, они заменяются в запросе ссылкой :BIND_VARIABLE. Эта замена производится при компиляции, а не при выполнении. Рассмотрим пример: tkyte@TKYTE816> create t a b l e t (cl i n t ) ; Table c r e a t e d . tkyte@TKYTE816> i n s e r t i n t o t values (1); 1 row created. tkyte@TKYTE816> create or replace procedure P 2 authid current user 3 as 4 c2 number default 5; 5 begin 6 update t set cl = c2; 7 end; 8 Procedure created. tkyte@TKYTE816> exec p PL/SQL procedure successfully completed. tkyte@TKYTE816> s e l e c t * from t ; Cl 5 tkyte@TKYTE816> grant execute on P t o u l ; Grant succeeded. Пока все выглядит нормально. Cl — это столбец в таблице Т, а С2 — переменная PL/SQL. Оператор UPDATE T SET Cl = С2 обрабатывается сервером при компиляции и преобразуется в UPDATE T SET C l = :ВIND_VARIABLE, а значение :BIND_VARIABLE передается при выполнении. Теперь, если зарегистрироваться как Vlf и создать в этой схеме таблицу Т: tkyte@TKYTE816> connect ul/pw ul@TKYTE816> drop t a b l e t ; Table dropped. ul@TKYTE816> c r e a t e t a b l e t (cl i n t , c2 i n t ) , Table created. ul@TKYTE816> i n s e r t i n t o t values (1, 2 ) ; 1 row created. ul@TKYTE816> exec tkyte.p PL/SQL procedure successfully completed. ul@TKYTE816> select * from t;
Права вызывающего и создателя Cl
5
5 2 9
C2
2
Это может показаться правильным или неправильным — смотря как к этому подойти. Мы выполнили оператор UPDATE T SET Cl = С2, но если бы мы это сделали в командной строке SQL*Plus, то столбец С1 получил бы значение 2, а не 5. Однако, поскольку сервер переписал этот запрос при перекомпиляции так, что в нем не осталось ссылок на С2, он делает то же самое с нашим экземпляром Т, что и с другим экземпляром Т: устанавливает столбцу С1 значение 5. Эта PL/SQL-процедура не может "видеть" столбец С2, поскольку С2 не существует в объекте, с которым она была скомпилирована. Сначала это кажется странным, поскольку оператора UPDATE в переписанном виде мы обычно не видим, но если вы знаете об этом, результат становится вполне объяснимым.
Java и права вызывающего PL/SQL-процедуры по умолчанию компилируются с правами создателя. Чтобы такая процедура работала с правами вызывающего, надо это специально указать. Java-процедура по умолчанию работает с правами вызывающего. Если необходимо, чтобы она выполнялась с правами создателя, надо явно указать это при загрузке. В качестве примера я создал таблицу Т, такую, что: ops$tkyte@DEV816> create t a b l e t (msg varchar2(50)); Table created. ops$tkyte@DEV816> i n s e r t i n t o t values ('Это таблица Т, принадлежащая *•» пользователю ' | | u s e r ) ; 1 row created. Я также создал и загрузил две хранимые процедуры на Java (для выполнения этого примера необходима привилегия CREATE PUBLIC SYNONYM). Эти хранимые процедуры на Java очень похожи на рассмотренные ранее примеры PL/SQL. Они обращаются к таблице Т, которая содержит строку, со сведениями о том, кому "принадлежит" эта таблица, и выдают информацию о пользователе сеанса, о текущем пользователе (схеме, определяющей привилегии) и текущей схеме: tkyte@TKYTE816> host type ir_java.java import j ava.sql.*; import oracle.j dbc.driver.*; public class i r Java public static void test() throws SQLException
I Connection cnx = new OracleDriver().defaultConnection(); String sql = "SELECT MSG, sys_context('userenv','session_user'), "+ "sys_context('userenv','current_user'), "+
530 ,
Глава 23 .
—
— _ — i — .
"sys_context('userenv','current_schema') "+ "FROM 'restatement stmt = cnx.createStatement(); ResultSet rset = stmt.executeQuery(sql) ; if (rset.nextO) System.out.println( rset.getString(l) + " session_user=" + rset.getString(2)+ " current_user=" + rset.getString(3)+ " current_schema=" + rset.getString(4)); rset.close(); stmt.close();
tkyte@TKYTE816> host dropjava -user tkyte/tkyte ir_java.java tkyte@TKYTE816> host loadjava -user tkyte/tkyte -synonym -grant ul verbose -resolve ir_java.java initialization complete loading : ir_java creating : ir_java resolver : resolving: ir_java synonym : ir_java Представленная выше подпрограмма загружается с правами вызывающего. Теперь загрузим ту же подпрограмму с другим именем. При загрузке подпрограммы с помощью loadjava укажем, что она должна выполняться с правами создателя: tkyte@TKYTE816> host type dr_java.java import j ava.sql.*; import oracle.jdbc.driver.*; public class dr_java {
... тот же код, что и в предыдущей примере ... } tkyte@TKYTE816> host dropjava -user tkyte/tkyte dr_java.Java tkyte@TKYTE816> host loadjava -user tkyte/tkyte -synonym -definer -grant ul -verbose -resolve dr_java initialization complete loading : dr_java creating : dr_java resolver : resolving: drjava synonym : dr_java Итак, отличия между IR_JAVA и DR_JAVA — имена классов и тот факт, что подпрограмма DR_JAVA была загружена с опцией -definer. Затем я создал спецификацию вызова PL/SQL, чтобы можно было выполнять эти процедуры из SQL*Plus. Обратите внимание, что создано четыре версии. Все вызовы хранимых процедур на Java выполняются только через SQL-уровень. Поскольку SQL-ypo-
Права вызывающего и создателя
531
вень, фактически, представляет собой интерфейсную PL/SQL-подпрограмму, в ней тоже можно задавать конструкцию AUTHID. Надо разобраться, что происходит, когда из подпрограммы с правами вызывающего/создателя на уровне PL/SQL вызывается Java-процедура с правами вызывающего/создателя: tkyte@TKYTE816> c r e a t e OR replace procedure ir_ir_java 2 authid current_user 3 as language Java name 'ir_java.test()'; 4 / Procedure created. tkyte@TKYTE816> grant execute on ir_ir_java to ul; Grant succeeded. tkyte@TKYTE816> create OR replace procedure dr_ir_java 2 as language Java name 'ir_java.test()'; 3
/
Procedure created. tkyte@TKYTE816> grant execute on dr_ir_java to ul; Grant succeeded. tkyte@TKYTE816> create OR replace procedure ir_dr_java 2 authid current_user 3 as language Java name 'dr_java.test()'; 4 / Procedure created. tkyte@TKYTE816> grant execute on ir_dr_java to ul; Grant succeeded. tkyte@TKYTE816> create OR replace procedure dr_dr_java 2 authid current_user 3 as language Java name 'dr_java.test()'; 4
/
Procedure created. tkyte@TKYTE816> grant execute on dr_dr_java to ul; Grant succeeded.
Теперь необходимо создать и наполнить данными таблицу Т в схеме TKYTE: tkyte@TKYTE816> drop table t; Table dropped. tkyte@TKYTE816> create table t (msg varchar2(50)); Table created. tkyte@TKYTE816> insert into t values ('Это таблица Т, принадлежащая ** пользователю ' || user); 1 row created. Итак, теперь мы готовы к проверке выполнения от имени пользователя U1, у которого сейчас появится таблица Т со строкой, тоже идентифицирующей владельца:
532
Глава 23
tkyte@TKYTE816> Sconnect ul/pw ul@TKYTE816> drop table t; Table dropped. ul@TKYTE816> create table t (msg varchar2(50)); Table created. ul@TKYTE816> insert into t values ('Это таблица Т, принадлежащая w пользователю ' || user); 1 row created. ul@TKYTE816> s e t serveroutput on s i z e 1000000 ul@TKYTE816> exec dbms_java.set_output(1000000); PL/SQL procedure successfully completed. ul@TKYTE816> exec t k y t e . i r _ i r _ j a v a Это таблица Т, принадлежащая пользователю Ul session_user=Ul current_user=Ul current_schema=Ul PL/SQL procedure successfully completed. Это показывает, что, когда хранимая Java-процедура с правами вызывающего вызывается из PL/SQL-процедуры с правами вызывающего, она работает как процедура с правами вызывающего. Текущий пользователь и текущая схема — U1, SQL-оператор в хранимой Java-процедуре, обращается к таблице U1.T, а не TKYTE.T. Теперь давайте вызовем тот же Java-код из процедуры с правами создателя: ul@TKYTE816> exec tkyte.dr_ir_java Это таблица Т, принадлежащая пользователю TKYTE session_user=Ul current_user=TKYTE current_schema=TKYTE PL/SQL procedure successfully completed. Теперь, хотя хранимая Java-процедура загружена с правами вызывающего, она работает как процедура с правами создателя. Это вполне можно было предвидеть исходя из предыдущих примеров. Процедура с правам вызывающего, вызываемая из процедуры с правами создателя, работает аналогично процедуре с правами создателя. Роли не учитываются; текущая схема фиксируется статически, как и текущий пользователь. Эта процедура обращается к таблице TKYTE.T, а не к U1.T, как в предыдущем примере, а текущий пользователь и схема имеет фиксированное значение — TKYTE. Давайте рассмотрим, что произойдет, если PL/SQL-процедура с правами вызывающего вызывает хранимую Java-процедуру, загруженную с правами создателя: ul@TKYTE816> exec tkyte.ir_dr_java пользе Это таблица Т, принадлежащая пользователю TKYTE session_user=Ul current user=TKYTE current schema =TKYTE PL/SQL procedure successfully completed. Это показывает, что если Java-процедура загружена с опцией -definer, она работает с правами создателя, даже если вызывается из процедуры с правами вызывающего. Результат последнего примера теперь уже очевиден. Из PL/SQL-процедуры с правами создателя вызывается Java-процедура с правами создателя:
Права вызывающего и создателя
5 3 3
ul@TKYTE816> exec tkyte.dr_dr_java Это таблица Т, принадлежащая пользователю TKYTE session_user=Ul current_user=TKYTE current_schema =TKYTE PL/SQL procedure successfully completed. Разумеется, она выполняется с правами создателя. Вы можете даже не заметить, что хранимая Java-процедура загружается с правами вызывающего, поскольку вызывается она обычно из PL/SQL-подпрограмм, а они стандартно компилируются с правами создателя. Обычно Java-код загружается в ту же схему, где создается спецификация вызова; и если она создана с правами создателя, Javaкод также работает с правами создателя. Я возьму на себя смелость предположить, что большинство пользователей не подозревает о такой особенности загрузки Java-кода, поскольку по результатам почти никогда нельзя догадаться о работе с правами вызывающего. Только если спецификация вызова на PL/SQL создана с конструкцией AUTHID CURRENT_USER, это свойство может проявиться. Еще одни случай, когда стандартная загрузка Java-кода с правами вызывающего имеет значение, — создание спецификации вызова не в той схеме, в которой загружен байткод Java. Используя тот же загруженный ранее Java-код, я создал от имени пользователя U1 несколько спецификаций вызова Java-кода из схемы пользователя TKYTE. Для этого пользователю U1 предоставлена привилегия CREATE PROCEDURE. Кроме того, используется тот факт, что при загрузке Java-кода задана опция -synonym, благодаря которой для загруженного кода создан общедоступный синоним, а также опция -grant U1, предоставившая пользователю U1 непосредственный доступ к Java-коду. Вот результат: ul@TKYTE816> create OR replace procedure ir_java 2 authid current_user 3 as language Java name 'ir_java.test()'; 4 / Procedure created. ul@TKYTE816> exec ir_java Это таблица Т, принадлежащая пользователю Ul session_user=Ul current_user=Ul current_schema=Ul PL/SQL procedure successfully completed. Вы видите, что процедура с правами вызывающего (процедура с правами создателя дала бы тот же результат), принадлежащая пользователю U1, выполняет SQL-операторы в Java-коде так, как если бы ее загрузил пользователь U1. Это свидетельствует о том, что Java-код загружен с правами вызывающего. В противном случае, SQL-оператор в Java-коде работал бы с разрешением имен и привилегиями пользователя TKYTE, а не Ш . Следующий пример показывает работу процедуры с правами создателя, созданной в схеме U1. Java-код работает от имени TKYTE: ul@TKYTE816> create OR replace procedure dr_java 2 as language Java name ' d r _ j a v a . t e s t ( ) ' ; 3 / Procedure created.
534
Глава 23
ul@TKYTE816> exec dr_java Это таблица Т, принадлежащая пользователю TKYTE session_user=Ul current_user=TKYTE current_schema =TKYTE PL/SQL procedure successfully completed. Java-код, загруженный с правами создателя, работает от имени TKYTE, а не Ш . Загружать Java-код с правами создателя пришлось принудительно с помощью опции -definer, поскольку свойства хранимых процедур на Java отличаются в этом отношении от свойств PL/SQL-процедур.
Возможные ошибки Других специфических ошибок, кроме тех, что обсуждалось в разделе "Проблемы", при использовании прав вызывающего или создателя не будет. Используя права вызывающего, важно четко понимать, как в PL/SQL обрабатываются встроенные SQL-операторы, чтобы избежать проблем с операторами SELECT * и изменяющимся порядком столбцов, "скрытыми" столбцами при выполнении и так далее. Кроме того, работавший без проблем PL/SQL-код при использовании прав вызывающего может выдавать различные ошибки для каждого из пользователей. Причина в том, что ссылки на объекты разрешаются по-разному. В каких-то схемах может не хватать привилегий, используются другие типы данных и т.д. При использовании прав вызывающего необходимо больше внимания уделять надежности кода и учитывать возможность возникновения ошибок там, где их обычно не бывает. Статические ссылки больше не гарантируют безошибочность работы кода. Ситуация напоминает скорее создание ODBC- или JDBC-программы со встроенными непосредственными вызовами SQL-операторов. Вы контролируете "компоновку" программы (вам известно, какие подпрограммы в клиентском приложении будут вызываться), но вы не контролируете работу SQL-операторов до тех пор, пока они не выполнятся. SQL-оператор в PL/SQL-подпрограмме с правами вызывающего работает так же, как в клиентском приложении, использующем интерфейс JDBC. Не проверив все варианты выполнения для каждого пользователя, вы не можете быть на 100 процентов уверены, что в производственной среде все будет работать без ошибок. Поэтому в коде надо больше заботиться об обработке ошибок, чем в традиционной хранимой процедуре.
Резюме В этой главе мы детально рассмотрели особенности процедур с правами создателя и вызывающего. Вы убедились, как легко обеспечить работу с правами вызывающего, но узнали и то, какой ценой это достигается с точки зрения: Q выявления и обработки ошибок; • трудно выявляемых ошибок, вызванных иной структурой таблицы при выполнении; Q дополнительного расхода пространства в разделяемой области SQL; Q дополнительных затрат времени на анализ операторов.
Права вызывающего и создателя
535
Во многих случаях эта цена оказывается неприемлемой. В других же случаях (например, при создании универсальной подпрограммы для выдачи в виде набора строк со значениями столбцов через запятую результатов запроса или при форматировании результатов запроса по вертикали, а не по горизонтали) возможность использовать права вызывающего — бесценна. Без нее просто не удалось бы получить желаемый результат. Подпрограммы с правами вызывающего имеет смысл применять в таких случаях: •
когда необходимо использовать динамические SQL-операторы (как в упомянутых выше примерах);
Q когда выполняемые SQL-операторы обеспечивают защиту на базе идентификатора схемы (как в случае работы со словарем данных или при реализации собственных средств детального контроля доступа в приложении); Q когда необходим учет ролей — только подпрограммы с правами вызывающего позволяют этого добиться. Права вызывающего можно использовать для обеспечения доступа к различным схемам на основе текущей схемы, возвращаемой вызовом SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), но при этом надо позаботиться о согласованности схем и наличии необходимых привилегий (или учитывать возможные проблемы с этим в коде). Надо также быть готовым к более интенсивному использованию разделяемого пула и дополнительному расходу ресурсов сервера на анализ операторов. Работа с правами создателя идеально подходит почти для всех хранимых процедур. Процедуры с правами вызывающего — мощное средство, но использовать его надо только по назначению.
4 i ВйН :
•
.
,
•
:
•
:
•
leFrogra 1 onclOrac • • . • • " '
•
•
•
.
•
•:
:
еРго Professional© afumingProfess LFrogrammingP ifrdcleProgr.a |sianal§racie "P-ro/essio'i . ;. ammlngProfss : eProgr3firaing] ;sional§rac:
s*
иЯЯМШН!
Основные стандартные •огСбТсэ I
В этом разделе книги будут рассмотрены стандартные пакеты в базе данных, которые, по моему мнению, должен знать каждый. Все эти пакеты описаны в руководстве Oracle8i Supplied PL/SQL Packages Reference. В фирменной документации обычно описаны точки входа (общедоступные процедуры и функции) стандартных пакетов и использование каждой функции/процедуры. Я же рассмотрю подробно, когда имеет смысл использовать тот или иной пакет. Не вдаваясь глубоко в работу каждой процедуры, я уделю внимание наиболее часто используемым точкам входа и продемонстрирую, как они используются. Исчерпывающий список содержащихся в каждом пакете процедур и подробное описание параметров вы сможете найти в упомянутом документе. Освоив это приложение, вы будете хорошо ориентироваться в назначении стандартных пакетов. Мы изучим не все пакеты. Это не означает, что остальные пакеты менее полезны, просто при разработке они используются редко. Мы рассмотрим пакеты, описанные в приложении. •
DBMS_ALERT и DBMS_PIPE. Средства межпроцессного взаимодействия в базе данных. Пакет DBMS_ALERT можно использовать для уведомления всех заинтересованных сеансов об определенном событии. Пакет DBMS_PIPE позволяет двум сеансам взаимодействовать, аналогично тому, как это происходит через сокет TCP/IP.
• DBMS_APPLICATION_INFO. Позволяет приложению записать полезную информацию в представления V$. Незаменим в случае контроля действий хранимой процедуры и записи другой информации. •
DBMS_JAVA. PL/SQL-пакет, используемый для работы с хранимыми процедурами на языке Java.
538
Приложение А
Q DBMS_JOB. Планировщик заданий в базе данных. Используется, если необходимо выполнять хранимую процедуру, например, ежесуточно в 2 часа ночи или просто для выполнения какого-либо действия в фоновом режиме. О DBMS_LOB. Пакет для работы с большими объектами (Large OBjects — LOB) в базе данных. •
DBMS_LOCK. Пакет для создания пользовательских блокировок, независимых от блокировок уровня строки или таблицы, устанавливаемых сервером Oracle.
•
DBMS_LOGMNR. Пакет для просмотра и анализа содержимого активных журналов повторного выполнения.
Q DBMS_OBFUSCATION_TOOLKIT. Обеспечивает шифрование данных в базе. a
DBMS_OUTPUT. Обеспечивает простые средства вывода информации на экран из PL/SQL для среды SQL*Plus и SVRMGRL.
• DBMS_PROFILER. Профилировщик исходного кода PL/SQL, встроенный в базу данных. Q DBMS_UTILITY. "Сборная солянка" полезных процедур. •
UTL_FILE. Обеспечивает средства ввода-вывода текстовых (а в Oracle 9.2.x и двоичных — прим. научн. ред.) файлов в PL/SQL. Позволяет читать и записывать текстовые файлы на сервере с помощью PL/SQL.
Q UTL_HTTP. Обеспечивает работу по протоколу HTTP (Hyper Text Transfer Protocol — протокол передачи гипертекста) из среды PL/SQL. Позволяет загружать Web-страницы в PL/SQL. •
UTL_RAW. Обеспечивает преобразование данных типа RAW в VARCHAR2, и наоборот. Используется для работы с протоколом TCP/IP, при обработке больших объектов типа BLOB и BFILE, а также для шифрования.
Q UTL_SMTP. Обеспечивает работу по протоколу SMTP (Simple Mail Transfer Protocol — простой протокол передачи электронной почты) из среды PL/SQL. В частности, позволяет послать сообщение по электронной почте из PL/SQL-подпрограммы. Q UTLJTCP. Предоставляет средства работы с сокетами TCP/IP в языке PL/SQL. Позволяет из PL/SQL-кода подключиться к любой службе TCP/IP.
Когда используются стандартные пакеты Причина использования стандартных пакетов проста: гораздо проще разрабатывать и сопровождать код, использующий стандартные средства, чем создавать их самому. Если корпорация Oracle поставляет пакет для определенных целей (например, шифрования данных), не имеет смысла писать такой пакет самому. Часто я сталкиваюсь с тем, что разработчики по незнанию создают средства, уже существующие в базе данных. Знание того, какие готовые инструментальные средства имеются, существенно упрощает разработку.
Основные стандартные пакеты
53.7
О стандартных пакетах Имена всех стандартных пакетов Oracle начинаются с префиксов DBMS_ или UTL_. Имена пакетов, созданных разработчиками серверных технологий (Server Technologies — теми, кто писал ядро базы данных), обычно начинаются с префикса DBMS_. Пакеты с именами, начинающимися с UTL_, происходят из других источников. В качестве примера можно назвать ШЪ_НТТР — пакет для выполнения HTTP-запросов из PL/SQL (для получения Web-страниц и т.п.). Подразделение по разработке сервера приложений (Application Server Division) корпорации Oracle создало этот пакет для поддержки механизма ICX (Inter-Cartridge eXchange — обмен данными между картриджами) в сервере OAS (Oracle Application Server — сервер приложений Oracle), который сейчас заменен сервером iAS, (internet Application Server — сервер приложений Internet). Это различие имен для разработчиков практического значения не имеет — просто интересно отметить. Большинство этих пакетов хранится в базе данных в скомпилированном, скрытом (wrapped) формате. Благодаря этому код защищен от любопытных глаз. Можно увидеть спецификацию пакета, но не реализацию. Если выбрать из базы данных код тела пакета DBMS_OUTPUT, будет получено примерно следующее: tkyte@TKYTE816> s e l e c t t e x t 2 from all_source 3 where name = 'DBMS_OUTPUT' 4 and type = 'PACKAGE BODY' 5 and line < 10 6 order by line TEXT package body dbms_output wrapped 0 abed abed abed abed abed abed abed 9 rows selected. Как видите, пользы от этого мало. Можно, однако, выбрать спецификацию пакета.* tkyte@TKYTE816> s e l e c t t e x t 2 from all_source 3 where name = 'DBMS_OUTPUT' 4 and type - 'PACKAGE' 5 and line < 26 * В базе данных, естественно, все комментарии в спецификации - на английском. Оригинал можно получить, выполнив представленный ниже оператор SELECT. - Прим. научн. ред.
540
Приложение А 6 7
order by l i n e /
TEXT package dbms_output as — —
ОБЗОР
— — — —
Эти процедуры накапливают информацию в буфере (с помощью "put" и "put_line") так, что ее можно выбрать в дальнейшем (с помощью "get_line" или "get_lines"). Если пакет отключен, то все вызовы просто игнорируются. Таким образом, эти подпрограммы активны, только если клиент способен обработать получаемую информацию. Это хорошо подходит для отладки или написания хранимых процедур, с помощью которых выдаются сообщения или отчеты в среде sql*dba или plus (например, "описательных процедур" и т.п.). Стандартный размер буфера — 20000. Минимальный — 2000, а максимальный - 1000000.
—
ПРИМЕР
— —
—
— — —
Предположим, из триггера необходимо выдать отладочную информацию. Для этого в триггере можно вызвать dbms_output.put_line('Мы получили: '||:new.col|| ' — новое значение'); Если клиент включил пакет dbms_output, строка-аргумент put__line будет помещена в буфер и клиент, выполнив оператор (предположительно, оператор вставки, удаления или изменения, вызвавшего срабатывание триггера), сможет выполнить
25 rows selected. В базе данных скрыт оперативный источник документации. Спецификация каждого из рассматриваемых пакетов содержит достаточно полное описание назначения пакета, действий каждой функции и процедуры, а также их использования. Это очень удобно при отсутствии документации, но и при ее наличии тоже пригодится, поскольку спецификация иногда содержит данные, отсутствующие в документации, или полезные примеры. Далее мы рассмотрим пакеты, являющиеся большим подспорьем при постоянной работе с СУБД Oracle. Эти пакеты часто используются всеми разработчиками. Мы также рассмотрим новые пакеты и способы обхода некоторых ограничений этих встроенных пакетов, с которыми, как я знаю по своему опыту, часто сталкиваются разработчики.
Пакеты DBMS_ALERT и DBMSPIPE
Пакеты DBMS_ALERT и DBMS_PIPE — очень мощные средства межпроцессного взаимодействия. Оба они обеспечивают возможность взаимодействия сеансов базы данных. Пакет DBMS_ALERT по функциональности во многом аналогичен сигналам операционной системы UNIX, a DBMS_PIPE очень похож на именованный канал UNIX. Поскольку разработчики приложений часто сомневаются, какой пакет в каких случаях использовать, я решил описать их вместе. Пакет DBMS_ALERT создавался для того, чтобы сеанс мог сигнализировать об определенном событии в базе данных. Другие сеансы, которых касается это событие, получают уведомление о том, что оно произошло. Уведомления эти посылаются в рамках транзакций, т.е. можно сигнализировать о событии в триггере или хранимой процедуре, но пока соответствующая транзакция не зафиксирована, уведомление ожидающим сеансам не посылается. Если транзакция отменена, уведомление не будет послано. Важно понимать, что сеанс, которому необходимо получить уведомление о событии в базе данных, должен либо периодически опрашивать базу данных, не поступил ли соответствующий сигнал, либо просто ждать в заблокированном состоянии возникновения соответствующего события. Пакет DBMS_PIPE более универсален. Он позволяет одному или нескольким сеансам читать сообщения с одной стороны именованного канала и при этом записывать сообщения в этот канал с другой стороны. Только один из читающих сеансов может получить сообщение, причем адресовать сообщение конкретному сеансу по одному именованному каналу нельзя. Если читающих сеансов больше одного, прочитает записанное в канал сообщение любой из них. Каналы не поддерживают транзакции: если сообщение послано, оно будет доступным другим сеансам. Фиксировать транзакцию не
54.Z
Приложение А
надо, а фиксация или откат соответствующей транзакции не повлияет на результат передачи сообщения по каналу.
Когда использовать сигналы и каналы Основное отличие между сигналами и каналами — это поддержка (или отсутствие поддержки) транзакций. С помощью сигналов можно передать сообщение одному или нескольким сеансам после того, как некоторое действие успешно зафиксировано в базе данных. Каналы позволяют немедленно передать сообщение одному сеансу. Сигналы имеет смысл использовать, например, в следующих случаях. Q На экране в виде графической диаграммы отображаются данные о курсе акций. Когда информация о курсе изменяется в базе данных, необходимо уведомить приложение, чтобы изменить соответственно экран. •
При добавлении новой записи в таблицу необходимо выдать в приложении диалоговое окно, уведомляющее пользователя о новом задании.
Каналы базы данных имеет смысл использовать, например, в следующих случаях. Q На другой машине в сети работает процесс, выполняющий определенные действия. Надо послать процессу сообщение с требованием выполнить необходимое действие. В этом случае канал базы данных используется аналогично сокету TCP/IP. Q Необходимо поставить в очередь в области SGA данные, которые должны быть прочитаны и обработаны другим процессом. При этом канал базы данных используется как непостоянная очередь (FIFO), доступная для чтения нескольким различным сеансам. Можно привести и другие примеры использования пакетов, но это — наиболее типичные варианты использования сигналов и каналов, позволяющие понять, когда именно надо применять каждый из пакетов. Сигналы используются для уведомления множества пользователей о произошедшем событии (после фиксации). Каналы используются для немедленной передачи сообщения другому сеансу (и, как правило, ожидания от него ответа). Теперь, когда назначение сигналов и каналов понятно, рассмотрим детали реализации каждого из этих механизмов.
Настройка Пакеты DBMS_ALERT и DBMS_PIPE стандартно устанавливаются в базе данных. В отличие от многих стандартных пакетов, привилегия EXECUTE для этих пакетов роли PUBLIC не предоставляется. В Oracle 8.0 и выше привилегия EXECUTE для этих пакетов предоставляется роли EXECUTE_CATALOG_ROLE. В предыдущих версиях никакие привилегии на эти пакеты по умолчанию не предоставлялись. Поскольку привилегия EXECUTE предоставлена роли, причем — не PUBLIC, вы не сможете создать хранимую процедуру, зависящую от этих пакетов, поскольку при компиляции хранимого кода роли никогда не действуют. Необходимо явно предоставить привилегию EXECUTE соответствующей учетной записи.
Пакеты DBMS_ALERT и DBMS_PIPE
543
Пакет DBMS ALERT Пакет DBMS_ALERT — очень небольшой и содержит всего семь точек входа. Я опишу здесь шесть наиболее интересных. Если для приложения требуется получить уведомление, то наиболее существенными окажутся следующие подпрограммы. •
REGISTER. Зарегистрироваться на получение указанного сигнала. В сеансе можно многократно вызывать подпрограмму REGISTER с разными именами сигналов, чтобы получать уведомления при наступлении одного из нескольких событий.
•
REMOVE. Снять регистрацию на получение сигнала, чтобы сервер не пытался уведомить о наступлении события.
Q REMOVEALL. Снять для сеанса регистрацию на получение всех сигналов. Q WAITANY. Ожидать уведомления о сигналах, на получение которых сеанс зарегистрирован. Эта подпрограмма выдает имя поступившего сигнала и обеспечивает доступ к сопровождающему его короткому сообщению. Ждать можно либо заданное время, либо вообще не ждать (что позволяет из приложения эпизодически опрашивать систему, чтобы узнать, не произошло ли событие, не блокируя при этом дальнейшую обработку ожиданием события). Q WAITONE. Ожидать уведомления об указанном сигнале. Как и в случае WAITANY, ждать можно определенное время или вообще не ждать. Приложение, желающие послать сигнал, или уведомить о событии, может сделать это с помощью следующей подпрограммы. •
SIGNAL. Послать сигнал о наступлении события при фиксации текущей транзакции. При откате посылка сигнала отменяется.
Итак, пакет DBMS_ALERT очень просто использовать. Клиентское приложение, для которого требуется получение уведомления о событии, может содержать код вида: tkyte@TKYTE816> begin 2 dbms_alert.register('MyAlert'); 3 end; 4 / PL/SQL procedure successfully completed. tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> declare 2 l_status number; 3 l_msg varchar2(1800) ; 4 begin 5 dbms_alert.waitone(name => 'MyAlert', 6 message => l_msg, 7 status => l_status, 8 timeout => dbms_alert.maxwait); 9 10 if (l_status = 0) 11 then 12 dbms_output.put_line('Сообщение события: ' | | l_msg);
544 13 14 15
Приложение А end
if;
end; /
Мы зарегистрировались на получение сигнала MyAlert, а затем вызвали процедуру DBMS_ALERT.WAITONE, ожидая поступление этого сигнала. Обратите внимание, что, поскольку использована константа DBMS_ALERT.MAXWAIT из пакета DBMS_ALERT, сеанс при выполнении этого вызова начнет ждать бесконечно. Сеанс блокируется в ожидании соответствующего события. Можно задать для клиентского приложения период ожидания 0 секунд, что исключит ожидание, и опрашивать сервер о наступлении события. Например, приложение Oracle Forms может использовать ежеминутно срабатывающий таймер, вызывающий процедуру DBMS_ALERT.WAITONE, чтобы узнать, не произошло ли некоторое событие. Если событие произошло, экран приложения изменяется. Можно с такой же частотой активизировать поток Java, проверяющий, не произошло ли событие, и обновляющий совместно используемую структуру данных, и т.д. Чтобы послать этот сигнал, достаточно выполнить следующее: tkyte@TKYTE816> exec dbms_alert.signal('MyAlert',
'Hello World');
PL/SQL procedure successfully completed. tkyte@TKYTE816> commit; Commit complete. в другом сеансе. В сеансе, заблокированном в ожидании события, вы должны немедленно увидеть: 15
/
Сообщение события: Hello World PL/SQL procedure successfully completed. To есть сеанс больше не заблокирован. Я продемонстрировал наиболее типичный вариант использования пакета DBMS_ALERT. Сеансы ждут сигнала с определенным именем. Пока в пославшем сигнал сеансе транзакция не зафиксирована, уведомление о сигнале не посылается. В этом легко убедиться с помощью двух сеансов SQL*PIus . Работа с сигналами становится более интересной, если задаться следующими вопросами. Q Что происходит, если несколько сигналов более-менее одновременно отправляются разными сеансами? Р Что происходит, если посылать сигнал несколько раз: сколько сигналов будет сгенерировано в конечном итоге? Q Что происходит, если более одного сеанса пошлет сигнал, после того как я зарегистрировался на его получение, но до вызова одной из процедур ожидания? А что произойдет, если несколько сеансов пошлют сигнал в промежутке между вызовами процедур ожидания? Ответы на эти вопросы позволят выявить побочные эффекты использования сигналов, которые необходимо учитывать. Я также предложу способы избежать некоторых проблем, связанных со всем изложенным выше.
Пакеты DBMS_ALERT и DBMS_PIPE
545
Одновременные сигналы нескольких сеансов Если повторно выполнить рассмотренный пример, зарегистрировавшись на получение сигнала MyAlert и ожидая его в одном сеансе, а затем запустить два дополнительных сеанса, можно будет увидеть, что произойдет при одновременной передаче сигналов из нескольких сеансов. На этот раз в обоих сеансах мы выполним: tkyte@TKYTE816> exec dbms_alert.signal('MyAlert 1 ,
'Hello World 1 );
(транзакции не фиксируются). Окажется, что сеанс, пославший сигнал вторым, заблокирован. Это показывает, что если N сеансов одновременно пытаются послать один и тот же сигнал, N-1 из них будут заблокированы при вызове DBMS_ALERT.SIGNAL. Продолжит работу только один из сеансов. Сигналы должны посылаться последовательно, и следует позаботиться о предотвращении подобных проблем. Должна обеспечиваться возможность одновременного доступа к базе данных множества сеансов. Пакет DBMS_ALERT — одно из тех средств, которое существенно снижает масштабируемость по этому показателю. Если создать для таблицы триггер на событие INSERT, а в коде этого триггера использовать вызов DBMS_ALERT.SIGNAL, при выполнении множества операторов INSERT все они выстроятся в очередь, если хоть один сеанс зарегистрировался на получение соответствующего сигнала. Поэтому имеет смысл ограничить количество сеансов, посылающих сигналы. Например, при получении оперативных данных из внешнего источника пакет DBMS_ALERT вполне можно использовать, если данные в таблицу вставляет только один сеанс. Если же речь идет о таблице аудита, в которую часто вставляют данные все сеансы, средства пакета DBMS_ALERT лучше не использовать. Один из способов избежать выстраивания сеансов в очередь — использовать пакет DBMS_JOB (которому посвящен специальный раздел в этом приложении). Можно написать процедуру, действия которой будут сводиться к передаче сигнала и фиксации транзакции: tkyte@TKYTE816> c r e a t e t a b l e alert_mess a i 3 e s 2 (job_id i n t primary key, 3 alert_name varchar2(30), 4 message varchar2(2000) 5 ) 6 / Table c r e a t e d . tkyte@TKYTE816> create or replace procedure background a l e r t ( p job in i n t ) 2 as 3 l_rec alert_messages%rowtype; 4 begin 5 select * into l_rec from alert_messages where job_id = p_job; 6 7 dbms alert.signal(l rec.alert name, 1 rec.message); 8 delete from alert_messages where job_id = p_job; 9 commit; 10 end; 11 / Procedure created. 18 3ax. 244
546
Приложение А
Тогда соответствующий триггер будет иметь вид: tkyte@TKYTE816> create table t (x i n t ) ; Table created. tkyte@TKYTE816> create or replace trigger t_trigger 2 after insert or update of x on t for each row 3 declare 4 l_job number; 5 begin 6 dbms_j ob.submit(l_j ob, 'background_alert(JOB);'); 7 insert into alert_messages 8 (job_id, alert_name, message) 9 values 10 (l_job, 'MyAlert', 'X в Т имеет значение ' I I :new.x); 11 end; 12 / Trigger created. И будет обеспечивать передачу сигнала фоновым процессом после фиксации. При этом: Q сигналы посылаются в рамках транзакций; •
приоритетные процессы (интерактивные приложения) в очередь не выстраиваются.
Недостаток этого подхода в том, что задания могут выполняться не сразу; может пройти некоторое время, прежде чем сигнал будет послан. Во многих случаях это приемлемо (важно уведомить ожидающие процессы о произошедшем событии, даже если и с небольшим опозданием). Средства расширенной поддержки очередей (advanced queues — AQ) также обеспечивают хорошо масштабируемый способ уведомления о событиях а базе данных. Использовать их сложнее, чем средства пакета DBMS_ALERT, но при этом обеспечивается большая гибкость.
Неоднократная передача сигнала в сеансе Теперь попытаемся ответить на вопрос, что произойдет, если послать одноименный сигнал в приложении несколько раз, а затем зафиксировать транзакцию? Сколько сигналов фактически будет послано? В данном случае ответ простой: один. Работа пакета DBMS_ALERT аналогична механизму передачи сигналов в ОС UNIX. В UNIX сигналы посылаются для уведомления процессов о событиях в операционной системе. Пример такого события — 'I/O is ready' (готовность к вводу-выводу), которое означает, что один из открытых файлов (или сокетов и т.п.) готов для продолжения операций вводавывода. Этот сигнал можно, например, использовать при создании сервера на основе протоколов TCP/IP. Операционная система уведомит вас, когда в одном из открытых сокетов появятся данные, ожидающие чтения; то есть не придется постоянно опрашивать состояние каждого сокета, проверяя, нет ли в нем данных для чтения. Если ОС пять раз определила, что в сокете есть данные для чтения, но у нее не было возможности уведомить об этом приложение, она не будет повторять сообщение пять раз. Вы полу-
Пакеты DBMS_ALERT и DBMS_PIPE
5 4 7
чите уведомление о событии "из сокета X можно читать", но не всю хронологию предыдущих событий по этому сокету. Пакет DBMS_ALERT работает точно так же. Возвращаясь к рассматриваемому примеру, можно выполнить фрагмент кода, регистрирующий сеанс на уведомление о событии и вызывающий процедуру WAITONE в ожидании этого события. В другом сеансе выполняем: tkyte@TKYTE816> begin 2 for i in 1 .. 10 loop 3 dbms_alert.signal('MyAlert', 'Сообщение ' | | 1 ) ; 4 end loop; 5 end; 6 / PL/SQL procedure successfully completed. 3d. tkyte@TKYTE816> commit; Commit complete. И в первом окне получаем результат: Сообщение события: сообщение 10 PL/SQL procedure successfully completed. Послано будет только последнее сообщение, о котором мы сигнализировали. Промежуточных сообщений никто никогда не увидит. Следует учитывать, что пакет DBMS_ALERT будет, как и задумано создателями, отбрасывать все предыдущие сообщения сеанса. С помощью этого пакета нельзя отправить в транзакции последовательность сообщений — это только механизм сигнализации. Он позволяет уведомить клиентское приложение, что "нечто произошло". Если вы предполагаете, что каждое событие, о котором вы уведомляли с помощью сигнала, будет получено всеми заинтересованными сеансами, — вас ждет разочарование (написанный на основе этого предположения код будет скорее всего ошибочен). Для решения этой проблемы можно использовать пакет DBMS_JOB, если для уведомления о каждом событии применять его средства. Однако можно использовать и другую технологию. С помощью средств расширенной поддержки очередей (которые в этой книге не рассматриваются) справиться с этой задачей намного проще.
Передача многочисленных сигналов несколькими сеансами до вызова процедуры ожидания Это последний вопрос: что произойдет, если сигнал будет послан несколькими сеансами после того, как на него поступил запрос, но прежде, чем вызвана процедура ожидания? Аналогичный вопрос: что произойдет, если между вызовами процедур ожидания несколько сеансов пошлют сигнал? Как и в случае многократного вызова DBMS_ALERT.SIGNAL в одном сеансе, запоминается только последний сигнал, и именно о нем получат уведомление сеансы. В этом можно убедиться, добавив команду PAUSE к используемому в примерах сценарию SQL*Plus:
54о
Приложение А
begin dbms_alert.register('MyAlert'); end; pause Затем в других сеансах вызовите процедуры DBMS_ALERT.SIGNAL с уникальными сообщениями (чтобы их можно было различать) и зафиксируйте каждое сообщение. Например, измените представленный ранее простой цикл следующим образом: tkyte@TKYTE816> begin 2 for i in 1 .. 10 loop 3 dbms_alert.signal('MyAlert', 'Сообщение ' II i ) ; 4 commit; 5 end loop; 6 end; 7 / PL/SQL procedure successfully completed. После этого в исходном сеансе просто нажмите клавишу Enter, и блок кода, вызывающий процедуру WAITONE, будет выполнен. Поскольку ожидаемый сигнал уже послан, этот блок кода немедленно завершит работу и выдаст строку, свидетельствующую о получении последнего сообщения (о чем оповестил сигнал). Все промежуточные сообщения других сеансов потеряны, как и было задумано создателями пакета. Итак, пакет DBMS_ALERT подходит для тех случаев, когда необходимо уведомить о событиях в базе данных множество клиентов. Об этих именованных событиях должно сообщать как можно меньше сеансов, из-за существенных проблем с очередностью доступа к процедурам пакета DBMS_ALERT. Поскольку неоднократные сообщения теряются, пакет DBMS_ALERT подходит в качестве средства уведомления о событии. Его можно использовать для уведомления клиента, например, об изменении данных в таблице Т, но попытка использовать его для уведомления об изменениях в отдельных строках таблицы Т закончится неудачей (поскольку сохраняется только последнее сообщение). Пакет DBMS_ALERT очень прост в использовании и практически не требует настройки.
Пакет DBMS_PIPE DBMS_PIPE — это стандартный пакет, обеспечивающий обмен данными между двумя сеансами. Это средство межпроцессного взаимодействия. Один сеанс может записывать сообщение в программный канал, а другой — читать это сообщение. В ОС UNIX аналогичный механизм реализован в виде именованного канала операционной системы. С помощью именованных каналов можно обеспечить запись данных одним процессом для другого. Пакет DBMS_PIPE, в отличие от DBMS_ALERT, — это пакет, работающий в режиме реального времени. При вызове функции SEND_MESSAGE немедленно посылается сообщение. Сервер не ждет выполнения оператора COMMIT; передача сообщения выполняется вне транзакции. Это позволяет использовать пакет DBMS_PIPE в тех случаях, когда DBMS_ALERT не подходит (и наоборот). С помощью пакета
Пакеты DBMS_ALERT и DBMS_PIPE
549
DBMS_PIPE можно обеспечить диалоговое взаимодействие двух сеансов (что с помощью DBMS_ALERT сделать невозможно). Один сеанс может "попросить" другой выполнить некоторое действие. Выполнив его, второй сеанс возвращает первому результат. Предположим, второй сеанс — это С-программа, которая снимает показания термометра, подключенного к последовательному порту компьютера, и возвращает значение температуры первому сеансу. Первому сеансу надо записать текущую температуру в базу данных. Он может послать сообщение "дай мне значение температуры" второму сеансу, который определяет это значение и выдает ответ первому сеансу. Первый и второй сеансы могут работать на разных компьютерах, главное, оба они подключены к одной базе данных. Я использую базу данных примерно так же, как сеть TCP/IP — для обеспечения взаимодействия между двумя процессами. Однако при использовании пакета DBMS_PIPE мне не нужно знать имя хоста и номер порта для подключения — достаточно имени канала базы данных, в который надо отправить запрос. В базе данных есть два типа каналов — общедоступные и пользовательские. Общедоступный канал можно создать явно, вызвав CREATE_PIPE, либо неявно, послав в него сообщение. Основное отличие между явно и неявно созданными каналами состоит в том, что канал, созданный явным вызовом CREATE_PIPE, удаляется приложением по завершении работы, тогда как неявно созданный канал удаляется из области SGA как устаревший после определенного промежутка времени. Общедоступный канал устроен так, что любой сеанс, имеющий доступ к пакету DBMS_PIPE, может читать и записывать в него сообщения. Поэтому общедоступные каналы не подходят для передачи секретных или просто важных данных. Поскольку каналы обычно используются для диалога, а общедоступные каналы позволяют перехватывать или вмешиваться в этот диалог любому, злонамеренный пользователь может удалять сообщения из канала либо добавлять свои, "мусорные". Любое из этих действий нарушает диалог или протокол обмена данными между сеансами. Поэтому в большинстве приложений применяются пользовательские каналы. К данным в пользовательских каналах можно обращаться только сеансам, работающим с эффективным идентификатором пользователя-владельца канала, или от имени специальных пользователей SYS и INTERNAL. Это означает, что только с помощью хранимых процедур с правами создателя (см. главу 23, посвященную правам создателя и вызывающего), принадлежащих владельцу канала, либо в сеансах от имени владельца канала, пользователя SYS или INTERNAL можно читать или записывать данные в этот канал. Это существенно увеличивает надежность каналов, поскольку ни один другой сеанс или код не может вмешаться в протокол или перехватить данные. Канал — это объект в области SGA экземпляра Oracle. Этот механизм вообще не связан с диском. Данные в каналах теряются при остановке и перезапуске сервера. Чаще всего каналы используют для создания специализированных служб или серверов. До появления внешних процедур в Oracle 8.0 они были единственным способом реализовать хранимые процедуры на языке, отличном от PL/SQL. Для этого создавался "канальный" сервер. Компонент ConText (предшественник компонента interMedia Text) был реализован в Oracle 7.3 с помощью каналов базы данных и с тех пор так и работает. Со временем часть его функций была реализована с помощью внешних процедур, но большая часть механизмов индексирования по-прежнему реализуется с помощью каналов.
550
Приложение А
Поскольку делать попытку читать данные из канала и писать их туда может любое количество сеансов, необходимо реализовать алгоритм, гарантирующий доставку сообщений нужному сеансу. Если предполагается создание специализированной службы (например, представленного ранее сервера температуры) и ее добавление в базу данных, необходимо гарантировать получение ответа, предназначенного сеансу А, именно сеансом А, а не сеансом В. Для удовлетворения этого стандартного требования обычно запрос выдается в один канал с общеизвестным именем, а вместе с обращением передается уникальное имя канала, из которого мы хотим прочитать ответ. Это можно проиллюстрировать следующей схемой: Приложение А Oracle8i
Какая температура? Ответ в канале "А" Канал температуры Какая температура?
Сервер температуры
Ответ в канале "В"
Приложение В
Шаг 1. Сеанс А пишет свое обращение — "Какая сейчас температура? Ответить в канал А" — в известный всем сеансам "канал температуры". Одновременно это могут делать и другие сеансы. Каждое сообщение помещается в канал по принципу очереди — "первым пришел, первым обслужен". Шаг 2. Сервер температуры читает одно сообщение из канала и запрашивает соответствующую службу, к которой он обеспечивает доступ. Шаг 3. Сервер температуры использует уникальное имя канала, которое запрашивающий сеанс (в данном случае канал А) указал в сообщении, для записи ответа. Для ответа используется неявный канал (так что канал ответа исчезает сразу после того, как работа с ним завершена). Если подобных вызовов предполагается много, имеет смысл использовать явно созданный канал, чтобы он сохранялся в области SGA в течение всего сеанса (не забудьте удалить его перед завершением сеанса!). Шаг 4. Сеанс А читает ответ из канала, который он указал серверу температуры для записи. Такая же последовательность событий произойдет и для сеанса В. Сервер температуры будет читать обращение, запрашивать температуру, определять по запросу канал, в который надо выдать ответ, и записывать ответ в него. Одна из интересных особенностей каналов базы данных — возможность читать из канала несколькими сеансами. Помещенное в канал сообщение будет прочитано только
Пакеты DBMS_ALERT и DBMS_PIPE
551
одним сеансом, но читать сообщения из канала может несколько сеансов одновременно. Это позволяет масштабировать представленную выше схему. По ней понятно, что запрашивать данные у сервера температуры может несколько сеансов, и он будет последовательно, по одному, обрабатывать эти запросы. Но ничего не мешает запустить несколько серверов температуры: Oracle8i
cZ С (
А Канал температурь
С ^
В __
С
С С
X -
Сервер температуры
>= Сервер температуры i
к г
75
Теперь можно обрабатывать два запроса одновременно. Если запустить пять экземпляров сервера, обрабатывать можно одновременно пять запросов. Это похоже на пул подключений или на работу многопотокового сервера Oracle. Имеется пул процессов, готовых к работе, и максимальный объем одновременно выполняемых действий в каждый момент времени определяется количеством запущенных процессов. Эта особенность каналов базы данных позволяет легко масштабировать систему.
Серверы каналов или внешние процедуры? В Oracle8 версии 8.0 впервые появилась возможность реализовать хранимые процедуры непосредственно на языке С, а сервер Oracle8i позволил создавать их также на языке Java. С учетом этого, нужны ли теперь пакет DBMS_PIPE и серверы каналов? Однозначный ответ: да. В главе, посвященной использованию внешних процедур, была описана соответствующая архитектура. Внешние процедуры на языке С, например, выполняются в отдельном адресном пространстве по отношению к хранимой процедуре на PL/SQL. Имеется однозначное соответствие между количеством сеансов, одновременно использующих внешние процедуры, и количеством созданных отдельных адресных пространств. Т.е., если 50 сеансов одновременно вызовут внешнюю процедуру, будет запущено 50 процессов или, по крайней мере, потоков EXTPROC. Механизм поддержки внешних процедур на языке С похож на механизм работы выделенного сервера Oracle. Сервер Oracle запускает отдельный серверный процесс для каждого из одновременно работающих сеансов; точно так же он запускает экземпляр EXTPROC для каждого из одновременных вызовов внешних подпрограмм. Внешние процедуры на Java выполняются аналогично: с соответствием один к одному. Для каждого сеанса, использующего внешнюю процедуру на Java, запускается отдельный экземпляр JVM, с собственной информацией о состоянии и специально выделенными ресурсами.
Приложение А
Сервер каналов, с другой стороны, подобен многопотоковому серверу (MTS) в Oracle. Вы создаете пул разделяемых ресурсов (запускаете N серверов каналов), и они обслуживают обращения. Если одновременно поступает больше обращений, чем можно обработать, они выстраиваются в очередь. Это очень похоже на многопотоковый режим работы сервера Oracle, когда обращения поступают в очередь в области SGA и выбираются из очереди разделяемым сервером после обработки им предыдущего обращения. Это хорошо демонстрирует ранее рассмотренный пример сервера температуры. На первой схеме показан один сервер канала; он определяет и выдает температуру клиентам по одному. На второй схеме представлены два сервера канала, обслуживающие все поступающие обращения. Одновременно к термометру будут обращаться не более двух клиентов. Это важно потому, что позволяет ограничить одновременный доступ к этому разделяемому ресурсу. Если бы использовались внешние процедуры и температуру запросили бы одновременно 50 сеансов, они могли бы "разрушить" программное обеспечение термометра, если оно не способно поддерживать такое количество одновременных обращений. Замените термометр любым другим разделяемым ресурсом, и возникнут такие же проблемы. Можно допустить несколько одновременных обращений, но если их будет много, то либо произойдет сбой, либо производительность снизится настолько, что практически ресурс работать перестанет. Еще одна причина использования сервера каналов может быть связана с тем, что для подключения к разделяемому ресурсу требуется много времени. Например, пару лет назад я работал над проектом в большом университете. Требовалось выполнять транзакции на мэйнфрейме (необходимо было обращаться к мэйнфрейму для получения информации о студентах). Подключение к мэйнфрейму могло потребовать от 30 до 60 секунд, но после этого информация выдавалась очень быстро (если только мы не перегружали мэйнфрейм огромным количеством одновременных обращений). Используя сервер каналов, мы смогли подключаться к серверу один раз, при запуске сервера каналов. Сервер каналов работал много дней, используя первоначальное подключение. Если бы использовалась внешняя процедура, пришлось бы инициировать подключение для каждого сеанса. Реализация на основе внешних процедур в этой среде просто не работала бы из-за продолжительности подключения к мэйнфрейму. Сервер каналов не только позволил ограничить количество одновременных обращений к мэйнфрейму, но и выполнять продолжительный процесс подключения один раз, а затем использовать это подключение сотни тысяч раз. Если вы знакомы с обоснованием использования программного обеспечения для организации пула подключений в трехкомпонентной среде, вам и так понятно, зачем могут понадобиться каналы. Они позволяют повторно использовать результаты выполнения продолжительной операции (подключения к базе данных, в случае ПО для организации пула подключений) и ограничить объем потребляемых одновременно ресурсов (размер пула подключений). Последнее отличие сервера каналов от внешних процедур связано с тем, где может работать сервер каналов. Предположим, в случае сервера температуры сервер баз данных работает на платформе Windows. Контроль температуры осуществляется на UNIX-машине. При этом все доступные серверу библиотеки тоже находятся в ОС UNIX. Поскольку сервер каналов — всего лишь клиент базы данных, его можно запрограммировать,
Пакеты DBMS_ALERT и DBMS_PIPE
5 5 3
скомпилировать и запустить в среде UNIX. Сервер каналов необязательно должен работать на той же машине и даже аппаратной платформе, что и сервер баз данных. Внешняя же процедура должна выполняться на той же машине, что и сервер базы данных, — такие процедуры нельзя выполнять на удаленной машине. Поэтому сервер каналов можно использовать в ситуациях, когда внешнюю процедуру использовать невозможно.
Пример в сети Internet На Web-сайте издательства (http://www.apress.com) можно найти пример небольшого сервера каналов. Он отвечает на часто задаваемый вопрос: как выполнить команду базовой операционной системы из PL/SQL? После добавления поддержки Java и внешних процедур на языке С, такую функцию выполнения команд можно легко реализовать с помощью любой из этих технологий. А если компилятора языка С просто нет или поддержка Java в базе данных не установлена — что тогда? Этот пример показывает, как создать небольшой "сервер каналов", способный выполнять команды операционной системы, используя только утилиту SQL*Plus и командный интерпретатор csh. Сервер получился сравнительно простым, содержащим лишь несколько строк кода командного интерпретатора csh и еще меньше текста на языке PL/SQL. Однако он показывает основные возможности каналов базы данных и должен натолкнуть вас на идеи по созданию других полезных приложений.
Резюме Каналы базы данных — мощное средство сервера Oracle, позволяющее двум сеансам вести диалог. Созданные по аналогии с именованными каналами в ОС UNIX, они позволяют разработать собственный протокол пересылки и получения сообщений. Небольшой пример, представленный на сайте издательства Apress, демонстрирует, как создать "сервер каналов" — внешний процесс, получающий обращения от сеансов базы данных и выполняющий для них кое-какие специальные действия. Каналы базы данных работают вне транзакций, что отличает их от сигналов, но именно это делает их во многих случаях незаменимыми. Я использовал каналы базы данных, в частности, для реализации следующих возможностей: •
передачи сообщении электронной почты;
•
распечатывания файлов;
а
интеграции с другими источниками данных, находящимися вне баз данных Oracle и не поддерживающими язык SQL;
Q реализации аналога процедуры DBMS_LOB.LOADFROMFILE для типов данных LONG и LONG RAW.
Пакет DBMSJ\PPLICATION_INFO
Это — один из наименее используемых стандартных пакетов, хотя его функциональные возможности пригодятся в любом приложении. Интересовались ли вы когда-нибудь следующими вопросами: • Что делает сеанс, какая форма обрабатывается, какой код выполняется в модуле? •
Насколько близко к завершению выполнение хранимой процедуры?
Q Насколько близко к завершению выполнение пакетного задания? О Какие значения связываемых переменных использованы в запросе? Пакет DBMS_APPLICATION_INFO можно использовать для получения ответов на эти и многие другие вопросы. Он позволяет устанавливать значения трех столбцов — CLIENT_INFO, ACTION и MODULE — соответствующей сеансу строки в представлении V$SESSION. Пакет предоставляет функции не только для установки этих значений, но и для их получения. Кроме того, один из параметров встроенных функций USERENV и SYS_CONTEXT позволяет получить значения столбца CLIENT_INFO в запросе. Можно, например, выполнить SELECT USERENV('CLIENT_INFO') F R O M DUAL или использовать конструкцию WHERE S O M E _ C O L U M N = SYS_CONTEXT(USERENV7CLIENT_INFO) в запросах. Значения, устанавливаемые в представлениях V$, сразу же доступны в других сеансах. Фиксировать их не нужно, что позволяет эффективно использовать эти представления для взаимодействия с "внешним миром". Наконец, пакет DBMS_APPLICATION_INFO позволяет задать значения в представлении динамической производительности V$SESSION_LONGOPS (LONG OPerationS). Это удобно для записи сделанного в продолжительных заданиях.
Пакет DBMS_APPLICATIONJNFO
555
Многие инструментальные средства Oracle, например SQL*Plus, уже используют возможности этого пакета. Я создал сценарий SHOWSQL.SQL, позволяющий узнать, какие SQL-операторы пользователи выполняют в настоящий момент в базе данных (этот сценарий можно найти на Web-сайте издательства Apress, http://www.apress.com). Часть этого сценария выбирает данные из представления VSSESSION, в которых значения столбцов CLIENTJNFO, MODULE или ACTION непустые (NOT NULL). При запуске этого сценария я получаю, например, следующие результаты: USERNAME
MODULE
OPS$TKYTE(107,19225) OPS$TKYTE(22,50901)
01@ s h o w s q l . s q l SQL*Plus
ACTION
CLIENT_INFO
Первая строка показывает, что текущий сеанс выполняет сценарий SHOWSQL.SQL на уровне 01. Это означает, что сценарий вызван непосредственно, а не из другого сценария. Если создать сценарий TEST.SQL с единственной строкой @SHOWSQL, то для представления этого вложенного вызова утилита SQL*Plus установит для сценария SHOWSQL уровень вызова 02. Вторая строка показывает другой сеанс SQL*Plus. В нем сейчас никакие сценарии не выполняются (возможно, в нем выполняется команда, введенная непосредственно в командной строке). Если добавить соответствующие вызовы процедур пакета DBMS_APPLICATION_INFO в приложение, можно добиться такого же результата, расширив возможности контроля работы приложения для его создателя и администратора базы данных. Для установки соответствующих значений в представлении VSSESSION используются следующие вызовы. Q S E T _ M O D U L E . Эта процедура позволяет установить в представлении VSSESSION значения столбцов MODULE и ACTION. Имя модуля должно быть не длиннее 48 байт, а значение поля, описывающего действие, не должно быть длиннее 32 байт. В качестве имени модуля обычно указывается имя приложения. Первым действием можно указать STARTUP или INITIALIZING, чтобы отметить начало работы приложения. •
SET_ACTION. Эта процедура позволяет установить в представлении VSSESSION значение столбца ACTION (действие). Это значение должно быть таким, чтобы можно было понять, какая часть кода программы выполняется. В качестве действия можно указывать, например, имя текущей активной экранной формы, имя функции в программе на Рго*С или имя PL/SQL-подпрограммы.
Q SET_CLIENT_INFO. Эта процедура позволяет сохранить до 64 байт специфической информации о приложении, которая может понадобиться. Обычно (см. далее) так сохраняются параметры представления или запроса. Имеются процедуры и для чтения соответствующей информации из представлений. Помимо установки значений в представлении VSSESSION этот пакет позволяет устанавливать значения в представлении динамической производительности V$SESSION_LONGOPS. Это представление позволяет сохранять несколько строк информации в различных столбцах. Вскоре мы более подробно рассмотрим эту возможность.
5 JD
Приложение А
Использование информации о клиенте Процедура SET_CLIENT_INFO позволяет установить не только значения в столбцах представления VSSESSION, но и значение переменной CLIENT_INFO, которое можно получить с помощью встроенных функций userenv (в версиях Oracle 7.3 и выше) или sys_context (именно ее лучше использовать в версиях Oracle 8i и выше). Например, можно создать параметризованное представление, результаты выборки данных из которого зависят от значения переменной CLIENT_INFO. Эту идею можно проиллюстрировать следующим примером: scott@TKYTE816> exec
dbms_application_info.set_client_info('KING');
PL/SQL procedure successfully completed. scott@TKYTE816> s e l e c t userenv('CLIENT_INFO') from dual; USERENV('CLIENT_INFO') KING scott@TKYTE816> select sys_context('userenv','client_infо•)from dual; SYS CONTEXT!1USERENV1,'CLIENT INFO') KING scott@TKYTE816> create or replace view 2 emp_view 3 as 4 select ename, empno 5 from emp 6 where ename = sys_context('userenv', •client_infо'); View created. scott@TKYTE816> select * from emp_view; ENAME KING
EMPNO 7839
scott@TKYTE816> exec dbms_application_infо.set_client_infо('BLAKE'); PL/SQL procedure successfully completed. scott@TKYTE816> select * from emp_view; ENAME
EMPNO
BLAKE
7698
Как видите, можно установить соответствующее значение и легко использовать его в запросах вместо константы. Это позволяет создавать сложные представления с условиями, значения которых уточняются при выполнении. При использовании представлений могут возникать проблемы, связанные с включением условия (predicate merging). Если оптимизатор включает условие в определение представления, запрос из представления выполняется очень быстро. В противном случае запрос выполняется медленно. Используя значение переменной CLIENT INFO, можно включить условие заранее, если
Пакет DBMS_APPLICATION_INFO
557
оптимизатор не может этого сделать. Разработчик приложения должен установить переменной соответствующее значение и выполнить SELECT * из представления. В результате он получит нужные данные. Средства пакета DBMS_APPLICATION_INFO я применяю также для записи значений связываемых переменных, используемых в запросе и другой необходимой информации, позволяющей легко понять, что именно делают процедуры. Например, если включить в долго выполняющийся процесс следующие вызовы: tkyte@TKYTE816> declare 1 2 l_owner varchar2(30) default 'SYS ; 3 l_cnt number default 0; 4 begin 5 dbms_application_inf о. set__client_inf о (' owner»' | | l_owner); 6 7 for x in ( s e l e c t * from a l l _ o b j e c t s where owner = l_owner) 8 loop 9 l_cnt := l _ c n t + l ; 10 dbms_application_info.set_action('processing row ' II l_cnt); 11 end loop; 12 end; 13 / то с помощью сценария SHOWSQL.SQL можно получить такую информацию: tkyte@TKYTE816> USERNAME
@showsql SID
TKYTE TKYTE
8 11
SERIAL* PROCESS
STATUS
206 780:716 ACTIVE 635 1004:1144 ACTIVE
TKYTE(11, 635) ospid = 1004:1144 program = SQLPLUS.EXE Saturday 15: 59 Saturday 16:15 SELECT * FROM ALLJDBJECTS WHERE OWNER = :bl USERNAME
MODULE
TKYTE(8,206) TKYTE(11,635)
01@ s h o w s q l . s q l SQL*Plus p r o c e s s i n g row owner=SYS 5393
ACTION
CLIENT_INFO
Сеанс (11,635) выполняет запрос SELECT * FROM ALL_OBJECTS WHERE OWNER = :B1. Отчет также показывает, что запрос выполнен пользователем SYS (owner=SYS) и что в момент вызова сценария showsql он уже обработал 5393 строки. В следующем разделе мы увидим, как с помощью представления V$SESSION_LONGOPS получить еще более точную информацию, если заранее известно, сколько действий или шагов будет выполнять процедура.
Использование представления V$SESSION_LONGOPS Многие действия в базе данных могут выполняться достаточно долго. К таким действиям относятся, в частности, восстановление с помощью Recovery Manager, сортировка
558
Приложение А
или загрузка больших объемов данных, выполнение сложных запросов, требующих распараллеливания. При выполнении этих продолжительных действий информация о ходе работы записывается в представление динамической производительности V$SESSION_LONGOPS, что позволяет оценить объем сделанного. Это представление можно использовать и в приложениях. В представлении отражается состояние действий, выполняющихся в базе данных более шести секунд. Другими словами, в функции сервера Oracle, которые, по предположению разработчиков, обычно выполняются дольше шести секунд, включены вызовы процедуры, вставляющей данные в представление V$SESSION_LONGOPS. Это не означает, что при выполнении действия продолжительностью более шести секунд в это представление автоматически будет что-то записываться. К таким действиям сейчас относятся многие функции резервного копирования и восстановления, сбора статистической информации и выполнение запросов. В каждой новой версии Oracle появляются новые действия. Изменения в этом представлении сразу же доступны для других сеансов, т.е. транзакцию фиксировать не нужно. Можно контролировать ход процесса, изменяющего это представление, из другого сеанса с помощью запросов к представлению V$SESSION_LONGOPS. Разработчики могут добавлять строки в это представление. Обычно приложение вставляет и изменяет одну строку, но при необходимости можно вставлять и несколько. Процедура для установки значений в этом представлении имеет следующие параметры: PROCEDURE SET_ SESSION_LONGOPS Argument Name RINDEX SLNO OP_NAME TARGET CONTEXT SOFAR TOTALWORK TARGET_DESC UNITS
Type
In/Out Default?
BINARY_INTEGER BINARYJENTEGER VARCHAR2 BINARY_INTEGER BINARY_INTEGER NUMBER NUMBER VARCHAR2 VARCHAR2
IN/OUT IN/OUT IN IN IN IN IN IN IN
DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT
Эти параметры описаны ниже. •
RINDEX Указывает серверу, какую строку в представлении V$SESSION_LONGOPS необходимо изменить. Если в качестве значения этого параметра указать DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS_NOHINT, в это представление будет автоматически вставлена новая строка, индекс которой будет записан в RINDEX. При указании в последующих вызовах процедуры SET_SESSION_LONGOPS этого индекса в качестве значения параметра RINDEX будет изменяться добавленная строка.
•
SLNO. Служебное значение. Первоначально надо передать значение NULL, а полученное в результате выполнения значение — игнорировать. При каждом вызове надо передавать одно и то же значение.
Пакет DBMS_APPLICATION_INFO
559
•
OP_NAME. Имя продолжительно выполняющегося процесса. Его длина не должна превышать 64 байт, а в качестве значения необходимо задавать строку, по которой легко определить, что именно выполняется.
•
TARGET. Обычно используется для передачи идентификатора объекта, с которым выполняется продолжительное действие (например, идентификатор таблицы, в которую загружаются данные). Можно передать любое значение, в том числе NULL.
•
CONTEXT. Число, задаваемое пользователем. Это число должно быть информативным для пользователя. Передать можно любое число.
Q SOFAR. В качестве значения можно передавать любое число, но если это число будет представлять собой процент или другую количественную характеристику уже выполненной части действия, сервер сможет автоматически оценить, сколько времени осталось до завершения действия. Например, если необходимо обработать 25 объектов и на обработку каждого из них уходит примерно одинаковое время, можно задать в качестве значения параметра SOFAR количество обработанных объектов, а общее их количество передать как следующий параметр, TOTALWORK. Сервер определит, сколько времени потребовалось для выполнения уже сделанной части, и оценит время, необходимое для завершения действия. •
TOTALWORK. В качестве значения можно передавать любое число, но имеет смысл сопоставлять его со значением параметра SOFAR. Если SOFAR представляет собой процент от TOTALWORK, отражающий ход выполнения действия, сервер сможет вычислить, сколько времени осталось до завершения действия.
•
TARGET_DESC. Этот параметр описывает значение TARGET, представленное выше. Если в качестве параметра TARGET передан идентификатор объекта, параметр может содержать имя объекта с этим идентификатором.
•
UNITS. Описательный параметр, задающий единицу измерения для значений параметров SOFAR и TOTALWORK. Это могут быть, например, файлы, итерации или вызовы.
Перечисленные выше значения может устанавливать пользователь. Если обратиться к представлению V$SESSION_LONGOPS, то в нем можно обнаружить намного больше столбцов, чем описано выше: [email protected]> desc v$session_longops Name Null? Type SID SERIAL* OPNAME TARGET TARGET_DESC SOFAR TOTALWORK UNITS STARTJTIME LAST_UPDATE_TIME TIME REMAINING —
NUMBER NUMBER VARCHAR2(64) ** VARCHAR2(64) ** VARCHAR2(32) ** NUMBER ** NUMBER ** VARCHAR2(32) ** DATE DATE NUMBER
560
Приложение А
ELAPSED_SECONDS CONTEXT MESSAGE USERNAME SQL_ADDRESS SQL_HASH_VALUE QCSID
NUMBER NUMBER * * VARCHAR2(512) VARCHAR2(30) RAW(4) NUMBER NUMBER
Значения столбцов, помеченных двумя звездочками (**), может устанавливать пользователь. Остальные столбцы имеют следующие значения. Q Столбцы SID и SERIAL* используются при соединении с представлением V$SESSION для получения информации о сеансе. •
Столбец STARTJTIME содержит время создания записи (обычно это время первого вызова процедуры DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS, с помощью которого и была создана строка).
•
Столбец LAST_UPDATE_TIME представляет время последнего вызова процедуры SET_SESSION_LONGOPS.
•
Столбец TIME_REMAINING содержит предполагаемое время, оставшееся до завершения действия. Его значение вычисляется как: ROUND(ELAPSED_SECONDS*((TOTALWORK/SOFAR)-1)).
О Столбец ELAPSED_SECONDS содержит количество секунд, прошедших с начала выполнения продолжительного действия до последнего изменения строки. Q Столбец MESSAGE — производный. Он представляет собой конкатенацию значений столбцов OPNAME, TARGET_DESC, TARGET, SOFAR, TOTALWORK и UNITS, описывающую выполняемое действие. Q Значение в столбце USERNAME — имя пользователя, выполняющего действие. Q Значения столбцов SQL_ADDRESS и SQL_HASH_VALUE можно использовать для поиска в представлении VSSQLAREA последнего SQL-оператора, выполненного процессом. Q Значение столбца QCSID используется при распараллеливании запроса. Это идентификатор сеанса-координатора параллельного запроса. Итак, что же можно получить с помощью данного представления? Небольшой пример поможет прояснить это. Выполним в одном сеансе следующий блок кода: tkyte@TKYTE816> declare 2 l_nohint number default dbms_application_info.set_session_longops_nohint; 3 l_rindex number default l_nohint; 4 l_slno number; 5 begin 6 for i in 1 .. 25 7 loop 8 dbms_lock.sleep(2); 9 dbms_application_infо.set_session_longops
Пакет DBMS APPLICATION INFO
10 11 12 13 14 15 16 17 18 19 20 21 22
561
(rindex => l_rindex, slno => l_slno, op_name => 'my long running operation', target => 1234, target_desc => '1234 i s my t a r g e t ' , context => 0, sofar => i, totalwork => 25, units => ' l o o p s ' end loop; end; /
Этот блок кода представляет собой продолжительное действие, выполняющееся в течение 50 секунд (вызов DBMS_LOCK.SLEEP просто приостанавливает выполнение на две секунды). В другом сеансе можно следить за ходом данного сеанса с помощью представленного ниже запроса (описание использованной в нем утилиты PRINT_TABLE см. в главе 23): tkyte@TKYTE816> begin f 2 print table('select b.* 3 from v$session a, v$session longops b where a.sid = b.sid 4 5 and a.serial# = b.serial#'); 6 end; 7 / 11 SID SERIAL* 635 OPNAME my long running operation TARGET 1234 TARGET DESC 1234 is my target SOFAR 2 TOTALWORK 25 UNITS loops STARTJTIME 28-apr-2001 16:02:46 LAST_UPDATE_TIME 28-apr-2001 16:02:46 TIME_REMAINING 0 ELAPSED_SECONDS 0 CONTEXT 0 MESSAGE my long running operation: 1234 is my target 1234: 2 out of 25 loops done USERNAME TKYTE SQL_ADDRESS 036C3758 SQL_HASH VALUE 1723303299 QCSID 0 PL/SQL procedure successfully completed. [email protected]> / SID SERIAL*
11 635
•
562
Приложение А
my long running operation 1234 1234 is my target 6 25 loops 28-apr-2001 16:02:46 28-apr-2001 16:02:55 29 9 0 my long running operation: 1234 is my target 1234: 6 out of 25 loops done TKYTE 036C3758 1723303299 0
OPNAME TARGET TARGET_DESC SOFAR TOTALWORK UNITS START_TIME LASTJJPDATEJTIME TIME_REMAINING ELAPSED_SECONDS CONTEXT MESSAGE USERNAME SQL_ADDRESS SQL_HASH_VALUE QCSID PL/SQL p r o c e d u r e
s u c c e s s f u l l y completed.
[email protected]> / SID SERIAL# OPNAME TARGET TARGET_DESC SOFAR TOTALWORK UNITS START_TIME LAST_UPDATE_TIME TIME_REMAINING ELAPSED_SECONDS CONTEXT MESSAGE USERNAME SQL_ADDRESS SQL_HASH_VALUE QCSID
11 635 my long running operation 1234 1234 is my target 10 25 loops 28-apr-2001 16:02:46 28-apr-2001 16:03:04 27 18 0 my long running operation: 1234 is my target 1234: 10 out of 25 loops done TKYTE 036C3758 1723303299 0
PL/SQL procedure successfully completed. Может возникнуть вопрос: зачем представление V$SESSION_LONGOPS соединяется с представлением V$SESSION, если из VSSESSION не выбирается информация? Дело в том, что представление V$SESSION_LONGOPS содержит строки как для текущего, так и для прежних сеансов. По завершении сеанса это представление не очишается. Данные остаются, пока какой-нибудь другой сеанс не использует повторно соответствующий слот. Поэтому, чтобы получить информацию только для текущих сеансов, необходимо использовать соединение или подзапрос.
Пакет DBMS_APPLICATION_INFO
563
Этот простой пример демонстрирует, что из представления V$SESSION_LONGOPS можно получить информацию, весьма ценную для пользователей и администраторов базы данных, которым необходимо контролировать ход выполнения продолжительных хранимых процедур, пакетных заданий, отчетов и т.п. Добавив лишь несколько вызовов, можно получить точную информацию в производственной среде. Вместо того чтобы гадать, что именно происходит в задании и сколько оно будет выполняться, можно получить точную информацию о том, что сделано, и обоснованную оценку времени, необходимого для завершения работы.
Резюме В этом разделе мы рассмотрели пакет DBMS_APPLICATION_INFO, о котором мало кто знает и использует его. Пакет можно и нужно использовать в любом приложении для регистрации в базе данных, что позволит администратору базы данных или пользователю, отвечающему за сервер, определить, какие приложения с ним работают. Очень важно использовать представление V$SESSION_LONGOPS в приложениях, выполняющихся дольше нескольких секунд. Только это позволит показать, что процесс не "висит", а выполняет необходимые действия. Oracle Enterprise Manager (OEM) и многие инструментальные средства независимых производителей обращаются к этому представлению и автоматически отображают информацию, которую приложения в нем устанавливают.
•
Пакет DBMS.JAVA
Пакет DBMS_JAVA весьма загадочен. Это PL/SQL-пакет, но он не описан в справочном руководстве Supplied PL/SQL Packages Reference. Пакет создавался для поддержки Java на сервере, так что можно предположить, что он описан в справочном руководстве Supplied Java Packages Reference (но там о нем тоже ничего нет). Описан этот пакет в руководстве Oracle8i Java Developer's Guide. Я уже многократно упоминал и использовал его в примерах, не вникая в детали. Пришло время рассмотреть процедуры этого пакета и описать их назначение и особенности использования. В пакет DBMS_JAVA входит почти 60 процедур и функций, но лишь некоторые из них действительно полезны для разработчиков ПО. Основная часть пакета предназначена для отладчиков (точнее, для тех, кто создает программы-отладчики). Кроме того, пакет включает ряд служебних подпрограмм и утилит экспорта/импорта. Все эти функции и процедуры мы рассматривать не будем.
Функции LONGNAME и SHORTNAME Это служебные функции для преобразования коротких, 30-символьных идентификаторов (все идентификаторы Oracle — не длинее 30 символов) в длинные имена Java, и наоборот. В словаре данных обычно находятся хешированные имена Java-классов, загруженных в базу данных. Причина в том, что их исходные имена — слишком длинные, а сервер такие имена не поддерживает. Эти две функции позволяют получить реальное имя по короткому (значению столбца OBJECTJVAME в представлении USER_OBJECTS), а также короткое имя для полного имени. Вот пример использования этих функций пользователем SYS (которому принадлежит много фрагментов Java-кода, если в базе данных установлена поддержка Java):
Пакет DBMS_JAVA
565
sys@TKYTE816> column long_nm format a30 word_wrapped sys@TKYTE816> colimin short_nm format a30 sys@TKYTE816> select dbms_java.longname(object_name) long_nm, 2 dbms_j ava.shortname(dbms_j ava.longname(obj ect_name)) short_nm 3 from user_objects where object_type = 'JAVA CLASS' 4 and rownum < 11 5 / LONG NM SHORT NM — — com/visigenic/vbroker/ir/Const/1001a851_ConstantDefImpl antDeflmpl oracle/sqlj/runtime/OraCustomD/10076b23_OraCustomDatumClosur atumClosure com/visigenic/vbroker/intercep /10322588_HandlerRegistryHelpe tor/HandlerRegistryHelper 10 rows selected. Как видите, применив функцию LONGNAME к значению OBJECT NAME, можно получить исходное имя Java-класса. Если применить к этому длинному имени функцию SHORTNAME, мы снова получим короткое хешированное имя, используемое внутренне сервером Oracle. -
Установка опций компилятора Для компилятора Java в базе данных можно задавать большинство опций, причем двумя способами. Опции можно задавать в командной строке при использовании процедуры loadjava или в таблице JAVA$OPTIONS. Опция, установленная в командной строке, всегда используется вместо соответствующего значения из таблицы JAVA$OPTIONS. Это, конечно же, происходит только при использовании компилятора Java в базе данных Oracle. Если используется отдельный компилятор Java вне базы данных (например, в среде JDeveloper), опции надо устанавливать в соответствующей среде. Можно устанавливать три опции компилятора, каждая из которых связана с прекомпилятором SQLJ (это Java-прекомпилятор, преобразующий встроенные операторы SQL в вызовы интерфейса JDBC), встроенным в базы данных. Эти опции представлены в следующей таблице. Опция
Назначение
Возможные значения
ONLINE
Выполнять ли проверку типов при компиляции (online) или при выполнении
True/False
DEBUG
Компилировать ли Java-код с включенной True/False отладкой. Аналог вызова javac -g из командной строки
ENCODING Кодировка исходного файла для компилятора Стандартные значения опций выделены полужирным.
Стандартно используется кодировка Latini
566
Приложение А
Я продемонстрирую использование средств пакета DBMS_JAVA для установки опций компилятора на примере опции online прекомпилятора SQLJ. Обычно эта опция имеет стандартное значение True, что требует от прекомпилятора SQLJ проверять семантику SQLJ-кода. Это означает, что прекомпилятор SQLJ обычно проверяет существование всех упомянутых в коде объектов базы данных, соответствие типов хост-переменных и т.п. Если необходимо делать эти проверки при выполнении (например, потому, что таблицы, на которые ссылается SQLJ-код, еще не созданы, но хотелось бы загрузить код в базу без ошибок), можно отключить проверку семантики с помощью процедуры DBMS_JAVA. SET_COMPILER_OPTION. Для иллюстрации используем следующий фрагмент кода. В нем выполняется попытка вставить данные в таблицу, которой в базе данных еще нет: tkyte@TKYTE816> create or replace and compile 2 java source named "bad_code" 3 as 4 import java.sql.SQLException; 5 6 public class bad_code extends Object 7 { 8 public static void wont_work() throws SQLException 9 { 10 #sql { 11 insert into non_existent_table values (1)
i!!
ь
15 / Java created. tkyte@TKYTE816> show errors Java source "bad_code" Errors for JAVA SOURCE bad_code: LINE/COL ERROR 0/0 0/0 0/0
0/0 0/0 0/0 0/0
bad_code:7: Warning: Database issued an error: PLS-00201: identifier 'NON_EXISTENT_TABLE' must be declared insert into non_existent_table values ( 1 ) AAAAAAAAAAAAAAAAAA
#sql { Info: 1 warnings
Теперь установим опции компилятора ONLINE значение FALSE. Для этого необходимо отключиться от сервера и подключиться снова. Проблема связана с тем, что среда времени выполнения языка Java проверяет существование таблицы JAVA$OPTIONS только при запуске. Если таблицы нет, повторных попыток прочитать из нее данные в сеансе не делается. Процедура DBMS_JAVA.SET_COMPILER_OPTION создаст эту таблицу автоматически, но только если она вызвана до запуска среды времени выполнения языка Java. Так что сеанс придется начинать заново.
Пакет DBMS JAVA
567
В следующем примере м ы организуем новый сеанс и убедимся, что таблицы JAVASOPTIONS нет. М ы установим опцию компилятора и увидим, что таблица автоматически создана. Наконец, мы создадим такую же Java-функцию, как в примере выше, и увидим, что теперь она компилируется без предупреждений благодаря установленной опции компилятора: tkyte@TKYTE816> disconnect Disconnected from 0racle8i Enterprise Edition Release 8.1.6.0.0 — *•* Production With the Partitioning option JServer Release 8.1.6.0.0 — Production tkyte@TKYTE816> connect tkyte/tkyte Connected. tkyte@TKYTE816> column value format alO tkyte@TKYTE816> column what format alO tkyte@TKYTE816> select * from java$options; select * from java$options ERROR at line 1: ORA-00942: table or view does not exist tkyte@TKYTE816> begin 2 dbms_j ava.set_compiler_option 3 ( what => 'bad_code', 4 optionName => 'online', 5 value => 'false') ; 6 end; 7 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from java$options; WHAT
OPT
VALUE
bad code
online
false
tkyte@TKYTE816> create or replace and compile 2 java source named "bad_code" 3 as 4 import java.sql.SQLException; 5 6 public class bad_code extends Object 7 { 8 public static void wont_work() throws SQLException 9 { 10 #sql { 11 insert into non_existent_table values (1) 12 }; 13 } 14 } 15 / Java created.
568
Приложение А
tkyte@TKYTE816> show e r r o r s Java source "bad_code" No e r r o r s . В данном случае процедура SET_COMPILER_OPTION вызывается с тремя параметрами. • WHAT. Шаблон, с которым надо сопоставлять код. Обычно Java-программы используют пакеты, поэтому полное имя будет иметь вид a.b.c.bad_code, а не просто bad_code. Если необходимо установить опцию для пакета а.Ь.с, это можно сделать. В результате для любого кода, имя которого соответствует шаблону а.Ь.с, будет использоваться соответствующая опция, если только нет более точной спецификации, тоже соответствующей данному пакету. Если опции заданы для значений WHAT, равных а.Ь.с и a.b.c.bad_code, будет использоваться опция для a.b.c.bad_code, поскольку она соответствует большей части имени. • OPTIONNAME. Одно из трех значений: ONLINE, DEBUG или ENCODING. О VALUE. Значение соответствующей опции. С процедурой SET_COMPILER_OPTION связаны еще две подпрограммы. О GET_COMPILER_OPTION. Эта функция возвращает значение указанной опции компилятора, даже если оно не отличается от стандартного. • RESET_COMPILER_OPTION. Эта процедура удаляет из таблицы JAVASOPTIONS строки, соответствующие шаблону WHAT и имеющие указанное значение в столбце OPTIONNAME. Вот примеры использования обеих подпрограмм. Начнем с использования GET_COMPILER_OPTION, чтобы узнать значение опции online: tkyte@TKYTE816> set serveroutput on tkyte8TKYTE816> begin 2 dbms_output.put_line 3 (dbms_java.get_compiler_optlon(what => 'bad_code', 4 optionName => ' o n l i n e ' ) ) ; 5 end; 6 / false PL/SQL procedure successfully completed. А затем сбросим ее с помощью процедуры RESET_COMPILER_OPTION: tkyte@TKYTE816> begin 2 dbms_java.reset_compiler_option(what => 'bad_code', 3 optionName => 'online'); 4 end; 5 / PL/SQL procedure successfully completed. Теперь убедимся, что функция GET_COMPILER_OPTION всегда возвращает значение указанной опции компилятора, даже если таблица JAVASOPTIONS пуста (при вызове RESET_COMPILER_OPTION соответствующая строка была удалена):
Пакет DBMS_JAVA
5 6 9
tkyte@TKYTE816> begin 2 dbms_output.put_line 3 (dbms_java.get_conpiler_option(what => 'bad_code', 4 optionName => 'online')); 5 end; 6
/
true PL/SQL procedure successfully completed. tkyte@TKYTE816> select * from java$options; no rows selected
Процедура SET_OUTPUT Эта процедура аналогична команде SET SERVEROUTPUT ON в SQL*Plus. Точно так же, как выполнение этой команды включает вывод строк с помощью пакета DBMS_OUTPUT, вызов DBMS_JAVA.SET_OUTPUT обеспечивает вывод результатов функций System.out.println и System.err.print на экран в SQL*Plus. Если не выполнить вызовы: SQL> s e t serveroutput on s i z e 1000000 SQL> exec dbms_java.set_output(1000000) перед выполнением хранимой процедуры на языке Java из среды SQL*Plus, то все сообщения, выдаваемые с помощью вызовов System.out.println, будут записываться в трассировочный файл в каталоге на сервере, задаваемом параметром инициализации USER_DUMP_DEST. Эта процедура используется при отладке хранимых процедур на языке Java, поскольку позволяет помещать в код выдачу отладочной информации в виде вызовов System.out.println во многом аналогично вызовам DBMS_OUTPUT.PUT_LINE в PL/SQL-коде. В дальнейшем можно отключить выдачу отладочной информации в Javaкоде, перенаправив ее в "корзину". Так что, если вас интересовало, куда делись результаты вызовов System.out в хранимой процедуре на языке Java, вы теперь знаете ответ. Они выдаются в трассировочный файл. Теперь вы сможете перенаправить их на экран в среде SQL*Plus.
Процедуры loadjava и dropjava Эти процедуры обеспечивают интерфейс для языка PL/SQL, позволяющий выполнять функции утилит командной строки loadjava и dropjava. Как и следовало ожидать для служебных процедур, при их вызове не надо указывать опцию -и имя_пользователя/пароль ИЛИ задавать тип используемого JDBC-драйвера — вы ведь уже подключились к базе данных. Процедуры загрузят Java-объекты в текущую схему. Эти процедуры имеют следующие прототипы: PROCEDURE loadjava(options varchar2) PROCEDURE loadjava(options varchar2, resolver varchar2) PROCEDURE dropjava(options varchar2)
570
Приложение А
Их можно использовать для загрузки файла activation8i.zip, который используется также в разделе приложения А, посвященном пакету U T L _ S M T P . Более детальную информацию об интерфейсе JavaMail можно найти на странице http://java.sun.com/ products/javamail/index.html. Рассмотрим пример: sys@TKYTE816> exec dbms_java.loadjava('-r -v -f -noverify -synonym -g "•• public c:\temp\activation8i.zip') initialization complete com/sun/activation/registries/LineTokenizer loading com/sun/activation/registries/LineTokenizer creating com/sun/activation/registries/MailcapEntry loading com/sun/activation/registries/MailcapEntry creating com/sun/activation/registries/MailcapFile loading com/sun/activation/registries/MailcapFile creating loading com/sun/activation/registries/MailcapParseException com/sun/activation/registries/MailcapParseException creating
Процедуры управления правами Это весьма странные процедуры. Выполните команду DESCRIBE для пакета DBMS_JAVA в базе данных и поищите в результатах упоминание о процедуре GRANT_PERMISSION. Вы его не найдете, хотя точно известно, что такая процедура должна быть (я несколько раз демонстрировал ее использование). Она существует, как и ряд других функций, связанных с правами доступа. Я опишу использование подпрограмм GRANT_PERMISSION/REVOKE_PERMISSION. Подробное описание использования процедур управления правами и всех соответствующих опций можно найти в руководстве Oracle Java Developers Guide. Процедуры управления правами описаны в главе 5, Security for Oracle 8i Java Applications, этого руководства. В Oracle 8.1.5 точность установки привилегий для Java была очень низкой. Можно было указать только JAVAUSERPRTV или JAVASYSPRIV. Это напоминает ситуацию, когда в базе данных имеются только роли RESOURCE и DBA — обе они предоставляют пользователям слишком много возможностей. В версии Oracle 8.1.6 реализация Java в базе данных поддерживает классы защиты Java 2. Теперь имеется очень детальный набор привилегий, которые можно избирательно предоставлять и отбирать, аналогично набору привилегий в базе данных. Описание и обсуждение соответствующих классов привилегий можно найти на Web-странице http://java.sun.eom/j2se/l.3/docs/api/java/ security/Permission.html.
Итак, для установки привилегий будем использовать две процедуры — GRANT_PERMISSION и REVOKE_PERMISSION. Вопрос в том, как определить необходимые привилегии? Проще всего установить Java-код, выполнить его и узнать, чего ему не хватает для нормальной работы. Например, обратимся к разделу, посвященному пакету UTL_SMTP. В нем я создаю хранимую процедуру SEND для посылки сообщения по электронной почте. Я также демонстрирую там, какие две привилегии необходимо предоставить с помощью процедуры GRANT_PERMISSION, чтобы процедура SEND заработала. Необходимые привилегии я определил именно так — выполняю SEND и читаю сообщения об ошибках. Например:
Пакет DBMS_JAVA 5 7 1 tkyte@TKYTE816> set serveroutput on size 1000000 tkyte@TKYTE816> exec dbms_java.set_output(1000000) PL/SQL procedure successfully completed. tkyte@TKYTE816> declare 2 ret_code number; 3 begin 4 ret_code := send( 5 p_from => '[email protected], 6 p_to => '[email protected], 7 p_cc => NULL, 8 p_bcc => NULL, 9 p_subject => •Use the attached Zip file', 10 p_body => 'to send email with attachments....', 11 p_smtp_host => 'aria.us.oracle.com', 12 p_attachment_data => null, 13 p_attachment_type => null, 14 p_attachment_file_name => null); 15 if ret_code = 1 then 16 dbms_output.put_line ('Сообщение послано успешно...'); 17 else 18 dbms_output.put_line ('Послать сообщение не удалось...'); 19 end if; 20 end; 21 / Java.security.AccessControlException: the Permission (Java.util.Property Permission * read,write) has not been granted by dbms_java.grant_permission to SchemaProtectionDomain(TKYTE|PolicyTableProxy(TKYTE)) Все предельно ясно. В сообщениях говорится, что пользователю TKYTE необходима привилегия типа java.util.PropertyPermission с именем * и параметрами read и write. Вот откуда я узнал, что надо выполнить: sys@TKYTE816> begin 2 dbms_java.grant_permis sion( 3 grantee => 'TKYTE', 1 4 permission_type => 'java.util.PropertyPermission , 5 permission_name => '*', 6 permission_action -> 'read,write' После этого при попытке выполнения я получил следующее сообщение об ошибке: Java.security.AccessControlException: t h e Permission (Java.net.SocketPer mission a r i a . u s . o r a c l e . c o m resolve) has not been granted by dbms_j ava.grant_permi s s ion t o SchemaProtectionDomain(TKYTE|PolicyTableProxy(TKYTE)) Предоставив соответствующую привилегию, я узнал, что кроме RESOLVE необходима привилегия CONNECT. Вот почему я выполнил: 8 9 10
dbms_java.grant_permission( grantee => 'TKYTE', permission_type => 'Java.net.SocketPermission',
572 11 12 13 14 15
Приложение А
permission_name => ' * ' , permission action => 'connect,resolve' ); end; /
И получил все необходимые соответствующей схеме привилегии. Обратите внимание, что в качестве значения permission_name я задал *, чтобы процедура могла разрешать имя любого хоста и подключаться к любому хосту, а не только к моему серверу SMTP. Процедура REVOKEJPERMISSION выполняет действие, обратное GRANT_PERMISSION. Она работает именно так, как ожидалось. Если передать те же параметры, что были переданы процедуре GRANT_PERMISSION, она отберет соответствующую привилегию у текущей схемы.
Резюме В этом разделе мы рассмотрели использование средств пакета DBMS_JAVA для выполнения различных действий, необходимых для поддержки Java-кода. Мы начали с описания того, как сервер Oracle, ограничивающий длину имен 30 символами, обрабатывает очень длинные имена, используемые в языке Java. Для каждого длинного имени Java-сервер создает уникальный 30-символьный идентификатор с помощью хеширования. Пакет DBMS_JAVA предоставляет функцию преобразования короткого имени в соответствующее ему длинное имя и функцию преобразования длинного имени в короткое с помощью хеширования. Затем было рассмотрено использование средств пакета DBMS_JAVA для установки, получения и сброса опций компилятора Java. Мы разобрались, как таблица JAVASOPTIONS используется для постоянного хранения стандартных опций компилятора и как вернуть этим опциям стандартные значения. Затем мы кратко рассмотрели процедуру SET_OUTPUT. Она перенаправляет вывод результатов вызовов System.out.println в Java-коде в сеанс SQL*Plus или SVRMGRL, аналогично тому, как команда SET SERVEROUTPUT ON обеспечивает выдачу результатов вызовов процедур PL/SQL-пакета DBMS_OUTPUT. Мы также рассмотрели альтернативный способ загрузки (с помощью вызова хранимой процедуры) исходного кода на языке Java, файлов классов и Java-архивов, ставший возможным благодаря пакету DBMS_JAVA в версиях Oracle8i Release 2 (8.1.6) и выше. Наконец, мы изучили процедуры управления правами доступа, предоставляемые этим пакетом в версиях, начиная с Oracle8i Release 2. Эти процедуры позволяют гибко управлять привилегиями для Java-кода, устанавливая, что он может и чего не может делать. Если вы используйте язык Java в базе данных Oracle, то постоянно будете применять эти процедуры при программировании.
Пакет DBMS.JOB
•
Пакет DBMS_JOB позволяет запланировать однократное или регулярное выполнение заданий в базе данных. Задание представляет собой хранимую процедуру, анонимный блок PL/SQL или внешнюю процедуру на языке С или Java. Эти задания выполняются серверными процессами в фоновом режиме. Задания можно выполнять регулярно (в 2 часа ночи каждые сутки) или однократно (выполнить задание сразу после фиксации транзакции и удалить его из очереди). Если вы знакомы с утилитами сгоп или at в ОС UNIX или Windows, то особенности использования пакета DBMS_JOB вам уже понятны. Задания выполняются в той же среде (пользователь, набор символов и т.п.), из которой они посланы на выполнение (но роли не действуют). Задания выполняются аналогично процедурам с правами создателя, т.е. роли для них не учитываются. Это можно продемонстрировать на следующем примере (процедуры, использованные в этом примере, позже будут рассмотрены подробно): tkyte@TKYTE816> create t a b l e t (msg varchar2(20),
cnt i n t ) ;
Table created. tkyte@TKYTE816> i n s e r t i n t o t s e l e c t 'from SQL*PLUS', count(*) from session_roles; 1 row created. tkyte@TKYTE816> variable n number tkyte@TKYTE816> exec dbms_job. submit (:n, 'insert into t select ''from job1', count(*) from session_roles;'); PL/SQL procedure successfully completed. tkyte@TKYTE816> print n
574
Приложение А N
81 tkyte@TKYTE816> exec dbms_job.run(:n); PL/SQL procedure successfully completed. tkyte@TKYTE816> s e l e c t * from t ; MSG f r o m SQL*PLUS
from job
CNT 10
0
Как видите, в среде SQL*Plus имеется 10 действующих ролей, а в среде выполнения задания — ни одной. Поскольку пользователи в качестве задания обычно вызывают хранимую процедуру, это не имеет значения, потому что при выполнении хранимой процедуры роли все равно не учитываются. Проблема возникает только при попытке выполнить процедуру, доступную через роль. Такая процедура не сработает, поскольку в заданиях роли не действуют. Часто пользователи спрашивают, как лучше всего скрыть имя пользователя/пароль, связанные с пакетным заданием (например, для периодического анализа таблиц), посылаемым с помощью утилиты сгоп или ее аналогов в среде Windows NT/2000. Их беспокоит, что пароль хранится в файле (и это правильно) или явно представлен в результатах выполнения команды ps в среде UNIX и т.п. Я всегда рекомендую вообще не использовать средства ОС для планирования заданий в базе данных, а вместо этого написать хранимую процедуру, выполняющую необходимые действия, и запланировать ее выполнение с помощью средств пакета DBMS_JOB. При этом ни имя пользователя, ни пароль нигде не хранится, и задание выполнится только в том случае, когда доступна база данных. Если сервер базы данных не будет работать в соответствующий момент, задание, естественно, не выполнится, поскольку именно сервер базы данных и отвечает за выполнение задания. Часто спрашивают также: как ускорить выполнение? Необходимо выполнить продолжительное действие, а пользователь не хочет ждать. Причем иногда ускорить выполнение действия нельзя. Например, я уже многие годы посылаю сообщения электронной почты с сервера базы данных. Я использовал для этого различные механизмы: каналы базы данных, пакет UTL_HTTP, внешние процедуры и средства языка Java. Все они работали примерно с одинаковой скоростью, но всегда — медленно. Иногда завершения работы по протоколу SMTP приходится ждать достаточно долго. Слишком долго в случае моего приложения, для которого ожидания более четверти секунды вообще недопустимы. Посылка сообщения по протоколу SMTP может иногда потребовать 2-3 секунды. Ускорить этот процесс нельзя, но можно создать видимость его более быстрого выполнения. Вместо отправки сообщения при нажатии пользователем соответствующей кнопки в приложении должно посылаться на выполнение задание, которое будет отправлять сообщение сразу после фиксации транзакции. При этом возникает два побочных эффекта. Во-первых, действие выполняется как бы быстрее, а во-вторых, отправка сообщения становится частью транзакции. Одно из свойств пакета DBMS_JOB состоит в том, что задание попадает в очередь только после фиксации транзакции. При откате транзакции задание удаляется из очереди и не выполняется. С помощью пакета
Пакет DBMS_JOB
575
DBMS_JOB мы не только создаем видимость более быстрой работы приложения, но и делаем его более надежным. Больше не будет посылаться уведомление по электронной почте из триггера при изменении строки, если это изменение затем было отменено. Либо строка изменяется, и отправляется сообщение; либо строка не изменяется, и сообщение не отправляется. Так что пакет DBMS_JOB имеет широкую сферу применения. Он может включить выполнение действий, выходящих "за рамки" транзакций (таких как отправка сообщений по электронной почте или создание таблицы при вставке строки в другую таблицу) в транзакции. Он позволяет создать видимость более быстрого выполнения действий, особенно, если продолжительные действия не должны выдавать пользователю результатов. Пакет позволяет планировать и автоматизировать многие задачи, для решения которых обычно создавались сценарии вне базы данных. Это, безусловно, очень полезный пакет. Для корректной работы пакета DBMS_JOB необходимо выполнить небольшую настройку сервера. Надо установить два параметра инициализации. • job_queue_interval. Задает периодичность (в секундах) проверки очередей и поиска заданий, готовых к выполнению. Если задание должно выполняться раз в 30 секунд, но параметр job_queue_interval имеет (стандартное) значение 60, это задание не будет выполняться раз в 30 секунд — в лучшем случае, раз в 60 секунд. Q job_queue_processes. Задает количество фоновых процессов для выполнения заданий. Значением может быть целое число от 0 (по умолчанию) до 36. Это значение можно менять без перезапуска сервера с помощью оператора ALTER SYSTEM SET JOB_QUEUE_PROCESSES=. Если оставить стандартное значение 0, задания из очереди автоматически никогда выполняться не будут. Процессы обработки очередей заданий можно увидеть в среде ОС UNIX — они получают имена ora_snpN_$ORACLE_SID, где N — число (0, 1, 2, ..., job_queue_processes-l). В среде Windows очереди заданий обрабатываются потоками операционной системы, увидеть которые с помощью стандартных средств нельзя. Многие системы работают со значением параметра job_queue_interval, равным 60 (другими словами, проверяют очереди раз в минуту), и значением параметра job_queue_processes, равным 1 (выполняют одновременно не более одного задания). При интенсивном использовании заданий или возможностей, для реализации которых используются задания (в частности, репликация и поддержка материализованных представлений используют очереди заданий), может потребоваться добавление дополнительных процессов и увеличение значения параметра инициализации job_queue_processes. После настройки и автоматического запуска очередей заданий можно начинать их использование. Основная процедура пакета DBMS_JOB — процедура SUBMIT. Она имеет следующий интерфейс: PROCEDURE SUBMIT Argument Name JOB WHAT NEXT_DATE INTERVAL
Type BINARY_INTEGER VARCHAR2 DATE VARCHAR2
In/Out OUT IN IN IN
Default?
DEFAULT DEFAULT
J/6
Приложение А
NO_PARSE INSTANCE FORCE
BOOLEAN BINARY_INTEGER BOOLEAN
IN IN IN
DEFAULT DEFAULT DEFAULT
Назначение аргументов процедуры SUBMIT описано ниже. Q JOB. Идентификатор задания. Присваивается системой (этот параметр передается в режиме OUT). Его можно использовать для получения информации о задании из представлений USER_JOBS или DBA_JOBS по идентификатору задания. Кроме того, некоторые процедуры, в частности RUN и REMOVE, требуют единственного параметра — идентификатора задания — для определения того, какое именно задание выполнять или удалять из очереди. • WHAT. Действие, которое необходимо выполнить. Можно передавать PL/SQLоператор или блок кода. Например, чтобы выполнить хранимую процедуру Р, можно передать процедуре строку Р; (включая точку с запятой). Значение параметра WHAT будет помещено в следующий PL/SQL-блок: DECLARE job BINARY_INTEGER := :job; next_date DATE := :mydate; broken BOOLEAN := FALSE; BEGIN WHAT :mydate := next_date; IF broken THEN :b :- 1; ELSE :b := 0; END IF; END; Вот почему любой оператор надо завершать точкой с запятой (;). Чтобы WHAT можно было заменить соответствующим кодом, точка с запятой необходима. Q NEXT_DATE. Время следующего (а поскольку мы только посылаем задание — первого) выполнения задания. Стандартное значение — SYSDATE — означает "выполнять немедленно" (после фиксации транзакции). Q INTERVAL. Строка, содержащая функцию, вычисляющую время следующего выполнения задания. Можно считать, что значение этой функции выбирается с помощью оператора 'select ... from dual'. Если передать строку sysdate+1, сервер выполнит SELECT sysdate+1 INTO :NEXT_DATE FROM DUAL. Ниже описаны особенности задания интервала выполнения задания, предотвращающие его смещение. Q NO_PARSE. Указывает, анализировался ли параметр WHAT при отправке задания. Анализируя строку, можно определить, выполнимо ли вообще задание. В общем случае параметру NO_PARSE надо всегда оставлять стандартное значение, False. При установке значения True параметр WHAT принимается "как есть", без проверки допустимости. • INSTANCE. Этот параметр имеет значение только в режиме Parallel Server, когда сервер Oracle работает на кластере слабо связанных машин. Он задает экземпляр, на котором будет выполняться задание. По умолчанию он имеет значение ANY_INSTANCE.
Пакет DBMS JOB
577
Q FORCE. Этот параметр также имеет значение только в режиме Parallel Server. При установке значения True (принятого по умолчанию) можно посылать задание с любым идентификатором экземпляра, даже если при отправке соответствующий экземпляр недоступен. При установке значения False отправка задания завершится неудачно, если указанный экземпляр недоступен. В пакете DBMS_JOB есть и другие подпрограммы. Задание посылается на выполнение с помощью процедуры SUBMIT, а остальные подпрограммы позволяют манипулировать заданиями в очередях и выполнять действия по их запуску (RUN), удалению из очереди (REMOVE) и изменению (CHANGE). Ниже описаны наиболее часто используемые подпрограммы, включая входные данные и назначение. Подпрограмма Входные данные
Описание
REMOVE
номер задания
Удаляет задание из очереди. Учтите, что если задание выполняется, удаление не остановит его выполнение. Удаление из очереди гарантирует, что задание не будет выполнено снова, но уже выполняющееся задание при этом не останавливается. Для остановки выполняющегося задания необходимо прекращать работу соответствующего сеанса с помощью оператора ALTER SYSTEM.
CHANGE
номер задания WHAT, NEXT.DATE, INTERVAL, INSTANCE, FORCE
Эта процедура работает как оператор UPDATE для представления JOBS. Она позволяет изменить любой параметр задания.
BROKEN
номер задания BROKEN (булево значение) NEXT DATE
Позволяет "разрушить" или "восстановить" • задание. Разрушенное задание не выполняется. Задание, не выполнившееся успешно 16 раз подряд, автоматически помечается как разрушенное, и сервер Oracle больше не будет его выполнять.
RUN
номер задания
Выполняет задание немедленно, в приоритетном режиме (в пользовательском сеансе). Полезно при отладке не срабатывающих заданий.
Теперь, когда вы достаточно хорошо представляете устройство пакета DBMS_JOB и его возможности, рассмотрим, как выполнить задание однократно, как организовать его периодическое выполнение, как контролировать выполнение заданий и определять возникающие при этом ошибки.
Однократное выполнение задания Многие из заданий в моей базе данных — однократные. Я часто использую пакет DBMS_JOB как аналог запуска процесса в фоновом режиме с помощью & в ОС UNIX или команды start в среде Windows. Представленный ранее пример с отправкой сооб19 Зак.244
57О
Приложение А
щения по электронной почте относится как раз к этой категории. Я использую пакет DBMS_JOB не только для включения отправки сообщения в транзакцию, но и для того, чтобы ускорить выполнение этого действия. Вот одна из возможных реализаций этого действия, демонстрирующая однократное выполнение задания. Начну с небольшой хранимой процедуры для отправки сообщений по электронной почте с использованием средств стандартного пакета UTL_SMTP: tkyte@TKYTE816> c r e a t e or replace 2 PROCEDURE s e n d j n a i l (p_sender IN VARCHAR2, 3 p _ r e c i p i e n t IN VARCHAR2, 4 p message IN VARCHAR2) 5 as 6 — Учтите, что надо указать хост, 7 — поддерживающий протокол SMTP и доступный для вас. 8 — К указанному хосту вы не получите доступа, поэтому его надо *-» изменить 9 l_mailhost VARCHAR2(255) := 'aria.us.oracle.com'; 10 l_mail_conn utl_smtp.connection; 11 BEGIN 12 l_mail_conn := utl_smtp.open_connection(l_mailhost, 25); 13 utl_smtp.helo(l_mail_conn, l_mailhost); 14 utl_smtp.mail(l_mail_conn, p_sender); 15 utl_smtp.rcpt(l_mail_conn, p_recipient); 16 utl_smtp.open_data(l_mail_conn); 17 utl_smtp.write_data(l_mail_conn, p_message); 18 utl_smtp. close_data (l_mail_conn) ; 19 utl_smtp. quit (l_mail_conn); 20 end; 21 / Procedure created. Теперь, чтобы определить продолжительность работы, я выполню эту процедуру дважды: tkyte@TKYTE816> s e t serveroutput on tkyte@TKYTE816> declare 2 l _ s t a r t number := dbms_utility.get_time; 3 begin 4 send_mail('[email protected]', 5 '[email protected]', 'Привет!'); 6 dbms_output.put_line 7 (round((dbms_utility.get_time-l_start)/100, 2) II 8 ' seconds'); 9 end; 10 / .81 seconds PL/SQL procedure successfully completed. tkyte@TKYTE816> / .79 seconds PL/SQL procedure successfully completed.
Пакет DBMSJOB
579
Похоже, она выполняется примерно восемь десятых секунды, в лучшем случае. Для меня это слишком долго. Можно ускорить выполнение, точнее, создать видимость этого. Я использую задания для видимости ускорения работы и получаю при этом преимущества отправки сообщений в рамках транзакции. Начнем с создания таблицы для хранения сообщений и процедуры, которая сможет посылать находящиеся в ней сообщения. Эта процедура и будет выполняться как фоновое задание. Вопрос в том, зачем для хранения сообщений используется таблица? Почему просто не передать текст сообщения как параметр задания? Причина в использовании связываемых переменных и разделяемого пула. Поскольку все задания будут создаваться с параметром WHAT, а сервер будет просто выполнять эту строку, надо помнить, что значение параметра WHAT окажется в разделяемом пуле. Можно посылать задания и так: dbms_j ob.submit(х,
'send_mail(''someoneSthere.com'', 1 'someoneGthere.com' ' , ' ' П р и в е т ! ' ' ) ; ' ) ;
но в результате в разделяемом пуле окажутся сотни тысяч уникальных операторов, что отрицательно скажется на производительности сервера. Поскольку предполагается рассылка большого количества сообщений (больше одного — уже много, и использовать связываемые переменные при этом обязательно), надо иметь возможность посылать задания вида: dbms_job.submit(x,
'background_send_mail(константа);');
Оказывается, этого очень легко добиться. Достаточно создать таблицу с полями для каждого передаваемого параметра при отправке задания на выполнение (в данном случае, отправитель, адресат и само сообщение) и первичным ключом. Например: tkyte@TKYTE816> create t a b l e send_mail_data(id number primary key, 2 sender varchar2(255), 3 r e c i p i e n t varchar2(255), 4 message varchar2(4000), 5 senton date default NULL); Table created. Я добавил в таблицу первичный ключ — столбец ID, и дату отправки сообщения в столбце senton. Мы будем использовать эту таблицу не только для организации очереди исходящих сообщений, но и как постоянный журнал, в котором будут регистрироваться все отправленные сообщения (пригодится, поверьте мне, когда кто-то скажет: "А меня не предупреждали..."). Осталось только придумать, как генерировать значение ключа для таблицы и передавать его фоновому процессу в виде строковой константы. К счастью, пакет DBMS_JOB содержит все необходимое для решения этой проблемы. При отправке задания на выполнение пакет автоматически создает для него уникальный идентификатор и возвращает его вызывающему. Поскольку блок кода, в который помещается переданное значение параметра WHAT, содержит идентификатор задания, мы можем его передавать. Процедура FAST_SEND_MAIL будет выглядеть так: tkyte@TKYTE816> create or replace 2 PROCEDURE fast_send_mail (p_sender IN VARCHAR2, 3 p_recipient IN VARCHAR2,
JoU 4 5 6 7 8 9 10 11 12 13 14
Приложение А p_message
IN VARCHAR2)
as l_job
number;
begin dbms_job.submit(l_job, 'background_send_mail(JOB);'); insert into send_mail_data (id, sender, recipient, message) values (l_job, p_sender, p__recipient, p_message); end; /
Procedure created. Эта процедура будет посылать на выполнение задание BACKGROUND_SEND_MAIL и передавать ему параметр JOB. Если обратиться к описанию параметра WHAT, вы увидите, что соответствующий блок кода включает три локальных переменных, к которым можно обращаться, — мы и передаем процедуре одну из них. Сразу после этого мы вставляем сообщение в таблицу очереди для последующей отправки. Итак, пакет DBMS_JOB создает первичный ключ, а затем мы вставляем этот первичный ключ и соответствующие данные в таблицу. Вот и все. Теперь необходимо создать несложную процедуру BACKGROUND_SEND_MAIL: tkyte@TKYTE816> c r e a t e or replace 2 procedure background_send_mail(p_job i n number) 3 as 4 l_rec send_mail_data%rowtype; 5 begin 6 select * into l_rec 7 from send_mail_data 8 where id = p_job; 9 10 send_mail(l_rec.sender, l_rec.recipient, l_rec.message); 11 update send_mail_data set senton = sysdate where id = p_job; 12 end; 13 / Procedure created. Она читает сохраненные данные, вызывает медленно работающую процедуру SEND_MAIL, а затем изменяет соответствующую запись, отражая в ней факт отправки сообщения по электронной почте. Теперь можно выполнить процедуру FAST_SEND_MAIL и определить, насколько быстро она выполняется: tkyte@TKYTE816> declare 2 l_start number := dbms_utility.get_time; 3 begin 4 fast_send_mail('[email protected]', 5 'snakeSsnake.com', 'Привет!'); 6 dbms_output.put_line 7 (round((dbms_utility.get_time-l_start)/100, 2) || 8 ' seconds'); 9 end;
Пакет DBMS_JOB
581
10 / .03 seconds PL/SQL procedure successfully completed. tkyte@TKYTE816> / .02 seconds PL/SQL procedure successfully completed. С точки зрения пользователя процедура FAST_SEND_MAIL работает в 26-40 раз быстрее, чем исходная. На самом же деле она работает не быстрее, но создает видимость быстрого выполнения (именно это и имеет значение). Фактическая отправка сообщения будет выполнена в фоновом режиме после фиксации транзакции. Об этом важно помнить. При выполнении этого примера не забудьте выполнить COMMIT, иначе сообщение никогда не будет послано. Задание не будет доступно в очереди для соответствующих процессов, пока транзакция не будет зафиксирована (в сеансе можно будет увидеть задание в представлении USER_JOBS, но процессы обработки очередей его не увидят, пока не будет зафиксирована транзакция). Не считайте это ограничением. На самом деле это полезное свойство, с помощью которого мы только что включили отправку сообщений по электронной почте в транзакцию. Если откатить транзакцию, сообщение не будет послано. После фиксации транзакции оно будет отправлено.
Текущие задания Еще одно стандартное применение пакета DBMS_JOB — для организации периодического выполнения заданий в базе данных. Как уже упоминалось, многие пользователи пытаются применять для выполнения заданий в базе данных утилиты ОС сгоп или at, но сталкиваются при этом с проблемами защиты пароля и т.п. Я всегда предлагаю использовать очереди заданий. Они не только избавляют от необходимости хранения регистрационной информации, но и гарантируют выполнение заданий только в случае работоспособности и доступности сервера базы данных. В случае сбоя сервер будет повторно пытаться выполнить задание. Например, если при первой попытке выполнения задания удаленная база данных недоступна, задание возвращается в очередь и делается попытка выполнить его снова. Сервер делает это 16 раз, с каждым разом немного увеличивая время ожидания, прежде чем пометит задание как "разрушенное". Подробнее об этом мы поговорим в подразделе "Контроль заданий и поиск ошибок". Утилиты сгоп и at автоматически этого не сделают. Кроме того, поскольку задания выполняются в базе данных, с помощью запросов можно определить их статус: когда последний раз выполнялось задание и выполнялось ли вообще, и т.п. Вся информация находится в одном месте. Другие средства сервера Oracle, такие как репликация и материализованные представления, неявно используют очереди заданий при реализации своих функциональных возможностей. Изменения моментальных снимков и обновления материализованных представлений выполняются с помощью заданий, вызывающих соответствующие хранимые процедуры. Предположим, необходимо ежесуточно в 3 часа ночи выполнять анализ всех таблиц в определенной схеме. Для этого можно использовать следующую хранимую процедуру: •
5о2
Приложение А
scott@TKYTE816> create or replace procedure analyze_my_tables 2 as 3 begin 4 for x in (select table_name from user_tables) 5 loop 6 execute immediate 7 'analyze table ' II x.table_name II ' compute s t a t i s t i c s ' ; 8 end loop; 9 end; 10 / Procedure created. Чтобы запланировать ее выполнение сегодня ночью в 3 часа (точнее, завтра утром), а затем ежесуточно в 3 часа ночи, можно использовать следующий вызов: scott@TKYTE816> declare 2 l_Job number; 3 begin 4 dbms_job.submit(job => l_job, 5 what => 'analyze_my_tables;', 6 next_date => trunc(sysdate)+l+3/24, 7 interval => 'trunc(sysdate)+1+3/24'); 8 end; 9 / PL/SQL procedure successfully completed. scott@TKYTE816> select job, to_char(sysdate,'dd-mon'), 2 to_char(next_date,'dd-mon-yyyy hh2 4: mi:ss'), 3 interval, what 4 from user_jobs 5 / JOB TO_CHA TO_CHAR(NEXT_DATE,'D INTERVAL
WHAT
33 09-jan 10-jan-2001 03:00:00 trunc(sysdate)+l+3/24 analyze_my_tables; Итак, в следующий раз это задание выполнится в 3 часа ночи 10 января. Для этого мы передали реальную дату, а не строку, как для параметра interval. Мы использовали функции для работы с датами, так что при выполнении, независимо от времени вызова, всегда будет возвращаться 3 часа следующего утра. Это важно. Точно такую же функцию, но в виде строки мы передали в качестве значения параметра INTERVAL. Используется функция, всегда возвращающая 3 утра завтрашнего дня, независимо от времени ее выполнения. Это предотвращает смещение заданий (jobs sliding). Может показаться, что, поскольку первый раз задание выполняется в 3 часа утра, можно задать интервал sysdate+1. Если выполнить это вычисление в 3 утра во вторник, в результате мы получим 3 утра среды. Получим, если задание гарантированно выполнится в указанное время, но это вовсе не обязательно. Задания в очереди обрабатываются последовательно, в соответствии с указанным временем выполнения. При наличии одного процесса обработки очереди сообщений и двух заданий, назначенных на 3 утра, очевидно, что одно из них не выполнится точно в 3 утра. Придется подождать, пока завершится выполнение первого задания. Даже при отсутствии заданий, назначенных на то же вре-
Пакет DBMS_JOB
583
мя, очереди заданий просматриваются периодически, например каждые 60 секунд. Задание, назначенное на выполнение в 3 утра, может быть выбрано из очереди в 3:00:45 утра. Если использовать функцию sysdate+1, в следующий раз задание может быть поставлено на выполнение в 3:00:46 утра. На следующий день в 3:00:45 утра задание еще не будет готово для выполнения и выполнится при следующем просмотре очереди, в 3:01:45 утра. Время выполнения задания медленно сдвигается. Однако это не самое худшее. Предположим, на следующий день в 3 утра с таблицами будут работать и проанализировать их не удастся. Хранимая процедура не сработает и будет возвращена в очередь для выполнения. Теперь это задание окажется смещенным на несколько минут позже 3 утра. Поэтому, чтобы предотвратить смещение времени выполнения заданий, необходимо использовать функцию, возвращающую фиксированный момент времени, если выполнение в конкретный момент времени является существенным. Если важно, чтобы задание выполнялось именно в 3 утра, надо использовать функцию, всегда возвращающую время 3 утра, независимо от времени ее выполнения. Многие из таких "не смещающихся" функций реализовать очень легко, другие — намного сложнее. Например, однажды меня попросили создать задание, собирающее статистическую информацию с помощью STATSPACK с понедельника по пятницу ровно в 7 часов утра и 3 часа дня. Значение параметра INTERVAL для этого задания задать непросто, но давайте рассмотрим для начала псевдокод: i f время - до 15:00 then вернуть 15:00 СЕГОДНЯ (другими словами, если мы выполняем это в 7 утра, надо выполнить задание сегодня в 3 часа дня) else вернуть 7 утра через 3 дня (если сегодня пятница) либо 1 день (в противном случае) end i f Осталось реализовать эту логику в виде симпатичной функции DECODE или, если для вас это слишком сложно, в виде PL/SQL-функции. Я использовал интервал: decode(sign(15-to_char(sysdate,'hh24')), 1, trunc(sysdate)+15/24, trunc(sysdate + decode(to_char(sysdate,'d'),
6, 3, l))+7/24)
Функция decode начинается с вычисления значения SIGN(15-TO_CHAR(SYSDATE,'HH24')).
SIGN — это функция, возвращающая -1, 0 или 1, если переданное ей выражение имеет, соответственно, отрицательное, нулевое и положительное значение. Если значение было положительным, предполагается, что вызов произошел до 3 часов дня (до 15 часов), поэтому в следующий раз надо будет выполнить задание в TRUNC(SYSDATE)+15/24 (сегодня в 15 часов). С другой стороны, если sign возвращает 0 или -1, надо вернуть значение TRUNC(SYSDATE + DECODE(TO_CHAR(SYSDATE,D), 6, 3, 1))+7/24. Здесь с помощью функции DECODE мы определяем день недели, чтобы узнать, сколько добавлять дней — три (в пятницу, чтобы вернуть понедельник) или один (в остальные рабочие дни). Полученное количество дней мы добавляем к значению SYSDATE, усекаем время до полуночи и добавляем 7 часов.
Приложение А
Бывает, что смещение даты вполне допустимо и даже желательно. Например, если необходимо собирать статистическую информацию из представлений V$ каждые 30 минут в процессе работы сервера, вполне допустимо использовать интервал SYSDATE+1/24/2, добавляющий к текущему моменту времени полчаса.
Нетривиальное планирование Бывает, как в рассмотренном выше примере, что значение NEXT_DATE вычислить одним оператором SQL сложно или время следующего выполнения задания определяется сложным алгоритмом. В этом случае можно определить время следующего выполнения в самом задании. В начале раздела было сказано, что задание выполняется в следующем PL/SQL-блоке: DECLARE job BINARY_INTEGER := : j o b ; n e x t _ d a t e DATE := :mydate; broken BOOLEAN := FALSE; BEGIN WHAT :mydate := next_date; IF broken THEN :b := 1; ELSE :b := 0; END IF; END; Вы уже видели (в подразделе "Однократное выполнение задания"), как использовать доступное в блоке значение JOB. Его можно использовать как первичный ключ для таблицы параметров, чтобы в максимальной степени обеспечить совместное использование SQL-операторов сеансами. Можно воспользоваться и значением переменной NEXT_DATE. Как демонстрирует представленный выше блок кода, сервер Oracle использует для установки значения переменной NEXT_DATE связываемую переменную :mydate в качестве входного параметра процедуры, но он также получает ее значение после выполнения WHAT (заданной процедуры). Если процедура изменит значение переменной NEXT_DATE, сервер Oracle будет использовать его в качестве времени следующего выполнения задания. Для иллюстрации этого приема создадим небольшую процедуру Р, которая будет выдавать сообщение в таблицу Т и устанавливать значение NEXT_DATE: tkyte@TKYTE816> create t a b l e t (msg varchar2(80)); Table created. tkyte@TKYTE816> c r e a t e or replace 2 procedure p(p_job in number, p_next_date in OUT date) 3 as 4 l_next_date date default p_next_date; 5 begin 6 p_next_date := trunc(sysdate)+l+3/24; 7 8 insert into t values 9 ('Next date имела значение "' I| 10 to_char(l_next_date,'dd-mon-yyyy hh24:mi:ss') II
Пакет DBMS_JOB 11 12 13 end; 14 /
' " Next date ИМЕЕТ значение ' |I to_char(p_next_date,'dd-mon-yyyy
5 8 5
hh24:mi:ss'));
Procedure created. Теперь пошлем эту процедуру на выполнение, используя метод, рассмотренный в подразделе "Однократное выполнение задания". Другими словами, не зададим значение параметра INTERVAL: tkyte@TKYTE816> v a r i a b l e n number tkyte@TKYTE816> exec dbms_job.submit(:n, 'p(JOB,NEXT_DATE);'); PL/SQL procedure successfully completed. tkyte@TKYTE816> s e l e c t what, i n t e r v a l , 2 to_char(last_date,'dd-mon-yyyy hh24:mi:ss') l a s t _ d a t e , 3 to_char(next_date,'dd-mon-yyyy hh24:mi:ss') next_date 4 from user_jobs 5 where job = :n 6 / WHAT
INTERVAL LAST_DATE
NEXT_DATE
p(JOB,NEXT_DATE); 18:23:01
null
28-apr-2001
В данном случае процедуре переданы только параметры JOB и NEXT_DATE. Их значения будут получены при выполнении. Как видите, это задание еще не выполнялось (столбец LAST_DATE имеет значение Null), а параметр INTERVAL получил пустое значение, так что значение NEXT_DATE будет вычисляться как SELECT NULL FROM DUAL. Обычно это означает, что задание выполнится один раз, после чего будет удалено из очереди заданий. Однако при выполнении этого задания оказывается: tkyte@TKYTE816> exec dbms_job.run(:n); PL/SQL procedure successfully completed. tkyte@TKYTE816> s e l e c t * from t ; MSG Next date имела значение " " Next date ИМЕЕТ значение 29-арг-2001
03:00:00
tkyte@TKYTE816> s e l e c t what, i n t e r v a l , 2 to_char(last_date,'dd-mon-yyyy hh24:mi:ss') l a s t _ d a t e , 3 to_char(next_date,'dd-mon-yyyy hh24:mi:ss') next_date 4 from user_jobs 5 where job = :n 6 / WHAT
INTERVAL LAST_DATE
p(JOB,NEXT_DATE);
null
NEXT_DATE
28-apr-2001 18:23:01 29-apr-2001 03:00:00
что параметр NEXT_DATE получает другое значение. Это значение NEXT_DATE было вычислено в самой процедуре, и задание снова оказалось в очереди. Пока задание бу-
586
Приложение А
дет устанавливать непустое значение NEXT_DATE, оно будет оставаться в очереди. Если когда-нибудь после успешного выполнения не будет установлено значение NEXT_DATE, задание будет удалено из очереди. Этот прием пригодится для заданий, в которых значение NEXT_DATE сложно вычислить или оно зависит от данных в других таблицах.
Контроль заданий и обнаружение ошибок Для контроля заданий в базе данных используется три основных представления. • USER_JOBS. Список всех заданий, посланных текущим зарегистрированным пользователем. У этого представления есть также общедоступный синоним, ALL_JOBS. ALL_JOBS содержит ту же информацию, что и USER_JOBS. • DBA_JOBS. Полный список всех заданий, находящихся в очередях базы данных. • DBA_JOBS_RUNNING. Список выполняющихся заданий. Обычно представление USER_JOBS доступно всем пользователям, а представления DBA_* — только пользователям с привилегией DBA или получившим привилегию SELECT непосредственно для этих представлений. В этих представлениях находится следующая информация. •
LAST_DATE/LAST_SEC. Когда последний раз выполнялось задание. LAST_DATE — столбец типа DATE. LAST_SEC — строка символов, содержащая только время (в формате часов:минут:секунд).
Q THIS_DATE/THIS_SEC. Если задание в настоящий момент выполняется, в этом столбце будет время начала выполнения. Как и пара столбцов LAST_DATE/ LAST_SEC, столбец THIS_DATE содержит дату и время, а столбец THIS_SEC — символьную строку, в которой указано только время. • NEXT_DATE/NEXT_SEC. Время, когда задание будет выполнено в следующий раз. Q TOTAL_TIME. Общее время выполнения задания в секундах. Включает время выполнения предыдущих прогонов — это суммарное значение. Q BROKEN. Флаг Yes/No, показывающий, что задание разрушено. Разрушенные задания не выполняются процессами обработчик очередей. Задание разрушается после 16 неудачных попыток выполнения. Для разрушения задания можно использовать процедуру DBMS_JOB.BROKEN (что временно предотвращает его выполнение). •
INTERVAL. Функция, возвращающая дату, которая вызывается в начале следующего выполнения задания, чтобы определить, когда снова выполнять задание.
Q FAILURES. Сколько раз подряд задание не было успешно выполнено. При успешном выполнении задания в этом столбце устанавливается значение 0. Q WHAT. Текст задания. Q NLS_ENV. Среда NLS (National Language Support — поддержка национальных языков), в которой будет выполняться задание. Включает язык, формат даты, фор-
Пакет DBMS_JOB
587
мат чисел и т.п. Среда NLS полностью наследуется из среды, откуда было послано задание. При изменении этой среды и повторной отправке задания оно будет выполняться в измененной среде. •
INSTANCE. Имеет смысл только в режиме Parallel Server. Это идентификатор экземпляра, на котором может выполняться задание, а в представлении DBA_JOBS_RUNNING — экземпляра, на котором оно выполняется.
Предположим, в этих представлениях обнаружено задание с положительным значением в столбце FAILURES. Где найти сообщения об ошибках для этого задания? Они не хранятся в базе данных, а записываются в журнал уведомлений (alert log) базы данных. Например, создана следующая процедура: tkyte@TKYTE816> create or replace procedure run_by_jobs 2 as 3 l_cnt number; 4 begin 5 select user_id into l_cnt f n a l l users; 6 — другой необходимый код 7 end; 8 / Procedure created. tkyte@TKYTE816> variable n number tkyte@TKYTE816> exec dbms_job.submit(:n, run by jobs;' ); PL/SQL procedure successfully completed. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> exec dbms lock.sleep(60); PL/SQL procedure successfully completed. tkyte@TKYTE816> select job, what, failures 2 from user_jobs 3 where job = :n; JOB WHAT FAILURES 35 run_by_jobs;
1
Если в базе данных больше одного пользователя (во всех базах данных их больше), эта процедура, определенно, не сработает. Оператор SELECT ... INTO всегда будет возвращать слишком много строк; при программировании была допущена ошибка. Однако, поскольку она происходит в фоновом режиме, причину ошибки трудно определить. К счастью, сообщение об ошибке записывается в журнал уведомлений базы данных. Если открыть этот файл в редакторе, в конце файла можно будет найти следующее: Tue Jan 09 13:07:51 2001 Errors in file C:\oracle\admin\tkyte816\bdump\tkyte816SNP0.TRC: ORA-12012: error on auto execute of job 35 ORA-01422: exact fetch returns more than requested number of rows
JOO
Приложение А
ORA-06512: a t "SCOTT.RUN_BY_JOBS", l i n e 5 ORA-06512: a t l i n e 1
Сообщение свидетельствует о том, что задание 35 (наше задание) выполнить не удалось. Что важнее, в сообщении указана причина неудачи. Такие же сообщения об ошибках выдаются при попытке выполнения процедуры в среде SQL*Plus. Эта информация принципиально важна для определения причины сбоя задания. На ее основе можно, изменив задание, обеспечить его корректную работу. Вот, пожалуй, и все о контроле выполнения заданий. Надо либо следить за журналом уведомлений, alert.log (это администратор базы данных должен делать всегда), либо периодически просматривать представление DBA_JOBS, чтобы убедиться в успешном выполнении заданий.
Резюме Пакет DBMS_JOB — замечательное средство сервера, обеспечивающее выполнение действий в фоновом режиме. Его можно использовать для автоматизации рутинных операций, таких как анализ таблиц, архивирование и очистка временных таблиц, да и любых других. Его можно использовать в приложениях для обеспечения сравнительно быстрого выполнения продолжительных действий (причем, выполнение это будет действительно быстрым с точки зрения пользователя). Пакет позволяет не писать для каждой ОС специфические сценарии, периодически выполняющие действия в базе данных. Более того, он избавляет от необходимости "зашивать" имена пользователей и пароли в сценарии для регистрации на сервере. Задание всегда выполняется от имени пользователя, который его послал, — регистрироваться вообще не нужно. Наконец, в отличие от средств периодического выполнения заданий операционных систем, эти задания сервера базы данных выполняются только в случае доступности сервера. Если система не работала в момент запланированного выполнения задания, оно не будет выполнено (очевидно, если сервер не работает, то и очереди заданий не просматриваются). Пакет DBMS_JOB — надежное средство, для которого я нашел множество применений.
•
Пакет DBMS LDB
DBMS_LOB — это стандартный пакет для работы с большими объектами (Large OBjects — LOBs) в базе данных. Большими объектами называют данные новых типов, появившиеся начиная с версии Oracle 8. Большие объекты поддерживают хранение и обработку до 4 Гбайт данных в одном столбце. Они заменяют считающиеся теперь ненужными типы данных LONG и LONG RAW. Использование типов данных LONG в Oracle было связано с множеством ограничений, в том числе: Q в таблице можно было иметь только один столбец типа LONG или LONG RAW; •
если объем данных превышал 32 Кбайта, с ними нельзя было работать в хранимых процедурах;
Q их нельзя было изменять по частям; Q многие операции в базе данных, например INSERT INTO T SELECT LONG_COL FROM T2, для столбцов типа LONG и LONG RAW не поддерживались; Q их нельзя было указывать в конструкции WHERE; Q таблицы со столбцами типа LONG и LONG RAW нельзя было реплицировать; Q и т.д... Типы данных LOB не имеют всех этих ограничений. Вместо писания всех функций и процедур пакета DBMS_LOB (их около 25) я собираюсь ответить на наиболее часто задаваемые вопросы об использовании пакета DBMS_LOB и больших объектов. Подпрограммы пакета либо чрезвычайно просты, либо хорошо описаны в документации Oracle. При использовании больших объектов основными являются два руководства:
590
Приложение А
Q Oracle8i Supplied PL/SQL Packages Reference. В этом руководстве можно найти обзор пакета DBMS_LOB и описание каждой его подпрограммы, включая все входные и выходные параметры. Соответствующий раздел пригодится как справочник. Его надо бегло просмотреть, чтобы знать основные возможности обработки больших объектов. •
Oracle8i Application Developer's Guide — Large Objects (LOBs). Руководство, полностью посвященное описанию программирования с использованием больших объектов в различных средах и языках. Каждый разработчик, предполагающий использовать большие объекты, должен его прочитать.
Кроме того, многое в работе с большими объектами зависит от языка программирования. Способ выполнения определенных действий в языке Java отличается от принятого в языке С, при программировании на PL/SQL и т.д. Вот почему корпорация Oracle разработала отдельные руководства Application Developer's Guide для языков PL/SQL, Pro*C, COBOL, VB и Java, а также библиотеки OCI, посвященные взаимодействию с большими объектами в каждом из языков. Кроме того, есть еще упомянутое исчерпывающее руководство Application Developer's Guide, посвященное большим объектам и полезное, независимо от используемого языка. Я рекомендую всем, кто собирается использовать большие объекты в приложениях, прочитать это руководство, а также специфическое руководство для выбранного языка разработки. В этих руководствах можно найти ответы на большинство вопросов. В этом разделе я собираюсь ответить на часто задаваемые вопросы о больших объектах, начиная с "как показать их на Web-странице?" и заканчивая "как преобразовать данные типа BLOB в тип CLOB, и наоборот? " — об этом недостаточно хорошо сказано в стандартной документации. С большими объектами работать очень просто, если разобраться в пакете DBMS_LOB (описан в руководстве Oracle 8i Supplied PL/SQL Packages Reference), и если вы этого еще не сделали, ознакомьтесь с его описанием сейчас, прежде чем читать данный раздел. Я предполагаю, что вы готовы к работе с большими объектами.
Как загружать большие объекты? Методов загрузки больших объектов немного. В главе 9, например, я демонстрировал загрузку больших объектов в базу данных с помощью средств SQLLDR. Кроме того, в предлагаемом корпорацией Oracle руководстве Application Developer's Guide для каждого языка продемонстрировано, как создавать и получать значения больших объектов на этом языке (у всех есть небольшие отличия). Я, однако, думаю, что при наличии каталога с файлами для загрузки проще всего использовать тип данных BFILE, объект DIRECTORY и процедуру LOADFROMFILE. В главе 9 (в первой части книги — прим. научн. ред.), посвященной загрузке данных, мы подробно рассмотрели использование процедуры DBMS_LOB.LOADFROMFILE. Детальную информацию вы сможете найти в этой главе. Кроме того, в представленном далее подразделе "Преобразования" рассматривается полный пример загрузки данных типа CLOB с помощью процедуры LOADFROMFILE.
Пакет DBMSJ-ОВ
591
Функция substr Небольшое примечание относительно функции substr, предлагаемой в пакете DBMS_LOB. Все остальные реализации функции substr, с которыми мне приходилось сталкиваться ( в том числе в языках SQL и PL/SQL), принимают следующие аргументы, в указанном порядке: substr(строка, с_какого_символа, сколько_символов); Так что substr('hello', 3, 2) даст в результате строку 11 — третий и четвертый символы (начиная с символа 3 выбрать 2 символа). В функции DBMS_LOB.SUBSTR, однако, порядок параметров другой: dbms_lob.substr(большой_объект,
сколько_символов, с_какого_символа)
Поэтому аналогичный вызов функции substr из пакета DBMS_LOB вернет строку ell. Небольшой тестовый пример подтверждает это: tkyte@TKYTE816> create t a b l e t ( s t r varchar2(10), lob c l o b ) ; Table created. tkyte@TKYTE816> i n s e r t i n t o t values ( ' h e l l o ' ,
'hello');
I row created. tkyte@TKYTE816> select substr(str, 3, 2), 2 dbms_lob.substr(lob, 3, 2) lob 3 from t 4 / SU LOB II ell Я постоянно передаю аргументы не в том порядке. Это — одна из тех вещей, о которых надо помнить!
Оператор SELECT FOR UPDATE в языке Java Чтобы изменить большой (не временный) объект в базе данных, строка, содержащая его, должна быть заблокирована соответствующим сеансом. Обычно это не учитывают те, кто пишет программы на языке Java/JDBC. Рассмотрим представленную далее небольшую программу на Java. Она: Q вставляет запись (поэтому резонно предположить, что эта запись заблокирована); О читает локатор только что созданного большого объекта; •
пытается использовать этот локатор большого объекта в DBMSJLOB.WRITEAPPEND.
процедуре
При выполнении этой Java-программы всегда выдается сообщение об ошибке: Java Test java.sql.SQLException: ORA-22920: row containing the LOB value is not locked ORA-06512: at "SYS.DBMS_LOB", line 715 ORA-06512: at line 1
J72,
Приложение А
Оказывается, вставленный большой объект больше не заблокирован сеансом. Это печальный побочный эффект стандартного режима "поддержки транзакций" протокола JDBC — по умолчанию транзакции не поддерживаются! Фиксация выполняется немедленно после каждого оператора. Если в следующем приложении не добавить вызов conn.setAutoCommit (false); сразу же после getConnection, оно не будет работать. Эта строка кода (по моему мнению) должна быть первой после любого подключения в программе, использующей интерфейс JDBC! import java.sql.*; import j ava.io.*; import oracle.jdbc.driver.*; import oracle.sql.*; // Для выполнения этого приложения нужна следующая таблица: // create table demo (id int primary key, theBlob blob); class Test { public static void main (String args []) throws SQLException , FileNotFoundException, IOException { DriverManager.registerDriver (new oracle.j dbc.driver.OracleDriver()); Connection conn = DriverManager.getConnection ("jdbc:oracle:thin:Garia:1521:ora8i", "scott", "tiger"); // Если хотите, чтобы программа сработала, уберите кошентарий со // следующей строки! // conn. setAutoCommit (false) ; Statement stint = conn. createStatement () ; // Вставляем в таблицу пустой BLOB // При первой вызове создадим его. stmt.execute ("insert into demo (id,theBlob) " + "values (l,empty_blob())"); // Теперь прочитаем его, чтобы можно было загрузить данные. ResultSet r s e t = stmt.executeQuery ("SELECT theBlob " + "FROM demo "+ "where id = 1"); if (rset.nextO) // Получить BLOB для загрузки. BLOB 1 mapBLOB = ((OracleResultSet)rset).getBLOB(l); // Вот данные, которые мы в него загрузим. File binaryFile = new File("/tmp/binary.dat"); FilelnputStream instream new FilelnputStream(binaryFile); // Мы будем загружать примерно по 32 Кбайт за раз. Это // максимальныйфрагмент, поддерживаемый пакетом dbms_lob // (ограничение языка PL/SQL).
Пакет DBMSJ.OB
593
int chunk = 32000; byte[] 1 buffer = new byte[chunk]; int l_nread = 0; // Используем простую процедуру writeappend для добавления // фрагмента файла в конец BLOB-обьекта. OracleCallableStatement cstmt = (OracleCallableStatement)conn.prepareCall ( "begin dbms lob.writeappend( :1, :2, :3 ); end;" ); // Читаем и записываем, поха не загрузим весь файл. cstmt.registerOutParameter(1, OracleTypes.BLOB); while ((l_nread= instream.read(l_buffer)) != -1) cstmt.setBLOB(1, l_mapBLOB); cstmt.setlnt(2, l_nread); cstmt.setBytes<3, l_buffer); cstmt.executeUpdate(); l_maPBLOB = cstmt.getBLOB(1); ) // Закрываем входной файл и завершаем оператор. instream.close(); cstmt.close(); } // Завершаем остальные операторы. rset.close(); stmt.closeO; conn.close (); } } Это общее ограничение протокола JDBC, влияющее, в частности, и на работу с большими объектами. Я не знаю, сколько разработчиков с удивлением обнаруживало, что интерфейс по умолчанию автоматически фиксирует транзакцию — это ведь должно делать приложение. Этого могут ожидать только разработчики, ранее использовавшие интерфейс ODBC! Аналогичное действие выполняет протокол ODBC в стандартном для него режиме автоматической фиксации.
Преобразования Часто пользователи хранят данные в объектах типа BLOB, но иногда нужно представить их как данные типа CLOB. Типичный пример: в столбец типа BLOB загружены двоичные и текстовые данные, и необходимо проанализировать текст. Анализировать данные типа BLOB сложно, поскольку сервер постоянно пытается преобразовать их в шестнадцатеричное представление, что нежелательно. В других случаях имеются данные типа LONG или LONG RAW, которые хотелось бы обрабатывать как данные типов CLOB или BLOB, поскольку функциональные возможности обработки этих типов намного превосходят те, что поддерживаются для типов LONG и LONG RAW.
Приложение А К счастью, эти преобразования легко выполнить. Можно преобразовать: • данные типа BLOB в VARCHAR2; Q VARCHAR2 - в RAW; • данные типа LONG — в CLOB; Q данные типа LONG RAW - BLOB. Рассмотрим сначала преобразование типа BLOB в VARCHAR2, и наоборот, а затем разберемся с преобразованиями типов LONG в CLOB и LONG RAW в BLOB.
Преобразование типа BLOB в VARCHAR2 и обратно В пакет UTL_RAW входят две полезные подпрограммы для работы с данными типа BLOB. Более детально пакет UTL_RAW мы рассмотрим в соответствующем разделе приложения. Пока речь идет о следующих подпрограммах: Q CAST_TO_VARCHAR2. Принимает данные типа RAW и меняет тип на VARCHAR2. Никакого преобразования данных фактически не происходит — речь идет только об изменении типа. Q CAST_TO_RAW. Принимает данные типа VARCHAR2 и меняет тип на RAW. И в этом случае данные не изменяются — изменяется только тип данных. Итак, если известно, что данные типа BLOB содержат текстовую информацию в соответствующей кодировке, эти функции действительно полезны. Используем рассмотренную ранее программу LOADFROMFILE для загрузки набора файлов в столбец типа BLOB. Хотелось бы просматривать значения в этом столбце в среде SQL*Plus (с маскировкой любых "недопустимых" символов, являющихся причиной некорректной работы программы SQL*Plus). Для этого можно использовать средства пакета UTL_RAW. Сначала загрузим ряд файлов в таблицу DEMO: scott@DEV816> create t a b l e demo 2 (id int primary key, 3 theBlob blob 4 ) 5 / Table created. scott@DEV816> create or replace directory my_files as '/export/home/tkyte'; Directory created. scott@DEV816> create sequence blob_seq; Sequence created. scott@DEV816> create or replace 2 procedure load_a_file(p_dir_name in varchar2, 3 p_file_name in varchar2) 4 as 5 1 blob blob;
Пакет DBMSJ.OB
595
1 bfile bfile; 6 7 begin 8 — Сначала необходимо создать большой объект в базе данных. — Для загрузки нужен пустой объект типа CLOB, BLOB или большой 9 10 — объект, созданный с помощью вызова CREATE TEMPORARY. 11 12 insert into demo values (blob_seq.nextval, empty blob()!i 13 returning theBlob into l_Blob; 14 — Затем открываем файл (объект типа BFILE), 15 16 — из которого будем загружать данные. 17 18 1 bfile := bfilename(p dir name, p file name); dbms lob.fileopen(1 bfile); 19 20 21 22 — После этого вызываем LOADFROMFILE, загружая в только что 23 — созданный CLOB все содержимое файла (объекта типа BFILE), 24 — который только что открыли. dbms lob.loadfromfile(l blob, 1 bfile, 25 26 dbms_lob.getlength(l_bfile)); 27 28 — Закрываем файл (объект типа BFILE), чтобы — избежать возможной нехватки дескрипторов файла. 29 30 31 dbms lob.fileclose(l bfile); 32 end; 33 / Procedure created. scott@DEV816> exec load_a_file('MY_FILES', 'clean.sql'); PL/SQL procedure successfully completed. scott@DEV816> exec load_a_file('MY_FILES', 'expdat.dmp'); PL/SQL procedure successfully completed. Итак, я загрузил два файла. Один из них — сценарий, над которым я сейчас работаю, clean.sql. Другой — файл экспорта expdat.dmp, подвернувшийся под руку. Теперь я собираюсь написать функцию, которую можно будет вызывать в SQL-операторах и позволяющую просматривать любой 4000-байтовый фрагмент данных типа BLOB в среде SQL*Plus. Просматривать можно не более 4000 байт, поскольку именно такое ограничение в SQL налагается на размер данных типа VARCHAR2. Представленная ниже функция CLEAN работает аналогично функции SUBSTR для обычной строки, но принимает параметр типа BLOB и необязательные параметры FROM_BYTE и FOR_BYTES. Это позволяет выбирать и выдавать подстроку объекта типа BLOB. Обратите внимание, как используется функция UTL_RAW.CAST_TO_VARCHAR2 для преобразования типа RAW в тип VARCHAR2. Если не использовать эту функцию, байты данных типа RAW перед помещением в переменную типа VARCHAR2 будут преобразовываться в шестнадцатеричное представление. С помощью этой функции мы просто меняем тип данных с RAW на VARCHAR2, не выполняя никаких преобразований:
Приложение А scott@DEV816> create or replace 2 function clean(p_raw in blob, 3 p_from_byte in number default 1, 4 p_for_bytes in number default 4000) 5 return varchar2 6 as 7 l_tmp varchar2(8192) default 8 utl_raw.cast_to_varchar2( 9 dbms_lob.substr(p_raw,p_for_bytes,p_from_byte) 10 ); 11 l_char char(l); 12 l_return varchar2(16384); 13 l_whitespace varchar2(25) default 14 chr(13) || chr(10) || chr(9); 15 l_ws_char varchar2(50) default 16 'rnt'; 17 18 begin 19 for i in 1 .. length(l_tmp) 20 loop 21 l_char := substr(l_tmp, i, 1 ) ; 22 23 — Если символ — "печатный" (а не управляющий), ничего с: ним 24 — делать не надо. Если это \, добавить еще один символ 25 — \, поскольку мы будем заменять символы новой строки и 26 — табуляции последовательностями \n, \t и т.д., поэтому 27 — надо различать в файле текст \п и символ новой строки. 28 29 if (ascii(l_char) between 32 and 127) 30 then 31 l_return := l_return I I l_char; 32 if (l_char = 'V ) then 33 l_return := l_return || ' V ; 34 end if; 35 36 — Если символ — пробельный, заменить его 37 — специальным символом типа \r, \n, \t 38 39 elsif (instr(l_whitespace, l_char) > 0) 40 then 41 l_return := l_return || 42 -V || 43 substr(l_ws_char, instr(l_whitespace,l_char), 1 ) ; 45 46 47 48 49 50 51 52
— Вместо всех остальных непечатных символов — просто выдать точку ('.')• else l_return := l_return II end if; end loop;
'.';
Пакет DBMS_LOB
597
53 — Теперь надо вернуть первые 4000 байт, поскольку больше 54 — язык SQL все равно не позволит увидеть. После обработки в 55 — строке может оказаться более 4000 символов, поскольку CHR(10) 56 — превратится в \п (используется два байта) и т.д., т.е. это 57 — необходимо. 58 return substr(l_return,1,4000); 59 end; 60 / Function created. scott@DEV816> select id, 2 dbms_lob.getlength(theBlob) len, 3 clean(theBlob,30,40) piece, 4 dbms_lob.substr(theBlob,40,30) raw_data 5 from demo; ID
LEN PIECE
RAW_DATA
1
3498 \ndrop sequence 0A64726F702073657175656E636520 blob_seq;\n\ncreate 626C6F625F7365713B0A0A63726561 table d 7465207461626C652064
2
2048 TE\nRTABLES\nlO24\nO 54450A525441424C45530A31303234 \n28\n4000\n 0A300A32380A343030300A0001001F 00010001000000000000
Как видите, теперь можно просматривать текстовые части данных типа BLOB в среде SQL*Plus, как обычный текст, воспользовавшись функцией CLEAN. Если использовать функцию DBMS_LOB.SUBSTR, возвращающую значение типа RAW, мы получим результат в шестнадцатеричном виде. Просматривая шестнадцатеричное представление, можно убедиться, что первый байт первого объекта типа BLOB имеет значение 0А, или CHR(10) — это символ новой строки. В текстовом представлении большого объекта можно увидеть, что функция CLEAN преобразовала 0А в \п (символ новой строки). Это подтверждает, что функция выполнена, как предполагалось. Во втором объекте типа BLOB мы видим много двоичных нулей (значений 00 в шестнадцатеричном представлении) в обычном представлении содержимого файла expdat.dmp. В функции CLEAN, как видите, они преобразуются в точки, поскольку подобные специальные символы, при выдаче непосредственно на терминал, будут выдаваться в нераспознаваемом виде (как мусор). Помимо функции CAST_TO_VARCHAR2 пакет UTL_RAW содержит функцию CAST_TO_RAW. Как было показано ранее, в объект типа BLOB можно поместить обычный текст. Если для изменения этих данных надо использовать строки, пришлось бы преобразовывать их в шестнадцатеричный вид. Например, следующий оператор: scott@DEV816> update demo 2 set theBlob - 'Hello World' 3 where id = 1 4 / set theBlob = 'Hello World' * ERROR at line 2: ORA-01465: invalid hex number
Приложение А
не работает. При неявном преобразовании данных из типа VARCHAR2 в RAW предполагается, что строка Hello World состоит из шестнадцатеричных цифр. Сервер Oracle берет первые два байта, преобразует их из шестнадцатеричного в десятичный вид и присваивает полученное значение первому байту данных типа RAW, и т.д. Надо либо преобразовать строку Hello World в шестнадцатеричный вид, либо изменить тип данных с VARCHAR2 на RAW — изменить только тип данных, не меняя сами байты данных. Например: scott@DEV816> update demo 2 s e t theBlob = utl_raw.cast_to_raw('Hello 3 where i d = 1 4 /
World')
1 row updated. scott@DEV816> commit; Commit complete. scott@DEV816> s e l e c t id, 2 dbms_lob.getlength(theBlob) len, 3 clean(theBlob) piece, 4 dbms_lob.substr(theBlob,40,l) raw_data 5 from demo 6 where id =1; ID LEN PIECE RAW_DATA 1
11 H e l l o World
48656C6C6F20576F726C64
Использование UTL_RAW.CAST_TO_RAW('Hello World') обычно намного проще преобразования строки Hello World в шестнадцатеричное представление — 48656C6C6F20576F726C64.
Преобразование данных типа LONG/LONG RAW в большой объект Преобразовать данные типа LONG или LONG RAW в большой объект очень просто. Стандартная функция TO_LOB языка SQL позволяет это сделать. Использование функции TO_LOB, однако, весьма ограничено. Ее можно применять исключительно в операторах INSERT или CREATE TABLE AS SELECT и только в языке SQL (но не в PL/SQL). В результате первого ограничения нельзя выполнять операторы, подобные следующему: a l t e r t a b l e t add column clob_columri; update t s e t clob_column = to_lob(long_column); a l t e r t a b l e t drop column long_column; При попытке выполнения UPDATE будет получено сообщение об ошибке: ORA-00932: inconsistent
datatypes
Пакет DBMSJ.OB
599
Для множественного преобразования типа в существующих таблицах со столбцами LONG/LONG RAW придется создавать новую таблицу. В большинстве случаев это вполне допустимо, поскольку данные типа LONG и LONG RAW хранятся в самой строке (inline) вместе с остальными данными таблицы. Если преобразовать их в большие объекты, а затем удалить столбец типа LONG, таблица окажется не в лучшем виде: будет много выделенного и неиспользуемого пространства. Такие таблицы лучше пересоздавать. Второе ограничение означает, что функцию TO_LOB нельзя использовать в PL/SQLблоке. Чтобы использовать TO_LOB в PL/SQL, придется прибегнуть к динамическому SQL. Вскоре я это продемонстрирую. В следующих примерах мы рассмотрим два способа использования функции TO_LOB. Один из них — использование функции TO_LOB в операторе CREATE TABLE AS SELECT или INSERT INTO. Другой способ пригодится, когда данные должны остаться в столбце типа LONG или LONG RAW. Например, старому приложению нужен именно тип LONG. Хотелось бы предоставить другим приложениям возможность работать с этим столбцом как с большим объектом, чтобы можно было обрабатывать его значение в PL/SQL по частям с помощью функций пакета DBMS_LOB, например READ и SUBSTR. Начнем с создания данных типа LONG и LONG RAW: ops$tkyte@DEV816> create t a b l e long_table 2 (id i n t primary key, 3 data long 4 ) 5 / Table created. ops$tkyte@DEV816> create table long_raw_table 2 (id int primary key, 3 data long raw 4 ) / Table created. ops$tkyte@DEV816> declare 2 l_tmp long := 'Hello World'; 3 l_raw long raw; 4 begin 5 while(length(l_tmp) < 32000) 6 loop 7 l_tmp := l_tmp I I ' Hello World 8 end loop; 9 10 insert into long_table 11 (id, data) values 12 (1, l_tmp); 13 14 1 raw := utl raw.cast to raw(l tmp) 15 16 insert into long raw table (id, data) values 17
OUU
Приложение А
18 (1, l_raw); 19 20 dbms_output.put_line('created long with length = ' || 21 length(l_tmp)); 22 end; 23 / created long with length = 32003 PL/SQL procedure successfully completed.
Пример множественного однократного преобразования типа Итак, имеется две таблицы с одной строкой и столбцом типа LONG или LONG RAW. Преобразование типа данных из LONG в CLOB легко выполнить с помощью следующего оператора CREATE TABLE AS SELECT:
ops$tkyte@DEV816> create table clob_table 2 as 3 select id, to_lob(data) data 4 from long_table; Table created. Кроме того, мы могли создать таблицу ранее и использовать для наполнения ее данными разновидность оператора INSERT INTO: ops$tkyte@DEV816> insert into clob_table 2 select id, to_lob(data) 3 from long_table; 1 row created.
Следующий пример показывает, что функция TO_LOB не работает в PL/SQL-блоке, как и следовало ожидать: ops$tkyte@DEV816> begin 2 insert into clob_table 3 select id, to_lob(data) 4 from long_table; 5 end; 6 / begin * ERROR at line 1: ORA-06550: line 3, column 16: PLS-00201: identifier 'TOJLOB' must be declared ORA-06550: line 2, column 5: PL/SQL: SQL Statement ignored Это ограничение легко обойти с помощью динамического SQL (придется выполнять оператор INSERT динамически, а не статически, как в примере выше). Теперь, разобравшись, как преобразовывать данные типа LONG или LONG RAW в тип CLOB или BLOB, рассмотрим производительность такого преобразования. Обычно таблицы со
Пакет DBMS LOB
601
столбцами типа LONG и LONG RAW — большого размера. Они большие по определению, поскольку используются для хранения очень больших объектов. Во многих случаях их размер достигает многих гигабайт. Вопрос в том, можно ли выполнить множественное преобразование за допустимое время? Рекомендую использовать следующие возможности: Q невосстановимые действия, такие как непосредственная вставка и опция NOLOGGING; Q распараллеливание операторов DML (в частности, параллельные вставки); •
параллельные запросы.
Ниже представлен пример использования этих возможностей. У меня есть большая таблица IMAGE, содержащая многие сотни загруженных из Web файлов. Таблица содержит столбцы NAME (название документа), MIME_TYPE (например, application/MSWord), IMG_SIZE (размер документа в байтах) и, наконец, сам документ в столбце типа and LONG RAW. Преобразуем эту таблицу так, чтобы документ хранился в столбце типа BLOB. Можно начать с создания новой таблицы: scott@DEV816> CREATE TABLE " S C O T T " . " T "
2 3 4 5 6 7
("NAME" VARCHAR2(255), "MIMEJTYPE" VARCHAR2(255), "IMG_SIZE" NUMBER, "IMAGE" BLOB) PCTFREE 0 PCTUSED 40 INITRANS 1
8 9 10 11
MAXTRANS 2 5 5 NOLOGGING TABLESPACE "USERS" LOB ("IMAGE") STORE AS
12 13 14 15
(TABLESPACE "USERS" DISABLE STORAGE IN ROW CHUNK 32768 PCTVERSION 10 NOCACHE
16 17
NOLOGGING ) ;
Table created.
Обратите внимание, что таблица и большой объект создаются с опцией NOLOGGING — это важно. Можно не создавать их так сразу, а применить оператор ALTER. Теперь, чтобы преобразовать данные из существующей таблицы IMAGE, выполним следующее: scott@DEV816> ALTER SESSION ENABLE PARALLEL DML; Session
altered.
scott@DEV816> INSERT /*+ APPEND PARALLEL(t,5) * / INTO t 2 SELECT /*+ PARALLEL(long_raw,5) * / 3 name, mime_type, img_size, t o _ l o b ( i m a g e ) 4 FROM long_raw;
В результате выполняется непосредственная параллельная вставка в объекты типа BLOB без журнализации. Для сравнения я выполнил INSERT INTO с включенной и
602
Приложение А
отключенной журнализацией и получил следующие результаты (на подмножестве преобразуемых строк): scott@DEV816> c r e a t e t a b l e t 2 as 3 select name, mime_type, img_size, to_lob(image) image 4 from image where 1=0; Table created. scott@DEV816> set autotrace on scott@DEV816> insert into t 2 select name, mime_type, img_size, to_lob(image) image 3 from image; 99 rows created. Execution Plan 0 1
0
INSERT STATEMENT Optimizer=CHOOSE TABLE ACCESS (FULL) OF 'IMAGE'
Statistics 1242 36057 12843 7870 34393500 1006 861 4 2 0 99
recursive c a l l s db block gets consistent gets physical reads redo size bytes sent via SQL*Net to c l i e n t bytes received via SQL*Net from c l i e n t SQL*Net roundtrips to/from c l i e n t s o r t s (memory) s o r t s (disk) rows processed
Обратите внимание, что в результате было сгенерировано 34 Мбайт данных повторного выполнения (если суммировать размеры 99 изображений, получится 32 Мбайт данных). Если таблица Т, как было показано ранее, создается с опцией NOLOGGING и используется непосредственная вставка, получим: scott@DEV816> INSERT /*+ APPEND */ INTO t 2 SELECT name, mime_type, img_size, to_lob(image) 3 FROM image; 99 rows created. Execution Plan 0 1
0
INSERT STATEMENT Optimizer=CHOOSE TABLE ACCESS (FULL) OF 'IMAGE'
Statistics 1242 36474 13079 6487
recursive calls db block gets consistent gets physical reads
Пакет DBMS LOB
1355104 1013 871 4 2 0 99
603
redo s i z e bytes sent via SQL*Net t o c l i e n t bytes received via SQL*Net from c l i e n t SQL*Net roundtrips to/from c l i e n t s o r t s (memory) s o r t s (disk) rows processed
Сгенерировано лишь около 1 Мбайт информации в журнал. Это преобразование выполняется существенно быстрее, при этом генерируется намного меньше данных в журналы повторного выполнения. Конечно, как и для всех невосстанавливаемых операций, необходимо обеспечить резервное копирование базы данных как можно раньше, чтобы новые объекты можно было восстановить. Иначе в случае сбоя диска преобразование данных придется выполнять заново. Представленный выше пример нельзя повторить непосредственно. У меня случайно под рукой оказалась таблица IMAGE, содержащая около 200 Мбайт данных. Она использовалась для демонстрации множественных однократных преобразований и влияния опции NOLOGGING на объем генерируемых при этом данных повторного выполнения.
Оперативное преобразование типа данных Во многих случаях необходимо читать данные типа LONG или LONG RAW в различных средах, но оказывается, что это не получается. Например, при использовании языка PL/SQL, если объем данных типа LONG RAW превышает 32 K6aftTf, их практически невозможно прочитать. В других языках и интерфейсах тоже есть проблемы с данными типа LONG и LONG RAW. С помощью функции TO_LOB и временной таблицы, однако, можно оперативно преобразовать данные типа LONG или LONG RAW в тип CLOB или BLOB. Это очень удобно, например, при использовании средств загрузки файлов в OAS4.X или WebDB. Эти средства загружают документы по сети (через Web) в таблицу базы данных, но, к сожалению, загружают они их в столбец типа LONG RAW. Это делает практически невозможной работу с документами в PL/SQL. Представленные ниже функции показывают, как обеспечить прозрачный доступ к таким данным через промежуточный BLOB-объект. Начнем с создания временной таблицы для хранения преобразованного объекта типа CLOB/BLOB и последовательности, идентифицирующей строку: ops$tkyte@DEV816> create global temporary table lob_ temp 2 (id int primary key, 3 с lob clob, 4 b_lob blob 5 ) 6 / Table created. ops$tkyte@DEV816> create sequence lob temp seq; Sequence created.
604
Приложение А
Теперь создадим функции TO_BLOB и TO_CLOB. Эти функции используют для оперативного преобразования данных типа LONG или LONG RAW следующий подход. Q Пользователь выбирает идентификатор строки из таблицы со столбцом типа LONG или LONG RAW, а не значение столбца LONG или LONG RAW в этой строке. Функции передается имя столбца типа LONG, имя таблицы и идентификатор нужной строки. Q Функция получает последовательный номер, идентифицирующий строку, которая будет создаваться во временной таблице. Q С помощью динамического SQL к указанному столбцу типа LONG или LONG RAW применяется функция TO_LOB. Использование динамического SQL не только делает функцию универсальной (она может работать со столбцом типа LONG в любой таблице), но и позволяет непосредственно вызывать функцию TO_LOB в языке PLSQL. •
Функция считывает из временной таблицы значение созданного объекта типа BLOB или CLOB таблицы и возвращает вызывающему.
Вот код для функций TO_BLOB и TO_CLOB: ops$tkyte@DEV816> c r e a t e or replace 2 function to_blob(p_cname in varchar2, 3 p_tname in varchar2, 4 p_rowid in rowid) return blob 5 as 6 l_blob blob; 7 l_id int; 8 begin 9 select lob temp seq.nextval into 1 id from dual; 10 ~ ~ 11 execute immediate 12 'insert into lob_temp (id,b_lob) 13 select :id, to_lob(' || p_cname II ') 14 from ' | | p_tname | | 15 ' where rowid = :rid ' 16 using IN l_id, IN p_rowid; 17 18 select b_lob into l_blob from lob_temp where id = l_id ; 19 20 return l_blob; 21 end; 22 / Function created. ops$tkyte@DEV816> create or replace 2 function to_clob(p_cname in varchar2, 3 p_tname in varchar2, 4 p_rowid in rowid) return clob 5 as 6 l_clob clob; 7 1 id int;
Пакет DBMSJ-OB
605
8 begin 9 select lob_temp_seq.nextval into l_id from dual; 10 11 execute immediate 12 'insert into lob_temp (id,c_lob) 13 select :id, to_lob(' || p_cname || ') 14 • || p_tname || from ' 15 ' where rowid = :rid 16 using IN l_id, IN p_rowid; 17 18 select c_lob into l_clob from lob_temp where id - l_id ; 19 20 return l_clob; 21 end; 22 / Function created. Теперь можно продемонстрировать использование этих функций с помощью простого PL/SQL-блока. Данные типа LONG RAW будут преобразованы в BLOB, и выдана длина полученного объекта и небольшая часть его данных: ops$tkyte@DEV816> declare 2 1_ЫоЬ blob; 3 l_rowid rowid; 4 begin 5 select rowid into l_rowid from long_raw_table; 6 l_blob := to_blob('data', 'long_raw_table', l_rowid); 7 dbms_output.put_line(dbms_lob.getlength(l_blob)); 8 dbms_output.put_line( 9 utl_raw.cast_to_varchar2( 10 dbms_lob. substr (1_ЫоЬ, 41,1) 11 ) 12 ); 13 end14 / 32003 Hello World Hello World Hello World Hello PL/SQL procedure successfully completed. Для тестирования функции TO_CLOB применяется практически такой же код, но использовать средства пакета UTL_RAW не нужно: ops$tkyte@DEV816> declare 2 l_clob clob; 3 l_rowid rowid; 4 begin 5 select rowid into l_rowid from long_table; 6 l_clob :- to_clob('data', 'long_table', l_rowid); 7 dbms_output.put_line(dbms_lob.getlength(l_clob)); 8 dbms_output.put_line(dbms_lob.substr(l_clob,41,1)); 9 end;
606
Приложение А
32003 Hello World Hello World Hello World Hello PL/SQL procedure successfully completed.
Запись значений объекта типа BLOB/CLOB на диск Этой возможности в пакете DBMS_LOB недостает. Пакет предоставляет средства загрузки больших объектов из файлов, но не создания файла, содержащего большой объект. Решение этой проблемы предложено в главах 18 и 19. Там приведен код на языке С и Java для внешней процедуры, записывающей значение столбца типа BLOB, CLOB в базе данных или временного большого объекта в файл файловой системы сервера. Обе реализации выполняют одну и ту же функцию, просто использованы разные языки. Применяйте ту из них, которая больше подходит для вашего сервера (например, если на сервере не установлена поддержка языка Java, но есть прекомпилятор Рго*С и компилятор языка С, то внешняя процедура на языке С подойдет больше).
Выдача большого объекта на Web-странице с помощью PL/SQL Представленный ниже пример предполагает, что в системе установлены и работают следующие компоненты: Q компонент прослушивания (lightweight listener) WebDB; Q сервер приложений OAS 2.x, 3.x или 4.x с PL/SQL-картриджем; Q сервер iAS с модулем mod_plsql. При отсутствии любого из этих компонентов пример выполнить не получится. В нем используется набор инструментальных средств PL/SQL Web Toolkit (речь идет о широко известных функциях НТР), а также PL/SQL-картридж или модуль. Предполагается также, что наборы символов (кодировки) на Web-сервере (клиенте сервера базы данных) и в базе данных совпадают. Дело в том, что PL/SQL-картридж или модуль использует для генерации страниц из базы данных тип VARCHAR2. Если набор символов у клиента (в данном случае клиентом является Web-сервер) отличается от набора символов в базе данных, будет выполнено преобразование. При этом обычно повреждаются данные типа BLOB. Предположим, Web-сервер работает на платформе Windows NT. Обычно для клиента на платформе Windows NT используется набор символов WE8ISO8859P1 — западноевропейская 8-битовая кодовая страница. А сервер баз данных работает на платформе Solaris. Стандартной и наиболее типичной кодовой страницей на этой платформе является 7-битовая US7ASCII. При попытке передачи значения BLOB через интерфейс VARCHAR2 в случае использования такой пары кодовых страниц окажется, что старший бит данных из базы сброшен. Данные изменятся. Только если кодировки на клиенте (Web-сервере) и сервере базы данных совпадают, данные передаются без искажений.
Пакет DBMS LOB
607
Итак, предполагая, что все предварительные условия выполнены, можно рассмотреть использование средств PL/SQL Web Toolkit для выдачи значения BLOB на Webстранице. Продолжим один из предыдущих примеров преобразования, в котором была создана таблица DEMO. Загрузим в нее еще один файл: ops$tkyte@DEV816> exec load_a_file('MY_FILES',
'demo.gif);
PL/SQL procedure successfully completed. Это будет GIF-файл. Теперь необходим пакет, который сможет выбрать это изображение в формате GIF и выдать его на Web-странице. Он может иметь следующий вид: ops$tkyte@DEV816> c r e a t e or replace package image_get 2 as 3 — Можно задать соответствующее имя процедуры 4 — для каждого типа отображаемых документов, 5 — например: 6 — procedure pdf 7 — procedure doc 8 — procedure txt 9 — и т.д. Некоторые браузеры (MS IE, например) при обработке 10 — документов используют расширения имен файлов, 11 — а не mime-типы 12 procedure gif(p_id in demo.id%type); 13 end; 14 / Package created. ops$tkyte@DEV816> create or replace package body image get 2 as 3 4 procedure gif(p_id in demo.id%type) 5 is 6 l_lob blob; 7 l_amt number default 32000; 8 l_off number default 1; 9 l_raw raw(32000); 10 begin j: 12 13 14 15 16 17 18 19 20 21 22 23 24 25
— Получить LOB-локатор для - нашего документа. select theBlob into l_lob from demo where id = p_id; — Выдать mime-заголовок для — документа этого типа. owa util.mime header('image/gif); begin loop dbms_lob.read(l_lob, l_amt, l_off, l_raw);
608
Приложение А
26 — Важно использовать вызов htp.PRN, чтобы избежать 27 — добавления в документ ненужных символов 28 — перевода строки. 29 htp.prn(utl_raw.cast_to_varchar2(l_raw)); 30 l_off := l_off+l_amt; 31 l_amt := 32000; 32 end loop; 33 exception 34 when no_data_found then 35 NULL; 36 end; 37 end; 38 39 end; 40 / Package body created. При наличии DAD (Database Access Descriptor — дескриптор доступа к базе данных, который обычно создается при настройке PL/SQL-картриджа или модуля) с именем mydata можно использовать адрес URL http://myhost:myport/pls/mydata/image_get.gif?p_id=3 для получения изображения. Аргумент P_ID=3 передается процедуре image_get.gif, требующий от нее выдать локатор большого объекта, который хранится в строке со значением id=3. Это изображение можно включить в страницу с помощью тэга IMG: 3To моя CTpaHHU.a Это мой GIF-файл
Резюме Большие объекты предлагают намного больше возможностей, чем устаревший тип данных LONG. В этом разделе я ответил на некоторые часто задаваемые вопросы, касающиеся работы с большими объектами. Мы рассмотрели, как загружать большие объекты в базу данных. Мы разобрались, как преобразовать данные типа BLOB в CLOB, и наоборот. Мы выяснили, как эффективно преобразовать все существующие унаследованные данные типа LONG и LONG RAW в типы CLOB и BLOB с помощью невосстановимых и распараллеливаемых действий. Наконец, мы обсудили использование средств PL/SQL Web Toolkit для получения данных типа CLOB или BLOB и отображения их на Web-странице.
Пакет DBMSJ-ОСК . Пакет DBMS_LOCK дает программисту доступ к механизму блокирования, используемому сервером Oracle. Он позволяет создавать собственные именованные блокировки. Эти блокировки можно контролировать точно так же, как и любые другие блокировки Oracle. Они будут отображаться в представлении динамической производительности V$LOCK как блокировки типа UL (user lock — пользовательская блокировка). Кроме того, они будут отображаться любыми стандартными средствами, такими как Oracle Enterprise Manager и сценарий UTLOCKT.SQL (который находится в каталоге [ORACLE_HOME]/rdbms/admin). Помимо обеспечения доступа к механизму блокирования пакет DBMS_LOCK (благодаря наличию функции SLEEP) позволяет приостановить работу PL/SQL-программы на указанное количество секунд. Пакет DBMS_LOCK имеет много применений, например. Q Предположим, имеется подпрограмма, использующая средства пакета UTL_FILE для записи сообщений проверки в файл операционной системы. Записывать сообщения в этот файл процессы должны поочередно. В некоторых операционных системах, например в ОС Solaris, записывать данные в файл могут одновременно много пользователей (ОС этого не предотвращает). В результате сообщения с данными проверки перемешиваются, и читать их сложно или невозможно. Пакет DBMS_LOCK можно использовать для обеспечения очередности доступа к этому файлу. Q Можно предотвратить одновременное выполнение взаимоисключающих действий. Предположим, имеется программа подготовки данных, которая работает только при условии, что данные не используются другими сеансами. Сеансы не должны обращаться к данным в процессе их подготовки. Сеанс подготовки должен уста20 Зак. 244
610
Приложение А
навливать именованную блокировку в режиме X (как исключительную). Другие сеансы должны пытаться установить эту же именованную блокировку в режиме S (как разделяемую). Запрос блокировки X будет ожидать, если имеются блокировки S, а запрос блокировки S будет ожидать, если удерживается блокировка X. В результате сеанс подготовки данных будет в состоянии ожидания, пока работают "обычные" сеансы, но если сеанс подготовки уже начался, все остальные сеансы будут заблокированы до его завершения. У этого пакета есть два основных варианта использования. Оба они подходят, если все сеансы согласованно используют блокировки (ничто не мешает сеансу использовать средства пакета UTL_FILE для открытия и записи в файл проверки без каких-либо попыток установить соответствующую блокировку). В качестве примера попытаемся решить проблему взаимоисключающего доступа, что пригодится во многих приложениях. Проблема возникает при попытке двух сеансов вставить данные в одну и ту же таблицу, для которой задано ограничение первичного ключа или уникальности. Если оба сеанса попытаются использовать одни и те же значения в столбцах, связанных этим ограничением, второй (третий и т.д.) сеанс будет заблокирован, пока не зафиксируется или отменится транзакция первого сеанса. Когда первый сеанс зафиксирует транзакцию, в заблокированных сеансах будет получено сообщение об ошибке. Только если в первом сеансе будет выполнен откат, один из последующих сеансов сможет успешно выполнить вставку. Суть проблемы в том, что пользователи после вынужденного ожидания узнают, что выполнить необходимое действие невозможно. Этой проблемы можно избежать при использовании оператора UPDATE, поскольку можно заранее заблокировать строку, которую предполагается менять, так, чтобы работа других сеансов не блокировалась. Другими словами, вместо выполнения: update emp s e t ename = 'King' where empno = 1234; можно написать: s e l e c t ename from emp where empno = 1234 FOR UPDATE NOWAIT; update emp s e t ename = 'King' where empno = 1234; За счет использования конструкции FOR UPDATE NOWAIT в операторе SELECT можно заблокировать строку для использования сеансом (так что выполнение UPDATE не будет заблокировано) или будет получено сообщение об ошибке ORA-54 'Resource Busy'. Если при выполнении оператора SELECT сообщений об ошибках не получено, строка уже заблокирована. Однако при выполнении операторов INSERT этот метод неприменим. Нет строки, которую можно было бы выбрать с помощью SELECT и заблокировать, а потому нет и способа предотвратить вставку строки с таким же значением в других сеансах, что приведет к блокированию и потенциально бесконечному ожиданию в текущем сеансе. Вот тут и поможет пакет DBMS_LOCK. Чтобы продемонстрировать, как, я создам таблицу с первичным ключом, предотвращающим одновременную вставку одних и тех же значений двумя (или более) сеансами. Для этой таблицы я задам триггер. Триггер будет использовать функцию DBMS_UTILITY.GET_HASH_VALUE (подробнее о ней см. в разделе, посвященном пакету DBMS_UTILITY, далее в этом приложении) для получения по первичному ключу числового хеш-значения в диапазоне от 0 до 1073741823 (ди-
Пакет DBMS_LOCK
6 1
апазон значений идентификаторов блокировок, допускаемых сервером Oracle). В этом примере я задал размер хеш-таблицы равным 1024, т.е. по первичным ключам будет получено одно из 1024 значений идентификаторов блокировок. Затем я использую вызов DBMS_LOCK.REQUEST для выделения исключительной блокировки с этим идентификатором. В каждый момент времени это сможет сделать только один сеанс, поэтому, если другой сеанс попытается вставить запись в таблицу с таким же первичным ключом, его запрос на блокировку завершится неудачно (и будет получено сообщение об ошибке RESOURCE BUSY): tkyte@TKYTE816> create table demo (x Int primary key); Table created. tkyte@TKYTE816> create or replace trigger demo_bifer 2 before insert on demo 3 for each row 4 declare 5 l_lock_id number; 6 resource_busy exception; 7 pragma exception_init(resource_busy, -54); 9 l_lock_id := 10 dbms_utility.get_hash_value(to_char(:new.x), 0, 1024); 11 12 if (dbms_lock.request 13 (id => l_lock_id, 14 lockmode => dbms_lock.x_mode, 15 timeout -> 0, 16 release_on_commit => TRUE) = 1) 17 then 18 raise resource_busy; 19 end if; 20 end; 21 / Trigger created.
•
Если в двух отдельных сеансах теперь выполнить: tkyte@TKYTE816> insert into demo values (1); 1 row created. то в первом сеансе оператор выполнится, но во втором будет выдано: tkyte@TKYTE816> insert into demo values (1); insert into demo values (1) ERROR at line 1: ORA-00054: resource busy and acquire with NOWAIT specified ORA-06512: a t "TKYTE.DEMO_BIFER", l i n e 15 ORA-04088: e r r o r during execution of t r i g g e r 'TKYTE.DEMO_BIFER' если в первом сеансе транзакция будет зафиксирована, то будет выдано сообщение о нарушении ограничения уникальности.
0 1 L
Приложение А
Идея здесь в том, чтобы в триггере брать первичный ключ таблицы и помещать его значение в строку символов. После этого можно использовать функцию DBMS_UTILITY.GET_HASH_VALUE для получения "почти уникального" хеш-значения для строки. Если использовать хеш-таблицу размером не более 1073741823 значений, можно будет заблокировать это значение в исключительном режиме с помощью пакета DBMS_LOCK. Можно также использовать подпрограмму ALLOCATE_UNIQUE пакета DBMS_LOCK, но на это потребуются дополнительные ресурсы. Подпрограмма ALLOCATE_UNIQUE создает уникальный идентификатор блокировки в диапазоне от 1073741824 до 1999999999. Для этого она использует другую таблицу в базе данных и рекурсивную (автономную) транзакцию. Благодаря хешированию используется меньше ресурсов и, кроме того, можно избежать вызова рекурсивных SQL-операторов. После хеширования мы берем полученное значение и с помощью пакета DBMS_LOCK запрашиваем блокировку с соответствующим идентификатором в исключительном режиме с нулевым временем ожидания (если значение кем-то уже заблокировано, происходит немедленный возврат). Если получить блокировку за это время не удалось, возбуждается исключительная ситуация ORA-54 RESOURCE BUSY. В противном случае можно выполнить оператор INSERT, и он не будет заблокирован. Конечно, если в качестве первичного ключа таблицы используется целое число и есть уверенность, что значения ключа не превысят 1 миллиарда, можно его не хешировать, а использовать непосредственно в качестве идентификатора блокировки. Нужно "поиграть" с размером хеш-таблицы (в моем примере — 1024), чтобы избежать сообщений RESOURCE BUSY, связанных с получением одного и того же хешзначения по разным строкам. Размер хеш-таблицы зависит от приложения (точнее, от используемых данных); на него также влияет количество одновременно выполняемых вставок. Кроме того, владельцу триггера понадобится непосредственно (не через роль) предоставленная привилегия EXECUTE на пакет DBMS_LOCK. Наконец, при вставке большого количества строк таким способом, без фиксации может не хватить ресурсов ENQUEUE_RESOURCES. В случае возникновения такой проблемы (при этом генерируется соответствующее сообщение) необходимо увеличить значение параметра инициализации ENQUEUE_RESOURCES. Можно также добавить в триггер флаг, позволяющий включать и отключать эту проверку. Например, если бы я планировал вставлять сотни/тысячи записей, то не хотел бы выполнять подобную проверку при каждой вставке. Пользовательские блокировки, а также количество первичных ключей с соответствующим хеш-значением, можно получить из представления VSLOCK. Например, если такой триггер был установлен для рассмотренной в предыдущих примерах таблицы DEMO, мы получим: tkyte@TKYTE816> i n s e r t i n t o demo values (1); 1 row created. tkyte@TKYTE816> s e l e c t sid, type, i d l 2 from v$lock 3 where sid = (select sid from v$mystat where rownum = 1) 4 / SID TY
ID1
8 TX
589913
Пакет DBMS LOCK
8 TM 8 UL
613
30536 827
tkyte@TKYTE816> b e g i n 2 dbms_output.put_line 3 (dbms_utility.get_hash_value(to_char(l), 4 end; 5 / 827
0, 1 0 2 4 ) ) ;
PL/SQL procedure successfully completed.
Обратите внимание на пользовательскую блокировку UL со значением ID1 827. Оказывается, что 827 — хеш-значение для результата функции TO_CHAR(1), примененной к первичному ключу. Чтобы завершить этот пример, нужно разобраться, что произойдет, если приложение допускает изменение первичного ключа. В идеале первичный ключ лучше не изменять, но некоторые приложения это делают. Надо учитывать последствия того, если один сеанс изменит значение первичного ключа: tkyte@TKYTE816> update demo s e t x = 2 where x = 1; 1 row updated. а другой сеанс попытается вставить строку с измененным значением первичного ключа: tkyte@TKYTE816> INSERT INTO'DEMO VALUES (2); Второй сеанс опять окажется заблокированным. Проблема в том, что не каждый процесс, который может изменить первичный ключ, учитывает измененную схему блокирования. Для решения этой проблемы, связанной с изменением первичного ключа, необходимо изменить событие, вызывающее срабатывание триггера: before i n s e r t OR UPDATE OF X on demo Если созданный триггер срабатывает до вставки данных в столбец X или каких-либо изменений его значения, будет происходить именно то, что требуется (и изменение тоже станет неблокирующим).
Пакет DBMS_LOCK открывает приложениям доступ к внутреннему механизму блокирования сервера Oracle. Как было продемонстрировано, эту возможность можно использовать для реализации специфического метода блокирования, расширяющего стандартные возможности. Мы рассмотрели способы использования этого механизма для обеспечения очередности доступа к общему ресурсу (например, к файлу ОС) и для координации конфликтующих процессов. Мы углубленно изучили использование средств пакета DBMS_LOCK для предотвращения блокирующих вставок. Этот пример показал особенности использования пакета DBMS_LOCK, а также отображение информации о соответствующих блокировках в представлении VSLOCK. В завершение я обратил ваше внимание на важность обеспечения координации действий сеансов, связанных со специфическим методом блокирования, описав, как изменение значения первичного ключа может нарушить работу созданного неблокирующего алгоритма вставок.
Пакет DBMS LOGMNR
Пакеты LogMiner, DBMS_LOGMNR и DBMS_LOGMNR_D, позволяют анализировать файлы журнала повторного выполнения сервера Oracle. Этот анализ может потребоваться в таких случаях: •
Необходимо определить, когда и кем таблица была удалена.
•
Необходимо проверить, какие действия выполнялись с таблицей или набором таблиц, чтобы разобраться, кто и что изменял. Такую проверку можно выполнить "постфактум". Обычно достаточно выполнить команду AUDIT (но ее надо выполнять заранее), и вы узнаете, что кто-то изменил таблицу, а вот что именно было изменено узнать таким образом невозможно. Пакеты LogMiner позволяют определить постфактум лицо, внесшее изменения, и какие именно данные были изменены.
Q Необходимо "отменить" транзакцию. Для этого надо узнать, что было сделано в этой транзакции, и создать PL/SQL-код для отмены этих действий. •
Необходимо получить эмпирические значения количества строк, изменяемых типичной транзакцией.
Q Необходимо выполнить ретроспективный анализ использования базы данных за определенный период времени. Q Необходимо определить, почему сервер вдруг стал генерировать в журнал по 10 Мбайт данных в минуту. Можно ли найти очевидные причины при беглом просмотре журналов?
Пакет DBMS LOGMNR
а
615
Необходимо разобраться, что в действительности происходит "за кадром". Содержимое журналов повторного выполнения показывает, что именно происходит при выполнении вставки в таблицу с триггером, изменяющим другую таблицу. Все результаты транзакции записываются в журнал. Пакеты LogMiner — прекрасное средство изучения этих результатов.
Пакеты LogMiner предоставляют средства для решения всех этих и многих других задач. В этом разделе я представлю краткий обзор использования пакетов LogMiner, а затем опишу ряд проблем при его использовании, о которых не сказано в руководстве Supplied PL/SQL Packages Reference, поставляемом корпорацией Oracle. Как и в случае прочих пакетов, рекомендуется прочитать разделы руководства Supplied PL/SQL Packages Reference, посвященные пакетам DBMS_LOGMNR и DBMS_LOGMNR_D, чтобы получить общее представление о функциях и процедурах, которые они содержат, и о принципах использования этих пакетов. Далее в разделе "Опции и использование" представлен обзор соответствующих процедур и их входных данных. Пакеты LogMiner лучше всего работают с архивными файлами журнала повторного выполнения, хотя их можно использовать и для анализа неактивных оперативных файлов журнала повторного выполнения. Попытка анализа активного оперативного файла журнала повторного выполнения может привести к выдаче сообщения об ошибке или дать некорректные результаты, поскольку файл журнала повторного выполнения содержит данные старых и новых транзакций. Интересно, что с помощью LogMiner можно анализировать файл журнала, первоначально созданный в другой базе данных. Даже версии серверов при этом могут не совпадать (архивные файлы версии 8.0 можно анализировать на сервере версии 8.1). Можно перенести архивный файл журнала повторного выполнения в другую систему и анализировать его там. Это весьма удобно в случае аудита или ретроспективного анализа тенденций использования, не влияющего на работу системы. Для этого, однако, надо использовать сервер на той же аппаратной платформе (т.е. обеспечить тот же порядок байтов, размер слова и т.п.). Желательно обеспечить такой же размер блоков базы данных, как в исходной (размер блока в базе данных, где выполняется анализ, не должен быть меньше, чем в базе данных, где журнал повторного выполнения сгенерирован), и совпадение кодовых страниц. Процесс использования пакетов LogMiner состоит из двух этапов. На первом — создается словарь данных для работы пакетов LogMiner. Именно это и позволяет анализировать файл журнала повторного выполнения не в той базе данных, где он был сгенерирован (пакеты LogMiner не используют существующий словарь данных). Используется словарь данных, экспортированный во внешний файл с помощью пакета DBMS_LOGMNR_D. Пакеты LogMiner можно использовать и без этого словаря данных, но разобраться в полученных результатах при этом практически невозможно. Формат представления этих результатов мы рассмотрим несколько позже. На втором этапе импортируются файлы журнала повторного выполнения, и запускается LogMiner. После запуска основного пакета LogMiner можно просматривать содержимое файлов журнала повторного выполнения с помощью SQL-операторов. С пакетами LogMiner связано четыре представления VS. Основное представление — V$LOGMNR_CONTENTS. Именно оно будет использоваться для анализа содержимого загруженных файлов журнала повторного выполнения. Более детально это представ-
616
Приложение А
ление мы рассмотрим в примере, а в конце раздела представлена таблица с описанием его столбцов. Остальные три представления описаны ниже. • V$LOGMNR_DICTIONARY. Это представление содержит информацию о загруженном файле словаря. Этот словарь был создан на первом этапе. Чтобы разобраться в содержимом файла журнала повторного выполнения, необходим файл словаря, задающий имя объекта с данным идентификатором, имена и типы данных столбцов таблиц и т.п. Это представление содержит не больше одной строки, описывающей текущий загруженный словарь. Q V$LOGMNR_LOGS. Это представление содержит информацию о файлах журнала повторного выполнения, которые пользователь загрузил в систему с помощью LogMiner. Содержимое файлов журнала повторного выполнения можно найти в представлении V$LOGMNR_CONTENTS. Там же вы найдете характеристики самих файлов: имя файла журнала повторного выполнения, имя базы данных, в которой он сгенерирован, номера системных изменений (SCNs — system change numbers), содержащихся в нем, и т.д. В представлении содержится по одной строке для каждого анализируемого файла. • V$LOGMNR_PARAMETERS. Это представление содержит параметры, переданные LogMiner при запуске. После вызова подпрограммы запуска LogMiner в нем будет одна строка. Важно отметить, что, поскольку пакеты LogMiner выделяют память в области PGA, средства LogMiner нельзя использовать в среде MTS. Дело в том, что в среде MTS каждое обращение к базе данных будет обрабатываться другим разделяемым сервером (процессом или потоком). Данные, загруженные первым процессом (первым разделяемым сервером) не доступны для второго процесса (второго разделяемого сервера). Для работы пакетов LogMiner необходимо подключиться к выделенному серверу. Кроме того, результат доступен в одном сеансе и только в процессе его работы. Если необходим дальнейший анализ, надо либо загрузить информацию повторно, либо сохранить ее в постоянной таблице, например, с помощью оператора CREATE TABLE AS SELECT. При анализе больших объемов данных размещение их в обычной таблице с помощью операторов CREATE TABLE AS SELECT или INSERT INTO имеет еще больше смысла. В дальнейшем эту информацию можно проиндексировать, тогда как представление V$LOGMNR_CONTENTS всегда просматривается полностью, что требует очень много ресурсов.
Обзор После обзора средств LogMiner мы рассмотрим назначение входных параметров стандартных пакетов LogMiner. Затем я расскажу вам, как с помощью LogMiner определить, когда в базе данных произошло определенное действие. Далее будет рассмотрено влияние пакетов LogMiner на использование памяти сеансами, а также кеширование пакетами файлов журнала повторного выполнения. Наконец, я опишу ограничения, связанные с использованием пакетов LogMiner, не упоминающиеся в документации.
Пакет DBMSJ.OGMNR
617
Этап 1: создание словаря данных Чтобы средства LogMiner могли сопоставить внутренним идентификаторам объектов и столбцов соответствующие имена, необходим словарь данных. Имеющийся в базе данных словарь при этом не используется. Словарь данных должен загружаться из внешнего файла. Это необходимо для того, чтобы журналы повторного выполнения можно было анализировать в другой базе данных. Кроме того, текущий словарь данных в базе может поддерживать уже не все объекты, находившиеся в базе данных в момент генерации файла журнала повторного выполнения, вот почему словарь данных необходимо импортировать. Чтобы понять назначение файла словаря данных, давайте рассмотрим результата работы LogMiner, когда словарь данных не загружен. Для этого загрузим архивный файл журнала повторного выполнения и запустим LogMiner. Затем выполним запрос к представлению V$LOGMNR_CONTENTS, чтобы определить его содержимое: tkyte@TKYTE816> begin 2 sys.dbms_logmnr.add_logfile 3 ('C:\oracle\oradata\tkyte816\archive\TKYTE816T001S01263.ARC', 4 sys.dbms_logmnr.NEW); 5 end; 6 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys.dbms_logmnr.start_logmnr; 3 end; 4 / PL/SQL procedure successfully completed. tkyte@TKYTE816> column sql_redo format a30 tkyte@TKYTE816> column sql_undo format a30 tkyte@TKYTE816> select sen, sql_redo, sql_undo from v$logmnr_contents 2 / SCN SQL_REDO 6.4430E+12 6.4430E+12 set transaction read write; 6.4430E+12 update UNKNOWN.Objn:30551 set Col[2] = HEXTORAWC787878') wh ere ROWID = 'AAAHdXAAGAAAAJKAA A1;
SQLJJNDO
update UNKNOWN.Objn:30551 set Col[2] = HEXTORAW('534d495448' ) where ROWID = 'AAAHdXAAGAAAA JKAAA';
6.4430E+12 6.4430E+12 commit; tkyteSTKYTE816> select utl_raw.cast_to_varchar2(hextoraw('787878')) from dual; UTL_RAW.CAST_TO_VARCHAR2(HEXTORAW('787878')) XXX
618
Приложение А
tkyte@TKYTE816> s e l e c t
utl_raw.cast_to_varchar2(hextoraw('534d495448')) from dual;
UTL_RAW.CAST_TO_VARCHAR2(HEXTORAW('534D495448')) SMITH Читать этот результат практически невозможно. Мы знаем, что изменен столбец 2 объекта с идентификатором 30551. Более того, можно получить значение HEXTORAW('787878') в виде строки символов. Можно обратиться к словарю данных и определить, какой объект имеет идентификатор 30551: tkyte@TKYTE816> s e l e c t object_name 2 from all_objects 3 where data_object_id = 30551; OBJECT NAME EMP
но сделать это можно только в той же базе данных, в которой был сгенерирован журнал повторного выполнения, и только в том случае, если этот объект еще существует. Далее можно выполнить команду DESCRIBE EMP и определить, что столбец 2 — это ENAME. Поэтому в столбце SQL_REDO пакет LogMiner поместил значение UPDATE EMP SET ENAME = 'XXX' WHERE ROWID = .... К счастью, подобные запутанные преобразования не придется выполнять при каждом анализе журнала. Мы убедимся, что, создав и загрузив словарь, можно получить намного более понятные результаты. Следующий пример показывает, каких результатов можно ожидать, создав файл словаря для пакетов LogMiner, а затем загрузив его. Начнем с создания файла словаря. Создать его весьма просто. Для этого должны быть выполнены следующие требования. Q Конфигурация параметров инициализации позволяет создавать с помощью пакета UTL_FILE файлы хотя бы в одном каталоге. Подробнее соответствующая настройка описана в разделе, посвященном пакету UTL_FILE. Пакет DBMS_LOGMNR_D, с помощью которого создается файл словаря данных, для выполнения ввода-вывода использует средства пакета UTL_FILE. Q Схема, в которой будет вызываться пакет DBMS_LOGMNR_D, имеет привилегию EXECUTE ON SYS.DBMS_LOGMNR_D, или ей предоставлена роль с привилегий выполнения этого пакета. По умолчанию роль EXECUTE_CATALOG_ROLE имеет привилегию для выполнения этого пакета. После настройки пакета UTL_FILE и получения привилегии EXECUTE ON DBMS_LOGMNR_D создание файла словаря представляет собой тривиальную задачу. Надо вызвать всего одну подпрограмму пакета DBMS_LOGMNR_D — процедуру BUILD. Достаточно выполнить примерно следующее: tkyte@TKYTE816> s e t serveroutput on tkyte@TKYTE816> begin 1 2 sys.dbms_logmnr_d.build('miner_dictionary.dat , 3 'c:\temp');
Пакет DBMSJ.OGMNR
6 1 9
4 end; 5 / LogMnr Dictionary Procedure started LogMnr Dictionary File Opened TABLE: OBJ$ recorded in LogMnr Dictionary File TABLE: TAB$ recorded in LogMnr Dictionary File TABLE: COL$ recorded in LogMnr Dictionary File TABLE: SEG$ recorded in LogMnr Dictionary File TABLE: UNDO$ recorded in LogMnr Dictionary File TABLE: UGROUP$ recorded in LogMnr Dictionary File TABLE: TS$ recorded in LogMnr Dictionary File TABLE: CLU$ recorded in LogMnr Dictionary File TABLE: IND$ recorded in LogMnr Dictionary File TABLE: ICOL$ recorded in LogMnr Dictionary File TABLE: LOB$ recorded in LogMnr Dictionary File TABLE: USER$ recorded in LogMnr Dictionary File TABLE: FILE$ recorded in LogMnr Dictionary File TABLE: PARTOBJ$ recorded in LogMnr Dictionary File TABLE: PARTCOL$ recorded in LogMnr Dictionary File TABLE: TABPART$ recorded in LogMnr Dictionary File TABLE: INDPART$ recorded in LogMnr Dictionary File TABLE: SUBPARTCOL$ recorded in LogMnr Dictionary File TABLE: TABSUBPART$ recorded in LogMnr Dictionary File TABLE: INDSUBPART$ recorded in LogMnr Dictionary File TABLE: TABCOMPART$ recorded in LogMnr Dictionary File TABLE: INDCOMPART$ recorded in LogMnr Dictionary File Procedure executed successfully — LogMnr Dictionary Created PL/SQL procedure successfully completed. Перед вызовом процедуры BUILD пакета D B M S _ L O G M N R _ D рекомендуется выполнять команду SET SERVEROUTPUT O N — это обеспечит выдачу информационных сообщений пакета D B M S _ L O G M N R _ D . Они помогут выяснить причины ошибки при выполнении DBMS_LOGMNR_D.BUILD. Выполненная выше команда создала файл C:\TEMP\MINER_DICTIONARY.DAT. Это обычный текстовый файл, который можно просматривать в текстовом редакторе. Файл содержит SQL-подобные операторы, которые анализируются и выполняются процедурой запуска основного пакета LogMiner. Теперь, при наличии файла словаря, можно посмотреть, какая информация содержится в представлении V$LOGMNR_CONTENTS: tkyte@TKYTE816> begin 2 sys.dbms_logmnr.add_logfile 3 ('C:\oracle\oradata\tkyte816\archive\TKYTE816T001S01263.ARC, 4 sys.dbms_logmnr.NEW); 5 end; 6 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys.dbms_logmnr.start_logmnr 3 (dictFileName => 'c:\temp\miner_dictionary.dat'); 4 end;
620 5
Приложение А /
PL/SQL procedure successfully completed. tkyte@TKYTE816> column sql_redo format a30 tkyte@TKYTE816> column sql_undo format a30 tkyte@TKYTE816> select sen, sql_redo, sql_undo from v$logmnr_contents 2 / SCN SQL_REDO
SQLJJNDO
6.4430E+12 6.4430E+12 set transaction read write; 6.4430E+12 update TKYTE.EMP set ENAME = ' update TKYTE.EMP set ENAME = ' xxx' where ROWID = 'AAAHdXAAGA SMITH' where ROWID = 'AAAHdXAA AAAJKAAA'; GAAAAJKAAA'; 6.4430E+12 6.4430E+12 commit; Теперь все гораздо понятнее: можно прочитать SQL-операторы, сгенерированные пакетом LogMiner, и повторяющие (или отменяющие) изучаемую транзакцию. Теперь можно переходить ко второму этапу — использованию средств LogMiner.
Этап 2: использование средств LogMiner Используем только что сгенерированный файл словаря для анализа содержимого архивных файлов журнала повторного выполнения. Перед загрузкой журнала повторного выполнения сгенерируем такой файл, все транзакции в котором будут известны. Это упростит интерпретацию результатов в первый раз. Мы сможем сопоставлять содержимое представления V$LOGMNR_CONTENTS с только что выполненными транзакциями. Для этого важно организовать тестовую базу данных, с которой будет работать только один пользователь. Это позволит искусственно ограничить содержимое журнала повторного выполнения. Для этой базы данных также понадобится привилегия ALTER SYSTEM, чтобы можно было принудительно вызвать архивирование файла журнала. Наконец, все гораздо проще, если база данных работает в режиме автоматического архивирования журналов. При этом очень легко найти соответствующий файл журнала повторного выполнения (это будет только что заархивированный файл — ниже я покажу, как его найти). При использовании базы данных в режиме NOARCHFVELOGMODE, вам придется найти активный журнал и определить, какой файл журнала был активным непосредственно перед ним. Итак, чтобы сгенерировать транзакцию-образец, выполним: tkyte@TKYTE816> a l t e r system archive log c u r r e n t ; System a l t e r e d . tkyte@TKYTE816> update emp set ename = lower(ename); 14 rows updated. tkyte@TKYTE816> update dept s e t dname = lower(dname); 4 rows updated.
Пакет DBMSJ.OGMNR 6 2 1 tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> alter system archive log current; System altered. tkyte@TKYTE816> column name format a80 tkyte@TKYTE816> select name 2 from v$archived_log 3 where completion_time = (select max(completion_time) 4 from v$archived_log) 5 / NAME С:\ORACLE\ORADATA\TKYTE816\ARCHIVE\TKYTE816T001S01267.ARC Теперь, с учетом того, что я — единственный пользователь, изменяющий базу данных, в сгенерированном архивном журнале окажутся только два выполненных изменения. Последний запрос к представлению V$ARCHFVED_LOG возвращает имя архивного файла журнала повторного выполнения, который необходимо анализировать. Его можно загрузить в LogMiner и анализировать с помощью приведенных ниже PL/SQL-блоков. Они добавят последний архивный файл журнала повторного выполнения к списку обрабатываемых, а затем запустят LogMiner: tkyte@TKYTE816> declare 2 l_name v$archived_log.name%type; 3 begin 4 5 select name into l_name 6 from v$archived_log 7 where completion_time = (select max(completion_time) 8 from v$archived_log); 9 10 sys.dbms_logmnr.add_logfile(l_name, sys.dbms_logmnr.NEW); 11 end; 12 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys.dbms_logmnr.start_logmnr 3 (dictFileName => 'c:\temp\miner_dictionary.dat'); 4 end; 5 / PL/SQL procedure successfully completed. Первый вызов процедуры, DBMS_LOGMNR.ADD_LOGFILE, загрузил архивный файл журнала повторного выполнения в LogMiner. Я передал имя архивного файла журнала повторного выполнения и опцию DBMS_LOGMNR.NEW. Поскольку этот файл добавляется первым, надо указывать опцию DBMS_LOGMNR.NEW. Поддерживаются также опции ADDFILE для добавления еще одного файла журнала в существующий
622
Приложение А
список файлов, а также REMOVEFILE — для удаления файла из списка рассматриваемых. После загрузки нужных файлов журнала можно вызывать процедуру DBMS_LOGMNR.START_LOGMNR, передавая ей имя созданного файла словаря. При вызове START_LOGMNR передается минимум информации — только имя файла словаря. Другие опции процедуры START_LOGMNR мы рассмотрим в разделе "Опции и использование". Теперь, после загрузки файла журнала и запуска LogMiner, все готово для просмотра содержимого представления V$LOGMNR_CONTENTS. Представление V$LOGMNR_CONTENTS содержит большое количество информации; для начала рассмотрим только небольшую ее часть. В частности, выберем значения столбцов SCN, SQL_REDO и SQL_UNDO. Если вы этого еще не знаете, SCN (порядковый номер системного изменения) — это простой механизм отсчета времени, который сервер Oracle использует для упорядочения транзакций и восстановления базы данных после сбоев. Эти номера также используются для обеспечения согласованности по чтению и при обработке контрольных точек в базе данных. SCN можно рассматривать как счетчик: при фиксации каждой транзакции значение SCN увеличивается на единицу. Вот запрос из предыдущего примера, в котором имена в таблицах Е М Р и DEPT переводятся в нижний регистр: tkyte@TKYTE816> column sql_redo format a20 word_wrapped tkyte@TKYTE816> column sql_undo format a20 word_wrapped tkyte@TKYTE816> s e l e c t sen, sql_redo, sql_undo from v$logmnr_contents 2 / SCN SQL_REDO
SQLJJNDO
6.4430E+12 set transaction read write; 6.4430E+12 update TKYTE.EMP set ENAME = 'smith' where ROWID = 'AAAHdYAAGAAAAJKAAA' ; 6.4430E+12 6.4430E+12 update TKYTE.EMP set ENAME = 'alien' where ROWID = 1 AAAHdYAAGAAAAJKAAB'
update TKYTE.EMP set ENAME = 'SMITH' where ROWID = 'AAAHdYAAGAAAAJKAAA'
update TKYTE.EMP set ENAME = 'ALLEN' where ROWID = 'AAAHdYAAGAAAAJKAAB'
...(несколько аналогичных строк выброшено)... 6.4430Е+12 update TKYTE.DEPT set DNAME = 'sales' where ROWID = 'AAAHdZAAGAAAAKKAAC'
update TKYTE.DEPT set DNAME = 'SALES' where ROWID = 'AAAHdZAAGAAAAKKAAC'
6.4430E+12 update TKYTE.DEPT set DNAME =
update TKYTE.DEPT set DNAME =
Пакет DBMS LOGMNR
623
1 operations' where 'OPERATIONS' where ROWID = ROWID = 'AAAHdZAAGAAAAKKAAD' 'AAAHdZAAGAAAAKKAAD' f
r
6.4430E+12 commit; 22 rows selected. Как видите, SQL-операторы сгенерировали в журнале повторного выполнения не две строки, а намного больше. Журнал повторного выполнения содержит измененные биты и байты, а не SQL-операторы. Поэтому многострочный оператор UPDATE EMP SET ENAME = LOWER(ENAME) представляется средствами LogMiner в виде набора однострочных изменений. В настоящее время LogMiner не позволяет получить реально выполненные SQL-операторы. Можно создать только делающие то же самое SQL-операторы, но в виде набора отдельных операторов. В этом примере мы пойдем на шаг дальше. В представлении V$LOGMNR_CONTENTS есть столбцы-заместители ("placeholder" columns). Столбцы-заместители позволяют найти изменения, выполненные в пяти (но не более) столбцах таблицы. Столбцы-заместители позволяют узнать имя измененного столбца, а также значение столбца до и после изменения. Поскольку эти столбцы выделены из текста SQL-операторов, очень легко найти транзакцию, изменившую, скажем, значение в столбце ENAME (в столбце-заместителе имени будет значение ENAME) с KING (в столбце предварительного значения будет KING) на king. Чтобы продемонстрировать это, выполним еще один небольшой пример с оператором UPDATE и создадим файл сопоставления столбцов (column mapping file). Файл сопоставления столбцов (будем сокращенно называть его файл colmap) позволяет указать средствам LogMiner, какие столбцы в таблице вас интересуют. Можно сопоставить до пяти столбцов в каждой таблице со столбцами-заместителями. Файл colmap имеет следующий формат: c o l m a p = TKYTE DEPT ( I , DEPTNO, 2 , DNAME, 3 , LOC); c o l m a p = TKYTE EMP ( 1 , EMPNO, 2 , ENAME, 3 , JOB, 4, MGR, 5, HIREDATE);
При просмотре строки в таблице DEPT столбцу DEPT DEPTNO будет сопоставлен первый столбец-заместитель. А при просмотре строки в таблице ЕМР этому столбцузаместителю будет сопоставлен столбец EMP EMPNO. Файл сопоставления столбцов в общем случае состоит из строк следующей структуры (полужирным выделены константы, <sp> представляет обязательный пробел) СО1шар<зр>-<зр>ВЛАДЕЛЕЦ<зр>ИМЯ_ТАБЛИЦЪК8р> (1 > <8р>ИМЯ_СТ0ЛБЦА [, <зр>2 , <зр> ^ИМЯ_СТОЛБЦА]...); Регистр символов везде имеет значение: ВЛАДЕЛЬЦА надо задавать в верхнем регистре, имя таблицы — с соблюдением регистра (обычно — в верхнем регистре, если только при создании объекта не использовались идентификаторы в кавычках). Пробелы указывать тоже обязательно. Для того чтобы упростить использование файла сопоставления столбцов я использую следующий сценарий: set linesize 500 set trimspool on set feedback off
624
Приложение А
set heading off set embedded on spool logmnr.opt select 'colmap = ' I I user | | ' max(decode(column_id, 1, max(decode(column_id, 1, max(decode(column_id, 2, max(decode(column_id, 2, max(decode(column_id, 3, max(decode(column_id, 3, max(decode(column_id, 4, max(decode(column_id, 4, max(decode(column_id, 5, max(decode(column_id, 5, from user_tab_columns group by user, table_name
I | table_name I I ' (' I column_id , null)) ||column_name, null)) ||column_id , null)) ||column_name, null)) '||column_id , null)) 1 I|column_name, null)) '||column_id , null)) ' I |column_naine, null)) 'I Icolumn_id , null)) 'I|column name, null)) I ' ) ; ' colmap
spool off в SQL*Plus для автоматической генерации файла logmnr.opt. Например, если выполнить этот сценарий в схеме, где имеются только таблицы ЕМР и DEPT, аналогичные тем, что принадлежат пользователю SCOTT/TIGER, вы получите: tkyte@TKYTE816> @colmap c o l m a p = TKYTE DEPT ( 1 , DEPTNO, 2 , DNAME, 3 , L O C ) ; c o l m a p = TKYTE EMP ( 1 , EMPNO, 2 , ENAME, 3 , JOB, 4 , MGR, 5 , HIREDATE);
Я всегда сопоставляю первых пять столбцов таблицы. Если вы хотите использовать другой набор пяти столбцов, отредактируйте полученный при выполнении этого сценария файл logmnr.opt, изменив имена столбцов. Например, в таблице ЕМР есть еще три столбца, не представленные в полученном файле colmap — SAL, COMM и DEPTNO. Если необходимо просматривать изменения столбца SAL, а не JOB, файл colmap должен выглядеть так: tkyte@TKYTE816> G c o l m a p c o l m a p = TKYTE DEPT ( 1 , DEPTNO, 2 , DNAME, 3 , LOC); c o l m a p = TKYTE EMP ( 1 , EMPNO, 2 , ENAME, 3 , SAL, 4 , MGR, 5 , HIREDATE);
Помимо использования соответствующего регистра символов и количества пробелов при работе с файлом colmap важно также следующее: •
Файл должен называться logmnr.opt. Другое имя использовать нельзя.
•
Этот файл должен быть в том же каталоге, что и файл словаря.
•
Файл colmap можно использовать только вместе с файлом словаря.
Итак, мы сейчас изменим все столбцы в таблице DEPT. Я использую четыре различных оператора UPDATE, каждый из которых будет изменять другую строку и набор столбцов. Это позволит увидеть результат сопоставления столбцов-заместителей: tkyte@TKYTE816> a l t e r system archive log current; tkyte@TKYTE816> update dept s e t deptno = 11
Пакет DBMSJ.OGMNR
625
2 where deptno = 4 0 •э / tkyte@TKYTE816> u p d a t e d e p t s e t dname = i n i t c a p ( d n a m e ) 2 where deptno = 1 0 3 / tkyte@TKYTE816> update dept set loc = initcap(loc) 2 where deptno = 20 3 / tkyte@TKYTE816> update dept set dname = initcap(dname), 2 loc = initcap(loc) 3 where deptno = 3 0 4 / tkyte@TKYTE816> commit; tkyte@TKYTE816> a l t e r system archive log
current;
Теперь можно найти изменения в каждом из столбцов, загрузив вновь сгенерированный архивный файл журнала повторного выполнения и запустив LogMiner с опцией USE_COLMAP. Обратите внимание, что я сгенерировал файл logmnr.opt с помощью представленного ранее сценария и поместил этот файл в тот же каталог, где находится словарь данных: tkyte@TKYTE816> 2 l_name 3 begin 4 5 select 6 from 7 where 8
declare v$archived_log.name%type; name into l_name v$archived_log completion_time = (select max(completion_time) from v$archived_log);
10 sys.dbms_logmnr.add_logfile(l_name, sys.dbms_logmnr.NEW); 11 end; 12 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys. dbms_loginnr. start_logmnr 3 (dictFileName => 'c:\temp\miner_dictionary.dat', 4 options => sys.dbms_logmnr.USE_COLMAP); 5 end; 6 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select sen, phi name, phi undo, phi redo, 2 ph2_name, ph2_undo, ph2_redo, 3 ph3_name, ph3_undo, ph3_redo 4 from v$logmnr_contents 5 where seg_name = 'DEPT' 6 /
626
Приложение А SCN PH1_NA PHI PHI PH2_N PH2_UNDO
6.4430E+12 DEPTNO 40 6.4430E+12 6.4430E+12 6.4430E+12
PH2_REDO
РНЗ PH3_UNDO PH3_REDO
11 DNAME a c c o u n t i n g
Accounting
DNAME s a l e s
Sales
LOC DALLAS LOC CHICAGO
Dallas Chicago
Итак, этот результат ясно показывает (в первой строке, например), что в столбце DEPTNO значение 40 (РН1) было изменено на 11. Это понятно, поскольку мы выполняли SET DEPTNO = 11 WHERE DEPTNO = 40. Обратите внимание, что остальные столбцы в перовой строке — пустые. Причина в том, что сервер Oracle записывает только измененные байты; для этой строки нет предварительного и окончательного образов столбцов DNAME и LOC. Вторая строка показывает изменение значения в столбце DNAME с accounting на Accounting и отсутствие изменений в столбцах DEPTNO или LOC, поскольку они не были затронуты. Последняя строка показывает, что при изменении двух столбцов одним оператором UPDATE они будут представлены в столбцахзаместителях. Как видите, использование столбцов-заместителей может пригодиться при поиске конкретной транзакции в большом объеме данных повторного выполнения. Если известно, что транзакция изменила таблицу X, поменяв в столбце Y значение с а на Ь, найти ее очень легко.
Опции и использование Функциональные возможности LogMiner реализуются двумя пакетами — DBMS_LOGMNR и DBMS_LOGMNR_D. Пакет DBMS_LOGMNR_D (_D в названии обозначает "словарь" — "dictionary") содержит всего одну процедуру, BUILD. Она применяется для создания словаря данных, используемого пакетом DBMS_LOGMNR при загрузке файла журнала повторного выполнения. Он позволяет сопоставить идентификаторам объектов имена таблиц, определить имена и типы данных столбцов по порядковому номеру и т.д. Использовать процедуру DBMS_LOGMNR_D.BUILD очень просто. Она имеет два параметра: Q DICTIONARY_FILENAME. Имя файла словаря, который необходимо создать. В наших примерах использовался файл miner_dictionary.dat. Q DICTIONARYLOCATION. Каталог, в котором этот файл будет создан. Процедура использует для создания файла средства пакета UTL_FILE, так что каталог должен быть перечислен среди допустимых каталогов, задаваемых параметром инициализации utl_file_dir. Подробнее о конфигурировании пакета UTL_FILE см. в соответствующем разделе в конце приложения А. Вот и все, что нужно знать о процедуре BUILD. Оба параметра указывать обязательно. Если при вызове этой процедуры получено сообщение об ошибке, подобное следующему: tkyte@TKYTE8I6> exec sys.dbms_logmnr_d.build('x.dat', 'с:\not_valid\'); BEGIN sys.dbms_logmnr_d.build('x.dat', ' с : \ n o t _ v a l i d \ ' ) ; END;
Пакет DBMSJ.OGMNR
6 2 7
ERROR a t l i n e 1 : ORA-01309: specified dictionary f i l e cannot be opened ORA-06510: PL/SQL: unhandled user-defined exception ORA-06512: a t "SYS.DBMS_LOGMNR_D", l i n e 793 ORA-06512: a t l i n e 1 значит, указанный каталог не указан в параметре инициализации utl_file_dir. Пакет DBMS_LOGMNR состоит из трех процедур. Q ADD_LOGFILE. Зарегистрировать набор файлов журнала для анализа. •
START_LOGMNR. Заполнить данными представление V$LOGMNR_CONTENTS.
•
END_LOGMNR. Освободить все ресурсы, выделенные при работе LogMiner. Эта процедура вызывается для корректного освобождения ресурсов перед завершением сеанса или при окончании работы с пакетами LogMiner.
Процедура ADD_LOGFILE, как было сказано ранее, вызывается еще до запуска LogMiner. Она создает список файлов журнала, которые будут обрабатываться при выполнении процедуры START_LOGMNR для заполнения представления V$LOGMNR_CONTENTS. Процедура ADD_LOGFILE принимает следующие параметры' •
LOGFILENAME. Полное имя файла архивного журнала повторного выполнения, который необходимо проанализировать.
•
OPTIONS. Задает, добавлять указанный файл или удалять. В качестве значения задаются следующие константы пакета DBMS_LOGMNR: •
DBMS_LOGMNR.NEW. Начать новый список. Если список уже существует, он очищается.
Q DBMS_LOGMNR.ADD. Добавить файл в уже существующий или пустой список. •
DBMS LOGMNR.REMOVEFILE. Удалить файл из списка.
Если необходимо проанализировать последние два архивных файла журнала повторного выполнения, процедура ADDJLOGFILE вызывается дважды. Например: tkyte@TKYTE816> declare 2 l_cnt number default 0; 3 begin 4 for x in (select name 5 from v?archived_log 6 order by completion_time desc) 7 loop 8 l_cnt := l_cnt+l; 9 exit when (1 cnt > 2); 10 11 sys.dbms_logmnr.add_logfile(x.name); 12 end loop; 13 14 sys. dbms_logmnr. start_logmnr
628
Приложение А
15 (dictFileName => 'c:\temp\miner_dictionary.dat', 16 options => sys.dbms_logmnr.USE_COLMAP); 17 end; 18 / PL/SQL procedure successfully completed. В одном сеансе после запуска LogMiner можно вызывать процедуру ADD_LOGFILE для добавления дополнительных файлов журнала, удаления тех из них, которые больше не представляют интереса, или (если указана опция DBMS_LOGMNR.NEW) для сброса списка файлов журнала так, чтобы он включал только один указанный новый файл. При вызове DBMS_LOGMNR.START_LOGMNR после изменения списка файлов содержимое представления V$LOGMNR_CONTENTS, по сути, сбрасывается и создается заново на основе информации в журнальных файлах, входящих в список. Процедура DBMS_LOGMNR.START_LOGMNR принимает много параметров. В рассмотренных ранее примерах мы использовали только два из шести имеющихся. Мы задавали имя файла словаря и опции (чтобы указать, что необходимо использовать файл colmap). В общем случае поддерживаются следующие параметры: Q STARTSCN и ENDSCN. Если точно известен диапазон интересующих номеров системных изменений, можно поместить в представление V$LOGMNR_CONTENTS только соответствующие строки. Это пригодится после загрузки всего файла журнала и определения максимального и минимального номера системных изменений, которые представляют интерес. Можно перезапустить LogMiner, указав этот диапазон, чтобы уменьшить объем данных в представлении V$LOGMNR_CONTENTS. По умолчанию эти параметры имеют значение 0 и не используются. Q STARTTIME и ENDTIME. Вместо указания диапазона SCN можно задать отрезок времени. Только записи журнала, попадающие в указанный отрезок времени, окажутся в представлении V$LOGMNR_CONTENTS. Эти значения игнорируются, если указаны значения STARTSCN и ENDSCN. По умолчанию используется отрезок времени с 1 января 1988 года по 1 января 2988 года. •
DICTFILENAME. Полное имя файла словаря, созданного процедурой DBMS_LOGMNR_D.BUILD.
a
OPTIONS. В настоящее время поддерживается только одна опция процедуры DBMS_LOGMNR.START_LOGMNR - опция DBMS_LOGMNR.USE_COLMAP Она задает поиск файла logmnr.opt в том же каталоге, что и файл DICTFILENAME. Важно помнить, что файл colmap может иметь только имя logmnr.opt и должен находиться в том же каталоге, что и файл словаря.
Последняя процедура в пакете DBMSJLOGMNR - DBMS_LOGMNR.END_LOGMNR. Она завершает сеанс LogMiner и очищает представление V$LOGMNR_CONTENTS. После вызова DBMS_LOGMNR.END_LOGMNR любые попытки обратиться к этому представлению дадут следующий результат: tkyte@TKYTE816> exec dbms_logmnr.end logmnr; PL/SQL procedure successfully completed. tkyte@TKYTE816> s e l e c t count(*) from v$logmnr_contents;
Пакет DBMSJ.OGMNR
629
s e l e c t count(*) from v$logmnr__contents ERROR a t l i n e 1: ORA-01306: dbms_logmnr.start_logmnr() must be invoked before selecting from v$logmnr_contents
Определение с помощью LogMiner, когда... Это наиболее типичный вариант использования пакетов LogMiner. Кто-то удалил таблицу. Надо восстановить ее или выяснить, кто это сделал. Другой пример: изменены данные в важной таблице, виновник не признается. Это произошло при отключенном аудите, но база данных работала в режиме архивирования журналов, и все резервные копии доступны. Хотелось бы восстановить данные из резервной копии до момента непосредственно перед определенным изменением (например, перед выполнением DROP TABLE). Можно восстановить таблицу, прекратив восстановление в нужный момент (чтобы таблица не была удалена снова), экспортировать эту таблицу в восстановленной базе данных, а затем импортировать в текущей. Это позволит восстановить таблицу и оставить в силе остальные изменения. Для этого необходимо знать либо точное время, либо значение SCN для оператора DROP TABLE. Поскольку часы у всех обычно показывают время неточно, а пользователи паникуют, они могут предоставить неверную информацию. Можно загрузить архивные файлы журнала за тот период, когда был выполнен оператор DROP TABLE, и найти точное значение SCN, до которого должно выполняться восстановление. Рассмотрим еще один небольшой пример, показывающий, какие операторы может выдать LogMiner при удалении таблицы. Я использую локально управляемые табличные пространства, так что, если вы используете табличные пространства, управляемые по словарю, у вас может получиться больше SQL-операторов, чем показано далее. Появление дополнительных SQL-операторов в случае управляемых по словарю табличных пространств связано с возвратом экстентов системе и освобождению выделенного таблице пространства. Итак, переходим к удалению таблицы: tkyte@TKYTE816> a l t e r system archive log c u r r e n t ; System a l t e r e d . tkyte@TKYTE816> drop t a b l e dept; Table dropped. tkyte@TKYTE816> a l t e r system archive log current; System a l t e r e d . Теперь необходимо найти значение в столбце SQL_REDO, представляющее удаление таблицы (оператор DROP TABLE). Если помните, LogMiner выдает неточный список выполненных SQL-операторов. Выдаются эквивалентные по действию SQL-операторы. Оператор DROP TABLE в результатах работы LogMiner отсутствует: мы увидим только изменения в словаре данных. В частности, нас интересует оператор DELETE, примененный к таблице SYS.OBJS — базовой для всех объектов. При удалении таблицы необходимо удалить соответствующую строку из таблицы SYS.OBJS. К счастью, при
632
Приложение А
мяти. Он подчитывает данные с диска по мере надобности. В оперативной памяти кешируется только часть информации. Если выполнить запрос к представлению V$LOGMNR_CONTENTS и после этого определить объем используемой памяти в области PGA, мы увидим, что по мере обращения к данным объем используемой памяти растет: tkyte@TKYTE816> create t a b l e tmp_logmnr_contents unrecoverable 2 as 3 s e l e c t * from v$logmnr_contents 4 / Table created. tkyte@TKYTE816> select a.name, b.value 2 from v$statname a, v$mystat b 3 where a.statistic! = b.statistic! 4 and lower(a.name) like '%pga%' с
/
NAME session pga memory session pga memory max
VALUE 19965696 19965696
Как видите, теперь сеансу надо почти 20 Мбайт памяти в области PGA.
Ограничения пакетов LogMiner Пакеты LogMiner имеют ряд ограничений, о которых вы должны знать. Ограничения связаны с использованием объектных типов Oracle и переносом строк.
Объектные типы Oracle Объектные типы лишь частично поддерживаются средствами LogMiner. Пакеты LogMiner не могут восстановить SQL-операторы, обычно используемые для доступа к данным объектных типов, и поддерживают не все объектные типы. Ограничения в этой области лучше всего продемонстрировать на примере. Начнем с небольшой схемы, в которой есть данные таких популярных объектных типов, как VARRAY и вложенные таблицы: tkyte@TKYTE816> create or replace type myScalarType 2 as object 3 (x i n t , у date, z varchar2(25)); 4 / Type created. tkyte@TKYTE816> create or replace type myArrayType 2 as varray(25) of myScalarType 3 / Type created. tkyte@TKYTE816> create or replace type туТаЫеТуре 2 as table of myScalarType
Пакет DBMSJ.OGMNR 3
633
/
Type created. tkyte@TKYTE816> drop table t; Table dropped. tkyte@TKYTE816> create table t (a int, b myArrayType, с myTableType) 2 nested table с store as c_tbl
я /
Table created. tkyte@TKYTE816> begin 2 sys.dbms_logmnr_d.build('miner_dictionary.dat', 3 'с:\temp'); 4 end; / PL/SQL procedure successfully completed. tkyte@TKYTE816> alter system switch logfile; System altered. tkyte@TKYTE816> insert into t values (1, 2 myArrayType(myScalarType(2, sysdate, 'hello')), 3 myTableType(myScalarType(3, sysdate+1, 'GoodBye')) 4 ); 1 row created. tkyte@TKYTE816> alter system switch logfile; System altered. Итак, в представленном выше примере мы создали ряд объектных типов, добавили таблицу, использующую эти типы, снова экспортировали словарь данных, а затем выполнили один оператор DML для этой таблицы. Теперь посмотрим, что скажут о выполненных действиях средства LogMiner: tkyte@TKYTE816> begin 2 sys.dbms_logmnr.add_logfile('C:\oracle\rdbms\ARC00028.001 1 , 3 dbms_logmnr.NEW); 4 end; PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys.dbms_logmnr.start_logmnr 3 (dictFileName => 'c:\temp\miner_dictionary.dat'); 4 end; PL/SQL procedure successfully completed. tkyte@TKYTE816> select sen, sql_redo, sql_undo 2 from v$logmnr_contents 3 /
634
Приложение А SCN SQL REDO
SQL UNDO
824288 824288 824288 824288 set transaction read write; 824288 insert into TKYTE.C_TBL(NESTED_T ABLE_ID,X,Y,Z) values (HEXTORAW('252cb5fad 8784e2ca93eb432c2d35 b7c'),3,TO_DATE('23JAN-2001 16:21:44', 'DD-MON-YYYY HH24:MI:SS'),'GoodBy e');
delete from TKYTE.CJTBL where NESTED_TABLE_ID = HEXTORAW('252cb5fad8 784e2ca93eb432c2d35b 7c') and X = 3 and Y TO_DATE('23-JAN-2001 16:21:44', 'DD-MON-YYYY HH24:MI:SS') and Z = 'GoodBye' and ROWID 'AAAFaqAADAAAAGzAAA'
824288 824288 824288 824288 insert into TKYTE.T(A,B,SYS_NCOO 00300004$) values (1,Unsupported Type,HEXTORAW('252cb 5fad8784e2ca93eb432c 2d35b7c'));
delete from TKYTE.T where A = 1 and В = Unsupported Type and SYS_NC0000300004$ = HEXTORAW('252cb5fad8 784e2ca93eb432c2d35b 7c') and ROWID = 1 AAAFapAADAAAARj AAA'
824288 10 rows selected. Как видите, один исходный оператор INSERT: tkyte@TKYTE816> i n s e r t i n t o t values ( 1 , 2 myArrayType(myScalarType(2, sysdate, ' h e l l o ' ) ) , 3 myTableType(myScalarType(3, sysdate+1, 'GoodBye')) 4 1 row created. был преобразован в два оператора INSERT: один — для подчиненной (вложенной) таблицы, другой — для главной таблицы Т. LogMiner не воспроизвел один оператор INSERT — он выдал эквивалентный набор SQL-операторов. Посмотрев на результат внимательнее, можно обнаружить в тексте оператора INSERT INTO T конструкцию Unsupported Type вместо одного из значений столбцов. Возвращаясь к исходному оператору INSERT,
Пакет DBMSJ.OGMNR
6 3 5
можно выяснить, что не поддерживается столбец типа VARRAY. Средства LogMiner не позволяют воспроизвести эту конструкцию. Это не делает пакеты LogMiner полностью бесполезными при работе с объектами. Просто результат нельзя использовать для отмены или повторного выполнения транзакций, поскольку соответствующие SQL-операторы воспроизводятся не полностью. Однако можно использовать результат для анализа тенденций, аудита и т.п. Более интересно, пожалуй, то, что пакеты позволяют увидеть, как сервер Oracle внутренне осуществляет поддержку объектных типов. Например, рассмотрим вставку строки в таблицуТ: i n s e r t i n t o t k y t e . t (a, b , SYS_NC0000300004$) values . . . Вполне понятно, что такое А и В. Это столбцы типа INT и МуАггауТуре (VARRAY). Однако куда делся столбец С и что за столбец SYS_NC0000300004$? Столбец С — это вложенная таблица, а вложенные таблицы хранятся в отдельной, подчиненной таблице. Столбец С не хранится в таблице Т; он хранится в отдельной таблице. Столбец SYS_NC0000300004$ — суррогатный первичный ключ для таблицы Т, использующийся как внешний ключ во вложенной таблице C_TBL. Если рассмотреть оператор INSERT для вставки данных во вложенную таблицу: i n s e r t i n t o tkyte.c_tbl(nested_table_id,
x, у, z) values . . .
можно увидеть, что во вложенную таблицу добавлен столбец NESTED_TABLE_ID, использующийся для соединения со столбцом T.SYS_NC0000300004$. Изучив значение, вставленное в оба эти столбца: HEXTORAW('252cb5fad8784e2ca93eb432c2d35b7c') можно выяснить, что сервер Oracle по умолчанию использует для соединения таблиц C_TBL и Т сгенерированное системой 16-байтовое значение типа RAW. Поэтому анализ действий с помощью LogMiner позволяет понять, как реализованы возможности сервера Oracle. В данном случае мы узнали, что тип вложенной таблицы реализуется как пара таблиц главная/подчиненная с суррогатным ключом в главной таблице и внешним ключом в подчиненной.
Перемещенные или расщепленные строки Средства LogMiner в настоящий момент не позволяют работать с перемещенными строками (migrated row) или расщепленными строками (chained row). Расщепленной называют строку, расположенную в нескольких блоках. Перемещенной называют строку, вставленную в один блок, а затем в результате изменения выросшую настолько, что она уже не вмещается в исходном блоке и поэтому перемещена в другой блок. Идентификатор у перемещенной строки остается прежним, а в блоке, куда она первоначально вставлялась, остается указатель на новое местонахождение строки. Перемещенные строки являются специальным случаем расщепленных строк. Это расщепленная строка, в первом блоке которой нет данных — все данные находятся во втором блоке. Чтобы разобраться, как средства LogMiner обрабатывают расщепленные строки, создадим одну такую строку. Начнем с таблицы, содержащей девять столбцов типа CHAR(2000). Я использую в базе данных блоки размером 8 Кбайт, так что, если во всех
636
Приложение А
девяти столбцах будут непустые значения, строка будет иметь размер 18000 байт, что слишком много для одного блока. Эта строка будет расщеплена не менее чем на три блока. Для демонстрации этого используем следующую таблицу: tkyte@TKYTE816> c r e a t e t a b l e t (x i n t primary key, 2 а char(2000), 3 b char(2000), с char(2000), 4 5 d char(2000), е char(2000), 6 7 f char(2000), 8 g char(2000), h char(2000), i char(2000)); 10 Table created. Теперь, чтобы продемонстрировать проблему, я вставлю строку в таблицу Т со значениями только в столбцах X и А. Размер этой строки будет составлять чуть больше 2000 байт. Поскольку столбцы В, С, D и т.д. пусты, они не будут занимать места. Эта строка поместится в один блок. Затем изменим строку, задав значения для столбцов В, С, D и Е. Поскольку значения типа CHAR всегда дополняются до заданной длины пробелами, размер строки увеличится с немногим более 2000 байт до примерно 10000 байт, в результате чего она будет расщеплена на два блока. Изменим значение всех столбцов строки, увеличив ее размер до 18 Кбайт и вызвав расщепление на три блока. Затем загрузим содержимое журнала повторного выполнения с помощью LogMiner и посмотрим, как оно будет обработано: tkyte@TKYTE816> begin 2 sys.dbms_logmnr_d.build('miner_dictionary.dat', 3 'c:\temp'); 4 —, PL/SQL procedure successfully completed. tkyte@TKYTE816> alter system archive log current; System altered. tkyte@TKYTE816> insert into t (x, a) values (1, 'non-chained'); 1 row created. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> update t set a - 'chained row', 2 b = 'x', с = 'x', 3 d = 'x', e = 'x' 4
where x = 1;
1 row updated. tkyte@TKYTE816> commit;
Пакет DBMSJ.OGMNR
637
Commit complete. tkyte@TKYTE816> update t set a = 'chained row1, 2 b = 'x', с = 'x', 3 d = 'x', e = 'x', 4 f = 'x', g = 'x', 5 h = 'x', i = 'x' 6 where x = 1;
:
1 row updated. tkyte@TKYTE816> commit; Commit complete. tkyte@TKYTE816> alter system archive log current; System altered. Создав состояние, которое мы хотим проанализировать, можно загружать соответствующий журнал с помощью LogMiner. He забудьте: после создания таблицы Т мы должны пересоздать файл словаря данных, или результат нельзя будет проанализировать! tkyte@TKYTE816> 2 l_name 3 begin 4 5 select 6 from 7 where 8
declare v$archived_log.name%type;
name into l_name v$archived_log completion_time = (select max(completion_time) from v$archived_log);
10 sys.dbms_logmnr.add_logfile(l_name, dbms_logmnr.NEW); 11 end; 12 / PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 sys.dbms_logmnr.start_logmnr 3 (dictFileName => 'c:\temp\ininer_dictionary.dat' ); 4 end; 5 / PL/SQL procedure successfully completed. tkyte@TKYTE816> select sen, sql_redo, sql_undo 2 from v$logmnr_contents 3 where sql_redo is not null or sql_undo is not null 4 / SCN SQL_REDO
SQL_UNDO
6442991118354 set transaction read write; 6442991118354 insert into TKYTE.T(X,A) va delete from TKYTE.T where X lues (1,'non-chained = 1 and A = 'non-chained
638
Приложение А 1
); =
6442991118355 6442991118356 6442991118356 6442991118356 6442991118357 6442991118358 6442991118358 6442991118358 6442991118358 6442991118359
commit; set transaction read write; Unsupported (Chained Row) Unsupported (Chained Row) commit; set transaction read write; Unsupported (Chained Row) Unsupported (Chained Row) Unsupported (Chained Row) commit;
' a n d ROWID 'AAAHdgAAGAAAACKAAA';
Unsupported (Chained Row) Unsupported (Chained Row)
Unsupported (Chained Row) Unsupported (Chained Row) Unsupported (Chained Row)
12 rows selected. Как видите, исходный оператор INSERT представлен средствами Log Miner, как ожидалось. А оператор UPDATE, расщепивший строку, в результатах LogMiner не значится. Вместо этого выдано Unsupported (Chained Row). Интересно отметить, что эта конструкция выдана дважды для первого оператора UPDATE и трижды — для второго. Пакет LogMiner выдает информацию об изменениях в базе данных по блокам. Если строка находится в двух блоках, в представлении V$LOGMNR_CONTENTS будет две записи об изменениях. Если строка расщеплена на три блока, будет три записи. Поэтому следует учитывать, что средства LogMiner не позволяют полностью воспроизвести SQL-операторы для повторного выполнения или отмены изменений в расщепленных и перемещенных строках.
Другие ограничения Кроме рассмотренных выше, средства LogMiner имеют ряд других ограничений. В настоящее время также не поддерживается: •
анализ таблиц, организованных по индексу;
•
анализ таблиц и индексов кластеров.
Представление V$LOGMNR_CONTENTS Представление V$LOGMNR_CONTENTS содержит по одной строке для каждого логического изменения в базе данных, выбранного из обработанных файлов журнала повторного выполнения. Мы уже неоднократно использовали это представление, но обращались лишь к отдельным его столбцам. В следующей таблице, созданной на основе документации Oracle, описаны все столбцы этого представления, включая доступную в них информацию: Столбец SCN
Описание Номер системного изменения для транзакции, выполнившей изменение.
.
Пакет DBMS LOGMNR
639
Столбец
Описание
TIMESTAMP
Дата генерации записи повторного выполнения. Временные отметки не могут влиять на упорядочение записей повторного выполнения Поскольку значение SCN присваивается при фиксации, только SCN можно использовать для упорядочения записей повторного выполнения. Упорядочение по столбцу TIMESTAMP в многопользовательской системе даст неверный порядок
THREAD#
Идентифицирует поток, сгенерировавший запись повторного выполнения
LOGJD
Идентифицирует файл журнала в представлении V$LOGMNR_FILES, содержащий данную запись повторного выполнения. Этот столбец является внешним ключом для представления V$LOGMNR_FILES
XIDUSN
Номер сегмента отмены (Undo Segment Number — USN) идентификатора транзакции (Transaction ID — XID). Идентификатор транзакции создается по значениям столбцов XIDUSN, XIDSLOT и XIDSQN и используется для определения транзакции, выполнившей изменение. Вместе взятые, эти три поля однозначно идентифицируют транзакцию
XIDSLOT
Номер слота идентификатора транзакции. Задает номер записи в таблице транзакций
XIDSQN
Порядковый номер идентификатора транзакции
RBASQN
Однозначно определяет журнал, содержавший рассматриваемую запись повторного выполнения. Значение RBA (Redo Block Address — адрес блока повторного выполнения) состоит из значений столбцов RBASQN, RBABLK и RBABYTE
RBABLK
Номер блока в файле журнала
RBABYTE
Смещение от начала блока, задаваемого предыдущим столбцом
UBAFIL
Номер файла UBA (Undo Block Address - адрес блока отмены), идентифицирующий файл, содержащий соответствующий блок отмены. Значение UBA создается на основе значений столбцов UBAFIL, UBABLK, UBASQN и UBAREC и используется для идентификации данных отмены, сгенерированных в процессе изменения
UBABLK
Номер блока UBA
UBAREC
Индекс записи UBA
UBASQN
Порядковый номер блока отмены UBA
ABS_FILE#
Абсолютный номер файла блока данных. Значение ABS_FILE# вместе со значениями REL_FILE#, DATA_BLOCK#, DATA_OBJ# и DATA_DOBJ идентифицирует блок, измененный транзакцией
REL_FILE#
Относительный номер файла блока данных. Задается относительно табличного пространства, в котором создан объект
DATA_BLOCK#
Номер блока данных
640
Приложение А
Столбец
Описание
DATA OBJ#
Номер объекта блока данных
DATA_DOBJ#
Номер объекта блока данных , идентифицирующий объект в табличном пространстве
SEG OWNER
Имя пользователя, которому принадлежит объект
SEG_NAME
Имя структуры, которой был выделен сегмент (другими словами, имя таблицы, имя кластера и т.п.). Имя сегмента для секционированных таблиц состоит из двух частей: после имени таблицы через запятую выдается имя секции (например, (ИмяТаблицы.ИмяСекции))
SEG TYPE
Тип сегмента в виде числа
SEG_TYPE_NAME
Тип сегмента в виде строки (другими словами, TABLE, INDEX и т.п.) В первой версии будет поддерживаться единственный тип сегмента, TABLE. Сегменты остальных типов будут обозначаться как UNSUPPORTED
TABLE_SPACE_NAME Имя табличного пространства ROW ID
Идентификатор строки
SESSION*
Идентификатор сеанса, сгенерировавшего данные повторного выполнения. Если номер сеанса из журнала повторного выполнения получить не удается, выдается пустое значение
SERIAL*
Порядковый номер сеанса, сгенерировавшего данные повторного выполнения. Значения SESSION* и SERIAL* позволяют однозначно определить соответствующий сеанс сервера Oracle. Если порядковый номер сеанса из журнала повторного выполнения получить не удается, выдается пустое значение
USERNAME
Имя пользователя, который выполнил действие, сгенерировавшее запись повторного выполнения. Вместо имени пользователя всегда будет выдаваться пустое значение, если только не включена опция аудита при архивировании. Аудит включается с помощью параметра инициализации TRANSACTION_AUDITING
SESSIONJNFO
Строка, содержащая регистрационное имя пользователя, информацию о клиенте, имя пользователя операционной системы, имя машины, терминал операционной системы и имя программы в операционной системе
ROLLBACK
Значением 1 (ИСТИНА) обозначаются действия и SQLоператоры, сгенерированные в результате запроса отката. В противном случае в этом столбце содержится значение О (ЛОЖЬ)
OPERATION
Тип операции SQL Будут выдаваться только значения INSERT, DELETE, UPDATE, COMMIT и BEGIN_TRANSACTION Все остальные операции будут представлены как UNSUPPORTED или INTERNAL OPERATION
Пакет DBMS LOGMNR
641
Столбец
Описание
SQL REDO, SQLJJNDO
Столбцы SQL_REDO и SQLJJNDO содержат SQL-подобные операторы, представляющие логические операции повторного выполнения и отмены, построенные на основе одной или нескольких записей архивного журнала повторного выполнения. Пустое значение показывает, что для этой записи повторного выполнения нельзя сгенерировать допустимый SQL-оператор. Некоторые записи повторного выполнения нельзя преобразовать в SQL-операторы. В этом случае в столбцах SQL_REDO и SQLJJNDO будут содержаться пустые значения, а в столбце STATUS - строка UNSUPPORTED
RS ID
Значение RSJD (Record Set ID - идентификатор набора записей) однозначно определяет набор записей, использованных для генерации SQL-оператора (набор может состоять из одной записи). Это значение можно использовать для выявления ситуаций, когда несколько записей генерируют один SQL-оператор. Во всех записях соответствующего набора будет одинаковое значение RSJD. SQL-оператор будет выдан только в последней строке набора. Столбцы SQL_REDO и SQLJJNDO во всех остальных строках набора будут пустыми. Учтите, что пара значений RSJD/SSN уникально идентифицирует сгенерированный SQL-оператор (см. описание столбца SSN)
SSN
Значение SSN (SQL Sequence Number - порядковый номер SQLоператора) можно использовать для идентификации нескольких строк с допустимыми операторами SQL_REDO, сгенерированных по одной записи повторного выполнения (при множественных вставках или непосредственной загрузке). Все такие строки fypyi иметь одинаковое значение RSJD, но уникальные значения SSN. Значение SSN увеличивается, начиная с 1 для каждого нового значения RSJD
CSF
Значение 1 (ИСТИНА) в столбце CSF (Continuation SQL Flag флаг продолжающегося SQL-оператора) показывает, что сгенерированный средствами LogMiner оператор REDOJ5QL или UNDO_SQL длиннее, чем максимальный размер данных типа VARCHAR2 (в настоящее время - 4000 символов). SQLоператоры, длина которых превышает это ограничение, будут занимать несколько строк. Остальная часть SQL-оператора будет содержаться в следующей строке. Пара значений RSJD, SSN у всех продолжающихся строк, которые соответствуют одному SQL-оператору, будет одинаковой. В последней строке значение в столбце CSF будет равно 0 (ЛОЖЬ), что обозначает завершение SQL-оператора
STATUS
Показывает состояние преобразования. Пустое значение означает успешное преобразование, а значение UNSUPPORTED означает, что эта версия пакетов LogMiner не поддерживает преобразование в SQL-операторы. Значение READJ=AILURE свидетельствует о внутреннем сбое операционной системы при попытке прочитать данные из файла журнала. Значение TRANSLATIONJrRROR показывает, что LogMiner не смог полностью выполнить преобразование (это может быть связано с повреждением журнала или устаревшим файлом словаря данных)
642
Приложение А
Столбец
Описание
PH1_NAME
Имя столбца-заместителя (Placeholder column name). Столбцы-заместители — это общие столбцы, с которыми можно связать любой столбец таблицы базы данных с помощью файла сопоставления LogMiner
РН1 REDO
Значение повторного выполнения для столбца-заместителя
РН1 UNDO
Значение отмены для столбца-заместителя
PH2_NAME
Имя столбца-заместителя
РН2 REDO
Значение повторного выполнения для столбца-заместителя
РН2 UNDO
Значение отмены для столбца-заместителя
РНЗ NAME
Имя столбца-заместителя
РНЗ REDO
Значение повторного выполнения для столбца-заместителя
РНЗ UNDO
Значение отмены для столбца-заместителя
РН4 NAME
Имя столбца-заместителя
РН4 REDO
Значение повторного выполнения для столбца-заместителя
РН4 UNDO
Значение отмены для столбца-заместителя
РН5 NAME
Имя столбца-заместителя
PH5_REDO
Значение повторного выполнения для столбца-заместителя
PH5_UNDO
Значение отмены для столбца-заместителя
Резюме Средства LogMiner используются не каждый день. Я не могу вспомнить ни одного приложения, которому бы они понадобились по ходу работы. Однако они позволяют определить, что происходило в базе данных, и с этой задачей справляются прекрасно. Вы видели, как пакеты LogMiner помогают при поиске "кто и когда это сделал" постфактум — именно для этого средства LogMiner используются. Или программа с ошибкой выполняется неправильно, или привилегированный пользователь сделал не то, что нужно (и не признается в содеянном). Если не был включен аудит, нет другого способа вернуться в прошлое и разобраться в том, что произошло. Средства LogMiner можно использовать и для отмены ошибочной транзакции, если удастся получить операторы SQL для отмены и повторного выполнения. В общем случае LogMiner — хорошее средство для "скрытой" работы. Процедуры этих пакетов не попадут в список десяти наиболее часто используемых, но иногда без них не обойтись.
Пакет DBMSXIBFUSCATIONLTOOLKIT
В этом разделе рассматривается шифрование данных. Мы обсудим использование стандартного пакета DBMS_OBFUSCATION_TOOLKIT в версиях Oracle 8.1.6 и 8.1.7, а также другую реализацию (оболочку), которую можно создать на базе этого пакета для расширения его функциональных возможностей. Мы затронем ряд проблем, связанных с использованием этого пакета, и очень важный аспект управления ключами. Пакеты для поддержки шифрования в базе данных появились в Oracle 8.I.6. Они были расширены в версии Oracle 8.1.7 и включают поддержку ключей шифрования большего размера и алгоритма хеширования MD5. В Oracle 8.1.6 поддерживается одинарный алгоритм шифрования DES (Data Encryption Standard) с 56-байтовым ключом. В Oracle 8.1.7 поддерживаются одинарное и тройное шифрование DES с использованием 56-, 112- и 168-битовых ключей. Алгоритм DES реализует шифрование с симметричным ключом. Это означает, что для шифрования и дешифрования данных используется один и тот же ключ. Алгоритм DES шифрует данные 64-битовыми (8-байтовыми) блоками с помощью 56-битового ключа. Ниже мы рассмотрим, как размер блока, 8 байт, должен учитываться пользователями подпрограмм шифрования. Алгоритм DES игнорирует 8 бит переданного 64-битового ключа. Однако разработчики должны передавать 64-битовый (8-байтовый) ключ. Тройной DES (3DES) гораздо устойчивей к взлому по сравнению с DES. Зашифрованные данные сложно расшифровать с помощью полного перебора. Потребуется 2**112 попыток при использовании двойного (с 16-байтовым ключом) алгоритма 3DES или 2**168 попыток при использовании тройного (с 24-байтовым ключом) алгоритма 3DES и всего лишь 2**56 попыток для обычного алгоритма DES. Как сказано в заключительной части рабочего документа rfcl321 (полный текст рабочего документа rfcl321 можно найти на сайте http://www.ietf.org/rfc.html), новый алгоритм MD5:
v)44
Приложение А ... принимает сообщение произвольной длины и выдает 128-битовый "отпечаток" или резюме сообщения ("message digest"). Предполагается, что задача создания двух сообщений с одинаковым резюме или сообщения с заданным резюме эффективно неразрешима. Алгоритм MD5 предназначен для приложений, использующих цифровую подпись, в которых большой файл необходимо безопасно "сжать", прежде чем шифровать приватным (секретным) ключом в системе шифрования с открытым ключом, например, RSA
По сути, алгоритм MD5 — это способ проверки целостности данных, намного более надежный, чем контрольные суммы и другие популярные методы. Для выполнения представленных ниже примеров использования алгоритмов DES3 и MD5 необходим доступ к серверу Oracle 8.I.7. Примеры, в которых используется алгоритм DES, работают во всех версиях, начиная с Oracle 8.I.6. При использовании подпрограмм шифрования и резюмирования по алгоритму MD5 пакета DBMS_OBFUSCATION_TOOLKIT должны выполняться следующие требования, что несколько затрудняет их применение. • Длина шифруемых данных должна быть кратна 8. 9-байтовое поле типа VARCHAR2, например, надо будет дополнять до 16 байт. Попытка зашифровать или расшифровать фрагмент данных, длина которого не кратна 8, завершится выдачей сообщения об ошибке. •
Ключ, используемый для шифрования данных, должен иметь длину 8 байт для процедуры DESEncrypt и 16 или 24 байт для процедур DES3Decrypt.
Q В зависимости от используемого вида шифрования необходимо вызывать разные подпрограммы. Например, если используется 56-битовое шифрование, вызываются подпрограммы DESENCRYPT и DESDECRYPT, если 112/168-битовое — подпрограммы DES3ENCRYPT и DES3DECRYPT. Лично я предпочитаю использовать один набор подпрограмм для всех трех вариантов. Q Подпрограммы шифрования в версии Oracle 8.1.6 являются процедурами, поэтому их нельзя использовать в SQL-операторах (процедуры нельзя вызывать в SQLоператорах непосредственно). •
Стандартные подпрограммы шифрования позволяют непосредственно шифровать данные объемом до 32 Кбайт. Они не обеспечивают шифрование/дешифрование больших объектов.
Q Среди подпрограмм шифрования в версии Oracle 8.1.7 есть функции. Однако эти функции перегружены так (см. представленные далее примеры), что использовать их в SQL-операторах тоже нельзя. •
Подпрограммы, реализующие алгоритм MD5, перегружены аналогично и тоже не могут использоваться в SQL-операторах.
По моему опыту, первое требование, связанное с необходимостью использовать данные, длина которых кратна 8, в приложениях выполнить труднее всего. Мне не хотелось бы заниматься тем, чтобы длина шифруемых данных, например зарплаты или других конфиденциальных данных, была кратна 8 байтам. К счастью, можно легко реализовать пакет-оболочку, обеспечивающий шифрование без учета этого и больший-
Пакет DBMS_OBFUSCATION_TOOLKIT
645
ства других требований. А вот обеспечение длины ключа 8, 16 или 24 байт должен взять на себя разработчик. Я собираюсь представить здесь пакет-оболочку, который будет работать в версиях 8.1.6 и выше, поддерживать все возможности шифрования и обеспечивать: •
возможность вызова функций в SQL-операторах;
•
использование одной и той же функции независимо от длины ключа;
•
возможность шифрования/дешифрования больших объектов в PL/SQL и в SQLоператорах;
Q успешную установку независимо от используемой версии сервера (8.1.6 или 8.1.7) — другими словами, независимость от наличия процедур DES3Encrypt/Decrypt и поддержки алгоритма MD5.
Пакет-оболочка Начнем со спецификации пакета. Зададим функциональный интерфейс, обеспечивающий шифрование и дешифрование данных типа VARCHAR, RAW, BLOB и CLOB. Используемый алгоритм (DES или 3DES с 16- или 24-байтовым ключом) будет выбираться в зависимости от длины ключа. В нашем интерфейсе длина ключа будет задавать алгоритм. Интерфейс устроен так, что ключ можно передавать при каждом вызове или установить на уровне пакета с помощью подпрограммы SETKEY. Преимущество использования подпрограммы SETKEY связано с тем, что проверка длины ключа и определение соответствующего алгоритма требует определенных ресурсов. Если ключ устанавливается один раз, а функции шифрования вызываются многократно, можно избежать повторного выполнения одних и тех же действий. Еще одна особенность при задании ключа состоит в том, что при работе с данными типа RAW или BLOB надо использовать ключ типа RAW. Если в качестве ключа для данных типа RAW/BLOB желательно использовать данные типа VARCHAR2, необходимо привести ключ к типу RAW с помощью средств пакета UTL_RAW, который рассматривается далее в этом приложении. С другой стороны, при работе с данными типа VARCHAR2 и CLOB, ключ должен быть типа VARCHAR2. Помимо организации дополнительного уровня интерфейса к средствам шифрования этот пакет обеспечивает доступ к подпрограммам CHECKSUM с алгоритмом MD5, если они установлены (используется сервер версии 8.1.7 и выше). Этот пакет-оболочка добавляет несколько возможных сообщений об ошибках к тем, что имеются в пакете DBMS_OBFUSCATION_TOOLKIT и описаны в документации (эти сообщения наш пакет просто передает вызывающему). В процессе работы с сервером версии 8.1.6 выдаются такие, не известные ранее сообщения: Q PLS-00302: component 'MD5' must be •
declared
PLS-00302: component 'DES3ENCRYPT' must be
Q PLS-00302: component 'THREEKEYMODE' must be
declared declared
646
Приложение А
Эти сообщения об ошибках генерируются при попытке использовать средства версии 8.1.7, шифрование по алгоритму DES3 или хеширование с помощью алгоритма MD5, на сервере версии 8.1.6. Вот предполагаемая спецификация пакета-оболочки. Описание процедур и функций представлено после кода: create or replace package crypt_pkg as function encryptString(p_data in varchar2, p_key in varchar2 default NULL) return varchar2; function decryptString(p_data in varchar2, p_key in varchar2 default NULL) return varchar2; function encryptRaw(p_data in raw, p_key in raw default NULL) return raw; function decryptRaw(p_data in raw, p_key in raw default NULL) return raw; function encryptLob(p_data in clob, p_key in varchar2 default NULL) return clob; function encryptLob(p_data in blob, p_key in raw default NULL) return blob; function decryptLob(p_data in clob, p_key in varchar2 default NULL) return clob; function decryptLob(p_data in blob, p_key in raw default NULL) return blob; subtype checksum_str is varchar2(16); subtype checksum_raw is raw(16); function md5str(p_data in function md5raw(p_data in function md51ob(p_data in function md51ob(p_data in procedure setKey(p_key in
varchar2) return checksum_str; raw) return checksum_raw; clob) return checksum_str; blob) return checksum_raw; varchar2);
end; / Функции ENCRYPTSTRING и DECRYPTSTRING используются для шифрования/ дешифрования любых данных типа STRING, DATE или NUMBER длиной до 32 Кбайт. Максимальный размер PL/SQL-переменной — 32 Кбайта, что существенно превышает максимальный размер строки, сохраняемой в базе данных, — 4000 байт. Эти функции можно вызывать непосредственно из SQL-операторов, так что, можно шифровать данные в базе с помощью операторов INSERT или UPDATE и выбирать уже расшифрованные данные с помощью оператора SELECT. Параметр KEY — необязательный. Если ключ установлен с помощью процедуры SETKEY, передавать его при каждом вызове необязательно. Имеются также функции ENCRYPTRAW и DECRYPTRAW. Они реализуют для данных типа RAW те же возможности, что и функции для данных типа VARCHAR2. Я намеренно избегаю перегрузки функций шифрования/дешифрования для данных типа RAW и VARCHAR2, давая им разные имена. Это делается для того, чтобы избежать следующей проблемы:
Пакет DBMS_OBFUSCATION_TOOLKIT
647
tkyte@TKYTE816> create or replace package overloaded 2 as 3 function foo(x in varchar2) return number; 4 function foo(x in raw) return number; 5 end; 6 / Package created. tkyte@TKYTE816> select overloaded.foo('hello') from dual; select overloaded.foo('hello') from dual * ERROR at line 1: ORA-06553: PLS-307: too many declarations of 'F00' match this call tkyte@TKYTE816> select overloaded.foo(hextoraw('aa')) from dual; select overloaded.foo( hextoraw('aa')) from dual * ERROR at line 1: ORA-06553: PLS-307: too many d e c l a r a t i o n s of 'F00' match t h i s c a l l Сервер не различает типы данных RAW и VARCHAR2 в сигнатурах перегруженных функций. Вызвать эти функции в SQL-операторах невозможно. Даже если использовать другие имена параметров функций (как в пакете DBMS_OBFUSCATION_TOOLKIT), нельзя вызвать эти функции из SQL-операторов, поскольку ключевая передача параметров в языке SQL не поддерживается. Единственно возможное решение — использовать функции с уникальными именами для идентификации необходимого типа данных параметра. Функции ENCRYPTLOB и DECRYPTLOB — это перегруженные функции, предназначенные для работы с данными типа CLOB или BLOB. Сервер Oracle позволяет различать эти типы в сигнатурах, и этим можно воспользоваться. Поскольку нельзя шифровать более 32 Кбайт с помощью подпрограмм пакета DBMS_OBFUSCATION_TOOLKIT, эти функции будут использовать алгоритм шифрования больших объектов частями по 32 Кбайта. Полученный большой объект будет представлять собой набор зашифрованных фрагментов данных размером по 32 Кбайта. Оболочка дешифрования, которую мы реализуем, учитывает упаковку данных подпрограммами шифрования больших объектов, дешифруя фрагменты и собирая из них исходный большой объект. Затем в пакете идут подпрограммы для вычисления контрольных сумм по алгоритму MD5. Чтобы точнее задать тип возвращаемых ими значений, я создал подтипы: subtype checksum_str i s varchar2(16); subtype checksum_raw i s raw(16); и указал, что эти функции возвращают именно эти типы. Пользователи пакета могут объявлять переменные этих типов: tkyte@TKYTE816> declare 2 checksum_variable crypt_pkg.checksum_str; 3 begin 4 null; 5 end;
Приложение А
6
/
PL/SQL procedure successfully completed. Это позволяет не гадать, каким будет размер возвращаемых контрольных сумм. Для данных типа VARCHAR2 предлагается четыре функции CHECKSUM (включая типы DATE и NUMBER, для которых выполняется неявное преобразование), RAW, CLOB и BLOB. Следует помнить, что контрольная сумма по алгоритму MD5 будет вычисляться только по первым 32 Кбайтам данных типа CLOB или BLOB, поскольку с переменными большего размера в языке PL/SQL работать нельзя. Представленная далее реализация пакета не только дает более удобные в использовании средства шифрования, но и демонстрирует несколько полезных приемов. Во-первых, она показывает, как легко создать свою оболочку со специализированным интерфейсом для стандартных пакетов базы данных. В данном случае мы обходим ряд очевидных ограничений пакета DBMS_OBFUSCATION_TOOLKIT. Во-вторых, показан один из методов разработки пакета, защищенного от будущих изменений в реализации стандартных пакетов. Хотелось бы создать один пакет-оболочку, который будет работать в версиях 8.1.6 и 8.1.7 и обеспечивать при этом полный доступ к возможностям версии 8.1.7. Если бы для доступа к подпрограммам DESENCRYPT, DES3DECRYPT и средствам поддержки алгоритма MD5 использовался статический SQL, пришлось бы создавать отдельную версию пакета для версии 8.1.6 сервера, поскольку функций для алгоритмов MD5 и DES3 в версии 8.1.6 нет. Использованный в реализации динамический вызов позволяет создать пакет, который можно использовать в обеих версиях сервера. При этом также сокращается объем кода, который необходимо написать. Вот реализация пакета CRYPT_PKG с описанием выполняемых действий. create or replace package body crypt_pkg as — глобальные переменные пакета g_charkey varchar2(48); g_stringFunction varchar2(1); g_rawFunction varchar2(1); g_stringWhich varchar2(75); g_rawWhich varchar2(75); g_chunkSize CONSTANT number default 32000; Пакет начинается с объявления глобальных переменных и констант. •
G_CHARKEY. В ней хранится ключ типа RAW или VARCHAR2 для использования подпрограммами шифрования. Ее длина — 48 байт, чтобы можно было хранить 24-байтовый ключ типа RAW (при этом длина удваивается из-за преобразования в шестнадцатеричный вид данных типа RAW при записи в переменную типа VARCHAR2).
• G_STRINGFUNCTION и G_RAWFUNCTION. Содержит после вызова SETKEY пустое значение либо строку '3'. Мы будем динамически добавлять эту строку к имени подпрограммы при выполнении, чтобы вызывать DESENCRYPT либо DES3ENCRYPT, в зависимости от размера ключа. Другими словами, она используется для формирования имени вызываемой функции.
Пакет DBMS OBFUSCATION TOOLKIT
649
• G_STRINGWHICH и G_RAWWHICH. Используется только для функций DES3ENCRYPT/DES3DECRYFT. Добавляет четвертый необязательный параметр, требующий использования тройного ключа, когда алгоритм 3DES работает в этом режиме. Тогда как предыдущая строковая переменная определяет, какую из функций вызывать: DESENCRYPT или DES3ENCRYPT, эта переменная задает, какое значение должно быть передано для размера ключа: для двойного или тройного. Q G_CHUNKSIZE. Константа, задающая размер фрагмента шифруемого/дешифруемого большого объекта. Она также задает максимальный размер данных, посылаемых функциям вычисления контрольной суммы по алгоритму MD5 при работе с большими объектами. Важно, чтобы это значение было кратно 8, — это предполагается в представленной далее реализации. Далее имеется шесть небольших служебных подпрограмм. Они являются вспомогательными и используются другими подпрограммами пакета: function p a d s t r ( p _ s t r i n varchar2) return varchar2 as l_len number default l e n g t h ( p _ s t r ) ; begin return to_char(l_len,'fm00000009') || rpad(p_str, (trunc(l_len/8)+sign(mod(l_len,8)))*8, chr(O)); end; function padraw(p_raw in raw) return raw as l_len number default utl_raw.length(p_raw); begin return utl_raw.concat(utl_raw.cast_to_raw(to_char(l_len,'йпОООООООЭ')), p_raw, utl_raw.cast_to_raw(rpad(chr(0), (8-mod(l_len,8))*sign(mod(l_len,8)), chr(0)))) ; end; При описании алгоритма шифрования DES было сказано, что DES шифрует данные блоками по 64 бита (8 байт). Значит, пакет DBMS_OBFUSCATION_TOOLKIT работает только с данными, длина которых кратна 8 байтам. Если шифруется строка длиной 7 байт, ее надо дополнить до 8. 9-байтовую строку необходимо дополнить до 16 байт. Две представленных выше подпрограммы кодируют и дополняют до нужной длины строки и данные типа RAW. Кодирование происходит путем помещения значения исходного размера данных перед данными в строку. Затем строка дополняется двоичными нулями (CHR(O)) до длины, кратной 8 байтам. Например, строка Hello World будет закодирована следующим образом: tkyte@TKYTE816> s e l e c t l e n g t h ( p a d s t r ) , padstr, dump(padstr) dump 2 from 3 (select to_char(l_len,'fm00000009') | | 4 rpad(p_str, 5 (trunc(l_len/8)+sign(mod(l_len,8)))*8,
65U
Приложение А
6 7 8 9 10 11 ) 12 /
chr(O)) padstr from (select length('Hello World') l_len, 'Hello World' p_str from dual )
LENGTH(PADSTR) PADSTR 24 OOOOOOllHello World
DUMP Typ=l Len=24: 48,48,48,48,48,4 8,49,49,72,101,108,108,111,32, 87,111,114,108,100,0,0,0,0,0
Окончательная длина закодированной строки — 24 байта (LENGTH(PADSDTR)), a исходная длина была 11 байт (это видно в первых восьми символах значения PADSTR). В столбце DUMP, где выданы десятичные значения байтов строки, можно увидеть, что строка завершается пятью двоичными нулями. Нам пришлось добавить 5 байт, чтобы дополнить 11-байтовую строку Hello World до длины, кратной 8. Далее идут подпрограммы, "отменяющие" выполненное выше дополнение нулевыми байтами: function unpadstr(p_str i n varchar2) r e t u r n varchar2 is
begin return substr(p_str, 9, to_number(substr(p_str, 1, 8))); end; function unpadraw(p_raw in raw) return raw is begin return utl_raw.substr(p_raw, 9, to_number(utl_raw.cast_to_varchar2(utl_raw.substr(p_raw,l,8)))) ; end; Они достаточно понятны. Предполагается, что в первых восьми байтах строки или данных типа RAW находится исходная длина строки, и возвращается соответствующая подстрока закодированных данных. Осталось две вспомогательные процедуры: procedure wa(p_clob in out clob, p_buffer in varchar2) is
begin dbms_lob.writeappend(p_clob, length(p_buffer), p_buffer); end; procedure wa(p_blob in out blob, p_buffer in raw) is begin dbms_lob.writeappend(p_blob, utl_raw.length(p_buffer), end;
p_buffer);
Они упрощают вызов DBMS_LOB.WRITEAPPEND, сокращая имя до двух букв (WA) и передавая длину записываемого фрагмента буфера — она всегда совпадает с полной длиной буфера.
Пакет DBMSJ3BFUSCATI0N_T00LKIT
65 1
Теперь рассмотрим первую общедоступную процедуру, SETKEY: procedure setKey(p_key in varchar2) as begin if (g_charkey = p_key OR p_key is NULL) then return; end if; g_charkey := p_key; if (length(g_charkey) not in (8, 16, 24, 16, 32, 48)) then raise_application_error(-20001, 'Key must be 8, 16, or 24 bytes'); end if; s e l e c t decode(length(g_charkey),8, ' ' , ' 3 ' ) , decode(length(g_charkey) , 8 , ' ' , 1 6 , ' ' , 2 4 , ' , which=>dbms_obfuscation_toolkit.ThreeKeyMode'), decode(length(g_charkey),16,'','3'), decode(length(g_charkey),16, ' ' , 32, ' ' , 4 8 , ' , which=>dbms_obfuscation_toolkit.ThreeKeyMode') i n t o g_stringFunction, g_stringWhich, g_rawFunction, g_rawWhich from dual; end; Процедура используется независимо от того, вызывали вы ее или нет. Остальные общедоступные подпрограммы вызывают процедуру SETKEY всегда. Она сравнивает переданный ключ P_KEY с тем, что хранится в глобальной переменной G_CHARKEY. Если они совпадают или ключ не задан, процедура завершает работу. Если же значение P_KEY отличается от значения G_CHARKEY, процедура продолжит работу. Сначала она проверит, допустима ли длина ключа и кратна ли 8. Ключ должен быть длиной 8, 16 или 24 байт. Поскольку процедуре могут быть переданы данные типа RAW, что вызывает представление каждого байта двухбайтовым шестнадцатеричным кодом, допускается также длина ключа 16, 32 и 48. Такая проверка, однако, не гарантирует, что ключ можно использовать. Например, можно передать четырехбайтовый ключ типа RAW, который в процедуре будет иметь длину 8 байт. В этом случае при дальнейшем выполнении подпрограмм пакета DBMS_OBFUSCATION_TOOLKIT будет получено сообщение об ошибке. Оператор SELECT с функцией DECODE используется для установки значений остальных глобальных переменных. Поскольку мы пока не можем различить типы данных RAW и VARCHAR2, то устанавливаем значения всем четырем переменным. Главное в этом фрагменте кода то, что если длина ключа — 8 байт (16 байт, если он типа RAW), то переменная FUNCTION получит значение пустой строки. Если же длина ключа — 16 или 24 байт (32 или 48 байт для ключа типа RAW), в переменную FUNCTION записывается строка ' 3 ' . Именно это в дальнейшем позволит вызвать подпрограмму DESENCRYPT или DES3Encrypt. Обратите внимание также на установку значения глобальной переменной WHICH. Она используется для передачи необязательного параметра подпрограмме DES3ENCRYPT. Если длина ключа — 8 или 16 байт (16 или 32 байта для ключа типа RAW), переменная получает значение Null, — параметр не передается. Если
652
Приложение А
длина ключа — 24 байта (48 байт для ключа типа RAW), она получает значение THREEKEYMODE, требующее от процедур ENCRYPT/DECRYPT использовать ключ большего размера. Теперь мы готовы к рассмотрению функций, выполняющих основные действия: function encryptString(p_data in varchar2, p_key in varchar2 default NULL) return varchar2 as l_encrypted long; begin setkey(p_key); execute immediate 'begin dbms_obfuscation_toolkit.des' | | g_StringFunction II 'encrypt (input_string => : 1 , key_string => :2, encrypted_string => : 3 ' II g_stringWhich |I ') ; end; ' using IN padstr(p_data), IN g_charkey, IN OUT l_encrypted; return l_encrypted; end; function encryptRaw(p_data in raw, p_key in raw default NULL) return raw as l_encrypted long raw; begin setkey(p_key); execute immediate 1 begin dbms_obfuscation__toolkit.des' | | g_RawFunction | | 'encrypt (input => : 1 , key => :2, encrypted_data => : 3 ' | | g_rawWhich I| ' ) ; end; ' using IN padraw(p_data), IN hextoraw(g_charkey), IN OUT l_encrypted; return l_encrypted; end; Функции ENCRYPTSTRING и ENCRYPTRAW действуют одинаково. Они обе динамически вызывают процедуру DESENCRYPT либо DES3ENCRYPT. Этот динамический вызов не только сокращает объем необходимого кода (поскольку избавляют от оператора IF THEN ELSE для статического вызова процедур), но и позволяют устанавливать пакет без изменений в версии 8.1.6 или 8.1.7. Поскольку мы не ссылаемся на подпрограммы пакета DBMS_OBFUSCATION_TOOLKIT статически, то сможем скомпилировать функцию в любой версии. Этот прием с динамическим вызовом пригодится в том случае, когда точно не известно, что будет установлено в базе данных. Я использовал его ранее при написании утилит, которые должны были устанавливаться на серверах версии 7.3, 8.0 и 8.1. Со временем в базовых пакетах появляются новые функции, и, если код работает в версии 8.1, хотелось бы их использовать. В версии 7.3 код тоже будет работать; в нем просто нельзя будет использовать новые функциональные возможное-
Пакет DBMS OBFUSCATION TOOLKIT
653
ти. В нашем случае именно так и происходит. При установке пакета на сервере версии 8.1.7 представленный выше код будет вызывать процедуру DES3ENCRYPT. При установке пакета на сервере версии 8.1.6 любая попытка вызвать процедуру DES3ENCRYPT приведет к ошибке времени выполнения (но не помешает установить пакет). Вызовы процедуры DESENCRYPT будут работать так же, как в версии 8.1.6. Эти функции динамически формируют строку на основе значений глобальных переменных FUNCTION и WHICH, которые мы установили в процедуре SETKEY. Мы либо добавим цифру 3 к имени процедуры, либо нет. Мы добавим необязательный четвертый параметр к вызову процедуры DES3ENCRYPT, если необходимо использовать тройной ключ. Затем выполняем строку, посылая данные и ключ для шифрования, и получаем данные в зашифрованном виде. Обратите внимание, как к исходным данным подставляются результаты применения функций PADSTR или PADRAW. Шифруется закодированная строка, дополненная до соответствующей длины. Теперь рассмотрим функции, выполняющие обратные действия: function decryptString(p_data in varchar2, p_key in varchar2 default NULL) return varchar2 as l_string long; begin setkey(p_key); execute immediate 'begin dbms_obfuscation_toolkit.des' | | g_StringFunction | | 'decrypt (input_string => : 1 , key_string => :2, decrypted_string => : 3 ' II g_stringWhich | | ' ) ; end; ' using IN p_data, IN g_charkey, IN OUT l_string; return unpadstr(l_string); end; function decryptRaw(p_data in raw, p_key in raw default NULL) return raw as l_string long raw; begin setkey(p_key); execute immediate 'begin dbms_obfuscation_toolkit.des' I I g_RawFunction I I 'decrypt (input => : 1 , key => :2, decrypted_data => :3 ' |I g_rawWhich | | ' ); end; • using IN p data, IN hextoraw(g charkey), IN OUT l_string; return unpadraw(l_string); end; Функции DECRYPTSTRING и DECRYPTRAW работают аналогично представленным ранее функциям ENCRYPT. Единственное отличие в том, что они вызывают из пакета DBMS_OBFUSCATION_TOOLKIT процедуры DECRYPT вместо ENCRYPT и
Приложение А используют функцию UNPAD для декодирования полученной строки или данных типа RAW. Перейдем к функциям для шифрования больших объектов: function encryptLob(p_data in clob, p_key in varchar2) return clob as l_clob clob; l_offset number default 1; l_len number default dbms_lob.getlength(p_data); begin setkey(p_key); dbms_lob.createtemporary(l_clob, TRUE); while (l_offset <= l_len) loop wa(l_clob, encryptString( dbms_lob.substr(p_data, g_chunkSize, l_offset))); l_offset := l_offset + g_chunksize; end loop; return l_clob; end; function encryptLob(p_data in blob, p_key in raw) return blob as l_blob blob; l_offset number default 1; l_len number default dbms_lob.getlength(p_data); begin setkey(p_key) ; dbms_lob.createtemporary(l_blob, TRUE); while (l_offset <= l_len) loop wa(l_blob, encryptRaw( dbms_lob.substr(p_data, g_chunkSize, l_offset))); l_offset := l_offset + g_chunksize; end loop; return l_blob; end; Это перегруженные функции для данных типа BLOB и CLOB. Вначале они создают временный большой объект, в который будут записываться зашифрованные данные. Поскольку при шифровании мы изменяем длину строки/данных типа RAW, обеспечивая сохранение исходной длины и дополнение до необходимой, делать это "на месте", используя существующий большой объект, не представляется возможным. Например, при наличии большого объекта размером 64 Кбайт мы "увеличим" первые 32 Кбайта. Теперь необходимо сместить остальные 32 Кбайта большого объекта, чтобы обеспечить пространство для записи увеличенного фрагмента данных. Кроме того, это не позволит вызывать данные функции из SQL-операторов, поскольку локатор большого объекта придется передавать в режиме IN/OUT, а при наличии параметров в режиме IN/OUT
Пакет DBMSJDBFUSCATION_TOOLKIT
6 5 5
вызывать функцию в SQL-операторах нельзя. Поэтому мы просто копируем зашифрованные данные в новый большой объект, который затем можно использовать где угодно, в том числе в операторе INSERT или UPDATE. Для шифрования и кодирования данных большого объекта используется следующий алгоритм. Начиная с байта 1 (L_OFFSET), мы шифруем G_CHUNKSIZE байтов данных. Они добавляются к ранее созданному временному большому объекту. Добавляем к смещению значение G_CHUNKSIZE и продолжаем выполнять тело цикла, пока не обработаем весь большой объект. Возвращаем временный большой объект вызывающему. Теперь перейдем к дешифрованию данных больших объектов: function decryptLob(p_data i n clob, p_key in varchar2 default NULL) return clob as l_clob clob; l_offset number default 1; l_len number default dbms_lob.getlength(p_data); begin setkey(p_key); dbms_lob.createtemporary(l_clob, TRUE); loop exit when l_offset > l_len; wa(l_clob, decryptString( dbms_lob.substr(p_data, g_chunksize+8, l_offset))); l_offset := l_offset + 8 + g_chunksize; end loop; return l_clob; end; •
function decryptLob(p_data in blob, p_key in raw default NULL) return blob as l_blob blob; l_offset number default 1; l_len number default dbms_lob.getlength(p_data); begin setkey(p_key); dbms lob.createtemporary(1 blob, TRUE); loop exit when ]L_offset > 1_ _len; wa(l blob, decryptRaw( dbms lob.substr(p data, g chunksize+8, 1 offset))); 1 offset :•= 1 offset + 8 + g chunksize; end loop; return 1 blob; end; В этих функциях мы снова, по тем же причинам, что и ранее, используем временный большой объект для дешифрования. На этот раз, однако, есть еще одна причина для использования временного большого объекта. Если не использовать временный большой объект для записи дешифрованных данных, данные будут дешифроваться непосредственно в базе. Последующие операторы SELECT будут выдавать уже дешифро-
656
Приложение А
ванные данные, если мы не скопируем их в новый большой объект. В данном случае использовать временный большой объект еще важнее. Проходим в цикле по фрагментам большого объекта. Начав со смещения 1 (с первого байта) большого объекта, выбираем с помощью SUBSTR из него G_CHUNKSIZE+8 байтов. Эти 8 байтов добавлены к данным функциями PADSTR/PADRAW при кодировании. Итак, обрабатываем большой объект фрагментами размером G_CHUNKSIZE+8 байтов, дешифруем данные и добавляем их к временному большому объекту. Этот объект затем возвращается клиенту. Теперь рассмотрим последнюю часть пакета CRYPT_PKG — интерфейс к подпрограммам, реализующим алгоритм MD5: function md5str(p_data in varchar2) return checksum_str is l_checksum_str checksum_str; begin execute immediate 'begin :x := dbms_obfuscation_toolkit.md5(input_string => :y); end;' using OUT l_checksum_str, IN p_data; return l_checksum_str; end; function md5raw(p_data in raw) return checksum_raw is l_checksum_raw checksum_raw; begin execute immediate 'begin :x := dbms_obfuscation_toolklt.md5(input => :y); end;' using OUT l_checksum_raw, IN p_data; return l_checksum_raw; end; function md51ob(p_data in clob) return checksum_str is l_checksum_str checksum_str; begin execute immediate 'begin :x := dbms_obfuscation_toolkit.md5(input_string => :y); end;' using OUT l_checksum__str, IN dbms_lob.substr(p_data,g_chunksize,1); return l_checksum_str; end; function md51ob(p_data in blob) return checksum_raw is l_checksum_raw checksum_raw; begin execute immediate 'begin :x := dbms_obfuscation_toolkit.md5(input => :y); end;' using OUT l_checksum_raw, IN dbms_lob.substr(p_data,g_chunksize,1); end; end;
return l_checksum_raw;
Пакет DBMS_OBFUSCATION_TOOLKIT
657
Эти функции просто передают данные исходным функциям пакета DBMS_OBFUSCATION_TOOLKIT. При этом они, правда, не перегружены, что позволяет их использовать непосредственно в SQL-операторах. Следует помнить, что функции MD5 для больших объектов вычисляют контрольные суммы только по первым G_CHUNKSIZE байтам данных. Это связано с ограничением языка PL/SQL на максимальный размер переменных. Теперь я продемонстрирую функциональные возможности пакета. Следующие примеры выполнялись на сервере Oracle 8.I.7. Такие примеры с использованием алгоритмов DES3 и MD5 в версии 8.1.6 не выполнятся. tkyte@DEV817> declare 2 l_str_data varchar2(25) := ' h e l l o world'; 3 l_str_enc varchar2(50); 4 l_str_decoded varchar2(25); 5 6 1 raw data raw(25) := u t l raw.cast to raw('Goodbye'); 7 l_raw_enc raw(50); 8 1 raw decoded raw(25); 9 " " 10 begin 11 crypt_pkg.setkey('MagicKey'); 12 13 l_str_enc := crypt_pkg.encryptString(l_str_data); 14 l_str_decoded := crypt_pkg.decryptString(l_str_enc); 15 16 dbms_output.put_line('Encoded In hex = ' | | 17 utl_raw.cast_to_raw(l_str_enc)); 18 dbms_output.put_line('Decoded = ' II l_str_decoded); 19 20 crypt_pkg.setkey(utl_raw.cast_to_raw('MagicKey')); 21 22 l_raw_enc := crypt_pkg.encryptRaw(l_raw_data); 23 l_raw_decoded := crypt_pkg.decryptRaw(l_raw_enc); 24 25 dbms_output.put_line('Encoded = ' II l_raw_enc); 26 dbms_output.put_line('Decoded = ' || 27 utl_raw.cast_to_varchar2(l_raw_decoded)); 28 end; 29 / Encoded In hex = 7004DB310AC6A8F210F8467278518CF988DF554B299B35EF Decoded » h e l l o world Encoded = E3CC4E04EF3951178DEB9AFAE9C99096 Decoded = Goodbye PL/SQL procedure successfully completed. Этот пример демонстрирует базовые возможности функций ENCRYPT и DECRYPT. Здесь я вызывал их из PL/SQL, ниже мы будем вызывать их в SQL-операторах. Я протестировал работу со строками и данными типа RAW. В строке 11 кода я вызываю процедуру SETKEY для установки ключа шифрования, который будет использоваться при шифровании и дешифровании данных типа VARCHAR2, равным строке MAGICKEY.
6 JO
Приложение А
Это позволяет не передавать строку остальным функциям. Затем я шифрую строку и помещаю результат в переменную L_STR_ENC. После этого строка дешифруется, чтобы убедиться, что все работает, как предполагалось. В строках 16-18 выдаются результаты. Поскольку в зашифрованных данных могут содержаться символы, "сводящие с ума" эмуляторы терминалов, я выдаю зашифрованную строку на терминал, вызвав UTL_RAW.CAST_TO_RAW в строке 17. Тип данных меняется с VARCHAR2 на RAW. Сами данные при этом не меняются. Поскольку данные типа RAW неявно преобразуются в строку шестнадцатеричных цифр, этот прием можно использовать как удобный способ отображения на экране данных в шестнадцатеричном виде. В строках с 20 по 27 я делаю то же самое для данных типа RAW. Снова необходимо вызвать процедуру SETKEY, передав на этот раз 8 байт данных типа RAW. Для преобразования ключа типа VARCHAR2 в ключ типа RAW я использовал функцию UTL_RAW.CAST_TO_RAW. Можно было также воспользоваться функцией HEXTORAW и передать строку шестнадцатеричных цифр. Затем я шифрую данные и дешифрую результат. Зашифрованные данные выдаются в явном виде (они все равно будут отображаться в шестнадцатеричном представлении) и привожу тип расшифрованных данных снова к VARCHAR2, чтобы можно было проверить корректность дешифровки. Результат подтверждает, что пакет работает. Рассмотрим, как этот пакет использовать в языке SQL. Для этого протестируем процесс шифрования тройным DES в режиме с двойным ключом: tkyte@DEV817> drop table t ; Table dropped. tkyte@DEV817> create table t 2 (id i n t primary key, data varchar2(255)); Table created. tkyte@DEV817> insert into t values 2 (1, crypt_pkg.encryptString('This i s row 1', 1 row created.
'MagicKeylsLonger'));
tkyte@DEV817> insert into t values 2 (2, crypt_pkg.encryptString( 'This is row 2', 'MagicKeylsLonger')); 1 row created. tkyte@DEV817> s e l e c t u t l _ r a w . c a s t _ t o _ r a w ( d a t a ) e n c r y p t e d _ i n _ h e x , 2 crypt_j?kg.decryptString(data,'MagicKeylsLonger') decrypted 3 from t 4 / ENCRYPTED_IN_HEX
DECRYPTED
0B9A809515519FA6A34F150941B318DA441FBB0C790E9481 This i s row 1 OB9A809515519FA6A34F150941B318DA20A936F9848ADC13 This i s row 2 Итак, задав 16-байтовый ключ процедуре CRYPT_PKG.ENCRYPTSTRING, мы автоматически переключились на использование процедуры DES3ENCRYPT пакета DBMS_OBFUSCATION_TOOLKIT. Этот пример показывает, насколько легко использовать средства пакета CRYPT_PKG в языке SQL. Все функции можно непосредствен-
Пакет DBMS_OBFUSCATION_TOOLKIT
6 5 9
но вызывать в SQL-операторах в тех же конструкциях, что и, например, функцию SUBSTR. Средства пакета CRYPT_PKG можно использовать в конструкции SET оператора UPDATE, в конструкции VALUES оператора INSERT, в списке выбора оператора SELECT и даже в конструкции WHERE любого оператора. Теперь посмотрим, как этот пакет можно использовать для больших объектов, продемонстрировав по ходу использование функций MD5. Для проверки используем объект типа CLOB размером 50 Кбайт. Сначала загружаем большой объект в базу данных: tkyte@DEV817> create table demo (id int, theClob clob); Table created. tkyte@DEV817> create or replace directory my_files as 2 '/dOl/home/tkyte'; Directory created. tkyte@DEV817> declare 2 l_clob clob; 3 l_bfile bfile; 4 begin 5 insert into demo values (1, empty_clob()) 6 returning theclob into l_clob; 7 8 l_bfile := bfilename('MY_FILES', 'htp.sql'); 9 dbms_lob.fileopen(l_bfile); 11 dbms_lob.loadfromfile(l_clob, l_bfile, 12 dbms_lob.getlength(ljbfile)); 13 14 dbms_lob.fileclose(l_bfile); 15 end; 16 / PL/SQL procedure successfully completed. Процедуры загрузили данные в объект типа CLOB. Теперь мы хотели бы выполнить с ним какие-нибудь действия. Снова используем язык SQL, поскольку это наиболее оптимальный способ работы с данными. Начнем с вычисления контрольной суммы по первым 32 Кбайтам данных объекта типа CLOB: tkyte@DEV817> s e l e c t d b m s _ l o b . g e t l e n g t h ( t h e c l o b ) l o b _ l e n , 2 u t l _ _ r a w . c a s t _ t o _ r a w ( c r y p t _ p k g . m d 5 l o b ( t h e c l o b ) ) md5_checksum 3 from demo; LOB_LEN MD5_CHECKSUM 50601 307D19748889C2DEAD879F89AD45D1BA Для того чтобы преобразовать перед выводом данные типа VARCHAR2, возвращаемые функциями MD5, в шестнадцатеричную строку, мы воспользовались функцией UTL_RAW.CAST_TO_RAW. Строка типа VARCHAR2 скорее всего будет содержать встроенные символы новой строки, табуляции или другие управляющие символы терминала. Представленный выше код показывает, насколько легко использовать функции MD5: достаточно передать им данные, и контрольная сумма будет вычислена.
660
Приложение А
Далее я продемонстрирую, как шифровать и дешифровать большой объект. Для этого используется простой оператор UPDATE. Обратите внимание, что на этот раз применяется ключ шифрования длиной 24 байта. Мы будем использовать подпрограмму DES3ENCRYPT, поскольку установили необязательный параметр which = > ThreeKeyMode. В результате будет выполнено шифрование тройным алгоритмом DES с тройным ключом: tkyte@DEV817> update demo 2 set theClob = cryptj?kg.encryptLob(theClob, 3 'MagicKeylsLongerEvenMore') 4 where id = 1; 1 row updated. tkyte@DEV817> s e l e c t dbms_lob.getlength(theclob) lob_len, 2 utl_raw.cast_to_raw(crypt_pkg.md51ob(theclob)) md5_checksum 3 from demo; LOB_LEN MD5_CHECKSUM 50624 FCBD33DA2336C83685B1A62956CA2D16
Длина объекта увеличилась с 50601 до 50624 байт, и рассчитанная по алгоритму MD5 контрольная сумма отличается от прежней — т.е. данные изменены. Если вспомнить представленное ранее описание алгоритма, мы взяли первые 32000 байт объекта типа CLOB, добавили в начале 8 байт при кодировании строки и зашифровали результат. Затем мы выбрали оставшиеся 18601 байт данных и дополнили их до 18608 байт (чтобы длина была кратна 8), и добавили еще 8 байт для сохранения исходной длины. Это и дало в результате увеличение длины до 50624 байт. Теперь рассмотрим, как выбрать из базы данных зашифрованный объект типа CLOB: tkyte@DEV817> s e l e c t 2 3 from demo 4 where id = 1;
dbms_lob.substr( crypt_pkg.decryptLob(theClob),
100, 1) data
DATA
set define off create or replace package htp as /* STRUCTURE tags */ procedure htmlOpen; procedure Интересно отметить, что я не передавал ключ дешифрования. Поскольку он сохранен в переменной пакета, то передавать его необязательно. Значение переменной пакета сохраняется между вызовами, но только на время сеанса. Ключ хранится в глобальной переменной в теле пакета Vi недоступен другим сеансам.
Проблемы Данные, зашифрованные средствами пакета DBMS_OBFUSCATION_TOOLKIT в системе с обратным порядком байтов ("little endian" system) не могут быть дешифрова-
Пакет DBMS_OBFUSCATION_TOOLKIT
6 6 1
ны с помощью того же ключа в системе с прямым порядком байтов ("big endian" system). Речь идет о порядке байтов в многобайтовом числе. На Intel-платформах (NT, многие дистрибутивы Linux и Solaris x86) принят обратный порядок байтов. Системы на базе процессоров Sparc и Rise обычно имеют прямой порядок байтов. Данные, зашифрованные на Windows NT с помощью ключа "12345678", нельзя расшифровать на Sparc Solaris с помощью этого же ключа. Следующий пример демонстрирует проблему (и способ ее избежать). На платформе Windows NT выполним: tkyte@TKYTE816> create t a b l e anothert (encrypted_data
varchar2(25));
Table created. tkyte@TKYTE816> i n s e r t i n t o anothert values 2 (crypt_j>kg.encryptString( ' h e l l o world 1 ,
'12345678"));
1 row created. tkyte@TKYTE816> select crypt_pkg.decryptstring(encrypted_data) from anothert; CRYPT_PKG.DECRYPTSTRING(ENCRYPTED_DATA) hello world tkyte@TKYTE816> host exp userid=tom/kyte tables=anothert Соответствующий файл EXPDAT.DMP передан по FTP на машину Sparc Solaris и загружены находящиеся в нем данные. При попытке выбрать данные я получил: ops$tkyte@DEV816> select 2 crypt_pkg.decryptstring(encrypted_data, '12345678') 3 from t ; crypt_j?kg.decryptstring(encrypted_data, '12345678') ERROR at line 2: ORA-06502: PL/SQL: numeric or value error: character to number conversion ORA-06512: at "OPS$TKYTE.CRYPT_PKG", line 84 ORA-06512: at "OPS$TKYTE.CRYPT_PKG", line 215 ORA-06512: at line 1 ops$tkyte@DEV816> select 2 crypt_pkg.decryptstring(encrypted_data, 3 from t;
'43218765')
CRYPT_PKG.DECRYPTSTRING(ENCRYPTED_DATA,'43218765') hello world Представленное выше сообщение об ошибке выдается пакетом-оболочкой. Я предполагаю, что первые 8 байт данных в строке — это число. Поскольку с помощью переданного ключа нельзя успешно дешифровать данные, первые 8 байт не содержат значение длины — это произвольный набор символов. Оказывается, 8 байтовый (или 16-, или 24-байтовый ключ) внутренне хранится как набор 4-байтовых целых чисел. Мы должны изменить порядок байтов в каждой 4-байтовой группе символов ключа, чтобы расшифровать данные в системе с другим поряд-
00.Z
Приложение А
ком байтов. Поэтому, если использовался ключ '12345678' на платформе Windows NT (Intel), я должен использовать ключ '43218765' на Sparc Solaris. Задаем первые 4 байта в обратном порядке, затем задаем в обратном порядке следующие 4 байта (и так далее — для ключей большего размера). Это важно помнить при переносе данных, например, с NT на Sparc Solaris и при запросе данных из удаленной базы. Вы должны быть готовы физически переупорядочить байты, чтобы успешно дешифровать данные. Эта проблема была решена в версиях, начиная с Oracle 8.1.7.1, так что теперь переставлять байты уже не нужно.
Управление ключами Я бы хотел кратко рассмотреть проблемы управления ключами. Шифрование — лишь часть действий, обеспечивающих защиту данных. Данных в базе шифруются для того, чтобы администратор базы данных, выполнив запрос к таблице, не смог понять, какие данные в ней находятся. Например, вы поддерживаете Web-сайт, где принимаете заказы клиентов. Клиенты передают номера кредитных карточек, которые сохраняются в базе данных. Необходимо гарантировать, что ни администратор базы данных, у которого есть возможность выполнять ее резервное копирование, ни злонамеренный хакер, взломавший базу данных, не смогли бы прочитать эту строго конфиденциальную информацию. Если хранить данные в явном виде, их легко сможет прочитать любой, получивший привилегии доступа администратора к базе данных. Если же данные хранятся в зашифрованном виде, этого не произойдет. Зашифрованные данные защищены настолько, насколько защищен ключ, использовавшийся для шифрования. Тут все дело в ключе. Если ключ известен, данные с таким же успехом можно вовсе не шифровать (при наличии ключа данные можно расшифровать оператором SELECT). Поэтому проблему генерации и защиты ключей следует хорошо продумать. Можно использовать различные подходы. Далее представлено несколько подходов, которые можно использовать, но с каждым из них связаны специфические проблемы.
Генерация и хранение ключей в клиентском приложении Можно вообще не хранить ключей в базе данных, вынеся их на другую машину (главное, не потерять их — потребуются сотни лет процессорного времени, чтобы их подобрать). При этом клиентское приложение, будь то сервер приложений или клиент в приложении с клиент-серверной архитектурой, сохраняет ключи в своей системе. Клиентское ПО определяет, имеет ли право обратившийся пользователь дешифровать данные, и посылает соответствующие ключи серверу базы данных. При использовании этого метода, связанного с передачей ключей по сети, необходимо добавить еще один уровень шифрования — шифрование потока данных протокола Net8. Связываемые переменные и строковые литералы по умолчанию передаются в явном виде. В этом случае, поскольку защита ключей принципиально важна, придется использовать технологии типа ASO (Advanced Security Option — расширенная защита).
Пакет DBMS OBFUSCATION TOOLKIT
663
Эта возможность протокола Net8 обеспечивает шифрование всего потока данных, так что никто не сможет перехватить ключи при передаче по сети. Если ключ безопасно хранится в клиентском приложении (это должны обеспечить вы сами) и используется ASO, это решение будет вполне надежным.
Хранение ключей в той же базе данных Предполагается хранение ключей вместе с данными в базе. Это решение — не идеально, поскольку есть вероятность, что при наличии достаточного времени администратор базы данных (или хакер, получивший привилегии учетной записи администратора) сможет найти ключи и получить зашифрованные с их помощью данные. В подобных случаях следует максимально затруднить поиск ключей, соответствующих данным. Сделать это сложно, поскольку и ключи, и данные хранятся в одной базе. Приложение не должно напрямую связывать таблицу ключей с таблицей данных. Предположим, имеется таблица со столбцами CUSTOMER_ID, CREDIT_CARD и другими данными. Столбец CUSTOMER_ID неизменен — это первичный ключ (а мы знаем, что изменять первичный ключ не стоит). Можно создать другую таблицу: ID number primary key, DATA varchar2(255) В этой таблице будут храниться ключи для всех идентификаторов клиентов. Создадим функцию в пакете, которая будет возвращать ключ, только при условии, что ее вызывает соответствующий пользователь и в соответствующей среде (аналогично тому, как при использовании средств тщательного контроля доступа данные можно получить только в соответствующем контексте приложения). Пакет будет предоставлять две основные функции. Q Функция 1: добавление нового клиента. В данном случае функция будет выполнять некоторые действия с идентификатором клиента, чтобы скрыть его (преобразовать в другую строку). Функция должна быть детерминированной, чтобы по заданному идентификатору клиента всегда выдавалась одна и та же строка. Чуть позже мы поговорим о том, что делать с идентификатором клиента или любой другой строкой. Для клиента также генерируется случайный ключ. Ниже представлен ряд способов генерации этого ключа. Затем с помощью динамического SQL строка вставляется в таблицу ключей. (Не стоит называть таблицу KEY_TABLE или еще как-нибудь, чтобы по имени было понятно ее назначение.) •
Функция 2: получение ключа для клиента. Функция принимает идентификатор клиента, пропускает его через ту же детерминированную функцию, что и предыдущая, а затем с помощью динамического SQL находит и возвращает ключ для клиента. Все это выполняется, только если текущий пользователь работает в соответствующей среде.
Динамический SQL используется для того, чтобы нельзя было понять, что пакет используется для управления ключами. Пользователь может обратиться к представлению ALL_DEPENDENCIES и выяснить, на какие таблицы статически ссылается пакет. При использовании динамического SQL не будет никакой связи между пакетом и таблицей
664
Приложение А
ключей. Это не позволит скрыть таблицу ключей от очень умного человека, но максимально затруднит ее поиск. Теперь о том, как скрыть идентификатор клиента или любой набор неизменных данных в строке. (Первичный ключ для этого подходит только при условии, что всегда остается неизменным.) Для этого имеется множество алгоритмов. Если бы я использовал Oracle 8.1.7, то мог бы послать результат конкатенации этих данных с некоторым постоянным значением (его часто называют "затравкой" — "salt") функциям резюмирования по алгоритму MD5 для получения 16-байтовой контрольной суммы. Именно ее я бы использовал в качестве ключа. В Oracle 8.1.6 я мог бы использовать такое же действие, но передавал бы значение функции DBMS_UTILITY.GET_HASH_VALUE с очень большим размером хеш-таблицы. Можно было бы применить операцию XOR после изменения порядка байтов в CUSTOMER_ID. Подойдет любой алгоритм, который сложно угадать по полученному результату. Вы можете возразить, что администратор базы данных может прочитать код, увидеть алгоритм и разобраться во всем этом. Нет, если скрыть (wrap) код. Скрыть PL/SQL-код очень просто (см. описание утилиты WRAP в руководстве PL/SQL User's Guide and Reference). Она берет исходных код и "шифрует" его. В базу данных загружается "зашифрованная" версия кода. Теперь код никто прочитать не сможет. Средств "дешифрации" для wrap нет. Сохраните только в каком-либо безопасном месте текст алгоритма. Восстановить текст после применения утилиты wrap и получить исходный код из базы данных не удастся. Для генерации ключа необходим своего рода генератор случайных строк. Его можно создать разными способами. Можно использовать те же приемы, что и для сокрытия значения CUSTOMER_ID. Можно использовать реальный генератор случайных чисел (например, пакет DBMS_RANDOM или собственной разработки). Задача в том, чтобы генерировать значение, которое будет сложно "угадать" на основе имеющейся информации. Лично я предпочел бы хранить ключи именно в базе данных. Если ключи находятся в клиентском приложении, всегда остается риск их потери вследствие сбоя носителя или другой катастрофы в системе. При хранении ключей в файловой системе риск остается. Только хранение ключей в базе данных гарантирует, что зашифрованные данные всегда удастся расшифровать: база данных всегда синхронизирована, а процедуры резервного копированиями восстановления — надежны.
Хранение ключей в файловой системе сервера базы данных Ключи шифрования данных можно также хранить в файловой системе сервера и обращаться к ним с помощью внешней процедуры на языке С. Я рекомендую использовать внешнюю процедуру на языке С, поскольку цель состоит в том, чтобы спрятать ключи от администратора базы данных, который обычно имеет доступ к учетной записи владельца программного обеспечения Oracle. Пакет UTL_FILE, работа с объектами типа BFILE и вызов хранимых процедур на языке Java, выполняющих ввод-вывод, осуществляется с правами пользователя-владельца программного обеспечения Oracle. Если
Пакет DBMS_OBFUSCATION_TOOLKIT
665
администратор базы данных работает с правами владельца программного обеспечения Oracle, он может прочитать файлы. А прочитав, сможет найти в них ключи. При использовании внешней процедуры, написанной на языке С, можно запустить службу EXTPROC (и соответствующий процесс прослушивания для службы EXTPROC) от имени другого пользователя. В этом случае пользователь Oracle не увидит ключи. К ним можно получить доступ только через процесс прослушивания EXTPROC. Это добавляет еще один уровень защиты. Подробнее о реализации этого подхода см. в главе 18.
Резюме Мы достаточно подробно рассмотрели пакет DBMS_OBFUSCATION_TOOLKIT. Я научил вас создать для него пакет-оболочку, предоставляющий те же функциональные возможности нужным образом (если моя реализация вам не подходит, напишите другую оболочку). Вы научились использовать динамический SQL для создания пакетов, которые можно устанавливать на серверах с разными возможностями (речь шла о возможностях шифрования в версиях 8.1.6 и 8.1.7). Мы обсудили проблему переноса данных на другую платформу при использовании пакета DMBS_OBFUSCATION_TOOLKIT, связанную с изменением порядка байтов в ключах. Вы узнали о возможности решать эту проблему переупорядочением байтов ключа. Интересным расширением пакета CRYPT_PKG было бы автоматическое определение порядка байтов в системе и перестановка байтов в ключе, чтобы избавить пользователя от необходимости учитывать это различие. Эта идея становится еще более привлекательной, если учесть, что в версиях начиная с 8.1.7.1 менять порядок следования байтов больше не нужно — соответствующий код в этой версии можно не выполнять, что обеспечивает одинаковые функциональные возможности во всех версиях сервера. Наконец, мы рассмотрели важную проблему управления ключами. Я потратил немало времени на разработку удобного пакета-оболочки, упрощающего шифрование и дешифрование данных. Вам для самостоятельного решения, однако, я оставляю самую сложную проблему — защиту ключей. Следует помнить, что при подозрении взлома ключей необходимо создать новый их набор, расшифровать и снова зашифровать все имеющиеся данные. Если все продумать заранее, подобных ситуаций можно избежать.
Пакет DBMSOUTPUT
Пакет DBMS_OUTPUT пользователи часто понимают неправильно. Они не понимают, что и как он делает и с какими ограничениями связано его использование. В этом разделе я попытаюсь объяснить все это. Я также предложу альтернативные пакеты с возможностями, аналогичными тем, что предоставляются пакетом DBMS_OUTPUT, но без упомянутых ограничений. Пакет DBMS_OUTPUT предназначен для эмуляции простых действий вывода на экран в языке PL/SQL. Он позволяет эмулировать выдачу на экран строки Hello World из PL/SQL-кода. Вы уже видели сотни примеров использования этого пакета. Вот типичный пример: ops$tkyte@DEV816> exec dbms_output.put_line('Hello World'); Hello World PL/SQL procedure successfully completed. Однако вы не видите команды SQL*Plus (или SVRMGRL), которая необходима, чтобы этот пример сработал. Вывод на экран можно включать и отключать следующим образом: ops$tkyte@DEV816> s e t serveroutput off ops$tkyte@DEV816> exec dbms_output.put_line('Hello World'); PL/SQL procedure successfully completed. ops$tkyte@DEV816> s e t serveroutput on ops$tkyte@DEV816> exec dbms_output.put_line('Hello World'); Hello World PL/SQL procedure successfully completed.
Пакет DBMS OUTPUT
667
На самом деле язык PL/SQL не позволяет выполнять ввод-вывод на экран (вот почему я написал, что пакет предназначен для эмуляции такой возможности). На самом деле ввод-вывод на экран выполняет утилита SQL*Plus — из языка PL/SQL ничего нельзя выдать на терминал. PL/SQL-код выполняется другим процессом, обычно работающим на другой машине в сети. Утилиты SQL*Plus, SVRMGRL и другие инструментальные средства, однако, могут выдавать результаты на экран весьма просто. Это легко обнаружить при использовании пакета DBMS_OUTPUT в Java-коде или в программе на Рго*С (или в любой другой программе) — выданные пакетом DBMS_OUTPUT результаты никогда не выдаются на экран. Дело в том, что приложение само отвечает за выдачу этих результатов.
Как работает пакет DBMS_OUTPUT В пакете DBMS_OUTPUT подпрограмм немного. Чаще всего используются следующие. •
PUT. Выдает строку, данные типа NUMBER или DATE в буфер вывода, не добавляя символ новой строки.
•
PUT_LINE. Выдает строку, данные типа NUMBER или DATE в буфер вывода и добавляет символ новой строки.
•
NEW_LINE. Выдает в буфер вывода символ новой строки.
•
ENABLE/DISABLE. Включает и отключает выдачу в буфер, при использовании DBMS_OUTPUT.
Эти процедуры выдают данные во внутренний буфер; PL/SQL-таблицу в теле пакета DBMS_OUTPUT. Общая длина выдаваемой строки (сумма всех байтов, помещенных в буфер пользователем до вызова процедуры PUT_LINE или NEW_LINE, завершающей эту строку) должна быть не более 255 байт. Выданные результаты буферизуются в этой таблице и не будут видны в среде SQL*Plus, пока не завершится выполнение соответствующего PL/SQL-кода. Язык PL/SQL не позволяет ничего выдавать на терминал, данные просто помещаются в PL/SQL-таблицу. При вызове процедуры DBMS_OUTPUT.PUT_LINE пакет DBMS_OUTPUT сохраняет соответствующие данные в массиве (PL/SQL-таблице) и возвращает управление. Пока работа вызывающей процедуры не завершится, результаты на экран не выдаются. Даже после этого результаты можно увидеть, только если используемый клиент "знает" о пакете DBMS_OUTPUT и сам позаботится о выдаче накопленных результатов. Утилита SQL*Plus, например, вызывает DBMS_OUTPUT.GETJLINES для получения части содержимого буфера пакета DBMS_OUTPUT и выдачи его на экран. Если вызвать хранимую процедуру из приложения на языке Java/JDBC, предположение о том, что результаты DBMS_OUTPUT окажутся там же, где и данные, выданные с помощью функции System.out.println, не оправдается. Если клиентское приложение не позаботится о выборке и выдаче данных, они просто исчезнут. Как сделать это в программе на языке Java/JDBC будет продемонстрировано в этом разделе. При использовании пакета DBMS_OUTPUT больше всего сбивает с толку то, что результат буферизуется и не выдается до завершения процедуры. Пользователи зна-
UUO
Приложение А
ют о пакете DBMS_OUTPUT и пытаются использовать его для контроля продолжительного процесса. Другими словами, они повсеместно вставляют в код вызовы DBMS_OUTPUT.PUT_LINE и запускают процедуру в среде SQL*Plus. Они ждут, что результаты начнут выдаваться на экран, и очень расстраиваются, когда этого не происходит (потому что и не может произойти). Не зная особенностей реализации, трудно понять, почему результаты не выдаются сразу. Если понять, что процедуры на языке PL/SQL (а также внешние процедуры на языках Java и С), работающие на сервере, не выполняют ввод-вывод на экран и что пакет DBMS_OUTPUT просто накапливает данные в большом массиве, ситуация проясняется. Вот когда имеет смысл вернуться к разделу, посвященному пакету DBMS_APPLICATION_INFO, и почитать о способе контроля работы продолжительных действий. Для контроля продолжительно работающих процессов надо использовать пакет DBMS_APPLICATION_INFO, а не DBMS_OUTPUT. Для чего же тогда пакет DBMS_OUTPUT? Он прекрасно подходит для выдачи простых отчетов и создания утилит. В главе 23 была представлена процедура PRINT_TABLE, использующая средства пакета DBMS_OUTPUT для генерации результатов следующего вида: SQL> exec p r i n t _ t a b l e ( ' s e l e c t * from all__users where username = u s e r 1 ) ; USERNAME USER_ID CREATED
: OPS$TKYTE : 334 : 02-oct-2000 10:02:12
PL/SQL procedure successfully completed.
Она выдает данные по одному столбцу в строке, а не одной, разбитой на части, строкой. Прекрасно подходит для выдачи длинных строк таблиц, которые переносятся по границе экрана, что затрудняет чтение. Теперь, зная, что пакет DBMS_OUTPUT работает путем помещения данных в PL/SQLтаблицу, можно изучать его реализацию. При включении DBMS_OUTPUT (вызовом DBMS_OUTPUT.ENABLE либо с помощью команды SET SERVEROUTPUT ON) мы не только позволяем накапливать данные, но и задаем максимальный объем данных, которые сможем накопить. По умолчанию, если выполнить: SQL> set serveroutput on
создается буфер DBMS_OUTPUT размером 20000 байт. При достижении этого предела будет выдано: begin * ERROR a t l i n e 1: ORA-20000: ORU-10027: buffer overflow, l i m i t of 20000 bytes ORA-06512: a t "SYS.DBMSJDUTPUT", l i n e 106 ORA-06512: a t "SYS.DBMS_OUTPUT", l i n e 65 ORA-06512: a t l i n e 3 Этот предел можно увеличить, выполнив команду SET SERVEROUTPUT (или вызвав процедуру DBMS_OUTPUT.ENABLE):
Пакет DBMSJDUTPUT
6 6 9
SQL> s e t serveroutput on size 1000000 SQL> s e t serveroutput on s i z e 1000001 SP2-0547: s i z e option 1000001 out of range (2000 through 1000000) Как видно из полученного сообщения об ошибке, однако, размер буфера должен быть в диапазоне от 20000 до 1000000 байт. Реально вы сможете поместить в буфер меньше данных, чем установленный предел. Пакет DBMS_OUTPUT использует простой алгоритм упаковки данных при помещении в PL/SQL-таблицу. Он не помещает i-ю строку в i-ый элемент массива, он плотно упаковывает массив. В первом элементе массива может оказаться первых пять строк, выданных с помощью этого пакета. Для этого (чтобы поместить несколько строк в одну) необходимо расходовать дополнительные ресурсы. Эти дополнительные ресурсы для размещения сведений о том, где находятся данные пользователя и какого они размера, выделяются из того же ограниченного пространства. Поэтому, даже выполнив команду SET SERVEROUTPUT ON SIZE 1000000, вы сможете выдать меньше миллиона байтов. Можно ли определить, сколько байтов можно будет выдать? Иногда — да, а иногда — нет. При фиксированном размере выдаваемой строки, когда все строки одинаковой длины, это можно определить. Можно точно рассчитать, сколько байтов удастся выдать. Если же выдаются строки переменной длины, то вычислить заранее, сколько байтов удастся выдать, нельзя. Ниже я представлю алгоритм, используемый сервером Oracle для упаковки данных. Мы знаем, что сервер Oracle сохраняет данные в массиве. Максимальное количество строк в этом массиве рассчитывается исходя из установки SET SERVEROUTPUT ON SIZE. Массив пакета DBMS_OUTPUT никогда не будет длиннее IDXLIMIT строк, где IDXLIMIT рассчитывается как: i d x l i m i t : - trunc((xxxxxx+499)
/ 500);
Итак, если выполнить SET SERVEROUTPUT ON SIZE 1000000, пакет DBMS_OUTPUT будет использовать не более 2000 элементов массива. Пакет DBMS_OUTPUT будет сохранять в каждом элементе массива не более 504 байт данных (обычно — меньше). Пакет DBMS_OUTPUT упаковывает данные в строку массива в следующем формате: Бу$ер(1) = '<sp>NNNBamH данные<зр>№ТОваши д а н н ы е . . . ' ; Буфер (2) = ' <зр>№й1ваши данные<зр>№й1ваши данные. . . ' ; Так что для каждой вьщаваемой строки будет использоваться дополнительно 4 байта — для одного пробела и трехзначного числа. Каждая строка в буфере DBMS_OUTPUT имеет длину не более 504 байт, и пакет DBMS_OUTPUT не будет переносить данные с одной строки на другую. Поэтому, например, если использовать максимальную длину строки и всегда выдавать строки по 255 байт, пакет DBMS_OUTPUT сможет упаковать в элемент массива только одну строку. Причина в том, что значение (255+4) * 2 = 518 больше, чем 504, а пакет DBMS_OUTPUT не будет делить строку на два элемента своего массива. Две строки такого размера просто не помещаются в одну строку массива DBMS_OUTPUT. Поэтому даже если затребовать буфер размером 1000000 байт, вы сможете поместить в него только 510000 байт данных — чуть больше половины запрошенного. Значение 510000 получено, исходя из того, что длина выдаваемых строк — 255 байт;
670
Приложение А
всего же строк может быть не более 2000 (вспомните представленное ранее вычисление значения IDXLIMIT); 255*2000 = 510000. С другой стороны, при использовании строк длиной 248 байт можно помещать по две строки в элемент массива, что позволит выдать 248 * 2 * 2000 = 992000 байт — чуть больше 99% запрошенного пространства. Фактически, это максимум того, на что можно рассчитывать при использовании пакета DBMS_OUTPUT — 992000 байт данных. Больше выдать с помощью этого пакета нельзя. Как уже было сказано, при использовании строк фиксированной длины очень легко подсчитать выдаваемое количество строк. Если известна фиксированная длина строки, например 79, 80 или 81 байт, легко все рассчитать: ops$tkyte@ORA8I.WORLD> s e l e c t trunc(504/(79+4)) * 79 * 2000 from dual; TRUNC(504/(79+4))*79*2000 948000 ops$tkyte@ORA8I.WORLD> s e l e c t trunc(504/(80+4)) * 80 * 2000 from dual; TRUNC(504/(80+4))*80*2000 960000 ops$tkyte@ORA8I.WORLD> select trunc(504/(81+4)) * 81 * 2000 from dual; TRUNC(504/(81+4))*81*2000 810000 Как видите, максимальный объем выдаваемых данных сильно зависит от размера выдаваемой строки. Проблема со строками переменной длины состоит в том, что максимальный объем результата предсказать нельзя. Он зависит от того, как идет выдача, от последовательности строк, получаемых пакетом DBMS_OUTPUT. Если выдавать одни и те же строки, но в другом порядке, их будет выдано больше или меньше. Это непосредственно связано с используемым алгоритмом упаковки. Эта особенность пакета DBMS_OUTPUT сбивает с толку. Вы можете выполнить процедуру один раз и успешно выдать отчет размером 700000 байт, а завтра та же процедура приведет к выдаче сообщения об ошибке ORA-20000: ORU-10027: buffer overflow после получения 650000 байт. Это связано со способом упаковки данных в буфере пакета DBMS_OUTPUT. Далее в этом разделе мы рассмотрим альтернативы пакету DBMS_OUTPUT, позволяющие избежать этой неоднозначности. Резонно задать вопрос: а зачем создатели пакета вообще делают эту упаковку? Причина в том, что, когда пакет DBMS_OUTPUT появился в версии 7.0, выделение памяти для PL/SQL-таблиц выполнялось совсем не так, как сейчас. При выделении слота в PL/SQL-таблице сразу же выделялась память для элемента максимального размера. Это означает, что, поскольку DBMS_OUTPUT использует элементы типа VARCHAR2(500), 500 байт будут выделены при вызове DBMS_OUTPUT.PUT_LINE('hello world"), т.е. тот же объем, что и при выдаче длинной строки. Результат, состоящий из 2000 строк, занял бы 1000000 байт, даже если выдать 2000 раз строку hello world, что фактически требует только около 22 Кбайт. Так что подобная упаковка была предусмотрена для предотвращения выделения лишней памяти в области PGA для буферного массива. В последних
Пакет DBMS_OUTPUT
671
версиях Oracle (начиная с 8.0) память выделяется по-другому. Размер элементов массива меняется динамически, и упаковка больше не нужна. Поэтому ее можно считать унаследованной от старых версий. Последнее, что хотелось бы сказать о работе пакета DBMS_OUTPUT, — это то, что начальные пробелы в выдаваемых строках удаляются. Ошибочно думать, что это "свойство" пакета DBMS_OUTPUT. Фактически, это "свойство" SQL*Plus (хотя, я знаю многих, кто склонен считать это ошибкой). Небольшой тест позволит понять, что я имею в виду: ops$tkvte@ORA8I.WORLD> exec dbms_output.put_line('
hello world');
hello world PL/SQL procedure successfully completed. При передаче пакету DBMS_OUTPUT строки ' hello world', начальные пробелы оказались удалены. Считается, что это делает пакет DBMS_OUTPUT, но на самом деле это не так. Усекает начальные пробелы утилита SQL*Plus. Простое решение этой проблемы — использовать расширенный синтаксис команды SET SERVEROUTPUT. Вот полный синтаксис этой команды: s e t s e r v e r o u t p u t {ON|OFF} [SIZE n] [FORMAT {WRAPPED|WORD_WRAPPED|TRUNCATED}] Значение конструкций формата выдачи строк представлено ниже. •
WRAPPED. Утилита SQL*Plus при необходимости переносит на новую строку выданные сервером результаты, начиная с позиции, задаваемой командой SET LINESIZE.
•
WORD_WRAPPED. Переносится каждая строка выданных сервером результатов, начиная с позиции, задаваемой командой SET LINESIZE. Перенос выполняется по словам. Утилита SQL*Plus выравнивает каждую строку влево, удаляя все начальные пробелы. Это значение является стандартным.
•
TRUNCATED. Каждая строка результатов сервера усекается до длины, задаваемой командой SET LINESIZE.
Действие каждой опции форматирования проще всего понять на примере: SQL>set linesize 20 SQL>set serveroutput on format wrapped SQL>exec dbms_output.put_line(' Hello World
Hello
World
!!!!!');
SQL>set serveroutput on format word_wrapped SQL>exec dbms_output.put_line(' Hello Hello World
World
!!!!!');
World
!!!!!');
inn
PL/SQL procedure successfully completed.
Mill
PL/SQL procedure successfully completed. SQL>set serveroutput on format truncated SQL>exec dbms_output.put_line(' Hello
672
Приложение А Hello
World
PL/SQL procedure successfully completed.
Пакет DBMSJDUTPUT в других средах Стандартные средства, такие как SQL*Plus и SVRMGRL, учитывают особенности работы пакета DBMS_OUTPUT. Большинство остальных сред — нет. Например, обычная программа на языке Java/JDBC о пакете DBMS_OUTPUT ничего "не знает". В этом подразделе мы рассмотрим, как учесть в такой программе особенности работы пакета DBMS_OUTPUT. Такой же подход можно применить в любой среде программирования. Методы, использованные мной для языка Java, можно применить в среде Рго*С, OCI, VB и в любой другой среде. Начнем с небольшой PL/SQL-процедуры, генерирующей данные для вывода: scott@TKYTE816> create or replace 2 procedure emp_report 3 as 4 begin 5 dbms_output.put_line 6 (rpad('Empno', 7) || 7 rpad('Ename1,12) || 8 rpad('Job',11)); 9 10 dbms_output.put_line 11 ' (rpadf- 1 , 5, '-') || 12 rpadf -',12,'-') || 13 rpadC -',11,'-')); 14 15 for x in (select * from emp) 16 loop 17 dbms_output.put_line 18 (to_char(x.empno, '9999') || ' 19 rpad(x.ename, 12) I| 20 rpad(x.job, 11)); 21 end loop; 22 end; 23 /
' ||
Procedure created. scott@TKYTE816> set serveroutput on format wrapped scott@TKYTE816> exec emp_report Empno Ename Job 7369 SMITH 7499 ALLEN
CLERK SALESMAN
7934
CLERK
MILLER
PL/SQL procedure successfully completed.
Пакет DBMS_OUTPUT
673
Теперь создадим класс, позволяющий работать с буфером пакета DBMS_OUTPUT в среде Java/JDBC: import java.sql.*; class DbmsOutput f
/*
* Переменные экземпляра. Всегда лучше использовать вызываемые, * или подготовленные, операторы и готовить (анализировать) * их один раз при выполнении программы, а не при каждом выполнении * оператора. Повторный анализ требует очень больших ресурсов. * Не забудьте также использовать СВЯЗЫВАЕМЫЕ ПЕРЕМЕННЫЕ! * * В этом классе мы используем три оператора. Один — для включения * DBMS_OUTPUT, аналог команды SET SERVEROUTPUT ON в SQL*Plus, * второй — для выключения, подобно SET SERVEROUTPUT OFF. * Третий — для "сброса" или выдачи результатов вызовов DBMS_OUTPUT * с помощью system.out. * */ private CallableStatement enable_stmt; private CallableStatement disable_stmt; private CallableStatement show_stmt; /* * * * * * * * * * * * * *
Конструктор готовит три оператора, которые предполагается выполнить. Оператор, который мы готовим для SHOW, — это блок кода для возврата строки результатов DBMS_OUTPUT. Обычно можно использовать тип PL/SQL-таблицы, но JDBC-драйверы не поддерживают типы PL/SQL-таблиц. Поэтому мы получаем результат и конкатенируем его в строку. Будем выбирать не более одной строки результата, так что можем превзойти значение параметра MAXBYTES. Если установить MAXBYTES равным 10, а первая строка имеет длину 100 байт, вы получите 100 байт. Параметр MAXBYTES не даст получить следующую строку, но разбиения строки не произойдет.
*/ public DbmsOutput(Connection conn) throws SQLException { enable_stmt = conn. prepareCall( "begin dbms_output. enable (:1); end;"); disable_stmt - conn.prepareCall("begin dbms_output.disable; end;"); show_stmt = conn.prepareCall( "declare - + " l_line varchar2(255); " + " l_done number; " + 11 l_buffer long; " + "begin " + " loop " + " exit when length(l_buffer)+255 > :maxbytes OR l_done = 1; " +
22 Зак. 244
0/4
Приложение А "
dbms_output.get_line(l_line, l_done); " + l_buffer := l_buffer II l_line || chr(10); " + " end loop; " + " :done := l_done; " + " :buffer := l_buffer; " + "end;");
} /* * ENABLE задает размер и выполняет * вызов DBMS_OUTPUT.ENABLE * */ public void enable(int size) throws SQLException { enable_stmt.setlnt(1, size); enable_stmt.executeUpdate(); } /* * DISABLE просто вызывает DBMS_OUTPUT.DISABLE */ public void disable() throws SQLException { disable_stmt.executeUpdate() ; } /* * Функция SHOW выполняет основную работу. Она циклически * выбирает данные DBMS_OUTPUT, * по 32000 байт за раз (плюс-минус 255 байт). * По умолчанию она выдает результат в стандартный * выходной поток (для перенаправления результатов * достаточно перенаправить System.out). */ public void show() throws SQLException < int
done = 0; show_stmt.registerOutParameter(2, java.sql.Types.INTEGER); show_stmt.registerOutParameter(3, j ava.sql.Types.VARCHAR); for(;;) <
show_stmt.setlnt(1, 32000); show_stmt.executeUpdate(); System.out.print(show_stmt.getString(3)); if ((done = show_stmt.getlnt(2)) = 1) break;
/* * Функция CLOSE закрывает все выполняемые операторы, связанные * с классом DbmsOutput. Ее надо вызывать, если вы создали оператор
Пакет DBMS_OUTPUT
675
* DbmsOutput и он выходит из области действия, как и для любого * вызываемого оператора, результирующего множества и т.п. */ public void close() throws SQLException enable_stmt.close(); disable_stmt.close(); show stmt.close();
Чтобы продемонстрировать использование этого класса, я создал небольшую тестовую программу на языке Java/JDBC. Здесь dbserver — имя сервера базы данных, a ora8i — имя службы, соответствующей экземпляру: import java.sql.*; class test { public static void main (String args []) throws SQLException DriverManager.registerDriver (new oracle.j dbc.driver.OracleDriver()); Connection conn = DriverManager.getConnection ("j dbc:oracle:thin:Sdbserver:1521:ora8i", "scott", "tiger"); conn.setAutoCommit (false); Statement stint = conn. createStatement () ; DbmsOutput dbmsOutput = new DbmsOutput(conn); dbmsOutput.enable(1000000); stmt.execute ("begin emp_report; end;"); stmt.close(); dbmsOutput.show(); dbmsOutput.close(); conn.closeO;
}
}
Теперь протестируем программу, скомпилировав и выполнив ее:
$ javac test.Java $ Java test Empno
Ename
Job
7369 7499 7521
SMITH ALLEN WARD
CLERK SALESMAN SALESMAN
676
Приложение А
Итак, это показывает, как в языке Java использовать средства пакета DBMS_OUTPUT. Как и в случае утилиты SQL*Plus, необходимо вызывать DbmsOutput.show() после выполнения оператора, выдающего какие-либо результаты. После выполнения оператора INSERT, UPDATE, DELETE или вызова хранимой процедуры утилита SQL*Plus вызывает подпрограмму DBMS_OUTPUT.GET_LINES для получения результата. Приложение на языке Java (или С, или VB) должно вызывать функцию SHOW для выдачи результатов.
Обход ограничений Я обнаружил два основных ограничения пакета DBMS_OUTPUT: Q Длина "строки" ограничена 255 байтами. Символ новой строки надо вставлять не реже, чем через 255 байт. •
Общий объем выдаваемых результатов ограничен и находится в пределах от 200000 байт (если выдавать по одному байту в строке) до 992000 байт (если выдавать по 248 байт в строке). Для некоторых действий этого хватает, для других — недостаточно, особенно если учесть, что общий объем результатов, которые можно выдать, зависит от длины и порядка выдачи строк.
Итак, что можно сделать? В следующих подразделах я предложу три способа обойти эти ограничения.
Использование небольшой функции-оболочки или другого пакета Иногда 255 байт не хватает. Необходимо выдать отладочную информацию, и получается строка длиной 500 символов. Ее надо выдать, при этом не так важен формат, как возможность получить результаты. В этом случае можно написать небольшую подпрограмму-оболочку. Во всех моих базах данных установлена такая подпрограмма, позволяющая обойти ограничение длины строки и заодно сократить вызов, поскольку строка DBMS_OUTPUT.PUT_LINE состоит из 20 символов, что многовато для постоянного набора. Я часто использую процедуру Р. Вот эта процедура: procedure p(p_string in varchar2) is l_string long default p_string; begin loop e x i t when l _ s t r i n g i s n u l l ; dbms_output.put_line(substr(l_string, l _ s t r i n g := substr(l_string, 251); end loop; end;
1, 248));
Она не переносит переданную строку по словам и вообще ничего особенного не делает. Она принимает строку размером до 32 Кбайт и выдает ее. Длинную строку она разобьет на ряд строк размером 248 байт каждая (248 — оптимальное значение, кото-
Пакет DBMSJDUTPUT
677
рое мы вычислили ранее; оно позволяет выдать максимальный объем результатов). Процедура меняет данные (поэтому она не подходит для увеличения длины строки в процедуре, создающей текстовый файл), выдавая переданную строку в виде нескольких строк. Процедура решает простую проблему. Она позволяет избавиться от сообщения об ошибке: ops$tkyte@ORA8I.WORLD> exec d b m s _ o u t p u t . p u t _ l i n e ( r p a d ( ' * ' , 2 5 6 , ' * ' ) ) BEGIN d b m s _ o u t p u t . p u t _ l i n e ( r p a d ( ' * ' , 2 5 6 , ' * ' ) ) ; END;
* ERROR a t l i n e 1:
ORA-20000: ORA-06512: ORA-06512: ORA-06512:
ORU-10028: line length overflow, limit of 255 bytes per line at "SYS.DBMS_OUTPUT", line 99 at "SYS.DBMS_OUTPUT", line 65 at line 1
получаемого при выдаче отладочной информации или печати отчета. Более надежный способ обойти это ограничение, особенно при сбросе данных в текстовый файл, — использовать вместо пакета DBMS_OUTPUT средства пакета UTL_FILE для записи непосредственно в файл. Пакет UTL_FILE ограничивает размер выдаваемой строки 32 Кбайтами и не ограничивает размер файла. С помощью пакета UTL_FILE можно создавать файлы только на сервере, так что это решение не подойдет, если предполагается использование утилиты SQL*Plus на клиенте, подключенном по сети, с выдачей результатов в локальный файл на клиенте. Если же необходимо создать текстовый файл данных для загрузки и его создание на сервере допустимо, пакет UTL_FILE вполне можно использовать. Итак, мы рассмотрели две из трех возможностей. Переходим к последней.
Создание аналога пакета DBMS_OUTPUT Это универсальное решение, хорошо работающее во всех средах. Мы собираемся заново изобрести велосипед, но велосипед более совершенный. Создадим пакет-аналог DBMS_OUTPUT, который: •
ограничивает длину строки 4000 байтами (это, к сожалению, ограничение языка SQL, а не PL/SQL);
•
не ограничивает количество выдаваемых строк;
•
позволяет получать результаты на клиенте, как и пакет DBMS_OUTPUT;
•
не позволяет утилите SQL*Plus удалять начальные пробелы в строке, независимо от режима;
•
позволяет выбирать результаты как результирующее множество на клиенте с помощью курсора (к результату можно будет делать запросы).
Начнем с создания SQL-типа. Этот тип будет использоваться для буфера, аналогичного тому, что применяется в пакете DBMS_OUTPUT. Поскольку это SQL-тип, можно применять к данным операторы SELECT *. Поскольку практически в любой среде можно выполнить оператор SELECT *, выдать результаты не представит сложности.
678
Приложение А
ops$tkyte@ORA8I.WORLD> create or replace type my_dbms_output_type 2 as t a b l e of varchar2(4000) 3 / Type created. Теперь переходим к спецификации пакета-аналога DBMS_OUTPUT. Этот пакет устроен подобно DBMS_OUTPUT. В нем нет только подпрограмм GET_LINE и GET_LINES, поскольку в нашей реализации они не нужны. Процедуры PUT, PUT_LINE и NEW_LINE работают точно так же, как их аналоги в пакете DBMS_OUTPUT. Функции GET, FLUSH и GET_AND_FLUSH — новые. Аналогов для них в пакете DBMS_OUTPUT нет. Эти функции используются для получения результата после выполнения хранимой процедуры. Функция GET будет просто возвращать данные из буфера, но не "стирать" их. Можно вызывать функцию GET повторно для получения одного и того же содержимого буфера (пакет DBMS_OUTPUT всегда сбрасывает буфер). Функция FLUSH позволяет сбросить буфер, другими словами, очистить его. Функция GET_AND_FLUSH, как можно догадаться, возвращает содержимое буфера и очищает его; следующие вызванные подпрограммы пакета будут работать с пустым буфером: tkyte@TKYTE816> create or replace package my_dbms_output 2 as 3 procedure enable; 4 procedure disable; 5 6 procedure put(s in varchar2); 7 procedure put_line(s in varchar2); 8 procedure new_line; 9 10 function get return my_dbms_output_type; 11 procedure flush; 12 function get_and_flush return my_dbms_output_type; 13 end; 14 / Package created. Используем некоторые из методов, которые рассмотрены в главе 20, посвященной использованию объектно-реляционных средств. В частности, используем возможность выполнять операторы SELECT * from PLSQL_FUNCTION — именно так и будет работать аналог пакета DBMS_OUTPUT. Наибольший интерес представляют подпрограммы ENABLE, DISABLE, PUT, PUT_LINE и NEW_LINE. Они работают более-менее похоже на одноименные подпрограммы пакета DBMS_OUTPUT. Основное различие в том, что процедура ENABLE не имеет параметров, а пакет MY_DBMS_OUTPUT по умолчанию выдает результаты (тогда как пакет DBMS_OUTPUT по умолчанию их не выдает). Выдаваемые результаты ограничены объемом оперативной памяти, который вы можете выделить в системе (учтите это!). Рассмотрим тело пакета. Реализация этого пакета очень проста. Имеется глобальная переменная пакета, используемая в качестве буфера для результатов. Мы добавляем строки текста в буфер, выделяя при необходимости дополнительную память. Чтобы сбросить буфер, присваиваем ему пустую таблицу. Поскольку все так просто, я представлю реализацию без комментариев:
Пакет DBMSJDUTPUT
679
tkyte@TKYTE816> create or replace package body my_dbms_output 2 as 3 4 g_data my_dbms_output_type := my_dbms_output_type(); 5 g_enabled boolean default TRUE; 6 7 procedure enable 8 is 9 begin 10 g_enabled := TRUE; 11 end; 12 13 procedure disable 14 is 15 begin 16 g_enabled := FALSE; 17 end; 18 19 procedure put(s in varchar2) 20 is 21 begin 22 if (NOT g_enabled) then return; end if; 23 if (g_data.count <> 0) then 24 g_data(g_data.last) := g_data(g_data.last) || s; 25 else 26 g_data.extend; 27 g_data(l) := s; 28 end if; 29 end; 30 31 procedure put_line(s in varchar2) 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
begin if (NOT g_enabled) then return; end if; put(s); g_data.extend; end; procedure new_line is begin if (NOT g_enabled) then put(null); g_data.extend; end;
im;
end
if;
procedure flush is l_empty my_dbms_output_type : = my_dbms_output_type () ; begin g_data := l_empty;
680
Приложение А
53 end; 54 55 function get return my_dbms_output_type 56 is 57 begin 58 return g_data; 59 end; 60 61 function get_and_flush return my_dbms_output_type 62 is 63 l_data my_dbms_output_type := g_data; 64 l_empty my_dbms_output_type : = my_dbms_output_type () ; 65 begin 66 g_data := l_empty; 67 return l_data; 68 end; 69 end; 70 / Package body created.
Теперь, чтобы сделать пакет действительно полезным, необходим простой метод получения содержимого буфера. Можно вызывать функции MY_DBMS_OUTPUT.GET или GET_AND_FLUSH и выбирать содержимое переменной объектного типа самостоятельно или использовать одно из созданных ниже представлений. Первое представление, MY_DBMS_OUTPUT_PEEK, обеспечивает SQL-интерфейс к функции GET. Оно позволяет многократно запрашивать данные из буфера результатов, фактически обеспечивая просмотр буфера без сброса результатов. Второе представление, MY_DBMS_OUTPUT_VIEW, позволяет выполнить запрос к буферу один раз — любые последующие вызовы подпрограмм PUT, PUT_LINE, NEW_LINE, GET или GET_AND_FLUSH будут работать с пустым буфером результатов. Оператор SELECT * FROM MY_DBMS_OUTPUT_VIEW аналогичен вызову функции DBMS_OUTPUT.GET_LINES. Буфер сбрасывается: tkyte@TKYTE816> create or replace 2 view my_dbms_output_peek (text) 3 as 4 select * 5 6 7
from TABLE (cast(my_dbms_output.get() as my_dbms_output_type)) /
View created. tkyte@TKYTE816> create or replace 2 view my_dbms_output_view (text) 3 as 4 select * 5 from TABLE (cast(my_dbms_output.get_and_flush() 6 as my_dbms_output_type)) 7 / View created.
Пакет DBMSJDUTPUT
6 8 1
Теперь все готово для демонстрации работы этого решения. Выполним процедуру, генерирующую данные в буфер, а затем посмотрим, как их выдать и что с ними можно делать: tkyte@TKYTE816> begin 2 my_dbms_output.put_line('hello'); 3 my_dbms_output.put('Hey ' ) ; 4 my_dbms_output.put('there ' ) ; 5 my_dbms_output.new_line; 6 7 for i in 1 .. 20 loop 9 my_dbms_output.put_line(rpad( ', i, 10 end loop; 11 end; 12 /
') II i ) ;
PL/SQL procedure successfully completed. tkyte@TKYTE816> select * 2 from my_dbms_output_peek 3 / TEXT hello Hey there 1 2 19 20 23 rows selected. Интересно, что утилита SQL*Plus, создатели которой ничего не знали о пакете MY_DBMS_OUTPUT, не выдает результаты автоматически. Надо ей помочь, выполнив запрос, выдающий результаты. Поскольку для получения результатов используются SQL-операторы, вы легко сможете написать собственный класс DbmsOutput на языке Java/JDBC. Это будет простой объект ResuItSet — ничего больше. В качестве последнего комментария к этому коду скажу, что результаты ожидают выборки в буфере: tkyte@TKYTE816> s e l e c t * 2 from my_dbms_output_j?eek 3 / TEXT hello Hey there 1 2
UO.Z
Приложение А
19 20 23 rows s e l e c t e d . Более того, при выборке можно задавать конструкцию WHERE, сортировать результаты, соединять их с другими таблицами и т.д. (как и для данных любой таблицы): tkyte@TKYTE816> s e l e c t * 2 from my_dbms_outputj?eek 3 where t e x t l i k e '%1%' 4 / TEXT
. .. 18 19 11 rows selected. Если же повторное обращение к данным нежелательно, можно выбрать результаты с помощью оператора SELECT из представления MY_DBMS_OUTPUT_VIEW: tkyte@TKYTE816> s e l e c t * 2 from my_dbms_output_view 3
/
TEXT hello Hey there
I
19 20 23 rows selected # tkyte@TKYTE816> select * 2 from my dbms output view 3 / no rows selected В этом случае данные можно получить только один раз. Эта новая реализация пакета DBMS_OUTPUT увеличивает допустимую длину строки с 255 байт до 4000 и фактически снимает ограничение на общий объем выдаваемых результатов (вы, однако, ограничены объемом оперативной памяти сервера). Она также предоставляет ряд новых возможностей (можно делать запросы к результатам, сортиро-
Пакет DBMS OUTPUT
683
вать их и т.д.). Она позволяет избавиться от стандартного удаления начальных пробелов в среде SQL*Plus. Наконец, в отличие от пакета UTL_FILE результаты MY_DBMS_OUTPUT можно сбросить в файл на клиенте точно так же, как и результаты пакета DBMS_OUTPUT, что делает пакет MY_DBMS_OUTPUT достойной заменой DBMS_OUTPUT для удаленного клиента. Вы можете спросить, почему я использовал при реализации объектный тип, а не временную таблицу. Причина в объеме кода и дополнительном расходе ресурсов. Объем кода для управления временной таблицей, связанного с добавлением столбца для запоминания порядка данных, по сравнению с этой простой реализацией окажется существенно больше. Кроме того, работа с временной таблицей требует ввода-вывода и дополнительного расхода ресурсов. Наконец, сложно реализовать "представление со сбросом", когда буфер результатов автоматически очищается при выборке данных. Короче, использование объектного типа облегчает реализацию. Если бы я собирался использовать этот пакет для выдачи десятков мегабайт результатов, то пересмотрел бы способ буферизации и использовал временную таблицу. Для средних же объемов данных эта реализация вполне подходит.
Резюме В этом разделе мы рассмотрели пакет DBMS_OUTPUT. Теперь, зная, как он работает, вы не пострадаете от побочных эффектов этой реализации. Вы будете готовы к тому, что между запрошенным размером буфера и суммарным объемом результатов, которые можно в него выдать, иногда нет очевидной зависимости. Вы будете знать, что выдать строку результатов длиной более 255 байт нельзя. Результаты DBMS_OUTPUT не выдаются, пока не завершится выполнение процедуры или оператора, но и тогда они будут выданы только при условии, что среда, из которой выполняются запросы, поддерживает пакет DBMS_OUTPUT. Помимо анализа особенностей пакета DBMS_OUTPUT мы рассмотрели способы обойти ограничения, связанные с его применением: для этого рекомендуется использование других средств. Можно использовать пакет UTL_FILE для создания текстовых файлов с результатами или процедуры типа Р, не только уменьшающие количество набираемых символов, но и обеспечивающие выдачу длинных строк. Можно реализовать и собственный пакет с аналогичными функциями, не имеющий подобных ограничений. Пакет DBMS_OUTPUT — удачный пример того, как тривиальный на первый взгляд компонент может оказаться сложной программой с неожиданными побочными эффектами. Когда читаешь описание пакета DBMS_OUTPUT в руководстве Oracle Supplied PL/SQL Packages Reference, все кажется простым и понятным. А потом неожиданно возникают проблемы с суммарным объемом выдаваемых результатов и т.п. Знание особенностей реализации пакета помогает избежать этих проблем.
Пакет DBMS_PROFILER
Появления стандартного средства профилирования ждали давно (по крайней мере, я). Пакет DBMS_PROFILER представляет собой профилировщик исходного кода для PL/SQL-приложений. Раньше приходилось настраивать производительность PL/SQLприложений с помощью средств SQL_TRACE и TKPROF. Они помогали выявить долго выполняющиеся SQL-операторы, но определить узкие места в в PL/SQL-коде из 5000 строк (особенно, если он написан кем-то другим) было практически невозможно. Чтобы определить проблемные фрагменты кода, приходилось вставлять в него множество вызовов функции DBMS_UTILITY.GET_TIME для измерения времени выполнения. Теперь этого делать не нужно: можно воспользоваться возможностями пакета DBMS_PROFILER. Я собираюсь продемонстрировать, как его обычно используют. Лично я использую небольшую часть функциональных возможностей этого пакета: нахожу с его помощью проблемные фрагменты и занимаюсь ими непосредственно. Я использую пакет DBMS_PROFILER очень примитивным способом. Он позволяет, однако, делать намного больше, чем представлено в этом разделе. Статистическая информация собирается в таблицы базы данных. Они позволяют сохранять статистическую информацию за несколько "прогонов" кода. Некоторых это устраивает, но я предпочитаю сохранять результаты одного-двух последних прогонов. Дополнительная информация лишь сбивает с толку. Иногда информации бывает слишком много. Администратору базы данных, возможно, придется установить профилировщик в базе данных. Процедура установки этого пакета проста: •
cd [ORACLE_HOME]/rdbms/admin;
Пакет DBMS_PROFILER •
6 8 5
с помощью SVRMGRL (или SQL*Plus — прим. научн. ред.) подключиться как SYS или INTERNAL;
Q выполнить сценарий profload.sql. После этого надо установить таблицы статистической информации. Их можно установить в базе данных в одном экземпляре, но я рекомендую каждому разработчику создать свой набор этих таблиц. К счастью, пакет DBMS_PROFILER создан с правами вызывающего и использует неуточненные имена таблиц, так что таблицы статистической информации можно установить в каждой схеме, и они будут корректно использоваться пакетом-профилировщиком. При использовании собственных таблиц каждый разработчик будет видеть только свои результаты профилирования, а не результаты коллег по работе. Чтобы создать таблицы статистической информации в своей схеме, надо выполнить сценарий [ORACLE_HOME]\rdbms\admin\proftab.sql в SQL*Plus. После выполнения сценария proftab.sql надо выполнить сценарий profrep.sql. Этот сценарий создает представления и пакеты для создания отчетов по таблицам профилировщика. Сценарий profrep.sql находится в файле [ORACLE_HOME]\plsql\demo\profrep.sql. Этот сценарий надо выполнить в своей схеме после создания таблиц. Я обычно создаю небольшой сценарий для очистки таблиц профилировщика и выполняю эту очистку постоянно. После одного-двух тестовых прогонов и анализа результатов я выполняю этот сценарий. В сценарии, который я назвал profreset.sql, выполняется следующее: — используются операторы d e l e t e , поскольку таблицы связаны ограничением внешнего ключа delete from plsql_profiler_data; delete from plsql_profiler_units; delete from plsql_j>rofiler_runs; Теперь можно начинать профилирование. Я собираюсь продемонстрировать использование этого пакета путем запуска двух разных реализаций алгоритма вычисления факториала. Один алгоритм — рекурсивный, а другой — итеративный. Для того чтобы определить, какой из них работает быстрее и какие фрагменты кода наиболее медленные в каждой реализации, используется профилировщик. Тестирование выполняется следующим образом: tkyte@TKYTE816> @profreset tkyte@TKYTE816> c r e a t e or replace 2 function fact_recursive(n i n t ) return number 3 as 4 begin 5 if (n =1) 6 then 7 return 1 8 else 9 return n fact_recursive(n-1); 10 end if; 11 end; 12 / Function created.
686
Приложение А
tkyte@TKYTE816> create or replace 2 function fact_iterative(n int) return number 3 as 4 l_result number default 1; 5 begin 6 for i in 2 .. n 7 loop 8 l_result := l_result * i; 9 end loop; 10 return l_result; 11 end; 12 / Function created. tkyte@TKYTE816> set serveroutput on tkyte@TKYTE816> exec dbms_profiler.start_profiler('factorial recursive') PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 for i in 1 .. 50 loop 3 dbms_output.put_line(fact_recursive(50)); 4 end loop; 5 end; 6 / 30414093201713378043612608166064768844300000000000000000000000000 30414093201713378043612608166064768844300000000000000000000000000 PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dbms_profiler.stop_profiler PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dbms_profiler.start_profiler('factorial iterative') PL/SQL procedure successfully completed. tkyte@TKYTE816> begin 2 for i in 1 .. 50 loop 3 dbms_output.put_line(fact_iterative(50)); 4 end loop; 5 end; 6 / 30414093201713378043612608166064768844300000000000000000000000000 30414093201713378043612608166064768844300000000000000000000000000 PL/SQL procedure successfully completed. tkyte@TKYTE816> exec dbms_profiler.stop_profiler PL/SQL procedure successfully completed. Для сбора статистической информации очередного прогона профилировщика необходимо вызвать процедуру START_PROFILER. Каждому прогону мы даем определен-
Пакет DBMS_PROFILER
687
ное имя, а затем выполняем соответствующий код. Я вызывал каждую из функций вычисления факториала 50 раз, прежде чем завершить сбор статистической информации для прогона. Теперь все готово для анализа результатов. В каталоге [ORACLE_HOME]/plsql/demo есть сценарий profsum.sql. He запускайте его: некоторые запросы этого сценария выполняются очень долго (иногда — несколько часов), и он выдает очень много данных. (Ниже представлен измененный сценарий profsum.sql, который использую я; он выдает почти ту же информацию, но запросы выполняются очень быстро, а многие отчеты с избыточной детализацией просто не создаются.) Кроме того, одни запросы учитывают время выполнения вызова STOP_PROFILER, другие — нет. Это затрудняет сравнение результатов запросов. Я изменил все запросы так, чтобы время выполнения вызовов пакета профилировщика нигде не учитывалось. Мой сценарий profsum.sql представлен ниже. Кроме того, он доступен для загрузки на сайте http://www.apress.com: set set set set set
echo off linesize 5000 trimspool on serveroutput on termout off
column column column column column column column column column column column
owner format all unit_name format al4 text format a21 word_wrapped runid format 9999 sees format 999.99 hsecs format 999.99 grand_total format 9999.99 run_comment format all word_wrapped line* format 99999 pet format 999.9 unit_owner format all
spool profsum.out /* Очистка и пересоздание итоговых результатов. */ update plsql_profiler_units set total_time = 0; execute prof report utilities.rollup all runs; — — — — prompt = prompt = prompt = — — prompt Суммарное время select grand_total/1000000000 as grand_total from plsql_profiler_grand_total; prompt = prompt = prompt ... ===== = prompt Суммарное время каждого прогона select runid, substr(run comment,!, 30) as run comment,
688
Приложение А
run_total_time/1000000000 as sees from (select a.runid, sum(a.total_time) run_total_time, b. run_comirient from plsql_j?rofiler_units a, plsql_profiler_runs b where a.runid = b.runid group by a.runid, b.run_comment) where run_total_time > 0 order by runid asc; prompt = prompt = prompt =— = prompt Процент времени, приходящийся на каждый модуль, отдельно для ^* каждого прогона select pi.runid, substr(p2.run_comment, I, 20) as run_comment, pi.unit_owner, decode(pi.unit_name, '', '', substr(pl.unit_name,1, 20)) as unit_name, pl.total_time/1000000000 as sees, TO_CHAR(100*pl.total_time/p2.run_total_time, '999.9') as percentage from plsql_profiler_units pi, (select a.runid, sum(a.total_time) run_total_time, b.run_comment from plsql_profiler_units a, plsql_profiler_runs b where a.runid = b.runid group by a.runid, b.run_comment) p2 where pi.runid=p2.runid and pi.total_time > 0 and p2.run_total_time > 0 and (pl.total_time/p2.run_total_time) >= .01 order by pi.runid asc, pi.total_time desc; column sees form 9.99 prompt = prompt = prompt = • = = prompt Процент времени, приходящийся на каждый модуль, суммарно по всем ^* прогонам select pl.unit_owner, decode(pi.unit_name, '', '', substr(pl.unit_name,l, 25)) as unit_name, pl.total_time/1000000000 as sees, TO_CHAR(100*pl.total_time/p2.grand_total, '99999.99') as percentage from plsql_profiler_units_cross_run pi, plsql_profiler_grand_total p2 order by pi.total_time DESC; prompt = prompt = prompt — — = = prompt Строки, потребовавшие более 1% суммарного времени, отдельно по *• каждому прогону select pi.runid as runid, pl.total_time/10000000 as Hsecs, pl.total_time/p4.grand_total*100 as pet,
Пакет DBMS_PROFILER
689
substr(p2.unit_owner, 1, 20) as owner, decode(p2.unit_name, '', '', substr(p2.unit_name,l, 20)) as unit_name, pl.line#, (select p3.text from all_source p3 where p3.owner = p2.unit_owner and p3.1ine = pl.line# and p3.name=p2.unit_name and p3.type not in ('PACKAGE', 'TYPE')) text from plsql_profiler_data pi, plsqljprofiler_units p2, plsql_profiler_grand_total p4 where (pi.total_time >= p4.grand_total/100) AND pl.runID = p2.runid and p2.unit_number=pl.unit_number order by pi.total_time desc; prompt = prompt = prompt === ===== prompt Наиболее популярные строки (более 1%), суммарно по всем прогонам select pl.total_time/10000000 as hsecs, pl.total_time/p4.grand_total*100 as pet, substr(pl.unit_owner, 1, 20) as unit_owner, decode(pi.unit_name, '', '', substr(pl.unit_name,l, 20)) as unit_name, pl.linef, (select p3.text from all_source p3 where (p3.1ine = pl.line#) and (p3.owner = pl.unit_owner) AND (рЗ.пате = pl.unit_name) and (p3.type not in ('PACKAGE1, 'TYPE'))) text from plsqljprofiler_lines_cross_run pi, plsql_profiler_grand_total p4 where (pi.total_time >= p4.grand_total/100) order by pi.total_time desc; execute prof_report_utilities.rollup_all_runs; prompt = prompt = prompt = = = = = = = = = = = = = = prompt Количество реально выполненных строк в программных единицах (с *• группировкой по unit_name) select pi.unit_owner, pl.unit_name, count(decode( pl.total_occur, 0, null, 0)) as lines_executed , count(pi.line#) as lines_present, count(decode( pl.total_occur, 0, null, 0))/count(pl.line#) *100 as pet from plsql_profiler_lines_cross_run pi where (pi.unit_type in ('PACKAGE BODY1, "TYPE BODY1, 1 PROCEDURE', 'FUNCTION]))
О УU
Приложение А
group by pl.unit_owner, pl.unit_name; prompt prompt prompt prompt select from where
= = =— = Количество реально выполненных строк для всех программных единиц count(pl.linef) as lines_executed plsql_profiler_lines_cross_run pi (pl.unit_type in ('PACKAGE BODY', 'TYPE BODY', 1 PROCEDURE', 'FUNCTION')) AND pi.total_occur > 0;
prompt prompt prompt prompt select from where
= = —===== Общее количество строк во всех программных единицах count(pl.linef) as lines_present plsql_profiler_lines_cross_run pi (pl.unit_type in ('PACKAGE BODY', 'TYPE BODY', 1 PROCEDURE', 'FUNCTION'));
spool off set termout on edit profsum.out set linesize 131 Я постарался поместить отчет в стандартное окно терминала шириной 80 символов. Вы можете изменить формат некоторых столбцов, если не так часто используете программу Telnet. Рассмотрим результаты, которые получены при тестировании функций, вычисляющих факториал, т.е. результаты работы представленного выше сценария profsum.sql. Суммарное время GRANDJTOTAL 5.57 Суммарное время выполнения двух тестов составило 5,57 секунды. Теперь посмотрим, сколько выполнялся каждый тест. Суммарное время каждого прогона RUNID RUN COMMENT
SECS
17 factorial recursive
3 .26
18 factorial iterative
2 .31
Рекурсивная версия уступает по производительности — она выполнялась почти в полтора раза дольше. Теперь посмотрим, сколько выполнялся каждый модуль (пакет или процедура) в тесте и какой процент это составляет от общего времени выполнения.
Пакет DBMS_PROFILER
69 1
Процент времени, приходящийся на каждый модуль (отдельно для каждого прогона): RUNID RUN_COMMENT UNIT_OWNER UNIT_NAME
SECS PERCEN
17 factorial recursive
TKYTE
FACT_RECURSIVE
1.87
57.5
17 factorial recursive
SYS
DBMS_OUTPUT
1.20
36.9
17 factorial recursive
.08
2.5
17 factorial recursive
.06
1.9
18 factorial iterative
SYS
DBMS_OUTPUT
1.24
53.6
18 factorial iterative
TKYTE
FACT_ITERATIVE
.89
38.5
18 factorial iterative
.08
3.4
18 factorial iterative
.06
2.7
8 rows selected. По этим данным видно, что в рекурсивной реализации 57% времени приходится на выполнение нашей функции, 37% — на выполнение процедуры DBMS_OUTPUT, a остальное время выполняются прочие подпрограммы. Во втором тесте результаты существенно отличаются. На выполнение нашего кода пришлось лишь 38% суммарного времени, причем это проценты от существенно меньшего времени! Это убедительно показывает, что вторая реализация эффективней первой. Столбец SECS содержит еще более показательные результаты. Как видите, рекурсивная функция выполнялась 1,87 секунды, а итеративная — 0,89. Если проигнорировать выполнение операторов DBMS_OUTPUT, окажется, что итеративная функция работает вдвое быстрее, чем рекурсивная. Учтите, что результаты в вашей системе могут отличаться. Если не выполнить команду SERVEROUTPUT ON в SQL*Plus, например, вызовы DBMS_OUTPUT могут даже не попасть в отчет. Если выполнять тесты на других машинах, значения будут существенно отличаться. Например, при выполнении тестов на машине Sparc Solaris, суммарное время (GRAND_TOTAL) составило около 1,0 секунды, а время выполнения каждого раздела кода отличалось. В процентах, тем не менее, конечные результаты практически совпали. Теперь рассмотрим, сколько времени суммарно в тестах выполнялся каждый модуль. Это покажет, какой фрагмент кода выполнялся дольше всего.
692
Приложение А
Процент времени, приходящийся на каждый модуль, суммарно по всем прогонам: UNIT OWNER UNIT NAME
SECS PERCENTAG
SYS TKYTE TKYTE SYS
2.44 1.87 .89 .33 .04
DBMS_OUTPUT FACT_RECURSIVE FACT_ITERATIVE DBMS_PROFILER
43.82 33.61 16.00 5.88 .69
Очевидно, что время выполнения можно уменьшить почти вдвое, убрав один вызов DBMS_OUTPUT. Если просто выполнить SET SERVEROUTPUT OFF, отключив выполнение DBMS_OUTPUT, и повторно выполнить тесты, окажется, что на эту процедуру приходится менее 3% общего времени выполнения. Сейчас, однако, именно эта процедура выполнялась дольше всего. Что еще интереснее — 33% времени заняло выполнение рекурсивной функции и 16% — итеративной. Итеративная функция работает намного быстрее. Теперь рассмотрим более детальную информацию. Строки, для выполнения которых потребовалось более 1% суммарного времени, *•* отдельно по каждому прогону: RUNID
HSECS
17 18 17 17 18 18 17 18 18 17 18 17 17 18 18 17 17 18 18 18 17 18
142.47 68.00 43.29 19.58 19.29 17.66 14.76 14.49 13.41 13.22 10.62 10.46 8.11 8.09 8.02 8.00 7.52 7.22 6.65 6.21 6.13 5.77
22 rows
PCT OWNER UNIT_NAME 25.6 12.2 7.8 3.5 3.5 3.2 2.7 2.6 2.4 2.4 1.9 1.9 1.5 1.5 1.4 1.4 1.4 1.3 1.2 1.1 1.1 1.0
TKYTE TKYTE TKYTE SYS TKYTE SYS SYS SYS SYS SYS SYS SYS SYS SYS SYS SYS SYS SYS
FACT_RECURSIVE FACT_ITERATIVE FACT_RECURSIVE DBMS_OUTPUT FACT_ITERATIVE DBMSjOUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMS_OUTPUT DBMSJDUTPUT
LINE TEXT 8 7 4 116 5 116 118 118 142 142 166 166 72 144 72 144 3 3 141 1 1 81
return n*fact_recursive(n-l); l_result := l _ r e s u l t * i ; if ( n = 1 ) а З аО 5 1 a 5 l c 6 e 8 1 bO for i in 2 .. n а З аО 5 1 a 5 l c 6 e 8 1 bO l c 5 1 8 1 bO а З аО l c 5 1 l c 5 1 8 1 bO а З аО l c 5 1 : 2 aO a 5 b b 4 2 e d b 7 1 9 : 2 aO a 5 b b 4 2 e d b 7 19 6e b 4 2 e d : 2 aO 7 e 5 1 b 4 6e b 4 2 e d : 2 aO 7 e 5 1 b 4 1TO_CHAR: 8 f aO bO 3 d b 4 5 5 6 a : 3 aO 1TO_CHAR: 8 f aO bO 3 d b 4 5 5 6 a : 3 aO
aO bO 3 d b 4 5 5 6 a : 3 aO 7e
1ORU-10028:: l i n e
length
selected.
Здесь выдается время выполнения (в сотых долях секунды) и процент от общего времени выполнения. В этих результатах нет ничего удивительного: можно было предположить, что дольше всего будет выполняться строка 8 рекурсивной и строка 7 итеративной функции. Это предположение подтверждается. В этой части кода показываются
Пакет DBMS PROFILER
693
конкретные строки кода, на которые надо обратить внимание. Обратите внимание на странного вида строки кода, начинающиеся с DBMS_OUTPUT. Так выглядит скрытый PL/SQL-код в базе данных. Это просто последовательность байтов, представляющая исходный код и скрывающая его от любопытных глаз. В следующей части отчета представлены суммарные результаты по всем тестам, тогда как в предыдущей части проценты вычислялись для каждого теста отдельно. Наиболее "популярные" строки (более 1%), суммарно по всем прогонам: HSECS
142.47 68.00 43.29 37.24 29.26 26.63 21.08 19.29 16.88 16.13 16.09 14.74 11.28 10.17 9.52 8.54 7.36 6.25 6.19 5.77
PCT OWNER UNIT NAME
25.6 12.2 7.8 6.7 5.3 4.8 3.8 3.5 3.0 2.9 2.9 2.6 2.0 1.8 1.7 1.5 1.3 1.1 1.1 1.0
TKYTE TKYTE TKYTE SYS SYS SYS SYS TKYTE SYS SYS SYS SYS SYS SYS SYS SYS SYS SYS
FACT_ RECURSIVE FACT [ITERATIVE FACT_ RECURSIVE DBMS_ OUTPUT DBMS_ OUTPUT DBMS _OUTPUT DBMS_ OUTPUT FACT_ ITERATIVE DBMS__OUTPUT DBMS_ OUTPUT DBMS_ OUTPUT DBMS_[OUTPUT DBMS__OUTPUT DBMS__OUTPUT DBMS__OUTPUT DBMS__OUTPUT DBMS__OUTPUT DBMS OUTPUT
LINE TEXT
8 return n * fact_recursive(n-l); 7 l_result := l_result * i; 4 if ( n = 1 ) 116 аЗ аО 51 a5 lc 6e 81 bO 118 lc 51 81 ЬО аЗ аО lc 51 142 :2 aO a5 b b4 2e d b7 19 166 6e b4 2e d :2 aO 7e 51 b4 5 for i in 2 .. n 1 72 1TO_CHAR: 144 8f aO bO 3d Ь4 55 6a :3 aO 3 81 1ORU-10028:: line length overflow, 147 4f 9a 8f aO bO 3d b4 55 73 1DATE: 117 a3 aO lc 51 81 ЬО аЗ аО 141 aO bO 3d b4 55 6a :3 aO 7e 96 1WHILE: 65 1499: 145 7e aO b4 2e d aO 57 ЬЗ
20 rows selected. Наконец, рассмотрим статистическую информацию о частоте выполнения отдельных строк кода. Она пригодится не только при профилировании и настройке производительности, но и при тестировании. Эта часть отчета показывает, какие операторы в коде выполнялись и какой процент кода "покрыт" в ходе тестирования: Количество реально выполненных строк в программных единицах (с '-> группировкой по unit_name) UNIT OWNER
UNIT NAME
SYS SYS TKYTE TKYTE
DBMS_OUTPUT DBMS_PROFILER FACT ITERATIVE FACT RECURSIVE
LINES EXECUTED LINES PRESENT
51 9 4 3
88 62 4 3
PCT 58.0 14.5 100.0 100.0
Количество реально выполненных строк для всех программных единиц
694
Приложение А
LINES_EXECUTED 67 т
_
Общее количество строк во всех программных единицах LINES PRESENT 157
Из 88 операторов пакета DBMS_OUTPUT выполнены 51. Интересно, как пакет DBMS_PROFILER подсчитывает строки или операторы. Утверждается, что функция FACT_ITERATIVE содержит 4 строки кода, но если обратиться к исходному коду: function fact_iterative(n int) return number as l_result number default 1; begin for i in 2 . . n loop l_result := l_result * i; end loop; return l_result; end;
О каких четырех строках идет речь — непонятно. Пакет DBMS_PROFILER считает операторы, а не строки кода. Речь идет о следующих четырех операторах: ...
l_result number default 1; for i in 2 .. n 1 result := 1 result * i; return 1 result;
Остальные строки, хотя и необходимы для компиляции и выполнения кода, к выполняемому коду не относятся и поэтому операторами не считаются. Пакет DBMS_PROFILER можно использовать для того, чтобы определить количество операторов, выполняемых в коде и в тестах.
Проблемы Единственная проблема, с которой я сталкивался при использовании пакета DBMS_PROFILER, связана с большим объемом генерируемых им данных и временем анализа этих данных.
Пакет DBMS_PROFILER
695
Небольшой тест, который мы выполнили, сгенерировал более 500 строк статистической информации в таблице PLSQL_PROFILER_DATA. Эта таблица содержит одиннадцать числовых столбцов, так что она не очень "широкая", но растет быстро. При выполнении каждого оператора в таблицу добавляется строка. Надо контролировать пространство, занимаемое таблицей, периодически удаляя из нее строки. Обычно эта проблема несущественна, но я видел, как при тестировании сложных PL/SQL-процедур в таблицу записывались тысячи (и даже сотни тысяч) строк. Время анализа результатов — более серьезная проблема. Сколько бы вы не настраивали производительность, всегда найдется строка кода, выполняющаяся дольше всего. Если удалить эту строку кода, ее место займет другая. Вы никогда не получите отчет DBMS_PROFILER, изучив который придете к выводу, что все работает прекрасно и настраивать больше нечего. Чтобы эффективно использовать это инструментальное средство, надо определить для себя, когда можно закончить настройку. Задайте либо определенное время настройки (например, эта процедура будет настраиваться в течение двух часов), либо критерий производительности (если процедура будет выполняться за N единиц времени, можно прекратить настройку). В противном случае вы будете (как и я иногда) тратить огромное время на настройку процедуры, которая просто не может работать быстрее. Пакет DBMS_PROFILER — замечательное средство, которое может выдать массу подробной информации. Старайтесь не погрязнуть в изучении всех этих деталей.
Резюме В этом разделе мы рассмотрели использование пакета DBMS_PROFILER. Он используется в основном для решения двух задач. При профилировании исходного кода можно найти строки, выполняющиеся дольше всего, или сравнить скорость работы двух алгоритмов. Можно также понять, какая часть кода охвачена при тестировании приложения. Хотя 100-процентный охват кода при тестировании не гарантирует его безошибочности, но приближает к ней. Мы также разработали отчет, построенный на базе предлагаемого корпорацией Oracle примера отчета профилировщика. Этот отчет выдает основную информацию, необходимую для успешного использования пакета DBMS_PROFILER. Он избавляет от необходимости изучать лишние детали, предоставляя итоговую информацию о том, что происходило в приложении, и подробно описывая наиболее неэффективные части. Этого отчета может оказаться вполне достаточно для выявления узких мест и настройки приложения.
•
Пакет DBMSJJTILITY
Пакет DBMS_UTILITY — это набор процедур различного назначения. В него помещено несколько отдельных, не связанных между собой процедур. Пакет DBMS_UTILITY стандартно устанавливается в базе данных, и привилегия EXECUTE для него предоставляется роли PUBLIC. Процедуры в этом пакете не взаимосвязаны, как в большинстве остальных пакетов. Например, все подпрограммы пакета UTL_FILE имеют общее назначение — выполнение ввода-вывода в файл. Подпрограммы в пакете DBMS_UTILITY практически независимы. Мы рассмотрим некоторые из этих подпрограмм, уделив особое внимание потенциальным проблемам при их использовании.
Процедура COMPILEJCHEMA Процедура COMPILE_SCHEMA предназначена для перекомпиляции недействительных (invalid) процедур, пакетов, триггеров, представлений, типов и других объектов схемы. Эта процедура работает в версии Oracle 8.1.6 на базе представления SYS.ORDER_OBJECT_BY_DEPENDENCY. Представление возвращает объекты в порядке зависимостей. Начиная с версии Oracle 8.1.7, это представление больше не используется (почему, будет показано далее). Если компилировать объекты в порядке, задаваемом этим представлением, объекты, которые можно успешно перекомпилировать, окажутся действительными (valid). Эта процедура выполняет оператор ALTER COMPILE от имени пользователя, который вызвал процедуру COMPILE_SCHEMA (т.е. она работает с правами вызывающего).
Пакет DBMS_UTILITY
697
Процедура COMPILE_SCHEMA требует передавать имена пользователей в верхнем регистре. Если вызвать: scott@TKYTE816> exec DBMS_UTILITY.compile_schema('scott'); скорее всего, ничего не произойдет, если при создании учетной записи имя пользователя scott не задано в нижнем регистре (как идентификатор в кавычках — прим. научн. ред.). Необходимо передать имя схемы как SCOTT. При использовании процедуры COMPILE_SCHEMA в версиях 8.1 сервера до 8.1.6.2 (т.е. во всех версиях 8.1.5, 8.1.6.0 и 8.1.6.1) возникает еще одна проблема. Если сервер поддерживает использование Java, в системе возникают рекурсивные зависимости. При вызове COMPILE_SCHEMA выдается сообщение об ошибке: scott@TKYTE816> exec dbms_utility.compile_schema(user); BEGIN dbms_utility.compile_schema(user); END; * ERROR a t l i n e 1: ORA-01436: CONNECT BY loop in user data ORA-06512: a t "SYS.DBMSJJTILITY", l i n e 195 ORA-06512: a t l i n e 1 Проблема связана с представлением SYS.ORDER_OBJECT_BY_DEPENDENCY, поэтому в версиях, начиная с Oracle 8.1.7, оно не используется. Если вы столкнетесь с этим сообщением об ошибке, можно создать собственную процедуру COMPILE_SCHEMA, работающую аналогично стандартной процедуре COMPILE_SCHEMA. В этой процедуре можно компилировать объекты в любом порядке. Типичное заблуждение состоит как раз в том, что объекты надо компилировать в строго определенном порядке. На самом деле компилировать объекты можно в произвольном порядке и получить тот же результат, что и при компиляции в порядке, определяемом зависимостями. Алгоритм следующий: 1. Выбираем недействительный объект схемы, который мы еще не пытались перекомпилировать. 2. Компилируем его. 3. Возвращаемся к первому шагу, пока есть недействительные объекты, которые мы еще не пытались перекомпилировать. Определенного порядка придерживаться не обязательно. Причина — в побочном эффекте компиляции недействительного объекта. При этом все недействительные объекты, от которых он зависит, тоже будут скомпилированы. Надо только продолжать компилировать объекты, пока недействительных не останется. (На самом деле недействительные объекты могут остаться, но лишь потому, что скомпилировать их невозможно вообще.) Может оказаться, что при компиляции всего одной процедуры перекомпилированными окажутся 10 или 20 других объектов. Если не пытаться перекомпилировать эти 10 или 20 объектов вручную (при этом исходный объект снова станет недействительным), все будет в порядке. Поскольку реализация этой процедуры представляет определенный интерес, я продемонстрирую ее. Для выполнения оператора ALTER COMPILE необходимо исполь-
698
Приложение А
зовать процедуру с правами вызывающего. Необходим также доступ к представлению DBA_OBJECTS для поиска следующего недействительного объекта и проверки состояния скомпилированного объекта. Не хотелось бы требовать обязательного доступа к представлению DBA_OBJECTS от пользователя, выполняющего процедуру. Для этого придется использовать подпрограммы как с правами вызывающего, так и с правами создателя. Необходимо, однако, сделать так, чтобы вызываемая пользователем основная процедура работала с правами вызывающего, — это обеспечит использование ролей. Ниже представлена моя реализация процедуры COMPILE_SCHEMA. Пользователь, выполняющий этот сценарий, должен получить привилегию SELECT на представление SYS.DBA_OBJECTS непосредственно (подробнее об этом можно прочитать в главе 23). Поскольку это сценарий для утилиты SQL*Plus, включающий ряд директив SQL*Plus, я представлю только сам сценарий, а не результаты его выполнения. Для указания имени схемы при компиляции объектов я использую подставляемую переменную SQL*Plus. Это делается потому, что процедура выполняется с правами вызывающего (и если необходимо всегда обращаться к одной и той же таблице, независимо от того, кто выполняет процедуру, имя таблицы необходимо уточнять), а я лично предпочитаю не полагаться на общедоступные синонимы. Я представлю сценарий по частям, комментируя каждую часть: column u new_val uname select user u from dual; drop table compile_schema_tmp / create global temporary table compile_schema_tmp (object_name varchar2(30), object_type varchar2(30), constraint compile_schema_tmp_pk primary key(obj ect_name,obj ect_type) ) on commit preserve rows
I grant a l l on compile_schema_tmp to public /
Сценарий начинается с получения имени текущего зарегистрировавшегося пользователя в подставляемую переменную SQL*Plus. Она будет использоваться в операторах CREATE OR REPLACE PROCEDURE. Это приходится делать, поскольку процедура должна выполняться с правами вызывающего и обращаться к созданной выше таблице. В главе 23 было описано, как разрешаются ссылки на таблицы с использованием стандартной схемы для пользователя, выполняющего процедуру. Используется одна временная таблица для всех сеансов, которая будет принадлежать пользователю, выполнившему этот сценарий. Поэтому необходимо явно указать имя пользователя в PL/SQL-процедуре. Временная таблица используется процедурами для запоминания
Пакет DBMSJJTILITY
699
того, какие объекты мы пытались перекомпилировать. Необходимо использовать конструкцию ON COMMIT PRESERVE ROWS, поскольку предполагается выполнять в процедурах операторы DDL (оператор ALTER COMPILE относится к операторам DDL), а при выполнении каждого такого оператора транзакция фиксируется. Теперь можно переходить к процедурам: create or replace procedure get_next_object_to_compile(p_username in varchar2, p_cmd out varchar2, p_obj out varchar2, p_typ out varchar2) as begin select 'alter ' || object_type II • ' I I p_username | | '.' || object_name || decode(object_type, 'PACKAGE BODY1, ' compile body', ' compile1), object_name, object_type into p_cmd, p_obj, p_typ from dba_objects a where owner = upper(p_username) and status = 'INVALID' and object_type <> 'UNDEFINED' and not exists (select null from compile_schema_tmp b where a.object_name = b.object_name and a.object_type = b.object_type and rownum = 1; insert into compile_schema_tmp (object_name, object_type) values (p_obj, p_typ); end; Это процедура с правами создателя, с помощью которой мы будем обращаться к представлению DBA_OBJECTS. Она будет возвращать некий недействительный объект для перекомпиляции, если мы еще не пытались его компилировать. Процедура просто находит первый такой объект. По мере выбора мы запоминаем эти объекты во временной таблице. Процедура возбуждает исключительную ситуацию NO_DATA_FOUND, когда в запрошенной схеме не остается объектов, требующих перекомпиляции. Этот факт будет использоваться в следующей процедуре для прекращения обработки. Затем мы создадим процедуру с правами вызывающего, которая будет фактически выполнять компиляцию. Это объясняет, зачем в представленном выше коде понадобилась директива COLUMN U NEW_VAL UNAME — необходимо вставить имя владельца временной таблицы, чтобы избежать использования синонима. Поскольку мы делаем это динамически при компиляции процедуры, это решение лучше, чем использование синонима: c r e a t e or replace procedure compile_schema(p_username i n varchar2) authid current user
7UU
Приложение А
as l_cmd l_obj l_typ begin delete
varchar2(512); dba_obj ects.obj ect_name%type; dba_obj ects.obj ect_type%type; from Suname. .compile_schema_tmp;
loop get_next_object_to_compile(p_username, l_cmd, l_obj, l_typ); dbms_output.put_line(l_cmd); begin execute immediate l_cmd; dbms_output.put_line('Успешно'); exception when others then dbms_output.put_line(sqlerrm); end; dbms_output.put_line(chr(9)); end loop; exception — процедура get_next_object_to_compile возбуждает эту — исключительную ситуацию, когда завершает работу when no_data_found then NULL; end; grant execute on compile_schema to public Вот и все. Теперь можно переходить в любую схему, где есть компилируемые объекты, и выполнять: scott@TKYTE816> exec tkyte.compile_schema('scott') alter PROCEDURE scott.ANALYZE_MY_TABLES compile Успешно alter PROCEDURE scott.CUST_LIST compile ORA-24344: success with compilation error alter TYPE scott.EMP_MASTER compile ORA-24344: success with compilation error alter PROCEDURE scott.FOO compile Успешно alter PACKAGE scott.LOADLOBS compile Успешно alter PROCEDURE scott.P compile Успешно alter PROCEDURE scott.RUN_BY_JOBS compile Успешно PL/SQL procedure successfully completed.
Пакет DBMS_UTILITY
70 1
Итак, процедура свыдает информацию о том, какие объекты она пытается компилировать, и результат компиляции. Судя по полученному результату, компилировалось семь объектов: при компиляции двух произошла ошибка, остальные пять успешно скомпилированы. Объекты компилировались в произвольном порядке — порядок просто не имеет значения. Эта процедура работает во всех версиях сервера.
Процедура ANALYZEJCHEMA Процедура ANALYZE_SCHEMA делает именно то, что можно предположить по ее названию, — выполняет операторы ANALYZE для сбора статистической информации об объектах в пользовательской схеме. Не рекомендуется применять ее для схем SYS или SYSTEM. В особенности не надо этого делать для схемы SYS, поскольку рекурсивные SQL-операторы, которые СУБД Oracle генерирует уже многие годы, оптимизированы для обработки оптимизатором, основанным на правилах. При наличии статистической информации о таблицах в схеме SYS сервер будет работать медленнее, чем мог бы. Эту процедуру можно использовать для анализа созданных пользователями прикладных схем. Процедура ANALYZE_SCHEMA принимает пять аргументов. •
SCHEMA. Схема, которую необходимо проанализировать.
•
METHOD. ESTIMATE, COMPUTE или DELETE. Если передано значение ESTIMATE, то одно из значений: ESTIMATE_ROWS, ESTIMATE_PERCENT должно быть ненулевым.
•
ESTIMATE_ROWS. Количество оцениваемых строк.
•
ESTIMATE_PERCENT. Процент оцениваемых строк. Если передано ненулевое значение параметра ESTIMATE_ROWS, этот параметр игнорируется.
Q METHOD_OPT [FOR TABLE] [FOR ALL [INDEXED] COLUMNS] [SIZE n] [FOR ALL INDEXES]. Это те же опции, что используются в операторе ANALYZE. Они подробно описаны в руководстве Oracle8i SQL Reference, в разделе, посвященном конструкции FOR оператора ANALYZE. Итак, например, все объекты в пользовательской схеме SCOTT можно проанализировать следующим образом. Начнем с удаления статистической информации, а затем соберем ее снова: scott@TKYTE816> exec dbms_utility.analyze_schema(user,
'delete');
PL/SQL procedure successfully completed. scott@TKYTE816> s e l e c t table_name, num_rows, 2 from user__tables; TABLE NAME BONUS CREATE$JAVA$LOB$TABLE DEPT
last_analyzed
NUM ROWS LAST ANAL
702
Приложение А
12 rows selected. scott@TKYTE816> exec dbms_utility.analyze_schema(user, 'compute'); PL/SQL procedure successfully completed. scott@TKYTE816> select table_name, num_rows, last_analyzed 2 from user_tables; TABLE NAME — BONUS CREATE$JAVA$LOB$TABLE DEPT
NUM ROWS LAST ANAL — — 0 03-FEB-01 58 03-FEB-01 4 03-FEB-01
12 rows selected. Этот простой пример показывает, что оператор ANALYZE COMPUTE выполняется — столбцы NUM_ROWS и LAST_ANALYZED получили значения. Процедура ANALYZE_SCHEMA работает в соответствии со своим названием. Если необходимо анализировать объекты с разной степенью детализации, она не поможет. Процедура применяет один и тот же метод анализа ко всем типам объектов. Например, при эксплуатации большого хранилища данных, если необходимо использовать гистограммы по определенным столбцам или наборам столбцов только в некоторых таблицах, процедуру ANALYZE_SCHEMA применять нельзя. С помощью процедуры ANALYZE_SCHEMA можно либо получить гистограммы для всех столбцов, либо не получить их вообще — избирательно обрабатывать столбцы нельзя. Если анализ объектов выходит за рамки элементарного, процедура ANALYZE_SCHEMA не позволит его выполнить. Она подходит для небольших и средних (по объему обрабатываемых данных) приложений. Если необходимо обрабатывать большие объемы данных, имеет смысл распараллелить анализ или использовать разные опции анализа для различных таблиц. Этого процедура ANALYZE_SCHEMA не обеспечивает. При использовании процедуры ANALYZE_SCHEMA следует учитывать следующие особенности. Первая связана с применением процедуры ANALYZE_SCHEMA к изменяющейся схеме. Вторая — с тем, какие объекты процедура ANALYZE_SCHEMA не анализирует. Рассмотрим их последовательно.
Применение процедуры ANALYZE_SCHEMA к изменяющейся схеме Предположим, процедура ANALYZE_SCHEMA выполняется в схеме SCOTT. В эту схему добавлено несколько больших таблиц, поэтому их анализ требует определенного времени. В другом сеансе вы удаляете или добавляете ряд объектов в схеме SCOTT. Удаленный объект не был обработан процедурой ANALYZE_SCHEMA. Когда процедура попытается его проанализировать, будет выдано сбивающее с толку сообщение: scott@TKYTE816> exec dbms_utility.analyze_schema(user, 'compute'); BEGIN dbms_utility.analyze_schema(user, 'compute'); END;
Пакет DBMSJJTILITY
703
* ERROR a t l i n e 1: ORA-20000: You have i n s u f f i c i e n t p r i v i l e g e s for an object i n t h i s schema. ORA-06512: a t "SYS.DBMSJJTILITY", l i n e 258 ORA-06512: a t l i n e 1 Очевидно, что все необходимые привилегии есть — объект принадлежит вашей схеме. Ошибка связана с тем, что таблицы, которую процедура пытается анализировать, больше нет. Вместо того чтобы определить отсутствие таблицы, процедура предполагает, что таблица существует и пользователю просто не хватает привилегий, чтобы ее проанализировать. В этом случае можно сделать только следующее: •
повторно выполнить процедуру ANALYZE_SCHEMA;
•
не удалять объекты по ходу выполнения процедуры ANALYZE_SCHEMA.
Следует также помнить, что, если объект добавлен в схему после начала выполнения процедуры ANALYZE_SCHEMA, он не будет проанализирован — процедура его не увидит. Это — не большая проблема, поскольку процедура ANALYZE_SCHEMA выполнится успешно.
Процедура ANALYZE_SCHEMA анализирует не все В процедуре ANALYZE_SCHEMA есть нерешенная проблема. Она не анализирует таблицы, организованные по индексу, если в них используется сегмент переполнения (подробнее о таблицах, организованных по индексу, и сегментах переполнения см. в главе 6). Например, если выполнить следующий код: scott@TKYTE816> drop t a b l e t ; Table dropped. scott@TKYTE816> c r e a t e t a b l e t (x i n t primary key, у date) 2 organization index 3 OVERFLOW TABLESPACE TOOLS 4 / Table created. scott@TKYTE816> execute dbms_utility.analyze_schema('SCOTT', 'COMPUTE') PL/SQL procedure successfully completed. scott@TKYTE816> select table_name, num_rows, last_analyzed 2 from user_tables 3 where table name = 'T' ; — TABLE NAME NUM ROWS LAST ANAL — — — T таблица Т не будет проанализирована. Однако если не указывать конструкцию OVERFLOW:
/U4
Приложение А
scott@TKYTE816> drop table t; Table dropped. scott@TKYTE816> create table t (x int primary key, у date) 2 organization index 3 / Table created. scott@TKYTE816> execute dbms_utility.analyze_schema('SCOTT', 'COMPUTE') PL/SQL procedure successfully completed. scott@TKYTE816> select table_name, num_rows, last_analyzed 2 from user_tables 3 where table_name = 'T'; TABLE_NAME NUM_ROWS LAST_ANAL T
0 03-FEB-01
таблица анализируется. Это не означает, что конструкцию OVERFLOW для таблиц, организованных по индексу, задавать не надо — просто такие таблицы анализируются отдельно, вручную.
Процедура ANALYZE_DATABASE Ей я посвящаю очень короткий раздел. Не используйте эту процедуру. Ее не имеет смысла использовать в базе данных любого размера. Кроме того, она имеет неприятный побочный эффект: анализирует словарь данных (объекты, принадлежащие пользователю SYS, никогда не нужно анализировать). Не используйте эту процедуру. Просто забудьте о ее существовании.
Функция FORMATJRRORJTACK На первый взгляд эта функция кажется очень полезной, но на самом деле она бесполезна. Фактически FORMAT_ERROR_STACK — это просто менее функциональная реализация функции SQLERRM (SQL ERRor Message). Простой пример поможет вам понять, что я имею в виду: scott@TKYTE816> c r e a t e or replace procedure p i 2 as 3 begin 4 raise program_error; 5 end; 6 / Procedure created. scott@TKYTE816> create or replace procedure p2 3 begin 4 5 end;
pi;
Пакет DBMSJJTILITY 6
705
/
Procedure created. scott@TKYTE816> create or replace procedure p3 2 as 3 begin 4 p2; 5 end; 6 / Procedure created. scott@TKYTE816> exec p3 BEGIN p3; END; * ERROR a t l i n e 1: ORA-06501: PL/SQL: program e r r o r ORA-06512: a t "SCOTT.PI", l i n e 4 ORA-06512: a t "SCOTT.P2", l i n e 4 ORA-06512: a t "SCOTT.P3", l i n e 4 ORA-06512: a t l i n e 1 В случае возникновения ошибки, если она не перехвачена обработчиком, выдается весь стек ошибок, который можно будет использовать в программе, использующей интерфейсы Pro*C, OCI, JDBC и т.п. Можно ожидать, что функция DBMS_UTILITY.FORMAT_ERROR_STACK будет возвращать подобную информацию. Оказывается, однако, что эту важную информацию она теряет: scott@TKYTE816> c r e a t e or replace procedure рЗ 2 as 3 begin 4 p2; 5 exception 6 when others then 7 dbms_output.put_line(dbms_utility.format_error_stack); 8 end; 9 / Procedure created. scott@TKYTE816> exec p3 ORA-06501: PL/SQL: program e r r o r PL/SQL procedure successfully completed. Как видите, при вызове функции FORMAT_ERROR_STACK информация стека ошибок потеряна! Функция возвращает ту же информацию, что и SQLERRM: scott@TKYTE816> c r e a t e or replace procedure рЗ 2 as 3 begin 4 p2; 5 exception 6 when others then 7 dbms_output.put line(sqlerrm);
23 Зак. 244
Приложение А
8 end; 9 / Procedure created. scott@TKYTE816> exec p3 ORA-06501: PL/SQL: program e r r o r PL/SQL procedure successfully completed. Я утверждал, что функция FORMAT_ERROR_STACK обеспечивает меньше возможностей, чем SQLERRM. Дело в том, что функция SQLERRM может не только возвращать сообщение о текущей ошибке, но и сообщение о любой ошибке, код которой передан как параметр: scott@TKYTE816> exec dbms_output.put_line(sqlerrm(-l)); ORA-00001: unique c o n s t r a i n t (.) v i o l a t e d PL/SQL procedure successfully completed. К сожалению, сейчас нет способа получить весь стек ошибок в языке PL/SQL. Чтобы получить фактический номер строки кода, при выполнении которой произошла ошибка, приходится пропускать фатальные ошибки до вызывающей процедуры в клиентском приложении.
Функция FORMAT_CALL_STACK К счастью, эта функция действительно очень полезна — не то, что функция FORMAT_ERROR_STACK. Она возвращает текущий стек вызовов. С ее помощью можно написать ряд полезных утилит, например MY_CALLER и WHO_AM_I. (Они выдают информацию о том, откуда вызвана текущая подпрограмма и как она называется — прим. научн. ред.) Эти утилиты вызывают рассматриваемую функцию чтобы определить, из какой строки какой подпрограммы вызваны. Такие сведения могут пригодиться при отладке и регистрации. Кроме того, процедуры могут работать по-разному, в зависимости от того, кто их вызвал и в какой среде. Прежде чем представить код моих утилит MY_CALLER и WHO_AM_I, давайте посмотрим, какую информацию можно получить в стеке вызовов и какие результаты должны выдавать эти утилиты. Если использовать процедуры P I , P2, РЗ из представленного ранее примера и переписать процедуру Р1 следующим образом: scott@TKYTE816> create or replace procedure pi 2 as 3 l_owner varchar2(30); 4 l_name varchar2(30); 5 l_lineno number; 6 l_type varchar2(30); 7 begin 8 dbms_output. put_line (' ') ; 9 dbms_output.put_line(dbms_utility.format_call_stack); 10 dbms_output .put_line (' ') ; 11 who_called_me(l_owner, l_name, l_lineno, l_type); 12 dbms_output.put_line(l_type | | ,' ' | | 13 1 owner || '.' || 1 name ||
Пакет DBMS_UTILITY
707
14 '('II l_lineno || ')') ; 15 dbms_output .put_line ( ' ') ; 16 dbms_output.put_line(who_am_i); 17 dbms_output .put_line ( ' ') ; 18 raise program_error; 19 end; 20
/
Procedure created, мы получим следующий результат: scott@TKYTE816> exec p3 PL/SQL Call Stack object line object handle number name 2fl91eO 9 procedure 39f0a9c 4 procedure 3aae318 4 procedure 3a3461c 1 anonymous
SCOTT.PI SCOTT.P2 SCOTT.P3 block
PROCEDURE SCOTT.P2(4) SCOTT.PI(16) BEGIN p 3 ; END; * ERROR a t l i n e 1: ORA-06501: PL/SQL: program e r r o r ORA-06512: a t "SCOTT.P2", l i n e 8 ORA-06512: a t "SCOTT.P3", l i n e 4 ORA-06512: a t l i n e 1 Итак, мы видим весь стек вызовов для процедуры Р1. Показано, что процедура Р1 была вызвана процедурой Р2, процедура Р2 была вызвана процедурой РЗ, которая, в свою очередь, была вызвана из анонимного блока. Кроме того, в программном коде можно выяснить, что процедура Р1 была вызвана в строке 4 процедуры SCOTT.P2. Наконец, можно выяснить, что выполняется сейчас процедура SCOTT.P1. Теперь, разобравшись, как выглядит стек вызовов и что мы хотим получить, можно представить код, позволяющий это сделать: tkyte@TKYTE816> c r e a t e or replace function my_caller r e t u r n varchar2 2
3 4 5 6 7 8 9 10
as owner name lineno caller_t call_stack n found_stack
varchar2(30); varchar2(30); number; varchar2(30); varchar2(4096) default dbms_utility.format_call_stack; number; BOOLEAN default FALSE;
/Uo
Приложение А
11 line varchar2(255); 12 cnt number := 0; 13 begin 14 15 loop 16 n := instr(call_stack, chr(10)); 17 exit when (cnt = 3 or n is NULL or n = 0 ) ; 18 19 line := substr(call_stack, 1, n-1); 20 call_stack := substr(call_stack, n+1); 21 22 if (NOT found_stack) then 23 if (line like '%handle%number%name%') then 24 found_stack := TRUE; 25 end if; 26 else 27 cnt := cnt + 1; 28 — cnt = 1 — это я 29 — cnt = 2 — это подпрограмма, которая меня вызвала 30 — cnt = 3 — это подпрограмма, которая вызвала •* подпрограмму, вызвавшую меня 31 if (cnt - 3) then 32 lineno := to_number(substr(line, 13, 6)); 33 line := substr(line, 21); 34 if (line like 'pr%') then 35 n := length('procedure ' ) ; 36 elsif (line like 'fun%') then 37 n := length('function ' ) ; 38 elsif (line like 'package body%') then 39 n := length('package body ' ) ; 40 elsif (line like 'pack%') then 41 n := length('package ' ) ; 42 elsif (line like 'anonymous block%') then 43 n := length('anonymous block ' ) ; 44 else — must be a trigger 45 n := 0; 46 end if; 47 if (n о 0) then 48 caller_t := ltrim(rtrim(upper(substr(line,1, w n-1)))); 49 line := substr(line, n ) ; 50 else 51 caller_t := 'TRIGGER1; 52 line := ltrim(line); 53 end if; 54 n := instr(line, ' . ' ) ; 55 owner := ltrim(rtrim(substr(line, 1, n-1))); 56 name := ltrim(rtrim(substr(line, n+1))); 57 end if; 58 end if; 59 end loop; 60 return owner || '.' || name;
Пакет DBMSJJTILITY 61
709
end; /
Function created. tkyte@TKYTE816> create or replace function who_am_i return varchar2 2 as 3 begin 4 return my_caller; 5 end; Function created. При наличии этих процедур можно реализовать интересные решения. В частности, они могут использоваться в следующих случаях. •
Для проверки. Процедуры аудита могут регистрировать не только пользователя, выполнившего определенное действие, но и код, выполнивший это действие.
•
Для отладки. Например, если снабдить код вызовами DBMS_APPLICATION_INFO.SET_CLIENT_INFO(WHO_AM_I), в другом сеансе можно выполнить запросы к представлению VSSESSION, чтобы определить, какой фрагмент кода сейчас выполняется. Подробнее о пакете DBMS_APPLICATION_INFO см. в начале приложения А.
Функция GETJNME Эта функция возвращает время, прошедшее с определенного момента, с точностью до сотых долей секунды. Функция GET_TIME не возвращает значение текущего времени, как можно было предположить по ее названию. Ее можно использовать для измерения периода времени между событиями. Обычно эта функция используется следующим образом: scott@TKYTE816> declare 2 1 start number; — 3 n number := 0; 4 begin 5 6 l_start := dbms_utility.get_time; 7 8 for x in 1 .. 100000 9 loop 10 n := n+1; 11 end loop; 12 13 dbms_output.put_line(' it took ' || 14 round((dbms_utility.get_time-l_start)/100, 2) II 15 ' seconds...'); 16 end; 17 / it took .12 seconds... PL/SQL procedure successfully completed.
71U
Приложение А
Итак, функция GET_TIME используется для измерения времени с точностью до сотых долей секунды. Нужно, однако, учитывать, что счетчик GET_TIME может переполниться, сбросится в ноль и начать отсчет сначала, если сервер работает достаточно долго. Сейчас на большинстве платформ сброс произойдет не раньше, чем через год. Для счетчика используется 32-битовое целое число, что позволяет хранить сотые доли секунды примерно для 497 дней. После этого произойдет переполнение счетчика, и отсчет начнется с нуля. На некоторых платформах операционная система увеличивает значение счетчика чаще, чем раз в одну сотую секунды. На этих платформах сброс счетчика может произойти раньше, чем через 497 дней. Например, на платформе Sequent таймер обнуляется за 71,58 минуты, поскольку в этой операционной системе счетчик отсчитывает микросекунды, поэтому 32-битовое целое переполняется существенно быстрее. На 64-битовых платформах счетчик не переполнится и за тысячи лет. Последнее замечание о функции GET_TIME. Значение, возвращаемое функцией GETJTIME, может быть получено и с помощью оператора SELECT * FROM V$TIMER. Это динамическое представление и функция GET_TIME возвращают одно и то же: tkyte@TKYTE816> s e l e c t hsecs, 2 from v$timer; HSECS
GETJTIME
7944822
7944822
dbms_utility.get_time
Функция GET_PARAMETER_VALUE Эта функция позволяет пользователю получить значение указанного параметра конфигурации. Даже при отсутствии доступа к представлению VSPARAMETER и невозможности выполнить команду SHOW PARAMETER, с помощью этой функции можно получить значение параметра инициализации сервера. Функция используется следующим образом: scott@TKYTE816> show parameter u t l _ f i l e _ d i r ORA-00942: t a b l e or view does not e x i s t scott@TKYTE816> s e l e c t * from v$parameter where name = ' u t l _ f i l e _ d i r ' 2 / s e l e c t * from v$parameter where name = ' u t l _ f i l e _ d i r ' * ERROR a t l i n e 1: ORA-00942: table or view does not exist scott@TKYTE816> declare 2 intval number; 3 strval varchar2(512); 4 begin 5 if (dbms_utility.get_parameter_value('utl_file_dir', 6 intval, 7 strval) = 0) 8 then 9 dbms_output.put_line('Значение = ' || intval);
Пакет DBMS_UTILITY
71
10 else 11 dbms output.put line( 'Значение = ' || strval); 12 end if; 13 end; 14 / Значение = c:\temp\ PL/SQL procedure successfully completed. Как видите, хотя пользователь SCOTT не может обратиться к представлению VSPARAMETER и выполнить команду SHOW PARAMETER, он может получить требуемое значение. Следует отметить, что параметры инициализации со значениями True/ False в файле init.ora будут выданы как числа: значение 1 обозначает ИСТИНУ (True), а значение 0 — ЛОЖЬ (False). Для параметров с несколькими значениями, например UTL_FILE_DIR, функция выдает только первое значение. Если использовать учетную запись, имеющую право для этой же базы данных выполнять команду SHOW PARAMETER: tkyte@TKYTE816> show parameter
utl_file_dir
NAME
TYPE
VALUE
utl file dir
string
c:\temp, c:\oracle
можно получить все значения.
Процедура NAME_RESOLVE Этой процедуре можно передать имя: а процедуры верхнего уровня; а функции верхнего уровня; •
пакета;
• синонима пакета, процедуры или функции верхнего уровня. Она выдает полное имя соответствующего объекта. Процедура позволяет узнать, является ли объект с указанным именем процедурой, функцией или пакетом и какой схеме он принадлежит. Рассмотрим простой пример: scott@TKYTE816> declare 2 type vcArray i s t a b l e of varchar2(30); 3 l_types vcArray := vcArray(null, n u l l , n u l l , n u l l , 'synonym', 4 n u l l , 'procedure', ' f u n c t i o n ' , 5 'package'); 6 7 1 schema varchar2(30); 8 l_partl varchar2(30); 9 I_part2 varchar2(30); I_j>art2 varchar2(30); 10 1 dblink varchar2(30); l_dblink varchar2(30); 11 1 type number; 12 l_obj# number; 13 begin
У 12
Приложение А
14 dbms_utility.name_resolve(name => 'DBMSJJTILITY', 15 context => 1, 16 schema => l_schema, 17 parti => l_partl, 18 part2 => I_part2, 19 dblink => l_dblink, 20 partl_type => l_type, 21 object_number => l_obj#); 22 if l_obj# IS NULL 23 then 24 dbms_output.put_line('Object not found or not valid. 1 ); 25 else 26 dbms_output.put(l_schema || '.' || nvl(l_partl,l_part2)); 27 if I_part2 is not null and l_partl is not null 28 then 29 dbms_output.put('.' II I_part2); 30 end if; 31 32 dbms_output.put_line(' is a ' || l_types(l_type) II 33 ' with object id ' || l_obj# || 34 ' and dblink "' | | l_dblink | | ' " • ); 35 end if; 36 end; 37 / SYS.DBMSJJTILITY i s a package with object id 2408 and dblink " " PL/SQL procedure successfully completed. Процедура NAME_RESOLVE по синониму DBMS_UTILITY определила, что речь идет о пакете, принадлежащем пользователю SYS. Следует отметить, что процедура NAME_RESOLVE работает только для процедур, функций, пакетов и синонимов, ссылающихся на один из этих трех типов объектов. В частности, она не работает для таблиц. При попытке передать имя таблицы ЕМР в схеме пользователя SCOTT, например, вы получите следующее сообщение об ошибке: declare ERROR a t l i n e 1: ORA-06564: object emp does not e x i s t ORA-06512: a t "SYS.DBMSJJTILITY", l i n e 68 ORA-06512: a t l i n e 9 Помимо того, что процедура NAME_RESOLVE не работает с таблицами, индексами и другими объектами, она работает не так, как указано в документации, при разрешении синонимов, ссылающихся на удаленные объекты по связи базы данных. В документации написано, что если передать процедуре NAME_RESOLVE синоним удаленного пакета или удаленной процедуры, то в качестве типа объекта будет возвращен синоним, а в качестве имени — имя связи базы данных. Проблема — в коде процедуры NAME_RESOLVE (документация описывает предполагаемые результаты, но процедура не работает должным образом). В текущей реализации процедура NAME_RESOLVE никогда не возвращает SYNONYM в качестве типа объекта. Вместо этого она уточняет
Пакет DBMS_UTILITY
713
имя удаленного объекта и возвращает его имя и идентификатор -1. Например, я настроил связь базы данных и создал синоним X для пакета DBMS_UTILITY@ora8i.world. При попытке уточнения имени этого синонима я получаю: SYS.DBMSJJTILITY I s a package with object id -1 and dblink "" PL/SQL procedure successfully completed. В выходном параметре DBLINK я должен был бы получить сообщение о том, что объект X — синоним и соответствующую информацию о связи. Как видите, однако, в переменной DBLINK мы получили значение Null, и единственный признак того, что пакет не является локальным, — значение -1 для идентификатора объекта. Не стоит предполагать, что эта особенность останется в следующих версиях СУБД Oracle. Было доказано, что проблема — в реализации процедуры NAME_RESOLVE, а не в документации. Документация правильно задает требования, но они некорректно реализованы. После исправления ошибки процедура NAME_RESOLVE будет работать для удаленных объектов иначе. Поэтому следует либо избегать использования процедуры NAME_RESOLVE для удаленных объектов, либо поместить вызов NAME_RESOLVE в подпрограмму, решающую эту проблему. Когда реализация этой процедуры изменится, можно будет реализовать прежние особенности работы для фрагментов кода, которые от них зависят. Последнее замечание относительно процедуры NAME_RESOLVE. Параметр CONTEXT плохо описан, а параметр OBJECT_NUMBER не описан в документации вовсе. Про параметр CONTEXT написано только, что: ... aieaeaf апой баёш -гёпём То 0 аТ 8 На самом деле, его значение должно быть целым числом от 1 до 7, или будет получено сообщение об ошибке: declare * ERROR at line 1: ORA-20005: ORU-10034: context argument must be 1 or 2 or 3 or 4 or 5 or 6 or 7 ORA-06512: a t "SYS.DBMSJJTILITY", l i n e 66 ORA-06512: a t l i n e 14 А если задать любое значение из этого диапазона, кроме 1, будет выдано одно из следующих сообщений об ошибке: ORA-04047: object specified i s incompatible with the flag specified ORA-06564: object OBJECT-NAME does not e x i s t Так что единственно допустимым значением контекста является 1. Параметр OBJECT_NUMBER вообще не описан. Это идентификатор объекта (значение столбца OBJECTJD), который можно найти в представлениях DBA_OBJECTS, ALL_OBJECTS и USER_OBJECTS. Например, возвращаясь к первому примеру, где было выдано значение OBJECT_ID 2048, можно выполнить запрос: scott@TKYTE816> s e l e c t owner, object_name 2 from all objects 3 where object id = 2408;
714
Приложение А
OWNER
OBJECT__NAME
SYS
DBMS_UTILITY
Процедура NAMEJOKENIZE Эта процедура разбивает строку, представляющую имя объекта, на компоненты. Для ссылок на объекты используется следующий синтаксис: [схема].[имя_объекта].[процедура!функция]@[связь_базы_данных] Процедура NAME_TOKENIZE в строке такого вида выделяет три начальных компонента и связь базы данных. Кроме того, она сообщает, на каком байте от начала строки ею завершен анализ имени объекта. Ниже представлен простой пример, демонстрирующий, что можно получить при передаче процедуре имени объекта. Учтите, что имена реальных объектов передавать необязательно (соответствующие таблицы и процедуры могут и не существовать), но обязательно передавать допустимые имена объектов. Если передан недопустимый (синтаксически) идентификатор, процедура NAMEJTOKENIZE возбудит исключительную ситуацию. Это позволяет проверить с помощью процедуры NAMEJTOKENIZE, является ли строка символов допустимым идентификатором: scott@TKYTE816> declare 2 1_а varchar2(30); 3 l_b varchar2(30); 4 l_c varchar2(30); 5 l_dblink varchar2(30); 6 l_next number; 7 8 type vcArray is table of varchar2(255); 9 l_names vcArray := 10 vcArray('owner.pkg.proc@database_link', 11 'owner.tbl@database_link', 12 'tbl', 13 '"Owner".tbl', 14 'pkg.proc', 15 'owner.pkg.proc', 16 'proc', 17 'owner.pkg.proc@dblink with junk', 18 '123'); 19 begin 20 for i in 1 . names .count 21 loop 22 begin => 1_ names(i) 23 dbms_uti: .name tokenize(name 24 => 1 a => l"b, 25 b => 1 26 с 27 dblink => l_dblink, 28 nextpos=> l_next); 29 30 dbms_output.put_line('name ' || l_names(i));
Пакет DBMS_UTILITY 31 dbms output.put line('A ' 32 dbms output.put line('B ' 33 dbms_output.put line('C ' 34 dbms output.put line('dblink ' dbms_output.put line('next ' 35 36 length (1_ 37 exception 38 39 when others then 40 dbms_output.put_line('name 41 dbms output.put line(sqlerrm 42 end; 43 end loop; 44 end; 45 / name owner.pkg.proc@database link A OWNER В С dblink next
715
1 l a); 1 l" b); 1 l~ c); 1 l"_dblink) ; 1 i }iext 1 1 ' ' 1 1 names(i))) ; i \ .
)r
1 l_names(i));
PKG PROC DATABASE LINK 28 28
Как видите, процедура позволяет выделить компоненты имени объекта. Параметр NEXT получил значение длины строки — в данном случае анализ закончился, когда был достигнут конец строки. Поскольку мы задали полное имя объекта, все четыре компонента имени получили значения. Теперь рассмотрим другие примеры: name А В С dblink next
owner.tbiedatabase link OWNER TBL
name А В С dblink next
tbl TBL
DATABASE LINK 23 23
3 3
Обратите внимание, что параметры В и С остались пустыми. Хотя идентификатор объекта строится по принципу СХЕМА.ОБЪЕКТ.ПРОЦЕДУРА, процедура NAME_TOKENIZE не поместила значение TBL в выходной параметр В. Она просто помещает первую часть имени в параметр А, следующую часть — в В и т.д. Параметры А, В и С не содержат логический компонент имени — они содержат первую найденную часть, вторую и т.д. name A В
"Owner".tbl Owner TBL
716
Приложение А
С dblink next
11 11
Этот результат представляет определенный интерес. В предыдущих примерах процедура NAME_TOKENIZE выдавала все имена в верхнем регистре. Дело в том, что все идентификаторы сервер переводит в верхний регистр, если только они не взяты в кавычки. Мы же передали идентификатор в кавычках. В таком идентификаторе процедура NAME_TOKENIZE сохранит регистр символов, но кавычки удалит. паше pkg.proc А PKG В PROC С dblink 8 8 next name А В С dblink next
owner.pkg.proc OWNER PKG PROC 14 14
name proc А PROC В С dblink 4 4 next name А В С dblink next
owner.pkg.procSdblink with junk OWNER PKG PROC DBLINK 22 31
В этом примере анализ прекратился раньше, чем был достигнут конец строки. Процедура NAMEJTOKENIZE сообщает, что прекратила анализ на 22-ом байте из 31. Это пробел перед строкой with junk. Процедура проигнорировала остаток строки после имени. name 123 ORA-00931: missing i d e n t i f i e r PL/SQL procedure successfully completed. Последний пример показывает, что при получении недопустимого идентификатора процедура NAME_TOKENIZE возбуждает исключительную ситуацию. Она проверяет, является ли каждая возвращаемая лексема допустимым идентификатором. Это поэво-
Пакет DBMS_UTILITY
717
ляет использовать ее для проверки допустимости имен в приложениях, создающих объекты в базе данных Oracle. Например, если создается средство построения модели данных и необходимо проверить допустимость имени, которое пользователь хочет задать для таблицы или столбца, можно применить процедуру NAMEJTOKENIZE.
Процедуры COMMA_TO_JABLEf TABLE_TO_COMMA Эти две процедуры преобразуют, соответственно, список идентификаторов через запятую в PL/SQL-таблицу (COMMA_TO_TABLE) и PL/SQL-таблицу произвольных строк в строку-список через запятую (TABLE_TO_COMMA). Я выделил слово "идентификаторов", потому что процедура COMMA_TO_TABLE использует для анализа строк процедуру NAME_TOKENIZE. Поэтому необходимо задавать допустимые идентификаторы Oracle (или идентификаторы в кавычках). При этом, однако, элемент списка все равно не может быть длиннее 30 символов. Эта утилита наиболее полезна для приложений, которые хранят список, например, имен таблиц в одной строке, и преобразуют их по ходу работы в PL/SQL-таблицу. Применить эту таблицу для других целей не удастся. Универсальная процедура COMMA_TO_TABLE, работающая со строками данных, перечисленных через запятую, была представлена в главе 20. Там я демонстрирую на ее примере, как выбирать данные с помощью SELECT из PL/SQL-функции. Вот пример использования стандартной процедуры COMMA_TO_TABLE, показывающий, как она обрабатывает длинные и недопустимые идентификаторы: scott@TKYTE816> declare 2 type vcArray i s t a b l e of varchar2(4000) ; 4 l_names vcArray := vcArray('emp,dept,bonus 1 , 5 'a, b , c', 6 '123, 456, 789', 7 '"123", "456", "789"', 8 '"This is a long string, longer then 32 characters","b",c'); 9 l_tablen number; 10 l_tab dbms_utility.uncl_array; 11 begin 12 for i in 1 .. l_names.count 13 loop 14 dbms_output.put_line(chr(10) || 15 '['II l_names(i) || ']'); 16 begin 18 19 20 21 22 23 24
dbms_utility.comma_to_table(l_names(i), l_tablen, l_tab); for j in l..l_tablen loop dbms_output.put_line('[' II l_tab(j) || ']'); end loop;
/16
Приложение А
25 26
1 names(i) := null; dbms utility.table_to comma(l_tab, 1 tablen, 1 names(i)); dbms output.put_line(l_names(i)); exception when others then dbms output.put line(sqlerrm);
28 29 30 31 32 33 end; end loop; 34 35 end; 36 / [emp,dept,bonus] [emp] Сdept] [bonus] emp,dept,.bonus
Этот пример демонстрирует как по строке emp,dept,bonus строится таблица идентификаторов, которая затем снова преобразуется в строку. [а, Ь, [а] [ Ь ] [ с] а, Ь,
с]
с
Этот пример показывает, что пробелы в списке сохраняются. Для удаления начальных и конечных пробелов, если они не нужны, надо использовать встроенную функцию TRIM. [123, 456, 789] ORA-00931: missing i d e n t i f i e r Это показывает, что для применения процедуры к строке чисел необходимо изменить формат строки, как показано ниже: ["123", "456", "789"] ["123"] [ "456"] [ "789"] "123", "456", "789" Числа в кавычках удалось извлечь из строки. Обратите внимание, однако, что в элементах таблицы сохранен не только начальный пробел, но и кавычки. Если они не нужны, удалять их придется отдельно. ["This i s a long string, longer than 32 characters","b",с] ORA-00972: identifier i s too long PL/SQL procedure successfully completed. Последний пример показывает, что, если передан слишком длинный идентификатор (длиннее 30 символов), процедура возбуждает исключительную ситуацию. Рассматриваемые процедуры подходят только для строк длиной до 30 символов. Хотя процеду-
Пакет DBMS_UTILITY
719
pa TABLE_TO_COMMA и позволяет формировать строку из элементов таблицы длиннее 30 символов, процедура COMMA_TO_TABLE не позволит преобразовать результат обратно в таблицу.
Процедура DBVERSION и функция PORTJTRING Процедура DB_VERSION появилась в версии Oracle 8.0 для определения версии сервера в приложениях. Ее можно было бы использовать, например, в пакете CRYPT_PKG (см. раздел приложения А, посвященный пакету DBMS_OBFUSCATION_TOOLKIT), чтобы предупредить пользователей, пытающихся использовать для шифрования процедуры DES3 на сервере Oracle 8.1.5, что это не сработает, до того, как они начнут получать сообщения об ошибках. Интерфейс этой процедуры предельно прост: scott@TKYTE816> d e c l a r e 2 1 version varchar2(255); 3 1 compatibility varchar2(255); 4 begin 5 dbms_utility.db_ version(1 version, 1 compatibility); 6 dbms output.put line(l version); 7 dbms output.put line(l_compatibility); 8 end; 9 / 8.1. 6.0.0 8.1. 6 PL/SQL procedure successfully completed. При этом она выдает более детальную информацию, чем более старая функция PORT_STRING: scott@TKYTE816> s e l e c t dbms_utility.port_string
from dual;
PORT STRING IBMPC/WIN_NT-8.1.0 Функция PORT_STRING не только вынуждает анализировать полученную строку, но и не позволяет определить, работаем ли мы с сервером версии 8.1.5, 8.1.6 или 8.1.7. Процедура DB_VERSION для получения такой информации подходит больше. С другой стороны, функция PORT_STRING позволяет определить, на какой операционной системе работает сервер.
Функция GET_HASH_VALUE Эта функция возвращает для переданной строки числовое хеш-значение. Ее можно использования для создания собственной "PL/SQL-таблицы" со строковыми индексами или, как было показано в разделе, посвященном пакету DBMS_LOCK, для реализации других алгоритмов. Учтите, что алгоритм, используемый для реализации функции GET_HASH_VALUE, менялся при переходе к следующей версии сервера, так что не надо использовать эту
72,0
Приложение А
функцию для генерации суррогатных ключей. Если сохранять возвращаемые этой функцией значения в таблице, при переносе приложения в следующую версию сервера возможны проблемы, поскольку по тем же входным данным будут выдаваться другие значения хеш-функции! Функция принимает три параметра. •
Строку, которую необходимо хешировать.
Q "Базовое число" для возвращаемых значений. Если необходимо получить числа в диапазоне от 0 до некоторого числа, надо указать базовое значение 0. Q Размер хеш-таблицы. Лучше, если это число будет степенью двойки. Чтобы продемонстрировать использование функции GET_HASH_VALUE, я реализую новый тип, HASHTABLETYPE, для поддержки хешей в языке PL/SQL. Он очень похож на PL/SQL-таблицу, проиндексированную не числами, а строками типа VARCHAR2. Обычно элементы PL/SQL-таблицы индексируются целыми числами. Новый же тип PL/SQL-таблицы позволяет индексировать элементы строками. Можно объявлять переменные типа HASHTABLETYPE, помещать (GET) и выбирать (PUT) из них данные. Такого рода таблиц можно создать сколько угодно. Вот спецификация соответствующих типов: tkyte@TKYTE816> c r e a t e or replace type myScalarType 2 as object 3 (key varchar2(4000), 4 val varchar2(4000) 5 ); 6 / Type created. tkyte@TKYTE816> create or replace type myArrayType 2 as varray(lOOOO) of myScalarType; 3 / Type created. tkyte@TKYTE816> create or replace type hashTableType 2 as object 3 ( 4 g_hash_size number, 5 g_hash_table myArrayType, 6 g_collision_cnt number, 7
8 9 10 11 12 13 14 15 16 17
static function new(p_hash_size in number) return hashTableType, member procedure put(p_key in varchar2, p_val in varchar2), member function get(p_key in varchar2) return varchar2, member procedure print stats
Пакет DBMS_UTILITY
72 1
18 ) ; 19 / Type created. Интересная особенность реализации состоит в добавлении статического метода-функции NEW. Это позволит создать собственный конструктор. Специального значения имя NEW не имеет. Это не ключевое слово. Функция NEW просто позволяет объявлять данные типа HASHTABLETYPE следующим образом: declare l_hashTable hashTableType := hashTableType.new(1024); а не как обычно: declare l_hashTable hashTableType
:= hashTableType(1024, myArrayType(),
0);
Я уверен, что первый вариант надежнее и понятнее второго. Второй вариант вызова раскрывает многие детали реализации (например, то, что используется тип массива, имеется переменная G_COLLISION_CNT, которой надо задавать значение 0, и т.п.). Пользователям знать об этом не обязательно. Теперь рассмотрим тело объектного типа: scott@TKYTE816> create or replace type body hashTableType 2 as 3 4 — Наш более "дружественный" конструктор. 5
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
static function new(p_hash_size in number) return hashTableType is begin return hashTableType(p__hash_size, myArrayType(), 0); end; member procedure put(p_key in varchar2, p_val in varchar2) is l_hash number := dbms_utility.get_hash_value(p_key, 1, g_hash_size); begin if (p_key is null) then raise_application_error(-20001, 'Пустой ключ не допускается'); end if;
Следующий фрагмент кода определяет, надо ли увеличить таблицу для размещения нового хешированного значения. Если — да, таблица увеличивается до необходимого для добавления индекса размера: 27 28
i f (l_hash > nvl( g_hash_table.count, then
0))
I 2,2, Приложение А 29 30 31
g_hash_table.extend(l_hash-nvl(g_hash_table.count,0)+1); end if;
Нет никакой гарантии, что запись с индексом, полученным при хешировании соответствующего ключа, пуста. При выявлении конфликта мы пытаемся поместить значение в следующий элемент набора. Выполняется до 1000 попыток поместить значение в таблицу. Если все 1000 попыток завершатся неудачно, попытка добавления считается неудачной. Это значит, что размер таблицы недостаточен: 35 36 37 38 39 40 41 42 43
for i in 0 .. 1000 loop — Если мы собираемся выйти за пределы таблицы, — сначала надо добавить новый слот. if (g_hash_table.count <= l_hash+i) then g_hash_table.extend; end if;
Следующий фрагмент реализует действие: если слот не используется или ключ уже находится в этом слоте, использовать и вернуть его. Странной кажется проверка того, пуст ли элемент G_HASH_TABLE или значение G_HASH_TABLE(L_HASH+I).KEY. Это показывает, что элемент набора может быть пустым (Null) или может содержать объект с пустыми атрибутами: 46 if (g_hash_table(l_hash+i) is null OR 47 nvl(g_hash_table(l_hash+i).key,p_key) = p_key) 48 then 49 g_hash_table(l_hash+i) := myScalarType(p_key,p_val); 50 return; 51 end if; 52 53 — Иначе увеличить счетчик количества конфликтов 54 — и перейти к следующему слоту. 55 g_collision_cnt := g_collision_cnt+l; 56 end loop; 57 58 — Если мы оказались здесь, значит, таблица слишком мала. 59 — Увеличьте ее. 60 raise_application_error(-20001, 'слишком много хеш-значений в '•• таблице') ; 61 end; 62 63 64 member function get(p_key in varchar2) return varchar2 65 is 66 l_hash number := 67 dbms_utility.get_hash_value(p_key, 1, g_hash_size); 68 begin Если необходимо получить значение, мы обращаемся к элементу индекса, в котором это значение хранится, а в случае возникновения конфликта, просматриваем до 1000
Пакет DBMS_UTILITY
723
следующих элементов. Поиск прекращается раньше, если обнаружится пустой слот (известно, что нужной записи после этого значения быть не может): 71 for i in l_hash . . least(l_hash+1000, nvl(g_hash_table.count,0)) 72 loop 73 — Если обнаружили ПУСТОЙ слот, мы ЗНАЕМ, что искомого 74 — значения в таблице нет, поскольку оно должно было быть ^ помещено в этот слот. 75 if (g_hash_table(i) is NULL) 76 then 77 return NULL; 78 end if; 79 80 — Если ключ найден, вернуть соответствующее значение. 81 if (g_hash_table(i).key = p_key) 82 then 83 return g_hash_table(i).val; 84 end if; 85 end loop; 86 87 — Ключа в таблице нет. Завершаем работу. 88 return null; 89 end; 90 Последняя процедура используется для выдачи полезной информации, например, о количестве выделенных и использованных слотов, а также о количестве конфликтов. Учтите, что количество конфликтов может превосходить размер таблицы! 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
member procedure print_stats is l_used number default 0; begin for i in 1 .. nvl(g_hash_table.count,0) loop if (g_hash_table(i) is not null) then l_used := l_used + 1; end if; end loop; dbms_output.put_line('Таблица увеличена до ' || g_hash_table.count); dbms_output.put_line( 'Мы используем ' | | l_used) ; dbms_output.put_line('Количество конфликтов...' || g_collision_cnt); end; end; /
Type body created.
724
Приложение А
Как видите, мы использовали функцию GET_HASH_VALUE для получения по строке числа, которое можно использовать для индексации табличного типа и выборки значения. Теперь можно рассмотреть, как используется этот новый тип: tkyte@TKYTE816> declare 2 l_hashTbl hashTableType := hashTableType.new(power(2,7)); 3 begin 4 for x in (select username, created from all_users) 5 loop 6 l_hashTbl.put(x.username, x.created); 7 end loop; 8 9 for x in (select username, to_char(created) created, 10 l_hashTbl.get(username) hash 11 from all_users) 12 loop 13 if (nvl(x.created, 'x') <> nvl(x.hash,'x')) 14 then 15 raise program_error; 16 end if; 17 end loop; 18 19 l_hashTbl.print_stats; 20 end; 21 / Таблица увеличена до 120 Используется 17 Количество конфликтов.... 1 PL/SQL procedure successfully completed.
Вот и все. М ы расширили возможности языка PL/SQL, добавив хеш-таблицу на базе стандартных пакетов.
Резюме Мы завершаем обзор основных подпрограмм пакета DBMS_UTILITY. Многие из них, в частности GETJTIME, GET_PARAMETER_VALUE, GET_HASH_VALUE и FORMAT_CALL_STACK, входят в мой список "часто даваемых ответов". Это означает, что именно они необходимы для решения многих проблем. Пользователи просто не знают о существовании этих подпрограмм.
Пакет UTLFILE
Стандартный пакет UTL_FILE позволяет читать и создавать текстовые файлы в файловой системе сервера в среде PL/SQL. Здесь существенны следующие ключевые слова. Q Текстовые файлы. Пакет UTL_FILE позволяет читать и создавать простые текстовые файлы. В частности, его нельзя использовать для чтения или создания двоичных файлов. Специальные символы, содержащиеся в двоичных данных, приводят к некорректной работе пакета UTL_FILE. Q В файловой системе сервера. Пакет UTL_FILE позволяет читать и записывать файлы только в файловой системе сервера баз данных. Он не позволяет читать или записывать в файловую систему компьютера, на котором работает клиент, если последний не подключен локально к серверу. Пакет UTL_FILE подходит для создания отчетов и сброса данных из базы в текстовые файлы, а также для чтения и загрузки данных. В главе 9 первой части книги представлен полный пример использования пакета UTL_FILE для создания текстового файла в формате, упрощающем загрузку. Пакет UTL_FILE также помогает при отладке. В главе 21, посвященной средствам детального контроля доступа, представлен пакет DEBUG. Этот пакет интенсивно использует средства пакета UTL_FILE для записи сообщений в файловую систему. Пакет UTL_FILE — очень полезен, но, используя его, нужно помнить об определенных ограничениях. В противном случае результаты могут оказаться некорректными (причем, выявиться это может не при тестировании, а при внедрении) и вызовут разочарование.
I А, О
Приложение А
гой машине. Но это не означает, что процесс, работающий на этой машине, сможет увидеть этот диск. Вот тут и возникают проблемы. Многие пользователи регистрируются на сервере и видят диск D:. Они включают в файл параметров конфигурации запись UTL_FILE_dir = d:\reports — каталог, в котором предполагается создание отчетов с помощью средств пакета UTL_FILE. При выполнении, однако, они получают сообщение об ошибке: ERROR a t l i n e 1: ORA-06510: PL/SQL: unhandled user-defined ORA-06512: a t "SYS.UTL_FILE'\ l i n e 98 ORA-06512: a t "SYS.UTL_FILE", l i n e 157
exception
С помощью обработчика исключительных ситуаций (ниже представлен код, который я обычно использую) это сообщение можно сделать более информативным, например: ERROR a t l i n e 1: ORA-20001: INVALID_PATH: F i l e location or filename was i n v a l i d . ORA-06512: a t "TKYTE.CSV", l i n e 51 ORA-06512: a t l i n e 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 I Services I OracleServiceXXXX I Startup I Log On As; (где ХХХХ — имя экземпляра) В ОС Windows 2000 следует выбрать: Control Panel I Administrative Tools I Services I OracleServiceXXXX I Properties I Log On Tab; (где ХХХХ — имя экземпляра) Выберите переключатель This Account и введите соответствующую информацию о регистрации в домене. После настройки служб для работы от имени пользователя с соответствующими привилегиями, параметр UTL_FILE_DIR можно задать одним из двух способов. Q С помощью сопоставленного диска. Чтобы использовать сопоставленный удаленному ресурсу диск, необходимо, чтобы пользователь, от имени которого запуска-
Пакет UTL_FILE
729
ется служба, настроил диск, указанный в значении параметра UTL_FILE_DIR, и был зарегистрирован на сервере при использовании пакета UTL_FILE. Q С помощью универсальных соглашений по именованию. Использование универсальных соглашений по именованию (Universal Naming Conventions — UNC) предпочтительнее сопоставления дисков, поскольку не требует регистрации пользователя. При этом значение параметра инициализации UTL_FILE_DIR задается в виде \\<имя машины >\<имя общего ресурса >\<путь>. Естественно, сервер Oracle после изменения свойств сетевой службы необходимо перезапустить.
Обработка исключительных ситуаций При возникновении ошибки пакет UTL_FILE возбуждает исключительную ситуацию. К сожалению, он возбуждает исключительные ситуации, задаваемые пользователем, — они определены в спецификации пакета. Если эти исключительные ситуации не перехвачены по имени, выдаются совершенно бесполезные сообщения об ошибках: ERROR a t l i n e 1: ORA-06510: PL/SQL: unhandled user-defined ORA-06512: a t "SYS.UTL_FILE", l i n e 98 ORA-06512: a t "SYS.UTL_FILE", l i n e 157
exception
О самой ошибке в этом сообщении ничего не сказано. Для решения этой проблемы надо включить обращения к пакету UTL_FILE в блок обработки исключительных ситуаций, где каждая из них перехватывается по имени. Я предпочитаю преобразовывать исключительные ситуации в вызовы процедуры RAISE_APPLICATION_ERROR. Это позволяет задать код ошибки ORA- и выдать более информативное сообщение об ошибке. В предыдущем примере мы использовали этот прием для преобразования представленного выше сообщения об ошибке в следующее: ORA-20001: INVALID PATH: F i l e location or filename was i n v a l i d . Это сообщение намного полезнее. Я всегда использую блок обработки исключительных ситуаций следующего вида:* 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.'); * Исходные сообщения об ошибках приводятся на английском, а в строке с кодом ошибки добавлен комментарий на русском языке с описанием причины ее возникновения. - Прим. научн. ред.
73U
Приложение А
when utl_file.invalid_operation then raise_application_error(-20003, — файл нельзя открыть или *•* обработать в указанном режиме •INVALIDJDPERATION: 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 минут я генерирую статическую начальную страницу заново на основе динамических данных. Это существенно сокращает ресурсы, необходимые для поддержки сайта. Такой прием я применяю для популярных динамических станиц, базовые данные которых меняются сравнительно редко. Ниже представлена универсальная процедура, которую я для этого использую: c r e a t e or replace procedure dump_page(p_dir p_fname is l_the Page htp.htbuf_arr; l_output utl_file.file_type; l_lines number default 99999999; begin l_output := utl_file.fopen(p_dir, p_fname, owa. get_j?age (l_thePage, l_lines) ; for i in 1 . . l_lines loop
in varchar2, in varchar2)
'w',
32000);
Пакет UTL_FILE
731
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'; v l ( l ) := ' 8 0 ' ; owa.init_cgi_env(nm.count, nm, v l ) ; — здесь выполните необходимую webdb-процедуру dump_page('directory', ' f i l e n a m e ' ) ; 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!
/ j 2,
Приложение А
Для решения этой проблемы необходимо использовать пакет UTL_FILE так, как это делалось в представленной выше процедуре DUMP_PAGE. Четвертый параметр вызова функции UTL_FILE.FOPEN задает максимальную длину строки текста, которую предполагается использовать. Я допускал использование строк длиной до 32000 байт.
Чтение содержимого каталога Этой возможности в пакете UTL_FILE не хватает. Часто необходимо создать периодически выполняющееся задание для просмотра каталога в поисках новых файлов и обработки этих файлов, например загрузки их содержимого в базу данных. К сожалению, стандартного способа прочитать список файлов в каталоге в языке PL/SQL нет. Можно, однако, реализовать эту возможность с помощью небольшого фрагмента кода на языке Java. Следующий пример демонстрирует, как это сделать. Сначала я создам пользователя с минимальным набором привилегий, необходимых для выполнения действий по загрузке данных и получения списка файлов в каталоге /tmp. Если необходимо читать содержимое других каталогов, придется добавить соответствующие вызовы dbms_java.grant_permission (подробнее о них см. в главе 19) или заменить /tmp на *, что позволит получить список файлов любого каталога. SQL> connect system/manager system@DEV816> drop user d i r l i s t cascade; User dropped. system@DEV816> grant create session, create table, create procedure 2 to d i r l i s t identified by d i r l i s t ; Grant succeeded. system@DEV816> begin 2 dbms_java.grant_permission 3 ('DIRLIST1, 4 'java.io.FilePermission', 5 Vtmp', 6 'read'); 7 end; 8 / PL/SQL procedure successfully completed. Затем, подключившись от имени пользователя DirList, мы создаем глобальную временную таблицу в его схеме (для хранения списка файлов каталога). Так, через временную таблицу, мы сможем получить результаты выполнения хранимой процедуры на языке Java в вызывающей среде. Можно использовать для этого и другие способы (строки, массивы и т.п.). SQL> connect d i r l i s t / d i r l i s t Connected. dirlist@DEV816> create global temporary table DIR_LIST 2 (filename varchar2(255)) 3 on commit delete rows 4 / Table created.
Пакет UTL_FILE
733
Теперь создадим хранимую процедуру на языке Java для получения списка файлов в указанном каталоге. Чтобы упростить программирование, я использую средства препроцессора SQLJ — это позволяет избежать написания большого количества вызовов JDBC: dirlist@DEV816> c r e a t e or replace 2 and compile Java source named "DirList" 3 as 4 import j ava.io.*; 5 import java.sql.*; 6 7 public class DirList 8
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Java
{
public static void getList(String directory) throws SQLException { File path = new File(directory); String[] list = path.listO; String element; for (int i = 0; i < list.length; i++) { element = list[i]; #sql { INSERT INTO DIR_LIST (FILENAME) VALUES (:element) }; } } } / created.
Затем необходимо создать процедуру сопоставления, связывающую языки PL/SQL и Java. Она достаточно проста: dirlist@DEV816> 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. Q С помощью пакета UTL_HTTP нельзя получать двоичные данные.
736
Приложение А
Q Интерфейс запроса страницы по частям (REQUEST_PIECES) неочевиден — намного проще было бы использовать данные типа CLOB или BLOB для возврата данных в виде "потока" (что обеспечило бы заодно и доступ к двоичным данным). Q Пакет не поддерживает "ключики" (cookies). Q Пакет не поддерживает даже простейшую аутентификацию. •
В пакете нет методов кодирования адреса URL.
Одна из возможностей, которые пакет UTL_HTTP поддерживает, — это использование протокола SSL. С помощью диспетчера Oracle Wallet можно выполнять запросы по протоколу HTTPS (HTTPS — это реализация протокола HTTP поверх SSL). Я продемонстрирую использование пакета UTL_HTTP для доступа по протоколу SSL, но соответствующие возможности в пакете HTTP_PKG реализовывать не будем. Полный текст пакета HTTP_PKG вследствие большого размера в этом разделе не приводится; он доступен на сайте издательства Apress по адресу http://www.apress.com.
Возможности пакета UTL НИР Рассмотрим сначала функциональные возможности пакета UTL_HTTP, поскольку предполагается реализовать аналогичные в пакете HTTP_PKG. Простейший вариант использования средств пакета UTL_HTTP представлен далее. В этом примере myserver — имя моего Web-сервера. Вы, разумеется, должны выполнить этот пример, обращаясь в Webсерверу, который вам доступен: ops$tkyte@DEV816> s e l e c t u t l _ h t t p . r e q u e s t ( ' h t t p : / / m y s e r v e r / ' )
from dual;
UTL_HTTP.REQUEST('HTTP://MYSERVER/') <TITLE>Oracle S e r v i c e Industries
Можно просто вызвать функцию UTL_HTTP.REQUEST, передав ей адрес URL. Пакет UTL_HTTP подключится к соответствующему Web-серверу и запросит (GET) указанную страницу, а затем вернет первые 2000 ее символов. Как уже было сказано, не пытайтесь использовать указанный в примере адрес URL — это адрес моего рабочего Web-сервера в корпорации Oracle. Вы не сможете к нему добраться — по истечении времени ожидания будет выдано сообщение об ошибке. Большинство сетей сегодня защищено межсетевыми экранами. Если необходимая страница доступна только через промежуточный сервер, я мог бы запросить ее и так.
Пакет UTLJHTTP
737
Обсуждение межсетевых экранов и промежуточных серверов выходит за рамки этой книги. Однако если известно имя хоста, на котором работает промежуточный сервер, можно выбрать через него страницу из Internet следующим образом: ops$tkyte@DEV816> s e l e c t utl_http.request('http://www.yahoo.com', *•* proxy') from dual ;
'www-
UTL_HTTP.REQUEST('HTTP://WWW.YAHOO.COM','WWW-PROXY') Yahoo!<meta http-equiv="PICS-Label" content='(PICS-1.1 http://www.rsac.org/ ratingsvOl.html" 1 gen t r u e for "http://www.yahoo.com" r ( n O s O v O l 0))'>