Текст
                    3-Е ИЗДАНИЕ
Oracle PL/SQL
ДЛЯ ПРОФЕССИОНАЛОВ
С. Фейерштейн, Б. Прибыл
р
Москва ¦ Санкт-Петербург ¦ Нижний Новгород - Воронеж
Ростов-на-Дону ¦ Екатеринбург ¦ Самара
Киев • Харьков • Минск
2003


THIRD EDITION Oracle PL/SQL Programming Stewen Feuerstein wint Bill Pribyl O'REILLr Beijing • Cambridge • Farnham • Koln • Paris • Sebastopol ¦ Taipei ¦ Tokyo
Краткое содержание Предисловие .¦ 21 Часть I. Программирование на PL/SQL Глава 1. Введение в PL/SQL 32 Глава 2. Написание и запуск кода PL/SQL 57 Глава 3. Основы языка 87 Часть II. Структура программы PL/SQL Глава 4. Условные операторы и переходы 110 Глава 5. Циклы 132 Глава 6. Обработка исключений 149 Часть III. Работа с данными в PL/SQL Глава 7. Работа с программными данными 174 Глава 8. Строки 196 Глава 9. Числа 243 Глава 10. Дата и время 271 Глава 11. Записи и коллекции 334 Глава 12. Другие типы данных 403 Часть IV. SQL и PL/SQL Глава 13. DML и управление транзакциями 444 Глава 14. Выборка данных 481 Глава 15. Динамический SQL и динамический PL/SQL 536
Краткое содержание Часть V. Создание приложений PL/SQL Глава 16. Процедуры, функции и параметры 568 Глава 17. Пакеты 616 Глава 18. Триггеры 645 Глава 19. Управление приложениями РЦ/SQL 700 Часть VI. Особые возможности PL/SQL Глава 20. Выполнение программ PL/SQL 740 Глава 21. Объектно-ориентированные возможности PL/SQL 799 Глава 22. Взаимодействие Java и PI7SQL 857 Глава 23. Внешние процедуры 892 Алфавитный указатель 919
Содержание Предисловие 21 Задачи этой книги 22 Структура книги 23 Используемые соглашения 27 Платформа и версия 28 О программном коде 28 Комментарии и вопросы 28 Благодарности 29 От издательства 30 Часть I. Программирование на PL/SQL Глава 1. Введение в PL/SQL.... 32 История PL/SQL 33 Истоки РЦ/SQL 33 Повышение переносимости приложений 34 Улучшенная защита приложений и защита целостности транзакций 34 Скромное начало, постоянное усовершенствование 35 Так вот что такое РЦ/SQL 35 Интеграция с SQL 36 Последовательность и условия выполнения строк программы 37 Когда что-то пошло не так 38 О версиях PL/SQL 39 Новые возможности OracleBI 40 Новые возможности Orade9i 45 Работа с несколькими версиями PL/SQL 49 Ресурсы для разработчиков PL/SQL 49 Книги о PL/SQL от O'Reilly 50 Другие печатные ресурсы 51 PL/SQL в Интернете 51 Средства разработки и утилиты 52 Несколько советов 53 Не стоит торопиться! 53 Не бойтесь обращаться за помощью 54 Поощряйте творческий, и даже радикальный, подход к разработке кода 55
8 Содержание Глава 2. Написание и запуск кода PL/SQL 57 SQL*Plus 58 Запуск SQL*Plus 59 Выполнение SQL-инструкции 62 Запуск программы на языке PL/SQL 63 Запуск сценария 64 Другие задачи SQL*Plus 65 Обработка ошибок в SQL*Plus 70 Достоинства и недостатки SQL*Plus 70 Выполнение базовых операций PL/SQL 71 Создание хранимой программы 72 Выполнение хранимой программы 75 Вывод хранимых программ 75 Управление привилегиями и создание синонимов хранимых программ 76 Удаление хранимой программы 77 Как скрыть исходный код хранимой программы 77 Средства разработки Oracle PL/SQL 78 Перенос программ на PL/SQL между клиентским компьютером и сервером 80 Вызов PlVSQL из других языков 81 С, с использованием прекомпилятора Oracle (Pro*Q 82 Java, с использованием JDBC 83 Perl, с использованием Perl DBI и DBD::Oracle 84 РЦ/SQL Server Pages 85 Что же дальше 86 Глава 3. Основы языка 87 Структура блока PL/SQL 87 Разделы блока PL/SQL 88 Анонимные блоки 89 Именованные блоки 92 Вложенные блоки 92 Область действия 93 Видимость переменной 94 Набор символов PL/SQL 96 Идентификаторы 97 Зарезервированные слива 99 Отступы и ключевые слова 101 Литералы 102 Одинарные кавычки внутри строки 102 Числовые литералы 103 Логические (булевы) литералы 103 Разделитель а виде точки с запятой 104 Комментарии 104 Однострочные комментарии 104 Многострочные комментарии 105 Ключевое слово PRAGMA 105 Метки 106
Содержание 9 Часть II. Структура программы PL/SQL Глава 4. Условные операторы и переходы 110 Операторы IF 110 Конструкция IF...THEN Ill Конструкция IF...THEN...ELSE 112 Конструкция IF...THEN...ELSIF " ш Вложенные операторы IF 116 Оператор CASE 118 Простые операторы CASE 119 Поисковый оператор CASE 120 Вложенные операторы CASE 122 Выражения CASE 123 Оператор GOTO 125 Ограничения на использование 126 Оператор NULL 129 Повышение читабельности программы 129 Обработка исключений с помощью оператора NULL 130 Использование оператора NULL после метки 131 Глава 5. Циклы 132 Основы циклов 132 Примеры разных циклов 132 Структура циклов PL/SQL 133 Простой цикл 134 Завершение простого цикла: операторы ЕХГТи EXIT WHEN 135 Эмуляция цикла REPEAT UNTIL 136 Цикл WHILE 136 Использование счетчика в цикле FOR 138 Правила для циклов FOR с числовым счетчиком 138 Примеры циклов FOR с числовым счетчиком 139 Обработка нетривиальных приращений 140 Использование курсора в цикле FOR 140 Примеры цикла FOR с курсором 141 Метки цикла 142 Полезные советы 144 Использование для счетчиков циклов понятных имен 144 Как правильно попрощаться 145 Получение информации о выполнении цикла FOR 146 SQL-инструкция в качестве цикла 146 Глава 6. Обработка исключений 149 Как в PL/SQL обрабатываются ошибки 150 Стратегия обработки исключений 150 Концепции и терминология обработки исключений 151
10 Содержание Определение исключений 152 Объявление именованных исключений 152 Как связать имя исключения с кодом ошибки 154 Об именованных системных исключениях 156 Область действия исключения 158 Как инициировать исключение 160 Оператор RAISE 160 Использование процедуры RAISE_APPLICATION_ERROR 162 Обработка исключений 163 Объединение нескольких исключений в одном обработчике 164 Необработанные исключения 165 Использование функций SQLCODE и SQLERRM в обработчиках исключений 165 Продолжение работы после инициирования исключения 166 Передача необработанного исключения 168 Использование стандартизированных программ обработки ошибок 171 Часть III. Работа с данными в PL/SQL Глава 7. Работа с программными данными 174 Именование программных данных 175 Обзор типов данных PL/SQL 176 Символьные типы данных 177 Числовые типы данных 177 Типы данных DATE TIMESTAMP и INTERVAL 178 Тип данных BOOLEAN 178 Двоичные данные 178 Типы данных R0WID и UROWID 179 Тип данных REF CURSOR 179 Типы данных для поддержки Интернета 179 Типы данных «any» 180 Типы данных, определяемые пользователем 180 Объявление программных данных 180 Объявление переменной 181 Объявление константы 181 Объявления с ограничениями 182 NOT NULL 182 Объявления с привязкой 183 Привязка к курсорам и таблицам 185 Преимущества объявлений с привязкой 185 Объявления с привязкой и ограничение NOT NULL 187 Подтипы данных, определяемые программистом 187 Преобразование типов данных 188 Неявное преобразование типов 188 Явное преобразование типов 190
Содержание 11 Глава 8. Строки 196 Наборы символов 196 Что такое набор символов 196 Типы наборов символов 197 Набор символов базы данных и набор символов национального языка 198 Вопросы, связанные с наборами символов 199 Строковые типы данных 204 Тип данных VARCHAR2 205 Тип данных CHAR 206 Типы данных NCHAR И NVARCHAR2 208 Строковые подтипы 208 О работе со строками 209 Пустая строка — это NULL 209 Смешение значений CHAR и VARCHAR2 210 Определение строковых констант 213 Строковые функции 214 Глава 9. Числа 243 Числовые типы данных 243 Тип данных NUMBER 244 Тип данных PLSJNTEGER 248 Тип данных BINARYJNTEGER 248 Числовые подтипы 248 Числовые преобразования 250 Модели форматирования чисел 250 Функция TO_CHAR 255 Использование функции CAST 260 Неявные преобразования 261 Числовые функции 262 Глава 10. Дата и время 271 Типы данных даты и времени 272 Тип данных DATE 272 Типы данных TIMESTAMP 273 Типы данных INTERVAL 277 Преобразование типов данных DATE и TIMESTAMP 280 Маски форматирования даты и времени 281 Преобразование строк в даты 285 Функция TO_DATE 286 Преобразование даты в строку 295 Функции CAST и EXTRACT 304 Арифметические операции над значениями даты-времени 306 Традиционные арифметические операции 306 Арифметические действия с интервалами 309 Функции обработки даты-времени 317 Функции получения даты и времени 318 Функции обработки информации о часовом поясе 320
12 Содержание функция ADD_MONTHS 322 Функция FROM_T2 324 функция LAST_DAY 325 Функция MONTH_BETWEEN 326 Функции ROUND и TRUNC 327 функция NEWJTIME 330 Функция NEXT.DAY 332 Глава 11. Записи и коллекции 334 Записи в PL/SQL 334 Эффективность использования записей 335 Объявление записей 336 Коллекции в PL/SQL 347 Определение типов коллекций и их объявление 352 Использование коллекций 356 Встроенные методы коллекций 360 Работа с коллекциями 368 Псевдофункции коллекций 393 Сопутствующие операции 399 Выбор типа коллекции 401 Глава 12. Другие типы данных 403 Тип данных BOOLEAN 403 Тип данных RAW 405 Типы данных UROWID и ROWID 405 Получение идентификаторов строк 406 Использование идентификаторов строк 407 Большие объекты данных 410 Тип данных BFILE 411 Тип данных BLOB 411 Тип данных CLOB 412 Тип данных NCLOB ». 412 Обработка больших объектов 413 Понятие локатора LOB 414 Большие объекты — пустые и равные NULL 415 Создание больших объектов 417 Запись данных в объекты типа LOB 418 Считывание данных из объектов типа LOB 420 Особенности данных типа BFILE 422 Временные объекты типа LOB 426 Встроенные операции над объектами типа LOB в Orade9i 430 Функции преобразования объектов типа LOB 434 Предопределенные объектные типы 434 Тип данных XMLType 435 Типы данных URI 438 Типы данных «any» 439
Содержание 13 Часть IV. SQL и PL/SQL Глава 13. DML и управление транзакциями 444 DML в PL/SQL 445 Краткое введение в DML 445 Атрибуты курсора для операций DML 448 Предложение RETURNING в DML-инструкции 450 DML и обработка исключений 451 DML и записи 452 Пакетные DML-операции и оператор FORALL 456 Оператор FORALL 457 Проблемы, вызываемые переключением контекста 458 Примеры использования оператора FORALL 460 Атрибуты курсора для оператора FORALL 461 Откат изменений, выполняемых с помощью оператора FORALL 462 Продолжение работы программы после исключения, инициированного при выполнении оператора FORALL 463 Управление транзакциями 464 Инструкция COMMIT 465 Инструкция ROLLBACK .,..466 Инструкция SAVEPOINT 466 Инструкция SET TRANSACTION 467 Инструкция LOCK TABLE 468 Автономные транзакции 468 Определение автономной транзакции 469 В каких случаях следует применять автономные транзакции 470 Правила и ограничения на использование автономных транзакций 470 Примеры автономных транзакций 475 Глава 14. Выборка данных 481 Основы курсоров 482 Некоторые термины, связанные с выборкой данных 483 Типичные операции с запросами и курсорами 484 Знакомство с атрибутами курсора 485 Ссылки на переменные PL/SQL в курсоре 488 Выбор между явным и неявным курсорами 490 Работа с неявными курсорами 491 Примеры неявных курсоров 492 Обработка ошибок, связанных с неявными курсорами 493 Атрибуты неявных курсоров 495 Работа с явными курсорами 497 Объявление явного курсора 498 Открытие явного курсора 501 Выборка данных из явного курсора 502 Псевдонимы столбцов явного курсора 503 Закрытие явного курсора 504
14 Содержание Атрибуты явных курсоров 506 Параметры курсора 507 Предложение BULK COLLECT 510 Ограничение на количество строк, возвращаемых инструкцией с предложением BULK COLLECT 512 Выборка более одного столбца 513 Совместное использование предложений RETURNING и BULK COLLECT 514 Инструкция SELECT с предложением FOR UPDATE 516 Снятие блокировок инструкцией СОММГТ 517 Предложение WHERE CURRENT OF 518 Курсорные переменные 520 Для чего нужны курсорные переменные 521 Сходство со статическими курсорами 522 Объявление типов REF CURSOR 523 Объявление курсорной переменной 523 Открытие курсорной переменной 524 Выборка данных из курсорной переменной 525 Правила использования курсорных переменных 528 Передача курсорных переменных в качестве аргументов 531 Ограничения, связанные с курсорными переменными 532 Курсорные выражения 533 Использование курсорных выражений 534 Ограничения, связанные с курсорными выражениями 535 Глава 15. Динамический SQL и динамический PL/SQL .536 Инструкции NDS 537 Инструкция EXECUTE IMMEDIATE 537 Инструкция OPEN FOR 540 Запросы с переменными-курсорами, возвращающие набор строк 541 Считывание данных в переменные или записи 543 Предложение USING в инструкции OPEN/ FOR 544 Универсальная процедура для выполнения запросов с предложением GROUP BY 545 Универсальный пакет для выполнения запросов с предложением GROUP BY 546 Передача значений параметров 548 Подстановка и конкатенация 548 Ограничения на использование метода подстановки 549 Режимы использования параметров 550 Дублирование формальных параметров 552 Передача значения NULL 553 Работа с объектами и коллекциями 553 Разработка приложений с использованием NDS 556 Совместное применение программ с правами вызывающего 556 Обработка ошибок 557 Динамический PL/SQL 559 Пакет NDS Utility 563
Содержание 15 Сравнение возможностей динамического SQL и пакета OBMS_SQL.. 564 Визуальное сравнение эквивалентных программ 564 Какую технологию в каких случаях предпочтительнее использовать 565 Часть V. Создание приложений PL/SQL Глава 16. Процедуры, функции и параметры 568 Модульный код 569 Процедура 570 Вызов процедуры 572 Заголовок процедуры 572 Тело процедуры 572 Дескриптор END 573 Оператор RETURN 573 Функции 573 Структура функции 574 Типы возвращаемых данных 576 Дескриптор END 576 Вызов функции 577 Функции без параметров 578 Заголовок функции 578 Тело функции 578 Оператор RETURN 579 Параметры 580 Объявление параметров 581 Формальные и фактические параметры 581 Связывание формальных и фактических параметров 582 Режимы использования параметра 584 Методы передачи параметров 587 Значения по умолчанию 589 Локальные модули 590 Преимущества локальной модуляризации 591 Область видимости локальных модулей 594 Локальные модули как средство повышения качества кода 594 Перегрузка модулей 594 Использование перегрузки 596 Поддержка разных типов данных 597 Ограничения на использование перегрузки 598 Предобъявления 599 Дополнительные вопросы 600 Вызов пользовательских функций в SQL 600 Ограничения для пользовательских функций, вызываемых в SQL 601 Директива RESTR1CT_REFERENCE (Orade8 и более ранние версии) 603 Табличные функции 605 Вызов функций в предложении FROM 606 Конвейерные функции 607
16 Содержание Функции трансформации 608 Функции, доступные для параллельного выполнения 612 Детерминированные функции б14 Модульный подход — в жизнь! 615 Глава 17. Пакеты 616 Для чего нужны пакеты 616 Демонстрация возможностей пакетов 617 Понятия и концепции, связанные с пакетами 620 Диаграмма Буча 621 Правила построения пакетов 622 Спецификация пакета 622 Тело пакета 624 Инициализация пакетов 625 Правила вызова элементов пакета 629 Работа с данными пакета 630 Глобальные данные в одном сеансе Oracle 631 Глобальные общие данные 631 Пакетные курсоры 632 Повторно инициализируемые пакеты 636 В каких случаях следует использовать пакеты , 638 Инкапсуляция операций сданными 639 Исключение жесткого кодирования литералов 641 Устранение недостатков встроенных функций 642 Группировка логически связанных функций 642 Кэширование статических данных сеанса 643 Пакеты и объектные типы 644 Глава 18. Триггеры. .„.645 Триггеры уровня инструкций DML , 646 Концепции триггеров 647 Создание триггера 649 Пример аудита, выполняемого с помощью триггера 654 Триггеры одного типа 660 Ошибки, возникающие при изменении таблицы: проблема и решение 662 Триггеры уровня DDL 669 Создание триггера 669 События триггеров 671 Параметры триггеров 672 Применение событий и параметров 674 Удаление неудаляемого триггера 677 Триггеры событий базы данных 678 Создание триггера события базы данных 678 Триггеры STARTUP 679 Триггеры SHUTDOWN 679 Триггеры LOGON 680 Триггеры LOGOFF 680
Содержание 17 Триггеры SERVERERROR 680 Неработоспособные триггеры 684 Триггеры INSTEAD OF 664 Триггер INSTEAD OF INSERT 686 Триггер INSTEAD OF UPDATE 688 Триггер INSTEAD OF DELETE 689 Заполнение таблиц 689 Триггеры AFTER SUSPEND 690 Предпосылки создания триггера 690 Пример реализации 691 Создание триггера 694 Функция ORA_SPACE_ERROR_INFO 694 Пакет DBMS_RESUMABLE 695 Многократный запуск триггера 696 Сопровождение триггеров , 697 Отключение, включение и удаление триггеров 697 Просмотр триггеров 698 Проверка состояния триггера VALID 699 Глава 19. Управление приложениями PL/SQL 700 Управление программным кодом и его анализ в базе данных 701 Представления словаря данных для программистов PL/SQL 701 Вывод информации о хранимых объектах 702 Вывод и поиск исходного кода 704 Защита кода хранимой программы 706 Как скрыть исходный код 706 Работа со скрытым кодом 707 Встроенная компиляция 708 Настройка, выполняемая администратором базы данных 709 Сравнение интерпретируемого и компилируемого режимов 710 Тестирование программ PL/SQL 711 Неэффективные технологии тестирования 712 Где найти дополнительную информацию 717 Отладка программ PL/SQL 717 Неупорядоченная отладка 718 Иррациональная отладка 719 Советы и стратегии отладки 719 Оптимизация программ PL/SQL 723 Анализ производительности PL/SQL 724 Трассировка выполнения кода 727 Повышение производительности приложения 730 Как избежать выполнения ненужного кода 730 Умение слушать 734 Использование пакетных данных для минимизации SQL-обращений к базе данных 736 Использование предложения BULK COLLECT и оператора FORALL 737
18 Содержание Часть VI. Особые возможности PL/SQL Глава 20. Выполнение программ PL/SQL.... 740 Заглянем внутрь 741 Концепции PL/SQL 741 Хранение PL/SQL-кода на сервере 747 Слишком большой DIANA-код 750 Управление зависимостями 751 Зависимости в серверном PL/SQL 751 Восстановление работоспособности 755 Взаимосвязи в клиентском PL/SQL 758 Удаленные зависимости 760 Как PL/SQL использует память сервера Oracle 761 Память сервера 762 Курсоры и память 764 Советы по экономии памяти 765 Трассировка памяти 774 Выполнение серверного кода PL/SQL 776 Компиляция анонимного блока 776 Компиляция хранимого объекта 777 Выполнение программ PL7SQL 778 Клиентский код PL/SQL 779 Поддерживаемые версии и функциональные возможности 780 Ограничения, касающиеся удаленных вызовов в Oracle 782 Клиентские библиотеки PIVSQL 784 Модели разрешений 787 Модель разрешений владельца 787 Модель разрешений на выполнение программ 792 Комбинированная модель разрешений л 794 Аппаратное обеспечение для PL/SQL: больше — лучше? 795 Однопроцессорная система 796 Симметричные мультипроцессорные системы 796 Кластерные системы 797 Что вам нужно знать 797 Глава 21. Объектно-ориентированные возможности PL/SQL 799 Обзор объектных возможностей Orade 800 Пример объектного приложения 802 Иерархия типов 802 Методы 805 Запись, извлечение и использование хранимых объектов 811 Эволюция и создание типов 818 И снова указатели 820 Типы данных «any» 828 Все делаем сами 832 Сравнение объектов "!!""..835
Содержание 19 Объектные представления 839 Существующие реляционные системы 840 Объектное представление с атрибутом-коллекцией 842 Объектные подпредсгавления 845 Объектное представление, реализующее обратное отношение 846 Триггеры INSTEAD OF 847 Различие между объектными представлениями и объектными таблицами 849 Сопровождение объектных типов и объектных представлений 850 Привилегии 852 О целесообразности применения объектно-ориентированного подхода 854 Глава 22. Взаимодействие Java и PL/SQL 857 Oracle и Java 857 Подготовка к использованию Java в Oracle 859 Установка Java 859 Проектирование и компиляция кода Java 860 Определение привилегий, необходимых для разработки и выполнения Java-кода 860 Примеры использования Java 862 Выбор функциональных элементов 863 Создание пользовательского класса Java 863 Компиляция и загрузка класса в Oracle 865 Создание оболочки PL/SQL 866 Удаление файлов из PL/SQL 867 Утилита loadjava 867 Утилита dropjava 871 Управление объектами Java в базе данных 872 Пространство имен Java в Oracle 872 Пакет DBMSJAVA 873 Функция LONGNAME: преобразование длинных имен Java 874 Функция GET_ COMPILER_OPTION и процедуры SET_ и RESET_COMPILER_OPTION: чтение и установка параметров компилятора 875 Процедура SET_OUTPUT: управление выводом из Java 876 Процедуры EXPORT_SOURCE, EXPORT_RESOURCE и EXPORTJXASS: экспорт объектов схемы 876 Как сделать методы Java доступными в PL/SQL 878 Спецификация вызова 878 Некоторые правила написания оболочек для Java-методов 880 Сопоставление типов данных 880 Вызов метода Java в SQL 882 Обработка исключений с помощью Java 882 Новые возможности файлового ввода-вывода 885 Другие примеры использования Java 890
20 Содержание Глава 23. Внешние процедуры 892 Концепция внешних процедур 893 Пример выполнения команды операционной системы 893 Механизм вызова внешних процедур 895 Особенности выполнения внешних процедур в Oracle 896 Конфигурирование Oracle Net 897 Настройка параметров конфигурации листенера 897 Уровень защиты, установленный в данной конфигурации 899 Создание библиотеки Oracle ; 900 Вызов внешней функции 901 Спецификация вызова 902 Список внешних параметров 903 Сопоставление параметров 905 Синтаксис предложения PARAMETERS 907 Свойства параметров 908 Инициирование исключений из программ на языке С 911 Агенты, отличные от используемых по умолчанию 914 Сопровождение внешних процедур 917 Удаление библиотек 917 Словарь данных 917 Советы по использованию внешних процедур 917 Алфавитный указатель 919
Предисловие Миллионы разработчиков приложений и администраторов баз данных но всем ми- мире используют программное обеспечение компании Oracle для создания комплекс- комплексных систем управления, большими объемами данных. Сердцем программного обес- обеспечения Oracle является PL/SQL - язык программирования, представляющий собой процедурное расширение Oracle-версии языка SQL (Structured Query Lan- Language) и использующийся для разработки приложений в среде Oracle Developer, самыми известными компонентами которой являются Forms Developer и Reports Developer. PL/SQL включается в состав практически каждого нового продукта, выпускае- выпускаемого Oracle. Профессиональные разработчики программного обеспечения поль- пользуются им для выполнения самых разнообразных задач: О реализации бизнес-логики в Oracle Server с помощью хранимых процедур PL/SQL и триггеров базы данных; О формирования XML-документов, их хранения в базе данных, а также для вы- выполнения операций с этими документами; О связывания web-страниц с базой данных Oracle; О выполнения и автоматизации задач администрирования базы данных - от за- защиты на уровне строк до управления сегментами отката в программах PL/SQL PL/SQL создан по образцу языка Ada1, разработанного в свое время для целей Министерства обороны США. Это высокоуровневый язык, в основу которого по- положены концепции абстракции данных, сокрытия информации и другие совре- современные стратегии разработки программного обеспечения. Oracle включила в него самые прогрессивные элементы процедурных языков программирования (причем в очень удачном сочетании!), благодаря чему получился исключительно мощный, гибкий и в то же время простой язык. Перечислим главные из них: О полный диапазон типов данных, от чисел и строк до таких комплексных струк- структур, как записи (аналогичны строкам таблиц реляционной базы данных), кол- коллекции (Oracle-версия массивов) и даже тип XMLType, предназначенный для управления XML-документами, которые обычно хранятся в базе данных Oracle; О явно определяемая и наглядная структура блоков, облегчающая расширение и сопровождение приложений PL/SQL; 1 Язык программирования, названный в честь математика Ады Лавелейс, считающейся первым в ми- мире программистом. Информация о языке Ada имеется по адресу http://www.adahome.com.
22 Предисловие О операторы управления потоком, в том числе операторы условного перехода, оператор CASE и три вида циклов; О обработчики исключений для обработки ошибок; О повторно используемые именованные блоки кода, такие как функции, проце- процедуры, триггеры, объектные типы (подобные классам в объектно-ориентиро- объектно-ориентированных языках программирования) и пакеты (наборы логически связанных программ и переменных). PL/SQL тесно интегрирован в язык SQL Oracle: SQL-инструкции можно вы- выполнять непосредственно из процедурной программы, без помощи какого-либо промежуточного API (программного интерфейса) вроде JDBC (Java DataBase Con- Connectivity) или ODBC (Open DataBase Connectivity). Более того, с помощью инст- инструкций SQL можно вызывать собственноручно написанные функции PL/SQL. Разработчики приложений Oracle, не желающие отставать от времени, долж- должны максимально полно использовать возможности языка PL/SQL. Для этого им сначала нужно освоить отдельные функциональные компоненты PL/SQL, набор которых постоянно расширяется, а затем научиться их совместному применению при разработке сложных приложений. Стала очевидной потребность разработчиков приложений Oracle в учебнике с подробнейшим изложением основ PL/SQ, где содержались бы не только описа- описания базовых блоков языка, но и примеры программ и приложений с их использо- использованием. Лишь при этом условии читатели смогут осваивать язык не методом проб и ошибок, а наиболее быстрым и эффективным способом — опираясь на опыт, на- наработанный другими программистами. Как и в любом другом языке, в PL/SQL практически любую задачу можно выполнить несколькими способами, но не все они подходят для конкретного случая. Настоящая книга призвана помочь вам со- сориентироваться в море возможностей, предоставляемых этим языком, научиться наиболее эффективному и продуктивному его использованию. Задачи этой книги Приступая к работе над книгой, авторы составили план и впоследствии стара- старались неуклонно ему следовать. Ниже описаны цели и задачи, которые были опре- определены как основные. О Изучение всех возможностей PL/SQL. В документации Oracle описаны все функциональные компоненты PL/SQL, но ничего не говорится о том, как их следует применять для достижения максимального эффекта. Обычно в учеб- учебных пособиях и руководствах освещаются одни и те же стандартные темы од- одним и тем же, часто весьма ограниченным, образом. Мы же постарались выйти за пределы этих ограничений и рассмотреть наряду со стандартными и нетри- нетривиальные, часто более эффективные методы решения тех или иных задач. О Использование PL/SQL для решения повседневных задач. В книге проана- проанализирован ряд интересных и наиболее целесообразных подходов к решению ти- типичных задач, с которыми ежедневно приходится сталкиваться разработчикам.
Предисловие 23 Поэтому здесь приведено так много примеров, причем не только небольших фрагментов кода, но и достаточно крупных компонентов приложений, кото- которые, возможно с определенными модификациями, читатели смогут интегри- интегрировать в собственные проекты. Очень много кода содержится в самой книге, но еще больше представлено на web-узле издательства O'Reilly, откуда его можно свободно загрузить на свой компьютер. Многие примеры демонстриру- демонстрируют, можно сказать, процесс выработки эффективных решений. Вы поймете, как на практике применять предлагаемые PL/SQL средства, пользуясь нестан- нестандартными или недокументированными возможностями и методами. О Создание эффективного кода, который легко сопровождать. PL/SQL обла- обладает большим потенциалом, позволяющим в рекордно быстрые сроки разраба- разрабатывать достаточно мощные приложения. Однако после создания приложения его жизненный цикл не заканчивается, а, наоборот, часто лишь начинается. И уже готовое приложение в дальнейшем может претеряевать множество из- изменений, связанных с его совершенствованием, адаптацией к изменяющимся требованиям и условиям, расширением и т. д. Поэтому мы не посчитали бы свою задачу выполненной, если бы научили вас только писать в предельно ко- короткие сроки необходимые программы. Мы хотим помочь вам освоить прин- принципы и приобрести навыки построения гибких приложений, программный код которых легко читается, понятен, без труда модифицируется — одним словом, требует минимальных затрат на сопровождение и доработку. Структура книги Оба автора книги и издательство O'Reilly & Associates приложили максимум уси- усилий, чтобы как можно более полно осветить процесс развития PL/SQL. В первом издании книги рассматривалось большинство функций PL/SQL, реализованных в PL/SQJL Release 2.3. Второе издание было дополнено описанием новых элемен- элементов языка, введенных в Oracle8, но представлентгым отдельно от остального со- содержимого книги. В настоящем, третьем, издании принят другой подход. Здесь описаны функции PL/SQL версии 9.2. РСУБД Огас1е9г Release 2 (9.2). Поэтому независимо от используемой вами версии Oracle - будь то Oracle7.3.4 (хотя мы надеемся, что у вас установлена более современная версия) или Огас1е9г - книга поможет найти ответ на любой вопрос. Вся представленная информация тесно связана; здесь нет отдельного раздела об Oracle9i. Теперь, заинтересовавшись ка- какой-либо конкретной функцией языка, откройте соответствующий раздел книги, и вы найдете там исчерпывающую информацию о доступности и степени реали- реализации этой функции в разных версиях Oracle. Еще в 1994 году, когда Стивен Фейерштейн только начал писать о PL/SQL, данный язык был настолько «скромен», что его полное описание можно было по- поместить в одну книгу. К 1997 году это сделать стало уже невозможным, и в изда- издательстве O'Reilly вышло несколько книг, посвященных разным аспектам исполь- использования PL/SQL, и в частности книга о встроенных пакетах Oracle.
24 Предисловие Работая над структурой данного издания, мы решили внести ряд изменений и дополнений. О Прежде всего, и что наиболее существенно, в книгу добавлены важные сведе- сведения, которых в ней недоставало, в том числе информация о принципах разра- разработки триггеров базы данных. О Теперь более полно используются возможности Интернета. Вместо того что- чтобы включать в книгу огромное количество сложных примеров, мы выделили их в отдельную группу и разместили на web-узле издательства O'ReUly, распо- расположенном по адресу http://www.oreilly.com/catalog/oraclep3 (подробнее о них рас- рассказывается в разделе «О программном коде»). Но не волнуйтесь — в книге все равно осталось очень много примеров. О После долгих размышлений мы вынесли на web-узел и несколько глав второго издания. Речь идет о главе 17, которая называется «Вызов функций PL/SQL из SQL», и главе 22, имеющей название «Советы по разработке кода». (Вопросы разработки кода также подробно освещены в отдельной книге, Oracle PL/SQL Best Practices.) О Чтобы улучшить общую структуру книги и уменьшить ее объем, многие главы пришлось реорганизовать. Например, вместо того чтобы по отдельности рас- рассматривать символьные данные и символьные функции, мы объединили в од- одной главе всю информацию, связанную со строками. Мы также объединили несколько других глав, и в частпости главы 24-26 предыдущего издания, по- посвященные отладке, повышению производительности и трассировке приложе- приложений. Большие фрагменты текста, не имеющие принципиального значения для книги, и в первую очередь анекдоты также были перемещены на web-узел. Авторы очень довольны результатами своего труда и надеются, что ими буде- будете довольны и вы. Книга теперь несет гораздо больше информации, чем раньше, и мы постарались при изложении материала сохранить присущий всем нашим публикациям юмор и разговорный тон, которые, судя иб отзывам читателей, об- облегчают чтение и способствуют восприятию содержимого. Так как у нас появилось несколько соавторов, вклад которых в третье издание книги весьма значителен, приведем краткую сводку о том, какая глава кем была написана. Глава 1 2 3 4 5 Автор Сгивен Билл Вилл и Стивен Джонатан и Стивен Стивен Глава 6 7 8 9 10 Автор Стивен Стивен Джонатан и Стивен Джонатан и Стивен Джонатан и Стивен Глава 11 12 13 14 15 Автор Стивен и Билл Джонатан Стивен Стивен Стивен Глава 16 17 18 19 20 Автор Стивен Стивен Дарил Стивен Билл Глава 21 22 23 Автор Билл Стивен Билл
Предисловие 25 О содержании Теперь наша книга состоит из шести частей. О Часть I. Программирование на PL/SQL. Главу 1 мы начнем с рассказа об ис- истории языка и основных его возможностях. Затем приведем краткий обзор не- некоторых главных функций PL/SQL. Глава 2 построена таким образом, чтобы вы с ходу могли начать программировать: она содержит четкие и простые ин- инструкции по выполнению кода PL/SQL в среде разработки SQL*Plus и описа- описание ряда распространенных инструментальных средств. В главе 3 рассказыва- рассказывается об основах языка PL/SQL: что такое оператор, какова структура блока, как создавать комментарии и т. п. О Часть II. Структура программы PL/SQL. В главах 4- 6 рассматриваются ус- условные (IF и CASE) и последовательные (GOTO и NULL) операторы управления по- потоком, циклы и методика обработки исключений в языке PL/SQL. Ознакомив- Ознакомившись с этой частью книги, вы научитесь составлять блоки кода, соответствую- соответствующие сложной логике приложения. О Часть III. Работа с данными в PL/SQL. Почти любая написанная вами про- программа должна выполнять те или иные операции с данными, и часто эти дан- данные являются локальными для процедуры или функции PL/SQL. Представ- Представленные здесь главы 7-12 посвящены различным типам программных данных, которые можно определять прямо в PL/SQ.L, таким как числа, строки, записи и коллекции. Вы узнаете о новых типах данных, введенных в Огас1е9г, и в ча- частности об INTERVAL, TIMESTAMP, XMLType. Кроме того, мы расскажем о встроенных функциях, предоставляемых Oracle в ваше распоряжение для выполнения раз- различных операций с данными. О Часть IV. SQL и PL/SQL. В главах 13-15 рассказывается об организации взаимодействия приложений с базами данных Oracle, осуществляемого по- посредством SQJL Из них вы узнаете, как определить транзакции, обновляющие, дополняющие и удаляющие данные таблиц, как запросить из базы данных ин- информацию для обработки в программах PL/SQL, как динамически выполнить SQL-инструкцию, используя возможности NDS, введенного в Oracle8i. О Часть V. Разработка приложений PL/SQL. В этой части книги обобщается весь материал, изложенный ранее. Приступая к ее изучению, вы уже будете знать, как объявлять переменные и как с ними работать, освоите важнейшие принципы обработки ошибок и построения циклов. В главах 16-19 рассказы- рассказывается о самых крупных «строительных блоках» приложений: процедурах, функ- функциях, триггерах и пакетах. В главе 19 также обсуждаются вопросы управления кодом PL/SQL, его оптимизации и отладки. О Часть VI. Особые возможности PL/SQL. Язык PL/SQL, столь мощный и бо- богатый, предоставляет немало функциональных возможностей и структурных элементов, которые, хотя используются и не очень часто, но позволяют макси- максимально просто и эффективно решать задачи, справиться с которыми при по- помощи других средств было бы очень трудно или вообще невозможно. В главе 20 рассказывается об этапах выполнения кода PL/SQL, об использовании памя- памяти, описано различие между серверным и клиентским PL/SQL. В главе 21 рас- рассматриваются объектно-ориентированные элементы Oracle (объектные типы
26 Предисловие и объектные представления). А в главах 22 и 23 показано, как вызывать код Java и С из приложений PL/SQL. Если пам уже приходилось заниматься программированием, но с PL/SQL вы еще не знакомы, прочтите нашу книгу от начала до конца. Но если вы являетесь профессиональным разработчиком приложений PL/SQL, то, возможно, вам бу- будет достаточно ознакомиться лишь с отдельньши ее разделами, с тем чтобы осво- освоить новые технологии. Но независимо от того, в каком качестве - учебника или справочного пособия — вы будете использовать эту книгу, надеемся, что она по- поможет вам научиться с максимальной отдачей применять все возможности, пре- предоставляемые PL/SQL. Какие темы нами не рассматриваются Какой бы объемной ни была книга, в ней невозможно рассказать обо всем, что ка- касается излагаемой темы. Oracle - огромная и сложная система, и мы рассматри- рассматриваем только входящий в ее состав язык PL/SQL. Ниже перечислены вопросы, ко- которые, к сожалению, выходят за рамки нашего издания, поэтому мы упоминаем о них лишь поверхностно. О Язык SQL. Мы предполагаем, что вы знаете язык SQL и даже имеете опыт его использования, в частности, можете составлять инструкции SELECT, INSERT, UP- UPDATE и DELETE. О Администрирование баз данных Oracle. Ознакомившись с книгой, админист- администраторы баз данных, конечно же, почерпнут из нее много полезного и интересно- интересного, в частности научатся писать PL/SQL-программы для создания и обслужи- обслуживания баз данных. Но по известной вам причине у нас не было возможности рассказать о нюансах языка определения данных (Data Definition Language, DDL), входящего в состав Oracle SQL. ' О Оптимизация приложений и баз данных. Тема оптимизации также рассмотре- рассмотрена нами лишь поверхностно: некоторые из относящихся к ней вопросов (в ча- частности, процессы трассировки, отладки и оптимизации программ PL/SQL), освещаются в главе 19. Те же читатели, кому такого рода информация необхо- необходима, могут обратиться к другим книгам, специально посвященным оптимиза- оптимизации приложений Oracle. О Технологии разработки приложений Oracle, не зависимые от PL/SQL. Очень мало мы рассказали и о методике разработки приложений с помощью таких инструментальных средств, как, скажем, Oracle Forms Developer, и это несмот- несмотря на то, что в них также используется язык PL/SQL. Мы решили сконцен- сконцентрировать ваше внимание на фундаментальных возможностях языка и на тех операциях, которые с его помощью можно выполнять в базах данных. Однако практически все сказанное здесь относится к использованию PL/SQL и в Forms Developer, и в Reports Developer. О Поддержка национальных языков в Oracle. В книге отсутствует и описание встроенных в Oracle средств поддержки национальных языков (National Lan- Language Support, NLS), предназначенных для разработки приложений на разных языках.
Предисловие 27 Используемые соглашения Ниже перечислены соглашения, которым мы старались строго следовать при из- изложении материала. О Стиль interface. Так выделяются имена файлов, URL, элементы интерфейса и команды среды разработки. О Стиль command. Так в книги выделяются фрагменты программного кода, эле- элементы языка и различные идентификаторы. О Курсив. Так выделяются новые термины и слова, на которых в тексте делается акцент. О ВЕРХНИЙ РЕГИСТР. В примерах кода ключевые слова PL/SQL, как прави- правило, представлены символами верхнего регистра. О нижний регистр. Символами нижнего регистра в примерах кода набраны иден- идентификаторы, определяемые пользователем, а именно имена переменных, пара- параметров и т. п. О Знаки препинания. При наборе примеров кода знаки препинания нужно вво- вводить в точности так, как указано в книге. О Отступы. Используемые в примерах кода отступы помогают отразить струк- структуру программ, но они не являются обязательными. О Квадратные скобки ([ ]). В описаниях синтаксиса в квадратные скобки заклю- заключаются необязательные элементы. О Фигурные скобки ({}). В описаниях синтаксиса в фигурные скобки заключа- заключается набор элементов, из которых нужно выбрать только один. О Вертикальная черта (|). В описаниях синтаксиса вертикальная черта разделяет элементы, заключенные в фигурные скобки, например: {TRUE | FALSE}. О Многоточие (...). В описаниях синтаксиса многоточие обозначает повторяю- повторяющиеся элементы, а в примерах кода — фрагменты программ, которые не отно- относятся к обсуждаемой теме и потому опущены. ПРИМЕЧАНИЕ ¦ В данном абзаце обычно содержатся советы, предложения и примечания. Таким образом может быть помечена информация, специфическая для конкретной версии Oracle. ВНИМАНИЕ Абзац с этим заглавием содержит всякого рода предупреждения. В частности, здесь могут упоми- упоминаться установки, которые оказывают отрицательное влияние на операционную систему или Orade. Платформа и версия Излагаемый материал и представленные в этом издании книги примеры, как пра- правило, не относятся к какой-либо конкретной машине или операционной системе. Что касается версий Oracle, то в тех случаях, когда приведенное описание касает- касается только Oracle8i или Oracle9f, об этом говорится явно. Существует множество версий PL/SQL, и не исключено, что вы будете поль- пользоваться даже несколькими из них. В главе 1 описываются наиболее широко ис- используемые в настоящее время версии этого языка, о которых вам следует знать (см. раздел «Работа с несколькими версиями PL/SQL»).
28 Предисловие О программном коде Весь приведенный в книге программный код мы поместили и на ее web-страницу, представленную на узле издательства O'Reilly. Чтобы попасть на нее, восполь- воспользуйтесь адресом: http://www.oreilly.com/catalog/oraclep3 и щелкните на ссылке Examples. Как уже упоминалось вьгок, помимо примеров по указанному адресу вы най- найдете несколько глав второго издания книги, изъятых из третьего издания или значительно сокращенных. Они могут быть особенно полезны читателям, кото- которые работают со старыми версиями Oracle. В тех случаях, когда приведенный в книге пример имеется на узле (совсем ма- маленькие фрагменты кода мы на узел не помещали), в первой строке примера при- приведено имя соответствующего файла. Например: /* Файл в web: Файл.pkg */. Комментарии и вопросы Мы постарались самым тщательным образом проверить всю приведенную в кни- книге информацию и протестировать программный код, но при таком объеме и раз- разнообразии информации, при такой скорости развития новых технологий могли быть допущены некоторые ошибки. Если вы обнаружите таковые в тексте или примерах, пожалуйста, напишите нам об этом или сообщите по телефону. Сообщаем наши координаты: O'Reilly & Associates 1005 Gravenstein Highway Sebastopol, CA 95472 8000-998-9938 (в США или Канаде) 707-829-0515 (международный или локальный) 707-829-0104 (факс) Вы, конечно же, можете послать нам сообщение и по электронной почте. А если хотите, чтобы вас включили в список рассылки, или желаете заказать каталог, обязательно напишите по адресу: info@oreilly.com Задать вопросы, прислать собственные комментарии к книге можно по адресу: bookquestions@oreilly.com Как упоминалось в предыдущих разделах, у книги имеется web-страница, где представлены программный код, обновленные ссылки, главы из предыдущих из- изданий и информация об ошибках. Адрес этой страницы: http://www.oreilly.com/catalog/oradep3 За дополнительной информацией об этой и других книгах издательства O'Reilly можно обратиться по адресу: http://www.orellly.com
Предисловие 29 Благодарности За время, прошедшее после выхода из печати в 1995 году первого издания этой книги, мы получили столько откликов, что прежде всего хотим выразить призна- признательность нашим читателям - за их поддержку, конструктивные замечания, за по- помощь в устранении замеченных ошибок. Для того чтобы наша книга содержала точную, доступную и актуальную ин- информацию, в нее вложен огромный труд, причем не только ее авторов, но и мно- множества других специалистов по Oracle, сотрудников издательства O'Reilly & As- Associates, а также наших друзей и членов семей. Ниже мы постарались перечислить всех, кто помогал нам в работе над третьим изданием книги. Далее приведены имена тех, кто внес значительный вклад в ее два первых издания. Прежде всего мы хотели бы поблагодарить за большой вклад в создание этой книга четырех замечательных специалистов по технологиям Oracle, а именно Джо- Джонатана Генника, Дарил Харлей, Брина Левелина и Стива Адамса. Джонатан су- существенно переработал пять старых глав, а Дарил написал интереснейшую главу о триггерах базы данных, помог нам рассказать о средствах поддержки нацио- национальных языков и стандартов в Oracle. Брин Левелин, менеджер продукта Oracle PL/SQL, предоставил важную информацию о новых функциях Oracle9i, отвечал на наши бесконечные вопросы о различных особенностях языка PL/SQL, прояв- проявляя при этом безграничное терпение, прорецензировал многие главы книги. Стив написал превосходный и достаточно подробный обзор всех этапов выполнения кода PL/SQL. Для рецензирования такой большой книги потребовалось, как вы понимаете, немало специалистов, причем мы просили их протестировать каждый фрагмент кода и каждую программу, с тем чтобы в печатной версии осталось как можно меньше ошибок. Мы глубоко признательны следующим профессионалам по Orac- Oracle PL/SQL, которые, не жалея ни сил ни времени, работали вместе над совершен- совершенствованием нашего труда. Это Дон Бейлс, Дик Болз, Дэн Клемидж, Стив Коснер, Жерард Харттерс, Двейн Кинг, Лери Элкинс, Чандрасекхаран Ийер, Вадим Лоев- ски, Джованни Жаремильо, Ракеш Пател, Джеймс Пэдфилд, Питер Линслей, Кри- Кристофер Рекикот, Алекс Романкевич, Скотт Сауерс, Джей Томас, Эдвард Ван Хат- тен, Саймон Лаурент, Тони Грофорд, Джеф Честер и Андре Вергисон. Но издание любой книги требует участия и других профессионалов. Команда литературных, технических и художественных редакторов во главе с нашим доб- добрым другом Деборой Рассел сделала все возможное, чтобы книга получилась дос- достойной столь уважаемого издательства, как O'Reilly. Огромная благодарность Ро- Робу Романо, создавшему превосходные иллюстрации, Джулии Фланеган, помощ- помощнику редактора, и всем остальным членам команды. Ниже перечислены имена людей, которым мы очень благодарны за их вклад в первое и второе издания книги. Дженифер Блейр, Ерик Камплин, Джо Селко, Авери Коен, Томас Дунбар, Р. Джеймс Форстайз, Майк Ганглер, Габриел Хофман, Карен Пейзер, Пит Шаффер, Девид Томпсон, Кейлейн Барклей, Супил Бхаргава, Борис Бурштейн, Грей Глос- сман, Радхакришна Хари, Джеймс Мэлори, Нимеш Мехта, Джеф Мюллер, Дейв Поснер, Крис Рекикот, Питер Вастерд, Зона Валкотт, Сохаиб Абасси, Пер Брон- дум, Иван Чонг, Билл Двайт, Стив Эрлич, Бушан Фоутдар, Кэн Джакобс, Нимиш
30 Предисловие Мехта, Стив Мьюенч, Шри Раждан, Марк Рихтер, Билл Хинман, Тони Заэмба, Джон Кордел, Беверли Гибсон, Майк Сьерра, Гиги Эстабрук, Эдди Фидман, Дон- Донна Вунтейлер, Крис Рейли, Мишель Вилэй, Дебби Кунха, Микаэл Дойч, Джон Файлз, Джульетта Мьюельнер, Кори Виллинг, Сет Мейшлин, Кисмет МакДона- хью Чен, Клэрмари Фишер О'Леари, Эрик Гилвер, Берт Скалзо, Джон Березневич, Том Уайт, Стив Хилкер, Томас Керейн, Шириш Пураник, Кэнон Мутхукарруп- пан, Джейн Эллин, Кимо Картер, Мадлен Ньюел, Роб Романо, Нэнси Прайест, Сет Мейшлин, Фред Полизо, Дональд Херкимер, Эрван Дарнел, Гарри Серносек, Патрик Прибил, Бил Уаткинс, Дебора Льюик и Лео Локк. Напоследок Стивен благодарит свою жену, Веву Сильва, и двух сыновей, Криса Сильва и Эли, за их поддержку и терпение. Билл выражает сердечную признательность своей семье за неизменную лю- любовь и хорошее настроение, несмотря на его почти круглосуточную занятость. (Как часто в его доме можно было услышать фразу: «Мама, папа снова заснул за компьютером!») От издательства Уважаемые читатели! Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютер- компьютерная редакция). Мы будем рады узнать ваше мнение! Информацию о других книгах издательств «Питер» и «Издательская группа BHV» вы найдете на web-узлах http://www.piter.com и http://www.bhv.kiev.ua.
Часть! П рогра м м и рова н ие на PL/SQL В первой части книги вы познакомитесь с языком PL/SQL, освоите его фундамен- фундаментальные концепции, узнаете, как выполнять код PL/SQJ.. В главе 1 рассказывает- рассказывается о создании PL/SQf., его достоинствах и основных особенностях. Глава 2 помо- поможет вам научиться писать и запускать простейшие программы. В ней содержат- содержатся инструкции по выполнению кода PL/SQJL в SQJ, "Plus и некоторых других средах. В главе 3 рассматриваются структура и ключевые слова языка. Из нее вы узнае- узнаете, что такое оператор PL/SQL, что такое блок и какова его структура, как ор- организуются комментарии к программам. ? Глава 1. Введение в PL/SQL ? Глава 2. Написание и запуск кода PL/SQL D Глава 3. Основы языка
1 Введение в PL/SQL > История PL/SQL > Так вот что такое PL/SQL > О версиях PL/SQL > Ресурсы для разработчиков PL/SQ > Несколько советов PL/SQL — это аббревиатура от Procedural Language extensions to the Structured Query Language, что в переводе с английского означает «процедурные языковые расширения для SQL». SQL 1гредставляет собой повсеместно распространенный язык формирования запросов, с помощью которых можно получать и обновлять информацию в реляционных базах данных. PL/SQL был разработан компанией Oracle Corporation с целью преодоления некоторых ограничений SQL и обес- обеспечения разработчиков приложений баз данных Oracle более совершенным инст- инструментарием для программирования. Перечислим основные особенности и достоинства PL/SQL О Высокоструктурированный, читабельный, удобный для сопровождения язык. Основанный на той же модели, что и язык Ада, PL/SQL, тем не менее, облада- обладает всеми достоинствами современных языков программирования. Если вы на- начинающий программист, то постигать азы этой профессии с PL/SQL будет очень легко. Ну а опытному программисту освоить его синтаксис ничего не стоит. Программный код на языке PL/SQL легко читается, что значительно упрощает его сопровождение и поддержку — а это важная характеристика ка- качественного программного обеспечения. О Стандартизированный и переносимый язык разработки приложений для баз данных Oracle. Если вы создали процедуру или функцию на PL/SQL для базы данных Personal Oracle, находящейся на портативном компьютере, то эту же процедуру можно применить и для базы данных, установленной на компью- компьютере корпоративной сети, причем без каких-либо изменений (конечно, при усло- условии совместимости версий Oracle). «Написать один раз и использовать везде» —
История РЦ/SQL 33 таков основной принцип функционирования PL/SQL, принятый задолго до появления языка Java. «Везде» в данном случае означает «три работе с любой базой данных Oracle». О Встроенный язык. PL/SQL не используется как самостоятельный язык про- программирования. Это встроенный язык, функционирующий только в конкрет- конкретной «хост-среде». Программы на PL/SQL запускаются из базы данных (с помо- помощью приложения SQL*Plus). Их можно также выполнять из формы, созданной в среде разработки приложений клиент-сервер Oracle Developer (клиентский PL/SQJL). Однако создать исполняемый файл программы на PL/SQL и запус- запускать его автономно вы не сможете. О разработке и выполнении программ на PL/SQL рассказывается в главе 2. О Высокопроизводительный, высокоинтегрированный язык доступа к базам данных. В настоящее время имеется множество средств для разработки про- программного обеспечения, взаимодействующего с базами данных Oracle. Напри- Например, можно использовать Java и JDBC (или SQLJ), Visual Basic и ODBC, а можно воспользоваться, скажем, Delphi и C++. Однако для написания высо- высокоэффективного кода для доступа к базе данных Oracle больше всего подходит язык PL/SQL В частности, Oracle имеет несколько расширений, предназна- предназначенных специально для PL/SQL, таких как инструкция FORALL, позволяющая на порядок повысить производительность запросов. История PL/SQL В индустрии программного обеспечения компания Oracle является лидером в ис- использовании декларативного, непроцедурного подхода к разработке баз данных и приложений. Технология реляционной базы данных Oracle Server считается наиболее прогрессивной, мощной и надежной. Средства разработки приложений от Oracle, подобные Oracle Forms, обеспечивают очень высокую производитель- производительность за счет применения визуального проектирования — подхода, основанного на определении большинства характеристик программ и их элементов по умолча- умолчанию, что избавляет программиста от огромного объема рутинной работы. Истоки PL/SQL Вначале Oracle-разработчиков в полной мере удовлетворял декларативный иод- ход к созданию баз данных и приложений в сочетании с мощной и основательной реляционной технологией. Но с развитием баз данных возрастали и требования к средствам разработки. Все чаще у программистов стала возникать потребность «проникнуть внутрь» продуктов. Им нужно было интегрировать в хранимые про- процедуры и формы разнообразные и достаточно сложные формулы, исключения и правила. В 1991 году была выпущена версия Огас1е6.0, что явилось важным шагом в развитии технологии баз данных Oracle. Ключевым компонентом новой версии стало так называемое процедурное дополнение, или PL/SQL. Примерно в то же время появилось долгожданное обновление для среды разработки SQL*Forms
34 Глава 1 • Введение в РЦ/SQL версии 2.3 (сейчас этот продукт называется Oracle.Forms). В SQL'Forms 3.0 был впервые интегрирован язык PL/SQL, позволяющий разработчикам создавать процедуры таким естественным способом, как программирование. Возможности первой версии PL/SQL были очень ограниченны. На стороне сервера этот язык использовался только для написания сценариев пакетной обра- обработки данных, состоящих из последовательности операторов и инструкций языка SQL. Иными словами, в то время еще нельзя было сохранять процедуры и функ- функции для их дальнейшего применения. SQL'Forms 3.0 позволяла создавать проце- процедуры и функции на стороне клиента, хотя поддержка функций еще не была доку- документирована, и поэтому многие разработчики ими не пользовались. Кроме того, в этой версии PL/SQL не поддерживались массивы и отсутствовало взаимодей- взаимодействие с операционной системой (для ввода и вывода). Так что до полноценного языка программирования ему было еще очень и очень далеко. Однако несмотря на все эти ограничения PL/SQL был принят разработчика- разработчиками с большим энтузиазмом. Уж очень остро к тому времени стала ощущаться по- потребность хотя бы в элементарных средствах программного управления запросами, таких как условный оператор IF в SO_L*Forms. Корпорация Oracle быстро опреде- определила слабые места SQjL*Forms — недостаточную переносимость и наличие про- проблем, связанных с полномочиями пользователей на выполнение программ. Повышение переносимости приложений Тем, кто знаком с маркетинговой и технической стратегией Oracle Corporation, может, показаться странным то, что вообще мог возникнуть вопрос о недостаточ- недостаточной переносимости приложений. Ведь с начала 1980-х годов именно переноси- переносимость была одной из сильных сторон Oracle. Когда появился PL/SQL, реляцион- реляционные СУБД (РСУБД) на основе языка С работали во многих операционных системах и на множестве аппаратных платформ. SQL*Plus и SQL*Forms легко адаптировались к разнообразным терминальным конфигурациям. Однако для ре- решения большого числа задач по-прежнему требовались средства таких языков, как COBOL, С и FORTRAN. И стоило разработчику выйти за рамки инструмен- инструментария Oracle, приложение тут же утрачивало переносимость. Язык PL/SQL призван был расширить диапазон приложений, реализуемых с помощью не зависимых от операционной системы программных средств. В на- настоящее время для создания таких легко переносимых приложений активно ис- используются специальные языки (например, Java). A PL/SQL по-прежнему зани- занимает особую нишу как один из пионеров в этой области и применяется разработ- разработчиками для создания легко переносимого кода приложений баз данных. Улучшенная защита приложений и защита целостности транзакций Язык PL/SQL должен был обеспечить строгий контроль за выполнением логиче- логических транзакций. Одним из средств такого контроля являются полномочия на выполнение. Вместо разрешения на обновление отдельных таблиц пользователи получают разрешение только на выполнение процедуры, с помощью которой осу- осуществляется доступ к необходимым структурам данных. Владельцем этой проце- процедуры является пользователь с учетной записью РСУБД Oracle, который, в свою
Так вот что такое РЦ/SQL... 35 очередь, может предоставлять разрешения на обновление таблиц, участвующих в выполняемых процедурой транзакциях. Таким образом, процедура становится «привратником» транзакции. Единственным способом выполнения транзакции программой (будь то приложение Oracle Forms или исполняемый файл Рго'С) является вызов процедуры, и это гарантирует целостность транзакции. Скромное начало, постоянное усовершенствование Корпорация Oracle последовательно вводила в PL/SQL все новые и новые усо- усовершенствования. Было разработано множество дополнительных пакетов, рас- расширяющих функциональность PL/SQL. В частности, этот язык был дополнен объектно-ориентированными возможностями, разнообразными структурами дан- данных (в том числе массивами), а также в значительной мере углублен и расширен. Язык SQL, каким бы мощным он ни был в той области использования, для ко- которой предназначен, не обладает программной гибкостью и мощью, необходимой для создания полноценных приложений. В то же время PL/SQL, оставаясь в пре- пределах не зависимого от операционной системы окружения Oracle, позволяет раз- разрабатывать вы'сокоэффективные приложения, полностью удовлетворяющие по- потребностям пользователей. Со времени своего появления PL/SQL прошел очень длинный путь. Разработ- Разработчики, которые пользовались его первой версией, слишком уж часто вынуждены были говорить: «Этого PL/SQL не позволяет». Если же теперь вы столкнетесь с задачей, которую, как вам кажется, нельзя решить с помощью PL/SQL, то не от- отчаивайтесь. Копайте глубже, исследуйте возможности самого языка и встроен- встроенных пакетов-дополнений, и необходимые средства, скорее всего, найдутся. Для знакомства с основами программирования на языке PL/SQL мы рассмот- рассмотрим несколько примеров программ. Так вот что такое PL/SQL... Если вы только приступаете к программированию и еще, конечно же, не освоили ни PL/SQL, ни даже SQL, может показаться, что перед вами очень сложная зада- задача. Но это не так. Она наверняка окажется гораздо более простой, чем вы думаете. Для нашего оптимизма есть два основания. О Выучить компьютерный язык значительно проще, чем второй или третий «че- «человеческий» язык. Почему? Да потому, что компьютеры не особенно умны (они «думают» - выполняют операции — быстро, но отнюдь не творчески). Чтобы указать компьютеру, что он должен делать, приходится пользоваться очень жестким синтаксисом. Следовательно, язык, на котором мы с ним обща- общаемся, тоже очень жесткий (не допускает никаких исключений), и потому вы- выучить его очень легко. О Язык PL/SQL прост и в сравнении с другими языками программирования. В основе его структуры лежат «блоки» с разными разделами, четко идентифи- идентифицируемыми с помощью ключевых слов.
36 Глава 1 • Введение в PLysQL Рассмотрим несколько примеров, в которых демонстрируются принципы при- применения ключевых элементов структуры и функционирования PL/SQL. Интеграция с SQL Одной из важнейших характеристик PL/SQL является его тесная интеграция с SQL Для выполнения SQL-инструкций в программах на PL/SQL не требуется промежуточного программного обеспечения, например ODBC (Open DataBase Connectivity - открытый интерфейс доступа к базам данных) или JDBC (Java DataBase Connectivity — средство организации доступа Java-приложений к базам данных). Вы можете просто вставить инструкцию UPDATE или SELECT в программный код, как в приведенном ниже примере: 1 DECLARE 2 l_book_count INTEGER; 3 4 BEGIN 5 SELECT COUNT(*) 6 INTO l_book_count 7 FROM books 8 WHERE author LIKE 'SFEUERSTEIN. STEVENS!'; 9 10 DBMS_OUTPUT.PUT_LINE ( 11 'Стивен является автором или соавтором ' || 12 l_book_count |[ 13 ' книг.'): 14 15 -- Я решил изменить написание своего имени... 16 UPDATE books 17 SET author = REPLACE (author. 'STEVEN'. 'STEPHEN') IB WHERE author LIKE 'SFEUERSTEIN. STEVEN*1: 19 END; Теперь давайте посмотрим, что делает этот код. Его подробное описание дано в следующей таблице. Строки Описание 1-3 Раздел объявления так называемого анонимного блока PL/SQL, в котором объявляется целочисленная переменная для хранения данных о количестве книг, автором или соавтором которых является Стивен Фернстайн. (Подробнее о структуре блока в PL/SQL рассказывается в главе 3) 4 Ключевое слово BEGIN указывает на начало исполняемого раздела — кода, который будет реализован при вводе данного блока в SQL*Plus 5-8 Делается запрос, определяющий общее количество книг, автором или соавтором которых является Стивен Фернстайн. Особенно интересна строка 6: используемое в ней предложение INTO на самом деле не является частью инструкции SQL, оно служит «мостом» между базой данных и локальными переменными PL/SQL 10-13 Для вывода количества блоков используется встроенная процедура DBMS_OUTPUT.PUT_LINE (то есть процедура из пакета DBMSJ3UTPUT, входящего в состав Oracle) 15 Однострочный комментарий, объясняющий назначение инструкции UPDATE 16-18 Чтобы изменить написание имени автора на Stephen, обновляется таблица книг. Для поиска всех вхождений слова STEVEN и замены их на STEPHEN используется встроенная функция REPLACE
Так вот что такое PL/SQL... 37 Последовательность и условия выполнения строк программы PL/SQL содержит специальный набор операторов, предназначенных для управ- управления последовательностью выполнения строк программы. В него входят такие операторы. О IF и CASE. Эти операторы реализуют логику условного выполнения. Например: «Если количество книг больше 1000, тогда...». О Полный набор операторов для реализации циклической обработки и итера- итераций. В их число входят операторы цикла FQR, WHILE и LOOP. О Оператор GOTO. Да-да, в PL/SQL есть даже оператор GOTO, позволяющий вы- выполнить переход из одного места программы в другое. Однако это не означает, что им следует пользоваться. Приведем процедуру (многократно используемый блок кода, который можно вызывать по имени), демонстрирующую работу отдельных операторов: 1 CREATE OR REPLACE PROCEDURE pay_out_balance ( 1 account_id_in IN accounts. idJITYPE) 3 IS 4 l_balance_remaining NUMBER: 5 BEGIN 6 LOOP 7 l_balancej"emaining :- account_balance (account_id_1n): В 9 IF l_balance_remaimng < 1000 10 THEN 11 EXIT; 12 ELSE 13 apply balance (account id_in. l_balance_remaining); 14 END IF:" 15 END LOOP; 16 END pay_out_balance; Проанализируем этот блок кода, воспользовавшись следующей таблицей. Строки Описание 1, 2 Заголовок процедуры, уменьшающей баланс банковского счета с целью оплаты счетов. В строке 2 задан список параметров процедуры, состоящий из одного входного значения (идентификационного номера банковского счета) 3, 4 Раздел объявлений процедуры. Обратите внимание, что вместо типичного ключевого слова DECLARE здесь для отделения заголовка процедуры от раздела объявлений используется ключевое слово IS (или AS} 6-15 Пример простого цикла LOOP. Окончание цикла определяется оператором EXIT LOOP (строка 15); в циклах FOR и WHILE условие окончания цикла задается по-другому 7 Вызов функции ассоипЦзаПапсе, определяющей баланс счета. Это пример вызова одной многократно используемой программы из другой. Вызов одной процедуры из другой продемонстрирован в строке 13 9-14 Оператор IF, который можно интерпретировать так: «Если баланс счета окажется меньше 1000 долларов, прекратить оплату счетов. В противном случае оплатить следующий счет»
38 Глава 1 • Введение в PL/SQL Когда что-то пошло не так Язык PL/SQL предоставляет разработчикам мощный механизм обработки оши- ошибок. В следующей процедуре мы получим имя и баланс счета на основе его иден- идентификатора. Затем проверим баланс. Если он низок, явно инициируется исклю- исключение, которое прекращает выполнение программы: 1 CREATE OR REPLACE PROCEDURE check_account ( 2 account_id_in IN accounts.idSTYPE) 3 IS 4 I_ba1ance_remaining NUMBER; 5 l_balance_below_minimum EXCEPTION; 6 l_account_name accounts. nameUYPE; 7 BEGIN 8 SELECT name 9 INTO 1_account_name 10 FROM accounts 11 WHERE id - accountjdjn; 12 13 l_balance_remaining :- account balance (accountjdjn); 14 15 DBMS_OUTPUT.put_line ( 16 'Баланс счета' ]| l_account_name ]| 17 ' - ' II l_balance_remaining): 18 19 IF l_balance_remaining < 1000 20 THEN 21 RAISE 1 balance below minimum; 22 END IF; 23 24 EXCEPTION 25 WHEN N0JATA FOUND 26 THEN 27 --Ошибочный идентификатор счета 2B log error t...); 29 30 WHEN l_balance below_m1n1mum 31 THEN 32 log error (...); 33 RAISE: 34 END: Рассмотрим подробнее ту часть кода, которая связана с обработкой ошибок. Строки Описание 5 Объявляем собственное исключение с именем l_balance_below_mlnlmum. В Oracle имеется набор заранее определенных исключений, таких как DUP_VAL_ON_INDEX, но для данного приложения нужно нечто более специализированное 8-11 Запрос извлекает имя счета. Если счета с указанным идентификатором не окажется, Oracle инициирует стандартное исключение NO_DATA_FOUND, что ведет к завершению программы 19-22 Если баланс слишком низок, явно инициируется собственное исключение, поскольку это свидетельствует о наличии серьезных проблем, связанных со счетом 24 Ключевое слово EXCEPTION отмечает конец исполняемого раздела и начало раздела исключений, в котором обрабатываются ошибки
О версиях PL/SQL 39 Строки Описании 25-28 Блок обработки ошибок для ситуации, когда счет не найден. Если инициировано исключение N0_DATA_F0l)ND, здесь оно перехватывается и ошибка записывается в журнал 30-33 Блок обработки ошибок для ситуации, когда баланс счета оказался слишком низким (специальное исключение для данного приложения). Если инициировано исключение l_balance_below_mlnlmum, оно перехватывается и ошибка записывается в журнал. Затем ввиду серьезности ошибки мы повторно инициируем то же исключение, в результате чего ошибка выйдет за пределы текущей ^ процедуры, в блок PL/SQL, из которого эта процедура вызвана Ознакомившись с этими примерами, вы получили общее представление о том, что собой представляет код PL/SQL, каковы его важнейшие синтаксические эле- элементы и каким образом создавать и читать такой программный код. О механиз- механизмах обработки ошибок PL/SQL подробно рассказывается в главе 6. О версиях PL/SQL Каждая версия базы данных Oracle выходит с собственной версией PL/SQL, уве- увеличивающей набор его функциональных возможностей. Поэтому одна из важ- важнейших задач программиста, использующего PL/SOL, - следить за появлением очередных версий. Необходимо постоянно осваивать новые возможности каждой из них, чтобы знать, как ими пользоваться в приложениях, и решать, насколько они могут быть полезными при модификации уже существующих приложений. Основные средства и возможности всех версий PL/SQL перечислены в табл. 1.1. Правда, приведен лишь их самый поверхностный обзор. Но в следующих разде- разделах о нововведениях последних версий PL/SO_L, входящих в OracleSi и Oracle9i, рассказывается более подробно. ПРИМЕЧАНИЕ- Каждый пакет Oracle Developer содержит собственную версию PL/SQL, которая обычно появляется через некоторое время после выхода самой СУБД. В этой главе (как и во всей книге) рассказывается прежде всего о программировании на стороне сервера. Таблица 1,1. СУБД Oracle и соответствующие версии PL/SQL Версия Версия Характеристики Oracle PL/SQL Oracle6 1.0 Исходная версия PL/SQL, использовавшаяся главным образом как язык сценариев в SQL*Plus (она еще не позволяла создавать многократно используемые вызываемые программы) и как язык программирования в SQL»Forms3 Oracle7 2.0 Обновление версии 1. Добавлена поддержка хранимых процедур, функций, пакетов, определяемых программистом записей, таблиц PL/SQL и множества расширений, включая DBMS_OUTPUT и DBMS_PIPE Orade7.1 2.1 Поддерживает определяемые программистом подтипы данных, позволяет использовать в SQL-инструкциях хранимые функции и поддерживает динамический SQL (посредством пакета DBMS_SQL). С появлением версии 2.1 стало возможным выполнять инструкции DDL из программ PL/SQL продолжение^
40 Глава 1 • Введение в PL/SQL Таблица 1.1 (продолжение) Версия Версия Oracle PL/SQL Характеристики Oracle7.3 2.3 Расширяет возможности таблиц PL/SQL, включает улучшенную поддержку удаленного управления взаимосвязями между таблицами и курсорных переменных. Добавлены средства файлового ввода-вывода (пакет UTL_FILE) OradeS 8.0 Новый номер версии отражает стремление Oracle синхронизировать номера версий PL/SQL с соответствующими номерами версий СУБД. PL/SQL 8 поддерживает многие усовершенствования Огас1е8, включая большие объекты (LOB), объектно-ориентированную структуру и определенные принципы разработки, коллекции (VARRAY и вложенные таблицы), а также средство Orade/AQ (Oracle/Advanced Queuing), реализующее надежную систему организации очередей OradeBI B.I Это первая из i-серий Oracle (базы данных для Интернета). В соответствующую версию PL/SQL включен впечатляющий набор новых средств и возможностей, в том числе новая версия динамического SQL (встроенный динамический SQL), поддержка Java для доступа к базе данных, процедуры с правами вызывающего, возможность разрешения на выполнение, высокопроизводительный «пакетный» язык DML, запросы и автономные транзакции Orade9i 9.0 Orade9l появился вскоре после OracleBi. В его первой версии Release 1 реализованы следующие возможности для разработчиков PL/SQL: поддержка наследования типов объектов, табличные функции и выражения с курсорами (предназначенные для распараллеливания процесса выполнения функций PL/SQL), многоуровневые коллекции (коллекции внутри коллекции), операторы и выражения CASE Orade9i 9.2 В самой последней версии Oracle, Oracle9i Release 2, главный акцент Release 2 сделан на языке XML (Extensible Markup Language). Кроме того, добавлены ассоциативные массивы (таблицы, которые можно индексировать не только целыми значениями, но и строками VARCHAR2), появилась возможность использовать основанный на записях язык DML (позволяющий, например, выполнять вставку с применением записи), полностью переписан пакет LJTL_FILE, дающий теперь возможность копировать, удалять и переименовывать файлы Новые возможности Oracle8i Для многих разработчиков Oracle8i остается все еще относительно новым про- продуктом, так что мы сочли полезным описать некоторые из его наиболее важных нововведений. О них коротко рассказывается в следующих разделах, а более под- подробно — в указанных в этих разделах главах. Автономные транзакции Одной из давних потребностей разработчиков PL/SQL была возможность вы- выполнять и затем сохранять или отменять инструкции DML (Data Manipulation Language - язык манипулирования данными) INSERT, UPDATE и DELETE безотноси- безотносительно к основной транзакции сеанса. Это можно делать с помощью автономных транзакций.
О версиях РЦ/SQL 41 Приведем несколько примеров, где применение автономных транзакций будет наиболее уместным. О Механизм регистрации ошибок. Это классический пример использования ав- автономных транзакций. Вам нужно записать информацию о зарегистрирован- зарегистрированной ошибке в таблицу базы данных, но вы не хотите, чтобы запись в журнал была частью логической транзакции. О Многократно используемые компоненты приложения. Вы разрабатываете интернет-приложение. При этом нужно объединить компоненты от нескольких производителей и несколько программных уровней, обеспечить их взаимодей- взаимодействие строго определенным образом. Если завершение транзакции одним ком- компонентом влияет на все остальные компоненты приложения, такое приложение не будет надежно функционировать в указанной среде. Эта проблема решается при помощи автономных транзакций. Определяя блок PL/SQL (анонимный блок, процедуру, функцию, пакетную процедуру, пакетную функцию или триггер базы данных) как автономную тран- транзакцию, вы изолируете инструкции DML в этом блоке от остальной части сеанса. Такой блок становится независимой транзакцией, начинаемой другой транзакцией, которая называется главной. В блоке автономной транзакции выполнение глав- главной транзакции приостанавливается. После выполнения операций SQL вы со- сохраняете результаты или производите их откат, а затем возобновляете главную транзакцию. Очень просто определить блок PL/SQL как автономную транзакцию. Для это- этого нужно только включить в раздел объявлений блока следующее выражение: PRAGMA AUTONOMOUSJRANSACTION; Механизм регистрации с использованием автономной транзакции позволяет сохранить изменения в журнале независимо от основной транзакции сеанса: PROCEDURE writejog t code IN INTEGER, text IN VARCHAR2) IS PRAGMA AUTONOMOUSJRANSACTION; BEGIN INSERT INTO log VALUES ( code, text, USER. SYSQATE ); COMMIT: END: Конечно, существуют определенные правила, которые при этом следует со- соблюдать. Они описаны в главе 13. Процедуры с правами вызывающего Когда-то, во времена Огас1е7 и Oracle 8.0, каждая вызываемая пользователем хра- хранимая программа выполнялась от имени владельца программы и с его правами. Эта модель разрешений называлась правами создателя (definer rights). Однако лишь 0,5 % приложений создавались с одной учетной записью Oracle. В осталь- остальных 99,5 % случаев процесс регулирования прав создателя становился кошмаром Для разработчиков, поскольку обычно код хранился в одной схеме и с помощью
42 Глава 1 • Введение в рц/sQL инструкций GRANT EXECUTE разрешения на его выполнение предоставлялись другим пользователям (непосредственно или через роли). Такое централизованное хранение кода не обеспечивает автоматического пе- переноса прав пользователя (называемого вызывающим) на объекты кода. Напри- Например, может оказаться так, что пользователь не имеет права (привилегии) приме- применить инструкцию DELETE к таблице, но вызванный им код имеет такое право, и удаление происходит! В одних случаях именно этого вы и хотите, но в других, особенно когда в программах используется динамический SQL, это может привести к крайне нежелательным последствиям. В Oracle 8.1 PL/SQL был расширен таким образом, чтобы во время компиля- компиляции вы могли решить, должна программа (или все программы пакета) выпол- выполняться с правами ее создателя или того, кто ее вызывает. Синтаксис, поддерживающий права вызывающего (invoker rights), очень прост. Продемонстрируем обычный механизм запуска процедуры rundl 1, основанный на новой динамической SQL-инструкции EXECUTE IMMEDIATE: CREATE OR REPLACE PROCEDURE runddl (ddljn in VARCHAR2) AUTHID CURRENTJJSER IS BEGIN EXECUTE IMMEDIATE ddljn; END: / Предложение AUTHID CURRENTJJSER перед ключевым словом IS указывает, что процедура runddl должна выполняться с правами вызывающего или «текущего» пользователя, а не с правами создателя. Подробнее о моделях с правами вызы- вызывающего и правами создателя рассказывается в главе 20. Динамический SQL Со времени появления Огас1е7.1 разработчики PL/SQL могут использовать для реализации динамического SQL и PL/SQL встроенный пакет DBMS_SQL. Это зна- значит, что запрос типа DELETE TABLE или CREATE TABLE либо даже блок PL/SQL можно реализовать в виде строки непосредственно во время работы программы и тут же его выполнить. Динамический SQL исключительно удобен для разработки систем, позволяющих пользователю осуществлять произвольные запросы, при выполне- выполнении инструкций языка определения данных (Data Definition Language, DDL) из кода PL/SQL, а также в тех случаях, когда заранее не известно, какой запрос дол- должен быть произведен программой. Особенно часто динамический SQL использу- используется в web-приложениях. Однако с динамическим SQL, выполняемым средствами из пакета DBMSSQL, свя- связан ряд проблем. К ним относятся, в частности, сложность пакета, способность работать только с типами данных СУБД Oracle 7, его небольшая скорость функ- функционирования. Поэтому сотрудники компании PL/SQL Central разработали еще одну реали- реализацию динамического SQL прямо в языке PL/SQL, которая называется встроен- встроенным динамическим SQJL (Native Dynamic SQL, NDS).
О версиях РЦ/SQL 43 Для поддержки динамических многострочных запросов язык NDS добавляет в PL/SQL инструкцию EXECUTE IMMEDIATE и расширяет инструкцию OPEN FOR (для курсорных переменных). Следующая функция с помощью NDS возвращает на- набор строк из заданной таблицы: CREATE OR REPLACE FUNCTION tabCount ( tab IN VARCHAR2. whr IN VARCHAR2 :- NULL) RETURN INTEGER IS retval INTEGER; BEGIN EXECUTE IMMEDIATE 'SELECT Cttltm*) FROM' || tab || 'WHERE' || NVL (whr. '1-1') INTO retval: RETURN retval; END: Если вы уже знакомы с пакетом DBMS_SQL, то, взглянув на данный код, без со- сомнения, вздохнете с облегчением. Более подробный рассказ об этой замечатель- замечательной возможности вы найдете в главе 15. Пакетные операции Среди важнейших задач команды разработчиков Oracle PL/SQL — повышение производительности языка. Годами жалуясь на недостаточную скорость выпол- выполнения кода, мы наконец получили долгожданный результат. (Хотя, возможно, за- заметное повышение производительности Oracle 8.0 связано отчасти с тем, что без существенной оптимизации языка нельзя было бы включить в него поддержку объектных типов.) Одно из усовершенствований заключается в появлении возможности выпол- выполнять пакетный DML из PL/SQL. Рассмотрим в качестве примера следующий код, который удаляет записи о служащих, идентифицируемых с помощью номеров за- заданного вложенного списка: CREATE TYPE empnos_list_t IS VARRAY(IOO) OF NUMBER: / CREATE OR REPLACE PROCEDURE del_etnps (listjin IN empnos_list_t) IS BEGIN FOR listnum IN list_in.FIRST..list_in.LAST LOOP DELETE FROM emp WHERE empno - list_in (listnum); END LOOP: END; Просто пишется, легко читается, но как насчет производительности? Ведь для каждой инструкции DELETE выполняется переключение контекста с PL/SQL на SQL. Если в списке 100 элементов, а следовательно, придется выполнить порядка 100 переключений, то это существенно отразится на производительности. Ввиду того что подобные операции выполняются часто и с ними связаны зна- значительные издержки, Oracle предложила нам пакетную разновидность цикла
44 Глава 1 • Введение в PL/SQL FOR - инструкцию FORALL. С ее использованием приведенную выше процедуру del_emps можно переписать так: CREATE OR REPLACE PROCEDURE del_emps (listjn IN empnos_l1st_t) IS BEGIN FORALL listnum IN listjn.FIRST, .listjn.LAST DELETE FROM emp WHERE etnpno - listjn Clistnim): END: Теперь количество переключений контекста уменьшится. Все операции DELETE будут объединены в одну пакетную операцию и переданы уровню SQL. В дополнение к пакетной DML-инструкции FORALL в Oracle 8.1 введено пред- предложение BULK COLLECT, являющееся разновидностью предложения INTO явного за- запроса. Эта операция позволяет извлечь несколько строк результатов запроса по- посредством одного переключения контекста. Инструкция FORALL подробно рассматривается в главе 13, а предложение BULK COLLECT - в главе 14. Новые возможности триггеров В Oracle8i для администрирования базы данных и публикации информации о про- происходящих в ней событиях значительно шире стали применяться триггеры. На- Написав триггеры для новых системных событий и применяя их с приложением Oracle Advanced Queuing (AQ), вы сможете воспользоваться преимуществами но- новой технологии — публикации-подписки. Функция публикации событий базы данных позволяет подписываться на со- события базы данных, равно как эти события могут подписываться на сообщения от других приложений. Синтаксис триггеров расширен для поддержки систем- системных событий и других происходящих в базе данных или схеме событий, связан- связанных с данными. Этот синтаксис теперь допускает вызов процедуры, являющейся телом триггера. Программные триггеры можно определить для: О инструкций DML (DELETE, INSERT и UPDATE); О инструкций DDL (CREATE, DROP и ALTER); О событий базы данных (SERVERERROR, LOGON, LOGOFF, STARTUP и SHUTDOWN). В Oracle8i область применения триггеров заметно расширена. О Триггеры для столбцов вложенных таблиц. Операция CAST.. .MULTISET позво- позволяет выполнять определяемые триггером действия только в ответ на измене- изменение атрибута во вложенной таблице. О Триггеры событий уровня базы данных. Можно определить триггеры для ре- реакции на такие системные события, как LOGON, DATABASE SHUTDOWN и даже SER- SERVERERROR. О Триггеры событий уровня схемы. Можно определить триггеры для обработ- обработки таких событий уровня пользователя или схемы, как CREATE, DROP и ALTER.
О версиях PL/SQL 45 В главе 18 все эти новые функции триггеров рассматриваются наряду с тради- традиционными возможностями триггеров DML, позволяющими им реагировать на события, которые совершаются при выполнении инструкций INSERT, DELETE и UPDATE. Вызов Java из PL/SQL Java — очень мощный язык и во многих отношениях более надежный, чем PL/SQJL Он включает сотни классов, обеспечивающих понятный и удобный программный интерфейс приложения (Application Programming Interface, API). B Oracle8i и бо- более поздних версиях появилась возможность вызывать из приложений PL/SQL хранимые процедуры Java, которые на самом деле являются хранящимися в базе данных Java-методами. Для этого создается «оболочка», или уровень PL/SQL, инкапсулирующий вызов Java-метода и делающий его доступным любой среде, из которой могут вызываться программы PL/SQL. С помощью такой «оболочки» файлы можно удалять, например, непосредст- непосредственно из кода программы PL/SQL: CREATE OR REPLACE PACKAGE xfile IS FUNCTION delete (file IN VARCHAR2) RETURN INTEGER AS LANGUAGE JAVA NAME 'Jfilel.delete (Java.iang.String) return int'; END; Использование Java в базах данных Oracle - это очень обширная тема, вклю- включающая, безусловно, программирование на языке Java, о котором в настоящей книге речь не идет. Однако в главе 22 приводится необходимый объем информа- информации для выполнения Java-кода из программ PL/SQL. Новые возможности Oracle9i Ряд замечательных нововведений в PL/SQL появился также с выходом версий ОгасЬЭг Release 1 и Release 2. Мы поговорим о них в следующих разделах. DML с ориентацией на записи Начиная с ОгаскЭг Release 2 в инструкциях INSERT и UPDATE можно использовать записи. Например: CREATE OR REPLACE PROCEDURE set_book_info С bookjn IN booksSROmPE) IS BEGIN INSERT INTO books VALUES (boofcjn); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN UPDATE books SET ROW = bookjn WHERE isbn » bookjn.isbn; END:
46Глава 1 ¦ Введение в Очевидно, что работать так гораздо удобнее, чем задавать отдельные перемен- переменные или поля в записи. Преимущества очевидны. О Код получается более лаконичным. Теперь мы можем работать на уровне за- записей, Больше не нужно объявлять отдельные переменные или, разделять запись на поля для передачи данных инструкции DML. О Код получается более надежным. Если вы работаете с записями *ROWTIPE, а не оперируете отдельными полями этих записей, то снижается вероятность воз- возникновения ошибки в момент внесения изменений в таблицы и представления, на которых основаны эти записи. Дополнительные сведения о языке DML, ориентированном на записи, вы най- найдете в главе 13. Табличные функции Табличная функция, вызываемая из предложения FROM запроса, возвращает ре- результирующий набор строк данных в форме коллекции PL/SQL. Такие функции были доступны и в Oracle8i, правда, с большими ограничениями, но в Oracle9i их использование значительно расширено. Перечислим дополнительные операции, которые теперь выполняются с их помощью. О Возврат строк результирующего набора данных «конвейерным*' способом. Данные возвращаются во время выполнения функции. О Параллельное выполнение запроса. Функция может обрабатываться в несколь- нескольких «подчиненных» процессах, использующих разные данные. Приведем пример заголовка конвейерной табличной функции, которая может работать в параллельном режиме и определена таким образом, что все строки для заданного отделения направляются одним «подчиненным» процессом и переда- передаются последовательно: CREATE OR REPLACE FUNCTION my_transform_fn ( p_1nput_rows in emplo,yee_info.recur_t) RETURN empl oyeej" nf о. transfomied_t PIPELINED CLUSTER P_INPUT_ROWS BY (department) PARALLELJNABLE (PARTITION P_INPUT_ROWS BY HASH (department)) Дополнительную информацию о табличных функциях, конвейеризации и па- параллельном выполнении кода вы найдете в главе 16. Новые и усовершенствованные типы данных В последней версии Oracle кардинально улучшена поддержка отметок времени, а также расширены возможности управления временем с учетом часовых поясов и вычисления временных интервалов (времени между двумя датами или отмет- отметками времени). В следующем блоке отражены новые возможности этих типов данных и показано, как используются соответствующие функции: DECLARE b05S_free TIMESTAMP(O) WITH TIME ZONE: stevenjeaves TIMESTAMP(O) WITH TIME ZONE: window_for_conversation INTERVAL DAYC) TO SEC0NDC3): BEGIN
О версиях PL/SQL 47 boss free :- TO TIMESTAMP TZ ( '29-JAN-20Q2" 12:00:00 US/Pac1f1c PST'. 'DD-MON-YYYY HH24:MI:SS TZR TZD1); StevenJ eaves :- TO TIMESTAMPJZ ( '29-JAN-2002 16:45:00 US/Central CST1, 'DD-MON-YYYY HH24:MI:SS TZR TZD'): w1ndow_for_conversat1on ;- stevenjeaves - boss_free; DBMS_OUTPUT.PUT_LINE ( TO_CHAR (w1ndow_for_conversation, 'HH:MI:SSXFF')); -- Неявное преобразование из TimeZone в Date... DBMS_OUTPUT.PUT_LINE (ADDJONTHS (bossjree, -5)): END; А теперь поговорим еще об одном аспекте Oracle. Средства XML уже интегри- интегрированы прямо в базы данных, и для их поддержки имеется новый тип данных — XMLIype (реализованный как тип объекта). СУБД Oracle явно ориентирована на усиленную поддержку этого языка, и встроенный тип данных XML должен уста- установить связь между SQL и XML. В Огас1е9г появилась возможность выполнять SQL-запросы к XML-документам, хранящимся в столбцах таблицы. Кроме того, для поиска содержимого этих документов можно использовать синтаксис XPath. Приведем пример вставки XML-документа в таблицу (обратите внимание на использование метода CREATEXML объектного типа XMLType, предназначенного для преобразования XML-документа в строку таблицы): INSERT INTO env analysis values ('acme silverplating1. to date ('15-02-2001'. 'dd-mm-yyyy'). xmltypeTcreatexml ( '<?xml version-"!.0> <report> <site>1105 5th Street</site> <substance>PCP</substance> <level>1054</level=> </report>')): Следующая инструкция CREATE с помощью синтаксиса XPath создает индекс из первых 26 символов имени клиента в заказе: CREATE UNIQUE INDEX i_purchase_order_reference ON purchaseorder p ( SUBSTRt XMLTYPE.GETSTRINGVAL( XMLTYPE.EXTRACT( p.PODOCUMENT, '/PurchaseOrder/Customer/textCП).1,26) ); Подробнее о типе данных XMLType рассказывается в главе 12. Наследование объектных типов Объектный тип (эквивалент «класса» в традиционной терминологии объектно- ориентированного подхода) впервые был введен в Огас1е8, но ему недоставало стандартных объектных функций, и прежде всего наследования.
48 Глава 1 • Введение в PL/SQL В Oracle9i появилась возможность определить иерархию объектных типов, в которой атрибуты и методы родительского типа наследуются подтипами-по- подтипами-потомками. Следующие три определения объектных типов являются примером такой иерархии (пища, food, здесь является родительским типом; десерт, dessert - ти- типом пищи, а пирожное, cake — типом десерта): CREATE TYPE food_t AS OBJECT ( name VARCHAR2{100). food_group VARCHAR2 A00), grownjn VARCHAR2 A00). MEMBER FUNCTION price RETURN NUMBER ) NOT FINAL; / CREATE TYPE dessert_t UNDER food_t ( contains_chocolate CHAR(l). year_created NUMBERD), OVERRIDING MEMBER FUNCTION price RETURN NUMBER ) NOT FINAL: / CREATE TYPE cakej UNDER dessertj ( diameter NUMBER, inscription VARCHAR2B00) -- Для типа cake_t цена вычисляется не явно. Обратите внимание, что тип dessertt подменяет функцию price корневого типа, а тип cake_t наследует функцию price типа dessertt. С введением поддержки наследования появились более основательные при- причины для использования в приложениях объектных типов. Об объектных типах и наследовании рассказывается в главе 21. Усовершенствования коллекций PL/SQL Коллекция PL/SQL представляет собой массивоподобную структуру, позволяю- позволяющую работать со списками информации. Эти списки могут состоять из простых дан- данных, таких как строки, или более сложных структур, таких как записи. В Огас1е9« возможности коллекций несколько расширены. Во-первых, введена поддержка многоуровневых коллекций, то есть коллекций внутри коллекций. Во-вторых, поя- появилась возможность индексировать содержимое одного из типов коллекции - а именно ассоциативного массива, называемого также индексной таблицей, — не только целыми числами, но и строками. Приведем пример использования строк в качестве значений индекса или иден- идентификаторов строк в коллекции: DECLARE TYPE population^ IS TABLE OF NUMBER INDEX BY VARCHAR2C64); country_population population_t: continent_population population_t: howmany NUMBER: row_id VARCHAR2C64): BEGIN country_populat1on('Col,ymphia1) :- 100000;
Ресурсы для разработчиков PL/SQL 49 country_population('Ribalizia') := 750000; howmany :- countryjopulationCCoiymphia1): rowjd :- continent_population.FIRST; DBMS_OUTPUT.PUT_LINE (conti nent_populati on(row_i d)): END; / Многоуровневые коллекции позволяют эмулировать многомерные массивы, а также другие сложные типы данных. Подробно о них будет рассказано в главе 11. Встроенная компиляция кода PL/SQL До появления Oracle9i исходный код PL/SQL всегда преобразовывался в специ- специальное представление, обычно называемое байт-кодом (bytecode), которое хра- хранилось в базе данных и интерпретировалось во время выполнения виртуальной машиной, входящей в состав Oracle. В Oracle9i введен новый подход. Теперь ис- исходный код PL/SQL можно откомпилировать во встроенный объектный код, ин- интегрированный в Oracle. Такая компиляция позволяет заметно повысить произ- производительность приложений, что особенно важно для программ, в которых осуще- осуществляется большое количество вычислений, но не так отражается на выполнении SQL-запросов. О встроенной компиляции кода подробно рассказывается в главе 19. Работа с несколькими версиями PL/SQL Следует всегда знать, с какой версией Oracle вы работаете, — лишь при этом ус- условии можно оптимальным образом использовать поддерживаемые ею возмож- возможности. Иногда для работы в разных версиях Oracle приходится писать специ- специальные программы на PL/SQL. В этом случае можно пойти двумя путями. О Избегать использования каких-либо функций PL/SQL, появившихся в более поздних версиях Oracle, чем те, для которых пишется программа. Этот подход называется «наименьшим общим знаменателем». О Поддерживать несколько версий программы — по одной для каждой версии Oracle (если вы хотите пользоваться теми новыми функциями, которые поя- появились в более поздних версиях). Такой код трудно реализовать, но когда это все же необходимо, попробуйте воспользоваться сценарием файла oneversion.sql, имеющегося на web-узле издательства O'Reilly. Этот сценарий позволяет оп- определить версию Oracle и с помощью переменных подстановки SQL*Plus, включать и отключать соответствующие части программного кода. Ресурсы для разработчиков PL/SQL Первое издание данной книги вышло в 1995 году. Тогда это событие стало на- настоящей сенсацией - впервые была написана книга о PL/SQL, издание которой не связано с компанией Oracle. Ее давно ожидали разработчики во всем мире. С тех пор появилось множество PL/SQL-ресурсов, среди которых различного рода
50 Глава 1 • Введение в PL/SQL публикации, среды разработки, утилиты и web-узлы. Однако настоящая книга по-прежнему остается самым важным и значительным из них, О важнейших из перечисленных ресурсов коротко рассказывается в следую- следующих разделах, Пользуясь этими ресурсами, многие из которых относительно не- недороги, а часть вообще предоставляется бесплатно, вы сможете значительно по- повысить свою квалификацию. Книги о PL/SQL от O'Reilly В настоящее время серия книг о PL/SQL издательства O'Reilly & Associates пред- представлена уже довольно внушительным списком. За более подробной информаци- информацией об этих изданиях вы можете обратиться на web-узел издательства, располо- расположенный по адресу: http://oracle.orellly.com. О Oracle PL/SQJL Programming (авторы Steven Feuerstein, Bill Pribyl). Книга, ко- которую вы сейчас читаете. Это настольная книга для многих профессиональ- профессиональных программистов PL/SQL, в которой рассматривается каждый элемент ба- базового языка PL/SQL. Во втором издании описывались версии Oracle вплоть до Огас1е8, а в настоящем, третьем, издании — вплоть до версии Oracle9t. О Learning Oracle PL/SQJL (авторы Bill Pribyl, Steven Feuerstein). Упрощенное введениез язык PL/SQL для новичков в программировании и для тех, кто пе- переходит к PL/SQL от других языков программирования. О Oracle PL/SQL Best Practices (автор Steven Feuerstein). Небольшая книжка, включающая в себя более 100 лучших советов, рекомендаций, приемов, кото- которые помогут читателю повысить свои навыки в разработке программ яа PL/SQL. Эта книга является чем-то вроде краткого конспекта do PL/SQL. В ней рас- рассматриваются возможности языка до версии Огас1е8г включительно. О Oracle PL/SQL Developer's Workbook (авторы Steven Feuerstein, Andrew Ode- wahn). Содержит серию вопросов и ответов, помогающих разработчику прове- проверить и дополнить свои знания о языке. В книге рассматриваются возможности языка вплоть до версии Oracle8i. О Oracle PL/SQL Language Pocket Reference (авторы Steven Feuerstein, Bill Pribyl, Chip Dawes) и Oracle PL/SQJL Built-ins Pocket Reference (авторы Steven Feuer- Feuerstein, John Beresnievicz, Chip Dawes). Два маленьких карманных справочника по Огас1е8г и Oracle8 соответственно. О Oracle PL/SQJL Built-in Packages (авторы Steven Feuerstein, Charles Dye, John Beresnievicz). Справочное руководство по всем стандартным пакетам, входя- входящим в комплект поставки Oracle. Эти пакеты позволяют упростить трудное задание, а иногда даже выполнить невозможное. Рассматриваются версии до Огас1е8 включительно. О Oracle PL/SQL Programming: Guide to Orade8i Features (автор Steven Feuerstein). Дополнение к книге Oracle PL/SQL Programming, содержащее обзор новых эле- элементов PL/SQL, появившихся в Oracle8z.
Ресурсы для разработчиков PL/SQL 51 О Advanced Oracle PL/SQJ. Programming with Packages (автор Steven Feuerstein). В книге рассказывается о том, как можно усовершенствовать свои программы путем разработки собственных пакетов PL/SQL, Рассматривается Огас1е7. О Oracle Web Applications: PL/SQL Developer's Introduction (автор Andrew Ode- wahn). Книга, призванная помочь читателю научиться создавать web-приложения баз данных. Содержит некоторый исторический материал о PL/SQL и про- программировании для Web. Рассматриваются версии до OracleSi включительно. О Oracle PL/SQJ* CD Bookshelf. Компакт-диск, который содержит электронные версии большинства перечисленных выше книг, а также копию книги Guide fa OracleBi Features. Рассматриваются версии до Огас1е8г включительно. Другие печатные ресурсы Помимо книг издательства O'Reilly, перечисленных в предыдущем разделе, су- существуют и другие печатные ресурсы, о которых вам следует знать. О Документация Oracle. Co времени появления первых версий Oracle с фраг- фрагментарной и откровенно слабой документацией многое изменилось. HTML- документы, входящие в состав СУБД, являются прекрасным источником ин- информации об огромном количестве технологий, предоставляемых сейчас Oracle. О Книги Oracle Press и других издательств. Oracle Press вьшускает собственную серию книг о PL/SQL, написанных Скоттом Урманом. Другие издательства технической литературы предлагают как вводные описания PL/SQL, так и бо- более полные книги по Oracle. О Oracle Professional от Pinnacle Publishing. Этот печатный ежемесячный инфор- информационный бюллетень (и его online-версия) содержит материалы по Oracle. В ка- каждом номере имеется статья о PL/SQL (ее автором обычно является Стивен Фернстайн), в которой рассматриваются какие-то новые возможности или при- приложения PL/SQL. Обращайтесь по адресу: http://www.oracleprofesslonalnewslet- ter.com/. О Oracle Magazine от Oracle Corporation. Ежемесячное издание, охватывающее широкий спектр технологий Oracle. Oracle Magazine (http://www.oramag.cbm/) часто освещает новые функции PL/SQL и помогает понять принципы взаимо- взаимодействия различных компонентов Oracle. PL/SQL в Интернете Для программистов, работающих с PL/SQL, существует несколько превосход- превосходных web-узлов. О Oracle Technology Network. Посетите web-узел Oracle Technology Network (OTN), иногда называемый TechNet, где «имеются сервисы и ресурсы, необходимые разработчикам для создания, тестирования и распространения приложений» на основе технологии Oracle. Он знаком миллионам разработчиков: здесь мож- можно загрузить программное обеспечение, документацию и код множества приме- примеров. Итак, обращайтесь по адресу: http://technet.us.oracle.com/ или прямо к стра- странице PL/SQL: http://otn.oracle.com/tecb/pLsql/contentMwl
52 Глава 1 • Введение в PL/SQL О PL/SQJ. Pipeline. Мы также рекомендуем присоединиться к PL/SQL Pipeline, «свободному сообществу интернет-портала, поддерживаемого компанией Quest Software. Этот узел предназначен для информирования, обучения и поддержки профессионалов в области информационных технологий со всего мира. Его ре- регулярно просматривают множество известных авторов и экспертов». PL/SQL Pipeline содержит архивы с учебным материалом и полезным кодом. На фору- форуме ведутся дискуссии, предусмотрены также бесплатные консультации для разработчиков и администраторов баз данных. Его адрес: http://www.quest-pipe- lines.com. О PL/Net.org. Узел содержит «собрания» программного обеспечения с открытым кодом, которое либо написано на PL/SQL, либо используется разработчика- разработчиками, отдающими предпочтение этому языку. Здесь вы можете найти его более подробное описание, просмотреть ответы на часто задаваемые вопросы (FAQJ. Еще на данном узле имеются ссылки на множество полезных утилит, таких как utPLSQL — среда для тестирования модулей. Его адрес: http://plnet.org О Open Directory Project. На узле проекта «dmoz» (Directory Mozilla) по адресу http://dmoz.org/Computers/Programming/Languages/PL-SQL/ вы найдете ссылки на узлы, посвященные PL/SQL. Здесь, в частности, имеется набор ссылок на ком- коммерческие и некоммерческие средства разработки (категория Developer Tools). Узел поддерживается командой редакторов-волонтеров. О Oracle FAQ. Этот узел содержит «независимую и полезную информацию о про- продуктах Oracle. Вы найдете здесь FAQ, сценарии, советы, новости, решения для некоторых задач, каталоги, форумы и чаты, а также многое другое, что сделает вашу работу с Oracle более успешной». Для этого вам нужно обратиться по ад- адресу http://www.orafaq.com/faq.htm. Средства разработки и утилиты Создавая программное обеспечение в программе Блокнот (в англоязычной версии Windows — Notepad) и затем запуская и тестируя код исключительно в SQjL'Plus, вы теряете много времени. Вы теряете его еще больше, если отлаживаете про- программы путем вставки в них бесконечного количества вызовов продедуры DBMS_ OUTPUT. PUTLINE, вместо того чтобы использовать удобные и эффективные средст- средства отладки исходного кода. На сегодня в распоряжении разработчиков PL/SQL имеется огромный выбор разнообразнейших вспомогательных средств, облегчающих работу, — от дорого- дорогостоящих коммерческих интегрированных сред разработки (Integrated Develop- Development Environment, IDE) до недорогих замен для SQL'Plus и небольших утилит для выполнения отдельных задач. В этой книге вы не найдете обзор всех указанных средств, равно как и рекомен- рекомендации по их выбору. Приведем лишь краткий список продуктов и URL, с которых вы можете начать самостоятельное исследование имеющихся возможностей. О TOAD (Tool for Oracle Application Developers). Самая популярная среда разра- разработки для PL/SQL. Она полюбилась разработчикам благодаря наличию брау- браузера со вкладками. TOAD доступна как в коммерческой, так и в бесплатной версиях. Обращайтесь по адресу: http://www.quest.com/toad.
Несколько советов 53 О SQL Navigator. Производитель этой среды разработки тот же, что и предыду- предыдущей (Quest). Пользователи ценят ее в первую очередь за фундаментальную поддержку Java для PL/SQL. Вы получите эту IDE, обратившись по адресу: http://www.quest.com/sqLnavigator. О PL/SQL Developer. PL/SQL Developer - прекрасное и относительно недорогое ПО с дружественным интерфейсом, которое включает множество функций. Позволяет сторонним разработчикам создавать и подключать компоненты, рас- расширяющие его возможности. Адрес web-узла, где представлен этот продукт: http://www.allroundautomatlons.nl/plsqldev.html. О TOra (Toolkit for Oracle). Набор инструментальных средств, который включает в себя браузер, редактор PL/SQL и отладчик для Linux или Windows. TOra доступен в двух версиях — коммерческой и версии с открытым кодом. Обра- Обращайтесь по адресу: http://www.globecom.se/tora Несколько советов С 1995 года, когда вышло в свет первое издание настоящей книги, у нас появи- появилась возможность обучать десятки тысяч разработчиков PL/SQL, помогать им и даже сотрудничать с ними. За это время мы многому научились и у наших чита- читателей, составили более полное представление об оптимальной форме подачи материала. Теперь мы хотим поделиться с вами рядом соображений, благодаря которым вы сможете эффективнее работать с таким мощным языком программи- программирования. Не стоит торопиться! Мы почти всегда работаем в очень жестких временных рамках, ведь, как правило, за короткое время нужно написать огромное количество кода. Получая очередное задание, мы должны немедленно приступать к работе — не так ли? Нет, не так. Если сразу же углубиться в написание программного кода, меха- механически превращая постановку задачи в сотни, тысячи и десятки тысяч строк, по- получится просто «каша», которую не удастся ни отладить, ни снабдить соответст- соответствующим сопровождением. Но в жесткий график вполне можно уложиться, если тщательно все спланировать. Мы настоятельно рекомендуем вам не поддаваться панике, расслабиться и, прежде чем приступать к написанию нового приложения или программы, провес- провести подготовительную работу. 1. Создайте сценарии тестирования. До того как будет написана первая строчка кода, вы должны определить, каким образом будет проверяться и насколько успешна реализация поставленной задачи. При таком подходе следует заранее тщательно продумать интерфейс программы и ее функциональные элементы. 2. Установите четкие правила написания SQL-инструкций в приложении. В об- общем случае мы рекомендуем разработчикам не включать в программы много
54 Глава 1 • Введение в PL/SQL SQL-инструкций. Большинство инструкций, в том числе однострочные запро- запросы на обновление и вставку, должны быть «скрыты» в заранее написанных и тщательно отлаженных процедурах и функциях (это называется инкапсуля- инкапсуляцией данных). Такие программы оптимизируются, тестируются и сопровожда- сопровождаются более эффективно, чем разбросанные по программному коду SQL-инст- SQL-инструкции (многие из которых по многу раз повторяются в приложении). 3. Установите четкие правила обработки исключений в приложении. Имеет смысл создать единый пакет обработки ошибок, который содержал бы все де- детали ведения журнала ошибок, определяющего, как исключения инициируют- инициируются и распространяются во вложенные блоки, и позволяющего избежать ввода специфических для приложения исключений прямо в программный код. Этим пакетом должны пользоваться все разработчики - в таком случае им не при- придется писать сложный, и забирающий много времени код обработки ошибок. 4. Выполняйте «пошаговую проработку». Другими словами, придерживайтесь принципа нисходящего проектирования, что позволит уменьшить сложность кода, с которым вы имеете дело в каждый конкретный момент. Применив дан- данный подход, вы обнаружите, что исполняемые разделы ваших модулей стано- становятся короче и проще для понимания. В будущем такой код будет легче сопро- сопровождать и модифицировать. Важнейшую роль в реализации этого принципа играет использование локальных, или вложенных, модулей, Это лишь несколько важных моментов, на которые следует обратить внима- внимание, приступая к написанию программного кода. Помните, что при разработке программного обеспечения спешка приведет к массе ошибок и огромным поте- потерям времени. Не бойтесь обращаться за помощью Если вы профессиональный разработчик программного обеспечения, то наверня- наверняка очень умны, много учились, повышали свою квалификацию и теперь отлично зарабатываете, создавая программный код. Вам. под силу решить практически лю- любую задачу, и вы очень этим гордитесь. Но, к сожалению, ваши успехи делают вас слишком самоуверенным и вы терпеть не можете обращаться за помощью, даже в случае возникновения серьезных затруднений. Но такое отношение к делу опас- опасно и деструктивно. Программное обеспечение пишется людьми. И поэтому важно понимать, что огромную роль в его разработке играет человеческая психология. Приведем лишь один пример. Джо, ведущий разработчик в команде из шести программистов, столк- столкнулся с затруднением при решении своей задачи. С нарастающим раздражением он несколько часов анализировал проблему, но не мог найти причину ошибки. При этом ему и в голову не приходило попросить о помощи кого-нибудь из кол- коллег, потому что все были менее опытны. Наконец, после долгих усилий, он сдался и обратился по телефону к сотруднице: «Сандра, не могли бы Вы зайти и взгля- взглянуть на мою программу? У меня проблема, которую я не могу решить.» Сандра подходит к его компьютеру и с первого взгляда замечает то, что он и сам должен был бы давно увидеть.
Несколько советов 55 Ура! Ошибка в программе исправлена, и можно продолжать работу. Джо вы- выражает признательность, но в глубине души он очень смущен. В голове у Джо крутятся мысли: «Почему же я не увидел этого раньше?» и «Если бы я потратил еще минут пять, я бы и сам обнаружил ошибку». Это понятно, но довольно глупо. Очень часто мы не можем найти источник проблемы из-за того, что слишком по- погружены в свой код. Иногда нужен просто свежий взгляд со стороны, и это не имеет ничего общего ни с опытом, ни с особыми знаниями. Именно поэтому мы настоятельно рекомендуем придерживаться следующих простых правил. О Не бойтесь признаться себе в том, что чего-то не знаете. Если скрывать, что вам не все известно о приложении или его программном коде, это не приведет ни к чему хорошему. Выработайте культуру взаимоотношений в коллективе, при которой вполне естественными считаются вопросы друг к другу и обоюд- обоюдные консультации. О Обращайтесь за помощью. Если вы не можете найти причину ошибки в тече- течение получаса, немедленно просите о помощи. Можно даже организовать сис- систему взаимопомощи, предусматривающую назначение каждому работнику на- напарника, к которому всегда можно обратиться. И не позволяйте себе или еще кому-либо в группе часами биться головой об стену, пытаясь самостоятельно выйти из затруднительного положения. О Организуйте процесс рецензирования кода. Не допускайте использования кода без его предварительного прочтения и конструктивной критики одним или несколькими разработчиками. Поощряйте творческий, и даже радикальный, подход к разработке кода Мы склонны превращать в рутину практически все составляющие нашей жизни, выбираем для чего-либо один привычный способ действия. Мы пишем код опре- определенным образом, делаем определенные предположения о продукте, отвергаем возможные решения, не проанализировав их как следует, поскольку заранее зна- знаем, что это не может быть выполнено. Разработчики очень необъективны в отно- отношении собственных программ и порой умаляют их достоинства. От них часто можно услышать такие высказывания: «Этот код не будет работать быстрее», «Эта программа не может функционировать так, как хочет пользователь; придет- придется подождать следующей версии», «Если бы я использовал продукт X, Y или Z, все делалось бы легко и быстро. А с такими средствами приходится буквально сражаться за каждую мелочь». Но на самом деле практически любая программа может работать немного бы- быстрее, делая именно то, что хочет пользователь. И хотя у каждого продукта, ко- конечно же, имеются свои ограничения, сильные и слабые стороны, не нужно ждать выхода его следующей версии. Лучше встретить проблему лицом к лицу и, не по- позволяя себе никаких оправданий, найти ее решение.
56 Глава 1 • Введение в PL/SQL Как же этого добиться? Следует отказаться от некоторых из своих представ- представлений и посмотреть на мир свежим взглядом. Пересмотрите выработанные года- годами профессиональные привычки. Отнеситесь к задаче творчески — постарайтесь отступить от традиционных методов, от зачастую ограниченных и механистиче- механистических подходов. Не бойтесь экспериментировать, даже если сочтете свои идеи радикальным отступлением от нормы. Вы будете удивлены тем, как многому можно научиться таким образом, вырастете как программист, способный к решению нестандарт- нестандартных задач. Многое становится возможным, когда вы отбрасываете мысль о не- неприемлемости какого-либо решения, а, наоборот, пытаетесь найти совершенно новый подход.
2 Написание и запуск кода PL/SQL р- SQL*Plus ^ Выполнение базовых операций PL/SQL > Средства разработки для Oracle PL/SQL ^ Вызов PL/SQL из других языков ^ Что же дальше Прежде чем вы приступите к изучению принципов программирования на PL/SQL, вам потребуются некоторые сведения о том, как компилировать и запускать свои программы. В этой главе приводится обзор основных средств, включая SQL*Plus, с помощью которых можно вызывать программы, написанные на PL/SQL. Дан- Данный материал поможет вам начать практическую работу с этим языком. Если вы уже овладели в некоторой степени навыками программирования баз данных, то, вероятно, знаете, что SQL (Structured Query Language — структури- структурированный язык запросов к базам данных) имеет широкий спектр применения, ис- используется разными способами и в разных ситуациях. Сказанное относиться и к PL/SQL, команды которого можно не только вызывать из множества других языков, но и выполнять в двух различных средах: О на сервере баз данных Oracle как хранимый код (так можно вызывать про- программы на PL/SQL из SQL*Plus, Java или другой среды); О в одной из сред разработки приложений Oracle, такой как Oracle Forms или Oracle Reports, в качестве программы, которая выполняется на клиентском компьютере. Давайте начнем с первой возможности — запуска на сервере Oracle. Тут име- имеются дополнительные варианты выбора клиентской среды, из которой загружа- загружается ваш код. Среди наиболее популярных средств программирования можно на- назвать следующие: О стандартный интерфейс командной строки SQL*Plus, связанный с сервером Oracle, на котором и выполняются операторы PL/SQL;
58 Глава 2 • Написание и запуск кода PU/SQL О другой язык программирования, в том числе С, C++, Java, Visual Basic, COBOL, ADA и FORTRAN, для которого Oracle предоставляет библиотеку времени выполнения и/или прекомпилятор, позволяющий включать в программу код SQL и PL/SQL. Поскольку средств выполнения кода PL/SQL очень много, вы, скоре всего, будете пользоваться одним или двумя из них. Поэтому прежде всего в этой главе рассматривается приложение SQL'Plus, хотя приводятся и примеры использова- использования PL/SQL из разных сред. SQL* Plus Дедушка клиентских средств Oracle - приложение SQL'Plus - представляет со- собой интерпретатор для SQL и PL/SQL с интерфейсом командной строки. Это оз- означает, что оно принимает от пользователя инструкции для доступа к базе данных и направляет их серверу Oracle, а результаты отображает на экране монитора. Примитивность пользовательского интерфейса SQL'Plus часто служит при- причиной того, что о нем отзываются пренебрежительно, а между тем это одно из са- самых удобных средств выполнения кода для Oracle. Здесь отсутствуют всякие за- забавные штучки и сложные меню. Когда автор этой книги только начинал рабо- работать с Oracle (примерно в 1986 году), предшественник SQL*Plus гордо назывался UFI — User Friendly Interface (дружественный пользовательский интерфейс). И хотя сейчас, два десятилетия спустя, даже версия SQL*Plus для Oracle9i едва ли завоюет награду за дружественный интерфейс, она, по крайней мере, не слиш- слишком часто дает сбои. Приложение SQL'Plus можно запустить разными способами. О Как консольную программу1. Программа выполняется из оболочки или ко- командной строки (окружения, которое иногда называют консолью): О В качестве программы с псевдографическим пользовательским интерфей- интерфейсом. Эта разновидность SQL'Plus доступна только в Microsoft Windows. Ее называют псевдографическим интерфейсом, поскольку она очень похожа на консольную программу, отличаясь от таковой, кроме прочего, наличием рас- растровых шрифтов. Oracle фактически сворачивает поддержку данного продук- продукта, и даже его «расширенная» поддержка завершается в сентябре 2005 года. О Через iSQL*Plus (в OracleJJi или более поздней версии). Программа выпол- выполняется из web-браузера машины, на которой работает HTTP-сервер Oracle и сервер iSQL'Plus. О Через SQL*PIus Worksheet. Графический пользовательский интерфейс Java с консольной версией SQL'Plus. Хотя он поддерживает небольшой журнал инструкций, ничего примечательного в этой версии среды разработки нет. Oracle называем это версией SQL'Plus с интерфейсом командной строки, но мы полагаем, что дан- данное определение не однозначно, поскольку гагтерфейс командной строки предоставляют два из че- четырех способов реализации SQL'Plus.
SQL*Plus 59 Главное окно консольной версии SQL'Plus показано на рис. 2.1. Обычно кон- консольную программу предпочитают остальным по следующим причинам: О она быстрее перерисовывает экран, что важно при выполнении сложных за- запросов; О у нее более полный журнал команд, используемых ранее в командной строке (по крайней мере на платформе Microsoft Windows); О в ней проще менять такие визуальные характеристики, как шрифт, цвет и раз- размер буфера прокрутки; О при работе с ней используется командная строка. Присоединен к: Oracle9i Release 9.6.1.1.1 - Production JSeruer Release 9.Q.I.1.1 - Production SQL> set Hnasize 94 pagesize 999 5QL> eolunn author Furant a3P uord_UY*ap SQL> coluwi title fornat аЭВ uord_uraii SQL> select isbn, author, tltlB from books 2 «hare louer(author) like 'xfauarxtainx' and lauer(authar) like ISBN AUTHOR TITLE Ш-596-ВШ381-1 Feuerttoin, Steuen, with Bill Oracle PL/SQL Programming Pribyl т-596-ОШ1вО-а Bill Pribyl with Steuen FBuerstein 1-S6592-4S7-B Feuerstein, Steuen. Bill Pribyl, Chip Daues Learning Oracle PL/SQL Oracle PVSQL Language Packet Reference jSQL> _ Рис. 2.1. Окно приложения SQL*Plus в консольном сеансе Отметим, что iSQ_L*Plus автоматически форматирует выходные данные за- запроса в виде HTML-таблиц. Это особенно удобно, когда нужно вывести множест- множество столбцов таблицы базы данных (ниже будет приведен пример). Запуск SQL*Plus Для того чтобы запустить консольную версию SQL'Plus, нужно просто ввести команду sql pi us в ответ на приглашение операционной системы 0S>. Это делается как в операционных системах Unix, так и в операционных системах компании Microsoft. Далее следуют текст, а также строки для ввода имени пользователя и пароля: Sqi*Plus: Release 9.2.0.1.0 - Production on Thu Jun 20 10:42:22 2002 Copyright (c) 1982. 2002. Oracle Corporation. All rights reserved. Enter user-name: bob Enter password: svrardish Connected to: 0racle9i Enterprise Edition Release 9.2.0.1.0 - Production With the Partitioning. OLAP and Oracle Data mining options JServer1 Release 9.2.0.1.0 - Production SQL>
60 Глава 2 » Написание и запуск кода PL/SQL О том, что ваше приложение настроено правильно, вы узнаете, увидев пригла- приглашение SQL>. (Пароль, в данном случае swordish, на экране отображаться не будет.) Можно также загрузить SQL*Plus, сразу задав имя пользователя и пароль в командной строке: 0S> sqlplus bob/swordish Однако так поступать не рекомендуется, поскольку пользователи некоторых операционных систем имеют возможность видеть аргументы вашей командной строки, что позволит им воспользоваться вашей учетной записью. С помощью ключа /NOLOG можно запустить SQL*Plus без подключения к базе данных, указав имя пользователя и пароль посредством команды CONNECT: 0S> sqlplus /nolog SQL*Plus: Release 9.2.0.1.0 - Production on Thu Jun 20 10:42:22 2002 Copyright (c) 1982. 2002, Oracle Corporation. All rights reserved. SQL> CONNECT bob/swordish SQL> Connected. Если компьютер, на котором вы запускаете SQ_L*Plus, также содержит пра- правильно сконфигурированное приложение Oracle Net1 и вы авторизованы админи- администратором для подключения к удаленным базам данных (то есть серверам баз данных, работающим на других компьютерах), то сможете подключаться к ним из SQL*Plus. Для этого наряду с именем пользователя и паролем нужно знать идентификатор подключения Oracle Net, называемый также именем сервиса. Идентификатор подключения может выглядеть так: test01.ariel.datacraft.com \ Идентификатор вводится после имени пользователя и пароля, отделяясь от них символом «©»: SQL» CONNECT bob/swordishetestOl.ar1e1.datacraft.com SQL> Connected. При запуске псевдографической версии SQL*Plus идентификационные дан- данные водятся в диалоговом окне Вход в Систему, где идентификатор подключения указывается в поле Строка Связи (рис. 2.2). Если вы хотите подключиться к серве- серверу базы данных, работающему на локальной машине, оставьте поле Строка Связи пустым. Для того чтобы запустить iSQL*Plus необходимо ввести в браузере соответст- соответствующий URL (http://MMfl_xocTa/isqlplus) и задать информацию о подключении, как показано на рис. 2.3. Запустив SO_L*Plus, можно делать следующее: О выполнять SQL-инструкции; О компилировать и сохранять программы на языке PL/SQL в базе данных; Oracle Net — это имя продукта, ранее называвшегося Net8 или SQJL'Net
SQL*Plus 61 О запускать программы на языке PL/SQL; О выполнять команды SQL*Plus; О запускать сценарии, которые могут содержать сразу несколько перечислен- перечисленных выше команд. Рассмотрим по очереди каждую из перечисленных возможностей. Входа Систему Имя Польз,: Пароль: | Строка Связи: 1 °К 1 {bob 1 jteslOl. 1 ariRl.da1acraft.com Отмена Рис. 2.2. Экран во время подключения к серверу базы данных из псевдографической версии SQL*Plus /SQL*Plus Имя пользователя: |ьоь Пароль: [— Идентификатор соединения: (teslOl.anel.dolaortcoj ПОЛНОМОЧИЯ: [Пользователь!^ ?шайка Рис. 2.3. Страница браузера с идентификационной информацией в среде разработки iSQL*Plus
62 Глава 2 • Написание и запуск кода PL/SQL Выполнение SQL-инструкции В консольной версии SQ,L*Plus запрос SELECT * FROM books: генерирует результат, подобный тому, который показан на рис. 2.1. (Чтобы полу- получить результаты в таком виде, нужно воспользоваться командами форматирования столбцов. Если бы это была книга об SQL*Plus или о том, как выводить данные из баз данных, мы бы рассказали здесь обо всех способах вывода информации в данной среде и об установке различных параметров представления и формати- форматирования выводимых данных.) Если вам нужно представить данные в удобочитаемом виде, лучше обратиться к iSQL*Plus. Достаточно задать в этой программе инструкцию в поле Введите опе- операторы и щелкнуть на кнопке Выполнить, как выходные данные появятся в браузе- браузере в виде таблицы (рис. 2.4). wm ¦ ;^;Ж:*:- -ж* ¦¦ ORACLS' iSQL*Plus '•& S Местоположение г. select * from books; ISSmncefcr PUSQL dratajnn. Muoing w |nQ Ввы аы recommendation* Feuersteln, 8Ш wtin Bll Prtbyl I1-SB5B3- W1+ Beginner* guide to Oracle1! PL/BQL Proflrommhg ;jEIH Pfibyt w№ Steven Lerguagi " j(Feuefsleln [dal)DaeetuDl '[0fecle^Lra"QLLjVguVge~P7ehel [Reference (tru ck Jonattie r П01.ОЗ.в1 а^-1ч1»ег1св7^йв"(аГ6"вс|7|^п^ав«1орв'г». jfFeuersiBin. Steven, ' -.„.""i -.:| fneludas 0гк1ввкз*вг»де ...IJBIB Pntyf. Chip Oawes.[giJ)"* | 84|! :|D1 D8.00 Рис. 2.4. Запрос и его результаты в ISQL*Plus SQL-инстрзтсции в консольном или псевдографическом интерфейсе следует за- завершать точкой с запятой, но в zSQ,L*Plus этот символ не является обязательным.
SQL*Plus 63 Запуск программы на языке PL/SQL Итак, начинаем. Введем в SQL'Plus небольшую программу на PL/SQL: SQL> BEGIN 2 DBMS_OUTPUT.PUT_LINE('У меня получилось 1'); 3 END: 4 / После ее выполнения вы увидите на экране следующее: PL/SQL procedure successfully completed. SQL> Успешное завершение (successfully completed) этой программы должно было бы означать вывод на экран заданного текста. Однако SQL*Plus довольно стран- странно ведет себя, по умолчанию он подавляет такой вывод. Для того чтобы увидеть выводимую программой строку, вам необходимо выполнить специальную коман- команду SQL'Plus - SERVEROUTPUT: SQL> SET SERVEROUTPUT ON SQL> BEGIN 2 DBMS OUTPUT.PUT LINE('y неня получилось Г); 3 END;" 4 / И только теперь после выполнения программы на экране отобразится следующее: У меня получилось! PL/SQL procedure successfully completed. SQL> Обычно команда SERVEROUTPUT помещается в файл запуска (о файле login.sql речь пойдет далее в этой главе), в таком случае она остается активной до тех пор, пока не произойдет одно из следующих событий: О разрыв соединения, выход из системы или завершение сеанса связи с базой данных иным способом; О вы явно зададите команду SERVEROUTPUT с атрибутом OFF; О Oracle отменит сеанс связи либо по вашему запросу, либо из-за ошибки ком- компиляции (см. главу 20). При вводе в консольном или псевдографическом приложении SQL*Plus инст- инструкции SQL или оператора PL/SQL программа назначает каждой строке, начиная со второй, порядковый номер. С какой целью в ней нумеруются строки? Тому есть две причины: во-первых, чтобы помочь вам определить, какую строку редак- редактировать с помощью встроенного строкового редактора, а во-вторых, чтобы в слу- случае обнаружения ошибки в вашем коде в сообщении об ошибке был указан номер строки. У вас не раз еще будет возможность это увидеть.
64 Глава 2 • Написание и запуск кода PL/SQL Ввод операторов PL/SQL в SQL'Plus завершается символом косой черты (строка 4 в приведенной выше программе). У этого символа есть несколько важ- важных назначений и характеристик. О Косая черта означает, что введенную инструкцию следует выполнить незави- независимо от того, была это инструкция SQL или оператор PL/SQL. О Косая черта — это команда SQL* Plus; она не является элементом языка SQL или PL/SQL. О Она должна стоять в отдельной строке, в которой нет никаких других команд. О В большинстве версий SQL*Plus до Oracle9i косая черта, случайно предварен- предваренная одним или несколькими пробелами, не работала! Начиная с Oracle9i среда SQL*Plus правильно интерпретирует ведущие пробелы, то есть попросту их игнорирует. Завершающие пробелы игнорируются во всех версиях. Для удобства SQL'Plus предлагает пользователям PL/SQL применять коман- команду EXECUTE, позволяющую не вводить команды BEGIN, END и завершающую косую черту. Таким образом, следующая строка эквивалентна приведенной выше про- программе: SQL> EXECUTE DBMS_OUTPUT.PUTJ.INEС'У неня получилось!1) Завершающая точка с запятой не обязательна, се лучше опустить. Как и боль- большинство других команд SQL*Plus, команду EXECUTE можно сократить, причем она не зависит от регистра символов. Поэтму указанную строку проще всего ввести так: SQL* exec dbms_output.putjine{'y меня получилось!') Запуск сценария Предположим, что вы знаете имя файла сценария на языке SQL или PL/SQB. То- Тогда для запуска этого сценария следует воспользоваться командой SQL*Plus В1. SQL> @errpkg.pkg Если же вы предпочитаете команды, являющиеся ключевыми словами, може- можете использовать эквивалентную ей команду START: SQL> START errpkg.pkg Результат будет точно таким же. В любом случае эта команда предписывает SQL*Plus выполнить следующие операции. 1. Открыть файл с именем errpkg.pkg. 2. Попытаться по очереди выполнить все команды SQL, PL/SQL и SQL*Plus, содержащиеся в указанном файле. 3. Закрыть файл и вернуть вас в SQL*Plus (если только в файле нет команды EXIT, выполнение которой приводит к выходу из SQL*Plus). Например: SQL> Perrpkg.pkg Package created. Package body created. SQL> 1 Команды START, ?и№ доступны в не-браузерной версии SQL'Plus. В iSQL'Plus для получения ана- аналогичных результатов используются команды Browse и Load script.
SQL*Plus 65 Как видите, результаты выполнения сценария выводятся на экран. В данном примере указано расширение файла pkg. По умолчанию SQL*Plus предполагает использование расширения sql. Поэтому если задать имя файла без соответствующего расширения, SQL'Plus будет искать файл errpkg.sql и в резуль- результате выдаст сообщение об ошибке: SQL> 0errpkg SP2-0310: unable to open file "errplcg.sql" SP2-Q310 — это номер ошибки, сгенерированной Oracle, a SP2 означает, что ошибка относится к SQL'Plus. (Дополнительные сведения об ошибках SQL*Plus вы най- найдете в документации Oracle SQJL*Plus User's Guide and Reference.) Нетрудно догадаться, что если файл сценария находится в другом каталоге, следует указать путь к файлу1: SQL> 0/files/src/release/l.O/errpkg.pkg С запуском сценария из другого каталога связан интересный вопрос: что если файл errpkg.pkg расположен и указанном каталоге и, в свою очередь, вызывает другие сценарии? Например, он может содержать такие строки (любая строка, начинающаяся с команды REM, является комментарием, и SQL* Plus ее игнорирует): REM имя файла: errpkg.pkg 8errpkg.pks @errpkg.pkb Предполагается, что в ходе выполнения файла-сценария errpkg.pkg будут вы- вызваны файлы errpkg.pks и errpkg.pkb. Однако если информация о пути отсутствует, где же SQL*Plus будет их искать? Ответ таков: только в текущем каталоге. И ве- вероятнее всего, нужных файлов там не окажется. Для решения данной проблемы имеется разновидность команды @ — это ко- команда @@. Она задает поиск файлов в каталоге текущего выполняемого файла. Та- Таким образом, вызовы в файле-сценарии errpkg.pkg лучше записывать так: REH пня файла: errpkg.pkg S@errpkg.pks Другие задачи SQL*Plus SQL'Plus поддерживает десятки команд, но мы остановимся лишь на-некоторых из них, самых важных или не всегда правильно интерпретируемых пользователя- пользователями. Для более обстоятельного изучения этого продукта следует обратиться к книге Oracle SQJ.*Plus: The Definitive Guide (автор/олайш Genmk, издательство O'Reilly), а чтобы получить краткую справочную информацию — к книге Oracle SQJ**Plus Pocket Reference того же автора. Пользовательские установки Как и многие другие программы с интерфейсом командной строки, SQL*Plus по- позволяет изменить пользователю некоторые параметры с помощью встроенных Вы будете приятно удивлены тем фактом, что начиная с СУБД Oracle8i косую черту (/) можно ис- использовать в качестве разделителя каталогов, как в Unix и операционных системах Microsoft. Это облегчает перенос сценариев между операционными системами.
66 Глава 2 • Написание и запуск кода PL/SQL переменных и установок. Один такой пример вы уже видели, когда мы примени- применили выражение SET SERVEROUTPUT. Посредством команды SQL'Plus SET можно уста- установить и множество других настроек. Так, выражение SET SUFFIX позволяет изме- изменить используемое по умолчанию расширение файла, a SET LINESIZE - задает максимальное количество символов в строке (символы, не помещающиеся в дан- данной строке, переносятся в следующую). Для того чтобы просмотреть все SET-yc- тановки текущего сеанса, выполните команду SQL» SHOW ALL "' Если вы используете графическую версию SQL'Plus, эти установки можно просмотреть и изменить с помощью команды Параметры > Конфигурация. Приложение SQL*Plus позволяет создавать собственные переменные в памя- памяти и задавать специальные переменные, посредством которых можно управлять его настройками. Переменные SQL'Plus бывают двух видов: DEFINE и переменные привязки. Чтобы присвоить значение DEFINE-переменной, нужно выполнить ко- команду DEFINE: SQL> DEFINE x - "опет 42" Увидеть значение х можно с помощью такой команды: SQL> DEFINE x DEFINE x - " ответ 42" (CHAR) Для ссылки на данную переменную предназначен символ &. Перед передачей инструкции Oracle SQL'Plus выполняет простую операцию подстановки, так что ссылку на переменную требуется заключить в кавычки: SELECT '&x' FROM DUAL; Переменную привязки нужно сначала объявить посредством команды VARIAB- VARIABLE, после чего можно будет использовать ее в PL/SQL и вывести ее значение на экран с помощью команды PRIN: ) SQL> VARIABLE X VARCHAR2U0) SQL> BEGIN 2 :x :- 'hullo1: 3 END; 4 / PL/SQL procedure successfully conpleted. SQL> PRINT :x X hullo У нас теперь две разные переменные х: одна определена с помощью команды DEFINE, а вторая - с помощью команды VARIABLE: SQL» SELECT :x, 'ftc' FROM DUAL; прежний 1: SELECT :x, 'Эх' FROM DUAL новый 1: SELECT :x. 'ответ.42' FROM DUAL :X '0TBET42' hullo Ответ 42
SQL*Plus 67 Запомните, что DEFINE-переменные всегда представляют собой символьные строки, а VARIABLE-переменные используются в SQL и PL/SQL как настоящие пе- переменные привязки. ТЕКУЩИЙ КАТАЛОГ В SQL*PLUS Каждый раз, когда вы загружаете SQL"Plus из командной строки, эта про- программа воспринимает текущий каталог операционной системы как собст- собственный текущий каталог. Загрузим ее следующим образом: C:\BILL4FILES> sqlplus Тогда любые операции с файлами в SQL*Plus (например, открытие и за- запуск сценария) по умолчанию будут считаться относящимися к каталогу C:\BILL\FILES. Сказанное верно и в отношении запуска графической версии SQL'Plus из командной строки операционной системы: C:\BIIX\FILES> sqlplusw Если вы используете для загрузки SQL*Plus ярлык или команду меню, текущим становится тот каталог, который операционная система связы- связывает с механизмом загрузки. Ну а как же изменить текущий каталог из SQL'Plus? Все зависит от ее версии. В консольной программе этого вообще нельзя сделать. Придется выйти, сменить каталог в операционной систе- системе и снова запустить SQL*Plus. В графической версии можно воспользо- воспользоваться командой Файл > Открыть или Файл > Сохранить, побочным эффек- эффектом применения которых является изменение текущего каталога. Если вы используете :SQL*Plus, понятие текущего каталога относится только к диалоговым окнам сохранения и извлечения файла, а возможность его реализации зависит от браузера. Сохранение выходной информации в файле Очень часто выходные данные сеанса SQL'Plus необходимо сохранить в файле. Предположим, вы сгенерировали отчет и хотите записать свои действия или ди- динамически сгенерировали команды, которые собираетесь выполнить позднее. В лю- любом случае для реализации этих задач в SQL*Plus используется команда SPOOL: SQL> SPOOL raport01.txt SQL> (nin_raport ... выходные ванные выводятся на экран и записываются в файл report01.txt... SQL> SPOOL OFF Первая команда, SPOOL, позволяет сохранять в техстовом файле report01.txt все строки данных, что следуют за ней. Последняя команда, SPOOL с атрибутом OFF, указывает SQL*Plus на необходимость остановить сохранение выходной инфор- информации и закрыть файл. Команда SPOOL также работает и в графической версии SQL*Plus.
68 Глава 2 • Написание и запуск кода PL/SQL Выход из SQL* Plus Для того чтобы выйти из SQL*Plus и вернуться в операционную систему, нужно выполнить команду EXIT: SQL> EXIT Если в момент выхода из приложения производилась запись выходных дан- данных в файл, SQL*Plus прекратит ее и закроет этот файл. А что произойдет, если выйти из SQL'Plus, не сохранив изменения в базе дан- данных? Обычно такие изменения производятся с помощью инструкций SQL или операторов PL/SQL (манипулирующих данными в базе данных), за которыми не следует явная инструкция COMMIT или ROLLBACK. Результат такой транзакции зави- зависит от текущей установки команды AUTOCOMMIT. По умолчанию команда AUTOCOMMIT используется с атрибутом ON и все несохраненные изменения записываются в базу данных. Если же применить эту команду с атрибутом OFF, Oracle выполнит откат всех несохраненных изменений: SQL> SET А1ДОС0ММ1Т OFF Для того чтобы разорвать соединение с базой данных, но остаться подключен- подключенным к SQL*Plus, нужно выполнить команду DISCONNECT, результат которой выгля- выглядит примерно так: SQL> DISCONNECT Disconnected from Oracle91 Enterprise Edition Release 9.2.0.1.0 - Production With the Partitioning. OLAP and Oracle Data Mining options JServer Release 9.2.0.1.0 - Production SQL> А зачем нужна команда DISCONNECT? Вы пишете сценарий, в ходе работы он из- изменяет параметры подключения к базе данных, поэтому перед очередным под- подключением лучше разорвать предыдущее. В противном случае, если использует- используется аутентификация, сценарий может автоматически подключиться к неверной учетной записи. Редактирование инструкции SQL*Plus сохраняет последнюю инструкцию в буфере, и для редактирования со- содержимого этого буфера можно использовать встроенный строковый редактор (что довольно неудобно) либо любой внешний редактор, выбранный вами. Для начала познакомимся с настройкой и работой внешнего текстового редактора. В ответ на команду EDIT SQL*Plus сохраняет буфер в файле, временно приос- приостанавливает свою работу и вызывает системный текстовый редактор: SQL> EDIT После окончания редактирования все изменения следует сохранить в буфере с помощью команды Сохранить системного текстового редактора. Если же требует- требуется сохранить измененный текст в файле, то по умолчанию таким файлом будет afiedt.buf, но вместо этого имени можно задать другое (с помощью выражения SET EDITFILE или команды Сохранить как системного редактора). Когда вы сохраните файл и выйдете из редактора, сеанс SQL*Plus поместит содержимое этого файла в буфер и продолжит свою работу. Если же вы хотите отредактировать сущест- существующий файл, просто укажите его имя в качестве аргумента команды EDIT: SQl> EDIT errpkg.plcg
SQL*Plus 69 Для Unix и Linux Oracle по умолчанию использует системный текстовый ре- редактор ed, а для операционных систем Microsoft Windows - Notepad. Хотя эти установки жестко запрограммированы в исполняемом файле sqlplus, их легко изменить, присвоив переменной _EDITOR другое значение. Например: SQL> DEFINE EDITOR - /bin/vi Здесь /bin/vi — полный путь к редактору (настоятельно рекомендуем его ис- использовать). Если же вы хотите работать со строковым редактором (а это в самом деле мо- может быть удобно), вам нужно знать следующие команды: О L - показать все строки буфера SQL и сделать текущей последнюю строку; О Ln- показать я-ю строку буфера SQL и сделать ее текущей; О DEL — удалить текущую строку, а следующую за ней сделать текущей; О C/old/new — заменить первое вхождение old в текущей строке на new (раздели- (разделителем может быть произвольный символ, в данном случае это косая черта); OR— запустить текущую инструкцию SQL; О I — перейти в режим ввода строк после текущей строки (режим заканчивается вводом пустой строки). Кстати говоря, команда EDIT прекрасно работает и в графической версии при- приложения SQL'Plus, а кроме того, в нем можно использовать операции вырезки и вставки. В iSQL*Plus команда EDIT не работает, но указанные операции выпол- выполняются, а также функционируют кнопки Сохранить сценарий и Загрузить сценарий. Автоматическая загрузка пользовательского окружения при запуске Для настройки среды разработки SQL*Plus можно изменять один или оба сцена- сценария ее запуска. SQL'Plus при запуске осуществляет две основные операции: О находит в корневом каталоге Oracle файл sqlplus/admin/glogin.sql и выполняет содержащиеся в нем команды (этот «глобальный» сценарий реализуется неза- независимо от того, кто запустил SQL*Plus из корневого каталогаTDracle и какой каталог был при этом текущим); О находит и выполняет файл login.sql из текущего каталога. Начальный сценарий может содержать те же типы команд, что и любой дру- другой сценарий на SQL'Plus: команды SET, SQL-инструкции, команды форматиро- форматирования столбцов и т. п. Оба файла выполняются в указанном порядке. В случае конфликта параметров или переменных преимущество получают установки по- последнего файла, login.sql. Приведем несколько интересных установок в файле lo- login.sql: REM Количество строк выходных ванных инструкции SELECT перед повторным выводом заголовков SET PAGESIZE 999 REM Ширина выводимой страницы в символах SET LINESIZE 132 REM Включение вывода сообщений DBMS OUTPUT SET SERVEROUTPUT ON SIZE 1000000 FORMAT WRAPPED
70 Глава 2 • Написание и запуск кода PL/SQL REM Закена текстового редактора, который вызывает SQL*Plus DEFINE EDITOR - vi REM Форматирование стойцов. извлекаемых из словаря данных COLUMN segmentjiame FORMAT A30 WORDWRAP COLUMN objectjiame FORMAT A30 WORD_WRAP В iSQL*Plus понятие текущего каталога отсутствует, поэтому файл login.sql не применяется. Здесь используется только файл glogin.sql, расположенный на том сервере, на котором выполняется данная программа. Обработка ошибок в SQL*Plus Способ, каким SQL*Plus информирует вас об успешном завершении операции, зависит от класса выполняемой команды. Для большинства команд SQL*Plus — это просто отсутствие какого-либо сообщения об ошибке. А успешное выполне- выполнение инструкций SQL и операторов PL/SQL обычно сопровождается какой-либо текстовой информацией. Если ошибка содержится в инструкции SQL или операторе PL/SQL, SQL*Plus по умолчанию сообщает о ней и продолжает работу. Это удобно, когда вы рабо- работаете в интерактивном режиме. Но если выполняется сценарий, желательно, а ча- часто даже необходимо, чтобы при возникновении ошибки работа SQL*Plus пре- прекращалась. Для этого применяется команда SQL» WHENEVER SQLERROR EXIT SQLSQLCOOE SQL*Plus прекратит работу, если после выполнения команды сервер базы дан- данных в ответ на какую-нибудь инструкцию SQL или оператор PL/SQL вернет со- сообщение об ошибке. Часть приведенной выше команды, 5QL.SQLC0DE, означает, что, оканчивая работу, SQL'Plus установит свой код завершения в ненулевое значение, которое можно определить в вызывающей среде1. В противном случае SQL*Plus всегда завершается с кодом 0, что иногда неверно истолковывается как успешное выполнение сценария. Более распространенной является другая форма указанной команды: SQL> WCNEVER SQLERROR EXIT ROLLBACK Это означает, что перед выходом SQL'Plus производится откат всех несохра- ненных изменений данных. Достоинства и недостатки SQL*Plus Кроме тех, что указаны выше, у SQL*Plus имеется несколько дополнительных функций, которыми вы наверняка с удовольствием будете пользоваться. О С помощью SQL*Plus можно запускать пакетные программы, задавая в ко- командной строке аргументы и ссылаясь на них в сценарии следующим образам: &1 (первый аргумент), &2 (второй аргумент) и т. д. О SQL'Plus обеспечивает полную поддержку всех инструкций SQL и операто- операторов PL/SQL. Это обычно имеет значение при использовании специфических Это можно сделать с помощью системной переменной $? в Unix и XERRORLEVEL* в Microsoft Windows.
Выполнение базовых операций PL7SQL 71 возможностей Oracle. В инструментальных средствах разработки сторонних производителей отдельные элементы указанных языков могут поддерживать- поддерживаться не на все 100 %. Например, некоторые из них до сих пор «не понимают» объектных типов Oracle, введенных несколько лет назад. (Об использовании объектных типов в PL/SQL рассказывается в главе 21.) О Для SQL*Plus подходит то же аппаратное обеспечение и те же программные платформы, что и для сервера Oracle. Как и любые другие инструментальные средства, SQL'Plus имеет свои недос- недостатки. О В консольных версиях SQL'Plus буфер команд содержит только последнюю из ранее использовавшихся команд. Журнала команд эта программа не ведет. О Когда в процессе сеанса связи с базой данных или выполнения сценария вы с помощью команды CONNECT переключаетесь на другого пользователя, SQL*Plus не повторяет стартовый сценарий login,sql или glogin.sql, а выполняет команду SERVEROUTPUT с атрибутом OFF. Это не совсем удобно для тех разработчиков, ко- которые задают в файле login.sql пользовательское приглашение командной стро- строки. Например, приглашение TEST9i> остается на экране и после того, как с по- помощью команды CONNECT пользователь переключился на базу данных PR0D9i. Поэтому не забывайте вручную запускать стартовый сценарий после каждого выполнения команды CONNECT. О SQL'Plus не имеет таких возможностей, характерных для современных ин- интерпретаторов команд, как автоматическое завершение ключевых слов и под- подсказки о доступных объектах базы данных, появляющиеся при вводе команд. О Интерактивная справочная система содержит информацию базовой докумен- документации по набору команд SQL'Plus. (Для получения справки по конкретной команде используется команда HELP.) О После запуска SQL*Plus изменить текущий каталог невозможно, что доволь- довольно неудобно, если вы часто открываете и сохраняете сценарии и при этом не хотите постоянно указывать полный путь к файлу. Поэтому, обнаружив, что вы не в том каталоге, выйдите из SQL*Plus, смените каталог и снова запустите программу. Как видите, с точки зрения программистов, SQL*Plus не является средством, отличающимся удобством в работе и изысканностью интерфейса. Но данное при- приложение используется повсеместно, работает надежно и наверняка будет поддер- поддерживаться до тех пор, пока существует Oracle Corporation. Выполнение базовых операций PL/SQL Теперь поговорим о создании, выполнении, удалении и других операциях с про- программами PL/SQL, выполняемых с помощью SQL'Plus. В этом разделе не будет обилия деталей - он, скорее, представляет собой вводный обзор темы, которую мы подробно рассмотрим в следующих главах.
72 Глава 2 • Написание и запуск кода РЦ/SQL Создание хранимой программы Для того чтобы написать собственную программу на PL/SQL, нужно воспользо- воспользоваться одной из SQL-инструкций CREATE. Например, если вы хотите создать хра- хранимую функцию с именем wordcount, которая подсчитывала бы количество слов в строке, выполните такую инструкцию: CREATE FUNCTION wordcount (str IN VARCHAR2) RETURN PLSJNTEGER AS здесь объявляются локальные переменные BEGIN здесь редлизуется алгоритм END; / Как и в случае простого блока BEGIN... END, приведенного выше, код этой инст- инструкции в SQL*Plus завершается вводом символа косой черты, который помеща- помещается в отдельную строку. Если администратор баз данных предоставил вам привилегию CREATE FUNCTION, эта инструкция заставит Oracle откомпилировать и сохранить в схеме заданную хранимую функцию. И если код будет откомпилирован успешно, вы увидите сле- следующее сообщение: Function created. При условии, что в схеме Oracle уже имеется объект (таблица или хранимая программа) с именем wordcount, выполнение инструкции CREATE FUNCTION завер- завершится сообщением об ошибке: ORA-00955: name is already used by an existing object. Это одна из причин, по которой Oracle поддерживает инструкцию CREATE OR REPLACE FUNCTION — вы будете ее использовать в 99 случаях из 100: CREATE OR REPLACE FUNCTION wordcount (str IN VARCHAR2) RETURN PLSJNTEGER AS то же. что и в примере кода, приведенном выше Инструкция CREATE OR REPLACE FUNCTION позволяет избежать побочных эффек- эффектов, вызванных удалением и повторным созданием программ; она сохраняет все привилегии на объект, предоставленные другим пользователям или ролям. При этом она заменит только объекты одного типа и не станет автоматически удалять таблицу wordcount лишь потому, что вы решили создать функцию с таким же именем. Программисты обычно сохраняют подобные инструкции, равно как и аноним- анонимные блоки, используемые более одного раза, в файлах операционной системы. Например, для хранения рассматриваемой функции можно было бы создать файл wordcount.fun, а для его запуска применить команду SQL'Plus @: SQL> erardcount.fun Function created. Как следует из данного примера, SQL'Plus по умолчанию не выводит содер- содержимое сценария на экран. Для того чтобы исходный код сценария, включая при- присвоенные Oracle номера строк, прокручивался на экране, воспользуемся командой SET ECHO ON. Особенно удобно ее применять в случае возникновения каких-либо
Выполнение базовых операций PL/SQL 73 проблем. Давайте намеренно допустим в нашей программе ошибку, закомменти- закомментировав объявление переменной: SQL» /* Файл в Web: wordcount.fun */ SQL> SET ECHO ON SQL> (awordcourrt.fun SQL> CREATE OR REPLACE FUNCTION wordcount (str IN VARCHAR2) Z RETURN PLSJNTEGER 3 AS 4 /* words PLS_INTEGER :- 0; ***3акоииентировано для преднаиеренной ошибки*** •/ 5 len PLSJNTEGER :- NVL(LENGTHtstr).O); 6 inside_a_word BOOLEAN: 7 BEGIN 8 FOR i IN l..len + 1 9 LOOP 10 IF ASCIKSUBSTRCstr. 1. D) < 33 OR 1 > len 11 THEN 12 IF 1nside_a_word 13 THEN 14 words :- words + 1: 15 inside_a_word :- FALSE; 16 END IF: 17 ELSE 18 inside_a_word :- TRUE; 19 END IF; 20 END LOOP; 21 RETURN words; 22 END; 23 / warning: Function created with compilation errors. SQL> Выведено предупреждающее сообщение, которое мало чем поможет при от- отладке. Поэтому нужен полный текст сообщения об ошибке. И получить его про- проще всего с помощью команды SHOW ERRORS, которую можно сократить до SHOW ERR: SQL> SHOW ERR Errors for FUNCTION WORDCOUNT: LINE/COL ERROR 14/13 PLS-0Q201: identifier 'WORDS' must be declared 14/13 PL/SQL: Statement ignored 21/4 PL/SQL: Statement ignored 21/11 PLS-00201: identifier 'WORDS' must be declared Почему SQLTlus для получения информации об ошибке заставляет вас вы- выполнить еще одно действие, не известно. Компилятор обнаружил оба вхождения переменной и сообщил точные номера строк и столбцов. Команда SHOW ERRORS за- запрашивает представление Oracle USER_ERRORS из словаря данных. Более подроб- подробную информацию об ошибке можно поискать по идентификатору (в данном слу- случае - PLS-00201) в документации Oracle Database Error Messages. Распространенной практикой является добавление команды SHOW ERRORS после каждой инструкции CREATE в хранимой программе PL/SQL. Поэтому «хороший»
74 Глава 2 • Написание и запуск кода PL/SQL шаблон для создания хранимых программ в SQL*Plus может иметь примерно та- такую форму: CREATE OR REPLACE тип программы AS ваш коя I SHOW ERRORS Обычно команда SET ECHO ON в сценарий не включается, а просто вводится в ко- командной строке, когда это требуется. Если ваша программа содержит ошибку, которая может быть определена ком- компилятором, в ответ на инструкцию CREATE Oracle сохранит эту программу в базе данных, но в нерабочем состоянии. Если же вы неверно используете синтаксис инструкции CREATE, Oracle не поймет, что вы хотите сделать, и не сохранит код. ВЫВОД ДРУГИХ ОШИБОК Большинство программистов Oracle знают тольхо одну форму команды SQL'Plus: SQL» SHOW ERRORS Они полагают, что для получения дополнительной информации об ошибке помимо сообщения о результате последней компиляции нужно непосред- непосредственно запрашивать представление USER_ERRORS. Однако если вы зададите в команде SHOW ERRORS категорию и имя объекта, то получите информацию о последних связанных с ним ошибках: SQL> SHOW ERRORS категория ш_обьекта Например, для того чтобы просмотреть информацию о последних ошиб- ошибках в процедуре wordcount, нужно выполнить такую команду; SQL> SHOW ERRORS PROCEDURE wordcount Вы должны насторожиться, получив сообщение No errors. Оно выводится в трех случаях: когда код объекта откомпилирован успеш- успешно, если вы указали неверную категорию (скажем, функцию вместо про- процедуры) и если объекта с заданным вами именем не существует. Полный список категорий, распознаваемых этой командой, зависит от версии СУБД; в Oracle9i он таков: DIMENSION FUNCTION JAVA SOURCE JAVA CLASS PACKAGE PACKAGE BODY PROCEDURE TRIGGER TYPE TYPE BODY VIEW
Выполнение базовых операций PL/SQL 75 Выполнение хранимой программы Вы познакомились с двумя способами вызова хранимой программы: первый со- состоит в ее заключении в простом блоке PL/SQL, второй — в использовании ко- команды EXECUTE среды SQL*Plus. Одни хранимые программы можно использовать в других. Например, функция wordcount применяется везде, где допускается исполь- использование целочисленного выражения. Протестируем функцию wordcount с вход- входным значением CHRO), которое является ASCII-кодом символа табуляции: BEGIN DBMS_OUTPUT.PUT LINEC'Введенная строка содержит ' || wordcount(CHR(9)) || ' слов'); END; / Вызов функции wordcount включен в выражение как аргумент процедуры DBMS_ OUTPUT. PLJT_LINE. В таких случаях PL/SQL автоматически преобразует целое число в строку, чтобы соединить его с двумя другими литеральными выражениями. Ре- Результат получается следующим: Введенная строка содержит 0 слов Многие функции PL/SQL можно вызывать и из SQL-инструкций. Приведем несколько примеров использования функции wordcount. О Включается в список выбора при необходимости вычислить количество слов в столбце таблицы: SELECT Isbn. wordcount(описание) FROM books: О Применяется в ANSI-совместимой инструкции CALL для связи выходных дан- данных с переменной SQL'Plus и вывода результата на печать: VARIABLE words NUMBER CALL wordcount('«eK07-opirt?_re/(C7-') INTO :words: PRINT :words О Используется с той же целью, что и в предыдущем примере, но выполняется из удаленной базы данных, определяемой ссылкой test.newyork.ora.com: CALL wordcountPtest.newyork.ora.comC некоторый текст') INTO ;words: PRINT :words О Принадлежит схеме Ы11 и выполняется при подключении к любой схеме,, имею- имеющей соответствующую привилегию: SELECT bill, wordcount (.описание) FROM books WHERE 1d - 10007: Вывод хранимых программ Рано или поздно вам потребуется просмотреть список имеющихся у вас храни- хранимых программ и последние версии их исходного кода, которые Oracle содержит в словаре данных. (Словарь данных Oracle - множество таблиц и объектов базы данных, которые хранятся в специальной ее области, - ведется исключительно ядром Oracle, содержит информацию об объектах базы данных, пользователях и событиях.) Эта задача выполняется при помощи инструментального средства с графическим пользовательским интерфейсом (такие средства упоминались в главе 1), но если его у вас нет, то можно написать несколько SQL-инструкций, из- извлекающих из словаря данных нужную информацию.
76 Глава 2 • Написание и запуск кода PL/SQL Так, чтобы просмотреть полный список программ (а также таблиц, индексов и других элементов), запросите информацию представления USER_OBJECT: SELECT * FROM USEROBJECTS: Это представление содержит сведения о каждом объекте, а именно его имя, тип, время создания, время последней компиляции, состояние (работоспособный или неработоспособный) и другую информацию. Если вам нужны данные об ин- интерфейсе программы SQL'Plus, проще всего воспользоваться командой DESCRIBE: SQL> DESCRIBE wordcount FUNCTION wordcount RETURNS BINARYINTEGER Argument Name Type In/Out Devault? STR VARCHAR2 IN Вы можете запросить полный исходный код хранимых программ, обратив- обратившись к представлению USERSOURCE или TRIGGERJJOURCE. О том, как получить ин- информацию из этих представлений, содержащихся в словаре данных, подробно рас- рассказывается в главе 19. Управление привилегиями и создание синонимов хранимых программ Созданную вами программу на PL/SQL обычно не может выполнять никто, кро- кроме вас или администратора базы данных. Предоставить право на ее применение другому пользователю можно с помощью инструкции GRANT: GRANT EXECUTE ON wordcount TO scott: Лишить пользователя этой привилегии позволит инструкция REVOKE: REVOKE EXECUTE ON wordcount FROM scott: Привилегия EXECUTE может быть предоставлена роли: GRANT EXECUTE ON wordcount TO alljvis; а также всем пользователям Oracle: GRANT EXECUTE ON wordcount TO PUBLIC: Если вы предоставите привилегию EXECUTE отдельному пользователю (напри- (например, с идентификатором scott), затем — роли, в которую входит этот пользова- пользователь, и, наконец, — всем пользователям, Oracle запомнит все три варианта ее пре- предоставления. Любой из них позволит пользователю scott выполнять программу. Но если вы захотите лишить данного пользователя этой возможности, то должны действовать следующим образом: сначала отменить привилегию пользователю с идентификатором scott, а затем аннулировать привилегию на выполнение функ- функции для всех пользователей (PUBLIC) и роли (или же исключить пользователя из этой роли). Для просмотра списка привилегий, которые вы предоставили другим пользо- пользователям и ролям, можно запросить информацию представления USER_TA8_PRIVS_MADE словаря данных. Имена программ в этом представлении почему-то выводятся в столбце table name.
Выполнение базовых операций PL/SQL 77 SQL> SELECT table_name. grantee, privilege 2 FROM USER_TAB_PRIVS_MADE 3 WHERE tablejiame - 'WORDCOUNT' 4 / TABLE_NAME GRANTEE PRIVILEGE WORDCOUrfT PUBLIC EXECUTE WORDCOUNT SCOn EXECUTE WORDCDUNT MIS_ALL EXECUTE Если пользователь scott имеет привилегию EXECUTE на выполнение программы wordcount, он, возможно, захочет создать ее синоним — в таком случае ему не при- придется писать перед именем программы префикс в виде имени содержащей ее схемы: SQL> CONNECT scott/tiger Соединено. SQL> CREATE OR REPLACE SYNONYM wordcount FOR bill.wordcount: Теперь можно выполнять данную программу, ссылаясь на ее синоним: If wordcount (локаль/ш_лереме«ная) > 100 THEN... Так удобнее, поскольку в случае изменения владельца программы достаточно будет изменить только ее синоним, а не все те хранимые программы, из которых она вызывается. Синоним можно определить для процедуры, функции, пакета или, как в по- последних версиях Oracle, пользовательского типа. Синонимы процедур, функций и пакетов могут скрывать не только схему, но и базу данных; синонимы для уда- удаленных программ создаются так же просто, как и для локальных. Однако сино- синоним для процедуры или функции внутри пакета или типа работать не будет. Удаление синонима производится следующим образом: DROP SYNONIM wordcount; Удаление хранимой программы Если какая-либо хранимая программа вам больше не нужна, ее можно удалить с помощью SQL-инструкции DROP. Например, приведенная ниже инструкция уда- удаляет хранимую функцию wordcount: DROP FUNCTION wordcount; Точно так же удаляются пакеты и типы: DROP PACKAGE мя пакета; DROP TYPE иия_типа; Когда вы удаляете программу, которая вызывается из других программ, по- последние помечаются как неработоспособные. Как скрыть исходный код хранимой программы При создании программы PL/SQL описанным выше способом ее исходный код помещается в словарь данных в виде текста, который администратор базы дан- данных может просмотреть и даже изменить. С целью сохранения профессиональ- профессиональных секретов и предотвращения вмешательства в программный код посторонних, перед распространением его нужно зашифровать или как-то иначе скрыть. В этом случае можно воспользоваться приложением Oracle с интерфейсом ко- командной строки, называемым Wrap. Данное приложение преобразует несколько
78 Глава 2 • Написание и запуск кода PL/SQL инструкций CREATE в комбинацию текстовых символов и шестнадцатеричных чи- чисел. Это действие не является шифрованием в прямом смысле слова, но все же направлено на сокрытие кода. Приведем несколько фрагментов преобразованно- преобразованного таким образом кода: CREATE OR REPLACE FUBCTION wordcount wrapped 0 abed abed .,.разрыв... 1WORDS: 10: 1LEN: 1NVL: 1LENCTH: 1INSIDE_A WORD: 1BOOLEAN: ... разрыв... a5 b 81 ЬО аЗ аО 1С 81 bO 91 51 aO 7e 51 аО Ь4 2e 63 37 :4 aO 51 a5 b a5 b 7e 51 M 2e :2 aO 7e b4 2e 52 10 :3 aO 7e 51 Ь4 2е d :2 aO d b7 19 Зс Ь7 :2 aO d b7 :2 19 Зс Ь7 aO 47 :2 aO Но если вам нужно зашифровать что-то по-настоящему, скажем, при передаче такой секретной информации, как пароль, полагаться на возможности програм- программы Wrap (см. главу 19) не следует. Средства разработки Oracle PL/SQL Если вы собираетесь использовать PL/SQL для программирования, и в частно- частности для создания пользовательского интерфейса, вам потребуются два специаль- специальных инструментальных средства для разработчиков, входящих в состав Oracle9i Developer Suite, а именно Oracle Forms и Oracle Reports. Этот программный про- продукт содержит также среду разработки для Java и менеджер программной конфи- конфигурации. Имеется еще одно инструментальное средство — Oracle Graphics, кото- которое не продается как отдельный продукт, но его функциональные возможности включены в Oracle Forms и Oracle Reports. Вместе с продуктами Forms Builder и Reports Developer поставляется испол- исполнительное ядро, позволяющее программистам запускать свои приложения. Одна- Однако окончательная передача приложения конечным пользователям предполагает его лицензирование с помощью исполнительной среды, такой как Oracle9t Forms Services или Oracle9i Reports Services. Данные приложения являются компонента- компонентами еще одного продукта — Oracle9i Application Server, который обычно выполняет- выполняется на сервере промежуточного уровня и предоставляет конечным пользователям Oracle позволяет использовать в приложеяиях защиту DES (Data Encryption Standard — Стандарт шифрования данных), для чего нужно обратиться к пакету DBMS_OBFUSCATI0N_TOOLKIT.
Средства разработки Oracle PL/SQL 79 формы и отчеты через браузеры, поддерживающие Java. Однако до сих пор широ- широко используются и более старые версии Oracle Forms и Oracle Reports, реализую- реализующиеся в конфигурациях под названием «толстые клиенты», в которых исполни- исполнительное программное обеспечение располагается на машине каждого конечного пользователя. В книге вам будут встречаться ссылки на клиентские средства раз- разработки Oracle — это и есть указанные средства. Относительно базы данных лю- любое программное обеспечение является клиентским, хотя на самом деле вы можете использовать многоуровневую систему, а не простую схему, состоящую из клиен- клиента и сервера На рис. 2.5 показан пользовательский интерфейс приложения Огас1е9г Forms Builder. Вторым справа на этом рисунке представлено окно встроенного редакто- редактора кода PL/SQL. Рис. 2.5. Пользовательский интерфейс приложения Oracle Forms Builder Чтобы помочь пользователям в реализации базовых функций, Oracle предос- предоставляет набор расширений PL/SQL, уникальных для каждого из средств разра- разработки. Так, в Oracle Forms программы на PL/SQL могут: О использовать значения элементов на экране в качестве переменных привязки (например, при вводе пользователем значения в поле isbn блока с именем bk, переменная :bk.isbn связывает значение поля с текущей записью); О заполнять раскрывающиеся списки данными из таблицы базы данных;
80 Глава 2 • Написание и запуск кода PL/SQL О автоматически выполнять обработку событий конечного пользователя, таких, скажем, как щелчок мыши; О управлять внешним видом пользовательского интерфейса, например путем вы- вывода либо сокрытия элементов управления или окон. Клиентский код PL/SQL может содержаться в трех перечисленных ниже час- частях приложения. О Триггеры. Эти блоки PL/SQL выполняются в ответ на специфические собы- события приложения, такие как запуск, щелчок мышью или проверка данных. О Программные блоки внутри приложения. Их можно вызывать из любых дру- других программ того же приложения. О Библиотека PL/SQL многократно применяемых пользовательских клиент- клиентских программ. Программы вызываются из любого клиентского приложения. И, конечно же, из этих приложений можно вызвать серверные программы на PL/SQL. Однако подобные вызовы реализованы в Oracle со множеством ограни- ограничений. Например, переменные и курсоры из серверных пакетов нельзя использо- использовать непосредственно в клиентских программах (для этого приходится писать специальные функции). Перенос программ на PL/SQL между клиентским компьютером и сервером Перенос кода PL/SQL между клиентом и сервером — дело несложное, но только в том случае, если вы не использовали никаких непереносимых элементов языка. На рис. 2.6 показан результат перетаскивания двух хранимых пакетов из базы данных в клиентскую библиотеку PL/SQL. Эти пакеты не компилируются, на что указывает звездочка справа от имени каждого из них. А связано это с тем, что в данных пакетах используется код PL/SQL, который не поддерживается клиент- клиентской стороной, например содержащий встроенный динамический SQL. Для ре- решения данной проблемы нужно удалить подобные программы из пакета или же модифицировать их код таким образом, чтобы в них использовались клиентские вызовы встроенной функции EXECSQL. С указанными пакетами связана еще одна проблема: хотя клиентское ядро PL/SQL может обрабатывать определяемые программистом исключения, которые инициируются с помощью встроенной процедуры RAISE_APPLICATION_ERROR, кли- клиентские приложения не могут сами инициировать эти исключения. Альтернатив- Альтернативным решением может быть сосредоточение всех определяемых программистом клиентских исключений в одном клиентском пакете. Очень часто разработанный для клиента код PL/SQL не пригоден для исполь- использования на сервере, поскольку содержит множество элементов, ограничивающих его переносимость. Например, если код включает ссылки на клиентские переменные привязки, подобные : bks. i sbn, или в нем используются специфичные для Oracle Forms встроенные функции, такие как SHOW_ALERT и EXECUTE_QUERY, он не откомпи- лируется на сервере.
Средства разработки Oracle PL/SQL 81 Рис 2.6. Окно Object Navigator после перетаскивания двух пакетов с сервера на клиентский компьютер Если вы часто используете клиентские средства Oracle, вам, пожалуй, стоит купить книгу Oracle Developer Advanced Forms & Reports (авторы Peter Koletzke и Paul Dorsey), но учтите, что она не для начинающих программистов. Вызов PL/SQL из других языков Возможно, вам захочется вызвать PL/SQL из программного кода, написанного на С, Java, Perl, Visual Basic или другом языке. И если вам когда-нибудь приходи- приходилось создавать приложения с межъязыковыми вызовами, то вы, по-видимому, знаете, что для сопоставления типов данных разных языков иногда приходится прибегать к различным ухищрениям. Это касается в первую очередь составных типов данных, наподобие массивов, записей и объектов, не говоря уже о семанти- семантике параметров или специфичных для конкретного производителя расширении «стандартных» API, в частности ODBC. Приведем несколько примеров. Предположим, у нас имеется функция PL/SQL, принимающая представленный в виде строки ISBN и возвращающая заголовок соответствующей ему книги: /* Файл в web: booktitle.fun */ CREATE OR REPLACE FUNCTION booktitle (isbnjn IN VARCHAR2) RETJRN VARCHAR2 IS I
82 Глава 2 • Написание и запуск кода PL/SQL IJsbn books.t1tle*TYPE; CURSOR 1cur IS SELECT title FROM books WHERE 1sbn - 1sbn_1n; BEGIN OPEN 1cur; FETCH 1cur INTO 1 1sbn: CLOSE 1cur; RETURN 1 1sbn: END; / В SQL'Plus существует несколько способов вызова этой функции. Простей- Простейший из них таков: SQL> EXEC DBMS_0UTPUT.PUT_LINE(bookt1tleC0-596-00l80-0'» Learning Oracle PL/SQL PL/SQL procedure successfully completed. Мы можем вызвать данную функцию не из одной среды, а из нескольких. Пе- Перечислим их: О С, с использованием прекомпилятора Oracle (Рго*С); О Java, с использованием метода JDBC; О Perl, с использованием интерфейсного модуля Perl DBI и драйвера DBD::Oracle; О PL/SQL Server Pages. Это искусственно созданные примеры - имя пользователя и пароль в них же- жестко закодированы, и программы просто выводят результаты. Тем не менее они демонстрируют, с какими проблемами вы можете столкнуться при работе с раз- разными языками. С, с использованием прекомпилятора Oracle (Pro*C) Oracle предоставляет как минимум два разных интерфейса для внешних систем: один называется OCI (Oracle Call Interface), а другой — Рго*С. В состав OCI вхо- входит множество функций, с помощью которых кодируются такие низкоуровневые операции, как открытие и выполнение программы, ее синтаксический анализ, оп- определение и связывание переменных, выборка информации из базы данных. И все это делается для реализации одного запроса. Поскольку даже простейшая про- программа на основе OCI, выполняющая не самую сложную задачу, содержит около 200 строк кода, рассмотрим пример использования Рго*С. Рго*С — это технология прекомпиляции, позволяющая совмещать в исходном коде операторы языков С, SQL и PL/SQL. Такие файлы обрабатываются про- программой Oracle Ргос, которая преобразует их в код на языке С: /* Файл э Web: callbooktitle.pc */ finclude <stdi о.h> finclude <string.h> EXEC SQL BEGIN DECLARE SECTION; VARCHAR uid[20]; VARCHAR pwd[ZD]; VARCHAR isbn[15];
Средства разработки Oracle PL/SQL S3 VARCHAR Dt1tle[400]: EXEC SQL END DECLARE SECTION; EXEC SQL INCLUDE SQLCA.H; 1nt sqlerrorO; 1nt ma1n() { I* Значения типа VARCHAR становятся структурам, содерицими массив символов, и имеет длину */ strcpy((char *)u1d.arr,"scott"); uid.len - (short) strlentlchar *)u1d.arr): strcpyGchar *)pwd.arr,"tiger"); pwd.len - (short) strlen((char *)pwd.arr); /* Это гибрид исключения и оператора goto */ EXEC SQL WHENEVER SQLERROR DO sqlerrorO: /* подклсчаенся к серверу Oracle, а затеи выполняем функции booktitle */ EXEC SQL CONNECT :u1d IDENTIFIED BY :pwd; EXEC SQL EXECUTE BEGIN :bt1tle :- ЬоокгШеСО-БЭ END; END-EXEC; /* вывод результата */ pr1ntf("Js\n\ btme.arr); /* Прерывзеи соединение с ORACLE. */ EXEC SQL COMMIT WORK RELEASE; ex1t@); sqTerrorO ( EXEC SQL WHENEVER SQLERROR CONTINUE; pr1ntf("\nS; ,70s \n". sqlca.sqlerrm.sqlerrmc); EXEC SQL ROLLBACK WORK RELEASE; exit(l); } Как видите, использование Pro* С — это не тот подход, от которого пуристы языка С придут в восторг. Однако во многих компаниях считают, что Pro*С (или Pro'Cobol, или прекомпиляторы для других поддерживаемых Oracle языков про- программирования) может служить разумной альтернативой Visual Basic (который слишком медлен) и OCI (который слишком сложен). Java, с использованием 3D ВС Как и С, Oracle поддерживает несколько разных подходов при подключении к базе данных. Один из них - встроенный SQL, который называется SQLJ, - подобен другим прекомпиляторным технологиям Oracle, хотя и имеет больше возможностей
84 Глава 2 • Написание и запуск кода PL/SQL для отладки. Но более популярен метод доступа к базе данных, называемый JDBC (Java DataBase Connectivity): /* Файл в Web: callbooktitle.java */ import java.sql.*; public class book public static void main(Str1ng[] args) throws SQLException // инициализируем драйвер и пытаемся установить подключение к базе данных DriverManager.registerDriver (new oracle.jdbc.driver.OracleDriverO): Connection conn - DriverManager.getConnection("jdbc:oracle:thin-.@lQcalhost:1521:092", "scott". "tiger"): II метод prepareCall использует "call"-синтаксис ANSI92 CallableStatement cstmt - conn.prepareCall("{? - call booktitleC?)}"): // связывание переменных и параметров cstmt.registerOutParameterd. Types.VARCHAR); cstmt.setStringB. -596-00180-0"): // Теперь мы можем выполнить запрос и получить результат. // закрыть подключение и вывести результат на печать cstmt.executeUpdateC): String bookTitle - cstmt.getString(l): conn.closet): System.out.printlntboolcTitle): В этом примере используется драйвер, отличающийся прекрасной совмести- совместимостью и простотой инсталляции (все необходимое для сетевых протоколов име- имеется в библиотеке Java), однако большое количество передаваемой через соедине- соединение информации сказывается на его производительности. В качестве альтернати- альтернативы можно использовать драйвер OCI. Пусть вас не пугает это имя - он совсем не так сложен, как одноименный интерфейс, о котором упоминалось в предыдущем разделе. Для того чтобы больше узнать о программировании на Java для Oracle, вы мо- можете обратиться к книгам издательства O'Reilly Java Programming with Oracle JDBC (автор Don Bales) к Java Programming with Oracle SQL} (автор Jason Price). Perl, с использованием Perl DBI и DBD::Oracle Очень любимый системными администраторами язык Perl можно назвать праро- прародителем всех языков с открытым исходным кодом. И сейчас, когда вылущена уже шестая его версия, он может практически все и распространен, кажется, повсеме- повсеместно. Если воспользоваться каким-нибудь эффективным средством, например CPAN (Comprehensive Perl Archive Network — обширный сетевой архив ресурсов Для языка PER), ничего не стоит установить такие интерфейсные модули, как DataBase Interface (DBI) и соответствующий ему драйвер Oracle, DBD::Oracle: /* Файл в Web: callboQktitle.pl */ #!/usr/bin/perl
Средства разработки Oracle PL/SQL 85 use strict: use DBI qw(:sql_types): # либо установить подключение, либо вывести сообщения die my $dbh - DBI->connect( 'dbi: Oracle-.o92'. 'scott1. 'tiger'. RaiseError => 1, AutoCommit -> 0 ) || die "Соединение с базой данных не установлено: $DBI::errstr"; щу Sretval: # вызов Oracle для синтаксического анализа инструкции eval { my $func = $dbh->prepare(q{ BEGIN :retval :- booktitle(isbri_1n -> :bindl): END; # связывание параметров и выполнение процедуры $func->bind_paramC":bindl". "Q-596-00180-Q"): $func->bind_param_1noutC":retval". \Jretval, SQL_VARCHAR); $func->execute; { warn "Хранимая процедура выполнена с ошибкой: $DBI::errstr\n"; $dbh->ro11back: } else { print "Хранимая процедура возвращает: Sretval\n": } # не'забыть отключиться $dbh->discornect; С другой стороны, Perl — один из тех языков, на которых очень легко написать совершенно нечитабельный код. К тому же он не обеспечивает желаемой скоро- скорости выполнения программ и не отличается компактностью кода. За дополнительной информацией о взаимодействии между Perl и Oracle вы можете обратиться к книгам издательства O'Reilly Perl for Oracle DBAs (авторы Andy Duncan и Jared Stil) и Programming the Perl DBI (автор Alligator Descartes). Кроме того, имеется множество замечательных книг, посвященных языку Perl, где эта тема освещается достаточно глубоко. PL/SQL Server Pages Рабочая среда PL/SQL Server Pages (PSP) является замечательным средством для быстрого создания web-страниц, а также еще одной из технологий прекомпи- ляции, позволяющих вставлять код PL/SQL в HTML-страницы: /* Файл з Web: favorite_plsql book.psp */ <19 page language-"PL/SQL" %>~
86 Глава 2 • Написание и запуск кода PIV5QL <S? plsql procedure-"favorite_plsql_book" *> <HTML> <HEAD> <TITLE>My favorite book about PL/SQL</TITLE> </HEAO> <BODY> </BODY> </HTML> Операторы наподобие <%- %> означают, что следует выполнить указанный код PL/SQL и вернуть результат на страницу. Если приведенную страницу правиль- правильно установить на web-сервере, подключенном к базе данных Oracle, она будет вы- выглядеть так, как показано на рис. 2.7. •5 My favonle book about PI/SQL - Mitrospft Internet ;;Файл Правка .Вид , Избранное .".Ospsra.. Справка * Apse: |:|/] http: //Jerome, datacraf t. com/pls/opp3/f avorlto _plsql_book. psp Leaining Oracle PL/SQL Рис. 2.7. Результат работы PL/SQL Server Pages Технология PL/SQL Server Pages пришлась по душе многим программистам и пользователям, так как предоставляет отличный способ быстрого создания web- узлов. За дополнительной информацией об этой технологии вы можете обратить- обратиться к книге Learning Oracle PL/SQL, написанной теми же авторами, что и книга, ко- которую вы сейчас держите в руках. 4то же дальше Итак, вы узнали, как использовать PL/SQL в SQL*Plus и ряде других распро- распространенных средах, а также совместно с другими языками программирования. Кроме того, код PL/SQL можно: О встраивать в программы на языках COBOL и FORTRAN, обрабатывать по- посредством прекомпиляторов Oracle; О вызывать из языка программирования Ada с помощью технологии, называе- называемой SQL*Module; О выполнять автоматически, как триггеры событий, происходящих в базе дан- данных Oracle, например при обновлении таблицы; О включать в расписание для выполнения в базе данных Oracle с использовани- использованием встроенного пакета DBMS_JOB. Некоторые из этих возможностей рассматриваются в последующих главах,
3 Основы языка > Структура блока PL/SQL > Набор символов РЦ/SQL > Идентификаторы > Литералы > Разделитель в виде точки с запятой > Комментарии > Ключевое слово PRAGMA > Метки Каждый язык, будь то человеческий или компьютерный, имеет синтаксис, сло- словарь и набор символов, и для того чтобы общаться с помощью этого языка, нужно выучить правила его использования. Многие с опаской приступают к изучению новых компьютерных языков, но вообще-то они очень просты, a PL/SQL еще и один из самых простых. Трудность общения на языке, основанном на байтах, свя- связана не с самим языком, а с компилятором или компьютером, с которым мы «го- «говорим». Большинство компиляторов — это «недалекие» создания, их трудно на- назвать творческими и гибкими, они не способны нестандартно мыслить, а их словарь крайне ограничен, Разве что соображают они очень-очень быстро. Если вы слышите что-нибудь вроде: «Давай выпьем чаю», го понимаете, о чем вам говорят, и знаете, что ответить. Однако если вы скажете PL/SQL: «подкинь-ка еще с полдюжины записей», то едва ли получите требуемое. Говоря на языке син- синтаксиса, для использования PL/SQL нужно расставлять все точки над <i». Поэто- Поэтому в данной главе описываются основные правила языка, помогающие общаться с компилятором PL/SQL, а также структура блока PL/SQL. Кроме того, вы уз- узнаете, что такое лексические единицы и для чего предназначено ключевое слово PRAGMA. Структура блока PL/SQL Все языки программирования позволяют организовать логически связанные эле- элементы в программные единицы. В PL/SQL фундаментальной организационной
88 Глава 3 • Основы языка программкой единицей является блок. Он лежат в основе двух ключевых языко- языковых концепций. О Модульность. Блок PL/SQL является базовой единицей кода. Из данных единиц создаются модули (например, процедуры и функции), которые, в свою очередь, составляют приложение. Именно блоки как организационные единицы низшего уровня определяют читабельность и простоту сопровождения про- программного кода. О Область действия. Блок определяет область действия или контекст логически связанных объектов. В блоке группируются связанные между собой объявле- объявления и выполняемые операторы. Блок может быть анонимным (то есть не имеющим имени) или именованным (представляющим собой процедуру и функцию). С блоком связано понятие пакета PL/SQL, который представляет собой набор из нескольких процедур и функций. Разделы блока PL/SQL Блок PL/SQL может содержать до четырех разделов, однако только один из них является обязательным. О Заголовок. Используется только в именованных блоках, определяет способ вызова именованного блока или программы. Не обязателен. О Раздел объявлений. Идентифицирует переменные, курсоры и вложенные блоки, на которые имеются ссылки в исполняемом блоке и блоке исключений. Не обязателен. О Исполняемый раздел. Операторы, которые выполняются ядром PL/SQL во время работы приложения. Обязателен. О Раздел исключений. Обрабатывает исключения (предупреждения и ошибки). Не обязателен. Структура блока PL/SQL для процедуры показана на рис. 3.1. Заголовок IS : Раздел объявлений j BEGIN ""' ' I Исполняемый раздел \ EXCEPTION ) Щ Раздел исключений ; I-END; ! Рис 3.1. Структура блока PL/SQL Порядок размещения разделов блока соответствует последовательности напи- написания и выполнения программы. 1. Определяются тип блока (процедура, функция, анонимный) и способ его вы- вызова (заголовок).
Структура блока PL/SQL 89 2. Объявляются все переменные и другие объекты PL/SQL, используемые в этом блоке (раздел объявлений). 3. Локальные переменные и другие объекты PL/SQL, объявленные выше, при- применяются для выполнения необходимых действий. 4. Обрабатываются все проблемные ситуации, которые могут возникнуть во вре- время выполнения блока (раздел исключений). Процедура, содержащая все четыре раздела, показана на рис. 3.2. В действи- действительности все перечисленные разделы блока создаются не сразу, а в несколько этапов. Не ожидайте, что вы достигнете совершенства с первой попытки! |PROCEOURE get_hanpy (enamejn IN VARCHAR2) IS hiredate DATE: BEGIN hiredate -.- SYSn.ME - 2: INSERT INTO employee (emp_name, hiredate) VALUES (ename 1n. hiredate): Заголовок Раздел объявлений - Исполняемый раздел EXCEPTION WHEN DUP VAL IN THEN DMBS .INDEX OUTPUT.PUT LINE 'Вставке не произведена'): - Разбел исключений END; Рис. 3.2. Процедура, которая содержит все четыре раздела Анонимные блоки Когда кто-то хочет остаться неизвестным, он не называет своего имени. То же можно сказать и об анонимном блоке PL/SQL, показанном на рис. 3.3: в нем во- вообще нет раздела заголовка, блок начинается ключевым словом DECLARE (или BEGIN). - Исполняемый раздел Рис. 3.3. Анонимный блок без разделов объявлений и исключений Анонимный блок не может быть вызван другим блоком, поскольку не имеет составляющей, с помощью которой к нему можно обратиться. Таким образом, он является чем-то вроде сценария, который предназначен для выполнения опера- операторов PL/SQJL, в большинстве случаев включающих вызовы процедур и функций. Поскольку анонимные блоки могут содержать собственные разделы объявлений BEGIN DBMS END: OUTPUT PUT LINE! ¦Hello word V. .-
90 Глава 3 • Основы языка и исключений, разработчики часто используют их для определения области ви- видимости идентификаторов и области действия обработки исключений в програм- программе большего размера. Эти свойства блока мы подробно рассмотрим в следующих разделах. Структура анонимного блока Общий формат анонимного блока PL/SQL таков: [ DECLARE ... необязательные операторы объявлений ... ] BEGIN ... исполняемые операторы ... [ EXCEPTION ... необязательные операторы обработки исключений ... ] END; Квадратные скобки указывают на необязательную часть синтаксиса. Ключе- Ключевые слова BEGIN и END являются обязательными, и между ними должен быть как минимум один исполняемый оператор. Примеры анонимных блоков Ниже приведены различные сочетания разделов, которые допустимы для блока PL/SQL. О Анонимный блок BEGIN.. .END без разделов объявлений и исключений: BEGIN -- Вывод текущей даты в стандартном формате DBMS OUTPUT.PUT LINEfSYSDATE): END; О Анонимный блок с разделом объявлений, но без раздела исключений: DECLARE l_right now DATE :- SYSDATE; BEGIN DBMS OUTPUT.PUT LINEC1 right now ); END: " - " О Анонимный блок с разделом объявлений, исполняемым разделом и разделом исключений: DECLARE -- Вызов определенной ранее функции для получения даты -- найма сотрудника по фамилии "FEUERSTEIN". IJiiredate DATE :- employee_pkg.date_of_hire ('FEUERSTEIN'); l_right_now DATE :- SYSDATE; l_old timer EXCEPTION; BEGIN IF ljiiredate < ADD_MONTHS A Mghtjiow. 6} THEN (WISE 1 old timer; ELSE l_hiredate :- rightjnow:
Структура блока PL/SQL 91 END IF; EXCEPTION WHEN l_old_timer THEN DBMS_OUTPUT.PUT LINEt'y вас нет доступа к информации.'); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('ОШИБКА: ' || SQLCODE); END; Анонимные блоки выполняют последовательности операторов и затем завер- завершают свою работу, действуя как процедуры. Фактически все анонимные блоки являются анонимными процедурами. Анонимные блоки в различных средах Анонимные блоки используются в средах, где код PL/SQL выполняется либо не- непосредственно, либо как часть некоторой программы этой среды (табл. 3.1). Объект, включающий конкретный блок, обеспечивает его контекст и в ряде случаев - имя программы. Таблица 3.1. Анонимные блоки в различных средах Объект, содержащий блок Среда Описание Клиентский триггер Средства разработки Oracle Триггер базы данных Сценарий Манипуляции данными на уровне таблицы или события базы данных SQL'Plus или его эквивалент Откомпилированная программа Встроенные языки (Рго*С и т. п.); OCI (Oracle Call Interface) «Чистые» исполняемые операторы в триггерах Forms Builder или Reports Builder, оформленные соответствующим инструментальным средством как анонимные блоки и отправляемые исполнительному ядру PL/SQL (подробнее о триггерах см. в главе 18) Тело триггера. Хотя у триггера имеется имя, код PL/SQL анонимен (не имеет имени) Программы и пакеггно выполняемые сценарии, вызывающие процедуры и/или функции. Кроме того, команда SQL*Plus EXECUTE транслирует свой аргумент в анонимный блок, заключая его в операторы BEGIN и END Анонимные блоки, встроенные в код программы и выполняемые сервером базы данных Когда вы связываете код PL/SQL с триггером или полем, пользуясь таким ин- инструментальным средством, как Forms Builder, этот код составляет анонимный блок PL/SQL. При этом можно создать полный блок с объявлениями, исполняемыми
92 Глава 3 • Основы языка операторами и разделом обработки исключений или же ограничиться только ис- исполняемыми операторами. Именованные блоки Хотя анонимные блоки PL/SQL применяются во многих приложениях Oracle, вероятно, большая часть написанного вами кода будет оформлена в виде имено- именованных блоков. Ранее вы уже видели несколько примеров хранимых процедур (см. рис. 3.1) и знаете, что их главной особенностью является наличие заголовка. Заголовок процедуры выглядит так: PROCEDURE [схема.]имя [(параметр [, параметр ...])] [AUID {DEFINER | CURRENTJJSER}] Заголовок функции в целом очень похож на него, но дополнительно содержит ключевое слово RETURN: FUNCTION [схема.]мя [(.параметр [, параметр ...])] RETURN тип возвращаеиыхданных [AUID {DEFINER | CURRENTJJSER}] [DETERMINISTIC] [PARALLEL ENABLE ...] [PIPELINED] Поскольку Oracle позволяет вызывать некоторые функции из SQL-инструк- SQL-инструкций, заголовок функции включает больше необязательных элементов, чем заго- заголовок процедуры. Таким образом, вам предоставляется гораздо больше возмож- возможностей управлять поведением функции во время ее выполнения. Более подробно процедуры и функции рассмотрены в главе 16. Вложенные блоки Вложенным является блок, который располагается внутри другого блока. При- Пример процедуры, содержащей анонимный вложенный блок, приведен ниже: PROCEDURE caictotals IS yearjtotal NUMBER; BEGIN year_total := 0; /* Начало вложенного блока */ DECLARE month_total NUMBER: BEGIN month_total :- year_total / 12: END sst_month_total: /* Конец вложенного блока */ END: Между операторами /* и */ помещается комментарий (см. раздел «Коммента- «Комментарии» далее в этой главе). Вложенный блок, в свою очередь, также может содер- содержать блок. Таким образом, допустимой является структура вложенных блоков, представленная на рис. 3.4.
Структура блока PL/SQL 93 DECLARE CURSOR mp_cur IS ...: BEGIN DECLARE total_sales NUMBER; BEGIN DECLARE Mredate DATE; BEGIN END; END END; Рис. З.4. Вложенные анонимные блоки Вложенный блок PL/SQL называют также дочерним или подблоком, а внеш- внешний блок — родительским. Область действия Главное преимущество вложения блоков — возможность ограничить область дей- действия всех объявленных во вложенном блоке объектов и исполняемых операто- операторов. Она, в свою очередь, позволяет не только лучше управлять поведением про- программы, но и уменьшить вероятность того, что по ошибке программист изменит значение не той переменной. Переменные, исключения, модули и некоторые другие структуры являются локальными для блока, в котором они объявлены. Когда выполнение блока пре- прекратится, ни на одну из этих структур сослаться будет нельзя. Так, в приведенной выше процедуре cal cjtotal s к элементам внешнего блока, в частности к перемен- переменной year_tota1, можно обращаться из любого места процедуры, тогда как элемен- элементы, объявленные во внутреннем блоке, из внешнего недоступны. Чаще всего вложенный блок создается с целью размещения в нем раздела об- обработки исключений. Когда исключение обрабатывается в программе, она про- продолжает свое выполнение, вместо того чтобы завершиться с ошибкой. Например, если вы не уверены в том, имеется ли в базе данных конкретная запись, можно поступить так: BEGIN 8EGIN /* вложенный блок */ INSERT INTO bookcopies (isbn. qty) VALUES (:newisbn, -.copies); EXCEPTION WHEN DUP_VAL ONJNDEX THEN UPDATE book_copies SET qty - :cop1es WHERE 1sbn - :newisbn:
94 Глава 3 • Основы языка END: /* вложенный блок */ books. <J1 stn butej nventory_report; END: В данном случае вложенный блок помогает обеспечить наличие в таблице нужной записи перед вызовом процедуры books. d1 str1 butej nventory_report. Kpo- - ме того, он предотвращает прекращение работы программы из-за исключения DUP_VAL_ON_INDEX. Однако в случае появления таких ошибок, как ORA-00942: Table or view does not exist, может оказаться, что работу программы необходимо пре- прервать. Важно понимать, что именно вы должны контролировать происходящее и решать, как будет действовать приложение в том или ином случае. Более подроб- подробно об обработке ошибок в программах PL/SQL рассказывается в главе б. Видимость переменной Важным свойством переменной, связанным с областью ее действия, является ви- видимость. Данное свойство определяет, можно ли обращаться к переменной толь- только по ее имени или же к имени необходимо добавлять префикс. «Видимые» идентификаторы Рассмотрим следующий фрагмент кода: DECLARE flrstjay DATE: last_day DATE; BEGIN first day :- SYSDATE; lastjay :- ADD MONTHS С first day, 6): END: Переменные f 1 rst_day и 1 ast_day объявлены в том же блоке, где они использу- используются, поэтому при обращении к ним указаны только их имена. Мы говорим, что идентификаторы этих переменных видимы. В общем случае видимыми иденти- идентификаторами являются: О индексная переменная цикла (видима и доступна только внутри цикла); О идентификатор, объявленный в текущем блоке; О идентификатор, объявленный в блоке, который включает текущий блок; О отдельный объект базы данных (таблица, представление и т. д.) или объект PL/SQL (процедура, функция), владельцем хоторого вы являетесь; О отдельный объект базы данных или объехт PL/SQL, на который у вас есть со- соответствующие права и который определяется соответствующим синонимом. Следовательно, ссылаться на эти объекты можно, просто указывая их имена. PL/SQL позволяет также обращаться к существующим объектам, которые не на- находятся в пределах непосредственной видимости блока. Как это делается, расска- рассказано в следующем разделе. Уточненные идентификаторы Характерными примерами идентификаторов, которые невидимы в области кода, где они используются, являются идентификаторы, объявленные в спецификации
Структура блока PL/SQL 95 пакета (имена переменных, типы данных, а также имена процедур и функций), Делая ссылку на такой объект, необходимо перед его именем указывать префикс и символ точки (аналогичным образом вы уточняете имя столбца посредством имени таблицы, в которой он содержится). Например: О рг1 се uti 1. computejneans - программа с именем conputejneans из пакета pri се_Л11; О math .pi - константа с именем р1, объявленная и инициализированная в пакете math; О : GLOBAL. company. 1 d - глобальная переменная в Oracle Forms. (В описании такой переменной указано, что она собой представляет, но по ее имени невозможно определить тип - это еще один аргумент в пользу соглашения об информативных именах.) Можно также использовать дополнительное уточнение, определяющее вла- владельца объекта. Например, выражение scott. рП ce_utn. computejneans указывает на процедуру computejneans пакета pr1ce_uti 1, принадлежащего пользо- пользователю Oracle с учетной записью scott. Уточнение идентификаторов с помощью имен модулей Обеспечить правильное применение идентификаторов в PL/SQL можно несколь- несколькими методами. В частности, при работе с пакетами удобно создавать переменные, имеющие глобальную область действия. Предположим, у нас есть пакет сотра- ny_pkg и в спецификации пакета объявлена переменная с именем 1 ast_company_i d: PACKAGE companyjjkg IS last_ccmpanyjd NUMBER: END comparej)kg; Далее, ссылаясь на переменную Таз^сапра^Шизвне^'неббходимо предварять ее имя именем этого пакета: IF new_companyjd - company_plcg. 1 ast_company_1 d THEN Поскольку переменная, объявленная в спецификации пакета, глобальна для сеанса, на нее можно ссылаться в любой программе, но доступна она только при указании имени пакета. Идентификатор можно уточнить также с помощью имени модуля, в котором он определен: PROCEDURE caic totals IS salary NUMBER; BEGIN DECLARE salary NUMBER; BEGIN salary :- calc totals.salary: END; end!"
96 Глава 3 • Основы языка В первом объявлении создается переменная salary, областью действия кото- которой является вся процедура. Однако во вложенном блоке объявляется другая пе- переменная с тем же именем. Поэтому ссылка на переменную sal агу во внутреннем блоке всегда сначала разрешается относительно объявления в этом блоке, где пе- переменная видима безо всяких уточнений. Чтобы во внутреннем блоке сделать ссылку на переменную salary, объявленную на уровне процедуры, необходимо уточнить ее имя посредством имени процедуры (ca1_totals.salary). В PL/SQL имеется много правил, с помощью которых можно найти выход из подобных ситуаций. И хотя знать их полезно, лучше использовать уникальные идентификаторы, чтобы избежать конфликтов имен вообще. Набор символов PL/SQL Программа PL/SQL представляет собой последовательность операторов, состоя- состоящих из одной или нескольких строк текста. Набор символов, из которых можно со- составлять эти строки текста, зависит от используемого в базе данных набора симво- символов. Для примера в табл. 3.2 приведены символы, входящие в набор US7ASCII. Таблица 3.2. Символы из набора US7ASCII, которые можно использовать в PL/SQL Тип Символы Буквы A-Z, э-z Цифры 0-9 Знаки ~!@#$%*()_-+=|[]{}:;"' <>,?/Л Пробельные Табуляция, пробел, новая строка, возврат каретки символы Каждое ключевое слово в PL/SQL состоит из разных комбинаций символов данного набора. По умолчанию язык SQL не чувствителен к регистру символов. Это означает, что символы верхнего регистра в нем интерпретируются так же, как символы нижнего, за исключением ситуаций, когда символы заключены в оди- одинарные кавычки, что превращает их в литеральные строки. Многие из указанных символов, в отдельности или в сочетании с другими символами, имеют в PL/SQL специальное назначение. Простые и составные спе- специальные символы описаны в табл. 3.3. Таблица 3.3. Простые и составные специальные символы в PL/SQL Символы Описание ; Завершает объявления и операторы % Индикатор атрибута (атрибут курсора, подобный %ISOPEN, или атрибут неявных объявлений, например %R0WTYPE); кроме этого, используется в качестве символа подстановки в условии LIKE _ Символ подстановки одного символа в условии LIKE @ Индикатор удаленного местоположения (произносится «эта»)
Набор символов PL7SQL 97 Символы Описание : Индикатор хост-переменной, например iblock.item в Oracle Forms ** Оператор возведения в степень о или != или Оператор сравнения «не равно» Л= или ~= 11 Оператор конкатенации « и >> Ограничители метки <= и >= операторы сравнения «меньше или равно» и «больше или равно» := Оператор присваивания => Оператор ассоциации Оператор диапазона Индикатор однострочного комментария I* и •/ Начальный и конечный ограничители многострочного комментария Символы группируются в лексические единицы, которые называют атомар- атомарными, поскольку они представляют собой наименьшие самостоятельные элемен- элементы языка. В PL/SQL лексическими единицами являются идентификатор, лите- литерал, разделитель и комментарий. Мы рассмотрим их в следующих разделах. Идентификаторы Идентификатор — это имя объекта PL/SQL, которым может быть: О константа; О скалярная переменная; О составная переменная (запись или коллекция); О исключение; О процедура; О функция; О пакет; О тип; " О курсор; О зарезервированное слово; О метка. К идентификаторам PL/SQL предъявляются следующие требования: О длина - до 30 символов; О должны начинаться с буквы; О не должны содержать пробелов (но могут включать символы «$», «_» и «#»).
98 Глава 3 • Основы языка Если единственным отличием двух идентификаторов является регистр одного или более символов, компилятор PL/SQL воспринимает их как один и тот же идентификатор1. В частности, одинаковыми считаются следующие идентифика- идентификаторы: lots_of_$MONEVS LCTS_of_$MONEYJ Lots_of_$MoneyJ Примеры допустимых имен идентификаторов вы найдете далее: company_1d# pr"lmary_acct_re5pons1 Ы1 ity Firstjame FirstName addressjinel S123456 Идентификаторы, которые приведены ниже, в PL/SQL недопустимы: lst_year -- Начинается с цифры procedure-name -- Содержит недопустимый символ "-" m1n1mum_J_dije -- Содержит недопустимый символ "Г max1mjm_valije_exploded_for_deta1l -- Имя слишком длинное company 10 -- Имя не должно содержать пробелов Идентификаторы - это имена, с помощью которых можно обращаться к эле- элементам программы, и лучше, когда они отражают назначение элементов. Не стоит пользоваться именами, подобными XI и temp, поскольку они ни о чем не говорят ни вам, ни программисту, которому придется работать с вашим кодом. Некоторые из перечисленных правил именования объектов можно нарушать, если заключить идентификатор в двойные кавычки. Однако мы не рекомендуем лользоваться этим методом. Хотя когда-нибудь вы наверняка встретите «крутой* код вроде следующего: 50L> DECUWE 2 "р1" CONSTANT NUMBER :- 3.141592654: 3 "PI" CONSTANT NUMBER :- 3.14159265356979323846: 4  p1" CONSTANT NUMBER :- 2 * "p1"; 5 BEGIN 6 DBMSJUTPUT.PUT LINECpi: ' || pp1"J; 7 OBMS_OUTPUT.PUT~LINECPI: ' || pD; 8 DBMSJRJTPUT.PUT LINEC2 p1: ' ||  рГ): 9* END; 10 / p1: 3.141592654 PI: 3.14159265358979323846 2 p1: 6.283185308 С этой целью компилятор для своего внутреннего представления на ранней стадии компиляции пере- переводит всю программу в верхний регистр.
Набор символов PL/SQL 99 Обратите внимание, что в строке 7 идентификатор pi используется без кавы- кавычек. Поскольку компилятор обеспечивает независимость от регистра, преобразуя все идентификаторы и ключевые слова в верхний регистр, данный идентифика- идентификатор указывает на переменную, объявленную в строке 3 как "PI". Иногда двойные кавычки применяются в SQL-инструкциях для ссылки на таблицы базы данных, имена столбцов которых содержат несколько слов, разде- разделенных пробелами. Зарезервированные слова Некоторые идентификаторы (такие как BEGIN, IF и THEN) имеют в языке PL/SQJL специальное назначение. К ним относятся: О ключевые слова; О идентификаторы пакета STANDARD. Вы не должны, и в большинстве случаев не сможете, использовать их в качест- качестве имен объектов, объявляемых в своих программах. Ключевые слова Существуют слова, которые, с точки зрения компилятора PL/SQL, имеют строго определенную семантику - являются ключевыми. К таковым относится, напри- например, слово END, завершающее программу, условный оператор IF и оператор цикла. Предположим, вы Попытаетесь объявить переменную с именем end, как в следую- следующем примере: DECLARE end VARCHAR2U0J :- 'Ы1р'; /* Не будет работать! END нельзя использовать в качестве имени перепенной,*/ BEGIN DBMS OUTPUT.PUT LINE(end); END; / В ответ получите сообщение об ошибке компиляции: PLS-00103: Encountered the symbol "END" when expecting one of the following: Слово end в разделе объявлений сообщает компилятору PL/SQL о преждевре- преждевременном завершении анонимного блока. Идентификаторы пакета STANDARD Не следует использовать в качестве идентификаторов имена объектов, которые определены в специальном пакете STANDARD. В нем объявлено большое число иден- идентификаторов - это имена встроенных исключений (подобные DUP VAL ON INDEX), функций (например, UPPER, REPLACE и ТО_ОАТЕ) и подтипов (таких как STRING"). Спи- Список функций и процедур модуля STANDARD можно просмотреть с помощью команды SQL> DESC SYS.STANDARD
100 Глава 3 • Основы языка Если все же вам потребуется назначить своей переменной имя встроенного объекта модуля STANDARD, то вы можете сделать это. На соответствующий модулю объект можно будет ссылаться, указав префикс STANDARD, как показано в следую- следующем примере: DECLARE dup_val_on_index EXCEPTION: -- локальное переопределение BEGIN INSERT INTO ... /* может вызвать встроенное исключение */ RAISE dup_val_on_index; -- разрешается как ссылка на локально объявленное исключение EXCEPTION WHEN dup_val_on_index THEN /* обработка локально объявленного исключения */ WHEN STANDARD.DUP_VAL_ON_INDEX THEN /* обработка обычного исключения */ end! ' Как избежать использования зарезервированных слов в качестве идентификаторов Придумать для идентификатора отвечающее требованиям имя совершенно не- несложно, поскольку существуют тысячи и тысячи сочетаний допустимых симво- символов. Проблема в том, как при этом избежать использования одного из зарезерви- зарезервированных слов — ведь в PL/SQL их сотни! Можно, конечно, проигнорировать данный момент и исправлять ошибки ком- компиляции по мере их возникновения. Именно так большинство и поступает, но на поиск ошибок иногда тратится целый рабочий день, поскольку сообщения об ошиб- ошибках компиляции часто не отражают причину ошибки. Некоторые программисты используют дорогостоящие средства разработки с мощным редактором, который имеет встроенные функции синтаксического анализа и сообщает о возможных проблемах по мере ввода программного кода. Мы же предпочитаем пользоваться дешевым редактором, который знает ключевые слова PL/SQL и автоматически их выделяет. Ниже приведен минимальный список слов, которые нельзя использовать в ка- качестве идентификаторов в программах PL/SQL (табл. 3.4). При составлении этой таблицы применялся следующий метод. Для каждого зарезервированного слова (их полный список хранится в представлении VSRESERVEDWORDS) мы пытались объ- объявить сначала переменную, а затем процедуру с таким же именем. Если компиля- компилятор не позволял выполнить одну или обе эти операции, соответствующее ключе- ключевое слово помещалось в список. Если вы работаете с Oracle 8.1.5 или более поздней версией и имеете соответ- соответствующие права, то для получения полного списка зарезервированных слов мо- можете выполнить следующую инструкцию SQL: SQL> SELECT * FROM VJRESERVED WORDS;
Набор символов PL/SQL 101 Хотя данный список очень длинный, он не включает идентификаторы модуля STANDARD. Это еще один аргумент в пользу «интеллектуальных» редакторов. Таблица 3.4. Минимальный список слов, которые нельзя использовать в качестве идентификаторов в программах PL/SQL ACCESS ADD ALL ALTER AND ANY AS ASC AT AUDIT BEGIN BETWEEN BY CASE CHAR CHECK CLOSE CLUSTER COLUMN COLUMNS COMMENT COMMIT COMPRESS CONNECT CREATE CURRENT CURSOR DATE DECIMAL DECLARE DEFAULT DELETE DESC DISTINCT DROP ELSE END EXCLUSIVE EXISTS RLE FLOAT FOR FROM FUNCTION GRANT GROUP HAVING IDENTIFIED IF IMMEDIATE IN INCREMENT INDEX INDEXES INITIAL INSERT INTEGER INTERSECT INTO IS LEVEL LIKE LOCK LONG MAXEXTENTS MINUS MLSLABEL MODE MODIFY NOAUDn" NOCOMPRESS NOT NOWAIT NULL NUMBER OF OFFLINE ON ONLINE OPEN OPTION OR ORDER OVERLAPS PACKAGE PCTFREE PRIOR PRIVILEGES PROCEDURE PUBLIC RAW RENAME RESOURCE RETURN REVOKE ROLLBACK ROW ROWID ROWNUM ROWS SAVEPOINT SELECT SESSION SET SHARE SIZE SMALLJNT START SUCCESSFUL SYNONYM SYSDATE TABLE THEN TO TRIGGER TYPE UID UNION UNIQUE UPDATE USE USER VALIDATE VALUES VARCHAR VARCHAR2 VIEW WHEN WHENEVER WHERE WITH Отступы и ключевые слова Идентификаторы необходимо отделять друг от друга хотя бы одним пробелом или разделителем. Кроме этого, вы можете форматировать текст программы, встав- вставляя дополнительные пробелы, символы разрыва строки (перевод строки и/или возврат каретки) и табуляции в местах, где полагаются пробелы, и это не вызовет сообщения об ошибке. Например, два приведенных ниже оператора эквивалентны: IF toojiany_orders THEN warn_user; ELS IF no orders entered
102 Глава 3 • Основы языка THEN prmpt_for_orders; END IF: IF toqjnanyjjrders THEN warn_user; ELSIF no orders_entered THEN prompt_for_orders; END IF; " Однако внутри лексических единиц, таких как оператор «не равно», представ- представленный символами «]=», наличие пробелов, символов разрыва строки или табу- табуляции не допускается. В частности, при компиляции строки IF max_salary ! - min_salary THEN вы получите сообщение об ошибке, поскольку между символами «1 -» имеется пробел. Питералы Литерал — это значение, с которым не связан идентификатор, оно существует «само по себе». Литерал может быть представлен одним из следующих типов данных: NUMBER (например, 415, 21,6 или NULL), STRING (например, 'Предложение', 101-FEB-2Q03', NULL) либо BOOLEAN (а именно TRUE, FALSE или NULL). Заметьте, что в PL/SQL нет возможности непосредственно задать литераль- литеральное значение даты. Значение ' 01-FEB-2003' - это строковый литерал, то есть по- последовательность символов, заключенная в одинарные кавычки. Такую строку можно конвертировать в дату с помощью PL/SQL или SQL, но внутри базы дан- данных Oracle для значений дат используется только двоичное представление. Строковый литерал может состоять из одного или более символов, входящих в набор символов PL/SQL. Литерал длиной нуль символов представляется как '' (две последовательные одинарные кавычки без символов между ними). В Огас- Ie9i строковый литерал нулевой длины имеет значение NULL1 и тип данных CHAR (строка нулевой длины). В отличие от идентификаторов, строковые литералы в PL/SQL чувствительны к регистру символов. Например, значения следующих двух литералов различны: 'Steven' 'steven' При проверке приведенного ниже условия возвращается значение FALSE: IF 'Steven' - 'steven' Одинарные кавычки внутри строки Иногда возникает необходимость включить одинарную кавычку в литерал как часть литерала. Для этого в месте, где должна стоять кавычка, ставятся два таких символа. Как это делается, показано ниже. Слева приведены различные строко- строковые литералы, а справа — их «внутренние» строковые представления. Пусть вас не смущает несоответствие данного утверждения с документацией Oracle, в которой сказано, что строка нулевой длины — это не то же самое, что значение NULL, как в стандарте ANSI.
Литералы 103 Литерал Значение There"s no business like show business.' There's no business like show business. "Hound of the baskervllles" "Hound of the baskervilles" 'NLS_LANGUAGE»"ENGUSH'" NLS_LANGUAGE='ENGUSH' '"hello"' 'hello' Перечень правил, которыми следует руководствоваться при включении оди- одинарной кавычки в литерал, приведен ниже. О Чтобы включить одинарную кавычку в литерал, поместите на ее место две подряд идущие одинарные кавычки. О Если нужно включить одинарную кавычку в начало или конец литерала, ис- используйте три подряд идущие одинарные кавычки. О Для создания строкового литерала, состоящего из одной одинарной кавычки, поместите подряд четыре одинарные кавычки. О Чтобы создать литерал, состоящий из двух одинарных кавычек, поставьте под- подряд шесть одинарных кавычек. Две одинарные кавычки подряд — это не то же самое, что символ двойной ка- кавычки, который в строковом литерале не имеет специального значения, а интер- интерпретируется так же, как буква или цифра. Числовые литералы Числовые литералы могут быть целыми или действительными (то есть содержа- содержащими дробную часть) числами. Заметьте, что PL/SQL рассматривает число 154,00 как действительное, хотя его дробная часть равняется нулю и с точки зрения ма- математики оно является целым. Целые и действительные числа имеют разное внут- внутреннее представление, и преобразование числа из одной формы в другую требует определенных ресурсов. Задавая числовые литералы, можно использовать также экспоненциальное пред- представление числа. При этом символ «Е» (в верхнем или нижнем регистре) обозна- обозначает умножение числа на 10 в степени п, например, 3.05Е19, 12е—5. Логические (булевы) литералы Для представления логических значений в Oracle определено два литерала: TRUE и FALSE. Это не строки, и их не нужно заключать в кавычки. Они используются для присвоения значений логическим переменным, как в следующем примере: DECLARE enough_money BOOLEAN; -- Объявляем логическую переменную BEGIN enoughjnoney := FALSE: -- Присваиваем ей значение END;
104 Глава 3 ¦ Основы языка При проверке логического значения литерал можно не указывать — тип пере- переменной говорит сам за себя: DECLARE enoughjnoney BOOLEAN; BEGIN IF enoughjnoney THEN Разделитель в виде точки с запятой Программа на PL/SQL представляет собой последовательность объявлений пе- переменных и операторов, границы которых определяются не кодом конца строки, а символом точки с запятой (;). Один оператор часто состоит из нескольких строк. Например, следующий оператор IF занимает четыре строки: IF salary < min_salary B003) THEN salary :- salary + salary *0.25; END IF; В нем вы видите два символа точки с запятой. Первый отмечает конец единст- единственного оператора присваивания в конструкции IF.. .END IF, а второй — конец оператора IF. Данный оператор можно разместить и в одной строке, причем ре- результат будет одинаковым: IF salary < min_salary B003) THEN salary :- salary + salary*0.25: END IF: Каждый оператор также должен завершаться точкой с запятой, даже если они вложены друг в друга. Но если вашей целью не является создание нечитабельного кода, советуем каждый оператор или объявление размещать на отдельной строке. Комментарии Наличие поясняющего и сопроводительного текста в коде (комментариев) явля- является основным признаком хорошей программы. В данной книге приводится мно- множество советов, касающихся того, как сделать программы самодокументируемыми за счет использования продуманных соглашений об именах и модульного подхо- подхода. Однако для понимания сложного программного кода этого еще не достаточно. PL/SQL предлагает разработчикам две разновидности комментариев: одностроч- однострочные и многострочные. Однострочные комментарии Однострочные комментарии начинаются двумя дефисами (--), между которыми не должно быть пробелов или каких-либо других символов. Весь текст после двух дефисов и до конца строки рассматривается как комментарий и игнорируется
Ключевое слово PRAGMA 105 компилятором. Если два дефиса стоят в начале строки, комментарием считается вся строка. Запомните: два дефиса отмечают как комментарий остаток строки в редакторе кода, а не всю инструкцию PL/SQL, после которой они стоят. Например, в сле- следующем операторе IF с помощью однострочного комментария поясняется содер- содержимое условного выражения: IF salary < min_salary B003) -- Функция min_salary возвращает минимальную годовую зарплату THEN salary :- salary + salary *0.25: END IF; Многострочные комментарии Если однострочные комментарии удобны для создания кратких замечаний к фраг- фрагменту кода или для временного исключения строки программы из обработки, то многострочные позволяют включать в программу длинный сопроводительный текст или пояснения. Многострочный комментарий помещается между начальным (/*) и конечным (*/) ограничителями. Весь текст между этими двумя парами символов рассмат- рассматривается как часть комментария и игнорируется компилятором. В следующем примере многострочный комментарий располагается в разделе заголовка процедуры. Слева, для того чтобы зрительно выделить комментарий в программе, создан столбик вертикальных черточек: PROCEDURE calc_revenue (company_id IN NUMBER) IS /* | Пня программы: calc_revenue I Автор.- Стивен Ферстайн | Программа/предназначена для | Для рабо- */ BEGIN 'ы программы требуется следующее аппаратное обеспечение: END: С помощью многострочных комментариев можно также отменить выполне- выполнение блока кода программы на время тестирования, как это сделано в следующем фрагменте: EXIT WHEN a_delimiter (next_char) /* OR (wasadelimiter AND NOT a_delimiter (next char)) */ Ключевое слово PRAGMA Ключевое слово PRAGMA указывает, что остальная часть оператора PL/SQL являет- является директивой компилятора (псевдоинструкцией), которая передает некоторую информацию компилятору и при трансляции не включается в исполняемый код.
106 Глава 3 • Основы языка Директивы компилятора задаются следующим образом: PRAGMA директива: Они могут располагаться в любом месте раздела объявлений. PL/SQL поддержи- поддерживает четыре директивы компилятора, краткое описание которых приведено в сле- следующей таблице. Директивы компилятора Описание AUTONOMOUS_TRANSACnON Предписывает исполнительному ядру PL/SQL выполнить сохранение или откат любых изменений, внесенных в базу данных в текущем блоке, без воздействия на главную транзакцию. Данная директива введена в Orade8l. Дополнительную информацию см. в главе 13 E^tCEPTIONJNrT Указывает компилятору связать конкретный номер ошибки с идентификатором, который объявлен в программе как исключение. Дополнительную информацию см. в главе б RESTRICT_REFERENCES Задает для компилятора уровень чистоты программы пакета (отсутствия в ней действий, вызывающих побочные эффекты). Дополнительную информацию см. в главе 16 SERIALLY_REUSABLE Указывает исполнительному ядру PL/SQL, что данные уровня пакета не должны сохраняться между обращениями к ним. Эта директива введена в OracleSi, Дополнительную информацию см. в главе 17 В следующем блоке демонстрируется, как использовать директиву EXCEPTION_ INIT для указания имени встроенного исключения. Если этого не сделать, исклю- исключение будет иметь только номер. DECLARE no_5uch_sequence EXCEPTION: PRAGMA EXCEPTIONJNIT (no_such_sequence. -2289); BEGIN EXCEPTION WHEN no_such_sequence THEN END: Метки Метка PL/SQL — это способ наименования некоторого фрагмента программы. Она имеет такой формат: «идентифика тор» Здесь идентификатор - это допустимый идентификатор PL/SQL (длиной до 30 символов и начинающийся с буквы), который не сопровождается символом
Метки 107 точки с запятой. Метка располагается непосредственно перед фрагментом кода, имя которого она определяет. Фрагмент кода должен включать исполняемый оператор, им может быть даже оператор NULL: BEGIN «the_spot» NULL: Поскольку анонимный блок представляет собой группу исполняемых опера- операторов, с помощью метки можно задавать имя анонимного блока (на время его вы- выполнения). Это показано в следующем фрагменте кода: «1nsert_but i gnore_dups» BEGIN INSERT INTO catalog VALUES (...); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN NULL; END 1nsert_but_ignore_dups; Блоки часто именуют для улучшения читабельности кода. Давая блоку имя, вы делаете код самодокументированным и наверняка еще раз задумаетесь над тем, что он делает. Это иногда помогает находить ошибки. Метки используют также, чтобы иметь возможность уточнить имя объекта внешнего анонимного блока в случае обращения к нему из внутреннего блока. Пример того, как это делается, приведен ниже: «outerbloclc» DECLARE counter INTEGER :- 0: BEGIN DECLARE counter INTEGER :- 1: BEGIN IF counter - outerblock.counter THEN END IF; END: END outerblock: Без метки блока невозможно различить две переменные с одинаковым име- именем counter (лучше присвоить этим переменным разные имена). Третья функция меток заключается в том, что они могут служить целевыми точками перехода для операторов GOTO. Однако в современных программах опера- операторы GOTO использовать не принято, поскольку схема обработки исключений дей- действует гораздо эффективнее. Во всем коде PL/SQL, который мне когда-либо до- доводилось видеть, я помню только один оператор GOTO.
108 Глава 3 • Основы языка Наиболее значимым является применение меток в качестве целевых точек вы- выхода для операторов EXIT во вложенных циклах: BEGIN «outerj oop» LOOP LOOP EXIT outerjoop: END LOOP; некотрый_опера тор: END LOOP; END; Без метки outerj oop осуществляется выход из внутреннего цикла, после чего выполняется некоторыйjonepamop. Однако с точки зрения алгоритма это не нужно. Последовательность выполнения операторов, которую задает метка в дан- данном случае, нельзя организовать никаким другим способом.
Часть II Структура программы PL/SQL В этой части книги рассматриваются основы программирования и структура про- программных операторов PL/SQJL, в частности описываются условные операторы (IF и CASE), операторы управления потоком (GOTO и NULL), циклы и методы обра- обработки исключений. Изучив данный материал, вы будете знать, как составлять блоки кода, отвечающие сложным требованиям, которые предъявляются к прило- приложениям. ? Глава 4. Условные операторы и переходы ? Глава 5. Циклы ? Глава 6. Обработка исключений
4 Условные операторы и переходы > Операторы IF > Операторы CASE > Оператор GOTO > Оператор NULL В настоящей главе вы познакомитесь с двумя типами управляющих операторов PL/SQL: условными операторами и операторами перехода. Операторы первого типа, присутствующие практически в любом коде, управляют последовательно- последовательностью выполнения программного кода на основе заданных условий. В языке PL/SQL к их числу относятся операторы IF и CASE (оператор CASE был введен только в Oracle9i). Кроме того, существуют так называемые CASE-выражения, со- совмещающие функции операторов IF и CASE. В отличие от условных операторов, операторы безусловного перехода - GOTO и NULL (не выполняют никаких функ- функций) — используются очень редко. Операторы IF В программах очень часто приходится реализовывать выполнение тех или иных операций в зависимости от определенных условий, например: О если оклад находится в пределах от десяти до двадцати тысяч долларов, на- назначить бонус в размере 1500 долларов; О если в пределах от двадцати до сорока тысяч — в размере 1000 долларов; О а если он превышает сорок тысяч долларов - в размере 500 долларов; или: О если пользователь предпочитает применить панель инструментов, вывести ее при открытии окна.
Операторы IF 111 Операторы IF позволяют реализовать в программе условную логику. Как сле- следует из представленной ниже таблицы, они бывают трех видов. Тип оператора IF Описание IF Это простейшая форма оператора IF. Условие, содержащееся между THEN ключевыми словами IF и THEN, определяет, должна ли выполняться группа END IF; операторов, находящаяся между THEN и END if. Если результат проверки условия равен FALSE, то данный код не обрабатывается IF Конструкция реализует логику «или-или». В зависимости от условия, THEN заданного между ключевыми словами IF и THEN, выполняется либо код, ELSE находящийся между THEN и ELSE, либо код между ELSE и END IF. Но END IF; в любом случае реализуется только одна из двух групп исполняемых операторов IF Эта последняя, и самая сложная, форма оператора IF выбирает действие на THEN основе набора взаимоисключающих условий и выполняет соответствующую ELSIF группу исполняемых операторов. Собираясь применить такой оператор ELSE в Огас1е91, подумайте, не воспользоваться ли вместо него оператором END IF; выбора CASE Конструкция IF...THEN Общий синтаксис конструкции IF.. .THEN таков: IF условие THEN ... последовательность исполняемых операторов ... END IF; Здесь условие - это логическая переменная, константа или логическое выра- выражение, результатом проверки которого является возвращаемое значение TRUE, FALSE или NULL. Исполняемые операторы, расположенные между ключевыми словами THEN и END IF, выполняются, если результат проверки условия равен TRUE, и не вы- выполняются - если он равен FALSE или NULL. Следующий условный оператор IF сравнивает два различных числовых значе- значения. Учтите, что если одно из них равно NULL, тогда все выражение возвращает NULL (и в данном случае бонус не назначается): IF salary > 40000 THEN g1ve_bonus(employee id, 500); END IF; Совершенно не обязательно размещать ключевые слова IF, THEN и END IF в от- отдельных строках. Фактически для любого оператора IF количество строк не име- имеет никакого значения. Поэтому приведенный выше пример можно было бы пере- переписать такаким образом. IF salary > 40000 THEN give_bonus(employee_id, 500); END IF; Для столь простого оператора IF так даже удобнее, хотя любой чуть более сложный оператор гораздо лучше читается, когда в нем каждое ключевое слово
112 Глава 4 • Условные операторы и переходы размещается в отдельной строке. Например, довольно трудно разобраться, в на- назначении следующего трехстрочного фрагмента кода: IF salary > 40000 THEN INSERT INTO employee_borms (eb_employee_id. eb_bonus_amt) VALUES (anployeejl 500): UPDATE emp_employee SET emp_bonus_given-l WHERE emp_ employee! d-employeejd; END IF: Никому не захочется тратить время на то, чтобы в этом разобраться. И тот же оператор становится вполне читабельным, если аккуратно разбить его на строки: IF salary > 40000 THEN INSERT INTO employee_bonus (et>_employee_id, eb_bonus_amt) VALUES (employeejd. 500); UPDATE emp_employee SET emp_bonus_given-l WHERE emp_emplo.yee_id-eaiployee id; END IF: Вопрос читабельности становится еще более важным при использовании клю- ключевых слов ELSE и ELSIF, а также вложенных операторов IF. Поэтому, чтобы сде- сделать логику операторов IF максимально наглядной, советуем применять все воз- возможности отступов и форматирования. И те программисты, которым придется сопровождать ваши программы в будущем, будут вам очень признательны. Конструкция IF...THEN...ELSE Конструкция IF.. .THEN.. .ELSE применяется в случаях, когда нужно выбрать одно из двух взаимоисключающих действий. Формат этой версии оператора IF таков: IF условие THEN ... последовательность операторов, выпопняеных в случае, когда результат проверки условия равен TRUE . . . ELSE ... последовательность операторов, выпопняеных в случае, когда результат проверки условия равен FALSE ми NULL ... END IF; Здесь условие - это логическая переменная, константа или логическое выра- выражение. Если его значением является TRUE, выполняются операторы, расположенные между ключевыми словами THEN и ELSE, а если таковым служит FALSE или NULL, то вы- выполняются операторы, расположенные между ключевыми словами ELSE и END IF. ПРИМЕЧАНИЕ Обратите внимание, что после ELSE ключевое слово THEN не используется. Важно помнить, что в конструкции IF.. .THEN.. .ELSE всегда выполняется одна из двух возможных последовательностей операторов. После выполнения соот- соответствующей последовательности операторов управление передается тому из них, который расположен сразу после ключевых слов END IF. Вот пример конструкции IF.. .THEN.. .ELSE, соответствующей последнему из при- приведенных в начале главы условий назначения бонуса: IF salary <- 40000 THEN
Операторы IF 113 give_bonus(employee 1d. 0): -- не назначать бонус ELSE give_bonus(employee_id. 500): -- назначить бонус 500 долларов END IF;" В этом примере сотрудникам с размерам оклада более 40 000 долларов назнача- назначается бонус в 500 долларов, а всем остальным он не назначается вообще. Или все же назначается? Что произойдет, если у одного из сотрудников оклад по какой-то причине окажется равным NULL? В этом случае будут выполнены операторы, сле- следующие за ключевым словом ELSE, и указанный сотрудник получит бонус, пола- полагающийся только высокооплачиваемым сотрудникам. Но ведь это не правильно! Поскольку мы не можем быть уверены, что оклад никогда не окажется равным NULL, нужно предусмотреть такую ситуацию, воспользовавшись функцией NVL; IF NVL(salary.O) <- 40000 THEN give_bonus(employee_id, 0): -- не назначать бонус ELSE give_borms(employeejd, 500): -¦ назначить бонус в 500 долларов END IF; Теперь, если значение переменной salary равно NULL, функция NVL возвращает О, и в этом случае бонус не назначается. ИСПОЛЬЗОВАНИЕ ЛОГИЧЕСКИХ ФЛАГОВ Логические переменные удобно использовать в качестве флагов. Это из- избавляет от необходимости вычислять одно и то же логическое выражение по нескольку раз. Помните, что результат такого выражения можно при- присвоить значению логической переменной. Например, вместо того чтобы писать: IF : customer, ordertotal > max_allowable_order THEN order_exceeds_ballance :- TRUE; ELSE order_exceeds_ballance :- FALSE: END IF; можно воспользоваться гораздо более простым выражением: orderexceedsball ance :- : customer, ordertotal > max_allowable_order: Теперь если где-либо в программном коде потребуется проверить, не пре- превышает ли заказ (order_total) максимально допустимое значение (maxal - 1 owabl e_order), достаточно будет такой простой и понятной конструкции: IF order_exceeds_bal1ance THEN Если вам еще не приходилось работать с логическими переменными, воз- возможно, на освоение этой техники уйдет некоторое время. Но затраты оку- окупятся сполна, поскольку в результате вы получите более простой и чита- читабельный код.
114 Глава 4 • Условные операторы и переходы Конструкция IF...THEN...ELSIF Данная форма оператора IF полезна в тех случаях, когда реализуется логика со многими альтернативными действиями; это уже не ситуация «или-или». Вот как записывается ряд взаимоисключающих альтернатив: IF условие 1 THEN операторы_1 ELSIF усповие_и операторы И [ELSE операторы else] END IF; ВНИМАНИЕ Используя ключевое слово ELSIF, будьте внимательны, чтобы не написать ELSEIF или ELSE IF. Это очень распространенная синтаксическая ошибка. ЗНАЧЕНИЯ NULL В ОПЕРАТОРЕ IF Следует очень четко понимать, как в операторе IF обрабатываются значе- значения NULL. Одно значение NULL в логическом выражении обычно превраща- превращает результат всего выражения в NULL, а это не TRUE и не FALSE. Например, два очень похожих оператора дают разные результаты: if х - г THEN DBMS OUTPUT.PUT LINECзначение переменной x равно 2'); ELSE DBMS_OUTPUT.PUT_LINECзначение переменной х не равно 2'); END IF: IF x о 2 THEN DBMS_OUTPUT.PUT_LINECзначение переменной х не равно 2'); ELSE DBMS_OUTPUT.PUT_LINE('значение переменной х разно 2'); END IF; Различие между ними связано с тем, что происходит, когда переменная х содержит значение NULL. В данном случае первый оператор IF.. .THEN... ELSE выводит строку «значение переменной х не равно 2», а второй — строку «значение переменной х равно 2». Вы спросите, почему? А потому, что ко- когда значением переменной х является NULL, значение проверяемого опера- оператором IF выражения не равно TRUE и управление передается ключевому слову ELSE. Защититься от случайных ошибок, связанных с обработкой значения NULL, можно либо за счет использования таких функций, как NVL, либо путем явной проверки выражения на равенство значению NULL с помощью пре- предиката IS NULL.
\ Операторы IF 115 Конструкция IF. .THEN...ELSIF представляет собой один из способов реализа- реализации функций оператора CASE. И если вы используете Oracle9i, имеет смысл при- применить оператор CASE, о котором рассказывается далее в этой главе. В каждом предложении ELSIF за условием должно следовать ключевое слово THEN, а наличие ELSE не является обязательным. Последнее ключевое слово, если оно вообще используется, ставится в конце конструкции IF.. .THEN... ELSIF и озна- означает «если неверно все вышеперечисленное». То есть когда ни одно из условий не равно TRUE, выполняются операторы, следующие за ELSE. Если же ключевое слово ELSE отсутствует, все исполняемые блоки конструкции IF.. .THEN.. .ELSIF игнорируются. Теперь приведем полную реализацию логики назначения бонуса, описанной в начале главы, с использованием конструкции IF... THEN... ELSIF: IF salary BETWEEN 10000 AND ZOOOO THEN g1ve_bonus tempioyeej d. 1500): ELSIF salary BETWEEN 200D0 AND 40000 THEN give_bonus(employeejd. 1A00); ELSIF salary > 40000 THEN give_bonus(employee 1d, 500); ELSE g1ve_bonus(employee 1d, 0); END IF: Условия в конструкции IF... ELSIF всегда выполняются в порядке их следова- следования в операторе. Если при проверке обоих условий возвращается TRUE, обрабаты- • ваются операторы, соответствующие первому из этих условий. В приведенном примере при окладе 20 000 долларов назначается бонус в 1500 долларов, хотя этот оклад удовлетворяет и второму условию, согласно которому бонус составля- составляет 1000 долларов (поскольку в диапазон, задаваемый в операторе BETWEEN, входят и границы этого диапазона). Как только в результате проверки одного из условий возвращается значение TRUE, последующие условия не проверяются вообще. ПРИМЕЧАНИЕ Оператор CASE, введенный в Orade9l, позволяет более эффективно запрограммировать пример с назначением бонуса, в чем вы сможете убедиться несколько позже. Хотя в конструкции IF.. .THEN.. .ELSIF и разрешается задавать условия, для ко- которых результаты проверки частично пересекаются, лучше этого не делать. В на- нашем случае ситуация неоднозначна, поскольку в коде недостаточно четко указано, что делать компилятору, когда оклад составляет 20 000 долларов. И если пола- полагать, что мы хотим назначить более высокий бонус низкооплачиваемым сотруд- сотрудникам (что кажется вполне обоснованным), лучше отказаться от оператора BET- BETWEEN и использовать логику «больше-меньше»: IF salary >- 10000 AND salary <- 20000 THEN give_bonus(employee id, 1500): ELSIF salary > 20000 AND salary <- 4000Q THEN give_bonus(employee_id. 1000): ELSIF salary > 40000
116 Глава 4 • Условные операторы и переходы THEN give_bonus(anployee_id. 500): ELSE g1ve_bonus(etnployee_id. 0): END IF: Отказавшись от использования условий, для которых результаты проверки являются частично пересекающимися, мы тем самым устранили неясность, с ко- которой пришлось бы столкнуться тем программистам, в чьи руки в будущем попала бы эта программа. А еще мы устранили вероятность случайных ошибок, которые могли бы произойти из-за того, что кто-то поменял бы местами предложения ELSIF. ВНИМАНИЕ Язык PL/SQL не требует, чтобы условия выполнения предложений ELSIF были взаимоисключающи- взаимоисключающими. Однако вам следует всегда помнить, что в том случае, если одно проверяемое значение удовле- удовлетворяет двум или более условиям, необходимо быть особенно внимательным и записать предложе- предложения ELSIF в правильном порядке. Вложенные операторы IF Любые операторы IF можно вкладывать друг в друга. Например, следующая мно- многоуровневая конструкция состоит из нескольких вложенных операторов IF: IF ycnoamj. THEN IF усповие_2 THEN операторы_2 ELSE IF у'словне_3 THEN операторы_3 ELSIF ycnoanej THEN операторы4 END IF; END IF; END IF; Сложную логику часто невозможно реализовать без вложенных операторов IF, но при их использовании следует быть очень внимательным. Как и вложенные циклы, они трудны для понимания и отладки. И если вы соберетесь применить операторы IF более чем на трех уровнях вложения, подумайте, нельзя ли пере- пересмотреть логику программы и реализовать поставленные условия более простым способом. Если эта задача окажется очень сложной, подумайте о создании одного или нескольких локальных модулей, скрывающих внутренние операторы IF. Главным преимуществом вложенных структур IF считается то, что они позво- позволяют отложить проверку внутренних условий. Условие внутреннего оператора IF проверяется только в том случае, если значение выражения во внешнем условии оказывается равным TRUE. Таким образом, очевидной целью вкладывания опера- операторов IF является проверка одного условия только в том случае, если истинно другое. Например, наш код для вычисления бонуса можно было бы дополнить та- таким фрагментом: IF award_bonus(employee_1d) THEN IF print_check(employeejd) THEN
Оператор CASE 11/ DBMS_0UTPLrr.PUT_LINE('4eK аыдан аля' || employee_id); END IF; END IF: Предположим, мы хотим выводить сообщение для каждого напечатанного че- чека на бонус, но при этом чек для нулевого значения, соответствующего не назна- назначенному бонусу, не должен печататься. Вложенный оператор IF может понадобиться в ситуации, когда проверка ус- условия является очень дорогостоящей (требуются дополнительные ресурсы памяти и время на его обработку процессором). Фрагмент кода, осуществляющий про- проверку, можно поместить во внутренний оператор IF — в таком случае данная опе- операция действительно будет выполняться лишь при необходимости. Особенно это важно для кода, встречающегося очень часто или располагающегося в тех облас- областях программы, для которых важна скорость получения ответа, Эту концепцию иллюстрирует следующий оператор IF: IF усповив_1 AND условие? THEN END IF: Для того чтобы определить, истинны ли оба условия оператора IF, компиля- компилятор PL/SQL проверяет их по очереди. Предположим, что условие_2 проверяется достаточно быстро: totalsales > 100000 Однако усповие_1 более сложное, и для его проверки процессору требуется мно- много времени, так как в нем, возможно, вызывается хранимая функция, выполняю- выполняющая запрос к базе данных. Если условие_2 проверяется за десятую долю секунды и результатом проверки оказывается значение FALSE, гусловие_1 проверяется в те- течение 3 с, а результатом является значение TRUE, тогда на выяснение того факта, что код в теле оператора IF выполнять не нужно, уходит более 3 с. Предположим теперь, что тот же оператор IF переписан следующим образом: IF условие 2 THEN IF условие_1 THEN END IF: END IF: Теперь услоаие_1 проверяется только в том случае, если результатом проверки условия_2 является значение TRUE. И в тех случаях, когда истинно условие to- totalsales <= 100000, пользователю не приходится ждать 3 с, пока программа про- продолжит свою работу. КАК ИЗБЕЖАТЬ СИНТАКСИЧЕСКИХ ОШИБОК Приведем несколько советов, касающихся применения оператора IF. ? Не забывайте использовать ключевые слова END IF. Ими обязатель- обязательно должны завершаться все три разновидности данного оператора. продолжение^
118 Глава 4 • Условные операторы и переходы Q Не забывайте ставить пробел между ключевыми словами END и IF. Если вместо END IF ввести ENDIF, компилятором будет выведено сооб- сообщение об ошибке: ORA-06550: Иге 14, column 4: PLS-00103: Encountered the symbol ";" when expecting one of the following: ? Ключевое слово ELSIF не должно содержать букву Е. Если вместо ключевого слова ELSIF указать ELSEIF, компилятор не воспримет по- последнее как часть оператора IF. Он интерпретирует его как имя пере- переменной или процедуры. ? Точку с запятой следует ставить только после ключевых слов END IF. После ключевых слов THEN, ELSE и ELSEIF, которые не являются отдель- отдельными исполняемыми операторами и, в отличие от END IF, не могут за- завершать оператор PL/SQL, указанный символ никогда не вводится. Но если вы все же поставите точку с запятой после одного из перечис- перечисленных ключевых слов, компилятор выдаст сообщение об ошибке, указывающее, что он ожидал увидеть перед точкой с запятой какой- нибудь оператор. Оператор CASE Операторы CASE, впервые появившиеся в Oracle9i, позволяют выбрать для выпол- выполнения одну из нескольких последовательностей исполняемых операторов. Сама по себе эта конструкция не нова, она давно и успешно используется в других язы- языках программирования. Более того, в 1992 году оператор CASE был описан в стан- стандарте языка SQL, хотя Oracle SQL не поддерживал его вплоть до версии Oracle8z, a PL/SQL - до версии Огас1е9г. Начиная с Oracle9i поддерживаются два типа операторов CASE: О простой оператор CASE — связывает одну или несколько последовательностей операторов PL/SQL с соответствующим значением (последовательность для выполнения выбирается с учетом результатов вычисления выражения, воз- возвращающего указанное значение); О поисковый оператор CASE — выбирает для выполнения одну из последователь- последовательностей операторов в зависимости от результатов вычисления списка логиче- логических условий (выполняется последовательность операторов, связанная с пер- первым условием, результат проверки которого оказался равным TRUE). В дополнение к операторам CASE PL/SQL поддерживает CASE-выражения. Вы- Выражение CASE очень похоже на оператор CASE, оно позволяет выбрать для вычисле- вычисления одно или несколько выражений. В итоге получается одно значение, тогда как результатом работы оператора CASE является выполнение последовательности операторов PL/SQL.
Оператор CASE 119 Простые операторы CASE Простой оператор CASE позволяет выбрать для выполнения одну из нескольких последовательностей операторов PL/SQL в зависимости от результата вычисле- вычисления одного выражения. Он имеет следующую форму записи: CASE выражение WHEN результат! THEN операторыJ WHEN результатJ THEN операторы_2 ELSE операторы else END CASE:" Ветвь ELSE здесь не обязательна. При выполнении такого оператора PL/SQ_L сначала вычисляет выражение. Затем результат сравнивается со значением ре- результат_1. Если они совпадают, то выполняются операторы_1, в противном случае проверяется значение результату и т. д. Приведем пример простого оператора CASE, в котором в качестве основы для выбора подходящего алгоритма вычисления бонуса используется значение пере- переменной employeejtype: CASE employee type WHEN 'S' THEN" award_5alary_bonus(employee_id); WHEN 'H' THEN award hourly_bonus(employee_id); WHEN TC THEN award_coimvlssioned_bonus(employee 1d); ELSE RAISE invalid employee type: END CASE; В этом операторе CASE имеется явно заданное ключевое слово ELS?. Однако в общем случае оно не является обязательным. Если его нет, компилятор PL/SQL неявно выполняет такой код: ELSE RAISE CASE_NOT_FOUNO; Иными словами, если не задать ключевое слово ELSE и если никакой из резуль- результатов в предложениях WHEN не соответствует результату выражения в операторе CASE, PL/SQL инициирует исключение CASE_NOT_FOUND. В этом и заключается от- отличие данного оператора от предыдущего. Когда в операторе IF отсутствует клю- ключевое слово ELSE, то при невыполнении условия ничего не происходит, тогда как в операторе CASE аналогичная ситуация приводит к ошибке. Теперь вы, надо полагать, хотите узнать, как с помощью оператора CASE реали- реализовать описанную в начале главы логику назначения бонуса. На первый взгляд это кажется невозможным. Однако подойдя к делу творчески, можно написать вот такой код: CASE TRUE WHEN salary >- 10000 AND salary <- 20000 THEN give_bonus(employee_id, 1500):
120 Глава 4 • Условные операторы и переходы WHEN salary > 20000 AND salary <- 40000 THEN give_bonus(employee_id. 1000); WHEN salary > 40000 THEN give_bonus(employee_i d. 500): ELSE gi ve_bonus Cerapl oyee_i d. 0): END CASE; На примере этого решения покажем (помимо того, что программирование ино- иногда требует творческого подхода), что элементы выражение и результат могут быть либо скалярными значениями, либо выражениями, результатами которых явля- являются скалярные значения. Вернувшись к оператору IF.. .THEN...ELSIF, реализующему ту же логику, вы увидите, что в оператор CASE мы ввели ключевое слово ELSE, тогда как в операторе IF.. .THEN.. .ELSIF его не было. Причина добавления ELSE проста: если ни одно из условий назначения бонуса не выполняется, оператор IF просто ничего не делает и бонус получается нулевым. Оператор же CASE в этом случае генерирует ошибку, и поэтому ситуацию, когда бонус является нулевым, приходится кодировать явно. Возможно, использование приведенного выше оператора CASE TRUE кажется вам замечательным нестандартным решением, но на самом деле он всего лишь явно реализует поисковый оператор CASE, о котором мы поговорим в следующем разделе. Поисковый оператор CASE Как вы, возможно, догадываетесь, поисковый оператор CASE (searched CASE) про- проверяет набор логических выражений и, обнаружив выражение, равное TRUE, вы- выполняет последовательность связанных с ним операторов. По сути дела, это экви- эквивалент оператора CASE TRUE, пример которого приведен в предыдущем разделе. Поисковый оператор CASE имеет следующую форму записи: CASE WHEN аыражение_1 THEN операторы_1 WHEN выраженис_2 THEN операторы 2 ELSE oneparopuelse END CASE: Он прекрасно подходит для реализации логики назначения бонуса: CASE WHEN salary >= 10000 AND salary <= 20000 THEN give_bonus(employee_id, 1500); WHEN salary > 20000 AND salary <- 40000 THEN give_bonus(anployee_id, 1000): WHEN salary > 40000 THEN give_bonus(employee_id, 500): ELSE give_bonus(employee_id, 0); END CASE;
Оператор CASE 121 Поисковый оператор CASE, равно как и простой оператор с этим же именем, действует в соответствии со следующими правилами. О Работа оператора заканчивается сразу же после выполнения последователь- последовательности исполняемых операторов, связанных с истинным выражением. Если ис- истинными оказываются несколько выражений, выполняются операторы, свя- связанные с первым из них. О Ключевое слово ELSE не обязательно. Если оно не задано и ни одно из выраже- выражений не оказалось равным TRUE, инициируется исключение CASE_NOT_FOUND. О Условия WHEN проверяются по порядку, сверху вниз. Рассмотрим еще одну реализацию логики назначения бонуса, в которой ис- используется то обстоятельство, что условия WHEN проверяются в порядке их записи. Отдельные выражения проще, но этот оператор не так нагляден, как предыдущий: CASE WHEN salary > 40000 THEN give_bonus(employee_id. 500): WHEN salary > 20000 THEN give_bonus(employee_id. 1000): WHEN salary >= 10000 THEN gi ve_bonus(employeeji d, 1500); ELSE g1ve_bonus(employee_id. 0): END CASE: Если оклад некоего сотрудника равен 20 000 долларов, результатом проверки первых двух условий будет FALSE, а третьего условия — TRUE, и сотрудник получит бонус в 1500 долларов. Если же его оклад равен 21 000 долларов, результат второ- второго условия будет вычислен как TRUE и сотрудник получит бонус в 1000 долларов. Выполнение оператора CASE будет завершено на второй ветви WHEN, а третье усло- условие даже не будет проверяться. Стоит ли использовать такой подход при написании операторов CASE — вопрос спорный. Как бы то ни было, имейте в виду, что вам могут встречаться такие опе- операторы, в которых результат зависит от порядка следования выражений, и при их отладке и модификадии требуется особая внимательность. Логика, зависящая от порядка следования однородных ветвей WHEN в операто- операторе, является потенциальным источником ошибок, возникающих при попытке по- поменять их местами. В качестве примера рассмотрим следующий оператор CASE, в котором при сумме оклада 20 000 долларов результат проверки условий в обеих ветвях WHEN оказывается равным TRUE: CASE WHEN salary BETWEEN 10000 AND 20000 THEN give_bonus(employee_id, 1500); WHEN salary BETWEEN 20000 AND salary 40000 THEN . g1ve_bonus(employee_id. 1000): Подумайте, что произойдет, если программист, которому поручено сопровож- сопровождать эту программу, решит переставить ветви WHEN, чтобы упорядочить их по убы- убыванию окладов. Не стоит полагать, что это маловероятно. Программисты часто
122 Глава 4 • Условные операторы и переходы склонны к совершенствованию уже работающего кода в соответствии с собствен- собственным представлением о порядке. Вот как может быть переписан приведенный выше код: CASE WHEN salary BETWEEN 20000 AND salary 40000 THEN give_bonus(employee_1d. 1000): WHEN salary BETWEEN 10000 AND 20000 THEN give_bonus(employeeHd, 1500): На первый взгляд все верно, не так ли? Отнюдь, поскольку из-за пересечения двух ветвей WHEN на значении 20 000 получается небольшая ошибка в програм- программной логике. Теперь сотрудник с таким окладом вместо бонуса в 1500 долларов получит только 1000. Вам понятно, почему следует избегать пересечения условий в ветвях WHEN и по- почему лучше сдерживать желание усовершенствовать уже работающий код. Если же вам придется вносить изменения в чужой программный код, всегда помните, что порядок логических выражений может иметь значение. ВНИМАНИЕ Поскольку условия в ветвях WHEN проверяются по порядку, можно немного повысить эффектив- эффективность кода, поместив ветви с наиболее вероятными условиями в начало списка. Кроме того, если у вас есть ветвь WHEN с «дорогостоящими» выражениями (например, требующими много времени на обработку процессором или больших ресурсов памяти), их можно поместить в конец, минимизиро- минимизировав таким образом вероятность их проверки. Поисковые операторы CASE используются в тех случаях, когда для поиска под- подлежащего выполнению набора операторов применяется набор логических выра- выражений. А простой оператор CASE имеет смысл использовать тогда, когда решение принимается на основании результата одного выражения. Вложенные операторы CASE Подобно операторам IF, операторы CASE могут быть вложенными. Например вло- вложенный оператор CASE включает следующая не слишком наглядная реализация логики назначения бонуса: CASE WHEN salary >- 10000 THEN CASE WHEN salary <- 20000 THEN give_bonus(employeejd. 1500): WHEN salary > 20000 THEN give_bonus(employee_id, 1000): END CASE; WHEN salary > 40000 THEN give_bonus tempi oyeeid. 500): WHEN salary < 10000 THEN gi ve_bonus(employee_i d. 0): END CASE:
Оператор CASE 123 Внутри оператора CASE могут использоваться любые операторы, так что внут- внутренний оператор CASE нетрудно заменить оператором IF. Точно так же любой опе- оператор, включая CASE, может быть вложен в оператор IF. Выражения CASE Выражения CASE выполняют ту же задачу, что и операторы CASE, но только не для исполняемых операторов, а для выражений. Простое выражение CASE позволяет выбрать для вычисления одно из нескольких выражений на основе заданного ска- скалярного значения. Поисковое выражение CASE последовательно вычисляет выра- выражения из заданного списка, пока одно из них не окажется равным TRUE, а затем возвращает результат связанного с ним выражения. Приведем синтаксис этих двух видов выражения CASE: Простое_выражеиие_Са5е :- CASE выражение WHEN результат_1 THEN результирующее вырамение 1 WHEN результатJ THEN ре зуль тирующее_выражение_2 "else результирующее выражение else END; Поисковое_аыражение Case :- CASE WHEN выражениеJ THEN резупьтирующее_выражение_1 WHEN выражениеJ THEN результ»рующее_выражение_2 ELSE результирующее выражение else END; Выражение CASE возвращает одно значение - результат выбранного для вы- вычисления выражения. Каждой ветви WHEN должно быть поставлено в соответствие одно результирующее выражение (но не оператор). В конце выражения CASE не нужно ставить ни точку с запятой, ни ключевые слова END CASE. Выражение CASE оканчивается единственным оператором END. Ниже дан пример простого выражения CASE, используемого совместно с про- процедурой PUT_LINE пакета DBMS_OUTPUT для вывода на экран значения логической пе- переменной. В этом примере выражение CASE преобразует логическое значение в символьную строку, которая затем выводится с помощью процедуры PUT_LINE: DECLARE booieanjtrue BOOLEAN :- TRUE; booleanfalse BOOLEAN :- FALSE: boolearwiull BOOLEAN; FUNCTION boolean_to_varchar2 (flag IN BOOLEAN) RETURN VARCHAR2 IS 8EGIN RETURN
124 Глава 4 • Условные операторы и переходы CASE flag WHEN TRUE THEN 'True' WHEN FALSE THEN 'False' ELSE 'NULL' END; END: BEGIN DBMS_0UTPUT.PUT_LINECboolean_to_varch3r2(boolean_true)): DBMS_OUTPUT.PUT_LINE(boolean_to_varchar2(boolean_false5): DBMS_OUTPUT.PUT_LINE(boolean_to_varcharZtboolean_nunM: END; Для реализации логики назначения бонуса можно использовать поисковое вы- выражение CASE, возвращающее значение бонуса для заданного оклада: DECLARE salary NUMBER :- 20000; enployeejid NUMBER :- 36325: PROCEDURE give_bonus (empjid IN NUMBER. bonus_amt IN NUMBER) IS BEGIN DBMS_OUTPUT.PUT_LINE(emp_id): DBMS_OUTPUT.PUT_LINECbonus_amt): END; BEGIN give_bonus(employee_1d. CASE WHEN salary >- 10000 AND salary <=20000 THEN 1500 WHEN salary > 20000 AND salary <- 40000 THEN 1000 WHEN salary > 40000 THEN 500 ELSE 0 END): END: Выражение CASE можно применить везде, где допускается использование вы- выражений любого другого типа. В следующем примере CASE-выражение использует- используется для вычисления бонуса, умножения его на 10 и присвоения результата пере- переменной, значение которой выводится на экран с помощью функции PUTLINE пакета DBMS_OUTPU: DECLARE salary NUMBER := 20000; employeejd NUMBER := 36325; bonjs_amount NUMBER; BEGIN bonus_amount := CASE ¦ • WHEN salary =~ 10000 AND salary <-20000 THEN 1500 WHEN salary > 20000 AND salary <- 40000 THEN 1000 WHEN salary > 40000 THEN 500 ELSE 0 END * 10; OBMS_OUTPUT.PUT_LINE(bonus_amount); END: В отличие от оператора CASE, в том случае, если ни одна из ветвей WHEN не вы- выбрана, выражение CASE не генерирует ошибку, а просто возвращает NULL.
Оператор GOTO 125 Оператор GOTO В языке PL/SQL имеется несколько специальных операторов, управляющих по- последовательностью выполнения основных операторов программы. В частности, операторы IF и CASE выбирают выполняемую часть кода на основе проверки задан- заданных условий; оператор LOOP (описанный в главе 5) используется для многократ- многократного выполнения фрагмента кода. В дополнение к этим строго структурированным методам управления последовательностью выполнения операторов программы PL/SQL предлагает программистам оператор GOTO. Этот оператор выполняет без- безусловный переход к другому исполняемому оператору в том же исполняемом разделе блока PL/SQL. Как и другие конструкции языка, он требует внимания и продуманного подхода. Общий формат оператора GOTO таков: GOTO иня_нетнч: Здесь имя_метки — это имя метки, идентифицирующей целевой оператор перехо- перехода. В программе данная метка выглядит так: «иня метки» ВЫРАЖЕНИЯ CASE В SQL-ИНСТРУКЦИЯХ Еще до того как выражения и операторы CASE появились в PL/SQL, они стали использоваться в SQL-инструкциях в виде поисковых выражений CASE, поддержка которых была введена в Oracle8i. К сожалению, и к боль- большому недовольству программистов, в SQL-инструкциях, вызываемых из кода PL/SQL, CASE-выражения использовать не разрешалось. Например, в SQL*Plus можно было выполнить такую SQL-инструкцию: SELECT CASE WHEN DUMMY-'X1 THEN -'Dual is OK' ELSE 'Dual 1s missed up' ' END FROM DUAL: Однако следующий код PL/SQL не работал: DECLARE dualjtessage VARCHAR2B0): BEGIN SELECT CASE WHEN DUMMY-'X1 THEN 'Dual is OK' ELSE 'Dual is missed up' END INTO dualjiessage FROM DUAL: DBMS_OUTPUT.PUT_LINEtdual_message): END: В Oracle9i такая ситуация больше не возникает. Теперь поддерживаются поисковые и простые выражения CASE и их можно использовать в SQL, в PL/SQL, а также в SQL-инструкциях, вызываемых из PL/SQL.
126 Глава 4 • Условные операторы и переходы Имя метки должно быть заключено в двойные угловые скобки (« »). Когда компилятор PL/SQL встречает оператор GOTO, он немедленно передает управле- управление первому исполняемому оператору, следующему за указанной меткой. Приве- Приведем полный блок кода, содержащий и метку, и оператор GOTO: BEGIN GOTO second_output: DBMS_0UTPUT.PUTJ_INE('3Ta строка никогда не будет выполнена'): «second_output» DBMS OUTPUT.PUT LINECMm здесь!'); END; "" " Несмотря на стойкое предубеждение против использования оператора GOTO, иногда он может быть очень полезен. Например, в некоторых случаях он спосо- способен упростить логику программы. С другой стороны, поскольку PL/SQL включает так много разнообразных управляющих конструкций и средств обеспечения мо- модульности программы, практически всегда найдется более удобный способ реше- решения задачи, чем использование оператора GOTO. Ограничения на использование На использование оператора GOTO налагается несколько ограничений, которые под- подробно описываются в следующих разделах. О За меткой должен следовать хотя бы один исполняемый оператор. О Целевая метка должна находиться в пределах области действия оператора GOTO. О Целевая метка должна находиться в той же части блока PL/SQL, что и опера- оператор GOTO. За меткой должен следовать хотя бы один исполняемый оператор Метка сама по себе не является исполняемым оператором (обратите внимание, что за угловыми скобками не ставится точка с запятой) и поэтому не может его заменять. В следующем фрагменте кода все ссылки на метку «al1_done» недо- недопустимы, поскольку за ней не следует ни один исполняемый оператор: IF statusjnout - 'COMPLETED' THEN «all done» /* Недопустим! */ ELSE schedule_act1v1ty; END IF; DECLARE CURSOR company cur IS ...; BEGIN FOR company_rec IN company cur LOOP apply_bonuses (company_rec,ccfflpany_1d); «all_done» /* Недопустимо! */ END LOOP; END;
Оператор GOTO 127 FUNCTION new_formula (moleculejn IN NUMBER) RETURN VARCHAR2 IS BEGIN .,. запрограммируйте соответствующую формулу ... RETURN fonnula_str1ng: «all_done» /* Недопустимо! */ END: Такие ключевые слова, как END, THEN, ELSE, ELSIF, END LOOP, не являются испол- исполняемыми операторами, и непосредственно перед ними метка ставиться не может. Целевая-метка должна находиться в пределах области действия оператора GOTO Область действия оператора GOTO определяется той из перечисленных ниже кон- конструкций, в которой он находится: О функция; О процедура; О анонимный блок; О оператор IF; О оператор LOOP; О обработчик исключения; О оператор CASE. Каждый из приведенных далее примеров программного кода содержит ошибку, которую можно отнести к числу наиболее распространенных. Как видите, в каж- каждом случае делается попытка перехода к метке, которая лежит вне области дейст- действия оператора GOTO, в результате чего генерируется ошибка PL/SQ.L: PLS-00375: Illegal GOTO statement: this GOTO cannot branch to label О Условие IF. Единственный способ войти в оператор IF - обеспечить выпол- выполнение его условия (когда результат проверки условия равен TRUE). Поэтому после обработки следующего кода будет сгенерировано сообщение об ошибке: GOTO label Inside 1f: IF status : 'NEW'" THEN «Iabel_1ns1dejf» /* Вне области действия! */ show newjwe; END IF;" Точно так же нельзя выполнить переход в оператор CASE. О Операторы BEGIN. Единственный способ войти во вложенный блок - прой- пройти через его оператор BEGIN. Синтаксис PL/SQL предусматривает соблюдение строгой последовательности входа и выхода. Поэтому после выполнения сле- следующего кода также будет сгенерировано сообщение об ошибке: GOTO label Inside subblock; BEGIN «Iabel_1ns1de_subblock» /• Выход за границы блока! */ NULL: END:
128 Глава 4 • Условные операторы и переходы О Область действия оператора IF. В операторе IF область действия операторов ограничивается соответствующим блоком, начинающимся с ключевого слова WHEN. Оператор GOTO не может передавать управление из одного блока в другой. Поэтому следующий код некорректен: GOTO 1abe"i_inside_if: IF status - 'NEW' THEN «new_status» GOTO old_status; /* Выход за границы блока! */ ELSIF status - 'OLD' THEN «old_status» GOTO new_status: /* Выход за границы блока! */ END IF: Аналогичным образом, невозможно перейти из одной ветви оператора CASE в другую. Ниже приведены рекомендации относительно того, как можно из- избежать таких ошибок. О Не производите переход в тело цикла. С помощью оператора GOTO нельзя пе- перейти в тело цикла. Поэтому следующий код ошибочен: FOR month_num IN 1 .. 12 LOOP «do_a_month» schedule_activity(montli_num); END LOOP; GOTO do_a_month; /* Невозможно снова войти в цикл.*/ О Не переходите в локальный модуль. Не разрешается переход с помощью опе- оператора GOTO из главного блока к метке в функции, процедуре или в другом мо- модуле. Поэтому приведенный ниже код сгенерирует ошибку: DECLARE FUNCTION localjiull IS BEGIN «decri p_case_statement» NULL; END; BEGIN GOTO decrip_case_statement: /* Метка здесь невидима. */ END: Целевая метка должна находиться в той же части блока PL/SQL, что и оператор GOTO Оператор GOTO из исполняемого раздела не может выполнить переход в раздел ис- исключений, а оператор GOTO из раздела исключений не может выполнить переход в исполняемый раздел. Оператор GOTO в разделе исключений должен ссылаться на метку, расположенную в этом же разделе исключений. Следующий код генерирует ту же ошибку PL/SQL (PLS-00375), что и примеры кода из предыдущего раздела: BEGIN /* || Метка и оператор GOTO должны находиться в одном разделе! */ GOTO out of here;
Оператор NULL 129 EXCEPTION WHEN OTHERS THEN «out_of_here» /* Вне области видимости! */ NULL: END: Оператор NULL Каждый оператор программы, как правило, выполняет определенное действие. Однако бывают случаи, когда нужно просто указать компилятору PL/SQL, что- чтобы он ничего не делал, и здесь на помощь приходит оператор NULL. Он имеет сле- следующий формат записи: NULL; Вы ведь не ожидали, что оператор, который «ничего не делает», будет иметь сложную структуру? Имя оператора NULL является зарезервированным словом PL/SQL, за которым следует точка с запятой (;), указывающая, что это оператор, а не значение NULL. Единственное выполняемое оператором NULL действие — пере- передача управления очередному исполняемому оператору. А о том, для чего может понадобиться такой оператор, вы узнаете из следующих разделов. Повышение читабельности программы Случается, что в определенной логической ситуации программа вообще не про- производит никаких действий. В большинстве таких случаев PL/SQL позволяет во- вообще не использовать операторы, тем не менее программа выполняется правильно. Однако у человека, который читает такую программу, происходящее может вы- вызвать определенные сомнения, поэтому ему лучше объяснить, что все идет по плану. Например, в операторе IF не обязательно должно присутствовать ключевое слово ELSE. В частности, оно отсутствует в следующем коде, который генерирует указанный пользователем отчет: IF :report.selection - 'DETAIL' THEN exec_detail_report: END IF; Но что должна делать программа, если указан другой отчет, а не DETAIL? Мож- Можно предположить, что ничего. Но поскольку в коде это явно не задано, кто-то мо- может подумать, что вы просто забыли запрограммировать соответствующее дейст- действие. Если же внести в эту программу ключевое слово ELSE, которое не должно выполнять какие-либо функции, это будет означать: «Не волнуйтесь, я действи- действительно хочу, чтобы в этом месте не производились никакие операции»: IF :report.selection = 'DETAIL' THEN exec detail report: ELSE NULL: -- Ничего не делать END IF;
130 Глава 4 • Условные операторы и переходы Обработка исключений с помощью оператора NULL В программе может присутствовать необязательный раздел исключений, содер- содержащий один или несколько обработчиков исключений. Эти обработчики обраба- обрабатывают ошибки, генерируемые при выполнении программы. Структура раздела исключений и последовательность его обработки очень похожи на структуру и последовательность обработки условного оператора CASE: EXCEPTION WHEN имя исилючения_1 THEN исполняеиые_операторы_1 WHEN иня_исключения_Н THEN исполняемые операторыJI WHEN OTHERS THEN исполняемые операторы others END: Если имя генерируемой ошибки соответствует исключению имя_исключения_1, вы- выполняются исполняемые_операторы_1\ если оно соответствует исключению имя_ис- илючения_И, выполняются кпопняеные_операторы_Н и т. д. Предложение WHEN OTHERS служит для обработки исключений, не обработанных операторами предшествую- предшествующих предложений WHEN (подобно ключевому слову ELSE оператора IF), Если вы ре- решили не писать программный код для обработки определенного исключения, на при этом не хотите, чтобы данное исключение считалось необработанным, може- можете поступить так: включить в блок обработки исключений имя исключения (или предложение WHEN OTHERS), но вместо соответствующих исполняемых операторов поместить единственный оператор NULL. Приведем пример такой программы: PROCEDURE calc avg sales BEGIN :sales.avg :- :sales.monthl / :sales.total: EXCEPTION WHEN ZERO_DIVIDE THEN isales.avg :- 0: RAISE FORMJRIGGER FAILURE: WHEN OTHERS THEN NULL: END: При условии, что общее количество продаж (sal es. total) равно нулю, среднее количество продаж (sales.avg) также устанавливается равным нулю и выполне- выполнение триггера в Oracle Forms прекращается. Если же происходит любое другое ис- исключение (например, исключение VALUE_ERROR, инициируемое в том случае, когда полученное в результате вычислений значение оказалось больше, чем может вме- вместить элемент sales.avg), то управление передается предложению WHEN OTHERS, не выполняющему никаких функций (то есть содержащему оператор NULL), и работа
Оператор NULL ' ' 131 продолжается. Поскольку исключение обработано, PL/SQL не инициирует его снова. Более подробно данный вопрос рассматривается в главе 6. Использование оператора NULL после метки В некоторых случаях переход к оператору NULL позволяет избежать выполнения дополнительных операторов. Большинство программистов стараются не пользо- пользоваться оператором GOTO, но бывают ситуации, когда без него трудно обойтись. Од- Однако помните, что за меткой, к которой осуществляется переход, должен стоять хотя бы один исполняемый оператор. В следующем примере оператор GOTO при- применяется для быстрого перехода в конец программы в том случае, если данные указывают, что дальнейшая обработка не требуется: PROCEDURE process_data (datajn IN ordersHROWTYPE, data action IN VARCHAR2) IS BEGIN -- Первая проверка IF data 1n.ship date IS NOT NULL THEN status :- validate sMpdate (data in.sMp date); IF status !- 0 THEN GOTO end_of_procedure7 END IF; -- Вторая проверка IF data 1n.order date IS NOT NULL THEN status :- validate orderdate (data in.order date): IF status I- 0 THEN GOTO end of procedure; " END IF; ... Дополнительные проверки ... « end of_procedure » NULL; " END: В случае обнаружения ошибки в одном из разделов остальные операторы, осу- осуществляющие проверку, отменяются с помощью оператора GOTO. Поскольку в кон- конце процедуры делать ничего не нужно, но там должен находиться хотя бы один исполняемый оператор, помещаем после метки NULL. И несмотря на то что с по- последним не связано никакое действие, он все же является исполняемым оператором.
5 Циклы > Основы циклов > Простой цикл > Цикл WHILE > Использование счетчика в цикле FOR > Использование курсора в цикле FOR > Метки циклов > Полезные советы В настоящей главе рассматриваются управляющие структуры PL/SQL, которые называются циклами. Они предназначены для многократного выполнения про- программного кода. PL/SQL поддерживает циклы трех видов: простой (он же беско- бесконечный), FOR и WHILE. В ходе изложения материала вы познакомитесь с правилами использования циклов и особенностями их построения. Из представленных ниже таблиц узнае- узнаете, как завершается цикл, когда проверяется условие его завершения и в каких случаях циклы какого вида следует применять. Основы циклов Возможность использования циклов разных видов позволяет выбрать оптималь- оптимальный способ решения каждой конкретной задачи. В большинстве случаев решить задачу можно с помощью любого цикла, но если воспользоваться самым подходя- подходящим из них, код будет более коротким и простым. Примеры разных циклов Чтобы вы сразу получили некоторое представление о каждом из циклов и о том, как они работают, рассмотрим три процедуры. Каждая из них содержит цикл, в теле которого выполняется одна и та же строка кода (процедура set_rank, уста- устанавливающая категорию с заданным номером): set_rank Crankingleuel):
Основы циклов 133 О Простой цикл. Процедура принимает в качестве аргумента максимальный но- номер категории и устанавливает категории, начиная с первого номера и закан- заканчивая самым большим. Заметьте, что от бесконечного выполнения цикла нас предохраняет оператор EXIT WHEN, обеспечивающий его завершение: PROCEDURE set_a11_ranks (max_rank_in IN INTEGER) IS rankingjevel NUMBER C) :- 1: BEGIN LOOP EXIT WHEN rankingjevel > max_rank_in: set_rank (rankingjevel): rankingjevel :- rankingjevel + 1: END LOOP; END set_all_ranks; О Цикл FOR. В этом случае устанавливается категория из фиксированного диа- диапазона, от 1 до максимального значения: PROCEDURE set_all_ranks (max rankjn IN INTEGER) IS rankingjevel NUMBER C) := 1 BEGIN FOR rankingjevel IN 1 .. max_rankjn LOOP set_rank С ranki ng_level); END LOOP: END set_all_ranks; О Цикл WHILE. Процедура принимает в качестве аргумента максимальный но- номер категории и устанавливает категории, начиная с первого номера и закан- заканчивая максимальным значением. Обратите внимание на то, что условие завер- завершения цикла задано в одной строке с ключевым словом WHILE: PROCEDURE set_all_ranks (max_rankjn IN INTEGER) IS rankingjevel NUMBERO) := 1; BEGIN WHILE rankingjevel <- max_rankjn LOOP set_rank С rank i ngJ evel); rankingjevel :- rankingjevel + 1: END LOOP: END set_all_ranks: В приведенном выше примере самым маленьким получился цикл FOR. Однако его можно использовать только потому, что заранее известно, сколько раз (тах_ rankjn) должно выполняться тело цикла. Во многих других случаях количество повторений может быть различным, и поэтому для них цикл FOR не подходит. Структура циклов PL/SQL Цикл определяется начинающими его зарезервированными словами, условием завершения и оператором END LOOP, отмечающим конец цикла в программе. Тело цикла - это последовательность исполняемых операторов, которая выполняется на каждой итерации цикла (рис. 5.1).
134 Глава 5 • циклы WHILE[need_more_data LOOP read_next_record (recordjd); need_more_data :- " record 1d IS NOT NULL; END LOOP; - Условие знершамл - Цикл - Галочий» Рис. 5.1. Цикл WHILE и его тело В общем случае цикл можно представить себе как процедуру или функцию. Его тело — «черный ящик», а условие завершения — это интерфейс такого ящика. Код, находящийся вне цикла, ничего не должен знать о его внутренней работе. Именно так можно рассматривать примеры разных циклов, которые приведены в этой главе. Помимо тех, что содержатся в настоящей главе, можно просмотреть несколько более подробных примеров использования циклов в Oracle Forms. Вы найдете их на узле O'Reilly в файлах highrec.doc и hlghrec.fp (демонстрируют принцип выбора элементов в Oracle Forms), а также в файлах ofquery.doc, postqry.fp и preqry.fp (опи- (описаны методы автоматической обработки до и после запроса в Oracle Forms). Простой цикл Структура простого цикла лежит в основе всех остальных циклических конст- конструкций. Такой цикл состоит из ключевого слова LOOP, исполняемого кода (тела цикла) и ключевых слов END LOOP: LOOP исполняемыеолера торы END LOOP; Цикл начинается оператором LOOP, а заканчивается оператором END LOOP. Тело цикла должно содержать как минимум один исполняемый оператор. Свойства простого цикла описаны в следующей таблице. Свойство Описание Условие завершения цикла Когда проверяется условие завершения цикла В каких случаях используется данный цикл Если в теле цикла выполняется оператор EXIT. В противном случае он работает бесконечно При выполнении оператора EXIT или EXIT WHEN. Таким образом, тело цикла (или его часть) всегда выполняется как минимум один раз Во-первых, если не известно, сколько раз будет выполняться тело цикла, а во-вторых, когда необходимо, чтобы тело цикла было выполнено хотя бы один раз Данный цикл удобно использовать в ситуации, когда нужно гарантировать хотя бы однократное выполнение тела цикла (или его части).
Простой цикл 135 Простой цикл завершается при выполнении в теле цикла оператора EXIT или его близкого родственника - оператора EXIT WHEN, а также в том случае, если в те- теле цикла инициируется исключение (которое остается необработанным). Завершение простого цикла: операторы EXIT и EXIT WHEN При использовании простых циклов нужна предельная внимательность. Убеди- Убедитесь, что в коде задано условие, при котором выполнение цикла останавливается. Для этого в его теле должен содержаться оператор EXIT или EXIT WHEN. Синтаксис данных операторов таков: EXIT EXIT WHEN условие: Здесь условие - это логическое выражение. Приведенный ниже пример показы- показывает, как оператор EXIT останавливает выполнение цикла и передает управление оператору, следующему за ключевыми словами END LOOP. Функция account_bal ance возвращает остаток денег на счету с идентификатором accounted. Если на этом счету осталось меньше 1000 долларов, выполняется оператор EXIT и цикл завер- завершается. В противном случае программа снимает с банковского счета клиента сум- сумму, необходимую для оплаты заказов: LOOP balance_remaining :- account_balance (accounted): IF balance remaining < 1000 THEN EXIT; ELSE applybalance (accountjd, balance_rema1n1ng): END IF: END LOOP Следует отметить, что оператор EXIT можно использовать только в цикле LOOP. PL/SQL для выхода из цикла предлагает еще один оператор, EXIT WHEN, предна- предназначенный для завершения цикла при наличии определенного условия. Он соче- сочетает в себе функции операторов IF.. .THEN и EXIT. Приведенный выше цикл опера- оператор EXIT WHEN изменяет следующим образом: LOOP /* Вычисление баланса */ balance_rema1n1ng :- account_balance (accountjd): /* Встраиваем условие в оператор EXIT */ EXIT WHEN balancej-emaining < 1000; /* Если цикл все еще выполняется, баланс уменьшается */ apply_balance (accountjd, balance_remaining): END LOOP; Как видите, для проверки условия завершения данного цикла оператор IF не Нужен. Его задачу выполняет оператор EXIT WHEN.
136 Глава 5 ¦ Циклы Так в каких же случаях следует использовать оператор EXIT WHEN, а в каких - просто EXIT? О Оператор EXIT WHEN больше подходит для ситуаций, когда условие завершения цикла определяется одним выражением, как в приведенном выше примере. О Если существует несколько условий завершения цикла, когда условия выхода из цикла разные, а также если должно быть определено возвращаемое значе- значение, предпочтение следует отдать использованию оператора IF или CASE с опе- оператором EXIT. Давайте рассмотрим пример, когда предпочтительнее использовать оператор EXIT. Он взят из функции, определяющей, являются ли два файла эквивалентны- эквивалентными (то есть содержащими одну и ту же информацию): IF (end_of_filel AND end_of_file2) THEN retval :- TRUE: EXIT: ELSIF (checkline !- againstline) THEN retval :- FALSE: EXIT: ELSIF (end_of_filel OR end_of_file2) THEN retval :- FALSE: EXIT: END IF: END LOOP; Эмуляция цикла REPEAT UNTIL В PL/SQL отсутствует цикл REPEAT UNTIL, в котором условие проверяется после выполнения тела цикла, в результате чего последнее выполняется как минимум один раз. Однако этот цикл легко эмулировать с помощью следующего простого цикла: LOOP ... тело цикла ... EXIT WHEN логическое_условие; END LOOP: Здесь логическое^условие — это логическая переменная или выражение, резуль- результатом проверки которого является значение TRUE или FALSE. Цикл WHILE Цикл WHILE относится к числу условных. Его выполнение продолжается до тех пор, пока результатом проверки определенного в цикле условия является значе- значение TRUE. А поскольку возможность выполнения этого цикла зависит от условия
Цикл WHILE 137 и не ограничено определенным количеством повторений, он используется имен- именно в тех случаях, когда количество повторений цикла заранее не известно. Приведем общий синтаксис цикла WHILE: WHILE условие исполияеные_операторы END LOOP: Здесь условие - это логическая переменная или выражение, результатом про- проверки которого является логическое значение TRUE, FALSE или NULL. Условие прове- проверяется на каждой итерации цикла перед выполнением операторов, находящихся в его теле. Если результат оказывается равным TRUE, тело цикла выполняется. В том случае, когда результат равен FALSE или NULL, цикл завершается и управле- управление передается исполняемому оператору, следующему за оператором END LOOP. Свойства цикла WHILE приведены в таблице. Свойство Описание Условие завершения Если значением логического выражения цикла является FALSE или NULL цикла Когда проверяется Перед первым и каждым последующим выполнением тела цикла. Таким условие завершения образом, не гарантируется даже однократное выполнение тела цикла цикла WHILE В каких случаях Во-первых, если не известно, сколько раз будет выполняться тело используется данный цикла; во-вторых, когда необходимо, чтобы возможность выполнения цикл цикла определялась условием; в-третьих, когда необязательно, чтобы тело цикла было выполнено хотя бы один раз Условие WHILE проверяется в начале цикла и в начале каждой его итерации, пе- перед выполнением тела цикла. Такого рода проверка позволяет сделать следую- следующие выводы. О Всю информацию, необходимую для вычисления условия, нужно задавать пе- перед первым выполнением цикла. О Вполне возможно, что цикл WHILE не будет выполнен ни разу. Приведем пример цикла WHILE из файла datemgr.pkg, который можно найти на узле O'Reilly. Здесь используется условие, представленное сложным логическим выражением. Остановка цикла WHILE вызвана одной из двух причин: либо закон- закончился список масок даты, которые применяются для выполнения преобразова- преобразования, либо преобразование успешно завершено (и теперь переменная date_conver- ted содержит значение TRUE): WHILE maskjindex <- flirt count AND NOT date converted LOOP BEGIN /* Попытка преобразовать строковое значение с использованием маски в строке таблицы */ retval :-TO_DATE (valuejn. fmts Cmask_index)); date_converted :- TRUE: EXCEPTION WHEN OTHERS THEN retval :- NULL: masMndex := mask index + 1; END: END LOOP:
138 Глава 5 ¦ Циклы Использование счетчика в цикле FOR В PL/SQL существует два вида цикла FOR: с числовым счетчиком и с курсором. Цикл с числовым счетчиком — это традиционный и хорошо знакомый всем про- программистам цикл FOR, имеющийся в большинстве языков программирования. Ко- Количество итераций данного цикла известно еще до его начала — это диапазон, за- задаваемый между ключевыми словами FOR и LOOP. При установке диапазона значе- значений счетчика цикла неявно объявляется сам счетчик (если он не был объявлен ранее), определяются начальное и конечное значения диапазона, а также задается направление изменения значений счетчика (от наименьшего к наибольшему или наоборот). Приведем общий синтаксис цикла FOR: FOR индекс_цикла IN [REVERSE] начальное_эначение .. конечное_значение LOOP чсполняекые_операторы END LOOP: Между ключевыми словами LOOP и END LOOP должен стоять хотя бы один ис- исполняемый оператор. Свойства цикла FOR с числовым счетчиком приведены в следующей таблице. Свойство Описание Условие завершения Если выполнено то количество итераций, которое определено цикла диапазоном значений счетчика. (Можно завершить цикл и оператором EXIT, но это делать не рекомендуется) Когда проверяется После каждого выполнения тела цикла компилятор PL/SQL проверяет условие завершения значение счетчика цикла, Если оно находится вне заданного диапазона, цикла выполнение цикла прекращается. В том случае, когда начальное значение больше конечного, тело цикла не выполняется В каких случаях Если тела цикла должна быть выполнено определенное количество раз используется данный цикл Правила для циклов FOR с числовым счетчиком При использовании цикла FOR с числовым счетчиком необходимо следовать та- таким правилам. О Не объявляйте счетчик цикла. По умолчанию он будет неявно объявлен как локальная переменная, имеющая тип данных INTEGER. Областью действия ука- указанной переменной будет весь цикл; на счетчик цикла нельзя ссылаться извне. О Выражения, используемые при определении диапазона счетчика цикла (на- (начального и конечного значений) вычисляются один раз. В ходе выполнения цикла они не пересчитываются. Поэтому если внутри цикла вы измените зна- значения переменных, которые используются при определении диапазона счет- счетчика, его граничные значения останутся прежними. О Никогда не меняйте значения счетчика цикла и границ диапазона внутри . цикла. Это порочная практика. Компилятор PL/SQL либо выдаст сообщение
Использование счетчика в цикле FOR 139 об ошибке, либо проигнорирует ваши действия — в любом случае у вас воз- возникнут проблемы. О Если вы хотите, чтобы значения счетчика уменьшались в направлении от конечного к начальному, используйте ключевое слово REVERSE. При этом первое значение в определении диапазона (ндчапьное_знзчение) должно быть меньше второго (конечное_значение). Но не меняйте порядок следования этих значений - ключевое слово REVERSE само сделает все, что нужно. Примеры циклов FOR с числовым счетчиком Далее будут рассмотрены примеры, демонстрирующие некоторые варианты син- синтаксиса цикла FOR с числовым счетчиком. О Цикл выполняется 10 раз; значение счетчика увеличивается от 1 до 10: FOR 1oop_counter IN 1 .. 10 LOOP ... исполняемые операторы . . . END LOOP; О Цикл выполняется 10 раз; значение счетчика уменьшается от 10 до 1: FOR loop_counter IN REVERSE 1 .. 10 LOOP ... исполняемые операторы ... END LOOP: О Цикл не выполняется ни разу. Используется ключевое слово REVERSE, так что значение счетчика цикла, loop_counter, изменяется от наибольшего до наи- наименьшего. Однако начальное и конечное значения счетчика заданы неверно: FOR loop_counter IN REVERSE 10 .. 1 LOOP /* Эта тело цикла не выполнится ни разу! */ END'LOOP: Даже если вы зададите направление с помощью ключевого слова REVERSE, на- начальное значение диапазона счетчика все равно должно быть указано первым. Если начальное значение больше конечного, тело цикла ни разу не будет вы- выполнено. Если же граничные значения будут одинаковыми, тело цикла будет выполнено один раз, О Цикл выполняется для диапазона, определяемого значениями переменной и выражения: FOR caicjndex IN start_per1od_number .. LEAST (end per1od_number, current period) LOOP ... исполняемые операторы . .. END LOOP; В этом примере количество итераций цикла определяется во время выполнения программы. Начальное и конечное значения вычисляются один раз, перед на- началом цикла, и затем используются в течение всего времени его выполнения.
140 Глава 5 • Циклы Обработка нетривиальных приращений PL/SQL не предоставляет специальных синтаксических способов задания шага приращения счетчика. Во всех разновидностях цикла FOR с числовым счетчиком значение счетчика на каждой итерации увеличивается на единицу. В том случае, когда приращение должно быть нестандартным (не равным еди- единице), придется писать специальный код. Например, что нужно сделать для того, чтобы тело цикла выполнялось только для четных чисел из диапазона от 1 до 100? Во-первых, можно использовать числовую функцию MOD, как в следующем примере: FOR loop Index IN 1 .. 100 LOOP [F MOD (loopjndex. 2) ~ 0 THEN /* Число четное, поэтому выполним вычисления */ calcj/aluesAoop_index); END IF; END LOOP; Во-вторых, в теле цикла значение счетчика можно умножить на два и исполь- использовать вдвое меньший диапазон: FOR everwiumber IN 1 .. 50 LOOP calc_va1ues(even_nurnber*2): END LOOP: В обоих случаях процедура cal cval ues выполняется только для четных чисел. В первом примере цикл FOR повторяется 100 раз, во втором — только 50. Но какой бы подход вы ни избрали, обязательно подробно его закомментируйте. Коммен- Комментарии помогут другим программистам при сопровождении вашей программы. Использование курсора в цикле FOR Цикл FOR с курсором - это цикл, связанный с явно заданным курсором или за- заданной непосредственно в рамках цикла инструкцией SELECT. Такой цикл следует использовать только в том случае, если вам нужно извлечь и обработать все запи- записи курсора, что приходится делать довольно часто. Цикл FOR с курсором — одна из замечательных возможностей PL/SQL, обеспе- обеспечивающая тесную и эффективную интеграцию процедурных конструкций с мощью языка доступа к базам данных SQL. В случае его применения заметно сокращает- сокращается объем кода, необходимого для извлечения данных из курсора, и уменьшается вероятность возникновения ошибок при циклической обработке данных — ведь именно циклы являются основными источниками ошибок в программах. Приведем базовый синтаксис цикла FOR с курсором: FOR инаекс_записи IN [mnjypcopa (явная_инструкция SELECT)] LOOP ислолняемые_операторы END LOOP:
Использование курсора в цикле FOR 141 Здесь индекс_залиси — неявно объявленная запись с атрибутом XR0WTYPE для курсо- курсора иня_курсора. ВНИМАНИЕ Не объявляйте явно запись (индекс_записи), на которой основан цикл. В этом нет необходимости, поскольку запись автоматически объявляется неявно, а к тому же подобное действие может привес- привести к логическим ошибкам. О том, как получить доступ к информации о записях, обработанных в цик- цикле FOR после его выполнения, рассказывается далее в этой главе. В цикле FOR можно также задать не курсор, а непосредственно SQL-инструк- SQL-инструкцию SELECT, как показано в следующем примере: FOR book_cur IN (SELECT * FROM book) LOOP showjjsage: END LOOP: На первый взгляд такая конструкция может показаться достаточно удобной, но мы рекомендовали бы ее избегать, поскольку встраивание инструкций SELECT в «неожиданные» места кода затрудняет его сопровождение и отладку. Свойства цикла FOR, в котором используется курсор, приведены ниже в виде таблицы. Свойство Описание Условие завершения Если выбраны все записи курсора. Цикл можно завершить и оператором цикла ВОТ, но так поступать не рекомендуется Когда проверяется После каждого выполнения тела цикла компилятор PL/SQL осуществляет условие завершения очередную выборку строки. В том случае, когда значение атрибута цикла курсора %NOTFOUND оказывается равным TRUE, цикл завершается. Если курсор не возвратит ни одной строки, тело цикла никогда не будет выполнено В каких случаях При необходимости выбрать и обработать каждую запись курсора используется данный цикл Данный цикл рекомендуется использовать, когда нужно безо всяких условий по очереди выбрать все строки курсора (и в цикле нет операторов EXIT и EXIT WHEN, вызывающих его преждевременное завершение). С помощью этого цикла можно упростить программный код и уменьшить вероятность появления ошибок. Примеры цикла FOR с курсором Предположим, нам необходимо обновить счета владельцев всех живущих в спе- специальном отеле домашних животных. Следующий код включает анонимный блок, в котором для выбора номера комнаты и идентификационного номера животного используется курсор occupancycur. Процедура updatebill назначает комнаты но- новым постояльцам отеля: 1 DECLARE 2 CURSOR occupancy_cur IS 3 SELECT pet_id. roomjiumber 4 FROM occupancy WHERE occupied_dt - TRUNC (SYSDATE); 5 occuparicy_rec occupancy_cur*ROWTYPE;
142 6 7 В 9 10 11 1Z 13 н 15. BEGIN OPEN occupancy cur; LOOP FETCH occupancy cur INTO occupancy rec; EXIT WHEN occupancy_cur*NOTRXJND; updatejm (occupancy rec.petjd. occupancy rec.room number); END LOOP; CLOSE occupancy cur; END: Глава 5 • Циклы Этот код последовательно и явно выполняет все необходимые действия: опре- определяет курсор (строка 2) и запись для этого курсора (строка 5), открывает курсор (строка 7), начинает выполнение цикла (строка 8), выбирает из курсора строку (строка 9), проверяет условие выхода из цикла (конец данных) с помощью соот- соответствующего атрибута курсора (строка 10) и, наконец, выполняет обновление (строка 11). После этого программист должен закрыть курсор (строка 14). А вот что получится, если переписать тот же код с использованием цикла FOR с курсором: DECLARE CURSOR occupancy_cur IS SELECT petjd, room number FROM occupancy WHERE occupied dt - TRUNC (SYSDATE): BEGIN FOR occupancy_rec IN occupancy_cur LCD? update Ы11 (occupancy rec.petjd. occupancy_rec.rooni_nufflber): END LOOP;" END; ¦• Как все просто и понятно! Исчезло объявление записи. Исчезли операторы OPEN, FETCH и CLOSE. Больше не нужно проверять атрибут BNOTFOUND. Нет никаких сложностей с организацией выборки данных. Далее PL/SQL с учетом того, что нужна каждая строка курсора и что ее следует поместить в соответствующую за- запись курсора, позаботится обо всем сам. Как и любому другому курсору, курсору в цикле FOR можно передавать пара- параметры. Если какой-либо из столбцов списка инструкции SELECT задается в качест- качестве выражения, обязательно определите для него псевдоним. Единственным спо- способом доступа к конкретному значению в строке курсора является применение синтаксиса с использованием точки имя_залиси. имя_столбца (например, occupan- су_гес. room_number), так что без псевдонима к столбцу-выражению обратиться не- невозможно. Метки цикла Циклу можно назначить имя, воспользовавшись для этой цели меткой. (О метхах рассказывалось в главе 3.) Метка цикла в PL/SQL имеет следующий формат: «имя метни»
Метки цикла 143 Метка должна стоять непосредственно перед оператором FOR: «all_emps» FOR ешр_гес IN елр_сиг LOOP END LOOP: Эту же метку можно указать и после зарезервированных слов END LOOP, как в следующем примере: «yearjoop» WHILE year number <- 1995 LOOP «nonthjoop» FOR monthjiumber IN 1. .12 LOOP END LOOP monthjoop; END LOOP yearjoop; В каких же случаях обычно используется метка цикла? Назовем несколько типичных ситуаций. О Если вы написали очень длинный цикл (начинающийся, скажем, на 50-й стро- строке, оканчивающийся на 725-й и содержащий 16 вложенных циклов), исполь- используйте метку цикла для того, чтобы явно связать его конец с началом. Эта визу- визуальная пометка поможет вам при отладке и сопровождении программы, иначе будет сложно отследить, какой оператор LOOP соответствует каждому из опера- операторов END LOOP. О Метку цикла можно использовать для уточнения имени индексной перемен- переменной цикла (записи или счетчика), что также делает текст программы более чи- читабельным: «yearjoop» FOR yearjiimber IN 1800..1995 LOOP «month J oop» FOR monthjiumber IN 1. .12 LOOP IF year 1oop.year_number - 1900 THEN ... END IF: END LODP monthjoop: END LOOP yearjoop; О При использовании вложенных циклов метку применяют как для повышения читабельности кода, гак и с целью более эффективного управления циклами. При желании выполнение именованного внешнего цикла можно остановить посредством оператора EXIT с заданной в нем меткой цикла: EXIT нвтиа_цикпа: EXIT кетка_циш WHEN условие:
144 ' Глава 5 • Циклы Но обычно не рекомендуется применять метку цикла подобным образом, так как логика программы становится в результате этого плохо структурированной (как в ситуации с операторами GOTO) и ее трудно отлаживать. Если вам потре- потребуется использовать подобный код, будет целесообразнее изменить структуру цикла, а возможно, заменить его циклом FOR или простым циклом WHILE. Полезные советы Циклы — это очень эффективные управляющие конструкции, но при их исполь- использовании необходима особая аккуратность и внимательность. Именно с циклами часто связаны вопросы производительности программ, и любая ошибка, возник- возникшая в цикле, повторяется ввиду многократности его выполнения. Логика, опре- определяющая условие остановки цикла, бывает очень сложной. В этом разделе при- приводится несколько советов по поводу того, как сделать циклы более четкими и понятными, а также максимально упростить их сопровождение. Использование для счетчиков циклов понятных имен Программист, которому поручено сопровождать программу, не должен уподоб- уподобляться Шерлоку Холмсу, с тем чтобы с помощью сложной дедукции определять смысл начального и конечного значений счетчика цикла FOR. Вы должны приме- применять понятные и информативные имена переменных и циклов, и тогда другим программистам (да и вам самим некоторое время спустя) легко будет разобраться в таком коде. Как можно понять следующий код, не говоря уже о его сопровождении? FOR i IN start_id .. endjd LOOP FOR j IN 1 .. 7 LOOP FOR k IN 1 .. 24 LOOP build_schedule (i, j, k): END LOOP; END LOOP: ¦ END LOOP; Неужели невозможно обойтись без подобных имен переменных? Тем не менее их можно видеть сплошь и рядом. А исправить этот код очень просто — достаточ- достаточно заменить имена переменных более информативными: FOR focus_accoLint IN startid end_id LOOP FOR day_1n_week IN 1 .. 7 LOOP FOR month_1n_biyear IN 1 .. 24 LOOP build_schedule (focus_account. dayjn_week. month_in_biyear); END LOOP: END LOOP; END LOOP;
Полезные советы 145 Теперь мы видим, что внутренний цикл просто перебирает месяцы двухлетне- двухлетнего периода A2 2 - 24). Как правильно попрощаться Один из фундаментальных принципов структурного программирования звучит так: «один вход, один выход». Первая его часть в PL/SQL реализуется автомати- автоматически. Какой бы цикл вы ни выбрали, у него всегда только одна точка входа — первый исполняемый оператор, следующий за ключевым словом LOOP. Но вполне реально написать цикл с несколькими точками выхода. Однако это не совсем удачная практика, поскольку цикл, который может завершаться несколькими спо- способами, трудно отлаживать и сопровождать. При задании выхода из цикла нужно придерживаться следующих правил. О Не использовать в циклах FOR и WHILE операторы EXIT и EXIT WHEN. Цикл FOR дол- должен завершаться только тогда, когда исчерпаны все начальные и конечные значения диапазона. Оператор EX IT внутри цикла FOR прерывает данный про- процесс и поэтому идет вразрез с самим назначением цикла FOR. Точно так же ус- условие окончания цикла WHILE задается в самом операторе WHILE, и больше нигде задавать или дополнять его не следует. О Не использовать в циклах операторы RETURN и GOTO, поскольку это вызывает преждевременное и неструктурированное завершение цикла. Применение ука- указанных операторов, возможно, покажется вам привлекательным приемом, по- позволяющим уменьшить количество кода. Однако некоторое время спустя по- понять, модифицировать и отладить такой код будет нелегко. Рассмотрим суть этих правил на примере цикла FOR, в котором используется курсор. Как вы уже видели, данный цикл облегчает просмотр возвращаемых кур- курсором записей, но не подходит для тех случаев, когда выход из цикла определяет- определяется некоторым условием, основанным на данных текущей записи. Предположим, что в цикле записи курсора просматриваются до тех пор, пока сумма значений определенного столбца (например, количество животных) не превысит макси- максимальное значение, как показано в следующем примере. Хотя это можно сделать с помощью цикла FOR и курсора, выполнив внутри этого цикла оператор EXIT, по- поступать так не следует. 1 DECLARE 2 CURSOR occupancy_cur IS 3 SELECT pet_id. roomjiumber 4 FROM occupancy WHERE occupied_dt - TRUNC (SYSDATE); 5 pet_count INTEGER :- 0; 6 BEGIN 7 FOR occupancy_rec IN occupancy_cur 8 LOOP 9 updatebill 10 (accupancy_rec.pet_id. ocaipancyrec.roomjiiimber): И ¦ pet_count :- pet_count + 1: 1Z EXIT WHEN petcount >- :GLOBAL.max_pets; 13 END LOOP; 14 END;
146 Глава 5 • Циклы В цикле FOR явно говорится, что его тело должно быть выполнено п раз (где п — количество шагов в цикле со счетчиком или количество записей в цикле с курсором). Оператор EXIT внутри цикла FOR (строка 12) изменяет логику его вы- выполнения, но в результате получается код, который трудно понять и отладить. Поэтому, если нужно остановить цикл на основании информации текущей запи- записи, лучше воспользоваться циклом WHILE или же простым циклом. Тогда структу- структура кода будет лучше отражать ваши намерения. Получение информации о выполнении цикла FOR Циклы FOR - это довольно строгие структуры, использовать которые очень удоб- удобно. Они выполняют в программе большую «административную» работу, особен- особенно циклы с курсором. Однако есть у них и существенный недостаток: позволив Oracle делать вместо нас определенную работу, мы ограничиваем собственные возможности доступа к конечным результатам цикла после его завершения. Предположим, нам нужно узнать, сколько записей обработано в цикле FOR с кур- курсором, и затем использовать это значение в программе. Было бы очень удобно на- написать примерно такой код: BEGIN FOR book_rec IN books_cur (author in -> 'FEURSTEIN,STEVEN') LOOP ... обработка данных ... END LOOP; IF books_cur*ROWCOUNT > 10 THEN ... Но, попытавшись это сделать, мы получим следующее сообщение об ошибке: ORA-01001: invalid cursor (недопустимый курсор), поскольку курсор был неявно открыт и закрыт Oracle. Так как же получить нужную нам информацию из уже завершившегося цикла? Для этого следует объявить переменную в том блоке, в который входит цикл FOR, и присвоить ей значение в его теле - в таком случае она останется доступной и после окончания цикла. Вот как это делается: DECLARE book count PLS INTEGER: BEGIN FOR book_rec IN books_cur (authorjn -> 'FEURSTEIN.STEVEN'3 LOOP ... обработка данных ... book count :- books airXROWCOUNT; END LOOP: IF bookcount > 10 THEN ... SQL-инструкция в качестве цикла На самом деле SQL-инструкцию SELECT можно рассматривать как цикл, посколь- поскольку она определяет действие, выполняемое компилятором SQL с набором данных. В некоторых случаях при реализации определенной задачи можно даже выби- выбирать между использованием цикла PL/SQL и SQL-инструкции. Давайте рассмот- рассмотрим пример, а затем сделаем некоторые выводы о том, какое решение лучше.
Полезные советы 147 Предположим, нам необходимо написать программу, которая переносит ин- информацию о выбывших из отеля животных из таблицы occupancy в таблицу осси- pancyjii story. Выбираем цикл FOR, в котором используется курсор. В теле цикла сначала добавляем каждую выбранную из курсора запись (представляющую вы- выбывшее из отеля животное) в таблицу occupancy_hi story, а затем удаляем ее из таблицы occupancy: DECLARE CURSOR checked_out_cur IS SELECT pet id. name, checkout_date FROM occupancy WHERE checkout date IS NOT NULL; BEGIN FOR checked out rec IN checked_out_cur UXP INSERT INTO occupancyhi story (petjd. name. checkout_date) VALUES (checked_out_rec.petjd. checlced_out_rec.name, checked_out_rec.checkout_date): DELETE FROM occupancy WHERE petjd - checked_out_rec.petjd; END LOOP: EM): Этот код все делает правильно, но является ли данное решение единствен- единственным? Конечно же, нет. Поставленную задачу можно также выполнить с помощью SQL-инструкций: BEGIN INSERT INTO occupancy_h1 story (petjd. NAME. checkout_date) SELECT petjd. NAME. checkout_date FROM occupancy WHERE checkout date IS NOT NULL: DELETE FROM occupancy WHERE checkout date IS NOT NULL; END; Каковы преимущества этого подхода? Код стал короче и выполняется более эффективно, поскольку уменьшилось количество переключений контекста (пе- (переходов от исполняемого ядра PL/SQL к исполняемому ядру SQL и обратно). Теперь обрабатываются только одна инструкция INSERT и одна инструкция DELETE. Однако у «стопроцентного»' SQL-подхода имеются свои недостатки. SQL-ин- SQL-инструкция обычно действует по принципу «все или ничего». Иными словами, если при обработке одной строки происходит ошибка, отменяются все инструкции IN- INSERT и DELETE и ни одна запись не будет вставлена или удалена. Кроме того, прихо- приходится дважды записывать предложение WHERE. Для нашего примера это не очень важно, но в случае более сложных запросов данное обстоятельство может иметь решающее значение. А первоначальный цикл FOR позволяет избежать дублирова- дублирования потенциально сложной логики в нескольких местах. Кроме того, PL/SQL более гибок, чем SQL Допустим, нам хотелось бы пере- переносить за одну операцию максимально возможное количество записей, а для тех записей, при перемещении которых произошли ошибки, просто записывать сооб- сообщения в журнал. В этом случае необходимо создать цикл FOR, дополненный разде- разделом исключений: BEGIN FOR checked_out_rec IN checked out_cur LOOP
148 Глава 5 • Циклы BEGIN INSERT INTO occupancyhistory ... DELETE FROM occupancy ... EXCEPTION WHEN OTHERS THEN "log_chedcout_error (checked out_rec): END: END LOOP; END: PL/SQL позволяет обрабатывать записи по одной и для каждой из них выпол- выполнять необходимые действия, которые могут быть и очень сложными. В таких слу- случаях удобнее использовать PL/SQL и SQL совместно. Но если ваша задача тако- такова, что для ее решения достаточно одного SQL, лучше им и воспользоваться — код получится и короче, и эффективнее.
Глава 6 Обработка исключении > Как в PL/SQL обрабатываются ошибки > Определение исключений > Как инициировать исключение > Обработка исключений К сожалению, многие программисты склонны выдавать желаемое за действитель- действительное и часто полагают, что проделали титаническую работу, реализовав какую-либо положительную функцию приложения, скажем, операцию формирования зака- заказов или счетов. Им невероятно трудно и с психологической точки зрения, и с точ- точки зрения ресурсов сконцентрироваться на неявном аспекте, например преду- предусмотреть, что произойдет, если пользователь нажмет не ту клавишу, или же что делать, если база данных окажется недоступной. Как правило, они пишут прило- приложения, предназначенные для работы в «лучшем из миров», где в программах не бывает ошибок, пользователи вводят лишь правильные данные и только долж- должным образом, а все системы — и аппаратные, и программные — всегда в полном порядке. Но жестокая реальность свидетельствует о том, что как бы вы ни старались, в приложении всегда останутся ошибки. И пользователь обязательно воспользу- воспользуется именно той последовательностью клавиш, на которую ваша программа реа- реагирует сбоем. Вот почему перед вами стоит очень простой выбор: либо потратить определенное время на основательную отладку и защиту программ, либо приго- приготовиться к тяжким и непрекращающимся баталиям с рассерженными пользова- пользователями. К счастью, PL/SQL предлагает достаточно мощный и гибкий механизм пере- перехвата и обработки ошибок. И вполне возможно написать на языке PL/SQL такое приложение, которое полностью защитит от ошибок и всех пользователей, и базу данных.
150 Глава 6 • Обработка исключений Как в PL/SQL обрабатываются ошибки В языке PL/SQL ошибки всех видов интерпретируются как исключения, то есть недопустимые ситуации, которых всячески следует избегать. К числу исключе- исключений относятся: О ошибки, которые генерируются системой (в частности такие, как нехватка па- памяти и повторяющееся значение индекса); О ошибки, вызванные действиями пользователя; О предупреждения, выдаваемые приложением пользователю. PL/SQL перехватывает ошибки и реагирует на них при помощи так называе- называемых обработчиков исключений, Механизм функционирования обработчиков ис- исключений позволяет четко отделить код обработки ошибок от исполняемых опера- операторов, дает возможность реализовать обработку ошибок, управляемую событиями, отказавшись от устаревшей линейной модели программирования. И независимо от того, как и по какой причине было инициировано конкретное исключение, оно обрабатывается одним и тем же обработчиком в разделе исключений. Когда происходит ошибка, будь то системная ошибка или ошибка в приложе- приложении, в PL/SQL инициируется исключение. В результате выполнение исполняе- исполняемого блока прерывается и управление передается отдельному разделу исключений в текущем блоке, если таковой имеется. После обработки исключения возврат в тот блок, из которого оно было инициировано, не возможен, поэтому управле- управление переходит во внешний блок. Куда передается управление, если в программе произошло исключение, показано на рис. 6.1. PROCEDURE Jimminy IS new_va1ue VARCHARE2B53: BEGIN / \ N. —new_va1 :- old_val || '-new'; IF new_val LIKE 'open*' . THEN •" END IF; EXCEPTION ^*-WHEN VALUEJRROR ._ THEN — Исполняемый раздал — Редел исключений END: Рис. 6.1. Архитектура обработки исключений Стратегия обработки исключений Механизм обработки исключений в PL/SQL достаточно мощен и гибок, но его эф- эффективное использование требует определенных навыков. Очень важно определить
Как в РЦ/SQL обрабатываются ошибки 151 собственную стратегию и последовательность обработки ошибок еще до начала работы над программным кодом. Для этого нужно ответить на ряд вопросов. О Когда и как следует протоколировать ошибки, чтобы их можно было просмот- просмотреть и исправить? О Когда и каким образом пользователь будет извещаться об ошибках? С учетом ответов на эти общие вопросы принимаются решения, связанные с более конкретными действиями. О Следует ли включать раздел обработки исключений в каждый блок PL/SQL? О Не лучше ли поместить раздел обработки исключений только в самый внеш- внешний блок? О Как обрабатывать транзакции в случае возникновения ошибки? Сложность обработки исключений отчасти заключается в том, что на каждый из этих вопросов не существует единого правильного ответа. Любое решение за- зависит от архитектуры приложения и от того, как оно используется (например, выполняется в пакетном режиме или в режиме управляемых пользователем тран- транзакций). И как бы вы ни ответили на поставленные вопросы, касающиеся конкрет- конкретного приложения, мы настоятельно рекомендуем «закодировать» стратегию и пра- правила обработки ошибок в стандартизированном пакете. Эта проблема еще будет затронута в конце главы. Ниже изложены концепции, лежащие в основе обработки исключений в PL/SQL, и даны примеры их различных видов, ознакомившись с которыми, вы поймете, как инициировать ошибки в программах и как их обрабатывать. Концепции и терминология обработки исключений Существует два типа исключений. О Системное исключение. Определено в Oracle и обычно инициируется испол- исполняемым ядром PL/SQL, обнаружившим ошибку. Одни системные исключе- исключения имеют имена (например, NO_DATA_FOUND), тогда как другие — только номера и описания. О Исключение, определяемое программистом. Определяется программистом, а следовательно, специфично для данного приложения. Имя исключения мож- можно связать с конкретной ошибкой Oracle с помощью директивы компилятора EXCEPTIONJNIT. Ошибке можно присвоить номер и создать для нее описание, воспользовавшись процедурой RAISE_APPLICATION_ERROR. Дадим определение некоторым из терминов, вводимых в этой главе. О Раздел исключений - это необязательный раздел блока PL/SQL (анонимно- (анонимного блока, процедуры, функции, триггера или инициализационного раздела па- пакета), содержащий один или несколько обработчиков исключений. Структура раздела исключений очень похожа на структуру оператора CASE, о котором рас- рассказывалось в главе 4. О Инициировать исключение - значит остановить выполнение текущего блока PL/SQL путем уведомления исполняемого ядра об ошибке. Исключение может
152 Глава 6 • Обработка исключений инициировать либо Oracle, либо ваш собственный программный код при по- помощи оператора RAISE или процедуры RAISE_APPLICATION_ERROR. О Обработать исключение — значит перехватить ошибку, передав управление обработчику исключения. Написанный программистом обработчик исключе- исключения может содержать код, который в ответ на исключение выполняет опреде- определенные действия (например, записывает информацию об ошибке в журнал, выводит соответствующее сообщение для пользователя или передает исклю- исключение во внешний блок). О Область действия - часть кода (блок или раздел), где может инициироваться исключение, а также часть кода, инициируемые исключения которой могут перехватываться и обрабатываться соответствующим разделом исключений. О Передача исключения — процесс передачи исключения во внешний блок, если в текущем блоке это исключение не обработано. О Необработанное исключение - это исключение, которое передается без соот- соответствующей обработки в «самом внешнем» блоке PL/SQL. После этого управ- управление передается исполнительной среде, которая уже сама определяет, как от- отреагировать на исключение (выполнить откат транзакции, вывести сообщение об ошибке или, скажем, проигнорировать ее). О Неименованное, или анонимное, исключение — это исключение, с которым связан код ошибки и описание, Такое исключение не имеет имени, которое можно было бы использовать в операторе RAISE или предложении WHEN обра- обработчика исключений. О Именованное исключение — это исключение, которому имя присвоено либо Oracle (в одном из встроенных пакетов), либо разработчиком. В частности, для этой цели можно использовать директиву компилятора EXCEPTION_INIT (в таком случае его можно будет применять и для инициирования, и для обра- обработки исключения). Определение исключений Прежде чем исключение можно будет инициировать и обрабатывать, его нужно определить. В Oracle заранее определены тысячи исключений, большинство из которых имеют только номера и поясняющие сообщения. Имена присваиваются только самым распространенным исключениям. Вы можете определить собственные исключения и использовать их лишь в сво- своих приложениях. Это можно сделать двумя способами, описанными ниже. Объявление именованных исключений Исключения PL/SQL, объявленные во встроенных пакетах, например в пакете STANDARD, связаны с внутренними ошибками и ошибками, генерируемыми системой. Однако многие проблемы, с которыми будет сталкиваться пользователь прило- приложения (и которые часто будут вызываться его действиями) специфичны именно для данного приложения. Возможно, вашей программе придется перехватывать
Определение исключений 153 и обрабатывать такие ошибки, как «отрицательный баланс на счету» или «дата звонка не может быть меньше текущей даты». Хотя у этих ошибок и иная природа, чем, скажем, у ошибки «деление на нуль», они также относятся к числу исключе- исключений, связанных с нормальной работой программы, и должны обрабатываться этой программой. Очень полезной особенностью модели обработки исключений PL/SQ.L при- принято считать отсутствие структурного различия между внутренними ошибками и ошибками, специфическими для приложения. Любое исключение может и долж- должно обрабатываться в разделе исключений независимо от типа ошибки. Конечно, для того чтобы обработать исключение, необходимо знать его имя. Поскольку в PL/SQL имена пользовательским ^исключениям автоматически не назначаются, вы должны делать это самостоятельно, определяя исключения в раз- разделе объявлений блока PL/SQL. При этом задается имя исключения, за которым следует ключевое слово EXCEPTION: Ш_кнлючвния EXCEPTION; Приведенный ниже раздел объявлений процедуры са 1 c_annuaI _saI es содержит два объявления исключений, определяемых программистом: PROCEDURE calc_annual_sales (company id_in IN company.companyjidJTYPE) IS Invalid companyjd EXCEPTION; negativiba lance EXCEPTION; duplicate__company BOOLEAN; BEGIN ... исполняемые операторы ... EXCEPTION WHEN NO_DATA_FOUND -- системное исключение THEN WHEN invalid_company_id THEN WHEN negative_balance THEN END; По своему формату имена исключений подобны именам логических перемен- переменных, но ссылаться на них можно только двумя способами. О Во-первых, это можно сделать с помощью оператора RAISE, помещенного в ис- исполняемый раздел программы (для того чтобы инициировать исключение): RAISE invalid_company_id; О Во-вторых, это делается посредством предложения WHEN раздела исключений (для обработки инициированного исключения): WHEN invalid_company_id THEN
154 Глаза 6 • Обработка исключений Как связать имя исключения с кодом ошибки В Oracle, как уже было сказано, определены имена лишь немногих, наиболее широ- широко распространенных исключений, Преобладающее большинство ошибок в СУБД имеют лишь номера и снабжены поясняющими их сообщениями. Инициировать исключение с номером ошибки (он должен быть представлен значением в диапа- диапазоне от -20 999 до -20 000) может и разработчик приложения, воспользовавшись для этой цели процедурой RAISE_APPLICATION_ERRQR, о которой рассказывается ниже, в разделе «Как инициировать исключение». Наличие в коде программы исключений без имен вполне допустимо, но такой код малопонятен и его трудно сопровождать. Предположим, вы написали про- программу, при выполнении которой Oracle генерирует ошибку, связанную с данны- данными, например такую, как ORA-01843; not в valid month. Для перехвата этой ошибки в код программы помещен обработчик исключений: EXCEPTION WHEN OTHERS THEN IF SQLCOOE - -1843 THEN Но, как видите, данный код совершенно не понятен, поэтому он обязательно должен сопровождаться комментарием. РИМЕЧАНИЕ- SQLCODE—это встроенная функция, которая возвращает номер последней сгенерированной ошиб- ошибки. Она будет рассмотрена далее в этой главе. Использование директивы EXCEPTION_INU С помощью директивы компилятора EXCEPTION_INIT предложение WHEN, использо- использовавшееся в предыдущем примере, можно изменить следующим образом: EXCEPTION WHEN Invalidjronth THEN И никакие литеральные номера ошибок, которые так трудно запомнить, не по- понадобятся. Теперь имя ошибки говорит само за себя. Вы спросите, как же связать его с определенной ошибкой? Ответ прост: воспользоваться EXCEPTION_INIT - ди- директивой компилятора (то есть командой, выполняемой во время компиляции программы), которая связывает заданное имя с внутренним кодом ошибки. Имя должно быть объявлено в программе как EXCEPTION. Установив такую связь, мож- можно инициировать исключение по имени и использовать это имя в предложении WHEN обработчика ошибок. Директива EXCEPTION_INIT располагается в разделе объявлений блока. Указан- Указанное в ней исключение должно уже быть объявлено либо в том же блоке, либо во внешнем, либо в спецификации пакета. Приведем синтаксис анонимного блока: DECLARE иия исключения EXCEPTION: PRAGMA EXCEPTIONJNIT (иня_нскптения. целое_чнсло): Здесь и«я_исключения - это имя исключения, объявляемого программистом, а целое_число - номер ошибки Oracle, которую следует связать с данным исклю- исключением. Причем номером ошибки может служить любое целое число, кроме: числа
Определение исключений 1-55 -1403; 0 или положительного числа, за исключением 1; отрицательного числа меньше -10 000 000. Число -1403 является кодом ошибки NQ_DATA_FOUND. Если по какой-то причине необходимо связать с этой ошибкой собственное именованное исключение, ука- укажите в директиве EXCEPTIONJNIT значение 100. NO_DATA_FDUND - это единственная, надо полагать, ошибка, для которой в Oracle определено два разных номера. Свя- Связано это со стандартом ANSI, который требует, чтобы для определения данной ошибки использовался номер 100. Ну а теперь рассмотрим пример возможного объявления исключения для об- обработки ошибки: ORA-2292 violated Integrity constraining (OWNER.CONSTRAINT) - child record found Данная ошибка происходит при попытке удалить родительскую запись, для которой в таблице имеются дочерние записи. (Дочерняя запись - это запись с внеш- внешним ключом, указывающим на родительскую таблицу.) PROCEDURE delete company (company 1d in IN NUMBER) IS /* Ofi-ьяаляем исключение. */ stm_have_emplqyees EXCEPTION: /* Связываен иия исключения с нонерон ошибки. */ PRAGMA EXCEPTIONJNIT (still have_employees, -2292); BEGIN /* Пытаемся удалить информацию о компании. */ DELETE FROM company WHERE company_id - company_id in; EXCEPTION /* Если найдена дочерняя запись, инициируется это исключение! */ WHEN still_have_employees THEN DBMS_OUTPUT.PUT_LINE С Пожалуйста, сначала удалите данные о служащих компании. ); END: Как лучше использовать директиву EXCEPTION_INIT Директиву EXCEPTION_INIT наиболее целесообразно использовать в двух следую- следующих случаях: О при необходимости присвоить имя безымянному системному исключению, на которое имеется ссылка в программе (следовательно, если в Oracle не опреде- определено имя для некоторой ошибки, это еще не означает, что вы должны работать с ней только исходя из ее номера); О когда нужно присвоить имя специфическому для приложения исключению, которое инициируется при помощи процедуры RAISE_APPLICATION_ERROR (о ней рассказывается далее, в разделе «Как инициировать исключение»; такой прием позволяет обрабатывать данное исключение исходя из имени, а не из номера). , В обоих случаях советуем собрать все директивы EXCEPTION_INIT в пакет, чтобы определения исключений не были разбросаны по всему коду приложения. Пред- Предположим, вы интенсивно используете динамический SQL (см. главу 15) и при
156 Глава 6 • Обработка исключений выполнении запросов у вас часто возникает ошибка «invalid column name» (не- (неверное имя столбца). Конечно, вы не сможете сразу запомнить ее код, но и опре- определять имя для исключения в 20 разных программах не рационально. Поэтому имеет смысл определить собственные «системные исключения» в отдельном па- пакете, предназначенном для работы с динамическим SQL: CREATE OR REPLACE PACKAGE dynsql IS invalidjablejiame EXCEPTION: PRAGMA EXCEPTIONJNIT (irival 1d_table_name. -903): invalidjrolumnjiame EXCEPTION; PRAGMA EXCEPTIONJNIT Cinvalid_coluninjiame. -904); Теперь для перехвата этих ошибок в каждой программе можно использовать следующую строку кода: , WHEN dynsql.inval1d_co!umnnarae THEN ... Коды ошибок -20NNN, передаваемые процедуре RAISE_APPLICATIDN_ERROR, не сле- следует задавать в виде литеральных значений. Создайте пакет, в котором этим ко- кодам будут присвоены имена. Он может быть, например, таким: PACKAGE errnums IS en_too^young CONSTANT NUMBER :- -20001; excjoojrong EXCEPTION; PRAGMA EXCEPTIONJNIT (exc_too_young. -20001); en saljxojow CONSTANT NUMBER := -20002; excjaljoolow EXCEPTION; PRAGMA EXCEPTIONJNIT (exc_sal Joojow . -20002): END errnums: Имея подобный пакет, указывать номер ошибки в следующем коде не обяза- обязательно: PROCEDURE validate_emp (birthdatejn IN DATE) IS min^years CONSTANT PLSJNTEGER :- 18; BEGIN IF ADO_MONTHS CSYSDATE. min^years * 12 * - 1) < birthdatejn THEN RAISE_APPLICATION_ERROR (errnums.errtoo_young, 'Возраст служещего должен быть не менее ' || min^ears || ' лет.'); END IF; END; 06 именованных системных исключениях В Oracle для некоторых исключений определены стандартные имена, которые за- задаются с помощью директивы компилятора EXCEPTIONJNIT во встроенных пакетах. Наиболее важные и широко применяемые из них определены в пакете STANDARD. Это один из двух используемых по умолчанию пакетов PL/SQL (вторым является DBMSJ5TANDARD). To обстоятельство, что эти два пакета используются по умолчанию, означает, что на определенные в них исключения можно ссылаться без указания в качестве префикса имени пакета. Например, если необходимо инициировать
Определение исключений 157 в программе исключение NO_DATA_FOUND, то это можно сделать посредством любого из двух операторов: WHEN NO_DATA_FQUND THEN WHEN STANDARD. NOJATAJOUND THEN Определения стандартных именованных исключений можно найти и в других встроенных пакетах, в частности в DBMS_LOS, предназначенном для проведения опе- операций над большими объектами. Вот пример одного такого определения из ука- указанного пакета: invalid_argval EXCEPTION: PRAGMA EXCEPTION_INIT(invalid_argval. -21560): Поскольку пакет DBMSJ.OB не относится к числу используемых по умолчанию, ссылка на это исключение должна предваряться именем пакета: WHEN DBMS_LOB.1nval1d_argval THEN... Значительная часть исключений, определенных в пакете STANDARD, перечисле- перечислена в табл. 6.1. Для каждого из них приведен номер ошибки Oracle, значение, воз- возвращаемое при вызове функции SQLCODE, и краткое описание. SQLCODE — это встроен- встроенная функция PL/SQ.L, которая возвращает код состояния последней выполнен- выполненной инструкции SQL или DML. Если данная инструкция выполнена без ошибок, функция возвращает нуль. Значение, возвращаемое функцией SQLCODE, совпадает с кодом ошибки Oracle, но существует одно исключение: определяемый стандар- стандартом ANSI номер ошибки NO_DATA_FOUND равен 100. Таблица 6.1. Стандартные исключения в РЦ/SQL Имя исключения Описание Ошибка Oracle/SQLCODE CURSOR_ALREADY_OPEN Попытка открыть (OPEN) курсор, который был открыт ранее. ORA-6511 SQLCODE=-6511 Поэтому перед повторным открытием курсор необходимо сначала закрыть (CLOSE) DUP_VAL_ON_INDEX Инициируется, когда инструкция INSERT или UPDATE пытается ORA-OO001 SQLCODE=-1 сохранить повторяющиеся значения в столбце или столбцах, которые объявлены с ограничением UNIQUE INVALID_CURSOR Ссылка на несуществующий курсор, Обычно это происходит при ORA-010Q1 SQLCODE=-1001 попытке извлечь данные (FETCH) из неоткрытого курсора или закрыть (CLOSE) курсор до его открытия (OPEN) INVAUD_NUMBER PL/SQL выполняет SQL-инструкцию, которая не может успешно ORA-01722 SQLCODE=-1722 преобразовать символьную строку в число. Это исключение отличается от исключения VALUE_ERROR, инициируемого только SQL-инструкцией LOGIN_DENIED Попытка программы подключиться к СУБД Oracle с неверным ORA-01017 SQLCODE=-1017 именем пользователя и паролем NO_DATA_FOUND Это исключение инициируется в трех случаях: при выполнении ORA-01403 SQLCODE=+100 инструкции SELECT INTO (неявный курсор), которая не возвращает ни одной строки; при наличии ссылки на неинициализированную строку локальной таблицы PL/SQL; при попытке выполнить операцию чтения после достижения конца файлз с использованием пакета UTL_FILE продолжением
158 Таблица 6.1 (продолжение) Имя исключения Ошибка Oracle/SQLCODE Описание Глава б ¦ Обработка исключений NOT_LOGGED_ON ORA-01012 SQLCODE=-1012 PROGRAM_ERROR ORA-06501 SQLCODE=-6501 STORAGE_ERROR ORA-06500 SQLCODE=-6500 TIMEOUT ON RESOURCE ORA-000S1 SQLCODE—51 TOO_MANY ROWS ORA-01422 SQLCODE=-1422 TRANSACTION_BACKED_OUT ORA-00061 SQLCODE—61 VALUE_ERROR ORA-06502 SQLCODE—6502 ZERO_DMDE ORA-01476 SQLCODE—1476 Профамма пытается обратиться к базе данных (обычно посредством инструкции DML) до подключения к СУБД Oracle Внутренняя программная ошибка PL/SQL. В сообщении об ошибке обычно предлагается обратиться в службу поддержки Oracle Программе PL/SQL недостаточно доступной памяти или память по какой-то причине повреждена Тайм-аут с СУБД при ожидании ресурса Инструкция SELECT INTO возвращает несколько строк, хотя может возвращать лишь одну строку; если же их больше одной, можно поместить инструкцию SELECT в явное определение курсора и выбирать (FETCH) строки из него по одной Удаленная часть транзакции отменена либо при помощи явной инструкции ROLLBACK, либо в результате какого-то другого действия (например, неудачного выполнения SQL- или DML-инструкции в удаленной базе данных) Ошибка, связанная с преобразованием, усечением или проверкой ограничений числовых или символьных данных, Это общее и очень распространенное исключение. Если подобная ошибка содержится а инструкции SQL или DML, то в блоке PL/SQL инициируется исключение INVAUD_NUMBER Попытка выполнить в программе деление числа на ноль Вот пример того, как можно использовать эту таблицу исключений. Предполо- Предположим, что ваша программа инициирует необрабатываемое исключение для ошибки ORA-6511. Обнаружив указанную ошибку в таблице, вы видите, что она связана с исключением CURSOR_ALREADY_OPEN. Найдите блок PL/SQL, в котором произошла данная ошибка, и добавьте в него обработчик исключения CURSOR_ALREADY_OPEN: EXCEPTION . WHEN OJRS0R_ALREADY OPEN THEN CLOSE my_cursor: END; Конечно, было бы хорошо проанализировать весь программный код и заранее определить, какие из стандартных исключений в нем могут инициироваться. В та- таком случае вы сможете решить, какие из них следует обрабатывать в отдельности, какие включить в предложение WHEN OTHERS, а какие оставить необработанными. Область действия исключения Область действия исключения — это та часть программного кода, к которой оно относится, то есть блок, где данное исключение может быть инициировано. В при- приведенной ниже таблице указаны области действия исключений четырех возмож- возможных типов.
Определение исключений 159 Тип исключения Область действия Именованное системное Исключение является глобальным, поскольку оно объявлено не исключение в каком-то конкретном блоке кода. Инициировать и обрабатывать системные исключения можно в любом блоке Именованное Это исключение можно инициировать и обрабатывать только в том исключение, исполнительном разделе и разделе исключений, которые входят определяемое в состав блока, где объявлено данное исключение (или в состав программистом любого из вложенных в него блоков). Если это исключение определено в спецификации пакета, его областью действия являются все те программы, владельцы которых имеют на этот пакет привилегию EXECUTE Анонимное системное Данное исключение можно обрабатывать в предложении WHEN исключение OTHERS любого раздела исключений, Если присвоить ему имя, его область действия будет такой же, как у именованного исключения, определяемого программистом Анонимное исключение, Такое исключение определяется в вызове процедуры определяемое RAISE_APPUCATTON_ERROR, а затем передается в вызывающую программистом программу Давайте рассмотрим пример исключения overdue_balance, объявленного в про- процедуре checfc_account (область его действия ограничена указанной процедурой): PROCEDURE check account (company 1d_1n IN NUMBER) IS overdue balance EXCEPTION; BEGIN ... исполняемые операторы ... LOOP '"iF ... THEN RAISE overdue balance; END IF; END LOOP; EXCEPTION WHEN overdue balance THEN ... END: ' С помощью оператора RAISE исключение overdue_balance можно инициировать в процедуре check_account, но не в программе, которая ее вызывает. Например, следующий анонимный блок генерирует ошибку компиляции: DECLARE company 1d NUMBER :- 100; BEGIN check_account A00); EXCEPTION WHEN overdue_balance /* В PL/SQL такая ссылка недопустима */ THEN ... END: PLS-00201: identifier "OVERDUEJALANCE11 must be declared Для приведенного выше анонимного блока процедура check_account является «черным ящиком». Все объявленные в ней идентификаторы, в том числе иденти- идентификаторы исключения, не видимы для внешних программ.
160 Глава б ¦ Обработка исключений Как инициировать исключение Исключение может быть инициировало Oracle при обнаружении ошибки. Его так- также может инициировать программист посредством оператора RAISE или процеду- процедуры RAISE_APPLICATION_ERROR. Как Oracle инициирует исключения, вы уже знаете. Теперь давайте рассмот- рассмотрим механизмы, с помощью которых это может сделать программист. Оператор RAISE Для того чтобы программист имел возможность самостоятельно инициировать именованные исключения, Oracle поддерживает оператор RAISE. С его помощью можно инициировать как собственные, так и системные исключения. Оператор имеет три формы: RAISE иня_исключения: RAISE имя_пакета.иия_исключения: RAISE: Первая форма (без имени пакета) предназначена для инициирования исклю- исключений, определенных в текущем блоке (или в содержащем его блоке), а также для инициирования системных исключений, объявленных в пакете STANDARD. Вот два примера исключений, в первом из которых инициируется исключение, опреде- определенное программистом: DECLARE invalicMd EXCEPTION; -- Все идентификатры должны начинаться с букаы 'X' id_value VARCHAR2: BEGIN idj/alue :- 1d_for ('SMITH'); IF SUBSTR Ad_value, 1. 1) !- 'X' THEN RAISE invalided; END IF; END; Если нужно, в любой момент можно инициировать системное исключение: BEGIN IF total_sales - О THEN RAISE ZERCHUVIDE; -- Определено в пакете STANDARD ELSE RETURN (sales_percerrtage_calculation Cmy_sales. total sales)); END IF; END: Если исключение объявлено в пакете (но не в STANDARD) и инициируется извне, имя исключения нужно уточнить именем пакета: IF da.ys_overdue (isbn_in. borroweMn) > 365 THEN RAISE overdue pkg.book_is_lost; END IF:
Как инициировать исключение ' 161 Третья RAISE не требует указывать имя исключения, но используется только в предложении WHEN раздела исключений. Ее синтаксис прост: RAISE: Этой формой оператора рекомендуется пользоваться, когда в обработчике ис- исключений нужно повторно инициировать (или передать) то же самое исключение: EXCEPTION WHEN NO_DATA_FOUND THEN -- Используем общий пакет для записи всей информации о -- контексте, такой как код ошибки, имя программы и т. д. erriog.putline (companyjdjn); -- А теперь передаем исключение NO_DATA_FQUND -- в родительский блок, не обрабатывая его RAISE; Эта возможность особенно полезна в тех случаях, когда информацию об ошиб- ошибке нужно записать в журнал, а сам процесс обработки ошибки возложить на ро- родительский блок. Таким образом определяется, где именно произошла ошибка, и при этом завершается работа одного или нескольких родительских блоков, без потери информации об ошибке. Инициирование исключений во вложенных блоках Когда исключение объявлено в блоке, оно является локальным для этого блока, но глобальным для всех вложенных в него блоков. В следующем варианте приве- приведенной выше процедуры check_accaunt содержится анонимный блок, в котором также инициируется исключение overdue_balance. И поскольку данный аноним- анонимный блок определен внутри блока процедуры, в нем допускается ссылка на это исключение: PROCEDURE check_account (company id in IN NUMBER) IS overdue_balance EXCEPTION; BEGIN - ... исполняемые операторы ... -- Начало вложенного блока BEGIN ... операторы во вложенной блоке ... RAISE overdue_balance: -- Исключение инициируется во вложенной блоке END: -- Конец вложенного блока LOOP IF ... THEN RAISE overdue balance: -- Исключение инициируется в родительской блоке ENO IF: ENO LOOP: EXCEPTION WHEN overdue_balance THEN ... -- Исключение обрабатывается в родительском блоке END;
162 Глава б ¦ Обработка исключений Когда исключение overdue_bal ance инициируется во вложенном или родитель- родительском блоке, управление немедленно передается последнему, где содержится един- единственный в процедуре раздел обработки исключений. А поскольку исключение overdue_balance объявляется для всей процедуры, во вложенных блоках к нему можно обращаться по имени. Использование процедуры RAISE_APPLICATION_ERROR Для инициирования исключений, специфических для приложения, Oracle пре- предоставляет процедуру RAISE_APPLICATION_ERROR (определенную в используемом по умолчанию пакете DBMS_STANDARD), Ее преимущество перед оператором RAISE (ко- (который тоже может инициировать специфические для приложения явно объяв- объявленные исключения) заключается в том, что она позволяет связать с исключени- исключением сообщение об ошибке. При вызове этой процедуры выполнение текущего блока PL/SQL прекраща- прекращается и любые изменения аргументов OUT и IN OUT (если таковые имеются) отменя- отменяются. Изменения, внесенные с помощью инструкции INSERT, UPDATE или DELETE в гло- глобальные структуры данных, такие как переменные пакетов и объекты баз данных, не отменяются. Для отката DML-инструкций необходимо явно указать в разделе обработки исключений оператор ROLLBACK. Вот заголовок процедуры RAISE_APPLICATION_ERROR: PROCEDURE RAISE_APPLICATION_ERROR ( пит Mnaryjnteger. msg varchar2. keeperrorstick boolean default FALSE): Здесь пит - это номер ошибки из диапазона от -20 999 до -20 000 (вы только по- подумайте: все остальные отрицательные числа, которые больше -20 000, Oracle ис- использует для собственных исключений!); msg - это сообщение об ошибке, длина которого не должна превышать 2048 символов (символы, выходящие за эту гра- границу, игнорируются); аргумент keeperrorstack указывает, хотите вы добавить ошибку к тем, что уже имеются в стеке (TRUE), или заменить существующую ошиб- ошибку (значение по умолчанию — FALSE). ПРИМЕЧАНИЕ- Предполагается, что Orade для обозначения пользовательских ошибок выделила диапазон номеров от -20 999 до -20 000, но учтите, что в нескольких встроенных пакетах, в том числе в DBMS_OUTPUT и DBMSJ3ESCRIBE, номера от-20 005 до -20 000 по-прежнему присваиваются системным ошибкам. Не очень удобно, конечно, но дело обстоит именно так. Более подробно об их использовании вы уз- узнаете из документации пакетов. А теперь давайте рассмотрим одну из возможных областей применения этой встроенной процедуры. Предположим, нам требуется, чтобы сообщения об ошиб- ошибках выдавались пользователям на разных языках. Создадим для них таблицу ег- ror_table и определим в ней язык каждого сообщения значением столбца string
Обработка исключений 163 language. Затем создадим процедуру, которая генерирует заданную ошибку, счи- считывая соответствующее сообщение с учетом значения параметра NLS_LANGUAGE: /* Файл в Web: raise by language.sp */ CREATE OR REPLACE PROCEDURE ra1se_byjanguage (codejn IN PLSJNTEGER) IS 1 message errorjable.error string; BEGIN SELECT error_str1ng INTO Ijnessage FROM error_table, v$nls_parameters v WHERE errorjnumber - codejn AND string language - vTvALUE AND v.parameter - 'NLS.LANGUAGE'; RAISE APPLICATION ERROR (code 1n. 1 message); END; При использовании процедуры RAISE_APPLICATION_ERROR вы сами определяете и номер ошибки, и поясняющее ее сообщение. Правда, это может вызвать у вас некоторую растерянность, и вы наверняка будете действовать по принципу: «Сом- «Сомневаюсь, что кто-нибудь уже задал номер -20 774, воспользуюсь им!» Для того чтобы облегчить себе управление кодами ошибок и обеспечить своих коллег со- согласованным интерфейсом для обработки серверных ошибок, имеет смысл соз- создать таблицу для хранения всех выделенных для нужд разработчиков номеров ошибок (-20 NNN) и связанных с ними имен исключений и сообщений об ошиб- ошибках. Тогда любой программист сможет просмотреть уже определенные вами или членами вашей команды ошибки и найти те из них, которые подходят для теку- текущей ситуации. Пример такой таблицы вы найдете в файле msginfo.prg на узле O'Reilly. Там же имеется код, генерирующий пакет с объявлениями всех «зареги- «зарегистрированных» исключений. Обработка исключений Как только инициируется исключение, нормальное выполнение блока PL/SQL останавливается и управление передается в раздел исключений. Затем исключе- исключение либо обрабатывается обработчиком исключений в текущем блоке PL/SQL, либо передается в родительский блок. Для того чтобы обработать или перехватить исключение, нужно написать для него обработчик. Обработчики исключений располагаются после исполняемой части блока, но перед завершающим его ключевым словом END. Начало блока ис- исключений отмечает ключевое слово EXCEPTION: DECLARE ... объявления .. . BEGIN ... исполняемые операторы . .. [ EXCEPTION . ' ... обработчики исключении ...] END;
164 Глава 6 • Обработка исключений Синтаксис обработчика исключений может быть таким: WHEN имя исключения [OR имя_исключения . ,. ] THEN исполняемые операторы или таким: WHEN OTHERS THEN исполняемые операторы В одном разделе исключений может быть несколько их обработчиков. Обработ- Обработчики структурируются подобно условному оператору CASE. EXCEPTION WHEN N0_DATA_FOUND -- Если инициировано исключение NO_DATA_FOUND. THEN -- то выполнить первый набор исполняемых операторов испопняеныеопера торы1 WHEN paymentoverdue -- Если просрочена оплата (payment_overdue), THEN -- то выполнить второй набор исполняемых операторов иаюлияеиые_опера торы2 WHEN OTHERS -- Если инициировано иное исключение, THEN -- то выполнить третий набор исполняемых операторов испотяенывоперг торыЗ END; Если имя, заданное в предложении WHEN, совпадает с инициированным исклю- исключением, то это исключение обрабатывается соответствующим набором операто- операторов. Обратите внимание, что таким образом исключения перехватываются толь- только исходя из имен, а не из кодов ошибок. Но если инициированное исключение не имеет имени или его имя не соответствует ни одному из имен, указанных в предложениях WHEN, тогда его обработка выполняется посредством операторов, заданных в предложении WHEN OTHERS (если таковое имеется). Любая ошибка мо- может быть перехвачена только одним обработчиком исключений. После выполне- выполнения операторов этого обработчика управление сразу же передается из текущего блока в родительский или вызывающий блок. Предложение WHEN OTHERS не является обязательным. Когда оно отсутствует, все необработанные исключения немедленно передаются в родительский блок, если таковой имеется. Предложение WHEN OTHERS должно быть последним обработ- обработчиком исключений в блоке. Если поместить после него еще одно предложение WHEN, компилятор выдаст следующее сообщение об ошибке: PLS-00370: OTHERS handler must be last among the exception handlers of a block Объединение нескольких исключений в одном обработчике В одном предложении WHEN, используя оператор OR, можно объединить несколько исключений — точно так же, как с его помощью объединяются логические выра- выражения: WHEN inval1d_company_id OR negative_balance THEN
Обработка исключений 165 В одном обработчике можно также комбинировать имена пользовательских и системных исключений: WHEN ba1ance_too_low OR lEROJIVDIOE OR D3MS_LDAP.INVALI0_SESSI0N THEN А вот применить оператор AND в этой конструкции нельзя, поскольку одновре- одновременно не может быть инициировано несколько исключений. Необработанные исключения Исключение, инициированное в программе, но не обработанное в соответствую- соответствующем разделе текущего или родительского блока PL/SQL, называется, как вы уже знаете, необработанным. PL/SQL возвращает сообщение об ошибке, вызвавшей необработанное исключение, в ту среду, где была запущена данная программа. Эта среда (ею может быть SQL*Plus, Oracle Forms или программа на языке Java) производит сообразные с ситуацией действия. В частности, SQL*Plus осуществ- осуществляет откат (ROLLBACK) всех DML-инсгрукций, выполненных в родительском блоке. Одним из важнейших моментов, связанных с проектированием архитектуры приложения, является вопрос о том, разрешается ли в нем использовать необра- необработанные исключения. Такие исключения различными средами обрабатываются по-разному, и не всегда это делается корректно. Если ваша программа PL/SQL вызывается не из PL/SQL-среды, в ее «самом внешнем» блоке можно запрограм- запрограммировать следующие действия: О перехват любых исключений, которые могут быть переданы в хост-среду; О запись информации об ошибке в журнал, с тем чтобы впоследствии ее мог проанализировать разработчик; О возврат кода состояния, описания и другой информации, необходимой хост- среде для выбора оптимального варианта действий. Использование функций SQLCODE и SQLERRM в обработчиках исключений Как вы уже знаете, предложение WHEN OTHERS используется для перехвата исклю- исключений, не указанных в предложениях WHEN. Однако в этом обработчике вам тоже нужна информация о том, какая именно ошибка произошла. Для ее получения можно воспользоваться функцией SQLCQDE, возвращающей номер текущей ошиб- ошибки (значение 0 указывает, что в стеке ошибок нет ни одной ошибки). Еще одна полезная функция, SQLERRM, возвращает поясняющее сообщение для текущей или для указанной разработчиком ошибки. Таким образом, механизм обработки конкретных исключений без использова- использования директивы EXCEPTIONJNIT включает в себя предложение WHEN OTHERS и вызов функции SQLCODE. Рассмотрим пример, в котором перехватываются два исключе- исключения, инициированные ошибками -2292 и -2291, и выполняется их обработка: PROCEDURE delete company (company id in IN NUMBER) IS BEGIN DELETE FROM company WHERE companyjd - companyjdjn:
166 Глава б • Обработка исключений EXCEPTION WHEN OTHERS THEN /* || Анонимный блок внутри обработчика исключения позволяет | объявить локальные переменные для хранения информации об ошибке */ DECLARE error_code NUMBER ;- SQLCODE: /* Максимальная длина строки, возвращаемой функцией SQLERRM */ errorjnsg VARCHAR2 E12) :- SQLERRM: ' BEGIN IF error_code - -2292 THEN /* Ииеются дочерние записи, Удалим их токе! */ DELETE FROM employee WHERE companyjd - company_1d_1n; /* Теперь удалим родительскую запись */ DELETE FROM company WHERE companyjd - company_1d_1n; ELSIF error_code - -2291 THEN /* Клеч родительской записи не найден */ DBMS_OIJTPUT.PUT LINE (' Неправильный идентификатор компании:' 11ТО CHAR (companyJd 1n)>: ELSE /* Это что-то вроде WHEN OTHERS внутри WHEN OTHERS! */ DBMS_OUTPUT.PUT_LINE С Ошибка при удалении информации о компании:' || error msg): END IFi END: -- Конец анонимного блока END delete_company: Продолжение работы после инициирования исключения Когда в блоке PL/SQL инициируется исключение, нормальная работа програм- программы прерывается и управление передается в раздел исключений. Однако и после обработки исключения управление не будет возвращено в тот блок, в котором оно было инициировано. А как же быть в тех случаях, когда необходимо обрабо- обработать исключение и продолжить работу, начиная с того места, где одно произошло? Предположим, нам нужно написать процедуру, которая выполняет последова- последовательность DML-инструкций по отношению к разным таблицам (удаляет данные из одной таблицы, обновляет другую, добавляет строки в третью). Первая версия этой процедуры может быть примерно такой: PROCEDURE change data IS BEGIN DELETE FROM employee WHERE ...: UPDATE company SET ...; INSERT INTO company_h1story SELECT * FROM company WHERE ...; END:
Обработка исключений 167 Данная процедура содержит все необходимые DML-инструхции. И хотя они выполняются последовательно, на самом деле логически не зависят друг от друга. А нам требуется, чтобы в случае неудачной попытки удалить записи выполня- выполнялись инструкции UPDATE и INSERT. При использовании приведенной версии процедуры change_data не гарантиру- гарантируется, что будет предпринята попытка выполнить каждую из трех инструкций. Если при обработке инструкции DELETE инициируется исключение, выполнение всей программы прекращается и управление передается в раздел исключений, если таковой имеется. Оставшиеся SQL-инструкции вообще не выполняются. Как же сделать так, чтобы выполнение программы продолжалось во время инициирования и обработки исключений. Одним из вариантов решения может быть размещение инструкции DELETE в собственном блоке. Рассмотрим такую вер- версию программы change_data: PROCEDURE change data IS BEGIN BEGIN DELETE FROM employee WHERE ...; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN UPDATE compare SET ... ; EXCEPTION WHEN OTHERS THEN NULL: END; BEGIN INSERT INTO company history SELECT * FROM company WHERE EXCEPTION WHEN OTHERS THEN NULL; END; END: 5 этой программе, если при выполнении инструкции DELETE инициируется ис- исключение, управление немедленно должно быть передано в раздел обработки ис- исключений. Но в какой? Поскольку инструкция DELETE находится теперь в отдель- отдельном блоке, у нее может быть собственный раздел исключений. Предложение WHEN OTHERS мгновенно обрабатывает эту ошибку. Затем управление передается из бло- блока инструкции DELETE процедуре change_data. Далее выполнение рассматриваемого родительского блока продолжается, об- обрабатывается очередная SQL-инструкция процедуры. Мы входим в следующий анонимный блок - на этот раз в блок инструкции UPDATE. Если она выполняется неудачно, исключение перехватывает обработчик WHEN OTHERS раздела исключений блока инструкции UPDATE, после чего управление снова возвращается процедуре change_data и выполняется инструкция INSERT - тоже в собственном анонимном блоке. На рис. 6.2 описанный процесс продемонстрирован на примере двух по- последовательных инструкций DELETE. Итак, подведем итог. Инициированное исключение всегда обрабатывается в те- текущем блоке, если, конечно, в нем имеется соответствующий обработчик. Вокруг любого оператора (или группы операторов) можно создать «виртуальный блок»,
168 Глава 6 • Обработка исключений поместив перед ним ключевое слово BEGIN, а после него — раздел исключений и ключевое слово END. Таким образом можно указывать, выполнение каких частей кода приложения в случае инициирования исключения должно быть прекраще- прекращено, а каких, наоборот, продолжено. Можно сказать, что при этом создаются свое- своеобразные «буфера» для хранения исключений в виде анонимных блоков с крити- критическим кодом. Одно исключение — и выполнение программы прекращается BEGIN DELETE /•— FROM tablel; f DELETE ( FROM table? V EXCEPTION END; После исключения управление переда следующей инструкции BEGIN ^—DELETE FROM tablel: С EXCEPTION ^---•.WHEN OTHERS THEN NULL: END: BEGIN ^ DELETE FROM table2 С EXCEPTION ^-—¦•WHEN OTHERS THEN NULL; END: Рис 6.2. Выполнение двух последовательных инструкций DELETE с использованием разных подходов к определению области действия их исключений Данную стратегию можно развить, поместив критический код в отдельные про- процедуры и функции. В таких именованных блоках PL/SQL тоже могут быть собст- собственные разделы исключений, защищающие программу от общего сбоя. Одним из важных преимуществ использования процедур и функций является то, что они скрывают от основной программы конструкцию BEGIN... EXCEPTION... END, загромо- загромождающую ее и затрудняющую восприятие основной логики. Такую, более струк- структурированную, программу легче читать, понимать и многократно использовать в раз- разных ситуациях. Передача необработанного исключения В программе инициированное исключение распространяется в соответствии с оп- определенными правилами. Сначала PL/SQL ищет обработчик исключения в теку- текущем блоке (анонимном блоке, процедуре или функции). Если такового нет, ис- исключение передается в родительский блок. Затем PL/SQL пытается обработать исключение, инициировав его еще раз в родительском блоке. И так в каждом внешнем по отношению к другому блоке до тех пор, пока все они не будут исчер- исчерпаны (рис. 6.3). После этого PL/SQL возвращает необработанное исключение в среду приложения, из которого был выполнен «самый внешний» блок PL/SQL. И только теперь исключение сможет остановить выполнение хост-программы. EXCEPTION "EXCEPTION :=>EXCEPTION - Множество необработанных исключений Рис. 6.3. Передача исключений во вложенных блоках
Обработка исключений 169 Потеря информации об исключении Структура процесса обработки локальных, определяемых программистом, исклю- исключений в PL/SQL такова, что можно легко потерять информацию об исключении (то есть о том, какая именно произошла ошибка). Проиллюстрируем это на при- примере. Мы объявили исключение BEGIN «local_block» DECLARE case_is_not made EXCEPTION; BEGIN END local_bloclc; При этом мы забыли включить в данный блок раздел обработки исключений. Область действия исключения case_1s_notjnade ограничена блоком local_block. Если указанное исключение не обрабатывается в данном блоке, оно передается в родительский, где нет никакой информации о нем. Мы знаем только то, что произошла ошибка, а какая именно — неизвестно. Объясняется это тем, что у всех пользовательских исключений один и тот же номер ошибки, 1, и одно и то же сообщение, «User-defined error» (ошибка, определяемая пользователем). Как видите, не слишком информативно. Уникально только имя исключения, но вне блока localblock оно не видимо. Следовательно, используя локально объявлен- объявленное (и инициированное) исключение, его обработчик необходимо всегда добав- добавлять в тот же блок, в котором оно объявлено, и обрабатывать это исключение ис- исходя из его имени. Примеры передачи исключения Теперь давайте рассмотрим несколько примеров передачи исключений через внеш- внешние блоки. На рис. 6.4 показано, как исключение too_nany_fau1ts, инициированное во внутреннем блоке, обрабатывается в следующем блоке, внешнем. «Самый внут- внутренний» блок (на рисунке — вложенный блок 2) содержит раздел исключений, так что PL/SQL сначала проверяет, обрабатывается ли в этом разделе иницииро- инициированное исключение toojnany_fau1ts. А поскольку оно не обрабатывается, PL/SQL закрывает данный блок и инициирует исключение too_many_faults во внешнем блоке, обозначенном на рисунке как вложенный блок 1. (Исполняемые операто- операторы, расположенные после вложенного блока 2, не выполняются.) Затем просмат- просматривается раздел исключений этого блока с целью поиска обработчика исключе- исключения too_many_ faults, который обрабатывает его и передает управление процедуре listjny_faults. Обратите внимание, что если исключение NO_DATA_FOUND будет инициировано в «самом внутреннем» блоке, то оно будет обработано в разделе исключений это- этого же блока. Затем управление будет передано во вложенный блок 1 и будут вы- выполнены исполняемые операторы, расположенные после вложенного блока 2. На рис. 6.5 мы видим пример обработки в «самом внешнем» блоке исключе- исключения, инициированного во внутреннем блоке. Внешний блок — это единственный блок, имеющий раздел исключений, поэтому, когда во вложенном блоке 2 ини- инициируется исключение toojnany_faults, PL/SQL прекращает выполнение этого блока и инициирует данное исключение в его родительском блоке, вложенном блоке 1. Но поскольку и у него нет раздела исключений, управление передается
170 Глава б • Обработка исключений «самому внешнему» блоку, процедуре 1ist_my_fau"lts. В указанной процедуре име- имеется раздел исключений, поэтому PL/SQL проверяет его, находит обработчик ис- исключения toomanyfaul ts, выполняет имеющийся там код и возвращает управление программе, вызвавшей процедуру I1st_nv/_faults. PROCEDURE I1st_my_faults IS BEGIN DECLARE too many_faults EXCEPTION BEGIN " ... Исполняемые операторы расположенные перед новый блоком ... BEGIN * SELECT SUM (faults) INTO num_ faults FROM profile ...: IF mm_faults > 100 THEN ^- RAISE too many_faults: END IF; EXEPTION WHEN NO_DATA_FOUND THEN ...: END: ... Исполняемые операторы, расположенные после вложенного блока 2 ... ч EXEPTION WHEN toojnany faults THEN END; ~ ¦ вложенный 6лок1 Вложенный блок г END list_my_faults: Рис. 6.4. Передача исключения в первый вложенный блок PROCEDURE Hst_my_faiilts IS too many faults EXCEPTION; BEGIN " BEGIN BEGIN SELECT SUM (faults) INTO numjaults FROM profile ...; IF num_faults > 100 THEN RAISE too many faults; END IF; END; - Вложенный бла*1 - Вложенный блок 2 EXEPTION WHEN too_many_faults THEN ...; END I1stjny_faults: Рис 6.5. Исключение, инициированное во вложенном блоке, обрабатывается в «самом внешнем» блоке
Обработка исключений 171 Использование стандартизированных программ обработки ошибок Обязательным элементом любого созданного профессионалами приложения яв- является надежная и согласованная схема обработки ошибок. Согласованность в этом вопросе важна как для пользователя, так и для разработчика. Если в случае воз- возникновения ошибки пользователю предоставляется понятная, хорошо структу- структурированная информация, он сможет более подробно рассказать об ошибке служ- службе поддержки. К тому же работая с таким обстоятельным приложением, он будет увереннее себя чувствовать. Если приложение всегда обрабатывает и протоколи- протоколирует ошибки определенным образом, программистам, занимающимся его поддерж- поддержкой и сопровождением, будет легче их найти и устранить. Все сказанное кажется убедительным и совершенно очевидным, не так ли? К сожалению, на практике, особенно в больших командах разработчиков, все про- происходит несколько иначе. Очень часто каждый разработчик идет своим путем, следуя личным принципам и приемам, записывая информацию в журналы собст- собственной структуры и т. д. Одним словом, без стандартизации отладка и сопровож- сопровождение приложений превращаются в кошмар. Рассмотрим пример типичного кода: EXCEPTION WHEN NOJATA FOUND THEN vjnsg :- 'Нет компании с данным идентификатором1 || TO_CHAR (v 1d): v_err :- SQLCODE; v_prog :- 'fixdebt': IN5ERT INTO en-log VALUES (v_err. v msg. v_prog, SYSDATE. USER): WHEN OTHERS THEN v_err :- SQLCODE: vjnsg -.- SQLERRM; v_prog :- 'fixdebt'; INSERT INTO errlog VALUES (v_err. vjnsg. vprog, SYSDATE. USER); RAISE: На первый взгляд этот код может показаться абсолютно нормальным. Вот как можно пояснить то, что он делает. Если компания с данным идентификатором не найдена, получаем значение SQLCODE, задаем имя программы, сообщение и записы- записываем строку с информацией об ошибке в таблицу ошибок. Работа родительского блока продолжается, поскольку ошибка в данном случае не очень серьезна. Если происходит любая другая ошибка, получаем ее код и соответствующее сообще- сообщение, задаем имя программы и записываем строку с информацией об ошибке в таб- таблицу ошибок, а затем передаем исключение в родительский блок, чтобы остано- остановить его выполнение (поскольку неизвестно, насколько серьезна эта ошибка). Так что же здесь не так? Чтобы подробно объяснить суть проблемы, достаточ- достаточно взглянуть на код. В нем одинаково жестко закодированы все действия по обра- обработке ошибок. В результате код, во-первых, получается большим, а во-вторых, его придется полностью переписывать в случае изменения схемы обработки ошибок. Обратите внимание еще и на тот факт, что информация об ошибке записывается
172 Глава б • Обработка исключений в таблицу базы данных. Это означает, что запись в журнале становится частью логической транзакции. И если потребуется выполнить откат транзакции, строки журнала ошибок будут утеряны. Существует несколько способов избежать потери информации. В частности, можно записывать данные в файл, использовать автономные транзакции, что даст возможность помещать данные в журнал вне основной транзакции. Но все равно код в случае его изменения придется исправлять в сотнях разных программ. А теперь посмотрите, как этот же раздел исключений можно переписать, ис- используя стандартизированный пакет: EXCEPTION WHEN NO_DATA_FOUND THEN errpkg.record_and_continue ( SQLCODE, 'Нет компании с данным идентификатором' || TO_CHAR tv_id)): WHEN OTHERS THEN errpkg.record_and stop; END; Такой пакет обработки ошибок скрывает все детали реализации процесса. Но достаточно просмотреть спецификацию пакета и станет ясно, какую из процедур обработки ошибок требуется использовать в каждом конкретном случае. Если нужно записать информацию об ошибке и продолжить работу родительского бло- блока, вызываем программу record_and_continue, а если нужно записать информацию об ошибке и остановить работу родительского блока, то обращаемся к record_ and_stop. Мы не знаем, как эти программы записывают информацию об ошибке, как они останавливают работу родительского блока, то есть передают исключение, но для нас это и не важно. Главное, что все происходит так, как определено стан- стандартами приложения. Теперь можно больше времени уделить разработке более интересных элементов приложения и не заниматься административной рутиной. На узле O'Reilly имеется файл errpkg.pkg, в котором вы найдете прототип стан- стандартизированного пакета для обработки ошибок. Правда, прежде чем использо- использовать его в приложениях, вам нужно будет завершить его реализацию, что поможет составить ясное представление о том, как конструировать подобные утилиты. Можете также просмотреть пакет plvexc библиотеки PL/Vision, который включа- включает свыше 60 пакетов, входящих в состав Quest Software PL/SQL Knowledge Ex- Expert. В указанном пакете имеется полная функционирующая реализация универ- универсального компонента обработки ошибок, предназначенного для многократного использования.
Часть III Работа с данными в PL/SQL Любая программа оперирует данными, причем значительная часть этих данных локальна, то есть определена в процедурах или функциях программы. В этой час- части книги описаны различные типы программных данных, которые используются в PL/SQJ., - числа, строки, записи и коллекции. Вы также узнаете о новых типах данных, введенных в 0racle9i (INTERVAL, TIMESTAMP, XMLType). Кроме того, в главах 7-12 рассматриваются встроенные функции Oracle, предназначенные для управ- управления данными и их преобразования. ? Глава 7. Работа с программными данными ? Глава 8. Строки ? Глава 9. Числа ? Глава 10. Дата и время ? Глава 11. Записи и коллекции П Глава 12. Другие типы данных
7 Работа с программными данными > Именование программных данных > Обзор типов данных PL/SQL ^ Объявление программных данных > Подтипы данных, определяемые программистом > Преобразование типов данных Вероятно, в любом написанном вами блоке программы PL/SQL вы будете опре- определять данные и выполнять с ними те или иные операции. Программные данные представляют собой структуры, которые существуют только в рамках сеанса PL/SQL и не хранятся в базе данных. Физически они располагаются в программ- программной глобальной области - Program Global Area (PGA). К программным данным относятся: О переменные и константы — значения переменных могут изменяться во время выполнения программы, а значения констант статичны, то есть устанавлива- устанавливаются при их объявлении и больше не изменяются; О скалярные и составные данные - скалярные данные состоят из одного значе- значения (числового или строкового), а составные данные имеют несколько значе- значений, как например запись, коллекция или экземпляр объектного типа; О контейнеры - могут содержать информацию, полученную из базы данных или иного источника. В настоящей главе мы расскажем о том, как объявляются данные в программе, и опишем правила, которым нужно следовать при выборе имен для этих данных. Затем рассмотрим все типы данных, поддерживаемые PL/SQL, и обсудим кон- концепцию преобразования типов. Завершается глава рекомендациями о том, как лучше работать с программными данными. Конкретные типы данных подробно описаны в остальных главах этой части.
Именование программных данных 175 Именование программных данных Перед использованием переменную или константу нужно объявить, присвоив ей имя и определив тип. Задавая имена структур данных, следуйте приведенным ниже правилам PL/SQL (они касаются также имен объектов базы данных, на- например таблиц или столбцов): О имя может иметь длину не более 30 символов; О оно должно начинаться с буквы и может состоять из букв, цифр, а также сим- символов «$», «#» и «_»; 0 имена нечувствительны к регистру (если только не заключены в двойные ка- кавычки). Согласно этим правилам допустимыми являются имена: 1 total_count T total_#_of_trees salary_in_$ Следующие три имени допустимы, однако считаются в PL/SQL идентичными из-за нечувствительности языка к регистру имен: d1apers_changed weekl DIAPERS_CHANGEDTwEEK1 D1apers_Changed_Weekl Приведенные ниже имена недопустимы: lst_account -- Имя начинается с цифры, а не буквы favor1te_1ce_cream_f1avours_that_dont_conta1n_nuts -- Слишком длинное имя eraa1l_address8bus1nessjoc ~ -- Имя содержит недопустимый символ 9 Из этих правил есть несколько исключений. Если имя в объявлении заклю- заключить в двойные кавычки, можно не придерживаться данных правил, кроме одно- одного: длина имени не должна превышать 30 символов. Например, допустимыми яв- являются следующие объявления: DECLARE ¦ "trulyjower_case" INTEGER: " DATE: -- Да-да, это имя состоит из пяти пробелов! 23 go!" VARCHAR2C10): BEGIN 23 до!" :- 'Steven': END: Когда вы указываете в исполняемом разделе эти имена, их всегда нужно за- заключать в двойные кавычки, иначе код не будет компилироваться без ошибок. При создании объектов базы данных в именах иногда используют двойные ка- кавычки, поскольку это позволяет сохранить в идентификаторах исходный регистр. Например, если в программе создать таблицу "docs"; она будет называться имен- именно docs, а не DOCS. Однако в любых других случаях лучше избегать применения двойных кавычек в программах PL/SQL. Еще одно исключение из правил именования данных связано с именами объек- объектов Java (подробнее об этом рассказывается в главе 22).
176 Глава 7 • Работа с программными данными Выбирая имена для переменных и констант, придерживайтесь следующих ре- рекомендаций. О Убедитесь, что имя соответствует назначению объекта и по нему можно бы- быстро определить это назначение. Попытайтесь сформулировать, что пред- представляет собой значение переменной, это поможет выбрать для нее более точ- точное имя. Например, если в переменной хранится общее количество звонков по поводу остывшего кофе, для нее подойдет имя total_cans_on_cold_coffe или total_cold_ca"ns. Имена totcoffe и t_#_callsjwcoff неудачны, поскольку со- составлены из сокращений, которые будут непонятны как постороннему челове- человеку, так и вам через некоторое время. О Выработайте определенные соглашения об именовании объектов и следуй- следуйте км. Соглашения обычно предполагают использование префиксов и суф- суффиксов, указывающих тип и назначение переменных. Например, имена всех локальных переменных должны иметь префикс *\_», а глобальных перемен- переменных, определенных в пакетах, - префикс «g_». Для записей применяется суф- суффикс «_П> и т. д. С полным набором соглашений об именах можно ознако- ознакомиться на узле O'Reilly (http://orade.orellly.com), щелкнув сначала на ссылке Oracle PL/SQL Best Practices, а затем на ссылке Examples. Обзор типов данных PL/SQL При объявлении переменной или константы необходимо определить ее тип дан- данных. (PL/SQL, за некоторыми исключениями, сильнотипизированный язык. Ни- Ниже поясняется, что это означает.) В PL/SQL определен широкий набор скаляр- скалярных и составных типов данных, кроме того, начиная с версии Огас1е8 можно соз- создавать пользовательские {абстрактные) типы данных. В PL/SQL практически все предопределенные типы данных объявлены в па- пакете STANDARD. Например, следующие операторы объявляют тип данных BOOLEAN и два числовых типа данных: create or replace package STANDARD 1s type BOOLEAN is (FALSE. TRUE): type NUMBER is NUMBER_BASE; subtype INTEGER 1s NUMBERC38): PL/SQL поддерживает распространенный «джентльменский набор» типов дан- данных, а также ряд других типов. В этом разделе приводится краткий обзор различ- различных предопределенных типов данных, их более подробное описание можно найти в главах 8-12, 14 и 21. ЧТО ОЗНАЧАЕТ СИЛЬНОТИПИЗИРОВАННЫЙ <Сильнотипизированный язык программирования — это язык, в котором каждый тип данных (целочисленный, символьный и т. д.) предопределен как часть языка и должен назначаться всем константам и вем переменным,
Обзор типов данных PL/SQL *¦/1 используемым в программе. Каждая операция допускается только для кон- конкретных типов данных. Компилятор требует обязательного определения типов данных и соответствующего их применения. Преимуществом стро- строгой типизации является то, что программист должен соблюдать опреде- определенные правила, и тем самым гарантируется согласованность результа- результатов. Ее недостаток состоит в том, что программист не может использовать типы данных, которых нет в стандартном наборе типов языка, и ограничи- ограничиваются возможности творческого применения типов данных.» (Это опре- определение приводится на www.whatls.com.) Символьные типы данных PL/SQL поддерживает строки фиксированной и переменной длины, состоящие как из традиционных символов, так и из символов Unicode. К строкам первого вида относятся строки типов CHAR и NCHAR, а к строкам второго вида — типов VAR- CKAR2 и NVARCHAR2. Объявление строки переменной длины, которая может содер- содержать до 2000 символов, имеет следующий вид: DECLARE 1_асс1 derrt_descr1 pt1 on VARCHAR2C2000); Правила работы с символьными данными, примеры их применения и встроен- встроенные функции, предназначенные для операций со строками в PL/SQL, описаны в главе 8. В базе данных до версии Огас1е8 поддерживаются также большие строки (тип данных LONG), а в версии OracleS и выше — большие объекты (LOB, Large Objects). Эти типы позволяют хранить и обрабатывать очень большие объемы данных (LOB может содержать до 4 Гбайт информации). Символьные типы данных LOB называются CLOB (Character Large Object — большой символьный объект) и NCLOB (National Language Support Character Large Object — большой символьный объект с поддержкой национальных языков). СОВЕТ' Существует множества правил, ограничивающих использование типа данных LONG. Мы не реко- рекомендуем применять его в версиях начиная с Огас1е8 и выше. В главе 12 рассматриваются правила работы с большими объектами, приво- приводится много примеров их применения, а также описываются встроенные функ- функции и пакет DBMS LOB, предназначенные для обработки таких объектов средствами PL/SQL. Числовые типы данных В PL/SQL имеются как действительные, так и целочисленные типы данных. Все они основаны на одном базовом типе NUMBER. Тип данных INTEGER поддерживает лишь целые числа, его разновидностями являются типы NATURAL и POSITIVE. Ниже приведен пример объявления числовой переменной, которая может иметь только три значения A, -1 и NULL).
178 Глава 7 • Работа с программными данными DECLARE I_d1rect1on SIGNTYPE; Правила работы с числовыми данными, примеры их применения и встроен- встроенные функции, предназначенные для операций с числами в PL/SQL, описываются в главе 9. Типы данных DATE TIMESTAMP и INTERVAL До появления версии OracleSh мир дат в Oracle был ограничен типом данных DATE, который использовался для хранения даты и времени. В Oracle9t были введены два новых типа данных, INTERVAL и TIMESTAMP, значительно расширившие возмож- возможности разработчиков программ на PL/SQL в отношении операций с датами и вре- временем, а также вычисления и хранения временных интервалов. Их использова- использование продемонстрировано в функции, вычисляющей возраст человека: CREATE OR REPLACE FUNCTION age (dob 1n IN DATE) RETURN INTERVAL YEAR TO MONTH IS retval INTERVAL YEAR TO MONTH; BEGIN RETURN (SYSDATE - dobjn) YEAR TO MONTH: END; Правилам работы со значениями даты, времени и интервалами времени по- посвящена глава 10, в ней также приведено много примеров и описаны соответст- соответствующие встроенные функции. Тип данных BOOLEAN PL/SQL поддерживает логический (булев) тип данных — BOOLEAN. Переменные этого типа могут принимать одно из трех значений - TRUE, FALSE или NULL - и по- позволяют писать понятный, легко читающийся код даже в случаях, когда он содер- содержит очень сложные логические выражения. Пример объявления логической пе- переменной с присваиванием ей значения по умолчанию приведен ниже: DECLARE 1_е11g1Ы e_for_d1scount BOOLEAN :- customer Imbalance > min balance AND customerJn.pref_type - 'MOST FAVORED1 AND customerjn.d1sc_e11g1b1Hty; Работа с логическими данными и примеры их использования описаны в главе 12. Двоичные данные Oracle поддерживает несколько форм двоичных данных, которые являются не- неструктурированными, не интерпретируются и не обрабатываются Oracle. Для этой цели служат типы RAW, LONG RAW, BFILE и BL08. Тип BFILE используется для двоичных данных, которые хранятся в виде файлов в операционной системе вне базы дан- данных. Тип RAW — это тип данных переменной длины, при операциях с которыми Oracle не выполняет преобразование символов. В остальном он подобен символь- символьному типу VARCHAR2.
Обзор типов данных PL/SQL В главе 12 рассматриваются правила работы с двоичными данными, приво- приводится много примеров, описываются встроенные функции и пакет DBMS_LQB, пред- предназначенные для операций с данными типа BFILE и другими двоичными данными в PL/SQL. Типы данных ROWID и UROWID Oracle поддерживает два типа данных, предназначенных для представления адре- адреса строки в таблице. Тип ROWID - это уникальный адрес строки таблицы, а тип UROWID - логическая позиция строки в индекс-таблице (Index-Organized Table, IOT). Правила работы с типами данных ROWID и UROWID описываются в главе 12. Тип данных REF CURSOR Тип данных REF CURSOR позволяет объявлять курсорные переменные, которые мо- могут использоваться со статическими и динамическими SQL-инструкциями, обес- обеспечивая большую гибкость в реализации самых разнообразных требований. Пе- Переменные этого типа могут быть заданы строго либо не строго. Во втором случае REF CURSOR представляет собой один из немногих слаботипизированных типов данных, поддерживаемых PL/SQL Ниже приведен пример строгого объявления типа данных REF CURSOR (связывание переменной этого курсора с конкретной записью с помощью атрибута SROWTYPE): DECLARE TYPE book_data_t IS REF CURSOR RETURN booMROWTYPE; book_curs_var book_data_t; Далее следуют два нестрогих объявления переменных типа REF CURSOR: DECLARE TYPE book_data_t IS REF CURSOR; book_ajrs_var book._data_t; Типу данных REF CURSOR и курсорным переменным посвящена глава 14. Типы данных для поддержки Интернета В Oracle9i появилась встроенная поддержка технологий, связанных с Интерне- Интернетом, в частности XML (Extensible Markup Language) и URI (Universal Resource Identifier). В Oracle имеются типы для работы с данными XML и URI, а также специальный класс идентификаторов URI (DBUri-REF), который используется для доступа к базе данных. Кроме того, в Oracle появился новый набор типов дан- данных, обеспечивающий хранение внешних и внутренних URI и доступ к ним из базы данных. Тип XMLType позволяет запрашивать из базы данных и сохранять в ней XML- данные. Это осуществляется с помощью таких функций, как SYS_XMLGEN из пакета DBMS_XMLGEN. Кроме того, используя язык XPath и встроенные операторы языка XML, вы можете выполнять поиск данных.
1Н0 Глава 7 • Работа с программными данными Типы данных для работы с URI, включая URIType и HttpURIType, входят в иерар- иерархию объектных типов и могут использоваться для хранения URL внешних web- страниц и файлов, а также для создания ссылок на информацию, хранящуюся в базе данных. В главе 12 рассматриваются правила работы с типом данных XMLType и различ- различными URI-типами, приводится примеры их использования и описываются встро- встроенные функции, предназначенные для операций с ними. Типы данных «any» Конечно, большую часть времени программисты пишут код, который выполняет узко специализированные задачи. Однако иногда им приходится создавать более универсальные программы. Типы данных «any» предназначены именно для та- таких ситуаций. Эта новая группа типов данных Oracle, появившаяся в Огас1е9г, значительно отличается от остальных. Типы данных «аду» позволяют динамически инкапсу- инкапсулировать описания типов, экземпляры данных и наборы экземпляров данных лю- любого другого типа, поддерживаемого языком SQL, и получать к ним доступ. С по- помощью объектных типов «any» и их методов можно, к примеру, определить тип данных, которые хранятся в конкретной вложенной таблице, не обращаясь к су- существующему определению этой таблицы. В группу типов данных «any» входят AnyType, AnyOata и AnyOataSet. Правила работы с типами данных «any» и несколько полезных примеров опи- описаны в главе 12. Типы данных, определяемые пользователем С тех пор как в Огас1е8 появились объектные типы данных, на основе всевозмож- всевозможных встроенных и пользовательских типов можно создавать типы данных произ- произвольной сложности, которые с большой точностью отражают структуру и поведе- поведение данных различных систем. Эта возможность подробно рассматривается в главе 21, там же рассказывается о том, как использовать добавленную в Огас1еЭг поддержку наследования объект- объектных типов данных. Объявление программных данных Прежде чем обращаться к переменной или константе, ее нужно объявить. (Единст- (Единственным исключением из этого правила являются индексные переменные циклов FOR.) Все объявления должны размещаться в разделе объявлений анонимного бло- блока, процедуры, функции, триггера, тела объектного типа или тела пакета. (Подроб- (Подробнее о структуре блока PL/SQL и его раздела объявлений рассказывается в главе 3.) В PL/SQL можно объявлять разные типы и структуры данных, включая пере- переменные, константы, типы TYPE (например, коллекцию или запись) и исключения. В этой главе описана, как объявляются переменные и константы. (Об операторе TYPE вы узнаете в главе 11, а о том, как объявлять исключения - в главе 6.)
Объявление программных данных 181 Объявление переменной Когда вы объявляете переменную, PL/SQL выделяет память для хранения ее зна- значения и присваивает ей имя, указывая которое вы можете это значение извлекать и изменять. В объявлении также задается тип данных переменной, он используется для проверки присваиваемых ей значений. Объявление переменной или констан- константы выполняется следующим образом: имя тип_данных [NOT NULL] [зацаиие_значення_поумопчанию~\; Здесь имя — это имя переменной или константы, тип_двнных — это тип или подтип данных, определяющий, данные какого типа можно присваивать переменной. При желании можно включить в объявление выражение NOT NULL, означающее, что если переменной присвоить значение NULL, инициируется исключение. Зада- Задавать значение по умолчанию обязательно только при объявлении констант, так как это обеспечивает их инициализацию. В следующем примере показано, как объявлять переменные разных типов: DECLARE -- Простое объявление числовой переменной 1_total_count NUMBER; -- Объявление числа, округляемого до ближайших сотых (центов) Ijtollarjunount NUMBER A0.2); -- Переменная, которой присваивается текущее значение даты -- и которая не может иметь значение NULL l_right_now DATE NOT NULL DEFAULT SYSOATE; -- Задание значения по умолчанию с помощью оператора присваивания 1_favor1te_flavour VARCHAR2A00) :- 'Вы любите кофе?'; -- Двухэтапный процесс объявления ассоциативного массива. -- Сначала тип таблицы: TYPE list_of_books_t IS TABLE OF booksSROWTYPE INDEX BY BINARYJNTEGER: -=¦ А затем конкретный список, с которым мы будем работать в данном блоке: оге11 ly_oracl e_boolcs 11 st_of_baoks_t: Чтобы при объявлении переменной или константе присвоить значение по умол- умолчанию, можно применить ключевое слово DEFAULT или оператор присваивания — в данном случае они эквивалентны и взаимозаменяемы. Мы предпочитаем для констант использовать оператор присваивания, а для начальных значений пере- переменных — ключевое слово DEFAULT. При объявлении константы задается не значе- значение по умолчанию, а значение, которое не может быть изменено впоследствии, поэтому DEFAULT кажется нам неуместным. Объявление константы Между объявлениями переменной и константы существуют два различия: объяв- объявление константы содержит ключевое слово CONSTANT, и в нем обязательно задается ее значение, которое не может быть изменено впоследствии: имя CONSTANT типланнух [NOT NULL] :- | DEFAULT значение:
182 Глава 7 • Работа с программными данными Далее следует несколько примеров объявлений констант: DECLARE -- Текущий год; в течение сеанса он не будет изненяться 1 currj-ear CONSTANT PLS INTEGER :- ~ TQJUMBER (T0_CHAR (SYSOATE. 'YYYY')); -- Использование мочевого слова DEFAULT l_author CONSTANT VARCHAR2A00) DEFAULT 'Bill Pribyll': -- Объявление сложного типа данных как константы -- Константы могут быть не только скалярными! 1_steven CONSTANT person_ot :- personot C'HUMAN', 'Steven Feurstein1. 175. '09-23-1958'); ПРИМЕЧАНИЕ Неименованная константа — это литеральное значение, подобное 2 или Bobby McGee. Литерал не обладает именем, но имеет тип данных, который не объявляется, а определяется непосредственно на основе значения литерала. Сейчас мы рассмотрим особенности объявления переменных. В большинстве случаев сказанное ниже относится и к константам. Объявления с ограничениями Объявление с ограничениями — это объявление с указанием числа, задающего до- допустимые значения переменной. Тип данных без ограничений — это тип данных, для которого такие ограничения не заданы. В качестве примера рассмотрим тип данных NUMBER. Для хранения переменной подобного типа выделяются 38 разрядов и соответствующий объем памяти. Если такая точность не требуется, переменную можно объявить с ограничением: itty_bitty_# NUMBERU): 1arge_but_constrai ned_# NUMBERC20.5); Переменной, объявленной с ограничениями, нужно меньше памяти, чем пере- переменной, объявленной без ограничений, как в следующем примере: nojimttsjiere NUMBER: NOT NULL Если переменной присваивается значение по умолчанию, можно также указать, что оно всегда должно быть определено, то есть добавить в объявление выраже- выражение NOT NULL. Например, следующее объявление инициализирует переменную сош- pany_name значением PCS R US и обеспечивает ее неравенство NULL: companyjiame VARCHAR2C60) NOT NULL DEFAULT 'PCS R US': Если код включает приведенную ниже строку, при ее выполнении будет акти- активизировано исключение VALUE_ERROR: company_name :- NULL:
Объявление программных данных 183 В следующем фрагменте кода переменная объявлена неправильно, поскольку отсутствует ее начальное значение, и после компиляции вы получите сообщение об ошибке: company_nare VARCHAR2FD) NOT NULL; -- NOT NULL требует задания начального значения! Объявления с привязкой Очень часто вы будете явно задавать типа данных: 1_company_name VARCHAR2U00); Помимо этого, в Oracle существует другой метод объявления переменных, на- называемый объявлением с привязкой (anchored declaration). Он обладает рядом пре- преимуществ в тех случаях, когда переменной необходимо присвоить значение из другого источника данных, например из строки таблицы. Привязывая переменную, вы устанавливаете ее тип данных на основе типа уже определенной структуры данных. Таковой может являться другая перемен- ная PL/SQL, предопределенный тип или подтип (TYPE или SUBTYPE), таблица базы данных либо конкретный столбец таблицы. В PL/SQL существуют два вида при- привязки. О Скалярная привязка. С помощью атрибута XTYPE переменная определяется на основе типа столбца таблицы или другой скалярной переменной PL/SQL. 0 Привязка к записи. Используя атрибут fcROWTYPE, можно определить перемен- переменную на основе таблицы или предопределенного явного курсора PL/SQL. Синтаксис объявления переменной с привязкой выглядит следующим образом: иня_перененной тт_атр*бутз ШРЕ [необязательное_зааание_значения по_умолчанию~}: ш_перененной имя_таблицы | wwjopccprfRQWTYPE [необязательное_за11ан»1е_значения_ло_умолчанию]: Здесь имя_переменной — это имя объявляемой переменной, тип_атри6утв — это либо имя ранее объявленной переменной PL/SQL, либо спецификация столбца табли- таблицы в формате таблица.столбец. Наличие ссылок привязки в коде допускается компилятором и не приводит к увеличению времени выполнения. Кроме того, привязка устанавливает зависи- зависимость между программным кодом и элементом, к которому выполняется привяз- привязка (таблицей, курсором или пакетом, содержащим переменную, на которую есть ссылка в объявлении). Это означает, что при изменении данного элемента привя- привязанный к нему программный код помечается как INVALID. При повторной компи- компиляции привязка выполняется заново, и таким образом код согласуется с данным элементом. На рис. 7.1 показано, как тип данных определяется на основе столбца таблицы базы данных и переменной PL/SQL. Ниже приведен пример привязки переменной к столбцу таблицы базы данных: 1 jampanyj d company. companyj dXTYPE: Аналогичным образом выполняется привязка к переменной PL/SQL; как пра- правило это делается для того, чтобы избежать нескольких объявлений одного и того же жестко закодированного типа данных. В подобном случае в пакете создают пе- переменную и затем в программах ссылаются на нее с помощью атрибута ITYPE.
184 Глава 7 • Работа с программными данными (Можно также создавать в пакете подтипы SUBTYPE; о них рассказывается далее в этой главе.) DECLARE emp_i d[emp.empnoXTYPE: | Словарь данных Таблица ЕМР empno | NUMBER(S) enarnej"VARCHAR2C0) hiredate ¦ DATE new_emp|emp_idSTYPE: BEGIN Рис. 7.1. Использование атрибута %TYPE В следующем примере приведен фрагмент кода пакета, предназначенного для облегчения работы с Oracle Advanced Queuing (AQ): /* Файл в web: aq.pkg */ CREATE OR REPLACE PACKAGE aq IS /* Стандартные типы данных, используемые в Oracle AQ. */ vjnsgid RAW C16): . SUBTYPE msgidjype IS vjnsgidJTYPE: vjiame VARCHAR2 D9): SUBTYPE namejtype IS vjiameXTYPE: END aq: В пакете aq тип данных для хранения идентификаторов сообщений определен как RAW A6). Вместо того чтобы помнить эту спецификацию и несколько раз же- жестко кодировать ее в приложении, можно объявить переменную для идентифика- идентификатора сообщения следующим образом: DECLARE my msg_id aq.msg1d_type; begin" Если Oracle изменит тип данных для идентификаторов сообщений, достаточ- достаточно изменить определение подтипа (SUBTYPE) в пакете aq, и после перекомпиляции все объявления в программах обновятся автоматически. Наличие объявлений с привязкой свидетельствует о том, что PL/SQL являет- является не просто процедурным языком программирования, а разработан специально как расширение языка Oracle SQL. Корпорация Oracle приложила большие уси- усилия для того, чтобы интегрировать программные конструкции PL/SQL с базами данных, для доступа к которым используется SQL. Значительным преимуществом объявлений с привязкой является то, что они позволяют писать очень гибкие приложения, которые автоматически настраива- настраиваются на последующие изменения структуры данных.
Объявление программных данных 185 Привязка к курсорам и таблицам Ранее мы уже продемонстрировали примеры того, как осуществляются объявле- объявления переменной с привязкой к столбцу базы данных и другой переменной PL/SQL. Теперь проследим, как используется атрибут привязки SROWTYPE. Допустим, нам нужно запросить одну строку из таблицы books. Вместо того чтобы с помощью атрибута ШРЕ объявлять для каждого столбца таблицы от- отдельную переменную, можно воспользоваться атрибутом fcROWTYPE: DECLARE l_book booksSROWTYPE; BEGIN SELECT * INTO l_book FROM books WHERE isbn - Ч-56592-335-91; process_book (l_book); END: Теперь предположим, что мы хотим извлечь из таблицы book только имя авто- автора и название интересующей нас книги. В этом случае сначала явно определим курсор, а затем на его основе объявим переменную: DECLARE CURSOR bookjrur IS SELECT author, title FROM books WHERE 1sbn - '1-56592-335-9'; l_book book_cur*ROWTYPE; BEGIN OPEN book cur: FETCH book_cur INTO l_book: process_book (l_book); END; Ниже приведен пример неявного использования атрибута SROWTYPE в объявле- объявлении записи book_rec цикла FOR: BEGIN FOR book_rec IN (SELECT * FROM books) LOOP process_book (book_rec): END LOOP: END; Преимущества объявлений с привязкой Во всех объявлениях, которые приведены в предыдущих главах книги, тип пере- переменной (символьный, числовой, логический, дата) задается явно. В каждом из них содержатся непосредственное указание типа данных и, как правило, ограни- ограничение, налагаемое на значение этого типа. Если программа содержит такие объяв- объявления, то считается, что данные в ней кодируются жестко. Это распространенный подход к объявлению переменных, но иногда он может вызвать проблемы. В первую очередь они касаются синхронизации со столбцами таблицы базы данных. Ведь в программе PL/SQL переменная часто «представляет» информа- информацию из таблицы базы данных. Если явно объявить переменную, а затем изменить
186 Глава 7 • Работа с программными данными структуру таблицы, данные которой содержит переменная, программа может ра- работать неправильно. Вторая проблема касается стандартизации локальных переменных. Представь- Представьте, что переменная PL/SQL хранит вычисляемые значения, которые используют- используются в разных местах приложения. Какими могут быть последствия того, что в не- нескольких местах кода жестко объявлена переменная одного типа? Синхронизация со столбцами таблицы базы данных База данных содержит информацию, которая считывается и используется в при- приложении. Операции с этой информацией выполняются с помощью как SQL, так и PL/SQL. При этом программы PL/SQL часто считывают информацию из базы данных, присваивают результат локальным переменным и после соответствую- соответствующей обработки записывают информацию из переменных обратно в базу данных. Предположим, что таблица с данными о компаниях содержит столбец NAME с типом данных VARCHAR2 F0). Локальную переменную для хранения значений это- этого столбца можно объявить следующим образом: DECLARE cname VARCHAR2C60): Теперь обратимся к приложению, в котором используется информация о ком- компаниях. В нем могут быть десятки различных процедур и отчетов, содержащих одно и то же объявление PL/SQL VARCHAR2 С 60). И все это прекрасно работает, пока не изменятся бизнес-требования или администратора базы данных не охва- охватит жажда перемен. Тогда ему ничего не стоит изменить определение типа столб- столбца NAME таблицы на VARCHAR2C100), чтобы в него помещались более длинные назва- названия компаний. 5 результате в таблице могут оказаться такие данные, что при считывании их в переменную cname будет инициироваться исключение VALUE_ERROR. После изменения типа данных столбца все программы, в которых объявлена и используется переменная cname, станут несовместимыми со структурой исход- исходных данных. Чтобы программа продолжала работать без ошибок, нужно изме- изменить все эти объявления. Стандартизация локальных переменных Еще один недостаток явного объявления типов данных проявляется при работе с переменными PL/SQL, которые содержат вычисляемые значения, не хранящие- хранящиеся в базе данных. Предположим, что программисты разработали приложение для управления финансами компании. Во многих его программах для хранения ито- итоговой выручки используется переменная total_revenue, объявленная следующим образом: total_revenue NUMBERA0,25; Как видите, руководитель компании имеет возможность подсчитать выручку до последнего пенни. В 1992 году, когда впервые была написана спецификация приложения, максимальная выручка, которую компания могла получить от одно- одного заказчика, составляла 99 млн. долларов, и поэтому для переменной использо- использовалось объявление NUMBERA0.2). Позже, в 1995 году, было принято предложение переоборудовать бомбардировщик В-2 в транспортный самолет, использующийся для доставки пшеницы, и фирма получила контракт на 2 млрд. долларов! Когда
Подтипы данных, определяемые программистом 18/ все уже готовились откупорить шампанское, ведущий программист сообщил, что не может сгенерировать отчеты для этого нового проекта, поскольку переменные total_revenue не позволяют разместить результат. Он принялся разыскивать в приложении экземпляры переменных total_re- venue, чтобы изменить их объявления. Это была долгая и кропотливая работа, по- поскольку объявления были расположены во многих местах кода. Все локальные структуры данных были не стандартизованы. Жаль, что он не объявил все ло- локальные переменные total_revenue со ссылкой на один тип данных, что позволило бы использовать объявления с атрибутом ЗГГУРЕ. Объявления с привязкой и ограничение NOT NULL Объявляя переменную, вы можете задать для нее ограничение NOT NULL, и оно бу- будет перенесено на переменные, объявляемые на ее основе с атрибутом ШРЕ. Если включить ограничение NOT NULL в объявление переменной, к которой с помощью атрибута ШРЕ привязываются другие переменные, то обязательно нужно зада- задавать начальные значения привязываемых к ней переменных. Предположим, что мы объявили переменную max_available_date с ограничением NOT NULL: DECLARE max available date DATE NOT NULL :- EAST_OAY CADD_MONTH (SYSDATE.3)): last_shTp_date max_avai1able_dateJiTYPE: Такое объявление переменной 1 astshi p_date вызовет следующую ошибку компи- компиляции: a variable declared NOT NULL must have an initialization assignment. Если вы используете переменную, объявленную с ограничением NOT NULL в объяв- объявлении с атрибутом ШРЕ, обязательно задавайте в этом объявлении ее начальное значение. Однако если источником значений переменной является столбец базы данных, объявленный как NOT NULL, делать это не обязательно, поскольку в подоб- подобном случае ограничение NOT NULL на переменную не переносится. Подтипы данных, определяемые программистом PL/SQL поддерживает оператор SUBTYPE, который позволяет программисту опре- определять собственные подтипы {абстрактные типы данных). В PL/SQL подтип типа данных - это тип данных с набором правил и подмножеством значений ис- исходного типа данных. Существуют два вида подтипов данных. О Подтип с ограничениями. Набор значений этого подтипа ограничен по срав- сравнению с набором значений исходного типа данных. Например, тип POSITIVE яв- является подтипом BINARYJNTEGER с ограничениями. В пакете STANDARD, где опре- определяются типы данных и функции, входящие в состав стандартного языка PL/SQL4, подтип POSITIVE определяется следующим образом: SUBTYPE POSITIVE IS BINARY INTEGER RANGE 1 ,. 2147483647;
188 Гл^ва 7 • Работа с программными данными В переменной, объявленной как POSITIVE, могут храниться только целочислен- целочисленные значения больше 0. О Подтип без ограничений. Это подтип, имеющий тот же набор значений, что и исходный тип данных. Например, тип FLOAT является подтипом NUMBER без ог- ограничений. Он определен в пакете STANDARD следующим образом: SUBTYPE FLOAT IS NUMBER; Иными словами, подтипы данных без ограничений — это псевдонимы или альтер- альтернативные имена исходных типов данных. До появления версии Огас1е8г разра- разработчики PL/SQL могли определять только подтипы данных без ограничений. Теперь же разрешаются и подтипы данных с ограничениями, как например: CREATE OR REPLACE PACKAGE utility AS • SUBTYPE big_string IS VARCHARZC32767): SUBTYPE big~db_string IS VARCHAR2C4000); END utility: Подтип нужно объявить в разделе объявлений анонимного блока PL/SQL, процедуры, функции или пакета. Вы уже видели, как объявляется подтип, ис- используемый PL/SQL в пакете STANDARD. Общий формат объявления подтипа сле- следующий: SUBTYPE мя_поятипа IS 6азовыЯ_тш: Здесь иня_подтипа - это имя нового определяемого подтипа, а базовый'_тип — имя типа данных, на котором основывается определяемый подтип. Имейте в виду, что на переменные, привязанные к подтипу, не распростра- распространяется ограничение NOT NULL и игнорируются начальные значения, включенные в исходное объявление переменной или спецификацию столбца. Преобразование типов данных Существует множество ситуаций, когда требуется преобразовать данные из одно- одного типа в другой. Такое преобразование можно выполнить двумя способами: О неявно — дать возможность сделать «наилучший» выбор исполнительному ядру PL/SQL; О явно - вызвать для преобразования функцию PL/SQL или воспользоваться соответствующим оператором. В данном разделе речь пойдет о том, как в PL/SQ.L реализуются эти два спосо- способа преобразования типов. Неявное преобразование типов Когда типы используемых в операции данных не согласуются друг с другом, PL/SQL выполняет их преобразование. Вы будете удивлены, узнав, как часто это делается. На рис. 7.2 показано, какие виды неявного преобразования типов вы- выполняются PL/SQL
Преобразование типов данных 189 Из ^\ BINARY INTEGER BLOS CHAR CLOB DATE LONG NUMBER PLS INTEGTR RAW UROWID VARCHAR2 BINARYJNTEGER • • • • BLOB • CHAR • • • • • • • • CLOB • DATE • LONG • • • • • • NUMBER • • • PLSJNTEGTR • • • • RAW • • • • UROWID • • VARCHAR2 • • • • • • • • • Рис. 7.2. Неявные преобразования типов, выполняемые PL/SQL Неявное преобразование типов осуществляется в том случае, когда вы задаете в операторе или выражении литеральное значение. В приведенном ниже примере PL/SQL преобразует литеральную строку «125» в числовое значение 125 и при- присваивает это значение числовой переменной: DECLARE ajiumber NUMBER; BEGIN ajiumber :- '125': END; Неявное преобразование типов выполняется также, когда вы передаете про- программе значения данных не того формата, который в ней используется. В следую- следующей процедуре таким параметром является дата. Вызывая эту процедуру, вы пе- передаете ей строку в формате DD-MON-YY, которая автоматически преобразует- преобразуется в дату: PROCEDURE change hiredate (empjdjn Ilf INTEGER, hiredatejn IN DATE) changejiiredate A004. '12-DK-94')< Ограничения неявного преобразования Преобразование может выполняться только между определенными типами дан- данных, и нельзя преобразовать один тип данных в любой другой (см. рис. 7.2). Более
190 Глава 7 ¦ Работа с программными данными того, некоторые неявные преобразования типов генерируют исключения. Рас- Рассмотрим следующую операцию присваивания: DECLARE ajiumber NUMBER; BEGIN a number :- 'abc'; END;' В PL/SQL нельзя преобразовать строку «be» в число, и поэтому при выпол- выполнении приведенного кода инициируется исключение VALUE_ERROR. Вы сами долж- должны позаботиться о том, чтобы значение, для которого PL/SQL придется выпол- выполнять преобразование типов, могла быть конвертировано без ошибок. Недостатки неявного преобразования Неявное преобразование типов имеет ряд недостатков. О Каждое такое преобразование означает частичную потерю контроля над про- программой. Вы не делаете его самостоятельно и никак им не управляете, а лишь предполагаете, что оно будет выполнено и даст желаемый эффект. В этом есть элемент неопределенности - изменение компанией Oracle способа или усло- условия преобразования может отразиться на программе. ¦ О Неявное преобразование типов в PL/SQL зависит от контекста. Оно может выполниться в одной программе и не выполниться в другой, хотя на первый взгляд код кажется одинаковым. Кроме того, результат преобразования типов в PL/SQL может оказаться не таким, как вы ожидаете. Программу легче читать и понять, если данные в ней преобразуются явно, по- поскольку при этом фиксируются различия между типами данных в разных табли- таблицах или в таблице и коде. Исключив из программы скрытые действия, вы устра- устраните и потенциальную возможность ошибок. Таким образом, в SQL и PL/SQL рекомендуется избегать неявного преобразо- преобразования типов. Лучше пользоваться функциями, которые выполняют явное преоб- преобразование и гарантируют, что будет сделано именно такое преобразование, как вы задумали. Явное преобразование типов Oracle предоставляет обширный набор функций и операторов, с помощью кото- которых можно выполнять преобразование типов данных в SQL и PL/SQL. Их пол- полный список приведен в табл. 7.1. Большая часть функций описывается в других главах книги (для них в последнем столбце указан номер главы). О функциях, которые нигде в книге не описаны, рассказывается далее в этой главе. Таблица 7.1. Встроенные функции преобразования Функция Выполняемое преобразование Глава ASCIISTR Строку из любого набора символов в строку ASCII из набора 8 символов базы данных CAST Одно значение встроенного типа данных или коллекции 7, 9,10 в другой встроенный тип данных или коллекцию. Этот способ может использоваться вместо традиционных -__ функций, таких как TO_DATE
Преобразование типов данных 191 Функция Выполняемое преобразование Глава CHARTOROWID CONVERT FROMJTZ HEXTORAW MULTISET NUMTODSINTERVAL NUMTOYMINTERVAL RAWTOHEX, RAWTONHEX REFTOHEX ROWIDTOCHAR, ROWIDTONCHAR TABLE THE TO.CHAR, TO_NCHAR (числовая версия) TO.CHAR, TO_NCHAR Сверсия для дат) TO.CHAR, TO_NCHAR (символьная версия) TO_BLOB то_аов, to_nclob TO^DATE TO_DSINTERVAL TO_LOB TO_MULTI_BYTE TO_NUMBER TO_RAW TO_SINGLE_BYTE TO_TIMESTAMP TO_TIMESTAMP_TZ TO_TIMESTAMP_LTZ TO.YMINTERVAL Строку в значение типа ROWID 7 Строку из одного набора символов в другой 7 В значение типа TIMESTAMP добавляет информацию 10 о часовом поясе, преобразуя его тем самым в значение типа TIMESTAMP WITH TIME ZONE Значение из шестнадцатерично-о формата в значение типа 7 RAW Таблицу базы данных в коллекцию 11 Число (или числовое выражение) в литерал INTERVAL DAY 10 ТО SECOND Число (или числовое выражение) в литерал INTERVAL YEAR 10 ТО MONTH Значение типа RAW в шестнадцатеричный формат 7 Значение типа REF в символьную строку, содержащую его 21 шестнадцатеричное представление Двоичное значение типа ROWID в символьную строку 7 Коллекцию в таблицу базы данных; по своему действию 11 обратна функции MULTISET Преобразует значение столбца в строку виртуальной 11 таблицы базы данных Число в строку (VARCHAR2 или NVARCHAR2 соответавенно) 9 Дату в строку 10 Данные из набора символов базы данных в набор S символов национального языка Значение типа RAW в значение типа BLOB 12 Значение типа VARCHAR2, NVARCHAR2 или NCLOB 12 в значение типа CLOB (либо NCLOB) Строку в дату 10 Значение типа CHAR, VARCHAR2, NCHAR или NVARCHAR2 10 в значение типа INTERVAL DAY TO SECOND Значение типа LONG в LOB 12 Однобайтовые символы исходной строки в их 8 многобайтовые эквиваленты (если это возможно) Строку в число 9 Значение типа BLOB в значение типа RAW 12 Многобайтовые символы исходной строки 8 в соответствующие однобайтовые символы Символьную строку в значение типа TIMESTAMP 10 Символьную строку в значение типа TIMESTAMP_TZ 10 Символьную строку в значение типа TIM ESTAMP_LTZ 10 Значение типа CHAR, VARCKAR2, NCHAR или NVARCHAR2 10 в значение типа INTERVAL YEAR TO MONTH продолжение^
192 Глава 7 • Работа с программными данными Таблица 7.1 (продолжение) Функция Выполняемое преобразование Глава TRANSLATE.,.USING Текст в набор символов, заданный для преобразовании .8 набора символов базы данных в национальный набор символов UNISTR В качестве аргумента принимает строку из любого набора 8 символов и преобразует ее в строку с кодировкой Unicode Функция CHARTOROWID Преобразует строку типа CHAR или VARCHAR2 в значение типа ROWIO. Синтаксис функ- функции имеет следующий вид: FUNCTION CHARTOROWID (исхоцная_строна IN CHAR) RETURN ROWID FUNCTION CHARTOROWID {исходнвя_строид IN VARCHAR2) RETURN ROWID Для успешного преобразования функцией CHARTOROWID строка должна быть за- задана в формате B8BBB8B8.RRRR.FFFF Здесь ВВВВВВВВ - номер блока в файле базы данных, RRRR - номер строки в блоке, a FFFF — номер файла базы данных. Все три числа необходимо указать в шестна- дцатеричном формате. Если исходная строка не соответствует этому формату, инициируется исключение VALUE_ERROR. Функция CAST Эта функция появилась в Огас1е8 и является очень удобным и гибким механиз- механизмом преобразования данных. Она преобразует значение любого (или почти лю- любого) встроенного типа данных или коллекции в другой встроенный тип данных или коллекцию. Одноименный оператор хорошо знаком тем, кто работал с объект- но-ориентированпыми языками программирования, где часто возникает необхо- необходимость преобразовать объект одного класса в объект другого. С помощью функции CAST можно преобразовать неименованное выражение (число, дату, результат вложенного запроса) или именованную коллекцию (на- (например, вложенную таблицу) в тип данных или именованную коллекцию совмес- совместимого типа. Допустимые преобразования между встроенными типами данных показаны на рис. 7.3. Необходимо соблюдать следующие правила: О нельзя преобразовывать типы данных LONG, LONG RAW, LOB и типы данных, специ- специфические для Oracle; О обозначению «DATE», которое используется на рисунке, соответствуют типы данных DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, INTERVAL DAY TO SECOND и INTERVAL YEAR TO MONTH; О для преобразования именованной коллекции определенного типа в именован- именованную коллекцию другого типа нужно, чтобы элементы обеих коллекций имели одинаковый тип; О нельзя преобразовать тип UROWID в тип ROWID, если значение UROWID представля- представляет собой ROWID индекс-таблицы.
Преобразование типов данных 193 ч\\ в Из ^\ VARCHAR2 NUMBER DATE RAW ROWTO, UROWID NCHAR, NVARCHAR2 CHAR, VARCHAR2 • • • • • NUMBER • • • DATE • • • RAW • • ROWID, UROWID • • • NCHAR. NVARCHAR2 • Рис. 7.3. Преобразование встроенных типов данных Ниже приведен пример использования функции CAST для преобразования ска- скалярных типов данных. Ее можно вызвать с помощью SQL-инструкции: SELECT employeejid. CAST (hirejate AS VARCHAR2 C0)) FROM employee; Она вызывается также в PL/SQL: DECLARE hdjislay VARCHAR2 C0): BEGIN hdjislay := CAST (SYSDATE AS VARCHAR2): END; Функцию CAST удобно использовать при работе с коллекциями PL/SQL, по- поскольку она позволяет преобразовывать коллекцию из одного типа в другой, а так- также для работы с коллекцией, объявленной как переменная PL/SQL. Эта тема рас- рассматривается подробнее в главе 11. Следующий пример продемонстрирует синтаксис и возможности указанных операций. Сначала создадим два типа вложенных таблиц и одну реляционную таблицу: CREATE TYPE naraesj AS TABLE OF VARCHAR2 A00); CREATE TYPE authors_t AS TABLE OF VARCHAR2 A00); CREATE TABLE favorite_authors (name VARCHAR2 B00)) Далее напишем программу, которая связывает данные из таблицы favori- te__authors с содержимым вложенной таблицы scifi_favorites, объявленной и за- заполненной в другой программе. Рассмотрим следующий блок: /* Файл в web: cast.sql */ 1 DECLARE 2 scifi_favorites authors_t 3 := authors_t CSheri S. Tepper1. 'Orson Scott Card1, 'Gene Wolfe1); 4 BEGIN 5 DBMS_OuTPUT.put_line ('I recommend that you read books by:'}: G 1 FO&rec IN (SELECT column value favs
194 Глава 7 ¦ Работа с программными данными 8 9 10 11 12 13 14 FROM UNION SELECT FROM LOOP DBMS OUTPUT.put END LOOP; TABLE (CAST (sc1fl_favor1tes AS names_t)) NAME favorite. lire (rec _authors) ,favs): 15 END; В строках 2 и 3 объявляется локальная вложенная таблица, и она заполняется именами нескольких популярных авторов, пишущих в жанре научной фантасти- фантастики. В строках 7-11 с помощью оператора UNION объединяются строки таблиц f avo- rite_authors и scifi_favor1tes. Для этого, используя функцию CAST, преобразуем вложенную таблицу scifi favorites (локальную и не видимую для ядра SQL) в тип вложенной таблицы namest. Заметьте, что коллекцию типа authorst можно преобразовать в коллекцию типа namest, поскольку их типы данных совместимы. После преобразования с помощью оператора TABLE сообщаем SQL, что вложен- вложенную таблицу следует интерпретировать как реляционную. На экране отобразятся следующие результаты: I recommend that you read books by. Gene Wolfe Orson Scott Card Robert Harris Sheri S. Tepper Tom Segev Toni Morrison Функция CONVERT Преобразует строки из одного набора символов в другой. Синтаксис функции имеет такой вид: FUNCTION CONVERT (исходкая_сгро/« IN VARCHAR2. новый_нзбар_синволов VARCHAR2 [, старый_на6ор_смво{юв VARCHAR2]) RETURN VARCHAR2 Третий аргумент, старый_набор_символов, необязателен. Если он не задан, при- применяется набор символов, используемый в базе данных по умолчанию. Функция CONVERT не переводит слова или фразы с одного языка на другой, а за- заменяет буквы или символы одного набора символов буквами или символами дру- другого. Двумя наиболее распространенными наборами символов являются US7ASCII G-разрядный набор ASCII для США) и F7DEC G-разрядный набор DEC для Франции). Функция HEXTORAW Преобразует шестнадцатеричную строку типа CHAR или VARCHAR2 в значение типа RAW. Синтаксис функции выглядит следующим образом: FUNCTION HEXTORAW (исходнэя_строка IN CHAfORETURN RAW FUNCTION HEXTORAW (исходнаястрока IN VARCHAR2)RETURN RAW
Преобразование типов данных 195 функция RAWTOHEX Преобразует значение типа RAW в шестнадцатеричную строку типа VARCHAR2. Син- Синтаксис функции приведен ниже: FUNCTION RAWTOHEX (HCX0HH0ejB0H4H0ejHa4eHHe IN RAWJRETURN VARCHAR2 Функция RAWTOHEX всегда возвращает строку переменной длины, хотя обратная ей перегруженная функция HEXTORAW поддерживает оба типа строк. функция RAWIDTOCHAR Преобразует двоичное значение типа RAWID в строку типа VARCHARZ, Синтаксис функции выглядит так; FUNCTION RAWIDTOCHAR (исходная_?трока IN RAWIDJRETURN VARCHAR2 Возвращаемая функцией строка имеет следующий формат: BBBBBBB8.mR.FFFF Здесь ВВВВВВВВ — номер блока в файле базы данных, RRRR — номер строки в блоке, a FFFF — номер файла базы данных. Все три числа должны быть заданы в шестна- дцатеричном формате, как в следующем примере: 11/14/1994 1988 2018
8 Строки > Наборы символов > Строковые типы данных > О работе со строками > Функции для работы со строками > Функции NLS Текст хранится в переменных символьных типов, а для работы с ними использу- используются символьные функции. Символьные строки имеют «свободную форму», в них можно хранить буквы, цифры, а также специальные символы. Существует несколько символьных типов данных, имеющих разное назначение. ПРИМЕЧАНИЕ- Хотя типы данных CLOB и LONG тоже можно считать символьными, они используются не так, как символьные типы данных, описанные в этой главе. Указанные типы данных лучше воспринимать как типы больших объектов. О больших объектах рассказывается в главе 12. Наборы символов Работа со строками традиционно считается очень простой темой. Однако по мере того как приложения становятся все более «интернациональными» и расширяет- расширяется поддержка Oracle разных наборов символов, и в частности Unicode, выясняет- выясняется, что не все так просто. Поэтому для эффективной работы со строками нужно иметь представление о том, что такое наборы символов, какими они бывают и как ими пользоваться. Что такое набор символов Набор символов — это совокупность символов (знаков), применяемых человеком, и соответствующий ей набор битовых последовательностей, используемых для
Наборы символов 197 представления этих символов в компьютере или на диске. В США обычно приме- применяется 7-битовый набор символов ASCII. Каждый 7-битовый символ ASCII пред- представлен семью битами 8-битового байта (значение восьмого бита — 0). Например, буква G в нем представлена значением 0100 0111. Если интерпретировать эту по- последовательность битов как число, получится шестнадцатеричное значение 0x47 или десятичное 71. С помощью семи битов можно представить всего 128 симво- символов, но этого вполне достаточно для представления всех букв английского алфа- алфавита и целого ряда дополнительных символов. Таким образом, набор символов — это набор представляемых знаков и соответствующие им числовые значения. Код ASCII, в котором для представления символа используется 7 бит, был од- одним из первых и применялся в основном в США. Это означает, что его создатели не собирались представлять буквы языков, отличных от английского. В результате центры стандартизации и отдельные компании определили для этой цели боль- большое количество других наборов символов. Многие из этих наборов являются надмножествами ASCII и используют восьмой бит, позволяющий представить еще 128 символов. Например, набор символов кодовой страницы Microsoft Win- Windows Code Page 1251 совместим с ASCII, но может представлять еще и символы кириллицы. Для представления букв большинства западных языков, основанных на ла- латинском алфавите и кириллице, вполне достаточно 256 символов. Однако этого слишком мало для восточных языков, таких как японский, корейский и китай- китайский. Поэтому для представления знака этих языков традиционно используется не менее двух байтов. Такие наборы символов называют многобайтовыми. Набор символов Unicode появился относительно недавно. В нем объединены все известные символы существующих алфавитов. При использовании Unicode любому символу независимо от платформы, программы и языка присваивается уникальный код. Unicode был принят в качестве стандарта такими лидерами ком- компьютерной индустрии, как Apple, HP, IBM, JustSystem, Microsoft, Oracle, SAP, Sun, Sybase, Unisys. ПРИМЕЧАНИЕ- За полной и подробной информацией об Unicode вы можете обратиться по адресу; http://L)ni- code.org. Типы наборов символов Наборы символов можно классифицировать по разным признакам, но для про- программистов PL/SQL важны прежде всего такие характеристики: О является набор многобайтовым или однобайтовым; О имеет он фиксированную или переменную длину. Давайте еще раз вернемся к набору символов ASCII. Используемые для пред- представления каждого символа 7 бит помещаются в 1 байт, и каждому символу соот- соответствует отдельный байт. Поэтому набор символов ASCII считается однобайто- однобайтовым. Кроме того, он является набором символов фиксированной длины, поскольку все символы представлены в нем одинаковым количеством байтов (в данном слу- случае — одним).
198 Глава 8 • Строки В наборах символов, состоящих более чем из 256 символов, как например в Uni- Unicode UTF-8, один байт часто используется для представления символов стан- стандартного набора ASCII и ряда других распространенных символов; символы ос- остальных наборов представляются несколькими байтами. Так, в UTF-8 буква G представляется одним байтом G1 или 0x41 в шестнадцатеричной системе), а сим- символ евро (€) — тремя байтами @хЕ282АС). Кроме того, отдельные символы этого набора могут быть представлены замещающими парами, то есть специальными последовательностями, состоящими из двух символов, для которых всегда ис- используются четыре байта. Наборы символов, подобные UTF-8, являются много- многобайтовыми, поскольку отдельные символы представлены в них более чем одним байтом и имеют переменную длину, так как количество байтов на один символ в них не всегда одинаково. Третий класс наборов символов - это многобайтовые наборы символов фик- фиксированной длины. Одним из них является набор символов Unicode UTF-16, в ко- котором каждый символ представлен двумя байтами. Например, буква Л представ- представлена байтами со значениями 0 и 65. ПРИМЕЧАНИЕ- Как вы понимаете, четвертого класса наборов символов, то есть однобайтовых наборов символов переменной длины, не существует. В однобайтовом наборе по определению всегда используется один байт на символ, и поэтому его символы всегда имеют фиксированную длину. Набор символов базы данных и набор символов национального языка С каждой базой данных Oracle связаны не один, а два набора символов. О Набор символов базы данных. Используется для представления значений столбцов типа CHAR и VARCHAR2, а также имен таблиц, столбцов, переменных PL/SQL, SQL-инструкций, большинства строковых литералов и других по- подобных идентификаторов. О Набор символов национального языка. Используется для представления зна- значений столбцов типа NCHAR и NVARCHAR2 и строковых литералов с префиксом N. Почему существует два набора символов? Так сложилось исторически, и в ос- основном это в свое время было связано с производительностью. Набор символов базы данных использовался для однобайтовых символов ASCII, а набор симво- символов национального языка — для многобайтовых символов фиксированной дли- длины. Получается, что поддерживаются и символы ASCII, и многобайтовые симво- символы, но при этом отсутствуют издержки, связанные с обработкой последних. Если вы не знаете, какой из двух наборов символов поддерживает ваша база данных, запросите информацию представления NLS_DATABASE_PARAMETERS словаря данных Oracle: SDL> SELECT * 2 FROM nls database parameters 3 WHERE parameter IN VNLS_CHARACTERSET'. 'NLSJCHARJHARACTERSET'); PARAMETER VALUE NLS CHARACTERSET WE8MSWIN1252 NLSJCHAR CHARACTERSET AL16UTF16
Наборы символов 199 Параметр NLS_CHARACTERSET возвращает имя используемого набора символов ба- базы данных Oracle, а параметр NLS_NCHAR_CHARACTERSET - имя национального набора символов в Oracle, используемого для типов данных NCHAR и NVARCHAR2. Применяемые в Oracle имена наборов символов — просто удобные сокраще- сокращения, распознаваемые программным обеспечением компании. Эти сокращения не всегда признаются центрами стандартизации и не являются официальными име- именами наборов символов. Например, набор Unicode UTF-16 в Oracle называется AL16UTF16. Как правило, имя набора символов в данной СУБД несет много по- полезной информации. Используемое здесь соглашение об именах наборов симво- символов проиллюстрировано на рис. 8.1. Количество битов для представления символа — 8 Регион/язык— Имя набора символов, соответствующего Западная Европа кодовой странице, — Microsoft Windows (Western Europe) Code Page 125! B-blt West Europe Рис, 8.1. Иллюстрация соглашения об именах наборов символов, принятого в Oracle Главным в этом соглашении является второй элемент, указывающий количе- количество битов, необходимых для представления одного символа. Если это число 7 или 8, можете быть уверены, что имеете дело с однобайтовым набором символов фиксированной длины, а если это число больше 8, например 16 или 32, значит речь идет о многобайтовом наборе символов. Символы многобайтовых наборов, как правило, имеют переменную длину. ПРИМЕЧАНИЕ- Иногда в конце имени набора символов присутствует буква S или С. Такой суффикс указывает, что данный набор символов может использоваться только на сервере (S) или только на клиентском ком- компьютере (С). Рассмотренное соглашение об именах не универсально, то есть оно применя- применяется не ко всем наборам символов в Oracle. Например, набор UTF-8 называется UTF8, что не соответствует данному соглашению. Вопросы, связанные с наборами символов Один байт или два? Фиксированная длина или переменная? Какое это имеет зна- значение? При объявлении строковой переменной от набора символов зависят мак- максимальное количество символов, помещающихся в строку, интерпретация резуль- результатов, возвращаемых рядом строковых функций и порядок сортировки (какая из строк считается большей, а какая - меньшей). Байты и символы Рассмотрим объявление строковой переменной: feature name CHARC5Q):
200 Глава 8 • Строки Какую длину имеет объявленная здесь строка — 50 символов или 50 байт? Это одно и то же в том случае, если набор символов базы данных — однобайтовый. Но если в базе данных используется многобайтовый набор символов, тогда действи- действительно нужно знать, какое значение переменной вы получите в результате. В Oracle8i и более ранних версиях системы длина строковых переменных все- всегда объявлялась в байтах, так что объявление CHAR E0) означает, что будет создана переменная длиной 50 байт. Количество символов, помещающихся в этих 50 байтах, зависит от используемого набора символов. Так, набор символов Oracle JA16EUC поддерживает и символы ASCII, и символы японского алфавита, которые имеют переменную длину (до 3 байт). Если все символы строки — 3-байтовые, она мо- может иметь длину до 16 символов. Ситуация усложняется тем, что большинство многобайтовых наборов состоят из символов переменной длины, поэтому количество символов, помещающихся в 50 байтах, зависит от содержимого конкретной строки. Например, в наборе Uni- Unicode UTF-8 символы могут иметь длину до 3 байт (замещающие пары мы в дан- данном случае не учитываем), так что если вам нужно, чтобы в значении переменной обязательно помещалось 50 символов, следует объявлять ее как CHAR A50). Объявление строк в Oracle9i Начиная с Огас1е9г при объявлении строковой переменной ее длину можно указы- указывать в байтах или в символах. Например, объявляя переменную типа CHAR для хранения 50 байт, нужно написать так: featurejiame CHARC50 BYTE); Если вы хотите, чтобы в переменную обязательно помещалось 50 символов из текущего набора символов, объявите ее следующим образом: featurejiame CHARC50 CHAR): А что же тогда означает такое объявление: featurejiame CHARC50): Для того чтобы ответить на вопрос, необходимо знать, как сконфигурирована база данных. А именно, нужно выяснить, каково значение параметра NLS_LENGTH_SE- MANTICS. С этой целью выполним такой запрос: SQL> SELECT * 2 FROM nls_session_parameters 3 WHERE parameter - 'NLS_LENGTH_SEMANTICS'; PARAMETER VALUE NLS_LENGTH_SEMANTICS BYTE По умолчанию параметр NLS_LENGTH_SEMANTICS имеет значение BYTE, указываю- указывающее, что используется байтовая семантика и объявление CHARE0) эквивалентно CHARED BYTE). А значение CHAR указывает на символьную семантику, когда СНАЯE0) интерпретируется как CHARE0 CHAR). Так что имейте в виду, что администратор базы данных может изменить значение параметра NLS_LENGTH_SEMANTICS, восполь- воспользовавшись инструкцией ALTER SYSTEM. Вам тоже удастся изменить эту установку - с помощью инструкции ALTER SESSION на уровне сеанса.
Наборы символов 201 Спецификаторы BYTE и CHAR применяются не только при объявлении перемен- переменных. Их использование отражается и на интерпретации значений переменных не- некоторыми строковыми функциями, а также на том, дополняются ли значения пе- переменных типа CHAR пробелами до их максимальной длины. Рассмотрим две таб- таблицы, содержащие символы набора UTF-8: SQL> DESCRIBE Utest Name UCHAR Null? SQL> DESCRIBE utest2 Name Null? Type CHARd Type CHAR) UCHAR2 CHARC3) Один столбец объявлен с использованием символьной семантики, другой — с использованием байтовой. При этом каждый столбец занимает три байта, как в следующем запросе к словарю: SQL> SELECT tablename, coluran_name. data length, charlength. char_used 2 FROM user tabcolumns 3 WHERE coiuwwiame IN ('UCHAR1,'UCHARZ'): TABLEJiAME COLUMNJWI DATAJ.ENGTH CHAR_LENGTH CHARJJSED UTEST UCHAR 3 1 С UTEST2 UCHAK2 3 3 В Из приведенного примера видно, что оба столбца имеют длину по три байта (DATA_LENGTH) и учитывается, какая семантика использовалась при объявлении ка- каждого из столбцов (CHARJJSED). Следующий фрагмент кода PL/SQL показывает, как семантика объявления переменной отражается на работе функции LENGTH: DECLARE uchar utest.ucharSTYPE; uchar2 utESt2.uchar2STYPE; BEGIN uchar := 'a'; uchar2 := '5'; DBMSJUTPUT. PUTJJNE(LENGTH(uchar)): DBMSJJUTPUT.PUT_LINE(LENGTH(uchar2)): DBMS_OUTPUT. PUTJJNECLENGTHBCuchar) 3; DBMS_OUTPUT.PIF LINECLENGTHBCuchar2K; END; Вот что получается в результате: 1 2 2 3 Функции LENGTH и LENGTHB возвращают именно такие значения потому, что пер- первая (более подробно о ней рассказывается ниже) всегда определяет длину строки
202 Глава В • Строки в символах, а вторая — количество байтов, занимаемых содержащимися в строке символами, а не количество байтов, указанных в объявлении строки. Приведем несколько пояснений к данному фрагменту кода. О Переменные uchar и uchar2 имеют такой же тип, как и столбцы базы данных. Поэтому каждая переменная занимает три байта. О Обеим переменным присвоен один символ — «а», представляемый в Unicode UTF-8 двумя байтами. О Поскольку переменная uchar объявлена с символьной семантикой, ее длина возвращается в символах. И так как «а» — это один символ, длина uchar всегда будет равняться одному символу. О Поскольку переменная uchar2 объявлена с байтовой семантикой, один свобод- свободный байт после двухбайтового символа «а» заполняется значением символа пробела (как всегда бывает с лишними байтами строк типа CHAR). Однако ре- результирующая длина при этом возвращается в символах. О Результаты, возвращаемые функцией LENGTH, не отражают того факта, что обе переменные занимают по три байта, поскольку эта функция подсчитывает сим- символы, а не байты. О Функция LENGTHS возвращает длину переменной в байтах, но на ее результат влияет семантика объявления переменной. Данная функция определяет коли- количество байтов, занимаемых не переменной, а ее значением. Значение «а» пере- переменной uchar2 занимает два байта, но к этому значению добавляется символ пробела, и всего получается три байта. Поскольку смешение байтовой и символьной семантики вызывает путаницу, рекомендуется не использовать спецификаторы BYTE и CHAR, а задавать нужное значение параметра базы данных — NLS_LENGTH_SEMANTICS. Но независимо от того, учтете ли вы эту рекомендацию, вам необходимо знать, как спецификаторы BYTE и CHAR влияют на строковые переменные. Семантика символьных функций Такие символьные функции, как SUBSTR и INSTR, работают с позицией символов в строке. Например, первая из них позволяет задать начальную и конечную пози- позиции подстроки, которую требуется извлечь из исходной символьной строки. Эти позиции можно задать либо в байтах, либо в символах. Функции SUBSTR и INSTR всегда работают с символами, а функции SUBSTRB и INSTRB (обратите внимание на суффикс В) - с байтами. При работе с многобайтовыми наборами символов име- имеет значение, какая из функции используется в каждом конкретном случае. Если же применяется набор символов Unicode, необходимо знать еще больше разно- разновидностей символьных функций. Кодовый индекс и кодовый блок При работе с Unicode вам придется иметь дело не только с байтами и символами. Нужно усвоить еще два понятия — кодовый индекс и кодовый блок. В Unicode кодовым, индексом называется числовое значение, соответствующее записи в кодо- кодовой таблице. Например, 0x0061 - это кодовый индекс для буквы а. Иногда кодо- кодовые индексы комбинируются, образуя один символ. Например, если объединить
Наборы символов ZU3 кодовый индекс буквы а со значением 0x0303 (кодовый индекс, соответствую- соответствующий тильде), получится символ «а». Кодовый индекс 0x0303 представляет собой комбинируемый диакритический знак, то есть он всегда используется для моди- модификации символа, а не самостоятельно. ПРИМЕЧАНИЕ- В Unicode не только несколько кодовых индексов могут представлять один символ, но и, наоборот, один кодовый индекс может представлять несколько символов. Если кодовый индекс — это числовое значение в кодовой таблице, соответст- соответствующее символу, то кодовый блок — это реальное представление кодового индек- индекса. Для примера возьмем кодовый индекс Unicode 0x0061, представляющий бук- букву а. Представление этого кодового индекса в UTF-8 занимает один байт — 0x61, который является кодовым блоком. В UTF-16 тот же кодовый индекс представ- представлен двумя байтами - 0x0061. Эти два байта также составляют один кодовый блок. Как видите, размер кодового блока (то есть количество занимаемых им байтов) зависит от используемой формы набора Unicode. Иногда значение кодового ин- индекса слишком велико для одного кодового блока. В таких случаях он представ- представляется двумя или несколькими кодовыми блоками. Например, кодовый индекс OxlDi IE, соответствующий символу скрипичного ключа ($ ), в UTF-16 представ- представлен двумя кодовыми блоками — 0xD834 и OxDDlE. Но ни одно из этих двух зна- значений само по себе не представляет кодовый индекс — они делают это только вместе, а тот, в свою очередь, представляет один символ. Понимание сущности таких понятий, как байт, символ, кодовый индекс и ко- кодовый блок, важно для работы с функциями, подобными LENGTH, INSTR и др. Что вас интересует, когда требуется узнать длину строки, - количество байтов, сим- символов, кодовых индексов или кодовых блоков? Oracle поддерживает разновидно- разновидности этих функций для работы с каждой из перечисленных выше семантик. На- Например, для того чтобы узнать количество содержащихся в строке символов Uni- Unicode, следует воспользоваться функцией LENGTHC. Некоторые символы Unicode могут быть представлены несколькими способами. В UTF-16 символ «а» пред- представляется как кодовый индекс ОхООЕЗ либо как два индекса — 0061 и 0x0303. Стандартная функция LENGTH воспримет два кодовых индекса как два символа, а функция LENGTHC распознает последовательность 0x0061,0x0303 как один символ. ВНИМАНИЕ В Unicode существует множество тонкостей и нюансов. Мы с вами уже говорили о том, что встреча- встречаются необычные случаи, когда один символ Unicode может интерпретироваться как несколько сим- символов определенного языка. Поэтому настоятельно рекомендуем тем, кто работает с Unicode, озна- ознакомиться с ресурсами, содержащимися на сайте http://unicode.org. Помимо функций LENGTH и LENGTHC существуют также функции LENGTH2 и LENGTH4. Функция LENGTH2 подсчитывает количество содержащихся в строке кодовых бло- блоков, а функция LENGTH4 - количество кодовых индексов. Поэтому важно понимать различие между кодовыми блоками и кодовыми индексами. Такие же разновид- разновидности, как у функции LENGTH, имеются и у других строковых функций, например У SUBSTR и INSTR. В документации Oracle вы найдете ссылки на UCS-2 (кодовый блок) и UCS-4 (кодовый индекс). Суффиксы функций LENGTH2 и LENGTH4 происходят именно от
204 Глава 8 • Строки этих аббревиатур. Первоначально они определялись стандартом ISO, дублиро- дублировавшим спецификацию Unicode, но теперь не используются. Эквивалентность строк Unicode Проверяя две символьные строки на эквивалентность, ядро PL/SQL не учитыва- учитывает, что один символ Unicode может иметь более одного представления. Строка, содержащая один кодовый индекс ОхООЕЗ, который представляет символ «а», не считается эквивалентной строке, содержащей этот же символ в виде двух кодо- кодовых индексов - 0x0061 и 0x0303. Например: DECLARE х NVARCHAR2C30): у NVARCHAR2C30): BEGIN х ¦.- UNISTRC\00E3-); у :- UNISTRC'Ч0061ЧОЗОЗ'); IF х - у THEN DBMS_OUTPUT.PUT LINEC'x = у1): ELS IF х о у THEN ~ DBMS_OUTPUT.PUT_LINEC'x <> у1): END IF; END; Результат выполнения этого кода таков: х о у Для обработки данной ситуации используется функция COMPOSE, о которой рас- рассказывается ниже. Делается это следующим образом: IF CQMPOSECx) = COMPOSE (у) Однако в одном случае функция COMPOSE не поможет — когда символ представ- представляется и замещающей парой, и значением кодового индекса. ВНИМАНИЕ - В Orade9l Release 1 функция COMPOSE вызывается va SQL-инструкции. В Release 2 ее можно вызы- вызывать и из кода PL/SQL. Порядок сортировки Порядок сортировки — это, пожалуй, единственный аспект работы с символьны- символьными данными, где не имеет значения проблема байтов и символов. В различных наборах для представления символов используются разные числовые значения. И это отражается на операторах сравнения, которые одну строку или символ срав- сравнивают с другой строкой или символом. Прописпая буква А «больше» или «мень- «меньше» строчной буквы а? Это зависит от используемого набора символов. Если та- таковым является набор ASCII, буквы верхнего регистра будут «меньше» букв нижнего регистра, если же используется набор EBCDIC — наоборот. Строковые типы данных Oracle поддерживает четыре строковых типа данных (табл. 8.1). Какой из них ис- использовать, зависит от ответов на следующие вопросы. О С какими строками вы работаете — переменной или фиксированной длины?
Строковые типы данных 205 О Какой набор символов вы хотите использовать — базы данных или нацио- национальный? Таблица 8.1. Строковые типы данных PL/SQL Набор символов Базы данных Национальный Фиксированная длина CHAR NCHAR Переменная длина VARCHAR2 NVARCHAR2 Типы данных фиксированной длины - CHAR и NCHAR - в приложениях Oracle используются очень редко. Их вообще не рекомендуется применять, если нет осо- особых причин работать именно со строкой фиксированной длины. Далее, в разделе «О работе со строками», рассказывается о проблемах, которые могут возникнуть при совместном использовании строковых переменных фиксированной и пере- переменной длины. Тип данных VARCHAR2 В переменных типа VARCHAR2 хранятся символьные строки переменной длины. При объявлении такой строки необходимо определить для нее максимальную длину, которая может иметь значение от 1 до 32 767 байт. Максимальная длина может быть задана в байтах или символах, но в любом случае для строки будет выделено определенное количество байтов. Общий синтаксис объявления переменной типа VARCHAR2 таков: ш_переменной VARCHAR2 [наксинальная_влина [CHAR | BYTE]); Здесь имя_переменной — это имя объявляемой переменной, максимальная_длина — ее максимальная длина, CHAR и BYTE — аргументы, указывающие, что максимальная _цт- на выражена в символах и в байтах соответственно. Если максимальная длина строковой переменной VARCHAR2 задается в символах (с использованием спецификатора CHAR), ее реальная длина в байтах определяет- определяется на основе максимального количества байтов, используемых для представле- представления одного символа в наборе символов базы данных. Например, в наборе Unicode UTF-8, как вы помните, для представления отдельных символов используется три байта. Поэтому если вы работаете с UTF-8, объявление переменной типа VAR- CHAR2, максимальная длина которой составляет 100 символов, эквивалентно объяв- объявлению этой же переменной с указанием длины, равной 300 байт. Если в объявлении переменной VARCHAR2 опустить спецификатор CHAR или BYTE, тогда в зависимости от значения параметра NLS_LENGTH_SEMANTICS заданное значе- значение максимальной длины будет интерпретировано как количество байтов или ко- количество символов. По умолчанию указанный параметр имеет значение BYTE, так что объявление без спецификатора обычно соответствует заданию длины строки в байтах. Приведем несколько примеров объявлений строк типа VARCHAR2: DECLARE sma11_string VARCHAR2C4): line_of_text VARCHAR2<2000):
Глава 8 • Строки featurejiame VARCHAR2C10Q BYTE): -- Строка длиной 100 байт empjiame VARCHARZOO CHAR); -- Строка длиной 30 символов Итак, максимальная длина переменной типа VARCHAR2 в PL/SQL составляет 32 767 байт — гораздо больше, чем максимальная длина переменной типа данных VARCHAR2 в РСУБД Oracle B000 байт в версиях до Oracle8i и 4000 байт в Огас1е8г и выше). Это ограничение действует независимо от того, определяете вы длину строки в байтах или символах. Так что если вы собираетесь помещать значение переменной VARCHAR2 в столбец базы данных, учтите, что его максимальная длина меньше, чем у переменной. Ни в PL/SQL, ни в SQL не предусмотрено никаких средств для автоматического устранения этого несоответствия. ПРИМЕЧАНИЕ Если вам нужно работать со строками длиной более 4000 байт (или 2000 байт в версиях до OradeSi), эгти строки можно хранить в столбцах типа CLOB Сто есть как большие символьные объекты). О столбцах данного типа подробно рассказывается в главе 12. Тип данных CHAR Тип данных CHAR определяет строху фиксированной длины. Объявляя такую стро- строку, нужно задать ее максимальную длину в диапазоне от 1 до 32 767 байт. (Это на- намного больше максимальной длины строки типа CHAR в РСУБД Oracle — 2000 байт, а также в версиях до Oracle8i — 255 байт.) Длина может быть задана как в байтах, так и в символах. Например, следующие два объявления создают строки длиной 100 байт и 100 символов соответственно: featurejwme CHARdQO BYTE): featurejiame CHARUOO CHAR); Реальное количество байтов в 100-символьной строке зависит от текущего набо- набора символов базы данных. Если используется набор символов переменной длины, PL/SQL выделяет для строки столько места, сколько необходимо, чтобы в нее поместилось заданное количество символов, для представления которых требу- требуется максимальное количество байтов. Например, в наборе UTF-8, где символы имеют длину от 1 до 3 байт, PL/SQL, создавая строку для хранения 100 симво- символов, зарезервирует 300 байт C байта х 100 символов). Мы уже знаем, что если не задать спецификатор CHAR или BYTE, результат будет зависеть от значения параметра NLS_LENGTH_SEMANTICS. И так как по умолчанию оно равно BYTE, результатом следующего объявления будет строка длиной 100 байт. featurejiame CHARUOO); Если же опустить длину строки, PL/SQL объявит строку длиной 1 байт. Пред- Предположим, что вы объявили переменную так: featurejiame CHAR; Как только этой переменной присваивается строка длиной более одного сим- символа, PL/SQL инициирует универсальное исключение - VALUE_ERROR. Но при этом не указывается, где именно возникла проблема. Так что в случае обнаружения ошибки проверьте объявления переменных на предмет незаданной длины стро- строки. Для того чтобы избежать проблем и облегчить работу программистам, кото- которые придут вам на смену, всегда задавайте длину строки типа CHAR.
Строковые типы данных 207 Приведем несколько примеров: yes ог_по CHARC1) DEFAULT 'У: line of_text CHARCBO CHAR); -- Всегда все 80 символов! whole paragraph CHARC100QO BYTE); -- Подунайте обо всех этих пробелах... Поскольку строка типа CHAR имеет фиксированную длину, то PL/SQL в случае необходимости дополняет присвоенное переменной CHAR значение символами про- пробелов, добиваясь таким образом соответствия заданной в объявлении максималь- максимальной длине. ПРИМЕЧАНИЕ До версии Огас1е7 строки типа данных CHAR имели переменную длину, а символьные строки посто- постоянной длины вообще не поддерживались. Однако для улучшения совместимости с реляционными базами данных IBM и для обеспечения соответствия стандартам ANSI в Огас1е7 тип CHAR был пере- переопределен как тип данных строк фиксированной длины, а для строк переменной длины был введен тип VARCHAR2. ФИКСИРОВАННАЯ ИЛИ ПЕРЕМЕННАЯ ДЛИНА СТРОКИ Объявляя переменную типа CHAR с указанием длины в байтах и используя ее для хранения символов многобайтового набора, можно получить совер- совершенно неожиданные результаты. Следующий пример был выполнен в сис- системе, где в качестве набора символов базы данных используется UTF-8: DECLARE х CHARC); BEGIN -- Присваиваем однобайтовый символ х :- 'а': DBMS_OUTPUT.PUT_LINE(LENGTH(x)): -- А теперь двухбайтовый символ х :- 'а1: DBMS_OUTPUT.PUT_LINE(LENGTH(x)): -- А теперь два символа общей длиной три байта х :- 'За1; DBMS_OUTPUT.PUT_LlNE(LENGTHCx)); END; Результаты выполнения этого кода таковы: 3 2 2 Правда, интересно, что фиксированная длина строки каким-то образом изменяется? Как и то, что длина символа «а» такая же, как длина пары символов «аа». Объясняется это тем, что функция LENGTH подсчитывает сим- символы. В первом случае строка содержит символ «а», за которой следуют два пробела, так что общая длина строки получается равной трем симво- символам. Во втором случае за двухбайтовым символом «а* следует один про- пробел, так что общая длина строки составляет два символа (хотя и три байта). В последнем случае за двухбайтовым символом «а> следует однобайто- однобайтовый символ «а», и общая длина строки снова равняется двум символам, продолжение^
208 Глава 8 • Строки Выполните тот же фрагмент кода, объявив переменную х как CHARC CHAR), и длина строки изменяться не будет. Таким образом, при работе с много- многобайтовыми символами длину строк в объявлении переменной следует за- задавать в символах. Типы данных NCHAR и NVARCHAR2 Типы данных NCHAR и NVARCHAR2 представляют строки, состоящие из символов на- национального набора, а не символов набора базы данных. Кроме того, длина таких строк всегда объявляется в символах (спецификатор CHAR при этом не использу- используется). Во всем остальном NCHAR и NVARCHAR2 ничем не отличаются от типов данных CHAR и VARCHAR2. Первый из них используется для хранения строк постоянной дли- длины, а второй - переменной. Например: line_of_text NVARCHAR2C20DQ); -- 2000 симаолов, переменная длина featurejiame NCHAR(IOO): -- 100 симаолов. постоянная длина В версиях до Oracle9i в качестве национального набора символов мог исполь- использоваться любой однобайтовый или многобайтовый набор. Однако начиная с Огас- 1еЭг, национальным набором символов, используемым для переменных типов NCHAR и NVARCHAR2, может быть только UTF-8 (многобайтовые символы со строками пе- переменной длины) или UTF-16 (многобайтовые символы со строками фиксиро- фиксированной длины). ПРИМЕЧАНИЕ Понятия национального набора символов и набора символов базы данных подробно описывались в разделе «Набор символов базы данных и набор символов национального языка» ранее в этой главе. В OracleSi типы данных NCHAR и NVARCHAR2 были усовершенствованы, и теперь их можно совместно использовать в выражениях с типами данных CHAR и VARCHAR2. Если в версиях до ОгасЬЭг в ответ на попытку сравнить значение VARCHAR2 со зна- значением NVARCHAR2 PL/SQL выдавал сообщение об ошибке PLS-00561: character mis- mismatch, то теперь подобное сравнение допускается. Конечно, за это приходится рас- расплачиваться некоторым снижением эффективности вычислений, поскольку PL/SQL должен неявно конвертировать значения к общему набору символов. Строковые подтипы PL/SQL поддерживает строковые подтипы данных (табл. 8.2), которые тоже мо- могут использоваться для объявления символьных строк. Однако многие из этих подтипов определены только для обеспечения совместимости со стандартом ANSI SQL Маловероятно, что они вам когда-либо понадобятся, но знать о них все же нужно. Каждый из перечисленных в таблице подтипов эквивалентен одному из базо- базовых типов данных PL/SQL, указанных в правом столбце. Например: featurejiame VARCHAR2U00); featurejiame CHARACTER VARYING(IOO); featurejiame CHAR VARYING(IOO); featurejiame STRINGUOO);
О работе со строками 2O9 Таблица 8.2. Строковые подтипы и эквивалентные им типы данных PL/SQL Подтип Эквивалентный тип CHAR VARYING CHARACTER CHARACTER VARYING NATIONAL CHAR NATIONAL CHAR VARYING NATIONAL CHARACTER NATIONAL CHARACTER VARYING NCHAR VARYING STRING VARCHAR VARCHAR2 CHAR VARCHAR2 NCHAR NVARCHAR2 NCHAR NVARCHAR2 NVARCHAR2 VARCHAR2 VARCHAR2 Подтип VARCHAR заслуживает особого внимания. Уже на протяжении несколь- нескольких лет корпорация Oracle собирается изменить значение подтипа данных VARC- VARCHAR (в результате чего он перестанет быть эквивалентным типу VARCHAR2) и преду- предупредила, что пользоваться им не следует. Мы придерживаемся этой рекоменда- рекомендации. Если существует вероятность такого изменения, не имеет смысла вводить в свои программы подтип данных VARCHAR. Используйте вместо него VARCHAR2. О работе со строками Работать со строками в основном довольно просто. Однако существует несколько тонкостей, о которых программистам обязательно следует знать. О них рассказы- рассказывается в следующем разделе. Пустая строка — это NULL Одна из причин постоянных недоразумений - особенно для тех программистов, которые перешли к Oracle от других СУБД и языков программирования — за- заключается в том, что Oracle интерпретирует пустые строки как значение NULL. Это не согласуется со стандартом ANSI SQL, определяющим четкое различие между пустой строкой и строковой переменной, имеющей значение NULL: /* Файл в web: chO8 emptyisnull.sql */ DECLARE empty varchar2 VARCHAR2U0) :- rl: emptyj:har CHARUO) :- "; BEGIN IF empty varchar2 IS NULL THEN DBMS_OUTPUT.PUT_LINE('Пустая строка типа varcnar? зто NULL1): END IF; IF '' IS NULL THEN DBMS_OUTPUT.PUT_LINEC это NULL'): END IF-
210 Глава 8 • Строки IF empty_char IS NULL THEN DBMS OUTPUT.PUT_LINE('Пустая строка типа char это NULL'): END IF;" END; Код выводит следующий результат: Пустая строка типа varchar2 это NULL " зто NULL Данный пример показывает, что переменная типа CHAR не интерпретируется как NULL. Ведь это переменная фиксированной длины, и она не может быть пустой по определению. В приведенном примере она просто заполняется пробелами до длины 10 символов. А вот переменная VARCHAR2 равняется NULL, поскольку ей при- присвоен строковый литерал нулевой длины. На эту особенность строковых переменных следует обращать внимание при их сравнивании в операторах IF. Возьмем программу, которая запрашивает у поль- пользователя имя и сравнивает это имя со значением, прочитанным из базы данных: DECLARE user_entered_name VARCHAR2C30); name_froni_database VARCHAR2O0): BEGIN IF user_entered_name <> name_fi"om_database THEN Если пользователь введет вместо имени пустую строку, результат проверки заданного в этом примере условия IF никогда не будет равен TRUE, поскольку зна- значение NULL не может быть равным или не равным никакому другому значению. Альтернативный способ записи данного оператора IF таков: IF (userenteredjiame о name_from_database) OR Cuser_entered_name IS NULL) THEN Это только один из способов решения проблемы равенства значению NULL пус- пустой строки, но его можно применять не в каждой ситуации. В общем случае мож- можно сказать так: вы должны продумать задачу, учесть, что пустые строки будут ин- интерпретироваться как NULL, и написать соответствующий код. Смешение значений CHAR и VARCHAR2 Если вы используете в программе строки как фиксированной (CHAR), так и пере- переменной (VARCHAR2) длины, следует учитывать способ выполнения операций в Oracle, где участвуют оба типа строк. Об этом рассказывается в следующих разделах. Перенос информации из базы данных в значение переменной При извлечении (SELECT или FETCH) данных из столбца базы данных типа CHAR и присвоению их значению переменной типа VARCHAR2 завершающие пробелы со- сохраняются. Если же перенести данные из столбца типа VARCHAR2 в переменную типа CHAR, PL/SQL автоматически дополнит извлекаемое значение пробелами до максимальной длины переменной. То есть результирующее значение переменной определяется ее типом, а не типом столбца.
О работе со строками 211 Перенос значения переменной в базу данных Если вы помещаете (INSERT или UPDATE) значение переменной типа CHAR в столбец базы данных типа VARCHAR2, ядро SQL не подавляет завершающие пробелы. На- Например, при выполнении следующего кода PL/SQL столбцу companyjiame базы данных присваивается значение ACME SHOWERS (где символ «¦» обозначает про- пробел). Иными словами, столбец дополняется пробелами до 20 символов, хотя ис- исходным значением была строка из 12 символов: DECLARE comp_id# NUMBER; compjmme CHARB0) :- 'ACME SHOWERS'; BEGIN SELECT company_id_seq.NEXTVAL INTO compjdf FROM dual; INSERT INTO company (companyjd. companyjiame) VALUES (comp_1d#, compjiame): END: С другой стороны, когда вы помещаете значение переменной типа VARCHAR2 в столбец базы данных типа CHAR, ядро SQL автоматически дополняет строку пе- переменной длины пробелами до максимальной длины столбца (заданной при соз- создании таблицы). Сравнение строк Предположим, что некоторая программа содержит следующий оператор сравне- сравнения строк: IF companyjiame - parent_company_n3me ... PL/SQL должен сравнить значения companyjiame и parent_companyjiame. Эта опе- операция выполняется одним из двух способов, в зависимости от типов переменных. О При наличии двух переменных типа CHAR PL/SQL производит сравнение с до- дополнением завершающими пробелами. О Если хоть одна из строк имеет переменную длину, PL/SQL выполняет сравне- сравнение без дополнения завершающими пробелами. Различие между этими двумя методами сравнения видно в следующем фраг- фрагменте кода: DECLARE companyjiame CHARC30) :" 'Мама мыла раму'; char_parent_company_name CHARC35) :- 'Мама мыла рану'; varchar2_parent_company_name VARCHAR2C35) := 'Мама мыла рану'; BEGIN -- Сравниваем два значения CHAR, поэтому строки дополняются пробелами IF companyjiame = char_parent_company_name THEN DBMSJXJTPUT.PUTJJNE ('Результат первой проверки равен TRUE')- ELSE DBMS OUTPUT.PUTJJNE ('Результат первой проверки равен FALSE1)- END IF;
212 ' Глава 8 • Строки -- Сравниваем CHAR и VARCHAR, поэтову строки не дополняются пробелами IF company_name - varchar2_parent_company_name THEN DBMS_OUTPUT.PUT_LINE ('Результат второй проверки равен TRUE1): ELSE DBMS_OUTPUT.PUTJ_INE ('Результат второй проверки равен FALSE'); END IF: END: Результат выполнения этого кода таков: Результат первой проверки равен TRUE Результат второй проверки рааен FALSE Первыми сравниваются два значения типа CHAR, поэтому производится допол- дополнение завершающими пробелами: PL/SQL дополняет более короткое из двух зна- значений до длины более длинного значения. После этого выполняется их сравнение. В данном примере PL/SQL добавляет пять пробелов в конец значения перемен- переменной company_name и затем сравнивает значения companyjiame и char_parent_compa- nyname. После этого обе строки имеют равную длину. Обратите внимание, что PL/SQL не изменяет значение переменной company_name. Он просто копирует это значение в другую структуру памяти и там модифицирует его для сравнения. Во второй операции участвуют значения типов VARCHAR2 и CHAR, поэтому PL/SQL выполняет сравнение без дополнения завершающими пробелами. Он не меняет ни одного из значений и использует их текущую длину. В данном случае первые 22 символа обеих строк одинаковы (Fejerstein and Friends), но значение перемен-, ной фиксированной длины company_name дополнено восемью пробелами, а значе- значение переменной varchar2_parent_company_nanie — нет. В результате строки оказыва- оказываются не равными. Участие в проверке значения типа VARCHAR2 говорит о том, что сравнение будет выполнено без дополнения завершающими пробелами. Это верно и для выраже- выражений, содержащих более двух переменных, а также для выражений с оператором IN. Например: IF menu_selection NOT IN (save_and close. cancel_and_ex1t. 'OPEN_SCREEN') THEN ... Если хоть одна из четырех строк данного примера (menu_selection, две имено- именованные константы и один литерал) объявлена как VARCHAR2, точное сравнение бу- будет выполнено без модификации значений. Обратите внимание на то, что литерал, подобный OPEN_SCREEN, всегда рассматривается как строка фиксированной длины, то есть значение типа CHAR. Символьные функции и аргументы типа CHAR Символьная функция — это функция, принимающая в качестве параметров одно или несколько символьных значений и возвращающая символьное или числовое значение. Возвращаемое функцией символьное значение всегда имеет тип VAR- CHAR2 (переменной длины), если речь идет не о функциях UPPER и LOWER. Указанные функции переводят заданную строку в верхний или нижний регистр соответст- соответственно и возвращают значение фиксированной длины типа CHAR, если заданные в качестве их аргументов строки имели тип CHAR.
О работе со строками 213 Определение строковых констант Задаваемые строковые константы берутся в одинарные кавычки; ¦Brighten the corner where you are' Если одинарную кавычку нужно включить в строковую константу, ее следует продублировать: 'Aren"t you glad you "re learning PL/SQL?1 Как правило, строковые константы задают на основе набора символов базы данных. Если такая константа присваивается значению переменной типа NCHAR или NVARCHAR2, она неявно преобразуется в символы национального набора. СУБД Oracle сама выполняет такие преобразования по мере необходимости, и програм- программисту об этом заботиться не нужно. Если же потребуется явно указать, что стро- строковая константа задана с применением национального набора символов, помес- поместите перед ней префикс N: Юта строка задана в национальной наборе символов.' При выполнении кода PL/SQL в SQL*Plus проблемы часто вызывает символ амперсанда (&). Дело в том, что в SQL*Plus указанный символ используется в каче- качестве префикса переменных, и, встретив его, SQL*Plus запрашивает у вас значение переменной. Например: SQL> BEGIN 2 DBMSJWTPJT.PUT_LINE('Tom & Jerry.'}; 3 END: 4 / Enter value for saving: Существует несколько способов решения этой проблемы. Один из них состо- состоит в выполнении команды SO_L*Plus SET DEFINE OFF, отменяющей подстановку пе- переменных. В следующих разделах подробно описывается каждая из функций, пе- перечисленных в таб. 8.3. Таблица 8.3. Строковые функции Иня Описание ASCII Возвращает ASCII-код символа ASQISTR Преобразует исходную строку в строку символов набора ASCII CHR Возвращает символ, соответствующий заданному коду COMPOSE Принимает строку символов набора Unicode и возвращает ее в нормализованной форме CONCAT Выполняет конкатенацию двух строк (то есть объединяет их в одну) DECOMPOSE Принимает строку символов набора Unicode и возвращает ее в виде строки, где каждый составной символ «разобран» на составляющие элементы GREATEST Возвращает строку, которая при сортировке по алфавиту будет последней INITCAP Переводит первую букву каждого слова в верхний регистр, а все остальные буквы — в нижний продолжением
214 Глава 8 ¦ Строки Таблица 8.3 (продолжение) Имя 1NSTR, INSTRB, INSTRC, INSTR2, INSTR4 LEAST LENGTH, LENGTHB, LENGTHC, LENGTH2, LENGTH4 LOWER LPAD LTRIM REPLACE RPAD RTRIM SOUNDEX SUBSTR, SUBSTRB, SUBSTRC, SUBSTR2, SUBSTR4 TO_CHAR TO_MULTI_BYTE TO_SINGLE_BYTE TRANSLATE TRANSLATE...USING TRIM UNISTR UPPER Описание Возвращает позицию заданной подстроки в строке Возвращает строку, которая при сортировке по алфавиту будет первой Возвращает длину строки Переводит все буквы строки в нижний регистр Дополняет строку слева заданными символами Удаляет все заданные символы в левой части строки Заменяет последовательность символов в строке указанной последовательностью символов Дополняет строку справа заданными символами Удаляет все заданные символы в правой части строки Возвращает символьную строку, содержащую фонетическое представление заданного аргумента Возвращает заданную часть строки Преобразует символы национального набора в символы набора базы данных Принимает строку и преобразует в ней однобайтовые символы в их многобайтовые эквиваленты. Например, в UTF-8 многие символы могут быть представлены как одним, так и несколькими байтами Выполняет действие, обратное действию функции TO_MULTT_BYTE — преобразует многобайтовые символы в их однобайтовые эквиваленты Преобразует отдельные символы в строке в указанные символы Преобразует символы одного набора в символы другого Позволяет выполнить действия функций RTRIM и LTRIM Выполняет действие, обратное действию функции ASCIISTR — преобразует строку в Unicode Переводит все буквы в строке в верхний регистр Строковые функции PL/SQL предоставляет богатый набор строковых функций, позволяющих полу- получать информацию о строках и модифицировать их содержимое. Перечень основ- основных строковых функций PL/SQL представлен в табл. 8.3, а ниже приводится их подробное описание. Кроме перечисленных в таблице существуют также функ- функции, специфичные для Oracle: Trusted Oracle и National Language Support (NLS). Последние рассматриваются далее в этой главе, в разделе «Функции NLS».
Строковые функции 215 функция ASCII Возвращает код типа данных NUMBER, представляющий заданный символ в наборе символов базы данных. Приведем ее синтаксис: FUNCTION ASCII (оцин_символ IN VARCHAR2) RETURN NUMBER Здесь один_символ — это символ, код которого необходимо определить. Хотя функция называется ASCI I, она возвращает код символа в текущем наборе симво- символов базы данных, например в EBCDIC Code Page 500 или 7-битовом ASCII. Об- Обратите внимание, что коды букв верхнего регистра отличаются от кодов тех же букв нижнего. Например, для 7-битового набора символов ASCII вызов ASCI I (' а') возвращает значение 97, а вызов ASCIIС 'А') - 65. Если передать функции ASCII более одного символа, она вернет код первого из них, а остальные просто проигнорирует. Поэтому следующие три вызова воз- возвращают оно и то же значение - 228: ASCII('flwip') ASCII('fl') ASCII Сс_е_ё_ж') Функция ASCIISTR Принимает в качестве аргумента строку любого набора символов и преобразует ее в строку набора ASCII. Любые не ASCII-символы выводятся в форме \XXXX, где ХХХХ — Unicode-значение символа. Синтаксис функции ASCIISTR таков: FUNCTION ASCIISTR (арока_1 IN VARCHAR2) RETURN VARCHAR2 Приведем пример ее использования: BEGIN DBMS_OUTPUT.PUT_UNE( ASCIISTRC'Символ 3 не является символом набора ASCII.') ); END: Результат выполнения этого кода: Символ S0QE3 не является символом набора ASCII. Функция UNISTR, описанная далее в этой главе, выполняет обратную операцию. ПРИМЕЧАНИЕ Информацию об Unicode, и в частности о байтовых кодах символов Unicode, вы найдете на web-узле по адресу: http://unlcode.org. Функция CHR Является обратной функции ASCII. Она возвращает символ VARCHAR2 (строку дли- длиной 1 символ), соответствующий заданному в ее аргументе коду. Приведем син- синтаксис функции: FUNCTION CHR (ков IN NUMBER) RETURN VARCHAR2 Здесь ков - это значение, определяющее символ в наборе символов базы данных.
216 Глава 8 • Строки Функцию CHR особенно удобно использовать в тех случаях, когда в программном коде нужна ссылка на непечатаемый символ. Например, символ новой строки в ASCII имеет код 10. Пользуясь функцией CHR, его можно найти в строке и вы- выполнить после его обнаружения определенные действия. Используя функцию CHR, можно также вставить в строку символ новой строки. Предположим, вам нужно создать отчет, в котором выводится адрес компании. Помимо строк с названиями города, страны и индекса адрес может содержать до четырех дополнительных строк, и значение каждой из них нужно выводить с но- новой строки. При этом пустых строк быть не должно. В этой ситуации SQL-инст- SQL-инструкция SELECT бессильна: SELECT name, addressl. address2, address3. address4, city || '. ' || state || ' '|| zipcode location FROM company; Если каждый столбец (поле отчета) выводится с новой строки, у вас получит- получится адрес, состоящий из шести строк, независимо от того, сколько из них равно NULL. Например: HAROLD HENDERSON 22 BUNKER COURT SUITE 100 WYANDACH, MN 66557 Функцией CHR можно воспользоваться и для удаления пустых строк: SELECT name 11 DECODE (addressl. NULL. NULL. CHR A0) || addressl) || DECODE (address2, NULL, NULL. CHR A0) 11 address2) || DECODE (address3. NULL. NULL, CHR A0) || address3) || DECODE Caddress4. NULL. NULL. CHR A0) \\ address*» Ц CHR A0) || city || '. ' || state 11 ' ' 11 zipcode FROM company; Теперь запрос возвращает по одному форматированному столбцу, соответст- соответствующему каждой компании. Функция DECODE позволяет включить в SQL-инст- SQL-инструкцию логику IF.. .THEN: «если строка адреса равна NULL, то добавить в формируе- формируемую строку NULL; в противном случае добавить символ перевода строки и за ним строку адреса». Таким образом, пустые строки адреса игнорируются, и он умень- уменьшается на две строки: HAROLD HENDERSON 22 BUNKER COURT SUITE 100 WYANDACH. MN 66557 По умолчанию функция CHR преобразует указанный код символа в символ на- набора базы данных. Если же вы предполагаете использовать символы националь- национального набора, воспользуйтесь предложением USING NCHAR_CS: DECLARE х NVARCHAR2C0); BEGIN X :- CHRF5 USING NCHAR_CS); Ту же операцию выполняет функция "NCHR, о ней будет рассказано ниже.
Строковые функции 217 Функция COMPOSE Принимает в качестве аргумента строку набора символов Unicode и возвращает эту же строку в нормализованной форме. Ее синтаксис таков: FUNCTION COMPOSE (.crpoxaj IN VARCHAR2) RETURN VARCHAR2 Набор символов Unicode позволяет представлять отдельные символы несколь- несколькими способами. Например, символ «а.» может быть представлен так: а - \00E3 Его можно представить и следующим образом: а = а\0303 В первом случае \00E3 - кодовый индекс Unicode (в шестнадцатеричной сис- системе) символа «а». Во втором случае \0303 — несамостоятельный символ тильды набора Unicode, добавляемый к предыдущему символу «а». Таким образом, сим- символ «а» формируется из буквы а и символа тильды. Полный символ \00E3 назы- называется составным символом. Функция COMPOSE принимает в качестве значения стро- строку, состоящую из двух определенных символов, такую как а\0303, и преобразует ее в один составной символ. Например; DECLARE х VARCHAR2C30): у VARCHAR2C0): BEGIN SELECT COMPOSECUNISTRC"a\0303 это составной символ')). .ASCIISTR(COMPOSE(UNISTRCa\0303 зто составной символ'))) INTO х. у FROM dual; DBMS_OUTPUT.PUT_LINE(x); DBMS_OUTPUT.PUT_LINE(y): ЕМЗ: Вот что выводит этот код: з это составной символ \00E3 это составной символ ВНИМАНИЕ В Oracle9i Release 1 функцию COMPOSE нужно было вызывать только из SQL-инструкции; в програм- программах PL/SQL она использоваться не могла. В Oracle9i Release 2 ее можно вызывать и из кода PL/SQL. Функция CONCAT Выполняет конкатенацию двух строк типа VARCHAR2 и возвращает объединенную строку, в которой исходные строки следуют одна за другой в заданном порядке. Ее синтаксис таков: FUNCTION CONCAT (сгрокд_1 IN VARCHAR2. crpoxaj IN VARCHAR2) RETURN VARCHAR2 Функция CONCAT всегда добавляет значение аргумента строиа_2 в конец значе- значения аргумента строка_1. Если одна из заданных строк равна NULL, она возвращает
218 • Глава 8 • Строки тот аргумент, значение которого не равно NULL. Приведем несколько примеров ис- использования этой функции (символ «-»» означает, что функция возвращает ука- указанное значение): CONCAT С'абв'. 'где') -» 'абвгде' CONCAT (NULL, 'нал') -> 'жзл' CONCAT Саб'. NULL) -> 'аб' CONCAT (NULL, NULL) -+ NULL В PL/SQL (и РСУБД Oracle) для объединения строк служит также оператор конкатенации — две вертикальные линии A1). Например: DECLARE х VARCriAR2A00): BEGIN х :- 'абв' |] 'где' || 'жзл'; DBMS_OUTPUT.PUT_LINE(x): END; Результат выполнения этого кода таков: абагдежзл Чтобы выполнить такую же операцию с помощью функции CONCAT, нужно один ее вызов вложить в другой: х :- ОТСАТСгаСАТГабв'. 'где'), 'жэл'): Как видите, оператором 11 пользоваться гораздо удобнее, чем функцией CONCAT, и результирующий код получается гораздо более читабельным. Функция DECOMPOSE Ее действие противоположно действию функции COMPOSE. Функция DECOMPOSE при- принимает строку набора символов Unicode и возвращает строку, в которой состав- составные символы заменены соответствующими им парами символов. Ее синтаксис таков: FUNCTION DECOMPOSE UrpoKal IN VARCHAR2) RETURN VARCHAR2 Рассмотрим следующий пример: DECLARE х VARCHAR2C0): BEGIN x :- '5 это составной символ'; SELECT ASCIISTR(DECOMPOSE(x)) INTO x FROM dual; DBMS_OUTPUT.PUT_LINE(x): END; Вот что выводит этот код: а\0303 это составной сиивол В данном случае выполнена декомпозиция 'символа «S* входной строки, в ре- результате чего получаем символ «а» и кодовый индекс, соответствующий несамо- несамостоятельному символу \0303, представляющему тильду.
Строковые функции 219 ВНИМАНИЕ В Oracle9i Release 1 функция DECOMPOSE, подобно функции COMPOSE, вызывалась не непосредст- непосредственно из программы PL/SQL, а только из SQL-инструкции. В Orade9i Release 2 это ограничение снято. функция GREATEST Принимает в качестве аргумента одну или несколько строк и возвращает строку, которая при сортировке по алфавиту оказалась бы последней. Сортировка строк производится в соответствии с кодами символов набора базы данных. Синтаксис функции таков: FUNCTION GREATEST (строка_1 IN VARCHAR2, строкаJ IN VARCHAR2. ...) RETURN VARCHAR2 Приведем пример: BEGIN DBMS_OUTPUT.PUT_LINE(GREATESTCВладимир'. 'Антон'. 'Борис')): END; Владимир Обратитесь также к описанию функции LEAST, противоположной по действию функции GREATEST. Функция INITCAP Изменяет регистр символов строкового аргумента, переводя первую букву каж- каждого слова строки в верхний регистр, а оставшиеся буквы — в нижний. Словом считается последовательность символов, отделенная от остальных символов пробе- пробелом или не буквенно-цифровым символом (как «#» или «_»¦). Синтаксис функ- функции INITCAP таков: FUNCTION INITCAP Шрокд_1 IN VARCHAR2) RETURN VARCHAR2 Приведем несколько примеров использования этой функции. О Перевод строки нижнего регистра в строку, каждое слово которой начинается с большой буквы: INITCAPC'это нижний регистр') -> 'Это Нижний Регистр' О Перевод строки верхнего регистра в строку, каждое слово которой начинается с большой буквы: ШТ(Ж'Б0ЛЫЮЙ>1СВШЖИЙ') -> 'Болыюй>И*Высокий' О Перевод строки, содержащей символы верхнего и нижнего регистров в строку, каждое слово которой начинается с большой буквы: IMITCAPt'чТоЭТ0всЕ_знаЧИТ7') -> 'Чтоэтовсе_3начит?' О Формирование имени переменной языка Visual Basic (для удаления символов подчеркивания воспользуемся функцией REPLACE, о которой рассказывает- рассказывается ниже в этой главе): REPLACE(INITCAP('ALMOST_UNREADABLE_VAR_NAME'). '_'. NULL) -> 'ATmostUnreadableVarName'
220 Глава 8 • Строки В каких же случаях применяется функция INITCAP? Во многих базах данных Oracle все символьные строки (такие как имена, названия, адреса и т. п.) хранятся в верхнем регистре. Это облегчает поиск записей, соответствующих определенно- определенному критерию. Хотя использовать такой «внутренний формат» удобно, выводимые данные получаются нечитабельными. Например: CUSTOMER TRACKING LIST - GENERATED ON 12-MAR-1994 LAST PAYMENT WEDNESDAY: PAUL JONSON. 123 MADISON AVE - $1200 LAST PAYMENT MONDAY: HARRY SIMMERSON. 555 AXELROD RD - $1500 Все сливается, трудно выделить отдельные слова и разные типы информации. Представленный ниже текст читается гораздо легче: Customer Tracking List - Generated On 12-Mar-1994 Last Payment Wednesday: Paul Jonson. 123 Madison Ave - $1200 Last Payment Monday: Harry Simmerson. 555 Axelrod Rd - $1500 Вы убедились, что функцию INITCAP удобно использовать при форматирова- форматировании выходных данных. Однако у нее имеется несколько недостатков. Во-первых, написание всех слов с большой буквы в заголовках — и иногда в других случа- случаях — для русского языка, в отличие от английского, не свойственно. А во-вторых, функция, не понимая смысла текста, не различает, например, тех слов, внутри ко- которых отдельные буквы должны быть большими, как D в слове MacDonald's: INITCAPC'MACDONALD"S') -> 'Macdonald'S1 Поэтому при печати отчетов для вывода данных пользуйтесь этой функцией осторожно. Функции INSTR, INSTRB, INSTRC, INSTR2, INSTR4 Семейство функций INSTR позволяет найти в строке заданную подстроку. Если в исходной строке найдена подстрока, функция возвращает позицию се первого символа. В противном случае она возвращает 0. Пять функций семейства INSTR различаются лишь способом восприятия, а сле- следовательно, и просмотра строк. О INSTR — строки состоят из символов. Возвращаемое значение указывает сим- символьную позицию подстроки. О INSTRB — строки состоят из байтов. Возвращаемое значение указывает байто- байтовую позицию, в которой найдена подстрока. О INSTRC — строки состоят из символов Unicode. Пары символов, представляю- представляющие составные символы, распознаются как составные символы (то есть сим- символы «а» и его представление «а\0303» воспринимаются как одинаковые). О INSTR2 — строки состоят из кодовых блоков Unicode. О INSTR4 - строки состоят из кодовых индексов Unicode.
Строковые функции 221 У всех функций INSTR один и тот же синтаксис: FUNCTION INSTR (.строкаJ IN VARCHAR2. строкаJ IN VARCHAR2 [. начальная _позщия IN NUMBER := 1 [. п-е_вхомцение IN NUMBER :- 1]]) RETURN NUMBER Здесь строка_1 - это строка, просматриваемая функцией INSTR в поиске параметра п-е_вхоивение строки, указанной в значении аргумента строка 2. Значение аргу- аргумента начальная_позиция определяет символьную (а не байтовую) позицию в стро- строке, с которой будет начат поиск. По умолчанию оно равно 1, то есть поиск начина- начинается с начала строки, определяемой аргументом строка_1. Аргумент п-е_ахождение также является необязательным и по умолчанию его значение равно 1, то есть ищется первое вхождение подстроки в строке. Аргументы начальная_позиция и п-е_вхожцение можно задавать и в виде литера- литералов (например, 5 или 157). переменных или сложных выражений, как в следую- следующем примере: INSTRCcompanyjiame. 'INC. (lastjocation+5)*10) Если значение аргумента начальная _позщня отрицательно, функция INSTR от- отсчитывает соответствующее количество символов с конца строки (справа) и затем начинает просмотр в направлении начала строки. На рис. 8.2 показаны два направ- направления поиска — в зависимости от знака значения аргумента начальная_позиция. INSTR(string.'A',N,M) N>0 ' ' ' " IaI Ul N 1 2...M N<0 M...2 1 ABS(N) Рис. 8.2. Просмотр строки с помощью функции INSTR Функция INSTR — очень полезная, в особенности когда используются все ее возможности. Большинство программистов применяют только два первых аргу мента, а об остальных — например, «искать с конца строки», «искать не первое, а я-е вхождение» — часто даже не знают. Не допускайте такой ошибки и освойте эту функцию в полной мере. ПРИМЕЧАНИЕ- В Orade7 при отрицательном или равном нулю значении аргумента начальная_позиция функции INSTR всегда возвращала 1. В OracleB эти же значения вызывали исключение VALUE_ERROR.
222 Глава 8 • Строки Рассмотрим несколько примеров. Вы узнаете, как использовать все четыре ар- аргумента функции INSTR, и увидите, какую пользу они могут принести. А когда бу- будете писать собственные программы, не забывайте о возможностях функции INSTR, позволяющих упростить программный код, выполняющий анализ символьных данных. С ее помощью можно сделать следующее. О Найти первое вхождение строки пожалуйста в строке Как вас зовут? Скажите. пожалуйста: INSTR ('Как вас зовут? Скажите, пожалуйста', 'пожалуйста') -» 25 Значения аргументов начальная_позиция и первое_вхождеиие по умолчанию равны 1. О Найти последнее вхождение строки а в строке Как вас зовут? Скажите, пожалуйста: INSTR ('Как вас зовут? Скажите, пожалуйста', 'а', -1) —> 34 Помните, что символьная позиция, возвращаемая функцией INSTR, всегда вы- вычисляется от крайнего слева символа строки (то есть символа с позицией 1). Для того чтобы найти последнее вхождение подстроки в строке, проще всего задать отрицательную начальную позицию. Номер вхождения при этом не за- задается, поскольку' последнее вхождение при поиске с конца является первым. О Найти предпоследнее вхождение строки а в строке Как вас зовут? Скажите, по- пожалуйста: INSTR ('Как вас зовут? Скажите, пожалуйста', 'а', -1, 2) -» 28 Тут все просто. Начиная поиск с конца строки, INSTR пропускает последнюю букву а в слове «пожалуйста», поскольку это последнее вхождение, и ищет следующее. При этом возвращаемая позиция, как всегда, считается слева, а не справа. О Найти ближайшую к вопросительному знаку позицию буквы а (до вопроси- вопросительного знака) в строке Как вас зовут? Скажите, пожалуйста: search_string .-- 'Как вас зовут? Скажите, пожалуйста'; teejoc :- INSTR (search_string , 'а', -1 * (LENGTH (search_string) - INSTR (search_string. '?') +1)): -> 6 В приведенном выше фрагменте кода мы динамически вычисляем позицию вопросительного знака (точнее, первого вопросительного знака; предполагает- предполагается, что он один). Затем вычитаем это значение из полной длины строки и ум- умножаем результат на -1, поскольку нам нужно определить расстояние от конца строки до вопросительного знака. С этой позиции функция и начинает поиск — от вопросительного знака к началу строки. Данный пример напоминает, что любой из аргументов функции INSTR может быть сложным выражением, вызывающим другие функции для выполнения некоторых вычислений. Этот же факт отражает и следующий пример. О Подтвердить допустимость введенного пользователем значения. В этом при- примере проверяется, имеется ли введенная пользователем команда в списке до- допустимых команд. Если да, она выполняется: IF INSTR ('|ADD|DELETE|CHANGE|VIEW|CALC|'.T I I cmd 11 ' |') > 0 THEN execute_co(imand (cmd);
Строковые функции 223 ELSE DBMS OUTPUT.PUTJ.INE GВы ввели неправильную команду. Произведите повторный ввод...'); END IF: В этом случае с помощью оператора конкатенации составляется строка, в ко- которой будет производиться поиск введенной команды. В конец введенной команды добавлена вертикальная черта, поскольку в списке команд она при- применяется в качестве разделителя. Здесь мы используем вызов функции INSTR в логическом операторе IF. Найдя соответствие, она возвращает ненулевое значение. Это означает, что результат проверки условия в операторе IF равен TRUE, и работа продолжается. В противном случае выводится сообщение об ошибке. Следующий пример, с применением набора символов базы данных UTF-8, по- показывает различие между функциями INSTR и INSTRB, которыми вы будете пользо- пользоваться чаще всего: DECLARE -- В качестве набора символов базы данных в этом примере используется Unicode UTF-8 х CHARC0 CHAR) :- 'Символ 3 занимает два байта.': BEGIN -- Определяем символьную позицию 'два' DBMS_OUTPUT.PUT_LINECINSTRCx. 'два')): -- Определяем байтовую позицию 'два' DBMS_OUTPUT.PUT_LINE(INSTRB(x. 'два')); END; Вот что получается в результате: 19 20 Разница в позиции слова «два» объясняется тем, что символ «а*> в Unicode UTF-8 занимает два байта. Поэтому слово «два» начинается с 19-го символа, но с 20-го байта. Функция INSTRC может распознавать составные символы, представленные дву- двумя символами. Как отмечалось в разделе, посвященном функции COMPOSE, альтер- альтернативным представлением символа «а» является «а\0303». Следующий пример показывает, что функция INSTRC распознает это альтернативное представление, тогда как функция INSTR - нет; DECLARE -- В качестве набора символов базы данных в этом примере используется Unicode UTF-B х CHARD0 CHAR) :- UNISTRC'Символ а\0303 составной.1); BEGIN -- INSTR не видит, что а\0303 - это то же самое, что 3 DBMS_OUTPUT.PUT_LINE(INSTRCx.'3')): -- А вот INSTRB понимает, что аШОЗ - 3 DBMS_0UTPUT.PUT_LINE(INSTRB(x.'3')): END; В результате получаем: О R
224 Глава 8 • Строки С точки зрения функции INSTR строка х вообще не содержит символа «а». Функция же INSTRB распознает «а\0303» как альтернативное представление этого символа. Использовавшаяся в объявлении переменной х функция UNISTR преоб- преобразует ASCII-строку, которую мы можем прочесть, в строку Unicode, требую- требующуюся в нашем примере. Функции INSTR2 и INSTR4 позволяют найти в строке кодовый блок или кодовый индекс, которые могут не соответствовать целому символу. В следующем приме- примере в качестве национального набора символов используется AL16UTF16. Символ «а» в строке х представлен как два кодовых индекса: один для символа «а», а вто- второй для тильды (\0303). Попытаемся найти кодовый индекс \0303 с помощью функции IN5TRC, а затем с помощью функции INSTR4: DECLARE -- Набор символов базы данных в этом примере Unicode UTF-16 х NCHARD0) := UNI5TRC'Символ а\0303 составной.1); BEGIN -- Ищен позицию \0303 с помощью INSTRC DBMS_OUTPUT.PUT_LINE(INSTRC(x. UNISTR('\0303'))): -- Ищен позицию \0303 с помощью INSTR4 DBMSOUTPUT. PUT_LINE( INSTR4(x. LMSTR('\0303'))): END; Вот что получается в результате: О 9 Функция INSTRC работает с целыми символами, и для поиска кодового индек- индекса, являющегося частью символа, она бесполезна. А вот функция INSTR4 может найти кодовый индекс \0303. Возвращаемое ею значение 9 указывает, что \0303 находится на 9-й позиции в строке. Функция INSTR2 действует подобным образом, но ищет кодовые блоки UCS-4. Взгляните на следующий пример: DECLARE х NCHARD0) ;= UNISTRC'Sto \D834\DD1E тест); BEGIN DBMSOUTPUT.PUTLINE CINSTR2(x. UNISTRC'\D834V))); DBMSJXJTPUT.PUTLINE UNSTR4(x. UNISTR('\D834V))); END; 5 0 Каждое значение в замещающей паре \D834\DD1E (скрипичный ключ) яв- является кодовым блоком, а два блока вместе составляют один кодовый индекс. Данный пример показывает, что функция INSTR2 находит кодовый блок в заме- замещающей паре, а функция INSTR4 — нет, поскольку первая воспринимает строку как последовательность кодовых блоков, а вторая — как последовательность ко- кодовых индексов. Поиск кодового блока в замещающей паре подобен поиску пер- первого байта во многобайтовом символе. Однако маловероятно, что вам когда-ни- когда-нибудь потребуется производить такой поиск.
Строковые функции 225 функция LEAST Принимает в качестве аргумента одну или несколько строк и возвращает строку, которая оказалась бы первой при сортировке заданных строк по алфавиту. Порядок сортировки основывается на наборе символов базы данных. Синтаксис функции LEAST таков: FUNCTION LEAST 1строкд_1 IN VARCHAR2. строкаJ IN VARCHAR2. ...) RETURN VARCHAR2 Приведем пример: BEGIN DBMS_OUTPUT.PUT_LINE(L?ASTCБерезовский1. 'Лебедко'. 'Пасыю'Э): END: Березовский Обратитесь также к описанию функции GREATEST, которая является обратной функции LEAST. Функции LENGTH, LENGTHB, LENGTHC, LENGTH2, LENGTH4 Семейство функций LENGTH возвращает длину строки. Приведем перечень этих функций с указанием единиц, в которых выражается длина строки: О LENGTH - символы; О LENGTHB - байты; О LENGTHC — символы Unicode, нормализованные, где это возможно; О LENGTH2 - кодовые блоки; О LENGTH4 — кодовые индексы. Синтаксис всех этих функций одинаков: FUNCTION LENGTH {строка_1 VARCHAR2) RETURN NUMBER Если значение аргумента строка_1 равняется NULL, то функция LENGTH возвраща- возвращает NULL — но не нуль! Помните, что строка нулевой длины — это «отсутствующее значение». Поэтому она не может иметь длину, даже нулевую, и всегда возвраща- возвращает либо NULL, либо положительное значение. ПРИМЕЧАНИЕ Одним исключением является использование функции LENGTH для определения длины перемен- переменной типа CLOB. Такая переменная может содержать нуль байтов и все же не быть равной NULL. Это единственный случай, когда LENGTH возвращает нуль. Приведем несколько примеров использования функции LENGTH: LENGTH (NULL) -* NULL LENGTH (") -> NULL -- To же самое, что строка NULL LENGTH Сабвг') -» 4 LENGTH С'абвг ') -* 5
226 Глава 8 • Строки Если строка_1 имеет фиксированную длину (тип данных CHAR), функция LENGTH подсчитывает и завершающие пробелы. Поэтому для переменной фиксирован- фиксированной длины она всегда возвращает одно и то же значение, независимо от того, ка- какое значение присвоено этой переменной. Если же необходимо подсчитать длину содержащейся в переменной строки без завершающих пробелов, просто удалите их с помощью функции RTRIM (о которой рассказывается далее в этой главе). В следующем примере переменной lengthl присваивается значение 60, а перемен- переменной Iength2 - значение 13: DECLARE company name CHAR<60) :- 'ACME PLUMBING1; lengthf NUMBER: 1 engthZ NUMBER: BEGIN lengthl :- LENGTH (comparvyjiame): Iength2 :- LENGTH (RTRIM (company name)): END: Рассмотрим еще один пример, в котором используется переменная типа NVAR- CHAR2 и набор символов AL16UTF16. Данный пример демонстрирует различие между функциями LENGTH, LENGTHB и LENGTHC. Запомните, что AL16UTF16 - это ис- использующееся в Oracle имя набора символов UTF-16, в котором каждый символ представлен двумя байтами: DECLARE -- В этом примере NVARCHAR2 - UTF-16. х NVARCHAR2E0) :- UNISTR('Символ 3 эквивалентен а\0303'); BEGIN DBMSJJUTPUT.PUT_LINE(LENGTH(x)); DBMS_OUTPUT.PUT_LINE(LENGTHB(x)): DBMS_OUTPUT.PUT_LINE(LENGTHC(x)): END; Вот результат выполнения этого кода: 25 50 24 Функция LENGTH подсчитывает символы, но «а\0303» воспринимается ею как' два разных двухбайтовых символа. Функция LENGTHB подсчитывает байты, и воз- возвращает значение, равное удвоенному количеству двухбайтовых символов, воз- возвращенному функцией LENGTH. И, наконец, LENGTHC подсчитывает символы Uni- Unicode и видит, что «а\0303» на самом деле представляет один символ «а*. Функция LOWER Переводит все буквы строки в нижний регистр. Синтаксис функции таков: FUNCTION LOWER (сгрокд_1 IN CHAR) RETURN CHAR FUNCTION LOWER (строка_1 IN VARCHARZ) RETURN VARCHAR2 Функция LOWER, как и UPPER, о которой будет рассказано ниже, возвращает стро- строку фиксированной длины, если входная строка имеет фиксированную длину,
Строковые функции 227 И строку переменной длины — в противном случае. При этом ни та, ни другая функция не меняет символов, которые не являются буквами, — например, цифр и специальных символов. Приведем несколько примеров использования функции LOWER: LOWER ('БОЛЬШИЕ БУКВЫ1) -> 'большие Вуквы' LOWER С123АБВ1) -> '123абв' Функции LOWER и UPPER удобно применять для согласования регистров симво- символов при сравнении строк. PL/SQL не чувствителен к регистру символов в отно- отношении собственного синтаксиса и идентификаторов, чего нельзя сказать о реги- регистре символьных строк, являющихся константами, литералами или значениями переменных. Строка АБВ - не то же самое, что абв, и поэтому в программах нужно внимательно обрабатывать такие значения. функция LPAD По умолчанию PL/SQL удаляет из символьной строки все завершающие пробе- пробелы, если только строка не объявлена как CHAR. Однако случается, что в начало или конец строки необходимо добавить несколько пробелов (или других символов). Тогда на помощь приходит функция LPAD (либо аналогичная ей функция RPAO), возвращающая строку, дополненную слева до заданной длины одним указанным символом (или несколькими символами). Вот ее синтаксис: FUNCTION LPAO {строка_1 IN VARCHAR2. конечная_цлина IN NUM8ER [, цополнящая_строка IN VARCHAR2]) RETURN VARCHAR2 Функция LPAD возвращает значение аргумента строка_1, дополненное слева до значения параметра конечная_длина символьной строкой дополнящая_строкд. При условии, что последний аргумент не задан, строка_1 дополняется слева пробела- пробелами. Аргумент конечная_длина является числом и должен быть задан обязательно. Если аргументы строка_1 и конечная_цгшна равны, функция LPAD возвращает значе- значение первого без изменений. Если же конечная длина меньше исходной длины строки, функция LPAD обрезает ее, то есть в исходной строке, заданной значением аргумента строка_1, возвращается количество символов, равное значению аргу- аргумента конечная_длина. Как видите, функция LPAD может не только добавить пробелы в левой части строки. Рассмотрим несколько примеров ее использования. О Вывод представляющей числовое значение строки, дополненной слева нуля- нулями до длины 10: LPAD C551. 10. '0') -> '0000000055' О Вывод представляющей числовое значение строки, дополненной слева нуля- нулями до длины 5: LPAD ('12345678'. 5. 'О') -» '12345' Функция LPAD интерпретирует значение аргумента конечная jnwa как макси- максимальную длину возвращаемой строки. Поэтому она отсчитывает слева (от на- начала строки) 5 символов и просто возвращает подстроку заданной длины.
228 Глава 8 • Строки О Повторение слова «ура!» перед словом «гол!П» до тех пор, пока длина строки не будет равна 44: LPAD ('гол!!!'. 44. 'ура!1) -» 'ура!ура!ура!ура!ура!ура!ура!ура!ура!ургол!!!' Поскольку длина строки гол!!! составляет 6 символов, а строки ура! — 4, для последнего полного вхождения дополняющей строки места не осталось. В ре- результате десятое повторение урезано на два символа. Функция LTRIM Действие функции LTRIM противоположно действию функции LPAD. Если послед- последняя добавляет символы с левой стороны строки, то первая их удаляет. И так же, как LPAD, функция LTRIM располагает большими возможностями, чем просто уда- удаление ведущих пробелов. Ее синтаксис таков: FUNCTION LTRIM {строка_1 IN VARCHAR2 [. удвляемые_синвопы IN VARCHAR2]) RETURN VARCHAR2 Функция LTRIM возвращает строку_1, из которой удалены все ведущие симво- символы, входящие в состав строки удаляемые_символы. Второй аргумент не обязателен и по умолчанию является символом пробела. Существует одно важное различие между функциями LTRIM и LPAD. Последняя добавляет слева заданную строку и повторяет ее до тех пор, пока длина результи- результирующей строки не достигнет заданного значения. Функция LTRIM удаляет все ве- ведущие символы, которые представляет строка уцаляемые_с1лнаолы. Приведем не- несколько примеров. О Удаление из фразы •« Книга — лучший подарок» всех ведущих пробелов: LTRIM (' Книга - лучший подарок') -э 'Книга - лучший подарок' Поскольку второй аргумент не задан, по умолчанию он равен символу пробе- пробела, и в результате удаляются все ведущие пробелы. О Удаление всех цифр в начале строки: my_string := '707457835абвгд'; LTRIM (my_string. '1234567890') -» 'абвгд' Задав во втором аргументе все возможные цифры, мы гарантируем, что они будут удалены из начала строки, независимо от порядка их следования в зна- значениях аргументов строка! и удаляемые_синволы. А что если мы захотим удалить из начала строки только заданную последова- последовательность, скажем, «аб»? Функция LTRIM для этого не подходит, поскольку она не сопоставляет последовательности символов. Для их удаления или замены ис- используется функция REPLACE, о которой рассказывается в следующем разделе. ПРИМЕЧАНИЕ- В ANSI-стандарте функции LTRIM соответствует функция TRIM.
Строковые функции 229 функция REPLACE Возвращает строку, в которой все вхождения заданной подстроки заменены ука- указанной замещающей строкой. Синтаксис функции таков: FUNCTION REPLACE (страка_1 IN VARCHAR2, искомая_строка IN VARCHAR2 [, занев1ащаи_арока IN VARCHAR2]) RETURN VARCHAR2 Если замещающая_строка не задана, функция REPLACE просто удаляет все вхожде- вхождения искомой_строки в строке_1. Если же не задана ни искомая_строка, ни замещаю- щая_строка, функция REPLACE возвращает NULL. Приведем несколько примеров использования функции REPLACE. О Удаление всех символов «С» в строке САБВ С СС QOQC: REPLACECCABB С СС OODCVC) -> 'АБВ 000' Поскольку мы не задали замещающую строку, функция REPLACE заменила все вхождения символа «О на NULL. О Замена всех вхождений числа 99 на 100 в следующей строке: REPLACEC'Число 99 равно числу 99!','99'.'100') -> 'Число 100 равно числу 100!' О Обработка вхождений одинарной кавычки в строке. В PL/SQL одинарная ка- кавычка является символом-ограничителем, указывающим начало и конец стро- строкового литерала. Допустим, пользователь вводит строку, содержащую одинар- одинарную кавычку: Русская неделя в ресторанах MacDonald's. Программа объединяет эту строку с другим текстом, и результирующая инст- инструкция SQL (динамически созданная Oracle Forms в режиме запроса) получа- получается ошибочной, поскольку в ней содержатся несбалансированные кавычки. Для разрешения данной проблемы нужно удвоить в строке каждую одинар- одинарную кавычку, указав тем самым SQL, что она является частью строки. С этой целью можно выполнить следующий оператор: criteriastring :- REPLACE Ссг1tena_string. —, """); Четыре кавычки во втором аргументе интерпретируются как строка, содержа- содержащая одну одинарную кавычку, а шесть кавычек в третьем аргументе — как строка, содержащая две одинарные кавычки. В каждом случае внешняя пара кавычек ограничивает строку, а каждая пара кавычек внутри нее интерпрети- интерпретируется как одна кавычка. О Удаление последовательностей символов «абв» в начале строки: абвабвааагдевкз - буквы алфавита: абв Нечто подобное делала описанная в предыдущем разделе функция LTRIM. Од- Однако мы хотим удалить последовательность символов «абв», а не просто сим- символы «а», «б» и «в» в любом порядке, и не в середине или конце обрабатывае- обрабатываемой строки, а только в начале. Например, не должны удаляться последова- последовательности «ааа». Эта задача более сложная, чем может показаться на первый взгляд. Если просто применить к строке функцию REPLACE, она удалит все по- последовательности «абв», вместо того чтобы удалить их только в начале строки: REPLACE С'абвабвааагдеёжз - буквы алфавита; абв', 'абв') -> 'ааагдеёжз - буквы алфавита:'
230 Глава 8 • Строки А это не то, что нам нужно. С другой стороны, если использовать функцию LTRIM, она удалит и ведущие символы «а»: ШЩ'абвабвааагде&м - буквы алфавита: абв'. 'эСв') -> 'гдеёжз - буквы алфавита: абв' Это тоже не то, что нам нужно. Требуется, чтобы осталась строка ааагдеёжз - буквы алфавита: абв. Решением проблемы является совместное использование функций LTRIM и REPLACE: my_string :- 'абвабаааагдеёжз - буквы алфавита: абв'; REPLACE (LTRIM (REPLACE (my_string. 'аба1. '&'), '?'), '@'. 'аба'); —> 'ааагдеёжэ - буквы алфавита; абв' Поясним этот фрагмент кода: сначала заменяем все вхождения строки абв в строковом значении локальной переменной my_string на специальный сим- символ @ (предполагается, что его в исходной строке нет), затем удаляем все веду- ведущие экземпляры символа @ и снова заменяем оставшиеся его экземпляры на абв, Функция RPAD Добавляет символы в конец строки. Она возвращает строку, дополненную справа до заданной длины указанной последовательностью символов. Ее синтаксис таков: FUNCTION RPAD (строка_1 IN VARCHAR2, конечнаяjnma IN NUMBER [, допопнящдя_строкд IN VARCHAR2]) RETURN VARCHAR2 Функция RPAD возвращает значение аргумента строка_1, дополненное справа до значения аргумента конечная_цлина символьной строкой дополнящая_строка. В том случае, когда последний аргумент не задан, строка_1 дополняется справа пробела- пробелами. Аргумент конечнаяjn^Ha должен быть задан обязательно. Если длина исход- исходной строки уже равна значению аргумента конечная_рлш, функция RPAD возвра- возвращает аргумент строка_1 без изменений. Если же конечная длина меньше исходной длины строки, функция RPAD обрезает эту строку, то есть возвращает в ней коли- количество символов, равное значению аргумента конечная jnma. Рассмотрим несколько примеров. О Вывод представляющей числовое значение строки, дополненной справа нуля- нулями до длины 10: RPAD C55'. 10, '0') -> '55QQ0QQQQQ' Можно также воспользоваться функцией TO_CHAR, преобразующей число в строку (следует помнить, что всегда имеется несколько способов решения задачи): TO_CHAR E5 * 100000000) -> '5500000Q001 О Вывод представляющей числовое значение строки, дополненной справа нуля- нулями до длины 5: RPAD Г123456781. 5. 'О') -> '12345' Функция RPAD интерпретирует значение аргумента конечная_длина как макси- максимальную длину возвращаемой строки. В результате она отсчитывает слева
Строковые функции 231 (от начала строки) S символов и просто возвращает подстроку заданной дли- длины. В этом случае она делает в точности то же самое, что и описанная ранее функция LPAD, а не возвращает 5 крайних символов справа (в данном случае 45678). О Повторение слова ¦«ура!» после «голШ» до тех пор, пока длина строки не ста- станет равной 44: RPAO ('гол!!!'. 44. 'ура!') —» 'гол!!!ура!ура i ура!ура!ура!>ра!ура!ура!ура!ур' Поскольку длина строки гол!!! равняется 6 символам, а длина строки ура! - 4, для последнего полного повторения дополняющей строки места не осталось. В результате десятое повторение урезано на два символа. С помощью функции RPAD (или LPAO) можно генерировать повторяющиеся по- последовательности символов. Например, можно создать строку из 60 дефисов, ис- используемую в качестве разделителя информации в отчете: RPAD С1-', 60. '-') Функции RPAD и LPAD вызываются также из SQL-инструкций. Функция RTRIM По своему действию противоположна функции RPAD и сходна с функцией LTRIM. Если RPAD добавляет символы с правой стороны строки, то RTRIM их удаляет. И, по- подобно функции LTRIM, функция RTRIM способна гораздо на большее, чем просто удалить завершающие пробелы. Ее синтаксис таков: FUNCTION RTRIM {строке_1 IN VARCHAR2 [. уяапяемые_символы IN VARCHAR2]) RETURN VARCHAR2 Функция RTRIM возвращает строку_1, из которой удалены все завершающие символы, входящие в строку удаляемые_символы. Второй аргумент не обязателен, и по умолчанию он является символом пробела. Приведем примеры. О Удаление из строки всех завершающих пробелов: LTRIM ('Бросай курить ') -> 'Бросай курить' Поскольку второй аргумент не задан, по умолчанию он является символом пробела, и в результате удаляются все завершающие пробелы. О Удаление последовательности слов «БАМ! БУМ! БАМ!» из строки Звуковые эффекты: БАМ! БУМ! AM!: n\y_str1ng :- 'Звуковые эффекты: БАМ! БУМ! AM!'; RTRIM (my_str1ng. ' БАМ! БУМ1J -» ' Звуковые эффекты:' В данном случае функция RTRIM удаляет из конца строки все буквы, имеющие- имеющиеся в строке БАМ! БУМ. И хотя, например, слова «AM» в строковом значении ар- аргумента удаляемые_снмволы нет, там есть все составляющие его буквы, и поэтому данное слово удаляется из исходной строки. Можно добавить, что двух вос- восклицательных знаков в значении аргумента упаляемые_синволы указывать не нужно, поскольку функция RTRIM ищет не слова «БАМ!» и «БУМ1», а только составляющие их символы.
232 Глава 8 • Строки ПРИМЕЧАНИЕ В ANSI-стандарте функции RTRIM соответствует функция TRIM. Функция SOUNDEX Обрабатывает строку с учетом ее фонетики (звучания), а не семантики (написа- (написанияI. Функция возвращает символьную строку, являющуюся «фонетическим представлением» аргумента. Ее спецификация такова: FUNCTION SOUNDEX (строка_1 IN VARCHAR2) RETURN VARCHAR2 Вот несколько значений, генерируемых функцией SOUNDEX: SOUNDEX ('smith') -> 'S530' SOUNDEX CSMYTHE') -> 'S530' При работе с функцией SOUNDEX следует придерживаться следующих правил. О Возвращаемое функцией значение всегда начинается с первой буквы исход- исходной строки. О Для формирования выходного значения SOUNDEX использует только первые пять согласных букв строки. О Для определения числовой части значения SOUNDEX применяет только соглас- согласные. Все гласные, за исключением начальной, игнорируются. О Функция SOUNDEX не чувствительна к регистру. Для одних и тех же букв в верх- верхнем и нижнем регистре возвращается одно и то же значение. Функция SOUNDEX используется для выполнения запросов, например динами- динамических, в которых точное значение, находящееся в базе данных, неизвестно или его трудно определить. ПРИМЕЧАНИЕ Функция SOUNDEX предназначена для работы с буквами английского языка, а с другими языками может работать плохо или даже не работать вовсе. Функции SUBSTR, SUBSTRB, SUBSTRC, SUBSTR2, SUBSTR4 Семейство функций SUBSTR - один из наиболее популярных наборов символьных функций. Эти функции позволяют извлечь из строки группу последовательных символов (подстроку). Подстрока задается значением начальной позиции и дли- длиной, а функции различаются только используемыми единицами измерения: О SUBSTR - начальная позиция и длина задаются в символах; О SUBSTRB — начальная позиция и длина задаются в байтах (при использовании однобайтового набора символов функции SUBSTRB и SUBSTR возвращают одина- одинаковые результаты); О SUBSTRC — начальная позиция и длина задаются в символах Unicode, после «сбор- «сборки» всех составных символов; 1 Для получения фонетического представления строки Oracle Corporation предлагает использовать алгоритм Дональда Нута, описанный в третьем томе книги Art о/ computer Programming.
Строковые функции 233 О SUBSTR2 — начальная позиция и длина задаются в кодовых блоках; О SU8STR4 — начальная позиция и длина задаются в кодовых индексах. Синтаксис этих пяти функций одинаков: FUNCTION SUBSTR (строкд_1 IN VARCHAR2, нача.~ьная_познция IN NUMBER [, длина_подстроки IN NUMBER]) RETURN VARCHAR2 Здесь строка_1 — исходная строка; начальная_позиция — начальная позиция под- подстроки в строке_Г, рлина_порстроки — длина возвращаемой подстроки. Последний аргумент не обязателен. Если вы не зададите его, функция SUBSTR вернет все символы строки_1 (от заданной начальной позиции). Если же аргумент цлина_подстроки задан, его значение должно быть больше нуля. Значение аргумента начальная_позиция не может быть равно нулю. Если оно меньше нуля, подстрока извлекается из конца строки; функция SUBSTR отсчитыва- отсчитывает количество символов, заданное значением аргумента начальная_позиция, от кон- конца строки_1. Однако и в этом случае извлекаемые символы располагаются справа от начальной позиции. На рис. 8.3 показано, как функцией SUBSTR используются разные значения аргументов. SUBSTRCstring.7.4) SUBSTR(string.-7.4) 4 г-* шовш ABSC-7) Рис. 8.3. Использование функцией SUBSTR разных значений аргументов Функция SUBSTR очень «снисходительна». Даже если нарушить правила зада- задания второго и третьего аргументов, ошибка не будет сгенерирована — функция просто вернет NULL или всю строку. Приведем несколько примеров использования функции SUBSTR. О Возвращение последнего символа строки: SUBSTR СКонпьитер1. -1) -> 'р' Это простейший способ вернуть последний символ. Можно, конечно, посту- поступить и так: SUBSTR ('Компьютер'. LENGTH ('Компьютер'), 1) -» 'р'
234 Глава 8 • Строки Иными словами, мы определяем длину строки и извлекаем из нее подстроку длиной в один символ, начиная с последней позиции. О Удаление элемента из списка строк. Кажется, что это задание противоречит назначению функции SUBSTR: требуется извлечь часть строки, оставив осталь- остальную ее часть нетронутой. Тем не менее SUBSTR с ним отлично справится. Пред- Предположим, на экран выводится список выбранных температурных режимов: |ЖАРКО|ХОЛОДНО|ТЕПЛО|ПРОХЛАДНО| Вертикальная черта разделяет элементы списка. Предположим, нужно уда- удалить один элемент списка, например ТЕПЛО. Для выполнения этой задачи проще всего определить начальную и конечную позиции удаляемого элемента, затем с помощью функции SUBSTR выделить оставшиеся части списка и соединить их вместе — без заданного элемента. DECLARE myjist VARCHAR2C50); tojelete VARCHAR2C20): start_pos NUMBER; end_pos NUMBER; BEGIN myjist :- '|ЖАРКО|ХОЛОДНО|ТЕПЛО|ПРОХЛАДНО|': tojelete :- 'ТЕПЛО1: start_pos :- INSTRdnyJist. tojtelete): -- первый удаляемый символ end_pos :- startjjDs + LENGTH(to_deleteJ: -- последний удаляемый символ myjist :- SUBSTR (myjist. 1. start_pos-l) SUBSTR (myjist. end_pos+l): DBMS_OUTPUT.PUT_LINE Cmyjist): END; Результат работы кода приведен ниже: |ЖАРКО|ХОЛОДНО(ПРОХЛАДНО| О Извлечение части строки, расположенной между заданными начальной и ко- конечной позициями. Такая задача решается очень часто. Функции SUBSTR нужно знать начальную позицию и количество извлекаемых символов. Однако часто бывает так, что известны лишь начальная и конечная позиции, и тогда прихо- приходится вычислять количество символов, находящихся между ними: разность двух значений, разность плюс единица или разность минус единица. Чтобы не было путаницы, приведем пример функции betwnstr. Она извлекает подстро- подстроку, вычисляя количество символов между начальной и конечной позициями, которое равно значению выражения конечная_позиция - начальная_позиция + Г. I* Файл a web: betwnstr.sf */ FUNCTION betwnstr ( stringjn IN VARCHAR2, startjn IN INTEGER. endjn IN INTEGER) RETURN VARCHAR2 IS BEGIN RETURN SUBSTR (stringjn. startjn. endjn - startjn + 1): END;
Строковые функции 235 Хотя эта функция не обеспечивает такой гибкости, как функция SUBSTR (на- (например, в отношении отрицательной начальной позиции), она может послу- послужить отправной точкой для инкапсуляции больших возможностей. Подобно семействам функций INSTR и LENGTH функция SUBSTR имеет разновид- разновидности, предназначенные для работы с многобайтовыми наборами символов и на- набором символов Unicode. В следующем блоке PL/SQL отражено различие между символьной и байтовой семантикой: DECLARE -- 8 этой примере NVARCHAR2 - UTF-16. х NVARCHAR2EQ CHAR) :- UNISTRC'Символ а\0303 - это аналог 3'); BEGIN DBMS_OUTPUT.PUT_LINE(SUBSTR(x. 17, 6)); DBMS_OUTPUT.PUT_LINE(SUBSTRB<x, 33, 12)); END: Вот что выводит данный код: аналог аналог Слово «аналог» содержит шесть символов, начиная с символьной позиции 17. Однако оно занимает 12 байт, начиная с байтовой позиции 33. Поскольку мы ис- используем набор символов UTF-16, каждый символ занимает два байта. Первые 16 символов занимают 32 байта. Поэтому 17-й символ начинается с 33-го байта и занимает байты 33 и 34. Функция TO_CHAR Преобразует символы национального набора в символы базы данных. Синтаксис функции таков: FUNCTION TO_CHAR <.синвопьные_аанные IN NVARCHAR2) RETURN VARCHAR2 Функция TO_CHAR может принимать значения следующих типов данных: NCHAR, NVARCHAR2, CLOB и NCLOB. Возвращаемое значение всегда имеет тип VARCHAR2. Приведем пример использования функции TOCHAR для преобразования строки национального набора символов в строку набора базы данных: DECLARE a VARCHAR2C30): Ь NVARCHAR2C30) :- 'Я хочу съесть торт1: BEGIN а :- TO_CHAR(b): END; О том, как преобразовать данные в обратном направлении, рассказывается да- далее в этой главе, в разделе «Функции NLS». Кроме того, вы можете обратиться . к описанию функции TRANSLATE.. .USING. ПРИМЕЧАНИЕ Функция TO_CHAR может использоваться для преобразования в символьный тип чисел, а также зна- значений даты и времени. О таком ее применении рассказывается в главах 9 и 10.
236 Глава 8 • Строки Функция TO_MULTI_BYTE Преобразует однобайтовые символьные данные в их многобайтовые эквиваленты. Некоторые многобайтовые наборы символов, и в частности UTF-8, поддержива- поддерживают более одного представления некоторых символов. В UTF-8 буквы (скажем, G) могут быть представлены одним или тремя байтами. Функция TO_MULTI_BYTE позво- позволяет преобразовывать данные в соответствии с обоими представлениями. Ее син- синтаксис таков: FUNCTION TOJIULTIJYTE [строка_1 IN VARCHAR2) RETURN VARCHAR2 Тип значения, возвращаемого функцией TO_MULTI_BYTE, определяется типом дан- данных входного значения и всегда с ним совпадает. В следующем примере функция TOMULTIBYTE используется для преобразова- преобразования буквы G в ее многобайтовое представление. Код предназначен для системы, в которой в качестве национального набора символов используется UTF-8: DECLARE g_one_byte NI/ARCHARZ A CHAR) :- 'S': g_three_bytes NVARCHAR2 tl CHAR); g_one_aga1n NVARCHARZU CHAR); dump_output VARCHAR2O0); BEGIN -- Преобразовать однобайтовый символ "G" в многобайтовое представление g_three_bytes :- TO_MULTI_BYTECg_one_byte): DBMSOUTPUT.PUT_LINE(LENGTH8(g_one_byte)); DBMS_OUTPUT.PUT_LINE(LENGTHB(g_three_bytes)); SELECT DUMP(g_three_bytes) INTO dump_output FROM dual: DBMS_OUTPUT.PUT_LINE(dump_output); -- Преобразовать это многобайтовое представление обратно в оинобайтовое g_one_again :- T0_SINGLE_BYTE(g_three_byte5): DBMS_OUTPUT.PUT_LINE(g_one_again || ' это снова ' || TO_CHAR(LENGTHB(g_one_again)) || ' байт. '): END; Вот результаты выполнения этого кода: 1 3 Тур-1 Len-3: 239.188.167 G это снова 1 байт. Как видите, функция TO_MULTI_BYTE в строке 8 вернула трехбайтовое представле- представление буквы G, соответствующее UTF-8. Далее мы вызвали функцию TO_SINGLE_BYTE, чтобы преобразовать это трехбайтовое представление обратно в однобайтовое. Функция TO_SINGLE_BYTE Преобразует многобайтовые символы в их однобайтовыс эквиваленты, выполняя действие, обратное действию функции TO_MULTI_BYTE. Синтаксис функции таков: FUNCTION TO_SINGLE_BYTE {строна_1 IN VARCHAR2) RETURN VARCHAR2
Строковые функции 237 Тип данных возвращаемого функцией TDMULTIBYTE значения всегда совпадает с типом данных ее входного значения. Пример использования этой функции при- приведен выше, в разделе, посвященном функции TO_MULTI_BYTE. функция TRANSLATE Является разновидностью функции REPLACE. Напомним, что последняя заменяет в строке все вхождения заданной подстроки другой подстрокой, то есть она обра- обрабатывает не отдельные символы, а их последовательности. В отличие от нее функ- функция TRANSLATE выполняет указанное действие посимвольно, заменяя я-й символ искомой последовательности m-м символом замещающей последовательности. Ее синтаксис таков: FUNCTION TRANSLATE 1строка_1 IN VARCHAR2. исконаи_послеоовател1>ность IN VARCHAR2. звмещающая_последователь ноеть IN VARCHAR2) RETURN VARCHAR2 Здесь строка_1 — это строка, символы которой должны быть заменены; искомая_ последовательность — символы, которые должны быть заменены (если они присут- присутствуют в строке_1); здмещащая_последовательность - символы, на которые произво- производится замена. В отличие от функции REPLACE, последний аргумент которой может быть опущен, для функции TRANSLATE необходимы все три аргумента. Однако лю- любой из них может быть равен NULL, и тогда TRANSLATE вернет NULL. Приведем несколько примеров использования функции TRANSLATE: TRANSLATE Сабвг1. аб'. '12') -> '12аг' TRANSLATE ('12345'. '15', 'хх'Э -> 'х234х' TRANSLATE ('какой чудесный день', 'аое'. '%%*') -* 'кШй ч*д*сный д*нь' TRANSLATE ('какой чудесный пень', 'аоеь'. 'X') 'кХкй чудсный пн': TRANSLATE ('какой чудесный я', 'a'. NULL) -> NULL Просмотрев эти примеры, вы могли бы сделать ряд заключений о правилах ра- работы с функцией TRANSLATE сомостоятельно, но мы, конечно, опишем их. О Если искомая строка содержит не найденный функцией символ, никакая за- замена символов не производится. О Если строка содержит символ, не найденный в искомой последовательности, тогда этот символ не заменяется. О Если в искомой последовательности больше символов, чем в замещающей, то- тогда символы, которые имеются в искомой последовательности и отсутствуют в замещающей, просто удаляются из исходной строки. В следующем примере буквы а, 6 и в заменяются на э, ю и я соответственно. А буква г удаляется из строки, так как в замещающей последовательности для нее нет символа замены. TRANSLATE ('абвгдежзл'. 'абвг'. 'эюя1) -» 'эюядежзл' В таких случаях для всех остальных символов искомой последовательности замещающим символом является NULL. Заменить символ на NULL — это то же самое, что просто удалить его из строки.
238 Глава 8 • Строки О Если значение любого из трех аргументов равняется NJLL, функция возвраща- возвращает NULL. Это соответствует сути данного значения: в результате операции над неизвестным значением получается неизвестное значение. Функция TRANSLATE используется и в тех случаях, когда нужно заменить в стро- строке некоторое подмножество символов независимо от порядка их расположения. Функция TRANSLATE...USING Поддерживается для совместимости со стандартом ANSI. Функция преобразует данные одного набора символов в данные другого. Ее синтаксис таков: TRANSLATE {текст USING (CHAR_CS | NCHAR.CS}) Здесь тенет — это текст, который вы хотите преобразовать. Им может быть сим- символьная переменная, выражение или литерально заданная текстовая строка. Ар- Аргумент NHAR_CS указывает, что входной текст должен быть преобразован в символы базы данных, а аргумент NCHAR_CS — что он должен быть преобразован в символы национального набора. Выходным типом данных является либо VARCHAR2, либо NVARCHAR2, в зависимо- зависимости от того, с какай целью выполняется преобразование - достичь соответствия набору символов базы данных или национальному набору. Как пользоваться этой функцией, демонстрирует такой простой пример; DECLARE a VARCHAR2C30) :- 'Алфавит1; Ь NUARCHAR2C30); BEGIN Ь :- TRANSLATED USING NCHAR_CS): END: В этом примере символы строки представлены символами набора базы дан- данных. С помощью функции TRANSLATE.. .USING они преобразуются в символы на- национального набора. ПРИМЕЧАНИЕ- В Orade91 можно просто присвоить значение VARCHAR2 переменной типа NVARCHAR2 и наоборот — и система неявно выполнит нужное преобразование. Если же вы хотите сделать это явно, можно, воспользовавшись функцией TO_CHAR или TO_NCHAR, преобразовать текст в набор символов базы данных или национальный набор символов соответственно. Oracle рекомендует пользоваться имен- именно указанными функциями, а не TRANSLATE...USING, поскольку они поддерживают более широкий набор входных типов данных. Функция TRIM Была введена в версии Огас1е8г для обеспечения более полной совместимости со стандартом ANSI SQL, сочетает в себе действия функций LTRIM и RTRIM. Функция TRIM немного отличается от остальных SQL-функций тем, что позволяет вместо аргументов использовать ключевые слова. Чего же еще ждать от функции, разра- разработанной комитетом? Функция TRIM так же не обычна, как TRANSLATE... USING. Приведем ее синтаксис: TRIM ([LEADING | TRAILING | BOTH] [увалаеиый_сшвоп] FROM строка)
Строковые функции 239 Здесь строкд — это строка, которую необходимо обрезать; удаляемый_символ — сим- символ, который вы хотите удалить из одного или обоих концов строки. По умолча- умолчанию удаляются пробелы. Аргумент LEADING | TRAILING | BOTH указывает место, где вы хотите удалить заданные символы: в начале строки, в конце или с обеих сто- сторон. По умолчанию строка обрезается с обеих сторон. Приведем несколько примеров использования функции TRIM. О Удаление из строки ведущих и завершающих пробелов: TRIMC Удаляем ведущие и завершавшие пробелы. '); -> 'Удалить ведущие и завершавшие пробелы ' О Удаление только ведущих пробелов: *:- ' Удаляем только ведущие пробелы. TRIMCLEADING FROM х); -> 'Удаляем только ведущие пробелы. О Удаление завершающей точки: х:- 'Удаляем точку.' у :- '.' TRIMCTRAILING у FROM х): -> 'Удаляем точку' Если функция TRIM совместима со ANSI-стандартом, стоит ли вообще исполь- использовать функции LTRIM и RTRIM? Стоит. С помощью функции TRIM можно удалить только один символ, а с помощью LTRIM и RTRIM — последовательность символов. Например: RTRIMC'Удаляем последовательность символов..:!..;:', '..:;!'); -> 'Удаляем последовательность символов' Один вызов RTRIM позволил удалить из строки все завершающие знаки препи- препинания. Функция TRIM такого делать не умеет. Можно, конечно, написать рекур- рекурсивную функцию PL/SQL, которая вызывала бы функцию TRIM для каждого воз- возможного знака препинания, но зачем? Функция UNISTR Выполняет операцию, противоположную той, что выполняет функция ASCIISTR, - преобразует строку в символы набора Unicode. Непечатные символы строки можно задавать с помощью синтаксиса \XXXX, где ХХХХ представляет значение кодового индекса Unicode. Синтаксис функции UNISTR таков: FUNCTION UNISTR (строюJ IN VARCHAK2) RETURN VARCHAR2 Вот пример ее использования: BEGIN DBMS_OUTPUT.PUT_LINE С UNISTR ('Символ \00E3 не является ASCII-символом.'} ): END; 'Символ 3 не является А5СП-символон.'
240 Глава 8 • Строки ПРИМЕЧАНИЕ Чтобы узнать значения кодовых индексов набора Unicode, можете обратиться по адресу: http://uni- code.org/cherts/ Функция UNISTR предоставляет доступ к полному набору символов Unicode, включая те, которые нельзя непосредственно вводить с клавиатуры. Функция UPPER Переводит все буквы заданной строки в верхний регистр. Как и LOWER, эта функ- функция перегружена, и для входной строки фиксированной длины она возвращает строку фиксированной длины, а для входной строки переменной длины - строку переменной длины. Вот ее синтаксис: FUNCTION UPPER (.строиа_1 IN CHAR) RETURN CHAR FUNCTION UPPER {строка_1 IN VARCHAR2) RETURN VARCHAR2 Функция UPPER не меняет символов, которые не являются буквенными, напри- например цифр и специальных символов, таких как знак доллара. Приведем несколько примеров использования функции UPPER: UPPERt'Москвич 412') -> 'МОСКВИЧ 412' UPPERCземля') -» 'ЗЕМЛЯ' Функции LOWER и UPPER можно применять для согласования регистров при срав- сравнении строк. Функции NLS Oracle поддерживает ряд строковых функций, предназначенных для работы с на- набором символов национального языка (табл. 8.4). Эти функции используются в тех случаях, когда возвращаемый результат зависит от конкретного набора сим- символов. Например, если в качестве набора символов базы данных используется ASCII, а в качестве национального набора символов — EBCDIC, с кодом 65 в этих наборах связаны разные символы. Для преобразования кода 65 в символ на- набора базы данных применяется функция CHR, а для преобразования этого же кода в символ национального набора — NLS -функция NCHR. Таблица 8.4. Функции для поддержки символов национального языка Имя Описание NCHR Возвращает символ национального набора, соответствующий заданному коду NLSJNrTCAP Переводил" первую букву каждого слова в верхний регистр, а все остальные буквы — в нижний в соответствии с правилами национального языка NLSJ.OWER Переводит все буквы строки в нижний регистр в соответствии с правилами национального языка NLSJJPPER Переводит все буквы строки в верхний регистр в соответствии с правилами национального языка NLSSORT Возвращает строку байтов, которая может использоваться для сортировки строки в соответствии с правилами национального языка TO_NCHAR Преобразует символы набора базы данных в символы национального набора
Строковые функции 241 Функция NCHR Похожа на функцию CHR, но возвращает символ национального набора. Ее син- синтаксис таков: FUNCTION NCHR (.коя IN NUMBER) RETURN NVARCHAR2 Значение, возвращаемое функцией NCHR, присваивается переменной типа NVAR- CHAR2 или NCHAR2. Если присвоить его переменной другого типа, скорее всего, бу- будет выполнено нежелательное преобразование. Приведем пример использования функции NCHR: DECLARE х NVARCHAR2O0): BEGIN х :- NCHRC65): В функции NCHR можно применить предложение USING NCHAR_CS, служащее той же цели, что и в функция CHR. Функция NLS_INITCAP Изменяет регистр символов строкового аргумента, переводя первую букву каж- каждого слова в верхний регистр, а оставшиеся буквы - в нижний, другими словами, делает то же, что и функция INITCAP. Кроме того, данная функция позволяет про- производить сортировку в соответствии с правилами определенного языка. Ее син- синтаксис таков: NLS_INITCAP (строка_1 [, 'ШЗ^ОКТ-имя^ослевовательности^сортироаки''}) Приведенный ниже пример показывает различие между функциями INITCAP и NLSJNITCAP: BEGIN DBMS_OUTPUT.PUT_LINE(INITCAPC'ijzer')); DBMS_OUTPUT.PUT_LINECNLS_INITCAPCijzer'. 'NLS SORT=XDUTCH'}); END: .Вот что выводит этот код: Ijzer Uzer В голландском (Dutch) языке пара символов «ij» считается одним символом. Благодаря спецификации NLS_SORT функция NLSJNITCAP правильно распознает ука- указанные символы и преобразует голландское слово «ijzer» (означающее «железо») в это же слово, написанное с большой буквы. Функция NLS_INITCAP возвращает значение типа VARCHAR2. Ее входное значение может содержать символы любого набора. Функция NLS.LOWER Переводит строку в нижний регистр в соответствии с правилами конкретного языка. Синтаксис функции таков: NLSJ.OWER (стропа! [. 'ШЛ_$Ш-°ит»_1ЮСпеяояатепъшхт»_сорт»ро»К111"\) Спецификация NLSSORT была описана выше, в разделе, посвященном функции NLS INITCAP.
242 Глава 8 • Строки Функция NLS_UPPER Переводит строку в верхний регистр в соответствии с правилами конкретного языка. Синтаксис функции и принцип ее использования такие же, как у NLS_LOWER. Функция NLSSORT Возвращает строку байтов, которая может использоваться для сортировки стро- строкового значения в соответствии с правилами конкретного языка. Возвращаемая строка имеет тип данных RAW. Синтаксис функции NLSSORT таков: NLSSORT (строка_1 [, 'Я?_Ъ<№-т_поспе№ательиости_сортиров1Ш"[) Приведем пример использования функции NLSSORT для сравнения двух строк в соответствии с правилами конкретного языка: IF NLSSORTtx, 1NLS_SORT-XFRENCH") > (у. 'NLSJORT-XFRENCH') THEN... В этом случае оператор IF применяется для проверки того, действительно ли строка х больше строки у. При проведении простого сравнения, х > у, результат зависит от того, будет ли двоичное представление х больше двоичного представ- представления у. В некоторых языках значения двоичных представлений символов не со- соответствуют тем, которые используются при сортировке. Строки RAW, возвращае- возвращаемые функцией NLSSORT, позволяют сравнить два значения на основе порядка сор- сортировки символов языка. Функция TOJ4CHAR Перегруженная функция, которая может использоваться для преобразования чи- чисел, значений даты и символьных строк набора базы данных в символьные стро- строки национального набора. С числами и датой функция TO_NCHAR работает так же, как функция TO_CHAR, описанная в главе 9. Единственным отличием является то, что последняя возвращает значение, имеющее тип данных NVARCHAR2, а не VARCHAR2. Описываемая в этом разделе версия функции TO_NCHAR преобразует данные из набора символов базы данных в символы национального набора. Назначение функции TO_NCHAR противоположно назначению функции TO_CHAR, описанной выше, в разделе, посвященном символьным функциям. Ее синтаксис таков: FUNCTION TOJCHAR (симаольние_цаниые IN VARCHAR2) RETURN NVARCHARZ Функция TONCHAR может принимать на входе значения следующих типов дан- данных: CHAR, VARCHAR2, CLOB и NCLOB. Возвращаемое значение всегда имеет тип NVARCHAR2. Приведем пример использования функции TO_NCHAR для преобразования стро- строки символов базы данных в строку символов национального набора: DECLARE a VARCHAR2O0) .- 'Алфавит': b NVARCHAR2C3Q): BEGIN b :- TOJCHAR(a): END; Если вы планируете применять функцию TO_NCHAR, вернитесь к описанию функ- функции TRANSLATE... USING в этой главе. Кроме того, имейте в виду, что TO_NCHAR может ис- использоваться так же, как функция TO_CHAR, для представления значений даты, време- времени и чисел в читабельной форме. Об этом рассказывается в главах 9 и 10.
9 Числа Числовые типы данных Числовые преобразования Числовые функции И что бы мы делали без чисел? Хотя люди, которые не любят математику, пред- предпочитают рассматривать любую информацию как текст, реальность такова, что большая часть информации в базах данных представляет собой числа. Каковы за- запасы на складе? Сколько мы должны? Насколько вырос бизнес? Наиболее точ- точные ответы на эги и многие другие вопросы можно получить именно в числовом выражении. Для работы с числами в PL/SQL нужно изучить: О числовые типы данных, имеющиеся в вашем распоряжении; О преобразование числовых значений в текст и обратно; О богатую библиотеку встроенных числовых функций PL/SQL. Эти темы рассматриваются в настоящей главе. Начнем с типов данных. Числовые типы данных PL/SQL, как и РСУБД Oracle, предлагает множество числовых типов данных, предназначенных для решения конкретных задач. В их основе лежат три типа - NUMBER, PLSJNTEGER, BINARYJNTEGER. В программах встречаются и другие числовые типы данных, например FLOAT и DECIMAL, но по сути они представляют собой альтернативные имена перечислен- перечисленных базовых типов данных.
244 Глава 9 • Числа Числовые типы данных Oracle реализованы таким образом, что они идентич- идентично функционируют на всех поддерживаемых Oracle платформах, благодаря чему повышается переносимость баз данных и приложений. Например, при делении чисел типа NUMBER округление выполняется одинаково точно на аппаратном обес- обеспечении Intel и Sun. Такая переносимость достигается за счет того, что вместо ап- паратно-ориентированных математических функций в Oracle используются соб- собственные математические подпрограммы. Исключением является тип данных PLS_I NTEGER, скорость обработки значений которого повышена в ущерб независи- независимости от аппаратной платформы. Тип данных NUMBER Это самый распространенный тип данных, с которым вам придется встречаться в Oracle и PL/SQL, и единственный числовой тип, непосредственно поддержи- поддерживаемый ядром базы данных Oracle. Его значимость по сравнению с другими чи- числовыми типами данных весьма велика, поскольку для работы с числами можно использовать только его, не прибегая к помощи остальных типов. Тип данных NUMBER поддерживает следующий диапазон абсолютных значений: от 1 х 1СГ127 до 9,9999999999999999999999999999999999999 х 10121 C8 девяток) ПРИМЕЧАНИЕ- Таким был диапазон допустимых значений при тестировании версии OradeSi Release 1. В Orade9i SQL Reference он определен следующим образом: от 1,0 х 103° до 9,99...9 х 10125 C8 девяток). Простейший способ объявить переменную типа NUMBER — задать ключевое сло- слово NUMBER: DECLARE х NUMBER: В результате вы получите переменную, значением которой является число с пла- плавающей запятой. Память, выделяемая Oracle для переменной, позволяет хранить 38 значащих цифр, а плавающая десятичная запятая необходима для более точ- точного представления присваиваемых переменной значений. При объявлении переменной типа NUMBER можно задать для ее значения коли- количество значащих цифр и количество цифр после запятой: NUMBER {А, В) Такое объявление определяет число с плавающей запятой, где А — общее ко- количество значащих цифр в числе. С этим параметром непосредственно связан объем памяти, выделяемый Oracle для значения переменной типа NUMBER. Пара- Параметр 8 определяет количество цифр справа или слева после десятичной запятой и влияет на результат округления числа. Оба параметра должны быть литеральны- литеральными целочисленными значениями; ни переменные, ни константы в объявлениях использовать нельзя. Допустимое значение параметра/! находится в диапазоне от 1 до 38, а параметра В - от -84 до 127. При объявлении чисел с фиксированной запятой параметр й обычно меньше А. Например, переменную для хранения де- денежных сумм можно объявить как NUMBER0.2). Способ интерпретации этого объяв- объявления показан на рис. 9.1.
Числовые типы данных 245 Л общее количество значащих цифр в числа равно 9 | _... NUMBERC9.2) 9999999,99 «._ J | L_ © Отсчитав влево две позиции, -' \ '- Ц# Количество цифр после запятой ставии десятичную запятую \ задано положительным значением, поэтому отсчитывать следует влево с этой позиции Рис 9.1. Типичное объявление числа с фиксированной запятой В результате объявления переменной как NUMBERC9.2) ее значением является число с фиксированной запятой, состоящее из семи цифр слева от десятичной запя- запятой и двух справа, как показано на рис. 9.1. Такая переменная может хранить значе- значения до десяти миллионов. Они будут округляться максимум до сотых (табл. 9.1). Таблица 9.1. Округление значений переменной, объявленной как NUMBER(9,2) Исходное значение Округленное значение, хранящееся в переменной 1 234,56 1 234,56 1 234 567,984623 1 234 567,98 1 234 567,985623 1 234 567,99 1 234 567,995623 1 234 568,00 10 000 000,00 Значение слишком большое для данной переменной. При попытке присвоить его переменной генерируется ошибка переполнения -10 000 000,00 Число слишком маленькое для данной переменной. При попытке присвоить его переменной генерируется ошибка потери значимости (отрицательного переполнения) Попытки присвоить переменной последние два из указанных в таблице значе- значений вызывают ошибки переполнения и потери значимости, поскольку для пред- представления этих значений требуется больше цифр, чем помещается в переменной. Для хранения значений свыше десяти миллионов нужно минимум восемь знача- значащих цифр слева от десятичной запятой. При округлении чисел до семи цифр бу- будут генерироваться ошибки. Результат объявления переменной с количеством цифр после десятичной за- запятой, большим, чем общее количество значащих цифр, приведен на рис. 9.2. Если количество цифр после десятичной запятой задано отрицательным значением, то результат будет другим (рис. 9.3). Переменная, показанная на рис. 9.2, содержит то же количество значащих цифр1 что и переменная на рис. 9.1, но используются они по-другому. Поскольку пара- параметр В равен 11, девять значащих цифр могут представлять только абсолютные значения меньше 0,01, которые округляются до стомиллиардных. Результаты присваивания нескольких значений переменной типа NUMBERO.il) приведены в табл. 9.2.
246 Глава 9 • Числа NUMBERC9.il) Общее количество значащих цифр равно 9 .00999999999 i t П t Н Н П Десятичная запятая ] I i I j ! j I I i i оказывает» на дав Л ;. 1 ; i i H i i i " ? f Г ? ? Г ? Wa4BU(tlX цифр Количество цифр после десятичной запятой задано положительным значением A1), поэтому отсчитывать следует © Отсчитываем влево 11 позиций с этой позиции влево и добавляем нули в те позиции, которые находятся после значащих цифр Рис. 9.2. Количество цифр после десятичной запятой больше общего количества значащих цифр NUMBERC9.-11) Q Общее количество значащих цифр равно 9 99999999900000000000, 1 s 3 i S в 7 в в 10 it О В результате получается Q Снова отсчитываем от крайней слева цифры, а данном случае i * j * а ° , о о -ш п - кппкшпЙ Ш1ат колтествоцифрпоследесятшной !! ! !|! !!!!| «—oeToS запятой задано отрицательнь» ' ' ' ' JSS значением, поэтому считаем вправо ">~v« © Отсчитываем П позиции и добавляем нули Рис. 9.3. Количество цифр после десятичной запятой задано отрицательным значением Таблица 9.2. Округление значений переменной, объявленной как NUMBER(9,11) Исходное значение Округленное значение, хранящееся в переменной 0,001 234 567 89 0,001 234 567 89 0,000 000 000 005 0,000 000 000 01 0,000 000 000 004 0,000 000 000 00 0,01 Значение слишком большое для данной переменной; для его записи в переменную в позиции сотых требуется значащая цифра. При попытке присвоить его переменной генерируется ошибка переполнения -0,01 Число слишком маленькое для данной переменной; для его записи в переменную в позиции сотых требуется значащая цифра. При попытке присвоить его переменной генерируется ошибка потери значимости (отрицательного переполнения)
Числовые типы данных 247 Если количество цифр после десятичной запятой задано отрицательным зна- значением, то десятичная запятая переносится вправо. Переменная, объявленная как NUMBERC9.-1D, показана на рис. 9.3. Мы задали девять значащих цифр, но, как видно из табл. 9.3, теперь вместо ма- малых значений вплоть до стомиллиардных наименьшим значением, которое может содержаться в переменной, стало 100 миллиардов. Отрицательное значение параметра, который определяет количество цифр по- после десятичной запятой, позволяет представлять очень большие значения, как показывают рис. 9.3 и табл. 9.3. Это осуществляется только за счет потери дан- данных, находящихся в младших разрядах. При записи в переменную, объявленную как NUMBERO. -11), любое абсолютное значение меньше 50 триллионов округляет- округляется до нуля. Таблица 9.3. Округление значений переменной, объявленной как NUMBER(9,-11) Исходное значение Округленное значение, хранящееся в переменной 50 000 000 000,123 100 000 000 000 49 999 999 999,999 0 150 000 975 230,001 150 000 000 000 000 100 000 000 000 000 000 000 или 1х1020 Число слишком большое для данной переменной; для его записи в переменную в позиции сотен квинтиллионов требуется значащая цифра. При попытке присвоить его переменной генерируется ошибка переполнения -100 000 000 000 000 000 000 или -lxlO20 Число слишком маленькое для данной переменной; для его записи в переменную в позиции сотен квинтиллионов требуется значащая цифра. При попытке присвоить его переменной генерируется ошибка потери значимости (отрицательного переполнения) Учтите, что при объявлении переменных типа NUMBER количество цифр после десятичной запятой не является обязательным параметром и по умолчанию рав- равняется нулю. Например, следующие два объявления эквивалентны: х NUMBER C9.Q): х NUMBER (9): В результате любого из них создается целочисленная переменная, в которой могут храниться числа от -999 999 999 до 999 999 999. При таком широком диапазоне допустимых значений и разнообразных воз- возможностях не удивительно, что тип данных NUMBER является столь распространен- распространенным. Используя в объявлениях только ключевое слово NUMBER, можно создать пе- переменные с плавающей запятой, а задавая рассмотренные параметры — перемен- переменные с фиксированной запятой. Если количество цифр после десятичной запятой указать равным нулю или не задать вовсе, получится целочисленная переменная. Таким образом, один тип данных NUMBER покрывает все возможные варианты чи- числовых значений.
248 Глава 9 • Числа Тип данных PLS.INTEGER Поддерживается версией PL/SQL Release 2.3 и выше. Позволяет хранить целые числа в диапазоне от -2 147 483 647 до 2 147 483 647. Эти значения хранятся в «родном» целочисленном формате аппаратной платформы. Ниже приведено несколько примеров объявлений переменных типа PLS_INTEGER: DECLARE loopjaianter PLSJNTEGER: days_in_standard_year CONSTANT PLSJNTEGER :- 365: emp_vacation_days PLSJNTEGER DEFAULT 14; Тип данных PLSJNTEGER был разработан для увеличения скорости вычисле- вычислений. При выполнении арифметических операций над значениями данного типа программное обеспечение Oracle использует встроенную машинную арифмети- арифметику, тогда как с другими числовыми типами Oracle работает посредством аппарт- но-независимой библиотеки математических функций. В результате операции со значениями типа PLSJNTEGER выполняются быстрее операций со значениями NUM- NUMBER. А поскольку значения PLSJNTEGER целочисленные, проблем совместимости при переходе с одной платформы на другую из-за них практически не бывает. Для достижения большей эффективности вычислений Oracle рекомендует все- всегда использовать тип PLSJNTEGER, если только значения не выходят за границы диапазона. Учтите, что в выражениях, где приходится выполнять частые преоб- преобразования в тип данных NUMBER и наоборот, лучше использовать только тип NUMBER. Тип данных PLSJNTEGER наиболее эффективен в целочисленной арифметике и для счетчиков циклов, где он используется безо всяких преобразований. Тип данных BINARY.INTEGER Позволяет хранить целые числа со знаком в двоичном формате. В отличие от типа данных PLSJNTEGER не использует встроенную машинную арифметику; для операций со значениями типа BINARY INTEGER применяет независимые от платформы библиотечные функции Oracle. В PL/SQL BINARYJNTEGER был первым двоичным целочисленным типом данных. Однако в настоящее время Oracle рекомендует использовать вместо него PLSJNTEGER. Поэтому BINARYJNTEGER следует применять только для достижения совместимости с программным кодом, написанным до выхода версии PL/SQL Release 2.3. Значения типа BINARYJNTEGER расположены в диапазоне от -2 147 483 647 до 2 147 483 647. Использование этого типа данных вместо NUMBER обеспечивает уско- ускорение вычислений при большом объеме операций с целочисленными значениями. Но в большинстве случаев эффект этого ускорения слишком незначителен. Числовые подтипы Oracle поддерживает ряд числовых подтипов данных. Большая их часть пред- представляет собой альтернативные имена для трех описанных нами базовых типов данных. Подтипы введены для достижения совместимости с типами данных ANSI SQL, SQL/DS и DB2 и обычно имеют те же диапазоны допустимых значений, что и их базовые типы. Однако иногда значения подтипа ограничены некоторым
Числовые типы данных 249 подмножеством значений базового типа. Подтипы числовых данных представле- представлены в табл. 9.4. Таблица 9.4. Предопределенные числовые подтипы данных Подтип DEC (А, В)' DECIMAL (А, В)' DOUBLE PRECISION FLOAT FLOAT (A_ двоичной_точности)* INT INTEGER NATURAL Совместимость ANSI IBM ANSI ANSI, IBM ANSI, IBM ANSI, NUMBER ANSI, IBM Соответствующий тип данных Oracle NUMBER (A, B)' NUMBER (A, A)' NUMBER NUMBER NUMBER, но NUMBER не может быть объявлен с параметром А двоичной точности' NUMBER BINARY INTEGER NATURALN NUMERIC (A, B)' POSITIVE POSITTVEN REAL SIGNTYPE SMALUNT ANSI ANSI ANSI, IBM To же, что и NATURAL, но с дополнительным ограничением — не допускаются значения NULL NUMBER (А, В)' Аналогичен BINARYJNTEGER, но допускаются только положительные значения A и больше) , То же, что POSITIVE, но с дополнительным ограничением — не допускаются значения NULL NUMBER BINARYJNTEGER NUMBERC8) Параметр А — общее количество значащих цифр, параметр В — количество цифр после десятичной запятой. Типы данных NUMERIC, DECIMAL и DEC позволяют объявлять только значения, представляющие числа с фиксированной запятой. Типы DOUBLE PRECISION и REAL эквивалентны типу NUMBER. С помощью типа данных FLOAT можно объявлять числа с плавающей запятой с двоичной точностью в диапазоне от 63 до 126 бит. Воз- Возможность определять размер значений в битах, а не цифрах обычно применяют редко, и, скорее всего, вам не придется использовать типы данных ANSI/IBM. Очень удобно использовать подтипы данных B1NARY_INTEGER, к которым отно- относятся NATURAL и POSITIVE. Они ограничивают значения, которые могут храниться в переменной, и их применение делает программу более «самодокументирован- «самодокументированной». Например, в том случае, когда значения переменной не являются отрица- отрицательными, ее можно объявить как NATURAL (числа больше 0) или POSITIVE (числа больше 1). Но заметьте, что тип данных PLSJNTEGER более эффективен, чем BI- BINARYJNTEGER и его подтипы.
250 Глава 9 • Числа Числовые преобразования Компьютеры лучше работают с числами в двоичном представлении, тогда как люди предпочитают видеть числовые данные в виде строк, состоящих из цифр, запятых и пробелов. PL/SQL позволяет преобразовывать числа в строки и наобо- наоборот. Обычно для выполнения таких преобразований применяются функции Т0_ CHAR и TONUMBER. Для того чтобы использовать все возможности этих функций, нужно знать, что такое модели форматирования чисел и как они используются. Модели форматирования чисел Числовые форматы применяются при работе с функциями TO_CHAR и TO_NUMBER. Задавая числовой формат в вызове функции TOCHAR, вы определяете, как преоб- преобразовать числовое значение в строку VARCHAR2. Таким образом можно задать знаки препинания, местоположение знака «+» или «-» и другие полезные элементы. И наоборот, задавая числовой формат в вызове TO_NUMBER, вы определяете, как следует интерпретировать строку при преобразовании ее в числовое значение. Модель форматирования числа (маска форматирования) — это строка, состоя- состоящая из одного или более элементов форматирования, перечисленных в табл. 9.5. Строка, полученная в результате преобразования числового значения, будет со- соответствовать сочетанию указанных элементов. Примеры использования разных форматов приведены в описаниях функций TO_CHAR и TOJWMBER. Элементы форматирования, описание которых начинается словом «Префикс:», могут применяться только в начале маски форматирования. Если описание начи- начинается словом «Суффикс:», такой элемент может использоваться только в конце маски. Для большинства элементов указано их влияние на преобразования числа в строковое представление. Однахо учтите, что большая их часть может исполь- использоваться и в обратной операции, то есть для определения способа преобразования строки в число. Таблица 9.5. Элементы модели форматирования Элемент Описание форматирования 9 Представляет возвращаемую значащую цифру. Нулевые старшие разряды числа выводятся как пробелы (ведущие и завершающие) О Представляет возвращаемую значащую цифру, Нулевые старшие разряды числа выводятся как нули $ Префикс; выводит перед числом знак доллара В Префикс; возвращает нулевое значение как пробел, даже если используется элемент форматирования О Ml Суффикс: помещает после отрицательного числа знак «-». Если число положительно, после него добавляется пробел S Префикс: помещает перед положительным числом знак «+», а перед отрицательным знак «-»
Числовые преобразования 251 Элемент форматирования Описание PR , (запятая) .(точка) ЕЕЕЕ RN или гп FM ТМ и X Суффикс: заключает отрицательное значение в угловые скобки (< и >). Положительные значения дополняются одним пробелом в начале и конце Определяет для возвращаемого значения местоположение десятичной запятой. Все элементы, которые находятся слева от D, задают формат целочисленной части значения. Символ, используемый в качестве десятичной запятой, определяется параметром базы данных NLS_NUMERIC_CHARACTERS Определяет для возвращаемого значения местоположение разделителя групп разрядов, например запятой, отделяющей разряд тысяч, как в числе 6,754 (число 6754, согласно форме записи, принятой в США). Символ, используемый в качестве разделителя групп разрядов, определяется параметром базы данных NLS_NUMERIC_CHARACTERS Указывает для возвращаемого значения местоположение символа обозначения валюты ISO, который определяется параметром базы данных NLSJSO.CURRENSY Указывает для возвращаемого значения местоположение текущего символа валюты, который определяется параметром базы данных NLS_CURRENSY Указывает, что в этой позиции возвращаемого значения должна стоять запятая, которая применяется в качестве разделителя групп разрядов (см. элемент форматирования G) Определяет, что в данной позиции в возвращаемом значении должна стоять точка. Эта точка, согласно форме записи числа, принятой в США, используется в качестве десятичной запятой (см. элемент форматирования D) Умножает число, расположенное в маске форматирования слева от символа V, на 10 в степени п, где п — число, обозначаемое в маске форматирования девятками после символа V Суффикс; указывает, что значение должно быть возвращено в научном формате (в экспоненциальном представлении) Определяет, что возвращаемое значение должно быть выведено римскими цифрами верхнего или нижнего регистра. Так можно выводить числа от 1 до 3999. Значение должно быть целочисленным. RN возвращает число, записанное римскими цифрами в верхнем регистре, a m — в нижнем Префикс; удаляет пробелы возвращаемого значения из старших или младших разрядов числа Префикс; возвращает число, используя минимальное количество символов. (ТМ означает «text minimum».) Для десятичного представления после ТМ ставится цифра 9 (по умолчанию), для экспоненциального — буква Е Помещает в заданную позицию символ евро, который определяется параметром базы данных NLS_DUAL_CURRENCY Возвращает число в виде шестнадцатеричного значения. Этот элемент можно предварить нулями, чтобы вернуть нулевые старшие разряды числа, или элементом FM, чтобы удалить ведущие и завершающие пробелы, Элемент X не может использоваться в сочетании с другими элементами форматирования
252 Глава 9 • Числа Обратите внимание на то, что в некоторых случаях одинаковое форматирова- форматирование можно задать с помощью двух разных элементов. Например, знак доллара, запятая и точка по своему действию аналогичны буквенным элементам L, G и 0. Однако буквенные элементы учитывают текущие установки NLS, и вы получите символы, соответствующие используемому языку. Например, в одних языках для разделения дробной части числа применяется точка, а в других — запятая. Знак доллара, запятая и точка ориентированы на стандарты США и всегда задают имен- именно эти символы. Мы рекомендуем использовать NLS-ориентированные элементы •форматирования — L, G и D. ОБОЗНАЧЕНИЕ ДЕНЕЖНЫХ ЕДИНИЦ Элементы форматирования $, 1_, С и U, которые можно использовать для обозначения денежных единиц, показаны в табл. 9.5. Их различие состоит в следующем: ? элемент $ ориентирован на национальную валюту США и всегда воз- возвращает знак доллара; Q элемент L учитывает текущую установку параметра NLS_CURRENSY, опре- определяющего соответствующий символ валюты. Если значением пара- параметра NLSJTRRITORYявляется United Kingdom, параметру NLS_CURRENSY бу- будет соответствовать символ «?>, и элемент форматирования L задает использование в качестве символа валюты «?»; ? элемент форматирования С подобен элементу L, но применяет ISO-обо- ISO-обозначение валюты, указанное в параметре NLSISOCURRENSY. Например, для США - это USD и т. д.; Q элемент форматирования U добавлен для поддержки евро. Он исполь- использует значение параметра NLS_DUAL_CURRENCY. Для стран, валютой кото- которых является евро, в данном параметре в качестве символа валюты за- задан евро (по умолчанию (€)). Данные о текущих установках параметров NLS_CURRENSY и NLS_ISO_CURRENSY находятся в системном представлении NLS_SESSION_PARAMETERS. Функция TO_NUMBER Преобразует строки фиксированной и переменной длины в числа. Используйте эту функцию, когда нужно преобразовать строку, представляющую число, в соот- соответствующее числовое значение. Синтаксис ее вызова следующий: T0_NUM8ERC с трока [. форндт1. napaHerptijilsll) Здесь строке — это строка, содержащая символьное представление числа; формат — необязательная маска форматирования, которая определяет, как функция TONUM- BER должна интерпретировать символьное представление числа, содержащегося в первом параметре; пара не!pujils — необязательная строка, которая содержит значения параметров NLS. Ее можно применять для подмены текущих установок параметров NLS уровня сеанса.
Числовые преобразования 253 Использование функции TO_NUMBER без параметров форматирования Часто функция TO_NUMBER вызывается без строки форматирования. Например, сле- следующие преобразования выполняются без использования дополнительных пара- параметров: DECLARE а Ь с d nl n2 BEGIN а Ь с d END: NUMBER: NUMBER: NUMBER: NUMBER: VARCHAR2BQ) := VARCHAR2C20) :- :- TO NUMBERC123 :- TO NUMBER(nl): :- TO NUMBER(n2): 1-123456,78': '+123456.78'; .45'); :- TO NUMBERC1.25E21): Как правило, функцию TONUMBER можно вызвать без параметров форматирова- форматирования в следующих случаях: О когда число представлено только цифрами с единственной десятичной запятой; О при использовании научного формата — например 1.25Е2; О когда знак числа (плюс или минус) стоит перед числом; при отсутствии знака число считается положительным. Если символьная строка не соответствует этим критериям или нужно округ- округлить значения до заданного количества десятичных знаков, вызывайте функцию TONUMBER с маской форматирования. Использование функции TO_NUMBER с маской форматирования Применение функции TO_NUMBER с маской форматирования позволяет получить более разнообразные представления чисел. Например, можно задать местополо- местоположение разделителей групп1 и символа валюты: а := TOJUMBERC'$123 456.78'. 'L999G999D99'): Не обязательно указывать в строке форматирования точное количество цифр. Функция TO_NUMBER позволяет задать в строке форматирования больше цифр, чем в преобразуемом значении. Следующая строка кода также будет выполнена без ошибки: 3 :- TOJUMBERC$123 456,78'. 'L999G999G999D99'); В этом и следующих примерах данной главы предполагается, что параметр NLS_NUMERIC_CHARACTERS определяет использование в качестве разделителя групп разрядов пробел, в качестве десятичного разделителя — запятую (то есть NLSJLMERICCHARACTERS-', '), а в качестве обозначения валюты — символ <$». — Примеч. перев.
254 Глава 9 • Числа Но если справа или слева от десятичной запятой значение содержит больше цифр, чем допускает маска форматирования, то преобразование не будет выпол- выполнено, и вы получите сообщение об ошибке ORA-06502: PL/SQL: numeric or value error. Первое из следующих двух преобразований завершится ошибкой, посколь- поскольку строка содержит десять цифр слева от десятичной запятой, тогда как маска разрешает только девять. Второе преобразование завершится ошибкой из-за того, что строка содержит слишком много цифр справа от десятичной запятой: а :- TO_NUMBERC$1234 567 890.78". 'L999G999G999D99'); а :- TO_NUMBER("$234 567 890.780'. 'L999G999G999D991): Используя элемент форматирования 0, можно правильно обработать нулевые старшие разряды числа: а :- TOJUMBERC'OOl 234'. '000GOO0'); С помощью элемента PR можно распознать угловые скобки как обозначение отрицательного числа: а :- TO_NUMBER('<123.45>'. '999D99PR'): Однако не все элементы форматирования предназначены для преобразования строк в числа. Например, элемент RN, который применяется для вывода числа римскими цифрами, предназначен только для форматирования выводимой ин- информации. Поэтому следующая попытка преобразования вызовет ошибку: а :- TO_NUMBER('cxxiir. 'rn'): Элемент ЕЕЕЕ тоже используется только для форматирования вывода. Для об- обратного преобразования он не нужен, поскольку функция TO_NUMBER распознает числа в научном формате безо всяких дополнительных указаний: а :- T0JUMBERC1.23456E-241); Передача функции TO_NUMBER установок NLS Действие многих элементов форматирования, перечисленных в табл. 9.5, опреде- определяется текущими установками параметров NLS. Например, элемент G представ- представляет разделитель групп разрядов, но какой именно символ используется в качест- качестве разделителя - зависит от текущего значения параметра NLS_NUMERIC_CHARACTERS в момент выполнения преобразования. Для того чтобы просмотреть эти установ- установки, можно запросить представление NLS_SESSION_PARAMETERS: SQL> SELECT * FROM nls_sess1on_paraiieters PARAMETER NLS LANGUAGE NLS TERRITORY NLS CURRENCY NLS ISO CURRENCY NLS NUMERIC CHARACTERS NLS CALENDAR NLS DATE FORMAT VALUE AMERICAN AMERICA $ AMERICA GREGORIAN DD-MON-RR Некоторые установки по умолчанию зависят от других. Если присвоить пара- параметру NLS_TERRITORY значение AMERICA, Oracle по умолчанию установит параметр NLS_NUMERIC_CHARACTERS в '..' (первый символ - десятичный разделитель, второй -
Числовые преобразования 25S разделитель групп разрядов). Это не мешает явно присвоить параметру NLS_CUR- RENCY с помощью команды ALTER SESSION другое значение: ALTER SESSION SET NLS_CURRENCY-'р.' Иногда определенные параметры NLS требуется переустановить только на вре- время вызова функции TOJWMBER. В данном случае нужные установки задают в этом вызове, и действуют они исключительно для него. Например, в следующем операто- операторе в вызове функции TO_NUMBER задаются установи* NLS, соответствующие NLS_TER- RITORY-FRANCE: а -ТО NUMBERCF123.456.7B1. ' L999G999D99'), 1NLS NUMERIC_CHARACTERS-' || '"NLS CURRENCY-"F || ' NLSJStKURRENCY-FRANCE1): Поскольку строка параметров NLS слишком длинная, мы разбили ее на три отдельных строки, объединенных с помощью оператора конкатенации, чтобы при- пример лучше выглядел на странице. Обратите внимание на дублирование кавычек. Установка параметра NLS_NUMERIC_CHARACTERS выглядит следующим образом: NLS_NUMERIC_CHARACTERS-',.' Поскольку это значение вместе с кавычками включается в строку параметров NLS, каждую кавычку нужно продублировать. В результате получается строка: 'NLS_NUMERIC_CHARACTERS-' В функции TO_NUMBER можно задавать только три приведенных в данном при- примере параметра NLS. Было бы удобнее, если бы можно было написать так: а :- TO_NUMBERС'F123.456.7В'. 'L999G999D99'. 'NLSJERRITORY-FRANCE'): Но, к сожалению, в функции TONUMBER нельзя задать параметр NLSJTRRITORY. Она поддерживает только NLSJUMERI ^CHARACTERS, NLS_CURRENCY и NLS_ISO_CURRENCY. ПРИМЕЧАНИЕ Более подробную информацию об установке различных параметров NLS вы найдете в документа- документации Oracle9i в части Oracle's Globalization Support Guide. He советуем задавать третий аргумент функции TONUMBER - лучше полагаться на установки сеанса, определяющие, как PL/SQL интерпретирует элементы маски форматирования L, G и D. Вместо того чтобы жестко кодировать информацию в про- программах, нужно дать возможность пользователю задавать их на уровне сеанса. Функция TO_CHAR Эта функция обратна функции TO_NUMBER. Она преобразует число в его символь- символьное представление. Используя необязательную маску форматирования, можно подробно указать, каким должно быть представление. Функция TO_CHAR вызыва- вызывается следующим образом: TO_CHAR <.чиспо [. формат[. napaneTpt)_nlsY\) Здесь число — это число, которое требуется представить в символьной форме. Оно может относиться к любому символьному типу PL/SQL: NUMBER, PLSJNTEGER или BINARY_INTEGER; формат — необязательная маска форматирования, определяю- определяющая, как функция TO_CHAR должна представить число в символьной форме; пара - "erpt/ji7s - необязательная строка, содержащая значения параметров NLS. Ее можно применять для подмены текущих установок параметров NLS уровня сеанса.
256 Глава 9 • Числа ПРИМЕЧАНИЕ — Если вы хотите, чтобы результат был представлен с помощью национального набора символов, ис- используйте вместо TO_CHAR функцию TO_NCHAR. При этом помните, что строка форматирования числа тоже должна быть представлена символами национального набора. Иначе результатом будет строка, состоящая из символов «#». Использование функции TO_CHAR без маски форматирования Функцию TO_NUMBER, как и TO_CHAR, можно вызывать без маски форматирования: DECLARE Ь VARCHAR2C0); BEGIN b :- TO_CHARA23456789.01): DBMSJirTPUT. PUT_LINE(b): END: Данный код выводит следующие значения: 123456789.01 Однако в отличие от TONUMBER функцию TOCHAR редко вызывают таким образом. Чтобы число лучше читалось, нужно задать, как минимум, разделитель групп разрядов. Использование функции TO_CHAR с маской форматирования При преобразовании числа в символьное представление функция TO_CHAR исполь- используется чаще всего с маской форматирования. Например, с ее помощью можно вы- вывести денежную сумму: DECLARE Ь VARCHAR2C30): BEGIN b := ТО_СНАШ234567В9.01. 'L999G999G999D99'): DBMSOUTPUT. PUTLINECb): END; Результат будет выглядеть следующим образом: $123 456 789,01 Элементы маски форматирования, описанные в табл. 9.5, позволяют очень гиб- гибко задавать формат символьного представления числа. Чтобы лучше понять, как они работают, стоит немного с ними поэкспериментировать. В следующем при- примере указывается, что в числе должны быть сохранены нулевые старшие разряды, но при этом элемент форматирования В требует превращения нулей в пробелы. Данный элемент предшествует цифровым элементам (нулям), но следует за ин- индикатором вывода обозначения валюты L: DECLARE b VARCHAR2C3Q): BEGIN b := TO_CHARA23.01. 'LBOOOGOOOG009D99'): D8MS_OUTPUT.PLrr LINE(b);
Числовые преобразования 257 Ь ¦- TD_CHAR@. 'LB000G0Q0G009D991); DBMS_0UTPUT.PU7_LINE(b); END; Результат будет иметь следующий вид: $000 000 123.01 Вы видите только одну строку, полученную после первого преобразования. В результате второго преобразования получился нуль, и элемент форматирова- форматирования В инициировал возврат функцией TOCHAR пустой строки, хотя в маске форма- форматирования указано, что нулевые старшие разряды числа следует оставить. В каче- качестве эксперимента можно выполнить этот пример без элемента В. ПРИМЕЧАНИЕ Не все комбинации элементов форматирования являются допустимыми. Например, нельзя исполь- использовать сочетание LRN, которое выводит перед числом, записанным римскими цифрами, символ ва- валюты. Oracle не документирует таких нюансов, поэтому о некоторых из них можно узнать только посредством эксперимента. Элемент форматирования V Едва ли вы часто будете пользоваться элементом V, но знать о нем нужно. Этот элемент форматирования позволяет масштабировать значение, и его действие лучше показать на примере (рис. 9.4). 123,45- 999V9999 О Применение часки форматирования с элементом V к числу 4f 2 Э v TT 9 9 9 © Накладываем число не формат. Десятичную запятую аыравнивави с позицией элемента V 1*7 Удаляем элемент V и десятичную запятую О Дописываем нули, чтобы результирующее количество значащих цифр получилось таким же, как в маска форматирования X 2345 0 0 © Возвращаем результат Рис 9.4. Элемент форматирования V Для чего может потребоваться масштабировать значение при преобразовании его в символьную форму? Рассмотрим следующий пример. Стандартная единица сделки на бирже равняется 100 акциям, и сообщая о реализованных на бирже ак- акциях обычно говорят о количестве проданных пакетов по 100 акций. Поэтому 123 продажи означает 123 пакета по 100 акций, то есть 12 300 акций. Следующий при- пример показывает, как использовать элемент V для масштабирования значения 123, учитывая, что на самом деле оно представляет количество сотен: DECLARE shares_50ld NUMBER :- 123: BEGIN DBMS OUTPUT.PUT LINE(
258 Глава 9 • Числа ТО CHARtShares sold, '999G9V99') ): END; Результат будет выглядеть следующим образом: 12 300 Заметьте, что в этом примере маска форматирования включает элемент G, оп- определяющий местоположение разделителя групп (пробела), который может быть задан только слева от элемента V, что не всегда удобно. Рассмотрим такую маску форматирования: T0_CHARA23.45. '9G99V9G999') Вы ожидаете результата 1 234 500, но элемент G нельзя задавать справа от V. Можно использовать маску 9G99V9999, чтобы получить результат 1 234500, или мас- маску 999V9999 для получения 1234500. Однако все это - не то, что нам нужно. Округление при преобразовании чисел в символьные строки В том случае, когда при преобразовании символьной строки в число слева от де- десятичной запятой имеется больше цифр, чем допускает маска форматирования, возникает ошибка. Если после десятичной запятой задать меньше цифр, то число будет округлено до заданного в маске количества цифр. Функция TO_CHAR возвратит строку, состоящую из символов «#», если преоб- преобразование не получилось из-за того, что слева от запятой было слишком много цифр. Например, следующее преобразование не будет выполнено, потому что число 123 не помещается в маску: DECLARE b VARCHAR2C30); BEGIN b .-TOCHARC1Z3.4567, '99.991: DBMS OUTPUT.PUTJJNECb); END: Если же дробная часть числа не помещается в маску, то получится округлен- округленный результат: BEGIN №HS_0UTPUT.PUTJ.IIE<T0_CHARA23.4567. 'ЮЭЮЭ1)): DBMS_OUTPUT.PUT LINE(T0_CHARA23.4567, 'MS')): END; 123.46 123 Цифры 5 и больше округляются в большую сторону, так что 123,4567 округля- округляется до 123,46, а цифры меньше 5 - в меньшую, поэтому 123,4ххх всегда округля- округляется до 123.
Числовые преобразования 259 Обработка пробелов при преобразовании чисел в символьные строки Преобразуя число в символьную строку, функция T0_CHAR всегда оставляет место для знака «-», даже если число положительное. DECLARE Ь VARCHARZC30); с VARCHAR2C30); BEGIN Ь :- ТО CHARC-123.4, '999D99"): с :- T0~CHARU23.4. '999D99'); DSMS_OUTPUT.PUT LINEC':1 || b || ' ' || TO CHAR(LENGTHCb)>): DBMS_OUTPUT.PUT~LINE(':' || с || ' ' || TOJHAR(LENGTHtc))): END: Результат преобразования имеет следующий вид: :-123.40 7 : 123.40 7 Обратите внимание, что каждая строка имеет длину семь символов, хотя для положительного числа достаточно и шести. Ведущий пробел применяется при выводе чисел в виде столбца. Но если пробел не нужен, можно его убрать. ПРИМЕЧАНИЕ Когда для представления отрицательных чисел используются угловые скобки (в маске задан эле- элемент PR), положительные числа дополняются одним ведущим и одним завершающим пробелом. Если необходимо, чтобы числа не содержали ни ведущих, ни завершающих пробелов, можно применить несколько методов. Один из них заключается в ис- использовании элемента форматирования ТМ, задающего «минимальное» представ- представление числа: DECLARE b VARCHAR2C30): с VARCHAR2C30): BEGIN Ь.:= TO_CHAR(-123.4. 'TM9'); с :- ТО CHARC1Z3.4. 'ТМ9'): DBMS_OUTPUT.PLTT_LINE(':' || b || ' ' || TO_CHAR(LENGTH(b))): DBMS_aUTPUT. PLTT_LINEC':' || С || ' ' || ТО CHAR(LENGTHCO) j; END: Результат выполненного преобразования выглядит следующим образом: :-123.4D 6 :123.40 7 Этот метод удобен, но элемент ТМ появился только в Oracle8i и более ранними версиями не поддерживается. Кроме того, он не позволяет использовать другие элементы форматирования. Например, нельзя задать формат ТМ999.99, чтобы по- получить после десятичной запятой две цифры. Если необходимы другие элементы форматирования или вы работаете с одной из ранних версий PL/SQL, можно Усечь результат преобразования: DECLARE Ь VARCHAR2O0): с VARCHAR2C0):
260 Глава 9 • Числа BEGIN b :- LTRIM(T0_CHAR(-123.4, '999D99')); с '.- LTRIM(T0_CHARA23.4, 999D99')): DBMS OUTPUT.PUT_LINE(':' || b || ' ' || TO_CHAR(LENGTH(b))): DBMS>JTPUT.PUT_LINE(':' || с || ' ' || TO_CHAR(LENGTH(c))): END; Ниже приведен результат преобразования: :-123.40 7 :123.40 6 В данном случае мы применили функцию LTRIM, чтобы удалить ведущие про- пробелы, и сохранили две фиксированные цифры справа от десятичной запятой. Если же знак выводится справа от числа, что осуществляется с помощью элемента фор- форматирования Ml, то можно воспользоваться функцией RTRIM. Когда отрицательные числа выводятся в угловых скобках и из-за этого у положительных имеются про- пробелы с двух сторон — тогда нужно применить функцию TRIM. Передача функции TO_CHAR значений параметров NLS Функция TOCHAR может принимать в качестве третьего параметра строку устано- установок NLS, подобно функции T0_NUM8ER. Например: BEGIN D8MS0UTPUT. PUTLI NEC TO_CHARA23456.78. '999G999D99'. ' NLS_NUHERIC_CHARACTERS=''.. " ') ); END; Результат имеет следующий вид: 123.456.78 Таким способом можно задавать три параметра NLS: NLS_NUMERIC_CHARACTERS, NLS_CURRENCY и NLS_ISO_CURRENCY. Ранее в этой главе приводился пример одновре- одновременного использования всех трех параметров. Использование функции CAST Эта функция появилась только в Oracle9i и может применяться для преобразова- преобразования числа в строку и наоборот. Синтаксис функции выглядит следующим образом: CAST (.выражение AS тип_данных) В приведенном ниже примере показано, как с помощью функции CAST преоб- преобразовать число типа NUMBER в строку типа VARCHAR2, а затем каждый символ этой строки в отдельности преобразовать в соответствующее числовое значение: DECLARE a NUMBER := 123.45: al VARCHAR2C30): Ь VARCHAR2C0) := '-123.45': Ы NUMBER; BEGIN al := CAST [a AS VARCHAR2): Ы :- CAST (b AS NUMBER); DBMS_OUTPUT.PUT_LINE (al); DBMS OUTPUT.PUT LINE (Ы); END:
Числовые преобразования Z61 Результат выполнения кода имеет следующий вид: 123.45 -123.45 У функции CAST есть один недостаток: она не поддерживает маски форматиро- форматирования чисел. Однако она является частью стандарта ANSI SQL, тогда как функ- функции TO_CHAR и T0JWMBER в этом стандарте не описаны. Если разработчику важно, чтобы программный код был полностью совместим со стандартом ANSI, следует использовать для преобразований чисел в строковые значения функцию CAST. В других случаях мы рекомендуем применять функции TO_CHAR и TO_NUMBER. ПРИМЕЧАНИЕ Поскольку PL/SQL не соответствует стандарту ANSI, написать на этом языке вполне совместимый с указанным стандартом код невозможно. Таким образом, функция CAST в программах на PL/SQL во- вообще не нужна. Она востребована только в SQL-инструкциях (SELECT, INSERT и т. п.), если они должны быть совместимы со стандартом ANSI. Неявные преобразования Еще один способ преобразования чисел в строки состоит в том, чтобы поручить выполнение этой работы PL/SQL. Такое преобразование называется неявным, поскольку оно не определяется явно в программном коде. Ниже приведен пример неявных преобразований: DECLARE a NUMBER: b VARCHARZOO); BEGIN a :- '-123.45': b :- -123.45; Однако неявные преобразования имеют ряд недостатков. Во-первых, при ис- использовании этого метода разработчик не имеет полного контроля над программ- программным кодом. Всегда лучше знать, когда и какое именно происходит преобразование, поэтому желательно выполнять его явно. Если полагаться на неявное преобразо- преобразование, то вы не сможете отследить, где и когда оно происходит, и код получится менее эффективным. Кроме того, наличие явных преобразований делает код бо- более понятным другим программистам. Во-вторых, результат неявного преобразования не всегда очевиден. Рассмот- Рассмотрим пример: DECLARE a NUMBER; BEGIN а :- '123.400' || 999; Какое значение будет содержаться в переменной а после выполнения данного Кода, зависит от того, как PL/SQL вычисляет выражение справа от оператора присваивания. Если он начинает с преобразования строки в число, вычисление будет выполнено следующим образом: а :- 423,400' || 999; а :- 123.4 || 999; а :- '123,4' || 999;
262 Глава 9 • Числа а :- '123.4999'; а :- 123.4999: Если PL/SQL начнет с преобразования числа в строку, результат будет иметь такой вид: - '123.4001 || 999: - '123.400' || '999': - '123.400999': - 123.400999: Даже если вы знаете, какой из двух результатов получится на самом деле, вряд ли другие программисты будут догадываться об этом, читая ваш код. В дан- данном случае лучше написать следующее (кстати, именно таким образом в Oracle вычисляется предыдущее выражение): а :- ТО NUMBER С 123.400' || ТО CHARO99)); ПРЕОБРАЗОВАНИЕ ОДНОГО ЧИСЛОВОГО ТИПА ДАННЫХ В ДРУГОЙ Язык PL/SQL поддерживает три числовых типа данных, NUMBER, PLS_IN- TEGER и ВI NARY_I NTEGER, и поскольку в нем отсутствуют функции для преоб- преобразования одного числового типа данных в другой, все подобные преобра- преобразования выполняются неявно в случае, если в выражении используются разные числовые типы. Это действие, как и любое другое, требует опреде- определенных затрат ресурсов, поэтому желательно свести количество подобных преобразований к минимуму. Большая часть описанных в следующем разделе числовых функций (если не все) принимает в качестве параметров значения типа NUMBER. Поэтому вызывая их с аргументами типа PLS_INTEGER или B1NARYJ NTEGER, вы приме- применяете неявное преобразование типов. Постарайтесь изолировать значения типа PLSJNTEGER и BINARYJNTEGER от переменных типа NUMBER и числовых функций, которые используются в программе. Числовые функции В PL/SQL реализовано множество числовых функций для работы с числами. Эти функции приведены в табл. 9.6 и подробно описаны в следующих разделах. Таблица 9.6. Встроенные числовые функции PL/SQL Функция Возвращаемое значение ABS Абсолютное значение числа ACOS Арккосинус заданного угла ASIN Арксинус заданного угла ATAN Арктангенс заданного угла
Числовые функции Функция Возвращаемое значение ATAN2 Арктангенс заданного угла, позволяет задать это число иначе, чем в функции ATAN BITAND Результат поразрядной операции AND по отношению к битам двух положительных целых чисел CEIL Наименьшее целое число, которое больше или равно заданному значению COS Косинус заданного угла COSH Гиперболический косинус заданного угла ЕХР(п) Число е в степени п, где е=2,71828183... FLOOR Наибольшее целое число, которое меньше или равно заданному значению LN(a) Натуральный логарифм числа а LOG{a,b) Логарифм числа b no основанию а MOD(a,b) Остаток деления а на Ь POWER(a,b) Результат возведения числа а в степень Ь ROUND(a,[b]) Результат округления числа а до Ь знаков после десятичной запятой SIGN(a) 1 — если число а имеет положительное значение, 0 — если а равно нулю, -1 — если а отрицательно SIN Синус заданного угла SINM Гиперболический синус заданного угла SQRT Квадратный корень числа TAN Тангенс заданного угла TANH Гиперболический тангенс заданного угла TRUNC(a,[b]) Число а, усеченное до о знаков после десятичной запятой Примите во внимание то, что тригонометрические и логарифмические функ- функции поддерживаются только в PL/SQL версии 2.0 и выше. Обратные тригономет- тригонометрические функции появились в версии 2.3. При использовании указанных функций учтите, что все углы задаются в ра- радианах, а не в градусах. Преобразование радианов в градусы и наоборот выполня- выполняется следующим образом: радианы - к * градусы/180 - Градусов а радианы градусы - радианы * 180/я - Радианов в градусы Корпорация Oracle не реализовала функцию, возвращающую значение к, но его можно получить косвенным методом: ACOS(-l) Арккосинус числа -1 равен значению п. Поскольку это число представляет со- собой бесконечную десятичную дробь, вы всегда будете работать с округленным его значением. Для получения нужной точности можно воспользоваться функцией ROUND. Далее рассмотрены функции, выполняющие округление и усечение число- числовых значений, а затем описана каждая из встроенных числовых функций PL/SQL.
264 Глава 9 • Числа Функции округления и усечения Существуют четыре числовые функции, выполняющие округление и усечение числовых значений: CEIL, FLOOR, ROUND и TRUNC. У начинающих программистов вы- выбор нужной функции для конкретной ситуации может вызвать определенные за- затруднения. Поэтому в табл. 9.7 мы приводим их сравнительное описание. Таблица 9.7. Функции, выполняющие округление и усечение чисел Функция Описание CEIL Возвращает наименьшее целое число, которое больше или равно заданному значению (от англ. ceiling — потолок) FLOOR Возвращает наибольшее целое число, которое меньше или равно заданному значению (от англ. floor — пол) ROUND Выполняет округление числа. Положительное количество цифр после десятичной запятой задает округление цифр, находящихся справа от десятичной запятой, а отрицательное — находящихся слева TRUNC Усекает число до заданного количества десятичных знаков, отбрасывая все цифры, находящиеся справа Результаты вызовов всех четырех функций с разными значениями показаны на рис. 9.5. О том, как округлять и усекать даты, вы можете узнать из главы 10. Количество десятичных знаков Рис. 9.5. Функции округления и усечения Функция ABS Возвращает абсолютное значение числа. Ее синтаксис имеет следующий вид: FUNCTION ABS (л NUMBER) RETURN NUMBER Функция ABS помогает упростить логику программного кода. Предположим, что программа выводит отчет доходов, расходов и соответствующие итоги. Если отклонение в строке составляет больше 100 долларов, она помечается как оши- ошибочная вне зависимости от того, было это значение положительным или отрица- отрицательным. Первая версия кода выглядела следующим образом: IF variance_table (linejtem_nu) BETWEEN 1 AND 100 OR variance_table Cline_item_nu> BETWEEN -100 AND -1 THEN apply_variance (statementjd); ELSE flag_error (statementjd, line_item_nu); END IF; Функция ROUND TRUNC FLOOR CEIL 1,75 0 60 50 55 56 1,3 0 60 50 55 56 55,56 1 60 50 55 56 55,56 -1 60 50 55 56
Числовые функции . 265 Здесь variance_taWe — таблица PL/SQL, в которой хранятся отклонения для ка- каждой строки отчета. В этот код можно внести два усовершенствования. Во-первых, вместо того чтобы жестко кодировать величины максимально допустимых отклонений, нуж- нужно присвоить их именованным константам. Во-вторых, чтобы проверка выполня- выполнялась только один раз, можно воспользоваться функцией ABS. После этих измене- изменений приведенный выше код примет следующий вид: IF ABS (variancetabie Cline_item_nu)) BETWEEN min_variance AND max_variance THEN app1y_variance (statement_id): ELSE flag_error (statementjd. line_1tem_nu): END IF; Функция ACOS Возвращает арккосинус угла. Синтаксис функции таков: FUNCTION ACOS (л NUMBER) RETURN NUMBER Здесь п — число из диапазона [-1; 1]. Значение, возвращаемое функцией ACOS, на- находится в пределах от 0 до л. Функция ASIN Возвращает арксинус угла. Синтаксис этой функции выглядит следующим образом: FUNCTION AS1N (л NUMBER) RETURN NUMBER Здесь п - число из диапазона [-1; 1]. Возвращаемое функцией AS IN значение на- находится в пределах от -тс/2 до л/2. Функция ATAN Возвращает арктангенс угла. Ниже приведен синтаксис данной функции: FUNCTION ATAN (л NUMBER) RETURN NUMBER Здесь п — число из диапазона (-°°; +«>). Функция ATAN возвращает значение, кото- которое находится в пределах от -л/2 до л/2. Функция ATAN2 Возвращает арктангенс п/тп. Синтаксис функции имеет следующий вид: FUNCTION ATAN2 (л NUMBER, m NUMBER) RETURN NUMBER Здесь пит — числа из диапазона (-°°; +°°). Значение, возвращаемое функцией ATAN2, находится в пределах от -л до л. Функция BITAND Выполняет поразрядную операцию AND над битами двух положительных целых Чисел. Синтаксис функции: FUNCTION BITAND (л NUMBER, m NUMBER) RETURN NUMBER
266 Глава 9 • Числа Следующий пример показывает, как применять функцию BITAND для чтения битов целочисленной переменной, используемой для хранения флагов: DECLARE -- Объявляем переменную для хранения флагов. -- биты которой интерпретируются следующим образом: -- 0000 |--0-женщина. 1-мужчина |—0-частичная занятость, 1-полный день | 0-почасовая оплата, 1-ставка | 0-офис. 1-фабрика b1t_flags PLSJNTEGER: BEGIN -- Инициализируем переменную флагов значением. -- соответствующим сотруднице, которая работает полный день. -- Значение 3 в двоичном представлении равно 11. bit_flags :- 3; -- Выполняем определенные действия. -- если сотрудник работает полный день. IF BITAND(bit_flags, 2) <>0 THEN DBMS_OUTPUT.PUT_LINE('Сотрудник работает полный день.'); Для того чтобы установить отдельные биты целочисленного значения, можно применить следующий метод. Первый из двух операторов сбрасывает бит полной занятости, а второй его устанавливает: b1t_flags :- BITAND Cbit_flags. 13); bitjlags :- BITAND tbit_flags. 13) + 2: Суть метода заключается в том, что в результате вызова функции BITAND с би- битовой маской возвращаются все биты, кроме того, который требуется установить (или сбросить). Значение 13 в двоичном формате представляется как 1101. По- Поэтому в значении, возвращаемом функцией 8ITAND с таким аргументом, второй бит всегда будет равен 0. Если нам нужно, чтобы значение данного бита было рав- равно 1, прибавим к результату 2. В двоичном представлении число 2 выглядит как 10, и после сложения второй бит результирующего числа будет равен 1. Максимальное положительное значение, которое можно присвоить перемен- переменной типа PLS_INTEGER, составляет 230, то есть для флагов доступно 30 бит. Функция CEIL Возвращает наименьшее целое число, которое больше или равно заданному зна- значению. Синтаксис функции выглядит следующим образом: FUNCTION CEIL (n NUMBER) RETURN NUMBER Ниже приведено несколько примеров ее вызова: CEIL F) -¦ 6 CEIL A19.1) -> 120 CEIL (-17.2) -+ -17 Можете сравнить функцию CEIL с другими числовыми функциями, обратив- обратившись к табл. 9.7 и рис. 9.5 раздела этой главы «Функции округления и усечения».
Числовые функции 267 функция COS Возвращает косинус заданного угла. Ниже приведен ее синтаксис: FUNCTION COS (.угол NUMBER) RETURN NUMBER Здесь угол должен быть задан в радианах. Если значение угла указано в градусах, функцию COS нужно вызвать следующим образом: niy_cosine :- С05(АС05(-1)*угол_в_гралуих/1ВО); функция COSH Возвращает гиперболический косинус заданного значения. Приведем синтаксис функции: FUNCTION COSH (л NUMBER) RETURN NUMBER Если п — действительное число, a f - мнимая единица, тогда связь между функциями COS и COSH можно выразить следующим образом: COS A * n) - COSH (n) Функция ЕХР Возвращает число е в степени п, где п - аргумент функции. Ниже описан синтак- синтаксис функции: FUNCTION EXP (n NUMBER) RETURN NUMBER Число е (приблизительно равное 2,71828) является основанием натурального логарифма. Функция FLOOR Возвращает наибольшее целое число, которое меньше или равно заданному зна- значению. Синтаксис функции имеет следующий вид: FUNCTION FLOOR (л NUMBER) RETURN NUMBER Ниже приведено несколько примеров ее вызова: FLOOR F.2) -> 6 FLOOR (-89.4) -> -90 Для сравнения функции FLOOR с другими числовыми функциями обратитесь к табл. 9.7 и рис. 9.5 раздела «Функции округления и усечения» этой главы. Функция LN Вычисляет натуральный логарифм числа. Ее синтаксис таков: FUNCTION LN (л NUMBER) RETURN NUMBER Значение аргумента п должно быть больше 0 или равно ему. Если использовать в этой функции отрицательный аргумент, генерируется следующее сообщение об ошибке: ORA-01428: argument '-Г is out of range
268 Глава 9 • Числа Функция LOG Вычисляет логарифм заданного числа по указанному основанию. Ее синтаксис следующий: FUNCTION (i NUMBER, n NUMBER) RETURN NUMBER Основанием логарифма b должно быть значение, которое больше 1, а п — чис- число, которое больше 0 или равно ему. Если какой-либо из аргументов функции не отвечает этим требованиям, PL/SQL выдает следующую ошибку: ORA-0142B: argument '-Г is out of range Функция MOD Возвращает остаток, полученный при делении одного заданного числа на другое. Синтаксис функции имеет следующий вид: FUNCTION MOD (делииое NUMBER, делитель NUMBER) RETURN NUMBER Если целитель равен 0, делимое возвращается без изменений. Ниже следует не- несколько примеров вызова функции MOD: MOD A0. 5) -> 0 MOD B, 1) -> 0 MOD C. 2) -> 1 С помощью функции MOD можно быстро определить, является число четным или нечетным: FUNCTION is_odd (numjn IN NUMBER) RETURN BOOLEAN IS BEGIN RETURN MOD (numjn. 2) - 1: END: FUNCTION is_even Cnumjn IN NUMBER) RETURN BOOLEAN IS BEGIN RETURN MOD (nunjn. 2) - 0: END: Функция POWER Возводит число, являющееся значением первого аргумента, в степень, заданную значением второго. Синтаксис этой функции выглядит следующим образом: FUNCTION POWER (.число NUMBER, степень NUMBER) RETURN NUMBER Если число отрицательное, степень должна быть целым числом. Приведенное ниже выражение вычисляет диапазон допустимых значений переменной типа BI - NARYJNTEGER (от -231 + 1 до 231 - 1): POWER (-2. 31) + 1 .. POWER B. 31) - 1 или: -2147483647 .. 2147483647
Числовые функции 269 функция ROUND Возвращает первый аргумент, который округлен до количества цифр после деся- десятичной запятой, заданного во втором аргументе. Синтаксис функции таков: FUNCTION (л NUMBER [. количество_знакоа NUMBER]) RETURN NUMBER Аргумент кол/чество_знаков необязателен и по умолчанию равен 0. Это означа- означает, что число п будет округляться до нуля знаков после десятичной запятой, то есть до целого. Значение количество_знаков может быть и меньше нуля. В этом случае функция ROUND округляет цифры слева от десятичной запятой. Ниже при- приведено несколько примеров ее вызова: ROUND A53.46) -> 153 ROUND A53.46. 1) -> 153.5 ROUND A53, -1) -+ 150 Сравнить функцию ROUND с другими числовыми функциями можно, обратив- обратившись к табл. 9.7 и рис. Э.5 раздела «Функции округления и усечения» этой главы. Функция SIGN Возвращает знак числа. Ее синтаксис имеет следующий вид: FUNCTION SIGN (я NUMBER) RETURN NUMBER Функция возвращает -1, если л меньше нуля, 0 — если п равно нулю, +1 — если п положительно. Функция SIN Возвращает синус заданного угла. Синтаксис функции выглядит следующим об- образом: FUNCTION SIN (угол NUMBER) RETURN NUMBER Здесь угол должен быть задан в радианах. Как вызвать функцию SIN, если значе- значение угла задано в градусах, показано ниже: my_sine :- SIN Функция SINH Возвращает гиперболический синус заданного значения. Синтаксис этой функ- функции имеет следующий вид: FUNCTION SINH Сл NUMBER) RETURN NUMBER Если п — действительное число, а 1 — мнимая единица, то связь между функ- функциями SIN и SINH можно выразить таким образом: SIN Ci * л) = SINH (n) Функция SQRT Возвращает квадратный корень числа. Синтаксис этой функции таков: FUNCTION (л NUMBER) RETURN NUMBER
270 Глава 9 • Числа Здесь п должно быть больше 0 или равно ему. Если п - отрицательно, PL/SQL выдает следующее сообщение об ошибке: ORA-01428: argument '-Г 1s out of range Функция TAN Возвращает тангенс заданного угла. Синтаксис функции имеет следующий вид: FUNCTION TAN [угол NUMBER) RETURN NUMBER Здесь угол должен быть задан в радианах. Если у вас указано значение угла в гра- градусах, функцию TAN нужно вызвать следующим образом: my_tane :- TAN Функция TANH Возвращает гиперболический тангенс заданного значения. Ее синтаксис таков: FUNCTION TANH (л NUMBER) RETURN NUMBER Если л — действительное число, а 7 — мнимая единица, то связь между функ- функциями TAN и TANH можно выразить следующим образом: TAN A * n) - TANH (n) Функция TRUNC Усекает значение первого аргумента до количества цифр после десятичной запя- запятой, заданного во втором аргументе. Синтаксис функции следующий: FUNCTION TRUNC (л NUMBERC. котчество_зн<!ков NUMBER]) RETURN NUMBER Аргумент количество_знаков необязателен и по умолчанию равен 0 (число п усе- усекается до целого). Значение количество_знаков может быть и меньше 0. В этом слу- случае функция TRUNC преобразует в нули цифры слева от десятичной запятой. Ниже приведено несколько примеров ее вызова: TRUNC C153.46) -> 153 TRUNC A53.46. 1) -> 153.4 TRUNC (-2003.16, -1) -» -2000 Для сравнения функции TRUNC с другими числовыми функциями обратитесь к табл. 9.7 и рис. 9.5 раздела «Функции округления и усечения» этой главы.
10 Дата и время р- Типы данных даты и времени ^ Преобразование типов данных DATE и TIMESTAMP > Математические операции над значениями даты-времени > Функции обработки значений даты и времени Большинство приложений выполняют те или иные операции со значениями даты и времени. Работать с датами довольно сложно: кроме того что приходится иметь дело со строго форматированными данными, существует множество правил опре- определения их допустимых значений и проведения коррехтных вычислений (следует учитывать високосные годы, национальные праздники и выходные, диапазоны дат и т. д.). К счастью, СУБД Oracle и PL/SQL очень хорошо обрабатывают различ- различные'виды информации о датах и времени. И СУБД и PL/SQL предоставляют целый набор типов данных для хране- хранения дат и времени в определенном внутреннем формате. Независимо от того, как значения даты и времени выглядят в программе при вводе и выводе, в PL/SQL и СУБД они всегда представляются одним и тем же стандартным способом. Любое значение даты или времени может храниться в Oracle как информация, которую можно разбить на следующие составляющие: год, месяц, день, часы, ми- минуты, секунды, доли секунды, смещение часового пояса в часах, смещение часово- часового пояса в минутах, название и аббревиатура часового пояса. Поддержка типов данных даты и времени — только часть дела. Еще необходим язык, который может манипулировать этими значениями естественным и разум- разумным способом - как настоящими датами и временем. Oracle обеспечивает разра- разработчиков исчерпывающим набором функций для выполнения всевозможных опе- операций с датами и временем. Например, если нужно преобразовать символьную строку в дату, Oracle предоставит функцию ТО_ОАТЕ, хоторая способна правильно интерпретировать множество разных форматов даты. Нужно преобразовать время,
272 Глава 10 • Дата и время чтобы обеспечить соответствие одного часового пояса другому, — воспользуйтесь функцией NEWJIME. В вашем распоряжении имеется богатый набор встроенных функций для ра- работы с датой и временем, и к тому же Oracle позволяет непосредственно выпол- выполнять над этими значениями ряд арифметических операций. Например, чтобы вы- вычислить количество дней между двумя датами, достаточно вычесть одну дату из другой. Для успешной работы нужно уметь правильно пользоваться таким широким набором средств. Следующий раздел посвящен разным типам данных для пред- представления дат и времени, которые поддерживаются PL/SQL. Далее речь пойдет о выполнении преобразований между ними и о.том, как присваивать значения даты и времени переменной. В оставшейся части этой главы рассказывается о функци- функциях обработки даты и времени, а также о том, как работать с поддерживаемыми Oracle типами данных даты и времени. Типы данных даты и времени Если вы не стоите перед проблемой выбора, тогда все просто. В OracleBi, Oracle8 и предшествующих им версиях для работы с датой и временем имелся лишь тип данных DATE. Однако в Oracle9i появилось несколько новых типов данных, а сле- следовательно, и функциональных возможностей. В основном они связаны с семей- семействами типов данных TIMESTAMP и INTERVAL и помимо выполнения своих непосред- непосредственных «обязанностей» помогают РСУБД больше соответствовать стандартам ANSI/ISO SQL Тип данных DATE Исходный тип данных для представления даты и времени DATE, поддерживаемый Oracle, является единственным, который был доступен до Oracle8z включитель- включительно. В нем в виде 7-байтового значения фиксированной длины хранится следую- следующая информация: год; месяц; день; часы; минуты; секунды. Подобные внутренние значения нельзя задать в операторе присваивания. Вме- Вместо этого приходится полагаться на неявное преобразование символьных и чи- числовых значений в дату или выполнять такое преобразование явно с помощью функции TO_DATE (см. раздел «Преобразование типов данных DATE и TIME- STAMP» далее в этой главе). Объявление переменных типа DATE Синтаксис объявления переменной типа DATE таков: иня_пврененной [CONSTANfT] DATE [:= | DEFAULT начальное_значение] Здесь иня_переменной — это имя объявляемой переменной. В такой переменной можно хранить значение даты или комбинированное значение даты-времени. ПРИМЕЧАНИЕ — Значения даты-времени всегда содержат компоненты времени. Вы можете их использовать либо иг- игнорировать.
Типы данных даты и времени 273 Приведем несколько примеров объявления переменных типа DATE: DECLARE todaysJJate CONSTANT DATE :- SYSDATE: hire date DATE DEFAULT SYSDATE: end_Df>ear DATE :- T0_DATEC 12/31/2002'. 'MM/DD/YYYY'); Когда использовать тип данных DATE Тип данных DATE применяется в программах для баз данных, содержащих столб- столбцы этого типа, а также в приложениях, предназначенных для работы с базами данных версий до Огас1е8г. И, конечно, вы будете его использовать в том случае, если у вас установлена Oracle8t, Oracle8 или более ранняя версия. Однако при разработке новых приложений, предназначенных для Oracle9i и последующих версий, больше подойдут новые типы данных TIMESTAMP. Ограничения на использование типа данных DATE Для типа данных DATE имеется два ограничения, о которых следует знать. О Этот тип данных позволяет хранить время с точностью до секунды, так что для отслеживания в реальном времени процессов, длительность которых из- измеряется долями секунды, он не подходит. О Значения типа DATE не содержат информации о часовом поясе. Если пользова- пользователи вашей системы находятся в разных часовых поясах, могут возникать про- проблемы. Извлекая из базы данных значение типа DATE, вы должны знать, какому часовому поясу оно соответствует. При использовании нового типа данных — TIMESTAMP — указанные ограниче- ограничения отсутствуют. Он позволяет хранить время с точностью до миллиардной доли секунды, а также информацию о часовом поясе вместе со значениями даты и вре- времени. ПРИМЕЧАНИЕ- Если вы работаете с Oracle9l или более ранней версией РСУБД и вам нужно отслеживать время с точностью до долей секунды, информацию можно хранить в виде чисел. Для определения време- времени с большой точностью используется функция БЕТ_ПМЕ пакета DBMSJ/nUTY. Типы данных TIMESTAMP В Oracle9i введено три типа данных TIMESTAMP, иногда называемых временной мет- меткой. Все они позволяют хранить время с точностью до миллиардной доли секун- секунды; два из них включают информацию о часовом поясе, но в разных форматах. О TIMESTAMP. Хранит значение даты и времени без информации о часовом поясе. Эквивалентен типу данных DATE, отличаясь от него лишь тем, что хранит вре- время с точностью до миллиардной доли секунды. О TIMESTAMP WITH TIME ZONE. Хранит вместе с каждым значением даты и времени информацию о часовом поясе. О TIMESTAMP WITH LOCAL TIME ZONE. Хранит значения даты и времени, которые, как предполагается, соответствуют «локальному» часовому поясу. Для столбцов базы данных этого типа «локальным» считается часовой пояс базы данных,
274 Глава 10 • Дата и время а для переменных PL/SQ.L — часовой пояс сеанса. Значения столбцов базы данных, присваиваемые значениям переменных PL/SQL или наоборот, преоб- преобразуются в соответствии с часовым поясом каждого из них. Как разные типы данных отражают точность значений даты-времени при их «перемещении» от пользователя одного часового пояса через базу данных к поль- пользователю другого часового пояса, можно судить по рис. 10.1. База данных Oracle ¦ горном часовом поясе (UTC -7:00) Восточный Джонатан DWJ00215:00;000° 06-02-200215:00:00.00 -5:00 - 06-02-200215:00:00.00 -5:00- 06-02-200215:00:00.00 -5:00 DATE ¦•> 0B-O2-2D02 15:00:00 TIMESTAMP ¦»¦ 06-02-2002 15:00:00.00 TIMESTAMP WITH TIME ZONE ¦*¦ 06-02-200215:00:00.00 -5:00 TIMESTAMP WITH LOCAL TIME ZONE *¦ 06-02-2002 13:00:00.00 Горный часовой пояс (UTC -7:00) Бе 06-02-2002 15:00:00.00 -7:00 ^Ш 064J-200215:00:00.00 -7:00 ^ 06-02-200215.00.00.00 -5.00 Прави 06-02-200213:00:00.00 -7:00 •*¦¦¦— Правильное время, но в часовой пояса пользователя --¦ ' Донна Рис. 10.1. Разные типы значений даты-времени На рисунке показан пользователь Джонатан, находящийся в Восточном часо- часовом поясе, разница во времени между которым и всеобщим скоординированным временем (Coordinated Universal Time, или UTC) составляет 5 ч. (Описание UTC см. во врезке.) Джонатан сохраняет значения даты-времени четырех разных типов в четырех столбцах базы данных. Эти значения представлены в стандартной нота- нотации ANSI/ISO, и все они равны 3 часам дня A5:00:00.00) в Восточном часовом поясе (-5:00) 6 февраля 2002 года @6-02-2002). База данных находится в Горном часовом поясе. Обратите внимание на то, как в ней меняется представление времени для каждого из типов данных. Типы DATE и TIMESTAMP игнорируют разницу часовых поясов пользователя и базы данных. Они не сохраняют информацию об исходном часовом поясе, так что эта инфор- информация теряется, а вместе с ней теряется и значение времени, которое было зафик- зафиксировано первым пользователем. При записи значения времени в столбец типа TIMESTAMP WITH TIME ZONE инфор- информация о часовом поясе сохраняется, так что значение столбца представляет ре- реальное время, зафиксированное Джонатаном. Сравните значение этого столбца
Типы данных даты и времени 275 со значением столбца типа TIMESTAMP WITH LOCAL TIME ZONE, в котором время, зафик- зафиксированное в Восточном часовом поясе, приведено в соответствие с Горным часо- часовым поясом. На этот раз сохранено правильное значение времени, но потеряна информация о том, в каком часовом поясе оно фиксировалось. ПРИМЕЧАНИЕ : В отношении типа данных TIMESTAMP WITH TIME ZONE на рис. 10.1 показана концепция, а не ре- реальный формат данных. Oracle представляет все значения этого типа как всеобщее скоординиро- скоординированное время (время UTC). Без сомнения, это сделано для того, чтобы облегчить сравнение значе- значений даты-времени и выполнение арифметических операций над этими значениями. Для значения на рисунке внутреннее представление будет следующим: 06-02-2002 15:00:00.00 -5:00. Информа- Информация о часовом поясе сохраняется таким образом, что дату и время легко преобразовать в соответст- соответствии с исходным часовым поясом, а внутренний формат UTC для вас будет совершенно прозрачным. Рассмотрим данные, полученные пользователем Донной. Она, как и база дан- данных, находится в Горном часовом поясе. Значения типа ОАТЕ и TIMESTAMP неверные. Джонатан зафиксировал время 15:00 в Восточном часовом поясе, а Донна видит то же время, 15:00, в Горном часовом поясе. С другими типами данных дело об- обстоит лучше. Значение типа TIMESTAMP WITH TIME ZONE Донна видит в точности так, как оно было введено Джонатаном в Восточном часовом поясе. Кроме того, Дон- Донна видит правильное время для значения типа TIMESTAMP WITH LOCAL TIME ZONE, но не знает, в каком часовом поясе оно было введено. ВСЕОБЩЕЕ СКООРДИНИРОВАННОЕ ВРЕМЯ Всеобщее скоординированное время (Coordinated Universal Time, или UTC) измеряется с высочайшей точностью при помощи атомных часов и состав- составляет основу всемирной системы времени. Например, все часовые пояса оп- определяются как разница в часах и минутах относительно мирового време- времени, соответствующего среднему местному времени относительно нулевого меридиана. Поскольку TJTC — атомарное время, оно периодически коррек- корректируется для синхронизации со временем, определяемым в зависимости от вращения Земли. Вы, вероятно, знакомы с понятием времени по Гринвичу (Greenwich Mean Time, GMT). Практически GMT эквивалентно UTC, но между ними есть разница. Почему используется аббревиатура UTC, а не CUT? Центры стандартиза- стандартизации никак не могли договориться между собой относительно того, какую аббревиатуру, английскую CUT или французскую TUC, им использовать. Поэтому сошлись на UTC, что не соответствует ни одному из языков. См. http://www.boulder.nist.gov/timefreq/general/misc.htm#Anchor-14550. За дополнительной информацией об UTC можно обратиться к документу U.S. Naval Observatory What is Universal Time?, расположенному по адресу: httpV/aa.usno.navy.mll/feq/docs/UT.html, а также к документу от National Insti- Institute of Standards and Technology, который вы найдете по адресу: http://phy- slcs.nlst.gov/GenInt/Tlme/world.html.
276 Глава 10 • Дата и время Объявление переменных типа TIMESTAMP Синтаксис объявления переменных типа TIMESTAMP более сложен, чем переменных типа DATE. Большинство имен типов данных семейства TIMESTAMP состоят из не- нескольких слов, к чему поначалу трудно привыкнуть. Кроме того, эти типы дан- данных позволяют задать количество десятичных знаков, используемое для пред- представления долей секунды. Вот синтаксис их объявлений: TIMESTAMP {{точность)] TIMESTAMP {{точность)] WITH TIME ZONE TIMESTAMP [{точность)] WITH LOCAL TIME ZONE Значение параметра точность определяет количество десятичных цифр, выде- выделенное для хранения значений долей секунды. По умолчанию оно равно 6, что оз- означает отслеживание времени с точностью до 0,000001 с. Допускаются значения точности от 0 до 9. ПРИМЕЧАНИЕ- Поведение переменной типа TTMESTAMP(O) идентично поведению переменной типа данных DATE, В каких случаях используются переменные типа TIMESTAMP Переменные типа TIMESTAMP используются в основном для отслеживания времени с точностью до долей секунды и для оперирования значениями времени в разных часовых поясах. Все разновидности типа данных TIMESTAMP поддерживают доли секунды, так что если для вашей работы этого достаточно, используйте тип TIMESTAMP, а если необходима информация о часовом поясе — тип TIMESTAMP WITH LOCAL TIME ZONE или TIMESTAMP WITH TIME ZONE. Информация о часовом поясе хранится в виде данных типа TIMESTAMP WITH TIME ZONE как часть значения даты-времени. Этот тип данных используется в тех слу- случаях, когда важно знать исходный часовой пояс, например при работе с данными, поступающими из источников, которые находятся в разных часовых поясах. ПРИМЕЧАНИЕ- Вследствие некоторых специфических обстоятельств, в частности из-за перехода на зимнее/летнее время, значение типа TIMESTAMP WITH TIME ZONE может интерпретироваться по-разному. Подроб- Подробнее об этом рассказывается в разделе «Преобразование типов данных DATE и TIMESTAMP». Если важно знать правильное время, но не важно, в каком часовом поясе оно было зафиксировано, можно пользоваться типом данных TIMESTAMP WITH LOCAL TIME ZONE. Значения этого типа автоматически приводятся в соответствие с текущим часовым поясом. Однако следует обязательно учесть тот факт, что в трехуровне- трехуровневых системах текущим часовым поясом легко может стать часовой пояс сервера web-приложения, а не пользователя, просматривающего данные в web-браузере. Тип данных TIMESTAMP WITH LOCAL TIME ZONE применяется при переносе данных из столбцов типа DATE старой базы данных в столбцы типа TIMESTAMP новой базы. Непосредственно перенести эти значения в столбец типа TIMESTAMP WITH TIME ZONE нельзя, поскольку тип DATE не содержит информации о часовом поясе. Но если вы
Типы данных даты и времени 277 хотите интерпретировать все значения типа DATE как относящиеся к текущему ча- часовому поясу, их можно представить как тип данных TIMESTAMP WITH LOCAL TIME ZONE. После этого значения даты-времени в результирующем столбце будут авто- автоматически преобразованы для достижения соответствия между часовым поясом базы данных и часовым поясом сеанса пользователя. Подобное преобразование в большей степени относится к сфере администрирования базы данных, нежели программирования, но знать о нем необходимо. Типы данных INTERVAL Типы данных INTERVAL введены только в Oracle9f. Чтобы получить более полное представление о них, вспомним, с какими еще значениями даты-времени мы мо- можем сталкиваться в повседневной жизни. О Момент — временная точка, определенная с некоторой точностью. Например, когда мы собираемся встать утром в заданное время, это время представляет момент. Он может быть определен с точностью до часа или же до минуты. Все типы данных DATE и TIMESTAMP позволяют задавать моменты времени. О Интервал — это понятие, которое относится не к конкретной временной точ- точке, а к определенному количеству времени. В повседневной жизни мы посто- постоянно имеем дело с временными интервалами. Например, мы работаем по во- восемь часов в день (надеемся), обедаем в течение часа (в мечтах!) и т. д. Два новых типа данных INTERVAL, введенных в Oracle9f, позволяют нам представ- представлять интервалы времени. О Период — это интервал, который начинается и заканчивается в заданные мо- моменты времени. Например, если вы встали в 8:00 и работали в течение восьми часов, 8-часовой интервал времени, начинающийся в 8:00, можно считать пе- периодом. В Oracle нет специализированного типа данных для непосредствен- непосредственной поддержки периодов, как нет его и в стандарте ANSI/ISO. В Oracle9i введены два новых типа данных INTERVAL, соответствующих стан- стандарту ANSI/ISO: О INTERVAL YEAR TO MONTH — позволяет определить интервал времени в годах и ме- месяцах; О INTERVAL DAY TO SECOND — позволяет определить интервал времени в днях, часах, минутах и секундах (включая доли секунды). Объявление переменных типа INTERVAL По сравнению с другими объявлениями переменных PL/SQL синтаксис объяв- объявлений переменных обоих типов INTERVAL необычен. Помимо того, что имена этих двух типов состоят из нескольких слов, для них задается не одно, а два значения, определяющих точность соответствующих параметров: Ш_перененнай INTERVAL YEAR [(.точность_пет)} ТО MONTH Или иня_перенеиной INTERVAL DAY Цточность^цней)] ТО SECOND 1{точность_ролвй_свкунаы)] Здесь имя_перемениой - это имя объявляемой переменной типа INTERVAL; точность_ пет - количество цифр (от 0 до 4), выделенное для представления количества лет
278 Глава 10 • Дата и время (по умолчанию 2); точность_дней — количество цифр (от 0 до 9), выделенное для представления количества дней (по умолчанию 2); точность_долей_секунды — ко- количество цифр (от 0 до 9), выделенное для представления количества долей се- секунды (по умолчанию 6). О точности значений интервалов, как правило, можно не беспокоиться. Значе- Значения типа INTERVAL YEAR TO MONTH всегда нормализуются таким образом, что количе- количество месяцев лежит в диапазоне от 0 до 11. Фактически Oracle не позволяет за- задать месяц значением больше 11; интервал в 1 год и 13 месяцев должен быть выражен как 2 года и 1 месяц. Значение параметра точность_лет устанавливает максимальный размер интервала типа INTERVAL YEAR TO MONTH. Точно так же значе- значение параметра точность_дней устанавливает максимальный размер интервала типа INTERVAL DAY TO SECOND. Точность для часов, минут и секунд значения типа INTERVAL DAY TO SECOND не нужно задавать по той же причине, по которой не задается точность для месяцев значения типа INTERVAL YEAR TO MONTH. Интервалы INTERVAL DAY TO SECOND всегда нор- нормализуются таким образом, что значения часов, минут и секунд находятся в есте- естественных диапазонах: 0-23 ч, 0-59 мин и 0-59 с (за исключением долей секунды). Доли секунды указываются потому, что значения типа INTERVAL DAY TO SECOND могут определять интервалы с указанной точностью до долей секунды. Значения типа INTERVAL YEAR TO MONTH не могут содержать долей месяца, и последние для них не задаются. В каких случаях используются типы данных INTERVAL Типы данных INTERVAL относительно новы для Oracle, поэтому поговорим об их применении, воспользовавшись несколькими примерами. Надеемся разбудить ва- ваши творческие способности и подтолкнуть к размышлениям о том, как эти типы данных могли бы использоваться в создаваемых вами системах. Представьте себе, что вы работаете на консервном заводе. На каждой банке консервов должен быть проставлен срок реализации, и ваша задача — написать программу для вычисления этой даты для консервов, производимых в данный момент. Это простейшая задача, требующая использования интервала. Пища в банке будет съедобна в течение определенного интервала времени, и дата ее реа- реализации соответствует концу этого интервала. Поскольку консервы хранятся дол- долго, мы будем исчислять интервалы в годах и месяцах. Задача усложняется тем, что каждый тип консервов имеет свой срок хране- хранения. Например, консервированные овощи хранятся дольше, чем мясо, а пища для домашних животных — дольше, чем пища для людей. Вы создаете таблицу, содер- содержащую информацию о сроке хранения каждого типа продуктов. Ее объявление может быть таким: CREATE TABLE good_for ( productjd NUMBER, good_for_per1od INTERVAL YEARB) TO MONTH NOT NULL ); Далее вы решаете, что вам нужна функция PL/SQL, возвращающая дату реа- реализации продукта, которая определяется на основе срока его хранения. Эта функ- функция может быть такой: CREATE OR REPLACE FUNCTION expirationjate ( good_for_periodjn IN shelf_life.good_for_period*TYPE
Типы данных даты и времени 279 ) RETURN DATE IS BEGIN -- Обрезаем текущую пату, и получэем -- только дату, без времени. RETURN TRUNC (SVSOATE) + good_for_per1od_ln; END; Если бы мы использовали два числовых столбца для года и месяцев вместо од- одного столбца типа INTERVAL, вычисления были бы сложнее: exp1rati on_date :- ADD_MONTHS(TRUNC(SYSDATE), (good_for^years_in * 1Z) + good_for_months_in ): Использование интервала вместо двух числовых значений имеет ряд преиму- преимуществ. Во-первых, программный код получается более простым, а значит, отладка его проще и уменьшается вероятность появления ошибок. Во-вторых, он становит- становится более читабельным и понятным. В-третьих, использование типа данных INTER- INTERVAL YEAR TO MONTH в таблице good_for гарантирует, что в ней будут храниться толь- только допустимые интервалы (например, интервал в 1 год и 24 месяца не приемлем). Кроме того, тип данных INTERVAL удобно применять в тех случаях, когда нужно определить разность между двумя датами. Рассмотрим следующий пример, в ко- котором вычисляется срок работы сотрудника: DECLARE start_date DATE; end_date DATE: servicejinterval INTERVAL YEAR TO MONTH; years_of_service NUMBER; months_of_service NUMBER; BEGIN -- Обычно мы извлекаем начальную и конечную даты из базы данных startjate :- T0_DATE('29-DEC-19B8'. 'dd-mon-yyyy'); end_date := TOJATE C26-DEC-1995'. 'dd-mon-yyyy1): -- Определяем и выводим количество проработанных лет и месяцев: servicejnterval :- (end_date - startjlate) YEAR TO MONTH; DBMS_OlJTPUT.PUT_LINE(serviceJnterval): -- Используем новую функцию EXTRACT, для того чтобы -- выделить отдельные компоненты - год и месяц years_of_service ;- EXTRACT(YEAR FROM serviceinterval): months_of service :- EXTRACTCMONTH FROM servicejnterval); DBMS_OUTPUT.PLfT_LINE(years_of_service || ' лет и ' II months of service II ' месяцев '}; END; Приведем строку, выполняющую реальные вычисления для получения коли- количества лет и месяцев работы: servicejnterval :•= (end_date - start_date) YEAR TO MONTH; Здесь VEAR TO MONTH — это часть синтаксиса выражения, возвращающего интервал. Подробнее о нем рассказывается далее в этой главе. Как видите, вычисление ин- интервала сводится к простому вычитанию одной даты из другой. Если бы мы не
280 Глава 10 • Дата и время использовали тип данных INTERVAL, нам бы пришлось написать что-то вроде сле- следующего: nranths_of_service :- ROUND(months_between(end_date. start_date)): years of service :- TRUNC(irranths_of_service/12); months_of_service := M0D(months_of_service,12); И снова решение, в котором не используется тип данных INTERVAL, оказывается сложнее как для программирования, так и с точки зрения понимания написанно- написанного кода. ПРИМЕЧАНИЕ Тип данных INTERVAL YEAR TO MONTH выполняет округление значений, и вы должны знать о воз- возможных последствиях этой операции. Данный вопрос подробно рассматривается ниже. ДЛЯ ЧЕГО НУЖНЫ ДВА ТИПА ДАННЫХ INTERVAL Поначалу мы удивились тому, что Oracle ввела сразу два типа данных INTERVAL. Решение обрабатывать годы и месяцы отдельно от дней, часов, минут и секунд показалось нам довольно странным. Почему бы не создать единый тип данных INTERVAL, покрывающий все возможности? Однако ока- оказалось, что за это нам нужно благодарить римского императора Юлия Це- Цезаря (создателя юлианского календаря), определившего продолжитель- продолжительность большинства месяцев. Два типа данных с разделительной линией на уровне месяцев пришлось определить потому, что месяц — это единственный компонент даты-вре- даты-времени, продолжительность которого может быть разной. Подумайте об ин- интервале в 1 месяц и 30 дней. Какую продолжительность он имеет на самом деле? Это меньше двух месяцев? В точности два месяца? Или, может быть, больше? Если этим месяцем является январь, тогда через 30 дней уже бу- будет март и получится интервал в 61 день. Если же этим месяцем является февраль, тогда интервал равен либо 59, либо 60 дням. Ну а если месяц — апрель, тогда интервал получится в 60 дней. Поэтому вместо того чтобы возиться со всеми этими сложностями, обу- обусловленными разной продолжительностью месяцев и возникающими при сравнении интервалов, выполнении математических операций с датами, а также при нормализации значений даты-времени, стандарт ANSI «раз- «разбивает» маску даты-времени на две части: в одной — год и месяц, во вто- второй — все остальное. За дополнительной информацией по этому вопросу обратитесь к книге A Guide to SQL Standard (автор CJ. Date, издательство Addison-Wesley, 1993). Преобразование типов данных DATE и TIMESTAMP Теперь, когда вы имеете представление о типах данных Oracle, предназначенных для работы с датой и временем, давайте посмотрим, как присваивать даты значе- значениям переменных этих типов и как такие значения впоследствии использовать.
Преобразование типов данных DATE и TIMESTAMP 281 Дату и время в понятной для человека форме можно представить в виде символь- символьных строк наподобие «8 марта 2003 года» и «10:30». Так что речь пойдет о преоб- преобразовании значений даты-времени — приведении символьных строк в соответст- соответствие с внутренним представлением Oracle и наоборот. PL/SQL проверяет и хранит даты от 1 января 4712 до н. э. до 31 декабря 9999 года н. э. Для просмотра диапазона дат в вашей версии Oracle запустите сценарий showdaterange.sql, имеющийся на web-узле издательства O'Reilly.) Если ввести дату без времени (во многих приложениях не требуется отслеживать время, так что PL/SQL позволяет его не задавать), то время в результирующем значении по умолчанию будет равно полуночи @0:00). * Oracle способна интерпретировать практически любой из известных форма- форматов даты и времени. Эта возможность основана на использовании маски форма- форматирования даты, которая представляет собой строку специальных символов, оп- определяющих формат даты для Oracle. Поэтому мы сначала рассмотрим маски форматирования, а затем проследим, как они используются при работе с пере- переменными PL/SQL. Маски форматирования даты и времени В версии Огас1е6 и более ранних версиях РСУБД по умолчанию дата задавалась в формате DD-MON-YY, наводившем ужас на пользователей и разработчиков. Хотя этот формат распространен во многих странах мира, в Соединенных Штатах им практически никто не пользуется. Начиная с Oracle7 администраторы базы данных могут сами задавать исполь- используемый по умолчанию формат (который вступает в силу при инициализации или запуске РСУБД). Этот формат определяется значением параметра NLS_DATE_FOR- МАТ, например: NLS_DATE_FORMAT - 'MM/DD/YYYY' Кроме того, используемый по умолчанию формат даты неявно задается с по- помощью другого инициализационного параметра, а именно NLS_TERRITORY. Указы- Указывая значение параметра NLSTERRITORY, вы устанавливаете соглашения о формате даты, языке даты, числовых форматах, символах валюты и первом дне недели. Формат даты можно задать также на уровне сеанса, что удобно делать, когда ваши потребности отличаются от потребностей большинства пользователей базы данных. Для этого применяется оператор ALTER SESSION. Следующий код работает в Oracle8i и более поздних версиях и устанавливает по умолчанию формат даты MM/DD/YYYY: BEGIN EXECUTE IMMEDIATE 'ALTER SESSION SET NLS DATE FORMAT = "MM/DD/YYYY'''¦ END; ~ " Для того чтобы узнать, какой формат даты по умолчанию используется в ва- вашем сеансе, нужно выполнить запрос к представлению NLS_SESSION_PARAMETERS: SELECT value FROM nls_session_parameters WHERE parameter- 'NLS DATE FORMAT1:
282 Глава 10 • Дата и время Из разделов «Преобразование строки в дату» и «Преобразование даты в стро- строку» вы узнаете, как задавать элементы маски при форматировании даты для от- отдельных преобразований, не ограничиваясь элементами, используемыми по умол- умолчанию. Как видите, при преобразовании даты и символьных данных очень важную роль играет маска их форматирования (такая как MMDDYY или Month DO, YYYY). Мас- Маска форматирования даты состоит из нескольких элементов. Например, элемента- элементами маски MM/DD/YYYY являются MM, DD, YYYY и две косые черты. В табл. 10.1 приведен полный список элементов форматирования даты и вре- времени и указано, как они используются. Элементы форматирования дат можно за- задавать в любых сочетаниях и в любом порядке. В параметре NLS_DATE_FORMAT один и тот же элемент нельзя задать дважды; например, можно использовать только один из элементов MONTH, MON или ММ, поскольку все они обозначают месяц. ПРИМЕЧАНИЕ- В более ранних версиях Oracle разрешалось дважды задавать один и тот же элемент даты (в маске «Моп (MM) DD, YYYY» дважды указан месяц). Однако начиная с Огас1е91 каждый элемент формати- форматирования используется только один раз. Некоторые из приведенных в табл. 10.1 элементов форматирования применя- применяются только для преобразования значении даты-времени внутреннего формата Oracle в символьные строки, но не наоборот. Такие элементы нельзя включать в используемую по умолчанию маску форматирования (то есть задавать в параметре NLS_DATE_FORMAT), поскольку эта маска применяется для преобразования в обоих на- направлениях. Указанные элементы помечены в таблице как «Только вывод». Таблица 10.1. Элементы маски форматирования даты и времени Элемент Описание SCC или СС Столетие. Если используется формат SCC, даты, произошедшие в период до н, э. предваряются знаком минус (-). Только вывод SYYYY или YYYY Год представляется четырьмя цифрами. Если используется формат SYYYY, даты до н. э. предваряются знаком минус (-) IYYY Год представляется четырьмя цифрами; соответствует стандарту ISO. Только вывод YYY, или YY, Последние три, две или одна цифра года. Если эти элементы используются при или Y преобразовании символьной строки з дату, по умолчанию принимается, что речь идет о текущем столетии IYY, или IY, Последние три, две или одна цифра года; соответствует стандарту ISO. или I Только вывод Y,YYY Год представляется четырьмя цифрами с запятой SYEAR, YEAR Год, представленный символьной строкой (то есть «two thousand two»). SYear, Year, Префикс S помещает перед датами до н. э. знак минус (-). Может быть записан syear или year в верхнем, нижнем или смешанном регистре. Только вывод RR Последние две цифры года. Этот формат используется для вывода года, не относящегося к текущему столетию RRRR Год представляется четырьмя цифрами ВС или AD Индикатор ВС (до н. э.) или AD (н. э.) без точек
Преобразование типов данных DATE и TIMESTAMP 283 Элемент Описание B.C. или A.D. Индикатор B.C. (до н. э.) или A.D. (н. э.) сточками Е Сокращенное название эры. Используется в японском, тайско-буддистском и некоторых других календарях. Только вывод ЕЕ Полное название эры q ' Квартал года, от 1-го до 4-го. Период январь-март считается первым кварталом, апрель-июнь — вторым и т. д. Только вывод ММ Номер месяца в году, от 01-го до 12-го. Январь представляется значением 01, сентябрь — 09 и т. п. RM Представление номера месяца римскими цифрами, от I до XII. Январь является месяцем I, сентябрь — ГХ и т. д. MONTH, Month Название месяца в символах верхнего, смешанного или нижнего регистра или month MON, Моп или Сокращенное название месяца, такое, как JAN для January. Может быть топ представлено символами верхнего, смешанного или нижнего регистра WW Неделя года, от 1-й до 53-й. Только вывод IW Неделя года, от 1-й до 52-й или от 1-й до 53-й; соответствует стандарту ISO. Только вывод W Неделя месяца, от 1-й до 5-й. Неделя 1 начинается с первого дня месяца и заканчивается седьмым. Только вывод DDD День года, от 1-го до 366-го DD День месяца, от 1-го до 31-го D День недели, от 1-го до 7-го. Номер дня недели зависит от того, какой день неявно определен как первый значением параметра базы данных NLSJTERRITORY DAY, Day или Название дня недели, представленное в символах верхнего, смешанного day или нижнего регистра DY, Dy или dy Сокращенное название дня недели, такое, как TUE для Tuesday, в верхнем, смешанном или нижнем регистре J Формат для отображения дня в юлианском календаре (количество дней i с 1 января 4712 года до н. э. — самой ранней даты, поддерживаемой РСУБД Orade) AM или РМ Индикатор времени до или после полудня без точек А.М. или P.M. Индикатор времени до или после полудня с точками TZD Аббревиатура названия часового пояса, например EST, PST и т. д. Этот формат используется только при вводе, вначале он может показаться непривычным TZH Смещение времени часового пояса в часах. Например, -5 соответствует часовому поясу, в котором время на пять часов меньше UTC ТОМ Смещение времени часового пояса в минутах. Например, -5:30 соответствует часовому поясу, в котором время на пять часов и тридцать минут меньше UTC. Таких часовых поясов немного TZR Регион действия часового пояса. Например, «US/Eastern» — это регион, для которого действуют время EST и EDT (см. табл. 10.8) продолжение^
284 Глава 10 • Дата и время Таблица 10.1 (продолжение) Элемент Описание НН или НН12 Часы, от 1 до 12. Формат НН12 используется только для вывода НН24 Часы, от 0 до 23 Ml Минуты в значении даты-времени, от 0 до 59 SS Секунды в значении даты-времени, от 0 до 59 SSSSS Количество секунд после полуночи в компоненте времени. Значения лежат в диапазоне от 0 до 86399, и каждый час состоит из 3600 с FF Доли секунды. Допустим только для значений типа T1MESTAMP. Независимо от количества десятичных цифр, которые вы хотите видеть или использовать, для формата задаются две буквы F. Другое их количество недопустимо X Локальный символ десятичного разделителя. В американском английском это точка (.). Данный элемент помещается перед FF, чтобы доли секунды интерпретировались и представлялись правильно ТН Суффикс, преобразующий число для представления его в виде порядкового числительного (для английского языка). Например, 4 превращается в 4th, а 1 — в lth. Этот элемент может стоять после любого элемента, представляющего число. Например, формат «DDth-Mon-YYYY» продуцирует выходное значение вида «15th-Nov-1961». Возвращаемое значение всегда дается на английском языке, независимо от языка даты SP Суффикс, преобразующий число в полное текстовое представление на английском языке. Этот элемент может стоять после любого элемента, обозначающего число. Например, формат «DDth-Mon-Yyyysp» продуцирует выходное значение вида «15tn-Nov-One Thousand Nine Hundred Sixty-One». Возвращаемое значение всегда дается на английском языке, независимо от языка даты. (Обратите внимание, что формат Yyyy соответствует смешанному регистру символов) SPTH Суффикс, преобразующий число для его полного текстового представления на английском языке. Например, 4 возвращается как FOURTH, a 1 как FIRST. Этот элемент может стоять после любого элемента, обозначающего число. Например, формат «Ddspth Mon, Yyyysp» продуцирует выходное значение вида «Fifteenth Nov, One Thousand Nine Hundred Sixty-One», Возвращаемое значение всегда дается на английском языке, независимо от языка даты. (Обратите внимание, что формат Yyyy допускает смешанный регистр символов) FX Элемент, требующий точного соответствия между данными и маской форматирования. (FX означает Format eXact — точный формат.) См. далее в этой главе раздел «Элемент форматирования FX» FM Элемент, который служит для удаления пробелов в результирующей строке. (FM означает Fill Mode — режим заполнения.) См. раздел «Элемент форматирования FM» далее в этой главе Другой текст Любые знаки препинания, в том числе запятая (,), косая черта (/) или дефис (-) будут воспроизведены в результирующей строке. В ней также будет воспроизведен текст, если включить его в маску форматирования, взяв в двойные кавычки (" "). Примеры использования этого элемента приводятся ниже В тех случаях, когда в результате форматирования даты генерируется ее тек- текстовое представление, а не цифровое (например, MONTH, MON, DAY, 0Y, AM и РМ), язык, используемый для написания данных слов, определяется параметрами NLS —
Преобразование типов данных DATE и "ПМЕ5ТАМР 285 NLS DATEJ.ANGUAGE и NLS_LANGUAGE - или необязательным аргументом языка даты, который можно передавать функциям TO_CHAR и TO_DATE. Приведем несколько примеров масок форматирования дат, составленных из приведенных выше элементов форматирования: ¦Month DD. YYYY1 ¦MM/DD/YY Day A.M.' ¦Year Month Qay HH24-.MI-.SS1 ¦J1 ¦SSSSS-YYYY-MM-DD1 ¦"A beautiful summer morning on the" DDth" day of "Month1 Дополнительные примеры использования различных элементов форматиро- форматирования вы найдете в описаниях функций TOCHAR и TODATE. ДАТЫ ISO Элементы IYY и IW в соответствии со стандартом ISO (International Orga- Organization for Standardization — Международная организация по стандарти- стандартизации) представляют год и неделю. В календаре, разработанном данной организацией, первый день года - всегда понедельник и определяется он с учетом следующих правил: Q когда 1 января приходится на попедельник, год начинается с этого же дня; Q когда 1 января приходится на один из дней со вторника по четверг, год начинается с предшествующего понедельника; ? когда 1 января приходится на один из дней с пятницы по воскресенье, год начинается со следующего понедельника. Следование этим правилам часто приводит к нелепым ситуациям. Напри- Например, 31 декабря 2001 года считается первым днем 2002 года в соответст- соответствии со стандартом ISO, и если вывести эту дату в формате IYYY, получится 31-DEC-2002. Недели ISO всегда начинаются с понедельника и нумеруются начиная с первого понедельника года ISO. Преобразование строк в даты Первым вопросом, с которым вы столкнетесь при работе с датами, будет принцип присвоения даты (или значения времени) значению переменной PL/SQL. Эта операция выполняется путем преобразования значения даты-времени для дости- достижения соответствия его символьного представления внутреннему формату Oracle. Такое преобразование можно выполнить неявно, присвоив символьной строке значение переменной типа даты-времени, или явно, с помощью встроенных функ- функций Oracle. Неявное преобразование — вещь рискованная, и мы не рекомендуем на него полагаться. Приведем пример неявного преобразования символьной строки при записи ее в переменную типа DATE: DECLARE birthday DATE;
286 Глава 10 • Дата и время BEGIN birthday :- '15-Nov-1961': END; Возможность такого преобразования зависит от установленного значения па- параметра NLS_DATE_FORMAT и преобразование будет выполняться до тех пор, пока ад- администратор базы данных не решит изменить это значение. Как только он это сделает, код перестанет функционировать. Нарушит работу кода и изменение зна- значения параметра NLS_DATE_FORMAT на уровне сеанса. Вместо того чтобы полагаться на неявные преобразования и установленное значение параметра NLS_OATE_FORMAT, лучше преобразовывать даты явно, с помо- помощью встроенных функций Oracle. Эти функции не только делают очевидным вы- выполняемое преобразование, но и позволяют задать используемый формат даты- времени. В следующих разделах описываются встроенные функции преобразования да- даты-времени. Отдельный раздел посвящен новой возможности Oracle9i - работе с литеральными значениями даты-времени. Литерал, задающий дату и время, — это стандартный текстовый формат ANSI/ISO. С его помощью можно также пре- преобразовать текст в значение типа DATE или TIMESTAMP. Функция TO_DATE Функция ТО_ОАТЕ преобразует символьную строку в значение типа данных DATE. Она является перегруженной — поддерживает как строковые, так и числовые вход- входные значения, синтаксис функции TODATE следующий: FUNCTION TO_OATE (вхопная_строка IN VARCHAR2 [. наска_форматироваиия IN VARCHAR2 [. язык_п1б IN VARCHAR2]] ) RETURN DATE FUNCTION TOJATE [вхоцное_число IN NUMBER [. маска^форматирования IN VARCHAR2 [. S3biK_nls IN VARCHAR2]] ) RETURN DATE Вторая версия этой функции может использоваться только с маской формати- форматирования J для юлианского календаря. Дата юлианского календаря представляется количеством дней, прошедших с 1 января 4712 года до н. э. И только такая дата может передаваться функции TO_DATE в числовом виде. Во всех остальных случаях аргументами функции TODATE могут являться: входная_строка - строковая переменная, литерал, именованная константа или вы- выражение, подлежащее преобразованию; маска_форматирования — маска форматиро- форматирования, которую функция TO_DATE будет использовать для преобразования строки (по умолчанию она равна значению параметра NLS_DATE_FORMAT); язык_пЪ - необя- необязательный параметр, который задает язык, используемый для интерпретации имен и сокращенных названий месяцев и дней недели в строке. Формат этого парамет- параметра таков: 'NLS DATE LANGUAGE-»3W'
Преобразование типов данных DATE и TIMESTAMP 287 Здесь язык — это язык, распознаваемый вашей базой данных. Обычно перечень поддерживаемых языков приводится в руководстве по установке Oracle. В следующем примере строка 123188 преобразуется в дату: ТО_ОАТЕС123188', 'MMDDYY') С помощью параметра язын_п!5 указано, что название месяца задано на испан- испанском языке: TODATECAbril 12 1991', 'Month 00 YYYY'. 'NLS_DATE_LANGUAGE-Spanish') Ошибки Oracle от ORA-01BD0 до ORA-01899 связаны с внутренними функциями обработки даты РСУБД и могут генерироваться при неудачном преобразовании даты. Прочитав описание этих ошибок, вы узнаете много нового о нюансах пра- правил преобразования даты. Перечислим некоторые из них. О Задающий дату литерал, переданный для преобразования функции TO_DATE, не может быть длиннее 220 символов. О Нельзя в одну маску форматирования включать и элемент, обозначающий дату юлианского календаря (J), и день года (DDD). О Включать в маску несколько элементов, представляющих один и тот же компо- компонент значения даты-времени, не разрешается. Например, маска форматирова- форматирования YYYY-YYY-DD-MM недопустима, поскольку в ней используются два представле- представления года: YYYY и YYY. О Нельзя в одной маске использовать 24-часовой формат времени (РР24) и эле- элемент, обозначающий время до или после полудня (АМ/РМ). Семейство функций TO_TIMESTAMP Существует три типа данных семейства TIMESTAMP, и с каждым из них связана оп- определенная функция преобразования символьной строки в значение типа TIME- STAMP: О TO_TIMESTAMP - преобразует символьную строку в значение типа TIMESTAMP; О TQ_TIMESTAMP_TZ - преобразует символьную строку в значение типа TIMESTAMP WITH TIME ZONE; О TO_JIMESTAMP_LTZ — преобразует символьную строку в значение типа TIMESTAMP WITH LOCAL TIME ZONE. Синтаксис этих трех функций однотипен. В отличие от функции Т0_0АТЕ, ни одна из функций TO_TIMESTAMP не поддерживает входные значения типа NUMBER. Приведем синтаксис функции TO_TIMESTAMP: FUNCTION TO TIMESTAMP (входная Ърокд IN VARCHAR2 [. песка форматирования IN VARCHAR2 [. язык_п1б IN VARCHAR2]] RETURN TIMESTAMP Ее аргументы очень похожи на аргументы функции TO_DATE: входная_строка - это символьная строка, представляющая значение даты-времени; маска_формати- Р°вания — строка элементов из табл. 10.1, определяющая формат входной строки.
288 Глава 10 • Дата и время По умолчанию маска форматирования равна значению одного из двух парамет- параметров NLS: О NLSJIMESTAMPJORMAT - для функций TOJIMESTAMP и TO_TIMESTAMP_LTZ; О NLS_TIMESTAMP_TZ_FORMAT - для функции TO_TIMESTAMP_TZ. Необязательный параметр asuKjils задает язык, используемый для интерпре- интерпретации имен и сокращенных названий месяцев и дней недели в строке. С семейством функций TOJIMESTAMP также используются элементы формати- форматирования, описанные в табл. 10.1. Например, следующие вызовы TOTIMESTAMP пре- преобразуют символьные строки в значения типа TIMESTAMP: -- Установки паранетроа NLS соответствуют принятым а США DECLARE a TIMESTAMP: b TIMESTAMP: BEGIN a :- TO_TIMESTAMP('24-Feb-2002 09.00.00.50 PM1): b :- TO_TIMESTAMP('02/24/2002 09.00.00.50 PM1. 'uri/dcl/yyyy hh:mi ;ssxff AM'); DBMS_OUTPUT.PUT_LINECa): DBMS_OUTPUT.PUT_LINECb); END; Вот результат выполнения этого кода: 24-Feb-2002 09.00.00.500000 PM 24-Feb-2002 09.00.00.500000 PM Обратите внимание на то, как представлены доли секунды (. 50), и на исполь- использование маски XFF. Элемент форматирования X определяет местоположение деся- десятичного разделителя (в данном случае точки), отделяющего количество целых секунд от дробной части. Можно было бы просто поставить в этом месте точку (. FF), но мы предпочли воспользоваться символом X, поскольку он указывает что на этом месте должен находиться разделитель, соответствующий установке пара- параметра NLSJERRITORY. Функции TO_TIMESTAMP_TZ и TO_TIMESTAMP_LTZ также могут преобразовывать сим- символьные строки, содержащие информацию о часовом поясе. О том, как с ними ра- работать, рассказывается в следующем разделе. Часовые пояса Возможное наличие информации о часовом поясе в функциях TO_TIMESTAMP_TZ и TO_TIMESTAMP_LTZ несколько усложняет их использование по сравнению с базо- базовой функцией TO_TIMESTAMP. Информация о часовом поясе задается одним из сле- следующих способов. О Как положительное или отрицательное значение смещения времени в часах и минутах относительно UTC (иногда называемого GMT или временем по Гринвичу). Например, значение -5:00 эквивалентно Восточному стандартно- стандартному времени США (U.S. Eastern Standard Time). Значение смещения должно находиться в диапазоне от -12:59 до +13:59. О В виде названия часового пояса, например: US/Eastern, US/Pacific и т. д.
Преобразование типов данных DATE и TIMESTAMP 289 О В виде сочетания названия и аббревиатуры часового пояса, например: US/Eas- US/Eastern EDT. Рассмотрим несколько примеров. Начнем с наиболее простого из них, в кото- котором часовой пояс вообще не указан: TOJIMESTAMPJZC'123198 083015.50', 'MMDOYY HHMISS.FF'); Дата и время здесь определены как 31 декабря 1998 года, 8 часов 30 минут 15 с половиной секунд до полудня. Поскольку часовой пояс не указан, Oracle счита- считает, что время относится к текущему часовому поясу. Каждый сеанс Oracle связан с конкретным часовым поясом, который можно определить с помощью следующе- следующего запроса: SQL» SELECT SESSIONTIMEZONE FROM DUAL: SESSIONTIMEZONE -05:00. Значение -05:00 указывает, что время часового пояса сеанса на пять часов нуль минут меньше UTC. Эта разница соответствует Восточному стандартному време- времени в Соединенных Штатах, а разница +10:00, например, — Восточному стандарт- стандартному времени в Австралии. Далее представим часовой пояс с использованием смещения в часах и мину- минутах относительно UTC. Обратите внимание на обозначения TZH и TZM, отмечаю- отмечающие, где во входной строке указано смещение времени, выраженное в часах и ми- минутах: TO_TIMESTAMP_TZ('123188 083015.50-05:00', 'MMDDYY HHMISS.FF TZH:TZM'); Здесь значение даты-времени интерпретируется как Восточное стандартное время США (независимо от часового пояса сеанса). Следующий пример показывает, как задать часовой пояс, используя имя ре- региона. В нем задается регион America/Detroit, что эквивалентно Восточному вре- времени в Соединенных Штатах. Обозначение TZR в маске форматирования указыва- указывает местоположение имени региона во входной строке (в параметре NLS_TERRITORY задана установка для США): TOJIMESTAMPJZC27-Oct-2002 01:30:00.00 America/Detroit'. 'dd-Mon-yyyy hh:mi:ssxff TZR'): Этот пример интересен тем, что он представляет не Восточное стандартное время, а просто Восточное время. Разница между ними заключается в том, что термин Восточное время обозначает либо Восточное стандартное время, либо Во- Восточное летнее время - в зависимости от того, выполнен ли переход на летнее время. А он может быть выполнен! Мы составили этот пример таким образом, чтобы он действительно был неоднозначным. 27 октября 2002 года - это дата, ко- когда заканчивается действие Восточного дневного времени и время 2:00 превраща- превращается в 1:00. Поэтому в указанный день 1:30 наступает дважды: первый раз по Вос- Восточному летнему времени, а второй раз по Восточному стандартному времени. Так какое же время мы имеем в виду, когда говорим, что сейчас 1:30 27 октября 2002 года? Названия региона не достаточно для различения стандартного и летнего вре- времени. Чтобы устранить эту неоднозначность, необходима спецификация часового
290 Глава 10 • Дата и время пояса, как в следующем примере. Обратите внимание, что в маску форматирова- форматирования добавлен элемент TZD: ТО TIMESTAMP_T7C'27-Oct-Z002 01:30:00.00 America/Detroit EOT'. 'dd-Mon-yyyy hh:mi:ssxff TZR TZD1); Нам нужно задать смещение часового пояса в часах и минутах (например, -5:00) или использовать сочетание названия и аббревиатуры часового пояса. Если указать только имя региона, то при наличии неоднозначности в определении вре- времени (летнее или стандартное) Oracle будет считать, что задано стандартное время. ПРИМЕЧАНИЕ Если значение параметра ERROR_ON_OVERLAP_TIME сеанса равно TRUE, Oracle выдает ошибку ка- каждый раз, когда вы задаете неоднозначное время. Полный список поддерживаемых Oracle названий регионов и часовых поясов содержит представление V$TIMEZONE_NAMES. Доступ к нему может получить любой пользователь базы данных. Анализируя информацию этого представления, обра- обратите внимание на то, что сокращения часовых поясов не уникальны (см. следую- следующую врезку). К ВОПРОСУ О СТАНДАРТЕ ЧАСОВЫХ ПОЯСОВ Часовые пояса настолько важны, что естественным было бы сделать пред- предположение о наличии международного стандарта, определяющего их на- названия и аббревиатуры. Тем не менее такого стандарта нет. Названия часо- часовых поясов и их аббревиатуры не только не стандартизированы, но неко- некоторые из них даже повторяются. Например, аббревиатура EST обозначает Восточное стандартное время и в США и в Австралии, однако оно соот- соответствует двум часовым поясам с разным смещением времени. Вот поче- почему функция TO_TIMESTAMP не позволяет задавать часовой пояс лишь с помо- помощью аббревиатуры его названия. Поскольку стандарта часовых поясов не существует, у вас может возник- возникнуть вопрос об источнике информации, содержащейся в представлении V$TIMEZONE_NAMES. Она получена по адресу: ftp://elsle.ncl.nlh.gov/pub. Если вы заинтересуетесь этой темой, обратите внимание на файл tzdata_2002c.tar.gz. Литералы типа DATE и TZMESTAMP Литералы типа DATE и TIMESTAMP являются частью стандарта ANSI SQL и введены только в Огас1е9:\ Они представляют еще одну возможность записи значений в переменные типа даты-времени. Литерал типа DATE состоит из ключевого слова DATE, за которым следует дата: DATE 'YYYY-MM-DD' Литерал типа TIMESTAMP состоит из ключевого слова TIMESTAMP, за которым сле- следуют дата и время: TIMESTAMP 'YYYY-MM-DD HH;M1:SS[.FFFFFFFFF] [[+|-)HH:MI]' Обозначение FFFFFFFFF представляет доли секунды и не является обязатель- обязательным. Такое представление содержит от одной до девяти цифр. Смещение часового
Преобразование типов данных DATE и TIMESTAMP 291 пояса (+НН:М1) может содержать знак плюс или минус, оно не является обяза- обязательным. Часы всегда задаются в 24-часовом формате. ПРИМЕЧАНИЕ- Еспи в литерале типа TIMESTAMP опустить смещение часового пояса, по умолчанию будет исполь- использоваться часовой пояс текущего сеанса. В следующем коде PL/SQL содержится несколько допустимых литералов типа DATE я TIMESTAMP: DECLARE a TIMESTAMP WITH TIME ZONE; b TIMESTAMP WITH TIME ZONE; с TIMESTAMP WITH TIME ZONE; d TIMESTAMP WITH TIME ZONE; e DATE: BEGIN -- Две цифры для волей секунды а :- HMESTAMP '2002-02-19 11:52.00.00 -05:00'; -- 9 цифр для долей секунды. 24-часовой формат, 14:00 - 2:00 пополудни о .- TIMESTAMP- '2002-02-19 14:00:00.000000000 -5:00'; -- Без долей секунды с .-TIMESTAMP '2002-02-19 13:52:00 -5:00': ¦- Без часового пояса, по умолчание используется часовой пояс текущего сеанса d :- TIMESTAMP '2002-02-19 13:52:00': -- Литерал типа DATE е :- DATE '2002-02-19': END: Формат литералов типа TIMESTAMP определяется стандартом ANSI/ISO, изме- изменить его не можете ни вы, ни администратор базы данных. Поэтому их использо- использование уместно везде, где нужно литерально задавать в программном коде значе- значения даты-времени. Применяя литерал типа TIMESTAMP, часовой пояс можно задать посредством име- имени региона его действия, Например, следующий блок показывает, что Тихоокеан- Тихоокеанское стандартное время США 10:52 (смещение -8:00) эквивалентно Восточному стандартному времени США 13:52: DECLARE a TIMESTAMP WITH TIME ZONE: b TIMESTAMP WITH TIME ZONE; BEGIN a :- TIMESTAMP '2002-02-19 10:52:00 -S'00'; о :- TIMESTAMP '2002-02-19 13:52:00 EST1; IF a - b THEN dbms output,put UneCa - b'); END IF;~ ~ END; Вот результат выполнения этого кода; а- ь
292 Глава 10 • Дата и время Вас могло смутить, что в данном примере используется обозначение EST, но оно иллюстрирует важный момент: запрос к представлению VSTIMEZONENAMES воз- возвращает обозначение EST и как название региона (столбец TZNAME), и как его аббре- аббревиатуру (столбец TZABBREV). В качестве названия региона обозначение EST может использоваться для задания часового пояса в литерале типа TIMESTAMP. Очевидно, что в этом примере оно интерпретируется как Восточное стандартное время США. Элемент форматирования FX В PL/SQL элемент FX (точный формат) используется в качестве модификатора маски форматирования. Он означает, что символьный аргумент в вызове функ- функции TODATE должен точно соответствовать маске форматирования. Если элемент FX не задан, функция TO_DATE не требует точного соответствия символьной строки формату. Она допускает следующие послабления. О Лишние пробелы в символьной строке игнорируются. Ни в какой части значе- значения даты-времени пробелы не являются значащими данными. Единственное их назначение - разделять части даты и времени: TO_DATE C'Jan 15 1994'. 'MON DD YYYY¦) О Такие числовые значения, как номер дня или года, не обязательно должны со- содержать ведущие нули, чтобы соответствовать маске. Если числа располагают- располагаются в строке в определенном месте (обычно они выделяются в строке с помощью разделительных символов), функция TO_DATE правильно преобразует числовые значения: TO_DATE Cl-1-41. 'DD-M-YYYY') TO_DATE ('7/16/94'. 'MM/DO/YY'} О Местоположение знаков препинания в преобразуемой строке должно соответ- соответствовать местоположению знаков препинания в маске форматирования . В сле- следующем примере в маске форматирования используются разделители в виде дефисов (-), а в исходной строке — в виде символов вставки (л). Но даже не- несмотря на это, у функции ТО_ОАТЕ не возникло затруднений: TODATE CJANUARY~r94'. 'Month-dd-yy') Такая гибкость — замечательное свойство функции, если вы, конечно, не хотите ограничить пользователя вводом данных только в стандартном формате. В про- противном случае используется модификатор FX, благодаря чему между маской фор- форматирования и строкой соблюдается строгое соответствие. Элемент FX не допускает гибкости в интерпретации строки. Строка не может содержать дополнительных пробелов, если их нет в маске форматирования. Чи- Числовые значения должны содержать ведущие нули, чтобы их длина в точности была равна длине соответствующих элементов маски. Все знаки препинания и литеральные значения должны соответствовать знакам препинания и тексту в ка- кавычках в маске форматирования (за исключением регистра, который всегда игно- игнорируется). Во всех следующих примерах PL/SQL генерирует одну из приведен- приведенных ниже ошибок: TO_DATE Cl-1-4'. 'fxDD-MM-YYYY') TO_DATE ('7/16/94', TXMM/DD/YY') TO_DATE ('JANUARY*!* the year of 94', 'FXMonth-dd-"WhatIsaynotdo"yy')
Преобразование типов данных DATE и 7TMESTAMP 293 ORA-01B61: literal does not match format string DRA-01B62: the numeric value does not match the length of the format item Модификатор FX может быть задан в верхнем, нижнем и смешанном регистре. Результат будет одинаковым. Так как он является переключателем, то может за- задаваться в маске форматирования несколько раз. Каждое его появление меняет установку на противоположную: первый модификатор FX требует точного соот- соответствия для всех следующих за ним элементов, второй — указывает, что для сле- следующих за ним элементов точное соответствие не требуется, и т. д. По умолча- умолчанию, то есть когда модификатор FX вообще отсутствует, точное соответствие не требуется ни для какой части строки. В приведенном ниже примере задано три модификатора FX. В результате точ- точное соответствие требуется для номера дня и года, но не для номера месяца: TO_DATE C'07-1-19941. 'FXDD-FXMM-FXYYYY') Следующая попытка преобразования даты вызовет ошибку ORA-01862, посколь- поскольку номер года задан не полностью: TD_DATE С'07-1-94'. 'FXDD-FXMM-FXYYYY') -- Неверный формат строки! Для дополнения строки пробелами или нулями в вызове функции TO_DATE, со- согласно требованиям маски форматирования, можно использовать модификатор FM. Иными словами, модификатор FM обеспечивает требуемое модификатором FX точное соответствие формату. Далее в этой главе, в разделе, посвященном эле- элементу FM, вы узнаете, как этот же модификатор удаляет ведущие нули и пробелы из строки, возвращаемой функцией TO_CHAR. Теперь вызов функции TO_DATE возвращает дату, поскольку модификатор fm в начале маски форматирования включает режим заполнения для всей строки, изменяя 1 на 01 и 94 на 1994: TO_DATE C07-1-941. 'FXfmDD-FXMM-FXYYYY') В одну строку форматирования можно вводить по нескольку экземпляров мо- модификаторов FM и FX, включая и отключая для разных частей строки соответст- соответствующие им режимы. Элемент форматирования RR После недавней смены тысячелетий все чаще используется четырехзначный фор- формат года, поскольку стала очевидной неоднозначность привычного двухзначного формата. Например, что означает 1 января 45 года — начало 1945 года или 2045 го- года? Однако не все желают менять свои привычки, так что вам, вполне вероятно, придется позволить пользователям вводить даты с двухзначным обозначением года. Использование такого обозначения может вызывать проблемы при определе- определении столетия и тысячелетия. Элемент форматирования YY всегда означает теку- текущее столетие, поэтому если пользователь в ноябре 1999 года вводит 1/1/1 или 1- Jan-1, то считается, что речь идет о 1 января 1901 года, а не о 1 января 2001 года. Какой же выход у менеджеров информационных систем? Одно из решений заключается в изменении логики приложений таким образом, чтобы при вводе пользователем номера года меньше 10 (или другого значения, которое вы посчи- посчитаете подходящим) данный год считался относящимся к следующему столетию. Это удастся осуществить, но сколько труда придется затратить?
294 • Глава 10 • Дата и время К счастью, в Oracle предусмотрен элемент форматирования, с помощью кото- которого данная проблема легко решается. Это элемент RR, позволяющий до 2000 года вводить даты XXI столетия, а после 2000 года - XX столетия. ПРИМЕЧАНИЕ- В приведенном ниже описании работы элемента RR столетие определяется твким образом: XX сто- столетие составляет период i90D-1999 годы, а XXI — период 2000-2099 годы. Мы понимаем, что эта определение не правильное, но так проще объяснить поведение указанного элемента. Когда текущий год относится к первой половине столетия (значения от О до 49), тогда при вводе даты первой половины столетия элемент RR возвращает текущее столетие, а если вводится дата второй половины столетия (значения от 50 до 99), элемент RR возвращает предыдущее столетие. И наоборот, если текущий год относится ко второй половине столетия (значе- (значения от 50 до 99), при вводе даты первой половины столетия элемент RR возвраща- возвращает следующее столетие, а при вводе даты второй половины столетия — текущее столетие. Не понятно? Нам тоже пришлось призадуматься, когда мы прочитали это впер- впервые. Правила работы элемента RR призваны помочь определить, какое столетие имел в виду пользователь, вводя две цифры, обозначающие год. Вот несколько примеров, Обратите внимание, что в 2002 году для 88-го года и 18-го года функ- функция TD_CHAR возвращает даты, относящиеся к XX и XXI столетиям соответственно: SELECT TO_CHAR (SYSDATE, ¦fflVDD/YYYY) "Current Date". TO_CHAR (TO_DATE C14-0CT-8B', "DD-MON-RR1J. 'YYYY') "Year 88", TO_CHAR (TOJJATE ('14-0CT-1B1, "DD-MON-RR1 J, 'YYYY1) "Year 18" FROM dual; Current Date Year 8B Year 18 02/25/2002 198B 2018 Когда наступит 2050 год, RR будет интерпретировать те же даты иначе: SELECT TO_CHAR (SYSDATE, 'HH/DD/YYYY') "Current Date". TO_CHAR (TO_DATE С10/14/88", 1HM/DD/RR'), 'YYYY1) "Year 88', TO_CHAR (TO_DATE ('10/14/18', 'HH/DO/RR'), 'YVYY') TEAR IB" FROM dual; Current Date Year 88 Year 18 02/25/2050 2088 2118 Конечно, если вы, используя элемент RR сейчас, захотите ввести дату второй половины XXI столетия (то есть захотите, чтобы 75-й год интерпретировался как 2075), вам придется соответственно дополнить программный код. До наступле- наступления 2050 года маски с элементом RR будут интерпретировать такие двухзначные номера года как относящиеся к XX столетию. Точно так же вам потребуется спе- специально дополнить программу, если нужно будет вводить номера годов 00-49, относящиеся к началу XX столетия, поскольку элемент RR распознает их как при- принадлежащие XXI столетию. Существует несколько способов активизации логики элемента RR в уже суще- существующих приложениях. Простейший из них заключается в том, чтобы просто
Преобразование типов данных DATE и TIMESTAMP 295 изменить используемую по умолчанию маску форматирования для даты в базе данных. Для этого нужно следующим образом изменить иниииализационный па- параметр NLS_DATE_FORMAT: NLS_DATE_FORMAT - 'MM/DD/RR' или NLS_DATE_FORMAT - 'DD-MON-RR' в зависимости от того, каким был предыдущий формат. И если только в ваших программах и отчетах нет жестко закодированных масок форматирования, счи- считайте, что дело сделано. Перезапустите базу данных - и приложение позволит пользователям вводить даты, относящиеся к XXI столетию. Если маски форма- форматирования заданы где-либо в свойствах элементов Oracle Forms или Oracle Re- Reports, вам придется изменить эти модули. Преобразование даты в строку Присвоить переменкой значение типа даты-времени - только полдела. Еще нуж- нужно прочитать его и представить в понятной для человека форме. Для этого Oracle предлагает воспользоваться функцией TO_CHAR. Функция TO_CHAR Предназначена для преобразования значения даты-времени в строку переменной длины. Эта функция применяется и для типа данных DATE, и для типов семейства TIMESTAMP. Кроме того, она выполняет преобразование чисел в символьные строки (см. главу 9). Синтаксис функции TO_CHAR следующий: FUNCTION TOJHAR lexoBHanjars IN DATE [. наска_форматирования IN VARCHAR2 [. язь/Kjils IN I/ARCHAR2]] ) RETURN VARCHAR2 Здесь входнди_датд — символьная строка, представляющая значение даты-време- даты-времени; маска_форматирования - строка элементов из числа перечисленных в табл. 10.1, определяющая формат выходной строки; язык_п1з - строка, задающая язык ото- отображения даты. Параметры маска_форнатирования и язык_п/5 не являются обяза- обязательными. ПРИМЕЧАНИЕ Для того чтобы результирующая строка была представлена символами национального набора, вме- вместо функции TO_CHAR следует воспользоваться функцией TO_NCHAR. Но имейте в виду, что в этом случае маска форматирования тоже должна быть задана в символах национального набора. В про- противном случае будет сгенерирована ошибка ORA-01821: date format not recognized. Если маска_форматирования не задана, по умолчанию используется формат даты, Установленный для базы данных. Это формат DD-MON-RR, если только администра- администратор базы данных не задал другой формат, изменив значение параметра NLS_DA- TE_FQRMAT. С помощью команды ALTER SESSION можно также изменить используе- используемый по умолчанию формат для текущего сеанса : ALTER SESSION SET NLS DATE FORMAT - 'DD/MM/YYYY';
296 Глава 10 • Дата и время Возможно, у вас не будет задан параметр язь1к_гIз; в таком случае по умолча- умолчанию используется язык, установленный для базы данных. Он задается либо пара- параметром NLSJ.ANGUAGE, либо параметром NLSDATELANGUAGE. Приведем несколько примеров применения функции для преобразования дат ТО CHAR. Примеры предваряются краткими пояснениями, определяющими особен- особенности использования функции. О Обратите внимание на то, что между месяцем и днем стоят два пробела, а пе- перед номером месяца — ведущий нуль: TO_CHAR CSYSDATE. 'Month DD. YYYY1) -> 'February 05. 1994' О Для подавления пробелов и нулей используется элемент FM: TO_CHAR (SYSDATE. 'FMMonth DO. YYYY') -> 'February 5. 1994' О Обратите внимание на различие регистра в обозначении месяца в следующих двух примерах: (Возможности форматирования в Oracle действительно заме- замечательны!) TO_CHAR (SYSDATE. 'MON DDth. YYYY1) -* 'FEB 05TH. 1994' TO_CHAR (SYSDATE. 'FMMon DDth. YYYY') -» 'Feb ЪТЛ. 1994' На элемент форматирования ТН не распространяются правила задания регист- регистра. Даже если вы наберете его в нижнем регистре (th) в выходную строку Oracle запишет ТН. О Для текущей даты, представленной порядковым номером дня года, выводится номер месяца и номер недели: TO_CHAR (SYSDATE. 'DDD DD D1) -> '036 05 7' TO_CHAR (SYSDATE. 'frnDDD DD D') -> '36 05 7' О А вот как интересно можно отформатировать отчет: TO_CHAR (SYSDATE. '"In month "RM" of year "YEAR') -> 'In month II of year NINETEEN NINETY FOUR' О Для переменных типа TIMESTAMP можно задавать время с точностью до милли- миллисекунд: TO_CHAR CATIMESTAMP. 'YYYY-MM-DD HH:MI:SS.FF AM TZH:TZM') -> значение типа '2002-02-19 01:52:00.123457000 PM -05:00' Работа со значениями времени, включающими доли секунды, требует особого внимания. В маске форматирования доли секунды представляются элементом FT. Возможно, вы захотите задать такое количество букв F, которое соответствует желаемому количеству десятичных цифр в выходной строке. Но у вас ничего не получится. В следующем примере мы пытаемся с помощью спецификации FFFFF указать, что нам нужно пять цифр после десятичного разделителя: DECLARE A TIMESTAMP WITH TIME ZONE' BEGIN A -TIMESTAMP '2002-02-19 13:52:00.123456789 -5:00'; DBMSJXJTPUT.PUT LINECTO CHARta.'YYYY-MM-DD HH:MI:SS.FFFFF AM TZH:TZM'J); END;
Преобразование типов данных DATE и TIMESTAMP 297 Результат будет таким: DECLARE ERROR at line 1: ORA-01821: date format not recognized ORA-06512: at line 5 Если вам нужно получить пять цифр после десятичного разделителя, объяви- объявите переменную как TIMESTAMPC5). Например: DECLARE A TIMESTAMPC5) WITH TIME ZONE: BEGIN A :- TIMESTAMP '2002-02-19 13:52:00.123456789 -5:00'; DBMS_OUTPUT.PUT_LINECTO_CHAR(a,'YYYY-MM-DO HH:MI:SS.FF AM TZH:TZM'M: END: Результат выполнения этого кода следующий: 2002-02-19 01:52:00.123460000 РМ -05:00 PL/SQL procedure successfully completed. Обратите внимание на полученное значение. Количество секунд на самом деле равняется 00,123456789. Однако мго значение округлено (не усечено, а именно ок- округлено) до 00,12346. ПРИМЕЧАНИЕ- К сожалению, Oracle жестко привязывает количество выводимых десятичных цифр к точности зна- значения переменной типа TIMESTAMP. Мы же полагаем, что эти два параметра не должны зависеть друг от друга. PL/SQL таким образом обрабатывает числа, и было бы логично, если бы он так же об- обрабатывал значения типа TIMESTAMP, Ничего не стоит по ошибке задать неверный формат даты, и с появлением ти- типов данных TIMESTAMP вероятность этого даже увеличилась. Некоторые элементы форматирования, использующиеся со значениями типа TIMESTAMP, не могут при- применяться со значениями типа DATE. Например, если попытаться преобразовать зна- значения типа DATE в символьную строку с помощью элементов FF, TZH и TZM, получится следующее: DECLARE A DATE: BEGIN А := SYSDATE; DBMS OUTPUT.PUT LINECTO CHAR(A.'YYYY-HM-DD HH:HI:SS.FF AM TZH-.TZM'))- END: ~ " Результат будет таким: A :- SYSDATE: ERROR at line 4: ORA-01821: date format not recognized ORA-06512: at line 5 Это сообщение об ошибке (формат даты не распознается) может сбить вас с толку. На самом деле проблема состоит в том, что формат даты применяется не
298 Глава 10 • Дата и время к тому типу данных. Поэтому если получите такое сообщение, проверьте не толь- только формат даты, но и тип данных, который вы собираетесь преобразовывать. Преобразование данных о часовых поясах в символьные строки Необходимость учитывать часовые пояса усложняет процесс преобразования зна- значений даты-времени в символьные строки. Информация о часовом поясе состоит из следующих элементов: смещение относительно UTC в часах и минутах, назва- название и аббревиатура часового пояса. Все они хранятся в переменной TIMESTAMP WITH TIME ZONE раздельно. При этом смещение указывается всегда, а возможность вывода названия и аббревиатуры часового пояса зависит от того, была ли эта информа- информация задана при установке значения переменной. Рассмотрим следующий пример: DECLARE A TIMESTAMP WITH TIME ZONE; В TIMESTAMP WITH TIME ZONE; С TIMESTAMP WITH TIME ZONE; ' BEGIN A :- TO TIMESTAMP TZ('2002-06-18 13:52:00.123456789 -5:00', ¦YYYY-MM-DO HHZ4:MI:SS.FF T2H:TZM'): В :- TOJIMESTAMP TZt'2002-06-18 13:52:00.123456789 US/Eastern', ¦YYYY-MM-DD HH24-MI:SS.FF TZR'); С :- TO TIMESTAMP TZ('2002-06-18 13:52:00.123456789 US/Eastern EDT1, 'YYYY-MM-DD HH24:MI;SS.FF TZR TZD1); DBMS_OUTPUT. PUT_LINE(TO_CHAR(A, 'YYYY-MM-DD HH:MI:SS.FF AM TZH:TZM TZR TZD')); DBMS_OUTPUT.PUT LINECTO CHARCB. 'YYYY-MM-DD HH:MI:SS7FF AM TZH:TZM TZR TZD')); DBMS_OUTPUT.PUT_LINECTO CHARCC, 'YYYY-MM-DD HH:MI:SS~FF AM TZH:TZM TZR TZD1)): END; Результат выполнения этого кода таков: 2002-06-18 01:52:00.123457000 РМ -05:00 -05:00 2002-06-18 01:52:00.123457000 РМ -04:00 US/EASTERN EDT 2002-06-18 01:52:00.123457000 РМ -04:00 US/EASTERN EDT Обратите внимание на следующее. О Для переменной А часовой пояс задан как смещение в часах относительно UTC. Поэтому при выводе значения этой переменной отображается только смещение. О Поскольку название региона для переменной А не задано, Oracle возвращает смещение для часового пояса. Это лучше, чем вообще отсутствие какай-либо информации. О Для переменной В задано название региона действия часового пояса. Для внут- внутреннего представления Oracle преобразует это название в смещение относи- относительно UTC, но название региона тоже сохраняет. Поэтому выводится и на- название региона, и смещение. О Для переменной В Oracle правильно распознала действующее в июне летнее время. В результате значение В было дополнено аббревиатурой EDT.
Преобразование типов данных DATE и TIMESTAMP 299 О Для переменной С заданы название и аббревиатура часового пояса, так что при выводе этой переменной отображаются оба значения. Смещение относительно UTC связано с регионами действия часовых поясов по принципу «один-ко-многим*; для определения региона знать только лишь сме- смещение недостаточно. Поэтому если имя региона не задано в исходном значении, Oracle не может его вывести. Элемент форматирования FM PL/SQL поддерживает элемент форматирования FM, используемый как модифи- модификатор маски форматирования. Этот элемент обеспечивает подавление дополняю- дополняющих пробелов и ведущих нулей, возвращаемых функцией TO_CHAR. По умолчанию функция TO_CHAR с маской форматирования генерирует и пробе- пробелы, и ведущие нули (между именем месяца и номером дня в нашем примере пять пробелов): TO_CHAR (SYSDATE. 'Month DD. YYYY1) -> 'April 05. 1994' Но если поместить в начало маски форматирования модификатор FM, то и лиш- лишние пробелы, и ведущие кули исчезнут: TCKHAR (SYSDATE, 'FMMonth DO, YYYY') -» 'April 5, 1994' Модификатор можно задавать в верхнем, нижнем, а также смешанном регист- регистре; на результат это никак не влияет. Модификатор FM работает как переключатель и может использоваться в маске форматирования неоднократно, каждый раз меняя свое действие на противопо- противоположное. По умолчанию, то есть если модификатор FM вообще отсутствует в маске форматирования, пробелы не подавляются, и ведущие нули включаются в ре- результирующее значение. Поэтому первое появление модификатора FM в маске ука- указывает, что для всех следующих элементов пробелы и ведущие нули подавляются. Второе его появление отключает подавление пробелов и ведущих нулей, третье — опять включает и т. д. В следующем примере дополняющие пробелы в конце имени месяца подавля- подавляются, но в результате второго включения модификатора FM сохраняется ведущий ноль в номере дня: TO_CHAR (SYSOATE, 'FMMonth fmOD. YYYY1) -> 'АргП 05. 1994' Если модификатор FM в маске отсутствует, преобразованная дата всегда допол- дополняется пробелами до фиксированной длины, зависящей от других элементов фор- форматирования. При использовании FM длина возвращаемой строки связана с фак- фактическим значением даты-времени и может быть разной. Модификатор FM может также применяться для заполнения исходной строки пробелами и нулями в соответствии с маской форматирования функции TO_DATE. Об этом рассказывалось ранее, в разделе, посвященном модификатору FX. Преобразование интервалов В отличие от значений даты-времени, представляющих конкретные моменты вре- времени, интервалы представляют промежутки времени. Их можно задавать в тек- текстовом виде, причем разными способами.
300 Глава 10 • Дата и время Интервал состоит из одного или нескольких элементов даты-времени. Напри- Например, его можно выразить в годах и месяцах. В табл. 10.2 перечислены стандартные имена каждого из этих элементов. Они используются в функциях преобразова- преобразования типов данных и в выражениях, о чем подробно рассказывается в следующих разделах. Таблица 10.2. Имена элементов интервала Имя Описание YEAR Некоторое количество лет в диапазоне от 1 до 999 999 999 MONTH Некоторое количество месяцев в диапазоне от 0 до И DAY Некоторое количество дней в диапазоне от 0 до 999 999 999 HOUR Некоторое количество часов в диапазоне от 0 до 23 MINUTE Некоторое количество минут в диапазоне от 0 до 59 SECOND Некоторое количество секунд в диапазоне от 0 до 59,999 999 999 При использовании в функциях преобразования типов имена элементов ин- интервала не чувствительны к регистру. Например, элементы YEAR, Year и year экви- эквивалентны. Семейство функций NUMTO Функции NUMTOYMINTERVAL и NUMTODSINTERVAL предназначены для преобразования от- отдельных числовых значений в значения типа INTERVAL. В вызове функции задают- задаются числовое значение и один из элементов интервала, перечисленных в табл. 10.2. В результате числовое значение преобразуется в этот элемент интервала. Функция NUMTOYMINTERVAL преобразует числовое значение в интервал типа INTER- INTERVAL YEAR TO MONTH, а функция NUMTODSINTERVAL - в интервал типа INTERVAL DAY TO SECOND. В следующем примере функция NUMTOYMINTERVAL используется для преобразо- преобразования значения 10.5 в значение типа INTERVAL YEAR TO MONTH. Второй аргумент этой функции — строка Year — указывает, что заданное в первом аргументе число пред- представляет количество лег: DECLARE A INTERVAL YEAR TO MONTH; BEGIN А ;¦= NUMTOYMINTERVAL A0.5. 'Year'): DBMSJXJTPUT.PUT LINE(A); END; Результат выполнения данного кода таков: +10-06 Как видите, исходное значение 10.5 преобразовано в значение, которое соот- соответствует 10 годам и 6 месяцам. Любое дробное количество лет преобразуется в эквивалентное количество месяцев, и результат округляется до целого. Напри- Например, если исходное значение равно 10,9 годам, оно будет преобразовано в 10 лет и 10 месяцев.
Преобразование типов данных DATE и TIMESTAMP 301 В следующем примере показано, как числовое значение преобразуется в ин- интервал типа INTERVAL DAY TO SECOND: DECLARE A INTERVAL DAY TO SECOND: BEGIN A :- NUMTODSINTERVAL A440, 'Minute'); DBMS_OUTPUT.PUT_LINE(A); END; В качестве результата получаем такое значение: +D1 00:00:00.000000 PL/SQL procedure successfully completed. Как видите, Oracle автоматически преобразовала входное значение 1400 ми- минут в интервал равный 1 дню. Это удобно, поскольку вам не придется заниматься такой работой самостоятельно. С помощью функций NUMTO ничего не стоит вывести любое количество минут (или секунд, или дней, или часов) в читабельном виде. Функции TO_xxINTERVAL Если функции NUMTO подходят для преобразования в интервалы числовых значе- значений, то для преобразования символьных строк можно использовать функции TO_YMINTERVAL и TO_DSINTERVAL. Выбор функции зависит от того, значение какого типа вы хотите получить - INTERVAL YEAR TO MONTH или INTERVAL DAY TO SECOND. Функция TO_YMINTERVAL преобразует символьную строку в значение типа INTER- INTERVAL YEAR TO MONTH. Вызывается она так: TO_YMINTERVALCK-W) Здесь аргумент Y представляет количество лет, а М — количество месяцев. Обяза- Обязательно должны быть заданы оба значения, разделенные дефисом. Точно так же функция TO_DSINTERVAL преобразует символьную строку в значе- значение типа INTERVAL DAY TO SECOND. А вот как она вызывается: TOJSINTERVAL CD HH:MI:SS') Здесь D — это количество дней, а аргумент HH:HI:SS представляет часы, минуты и секунды, разделенные символами двоеточия. Следующий пример демонстрирует использование обеих функций: DECLARE A INTERVAL YEAR TO MONTH: В INTERVAL DAY TO SECOND; С INTERVAL DAY TO SECOND' BEGIN TOJMINTERVAL Г40-3'); -- иой возраст TO DSINTERVAL ('10 1:02:10'); С END; TO_DSINTERVAL CIO 1:02:10.123'); -- доли секунды При вызове любой из них нужно задавать полный набор значений. Например, нельзя вызвать функцию TO_YMINTERVAL, указав только год, или вызвать функцию TOj)S INTERVAL, не задав секунды. Можно опустить только доли секунды.
302 Глава 10 • Дата и время Выражения с интервальными значениями Выражения с интервальными значениями используются подобно литералам типа TIMESTAMP и предоставляют еще одну возможность для преобразования символь- символьной строки в значение типа INTERVAL. При этом они обладают большей гибкостью, чем семейство функций ТО_ххINTERVAL. Выражение с интервальным значением имеет такой синтаксис: INTERVAL 'символьное_пре/!стдвление' начальный_эленент ТО конечный_элемент Здесь символьное_представлеиие - это символьная строка, представляющая интер- интервал (описание представления двух типов интервалов в символьной форме дано выше, в разделе «Функции TO_xxINTERVAI>); начальный_элемент задает началь- начальный элемент интервала; конечный_злемент задает конечный элемент интервала. В отличие от функций TOjwINTERVAL выражения с интервальными значениями позволяют задавать интервал с использованием любой последовательности эле- элементов даты-времени (см. табл. 10.2). Указываются только два ограничения: эле- элементы должны быть последовательными и в одном интервале не допускается пе- переход от месяцев к дням. Приведем несколько примеров: DECLARE A INTERVAL YEAR TO MONTH; В INTERVAL YEAR TO MONTH; С INTERVAL DAY TO SECOND; D INTERVAL DAY TO SECOND; BEGIN /* Несколько примеров интервалов YEAR TO MONTH */ A ;- INTERVAL '40-3' YEAR TO MONTH: В ;- INTERVAL 40' YEAR; /* Несколько примеров интервалов DAY TO SECOND */ С ;- INTERVAL '10 1:02:10.123' DAY TO SECOND: /* He работает в 0racle91 Release 1 из-за ошибки */ -- 0 :- INTERVAL '1:02' HOUR TO МГМЯЕ; /* Ниже приводятся два обходных способа задания интервала HOUR TO MINUTE, представляющего сообой только часть интервала DAY TO SECOND */ SELECT INTERVAL '1:02' HOUR TO MINUTE INTO D FROM dual: D :- INTERVAL '1' HOUR + INTERVAL '02' MINUTE: END: ВНИМАНИЕ В Oracle9l Release 1 и Release 2 выражение INTERVAL '1:02' HOUR TO MINUTE можно использовать в SQL-инструкции, но не в операторе PL/SQL. Более того, при выполнении программы будет выдано сообщение об использовании ключевого слова BULK в неверном контексте. Это известная ошибка, которая, по всей вероятности, будет исправлена в следующих выпусках продукта.
Преобразование типов данных DATE и TIMESTAMP 303 Поскольку не существует типа данных для представления интервалов, вклю- включающих и месяцы и дни, любая попытка задать в интервале оба значения приве- приведет к ошибке. Например, ошибку сгенерирует следующее выражение интервала: INTERVAL '1-15' MONTH TO DAY Недопустим также пропуск элементов интервала, Нельзя, скажем, задать ко- количество дней и минут, не указав количество часов. Oracle выполняет нормализацию значений интервалов. В следующем примере 72 час 15 мин преобразуются в значение 3 дня, 0 ч и 15 мин: DECLARE A INTERVAL DAY TO SECOND; BEGIN SELECT INTERVAL '72:15' HOUR TO MINUTE INTO A FROM DUAL; DBMS OUTPUT.PUT LINECA): END: Результат выполнения этого хода таков: +03 00:15:00.000000 Oracle почему-то нормализует только старшие значения (например, часы). По- Попытка задать интервал 72:75 G2 час 75 мин) приведет к ошибке. Форматирование интервалов для вывода До сих пор речь шла о преобразовании интервалов и выводе данных посредством поддерживаемого Oracle механизма неявного преобразования значений. Вы, ве- вероятно, думаете, что для контроля выводимого значения интервала можно вос- воспользоваться функцией TO_CHAR с маской форматирования. Увы, это не так. Для форматирования интервалов функция TO_CHAR применяться не может. Например, выполнение кода DECLARE A INTERVAL YEAR TO MONTH: BEGIN А :- INTERVAL '40-3' YEAR TO MONTH; DBMS OUTPUT.PUT LINEtTO CHARCA. 'YY "лет" и MM "месяца")); END-: приведет к такому результату: +000040-03 Честно говоря, не понятно, чем руководствовались инженеры из Oracle, при- принимая решение о том, что функция T0_CHAR не должна поддерживать интервалы. Если вас не устраивает то, как выполняется неявное преобразование значения интервала в символьную строку, можете воспользоваться функцией EXTRACT (впервые появившейся в Oracle9t): DECLARE A INTERVAL YEAR TO MONTH: BEGIN A :- INTERVAL '40-3' YEAR TO MONTH; DBMSJWTPUT.PUT LINEt EXTRACT(YEAR~FROM A) 11 " лет и ' 11 EXTRACT(MONTH FROM A) || ' иесяца ' END;'
304 Глава 10 • Дата и время Теперь результат будет таким: 40 лет и 3 месяца Функция EXTRACT, конечно, не так удобна, как TO_CHAR, но обладает тем преиму- преимуществом, что обрабатывает интервалы. Функции CAST и EXTRACT Функции CAST и EXTRACT описаны в стандарте ANSI (SQL-92). Их поддержка толь- только недавно включена в Oracle. Функция CAST появилась в Огас1е8 как механизм явного определения типа коллекции, но в Oracle8i ее возможности были расши- расширены. Теперь с помощью функции CAST можно выполнять преобразования между встроенными типами данных, в частности взаимопреобразования значений да- даты-времени и символьных строк. Функция EXTRACT, введенная в Oracle9i, позво- позволяет выделять отдельные компоненты из значений даты-времени и интервалов. Функция CAST Данная функция может использоваться для выполнения следующих операций с датой и временем: О преобразование символьной строки в значение даты-времени; О преобразование значения даты-времени в символьную строку; О преобразование значения одного типа даты-времени (например, DATE) в значе- значение другого типа (например, TIMESTAMP). При использовании функции CAST для преобразования значений даты-време- даты-времени в символьные строки и обратно учитываются значения таких параметров NLS: О NLS_DATE_FORMAT — при преобразовании в тип данных DATE и обратно; О NLS_TIMESTAMP_FORMAT - при преобразовании в типы данных TIMESTAMP и TIME- STAMP WITH LOCAL TIME ZONE и обратно; О- NLS_TIMESTAMP_TZ_FORHAT - при преобразовании в тип данных TIMESTAMP WITH TIME ZONE и обратно. В следующем примере показано, как выполняется каждый тип преобразования: DECLARE a TIMESTAMP WITH TIME ZONE: b VARCHAR2DQ); с TIMESTAMP WITH LOCAL TIME ZONE: BEGIN a :- CAST C24-Feb-2002 09.00.00.00 PM US/Eastern1 AS TIMESTANP WITH TIME ZONE): b :- CAST (a AS VARCHAR2); с :- CAST (a AS TIMESTAMP WITH LOCAL TIME ZONE); D8MS_0UTPUT.PUT_LINE(a): DBMS_OUTPUT.PUT_LINE(b); DBMSJ)UTPUT.PUTLINE(C); END;
Преобразование типов данных DATE и TTMESTAMP ' 305 Результат выполнения этого кода таков: 24-FEB-02 09.00.00.000000 РМ US/EASTERN 24-FEB-02 09.00.00.000000 РМ US/EASTERN 24-FEB-02 09.00.00.000000 РМ Здесь на основе символьной строки генерируется значение типа TIMESTAMP WITH TIME ZONE, потом это значение преобразуется в VARCHAR2, а затем в TIMESTAMP WITH LOCAL TIME ZONE. Учтите, что при выполнении преобразования от типа данных TIMESTAMP WITH TIME ZONE к типу данных TIMESTAMP WITH LOCAL TIME ZONE информация о часовом поясе теряется. ПРИМЕЧАНИЕ В SQL-инструкции при вызове функции CAST можно указать размер значения типа данных, напри- например: CAST(z AS VARCHAR2(<K))). Однако PL/SQL не позволяет задать размер значения целевого типа данных. Функция EXTRACT Является функцией стандарта ANSI, используемой для извлечения компонентов из значения даты-времени или интервала. Вот ее синтаксис: EXTRACT (иня_кошюнента. FROM {цдта_время | интервап)) Здесь иня_конпонента — это имя одного из элементов даты-времени, перечислен- перечисленных в табл. 10.3. Имена компонентов не чувствительны к регистру символов. В аргументе вата_аремя или интервап задается значение даты-времени или интер- интервала. Тип возвращаемого функцией значения зависит от извлекаемого компонента. Таблица 10.3. Имена компонентов даты-времени для использования в функции EXTRACT Имя компонента Возвращаемый тип данных YEAR MONTH DAY HOUR MINUTE SECOND TIMEZONE_HOUR TIMEZONE_MINUTE TIMEZONE_REGION TIMEZONE ABBR NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER VARCHAR2 VARCHAR2 Следующий пример демонстрирует использование функции EXTRACT для про- проверки того, является ли текущий месяц ноябрем: BEGIN IF EXTRACT (MONTH FROM SYSDATE) - 11 THEN DBMS OUTPUT.PUT LINE('Сейчас ноябрь1)¦ ELSE "
306 Глава 10 • Дата и время DBMS OUTPUT.PUT LINE('Сейчас не ноябрь1): END IF;" END: Функцию EXTRACT удобно использовать в тех случаях, когда управление пото- потоком выполнения программы осуществляется на основе значения одного из эле- элементов даты-времени, а также в тех случаях, когда требуется получить числовое значение одного из элементов даты-времени. Однако не рекомендуется использо- использовать ее для преобразования значения даты-времени в символьную строку, как в следующем примере: BEGIN DBMS_OUTPUT.PUT_LINEC'Сегодняшняя дата: ' || EXTRACTCMONTH FROM SYSDATE) || 7' || EXTRACTCDAY FROM SYSDATE) || '/' \\ EXTRACT(YEAR FROM SYSDATE)); END; В результате выполнения кода будет выведена такая строка: Сегодняшняя дата: 2/24/2002 Посмотрите, какая громоздкая конструкция! Функция EXTRACT не предназна- предназначена для преобразования значения даты-времени в символьную строку, посколь- поскольку она возвращает главным образом числовые, а не строковые значения. В этом примере все числа неявно преобразуются в символьные строки. Более того, EXT- EXTRACT не возвращает ни названий дней недели, ни названий месяцев и не делает многого другого, что может делать функция TO_CHAR. Арифметические операции над значениями даты-времени PL/SQL поддерживает арифметические операции над значениями даты-времени, числовыми значениями и значениями интервалов. Например, можно прибавить к дате заданное количество дней или вычесть одну дату из другой, чтобы полу- получить интервал времени между ними. С появлением в Oracle9i типа данных INTER- INTERVAL выполнение арифметических операций над значениями даты-времени суще- существенно усложнилось. Традиционные арифметические операции До выхода Oracle9t при работе с датой можно было выполнять всего два арифме- арифметических действия: О прибавлять числовые значения, представляющие некоторое количество дней, к значению типа ОАТЕ и вычитать их из значения этого типа; О вычитать одно значение типа ОАТЕ из другого для получения количества дней между ними. В Oracle9i эти возможности по-прежнему поддерживаются, но только для типа данных DATE.
Арифметические операции над значениями даты-времени 307 ПРИМЕЧАНИЕ ; Кроме того, для выполнения операций с датами могли (и по-прежнему могут) использоваться функ- функции ADD_MONTHS и MONTHS_BETWEEN, Мы расскажем о них далее в этой главе, в разделе «Функ- «Функции обработки даты-времени». Сложение и вычитание числовых значений При выполнении арифметических операций с датами целочисленные значения представляют дни. Для сдвига даты на один день вперед (в будущее) нужно про- просто добавить к ней 1: h1re_date + 1 Можно прибавить к дате и дробное значение, представляющее часть дня. Наи- Наиболее часто употребляемые дробные значения приведены в табл. 10.4. Таблица 10.4. Дробные значения, используемые для представления даты в арифметических операциях Значение Представляет 1/24 Один час 1/1440 Одна минута 1/86400 Одна секунда В следующем примере к значению переменной starttime прибавляется 8/24, в результате чего время изменяется с полуночи @0:00) на 8:00 AM: DECLARE start time DATE :- DATE 000-09-ОГ: BEGIN DBMS_OUTPUT.PUT LINECTO CHARCstart time.'YYYY-MM-DD HH:MI AM1)): DBMS OUTPUT.PUT~LINE(T0"CHARCstart"t1me+e/24,'YYYY-MM-DD HH:MI AM')): END; Результат выполнения этого кода таков: 2000-09-01 12:00 AM 2000-09-01 08:00 AM Обратите внимание, что мы представили 8 ч как 8/24, а не как 1/3 или деся- десятичный эквивалент этого значения - 0,33333333. Используя значение 8/24, мы тем самым показываем другим программистам, что к дате добавляется 8 ч. И вам советуем при сложении чисел, представляющих элемент значения даты-времени, с датой и вычитании таких чисел из даты всегда использовать знаменатели, ука- указанные в табл. 10.4. Они делают операции с датами более наглядными. Приведем несколько примеров: О для того чтобы прибавить к значению даты-времени 12, используйте не дроб- дробное значение 1/2, а 12/24; О для того чтобы прибавить к значению даты-времени 1 день и 12 ч, используйте выражение 1+12/24 или 36/24, но не значение 1,5; О одну минуту можно задать как 60 с F0/86400) или 1 мин A/1440), в зависи- зависимости от того, в чем, минутах или секундах исчисляется время в вашем прило- приложении.
308 ' Глава 10 • Дата и время Избегайте использования знаменателей, отличных от тех, что представлены в табл. 10.4. Например, скажите, сколько времени представляет дробь 1/48? Вы задумались? А если бы вопрос стоял о значении 30/1440, вы бы сразу поняли, что речь идет о 30 мин. Так вот, если сократить дробь 30/1440, получится 1/48, то есть те же 30 мин. Вы, конечно же, можете оперировать значением 1/48, но мы реко- рекомендуем в программах, которые должны быть понятны тем, кто их читает, исполь- использовать только значение 30/1440. ВНИМАНИЕ При сложении чисел со значениями TIMESTAMP и вычитании их из этих значений производится не- неявное преобразование последних в тип данных DATE. Описанный в этом разделе способ сложения и вычитания числовых значений следует использовать только с типом данных DATE. Следующий пример показыва- показывает, что значения типа TIMESTAMP WITH TIME ZONE в таких операциях неявно преобра- преобразуются в тип данных DATE: DECLARE startjime TIMESTAMP WITH TIME ZONE :- TIMESTAMP '2000-09-01 00:00:00.00 -7:00'; BEGIN DBMS_OUTPUT.PUT_LINE(start_time); DBMS_OUTPUT.PUT_LINE(start_time+8/24); DBMS_0UTPUT.PLfT_LINE(T0_CHARCstart_time+8/24,'DD-M0N-YY HH.MI.SS')): END; Результат выполнения этого кода таков: 01-SEP-00 12.00.00.000000 AM -07:00 Ol-SEP-00 Ol-SEP-00 08.00.00 Во второй и в третьей строках выходных данных к исходному значению вре- времени прибавлено 8. Однако в результате вторая строка оказалась форматирован- форматированной как значение DATE, а не TIMESTAMP — это очевидное следствие неявного преоб- преобразования типов. Третья строка показывает, что значение времени сохранилось, но часовой пояс и доли секунды исчезли. Вычисление разности двух дат В PL/SQL невозможно сложить два значения типа DATE. И в этом есть определенный смысл. Например, что значит сложить 15 ноября 1961 года с 18 июня 1961 года? А вот вычитать значения дат можно, поскольку разностью в этом случае будет конкретное понятие — количество дней между ними. Например: BEGIN DBMS_QUTPUT.PUT_LINE ( TQJATEt'lS-Nov-1961'. 'dd-Mon-уто') - TO_DATE('1B-Jun-I961'. 'dd-Mon-yyyy'1 ): END; Результат таков: 150 Он показывает, что 18 июня 1961 года наступило на 150 дней раньше, чем 15 нояб- ноября 1961 года. С результатами вычитания значений типа DATE нужно обращаться
Арифметические операции над значениями даты-времени 309 внимательно, поскольку если в них присутствует компонент времени, результат получается дробным. Приведем другой пример: BEGIN DBMS_OUTPUT.PUT_LINE ( TO_DATEC'15-Nov-1961 12:01 am1. 'dd-Mon-yyyy hh:mi am1) - T0_DATE('18-Jun-1961 11:59 pm'. 'dd-Mon-yyyy hh:m1 am1) ): END; На этот раз получаем следующее: 149.001388888888888888888888888888888889 Если вас интересует количество календарных дней между двумя датами в этом примере, результат, конечно, неверен. Он верен с точки зрения определения ко- количества 24-часовых периодов между двумя значениями типа DATE, но для боль- большинства людей важно, что эти даты разделяют 150 дней, а не 149,00139 24-часо- 24-часовых периодов. Для получения правильного результата можно применить к каж- каждому из исходных значений функцию TRUNC: BEGIN DBMS_OUTPUT.PUT_LINE t TRUNC(T0_DATE('15-Nov-1961 12:01 am1. 'dd-Mon-yyyy hh:m1 am')) - TRUNCtT0_0ATEC18-Jun-1961 11:59 pm1. 'dd-Mon-yyyy hh:nvi am1)) ): END: И вот результат: 150 Функция TRUNC удаляет из переданного ей значения DATE компонент времени, так что операция вычитания здесь эквивалентна операции вычитания в первом примере этого раздела, и результат представляет количество календарных дней между двумя датами. Это очень полезная функция для работы с датами, поэтому мы изучим ее подробнее далее в этой главе, в разделе «Функции обработки да- даты-времени». ПРИМЕЧАНИЕ- Результат вычитания одного значения типа DATE из другого имеет тип данных NUMBER, а результат вычитания значений типа TIMESTAMP — тип данных INTERVAL DAY TO SECOND. Арифметические действия с интервалами В Oracle9i введен тип данных INTERVAL, поднявший выполнение арифметических операций со значениями даты-времени на очередной уровень сложности. Эта кон- концепция не нова. Если вы выполняли арифметические операции с датами в старых версиях Oracle, вам уже приходилось работать с интервалами. Фигурирующие в этих операциях числа представляют собой не что иное, как интервалы, выра- выраженные в днях и частях дней. Возьмем, к примеру, операцию прибавления к дате одного дня. В Oracle9z эту операцию можно выполнить одним из двух способов: ^ + 1: h1re_date + INTERVAL '1' DAY:
310 Глава 10 • Дата и время Как видите, интервалы уже использовались. Просто теперь эта концепция фор- формализована, и в Oracle9« появились еще три способа измерения интервалов: О с помощью типа данных INTERVAL YEAR TO MONTH - в годах и месяцах; О с помощью типа данных INTERVAL DAY TO SECOND - в днях, часах, минутах и се- секундах; О с помощью числовых значений — в днях и частях дня. Учитывая все сказанное, арифметические действия с интервалами в Oracle9i можно свести к следующим трем типам операций: О сложение интервала со значением даты-времени и вычитание интервала из него; О вычитание одного значения даты-времени из другого для получения интерва- интервала времени между ними; О сложение одного интервала с другим и вычитание одного интервала из другого. Единственное, что здесь действительно ново, так это терминология и типы данных INTERVAL. В версиях до Oracle9i можно было складывать и вычитать ин- интервалы даты - это означало сложение и вычитание чисел, тогда как сейчас, при работе с новым семейством типов TIMESTAMP, можно складывать и вычитать значе- значения типов данных INTERVAL. Сложение и вычитание интервала со значением даты-времени Базовая концепция сложения интервала времени со значением даты-времени и вычитания интервала времени из значения даты-времени в Oracle9t осталась тою же, что и в предыдущих версиях РСУБД. Например, в Oracle8f можно было выполнить такую операцию: hire_date ¦ 1: А в Огас1е9г то же самое можно сделать следующим образом: hire_date + INTERVAL 'I' DAY: В этой записи нет ничего нового, кроме типа данных INTERVAL DAY TO SECOND. Выражение INTERVAL 'Г DAY генерирует значение типа INTERVAL DAY TO SECOND. По- Поскольку задано только количество дней, по умолчанию часы, минуты и секунды устанавливаются в нуль. О том, как составлять подобные выражения с интер- интервальными значениями, подробнее рассказывалось ранее в этой главе. В следующем фрагменте кода содержится еще несколько примеров использо- использования арифметических операций с интервалами в Oracle9z: DECLARE h1re_date TIMESTAMP WITH TIME ZONE; a INTERVAL YEAR TO MONTH; b INTERVAL DAY TO SECOND; BEGIN hire date :- TIMESTAMP '2000-09-01 00:00:00 -5:00': DBMSJ3UTPUT.PUT_LINE(h1re_date): a :- INTERVAL '1-2' YEAR TO MONTH;
Арифметические операции над значениями даты-времени 311 b :- INTERVAL '3 4:5:6.7' DAY TO SECOND: -- Добавляем несколько лет и месяцев hire date :- h1re_date + а: DBMSl0UTPUT.PUT_LINE(h1re_date): -- Добавляем несколько дней, часов, минут и секунд h1re_date :- hire date ¦ Ь; DBMSJJUTPUT. PUT_lTnE(M re_date): END: Результат выполнения этого кода таков: 01-SEP-00 12.00.00.000000 AM -05:00 Ol-NOV-01 12.00.00.000000 AM -05:00 04-NOV-01 04.05.06.700000 AN -05:00 Вычисление интервала между двумя значениями даты-времени Если вы вычисляете интервал между двумя значениями типа DATE, результатом является количество 24-часовых периодов (не то же самое, что количество дней) между ними. Если получено целочисленное значение, значит, разница представ- представляет точное число дней. Если же значение является дробным, то разница включа- включает также некоторое количество часов, минут и секунд. Недостаток такого механизма вычисления интервалов заключался в том, что в результате получались дробные числа, Взгляните на следующий пример, взя- взятый из предыдущего раздела: BEGIN DBMS_OUTPUT.PUT_LINE ( TO_DATE('15-Nov-1961 12:01 am'. 'dd-Моп-уууу hh:mi am') - TD_DATE('18-JLin-1961 11:59 pm1. 'dd-Mon-yyyy hh:mi am') ): END; А вот результат выполнения кода: 149.00138888888В8888888888В8888888888889 Что представлено здесь значением 149, понятно — количество дней, но что та- такое .00138888ВВ8888888888888В88888В88В889? Сколько это часов, например? Конеч- Конечно, можно вычислить представленное этим числом количество часов, минут и се- секунд, если они нужны для дальнейшей работы, но вы можете не получить точных результатов, особенно если имеете дело с бесконечной десятичной дробью. Иное дело Oracle9il Новый тип данных INTERVAL позволяет без труда вычислять интер- интервалы в понятном для человека представлении. Например: DECLARE a INTERVAL DAYC3) ТО SECOND @): BEGIN -- Вычисляем интервал и присваиваем его -- переменной типа INTERVAL DAY TO SECOND а :- ТО TIMESTAMP('15-Nov-1961 12:01 am'. 'dd-Моп-уууу hh:m1 am') - TO_TIMESTAMPC'1B-Jun-I961 11:59 pm'. 'dd-Mon-yyyy hh:m1 an'); ' DBMS_OUTPUT.PUT LINE (a); END:
312 Глава 10 • Дата и время Результат выполнения этого кода таков: +149 00:02:00 В данном примере одна дата вычитается из другой, а разность присваивается переменной типа INTERVAL DAY TO SECOND. Теперь вместо непонятного числа с по- повторяющейся дробной частью мы получили интервал, равный в точности 149 дням, О ч, 2 мин и 0 с. ВНИМАНИЕ Значения типа INTERVAL возвращаются только в результате выполнения операции вычитания зна- значений новых типов данных TIMESTAMP. Как ни странно, PL/SQL не позволяет получить в качестве разности между двумя значениями переменных типа TIMESTAMP значение типа INTERVAL YEAR TO MONTH. DECLARE a TIMESTAMP: b TIMESTAMP: С INTERVAL YEAR TO MONTH; BEGIN a := TO_TIMESTAMP('15-Nov-1961 12:01 am','dd-Mon-yyyy hh:mi am1): Ь := TO_TIMESTAMP('18-3un-1961 11:59 pm1. 'dd-Моп-уш hh:mi am1); с := a - b: с := CASTCa - b AS INTERVAL YEAR TO MONTH): END; Oracle никак это не объясняет - в документации вообще не сказано о том, что такая операция невозможна. После некоторых размышлений мы пришли к сле- следующим выводам. 1. Каждое выражение генерирует результат определенного типа данных. В слу- случае вычитания одного значения типа TIMESTAMP из другого это всегда INTERVAL DAY TO SECOND. 2. Тип результата выражения не зависит от типа переменной, которой он при- присваивается. PL/SQ.L не может изменить семантику выражения левой либо пра- правой части оператора присваивания для достижения соответствия между ними. 3. Когда результат выражения имеет тип, не соответствующий типу присваивае- присваиваемой переменной, PL/SQL выполняет его неявное преобразование в тип пере- переменной. 4. Неявное преобразование значения типа INTERVAL DAY TO SECOND в тип INTERVAL YEAR TO MONTH невозможно, поскольку количество дней в разных месяцах раз- различное и значение типа INTERVAL не может содержать и месяцы, и дни (см. раз- раздел этой главы «Типы данных INTERVAL»). Интервалы могут быть представлены как положительными, так и отрицатель- отрицательными значениями. До сих пор в этой главе речь шла об интервалах, представлен- представленных положительными значениями. В контексте вычитания положительное зна- значение интервала указывает, что вычитаемая дата, наступает раньше той, из кото- которой производилось вычитание. Например: 15-NOV-1961 - 18-0Ш1-1961 = +150
Арифметические операции над значениями даты-времени 313 Отрицательное же значение интервала указывает, что вычитаемая дата, насту- наступает позже той, из которой производилось вычитание: 18-Jun-1961 - 15-NOV-1961 - -150 Таким образом, знак результата указывает на направление действия с датами в пределах интервала. Мы поговорим об этом подробнее в разделе «Сложение и вычитание интервалов». К сожалению, в Oracle нет функции, которая возвра- возвращала бы абсолютное значение интервала подобно тому, как функция ABS возвра- возвращает абсолютное значение числа. Преобразование типа данных DATE в TIMESTAMP В предыдущем разделе отмечалось, что каждое выражение генерирует результат определенного типа данных. Результат вычитания с участием двух значений типа TIMESTAMP имеет тип INTERVAL DAY TO SECOND. Однако для обеспечения совместимо- совместимости с предыдущими версиями Oracle результат вычитания двух дат по-прежнему является числом. Поэтому если вы хотите вычесть одно значение типа DATE из другого и получить значение типа INTERVAL DAY TO SECOND, нужно сначала явно пре- преобразовать с помощью функции CAST исходные значения в тип TIMESTAMP. Например: DECLARE a DATE: b DATE; с INTERVAL DAYC3) ТО SECONDCO): BEGIN a :- T0_DATE('15-Nov-1961 12:01 am1.'dd-Mon-yyyy hh:m1 am1): b :-T0_DATE('18-Jun-1961 11:59 pm','dd-Mon-yyyy hh:m1 am1): с :- CASTCa AS TIMESTAMP) - CAST(b AS TIMESTAMP); DBMS_OUTPUT.PUT_LINE(c); END; Результат выполнения этого кода таков: +149 00:02:00 Если в одной операции вычитания участвуют значения типа DATE и TIMESTAMP, PL/SQL неявно преобразует тип DATE в TIMESTAMP: DECLARE a DATE: b TIMESTAMP; с INTERVAL DAYC) TO SECONDCO); d INTERVAL DAYC) TO SECOND(O); BEGIN a :- TO_DATE('15-Nov-1961 12:01 am','dd-Mon-yyyy hh:m1 am1); b :- T0_TIMESTAMPC'18-Jun-1961 11:59 pm', 'dd-Моп-уууу hh:m1 am'); С :- a - b; d ;- b - a; DBMS_OUTPUT.PUT LINE(c): DBMS_OUTPUT.PUT LINE(d): END;
314 Глава 10 « Дата и время Результат выполнения этого кода таков: +149 00:02:00 -149 00:02:00 Однако при совмещении разных типов значений даты-времени в одном выра- выражении лучше выполнять явное преобразование типов, чтобы сделать данный про- процесс абсолютно определенным как для компилятора, так и для программиста, ко- который будет сопровождать программу, содержащую такие преобразования. Сложение и вычитание интервалов Единственное правило, которого вам следует придерживаться при сложении и вы- вычитании, заключается в том, что оба интервала должны иметь один и тот же тип данных. Например: DECLARE al INTERVAL DAY TO SECOND ;- '2 3:4:5.6'; Ы INTERVAL DAY TO SECOND :- '1 1:1:1.1'; a2 INTERVAL YEAR TO MONTH :- '2-10'; Ь2 INTERVAL YEAR TO MONTH :- 'l-l1: a3 NUMBER :- 3; ЬЗ NUMBER :- 1; BEGIN DBMS_OUTPUT.PLJT_LINE(al - Ы); DBMS_0UTPUT.PUT_LINECa2 - Ь2); DBMS OUTPUT.PUT_LINE(a3 - ЬЗ): END: Результат выполнения кода таков: +000000001 02:03:04oSQ0000000 +000000001-09 2 Данный пример показывает результаты трех операций вычитания. В первых двух случаях вычитались интервалы INTERVAL DAY TO SECOND и INTERVAL YEAR TO MONTH, а в третьем — два числа. Помните, что при работе с типом данных DATE интервал между двумя значениями типа DATE имеет тип NUMBER. Умножение и деление интервалов Операции умножения и деления не применимы к датам, но зато применимы к ин- интервалам: их можно умножать и делить на числа. Приведем несколько примеров: DECLARE al INTERVAL DAY TO SECOND :- '2 3:4:5.6'; a2 INTERVAL YEAR TO MONTH :- '2-10'¦ a3 NUMBER ;- 3; BEGIN -- Умножение интервала DBMS_OUTPUT.PUT LINEtal * 2); DBMS_0UTPUT.PUT~LINE(a2 * !)¦ DBMSJXJTPUT.PUT LINE(a3 * 2);
Арифметические операции над значениями даты-времени 315 -- Деление интервала DBMS OUTPUT.PUT_LINE(al / 2); DBMS~OUTPUT.PUT LINEtaZ / 2); DBMSJ)UTPUT.PUT_LINE<a3 / 2); END: Результат выполнения данного кода таков: +000000004 Об:0В:11.200000000 +000000005-08 6 +000000001 01:32:02.800000000 +000000001-05 1.5 Типы данных INTERVAL без ограничений Интервалы можно объявлять с разным уровнем точности, причем значения раз- разной точности не полностью совместимы между собой. Посмотрите на следующий пример, показывающий, как в результате невнимательного присвоения значений объявленным по-разному переменным могут потеряться цифры, определяющие доли секунды: DECLARE A INTERVAL DAY TO SECOND: С INTERVAL DAYC9) ТО SECONDS); BEGIN А :- '10 0:0:0.123456': С :- А; DBMS_OUTPUT.PUT_LINE(A): DBMS_OUTPUT.PUT_LINE(CJ; END: Результат выполнения кода таков: +10 00:00:00.123456 +000000010 00:00:00.12 Значение, присвоенное переменной А, определяет 0,123456 с, что допустимо, поскольку по умолчанию точность переменной типа INTERVAL DAY TO SECOND состав- составляет шесть цифр после десятичной запятой. Однако когда значение переменной А присваивается переменной С, четыре цифры теряются, поскольку в объявлении переменной С указана меньшая точность. На первый взгляд может показаться, что проблема заключается лишь в объяв- объявлении типов данных, но следует учесть еще один момент - использование проце- процедур и функций, принимающих в качестве параметров значения типа INTERVAL. Об- Обратите внимание на потерю точности в следующем примере, где значение пере- переменной В удваивается с помощью функции double щу interval: DECLARE В INTERVAL DAY(9) TG SECOND»): FUNCTION doublejuyjnterval t A IN INTERVAL DAY TO SECOND) RETURN INTERVAL DAY TO SECOND
316 Глава 10 • Дата и время IS BEGIN RETURN A * 2: END: BEGIN В :- '1 0:0:0.123456789'; DBMS_OUTPUT.PUT_LINE(B); DBMSJWPUT.PUT_LINE(doublejny_interval(B)); END: Результат выполнения этого кода: +ООС000001 00:00:00.123456789 +02 00:00:00.246914 Мы потеряли цифры не только в дробной части секунды, но и в значении ко- количества дней. А если бы переменной В было присвоено значение, равное 100 или более дням, вызов функции double_my_interval вообще завершился бы ошибкой ORA-01873: the leading precision of the interval is too small. Дело здесь в том, что задаваемая по умолчанию точность типов данных INTER- INTERVAL не равна максимально возможной точности. Эта проблема усложняется еще и тем, что PL/SQL не позволяет задавать точность при объявлении параметров процедур и функций. Например, нельзя написать такое объявление: FUNCTION doublejnyjnterval С A IN INTERVAL DAY(9) TO SEC0NDC9)) RETURN INTERVAL DAY TO SECOND IS К счастью, в PL/SQL имеются два типа, структуры данных которых объявля- объявляются без ограничений: О YMINTERVALJJNCONSTRAINED - принимает любое значение типа INTERVAL YEAR TO MONTH без потери точности; О DSINTERVALJJNCONSTRAINED - принимает любое значение типа INTERVAL DAY TO SECOND без потери точности. Воспользовавшись типом данных DSINTERVALJJNCONSTRAINED, приведенный выше пример можно переписать следующим образом: DECLARE В INTERVAL DAYC9) ТО SECONDO): FUNCTION double_my_interval ( A IN DSINTERVALJNCONSTRAINED) RETURN DSINTERVALJJNCONSTRAINED IS BEGIN RETURN A * 2; ' . END; BEGIN В :- '100 0:0:0.123456789'; DBMS_OUTPUT.PUT_LINE(B); DBMS_OUTPUT.PUT_LINE(double_my interval(B)): END: Его результат будет таким: +000000100 00:00:00.123456789 +000000200 00:00:00.246913578
Функции обработки даты-времени 317 Обратите внимание, что тип данных DSINTERVALJJNCONSTRAINED использовался дважды: один раз для задания типа формального параметра функции double_ my interval, а второй — для задания типа возвращаемого ею значения. Теперь эту функцию можно вызывать для любого значения типа INTERVAL DAY TO SECOND без потери точности. Функции обработки даты-времени Описанные в предыдущем разделе арифметические действия с датой не обеспе- обеспечивают реализации всех функциональных возможностей, необходимых для работы со значениями даты-времени, и поэтому PL/SQL предлагает для этой цели набор разнообразнейших встроенных функций. Вы уже знаете о встроенных функциях преобразования, таких как TO_DATE и NUMTOYMINTERVAL. Другие функции обработки даты и времени представлены в табл. 10.5. ВНИМАНИЕ Избегайте использования традиционных функций Oracle, обрабатывающих значения типа DATE, ко- когда имеете дело с новыми типами данных TIMESTAMP. Вместо этого для манипулирования значе- значениями типов данных TIMESTAMP и INTERVAL следует применять новые функции. А DATE-функции пригодны в большей мере для обработки значений типа данных DATE. Многие из приведенных в таблице функций, в том числе ADD_MONTHS, принима- принимают значения типа DATE. Использование таких функций с новыми типами данных TIMESTAMP проблематично. Хотя любой из этих функций можно передать значе- значение типа TIMESTAMP, Oracle неявно преобразует его в значение типа данных DATE, и только тогда функция выполнит свою задачу. В результате этого преобразования теряются доли секунды и информация о часовом поясе. Результирующее значе- значение указанной функции имеет тип данных DATE. Например, если передать функ- функции ADD_MONTHS значение типа TIMESTAMP WITH TIME ZONE, будет выведено значение типа DATE без информации о часовом поясе и без указания долей секунды. Так что будьте внимательны. Таблица 10.5. Встроенные функции обработки значений даты-времени Имя Описание ADD_MONTHS Прибавляет к дате заданное количество месяцев CURRENT.DATE Возвращает в часовом поясе сеанса текущую дату и время как значение типа DATE CURRENT_TIMESTAMP Возвращает в часовом поясе сеанса текущую дату и время как значение типа TIMESTAMP WITH TIME ZONE DBTIMEZONE Возвращает смещение часового пояса базы данных относительно UTC в форме символьной строки (то есть '-05:00') FR0MJT7 Добавляет к значению типа TIMESTAMP информацию о часовом поясе, преобразуя его в значение типа TIMESTAMP WITH "ПМЕ ZONE LAST_DAY(flaTa) Возвращает последний день месяца, содержащийся в заданной входной дате продолжением
318 Глава 10 • Дата и время Таблица 10.5 (продолжение) Имя Описание LOCALTIMESTAMP Возвращает текущую дату и время как значение типа TIMESTAMP в локальном часовом поясе MONTH_BETWEEN Вычисляет количество месяцев между двумя датами NEW_TIME Значение типа DATE одного часового пояса преобразует в аналогичное значение другого пояса NEXT_DAY Возвращает дату первого дня недели, следующего за указанной датой ROUND Возвращает значение типа DATE, округленное до заданных единиц SESSIONTIMEZONE Возвращает смещение часового пояса сеанса (относительно UTC) в форме символьной строки (то есть '-05:00') SYSDATE Возвращает текущую дату и время сервера Oracle как значение типа DATE SYSTTMESTAMP Возвращает текущую дату и время сервера Oracle как значение типа TIMESTAMP WITH TIME ZONE TRUNC Возвращает значение типа DATE, усеченное до заданных единиц TZ_OFF5ET Возвращает смещение относительно UTC часового пояса, заданного его названием или аббревиатурой, в форме символьной строки (то есть '-05:00') Функции получения даты и времени Получить текущую дату и время в PL/SQL всегда было просто - достаточно лишь вызвать функцию SYSDATE. Однако в Огас1е9г для этого появились и другие возможности. Ниже приведен синтаксис всех функций Oracle9i, возвращающих текущую дату и время: CURRENTJATE RETURN DATE CURRENT TIMESTAMP RETURN TIMESTAMP TZJJNCONSTRAINED LOCALTIMESTAMP RETURN TIMESTAMPJNCONSTRAINED SYSDATE RETURN DATE SYSTIMESTAMP RETURN TIMESTAMP TZ UNCONSTRAINED ПРИМЕЧАНИЕ- ¦ ¦* 1^ ¦¦ II I ¦ I Ъв В документации по SQL приведен необязательный параметр функций CURRENT_TIMESTAMP и LOCALTIMESTAMP, в котором задается точность возвращаемого значения. В OracleSI Release 1 язык SQL поддерживает этот параметр, a PL/SQL — нет. Его поддержка включена s Oracle9l Release 2. Какую же из перечисленных функций следует использовать в каждом кон- конкретном случае? Ответ на этот вопрос зависит от нескольких факторов, которые лучше рассматривать в следующем порядке. 1. Нужно определить, используется версия РСУБД до Oracle8i или необходимо обеспечить совместимость приложения с такой версией. В этом случае выбор однозначен: SYSDATE. 2. Важна информации о часовом поясе. База данных и каждый сеанс связаны с оп- определенным часовым поясом. При этом часовой пояс сеанса может отличаться
Функции обработки даты-времени 319 от часового пояса базы данных. Выберите то время, которое вам нужно, - в часо- часовом поясе пользователя или в часовом поясе сервера. 3. Не менее важен тип данных. Некоторые функции возвращают значение типа DATE, одна функция - типа TIMESTAMP, а остальные - TIMESTAMP WITH TIME ZONE. Определив нужный часовой пояс (сервера или пользователя), сузьте выбор, основываясь на типе данных возвращаемого функцией значения, Для того чтобы облегчить для вас выбор ф;, нкции, в дополнение к этому пе- перечню факторов мы приводим табл. 10.6. Обратите внимание на то, что в наиме- наименовании функций Oracle не последовательна: функции LOCALTIMESTAMP и SYSTI- MESTAMP имеют однотипные имена, но возвращают разные типы данных. Таблица 10.6. Перечень функций, возвращающих текущую дату и время Функция OJRRENT_DATE OIRRENTJIMESTAMP LOCALTIMESTAMP SYSDATE SYSTIMESTAMP Часовой пояс Сеанса Сеанса Сеанса Сервера Сервера Тип данных DATE TIMESTAMP WITH TIME ZONE TIMESTAMP DATE TCMESTAMP WITH TIME ZONE Если вы, например, хотите получить текущую дату и время сеанса и присво- присвоить это значение переменной типа DATE, вам нужно воспользоваться функцией CURRENT DATE. Если же вы работаете с датами из других часовых поясов, используя переменные типа TIMESTAMP WITH TIME ZONE, выберите фунхцию CURRENTJIMESTAMP. При выборе функции, возвращающей время в часовом поясе сеанса, убеди- убедитесь, что администратором базы данных правильно сконфигурирован сервер и что верно задан часовой пояс сеанса. Следующий пример показывает различие меж- между результатами работы функции CURRENT_TIMESTAMP, которая предназначена для определения времени в часовом поясе сервера, и функции SYSTIMESTAMP, исполь- используемой для получения времени в часовом поясе сеанса: BEGIN DBMS OUTPUT.PUT LINECCURRENT TIMESTAMP): DBMS OUTPUT.PUT LINE(SYSTIMESTAMP): END; Результат выполнения кода таков: 24-FEB-02 Q4.55.54.803009QOQ РМ -Q8.0Q 24-FEB-02 07.55.54.804221000 РМ -05:00 В данном примере в часовом поясе сеанса используется Тихоокеанское стан- стандартное время (-8:00), а в часовом поясе сервера - Восточное стандартное время (-5:00). Обратите внимание на разницу в 3 ч между этими двумя значениями, а также на соответствующее смещение часовых поясов. А что если не существует функции, возвращающей значение нужного вам типа данных? Если вы хотите, например, присвоить время часового пояса сервера значению переменной типа TIMESTAMP, Oracle выполнит неявное преобразование
320 Глава 10 • Дата и время типов. Но лучше в подобных случаях производить явное преобразование с помо- помощью функции CAST: DECLARE¦ a TIMESTAMP: b TIMESTAMP; BEGIN a :- CASTtSYSTIMESTAMP AS TIMESTAMP); b ;- SYSDATE: DBMS_OUTPUT.PUT_LINECT0_CHAR(a,'DD-MON-YYYY HH:MI:SS AM1)): DBMS_0UTPUT.PUT_LINECT0_CHAR(a,'DD-MON-YYYY HH:MI:SS AM')): END; Результат выполнения этого кода: 24-FEB-02 06.46.39 РМ 24-FEB-Q2 06.46.39 РМ В данном примере результат вызова функции SYSTIMESTAMP с помощью функ- функции CAST явно преобразуется из типа данных TIMESTAMP WITH TIME ZONE в тип данных TIMESTAMP. А результат вызова функции SYSDATE неявно преобразуется из типа дан- данных DATE в тип данных TIMESTAMP. Если вы пользуетесь этими функциями для определения времени с точностью до долей секунды, учитывайте ограничения аппаратного обеспечения и операци- операционной системы. Функции CURRENTJIMESTAMP, LOCALTIMESTAMP и SYSTIMESTAMP возвра- возвращают значения типа TIMESTAMP WITH TIME ZONE или TIMESTAMP. Указанные типы дан- данных позволяют хранить время с точностью до миллиардной доли секунды. Все это очень хорошо, но подумайте о том, каким образом определяется время в дан- данном случае. Oracle (по всей видимости) получает время от операционной системы, а операционная система — от аппаратного обеспечения. И если ваша операцион- операционная система или аппаратное обеспечение отслеживают время с точностью только до сотых долей секунды, Oracle не сможет вернуть более точных результатов. На- Например, в системе Suse Linux 7.2 на базе процессора Intel x86 нам удалось изме- измерить время лишь с точностью до миллионных долей секунды (шесть цифр), тогда как на том же аппаратном обеспечении с Windows XP мы получили миллиард- миллиардные доли секунды. Функции обработки информации о часовом поясе Если требуется узнать, какой часовой пояс установлен для вашего сеанса или для базы данных либо если необходимо определить смещение некоторого часового пояса относительно UTC, имеет смысл воспользоваться одной из трех следую- следующих функций: О DBTIMEZONE RETURN VARCHAR2; О SESSIONTIMEZONE RETURN VARCHAR2; О TZ_OFFSET {часовой_пояс) RETURN VARCHAR2. Функции DBTIMEZONE и SESSIONTIMEZONE возвращают часовые пояса базы данных и сеанса, идентифицируемые смещением относительно UTC. Например: BEGIN DBMS OUTPUT.PUT LINE(DBTIMEZONE);
Функции обработки даты-времени 321 DBMS_OUTPUT.PUTJ-INE(SESSIONTIMEZONE); END; Результат выполнения этого кода: -07:00 -08:00 Но какие часовые пояса представляют эти смещения? Напомним, что пред- представление V$TIMEZONE_NAMES возвращает список названий часовых поясов и их аб- аббревиатур. Если вам нужно узнать смещение часового пояса для заданного регио- региона, воспользуйтесь функцией TZJFFSET. Например, следующий запрос возвращает список названий всех часовых поясов, соответствующих смещению -07:00. Этот запрос был выполнен, когда действовало зимнее время. Для летнего времени ре- результаты были бы другими. В частности, регион America/Denver большую часть года относится к часовому поясу, которому соответствует смещение относитель- относительно UTC -07:00, а в период действия летнего времени — к часовому поясу со смеще- смещением -06:00: SELECT TZNAME, TZABBREV FROM VSTIMEZONE NAMES WHERE SUBSTR(TZ_0FFSET(TZNAffi).1.6) - '-07:00': TZNAME TZABBREV America/Denver America/Denver America/Denver America/Denver America/Edmonton America/Edmonton America/Edmonton LMT MST MWT MDT LMT MST MDT А с какой целью в этом примере использовалась функция SUBSTR? В Огас1е9г Release 1 в функции TZ_OFFSET имеется ошибка, из-за которой данная функция возвращает в конце строки лишний нулевой байт: SELECT TZOFFSETC US/EASTERN') offset. LENGTH(TZ_OFFSET('US/EASTERN')) 1ength, DUMP(TZ_OFFSETC'US/EASTERN1)) dump FROM dual: OFFSET LENGTH DUMP -05:00 7 Typ-1 Len-7: 45.48.53,58.48.48.0 Обратите внимание на нулевой байт в конце столбца DUMP и посмотрите, что произойдет при выполнении следующей инструкции SELECT: SELECT TZNAME FROM V$TIMEZONE_NAMES WHERE TZ_OFFSET(TZNAME) - '-07:00': ERROR at line 3: 0RA-01S75: time zone minute must be between -59 and 59
322 Глава 10 • Дата и время В худшем случае эта инструкция не вернет ни одной строки - такой результат был нами получен при тестировании программы в Windows XP. Однако в Linux и Windows 2000 была сгенерирована указанная выше ошибка. Для удаления лиш- лишнего нуля воспользуйтесь функцией SUBSTR, и все будет в порядке. Функция ADD.MONTHS Возвращает дату, полученную путем добавления к заданной дате определенного количества месяцев. Ее синтаксис таков: FUNCTION ADD_MQNTHS (входая^ага IN ОАТЕ, юптчесгво_несяцев NUMBER) RETURN DATE FUNCTION ADD"MONTHS {нотчество_неств NUMBER, uxomjard IN DATE) RETURN DATE Как видите, функция ADD_MONTHS перегружена. При ее вызове посредством зна- значения первого аргумента задается дата, а посредством второго - количество ме- месяцев, но можно сделать и наоборот: задать количество месяцев с помощью пер- первого аргумента, а дату — с помощью второго. Однако и в том и в другом случае оба аргумента являются обязательными. Если значение аргумента копичестао_месяцев является положительным, функ- функция AOD_MONTHS возвращает дату, наступающую через заданное количество меся- месяцев после входной даты. Если же значение аргумента количество_месяиев отрица- отрицательно, она будет возвращать дату, наступающую на заданное количество месяцев раньше входной даты. Приведем несколько примеров использования функции ADD_MONTHS. О Сдвиг на три месяца вперед: ADD_«ONTHS('1Z-JAN-19951, 3) -> 12-APR-1995 О Сдвиг на три месяца назад: ADD_MQNTHSC'12-MAR-1990'. 3) -> 12-MAR-1989 Функция ADD_MONTHS всегда сдвигает дату на целое количество месяцев. В аргу- аргументе количество_месяцев можно задать и дробное число, но только положитель- положительное, и функция ADD_MONTHS округлит его до ближайшего наименьшего целого, как в следующем примере. Обратите внимание на результат выполнения функции, когда этот аргумент задан отрицательным значением: BEGIN DBMS OUTPUT.PUT_LINECADD_MONTHS C2B-FEB-1989', 1.9999)): DBMS OUTPUT.PUT LINE(ADD_MONTHS ('28-FEB-19891, D): DBMS~OUTPUT.PUTJJNE(ADD_MONTHS C2B-FEB-19891. -1.9999)); DBMS~OUTPUT.PUT_UNE(ADD_MONTHS C28-FEB-19891. -1)); END: Результат выполнения данного кода: 31-MAR-19B9 31-MAR-1989 31-JAN-1989 31-JAN-1989 Если хотите сдвинуть дату на дробное количество месяцев, просто прибавьте к ней или вычтите из нее нужное количество дней. PL/SQL позволяет произво- производить со значениями даты арифметические операции. Более подробно об этом
функции обработки даты-времени 323 было рассказано в разделе этой главы «Арифметические операции над значения- значениями даты-времени». Если входная дата функции ADD_MONTHS не является последним днем месяца, возвращаемая ею дата приходится на тот же день месяца. Если номер дня вход- входной даты больше номера последнего дня месяца, на который должна прийтись ре- результирующая дата, функция ADD_MONTHS возвращает последний день заданного месяца. Приведенный ниже пример иллюстрирует эти правила. В первом вызове функции ADD_MONTHS к 15 января прибавляется один месяц, в результате чего по- получается 15 февраля. Поскольку в феврале нет 31-го дня, второй вызов возвраща- возвращает последний день февраля: BEGIN DBMS OUTPUT.PUT_LINE<ADD_MONTHS C15-JAN-19951, 1»; DBMS~0UTPUT.Pl/T_LINE(ADD_MONTHS C31-JAN-19951. D): END; Приведем результат выполнения этого кода: 15-FEB-1995 28-FE8-199S Такое поведение функции A0D_MONTHS вполне разумно и понятно. Однако что будет, если входная дата придется на последний день месяца, а в результирую- результирующем месяце будет больше дней, чем в исходном? Что получится, если прибавить два месяца к 28 февраля 1994 года, - 30 апреля 1994 года (последний день апре- апреля) или 28 апреля 1994 (тот же день месяца, что и в исходной дате)? Ответ таков: если входная дата представляет последний день месяца, тогда и выходная дата будет представлять последний день месяца. Например: BEGIN DBMS_OUTPUT.PUT L1NE(ADD_MONTHS С 28-FEB-1994'. Z)); END; 30-APR-1994 Если передать функции ADD_MONTHS дату, представляющую последний день ме- месяца, PL/SQ.L вернет последний день результирующего месяца независимо от ко- количества дней в каждом из них. В одних случаях это удобно, а в других — не со- совсем (см. врезку). КАК СОХРАНИТЬ ИСХОДНЫЙ ДЕНЬ МЕСЯЦА Если входная дата функции ADD_MONTHS приходится на конец месяца, эта функция всегда возвращает дату конца результирующего месяца. Однако такое ее поведение не всегда удобно. Следующая функция PL/SQL, кото- которая названа my_add_months, когда это возможно, сохраняет исходный день месяца: FUNCTION my_add months t datejn IN DATE, months shift IN NUMBER) RETURN DATE IS date_out DATE: uayjn NUMBER: продолжение.?'
324 Глава 10 • Дата и время day_out NUMBER; BEGIN date_out :- ADD MONTHS(date_in. nranths_sh-fft): day In :- TO №JMBER{T0_CHAR(date_1n.'DD')): day~out :- TOJWMBERCTOJHARCdatejJut.'DD1)); IF dayjaut > dayjn THEN date_out :- date_out - (day out - dayjin); END IF: RETURN date_out: END: Единственный случай, при котором данная функция возвращает другой день, - это когда исходного дня просто нет в результирующем месяце. Например, если добавить к 31 января 2002 года один месяц, получится 28 февраля 2002 года. Однако если прибавить один месяц к 28 февраля 2002 года, получится 28 марта 2002 — то есть день месяца будет сохранен. Указанная функция также сохраняет все компоненты времени дня: часы, минуты и секунды. Функция FROM_TZ Добавляет к значению типа TIMESTAMP информацию о часовом поясе, тем самым преобразуя его в значение типа TIMESTAMP WITH TIME ZONE. Ее синтаксис таков: FUNCTION FROM_TZ CHd4eHueJimestanp IN TIMESTAMP, часовой пояс IN VARCHAR2) Часовой пояс можно задать либо в виде смещения относительно UTC, либо в виде названия региона. Функция возвращает ту же дату, которая ей была перс- дана, но с добавлением заданного часового пояса. Приведем программу, содержащую два вызова функции FROM_TZ: DECLARE w TIMESTAMP :- CAST('24-Jun-2002 3.41.00.00 PM' AS TIMESTAMP): x TIMESTAMP WITH TIME ZONE; у TIMESTAMP WITH TIME ZONE; BEGIN x :- FROH_TZ(w.'-5:00'): у :- FROM_TZ(w.'US/Eastern'): DBMS_OUTPUT.PUT_LINE(x); DBMS_OUTPUT.PUT_LINE(y); END; А вот результат выполнения кода: 24-JUN-02 03.41.00.000000 PM -05:00 24-JUN-02 03.41.00.000000 PM US/EASTERN Как видите, дата и время остались неизменными, однако выходные значения теперь содержат информацию о часовом поясе, а их тип изменился с TIMESTAMP па TIMESTAMP WITH TIME ZONE.
Функции обработки даты-времени 325 функция LAST_DAY Возвращает дату последнего дня месяца, к которому относится заданная дата. Синтаксис функции таков: FUNCTION LASTJJAY (.входая_ватд IN DATE) RETURN DATE Необходимость в этой функции возникла из-за того, что в месяцах разное ко- количество дней. С ее помощью можно, например, узнать, сколько дней в феврале заданного года — 28 или 29. Приведем несколько примеров использования функ- функции LAST_DAY: BEGIN -- Выведен последний лень месяца DBMS_OUTPUT.Pin"_LINE (LASTJAY ('12-JAN-99')); -- Если это последний день иесяцз. на нем и остановимся DBMS_OUTPUT.PUT_LINE (LASTJJAY С 31-JAN-991)): -- Выведем последний день месяца три месяца спустя DBMSOUTPUT.PUTJJNE (LAST_DAY (ADD_MONTHS (SYSDATE. 3))): -- Выведем количество дней до конца текущего месяца DBMS_OUTPUT.PUT_LINE (LAST_DAY (SYSDATE) - SYSDATE); END; 31-JAN-0099 31-JAN-0099 31-MAY-2002 4 ОПРЕДЕЛЕНИЕ ПЕРВОГО ДНЯ МЕСЯЦА В Oracle имеется функция LASTDAY (последний день), так почему же не быть и функции FIRST_DAY (первый день)? Для нее обязательно нашлось бы применение. Например, часто приходится писать запросы, в которых нужно определить первый и последний дни месяца введенной пользова- пользователем даты. Простейший способ получить первый день месяца таков: ADD_MONTHS(LAST_DAY(x). -D+1 Он основан на специфическом поведении функции ADD_MONTHS в отноше- отношении конца месяца. Сначала с помощью функции LAST_DAY мы определяем дату последнего дня текущего месяца, а затем с помощью функции ADD_ MONTHS прибавляем к ней -1 месяц, чтобы получить дату последнего дня предыдущего месяца. В завершение прибавляем к результату 1 день и по- получаем первый день текущего месяца. Хотя это может показаться немного сложным, данный подход позволяет избежать манипуляций со строками и сохраняет компонент времени дня, который может входить в состав исходного значения.
326 Глава 10 • Дата и время Функция MONTH.BETWEEN Вычисляет количество месяцев между двумя датами, а в качестве результата воз- возвращает число. Ее синтаксис таков: FUNCTION MONTH BETWEEN (даraj IN DATE, вате 2 IN DATE) RETURN NUMBER Функция MONTH_BETWEEN действует в соответствии со следующими правилами: О если дата_1 наступает позже даты_2, функция возвращает положительное зна- значение; О если дата_1 наступает раньше даты_2, функция возвращает отрицательное зна- значение; О если дата_1 и дата_2 относятся к одному месяцу, функция возвращает дробное значение из диапазона от -1 до +1; О если цата_1 и ддта_2 приходятся на последние дни соответствующих месяцев, функция возвращает целое число (без дробного компонента); О если дата_1 и да та_2 относятся к разным месяцам и хотя бы одна из них не при- приходится на последний день месяца, функция возвращает дробное значение. (Дробный компонент вычисляется на основе того, что месяц состоит из 31 дня, с учетом разницы компонентов времени двух дат.) Приведем несколько примеров использования функции MONTH_BETWEEN: BEGIN -- Разница между двумя последними днями месяцев; первый месяц наступает раньше второго: D8MS_0UTPUT.PUT_LINEC MONTHSJETWEEN C31-JAN-1994'. '28-FEB-1994' )>: -- Разница между двумя последними днями месяцев; первый месяц наступает позже второго: DBMS_OUTPUT.PUT_LINE( MONTHSJETWEEN C31-MAR-1995', '28-FEB-1994')): -- Две даты одного месяца: DBMS JUTPltf. PUT LINEC MONTHSJETWEEN ('28-FEB-1994', '15-FEB-1994')); -- Вычисления с дробным компонентом: DBMS_OUTPUT.PUT_LINE( MONTHSJETWEEN C31-JAN-19941, 'l-MAR-19941)); DBMSJXJTPUT.PUT LINE! MONTHS BETWEEN C31-JAN-1994', 7-MAR-1994')); DBMS_OUTPUT.PUT_LINE( MONTHSJETWEEN ('31 -JAN-1994'. '10-MAR-19941)): END: Результаты выполнения этого кода таковы: -1 13 .4193548387096774193548387096774193548387 -1.03225806451612903225806451612903225806 -1.06451612903225806451612903225806451613 -1.32258064516129032258064516129032258065
Функции обработки даты-времени 327 Вы, очевидно, заметили здесь определенную закономерность. Как уже было сказано, функция MQNTH_BETWEEN вычисляет дробный компонент количества меся- месяцев исходя из предположения, что каждый месяц содержит 31 день. Поэтому на каждый день сверх полного месяца к результирующему значению прибавляется 1/31 месяца: 1/31-0.32258065... В соответствии с оговоренным правилом количество месяцев между 31 января 1994 года и 28 февраля 1994 года равняется 1, а количество месяцев между 31 ян- января 1994 года и 1 марта 1994 года на 0,32258065 больше. Функции ROUND и TRUNC Функция ROUND округляет дату до ближайшей даты в соответствии с заданной маской форматирования, а функция TRUNC усекает значение даты-времени до бли- ближайшей даты, Указанные функции обработки значений даты-времени действуют аналогично одноименным числовым функциям. (См, раздел «Функции округле- округления и усечения» в главе 9.) Их синтаксис таков: FUNCTION ROUND {exoaaajara IN DATE [ иаска_форматироаенш1, VARCHAR2]) RETURN DATE FUNCTION TRUNC [ixoBinjaia IN DATE [ маска_форнатирования. VARCHAR2]) RETURN DATE Данные функции часто применяются для установки компонента времени в зна- значении типа DATE равным полуночи A2:00:00 AM). Это значение по общему согла- соглашению используется в тех случаях, когда важна только дата, а не время. Различие между двумя функциями состоит в следующем: компонент времени либо отбра- отбрасывается полностью, либо используется для округления в ту или иную строну ре- результирующего значения типа DATE. Рассмотрим работу обеих функций: DECLARE date_in DATE :- TO_DATE('24-Feb-2002 05:16:00 PM1 ,'DD-MON-YYYV HH:MI:SS AM'): date_rounded DATE; date_truncated DATE; BEGIN date_rounded :- ROUNDtdate 1n): datejxuncated :- TRUNC(dateJn); DBMS_OUTPUT.PUT LINEt TO_CHAR(date~rounded. 'DO-MON-YYYY HH:MI:SS AM'JJ; DBMS_OUTPUT.PUT_LINE( TO CHAR(date_truncated,'DD-MON-YYYY HH:MI:SS AM1)): END: Результат выполнения этого кода следующий: 25-FE8-2002 12:00:00 AM 24-FEB-2002 12:00:00 AM Проанализируем значение переменной date_rounded. По сравнению с исход- исходным значением дата дня месяца увеличена на единицу — теперь это 25 февраля. Так получилось потому, что время 5:16 после полудня ближе к полуночи следую- следующего дня B5 февраля), чем к полуночи 24 февраля. А вот значение переменной
328 Глава 10 • Дата и время date_truncated, напротив, по-прежнему соответствует 24 февраля. Функция TRUNC просто удалила информацию о времени. Функция ROUND эту информацию учиты- учитывает и, хотя тоже удаляет ее из результирующего значения, в зависимости от зна- значения времени может увеличить на единицу число месяца. Если бы время равня- равнялось 5:16 утра, округление было бы произведено в сторону уменьшения значения и обе функции вернули бы один и тот же результат. По умолчанию функции ROUND и TRUNC округляют и усекают значения даты- времени до даты, и эти операции выполняются чаще всего. Но возможно округле- округление и усечение и до других компонентов. При необходимости это сделать в вызове функции нужно задать соответствующую маску, содержащую один из элементов, которые перечислены в табл. 10.7. Округление будет производиться до элемента, заданного в этой маске. Таблица 10.7. Элементы маски форматирования для функций ROUND и TRUNC Маска форматирования Округляет или усекает до SCC или СС Столетия SYYYY, YYYY, YEAR, SYEAR, Года (округление до следующего года начиная с 1 июля) YYY, YY или Y IYYY, IYY, ГУ или I Года (стандарт ISO) Q Квартала (начиная с 16 числа второго месяца квартала выполняется округление в сторону увеличения значения) MONTH, MON, ММ или RM Месяца (начиная с 16 числа, которое не обязательно совпадает с серединой месяца, выполняется округление в сторону увеличения значения) WW IW W DDD, DD или J DAY, DY или D НН, НН12 или НН24 Ml Дня недели, на который приходится первый день года Дня недели, на который приходится первый день года (стандарт КО) Дня недели, на который приходится первый день месяца Дня Первого дня недели Часа Минуты Следующий пример демонстрирует, как с помощью функции TRUNC значение даты-времени усекается до начала года или начала месяца: DECLARE date_in DATE :- TO_DATE('24-Feb-2002 05:36:00 PM1 .¦DO-MON-YYYY HH:MI:SS AM'); trunc^oj'ear DATE; trunc_to_month DATE: BEGIN trunc_toj>ear :- TRUNC(datejn.'YYYY'); trunc_to_month :-TRUNCCdateJn. 'MM');
функции обработки даты-времени 329 DBMS_OUTPUT.PUT_LINE( T0_CHAR(trunc_tOJ-ear, 'DD-MON-YYYY HH.MI.-SS AM1)); DBMS_OUTPUT.PUT_LINE( TO_CHAR(trunc_to_month. 'DD-MON-YYYY HH;MI:SS AM')); END: Результат выполнения этого кода: 01-JAN-2002 12:00:00 AM 01-FEB-2002 12:00:00 AM Как видите, функция TRUNC позволяет мгновенно перейти к началу года и к на- началу месяца. ПРИМЕЧАНИЕ Если вы прочитали врезку «Определение первого дня месяца», у вас наверное возник вопрос, поче- почему для получения первого дня месяца нельзя использовать функцию TRUNC. Это возможно, если для вас не является важным сохранение компонента времени. Так что если время должно быть со- сохранено, используйте прием, описанный во врезке, а если нет—смело вызывайте функцию TRUNC. Следующий пример демонстрирует два разных способа применения функции ROUND. В первом с ее помощью определяется столетие, ближайшее к 24 февраля 2002 года и к 24 февраля 1902 года. После этого мы посредством той же функции округляем значение даты-времени с точностью до часов: DECLARE date in_l DATE datejin_2 DATE date in 3 DATE round_l DATE; round_2 DATE; round 3 DATE: - TO_DATE('24-Feb-2Q02'.'DD-MON-YYYY1); - TO_DATE('24-Feb-1902','DD-MON-YYYY1): = TO_DATE('24-Feb-2002 05:36:00 PM1 . 'DD-HON-YYYY HH:MI:SS AM'): BEGIN round_l round_2 round 3 - RQUNDCdateJnJ.'CC1); - ROUNDCdateJnJ. 'CO: - ROUND(date in 3. I1); DBMS_CUTPUT.PUT_LINECTO_CHAR(round_l.'DD-MON-YYYY HH:MI:SS AM1)); DBMS_0UTPUT.PUT_LINE(T0_CHARCround_2.'DD-MQN-YYYY HH:MI:SS AM1)); DBMS_OUTPUT.PUT_LINEСТО CHAR(round_3,'DD-MON-YYYY HH:MI:SS AM1)); END: Результат выполнения этого кода следующий: 01-JAN-2001 12:00:00 AM 01-JAN-1901 12:00:00 AM 24-FE8-20D2 06:00:00 PM При округлении с точностью до столетия в качестве результата получаем не столетие, а дату и время его начала. Oracle использует научное определение сто- столетия, а не его популярное определение, так что, к примеру, XXI столетие начи- начинается в 2001 году, а не в 2000. Объяснение третьей выходной строки очень про- простое: время 5:36 после полудня было округлено до ближайшего часа, то есть до 06:00.
330 . Глава 10 • Дата и время ВНИМАНИЕ ¦ При использовании функций ROUND и TRUNC имейте в виду то, что ненужные компоненты да- даты-времени никуда не «исчезают». Все переменные типа DATE содержат значения лет, месяцев, дней, часов, минут и секунд, Когда 5:36 пополудни округляется до 06:00 пополудни, компонент ми- минут остается — он просто обнуляется, Функцию TRUNC удобно использовать при сравнении двух дат, представленных значениями переменных типа DATE. Рассмотрим следующий пример: IF request_date BETWEEN start date AND end_date THEN Даты, заданные значениями переменных request_date и start_date, могут быть одинаковыми, но если не указан компонент времени, оператор IF может вернуть неверный с точки зрения логики приложения результат. Например, если пользо- пользователь ввел значение переменной request_date без компонента времени, время в этой переменой будет установлено равным полуночи указанного пользователем дня. Если при этом значение переменной start_date было сгенерировано с помо- помощью функции SYSDATE, его компонент времени будет отражать время на момент сравнения. И поскольку полночь наступает раньше любого другого времени оп- определенного дня, значение переменной request_date не попадет в заданный в срав- сравнении диапазон, хотя на первый взгляд может показаться иначе. Если вы точно не знаете, какие компоненты времени содержатся в значениях переменных даты-времени, и вам нужно, чтобы в операциях обработки дат ком- компонент времени игнорировался, воспользуйтесь для его усечения функцией TRUNC: IF TRUNC trequestjate) BETWEEN TRUNC (start_date) AND TRUNC (end_date) THEN Функция TRUNC устанавливает время в 12:00:00 AM. В этом примере все значе- значения даты-времени усекаются до даты, так что время никак не влияет на результат сравнения. Функция NEWJTIME Не просто определить, сколько времени в Анкоридже, когда в Чикаго 3 часа по- пополудни. К счастью, PL/SQL предоставляет на этот случай функцию NEW_TIME. Она переводит даты вместе с компонентами времени из одного часового пояса в другой. Ее синтаксис таков: FUNCTION NEW TIME [BXOgd»jtTa IN DATE, пояс 1 VARCHAR2. notcj VARCHAR2) RETURN DATE Здесь BxonaajaTa — это исходная дата; nostcj. — ее часовой пояс (обычно это ваш локальный часовой пояс); пояс_2 — тот часовой пояс, для которого нужно полу- получить эквивалент исходной даты. ПРИМЕЧАНИЕ- Если вы создаете программное обеспечение, которое должно работать в нескольких часовых поя- поясах, мы рекомендуем использовать новые возможности типа данных TTMESTAMP WITH TIME ZONE, в не функцию NEWJTTME со значениями типа DATE.
Функции обработки даты-времени 331 Названия допустимых часовых поясов перечислены в табл. 10.8. Таблица 10.8. Аббревиатуры названий часовых поясов Аббревиатура Название AST Atlantic Standard Time (Атлантическое стандартное время) ADT Atlantic Daylight Time (Атлантическое летнее время) BST Bering Standard Time (Берингово стандартное время) BDT Bering Daylight Time (Берингово летнее время) CST Central Standard Time (Центральное стандартное время) CDT Central Daylight Time (Центральное летнее время) EST Eastern Standard Time (Восточное стандартное время) EDT Eastern Daylight Time (Восточное летнее время) GMT Greenwich Mean Time (Время по Гринвичу) MST Alaska-Hawaii Standard Time (Стандартное время на Аляске и Гавайях) HDT Alaska-Hawaii Daylight Time (Летнее время на Аляске и Гавайях) MST Mountain Standard Time (Горное стандартное время) MDT Mountain Daylight Time (Горное летнее время) NST v Newfoundland Standard Time (Стандартное время в часовом поясе Ньюфаундленд) PST Pacific Standard Time (Тихоокеанское стандартное время) PDT . Pacific Daylight Time (Тихоокеанское летнее время) YST Yukon Standard Time (Стандартное время в часовом поясе Юкон) YDT Yukon Daylight Time (Летнее время в часовом поясе Юкон) ПРИМЕЧАНИЕ- Имейте в виду, что функция NEWJTIME принимает не все аббревиатуры названий часовых поясов, возвращаемые представлением V$TIMEZONE_NAMES. Поэтому используйте только аббревиатуры, приведенные в табл. 10.8. Как показывает следующий пример, спецификации часового пояса в функции NEW_TIME не чувствительны к регистру символов: BEGIN OBMS_OUTPUT.PUT_LINEC TO CHAR (NEW TIME (TO DATE ('09151994 12:30 AM1, 'MMDDYYYY HH:MI AM1), 'CST1, 'hdf). 'Month 0D. YYYY HH:MI AM1)); END; Этот код выводит следующую строку: September 14, 1994, 9:30 РМ Таким образом, когда в Чикаго 12:30 утра 15 сентября 1994 года, в Анкоридже 9:30 вечера 14 сентября 1994 года.
332 Глава 10 • Дата и время ПРИМЕЧАНИЕ Для того чтобы в вычислениях использовалось время, отличное от полуночи, применяется функция TO_DATE, А для вывода результирующих даты и времени в читабельной форме предназначена функция TO_CHAR, имеющая другую маску форматирования, поскольку по умолчанию PL/SQL не выводит компонент времени. Несмотря на то что функция NEW_TIME выполняет преобразование с целью дос- достижения соответствия между часовыми поясами, она не учитывает информацию о часовом поясе, имеющуюся в значениях типа TIMESTAMP WITH TIME ZONE. Об этом свидетельствует следующий пример: DECLARE a TIMESTAMP WITH TIME ZONE :« TO_TIMESTAMP_TZ ('09151994 12:30 AM -5:00', 'MMDDYYYY HH:MI AM TZH;TZM'); BEGIN DBMS_DUTPUT.PUT_L1NEU): DBMS OUTPUT.PUT_LINEC TO_CHAR (NEWJIME U.'CST'. 'hdt'). 'Month DD. YYYY HH:MI AM')): END: Результат выполнения кода таков: 15-SEP-94 12.30.00.000000 AM -05:00 September 14. 1994, 9:30 PM Обратите внимание на то, что возвращаемое время в этом примере, как и в пре- предыдущем, равняется 9:30 РМ. Функция NEWTIME принимает значение типа DATE, и когда переданное ей значение типа TIMESTAMP WITH TIME ZONE неявно преобразует- преобразуется в тип данных DATE, информация о часовом поясе теряется. Вот и получается, что время 12:30 AM сдвигается на +9:00 часов от GST к HDT. Функция NEXT_DAY Возвращает дату первого после заданной даты дня, выпадающего на указанный день недели. Вот ее синтаксис: FUNCTION NEXT_DAY Ыоднт_ша IN DATE. день_неаелн VARCHAR2) RETURN DATE В параметре день недели должно быть задано название дня недели на языке ва- вашего сеанса (определяемом значением инициализационного параметра базы дан- данных NLS_DATE_LANGUAGE). Компонент времени возвращаемой даты в точности равен компоненту времени исходной даты. Если день недели входной_дзты совпадает с заданным днем_недели, функция NEXT_DAY возвращает дату, которая наступает ров- ровно на неделю (семь дней) позже входной_даты. Исходную дату она не возвращает никогда. Приведем несколько примеров использования функции NEXTDAY. В них опре- определяется дата первого понедельника и первой среды 1997 года: BEGIN -- Можно использовать как полные так и сокращенные названия дней недели: DBMS_OUTPUT.PUT_LINE(NEXT_DAY ('01-JAN-1997', 'MONDAY')); DBMS OUTPUT.PUT LINECNEXT DAV ('Ol-JAN-19971. 'MON')):
Функции обработки даты-времени -- Регистр при наборе названия дня недели не имеет значения: DBMS_QUTPUT.PUT_LINE(NEXT_DAY C01-JAN-19971 , 'monday'»; -- Следующая среда наступает ровно через недело: D3MS_0UTPirr.PUT_UNE(NEXT_0AY ('01-JAN-1997', 'WEDNESDAY')); -- Если язык даты испанский: EXECUTE IMMEDIATE 'ALTER SESSION SET NLS_DATE_LANGUAGE-"SPANISH1 DBMS_OUTPUT.PUT_LINE(NEXT_DAY ('01-ENE-19971. 'LUNES')); END; Вот что выводит этот код: 06-JAN-97 06-JAN-97 06-JAN-97 07-JAN-97 06-ENE-97
1 Записи и коллекции > Записи в PL/SQL > Коллекции в PL/SQL > Определение типов коллекций и их объявление > Где используются коллекции > Встроенные методы коллекций > Работа с коллекциями > Псевдофункции коллекций > Сопутствующие операции > Выбор типа коллекции Данные могут храниться и обрабатываться в программах как в виде отдельных скалярных значений, так и в виде структур, состоящих из нескольких элементов с отдельными значениями. Записи и коллекции, о которых рассказывается в этой главе, являются составными структурами данных. С точки зрения концепции и структуры, записи в программах PL/SQL очень похожи на строки в таблицах баз данных. Запись как целое не имеет собственно- собственного значения; однако значение имеет каждый ее компонент, или поле, а объедине- объединение их в единую запись позволяет хранить и обрабатывать все значения как одно целое. Познакомившись с этой структурой данных, вы поймете, насколько проще с ее помощью выполняются многие задачи, которые так часто приходится решать программистам. Коллекция — это структура данных, похожая на одномерный массив. Ее можно считать близким аналогом традиционных массивов, имеющихся в языке PL/SQL. Коллекции часто используются для управления списками информации. Вы озна- ознакомитесь с тремя разными типами коллекций (ассоциативным массивом, вложен- вложенной таблицей и массивом VARRAY), узнаете, как объявлять эти структуры, как с ними работать и как выбрать тип, подходящий для конкретного случая. Но для начала обратимся к записям. Они проще и чаще используются в коллекциях. Записи в PL/SQL По своей структуре запись похожа на строку таблицы базы данных. Каждая стро- строка таблицы состоит из одного или нескольких полей, которые могут содержать
Записи в PL/SQL j-ээ данные разных типов. Существует три способа определения записи, но после ее определения доступ к полям и их изменение всегда выполняются в соответствии с одними и теми же правилами. Мы рассмотрим основные преимущества приме- применения записей, опишем разные способы их задания, приведем примеры использо- использования в программах. Эффективность использования записей Запись - это высокоуровневое средство адресации и обработки данных, опреде- определенных внутри программ PL/SQL (в отличие от информации, хранящейся в таб- таблицах баз данных). Представление данных в виде записей дает разработчикам оп- определенные преимущества, о которых рассказывается в следующих разделах. Абстракция данных Используя метод абстракции при моделировании сущностей реального мира, вы намеренно игнорируете определенные свойства объекта, обобщаете его характе- характеристики, отделяете его от особенностей конкретной реализации, то есть старае- стараетесь составить об этом объекте обобщенное представление. Но все действия, вы- выполняемые, скажем, с помощью функций, содержащихся в проектируемом вами программном модуле, должны отражаться (абстрагироваться) в его имени (или спецификации). Для того чтобы создать запись, нужно выделить атрибуты или поля описывае- описываемого ею объекта. Затем нужно задать отношение между этими атрибутами и при- присвоить ему имя. Получившийся набор атрибутов, связанных определенным отно- отношением, — это и есть запись. Агрегатные операции Организация информации в виде записей позволяет выполнять различные опе- операции не только над их отдельными атрибутами, но и над записью как единым це- целым. Такие агрегатные операции лишь усиливают абстракцию данных, На практи- практике нередки случаи, когда интерес представляют не столько значения в отдельных полях, сколько содержимое всей записи в целом. Предположим, что по роду своей деятельности менеджер часто взаимодейст- взаимодействует с различными компаниями. Информация о каждой из них хранится в базе данных в виде отдельной записи. Содержимое полей этой записи может быть раз- разным, но для менеджера это не имеет особого значения. Например, ему не важно, сколько строк занимает адрес компании. Главное, чтобы информацию о компани- компаниях можно было изменять, удалять или анализировать. Таким образом, организа- организация данных в виде записей позволяет обрабатывать сведения о конкретной ком- компании как единое целое, отбрасывая отдельные реквизиты, если они не нужны, но в то же время обеспечивает доступ к каждому из этих реквизитов. Такой подход позволяет рассматривать данные как наборы объектов, к которым применимы определенные правила. Компактность и простота кода Использование записей помогает программисту писать более понятный и ком- компактный код, содержащий меньше комментариев и объявлений переменных. Это
л jo Глава 11 • Записи и коллекции очень удобно как на этапе разработки программного кода, так и при его дальней-" шем сопровождении. Для того чтобы более полно использовать все возможно- возможности, предоставляемые этими замечательными структурами, старайтесь придер- придерживаться следующих рекомендаций. О Создавайте записи, соответствующие курсорам. Создавая в программе кур- курсор, тут же добавьте соответствующую запись (исключение составляет лишь курсор цикла FOR). Данные из курсора всегда извлекайте только в запись, но не в отдельные переменные. В тех немногих случаях, когда для этого потребуют- потребуются дополнительные усилия, вы непременно похвалите себя за строгое следова- следование этому принципу и оцените «элегантность» полученного кода. А в Огас1е9г Release 2 записи можно использовать даже в DML-инструкциях! О Создавайте записи на основе таблиц. Если в программе должны храниться данные, полученные из таблицы, создайте запись на базе таблицы (или вос- воспользуйтесь предопределенной записью). Старайтесь свести к минимуму ко- количество переменных и динамически связывайте структуры данных програм- программы со структурами данных РСУБД при помощи атрибута 8ROWTYPE. О Передавайте записи в качестве параметров. Вызываемым процедурам по воз- возможности передавайте не отдельные переменные, а целые записи. Тем самым вы уменьшите вероятность изменения синтаксиса вызова процедур, благодаря чему программный код станет более стабильным. Однако у этой технологии имеется и недостаток: если запись передается как параметр типа OUT или IN OUT, программа сохраняет значения ее полей при откате транзакции, что требует до- дополнительного объема памяти и увеличивает нагрузку на процессор. (Иногда в подобных случаях можно воспользоваться параметром NOCOPY, о котором рас- рассказывается в главе 16.) Объявление записей В PL/SQL существует три способа объявления записи. О Запись на основе курсора. Для объявления записи на основе явно заданного курсора или курсора, представленного переменной, где каждое поле соответ- соответствует столбцу или именованному выражению в инструкции SELECT, предна- предназначен атрибут XRDWTYPE. Вот пример объявления записи, которая имеет ту же структуру, что и явный курсор: DECLARE CURSOR my_books_cur IS SELECT * FROM books WHERE author LIKE 'JFEUERSTEIMT: one_SF_book my_books_curSROWTYPE: О Запись на основе таблицы. Для объявления записи, каждое поле которой со- соответствует значению одноименного столбца таблицы, используется атрибут XROWTYPE с именем таблицы. Приведем пример объявления записи one_book, ко- которая имеет ту же структуру, что и таблица books: DECLARE one book booksJROWTYPE;
Записи в PL/SQL 337 О 13апись, определяемая программистом. Для того чтобы создать запись с нуля, }то есть явно определить каждое поле, задав его имя и тип данных, нужно вос- воспользоваться оператором TYPE.. .RECORD. Значением поля записи, определяемой программистом, может быть не только скалярное значение, но и другая за- пись. В следующем примере определяется тип записи, содержащей некоторую информацию о писательской карьере автора книги, и объявляется экземпляр записи этого типа: DECLARE TYPE book_info_rt IS RECORD ( author books.authorHTYPE. category VARCHAR2A00). tota!_page_count POSITIVE); steven_as_author book_info_rt; ¦ Обратите внимание на тот факт, что в объявлении записи пользовательского типа атрибут fcROWTYPE отсутствует. Запись в данном случае имеет тип bookjin- fo_rt. Общий формат объявления атрибута IROWTYPE таков: иня_записи [имя_схемы. ]*ш_ойьек7\эОТкТУРЕ: Указывать параметр имя_схемы не обязательно (если он не задан, то для разре- разрешения ссылки применяется схема, внутри которой компилируется данный код). В качестве указанного параметра может служить явный курсор, курсорная пере- переменная, таблица, представление или их синоним. Вот пример создания записи на основе курсорной переменной: DECLARE ТУРЕ Ьоок_гс IS REF CURSOR RETURN booksSROWTYPE; book_cv book_rc; one_book book cvSROWYPE: BEGIN Запись можно объявить и неявно, например в цикле FOR с курсором или SQL-инструкцией. Ниже приведен блок кода, в котором раздел объявлений не содержит определения записи book_rec; PL/SQL автоматически объявляет запись с атрибутом IROWTYPE в цикле с использованием SQL-инструкции SELECT: BEGIN FOR book_rec IN (SELECT * FROM books) LOOP ca1culate_total_sales (book_rec): END LOOP: END; Поскольку применение оператора TYPE считается самым интересным и слож- сложным способом определения записи, рассмотрим его более подробно. Записи, определяемые программистом Записи, создаваемые на основе таблиц и курсоров, используются в тех случаях, когда в программе необходимо предусмотреть структуры для хранения опреде- определенных данных. Однако такие записи не охватывают всех потребностей програм- программистов в составных типах данных. Предположим, что программисту понадобится
Глава 11 • Записи и коллекции запись, структура которой не имеет ничего общего со структурой таблиц и пред- представлений. Придется ли ему создавать курсор только для того, чтобы получить запись нужной структуры? Конечно же, нет. В подобных ситуациях PL/SQL по- позволяет программисту самостоятельно определить структуру записи с помощью оператора TYPE... RECORD. Создавая запись самостоятельно, вы указываете количество ее полей, их име- имена и типы данных. Для объявления записи определяемого программистом типа нужно выполнить две операции: О с помощью оператора TYPE... RECORD определить тип записи, задав ее структуру; О на основе этого типа объявить реальные записи, которые затем можно будет использовать в программе. Определение типа записи Тип записи определяется с помощью оператора TYPE... RECORD, в котором задается имя новой структуры и входящие в нее поля. Общий синтаксис этого оператора таков: TYPE ш_типа IS RECORD <.иня_попя1 имя_поля2 ш_попяН )', Здесь тя_попяН - это имя ЛГ-го поля записи, а тип_ранныхН - как вы, надо полагать, догадались, тип данных указанного поля. Запись может иметь любой из перечисленных ниже типов: О жестко запрограммированный скалярный тип данных (VARCHAR2, NUMBER и т. д.); О какой-либо подтип, определяемый программистом; О тип, устанавливаемый на основе типа уже определенной структуры данных, например объявление с привязкой посредством атрибута 3ITYPE или XROWTYPE (в последнем случае мы создаем вложенную запись); О тип коллекции PL/SQt; поле записи может быть списком или даже коллекцией; О тип REF CURSOR, то есть поле содержит курсорную переменную. Вот как задается оператор TYPE... RECORD: TYPE company_rectype IS RECORD (comp# company. companyjoTTYPE, name company.nameiTYPE): Тип записи можно определить в разделе объявлений или в спецификации паке- пакета. Последний подход позволяет ссылаться на тип записи в любом блоке PL/SQL, откомпилированном внутри схемы, к которой принадлежит пакет, а также в бло- блоках PL/SQL, откомпилированных в схемах с разрешением EXECUTE на этот пакет. Объявление записи Создавая собственные пользовательские типы записей, вы можете использовать их в объявлении конкретных записей: имя записи тип записи:
Записи в PL/SQL Здесь имя_3дписи — это имя объявляемой записи, а тип_з алией — ее тип, определен- определенный в операторе TYPE... RECORD. Для того чтобы создать, скажем, запись с инфор- информацией о покупках клиента, сначала нужно определить ее тип: TYPE customer_sales_rectype IS RECORD (customerjd NUMBER E). customer name customer.nameSTYPE. total_saTes NUMBER A5,2) ); Запись представляет собой структуру, состоящую из трех полей, которые со- содержат первичный ключ, имя клиента и общую сумму его покупок. Используя данный тип, можно объявлять записи на его основе: prev custoraer_sales_rec customer_sales_rectype: tap_customer_rec customer_sales_rectype; Обратите внимание: для того чтобы показать, что это объявление записи, ат- атрибут XROWTYPE или какое-нибудь другое ключевое слово не требуется. Атрибут XROWTYPE нужен только для объявления записей, создаваемых на основе таблиц и курсоров. В определении типа записи для каждого поля наряду с его именем и типом можно задать значение по умолчанию. Для этой цели используется классический синтаксис DEFAULT или :-. Здесь же можно указать, что поле записи не должно быть пустым (NOT NULL) - в таком случае для него обязательно задается значение по умолчанию. Имя каждого поля записи должно быть уникальным. Примеры определения типа записи Предположим, что в нашей программе объявлены подтип, курсор и ассоциатив- ассоциативный массив1: DECLARE SUBTYPE long_Hne_type IS VARCHAR2C20Q0); CURSOR company_sales_cur is SELECT name, SUM (order_amount) total_sales FROM company c, order о WHERE с companyjid - o.companyjd; TYPE emp1oyee_ids_tabletype IS TABLE OF employee.employee_1d*TYPE INDEX BY BINARYJNTEGER: В том же разделе объявлений определены две записи. О Задаваемая программистом запись представляет подмножество столбцов таб- таблицы company и таблицу PL/SQL employeejdstabletype (для привязки полей записи к столбцам таблицы используется атрибут STYPE, третье поле записи является ассоциативным массивом с идентификационными кодами сотруд- сотрудников): TYPE companyj-ectype IS RECORD (companyj d company. companyjdSTYPE. Как будет рассказано ниже, при описании коллекций, ассоциативным массивом в Oracle9i называ- называется индексная таблица.
j«#u Глава 11 ¦ Записи и коллекции company jiame company.nameXTYPE. rew_hires_tab employee_1ds_tabletype); О Смешанная запись демонстрирует разные типы объявлений полей, а именно с ограничением NOT NULL, с использованием подтипа и атрибута ШРЕ, со значе- значением по умолчанию, с ассоциативным массивом и вложенной записью: TYPE mishmash_rectype IS RECORD (empjiumber NUMBER(IO) NOT NULL. pa ragra ph_text 1ong_li netype, company_nm company.name^TYPE. total_sales company_sales.total_salesJ;TYPE :- 0. new_hi res_tab employeej ds_tabletype.' prefers_nonsmoking_fl BOOLEAN :- FALSE. new_company_rec company_rectype ): Разнообразие способов создания записей в PL/SQL просто впечатляет. Записи могут включать таблицы, представления и инструкции SELECT. Как правило, они имеют произвольную структуру и содержат поля, представляющие собой другие записи и ассоциативные массивы. Обработка записей Независимо от того как вы определяете запись (на основе таблицы или курсора либо с помощью явного оператора TYPE. ..RECORD), приемы работы с нею всегда одинаковы. Причем обрабатывается либо вся запись как единое целое, либо каж- каждое из ее полей в отдельности. Операции над записями Обработка отдельной записи подразумевает выполнение над ней некоторых опе- операций как над единым целым. В настоящее время PL/SQL поддерживает следую- следующие операции над записями: О копирование содержимого одной записи в другую (если они имеют совмести- совместимую структуру, то есть одинаковое количество полей одного того же или взаи- мопреобразуемых типов); О присваивание записи значения NULL (посредством простого оператора присваи- присваивания); О передача записи функции в качестве аргумента; О возврат записи из функции (с применением оператора RETURN). Но на выполнение операций над отдельными записями существует ряд огра- ограничений. О Для того чтобы проверить, содержат ли все поля записи значение NULL, исполь- использовать синтаксис IS NULL нельзя. Осуществить такую проверку можно лишь путем применения оператора IS NULL по отношению к каждому полю. О Невозможно сравнить две записи при помощи единственной операции. В ча- частности, нельзя определить, равны или нет две записи (то есть значения всех их полей), или же узнать, какая из записей больше. К сожалению, для того чтобы ответить на эти вопросы, нужно сравнить каждую пару полей. Далее в этой главе, в разделе «Сравнение записей», рассказывается о том, как создать утилиту, автоматически генерирующую код для сравнения двух записей.
Записи в PL/SQL 341 О Только в Oracle9i Release 2 появилась возможность добавлять новые записи в таблицу базы данных целиком. В предыдущих же версиях системы значение каждого поля нужно было записывать в соответствующий столбец таблицы отдельно. Более полная информация об инструкциях языка DML, предназна- предназначенных для обработки записей, содержится в главе 13. Выполнять операции над отдельными записями можно лишь при условии, что они имеют совместимую структуру. Иными словами, у обрабатываемых записей должно быть одно и то же количество полей и эти поля должны иметь одинако- одинаковые или взаимопреобразуемые типы данных. Предположим, что создана такая таблица: CREATE TABLE cust_sales_roundup ( customer id NUMBER E3. customer'name VARCHAR2 A00). total_sales NUMBER A5.2) ); Три объявленные ниже записи имеют совместимую, структуру, и их можно ис- использовать в разных операциях: DECLARE cust_sales_roundup_rec cust_sales_roundupXROWTYPE: CURSOR cust_sales_cur IS SELECT * FROM cust_sales_roundup: cust_sales_rec cust_sales_curXRQWTYPE; TYPE customer sales_rectype IS RECORD (customeMd NUMBERE), customerjiame customer.nameXTYPE. total_sales NUMBERA5.2) prefererred_cust_rec customer_sales rectype; BEGIN -- Присвоим содеожиное одной записи другой. cust_sales_roundup_rec :- cust_sales_rec: prefererred_cust_rec :- cust sales_rec: END; Рассмотрим еще несколько примеров выполнения операций над отдельными записями. Ниже записи присваивается значение по умолчанию. Запись можно инициализировать при ее объявлении, присвоив содержимое другой, совместимой с ней записи. Сейчас же локальной переменной curr_company_rec присваивается запись, переданная процедуре в качестве аргумента. Теперь значения отдельных полей записи можно изменять: PROCEDURE compare_compan1es (prev_company_rec IN companyXROWTYPE) IS curr company_rec companyXROWTYPE :- prev company_rec: BEGIN END;
342 Глава 11 • Записи и коллекции В следующем примере сначала создается новый тип записи и объявляется за- запись этого типа. Затем создается второй тип записи, для которого в качестве типа столбца устанавливается первый тип. В определении данного типа столбец-за- столбец-запись инициализируется переменной-записью: DECLARE TYPE f1rst_rectype IS RECORD ( varl VARCHAR2U0Q) -.-'WHY NOT'); flrst_rec f1rst_rectype: TYPE second_rectype IS RECORD (nested'_rec first rectype -.-'first rec); BEGIN ENO: Разумеется, операцию присваивания можно осуществить и в разделе выпол- выполнения. Ниже объявляются две разные записи типа га in_forest_hi story и инфор- информация из первой из них присваивается второй записи: DECLARE prev_ra1n_forest_rec rai reforestJiistorySROKTYPE; curr_ra1 reforest rec rain_forest_history3!ROWTYPE; BEGIN ... Инициализация записи prev_rain_forest_rec ... -- Копирование данных первой записи во аторую curr_rain_forest_rec :- prev_rain_forest_rec; В результате выполнения такой операции значение каждого поля первой за- записи присваивается соответствующему полю второй записи. Значения полям мож- можно было бы присваивать и по отдельности, но, согласитесь, так гораздо удобнее. Поэтому старайтесь обрабатывать записи как одно целое — это позволит сэконо- сэкономить время, упростить программный код и облегчить его сопровождение. Для перемещения данных строки таблицы в запись достаточно одного опера- оператора FETCH с предложением INTO, в котором указывается эта запись. Вот несколько примеров выполнения такой операции: DECLARE /* || Объявляем курсор, а затем при покощи атрибута «R0WYPE || определяем запись на его основе */ CURSOR cust_sales_cur IS SELECT customerjd, name. SUM (total_sales) tot_sales FROM cust_sales_roundup WHERE sold_on < ADD_MONTHS CSYSDATE". -3) . GROUP BY customerjd. name; cust_sales_rec cust_sales_curtROWTYPE: BEGIN /* Перемещаем значения из курсора непосредственно в запись */
Записи в PL/SQL 343 OPEN cust sales_cur; FETCH cust_sales_cur INTO cust_sales_rec: В следующем блоке кода сначала задается тип записи, который соответствует данным, возвращаемым неявным курсором, а затем производится извлечение дан- данных (SELECT) непосредственно в запись: DECLARE TYPE customer sales j-ectype IS RECORD (customerJd NUMBER E), customer_name customer.namelTYPE. total_sales NUMBER A5,2) ); top_customer_rec customerjal es_rectype: BEGIN ~ /* Пересылаем значения непосредственно а запись */ SELECT customerjd, name, SUM (total_sales) INTO top_customer_rec FROM cust_salesj-oundup WHERE sold_on < ADD_MONTHS (SYSDATE, -3) GROUP 8Y customerjd. name: Как уже было сказано, можно объявить функцию, возвращающую запись. Следующий пример показывает, как это делается, а к тому же демонстрирует воз- возможность «обнуления» записи — присваивания ей значения NULL: CREATE OR REPLACE FUNCTION bestseller ( weekjn IN PLSJNTEGER. yearjn IN PLSJNTEGER) RETURN booksSROWTYPE IS returnj/alue booksfcROWTYPE; BEGIN SELECT * INTO return value FROM books В WHERE weskjiumber - weekjn AND year - yearjn AND sales - (SELECT MAX (sales) FROM book sales BS WHERE BS.Isbn - B.isbn AND weekjnumber - weekjn AND year - yearjn); RETURN return value; EXCEPTION WHEN NOJATAJOUND OR TOO_MANY_ROWS THEN -- Овеспечиваен возврат значения NULL return_value :- NULL; RETURN return_value; END bestseller; По возможности старайтесь работать с записями на агрегатном уровне, то есть обрабатывать каждую из них как единое целое, не разбивая на отдельные поля. Благодаря этому результирующий код получится более простым, понятным и ста- стабильным. Конечно, существует множество ситуаций, когда необходимо получить доступ лишь к отдельным полям записи, и мы расскажем, как это делается.
344 Глава 11 • Записи и коллекции Операции над отдельными полями Чтобы получить доступ к отдельному полю записи (для выполнения операции чтения или изменения), используйте точечную нотацию, такую же, как при иден- идентификации столбца таблицы базы данных. Приведем ее синтаксис: [иня_схемы.][имя_пакета.]имя_записи.иня_поля Имя пакета нужно указывать только в том случае, если запись определена в спецификации другого пакета (не того, с которым вы работаете в настоящее время). А имя схемы потребуется определить лишь при условии, что пакет при- принадлежит не той схеме, где компилируется данный код. Идентифицировав поле записи посредством точечной нотации, хранящееся в нем значение можно обрабатывать точно так же, как любую другую перемен- переменную. Рассмотрим несколько примеров. Оператор присваивания :- позволяет изменить значение в конкретном поле записи. В первом примере в поле total_sales записи top_customer_rec заносится значение 0. Во втором примере вызывается функция check_report_status, которая возвращает значение флага outputgenerated (ему присваивается либо TRUE, либо FALSE): BEGIN top_custofrer_rec. totalsales : - 0; report_rec.output_generated ;= check_report_status (reportj-ec. reportjd): END: Код следующего примера создает запись на основе таблицы rain_forest_histo- rain_forest_history, записывает в ее поля значения и возвращает запись в ту же таблицу: DECLARE ¦ rain_forest_rec ra1 nforesthi story^ROWTYPE: BEGIN /* Установка значения полей записи */ rain_forest_rec.country_code :- 1005: rain_forestjrec.analysis_date :- ADD_MONTHS (TRUNC (SYSDATE), -3); rain_forest_rec.size_in_acres :- 32: ra1n_forest_rec.species_lost :- 425; /* Вставка в таблицу строки значений из записи */ INSERT INTO га i n_fо res t_h i s t?гу (courtrycode. analysis_date, size in_acres. spedesjost) VALUES t rai n_forest_rec.country_code. ra1n_forestj-ec.anaiysis_date. ra1n_forest_rec.sizej n_acres. rain_forest_rec.species_1ost): END;
Записи в PL/SQL 345 Обратите внимание, что в поле analysis_date можно записать любое значение или выражение, которое имеет тип DATE. Сказанное касается и всех других полей, а также более сложных структур. PL/SQL позволяет создавать структуры вложенных записей, то есть такие, в которых одно из полей внешней записи представляет собой отдельную запись. В следующем примере сначала определяется тип записи, в которую заносятся фрагменты номера телефона (phone_rectype), а затем - тип записи, в которой объе- объединяются в единую структуру contact_set_rectype несколько телефонных номе- номеров одного человека: DECLARE TYPE phone_rectype IS RECORD (intljjrefix VARCHAR2B), area_code VARCHAR2C), exchange VARCHAR2O). phnjiumber VARCHAR2D), extension VARCHAR2D) ): TYPE contact_set_rectype IS RECORD Cday_phone# phone_rectype, /* Вложенная запись */ eve_pnone# phone__rectype, /* Вложенная запись */ fax_phone# phone_rectype, /* Вложенная запись */ home_phone# phone_rectype. /* Вложенная запись */ cell_phone# phone_rectype /* вложенная запись */ ): .auth_rep_info_rec contact_set_rectype; BEGIN END; Для ссылки на поля вложенной записи используется точечная нотация, при- причем таким образом можно обращаться к полям любого уровня вложенности. Вло- Вложенные структуры необходимо последовательно перечислить, разделив их точка- точками. В приведенном ниже фрагменте кода в поле номера факса заносится содер- содержимое поля домашнего телефона (что, конечно же, является ошибкой): auth_rep_irrfo_rec.fax_phone#.area_code :- auth_rep_1nfo_rec.home_phone#.area_code; Следующий пример демонстрирует принцип использования ссылок на записи и типы записей, объявленные в пакете. Предположим, что пользователь форми- формирует список книг, которые он планирует прочесть за время летнего отпуска. Для этой цели создается следующая спецификация пакета: CREATE OR REPLACE PACKAGE summer IS TYPE reading_list_rt IS RECORD ( favorite_author VARCHAR2 A00). title VARCHAR2 A00). fin1sh_by DATE);
346 Глава 11 • Записи и коллекции must_read read1ngJ1st_rt; w1fes_favor1te read1ng_Mst_rt; END SLinmer; CREATE OR REPLACE PACKAGE BODY summer IS BEGIN -- Раздел инициализации must_read,favor1te_author :- 'Tepper. SheM S.'; must~read.t1tle :- 'Gate to WomerT's Country': END summer; После компиляции такого пакета формируется список литературы: DECLARE ftrst_book summer.reading_l1st_rt; second book summer.readi ng 11st rt: BEGIN summer.must_read.f1n1sh_by ;- T0_DATE CO1-AUG-2002'. 'DD-MON-YYYY'); f1rst_book :- summer.must_read; secorid_book.favor1te_author ;- 'Morris. Benny'; second book.title :-~'R1ghteousV1ct1ms'; second"book.f1n1sh_by :- TO_DATE ('01-SEP-20021. ¦DD-MON-YYYY'); END; Далее объявляются две записи для представления информации о книгах. Сна- Сначала устанавливается значение поля finish_by объявленной в пакете summer запи- записи must_read (обратите внимание на синтаксис пакет.запись.попе), затем запись присваивается переменной, представляющей первую книгу из числа запланиро- запланированных для чтения. После этого значения присваиваются отдельным полям запи- записи, относящейся ко второй книге. Кстати говоря, при работе со встроенным пакетом UTL_FILE, предназначенным для файлового ввода-ввода, необходимо следовать тем же правилам обработки записей. Объявление типа данных UTL_FILE.FILE_TYPE — это объявление типа запи- записи. Вот почему, объявляя дескриптор файла, вы на самом деле объявляете запись: DECLARE my_file_1d UTLJILE.FILEJYPE: Сравнение записей Ну а как узнать, равны ли две записи, то есть совпадают ли их соответствующие поля? Было бы неплохо, если бы PL/SQL позволял выполнять непосредственное сравнение. Например: DECLARE flrst_book sumer. readi ng_l1st_rt :- summer.must_read; second_book summer.reading 11st rt :- summer.wifes favorite: BEGIN IF first book - second book THEN lots to talk_about; END IF;" END;
Коллекции в PL/SQL 347 Но, к сожалению, это невозможно. Для сравнения двух записей требуется на- написать код для сравнения всех их полей по отдельности. Если количество полей невелико, сделать это довольно просто. Например, в случае записей, рассмотрен- рассмотренных в предыдущем примере, сравнение выполняется следующим образом: DECLARE first book suimer.reading_11st_rt :- summer.must_read; second book summer.reading_Hst_rt :• summer.w1fes_favor1te: BEGIN [F first book.favorite author - sECond_book,favorlte_author AND first book.title - secondjrcok.title AND first book.finish by - second_book.f1n1sh_by THEN lots_to talk about: END IF; END; Однако здесь возникает одна проблема. Если вы хотите указать, что две запи- записи со значениями NULL будут равны между собой, оператор сравнения нужно мо- модифицировать следующим образом: (first book.favorite author - second_book.favorite_author ORT f1rst_book.favorite_author IS NULL AND second_book.favor1te_author IS NULL) Как видите, код получился довольно громоздким. А правда, было бы неплохо сгенерировать его автоматически? Оказывается, это и в самом деле возможно — по крайней мере для тех записей, которые определены с атрибутом SROWTYPE на ос- основе таблиц или представлений. В таком случае имена полей можно получить из представления ALL_TAB_COLUMNS в словаре данных. А готовый генератор кода для сравнения записей, разработанный Дэном Спен- Спенсером (Dan Spenser), вы найдете на узле издательства O'Reilly в файле gen_re- cord_comparlson.pkg. Коллекции в PL/SQL Коллекция - это составной тип данных, предназначенный для хранения одномер- одномерных массивов в программах PL/SQL. Что это означает? Коллекции используются Для хранения множества однотипных элементов в коде PL/SQL или в столбцах базы данных. Ниже перечислено несколько операций, при выполнении которых наиболее целесообразно использовать коллекции. О Эмуляция двунаправленных курсоров и курсоров с произвольным доступом. PL/SQL позволяет извлекать записи курсора только последовательно. Но если загрузить результирующий набор строк курсора в коллекцию, то можно будет перемещаться между записями в любом направлении и обращаться к каждой строке курсора произвольное количество раз,
348 Глава 11 • Записи и коллекции О Хранение списков подчиненной информации непосредственно в столбцах таб- таблицы — в виде вложенной таблицы или массива VARRAY. (О вложенных табли- таблицах, структурах VARRAY и ассоциативных массивах рассказывается далее в этой главе, в разделе «Типы коллекций».) Применение коллекций позволяет за- заметно повысить производительность поиска. О Отслеживание элементов данных, отобранных в программе для специальной обработки. 0 Кэширование статичной информации базы данных, которая часто указывает- указывается в запросах (коллекции позволяют повысить быстродействие запросов). В следующих разделах рассказывается о принципах создания и о применении типов коллекций в базах данных и в программах PL/SQL, приводится синтаксис используемого при этом кода. Вы познакомитесь с тремя способами инициализа- инициализации коллекций, а также со встроенными методами управления их содержимым, такими как NEXT, DELETE и TRIM. И хотя охватить все аспекты использования кол- коллекций в SQL (например, в качестве столбцов таблицы) невозможно, рассмот- рассмотренные примеры покажут вам, как важны и полезны эти структуры, несмотря на свою сложность. Пример простой коллекции Структура и синтаксис коллекций гораздо сложнее структуры и синтаксиса ска- скалярных значений типа даты и числа. Рассмотрим концепцию и терминологию коллекций на очень простом примере. 1 DECLARE 2 TYPE list_of_dates_t IS TABLE OF DATE; 3 4 TYPE list_of_names_t IS TABLE OF VARCHAR2 A00) 5 INDEX BY BINARYJNTEGER: 6 7 birthdays list of_dates_t 8 := Hst_of_dates_t (): 9 happyfamily list_of__names_t; 10 BEGIN 11 birthdays.EXTEND; 12 birthdays A) :- '23-SEP-19581: 13 birthdays.EXTEND; 14 birthdays B) : = '01-OCT-1986';- 15 16 happyfamily (-15070) :- 'Steven'; 17 happyfanily (88) :• 'Veva': 18 happyfamily (909) :- 'Chris1: 19 happyfamily B020202020) ;- 'Eli'; 20 21 DBMSJUTPUT.putJine (birthdays.COUNT); 22 DBMSJWTPUT.putJine (happyfamily.FIRST): 23 END: Вот что выводит данная программа: 2 -15070
Коллекции в РЦ/SQL 349 А выполняемые ею действия перечислены в приведенной ниже таблице. Строки Описание 2-5 Определения типов коллекций. Для работы с коллекцией (списком) нужно определить ее тип, а затем объявить конкретные коллекции этого типа. В данном примере создаются два типа коллекций: вложенная таблица (в строке 2) и ассоциативный массив (в строках 4, 5) 7, 8 Объявление реальной вложенной таблицы, Обратите внимание, что вложенная таблица инициализируется вызовом конструктора — функции с тем же именем, что и у типа таблицы, автоматически генерируемой PL/SQL 9 Объявление ассоциативного массива, Для коллекции данного типа конструктор не нужен 11-14 С помощью стандартного оператора присваивания задаются две строки вложенной таблицы. Обратите внимание, что место для арок необходимо явно выделить с помощью метода EXTEND. Если этого не сделать, в результате выполнения подобной операции присваивания будет сгенерирована ошибка. Обратите также внимание, что строки вложенной таблицы birthdays заполняются последовательно 16-19 Четырем элементам коллекции happyfamily присваиваются строковые значения. Ассоциативный массив не нужно расширять явно, при помощи метода EXTEND. Достаточно присвоить значение строке с заданным номером, и таковая появится в коллекции. Обратите внимание, что строки добавляются не последовательно. Фактически их номера выбираются случайным образом и могут быть как положительными, так и отрицательными числами. Это пример разреженной коллекции 21 Определение с помощью метода COUNT количества строк, содержащихся во вложенной таблице birthdays 22 Вызов метода FIRST для определения первого или наименьшего номера строки в коллекции happyfamily Типы коллекций Oracle поддерживает три типа коллекций. О Ассоциативные массивы. Это одномерные неограниченные разреженные кол- коллекции, состоящие из однородных элементов, которые можно обработать толь- только в PL/SQL. Ранее, в PL/SQL Release 2, они назывались таблицами PL/SQL, а в Oracle8 и Oracle8i - индексными таблицами, поскольку, объявляя такую коллекцию, приходилось явно указывать, что она «индексируется» номером строки. Теперь, в Огас1е9г, они называются ассоциативными массивами. Этот термин выбран потому, что предложение INDEX BY может использоваться для индексирования содержимого коллекций посредством значений типа VARCHAR2 или PLSJNTEGER. О Вложенные таблицы. Так называются одномерные, несвязанные коллекции, также состоящие из однородных элементов. Первоначально они заполняются полностью, но позднее, из-за удаления некоторых элементов, могут стать раз- разреженными. Вложенные таблицы можно определять и в PL/SQL, и в базах данных (например, в качестве столбцов таблиц). О Массив типа VARRAY. Подобно двум другим типам коллекций, массивы VAR- VARRAY (массивы переменной длины) являются одномерными коллекциями, состоя- состоящими из однородных элементов. Однако их размер всегда ограничен, и они не
350 Глава И • Записи и коллекции могут быть разреженными. Подобно вложенным таблицам, массивы VARRAY ис- используются и в PL/SQL, и в базах данных. Однако порядок их элементов при сохранении и извлечении, в отличие от вложенных таблиц, сохраняется. Терминология, используемая при работе с коллекциями С коллекциями связан целый ряд специфических терминов. О Коллекция. Этот термин может иметь несколько значений; ¦ • вложенная таблица, ассоциативный массив или массив типа данных VARRAY; • переменная PL/SQL типа вложенной таблицы, ассоциативного массива или VARRAY; • столбец таблицы, где хранятся значения типа вложенной таблицы или мас- массива VARRAY. Однако независимо от конкретного использования коллекция всегда остается списком элементов. О Одномерная коллекция. В каждой строке коллекции имеется всего одно поле, и с этой точки зрения она похожа на одномерный массив. Нельзя объявить кол- коллекцию, на элементы которой можно было бы ссылаться следующим образом: myjable A0. 44) Коллекция ny_tab1e A0. 44) - это двухмерная структура, но в настоящее вре- время такие структуры не поддерживаются. О Однородные элементы. Коллекция может содержать только один столбец, и этот столбец должен иметь тип данных, указанный в определении типа кол- коллекции. Поэтому все строки коллекции содержат значения одного типа, а сле- следовательно, коллекция является однородной. Однако тип данных столбца мо- может быть составным; например, можно объявить коллекцию, состоящую из записей, то есть таблицу. А в Oracle9i можно объявлять и многоуровневые коллекции: полем коллекции служит коллекция или запись, полем в которой, в свою очередь, также является коллекция или запись и т. д. О Ограниченная и неограниченная коллекции. Коллекция называется ограни- ограниченной, если заранее определены границы возможных значений индексов, или номеров, ее элементов. Если же верхняя или нижняя граница номеров элемен- элементов не указана, коллекция называется неограниченной. Коллекции типа VARRAY (массивы переменной длины) всегда ограниченны. При определении такой коллекции следует указать максимальное количество ее элементов (номер первого элемента всегда равен 1). Вложенные таблицы и ассоциативные мас- массивы мы определяем как неограниченные, поскольку максимальное количест- количество элементов в них ограничивается только теоретически. Границы индекса ассоциативного массива, индексируемого посредством пере- переменной типа BINARYINTEGER, таковы: наименьшим значением является -231 + 1, а наибольшее значение не превышает 231 - 1. Границы индекса вложенной таб- таблицы изменяются в пределах диапазона от 1 до 231 - 1. Это означает, что в ас- ассоциативном массиве можно определить приблизительно 4,3 млрд. элементов, а во вложенной — 2,14 млрд. элементов. Но на самом деле количество элементов во вложенных таблицах и ассоциативных массивах (в указанных структурах)
Коллекции в PL/SQL 351 ограничивается техническими возможностями системы - объемом памяти компьютера. Вот почему ассоциативные массивы и вложенные таблицы при- принято считать неограниченными структурами. О Разреженные и плотные коллекции. Коллекция (или массив, или список) на- называется плотной, если все ее элементы, от первого до последнего, определены и каждому из них присвоено некоторое значение (таковым может быть и NULL). Коллекция считается разреженной, если отдечьные ее элементы отсутствуют, как в рассмотренном выше примере. Не обязательно определять все элементы коллекции и заполнять ее полностью. Массивы VARRAY всегда являются плот- плотными. Вложенные таблицы первоначально всегда плотные, но по мере удале- удаления некоторых элементов становятся разреженными. Ассоциативные массивы могут с самого начала быть разреженными, что зависит от способа их заполнения. (Оказывается, разреженность — это очень ценное свойство, позволяющее до- добавлять элементы в коллекцию, используя первичный ключ или другие клю- ключевые данные, например номер записи. Таким образом задается порядок дан- данных в коллекции, что значительно ускоряет их поиск.) О Коллекция, индексированная целочисленными значениями. К любому эле- элементу коллекции можно обратиться, указав его номер, который представляет собой целочисленное значение. Создавая коллекцию типа ассоциативного мас- массива, следует явно указать, посредством каких индексов — номеров записей или символьных значений — можно получить доступ к ее элементам. В качест- качестве индексов коллекций двух других типов используются только номера строк. О Коллекция, индексированная строками. В Oracle9i Release 2 в качестве ин- индексов ассоциативных массивов можно использовать не только номера строк, но и символьные значения. Эта возможность не поддерживается ни для вло- вложенных таблиц, ни для массивов VARRAY. О Внешняя таблица. Так называют таблицу, содержащую столбец типа вложен- вложенной таблицы или массива VARRAY. О Внутренняя таблица. Так принято называть коллекцию, содержащуюся в столб- столбце таблицы. О Вспомогательная таблица. Физическая таблица, создаваемая Oracle для хра- хранения внутренней таблицы (столбца, содержащего вложенную таблицу или массив VARRAY). Использование коллекций Опыт показывает, что лишь немногие разработчики знают о коллекциях и доста- достаточно активно их используют. Это удивительно, поскольку речь идет об исклю- исключительно удобных структурах. Единственным их недостатком является относи- относительная сложность. Наличие трех типов коллекций, необходимость выполнения нескольких действий для создания каждой из них, использование коллекций и в программах PL/SQL, и в качестве объектов базы данных, более сложный по срав- сравнению с обычными переменными синтаксис — все эти факторы, к сожалению, Уменьшают интерес программистов к коллекциям. Авторы пытались построить эту главу таким образом, чтобы рассказать о кол- коллекциях как можно подробнее и вместе с тем избежать повторений одной и той
352 Глава ц • записи и коллекции же информации, относящейся к разным типам коллекций. Тем не менее глава по- получилась довольно объемной. Вот краткий перечень тем, рассматриваемых в ос- оставшейся ее части. Вначале мы покажем, как определяются типы (или шаблоны) коллекций и как создаются конкретные экземпляры коллекций этих типов. Да- Далее будут рассмотрены многочисленные встроенные функции и методы, предос- предоставляемые Oracle для анализа и обработки содержимого коллекций. Полученные в результате знания позволят вам понять основные особенности работы с коллек- коллекциями, в том числе изучить процесс инициализации вложенных таблиц и масси- массивов VARRAY, способы заполнения коллекций и доступа к их данным. Завершается глава рассказом о псевдофункциях, которые позволяют работать с коллекциями как с таблицами реляционной базы данных. Здесь же будут приведены советы по выбору наиболее подходящего типа коллекции. Определение типов коллекций и их объявление Приступая к работе с коллекцией, таковую нужно объявить, указав прежде всего ее тип. Существует два способа задания типа коллекции. О Во-первых, с использованием команды CREATE TYPE. Так определяется тип вло- вложенной таблицы и тип VARRAY в базе данных, что позволяет использовать его для объявления столбцов таблиц базы данных, переменных в программах PL/SQL, а также атрибутов объектных типов. О Во-вторых, с применением синтаксиса TYPE...IS. Так определяется тип кол- коллекции в программе PL/SQL, и в этом случае он доступен лишь в том блоке, где был декларирован. Объявление ассоциативного массива Как и запись, ассоциативный массив объявляется в два этапа. О Сначала при помощи оператора TYPE.. .TABLE определяется конкретная струк- структура ассоциативного массива (состоит из строк, дат и т. п.). В результате соз- создается тип данных, который можно использовать в операторах объявления. О Затем на основе этого типа объявляется собственно коллекция, то есть созда- создается конкретный экземпляр определенного типа данных — ассоциативный массив. Определение ассоциативного массива Оператор для определения ассоциативного массива TYPE имеет следующий формат: TYPE иня_гипа_табти]ы IS TABLE OF тип_данных [NOT NULL] INDEX BY [BINARYJNTEGER | подтип_типа BINARYJNTEGER | VARCHAR2 (.максинальиый_рдзмер)У. Здесь имя_типа_таблицы — это имя создаваемой табличной структуры, а тип_двн- ных - это тип данных единственного столбца таблицы. При желании можно доба- добавить ограничение NOT NULL, указывающее, что каждая строка таблицы должна со- содержать значение. До Огас1еЭг Release 2 единственным способом определения индекса ассоциа- ассоциативного массива был такой: INDEX BY BINARY INTEGER
Коллекции в PL/SQL 353 Теперь этот индекс можно определить и по-другому: INDEX BY PLSJNTEGER INDEX BY NATURAL INDEX BY POSITIVE INDEX BY \1Ш:1Ш2(.максимальный_размер) INDEX BY таблица.столбец%ТУРЕ INDEX BY пакет. ларежениаяШРЕ INDEX BY подтип В этом объявлении все типы и подтипы должны быть совместимыми с типом данных VARCHAR 2. Присвоение имен табличным типам производится в соответствии с теми же правилами именования, которые используются для любых других идентифика- идентификаторов PL/SQL: имя должно начинаться с буквы, иметь длину до 30 символов, мо- может содержать некоторые специальные символы, например символ подчеркивания и знак доллара. Более подробно об этом рассказывается ниже, в разделе «Исполь- «Использование ассоциативных массивов типа VARCHAR2». Для поля табличного типа можно задать один из двух типов данных: О скалярный тип данных — любой поддерживаемый PL/SQL скалярный тип, в том числе VARCHAR2, CLOB, POSITIVE, DATE и BOOLEAN; О тип данных с привязкой - определяется ранее объявленной переменной или курсором, заданным при помощи атрибута ЖТУРЕ. Вот несколько примеров определения типов ассоциативных массивов: TYPE company_lceys_tabtype IS TABLE DF company. сотрапу_1аШРЕ NOT NULL INDEX BY BINARYJNTECER: TYPE booklist_tabtype IS TABLE OF bookslROWTYPE INDEX BY BINARYJNTEGER: TYPE books_by_author_tabtype IS TABLE OF booksSROWTYPE INDEX BY VARCHAR2A00); Объявление коллекции Создав табличный тип, вы можете ссылаться на него в объявлениях конкретных коллекций. Общий формат объявления коллекции таков: иня^коллекции табличный_тил; Здесь имя_ноллекции — это имя объявляемой коллекции, а табличный_тип — имя ра- ранее созданного табличного типа. В следующем примере объявляется табличный тип для хранения первичных ключей таблиц с названиями компаний и именами сотрудников и на его основе создаются две коллекции первичных ключей: CREATE OR REPLACE PACKAGE company_pkg /* Объявление табличного типа дня первичных кличей */ TYPE primary keysjabtype IS TABLE OF NUMBER NOT NULL INDEX BY BINARYJNTEGER: I* Создание двух коллекций на основе табличного типа */ eompany_keys_tab primary_keys_tabtype; emp_keys_tab pnmary_keys_tabtype. END company_pkg:
354 Глава 11 • Записи и коллекции Объявление вложенной таблицы или массива VARRAY Перед объявлением любой вложенной таблицы или массива VARRAY, равно как пе- перед объявлением ассоциативного массива, нужно определить соответствующий тип. Его можно задать либо в базе данных, либо в блоке PL/SQL. Тип вложенной таблицы, который можно использовать в базе данных (а не только в программе PL/SQL), создается следующим образом: CREATE [OR REPLACE] TYPE имя_тнпа AS | IS TABLE OF тип_панных_эленента [NOT NULL]; Тип данных VARRAY, который будет доступен в базе данных (а не только в про- программе PL/SQL), создается так: CREATE [OR REPLACE] TYPE иня_тш AS ] IS VARRAY (.наксимальный_индекс) OF т\лп_даиных_эленента [NOT NULL]: Для удаления любого типа данных необходимо выполнить такую команду: DROP TYPE имя_типа [FORCE]: А вот как в программе PL/SQL создается тип вложенной таблицы: TYPE иня_типа IS TABLE OF тип_#анных_элемента [NOT NULL]: и тип данных VARRAY: TYPE имя_типа IS VARRAY (максииальныйиндекс) OF TKnjaHHkix_3neneHTa [NOT NULL]: Опишем параметры, используемые при определении типов данных. О Опция OR REPLACE позволяет повторно определить существующий тип данных, если от него не зависят никакие другие объекты базы данных. Это достаточно удобный прием, особенно, когда необходимо сохранять разрешения на выпол- выполнение операций замены. О Параметр иня_типа — это допустимый идентификатор SQJL. или PL/SQL, кото- который позднее будет использоваться в объявлениях переменных и столбцов. О Параметр тип_данных__элементд определяет тип данных элементов коллекции. Все элементы имеют один и тот же тип. Это может быть почти любой скаляр- скалярный или объектный (в том числе REF) тип данных. Если элементы являются объектами, объектный тип не должен иметь атрибутов, представляющих со- собой коллекции. Когда в PL/SQL элементами коллекции являются записи (RE- (RECORD), поля таких записей могут быть только скалярными элементами или объектами. Некоторые типы данных, в том числе BOOLEAN, NCHAR, NCLOB, NVAR- CHAR2, REF CURSOR, TABLE и VARRAY, в коллекциях не используются вообще. О Опция NOT NULL указывает, что переменная данного типа не должна содержать пустых элементов. Однако вся коллекция при этом может быть равна NULL (то есть не инициализирована). О Параметр максимальный_инрекс - это значение, определяющее максимальное ко- количество элементов в массиве VARRAY. Изменить его после объявления невозможно. О Опция FORCE предписывает Oracle удалить тип данных даже в том случае, ко- когда на него ссылается другой тип. Например, если в определении объектного типа указывается тип коллекции, его можно удалить, добавив в соответствую- соответствующую команду ключевое слово FORCE.
Коллекции в PL/SQL 355 Обратите внимание, что единственным различием в синтаксисе определения типов вложенной таблицы и ассоциативного массива является отсутствие в пер- первом случае предложения INDEX 8Y. Синтаксис определения массива VARRAY отлича- отличается от синтаксиса определения типа вложенной таблицы наличием ключевого слова VARRAY и необходимостью ограничивать количество элементов. Примеры объявления вложенных таблиц и массивов VARRAY Вот несколько примеров объявления в PL/SQL вложенных таблиц. Сначала в ба- базе данных создается тип вложенной таблицы: CREATE OR REPLACE TYPE Color_tab_t AS TABLE OF VARCHAR2C30): Затем объявляется несколько переменных PL/SQL Причем использовать толь- только те типы, которые определены в базе данных, не обязательно. Можно опреде- определить только локальные типы, а можно - и локальные и глобальные: DECLARE -- Переменная для хранения списка цветов шрифта /* В следующей переиеннай позднее будет храниться временная || копия font_colors. Обратите внииание. что для ссылки на тип || данных переменной font_colors можно использовать || спецификацию ШРЕ. Ниже представлены два разных способа || обьявления переменных типа Color_tab_t. */ font_co1ors_save forvt_colors*TYPE: -- Переменная для хранения списка цветов для рисования paintjnixture Color_array_t: /* Как и для индексных таблиц 0гас1е7. можно определить || табличный тип данных прямо здесь, в разделе объявлений... */ TYPE NumberJ IS TABLE OF NUMBER: /*... и тогда можно будет использовать новый тип в объявлении || локальной переменной. В следующей строке переменная || объявляется и инициализируется одновременно. Обратите I| внимание на использование конструктора || Numbet_t(значение, значение, ...). расположенного справа от оператора присваиваний */ my_favorite_numbers NumberJ: :- Number_tD2. 65536): /* Можно также сослаться на тип данных Color_tab_t из | словаря данных. В следующей строке объявляется локальная II переменная my_favorite_colors. которая инициализируется || двумя элементами с помощью предоставляемого Oracle || конструктора. */ my_favorite_colors Color_tab_t :- Color tab t('PURPLE'. 'GREEN')' END:
356 Глава 11 ¦ Записи и коллекции Этот код демонстрирует принцип использования конструкторов типов — спе- специальных функций, которые Oracle автоматически предоставляет для каждого созданного программистом типа. Конструктор служит для инициализации (или заполнения) переменной соответствующего типа. Его имя совпадает с именем типа, а аргументами являются значения разделенного запятыми списка элементов. Подробнее об этом рассказывается в разделе «Инициализация переменных-кол- переменных-коллекций». Использование коллекций В следующих разделах описываются особенности объявления и применения кол- коллекций в различных фрагментах программ PL/SQL. Поскольку типы коллекций можно определять в базе данных (лишь вложенные таблицы и массивы VARRAY), эти структуры можно задействовать не только в программах PL/SQL, но и в таб- таблицах и объектных типах. Коллекции как компоненты записи Коллекции могут использоваться в записях наряду с другими типами данных. В состав записи может входить одна или несколько таких структур, включая мас- массивы VARRAY, вложенные таблицы и ассоциативные массивы. Например: DECLARE TYPE toy_rec_t IS RECORD ( manufacturer INTEGER. sh1pping_weight_kg NUMBER, domestic_colors Color_array_t, international_colors Color_tab_t ): Тип данных RECORD нельзя использовать в базе данных — он предназначен толь- только для программ PL/SQJL Однако в базе данных его нетрудно заменить объектным типом. В состав объектного типа также входит набор атрибутов, которые могут быть коллекциями (вложенными таблицами или массивами VARRAY). А элементы коллекций, в свою очередь, могут быть объектами. Подробнее об объектных ти- типах рассказывается в главе 21. Коллекции в качестве параметров программы Коллекции могут использоваться как параметры программных модулей. Однако в этом случае невозможно получить пользовательский тип данных, определен- определенный внутри модуля. Чтобы использовать этот тип данных, его необходимо объя- объявить вне модуля - либо с помощью оператора CREATE TYPE, либо с помощью обще- общедоступного объявления в пакете. Следующая функция объединяет два набора цветов, заданных входными параметрами типа color_type_t. Значение выходного параметра представляет собой результат объединения наборов цветов, заданных двумя входными параметрами. Полная реализация этой функции имеется в фай- файле make_colors_superset.sp на узле издательства O'Reilly. CREATE PROCEDURE nake_colors_superset Cfirst_colors IN Color_tab_t. second_colors IN Color_tab_t, superset OUT Color_tab_t)
Коллекции в PL/SQL 357 Вызвать ее можно следующим образом: DECLARE my_colors Color_tab_t: your_colors Color_tab_t: our_colors Color_tab_t: 8EGIN malce_colors_superset ( n\y_colors, your_colors. our_colors ): END: За дополнительной информацией по этой теме вам следует обратиться к раз- разделу данной главы «Передача ассоциативных массивов в качестве параметров». Коллекции как типы данных для значений, возвращаемых функцией Ниже приведен пример функции, возвращающей значение типа color_type_t. Этот же тип имеет и локальная переменная данной функции. В отношении возвращае- возвращаемых функциями значений действует то же правило, что и в отношении их пара- параметров: пользовательские типы данных должны быть определены вне модуля. CREATE FUNCTION true_colors (whose_id IN NUMBER) RETURN Color_tab_t AS l_colors Color_tab_t; BEGIN SELECT favorite_colors INTO l_colors FROM personality_inventory WHERE personnel - whose_id: RETURN 1 colors: EXCEPTION WHEN NOJATAJOUND THEN RETURN NULL: END: Этот пример демонстрирует давно назревшую и наконец-то реализованную возможность возврата сложных структур данных. Чуть позже мы еще вернемся к нему. Вы наверняка спросите, как же эта функция используется в программе PL/SQL. Поскольку она способна заменить переменную типа color_type_t, то вы можете либо присвоить возвращаемое ею значение переменной типа коллекции, либо присвоить переменной (тип которой совместим с типом элементов коллек- коллекции) один из элементов возвращаемой коллекции. Первый способ весьма прост. Однако обратите внимание, что это один их тех случаев, когда переменную коллекции не обязательно инициализировать явно: DECLARE со1ог_аггау Color_tab_t; BEGIN
358 Глава 11 • Записи и коллекции со1ог_аггау :=¦ true_colors (8041); ENO; В соответствии со вторым способом сразу после вызова функции необходимо задать индекс требуемого элемента коллекции. Общий синтаксис оператора при- присваивания в данном случае имеет такой вид: перенениая_тилд_элеменга := функция (,) (иедекс): Вот пример вызова функции true_colors: DECLARE one_of"_my_favor1te_colors VARCHAR2C30); BEGIN one_of_my_favorite_colors := true_colors (whose_1d=> B041X1): END; Правда, этот код имеет один существенный недостаток: если в таблице базы данных не окажется записи, в столбце person_id которой содержится значение 8041, то в ответ на попытку считать первый элемент результирующей коллекции будет инициировано исключение COLLECTION_IS_NULL. Его необходимо перехватить и обработать в соответствии с требованиями приложения. В предыдущем примере передача параметров функции осуществлялась в со- соответствии с их именами (whose_id->). Этот синтаксис делает код более читабель- читабельным, но вообще-то он не является обязательным. Подробнее данный вопрос осве- освещается в главе 16. Коллекции как «столбцы» таблицы базы данных Вложенные таблицы и структуры VARRAY позволяют хранить в столбце таблицы базы данных неатомарные значения и записывать или считывать их с помощью одной команды. Например, в используемой в некотором подразделении таблице employees в одном из столбцов может храниться информация о датах рождения членов семьи сотрудника (табл. 11.1). Таблица 11.1. Хранение списка дат в виде коллекции в столбце таблицы сотрудников Поле Id NUMBER 10010 10020 10030 10040 10050 Поле Name (VARCHAR2) Zaphod Beeblebrox Molly Squiggly Joseph Josephs Gepheus Usrttn Deirde Quattlebaum - Поле Dependents ages (Dependent_birthdate_t) 12-JAN-1763 4-JUL-1977 22-MAR-2021 15-NOV-1968 15-NOV-1968 27-JUN-1995 24-AUG-1996 19-JUN-1997 21-SEP-1997
Коллекции в РЦ/SQL 359 Создать такую таблицу довольно просто. Сначала нужно определить тип кол- коллекции: CREATE TYPE Dependent_birthdatej AS VARRAYA0) OF DATE; который затем можно использовать в определении таблицы: CREATE TABLE employees ( Id NUMBER. Name(VARCHAR2). ... другие столбцы .... Dependents_ages Dependent_bi rthdate_t ): Заполнить таблицу можно с помощью инструкции INSERT, в которой стандарт- стандартный конструктор преобразует список дат в значения данных нужного типа: INSERT INTO employees VALUES D2, 'Zaphod Beeblebrox' Dependent_birthdate_t('12-JAN-1763', '4-JUL-1977'. '22-MAR-2021')): Теперь рассмотрим пример использования столбца типа вложенной таблицы. Создавая внешнюю таблицу personal ity_i inventory, Oracle необходимо указать имя таблицы для записи данных столбца типа вложенной таблицы: CREATE TABLE personal ity_iinventory ( personjd NUMBER, favorite_colors Color tab_t. datejested DATE, test_results BLOB) NESTED TABLE favorite_colors STORE AS favorite_colors_st; Предложение NESTED TABLE.. .STORE AS сообщает Oracle, что таблица для хране- хранения данных столбца favorite_colors (вспомогательная таблица) должна иметь имя favorite_colors_st. Эта таблица будет храниться отдельно от остальных данных таблицы persona!ity_inventory. Ограничений на ее размер не существует. Данные вспомогательной таблицы нельзя обрабатывать непосредственно. По- Попытка прямого считывания или записи в нее информации приведет к возникно- возникновению ошибки. Данные этой таблицы доступны только посредством ссылки на внешнюю таблицу. Далее, в разделе «Псевдофункции коллекций», приводится несколько примеров того, как это делается. Физические атрибуты таблицы хранения тоже нельзя задать напрямую — они наследуются от «самой внешней» таблицы. Главное различие между вложенными таблицами и структурами VARRAY заклю- заключается в том, как они используются в качестве типов данных столбцов таблиц. Несмотря на то что массив VARRAY, подобно вложенной таблице, позволяет сохра- сохранять в одном столбце множество значений, для него необходимо указать макси- максимальную длину массива, который будет храниться в таблице вместе с остальны- остальными данными. Поэтому разработчики компании Oracle утверждают, что столбцы типа VARRAY предназначены для «маленьких» массивов, а вложенные таблицы - для больших. Коллекции как атрибуты объектного типа В следующем примере моделируются данные, используемые для определения ха- характеристик автомобиля. Каждый объект типа auto_spec_t содержит набор цве- цветов, в которые может быть выкрашена машина. CREATE ТУРЕ Auto_spec_t A5 OBJECT ( make VARCHAR2C30J.
360 Глава 11 • Записи и коллекции model VARCHAR2C30). available_colnrs Co1or_tab_t ); Поскольку объектный тип не требует места для хранения данных, в его опре- определении не нужно указывать имя дополнительной таблицы. Мы зададим его поз- позже, когда будем создавать таблицу со столбцом этого типа: CREATE TABLE auto_specs OF Auto_spec_t NESTED TABLE available_colors STORE AS available_co1ors_st: Приведенная выше инструкция требует пояснений. Создавая таблицу объек- объектов, Oracle просматривает объявление объектного типа, чтобы определить, какие столбцы должна содержать такая таблица. Обнаружив, что один из столбцов, а точнее available_colors, является вложенной таблицей, Oracle создает отдель- отдельную таблицу для хранения его данных. Предложение ... NESTED TABLE avail at)! ecolors STORE AS available_colors_st сообщает Oracle, что вспомогательная таблица для хранения данных столбца avai - lable_colors должна называться available_colors_st. Подробнее об объектных ти- типах Oracle рассказывается в главе 21. Встроенные методы коллекций PL/SQL предоставляет для создаваемых вами коллекций множество встроенных функций и процедур, называемых методами коллекций. С их помощью можно по- получать информацию о содержимом коллекции и изменять таковое. Метод (функция Описание или процедура) Функция COUNT Возвращает текущее количество (COUNT] элементов в коллекции Процедура DELETE Удаляет из вложенной таблицы один или несколько элементов. Уменьшает значение, возвращаемое функцией COUNT, если заданные элементы еще не удалены. Может использоваться и со структурами VARRAY, но только для удаления всего их содержимого Функция EXISTS Возвращает значение TRUE или FALSE, определяющее, существует ли в коллекции заданный элемент Процедура EXTEND Увеличивает количество элементов в коллекции, а также значения, возвращаемые функцией COUNT. He применима по отношению к ассоциативным массивам Функции FIRST, LAST Возвращают индексы первого (FIRST) и последнего (LAST) элементов в коллекции Функция LIMIT Возвращает максимальное количество элементов в массиве VARRAY Функции PRIOR, NEXT Возвращают индексы элементов, предшествующих заданному (PRIOR) и следующему за ним (NEXT) Функция TRIM Удаляет элементы начиная с конца коллекции. Уменьшает значение, возвращаемое функцией COUNT, если элементы еще не удалены Все эти конструкции называются методами, потому что синтаксис их вызова отличается от синтаксиса вызова обычных процедур и функций. Это типичный
Коллекции в PL/SQL 361 синтаксис вызова методов, используемый в объектно-ориентированных языках программирования, и в частности в C++. Рассмотрим синтаксис вызова на примере метода LAST. Функция LAST возвра- возвращает наибольший индекс элемента ассоциативного массива. Стандартный вызов этой функции мог бы быть таким: 1f LAST (companyJ:able) > 10 THEN... /* Неверный синтаксис */ Как видите, в качестве аргумента ей передается ассоциативный массив. Но по- поскольку функция LAST является методом, она вызывается как «член» объекта, в нашем примере — ассоциативного массива. Приведем правильный синтаксис ее вызова: if company_table.LAST > 10 THEN... /* Правильный синтаксис */ В общем случае синтаксис вызова методов ассоциативного массива, если речь идет об операции, не требующей передачи аргументов, таков: иия_тдбпицы.операция а если об операции, аргументами которой являются индексы элементов, — таков: иия_таблицы.опердция(инаекс [. индекс]) Например, следующая инструкция возвращает значение TRUE, если в ассоциа- ассоциативном массиве company_tab определена запись 15: company_tab.EXISTSC15) Методы коллекций не доступны из SQL, их можно использовать только в про- программах PL/SQL. Метод COUNT Метод COUNT возвращает количество элементов в ассоциативном массиве, вложен- вложенной таблице или массиве VARRAY. Метод не подсчитывает элементы, удаленные из коллекции с помощью метода DELETE или TRIM. Синтаксис вызова метода COUNT следующий: FUNCTION COUNT RETURN PLSJNTEGER Ниже приведен пример, в котором объявляется и инициализируется коллек- коллекция vol unteerj i st. Перед выполнением с ней необходимых действий код прове- проверяет, содержит ли коллекция хотя бы один элемент: DECLARE vol unteerj ist vol unteerj ist_a r :- volunteer list arCSteven1); BEGIN IF volunteerjist.COUNT > 0 THEN assign tasks Сvolunteerjist): END IF: END; Ограничения по применению Если метод COUNT вызван для инициализированной коллекции, не содержащей ни одного элемента, он возвращает значение 0. Это же значение он возвращает и при наличии пустого ассоциативного массива.
362 Глава 11 • Записи и коллекции Возможные исключения Если метод COUNT вызван для неинициализированной вложенной таблицы или структуры VARRAY, он генерирует исключение COLLECTIOM_IS_NULL Учтите, что в слу- случае ассоциативных массивов такое исключение не генерируется, поскольку они не требуют инициализации. Метод DELETE Метод DELETE предназначен для удаления одного, нескольких или всех элементов ассоциативного массива, вложенной таблицы или массива VARRAY. Будучи вызван- вызванным без аргументов, он удаляет все элементы коллекции. Вызов DELETECi) удаля- удаляет i-й элемент вложенной таблицы или ассоциативного массива. А вызов DELE- TEC i, j) удаляет все элементы с индексами от i до j включительно. При вызове с параметрами метод резервирует место, занимавшееся «удаленным» элементом, и позднее этому элементу можно присвоить новое значение. Фактически PL/SQL освобождает память лишь при условии, что программа удаляет количество элементов, достаточное для освобождения целой страницы памяти. Если же метод DELETE вызывается без параметров и очищает всю коллек- коллекцию, память освобождается немедленно. В любом случае эта операция выполня- выполняется автоматически и не требует участия программиста. ПРИМЕЧАНИЕ- Метод DELETE применительно к массивам VARRAY может вызываться только без аргументов. Иными словами, с помощью указанного метода из этой структуры нельзя удалять отдельные элементы, по- поскольку в таком случае она станет разреженной, что совершенно недопустимо. Единственный спо- способ удалить из VARRAY один или несколько элементов — воспользоваться методом TRIM, предна- предназначенным для удаления группы расположенных рядом элементов начиная с конца коллекции. Синтаксис перегруженного метода DELETE таков: PROCEDURE DELETE PROCEDURE DELETE (i [BINARYJNTEGER | VARCHAR2(SIZE_LIMIT)]) PROCEDURE DELETE Ci [BINARYJNTEGER | VARCHAR2CSIZE LIMIT)], j [BINARYJNTEGER | VARCHAR2CSIZE_LIMIT)]) Следующая процедура удаляет из коллекции все элементы, кроме последнего. В ней используются четыре метода: FIRST — для получения номера первого уда- удаляемого элемента; LAST — для получения номера последнего удаляемого элемента; PRIOR — для определения номера предпоследнего элемента; DELETE — для удаления всех элементов, кроме последнего. CREATE PROCEDURE keepjast (thejist IN OUT List t) AS first_elt PLSJNTEGER :- thejist.FIRST; nextjojast_elt PLSJNTEGER := thejist.PRIORttheJist.LAST); BEGIN theji st. DELETEt first_elt. next Jo last_elt); END: Приведем еще несколько примеров удаления элементов из различных таблиц. Сначала мы удаляем все элементы, кроме последнего, из таблицы names: names.DELETE:
Коллекции в PL/SQL 363 затем элемент 77 из таблицы globals: globaIs.DELETE G7): и наконец, все элементы начиная с элемента, имеющего индекс -15 000, и закан- заканчивая элементом с индексом 0, из таблицы temp_readi ngs: tetnp_readings.DELETE(-15000. 0); Ограничения по применению Если значения индексов i и/или j указывают на несуществующие элементы, ме- метод DELETE пытается «сделать наилучшее» и не генерирует исключение. Напри- Например, если таблица содержит три элемента, то вызов метода DELETE (-5, 1) удалит первый из них, а вызов DELETE(-5) не выполнит никаких действий. Возможные исключения Вызов метода DELETE для неинициализированной вложенной таблицы или масси- массива VARRAY инициирует исключение COLLECTION_IS_NULL. Метод EXISTS Данный метод используется с вложенными таблицами, ассоциативными масси- массивами и массивами VARRAY для определения наличия в коллекции заданного эле- элемента. Если таковой имеется, он возвращает значение TRUE, а если отсутствует - значение FALSE. Значение NULL метод не возвращает ни при каких условиях. Кроме того, метод EXISTS возвращает FALSE и в том случае, если заданный элемент был удален из коллекции с помощью метода TRIM или DELETE. Синтаксис этого метода: FUNCTION EXISTS Ci [BINARYJNTEGER | VARCHAR2(SIZE_LIflIT)]) RETURN BOOLEAN Ниже приведен блок кода, который проверяет, присутствует ли заданный эле- элемент в коллекции, и при положительном ответе присваивает ему значение NULL: DECLARE myjist Col or_tab_t :- Color_tab_tO: element INTEGER := 1: BEGIN IF myjist. EXISTS (element) THEN щу_11st[element) :- NULL: END IF: END; Ограничения по применению Если метод EXISTS вызывается для неинициализированной (содержащей единст- единственное значение NULL) вложенной таблицы или структуры VARRAY либо для ини- инициализированной коллекции, не содержащей ни одного элемента, он просто воз- возвращает значение FALSE. Поэтому его можно вызвать без предварительного обра- обращения к методу COUNT, не рискуя получить сообщение об ошибке. Возможные исключения Если i не является целым числом и не может быть в него преобразовано, метод EXISTS инициирует исключение VALUE_ERROR. Это исключение может быть вызвано любыми методами коллекции, которые имеют входные параметры.
364 Глава 11 • Записи и коллекции Метод EXTEND Для того чтобы добавить элемент во вложенную таблицу или массив VARRAY, нуж- нужно сначала выделить для него область памяти. Соответствующая операция, не за- зависящая от операции присваивания элементу значения, выполняется с помощью метода EXTEND. Однако помните, что указанный метод не применяется к ассоциа- ассоциативным массивам. Метод EXTEND, как уже было сказано, добавляет элементы в коллекцию. Будучи вызванным без аргументов, он добавляет один аргумент, равный NULL. Вызов EX- EXTEND (п) добавляет п элементов NULL, а вызов EXTENDCn, i) — я элементов и всем им присваивает значение i-ro элемента. Последняя форма метода применяется к кол- коллекциям, для элементов которых задано ограничение NOT NULL. Синтаксис перегруженного метода EXTEND приведен ниже: PROCEDURE EXTEND (n PLSJNTEGER :- 1) PROCEDURE EXTEND (n PLSJNTEGER. i PLSJNTEGER) В следующем примере процедура push добавляет в список один элемент и при- присваивает ему новое значение: CREATE PROCEDURE push (thejist IN OUT List_t. new_value IN VARCHAR2) AS BEGIN theJi st. EXTEND ; theJistttheJist.LAST) := new_va1ue; END: Далее мы рассмотрим фрагмент кода, в котором с помощью метода EXTEND фор- формируется коллекция, состоящая из 10 элементов с одинаковыми значениями. Для этого в коллекцию сначала добавляется один элемент и ему явно присваивается нужное значение. При повторном вызове метода EXTEND в коллекцию добавляется еще 9 элементов, которым присваивается значение первого элемента коллекции. CREATE PROCEDURE push_ten (thejist IN OUT List_t. new_value IN VARCHAR2) AS l_copyfrom PLSJNTECER; BEGIN thejist. EXTEND: l_copyfroni := theJist.LAST; theJist(l_coDyfroni) :=¦ new_va1ue; thejist.EXTEND (9. l_copyfrom); END: Ограничения по применению Если в результате вызова метода TRIM или DELETE из коллекции было удалено не- несколько последних элементов, метод EXTEND «перепрыгнет* через них и присвоит новому элементу индекс следующего за ними элемента. Если параметр п метода имеет значение NULL, метод не выполняет никаких действий. Возможные исключения Если метод EXTEND вызван для неинициализированной вложенной таблицы или структуры VARRAY, инициируется исключение COLLECTION_IS_NULL. Попытка доба- добавить в массив VARRAY элементы, индекс которых превышает максимальный индекс массива в его объявлении, инициирует исключение SUBSCRIPT_BEYOND_LIMIT.
Коллекции в PL/SQL 365 Методы FIRST и LAST Методы FIRST и LAST возвращают соответственно наименьший и наибольший ин- индексы элементов вложенной таблицы, ассоциативного массива или массива VAR- RAY. Приведем их синтаксис: FUNCTION FIRST RETURN PLSJNTEGER FUNCTION LAST RETURN PLSJNTEGER В следующем примере производится полный просмотр коллекции: FOR indx IN holidays.FIRST .. holidays.LAST ¦ LOOP send_everyone_home (indx): END LOOP: Запомните, что такой цикл будет выполнен корректно (то есть не станет при- причиной исключения NO_DATA_FOUND) лишь при условии, что коллекция является плотной. А теперь рассмотрим пример, в котором метод LAST применяется для добавле- добавления элементов в ассоциативный массив companies. Данные в массив копируются из базы данных в цикле FOR с курсором. Перед началом заполнения массива кол- коллекция compani es пуста и метод LAST возвращает NULL. Поэтому в выражении для вычисления индекса массива используется функция NVL, которая возвращает зна- значение второго аргумента, если ее первый аргумент пуст. Таким образом, индекс первого добавляемого в коллекцию элемента становится равным единице. FOR company гее IN company_cur LOOP companies (NVL (companies.LAST, 0) + l).company_id :- company_rec.companyj d: END LOOP: Ограничения по применению Если в вызовах методов FIRST и LAST указать неинициализированную коллекцию, то есть такую, которая не содержит ни одного элемента, оба метода возвратят зна- значение NULL. Для массива VARRAY, в котором всегда имеется хотя бы один элемент, метод FIRST постоянно возвращает значение 1, а метод LAST — то же значение, что и метод COUNT. Возможные исключения При условии, что методы FIRST и LAST вызываются для неинициализированной вложенной таблицы или массива VARRAY, инициируется исключение CCLLECTI- ONJSJULL. Метод LIMIT Метод LIMIT возвращает максимальное количество элементов, которое можно оп- определить в массиве VARRAY. В случае вложенной таблицы или ассоциативного мас- массива он возвращает NULL. Синтаксис этого метода таков: FUNCTION LIMIT RETURN PLS INTEGER
366 Глава 11 • Записи и коллекции В следующем примере добавлению нового элемента в конец массива VARRAY предшествует проверка того, есть ли в нем еще место: IF myJist.LAST < rnyjist.LIMIT THEN niyjist. EXTEND: END IF: Ограничения по применению Особых ограничений по использованию метода LIMIT не существует. Возможные исключения Вызов метода LIMIT для неинициализированной вложенной таблицы или массива VARRAY генерирует исключение COLLECTION_IS_NULL. Методы PRIOR И NEXT Методы PRIOR и NEXT используются для перемещения по коллекциям — вложен- вложенным таблицам, ассоциативным массивам и массивам VARRAY. Вызов методов про- производится следующим образом: FUNCTION PRIOR G [BINARYJNTEGER | VARCHAR2CSIZE_LIMIT)]) RETURN [BINARYJNTEGER | VARCHAR2(SIZE_LIMIT)] FUNCTION NEXT G [BINARYJNTEGER | VARCHAR2CSIZEJJMIT)]) RETURN [BINARYJNTEGER | VARCHAR2(SIZE_LIMITH Обоим методам передается индекс элемента в коллекции. Метод PRIOR возвра- возвращает индекс предыдущего элемента коллекции, а метод NEXT — следующего. При- Приведенная ниже функция возвращает сумму чисел, хранящихся в коллекции Li st_t. CREATE FUNCTION compute_sun (thejist IN ListJ) RETURN NUMBER AS row index PLSJNTECER :- thejist.FIRST: total NUMBER := 0; BEGIN LOOP EXIT WHEN rowjndex IS NULL; total :-total + theJ1st( rowjndex); rowjndex :- thejist.NEXT(rowJndex): END LOOP: RETURN total; END: Данная функция сканирует всю коллекцию, последовательно перемещая ука- указатель от первого элемента к последнему с помощью метода NEXT. Воспользовав- Воспользовавшись методом PRIOR, функцию можно переписать так, чтобы переход производил- производился в направлении от конца коллекции к ее началу: CREATE FUNCTION compute_sura (thejist IN ListJ;) RETURN NUMBER AS rowjndex PLSJNTEGER := thejist.LAST; total NUMBER :- 0: BEGIN LOOP EXIT WHEN rowjndex IS NULL; total :- total + the listCrow index):
Коллекции в PL/SQL 367 rowjndex := theJist.PRIOR(rowJndex): END LOOP: RETURN total : END: Правда, сейчас направление перемещения для нас не имеет никакого значе- значения, поскольку нужно лишь просмотреть все элементы коллекции. Но в боль- большинстве случаев оно является очень важным условием. Ограничения по применению Методы PRIOR и NEXT для инициализированной коллекции, не содержащей ни од- одного элемента, возвращают значение NULL. Это же значение метод NEXT возвращает и в том случае, если значение i больше или равно количеству элементов коллек- коллекции (COUNT), а метод PRIOR — при условии, что i меньше или равно индексу перво- первого элемента (FIRST). ПРИМЕЧАНИЕ В настоящее время, если коллекция не пуста, а параметр i больше или равен значению COUNT, ме- метод PRIOR возвращает в качестве значения индекс последнего элемента коллекции, а если i меньше или равно индексу первого элемента, метод NEXT возвращает значение FIRST. Однако не известно, сохранится ли такое их поведение в будущих версиях Oracle. Метод TRIM Метод TRIM удаляет п последних элементов коллекции - вложенной таблицы или массива VARRAY. Если в вызове метода аргументы не указаны, он удалит только один элемент. Имейте в виду, что при совместном использовании методов TRIM и DELETE возможна накладка: если заданный в вызове метода TRIM элемент был уже удален с помощью метода DELETE, метод TRIM сделает это еще раз, поэтому количе- количество удаленных элементов окажется меньшим, чем вы рассчитывали. ВНИМАНИЕ Попытка вызвать метод TRIM для обработки ассоциативного массива приведет к ошибке времени компиляции. Синтаксис метода TRIM следующий: PROCEDURE TRIM (л PLSJNTEGER := 1) Приведенная далее функция извлекает из списка последнее значение и воз- возвращает его вызывающему блоку. Данная операция, называемая выталкиванием, реализуется как выборка значения с последующим усечением коллекции на один элемент: CREATE FUNCTION pop (thejist IN OUT List t) RETURN VARCHAR2 AS l_value VARCHAR2C30); BEGIN IF thejist. COUNT >=¦ 1 THEN /* Сохраняем значение последнего элемента коллекции, || которое будет возвращено функцией */ l_value :- theJistCtheJist.LAST): the list.TRIM:
368 Глава 11 • Записи и коллекции END IF: RETURN "l_value: END; Ограничения по применению Если передаваемое методу TRIM значение л равно NULL, метод не выполнит ника- никаких действий. Возможные исключения При попытке удалить больше элементов, чем имеется в коллекции, инициирует- инициируется исключение SUBSCRIPT_BEYOND_COUNT. Если метод TRIM вызывается для неинициа- неинициализированной вложенной таблицы или массива VARRAY, инициируется исключе- исключение COLLECTIONJSJULL. ВНИМАНИЕ Вызывая методы TRIM и DELETE для одной и той же коллекции, можно получить неожиданные ре- результаты. Как вы думаете, на сколько элементов меньше станет в коллекции, если последний эле- элемент удалить с помощью метода DELETE, а затем вызвать метод TRIM, указав то же значение пара- параметра? Если вы считаете, что на два, то ошибаетесь — на самом деле оба метода удалят один и тот же элемент. Поэтому, чтобы избежать накладок, компания Oracle рекомендует не использовать оба эти метода при работе с одной коллекцией. Работа с коллекциями Итак, вы уже знаете, как определяются типы коллекций в базе данных и в про- программах PL/SQL и как объявляются коллекции на основе этих типов. Далее речь пойдет о программном коде, необходимом для работы с готовыми коллекциями. Но прежде чем приступить к его написанию, нужно ответить на три вопроса: О Как правильно инициализировать переменную коллекции? О Как присвоить элементам коллекции значения, не вызвав исключений? О Как добавлять и удалять элементы коллекции? Кроме того, для полного освоения методики работы с коллекциями нужно по- понять, как сохранять и извлекать из них наборы данных. Об этом и о других инте- интересных операциях с коллекциями рассказывается в разделе, посвященном псев- псевдофункциям коллекций. Инициализация переменных-коллекций Ассоциативные массивы не требуют явной инициализации. Достаточно просто объявить такой массив, и он автоматически инициализируется «пустыми» значе- значениями. После этого можно сразу присваивать значения любым его элементам. В качестве индексов коллекции могут выступать практически любые положи- положительные и отрицательные значения. Программа может даже назначать элементам ассоциативного массива произвольные индексы, пропуская огромные диапазоны значений без каких-либо потерь памяти1. Выделение памяти для вложенных таблиц и массивов VARRAY производится иначе, чем для ассоциативных массивов. Прежде всего, если не инициализиро- инициализировать коллекцию одного из этих двух типов, ей автоматически будет присвоено Благодаря тому что ассоциативные массивы часто бывают разреженными, их можно использовать для представления в памяти любой таблицы базы данных с целочисленными первичными ключами.
Коллекции в PL/SQL 3bS значение NULL, и любая попытка получить или присвоить значение содержащему- содержащемуся в ней элементу приведет к ошибке времени выполнения. Например: DECLARE /* Переменная cool_co1ors не инициализирована при объявлении. || позтоиу она содержит значение NULL. */ cool_co"lors Color_tab_t; BEGIN IF cool_colors IS NULL THEN -- результат проверки равен TRUE IF coolcolors(l) IS NULL THEN -- будет сгенерировано исключение cool colors(l) :- 'BLUE': -- будет сгенерировано исключение Таким образом, использованию коллекции должна предшествовать ее инициа- инициализация. Выполнить таковую можно одним из трех способов: О явно, с помощью конструктора; О неявно, путем непосредственного присваивания ей содержимого другой пере- переменной-коллекции; О неявно, путем выборки записей из базы данных. При этом не важно, какое количество элементов вы запишете в коллекцию. Их может не быть вообще, а может быть один или несколько, и впоследствии всегда можно будет добавить еще. И пусть вас не смущает тот факт, что при объявлении массива VARRAY указывается количество его элементов — это максимально допус- допустимое, но отнюдь не обязательное значение. Тот факт, что вы задали максималь- максимальное количество элементов массива, еще не значит, что при инициализации следу- следует поместить в него именно такое их количество. Явная инициализация с помощью конструктора В одном из предыдущих примеров мы объявили две переменные: my_favorite__colors Color_tab_t := Color_tab_tC'PURPLE'. 'GREEN'); my_favorite_numbers Number_t := Number_tD2, 65536); Здесь ColortabtO — это функция-конструктор, предоставляемая Oracle для соз- создании типа коллекции Col or_tab_t. Она принимает произвольное количество ар- аргументов соответствующего типа данных, а именно типа VARCHAR2C30), поскольку Исходное его определение было таким: CREATE TABLE Color_tab_t AS TABLE OF VARCHAR2C0); При инициализации переменной Oracle выделяет память для хранения значе- значений, заданных посредством аргументов конструктора, и таким образом создаются и заполняются «ячейки» для элементов коллекции. Для того чтобы исправить приведенный выше код, достаточно присвоить пе- переменной корректное значение: DECLARE cool_colors Color_tab_t :- Color tab t('VIOLET'); -- инициализация BEGIN IF cool_colors<l) IS NULL THEN - Теперь все в порядке!
370 Глава П • Записи и коллекции А как вы думаете, что происходит при инициализации в данном случае: working_co"lors Color_tab_t :» Color_tab_t() Так создается пустая коллекция. «Пустота» — это некое загадочное состояние коллекции, когда она не равна значению NULL, но и не содержит данных. Создав пустую коллекцию, ее нужно расширить с помощью метода EXTEND — лишь после этого в нее можно добавлять новые элементы. О методе EXTEND рассказывалось ра- ранее в этой главе. Неявная инициализация путем непосредственного присваивания Если две переменные, объявленные как коллекции, имеют один и тот же тип дан- данных, значение одной из них можно присвоить другой. И если переменная, кото- которой присваивается значение, еще не была инициализирована, это будет сделано автоматически, в результате данной операции. Рассмотрим пример, в котором переменной weddingcolors присваивается зна- значение переменной earthcoiors: DECLARE earth_colors Color_tab_t ;- Co1or_tab_tC'BRICK1. 'RUST'. 'DIRT'); wedding_colors Color_tab_t; BEGIN wedding_colors :- earthcolors; wedd!ng_colors<3) :- 'CANVAS': END; Этот код инициализирует переменную wedding_colors и записывает в нее эле- элементы переменной earth_colors. В результате мы получаем две независимые пе- переменные с одинаковыми значениями, каждое из которых можно изменять, не влияя на другое; при изменении значения третьего элемента коллекции wed- dingcolors третий элемент коллекции earthcolors остается неизменным. Учтите, что одной только совместимости типов данных переменных, объяв- объявленных в качестве коллекций, недостаточно для выполнения операции присваи- присваивания. Если вы объявите два типа коллекций с одинаковыми определениями, то получите два разных типа данных, причем переменные одного типа нельзя будет присвоить переменным другого типа. Неявная инициализация путей выборки из базы данных Если таблица базы данных содержит значения, объявленные как коллекции, су- существуют достаточно интересные способы перемещения этих данных между таб- таблицей и PL/SQL-кодом. Как и при прямом присваивании, при выборке данных из таблицы и присваивании их переменной, объявленной как коллекция, с помо- помощью инструкции FETCH или SELECT INTO выполняется автоматическая инициализа- инициализация этой переменной. Таким образом, коллекции могут оказаться даже очень по- полезными! В одном из предыдущих примеров мы уже показали, как с помощью одной ко- команды считать из базы данных всю коллекцию. Рассмотрим эту операцию под- подробнее. Прежде всего создадим таблицу с коллекцией, в которую запишем не- несколько значений: CREATE TABLE colorjnodels ( madeljype VARCKAR2UZ). colors Color tab t)
коллекции в PL/SQL 371 NESTED TABLE colors STORE AS color_model_colors_tab; NSERT INTO colorjnodels VALUES ('RGB1. Color_tab_t('RED'.'GREEN'.'BLUE')); Теперь — самое главное. За один цикл обращения к базе данных мы считыва- считываем все значения из столбца col ors заданной строки и помещаем их в локальную кременную: TECLARE l_colors Color_tab_t: 4EGIN /* Выбираем все аложенные значения за один цикл. || Это саиое примечательное! */ SELECT colors INTO l_colors FROM colorjradels WHERE model_type - 'RGB'; iND: Обратите внимание на несколько важных моментов. О При выборке значений из базы данных управление индексами коллекции l_colors осуществляется Oracle, а не программистом. О Начальное значение индекса, присваиваемое Oracle, равно 1 (а не 0, как в не- некоторых других языках). Значение каждого последующего индекса на едини- единицу больше значения предыдущего. О Операция присваивания значений элементам коллекции требует инициализа- инициализации локальной переменной, объявленной как коллекция. Мы не инициализи- инициализировали переменную l_colors с помощью конструктора, но PL/SQJL известно, как ее обрабатывать. Содержимое коллекции можно легко изменить и с такой же легкостью запи- записать информацию обратно в таблицу базы данных. Давайте, шутки ради, созда- создадим следующую цветовую модель Fuschia-Green-Blue: DECLARE color_tab Co1or_tab t: BEGIN SELECT colors INTO color_tab FROM colorjnodels WHERE modeljype - 'RGB'; FOR element IN 1..color_tab.COUNT LOOP IF color jtab(element) = 'RED' THEN color Jab(element) :•= 'FUSCHIA': END IF; END LOOP: /* Это самая интересная часть примера. Достаточно || одной инструкции вставки - и вся вложенная таблица || отправляется обратно в базу данных, в таблицу color models. */ INSERT INTO colorjradels VALUES C'FGB'. color tab)' .Ntl:
372 Глава И • записи и коллекции Интеграция массивов VARRAY Возможно, кто-то спросит, реализована ли интеграция баз данных с PL/SQL и для массивов VARRAY? В общем-то, да, хотя имеется несколько особенностей такой ин- интеграции. Прежде всего, при записи содержимого вложенной таблицы в базу данных, а также при его считывании Oracle не всегда сохраняет порядок следования эле- элементов таблицы. Это понятно, поскольку хорошо известно, что реляционные базы данных не заботятся о порядке строк. А вот при чтении и записи содержимого массивов VARRAY порядок элементов сохраняется. Сохранение порядка следования элементов - очень полезная черта массива VARRAY, и очень часто порядок несет определенную информационную нагрузку. Если, предположим, нужно сохранить цвета в порядке их важности, их следует записать в столбец типа VARRAY. При выполнении каждой операции чтения вы бу- будете получать элементы из этого столбца в той последовательности, в которой их записали. Если же придерживаться строго реляционной модели, потребуются два столбца: один для хранения целочисленных значений, которые определяют сте- степень важности цветов, другой — для хранения их названий. Указанное свойство массивов VARRAY открывает широкие возможности для на- написания новых интересных функций. Например, вы можете запрограммировать вставку дополнительного цвета в начало списка со сдвигом всех остальных эле- элементов. Еще одно отличие структур VARRAY от вложенных таблиц, связанное с интегра- интеграцией базы данных и PL/SQL, заключается в том, что некоторые инструкции SE- SELECT для выборки вложенных таблиц нельзя использовать с массивами VARRAY «как есть» — они нуждаются в модификации. Подробнее об этом будет рассказа- рассказано в разделе «Псевдофункции коллекций». Присваивание значений элементам коллекции Для присваивания значений элементам коллекции используется стандартный оператор присваивания PL/SQL: countdown_test_list C43) :- 'Internal pressure'; conpany_names_table (last_name_row+10) := 'Dohnstone Clingers'; Допускается и ¦«агрегатное» присваивание всего содержимого коллекции дру- другой коллекции того же типа: DECLARE TYPE name_table IS TABLE OF VARCHAR2C100) INDEX BY BINARYJNTEGER: oldjiames name_table: new_names name_table; BEGIN /* Присваиваем значения строкам таблицы old names */ old_names(l) :- 'Smith': old_namesB) ¦.- 'Harrison'; /* Присваиваем значения строкам таблицы new_names */ new_names(lll) := 'Hanrahan': new_namesC42) .— 'Blimey';
Коллекции в PL/SQL 373 /* Копируем значения из таблицы old_names в таблицу new_names */ old_names := newjiames; /* Следующий оператор генерирует исключение NQ_DATA_FOUND */ DBMS_OUTPUT.PUT_LINE (old_names (i)); END: В результате выполнения оператора присваивания содержимое одной коллек- коллекции полностью замещается содержимым другой коллекции. В этом примере при- присваиванию предшествует определение первого и второго элементов коллекции oldjiames. Однако после выполнения присваивания в коллекции оказываются за- заполненными только элементы 111 и 342. Поэтому при попытке обратиться к стро- строке 1 PL/SQL инициирует исключение NO_DATA_FOUNO. Ассоциативные массивы позволяют присваивать фиксированные значения про- произвольным элементам коллекции (с индексами от -231 + 1 до 231 - 1). В результа- результате выполнения такой операции автоматически создается соответствующий эле- элемент коллекции с заданным значением. Однако ни вложенные таблицы, ни массивы VARRAY, в отличие от ассоциативных массивов, не позволяют присваивать значения произвольным элементам коллек- коллекции. В этих структурах индексы назначаются не программистом, a PL/SQL, и зна- значения их увеличиваются последовательно. Это означает, что п инициализирован- инициализированных элементов будут иметь индексы от 1 до и, и вы не сможете присвоить значения никаким другим элементам. Кроме того, прежде чем присвоить значение строке вложенной таблицы или массива VARRAY, нужно расширить коллекцию с помощью метода EXTEND, о котором рассказывается далее в этой главе. Ссылки на не определенный элемент Конкретный элемент входит в коллекцию при соблюдении одного из двух сле- следующих условий: О коллекция является ассоциативным массивом, и этому элементу присвоено значение (таким образом, элемент определен); О коллекция является вложенной таблицей или массивом VARRAY, и эта структу- структура явно расширена путем добавления нового элемента (созданному таким спо- способом элементу можно присваивать значения). При попытке обратиться к несуществующему элементу (для чтения или запи- записи в него значения) PL/SQL инициирует одно из трех возможных исключений. Проиллюстрируем сказанное следующим примером: CREATE TYPE stringsjit IS TABLE OF VARCHAR2 A00): 1 DECLARE 2 TYPE stringsjbt IS TABLE OF VARCHAR2 A00) 3 INDEX BY BINARY INTEGER; 4 5 strings stringsjit := stringsjit 0: 6 ibt_strings stringsjbt: 7 BEGIN 8 BEGIN 9 IF ibt_stringsB) = 'a' THEN NULL; END IF; И EXCEPTION
374 Глава 11 • Записи и коллекции Ц WHEN OTHERS 12 THEN DBMS_OUTPUT.pirt_line С 'а. ' ||SQLERRM): 13 END; 14 BEGIN 15 strings.EXTEND: strings B) :- 'b1; 16 EXCEPTION 17 WHEN OTHERS 18 THEN DBMS_WTPirr.put_line ( 'b. ' || SQLERRM); 19 END: 20 21 BEGIN 22 strings @) :- 'c'; 23 EXCEPTION 24 WHEN OTHERS 25 THEN DBMS_OUTPUT.putline ( 'c, ' || SQLERRM): 26 END: 27 END: В результате выполнения данного сценария получаем: a. ORA-01403: no data found b. ORA-06533: Subscript beyond count с ORA-06532: Subscript outside of limit Краткое объяснение приведенного выше кода дается в следующей таблице. Строка Описание 9 Попытка считать содержимое второго элемента ассоциативного массива, в котором не определен ни один элемент (он объявлен в строке б). Поэтому PL/SQL инициирует исключение NO_DATA_FOUND 15 Расширение вложенной таблицы на один элемент. Операция присваивания значения второму элементу коллекции некорректна, поскольку расширение выполнено только один раз, индекс 2 ошибочен 22 Попытка присвоить значение элементу вложенной таблицы с нулевым индексом, что по определению невозможно. Элементов с нулевым индексом ни во вложенных таблицах, ни в массивах VARRAY нет (но они могут присутствовать в ассоциативных массивах) Работа с коллекциями составных элементов Возможности коллекций, впервые введенные в PL/SQL версии 2 (Огас1е7), были весьма ограниченными. Например, ассоциативные массивы заполнялись только скалярными значениями (датами, числами, строками и т. д.), что очень сужало сферу применения этих полезных структур, в то время называвшихся, как уже было сказано, таблицами PL/SQL. Но это было давно, а теперь коллекции поддерживают гораздо более сложные структуры данных: О коллекции записей (Oracle 7.3.4 и выше) — используются при условии, что записи не содержат в качестве полей другие записи или коллекции; О коллекции объектов (Огас1е8 и выше) — создаются в виде коллекций экземп- экземпляров объектных типов, записей, LOB, типов данных XML и т. д.;
Коллекции в PL/SQL 375 О коллекции коллекций (Oracle9i и выше) — представляют собой многоуровне- многоуровневые коллекции, в том числе коллекции коллекций и коллекции типов данных. Рассмотрим примеры коллекций всех типов. Коллекции записей Для создания коллекции записей в предложении TABLE OF ее объявления задается тип записи (либо со спецификацией %ROWTYPE, либо тип, задаваемый программи- программистом). Этот способ применим только к типам коллекций, используемым в про- программах PL/SQL. Вложенные таблицы и типы VARRAY, определенные в базе дан- данных, не могут содержать fcROWTYPE-ссылок на записи. Вот пример определения коллекции записей, основанной на пользователь- пользовательском типе записей: CREATE OR REPLACE PACKAGE CDmpensation_pkg IS TYPE reward rt IS RECORD ( ran VARCHAR2I2000). sal NUMBER. com NUMBER TYPE reward_tt IS TABLE OF reward_rt INDEX BY BINARYJNTEGER END compensation_pkg: Определив перечисленные типы коллекций в спецификации пакета, в других программах можно объявлять коллекции на их основе. Например: DECLARE hoi i daybonuses compensat!on_pkg.reward_tt : Коллекции записей особенно удобно использовать для создания в динамиче- динамической памяти структур, эквивалентных таблицам базы данных. В каких случаях это может понадобиться? Предположим, что каждый месяц в определенное вре- время запускается пакетная процедура, обрабатывающая таблицы, которые модифи- модифицируются в течение всего месяца. Процесс обработки и анализа данных достаточно трудоемок, требует нескольких проходов по таблицам. Разумеется, можно просто по нескольку раз считывать данные из таблиц, но это существенно замедлит про- процесс и потребует интенсивного использования ресурсов системы. Целесообразнее скопировать данные из таблицы (или таблиц) в коллекцию, где их можно обработать гораздо быстрее, имея к тому же возможность произ- произвольного доступа к записям. Таким образом, в программе PL/SQL, по сути, эму- эмулируется двунаправленный курсор. Рассмотрим простой пример считывания данных из таблицы в коллекцию. Как видите, для перемещения данных (путем выполнения запроса) требуется на удивление мало программного кода: DECLARE CURSOR hairstylescur IS SELECT * FROM hairstyles: TYPE local_hairstyles_tab IS TABLE OF hairstyles_curSROWTYPE INDEX BY BINARYJNTEGER: local hairstyles Iocal_ha1rstyles tab: BEGIN FOR hairstyles_rec IN hairstyles_cur LOOP
376 Глава 11 ¦ Записи и коллекции local hairstyles (hairstyles_rec.code):= hairstyles_rec: END LOOP? END: В настоящем примере в качестве номера строки используется значение столб- столбца code из таблицы hai rsty I es. Ассоциативный массив при этом заполняется непо- непоследовательно. Вы спрашиваете, почему? Дело в том, что при таком подходе, по- получив код то ли от другой программы, то ли от пользователя, можно немедленно извлечь остальные данные строки, не выполняя поиск в коллекции. А теперь посмотрим, как можно эмулировать двунаправленный курсор. Ос- Основная идея заключается в том, чтобы считать информацию из базы данных и без повторного обращения к ней обработать набор записей в программе. Существует два подхода к реализации такой идеи: О включить код для работы с коллекцией в главную программу; О создать отдельный пакет, инкапсулирующий доступ к данным коллекции. Как правило, предпочтение следует отдавать второму подходу, который пред- предполагает создание для сложных структур и данных отдельного, строго определен- определенного и ориентированного на многократное использование API. Приведем специ- спецификацию пакета для эмуляции двунаправленного курсора: /* Файл в web: bidir.pkg */ CREATE OR REPLACE PACKAGE bidir IS FUNCTION rowforid (idjn IN employee.employee idSTYPE) RETURN employeeSROWTYPE; FUNCTION firstrow RETURN PLSJNTEGER; FUNCTION lastrow RETURN PLSJNTEGER: FUNCTION rowcount RETURN PLSJNTEGER; FUNCTION end_of_data RETURN BOOLEAN: '• PROCEDURE setrow (nth IN PLSJNTEGER); FUNCTION currrow RETURN employeeSROWTYPE; PROCEDURE nextrow; PROCEDURE prevrow; END: А как пользоваться этим API? Ниже представлен пример программы, которая с его помощью просматривает данные таблицы empl oyee — сначала передвигаясь от ее начала к концу, а затем в обратном направлении: DECLARE l_employee employeeZROWTYPE: BEGIN LOOP EXIT WHEN bidir.end_of_data; l_employee :- bidir.currrow: DBMS_OUTPUT.putJ i ne A_employee.1astjiame); bidir.n
Коллекции в PL/SQL 377 END LODP; bidir.setrow (bidir.lastrow); LOOP EXIT WHEN bidir.end_of_data; l_employee :- bidir.currrow: DBMSJXJTPUT.put_l i ne П_employ ее. 1 astjiame); bidir.prevrow; END LOOP: END; Проницательный читатель наверняка заинтересуется, когда же в коллекцию загружаются данные и где находится сама коллекция. Ведь в представленном коде ничего похожего на нее явно не содержится. Сначала ответим на второй вопрос. Вы не видите коллекцию потому, что она скрыта в спецификации пакета. Пользователь пакета никогда не имеет с ней дела и даже не должен знать о ее существовании. Это основная идея создания API. Вы просто вызываете ту или иную программу, и она делает все, что нужно. Ну а теперь коротко о загрузке данных в коллекцию. Может показаться, что это делается неким волшебным образом, но когда вы прочитаете главу 17, посвя- посвященную пакетам, все станет на свои места. Заглянув в тело пакета, вы найдете в нем такой раздел инициализации: BEGIN -- Инициализация пакета FOR гее IN (SELECT * FROM employee) LOOP employees (rec.employee_1d) := rec: END LOOP: g_currrow :- firstrow: END; ВНИМАНИЕ Переменная g_currrow объявлена в теле пакета. Именно поэтому она отсутствует в приведенной выше спецификации. Это означает, что при первом обращении к какому-либо элементу пакета авто- автоматически выполняется инициализационный код, копирующий содержимое таб- таблицы employee в коллекцию employees. Когда же это происходит в приведенной выше программе? В цикле, при вызове функции bidir.end_of_data, когда мы про- проверяем, остались ли еще непросмотренные данные. Мы предлагаем вам самостоятельно ознакомиться с реализацией пакета bi di г. Его простой и понятный код наглядно демонстрирует основные принципы техно- технологии создания API для работы с данными посредством коллекций. Коллекции других сложных типов данных В создании и использовании коллекций новых типов данных Oracle нет ничего сложного, но если вы не имеете какого-либо опыта работы с этими типами данных,
378 Глава 11 • Записи и коллекции некоторые моменты могут вызвать затруднение. Рассмотрим пример коллекции объектов: /* Файл в web: object_collection.sql */ CREATE TYPE pett IS OBJECT ( tagjio INTEGER. NAME VARCHAR2 F0). MEMBER FUNCTION set_tag_no (new_tag_no IN INTEGER) RETURN pet_t) / DECLARE TYPE pets_t IS TABLE OF pet_t: pets pets_t := pets_t ( pet_t A050. 'Sanmy'). b1rd_t A075. 'Mercury1. 14) ); BEGIN FDR indx IN pets.FIRST .. pets.LAST LOOP DBMSJXJTPUT.putJine (pets (indx).NAME): END LOOP: END; После определения объектного типа вы можете объявить основанную на нем коллекцию и заполнить ее экземплярами того же типа. Так же просто создаются и коллекции других типов. Все правила, касающиеся применения переменных этих типов данных, применимы и к строкам состоящих из них коллекций. Многоуровневые коллекции В Oracle9t коллекции можно вкладывать друг в друга, создавая таким образом многоуровневые коллекции. Мы рассмотрим пример подобной структуры, а затем поговорим об ее использовании в приложениях. Предположим, что нужно разработать систему для хранения информации о нескольких домашних животных. Помимо таких сведений, как, скажем, порода, имя и возраст, в ней должна содержаться информация о периодичности посеще- посещения ветеринара, поэтому мы создаем объектный тип vet_visit_t: CREATE TYPE vet_visit_t 15 OBJECT ( visit_date DATE, reason VARCHAR2 A00) ): Обратите внимание, что объекты данного типа представляют не животных, а содержат описание визитов к ветеринару. Создадим вложенную таблицу с ин- информацией о таких визитах (предполагается, что ветеринара нужно посещать хотя бы раз в год): CREATE TYPE vet_visits_t IS TABLE OF vet_visit_t Определив эти две структуры данных, можно создать объектный тип для хра- хранения сведений о животных: CREATE TYPE pet_t IS OBJECT ( tagjio INTEGER, name VARCHARZ F0).
Коллекции в PL/SQL 379 petcare vet_visits_t, MEMBER FUNCTION set_tag_no (new_tag_no IN INTEGER) RETURN pet_t) NOT FINAL; Предлагаемый объектный тип имеет три атрибута и один метод. Каждый соз- созданный на его основе объект будет снабжен идентификатором, именем и списком с информацией о визитах к ветеринару. Для модификации идентификатора жи- животного можно вызвать программу set_tag_no. Используемый нами объектный тип объявлен с опцией NOT FINAL, чтобы впоследствии его можно было дополнить или расширить, используя введенную в Oracle9« технологию поддержки наследова- наследования. Например, для типа «домашнее животное» можно определить подтип «соба- «собака». Более подробно об объектах и наследовании рассказывается в главе 21. Итак, у нас имеется объектный тип, одним из атрибутов которого является вложенная таблица. Для отслеживания информации о визитах к ветеринару от- отдельная таблица базы данных не понадобилась — такого рода сведения содержат- содержатся в самом объекте. Ну а теперь расскажем о принципе работы с многоуровневыми коллекциями в Oracle9i. В приведенном далее примере в строках 1-10 создается и заполняется многоуровневая коллекция, а в строках 11-14 мы обращаемся к этой коллекции для получения информации: /* Файл в web: multilevel_collections.sql */ 1 DECLARE 2 TYPE bunch_of_pets_t IS TABLE OF pet_t INDEX BY BINARYJNTEGER: 3 iw_pets bunch_of_pets_t; 4 BEGIN 5 mypets CD :- 6 petjt A00. 'Mercury'. 7 vet_visits_t t 8 vet_v1sit_t COl-Jan-ZOOl1. 'Clip wings'). 9 vet_visit_t Cul-Apr-20021, 'Check cholesterol1)) 10 ); 11 DBMSJXJTPUT.putJine (my_pets (D.NAME); 12 DBMS_OUTPUT.put_line (my_pets A).petcare B).reason): 13 DBMS_OUTPUT.putJine (my_pets.COUNT): 14 DBMS J3UTPUT. put Jine (myjjetsd).petcare.LAST); 15 END; Результат выполнения этого сценария будет следующим: Mercury Check cholesterol 1 2 Как работает приведенный выше код, можно понять из следующей таблицы. Строки Описание 2, 3 Объявляется локальный тип ассоциативного массива, в котором каждая строка содержит один объект типа pet_t. Затем объявляется коллекция для хранения информации о «домашнем зверинце» продолжение^
380 Глава 11 ¦ Записи и коллекции Строки Описание 5-Ю Первому элементу ассоциативного массива в качестве значения присваивается объект типа pet_t. Как видите, синтаксис работы с подобными сложными вложенными объектами довольно громоздок. Проанализируем последовательность выполняемых действий. Для создания экземпляра объекта типа pet_t нужно задать идентификатор, имя и список визитов к ветеринару, представляющий собой вложенную таблицу, а для создания вложенной таблицы типа vet_visits_t следует вызвать одноименный конструктор. Можно задать пустой или даже неинициализированный список либо инициализировать вложенную таблицу значениями. Мы указываем эти значения в строках 8 и 9. Каждый элемент коллекции vet_visits_t является объектом типа vet_visit_t, для создания которого нам опять-таки нужен конструктор. Этому конструктору, имеющему то же имя, что и объектный тип, передаются значения каждого из атрибутов (в данном случае дата и причина визита к ветеринару) 11 вывод значения атрибута пате объекта pet_t первого элемента ассоциативного массива my_pets 12 Вывод значения атрибута reason объекта vet_visit_t второго элемента вложенной таблицы, хранящейся в первом элементе ассоциативного массива my_pets 13,14 Демонстрация того, как используются методы (в данном случае — COUNT и LAST) для внешней и вложенной коллекций В этом примере коллекции обоих уровней имели имена: внешняя коллекция (ассоциативный массив) называлась my_pets, а внутренняя (вложенная таблица) - petcare. Однако, как свидетельствует следующий пример, так бывает не всегда. Неименованные вложенные коллекции Предположим, нам нужно разработать приложение для хранения характерных для разных языков прозвищ и уменьшительных имен людей. Для начала ограни- ограничимся четырьмя языками и тремя областями использования прозвищ (в семье, среди друзей и на работе). Они определяются следующими именованными кон- константами: /* Файл в web: multilevel_collections2.sql */ CREATE OR REPLACE PACKAGE nicknames IS french CONSTANT PLS INTEGER amencan_english CONSTANT PLS~INTEGER german CONSTANT PLSJNTEGER arable CONSTANT PLSJNTEGER fromjwil.y CONSTANT PLS INTEGER fromJri ends CONSTANT PLSJNTEGER from_coneagues CONSTANT PLSJNTEGER - 1005; - 1013; - 2005; - 3107; 99: 111: Для поддержки всех этих прозвищ создадим два типа многоуровневых кол- коллекций, как в следующем разделе пакета: CREATE OR REPLACE PACKAGE nicknames IS
Коллекции в PL/SQL . 381 TYPE str1ngs_t IS TABLE OF VAKCHAR2 C0) INDEX BY BINARYJNTEGER: TYPE nickname_set_t IS TABLE OF strings_t INDEX BY BINARYJNTEGER; TYPE multiple_sets_t IS TABLE OF niclcnanie_set_t INDEX BY BINARYJNTEGER; Элементами коллекции nickname_setj; являются вложенные коллекции сим- символьных строк, каждая из которых представляет собой прозвище. Один элемент будет содержать прозвища, используемые в быту, другой — принятые среди дру- друзей, а третий — на работе. В коллекции multiple_sets_t каждый элемент является коллекцией прозвищ. Один содержит их английские варианты, другой француз- французские и т. д. Обратите внимание, что столбцы обоих типов, nickname_set_t и multip- le_setsj, не имеют имен — задаются только их типы данных. В описываемом пакете используется несколько функций для преобразования прозвищ (tojrench, to_german и to_arabic). Каждая из них принимает набор про- прозвищ на английском языке и возвращает их перевод на нужный язык в виде кол- коллекции того же типа. Заголовок функции может быть таким: FUNCTION tojrench (nicknamesjn IN nickname_setj) RETURN n1ckname_setj:: Откомпилировав пакет nicknames, вы можете использовать все его компонен- компоненты, даже не думая о том, как они реализованы. Следующая программа демонстри- демонстрирует принцип применения многоуровневой коллекции с анонимными столбцами: 1 CREATE OR REPLACE PROCEDURE set_steven_nicknames 2 IS 3 stevenjii cknames nicknames. nickname_setj: 4 universaljiicknames nicknames. multiple_setsj; 5 BEGIN 6 steven_nicknames (99) AQG0) :- 'Steve': 7 steven_n1cknames 6 (n1cknames.fram_colleagues) ?2000) ;= 'Troublemaker'; 9 stevenjiicknames 10 (nicknames.from_colleagues) C000) := 'All-around Great Guy'; 11 stevenjiicknames 12 (nicknames.from family) G89) := 'Whinner'; 13 14 15 universaljiicknames nicknames.american_english) := 16 stevenjiicknames; 17 universal jiicknames (nicknames.french) :- 18 nicknames.tojrench (stevenjiicknames); 19 universal jiicknames (nicknames.german) := 20 nicknames.to_german (stevenjiicknames): 21 universaljiicknames (nicknames.arable) .-- 22 nicknames.to arabic (Steven nicknames): 23 ~ 24 DBMS_OUTPUT.PUT_LINE ( 25 universaljiicknames 26 (nicknames.americanjenglish) 27 [nicknames.from_colleagues)
382 Глава 11 • Записи и коллекции 28 B000)); 29 30 DBMS_OUTPuT.PUT_LINE ( 31 universaljricknamesU005Klll)C2000)): 32 END: Результат выполнения сценария (но лишь при условии реализации програм- программы перевода) будет следующим: Troublemaker Provocateur Осуществляемые в приведенном фрагменте кода действия перечислены в таб- таблице, которую вы видите ниже. Строки Описание 3, 1 Определение двух коллекций: одной — для прозвищ на английском языке, а другой — для прозвищ на других языках 6-12 Заполнение коллекции steven_nicknames четырьмя прозвищами: одно принято среди друзей, два — среди коллег и одно среди членов семьи. В строке б используются жестко закодированные литералы, а в строках 7-12 константы. Номера элементов, в которые записываются символьные строки прозвищ, могут быть любыми. В строке б приведен оператор выбора строки в многоуровневой анонимной коллекции: steven_nicknames (99) A000) := 'Steve';. Этот оператор помещает значение 'Steve' в 1000-ю строку внутренней коллекции, входящей в состав 99-й строки внешней коллекции. Поскольку внутренние коллекции являются анонимными, можно просто задать два индекса подряд 15-22 Переход на другой уровень иерархии коллекций. Прозвища на английском языке теперь переводятся на французский, немецкий и арабский. Полученные в результате перевода коллекции записываются в соответствующие им коллекции universaLnlcknarnes. Чтобы избежать ошибок и путаницы, используются именованные константы; код при этом становится более читабельным 24-31 В последних строках процедуры выводится информация из результирующей коллекции. Этот фрагмент кода демонстрирует трехуровневую индексацию: сначала с помощью именованных констант, а затем — явно заданными литеральными значениями: universal_nicknamesA005)(lll)B000) Код, как видите, довольно сложный, особенно при работе с анонимными столб- столбцами коллекций. Однако его можно упростить, если использовать коллекции объектов или записей, в которых каждый столбец имеет имя (как атрибут объект- объектного типа или поле записи). Если у вас возникнет вопрос, насколько глубоко можно вкладывать такие мно- многоуровневые коллекции, проведите небольшое исследование. Напишите генера- генератор кода для прохождения по многим уровням и с его помощью создайте проце- процедуру, которая объявляла бы N типов коллекций (каждый такой тип должен слу- служить вложенной таблицей — TABLE OF — элементов предыдущего типа), а затем присваивала бы значение элементу, принадлежащему последнему уровню полу- полученной структуры. Например, автору удалось создать коллекцию из 250 вложенных коллекций, после чего его компьютер выдал ошибку памяти. Конечно, придумать задачу, для
Коллекции в PL/SQL 383 решения которой потребовалось бы создавать столь сложную структуру, непро- непросто. Можно смело сказать, что практически количеству вложений коллекций в PL/SQL нет предела. Чтобы провести предлагаемый эксперимент в своей сис- системе, можете воспользоваться файлом gen_multcoll.sp, представленным на узле из- издательства O'Reilly. Многоуровневые коллекции сложны для понимания и сопровождения, но они обладают невероятной гибкостью, благодаря чему с их помощью можно реализо- вывать очень оригинальные решения. Последовательные и непоследовательные ассоциативные массивы Ассоциативные массивы, как вы помните, могут быть разреженными. Поэтому совершенно не обязательно записывать данные сначала в элемент 1, затем - в элемент 2 и г. д. Это означает, что, создавая ассоциативный массив, вы встаете перед выбором, каким образом его заполнить. При последовательном заполнении коллекции операция начинается с любого элемента (чаще всего — с 0 или 1) и новые значения добавляются в конец коллекции, причем индекс очередного элемента увеличивается на 1. Этот подход хорош для случаев, когда имеет значение поря- порядок элементов в списке. Если заполнение осуществляется в произвольном поряд- порядке, номер элемента используется в качестве своего рода «целочисленного ключа». Обычно он равен значению первичного ключа таблицы базы данных, из которой данные считываются в коллекцию. Возможность случайного (то есть не последо- последовательного) размещения значений в таблице очень полезна в тех случаях, когда первичные ключи записей таблицы генерируются не непосредственно, а опреде- определяются данными приложения. Примеры использования каждого из указанных подходов представлены ниже. Последовательное заполнение коллекции Попробуем заменить функцию Oracle TODATE собственной, более удобной для ра- работы функцией. Как можно добиться того, чтобы она, скажем, преобразовывала любое строковое представление даты в реальную дату? Это позволило бы пользо- пользователю задавать дату в произвольном строковом формате. Идея создаваемой функции заключается в следующем. Сначала в коллекции сохраняется несколько шаблонов форматов. Затем вызывается функция niy_to_ date, которая последовательно считывает из нее шаблоны и, используя таковые, пытается преобразовать строку символов в значение даты. Это должно продол- продолжаться до тех пор, пока один из шаблонов не окажется подходящим. Результат преобразования строки будет зависеть от порядка записи шаблонов форматиро- форматирования в коллекции. Например, строка 04-02-01 может быть интерпретирована как MM-OD-RR (месяц, день, год) или DD-HM-RR (день, месяц, год), в зависимости от того, какой из шаблонов проверяется первым. Код функции my_to_date приведен ниже: /* Файл в web: mytodate.sf */ CREATE OR REPLACE FUNCTION my_to_date (valuejn IN VARCHAR2) RETURN DATE IS TYPE mask_t IS TABLE OF VARCHAR2 C0) INDEX BY BINARY INTEGER;
384 Глава 11 • Записи и коллекции fmts mask_t; retval DATE .- NULL: maskjndex INTEGER := 1; date_converted BOOLEAN :- FALSE: PROCEDURE init_fmts IS BEGIN frat5 A) fmts B) fmts C) fmts D) = 'OD-MON-RR1: - 'DD-MON-YYYY': - 'DD-MON1; - 'MM/DD1: END; BEGIN init_fmts; WHILE maskjndex IS NOT NULL AND NOT date_converted LOOP BEGIN retval :-TODATE Cvaluejn. fmts (maskjndex)): date_converted :- TRUE; EXCEPTION WHEN OTHERS THEN maskjndex :- fmts.NEXT (maskjndex); IF maskjndex IS NULL THEN RAISE; END IF: END; END LOOP; RETURN retval; END my_to_date; Заполнение коллекции в произвольном порядке При функционировании многих приложений баз данных выполняется большое количество однотипных запросов, извлекающих из нее информацию, которая очень редко изменяется (или вообще не изменяется). Возникает вопрос: если ин- информация не изменяется, зачем но нескольку раз обращаться к базе данных? Бо- Более того, зачем это делать на протяжении одного пользовательского сеанса? Ведь даже если данные кэшируются в глобальной области системы (System Global Area, SGA), необходимо время для поиска информации в буферах данных, а также для ее возврата в программную область сеанса (Program Global Area, PGA). .Чтобы избежать лишних затрат, вы можете взять за правило следующее: нико- никогда не обращаться к одной и той же справочной таблице более одного раза в тече- течение одного сеанса. При первом обращении эту таблицу можно сохранить в про- программной области сеанса, и с этого момента она станет доступной для всех после- последующих запросов. В качестве ключа для выборки данных можно использовать индекс коллекции.
Коллекции в PL/SQL Рассмотрим предлагаемый механизм на примере. Предположим, у нас имеет- имеется таблица hairstyles, содержащая числовые коды (первичные ключи) и назва- названия причесок. Эти названия меняются крайне редко. Ниже приведено тело паке- пакета, в котором создается коллекция для кэширования пары значений, включаю- включающих код и название. /* файл s web: justonce.sql */ 1 CREATE OR REPLACE PACKAGE BODY justonce 2 IS 3 TYPE desc_t IS TABLE OF hairstyles.descriptionXTYPE 4 INDEX BY BINARYJNTEGER; 5 descriptions desc_t; 6 7 FUNCTION description (codejn IN hairstyles.codelTYPE) 8 RETURN hairstyles-descriptionlTYPE 9 IS 10 returnvalue hairstyles. descriptionSITYPE; 11 12 FUNCTION descjromjiatabase RETURN hairstyles.description*TYPE 13 IS 14 CURSOR desc_cur IS 15 SELECT description FROM hairstyles WHERE code - codejn; 16 desc rec desc_curSROWTYPE: 17 BEGIN 18 OPEN desc_cjr; 19 FETCH desc_cur INTO desc_rec: 20 RETURN descrec. description: 21 END; 22 BEGIN 23 RETURN descriptions (codejn); 24 EXCEPTION 25 WHEN NOJATAJOUNO THEN 26 descriptions (codejn) :- desc fromjiatabase: 27 RETURN descriptions (codejn); 23 END: 29 END justonce: Важнейшие аспекты работы этой программы описаны в таблице. Строки Описание 3-5 Определяется тип коллекции, и объявляется коллекция для хранения кэшируемых данных 7, 8 Задается заголовок функции. Функция выполняет типичный запрос к базе данных и извлекает значения столбца description с заданным значением в столбце code. Код ее реализации скрыт, как и полагается в таких случаях 12-21 Производится тривиальный запрос к базе данных. Только теперь мы имеем дело с локальной функцией внутри главной функции 23 Это весь исполняемый раздел! Он просто возвращает название прически, хранящееся в строке с заданным кодом. При первом вызове этой функции строка еще не выбрана. Поэтому PL/SQL инициирует исключение NO_DATA_FOUND (см. строки 25-27). А во всех последующих обращениях к коду строка уже определена, и функция сразу возвращает значение продолжением
386 Глава 11 • Записи и коллекции Строки Описание 25-27 В течение сеанса данные еще не запрашивались. Поэтому нужно перехватить сообщение об ошибке, найти информацию в базе данных и поместить ее в коллекцию, а после этого вернуть значение. Теперь все готово к последующим операциям выборки Вы спрашиваете, какая польза от такого кэширования? Автор провел неболь- небольшое тестирование, работая с локальной базой данных на своем компьютере, и об- обнаружил, что на выполнение 10 000 запросов к таблице hairstyles уходит около 2 с. Это достаточно высокое быстродействие. В то же время выполнение тех же 10 000 запросов с помощью приведенной выше функции заняло всего 0,1 с. Уско- Ускорение более чем на порядок — и это только при работе с локальной базой данных! В реальных условиях оно, надо полагать, будет гораздо более значительным, так что потрудиться стоит. Приведем еще несколько замечаний, касающихся описанной технологии кэ- кэширования. Речь пойдет о классической задаче выбора компромиссного решения между загрузкой центрального процессора и памяти. Для каждого сеанса создает- создается собственная копия коллекции (это данные программы, хранящиеся в ее PGA). Если к базе данных подключены 10 000 пользователей, то для такого же количе- количества кэшей потребуется достаточно большой объем памяти. Поэтому данный подход годится только для небольших статических таблиц. А для того чтобы статиче- статическую таблицу можно было кэшировать в коллекции, у таблицы должен быть це- целочисленный первичный ключ. Если первичный ключ формируется на основе нескольких полей или является строковым значением, для формирования хэш- значений, которые можно использовать в качестве индексов в коллекции, можно воспользоваться функцией DBMS_UTILITY.GET_HASH_VALUE. Однако программа в ре- результате этого станет более сложной и менее привлекательной. Несколько под- подробнее о кэшировании данных рассказывается в главах 17 и 20. Передача ассоциативных массивов в качестве параметров Мы уже рассмотрели пример использования коллекций в качестве параметров модуля. Однако применение в этом качестве ассоциативных массивов имеет не- некоторые особенности. Коллекции в языке PL/SQL — это просто один из типов данных, и не удиви- удивительно, что их можно передавать в процедуры или функции. Такое свойство кол- коллекций чрезвычайно важно на практике, поскольку позволяет в одном вызове пе- передать в модуль все значения, которые хранятся в таблице. В приведенной ниже спецификации пакета определяются два модуля. Первый из них принимает в ка- качестве параметра таблицу PL/SQL, а второй — возвращает ее вызывающему коду. Процедура send_promos отправляет всем перечисленным в таблице компаниям рек- рекламные листки, а функция compani es_overdue формирует таблицу с названиями компаний, которые имеют неоплаченные счета. CREATE OR REPLACE PACKAGE companyjkg IS /* Вложенная таблица, структура которой соответствует структуре таблицы company */ TYPE companies_tabtype IS TABLE OF companySROWTYPE;
Коллекции в PL/SQL 387 /* Параметром является таблица первичных клочей компаний */ PROCEDURE send_promos Ccompanies_in IN companies_tabtype); /* Функция возвращает таблицу с названиями коипаний */ FUNCTION companies_overdje (overdue_date_in IN DATE) RETURN compani esjabtype; END company_pkg; Итак, у нас есть пакет, содержащий определения типа таблицы и процедуры, где используются таблицы указанного типа. Теперь заданные процедуры можно вызывать, объявив предварительно переменную типа compani es_tabtype. Вот как это делается: CREATE OR REPLACE PROCEDURE send_1nw>1ces IS indx PLSINTECER: ;* Объявляем вложенную таблицу на основе определенного в пакете типа *1 companies company pkg. corapaniestabtype; BEGIN companies :- company_pkg.companies_averdue (SYSDATE-30); indx :- companies.FIRST; LOOP EXIT WHEN indx IS NULL: send_i nvoi ce (compani es(i ndx).companyj d): indx := companies.NEXT (indx): END LOOP; END: Интеграция с сервером Рассмотрим еще один пример, демонстрирующий, насколько использование кол- коллекций может облегчить пересылку данных между программой PL/SQL и серве- сервером. Основным логическим объектом в нашем примере является список квартир, для хранения которого используется вложенная таблица объектов. Все квартиры описываются с помощью следующих атрибутов: CREATE TYPE Apartment_t AS OBJECT ( unitjro NUMBER, square_feet NUMBER. bedrooms NUMBER. bathrooms NUMBER. rent_in_doTlars NUMBER ): На основе этой структуры можно определить вложенную таблицу для хране- хранения списка квартир: CREATE TYPE Appartment_tab_t AS TABLE OF Appartment_t; Теперь, задавая для столбца табличный тип, можно создать таблицу базы дан- данных: CREATE TABLE apartment_camplexes ( name VARCHAR2G5). landlordname VARCHARZC45),
388 Глава 11 • Записи и коллекции apartments Apartment_tab_t) NESTED TABLE apartments STORE AS apartments_store_tab; Приведем инструкцию INSERT, заполняющую полученную таблицу (обратите внимание на вложенные вызовы конструкторов, создающие коллекцию объек- объектов): INSERT INTO араrtment_complexes VALUES ('RIVER OAKS FOUR','MR. JOHNSON', Apartment_tab_t( Apartment_t(l. 780. 2. 1. 975). Apartment_tB. 1E00. 3, 2. 1590). Apartment_tC3, 690, 1. 1.5. 800), Apartment_tD, 690. 1. 2. 450). Apartment_tE. 870. 2. 2. 990) INSERT INTO apartment_comp1exes VALUES CGALLERIA PLACE'. 'MS. DODENHOFF', Apartment_tab_t( Apartment_tA01. 1000. 3. 2. 1295), Apartnent_tC102. 800. 2. 1. 995). Apartment_tA03, 800. 2. 1. 995), Apartment_tC2Dl, 920, 3. 1.5. 1195). Apartment_tB02. 920. 3. 1.5. 1195). Apartment_tB05. 1000, 3, 2, 1295) На этом подготовительная работа заканчивается, и теперь можно продемонст- продемонстрировать замечательные возможности, которые мы получаем благодаря хране- хранению коллекций в базе данных. Предположим, что новый менеджер квартирного комплекса River Oaks Four, в отличие от своего предшественника, не намереп заниматься дешевыми кварти- квартирами. Он решил отказаться от квартир, арендная плата за которые составляет ме- менее 500 долларов, и на 15% повысить цену на все остальное жилье. Это можно реализовать с помощью следующего кода: DECLARE /* Объявление курсора для извлечения коллекции объектов- || квартир. Перед обновлением записи || ее стоит заблокировать с помощью параметра FOR UPDATE */ CURSOR aptcur IS SELECT apartments FROM apartment_complexes WHERE name = 'RIVER OAKS FOUR' FOR UPDATE OF apartments: /* Необходим объявить локальную переменную для хранения коллекции |[ считанной из базы данных информации о квартирах */ l_apartments apartment_tab_t: which INTEGER:
Коллекции в PL/SQL 389 BEGIN /* Достаточно единственной выборки! */ OPEN aptcur: FETCH aptcur INTO l_apartments; CLOSE aptcur; /* Просмотр объектов-квартир в коллекции и удаление || тех из них. которые не соответствует заданному критерию */ which :- I apartments.FIRST; LOOP EXIT WHEN which IS NULL: IF l_apartments(wh1ch).rentJn_doTlars < 500 THEN l_apartments.DELETECwhich): END IF; which := l_apartments.NEXT(which); END LOOP: /* Просмотр оставшихся квартир и увеличение арендной || платы. Обратите внимание, что этот код не обрабатывает, || удаленные элементы */ which :- l_apartments.FIRST; LOOP EXIT WHEN which IS NULL; l_apartments(which).rent_in_doTlars :- l_apartments(which).rent_in_dollars * 1.15; which :- l_apartments.NEXT(which); END LOOP: /* В завершение передача всей коллекции квартир || обратно на сервер - опять-таки одной командой! */ UPDATE apartment_complexes SET apartments - l_apartments WHERE name - 'RIVER OAKS FOUR1; END; В этом примере наиболее примечательными моментами являются выборка и запись данных — операции, каждая из которых выполняется одной командой. Приведенный код эмулирует создание клиентского кэша данных, который явля- является одним из важных компонентов многих объектно-ориентированных архитек- архитектур и архитектур типа клиент-сервер. Клиентский кэш помогает сократить сете- сетевой трафик, а коллекции представляют собой удобное средство для создания кэша. Использование ассоциативных массивов с индексами типа VARCHAR2 В Огас1е9г Release 2 таблицы PL/SQL, называемые теперь ассоциативными мас- массивами, приобрели невероятную гибкость. В частности, в качестве их индексов могут выступать не только целочисленные значения (то есть номера элементов), но и символьные строки. Рассмотрим несколько примеров применения такой но- новой возможности.
390 Глава 11 • записи и коллекции Следующий блок кода демонстрирует основы данной технологии. /* Файл в web: assoc_array.sql */ DECLARE TYPE population_type IS TABLE OF NUMBER INDEX BY VARCHAR2C64); country_population population_type; continent_popu1ation populati on_type: hownany NUMBER: limit VARCHAR2F4): BEGIN country_population('Greenland'):- 100000; countrypopul ati on(' Ice! and'):» 750000: continent_population('Australia'):- 30000000; continent_popu1ation('Antarctica'):-1000; -- Создание нового зленента continent_popul ati on(' Antarcti ca'): - 1001; -- Замена предыдущего значения limit :=¦ continent_population.FIRST; DBMS_OUTPUT.PUT_LINE [limit); 08MS_0UTPUT.PUT_LINE (continentj»pulat1onA1m1t)); limit :- continent_population.LAST; DBMS_OJTPUT.PUT_LINE {limit): DBMSJXJTPUT.PUT LINE (continent_population(Hmit)): END: Результат выполнения этого кода: Antarctica 1001 Australia 30000000 Обратите внимание, что для ассоциативного массива данного типа методы FIRST, LAST, PRIOR и NEXT возвращают не числа, а строки. Заметьте также, что в опре- определении типа ассоциативного массива не может использоваться спецификация XTYPE — следует задавать литеральные, жестко закодированные определения. Ну а для чего, спросите вы, может понадобиться использовать строковую ин- индексацию коллекций вместо числовой? Предположим, нам необходимо произве- произвести большой объем операций по обработке информации о сотрудниках. Это по- потребует множества перемещений по набору записей о сотрудниках в обоих на- направлениях, выполнения большого количества операций, связанных с поиском нужной записи по кодам сотрудников, их фамилиям и идентификационным ко- кодам, принятым во многих странах. DECLARE ТУРЕ name_t IS TABLE OF employeeSROWTYPE INDEX BY VARCHAR2A00): TYPE id_t IS TABLE OF employeeSROWTYPE INDEX BY BINARYJNTEGER; by_name name_t; by_ssn name_t:
Коллекции в PL/SQL 391 by_id id_t: ceojiame employee, last jiamemPE :- 'ELLISON1; PROCEDURE load_arraj/s IS BEGIN FOR rec IN (SELECT * FROM employee) LOOP -- Загрузка трех массивов за осин проход по таблице базы данных, byjiame (rec.lastjiame) :- rec: by_ssn (rec.ssn) :- rec; byjd (rec.employee_id) :- rec; END LOOP; END: BEGIN load_arrays; -- Считывание информации, соответствующей определенной фамилии или коду: IF byjiame (ceojiame).salary > by_id G645).salary THEN makejjdjustment (byjiame): END IF; END: Как следует из этого примера, создать несколько точек входа для кэшируемых данных, считанных из реляционной таблицы, вовсе не сложно. А для того чтобы еще более упростить для вас реализацию предлагаемой технологии, автор книги разработал утилиту, которую вы найдете в файле genaa.sp на узле издательства O'Reilly. Указанная утилита генерирует пакет для кэширования заданной табли- таблицы реляционной базы данных. Создаваемый ею пакет заполняет коллекцию на основе целочисленного первичного ключа и еще по одной коллекции на основе каждого определенного для таблицы уникального индекса (индексируются зна- значениями типа BINARY_INTEGER и VARCHAR2 в зависимости от типа столбца индекса). Пример использования ассоциативных массивов, индексируемых значениями типа VARCHAR2, для работы в PL/SQL со списками информации содержится в фай- файле summer_reading.pkg. Эмуляция альтернативных индексов коллекций До появления Oracle9i Release 2 в качестве индексов ассоциативных массивов можно было использовать только значения типа ВIN ARY_ INTEGER. Для поиска, ска- скажем, конкретной книги в коллекции, состоящей из большого количества элемен- элементов, приходилось сканировать всю коллекцию, что занимало много времени. Су- Существует ли другой способ решения такого рода задачи? Первое, что можно пред- предложить в подобной ситуации, — создать собственный, альтернативный индекс коллекции. Проще всего это сделать с помощью предоставляемой Oracle встроен- встроенной функции хэширования DBMS_UTILITY_GET_HASH_VALUE. Функция принимает стро- строку, а возвращает целочисленное значение, которое должно быть уникальным для каждой отличной от остальных строки, но на практике гарантировать эту уни- уникальность невозможно. В результате приходится включать в программу логику
392 Глава 11 • Записи и (Коллекции разрешения конфликтов. Звучит пугающе? На самом деле все очень просто, при- причем вам даже не придется реализовывать такой алгоритм. В файле altind.pkg, со- содержащемся на узле издательства O'Reilly, вы найдете код обработки информа- информации о сотрудниках, который очень просто адаптировать для работы с любой дру- другой таблицей. Основная идея этой программы следующая. Заполняя главную коллекцию ин- информацией, которую вам предстоит просматривать и обрабатывать (несколько раз, с разных позиций и т. д.), вы должны будете одновременно генерировать хэш-значения для значений альтернативного индекса и использовать их в качестве номеров элементов его коллекции. Таким образом, по окончании загрузки у вас будут заполнены две коллекции: одна с обрабатываемыми данными, другая — с номерами элементов первой коллекции, соответствующими значениям альтер- альтернативного индекса. Когда вам понадобится найти в первой коллекции элемент на основе строкового значения (например, фамилии сотрудника), вы определите хэш-код этого строкового значения, найдете во второй коллекции элемент с но- номером, равным этому хэш-коду, и считаете из него номер искомого элемента главной коллекции. Графическое представление описанного процесса показано на рис. 11.1. Хэшируемое символьное значение, на основе которого генерируется номер элемента альтернативного индекса Коллекция кэшируемых данных Полный набор данных о сотрудниках Коллекция альтернативного индекса Код и фамилия сотрудника Номер элемента ШШйМ 78955 fe 2030S5 мштшясякв 1109878 1055 SMITH 3458 FELLON 7988 DIONA Полный цикл поиска строки по фамилии Рис. 11.1. Заполнение коллекции и доступ к хэш-индексу Все эти данные лучше всего объединить в инициализационном разделе паке- пакета, как показано ниже на примере пакета aitind: CREATE OR REPLACE PACKAGE BODY ALTIND IS ... тело пакета ... PROCEDURE loadcache IS BEGIN loadtab.DELETE:
Коллекции в PL/SQL 393 hashtab.DELETE: FOR гее IN ( SaECT * FROM employee) LOOP loadtab Crec.emp'loyeejd) :=• rec; add_to_altind (rec.last_name. rec.employeejd): END LOOP;" END; BEGIN loadcache: END; За дополнительной информацией по вопросам реализации этой технологии обращайтесь к файлу alOnd.pkg. Псевдофункции коллекций Псевдофункции Oracle позволяют работать с таблицами базы данных как с кол- коллекциями, а с коллекциями — как с таблицами. Они предоставляют в распоряже- распоряжение программистов богатый и интересный набор средств, значительно облегчаю- облегчающих решение тех задач, для которых удобно использовать определенное представ- представление данных. ВНИМАНИЕ Псевдофункции коллекций не являются функциями PL/SQL — они существуют только в SQL. Для их использования в PL/SQL приходится включать в программы соответствующие SQL-инструкции. Из последующих разделов вы узнаете, как это делается. Существует четыре псевдофункции коллекций. О THE (в настоящее время пользоваться ею не рекомендуется) — представляет значение одного столбца-коллекции одной строки таблицы базы данных в виде виртуальной таблицы. Эта псевдофункция позволяет манипулировать элемен- элементами коллекции, хранящейся в базе данных. О CAST — преобразует коллекцию одного типа в коллекцию другого типа. С ее по- помощью можно, например, преобразовать массив VARRAY во вложенную таблицу. О MULTISET — представляет таблицу базы данных в виде коллекции. С помощью функций MULTISET и CAST из таблицы базы данных можно считать набор строк и представить его в виде столбца типа коллекции. О TABLE — представляет коллекцию в виде таблицы базы данных. Выполняет дей- действие, обратное производимому функцией MULTISET. Эти псевдофункции предназначены для работы с коллекциями, хранящимися в базе данных. Обладая рядом важных достоинств, они, в частности, обеспечива- обеспечивают невероятно эффективное перемещение данных между базой данных и прило- приложением. Правда, нельзя сказать, что эти псевдофункции просты и понятны. Но если вы не пожалеете времени на их освоение, в вашем распоряжении окажется мощный и гибкий инструментарий, который не раз сослужит вам хорошую службу.
394 Глава 11 • Записи и коллекции Псевдофункция THE Если в таблице базы данных имеется столбец типа вложенной таблицы и вам нужно вставлять, обновлять, удалять его содержимое, традиционные SQL-инст- SQL-инструкции для этой цели не подходят. Oracle предлагает в таком случае использо- использовать ключевое слово THE, указывающее, какая строка внешней таблицы должна быть обработана. ВНИМАНИЕ В Огас1е81 и более поздних версиях оператор THE заменен оператором TABLE, о котором речь пойдет в следующем разделе. Как вы помните, ранее нами была создана таблица colorjiradels: CREATE TABLE colorjnodels ( model_type VARCHAR2A2). colors Color_tah_t) NESTED TABLE colors STORE AS color_model_colors_tab; В нее была добавлена строка, в столбце modeltype которой содержалось значе- значение ' RGB', а в столбце colors - значения ('RED', 'GREEN', 'BLUE'). Теперь предпо- предположим, что таблица colorjrradels заполнена полудюжиной записей. Как с помо- помощью инструкции SELECT извлечь из нее информацию обо всех цветах, составляю- составляющих заданную цветовую модель? Это можно сделать так: SELECT VALUE(c) FROM THE(SELECT colors FROM colorjiradels WHERE modeljype - 'RGB') c; Данная инструкция означает следующее: «извлечь отдельные элементы моде- модели RGB» или же, если говорить более точно, «извлечь значение каждого элемента вложенной таблицы colors, входящей в состав таблицы col orjnodel s». Возвращае- Возвращаемые ею результаты будут такими: VALUECC) RED GREEN BLUE Собственно говоря, в этом нет ничего необычного - мы просто включили в пред- предложение FROM вложенный запрос. Приведенный выше запрос можно было бы сфор- сформировать несколько иначе, воспользовавшись предопределенным псевдонимом столбца COLUMN_VALUE. Он предназначен специально для ссылки на элементы вло- вложенной таблицы, где хранятся скалярные значения. Рассмотрим еще одну инст- инструкцию, возвращающую те же результаты, что и предыдущая: SELECT COLUMN_VALUE FROM THE(SELECT colors FROM colorjnodeis WHERE model_type - 'RGB1); Интересно, что вложенный запрос THE можно использовать в качестве источни- источника данных и в других SQL-инструкций, а именно INSERT, JPDATE и DELETE. Проиллю- Проиллюстрируем сказанное несколькими примерами: BEGIN -- Замена в коллекции значения BLUE значением BURGUNDI
Коллекции в PL/SQL 395 UPDATE TTOSELECT colors FROM col orjnodel s WHERE model_type - 'RGB') SET COLUMNJALUE - 'BURGUNDY1 WHERE COLUMNJALUE - 'BLUE': -- Добавление еще одного цвета INSERT INTO THE ( SELECT colors FROM colorjnodels WHERE mode1_type - 'RGB') VALUES ('EXTRA-COLOR'): -- Вывод информации о текущих цветах SELECT COLUMNJALUE FROM THE(SELECT colors FROM color_models WHERE model_type - 'RGB'): -- Удаление лишнего ивета DELETE THEtSELECT colors FROM colorjiodels WHERE model J:ype - 'RGB1) WHERE COLUMNJALUE - 'EXTRA-COLOR': END: Псевдофункция TABLE Оператор TABLE преобразует столбец-коллекцию в такую структуру, из которой данные можно извлекать с помощью инструкции SELECT. Звучит мудрено, но на практике эта операция выполняется очень легко. Предположим, у нас имеется таблица базы данных со столбцом типа коллек- коллекции. Как узнать, какие строки таблицы содержат коллекции, соответствующие заданному критерию? Иными словами, как выполнить выборку из таблицы базы данных, задав в предложении WHERE условие, которому должно соответствовать содержимое коллекции? Хорошо было бы просто написать: SELECT * FROM tablejiame WHERE collect!on_column HAS CONTENTS 'whatever': -- Неправильный синтаксис! Логически это именно те функции, которые выполняет оператор TABLE. Теперь вернемся к таблице col orjnodel s. Как выбрать из нее список всех цветовых моде- моделей, содержащих цвет RED? Если мы сделаем это следующим образом: SELECT * FROM colorjnodels c WHERE 'RED' IN (SELECT * FROM TABLEtc.colors)); SQL*Plus выведет такой результат MODEL TYPE COLORS RGB COLORJABJCREO', 'GREEN'. 'BLUE') Запрос можно сформулировать и по-другому: «пройти по таблице col orjnodel s и вернуть все строки, в которых список цветов содержит хотя бы один элемент RED». Если бы описываемых цветовых моделей, содержащих цвет RED, в таблице было больше, SQL "Plus вывел бы их всех. Как следует из последнего примера, оператор TABLE в качестве параметра при- принимает имя столбца-коллекции, уточненное псевдонимом таблицы: TABLE(псеводоним.нмякоплекции)
396 Глава 11 • Записи и коллекции а возвращает содержимое коллекции, представленное в виде виртуальной табли- таблицы базы данных. Поэтому из результирующей структуры данные можно извле- извлекать при помощи инструкции SELECT. В нашем примере она используется в подза- подзапросе. Не напоминает ли вам псевдофункция TABLE псевдофункцию THE? Они обе воз- возвращают структуры данных, которые могут использоваться в оставшейся части SQL-инструкции в качестве виртуальных таблиц базы данных, а различаются толь- только входными данными. Функция TABLE оперирует вложенной таблицей (столб- (столбцом), а функция THE — результатом, возвращаемым инструкцией SELECT, то есть строкой с одним столбцом типа вложенной таблицы. При этом, когда вы используете псевдофункцию THE в качестве источника дан- данных для операции INSERT, UPDATE или DELETE, «за сценой» вызывается функция TABLE, преобразующая результат вложенного запроса в виртуальную таблицу базы дан- данных, с которой может работать DML. Повторяем еще раз: ни одна из псевдофункций коллекций не может использо- использоваться в PL/SQL, но программисты должны о них знать, чтобы применять их в SQL-инструкциях. Псевдофункции, и прежде всего TABLE, особенно полезны при работе с новыми табличными функциями Oracle9f. К этой категории, как вы пом- помните, относятся функции, возвращающие коллекции. Табличные функции могут использоваться в предложении FROM SQL-запросов. О них достаточно подробно рассказывается в главе 16. На наш взгляд, все эти новые возможности просто замечательны, а отдельные трудности, с которыми приходится сталкиваться при их освоении и использова- использовании, следует воспринимать как увлекательнейшую гимнастику для ума. Возмож- Возможно, не все из вас разделяют это мнение, но наверняка никто не станет отрицать, что программировать для Oracle, по крайней мере, не скучно. Псевдофункция CAST Псевдофункция THE не работает непосредственно с массивами VARRAY. Однако с по- помощью оператора CAST вы можете представить такой массив в виде вложенной таблицы и использовать результат в инструкциях SELECT, подобных описанным в предыдущем разделе. Преобразование именованных коллекций Ниже приведен пример преобразования именованной коллекции типа данных VARRAY во вложенную таблицу. Предположим, у нас имеется таблица col orjnodel s со столбцом типа VARRAY: CREATE TYPE Color_array_t AS VARRAYU6) OF VARCHAR2C0): CREATE TABLE color_models_a ( model type VARCHAR2U2). colors Co1or_array_t): Столбец colors можно преобразовать во вложенную таблицу и использовать как аргумент псевдофункции THE: SELECT COLUMNJALUE FROM THE(SELECT CASTtcolors AS Color_tab_t) FROM color_models_a WHERE model_type - 'FGB1);
Коллекции в PL/SQL 397 В этой инструкции функция CAST преобразует коллекцию типа colorarrayt в коллекцию типа color_tab_t. Между функциями CAST и THE имеются существенные различия. Как было от- отмечено в предыдущем разделе, функция THE может использоваться в левой и пра- правой части инструкций INSERT, UPDATE и DELETE. Вложенные запросы THE обычно слу- служат источниками или приемниками данных этих инструкций DML. Что касается функции CAST, то она используется только в предложениях SELECT или в правой части DML-инструкций. Преобразование неименованных коллекций Функция CAST способна преобразовать набор записей, представляющий собой ре- результат выполнения вложенного запроса, в коллекцию нужного типа. Правда, она может это сделать лишь с помощью функции MULTISET, о которой рассказывается в следующем разделе. Псевдофункция MULTISET Функция MULTISET используется только как аргумент функции CAST. Она позволя- позволяет извлечь из базы данных набор значений и тут же преобразовать его в коллек- коллекцию нужного типа. Приведем простейшую форму этой псевдофункции: SELECT CAST [MULTISET (SELECT поле FROM таблица) AS тип коллекции) FROM DUAL: Если у нас имеется реляционная таблица цветов: CREATE TABLE some_colors ( colorname VARCHAR2C3C). color_classification VARCHAR2C0)); и мы хотим преобразовать ее в коллекцию, чтобы посредством одной команды скопировать в программную переменную, это можно сделать так: DECLARE some_hot_colors Color_tab_t: BEGIN SELECT CAST(MULTISET(SELECT colorjiame FROM some_colors WHERE color_classification - 'HOT1) AS Color_tab_t) INTO some_hot_colors FROM DUAL; END: Кроме того, функция MULTISET используется в коррелированных подзапросах, включаемых в список SELECT: SELECT outerfield. CAST(MULTISET(SELECT field FROM whateverTable WHERE correlationCriteria) AS collectionTypeName) FROM outer-Table: Данная технология позволяет представлять объединения в виде таблиц с кол- коллекциями. Рассмотрим в-качестве примера таблицу с информацией о птицах
398 Глава 11 • Записи и коллекции и связанную с ней таблицу стран, в которых водится каждый из упоминаемых в первой таблице видов птиц: CREATE TABLE birds ( genus VARCHAR2A28). species VARCHAR2A28). colors Colorarrayt. PRIMARY KEY (genus, species) >; CREATE TABLE birdjiabitats ( genus VARCHAR2U28). species VARCHAR2C128), country VARCHAR2C60). FOREIGN KEY (genus, species) REFERENCES birds (genus, species) ): CREATE TYPE Country_tab_t AS TABLE OF VARCHAR2F0): Нам нужно объединить главную и подчиненную таблицы с помощью инструк- инструкции SELECT, преобразующей подчиненные записи в коллекции. Такая возможность имеет огромное значение для приложений с архитектурой клиент-сервер, посколь- поскольку позволяет значительно сократить количество обращений к серверу и при этом не дублировать записи главной таблицы для каждой записи подчиненной табли- таблицы. Вот как выполняется такой запрос: DECLARE CURSOR bird curs IS SELECT b.genus, b.species. CASTCMULTISETCSELECT bh.country FROM birdhaMtats bh WHERE bh.genus - b.genus AND bh.species - b.species) AS countrytabt) FROM birds b; bird_row bird_curs*ROWTYPE: BEGIN OPEN bird_curs: FETCH bird_curs into bird_row; CLOSE bird_curs; END; Как и псевдофункция CAST, псевдофункция MULTISET не может использоваться в инструкциях INSERT, UPDATE и DELETE. Сортировка содержимого коллекций Одной из замечательных особенностей псевдофункций является то, что они по- позволяют выполнять SQL-операции над структурами данных PL/SQL (в частно- частности, над вложенными таблицами и массивами VARRAY). Например, воспользовав- воспользовавшись предложением ORDER BY, данные можно выбрать из вложенной таблицы в определенном порядке. Предположим, нам необходимо сформировать таблицу базы данных со сведениями о нескольких авторах: CREATE TYPE names_t AS TABLE OF VARCHAR2 A00); CREATE TYPE authors t AS TABLE OF VARCHAR2 A00);
Коллекции в PL/SQL 399 CREATE TABLE favoriteauthors (name varchar2B00)) BEGIN INSERT 1ИГ0 favorite_authors VALUES ('Robert Harris'): INSERT INTO favorite_authors VALUES ('Tom Segev'); INSERT INTO favorite_authors VALUES CToni Morrison'); END: Объединим эту информацию с данными программы PL/SQL: DECLARE scifi_favorites authors_t :-authors_t CSheri S. Tepper1. 'Orson Scott Card'. 'Gene Wolfe1): BEGIN DBMS_OUTPUT.putJine ('I recommend that you read books by:'); FOR rec IN (SELECT colurnnj/alue favs FROM TABLE (cast (scifi favorites AS namesj)) UNION SELECT NAME FROM favorite_authors) LOOP DBMS_OUTPUT.putj i ne (rec.favs): END LOOP: END; Обратите внимание, что для объединения данных из таблицы базы данных и коллекции можно использовать оператор UNION: DECLARE scif1_favorites authors_t :- authors_t CSheri S. Tepper', 'Orson Scott Card'. 'Gene Wolfe'): BEGIN DBMS_OUTPUT.put_line ('I recommend that you read books by:'); FOR rec IN (SELECT column_value favs FROM TABLE (cast (sdf1_favorites AS namesj)) ORDER BY co1umn_value) LOOP D8MS_0UTPUT.put_line (rec.favs): END LOOP; END; Сопутствующие операции В этом разделе приводится ряд сведений, необходимых в первую очередь для ра- работы с вложенными таблицами и массивами VARRAY. Речь пойдет о вспомогатель- вспомогательных операциях. Привилегии Если тип данных коллекции определен в базе данных, с ним могут работать не- несколько пользователей Oracle, каждый из которых является владельцем одной или нескольких схем. Естественно, что в этом случае возможность доступа к данным определяется привилегиями, управлять которыми достаточно просто. Единствен- Единственной привилегией пользователей Oracle, связанной с применением типов коллек- коллекций, является привилегия на выполнение EXECUTE.
400 Глава 11 • Записи и коллекции Если, предположим, ваше имя Scott и вы хотите позволить пользователю Joe применять в своих программах созданный вами тип Co1or_tab_t, то должны пре- предоставить ему привилегию EXECUTE: GRANT EXECUTE Color_tab_t ON TO Joe; Пользователь Joe сможет ссылаться на данный тип с помощью синтаксиса схема. тип. Например: CREATE TABLE my_stuff_to_paint ( which_stuff VARCHAR2C512). paintjnixture SCOTT.Color_tab_t ) NESTED TABLE paintjnixture STORE AS paintjirixture_st: Привилегия EXECUTE необходима и пользователям, выполняющим анонимные блоки PL/SQL, в которых задействован определенный вами тип коллекции. Это одна из причин, па которым лучше создавать именованные модули PL/SQL — пакеты, процедуры и функции. Предоставляя кому-либо привилегию EXECUTE на некоторый модуль, вы предоставляете ему на время выполнения этого модуля собственные разрешения. Для таблиц, включающих столбцы-коллекции, действуют традиционные при- привилегии SELECT, INSERT, UPDATE и DELETE, если только в этих инструкциях не форми- формируются коллекции для записи в столбцы. Однако если пользователь хочет обнов- обновлять содержимое столбца-коллекции, у него должна быть привилегия EXECUTE на соответствующий тип, поскольку иначе он не сможет пользоваться конструкто- конструктором данного типа. Коллекции и словарь данных При работе с вложенными таблицами и массивами VARRAY вам наверняка приго- пригодятся новые элементы словаря данных, описанные в табл. 11.2. Пользовательские типы в словаре данных называются просто TYPE. Определения типов коллекций возвращает представление USER_SOURCE (или OBA_SOURCE, или ALL_SOURCE). Таблица 11.2. Элементы словаря данных для типов-коллекций Для получения ответа ...используйте данное на вопрос представление. ..как в следующем примере Какие типы коллекций были созданы? Каким было исходное определение типа коллекции Foo_t? USERJTYPES USER_SOURCE Какие столбцы основаны USER_TAB_COLUMNS на коллекции типа Foo_t? Какие объекты базы данных зависят от коллекции типа Foo_t? USER_DEPENDENCIES SELECT type_name FROM user types WHERE typecode = 'COLLECTION'; SELECT text FROM user_source WHERE name = 'FOOJT AND type = TYPE1 ORDER BY line; SELECT table_name, column_name FROM user tab_columns WHERE datajype = 'FOOJT; SELECT name, type FROM user_dependecies WHERE referenced_name = 'FOOJT;
Коллекции в PL/SQL 401 Выбор типа коллекции Можно ли определить, какая коллекция больше всего подходит для вашей про- программы? Очень часто выбор очевиден, но бывает и так, что приемлемых вариан- вариантов несколько. Ключевые различия между ассоциативными массивами, вложен- вложенными таблицами и массивами VARRAY перечислены в табл. 11.3. Как правило, программисты предпочитают использовать ассоциативные мас- массивы. Почему? Причин несколько. Для них приходится писать меньше всего про- программного кода. Их не нужно ни инициализировать, ни расширять. Долгое время это был самый эффективный тип коллекций (хотя данный фактор, конечно же, скоро потеряет значение). Однако если вам нужно хранить коллекцию в таблице базы данных, ассоциативный массив для этого не подойдет — нужно сделать вы- выбор между вложенной таблицей и массивом VARRAY. Помимо этих, самых очевидных, соображений примите во внимание следую- следующие советы. О Если вы намерены хранить в коллекции-столбце большое количество данных, то единственным выбором должна стать вложенная таблица. Для хранения структур указанного типа Oracle создает отдельные таблицы, размер которых практически не ограничен. О Если вам нужно сохранять порядок элементов в столбце коллекции, если объем данных невелик, используйте массив VARRAY. Что значит «невелик»? Лучше всего, если размер массива не будет превышать размер блока базы данных. Ко- Когда массив занимает более одного блока, его обработка замедляется. Размер блока базы данных задается при создании таковой и обычно составляет 2, 4 или 8 К. О Тип коллекции VARRAY удобен еще в нескольких случаях: когда нужно иметь гарантию, что элементы не будут удаляться из середины коллекции; когда ко- количество данных ограниченно; когда коллекции должны извлекаться из базы данных целиком. О Если вам нужна разреженная структура, единственным выбором является ас- ассоциативный массив. Разумеется, можно добавлять и удалять элементы вло- вложенной таблицы (как в примерах из раздела, посвященного методам NEXT и PRIOR), но это крайне неэффективный метод, и приемлемый разве что при наличии очень маленьких коллекций. О Если программа должна работать и в Огас1е7, и в Огас1е8, наиболее целесооб- целесообразным выбором являются ассоциативные массивы. Как и в том случае, когда в коллекции используются отрицательные индексы. Таблица 11.3. Сравнение типов коллекций Oracle Характеристика Размерность Можно ли использовать в SQL Ассоциативный массив Одномерный Нет Вложенная таблица Одномерная Да Массив типа VARRAY Одномерный Да продолжением
402 Глава 11 • Записи и коллекции Таблица 11.3 (продолжение) Характеристика Ассоциативный массив Вложенная таблица Массив типа VARRAY Можно ли использовать в качестве типа данных столбца таблицы Содержимое в неинициализированном состоянии Нет Пуст (не может быть равен NULL); элементы не определены Тип данных, BINARYJNTEGER используемый для ссылки (-2 147 483 647 ... на элементы в PL/SQL 2 147 483 647) VARCHAR2 (Orade9l Release 2 и выше) Допустима ли разреженность данных Накладывается ли Нет ограничение на длину Можно ли присваивать Да значения любому элементу в любой момент INTEGER (положительные целые числа от 1 до 2 147 483 647) Да Способ расширения Допустима ли операция сравнения Сохранение порядка строк и индексации при записи в таблицу и извлечении из таблицы базы данных Присваивание значения элементу с новым индексом Нет Да; данные хранятся Да; данные хранятся вне таблицы прямо в таблице (в отдельной таблице) Содержит атомарное Содержит атомарное значение NULL; значение NULL; нельзя ссылаться на нельзя ссылаться на элементы элементы INTEGER (положуггельные целые числа от 1 до 2 147 483 647) Первоначально — Нет нет; после удалений — да Возможно Да расширение Нет; сначала может Нет; сначала может потребоваться вызвать метод EXTEND Использование встроенной процедуры EXTEND без предопределенного максимума (для сжатия используется метод TRIM) Нет Нет потребоваться вызвать метод EXTEND, и расширение возможно только до верхней границы Использование встроенной процедуры EXTEND, но только до предопределенного максимума (для сжатия используется метод TRIM) Нет Да
12 Другие типы данных ^ Тип данных BOOLEAN ^ Тип данных RAW ^ Типы данных UROWID и ROWID > Типы данных LOB ^ Работа с большими объектами > Предопределенные объектные типы В настоящей главе рассматриваются встроенные типы данных PL/SQL, о кото- которых ничего не было сказано в предыдущих главах. Речь пойдет о типах BOOLEAN, UROWID (ROWID), а также о семействе типов LOB (Large OBjects) - больших объек- объектах. Кроме того, будет описано несколько важных предопределенных объектных типов, в том числе тип XMLType, который предназначен для хранения XML-данных в столбце таблицы, и тип ANYDATA, используемый для хранения любых данных. Тип данных BOOLEAN В версии языка SQL для Oracle имеется ряд элементов, которые отсутствуют в PL/SQL (например, конструкция DECODE). PL/SQL, со своей стороны, включает компоненты, которых нет в стандартном языке SQL Одним из них, особенно важным для программистов, является тип данных BOOLEAN1. Переменные этого типа могут принимать только три значения: TRUE, FALSE и NULL. Поэтому его часто называют логическим типом данных. РСУБД Oracle не поддерживает тип данных BOOLEAN. Вот почему для хранения логических значений в таблицу обычно вставляют столбец типа CHAR(l) и записы- записывают в него значения Y и N, обозначающие TRUE и FALSE. Однако значения данного столбца, конечно же, нельзя присваивать логическим переменным PL/SQL, как Булев тип данных назван по имени Джорджа Буля, жившего в первой половине XIX столетия
404 Глава 12 ¦ Другие типы данных нельзя выполнять и обратную операцию — сохранять в столбце значения TRUE и FALSE. Иными словами, речь идет просто об эмуляции логического типа данных, а вовсе не об эквиваленте настоящего логического типа PL/SQL. Логические переменные и выражения очень широко используются програм- программистами. То обстоятельство, что логическая переменная может принимать толь- только три значения — TRUE, FALSE и NULL, — позволяет сделать программу более чита- читабельной. Код, содержащий логические переменные и выражения, является более простым для понимания, поскольку похож на естественный английский язык. Сложное логическое выражение со множеством переменных и операций провер- проверки можно заменить одной логической переменой, имя которой выражает суть вы- вычисляемого выражения. Приведем пример оператора IF с одной логической переменной, или функци- функцией; глядя на этот код, невозможно определить, что именно - функция или пере- переменная - в ней используется: IF report_requested THEN print_report (reporMd): END IF; Программная логика данного фрагмента кода делает его не только самодоку- самодокументированным, но и защищенным от будущих изменений. Рассмотрим, к примеру, код реализации пользовательского интерфейса, который может предшествовать данному оператору. Как программа определяет, что пользователю нужно печа- печатать отчет? Она либо выводит окно запроса с вариантами ответов «Да» и «Нет», либо предлагает установить флажок или выбрать из списка нужную опцию. Для приведенного кода это не имеет значения. Код реализации пользовательского ин- интерфейса может быть любым, и при желании его можно в любой момент изме- изменить — главное, чтобы он правильно присваивал значение логической перемен- переменной report_requested. В представленном выше примере проверяется значение обычной логической переменной. Если оно равно TRUE, отчет выводится на печать. В противном случае отчет не печатается, а выполняются указанные далее в программе действия. Ана- Аналогичную проверку можно было бы организовать и с помощью оператора IF вида: IF report_requested = TRUE THEN pr1nt_report (reportjd); END IF; Код не содержит ни логических, ни синтаксических ошибок, но этот оператор не соответствует сути логической переменной. Поскольку логическая перемен- переменная сама может принимать значения TRUE, FALSE и NULL, то не имеет смысла сравни- сравнивать ее с этими значениями, чтобы опять-таки получить TRUE, FALSE или NULL. К то- тому же назначив переменной подходящее имя, вы сделаете конструкцию IF.. .THEN более лаконичной и понятной. Тот факт, что логическая переменная может принимать и значение NULL, отра- отражается на структуре оператора IF.. .THEN.. .ELSE. Для примера сравните два сле- следующих оператора: IF report_requested THEN
Тип данных RAW 405 -- Код. который выполняется, если report_requested » TRUE -- Код. который выполняется, если report_reqiiested » FALSE или IS NULL END IF; IF NOT report_requested THEN -- Код. который выполняется, если report_requested » FALSE ELSE -- Код. который выполняется, если report_requested - TRUE или IS NULL END IF: Если каждому из трех возможных значений переменной должны соответство- соответствовать определенные действия, следует использовать такой оператор IF: IF reportj-equested THEN -- Код. который выполняется, если report_requested = TRUE ELSIF NOT report_requested -- Код. который выполняется, если report_requested = FALSE ELSE -- Код. который выполняется, если report_requested IS NULL END IF; Подробнее об обработке значений NULL в операторах IF рассказывается в главе 4. Тип данных RAW Тип данных RAW предназначен для хранения и обработки двоичных данных срав- сравнительно небольших объемов. При пересылке между базой данных и программой данные типа RAW, в отличие от VARCHAR2 и других символьных типов, никогда не преобразовываются из одного набора символов в другой. Переменные типа RAW объявляются так: имя_переменной КШ(.наксимдльный_рдЗнер) Значение максимальный^азмер должно находиться в диапазоне от 1 до 32767. Имейте в виду, что переменная типа RAW может содержать порядка 32 767 байт данных, а столбец базы данных этого же типа — не более 2000 байт. Тип данных RAW используется редко. Как уже было сказано, он предназначен Для хранения малых объемов двоичных данных. Если же вам придется работать с большими объемами информации, например с изображениями или звуком, об- обратитесь к типу данных BLOB (Binary Large OBject - большой двоичный объект). О нем рассказывается далее в этой главе. Типы данных UROWID и ROWID Типы данных UROWID и ROWID предназначены для работы с идентификаторами строк базы данных. ROWID — это идентификатор строки (row identifier), а точнее, двоичное значение, идентифицирующее строку данных в таблице Oracle. В некоторых слу- случаях его применение в инструкциях UPDATE и DELETE позволяет повысить скорость обработки, поскольку доступ к строке по ее идентификатору выполняется быстрее,
406 Глава 12 • Другие типы данных чем по первичному ключу. На рис. 12.1 продемонстрированы два способа обнов- обновления записей в таблице, а именно с использованием идентификатора и первич- первичного ключа. UPDATE employee SET first name-'Heather' WHERE last name='Grubbs'-. UPDATE employee SET first name-'Heather' WHERE ROWTD=eniployee rowid: О Поиск в индексе записи last_name с цепью обнаружения идентификатора mwid Q Используем идентификатор rowid для извлечения блока базы данных, содержащего обновляемую строку © Изменяет данные, записываем блок на жесткий диск last_name rowid rowid блок данных блокданных Индекс Таблица .<.... Таблица Минуя шаг 1, переходим к выполнению шага 2 Рис 12.1. Идентификатор типа ROWID указывает непосредственно на строку таблицы В истории Oracle ROWID был одним из первых типов данных, предназначенных для идентификации строк таблиц. По мере добавления новых функциональных возможностей, таких как использование индекс-таблиц и типов данных для межсе- межсетевых шлюзов, компания Oracle, естественно, разрабатывала и новые типы иден- идентификаторов строк, и новые типы данных для их хранения. Так появился тип данных UROWID, используемый для хранения идентификаторов строк таблиц лю- любого типа. Буква Ub его названии означает «universal» (универсальный). ПРИМЕЧАНИЕ- Мы рекомендуем использовать тип данных UROWID во всех новых программах, оперирующих с идентификаторами строк. Тип данных ROWID в Oracle сохраняется для обеспечения обратной со- совместимости, но он, в отличие от UROWID, не поддерживает все типы идентификаторов строк, ис- использующихся в современных базах данных Oracle. Получение идентификаторов строк Идентификатор строки — это создаваемое Oracle двоичное значение, однозначно определяющее физический адрес каждой строки в таблице. Оно генерируется, когда строка добавляется в таблицу. Изменить это значение невозможно, его мож- можно только считать с помощью инструкции SELECT, если указать в ней псевдостол- псевдостолбец RfMD. Мы применили здесь термин «псевдостолбец» потому, что на самом деле столбца с именем ROWID в таблице не существует. Это просто ключевое слово, которое указывается в списке столбцов инструкции SELECT и означает, что мы хо- хотим получить значение идентификатора строки. Для того чтобы сохранить идентификатор строки, полученный с помощью ин- инструкции FETCH или SELECT, и иметь возможность использовать его в программе PL/SQL, нужно объявить для него переменную типа UROWIO. Например: DECLARE employee_rowid UROWID:
Типы данных UROWID и ROWID 407 empToyee_salary NUMBER: BEGIN -- Считываем информации о сотруднике, которую мы собираенся модифицировать SELECT rowid. salary INTO employeej-Dwid. employee_sa1ary FROM employee WHERE last_name-'Grubbs' AND first_name-']oriri'; END: Возможно, кому-то интересно узнать, что представляет собой ROW ID на самом деле и какие данные хранятся в переменной типа URDWID. Если вы не являетесь ад- администратором базы данных, это не имеет для вас никакого значения. Но если вы все же решите разобраться в этом, выполните в SQL*Plus запрос на выборку иден- идентификаторов строк из какой-нибудь таблицы. Ниже в качестве примера приведено несколько идентификаторов строк, взятых из индекс-таблицы: SQL> SELECT rowid FROM employee; ROWID *BAIACiICwQL+ *BAIACiICwQP+ Следующие идентификаторы получены с помощью инструкции SELECT из обыч- обычной таблицы (в Огас1е7 результаты будут иными): SQL> SELECT rowid FROM rtest: ROWID AAAHZpAAIAAAAoSAAA MAH2pAAIAAAAoSAAB Если вам нужно оперировать с компонентами идентификаторов строк, обрати- обратитесь к встроенному пакету DBMS_ROWID, достаточно подробная информация о котором содержится в книге Oracle Built-in Packages (издательство O'Reilly) и в руково- руководстве Oracle, в разделе Supplied PL/SQL Packages and Types Reference. Но в боль- большинстве случаев разбираться в содержимом идентификаторов строк нет никакой необходимости. Для решения большинства задач, в том числе для анализа всех примеров из этой книги, достаточно знать, что это некоторое значение, иденти- идентифицирующее строку таблицы. Использование идентификаторов строк Итак, вы уже знаете, как присвоить значение идентификатора строки переменной PL/SQL-программы, но зачем это делать? На практике идентификаторы строк час- часто используются при повторном доступе к строке. Вспомните пример из преды- предыдущего раздела, когда из базы данных извлекается информация об окладе сотруд- сотрудника. Предположим, нам необходимо изменить величину оклада и ввести в базу Данных новое значение. Для этой цели можно, конечно, написать инструкцию UPDATE с тем же предложением WHERE, которое использовалось в инструкции SELECT: DECLARE employee_rowid UROWID; employee salary NUMBER; BEGIN
408 Глава 12 • Другие типы данных -- Считываем информации о сотруднике, которую мы собираемся модифицировать SELECT rowid. salary INTO employeej-owid. employee_salary FROM employee WHERE lastjiame-'Grubbs1 AND first_name='John'; /* Вычисляем новый оклад */ UPDATE employee SET salary - employee_salary WHERE last name-'Grubbs' AND first_name='John'; END: Этот код, безусловно, выполнит поставленную задачу, но он имеет один суще- существенный недостаток: в инструкциях UPDATE и SELECT повторяются одни и те же па- параметры доступа. Следовательно, при выполнении каждой из них производится обращение к одному или нескольким индексам. А какой смысл по нескольку раз повторять одни и те же действия для поиска одного идентификатора строки? Го- Гораздо целесообразнее присвоить идентификатор строки переменной, а затем ис- использовать его в инструкции UPDATE (в случае надобности): DECLARE employee_rowid UROWID: employeesalary NUMBER: BEGIN -- Считывание информации о сотруднике SELECT rowid. salary INTO employee_rowid. employee_salary FROM employee WHERE lastjiame-'Grubbs' AND firstjiame-'Dohn'; /* Вычисление величины нового оклада */ UPDATE employee SET salary = empioyEejsalary WHERE rowid ¦= employee rowid: END: Если employee — это обычная таблица, организованная по принципу кучи, иден- идентификатор строки в предложении WHERE инструкции UPDATE указывает непосредст- непосредственно на местоположение строки на диске. Но если employee является индекс-табли- индекс-таблицей, то время доступа зависит от скорости обновления ее данных; однако и в этом случае идентификатор строки обеспечивает максимально быстрый доступ к ее содержимому. ПРИМЕЧАНИЕ- Иногда лучший способ достижения определенного результата — это использование для извлечения данных явного курсора и последующая модификация или удаление данных с помощью предложе- предложения WHERE CURRENT OF CURSOR. Подробнее об этой технологии рассказывается в главе 14. Изменяются ли идентификаторы строк А можно ли изменить значения идентификаторов строк? Хороший вопрос. Если вы считываете идентификатор строки и собираетесь использовать его в програм- программе, то нужно знать, долго ли он останется неизменным. Физические идентифика- идентификаторы, относящиеся к стандартным таблицам, которые организованы по принципу кучи, не меняются никогда. И лишь при условии, что вы удалите строку и встави- вставите ее снова, ей будет назначен новый идентификатор. Но ведь только что было
Типы данных UROWID и ROWID 409 сказано, что физические идентификаторы никогда не меняются! Нет ли здесь противоречия? Абсолютно никакого. Когда вы удаляете строку и вставляете ее снова, вы вставляете новую строку с теми же значениями, и она естественным об- образом получает новый идентификатор. Логические идентификаторы строк, используемые в индекс-таблицах, не так долговечны, как физические. Более того, они могут меняться при изменении пер- первичного ключа таблицы. Но если у вас нет привычки изменять первичные ключи, последнее обстоятельство на вас никак не повлияет. Использование идентификаторов строк в Oracle Forms В Oracle Forms идентификаторы строк можно использовать для доступа к строке базы данных, которая соответствует записи, отображаемой на экране. Когда вы добавляете новый блок базовой таблицы, в него в качестве «невидимого псевдо- псевдоэлемента» автоматически включается идентификатор строки. Его нет в списке элементов, но на него можно ссылаться в триггерах и программах PL/SQL. На- Например, для того чтобы обновить на экране имя сотрудника, нужно выполнить следующую инструкцию: UPDATE employee SET last_name - -.employee.lastjiame WHERE rowid - :employee.rowid; Использование идентификаторов строк в цикле FOR с курсором Идентификатор строки можно использовать в цикле FOR с курсором (или в любом другом цикле, где производится выборка записей из курсора) для внесения изме- изменений в только что выбранную строку: PROCEDURE remove_internal_competitors IS BEGIN FOR emp_rec IN (SELECT connections, rowid FROM enployee WHERE sal > 500000) LOOP IF emp_rec.connections IN ('President'. 'CEO') THEN send_holiday_greetings; ELSE DELETE FROM employee WHERE rowid - emp_rec.rowid: END IF: END LOOP: END; В этой процедуре в инструкции DELETE используется идентификатор строки, соответствующий записи emp_rec. Процедура удаляет записи о сотрудниках, чей оклад превышает 500 000 долларов, если они не являются ни президентом (Pre- (President), ни главным администратором (CEO - Chief Executive Officer). Обратите внимание, что разрешением на выполнение этой процедуры управляет админист- администратор базы данных. Вы можете также проверить, не связан ли он каким-то образом
410 Глава 12 ¦ Другие типы данных с президентом или главным администратором? Как бы там ни было, использова- использование идентификатора строк гарантирует наискорейшее удаление строки с инфор- информацией о выбранном сотруднике. Конечно, в приведенной выше процедуре можно просто считать содержимое столбца с идентификатором сотрудника (первичный ключ таблицы сотрудников) и удалить строку с этим идентификатором: DELETE FROM employee WHERE employeejd - empjd: Нужны ли идентификаторы строк Нельзя с полной уверенностью сказать, что теоретическое уменьшение времени поиска нужной записи при использовании идентификаторов строк доказывает целесообразность их применения. Ведь результирующий код в этом случае полу- получается менее понятным, чем при использовании первичных ключей. Кроме того, работая над таким кодом, необходимо точно знать, в какой момент и в каких слу- случаях идентификаторы строк будут изменяться. Мы слышали о программистах, хранивших идентификаторы строк в таблицах, все программы которых вдруг пре- прекратили выполняться из-за того, что администратор базы данных в один прекрас- прекрасный день импортировал или экспортировал данные с целью их реорганизации. Так что вне зависимости от того, будете вы пользоваться идентификаторами строк или нет, ни в коем случае не сохраняйте их в таблицах базы данных. Еще одна проблема, которая возникает при использовании идентификаторов строк, связана с переносимостью приложений. Идентификаторы строк не под- поддерживаются стандартом ANSI SQL - они отражают лишь внутреннюю структуру данных в РСУБД Oracle. Применение идентификаторов строк в будущем может вызвать проблемы, связанные с переносимостью, поскольку другими, не-Oracle, базами данных они не распознаются и не поддерживаются. Поэтому, если созда- создаваемое приложение должно подключаться к каким-либо другим источникам дан- данных, избегайте ссылок на псевдостолбец ROW ID и использования типов данных R0- WID и UROWID. Большие объекты данных Oracle и PL/SQL поддерживают несколько разновидностей типов данных, пред- предназначенных специально для работы с большими объектами (LOB, Large OBjects). Объекты типа LOB, или LOB-объекты, позволяют хранить огромные, порядка 4 Гбайт, объемы двоичных (например, изображений) или текстовых данных. - В PL/SQL можно объявлять LOB-объекты четырех типов. О BFILE - двоичный файл. Переменная этого типа содержит локатор файла, ука- указывающий на файл операционной системы, хранящийся вне базы данных. Oracle интерпретирует содержимое данного файла как двоичные данные. О BLOB — большой двоичный объект. Переменная этого типа содержит локатор LOS, указывающий на большой двоичный объект, хранящийся в базе данных. О CLOB — большой символьный объект. Переменная этого типа содержит локатор LOB, указывающий на хранящийся внутри базы данных большой блок тексто- текстовых данных, которые состоят из однобайтовых символов.
большие объекты данных 411 О NCLOB - большой символьный объект, который поддерживает средства работы с символами различных языков. Переменная этого типа содержит локатор LOB, указывающий на хранящийся внутри базы данных большой блок текстовых данных, состоящих из однобайтовых символов или многобайтовых символов переменой длины. Объекты типа L08 можно разделить на две категории: внутренние и внешние. Внутренние большие объекты (типы BLOB. CLOB и NCLOB) хранятся в базе данных и могут участвовать в транзакции на сервере базы данных. Внешние большие объек- объекты (тип BFILE) представляют двоичные данные, хранящиеся в файлах операцион- операционной системы вне таблиц базы данных. Они не могут участвовать в транзакциях, то есть вносимые в них изменения нельзя сохранить или отменить в зависимости от результата транзакции. Поэтому целостность данных в случае использования типа BFILE зависит от целостности данных файловой системы. Тип данных BFILE Тип данных BFILE используется для доступа только к большим двоичным объек- объектам (размером до 4 Гбайт), хранящимся в файлах вне базы данных. Переменные этого типа обеспечивают доступ в режиме байтового потока (и только для чте- чтения) к файлам, находящимся на жестком диске, CD-ROM или другом устройстве. При объявлении переменной типа BFILE память выделяется для хранения ло- локатора файла, но не его содержимого. Локатор файла включает псевдоним ката- каталога и имя файла. Подробнее о локаторах файлов рассказывается далее в этой главе, в разделе «Особенности типа данных BFILE». Вот как объявляется пере- переменная типа BFILE: DECLARE web_page BFILE; Тип данных BLOB Тип данных BLOB предназначен для хранения больших двоичных объектов внутри базы данных. Данные, связанные со значениями типа BLOB (кроме тех, для записи которых требуется минимальный объем памяти), хранятся «вне строк». Это озна- означает, что если в таблице имеется столбец типа BLOB, строка данных этой таблицы содержит указатель, или локатор, реального местоположения данных столбца. Переменная типа BLOB представляет собой указатель на большой двоичный объект. Подобные объекты могут иметь размер до 4 Гбайт и принимать участие в транзакциях. Иными словами, любые изменения, вносимые в BLOB-объект (на- (например, с помощью программ из встроенного пакета DBMSLOB), можно сохранить или отменить одновременно с другими изменениями, производимыми в ходе тран- закции. Однако локатор BLOB должен быть локальным в пределах транзакции или сеанса. Объявляется переменной типа BLOB следующим образом: DECLARE photo BLOB:
412 Глава 12 • Другие типы данных Тип данных CLOB Тип данных CLOB предназначен для хранения в базе данных больших блоков одно- однобайтовых символьных данных; символьные данные переменной длины он не под- поддерживает. Как и BLOB-объекты, объекты CLOB хранятся «вне строк». Переменная типа CL08 содержит указатель на большой блок символьных дан- данных. Размер CLOB-объектов достигает 4 Гбайт, и они могут принимать участие в транзакциях. Как и BLOB-локаторы, локаторы CLDB не должны быть видимыми за пределами транзакции или сеанса. Ниже приведем пример объявления перемен- переменной типа CLOB: DECLARE directions CLOB: ТИПЫ ДАННЫХ LONG И LONG RAW Возможно, те из вас, кто уже знаком с Oracle, заметили, что мы до сих пор не упоминали о типах данных LONG и LONG RAW. И это не случайно. Конечно, в столбцах типа LONG базы данных можно хранить большие объемы (до 2 Гбайт) символьных данных, а в столбцах LONG RAW - двоичных. Однако максимальная длина соответствующих им переменных PL/SQL значитель- значительно меньше: всего лишь 32 760 байт, что даже немного меньше длины пере- переменных типа VARCHAR2 и RAW C2 767 байт). Учитывая столь странное огра- ограничение, в программах PL/SQL следует использовать переменные типа VARCHAR2 и RAW, а не типа LONG и LONG RAW. Значения типа LONG и LONG RAW, извлекаемые из базы данных и содержащие свыше 32 767 байт данных, нельзя присваивать переменным типа VARCHAR2 и RAW. По этой причине типы данных LONG и LONG RAW лучше вообще не при- применять. Тем более что они относятся к числу устаревших и поддержива- поддерживаются только для обеспечения обратной совместимости кода. Компания Oracle не рекомендует ими пользоваться, и мы с ней полностью согласны. В новых приложениях вместо них лучше использовать типы данных CLOB и BLOB. А для существующих приложений в документации Oracle9i Appli- Application Developer Guide — Large Objects (LOBs) приводятся инструкции по преобразованию данных типа LONG в данные типа LOB. Тип данных NCLOB Тип данных NCLOB предназначен для хранения больших блоков символьных дан- данных - как однобайтовых, так и многобайтовых фиксированной и переменной длины. Между типами данных NCLOB и CLOB существует такое же соответствие, как и между символьными типами NVARCHAR2 и VARCHAR2. О наборах символов подробно рассказывается в главе 8. ПРИМЕЧАНИЕ В Огас1е8 тип NCLOB не поддерживает символьные данные переменной длины — такие данные ис- используются начиная с версии OracleSI.
Обработка больших объектов Переменная типа NCLOB содержит указатель на большой блок символьных дан- данных. Объект типа NCLOB может иметь размер до 4 Гбайт и участвовать в транзакци- транзакциях. Иными словами, любые изменения, вносимые в NCLOB-объект, можно сохранить или отменить вместе с другими изменениями, выполняемыми в ходе транзакции. Видимость локаторов BLOB и CLOB, как и NCLOB-локаторов, должна ограничиваться транзакцией или сеансом. А вот как объявляется переменная типа NCLOB: DECLARE directions NCLOB; Обработка больших объектов Тема работы с большими объектами очень объемна, поэтому мы не сможем осветить все ее аспекты. Данный раздел следует рассматривать как введение в программи- программирование больших объектов. Мы расскажем об их некоторых важных особенно- особенностях и приведем ряд примеров. Излагаемый материал может лишь послужить ос- основой для ваших дальнейших исследований в этой области. Сначала речь пойдет о типах больших объектов, имеющихся в распоряжении разработчиков программ PL/SQL. Затем мы поговорим о локаторах больших объек- объектов и о том, как создавать объекты этих типов. Вы узнаете, как с помощью про- программ из пакета OBMSLOB манипулировать содержимым больших объектов, и по- познакомитесь с новым синтаксисом Огас1е9г, позволяющим обрабатывать большие объекты с применением таких встроенных функций, как SUBSTR (хотя это и приводит с снижению быстродействия приложения, о чем и рассказывается в этом разделе). Все представленные в данном разделе примеры основаны на таблице, создан- созданной следующей SQL-инструкцией: CREATE TABLE waterfalls ( fallsjiame VARCHAR2C80). falUjhoto BLOB. falls_d1rections CLOB, falls_description NCLOB. falls_web_page BFILE): Таблица waterfal Is содержит информацию о водопадах, расположенных в север- северной части штата Мичиган. Каждый столбец таблицы предназначен для хранения больших объектов одного из четырех типов. Любую фотографию можно предста- представить в виде двоичных данных большого объема, поэтому для столбца fal I sphoto определен тип данных BLOB. Столбцы с информацией о местоположении и с опи- описанием водопадов (falls_directions и fal ls_descriptions) содержат текстовые дан- данные, поэтому определены соответственно как CLOB и NCLOB. В реальной таблице они, конечно, имели бы один и тот же тип. Коды web-страниц водопадов хранятся в HTML-файлах вне базы данных. Поэтому мы включили в таблицу столбец типа BFILE, содержащий указатели на эти файлы. ВНИМАНИЕ При описании больших объектов мы будем часто использовать аббревиатуру LOB, обозначающую все описываемые типы данных— BLOB, CLOB, NCLOB и BFILE. А при обсуждении конкретного типа будет даваться его полное название.
414 Глава 12 • Другие типы данных Понятие локатора LOB Важнейшая концепция типов данных LOB связана с понятием локатора LOB. Ло- Локатором называется хранящийся в базе данных указатель на данные большого объекта, которым может быть файл или даже значение. Чтобы понять принцип его действия, рассмотрим, что же происходит во время выборки данных из столб- столбца типа BLOB и присвоения их переменной PL/SQL того же типа: DECLARE photo BLOB; BEGIN SELECT falls_photo INTO photo FROM waterfalls WHERE fallsjiame-1Dryer Hose1; Какое значение будет иметь переменная photo после выполнения этой инст- инструкции SELECT? Будет ли в нее помещена сама фотография? Нет. В переменной будет храниться лишь указатель на двоичные данные. Результат выполнения ин- инструкции SELECT представлен на рис. 12.2. База данных j Указатель j иаданные 1 BLOB в базе данных Программа PL/SQL j Переменная photo j Локатор LOB ASB4237O1CFF26 9901OA3DO00099 0123456789ABCD Рис. 12.2. Локатор LOB указывает на объект в базе данных Описанный выше принцип использования столбцов базы данных довольно уникален. Переменные и значения в столбцах типа LOB содержат локаторы боль- больших объектов, которые идентифицируют реальные данные, хранящиеся в другом месте базы данных или вне ее. Для работы с данными типа LOB нужно сначала из- извлечь локатор, а затем с помощью программ встроенного пакета DBMS_LOB полу- получить и/или модифицировать реальные данные. Вот что нужно сделать для того, чтобы получить двоичные данные фотографии, локатор которой хранится в столб- столбце типа BLOB нашего примера. 1. Выполнить инструкцию SELECT и выбрать из базы данных LOB-локатор фото- фотографии. 2. С помощью функции DBMSLOB.OPEN открыть объект типа LOB. 3. Вызвать функцию DBMS_LOB. GETCHUNKSIZE для получения оптимального (с точки зрения затрат на выполнение операции чтения или записи) размера фрагмен- фрагмента данных типа LOB. 4. Вызвать функцию DBMSJ-OB .GETLENGTH, с тем чтобы определить количество бай- байтов или символов объекта типа LOB.
Обработка больших объектов 415 5. Несколько раз вызвать функцию DBMSLOB. READ для считывания данных типа LOB. 6 Закрыть ранее открытый объект типа LOB. Правда, не все эти действия являются обязательными, и не волнуйтесь, если что-то вам пока непонятно. Далее мы расскажем о них подробнее. Необходимость использования пакета DBMS_LOB для работы с LOB-локаторами делает применение больших объектов значительно более сложным, чем примене- применение обычных типов данных. Однако предлагаемый Oracle подход имеет сущест- существенные преимущества. Напомним, что в столбцах типа LOB можно хранить до 4 Гбайт данных. Представьте себе, что могло бы произойти, если бы данные хранились в самих таблицах и вы с помощью инструкции SELECT извлекали бы не локаторы, а 4-гигабайтовые значения. Во-первых, эти значения программе пришлось бы из- извлекать непосредственно при выполнении инструкции SELECT. Во-вторых, для их хранения потребовался бы огромный объем памяти (который не всегда имеется в распоряжении программы). А самое главное, все эти данные вам вряд ли потре- потребовались бы одновременно. Например, изображения выводятся на экран обычно по одному, а не все сразу. И текст из большого столбца типа LOB также может счи- тываться не целиком, а по частям. Но благодаря тому что в столбцах типа LOB хра- хранятся не данные, а локаторы, запросы к таким столбцам производятся гораздо быстрее. Получив локатор, вы можете в любой момент считать нужные данные — несколько значений, одно или лишь часть значения. Если вам из 4-гигабайтового столбца типа LOB потребуются, скажем, 100 байт данных, вы сможете прочитать только эти 100 байт, а не весь столбец. Итак, подытожим сказанное. В столбцах типа LOB содержатся указатели, а не сами данные, и инструкция SELECT считывает в переменную типа LOB, объявлен- объявленную в PL/SQL, исключительно значение указателя. ДОКУМЕНТАЦИЯ ORACLE, ПОСВЯЩЕННАЯ LOB-ДАННЫМ Если вам часто приходится работать с большими объектами, настоятель- настоятельно рекомендуем ознакомиться со следующими документами Oracle: ? Oracle9i Application Developer Guide - Large Objects (LOBs) - содержит полную информацию о программировании для LOB; ? Oracle9i Supplied PL/SQL Packages and Types Reference — включает, в частности, главу с описанием пакета DBMS_LOB; ? Oracle9i SQL Reference - содержит важную информацию о LOB-объек- тах (см. раздел Datatypes в главе 2). Это далеко не полный список документации об объектах LOB, но вы найде- найдете в нем все наиболее важные сведения. Большие объекты — пустые и равные NULL Теперь, когда вы понимаете различие между локатором LOB и значением, на кото- Рое он указывает, мы введем еще одно важное понятие, а именно пустой объект Ш LOB. Так называется локатор, не указывающий ни на какие данные. Это не то
416 Глава 12 • Другие типы данных же самое, что значение NULL, то есть LOB-столбец (или переменная), не содержа- содержащий локатора LOB. Рассмотрим пример: SQL> DECLARE 2 3 4 5 б 7 8 9 10 directions CLOB; BEGIN IF directions DBMS OUTPUT, ELSE DBHS OUTPUT, END IF; END; / directions is NULL IS NULL THEN ,PUT_LINE('directions is NULL'): ,PUT_LINEC'directions is not NULL'): Значение переменной di rections типа CLOB, объявленной в данном блоке, равно NULL, поскольку ей пока не присвоено никакое значение. Выглядит все привычно, не так ли? Да и происходит все то же самое, что и при работе с данными любого другого типа, поэтому результатом объявления переменной без присваивания ей значения является NULL. Создадим для этой переменной локатор LOB. В следующем блоке кода выполняется инструкция INSERT, в которой вызывается функция ЕМР- TY_CLOB, а затем - инструкция SELECT, извлекающая из базы данных локатор, сге- сгенерированный функцией EMPTY_CLOB. Мы сохраняем его в переменной directions. О том, почему мы действуем так неэффективно, рассказывается в следующем разделе. А пока сконцентрируем наше внимание на результирующих данных это- этого фрагмента кода. DECLARE directions CLOB; BEGIN -- Удаление всех существующих строк со значением 'Munising Falls'. -- что позволит выполнить этот прииер несколько раз DELETE FROM waterfalls WHERE falls_name='Munis1ng Falls': -- Вставка новой строки и вызов функции EMPTY_CLOB для создания локатора LOB INSERT INTO waterfalls (falls_name.falls_directions) VALUES ('Hunising Falls',EMPTY_CLOBO); -- Считывание локатора LOB. созданного предыдущей инструкцией INSERT SELECT falls_directions INTO directions FROM waterfalls WHERE fallsjiame-'Munising Falls': IF directions IS NULL THEN DBMS_OUTPUT.PUT_LINE('directions is NULL1); ELSE DBMS_OUTPUT.PUT_LINE('directions is not NULL');' END IF; DBMS_OUTPUT.PUT_LINE('Length = '
Обработка больших объектов 417 || DBMSJ-OB.GETLENGTH(directions)): END; Вот что выводит этот код: , directions is not NULL Length - 0 В данном примере встроенная функция EMPTYCLOB возвращает локатор CLOB, который сначала сохраняется в базе данных, а затем считывается из нее. Пере- Переменная типа CLOB больше не равна NULL, поскольку теперь она содержит локатор. Однако результат вызова функции DBMS_LO8. GETLENGTH свидетельствует о том, что этот локатор не указывает ни на какие данные. Поэтому, хотя значение объекта directions и не равно NULL/он пока пуст. Как видите, это важное обстоятельство, из-за которого проверка на наличие данных в объекте LOB является более слож- сложной задачей, чем при работе с другими типами данных. В случае использования традиционных скалярных типов данных достаточно простой проверки условия IS NULL: IF somejiumber IS NULL THEN. -- Вы знаете, что данных нет Если результатом такой проверки значений типа NUMBER либо VARCHAR2 (или лю- любого другого скалярного типа данных) оказывается значение TRUE — переменная пуста. Однако при работе с объектами типа LCB кроме этой проверки, позволяю- позволяющей определить, содержит ли переменная локатор, необходимо еще проверить и длину строки данных, на которые этот локатор указывает: IF some_clob IS NULL THEN -- Нет ни данных, ни локатора. ELSIF DBMS_LOB.GETLENGTH(some_clob) = О THEN -- Нет данных ELSE -- Наконец выяснили, что данные есть END IF; Как следует из этого примера, не зная значения локатора, нельзя проверить длину строки данных типа LOB. Для того чтобы определить, содержит ли объект данные, нужно сначала убедиться в наличии локатора, выполнив проверку IS NULL, а затем проверить длину объекта. Создание больших объектов В предыдущем разделе для создания локатора LOB использовался код -- Вставка новой строки с помощью вызова функции EMPTY_CLOB для создания локатора LOB INSERT INTO waterfalls Cfal1s_name,falls_directions) VALUES CMurdsing Falls',EMPTY_CLOBO); За ним следовала инструкция SELECT, предназначенная для выборки значений локатора из таблицы. Выглядит не слишком изящно, не так ли? У вас наверняка возникнет вопрос, почему нельзя было сразу сделать так: directions := EMPTY CLOBC):
418 Глава 12 • Другие типы данных На то имеются основания. Еще раз напомним, что в переменной типа CLOB на самом деле содержится локатор, указывающий на реальные данные в базе дан- данных, и что объекты LOB в оперативной памяти не хранятся — они содержатся на диске, в одном или нескольких файлах базы данных. Более того, параметры хра- хранения конкретного объекта на диске задаются как часть определения содержащей его таблицы. Вызвав функцию EMPTY_CLOB(), получаем пустой LOB-объект (пожалуй, правильнее было бы сказать «пустой локатор»), то есть локатор, не указывающий на конкретное место на диске. После вставки этого пустого объекта в таблицу waterfalls Oracle определяет место его хранения (основываясь на информации, заданной администратором базы данных при определении таблицы), обновляет локатор, с тем чтобы он указывал на данное место, и сохраняет его в строке таб- таблицы. И тольхо теперь, когда процесс создания локатора LOB можно считать за- завершенным, его следует сохранить в переменной, то есть извлечь из таблицы wa- terfalls. Сказанное отнюдь не означает, что в инструкцию INSERT нужно обязательно включить вызов функции EMPTY_CLOB(). Вы можете сначала присвоить результат вызова переменной типа CLOB, а затем вставить значение этой переменной в таб- таблицу: -- Вставка новой строки с помощью вызова функции EMPTY CLOB для создания локатора LOB di rections :- EMPTY_CLOB(); INSERT INTO waterfalls С falls_name,fa11s_d1recti ons) VALUES ("Munis-ing Falls'.directions): После выполнения этого кода нужно обязательно обновить переменную di - rections, записав в нее значение столбца falls_directions из таблицы waterfalls. ПРИМЕЧАНИЕ Для создания пустого локатора BLOB используется функция EMPTY_BLOB(), а для создания пустых локаторов CLOB и NCLOB — функция EMPTY_CLOB(). Начиная с версии Огас1е8г с объектами типа LOB можно работать, не добавляя строк в базу данных. В таких случаях создаются временные объекты LOB, о кото- которых рассказывается ниже, в разделе «Временные объекты LOB». Запись данных в объекты типа LOB При наличии локатора данные записываются в LOB-объект с помощью одной из двух процедур пакета DBMS_LOB: О DBMS_LOB. WRITE — запись производится с учетом заданного смещения; О DBMS_LOB. WRITEAPPEND — данные добавляются в конец объекта. В приведенном ниже коде (это расширенная версия примера, рассматривае- рассматриваемого в предыдущих разделах главы) сначала создается локатор LOB для столбца di recti ons таблицы waterfal 1 s, а затем функция DBMS_LOB. WRITE записывает данные в CLOB-поле строки «Munising Falls*. Два следующих фрагмента данных дописы- дописываются в этот же столбец посредством функции DBMS_LOB.WRITEAPPEND: DECLARE directions CLOB:
Обработка больших объектов 419 amount BINARYJNTEGER; offset INTEGER; firstjtt rection VARCHAR2A00): more_direct1ons VARCHARZE00); BEGIN -. удаление scex существующих строк со значениен 'Mm1s1ng Falls', -- благодаря чему этот пример можно будет выполнить несколько раз DELETE FROM waterfalls WHERE fall sjiame-' Muni sing Falls'; .- Вставка новой строки и вызов функции EMPTY_CLOB для создания локатора LOB INSERT INTO waterfalls (fallsjiame.fali5_directions) VALUES ('Munising Falls',EMPTY_CLOBO); - Считывание локатора LOB. созданного предыдущей инструкцией INSERT SELECT falls_direct1ons INTO directions FROM waterfalls WHERE fallsjiame-'Miinising Falls'; -- Открытие овьекта LOB; зто делать необязательно, но лучше аккуратно открывать ¦- и закрывать LOB-объекты DBMS_LO8.OPENCdirections. DBMS_LOB.LOB_READWRITE); ¦- С помощью вызова DBMSLOB. WRITE начинаем запись. first_direction :- 'Follow 1-75 across the Mackinac Bridge.'; amount :- LENCTH(f1rst_d1rection); т- количество считываемых символов offset :- 1: -- считывание первого символа CLOB DBMS_LOB.WRITE(directions. amount, offset, first_direction); -- С помощью вызова DBMSJ.OB.WRITEAPPEND добавляем дополнительные указания. nrare_directions :=¦ ' Take US-Z west from St. Ignace to Blaney Park.' || ' Turn north on M-77 and drive to Seney.' || ' From Seney, take M-28 west to Munising.'; DBMS_LOB.WRITEAPPENDCdi recti ons. LENGTH(more_d1rections). more_directions): -- И еще раз добавляем указания. more_directions := ' In front of the paper mill, turn right on H-58.' || ' Follow H-58 to Washington Street. Veer left onto' || ' Washington Street. You"ll find the Munising' || ' Falls visitor center across from the hospital at' || ' the point where Washington Street becomes' II ' Sand Point Road.'; DBMS_LOB.WRITEAPPENDCdi recti ons. LENGTH(more_directions). more_directions); ¦- Закрываем объект LOB и завершаем работу DBMSJ.OB.CLOSE С di recti ons): END;
420 Глава 12 • Другие типы данных В этом примере используются обе процедуры, WRITE и WRITEAPPEND. Однако на практике можно ограничиться лишь процедурой WRITEAPPEND, поскольку сначала объект не содержит никаких данных. Обращаем ваше внимание на то, как проис- происходит открытие и закрытие объекта типа CLOB. Процедуры OPEN и CLOSE лучше вы- вызывать явно, особенно при использовании утилиты Oracle Text. В противном слу- случае создаваемые Oracle Text индексы будут обновляться при каждом вызове про- процедуры WRITE или WRITEAPPEND, а не один раз при вызове процедуры CLOSE. ПРИМЕЧАНИЕ- В разделе, посвященном типу данных BFILE, рассказывается, как прочитать данные LOB прямо из внешнего файла операционной системы. При записи данных в объекты типа LOB обновлять LOB-столбец в таблице нет необходимости, поскольку в нем, как мы уже не раз говорили, хранится лока- локатор (который не изменяется), а не данные. Обновление выполняется в контексте транзакции. В данном примере мы не сохраняли результаты транзакции с помо- помощью инструкции COMMIT. Однако если вы хотите, чтобы записанная в объект типа LOB информация была сохранена в базе данных, после выполнения блока команд PL/SQL нужно задать инструкцию COMMIT. Если же после блока команд PL/SQL выполнить инструкцию ROLLBACK, все произведенные в этом блоке изменения дан- данных будут отменены. В нашем примере производится запись данных в столбец типа CLOB. Данные типа BLOB обрабатываются точно так же, с той лишь разницей, что аргументы про- процедур WRITE и WRITEAPPEND имеют тип данных RAW, а не VARCHAR2. Еще один пример, реализованный в SQL*Phis, выводит на экран данные, помещенные ранее в таб- таблицу waterfalls. Из следующего раздела вы поймете, как извлекать данные с по- помощью разных процедур пакета D8MSJ.0B: SQL> SET LONG 2000 SQL> COLUMN fallsdirections WORD_WRAPPED FORMAT A70 SQL> SELECT fallsjirections 2 FROM waterfalls 3 WHERE fall5_name-'Mun1s1ng Falls': FALLS DIRECTIONS Follow 1-75 across the Mackinac Bridge. Take US-2 west from St. Ignace to Blaney Park. Turn north on M-77 and drive tn Seney. From Seney, take M-28 west to Munising. In front of the paper mill, turn right on H-58. Follow H-58 to Washington Street. Veer left onto Washington Street. You'll find the Munising Falls visitor center across from the hospital at the point where Washington Street becomes Sand Point Road. Считывание данных из объектов типа LOB Чтобы данные из LOB-объекта стали доступными для программы, нужно вызвать процедуру DBMS_LOB. READ. Но сначала, конечно же, следует извлечь локатор LOB. При считывании данных типа CLOB указывается смещение, задаваемое в символах. Операция считывания начинается с позиции, заданной смещением, а первый счи- считываемый символ CLOB всегда имеет номер 1. В случае применения типа данных
Обработка больших объектов 8L0B смещение задается в байтах. Кроме того, в вызове процедуры DBMS_LOB. READ определяется количество считываемых символов или байтов. Поскольку объекты типа LOB могут быть очень большими, имеет смысл извлекать их содержимое по частям. Рассмотрим фрагмент кода, в котором сохраняются и выводятся данные о ме- местоположении водопада Munising Falls. Мы четко определили количество считы- считываемых символов, с тем чтобы оно соответствовало ограничению на длину строки в пакете DBMS_OUTPUT и выводимый текст выглядел аккуратно. DECLARE directions CLOB: directionsj VARCHAR2C300); directions_2 VARCHAR2C300): • chars_read_l BINARYJNTEGER: chars_read_2 BINARYJNTECER; offset INTEGER; BEGIN -- Извлекаем ранее добавленный локатор LOB SELECT fallsjjirections INTO directions FROM waterfalls WHERE falls_name-'Muni sing Falls': -- Начало чтения с позиции первого символа offset :-= 1: -- Попытка считать 229 символов. В переменную chars_read_l -- будет записано реальное количество считанных символов chars_read_l :- 229; DBMSLOB.READ(directions. chars_read_l, offset, directions_l): -- Если считано 229 символов, информация о смещении обновляется -- и производится попытка считать еще 255 символов IF chars_read_l - 229 THEN offset ¦.- offset + chars_read_l; chars_read_2 :- 255; DBMSJOB.READCdirections, chars_read_2. offset, directions 2): ELSE chars_read_2 :- 0; direct1ons_2 := ": END IF; -- Вывод общего количества считанных символов. DBMS_OUTPUT.PUT_LINE('Characters read - ' || TO_CHAR(chars_read_l + chars_read_2)): -- Вывод значений переменных DBMS OUTPUT.PUT LINE(directions 15; DBMS_OUTPUT.PUT LlNE(directions 2); END; В результате выполнения этого кода будет выведен следующий текст: Characters read - 414 Follow 1-75 across the Mackinac Bridge. Take US-2 west from St. Ignace to Blaney Park.
422 Глава 12 • Другие типы данных Turn north on M-77 and drive to Seney. From Seney, take M-28 west to Mun1s1ng. In front of the paper mill, turn right on H-58. Follow H-58 to Washington Street. Veer left onto Washington Street. You'll find the Mun1s1ng Falls visitor center across from the hospital at the point where Washington Street becomes Sand Point Road. Процедуре DBMSJ.OB. READ в качестве второго параметра, объявленного здесь как IN OUT, передается переменная chars_read_l (количество считанных символов). По окончании операции процедура обновляет его таким образом, чтобы отразить ре- реальное количество прочитанных символов. Если количество прочитанных симво- символов (или байтов) окажется меньше указанного вами, то это будет означать, что считаны все данные, хранящиеся в объекте. К сожалению, обновить таким же способом информацию о смещении невозможно. Вы должны это делать само- самостоятельно, по мере считывания последовательных фрагментов объектов типа LOS, основываясь на количестве прочитанных символов. ПРИМЕЧАНИЕ Для определения размеров больших объектов можно воспользоваться функцией DBMS_LOB.GET- LENGTH(roKaTop_LOB). Возвращаемое ею значение равно количеству байтов объекта BLOB или BFILE либо количеству символов объекта CLOB. Особенности данных типа BFILE Как уже упоминалось ранее, типы данных BLOB, CLOB и NCLOB представляют собой внутренние большие объекты, хранящиеся в базе данных, в то время как BFILE яв- является внешним типом данных. Попытаемся определить, чем объекты BFILE отли- отличаются от внутренних объектов типа LOB в первую очередь. ( О Значение BFILE хранится не в базе данных, а в файле операционной системы. О Объекты BFILE не участвуют в транзакциях (то есть внесенные в них измене- изменения нельзя ни отменить, ни сохранить в транзакции). Однако изменения, вне- внесенные в локатор BFILE, в транзакциях можно и отменять, и сохранять. О И в PL/SQL, и в Oracle объекты BFILE можно только считывать. Записывать их Oracle не позволяет. Внешние файлы, на которые указывают локаторы BFILE, нужно создавать вне СУБД Oracle. Работая с данными типа BFILE в PL/SQL, вы фактически работаете с локато- локатором LOB, который просто указывает на хранящийся на сервере файл. Поэтому в разных строках таблицы базы данных в столбце типа BFILE могут быть записаны указатели на один и тот же файл. Значение локатора BFILE состоит из псевдонима каталога и имени файла. Фор- Формирует локатор функция BFILENAME, которая принимает два указанных параметра в качестве аргументов. Псевдоним каталога - это просто определенное в Oracle имя, соответствующее имени каталога операционной системы. Псевдонимы по- позволяют программам PL/SQL работать с каталогами независимо от файловой системы. Имея привилегию CREATE ANY DIRECTORY, вы можете создать псевдоним каталога и предоставить доступ к нему другим пользователям: CREATE DIRECTORY bfilejata AS 'c:\PLSOL Book\Chl2_Misc_Datatypes'; GRANT READ ON DIRECTORY bf1le_data TO gennick;
Обработка больших объектов 423 Создание псевдонимов каталогов и управление доступом к ним - это функ- функции администратора базы данных, не имеющие прямого отношения к программи- программированию на PL/SQL Поэтому мы не затрагиваем данную тему. Если же вам нуж- нужны более подробные сведения, поговорите с администратором базы данных или же ознакомьтесь с разделом документации Oracle SQL Reference Manual, посвя- посвященным команде CREATE DIRECTORY. Создание локатора BFZLE Локатор BFILE создается очень просто: нужно вызвать функцию BFILENAME и пере- передать ей псевдоним каталога и имя файла. В отличие от локаторов других типов LOS, его не нужно сохранять в базе данных и считывать перед использованием. Вот как создается локатор SFILE для HTML-файла с web-страницей водопада Tan- Tannery Falls, который затем сохраняется в таблице waterfalls. DECLARE web_page BFILE; BEGIN -- Удаление строки Tannery Falls. -- благодаря чему этот пример можно Судет аыполнить несколько раз DELETE FROM waterfalls WHERE fallsjiame-1Tannery Falls'; — Вызов функции BFILENAME для создания локатора BFILE web_page :- BFILENAME('BFILE_DATA'.'Tannery Falls.htm1); -- Сохранение нового локатора в таОлице waterfalls INSERT INTO waterfalls (falls_name. falls_web_page) VALUES ('Tannery Falls',web_page); END; Объединив псевдоним каталога и имя файла, получаем значение локатора BFILE. Реальный файл и каталог не обязательно должны существовать. Иными словами, Oracle позволяет создавать псевдонимы для несуществующих каталогов, a BFILENAME — локаторы для несуществующих файлов. Иногда это очень удобно. ВНИМАНИЕ Функция BFILENAME чувствительна к регистру символов, поэтому имя псевдонима каталога должно в точности соответствовать имени псевдонима, возвращаемому представлением ALLJJIRECTORIES из словаря данных. Выполняя этот пример, мы сначала указали псевдоним каталога как bfile_data, но при попытке получить доступ к данным внешнего файла увидели сообщение об ошибке. В боль- большинстве случаев при вызове функции BFILENAME псевдоним каталога задается в верхнем регистре. Доступ к данным BFILE Получив локатор BFILE, вы можете считывать данные из внешнего файла практи- практически таким же образом, как из объекта BLOB. Единственным различием является то, что перед считыванием файл обязательно нужно открыть с помощью функции DBMS_LOB. FILEOPEN, а по окончании работы закрыть посредством функции DBMS_ LOB.FILECLOSE. В следующем примере извлекаются первые 60 байт HTML-кода web-страницы водопада Tannery Falls. Результирующие данные, имеющие тип RAW, преобразуются в символьную строку с помощью функции UTL_RAW.CAST_TO VARCHAR2: DECLARE webjage BFILE;
424 Глава 12 • Другие типы данных html RAWC60): amount BINARYJNTEGER := 60; offset INTEGER :- 1: BEGIN -- Считываем LOB-локатор web-страницы SELECT falls_web_page INTO web_page FROM waterfalls WHERE falls_name='Tannery Falls': -- Открываем файл, считываем 60 байт и закрываем файл DBMS_LDB.OPENCweb_page): DBMS_LOB.READ(web_page, amount, offset, html); DBMS_LOB.CLOSE(web_page): -- Для вызова результатов в шестнадцатеричнон коде удалите -- символы комментария из следующей строки: --DBMSJOUTPUT.PUT_LINE<RAWTOHEX(html)): -- Преобразуем данные типа RAW в читабельнус символьную строку DBMS OUTPUT.PUT_LINE(UTL RAW.CAST_TD_VARCHAR2 С htmlJ): END: Вот что выводит этот код: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TransitionaWEN Максимальное число одновременно открытых в сеансе объектов BFILE уста- устанавливается параметром базы данных SESSION_MAX_OPEN_FILES. Этот параметр оп- определяет верхнюю границу количества файлов, которые могут быть одновремен- одновременно открыты в сеансе (причем файлов не только типа BFILE, но и любых других типов, включая и те, что открываются с применением программ из пакета L!TL_FILE). Напомним еще раз, что из базы данных Oracle данные BFILE можно только счи- считывать. Тип данных BFILE идеально подходит для доступа к двоичным данным, таким как коллекции изображений, сгенерированные вне СУБД. В частности, вы можете загрузить на сервер изображения, отснятые с помощью цифровой каме- камеры, и создать локаторы BFILE для доступа к этим файлам из программ PL/SQL. Использование BFILE для загрузки столбцов LOB Тип данных BFILE не только обеспечивает доступ к двоичным файлам, созданным вне базы данных Oracle, но и является удобным средством для загрузки внешних файлов в столбцы данных типа LQB. В Oracle9t Release 1 для чтения двоичных данных из объекта BFILE и записи их в столбец BLOB использовалась функция OBMSLQB.LOADFROMFILE, в Oracle9i Release 2 для этих целей имеются более удобные функции. О Функция DBMSJ.0B.L0ADCLOBFROMFILE загружает в столбец типа CLOB данные из объекта BFILE; обеспечивает необходимое преобразование символов. О Функция DBMS_LOB. LOADBLOBFROMFILE загружает в столбец типа BLOB данные из объекта BFILE; выполняет те же действия, что и DBMS_LOB. LOADFROMFILE, но ее ин- интерфейс согласован с интерфейсом функции DBMSJ.OB.LOADCLOBFROMFILE.
Обработка больших объектов 425 Представьте себе, что сведения о местоположении водопада Tannery Falls за- записаны во внешний текстовый файл TanneryFalls.directions, хранящийся в каталоге, на который указывает псевдоним BFILE_DATA. Следующий пример показывает, как с помощью функции DBMSLOB.LOADCLOBFROMFILE данные из этого файла можно за- записать в столбец fa11s_directions типа CLOB таблицы waterfalls. DECLARE Tannery Falls_Directions BFILE :- BFILENAMECBFILEJWA1, 'TanneryFalls.directions'); directions CLOB: destination_offset INTEGER :- 1; source_offset INTEGER :- 1; language_context INTEGER :- DBMS_LQB.default_lang_ctx; warningjnessage INTEGER: BEGIN -- Удаляем строку Tannery Falls. -- что позволит выполнить этот пример несколько раз DELETE FROM waterfalls WHERE fallsjiame-'Tannery Falls'; -- Вставляем новую строку, используя функцию EMPTYCLOB для создания локатора L08 INSERT INTO waterfalls (falls_name,falls_directions) VALUES ('Tannery Falls',EMPTY_CLOBC)): -- Извлекаем локатор LOB. созданный предыдущей инструкцией INSERT SELECT fall s_di recti ons INTO directions FROM waterfalls WHERE falls_name='Tannery Falls'; -- Открываем целевой объект CLOB и исходный BFILE. DBMSJ.OB.OPEN(di recti ons. 0BMS_LOB.LOB_READWRITE); DBMS_LOB.OPEN(Tannery_Falls_Directions); -- Загружаем содеркиное BFILE в столбец CLOB DBMS_LOB. LOADCLOBFROMFILE Cdi recti ons. Tanneryjal 1 s_Di recti ons. DBMS_LOB.LOBMAXSIZE. destination_offset. sDurce_offset. NLS_CHAR5ET_ID('US7ASCIГ), 1anguage_context. warni ngjnessage); -- Проверяем наличие единственно возможного предупреждения IF warningjnessage = DBMSLOB.WARN!NCONVERTIBLE_CHAR THEN dbms_output. putj i ne ( 'Warning! Some characters couldn''t be converted.'): END IF; -- Закрываем оба объекта типа LOB. DBMSJ.OB.CLOSE(di recti ons); DBMS LOB.CLOSECTannery Falls Directions); END; Работа приведенного выше фрагмента кода начинается фактически с вызова Функции DBMS_LOB. LQADCLOBFROMFILE. Эта процедура считывает данные из внешнего
426 Глава 12 • Другие типы данных файла, выполняет необходимые преобразования символов и записывает данные в столбец типа CLOB. Для определения количества загружаемых данных мы вос- воспользовались новой константой - DBMSJ.OB.LOBMAXSIZE. Поскольку требуется за- загрузить все данные из внешнего файла, значение константы DBMS_LOB. L0BMAX51ZE должно быть равным максимально допустимому размеру данных типа CLOB. Для источника и приемника данных задается одинаковое смещение — 1. Бла- Благодаря этому данные теперь можно считывать, начиная с первого символа объекта BFILE, а записывать — начиная с позиции первого символа объекта типа CLOB. Для обеспечения возможности повторных вызовов функции LOADCLOBFROMFILE проце- процедура обновляет оба эти значения смещения таким образом, чтобы они указывали на первый еще не считанный символ. Поскольку смещение в обоих случаях зада- задается как IN OUT, в вызове процедуры его нужно указывать как переменную, а не константу. Функция NLS_CHARSET_ID возвращает идентификатор набора символов, исполь- используемого во внешнем файле. Он необходим процедуре LOADCLOBFROMFILE для преоб- преобразования прочитанных данных в кодировку символов базы данных. Единствен- Единственное предупреждение, которое может вернуть процедура LOADCLOBFRQMFILE, — это сообщение о том, что некоторые символы не поддаются преобразованию. Нали- Наличие такого предупреждения мы проверяем посредством оператора IF, выполняе- выполняемого после загрузки данных. ПРИМЕЧАНИЕ Предупреждение не относится к категории сообщений об ошибках PL/SQL; даже несмотря на его на- наличие, загрузка все равно завершается. Рассмотрим реализованный в SQJL*Plus пример, демпнстрирующий, какие дан- данные загружаются из внешнего файла с помощью процедуры LOADCLOBFROMFILE. SQL> SET LONG 2000 SQL> COLUMN falls_d1rect1ons WORD_WRAPPED FORMAT A70 SQL> SELECT failsjdirectlons FROM waterfalls WHERE fal 1 s_na«ie-' Tannery Fal 1 s'; FALLS DIRECTIONS From downtown Munising. take Munising Avenue east. It will shortly turn Into H-58. Watch for Washington Street veering off to your left. At that intersection you'll see a wooden stairway going into the woods on your right. Go up that stairway and follow the trail to the falls. Do not park on H-58! You'll get a ticket. You can park on Nestor Street, which is just uphill from the stairway. Временные объекты типа LOB До сих пор речь шла о долговременном хранении больших объемов неструктури- неструктурированной информации, ставшем возможным благодаря использованию разных типов данных LOB. Применяемые при этом объекты LOB называются постоянными. Однако многим приложениям требуются временные LOB, используемые в качестве локальных переменных и не хранящиеся в базе данных. В этом разделе рассказы- рассказывается о временных объектах LOB и о том, как ими оперировать с помощью про- программ встроенного пакета DBMSLOB.
428 Глава 12 • Другие типы данных BEGIN -- Присвоение значения неинициализированной переменной -- CLOB или BLOB позволяет указать PL/SQL на необходимость неявно создать -- временный объект LOB,существующий только в течение текущего сеанса temp clob :-' http://www.nps.gDv/p1ro/': temp'blob := HEXT0RAWC7A1): END; Оба способа создания временных больших объектов равноправны, если не счи- считать того факта, что вызов функции DBMS_LOB. CREATETEMPORARY более определенно говорит о намерениях программиста. Таблица 12.1. Параметры процедуры CREATETEMPORARY Параметр Описание lobjoc Локатор LOB Cache Указывает, следует ли поместить объект LOB в буферный кэш Dur Определяет время жизни LQB. Его значением может быть одна из двух именованных констант: DBMS_IOB.SESSION, указывающая, что временный LOB должен быть удален в конце сеанса (это значение используется по умолчанию], или DBMS_LOB.CAUL, указывающая, что таковой должен быть удален в конце текущего вызова программы, в которой он был создан Освобождение временного объекта типа LOB Процедура DBMS_LOB. FREETEMPORARY освобождает временный объект типа BLOB или CLOB в текущем временном табличном пространстве. Ее синтаксис таков: PROCEDURE DBMSLOB.FREETEMPORARY С lobjoc IN OUT NOCOPY [BLOB | CLOB CHARACTER SET ANY_CS ]) Приведем пример, в котором сначала создаются, а затем явно освобождаются два временных объекта типа LOB. DECLARE tempjrlob CLOB: temp_blob BLOB: BEGIN -- Присвоение значения неинициализированной переменной -- CLOB или BLOB указывает PL/SQL на необходимость неявного создания -- временного объекта LOB,существующего только в течение текущего сеанса tempjrlob:-'http://www.exploringthenorth.com/alger/alger.html1: tempjlob :- HEXT0RAWC7A1): DBMSJ.OB.FREETEMPORARY(temp_clob); DBMS_LDB.FREETEMPORARY(temp blob): END: После вызова процедуры FREETEMPORARY освобожденный локатор LOB (lob_1oc) помечается как недействительный. Если вы присвоите его другому локатору LOB с помощью обычного оператора присваивания PL/SQL, то и этот локатор будет освобожден и помечен как недействительный.
Обработка больших объектов 1РИМЕЧАНИЕ РЦ/SQL неявно освобождает временные объекты типа LOB, когда они выходят за пределы области видимости блока. Как определить, является ли объект типа LOB временным функция ISTEMPORARY позволяет определить, объект какого типа — временный или постоянный - идентифицируется заданным локатором LOB. Она возвращает це- целое значение 1, если объект временный, и 0, если он постоянный. Синтахсис ука- указанной функции таков: PROCEDURE DBMS_LOB.ISTEMPORARY ( lobjoc IN [BLOB | CLD9 CHARACTER SET ANY_CS ]) RETURN INTEGER Эта функция вызывается из SQL; вероятно, поэтому в Oracle она не определе- определена как логическая. Управление временными объектами типа LOB Временные большие объекты обрабатываются не так, как обычные постоянные. Для них не поддерживаются транзакции, операции согласованного чтения, отка- отката и т. д. Такое сокращение количества функциональных возможностей имеет ряд следствий. О Если при обработке временного объекта типа LOB возникает ошибка, этот объект следует освобождать и начинать все сначала. О Одному временному объекту LOB нельзя присвоить несколько локаторов. От- Отсутствие поддержки операций согласованного чтения и отката при использо- использовании нескольких локаторов может вызвать значительное снижение произво- производительности. О Если пользователь модифицирует временный объект типа LOB, на который указывает еще один локатор, создается копия данного объекта, называемая в Oracle глубокой копией. После этого локаторы будут идентифицировать раз- разные объекты. Для минимизации количества глубоких копий при передаче LOB- локаторов в качестве аргументов используйте директиву компилятора NOCOPY. О Для того чтобы преобразовать временный объект в постоянный, нужно вы- вызвать программу DBMS_LOB. COPY и скопировать временный объект типа LOB в по- постоянный. О Временные LOB-локаторы постоянны на протяжении всего сеанса. Их нельзя передать из одного сеанса в другой (например, через канал базы данных), что- чтобы сделать соответствующий объект видимым в этом, втором, сеансе. В Oracle появилось новое представление VSTEMPORARYLOBS, определяющее, какое количество кэпшруемых и некэшируемых больших объектов активно в текущем сеансе. Администратор базы данных может, объединив информацию представле- представлений V$TEMPORARY_LOBS и OBAJEGMENTS, узнать, сколько дискового пространства ис- используется для хранения временных объектов типа LOB.
430 Глава 12 • Другие типы данных Встроенные операции над объектами типа LOB в Oracle9I Со времени появления больших объектов программисты мечтали оперировать с ними как с обычными скалярными переменными. В частности, они хотели бы обрабатывать объекты типа CLQB как большие символьные строки, передавая их SQL-функциям или, скажем, используя в предложениях WHERE. Однако, к большо- большому сожалению, объекты типа CLOB невозможно было использовать в качестве пе- переменных типа VARCHAR2, Например, в Oracle8 и Огас1е8г значения в столбцах типа CLOB нельзя было обрабатывать с помощью символьных функций: SELECT SUBSTRCfalls directions.1.60) FROM waterfalls; Однако начиная с Огас1е9г Release 1 появилась возможность во многих случа- случаях работать с данными типа CLOB так же, как с данными типа VARCHAR2. О Объекты типа CL08 можно передавать большинству функций SQL и PL/SQL, предназначенных для работы с данными типа VARCHAR2. О В PL/SQL (но не в языке SQL) с LQB-переменными можно использовать раз- разнообразные операторы сравнения, например операторы < (меньше), > (боль- (больше) и - (равно). О Значения типа CLOB можно присваивать переменным типа VARCHAR2 (или наобо- наоборот) с помощью оператора присваивания или инструкции SELECT... INTO. Это стало возможным благодаря тому, что PL /SQL выполняет неявное преобра- преобразование типов данных VARCHAR2 и CLOB. ПРИМЕЧАНИЕ- Такие новые свои возможности Oracle называет поддержкой SQL-семантики для LOB-объектов. Это означает, что теперь программисты, использующие PL/SQL, могут управлять объектами типа LOB с помощью встроенных операторов, а не программ отдельного пакета. Следующий пример демонстрирует некоторые новые возможности Oracle9i при работе с большими объектами: DECLARE name CLOB; namejjpper CLOB: directions CLOB: blank space VARCHAR2C1) :- ' ': BEGIN -- Считываем данные типа VARCHAR2 а объект CLOB, вызываем функций для обработки объекта CLOB SELECT fallsjiame. SUBSTR(falls_d1rect1ons.1.500) INTO name, directions FROM waterfalls WHERE fallsjiame - 'Munising Falls'; -- Преобразуем текст объекта типа CLOB в символы верхнего регистра -- Сравниваем два объекта CLOB IF name - namejjpper THEN DBMS OUTPUT.PUTjJNECWe did not need to uppercase the name.1); END IF;"
Обработка больших объектов -- Объединяем ооъект типа CLOB со строками VARCHAR2. IF INSTR(d1rect1ons.'Mack1nac Sridgel) о О THEN DBMS OUTPUT.PUT_LINE('To get to '. || name_upper || blank_space || 'yoiTmust cross the Mackinac BrTdge.'); END IF; END; В результате реализации этого примера выводится следующее сообщение: То get to MUNISING FALLS you must cross the MacHnac Bridge. Приведенный фрагмент кода выполняет несколько интересных операций: О считывает значения типа VARCHAR2 из столбца failsjiame в переменную типа CLOB (это пример неявного преобразования типов данных VARCHAR2 и CLOB); О с помощью функции SUBSTR ограничивает длину извлекаемого из столбца fal Is_direct1ons текста 500 символами, а затем с, помощью функции UPPER пре- преобразует название водопада в символы верхнего регистра (это примеры при- применения к большим объектам функций SQL и PL/SQL); О сравнивает значения переменных пате и name_upper в операторе IF, демонстри- демонстрируя таким образом возможность применения операторов сравнения к объек- объектам типа LOS; О представленное в символах верхнего регистра название водопада, имеющее тип CLOB, объединяет с двумя строковыми константами и одной переменной типа VARCHAR2 (это пример конкатенации строк объекта типа CLOB). Однако применение описанных возможностей связано с рядом трудностей и ограничений. В частности, не каждой функции, выполняющей обработку строк типа VARCHAR2, можно передавать значения типа CL08. Сказанное касается и опера- операторов сравнения: не все они применимы к большим объектам. Эти и ряд других ограничений подробно описаны в разделе SQL Semantics Support for LOBs главы 7 документации по Oracle Modeling and Design Application Developer's Guide. Тем, кто использует новые возможности системы, мы настоятельно рекомендуем сле- следовать приведенным там советам. ПРИМЕЧАНИЕ- Описываемая выше семантика SQL применима только к внутренним объектам типов CLOB, BLOB и NCLOB. На объекты типа SFILE она не распространяется. Результатом использования SQL-семантики может стать создание временных объектов типа LOB Используя при работе с большими объектами SQL-семантику, важно помнить, что в результате этого часто создается временный объект LOB. В качестве примера рассмотрим код, выполняющий передачу значения типа CLDB функции UPPER: DECLARE directions CLOB: BEGIN SELECT UPPERCfalls_d1rections) INTO directions FROM waterfalls WHERE fallsjiame - 'Munising Falls': END:
Глава 12 • Другие типы данных В связи с тем, что размер значений типа CLOB может быть очень большим, они всегда хранятся на диске. Сразу преобразовать извлекаемое значение в символы верхнего регистра Oracle не может, поскольку это означало бы изменение значения в базе данных. Невозможно изменить его и в оперативной памяти, поскольку из таблицы базы данных извлекается только локатор, а не само значение. Так что программному обеспечению базы данных ничего не остается, как только создать временный объект типа CLOB во временном табличном пространстве текущего се- сеанса. Функция копирует исходное значение типа CLDB во временный объект и там преобразует его в символы верхнего регистра. Затем инструкция SELECT возвра- возвращает локатор LOB, идентифицирующий временный объект типа CLOB, а не исход- исходный объект в базе данных. Этот факт имеет два важных следствия. О Возвращенный функцией или выражением локатор нельзя использовать для обновления исходного большого объекта. Переменная directions в нашем при- примере не используется для обновления постоянного объекта LOB, хранящегося в базе данных, поскольку она указывает на временный объект, возвращенный функцией UPPER. О На создание временных объектов типа LOB, размер которых может достигать десятков и сотен мегабайтов, затрачиваются дисковое пространство и ресурсы процессора. Мы подробнее обсудим этот вопрос ниже, в разделе «Влияние SQL-семантики на производительность». Если мы хотим получить возможность извлекать содержимое столбца falls_di - rections, представленное в символах верхнего регистра, и при этом сохранить воз- возможность обновлять данный столбец, нам потребуются два локатора LOB: DECLARE d1rections_upper CLOB; directions_persistent CLOB: BEGIN SELECT UPPERtfalls_directions). falls_directions INTO d1rectionsupper. d1rections_persistent FROM waterfalls WHERE fallsjiame - 'Munising Falls'; END; Теперь вы можете получить доступ к тексту, представленному символами верхнего регистра, с помощью локатора directions_upper и модифицировать ис- исходный текст, воспользовавшись локатором directionspersistent. При этом опе- операция считывания дополнительного локатора никак не снижает быстродействие приложения. Небольшая задержка возникает только во время преобразования текста в символы верхнего регистра и записи его во временный объект типа CLOB. Локатор в переменной di recti ons_persi stent - это просто указатель на значение в таблице базы данных. В общем случае строковые функции, выполняющие обработку значений типа VARCHAR2, при получении значения типа CLQ8 возвращают временные объекты CLOB. Так происходит и при вычислении выражений, результатом которых является значение типа CLOB.Временные объекты типа CLOB и BLOB нельзя использовать для обновления исходных значений, заданных в вызове функции или выражения.
Обработка ьольших ооъекгов Влияние SQL-семантики на производительность Прежде чем использовать новую SQL-семантику для работы с данными больших объектов, стоит подуматься, как это отразится на производительности приложе- приложения. Напомним, что буква L в аббревиатуре LOB означает «large», то есть «боль- «большой», а точнее, объемом до 4 Гбайт. Следовательно, работая с большими объекта- объектами как с данными обычных типов, можно столкнуться с серьезными проблемами. Рассмотрим запрос, возвращающий названия водопадов, для посещения которых может потребоваться переезд через мост Mackinac Bridge: SELECT fallsjiame FROM waterfalls WHERE INSTR(UPPER(falls_directiOPs).'MACKINAC BRIDGE1) «¦ 0; Какие действия должна произвести СУБД, чтобы выполнить этот запрос. Сна- Сначала необходимо извлечь содержимое всех строк столбца fal ls_directions табли- таблицы waterfalls, преобразовать его в символы верхнего регистра и поместить ре- результат во временный объект типа CLOB (во временном табличном пространстве). Затем, вызвав функцию INSTR и передав ей содержимое временного объекта, нужно найти строку MACKINAC BRIDGE. В наших примерах ширина столбца fal I s_di recti ons невелика, но представьте, что было бы, если бы его размер составлял, скажем, 1 Гбайт. Сколько места понадобилось бы для хранения во временном табличном пространстве всех временных объектов типа LOS! И сколько времени потребова- потребовалось бы на их копирование, преобразование их содержимого в символы верхнего регистра, на выделение для этих объектов места во временном табличном про- пространстве и для его освобождения, а также на посимвольный просмотр данных функцией INSTR! Не удивляйтесь, если выполнение ряда подобных запросов вы- вызовет недовольство администратора базы данных. ORACLE TEXT И СЕМАНТИКА SQL Повысить эффективность поиска нужной строки, преобразованной в сим- символы верхнего регистра, в объекте типа CLOB можно с помощью утилиты Oracle Text. Предположим, вам требуется выполнить такой запрос: SELECT falls_name FROM waterfalls WHERE INSTR(UPPER(falls_directions). 'MACKINAC BRIDGED о 0; Если столбец fall s_di recti ons имеет тип CLOB, этот запрос будет не очень эффективным. Однако определив с помощью Oracle Text для указанного столбца нечувствительный к регистру индекс и воспользовавшись предика- предикатом CONTAINS, вы сможете организовать более результативный поиск данных: SELECT fallsjiame FROM waterfalls WHERE CONTAINS(fans_directions, mackinac bridgel) > 0: Более полная информация о предикате CONTAINS и нечувствительных к ре- регистру индексах Oracle Text содержится в документации Oracle Text Ap- Application Developer's Guide.
Глава 12 • Другие типы данных Поскольку использование SQL-семантики при работе с большими объектами может привести к значительному снижению производительности приложения, согласно документации Oracle не следует применять ату семантику к объектам размером свыше 100 Кбайт. Мы же не даем никаких конкретных рекомендаций относительно размера объектов, а предлагаем оценивать преимущества и недостат- недостатки данного подхода в каждом конкретном случае. Причем принять то или иное решение необходимо еще до начала разработки программы, и, возможно, вам для этого придется провести ряд тестов. Функции преобразования объектов типа LOB СУБД Oracle предоставляет в ваше распоряжение несколько функций, предна- предназначенных для преобразования больших объектов. Перечень этих функций и их краткие характеристики приведены в табл. 12.2. Таблица 12.2. Функции преобразования больших объектов Функция Описание ТО_СШВ(символьные_данные) Преобразует символьные данные в данные типа CLOB. Входным параметром функции TO_CLOB должно быть значение типа VARCHAR2, NVARCHAR2, CHAR, NCHAR, CLOB или NCLOB. При необходимости (например, когда значение имеет тип NVARCHAR2) входные данные, представленные символами национального набора, преобразуются в набор символов базы данных ТО_В1.ОВ(данные_типа_гам) Подобна функции TO_CLOB; преобразует данные типа RAW или LONG RAW в данные типа BLOB ТО_МС1ОВ(символьные_данные) Подобна функции TO_CLOB, отличаясь лишь тем, что ее результат имеет тип NCLOB и представлен символами национального набора ТСН_ОВ(данные_типа_1опд) Принимает данные типа LONG или LONG RAW и преобразует их соответственно в данные типа CLOB или BLOB. Функцию TOJ.OB можно вызвать из списка SELECT вложенного запроса инструкции INSERT...SELECT...FROM TO_RAW Принимает значение типа BLOB и возвращает его как значение типа RAW Функция TOJ.OB предназначена для одноразового перемещения данных из столб- столбцов типа LONG и LONG RAW в столбцы типа CLOB и BLOB, поскольку типы данных LONG и LONG RAW более не используются. Функции TD_CLOB и TO_NCLOB являются удобными средствами взаимопреобразования символьных данных больших объектов и набо- наборов символов базы данных и национального языка. Предопределенные объектные типы В Oracle9i реализовано несколько предопределенных объектных типов, имеющих различное назначение: О XMLType - хранение и обработка данных в XML-формате;
Предопределенные объектные типы О типы URI - хранение унифицированных идентификаторов ресурсов (в частно- частности, HTML-адресов); О типы «any» - определение переменных PL/SQL, в которых могут храниться данные любых типов. В следующих разделах приведено краткое описание этих объектных типов и ука- указаны источники более полной информации о них, Тип данных XMLType Популярность технологии XML (Extensible Markup Language) растет очень быст- быстро, и без ее освоения не обойтись ни одному программисту. Хранить XML-дан- XML-данные в базе данных Oracle и управлять ими из среды SQL и PL/SQL позволяет предопределенный тип данных XMLType. Особенности языка XML — обширная тема, обсуждение которой выходит за рамки настоящей книги, Поэтому, излагая данный материал, мы исходили из пред- предположения, что вы имеете хотя бы поверхностное представление о технологии XML. Ниже речь пойдет лишь о значении объектного типа XMLType и о предостав- предоставляемых им возможностях, а его подробное описание вы найдете в книге Building Oracle XML Applications (автор Steve Muench, издательство O'Reilly). Если же вам нужен хороший источник информации о технологии XML, обратитесь к книге Learning XML (автор Erik T. Ray, издательство O'Reilly). ПРИМЕЧАНИЕ- В Oracle9l Release 1 для ссылки на объекты типа XMLType использовался префикс «SYS.». В Release 2 появилась возможность применять синонимы имен объектных типов и введен сценарий построения базы данных ($ORACLE_HOME/rdbms/admln/dbmsxmlt.sql), создающий объектный тип XMLType, ко- который теперь определяет и общедоступный (public) синоним XMLTYPE, указывающий на предопре- предопределенный объектный тип SYS.XMLType. Если вы используете тип данных XMLType, для хранения информации в XML-фор- XML-формате достаточно создать таблицу: CREATE TABLE falls С falljd NUMBER; fall SYS.XMLType ): В этой таблице для хранения XML-данных определен столбец fall с типом XMLType. Чтобы записать в него информацию, нужно вызвать статический метод CresteXML, который принимает XML-данные и создает для их хранения новый объект XMLType. Полученный объект можно поместить в столбец базы данных. Ме- Метод CreateXML является перегруженным: входными данными для него могут слу- служить и данные типа VARCHAR2, и данные типа CLOB. Рассмотрим в качестве примера три инструкции INSERT, которые создают XML-документы и помещают их в таблицу falls: INSERT INTO falls VALUES A, XMLType.CreateXMLC vers1on-"l,0"?>
Глава 12 • Другие типы данных <name>Munis1ng Falls</name> <county>Al ger</county> <state>MWstate> <url> http: //ml cMganwaterf al 1 s. com/mun1 s 1 ng_fa 11 s/mun1 s1 ng_fal 1 s. html <url> INSERT INTO falls VALUES B. XMLType. CreateXML С ¦<?xm1 version-'!,0"?> <name>Au Train FaTls</name> <county>Alger</county> <state>Ml</state> <url> http://mlchiganwaterfal1s.com/autra1n_fal1 s/autra1n_fal 1s.html <url> INSERT INTO falls VALUES C. XMLType.CreateXHLC '<?ял1 version-"]..0"?> <fan> <name>LaLighing Whitefish Falls«/name> <county>Alger</county> <state>MI</state> Для выборки XML-данных из таблицы можно применить несколько методов объекта XMLType. Метод existsNode, вызываемый в следующем примере, проверяет существование в XML-документе заданного узла. Аналогичную проверку выпол- выполняет встроенная функция SQL EXISTSNODE. Она, как и указанный метод, иденти- идентифицирует узел с помощью выражения XPath1. Приведенные далее инструкции возвращают одинаковые результаты: SQL> SELECT fall id 2 FROM falls f 3 WHERE f.fall.existsNodeC/fan/urV) > 0; SQL> SELECT falljd 2 FROM falls 3 WHERE EXISTSNODE(fall.7fall/urT) > 0: FALL ID Конечно, с XML-данными можно работать и в PL/SQL В следующем приме- примере переменной PL/SQL типа XMLType присваивается значение из столбца fal 1 пер- первой добавленной нами строки таблицы. Таким образом, в программу PL/SQL XPath — этот синтаксис описываемых частей XML-документа. Кроме того, XPath позволяет задать конкретный узел или значение атрибута документа.
Предопределенные объектные типы 437 считывается весь XML-документ, и мы можем работать с ним, как с любой дру- другой информацией. В данном случае после выборки документа мы извлекаем текст из узла /fall/url и выводим его. «demo_block» DECLARE fall XMLType; url VARCHAR2C80): BEGIN -- Извлекаем XML-данные из первой добавленной строки таблицы SELECT fall INTO demojjlock.fan FROM falls f WHERE f.falljid - 1: -- Извлекаем и выводим значение URL url :- fall.extracU7fall/url/text()').getStringVal; DBMS_OUTPUT.PUT_LINE(url): END: Обратите внимание на следующие строки: SELECT fall INTO demo_block.fall url := fall.extractGfall/url/text(V).getStringVal: В первой из них имя переменной, fall, совпадает с именем столбца таблицы в базе данных. Поэтому в запросе мы уточняем имя переменной, указывая допол- дополнительно имя блока PL/SQL. Для получения текста URL во второй строке мы вызываем два метода объекта XMLType: О extract — возвращает объект XMLType, содержащий только заданный фрагмент исходного XML-документа (для определения требуемого фрагмента исполь- используется выражение Xpath); О getStringVal — возвращает текст XML-документа. В рассмотренном примере метод getStringVal вызывается для XML-докумен- XML-документа, который возвращается методом extract, в результате чего мы получаем текст URL из первой добавленной нами строки таблицы. Метод extract возвращает со- содержимое узла <url> в виде объекта XMLType, а метод getStringVal извлекает из него содержимое в виде текстовой строки, которую можно вывести на экран. Столбец XMLType можно даже проиндексировать, обеспечив тем самым более эффективный поиск XML-документов. Для создания индекса вы должны иметь привилегии QUERY REWRITE. В следующем примере индекс создается на основе пер- первых 80 символов имен каждого водопада, перечисленных в таблице falls: CREATE INDEX failsjjyjiame ON falls f ( SUBSTRt XMLType. getStringVaU XMLType.extract(f.fall,'/fall/nane/textC)')
438 Глава 12 ¦ Другие типы данных Обратите внимание на то, как используется функция SUBSTR. Метод getString- Val возвращает строку, которая является слишком длинной для индекса, в резуль- результате чего возникает ошибка ORA-01450; MAXIMUM KEY LENGTH C166) EXCEEDED, указы- указывающая на превышение максимальной длины ключа. Функция же SUBSTR умень- уменьшает длину возвращаемой строки до некоторой приемлемой длины. Если вы решите задействовать объект XMLType в своем приложении, за более полной и актуальной информацией обращайтесь к документации Oracle. Тип XMLType был введен в Oracle9i Release 1. Но в Oracle Release 2 появилось множест- множество новых возможностей. Важнейшим ресурсом для программистов, работающих с XML, является руководство Application Developer's Guide - XML. Некоторая по- полезная информация об XMLType, а также о встроенных SQL-функциях для под- поддержки формата XML имеется в документе SQL Reference, Типы данных URI Тип данных URI представлен одним основным типом данных и несколькими под- подтипами, используемыми для хранения URI (Uniform Resource Identifier) в пере- переменных PL/SQL и столбцах баз данных. Основной тип данных TJRI называется UriType. В переменной этого типа может храниться экземпляр любого из следую- следующих подтипов: О HttpUriType — специфический для URL подтип UriType, обычно идентифици- идентифицирующий web-страницы; О DbUriType — подтип UriType, поддерживающий URL, которые представлены в ви- виде выражения Xpath; О XDBUriType — подтип UriType, поддерживающий TJRL и идентифицирующий объекты Oracle XML DB (XML DB — это набор XML-технологий, встроен- встроенных в базу данных Oracle). В Oracle9t поддержка работы с URI реализована в пакете UriFactory. Входя- Входящие в него программы позволяют автоматически строить объекты соответствую- соответствующего типа для заданного URI. Объектные типы URI создаются посредством сценария dbmsurf.sql, хранящегося в каталоге $ORACLE_HQME/rdbms/admin. Их владельцем является пользователь SYS, поэтому в Oracle9z Release 1 для ссылки на них (как и при работе с типом данных XMLType) необходимо задавать префикс «SYS.» (в Release 2 он более не нужен). Как используется тип данных HttpUriType, можно судить по такому примеру; DECLARE WebPageURL HttpUriType; WebPage CLOB; BEGIN -- Создаем эхзеипляр объектного типа, идентифицирующий -- сообщение Джонатана Генника WebPageURL :-HttpUriType.createUr1( 1 http://genni ck.com/message.pisql');
Предопределенные объектные типы 439 -- Извлекаем сообщение посредством HTTP WebPage :- WebPageURL.getclobO: ¦- Выводим сообщение DBMS_OUTPUT.PLTT_L1NEC CSUBSTRCWebPage.1.60))): END: В результате выполнения приведенного кода отображается следующий текст: Brighten the corner where you are. За дополнительной информацией об использовании семейства типов UriType об- обращайтесь к документации Oracle<H XML API Reference - XDK and Oracle XML DB. Типы данных «any» Новое семейство типов «any» предназначено для проведения операций над дан- данными, тип которых до выполнения программы не известен. Функции-члены это- этого типа способны анализировать данные и самостоятельно определять их тип. Это позволяет им корректно обрабатывать информацию. К семейству «any» от- относятся следующие типы данных: О AnyOata — может содержать одиночное значение любого типа: скалярную вели- величину, объект, созданный пользователем, вложенную таблицу, массив типа VAR- RAY или какое-либо другое значение не упоминаемого здесь типа; О AnyDataSet — может содержать набор значений любого типа, но все значения в этом наборе должны быть одного типа; О Any Type — обычно содержит описание типа, это «тип без данных». Типы данных «аду» создаются сценарием dbmsany.sql, хранящимся в каталоге $ORACLE_HOME/rdbms/admin. Их владельцем является пользователь SYS, поэтому в Oracle9i Release 1 для ссылки на них (как и для типа данных XMLType) нужно указывать префикс «SYS.» (в Release 2 он более не нужен). В дополнение к типам «any» сценарий dbmsany.sql создает пакет DBMS_TYPES, в котором определены приведенные ниже константы. Их можно использовать со- совместно с анализирующими функциями, и в частности с GETTYPE, для определения типа данных, хранящихся в конкретной переменной AnyData или AnyDataSet. Чи- Числовые значения этих констант для нас не представляют интереса - ведь кон- константы для того и определены, чтобы пользоваться именами, а не значениями. TYPECODEJATE TYPECODEJUMBER TYPECODE_RAW TYPECODE_CHAR TYPEC0DEJ/ARCHAR2 TYPECODEJARCHAR TYPECODE_MLSLABEL TYPECODE BLOB TYPECODE_BFILE TYPECODE_CLOB TYPECODE_CFILE TYPECODE TIMESTAMP
440 Глава 12 • Другие типы данных TYPECODETIMESTAMPJ7 TYPECODEJIMESTAMPJ-TZ TYPECODE_INTERVAL_YM TYPECODEJNTERVAL_DS TYPECDDE_REF TYPECODE_OBJECT TYPECODEJARRAY TYPECODEJABLE TYPECODEJAMEDCOLLECTION TYPECODEJPAQUE В приведенном ниже примере создаются два пользовательских типа, представ- представляющих два географических объекта: водопады и реки. Далее блок кода PL/SQL с помощью функции SYS.AnyType определяет массив неоднородных географиче- географических объектов (элементами которого могут быть и реки, и водопады). Итак, сна- сначала создаются два объектных типа: CREATE OR REPLACE TYPE waterfall AS OBJECT С name VARCHAR2C0). height NUMBER ); CREATE OR REPLACE TYPE river AS OBJECT ( name VARCHAR2C30). length NUM8ER ); Далее выполняется блок PL/SQL: DECLARE TYPE feature_array IS VARRAYB) OF SYS.AnyData; features feature_array; wf waterfall: rv river: ret_val NUMBER; BEGIN -- Создаем массив с элементами разных объектных типов features :- feature_array( AnyData.ConvertObject( waterfall('Grand Sable Falls'.30)). AnyData.ConvertObject( rivert'Manistique River', 85.40)) -- Выводим данные из этого массива FOR х IN 1.. features.COUNT LOOP -- Выполняем код, соответствующий текущему -- объектному типу. ВНДО1АНИЕ! Замените строку GENNICK -- именем своей схемы CASE features(x).GetTypeName WHEN 'GENNIC<.WATERFALL' THEN ret_val :- features(x).CetObject(wf>:
Предопределенные объектные типы 441 DBMS__OUTPUT. PUTLINEC 'Waterfal I: ' || wf.name || '. Height - ' || wf.height || ' feet.1): WHEN 'GENNICK.RIVER1 THEN ret_val :- featuresCx).GetObject(rv); DBMS_OUTPUT.PIK_LINE('River: ' || rv.name || '. Length - ' || rv.length || ' miles.'); END CASE; END LOOP; END: Результат выполнения этого кода будет таким: Waterfall: Grand Sable Falls. Height = 30 feet. River: Manistique River, Length - 85.4 miles. Проанализируем приведенный выше код. Необходимые для его работы объек- объекты хранятся в массиве VARRAY, инициализированном следующим образом: features := feature_array( AnyData.ConvertObject( waterfall('Grand Sable Falls'.30)). AnyData.ConvertObjectt river('Manistique River', 85.40)) ): Рассмотрим структуру кода, описывающего объект Grand Sable Falls. О В строке waterfall (' Grand Sable Falls'. 30) вызывается конструктор типа wa- waterfall для создания соответствующего ему объекта. О В строке AnyData.ConvertObject объект типа waterfall преобразуется в экземп- экземпляр объекта SYS.AnyData, который после этого можно будет записать в массив объектов SYS.AnyData. О Функция featurearray вызывает конструктор массива. Каждый аргумент этой функции имеет тип AnyData. В данном случае массив состоит из двух передан- переданных нами объектов. Массивы VARRAY рассматривались, как вы помните, в главе 11, а об объектных типах будет рассказано в главе 21. Следующий важный фрагмент кода — цикл FOR, в котором последовательно ана- анализируются объекты массива features. При вызове пользовательских объектов имя типа предваряется именем схемы пользователя, создавшего данный объект. Это имя схемы мы включаем в предложение WHEN: WHEN 'GENNICK.WATERFALL' THEN Если вы решите проверить работу данного кода, не забудьте заменить имя GENNICK именем своей схемы. ПРИМЕЧАНИЕ При использовании встроенных типов данных, таких как NUMBER, DATE и VARCHAR2, функция GetTypeName возвращает просто имя типа. Имя схемы указывается только для типов, определен- определенных пользователем (то есть созданных с помощью оператора CREATE TYPE). Определив тип данных, мы извлекаем объект из массива посредством такого вызова: ret_val :- features(x).GetObjecttwf):
442 Глава 12 • Другие типы данных В данном примере возвращаемое функцией значение игнорируется. В общем случае результатом работы функции может быть одно из двух значений: О DBMS_TYPES.SUCCESS - значение, свидетельствующее о том, что переменная типа Any содержит данные определенного типа (в нашем случае - объект); О DBMS_TYPES.NO_DATA — значение, указывающее, что в переменной типа AnyData не оказалось никаких данных. Присвоив переменной в качестве значения экземпляр объекта, можно напи- написать оператор, соответствующий его структуре. Например, информация о водопа- водопаде выводится так: D8MS_0UTPUT. PUT_L INE (¦ Waterfa 11:' || wf.name Ц1. Height - ' || wf.height || ' feet.1); Информацию о типах данных «any» вы найдете также в главе 21, где они рас- рассматриваются с позиций объектно-ориентированного программирования. Кроме тога, советуем вам ознакомиться с документами Oracle9i Supplied PL/SQL Packa- Packages and Type Reference и OracleSi SQ?. Reference, а также воспользоваться сцена- сценариями anynums.pkg и anynums.tst, представленными на узле издательства O'Reilly. ПРИМЕЧАНИЕ С позиций объектно-ориентированного программирования, существуют более эффективные спосо- способы работы с объектными типами данных. Рассмотренный пример приведен лишь с целью демонст- демонстрации возможностей предопределенного объектного типа SYS.AnyData.
Часть IV SQL и PL/SQL Эта часть книги посвящена одной из важнейших задач, выполняемых программа- программами PL/SQL, — взаимодействию с базами данных, которое осуществляется с помо- помощью SQL. В главах 13-15 рассказывается, как объявлять в таблицах базы данных транзакции для обновления, добавления и удаления информации; как запрашивать из базы данных информацию для обработки ее в программах PL/SQjL; как динами- динамически выполнять SQL-инструкции, используя встроенный динамический SQL (NDS). ? Глава 13. DML и управление транзакциями ? Глава 14. Выборка данных D Глава 15. Динамический SQL и динамический PL/SQL
13 DML и управление транзакциями > DML и PL/SQL > Пакетные DML-операции и оператор FORALL > Управление транзакциями > Автономные транзакции При работе с базой данных Oracle используются языки SQL и интегрированный PL/SQL. Из программы PL/SQL можно выполнять инструкции языка манипу- манипулирования данными DML (Data Manipulation Language), в частности INSERT, UPDATE и DELETE, и, конечно же, запросы на выборку информации. ПРИМЕЧАНИЕ- DML-инструкции в программах PL/SQL выполняются только в режиме динамического SQL. Эта тема освещается в главе 15. Вы можете объединить несколько SQL-инструкций в одну логическую тран- транзакцию - в таком случае их результаты либо будут сохраняться (в терминологии SQL «фиксироваться») все вместе, либо будут все вместе отменяться («откаты- («откатываться»). В этой главе рассматриваются SQL-инструкции, используемые в PL/SQL для управления транзакциями. Для того чтобы оценить важность транзакций в Oracle, нужно рассмотреть их основополагающий принцип, называемый «ACID» (Atomicity, Consistency, Isola- Isolation, Durability): транзакция атомарна, согласованна, изолированна и устойчива. Эти четыре характеристики транзакции определяются следующим образом. О Атомарность. Изменения состояния, вносимые в ходе одной транзакции, ато- атомарны: либо выполняются все эти изменения, либо не выполняется ни одно из них. О Согласованность. Транзакция изменяет состояние базы данных. Выполнен- Выполненные в течение транзакции действия не нарушают ограничений целостности, связанных с данным состоянием.
DML в PMSQL 445 О Изолированность. Возможно параллельное выполнение множества транзак- транзакций, но с точки зрения каждой конкретной транзакции остальные кажутся вы- выполняемыми до или после нее. О Устойчивость. После успешного завершения транзакции измененные данные становятся устойчивыми к последующим сбоям. Можно либо зафиксировать начатую транзакцию с помощью инструкции COM- COMMIT, либо выполнить ее откат посредством инструкции ROLLBACK. В любом из этих случаев будут освобождены заблокированные транзакцией ресурсы (инструкция ROLLBACK TO снимет только некоторые блокировки). Затем сеанс, как правило, на- начинает новую транзакцию. По умолчанию в PL/SQL неявно определяется одна транзакция на весь сеанс, и все выполняемые в ходе этого сеанса изменения дан- данных являются частью транзакции. Однако начиная с Oracle8i применение техно- технологии автономных транзакций позволяет определять вложенные транзакции, выполняемые внутри главной транзакции уровня сеанса. Об этой возможности подробно рассказывается в конце главы, в разделе «Автономные транзакции». DML в PL/SQL Из блока кода PL/SQL можно выполнять DML-инструкции (INSERT, UPDATE и DE- DELETE), оперирующие любыми доступными структурами данных. ПРИМЕЧАНИЕ Если используется модель разрешений создателя, права доступа к этим структурам данных опреде- определяются во время компиляции. Если же используется модель разрешений вызывающего, с предложе- предложением AUTHID CURRENTJJSER, права доступа определяются во время выполнения программы. Под- Подробнее об этом рассказывается в главе 20. Краткое введение в DML Поскольку полное описание DML-инструкций языка Oracle SQL не входит в зада- задачи этой книги, мы приведем лишь краткий обзор их базового синтаксиса, а затем рассмотрим несколько вопросов, связанных с использованием DML в PL/SQL: О примеры всех DML-инструкций; О атрибуты курсора для DML-инструкций; О особые элементы DML-инструкций в PL/SQL, такие, например, как предло- предложение RETURNING. За дополнительной информацией вы можете обратиться к документации Orac- Oracle и другим источникам, содержащим сведения о SQL. В языке SQL определены три инструкции DML: О INSERT — вставляет в таблицу одну или несколько новых строк; О UPDATE — обновляет в одной или нескольких существующих строках таблицы значения одного или нескольких столбцов; О DELETE - удаляет из таблицы одну или несколько строк.
446 Глава 13 • DML и управление транзакциями Инструкция INSERT Существуют две базовые разновидности инструкции INSERT, синтаксис которых, конечно же, несколько различается. О Вставка одной строки с явно заданным списком значений: INSERT INTO таблица [(столбец^, стопбец_2 столвец_Ю] VALUES (значение_1. значеиие_2 значение^); О Вставка в таблицу одной или нескольких строк, определяемых инструкцией SELECT, которая извлекает данные из одной или нескольких других таблиц: INSERT INTO таблица [(столбец 1, столбец_2 столбец_Щ AS SELECT ...; Рассмотрим несколько примеров инструкций INSERT, выполняемых в блоке PL/SQL. Для начала вставим новую строку в таблицу books. Заметьте, в том слу- случае, если в предложении VALUES заданы значения для всех столбцов, список столб- столбцов можно опустить: BEGIN INSERT INTO books VALUES С1-56592-335-91. 'Oracle PL/SQL Programming1. 'Reference for PL/SQL developers.' | 'including examples and best practice ' [ 1 recommendations.'. 'Feuerstein,Steven, with Bill Pribyl1. . TO DATE C'Ol-SEP-19971. 'DD-MON-YYYY'). 9B7): END; Можно также задать список имен столбцов, а их значения указать в виде зна- значений переменных, а не литералов: DECLARE l_isbn books.isbnSTYPE :- '1-56592-335-9'; l_title books.titleJTYPE :- 'Oracle PL/SQL Programming1; l_summary books.summarySTYPE :- 'Reference for PL/SQL developers.' || 'including examples and best practice ' || 1 recommendations.'; l_author books.authorSTYPE := 'Feuerstein.Steven, with Bill Pribyl'; l_date_published books.date_published*TYPE := TC_DATE С01-SEP-1997'. 'DD-MON-YYYY'): l_page_count books.page_countCTYPE := 987: BEGIN INSERT INTO books С isbn, title, summary, author. date_published. page_count) VALUES С ljsbn. l_title. l_sumiary. l_author, l_date_published, l_page_count); END:
DML в PL/SQL 447 Ниже приведен пример инструкции INSERT SELECT FROM, создающей «продолже- «продолжение» списка книг, написанных Стивеном Ферстайном. Обратите внимание на то, что при извлечении данных из существующих строк значения столбцов изменяются: BEGIN INSERT INTO books ( isbn. title, surmary, author, date_pubHshed. page_count) SELECT SUBSTR C1sbn, 1. LENGTH(isbn)-l) || 'X'. title || '- Part Deux', sumnary || 'plus newer stuff, author. ADD_MONTHS (date_published. 12). page count FROM books WHERE UPPER (author) LIKE 'XFEUERSTEIN. STEVEN»': END: Инструкция UPDATE С помощью этой инструкции можно обновить один или несколько столбцов или одну или несколько строк таблицы. Приведем ее синтаксис: UPDATE таблица SET столбец 1 - значение_1 [, столбец 2 - значение^ столбецJI - знзче«ие_Л/)] [WHERE условие']: " Предложение WHERE не обязательно; если оно не задано, обновляются все стро- строки таблицы. Вот несколько примеров инструкции UPDATE. О Представление названий книг таблицы books в символах верхнего регистра: UPDATE books SET title - UPPER (title): О Выполнение процедуры, удаляющей компонент времени из даты издания книг, которые были написаны указанным в аргументе автором, и переводящей на- названия этих книг в символы верхнего регистра. Как видите, инструкцию UPDA- UPDATE можно выполнять как саму по себе, так и в блоке PL/SQL: CREATE OR REPLACE PROCEDURE remove_time ( authorjn IN VARCHAR2) IS BEGIN UPDATE books SET title = UPPER (title). date_published - TRUNC (date_publ1shed) WHERE author LIKE authorjn: END; Инструкция DELETE В заключение рассмотрим инструкцию DELETE. Она предназначена для удаления одной, нескольких или всех строк таблицы. Вот ее синтаксис: DELETE FROM таблица [WHERE условие'];
448 Глава 13 • DML и управление транзакциями Предложение WHERE не обязательно. Если оно не задано, удаляются все строки таблицы. Приведем несколько примеров использования этой инструкции. О Удаление всей информации о книгах из таблицы books: DELETE FROM books: О Удаление из таблицы books всей информации о книгах, изданных до опреде- определенной даты, с возвратом их общего количества: CREATE OR REPLACE PROCEDURE remove_books ( datejn IN DATE. removal count out OUT PLS INTEGER) IS BEGIN DELETE FROM books WHERE date_published < datejn: removal countjwt := SCUROWCOUNT; END: Конечно, все эти инструкции DML в реальных приложениях обычно бывают гораздо сложнее. Например, большое количество столбцов может обновляться данными, сгенерированными вложенным запросом. В OracleSh" имя таблицы не- несложно заменить табличной функцией, возвращающей результирующий набор строк, с которым работает инструкция DML. Как уже было сказано, эта глава посвящена прежде всего особенностям ис- использования DML в PL/SQL. Из нее вы узнаете, как лучше применять возмож- возможности DML в программах PL/SQL и как управлять транзакциями, неявно опре- определяемыми при выполнении DML-ииструкций. Для начала рассмотрим атрибуты, предоставляемые Oracle для работы с неяв- неявными курсорами, следующими после DML-инструкциями. Атрибуты курсора для операций DML Для доступа к информации о последней операции, выполненной SQL-инструк- SQL-инструкцией, Oracle предоставляет несколько атрибутов курсоров, неявно открываемых для этой операции: SQLSFOUND SQL2N0TF0UND SQLSROWCOUNT SQLIISOPEN Атрибуты неявных курсоров возвращают информацию о выполнении инст- инструкций INSERT, UPDATE, DELETE и SELECT INTO. Атрибуты курсора для инструкций, мо- модифицирующих данные, рассматриваются в настоящей главе, а для инструкции SELECT INTO - в главе 14. Прежде всего следует помнить, что значения атрибутов неявного курсора все- всегда относятся к последней выполненной SQL-инструкции, независимо от того, в каком блоке выполнялся неявный курсор. До открытия первого SQL-курсора сеанса значения всех неявных атрибутов курсора равны NULL. (Исключение со- составляет атрибут SQLXISOPEN, который возвращает FALSE.) Значения, возвращаемые атрибутами неявных курсоров, описаны в табл. 13.1.
DML в PL/SQL 449 Таблица 13.1. Атрибуты неявных курсоров для DML-инструкций Имя Описание 5QL%FOUND Возвращает TRUE, если хотя бы одна или несколько строк успешно модифицированы (созданы, изменены или удалены) SQL%NOTFOUND Возвращает TRUE, если DML-инструкция не модифицировала ни одной строки SQL%ROWCOUNT Возвращает количество строк, модифицированных DML-инструкцией SQL%ISOPEN Для неявных курсоров всегда возвращает FALSE, поскольку Oracle закрывает и открывает их автоматически Теперь посмотрим, как эти атрибуты используются. О С помощью атрибута SQL* FOUND можно определить, обработала ли DML-инст- DML-инструкция хотя бы одну строку. Предположим, налример, что автор издает свои произведения под разными именами, а нам время от времени необходимо об- обновлять записи с информацией обо всех книгах данного автора. Эту задачу выполняет процедура, обновляющая данные столбца author и возвращающая значение переменной типа BOOLEAN, указывающее, было ли произведено хоть одно обновление: CREATE OR REPLACE PROCEDURE change_author_name ( old_name_in IN books.authorlTYPE. new_name_1n IN books.authoriTYPE. changesjrrade_aut OUT BOOLEAN) IS BEGIN UPDATE books SET author - new_name_in WHERE author = old_name_in; changes_made_out :- SQLSIFOUND: END: О Атрибут SQLXNOTFOUND позволяет убедиться, что DML-инструкция не обработала ни одной строки. Его значение противоположно значению атрибута SQLUFOUND. О Атрибут SQLXROWCOUNT позволит выяснить, сколько строк обработала DML-ин- DML-инструкция. Вот новая версия приведенной выше процедуры, возвращающая бо- более полную информацию: CREATE OR REPLACE PROCEDURE change_author_nare ( old_name_in IN books.authortTYPE. ' newjiamejin IN books.authorUYPE. rename_count_out OUT PLSJNTEGER) IS BEGIN UPDATE books SET author - new_name_1n WHERE author - oldjnamejn; rename_count_out := SQLSROWCQUNT; END:
450 Глава 13 • DML и управление транзакциями Предложение RETURNING в DML-инструкции Предположим, что вы выполнили инструкцию INSERT, UPDATE или DELETE и хотите получить ее результаты для дальнейшей обработки. Вместо того чтобы выпол- выполнять вслед за ней явный SQL-запрос, можно включить в инструкцию предложе- предложение RETURNING, позволяющее задать переменные программы для записи в них ре- результирующих данных. Это позволит сократить сетевой трафик и затраты ресурсов сервера, а также минимизировать количество курсоров, открываемых и управ- управляемых приложением. Вот несколько примеров, демонстрирующих эту возможность. О В следующем блоке с помощью предложения RETURNING в значение переменной программы считывается новый оклад сотрудника, вычисляемый и обновляе- обновляемый инструкцией UPDATE, и его фамилия: DECLARE щупате employee.lastjiameXTYPE; mysal employee.salaryJTYPE; BEGIN FOR rec IN (SELECT * FROM employee) LOOP UPDATE employee SET salary - new_compensat1on (rec) WHERE employeejd - rec.employee 1d RETURNING salary, lastjiame INTO mysal. myname: DBMS_OUTPUT.PUT_LINE ('Новый оклад' || myname [| ' - ' || mysal): END LOOP: END; О В следующем блоке с помощью предложения RETURNING программа считывает из базы данных локатор LOB. После этого она открывает файл, указанный в значении переменной типа BFILE и считывает хранящееся в нем изображение во BLOB-столбец таблицы базы данных: DECLARE pic_fi1e BFILE :- BFILENAME('WEB_PIX'. 'memories.jpg'); picjjlobjoc BLOB :- EMPTY_BLOB(): BEGIN INSERT INTO web_graphic_blobs VALUES A. pic_blob_loc) RETURNING image INTO plc_blob_loc: DBMS_LOB.FlLE0PEN(pic file, DBNS_LOB.FILE_READONLY); DBMS_L0B.L0ADFR0MFILE7dest_lob -> pic_blob_loc. srcjob => pic_file. amount -> DBMS_LOB.GETLENGTH(pic file)): DBMSJ.OB. FILECLOSE(pic_fi 1 e): END; О Предположим, что мы выполняем инструкцию UPDATE, модифицирующую не- несколько строк. В этом случае можно не просто сохранить возвращаемые значе- значения в переменных, а записать их как элементы коллекции, воспользовавшись специальным предложением BULK COLLECT. В данном случае инструкция UPDATE
DML в PL7SQL 451 интегрируется в специальный оператор FORALL, о котором рассказывается в этой главе: DECLARE names name_tab; new_salaries number_tab: BEGIN populate_names_array (names); FORALL indx IN names.FIRST .. names.LAST UPDATE compensation SET salary - newjrompensation (namesdndx)) WHERE name - names dndx) RETURNING salary BULK COLLECT INTO new_salaries; END; DML и обработка исключений Когда в блоке PL/SQL инициируется исключение, Oracle не выполняет откат из- изменений, внесенных входящими в его состав DML-инструкциями. Право управ- управления логическими транзакциями приложения предоставлено программисту. Рассмотрим следующую процедуру: CREATE OR REPLACE PROCEDURE empty library ( pre_empty_count OUT PLS INTEGER? IS BEGIN -- Функция tabCount возвращает количество строк -- в заданной таблице, используя встроенный -- динамический SQL. Подробности си. в главе 15, pre_empty_count :- tabcount ('books'); DELETE FROM books; RAISE NO_DATA_FOUND; END; Обратите внимание, что перед инициированием исключения установлено зна- значение параметра OUT. Теперь давайте запустим анонимный блок, вызывающий эту процедуру, и проанализируем результаты: DECLARE tab!e_count NUMBER :- -1: BEGIN INSERT INTO books VALUES (...); emptyjibrary (table_count); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE (tabcount ('books')); DBMS_OUTPUT.PUT_L1NE (table count): END;
452 Глава 13 • DML и управление транзакциями Код выводит следующие значения: О -1 Как видите, исключение было инициировано, но строки из таблицы книг при этом остались удаленными; дело в том, что Oracle не выполняет автоматического отката изменений. Однако переменная tab! e_count сохранила исходное значение. Получается, что Oracle производит откат для значений переменных программы, измененных после инициирования исключения. Таким образом, за откат транзакции отвечаете вы сами: хотите — выполняете его, а хотите — нет. Обдумывая решение, примите во внимание следующие сооб- соображения. О Если для блока выполняется автономная транзакция (о чем рассказывается далее в этой главе), в обработчике исключения нужно произвести ее откат либо фиксацию. О Для определения области отката следует воспользоваться точками сохране- сохранения. Можно произвести откат транзакции до конкретной точки сохранения, тем самым оставив часть изменений, выполненных в течение сеанса. О точках сохранения рассказывается далее в этой главе. О Если исключение передается за пределы «самого внешнего» блока (то есть остает- остается необработанным), то в среде выполнения PL/SQL, и в частности в SQLTlus, автоматически осуществляется откат транзакции и все изменения отменяются. DML и записи В Oracle9i Release 2 в инструкциях INSERT и DELETE можно использовать записи PL/SQL. Приведем пример: CREATE OR REPLACE PROCEDURE setbookjnfo ( bookjn IN booksSROkffYPE) IS BEGIN INSERT INTO books VALUES bookjn: EXCEPTION WHEN DUP VAL_ON_INDEX THEN UPDATE books SET ROW - book_in WHERE isbn = bookjn. 1sbn: END; Это очень важное нововведение, облегчающее работу программиста и позво- позволяющее сделать программный код более компактным (теперь можно, игнорируя некоторые значения, работать на уровне записей, то есть не объявлять отдельные переменные и не выделять отдельные поля записи перед передачей данных DML- инструкции), а также более надежным (когда в коде используются записи типа SfROWTYPE и не выполняются явные операции с полями указанных записей при из- изменении структуры основанных на этих записях таблиц и представлений, не при- приходится вносить значительных изменений в программный код).
DML в PL/SQL 453 Однако на использование записей в DML-инструкциях налагаются опреде- определенные ограничения, о которых рассказывается далее, в разделе «Ограничения, касающиеся операций вставки и обновления». Вставка на основе записей Б инструкциях INSERT записи можно использовать как для добавления единствен- единственной строки, так и для пакетной вставки (посредством инструкции FORALL). Запи- Записи могут создаваться с помощью объявления XROWTYPE на основе строки таблицы, в которую производится вставка, или же объявляться явно с помощью инструк- инструкции TYPE на основе типа данных, совместимого со структурой таблицы. Приведем несколько примеров. О Вставка в таблицу книг данных из записи, объявленной как XROWTYPE: DECLARE my_book booksSROWTYPE; BEGIN my_book.isbn :- '1-56592-335-9'; щу_Ьоок.title :- 'ORACLE PL/SQL PROGRAMMING1; my_book.surmary ;= 'General user guide and reference'; myjwok. author :- 'FEUERSTEIN. STEVEN AND BILL PRIBYL'; n\y_book.page_count ;- 1000; INSERT INTO books VALUES щу_Ьоок: END: Обратите внимание на тот факт, что имя записи не заключается в скобки. Если мы напишем следующее: INSERT INTO books VALUES (гцу_Ьоок): Oracle выдаст сообщение об ошибке ORA-0Q947: not enough values. Можно выполнить вставку данных, взятых из записи, тип которой определен программистом, но этот тип должен быть полностью совместимым с определе- определением строки таблицы. В частности, нельзя вставить в таблицу запись, содер- содержащую подмножество столбцов таблицы. О Выполнение вставки с помощью оператора FORALL - таким образом, в таблицу вставляются целые коллекции записей: DECLARE TYPE book_list_t IS TABLE OF booksSRCWTYPE; myjxioks book list_t :- book_list_t(): BEGIN n\y_books. EXTEND B); my_books(l).isbn := '1-56592-335-9': n\y_booksn).title :- 'ORACLE PL/SQL PROGRAMMING': my_booksB).isbn :- '0-596-00121-5' : my_booksB).title :- 'ORACLE PL/SQL BEST PRACTICES'; . FORALL Indx IN myjjooks.FIRST .. my_books.LAST INSERT INTO books VALUES my books(indx); END;
454 Глава 13 • DML и управление транзакциями Обновление на основе записей В Огас1е9г Release 2 можно обновить всю строку таблицы, добавив в нее данные из записи PL/SQL. В следующем примере для обновления строки таблицы books используется запись, созданная с помощью спецификации fcROWTYPE. Обратите внимание на новое ключевое слово ROW, указывающее, что вся строка обновляется данными из записи: DECLARE my_book booksSROWTYPE; BEGIN my book.isbn :- '1-56592-335-9'; myjwok.title :- 'ORACLE PL/SQL PROGRAMMING1: my book.suimary :- 'General user guide and reference'; myjook.author :- 'FEUERSTEIN, STEVEN AND BILL PRIBYL'; my_book.page_count :- 980; -- предполагаемое количество страниц в третьей издании UPDATE books SET ROW - щу_Ьоок WHERE 1sbn - my book.isbn; END; Существует несколько ограничений, касающихся обновления строк на основе записей. О При использовании ключевого слова ROW должна обновляться вся строка. По- Пока еще нет возможности, позволяющей выполнять обновление подмножества столбцов, но не исключено, что она появится в следующих версиях Oracle. О Нельзя обновлять данные с использованием вложенного запроса. Использование записей с предложением RETURNING В DML-инструкции можно включать предложение RETURNING, возвращающее зна- значения столбцов (и основанных на них выражений) из обработанных ими строк. Возвращаемые данные могут быть помещены в запись или даже в коллекцию за- записей: DECLARE ny_book_new_info booksXROWTYPE; n\y_book_returnjnfo booksSROWTYPE; BEGIN my_book.1sbn :- '1-56592-335-9'; myjrcok.title :- 'ORACLE PL/SQL PROGRAMMING1: myjjook.sumrary :- 'General user guide and reference'; my_book.author :- 'FEUERSTEIN. STEVEN AND BILL PRIBYL1; my_book.page_count ;- 980; -- предполагаемое количество страниц в третьем издании UPDATE books SET ROW - my_book_new_info ¦ WHERE 1sbn - my_book.isbn RETURNING isbn, title, summary, author. page_count INTO my_book return_info; END;
DML в PL/SQL 455 Заметьте, что в предложении RETURNING перечисляются все столбцы таблицы. К сожалению, Oracle не поддерживает использования символа «V Если инструкция обновляет более одной строки, в ней можно использовать предложение BULK COLLECT INTO (см. главу 14), позволяющее записать результи- результирующие данные для каждой строки в коллекцию записей: DECLARE my_book_newjnfo booksXROWTYPE: TYPE bookjist_t IS TABLE OF books INDEX BY BINARYJNTEGER; my_books bookJ1s_t: begin" -- Издатель сменил формат всех книг об Oracle, -- и поэтому для каждой из них нужно увеличить количество страниц на 10 %. UPDATE books SET page_count - page count * 0.9 WHERE UPPER (title) LIKE '«ORACLE*1 RETURNING 1sbn, title, sumary, author, page_count BULK COLLECT INTO «lyjjoolcs: END: Ограничения, касающиеся операций вставки и обновления Если вы захотите освоить операции вставки и обновления с использованием запи- записей, имейте в виду, что на их применение существуют определенные ограничения. О Переменную типа записи можно использовать лишь в перечисленных ниже конструкциях: • в правой части предложения SET инструкции UPDATE; • в предложении VALUES инструкции INSERT; • во вложенном предложении INTO предложения RETURNING. О Ключевое слово ROW используется только в левой части предложения SET. В этом случае других предложений SET быть не может (то есть нельзя задать предложение SET со строкой и затем предложение SET с отдельным столбцом). О При вставке записи не следует задавать значения отдельных столбцов. О Нельзя вставлять либо обновлять строку, указывая в инструкции INSERT или UPDATE запись (либо возвращающую ее функцию), содержащую, в свою оче- очередь, вложенную запись. О Записи не рекомендуется использовать в динамически выполняемых инструк- инструкциях DML (то есть в инструкциях, которые выполняются с помощью операто- оператора EXECUTE IMMEDIATE). Это потребовало бы от Oracle поддержки динамической привязки типа записи PL/SQL с SQL-инструкцией. Oracle же поддерживает динамическую привязку только для типов данных SQL,
456 Глава 13 • DML и управление транзакциями Пакетные DML-операции и оператор FORALL В Oracle8i введен новый оператор, значительно расширивший возможности ис- использования DML-инструкций в программах PL/SQL. Это оператор FORALL, свя- связывающий элементы одной или нескольких коллекций с SQL-инструкцией перед ее отправкой ядру SQL. Вы уже знаете, что PL/SQL тесно интегрирован с ядром SQL базы данных Oracle. Он является языком программирования, предназначен- предназначенным для баз данных Oracle, и, несмотря на возможность использования в этом ка- качестве языка Java (пока, скорее, теоретическую), ему все еще отдается безуслов- безусловное предпочтение. Но даже такая тесная интеграция не означает, что выполнение SQL-инструк- SQL-инструкций из программ PL/SQL не связано ни с какими издержками. Когда исполняю- исполняющее ядро PL/SQL обрабатывает блок кода, оно самостоятельно выполняет проце- процедурные операторы, a SQL-инструкции пересылает ядру SQL, которое их обраба- обрабатывает и, если нужно, возвращает результирующую информацию ядру PL/SQL. Передача управления (рис. 13.1) между ядрами PL/SQL и S QL называется пе- переключением контекста. На каждое такое переключение затрачиваются дополни- дополнительное время и ресурсы. Эти затраты могут быть разными, часто — довольно ощутимыми. Начиная с версии Oracle8i в PL/SQL появилась возможность объе- объединить несколько переключений контекста в одно, и тем самым повысить произ- производительность приложения. Эта возможность реализована в виде оператора FORALL, о котором рассказывается в данной главе, и предложения BULK COLLECT, об- обсуждаемого в главе 14. MpoPUSQL Блок PL/SQL Подсистема выполнения инструкций PL/SQL Ядро SQL Подсистема выполнения инструкций SQL Риь 13.1. Переключение контекста между PL/SQL и SQL При пакетном связывании для SQL-инструкции задается набор входных дан- данных. Ядро SQL выполняет инструкцию для каждого элемента набора, в результате чего за одно переключение контекста выполняется группа однотипных SQL-инст- SQL-инструкций. Этот процесс показан на рис. 13.2.
Пакетные DML-операции и оператор FORALL 457 Ядро PUSQL CREATE OR REPLASE PROCEDURE order_bDOks ( 1sbn_1n IN name_varray new_count_in IN numbervarray) IS BEGIN FORALL 1ndx IN isbn in.FIRST .. UbnJn.LAST UPDATE books SET page_count - new count in dndx) WHERE isbn -~1sbnjn (Index); End; Ядро SQL UPDATE UPDATE UPDATE UPDATE UPDATE UPDATE Подсистема выполнения инструкций SQL -mm Группа инструкций UPDATE передается ядру SQL за один раз Рис. 13.2. Одно переключение контекста для оператора FORALL Оператор FORALL Несмотря на то что оператор FORALL содержит итерационную схему (в соответст- соответствии с которой он по очереди перебирает все элементы коллекции), это1 не цикл FOR. Следовательно, в нем нет ни ключевого слова LOOP, ни ключевых слов END LOOP. Синтаксис этого оператора таков: FORALL инденс_загтси IN началыюе_значение.. .конечное_значение sql и Здесь индекс_записии — это индексная переменная, в которую будет помещаться номер очередного элемента коллекции, перебираемой оператором FORALL; началь- ное_значение - индекс элемента коллекции, с которого будет начат перебор; конеч- ное_значение — индекс элемента коллекции, на котором перебор будет окончен; sql j/нструкция — SQL-инструкция, которая должна быть выполнена для каждого элемента коллекции. При работе с оператором FORALL следует придерживаться таких правил. О В теле оператора должна содержаться одна DML-инструкция - INSERT, UPDATE или DELETE. О DML-инструкция должна ссылаться на элементы коллекции с помощью пере- переменной инцекс_записи, заданной в операторе FORALL. Областью действия пере- переменной является только оператор FORALL; вне такового она недоступна. Однако обратите внимание, что начальное и конечное значения не обязательно долж- должны охватывать всю коллекцию. О Объявлять переменную индекс_записи запрещено. Она объявляется неявно яд- ядром PL/SQL и имеет тип PLSJNTEGER.
458 Глава 13 • DML и управление транзакциями О Начальное и конечное значения определяют допустимый диапазон упорядо- упорядоченных номеров индексов коллекции. Если коллекция индексов содержит про- пропущенные номера, будет выдано следующее сообщение об ошибке: ORA-22160: element at index[3] does rot exist О Индекс элемента коллекции, задаваемый в DML-инструкции, не может быть выражением. Например, код DECLARE names name_varray :- name varrayO: BEGIN FORALL 1ndx IN names.FIRST .. names.LAST DELETE FROM emp WHERE ename - names dndx+10): END: генерирует ошибку: PLS-00430: FORALL Iteration variable INDX 1s not allowed 1n this context Проблемы, вызываемые переключением контекста Прежде чем перейти к более подробному описанию оператора FORALL, давайте про- проанализируем ситуации, в которых переключение контекста может вызвать опре- определенные проблемы. Такие ситуации возможны при обработке нескольких строк данных, хранящихся в коллекции (VARRAY, вложенной таблице или ассоциативном массиве). Предположим, нам нужна процедура для обновления количества страниц за- заданных книг в таблице books. Входные данные программы содержатся в двух кол- коллекциях: в одной — ISBN-номера обновляемых книг, во второй — новое количе- количество их страниц. Для версий Oracle до 8.1 подошло бы следующее решение (в нем используются два объявленных ранее массива типа VARRAY - name_varray и num- ber_varray): CREATE OR REPLACE PROCEDURE order_books ( isbn_in IN name_varray, new_count_in IN number varray) IS BEGIN FOR indx IN isbnjn.FIRST .. 1sbn_1n.LAST LOOP UPDATE books SET page_count - new_count_1n (indx) WHERE isbn - isbnjn Andx): END LOOP: END: Если нужно вставить 100 строк, придется выполнить по меньшей мере 100 пе- переключений контекста, поскольку для каждого обновления нужно очередной раз обращаться к ядру SQ.L. Как производятся такие многократные, но неизбежные ранее, переключения контекста можно судить по рис. 13.3.
Пакетные DML-операции и оператор FORALL 459 PUSQL-нашина CREATE OR REPLACE PROCEDURE orderjooks ( isbn_1n IN name_varray, new_count_in IN number_varray) IS BEGIN FOR 1ndx IN 1sbn 1n.FIRST .. 1sbn~1n.LAST LOOP UPDATE books SET page_count - new count 1n dndx) WHERE 1sbn - Isbnjn Andx); END LOOP: END: 1 UPDATE UPDATE UPDATE UPDATE I UPDATE UPDATE SQL-машина Подсистема выполнения инструкций SQL Каждая инструкция UPDATE передается ядру SQL отдельна Рис 13.3. Многократное переключение контекста для выполнения последовательности обновлений Точно так же происходит выборка нескольких строк информации из курсора и помещение их в коллекцию, в этом случае тоже выполняется многократное пе- переключение контекста: DECLARE CURSOR maJorjjoTI uters IS SELECT name, mileage FROM cars_and_trucks WHERE veMdejtype IN t'SUV1. 'PICKUP'): names name_varray ;- name_varray(); mileages number varray :- number_varrayO; BEGIN FOR bad_car IN major polluters LOOP names.EXTEND; names tmajor_polluters3!ROWCOUNT) :- bad_car.name; mileages.EXTEND: mileages (majorjollutersSROWCOUNT) :- bad_car.mileage; END LOOP: ... работа с данными в массивах ... END: Если вам придется программировать подобные операции, воспользуйтесь од- одним из описанных ниже пакетных операторов. При этом обращайте внимание на следующие элементы программного кода: О SQL-инструкцию, многократно выполняющуюся в цикле PL/SQL (это не обя- обязательно должен быть цикл FOR, хотя он является первым кандидатом на ис- использование при пакетной обработке);
460 Глава 13 • DML и управление транзакциями О параметры, которые можно было бы использовать для привязки переменных, при обработке оператором FORALL должны быть помещены в коллекцию. Примеры использования оператора FORALL Приведем несколько примеров использования оператора FORALL. О Переписывание процедуры order_books: CREATE OR REPLACE PROCEDURE order-books ( isbn_in IN name_varray. new_count_1n IN number_varray) IS BEGIN FORALL indx IN isbn_in.FIRST .. isbnJn.LAST UPDATE books SET page_count - new_count_in (indx) WHERE isbn - isbnjin tindx); END: Обратите внимание, что в этом примере произведены лишь незначительные изменения: оператор FOR заменен оператором FORALL и удалены ключевые слова LOOP и END LOOP. SQL-инструкции передается каждая из записей двух коллек- коллекций. На рис. 13.2 показано, как выполняется подобный код. О Вставка SQL-инструкцией INSERT в таблицу строк, состоящих из данных не- нескольких коллекций. Здесь используются три коллекции: denial, patient_name Hill nesses, но индексы в предложении VALUES инструкции INSERT заданы толь- только для двух из них. В результате пакетное связывание будет выполнено толь- только для первых двух коллекций, а третья (заданная без индекса), будет полно- полностью помещаться в столбец таблицы для каждой добавляемой строки: FORALL indx IN denial.FIRST .. denial.LAST INSERT INTO health_coverage VALUES (denial(indx), patientjiameCindx). illnesses): О Получение информации для каждой отдельной инструкции DELETE посредст- посредством включения в оператор FORALL предложения RETURNING со вложенным пред- предложением BULK COLLECT INTO: CREATE OR REPLACE FUNCTION remove_emps_by_dept (deptlist dlist_t) RETURN enolist_t IS enolist enolist_t: BEGIN FORALL aDept IN deptlist.FIRST..deptlist.LAST DELETE FROM emp WHERE deptno IN deptlistUDept) RETURNING empno BULK COLLECT INTO enolist: RETURN enolist; END:
Пакетные DML-операции и оператор FORALL <ии. Атрибуты курсора для оператора FORALL После выполнения в операторе FORALL ряда однотипных SQL-инструкций можно получить информацию о результатах, обратившись к специальным атрибутам кур- курсора. Часть этих атрибутов вам уже знакома. Кроме того, Oracle предлагает до- дополнительный атрибут *BULK_ROWCOUNT, возвращающий информацию о пакетной операции. Все атрибуты курсора описаны в табл. 13.2. Таблица 13.2. Атрибуты неявного курсора для DML-инструкций Имя Описание SQL%FOUNO Возвращает TRUE, если последняя SQL-инструкция успешно модифицировала хотя бы одну строку 5QL%NOTFOUND Возвращает TRUE, если последняя DML-инструкция не модифицировала ни одной строки SQL%ROWCOUNT Возвращает количество строк, модифицированных всеми DML-инструкциями, а не только последней из них SQL%ISOPEN Всегда возвращает FALSE. Этим атрибутом пользоваться не следует SQL%BULK_ROWCOUNT Возвращает коллекцию с информацией о количестве строк, обработанных каждой SQL-инструкцией, выполненной в операторе FORALL. Когда значение атрибута %BULK_ROWCOUNT равняется нулю, значения атрибутов %FOUND и %NOTFOUND равны FALSE и TRUE соответственно Рассмотрим составной атрибут 2BULKRQWCOUNT. Он предназначен специально для использования с оператором FORALL и имеет семантику ассоциативного масси- массива или коллекции (то есть работает как указанные структуры). В JV-й элемент этой коллекции Oracle помещает количество строк, обработанных N-й инструк- инструкцией INSERT, DELETE иди UPDATE. Если не обработана ни одна строка, ЛГ-й элемент коллекции содержит значение 0. Приведем пример использования атрибута ?BULK_ROWCOUNT (а также атрибута SRDWCOUNT): DECLARE TYPE isbnjist IS TABLE OF VARCHAR2U3): my_books isbnjist :- isbnjist ('1-56592-375-8'. '0-596-00121-5'. '1-56592-849-0'. '1-56592-335-9'. '1-56592-674-9'. '1-56592-675-7'. '0-596-00180-0'. '1-56592-457-6'): BEGIN FORALL bookjndex IN myjsooks. FIRST. .mybooks. LAST UPDATE books SET page_count WHERE isbn - iqy_books (bookjndex):
46Z Глава 13 ¦ DML и управление транзакциями -- Обновлено ли столько книг, сколько ожидалось? IF SQLXROWCOUNT !- В THEN DBMS_OUTPUT.PUT_LINE t'Обновлены не все книги!1); END IF;~ -- Проверяем выполнение инструкции UPDATE для книги -- с 1sbn 1-5659Z-335-9 IF SQL*BULK_ROWCOUNTD) - О THEN DBMS_OUTPJT.PUT_LINE ( 'Что происходит с книгой Oracle PL/SQL Programing?'); END IF: END; Существует несколько особенностей применения данного атрибута. О В операторе FORALL и в атрибуте XBULK_ROWCOUNT используются одни и те же номе- номера записей коллекции. Например, если в коллекции, переданной оператору FO- FORALL, данные имеются в элементах 10-200, значит, псевдоколлекция *BULK_ROW- COUNT тоже будет содержать значащие элементы 10-200. О Если посредством инструкции INSERT вставляется только одна строка (напри- (например, когда в предложении VALUES для нее задан список значений), значением элемента в атрибуте *BULK_ROWCOUNT является 1. А для инструкции INSERT... SELECT значение элемента в атрибуте *BULK_ROWCOUNT может быть больше 1. О Значениями элементов псевдоколлекции, возвращаемой атрибутом %BULK_ROW- COUNT, для операций удаления и вставки могут быть любые натуральные числа @ или положительные числа); в зависимости от содержимого предложения WHERE эти инструкции могут обрабатывать одну или несколько строк. Откат изменений, выполняемых с помощью оператора FORALL Итак, оператор FORALL позволяет передать ядру SQL группу однотипных инструк- инструкций. С точки зрения переключения контекста это означает, что выполняется еди- единый блок SQL-кода, состоящий из ряда отдельных инструкций. Что же происходит, если не удается выполнить одну из инструкций этого бло- блока? По умолчанию Oracle действует следующим образом. О Выполнение оператора FORALL прекращается - Oracle никогда не пропустит «провинившуюся» инструкцию и не продолжит обработку со следующей за- записи коллекции. О Выполняется откат текущей инструкции DML до неявной точки сохранения, установленной ядром PL/SQL перед выполнением инструкции. О Предшествующие операции DML, выполненные в составе текущего оператора FORALL, не откатываются.
Пакетные DML-операции и оператор FORALL 463 Продолжение работы программы после исключения, инициированного при выполнении оператора FORALL В Oracle9i введено новое предложение SAVE EXCEPTIONS, предназначенное для ис- использования в операторе FORALL, Оно предписывает Oracle продолжить выполне- выполнение блока даже в случае возникновения ошибки. Если при выполнении очеред- очередной инструкции блока инициируется исключение, Oracle сохраняет информацию об этом исключении (или о нескольких исключениях, если произойдет несколько ошибок подряд), а затем, когда будет выполнен весь блок инструкций, инициирует исключение ORA-24381. После этого управление, как обычно, передается в раздел исключений, где программа может прочитать информацию об отдельных ошиб- ошибках из псевдоколлекции SQL*BULK_EXCEPTIONS. Приведем пример программного кода, в котором производится пакетное свя- связывание и обрабатываются исключения, инициированные для отдельных SQL-ин- струхций этой операции: /* Файл в web; bulkexc.sql */ 1 CREATE OR REPLACE PROCEDURE bulk_excepttons t 2 whr 1n IN VARCHAR2 .- NULL) 3 IS 4 TYPE namel1st_t IS TABLE OF VARCHAR2 A00): 5 enames w1th_errors namel1st_t :- 6 namelist t ('LITTLE1, 'BIGBIGSERBIGGEST1, 'SNITHIE'. "): 7 bulk errors EXCEPTION: 8 PRAGMA EXCEPTIONJNIT Cbulk_errors. -24381): 9 BEGIN 10 FORALL indx IN 11 enames_w1th_errors,FIRST .. 12 enames_w1th_errors.LAST 13 SAVE EXCEPTIONS 14 EXECUTE IMMEDIATE 15 'UPDATE emp SET ename - :newnams' 16 USING enames w1th_errors(indx); 17 EXCEPTION 18 WHEN bulk_errors 19 THEN 20 FOR indx IN 1 .. SQUBULKJXCEPTIONS.COUNT 21 LOOP 22 DBMS_OUTPUT.PUT_LINE ( 23 'Error ' || indx || ' occurred during ' || 24 'iteration ' || SQL»BULK_EXCEPTIONS(indx).ERROR_INDEX || 25 ' updating name to ' || 26 enames_with errors! 27 SQLXBULKJXCEPTIONSAndx).ERROR_INOEX)): 28 DBMSJXJTPUT.PUTJJNE ( 29 'Oracle error is ' || 30 SQLERRMC-1 * SQL*BULK_EXCEPTIONS(ir,dx).ERROR_CODE)); 31 END LOOP: 32 END:
464 Глава 13 • DML и управление транзакциями Результаты выполнения данного кода таковы: SQL? exec bulk_except1ons Error 1 occurred during iteration 2 updating name to BIGBIGGERBIGGEST Oracle error 1s ORA-01401: inserted value too large for column Error 2 occurred during Iteration 4 updating name to Oracle error is ORA-01407: cannot update 0 to NULL Эти сообщения означают, что при выполнении DML-инструкции для элемен- элементов коллекции enames_with_errors Oracle инициировал два исключения. Однако после первого из них выполнение операции пакетного связывания не было пре- прервано и Oracle успешно записал в столбец паше значение третьего элемента кол- коллекции. Проанализируем приведенный выше код, воспользовавшись следующей таблицей. Строки Описание 4-6 Объявление и заполнение коллекции, которая будет управлять выполнением оператора FORALL Намеренное помещение в эту коллекцию данных, которые вызовут две ошибки 8, 9 Объявление именованного исключения с целью сделать раздел исключений более читабельным 11-17 Выполнение динамической инструкции UPDATE и оператора FORALL, обрабатывающего коллекцию enames_wlth_errors 19 Перехват исключения исходя из имени. Однако можно было бы перехватить его и по номеру: WHEN OTHERS THEN IF SQLCODE = -24381 20 Просмотр в цикле FOR с числовым счетчиком содержимого псевдоколлекции, возвращаемой атрибутом SQL%BULK_EXCEPTIONS. Обратите внимание, что для этой коллекции можно вызвать метод COUNT, возвращающий количество ее записей (сгенерированных ошибок), но нельзя вызвать другие методы, как например FIRST или LAST 22-30 Извлечение информации из коллекции и вывод ее на экран (или запись в журнал) 24 Возвращение в поле ERRORJNDEX каждой записи псевдоколлекции номера записи заданной в операторе FORALL коллекции, для которой было инициировано исключение 30 Возвращение в поле ERROR_CODE каждой записи псевдоколлекции номера инициированного исключения. Обратите внимание, что это значение хранится как положительное целое число, которое, прежде чем передать функции SQLERRM или вывести на экран, следует умножить на -1 Управление транзакциями Как и положено реляционной СУБД, Oracle поддерживает очень мощную и на- надежную модель транзакций. Код приложения определяет логическую последова- последовательность вьшолняемых транзакцией операций, результаты которых должны быть
Управление транзакциями 465 либо сохранены с помощью инструкции COMMIT, либо отменены посредством инст- инструкции ROLLBACK. Транзакция начинается неявно с первой SQL-инструкции, вы- выполняемой после инструкции COMMIT или ROLLBACK (или с начала сеанса) и продол- продолжается после обработки инструкции ROLLBACK TO SAVEPOINT. Для управления транзакциями PL/SQJL. предоставляет несколько различных инструкций: О COMMIT - сохраняет (фиксирует) все изменения, внесенные со времени выпол- выполнения последней инструкции COMMIT или ROLLBACK, и снимает все блокировки; О ROLLBACK — отменяет (откатывает) все изменения, внесенные со времени вы- выполнения последней инструкции COMMIT или ROLLBACK, и снимает все блокировки; О ROLLBACK TO SAVEPOINT - отменяет все изменения, выполненные со времени ус- установки последней точки сохранения, и снимает все блокировки, установлен- установленные в этой части кода; О SAVEPOINT - устанавливает точку сохранения, после чего становится возмож- возможным частичный откат транзакции; О SET TRANSACTION - позволяет начать сеанс чтения или чтения-записи, устано- установить уровень изоляции или связать текущую транзакцию с заданным сегмен- сегментом отката; О LOCK TABLE — позволяет заблокировать всю таблицу базы данных в указанном режиме. (По умолчанию к таблице обычно применяется блокировка на уровне строк.) Инструкция COMMIT Фиксирует все изменения, внесенные в базу данных в ходе сеанса текущей тран- транзакцией. После выполнения этой инструкции изменения становятся видимыми для других сеансов или для пользователей. Синтаксис этой инструкции таков: COMMIT [WORK] [COMMENT текст]: Ключевое слово WORK не обязательно — оно служит только для улучшения чи- читабельности кода. Ключевое слово COMMENT также не является обязательным и используется для задания комментария, который будет связан с текущей транзакцией. Текстом ком- комментария должен быть заключенный в одинарные кавычки литерал длиной до 50 символов. Обычно комментарии задаются для распределенных транзакций с целью облегчения их анализа и разрешения сомнительных транзакций (тран- (транзакции, не завершенные по причине недоступности координирующих их серве- серверов) в среде с двухфазовой фиксацией. Они хранятся в словаре данных вместе с идентификаторами транзакций. Обратите внимание на то, что инструкция COMMIT снимает все блокировки таб- таблиц, установленные во время текущего сеанса, например для инструкции SELECT FOR UPDATE. Кроме того, она удаляет все точки сохранения, установленные после выполнения последней инструкции COMMIT или ROLLBACK. Откат зафиксированных изменений с помощью инструкции ROLLBACK становится невозможным.
466 Глава 13 • DML и управление транзакциями Приведем несколько примеров использования инструкции COMMIT: COMMIT; COMMIT WORK; COMMIT COMMENT 'maintaining account balance1: Инструкция ROLLBACK Инструкция ROLLBACK выполняет откат всех или части изменений, внесенных в ба- базу данных в ходе сеанса текущей транзакции. Для чего это может потребоваться? При выполнении SQL-инструкций, задаваемых пользователем, инструкция ROL- ROLLBACK позволяет исправить некоторые ошибки, например: DELETE FROM orders: «Нет, нет! Я хотел удалить только те заказы, которые были сделаны до мая 1995 года!». При возникновении подобной ситуации следует выполнить инструк- инструкцию ROLLBACK и повторить удаление с корректным условием. Что касается SQL-ин- SQL-инструкций, выполняемых приложением, то в случае появления проблем их откат позволяет вернуться в исходное состояние. Синтаксис инструкции ROLLBACK таков: ROLLBACK [WORK] [TO [SAVEPOINT] имя_точки_сохраненияУ, Существуют две базовые модификации инструкции ROLLBACK: одна - без пара- параметров, а другая — с предложением ТО, указывающим, до какой точки сохранения следует произвести откат. Первая отменяет все изменения, выполненные в ходе текущей транзакции, а вторая отменяет все изменения и снимает все блокировки, внесенные и установленные до точки сохранения, идентифицируемой отметкой иня_точки_сохранения. (О том, как установить в приложении точку сохранения, рас- рассказывается в следующем разделе.) Параметр имя_точки_сохранения — это необъявляемый идентификатор Oracle. Им не может быть ни литерал (заключенный в кавычки), ни имя переменной. Приведем несколько примеров инструкции ROLLBACK: ROLLBACK: ROLLBACK WORK; ROLLBACK TO begin_cleanup; При откате до заданной точки сохранения все установленные после нее точки стираются, но данная точка остается. Это означает, что можно возобновить с нее транзакцию и при необходимости снова вернуться к этой же точке сохранения. Перед выполнением инструкции INSERT, UPDATE или DELETE PL/SQL автомати- автоматически устанавливает неявную точку сохранения, и если инструкция завершается ошибкой, выполняется автоматический откат до этой точки. Иными словами, лю- любая неудачная DML-инструкция просто отменяется. Инструкция SAVEPOINT Устанавливает в транзакции именованный маркер, позволяющий в случае необ- необходимости выполнить откат транзакции до отмеченной им точки сохранения. При таком откате отменяются все изменения и удаляются все блокировки после этой
Управление транзакциями 467 точки, но сохраняются изменения и блокировки, выполненные и установленные до нее. Синтахсис инструкции SAVEPOINT таков: SAVEPOINT имя_точки_сохранения; Здесь иня_точки_сохранения - необъявляемый идентификатор. Он должен соответ- соответствовать общим правилам формирования идентификаторов Oracle, то есть иметь длину до 30 символов, начинаться с буквы, и состоять из букв, цифр и символов «#», «$» и «_». Однако объявлять его не нужно (да и невозможно). Область действия точки сохранения не ограничивается блоком PL/SQL, в ко- котором она установлена. Если в ходе транзакции имя точки сохранения использо- использовано повторно, эта точка просто «перемещается» в новую позицию, причем неза- независимо от процедуры, функции или анонимного блока, в котором выполняется инструкция SAVEPOINT. Поэтому если точка сохранения устанавливается в рекур- рекурсивной программе, на самом деле на каждом уровне рекурсии она задается заново; и не рассчитывайте, что, вернувшись из очередного вызова, можно будет выпол- выполнить откат к предыдущей точке сохранения: она только одна — та, что установле- установлена последней. Инструкция SET TRANSACTION Позволяет начать сеанс чтения или чтения-записи, установить уровень изоляции или связать текущую транзакцию с заданным сегментом отката. Эта инструкция должна быть первой SQL-инструкцией транзакции и дважды использоваться в хо- ходе одной транзакции не может. У нее имеются четыре модификации. О SET TRANSACTION READ ONLY — определяет текущую транзакцию как «только для чтения». В транзакциях указанного типа всем запросам доступны лишь те изме- изменения, которые были зафиксированы до начала транзакции (благодаря чему запросам всегда доступны согласованные данные таблиц). Они применяются, в частности, в медленно формируемых отчетах со множеством запросов, бла- благодаря чему в них используются строго согласованные данные. О SET TRANSACTION READ WRITE - определяет текущую транзакцию как операцию чтения и записи данных в таблицу. О SET TRANSACTION ISOLATION LEVEL SERIALIZABLE | READ COMMITTED - определяет спо- способ выполнения транзакции, модифицирующей базу данных. С ее помощью можно задать один из двух уровней изоляции транзакции: SERIALIZABLE или READ COMMITED. Если выбран уровень SERIALIZABLE, то инструкции манипулиро- манипулирования данными (UPDATE, INSERT или DELETE), пытающейся модифицировать таб- таблицу, которая уже изменена незафиксированной транзакцией, будет отказано в этой операции. Для выполнения данной инструкции в инициализационном параметре базы данных COMPATIBLE должна быть задана версия 7.3.0 или выше. Если задан уровень READ COMMITED, DML-инструкция, которой требуется доступ к строке, заблокированной другой транзакцией, будет ждать снятия этой бло- блокировки.
468 Глава 13 ¦ DML и управление транзакциями О SET TRANSACTION USE ROLLBACK SEGMENT иня_сегнентд - назначает текущей транзак- транзакции заданный сегмент отката и определяет транзакцию как «только для чте- чтения». Она не может использоваться совместно с инструкцией SET TRANSACTION READ ONLY. Инструкция LOCK TABLE Позволяет заблокировать всю таблицу базы данных в указанном режиме. Она за- запрещает или разрешает модифицировать данные в таблице другим транзакциям на то время, пока вы с ней работаете. Синтаксис инструкции LOCK TABLE: LOCK TABLE список_гаВлиц IN ремт_6покировкк MODE [NOWAIT]; Здесь список_габлиц — список, в котором заданы одна или несколько таблиц либо представлений (то ли удаленных элементов, доступных через канал связи с базой данных); раким_блокировки - один из шести режимов: ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE. Если задано ключевое слово NOWAIT, Oracle не ждет снятия блокировки в том случае, когда нужная таблица заблокирована другим пользователем. Если же это ключевое слово не задано, Oracle ждет освобождения таблицы сколь угодно долго. Блокировка таблицы не мешает другим пользователям считывать из нее данные. Вот примеры допустимых инструкций LXK TABLE: LOCK TABLE emp IN ROW EXCLUSIVE MODE: LOCK TA8LE emp. dept IN SHARE MODE NOWAIT: LOCK TABLE scott.emp@new_jyork IN SHARE UPDATE MODE; Автономные транзакции До версии PL/SQL 8.1 в каждый конкретный момент времени в одном сеансе могла выполняться только одна активная транзакция. Иными словами, произво- производимые в сеансе изменения должны были фиксироваться или отменяться только одновременно. Это ограничение рассматривалось разработчиками как большое неудобство, поскольку им нужна была возможность сохранять и отменять от- отдельные DML-инструкции (INSERT, UPDATE и DELETE), не затрагивая всю транзак- транзакцию сеанса. Указанная возможность была реализована в PL/SQL 8.1 в виде так называемых автономных транзакций. Определяя блок PL/SQL (анонимный блок, процедуру, функцию, пакетную процедуру, пакетную функцию или триггер базы данных) как автономную транзакцию, вы изолируете выполняемые в нем DML-инструк- DML-инструкции от контекста транзакции вызывающего кода. Этот блок определяется как не- независимая транзакция, начатая другой транзакцией, которая является главной. В блоке автономной транзакции главная транзакция приостанавливается. Вы выполняете SQL-операции, затем производите их фиксацию или откат и возоб- возобновляете главную транзакцию (рис. 13.4).
Автономные транзакции 469 Главная транзакция (ГТ) PROCEDURE add_emp(...) IS BEGIN UPDATE ...: UPDATE ...: SAVEPOINT start_add; INSERT ...: COMMIT: EXCEPTION WHEN OTHERS THEN ROLLBACK TO start_add: write_log( Приостановка ГТ SQLCODE, SQLERRM;) Автономная транзакция (AT) PROCEDURE write_log(...) IS PRAGMA **. AUTONOMOUSJANSACTION BEGIN INSERT INTO log VALUESC code, text, > Начало AT USER.SYSDATE Рис. 13.4. Последовательность выполнения главной и вложенной (автономной) транзакций Определение автономной транзакции Определить блок PL/SQL как автономную транзакцию очень просто. Для этого достаточно включить в его раздел объявлений следующую директиву: PRAGMA AUTONOMOUSJRANSACnON; Данная директива предписывает компилятору PL/SQL определить блок кода как автономный, или независимый. Автономным может быть один из следующих блоков: О «самый внешний» анонимный блок PL/SQL (не вложенный); О функция или процедура, определенная в пакете или как отдельная программа; О метод (функция или процедура) объектного типа; О триггер базы данных. Директива AUTONOMOUSJTRANSACTION может располагаться, в любом месте раздела объявлений блока PL/SQL. Однако лучше всего поместить ее перед определе- определениями структур данных, чтобы каждый программист, которому придется читать ваш код, сразу идентифицировал программу как автономную транзакцию. Введе- Введение описанной директивы — единственное изменение, произведенное в PL/SQL Для поддержки автономных транзакций. Остальные операторы, инструкции COM- COMMIT и ROLLBACK, а также инструкции DML остались прежними. Теперь автономная транзакция просто меняет область их действия.
470 Глава 13 • DML и управление транзакциями В каких случаях следует применять автономные транзакции При использовании автономных транзакций необходимо придерживаться про- простого принципа: программный модуль определяется как автономный, если вы- выполняемые в нем изменения должны быть изолированы от контекста транзак- транзакции вызывающего кода. Ниже описаны типичные случаи применения автономных транзакций. О Механизм протоколирования. Вам требуется записать информацию об ошиб- ошибке в таблицу базы данных, а также выполнить откат внутренней транзакции, обусловленной возникновением ошибки. Вы не хотите, чтобы из таблицы-жур- таблицы-журнала были удалены эта и другие записи об ошибках. Какой же выход? Сделать внутреннюю транзакцию автономной! О Фиксация и откат в триггерах базы данных. Определив триггер как автоном- автономную транзакцию, все выполненные им изменения можно фиксировать или от- отменять. Разработчики давно мечтали о такой возможности. О Счетчик попыток. Предположим, что вы хотите предоставить пользователю N попыток доступа к ресурсу. Более того, вам требуется, чтобы количество по- попыток запоминалось между подключениями к базе данных. Для этого необхо- необходима инструкция COMMIT, независимая от транзакции, Пример утилиты, выпол- выполняющей подобную задачу, вы найдете в файлах retry.pkg и retry.tst на узле изда- издательства O'Reilly. О Счетчик использования программы. Вам нужно знать, как часто в ходе сеанса приложения вызывается некоторая программа. Такого рода информация не зависит от выполняемой приложением транзакции. О Многократно используемые компоненты приложения. Для программ этого типа возможность определять автономные транзакции просто жизненно необ- необходима. В современных системах, особенно в распределенном и многоуровне- многоуровневом мире Интернета, необходимы независимые программные единицы (иногда называемые картриджами), выполняющие свою работу без каких-либо побоч- побочных эффектов для вызывающего их окружения. Автономные транзакции иг- играют важную роль в этой области. Ниже речь пойдет о применении автономных транзакций в каждом из указан- указанных случаев, но сначала поговорим о том, что можно и чего нельзя делать в рам- рамках автономной транзакции. Правила и ограничения на использование автономных транзакций Включить в программный код директиву AUT0N0MQUS_TRANSACTION не составляет осо- особого труда. Однако при ее использовании существует целый ряд правил и огра- ограничений.
Автономные транзакции 471 О Автономной транзакцией может быть только анонимный блок «самого внеш- внешнего» уровня. Поэтому следующий код допустим: DECLARE PRAGMA AUTONOMOUSJRANSACTION; щуетрпо NUMBER: BEGIN INSERT INTO emp VALUES (щуетрпо. ...); COMMIT; END: В то же время программа DECLARE гцуетрпо NUMBER: BEGIN DECLARE PRAGMA AUTONOMOUS TRANSACTION; BEGIN INSERT INTO emp VALUES (inyempno, ...): COMMIT; END; END; вызовет ошибку: PLS-00710: PRAGMA AUTONOMOUSJRANSACTION cannot be declared here О Если для выполнения автономной транзакции потребуется доступ к ресурсу, заблокированному главной транзакцией (приостановленной до завершения ав- автономной программы), произойдет взаимоблокировка. Вот простой пример: программа обновляет таблицу, а затем, не зафиксировав изменения, вызывает автономную процедуру, которая должна внести изменения в ту же таблицу: /* Файл a web: autondiock.sql */ CREATE OR REPLACE PROCEDURE update_salary(deptjn IN NUMBER) IS PRAGMA AUTONOMOUSJRANSACTION: CURSOR myemps IS SELECT empno FROM emp WHERE deptno - dept in FOR UPDATE NOWAITT BEGIN FOR rec IN myemps LOOP UPDATE emp SET sal - sal * 2 WHERE empno - rec.empno; END LOOP; COMMIT: END: BEGIN UPDATE emp SET sal - sal * 2; update_salary(lO): END;
472 Глава 13 • DML и управление транзакциями Результаты выполнения этого кода не радуют: ERROR at line 1: DRA-00054: resource busy and acquire with NOWAIT specified О С помощью одной директивы невозможно определить как автономные все под- подпрограммы пакета или все методы объектного типа. Эту директиву нужно явно включать в каждую программу. Поэтому на основании только спецификации пакета нельзя выяснить, какие из программ работают в режиме автономной транзакции. О Для выхода из программы с автономной транзакцией без ошибок нужно вы- выполнить явную фиксацию или откат транзакции. Если в программе содержит- содержится незавершенная транзакция, исполняющее ядро инициирует исключение и производит откат незафиксированной транзакции: ORA-06519: active autonmous transaction deleted and rolled back О Инструкции COMMIT и ROLLBACK завершают активную автономную транзакцию, но не программу, в которой она содержится. Поэтому в блоке с автономной транзакцией может быть несколько инструкций COMMIT и ROLLBACK. О При выполнении отката до точки сохранения эта точка должна быть определе- определена в текущей транзакции. В частности, нельзя выполнить откат из автономной транзакции до точки сохранения, определенной в главной транзакции. Если же вы это сделаете, исполняющее ядро инициирует следующее исключение: ORA-01086: savepoint ' вашаточкдсохраненкя' never established О Параметр TRANSACTIONS в инициализационном файле Oracle определяет допус- допустимое количество выполняемых одновременно автономных транзакций для одного сеанса. Если в приложении используется множество программ, выпол- выполняющихся в режиме автономных транзакций, этот предел может быть превы- превышен, что вызовет инициирование следующего исключения: ORA-01574: maximum number of concurrent transactions exceeded В подобном случае следует увеличить значение параметра TRANSACTIONS. По умолчанию оно равно 75. Автономные транзакции в SQL Начиная с Oracle 7.3 появилась возможность вызывать из SQL пользовательские функции, соблюдая определенные правила. Главное из них такое: эти функции не должны обновлять базу данных. И конечно, в них нельзя сохранять или отменять изменения. Однако с появлением автономных транзакций картина изменилась. Автоном- Автономная транзакция никогда не нарушает два уровня чистоты программы — RNTJS (Reads No Database State — не читать из базы данных) и WNDS (Writes No Data- Database State — не записывать в базу данных), даже если программа выполняет чте- чтение из базы данных или запись в таковую. Это возможно благодаря тому, что уровни чистоты относятся к SQL-инструкции (которая в этом случае является главной транзакцией). Тем не менее DML-операции автономной транзакции ни- никогда не отражаются на главной транзакции.
Автономные транзакции 473 Поэтому если программа определена как выполняющая автономную транзак- транзакцию, ее можно непосредственно или косвенно вызывать из SQL-инструкции. Ко- Конечно, в том случае, когда программа не может обеспечить другого уровня чисто- чистоты, необходимого для вызова из SQL-инструкций, например WNPS (Writes No Package State — не писать в переменные пакета), ее нельзя вызывать из опреде- определенных их частей, и в частности из предложения WHERE. Предположим, что нам нужно знать, какие строки обрабатываются запросом. Для этого мы создаем таблицу query_trace: /* Файл в web: trcfunc.sql */ CREATE TABLE query_trace С tablejiame VARCHAR2C30), rowidjnfo ROWIO. queriedjty VARCHAR2C0). queried_a.t DATE Для вставки данных в таблицу напишем простую функцию: CREATE OR REPLACE FUNCTION traceit ( tab IN VARCHAR2. rowid_in IN ROWID) RETURN INTEGER IS BEGIN INSERT INTO query_trace VALUES (tab. rowidjn. USER. SYSDATE); RETURN 0: END; Попытавшись использовать эту функцию в запросе, мы получим сообщение об ошибке: SQL> select ename. traceit t'emp'. rwld) from emp; ERROR at line 1: ORA-14551: cannot perform a DML operation inside a query Однако если определить эту функцию как автономную транзакцию, добавив Директиву AirTONOMOUSJTRANSACTION, и зафиксировать результаты перед выполнени- выполнением оператора RETURN, результаты будут совсем иными. Запрос сработает, и табли- таблица query_trace будет заполнена данными о выбранных строках: SQL> SELECT enane, traceit Гетр', rowid) from emp; ENAME TRACEITC EMP', ROWID) KING 0 MILLER 0 M rows selected. QL> SELECT tablename, rowidjnfo, quer1ed_by. 2 TO_CHAR Cquer1ed_at. 'HH:MI:SS') queriedat 3 FROM querytrace;
474 Глава 13 • DML и управление транзакциями TABLEJAME ROWIDJNFO QUERIEDBY QUER[ED_AT emp AAADEPMCAAAAgDAM SCOn 05:32:54 emp AAADEPAACAAAAgOAAN SCOTT 05:36:50 Существуют и другие способы трассировки запросов: например, можно выво- выводить данные на экран с помощью программ встроенного пакета DBMS_OUTPUT или конвейерным методом пересылать информацию, используя программы пакета DBMS_PIPE. Но теперь, когда Oracle поддерживает автономные транзакции, удобнее записывать информацию в таблицы базы данных, разумеется, тщательно проана- проанализировав издержки, к которым приводит эта операция. Область видимости транзакции После выполнения в автономной транзакции инструкции COMMIT или ROLLBACK из- изменения по умолчанию немедленно становятся видимыми в главной транзакции. Но что если потребуется скрыть их от последней? Для этой цели в Oracle имеется специальная модификация инструкции SET TRANSACTION: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE: По умолчанию для транзакции устанавливается уровень изоляции READ COM- MITED, при котором зафиксированные изменения тут же становятся видимыми главной транзакции. Инструкцию SET TRANSACTION, как обычно, следует выполнить до начала тран- транзакции (то есть перед первой SQL-инструкцией). Кроме того, данная установка влияет на весь сеанс, а не только на текущую программу. Следующий сценарий демонстрирует уровень изоляции SERIALIZABLE в действии. (Вы найдете этот сце- сценарий в файле autonserial.sql на узле O'Reilly и сможете самостоятельно выпол- выполнить описанные ниже операции.) Итак, сначала создается автономная процедура: /* Файл в web: autonserial.sql */ CREATE OR REPLACE PROCEDURE f1re_em_all IS PRAGMA AUTONOMOUSJRANSACTION: BEGIN DELETE FROM anpZ; COMMIT: END; Затем выполняется сценарий, который устанавливает уровень изоляции SE- SERIALIZABLE и выводит количество строк таблицы етр2 в следующие моменты: О до вызова процедуры f i re_em_a 11; О после вызова процедуры f 1 re_em_al I, но до фиксации или отката главной тран- транзакции; О после фиксации главной транзакции.
Автономные транзакции 475 Вот этот сценарий: DECLARE PROCEDURE showcount (str VARCHAR2) IS num INTEGER: BEGIN SELECT COUNT(*) INTO num FROM tasks: DBMS_OUTPUT.PUT_LINE (str || ' ' || num || ' строк1); END: BEGIN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; showcount С'Перед изолированной автоноиной транзакцией усалено'): f1ГЕ_ет_аТ1: showcount ('После изолированной автономной транзакции удалено'); COMMIT; showcount ('После главной транзакции зафиксировало'}; END: Он выводит следующие строки: Перед изолированной автономной транзакцией удалено 14 строк После изолированной автономной транзакцией удалено 14 строк После главной транзакции зафиксирован™ О строк Примеры автономных транзакций В этом разделе приведено несколько примеров использования автономных тран- транзакций в приложениях. Создание механизма автономного протоколирования Одним из распространенных требований к приложениям является ведение жур- журнала ошибок, происходящих в ходе выполнения транзакций. Самым удобным ме- местом для хранения журнала является таблица базы данных, поскольку она всегда доступна и из нее удобно с помощью SQL извлекать информацию для анализа. Однако у такого журнала имеется один недостаток: операции записи в журнал становятся частью транзакции. И если в программе будет выполнена инструкция ROLLBACK, из журнала может быть удалена часть записей об ошибках. Это, конечно же, недопустимо. Можно попытаться с помощью точек сохранения выполнить откат таким образом, чтобы записи журнала оставались нетронутыми, но это крайне неудобно. А вот если воспользоваться механизмом автономных транзак- транзакций, протоколирование значительно упростится и станет более надежным. Предположим, что у нас имеется следующая таблица-журнал: /* Файл в web: log.pkg */ CREATE TABLE logtab ( code INTEGER. text VARCHAR2D000). created_on DATE. createdj» VARCHAR2A00). changed on DATE. changedJ>y VARCHAR2C100).
476 Глава 13 • DML и управление транзакциями machine VARCHAR2U00). program VARCHAR2U00) ); Ее можно использовать для хранения информации об ошибках (с помощью функций SQLCODE и SQLERRM), а также для протоколирования других событий, не связанных с ошибками. В столбцы machine и program помещается информация из виртуальной таблицы VSSESSION. Вы спрашиваете, как пользоваться этим журналом? Прежде всего не следует делать следующего: EXCEPTION . WHEN OTHERS THEN v_code ;- SQLCODE; vjnsg :- SQLERRM: INSERT INTO logtab VALUES ( v_code. vjnsg. SYSDATE, USER, SYSDATE. USER. NULL. NULL): END: Иными словами, никогда не реализуйте механизм протоколирования в разде- разделе исключений и других разделах основного кода приложения. Выполняйте все операции с таблицей-журналом на отдельном программном уровне (технология называется инкапсуляцией). Для этого есть два существенных основания. О Если структура таблицы однажды изменится, программы, в которых произво- производится протоколирование ошибок, не пострадают. 0 Программистам гораздо проще работать с журналом, их операции с таблицей всегда одинаковы. В нашем примере протоколирование осуществляется с помощью простого па- пакета, состоящего из двух процедур: CREATE OR REPLACE PACKAGE log IS PROCEDURE putline ( codejin IN INTEGER, textjin IN VARCHAR2): PROCEDURE saveline ( codejin IN INTEGER, textjin IN VARCHAR2): END; В чем разница между процедурами putline и saveline? Процедура log.save!ine (как вы увидите далее в теле пакета) является автономной транзакцией, тогда как 1 og. put! i ne просто вставляет строку в таблицу. Вот тело пакета: /* Файл в web: log.pkg */ CREATE OR REPLACE PACKAGE BODY log IS CURSOR sess IS SELECT MACHINE. PROGRAM FROM VSSESSION WHERE AUDSID - USERENV('SESSIONID'): rec sessIROWTYPE:
Автономные транзакции 477 PROCEDURE putline С codejn IN INTEGER, textjn IN VARCHAR2) IS BEGIN INSERT INTO logtab VALUES ( codejn. textjn. SYSDATE. USER, SYSDATE, USER. rec.machine, rec.program): END: PROCEDURE saveiine! codejn IN INTEGER, textjn IN VARCHAR2) IS PRAGMA AUTONOMOUSJRANSACTION: BEGIN putl1ne(CQdeJn. text in): COItllT: EXCEPTION WHEN OTHERS THEN ROLLBACK: END: END: Следующие комментарии помогут вам понять происходящее в программах. О Во время инициализации пакета мы считываем из представления VSSESSION информацию, которую потом будем записывать в журнал (значения в течение сеанса не изменяются, и поэтому достаточно запросить их только один раз). О Процедура putl i ne выполняет обыкновенную вставку. Возможно, вы захотите добавить в нее раздел обработки исключений, если возьмете пакет 1 од за осно- основу подсистемы протоколирования ошибок для реального приложения. О Процедура savel i пе вызывает процедуру putl i ne, но делает это в контексте ав- автономной транзакции. При наличии описываемого пакета обработка ошибок значительно упрощается: EXCEPTION WHEN OTHERS THEN log.saveline (SOLCODE, SQLERRM): END; Отлично. Разработчикам не нужно задумываться о структуре таблицы журна- журнала и, более того, они могут вообще не знать, что запись производится в таблицу "базы данных. А поскольку транзакция автономна, что бы ни происходило в при- приложении - журнал останется в целости и сохранности.
478 Глава 13 • DML и управление транзакциями Использование автономных транзакций в триггерах базы данных Благодаря введению автономных транзакций появились новые возможности при работе с триггерами (о которых рассказывается в главе 18): теперь в них можно использовать инструкции COMMIT и ROLLBACK, чего ранее не разрешалось. При этом фиксация или откат транзакции в триггере не отражается на главной транзакции, из которой он был запущен; эти операции относятся только к DML-инструкциям, выполняемым внутри триггера (и хранимых программ, вызываемых из триггера). В триггерах базы данных часто осуществляются действия, которые не должны отражаться на исходной транзакции. Предположим, что нам нужно отслеживать все выполняемые по отношению к таблице операции независимо от того, завер- завершены они или нет; следует тахже знать, какие из операций завершились ошибками. Давайте посмотрим, как это можно сделать с помощью автономных транзакций. Создание триггера базы данных Для начала напишем простой триггер с автономной транзакцией, записывающий сообщение в таблицу ceo_comp_hi story. Вот определения двух таблиц: /* Файл a web: autontrigger.sql */ CREATE TABLE ceo_compensat1on С company VARCHAR2C100), name VARCHAR2A00). compensation NUMBER, layoffs NUMBER); CREATE TABLE ceo_comp_h1story ( name VARCHAR2A00), description VARCHAR2B55), occurred_on DATE): А вот сам триггер: CREATE OR REPLACE TRIGGER bef ins_ceo_co<np BEFORE INSERT ON ceo_compensat1on FOR EACH ROW DECLARE PRAGMA AUTONOMOUS_TRANSACTION: BEGIN INSERT INTO ceo_comp_h1story VALUES С mew.name. 'BEFORE INSERT', SYSDATE): COMMIT; END; С помощью этого триггера можно протоколировать все попытки вставки дан- данных в таблицу ceo_compensation, как показано в следующем примере: BEGIN INSERT INTO ceojrompensation VALUES ( 'Mattel1. 'Jill Barad', 91000D0, 2700); INSERT INTO ceo_compensation VALUES С 'American Express Company', 'Harvey Golub'. 33200000. 3300); INSERT INTO ceo_compensation VALUES ( 'Eastman Kodak'. 'George Fisher'. 10700000. 20100):
Автономные транзакции 479 ROLLBACK: END: SQL> SELECT name, 2 description, 3 TO_CHAR (occurred_on, 'MM/DD/mY HH:MI:SS') occurredjjn 4 FROM ceocoip history: NAME DESCRIPTION OCCURREDJJN Mattel BEFORE INSERT 03/17/1999 04:00:56 American Express Company BEFORE INSERT 03/17/1999 04:00:56 Eastman Kodak BEFORE INSERT 03/17/1999 04:00:56 В файле autontrigger.sql на узле издательства O'Reilly вы найдете весь код, не- необходимый для создания объектов и тестирования триггера. Доработка триггера базы данных К сожалению, у нашего триггера имеется один существенный недостаток. Он оп- определяется как выполняющий транзакцию, поскольку в теле триггера модифици- модифицируется таблица базы данных и фиксируются изменения. Но что если в нем же по- потребуется произвести некоторые операции, относящиеся к главной транзакции? Эти изменения в случае отката главной транзакции сохранятся, что, конечно же, недопустимо с точки зрения целостности данных. Поэтому в общем случае не стоит определять триггеры базы данных как авто- автономные транзакции. Все независимые DML-операции (такие как запись инфор- информации в журнал) лучше осуществлять в отдельной процедуре, вызываемой из триггера и определяемой в качестве выполняющей автономную транзакцию. Это решение реализовано в сценарии autontrlgger2.sql, который вы также най- найдете на узле O'Reilly. Сначала создается процедура /• Файл в web: autontr1gger2.sql */ CREATE OR REPLACE PROCEDURE audit ceo corap ( name IN VARCHAR2. description IN VARCHAR2, occurred on IN DATE ) IS PRAGMA AUTONOMOUSJRANSACTION; BEGIN INSERT INTO ceo_comp_h1story VALUES ( aud1t_ceo_comp.name. audit_ceo_comp.description, audit_ceo_comp.occurred on ): COMMIT: END: Новый триггер, вызывающий данную процедуру, будет таким: CREATE OR REPLACE TRIGGER aft_i ns_ceo_comp Af^ER INSERT ON ceo compensation FOR EACH ROW DECLARE ok BOOLEAN :- is valid comp info (:new.name): BEGIN
480 Глава 13 • DML и управление транзакциями IF ok THEN audit_ceo_ccmp ( :new.name. 'AFTER INSERT1. SYSDATE); ELSE RAISE VALUEJRROR: END IF; END: Обратите внимание на следующие усовершенствования. О Мы изменили спецификацию триггера - теперь это AFTER INSERT, а не BEFORE INSERT. Пусть он лучше вызывается после операции INSERT и протоколирует уже выполненные изменения. О Если функция i s_va I i dcompi nf о возвращает FALSE, протоколирование не вы- выполняется, а транзакция прекращается и инициируется исключение. Это еще одна из причин, по которым триггер не должен быть автономным: в одних слу- случаях вызываемая им процедура должна выполнятся, а в других — главная тран- транзакция должна быть остановлена с помощью исключения. Новая технология автономных транзакций очень удачна, и вы, наверняка, не раз будете иметь возможность это оценить. Но учтите, что транзакции требуют самого тщательного планирования. И поэтому независимые транзакции всегда следует скрывать за процедурным интерфейсом.
14 Выборка данных ^ Основы курсоров р- Работа с неявными курсорами ^ Работа с явными курсорами *¦ Предложение BULK COLLECT > Инструкция SELECT с предложением FOR UPDATE > Курсорные переменные > Курсорные выражения (Oracle9i) Одной из важнейших характеристик PL/SQL является тесная интеграция с ба- базой данных Oracle как в отношении изменения данных в таблицах, так и в отно- отношении выборки информации из таблиц. В этой главе рассматриваются элементы PL/SQL, связанные с выборкой информации из базы данных и ее обработкой в программах PL/SQL. Когда SQL-инструкция выполняется из PL/SQL, РСУБД Oracle назначает ей приватную рабочую область, а некоторые данные записывает в системную гло- глобальную область (System Global Area, SGA). В приватной рабочей области содер- содержится информация об SQL-инструкции и набор данных, возвращаемых или об- обрабатываемых этой инструкцией. PL/SQL предоставляет программистам несколько механизмов доступа к этой рабочей области и содержащейся в ней информации. Перечислим их. О Неявные курсоры. Инструкция SELECT.. .INTO считывает одну строку данных и присваивает ее в качестве значения локальной переменной программы. Это простейший (и зачастую наиболее эффективный) способ доступа к данным, но он часто ведет к написанию сходных и даже одинаковых SQL-инструкций во многих местах программы. О Явные курсоры. Запрос можно явно объявить как курсор в разделе объявле- объявлений локального блока или пакета. После этого такой курсор можно будет от- открывать и выбирать из него данные в одной или нескольких программах, при- причем возможности управления явным курсором шире, чем неявным.
482 Глава М • Выборка данных О Курсорные переменные. Курсорные переменные (в объявлении которых за- задан тип REF CURSOR) позволяют передавать из программы в программу указа- указатель на результирующий набор строк запроса. В любой программе, имея дос- доступ к такой переменной, можно открыть курсор, извлечь из него необходимые данные и закрыть его. О Курсорные выражения. Введенные в Огас1е9г курсорные выражения, зада- задаваемые с помощью оператора CURSOR, преобразуют извлекаемые инструкцией SELECT данные в результирующий набор строк типа REF CURSOR и могут исполь- использоваться совместно с табличными функциями для повышения производитель- производительности приложения. О Динамические SQL-запросы. Oracle позволяет динамически составлять и вы- выполнять запросы, используя либо встроенный динамический SQL (NDS), ли- либо программы пакета DBMSSQL. Об NDS, введенном в Oracle8i, рассказывается в главе 15, а встроенный пакет DBMS_SQL подробно описан в книге Oracle Built-in Packages издательства O'Reilly. В этой главе рассказывается о неявных и явных курсорах, курсорных пере- переменных и выражениях. Основы курсоров Простейшую форму курсора можно представить себе как указатель на таблицу в базе данных. Например, следующее объявление курсора связывает всю таблицу employee с курсором employee_c'ur: CURSOR anployee_cur IS SELECT * FROM employee: После объявления курсора его можно открыть: OPEN employee_cur: Далее можно выбирать из него строки: FETCH employee_cur into employeej-ec; Окончив работу с курсором, его следует закрыть: CLOSE employee_cur; В рассматриваемом примере каждая выбранная из курсора запись представля- представляет собой строку таблицы employee. С курсором можно связать любую допустимую инструкцию SELECT. В следующем примере в объявлении курсора объединяются три таблицы: DECLARE CURSOR joke_feedback_cur IS SELECT J.name. R.laugh_volume. C.name FROM joke J. response R. comedian С WHERE J.jokejid - R.jokejd AND J.jokeMd - C.jokerjd; BEGIN END:
Основы курсоров 483 В данном случае курсор действует не как указатель на конкретную таблицу базы данных — он указывает на виртуальную таблицу или неявное представление, определяемое инструкцией SELECT. (Мы называем такую таблицу виртуальной по- потому, что инструкция SELECT генерирует данные, имеющие табличную структуру, но эта таблица существует только временно, пока программа работает с возвра- возвращенными инструкцией данными.) Если тройное объединение возвращает табли- таблицу, содержащую 20 строк и три столбца, курсор функционирует как указатель на эти 20 строк. Некоторые термины, связанные с выборкой данных В PL/SQL имеется множество возможностей выполнения SQL-инструкций, и все они реализованы в программах как курсоры того или иного типа. Прежде чем приступить к их освоению, вам нужно познакомиться с методами выборки дан- данных и используемой при этом терминологией. О Статический SQL. SQL-инструкция является статической, если она опреде- определяется во время компиляции программы. О Динамический SQL. SQL-инструкция является динамической, если она опре- определяется во время выполнения программы и затем выполняется, так что в про- программном коде нет ее полного, жестко закодированного объявления. Для ди- динамического выполнения SQL-инструкций могут использоваться программы встроенного пакета DBMSJSQL (имеющегося во всех версиях Oracle) или встро- встроенный динамический SQL, введенный в Oracle8i и описанный в главе 15. О Результирующий набор строк. Это набор строк, содержащих результирую- результирующие данные, определяемые SQL-инструкцией. Результирующий набор строк кэшируется в системной глобальной области с целью ускорения чтения и мо- модификации его данных. Для доступа к нему Oracle поддерживает указатель, который мы будем называть текущей строкой. О Неявный курсор. При каждом выполнении инструкции DML (INSERT, UPDATE или DELETE) либо инструкции SELECT INTO, возвращающей строку из базы дан- данных прямо в структуру данных программы, PL/SQL создает неявный курсор. Курсор этого типа называется неявным, поскольку Oracle автоматически вы- выполняет многие связанные с ним операции, такие как открытие, выборка строк и даже закрытие. О Явный курсор. Это инструкция SELECT, явно определенная в программе как курсор. Все операции с явным курсором (его открытие, выборка данных, за- закрытие и т. д.) в программе должны выполняться явно. Как правило, явные курсоры используются для выборки из базы данных набора строк с помощью статического SQL. О Курсорная переменная. Это объявленная программистом переменная, указы- указывающая на объект курсора в базе данных. Ее значение (то есть указатель на курсор или результирующий набор строк) во время выполнения програм- программы может меняться, как у самой настоящей переменной. В разные моменты времени курсорная переменная может указывать на разные объекты курсора.
484 Глава 14 • Выборка данных Курсорную переменную можно передать в качестве параметра процедуре или функции. Такие переменные очень полезны для передачи результирующих наборов строк из программы PL/SQL в другие среды, такие как Java или Vi- Visual Basic. О Атрибут курсора. Атрибут курсора имеет форму %иня_атрибута и добавляется к имени курсора или курсорной переменной. Это что-то вроде внутренней пе- переменной Oracle, возвращающей информацию о состоянии курсора, например о том, открыт ли курсор или сколько строк из курсора вернул запрос. Для яв- явных и неявных курсоров и для динамического SQL атрибуты курсора работа- работают немного по-разному. О Инструкция SELECT...FOR UPDATE. Это особая разновидность обычной инструкции SELECT, устанавливающая блокировку на каждую возвращаемую запросом строку данных. Пользоваться ею следует только в тех случаях, когда нужно «зарезервировать» запрошенные данные, с тем чтобы никто другой не смог их изменить, пока с ними работаете вы. О Пакетная обработка. В OracleBi и выше PL/SQL поддерживает запросы с пред- предложением BULK COLLECT, позволяющим за один раз выбрать из базы данных бо- более одной строки. Типичные операции с запросами и курсорами Независимо от типа курсора процесс выполнения SQL-инструкций всегда состо- состоит из одних и тех же действий. В одних случаях PL/SQL производит их автома- автоматически, а в других, как, например, при использовании явного курсора, они явно организуются программистом. О Синтаксический анализ. Первым делом при обработке SQL-инструкции дол- должен быть выполнен ее синтаксический анализ, то есть проверка ее корректно- корректности и формирование плана ее выполнения (с использованием оптимизации по синтаксису или по стоимости, в зависимости от того, какое значение парамет- параметра OPTIMIZERMODE задал администратор базы данных). О Привязка. Привязкой, или связыванием, называется установка соответствия между значениями хост-переменных программы и параметрами SQL-инструк- SQL-инструкции. Для статического SQL привязка производится ядром PL/SQL. Привязка параметров в динамическом SQL выполняется явно с помощью переменных привязки. О Открытие. При открытии курсора определяется результирующий набор строк SQL-инструкции, для чего используются переменные привязки. Указатель активной или текущей строки указывает на первую строку результирующего набора. Иногда явное открытие курсора не требуется; ядро PL/SQL выполня- выполняет эту операцию автоматически (так происходит в случае применения неяв- неявных курсоров и встроенного динамического SQL). О Выполнение. На этой стадии инструкция выполняется ядром SQL. О Выборка. Выборка очередной строки из результирующего набора строк кур- курсора осуществляется с помощью инструкции FETCH. После каждой выборки
Основы курсоров 485 PL/SQL перемещает указатель на одну строку вперед. Работая с явными кур- курсорами, помните, что и после того как все строки будут исчерпаны, можно сно- снова и снова выполнять инструкцию FETCH, но PL/SQL ничего не будет делать (и не станет инициировать исключение). О Закрытие. Эта операция закрывает курсор и освобождает используемую им память. После закрытия курсор больше не содержит результирующего набора строк. Иногда явное закрытие курсора не требуется, поскольку PL/SQL дела- делает это сам (для неявных курсоров и встроенного динамического SQL). Операции, выполняемые для выборки информации из базы данных в про- программу PL/SQL, показаны на рис. 14.1. База данных Таблица books Извлечение подмножестве [данных таблицы Общая память (SGA) Коиандв вьборхи Следующая строка данных Программа PL/SQL FUNCTION book_in_stock... RETURN BOOLEAN IS CURSOR book_cur IS...; book_rec book_curJ!ROWTYPE: BEGIN OPEN book cur; FETCH book cur: INTO book rec: CLOSE book_cur; END book in stock Рис. 14.1. Упрощенное представление процесса выборки данных с помощью курсора Знакомство с атрибутами курсора В данном разделе перечисляются и вкратце описываются атрибуты курсоров, о которых подробно рассказывается в этой главе, в разделах, посвященных раз- разным типам курсоров, а также в главах 13 и 15. PL/SQL поддерживает шесть атрибутов курсоров, перечисленных в табл. 14.1. Таблица 14.1. Атрибуты курсоров Имя Что возвращает %FOUND Значение TRUE, если успешно выбрана хотя бы одна строка; в противной случае возвращает значение FALSE %NOTFOUND Значение TRUE, если инструкция не выбрала ни одной строки; в противном случае возвращает значение FALSE продолжение^
486 Глава 14 • Выборка данных Таблица 14.1 (продолжение) _^ Имя Что возвращает %ROWC0UNT Количество строк, выбранных из курсора на данный момент времени %ISOPEN Значение TRUE, если курсор открыт; в противном случае возвращает значение FALSE %BULK_ROWCOUNT Коллекцию, в которой для каждого элемента исходной коллекции, заданной в операторе FORALL, указано количество строк, модифицированных SQL-инструкцией %BULK_EXCEP"nONS Коллекцию, в которой для каждого элемента исходной коллекции, заданной в операторе FORALL, указано инициированное Orade исключение Для ссылки на атрибут курсора укажите в виде его префикса имя курсора или курсорной переменной, Вот несколько примеров. О Открыт ли еще явный курсор? DECLARE CURSOR happiness_cur IS SELECT simplejtelights FROM ...: BEGIN OPEN happiness_cur; IF happiness_cur*ISOPEN THEN ... О Сколько строк мы выбрали из неявного курсора? (Обратите внимание, что именем курсора в данном случае является SQL.) DECLARE "TYPE numjab IS TABLE OF NUMBER: deptnums nutn_tab: BEGIN SELECT deptno BULK COLLECT INTO deptnums FROM dept; DBMS_OUTPUT.PUT_LINE CSQLXROUCOUNT); END; Атрибут курсора %FOUND Атрибут 3IFOUND возвращает информацию о результате самой последней выборки из курсора. Он возвращает TRUE, если последняя инструкция FETCH для явного курсора вернула строку, и FALSE — в противном случае. Если курсор еще не отрыт, обращение к атрибуту IFOUNTJ вызывает исключение INVALIDCURSOR. Этот атрибут можно использовать для любого открытого курсора, указав, как обычно, перед именем атрибута имя курсора. В следующем примере в цикле перебираются все строки курсора саПегсыг (содержащие сведения о звонящих), и для каждой строки обновляются все строки таблицы cal 1, содержащие информацию о звонках данного человека до текущей даты. Если после очередной выборки атрибут XFQUND возвращает FALSE, выполняется
Основы курсоров 487 выход из цикла. Этот атрибут проверяется также после каждого обновления, пе- перед выводом информации об обновленной строке. OPEN caller_cur: LOOP FETCH caller_cur INTO caller_rec; EXIT WHEN NOT caller_curSFOUND: UPDATE call SET callerjd - caller_rec.caller_1d WHERE call_timestarnp < SYSDATE; IF SQLSFDUND THEN DBMS_OUTPUT.PUT_LINE ( 'Информация о звонках обновлена для' || callerrec. callerjd); END IF; END LOOP; CLOSE caller_cur; Атрибут курсора %NOTFOUND Атрибут BNOTFDUND противоположен атрибуту fcFOUND. Он возвращает TRUE, если по- последняя инструкция FETCH для явного курсора не вернула ни оной строки, по- поскольку последняя строка курсора уже выбрана предыдущей операцией. Если же курсор не может вернуть строку из-за ошибки, инициируется соответствующее исключение. Если курсор еще не отрыт, обращение к атрибуту ENOTFOUND вызывает исключе- исключение INVALID_CURSOR. Для любого открытого курсора можно использовать этот ат- атрибут, указав, как обычно, перед именем атрибута имя курсора. В каких случаях лучше использовать атрибут XFOUND, а в каких 2N0TF0UND? Прин- Принципиальной разницы здесь нет. Просто выбирайте тот атрибут, который является для проверяемого условия более естественным. Например, в приведенном выше примере для выхода из цикла используется следующий оператор: EXIT WHEN NOT caller_cur*FOUND: Однако более читабельным (и эффективным) был бы следующий, эквивалент- эквивалентный ему оператор: EXIT WHEN caller_curSNOTFOUND; Атрибут курсора "/oROWCOUNT Атрибут ^ROWCOUNT возвращает количество строк, извлеченных из курсора на мо- момент обращения к этому атрибуту. Сразу после открытия курсора атрибут воз- возвращает нуль. Если же обратиться к атрибуту &ROWCOUNT еще не открытого курсора, будет инициировано исключение INVALIDCURSOR. После выборки очередной стро- строки значение атрибута fcROWCQUNT увеличивается на 1. С помощью атрибута XROWCOLJNT можно узнать, выбрано ли заданное количест- количество строк (или обновлено - в случае использования курсора для DML-операций), либо остановить программу после выполнения указанного количества итераций.
488 Глава 14 • Выборка данных Атрибут курсора %ISOPEN Атрибут SISOPEN возвращает значение TRUE, если курсор открыт, и значение FALSE в противном случае. Атрибут курсора %BULK_ROWCOUNT Атрибут *BULK_ROWCOLINT, предназначенный для использования с оператором FORALL, возвращает количество строк, обработанных каждой DML-инструкцией. Этот ат- атрибут имеет семантику ассоциативного массива. Подробнее о нем рассказывается в главе 13. Атрибут курсора %BULK_EXCEPTIONS Атрибут *BULK_EXCEPTIONS, предназначенный для использования с оператором FO- FORALL, возвращает информацию об исключениях, которые были инициированы при выполнении каждой DML-инструкции. Этот атрибут имеет семантику ассоциа- ассоциативного массива. Подробнее о нем рассказывается в главе 13. ВНИМАНИЕ Как показывают приведенные выше примеры, атрибуты курсоров можно использовать в програм- программах PL/SQL, но их нельзя применять в SQL-инструкциях. Например, попытавшись использовать ат- атрибут %ROWCOUNT в предложении WHERE инструкции SELECT: SELECT callerjd. companyid FROM caller WHERE company_id - caller_cur*ROWCOUhfl' вы получите сообщение об ошибке: PLS-OO229: Attribute expression within SQL expression Ссылки на переменные PL/SQL в курсоре Поскольку курсор должен быть связан с инструкцией SELECT, в нем всегда тгрисуг- ствовует хотя бы одна ссылка на таблицу базы данных, из которой будет выпол- выполняться выборка строк, а также условие выборки (предложение WHERE). Однако это не означает, что инструкция SELECT может возвращать информацию только из базы данных. Список выражений, задаваемых между ключевыми словами SELECT и FROM, на- называется списком выборки. Во встроенном SQL список выборки может содержать столбцы и выражения (SQL-функции этих столбцов, константы, и т. п.). В PL/SQL список выборки обычно содержит переменные PL/SQL и сложные выражения. В следующем примере курсора инструкция SELECT извлекает строки на основе таблицы employee, но в списке выборки заданы не только столбцы этой таблицы, но и переменная PL/SQL, а также переменная привязки из хост-среды (элемент Oracle Forms): DECLARE /* Локальная переменная PL/SQL */ projected_bonus NUMBER :=¦ 1000: /* || Курсор прибавляет $1000 к окладу каждого сотрудника. || принятого на работу более трех лет назад. */
Основы курсоров 489 CURSOR employee_cur IS SELECT employeejd, salary + projected_bonus new_salary. /* Псевдоним столбца */ :review.evaluation /* Переменная привязки */ FROM employee WHERE hiredate < ADD_MONTHS (SYSDATE. -36): BEGIN END; На локальные данные программы PL/SQL (переменные и константы), а также на переменные привязки хост-среды можно ссылаться в предложениях WHERE, GROUP BY и HAVING инструкции SELECT курсора. Приоритет идентификаторов в инструкции курсора Совместное использование переменных и столбцов базы данных требует внима- внимательного отношения к их именам. Например, переменным часто присваивают имена столбцов данных, которые они представляют. Это очень удобно, но только до тех пор, пока вы не захотите задать в SQL-инструкции и локальные перемен- переменные, и столбцы таблицы. В следующем примере из таблицы выбираются записи обо всех сотрудниках, принятых на работу более трех лет назад, для того чтобы затем прибавить к их ок- окладам по $1000. В таблице сотрудников имеется столбец с именем salary, и такая же переменная имеется в программе. Хотя программный код будет откомпилиро- откомпилирован без ошибок, результаты мы получим неправильные. PROCEDURE irnprove_QOL IS /* Локальная переменная с тем же именем, что и столбец */ salary NUMBER :- 1000; CURSOR double_sal cur IS SELECT salary + salary FROM employee WHERE hiredate < AOD_M0NTHS {SYSDATE. -36); BEGIN end! Вместо того чтобы добавить к окладу $1000, этот код удвоит каждый оклад. В SQL-инструкции неуточненный идентификатор sal ary будет воспринят как об- обращение к столбцу таблицы employee. Для получения нужных результатов нужно уточнить имя переменной PL/SQL именем процедуры: CURSOR double sal cur IS ~ ~ SELECT salary + improve_QOL.salary FROM employee WHERE hiredate < ADD_MONTHS (SYSDATE. -36);
490 Глава 14 • Выборка данных В данном случае мы указываем компилятору, что второй идентификатор sa- salary соответствует переменной процедуры improve_QOL. Поэтому SQL-инструкция сложит значение столбца со значением переменной. Стандартные соглашения об именах Еще одним распространенным способом минимизации конфликтов имен между SQL и PL/SQL является использование стандартизированного соглашения об именах. Предположим, что в некоторой компании принято прибавлять к именам всех локальных переменных префикс «1_». Тогда объявление переменной будет таким: PROCEDURE improve_QOL IS /* Локальная переменная с тем не именем, что и столбец */ Isalary NUMBER :- 1000; А курсор будет определен так: CURSOR double_sal_cur IS SELECT salary + I_salar7 FROM employee WHERE hiredate < ADD_MONTHS tSYSDATE. -36): Код будет прекрасно работать, если только однажды администратору базы дан- данных не придет в голову добавить в таблицу employee столбец с именем Isalary. Конечно, это маловероятно, но все же возможно. Поэтому во избежание ошибок надежнее всего имена переменных PL/SQL в SQL-инструкциях всегда уточнять, не надеясь ни на какие соглашения. Выбор между явным и неявным курсорами Все последние годы знатоки Oracle (включая и авторов данной книги) убежденно доказывали, что для однострочной выборки данных никогда не следует использо- использовать неявные курсоры, мотивируя это тем, что неявные курсоры, соответствуя стандарту ANSI, всегда выполняют две выборки, из-за чего они менее эффектив- эффективны, чем явные курсоры. Так утверждалось и в первых двух изданиях этой книги, но пришло время на- нарушить сложившуюся традицию. Начиная с OracleB выполнение запросов в РСУБД было оптимизировано, и теперь неявный курсор выполняется даже эффективнее эквивалентного явного курсора. Означает ли это, что теперь лучше всегда пользоваться неявными курсорами? Вовсе нет. По-прежнему имеются убедительные доводы в пользу применения яв- явных курсоров. О В некоторых случаях явные курсоры эффективнее неявных. Часто выполняе- выполняемые критические запросы лучше всего протестировать в двух видах (с явным и неявным курсором), чтобы точно выяснить, как лучше выполнять каждый из них в каждом конкретном случае. О Явными курсорами проще управлять из программы. Например, если строка не найдена, Oracle не инициирует исключение, а просто вызывает завершение ис- исполняемого блока.
Работа с неявными курсорами 491 Поэтому мы предлагаем вместо формулировки «явный или неявный?» поста- поставить вопрос так: «инкапсулированный или открытый?» И ответ на него будет та- таким: всегда инкапсулируйте однострочные запросы, скрывая их за интерфейсом функции и возвращая результирующие данные с помощью предложения RETURN. Так что не стоит больше тратить время на обдумывание выбора между явными и неявными курсорами. Задумайтесь лучше о том, как усовершенствовать про- программный код, в котором многократно и в разных местах выполняются одни и те же однострочные запросы. Инкапсулируйте эти запросы в функции, причем луч- лучше всего в пакетные. Работа с неявными курсорами При каждом выполнении инструкции DML (INSERT, UPDATE или DELETE) или инст- инструкции SELECT INTO, возвращающей одну строку из таблицы базы данных в струк- структуру данных программы, PL/SQL автоматически создает для нее курсор. Курсор этого типа называется неявным, поскольку Oracle автоматически выполняет мно- многие связанные с ним операции, такие как выделение курсора, его открытие, вы- выборка строк и т. д. ПРИМЕЧАНИЕ- 0 DML-инструкциях, выполняемых с помощью неявных курсоров, рассказывается в главе 13. Данная глава посвящена только запросам. Неявный курсор — это инструкция SELECT, обладающая следующими характе- характеристиками. О Эта инструкция определяется в исполняемом разделе блока, а не в разделе объявлений, как явные курсоры. О В инструкции содержится предложение INTO (или BULK COLLECT INTO), которое является частью языка PL/SQL, а не SQL, и представляет собой механизм пе- пересылки данных из базы данных в локальные структуры данных PL/SQL. О Инструкцию SELECT не нужно открывать, выбирать из нее данные и закрывать; все эти операции осуществляются автоматически. Общая структура запроса, который выполняется с помощью неявного курсора, имеет следующий вид: SELECT список_стопбцов [BULK COLLECT] INTO cnncoK_nepeneHHUX_PL/SQL ¦¦.оставшаяся часть инструкции SELECT... Итак, если в программе используется неявный курсор, Oracle автоматически открывает его, выбирает из него строки и закрывает; над этими операциями про- программист не властен. Однако он может получить информацию о последней вы- выполненной SQL-инструкции, проанализировав значения атрибутов неявного кур- курсора SQL, как рассказывается далее в этой главе.
492 Глава 14 • Выборка данных ПРИМЕЧАНИЕ В следующих разделах, говоря о неявном курсоре, мы будем иметь в виду инструкцию SELECT INTO, извлекающую (или пытающуюся извлечь) одну строку данных. Ниже мы обсудим ее разновидность SELECT BULK COLLECT INTO, позволяющую с помощью одного неявного запроса получить несколько строк данных. Примеры неявных курсоров Неявные курсоры часто используются для поиска данных на основе значений первичного ключа. В следующем примере выполняется поиск названия книги по ее коду ISBN: DECLAflE 1 title books.mieSTYPE: BEGIN SELECT title INTO ljitle FROM books WHERE isbn - '0-596-00121-5': END: После того как название книги выбрано и присвоено значению локальной пе- переменной l_title, с последней можно работать, как с любой другой переменной: изменять ее значение, выводить на экран или передавать для обработки другой программе PL/SQL. Вот пример неявного курсора, извлекающего из таблицы строку данных и по- помещающего ее в запись программы: DECLARE T_book booksSJROWTYPE: BEGIN SELECT * INTO l_t»Ok FROM books WHERE isbn - '0-596-00121-5'; END: Из запроса можно получить информацию уровня группы. Например, следую- следующий запрос вычисляет и возвращает сумму окладов по отделу. И снова PL/SQL создает для него неявный курсор: SELECT SUM (salary) INTO departmentjtotal FROM employee WHERE departmentjiumber = 10: Благодаря тесной интеграции PL/SQL с базой данных Oracle с помощью за- запросов из базы данных можно извлекать и сложные типы данных, такие как объек- объекты и коллекции. Однако для выборки из базы данных нескольких строк нужно либо использо- использовать явный курсор, либо включить в запрос предложение BULK COLLECT INTO (под- (поддерживаемое только Oracle8i и более поздними версиями). Оба подхода обсужда- обсуждаются далее в этой главе.
Работа с неявными курсорами 493 ПРИМЕЧАНИЕ- — — Как уже упоминалось, однострочные запросы лучше всегда инкапсулировать в функции. Этот во- вопрос подробно обсуждался в разделе «Выбор между явным и неявным курсорами». Обработка ошибок, связанных с неявными курсорами Инструкция SELECT, выполняемая как неявный курсор, представляет собой «чер- «черный ящик». Она передается в базу данных и возвращает одну строку информа- информации. Что происходит с курсором, как он открывается, выполняет выборку данных и закрывается, вы не знаете. Зато оказывается, что в двух случаях при выполне- выполнении инструкции SELECT Oracle инициирует исключения. О По сформированному программистом запросу не найдено ни одной строки данных, соответствующей заданному критерию. В этом случае Oracle иниции- инициирует исключение NO_DATA_FOUND. О Инструкция SELECT вернула несколько строк. Следовательно, Oracle иниции- инициирует исключение TOO_MANY_ROWS. В каждом из этих случаев (так же как и в случае любого другого исключения, инициированного при выполнении SQL-инструкции), выполнение текущего бло- блока прерывается и управление передается в раздел исключений. Этим процессом вы не управляете; у вас даже нет возможности сообщить Oracle, что данный за- запрос может не вернуть ни одной строки, и это не является ошибкой. Так что, если вы используете неявный курсор, в раздел исключений нужно обязательно вклю- включить код, перехватывающий и обрабатывающий эти два исключения (и, возмож- возможно, другие исключения, как того требует логика программы). В следующем блоке кода производится поиск названия книги по ее коду ISBN и обрабатываются возможные исключения: DECLARE l_isbn books.isbnmPE :- '0-596-00121-5': I_t1tle books.title*TYPE: BEGIN SELECT title INTO I_t1tle FROM books WHERE isbn - ljsbn; EXCEPTION WHEN N0_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE ('Неизвестная книга: ' || ljsbn): WHEN TOO_MANY_ROWS THEN errpkg. recNstop С Нарушение целостности данных для: ' || ljsbn); RAISE: END: Работая с неявным курсором, программист может сделать некоторые небезо- небезопасные предположения об извлекаемых данных. Приведем пару примеров. О В таблице не может быть более одной книги с указанным ISBN — ведь это га- гарантируется заданным для таблицы ограничением.
494 Глава 14 • Выборка данных О В таблице обязательно имеется строка с данными о содержащейся в ней книге. Поэтому не стоит задумываться об исключении NO_DATA_FOUND. Последствия таких предположений часто плачевны, поскольку, опираясь на них, программисты не включают в программы соответствующие обработчики ис- исключений. Конечно, в тот момент, когда вы работаете с таблицей и точно знаете, какие данные она содержит, ваш код может быть благополучно выполнен и запрос вер- вернет в точности одну строку. Однако если данные изменятся, запрос может вер- вернуть, к примеру, две строки вместо одной, и тогда программа сгенерирует исклю- исключение. В этом случае отсутствие обработчика исключения может поставить под удар все приложение. Как правило, при использовании неявных курсоров лучше всегда включать в программу обработчики исключений NO_DATA_FOUND и TOO_MANV_ROWS. В обобщен- обобщенной формулировке это правило звучит так: в программе должны присутствовать обработчики всех исключений, которые в ней могут произойти. А вот действия, выполняемые этими обработчиками, могут быть самыми разными. Возьмем все тот же код, извлекающий название книги по заданному коду ISBN. Ниже он реа- реализован в виде функции с двумя обработчиками ошибок. Обработчик NODA- TA_FOUND возвращает значение, а обработчик TOO_MANY_ROWS записывает ошибку в жур- журнал и повторно инициирует исключение, вызывая завершение работы функции. (О пакете errpkg подробно рассказывается в главе 6.) CREATE OR REPLACE FUNCTION book_title ( ljsbn IN books.isbnSTYPE) RETURN books.titleCTYPE IS return_value books.titleXTYPE; BEGIN SELECT title INTO return_value FROM books WHERE 1sbn - ljsbn; RETURN return_value; EXCEPTION WHEN NO_OATA_FOL)NO THEN RETURN NULL; WHEN TOO_MANY_ROWS THEN errpkg.recNstop ('Нарушение целостности данных для: ' II ljsbn); RAISE: END; Почему же эти два обработчика ведут себя так по-разному? Дело в том, что функция должна вернуть название книги, которое не может быть представлено значением NULL. Она использует для проверки ISBN («существует ли книга с дан- данным ISBN?»-), и поэтому возможна ситуация когда, не найдя заданной книги, она не станет инициировать исключение. В этом факте нет ничего «плохого». Например,
Работа с неявными курсорами 495 логика программы может быть такой: «если книги с данным ISBN не существует, используем его для новой книги*. Вот как она программируется: IF book_title('0-596-001Z1-7') IS NULL THEN... Иными словами, то, что запрос не выбрал ни одной строки, не всегда является свидетельством ошибки, и, в частности, функция не должна интерпретировать это как ошибку. С другой стороны, если запрос сгенерировал исключение TOO_MANY_ROWS, для нашей программы это действительно ошибка, поскольку не может существовать двух книг с одинаковыми ISBN. Поэтому в данном случае информация об ошиб- ошибке записывается в журнал и программа прекращает свою работу. Атрибуты неявных курсоров Oracle позволяет получить информацию о последнем запросе, выполненном с по- помощью неявного курсора, для чего программисту предоставляются четыре атри- атрибута, описанных в табл. 14.2. Поскольку неявный курсор не имеет имени, для его обозначения используется ключевое слово SQL. Таблица 14.2. Атрибуты неявных курсоров Имя Что возвращает SQL%FOUND Значение TRUE, если инструкция успешно выбрала одну строку (или несколько строк, при условии, что выполнялся запрос с предложением BULK COLLECT INTO) SQL%NOTFOUND Значение TRUE, если инструкция не выбрала ни одной строки (в этом случае Oracle также инициирует исключение NO_DATA_FOUND) SQL%ROWCOUNT Количество выбранных строк SQL%ISOPEN Значение FALSE, поскольку Oracle закрывает и открывает неявные курсоры автоматически Эти атрибуты курсора возвращают информацию о выполнении инструкций INSERT, UPDATE, DELETE и SELECT INTO. Если в текущем сеансе не выполнялся ни один запрос на основе неявного курсора, все атрибуты возвращают NULL. В противном случае значения атрибутов всегда относятся к последней SQL-инструкции неза- независимо от того, из какого блока или программы она выполнялась. Давайте обсудим следствия последнего утверждения. Рассмотрим следующие две программы: CREATE OR REPLACE PROCEDURE rerave_from_circulation (isbn in in books.isbnKTYPE) IS BEGIN DELETE FROM books WHERE isbn - isbn_in; END; / CREATE OR REPLACE PROCEDURE show book count
496 Глава 14 • Выборка данных IS 1_count INTEGER; BEGIN SELECT COUNT (*) INTO 1_count FROM books; -- Такой книги нет! remove_from_circulation CO-000-ODOOD-O'): D8MS_OUTPUT.PUT_LINE (SQLSROWCOUNT); END: Независимо от количества строк данных в таблице books результат всегда бу- будет равным 0. Поскольку после инструкции SELECT INTO вызывается процедура remove_from_caldilation, атрибут SQLXROWCOUNT возвращает количество строк, уда- удаленных выполняемой в этой процедуре инструкцией DELETE, а вовсе не количест- количество строк, возвращенных запросом. Если вам нужна гарантия того, что значения атрибутов относятся к заданной SQL-инструкции, сохраняйте зти атрибуты в локальных переменных сразу после ее выполнения — как в следующем примере: CREATE OR REPLACE PROCEDURE show_book_count IS l_count INTEGER: l_foundsome INTEGER; BEGIN SELECT COUNT (.*} INTO l_count FROM books; -- Делаем копив значения атрибута: l_foundsome :- SQLSROWCOUNT: -- Такой книги нет! remove_frora_cirailation ('0-000-00000-0'); -- -- Теперь можно вернуться к предыдущему значению атрибута DBMSJXJTPUT.putJine (l_foundsome): END: Теперь рассмотрим особенности атрибутов неявных курсоров. О SQL^FOUND. Этот атрибут позволяет узнать, верну-л ли запрос строку. Если най- найдена хотя бы одна строка, атрибут SQLXFOUND возвращает TRUE; в противном слу- случае он возвращает FALSE. О SQL35NOTFOUND. Значение атрибута SQLSNOTFOUND прямо противоположно преды- предыдущему: он возвращает TRUE, если не найдена ни одна строка (напомним, что в этом случае PL/SQL к тому же инициирует исключение NO_DATA_FOUND), и FAL- FALSE в противном случае.
Работа с явными курсорами 497 О SQUISOPEN. Для неявных курсоров данный атрибут не возвращает никаких по- полезных сведений. Его значением всегда является FALSE, поскольку Oracle авто- автоматически закрывает неявный курсор еще до того, как вы обратитесь к атрибу- атрибуту SQL2IS0PEN. О SQL31ROWCOUNT С помощью SQLfcROWCOUNT можно определить количество строк, воз- возвращенных инструкцией SELECT INTO. Данный атрибут возвращает 1, если в ин- инструкции нет предложения BULK COLLECT INTO, либо 0, если не найдена ни одна строка. В последнем случае PL/SQL инициирует исключение NO_DATA_FOl!ND. ПРИМЕЧАНИЕ Атрибут SQL%ROWCOUNT возвращает 1 даже в том случае, если в переменную помещается значе- значение, вычисленное на основе данных нескольких строк. Например, после выполнения инструкции: SELECT COUNT(*) INTO l_count FROM dual WHERE 1=2; атрибут SQL%ROWCOUNT будет установлен в 1, поскольку переменной l_count присвоено значе- значение 0. Работа с явными курсорами Явный курсор — это инструкция SELECT, явно определенная в разделе объявлений программы. При объявлении явного курсора ему присваивается имя. Для инст- инструкций UPDATE, DELETE и INSERT явные курсоры создавать нельзя. Определив инструкцию SELECT как явный курсор, программист получает кон- контроль над основными стадиями выборки информации из базы данных. Он опре- определяет, когда открыть курсор с помощью оператора OPEN, когда выбрать из него строки с помощью инструкции FETCH (то есть извлечь данные из таблицы или таб- таблиц, заданных в инструкции SELECT), сколько выбрать строк и когда закрыть кур- курсор с помощью оператора CLOSE. Информация о текущем состоянии курсора дос- доступна через его атрибуты. Рассмотрим пример. Следующая функция определяет (и возвращает) степень зависти, испытываемой автором книги к своим друзьям в зависимости от их ме- местожительства: 1 CREATE OR REPLACE FUNCTION jealousyjevel ( 2 NAMEJN IN friends.NAMEmPE) RETURN NUMBER 3 AS 4 CURSOR jealousy_cur 5 IS 6 SELECT location FROM friends 7 WHERE NAME - UPPER (NAMEJN); 8 9 jealousy_rec jealousy_curSROWTYPE: 10 retval NUMBER; И BEGIN 12 OPEN jealousy cur; 13 14 FETCH jealousy cur INTO jealousy rec 15 16 IF Jealousy_curSFOUND 17 THEN
498 Глава 14 • Выборка данных 18 IF jealousy_rec.location - 'PUERTO RICO' 19 THEN retval :- 10: 20 ELSIF jealousy_rec.location = 'CHICAGO' 21 THEN retval .:- 1: 22 END IF: 23 END IF; 24 25 CLOSE emptyp_cur; 26 27 RETURN level_out: 28 END: Этот блок PL/SQL выполняет следующие операции с курсором: Строки Действие 4-7 Объявление курсора 9 Объявление записи на основе курсора 12 Открытие курсора 14 Выборка одной строки из курсора 16 Проверка атрибута курсора с целью узнать, найдена ли строка 18-22 Анализ содержимого выбранной строки для вычисления степени зависти 25 Закрытие курсора В следующих разделах подробно рассматривается каждая из перечисленных операций. Слово «курсор» в них относится к явному курсору, если только не ука- указано обратное. Объявление явного курсора Для того чтобы получить возможность использовать явный курсор, его нужно объявить в разделе объявлений блока PL/SQL или пакета: CURSOR имя^курсора [(параметр [.параметр ...])] [RETURN спецификация_return] IS mcTpyxi)№_SELECT [FOR UPDATE [OF список_столбцов]]; Здесь иня_курсора — это имя объявляемого курсора; спецификацияjreturn — необя- необязательное предложение RETURN; инструкция_SELECT - любая допустимая SQL-инст- SQL-инструкция SELECT. Курсору могут передаваться параметры, о чем рассказывается далее, в разделе «Параметры курсора». После объявления курсора его можно открыть с помощью оператора OPEN и выбрать из него строки с помощью инструкции FETCH. Вот несколько примеров объявлений явных курсоров. О Курсор без параметров. Результирующим набором строк этого курсора явля- является набор идентификаторов компаний, выбранных из всех строк таблицы: CURSOR company_cur IS SELECT company_id FROM company:
Работа с явными курсорами 499 О Курсор с параметрами. Результирующий набор строк этого курсора содер- содержит единственную строку с именем компании, соответствующим идентифи- идентификатору компании, переданному курсору в качестве параметра: CURSOR name_cur (companyjdjn IN NUMBER) IS SELECT name FROM company WHERE companyjd - companyjdjn; О Курсор с предложением RETURN. Результирующий набор строк этого кур- курсора содержит все данные таблицы employee для подразделения с идентифика- идентификатором 10: CURSOR emp cur RETURN employeeXROWTYPE IS SELECT * FROM employee WHERE departmentald = 10: Имя курсора Имя явного курсора должно иметь длину до 30 символов и соответствовать тем же правилам, которым соответствуют все остальные идентификаторы PL/SQL. Однако курсор не является переменной. Это идентификатор указателя на запрос. Имени курсора не присваивается значение и его нельзя применять в выражениях. Курсор используется только в операторах OPEN, CLOSE и инструкции FETCH, а также в качестве уточнителя для атрибутов курсора. Объявление курсора в пакете Явные курсоры объявляются в разделе объявлений блока PL/SQL. Можно объя- объявить курсор на уровне пакета, но не в конкретной процедуре или функции пакета. О пакетах подробно рассказывается в главе 17. Перед чтением этого раздела про- просмотрите главу 17, чтобы составить общее представление о том, что такое пакеты и какова их структура. Вот пример объявления двух курсоров в пакете: CREATE OR REPLACE PACKAGE book_info IS CURSOR titles_cur IS SELECT title FROM books: CURSOR books_cur (t1tle_f1lter_1n IN books.t1tle*TYPE) RETURN books*ROWTYPE IS SELECT * FROM books WHERE title LIKE title_filter_in; END; Первый курсор, titles_cur, возвращает только названия книг. Второй, books_ cur, возвращает все строки таблицы books, в которых названия книг соответству- соответствуют шаблону, заданному в качестве параметра курсора (например, «Все книги, со- содержащие 'PL/SQLV). Обратите внимание, что во втором курсоре используется
500 Глава 14 • Выборка данных предложение RETURN, объявляющее структуру данных, возвращаемую инструкци- инструкцией FETCH. В предложении RETURN могут быть указаны: О запись, объявленная на основе строки таблицы базы данных с помощью атри- атрибута XROLJTYPE; О запись, объявленная на основе другого, ранее объявленного курсора, также с помощью атрибута IROWTYPE; О запись, определенная программистом. Количество выражений в списке выборки курсора должно соответствовать количеству столбцов записи, указанной в предложении RETURN в виде имя_тдбли- «b$ROWTYPE, /<ypcopXROWTYPE или тип_записи. Например, если второй элемент списка выборки имеет тип NUMBER, второй столбец записи в предложении RETURN не может иметь тип VARCHAR2 или BOOLEAN. Прежде чем перейти к подробному рассмотрению предложения RETURN и его преимуществ, давайте сначала ответим на такой вопрос: для чего вообще может понадобиться объявлять курсоры в пакете? Почему не объявить явный курсор в той программе, в которой он используется — в процедуре, функции или ано- анонимном блоке? Ответ прост. Определив курсор в пакете, можно многократно использовать за- заданный в нем запрос, не повторяя один и тот же код снова и снова в разных мес- местах приложения. Реализовав запрос в одном месте, его легко модифицировать в случае необходимости, что значительно облегчает разработку и сопровождение приложения. Кроме того, экономится время на синтаксическом анализе запросов. 1РИМЕЧАНИЕ- Объявляя курсор в пакете для многократного использования, нужно иметь в виду следующий важ- важный фактор. Структуры данных, определяемые на уровне пакета (а не внутри конкретной функции или процедуры), включая и курсоры, сохраняют свои значения в течение всего сеанса. Это означа- означает, что объявленный в пакете курсор остается открытым до тех пор, пока вы его явно не закроете, или до окончания сеанса. Курсоры, объявленные в локальных блоках кода, закрываются автомати- автоматически по завершении работы таковых. Ну а теперь поговорим о предложении RETURN. Одной из интересных особенно- особенностей объявления курсора в пакете является возможность отделить заголовок кур- курсора от его тела. Такой заголовок, больше напоминающий заголовок функции, со- содержит информацию, которая необходима программисту для работы: имя курсора, его параметры и тип возвращаемых данных. Телом курсора служит инструкция SELECT. Вот новая версия объявления курсора books_cur в пакете bookjnfo, иллюстри- иллюстрирующая эту технологию: CREATE OR REPLACE PACKAGE bookjnfo IS CURSOR books_cur (title filterjin IN books.titieXTYPE) RETURN booksXROWTYPE" END;
Работа с явными курсорами 501 CREATE OR REPLACE PACKAGE BODY bookjnfo IS CURSOR books_cur (title_filterjri IN books.titleJTTYPE) RETURN booksJSROWTm IS SELECT * FROM books WHERE title LIKE title_f1lter_in: END: Такое разделение объявления курсора может служить двум целям. О Сокрытие информации. Курсор в пакете является «черным ящиком». Это удобно для программистов, поскольку им не нужно ни писать, ни даже видеть инструкцию SELECT. Достаточно только знать, какие записи возвращает этот курсор, в каком порядке и какие столбцы они содержат. Программист, рабо- работающий с пакетом, использует курсор в приложении как любой другой предо- предопределенный элемент. О Минимизация перекомпиляций. Если скрыть определение запроса в теле па- пакета, изменения в инструкцию SELECT можно вносить, не меняя заголовок кур- курсора в спецификации пакета. Это позволяет совершенствовать, исправлять и повторно компилировать код без перекомпиляции спецификации пакета, бла- благодаря чему зависящие от этого пакета программы не будут помечены как не- недействительные и их также не нужно будет перекомпилировать. Открытие явного курсора Объявив курсор в разделе объявлений, его нужно открыть. Синтаксис оператора OPEN очень прост: OPEN имя_курсорд {{аргумент [. аргумент ...])]: Здесь иня_курсора — это имя объявленного ранее курсора, а аргумент — это значе- значение, передаваемое курсору, если он объявлен со списком параметров. ПРИМЕЧАНИЕ Oracle поддерживает еще одну разновидность синтаксиса открытия курсора: OPEN имя_нурсорд FOR Он используется для курсорных переменных и встроенного динамического SQL (см. главу 15). Открывая курсор, PL/SQL выполняет содержащийся в нем запрос. Кроме того, он идентифицирует активный набор данных — строки всех участвующих в запро- запросе таблиц, соответствующих критерию предложения WHERE и условию объедине- объединения. Оператор OPEN не извлекает данные — это задача инструкции FETCH. Независимо от того, когда будет выполнена первая выборка данных, реализо- реализованная в Oracle модель согласованности чтения гарантирует, что все операции выборки будут возвращать данные в том состоянии, в каком они находились на момент открытия курсора. Иными словами, от открытия и до закрытия курсора при выборке из него данных полностью игнорируются выполняемые за это время операции вставки, обновления и удаления.
502 Глава 14 • Выборка данных Более того, если в инструкции SELECT используется предложение FOR UPDATE, все идентифицируемые курсором строки блокируются при его открытии. (Об этом подробнее рассказывается далее в этой главе, в разделе «Инструкция SELECT с предложением FOR UPDATE».) Если вы попытаетесь открыть уже открытый курсор, PL/SQL выдаст следую- следующее сообщение об ошибке: ORA-06511: PL/SQL: cursor already open Поэтому перед открытием курсора следует проверить его состояние, обратив- обратившись к атрибуту fclSOPEN: IF NOT company_cur*ISOPEN THEN OPEN company_cur; END IF: Атрибуты явных курсоров описываются ниже, в посвященном им разделе. Там рассказывается, как лучше всего использовать их в программах. ПРИМЕЧАНИЕ — Если в программе выполняется цикл FOR, использующий курсор, этот курсор не нуждается в явном открытии. Выборка данных из явного курсора Инструкция SELECT создает виртуальную таблицу - набор строк, определяемых предложением WHERE со столбцами, заданными в предложении SELECT. Курсор пред- представляет эту таблицу в программе PL/SQL. Основным назначением курсоров в программах PL/SQL является выборка строк для их обработки. Выборка строк курсора выполняется с помощью инструкции FETCH: FETCH имя_курсора INTO запись_или_список_переменных; Здесь имя_курсора — это имя курсора, из которого выбирается строка, а запись_или_ список_перененных — это структура или структуры данных PL/SQL, в которые ко- копируется следующая строка активного набора записей. Данные могут помещать- помещаться в запись PL/SQL (объявленную с помощью атрибута fcROWTYPE или оператора TYPE) или в переменные (неременные PL/SQL или переменные привязки, как на- например, в элементы Oracle Forms). Примеры явных курсоров Следующие примеры демонстрируют разные способы выборки данных. О Выборка данных из курсора в запись PL/SQL DECLARE CURSOR company_cur is SELECT ...: company rec company curSROWTYPE: BEGIN OPEN company_cur; FETCH company_cur INTO company_rec: О Выборка данных из курсора в переменную: FETCH new_balance сиг INTO new_balance dollars;
Работа с явными курсорами 503 О Выборка данных из курсора в строку таблицы PL/SQL, переменную и пере- переменную привязки Oracle Forms: FETCH ипр_пате_сиг INTO empjiame CD. hiredate. :dept.min_salary: ПРИМЕЧАНИЕ- Лучше всего помещать выбираемые из курсора данные в запись, объявленную на основе того же курсора с атрибутом %ROWTYPE; избегайте извлечения значений в переменные. Первый способ де- делает программный код более лаконичным и гибким, позволяет изменить список выборки, не меняя инструкцию FETCH. Выборка после обработки последней строки Открыв курсор, вы по очереди выбираете из него строки, пока все они не будут исчерпаны. Однако и после этого можно выполнять инструкцию FETCH. Как ни странно, в этом случае PL/SQL не инициирует исключение. Он просто ничего не делает. Поскольку выбирать больше нечего, значения переменных в пред- предложении INTO инструкции FETCH не меняются. Иными словами, инструкция FETCH не устанавливает значения этих переменных равными NULL. Поэтому, если вам понадобится узнать, успешно ли прошла выборка очеред- очередной строки, проверка переменных из списка INTO ничего не даст. Следует прове- проверить атрибут &FOUND или XNOTFOUND (о них рассказывается ниже, в разделе, посвя- посвященном атрибутам явных курсоров). Псевдонимы столбцов явного курсора Инструкция SELECT в объявлении курсора определяет список возвращаемых им столбцов. Наряду с именами столбцов таблиц этот список может содержать выра- выражения, называемые вычисляемыми, или виртуальными, столбцами. Псевдоним столбца — это альтернативное имя, указанное в инструкции SELECT для столбца или выражения. Задав подходящие псевдонимы в SQL*Plus, можно вывести результаты произвольного запроса в виде читабельного отчета. В этом случае псевдонимы не являются обязательными. Для явного курсора псевдони- псевдонимы вычисляемых столбцов необходимы в следующих случаях: О когда данные выбираются из курсора в запись, объявленную с атрибутом %\Ш- TYPE на основе того же курсора; О когда в программе имеется ссылка на вычисляемый столбец. Рассмотрим следующий запрос. Инструкция SELECT выбирает имена всех ком- компаний, заказывавших товары в течение 2001 года, а также общую сумму заказов (предполагается, что для текущего экземпляра базы данных по умолчанию ис- используется маска форматирования DD-MON-YYYY): SELECT companyjiame, SUM (inv_amt) FROM company C. invoice I WHERE C.company id = I.company_1d AND [.1nvoice~date BETWEEN 'Ol-JAN-2001' AND '31-DEC-20011;
504 Глава 14 ¦ Выборка данных Если выполнить эту инструкцию в SQL*Plus, результаты будут примерно та- такими: COMPANY NAME SUM (INV_AMT) ACME TURBO INC. 1000 WASHINGTON HAIR CO. 25.20 Как видите, заголовок столбца SUM (INVAMT) не слишком подходит для отчета, но для того чтобы просто просмотреть данные, он вполне годится. Теперь выпол- выполним тот же запрос в программе PL/SQL с помощью явного курсора и добавим псевдоним столбца: DECLARE CURSOR conp_cur IS SELECT company_name. SUM (inv_amt) total_sales FROM company C, invoice I WHERE C.company_id = I.company_id AND I.invoicejate BETWEEN 'Ol-JAN-20011 AND '31-DEC-2001'; comp_rec compjrurSlROWTYPE: BEGIN OPEN comp_cur; FETCH comp_cur INTO comp_rec: END: Теперь с вычисляемым столбцом можно работать точно так же, как с любым другим столбцом запроса: IF comp_rec.total_sa1ss > 5000 THEN DBMS_OUTPUT.PUT_LINE С'Вы превысили лимит кредита $5000 на' || TO_CHAR E000-comp_rec.total_sales. 'S9999')): END IF: Если поместить строку в запись, объявленную с атрибутом SROWTYPE, доступ к вы- вычисляемому столбцу можно будет получить только по имени - ведь структура записи формируется на основе структуры курсора. Закрытие явного курсора Когда-то в детстве нас учили прибирать за собой, и эта привычка осталась у нас (надеемся) на всю жизнь. Оказалось, что исключительно важную роль это прави- правило играет в программировании, и особенно когда дело доходит до управления курсорами. Никогда не забывайте закрыть курсор, если он вам больше не нужен! Вот синтаксис оператора CLOSE: CLOSE имякурсора: Ниже приводится несколько важных советов и соображений, связанных с за- закрытием явных курсоров. О Если курсор объявлен и открыт в процедуре, не забудьте его закрыть. Иначе считайте, что вы запрограммировали утечку памяти. Строго говоря, курсор (как и любая другая структура данных) должен автоматически закрываться
Работа с явными курсорами 505 и уничтожаться, как только выполнение приложение выходит за пределы его области действия. И, как правило, при выходе из процедуры, функции или анонимного блока PL/SQL действительно закрывает все открытые в нем кур- курсоры. Однако этот процесс связан с определенными издержками, и поэтому иногда PL/SQL выявляет и закрывает открытые курсоры не сразу. Кроме того, курсоры типа REF CURSOR по определению не могут быть закрыты неявно. Един- Единственное, в чем вы можете быть уверены, так это в том, что по завершении ра- работы «самого внешнего» блока PL/SQL, когда управление будет возвращено SQL или другой вызывающей программе, PL/SQL неявно закроет все откры- открытые этим блоком или вложенными блоками курсоры, кроме REF CURSOR. ПРИМЕЧАНИЕ Вложенные анонимные блоки могут служить примером того, как PL/SQL (по крайней мере, в Oracle9i Release 1) не выполняет явного закрытия курсора. Интересное обсуждение этой темы вы найдете в статье Джонатана Генника «Does PL/SQL Implicitly dose Cursors» по адресу http://gennick.com/ open_cursors.html. О Если курсор объявлен в пакете на уровне пакета и открыт в некотором блоке или программе, он останется открытым до тех пор, пока вы его явно не закроете, или до завершения сеанса. Поэтому, окончив работу с таким курсором, его сле- следует сразу же закрыть с помощью оператора CLOSE, как в следующем примере: BEGIN OPEN my_package.my_cursor; ... Работаен с курсором ... CLOSE my_package.my_cursor; EXCEPTION WHEN OTHERS THEN CLOSE my_package.my_ciirsor; END; О Очень важно как можно раньше закрывать курсоры с запросами SELECT FOR UPDATE, поскольку для этих запросов выполняется блокировка на уровне строк. О Курсор следует закрывать только в том случае, если он открыт. Состояние курсора проверяется с помощью атрибута fclSOPEN: IF company_cur*ISOPEN THEN CLOSE company_cur: END IF; О Если оставить открытыми слишком много курсоров, можно превысить значе- значение параметра базы данных OPEN_CURSORS. В этом случае появится сообщение об ошибке: ORA-01000: maximum open cursors exceeded Получив такое сообщение, прежде всего проверьте, как используются курсо- курсоры, объявленные в пакетах.
506 Глава 14 • Выборка данных Атрибуты явных курсоров Oracle поддерживает четыре атрибута («FOUND, *NOTFOUND, SROWCOUNT, *ISOPEN), воз- возвращающих информацию о состоянии явного курсора. Ссылка на атрибут имеет следующий синтаксис: курсорХдтрибут Здесь курсор — это имя объявленного курсора. Значения, возвращаемые атрибутами явных курсоров, приведены в табл. 14.3. Таблица 14.3. Атрибуты явных курсоров Имя Что возвращают Kypcop%FOUND Kypcop%NOTFOUND Kypcop%ROWCOUNT Kypcop%ISOPEN Значение TRUE, если строка выбрана успешно Значение TRUE, если не была выбрана ни одна строка Количество строк, выбранных из указанного курсора на данный момент времени Значение TRUE, если заданный курсор открыт Значения атрибутов курсоров до и после выполнения различных операций с ними указаны в табл. 14.4. Таблица 14.4. Возможные значения атрибутов курсоров ДО OPEN После OPEN До первой FETCH После первой FETCH Перед последующей FETCH После последующих FETCH Перед последней FETCH После последней FETCH Перед CLOSE После CLOSE %FOUND Исключение ORA-01001 NULL NULL TRUE TRUE TRUE TRUE FALSE FALSE Исключение o/oNOTFOUND Исключение ORA-01001 NULL NULL FALSE FALSE FALSE FALSE TRUE TRUE Исключение %ISOPEN FALSE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE FALSE %ROWCOUNT Исключение ORA-01001 0 0 1 1 Зависит от данных Зависит от данных Зависит от данных Зависит от данных Исключение Работая с атрибутами явных курсоров, имейте в виду следующее. О При попытке обратиться к атрибуту XFOUND, 2NOTF0UND или 2R0WC0UNT до откры- открытия курсора или после его закрытия Oracle инициирует исключение INVA- LID_CURSOR (ORA-01001). О Если после первого выполнения инструкции FETCH результирующий набор строк оказывается пустым, атрибуты курсора возвращают следующие значе- значения: «FOUND - FALSE, KNOTFOUND = TRUE и *ROWCOUNT = D.
Работа с явными курсорами 507 О Если используется предложение BULK COLLECT, атрибут 2ROWC0UNT может вер- вернуть значение, отличное от 0 или 1 — количество строк, извлеченных в задан- заданные коллекции. Подробнее об этом рассказывается в разделе «Предложение BULK COLLECT». Использование всех этих атрибутов демонстрирует следующий пример: CREATE OR REPLACE PACKAGE bookinfopkg IS CURSOR bard_cur IS SELECT title. date_published FROM books WHERE UPPERCauthor) LIKE 'SHAKESPEARE*'; rec bard_curtrowtype; END bookinfo_pkg: / DECLARE bard_rec booki nfo_pkg. bard_curSROWTYPE; BEGIN -- Проверяем, не открыт ли уже курсор. -- Это возможно, поскольку курсор определен в пакете. -- Поэтому закрываем его и открываем повторно, чтобы -- получить "свежий" результирующий набор IF booki nfo_pkg.bard_cur«ISOPEN THEN CLOSE bookinfo_pkg.bard_cur; END IF: OPEN bookinfoj)kg.bard_cur; -- По очереди выбираем строки, но останавливаемся -- после вывода первых пяти произведений Шекспира -- или когда будут исчерпаны все строки LOOP FETCH boofcinfo_pkg.bard_cur INTO bookinfo_pkg.rec; EXIT WHEN bookinfoj>kg.bard_curSNOTFOUND OR bookinfo_pkg.bard_curOTWCOUNT - 6: DBMSJWTPUT.putJine ( booki nfo_pkg.bard_cur*ROWCOUNT II'I || bookinfo_pkg.rec.title |j'. издана в ' jj TO_CHAR Cbookinfo_pkg.rec.date_published. 'YYYY') ): END LOOP: CLOSE bookinfo_pkg.bard cur; END; Параметры курсора Вы, конечно, уже знакомы с понятием параметров процедур и функций. Парамет- Параметры - это средство передачи информации в программный модуль и из него. При правильном использовании они делают модули более полезными и гибкими.
508 Глава 14 • Выборка данных PL/SQL позволяет передавать параметры курсорам. Они выполняют те же функции, что и параметры программных модулей, а также несколько дополни- дополнительных. О Расширение возможности многократного использования курсоров. Вместо того чтобы жестко кодировать в предложении WHERE значения, определяющие условие отбора данных, можно использовать параметры для передачи в это предложение нового набора значений при каждом открытии курсора. О Решение проблем, связанных с областью действия курсоров. Если вместо жестко закодированных значений в запросе используются параметры, резуль- результирующий набор строк курсора не привязан к конкретной переменной про- программы или блока. Если в программе имеются вложенные блоки, курсор мож- можно определить на верхнем уровне и использовать его во вложенных блоках с объявленными в них переменными. Количество параметров курсора не ограничено. В операторе OPEN для каждого параметра должно быть задано значение, за исключением параметров, для кото- которых определены значения по умолчанию. В каких случаях курсору требуются параметры? Общее правило здесь то же, что и для процедур и функций: если предполагается, что курсор будет использо- использоваться в разных местах и с разными значениями в предложении WHERE, значит, ему нужны параметры. Давайте обсудим различия между курсорами с параметрами и курсорами без параметров. Вот пример курсора без параметров: CURSOR joke_cur IS SELECT name, category. 1ast_used_date FROM joke: Он возвращает все строки таблицы joke. Если же нам нужны только строки, соответствующие категории HUSBAND, в запрос следует добавить предложение WHERE: CURSOR joke_cur IS SELECT name, category, last_used_date FROM joke WHERE category - 'HUSBAND'; Для выполнения этой задачи мы не стали использовать параметры, да они и не нужны. В данном случае курсор возвращает все строки, относящиеся к кон- конкретной категории. Ну а как быть, если при каждом обращении к этому курсору категория изменяется? Курсоры с параметрами Конечно же, мы не станем определять отдельный курсор для каждой из катего- категорий — это совершенно не согласуется с принципом разработки приложений, управляемых данными. Нам нужен один-единственный курсор, но такой, для ко- которого можно было бы менять категорию, но он все равно возвращал бы требуе- требуемую информацию. И лучшим (хотя и не единственным) способом решения подоб- подобной задачи является создание курсора с параметрами: DECLARE /* || Курсор со списком параметров, состоящим из единственного строкового параметра */
Работа с явными курсорами 509 CURSOR joke_cur (category in VARCHAR2) IS SELECT name, category. 1ast_used_date FROM joke WHERE category - UPPER (categoryjn); jokej-ec joke_curtROWTYPE; BEGIN /* Теперь, открывая курсор, мы передаем ему аргунент */ OPEN joke_cur t:joke.category): FETCH jokecur INTO Jokerec: Между именем курсора и ключевым словом IS теперь содержится список па- параметров. Жестко закодированное значение HUSBAND удалено из предложения WHE- WHERE, а вместо него появилась ссылка на параметр — UPPER(category_i п). При откры- открытии курсора можно будет задавать значения HUSBAND, husband или HuSbAnD, но он все равно будет работать. Название категории, для которой наш курсор должен вер- вернуть строки таблицы joke, задается в операторе OPEN (в скобках) в виде литерала, константы или выражения. При открытии курсора выполняется его синтаксиче- синтаксический анализ и связывание со значением параметра category_i n. Затем идентифи- идентифицируется результирующий набор строк — и курсор готов к выборке. Открытие курсора с параметрами Итак, новый курсор joke_cur можно открывать с указанием любой категории: OPEN joke_cur t:jokes_pkg.category): OPEN joke_oir ('Husband'); OPEN joke_cur ('politician1): OPEN joke_cur (:jokes_pkg.relation || 'IN-LAW'); Параметры курсора чаще всего используются в предложении WHERE, но ссы- ссылаться на них можно и в других местах инструкции SELECT, как показано в сле- следующем примере: DECLARE CURSOR joke_cur (categoryjn VARCHAR2) IS SELECT name. category_1n, last_used_date FROM joke WHERE category - UPPER (categoryjn): Вместо того чтобы считывать категорию из таблицы, мы поместили параметр category_i n в список выборки, благодаря чему будет возвращено значение этого параметра. Область действия параметров курсора Область действия параметров курсора ограничивается самим курсором. Вне ин- инструкции SELECT, ассоциированной с курсором, на них ссылаться нельзя. Напри- Например, следующий фрагмент кода PL/SQL не будет откомпилирован, поскольку идентификатор programjiame не является локальной переменной программы. Это формальный параметр, определенный только внутри курсора: DECLARE CURSOR scariness_cur (program_name VARCHAR2)
510 Глава 14 • Выборка данных IS SELECT SUM (scaryjevel) t.otal_scaryjevel FROM tales_from_the_crypt WHERE prog_name - programjiame: BEGIN program_name :- 'THE BREATHING MUMMY': /* недопустимо */ ?PEN scariness_cur (programjiame); END: Режимы параметров курсора Синтаксис параметров курсора очень похож на синтаксис процедур и функций с той разницей, что параметры курсоров могут быть только входными — специ- спецификации OUT и IN OUT для них не допускаются. Информацию, извлекаемую из базы данных с помощью курсора, можно передать программе PL/SQL только пу- путем выборки значений из курсора в переменные, записи или коллекции, заданные в предложении INTO инструкции FETCH. (О режимах использования параметров подробно рассказывается в главе 16.) Значения параметров по умолчанию Параметрам курсора могут присваиваться значения по умолчанию. Вот пример курсора со значением параметра по умолчанию: CURSOR emp_cur (empjdjn NUMBER :- 0) IS SELECT employee_id, emp_name FROM employee WHERE employeejid - emp_id_in: Если сотрудника с идентификатором 1001 зовут Joe Smith, следующий код присвоит переменной my_emp_id значение 1001, а переменной my_emp_name - значе- значение JOE SMITH: OPEN emp_cur C10D1): FETCH emp_cur INTO my_empjd. тцу_етр_пате: Поскольку для параметра emp_id_in определено значение по умолчанию, в ин- инструкции FETCH его значение можно не задавать. Курсор же вернет информацию о сотруднике с кодом 0, так как именно 0 в данном случае является значением по умолчанию. Предложение BULK COLLECT В Oracle8i был введен новый структурный компонент, значительно повысивший производительность запросов в PL/SQL, — предложение BULK COLLECT. С его по- помощью можно за одно обращение к базе данных извлечь из явного или неявного курсора несколько строк данных. Выборка данных с помощью запроса с предложе- предложением BULK COLLECT сокращает количество переключений контекста между PL/SQL и SQL, благодаря чему работа выполняется быстрее и с меньшими издержками. Рассмотрим следующий фрагмент кода. Нам нужно прочитать сотни строк данных об автомобилях, загрязняющих окружающую среду. Эти данные следует
Предложение BULK COLLECT 511 поместить в набор коллекций, благодаря чему проводить их дальнейший анализ и обработку станет намного удобнее. DECLARE CURSOR major_polluters_cur IS SELECT name, mileage FROM transportation WHERE TYPE - 'AUTOMOBILE' AND mileage < 20: names name_varray: mileages numberj/array; BEGIN FOR bad_car IN major_polluters LOOP names.EXTEND; names (majorjjollutersSROWCOUNT) := bad_car.NAME; mileages.EXTEND: mileages {majorjrallutersSKQWCOUNT) :- badca r mileage ; END LOOP: -- Далее можно работать с данными в коллекциях END; Этот код выполняет поставленную задачу, но далеко не самым эффективным способом. Если, к примеру, в таблице transportation содержится 2000 строк, PL/SQL придется произвести 2000 операций выборки данных из глобальной сис- системной области. Значительному ускорению работы может благоприятствовать предложение BULK COLLECT. Включив его в курсор (явный или неявный), вы указываете ядру на необходимость SQL перед передачей управления PL/SQL выполнить пакетное связывание выходных данных из множества строк с заданными в запросе коллек- коллекциями. Синтаксис этого предложения таков: ... BULK COLLECT INTO иня_коплекции[. имя_коллекции] ... Здесь параметр имя_коллекции идентифицирует коллекцию, в которую должны быть помещены выходные данные курсора. Правда, существует несколько правил и ограничений, связанных с использо- использованием предложения BULK COLLECT. О Ранее предложение BULK COLLECT можно было использовать только в статиче- статических SQL-инструкциях. Начиная с Oracle9i оно применяется и в динамиче- динамическом SQL. Более подробные сведения по этому вопросу и примеры приведены в главе 15. О Ключевые слова BULK COLLECT можно использовать только в предложениях SE- SELECT INTO, FETCH INTO и RETURN INTO. О В коллекциях, заданных в предложении BULK COLLECT, могут храниться только скалярные значения (строки, числа, даты). Иными словами, строки данных нельзя поместить в записи, являющиеся элементами коллекции. (В Oracle9i Release 2 такая возможность появилась.) О Ядро SQL автоматически инициализирует и расширяет коллекции, заданные в предложении BULK COLLECT. Их заполнение начинается с индекса 1, и далее
512 Глава 14 • Выборка данных элементы добавляются последовательно (без пропусков), заменяя определен- определенные ранее элементы. Q Инструкцию SELECT...BULK COLLECT нельзя использовать в операторе FORALL. Рассмотрим эти правила на примерах. Вот новая версия предыдущего приме- примера, усовершенствованная с помощью предложения BULK COLLECT: DECLARE names name_varray: mileages number_varray; BEGIN SELECT name, mileage FROM transportation BULK COLLECT INTO names, mileages WHERE TYPE - 'AUTOMOBILE' AND mileage < 20: -- Далее можно работать с данными в коллекциях END; Мы не только добавили предложение BULK COLLECT, но и удалили из программы код, выполняющий инициализацию и расширение коллекций. А теперь перепи- перепишем этот пример еще раз с использованием явного курсора: DECLARE CURSOR major_pollliters IS SELECT name, mileage FROM transportation WHERE TYPE - 'AUTOMOBILE' AND mileage < 20; names name_varray; mileages number_varray; BEGIN OPEN major_polluters; FETCH major_polluters BULK COLLECT INTO names, mileages: -- Далее можно работать с данными в коллекциях END: Ограничение на количество строк, возвращаемых инструкцией с предложением BULK COLLECT Для предложения BULK COLLECT Oracle поддерживает сопутствующее предложе- предложение LIMIT, ограничивающее количество строк, выбираемых из базы данных. Его синтаксис таков: FETCH ... BULK COLLECT INTO ... [LIMIT строки]: Здесь строки — это литерал, переменная или выражение, возвращающее значение типа NUMBER (иначе Oracle инициирует исключение VALUE_ERROR). Теперь рассмотрим такой пример. Предположим, что таблица transportation очень велика, а мы хотим получать за одну выборку не более 5000 строк. В этом случае в инструкцию FETCH можно добавить предложение LIMIT: DECLARE CURSOR major_po11uters IS SELECT name FROM transportation
Предложение BULK COLLECT S13 WHERE TYPE - 'AUTOMOBILE' AND mileage < 20; names name_varray: BEGIN OPEN major_polluters: FETCH major polluters BULK COLLECT INTO names LIMIT 5000: END: Выборка более одного столбца Как показывают приведенные выше примеры, предложение BULK COLLECT позволя- позволяет выбрать из курсора данные нескольких столбцов и поместить их в несколько отдельных коллекций. Без сомнения, более целесообразным решением было бы извлечение набора строк в одну коллекцию записей. Такая возможность появи- появилась в Oracle9i Release 2. Предположим, что нам нужно считать из таблицы transportation информацию обо всех автомобилях с расходом топлива менее галлона на 20 миль. В Огас1е9: Release 2 и последующих версиях РСУБД эту задачу можно выполнить самым оптимальным образом: DECLARE -- Обьявпяем коллекции на основе заданного типа. TYPE VehTab IS TABLE OF trans portationl&ROWTYPE; gas guzzlers VehTab; BEGIN SELECT * BULK COLLECT INTO gas_guzzlers FROM transportation WHERE mileage < 20; До Oracle9i Release 2 при выполнении этого кода было бы инициировано сле- следующее исключение: PLS-00597: expression 'GAS_GUZZLERS' in the INTO list is of wrong type В Oracle8i и Oracle9z Release 1 нам пришлось бы объявить несколько коллек- коллекций и поместить в них отдельные столбцы: DECLARE guzzlerjiype name_varray: guzzlerjiame name_varray; guzzlerjnileage number_varray; CURSOR lowjrileagejzur IS SELECT vehicle_type. name, mileage FROM transportation WHERE mileage < 10- BEGIN OPEN low_mileage_cur: FETCH lowjiileage_cur BULK COLLECT INTO guzzlertype. guzzler name, guzzler mileage: END;
514 Глава 14 • Выборка данных Не забывайте, что при извлечении данных из курсора в коллекцию записей также можно использовать предложение LIMIT, ограничивающее количество вы- выбираемых строк. Совместное использование предложений RETURNING и BULK COLLECT Вы уже знаете, как использовать предложение BULK COLLECT с явными и неявными курсорами. Но его можно использовать и в операторе FORALL совместно с предло- предложением RETURNING. Предложение RETURNING, введенное в Огас1е8, позволяет получить информацию из DML-инструкции (например, обновленное значение оклада), помогая обой- обойтись без дополнительных запросов к базе данных. Предположим, что в Конгрессе принят закон, требующий, чтобы оклад наибо- наиболее высокооплачиваемых сотрудников компании не превышал оклад ее низкооп- низкооплачиваемых сотрудников более чем в 50 раз. Мы работаем в ИТ-подразделении только что созданной компании Northrop-Ford- Mattel-Yahoo_ATT, общее число сотрудников которой составляет 250 000 человек. Поскольку руководство компа- . нии не намерено уменьшать доходы генерального директора, было решено повы- повысить оклады сотрудников, зарабатывающих менее одной пятидесятой части его общего дохода за 2004 год, составляющего 145 миллионов долларов, и урезать ок- оклады всего высшего руководства — должен же кто-то компенсировать такие ог- огромные расходы. Похоже, придется потрудиться, чтобы пересчитать и обновить такое количе- количество данных, а для ускорения процесса можно воспользоваться оператором FO- FORALL. После выполнения всех расчетов и внесения изменений нам нужно будет вывести отчет со старыми и новыми окладами сотрудников. Сформировать его нам поможет предложение RETURNING. Для начала напишем функцию, которая должна возвращать информацию о за- заработной плате генерального директора: /* Файл в web: nnlyfair.sql */ CREATE OR REPLACE FUNCTION salforexec (titlejn IN VARCHAR2) RETURN NUMBER IS CURSOR ceo_compensation IS SELECT salary + bonus + stock_options + mercedes_benz_allowance + yacht_allowance FROM compensation WHERE title - titlejn; big_bucks NUMBER; BEGIN OPEN ceo_compensation; FETCH ceo_compensation INTO bigjiucks; RETURN b1g_bucks: END;
Предложение BULK COLLECT 515 В главном блоке программы обновления мы объявим ряд локальных перемен- переменных и запрос для определения низкооплачиваемых и высокооплачиваемых со- сотрудников, чью зарплату придется пересмотреть: DECLARE big_bucks NUMBER :- salforexec CCEO'); min_sal NUMBER :- big_bucks / 50: names name_tab: oldsalaries number_tab; new_salaries number_tab: CURSOR affected_employees (ceosal IN NUMBER) IS SELECT name, salary + bonus old_salary FROM compensation WHERE title !- 'CEO' AND ((salary + bonus < ceosal / 50) OR (salary + bonus > ceosal / 10)); В начале исполняемого раздела определяемые этим запросом данные помеща- помещаются в две коллекции с помощью инструкции FETCH, заданной с предложением BULK COLLECT: BEGIN OPEN affected_enployees (big_bucks): FETCH affected employees BULK COLLECMNT0 names. old_salaries; CLOSE affected_employees: Затем выполняется оператор FORALL для последовательного перебора элемен- элементов коллекции names: FORALL indx IN names.FIRST .. names.LAST UPDATE compensation SET salary - DECODE ( GREATEST (nin_sal. salary), min_sal, min_sal, salary / 5) WHERE name - names (indx) RETURNING salary BULK COLLECT INTO new_salaries; Инструкция UPDATE должна либо увеличивать оклад сотрудника на 80 %, либо уменьшать его. В конце используется предложение RETURNING, с помощью которо- которого мы помещаем новые оклады в коллекцию new_salaries. Вот и все. Благодаря предложению RETURNING нам не придется выполнять еще один запрос, выбирающий из базы данных новые оклады сотрудников, и можно сразу приступать к формированию отчета: FOR indx IN names.FIRST .. names.LAST LOOP DBMS_OUTPUT.PUT_LINE ( RPAO (names(indx). 20) || RPAD ('Old: ' || old_salariesAndx). 15) || ' New: ' || new_salar1esAndx)
516 Глава 14 • Выборка данных END LOOP: END: А вот каким будет отчет, сгенерированный сценарием, который содержится в файле onlyfair.sql: John DayAndNight Old: 105Q0 New: 2900000 Holly Cubicle Old: 5ZQQ0 New: 2900000 Sandra Watchthebucks Old: 22000000 New: 4000000 Теперь каждый сотрудник нашей компании сможет позволить себе хорошее жилье и качественное медицинское обслуживание. И при этом все будут платить большие суммы налогов, которые пойдут, в частности, на содержание государст- государственных школ и больниц. ПРИМЕЧАНИЕ- Значения столбцов и выражения, возвращаемые предложением RETURNING оператора FORALL, до- добавляются в коллекции после ранее записанных туда значений. Если же предложение RETURNING используется в обычном цикле FOR, предыдущие значения заменяются данными, возвращенными последней DML-инструкцией. Инструкция SELECT с предложением FOR UPDATE При выполнении инструкции SELECT для выборки строк из базы данных эти стро- строки не блокируются. Заблокированными являются только те строки, которые уже модифицированы, но еще не зафиксированы приложениями. Но даже эти строки доступны для чтения в том состоянии, в котором они находились до внесения из- изменений. Так что Oracle обеспечивает максимальную свободу доступа к данным таблиц, блокируя минимальное количество строк на самое короткое время и в са- самом гибком режиме. Однако иногда возникает необходимость заблокировать набор строк еще до того, как вы приступите к их изменению. Это можно сделать в инструкции SELECT с помощью предложения FOR UPDATE. При выполнении инструкции SELECT...FOR UPDATE Oracle автоматически блоки- блокирует заданные в ней строки для монопольного доступа. С этого момента никто другой не может изменять эти строки до тех пор, пока вы не выполните инструк- инструкцию ROLLBACK или C0WI?, хотя другие сеансы по-прежнему могут читать из них данные. Ниже приведены два примера, демонстрирующие принцип использования пред- предложения FOR UPDATE в курсорах: CURSOR toyscur IS SELECT name, manufacturer, preferencejevel. sell_at_yardsale_flag FROM my_sons_collection WHERE hours used = 0 FOR UPDATE: CURSOR fall Jobs_cur IS SELECT task, expectedjiours. tools_required. do_itj'ourself_"flag
Инструкция SELECT с предложением FOR UPDATE эх/ FROM winterize WHERE year - TO_CHAR (SYSDATE. 'YYYY') FOR UPDATE OF task: В первом курсоре используется предложение без параметров, а во втором — предложение с заданным в нем именем столбца. Предложение FOR UPDATE может применяться в инструкциях SELECT, выбираю- выбирающих данные из нескольких таблиц. Если при этом в указанном предложении име- имеется список OF, Oracle блокирует строки только тех таблиц, столбцы которых ука- указаны в этом списке. В следующем примере не блокируется ни одна строка табли- таблицы winterize. Вы можете ответить, почему? CURSOR fall_jobs_cur IS SELECT w.task, w.expectedjiours. w.tools_required, w.do_it_yourself_f1ag FROM winterize w. husband_conf1g he WHERE YEAR - TOCHAR CSYSOATE. 'YYYY') AND w.taskjd - hc.taskjd ¦FOR UPDATE OF husband_config.nax_procrastination_allowed: Дело в том, что в списке OF предложения FOR UPDATE задан только столбец пих_ procrastination_a 1 lowed; ни один столбец таблицы winterize в нем не указан. Список OF предложения FOR UPDATE не ограничивает вас изменением только ука- указанных в нем столбцов. Он просто помогает более четко документировать свои намерения. Если в инструкции SELECT задано предложение FOR UPDATE без списка OF, Oracle блокирует все отбираемые запросом строки всех таблиц, перечислен- перечисленных в предложении FROM. Более того, инструкция SELECT...FOR UPDATE вообще не требует последующего выполнения инструкций DELETE или UPDATE - она просто сообщает Oracle, что вам, возможно, потребуется это сделать. И наконец, предложение FOR UPDATE можно дополнить ключевым словом NOWAIT, означающим, что если таблица заблокирована другим пользователем, Oracle не должна ждать ее освобождения. В этом случае управление будет сразу возвращено программе, чтобы она могла пока заняться чем-нибудь другим и спустя некото- некоторое повторить попытку доступа. При отсутствии ключевого слова NOWAIT процесс ожидания будет заблокирован до тех пор, пока таблица не освободится. Причем тайм-аут в данном случае предусмотрен только для удаленных таблиц. Он опре- определяется значением иншшализационного параметра DISTRIBUTED_LOCK_TIMEOUT. Что касается локальных таблиц, ждать их освобождения можно бесконечно долго. Снятие блокировок инструкцией COMMIT Как только для курсора с предложением FOR UPDATE выполняется оператор OPEN, все строки его результирующего набора блокируются и остаются заблокирован- заблокированными до тех пор, пока в сеансе не выполнится инструкция COMMIT для фиксации внесенных изменений или инструкция ROLLBACK для их отката. В любом из этих слу- случаев заблокированные строки будут освобождены. Поэтому после выполнения
518 Глава 14 • Выборка данных инструкции COMMIT или ROLLBACK текущая позиция в курсоре будет утеряна, и вы не сможете выполнить следующую выборку с помощью инструкции FETCH. Рассмотрим программу, определяющую мероприятия (задания), которые не- необходимо выполнить в процессе подготовки к зиме; DECLARE /* Что нужно сделать осенью, чтобы подготовиться к зиие */ CURSOR fall jobs cur IS SELECT task, expected hours, tools required, do it_>ourself_flag FROM winterize WHERE year - TOJUMBER (TOJHAR (SYSDATE. 'YYYY')) AND completed_flag - 'NOTYET1 FOR UPDATE OF task; BEGIN /* Для каждого выбранного из курсора задания... */ FOR job_rec IN falljobs_cur LOOP IF job_rec.dojt_jwrself_flag - 'YOUCANDOIT' THEN /* || Найдено очередное задание, фиксируем изменения. */ UPDATE winterize SET responsible - 'STEVEN' WHERE task = job_rec.task AND year - T0_CHAR (SYSDATE, 'YYYY'J: COMMIT; END IF; END LOOP: END: Предположим, что этот цикл нашел первое задание, помеченное как YOUCAN- DQIT. Ответственным за его реализацию назначается STEVEN, и программа пытается выбрать следующую строку, Однако в ответ она получает исключение: ORA-01002: fetch out of sequence Если в процессе извлечения данных из курсора, задаваемого как SELECT...FOR UPDATE, нужно производить фиксацию или откат изменений, придется включить в программу код, прекращающий дальнейшую выборку (например, оператор EXIT, выполняющий выход из цикла). Предложение WHERE CURRENT OF PL/SQL позволяет использовать в инструкциях UPDATE и DELETE специальное пред- предложение WHERE CURRENT OF, облегчающее процесс изменения последней выбранной из курсора строки данных. Обновления столбцов последней выбранной строки можно выполнить с помо- помощью такой инструкции: UPDATE имя_табпицы SET превложение set WHERE CURRENT OF ш_курсора\
Инструкция SELECT с предложением FOR UPDATE 519 Аналогичным образом производится удаление последней выбранной строки: DELETE FROM имя_габлицы WHERE CURRENT OF тя_курсора: Обратите внимание, что в предложении WHERE CURRENT OF указывается курсор, а не запись, в которую была помещена очередная строка. Важнейшим достоинством использования предложения WHERE CURRENT OF при модификации или удалении последней извлеченной строки из курсора, является то, что один и тот же критерий поиска строки не нужно задавать в двух местах программы. Не будь его, пришлось бы ввести предложение WHERE в определение курсора, а затем повторить его в соответствующих инструкциях UPDATE и DELETE. Если бы впоследствии потребовалось изменить условие поиска, изменения нуж- нужно было бы вносить во все SQL-инструкции, в которых оно используется. Вопрос избыточности кода достаточно серьезен, и не стоит недооценивать его значение. В PL/SQL есть множество средств минимизации избыточности, и о них нужно знать и активно ими пользоваться. Такие элементы, как предложение WHERE CURRENT OF, атрибуты объявлений «TYPE и 2ROWTYPE, цикл FOR с курсором, ло- локальные модули и другие языковые конструкции PL/SQL, могут значительно об- облегчить разработку, отладку и сопровождение приложений Oracle. Давайте посмотрим, как с помощью предложения WHERE CURRENT OF усовершен- усовершенствовать пример из предыдущего раздела. В цикле FOR нужно обновить строку, только что выбранную из курсора. Попытаемся сделать это с помощью инструк- инструкции UPDATE, в которой задано то же условие, что и в инструкции SELECT курсора (первичный ключ таблицы составляют значения столбцов task и year). WHERE task - job rec.task AND year - TO~CHAR CSYSDATE. 'YYYY'); Как рассказывалось выше, это неверный подход: одна и та же логика запро- запрограммирована в двух местах, и в случае внесения каких-либо изменений нужно следить за синхронизацией этого кода. Как было бы хорошо, если бы PL/SQL предоставлял программные эквиваленты таких действий, как удаление только что выбранной строки и обновление столбцов только что выбранной строки. Это именно то, что делает предложение WHERE CURRENT OF! В новой версии про- программы подготовки к зиме воспользуемся им и заодно заменим цикл FOR простым циклом с условным выходом: DECLARE CURSOR fall Jobs_cur IS SELECT ... то же, что и в предыдущей примере ... ; job_rec falTjoobs curXROWTYPE; BEGIN OPEN fall jobs_cur; LOOP FETCH fall_jobs_cur into job_rec: IF fall jobs curlNOTFOUND THEN EXIT; ELSIF Job_rec.do_1t_yourself_flag - 'YOUCANDOIT' THEN
520 Глава 14 • Выборка данных UPDATE winterize SET responsible - 'STEVEN' WHERE CURRENT OF fall_jobs_cur; COMMIT: EXIT: END IF; END LOOP: CLOSE falljobs_cur; END; Курсорные переменные Курсорная переменная — это переменная, указывающая на курсор. В отличие от явного курсора, имя которого в PL/SQL используется как идентификатор рабочей области результирующего набора строк, курсорная переменная содержит ссылку на эту рабочую область. Явные и неявные курсоры, с учетом того, что они привя- привязаны к конкретному запросу, можно назвать статическими. С помощью же кур- курсорной переменной можно выполнить любой запрос и даже несколько разных за- запросов в одной программе. Важнейшим преимуществом курсорных переменных является то, что они пре- предоставляют механизм передачи результатов запроса (выбранных из строк курсора) между разными программами PL/SQL, в том числе между клиентскими и сервер- серверными программами. До выхода PL/SQL Release 2.3 приходилось выбирать из кур- курсора все данные, сохранять их в переменных PL/SQL (обычно для этой цели ис- использовалась таблица) и передавать эти переменные в качестве аргументов. А курсорная переменная позволяет передать другой программе ссылку на объект курсора, чтобы та могла работать с его данными. Это упрощает программный код и повышает его эффективность. Кроме того, благодаря этой новой технологии курсоры могут совместно ис- использоваться несколькими программами. Например, в архитектуре клиент-сер- клиент-сервер клиентская программа может открыть курсор и начать выборку из него дан- данных, а затем передать указывающую на него переменную в качестве аргумента хранимой процедуре, выполняющейся на сервере. Эта процедура может продол- продолжить выборку и некоторое время спустя снова передать управление клиентской программе, которая закроет курсор. Так же могут взаимодействовать и две раз- разные хранимые программы из одного или разных экземпляров СУБД Oracle. Описанный процесс, схематически показанный на рис. 14.2, который демонст- демонстрирует новые возможности программ PL/SQL — совместное использование дан- данных и управление курсорами. Программный код с курсорными переменными очень похож на код с явными курсорами. В следующем примере объявляется тип курсора (называемый REF CURSOR) для таблицы company, затем объявляется курсорная переменная этого типа, после чего курсорная переменная открывается, из нее выбираются строки и она закрывается. DECLARE /* Создаем тип цурсора */ TYPE company_curtype IS REF CURSOR RETURN companyJROWTYPE;
Курсорные переменные 521 /* Объявляем курсорную переменную этого типа */ company_curvar company_curtype: /* Объявляем запись той же структуры, что и курсорная переменная */ company_rec campanyXROWTYPE: BEGIN /* Открываем курсорную переменную и связываем ее с SQL-инструкцией */ OPEN company_curvar FOR SELECT * FROM company: /* Извлекаем значение курсорной переменной */ FETCH company_curvar INTO companyrec; /* Закрываем курсорную переменную */ CLOSE company_curvar: END; Программа 1 : [ OPEN курсор] f FETCH строка il вьаов второй программы I 1 с передачей курсорной передачей курсорной переменной • [FETCH строка 2j (FETCH строка з) [ CLOSE курсор Возврат в первую програиму Рис 14.2. Передача ссылки на курсор между двумя программами Все это очень похоже на работу с обычным явным курсором, если, конечно, не считать определения типа REF CURSOR и синтаксиса оператора OPEN FOR, в котором запрос задается при открытии курсорной переменной, — здесь он несколько иной. Несмотря на схожесть синтаксиса, сам факт, что курсорная переменная явля- является все же переменной, открывает множество новых возможностей. О них рас- рассказывается в следующих разделах. Для чего нужны курсорные переменные Курсорная переменная предоставляет профаммисту ряд новых возможностей. О По ходу выполнения программы курсорную переменную можно связывать с разными запросами. Иными словами, ее можно использовать для выборки данных из разных результирующих наборов строк. О Курсорную переменную можно передать в качестве аргумента процедуре или функции. Это означает, что результирующий набор строк курсора будет со- совместно использоваться вызываемой и вызывающей программами.
522 Глава 14 • Выборка данных О Курсорные переменные обладают всеми возможностями статических курсо- курсоров PL/SQL. К ним можно применять операторы OPEN, CLOSE и инструкцию FETCH, а также стандартные атрибуты курсоров: SFOUND, SNOTFOUND, SROWCOUNT и XISOPEN. О Значение одной курсорной переменной (вместе с результирующим набором строк) можно присвоить в качестве значения другой курсорной переменной. Однако у этой операции присваивания имеются определенные ограничения, о которых будет рассказано ниже. Сходство со статическими курсорами Семантика использования курсорных переменных почти совпадает с семантикой статических курсоров. Объявление курсорной переменной и открывающий ее опе- оператор изменены, но все остальные операции остались прежними. О Оператор CLOSE. В следующем примере объявляются тип REF CURSOR и кур- курсорная переменная на его основе. Затем эта переменная закрывается, причем синтаксис оператора CLOSE тот же, что и у статического курсора: DECLARE TYPE var_cur_type IS REF CURSOR; var_cur var cur_type; BEGIN CLOSE var cur: END; О Атрибуты курсорной переменной. Для получения информации о курсорной переменной используются атрибуты статического курсора. В частности, для курсорной переменной из предыдущего примера обращение к атрибутам вы- выполняется так: var_cur*ISOPEN var curSSFOUND var~curXNOTFOUND var~cur*ROWCOUNT О Выборка из курсорной переменной. Для выборки данных из курсорной пере- переменной в локальные структуры данных PL/SQL используется инструкция FETCH с тем же синтаксисом, что и у статических курсоров. Однако здесь дейст- действуют дополнительные правила, требующие соответствия структуры данных строки курсорной переменной и структур данных, указанных справа от клю- ключевого слова INTO. Эти правила приводятся далее, в разделе «Правила для кур- курсорных переменных». Поскольку синтаксис перечисленных операторов и атрибутов, предназначен- предназначенных для работы с курсорными переменными, во многом совпадает с синтаксисом их версий для статических курсоров, ниже рассматриваются только элементы, уникальные для курсорных переменных.
Курсорные переменные 523 Объявление типов REF CURSOR Подобно таблице PL/SQL или записи определяемого программистом типа курсор- курсорная переменная объявляется в два этапа. О Сначала создается тип курсора (в Огас1е9г можно использовать предопределен- предопределенный слаботипизированный тип SYS_REFCURSOR). О Затем на его основе объявляется фактическая курсорная переменная. Синтаксис объявления типа курсора таков: TYPE им_типа_курсора IS REF CURSOR [ RETURN возвращаеный_тип']: Здесь mnjmajtypcopa — это имя создаваемого типа курсора, а возвращаемый_тм — спецификация возвращаемых данных курсора. Это может быть любая структура данных, использование которой допускается в предложении RETURN, определенная с помощью атрибута SROWTYPE или путем ссылки на ранее объявленный тип записи. Обратите внимание, что предложение RETURN в объявлении типа REF CURSOR не обязательно. Поэтому допустимы оба следующих объявления: TYPE company curtype IS REF CURSOR RETURN companyZROWTYPE; TYPE gener1cj:urtype IS REF CURSOR В первом случае мы объявили силънотпитшзированный тип, поскольку тип структуры, возвращаемой курсорной переменной, задается в момент объявления (непосредственно или путем привязки к типу строки таблицы). Поэтому, выпол- выполняя синтаксический анализ инструкции FETCH, компилятор может сразу опреде- определить, соответствует ли тип заданной в нем структуры данных спецификации RE- RETURN в определении REF CURSOR. Но курсорная переменная, объявленная на основе данного типа, может использоваться только с SQL-инструкциями, возвращаю- возвращающими строки соответствующего типа, и с такими же инструкциями FETCH, Во втором случае, без предложения RETURN, мы объявили слаботипизирован- слаботипизированный тип. Тип возвращаемой структуры данных для него не задается. Курсорная переменная, объявленная на основе такого типа, может использоваться более гиб- гибко, поскольку для нее можно задавать любые запросы с любой структурой воз- возвращаемых данных, причем с помощью одной и той же переменной можно пооче- поочередно выполнить несколько разных запросов, возвращающих результаты разных типов. Объявление курсорной переменной Синтаксис объявления курсорной переменной таков: чня_курсорной_переменой им*_типа_курсора; Здесь имя_курсорной_переменной — это имя объявляемой переменной, а нмя_тила_ курсора — это имя типа курсора, объявленного ранее с помощью оператора TY- TYPE. . .REF CURSOR. Как создается курсорная переменная, вы поймете из кода: DECLARE /* Создание типа курсора для хранения информации о спортивных автомобилях */ TYPE sports_car_cur_type IS REF CURSOR RETURN carSROWTYPE;
524 Глава 14 • Выборка данных /* Создание курсорной переменной для хранения информации о спортивных автомобилях */ sports_car_cur sports_car_cur_type; BEGIN END: Важно понимать различие между операцией объявления курсорной перемен- переменной и операцией создания реального объекта курсора, то есть результирующего набора строк, идентифицируемого SQL-инструкцией курсора. Подобно тому как константа является значением, а переменная — указателем на значение, так и ста- статический курсор является набором данных, а курсорная переменная — указате- указателем на этот набор. Указанное различие иллюстрирует рис. 14.3. Обратите внима- внимание, что две разные переменные в различных программах ссылаются на один и тот же объект курсора. Программа PL/SQL Курсорная 1 переменная 1 | j 1 П 1 бъе к Курсорная I переменная 2 | Общая память Рез набо КГ KVDCODa В* ~ ¦ —— мая ульт рстр ирующи окзапрс 1 й юа Рис. 14.3. Курсоры и курсорные переменные В результате объявления курсорной переменной объект курсора не создается. Для создания такового, а также для помещения ссылки на него в переменную нужно выполнить оператор OPEN FOR. Открытие курсорной переменной Инициализацию курсорной переменной (то есть создание объекта курсора и по- помещение ссылки на него в переменную) выполняет оператор OPEN с предложени- предложением FOR, в котором задается SQL-инструкция: OPEN иня_курсора FOR HHcrpyKunsselect: Здесь нмя_курсора — это имя курсора или курсорной переменной, а инструкция_se- lect - это SQL-инструкция SELECT, используемая для формирования набора строк курсора. Для курсорной переменной сильнотипизированного типа количество и типы данных столбцов, указанных в инструкции SELECT, должны соответствовать (быть
\ Курсорные переменные совместимыми) структуре, заданной в предложении RETURN оператора TYPE... REF CURSOR. Пример совместимых типов показан на рис. 14.4. Подробнее о совмести- совместимости возвращаемых курсорами структур рассказывается далее, в разделе «Пра- «Правила для курсорных переменных». DECLARE TYPE emp_curtype IS REF CURSOR RETURN empWKWTYPE; emp_curvar emp_curtype; BEGIN OPEN emp_curvar FOR SELECT|* FROM enp; | END; Рис 14.4. Совместимость типа данных REF CURSOR и типа данных значений, возвращаемых инструкцией SELECT Если курсорная переменная объявлена на основе слаботипизированного типа, ее можно открывать с любым запросом, независимо от структуры возвращаемых им данных. В следующем примере курсорная переменная трижды открывается с тремя разными запросами (а последний оператор OPEN вообще никак не связан с таблицей employee!): DECLARE TYPE emp_curtype IS REF CURSOR; emp_curvar emp_curtype; BEGIN OPEN emp_curvar FOR SELECT * FROM emp: OPEN emp_curvar FOR SELECT employeejd FROM emp; OPEN emp_curvar FOR SELECT company_id. name FROM company: END; Если курсорной переменной еще не присвоен никакой объект курсора, опера- оператор OPEN FOR неявно создает его для нее. Если же переменная уже указывает на объект курсора, оператор OPEN FOR не создает новый курсор, а ассоциирует с суще- существующим новый запрос. Таким образом, объект курсора является структурой, отдельной и от курсорной переменной, и от запроса. Выборка данных из курсорной переменной Как уже упоминалось, синтаксис инструкции FETCH для курсорной переменной точно такой же, как для статического курсора: FETCH иня_курсорной_переменной INTO ш_эаписи; FETCH имя_цурсорной_перепенной INTO имя_переменной. имя_перененной ...: Когда курсорная переменная объявляется на основе сильнотипизированного типа, компилятор PL/SQL проверяет совместимость типов структур, которые за- задаются в предложении INTO, с типом, заданным в предложении RETURN оператора TYPE...REF CURSOR.
526 Глава 14 • Выборка данных Если же курсорная переменная объявлена на основе слаботипизированного типа, компилятор не может выполнить подобную проверку. Данные из такой кур- курсорной переменной могут извлекаться в любые структуры данных, поскольку ком- компилятор не знает, какой объект курсора (и какая инструкция) будет ей присвоен на момент выборки. Поэтому проверка совместимости производится во время выполнения програм- программы, причем непосредственно в момент выборки данных. И если окажется, что возвращенные запросом данные не соответствуют структурам, указанным в пред- предложении INTO, исполняющее ядро PL/SQL сгенерирует исключение ROWTYPE_MIS- МАТСН. Учтите, что при необходимости и наличии соответствующих возможностей PL/SQ.L выполняет неявное преобразование данных. Обработка исключения ROWTYPE_MISMATCH Перехватив исключение ROWTYPEMISMATCH, можно попытаться выбрать данные из курсорной переменной с другим предложением INTO. В случае успеха из объекта курсора будет считана первая строка результирующего набора. Рассмотрим пример централизованной базы данных, состоящей из множества таблиц с информацией о частной собственности, о коммерческой собственности и т. д. Кроме того, существует единая таблица адресов, где каждому адресу по- поставлен в соответствие тип здания (жилой дом, ведомственное здание и т. д.). Процедура open_state_l 1st получает в качестве параметра адрес здания, открыва- открывает курсорную переменную слаботипизированного типа и связывает с ней запрос на выборку информации об этом здании из соответствующей таблицы. С помо- помощью этой процедуры сведения о нужном здании может получить сотрудник любо- любого подразделения, занимающегося конкретным типом недвижимости. Вот как она создается. О Сначала определяется слаботипизированный тип REF CURSOR: TYPE bu1ld1ng_curtype IS REF CURSOR; О Затем создается процедура. Обратите внимание, что курсорная переменная передается как IN OUT, то есть является входной и выходной: PROCEDURE open site 11st CaddressJrflN VARCHAR2. site cur inout IN OUT building curtype) IS home type CONSTANT INTEGER :- 1; commercial_type CONSTANT INTEGER :- 2; I* Статический курсор для определения типа здания */ CURSOR s1te_type_cur IS SELECT s1te_type FROM propertyjiaster WHERE address - address 1n; s1te_type_rec site_type_cur3!RCWrYPE: BEGIN /* Считываем информацию о типе знания для данного адреса */ OPEN s1te_type cur; FETCH s1te_typi cur INTO site type rec: CLOSE s1te_typej:ur; /* Теперь с учетом типа здания проиэволим выборку из таблицы */
Курсорные переменные 527 IF site type rec.s1te_type - home_type THEN /* Таблица жилых домов */ OPEN s1te_cur_1nout FOR SELECT * FROM home_propert1es WHERE address LIKE '*' || addressjn || T: ELSIF site type_rec.s1te_type - conmerc1al_type THEN /* Таблица коммерческой собственности */ OPEN s1te_cur_1nout FOR SELECT * FROM comercial_properties WHERE address LIKE T || addressjn || 'f: END IF; END open_s1te_l1st: Процедура готова, и теперь ее можно использовать. В следующем примере процедур6 open_state_l i st передается адрес здания, а за- затем выполняется попытка выборки информации из полученного курсора. При этом предполагается, что курсор содержит сведения о жилом доме. Если это не так, PL/SQL инициирует исключение ROWTYPE_MISMATCH, указывающее на несовмес- несовместимость типов записей. После этого управление передается в раздел обработки исключений, где повторяется попытка выборки строки, но на этот раз в запись, соответствующую коммерческому зданию. DECLARE /* Об1являен курсорную переменную */ bulld1ng_curvar buiiding_curtype: /* Определяем структуры записей для двух разных таблиц */ home_rec home_properti esJiROWTYPE; comerci a1_rec commerd a1_propert1 esKROWTYPE: BEGIN /* Запрашиваем у пользователя адрес */ promptjor_address (address_str1ng); /* Присваиваем курсорной переменной запрос с учетом адреса */ open_s1teJ1st (address_strtng. build1ng_curvar): /* Пытаемся выбрать строку из курсорной переменной в запись, соответствувцуо жилому дому */ FETCH bund1ng_curvar INTO hore_rec; /* Если все получилось, выводим информацию */ show_home_s1te (home rec); EXCEPTION /* Если первая строка не относится к жилому дому... */ WHEN ROWTYPE_MISMATCH THEN /* Выбираем ту же строку а запись, соответствующую коммерческому зданию */ FETCH bu1ld1ng_curvar INTO conmerc1al_rec; /* Выводим информации о коммерческом здании */ show camierc1al_s1te (conjnercial_rec); END;
528 Глава 14 ¦ Выборка данных Правила использования курсорных переменных В этом разделе рассматриваются правила, вопросы и рекомендации, связанные с использованием курсорных переменных в программах. Речь пойдет о правилах соответствия типов данных строк, псевдонимах курсорных переменных и облас- области их действия. Прежде всего помните, что курсорная переменная — это ссылка на объект кур- курсора, представляющий данные, которые выбраны из базы данных с помощью со- содержащегося в нем запроса. Это не сам объект курсора. Курсорная переменная может быть связана с определенным запросом при одном из следующих условий: О если выполнен оператор OPEN FOR, назначающий курсорной переменной этот запрос; О если курсорной переменной присвоено значение другой курсорной перемен- переменной, указывающей на этот запрос. С курсорными переменными может использоваться оператор присваивания, и эти переменные могут передаваться в качестве аргументов процедурам и функ- функциям. Чтобы иметь возможность выполнять такие операции, переменные долж- должны удовлетворять правилам соответствия типов строк, проверяемым во время компиляции или во время выполнения, в зависимости от типа курсорных пере- переменных. Правила соответствия типов строк, проверяемые во время компиляции В процессе компиляции программы PL/SQL проверяет, соблюдаются ли следую- следующие правила. О Для двух курсорных переменных допустимо применять оператор присваива- присваивания или использовать их в качестве передаваемых параметров в любом из сле- следующих случаев: • обе переменные (или параметра) определены на основе сильнотипизирован- ных типов REF CURSOR с одним и тем же именем типа возвращаемой записи; • обе переменные (или параметра) определены на основе слаботипизирован- ных типов REF CURSOR, независимо от типа возвращаемой записи; • одна переменная (или параметр) определена на основе сильнотипизиро- ванного типа REF CURSOR, а другая - на основе слаботипизированного типа. О Для курсорной переменной (или параметра), определенной на основе сильно- типизированного типа REF CURSOR, можно открыть запрос, возвращающий строку, тип которой структурно эквивалентен возвращаемому типу, заданному в оп- определении типа курсорной переменной. О Для курсорной переменной (или параметра), определенной на основе слабо- типизированного типа REF CURSOR, можно открыть любой запрос. Из этой пере- переменной можно выбирать данные в любой список переменных или в запись лю- любого типа. Если одна из курсорных переменных определена на основе слаботипизиро- слаботипизированного типа REF CURSOR, компилятор PL/SQL не может проверить соответствие
Курсорные переменные 529 типов возвращаемых строк. Такая проверка будет произведена во время выпол- выполнения программы с учетом правил, описанных в следующем разделе. Правила соответствия типов строк, проверяемые во время выполнения Во время выполнения программы PL/SQL проверяет, соблюдаются ли следую- следующие правила: О Курсорной переменной (или параметру), определенной на основе слаботили- зированного типа REF CURSOR, можно назначить любой запрос независимо от типа возвращаемых им строк. О Курсорной переменной (или параметру), определенной на основе сильноти- пизированного типа REF CURSOR, можно назначить только запрос, структурно соответствующий типу записи, заданному в предложении RETURN определения типа курсорной переменной. О Две записи (или два списка, переменных) считаются структурно соответст- соответствующими друг другу, если соблюдены два условия: • количество полей в обеих записях (или количество переменных в обоих списках) одинаково; • для каждого поля одной записи (или переменой из одного списка) соответ- соответствующее поле (или переменная из другого списка) имеет тот же тип дан- данных или может быть неявно преобразовано к тому же типу данных. О Список переменных и запись в предложении INTO инструкции FETCH должны структурно соответствовать связанному с курсорной переменной запросу. Это правило касается и статических курсоров. Псевдоним объекта курсора Если одна курсорная переменная присваивается другой курсорной переменной, то обе они становятся псевдонимами одного и того же объекта курсора. В данном случае в результате присваивания в принимающую переменную копируется толь- только ссылка на объект курсора, так что сам курсор (включая запрос и результирую- результирующий набор строк) остается на своем месте, но теперь на него указывают две пере- переменные. Результаты любой операции с этим курсором, выполняемой с помощью одной из переменных, сразу же становятся доступными для другой переменной. Принцип действия псевдонимов курсоров демонстрирует приведенный ниже анонимный блок: 1 DECLARE 2 TYPE curvarjype IS REF CURSOR; 3 curvarl curvar_type; 4 curvar2 curvar_type; 5 story fa1ry_tales3;R0WTYPE: 6 BEGIN 7 OPEN curvarl FOR SELECT * FROM fairy_tales; 8 curvar2 :- curvarl: 9 FETCH curvarl INTO story: 10 FETCH curvar2 INTO story:
530 Глава 14 • Выборка данных 11 CLOSE curvar2; 12 FETCH curvarl INTO story; 13 END: Выполняемые этим кодом действия описаны в следующей таблице. Строки Описания 2-5 Объявление слаботипизированного типа REF CURSOR и курсорных переменных на его основе 7 Создание объекта курсора и назначение его курсорной переменной curvarl 8 Назначение того же объекта курсора второй курсорной переменной, curvar2. (Теперь у нас две переменные, которые можно использовать для работы с одним и тем же результирующим набором строк) 9 Выборка первой строки с помощью переменной curvarl 10 Выборка второй строки с помощью переменной curvar2. (При этом совершенно не важно, какую из переменных использовать для выборки, Указатель текущей строки находится в объекте курсора, а не в переменной) 11 Закрытие объекта курсора с помощью переменной curvar2 12 При попытке выборки строки с помощью переменной curvarl инициируется исключение INVALJD_CURSOR. (Поскольку объект курсора закрыт, ни одна из ссылающихся на него переменных больше не может использоваться для доступа к строкам результирующего набора) Любое изменение состояния объекта курсора является видимым для любой ссылающейся на него переменной. Область видимости объекта курсора Область видимости курсорной переменной та же, что и у статического курсора, а именно блок PL/SQL, в котором эта переменная объявлена (если только она не объявлена в пакете, что делает ее глобально доступной). А вот область видимости объекта курсора совершенно иная. С момента выполнения оператора OPEN FOR, создающего объект курсора, по- последний остается доступным до тех пор, пока на него ссылается хоть одна актив- активная переменная. Как вы уже знаете, можно создать объект курсора в одной области (блоке PL/SQL), а затем присвоить ссылку на него переменой из другой области. Когда выполнение программы выходит за пределы первой области, первая ссы- ссылающаяся на него переменная становится недоступной, но курсор останется дос- доступным до тех пор, пока доступна вторая ссылающаяся на него переменная. Следующий пример с вложенными блоками демонстрирует доступ к объекту курсора извне области, в которой он был создан. DECLARE /* Определяем тип REF CURSOR, курсорную и скалярную переиенные */ TYPE curvarjype IS REF CURSOR; curvarl curvar_type; do^you_get_it VARCHAR2C10Q): BEGIN /* || Во вложенном блоке создается объект курсора, который || назначается курсорной переменной curvarl. */
Курсорные переменные 531 DECLARE curvar2 curvar_type: BEGIN OPEN curvar2 FDR SELECT punchjine FROM Jokes: curvarl :- curvar2: END: /* || Курсорная переменная curvarZ больше не активна, но ссылка || на курсор была скопирована в переменную curvarl. || объявленную ао внешнем блоке. Поэтому данные из объекта курсора || иожно выбирать с помощью курсорной переменной curvarl. */ FETCH curvarl INTO do_^ou_get_U; CLOSE curvarl; END: Передача курсорных переменных в качестве аргументов Курсорную переменную можно передать в качестве аргумента процедуре или функции. В списке параметров такой процедуры (или функции) должен быть за- задан режим использования и тип REF CURSOR. Идентификация типа REF CURSOR В заголовке программы должен быть идентифицирован предварительно объяв- объявленный тип параметра курсорной переменной. Если вы создаете локальный мо- модуль внутри другой программы (о локальных модулях подробно рассказывается в главе 16), тип курсора можно определить в той же программе. Вот пример: DECLARE /* Определяем тип REF CURSOR */ TYPE curvar_type IS REF CURSOR RETURN companySROWTYPE; /* Указываем его s списке параметров */ PROCEDURE open_query (curvar_out OUT curvarjype) IS local_cur curvar type; BEGIN OPEN local_cur FOR SELECT * FROM company; curvar out":- local cur; END; BEGIN end!" Если вы создаете отдельную процедуру или функцию, объявление типа REF CURSOR придется поместить в пакет. Для ссылки на этот тип должен использовать- использоваться точечный синтаксис. Последовательность выполняемых действий в этом слу- случае должна быть такой. 1. Создаем пакет с объявлением типа REF CURSOR: PACKAGE company IS /* Определяем тип REF CURSOR */
532 Глава 14 • Выборка данных TYPE curvar_type IS REF CURSOR RETURN companySROWTYPE; END package: 2. В отдельной процедуре ссылаемся на тип REF CURSOR, предварив имя типа име- именем пакета: PROCEDURE open_company (curvar_out OUT company.curvar_type) IS BEGIN END: Режимы использования параметров Значение переменной типа REF CURSOR может являться входным параметром (IN), выходным параметром (OUT), а также и тем и другим (IN OUT). Напомним, что значением курсорной переменной служит ссылка на объект курсора, а не сам объект. Поэтому значение переменной не меняется после вы- выборки строк из курсора или после его закрытия. Значение курсорной переменной можно изменить двумя способами: О путем присваивания значения другой курсорной переменной; О с использованием оператора OPEN FOR. Если курсорная переменная уже указывает на объект курсора, оператор OPEN FOR не меняет эту ссылку. Он просто модифицирует связанный с курсором запрос и результирующий набор строк. Приведем пример процедуры, параметрами которой являются курсорные пе- переменные: PROCEDURE assign_curvar (old_curvar_in IN company.curvarjtype. new_curvar_out OUT company.curvar_type) IS BEGIN new_curvar_out := old_curvar_in: END; Эта процедура колирует значение одной курсорной переменной в другую. Ее первый параметр является входным, поскольку он используется в правой части оператора присваивания, а второй — выходным, поскольку его значение изменя- изменяется процедурой. (Режимом второго параметра мог быть и IN OUT.) Обратите вни- внимание, что тип curvar_type определен в пакете company. Ограничения, связанные с курсорными переменными Курсорные переменные подчиняются следующим ограничениям. Некоторые из них, возможно, будут отменены в следующих версиях Oracle. О Курсорная переменная не может быть объявлена в пакете, поскольку она не имеет постоянного значения. О Курсорную переменную нельзя передавать между серверами с помощью уда- удаленных вызовов процедуры.
Курсорные выражения 533 О Если курсорная переменная передается PL/SQL как переменная привязки или хост-переменная, на сервере нельзя выбирать из нее строки, не открыв ее в том же вызове. О В запросе, заданном для курсорной переменной в операторе OPEN FROM, раньше нельзя было использовать предложение FOR UPDATE. (Такая возможность появи- появилась только в Огас1е9г.) О Курсорную переменную нельзя с помощью операторов сравнения проверять на равенство или неравенство какому либо значению (в том числе значению NULL). О Курсорной переменной нельзя присвоить значение NULL. Попытка сделать это приведет к исключению PLS-00382: Expression is of wrong type. О Значение курсорной переменной не может храниться в столбце базы данных. Типы REF CURSOR не могут использоваться для определения столбцов таблиц. О Значение курсорной переменной не может содержаться во вложенной табли- таблице, ассоциативном массиве или VARRAY. Типы REF CURSOR не могут использовать- использоваться для определения элементов коллекции. Курсорные выражения В Oracle9i введен еще один новый мощный элемент языка SQL: курсорные выра- выражения. Курсорное выражение — это выражение со специальным оператором CUR- CURSOR, используемое в SQL-запросе и определяющее вложенный курсор. Каждая строка результирующего набора вложенного курсора может содержать диапазон значений, допустимых для SQL-запросов; кроме того, она может включать другие курсоры, определяемые вложенными запросами. ПРИМЕЧАНИЕ- Оператор CURSOR, введенный в Orade8i для SQL, не мог использоваться в программах PL/SQL. В Огас1е91 указанный недостаток был устранен, и теперь курсорные выражения могут применяться в SQL-инструкциях программ PL/SQL. С помощью курсорного выражения можно вернуть большой и сложный набор связанных значений из одной или нескольких таблиц. Такой результирующий набор данных обычно обрабатывается с помощью ряда вложенных циклов: глав- главный цикл выбирает строки основного курсора, а вложенные циклы выбирают строки вложенных курсоров. Курсорное выражение может быть довольно сложным, что зависит от сложно- сложности извлекаемых с его помощью данных. Однако это столь мощный и гибкий ме- механизм выборки, что его обязательно нужно освоить. Перечислим структурные компоненты языка, в которых могут использоваться курсорные выражения: О объявления явных курсоров; О динамические SQL-запросы; О объявления и переменные типа REF CURSOR. В неявных курсорах курсорные выражения никогда не используются.
534 Глава 14 • Выборка данных Синтаксис курсорного выражения очень прост: CURSOR (вложенный_запрос) Oracle неявно открывает вложенный курсор при выборке строки, содержащей его выражение, из родительского или внешнего курсора. Он закрывается, когда: О вы его явно закрываете; О родительский курсор повторно выполняется, закрывается или отменяется; О при выборке из родительского курсора инициируется исключение. В данном случае вложенный курсор закрывается вместе с родительским. Использование курсорных выражений Следующая процедура демонстрирует принцип использования вложенного кур- курсорного выражения. Запрос верхнего уровня выбирает два элемента данных: на- название города и отделение компании в этом городе, информация о котором содер- содержится во вложенном курсоре. Этот вложенный курсор, в свою очередь, извлекает с помощью курсорного выражения еще один вложенный курсор — на этот раз с фамилиями всех сотрудников каждого отделения. Те же данные можно было бы извлечь из базы данных с помощью нескольких явных курсоров, открываемых и обрабатываемых во вложенных циклах. Однако курсорное выражение позволяет применить другой подход, более лаконичный и эффективный, переложив большую часть работы на подсистему выполнения SQL-инструкций и тем самым сократив количество переключений контекста. CREATE OR REPLACE PROCEDURE emp_report (p lodd NUMBER) IS TYPE refcursor IS REF CURSOR: -- Запрос возвращает 2 столбца, второй из которых является -- wcopon. а значит, позволяет перебрать набор связанных строк . CURSOR all_1n_one cur IS SELECT l.dty," CURSOR (SELECT d.departmentjiame. CURSORCSELECT e.lastjiame FROM employees e WHERE e. department.^ - d.departmentjd) AS ename FROM departments d WHERE I.locationj'd - d.location id) AS dname FROM locations 1 WHERE l.location_id - pjocid; departments_cur refcursor: employees_cur refcursor: v_dty locat1ons.dty5!TYPE: v_dname departments.departmentjiameSTYPE; v_ename employees.last_name*TYPE;
Курсорные выражения 535 BEGIN OPEN all_in_orie_cur; LOOP FETCH all_in_one_cur INTO v_dty. departments_cur; EXIT WHEN all_in_one_cur*NOTFOUND; -- Перебираем список отделений, причем для этого НЕ НУЖНО -- явно открывать курсор. Oracle делает это автоматически LOOP FETCH department5_cur INTO v_dname, emplayees_cur: EXIT WHEN departments_cur*N0TFOUN0; -- Теперь можно перебрать список сотрудников текущего -- отделения. И снова для этого не нужно явно открывать курсор LOOP FETCH employees_cur INTO v ename; EXIT WHEN employees_curXNOTFOUND: DBMS_OUTPUT.putj1ne ( vj:ity I ' ' | v dname I "' | v ename); END LOOP: " END LOOP; END LOOP; CLOSE allJn one_cur; END; Ограничения, связанные с курсорными выражениями На применение курсорных выражений налагается ряд ограничений. О Курсорное выражение не мажет использоваться в неявном курсоре по причи- причине отсутствия механизма выборки из вложенного курсора в структуру данных PL/SQL. О Курсорные выражения можно задавать только в «самом внешнем» списке вы- выборки запроса. О Курсорные выражения нельзя использовать в инструкции SELECT, вложенной в другой запрос (исключение составляет вложенный запрос курсорного выра- выражения). О Курсорное выражение может быть применено в качестве аргумента табличной функции, заданной в предложении FROM инструкции SELECT. О Курсорные выражения нельзя использовать в определении представления. Q Если курсорное выражение задано в динамической SQL-инструкции, по отно- отношению к нему нельзя применять операции BIND и EXECUTE.
15 Динамический SQL и динамический PL/SQL > Динамические запросы SQL > Многострочные запросы с курсорными переменными > Привязка переменных > Работа с объектами и коллекциями > Разработка приложений с использованием динамического SQL > Пакет Nds Utility > Сравнение динамического SQL и пакета DBMS_SQL Слова «динамический» и -«статический»- являются антонимами. Статическими называются жестко закодированные инструкции и операторы, которые не изме- изменяются с момента компиляции программы. Инструкции динамического SQL формируются, компилируются и вызываются непосредственно во время выпол- выполнения программы. Следует отметить, что такая гибкость языка открывает перед программистами огромные возможности и позволяет писать универсальный код многократного использования. Давайте рассмотрим, какие именно возможности предоставляют разработчикам динамический SQL и динамический PL/SQL1. О Выполнение инструкций DDL. С помощью статического SQL в программах PL/SQL можно выполнять только DML-инструкции. Если же вам понадобит- понадобится создать таблицу или удалить индекс, воспользуйтесь динамическим SQL. О Проектирование серверных компонентов web-приложений. Можно разре- разрешить пользователю выбирать столбцы и изменять порядок просмотра данных, то есть выводить или модифицировать данные произвольным образом по за- запросу. О Создание универсальной программы синтаксического анализа строк. Такая программа может принимать список значений в виде строки с разделителями и помещать его элементы в коллекцию. 1 Далее в этой главе под термином «динамический SQL» мы будем подразумевать и динамические блоки PL/SQL, если явно не указано обратное.
Инструкции NDS 537 Начиная с Огас1е7.1 поддержка динамического SQL осуществляется с помо- помощью встроенного пакета DBMS_SQL. В Oracle 8г для этого появилась еще одна воз- возможность - встроенный динамический SQL (Native Dynamic SQL, NDS). О па- пакете DBMS_SQL подробно рассказывается в ряде других книг, а настоящая глава целиком посвящена NDS, который является более удобным и эффективным сред- средством написания и выполнения динамического кода. Кроме того, в отличие от па- пакета DBMS_SQL, NDS интегрирован в язык PL/SQL и является его составной ча- частью. Но поскольку в некоторых случаях у вас может возникнуть необходимость воспользоваться пакетом DBMS SQL, в конце главы мы сравним оба средства и при- приведем некоторые рекомендации по их использованию. Инструкции NDS Главным достоинством NDS является его простота. В отличие от пакета DBMSSQL, для работы с которым требуется знание десятка программ и множества правил их использования, NDS представлен в языке PL/SQL единственной новой инструк- инструкцией, EXECUTE IMMEDIATE, немедленно выполняющей заданную SQL-инструкцию, а также расширением существующей инструкции OPEN FOR, позволяющей выпол- выполнять сложные динамические запросы. ПРИМЕЧАНИЕ : Инструкции EXECUTE IMMEDIATE и OPEN FOR доступны в Oracle Forms Builder и Oracle Reports Builder только в версиях OradeSi и Orade9l. При работе с другими версиями РСУБД вам придется писать хранимые программы, которые скрывают вызовы этих инструкций и могут быть вызваны из клиент- клиентского кода P17SQL. Инструкция EXECUTE IMMEDIATE Инструкция EXECUTE IMMEDIATE, используемая для немедленного выполнения за- заданной SQL-инструкции, имеет следующий синтаксис: EXECUTE IMMEDIATE crpOKdJQL [INTO [переменней. переменная']... \ здпмсь}] [USING [IN | OUT 1 IN OUT] аргумент [. [IN | OUT | IN OUT] аргумент]...]; Здесь строка_SQL — символьное выражение, содержащее SQL-инструкцию или блок PL/SQL; переменная — переменная, которой присваивается содержимое поля, воз- возвращаемого запросом; запись ~ запись, основанная на типе данных, который оп- определяется пользователем или объявляется с помощью атрибута SROWTYPE, и при- принимающая всю возвращаемую запросом строку; аргумент — выражение, значение которого передается SQL-инструкции или блоку PL/SQL, либо идентификатор, являющийся входной и/или выходной переменной для функции или процедуры, вызываемой из блока PL/SQL; INTO — предложение, используемое для одностроч- однострочных запросов (для каждого возвращаемого запросом столбца в этом предложении должна быть задана отдельная переменная или же ему должно соответствовать поле записи совместимого типа); USING — предложение, определяющее параметры SQL-инструкции и используемое как в динамическом SQL, так и в динамическом
538 Глава 15 • Динамический SQL и динамический PL/SQL PL/SQL (способ передачи параметра задается только в PL/SQL, причем по умол- умолчанию для него установлен режим передачи IN). Инструкция EXECUTE IMMEDIATE может использоваться для выполнения любой SQL-инструкции или блока PL/SQL, за исключением многострочных запросов. Если SQL-строка заканчивается точкой с запятой, она интерпретируется как блок PL/SQL, в ином случае воспринимается как инструкция языка манипули- манипулирования данными DML (например, SELECT, INSERT, UPDATE или DELETE) либо языка определения данных DDL (например, CREATE TABLE). Строка может содержать формальные параметры, но с их помощью не могут быть заданы имена объектов схемы, скажем такие, как имена столбцов таблицы. При выполнении инструкции исполняющее ядро заменяет в SQL-строке фор- формальные параметры (идентификаторы, начинающиеся с двоеточия, например, :salary_va!ue) фактическими значениями параметров подстановки в соответст- соответствии с их позицией. В качестве параметров подстановки разрешается задавать вы- выражения, возвращающие числа, даты и строки, но логические выражения зада- задавать нельзя, поскольку тип данных BOOLEAN поддерживается только в PL/SQL. He разрешается и передача символьного значения NULL - вместо него следует указы- указывать переменную соответствующего типа, содержащую это значение. Встроенный динамический SQL поддерживает все типы данных SQL. Пере- Переменные и параметры инструкции могут быть коллекциями, большими объектами (LOB), экземплярами объектных типов и типа REF. Однако NDS не поддерживает типы данных, специфические для PL/SQL, такие как BOOLEAN, ассоциативные мас- массивы и пользовательские типы записей. В предложении INTO может быть задана запись PL/SQL. Рассмотрим несколько примеров. О Вот как создается индекс: EXECUTE IMMEDIATE 'CREATE INDEX emp_u_l ON employee (lastjiameI; Согласитесь, что это достаточно просто. О Хранимую процедуру, выполняющую любую инструкцию DDL, можно соз- создать так: CREATE OR REPLACE PROCEDURE execDOL (ddl_string IN VARCHAR2) IS BEGIN EXECUTE IMMEDIATE ddl_str1ng: END: После формирования данной процедуры индекс создается следующим обра- образом: execDDL ('CREATE INDEX emp_u_l ON employee (lastjiame)'): О Теперь мы попытаемся получить количество строк в таблице указанной схе- схемы для заданного предложения WHERE: /* Файл в web: tabcount.sf */ CREATE OR REPLACE FUNCTION tabCount С tab IN VARCHAR2. whr IN VARCHAR2 :- NULL, SCh IN VARCHAR2 :- NULL) RETURN INTEGER
Инструкции NDS 539 IS retval INTEGER: SEGIN EXECUTE IMMEDIATE 'SELECT COUNTt*) FROM ' || NVL (sch. USER) || '.' 11 tab 11 1 WHERE ' || NVL (whr. '1-1') INTO retval: RETURN retval: END: В дальнейшем нам больше не понадобится писать инструкцию SELECT COUNTC*) ни в SQL* Plus, ни в программах PL/SQL. Для подсчета строк можно ввести следующий блок кода: BEGIN IF tabCount Гетр', 'deptno - ' || v_dept) > 100 THEN DBMS_OUTPUT.PUT_LINE ('Growing fast!'): END IF; О А вот как производится изменение числового значения в любом столбце лю- любой таблицы (созданная функция возвращает также количество обновленных строк): /* Файл в CREATE OR tab IN col IN val IN whr IN sch IN RETURN IS BEGIN web: updnval.sf */ REPLACE FUNCTION updNVal ( VARCHAR2. VARCHAR2. NUMBER. VARCHAR2 :- NULL, VARCHAR2 :- NULL) INTEGER EXECUTE IMMEDIATE 'UPDATE ' || NVL (sch. USER) ||'. SET ' || col 1 ' - :the value WHERE ' || NVL (whr. '1-1'У USING val; RETURN END; SQLSROWCOUNT; tab He вызывает сомнений, что такой маленький объем кода - совсем небольшая плата за гибкость, которую предоставляет эта функция. В приведенном примере показано, как используется параметр подстановки: после синтаксического анализа инструкции UPDATE ядро PL/SQL заменяет в ней формальный параметр : the_val ue значением переменной val. Обратите внимание, что в этом случае атрибут курсо- курсора SQLIR0WCOUNT можно использовать точно так же, как при выполнении статиче- статической SQL-инструкции. Рассмотрим еще один пример применения динамического SQL. Предположим, что ежедневно в 9 утра должна выполняться определенная хра- хранимая процедура, формирующая график деловых встреч сотрудника определенной компании. Структура имени каждой процедуры может быть такой: DAYNAME_set_ schedule. Здесь строка символов DAYNAME идентифицирует день недели. Каждая
540 Глава 15 • Динамический SQL и динамический PL/SQL процедура имеет четыре одинаковых параметра: входные — идентификационный код сотрудника и время первой встречи, и выходные — фамилия сотрудника и ко- количество встреч за день. Покажем, как составляется такой график: /* Файл в web: run9am.sp */ CREATE OR REPLACE PROCEDURE run_9am_procedure ( id_in IN employee. employeeJdmPE. hour_in IN INTEGER ) IS v_apptcount INTEGER; v_name VARCHAR2 A00); BEGIN . EXECUTE IMMEDIATE 'BEGIN ' || TO_CHAR (SYSDATE, 'DAY') II '_set_schedule (;id, .-hour. :name, :appts); END:1 USING IN idjn. IN hourjn. OUT vjiame. OUT v_apptcount: D8MS_0UTPUT.put_line { 'Employee ' | v_name | ' has ' || v_apptcount I| ' appointments on ' || TO_CHAR CSYSDATE) ); END; Как видите, у оператора EXECUTE IMMEDIATE исключительно простой и удобный синтаксис. Инструкция OPEN FOR Эта инструкция впервые появилась в Огас1е7 и предназначалась для обработки переменных-курсоров. Затем ее синтаксис был расширен для поддержки много- многострочных динамических запросов. Выполнение такого запроса с помощью пакета DBMS_SQL является очень сложной процедурой: приходится производить синтак- синтаксический анализ и подстановку, отдельно определять каждый столбец, выпол- выполнять инструкцию, выбирать сначала строки, а затем — последовательно значения каждого столбца. Объем программы заметно увеличится. Для динамического SQL разработчики Oracle сохранили существующий син- синтаксис инструкции OPEN, но расширили его очень естественным образом: OPEN [перепетая курсор \ -.хост переменная курсор) FOR crpoKaSQL [USING аргумент [, аргумент]. . .]; Здесь переменная_курсор — слаботипизированная переменная-курсор; :хост_пере- ненная_нурсор — переменная-курсор, объявленная в хост-среде PL/SQL, например в программе Oracle Call Interface (OCI); crpoKaSQL - инструкция SELECT, подле- подлежащая динамическому выполнению; USING — такое же предположение, как в опе- операторе EXECUTE IMMEDIATE. О том, что такое переменная-курсор, говорилось в главе 14. Далее в этой главе мы подробно расскажем об его использовании с NDS.
Запросы с переменными-курсорами, возвращающие набор строк 541 В следующем примере объявляются тип REF CURSOR и основанная на нем пере- переменная-курсор, а затем с помощью оператора OPEN FOR открывается динамический запрос: CREATE OR REPLACE PROCEDURE show_parts_i inventory ( parts_table IN VARCHAR2, where_in IN VARCHAR2 := NULL) IS TYPE query_curtype IS REF CURSOR: dyncur query_curtype; BEGIN OPEN dyncur FOR 1 SELECT * FROM ' || parts_table ' WHERE ' || NVL (wherejn, '1-1'); Затем точно так же, как при использовании явного жестко закодированного курсора, осуществляются выборка строк, закрытие переменной-курсора и считы- считывание значений его атрибутов. В следующих разделах все это будет показано на примерах. Запросы с переменными-курсорами, возвращающие набор строк Теперь, когда вы знакомы с синтаксисом оператора OPEN FOR и с переменными- курсорами, рассмотрим более детально, как выполняются динамические запросы, возвращающие несколько строк. Ядро PL/SQL выполняет оператор OPEN FOR следующим образом: О связывает переменную-курсор с SQL-инструкцией, заданной в строке запроса; О вычисляет значения параметров подстановки и заменяет ими формальные па- параметры в строке запроса; О выполняет запрос; О идентифицирует результирующий набор; О устанавливает курсор на первую строку результирующего набора; О обнуляет счетчик выбранных строк, возвращаемый атрибутом IROWCOUNT. Обратите внимание, что параметры подстановки, заданные в предложении USING, вычисляются только при открытии курсора. Это означает, что для передачи тому же динамическому запросу другого набора параметров нужно выполнить новый оператор OPEN FOR. Для выполнения запроса, возвращающего набор строк, необходимо сделать следующее: О объявить тип REF CURSOR (если он еще не объявлен, например, в спецификации пакета) и на основе этого типа объявить переменную-курсор; О задав строку запроса и аргументы, открыть переменную-курсор; О с помощью оператора FETCH по одной извлечь записи результирующего набора;
542 Глава 15 • Динамический SQL и динамический PL/SQL О при необходимости проверить значения атрибутов (SFOUND, XNOTFOUND, SR0W- COUNT, SISOPEN); О закрыть переменную-курсор с помощью обычного оператора CLOSE. Далее приведена простая программа, которая отображает значения указанно- указанного поля заданной таблицы в строках, отбираемых с помощью предложения WHERE (столбец может содержать числа, даты или строки): /* Файл s web: showcol.sp */ CREATE OR REPLACE PROCEDURE showcol ( tab IN VARCHAR2. col IN VARCHAR2. whr IN VARCHARZ :- NULL) IS TYPE cv_type IS REF CURSOR: cv cv_type: val VARCHAR2C32767); BEGIN /* Формируем динамический запрос и открываем курсор */ OPEN cv FOR 'SELECT ' || col || 1 FROM ' || tab j ' WHERE ' 11 NVL (whr, '1 - Г): LOOP /* Выбираем следующую строку и прекращаем работу, если строк больше нет */ FETCH cv INTO val: EXIT WHEN cvSNOTFOUND: /* Выводим данные с заголовком перед первой строкой */ IF cvSRDWCOUNT - 1 THEN DBMS_OUTPUT.PUT_LINE (RPAD ('-'. 60, '-')); DBMS_OUTPUT.PUT_LINE ( 'Contents of ' || UPPER (tab) || '.' || UPPER (col)); DBMS_OUTPUT.PUT_LINE (RPAD ('-', 60, '-')); END IF; DBMS OUTPUT.PUTJJNE (val); END LOOP: CLOSE cv; -- Работа программы завершена, закрываем курсор! END; Вот каким будет результат выполнения этой процедуры: SQL> exec showcol Гетр1, 'ename1, 'deptno=10'J Contents of EMP.ENAME CLARK KING MILLER
Запросы с переменными-курсорами, возвращающие набор строк 543 Столбцы можно даже комбинировать: BEGIN showed ( ¦anp', ¦ename 11 "-$¦' 11 saV . ¦conrn IS NOT NULL'): ЕЮ; Contents of EMP.ENAME || '-$¦ || SAL ALLEN-S1600 WARD-$1250 MARTIN-S1250 TWER-$1500 Считывание данных в переменные или записи Инструкция FETCH в процедуре showcol, приведенной в предыдущем разделе, счи- считывает данные и присваивает их в качестве значений скалярной переменной. Если возвращаемые строки состоят из нескольких значений, можно присваивать их не- нескольким отдельным переменным: DECLARE TYPE cv_type IS REF CURSOR: cv cv_type: mega_bucks company.ceo_compensationlTYPE: achieved_by company.cost_cutti ngXTYPE; BEGIN OPEN cv FOR 'SELECT ceo_compensation. cost_cutting FROM company WHERE ' 1[ NVL (whr, '1 - 1'); LOOP FETCH cv INTO mega_buclcs, achieved_by; Однако с длинным списком переменных работать неудобно, не говоря уже о том, что переменные нужно объявить, затем следить за их синхронизацией с SQL-инструкцией и т. д. Встроенный динамический SQL позволяет считывать данные в запись, как показано в следующем примере: DECLARE TYPE cv_type IS REF CURSOR: cv cvjtype: ceojnfo companylROWTYPE; BEGIN OPEN cv FOR 'SELECT * FROM company WHERE ' || NVL (whr, '1 - 1'): LOOP FETCH cv INTO ceo info;
544 Глава 15 • Динамический SQL и динамический PL/SQL Конечно, инструкция SELECT * приемлема далеко не для всех случаев выборки данных — ведь таблица может содержать огромное количество столбцов, тогда как в коде в определенный момент может понадобиться лишь несколько значе- значений. Поэтому лучше создавать отдельные типы записей, соответствующих раз- разным требованиям. Их удобно поместить в пакет и использовать в разных частях кода приложения. Покажем, как создается такой пакет: CREATE OR REPLACE PACKAGE companyjtruc IS TYPE dynsql_curtype IS REF CURSOR; TYPE ceo_1nfo_rt IS RECORD ( mega_buclts company. ceo_compensation3;TYPE, achieved_by conpany.cost_cutting!STYPE); END company_strue; Теперь наш код можно переписать по-другому: DECLARE cur company_stnjc.dynsql_airtype: rec company_struc.ceo_info_rt: BEGIN OPEN cv FOR "SELECT ceo_compensation, cost_cutting FROM company WHERE ' || NVL (whr. '1 - 1'): LOOP FETCH cv INTO rec; Предложение USING в инструкции OPEN FOR В инструкции OPEN FOR, как и в EXECUTE IMMEDIATE, можно задавать параметры SQL- инструкции. Однако учтите, что при формировании запросов допускается приме- применение только параметров типа IN. Использование запросов с параметрами позво- позволяет значительно сократить количество готовых инструкций, каптируемых в SGA, и повысить вероятность того, что в момент обращения к инструкции таковая все еще будет находиться в глобальной системной области. Кроме того, применение таких запросов упрощает решение многих задач, благодаря чему оптимизируется программный код и облегчается его сопровождение. Вернемся к процедуре showcol, в которой можно задать произвольное предло- предложение WHERE. Конкретизируем наши требования к запросу: необходимо вывести (или обработать) информацию тех строк таблицы, которые содержат значения даты из определенного диапазона. Иными словами, нам нужно иметь возмож- возможность выполнить такой запрос: SELECT ename FROM emp WHERE hiredate BETWEEN x AND у: или же такой: SELECT flavour FROM favorlties WHERE preferencejeriod BETWEEN x AND y;
Запросы с переменными-курсорами, При этом очень важно, чтобы в условии WHERE не учитывался компонент вре- времени. Вот объявление процедуры, выполняющей эту задачу: /* Файл в web: showdtcol.sp */ PROCEDURE showcol С tab IN VARCHAR2. col IN VARCHAR2. dtcol IN VARCHAR2, dtl IN CKTE. dtZ IN DATE :- NULL) Ниже в инструкции OPEN FOR задается запрос с двумя формальными параметрами и соответствующее ему предложение USING: OPEN cv FOR 'SELECT ' || col || ' FROM ' j| tab j j 1 WHERE ' || dtcol || ¦ BETWEEN TRUNC (:startdt) AND TRUNC (:enddt)' USING dtl. NVL (dt2, dtl+lj; Параметры в предложении USING определены таким образом, чтобы в том слу- ' чае, если пользователь не задаст конечную дату, предложение WHERE вернуло строки с датой, указанной в параметре dtl. Остальная часть процедуры showcol не изме- изменилась, если не считать небольших модификаций, связанных с выводом заголов- заголовка столбца. Теперь при вызове новой версии этой процедуры потребуем вывода фамилий сотрудников, принятых на работу в 1982 году: BEGIN showcol Гетр'. 'ename1, 'hiredate'. 'Ol-jan-821, 131-dec-82'2: END; Contents of EMP.ENAME for HIREDATE between 01-0AN-82 and 31-DEC-82 MILLER Универсальная процедура для выполнения запросов с предложением GROUP BY Зам наверняка много раз приходилось писать запросы такого вида: SELECT столбцы. COUNT!*) FROM таблица «OUP BY столбцы: Существует разновидность данной инструкции с предложением HAVING, позво- шющая отобрать итоговую информацию по некоторому критерию. Это очень рас- гоостраненный вид запросов с группировкой. С помощью NDS достаточно про- то написать код формирования и выполнения такого запроса для любой таблицы, - также для любого столбца (или нескольких столбцов).
Ь4Ь Глава 15 • Динамический SQL и динамический kl/ЬЩ. Приведем заголовок этой процедуры: /* Файл в web: countby.sp */ PROCEDURE countBy ( tab IN VARCHAR2, col IN VARCHARZ. atleast IN INTEGER :- NULL. sch IN VARCHAR2 :- NJLL. maxlen IN INTEGER :- 30); Здесь tab — имя таблицы; col — имя столбца; atleast — имя схемы (по умолчанию используется схема USER); maxlen — параметр, используемый при форматировании выводимых данных. Ненулевое значение параметра sch указывает, что в инструк- инструкцию SELECT включается предложение HAVING С0ШТ(*) с условием «больше данного значения». Полный текст этой процедуры находится в файле countby.sp на узле издатель- издательства O'Reilly. Ниже приведена основная часть ее кода, за исключением строк фор- форматирования (строки заголовка и т. п.): IS ИРЕ cv_type IS REF CURSOR; cv cv_type: sql_string VARCHAR2 C2767) :- 'SELECT ' || CQl || r. C0UNT(*) FROM ' || NVL (sch. USER) || '.' || tab || 1 GROUP BY ' || col; v_val VARCHAR2 C2767); v_count INTEGER; BEGIN IF atleast IS NOT NULL THEN sql_string := sql_string || ' HAVING COUNT(*) >- ' |I atleast; END IF: OPEN cv FOR sql_string; LOOP FETCH cv INTO v_val, v_count; EXIT WHEN cvINOTFOUNQ; DBMS_OUTPUT.put_line (RPAD (v val. MAXLEN) || ' ' |l v_count): END LOOP; CLOSE cv; END: Подготовить несколько таких универсальных утилит достаточно просто, и их код получается довольно компактным. Для этой работы нужно только тщательно проанализировать процесс составления SQL-инструкции. Универсальный пакет для выполнения запросов с предложением GROUP BY Для проведения тестирования достаточно получить минимальный объем выво- выводимой информации, но ее, как правило, бывает недостаточно для дальнейшей
Запросы с переменными-курсорами, возвращающие набор строк обработки считываемых данных. Попробуем модифицировать процедуру countBy из предыдущего раздела таким образом, чтобы она сохраняла результаты выпол- выполнения динамического запроса в ассоциативном массиве для последующей обработки. Далее показана спецификация используемого для этой цели пакета: /* Файл в web: countby.pkg */ CREATE OR REPLACE PACKAGE grp IS TYPE results_rt IS RECORD С val VARCHAR2C4000). countby INTEGER); TYPE results_tt IS TABLE OF resu1ts_rt INDEX BY BINARYJNTEGER: FUNCTION countBy ( tab IN VARCHAR2. col IN VARCHAR2. atleast IN INTEGER :- NULL, sen IN VARCHAR2 :- NULL, roaxlen IN INTEGER :- 30) RETURN results_tt: END grp; Код функции countBy практически такой же, как код одноименной процедуры. Единственное отличие заключается в том, что теперь результаты запроса сохра- сохраняются в записи, которая затем добавляется в ассоциативный массив. Оба эти из- изменения внесены в цикл выборки строк: LOOP FETCH cv INTO rec: EXIT WHEN cvSNOTFOUND: retvai(cvlROWCOUNT) := rec; END LODP: Подготовив такой пакет, можно использовать его в собственном коде: /* Файл a web: countby.tst */ DECLARE results grp.results_tt ; indx PLSJNTEGER: minrow PLSJNTEGER; maxrow PLSJNTEGER; BEGIN results :- grp.countby С employee'. 'department_id'); /* Находим минимальное и максимальное значения */ indx :- results.FIRST; LOOP EXIT WHEN indx IS NULL: IF minrow IS NULL OR minrow > resultstindx).countby THEN minrow :- indx; END IF;
Глава 15 • Динамический SQL и динамический PL/SQL IF maxrow IS NULL OR maxrow < results(indx).countby THEN maxrow :- indx: END IF; DBMS_OUTPUT.PUT_LINE ( resu1tsAndx).val || ' - " II /* Выполняем другие операции обработки... */ /* Переходим к следующему значении */ indx .- results.NEXT(indx); END LOOP: END: Передача значений параметров Вы уже видели несколько примеров использования динамических SQL-инструк- SQL-инструкций с параметрами. Теперь давайте рассмотрим общие правила и особые случаи передачи значений этих параметров динамическому SQL-запросу. Подстановка и конкатенация Существуют два способа передачи значений в SQL-инструкцию: путем подста- подстановки значений параметров и с помощью конкатенации. Примеры использова- использования обоих способов показаны ниже. Подстановка параметров: EXECUTE IMMEDIATE 'UPDATE ' | | tab 'SET sal = :new sal' USING v_sal: Конкатенация: EXECUTE IMMEDIATE 'UPDATE ¦ | | tab 'SET sal - ' | | v sal: Подстановка требует использования формальных параметров и предложения USING, а при конкатенации значения вставляются прямо в SQL-строку. Попробу- Попробуем разобраться, в каких случаях какой подход лучше. Хотя подстановка значений и имеет некоторые ограничения, описанные в следующем разделе, мы рекоменду- рекомендуем по возможности использовать именно этот способ. Во-первых, подстановка параметров ускоряет работу приложения. SQL-инст- SQL-инструкция с параметрами содержит только формальные параметры (маркеры), отме- отмечающие местоположение значений, а не сами значения. Поэтому в одну инструк- инструкцию можно подставлять разные значения параметров, не меняя ее саму. И при этом приложение сможет пользоваться подготовленными курсорами, кэшируе- мыми в системной глобальной области базы данных (SGA).
Передача значений параметров 549 Во-вторых, метод подстановки заметно упрощает процесс создания и измене- изменения динамических SQL-инструкций. Задавая в такой инструкции формальные параметры, не нужно преобразовывать их в строковый тип данных — ядро NDS само сделает все, что нужно. А вот в случае конкатенации приходится писать очень сложные строковые выражения, потенциально являющиеся источниками ошибок, с множеством одинарных кавычек, вызовами функций TO_DATE и TOCHAR и т. д. Сравните, например, следующие инструкции EXECUTE IMMEDIATE, выполняю- выполняющие одну и ту же SQL-инструкцию: /* Использование парапетров подстановки*/ EXECUTE IMMEDIATE ¦UPDATE employee SET salary - .-val WHERE hirejate BETWEEN :1odate AND :h1date' USING v_startT v_end: /* Использование конкатенации */ EXECUTE IMMEDIATE ¦ 'UPDATE employee SET salary = ' || valjn || 1 WHERE hire_date BETWEEN ' |j 1 TO_DATE C" || TQ_CHAR (v_start) || '")' || 1 AND ' || 1 TO_DATE (' " || TO_CHAR (v_end) 11 ' ")'; Теперь понятно, почему лучше использовать подстановку параметров, но это возможно не во всех случаях. Ограничения на использование данного способа пе- передачи значений рассмотрены в следующем разделе. Ограничения на использование метода подстановки В качестве параметров подстановки SQL-инструкций можно использовать толь- только значения данных (литералов, переменных, выражений), но не имена объектов схемы базы данных (таблиц, столбцов и т. п.) Для задания этих элементов в дина- динамической SQL-инструкции должна использоваться конкатенация. Предположим, что нам нужна процедура, очищающая с помощью инструкции TRUNCATE заданную таблицу или представление. Попытка ее реализации может быть, например, такой: CREATE OR REPLACE PROCEDURE truncobj ( nm IN VARCHAR2, tp IN VARCHAR2 :- 'TABLE'. sch IN VARCHAR2 :- NULL) IS BEGIN EXECUTE IMMEDIATE 'TRUNCATE :trunc_type :obj_name' USING tp, NVL (sch, USER) || '.' || ran: END; На первый взгляд этот код правильный, однако, попытавшись его выполнить, мы получим сообщение об ошибке: ORA-03290: invalid truncate comand - missing CLUSTER or TABLE keyword
550 Глава 15 • Динамический SQL и динамический PL/SQL Упростим инструкцию, осуществляющую очистку таблицы: EXECUTE IMMEDIATE 'TRUNCATE TABLE :obj_name' USING ran; и все равно получим сообщение об ошибке: ORA-00903: invalid table name Почему же в NDS (впрочем, как и в пакете DBMSSQL) имеется такое ограниче- ограничение? При передаче строки оператору EXECUTE IMMEDIATE исполняющее ядро долж- должно прежде всего выполнить синтаксический анализ инструкции, чтобы убедиться в ее правильном определении. PL/SQL может понять, что следующая инструк- инструкция задана правильно, даже не эная значения параметра : xyz: 'UPDATE emp SET sal - :*yz' Но корректность инструкции, приведенной ниже, он определить не сможет: 'UPDATE emp SET :col_name - :*yz' Ведь если вместо формального параметра :col name будет подставлено значе- значение, не являющееся именем столбца, получится синтаксически неверная инст- инструкция. Поэтому в данном случае нужно использовать конкатенацию: CREATE OR REPLACE PROCEDURE truncobj С nm IN VARCHAR2. tp IN VARCHAR2 :- 'TABLE'. sen IK VARCHAR2 :- NULL) IS BEGIN EXECUTE IMMEDIATE 'TRUNCATE ' || tp || ' ' || NVL (sen. USER) || '.' [| run; END; Режимы использования параметров При передаче значений параметров SQL-инструкции можно использовать один из трех режимов: IN (только чтение, задан по умолчанию), OUT (только запись) и IN OUT (чтение и запись). Когда выполняется динамический запрос, все пара- параметры SQL-инструкции, за исключением параметра в предложении RETURNING, должны передаваться в режиме IN: CREATE OR REPLACE PROCEDURE wrongJncentive С companyjn IN INTEGER, newjayoffs IN NUMBER ) IS sql_string VARCHARZ(ZOOO): sal_after_layoffs NUMBER: BEGIN sql_string :- 'UPDATE ceo_compensation SET salary - salary + 10 * :layoffs WHERE companyid - :company RETURNING salary INTO :newsal'; EXECUTE IMMEDIATE sql_string
Передача значений параметров 551 USING newjayoffs, сапрапу_1п, OUT sal_after_layoffs; [)BMS_OUTPUT.PUT_LINE С 'CEO compensation after latest round of layoffs $' || sal_after_layoffs): END; Параметры подстановки SQL-инструкции, передаваемые в режимах OUT и IN OUT, используются в первую очередь при выполнении динамического PL/SQL. В этом случае режимы передачи параметров могут быть такими же, как у обыкно- обыкновенной программы PL/SQL. Ниже приведено несколько общих рекомендаций, касающихся использования предложения USING при выполнении динамического PL/SQL. О В качестве параметра подстановки, передаваемого в режиме IN, может быть за- задан любой элемент соответствующего типа: литеральное значение, именован- именованная константа, переменная или сложное выражение. Такой элемент сначала вычисляется, а затем передается в динамический блок PL/SQL. О Для значения параметра динамической инструкции, задаваемого в режиме OUT или IN OUT, следует объявить переменную. О Значения можно подставлять только вместо тех параметров динамического блока PL/SQL, типы данных которых поддерживаются SQL. Например, если параметр процедуры имеет тип BOOLEAN, его значение нельзя задать или счи- считать с помощью предложения USING. Рассмотрим несколько примеров. Вот заголовок процедуры с параметрами IN, OUT и IN OUT: PROCEDURE analyze_new_technology ( techjiame IN VARCHAR2. analysisjear IN INTEGER, number_of_adherents IN OUT NUMBER. projected_revenue OUT NUMBER. Поскольку процедура имеет четыре параметра, в предложении USING также долж- должно быть указано четыре элемента. Для первых двух параметров, передаваемых в режиме IN, следует задать литеральные значения или выражения, а следующие Два элемента должны быть именами переменных, так как для них заданы режимы OUT и IN OUT. Ниже, как вы видите, дан пример динамического вызова этой проце- процедуры: DECLARE devoted_followers NUMBER; est_revenue NUMBER; BEGIN EXECUTE IMMEDIATE 'BEGIN analyze_new_technology t:pl. :p2. :p3, :p4); END:' " USING 'Java', 2002. devoted_followers. est_revenue: END;
552 Глава 15 • Динамический SQL и динамический PL/SQL Дублирование формальных параметров При выполнении динамической SQL-инструкции связь между формальными и фактическими параметрами устанавливается в соответствии с их позициями Однако интерпретация одноименных параметров зависит от того, какой код, SQL или PL/SQL, выполняется с помощью оператора EXECUTE IMMEDIATE. О При выполнении динамической SQL-инструкции (DML- или DDL-строки, не оканчивающейся точкой с запятой) параметр подстановки нужно задать для каждого формального параметра, даже если их имена повторяются. О Когда выполняется динамический блок PL/SQL (строки, оканчивающейся точ- точкой с запятой), нужно указать параметр подстановки для каждого уникально- уникального формального параметра. Далее приведен пример динамической SQL-инструкции с повторяющимися формальными параметрами. Особое внимание обратите на повторяющийся пара- параметр подстановки val_in в предложении USING: CREATE OR REPLACE PROCEDURE updnumval ( coljn IN VARCHAR2, startjn IN DATE, endjn IN DATE, valjn IN NUMBER) IS dml_str VARCHAR2<32767) .- 'UPDATE emp SET ' || coljn || ' - :va! WHERE hiredate BETWEEN :lodate AND :hidate AND :val IS NOT NULL1; BEGIN EXECUTE IMMEDIATE dml_str USING valjn. startjn. endjn. val 1n: ENO: А вот динамический блок PL/SQL с повторяющимися формальными пара- параметрами — для него в предложении USING параметр val J п задан только один раз: CREATE OR REPLACE PROCEDURE updnurwal ( coljn IN VARCHAR2. startjn IN DATE, endjn IN DATE, valjn IN NUMBER) IS dml_str VARCHAR2C3Z767) :- 'BEGIN UPDATE emp SET ' || coljn || ' - :val WHERE hiredate BETWEEN .-lodate AND :h1date AND :val IS NOT NULL; END;1; BEGIN EXECUTE IMMEDIATE dml_str USING valjn. startjn, endjn: END:
Работа с объектами и коллекциями 553 Передача значения NULL При передаче значения NULL в качестве параметра подстановки, например, как в операторе EXECUTE IMMEDIATE ¦UPDATE employee SET salary - :newsal WHERE hire_date IS NULL1 USING NULL; вы получите сообщение об ошибке ORA-00457: in USING clause, expressions have to be of SQL types которое означает, что NULL не имеет типа данных и поэтому не является значени- значением одного из типов данных SQL. Так каким же способом можно передать в динамически выполняемый код зна- значение NULL? Для этого существует две возможности. Во-первых, можно скрыть данное значение в переменной, для чего проще все- всего использовать неинициализированную переменную подходящего типа: DECLARE /• Исходным значением переменной по умолчанию является NULL */ no_salary_when_fired NUMBER; BEGIN EXECUTE IMMEDIATE 'UPDATE employee SET salary - :newsal WHERE hire_date IS NULL' USING no_salary_when fired; END; Во-вторых, с помощью функции преобразовании типов явно можно преобра- преобразовать значение NULL в типизированное значение: BEGIN EXECUTE IMMEDIATE 'UPDATE employee SET salary - :newsal WHERE hire_date IS NULL1 USING TOJUMBER (NULL); END; Работа с объектами и коллекциями Одним из важнейших преимуществ динамических SQL-запросов по сравнению с пакетом DBMS_SQL является поддержка типов данных, которые были введены в Огас1е7 и последующих версиях РСУБД, в частности объектов и коллекций. Предположим, что требуется спроектировать систему администрирования кор- корпорации HealthS.Com, работающей в сфере здравоохранения. Для минимизации затрат база данных должна быть распределенной, поэтому на каждом региональ- региональном сервере должны храниться таблицы с информацией о клиентах специализи- специализированного госпиталя HealthS.Com.
554 Глава 15 • Динамический SQL и динамический PL/SQL Начнем мы с определения объектного типа (person) и типа VARRAY (preexi- sting_conditions): CREATE TYPE person AS OBJECTC паше VARCHAR2 E0). dob DATE, income NUMBER); CREATE TYPE preexistingjjonditions IS TABLE OF VARCHAR2 B5): Далее можно создать пакет для управления важнейшей информацией — дан- данными, необходимыми для контроля доходов корпорации HealthS.Com. Приведем его спецификацию: /* Файл в web: healths.pkg */ CREATE OR REPLACE PACKAGE healths AS PROCEDURE setupjiewjiospital (hospjiame IN VARCHARZ); PROCEDURE add_profit_source ( hospjiame IN VARCHAR2, pers IN person. cond IN preexistingjjonditions ); PROCEDURE weed_out_poor_and_sick ( hospjiame VARCHAR2, minjinccme IN NUMBER :- 100000, max_preexist_cond IN INTEGER :- 1 ); PROCEDURE show_profit_centers (hospjiame VARCHAR2): END healthS; Пакет позволяет создать новую таблицу, которая будет хранить информацию о госпитале. Вот как данная задача может быть реализована в теле пакета: FUNCTION tabname (hospjiame IN VARCHAR2) RETURN VARCHAR2 IS BEGIN RETURN hospjiame || 'jjrofitjrenter'; END; PROCEDURE setup jiewjiospital (hospjiame IN VARCHARZ) IS BEGIN EXECUTE IMMEDIATE 'CREATE TABLE ' || tabname (hospjiame) || ' ( pers Person. cond preexistingjxnditions) NESTED TABLE cond STORE AS condjsf: END: Кроме того, данный пакет предоставляет возможность добавлять в таблицу за- записи о потенциальных клиентах, включая сведения о состоянии их здоровья. Ни- Ниже показано, как в теле пакета реализована эта задача: PROCEDURE addjjrofitjsource ( hospjiame IN VARCHAR2, pers IN person,
Работа с объектами и коллекциями 555 cond IN preexisting_conditions ) IS BEGIN EXECUTE IMMEDIATE 'INSERT INTO ' || tabname (hospjiame) || 1 VALUES (rrevenuegenerator. :revenue_lnhibitors)' USING pers. cond: END; Вьшолняемые объектами и коллекциями функции совершенно очевидны. В дан- данном случае можно использовать и скалярные значения, но ни синтаксис, ни код от этого не изменятся. Пакет позволяет удалить из списка всех пациентов, страдающих многими за- заболеваниями или имеющих низкие доходы, что сведет к минимуму финансовый риск корпорации. Эта программа самая сложная: PROCEDURE weed_out_poor_and_sick t hospjiame VARCHAR2, minjincome IN NUMBER :- 100000. max_preexist_cond IN INTEGER :- 1 ) IS cv refcurtyp: human person; knownbugs preexisting_conditions: vjable VARCHARZ C0) :- tabname (hospname); v_rowid ROWID: BEGIN /* Находин все записи о больных, у которых количество заболеваний превышает допустимое значение или доход ниже определенного уровня. и удаляем их из таблицы. */ OPEN cv FOR 'SELECT ROWID. pers. cond FROM ¦ |l v_table || : ' alias WHERE (SELECT COUNTt*) FROM TABLE (alias.cond» > ipreexist OR alias.pers.income < :income' USING max_preexist_cond. minjncome; LOOP FETCH cv INTO v_rowid. human. known_bugs: EXIT WHEN cvSNOTFOUND; EXECUTE IMMEDIATE 'DELETE FROM ' || vjable || 1 WHERE ROWID - :r1d' USING v rowid; END LOOP: CLOSE cv: END:
556 Глава 15 • Динамический SQL и динамический PL/SQL ПРИМЕЧАНИЕ — Мы извлекаем значение ROWID каждой строки, чтобы потом ее легче было идентифицировать при удалении. Гораздо удобнее было бы добавить в запрос предложение FOR UPDATE, а затем исполь- использовать в инструкции DELETE предложение «WHERE CURRENT OF cv», однако это невозможно, по- поскольку для ссылки на переменную-курсор в динамической SQL-инструкции необходимо, чтобы эта переменная была глобальной. А объявить ее в пакете нельзя, потому что у нее нет постоянного значения. Подробнее об этом рассказано далее, в разделе «Динамический PL/SQL». Разработка приложений с использованием NDS Вы уже хорошо понимаете, как работает динамический SQL в PL/SQL. Теперь мы рассмотрим некоторые вопросы, касающиеся разработки приложений с ис- использованием NDS. Совместное применение программ с правами вызывающего Автор на собственном опыте убедился в удобстве использования NDS и разрабо- разработал на его основе множество универсальных функций и процедур, осуществляю- осуществляющих следующие задачи: О выполнение произвольных DDL-инструкций; О подсчет количества строк в любой таблице; О подсчет количества строк, группируемых в соответствии с содержимым задан- заданного столбца. Посчитав эти утилиты достаточно полезными, он откомпилировал их для схе- схемы COMMON и предоставил права на их выполнение EXECUTE как PUBLIC. Однако осталась нерешенной одна важная проблема. Если не используется модель прав вызывающего, описанная в главе 20, пользователь Sandra регистри- регистрирующийся в схеме SANDRA и выполняющий команду SQL> exec COMMON. execDDL C'create table trap (x date)'); создает таблицу в схеме COMMON. Применение названной модели означает, что соз- создаваемые пользователем хранимые программы будут выполняться от имени и с правами вызывающей схемы, а не той, для которой они определены (как происхо- происходит по умолчанию в Огас1е8.1 и что было единственно возможным до появления данной версии РСУБД). К счастью, воспользоваться этой новой возможностью очень просто. Ниже при- приведена версия процедуры execDDL, выполняющей любую DDL-инструкцию, но все- всегда в вызывающей схеме: CREATE OR REPLACE PROCEDURE execDDL tddl_string IN VARCHAR2) AUTHID CURRENT USER IS BEGIN EXECUTE IMMEDIATE ddl_string; END;
разработка приложений с использованием NDS 557 Мы советуем включать предложение AUTHID CURRENTJJSER во все программы, выполняющие динамические SQL-инструкции, и в частности те, которые вы пла- планируете использовать совместно с другими разработчиками. Обработка ошибок Любое надежное приложение должно осуществлять перехват и обработку оши- ошибок. Особенно важно их обнаружить и исправить при выполнении инструкций динамического SQL. Возможно, вам придется объединять в запросе список полей со списком таблиц и с предложением WHERE, изменяющимся при каждом выполне- выполнении инструкции. Все эти элементы необходимо разделять запятыми, оператора- операторами AND, OR и т. п. А что произойдет, если вы где-то допустите ошибку? Oracle, ко- конечно же, выдаст сообщение о ней. Обычно такая информация помогает выявить проблему, но часто оказывается недостаточной. Представим, что вы проектируете очень сложное приложение, в коде которого используются динамические SQL-за- SQL-запросы. При этом вы настолько хорошо освоили динамический SQL, что вам ни- ничего не стоит сразу написать любой оператор EXECUTE IMMEDIATE или OPEN FOR. И вот после программирования логики программы и вставки стандартного кода обра- обработки исключений, выводящего сообщение об ошибках, вы хотите проверить ре- результат, для чего пишете сценарий тестирования, выполняющий весь код, и по- помещаете его в файл testall.sql (который можно найти на web-узле издательства O'Reilly). Затем, испытывая понятное волнение, запускаете тест: SQL> etestall и видите следующее: ОНА-00942: table or view does not exist QRA-00904: invalid column name ORA-00921: unexpected end of SQL coimand ORA-00936: missing expression Возникает вопрос: что же делать с этим перечнем ошибок и как понять, к ка- какой инструкции относится каждая из них? Рекомендуем заранее принять следующие меры, чтобы не тратить массу вре- времени на отладку написанных динамических инструкций: О обязательно включать в программы, выполняющие операторы EXECUTE IMME- IMMEDIATE и OPEN FOR, секцию обработки исключений; О в каждом обработчике записывать в журнал или выводить на экран сообщение об ошибке и SQL-инструкцию; О для наблюдения за формированием и выполнением динамического кода по- поместить перед каждой инструкцией операторы трассировки. Чтобы показать, как эти рекомендации можно выполнить в конкретной про- программе, модифицируем процедуру execDDL. Вот ее первоначальный текст: CREATE OR REPLACE PROCEDURE execDDL (ddl_str1ng IN VARCHAR2) AUTHID CURRENTJJSER IS BEGIN EXECUTE IMMEDIATE ddl string: END-
558 Глава 15 • Динамический SQL и динамический PL/SQL Теперь добавим в нее секцию обработки исключений: /* Файл в web: execddl.sp */ CREATE OR REPLACE PROCEDURE execDDL Cddl_string IN VARCHAR2) AUTHID CURRENT_USER IS BEGIN EXECUTE IMMEDIATE ddl_string; EXCEPTION WHEN OTHERS THEN DBMS OUTPUT.PUTJ.INE ('Dynamic SQL Failure: ' || SQLERRM): DBMS"OUTPUT.PUT_LINE ( on statement: "' 11 ddl_string || ""); RAISE: END; Если, задав инструкцию с неверным синтаксисом, вы попытаетесь создать с ее помощью таблицу, то получите следующее сообщение: SQL> exec execddl ('create table x') Dynamic SOL Failure: ORA-00906: missing left parenthesis on statement: "create table x" Это простой пример, в реальном приложении вам, конечно, потребуется нечто более сложное, чем пакет DBMS_OUTPUT. ПРИМЕЧАНИЕ- Если динамический запрос, выполняемый средствами DBMS_SQL, не пройдет синтаксический ана- анализ и вы явно не закроете курсор в разделе исключений, данный курсор останется открытым. После ряда таких операций может быть сгенерирована ошибка «ORA-01000; maximum open cursors exceeded». При использовании NDS этого не произойдет, так как переменные-курсоры, объявлен- объявленные в локальном блоке, автоматически закрываются при выходе из этого блока, а занимаемая ими память освобождается. Процедура execDDL может выполнять не только DDL-инструкции. Она подхо- подходит для выполнения любой строки кода SQL, не требующей применения предло- предложения USING или INTO. Для этих целей предлагается применять и программу, кото- которая может и должна использоваться вместо оператора EXECUTE IMMEDIATE, поскольку в нее встроен необходимый код обработки ошибок. Такая процедура находится в пакете ndsutil (см. ниже раздел «Пакет NDS Utility»). Подобную программу можно создать и для оператора OPEN FOR — опять же, только для ситуаций, в которых нетрудно обойтись без предложения USING. По- Поскольку оператор OPEN FOR устанавливает значение курсора, его, конечно, следует реализовать как функцию, возвращающую слабый тип REF CURSOR: PACKAGE ndsutil IS TYPE cvjype IS REF CURSOR: FUNCTION openFor (sql_string IN VARCHAR2) RETURN cv_type: END; Пакет NDS Utility, который можно найти на узле O'Reilly в файле ndsutil.pkg, содержит полную реализацию функции openFor, очень похожей на приведенную выше процедуру execDDL.
Разработка приложений с использованием NDS 559 Динамический PL/SQL Динамический PL/SQL открывает перед программистом ряд интереснейших воз- возможностей. Во время выполнения приложения с помощью NDS можно: О создавать программу, в том числе пакет, содержащий глобальные структуры данных; О получать и модифицировать значения глобальных переменных по их именам; О вызывать функции и процедуры, имена которых не известны во время компи- компиляции. Автор использовал эту технологию при разработке очень мощных генераторов кода, которые динамически формируют вычислительные блоки и множество дру- других компонентов программ. Динамический PL/SQL обеспечивает высокий уро- уровень гибкости, но реализовать предоставляемые им возможности одновременно и сложно, и невероятно интересно. Ниже приведено несколько правил и советов, которые пригодятся вам при ра- работе с динамическими блоками PL/SQL. О Динамическая строка должна быть допустимым блоком PL/SQL, начинаться с ключевого слова DECLARE или BEGIN, а завершаться оператором END и точкой с запятой. (Строка, заканчивающаяся по-другому, не считается кодом PL/SQL.) О В динамическом блоке доступны только глобальные элементы PL/SQL (функ- (функции, процедуры и другие элементы, объявленные в спецификации пакета). Ди- Динамические блоки PL/SQL выполняются вне локального внешнего блока. О Ошибки, возникающие в динамическом блоке PL/SQL, могут быть перехваче- перехвачены и обработаны локальным блоком, в котором с помощью оператора EXECUTE IMMEDIATE выполнялись динамические инструкции SQL. Рассмотрим данные правила подробнее. Сначала напишем маленькую утили- утилиту для выполнения PL/SQL-кода: /* Файл в web: dynplsql.sp */ CREATE OR REPLACE PROCEDURE (tynPLSQL Cblk IN VARCHAR2) IS BEGIN EXECUTE IMMEDIATE 'BEGIN ' || RTRIM (blk. ':') || ': END:1: END: При реализации приведенной программы учитывался ряд правил, относящих- относящихся к выполнению кода PL/SQL. Заключение полученной процедурой строки ме- между операторами BEGIN... END гарантирует, что она будет выполнена как допусти- допустимый блок PL/SQL. Например, следующая команда динамически выполняет про- процедуру calc_totals; SQL> exec dynPLSQLC'caicJiotais1); Теперь с помощью этой программы определим, на какие структуры данных можно ссылаться из динамического блока PL/SQL. В следующем анонимном
560 Глава 15 • Динамический SQL и динамический PL/SQL блоке динамический PL/SQL используется для присвоения значения 5 локаль- локальной переменной пит: «dynamic» DECLARE пит NUMBER: BEGIN dynPLSQL Спит :- 5'): END; Приведенная строка выполняется в собственном блоке BEGIN.. .END, который кажется вложенным в блок dynamic. Однако при выполнении этого сценария мы получим следующие сообщения об ошибках: PLS-00201: identifier 'NUM' must be declared ORA-06512: at "SCOTT.DYNPLSQL11, Tine 4 Ядро PL/SQL не может разрешить ссылку на переменную с именем пит, при- причем такое сообщение об ошибке появится даже в том случае, если мы уточним имя переменной именем блока: «dynamic» DECLARE num NUMBER; BEGIN /* Вызывает ошибку PLS-00302! */ dynPLSOL ('dynamic.num :-5'); END; Предположим, что переменная пит определена в пакете dynamic: CREATE OR REPLACE PACKAGE dynamic IS num NUMBER: END; Теперь динамическое присваивание значения этой новой переменной будет выполнено успешно: BEGIN dynPLSQLС dynamic.пил :- 5'); END; Эти два фрагмента кода различаются между собой тем, что в первом случае переменная объявлялась локально в анонимном блоке PL/SQL, а во втором она была объявлена в пакете как общая глобальная переменная. . Получается, что динамически составленный и выполняемый блок PL/SQL не считается вложенным, он выполняется как процедура или функция, вызываемая из текущего блока. Поэтому любые переменные, локальные для текущего или внешнего блока, в динамическом блоке PL/SQL не доступны, из него можно ссы- ссылаться только на глобально определенные программы и структуры данных. Это отдельные функции и процедуры, а также структуры данных, объявляемые в спе- спецификации пакета. К счастью, несмотря на это, динамический блок выполняется в контексте вы- вызывающего блока. Это означает, что если в динамическом блоке инициируется
Разработка приложений с использованием NDS 561 исключение, оно перехватывается обработчиком исключений вызывающего бло- блока. Поэтому, если выполнить в SQL*Plus анонимный блок BEGIN dynPLSOL Cundefined.packagevar :- "abc"'): EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE (SOLCOOE); END; исключение будет обработано. ПРИМЕЧАНИЕ Присваивание в этом анонимном блоке представляет собой пример использования косвенной ссыл- ссылки — мы не ссылаемся на переменную явно, а задаем ее имя. В среде разработки Oracle Forms Builder (ранее называвшейся SQL*Forms или Oracle Forms) механизм косвенных ссылок реализован в программах NAME_IN и COPY. Он позволяет разработчикам программировать логику, совместно используемую всеми формами приложения. PL/SQL не поддерживает косвенных ссылок, но они мо- могут быть реализованы с помощью динамического PL/SQL. Пример такой реализации вы найдете в файле dynvar.pkg на узле O'Reilly. В следующих разделах приведено несколько примеров использования дина- динамического PL/SQL. Пример сокращения кода Приведем пример из реальной жизни. Разработчики одной из страховых компаний Чикаго, где автор работает консультантом, столкнулись с серьезной проблемой: объем кода программы, которая требовала постоянной модернизации, увеличил- увеличился настолько, что возникли трудности с ее компиляцией. Программа выглядела следующим образом: CREATE OR REPLACE PROCEDURE processjine (line IN INTEGER) IS BEGIN IF line - 1 THEN processjinel: ELSIF line - 2 THEN process_line2: ELSIF line - 514 THEN process Iine514; ELSIF line - 2057 THEN procesijine2057; END IF; END: Здесь значение в поле 1 i ne каждой строки таблицы задает одно из условий стра- страхования, разработанных компанией для достижения своей главной цели — мини- минимизации выплат по страховкам. Каждому значению соответствовала отдельная программа processl 1 ne, в которой выполнялись необходимые расчеты. Поскольку количество условий постоянно увеличивалось, программа становилась все боль- больше и больше. Это типичный пример немасштабируемого приложения. В подобных случаях программист должен найти повторяющийся програм- программный код и, если таковой будет выявлен, на его основе создать программу для многократного использования или конструкцию динамического PL/SQL. Решая данную задачу, автор воспользовался пакетом DBMS_SQL, но гораздо боль- больше подходит для этих целей динамический PL/SQL CREATE OR REPLACE PROCEDURE process line dire IN INTEGER) IS
562 Глава 15 • Динамический SQL и динамический PL/SQL BEGIN EXECUTE IMMEDIATE 'BEGIN processjine1 || line 11 ': END:": END: Теперь тысячи строк кода превратились в один исполняемый оператор! Ко- Конечно, в большинстве случаев идентифицировать шаблон и реализовать его в виде динамического PL/SQL очень непросто, но получаемый результат всегда стоит затраченных усилий. Универсальная функция для вычислений Представим, что мы создали приложение с графическим интерфейсом, предна- предназначенное для выполнения математических расчетов. Пользователю достаточно выбрать формулу, задать аргументы — и результат готов. Приложение поддержи- поддерживает несколько десятков разных формул, принимающих от одного до пяти аргу- аргументов, и каждая из них возвращает единственное значение. Для всех формул можно было бы создать по отдельному окну. Однако это ре- решение не только трудоемкое, но и неэффективное с точки зрения расширения приложения — ведь если вам понадобится добавить новую формулу, придется проектировать новое окно. Видимо, лучше разработать такой код, чтобы пользо- пользователь сам мог добавлять окно, когда ему потребуется новая формула. Чтобы решить эту задачу, можно создать набор таблиц базы данных с необхо- необходимой для расчетов информацией (описание формул, имена функций, количест- количество их аргументов, описание каждого аргумента и т. д.). Предположим, что такие таблицы уже созданы и осталось написать утилиту, выполняющую заданные вы- вычисления. Воспользуемся для этого динамическим PL/SQL. Ниже приведен заголовок функции, принимающей до пяти аргументов и вы- выполняющей заданное действие: /* Файл в web: dyncalc.sf */ CREATE OR REPLACE FUNCTION dyncalc ( operjn IN VARCHAR2, nargsjn IN INTEGER argljn IN VARCHAR2 arg2Jn IN VARCHARZ arg3_in IN VARCHARZ arg4_in IN VARCHAR2 arg5_1n IN VARCHAR2 RETURN VARCHAR2 = 0. - NULL. - NULL. - NULL, - NULL. - NULL В этом случае используется оператор EXECUTE IMMEDIATE, находящийся в каскад- каскадном операторе IF. Вот часть тела функции: ELSIF nargsjn = 2 THEN EXECUTE IMMEDIATE v_code || 4:1. :2); END;1 USING OUT retval, argljn. arg2_in; ELSIF nargsjn - 3 THEN EXECUTE IMMEDIATE v_code || 4:1. :2. :3): END;1 USING OUT retval. argljn. arg2 in. arg3 in;
Пакет NDS Utility 563 Как видите, ничего сложного, и этот код прекрасно работает в SQL*Plus: SQL> BEGIN 2 DBMS_OUTPUT.PUTJ-INE (dyncalcCsysdate')): 3 DBMS_OUTPUT.PUT_LINE (dyncalcC'power'. 2. 2, 44)); 4 DBMSJMJTPIJT.PUT LINE ( 5 dyncaic ('greatest1. 5. 66, 5. 88. 1020. -4)): 6 END: 7 / 05-MAY-99 17592186044416 1020 У динамических функций имеются определенные ограничения, которые мож- можно преодолеть, если вместо набора жестко закодированных параметров (от arglin до a rg5_i n) использовать единственный массив. Эта технология описана в файле dyncalc.pkg, который вы найдете на узле издательства O'Reilly. Еще более интересный пример применения динамического PL/SQL содер- содержится в файле str2l!st.pkg. В нем реализована универсальная программа синтакси- синтаксического анализа строк, выбирающая из строки с разделителями отдельные эле- элементы и помещающая их в заданную коллекцию. Пакет NDS Utility Чтобы облегчить использование универсальных утилит, описанных в данной гла- главе, автор создал единый пакет NDS Utility и также разместил его, в виде файла с именем ndsutil.pkg, на узле издательства O'Reilly. Содержащиеся в данном пакете программы перечислены в табл. 15.1. Таблица 15.1. Содержимое пакета NDS Utility Имя Описание execlmmed Заменяет оператор EXECUTE IMMEDIATE без предложений USING и INTO; включает обработку ошибок openFor Заменяет оператор OPEN FOR без предложения USING; включает обработку ошибок tabCount Возвращает количество строк в указанной таблице, заданных с использованием необязательного предложения WHERE countBy Возвращает количество строк в таблице для заданного выражения GROUP BY с необязательным предложением HAVING showCol Выводит содержимое одного столбца заданной таблицы dynPLSQL Выполняет динамическую строку PL/SQL, заключая ее внутрь блока с точкой с запятой в конце. Предложение USING не допускается Пакет определен с использованием модели разрешений вызывающего (AUTHID CURRENTJJSER). Это означает, что независимо от того, кто является владельцем па- пакета, разрешение внешних ссылок в динамическом SQL производится в соответ- соответствии с правами вызывающей схемы, а не владельца пакета.
564 Глава 15 • Динамический SQL и динамический PL/SQL Все программы содержат разделы исключений, в которых выводится инфор- информация об ошибке и «провинившейся» SQL-инструкции. Процедуры повторно ини- инициируют исключение, а функции обычно возвращают значение NULL или пустую структуру. Сравнение возможностей динамического SQL и пакета DBMS_SQL Динамический SQL является частью языка PL/SQL, поэтому он и рассматрива- рассматривается в нашей книге. Что касается D8MS_SQL, то это встроенный пакет, не имеющий непосредственного отношения к PL/SQL. Давайте попытаемся определить, есть ли вообще смысл использовать пакет 08MS_SQL, имея в распоряжении NDS. Визуальное сравнение эквивалентных программ Попробуем сравнить две функционально эквивалентные программы, выводящие список имен сотрудников, выбранных из таблицы на основании заданного произ- произвольного предложения WHERE. Первая процедура реализована с помощью пакета DBMS_SQL, а вторая — с помощью динамического SQL. Вот реализация первой процедуры: CREATE OR REPLACE PROCEDURE showemps (wherejn IN VARCHAR2 :- NULL) IS cur INTEGER :- DBMS_SQL.OPEN_CURSOR: rec emplDyeelROWTYPE: fdbk INTEGER; BEGIN DBMSJQL. PARSE (cur. 'SELECT employeejd, last_name FROM employee WHERE ¦ || NVL (wherejn. '1=Г), DBMSJ3QL.NATIVE); DBMS_SQL.DEFINE_COLUMN (cur. 1. 1): DBMS_SQL.DEFINE_COLUMN (cur. 2. user. 3D); fdbk; -.- DBMS_SQL.EXECUTE (cur); LOOP /* Выбираем следуницую строку. После просмотра всех строк выходим из цикла*/ EXIT WHEN DBMS_SQL.FETCH_ROWS (cur) - 0; DBMS_SQL.COLUMN_VALUE (cur. 1. rec.employeejd); OBHSJSQL.COLUMN J/ALUE (cur. 2. rec.lastjiame): uBMS_OUTPuT.PUT_LINE ( TO_CHAR (rec.enployeejd) || '-' || rec.lastjiame); END LOOP: DBMSJ3QL. CLJQSEJCURSOR (cur); END:
Сравнение возможностей динамического SQL и пакета DBMS_SQL 565 А это реализация процедуры с использованием динамически формируемых SQL-инструкций: CREATE OR REPLACE PROCEDURE showemps (wherejn IN VARCHAR2 :- NULL) IS TYPE cvjiyp IS REF CURSOR: cv cv_typ; v_1d employee.employeeJtffTYPE; v_nm emp1oyee.1ast_nameSTYPE: BEGIN OPEN cv FOR 'SELECT employee_id. lastjiame FROM employee WHERE ' || NVL (wherejn. '1-1'): LOOP FETCH cv INTO vjd. v_nm; EXIT WHEN cvSNOTFOUND: DBMS OUTPUT.PUT_LINE ( TO_CHAR (vjd) || '-' || v_nm); END LOOP: CLOSE cv: END: Как видите, во втором случае программа значительно упрощается, а объем кода уменьшается. И поскольку здесь используются «родные» элементы PL/SQL, а не программы встроенного пакета, этот код гораздо проще писать, читать и со- сопровождать. Какую технологию в каких случаях предпочтительнее использовать Если перед вами станет вопрос, какой из описанных технологий отдать пред- предпочтение, советуем остановить свой выбор на встроенном динамическом SQL, поскольку он: О всегда эффективнее, чем DBMS SQL (иногда его преимущества бывают очень зна- значительными); О гораздо проще в применении, а программный код на его основе короче и по- потенциально содержит меньшее количество ошибок (в этом вы имели возмож- возможность убедиться, рассмотрев примеры предыдущего раздела); О работает со всеми типами данных SQL, включая пользовательские объекты и коллекции (массивы VARRAY, вложенные таблицы и ассоциативные массивы), в отличие от пакета DBMS_SQL, работающего только с типами данных, совмести- совместимыми с Огас1е7; О позволяет выбирать несколько столбцов информации прямо в запись PL/SQL (при использовании DBMSSQL выборка производится в отдельные переменные). Если все это так, зачем же теперь нужен пакет DBMS_SQL? Оказывается, с его по- помощью можно выполнить задачи, которые NDS не по силам. О Пакет DBMSSQL поддерживает динамический SQL версии «Method 4», приме- применимый и для тех случаев, когда во время компиляции вы не знаете количество запрошенных столбцов и переменных привязки. Это самая сложная форма
566 Глава 15 • Динамический SQL и динамический PL/SQL динамического SQL, которую NDS не поддерживает. При использовании NDS элементы в предложениях USING и INTO кодируются жестко, поэтому если вы не знаете, сколько переменных подстановки будет использовано, написать пред- предложение USING для оператора EXECUTE IMMEDIATE будет непросто. ПРИМЕЧАНИЕ Последнее ограничение можно обойти, если составить не просто динамический запрос, а динамиче- динамический блок PL/SQL, содержащий динамическую строку запроса с переменным количеством элементов в предложениях USING и INTO. Однако это сложное решение требует выполнения оператора EXE- EXECUTE IMMEDIATE из динамической строки. О DBMS_SQL позволяет описать столбцы динамического курсора, задав информа- информацию о каждом из них в виде ассоциативного массива записей. Благодаря этому на основе данного пакета можно создавать предельно универсальный код. При- Примером использования такой возможности служат программы, приведенные в файле desccols.pkg. О С помощью DBMS_SQL можно выполнять код SQL и PL/SQL произвольной дли- длины, поскольку для его определения используются коллекции. Длина строки в случае применения NDS не должна превышать 32 Кбайт. О Программы пакета DBMSSQL можно вызывать из клиентского кода PL/SQL, на- например из библиотек Oracle Forms. При использовании NDS это делать невоз- невозможно, поскольку он не поддерживается версиями PL/SQL, применяемыми в средствах разработки. В заключение следует сказать, что NDS удовлетворяет от 80 до 90 % требова- требований, с которыми вам приходится сталкиваться при написании и выполнении ди- динамического кода. Однако полезно знать и о существовании пакета DBMS_SQL, ко- который может обеспечить ряд дополнительных возможностей.
Часть V Создание приложений PL/SQL Итак, вы уже умеете объявлять переменные и работать с ними, знаете, как осу- осуществлять обработку ошибок и создавать циклы. Словом, вы готовы приступить к разработке приложений. Именно этой теме и посвящена данная часть книги. Сначала мы научимся создавать процедуры, функции, пакеты и триггеры, а затем рассмотрим, как управлять приложением в целом. П Глава 16. Процедуры, функции и параметры ? Глава 17. Пакеты ? Глава 18. Триггеры ? Глава 19. Управление приложениями PL/SQL
16 Процедуры, функции и параметры > Модульный код > Процедура > Функция > Параметры > Локальные модули > Перегрузка модулей > Предобъявления > Дополнительные вопросы > Модульный подход — в жизнь! В предыдущих частях книга подробно рассматривались основные компоненты и кон- конструкции языка PL/SQL — курсоры, исключения, циклы, переменные и т. д. Од- Однако для создания хорошо структурированного, легкого для понимания и сопро- сопровождения приложения уметь работать со всеми этими компонентами недостаточ- недостаточно — нужно правильно скомпоновать все части кода. Далеко не все задачи, с которыми мы сталкиваемся в жизни, имеют линейный алгоритм решения. Только некоторые решения можно сразу понять и описать на бумаге или, скажем, средствами компьютера. Большинство проектируемых нами систем объемны и сложны, они включают много пересекающихся и даже кон- конфликтующих компонентов. Кроме того, пользователь требует и в итоге должен получить приложения, которые легки в применении и значительно мощнее своих предшественников. Достижение этой цели сопровождается еще большим услож- усложнением внутренней реализации приложений. Сейчас одной из самых важных задач в профессии программиста является уп- упрощение рабочей среды. Человек не способен работать подобно большому парал- параллельному компьютеру. Даже для самых ярких умов весьма сложно удерживать в голове пять-семь задач одновременно. Большие проекты мы должны разбивать на меньшие, лучше управляемые компоненты, а те, в свою очередь, декомпоно- вать на отдельные программы. И лишь создав и протестировав нужные програм- программы, мы сможем сконструировать из них полнофункциональные приложения. Если вы воспользуетесь нисходящим методом разработки или другой подобной методикой, то разделение кода на процедуры, функции и объектные типы позво- позволит вам создать надежное и легкое в сопровождении приложение.
Модульный код 569 Модульный код Модуляризация — это разбиение больших блоков кода на меньшие блоки (моду- (модули), которые можно вызывать из других модулей. Этот процесс аналогичен нор- нормализации данных и, обеспечивая те же преимущества, обладает рядом досто- достоинств. В частности, модуляризация позволяет повысить ряд характеристик кода. О Доступность для повторного использования. Разбивая большие программы на отдельные компоненты, которые работают совместно, вы увидите, что мно- многие модули используются более чем одной программой приложения. При пра- правильной организации эти программы-утилиты могут применяться не только вашим, но и другими приложениями. О Управляемость. Что удобней отлаживать: одну программу в 10 000 строк или пять отдельных программ по 2000 строк, которые вызывают друг друга при необходимости? Ответ однозначен: конечно, ум человека лучше приспособлен к решению небольших задач. К тому же при использовании второго подхода код можно протестировать и отладить на более низком уровне, то есть до того, как отдельные модули будут скомбинированы для проведения более сложных тестов. О Читабельность. Имена модулей в той или иной степени отражают их назначе- назначение. Чем больше кода вы переместите в программный интерфейс или скроете за ним, тем легче будет понять, что делает программа. Модуляризация дает возможность сосредоточиться на задаче в целом, а не на отдельных фрагмен- фрагментах кода. О Надежность. Если код разбит на модули, в нем меньше ошибок, а обнаружен- обнаруженные ошибки легче исправлять, так как они локализованы на уровне модуля. Кроме того, такой код легче сопровождать другим программистам, он имеет меньший объем и лучше читается. Поскольку вы уже изучили все имеющиеся в языке PL/SQL конструкции и умеете создавать циклы, ветвления и курсоры, то в принципе можете приступать к написанию программ. Однако вы не получите качественное приложение, если не умеете создавать и комбинировать модули. В PL/SQL выполнить разбиение кода на модули можно с помощью перечис- перечисленных ниже структур. О Процедура. Программа, которая осуществляет одно или несколько действий и вызывается как исполняемый оператор PL/SQL. Передавать данные проце- процедуре и получать их из нее можно с помощью списка параметров. О Функция. Программа, которая возвращает одно значение и используется как выражение PL/SQL. Передавать данные функции можно с помощью ее спи- списка параметров. О Триггер базы данных. Набор команд, который вызывается при наступлении в базе данных некоторого события (такого, как подключение к базе данных, модификация строки таблицы или DDL-операция).
570 Глава 16 • Процедуры, функции и параметры О Пакет. Именованный набор процедур, функций, типов и переменных. Пакет не является модулем, скорее, это мета-модуль, но он тесно связан с реализаци- реализацией модульного подхода. О Объектный тип или экземпляр объектного типа. Эмуляция объектно-ориен- объектно-ориентированного класса в Oracle. Объектный тип инкапсулирует состояние и пове- поведение данных, комбинируя их (как реляционная таблица) с правилами (про- (процедурами и функциями, которые манипулируют этими данными). Пакеты будут рассмотрены ниже, в главе 17, триггеры баз данных - в главе 18, а объектные типы — в главе 21. Сейчас мы сконцентрируем свое внимание на принципах создания процедур и функций, опишем списки их параметров. Термин «модуль» в данном контексте означает функцию или процедуру. Как и во многих других языках программирования, в PL/SQL модули могут вызы- вызывать другие именованные модули. Передавать информацию в модули и получать ее из них можно путем использования параметров. Кроме того, модульная структу- структура PL/SQL хорошо сочетается с реализацией обработчиков исключений и пре- предоставляет программисту все возможности по части обработки ошибок (см. главу 6). В этой главе описано, как объявляются процедуры и функции и как устанав- устанавливаются списки параметров для них. Кроме того, мы исследуем такие «экзоти- «экзотические» аспекты разработки программ, как локальные модули, перегрузка, пре- добъявления, а также детерминированные и табличные функции. Процедура Процедура — это модуль, который осуществляет одно или несколько действий. Поскольку вызов процедуры в PL/SQL является отдельным исполняемым опера- оператором, блок кода PL/SQL может состоять только из вызова процедуры. Процеду- Процедуры относятся к числу ключевых компонентов модульного кода, позволяющих раз- разработчикам оптимизировать код и повторно использовать его. Процедура PL/SQL имеет следующую структуру: PROCEDURE [схема.]иня [(параметр.[параметр ...])] [AUTHID | CURRENTJJSER] IS [объявления'] BEGIN исполняемые операторы [EXCEPTION обработчики исключений] END [имя]: Назначение каждого элемента структуры описано ниже. О схема - имя схемы, которой будет принадлежать процедура (необязательный аргумент). По умолчанию применяется имя схемы текущего пользователя. Ес- Если значение схема отлично от имени схемы текущего пользователя, этот поль- пользователь должен обладать привилегиями на создание процедуры в другой схеме. О имя — имя процедуры. Следует сразу же за ключевым словом PROCEDURE. О параметр — необязательный список параметров, которые применяются для пе- передачи данных в процедуру и возврата информации из процедуры в вызываю- вызывающую программу.
Процедура 571 О AUTHID | CURRENTJJSER — определяет, с какими полномочиями будет вызывать- вызываться процедура: создателя (владельца) или текущего пользователя. В первом случае выполнение процедуры осуществляется с правами создателя, во вто- втором — с правами вгизывающега. О объявления - объявления локальных идентификаторов для этой процедуры. При отсутствии таковых между ключевыми словами IS и BEGIN не будет ника- никаких выражений. О испопняемые операторы — операторы, которые выполняются процедурой при вы- вызове. Между ключевыми словами BEGIN и END или EXCEPTION должен находиться, по крайней мере, один исполняемый оператор. О обработчики исключений — обработчики исключений для процедуры. Не являют- являются обязательными. Если в процедуре не обрабатываются никакие исключения, слово EXCEPTION можно опустить и завершить исполняемый раздел ключевым словом END. На рис. 16.1 показан код процедуры applydiscount, который содержит все че- четыре раздела, характерных для именованного блока PL/SQL. PROCEDURE appiyjiscount (companyjdjn IN company.company_id%TYPE, discount in IN NUMBER) IS ml rediscount CONSTANT NUMBER :- 0.05: maxjiscount CONSTANT NUMBER :- 0.25; invalid discount EXCEPTION; Заголовок Рездел объявлений BEGIN IF discount_in BETWEEN min_discount AND max_discount THEN UPDATE item SET i tem_amount-i tem_amount*[1-discountj n) WHERE EXISTS (SELECT 'x' FROM order WHERE order.order_id = item.order_id AND order.companyjid - company_idjn): IF SQLSROWCOUNT = 0 THEN RAISE NO_DATA_FOUND: END IF; ELSE RAISE invalid_discount: END IF; Исполняемый развел EXCEPTION WHEN invalidjiscount • THEN DBMS_OUWr.PUT_LINE( 'Указана WHEN NO DATA FOUND THEN DBMS_OUTPUT.PUT_LINE( TO CHARtcompany id Для in)) неверная компании скидка1); •- нет заказов:' 11 Раздел исключений END apply_discount; Рис. 16.1. Код процедуры apply_discount
572 Глава 16 • Процедуры, функции и параметры Вызов процедуры Процедура вызывается как исполняемый оператор PL/SQL. Другими словами, ее вызов должен заканчиваться символом точки с запятой (;) и может предшество- предшествовать другим инструкциям SQL либо операторам PL/SQL (если таковые имеют- имеются) в исполняемом разделе блока PL/SQL или следовать за ними. Например, в приведенном ниже коде содержится вызов процедуры applydiscount: BEGIN apply_discount( new_company_id, 0.15); --15* скидки END: Если процедура не имеет параметров, она вызывается без круглых скобок: di splay_store_summary; В Oracle8i и более поздних версиях вызов процедуры может включать пустые скобки, как показано ниже: d1splay_store_sumary(): Заголовок процедуры Часть определения процедуры, которая расположена перед ключевым словом IS, называется ее заголовком. Здесь содержится вся информация, необходимая для вызова процедуры: О имя процедуры; О ключевое слово AUTHID (если это требуется); О список параметров (если он существует). Например, заголовок процедуры applydiscount, описанной в предыдущем раз- разделе, включает тип модуля, имя процедуры и список из двух параметров: PROCEDURE applydiscount (company_id_in IN company.company_1dfcTYPE. discountjn IN NUMBER) Как видите, для вызова процедуры не нужно знать, какова ее внутренняя реа- реализация. Тело процедуры Тело процедуры содержит код, требуемый для ее реализации, и состоит из разде- разделов объявлений, исполняемого и исключений. Весь код после ключевого слова IS является телом процедуры. Разделы исключений и объявлений не обязательны. Когда обработчики исключений не используются, ключевое слово EXCEPTION не указывается и код процедуры завершается ключевым словом END. Если в коде процедуры нет никаких объявлений, то непосредственно за ключевым словом IS следует ключевое слово BEGIN. Процедура должна содержать, по крайней мере, один исполняемый оператор. Соблюсти это условие обычно не составляет проблемы, наоборот, трудно обеспе- обеспечить эффективность кода процедуры в сочетании с его компактностью. Ведь с уве- • личением объема кода исполняемого раздела ухудшается его управляемость (в сле- следующих разделах этой главы данный вопрос рассматривается более подробно).
функции Дескриптор END После ключевого слова END можно добавить имя процедуры. Оно служит меткой, которая связывает конец процедуры с ее началом. Использовать дескриптор вме- вместе с ключевым словом END удобно в тех случаях, если код процедуры занимает более чем одну страницу или если она находится в теле пакета. PROCEDURE displ ^stores (regionjn IN VARCHAR2) IS BEGIN END display_stores; Оператор RETURN Ключевое слово RETURN обычно ассоциируется с функциями, поскольку они долж- должны возвращать значения (или инициировать исключения). Однако PL/SQL по- позволяет использовать оператор RETURN в процедурах. Версия этого оператора для процедур не принимает выражений и не может возвращать значений в вызываю- вызывающий программный модуль — она просто прекращает выполнение процедуры и пе- передает управление вызывающему коду. Такой способ использования оператора RETURN встречается нечасто, поскольку в этом случае создаются два или несколько выходов из процедуры, а это делает поток выполнения сложным для понимания и плохо управляемым. Если вы хо- хотите сохранить структурированность программных модулей, избегайте примене- применения операторов RETURN и GOTO. Функции Функция — это модуль, который возвращает значение. В отличие от вызова про- процедуры, представляющего собой отдельный оператор, вызов функции всегда яв- является частью оператора, то есть он включается в выражение или служит в каче- качестве значения по умолчанию, присваиваемого переменной при объявлении. Возвращаемое функцией значение принадлежит к определенному типу дан- данных. Функция может использоваться вместо выражения, которое имеет тот же тип данных, что и возвращаемое ею значение. Функции играют особенно важную роль в обеспечении модульного подхода. К примеру, реализация отдельного бизнес-правила или формулы в приложении должна помещаться в функцию. Любой запрос, возвращающий одну строку, так- также должен объявляться внутри функции, для того чтобы его можно было исполь- использовать повторно. ПРИМЕЧАНИЕ- Некоторые программисты функциям предпочитают процедуры, возвращающие информацию через список параметров. Если вы принадлежите к их числу, убедитесь, что поместили в процедуры биз- бизнес-правила, формулы и запросы, возвращающие одну строку. Если в приложении не определяются или не используются функции, то со временем его будет тяжело поддерживать и модифицировать.
574 Глава 16 • Процедуры, функции и параметры Структура функции Функция имеет почти такую же структуру, как и процедура, за исключением того, что предназначение ключевого слова RETURN в ней совсем иное. Структура функ- функции такова: FUNCTION [схема].имя [(.параметр [. параметр ...])] RETURN типвозвращаемыхданных [AUTHIO DEFINER | CURRENT.USER] ¦ [DETERMINISTIC] [PARALLEL ENABLE...] [PIPELINED] IS [объявления] BEGIN исполняемые операторы [EXCEPTION обработчики исключений'] END [имя]: Назначение каждого элемента структуры описано ниже. О схема — имя схемы, которой будет принадлежать процедура (необязательный аргумент). По умолчанию применяется имя схемы текущего пользователя. Ес- Если значение схемд отлично от имени схемы текущего пользователя, этот поль- пользователь должен обладать привилегиями на создание процедуры в другой схеме. О имя — имя процедуры. Следует сразу же за ключевым словом FUNCTION. О параметр — необязательный список параметров, которые применяются для пе- передачи данных функции и возврата информации из нее в вызывающую про- программу. О тип_воэвращаемых_Дднных — тип возвращаемого функцией значения. Это обяза- обязательная часть заголовка функции (описывается более детально в следующих разделах). О AUTHIO DEFINER | CURRENTJJSER — определяет, с какими полномочиями будет вы- вызываться процедура: создателя (владельца) или текущего пользователя. В пер- первом случае выполнение процедуры происходит с правами создателя, во вто- втором — с правами вызывающего. О DETERMINISTIC — используется для оптимизации и позволяет системе приме- применить сохраненную копию возвращаемого функцией значения, если это воз- возможно. Оптимизатор запросов может выбрать сохраненную копию или снова вызвать функцию. О PARALLELENABLE — используется для оптимизации и позволяет функции вы- выполняться параллельно в случае, когда она вызывается из инструкции SELECT. О PIPELINE — указывает, что результат табличной функции должен возвращаться построчно, с помощью оператора PIPE ROW.
Функции О объявления - объявления локальных идентификаторов для указанной функ- функции. Если таких идентификаторов нет, между ключевыми словами IS и BEGIN не будет никаких выражений. О исполняемые операторы - операторы, которые выполняются функцией при вызо- вызове. Между ключевыми словами BEGIN и END или EXCEPTION должен находиться, по крайней мере, один исполняемый оператор. О обработчики исключений — обработчики исключений для процедуры (не являют- являются обязательными). Если в процедуре не обрабатываются никакие исключе- исключения, слово EXCEPTION можно опустить и завершить исполняемый раздел ключе- ключевым словом END. На рис. 16.2. показаны код функции totsaies и ее структурные элементы. За- Заметьте, что данная функция не имеет раздела исключений. FUNCTION tot_sales (companyjdjn IN company.companyjcftTYPE, statusjn IN order.status jrodeSTYPE :- NULL) RETURN NUMBER Заголовок IS /* Представление кода состояния заказа а верхней регистре*/ statusjnt order.status_cade*TYPE : = UPPER(statusjn); /*Параметризированный курсор, возвращающий общий объем продаж со скидкой*/ CURSOR 5ales_cur (statusjn IN 5tatus_codeJ!TYPE) IS SELECT SUM (amount*discount) FROM item WHERE EXISTS (SELECT 'X1 FROM order WHERE order.orderjd - item.orderjd AND company jd = company_1d_in AND status_code LIKE statusjn): /¦Возвращаемое функцией значение*/ return value NUMBER; BEGIN Раздел объявлений OPEN sales_cur Cstatusjnt): FETCH sales_cur INTO return_value: IF sales_cur*NOTFOUND THEN CLOSE sales_cur; RETURN NULL: ELSE CLOSE sales_cur: RETURN return_value: END IF: END tot sales: Исполняемый раздел Рис. 16.2. Код функции tot_sales
Э/О I Sldbd ID - I 1[Л1Цедуры, функции и na^anGifjDt Типы возвращаемых данных Функция может возвращать данные любого типа, поддерживаемого PL/SQL, — от скаляров (единичных значений типа даты или строки) до сложных структур, таких как коллекции, объектные типы, переменные курсоры и LOB (Large Ob- Objects — большие объекты). С помощью функции нельзя возвращать исключения, поскольку в PL/SQL они не типизированы. Несколько примеров использования предложения RETURN вы найдете ниже. О Функция возвращает строку: CREATE OR REPLACE FUNCTION favoritejiickname ( name_in IN VARCHAR2) RETURN VARCHAR2 IS ... END; О Функция-член объектного типа возвращает значение типа DATE: CREATE TYPE pet_t IS OBJECT ( tag_no INTEGER. NAME VARCHAR2 F0). breed VARCHAR2Q00). dob DATE. MEMBER FUNCTION age (new_tag_no IN INTEGER) RETURN DATE ) О Возврат записи, имеющей ту же структуру, что и у таблицы books: CREATE OR REPLACE PACKAGE bookjnfo IS FUNCTION onerow(isbn_in IN books.isbnJTYPE) RETURN booksXROWTYPE; О Возврат курсорной переменной типа overdue_rct: CREATE OR REPLACE PACKAGE bookjnfo IS TYPE overdue_rt IS RECORD Cisbn books.isbnXTYPE. days_overdue PLSJNTEGER): TYPE overdue_rct IS REF CURSOR RETURN overdue_rt: FUNCTION overdue_info (user-name in IN libjjsers.usernameJTYPE) RETURN overdue ret: ~ ¦ Дескриптор END После ключевого слова END можно добавить имя функции. Оно служит меткой, которая связывает конец функции с ее началом. Использование вместе с END деск- дескриптора удобно в тех случаях, если код функции занимает более одной страницы или если она находится в теле пакета. FUNCTION tot sales (company in IN INTEGER) RETURN NUMBER IS BEGIN END tot sales:
Функции 577 Вызов функции Вызов функции может производиться из любого места исполняемого оператора PL/SQL, где допускается использование выражения. Из приведенных ниже при- примеров вы увидите, каким образом могут вызываться функции, определенные ра- ранее в разделе «Типы возвращаемых данных». О Установка значения переменной по умолчанию с помощью вызова функции: DECLARE vjiiclcname VARCHAR2U0O) : = favorite_nickname('Steven'); О Использование функции-члена для объектного типа pet_t в условии: DECLARE my_parrot pet_t :» pet_t A001. 'Mercury'. 'African Grey'. TO_DATE ('09/23/1996'. "rMM/DD/YYYY1)): BEGIN IF my_parrot.age < INTERVAL '50' YEAR -- тип INTERVAL THEN DBMS_OUTPUT.PUT_LINE ('Это еще молодая птица!1): END IF; О Вставка в запись строки с информацией о книге: DECLARE my_first_book booksSSROWTYPE: BEGIN my_first_boolc :- bookjnfo.onerow Cl-56592-335-91): О Получение информации о книгах с просроченной датой для конкретного поль- пользователя с помощью значения курсорной переменной: DECLARE щу overdue_info overdue_rct: BEGIN" my_overdue_info := book_info.overdue_info CSTEVENJEUERSTEIN1); О Вызов функции в инструкции CREATE VIEW и передача ей в качестве аргумента результирующего набора значений, возвращаемого курсорным выражением: CREATE OR REPLACE VIEW youngjnanagers AS SELECT managers.employeejd manager_employee_id FROM employees managers WHERE Most_Reports Before_Manager ( CURSOR (SELECT reports, hi re_date FROM employees reports WHERE reports.manager_id = managers.etnployee id ). managers.hire_date
578 Глава 16 ¦ Процедуры, функции и параметры Функции без параметров Если функция не имеет параметров, то в ее вызове не требуется использовать круглые скобки. Это показано ниже на примере метода age объектного типа pet_t: IF my_parrot.age < INTERVAL '50' YEAR -- тип INTERVAL Начиная с Oracle8i в вызов функции можно включать пустые скобки: IF my_parrot.ageO < ... Заголовок функции Часть определения функции, которая расположена перед ключевым словом IS, называется ее заголовком. В заголовке содержится вся информация, необходимая для вызова функции: О имя функции; О модификаторы определения и поведения функции; О список параметров; О тип возвращаемого значения. Как видите, для того чтобы вызвать функцию из внешней программы, про- программисту не требуется знать ее внутреннюю реализацию. Заголовок приведенной ранее функции tot_sales имеет следующий вид: FUNCTION tot_sales (company_id_in IN company.companyJdmPE, status in IN order.status codeSTYPE :- NULL) RETURN NUMBER Здесь указаны тип модуля, имя функции, список из двух параметров и тип возвращаемого значения — NUMBER. Таким образом, вызов функции tot_sal es мож- можно включать в любые выражения, использующие числовые значения, например: DECLARE v_sales NUMBER; BEGIN v_sales :-totjales A505, 'ACTIVE1); Тело функции Тело функции содержит код, требуемый для ее реализации, и состоит из разделов объявлений — исполняемого и исключений. Весь код после ключевого слова IS является телом функции. Разделы исключений и объявлений не обязательны. Когда обработчики исключений не используются, ключевое слово EXCEPTION не указывается и код функции завершается ключевым словом END. Если в коде функ- функции нет никаких объявлений, то непосредственно за ключевым словом IS следует ключевое слово BEGIN. Исполняемый раздел функции должен содержать оператор RETURN, хотя его от- отсутствие никак не проявляется на этапе компиляции. Если функция завершает свое выполнение без вызова оператора RETURN, Oracle инициирует следующую ошиб- ошибку (верный признак плохо сконструированной функции): ORA-06503: PL/SQL : Function returned without value
Функции 579 Оператор RETURN В исполняемом разделе функции должен находиться, по крайней мере, один опе- оператор RETURN, который задает возвращаемое ею значение. Операторов может быть и несколько, но в одном вызове функции должен выполняться только один из них. После обработки оператора RETURN выполнение функции прекращается, и управ- управление передается вызывающему блоку PL/SQL. Если ключевое слово RETURN в заголовке определяет тип данных возвращаемо- возвращаемого значения, то оператор RETURN в исполняемом разделе функции задает само это значение. Вы должны включить ключевое слово RETURN в заголовок и, кроме того, использовать хотя бы один оператор RETURN в теле функции. При этом тип дан- данных, указанный в заголовке, должен совпадать с типом данных выражения, воз- возвращаемого оператором RETURN, Возвращаемые значения Оператор RETURN может возвращать любое выражение, совместимое с типом дан- данных, который указан в заголовке. Это выражение может включать сложные вычис- вычисления, вызовы других функций, в частности функций преобразования данных: RETURN 'buy me lunch': RETURN POWER (maxjalary, 5): RETURN A00 - pct_of_total_salary (employeejd)); RETURN TO_DATE C'01' || earliestjnonth || mifial^year, •DDMMYY'); Оператор RETURN может возвращать и сложные структуры данных, такие как экземпляры объектных типов, коллекции и записи. Выражения, указанные в операторе RETURN, вычисляются при его вызове, а ре- результаты передаются вызывающему блоку в процессе передачи управления. Использование нескольких операторов RETURN В функции tot_sal es, показанной на рис. 16.2, используются два оператора RETURN. Они позволяют задать следующее поведение функции. Если из курсора нельзя получить информацию о продажах, то возвращается NULL (не путайте с нулевым значением). Но если значение из курсора получено, оно передается вызывающей программе. Таким образом, оператор RETURN передает либо значение NULL, либо значение переменной return_value. Несмотря на то что исполняемый раздел может содержать несколько операто- операторов RETURN, в большинстве случаев рекомендуется использовать только один та- такой оператор, в последней строке раздела. Оператор RETURN как последний исполняемый оператор Если вы хотите всегда быть уверены в том, что созданная вами функция возвра- возвращает значение, используйте оператор RETURN в качестве последнего исполняемого ее оператора. Объявите переменную return_val ue (имя переменной указывает, что в ней содержится возвращаемое функцией значение), напишите весь код, в кото- котором вычисляется возвращаемое значение, а затем, в самом конце кода функции, верните его с помощью оператора RETURN: FUNCTION do_H_all (parameterjist) RETURN NUMBER IS return value NUMBER;
580 Глава 16 • Процедуры, функции и параметры BEGIN ... исполняемые операторы ... ¦ RETURN return_value; END: Как избежать использования нескольких операторов RETURN в функции totsa- les, показано ниже: IF sales_curJ!NOTFOUND THEN returnj/alue:-NULL; END IF; CLOSE sales_cur; RETURN return value; Параметры Для передачи информации между модулем (процедурой или функцией) и вызы- вызывающим блоком PL/SQL используются параметры. Они относятся к числу не менее важных компонентов модуля, чем находящиеся в нем исполняемые опера- операторы. Ведь если список параметров не понятен или плохо составлен, то другим программистам сложно применять такой модуль, и они наверняка откажутся от этой идеи. В таком случае уже будет неважно, насколько хороша внутренняя реа- реализация модуля. Ниже приведены рекомендации, которые, как мы надеемся, помогут вам пра- правильно определить список параметров модуля. О Количество параметров. Если процедура или функция имеет слишком мало параметров, это может послужить ограничением для ее использования. К ана- аналогичному результату приводит и ситуация, когда параметров слишком мно- много. Конечно, число параметров сильно зависит от характера задачи, однако для оптимального их представления можно использовать разные способы (напри- (например, связывание параметров в одну запись). О Типы параметров. При выборе типа необходимо учитывать, для каких целей будут использоваться параметры: только для чтения, только для записи либо для чтения и записи. О Имена параметров. Параметры следует называть так, чтобы имена отражали их назначение в модуле и были легки для восприятия. О Значения параметров по умолчанию. Обратите внимание на то, каким обра- образом устанавливаются значения по умолчанию. Решите, когда использовать зна- значения по умолчанию, а когда нужно заставить программиста ввести опреде- определенное значение. PL/SQL предлагает большой арсенал возможностей для создания и эффек- эффективного использования параметров. Мы ознакомимся с ними в следующих разде- разделах главы.
Параметры 581 Объявление параметров Все формальные параметры определяются в списке параметров программы. При этом применяется почти такой же синтаксис, как при объявлении переменных в разделе объявлений блока PL/SQL. Однако параметры, в отличие от перемен- переменных, должны иметь режим использования. Кроме того, при объявлении парамет- параметров нельзя указывать ограничения на принимаемые ими значения. Поэтому, если в приведенном ниже объявлении размер переменной company_name ограничивается 60 символами: DECLARE companyjiame VARCHAR2F0); то при объявлении параметра ограничивающую часть определения необходимо опускать: PROCEDURE display_company (companyjiame IN VARCHAR2) IS ... Формальные и фактические параметры Очень важно понимать различие между формальными и фактическими парамет- параметрами. Формальные параметры представляют собой имена, объявленные в'списке параметров заголовка модуля, тогда как фактические — это значения или выра- выражения, которые помещаются в список параметров при вызове модуля. Рассмотрим функцию totsales, заголовок которой выглядит следующим об- образом: FUNCTION tot_sales (company_id_in IN company.company_id*TYPE. statusjn IN order.status_code*TYPE :- NULL) RETURN std_types.dollar_amount; Формальными здесь являются: company_id_in — первичный ключ таблицы com- company, а также statusjin — параметр, определяющий состояние заказов, которые должны быть включены в расчет объема продаж. Данные параметры вне функции не существуют, их можно считать «заполни- «заполнителями» для фактических значений, передаваемых функции при использовании ее в программе. Когда функция tnt_sal es вызывается в коде, на место формальных параметров помещаются фактические, то есть переменные, значения которых будут переданы данной функции. В этом примере значение переменной companyidin является первичным клю- ключом, указывающим на запись таблицы company. В приведенных ниже первых трех строках функции tot_sales передаются разные жестко закодированные значения состояния заказа. В последнем вызове состояние заказа не указано, поэтому функ- функция присваивает переменной status_i n значение по умолчанию (заданное в заго- заголовке функции): new_sales := tot_sales (company_id_in. 'N'): paid_sales :- tot_sales (company_id_1n. 'P'): shippedsales :- tot_sales (companyjd_in. 'S'); allsales :- tot_sales (company_id_in):
582 Глава 16 • Процедуры, функции и параметры При вызове функции tot_sales используются значения фактических парамет- параметров. Результаты вычислений присваиваются соответствующим формальным па- параметрам внутри функции (учтите, что это справедливо только в отношении па- параметров с режимом использования IN или IN OUT; выходные параметры, имеющие режим использования OUT, в функцию не копируются). Формальные и соответствующие им фактические параметры должны иметь один и тот же или совместимый тип данных. Во многих ситуациях PL/SQL мо- может выполнить автоматическое преобразование типов, но лучше этого избегать. Используйте функции преобразования, подобные TOCHAR и TODATE (см. главу 10), и вы всегда будете точно знать, данные какого типа передаются в модули. Связывание формальных и фактических параметров Каким образом при выполнении программы определяется, какому формальному параметру должен соответствовать фактический? PL/SQL предлагает два метода установки такого соответствия: О по позиции, когда фактический параметр ассоциируется с формальным неявно; О по имени, когда фактический параметр ассоциируется с формальным явно. Связывание по позиции Во всех приведенных ранее примерах применялось связывание параметров в со- соответствии с позицией. При его использовании PL/SQL принимает во внимание относительные позиции параметров, то есть он ассоциирует JV-й фактический па- параметр в вызове программы с N-м формальным параметром в заголовке программы. В примере с функцией tot_sale первый фактический параметр, :order.compa- ny_id, будет отвечать первому формальному, company_id_in, а второй фактический параметр, 'N', — второму формальному параметру, statusjn: new_sales :=- totsales (: order, companyjd. 'N'); FUNCTION tot_sales (company_id_in IN company.company_id*TYPE. statusjn IN order.status_code*TYPE :- NULL) RETURN std_types.dollar_arrount; Итак, теперь вы знаете, каким способом компилятор передает значения пара- параметров в модули. Метод связывания параметров по позиции, вне всякого сомне- сомнения, является наиболее наглядным и понятным (рис. 16.3). Заголовок процедуры PROCEDURE calc all (id in IN INTEGER, total out OUT NUMBER) Вызов процедуры calc all A007, tot sales) Рис. 16.3. Связывание параметров по позиции
Параметры 583 Связывание по имени Чтобы установить соответствие параметров по имени, требуется при вызове под- подпрограммы явно связать формальный параметр с фактическим. Делается это с по- помощью комбинации символов -> следующим образом: иня_формального_параметра -> значение_параметра Поскольку имя формального параметра указано явно, у PL/SQL отпадает не- необходимость учитывать порядок параметров. Поэтому в данном случае при вызо- вызове модуля не обязательно задавать параметры в том же порядке, в каком они ука- указаны в заголовке. Таким образом, функцию tot_sa1es для обработки новых зака- заказов можно вызвать любым из приведенных ниже способов: r.ew_sales :- tot_sales (companyJdjin -> : order, coaipanyjd. statusjn -> 'N'); new_sales :- tot_sa1es(status_1n -> 'N', company_id_in -> :order.companyjd); В одном вызове можно комбинировать оба метода связывания фактических и формальных параметров: : order. new_sales :- totsales (: order, companyjd, statusjn -> 'N'): При этом все «позиционные» параметры должны быть указаны перед «имено- «именованными». Для установления соответствия по позиции необходима какая-то точ- точка отсчета, и в качестве таковой используется первый параметр. Если же размес- разместить «именованный» параметр перед «позиционным», PL/SQL не сможет найти позицию последнего. Оба приведенных ниже вызова функции totsal es выполне- выполнены не будут. Первый вызов не выполнится потому, что соответствие по имени за- задано раньше соответствия по позиции, а второй — по той причине, что параметры указаны в неправильном порядке (PL/SQL попытается преобразовать ' N' в зна- значение переменной companyjdjn): :order.new_sales :- tot_sales (companyj'dj'n -> :order.company id. 'N'): :order.new_sales :- tot_sales('N'. companyjdjn -> :order.companyjd); Преимущества связывания по имени Теперь, когда вы знаете о разных методах связывания фактических и формаль- формальных параметров, у вас может возникнуть вопрос: зачем использовать связывание по имени? В качестве ответа предлагаем описание преимуществ, которые обеспе- обеспечивает данный метод. О Повышение информативности. При использовании связывания по имени вы- вызов программы содержит сведения о формальных параметрах, с которыми ас- ассоциируются фактические значения. Если вы не знаете, что делают модули, вызываемые приложением, наличие имен формальных параметров помогает понять назначение конкретного программного вызова. Именно по этой причи- причине в некоторых средах разработки связывание параметров по имени является стандартом. Оно особенно эффективно в случае, когда имена формальных па- параметров отвечают определенному соглашению (например, в качестве послед- последнего элемента имени указан режим передачи).
584 Глава 16 • Процедуры, функции и параметры О Обеспечение гибкости при описании параметров. Вы имеете возможность размещать параметры в удобном для работы порядке и включать в список толь- только те из них, которые действительно необходимы. В сложных приложениях иногда требуется создавать процедуры с десятком параметров. Однако, как вы знаете, любой параметр, значение которого определяется по умолчанию, при вызове модуля может быть опущен. При использовании связывания по имени более удобно передавать модулю только те значения, которые необходимы для решения текущей задачи. Помните, что метод связывания не влияет на внутреннюю реализацию моду- модуля, а лишь определяет способ его вызова. Режимы использования параметра При определении параметра указывается также допустимый способ его примене- применения. Он задается с помощью одного из трех указанных в таблице режимов. Режим Предназначение Использование параметра IN Только для чтения Значение параметра может применяться, но не может быть изменено в модуле OUT Только для записи В модуле можно присваивать значение параметру, но нельзя использовать его. В некоторых случаях Oracle позволяет применять выходные параметры не так, как указано в определении, но таких ситуаций рекомендуется избегать IN OUT Для чтения и записи В модуле можно использовать и изменять значение параметра Каждый режим определяет, каким образом программа может применять зна- значения, присвоенные формальному параметру. Режим параметра указывается не- непосредственно после его имени, но перед типом данных и необязательным значе- значением по умолчанию. В приведенном ниже заголовке процедуры задействованы все три режима передачи параметров: PROCEDURE predictjsctivity Clast_date_in IN DATE, taskdescinout IN OUT VARCHAR2. next_date_out OUT DATE) Процедура predict_activity принимает информацию о дате последнего дейст- действия (lastdate) и описание этого действия (taskdescinout). Возвращает она два значения: описание действия (возможно модифицированное) и дату следующего действия (next_date_out). Поскольку параметр task_desk_inout имеет режим IN OUT, программа может читать и изменять его значение. Рассмотрим каждый из режимов использования параметра более детально. Режим IN Этот режим параметра позволяет только передавать значение из вызывающего блока в модуль; обратная передача, из модуля в вызывающий блок PL/SQL, за- запрещается. Другими словами, параметры, объявленные в режиме IN, то есть входные
Параметры 585 параметры, функционируют внутри модуля как константы. Это означает, что дан- данным параметрам нельзя присваивать значения или каким-либо другим методом модифицировать их. Режим IN применяется по умолчанию. Таким образом, если при объявлении параметра не указать режим его использования, параметр автоматически рассмат- рассматривается как входной. Рекомендуется всегда указывать режим параметра, чтобы такого рода информация хранилась непосредственно в коде. В заголовке програм- программы входным параметрам можно присваивать значение по умолчанию (см. далее раздел «Значения по умолчанию»). Значением входного параметра может быть переменная, именованная констан- константа, литерал или сложное выражение. Таким образом, все приведенные ниже вы- вызовы функции display_title корректны: DECLARE happy_title CONSTANT VARCHAR2C30) :- 'С днем рождения': changTng_title VARCHAR2C30) :- 'С юбилеен1: spc VARCHARZ(l) :- CHRC2) -- ASCII-код пробела; BEGIN display_title t'C днем рождения'): -- литерал d1splay_t1tle (happy_title): -- константа Changing_titie :- happy_title: dispTaytitle (changing_title); -- переменная displayjtitle CC || spc || 'днем рождения'): -- выражение display_title (INITCAP (happy_title)): -- еще одно выражение END: Если вы хотите передать данные из своей программы, то вам понадобятся ре- режимы OUT и IN OUT. Режим OUT По своему назначению выходные параметры, то есть объявленные с использова- использованием ключевого слова OUT, противоположны параметрам, объявленным как IN. Выходные параметры используются для возврата значений из модуля в вызы- вызывающий блок PL/SQL. Они ведут себя как возвращаемые функциями значения и отличаются от них лишь тем, что описываются в списке параметров и их общее количество не ограничено. В программе выходные параметры действуют как неинициализированные пе- переменные и до успешного ее завершения не содержат никакого значения (кроме случаев, когда используется предложение NOCOPY, описанное далее). Во время вы- выполнения программы, при присвоении значения выходному параметру, создается его внутренняя копия. Когда программа успешно завершается и возвращает управ- управление вызывающему блоку, значение этой локальной копии передается фактиче- фактическому выходному параметру и становится доступным в вызывающем блоке. Применяя выходные параметры, вы должны обязательно учитывать следую- следующие их особенности. О Значение выходного параметра невозможно присвоить другой переменной или использовать в выражениях, присваивающих значения этому же параметру. В некоторых случаях Oracle позволяет применять выходные параметры ина- иначе, чем предписывает их определение, однако таких ситуаций лучше избегать.
586 Глава 16 • Процедуры, функции и параметры О Для выходного параметра нельзя установить значение по умолчанию. Ему мож- можно присвоить значение только в теле модуля. О Если в программе инициируется исключение, значения выходных параметров отменяются. Поскольку значения этим параметрам до корректного заверше- завершения программы не присваиваются, любые промежуточные изменения игнори- игнорируются. Даже если обработчик перехватывает исключение и присваивает вы- выходному параметру значение, эта операция не выполняется, и данная перемен- переменная будет иметь то же значение, что и до вызова программы. О Фактический параметр, который соответствует формальному выходному па- параметру, должен быть переменной. Он не может представлять собой констан- константу, литерал или выражение, поскольку эти объекты не позволяют PL/SQL по- поместить выходное значение. Режим IN OUT Используя режим IN OUT, значения параметра можно передавать в модуль и воз- возвращать их обратно вызывающей программе. Возвращать можно как начальное, не измененное значение, так и новое, установленное внутри модуля. Параметры с режимом использования IN OUT, как и выходные параметры, имеют ряд особен- особенностей. О Во-первых, для них нельзя задать значение по умолчанию. О Во-вторых, фактический параметр, который соответствует формальному, дол- должен быть переменной. Он не может представлять собой константу, литерал или выражение, поскольку эти объекты не позволяют PL/SQL поместить вы- выходное значение. Какие-либо другие ограничения по применению параметров в режиме IN OUT отсутствуют. Эти параметры можно указывать с обеих сторон оператора присваи- присваивания, поскольку они ведут себя как инициализированные переменные. Начав выполнение программы, PL/SQ.L сохраняет значение рассматриваемого парамет- параметра и при необходимости использует его в программе. Процедура combine_and_format_names, код которой приведен ниже, объединяет имя и фамилию в полное имя, применяя указанный формат («ФАМИЛИЯ, ИМЯ» либо «ИМЯ ФАМИЛИЯ*). Для выполнения этой операции процедуре требует- требуется передать имя и фамилию. Затем мы переводим символы имени и фамилии в верхний регистр, чтобы привести их к стандарту. Заметьте, что в этой процеду- процедуре используются все три режима передачи параметров: IN, IN OUT и OUT. PROCEDURE combine and format_names (f1rst_name_1nout In OUT VARCHAR2. last_name_1nout IN OUT VARCHAR2. full_name out OUT VARCHAR2, name formatjn IN VARCHARZ :- 'LAST, FIRST1) IS BEGIN /* Преобразование символов имени и фамилии з сииво/ш верхнего регистра */ first_riame_irraut :- UPPER (f1rst_name_1nout); last name Tnout :- UPPER (last name 1nout);
Параметры 587 /* Объединение имени и фамилии в соответствии с форматом */ IF name_format_in - 'LAST. FIRST1 THEN fuTl_name_OLit :- last_name_inout || '. ' || first_name_inout; ELSIF naraE_forraat_in - 'FIRST LAST' THEN full_name out :- first_name_inout || ' ' II last_name_inout: END IF: END: Имя и фамилия должны быть параметрами с режимом использования IN OUT. Параметр fulinameout является только выходным и служит для возврата полно- полного имени. Если фактический параметр, который применяется для получения пол- полного имени из процедуры, до передачи в процедуру имел некоторое значение, то в ней не стоит явно его использовать. Параметр name_format_in является входным, поскольку он лишь задает формат полного имени и не изменяется. Каждый режим параметра имеет свои особенности и назначение. Для того что- чтобы режимы применялись в модуле правильно, к их выбору следует подходить очень внимательно. Методы передачи параметров Начиная с версии PL/SQL 8.1 существует возможность определять метод переда- передачи параметров в подпрограмму. Реализуется она с помощью предложения NOCOPY, которое указывает компилятору, как нужно работать со структурами данных, имею- имеющими режим использования OUT или IN OUT. Значения параметров в подпрограмму могут передаваться следующим образом. О По ссылке. Если фактический параметр передается по ссылке, то с соответст- соответствующим формальным параметром связывается указатель на данный параметр. После этого оба параметра ссылаются (указывают) на одну и ту же ячейку па- памяти, в которой содержится требуемое значение. О По значению. Когда фактический параметр передается по значению, его зна- значение копируется в соответствующий формальный параметр. Если впоследст- впоследствии программа завершается без инициирования исключений, значение фор- формального параметра присваивается фактическому. В случае же возникнове- возникновения ошибки измененное значение обратно не передается. Каким образом выполняется передача параметров в PL/SQL, когда предложе- предложение NOCOPY не используется, можно судить по приведенной ниже таблице. Ражим Передача (по умолчанию} IN OUT IN OUT По ссылке По значению По значению Если в качестве параметров передаются большие структуры данных (коллек- (коллекции, записи или экземпляры объектных типов), объявленные с ключевыми сло- словами OUT или IN OUT, то применяется передача по значению, которая может суще- существенно замедлить работу программы и увеличить объем потребляемой ею памяти.
588 Глава 16 • Процедуры, функции и параметры Для предотвращения таких ситуаций и служит предложение NOCOPY. Применяется оно следующим образом: имя_паранетра [IN | IN OUT | OUT | IN OUT NOCOPY | OUT NOCOPY] Обратите внимание, что предложение NOCOPY используется только совместно с режимами OUT и IN OUT. Список параметров, в котором содержится предложение NQCOPY, приведен ниже: PROCEDURE analyze_results ( datejn IN DATE. values IN OUT NOCOPY numbers_array. validity_flags IN OUT NOCOPY validityj-ectype ); Применяя предложение NOCOPY, следует учитывать две важные особенности. О Каждый раз при вызове подпрограммы, которая содержит выходной параметр, объявленный как NOCOPY, значение фактического параметра, соответствующего выходному, устанавливается в NULL. О Предложение NOCOPY - это указание, а не команда, поэтому не исключено, что компилятор самостоятельно примет решение о невозможности выполнить ваш запрос. Случаи, когда именно это происходит, подробно описаны в следую- следующем разделе. Ограничения при использовании предложения NOCOPY Иногда компилятор PL/SQL игнорирует предложение NOCOPY и использует метод передачи по значению, который задан для параметров с режимами OUT и IN OUT no умолчанию. Это может иметь место в следующих ситуациях. О Фактический параметр является элементом ассоциативного массива. Пред- Предложение NOCOPY применяется к ассоциативному массиву (который может иметь структуру записи), но не к отдельному элементу таблицы. Данное ограниче- ограничение несложно обойти таким образом: скопировать структуру в переменную, скаляр или запись, а затем передать ее как параметр, объявление которого со- содержит предложение NDCOPY. О Наличие определенных ограничений на фактические параметры. Установка некоторых ограничений приводит к тому, что предложение NOCOPY игнорирует- игнорируется. Речь идет об указании количества знаков после запятой для числовой пе- переменной и ограничении NOT NULL. На строковую переменную, объявленную с ограничением длины, это не распространяется. О Фактические и формальные параметры являются записями. При этом одна или обе записи объявлены с использованием атрибута ^ROWTYPE или ШРЕ, а ог- ограничения на соответствующие поля этих двух записей разные. О При передаче фактического параметра PL/SQL должен выполнить неявное преобразование типов данных. Выход из этой ситуации может быть следую- , щим. Поскольку всегда лучше использовать явное преобразование типов дан- данных, выполните его, а затем передавайте преобразованные значения как пара- параметры, в объявлении которых имеется предложение NOCOPY.
Параметры 589 О Обращение к подпрограмме осуществляется путем внешнего или удаленно- удаленного вызова процедуры. В таких ситуациях PL/SQL всегда будет передавать фактические параметры по значению. Использование предложения NOCOPY В ряде случаев применение NOCOPY позволяет повысить производительность про- программ, имеющих параметры с режимами IN OUT или OUT. Естественно, приобрете- приобретение этой выгоды сопряжено с некоторыми проблемами. В частности, если про- программа завершается с необработанным исключением, значениям фактических N0- COPY-параметров доверять нельзя. Для того чтобы понять, что же подразумевается в данном случае под словом «доверять», рассмотрим, как PL/SQL поступает с параметрами, когда программа завершается необработанным исключением. Предположим, процедуре calcula- tetotals передана запись в режиме использования IN OUT. Исполняющее ядро PL/SQL создает копию этой записи и во время выполнения программы вносит в нее изменения. Значение фактического параметра не модифицируется до тех пор, пока процедура calculatetotals не будет успешно завершена. Когда это про- происходит, значение, хранящееся в локальной копии, записывается в фактический параметр, и программа, вызвавшая процедуру calculate_totals, может получить доступ к измененным данным. Если процедура calculatetotals заканчивается с необработанным исключением, значение фактического параметра не изменяется. При использовании предложения NOCOPY мы будем иметь другую ситуацию. Когда параметр передается по ссылке (эффект наличия NOCOPY), любые изменения значения формального параметра сразу же отражаются на значении фактического. Предположим, процедура calculate_totals считывает коллекцию из 10 000 строк и вносит изменения в каждую из них. Если на строке с номером 5 000 будет ини- инициировано исключение, которое не обрабатывается процедурой calculatetotals, значение фактического параметра окажется измененным только наполовину. Как вы наверняка убедились, пользоваться предложением NOCOPY следует осто- осторожно и только в тех случаях, когда точно известно, что проблема с производи- производительностью вызвана методом передачи параметров. При этом нужно учитывать возможные последствия, связанные с инициированием исключений. Значения по умолчанию В приведенных ранее примерах показано, что входным параметрам можно при- присваивать значения по умолчанию и тогда их не обязательно включать в список параметров при вызове программы. Значение параметра по умолчанию использу- используется программой только в том случае, если при ее вызове список параметров не содержит этого параметра. Конечно же, вы должны обязательно указывать фак- фактические значения параметров с режимом передачи IN OUT. Значение параметра по умолчанию подобно значению по умолчанию, которое задается при объявлении переменной. Указать его можно двумя способами: при помощи ключевого слова DEFAULT или посредством оператора присваивания (:=¦), как показано в этом примере: PROCEDURE astrology_reading (sign in IN VARCHAR2 :- 'LIBRA', born~at_in IN DATE DEFAULT SYSDATE) IS
590 Глава 16 • Процедуры, функции и параметры Если значения по умолчанию заданы, в вызов программы не обязательно вклю- включать все фактические параметры - для пропущенных параметров программа ис- использует значения по умолчанию. Если параметр задан, его значение заменяет значение по умолчанию. Ниже приведены различные способы вызова процедуры astrology_reading с использованием связывания по позиции: BEGIN astrology readingC'SCORPIO'. ТО_ОАТЁ ('12-24-2001 17:56:10'. 'MM-DD-YYYY HH24:MI:SS')); astrology_reading('SCORPIO'): astrology_read1ng; END: В первом вызове значения обоих параметров заданы явно, во втором указано только значение первого фактического параметра, поэтому переменная born_at_in будет равна текущей дате и времени. В третьем вызове параметры вообще не ука- указаны, поэтому в нем нет круглых скобок. При этом в теле процедуры используют- используются оба значения по умолчанию. Как поступить, если в процедуре нужно задать время рождения, но не указы- указывать знак зодиака? Для того чтобы пропустить первый параметр и присвоить ему значение по умолчанию, необходимо применить связывание по имени. Указывая имена формальных параметров, вы можете задавать только те, которым хотите назначить значения явно. В приведенном ниже вызове для первого параметра применяется значение по умолчанию - LIBRA, а текущее время заменено новым значением: BEGIN astrology„reading С born_at_1n -> TOJATE ('12-24-2001 17;56:10\ 'MM-DD-YYYY HH24:M1:SS1}): END; Покальные модули Локальный модуль - это процедура или функция, которая определена в разделе объявлений блока PL/SQL (анонимного либо именованного). Локальным такой модуль считается по той причине, что он определен только внутри родительского блока PL/SQL и не может быть вызван из какого-либо другого блока, определен- определенного вне родительского. Рассмотрим в качестве примера рис. 16.4. Представленные на нем блоки, внеш- внешние для процедуры calc_overdue_fines, не могут непосредственно вызывать ее ло- локальные процедуры или функции. Синтаксис объявления локальной процедуры или функции совпадает с исполь- используемым для отдельного модуля. Например, в анонимном блоке, который приве- приведен ниже, объявлена локальная процедура showdate: DECLARE PROCEDURE show_date (datejin IN DATE) IS BEGIN DBMS_OUTPUT.PUT_LINE (TO_CHAR (datejn. 'Month DD, YYYY1));
Локальные модули 591 END: BEGIN END; Вы можете вызвать процедуру calc_ovenluejines Procedure calc overdue fines IS Procedure locall Procedure Iocal2 BEGIN locall; 1OC812; END: Wo вы не макете вызвать локальны» процедуры или функции Рис. 16.4 Локальные модули вне процедуры недоступны В разделе объявлений локальные модули должны быть расположены после остальных объявлений. Таким образом, объявить переменные, курсоры, исклю- исключения, типы, записи, таблицы и т. д. необходимо до первого ключевого слова PRO- PROCEDURE или FUNCTION. О том, что дает нам использование модулей, расскажем в следующем разделе. Преимущества локальной модуляризации Использование локальных модулей обеспечивает следующие преимущества. О Уменьшение объема кода за" счет исключения повторяющихся фрагментов. Это послужило наиболее веской причиной создания локальных модулей. Умень- Уменьшение объема кода сопровождается повышением его качества, так как число строк и количество потенциальных ошибок в них уменьшаются, а поддержка кода значительно упрощается. Внесение изменений производится только в ло- локальном модуле, а результаты распространяются на все приложение. О Улучшение читабельности кода. Даже если модуль не содержит повторяю- повторяющихся блоков кода, взаимосвязанные операторы лучше поместить в локаль- локальный модуль. В этом случае вам будет легче отслеживать логику, заложенную в родительском модуле. Уменьшение объема кода Рассмотрим на примере, как можно сократить объем кода. Процедура cal ^per- ^percentage принимает числовые значения из пакета sales_pkg, рассчитывает долю ка- каждой продажи в объеме продаж (это значение передается как параметр) и затем форматирует число, которое будет отображено в отчете или форме. Приведенный
592 Глава 16 ¦ Процедуры, функции и параметры пример включает только три однотипных расчета, однако мы взяли его из реаль- реального приложения, в котором таких расчетов было 231. PROCEDURE calc_percentages (tot_sales_in IN NUMBER! IS 8EGIN ¦.profile.food_sales_stg :- TO_CHAR [(sales pkg.food_sales / tot_sales_in) * 100. ¦$999999'): :profile.service_sales_stg := TO_CHAR ((salesjjkg.servicesales / tot_sales in) * 100. '$999999'); ;profi1e.toy_sales_stg :- TO_CHAR Usalesj>kg.toy_sales / tot_sales_in) * 100. '$999999'); END; Этот код требует довольно много времени для ввода и не удобен в сопровож- сопровождении. Если придется изменять формат представления или алгоритм вычисле- вычисления процентов, то правки нужно будет вносить в каждое выражение. Использование же локальных модулей позволяет весь повторяющийся код скон- сконцентрировать в одной функции, которая затем будет многократно вызываться в процедуре ca1c_percentages. Версия этой процедуры с локальным модулем пока- показана ниже: PROCEDURE calc_percentages (tot_sales_in IN NUMBER) IS /ЧЮьявление функции непосредственно в процедуре*/ FUNCTION pct_stg tvaljn IN NUMBER) RETURN VARCHAR2 IS BEGIN RETURN TOJHAR ((val_in/tot sales_in)*100. '$999999'): END; BEGIN :profile.food_sales_stg :- pct_stg (sales_pkg.food_sales); :profile.service_sales_stg :- pct_stg (sales_pkg.service_sales): :profile.toy_sales_stg :- pct_stg (sales pkg.toy_sales); END; Все расчеты — деление на значение переменной tot_sales_in, умножение на 100 и форматирование с помощью функции TO_CHAR - перенесены в функцию pct_stg, которая определяется в разделе объявлений процедуры. Благодаря нали- наличию вызовов этой функции в теле процедуры calcpercentages исполняемые опе- операторы последней значительно упростились. Кроме того, теперь при изменении расчетной формулы достаточно будет внести правку в локальную функцию, и это отразится на всех операторах присваивания процедуры. Улучшение читабельности кода Использование локальных модулей позволяет улучшить читабельность кода, уп- упростить его сопровождение и реализовать такие методы, как нисходящее проек- проектирование или пошаговую детализацию. Кроме того, локальные модули можно применять для «декомпозиции» существующей программы с целью улучшения ее читабельности.
Локальные модули 593 Конечным результатом такого использования локальных модулей является уменьшение объема исполняемого раздела программы, поскольку код, реализую- реализующий логику приложения, переносится из исполняемого раздела в локальный мо- модуль, вызываемый из этого раздела. Если программа имеет небольшой исполняе- исполняемый раздел, ее намного проще читать и воспринимать. ПРИМЕЧАНИЕ— : Считается, что объем кода исполняемого раздела не должен превышать 60 арок (объем текста, ко- который помещается на страницу или экран). Вначале этот совет может показаться странным, но, по- последовав ему, вы обнаружите, что он весьма полезен. Предположим, что имеется набор циклов WHILE (отдельные из них являются вложенными), которые содержат сложные вычисления и вложенные условные операторы. Даже при наличии подробных комментариев довольно сложно про- проследить порядок выполнения операторов программы, если ее код состоит из не- нескольких страниц. Еще труднее это сделать, когда оператор IF или LOOP конструк- конструкции находится на одной странице, а оператор END IF или END LOOP — на другой. Если же перенести связанные между собой операторы в один или несколько локальных модулей, а затем вызывать эти модули в программе, программа будет документировать сама себя. Приведенная ниже процедура assign_workload явля- является лишь упрощенной реализацией данного подхода, однако она хорошо демон- демонстрирует преимущества, предлагаемые локальными модулями: PROCEDURE assign_worlcload (departmentjn IN emp.deptnoSTYPE) IS CURSOR emps_in dept_cur (departmebtjn IN emp.deptnorTVPE) IS SELECT * FROM emp WHERE deptno = departmentjn: PROCEDURE ass1gnjiext_open_case (empjdjn IN NUMBER, case_out OUT NUMBER) IS BEGIN ... полная реализация ... END; FUNCTION next_appointment (casej'd in IN NUMBER) RETURN DATE IS BEGIN ... полная реализация .. . END: PROCEDURE schedule_case (casejn IN NUMBER, dateJn IN DATE) IS BEGIN ... полная реализация ... END: BEGIN /Основной*/ FOR emp rec IN emps_1n_dept_cur (departmentjn) LOOP IF analysis.caseload (emp_rec.aipjd) <¦ analysis.avg_cases (departmentjn); THEN assign jiext_open_case(enipj-ec.empj'd. case#): 5chedule_case (case#. next_appointment (case#)); END IF;
594 Глава 16 • Процедуры, функции и параметры END LOOP; END ass1gn_workload: В процедуре assign_workload объявлены три локальных модуля: assi gn_next_open_case next_appointment schedule_case Кроме них, процедура вызывает analysis.caseload и analysis.avg_cases — две программы из пакета, которые уже созданы и легко могут быть включены в нее. Для того чтобы понять, как работает процедура assign_workload, нет надобности досконально изучать весь ее код. При чтении кода, находящегося в теле процедуры, по именам локальных модулей, даже в отсутствие комментариев, легко догадать- догадаться, что делает каждый из них. Конечно, данный подход даст желаемый результат лишь при условии, что вы будете назначать процедурам и функциям информа- информативные имена. Область видимости локальных модулей Раздел объявлений локального модуля очень похож на тело пакета, также содер- содержащее объявления модулей (см. главу 17). Различие между локальными и пакет- пакетными модулями заключается в их областях видимости: локальные модули могут быть вызваны только из блока, в котором объявлены, а пакетные — как минимум из самого пакета. Если же пакетные модули указаны в спецификации пакета, то они могут быть вызваны из любой программы приложения. Таким образом, локальные модули следует использовать для инкапсуляции кода, который вызывается только в текущей программе. В других ситуациях не- необходимо создавать пакет. Локальные модули как средство повышения качества кода Если перед вами поставят задачу, реализация которой потребует кода длиной бо- более 20 строк, то вы, скорее всего, создадите в программе несколько локальных мо- модулей. Так значительно легче найти решение задачи, поскольку представление на уровне модулей обеспечивает более высокий уровень абстракции. Кроме того, модульный подход позволяет применить методы нисходящего проектирования и пошаговой детализации. Даже если модуляризация проведена лишь на уровне одной программы, локальный модуль впоследствии легче извлечь из нее и пре- превратить в независимую, доступную для повторного использования процедуру или функцию. Перегрузка модулей Программы, которые существуют в одной и той же области видимости и имеют одинаковые имена, называются перегруженными. PL/SQL поддерживает перегруз- перегрузку процедур и функций в разделе объявлений блока (именованного или неимено- неименованного), в теле и спецификации пакета, а также в объявлении объектного типа.
Перегрузка модулей 595 Перегрузка — это очень мощный прием, и вы должны научиться использовать все его возможности. В приведенном ниже простом примере содержатся три перегруженных моду- модуля, которые определены в разделе объявлений анонимного блока, а следователь- следовательно, являются локальными: DECLARE /* Первая версия принимает параметр типа DATE */ FUNCTION value_ok (datejn IN DATE) RETURN BOOLEAN IS BEGIN RETURN date 1n <- SYSDATE; END; /* Вторая версия принимает параметр типа NUMBER */ FUNCTION value ok (nuiterjn IN NUMBER) RETURN BOOLEAN IS BEGIN RETURN numbeMn > 0: END; /* Третья версия - это процедура! */ PROCEDURE vaiuejjk (number 1n IN NUMBER) RETURN BOOLEAN IS BEGIN IF number 1n > 0 THEN DBMS OUTPUT.PUT LINE (number 1n || 'Ok!'): ELSE OBMS_OUTPUT.PUT_LINE (numbeMn 11 '1s not Ok!'); END IF; END: BEGIN Когда исполняемое ядро PL/SQL встречает в программе строку вида IF value_ok (SYSDATE) THEN ... список фактических параметров сравнивается со списками формальных парамет- параметров различных перегруженных модулей. В результате PL/SQL выполняет код того модуля, для которого указанные списки совпадают. ПРИМЕЧАНИЕ- Перегрузку также называют статическим полиморфизмом. Термин «полиморфизм» означает воз- возможность языка определять и использовать несколько программ с одинаковыми именами, Если ре- решение о том, какой вариант программы вызывать, принимается на этапе компиляции, — это стати- статический полиморфизм. Когда такое решение принимается во время выполнения, то говорят о динамическом полиморфизме, Перегрузка может существенно упростить работу программиста, так как она позволяет сгруппировать под одинаковыми именами интерфейсы вызова несколь- нескольких похожих программ. В результате отпадает необходимость помнить, к приме- примеру, имена шести подпрограмм, добавляющих значения в таблицы PL/SQL (дату, строку, булево значение, число и т. д.). Вы просто указываете, что вам необходи- необходимо добавить значение, и передаете его компилятору, a PL/SQL и перегруженные модули определят ваши намерения и выполнят всю работу.
596 Глава 16 • Процедуры, функции и параметры На создание перегруженных модулей может потребоваться дополнительное время, однако эти затраты окупятся, поскольку ваши программы станут более эффективными и удобными в применении. Использование перегрузки Перегрузка модулей является наиболее подходящим решением в следующих си- ситуациях. О Необходимость поддержки разных типов и наборов данных. Когда одно и то же действие применяется к разным видам или наборам данных, перегрузка обеспечивает несколько вариантов активизации этого действия, а не просто дает возможность назвать различные действия одним и тем же именем. О Настройка программы под различные требования. Для расширения сферы применения программы можно создать несколько ее версий, снабдив их раз- различными вариантами вызова. При реализации этой задачи часто требуется про- производить перегрузку процедур и функций. В пользу необходимости выполне- выполнения таковой может свидетельствовать создание невостребованного, «лишнего» кода. К примеру, работая с пакетом DSMS_SQL, вы вызываете функцию DBMS_ SQL. EXECUTE, однако при выполнении ею DDL-инструкций возвращаемое зна- значение игнорируется. Если для этой функции создать перегруженную процеду- процедуру, DDL-инструкцию можно выполнить следующим образом: BEGIN DBMS_SQL.EXECUTE С'CREATE TABLE «yz ...'): Без применения перегрузки вы должны вызвать функцию: DECLARE feedback PLSJNTEGER: BEGIN feedback :- DBMSJQL. EXECUTE ('CREATE TABLE xyz ...'): а затем игнорировать (не использовать) переменную feedback. О Необходимость выполнить перегрузку по типу, а не по значению. В этом случае при определении того, какая из перегруженных подпрограмм должна вызываться, учитывается тип данных, а не их значение. Этот способ перегруз- перегрузки применяют очень редко. Продемонстрировать его можно на примере функ- функции DBMS__SQL.DEFIN?_COLUMN. Работая с ней, пакету DBMSJQL требуется указать тип каждого столбца, полученного при помощи динамического запроса. Для задания числового столбца можно использовать вызов DBMS_SQL.DEFINE_COLUMN (cur, 1,1): или вызов DBMS_SQL.DEFINE_COLUMN (cur. 1. DBMSJJTILITY.GETJTME): Как видите, в данном случае само значение этого числа не важно. Далее мы рассмотрим пример использования перегрузки, а затем поговорим о связанных с ней ограничениях.
Перегрузка модулей 597 Поддержка разных типов данных Перегрузку рекомендуется использовать для применения одного и того же дейст- действия по отношению к разным видам или наборам данных. Как было отмечено ра- ранее, в данном случае перегрузка обеспечивает несколько способов активизации этого действия. Перегрузка с целью поддержки нескольких типов данных реали- реализована в процедуре DBMS_OUTPUT.PUT_LINE, однако на тип данных BOOLEAN такая воз- возможность не распространяется. Рассмотрим фрагмент спецификации пакета DBMS_OLTTPUT: CREATE OR REPLACE PACKAGE DBMSJMPUT AS PROCEDURE putjhne (a VARCHAR2): PROCEDURE putjine (a NUMBER); END DBMS_QUTPUT: Как видите, вы не можете использовать пакет DBMS_OUTPUT для вывода значения переменной типа BOOLEAN, поэтому для отображения ее значения придется писать оператор IF, подобный приведенному ниже: IF I_student_is_registred THEN DBMS OUTPUT.PUTJ.INE CTRUE'); ELSE 0BMS_OUTPUT.PUT_LINE ('FALSE'): END IF; К счастью, эту проблему довольно легко решить — достаточно создать свой пакет с перегруженными процедурами DBMS_OUTPUT.PUT_LINE. Пример такого паке- пакета приведен ниже. Вы также можете расширить свои возможности, как это сдела- сделали мы с помощью процедуры do. pi: /* Файл в web: do.ptcg */ CREATE OR REPLACE PACKAGE DO IS PROCEDURE pi (booleanjn IN BOOLEAN): /* Вывод строки *J PROCEDURE pi (charjn IN VARCHAR2); /* Выаод строки и значения перененной типа Boolean */ PROCEDURE pi ( спаМл IN VARCHAR2. booleanjn IN BOOLEAN): PROCEDURE pi (xmljn IN SYS.XHLType); END DO; Данный пакет устанавливается поверх пакета DBMS_OUTPUT и дополняет возмож- возможности имеющейся в нем процедуры. В частности, с помощью процедуры do.pl отображать значения типа BOOLEAN можно без использования оператора IF: DECLARE vjs_valid BOOLEAN :- book info.is valid isbn ('5-88888-66');
598 Глава 16 • Процедуры, функции и параметры BEGIN do.pl (v_is_val1d): Процедуру do. pi можно применять и при работе с относительно сложными типами данных, такими как XMLType: /* Файл в web: xmltype.sql */ DECLARE one report XMLTYPE; BEGIN ~ SELECT ea.report INTO one_report FROM envanalysis ea WHERE company - 'SMOKESTAX INC; do.pl (one_report): END: Ограничения на использование перегрузки Выполняя перегрузку, необходимо учитывать несколько обстоятельств. Во-пер- Во-первых, исполнительное ядро PL/SQL, как на этапе компиляции, так и на этапе за- запуска, должно быть в состоянии отличить друг от друга перегруженные версии подпрограммы. Во-вторых, оно не может выполнять две подпрограммы одновре- одновременно и при компиляции кода PL/SQL все «неподходящие» перегруженные мо- модули будут проигнорированы. Так как все перегруженные версии программы име- имеют одинаковые имена, исполнительное ядро PL/SQL не может различить их по именам. Для определения того, какую из них следует выполнить, PL/SQL ис- использует списки параметров и/или сведения о типе программы (процедура или функция). Все это приводит к тому, что на перегруженные программы наклады- накладывается ряд ограничений. О У перегруженных программ типы хотя бы одного параметра должны при- принадлежать к разным семействам. Типы данных INTEGER, REAL, DECIMAL, FLOAT и другие являются числовыми подтипами, а типы данных CHAR, VARCHAR2 и LONG - символьными. Если соответствующие параметры отличаются только подти- подтипом, то есть принадлежат к одному супертипу, у PL/SQL не будет достаточно информации для определения того, какую из программ выполнять. О Если у перегруженных программ различны только имена параметров, то та- такие программы должны вызываться с использованием метода связывания по имени. Когда имя аргумента не указано, то как в этом случае компилятор сможет различить вызовы двух перегруженным программам? И вообще, не стоит использовать связывание по имени как единственно возможный подход. О Список параметров перегруженных программ должен различаться не толь- только режимом использования параметров. Даже если в одной версии для пара- параметра задан режим IN, а в другой — IN OUT, PL/SQL не видит различия в вызо- вызовах программы. О Перегруженные функции не могут различаться только типом возвращаемо- возвращаемого значения (указан в предложении RETURN функции). В момент вызова перегруженной функции компилятор не знает, значение какого типа она будет
Предобъявления 599 возвращать. Поэтому он не может определить, какую из версий функции ис- использовать, если все остальные параметры идентичны. О Все перегруженные программы должны быть объявлены в одном и том же блоке PL/SQL или иметь одну область видимости (анонимный блок, пакет, отдельная процедура или функция). Нельзя определить одну версию про- программы в одном блоке (с его областью видимости), а другую — в другом. Вы не можете перегружать две отдельные программы, так как в этом случае одна из них просто заменит другую. Предобъявления Язык PL/SQL требует, чтобы элементы были объявлены до того, как они будут использоваться в коде программы. Но как PL/SQL может определить, насколько корректно вы их применяете? Если одни модули вызывают другие, может воз- возникнуть ситуация, когда невозможно объявить все модули до того, как встретят- встретятся ссылки на них. Например, что делать, если программа А вызывает программу Б, а программа Б, в свою очередь, - программу А? К счастью, PL/SQL поддержи- поддерживает рекурсию, в том числе и взаимную, при которой две или несколько программ явно либо неявно вызывают друг друга. Тем, кто использует взаимную рекурсию, будет приятно узнать, что PL/SQL позволяет выполнять предобъявление локального модуля, то есть объявлять мо- модуль до фактической его реализации. Это дает возможность вызывать модуль из других программ до его определения. Предобъявление состоит из заголовка программы, за которым следует точка с запятой. Эта конструкция называется заголовком модуля. Такой заголовок вклю- включает список параметров (и предложение RETURN — для функций) и предоставляет всю информацию, которая необходима PL/SQL для объявления модуля и пра- правильной обработки любых обращений к нему. В приведенном ниже примере внутри процедуры определяются две взаимно рекурсивные функции. Следовательно, заголовок второй функции, total_cost, не- необходимо определить до объявления функции net_profit: PROCEDURE perform_ca1cs (year_in IN INTEGER) IS /* Только заголовок для функции total cost */ FUNCTION total_cost (...) RETURN NUMBER: /* Функция net profit, использующая функцию total cost */ FUNCTION net_proflt (...) RETURN NUMBER IS BEGIN RETURN tot_sales (...) - total cost (. . .); END; /* Функция total_cost. использующая функцию net_prof1t */ FUNCTION total_cost (...) RETURN NUMBER IS BEGIN IF «условие, основанное на параметрам THEN
600 Глава 16 • Процедуры, функции и параметры RETURN net_profit (...)* 0.10; ELSE RETURN <зиачение параметра>: END IF: END; BEGIN END: Существует ряд правил, о которых необходимо помнить при использовании предобъявлений. О Предобъявления нельзя применять по отношению к переменным или курсо- курсорам. Эта техника «работает» только с модулями (процедурами и функциями). О Определение предобъявленной программы всегда должно находиться в разде- разделе объявлений того блока PL/SQL, в котором используется предобъявление (анонимного блока, процедуры, функции или тела пакета). Вам нередко придется сталкиваться с ситуациями, когда без предобъявлений не обойтись, во многих случаях они помогают сделать код легким для чтения. Од- Однако рекомендуем использовать их только в тех случаях, когда это действительно необходимо. Дополнительные вопросы Этот раздел будет полезен в первую очередь опытным программистам, поскольку в нем рассматривается ряд сложных вопросов, связанных с модуляризацией, в ча- частности вызов функций в SQL, использование табличных и детерминированных функций. Вызов пользовательских функций в SQL Oracle позволяет вызывать пользовательские функции, что дает возможность рас- расширить язык SQL и адаптировать его под приложение. Требования к вызываемым функциям Для того чтобы созданную программистом функцию PL/SQL можно было вызы- вызывать из инструкций SQJL, она должна отвечать следующим требованиям. О Храниться в базе данных. Функция, определенная на стороне клиента среды PL/SQL, не может быть вызвана из SQL-инструкции, так как SQL не сможет обработать ее вызов. О Все параметры функции должны иметь режим использования IN. Режимы IN OUT и OUT применять в этом случае нельзя. Отметим, что для параметров функ- функций вообще не рекомендуется назначать режим IN OUT или OUT, поскольку это противоречит идее, что функция должна возвращать одно значение. О Типы данных параметров функции и тип данных в предложении RETURN долж- должны распознаваться сервером Oracle. Все типы базы данных Oracle использу- используются в PL/SQL, но в PL/SQL добавлены новые типы данных, которые пока не
Дополнительные вопросы 601 поддерживаются базой данных. Речь идет о типах BOOLEAN, BINARY_INTEGER, ассо- ассоциативных массивах, записях PL/SQL и определяемых программистом под- подтипах. О При работе с версиями РСУБД, предшествующими Огас1е8г, для функции, объявленной в пакете, следует использовать директиву RESTRICT_REFERENCES. Ес- Если вы хотите вызвать определенную в пакете функцию из SQL, необходимо добавить эту директиву в спецификацию пакета. (Более подробно данная тема освещена в разделе «Директива RESTRICT_REFERENCES (Oracle8 и более ранние версии)» далее в этой главе.) ПРИМЕЧАНИЕ По умолчанию пользовательские функции, вызываемые в SQL, оперируют данными одной строки, а не столбца (как агрегатные функции SUM, MIN и AVG). Чтобы создать агрегатные функции, вызы- вызываемые в SQL, необходимо использовать интерфейс ODCIAggregate, который является частью сре- среды Oracle's Extensibility Framework. За более детальной информацией по этой теме обращайтесь к документации Orade. Ограничения для пользовательских функций, вызываемых в SQL С целью защиты от побочных эффектов и непредсказуемого поведения хранимых функций РСУБД Oracle не позволяет им выполнять в SQL следующие действия. О Хранимые функции не могут модифицировать таблицы базы данных и выпол- выполнять инструкции INSERT, UPDATE и DELETE. Эти ограничения ослабляются, если функция определена как автономная транзакция (описывается в главе 13). В таком случае любые вносимые ею изменения осуществляются независимо от внешней транзакции, в которой выполняется запрос. О Хранимые функции, которые вызываются удаленно или в параллельном режи- режиме, не могут читать или изменять значения переменных пакета. Сервер Oracle не поддерживает побочные эффекты, действие которых выходит за рамки се- сеанса пользователя. О Хранимая функция может изменять значения переменных пакета, только если она вызывается в списке выборки либо в предложении VALUES или SET. Если хранимая функция вызывается в предложении WHERE или GROUP BY, она не мо- может изменять значения переменных пакета. О В версиях, предшествовавших Огас1е8, невозможно вызвать из хранимой функ- функции процедуру RAISE_APPLICATION_ERROR. О Хранимая функция не может вызывать другой модуль (хранимую процедуру или функцию), который не соответствует приведенным выше требованиям. «Уровень чистоты» функции определяется уровнем самого «нечистого» из вы- вызываемых ею модулей. О Хранимая функция не может ссылаться на представление, которое нарушает любое из предшествующих правил. Представление - это хранимая инструк- инструкция SELECT, которая способна вызывать хранимые функции.
602 Глава 16 • Процедуры, функции и параметры Функция DECODE как замена оператора IF Функция DECODE реализует возможности оператора IF в непроцедурной среде SQL, предоставляемой сервером Oracle. С ее помощью можно создавать матричные от- отчеты, имеющие фиксированное число столбцов, и задавать в запросе сложные ус- условия типа IF,. .THEN... ELSE. Недостатком данной функции является то, что в слу- случае ее применения код получается сложным. Рассмотрим пример использования функции DECODE, в котором проверяется, принадлежит ли дата заказа определен- определенному временному диапазону, и, если это так, то увеличивается значение соответ- соответствующего счетчика заказов: SELECT FC.,year_number. SUM (DECODE [GREATEST (shipjate. FC.ql_sdate), shipjate, DECODE (LEAST (shipjate. FC.ql_edate), shipjate, 1, 0). 0)) Ql_resu1ts. SUM (DECODE (GREATEST (shipjate, FC.q2_sdate). sh1p_date. DECODE (LEAST (shipjate, FC.q2_edate). ship date, 1, 0). ~ 0)) Q2_results, SUM (DECODE (GREATEST (shipjate. FC.q3_sdate). ship_date. DECODE (LEAST (shipjate. FC.q3_edate). shipjate. I, 0), 0)) Q3_results. SUM (DECODE (GREATEST (shipjate. FC.q4_sdate). shipjate. DECODE (LEAST (shipjate. FC.q4_edate). shipjate, 1, 0), 0)) Q4_results FROM orders 0, fiscal_calendar FC GROUP BY yearjiumber; Результат подобного запроса должен выглядеть примерно так: YEAR NUMBER Ql RESULTS 02 RESULTS Q3 RESULTS 04 RESULTS 1993 12000 14005 22000 40000 1994 10000 15000 21000 55004 Хотя с помощью функции DECODE создавать такие отчеты очень удобно, однако код SQL при этом выглядит громоздко. Условие, которое применяется при фор- формировании значения Ql RESULTS, звучит так: если дата отгрузки больше или равна дате начала первого квартала и меньше или равна дате конца первого квартала, то к сумме общего числа заказов, отгруженных в этом квартале, необходимо доба- добавить единицу, в противном случае — добавить нуль.
Дополнительные вопросы 603 Тем, кто не имеет опыта работы с функцией DECODE, рассматриваемый код по- покажется сложным. Однако его можно несколько упростить путем создания хра- хранимой функции incr_in_range: FUNCTION incrjn_range (sh1p_date_in IN DATE, sdatejn IN DATE, edatejn IN DATE) RETURN INTEGER IS BEGIN IF ship_datejn BETWEEN sdatejn AND edatejn THEN RETURN 1; ELSE RETURN 0; END IF; END: При ее использовании SQL-код предыдущей программы будет выглядеть так: SELECT FC.yearjiumber. SUM Ancrjn_range (ship_date. ql_sdate. ql_edate)) Qlj-esults, SUM Ancrjn_range tship_date, q2_sdate, q2_edate)) Q2_results, SUM Ancrjn_range Csh1p_date. q3_sdate. q3_edate)) ОЗЗг-esults. SUM Ancrjn_range [sh1p_date, q4_sdate, q4 edate)) Q4 results FROM orders 0. f1scal_calendar FC GROUP BY year_number: Хранимая функция incrjn_range позволила избавиться от избыточного кода, после чего инструкция SELECT стала намного понятнее. Более того, теперь эту функ- функцию можно использовать для аналогичных целей и в других инструкциях SQL. Директива RESTRICT_REFERENCE (Orade8 и более ранние версии) В версиях РСУБД до Oracle8i вызов объявленной в пакете функции из SQL воз- возможен только при наличии директивы компилятора RESTRICT_REFERENCES. Указан- Указанная директива определяет «уровень чистоты» функции, сообщая Oracle о том, что функция создает определенные побочные эффекты или же что таковые у нее от- отсутствуют. Работать с директивой RESTRICT_REFERENCE непросто, поэтому многих PL/SQL- разработчиков порадовало известие о том, что в Oracle8i она стала необязатель- необязательной. Материал, представленный в данном разделе, предназначен в первую оче- очередь для тех, кто применяет Огас1е8 и более ранние версии РСУБД. Для каждой пакетной функции, которую вы хотите использовать в инструк- инструкции SQL, требуется задать отдельную директиву и расположить ее после объяв- объявления функции в спецификации пакета. При установке «уровня чистоты» с по- помощью рассматриваемой директивы используется следующий синтаксис: PRAGMA RESTRICT_REFERENCES Шя_фунщт. WNDS [. WNPS] [, RNDS] [. RNPS])
604 Глава 16 • Процедуры, функции и параметры Здесь имя_функции — это имя используемой функции. Ее «уровень чистоты» зада- задается с помощью четырех параметров, указывающих, чего функция не делает: О WNDS (Writes No Database State) — не модифицирует таблицы базы данных; О WNPS (Writes No Package State) - не модифицирует переменные пакета; О RNDS (Reads No Database State) — не считывает информацию, представленную в таблицах базы данных; О RNPS (Reads No Package State) — не считывает значения переменных пакета. Ниже приведен пример того, как устанавливается «уровень чистоты» для двух функций пакета companyjfinancials: PACKAGE company_financ1als IS FUNCTION company_type Ctype_code_in IN VARCHAR2) RETURN VARCHAR2: FUNCTION conpanyjiame ( company_id_in IN company.company_idSTYPE) RETURN VARCHAR2; PRAGMA RESTRICT_REFERENCES ( cofnpany_type, WNDS. RNDS. WNPS. RNPS): PRAGMA RESTRICT_REFERENCES ( companyjiane. WNDS. WNPS. RNPS): END company_financials; Функция company name пакета считывает из базы данных имя указанной компа- компании. Заметьте, что обе директивы размещены рядом, в конце спецификации паке- пакета, поскольку никакая директива не должна следовать непосредственно за специ- спецификацией функции. Кроме того, мы столкнулись здесь с проблемой указания ар- аргументов WNPS и RNPS для обеих функций. Для того чтобы компилятор не смог проигнорировать функцию, Oracle рекомендует установить самый высокий из воз- возможных «уровней чистоты». 1РИМЕЧАНИЕ- Если функция, которую вы хотите использовать в SQL, вызывает какую-либо процедуру или функ- функцию пакета, для последней следует задать директиву RESTRICT_REFERENCE, Следовательно, неяв- неявно вызываемые из SQL программы также должны подчиняться приведенным правилам. Если функция нарушит установленную для нее директиву, вы получите сооб- сообщение об ошибке PLS-00452: subprogram 'program' violates its associated pragma. Предположим, что тело пакета company _financials выглядит следующим образом: CREATE OR REPLACE PACKAGE BODY company financials IS FUNCTION companyjype (type_code_1n IN VARCHAR2) RETURN VARCHAR2 IS vsal NUMBER: BEGIN
Дополнительные вопросы 605 SELECT sal INTO v_sal FROM anp WHERE empno - 1; RETURN 'bigone'; END: FUNCTION companyjiame (campany_id_in IN company.company_id*TYPE) RETURN VARCHAR2 IS BEGIN UPDATE вир SET sal = 0: RETURN 'bigone': END: ENO company_financials: При его компиляции мы получим следующее сообщение об ошибке: 3/4 PLS-00452: Subprogram 'cohpanyjype' violates its associated pragma / Объясняется это тем, что функция company_type считывает информацию из базы данных, а для нее установлен уровень чистоты RNDS. Если убрать эту инструкцию SELECT, будет получено такое сообщение: 11/4 PLS-00452: Subprogram 'COMPANYJIAME' violates its associated pragma В данном случае ошибка появляется потому, что функция company_name моди- модифицирует базу данных, а для нее установлен уровень чистоты WNOS. В некоторых случаях функция как будто не нарушает объявленный для нее уровень чистоты и не использует инструкции UPDATE, DELETE или UPDATE, но компилятор ее не обра- обрабатывает. Это может случиться по той причине, что вы вызываете встроенный па- пакет или каким-либо другим образом нарушаете правила. Еще раз отметим, что при использовании Огас1е8г и более поздних версий РСУБД директива RESTRICT_ REFERENCE не требуется — исполнительное ядро автоматически проверит ваш код на наличие нарушений. Табличные функции Oracle8i и Oracle9i позволяют создавать табличные функции. Эти функции воз- возвращают результирующий набор данных (коллекцию), который можно рассмат- рассматривать как реляционную таблицу в предложении FROM запроса. Вот почему их на- называют табличными.. Табличные функции мы рассмотрим в следующих разделах, сконцентрировав ваше внимание на тех из них, которые доступны в Огас1е9г. О Конвейерные функции. Возвращают результирующий набор данных в «кон- «конвейерном стиле», то есть возврат данных производится до завершения работы функции (поддерживается в ОгаскЭг). Конвейерные табличные функции удоб- удобно примепять для выполнения последовательной трансформации данных, по- поскольку они позволяют не задействовать локальные структуры данных PL/SQL для хранения промежуточных результатов. О Функции трансформации. Участвуют в параллельном выполнении запросов, то есть они могут запускаться одновременно в нескольких параллельных про- процессах (поддерживается в Oracle9i). Далее будет рассказано, как табличные функции следует объявлять и использо- использовать в приложениях.
606 Глава 16 • Процедуры, функции и параметры Вызов функций в предложении FROM Чтобы обеспечить вызов функции из предложения FROM, требуется выполнить сле- следующие действия. О В предложении RETURN определить тип возвращаемых функцией данных как коллекцию (вложенную таблицу или VARRAY). О Убедиться, что все остальные параметры функции имеют режим использова- использования IN и тип данных, поддерживаемый SQ.L. (В запросе нельая вызывать функ- функцию, имеющую аргумент типа BOOLEAN.) О Поместить вызовы функции в операторы TABLE и CAST. Рассмотрим пример, который работает как в Oracle8i, так и в Oracle9z. Снача- Сначала создадим вложенный табличный тип, базирующийся на объектном типе pet_t; CREATE TYPE pet_t IS OBJECT ( NAME VARCHAR2 F0), breed VARCHAR2 A00). dob DATE): CREATE TYPE pet_nt IS TABLE OF pet_t: После этого создадим функцию pet_family, которая принимает в качестве ар- аргументов два объекта, типа pet_t — mom_in (мать) и dad_in (отец), и в зависимости от вида животного возвращает вложенную таблицу с информацией о составе его «семьи», определенном в коллекции. CREATE OR REPLACE FUNCTION petjanrily (dad in IN pet t. mom in IN pet t) RETURN pet_nt IS 1_count PLSJNTEGER; retval pet_nt :- petjvt 0; PROCEDURE extend assign (pet 1n IN pet t) IS BEGIN retval.EXTEND; retval (retval.LAST) :- pet 1n; END; BEGIN extend_assign (dadjn); extend_assign (monjin); IF momjn. breed - 'RABBIT' THEN 1 count :- 12; ELSIF mm_1n.breed - 'DOG' THEN. Г count :- 4; ELSIF motnjn.breed - 'KANGAROO' THEN l_count :- 1; END IF; FOR indx IN 1 .. l_count LOOP extend_ass1gn (pet_t ('BABY1 || 1ndx, mom 1n.breed, SYSDATE)); END LOOP; RETURN retval; END;
Дополнительные вопросы 607 Эту функцию можно вызвать из предложения FROM запроса. Делается это сле- следующим образом: SQL> SELECT * 2 FROM TABLE CCAST t 3 pet_fMi1ly ( 4 pet t ('Hoppy', 'RABBIT'. SYSOATE), 5 petit ('Hippy1, 'RABBIT'. SYSDATE) 6 ) AS "pet_nt 7 )); NAME BREED DOB Hoppy RABBIT 27-FEB-02 Hippy RABBIT Z7-FEB-02 BABY1 RABBIT 27-FEB-02 BABY2 RABBIT 27-FEB-02 BABY11 RABBIT 27-FEB-02 BABY12 RABBIT 27-FEB-02 Как было отмечено выше, этот подход можно реализовать в версиях Oracle8i и Огас1е9>. Сейчас мы рассмотрим дополнительные возможности, доступные толь- только в Огас1е9:. Конвейерные функции Конвейерные функции - это функции, которые получают набор строк и возвра- возвращают их результирующий набор итеративно, то есть по одной строке. Внутри та- такой функции содержится оператор PIPE ROW, который вьвдает одну строку резуль- результата и приостанавливает выполнение функции до тех пор, пока среда, вызвавшая данную функцию, не потребует вывода следующей строки. Этот прием позволяет «нанизывать» такие функции друг на друга и организовывать конвейер преобра- преобразования данных. Ниже приведена конвейерная версия функции pet_f ami ly: 1 CREATE FUNCTION pet_family (dad_in IN pet t. nramjn IN pet_t) 2 RETURN pet_nt PIPELINED 3 IS 4 l_count PLSJNTEGER; 5 retval pet_nt :- pet nt 0; 6 7 BEGIN 8 PIPE ROW (dadjn): 9 PIPE ROW (momin); 10 11 IF monjn.breed - 'RABBIT1 THEN 1 count :- 12: 12 ELSIF nramjn.breed - 'DOG' THEN 1"count :- 4: 13 ELSIF ranjin.breed - 'KANGAROO' THEN 1 count ;- 1; 14 END IF; 15 16 FOR 1ndx IN 1 .. 1 count 17 LOOP 18 PIPE ROW (pet t C8ABY1 || indx, morajn.breed. SYSDATE)):
608 Глава 16 • Процедуры, функции и параметры 19 END LOOP: 20 21 RETURN; 22 END: Изменения, которые были внесены в исходную версию функции, перечисле- перечислены в следующей таблице. Строка Описание 2 Введено ключевое слово PIPELINED, необходимое для указания Oracle на то, что строки будут возвращаться последовательно 8, 9,18 Вместо присваивания строк данных коллекции подготавливается строка информации (имеющая ту же структуру, что и строка коллекции, тип которой указан в предложении RETURN функции), а затем, для последовательного возврата данных из функции, используется оператор PIPE ROW 21 Введен пустой оператор RETURN (ранее доступный только в процедурах); применяемый в конвейерных функциях для передачи управления вызывающему блоку Конвейерные функции можно вызывать из инструкций SQL, как это показано выше, или используя более простой синтаксис: SELECT * FROM TABLE (pet_family ( pet_t ('Bob'. 'KANGAROO1. SYSOATE). pet_t ('Sally'. 'KANGAROO'. SYSDATE))); Таким образом, теперь нет необходимости использовать функцию CAST для яв- явного приведения типа коллекции к типу, распознаваемому базой данных. Какие же преимущества дает использование конвейерных функций? В пер- первую очередь повышается эффективность приложений, в которых работа со стро- строками данных должна начинаться до формирования полного результирующего на- набора строк. В конце следующего раздела будет приведен сценарий, выполняю- выполняющий сравнение производительности разных приложений. Функции трансформации Функции трансформации —. это конвейерные функции, которые принимают в ка- качестве параметра результирующий набор данных (с помощью курсорного выра- выражения) и возвращают также результирующий набор данных. Функции такого типа поддерживаются в Oracle9i и могут обеспечить существенное повышение произ- производительности приложения. Вы можете определить табличную функцию с IN-аргументом типа REFCURSOR и вызвать ее, задав курсорное выражение в качестве фактического параметра. Та- Такой прием позволяет создать конвейер для передачи данных от одной функции к другой или из одной инструкции SQL в следующую, без организации промежу- промежуточного хранения данных. Рассмотрим, как использовать эти возможности. ПРИМЕЧАНИЕ Приведенный ниже код находится в файле tabfunc.sql на web-узле издательства O'Reilly.
Дополнительные вопросы 609 Предположим, что у нас имеется таблица с информацией, полученной от тике- тикера (биржевого аппарата, передающего котировки ценных бумаг). Она содержит отдельную строку для цен на момент открытия и закрытия биржи: CREATE TABLE StockTabl e ( ticker VARCHAR2A0). open_price NUMBER. close_price NUMBER): Эту информацию необходимо поместить в другую таблицу: CREATE TABLE TickerTable С ticker VARCHAR2U0). PhceType VARCHAR2C1). price NUMBER): Другими словами, одна строка таблицы StockTabl e должна быть преобразована в две строки таблицы TickerTable. Существует несколько путей достижения этой цели. При использовании традиционных методов в версиях до Огас1е9г можно написать код, подобный этому: FOR rec IN (SELECT * FROM stocktable) LOOP INSERT INTO tickertable (ticker, pricetype, price) VALUES (rec.ticker. '0'. rec.open_price); INSERT INTO tickertable (ticker, pricetype. price) VALUES (rec.ticker, 'C. rec.close_price); ENO LOOP; Однако при наличии больших объемов данных программа будет работать не так эффективно, как хотелось бы. Рассмотрим, как с помощью функции транс- трансформации можно ускорить выполнение этой задачи. Сначала создадим тип коллекции, которая будет использоваться в функции: CREATE TYPE TickerType AS OBJECT ( ticker VARCHAR2(W). PhceType VARCHAR2U), price NUMBER); CREATE TYPE TickerTypeSet AS TABLE OF TickerType: Затем создадим пакет, в спецификации которого содержится объявление типа REF_CURSOR (что позволяет ссылаться на него из функции): CREATE OR REPLACE PACKAGE refcur_pkg TYPE refcurj IS REF CURSOR RETURN StockTablelROWTYPE: END refcur_pkg: Код функции трансформации имеет следующий вид: CREATE OR REPLACE FUNCTION StockPivot ( p refcur_pkg.refcur_t) RETURN TickerTypeSet PIPELINED
610 Глава 16 • Процедуры, функции и параметры IS out_rec Ticker-Type ;- TickerType (NULL. NULL. NULL): in_rec p*ROWTYPE; BEGIN LOOP FETCH p INTO in_rec: EXIT WHEN pSINOTFOUND: -- первая строка out_rec.ticker :- in_rec.Ticker; out_rec.PriceType :- '0': out rec.price :- in_rec.Open_Price: PIPE ROW (out_rec): -- вторая строка outj-ec.Pri ceType :- 'С'; out_rec.Price :- in_rec.Close_Price; PIPE ROW (out_rec): END LOOP; CLOSE p; RETURN: END; Вот та же самая функция, определенная без использования конвейера: CREATE OR REPLACE FUNCTION StockPIvotjiopl ( p refcur_pkg.refcur_t) RETURN TickerTypeSet IS out rec TickerType ;- TickerType (NULL, NULL, NULL); 1n_rec pSROWTYPE; retval TickerTypeSet :-TidcerTypeSetO: BEGIN retval,DELETE: LOOP FETCH p INTO 1n rec; EXIT WHEN pKNOTFOUND: out_rec.ticker :- 1n_rec.Ticker; out_rec.PnceType :- '0'; out rec.price ;- in rec.Open Price; retval.EXTEND; retval (retval.LAST) ;- out_rec; out_rec. Pri ceType :- 'C; out_rec.Price :- in rec.Close Price; retval.EXTEND; retval (retval.LAST) :- out_rec; END LOOP; CLOSE p; RETURN retval;, END;
Дополнительные вопросы Как видите, существует несколько подходов к решению одной и той же зада- задачи. Вы спрашиваете, какой из них выбрать? При обработке больших объемов дан- данных предпочтение следует отдать наиболее эффективной реализации. В файле tabfunc.sql1 вы найдете анонимный блок, который сравнивает производительность четырех подходов. О Непосредственная вставка из инструкции SELECT с использованием конвейер- конвейерной функции: INSERT INTO tickertable SELECT * FROM TABLE (StockPivot (CURSOR(SELECT * FROM StockTable))); О Непосредственная вставка из инструкции SELECT с использованием неконвейер- неконвейерной функции: INSERT INTO tickertable SELECT * FROM TABLE (StockPivotjnopl (CURSORSSELECT * FROM Stock!able»): О Хранение трансформируемых данных в локальной коллекции. Для передачи содержимого коллекции в таблицу можно использовать простой цикл: OPEN curvar FOR SELECT * FROM stocktable: rpystock :- stockpivotjiopl (curvar); 1ndx :- rnystock,FIRST: LOOP EXIT WHEN 1ndx IS NULL: INSERT INTO tickertable (ticker. pricetype. price) VALUES (inystock dndx).ticker, mystock dndx), pricetype, mystock dndx).price); END LOOP; О «Старый» метод - использование цикла FOR с курсором для преобразования одной строки таблицы StockTable в две строки таблицы ПскегТаЫе: FOR rec IN (SELECT * FROM stocktable) LOOP INSERT INTO tickertable (ticker, pricetype, price) VALUES (rec.ticker, 'О'. rec,open_pr1ce); INSERT INTO tickertable (ticker, pricetype. price) VALUES (rec.ticker, "C, rec.close_pr1ce); END LOOP; В файле tabfunc.sql используется механизм таймера PL/Vision (PLVtmr), предназначенный для вы- вычисления времени выполнения программы. Этот пакет можно установить, запустив сценарий plvtmr.pkg.
612 Глава 16 • Процедуры, функции и параметры При запуске блока, сравнивающего время выполнения этих четырех подходов в сеансе SQL'Plus, были получены такие результаты: All SQL with Pipelining function Elapsed: 2.47 seconds. All SQL with non-pipel1n1ng function Elapsed: 1.78 seconds. Intermediate collection Elapsed: 6.71 seconds. Cursor FOR Loop and two inserts Elapsed: 6.9 seconds. Проанализировав их, можно сделать следующие выводы. О Использование табличных функций (как конвейерных, так и обычных) при трансформации данных в отдельной SQL-инструкции заметно улучшает про- производительность. О Конвейерная обработка не помогла, а наоборот, уменьшила скорость выполне- выполнения. Фактически мы не получили никаких преимуществ от использования конвейера. Во всех случаях, прежде чем приступить к осуществлению любых операций с данными, нам приходилось ждать, пока завершится выполнение логики программы. Есть надежда добиться улучшения временных показателей при выполнении логики программы в параллельных вычислениях, то есть в случае, когда мы полу- получаем первые N строк и начинаем обрабатывать их до поступления остальных дан- данных. Эта ситуация сымитирована в файле tabfunc.sql. Мы сравнили, сколько времени тратится на выполнение каждой из приведен- приведенных ниже инструкций: -- С конвейерной обработкой INSERT INTO tickertable SELECT * FROM TABLE (StocfcPivot (CURSDRtSELECT * FROM StoclcTable))) WHERE ROWNUM < ID: -- Без конвейерной обработки INSERT INTO tickertable SELECT * FROM TABLE (StockPivotjiopl (CURSORCSELECT * FROM StoclcTable))) WHERE ROWNUM < 10; Полученные нами результаты свидетельствуют о том, что конвейерные функ- функции работают эффективно: Pipelining first 10 rows Elapsed: .08 seconds. No pipelining first 10 rows Elapsed: 1.77 seconds ¦ Функции, доступные для параллельного выполнения В Огас1е9г одним из важных новшеств, касающихся PL/SQL, является возмож- возможность выполнения функций в параллельных запросах. В предыдущих версиях Oracle вызов функции PL/SQL из SQL-инструкции приводил к сериализации за- запроса (это одна из основных проблем, характерных для приложений хранилищ, данных). В Огас1е9г в заголовок функции можно добавить информацию, которая
Дополнительные вопросы 613 указывает исполнительному ядру, каким образом может быть использована эта функция. Если вы хотите, чтобы функция выполнялась в параллельном режиме, она должна иметь один строготипизированный входной параметр типа REFCURSOR1. Примеры использования этого подхода приведены ниже. О Функция может выполняться в параллельном режиме, и передаваемые в нее данные можно произвольно разбивать: CREATE OR REPLACE FUNCTION my_transform_fn (p_input_rows in employeejnfo.recur_t) RETURN emp1oyee_info.transfonTied_t PIPELINED PARALLELJNABLE [PARTITION p_input_rows BY ANY) Ключевое слово ANY указывает, что результаты не зависят от порядка, в кото- котором функция получает входные строки. Когда используется это ключевое сло- слово, исполнительное ядро разбивает данные между параллельными процессами случайным образом. Оно подходит для использования в функциях, которые принимают одну строку таблицы, манипулируют ее столбцами и генерируют результирующие строки только на основе столбцов этой строки. Если про- программа реализует другие зависимости, результат будет непредсказуемым. О Функция может выполняться параллельно, а все строки, относящиеся к опре- определенному отделу, будут обрабатываться одним процессом и передаваться ему последовательно: CREATE OR REPLACE FUNCTION my_transform_fn ( p_input_rows 1п employee_lnfo.recur_t) RETURN employee_info.transformed_t PIPELINED CLUSTER P_INPUT_ROWS BY (department) PARALLEL ENABLE (PARTITION P_INPUT_ROWS 8Y HASH (department)) В Oracle применяется понятие «кластеризация», используемое для обозначе- обозначения данного режима обработки, и термин «кластерный ключ» — для обозначе- обозначения столбца (в данном случае ' department'), по которому выполняется агреги- агрегирование. Важно, что алгоритм не зависит от порядка значений кластерного ключа, и Oracle вообще не гарантирует здесь какой-либо упорядоченности. Это позволяет использовать более быстрые алгоритмы, чем в случае, когда строки должны быть кластеризованы и доставлены в порядке, определяемом значенями кластерного ключа. В зависимости от имеющейся у нас информации о распределении значений, в этом примере можно применять форму HASH (department) или RANGE (department). Форма HASH (department) обеспечивает большую скорость обработки, и будет естественным использовать ее совместно с предложением CLUSTER... BY. Входной параметр типа REF_OJRSOR может не быть строготипизированным при разбиении данных с использованием ключевого слова ANY.
614 Глава 16 ¦ Процедуры, функции и параметры О Функция может выполняться параллельно, а строки передаются конкретным параллельным процессам, как указано в предложении PARTITION...BY, и сортиру- сортируются этим процессом локально. Это позволяет распараллелить сортировку: CREATE OR REPLACE FUNCTION n\y_transform_fn ( p_input rows 1n employee_info.reair_t) RETURN employeejnfo.transformed J; PIPELINED ORDER PJNPUT ROWS BY [CD PARALLEL ENABLE (PARTITION P_INPUT_ROWS BY RANGE (CD) Поскольку сортировка распараллелена, не нужно использовать предложение ORDER... BY в инструкции SELECT, которая применяется для вызова табличной функ- функции. (Фактически наличие предложения ORDER...BY в инструкций SELECT не по- позволит распараллелить сортировку.) Более логично использовать ключевое сло- слово RANGE с предложением ORDER..BY, хотя программа и будет работать медленнее, чем с предложением CLUSTER...BY. ПРИМЕЧАНИЕ Предложение CLUSTER...BY не может применяться вместе с предложением ORDER...BY в объявле- объявлении табличной функции. Таким образом, алгоритм, который зависит от кластеризации по одному ключу (например, d) и использует упорядочивание строк с одинаковым значением ключа кластери- кластеризации по другому столбцу (например, с2), должен был бы распараллеливаться с использованием предложения ORDER...BY в объявлении табличной функции. Детерминированные функции Функция называется детерминированной, если при одном и том же наборе вход- входных параметров она всегда возвращает одно и то же значение. Пример такой функ- функции приведен ниже: CREATE ОЯ REPLACE FUNCTION betwnStr ( stringjn IN VARCHAR2.start_in IN INTEGER, end_in IN INTEGER ) RETURN VARCHAR2 IS BEGIN RETURN ( SUBSTR С string 1n. start 1n, end 1n - startjn + D); END: Если в качестве входных параметров использовать, например, последователь- последовательность символов «abcdef», значение 3 (параметр startjn) и значение 5 (параметр end_i n), то сколько бы раз вы ни вызывали функцию betwnStr с этим набором па- параметров, она всегда будет возвращать подстроку «cde». Почему бы Oracle не сохранить результаты, связанные со списком аргумен- аргументов? Ведь в этом случае при последующем вызове функции с теми же аргумента- аргументами можно получить результат, не выполняя ее. Такого эффекта можно добиться, добавив предложение DETERMINISTIC в заголовок функции: CREATE OR REPLACE FUNCTION betwnStr ( Stringjn IN VARCHAR2, Startjn IN INTEGER, endjn IN INTEGER
Дополнительные вопросы 615 RETURN VARCHAR2 DETERMINISTIC IS BEGIN RETURN ( SUBSTR ( stdngjin, startjin, end_1n - start_in +1)); END: Решение об использовании сохраненной копии возвращаемых функцией ре- результатов (если такая копия доступна) принимается оптимизатором запросов Oracle. Сохраненные копии могут браться из материализованного представле- представления, функционального индекса или повторного вызова одной и той же функции в SQL-инструкции. ПРИМЕЧАНИЕ Для того чтобы функцию можно было вызвать в выражении, основанном на функциональном индек- индексе, или из запроса материализованного представления, помеченного как REFRESH FAST или * ENAMBLE QUERY REWRITE, ее следует объявить как DETERMINISTIC. В Oracle не имеется надежного способа проверки того, что объявленная как DETERMINISTIC функция не имеет никаких побочных эффектов. Поэтому при ис- использовании таких функций вся ответственность за последствия возлагается на программиста. Детерминированная функция не должна основываться на пере- переменных пакета и иметь право изменять результирующие значения в базе данных. Модульный подход — в жизнь! По мере развития языка PL/SQL и средств Oracle становятся более сложными и приложения, которые разрабатываются с их помощью. Если вы будете плохо вла- владеть техникой модуляризации в PL/SQL, то вряд ли сможете реализовать круп- крупный проект. Конечно, мы не смогли описать все особенности модуляризации в PL/SQL, однако надеемся, что представленного нами материала достаточно для того, что- чтобы подтолкнуть вас к мысли о необходимости совершенствоваться в этой техноло- технологии. Вам предстоит изучить полный спектр возможностей пакетов, способы рас- расширения пакетов, которые поставляются совместно с инструментами и базами данных, различные приемы повторного использования кода и многое другое. Когда вы овладеете техникой модуляризации, то заметите, что тратите больше времени на разработку, а процесс отладки проходит быстрее. Ваши программы станут более читабельными и легкими в сопровождении. Словом, беритесь за дело и внедряйте модульный подход!
17 Пакеты > Для чего нужны пакеты > Правила построения пакетов > Правила вызова элементов пакета ^ Работа с данными пакета > В каких случаях следует использовать пакеты > Пакеты и объектные типы Пакет представляет собой сгруппированный по определенным правилам набор элементов кода PL/SQL. Он обеспечивает логическую структуру, предназначен- предназначенную для организации программ, курсоров, типов данных и переменных. Значи- Значимость пакетов объясняется тем, что они позволяют скрывать логику и данные, а также определять глобальные данные, существующие в течение сеанса. В предыдущих изданиях книги было отмечено, что пакеты относятся к числу наименее популярных средств PL/SQL, хотя на самом деле они крайне необходи- необходимы для разработки хорошо структурированных приложений PL/SQL. К счастью, с тех пор ситуация изменилась — разработчики оценили возможности пакетов и стали активно их применять. Для чего нужны пакеты Пакеты — очень важная составная часть языка PL/SQL, без использования кото- которой невозможно создать сложный прикладной проект. Чтобы это понять, рас- рассмотрим, какие преимущества обеспечивают пакеты. О Упрощают сопровождение и расширение приложений. При увеличении объема кода качество приложений определяется тем, насколько легко их сопровож- сопровождать. С этой точки зрения пакеты играют исключительна важную роль, по- поскольку обеспечивают инкапсуляцию кода (в частности, они позволяют скрыть SQL-инструкции за интерфейсом процедур), дают возможность определить
Для чего нужны пакеты 617 константы и сгруппировать логически связанные функции. В результате при- приложение становится более структурированным и надежным. О Повышают производительность приложений. Во многих ситуациях исполь- использование пакетов позволяет повысить производительность и эффективность ра- работы приложений. Определив постоянные структуры данных уровня пакета, можно кэшировать статические значения из базы данных. Это дает возмож- возможность избежать повторных запросов и, следовательно, значительно ускорить получение результата. Кроме того, подсистема управления памятью Oracle оп- оптимизирована для доступа к откомпилированному коду пакетов (подробнее об этом рассказывается в главе 20). О Поддержка приложений и исправление недостатков встроенных элементов. Некоторые из существующих программных компонентов Oracle имеют опре- определенные недостатки, в частности не лучшим образом реализованы важнейшие функции встроенных пакетов UTL_FILE и DBMS_OUTPUT. Мириться с этим совсем не обязательно, можно разработать собственный пакет, устранить значитель- значительную часть недостатков и использовать его вместо стандартного. Например, в сценарии файла do.pkg, имеющемся на узле O'Reilly, реализована замена встро- встроенной процедуры DBMS_OUTPUT. PUT_LINE перегруженной версией для типа данных ¦ ХМТуре. Подобного результата можно достичь и с помощью отдельных функ- функций и процедур, но использование пакетов является более предпочтительным подходом. Концепция пакетов очень проста. Единственная сложность заключается в том, чтобы научиться эффективно применять в приложении их богатые возможности. Прежде чем мы приступим к рассмотрению преимуществ пакетов и обсуждению синтаксиса их определения, внимательно ознакомьтесь с приведенной ниже ре- рекомендацией. Всегда стройте приложение на основе пакетов и старайтесь не применять от- отдельные процедуры и функции. Даже если вам сначала кажется, что для реализа- реализации определенной возможности достаточно одной процедуры или функции, в бу- будущем наверняка к ней добавятся еще несколько. Когда вы поймете, что их лучше объединить в пакет, придется искать все вызовы процедур и функций и прибав- прибавлять к ним префикс иняпакета. Демонстрация возможностей пакетов Пакет состоит из двух частей — спецификации и тела. Спецификация является обязательной частью и определяет, как разработчик может использовать пакет: какие программы можно вызывать, какие курсоры открывать и т. д. Тело паке- пакета — это необязательная, но почти всегда присутствующая часть, она содержит код перечисленных в спецификации программ (и, возможно, курсоров), а также другие необходимые элементы кода. Предположим, что нам нужна программа для получения полного имени со- сотрудника, которое хранится в базе данных в виде двух отдельных элементов: име- имени и фамилии. Задача очень проста, и на первый взгляд кажется, что для ее реше- решения достаточно создать приведенную далее процедуру.
618 Глава 17 • Пакеты CREATE OR REPLACE PROCEDURE process_employee ( employeejdjn IN employee.employeeJdXTYPE) IS ljiillname VARCHAR2U00); BEGIN SELECT lastjiame || '.' II f1rst_name INTO iJVMname FROM employee WHERE employeejd - employeejdjn; END; Однако этот простой код обладает рядом скрытых недостатков. О Жестко закодирована длина переменной l_fullname. Поскольку полное имя - это производное значение, представляющее собой конкатенацию содержимого двух столбцов, лучше так не делать. Если длина столбцов lastjiame и fi rstjia- me, хранящих фамилию и имя, будет увеличена, код данной процедуры придет- придется изменять. 0 Жестко закодировано правило, согласно которому составляется полное имя. Поэтому если через определенное время пользователь захочет видеть полное имя в формате «ИМЯ ФАМИЛИЯ», вам придется во всем коде искать конст- конструкции «ФАМИЛИЯ, ИМЯ». О SQL-запрос этого вида в разных форматах может встречаться в нескольких местах приложения. Дублирование способно затруднить сопровождение при- приложения и его оптимизацию. Приложения нужно строить таким образом, чтобы избежать жесткого кодиро- кодирования подобных элементов. Определение типа данных для полного имени, его представление, запрос к базе данных и т. п. должны кодироваться один раз в стро- строго определенном месте и быть доступны из любой точки приложения. Местом, которое удовлетворяет всем этим требованиям, является пакет. Рассмотрим следующую спецификацию пакета: 1 CREATE OR REPLACE PACKAGE employee_pkg 2 AS 3 max_salary NUMBER; 4 5 SUBTYPE fuTlname_t IS VARCHAR2 B00): 6 7 FUNCTION full name ( 8 1 employee. last_namemPE. 9 f employee.first name*TYPE) 10 RETURN full name t" 11 12 FUNCTION full name ( 13 employeejdjn IN employ ее. employee 1d*TYPE) 14 RETURN full name t: 15 16 END employeejrtg;
для чего нужны пакеты 619 Это список элементов, предназначенных для использования разработчиками, которые имеют доступ к данному пакету. В строке 5 объявляется новый тип дан- данных ful I namejt, максимальная длина его значений составляет 200 символов, но впоследствии ее легко будет изменить. Затем объявляется функция full name (строки 7-10), которая принимает фа- фамилию, имя и возвращает полное имя. Обратите внимание, что способ составле- составления полного имени в спецификации пакета не указан. Наконец, в строках 12-14 объявляется вторая функция с именем f ul I name. Она принимает первичный ключ таблицы сотрудников и возвращает полное имя со- сотрудника. Это типичный пример перегрузки, о которой рассказывалось в главе 16. До реализации данного пакета перепишем исходный блок кода таким образом, чтобы в нем использовались элементы пакета (обратите внимание на точечный синтаксис, аналогичный применяемому при ссылке на столбец таблицы - табли- ца.стобец): DECLARE l_name employee_pleg.fulinanej.; employee idjn employee.employee idltype :- 1: BEGIN l_name :- empl oyee_pkg. full name (employeejdjn); END; Переменная l_name теперь объявлена с применением типа данных fullnamet пакета empl oyee_pkg, и для того чтобы ей присвоить значение, вызывается соответ- соответствующая функция этого же пакета. Таким образом, формула построения полного имени и SQL-запрос для выборки его составляющих вынесены из кода приложе- приложения в специальный «контейнер» с функциональными элементами, специфиче- специфическими для обработки данных о сотрудниках. Полученный код прост и лаконичен. Если потребуется изменить формулу составления полного имени или увеличить размер его типа данных, достаточно внести соответствующие изменения в специ- спецификацию или тело пакета и перекомпилировать его код. Теперь рассмотрим, что собой представляет тело пакета: 1 CREATE OR REPLACE PACKAGE 80DV employee_pkg 2 AS 3 FUNCTION full name ( 4 1 employee. lastjiamefcTYPE. 5 f employee.first_namemPE 6 ) 7 RETURN full nameJ: 8 IS 9 BEGIN 10 RETURN 1 || '.' || f; 11 END: 12 FUNCTION full name (employeejdjn IN employee, employee 1d*TYPE) 13 RETURN fullnamej; 14 IS 15 retval full name t: 16 8EGIN 17 SELECT fullname (lastjiane, f1rst_name) INTO retval IB FROM employee
620 Глава 17 • Пакеты 19. WHERE employeejd - employee_id_in; 20 RETURN retval; 21 EXCEPTION 22 WHEN NO_DATA_FOUND THEN RETURN NULL; 23 WHEN TOO_MANY_ROWS THEN errpkg.recnstop; 24 END: 25 END employee_pkg; В строках 3-11 находится функция-оболочка для формулы, задающей полное имя в формате «ФАМИЛИЯ, ИМЯ». Вторая функция (строки 12-24) содержит типичный запрос на выборку одной строки, выполняемый с помощью неявного курсора. Обратите внимание на то, что в строке 17 запрос вызывает функцию ful I name, возвращающую комбинацию двух компонентов имени. Если теперь пользователь попросит изменить формат представления полного имени сотрудника на «ИМЯ ФАМИЛИЯ», не нужно искать по всему приложе- приложению последовательность символов 11 ',' 11 — для установки нового формата достаточно модифицировать в пакете empl oyee_pkg функцию ful I name. Понятия и концепции, связанные с пакетами Прежде чем рассматривать особенности синтаксиса и структуры пакетов, пояс- поясним некоторые связанные с ними понятия и концепции. О Сокрытие информации. Выполняя сокрытие информации о системе или при- приложении, обычно преследуют две цели. Во-первых, освободить пользователя (разработчика приложения) от необходимости вникать в ненужные детали и по- помочь ему сконцентрироваться на важном. Во-вторых, воспрепятствовать дос- доступу к конфиденциальной информации. Например, формула, вычисляющая некоторое значение, может быть не важна для разработчика, а может быть и секретна. В обоих случаях имеет смысл предоставить ему интерфейс для вы- вызова функции, вычисляющей это значение. О Общие и личные элементы. Концепция общих и личных элементов тесно свя- связана с концепцией сокрытия информации. Общий код определяется в специ- спецификации пакета и доступен любой схеме, имеющей на этот пакет привилегию EXECUTE. Личный код определяется и видим только внутри пакета. Внешние программы, использующие пакет, не могут видеть или вызывать личный код. Приступая к разработке пакета, нужно решить, какие из его элементов будут общими, а какие — личными. Кроме того, тело пакета можно скрыть от других схем/разработчиков. В таком случае пакет применяется для сокрытия деталей реализации программ. Это особенно важно, если требуется изолировать пере- переменные компоненты приложения, такие как фрагменты кода, зависящие от платформы, часто меняющиеся структуры данных и временные решения. О Спецификация пакета. Она содержит определения всех общедоступных эле- элементов пакета, на которые можно ссылаться извне. Спецификация похожа на большой раздел объявлений, она не содержит блоков PL/SQL или исполняе- исполняемого кода. Если спецификация хорошо спроектирована, разработчик может узнать из нее всю требуемую для использования пакета информацию. При этом никогда не возникнет необходимость заглянуть «за интерфейс», то есть в тело пакета, содержащее реализацию его компонентов.
Для чего нужны пакеты 621 О Тело пакета. Здесь находится весь код, который необходим для реализации элементов, определенных в спецификации пакета. Тело может содержать от- отсутствующие в спецификации личные элементы, на которые нельзя ссылаться извне пакета, в частности объявления переменных и определения пакетных модулей. Кроме того, в теле пакета может находиться исполняемый (инициа- лизационныи) раздел, который выполняется только один раз для инициализа- инициализации пакета. О Инициализация. Концепция инициализации хорошо известна любому про- программисту. Однако в контексте пакетов она имеет особое значение. В данном случае происходит не присвоение значения отдельной переменной, а инициа- инициализация всего пакета путем выполнения заданного кода произвольной слож- сложности. При этом Oracle самостоятельно следит за тем, чтобы пакет инициали- инициализировался только один раз за сеанс. О Постоянство в течение сеанса. Концепция постоянства (или сохраняемости) также хорошо знакома программистам. Когда вы подключаетесь к Oracle (на- (начинаете сеанс) и выполняете программу, присваивающую значение перемен- переменной уровня пакета (то есть переменной, объявленной в пакете вне содержа- содержащихся в нем программ), эта переменная сохраняет значение в течение всего сеанса, даже если выполнение присвоившей его программы завершается. Диаграмма Буча Давайте подробнее рассмотрим концепцию личных и общих данных в пакетах, поскольку именно она обеспечивает программистам максимально гибкий кон- контроль над структурами данных и программами. Для этого воспользуемся диа- диаграммами Буча, названными так по имени их создателя - Града Буча. Область, обозначенная на диаграмме надписью «Снаружи» (рис. 17.1), вклю- включает все написанные вами программы, которые не входят в пакет (то есть являют- являются внешними по отношению к нему). Область «Внутри» представляет собой тело пакета (его реализацию). Внутри Тело пакета Снаружи Внешняя : программа Внешняя программа Рис. 17.1. Диаграмма Буча для общих и личных элементов пакета
622 Глава 17 • Пакеты На основании диаграммы Буча можно сделать следующие выводы. О Внешние программы не имеют возможности «пересечь» границу между наруж- наружной и внутренней областями. Это означает, что внешняя программа не может обращаться к элементам, определенным внутри пакета, они являются личными и невидимы извне. О Для элементов, определенных в спецификации пакета (они обозначены на ри- рисунке как «Общие»), граница между внутренней и наружной областями не яв- является препятствием. Общие программы могут вызываться внешними и лич- личными программами, а также сами вызывать любые элементы пакета. О Общие элементы пакета являются единственной «дорогой» внутрь его, а спе- спецификация пакета действует как «контрольно-пропускной механизм». О Чтобы личный объект (модуль или курсор) стал общим, нужно добавить его в спецификацию и перекомпилировать пакет. После этого объект будет видим извне. Правила построения пакетов Пакет имеет очень простую конструкцию. Хотя синтаксис и правила его построе- построения вы изучите быстро, полное понимание нюансов реализации пакета придет через некоторое время. В данном разделе рассматриваются правила построения паке- пакетов, а далее в этой главе рассказывается, в каких случаях пакеты могут быть вам полезны. Чтобы создать пакет, необходимо написать его спецификацию и, как правило, тело. При этом нужно решить, какие элементы пакета будут указаны в специфи- спецификации, а какие - скрыты в теле. В пакет также можно включить блок кода, кото- который Oracle будет выполнять при инициализации пакета. Спецификация пакета Спецификация пакета содержит список всех доступных его элементов и предос- предоставляет разработчику информацию, необходимую для применения пакета в при- приложениях. Ее часто называют программным интерфейсом — API. Для того чтобы узнать, как применять описанные в спецификации элементы, разработчику не нужно изучать код, находящийся в теле пакета. При разработке спецификации пакета необходимо учитывать следующие осо- особенности. О Элементы, объявленные вне конкретных процедур и функций пакета, называ- называются данными уровня пакета. В качестве таковых могут выступать перемен- переменные и константы практически любых типов — числа, строки, коллекции и т. п. Однако если объявление констант на уровне пакета является приемлемым, то объявления переменных лучше избегать. В пакете (как в спецификации, так и в его теле) нельзя объявлять курсорные переменные (типа REF CURSOR), поскольку они не могут сохранять свое значе- значение на протяжении сеанса (о постоянстве данных пакетов подробно рассказы- рассказывается в разделе «Работа с данными пакета» далее в этой главе).
Правила построения пакетов 623 О В спецификации допускается объявление типов для любых структур данных, таких как коллекции, записи или курсорные переменные. О В спецификации можно объявлять процедуры и функции, но в ней должны быть указаны только их заголовки (часть определения процедуры или функ- функции до ключевого слова IS или AS). О В спецификацию пакета допускается включать явные курсоры. Они могут быть представлены в одной из двух форм: SQL-запрос является либо частью объяв- объявления курсора, либо он скрыт в теле пакета (тогда в объявлении будет присут- присутствовать только предложение RETURN). Эта тема подробно рассматривается в раз- разделе «Работа с пакетными курсорами». О Если в спецификации пакета объявлены процедура или функция либо пакет- пакетный курсор без запроса, то тело пакета должно включать реализацию данных элементов. О Спецификация пакета может содержать предложение AUTHID, определяющее, как будут разрешаться ссылки на объекты данных: в соответствии с привиле- привилегиями владельца пакета (AUTHID DEFINER) или того, кто его вызывает (AUTHID CURRENTJJSER). Подробнее об этом рассказывается в главе 20. О После оператора END в конце спецификации пакета можно поместить необяза- необязательную метку, идентифицирующую пакет. END repackage; Рассмотрим пример спецификации пакета, иллюстрирующий данные особен- особенности и правила: 1 CREATE OR REPLACE PACKAGE favorites pkg 2 AUTHID CURRENTJSER 3 IS 4 -- Две константы: обратите внимание на то. 5 -- что ии присвоены информативные имена. 6 7 c_chocolate CONSTANT PLSJNTEGER :- 16: 8 c_strawberry CONSTANT PLSJNTEGER :- 29: 9 10 -- Объявление типа вложенной таблииы 11 TYPE codesjit IS TABLE OF INTEGER; 12 13 -- Вложенная таблица, объявленная нз основе этого типа 14 rny_favorites codes_nt: 15 16 -- REF CURSOR, возвращающий информации из таблицы favorites 17 TYPE fav_info_rct IS REF CURSOR RETURN favoritesSRDWTYPE; 18. 19 -- Процедура, принимающая список значений объявленного 20 -- выше типа codesjit и выводящая соответствующую 21 -- информацию из таблицы 22 PROCEDURE show_favor1tes Oistjn IN codesjit); 23
624 Глава 17 • Пакеты 24 -- Функция, возвращающая всю информации из таблицы 25 -- favorites о самом популярном элементе 26 FUNCTION mostjopular RETURN fa\ijr\fo_rct; 27 28 END favorites_pkg; -- Закрывающая метка пакета Как видите, пакет имеет почти такую же структуру спецификации, как раздел объявлений блока PL/SQL. Единственным отличием является то, что специфи- спецификация не может содержать кода реализации. Тело пакета Тело пакета содержит весь код, требуемый для реализации спецификации пакета. Оно не является обязательным компонентом (примеры пакетов без тела вы най- найдете в разделе «В каких случаях следует использовать пакеты»). Однако при на- наличии указанных ниже условий тело пакета является необходимым. О В спецификации пакета содержится объявление курсора с предложением RETURN. В теле пакета нужно задать инструкцию SELECT. О В спецификации пакета содержится объявление процедуры или функции. В этом случае в теле пакета должна присутствовать полная реализация данно- данного модуля. О Необходимо выполнить код в инициализационном разделе тела пакета. По- Поскольку спецификация пакета не содержит исполняемого раздела (исполняе- (исполняемых операторов между ключевыми словами BEGIN и END), ишщиализационный раздел может размещаться исключительно в теле пакета. Тело пакета по своей структуре очень похоже на процедуру, однако в отличие от нее характеризуется следующими особенностями. О В теле пакета может находиться раздел объявлений, исполняемый раздел и раз- раздел обработки исключений. О Раздел объявлений включает полную реализацию курсоров и программ, объяв- объявленных в спецификации, а также определение личных элементов (не объяв- объявленных в спецификации). Он может быть пустым, если в пакете имеется раз- раздел инициализации. О Исполняемый раздел пакета (инициализационный) содержит необязательный код, выполняемый при создании и обработке пакета в течение сеанса. Эта тема подробно освещается в следующем разделе. О Раздел исключений содержит код для обработки исключений, которые гене- генерируются при выполнении инициализационного раздела. Он может присутст- присутствовать в конце пакета лишь в том случае, когда в пакете имеется инициализа- инициализационный раздел. О Тело пакета может включать: только раздел объявлений; только исполняемый раздел; исполняемый раздел и раздел исключений; раздел объявлений, испол- исполняемый раздел и раздел исключений. О Тело пакета не может содержать предложения AUTH1D, так как оно должно рас- располагаться в спецификации. В теле пакета можно ссылаться на любые элемен- элементы, объявленные в спецификации.
Правила построения пакетов 625 О Все правила и ограничения, используемые при объявлении структур данных уровня пакета в его спецификации, относятся и к телу пакета. В частности, в теле пакета нельзя объявлять курсорные переменные. 0 После ключевого слова END в конце тела пакета может располагаться необяза- необязательная метка, идентифицирующая пакет: END щу_раскаде: Рассмотрим пример реализации тела пакета favorites_pkg: CREATE OR REPLACE PACKAGE BODY favorites_pkg IS -- Личная переменная g_most_popular PLSJNTEGER :- 29: -- Реализация процедуры 1 PROCEDURE show_favoi-ites (I1st_in IN codesjrt) IS BEGIN FOR indx IN listjn.FIRST .. list in.LAST LOOP DBMS_OUTPUT.putJine O1st_1n dndx)): END LOOP: END show_favorites; -- Реализация функции FUNCTION most_popular RETURN fav_info_rct IS retval fav_info_rct; null_cv fav_1nfo ret: BEGIN OPEN retval FOR SELECT * FROM favorites WHERE code - gjnostpopular; RETURN retval; EXCEPTION WHEN NO_DATA_F0UND THEN RETURN null_cv: END mostjjopular; END favontes_pkg; -- Закрывающая метка пакета Другие примеры пакетов приводятся в разделе «В каких случаях следует ис- использовать пакеты». Инициализация пакетов Пакет может содержать структуры данных, сохраняющиеся на протяжении всего сеанса (см. раздел «Работа с данными пакета»). Когда в течение сеанса впервые происходит обращение к пакету (вызывается объявленная в нем программа, счи- тывается или записывается значение переменной либо используется объявлен- объявленный в пакете тип), Oracle инициализирует его, выполняя следующие действия (одно или все). 1. Обрабатывает данные уровня пакета (значения переменных и констант).
626 Глава 17 • Пакеты 2. Присваивает переменным и константам значения по умолчанию, указанные в их объявлениях. 3. Выполняет блок кода, содержащийся в инициализационном разделе. ПРИМЕЧАНИЕ Если возникает необходимость перекомпиляции пакета или сбрасывается состояние всего сеанса, на что указывает ошибка ORA-04068: existing state of packages has been discarded, то во время сеан- сеанса пакет может быть инициализирован повторно. Инициализационный раздел пакета составляют все операторы, находящиеся между ключевым словом BEGIN (вне определений процедур и функций) и ключе- ключевым словом END, завершающим тело пакета. Например, инициализационный раз- раздел пакета favorites_pkg может выглядеть следующим образом: CREATE OR REPLACE PACKAGE BODY favorites_pkg IS g_most_popu1ar PLSJNTEGER: PROCEDURE show_favor1tes (Hstjn IN codesjit) ... END: FUNCTION most_popular RETURN fav_1nfo_rct ... END; PROCEDURE analyze_favor1tes (yearjn IN INTEGER) ... END: -- Инициализационный раздел BEGIN gjirast_popular :- c_chocolate; -- Используем новую функцию 0гас1е91 EXTRACT для получения -- года из значения, возвращенного функцией SYSDATE analyze_favorites (EXTRACT (YEAR FROM SYSDATE)): END favor1tes_pkg; PL/SQL автоматически определяет, когда должен выполняться код инициа- лизационного раздела. Это означает, что нет необходимости вызывать его явно и можно быть уверенным, что он будет выполнен только один раз. О назначении инициализационного раздела будет рассказано ниже. Выполнение сложной логики при инициализации Используемые по умолчанию значения данных пакета можно присвоить непо- непосредственно в разделе объявлений. Однако этот подход имеет несколько недос- недостатков. О Логика, применяемая для присвоения значений по умолчанию, может быть довольно сложной. О Если в результате присвоения значения по умолчанию инициируется исклю- исключение, то его нельзя перехватить в пакете, а следовательно, оно выходит из па- пакета необработанным. Этот вопрос подробнее рассматривается в разделе «Ко- «Когда инициализация не удается».
Правила построения пакетов 627 Таким образом, установка начальных значений данных в инициализационном разделе пакета имеет определенные преимущества перед присвоением значений по умолчанию. Она позволяет при формировании значений воспользоваться все- всеми возможностями, которые обеспечивает программный код, и обработать внут- внутри пакета все возникающие в ходе этого процесса исключения. Кэширование статической информации сеанса Еще одним важным мотивом для включения в пакет раздела инициализации яв- является необходимость кэширования статической информации, не изменяющейся в течение всего сеанса. Если вам нужна уверенность, что информация извлекает- извлекается только один раз за сеанс, идеальным местом для выполнения подобной опера- операции является инициалиэационный раздел пакета. При работе с кэшируемыми данными пакета важно поддерживать баланс меж- между затратами памяти и процессорного времени. Кэширование данных в пакетных переменных позволяет ускорить доступ к данным за счет их перемещения в гло- глобальную область программ каждого пользователя. Если существует 1000 разных сеансов, то существует и 1000 копий кзшируемых данных, что означает ощутимое увеличение потребления памяти при уменьшении процессорного времени, затра- затрачиваемого на доступ к данным. Далее, в разделе «Кэширование статических дан- данных сеанса», об этой технологии рассказывается подробнее. Как избежать побочных эффектов инициализации Старайтесь не устанавливать значения глобальных переменных пакета в других пакетах (не важно, в каком разделе). Это поможет избежать проблем при выпол- выполнении кода и упростит сопровождение кода другими программистами. Все, что происходит в разделе инициализации, должно касаться только текущего пакета. Помните: этот код выполняется при первом обращении приложения к данному па- пакету, поэтому не стоит заставлять пользователя ждать окончания операций, кото- которые могли бы выполняться в других пакетах или триггерах. Ниже приведен при- пример того, как не следует поступать: CREATE OR REPLACE PACKAGE BODY company IS BEGIN /* 11 В инициагшэационном разделе пакета companyjkg обновляется || глобальные санные другого пакета. Это очень плохо! */ SELECT SUM (salary) INTO employee_pl<g.max_salary FROM employee: END company; Если задачи инициализации все же требуют установки значений переменных из других пакетов, то лучше объединить соответствующие операции в пакетную процедуру, присвоить ей информативное имя (например, init_environment) и вы- вызвать ее в нужный момент при инициализации приложения.
628 Глава 17 ¦ Пакеты Когда инициализация не удается Процесс инициализации пакета включает объявление данных, присвоение значе- значений по умолчанию и выполнение инициализационного раздела. Если в ходе дан- данного процесса произойдет ошибка, которая вызовет его аварийное завершение, Oracle посчитает пакет инициализированным и не станет повторно выполнять инициализационный код в течение сеанса. Чтобы в этом убедиться, достаточно создать такой пакет: /* Файл в web: vaierr.pkg */ CREATE OR REPLACE PACKAGE valerr IS FUNCTION get RETURN VARCHAR2: END valerr: CREATE OR REPLACE PACKAGE BODY valerr IS -- Обратите внимание: это глобальная переменная уровня пакета. -- но она является личной для данного пакета v VARCHAR2U) :- 'АБВ'; FUNCTION get RETURN VARCHAR2 IS BEGIN RETURN v: END: BEGIN DBMS_OUTPUT.PUT_LINE ('Ранее я видел вас в...1): EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE ('Перехвачена ошибка!1): ЕЖ valerr; Предположим, что мы подключились к Oracle из SQL*Plus и попытались вы- вызвать функцию valerr.get (впервыеза сеанс). Результат будет выглядеть следую- следующим образом: SQL> exec DBMS OUTPUT.PUT LINE (valerr.get) * ERROR at line 1: ORA-Q6502: PL/SQL: numeric or value error: character string buffer too snail Это означает, что попытка объявления переменной v с присвоением ей началь- начального значения АБВ не удалась, — она вызвала исключение VALUE ERROR. Раздел ис- исключений в конце пакета его не перехватил, поскольку он может перехватывать только те ошибки, которые генерируются в инициализационном разделе. Таким образом, исключение осталось необработанным. Однако если вызвать функцию второй раз за сеанс, ошибки не будет: SQL> BEGIN 2 DBMS_OUTPUT.PJT_LINE ('Значение переменной V раано ' || NVL (valerr.get. 'NULL'));
Правила вызова элементов пакета 629 3 END; 4 / Значение переменной V равно NULL Строка «Ранее я видел вас в...» так и не была выведена. Фактически отобра- отображающий ее оператор никогда не выполняется. Пакетная функция в первый раз завершается ошибкой, но затем каждый раз благополучно выполняется. Это при- пример классической невоспроизводимой ошибки, которая возникает во время сбоя при инициализации пакета. Подобные ошибки находить очень трудно. А для того чтобы их избежать, дос- достаточно переместить в раздел инициализации пакета присвоения всех значений по умолчанию и в разделе исключений обработать все возможные ошибки, как это сделано в следующем примере: CREATE OR REPLACE PACKAGE BODY vaierr IS v VARCHAR2U): FUNCTION get RETURN VAKCHAR2 IS BEGIN ... END; BEGIN v :- 'АБВ1; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE ('Ошибка инициализации пакета valerr:'); DBMS_OUTPUT.PUT_LINE (SQLERRM); END valerr; Правила вызова элементов пакета Говорить о вызове или выполнении пакета в целом не имеет смысла, поскольку он является всего лишь контейнером для элементов кода. Вы можете вызвать только элементы пакета (как изнутри, так и извне). Для ссылки на элементы па- пакета извне используется такой же точечный синтаксис, как для ссылки на столб- столбцы таблиц. Давайте рассмотрим несколько примеров. В следующей спецификации пакета объявляются константа, исключение, кур- курсор и несколько модулей: CREATE OR REPLACE PACKAGE petsjnc IS max_pets_in_facility CONSTANT INTEGER :- 120; pet_is_sick EXCEPTION; CURSOR pet_cur (petjdjn IN pet.idUYPE) RETURN petXROtfTYPE: FUNCTION next_pet_shots (pet_id_in IN pet.idXTYPE) RETURN DATE; PROCEDURE set_schedule (petjdjn IN pet.idJSTYPE): END petsjnc;
630 Глава 17 • Пакеты Для ссылки на любые из этих элементов имя элемента предваряется именем пакета: DECLARE -- Константа объявлена на основе типа столбца Id таблицы pet c_pet CONSTANT pet.idXTYPE:- 1099: v next_apppo1ntment DATE; BEGIN IF pets 1nc.max_petsjn_fac1lity > 100 THEN OPEN pets 1nc.pet_cur (c_pet); ELSE v_next_apppo1ntment:- pets_1nc.next_pet_shots (c_pet); END Tf; EXCEPTION WHEN pets_1nc.pet_1s_s1ck THEN petsjnc.5et_scheclule tc_pet); ENO; При обращении к элементам пакета необходимо соблюдать два правила. О Если элемент определяется в спецификации пакета, то для ссылки на него из внешней программы следует использовать точечный синтаксис: имя_паке- та.имя_эпенента. О Когда ссылка на элемент осуществляется в самом пакете (спецификации или теле), имя пакета задавать не нужно, поскольку PL/SQL автоматически иден- идентифицирует ее как обращение к элементу, объявленному в пределах пакета. 'абота с данными пакета Данные пакета — это переменные и константы, определенные на уровне пакета, а не в конкретной его функции или процедуре. Их областью видимости являет- является не отдельная программа, а весь пакет. Структуры данных пякета существуют (и сохраняют свои значения) на протяжении всего сеанса (а не только во время выполнения программы). Если данные пакета объявлены в его теле, они сохраняются в течение сеанса, но доступны только элементам пакета (то есть являются личными). В случае, когда данные пакета объявлены в его спецификации (то есть общие), они также сохраняются в течение всего сеанса, но доступны (для чтения и изме- изменения) любой пользовательской программе, имеющей на пакет привилегию EXE- EXECUTE. Общие данные пакета похожи на GLOBAL-переменные Oracle Forms. Если пакетная процедура открывает курсор, он остается открытым и доступ- доступным в продолжение всего сеанса. Нет необходимости объявлять курсор в каждой программе. Один модуль может его открыть, а другой - выполнить выборку дан- данных. Переменные пакета могут применяться для передачи данных между тран- транзакциями, поскольку они привязаны не к транзакции, а к сеансу.
Работа с данными пакета 631 Глобальные данные в одном сеансе Oracle Благодаря архитектуре SGA структуры данных пакета функционируют в PL/SQL как глобальные. Однако они доступны только в пределах одного сеанса, или под- подключения к базе данных Oracle, и не могут совместно использоваться нескольки- несколькими сеансами. Если доступ к данным нужно обеспечить для нескольких сеансов Oracle, используйте пакет DBMS_PIPE. (Его описание имеется в документации Orac- Oracle Built-in Packages.) Разные части приложения не всегда работают с Oracle, используя одно под- подключение. В некоторых случаях среда, из которой выполняется компонент при- приложения, устанавливает для него новое подключение. При этом данные пакета, записанные первым подключением, будут недоступны для второго. Предполо- Предположим, что форма генерирует отчет с помощью Oracle Reports. По умолчанию Oracle Reports создает для отчета отдельное подключение к базе данных с тем же име- именем пользователя и паролем. Даже если отчет обратится к тому же пакету и струк- структурам данных, что и форма, значения, которые хранятся в структурах данных, доступных форме и отчету, будут разными, поскольку сеанс отчета имеет свой эк- экземпляр пакета и всех его структур. Как уже было сказано, глобальные данные пакета (то есть данные, объявлен- объявленные вне процедур и функций пакета и поэтому существующие в течение всего се- сеанса) могут быть личными или общими. Давайте подробнее рассмотрим каждую из этих категорий. Глобальные общие данные Любые структуры данных, объявленные в спецификации пакета, являются гло- глобальными и общими, то есть они существуют в течение всего сеанса и доступны программам вне этого пакета. Например, можно определить в спецификации па- пакета таблицу PL/SQL и применять ее для хранения списка сотрудников, которых предстоит повысить в должности. Можно создать пакет констант, используемых во всех программах проекта, и все разработчики смогут применять эти констан- константы, вместо того чтобы жестко кодировать их значения в программах. Если гло- глобальные общие данные не объявлены в пакете с ключевым словом CONSTANT, то их можно изменять. Глобальные данные требуют тщательного планирования. Конечно, разработ- разработчикам удобно, когда информация доступна в любой момент времени и из любого места приложения. Однако в этом и таится главная опасность, поскольку вам не- неизвестно, где и как она изменяется. Спецификация должна предоставлять разработчику необходимую информа- информацию о модуле. Однако на основе спецификации глобальных данных невозможно определить, какие программы и пакеты считывают и изменяют их значения. По- Поэтому вы не знаете, что происходит в приложении, какие данные модифицируют- модифицируются и какие программы при этом используются. Всегда предпочтительнее передавать данные модулям в качестве параметров. В тахом случае структуры данных будут описаны в спецификации и видны разра- разработчиками. Именованные глобальные структуры данных следует создавать для
G32 Глава 17 • Пакеты хранения информации, которая глобальна на уровне приложения, в частности для констант и конфигурационных параметров. Пакетные курсоры Одним из наиболее интересных типов пакетных данных является явный курсор, о котором рассказывалось в главе 14. Его можно объявить в теле либо в специфи- спецификации пакета. Состояние курсора (открыт или закрыт), а также указатель на его набор данных сохраняются в течение всего сеанса. Это означает, что открыть па- пакетный курсор можно в одной программе, выбрать из него данные — во второй, а закрыть — в третьей. Такая гибкость курсоров предоставляет большие возмож- возможности, но в то же время она может стать источником проблем. Объявление пакетных курсоров Явный курсор в спецификации пакета можно объявлять двумя способами. О Полностью (заголовок курсора и запрос). Именно так объявляются локаль- локальные курсоры в блоках PL/SQL. 0 Частично (только заголовок курсора). В этом случае запрос определяется в те- теле пакета, поэтому реализация курсора оказывается скрытой от применяюще- применяющего пакет разработчика. При использовании второго способа в объявление нужно добавить предложе- предложение RETURN, указывающее, какие данные будут возвращены при выборке из курсо- курсора. На самом деле эти данные определяются инструкцией SELECT, но она в специ- спецификации отсутствует. В предложении RETURN можно задать одну из следующих структур данных: О запись, объявленную на основе таблицы базы данных с использованием атри- атрибута «ROWTYPE; О запись, определенную программистом. Объявление курсора в теле пакета осуществляется так же, как в блоке PL/SQL. Ниже приведен пример спецификации пакета, демонстрирующий оба подхода: 1 CREATE OR REPLACE PACKAGE bookjnfo 2 IS 3 CURSOR b^author_cur С ' 4 authorjin IN books.authorrTYPE 5 ) 6 IS 7 SELECT * 8 FROM books 9 WHERE author - authorjn; 10 11 CURSOR bytitle_cur ( 12 title_fliter in IN books.titleSTYPE 13 ) RETURN booksSROWTYPE: H 15 TYPE author_sunrnary_rt IS RECORD ( 16 author books.authorSTYPE, 17 total_page_count PLS INTEGER. 18 total book count PLS~INTEGER):
Работа с данными пакета 633 19 20 CURSOR summaryjrur ( 21 authoMn IN books.authorJTYPE 22 ) RETURN author_summary_rt; 23 PROCEDURE display ( 24 book_rec IN booksKROWTYPE 25 ); 26 END bookjnfo; В строках 3-9 представлено типичное определение явного курсора, полностью заданное в спецификации пакета. Строки 11-13 содержат определение курсора без запроса, эта спецификация указывает, что, открыв курсор и выбрав из него данные, вы получите строки из таблицы books для заданных значений столбца books.title. В строках 15—18 определяется новый тип записи для хранения ин- информации об авторе, а в строках 20-22 объявляется курсор, возвращающий ин- информацию о заданном авторе (три определяемых записью значения). Рассмотрим, какой код должно содержать тело пакета для трех объявленных курсоров: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 CREATE OR REPLACE PACKAGE BODY book info IS CURSOR bytHle cur С title filter in IN books.titleSTYPE ) RETURN booksSRCWTYPE IS SELECT * FROM books WHERE title LIKE UPPER Ctitle_f1lter_in); CURSOR summary_cur С authorjn IN books. authorSTYPE ) RETURN author summary rt IS SELECT author. SUM (page count), COUNT (*} FROM books WHERE author - author 1n: PROCEDURE display ( book rec IN booksJROWTYPE) IS BEGIN NULL: END; END book info; Поскольку у нас имеются два курсора с предложениями RETURN, их определе- определения нужно завершить в теле пакета. Список выбираемых запросом элементов в теле пакета должен соответствовать (по количеству элементов и их типам дан- данных) предложению RETURN в спецификации пакета. Если это условие не соблюда- соблюдается, то при компиляции пакета вы получите одно из следующих сообщений: 20/11 PLS-00323: subprogram or cursor '<cursor>' 1s declared in a package specification and must be defined 1n the package body 5/13 PLS-DD400: different number of columns between cursor SELECT statement and return value
634 Глава 17 ¦ Пакеты Работа с пакетными курсорами Использование пакетных курсоров обеспечивает ряд преимуществ. Кроме того, для их открытия, выборки данных и закрытия не требуется изучать новый син- синтаксис, нужно только задать имя пакета перед именем курсора. Например, чтобы запросить информацию о книгах по PL/SQL, можно выполнить такой блок кода: DECLARE onebook book 1nfo.byt1tle curXROWTYPE: BEGIN OPEN book Info.bytitle cur CIPL/SQL*'); LOOP EXIT WHEN bookJnfo.nyt1tle_cur»N0TF0UND: FETCH book_1nfo.byt1tle_cur INTO onebook; book Info.display (onebook); END LOOP: CLOSE book 1nfo.byt1tle cur: END: Как видите, на основе пакетного курсора, точно так же, как и на основе ло- локального явного курсора, с помощью атрибута XROWTYPE можно объявить перемен- переменную для выборки результирующих данных. Однако в этом простом фрагменте кода есть скрытый нюанс. Поскольку кур- курсор объявлен в спецификации пакета, его область видимости не ограничена кон- конкретным блоком PL/SQL. Предположим, что мы выполнили следующий код: BEGIN -- Мы только открываем курсор OPEN book_info.byfitle_cur ('«PEACES'): END; Если затем в этом же сеансе запустить приведенный выше анонимный блок с циклом LOOP, то Oracle выдаст ошибку: ORA-06511: PL/SQL-, cursor already open В данном сообщении сказано, что курсор уже открыт. Так произошло потому, что блок, выполненный первым, не закрыл курсор и по завершении его работы курсор остался открытым. Используя пакетные курсоры, никогда не рассчитывайте на то, что курсор за- закрыт (и его можно открыть) либо открыт (и его можно закрыть). Закончив рабо- работу с пакетным курсором, всегда явно его закрывайте. ПРИМЕЧАНИЕ Эти правила применимы также к курсорам других типов, например к локально определяемым явным курсорам и курсорам, создаваемым с помощью пакета DBMS_SQL Однако для пакетных курсоров они особенно важны. Если данными правилами пренебрегать, то работа созданного вами приложе- приложения будет нестабильной, в процессе его функционирования возможно появление неожиданных и необрабатываемых исключений. Поэтому лучше написать проце- процедуры, которые выполняют открытие и закрытие курсоров и учитывают все воз- возможные их состояния. Этот подход реализован в следующем пакете: CREATE OR REPLACE PACKAGE personnel IS
Работа с данными пакета . 635 CURSOR emps_for_dept ( deptnojn IN employee.deptno*TYPE) IS SELECT * FROM employee WHERE deptno - deptnojn; PROCEDURE open_empsj1or_dept( deptnojn IN employee.deptnoXTYPE, closejf_open IN BOOLEAN :- TRUE PROCEDURE close_empsj'or_depti END personnel; Как видите, вместе с курсором объявлены две сопутствующие процедуры, пред- предназначенные для его открытия и закрытия. Если нам понадобится перебрать в цик- цикле строки курсора, это можно сделать так: DECLARE one emp personnel. emps for deptJiROWTYPE; BEGIN ~ personnel.open_emps_for_dept A055): LOOP EXIT WHEN personnel, emps JbrjiepUNOTFOUND: FETCH personnel.empsjfor_dept INTO one_emp; END LOOP; personnel .с1о5е_етр5^ог_ёер1: END: В данном фрагменте кода не используются явные операторы OPEN и CLOSE. Вме- Вместо них вызываются соответствующие процедуры, скрывающие особенности ра- работы с пакетными курсорами. Советуем проанализировать реализацию этих про- процедур в файле opendose.sql (имеющемся на узле O'Reilly). В большом проекте, над которым работают несколько программистов, реали- реализация курсоров в пакетах, доступных всем программистам, — не просто удобство, а жизненная необходимость. Как правило, многие программы проекта обращают- обращаются к одним и тем же данным, и проектирование оптимальных структур для эти* данных требует много времени и усилий. Поэтому такие структуры (записи, кур- курсоры и сопутствующие процедуры) должны храниться централизованно и быть доступными из всех программ проекта. Иначе каждый программист будет опре- определять собственные курсоры, причем по нескольку раз. В результате неизбежны расхождения и недоработки, которые отразятся как на производительности, так и на отладке и сопровождении проекта. В проектах небольшого размера, созда- создаваемых одним программистом, наличие пакетных курсоров также обеспечивает согласованность и структурированность кода, ускоряет его разработку и упрощает сопровождение. Использование пакетных курсоров является одним из важней- важнейших примеров того, как с помощью пакетов можно инкапсулировать доступ к струк- структурам данных (подробнее об этом рассказывается далее, в разделе «В каких слу- случаях следует использовать пакеты»).
636 Глава 17 • Пакеты Один из рецензентов этой книги, Дж. Т. Томас, предложил следующую аль- альтернативную точку зрения: «Вместо того чтобы работать с пакетными курсорами, можно достичь такого же эффекта путем инкапсуляции логики и помещения дан- данных в представления базы данных, чтобы обеспечить возможность доступа к ним всем разработчикам. Это позволит разработчикам самим отвечать за собственные курсоры. Идея заключается в том, что все равно невозможно обеспечить правиль- правильное использование разработчиками общедоступных пакетных курсоров. В част- . ности, насколько мне известно, разработчики часто пренебрегают процедурами открытия и закрытия курсоров или забывают их вызвать, поскольку курсор все равно видим, разработчикам ничего не стоит непосредственно открыть или закрыть его. Таким образом, эта архитектура остается уязвимой и ее функционирование зависит исключительно от добросовестности и внимательности разработчиков. Кроме того, наличие пакетных курсоров и процедур их открытия и закрытия мо- может создать у команды ложное ощущение надежности.» Повторно инициализируемые пакеты По умолчанию пакетные данные сохраняются в течение всего сеанса (или до пе- перекомпиляции пакета). Это исключительно удобное свойство пакетов, но и у него имеются определенные отрицательные стороны. О Постоянство глобально доступных (общих и личных) структур данных сопро- сопровождается нежелательными побочными эффектами. В частности, можно слу- случайно оставить пакетный курсор открытым, а в другой программе попытаться открыть его без предварительной проверки, в результате чего будет сгенери- сгенерирована ошибка. О Если данные хранятся в структурах уровня пакетов (данные пакетов распола- располагаются в пользовательской глобальной памяти - UGA), то может оказаться, что программы занимают очень большой объем памяти и не освобождают ее. Для оптимального использования памяти при работе с пакетами можно при- применять директиву SERIALLY_REUSABLE. Она указывает Oracle, что пакет является по- повторно инициализируемым, то есть его состояние (состояние переменных, откры- открытых пакетных курсоров и т. п.) нужно сохранять не на протяжении сеанса, а на время одного вызова пакетной программы. Директива SERIALLYREUSABLE должна располагаться и в теле, и в спецификации пакета. Рассмотрим ее действие на примере пакета book_i nfo. В нем имеются две от- отдельные программы: для заполнения списка книг и для вывода этого списка: /* Файл в web: serialpkg.sql */ CREATE OR REPLACE PACKAGE bookjnfc IS PRAGMA SERIALLY_REUSABLE: PROCEDURE fmjist; PROCEDURE show 11st: ' ¦ END; ¦
Работа с данными пакета 637 Как показано ниже, в теле пакета список объявляется как личный глобальный ассоциативный массив: /* Файл в web: serialpkg.sql */ CREATE OR REPLACE PACKAGE BODY bookjnfo IS PRAGMA SERIALLYJEUSABLE; TYPE bookjistj IS TABLE OF booksSROWTYPE INDEX BY BINARYJNTEGER: my_books bookJ1st_t: PROCEDURE filljist IS BEGIN FOR rec IN (SELECT * FROM books WHERE AUTHOR LIKE '«FEUERSTEINS') LOOP my_books (NVL (my_books.LAST. 0) + 1) :- rec: END LOOP: END filljist: PROCEDURE showjist IS BEGIN IF my_books.COUNT - 0 THEN DBMS_OLTPUT.PUT_LINE С** Книг нет...1): ELSE FOR 1ndx IN rny_books.FIRST .. myjooks.LAST LOOP DBMS_OUTPUT.PUT_LINE (my_books (indx).title); END LOOP; END IF; END showjist: END: Чтобы посмотреть, как это работает, заполним список и выведем его на экран. Сначала выполним данную операцию в одном блоке: SQL> BEGIN 2 DBMS_OUTPUr.PUT_LINE ( 3 'Заполняем и выводим список а одном блоке:' 4 ): 5 bookjnfo.finjist; 6 bookjnfo. showjist; 7 END; 8 / Заполняем и выаодии список а одном блоке: Oracle PL/SQL Programming Oracle PL/SOL Best Practices Oracle PL/SQL Built-in Packages
638 Глава 17 • Пакеты Затем заполним и выведем список в двух разных блоках. В результате коллек- коллекция окажется пустой: SQL> BEGIN 2 OBMS OUTPUT.PUT J.INE ('Заполняем список в первой блоке'): 3 bookjnfo.fi I'M 1st: 4 END; 5 / Заполняем список в первом блоке SQL> BEGIN 2 DBMS_OLJTPUT.PUT_LINE ГВыаодии список во второй блоке:'); 3 book info.show list; 4 END; 5 / Выводим список so втором блоке: ** Нет книг... Работая с повторно инициализируемыми пакетами, необходимо учитывать их следующие особенности. О Глобальная память для такого пакета выделяется в системной глобальной об- области (SGA), а не в пользовательской глобальной области (UGA), что позволя- позволяет повторно применять рабочую область пакета. При каждом новом обращении к пакету его общие переменные инициализируются значениями по умолчанию или значением NULL, а его инициализационный раздел выполняется повторно. О Максимальное количество рабочих областей, необходимых для повторно ини- инициализируемого пакета, равняется количеству одновременно работающих с этим пакетом пользователей. Увеличение объема используемой памяти в SGA ком- компенсируется уменьшением объема памяти, задействованного в UGA. Когда нуж- нужно предоставить память из SGA другим запросам, Oracle освобождает неис- неиспользуемые области памяти. В каких случаях следует использовать пакеты Итак, мы уже изучили правила, синтаксис и особенности построения пакетов PL/SQL и теперь можем перейти к рассмотрению ситуаций, в которых их лучше применять. О Инкапсуляция (сокрытие) операций с данными. Программисты, работая над проектом, не должны сами писать 50,Ь-инструкции, поскольку это усложняет сопровождение проекта, гораздо эффективнее — создать общедоступный ин- интерфейс для работы с данными. О Исключение жесткого кодирования литералов. Для того чтобы избежать же- жесткого кодирования одних и тех же значений в нескольких программах, нужно определить все литеральные значения как константы и объединить их в пакеты. Можно также объявить константы в процедурах и функциях, но преимущест- преимущество пакетных констант заключается в их общедоступности.
В каких случаях следует использовать пакеты 639 О Устранение недостатков встроенных функций. Некоторые встроенные паке- пакеты Oracle, например UTL_FILE и DBMSOUTPUT, далеки от совершенства, однако вы можете надстраивать над ними собственные пакеты, в которых значительная часть недостатков исправлена. О Объединение в группу логически связанных функций. Если у вас имеется де- десяток процедур и функций, относящихся к реализации некоторой части прило- приложения, сгруппируйте их в пакет, чтобы упростить управление и сопровожде- сопровождение кода. О Кэширование статических данных для ускорения работы приложения. Для кэширования данных, не изменяющихся в течение сеанса, пользуйтесь посто- постоянными структурами данных пакетов. В следующих разделах каждая из описанных выше ситуаций рассматривается .более подробно. Инкапсуляция операций с данными Вместо того чтобы разрешить разработчикам писать все SQL-инструкции для дос- доступа к данным, можно выделить набор типичных инструкций, реализовать их в виде пакета и предоставить всем членам группы единый интерфейс доступа к этим инструкциям. Таким образом, разработчики смогут использовать готовый, протестирован- протестированный и оптимизированный код, который будет выполнять все необходимые опера- операции. Например, можно создать процедуру для добавления строк (перегружен- (перегруженную для поддержки записей), которая выполняет инструкцию INSERT и реализует стандартные правила обработки ошибок, функцию, предназначенную для извле- извлечения одной строки на основе первичного ключа, а также набор курсоров для вы- выполнения типичных запросов к отдельной таблице или группе связанных таблиц. При эффективном использовании такого подхода разработчикам, отвечающим за реализацию пользовательского интерфейса приложения, нет необходимости знать, как для получения применяемого ими набора данных объединяются не- нескольких связанных нормализованных таблиц. Они будут использовать курсор, а анализ данных выполнит за них другая группа разработчиков. Им не нужно за- заботиться также о том, что происходит при попытке добавить в базу данных строку, которая уже существует в ней. Соответствующая логика инкапсулирована в про- процедуру добавления строки (и эту процедуру легко модифицировать для примене- применения новой инструкции MERGE, введенной в Oracle9t). Данный подход позволяет также упростить сопровождение кода приложения и его обновление при изменении структуры данных. Программистам, отвечаю- отвечающим за работу с таблицами и объектными типами, достаточно внести в один па- пакет необходимые изменения, и они автоматически распространятся на все про- программы, использующие пакет. Инкапсуляция данных - довольно сложная задача, для ее успешной реализа- реализации требуется тщательный анализ и планирование. В файлах te_employee.pks и te_employee.pkb на узле O'Reilly описан пакет, инкапсулирующий доступ к таблице employee. Рассмотрим процедуру, с помощью которой назначается премия каждому
640 Глава 17 • Пакеты сотруднику заданного отдела, который проработал в компании минимум шесть ме- месяцев. Ниже приведена часть такой процедуры, содержащая код SQL: CREATE OR REPLACE PROCEDURE give_banus С deptjn IN employee, department jdXTYPE. bonusjn IN NUMBER) IS vjiame VARCHAR2E0): CURSOR by_dept_cur ' IS SELECT * FROM employee WHERE departmentjd = deptjn: BEGIN /* Извлекаем инфориацис для заданного отдела */ SELECT name INTO v_name FROM department WHERE departmentjd - deptjn; /* Для каждого сотрудника из заданного отдела... */ FOR rec IN by_dept_cur LOOP IF ADD_MONTHS (SYSDATE. -6) > rec.hire_date THEN UPDATE employee SET salary = rec.salary + bonusjn WHERE employeejd - rec.employeejd; END IF: END LOOP; END: Сравните этот код с альтернативной реализацией, в которой доступ к данным инкапсулирован в пакете te_employee: CREATE OR REPLACE PROCEDURE givejonus ( deptjn IN employee, department jdJTYPE. bonusjn IN NUMBER) IS deptj-ec departmenUROWTYPE: fdbk INTEGER; BEGIN dept_rec :- te_department.onerow (deptjn); /* Обязательно закрываем пакетный курсор */ te_employee.close_emp_deptJ ookupjall_cur: FOR rec IN te_employee.emp dept_lookup_all_cur (deptjn) ¦ LOOP IF ADD.MONTHS (SYSDATE. -6) > rec.hire_date THEN tejsnployee.updSsalary ( rec.employeejd. rec. salary + bonus jn. fdbk): END IF: END LOOP; END:
В каких случаях следует использовать пакеты Как видите, весь SQL-код удален из программы и заменен вызовами общедос- общедоступных процедур и функций. Этот подход позволяет ускорить написание про- программ и сделать их более надежными. Разработка таких пакетов — дело непростое, и большинство программистов едва ли сможет или захочет внедрить практику стопроцентной инкапсуляции. Однако даже без кардинального пересмотра принципов разработки программно- программного кода технология инкапсуляции может принести большую пользу. Как мини- минимум имеет смысл осуществить следующее. О Скрыть все запросы на выборку одной строки за интерфейсом функций. Это обеспечит возможность обработки ошибок и выбора оптимальной реализации (например, явного или неявного курсора). О Идентифицировать таблицы, которые чаще всего используют разработчики, и создать для них программный интерфейсный доступ. О Создать пакетные программы для выполнения сложных транзакций. Если до- добавление нового заказа требует вставки двух строк, обновления шести строк и т. д., то лучше заключить ату логику в пакетную процедуру, которая обеспе- обеспечивает ее постоянную и точную реализацию и избавляет программиста от лиш- лишних деталей. Исключение жесткого кодирования литералов Практически любое приложение содержит множество «магических» значений — литералов, имеющих особое предназначение в системе. Это могут быть коды ти- "пов, границы диапазонов и т. п. Пользователи будут говорить вам, что эти значе- значения никогда не меняются. «Этот документ всегда содержит 25 строк», а «Роди- «Родительская компания называется ATLAS HQ и всегда будет называться именно так» - подобные высказывания слышит от пользователя любой разработчик, но только наиболее наивные и неопытные опираются на них при создании кода. Для приме- примера рассмотрим следующий оператор IF: IF footing_difference BETWEEN 1 and 100 THEN adjust_1ine_item: END IF; IF cust_status - 'C THEN reopen_customer: END IF: Тот, кто пишет подобный код, собственноручно создает себе потенциальные проблемы. Серьезный и опытный разработчик сначала формирует пакет имено- именованных констант. CREATE OR REPLACE PACKAGE configjkg IS closedstatus CONSTANT VARCHAR2A) openstatus CONSTANT VARCHARZ(l) active_status CONSTANT VARCHAR2U) inactive_status CONSTANT VARCHAR2A) 'C; '0'; 'A1: 'Г;
642 Глава 17 • Пакеты irrinjjifference CONSTANT NUMBER :- 1: max_difference CONSTANT NUMBER :- 100: earliest_date CONSTANT DATE := SYSDATE: latest_date CONSTANT DATE :- ADD_MONTHS (SYSDATE. 120); END config_pkg: При использовании данного пакета два приведенных выше оператора IF при- примут такой вид: / IF footing_difference BETWEEN config_pkg.ini n_difference and conf1gjkg.max_difference THEN adjustjlinejtem; END IF; IF cust_status * config_pkg.closed_status THEN reopen_customer; END IF; Если какое-либо из «магических» значений изменится, достаточно будет из- изменить соответствующую константу в пакете. Если в программах есть жестко за- закодированные литеральные значения, то готовьтесь к тому, что вам придется вно- вносить в код повторяющиеся изменения. Это может случиться как во время разра- разработки, так и при сопровождении приложения. Во избежание данной проблемы рекомендуем никогда не включать литеральные значения прямо в программы, а помещать их в пакет. Устранение недостатков встроенных функций Некоторые пакеты Oracle, в частности UTL_FILE и DBMS_OUTPUT, с функциональной точки зрения оставляют желать лучшего. Если вы не можете полностью их заме- заменить, то надстройте над ними собственные пакеты, в которых устранены недос- недостатки и в которые добавлены недостающие функции. Часто вместо того чтобы заполнять книгу примерами, мы приводим имена файлов на узле O'Reilly, в которых содержится демонстрационный код. Так по- поступим и в данном случае и посоветуем вам просмотреть следующие пакеты. О filepath.pkg. Добавляет в пакет UTL_FILE поддержку пути, что позволяет искать нужный файл в нескольких заданных каталогах. О dbparm.pkg. Облегчает использование утилиты DBMS_UTILITY.GET_PARAMETER_VA- LUE, предназначенной для извлечения значений из инициализационного фай- файла (INU.ORA). О do.pkg. Дорабатывает процедуру вывода строки (пакет DBMS_OUTPUT), имеющую некоторые конструктивные недостатки, например, она не может выводить зна- значения типа BOOLEAN или строки длиннее 255 байт. Группировка логически связанных функций Если вы имеете десяток процедур и функций, связанных с реализацией некото- некоторой части приложения, объедините их в пакет, чтобы облегчить сопровождение и поиск кода. Это особенно важно при кодировании бизнес-логики приложения.
В каких случаях следует использовать пакеты 643 Учтите, что не следует жестко кодировать бизнес-логику (тем более, многократ- многократно) в отдельных компонентах приложения или размещать ее в отдельных про- программах, которые чрезвычайно трудно сопровождать. Разработку нового приложения нужно начинать с создания набора пакетов, инкапсулирующего все применяемые в нем бизнес-правила. Причем эти правила могут быть частью большого пакета, предоставляющего интерфейс к определен- определенным структурам данных, или хранится в специально созданном для них пакете. Рассмотрим следующий пример: /* Файл в web: custrules.pkg */ CREATE OR REPLACE PACKAGE customer_rules IS FUNCTION min_balance RETURN PLSJNTEGER: FUNCTION eligible_for_discourit (ojstomerjin IN customerMMTYPE) RETURN BOOLEAN; FUNCTION eligible_for_discount (customerjcHn IN customer.customer_id*TYPE) RETURN BOOLEAN; END custotner_rii1 es; Функция el igiЫ e_for_di scount, определяющая право на скидку, скрыта за ин- интерфейсом пакета. Ее перегрузка обеспечивает два интерфейса к формуле: одна версия функции принимает первичный ключ и ищет в базе данных необходимую информацию о клиенте, а вторая — получает полную информацию о клиенте, уже загруженную в запись, объявленную как SROWTYPE. Если в какой-либо программе данные о клиенте уже прочитаны из базы данных, то вызов второй версии функ- функции позволяет избежать повторного запроса к базе данных. Конечно, в данном контексте под логически связанными понимаются не толь- только функции, реализующие бизнес-логику приложения. Так, вы можете разрабо- разработать и объединить в пакет функции, которые выполняют типичные операции со строками и дополняют встроенные функции Oracle. Кэширование статических данных сеанса Постоянство структур данных пакета позволяет ускорить работу приложения за счет кэширования статических данных. Приведем перечень возможных объектов кэширования и имена файлов с примерами, которые находятся на узле O'Reilly. О Отдельные значения, в частности имя сеанса, возвращаемое функцией USER (например, thisuser.pkg и thisuser.tst). О Строка или набор разнотипных значений, таких как конфигурационная ин- информация для данного пользователя (например, init.pkg и init.tst). О Набор однотипных значений, в частности содержимое статической таблицы с набором кодов и описаний (например, таблица сотрудников emplu.pkg и ет- plu.tst). Файлы с расширением .tst предназначены для сравнения производительности с кэшированием и без него.
644 Глава 17 • Пакеты При использовании данной технологии помните, что для каждого сеанса, ра- работающего с определенным пакетом, данные кэшируются отдельно. Это означает, что если одна квитируемая копия таблицы занимает 20 Кбайт и они создаются для 1000 пользователей, то для хранения этих копий потребуется дополнительно 2 Мбайт памяти. Пакеты и объектные типы Как пакеты, так и объектные типы представляют собой контейнеры, объединяю- объединяющие данные и элементы кода. Однако нужно ли нам иметь два типа контейнеров и не дублируют ли они функции друг друга? Не вытесняются ли пакеты объект- объектными типами, особенно после того как в Oracle была добавлена поддержка насле- наследования? И если нет, то в каких случаях следует использовать объектные типы, а в каких - пакеты? Сейчас мы попытаемся ответить на эти вопросы. Пакеты и объектные типы действительно имеют много общего: и те и другие могут содержать одну или более программ и структур данных, а также специфи- спецификацию и тело. Однако между ними есть и принципиальные отличия. О Объектный тип — это шаблон данных, на основе которого можно создавать лю- любое количество экземпляров типа (объектов). Каждый экземпляр имеет пол- полный набор атрибутов (данных) и связан с методами (процедурами и функция- функциями) шаблона. Экземпляры объектных типов могут храниться в базе данных. Пакет же является одиночной структурой, подобной статическому объектно- объектному типу, и создавать его экземпляры нельзя. О В Огас1е9г и последующих версиях объектные типы поддерживают наследова- наследование. Это означает, что можно объявить объектный подтип, который унаследу- унаследует все атрибуты и методы супертипа. Для пакетов концепция иерархии и на- наследования неприменима. Подробнее об объектной архитектуре рассказыва- рассказывается в главе 21. О Пакеты позволяют создавать личные, скрытые данные и программы. Объект- Объектные типы не поддерживают скрытых методов и атрибутов — все их компонен- компоненты объявляются как общие и являются общедоступными (хотя реализация методов скрыта в теле пакета). В настоящее время лишь немногие используют объектные типы и объектно- реляционную модель Oracle. Для большинства разработчиков пакеты все еще ос- остаются базовыми строительными блоками приложений PL/SQL. Если вы планируете применять объектные типы (а об этом с выходом версии Oracle9i стоит подумать весьма серьезно), рекомендуем поместить значительную часть сложного кода в пакеты и вызывать его из методов объектных типов. Это позволит сохранить гибкость кода, присущую объектному подходу, и обеспечить возможность совместного использования кода разными элементами приложения.
18 Триггеры > Триггеры уровня инструкций DML > Триггеры уровня инструкций DDL > Триггеры событий базы данных > Замещающие триггеры > Триггеры AFTER SUSPEND Триггеры - это особые хранимые процедуры, выполняемые в ответ на происхо- происходящие в базе данных события. Они относятся к числу наиболее важных элемен- элементов промышленных приложений базы данных Oracle и обычно связаны со сле- следующими операциями и событиями. О Проверка внесенных в таблицы изменений. Поскольку логика проверки дан- данных непосредственно связана с конкретным объектом базы данных, использо- использование триггеров гарантирует ее строгое выполнение и соблюдение. О Автоматизация сопровождения базы данных. Уже в Огас1е8г можно было ис- использовать триггеры, запускаемые автоматически при загрузке и выгрузке базы данных для выполнения операций инициализации и очистки. Это значитель- значительно удобнее, чем создавать для этих операций внешние по отношению к базе данных сценарии. О Установка ограничений на выполнение операций над объектами базы дан- данных. Триггеры можно использовать для проверки того, допускается ли выпол- выполнение определенной операции над конкретным объектом базы данных (напри- (например, удаление или модификация таблицы). Когда правила проверки реализо- реализованы в виде триггеров, обойти их очень трудно, если вообще возможно. События и структурные компоненты процесса обработки данных, с которыми можно связывать триггеры, перечислены ниже. О Инструкции языка манипулирования данными (DML). Триггеры DML за- запускаются в ответ на вставку, обновление или удаление строки таблицы базы данных. Их можно использовать с целью проверки значений, устанавливаемых
646 Глава 18 • Триггеры по умолчанию, выполнения аудита изменении и даже запрета определенных DM L-инструкций. О Инструкции языка определения данных (DDL). Триггеры DDL запускаются в ответ на выполнение DDL-инструкций, например, при создании таблицы. С их помощью можно выполнять аудит и запрещать определенные операции. О События базы данных. Триггеры событий базы данных используются при за- запуске и остановке базы данных, при подключении и отключении сервера, а так- также в случае возникновения ошибок Oracle. Начиная с Oracle8i они также по- позволяют контролировать активность базы данных. Q Триггеры INSTEAD OF. Замещающие триггеры (триггеры INSTEAD OF) явля- являются альтернативой триггерам DML. Они запускаются непосредственно пе- перед операциями вставки, обновления, удаления, и их код определяет, какие действия следует выполнить вместо соответствующей операции. Триггеры IN- INSTEAD OF управляют операциями над представлениями, но не над таблицами. С их помощью можно преобразовывать необновляемые представления в об- обновляемые, изменяя при необходимости их поведение. О Приостановленные инструкции. В Oracle9i введена концепция приостанов- приостановленных инструкций. Если в ходе выполнения инструкции возникла проблема доступности пространства (недостаточно табличного пространства или исчер- исчерпана квота), Oracle может перевести ее в режим приостановления до тех пор, пока эта проблема не будет решена. С данным событием можно связать триг- триггер, автоматически уведомляющий пользователя или приложение о проблеме или даже самостоятельно ее устраняющий. Все эти типы триггеров рассматриваются в данной главе. Для каждого из них приводится синтаксис, примеры и рекомендации по применению. Триггеры уровня инструкций DML Триггеры уровня инструкций языка манипулирования данными (триггеры DML) запускаются после вставки, обновления или удаления строки конкретной табли- таблицы (рис. 18.1). Это наиболее распространенный тип триггеров, особенно часто применяемый разработчиками. Остальные триггеры используются преимущест- преимущественно администраторами базы данных. Существует несколько способов запуска триггеров DML. Это можно делать до или после выполнения DML-инструкции, а также до или после обработки каж- каждой строки. Триггер может быть запущен при выполнении одной, двух или всех трех возможных инструкций (INSERT, UPDATE и DELETE). Для того чтобы правильно сконфигурировать триггер, необходимо ответить на ряд вопросов. О Как триггер будет запускаться — по одному разу для каждой инструкции SQL или для каждой модифицируемой ею записи? О Когда именно должен вызываться создаваемый триггер — до или после вы- выполнения операции над записями?
Триггеры уровня инструкций DML 647 О Какие действия должен выполнить триггер — вставку, обновление, удаление или все три операции сразу? Приложение для библиотеки INSERT INTO books... - UPDATE books База данных Таблица books :¦>¦ i ! ! INSERT trigger BEGIN IF... END; UPDATE trigger BEGIN END; DELETE trigger BEGIN END; Операции с каталогом DELETE FROM books..." Рис. 18.1. Триггеры DML запускаются в ответ на изменения, производимые в таблице базы данных Концепции триггеров Прежде чем переходить к синтаксису и примерам использования триггеров DML, вам следует познакомиться с их концепциями и терминологией. О Триггер BEFORE. Вызывается до внесения каких-либо изменений, в том чис- числе до вставки записи (BEFORE INSERT). О Триггер AFTER. Выполняется после того, как производятся все изменения, в частности после операции вставки записи (AFTER INSERT). О Триггер уровня инструкции. Выполняется для отдельной SQL-инструкции, которая может обрабатывать одну или более записей базы данных. О Триггер уровня записи. Вызывается для отдельной записи, обрабатываемой SQL-инструкцией. Если, предположим, таблица books содержит 1000 строк, то следующая инструкция UPDATE модифицирует все эти строки: UPDATE books SET title - UPPER (title); И если для данной таблицы определен триггер уровня записи, он будет запу- запущен 1000 раз. О Псевдозапись NEW. Структура данных с именем NEW, которая так же выгля- выглядит и обладает такими же свойствами, как и запись PL/SQL. Эта псевдозапись доступна только внутри триггеров обновления и вставки; она содержит значе- значения модифицированной записи после внесения изменений. О Псевдозапись OLD. Структура данных с именем OLD, которая так же выгля- выглядит и обладает такими же свойствами, как и запись PL/SQL. Эта псевдозапись доступна только внутри триггеров обновления и удаления; она содержит зна- значения модифицируемой записи до внесения изменений. О Предложение WHEN. Часть триггера DML, определяющая условия выпол- выполнения кода триггера (и позволяющая избежать ненужных операций).
648 Глава 18 • Триггеры Сценарии-примеры для работы с триггерами DML Мы поместили на web-узел издательства O'Reilly несколько сценариев, демонст- демонстрирующих работу перечисленных в предыдущем разделе типов триггеров. Тип триггера Файлы Описание Триггеры уровня инструкции и уровня записи copyjables.sql statement vs row.sql Триггеры BEFORE и AFTER Триггеры для разных DML-операций before_vs_after.sql one_trigger_per_type.sql Создает две идентичные таблицы: одну с данными, а другую пустую Создает два простых триггера: один уровня инструкции, а другой уровня записи. После выполнения этих сценариев выполните следующую инструкцию и просмотрите результаты (с активизированной командой SERVEROUTPUT ON, чтобы они выводились на экран): INSERT INTO tojable SELECT * FROM fromjable; Создает триггеры BEFORE и AFTER. После выполнения сценария выполните следующую инструкцию и просмотрите результаты: INSERT INTO tojable SELECT • FROM fromjable; Создает триггеры AFTER INSERT, UPDATE и DELETE для таблицы tojable. После выполнения сценария выполните следующие инструкции и просмотрите результаты: INSERT INTO tojable VALUES A); UPDATE tojable SET coll = 10; DELETE tojable; Триггеры в транзакциях По умолчанию триггеры DML принимают участие в транзакциях, из которых они запущены. Это означает, что: О если триггер инициирует исключение, будет выполнен откат соответствую- соответствующей части транзакции; О если триггер сам выполнит DML-инструкцию (например, вставит запись в таб- таблицу-журнал), она станет частью главной транзакции; О в триггере DML нельзя выполнять инструкции COMMIT и ROLLBACK. РИМЕЧАНИЕ Если вы определите триггер DML как автономную транзакцию (см. главу 13), то все инструкции DML, выполняемые внутри данного триггера, будут сохраняться или отменятся независимо от основной транзакции, В следующем разделе приводится синтаксис объявления триггера DML и рас- рассматривается пример, в котором использована большая часть компонентов и па- параметров триггеров этого типа.
Триггеры уровня инструкций DML 649 Создание триггера Инструкция создания (или замены) триггера DML имеет следующий синтаксис: 1 CREATE [OR REPLACE] TRIGGER тя_тряггерд 2 [BEFORE | AFTER] 3 (INSERT | DELETE | UPDATE | UPDATE OF column list} ON иня_таблицы 4 [FOR EACH ROW] 5 [WHEN (...)] 6 [DECLARE ... ] 7 BEGIN 8 ... исполняемые_операторы ... 9 [EXCEPTION ... ] 10 END [ш_триггера1; Описание всех перечисленных здесь элементов приведено в таблице. Строки Описание 1 Создание триггера с заданным именем. Предложение OR REPLACE не обязательно. Если триггер существует, а предложение OR REPLACE отсутствует, в ответ на попытку создать триггер будет сгенерирована ошибка ORA-4081 2 Задание условий запуска триггера: до (BEFORE) или после (AFTER) выполнения инструкции либо обработки строки 3 Определение типа DML-инструкции, с которой связывается триггер: INSERT, UPDATE или DELETE. Обратите внимание, что триггер, связанный с инструкцией UPDATE, может быть задан для всей строки или только для списка столбцов, разделенных запятыми. Столбцы можно комбинировать (с помощью оператора OR) и задавать в любом порядке. Кроме того, в строке 3 определяется таблица, с которой связывается данный триггер. Помните, что каждый триггер DML должен быть связан с одной таблицей 4 Если задано предложение FOR EACH ROW, триггер будет запускаться для каждой обрабатываемой инструкцией строки. Но если это предложение отсутствует, по умолчанию триггер будет запускаться только по одному разу для каждой инструкции (то есть будет создан триггер уровня инструкции) 5 Необязательное предложение WHEN, позволяющее задать логику для избежания ненужного выполнения триггера 6 Необязательный раздел объявлений для анонимного блока, составляющего код триггера. Если объявлять локальные переменные не требуется, это ключевое слово может отсутствовать. Никогда не объявляйте псевдозаписи NEW и OLD. Они создаются автоматически 7, 8 Исполняемый раздел триггера. Он является обязательным и должен содержать как минимум одну инструкцию 9 Необязательный раздел исключений. В нем перехватываются и обрабатываются исключения, инициируемые только в исполняемом разделе 10 Обязательный оператор END. Для наглядности в него можно включить имя триггера Приведем два примера использования триггеров DML Сначала мы рассмот- рассмотрим триггер, выполняющий несколько проверок при добавлении или изменении строки в таблице сотрудников. В нем содержимое полей псевдозаписи NEW переда- передается отдельным программам проверки: CREATE OR REPLACE TRIGGER validate_employee_changes AFTER INSERT OR UPDATE
650 Глава 18 • Триггеры ON employee FOR EACH ROW BEGIN check_age С:NEW.date_of_Ы rth); chedc_resume C:NEW.resume); END: Следующий три1тер, запускаемый перед вставкой данных, выполняет провер- проверку изменений, производимых в таблице ceo_compensation. Для сохранения новой строки таблицы аудита вне главной транзакции в нем используется введенная в Oracle8i технология автономных транзакций: CREATE OR REPLACE TRIGGER befjns_ceo_comp AFTER INSERT ON ceo_compensation FOR EACH ROW DECLARE PRAGMA AUTONOMOUSJRANSACTION; BEGIN INSERT INTO ceo_comp_history VALUES (:NEW.name, : OLD.compensation. :NEW.compensation, ¦AFTER INSERT'. SYSDATE): COftUT: END; Предложение WHEN Предложение WHEN предназначено для определения условий, при которых должен выполняться код триггера. В приведенном далее примере с его помощью мы ука- указываем, что основной код триггера должен быть реализован только в том случае, если изменились значения в двух заданным столбцах таблицы: CREATE OR REPLACE TRIGGER check_raise AFTER UPDATE OF salary, commission ON employee FOR EACH ROW WHEN ((OLD.salary !- NEW.salary OR (OLD.salary IS NULL AND NEW.salary IS NULL)) OR (OLD.commission !- NEW.commission OR (OLD.commission IS NULL AND NEW.commission IS NULL))) BEGIN Иными словами, если при обновлении записи пользователь по какой-то при- причине повторно запишет в поле salary то же самое значение, триггер активизиру- активизируется, но его основной код выполняться не будет. Поэтому, проверяя это условие в предложении WHEN, можно избежать расходов, связанных с запуском соответст- соответствующего кода PL/SQL. ПРИМЕЧАНИЕ- В файле genwhen.sp имеется процедура, формирующая предложение WHEN, в котором осуществля- осуществляется проверка равенства нового (записываемого) и старого (первоначального) значений.
Триггеры уровня инструкций DML - 651 ПРИМЕЧАНИЕ Предложение WHEN может использоваться только в триггерах уровня записи. Поместив его в триг- триггер уровня инструкции, вы получите сообщение об ошибке компиляции (ORA-04077). В большинстве случаев предложение WHEN содержит ссылки на поля псевдоза- псевдозаписей NEW и OLD. В это предложение разрешается также помещать вызовы встроен- встроенных функций, что и сделано в следующем примере, где с помощью функции SYS- DATE ограничивается время вставки новых записей: CREATE OR REPLACE TRIGGER valid_when_clause BEFORE INSERT ON frame FOR EACH ROW WHEN С T0_CHAR(SYSDATE.'HH24') BETWEEN 9 AND 17 ) При использовании предложения WHEN необходимо соблюдать ряд правил. О Логические выражения всегда должны заключаться в скобки. Эти скобки не обязательны в операторе IF, но необходимы в предложении WHEN триггера. О Перед идентификаторами NEW и OLD нельзя вводить двоеточие (:). Этот символ, обозначающий хост-переменную, необходим в коде триггера PL/SQL, так как позволяет отличить хост-переменные от локальных переменных триггера, од- однако он не должен применяться в предложении WHEN. О В предложении WHEN следует использовать только встроенные функции. Поль- Пользовательские функции или функции, определенные во встроенных пакетах (та- (таких как DBMS_UTILITY), в нем вызывать нельзя (такой вызов приведет к ошибке ORA-04076: invalid NEW or OLD specification error). Чтобы вызвать встроенную функцию, переместите условия отбора записей в начало исполняемого разде- раздела триггера. ПРЕДЛОЖЕНИЕ FOR EACH ROW ПЕРЕД WHEN В некоторых версиях PL/SQL перед предложением WHEN должно стоять предложение FOR EACH ROW, поскольку предполагается, что по умолчанию создается триггер уровня инструкции. Например: SQL> CREATE OR REPLACE TRIGGER row_must_be_before_when 2 BEFORE INSERT ON frame 3 WHEN ( new.strike = Y' ) 4 FOR EACH ROW 5 BEGIN NULL; END; 8 / ERROR at line 3: ORA-04077: WHEN clause cannot be used with table level triggers SQL> CREATE OR REPLACE TRIGGER row_nust_be_before_when 2 BEFORE INSERT ON frame 3 FOR EACH ROW 4 WHEN ( new.strike - T ) 5 BEGIN NULL: END; 8 / Trigger created.
652 Глава 18 • Триггеры Работа с псевдозаписями NEW и OLD При запуске триггера уровня записи исполняющее ядро PL/SQL создает и запол- заполняет две структуры данных, функционирующие подобно записям. Речь идет о псевдозаписях NEW и OLD (это не настоящие записи, поскольку они не обладают всеми свойствами записей PL/SQL). В псевдозаписи OLD хранятся исходные зна- значения обрабатываемой триггером записи, а в псевдозаписи NEW — новые. Их струк- структура идентична структуре записи, объявленной с атрибутом 2R0WTYPE и создавае- создаваемой на основе таблицы, с которой связан триггер. Вот несколько положений, которые следует принимать во внимание при рабо- работе с псевдозаписями NEW и OLD. О Для триггеров, связанных с инструкцией INSERT, структура OLD не содержит данных, поскольку старого набора значений у операции вставки нет. О Для триггеров, связанных с инструкцией UPDATE, заполняются обе структуры, и OLD и NEW. Структура OLD содержит исходные значения записи до обновления, a NEW — значения, которые будут содержаться в строке после обновления. О Для триггеров, связанных с инструкцией DELETE, заполняется только структу- структура OLD, а структура NEW остается пустой, поскольку запись удаляется. О В поле ROWID обеих псевдозаписей, NEW и OLD, всегда помещается одно и то же значение. О Значения полей записи OLD изменять нельзя. При попытке сделать это Oracle сгенерирует ошибку 0RA-04085. Значения полей структуры NEW модифициро- модифицировать можно. О Структуры NEW и OLD нельзя передавать в качестве параметров процедурам или функциями, вызываемым из триггера. Разрешается передавать лишь их от- отдельные поля. В файле gentrigrec.sp имеется программа для генерирования кода, выполняющего копирование значений полей NEW и OLD в структуру, кото- которую можно передать в качестве параметра. О В ссылках на структуры NEW и OLD в анонимном блоке триггера соответствую- соответствующие ключевые слова нужно предварять двоеточием: IF :NEW.salary > 10000 THEN... О Над структурами NEW и OLD нельзя выполнять операции уровня записи. Напри- Например, следующий оператор вызовет ошибку компиляции триггера: BEGIN :new := NULL: END: С помощью предложения REFERENCING в триггере, можно менять имена псевдо- псевдозаписей базы данных, что позволяет писать более самодокументированный код, ориентированный на конкретное приложение. Приведем пример: CREATE OR REPLACE TRIGGER audit_update AFTER UPDATE ON frame REFERENCING OLD AS prior_to_cheat NEW AS after_cheat FOR EACH ROW ¦ BEGIN INSERT INTO framejudit (bowlerjd. game id.
Триггеры уровня инструкций DML 653 framejiumber, o1d_strike. new_strike, old_spare. newspare, old_score, new_score, change_date, operation) VALUES (:af tercheat.bowlerjd. :after_cheat.game_id, :after_cheat.frame_number, :pnor_to_cheat. strike. :after_cheat.strike. :pnor_to_cheat.spare. :after_cheat.spare, :prior_to_cheat.score. :after_cheat.score. SYSDATE. 'UPDATE'); END: Запустите сценарий файла full_old_and_new.sql, и вы увидите, как ведут себя псевдозаписи NEW и OLD. Определение инструкций DML Oracle предлагает набор функций (они также называются операционными дирек- директивами), позволяющих определить, какая инструкция DML вызвала запуск триг- триггера. О том, какое значение при каком условии возвращает каждая из этих функ- функций, рассказывается ниже. О INSERTING - возвращает TRUE, если триггер запущен в ответ на вставку записи в таблицу, с которой он связан, и FALSE в противном случае. О UPDATING — возвращает TRUE, если триггер запущен в ответ на обновление запи- записи в таблице, с которой он связан, и FALSE в противном случае. О DELETING — возвращает TRUE, если триггер запущен в ответ на удаление записи из таблицы, с которой он связан, и FALSE в противном случае. Пользуясь этими директивами, можно создать один триггер, который выпол- выполнял бы действия, связанные с несколькими операциями. Например: /* Файл в web: one_trigger_does_it_all,sql */ CREATE OR REPLACE TRIGGER three_for_the price_of_one BEFORE DELETE OR INSERT OR UPDATE ON account_transaction FOR EACH ROW BEGIN -- Сохраняем информацию о пользователе, который вставил новую строку IF INSERTING THEN :NEW.created_by :- USER: :NEW.created_date := SYSDATE; -- Протоколируем удаление с помощью специальной программы аулита ELSIF DELETING THEN
654 Глава 18 • Триггеры audit_deletion(USER.SYSDATE): -- Сохраняем информацию о пользователе, который последний обновлял строку ELSIF UPDATING THEN :NEW.LAST_UPDATED_SY :- USER: :NEW.LAST_UPDATEO_DATE :- SYSDATE: END IF: END: Функция UPDATING представляет собой перегруженную версию, принимающую в качестве аргумента имя конкретного столбца. Перегрузка функций представля- представляет собой удобный способ изоляции операций обновления отдельных столбцов. /* Файл в web: overloaded jjpdate.sql */ CREATE OR REPLACE TRIGGER validatejjpdate BEFORE UPDATE ON account_transaction FOR EACH ROW BEGIN IF UPDATING('ACCWNTJO') THEN applicat1on_error_handler('ERROR'. 'Account number cannot be updated'): END IF; END: Спецификация имени столбца не чувствительна к регистру клавиатуры. Имя столбца до запуска триггера не анализируется, и если в таблице, связанной с триг- триггером, заданного столбца не оказывается, функция просто возвращает значение FALSE. ПРИМЕЧАНИЕ- Операционные директивы можно вызывать из любого кода PL/SQL, а не только в триггерах, Однако значение TRUE они возвращают лишь в том случае, если используются в триггерах или программах, вызванных из таковых. Пример аудита, выполняемого с помощью триггера Одной из задач приложения, для реализации которой идеально подходят тригге- триггеры, является аудит изменений, вносимых в базу данных. Аудит - это ряд мани- манипуляций, позволяющих определить действия каждого в системе и их правомер- правомерность. Под обеспечением безопасности подразумевается прежде всего невозмож- невозможность для пользователей выполнять недопустимые операции. С помощью аудита можно выявить и записи, введенные мошенническим путем, и несанкциониро- несанкционированные запросы. Рассмотрим простейшее решение задачи аудита с помощью приложения, напи- написанного администратором кегельбана Паркером, который неожиданно стал полу- получать от посетителей жалобы по поводу мошенничества отдельных игроков. Что- Чтобы проверить справедливость жалоб, Паркер разработал данное приложение. Основу приложения составляет таблица frame, в которую записывается счет (в отдельной рамке для каждого игрока): /* Файл в web: bowlerama_tables.sql */ CREATE TABLE frame
Триггеры уровня инструкций DML 655 С bowlerjd NUMBER, gamejd NUMBER. frame_number NUMBER, strike VARCHARZ(l) DEFAULT 'N1. spare VARCHARZ(l) DEFAULT 'N1. score NUMBER. CONSTRAINT frame_pk PRIMARY KEY (bowlerjd. gamejd. framejiumber)): В данном фрагменте кода нас интересует только процедура регистрации дос- доступа к данным, в частности сведения о том, кто добавлял или изменял строки (если, конечно, такие действия производились). Поэтому создается еще одна таб- таблица, frame_audit, которая будет содержать измененные записи со старыми и но- новыми значениями. Сравнивая эти значения, можно выяснить, кто и когда моди- модифицировал таблицу frame: CREATE TABLE (bowlerjd game id frame number old_strike new_strike old_spare newspare old_score new_score change date operation frame audit NUMBER. NUM8ER. NUMBER. VARCHAR2O. VARCHAR2U). VARCHAR2A). VARCHAR2U), NUMBER. NUMBER. DATE. VARCHAR2C6)); При внесении изменений в таблицу frame Паркер сохраняет в таблице fra- meaudit значения модифицируемых записей до и после операции обновления. Это делается с помощью одного триггера: /* Файл в web: bowlerama_full_audit.sql */ 1 CREATE OR REPLACE TRIGGER audit Jrames 2 AFTER INSERT OR UPDATE OR DELETE ON frame 3 FOR EACH ROW 4 BEGIN 5 IF INSERTING THEN 6 INSERT INTO framejudittbowlerjd.gamejd,frame_nuniber, 7 new_strike.new_spare.newj>core. 8 change_date,operation) 9 VALUES(:new.bowlerJd,:new.gamejd,:new.framejiumber, 10 :new.strike.-.new.spare,:new.score, 11 SYSDATE.1INSERT'): 12 13 ELSIF UPDATING THEN 14 INSERT INTO frame_audit(bowlerjd.gamejd.framejiumber. 15 o1d_strike.new_strike. 16 old_spare.new_spare. 17 . old_score.new_score. 18 change_date.operation) 19 VALUES(:new.bowlerjd. :new.gamejd. .new.frsme_number, 20 :old.strike.:new.strike. 21 ¦ :old.spare,:new.spare,
656 Глава 18 • Триггеры 22 23 24 25 26 27 2В 29 30 31 32 33 :old.score.:new.score. SYSDATE.'UPDATE'): ELSIF DELETING THEN INSERT INTO frame_audit(bowler_id.game_id,framejiumber, old strike.old spare.old score. change_date.operation) VALUESt:old.bowlerjd.:old.gamejd.:old.frame_number. :old.strike.:old.spare.:old.score, SYSDATE,'DELETE'); END IF; END: Обратите внимание, что при добавлении записи в таблицу результатов (стро- (строки 6-11) для заполнения строки таблицы frameaudit применяется псевдозапись NEW. При обновлении записи (строки 14-23) используется информация из псев- псевдозаписей NEW и OLD, а в случае ее удаления (строки 26-31) — из псевдозаписи OLD. Конечно Паркер никому не сообщил о новой системе аудита. Сэм же (очень амбициозный, но не очень умелый игрок) решил любым способом повысить свой рейтинг в клубе. Поскольку владельцем кегельбана был его отец, он имел доступ к SQL*Plus. Этого было достаточно, чтобы однажды подключиться к базе данных прямо из SQL*Plus и попытаться «улучпгить» свои результаты. Сначала Сэм проставил себе «сбито» в первой рамке: SQL> INSERT INTO frane 2 (BOWLER_ID,GAME_ID,FRAME_NUI«ER.STRIKE) 3 VALUESd.l.l.'Y'i: 1 row created. Но затем он решил быть осторожнее и изменил значение в первой рамке на «резерв»: SOL> UPDATE frame 2 SET strike = 'N', 3 spare - 'Y' 4 WHERE bowlerjd - 1 5 AND game_id - 1 6 AND framejuimber - 1; 1 row updated. Вдруг Сэм услышал в коридоре шум и поэтому сразу же восстановил записи: SQL> DELETE frame 2 WHERE bowlerid = 1 3 AND gamejid - 1 4 AND framenumber = 1; 1 row deleted. SQL> C0M1IT; Commit complete. Он даже проверил, действительно ли удалена введенная им запись: SQL> SELECT * FROM frame: no rows selected
Триггеры уровня инструкций DML 657 Все обошлось, и Сэм вздохнул с облегчением. Но он решил позже вернуться и все-таки воплотить задуманный план в жизнь. Просматривая вечером таблицу frameaudit, Паркер сразу обнаружил продел- проделки Сэма (он мог бы даже автоматизировать просмотр с помощью пакета OBMSJOB): SELECT FROM bowlerjd. game id. f ramejiumber. old_strike. new strike. old spare, new spare. change_date. operation frame_audit; Вот результаты выполнения такого запроса: BOWLER, 1 1 1 .ID GAMEJD FRAME 1 1 1 NUMBER 0 1 1 Y 1 N N 0 N CHANGE_DA Y N 12-SEP-OO N N Y 12-SEP-OO N 12-SEP-OO OPERAT INSERT UPDATE DELETE Ни одно из внесенных Сэмом изменений не укрылось от взгляда Паркера. Факт выполнения всех трех инструкций был зафиксирован триггером. Предложение WHEN После нескольких месяцев успешной эксплуатации приложения Паркер посчи- посчитал необходимым его усовершенствовать. Проанализировав логику приложения, он понял, что данные в таблице frame изменяются только в полях strike, spare и score. Поэтому и число операций, при выполнении которых запускается триггер, можно ограничить еще больше: CREATE OR REPLACE TRIGGER audit_update AFTER UPDATE OF strike, spare, score ON frame REFERENCING OLD AS pr1or_to_cheat NEW AS after_cheat FOR EACH ROW BEGIN INSERT INTO frame_audit (...) VALUES (...): END; Прошло несколько недель, и Паркер решил еще раз модифицировать код при- приложения. Оказалось, что новые записи помещаются в таблицу frame_audit и п тех случаях, когда при обновлении содержимого строки значения в их полях на са- самом деле не менялись (то есть новое и старое значения одинаковы). В результате таблица контроля заполнялась ненужными записями, свидетельствующими лишь о том, что никаких изменений пока не произошло: SQL> UPDATE FRAME 2 SET strike - strike; 1 row updated.
658 Глава 18 • Триггеры SQL> SELECT old_strike, 2 newstrike. 3 old_spare, 4 newjpare, 5 old_score, 6 new_score 7 FROM frane_audit; 0 N 0 N OLD SCORE NEWJCORE Y Y N N Паркер еще больше ограничил функции триггера, поместив в него предложе- предложение WHEN. В результате триггер стал запускаться только в том случае, если значе- значения в полях strike, spare, score действительно изменялись: /* Оайл в web: final_audit.sql */ CREATE OR REPLACE TRIGGER auditjjpdate AFTER UPDATE OF STRIKE. SPARE. SCORE ON FRAME REFERENCING OLD AS prior_to_cheat NEW AS afterjrheat FOR EACH ROW WHEN ( prior_to_cheat.strike !- after_cheat.strike OR prior_to_cheat.spare != after_cheat.spare OR prior_to_cheat.score !- after_cheat.score ) BEGIN INSERT INTO FRAME_AUDIT(bowler_id,game_id.framejmmber. old_strike.new_strike. . old_spare,new_spare, old_score,new_score, change_date. operati on) VALUESC: after_cheat. bowl er_i d,: aftercheat. game_id.; af ter_cheat. framejiumber. :prior_to_cheat.strike.:after_cheat.strike. iprior_to_cheat.spare,:after_cheat.spare, :prior_to_cheat.score.:afterjrheat.score. SYSDATE.'UPDATE'); END; Теперь количество записей в таблице frame_audit будет минимальным, что по- позволит быстро идентифицировать вероятных нарушителей. Паркер провел завер- завершающее тестирование триггера: SQL> 2 UPDATE frame ' SET strike = strike; 1 row updated. SQL> 2 3 4 5 Б 7 SELECT old_strike. new strike. o1d_spare. new_spare, old score. new score FROM frame audit; no rows selected
Триггеры уровня инструкций DML 659 Использование псевдозаписеи для оптимизации процесса выполнения триггеров Обеспечив наиболее приемлемую, с его точки зрения, эффективность контроля, Паркер решил сделать приложение более дружественным. Главная задача теперь состояла в том, чтобы при записи в базу данных информации о бросках с резуль- результатами «сбито» и «резерв» система увеличивала счет в рамке на 10. Это позволи- позволило бы вводить только информацию о бросках, так как подсчет система должна производить автоматически. CREATE OR REPLACE TRIGGER set_score BEFORE INSERT ON frame FOR EACH ROW WHEN С NEW.score IS NULL ) BEGIN IF :NEW.strike = 'Y' OR :NEW.spare - 'V THEN :NEW.score :- :NEW.score + 10; END IF; END; РИМЕЧАНИЕ Следует помнить, что значения палей в записи NEW могут изменять только триггеры BEFORE уровня строки. А в завершение Паркер добавил в триггер логику проверки счета, с тем чтобы таким образом можно было отслеживать и отменять определенные изменения, которые запрещено производить в таблице базы данных: /* Файл в web: validate_score.sq: */ CREATE OR REPLACE TRIGGER validate_score AFTER INSERT OR UPDATE ON frame FOR EACH ROW BEGIN IF :NEW.strike - 'Y1 AND :NEW.score < 10 THEN RAISE APPLICATI0N_ERROR(-20001.'ERROR: Score For Strike Must Be >= 10'): ELSIF :NEwTspare - T AND .-NEW.score < 10 THEN RAISEAPPLICATIONERROR t - 20001. 'ERROR: Score For Spare Must Be >= 10'); ELSIF :NEW.strike = 'Y1 AND :NEW.spare - 'Y' THEN RAISE_APPLICATI0N_ERROR(-20001,'ERROR: Cannot Enter Spare And Strike For Same Frame'): END IF; END; Теперь попытка вставить строку, не соответствующую правилам, инициирует сообщение об ошибке: SQL> INSERT INTO frame VALUES A,1,1,NULL.NULL,5); INSERT INTO frame * ERROR at line 1: ORA-20000: ERROR: Score For Strike Must Be Less >= 10
660 Глава 18 • Триггеры SQL> INSERT INTO frame VALUESd.1,1. T .NULL.5): INSERT INTO frame * ERROR at line 1: ORA-20001: ERROR: Score For Strike Must >- 10 Триггеры одного типа Oracle позволяет связать с таблицей базы данных несколько триггеров одного типа. Рассмотрим такую возможность еще на одном примере, на этот раз связан- связанном с игрой в гольф. Следующий триггер уровня строки вызывается при вставке в таблицу новой записи и добавляет в нес комментарий, текст которого определя- определяется соотношением текущего счета и номинального значения 72: /* Файл в web: golf_commentary.sql */ CREATE OR REPLACE TRIGGER golf_coimentary BEFORE INSERT ON golf_scores FOR EACH ROW BEGIN IF :NEW.score < 72 THEN :NEW.commentary :- 'Under Par'; ELSIF :NEW.score = 72 THEN :NEW.commentary :- 'Par'; ELSE :NEW.commentary :¦= 'Over Par'; END IF; END; Эти же действия можно выполнить и с помощью трех отдельных триггеров уровня строки типа BEFORE INSERT с взаимоисключающими условиями, задавае- задаваемыми в предложениях WHEN: CREATE OR REPLACE TRIGGER golf_commentary_under_par BEFORE INSERT ON golf_scores FOR EACH ROW WHEN (NEW.score < 721 BEGIN :NEW.commentary :- 'Under Par'; END: CREATE OR REPLACE TRIGGER golf_commentary_par BEFORE INSERT ON golf_scores FOR EACH ROW WHEN (NEW.score = 72) BEGIN :NEW.commentary := 'Par'; END; CREATE OR REPLACE TRIGGER golf_comnentary_over_par BEFORE INSERT ON golf_scores FOR EACH ROW WHEN (NEW.score > 72) BEGIN :NEW.commentary :- 'Over Par': END;
Триггеры уровня инструкций DML 661 Каждая реализация триггера имеет свои достоинства и недостатки. Один триг- триггер проще сопровождать, поскольку все правила сосредоточены в одном месте, однако в случае применения достаточно сложной программной логики предпоч- предпочтительнее использовать отдельные триггеры для каждой операции проверки. Главным недостатком использования нескольких триггеров для отслежива- отслеживания одной операции является невозможность определить порядок их запуска. В приведенном выше примере это обстоятельство не имеет значения, но в других случаях оно может привести к серьезным проблемам. Скажем, вы можете опреде- определить, какое значение получено в результате выполнения запроса SELECT в данном фрагменте кода: /* Файл в web: multiple_trigger_seq.sql */ DROP TABLE incremented_values; CREATE TABLE incremented values Cvaluejnserted NUMBER. valuejncremented NUMBER); CREATE OR REPLACE TRIGGER iincrementJ>y_one BEFORE INSERT ON incremented_values FOR EACH ROW BEGIN :new.valuejncremented := :new.value_incremented + 1; END: CREATE OR REPLACE TRIGGER increment_by_two BEFORE INSERT ON incrementedj/alues FOR EACH ROW BEGIN IF :new.valuejncremented > 1 THEN :new.valuejncremented := :new.valuejncremented + 2; END IF: END: INSERT INTO 1ncremented_values VALUESC1.1): SELECT * FROM incrementedj/alues: Есть какие-нибудь предположения? Для моей базы данных результаты полу- получились такими: SQL> SELECT * 2 FROM 1ncrementedj/alues; VALUEJNSERTED VALUEJNCREMENTED 1 2 Это означает, что первым сработал триггер increment_by_two, который не вы- выполнил никаких действий, поскольку значение в столбце valuejncremented не пре- превышало 1; затем сработал триггер incrementbyone, увеличивший значение столб- столбца val ueji ncremented на 1. А вы тоже должны получить такой результат? Вовсе не обязательно. Будет ли этот результат всегда одним и тем же? Опять-таки, ничего
662 Глава 18 • Триггеры нельзя гарантировать. Oracle утверждает, что порядок запуска однотипных триг- триггеров, связанных с одной таблицей, не определен и произволен, поэтому задать его явно невозможно. На этот счет имеется несколько мнений, среди которых наиболее популярны два следующих: триггеры запускаются в порядке, обратном порядку их создания, или же в соответствии с идентификаторами их объектов. Однако полагаться на подобные предположения не стоит. Ошибки, возникающие при изменении таблицы: проблема и решение Известно, что изменяющиеся объекты всегда трудно анализировать и оценивать. Поэтому когда триггер уровня строки пытается прочитать или изменить данные в таблице, находящейся в состоянии изменения (с помощью инструкции UPDATE, INSERT или DELETE), генерируется ошибка с кодом ORA-4091. Предположим, для таблицы сотрудников требуется задать некоторое ограни- ограничение на значения в столбцах, заключающееся в том, что при повышении оклада сотрудника повый его оклад не должен превышать максимальный по подразделе- подразделению более чем на 20 %. Проверку этого условия можно было бы реализовать с помощью следующего триггера: CREATE OR REPLACE TRIGGER brake_on_raises BEFORE UPDATE OF salary ON employee FOR EACH ROW DECLARE l_curr_max NUMBER: BEGIN SELECT MAX (salary) INTO l_curr_nax FROM employee: IF l_curr_max * 1.20 < :NEW.salary THEN errpkg.RAISE ( empl oyeerul es. en_sal aryi ncrease_too_l arge, :NEW.employee_id, :NEW.salary ): END IF: END: Однако при попытке удвоить, скажем, оклад программиста PL/SQL Oracle выдаст такое сообщение об ошибке: ORA-04091: table SCOTT.EMPLOYEE is mutating, trigger/function may not see it Тем не менее существует несколько приемов, которые помогут предотвратить появление сообщения о данной ошибке. О В общем случае триггер уровня строки не может считывать или записывать данные таблицы, с которой он связан. Но подобное ограничение относится только к триггерам уровня строки. Триггеры уровня инструкции могут и счи- считывать и записывать данные своей таблицы, что дает возможность произвести необходимые действия. О Если триггер выполняется как автономная транзакция (для этого в него нуж- нужно добавить директиву PRAGMA AUTONOMOUS TRANSACTION и выполнить в его теле
Триггеры уровня инструкций DML 663 инструкцию COMMIT), тогда в нем можно запрашивать содержимое его табли- таблицы. Однако модификация такой таблицы все равно будет запрещена. Изменяемые таблицы и внешние ключи Ошибка, возникающая при модификации таблицы, которая находится в состоя- состоянии изменения, часто связана с внешними ключами. Рассмотрим еще один при- пример. Предположим, система управления транзакциями банка реализована на ос- основе Огас1е7.3.4. Владельцы банка оказались столь щедры, что запланировали выплачивать по 100 долларов каждому клиенту, открывающему новый счет. Как обычно, разработчикам сообщили об этом в пятницу вечером, поэтому бедняжке Кэй пришлось поработать и в выходные. Кэй подумала, что для решения этой задачи больше всего подходит триггер INSERT, который должен вносить соответствующие изменения в таблицу базы дан- данных. То есть, как только в таблице account (содержащей список счетов) будет соз- создана очередная новая запись, триггер тут же добавит в таблицу accounttran- saction (с результатами транзакций счетов) запись об операции депозита. Такое решение показалось Кэй просто замечательным, поскольку на его реализацию вместе с отладкой и тестированием, как она предполагала, уйдет всего несколько часов и остаток выходных можно будет провести за игрой в кегли. Зная, что таблицы account и account_transaction связаны между собой внеш- внешним ключом, образованным на основе столбца accounted, Кэй создала следую- следующий триггер: CREATE TRIGGER give_away_freejioney AFTER INSERT ON account FDR EACH ROW BEGIN INSERT INTO account_transacti'on Ctransactionjd, accounted, transaction_type, transacti on_amount, comments) VALUES(account_transaction_seq.nextval. : NEW. accounted. 'DEP', 100. 'Free Money!'): END: Кэй была полностью уверена в своем триггере (что могло быть не так со столь простым фрагментом кода?), но все-таки решила проверить его. К своему огром- огромному удивлению она тут же получила такой ответ: SQL* INSERT INTO account 2 (accounted, accountjwner) 3 VALUESA.Test1): INSERT INTO account # ERROR at line 1: ORA-04091: table SCOTT.ACCOUNT is mutating, trigger/function may not see it ORA-06512: at "SCOTT.CIVE_AWAY_FREE_MONEY". line 2 0RA-Q4088: error during execution of trigger 'SCOTT.GIVE AWAY FREE MONEY1
664 Глава 18 • Триггеры Таблица account в этой транзакции действительно изменяется из-за вставки строки с новым счетом. Когда триггер пытается добавить новую запись в таблицу account transaction, Oracle должна проверить значение в столбце account! d таб- таблицы account. Вот тут-то и происходит ошибка: читать данные из таблицы account пока нельзя! Конечно, можно сказать, что эта ошибка не имеет смысла, поскольку вся необ- необходимая информация легко доступна; первичный ключ задан в исходной инст- инструкции INSERT. Как бы там ни было, факт остается фактом: созданный триггер на- нарушает ограничение целостности, налагаемое на триггеры уровня строки. ПРИМЕЧАНИЕ Начиная с Oracle 8.1.5 указанная ошибка больше не генерируется. Описанное ограничение относится только к триггерам уровня строки; на триг- триггеры уровня инструкции оно не распространяется. Как обойти ошибки при изменении таблицы Какие же шаги следует предпринять, встретившись с такой ошибкой? Некоторые разработчики отказываются от использования триггера и ищут другой способ реа- реализации этой же логики. Однако отказываться от триггеров вовсе не обязатель- обязательно — можно избежать ошибки, воспользовавшись тем обстоятельством, что для триггеров уровня инструкции не существует ограничений на доступ к таблице, с которой они связаны, а также тем, что структуры данных уровня пакета сохра- сохраняются в течение всего сеанса. Один из способов избежать ошибки указанного типа состоит в использовании коллекций. Предположим, Oracle не позволяет выполнять определенные дейст- действия в триггере уровня строки, требуя отложить их до момента запуска триггера уровня инструкции. Следовательно, вы должны побеспокоиться, чтобы эти дей- действия были выполнены позже, то есть сохранить информацию о том, что нужно делать. На роль «списка задач» отлично подходят коллекции PL/SQL. Итак, триггер уровня строки, вместо того чтобы выполнить необходимые дей- действия, просто сохраняет в коллекции всю информацию, нужную для выполнения этих же действий на уровне инструкции. В триггере уровня инструкции можно просмотреть всю коллекцию и выполнить тот код, который нужно было выпол- выполнить в триггере уровня строки (эти действия показаны на рис. 18.2). Посмотрим, как Кэй применила описанную технологию. Добавление задачи в список Запускается триггер строки 1 — ¦¦-- ¦¦—- -*¦ Запускается триггер строки N I Список задач "^ (коллекция) ^¦¦¦-¦"""""* Считывание Списка задач и выполнение ....-¦- соответствующих Триггер инструкции :*"" действий Обработка строк закончена; переход к триггеру уровня инструкции Рис 18.2. Использование коллекции для обхода ошибки, связанной с внесением изменений в таблицу
Триггеры уровня инструкций DML 665 Итак, Кэй реализовала следующий алгоритм. 1. Инициализация коллекций в пакете PL/SQL в начале выполнения инструк- инструкции. 2. Сохранение в коллекции идентификатора каждой обрабатываемой строки. 3. Обработка каждого элемента коллекции по завершении инструкции. Вот какой пакет PL/SQ.L она создала (мы приводим лишь его начало): /* Файл в web: mutation_zone.sql */ CREATE OR REPLACE PACKAGE giveawaymoney AS PROCEDURE init_table5: END give_away_money; CREATE OR REPLACE PACKAGE BODY give_away_money AS -- структура для хранения номеров счетоа TYPE v_account_table_type IS TABLE OF NUMBER INDEX BY BINARYJNTEGER; v_account_table v_account_table_type: /* - */ PROCEDURE init_tables IS /* */ BEGIN -- Инициализация списка номеров счетоа путем его очистки v_account_tabl e.DELETE: END init_tables; END g1ve_awayjnoney: Эта простая инициализационная процедура очищает коллекцию, в которую добавляются номера новых счетов. Ее нужно связать с инструкцией INSERT, встав- вставляющей новые записи в таблицу account: CREATE OR REPLACE TRIGGER beforejnsert statement BEFORE INSERT ON account BEGIN /* || Инициализируем таблицы PL/SQL для хранения счетов, на || которые нам предстоит положить по 100 долларов. */ g1ve_away_money.i ni t_tables: END; Теперь при каждом выполнении инструкции INSERT для таблицы account кол- коллекция будет очищаться. Далее нам нужно сохранить номера добавляемых в таблицу account счетов. Для этого поместим в пакет give_away_money следующую процедуру: PROCEDURE add_account_to_list ( p_account NUMBER ) IS BEGIN v_account_table(NVL(v_account_table.LAST,0) + 1) :- p_account; END add account to 11st:
666 Глава 18 • Триггеры Данная процедура вызывается из триггера AFTER INSERT уровня строки: CREATE OR REPLACE TRIGGER after_insert_row AFTER INSERT ON account FOR EACH ROW BEGIN /* || Добавляем в список новый счет, на который нужно || будет положить 100 долларов. */ ¦ gi ve_away_money.add_account_to_li st(:NEW.accountj d): END; Наконец, последняя процедура пакета выполняет то, ради чего, собственно, мы все это затеяли: добавляет в таблицу записи об открытии новых счетов по 100 долларов: PROCEDURE give_it_away_now IS -- Кладем 100 долларов на каждый новый счет v_element PLS INTEGER: BEGIN v_element := v_account_table.FIRST: LOOP EXIT WHEN v_element IS NULL; INSERT INTO~account_transaction (transactionjd. accourvMd. transaction_type. transaction_amOLint, comments) VALUES(account_transaction_seq.nextval. v_account table(v_element). 'DEP'. 100. 'Free Money!'); v_element ;= v account_table.NEXT(v_element): END LOOP: END giveitawayjiow; Эта процедура вызывается из триггера: CREATE OR REPLACE TRIGGER after jnsert_statement AFTER INSERT ON account BEGIN -- Наконец кладем деньги на счет! gi ve_away_money.gi ve_i t_away_now; END; И вот теперь, после удаления исходного триггера Кэй и замены его пакетом с новыми триггерами, тестирование прошло успешно: SQL> INSERT INTO account (accounted. account_owner) 2 VALUESU, 'Test'): SQL> SELECT * FROM accountjransaction; 2 WHERE account id - 1;
Триггеры уровня инструкций DML 667 TRANSACTION ID ACCOUNT ID TRA TRANSACTION AMOUNT COMMENTS SQL> 2 3 4 5 6 7 SOL> 2 3 1 BESIN FOR counter IN i 1 DEP '0..25 LOOP INSERT INTO account (account id.account owner) VALUES[counter.'Test1); END LOOP: END; SELECT * FROM account transaction WHERE accountjd TRANSACTIONJD ACCOUNT. 2 3 4 5 6 7 6 rows selected. BETWEEN 20 AND 25 .ID TRA TRANSACTION. 20 DEP 21 DEP 22 DEP 23 DEP 24 DEP 25 DEP 100 AMOUNT 100 100 100 100 100 100 Free Money! COMMENTS Free Free Free Free Free Free Money! Money! Money! Money! Money! Money! Таким образом, благодаря слаженному взаимодействию триггеров уровня ин- инструкции, триггеров уровня строки и пакетных процедур PL/SQL Кэй, наконец, свободна и может провести остаток уик-энда, не беспокоясь о проблеме, связан- связанной с изменением таблиц: Частичное решение проблемы изменяемых таблиц По мере усовершенствования Oracle проблема изменяемых таблиц постепенно решается. Исходный триггер Кэй будет прекрасно работать уже в Orade8.1.5, по- поскольку проверка ограничения внешнего ключа теперь выполняется после запус- запуска триггера уровня строки. Правда, при наличии внешнего ключа, определенного с предложением ON DELETE CASCADE, ошибка изменяемых таблиц по-прежнему про- проявляется, но это единственный случай. Рассмотрим простой пример, в котором используются две таблицы - главная и подчиненная. Они связаны ограничением внешнего ключа, заданным предло- предложением ON DELETE CASCADE: CREATE TABLE master_table (masterjd NUMBER NOT NULL PRIMARY KEY): CREATE TABLE deta11_table (detailjd NUMBER NOT NULL, masterjd NUMBER NOT NULL. CONSTRAINT detailjoemp FOREIGN KEY (master id)
668 Глава 18 • Триггеры REFERENCES master_table (masterjd) ON DELETE CASCADE); CREATE OR REPLACE TRIGGER after_delete_master AFTER DELETE ON master_table FOR EACH ROW DECLARE CURSOR curs_count detail IS SELECT COUNTC*) FROM detail_table: v_detail_count NUMBER; BEGIN OPEN curs_count_detail, FETCH curs_count_detai1 INTO v__detail_count: CLOSE curs_couirt_detai 1; END; Удаление строки из таблицы mastertabl e приведет к исчезновению связанных с ней строк в таблице detai l_table. Oracle не может гарантировать достоверность результата, возвращаемого функцией COUNT, поскольку неизвестно, сколько строк после этого останется. Нам же не остается ничего другого, как объявить о воз- возможном изменении таблицы: SQL> DELETE master_table; * ERROR at line 1: ORA-04091; table SCOTT.DETAILJABLE is mutating, trigger/function may not see it ORA-06512: at "SCOTT.AFTER_DELETE_MASTER". line 3 . ORA-06512: at "SCOTT.AFTER_DELETE_MASTER". line 7 ORA-04088: error during execution of trigger 'SCOTT.AFTER_DELETE_MASTER' Но уже в Oracle8.1 подобной ситуации можно избежать, если триггер выпол- выполняется в режиме автономной транзакции: CREATE OR REPLACE TRIGGER after_delete_master AFTER DELETE ON masterjtable FOR EACH ROW DECLARE -- автономная транзакция PRAGMA AUTONOMOUSJRANSACTION: CURSOR curs_count_detail IS SELECT COUNTC*) FROM detai ljable: v_detail_count NUMBER; BEGIN OPEN curs_count_detail: FETCH curs_count_detail INTO v_detail_count: DBMS_OUTPUT.PUT_LINE('detail count- ' || v_detail_count): CLOSE curs_count_detai1: END;
Триггеры уровня DDL bbS Теперь при запуске триггера ошибок не будет, поскольку он больше не являет- является частью транзакции, при выполнении которой удаляются строки главной таб- таблицы: SQL> DELETE master_table: detail count - 1 1 row deleted. Как видите, проблем с изменяемыми таблицами становится все меньше, а еле-, довательно, становятся все менее заметными связанные с ними неудобства. Триггеры уровня DDL В OracleB была введена долгожданная функция запуска триггеров при выполне- выполнении инструкций языка определения данных (Data Definition Lanquage, DDL). Ес- Если говорить проще, инструкцией DDL является любая SQL-инструкция, создаю- создающая или модифицирующая объект базы данных, такой как таблица или индекс. Примерами DDL-инструкций могут служить инструкции CREATE TABLE/ALTER TABLE, CREATE INDEX, CREATE TRIGGER/DROP TRIGGER, каждая из которых создает, изменяет или удаляет объект базы данных. Синтаксис триггеров DDL-инструкций тот же, что и у триггеров DML. Они различаются лишь перечнем событий и тем, что триггеры DDL не связываются с конкретными таблицами. Создание триггера Инструкция создания (или замены) триггера DDL имеет следующий синтаксис: 1 CREATE [OR REPLACE] TRIGGER иня_триггера 2 {BEFORE | AFTER) {co6biTne_DDL} ON {DATABASE | SCHEMA} 3 DECLARE 4 Объявлении переменных 5 BEGIN 6 ... ma триггера... 7 END: Элементы триггера описаны в приведенной ниже таблице. Строки Описание 1 Создание триггера с указанным именем. Если триггер существует, но строка OR REPLACE не задана, об этом вас уведомит уже знакомое сообщение об ошибке Oracle 4081 2 Данная строка очень информативна. Она определяет, должен триггер запускаться до или после наступления заданного события DDL, а также должен он запускаться для всех операций в базе данных или только в заданной схеме 3-7 Образец тела триггера
670 Глава 18 • Триггеры Приведем пример простого триггера с функциями информатора, объявляю- объявляющего о создании всех объектов: /* Файл в web: uninformed_town_crier.sql */ SQL> CREATE OR REPLACE TRIGGER townjxier 2 AFTER CREATE ON SCHEMA 3 BEGIN 4 08MS_0UTPUT.PUT_LINE{'I believe you have created something!'): 5 END: 6 / Trigger created. SQL> SET SERVEROUTPUT ON SQL> CREATE TABLE ajable 2 (coll NUMBER); Table created. SQL> CREATE INDEX anjndex ON a_table(co11): Index created. SOL> CREATE FUNCTION a_funct1on RETURN BOOLEAN AS 2 BEGIN 3 RETURN(TRUE): 4 END; 5 / Function created. SQL> /*-- очистка буфера DBMS_OUTPUT */ SQL* BEGIN NULL: END; 2 / I believe you have created something! I believe you have created something! I believe you have created something! PL/SQL procedure successfully completed. ВНИМАНИЕ Текст, возвращаемый встроенным пакетом DBMS_OUTPUT, не будет выводиться триггером DDL до тех пор, пока вы не выполните блок PL/SQL, даже если этот блок не реализует никаких действий. Со временем пользователи начнут просто игнорировать информатор, посколь- поскольку, сообщая о создании чего-то, он не уточняет, что же именно создается. Но на самом деле триггеры DDL способны предоставлять более полную информацию, ведь триггер можно переписать и таким образом: /* Файл з web: informed_town_crier.sql */ SQL> CREATE OR REPLACE TRIGGER townjxier 2 AFTER CREATE ON SCHEMA 3 BEGIN 4 -- используем атрибуты события для получения более полной информации 5 DBMS_OUTPLT.PUT_LINEt'I believe you have created a ' |l 6 ORA_DICT_OBJ_TYPE || ' called ' || 7 ORA_DICT_OBJ_NAME): 8 END;
Триггеры уровня DDL 9 / Trigger created. SQL> SET SERVEROUTPUT ON SQL> CREATE TABLE a_table 2 (coll NUMBER); Table created. SQL> CREATE INDEX arMndex ON a_table(coll); Index created. SQL> CREATE FUNCTION a_function RETURN BOOLEAN AS 2 BEGIN 3 RETURN(TRUE); ' 4 END; 5 / Function created. SQL> /*-- очистка буфера DBMSOUTPUT */ SQL> BEGIN NULL; END; 2 / I believe you have created a TABLE called A_TABLE I believe you have created a INDEX called ANJNDEX I believe you have created a FUNCTION called A_FUNCTION PL/SQL procedure successfully completed. Теперь к информатору станут, наконец, прислушиваться. В приведенных выше примерах задействованы два важнейших элемента, необходимых для создания триггеров DDL: события, с которыми они связываются, и атрибуты событий, ко- которые в них доступны. События триггеров Список событий, которые можно связать с триггерами DDL (одним из таких со- событий является выдача определенной S QL-инструкции, например инструкции CREATE), приведен в табл. 18.1. Любой триггер DDL может вызываться до или по- после наступления описанных здесь событий. Таблица 18.1. События, связываемые с триггерами DDL Событие Описание CREATE Создание объекта базы данных с применением SQL-инструкции CREATE ALTER Изменение объекта базы данных с помощью SQL-инструкции ALTER DROP Удаление объекта базы данных с использованием SQL-инструкции DROP ANALYZE Анализ состояния объекта базы данных с помощью SQL-инструкции ANALYZE ASSOCIATE STATISTICS Связывание статистики с объектом базы данных продолжение^
672 Глава 18 • Триггеры Таблица 18.1 (продолжение) Событие Описание _^^ AUDIT Включение средства аудита базы данных с помощью SQL-инструкции AUDIT . NOAUDIT Выключение средств аудита базы данных с помощью SQL-инструкции NOAUDIT COMMENT Создание комментария для объекта базы данных DDL Наступление любого из перечисленных выше событий DISASSOCIATE STATISTICS Удаление статистики объекта базы данных GRANT Назначение прав с использованием SQL-инструкции GRANT RENAME Переименование объекта базы данных с помощью SQL-инструкции RENAME REVOKE Отмена прав с применением SQL-инструкции REVOKE TRUNCATE Очистка таблицы посредством SQL-инструкции TRUNCATE Как и триггеры DML, триггеры DDL запускаются, когда в заданной базе дан- данных или схеме происходят события, с которыми они связаны. Никаких ограниче- ограничений на количество типов триггеров, которые могут быть определены в базе дан- данных или схеме, не существует. Параметры триггеров Oracle предоставляет набор функций (определены в пакете DBMS_STANDARD), воз- возвращающих информацию о причине запуска триггера DDL и другие связанные с ним параметры (например, имя удаляемой таблицы). Перечень этих функций приведен в табл. 18.2, а примеры их использования — в следующих разделах. Таблица 18.2. Список функций пакета DBMS_STANDARD Функция Что возвращает ORA_SYSEVENT Тип события, вызвавшего запуск триггера DDL (например, CREATE, DROP ИЛИ ALTER) ORA_LOGIN_USER Имя пользователя Oracle, для которого запущен триггер ORA_INSTANCE_NUM Номер экземпляра базы данных ORA_DATABASE_NAME Имя базы данных ORA_CLIENT_IP_ADDRESS IP-адрес клиента ORA_DICT_OBJ_TYPE Тип объекта базы данных, связанного с DDL-инструкцией, вызвавшей запуск триггера (например, TABLE или INDEX) ORA_DICT_OBJ_NAME Имя объекта базы данных, связанного с DDL-инструкцией, которая вызвала запуск триггера ORA_DIC T_OBJ_OWNER Возвращает имя владельца объекта базы данных, связанного с DDL-инструкцией, которая вызвала запуск триггера ORA_IS_CREATING_NESTED_TABLE Значение TRUE, если создается вложенная таблица, и FALSE в противном случае
i риггеры уровня uljl Функция Что возвращает ORA_DES_ENCRYPTED_PASSWORD ORA_IS_ALTER_COLUMN ORA_IS_DROP_COLUMN ORA_DICT_OBJ_NAME_USr ORAJ)ICT_OBJ_OWNER_LIST ORA_GRANTEE ORAJ/VITH_GRANT_OPTION ORA_PRMLEGEJJST ORA_REVOKEE Зашифрованный пароль текущего пользователя. При шифровании используется алгоритм DES Значение TRUE, если изменяется столбец, заданный параметром COLUMN_NAME, или FALSE в противном случае Значение TRUE, если удаляется столбец, заданный параметром COLUMN_NAME, или FALSE в противном случае Количество обработанных инструкцией объектов. Кроме того, в параметре NAME_LIST возвращается полный список этих объектов, представляющий собой коллекцию типа DBMS_STANDARD.ORA_NAMEJJST_T Количество обработанных инструкцией объектов. Кроме того, в параметре NAME_UST возвращается полный список имен владельцев этих объектов, представляющий собой . коллекцию типа DBMS_STANDARD.ORA_NAME_LIST_T Количество пользователей, получивших права. В аргументе USERJJST содержится полный список этих пользователей, представляющий собой коллекцию типа DBMS_STANDARD.ORA_NAME_LIST_T Значение TRUE, если права предоставлены с правом предоставления привилегий другим пользователям, и FALSE в противном случае Количество предоставленных или отмененных прав. В аргументе PRIVILEGE_LIST содержится полный список этих разрешений, представляющий собой коллекцию типа DBMS_STANDARD.ORA_NAMEJIST_T Возвращает количество пользователей, лишенных привилегий. В аргументе USER_LIST содержится полный список этих пользователей, представляющий собой коллекцию типа DBMS_STANDARD.ORA_NAME_LIST_T Относительно этой таблицы хотелось бы добавить следующее. О Тип данных ORA_NAME_LIST_T определен в пакете DBMSJTANDARD как TYPE ora_name_list_t IS TABLE OF VARCHAR2F4): Иными словами, это вложенная таблица строк, каждая из которых может со- содержать до 64 символов. О События триггеров DDL и функции параметров также определены в пакете DBMS_STANDARD. Для каждой из функций этого пакета Oracle создает независи- независимую функцию, добавляя к ее имени префикс 0RA_, для чего при создании базы данных выполняется сценарий $ORACLE_HOME/rdbms/dbmstrig.sql. В отдельных версиях Oracle в этом сценарии имеются ошибки, из-за которых независимые функции невидимы или не выполняются. Приведем пример из сценария для Огас1е8.1.7, с тем чтобы показать вам ошибку в имени функции pri vilege_l ist: create public synonym ora_privilege_list for pri vilegl 1st В Oracle9i эта ошибка уже исправлена, но могут встретиться другие ошибки. В таком случае можно попросить администратора базы данных проверить сце- сценарий и внести необходимые исправления.
I лава ю • i риггеры О Представление словаря данных USERSOURCE не обновляется до тех пор, пока не выполнятся триггеры BEFORE и AFTER. Иными словами, с помощью описанных функций невозможно создать систему управления версиями, основанную на триггерах базы данных. Применение событий и параметров Возможности триггеров DDL лучше всего продемонстрировать на примерах. Для начала рассмотрим триггер, который блокирует создание любых объектов базы данных: CREATE OR REPLACE TRIGGER no_create AFTER CREATE ON SCHEMA BEGIN RAI5E_APPLICATI0N_ERR0R С -20000. 'ERROR : Objects cannot be created in the production database.' ); END; После его создания в базе данных нельзя больше создать ни одного объекта. SQL> CREATE TABLE demo (coll NUMBER); * ERROR at line 1: ORA-20000: Objects cannot be created in the production database. Данное сообщение об ошибке не назовешь информативным. Из него лишь по- понятно, что в базе данных нельзя создать объект. Можно было бы вывести более информативное сообщение, указав, например, какой именно объект пытался соз- создать пользователь: /* Файл a web: no_create.sql */ CREATE OR REPLACE TRIGGER recreate AFTER DDL ON SCHEMA BEGIN IF ORA_SYSEVENT = 'CREATE' THEN RAISE_APPLICATION_ERROR(-20000. 'Cannot create the ' || ORA_DICT_OBJ_TYPE || ' named ' |j ORA_DICT_OBJ_NAME || 1 as requested by ' || 0RA_DICTJ)BJ_OWNER); END IF; END: При наличии такого триггера попытка создать таблицу в базе данных приве- приведет к появлению более подробного сообщения об ошибке: SQL> CREATE TABLE demo (coll NUMBER); * ERROR at line 1: ORA-20000: Cannot create the TABLE named DEMO as requested by SCOTT Можно было бы даже реализовать эту логику в виде триггера BEFORE и вос- воспользоваться событием ORASYSEVENT: CREATE OR REPLACE TRIGGER no create BEFORE DDL ON SCHEMA BEGIN
|риггеры уровня DDL IF ORAJYSEVENT - 'CREATE1 THEN RAISE_APPLICATION_ERROR(-20000, ¦Cannot create the ' || ORA_DICT_OBJ_TYPE || ¦ named ' II ORA_DICT_OBJ_NAME || 1 as requested bj ' || ORA_D1CT_OBJ_OWNERJ: ELSIF ORAJYSEVENT = 'DROP1 THEN -- Логика операций DROP END IF; END: Как определить, какой столбец изменен С помощью функции ORA_IS_ALTER_COLUMN можно определить, какой столбец был изменен в результате применения инструкции ALTER TABLE: /* Файл в web: preserve_app_cols.sql */ CREATE OR REPLACE TRIGGER preserve_app_cols AFTER ALTER ON SCHEMA DECLARE -- курсор для чтения имен столбцов таблицы CURSOR curs_get_columns (cpjwner VARCHAR2, cpjable VARCHAR2) IS SELECT columnjiame FROM all_tab_coljnins WHERE owner = cp_owner AND table_name = cp_table; BEGIN -- если это та таблица, которая была изменена... IF ora_dict_obj_type - 'TABLE' THEN -- для каждого столбца таблицы... FOR v_column_rec IN curs_get_columns t ora_dict_obj_owner. ora_dict_obj_name ') LOOP -- если текущий столбец был изменен, собщин об этой IF ORA_IS_ALTER_COLUMN (v_colLmn_rec.column_name) THEN -- если таблица/столбец является базовым... IF mycheck.is_app1ication_column ( ora_di ct_obj_owner, ora_dict_obj_name, v_column_rec.column name ) THEN RAISE_APPLICATION_ERROR ( -20001. 'Cannot alter core application attributes' ): END IF; -- таблица/столбец является базовым END IF: -- изменен текущий столбец END LOOP; -- каждый столбец а таблице END IF; -- таблица была изменена END:
I лава ю • i риггеры Теперь все попытки изменить базовые столбцы приложения будут пресекать- пресекаться системой. Результаты проверки в триггере наличия запрета на изменение содержимого таблицы не влияют на возможность добавления новых столбцов. Когда триггер DDL вызывается для только что добавленного столбца, информации об этом столб- столбце еще нет в словаре данных. Чтобы получить возможность выявлять попытки удаления определенных столб- столбцов, следует добавить следующий код: IF ORA_IS_DROP COLUMN CC0L21) THEN -- выполнить код! ELSE -- выполнить другой код! END IF: ПРИМЕЧАНИЕ- Функциям ORA_IS_DROP_C0LUMN и ORA_IS_ALTER_COLUMN ничего не известно о таблице, в состав которой входит определенный столбец; в качестве параметра им передается только имя столбца. Параметры, возвращаемые функциями Некоторые функции возвращают параметры двух типов: список элементов и ко- количество элементов. Например, функция ORA_GRANTEE возвращает список и коли- количество пользователей, которым предоставлены определенные права, а функция ORA_PRIVILEGE_L IST — список и количество предоставленных прав. Обычно обе они применяются в триггерах AFTER GRANT. Примером использования этих функций может служить файл whaLprivs.sql, представленный на узле издательства O'Reil- O'Reilly. Вот часть кода данного примера: /* Файл в web: what_privs.sql */ CREATE OR REPLACE TRIGGER what_privs AFTER GRANT ON SCHEMA DECLARE vgranttype VARCHAR2 C0); vjiun_grantees BINARYJNTEGER; v_grantee_Hst ora name_l1st_t: v_num_privs BINARYJNTEGER: v_priv_list ora name_list_t: BEGIN v_grant_type :-= ora_dict_obj_type; v_num_grantees := ora_grantee (v_grantee_list): v_num_privs := ora_privilege_list (v_pri«_1ist): IF v_grant_type - 'ROLE PRIVILEGE1 THEN DBMSJXJTPUT.putJine ( CHR (9) || The following roles/privileges were granted' FOR counter IN 1 .. v_num_pnvs LOOP
Триггеры уровня DDL 677 DBMSJJUTPUT.putJine ( CHR (9) || CHR (9) || 'Privilege ' || v_priv_list (counter) END LOOP: Такой триггер прекрасно подходит для получения подробной информации о правах пользователей и объектах базы данных, указанных в инструкциях GRANT. Эту информацию можно также сохранить в базе данных, добавив журнал с под- подробными сведениями об изменениях. SQL> GRANT DBA TO book WITH ADMIN OPTION: Grant succeeded. SQL> EXEC DBMS_OUTPUT.PuT_LINE('Flush buffer'); The following roles/privileges were granted Privilege UNLIMITED TABLESPACE Privilege DBA Grant Recipient BOOK Flush buffer SQL> GRANT SELECT ON x TO system WITH GRANT OPTION: Grant succeeded. SQL> EXEC DBMS_OUTPu7.PUT_LlNECF1ush buffer1); The following object privileges were granted Privilege SELECT On X with grant option Grant Recipient SYSTEM Flush buffer Удаление неудаляемого триггера Мы только что продемонстрировали одну из возможных целей применения триг- триггеров DDL, а именно блокировку выполнения DDL-инструкций, связанных с кон- конкретным объектом или типом объектов. Но что если мы создадим триггер, пре- препятствующий выполнению инструкции DROP, а затем попытаемся его удалить? Не получится ли так, что будет создан неудаляемый триггер? К счастью, как показы- показывает этот пример, Oracle позаботилась о том, чтобы такого не случилось: SQL> CREATE OR REPLACE TRIGGER undroppable 2 BEFORE DROP ON SCHEMA 3 BEGIN 4 RAISE APPLICATI0N_ERROR(-20000."You cannot drop nel I an invindblel'); 5 END; SQL> DROP TABLE employee; * ERROR at line 1: ORA-20000: You cannot drop me! I am invincible! SQL> DROP TRIGGER undroppable: Trigger dropped.
678 Глава 18 • Триггеры Триггеры событий базы данных Триггеры событий базы данных запускаются, когда происходит событие уровня базы данных. Перечислим их с указанием условия запуска: О STARTUP - при открытии базы данных; О SHUTDOWN - при нормальном закрытии базы данных; О SERVERERROR — при возникновении ошибки Oracle; О LOGON - при запуске сеанса Oracle; 0 LOGOFF — при нормальном завершении сеанса Oracle. Эти триггеры предоставляют прекрасные возможности для автоматизации про- процесса администрирования базы данных и управления ее функционированием. Создание триггера события базы данных Синтаксис объявления этих триггеров очень похож на синтаксис триггеров DDL. 1 CREATE [OR REPLACE] TRIGGER имя_триггера 2 {BEFORE | AFTER} {событие_6a3bijanHux) ON {DATABASE | SCHEMA} 3 DECLARE 4 Объявления переменных 5 BEGIN 6 ... некоторый код ... 7 END: He каждое событие может иметь оба триггера: BEFORE и AFTER. Объясняется это тем, что некоторые события просто невозможно связать с триггерами. Перечис- Перечислим такие случаи. О BEFORE STARTUP. Даже если такие триггеры можно было бы создавать, когда их следует запускать? Попытка создать триггер этого типа вызывает сообщение об ошибке: ORA-30500: database open triggers and server error triggers cannot have BEFORE type О AFTER SHUTDOWN. Опять же, когда должны запускаться такие триггеры? Попытка создать триггеры этого типа вызывает сообщение об ошибке: ORA-30501: instance shutdown triggers cannot have AFTER type О BEFORE LOGON. Эти триггеры должны содержать удивительно чуткий код: «Ка- «Кажется, кто-то собирается подключиться — сделаем что-нибудь!» Поскольку Oracle настроена очень реалистично, она не позволяет создавать такие тригге- триггеры и возвращает следующее сообщение: ORA-3D508: client logon triggers cannot have BEFORE type О AFTER LOGOFF. В данном случае была бы уместна такая фраза: «Подождите, по- пожалуйста, вернитесь! Не отключайтесь!» Попытка создать триггер этого типа также вызывает сообщение об ошибке: ORA-30509: client logoff triggers cannot have AFTER type
Триггеры событий базы данных 679 О BEFORE SERVERERROR. Такой триггеры был бы мечтой любого программиста. Толь- Только представьте себе: CREATE OR REPLACE TRIGGER BEFORE_SERVERERROR BEFORE SERVERERROR ON DATABASE BEGIN diagnose_impend1ng_error: -- обнаруживаем будущую ошибку fix_error_condition: -- исправляеп ее причину continue_as_if_nothing_happened: -- продолжаем, как будто ничего не случилось END: К сожалению, наши мечты будут развеяны таким сообщением об ошибке: 0RA-30500: database open triggers and server error triggers cannot have BEFORE type Триггеры STARTUP Триггеры типа STARTUP запускаются при загрузке базы данных. Это прекрасный момент для выполнения подготовительных работ, в частности для фиксации объек- объектов в общем пуле (иначе они могут быть удалены в соответствии с алгоритмом «дольше всех не использовавшийся»). ПРИМЕЧАНИЕ Для того чтобы создать триггер события STARTUP, пользователь должен иметь привилегию ADMI- ADMINISTER DATABASE TRIGGER. Вот как создается триггер STARTUP: CREATE OR REPLACE TRIGGER startup pinner AFTER STARTUP ON DATABASE BEGIN pin_plsql_paclcages: pin_application packages; END: Триггеры SHUTDOWN Триггеры BEFORE SHUTDOWN запускаются перед закрытием базы данных. Они пред- представляют собой прекрасное средство для сбора статистических данных о системе. Приведем пример создания триггера события SHUTDOWN: CREATE OR REPLACE TRIGGER after shutdown AFTER SHUTDOWN ON DATABASE BEGIN gather system_stats; END: ПРИМЕЧАНИЕ- Триггеры SHUTDOWN запускаются только в случае останова базы данных в режиме NORMAL или IMMEDIATE. Но они не выполняются, когда база данных останавливается в режиме ABORT, а также при аварийном завершении ее работы.
680 Глава 18 • Триггеры Триггеры LOGON Триггеры AFTER LOGON запускаются в начале сеанса Oracle. Их удобно использо- использовать для формирования контекста сеанса и выполнения других установочно-на- строечных задач. Вот пример триггера события LOGON: CREATE OR REPLACE TRIGGER afterJogon AFTER LOGON ON SCHEMA DECLARE v_sql VARCHAR2U00) :- 'ALTER SESSION ENABLE RESUMABLE ' || 'TIMEOUT 10 NAME ' || "" || 'OLAP Session1 || ""; BEGIN EXECUTE IMMEDIATE w_sql; DBMS_SESSION.SET_CONTEXTC'OLAP Namespace1. 'Customer ID1. load_user_customer_id): END: Триггеры LOGOFF Триггеры BEFORE LOGOFF запускаются при нормальном завершении сеанса, то есть при отсоединении пользователя от базы данных. Это удобное средство для сбора статистических сведений о параметрах сеансов: CREATE OR REPLACE TRIGGER before_Logoff BEFORE LOGOFF ON DATABASE BEGIN gather_sess1on_stats: END; Триггеры SERVERERROR Триггеры AFTER SERVERERROR запускаются в момент, когда Oracle генерирует ка- какую-либо ошибку. Исключение составляют следующие ошибки (для них тригге- триггеры не создаются): О ORA-0Q600 — внутренняя ошибка Oracle (Oracle internal error); О ORA-01034 - Oracle недоступна (Oracle not available); О ORA-01403 — данные не найдены (No data found); О ORA-01422 — при выборке данных возвращено больше строк, чем указано в за- запросе (Exact fetch returns more than requested number of rows); О ORA-01423 — при попытке выборки дополнительных строк произошла ошибка (Error encountered while checking for extra rows in an exact fetch); О ORA-04030 — нехватка памяти процесса при попытке выделения N байт памяти (Out-of-process memory when trying to allocate N bytes). Кроме того, триггеры AFTER SERVERERROR не запускаются, когда исключение ини- инициируется внутри триггера (во избежание рекурсивного выполнения триггера).
Триггеры событий базы данных 681 Oracle предоставляет две встроенные функции (как и остальные, они опреде- определены в пакете DBMS_STANDARD), возвращающие информацию о стеке ошибок, кото- который формируется при возникновении исключения. О ORASERVERERROR — возвращает номер ошибки Oracle в заданной позиции сте- стека. Если в этой позиции ошибки нет, возвращает 0. О ORA_IS_SERVERERROR — возвращает значение TRUE, если в стеке ошибок текущего исключения имеется ошибка с заданным номером. Примеры триггеров события SERVERERROR Рассмотрим несколько примеров использования функций SERVERERROR. Начнем с очень простого примера триггера, сообщающего о том, что произошла ошибка: CREATE OR REPLACE TRIGGER error_echo AFTER SERVERERROR ON SCHEMA BEGIN DBMS_OUTPUT.PUT_LINE ('You experienced an error1): END: После возникновения ошибки Oracle (и при условии, что параметр SERVEROUT- PUT имеет значение ON) на экран будет выведено такое сообщение: SQL> SET SERVEROUTPUT ON SQL> EXEC DBMSOUTPUT.Pin_LINE[TO_NUMBERCA')): You experienced an error BEGIN DBMS OUTPUT.PUT LINEUO NUMBERC A')); END; ERROR at line 1: ORA-06502: PL/SOL: numeric or value error: character to number conversion error ORA-06512: at line 1 Обратите внимание на тот факт, что сообщение об ошибке Oracle выводится после сообщения триггера. Благодаря этому информацию об ошибке можно по- получить еще до того, как Oracle предпримет какие-либо действия (см. следующий пример). РИМЕЧАНИЕ Триггеры SERVERERROR способны автоматически изолироваться в собственной автономной тран- транзакции (об автономных транзакциях рассказывается в главе 13). Это означает, что можно, к приме- примеру, записать информацию об ошибке в таблицу-журнал и сохранить ее с помощью инструкции COMMIT, не затрагивая транзакцию сеанса, в которой произошла ошибка. Рассмотрим пример, в котором триггер error_echo обеспечивает гарантию того, что информация обо всех ошибках (кроме нескольких перечисленных выше) бу- будет автоматически записываться в журнал. При этом не имеет значения, кем или чем была вызвана ошибка — пользователем, самим приложением или какой-то программой. /* Файл в web: error log.sql */ CREATE OR REPLACE TRIGGER error-Jog AFTER SERVERERROR ON SCHEMA DECLARE
682 Глава 18 • Триггеры v_errnum NUMBER; -- номер шибки Oracle vjiow DATE :- SYSDATE; -- текущее время v_caunter NUMBER :- 1: -- счетчик стека BEGIN -- для каждой ошибки в стеке ... LOOP -- считывание из стека номера ошибки v_errnum :- ORA_SERVER_ERROR(v_counter); -- если номер ошибки равен 0. выполнение копа завершается EXIT WHEN v_errnum - 0: -- запись информации об ошибке в журнал; фиксация транзакции -- не требуется, поскольку данная транзакция автономна INSERT INTO errorjogfusername. еггог_Л1лпЬег. sequence, timestamp) VALUEStUSER, v_errnum, v_counter. v_now): -- увеличение значения счетчика и повторение операции v_counter :- v_counter + 1; END LOOP: -- для каждой ошибки а стеке END: Помните, что все новые строки таблицы error! од сохраняются в момент вы- выполнения оператора END, поскольку триггер запускается как автономная транзак- транзакция. Действие самого триггера демонстрируется в следующем примере: SQL> EXEC DBMS_OUTPUT.PUT_LINECTO_NUMBERCA')); * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: character to number conversion error SQL> SELECT * FROM error!og; USERNAME ERROR NUMBER SEQUENCE TIMESTAMP BOOK 6502 1 04-JAN-02 BOOK 6512 2 04-JAN-D2 Почему же в таблице содержатся записи о двух ошибках, если Oracle возвра- возвратила только одну из них? Реальный стек ошибок Oracle содержит описание оши- ошибок ORA-06502 и ORA-06512, так что они обе были запротоколированы в порядке их расположения в стеке. Если вам нужно быстро определить, имеется ли в стеке информация об ошиб- ошибке с определенным номером, воспользуйтесь функцией QRA_IS_SERVERERROR. Эта
Триггеры событий базы данных 683 функция особенно пригодится для мониторипга конкретных ошибок, наподобие пользовательских исключений, требующих дополнительной обработки. Вот при- пример кода, в котором применена данная функция: -- нестандартная обработка пользовательских ошибок -- с нонерами от 20000 яо 20010. генерируеиых -- с помощью вызовов RAISE_APPLICATION_ERROR FOR errnum IN 20000 .. 20010 LOOP IF ORA_IS_SERVERERROR (errnum) THEN log_user_defined_error Cerr-mm): END IF; END LOOP: ПРИМЕЧАНИЕ- Номера всех ошибок Oracle представлены отрицательными значениями, за исключением 1 (пользо- (пользовательское исключение) и 100 (синоним ошибки -1403, NO_DATA_FOUND). Однако в вызове функ- функции ORA_IS_SERVERERROR должен задаваться положительный номер ошибки, как в приведенном выше примере. Централизованный обработчик ошибок Хотя для каждой схемы базы данных можно определить отдельный триггер SER- VERERROR, мы рекомендуем создать один централизованный триггер и сопутствую- сопутствующий пакет PL/SQL Такой прием дает вам ряд преимуществ. О Централизованная обработка ошибок. В памяти Oracle хранится только один пакет и один тригтер для обработки ошибок. О Возможность использовать один журнал ошибок в течение всего сеанса. В журнале накапливается информация об ошибках, возникающих на протя- протяжении всего сеанса. Благодаря этому существует возможность получать сведе- сведения о количестве повторений данной ошибки, времени и дате ее первого или последнего возникновения и т. п. Журнал может очищаться по мере необходи- необходимости. При желании его можно сохранить в таблице базы данных. О Отбор информации из журнала ошибок. Из журнала ошибок можно извле- извлекать информацию об ошибках с определенным номером и/или ошибках, про- происшедших в указанном временном диапазоне. Готовый централизованный пакет для обработки ошибок содержится в файле errorjog.sql, представленном на узле издательства O'Reilly. Имея подобный пакет, можно создать следующий триггер обработки события SERVERERROR: CREATE OR REPLACE TRIGGER errorjog AFTER SERVERERROR ON DATABASE BEGIN central_error_log.1og_error: END: Ниже приводится несколько примеров' его использования. Сначала генериру- генерируем ошибку: SQL> EXEC DBMS_OUTPUT.PUT_LINECTO_NUMBER('A1)):
684 • Глава 18 • Триггеры ERROR at line 1: ORA-06502: PL/SOL: numeric or value error: character to number conversion error Теперь можно найти ошибку с заданным номером и сохранить информацию о ней в записи: DECLARE v_find_record central_error_log.v_find_record: BEGIN central_error_log.find_errorF502.v_find_record): DBMS_OJTPUT.PUT_LINE('Total Found - ' || v_find_record.total_found); QBMS_OUTPUT.PUT_LINE('Min Timestamp - ' || v_find_record.min_timestamp); DBMS_OUTPUT.PUT_LINE('Max Timestamp = ' || v_find_record.max_t1mestamp): END: Результат выполнения этого кода таков: Total Found =¦ 1 Min Timestamp - 04-JAN-02 Max Timestamp = 04-JAN-D2 Неработоспособные триггеры Неработоспособный триггер может оказать отрицательное влияние на функцио- функционирование объектов базы данных. Что мы имеем в виду, вы узнаете из представ- представленной ниже таблицы. Событие Что произойдет, если триггер неработоспособен AFTER SERVERERROR Для всех транзакций, в которых возникнут ошибки Oracle, будет выполняться откат с выводом сообщения о неработоспособном триггере AFTER LOGON Подключаться к Oracle смогут только пользователи с привилегией ADMINISTER DATABASE TRIGGER BEFORE LOGOFF Отсоединяясь от базы данных, пользователи будут получать сообщение о неработоспособном триггере BEFORE SHUTDOWN В журнал сообщений будет записано сообщение о неработоспособном триггере, но останов базы данных будет выполняться корректно AFTER STARTUP В журнал сообщений базы данных будет записываться сообщение о неработоспособном триггере, но запуск базы данных будет выполняться корректно Триггеры INSTEAD OF Триггеры INSTEAD OF (замещающие триггеры) предназначены для выполнения опе- операций вставки, обновления и удаления элементов представлений, но не таблиц. С их помощью можно сделать необновляемое представление обновляемым и из- изменить поведение обновляемого представления по умолчанию.
Триггеры событий базы данных 685 Создание триггера INSTEAD OF Инструкция создания (или замены) триггера INSTEAD OF имеет следующий син- синтаксис: 1 CREATE [OR REPLACE] TRIGGER иня_триггера 2 INSTEAD OF операция ON имя_превставления 3 ON имя_прецставления 4 FOR EACH ROW 5 BEGIN 6 ... программный коя ... 7 END; Более подробно о выполняемых ею действиях вы узнаете из таблицы. Строки Описание 1 Создается триггер с заданным именем. Если триггер существует, но предложение OR REPLACE не задано, Oracle сгенерирует ошибку ORA-4081 2 Строка идентифицирует триггеры INSTEAD OF. Поскольку они запускаются не в ответ на определенные события, а выполняются вместо инструкции SQL, для них не нужно указывать ни ключевое слово AFTER или BEFORE, ни имя события. Правда, необходимо задать инструкцию — INSERT, UPDATE или DELETE, — которую должен выполнять триггер 3 Эта строка подобна строке ON триггеров DDL и событий базы данных, однако вместо ключевого слова DATABASE или SCHEMA в ней задается имя представления, с которым связан триггер 4-7 Вставляется стандартный код PL/SQL Принцип действия триггеров INSTEAD OF проще всего объяснить на примере системы доставки пиццы (это моя любимая тема). Наша система учета доставки будет состоять из трех таблиц: одна для учета доставленной продукции, вторая - для хранения списка районов доставки, а третья — для учета объемов работы, вы- выполненной курьерами. /* Файл в web: pizza tables.sql */ CREATE TABLE delivery (del i very jd NUMBER. deli very_start DATE. deli very end DATE. area_id NUMBER. driver jd NUMBER); CREATE TABLE area (areajd NUMBER, area_desc VARCHAR2C0)); CREATE TABLE driver (driverjd NUMBER, driverjiame VARCHAR2C01): Желая упростить пример, мы решили отказаться как от первичных, так и от внешних ключей.
686 Глава 18 • Триггеры Нам нужны три последовательности, обеспечивающие таблицы уникальными идентификаторами: CREATE SEQUENCE del1very_id_seq: CREATE SEQUENCE area_id_seq; CREATE SEQUENCE driver_id_seq: Чтобы не усложнять структуру базы данных, объединяем всю информацию о доставках, районах и курьрах в одно представление: CREATE OR REPLACE VIEW delivery_info AS SELECT d.del iveryjd. d.delivery start. d.delivery_end. a.area_desc, dr.driverjiame FROM delivery d. area a. driver dr WHERE a.areajd = d.area_id ANO dr.driverjd - d.driverid; Поскольку вся система запросов строится вокруг этого представления, почему бы нам заодно не выполнять с его помощью вставку, обновление и удаление дан- данных? В этом деле нам помогут триггеры INSTEAD OF. Наша же задача — сообщить базе данных, как выполнять операции вставки, обновления и удаления для пред- представления del I very_i nf о. Начнем с триггера INSTEAD OF INSERT. Триггер INSTEAD OF INSERT Триггер INSERT должен выполнить четыре основные функции: О предоставить гарантию того, что столбец deli very end будет содержать значе- значение NULL (на факт доставки указывает только обновление записи); О определить идентификатор курьера с учетом заданного имени, но если нуж- нужное имя в таблице отсутствует, триггер присвоит курьеру другой идентифика- идентификатор и добавит строку в таблицу курьеров, записав в нее заданное имя и новый идентификатор; О определить идентификатор района на основе заданного названия, но если нуж- нужное название в таблице отсутствует, триггер присвоит району другой иденти- идентификатор и добавит строку в таблицу районов, записав в нее заданное название и новый идентификатор; О добавить строку в таблицу доставки. Имейте в виду, что данный пример демонстрирует использование триггеров, а не эффективный способ построения бизнес-систем! Через некоторое время у нас наверняка скопятся повторяющиеся строки с идентификаторами. Однако это представление ускоряет работу, не требуя обязательного предварительного опре- определения курьеров и районов. А ведь при доставке пиццы время — деньги! /* Файл з web: pizza_triggers.sql */ CREATE OR REPLACE TRIGGER deliveryjnfojnsert INSTEAD OF INSERT ON delivery_info
Триггеры событий базы данных 687 DECLARE -- курсор для получения идентификатора курьера исходя из его имени CURSOR curs_get_driveMd С cp_driver_name VARCHAR2 ) IS SELECT driverid FROM driver WHERE driver_name = cp_driver_name; vjJriveMd NUMBER; -- курсор для получения идентификатора района исходя из его названия CURSOR curs_get_area_id ( cp_area_desc VARCHAR2 ) IS SELECT areajd FROM area WHERE areadesc - cp_area_desc: v_area_id NUMBER; BEGIN /* || Значение столбца delivery_end должно быть равно NULL */ IF :new.del1very_end IS NOT NULL THEN RAISE_APPLICATION_ERROR(-20000.'Delivery end date value must be NULL when delivery created'): END IF; /* || Пытаемся получить идентификатор курьера исходя из его имени || Если имя не найдено, создаем новый идентификатор || на основе последовательности */ OPEN curs_get_driverJd(UPPER(:new.driver_name)); FETCH curs_get_driver_id INTO v_driver_id; IF curs_get_driver_idXNOTFOUND THEN SELECT driver_id_seq.nextval INTO v_dhver_id FROM DUAL; INSERT INTO dnver(driverjd.driver_name) VALUESC.v_dnver_id.UPPER( -.new.driverjiame»: END IF; CLOSE curs_get_driver_id; /* || Пытаемся получить идентификатор района исходя из его названия || Если название в таблице отсутствует, создаем идентификатор || на основе последовательности */ OPEN curs_get_area_idtUPPER(;new.area_desc)); FETCH curs_get_area_id INTO v_area_1d; IF ajrs_get_area_idfcNOTFOUND THEN SELECT area_id_seq.nextval INTO v_area_id FROM DUAL; INSERT INTO areatarea id.area desc)
688 Глава 18 • Триггеры VALUESCv_area_i d.UPPERC:new.area_desc)); END IF: CLOSE curs_get_area_id; /* || Добавляем строку с информацией о доставленной продукции */ INSERT INTO delivery(delivery_id. delivery_start, delivery_end. areajd. driver_id) VALUES(deliveryJd_seq.nextval. NVL(:NEW.delivery_start.SYSDATE), NULL, v_area_id. v_driver_id); END: Триггер INSTEAD OF UPDATE Теперь рассмотрим триггер UPDATE. Для того чтобы упростить код, мы будем об- обновлять только столбец deli very end, причем только в том случае, если он содер- содержит значение NULL (курьеры должны строго соблюдать время доставки): CREATE OR REPLACE TRIGGER deliveryjnfo_update INSTEAD OF UPDATE ON deliveryjnfo DECLARE -- курсор для получения строки доставки CURSOR oirs_get_del1very ( cpjeliveryid NUMBER ) IS SELECT deliveryend FROM delivery WHERE deliveryjd - cp_delfvery_id FOR UPDATE OF delivery_end; v_del1very_end DATE: BEGIN OPEN curs_get_delivery(:NEW.deliveryjd): FETCH curs_get_delivery INTO v_del1very_end: IF v_delivery_end IS NOT NULL THEN RAISE_APPLICATION_ERRORC-20Q0O.'The delivery end date has already been set'): ELSE UPDATE delivery SET delivcry_end = :new.delivery_end WHERE CURRENT OF curs_get_delivery; END IF; CLOSE curs_get_delivery: •END:
Триггеры событий базы данных 689 Триггер INSTEAD OF DELETE В рассматриваемом примере триггер DELETE имеет самую простую структуру. Его основная задача — следить за тем, чтобы никто не удалил заполненную строку, а затем самому удалить указанную строку доставки. Строки в таблицах курьеров и районов остаются неизменными. CREATE OR REPLACE TRIGGER delivery_info_delete INSTEAD OF DELETE ON deliveryjnfo BEGIN IF :new.delivery_end IS NOT NULL THEN RAISE_APPLICATION_ERROR(-20000,'Completed deliveries cannot be deleted'): END IF; DELETE delivery WHERE delivery_id - :new.delivery_id: END: Заполнение таблиц Теперь мы можем заполнить все три таблицы, воспользовавшись только инструк- инструкцией INSERT, которая содержит полную информацию о доставке: SQL> INSERT INTO delivery info (del Iveryid, 2 deliverystart, 3 deliveryend, 4 areadesc, 5 drivername) 6 VALUES 7 (NULL, NULL, NULL. 'LOCAL COLLEGE'. 'BIG TED'); 1 row created. SQL> SELECT * FROM delivery; DELIVERYJD DELIVERY, DELIVERY, AREAJD DRIVERJD 1 13-JAN-02 1 1 SQJ> SELECT * FROM area; AREAIO AREADESC 1 LOCAL COLLEGE SQL> SELECT * FROM driver; DRIVER ID DRIVER NAME 1 BIG TED
690 Глава 18 • Триггеры Триггеры AFTER SUSPEND В Oracle9i был введен новый тип триггеров, запускаемых в ответ на приостанов- приостановление выполнения инструкции. Одной из причин такого приостановления может быть нехватка памяти, например, вследствие превышения квоты выделенного про- процессу табличного пространства. Триггер AFTER SUSPEND может использоваться для урегулирования ситуации, в результате чего выполнение инструкции будет во- возобновлено. Это просто подарок для разработчиков, которым надоели проблемы, связанные с памятью, и для администраторов баз данных, которым приходится их решать. В качестве примера рассмотрим проблему, с которой столкнулся Бэч Онли - лучший разработчик компании Totally ControUed Systems. Он отвечал за сопрово- сопровождение сотен программ, работающих на протяжении ночи и выполняющих длин- длинные транзакции, объединяющих информацию и распределяющих ее между от- отдельными приложениями. Как минимум дважды в неделю в предрассветные часы его будил пейджер, из-за того что в одной из программ возникала следующая ошибка Oracle: ERROR at line 1: ORA-01536: space quota exceeded for tablespace 'USERS1 И тогда Беч должен был звонить главному администратору базы данных Дону Планахеду и просить его увеличить квоту табличного пространства. Обычно Дон спрашивал: «Сколько тебе нужно?», на что Бэч бормотал: «Не знаю, объем дан- данных каждый раз иной»-. И оба они оставались недовольны друг другом. Предпосылки создания триггера Наилучшим выходом из сложившейся ситуации, конечно же, стало использова- использование триггера AFTER SUSPEND. Вот как все было реализовано. Бэч обнаружил в коде строку, при выполнении которой чаще всего происхо- происходила ошибка. Это была инструкция INSERT в конце программы, на выполнение ко- которой уходило много времени: INSERT'INTO monthly_summary С acct_no, trx_count. totaljn, total_out) VALUES ( v_acct. v_trx_count. v_total_in, v_tota"l_out): А самое неприятное было то, что значения, вычислявшиеся часами, могли быть утеряны в случае неудачного выполнения единственной инструкции INSERT. Бэч, естественно, предложил приостанавливать работу программы на то время, пока он будет просить Дона о выделении дополнительного табличного пространства. Оказалось, что это нетрудно сделать с помощью простой инструкции ALTER SESSION: ALTER SESSION ENABLE RESUMABLE TIMEOUT 3600 NAME 'Monthly Simmary1: Как только в сеансе Oracle возникнет ошибка из-за нехватки памяти, выполне- выполнение сеанса должно приостановиться (с потенциальным возобновлением) на 3600 с, то есть на 1 ч. Этого вполне достаточно, чтобы система мониторинга передала со- сообщение на пейджер Бэчу, тот позвонил Дону, а Дон выделил дополнительную
Триггеры AFTER SUSPEND 691 память. Решение, конечно, далеко от совершенства, но по крайней мере вычис- вычисленные данные больше не будут теряться. Еще одна проблема заключалась в том, что ошибка возникала ночью, когда оба сотрудника не могли быстро принимать решения из-за усталости или просто по причине взаимного непонимания. Однако оказалось, что от такого рода объяс- объяснений легко уйти, воспользовавшись еще одним элементом системы приостанов- приостановления/возобновления — представлением DBA_RESUMABLE. Оно возвращает инфор- информацию обо всех сеансах, в которых выполнялась приведенная выше инструкция ALTER SESSION. ПРИМЕЧАНИЕ Для того чтобы пользователь мог задать опцию RESUMABLE, он должен иметь системную привиле- привилегию с таким же именем. Теперь во время приостановления программы Бэчу достаточно позвонить Дону и обратиться к нему с просьбой проверить представление DBA_RESUMABLE. Дон вы- выполняет запрос для этого представления, и ему сразу становится ясно, в чем дело: SOL> run 1 SELECT sessionjd, 2 name, 3 status, 4 errorjiumber 5 FROM dbaresuraable SESSIONJD NAME STATUS ERRORJUMBER' 8 Monthly Summary SUSPENDED 1536 1 row selected. Данные, полученные в результате выполнения этого запроса, говорили о том, что сеанс 8 был приостановлен из-за ошибки ORA-01536: space quota exceeded for tablespace 'имя табличного пространства'. По своему опыту Дон знал, о какой схеме и о каком табличном пространстве идет речь, поэтому он сразу же оценивал си- ситуацию и сообщал Бэчу об объеме выделяемой памяти. Выполнение приостанов- приостановленной инструкции в программе Бэча немедленно возобновлялось, а Дон и Бэч с чувством исполненного долга отправлялись спать. Пример реализации В конце концов Дону и Бэчу надоели постоянные ночные разговоры об одном и том же, и Дон решил автоматизировать процесс выделения дополнительной па- памяти с помощью триггера AFTER SUSPEND. Вот какой триггер он написал и устано- установил, подключившись к базе данных с учетной записью администратора: /* Файл 8 web: smart_space_quota.sql */ CREATE OR REPLACE TRIGGER after_suspend AFTER SUSPEND ON DATABASE DECLARE
692 Глава 18 • Триггеры -- курсор для получения имени пользователя текущего сеанса CURSOR curs_get_username IS SELECT username FROM v$session WHERE audsid - SYS_CONTEXT('USERENV.'SESSIONID'): v_username VARCHAR2O0): -- курсор для определения квоты для пользователя и табличного пространства CURSOR curs_get_ts_quota С cp_tbspc VARCHAR2, cp_user VARCHAR2 ) IS SELECT max_bytes FROM dba_ts_quotas WHERE tabTespace_name = cp_tbspc AND username = cp_user; v_old_quota NUMBER: v_new_quota NUMBER: -- содержит информацию из таблицы SPACE ERROR_INFO v_error_type VARCHAR2C30); v_object_type VARCHAR2O0); v_object_owner VARCHAR2C0): v_tbspc_name VARCHAR2C0); v_object_name VARCHAR2C30); v_subobject_name VARCHAR2C30); -- код SQL для выделения дополнительных ресурсов v_sql VARCHAR2U000); BEGIN -- если ошибка связана с пространством... IF ORA_SPACE_ERROR_INFO [ error_type => v_error_type, object_type => v_object_type. object_owner -> v_object_owner, table_space_name => v_tbspc_name. object_name -> v_object_name. sub_object_name => v_subobject_name ) THEN -- если ошибка обусловлена превышением квоты -- табличного пространства... IF v_error_type = 'SPACE QUOTA EXCEEDED1 AND v_object_type - 'TABLE SPACE' THEN -- получаем имя пользователя OPEN curs_get_username: FETCH curs_get_username INTO v_usernarae: CLOSE curs_get_username: -- определяем текущую квоту OPEN curs_get_ts_qjota(v_object_name,vjjsername); FETCH curs_get_ts_quota INTO v_ald_quota: CLOSE curs_get_ts_quota:
Триггеры AFTER SUSPEND . 693 -- Формируем инструкцию ALTER USER и отправляем ее -- заданию fixer, поскольку, если попытаться выполнить --ее прямо отсюда, произойдет ошибка -- ORA-30511: invalid DDL operation in system triggers v_new_quota :- v_old_quota + 40960; v_sql :- 'ALTER USER ' || vjjsername || ' ' || 'QUOTA ' || v_new_quota || ' ' || 'ON ' || v_object_name: fixer. fix_this(v_sql); END IF; -- превышена квота табличного пространства END IF: -- ошибка, связанная с пространством END: Созданный триггер запускается в случае приостановления выполнения инст- инструкции и пытается изменить ситуацию. (Обратите внимание, что в данном приме- примере обрабатывается единственная ошибка, которая возможна при недостатке таб- табличного пространства.) НЕДОПУСТИМЫЕ ОПЕРАЦИИ DDL В СИСТЕМНЫХ ТРИГГЕРАХ Системным триггерам AFTER. SUSPEND не разрешается выполнять определен- определенные DDL-инструкции, в том числе ALTER USER и ALTER TABLESPACE. Поэтому они не имеют возможности непосредственно исправить выявленную ошиб- ошибку. Единственное, что им остается, — это сгенерировать сообщение об ошиб- ошибке ORA-30511-. Invalid DDL operation in system triggers. Но при необходимо- необходимости вы можете обойти это ограничения, выполнив следующие действия. 1. В триггере AFTER SUSPEND составьте SQL-инструкцию, способную устра- устранить проблему, и запишите ее в таблицу. 2. Создайте пакет PL/SQL для считывания указанной SQL-инструкции из таблицы и последующего ее выполнения. 3. Определите этот пакет в качестве задания, выполняемого с помощью пакета DBMSJOB, предположим, через каждую минуту. Теперь когда при работе приложения возникает ошибка из-за недостатка таб- табличной памяти, запускается триггер AFTER SUSPEND уровня базы данных, который генерирует SQL-инструкцию, увеличивающую размеры памяти, и с помощью па- пакета fixer помещает ее в специальную таблицу. Выполняемое в фоновом режиме задание fixer считывает эту SQL-инструкцию и запускает ее. Таким образом, про- проблема недостаточной квоты решается без единого телефонного звонка. 1РИМЕЧАНИЕ- Полный исходный код триггера after_suspend и пакета fixer имеется в файле fixer.sql на web-узле из- издательства O'Reilly.
694 Глава 18 • Триггеры Создание триггера Синтаксис триггера AFTER SUSPEND тот же, что и у триггеров событий базы данных. В объявлении этого триггера определяется событие (SUSPEND), время (AFTER) и об- область его действия (DATABASE или SCHEMA) CREATE [OR REPLACE] TRIGGER ння_триггера AFTER SUSPEND ON {DATABASE | SCHEMA} BEGIN ... код ... END; Функция ORA_SPACE_ERROR_INFO Информацию о причине приостановления выполнения инструкции можно по- получить с помощью функции ORASPACEERRORINFO, как показано в приведенных выше примерах. Рассмотрим синтаксис этой функции. Ее параметры описаны в табл. 18.3. Таблица 1Я.З. Параметры функции ORA_SPACE_ERROR_INFO Параметр Описание ERROR_TYPE Тип ошибки; может быть одним из следующих: SPACE QUOTA EXCEEDED — пользователь превысил свою квоту табличного пространства, MAX EXTENTS REACHED — объект пытается выйти за пределы выделенной для него памяти (измеряется в экстентах), N0 MORE SPACE — в табличном пространстве недостаточно места для записи новой информации OBJECT_TYPE Тип объекта, с которым связана ошибка OBJECT_OWNER Имя владелец объекта, с которым связана ошибка TABLE_SPACE_NAME Табличное пространство, с которым связана ошибка OBJECT_NAME Имя объекта, с которым связана ошибка SUB_OBiECT_NAME Имя подчиненного объекта, с которым связана ошибка Данная функция возвращает значение TRUE, если приостановление произошло вследствие одной из указанных в таблице ошибок, или значение FALSE в против- противном случае. Функция ORA_SPACE_ERROR_INFO не устраняет возникшие в системе проблемы; ее задача — предоставление информации, необходимой для их решения. В опи- описанном выше случае ошибка возникала из-за недостатка квоты табличного про- пространства. Приведем еще два примера SQL-инструкций, которые могут использо- использоваться для исправления ошибок, обнаруженных с помощью функции ORA_SPA- CE_ERROR_INFO. Если таблица или индекс заняли всю выделенную для них память, можно вы- выполнить инструкцию ALTER <ш_оВьекта> <впацепец_о6ъекта>.<имя_объекта> STORAGE (MAXEXTENTS UNLIMITED);
Триггеры AFTER SUSPEND 695 Когда же вам не хватает табличного пространства, воспользуйтесь инструкцией /* Предполагается, что используются Oracle Managed Files (9i), поэтому явное объявление файла санных не требуется */ ALTER TABLESPACE <table_space_name^ ADD DATAFILE: Пакет DBMS_RESUMABLE Если функция ORA_SPACE_ERROR_INFO возвращает значение FALSE, то причину приос- приостановления инструкции устранить невозможно. Нельзя также допустить, чтобы инструкция оставалась приостановленной. Ее выполнение должно быть заверше- завершено путем вызова из триггера AFTER_SUSPEND процедуры ABORT, которая входит в со- состав пакета OBMSRESUMABLE. Проиллюстрируем сказанное следующим примером: /* Файл в web: lccal_abort.sql */ CREATE OR REPLACE TRIGGER after_suspend AFTER SUSPEND ON SCHEMA DECLARE CURSOR curs_get_sid IS SELECT sid FROM v$session WHERE audsid - SYS_CONTEXTCUSERENV . 'SESSIONID1): v_sid NUMBER: BEGIN IF ORA_SPACE_ERROR_INFO( ... ... пытаемся изменить ситуацию. ... ELSE -- но сделать это невозможно OPEN curs_get_sid; FETCH curs_get_sid INTO v_sid: CLOSE curs_get_sid; DBMS_RESUMABLE.A8ORT[v_sid); END IF: END: Процедура ABORT принимает единственный параметр — идентификатор сеанса, в котором операция должна быть завершена. Это позволяет вызывать данную процедуру из триггеров уровня схемы и базы данных. Если пользователь потре- потребовал отменить текущую операцию, но не указал, какую именно, будет получено сообщение об ошибке такого содержания: ORA-01013: user requested cancel of current operation Функция GET_SESSION_TIMEOUT возвращает, а функция SET_SESSION_TIMEOUT уста- устанавливает значение тайм-аута приостановленного сеанса, определяемого задан- заданным идентификатором: FUNCTION DBMS_REUSABLE.GET_SESSION_TIMEOUT (sessionid IN NUMBER) RETURN NUMBER: PROCEDURE DBMS_REUSA8LE.SET_SESSION_TIME0UT С sessionid IN NUMBER. TIMEOUT IN NUMBER):
696 Глава 18 • Триггеры Функция GET_TIMEOUT возвращает, а функция SET_TIMEOUT устанавливает значе- значение тайм-аута текущего сеанса; FUNCTION DBMS_REUSABLE.GET_TIMEOLTT RETURN NUMBER: PROCEDURE DBMSJEUSABLE.SETJIMEOUT (TIMEOUT IN NUMBER); ПРИМЕЧАНИЕ Новое значение тайм-аута вступает в действие немедленно; только не следует задавать его рав- равным нулю. Многократный запуск триггера Триггер AFTERSUSPEND запускается в случае приостановления инструкции. Поэто- Поэтому в течение выполнения одной инструкции он может запускаться неоднократно. В качестве примера рассмотрим следующий триггер: /* Файл в web: increment_extents.sql */ CREATE OR REPLACE TRIGGER after_suspend AFTER SUSPEND ON SCHEMA DECLARE -- получение нового максимального значения Стершее плис единица) CURSOR curs_get_extents IS SELECT max_extents + 1 FROM user_tables WHERE table_naine = ' MONTHLY_SUMMARY ¦; v_new_max NUMBER; BEGIN -- выбор нового максимального количества экстентов OPEN curs_get_extents; FETCH curs_get_extents INTO v_new_max: CLOSE curs_get_extents: -- изменение таблицы путем задания нового -- максимального количества экстентов EXECUTE IMMEDIATE 'ALTER TABLE MONTHLY_SUMMARY ' || 'STORAGE ( MAXEXTENTS ' || v_new_max 11 ')'; DBMS_OUTPUT.PUT_LINECIncremented MAXEXTENTS to ' j| v_newjnax); END; Если мы создадим пустую таблицу, задав для ее параметра MAXEXTENTS (макси- (максимальное количество экстентов) значение 1, то при попытке поместить в таблицу данные объемом в 4 экстента получим такой результат: SQL> @test Incremented MAXEXTENTS to 2 Incremented MAXEXTENTS to 3 Incremented MAXEXTENTS to 4 PL/SQL procedure successfully completed. В предыдущих примерах было показано, как обрабатывать ошибки, которые возникают из-за недостатка табличного пространства, приостанавливая инструк- инструкции до тех пор, пока не будет обеспечена возможность их продолжения. Такой
Сопровождение триггеров 697 подход позволяет устанавливать для приложения минимальные параметры таб- табличного пространства, квоты, а также выделять необходимое количество экстен- экстентов памяти, изменяя его по мере необходимости. И хотя для администратора базы данных это может показаться пределом мечтаний, на самом деле подобный под- подход имеет существенные недостатки. О Периодические приостановления. Периодические приостановления инструк- инструкции могут значительно затруднить работу приложений, предназначенных для оперативной обработки транзакций (OLTP) и отличающихся особо высокой нагрузкой. Этот прием может быть совершенно недопустим, если на устране- устранение проблем уходит много времени. О Соревнование за ресурсы. Если таблица, которая связана с приостановлен- приостановленной инструкцией, окажется заблокированной, выполнение других инструкций будет отменено или отложено на неопределенное время. О Повышенные затраты. Дополнительные затраты ресурсов системы из-за по- постоянного подключения новых экстентов или файлов данных. Затраты могут быть большими, чем при нормальном выполнении приложения. По этим причинам мы бы рекомендовали пользоваться триггерами AFTER SUS- SUSPEND очень осторожно. Они подходят для восстановления длительных или итера- итеративных процессов, перед перезапуском которых необходимо вызывать DML-ин- струкции, отменяющие уже выполненные действия. Однако они неприемлемы для приложений оперативной обработки транзакций. Сопровождение триггеров Oracle поддерживает ряд DDL-инструкций, которые позволяют повысить эффек- эффективность управления триггерами. С их помощью можно включать, отключать и удалять триггеры, просматривать информацию о них, проверять их состояние. Отключение, включение и удаление триггеров Отключенный триггер не запускается в ответ на событие, с которым он связан. Синтаксис отключения триггера очень прост: ALTER TRIGGER иня_триггера DISABLE: Например, триггер emp_after_insert отключается таким образом: ALTER TRIGGER emp_after_insert DISABLE: Отключенный триггер можно включить повторно с помощью следующей ин- инструкции: ALTER TRIGGER majpnrrepa ENABLE: В инструкции ALTER TRIGGER задается только имя триггера; ни его тип, ни что- либо иное задавать не нужно. Эту инструкцию можно вызывать из хранимой про- процедуры, как в следующем примере, где включение и отключение всех триггеров таблицы выполняется в помощью динамического SQL: /* Файл в web: settrig.sp */ CREATE OR REPLACE PROCEDURE settrig (tab IN VARCHAR2. action IN VARCHAR2)
698 Глава IB • Триггеры IS v_act1on VARCHAR2 A0) :- UPPER (action); v_other_actiOrt VARCHAR2 A0) :- 'DISABLED'; BEGIN IF v action - 'DISABLE1 then' v_Qther action :- 'ENABLED'; END IF; FOR rec IN (SELECT trigger_name FROM user triggers WHERE tabli_owner - USER AND table_name - UPPER (tab) AND status - v_other_act1on) LOOP EXECUTE IMMEDIATE 'ALTER TRIGGER ' || rec.triggerjiame j| ' ' [| v_action; DBMSJXJTPUT.putJine ( 'Set status of ' || rec.trigger_name || ' to ' |j v_action ): END LOOP: END: Инструкция, выполняющая удаление триггера, так же проста: DROP TRIGGER нмя_триггера: Просмотр триггеров В словаре данных Oracle имеется несколько представлений, возвращающих раз- разнообразную информацию о триггерах: О DBAJRIGGERS - все триггеры базы данных; О ALLJRIGGERS - все триггеры, доступные текущему пользователю; О USERTRIGGFiRS — все триггеры, принадлежащие текущему пользователю. В табл. 18.4 перечислены столбцы представлений, содержимое которых наи- наиболее важно для пользователей и системы. Таблица 18Л. Содержимое столбцов представлений, возвращающих наиболее ценную информацию о триггерах Имя столбца Содержимое TRIGGER_NAME TRIGGERJTYPE TRIGGERING EVENT Имя триггера Тип триггера: для триггеров DML — BEFORE STATEMENT, BEFORE EACH ROW, AFTER EACH ROW или AFTER STATEMENT; для триггеров DDL — BEFORE EVENT или AFTER EVENT; для триггеров INSTEAD OF — INSTEAD OF; для триггеров AFTER.SUSPEND — AFTER EVENT Событие, вызвавшее запуск триггера: для триггеров DML — UPDATE, INSERT или DELETE; для триггеров DDL — операция DDL (полный список событий приведен выше, в разделе, посвященном триггерам данного типа); для триггеров событий базы данных — ERROR, LOGON, LOGOFF, STARTUP или SHUTDOWN; для триггеров INSTEAD OF — INSERT, UPDATE ИЛИ DELETE; для триггеров AFTER SUSPEND — SUSPEND
Сопровождение триггеров 699 Имя столбца Содержимое TABLE_OWNER Различная информация в зависимости от типа триггера: для триггеров OML — имя владельца таблицы, с которой связан данный триггер; для триггеров DDL, при условии, что это триггеры уровня базы данных, — SYS, в противном случае — имя владельца триггера; для триггеров событий базы данных, при условии, что это триггеры уровня базы данных, — 5YS, в противном случае — имя владельца триггера; для триггеров INSTEAD OF — имя владельца представления, с которым связан данный триггер; для триггеров AFTER 5USPEND, при условии, что это триггеры уровня базы данных, — SYS, в противном случае — имя владельца триггера BASE_OBJECT_TYPE Тип объекта, с которым связан триггер: для триггеров DML — TABLE; для триггеров DDL— SCHEMA ИЛИ DATABASE; для триггеров событий базы данных — SCHEMA ИЛИ DATABASE; для триггеров INSTEAD OF — VIEW; для триггеров AFTER SUSPEND — SCHEMA ИЛИ DATABASE TABLEJWME Для триггеров DML — имя таблицы, с которой связан триггер; для остальных типов триггеров — значение NULL REFERENCING_NAMES Для триггеров DML (уровня стропи) — предложение, используемое для определения псевдонимов записей OLD и NEW; для остальных типов триггеров — текст «REFERENCING NEW AS NEW OLD AS OLD» WHEN_CLAUSE Для триггеров DML — предложение с условием выполнения триггера STATUS Состояние триггера (ENABLED или DISABLED) ACTION_TYPE Определение того, выполняет триггер вызов (CALL) или содержит код PL/SQL (PL/SQL) TRIGGER.BODY Текст тела триггера (столбец типа LONG) Проверка состояния триггера VALID Как это ни странно, но ни одно из указанных выше представлений не содержит информации о том, находится ли триггер в работоспособном состоянии. Если триг- триггер создан с помощью ошибочной инструкции PL/SQL, он сохраняется в базе дан- данных, но помечается как неработоспособный (INVALID). Для того чтобы определить, находится ли триггер в этом состоянии, нужно извлечь информацию из предстаз- ления USERJ3BJECTS или ALL_OBJECTS, как показано в следующем примере: SQL> CREATE OR REPLACE TRIGGER Invalid trigger 2 AFTER DDL ON SCHEMA 3 8EGIN 4 NULL 5 END; 6 / Warning: Trigger created with compilation errors. SQL> SELECT objectjiame. 2 object_type. 3 status 4 FROM jser objects 5 WHERE objectjiame - 'INVALID_TRIGGER'; OBJECT_NAHE OBJECTJYPE STATUS INVALID STATE TRIGGER INVALID
19 Управление приложениями PL/SQL > Управление программным кодом и его анализ в базе данных > Защита кода хранимой программы > Встроенная компиляция > Тестирование программ PL/SQL > Отладка программ PL/SQL > Оптимизация программ РЦ/SQL > Повышение производительности приложений Написание программного кода — это лишь одна из составляющих длительного процесса разработки и сопровождения приложений. Осветить данную тему пол- полностью в одной главе невозможно, мы только предлагаем вашему вниманию ряд идей и советов по следующим вопросам. О Управление программным кодом и его анализ в базе данных. При компиля- компиляции программ PL/SQL исходный код загружается в словарь базы данных в раз- разных формах (в виде текста программы, зависимостей, информации о парамет- параметрах и т. п.). С помощью SQL из этого словаря можно запрашивать информа- информацию, необходимую для управления программным кодом. О Защита кода хранимой программы. Oracle предлагает такой способ хранения исходного кода, при котором конфиденциальная информация скрыта от по- посторонних глаз. Указанный способ полезен для производителей, разрабаты- разрабатывающих и реализующих приложения на основе хранимого кода PL/SQL. О Использование встроенной компиляции. Начиная с Огас1е9г исходный код PL/SQJL можно скомпилировать во встроенный объектный код, компонуемый в Oracle. Встроенная компиляция позволяет значительно повысить произво- производительность приложения. (Ее влияние особенно заметно в программах, вы- выполняющих большой объем вычислений, однако на производительности SQL не отражается.) О Тестирование программ PL/SQ.L. Ниже будут рассмотрены способы тестиро- тестирования программ PL/SQL с помощью утилиты тестирования исходного кода utPLSQL.
Управление программным кодом и его анализ в базе данных 701 О Отладка программ PL/SQ.L. В состав многих средств разработки сейчас вхо- входи графические отладчики на основе API. Это очень мощные инструменты, но все же они охватывают лишь малую часть всего процесса отладки. В данной главе рассматриваются некоторые технологии и подходы, которые помогут вам выработать собственные концепции отладки, эффективно спланировать и вы- выполнить этот процесс. О Настройка программ PL/SQL. Подробное описание процесса настройки кода PL/SQL невозможно уместить в одну главу. Однако мы приведем ряд наибо- наиболее важных советов и рекомендаций по анализу процесса выполнения про- программ с помощью встроенных утилит трассировки. Управление программным кодом и его анализ в базе данных При выполнении SQL-инструкции CREATE OR REPLACE исходный код программы и другие ее представления сохраняются а базе данных Это дает разработчикам следующие преимущества. О Информацию о программном коде можно получить с помощью запросов SQL. Сформировав запрос к представлению словаря данных, вы получите список программ, модифицированных в определенный день, или узнаете, какие из программ помечены как неработоспособные и нуждаются в перекомпиляции. О База данных управляет зависимостями между хранимыми объектами. Если, предположим, структура таблицы, с которой работает хранимая функция, из- изменилась, состояние функции автоматически устанавливается в INVALID. В слу- случае вызова данной функции она будет автоматически перекомпилирована. Таким образом, с помощью SQL программист может работать с хранящимся программным кодом, выполнять его анализ, документировать изменения и до- дополнения и т. я. В следующем разделе рассказывается об источниках информа- информации, находящейся в словаре данных. Представления словаря данных для программистов PL/SQL Словарь данных Oracle — это настоящие джунгли! В нем сотни представлений, основанных на сотнях таблиц, множество сложных взаимосвязей, специальных кодов и слишком много неоптимизированных определений представлений. В це- целом можно выделить три типа, или уровня, представлений словаря данных: О USER_* — представления, которые содержат информацию об объектах базы дан- данных, принадлежащих текущей схеме; О ALL* — представления, включающие сведения об объектах базы данных, дос- доступных в текущей схеме (либо принадлежащих ей, либо доступных благодаря соответствующим привилегиям); О DBA_* — представления с информацией обо всех объектах базы данных.
702 Глава 19 • Управление приложениями Поскольку хранимые объекты содержатся в таблицах словаря данных, для по- получения информации о доступных программах можно использовать SQL. С его помощью запрашивается информация из таких представлений: О USER_DEPENOENCIES - все зависимости объектов, принадлежащих текущей схеме; О USER_?RRORS — текущий набор ошибок для всех принадлежащих схеме объек- объектов; доступ к этому представлению осуществляется посредством команды SHOW ERRORS приложения SQL*Plus, описанной в главе 2; О USER_OBJECTS - принадлежащие текущей схеме объекты; О USERJ)8J?CT_SIZE — размер принадлежащих текущей схеме объектов; О USER_SOURCE — исходный код всех принадлежащих текущей схеме объектов в текстовом виде; О USER_TRIGGERS - принадлежащие текущей схеме триггеры базы данных; О USERARGUMENTS - аргументы (параметры) всех принадлежащих текущей схеме процедур и функции. Для просмотра структуры любого из этих представлений можно или активи- активизировать команду DESC (describe - описать) в SQL*Plus, или обратиться к доку- документации Oracle. Примеры использования перечисленных представлений приве- приведены в следующем разделе. Вывод информации о хранимых объектах В представлении USER_OBJECTS возвращается следующая ключевая информа- информация об объекте: О 0BJECTJ1AME - имя объекта; О OBJECTJYPE - тип объекта; О STATUS - состояние объекта, VALID или INVALID. Вот некоторые типы объектов (интересующие разработчиков PL/SQL в наи- наибольшей степени), которые доступны посредством этого представления: SQL> SELECT distinct objecttype FROM userobjects; OBJECTJYPE FUNCTION INDEX JAVA CLASS JAVA SOURCE LOB PACKAGE PACKAGE BODY PROCEDURE SEQUENCE SYNONYM TABLE TRIGGER TYPE
вправление программным кодом и его анализ в оазе данных /03 TYPE BOOY VIEW Используя представление USER_OBJECTS, вы можете получить полный список объектов PL/SQL, имеющихся в базе данных. Для этого создан следующий сце- сценарий SQL*Plus (вы найдете его на узле O'Reilly в файле psobj.sql): /* Файл 8 web: psobj.sql */ SET PAGESIZE 66 COLUMN object_type FORMAT A20 COLUMN objectjiame FORMAT A30 COLUMN status FORMAT A10 BREAK ON object_type SKIP 1 SPOOL psobj.lis SELECT objecttype, objectjiame, status FROM user_objects WHERE Object_type IN ( 1 PACKAGE', 'PACKAGE BODY'. 'FUNCTION'. 'PROCEDURE'. 'TYPE'. 'TYPE BODY') ORDER BY objectjype. status, object_name: / SPOOL OFF В результате выполнения этого сценария выводится приведенный ниже спи- список объектов: OBJECT TYPE OBJECT NAME STATUS FUNCTION PACKAGE PACKAGE BODY PROCEDURE TYPE DEVELOP ANALYSIS NUMBER OF ATOMICS FREQJNSTR CONFIG PKG EXCHDLR_PKG EXCHDLRJ>KG ASSESS POPULARITY ASSERTCONDITION DESSERT T INVALID INVALID VALID VALID VALID VALID INVALID VALID VALID TYPE BODY DESSERTJ" VALID Обратите внимание, что значительная часть модулей помечена как INVALID. Причиной этого могут быть изменения в таблицах, на которые ссылается модуль, или изменения в программах, которые он вызывает. СУБД автоматически пере- перекомпилирует эти объекты, когда программа пытается к ним обратиться. Иными словами, перекомпиляция происходит во время выполнения, так что пользова- пользователь запускает программу и ждет, пока не будут перекомпилированы все необхо- необходимые для ее работы объекты. Чтобы избежать задержки, связанной с автомати- автоматической перекомпиляцией, можно вручную откомпилировать недействительные модули. С этой целью лучше всего воспользоваться утилитой для перекомпиля- перекомпиляции, разработанной Соломоном Якобсоном, которую вы найдете в файле recom- pile.sq| на узле O'Reilly.
7O4 Глава IS • Управление приложениями Вывод и поиск исходного кода Исходный код программ следует всегда хранить в текстовых файлах. Вы можете также пользоваться специальными средствами, в функции которых входит управ- управление исходным кодом и хранение его вне базы данных. Однако у программ, на- находящихся в базе данных, есть важное преимущество: их исходный код можно анализировать с помощью SQL-запросов. Представление USER_SOURCE возвращает исходный код всех объектов, принадле- принадлежащих текущему пользователю. Его структура такова: Name Null? Type NAME NOT NULL VARCHAR2C30) TYPE VARCHAR2A2> LINE NOT NULL NUMBER TEXT VARCHAR2B000) Здесь NAME - это имя объекта; TYPE - его тип (PROCEDURE, FUNCTION, PACKAGE, PACKAGE BODY); LINE — номер строки, a TEXT — текст исходного кода. Для разработчиков представление USER_SOURCE является очень ценным ресур- ресурсом. Вот что оно позволяет сделать посредством соответствующих запросов: О вывести строку исходного кода с заданным номером; О проверить стандарт кодирования; О идентифицировать возможные ошибки и слабые места исходного кода. Предположим, что мы установили правило, согласно которому отдельные раз- разработчики никогда не должны жестко кодировать пользовательские номера оши- ошибок, лежащие в диапазоне от -20 999 до -20 000 (поскольку такое кодирование может привести к возникновению конфликтов). Конечно, мы не сможем воспре- воспрепятствовать написанию разработчиком такой строки: RAISE_APPLICAT10N_ERR0R (-20306. 'Balance too low'): но зато способны создать пакет, идентифицирующий программы с подобными фрагментами кода. Он очень прост, а его главная процедура имеет вид: /* Файл в web: valstd.pkg */ CREATE OR REPLACE PACKAGE BODY valstd IS CURSOR objwith cur (str IN VARCHAR2) IS SELECT name, text FROM USERJOURCE WHERE UPPER (text) LIKE T || UPPER (str) j| T AND name !- 'VALSTD'; PROCEDURE prcjgwith (str IN VARCHAR2) IS BEGIN FOR prog_rec IN abjwith_cur (str) LOOP DBMS_OUTPUT.PUT_LINE (prog_rec.name || ': ' || prog_rec.text); END LOOP; END;
вправление программным кодом и его анализ в оазе данных /МЬ PROCEDURE pw_rae IS prog_rec objwith_curJ!RO«TYPE; BEGIN progwith ('RAISE_APPLICATION_ERROR'); END: END valstd: После компиляции этого пакета в схеме можно проверить наличие в програм- программах значений -20 NNN: SQL> EXEC valstd.progwith C-20'): CHECKJALANCE - RAISE APPLICATIONJRROR (-20306. 'Balance too low1); MY_SESSIOM - PRAGMA EXCEPTI0N_INITfdb11nk_not_open.-Z081); VSESSTAT - CREATE DATE : 1999-07-20 Обратите внимание, что вывод программой третьей строки не противоречит правилам; она возвращена только потому, что условие отбора является недоста- недостаточно жестким. Еще одним примером применения данного пакета служит поиск всех объявле- объявлений с использованием типа данных фиксированной длины CHAR: SQL> EXEC valstd.progwith CCHAR'); Конечно, данное аналитическое средство является достаточно примитивным, но ничто не мешает его усложнить. Можно, к примеру, разместить в интранете HTML-документ с информацией об исправлениях, которые нужно внести в про- программный код, и автоматически вызывать его каждое воскресенье с помощью па- пакета DSMS_JOB, с тем чтобы в понедельник утром администратор проверял наличие в сети интранет обратной связи. Следующая процедура идентифицирует все независимые процедуры и функ- функции, не имеющие раздела исключений (к сожалению, для пакетных программ она не подходит). Как и предыдущая, она хороша прежде всего своей простотой, хотя и не отличается интеллектуальностью (в частности, проверяет сама себя). /* Файл s web: shownoexc.sp */ CREATE OR REPLACE PROCEDURE show_no exc_sections IS ~ ' • CURSOR check for_exc <nm IN VARCHAR2) IS SELECT lire FROM user_source WHERE NAME - ran AND INSTR (UPPER (text). 'EXCEPTION') > 0: check_rec check_for_excS!ROWTYPE; BEGIN FOR objrec IN (SELECT objectjiame, objectjype FROM user_objects WHERE object_type IN ('PROCEDURE'. 'FUNCTION')) LOOP OPEN chEck_for_exc [obj_rec.object_name): FETCH check_for_exc INTO check_rec; IF cfieclc_for_excSFOUND THEN NULL;
706 ¦ Глава 19 • Управление приложениями PL/SQL ELSE DBMSJXJTPUT.putJHne { obj_rec.object_type II ' ' || obj_rec.object_name || ' does not contain the EXCEPTION keyword.' ): END IF; CLOSE check_for_exc: END LOOP: END: Защита кода хранимой программы Почти все приложения содержат закрытую информацию. Например, разработав приложение PL/SQL для продажи, программисты обычно не хотят, чтобы поль- пользователи (или конкуренты) получили доступ к их коду. Oracle предоставляет спе- специальную программу под названием wrap (что в переводе с английского означает •вскрывать»), предназначенную для сокрытия большинства или даже всех секретов приложения. ПРИМЕЧАНИЕ Некоторые разработчики считают, что программа wrap «шифрует» код, но это неверно. Если вам нужно передать, действительно секретную информацию, например пароль, полагаться на эту про- программу не стоит. Однако следует иметь в виду, что с помощью встроенного пакета DBMS_OBFUS- CATION_TOOLXn" Oracle позволяет интегрировать в приложение защиту DES (Data Encryption Standard — стандарт шифрования данных). Более подробные сведения по этому вопросу вы найдете в документации Orade. Когда вы скрываете исходный код PL/SQL с помощью программы wrap, чита- читабельный ASCII-текст преобразуется в нечитабельный. Результирующий код мо- может передаваться пользователям, п региональные офисы и т. д. для создания со- соответствующих программ в других экземплярах базы данных. Он характеризуется такой же переносимостью, что и исходный код PL/SQL, и может импортировать- импортироваться и экспортироваться. База данных Oracle воспринимает программы со скрытым кодом точно так же, как и программы, текст которых является читабельным. Ины- Иными словами, скрытые программы в базе данных с функциональной точки зрения ничем не отличаются от обычных программ PL/SQL; единственное различие за- заключается в том, что их нельзя прочитать, запросив через представление USER_ SOURCE. Как скрыть исходный код Для того чтобы скрыть исходный код PL/SQL, нужно запустить выполняемый файл wrap.exe из каталога bin экземпляра Oracle. Формат команды wrap таков: wrap 1паше=исх_файл [<х\ате-рез_файл~}
Защита кода хранимой программы 707 Здесь исх_фдйл — это исходная, читабельная версия программы, а рез_файл - ре- результирующий файл, куда будет записана скрытая версия кода. Если исходный файл задан без расширения, по умолчанию принимается расширение .sql. ПРИМЕЧАНИЕ До появления OradeSI выполняемые файлы программы wrap назывались wrapNN.exe, где NN — но- номер версии Orade (например, wrap72.exe или wrap73.exe). Если аргумент oname не задан, программа wrap создает файл с именем, которое совпадает с именем исходного файла, и расширением .plb, назначаемым по умол- умолчанию. Расширение расшифровывается как «PL/SQL binary» (это название яв- является не совсем точным, однако оно отражает суть процесса и означает, что дво- двоичные файлы тоже нечитабельны). Программа wrap, в частности, применяется, если нужно сделать следующее: О скрыть программу, используя установки по умолчанию: C:\oracle\ora90\bin\wrap iname-secretprog О скрыть тело пакета, явно задав расширение исходного файла и имя результи- результирующего; обратите внимание, что скрытый файл не обязательно должен иметь то же имя или расширение, что и исходный: c:\oracle\ora90\bin\wrap 1пате-5есге1Ьос!у.5рЬ onaire-5hhhhh.bin Работа со скрытым кодом Ниже приведено несколько советов по работе со скрытым кодом. О Создавайте командные файлы для быстрого и унифицированного сокрытия содержимого одного или более файлов. В Windows NT в каталоге своих ис- исходных файлов вы можете создать файл с расширением .bat и включить в него следующую строку: c:\orant\b1n\wrap Iriame-plvrep.sps oname»plvrep.pls Конечно, можно также создать командный файл с параметрами, и передавать ему имена скрываемых файлов. О Для того чтобы скрыть исходный код, нужно поместить его в файл операцион- операционной системы. Если же вы работаете в специализированной среде разработки PL/SQL, оперирующей кодом, который хранится прямо в базе данных, при- придется сохранить исходный код в файле, скрыть его, а затем откомпилировать и поместить обратно в базу данных, в результате чего он станет нечитабельным. При передаче кода пользователям это допустимо, но для разработки и сопро- сопровождения приложений совершенно не годится. О Скрывать можно только спецификации пакетов, тела пакетов, независимые функции и процедуры. Если программе wrap передать файл, содержащий дру- другой код SQL или PL/SQL, этот файл изменен не будет. О Для того чтобы узнать, скрыта ли программа, достаточно посмотреть на ее за- заголовок. Если программа скрыта, он будет содержать ключевое слово WRAPPED: PACKAGE BODY имя пакете WRAPPED
708 Глава 19 • Управление приложениями PL/SQL Впрочем, даже если вы не заметите указанного слова в заголовке, то, посмот- посмотрев ниже, поймете, что исходный текст скрыт, поскольку он будет выглядеть примерно так: LINE TEXT 45 abed 46 95a4Z5ff 47 aZ 48 7 PACKAGE: Каким бы плохим ни был ваш стиль программирования, все же он наверняка не настолько ужасен! О Скрытый код имеет намного больший объем, чем исходный. Так, пакет разме- размером 57 Кбайт после сокрытия занял 153 Кбайт, а пакет размером 86 Кбайт - занял 357 Кбайт. Таким образом, для хранения скрытого кода в базе данных требуется больше места. Размер откомпилированного кода остается неизмен- неизменным, хотя время компиляции также может увеличиться. •строенная компиляция В версиях, предшествовавших Огас1е9г, после компиляции исходного кода всегда создавалось представление (обычно называемое байт-кодом), которое сохраня- сохранялось в базе данных и интерпретировалось во время выполнения программы вир- виртуальной машиной, входящей в состав Oracle. Выполняемый код, обрабатывае- обрабатываемый этой виртуальной машиной, соответствовал платформе, где он выполнялся. В Oracle9t наряду с существующим предусмотрен новый подход к компиляции программ. Теперь исходный код можно, при желании, откомпилировать во встро- встроенный объектный код, а затем скомпоновать его с Oracle. Этот процесс называет- называется встроенной компиляцией. (Однако учтите, что к анонимным блокам PL/SQL встроенная компиляция неприменима.) Как выполнять встроенную компиляцию? Что хорошего в данной возможно- возможности? В каких случаях ею пользоваться? Ответы на все эти вопросы вы найдете в настоящем разделе. PL/SQL часто применяется в качестве оболочки при выполнении SQL-инст- SQL-инструкций, установке переменных привязки и обработки результирующего набора строк. Для программ, осуществляющих операции подобного типа, скорость вы- выполнения кода PL/SQL обычно не очень важна, поскольку их производитель- производительность определяется скоростью выполнения SQL-инструкций. (Некоторое замед- замедление способна вызвать неэффективная организация переключения контекста, но это можно исправить с помощью оператора FORALL и предложения BULK COLLECT, рассматриваемых в главе 13.) Существует, однако, множество других приложений и программ, выполняю- выполняющих большой объем вычислений с помощью PL/SQL независимо от базы дан- данных. Ведь PL/SQL — это еще и полнофункциональный процедурный язык. Пред- Предположим, что мы написали программу для поиска всех прямоугольных треуголь- треугольников с целочисленными значением длины сторон. Причем требуется отобрать только уникальные треугольники, то есть такие, стороны которых не кратны сто- сторонам одного из уже найденных треугольников.
Встроенная компиляция 709 Наша программа выполняет поиск всех возможных комбинаций длины двух катетов треугольников в диапазоне от единицы до заданного максимального зна- значения. При этом отбираются лишь те треугольники, у которых квадратный ко- корень суммы квадратов катетов отличается от целого числа не более чем на 0,01. Треугольники, для которых данное условие выполняется, проверяются затем по теореме Пифагора с использованием целочисленной арифметики. После этого найденный треугольник сравнивается с полученным ранее. Каждый новый тре- треугольник сохраняется в таблице PL/SQL, а полный набор кратных ему треуголь- треугольников (включая треугольники с максимальными параметрами) сохраняется в от- отдельной таблице PL/SQL, что позволяет ускорить проверку на уникальность. Для реализации этого алгоритма требуются следующие пограммные компонен- компоненты: два цикла (один из них должен быть вложен в другой), в теле которых выпол- выполняется несколько арифметических операций, отбор и сравнение; процедуры, осу- осуществляющие сравнение путем перебора значений таблицы PL/SQL (с дополни- дополнительными арифметическими операциями), и дополнение таблиц PL/SQL в случае необходимости. Так как же встроенная компиляция отразится на функционировании подобно- подобного кода? Мы измерили время выполнения двух версий данной программы (интер- (интерпретируемой и откомпилированной встроенным компилятором) для ртах - 5000, то есть для 12,5 млн. повторений тела внутреннего цикла. Получилось соответст- соответственно 548 и 366 с (на компьютере Sun Ultera60, без учета нагрузки программы- тестировщика). Таким образом, скорость выполнения программы, откомпилиро- откомпилированной встроенным компилятором, на 33 % выше. Совсем неплохо, если учесть, что речь идет о полупрозрачном усовершенство- усовершенствовании, использование которого не требует изменения программного кода! При этом важно, что, ускоряя работу одних программ (производящих большой объем вычислений), встроенная компиляция не замедляет выполнения других. Вы спра- спрашиваете, как же ее реализовать? Об этом мы расскажем дальше. Настройка, выполняемая администратором базы данных Встроенная компиляция PL/SQL заключается в преобразовании исходного кода PL/SQL в исходный С-код с последующей компиляцией для той платформы, на которой она выполняется. Компиляция и компоновка исходного С-кода произво- производятся с помощью программ сторонних производителей, местоположение которых задается администратором базы данных, обычно в файле JNIT.ORA, содержащем инициализационные параметры. Объектный код каждого откомпилированного таким образом библиотечного модуля PL/SQL сохраняется в каталоге файловой системы текущей платформы, также указанном администратором базы данных. Естественно, что встроенная компиляция выполняется дольше, чем компиляция в интерпретируемую форму (по нашему опыту, примерно вдвое дольше). Она требует ряда дополнительных действий: генерации С-кода на основе выходных данных компилятора PL/SQL, его сохранения, вызова компилятора С и выпол- выполнения им компиляции, компоновки результирующего объектного кода в Oracle. Корпорация Oracle рекомендует настроить С-компилятор таким образом, чтобы он не выполнял никакой оптимизации. Наши тесты показали, что оптимизация
710 Глава 19 • Управление приложениями PL/SQL С-кода незначительно повышает скорость его выполнения, но значительно за- замедляет компиляцию. Сравнение интерпретируемого и компилируемого режимов Режим компиляции программ PL/SQL определяется значением параметра PLSOL_ COMPILER_FLAGS, которое задается следующим обрплам: ALTER SESSION SET plsql compiler_flags = ¦NATIVE' 7* или 'INTERPRETED' */: Установленный режим компиляции действует для всех библиотек PL/SQL, компилируемых в течение сеанса. Этот режим сохраняется вместе с метаданны- метаданными библиотечного модуля, так что если программа в дальнейшем автоматически перекомпилируется в результате проверки зависимостей, будет использоваться первоначально заданный режим. Для того чтобы узнать, в каком режиме компилируется данная программа, нужно выполнить следующий запрос к словарю данных: SELECT o.object_name NAME, 5.param_value compjnode FROM USERJTOREDJETTINGS s, USER_OBJECTS a WHERE o.objectjd - s.objectjd AND paramjiame - 'pisql_compilGr_flags' AND o.object_type IN ('PACKAGE'."'PROCEDURE', 'FUNCTION'); Важно помнить следующее: если для перекомпиляции всех неработоспособ- неработоспособных программных модулей схемы обратиться к встроенному пакету DBMSJJTILI- TY.COMPILE_SCHEMA, он будет использовать текущее значение параметра PLSQL_COM- PILER_FLAGS, а не режим компиляции, заданный для каждого программного моду- модуля. Oracle рекомендует вызывать все библиотечные модули PL/SQL из одного модуля верхнего уровня (см. врезку), что позволит избежать переключения кон- контекста, который происходит, когда библиотечный модуль, откомпилированный в одном режиме, вызывает модуль, откомпилированный в другом, Эта рекомен- рекомендация касается и библиотечных модулей, предоставленных Oracle. Они всегда поставляются откомпилированными в интерпретируемом режиме. ПЕРЕВОД ВСЕЙ БАЗЫ ДАННЫХ В РЕЖИМ ВСТРОЕННОЙ КОМПИЛЯЦИИ Простейший способ выполнить рекомендации Oracle относительно ком- компиляции в одном режиме всех библиотечных модулей, вызываемых из од- одного модуля верхнего уровня, — это компилировать все эти модули базы данных в режиме NATIVE. В составе будущих версий базы данных (тех, что выйдут после Oracle9i Release 2) будут поставляться сценарий, выпол- выполняющий эту задачу, а также сценарий, переводящий всю базу данных в ре- режим INTERPRETED. Пока же при переводе базы данных в режим NATIVE можно руководствоваться инструкциями, опубликованными на узле Oracle Tech- Technology Network {http://otn.oracle.com).
Тестирование программ PL/SQL 711 Какой же вывод можно сделать из сказанного? Если приложение выполняет значительное количество вычислений, стоит всерьез задуматься о переходе к ре- режиму встроенной компиляции. Тестирование программ PL/SQL Каждый программист согласится с тем, что заниматься созданием программы или искать пути реализации какой-либо новой идеи всегда намного приятнее, чем «доводить до ума» уже готовый проект. Никому не хочется тратить время на тес- тестирование программного обеспечения, не хочется писать к нему документацию. В большинстве случае разработчики решают, что если пользователи не нашли ошибок сразу, значит, их нет, и не выполняют достаточного количества тестов. С чем же все это связано? О Ориентирование лишь на успех. Мы настолько убеждены в том, что наш код будет работать правильно, что предпочитаем даже не задумываться о возмож- возможности появления плохих новостей. Мы проводим некоторое поверхностное тестирование, убеждаемся, что все работает, и предоставляем возможность дру- другим искать ошибки, если они вообще есть. О Сроки. Многие рассуждают так: сейчас время Интернета! Рынок есть рынок. Все должно быть готово на завтра, так что воспользуемся стратегией компа- компаний Microsoft и Netscape и станем выпускать «бета-версии» программного обес- обеспечения как готовые продукты, а пользователи пусть их тестируют. О Некомпетентность руководства. Как правило, руководители информацион- информационных отделов ничего не смыслят в разработке программного обеспечения, не понимают сути этого процесса. И если у нас нет времени на создание полно- полноценного проекта (включая проектирование, написание программного кода, тес- тестирование, документирование и усовершенствование), результатом работы ста- станет создание полной ошибок неработоспособной программы. О Нежелание тратить время на тестирование. Создание тестовых программ обыч- обычно считается пустой тратой времени, ведь всегда найдется более важная рабо- работа. Одним из следствий подобного похода является то, что значительная часть обязанностей по тестированию возлагается на отдел контроля качества (если таковой имеется в компании). Привлечение к работе профессионалов в облас- области контроля качества может оказать огромное влияние на результат, однако и разработчики не должны снимать с себя функцию тестирования собственно- собственного кода. Таким образом, программный код всегда нуждается в дополнительном тести- тестировании. Автор данной книги также в свое время потратил не мало сил на усовер- усовершенствование процесса тестирования программ. Он изучил схемы тестирования, предложенные другими программистами, работающими с объектно-ориентиро- объектно-ориентированными языками. Затем создал собственную схему для тестирования программ PL/SQL и реализовал ее в виде утилиты utPLSQL. Это проект с открытым ко- кодом, которым сейчас пользуются разработчики по всему миру.
712 Глава 19 • Управление приложениями P17SQL В следующих разделах мы рассмотрим общепринятые способы тестирования программного кода. После этого расскажем об утилите utPLSQL, о том, как с ией работать и где найти требуемое программное обеспечение. Неэффективные технологии тестирования Предположим, требуется создать приложение, обрабатывающее большое количе- количество строк. В PL/SQL имеется функция SUBSTR, которая возвращает заданную часть строки. Однако ее использование связано с рядом проблем. Дело в том, что эту функцию удобно применять, когда вы знаете, где начинается подстрока и ка- какой она длины. Однако очень часто известно лишь местоположение начального и конечного символов, а длину подстроки нужно вычислять: mystring :- SUBSTR (full_string. S. 17): -- начало и конец? Ой, нет... tuystring :- SUBSTR (full_str1ng. 5. 12): -- конец - начало? mystring :- SUBSTR {fuTI_string, 5, 13): -- конец - начало + 1? mystring :« SUBSTR (full_string, 5. 11); -- конец - начало - 1? Почему мы должны помнить, по какой формуле вычисляется длина подстро- подстроки? В конце концов мы снова возвращаемся к старому проверенному способу: бе- берем лист бумаги, пишем «abcdefgh», ставим пометку над «с», затем еще одну над «g» и считаем на пальцах, после чего вспоминаем, что формула имеет такой вид: ¦«конец - начало +1». Не проще ли создать такую функцию, которая производила бы подобные вы- вычисления вместо нас? CREATE OR REPLACE FUNCTION betwnStr ( stringjn IN VARCHAR2. startjn IN INTEGER, endjn IN INTEGER) RETURN VARCHAR2 IS BEGIN RETURN ( SUBSTR ( string jn. startjn. endjn - startjn + 1 END; Итак, мы создали функцию betwnstr. Теперь ее следует протестировать, для чего пишем тестовый сценарий: BEGIN DBMS_OUTPUT.PUT_LINE (betwnstr ('abcdefgh1. 3. 5)): END: Запускаем данный сценарий (предварительно сохранив его в файле betwnstr.tst) и убеждаемся, что все работает корректно: SQL> ftetwnstr.tst cde
Тестирование программ PL/SQL 713 Все? Тестирование завершено? Отнюдь. Существует еще множество условий, которые нужно проверить, чтобы с уверенностью сказать, что функция работает правильно. Например, выясним, будет ли функционировать наш сценарий, если конечную позицию подстроки задать за пределами строки. С этой целью откры- открываем сценарий, вносим соответствующие изменения: BEGIN DBMS_OUTPUT.PUT_LINE (betwnstr Cabcdefgh'. 5. 10)): END: и снова выполняем: SQL> gbetwnstr.tst efgh Вроде бы, опять все в порядке. Подумав еще немного, можно добавить ряд других условий (см. приведенную ниже таблицу). Начало Конец NULL NOT NULL NOT NULL NULL NULL NULL 3 (положительное число) 1 (меньшее положительное число) 3 (положительное число) 100 (больше длины строки) Для того чтобы проверить все эти условия, нужно снова обратиться к файлу betwnstr.tst и провести соответствующее количество тестов. И лишь после этого можно с чувством выполненного долга откинуться на спинку стула и немного от- отдохнуть. Может показаться, что тестирование программы проведено очень тщательно. Однако у этого подхода имеются серьезные недостатки. О Правильность результатов тестирования приходится проверять визуально. Но как определить, какой ответ является корректным - «cde» или, может быть, «efg»? Нам как разработчикам нужно вновь принимать самостоятельное ре- решение, на что тратится дополнительное время. О При выполнении очередного тестирования код предыдущей тестовой програм- программы пропадает, так как новые значения вводятся поверх старых. О Мы не отслеживаем, какие именно тесты проводятся, а по окончании, процеду- процедуры удаляем тестовый сценарий. Очевидно, что последствия подобного подхода к делу плачевны. Допустим, что в программу потребуется добавить дополнительные функции или что через некоторое время при ее использовании будет обнаружена какая-либо ошибка. В нашем примере подобной ошибкой является передача отрицательного началь- начального значения. В ответ на это функция SU8STR отсчитывает от конца строки jV по- позиций и возвращает заданное количество символов (слева направо). В частности, в следующем случае: BEGIN DBMS_OUTPUT.PUT_J.INE (SUBSTR Cabcdefgh1. -3 5)); END:
714 Глава 19 • Управление приложениями PL/SQL функция SUBSTR возвращает значение fgh. Если те же аргументы передать функ- функции betwnstr, она вернет очень похожий, но несколько подозрительный результат; SQL> BEGIN 2 D8MS_0UTPUT.PUT_LINE (SUBSTR ('abcdefgh'. -3. 5)); 3 OBMS_OUTPUT.PUT_LINE (betwnstr ('abcdefgh'. -3. 5)); 4 END: 5 / fgh fgh Но что за строка будет содержаться между позициями -3 и -5? Скорее всего, функция betwnstr должна принимать значение -3 как начальную позицию, а зна- значение -5 — как конечную, после чего возвращать def. Оставляем задачу измене- изменения кода данной функции читателю в качестве домашнего упражнения, посколь- поскольку к тестированию модулей это не относится. А пока давайте предположим, что функция betwnstr была модифицирована так, чтобы интерпретировать отрица- отрицательные значения как начальную и конечную позиции. Рассмотрим ситуацию с точки зрения тестирования. Для того чтобы убедить- убедиться, что код работает, нужно, повторив все предыдущие тесты, выполнить ряд до- дополнительных проверок, используя в качестве входных данных разные сочетания положительных и отрицательных чисел. Но что делать, если все выполненные ра- ранее тестовые программы не были сохранены? Мы просто запускали их, убежда- убеждались, что код работает, а затем заменяли новыми. Итак, нужно воссоздать тесты, которые были проведены ранее. Но, как всегда, оказывается, что на это совершенно не осталось времени. По плану программа должна быть сдана, как бы плохо она ни была протестирована. Подобные слова не раз приходилось слышать, не правда ли? Это явный признак того, что нужно остановиться и тщательно обдумать свои дальнейшие действия. Прежде всего, безусловно, в тестовом сценарии нужно проверить работу функ- функции с разными значениями входных параметров: SET SERVEROUTPUT ON FORMAT WRAPPED BEGIN DBMSJMPUT.PUT LINE (betwnstr Г abcdefgh', 3. 5)); DBMS OUTPUT.PUTJ.INE (betwnstr ('abcdefgh'. 0. 2)); DBMS~OUTPUT.PUT_LINE (betwnstr ('abcdefgh'. NULL. 5)); DBMS_OUTPUT.PUTJ_INE (betwnstr Сabcdefgh', 3. NULL)): DBMSJXJTPUT.PUTJ-INE (betwnstr ('abcdefgh1. 3, 100)); D8MS_OUTPUT.PUTJ-INE (betwnstr ('abcdefgh1. -3. -5)): DBMSJHJTPUT. PUTJ-INE (betwnstr С abcdefgh1. -3. 0)): END; Выполнение данного сценария (который в случае модификации программы без труда можно дополнить) приводит к следующим результатам: SQL» «jetwnstr.tst cde a be cdefgh def abedef
Тестирование программ PL/SQL 715 К сожалению, правильность этих результатов установить нелегко. Не кажется ли вам, что должен существовать более удобный способ тестирования кода, по- позволяющий выполнить исчерпывающий набор тестов без больших затрат време- времени? Для подобных случаев автором была разработана утилита utPLSQL, о кото- которой выше уже упоминалось. Чтобы протестировать программу betwnstr с помо- помощью этой утилиты, в SQL*Plus нужно выполнить такую команду: SQL> exec utplsql.test ('betwnstr') Если тестируемая программа содержит ошибки, на экран будет выведено со- сообщение: > FFFFFFF АА > F > F э F > FFFF > F > F > F > F FAILURE: А А А А А А А А АААААААА А А А А А А "aetwnstr" III I I I I I ; I III L L L L L L L L LLLLLLL U U и и и и и и и и и и J и и иии RRRRR R R R R R R RRRRRR R R R R R R R R ЕЕЕЕЕЕЕ Е • Е Е ЕЕЕЕ Е Е Е ЕЕЕЕЕЕЕ FAILURE - EQ "normal" Expected "cde" and got "с" FAILURE - EQ "zero start" Expected "abc" and got "a" SUCCESS - ISNULL "null start" Expected "" and got "" SUCCESS - ISNULL "big start small end" Expected " and got "" Но если после исправления ошибки тестирование пройдет успешно, появится другое сообщение: SOL> exec utplsqi.test Сbetwnstr') SSSS U U ССС ССС ЕЕЕЕЕЕЕ SSSS SSSS > s s и > s и > s и > SSSS U > S U > s и и с с с и с ее и с и с и с и с с с с с с С Е С Е Е ЕЕЕЕ Е С Е SSSS S S S S SSSS SSSS s s s s- S U U С С С С Е SSSS иии ССС ССС ЕЕЕЕЕЕЕ SSSS SSSS SUCCESS: "betwnstr" SUCCESS - EQ "normal" Expected "cde* and got "cde" SUCCESS - EQ "zero start" Expected "abc" and got "abc" SUCCESS - ISNULL "null start" Expected "" and got "" SUCCESS - ISNULL "big start small end" Expected "" and got "" Замечательно, правда? Утилита utPLSQL сообщает, успешно ли прошло тес- тестирование, и даже информирует о результатах каждого из тестов. Как же это де- делается? К сожалению, не все здесь происходит автоматически. Вам нужно создать тес- тестирующий пакет в соответствии с определенными правилами (касающимися,
716 Глава 19 • Управление приложениями PL/SQL прежде всего, именования процедур и спецификации). Например, спецификация тестирующего пакета для программы betwnstr выглядит так: CREATE OR REPLACE PACKAGE ut_betwnstr IS PROCEDURE ut_setup; PROCEDURE ut_teardown: -- Для каждой тестируемой програипы... PROCEDURE ut_betwnstrj END ut_betwnstr; Иными словами, создаются процедура инициализации теста ut_setup, запус- запускаемая до тестирования модуля, и процедура utjteardown, которая выполняется после тестирования. Сама тестирующая процедура называется utbetwnstr. Вот фрагмент ее хода: PROCEDURE utJETWNSTR IS BEGIN utAssert.eq ( 'normal'. BETUNSTRCabcdefg1. 3. 5), 'def ); utAssert.isnull ( 'null start', BETMNSTRCabcdefg1. NULL, 5). ): END ut_BETWNSTR: Процедура вызывает программы утилиты utPLSQL, проверяющие, соответст- соответствуют ли выходные данные программы betwnstr ожидаемым. Результаты тестиро- тестирования сохраняются в таблице базы данных. После успешного выполнения всех тестов утилита utPLSQL запрашивает их из таблицы и выводит на экран. Пол- Полный цикл выполнения упоминаемых выше тестовых программ с помощью утили- утилиты utPLSQL показан на рис. 19.1. Даже не вникая во все детали кода и архитектуры, вы наверняка заметите пре- преимущества этой технологии. О Для поведения тестов используется специальная программа utPLSQLtest. Она сообщает программисту о том, успешно ли выполнены разработанные им тес- тесты, тем самым избавляя его от необходимости самостоятельно проверять ре- результаты. О Для тестирования программы с помощью утилиты utPLSQL создается проце- процедура тестирования (в нашем случае - utbetwnstr). Таким образом, тесты для многократного выполнения «кэшируются» автоматически и после каждого из- изменения программного кода его тестирование можно быстро повторить. Мы уверены, что представленная программа тестирования будет полезна любо- 'азрабогчику.
Отладка программ PL/SQL 717 Ваш тестовый пакет Проверка результатов JL utPLSQLtast Отчет о результатах ut_Setup Х uLJesme utJJetwnStr _L >| AssertEQ I >j AssertNull I > | AssiWEqTable| Таблица результатов Проверка на наличие значений NULL Неверный идентификатор Верный идентификатор Начальное значение неверно Конечное значение неверно Уникальное имя Рис. 19.1. Последовательность обработки данных утилитой utPLSQL Где найти дополнительную информацию Дополнительные сведения об утилите utPLSQL вы получите, посетив домаш- домашнюю страницу проекта по адресу http://sourceforge.net/pfDjects/utplsql/. Загрузите это программное обеспечение и, пожалуйста, присылайте нам сооб- сообщения о возможных способах его усовершенствования (и, конечно, отчеты об ошиб- ошибках). Но даже если вы не станете пользоваться этой утилитой, надеемся, что она подскажет вам несколько идей относительно усовершенствования вашего собст- собственного процесса тестирования. Отладка программ PL/SQL Тестирование программы — это поиск ошибок в программном коде, а ее отлад- отладка — это определение и устранение причин, обусловивших их появление. Речь идет о двух совершенно разных процессах, и путать их не следует. Протестировав программу и выявив ошибки, разработчик должен их исправить. С этого момента и начинается отладка. Многие программисты считают отладку самой трудной частью программиро- программирования. Причины ее сложности заключаются в следующем. О Недостаточное понимание задачи, решаемой данной программой. Большин- Большинство разработчиков предпочитают заниматься лишь написанием программно- программного кода и не любят тратить время на то, чтобы разбираться в спецификации. Поэтому вероятность того, что созданная таким образом программа будет от- отвечать требованиям, ничтожно мала.
718 Глава 19 • Управление приложениями PL/SQL О Плохой стиль программирования. Трудные для чтения программы (то есть плохо документированные, содержащие слишком много комментариев, с не- неудачными именами переменных и т. п.), а также программы, неправильно раз- разделенные на модули, отлаживать гораздо сложнее, чем корректно спроектиро- спроектированный и структурированный код. О Большое количество ошибок в программе. Не имея необходимых навыков анализа и кодирования, можно написать программу, включающую множество ошибок. О Слабые навыки отладки. Существует множество разных подходов к выявле- выявлению причин появления ошибок. Некоторые из них трлько затрудняют работу, поэтому очень важным является выбор правильной технологии отладки. В следующих разделах рассматриваются методы отладки, которыми лучше не пользоваться, а также методы, способные значительно облегчить этот процесс и сделать его более эффективным. Неупорядоченная отладка Столкнувшись с ошибкой, вы начинаете бурную деятельность по поиску причи- причины ее возникновения. И хотя сам факт ошибки может свидетельствовать о том, что задача плохо проанализирована или что не найден наиболее удачный метод ее решения, вы и сейчас не склонны к анализу. Вместо этого вы помещаете в про- программу множество операторов MESSAGE (в Oracle Forms), SRW.MESSAGE (в Oracle Re- Reports) или DBMS_OUTPUT. PUT_LINE (в хранимых модулях) и надеетесь, что это поможет. Причем вы даже не сохраняете копию программы перед внесением изменения, поскольку считаете, что это займет слишком много времени. Вам, как всегда, не- некогда, поэтому вы уверены, что ответы на все вопросы появятся сами собой, по- после чего можно будет удалить отладочные операторы. В результате оказывается, что потрачена масса времени на просмотр ненуж- ненужной информации. Ведь вы проверяете все подряд, хотя большинство используе- используемых конструкций давно выверены и отлажены. Вы пропускаете обед, чтобы не отвлекаться от работы, поглощаете не одну чашку кофе, пытаясь максимально сконцентрировать свое внимание. Даже не имея ни малейшего понятия о причине возникновения проблемы, вы думаете, что если попробовать то-то и то-то, нужный результат таки будет получен. Вы вносите из- изменения, после чего тратите несколько минут на компиляцию, написание и вы- выполнение теста лишь для того, чтобы убедиться в бесплодности всех своих уси- усилий. В результате появляется новая проблема, поскольку вы не учли влияния внесенного изменения на другие части программы. Затем вы возвращаете программу в исходное состояние (и дай Бог, чтобы вам удалось сделать это аккуратно, больше ничего не испортив) и пытаетесь изме- изменить еще что-нибудь в тщетной надежде, что это, наконец, поможет. Но не тут-то было. Ваш сослуживец, заметив, что у вас уже дрожат руки, предлагает свою по- помощь. Однако вы не знаете, как описать проблему, поскольку так и не смогли вы- выяснить, что же здесь не так. Более того, смущены тем, что уже успели сделать (превратив программу в минное поле, сплошь усеянное операторами трассировки),
Отладка программ PL/SQL 719 и осознаете, что у вас даже кет исходной версии, которую можно было бы пока- показать коллеге. Поэтому вы в резкой форме отвергаете помощь лучшего в группе программиста, звоните домой и сообщайте, что к ужину сегодня не явитесь. Почему? Потому, что нужно устранить ошибку! Иррациональная отладка Вы сгенерировали отчет, а он оказался пустым. При этом вы уверены, что вашей вины здесь быть не может, хотя в течение последнего часа занимались тем, что вносили изменения в структуры данных и программный код, который их запра- запрашивает и форматирует. Позвонив в отдел внутренней поддержки, вы спрашиваете, нет ли проблем с сетью, хотя File Manager сообщает, что все сетевые диски доступны. Потом вы интересуетесь, не девалась ли куда-нибудь база данных, хотя только что успешно к ней подключались, отбираете десять минут времени у специалиста из отдела поддержки и, ничего от него не добившись, в раздражении вешаете трубку. «Они там ничего не понимают», — бормочете вы. Но как бы там ни было, ис- источник проблемы приходится искать самостоятельно. Так что вы 'углубляетесь в только что модифицироватшый код и проверяете каждую строчку, пока не най- найдете причину своих неприятностей. Следующие два часа вы вслух разговариваете с самим собой: «Посмотрю-ка сюда! Я вызвал хранимую процедуру внутри опе- оператора IF. Раньше я этого никогда не делал. Может быть, хранимые программы так вызывать нельзя?» Вы удаляете оператор IF, заменив его оператором GOTO, но это не решает проблему. Поиски продолжаются. «Код кажется правильным. Но он вызывает програм- программу, которую когда-то написал Джо». Джо уже давно уволился, так что, конечно же, виноват он: «Программа, наверное, уже не работает, тем более что мы обнови- обновили систему голосовой почты». Вы решаете протестировать программу Джо, кото- которая не обновлялась уже два года и не поддерживает голосовую почту. Однако эта программа, как ни странно, функционирует. Вы начинаете отчаиваться: «Может быть, этот отчет следует выполнять толь- только по выходным? А допускается ли помещать локальные модули в анонимные блоки? Вероятно, их можно использовать только в процедурах и функциях! Ка- Кажется, я слышал о подобной ошибке. Надо бы поискать обходные пути...» Теперь вы начинаете сердиться и понимаете, почему ваш восьмилетний сын лупит по монитору, когда не может выйти на последний уровень, играя в Return to Castle Wolfenstein. И уже собравшись домой, вдруг осознаете, что подключены к базе данных, предназначенной специально для разработки и не содержащей ни- никаких данных. Вы подключаетесь к другому экземпляру базы данных, запускаете отчет, и все оказывается в порядке. За исключением того, что теперь ваш отчет содержит оператор GOTO и массу других добавленных вами конструкций. Советы и стратегии отладки Эта глава не претендует на звание исчерпывающего руководства по отладке. Од- Однако приведенные здесь советы и приемы помогут вам упорядочить собственные подходы и повысить навыки.
720 Глава 19 • Управление приложениями PL/SQL Пользуйтесь отладчиком исходного кода Наиболее эффективным способом минимизации времени отладки кода является использование отладчика. Отладчик входит в состав практически любой интег- интегрированной среды разработки приложений PL/SQL (IDE). И если вы пользуе- пользуетесь TOAD или SQL Navigator от Quest, PL/SQL Developer от Allround Automa- Automations или SQL Station от Computer Associates (либо другими подобными средст- средствами с графическим пользовательским интерфейсом), то сможете устанавливать в программах точки останова посредством щелчков мышью, выполнять код в по- пошаговом режиме, просматривать значения переменных и т. д. Советы, приведенные в этом разделе, будут вам полезны только в том случае, если вы пользуетесь отладчиком с графическим пользовательским интерфейсом. Если же вы отлаживаете программы старым дедовским методом (вставляя в про- программный код множество вызовов процедуры DBMSJXJTPUT.PUTJ-INE), то зря трати- тратите время. Собирайте информацию Соберите как можно больше сведений о том, где, когда и при каких условиях про- произошла ошибка. Маловероятно, что столкнувшись с этой ошибкой впервые, вы получите о ней все необходимые данные. И помните, что многие проблемы, кото- которые на первый взгляд кажутся простыми, на самом деле могут потребовать тща- тщательного тестирования и анализа. Так что не бросайтесь сразу же модифициро- модифицировать код в уверенности, что вы понимаете, в чем здесь дело, а сначала выполните следующие действия. 1. Запустите программу еще раз, чтобы посмотреть, воспроизводится ли ошибка. Если ошибка не повторяется, вам вряд ли удастся понять, чем она была обу- обусловлена, и исправить ее. Так что постарайтесь определить условия, при кото- которых ошибка повторяется, и воспроизвести ее еще раз. 2. Ограничьте область тестирования и отладки программы — не исключено, что так вы значительно быстрее найдете ошибку. 3. Проанализируйте обстоятельства, при которых ошибка не возникает. Выявле- Выявление подобных ситуаций также помогает сузить область поиска и осознать при- причину ошибки. Итак, чем больше информации об ошибке собрано, тем легче найти и устра- устранить причину ее возникновения. Поэтому не жалейте времени на дополнитель- дополнительное тестирование и анализ поведения программы. Не теряйте здравый смысл Вне зависимости от того, какой язык программирования используется, все про- программы подчиняются определенным логическим правилам. Язык PL/SQL имеет один синтаксис, язык С — другой. В них применяются разные ключевые слова и операторы (хотя есть и общие, например IF, но их спецификации несколько разные). И уж совсем иначе выглядит программа на языке LISP — элегантная и требующая особого подхода к реализации программной логики. Однако за всем этим стоит логика, выражаемая с помощью тех или иных операторов.
Отладка программ PL/SQL 721 Именно логическая строгость и облегчает изучение новых языков программи- программирования (до сравнению с обычными национальными языками). Если вы в со- состоянии четко определить для себя задачу и выработать последовательность ее решения, особенности конкретного языка не имеют особого значения для успеш- успешного создания программы. Тем не менее программисты удивительным образом ухитряются обходить вся- всякую логику и здравый смысл и выбирают самые иррациональные пути решения проблем. Как только наша «репутация» оказывается под угрозой, как только что- то идет не так, мы начинаем подозревать и обвинять всех и все вокруг: коллег, компьютер, компилятор, текстовый процессор — кого угодно, только не себя. Но помните, что, не желая признать свой промах, вы отказываетесь от поиска путей решения проблемы. Компьютеры и компиляторы не отличаются интеллектом, но они быстры, надежны и последовательны. Все, что они умеют, - это следовать правилам, которые записаны в вашей программе. Так что, обнаружив ошибку в про- программном коде, примите ответственность за нее на себя. Признайте, что именно вы сделали что-то не так, а не компилятор PL/SQL, не Oracle Forms и не тексто- текстовый редактор. Обнаружив, что один из базовых элементов кода не работает, сделайте пере- перерыв. А лучше попросите кого-нибудь из коллег взглянуть на ваш код. Ведь, по- посмотрев со стороны, часто проще заметить то, что лежит на поверхности, но чего вы сами не можете увидеть уже битый час. Выполняйте анализ вместо попыток Итак, после того как вы собрали всю необходимую информацию об ошибке, ее нужно проанализировать. У многих программистов анализ принимает такую фор- форму: «Хм, кажется, причина в этом. Сейчас внесем изменения, перекомпилируем и проверим, что получилось». Чем плох подобный подход? Если вы ищете решение путем проверки возмож- возможных вариантов, это означает, что: О вы не уверены в способности данного изменения действительно решить про- проблему; имея такую уверенность, вы бы не «проверяли», а просто тестировали внесенное изменение; О вы не проанализировали ошибку, чтобы найти причины ее появления; если бы вы имели представление о том, чем вызвана ошибка, то знали бы, как ее испра- исправить; не будучи уверенным, вы действуете методом проб и ошибок, а это пло- плохая практика; О даже если внесенное вами изменение устраняет ошибку, вы не знаете наверня- наверняка, не приняла ли она другую форму и не появится ли вновь (если вы не пони- понимаете, в чем состоит проблема, ее внешнее исчезновение еще не означает, что она действительно решена); все, что можно сказать в данном случае, - это то, что при известных вам условиях данная проблема больше не появляется. Чтобы окончательно избавиться от проблемы, таковую нужно полностью про- проанализировать и определить ее источник. Только тогда будет найдено правиль- правильное и надежное решение. Отыскав потенциальное решение, очень внимательно проанализируйте про- программный код, не выполняя его. Попробуйте придумать разные сценарии для
722 Глава 19 • Управление приложениями PI/SQL проверки своей гипотезы. И лишь обретя уверенность в том, что вы действительно поняли проблему и нашли ее решение, внесите изменения и протестируйте новую версию программы. Помните: нужно не пробовать, а исправлять и тестировать. Делайте перерывы и обращайтесь за помощью Слишком погрузившись в проблему, трудно сохранить способность оценивать ее объективно. Поэтому если вы чувствуете, что застряли и не продвигаетесь впе- вперед, когда все средства перепробованы, а решения все нет, примите следующие радикальные меры: 1. Сделайте перерыв. 2. Обратитесь за помощью. Попытки «высидеть» решение обычно не приводят к успеху. Если накопились усталость и психологическое напряжение, если вы потеряли цель и начали дейст- действовать нерационально, если вы уже много часов не отрываете взгляда от экрана, попробуйте отвлечься и немного передохнуть - есть вероятность, что решение появится само собой. Работая за компьютером, возьмите себе за правило как у.инимум один раз в час вставать из-за стола, чтобы немного прогуляться. Расслабьтесь и позвольте «ней- росетям» вашего мозга установить новые соединения, и, вероятно, у вас появятся новые идеи. Ведь окружающий мир не прекращает существовать, даже когда ваши глаза прикованы к экрану. Взгляните, что происходит за окном. Еще более эффективный путь - попросить кого-нибудь просмотреть ваш код. Это действительно очень помогает. Можно часами сражаться с программой, а за- затем, рассказывая кому-нибудь о своих затруднених, вдруг попят7>, в чем дело. Ошибка может быть очень простой, например она может состоять в несоответст- несоответствии имен, в неверном предположении или в неправильной логике оператора IF. В любом случае существует огромная вероятность того, что, позвав кого-либо на помощь, вы тут же сами во всем разберетесь. Но даже если на вас ни снизойдет неожиданное озарение, причину ошибки поможет выявить свеж.ш взгляд посто- постороннего человека, который: О не писал этот код и не имеет неверных предположений о том, как он должен работать; О не зациклен на программе и вообще не волнуется о том, будет она функциони- функционировать или нет. Кроме того, обращаясь за помощью к коллегам, вы демонстрируете им, что уважаете их мнение и квалификацию. Если же вы один из лучших программи- программистов в группе, то признаете, что также иногда делаете ошибки и нуждаетесь в по- помощи. Это помогает создать атмосферу доверия, взаимопомощи и сотрудничест- сотрудничества, что в конечном счете повышает эффективность работы всей группы. Изменяйте и тестируйте разные области кода по очереди Одним из главных недостатков хороших программистов является то, что они слишком уверены в своих способностях и квалификации и поэтому пытаются ре- решать одним махом множество проблем. Внеся определенное число изменений и запустив тест, мы получаем неутешительные результаты. Оказывается, что, во-первых, изменения вызвали новые проблемы (это часто случается и является
Оптимизация программ РЦ/SQL 723 явным признаком того, что программа требует длительного тестирования и от- отладки), а во-вторых, исчезли не все исходные ошибки. Но хуже всего то, что мы понятия не имеем, какие изменения способствовали устранению ошибок, а какие послужили причиной появления новых. Подобный способ отладки вносит в программу лишь сумбур и путаницу, по- поэтому нужно вернуться к исходной точке и действовать более последовательно. Если вы вносите не самые элементарные изменения, исправляйте по одной ошибке за раз и сразу тестируйте программу Применение такой тактики подра- подразумевает выполнение большого количества перекомпиляций и тестов, но в конеч- конечном счете помогает сэкономить время и силы. Описанная методика тестирования называется инкрементальной. Она требует, чтобы перед тестированием программы, в которой вызываются другие модули, таковые тестировались по отдельности. Когда дело дойдет до тестирования всей программы, вы точно будете знать, что возникающие проблемы обусловлены не действием отдельных модулей, а их взаимодействием. (О том, как тестировать модули, рассказывалось ранее, в разделе -«Тестирование программ PL/SQL».) Оптимизация программ PL/SQL Оптимизация приложения Oracle — это сложный процесс, включающий оптими- оптимизацию SQL-инструкций, проверку правильности конфигурации системной гло- глобальной области (SGA), оптимизацию алгоритмов и т. д. Настройка отдельных программ PL/SQL немного проще, но тоже сопряжена с преодолением ряда труд- трудностей. Прежде чем тратить время на попытку повысить производительность кода PL/SQL, необходимо сделать следующее. О Настроить доступ к коду и данным в SGA. Перед выполнением программный код загружается в область памяти Oracle, называемую SGA. Этот процесс мо- может потребовать оптимизации, как правило, осуществляемой администрато- администратором базы данных. О SGA и этапах выполнения программ PL/SQL подробно рассказывается в главе 20. О Оптимизировать SQL-инструкции. Практически в любом приложении, раз- разработанном для РСУБД Oracle, большая часть операций по настройке связана с оптимизацией SQL-инструкций. Например, если в программе производится неэффективное объединение данных из 16 таблиц, это сводит на нет все по- попытки усовершенствования процедурной части блока кода. Иными словами, если программа выполняется, скажем, 20 ч, а вам нужно, чтобы она решала не- некоторую задачу максимум за 30 мин, все надежды возлагаются на оптимиза- оптимизацию SQL. Существует множество инструментальных средств сторонних про- производителей, помогающих администраторам баз данных и разработчикам в вы- выполнении сложного анализа SQL-кода и предлагающих другие альтернативы. Итак, убедившись, что код PL/SQL выполняется достаточно эффективно, следу- следует «переключиться» на программный код. Вот наши рекомендации в этой связи. О Используйте для разработки приложения лучшие из известных вам стратегий и стандартов. При реализации исходных требований к программе старайтесь
724 Глава 19 • Управление приложениями PL/SQL применять наиболее эффективные подходы, однако не стоит чрезмерно беспо- беспокоиться о совершенствовании каждой строки кода. Помните, что большая часть написанного кода не является слабым местом программы и не нуждается в оп- оптимизации. О Анализируйте эффективность выполнения различных компонентов прило- приложения. Если скорость работы приложения недостаточно высока, нужно выяс- выяснить, какие именно элементы приложения замедляют его работу и целиком сконцентрироваться на них. О Оптимизируйте алгоритмы. Поскольку PL/SQL является процедурным язы- языком программирования, его часто применяют для реализации сложных фор- формул и алгоритмов. В программах можно встретить условные операторы, цик- циклы, даже операторы GOTO и многократно используемые модули. Программные алгоритмы могут быть реализованы множеством разных способов, причем не всегда успешно. Как же оптимизировать неудачно написанные алгоритмы? Это непростой вопрос. Ведь процесс настройки алгоритмов гораздо сложнее про- процесса оптимизации инструкций языка SQL (который структурирован, поэто- поэтому по отношению к нему может применяться автоматизированный анализ). О Пользуйтесь всеми доступными средствами повышения производительно- производительности программ PL/SQL. Корпорация Oracle наполнила свой продукт множест- множеством средств, повышающих эффективность выполнения программного кода. Пользуйтесь такими операторами, как RETURNING и FORALL, и ни в коем случае не применяйте устаревшие методы. Подробное рассмотрение таких операций, как оптимизация SQL, а также кон- конфигурирование базы данных и SGA, выходит за рамки нашей книги. В частности, для более детального изложения одной из перечисленных тем — оптимизации PL/SQL — потребовалось бы несколько глав. Кроме того, разработчики часто об- обнаруживают, что большинство советов нередко оказываются неприменимыми к конкретным приложениям и средствам. Так что оставшуюся часть этой главы мы посвятим анализу производительности программного кода и лишь в конце приве- приведем некоторые советы, относящиеся к оптимизации приложений самого широкого диапазона. Анализ производительности PL/SQL Перед тем как приступать к оптимизации приложения, нужно выяснить, какие из его компонентов замедляют работу, а уж затем прилагать определенные усилия. И Oracle, и сторонние производители предлагают множество инструментов, по- помогающих проанализировать работу приложения. Большинство из них выполняют прежде всего анализ содержащихся в программе SQL-инструкций, предлагают их альтернативную реализацию и т. п. Это очень мощные средства, но несмотря на огромное количество предоставляемых ими вспомогательных данных, они не дают разработчикам ответа на главный вопрос: как быстро выполняется конкретная программа и насколько повысится скорость ее работы после внесения определен- определенного изменения?
Оптимизация программ РЦ/SQL 725 Для получения ответов на приведенные вопросы Oracle предлагает множество встроенных средств. Перечислим наиболее полезные из них. О DBMS_PROFILER — встроенный пакет, который позволяет активизировать для те- текущего сеанса профайлер (подпрограмму протоколирования, позволяющую оценить время выполнения отдельных функций). Это означает, что при запус- запуске программного кода Oracle начнет записывать в таблицы подробную инфор- информацию о том, как долго выполняется каждая строка кода. После этого можно будет выполнять запросы к этим таблицам или, что более предпочтительно, выводить данные в удобной графической форме, используя такие средства, как TOAD и SQL Navigator. О DBMS_UTILITY.GET_TIME — встроенная функция, вычисляющая время выполне- выполнения кода с точностью до сотой доли секунды. С этой функцией взаимодейст- взаимодействуют сценарии файлов tmr81.ot и pivtmr.pkg (имеются на узле O'Reilly). Таким образом можно четко фиксировать время выполнения каждой операции в про- программе и даже сравнивать разные реализации одной и той же логики. Ниже приводится несколько советов и примеров, касающихся использования инструментов, которые обеспечивают взаимодействие с программами пакета DBMS_ PROFILER. Oracle пока не устанавливает пакет D8M5_PROF ILER автоматически. Для того что- чтобы узнать, имеется ли он в вашей системе, подключитесь к своей схеме в SQL*Plus и выполните такую команду: SQL> DESC DBMS_PRQFILER Появление сообщения вида ERROR: ORA-04043: object dbms profiler does not exist свидетельствует о том, что этот пакет нужно инсталлировать. Если вы работаете с Oracle 7.x или 8.0, попросите администратора базы дан- данных выполнить от имени пользователя, имеющего привилегию SYSDBA, следую- следующие сценарии (первый создает спецификацию пакета, а второй - его тело): #tera/rar_0R4Ci?Vrdbms/admiri/dbmspbp.sql Wara/ior_0ft4Ci?/rdbms/admiri/prvtpbp.plb Для Отас1е8« и Oracle9t, также от имени пользователя, имеющего привилегию SYSDBA, надлежит выполнить такой сценарий: $КаTdnor_ORACLE/rdbms/admi n/prof1oad.sql Затем в собственной схеме требуется запустить сценарий: $Катапог_ШС1.Е /rdbnis/admin/proftab. sql Это приведет к созданию трех таблиц, которые будут заполняться информа- информацией, полученной с помощью программ пакета DBMS_PROFI(_?R: О PLSQL_PROFILER_RUNS - родительская таблица с информацией о запускаемых про- программах; О PLSQL_PROFILER_UNITS — программные модули, выполняемые в составе данной программы;
726 Глава 19 • Управление приложениями PL/SQL О PLSQL_PROFJ LER_DATA - данные профайлера для каждой строки программного модуля. Возможно, вы найдете полезными некоторые образцы запросов и пакеты для вывода отчетов, предлагаемые Oracle в следующих файлах: %Кдталог_ШС1Е1р'] sql /demo/prof rep. sql %Катшг_ORACLEIti\ sqT /demo/profsum. sql После определения всех этих объектов можно собирать протоколируемую ин- информацию о приложении с помощью приведенного ниже кода; BEGIN DBMSJXJTPUT.PUT LINE ( OBMS_PROFiLER.START_PROFILER ( 'showemps ' || TO_CHAR (SYSDATE. 'YYYYMMDD HH24:MI:SSP) showemps; DBMS_OUrPUT.PUTLINE С OBMS_PROFILER.STOP_PROFRER); END: По окончании выполнения программы вы можете осуществить запросы к таб- таблицам PLSQL_PROFILER_..., чтобы просмотреть результаты работы профайлера. Вот пример такого запроса, выводящий строки кода, выполнение которых заняло не менее 1 % общего времени работы программы: /* Файл в web: slowest.sql */ update p1sql_profiler_units set total_t1me - 0; execute prof_report_uti1ities.rol1up_allj-uns: set pagesize 999 set lines Ue 120 column unit format a20 col urn line# format 99999 column time_Count format alS column text format a60 spool slowest2.txt select to_Char(pl.total_t1me/l0000G00. '99999999') || '- TO_CHAR (pl.total_occur) as t1me_count. substr(p2.un1t_owner. 1, 20) || '.' || oecode(p2.unit_name. ''. ^anonymous^'. substr(p2.un1t_name.l. 20)) as unit. T0_CHAR (pl.ltne#) || ¦-' || p3.text text from plsqlj}rofiler_data pi. plsql_profiler_Linits p2. all_source p3. plsql_prof1ler_grand_total p4 where p2.urnt_owner NOT IN ('SYS1. 'SYSTEM') AND pl.runID > &&firstparm AND
Оптимизация программ PL/SQL 727 (pl.total_fime >- p4.grand_total/100) AND pl.runID - pZ.runid ANO pZ.unit_nijmber=pl.unit_number AND p3.type-'PACKAG? B0Dr~AND p3.owner - pZ.unit_owner ANO p3.11ne - pl.line#~AND p3.name-p2.urnt_name order by pl.total_t1me desc: spool off Как видите, эти запросы очень сложны (для создания такого объединения че- четырех таблиц мы обратились к одному из готовых запросов, предоставленных Oracle). Поэтому гораздо удобнее применять специальные инструментальные сред- средства PL/SQL с графическим пользовательским интерфейсом. Проанализировав программный код приложения и выявив его слабые места, нужно решить, какие изменения позволят повысить его производительность. В этом вам поможет материал следующих разделов. Трассировка выполнения кода В ранних версиях Oracle уже можно было производить трассировку кода PL/SQL, но только в Oracle8i появился настоящий API, позволяющий осуществлять пол- полноценную трассировку процедур, функций и исключений PL/SQL. Программы для запуска и прекращения трассировки сеанса находятся во встроенном пакете DBMSJTRACE. Когда производится трассировка, данные о выполнении программы записываются в соответствующий файл трассировки, ПРИМЕЧАНИЕ— — — — — Средства трассировки PL/SQL позволяют записывать действия, производимые кодом, в специаль- специальный файл. А средства профилирования (реализованные в виде описанного выше пакета DBM5_PRO- FTLER) предназначены для проведения более подробного анализа работы приложения, включая хронометраж операций и подсчет количества выполнений конкретной строки. Установка пакета DBMSJTRACE Этот пакет не всегда автоматически инсталлируется вместе со всеми остальными встроенными пакетами. Для того чтобы определить, установлен ли он, нужно под- подключиться к базе данных в качестве пользователя с учетной записью SYS (или с другой учетной записью, имеющей привилегии SYSDBA) и выполнить команду: BEGIN D8MS_TRACE.CLEAR_PLSQL_TRACE: END: Если на экране появится строка вида PLS-OOZQ1-. identifier ¦D8MS_TRACE.CLEAR_PLSQL_TRACE' must be declared значит, пакет нужно устанавливать. С этой Целью, используя для подключения ту же учетную запись, выполните следующие файлы в указанном порядке: tKaranor_0/Maf/rdbms/admin/dbnispbt.sql SKa ra/iorJMCLE/ rdbms /admi n/prvtbpt. pi b
728 Глава 19 • Управление приложениями PL/SQL. Программы из пакета DBMS_TRACE В пакете DBMSTRACE содержатся три программы: О SET_PLSQL_TRACE - активизирует трассировку в текущем сеансе; О CLEAR_PLSQL_TRACE — останавливает трассировку в текущем сеансе; О PLSQL TRACEVERSION - возвращает основной и дополнительный номера версий текущего трассируемого пакета PL/SQL Для трассировки выполняемого кода PL/SQL сначала нужно осуществить в те- текущем сеансе такой вызов: DBMS_TRACE.SET_PLSQL_TRACE 1уроаень_трэссировки INTEGER); Здесь значением аргумента уровень_грассировки служит одна из констант, пере- перечисленных ниже. О Константы, определяющие, какие элементы программы PL/SQL подлежат трас- трассировке: DBMS DBMS" DBMS" TRACE trace "TRACE.trace" "trace trace _all_calls _enabled_calls all exceptions DBMS TRACE.trace enabled exceptions DBMS DBMS" DBMS DBMS^ TRACE "trace "trace "trace trace_ trace_ trace_ trace_ all sql enabled_sq1 alljines enabledjines constant constant constant constant constant constant constant constant INTEGER INTEGER INTEGER INTEGER INTEGER INTEGER INTEGER INTEGER О Константы, управляющие процессом трассировки DBMS DBMS" DBHS" DBMS^ TRACE "trace TRACE "trace ПРИМЕЧАНИЕ- trace stop constant trace_pause constant trace_ trace_ .resume constant limit constant INTEGER :- INTEGER :- INTEGER :- INTEGER :- 16384; 4096; 8192; 16: - 1: = 2: = 4: -8; -32: - 64; = 128 -256- Комбинируя константы из пакета DBMSJTRACE, можно активизировать одновременную трассировку нескольких элементов языка PL/SQL. Обратите внимание, что констэклы, управляющие процессом трассировки (в частности, DBMS_TRACE.trace_pause) не могут использоваться в сочетании с други- другими константами (например, с DBMS_TRACE.trace_enabled_calls). Для того чтобы активизировать трассировку всех программ, выполняемых в те- текущем сеансе, нужно произвести такой вызов: DBMS_TRACE.SET_PLSQL_TRACE (DBMS_TRACE.trace_all_calls): Трассировка всех исключений, инициируемых в течение текущего сеанса, акти- активизируется следующим образом: OBMS_TRACE.SET_PLSQL_TRACE CDBMS_TRACE.trace_all_except1ons); Далее запускается программный код. При необходимости остановить трасси- трассировку выполняется вызов: DBMSTRACE. CLEARPLSQLJRACE; После этого можно проанализировать содержимое файла трассировки. Имена файлов трассировки генерируются СУБД Oracle; просмотрев даты модификации
Оптимизация программ PL/5QL 729 всех файлов, легко найти нужный. О том, где эти файлы хранятся, рассказывает- рассказывается далее, в разделе «Форматирование собранных данных». Учтите, что трассировка PL/SQL на многопоточном сервере невозможна. Управление содержимым файла трассировки Файлы трассировки, генерируемые пакетом D8MS_TRACE, могут быть очень боль- большими. Чтобы уменьшить объем выходных данных, следует трассировать не все подряд, а только конкретные программы. ПРИМЕЧАНИЕ- Указанный подход нельзя использовать с удаленными вызовами процедур. Для того чтобы созданную или измененную в течение сеанса программу сде- сделать доступной для проведения трассировки, следует изменить установку сеанса, управляющую отладкой: ALTER SESSION SET PLSQL_OEBUG-TRUE; Если вы не хотите менять установку всего сеанса, перекомпилируйте конкрет- конкретный программный модуль в режиме отладки следующим образом (к анонимным блокам это неприменимо): ALTER [PROCEDURE | FUNCTION | PACKAGE BODY] имя_программы COMPILE DEBUG: После того как вы сделаете нужные программы доступными для выполнения трассировки, инициируйте трассировку только этих программных модулей: DBMSJRACE. SET_PLSQL JRACE (DBMSJRACE. trace_enat)l ed_cal Is); Трассировку исключений можно ограничить лишь теми исключениями, кото- которые инициируются в доступных для данного процесса программах: DBMSJRACE, SET_PLSQL_TRACE (DBMSJRACEЛгасе_епаЫed_exceptions); Если вы установили два уровня трассировки (для всех программ или исклю- исключений, а также для заданных программ или исключений), будет действовать уро- уровень «для всех». Приостановление и возобновление процесса трассировки Процедура S?T_PLSQL_TRACE способна не только определять, какая информация под- подлежит трассировке. С ее помощью можно также приостанавливать и возобнов- возобновлять процесс трассировки. В следующем вызове запрашивается информация, ко- которая не фиксировалась, пока трассировка не была возобновлена: DBMSJRACE.SET_PLSQLJRACE(DBMSJRACE.trace_pause); Пакет DBMS_TRACE помещает в файл трассировки запись, указывающую на то, когда трассировка была приостановлена и когда возобновлена. Посредством константы DBMS_TRACE. trace I i mi t можно указать, что в файле трас- трассировки должна сохраняться информация только о 8192 трассируемых событиях. Этот подход гарантирует, что включение трассировки не приведет к перегрузке базы данных. По завершении сеанса трассировки будут сохранены лишь 8192 по- последних записей.
730 Глава 19 • Управление приложениями PL/SQL Форматирование собранных данных Если вы активизировали трассировку только определенных программных моду- модулей, но не текущей программы, данные трассировки записаны не будут. Если же назначена трассировка текущей программы, в файл трассировки записывается ее тип, имя и глубина стека. При трассировке исключений определяется номер строки программы, в про- процессе выполнения которой оно было инициировано, его тип (пользовательское или предопределенное) и, в случае предопределенного исключения, его номер (номер ошибки, связанной с пользовательским исключением, всегда равен 1). В качестве примера приведем результат трассировки процедуры showemps: *** 1999.06.14.09.59.25.394 *** SESSION I0:(9.7) 1999.06.14.09.59.25.344 -- PL/SOL TRACE INFORMATION Levels set : 1 Trace: ANONYMOUS BLOCK: Stack depth - 1 Trace: PROCEDURE SCOTT.SHOWEMPS: Call to entry at line 5 Stack depth - 2 Trace: PACKAGE BODY SYS.DBMS SQL: Call to entry at Hne 1 Stack depth - 3 Trace: PACKAGE BODY SYS.OBMS~SYS_SQL: Call to entry at line 1 Stack depth - 4 Trace: PACKAGE BODY SYS. D8MSJYS_SQL: ICD vector index - 21 Stack depth = 4 Trace: PACKAGE PLVPRO.P: Call to entry at line 26 Stack depth - 3 Trace: PACKAGE PLVPRO.P: ICO vector index - 6 Stack depth - 3 Trace: PACKAGE BODY PLVPRO.P: Call to entry at line 1 Stack depth = 3 Trace: PACKAGE BODY PLVPRO.P: Call to entry at Hne 1 Stack depth - 3 Trace: PACKAGE BODY PLVPRO.P: Call to entry at line 1 Stack depth - 4 Повышение производительности приложения После того как мы рассмотрели некоторые способы анализа работы приложения, предлагаем вашему вниманию несколько советов по повышению его производи- производительности. Как избежать выполнения ненужного кода Ответить на этот вопрос непросто. И совсем уж не понятно, зачем подобный код создавать и вводить его в программу. Вот только реальность такова, что процесс программирования нередко сопро- сопровождается спешкой. Мы нервничаем, опасаясь, что не уложимся в установленные сроки, например из-за того, что начальство решило сэкономить на необходимых инструментальных средствах. В подобных условиях ничего не стоит вопреки за- законам логики написать несколько «холостых»- модулей или фрагментов кода, ко- которые выполняются впустую и замедляют работу приложения. Удаление этого кода позволяет повысить производительность приложения, причем иногда до- довольно существенно. Поэтому никогда не стоит жалеть времени на оптимизацию своей программы.
Повышение производительности приложения 731 Поиск ненужного кода Как же отыскать неэффективные фрагменты в приложении, содержащем ты- тысячи строк кода? Вот несколько рекомендаций. О Проверьте циклы. Кол в теле цикла (такого как FOR, WHILE либо простого цик- цикла) обычно выполняется более одного раза. Поэтому влияние любого неэф- неэффективного фрагмента в нем многократно усиливается. О Проверьте SQL-инструкции. Прежде всего следует убедиться, что все SQL-ин- SQL-инструкции оптимизированы. Эта тема выходит за рамки нашей книги, однако имеется множество других книг, где освещается данный материал. Имейте в виду, что в некоторых случаях выполнение определенной операции только при помощи SQL неэффективно, и поэтому, применив для указанной цели PL/SQL, можно ускорить дело. О Просмотрите фрагменты кода, подвергавшиеся многократным исправлениям. В любой сложной программе, которая эксплуатировалась более шести месяцев, обычно можно найти модифицированные фрагменты кода, подвергавшиеся многочисленным изменениям. Результатом таких изменений часто становит- становится неэффективный код. О Проверьте разделы объявлений. В разделе объявлений объявляются пере- переменные, им присваиваются начальные значения и т. п. Однако в этом разделе могут выполняться действия (скажем, формирование значений переменных по умолчанию), которые не всегда нужны и поэтому их не стоит выполнять в начале блока. Давайте рассмотрим некоторые из перечисленных рекомендаций подробнее. Проверка циклов Как уже говорилось, код в теле циклов обычно выполняется более одного раза, поэтому влияние каждого его неэффективного фрагмента усиливается во много раз. Приведем пример. Занимаясь оптимизацией кода одного из клиентов, про- программист обнаружил 30-строчную функцию, которая выполнялась за полсекун- полсекунды, но вызывалась так часто, что общее время ее работы составляло пять часов. Оптимизировав данную функцию, он смог добиться сокращения времени работы всей программы до двадцати минут. Так что всегда первым делом обращайтесь к циклам и проверяйте, нет ли у вас подобных проблем. Вот еще один пример. Процедура принимает в качестве аргумента некоторое имя и по очереди обрабатывает строки курсора: PROCEDURE process_data (nm_in IN VARCRAR2) IS BEGIN FOR rec IN my_package.niy_cursor LOOP process_record ( UPPER (nin_in}. rec.total_prodLiction): i END LOOP: END: В данном случае проблема заключается в том, что функция UPPER является ар- аргументом процедуры на каждой итерации цикла. Это нецелесообразно, поскольку
732 Глава 19 * Управление приложениями PL/SQL значение аргумента не изменяется. Объявив переменную для хранения того же имени в верхнем регистре и присвоив ей значение перед входом в цикл, мы избав- избавляем программу от лишней и совершенно бессмысленной работы: PROCEDURE process_data (nm_in IN VARCHAR2) IS v_™ some_table.some colunrtSTYPE := UPPER (nm in): BEGIN FOR rec IN my_paclcage.my_cursor LOOP process_record Cv nm. rec.total_production): END LOOP: END: Конечно, выявить факт нерационального выполнения кода не всегда просто. Обнаружив, в частности, что процедура process_data является слабым местом про- программы (ограничивает ее производительность), мы постарались бы проверить, как работает код, где она используется. Иногда неверные предположения о рабо- работе вызываемого кода могут послужить причиной задания излишних или даже не- некорректных действий программы. Например, в результате исследования могло бы выясниться, что процедура process_record первым делом всегда переводит по- полученное имя в верхний регистр, а следовательно, сразу исчезла бы необходи- необходимость в вызове фуЕ1кции UPPER. Ниже приведен еще один пример программы с интенсивными вычислениями в теле цикла. Какие действия в данном случае являются излишними? 1 CREATE OR REPLACE PROCEDURE process_data (tab in IN VARCHAR2) 2 IS 3 cursorjd PLSJNTEGER: 4 exec_stat PLSJNTEGER; 5 BEGIN 6 FOR rec IN (SELECT ... FROM ...) 7 LOOP 8 cursorjd := DBMS SQL.open_cursor; 9 10 DBMS_SQL.parse (cursorjd, 11 'SELECT ... 12 FROM employee E. ' 11 tabjn || 13 'WHERE D.departmentjd - ' || redd || 14 'AND ...'. 15 DBMS SQL.native 16 ); 17 exec_stat :- DBMS J>0L.execute {Cursorjd); 18 DBMS_SQL.close_cursor (cursorjd); 19 END LOOP; 20 END; Ответить на этот вопрос не так-то просто. Однако если вы знаете, как работает пакет D8MSJ5QL (встроенный пакет для выполнения динамического SQL, описан- описанный в главе 15), вам известно следующее. О Возможно многократное использование курсора, выделенного с помощью функ- функции OBMSSQL. 0RENCURSOR. Поэтому незачем открывать и закрывать курсор на каждой итерации цикла.
Повышение производительности приложения /аз О При работе с динамическим SQL нужно внимательно проанализировать SQL- сгроки. В данном случае, взглянув на строки 11-14, мы видим, что на каждой итерации цикла меняется только одно значение — departmentid. В этом вари- варианте кода процедуры оно является частью SQL-строки, в результате чего для каждого нового значения синтаксический анализ запроса выполняется по не- нескольку раз. Эффективнее было бы использовать привязку, как показано ниже. Вот новый вариант процедуры process_data, где в цикле производятся только необходимые операции: CREATE OR REPLACE PROCEDURE process_data (tabjn IN VARCHARZ) IS cursorjd PLSJNTEGER: exec_stat PLSJNTEGER; BEGIN cursorjd := D8MS_SQL.open_airsor; DBMS_SQL.parse ( cursorjd. 'SELECT ... FROM employee E. ' || tabjn || 'WHERE O.departmentjd - :my_ID AND ...'. DSNSJQL. native) ; FOR rec IN (SELECT ... FROM ...) LOOP . . DBMSJJQL.bind_variable (cursorjd, 'my 1d'. redd): exec_stat :- DBMS_SQL.EXECUTE (cursorjd): ЕЮ LOOP; DBMSJ>QL.close_cursor (cursorjd): END: Своевременное выполнение кода Тот факт, что раздел объявлений располагается перед выполняемым разделом, вовсе не означает, что все переменные программы должны быть объявлены имен- именно в нем. Вполне возможно, что некоторые производимые здесь действия (напри- (например, формирование значений переменных по умолчанию) лучше перенести в дру- другие части кода, поскольку логически они должны выполняться один раз. Рассмотрим следующий блок кода; PROCEDURE always do_everythiщ (criteria 1n IN BOOLEAN) IS big_string VARCHAR2C2767) :- teruninutejookup (•-¦): bigjist I1st_types.big_sthngsjt :- twojninute_number_cruncher (...); BEGIN IF NOT criteriajn THEN use_big_str1ng (big_string): process_bigjist (bigjist); ELSE /* Никакие важные операции здесь не выполняется */ END IF: END:
734 Глава 19 • Управление приложениями PL/SQL В этой программе объявляется длинная строка и вызывается функция, кото- которая тратит на ее заполнение 10 мин. Кроме того, здесь объявляется и заполняется большая коллекция (объявленного в пакете табличного типа), для инициализа- инициализации которой используется функция, выполняющая длительные и интенсивные вычисления. Оба этих действия производятся в разделе инициализации. После тестирования приложения, вызывающего приведенную процедуру, оказалось, что оно работает чересчур медленно. Проанализировав его логику, мы выяснили, что та часть процедуры, где используются две огромные структуры, выполняется при каждом ее вызове, а не только когда значение переменной cri ten' a_i n равно fa 1 se. Отыскав источник проблемы, мы можем переписать программу следующим образом (с использованием вложенных блоков): PROCEDURE only_as_needed (criteria in IN BOOLEAN) IS PROCEDURE heavydutyjirocessing IS bigstring VARCHAR2 C2767) :- ten_minute_laokup (...); bigjist Iist_types.big_5tring5_tt := two_minLrte_njmber_cruncher {...): BEGIN use_big_string (big_strinq): proces5_bi g_li st {bi g_li st): END: BEGIN IF NOT criteria in THEN heavy_duty_processi ng; ELSE /* Никакие важные операции зпесь не вмполияится */ END IF: END: Теперь длительные операции вызываются только при условии, что они дейст- действительно необходимы. У предложенного решения имеется еще одно достоинство: когда выполнение вложенного блока завершается, освобождается занимаемая им память. И если программа будет еще достаточно долго работать, все это время в памяти не будут содержаться лишние структуры данных. Умение слушать Умеете ли вы слушать? О чем вы думаете, когда вам что-либо говорят: о своем от- ответе или о том, какую информацию хочет донести до вас собеседник? Умение слушать - это качество, присущее каждому умному человеку. Но оно является особенно важным для программиста, которому нужно выяснить требо- требования пользователя и воплотить их в программе. Невнимательность в этом случае обходится очень дорого и приводит к написанию кода, который не соответствует требованиям или работает неэффективно. Рассмотрим приведенный ниже фраг- фрагмент программы: CREATE OR REPLACE PROCEDURE removejept ( deptnojn IH efflp.deptnoSSTYPE. newjteptno in IN emp.deptnctfTYPE) IS
Повышение производительности приложения einp_count NUMBER: BEGIN SELECT COUNK*) INTO erap_count FROM emp WHERE deptno - deptnojn; IF snp_count > 0 THEN UPDATE emp SET deptno - new_deptno_in WHERE deptno - deptno_in; END IF: DELETE FROM dept WHERE deptno = deptnojn; END drop_dept; Используемая в нем процедура удаляет строку из таблицы подразделений, но сначала назначает входящим в указанное подразделение сотрудникам другое под- подразделение. Логика программы очень проста: если сотрудник относится к удаляе- удаляемому подразделению, нужно обновить его строку, записав в нее другой номер подразделения. После перевода всех сотрудников в новое подразделение старое удаляется. Как по-вашему, что здесь неправильно? Явно неверных действий программа не выполняет, но значительная часть ее кода является избыточной. Прежде всего, если инструкция UPDATE не обновляет ни одной строки, она не может генериро- генерировать ошибку. Поэтому предварительная проверка наличия сотрудников в удаляе- удаляемом подразделении не нужна, а следовательно код, процедуры можно сократить следующим образом: CREATE OR REPLACE PROCEDURE removejJept t deptncMn IN emp.deptnoSTYPE. new_deptno_in IN emp.deptnoUTYPE) IS emp_count NUMBER: BEGIN UPDATE emp SET deptno - new__deptno_":n WHERE deptno - deptno_in; DELETE FROM dept WHERE deptno - deptnajn: END drop_dept: Предположим, что проверка наличия сотрудников необходима. Давайте раз- разберемся, что происходит. Нам нужно получить ответ на вопрос, есть ли в подразде- подразделении сотрудники, но вместо этого код определяет, сколько сотрудников в под- подразделении. И хотя ответ на второй вопрос легко преобразовать в ответ на первый, для чего достаточно проверить логическое выражение (empcount > 0), программа явно выполняет лишние действия. Существует множество способов выяснить, имеется лм в подразделении хоть один сотрудник. И выбранный метод может в значительной степени повлиять на производительность. Для сравнения разных подходов к решению этой задачи об- рататесь к сценарию atleastone.sql, который находится на узле O'Reilly.
736 Глава 19 • Управление приложениями ПРИМЕЧАНИЕ В начале сценария atleastone.sql создается довольно большая копия таблицы employee. Этот код за- закомментирован, чтобы избежать его выполнений, если такая таблица действительно присутствует в базе данных. Ради эксперимента вы можете удалить символы комментария. Просмотрев указанный сценарий, можно сделать следующий вывод: при необ- необходимости выполнить одноразовую выборку данных и выявить наличие хотя бы одного элемента в некотором списке одним из наиболее эффективных решений является использование явного курсора. А самое главное в том, что это решение позволяет получить конкретный ответ на поставленный пользователем вопрос, а следовательно, удовлетворяет его требованиям. Использование пакетных данных для минимизации SQL-обращений к базе данных Когда переменная объявляется в теле или спецификации пакета, область ее види- видимости не ограничивается конкретной процедурой или функцией. Областью види- видимости данных уровня пакета является «весь сеанс подключения к Oracle», и зна- значения этих данных сохраняются в течение всего сеанса. Этот факт можно исполь- использовать для минимизации частоты обращений к системной глобальной области (SGA). Поиск в структурах данных, хранящихся в собственной глобальной об- области программы (PGA), выполняется гораздо быстрее поиска в SGA, даже если нужная вам информация располагается в общей памяти. Минимизация особенно важна, когда приложению нужно многократно обра- обращаться к данным, не изменяющимся в течение сеанса. Самым очевидным приме- примером является значение, возвращаемое встроенной функцией USER. Оно не меняется в течение сеанса, поскольку идентифицирует текущего пользователя. Так зачем же вызывать эту функцию более одного раза? Иногда кажется, что встроенная функция работает очень медленно. Однако это не так: сама функция выполняется достаточно быстро, но в связи с тем, что она, во-первых, вызывается приложением неоднократно, а во-вторых, выполняет инструкцию SELECT... FROM dual, суммарное время ее работы оказывается весьма ощу- ощутимым. Несколько лет назад автор столкнулся с такой ситуацией при разработке генератора кода PL/Generator. После анализа процесса выполнения своей про- программы он обнаружил, что функция USER вызывается в пакете 15 000 раз. Это сви- свидетельствовало о том, что программный код составлен нерационально, но у автора не было времени на его просмотр и исправление. Поэтому он поступил иначе — создал следующий пакет: CREATE OR REPLACE PACKAGE tMsuser IS name CONSTANT VARCHAR2C30) ¦= USER- END. а затем выполнил операцию глобального поиска и замены (заменив имя функции USER именем переменной thisuser.name). После перекомпилирования программы и ее запуска выяснилось, что сэкономлено несколько секунд 45-сехунпдого про- процесса. Неплохо. Воспользовавшись пакетной переменной, автор обошелся един- единственным вызовом функции USER, после чего просто обращался к константе thi- thisuser.name.
Повышение производительности приложения 737 Это основная идея, лежащая в основе кэширования данных в структурах уров- уровня сеанса. Конечно, требования могут быть гораздо более сложными — возможно, вы захотите кэшировать набор связанных данных, например структуру записи или даже список запрошенных данных. Вместо того чтобы подробно описывать каждый из этих случаев, предлагаем вам несколько сценариев (имеются на узле O'Reilly). Поэкспериментировав с ни- ними и убедившись в полезности используемых там приемов, вы сможете адаптиро- адаптировать их код для собственных нужд. О Кэширование одного значения. Следует избегать повторяющихся вызовов та- таких функций, как USER, SYSOATE (возвращаемые значения которых в Огас1е9г больше не извлекаются с помощью запроса SELECT... FROM dual), и даже исполь- использования специфических для приложения статических значений. Обратите вни- внимание на два следующих сценария: • thisuser.pkg — позволяет создать пакет, который кэширует значение, возвра- возвращаемое функцией USER двумя способами: с помощью именованной констан- константы и с помощью функции (вызываемой из клиентского PL/SQL); • thisuser.tst - тестовый сценарий, демонстрирующий способ повышения про- производительности за счет кэширования. О Кэширование одной строки данных. Тестовый сценарий извлекает данные из конфигурационной таблицы текущего пользователя и кэширует их в записи, определенной на уровне пакета: • init.pkg — создает таблицу пользовательской конфигурации и пакет, запол- заполняет таблицу тестовыми данными; • init.tst — тестовый сценарий. О Кэширование нескольких строк данных. Более интересный и сложный при- пример. Следующие сценарии демонстрируют принципы использования коллек- коллекции для кэширования нескольких строк данных, что позволяет производить в течение сеанса не более одного запроса к каждой строке (указанная техноло- технология работает только в том случае, когда данные не меняются в течение всего сеанса; пожалуй, она больше подходит для маленьких поисковых таблиц типа «код-описание»): • emplu.pkg — создает два пакета, один из которых извлекает данные с помо- помощью стандартного запроса к базе данных, а другой обращается за этими данными в кэш, созданный на основе коллекции; • emplu.tst — тестовый сценарий, демонстрирующий повышение производи- производительности программы за счет кэширования. Использование предложения BULK COLLECT и оператора FORALL В Oracle9i были значительно усовершенствованы два элемента, которые появи- появились в Огас1е8г: предложение BULK COLLECT и оператор FORALL. Эти элементы позво- позволяют кардинально уменьшить время выполнения SQL-одераций, обрабатывающих большое количество строк данных. Если вы выполняете SQJl-инструкции, обра- обрабатывающие более десяти строк за раз, воспользуйтесь такими элементами для ускорения работы программы.
738 Глава 19 • Управление приложениями PL/SQL Действия оператора FORALL похожи на действия цикла FOR. Указанный опера- оператор «собирает» однородные DML-инструкции а группу и направляет их ядру SQL для пакетной обработки. Приведем пример: CREATE OR REPLACE PROCEDURE update_tragedies ( warcrim_ids IN name_varray. nun_victims IN number_varray ) IS BEGIN FORALL iridx IN warcrimjds.FIRST .. warcr1m_ids.LAST UPDATE war_criminal SET v1ctim_count - nunMrictims (indx) WHERE war_cr1m1na1_id - warcrimjds (indx): END: Предложение BULK COLLECT может использоваться для извлечения нескольких строк за одно обращение при выполнении операций выборки из явных и неявных курсоров: DECLARE names naine_varray: mileages nunfoer_varray; BEGIN SELECT name, mileage BULK COLLECT INTO names, mileages FROM transportation WHERE TYPE - 'AUTOMOBILE' AND mileage < 20: -- А теперь работаем с данными в коллекциях END:" И наконец, ниже приведен пример совместного использования оператора FO- FORALL и предложения BULK COLLECT, в котором последнее дополнено предложением RETURNING: CREATE OR REPLACE FUNCTION remove_emps_ty_dept (deptlist dlist t) RETURN enolistjt IS enolist enolistjt: BEGIN FORALL aDept IN deptlist.FIRST..deptlist.LAST DELETE FROM emp WHERE deptno IN deptlist(aDapt) RETURNING empno BULK COLLECT INTO enolist: RETURN enolist: END: Как видите, чтобы воспользоваться преимуществами оператора FORALL и пред- предложения BULK COLLECT, нужно свободно владеть приемами работы с коллекциями. Об этих массивоподобных структурах подробно рассказывается в главе 11. В гла- главе 14 рассматривается предложение BULK COLLECT, а в главе 13 - оператор FORALL.
Часть VI Особые возможности PL/SQL В арсенале такого мощного и богатого языка, как PL/SQL, имеется множество элементов, которыми программисты пользуются сравнительно редко, но обойтись без которых в особо сложных и ответстееннъа: ситуациях просто невозможно. В этой части книги затрагиваются наиболее сложные аспекты в изучении компонен- компонентов и возможностей PL/SQJ.. В главе 20 рассматриваются этапы и условия выпол- выполнения программ PL/SQL, механизмы управления памятью, особенности клиентско- клиентского и серверного PL/SQL. Глава 21 посвящена использованию объектно-Ориентиро- объектно-Ориентированных средств Oracle (объектных типов и объектных представлений). В главах 22 и 23 рассказывается о методике вызова кода Java и С из приложений PL/SQL. П Глава 20. Выполнение программ PL/SQL ? Глава 21. Объектно-ориентированные возможности PL/SQL ? Глава 22. Взаимодействие Java и PL/SQL ? Глава 23. Внешние процедуры
20 Выполнение программ PL/SQL > Заглянем внутрь > Управление зависимостями > Как PL/SQL использует память сервера Oracle > Выполнение серверного кода PL/SQL > Клиентский код PL/SQL > Модели разрешений > Аппаратное обеспечение для PL/SQL: больше — лучше? > Что еще вам нужно знать Вспомним, что в главе 2 рассказывалось о множестве программных сред, из кото- которых можно вызывать программы PL/SQL. Однако исполняющее ядро PL/SQL имеется только внутри сервера базы данных Oracle и в средствах Oracle Deve- Developer, таких как Oracle Forms Builder и Oracle Reports Builder, выполняемых на клиентской машине. Поэтому разработчик приложения должен принять ряд кон- конструкторских решений, ответив на следующие вопросы. О Где следует разместить код приложения - на сервере, на клиенте или и там и там? О Что лучше использовать — анонимный блок, процедуру верхнего уровня или пакет? О Где выполнять компиляцию серверного кода — на сервере или на клиентской машине? О Какие из клиентских программ лучше хранить не в программе Oracle Forms, а в библиотеках PL/SQL? Кроме того, разработчик должен решить, следует ли учитывать при выполне- выполнении программы привилегии ее владельца или пользователя. Даже опытный программист не сразу сможет дать ответы на все перечислен- перечисленные вопросы. И здесь ему пригодится понимание принципов функционирования PL/SQJL Задача этой главы — помочь вам изучить архитектуру приложений PL/SQ.L и сделать все возможное для усовершенствования их структуры и повы- • шения производительности.
Заглянем внутрь 741 Основной акцент в настоящей главе делается на тех аспектах PL/SQL, кото- которые интересуют разработчика приложений. Но и те вопросы, которыми занима- занимается администратор базы данных, вам, думаем, тоже будут небезынтересны. На- Например, как сервер выделяет память выполняющимся программам, зависит от того, в каком режиме, разделяемого или выделенного сервера, работает Oracle. Это может оказать существенное влияние на производительность приложения, а потому понимание подобного рода вопросов для разработчика приложений не менее важно, чем для администратора базы данных. Заглянем внутрь О PL/SQL обычно говорят как об интерпретируемом языке. Однако в книгах, по- подобных этой, упоминается компилятор PL/SQL. Так с каким же языком мы име- имеем дело: интерпретируемым или компилируемым? Истина, как это часто бывает, лежит где-то посередине. Сначала исходный код обрабатывается компилятором, который генерирует машинно-независимый байт-код. Затем виртуальная маши- машина PL/SQL, входящая в состав сервера Oracle, во время выполнения интерпре- интерпретирует этот байт-код. И, что еще более интересно, ОгаскЭг может по вашему указанию преобразовать программу на языке PL/SQL в код С, откомпилировать ее в совместно используемую библиотеку с помощью компилятора конкретной машины, которая будет динамически загружать данную версию во время выпол- выполнения. Концепции PL/SQL Начнем мы с некоторых важных терминов и концепций, относящихся к обработ- обработке исходного кода PL/SQL в Oracle. О Компилятор PL/SQ.L — это компонент Oracle, который выполняет проверку синтаксиса кода и синтаксический анализ, проверяет допустимость и семан- семантику имен (например, соответствие вызова хранимой программы ее сигнату- сигнатуре) и генерирует два варианта откомпилированного двоичного кода. О DIANA - промежуточный вариант кода PL/SQL, генерируемый компилято- компилятором с целью выполнения синтаксического и семантического анализа. Это пер- первый из двух основных результатов работы компилятора. Код DIANA включает представление спецификации вызова хранимого объекта, то есть информацию из заголовка программы, и в частности имена параметров, их последователь- последовательность и типы данных. Код DIANA генерируется даже для таблиц, представлений и последовательностей. Аббревиатура DIANA расшифровывается как Distri- Distributed Intermediate Annotated Notation for Ada (распределенная промежуточ- промежуточная аннотированная нотация для Ada), хотя DIANA-представление Oracle от- отличается от варианта для Ada. О Машинно-зависимый псевдокод (байт-код) — исполняемый вариант отком- откомпилированной программы PL/SQL; второй из двух основных результатов ра- работы компилятора, иногда называемый в документации Oracle т-кодом или Р-кодом. Этот код передается исполняемому ядру PL/SQL. Полезно знать, что
742 Глава 20 • Выполнение программ PIVSQL байт-код очень похож на объектный файл, который, как аы помните, имеет расширение .о. Вам может встретиться (например, в сценарии создания базы данных sql.bsq) упоминание и о третьем варианте откомпилированного кода PL/SQL, называе- называемом переносимым р-кодом, но Oracle такой код уже не генерирует. Однако про- пространство для хранения переносимого р-кода по-прежнему используется в про- процессе работы с символической информацией, генерируемой компилятором PL/SQL при обработке программы с параметром DEBUG. Вы, вероятно, уже знаете, что компилятор PL/SQL не запускается программи- программистом, как компилятор С или даже Java. Система Oracle вызывает его сама, когда это необходимо. Фактически начинающий программист PL/SQL может даже не знать о его существовании! Каждый раз, когда вы создаете программу PL/SQL с помощью инструкции CREATE PROCEDURE, Oracle запускает для нее компилятор. Ниже мы расскажем о том, каким образом происходит в Oracle перекомпиляция хранимого кода после каждого изменения объектов, от которых он зависит. Хотя Oracle и «скрывает» наличие компилятора, хороший программист всегда знает, что происходит внутри системы. С процессом выполнения откомпилиро- откомпилированного кода PL/SQL связаны два основных понятия. О Исполняющее ядро PL/SQL (виртуальная машина PL/SQL). Так называется компонент Oracle, выполняющий байт-код программ PL/SQL, который при необходимости вызывает серверное ядро SQL и возвращает результаты вызы- вызывающей среде. В таких клиентских приложениях, как Oracle Forms, испол- исполняющее ядро обычно открывает сеанс подключения к удаленной базе данных и взаимодействует с ядром SQL через сетевой протокол. О Сеанс (Oracle). Относительно сервера PL/SQL-сеансом называются процесс и пространство памяти, связанные с аутентифицированным пользователем че- через сеть или межпроцессное соединение. Каждый сеанс имеет собственную об- область памяти, где хранятся данные выполняющейся программы. Хотя многие полагают, что сеанс начинается в момент входа пользователя в систему, а за- заканчивается при выходе из нее, существуют и рекурсивные сеансы, для которых аутентификация не выполняется — Oracle использует их как временное сред- средство изменения идентификационных данных пользователя. Например, в рекур- рекурсивном сеансе Oracle всегда выполняет программы с правами создателя. Рассмотрим несколько способов реализации простейшей программы из попу- популярной клиентской среды SQL'Plus. Это прекрасный пример приложения, ори- ориентированного на выполнение в рамках сеанса и предоставляющего доступ к ок- окружению PL/SQL, которое входит в состав сервера базы данных. (Об SQL*Plus и его использовании для выполнения кода PL/SQL рассказывается в главе 2.) Конечно, при желании сервер можно вызывать из других приложений, например из клиентских программ Oracle, и даже из таких языков, как Perl, С или Java. Но все происходящее на сервере не зависит от клиентского окружения. Для выполнения кода PL/SQL из SQL*Plus нужен анонимный блок верхнего уровня. Вы, вероятно, знаете, что команда EXECUTE в SQL'Plus преобразует вызов программы в анонимный блок, но известно ли вам, что SQL-инструкция CALL тоже генерирует упрощенный вариант анонимного блока? По сути дела, прежде
Заглянем внутрь 743 чем в ОгаскЭг появилась возможность непосредственного вызова PL/SQL из SQL, все такого рода вызовы производились с использованием анонимных блоков. Рассмотрим простейший анонимный блок: BEGIN NULL; END: Что происходит при его отправке серверу Oracle? Для того чтобы ответить на данный вопрос, обратимся к рис. 20.1. SQL*Plus SOL> BEGIN 2 NULL; 3 END; ¦"" 4 / PL/SQL procedure successfully complete?. SQL> BEGIN NULL: END: Сервер РСУБД Oracle i Компилятор! Исполняемый код ¦¦"+¦*¦ pi /sol .1 '¦' PUSQL Исполняющее - | Код успвшногазавершеиия или сообщение об ошибке .ЛДР0 "USOL ,;, Рис. 20.1. Пример анонимного блока, не выполняющего никаких действий Проанализируем представленный на этом рисунке процесс. 1. Пользователь построчно вводит блок, а затем дает PL/SQL команду «начать» (косая черта). Как следует из рисунка, SQL*Plus отправляет весь блок кода, за исключением косой черты, серверу. Передача осуществляется через соедине- соединение, установленное для текущего сеанса (например, через Oracle Net). 2. Компилятор PL/SQL пытается откомпилировать полученный анонимный блок и сгенерировать внутренние структуры данных (в том числе DIANA-код), не- необходимые для анализа кода и генерирования байт-кода1. На первом этапе компилятор проверяет синтаксис, чтобы убедиться, что программа соответст- соответствует грамматике языка. Код рассматриваемого примера не содержит ни одного идентификатора, а только ключевые слова языка. Если компиляция проходит успешно, Oracle помещает байт-код блока в общую область памяти; в против- противном случае компилятор возвращает сообщения об ошибках сеансу SQL'Plus. 3. Исполняющее ядро PL/SQL интерпретирует байт-код и возвращает сеансу SQL'Plus код, свидетельствующий об успешном или неудачном завершении сеанса. Добавим в анонимный блок SQL-запрос и посмотрим, как изменится процесс его выполнения. На рис. 20.2 показаны некоторые компоненты сервера Oracle, которые участвуют в выполнении SQL-инструкций. Если Oracle уже откомпилировала такой же код данного сеанса, вполне вероятно, что повторная компиляция не понадобится. Дело в том, что результаты столь дорогостоящей операции, как ком- компиляция, сервер кэширует в памяти и старается использовать повторно.
744 Глава 20 • Выполнение программ PL/SQL SQL'Plus SQL> DECLARE 2 str VARCHAR(l); 3 BEGIN 4 SELECT dummy INTO str 5 FROM DUAL: 6 END: 7 / PL/SQL procedur completed. SQL> Код успешного завершения или сообщение об ошибке DECLWE Str VARCHARC1): BEGIN SELECT durnny INTO str FROM DUAL: END; Сервер РСУБД Oracle Запросы на выполнение Компилятор '¦ PL/SQL | \ Исполняеиьп код i (байт-код) Исполняющее ядро PUSQL Полностью разрешенные имена и вызовы идентификаторов PUSQL SELECT duimy FROM DUAL План выполнения SQL-шструхции Исполняющее i ядро PUSQL 3 Результаты выполнения SQL-инструкции Рис 20.2. Этапы выполнения анонимного блока, содержащего SQL-инструкцию В этом примере значения столбца извлекаются из известной вам встроенной таблицы DUAL. Такое незначительное изменение связано с множеством новых кон- концепций. Убедившись, что код PL/SQL ке содержит синтаксических ошибок, компиля- компилятор PL/SQL передает SQL-инструкцию синтаксическому анализатору SQL (син- (синтаксический анализ является первой стадией компиляции). Анализатор проверяет синтаксис инструкции, после чего преобразует разрешенные имена в соответст- соответствующие адреса (разрешение имен). На этой стадии он определяет, что представ- представляет собой каждый идентификатор (не ключевое слово). В нашем примере SQL- инструкция содержит идентификаторы DUAL, dummy и str. Анализатор выяснит, что DUAL — это имя таблицы, a dummy — ее столбец. Что касается идентификатора str, то он будет возвращен компилятору PL/SQL для проверки разрешения имен.
Заглянем внутрь 745 Кроме того, анализатор SQL, проверив разрешения, должен убедиться, что исход- исходный сеанс имеет полномочия выполнять вызываемую SQL-операцию. ПРИМЕЧАНИЕ- — В Oracle9i для языков PL/SQL и SQL используется общий анализатор SQL. В ранних версиях РСУБД у PL/SQL бып собственный анализатор SQL, но это привело к некоторым разногласиям между SQL, выполнявшимся интерпретатором команд, и SQL, вызываемым из программ PL/SQL. Например, в Orade 8.1.7 компилятор SQL в PL/SQL не знал о новой функции NVL2. Поэтому оператор SELECT NVL2(NULL, 1,1) FROM DUAL; можно было выполнить из командной строки, но a PL/SQL он выдавал ошибку «PLS-00201: identifier WL21 must be declared». Хотелось бы напомнить, что исполняемый байт-код PL/SQL содержит не толь- только двоичный код программной логики, но и все статические SQL-инструкции в текстовой форме. Обычно PL/SQL слегка модифицирует SQL-код, удаляя пред- предложения INTO, подставляет вместо переменных привязки локальные переменные программы и преобразует первое ключевое слово SQL-инструкции (SELECT, UPDATE и т. п.) в символы верхнего регистра. Поэтому если, к примеру, myvar является ло- локальной переменной программы, PL/SQL преобразует инструкцию select duimiy Into str from dual where x » myvar в инструкцию select dunmy from dual where x - :bl Но возвратимся к нашему примеру. После всех проверок на этапе компиля- компиляции Oracle переводит анонимный блок на этап выполнения. Компилятор еще раз проверяет синтаксис, разрешение имен, семантику и привилегии, после чего гене- генерирует наиболее эффективный, с его точки зрения, план выполнения инструк- инструкции — в нашем примере это сделать совсем не сложно. Готовый план он направ- направляет исполняющему ядру SQL, которое производит выборку строк и передает результаты обратно PL/SQL. А что произойдет, если анонимный блок вызовет хранимую программу? По- Подобная ситуация проиллюстрирована на рис. 20.3. Для упрощения картины SQL- ивструкция удалена из блока. На этот раз компилятору нужно разрешить внешнюю ссылку foo, чтобы выяс- выяснить, что она указывает на программу, которую разрешено выполнять данному пользователю. Кроме того, ему требуется DIANA-представление процедуры foo, позволяющее определить, что анонимный блок вызывает ее правильно. Обратите внимание на одно из преимуществ хранимого кода: поскольку и DIANA-пред- DIANA-представление и байт-код уже хранятся в словаре данных, Oracle не приходится тра- тратить время на перекомпиляцию процедуры. И более того, как только код прочи- прочитан с диска, Oracle записывает его в область памяти, называемую библиотечным кэшем, устраняя тем самым необходимость его повторного считывания с диска при последующих вызовах. После первоначальной компиляции (если зависимости, требующие автомати- автоматической перекомпиляции, не изменяются) для выполнения программы Oracle тре- требуется только ее байт-код. Иными словами, если программа foo вызовет програм- программу bar, Oracle не нужно считывать DIANA-представление bar, благодаря чему экономится время на операциях ввода-вывода и пространство памяти в библио- библиотечном кэше.
746 Глава 20 • Выполнение программ PL/SQL SQt/Pka SQt> BEGIN 2 fao; 3 END: 4 / "¦" """ PL/SQL procedure successfully completed. SQL> f 1 BEGIN F00: END: t i (fed успешного заввршжя или сообщение об ошибке Сервер РСУБД Oracle | Компилятор? ft»(DIANA) ' '** PL/SQL i* Словарь ! ч—ху- ¦ ¦ -ь данных j Анонимный блек Orade ! (байт-иод) j : I i . * I Исполняющее \ ц» (Вайянкх)) 1 «poPUSQL ¦*« -- ; Рис 20.3. Выполнение программы, вызывающей хранимую процедуру > Предположим, что хранимая процедура включает длинную инструкцию SE- SELECT. Поскольку перед записью программы PL/SQL в словарь данных Oracle ее компилирует, можно было бы предположить, что будет откомпилирована и вхо- входящая в состав программы SQL-инструкция. Но не тут то было. Oracle поступает с SQL-инструкциями в хранимых процедурах точно так же, как и в анонимных блоках, то есть сохраняет их в текстовой форме внутри байт-кода программы и ком- компилирует во время выполнения. Однако во время выполнения программы PL/SQL делает все возможное для того, чтобы избежать мягкого синтаксического анализа строки запроса (этим тер- термином принято называть осуществляемое Oracle преобразование строки запроса в откомпилированную форму в памяти). При выполнении SQL-инструкции из такого приложения, как SQL'Plus, мягкого синтаксического анализа не избежать. Поэтому для ускорения анализа SQL-запроса достаточно поместить ее в блок PL/SQL. В некоторых других приложениях мягкий анализ тоже можно не произ- производить, но даже низкоуровневый С-интерфейс, известный как Oracle Call Inter- Interface (OCI), имеет коммуникационные издержки (сетевые или IPC), не свойст- свойственные хранимому коду PL/SQL. В завершение давайте рассмотрим встроенную компиляцию — процесс, кото- который правильнее было бы назвать встроенным выполнением. Эта функция Oracle9t позволяет перевести серверный код PL/SQL на С, выполнить его компиляцию и связывание и получить совместно используемый объектный файл. Такой код обычно обрабатывается быстрее, чем обычный откомпилированный PL/SQL-код, если только программа большую часть времени не занимается выполнением SQL- инструкций. При наличии процедуры с именем bar, откомпилированной таким образом, Oracle хранит ее новую версию в операционной системе (рис. 20.4). В данном случае компилятор генерирует обычный код DIANA, но без байт-ко- байт-кода. Вместо него в словарь данных и структуры библиотечного кэша записывают- записываются имя и информация о местоположении объектного файла. Вызываемая про- программа, хранящаяся в файле совместно используемой библиотеки, не содержит
Заглянем внутрь 747 интерпретируемого байт-кода; для ее динамической загрузки и выполнения Oracle осуществляет вызов на уровне операционной системы. SQL*Plus SQL> BEGIN Z bar: 3 END: 4 / PL/SQL procedure successfully completed. SQL> 4 Компилятор " PL/SQL f" Сервер РСУБД Oracle ! Анонимный Впак (бзйпыод) Словарь данных Oracle Код успешного завершения или сообщение об ошибке Исполняющее ядро PL/SQL Информация вызов ¦_ Совместно используемая библиотека Файл .so или .DLL Результаты и информация : об успешной либо неудачном вызове ..,.,:,...: Рис 20.4. Выполнение анонимного блока, вызывающего программу Существует много других способов генерации хранимого кода и его вызова из PL/SQL. Во-первых, это можно делать с помощью хранимых процедур Java. Стандартная установка сервера Oracle включает не только виртуальную машину PL/SQL, но и виртуальную машину Java. Вам же достаточно написать специфи- спецификацию вызова PL/SQL, логика которого будет реализована в виде статического Java-класса. Во-вторых, можно воспользоваться внешними процедурами. Испол- Исполняемую часть подпрограммы PL/SQL можно реализовать в виде пользователь- пользовательского кода С, и Oracle будет выполнять этот код как независимый от сервера Oracle процесс в выделенном ему пространстве памяти. Мы расскажем об обоих подходах несколько позже, в специально посвященных им главах. Хранение PL/SQL-кода на сервере Теперь, когда вам известно, какую роль играют различные компоненты в процес- процессах компиляции и выполнения кода PL/SQL, давайте разберемся, как различные варианты программного кода хранятся на диске. Возможные места его хранения на сервере базы данных Oracle перечислены в табл. 20.1.
748 Глава 20 • Выполнение программ PL/SQL Таблица 20.1. Хранение серверного кода PL/SQL и метаданных Месторасположе- Месторасположение (все таблицы принадлежат SYS) Имя представления. Использующиеся обычно используемое объекты PL/SQL разработчиками приложений Наиболее полезное содержимое USER OBJECTS Все, кроме анонимных Имена, объектные блоков типы,время выполнения последней DDL-инструкции, состояние компиляции (VALID или INVALID) SOURCE* TRIGGER* ERROR$ DEPENDENCY* SETTINGS* USER_SOURCE USERJTRIGGERS USER_ERRORS USER_DEPENDENCIES Все, кроме триггеров и анонимных блоков Триггеры Исходный код Исходный ход, описание события, к которому присоединён триггер Последняя ошибка Все, кроме анонимных блоков любой хранимой программы PL/SQL (включая триггеры) Все, кроме анонимных Иерархия объектных блоков зависимостей USER_STORED_SETTINGS Все, кроме анонимных Флаги компилятора блоков PL/SQL IDL UB1* USER OBJECT SIZE IDL CHAR* IDL UB2* IDL_S84* Каталог операционной системы, заданный в параметре базы данных PLSQL NATIVE LJBRARY_DIR Все, кроме анонимных блоков Откомпилированные встроенным компилятором Внутреннее представление DIANA- и байт-кода, а также отладочных данных; используется компилятором и исполняющим ядром Совместно используемые объектные файлы. полученные путем компиляции программ PL/SQL через С С помощью указанных выше представлений можно, в частности, выполнить следующие действия: О определить, какие объекты находятся в состоянии INVALID (USER_OBJECTS); О установить, какие программы откомпилированы путем встроенной компиля- компиляции (USER_STORE0_ SETTINGS); О выясгапъ, какие локальные объекты стали недействительными после измене- изменения в конкретной программе PL/SQL (USER_DEPENDENCIES);
Заглянем внутрь 749 О восстановить исходный код программы, если отсутствует сценарий, с помо- помощью которого она была создана (USERJ5OURCE, USER_TRIGGERS); О определить размер хранимых программ (USER_QBJECT_SIZE). На последней возможности стоит остановиться подробнее. Обратившись к пред- представлению USER_OBJECT_SIZE, можно узнать размер каждой части хранимой про- программы. Столбцы этого представления содержат информацию о количестве бай- байтов, занимаемых на диске определенным кодом или данными определенного типа: О столбец SOURCE_SIZE - исходный код; О столбец PARSEDJIZE - DIANA-код; О столбец CODESIZE - байт-код, а также информация полученная в процессе от- отладки (сохраняется только в том случае, если программа откомпилирована с указанием параметра DEBUG); О столбец ERRORSIZE - текст сообщений об ошибках (если ошибки отсутствуют, содержимое столбца равно 0). Правда, эта информация не так полезна, как может показаться на первый взгляд. Например, когда Oracle помещает в таблицы словаря данных DIANA-код, она за- записывает его не совсем в той форме, в какой он находится в памяти. На диске он записан в виде сжатого линейного списка, а в памяти разворачивается в древооб- древообразную структуру. Рано или поздно вам потребуется выяснить, на что именно па- память израсходуется во время выполнения программ; некоторые сведения по этой важной теме вы найдете ниже, в разделе «Как PL/SQL использует память серве- сервера Oracle». Однако информация, возвращаемая представлением USER_OBJECT_SIZE, подтверждает один важный факт: Oracle не хранит DIANA-код для тела пакета. Значение столбца PARSED_SIZE для тела пакета всегда равно нулю. ПРИМЕЧАНИЕ- Поскольку Orade не хранит DIANA-код для тела пакета, помещение PL/SQL-программ в пакеты в ви- виде функций, процедур или типов данных верхнего уровня способствует уменьшению общей нагруз- нагрузки на сервер. А нужен ли Oracle вообще DIANA-код тела пакета? Да, нужен, но только на этапе компиляции, поскольку компилятор использует его для генерации байт-ко- байт-кода, а также для проверки соответствия сигнатур, имеющихся в теле пакета, сигна- сигнатурам в его спецификации. Поскольку тело пакета хранимым объектам теперь не нужно, Oracle удаляет его DIANA-код. А если впоследствии тело пакета потребу- потребуется перекомпилировать (например, из-за изменений в объектах, от которых оно зависит), компилятор всегда сможет заново сгенерировать DIANA-код, повторно проверить типы данных, еще раз сгенерировать байт-код и удалить DIANA-код. Даже если бы Oracle сохраняла старый DIANA-код, для перекомпиляции он был бы бесполезным. Мы, конечно же, не утверждаем, что все без разбора компоненты приложения должны помещаться в пакеты. Такие преимущества, как простота разработки, мо- могут быть важнее стремления максимально повысить производительность и снизить нагрузку на сервер базы данных. В качестве примера можно упомянуть о компо- компоненте Oracle PL/SQL Server Pages (PSP), который генерирует процедуры PL/SQL
750 Глава 20 • Выполнение программ PL/SQL верхнего уровня на основе HTML-файлов . Но в целом чем больше объем кода приложения, тем существеннее преимущества от помещения его в пакеты. Слишком большой DIANA-код При компиляции очень большой программы PL/SQL от сервера можно получить сообщение об ошибке PLS-00123: program too 1 arge, а от PL/SQL с клиентской сто- стороны — сообщение CDI-11005: (SQL execution error). Это означает, что при попыт- попытке создать структуру DIANA-кода компилятор вышел за пределы максимально допустимого количества узлов дерева. В таком случае проще всего разбить про- программу не несколько подпрограмм. Но поступать подобным образом не всегда це- целесообразно. Трудно предсказать, какая структура потребуется для реализации программы, поскольку не каждый ее узел соответствует какой-либо единице исходного кода, например, токену или строке кода. Верхние границы количества узлов для серве- серверов разных версий и программ PL/SQL различных типов приведены в табл. 20.2. Хотя крайний справа столбец и дает приблизительное представление о размере исходного кода, не воспринимайте указанные в нем значения буквально. Эти оценки основаны на «типичном» коде (в среднем по четыре байта на один узел DIANA-кода), но ваш код может оказаться более сложным. Таблица 20.2. Верхние границы размеров Программ PL/SQL , Тип кода PL/SQL Версия Максимальный размер Оценка максимального сервера дерева DIANA-кода размера исходного (количество узлов) кода в байтах Тело пакета или типа 7.3 21* A6 384) 64 К данных, отдельная 8.0.x и выше 215 C2768) 128 К функция или процедура 8.1.5 и выше 2м F7 108 864) 256 М Сигнатура (заголовок) 7.3 214 64 К отдельной функции или 8.0.x и выше 2й 126 К процедуры Спецификация пакета или 7.3 2W (теоретически) 64 К (теоретически) типа данных, анонимный 8.1.6. и ниже 2а (теоретически), 128 К (теоретически), блок 8.1.7 и выше от 213 до 214 (на практике) от 32 К до 64 К 2а (на практике) 128_К Из таблицы видно, что компания Oracle постоянно работает над увеличением предельного размера программ. В последних версиях выход за эти пределы уже маловероятен. У компилятора PL/SQL имеются и другие ограничения, касающиеся степени сложности кода, в частности максимального количества уровней вложенности блоков B55) и максимального количества передаваемых процедуре или функции параметров F5 536). Некоторые из них могут стать причиной возникновения Наверняка эти процедуры тоже можно было бы поместить в пакеты, но если вы ведете разработку в режиме «интернет-времевиэ, такая дополнительная работа, бывает, просто не вписывается в ваш график.
Управление Зависимостями . 751 проблем. Полный список таких ограничений приведен в приложении к офици- официальной документации PL/SQJ. User's Guide and Reference. Управление зависимостями Еще одной важной фазой компиляции и выполнения PL/SQL-программы явля- является проверка ее зависимостей. Зависимость в PL/SQL — вид связи между про- программой и некоторым объектом Oracle вне этой программы. Серверные програм- программы PL/SQL могут зависеть от таблиц, представлений, типов данных, процедур, функций, последовательностей и спецификаций пакетов, но не от их тела пакетов или тела типов данных. Клиентские программы PL/SQL могут иметь дополни- дополнительные зависимости от таких существующих лишь в клиентских модулях эле- элементов, как поля формы. Основная цель проверки зависимостей в PL/SQL - не допустить выполнения программы, если хоть один из объектов, от которых она зависит, изменился со времени ее последней компиляции. Управление зависимостями производится автоматически: система Oracle сама отслеживает программные компоненты и осуществляет их перекомпиляцию, ко- когда это необходимо. Тем не менее некоторая ответственность за синхронизацию кода лежит на программистах, и в следующих разделах рассказывается, как, когда и для чего им нужно вмешиваться в данный процесс. Зависимости в серверном PL/SQL Если вы работаете с серверными программами PL/SQL, для получения инфор- информации о зависимостях можно обращаться к словарю данных. Ниже показано, как получить из него сведения о происходящем в базе данных. Предположим, что на сервере имеется пакет bookworm, а в нем функция, извлекающая данные из табли- таблицы books. Если сначала была создана таблица, а затем пакет, в словаре данных должна содержаться следующая информация: SQL> SELECT object_name, object_type. status 2 FROM USER.OBJECTS 3 WHERE objectjiame - 'BOOKWORM': OBJECT NAME OBJECT TYPE STATUS BOOKWORM PACKAGE VALID BOOKWORM PACKAGE BODY VALID Это означает, что существуют два объекта с именем BOOKWORM: спецификация пакета и его тело. И оба они находятся в состоянии VALID, При компиляции пакета Oracle с помощью DIANA-кода формирует список других объектов, необходимых для успешной компиляции пакета BOOKWORM. Все зависимости объектов можно исследовать, выполнив дорогостоящий (то есть мед- медленный) запрос к представлению из словаря данных USER_D?PENOENCIES: SQL> SELECT паве. type, referencedname, referenced type 2 FROM USER DEPENDENCIES
752 Глава 20 • Выполнение программ PL/SQL WHERE naie - 'B00KWORH'; NAME TYPE REFERNCED NAME RcFERNCED TYPE BOOKWORM PACKAGE STANDARD PACKAGE BOOKWORM PACKAGE BODY STANDARD PACKAGE BOOKWORM PACKAGE BODY BOOKS TABLE BOOKWORM PACKAGE BODY BOOKWORM PACKAGE На рис. 20.5 этаже информация представлена в виде ориентированного графа, в котором стрелки обозначают отношения типа «зависит от». Спецификация пакета STANDARD Таблица books Спецификация пакета bookworm Л Тело пакета bookworm Рис. 20.5. Граф зависимостей пакета bookworm Иными словами, на рис. 20.5 показано, что спецификация и тело пакета book- bookworm зависят от встроенного пакета с именем STANOARO (см. врезку «Пакет STAN- STANDARD»), а тело пакета bookworm — от его же спецификации и от таблицы books. Для отслеживания зависимостей Oracle записывает в словарь данных тело и спе- спецификацию пакета как два разных элемента. Тело любого пакета зависит от его спецификации, но спецификация никогда не зависит от тела. От тела пакета не зависит ни один объект — собственно говоря, его может и вовсе не быть. Если вам приходилось достаточно долго заниматься сопровождением програм- программного обеспечения, вы знаете, что при подобном анализе учитываются не столько зависимости, сколько ссылки одних объектов на другие. Предположим, мне нуж- нужно изменить структуру таблицы books. Прежде всего необходимо выяснить, на ка- какие из объектов это может повлиять: SQL» SELECT name. type. 2 FROM USER_DEPENDENCIES 3 WHERE referenced_name - 'BOOKS' 4 AND referencedtype - 'TABLE': NAME TYPE ADDJOOK TEST_800K BOOK BOOKWORM FORMSTEST PPOCEDURE PACKAGE BODY PACKAGE BODY PACKAGE BODY PACKAGE
Управление зависимостями 753 Как видите, в приведенной выше схеме помимо пакета bookworm имеются дру- другие программы, о которых ранее не упоминалось. Но Oracle ничего не забывает. И хотя Oracle очень тщательно контролирует зависимости, все их предусмот- предусмотреть невозможно. РСУБД фиксирует локальные хранимые объекты, ссылки на которые оформлены в виде статических вызовов. Но существует множество спо- способов создания программ, которых не будет в представлении USER_DEPENDENCIES. Речь идет о внешних программах с интегрированным кодом SQL или PL/SQL, удаленных хранимых процедурах, клиентских программах, вызывающих локаль- локальные хранимые объекты, и, наконец, о локальных программах, в которых исполь- используется динамический SQL. Итак, попробуем изменить структуру таблицы books, добавив в нее один но- новый столбец: ALTER TABLE books ADO popuiarityjndex NUMBER; Oracle сразу же автоматически пометит все ссылки на таблицу books как не- недействительные (INVALID). При любом изменении определения объекта, даже при его перекомпиляции без внесения изменений, все ссылки на объект также будут помечены как недействительные (см. ниже врезку «Перевод объектов в недейст- недействительное состояние»). Но на практике этот процесс еще более сложный. Если вы являетесь владельцем программы, выполняющей заданную DML-инструкцию, которая содержит ссылку на таблицу другой схемы, то после лишения вас приви- привилегии на вьшолнение указанной инструкции, вся программа будет помечена как неработоспособная. Вот что можно получить после изменения структуры таблицы: SQL> SELECT objectjiame, objecttype. status 2 FROM USER08JECTS 3 WHERE objectjiame = 'INVALID'; OBJECTJIAME ADD BOOK BOOK BOOKWORM R3RMSTEST FORMSTEST TESTBOOK OBJECTIVE PPOCEDURE PACKAGE BODY PACKAGE BODY PACKAGE PACKAGE BODY PACKAGE 800Y STATUS INVALID INVALID INVALID INVALID INVALID INVALID Данный пример иллюстрирует преимущества организации пакетов в виде двух частей: тело пакета оказалось в этом списке помечено как INVALID, а его специфи- спецификация в полном порядке. И это замечательно. Ведь если бы спецификация тоже была помечена как недействительная, таковыми стали бы и все зависящие от нее объекты. 1РИМЕЧАНИЕ- Использование пакетов позволяет разорвать цепь зависимостей и избавляет от необходимости осу- осуществления перекомпиляций.
754 Глава 20 • Выполнение программ PL/SQL ПАКЕТ STANDARD В любой стандартной инсталляции Oracle имеется встроенный пакет STAN- STANDARD, который содержит множество базовых элементов языка PL/SQL, в том числе: О функции, например INSTR и LOWER; О операторы сравнения, такие как NOT, = (равно) и > (больше); О предопределенные исключения, в частности DUPVALON INDEX И VALUE_ERROR; О подтипы, например STRING и INTEGER. Если вы хотите ознакомиться с исходным кодом пакета, откройте файл standard.sql, обычно находящийся в каталоге $ORACLE_HOME/rdbms/admin. Спецификация пакета STANDARD представляет собой корневой узел графа зависимостей PL/SQL, то есть она не зависит от других программ PL/SQL (но большая часть программ PL/SQL зависит от нее). Если вы переком- перекомпилируете данную спецификацию, Oracle пометит практически все ссыл- ссылки на программы PL/SQL как недействительные. В состав клиентской среды PL/SQL входит и версия пакета STANDARD, ос- основные компоненты которой такие же, как у серверной версии. Кроме того, имеются расширения этого пакета, включающие программы, специфиче- специфические для данного приложения, в том числе CALL_FORM. Еще одним средством просмотра программных зависимостей является проце- процедура Oracle DEPTREEFILL, используемая в сочетании с представлениями DEPTREE и IDEPTREE. Вызовите эту процедуру, предположим, с такими параметрами: SQL> EXEC DEPTREEFILU 'TABLE1. 'SCOTT'. 'BOOKS1) После того как вы выполните запрос к представлению IDEPTREE, будет получен следующий листинг; SOL> SELECT * FROM IDEPTREE-. DEPE№)ENC1?S TABLE SCOTT.BOOKS PROCEDURE SCOTT.ADD_BOOK PROCEDURE BODY SCOTTJOOK PACKAGE BODY SCOTT.TEST_BOOK PACKAGE BODY SCOTT.BOOKWORM PACKAGE SCOTT.FORMSTEST PACKAGE BODY SCOTT.FORMSTEST Листинг содержит результаты выполнения рекурсивного запроса, просматри- просматривающего все ссылки объекта. Чтобы их получить, запустите сценарий utidtree.sql (из каталога rdbms/admin) и создайте процедуру и представления в своей схеме. Но вы можете эмулировать его также с помощью другого запроса: SELECT RPAD С ', 3*{LEVEL-D) || name 11 ' С || type || ') ' FROM user_dependencies
Управление зависимостями 755 CONNECT BY PRIOR RTRMname I \ type) - RTRIM(reference<J_nanie j | referenced_type) START WITH referencedjmme = 'name' AND referencedtype - 'type'; Теперь, когда вы знаете, как сервер отслеживает связи между объектами, да- давайте посмотрим, что Oracle делает с такого рода информацией. Восстановление работоспособности Программа может стать неработоспособной либо в результате изменения объек- объектов, на которые она ссылается, либо в результате неудачной компиляции. В лю- любом случае такую программу нельзя будет выполнить, пока успешная перекомпи- перекомпиляция не восстановит ее. Это можно сделать тремя способами: О вручную, выполнив перекомпиляцию пакета с помощью команды ALTER; О с помощью сценария, то есть программы, которая ищет недействительные па- пакеты и вызывает для них команду ALTER; О автоматически, положившись на встроенные правила перекомпиляции Oracle. Перекомпиляция вручную Этот метод прост, но утомителен. Предположим, вы заглянули в словарь данных и выяснили, что в перекомпиляции нуждаются три хранимые программы: ALTER PACKAGE bookworm COMPILE BODY; ALTER PACKAGE book COMPILE BODY: ALTER PROCEDURE add_book COMPILE: Однако не всегда простой перекомпиляции может быть достаточно. Если из- изменилась сигнатура вызываемой программы,, сначала придется отредактировать вызывающий код! Перекомпиляция с помощью сценария Существует несколько сценариев, облегчающих выполнение этой задачи. Собст- Собственный сценарий Oracle, предназначенный для данной цели, называется utlrp.sql. Он находится все в том же подкаталоге rdbms/admfn и прекрасно работает, но для его запуска требуется иметь привилегии администратора базы данных. В общем случае сценарии этого типа недостаточно интеллектуальны, чтобы при переком- перекомпиляции учитывать зависимость между объектами. Они просто проверяют список неработоспособных программ и поочередно их компилируют; затем снова прове- проверяют список и повторяют данную операцию до тех пор, пока все программы не окажутся работоспособными или пока не будут перекомпилированы все необхо- необходимые программы. Причем заголовки пакетов они компилируют до компиляции тела пакета. Если вам нужно перекомпилировать большое количество программ и ресурсы машины (процессор, память, диск) позволяют это сделать, попробуйте восполь- воспользоваться пакетом Oracle LTLRECOMP (utlrcmp.sql), с помощью которого можно вы- выполнять перекомпиляцию программ параллельно, помещая соответствующие за- запросы в очередь заданий Oracle. Однако даже сама система предупреждает, что пакет не в состоянии в значительной степени ускорить перекомпиляцию из-за су- существующей конкуренции при записи в системные таблицы.
756 Глава 20 • Выполнение программ PL/SQL ПРИМЕЧАНИЕ Соломон Якобсон (Solomon Yakobson) предложил утилиту перекомпиляции, которая обрабатывает объекты с учетом существующих между ними зависимостей. Этот сценарий под заголовком «А тоге sophisticated Recompile Utility» содержится на web-узле Quest Pipelines, в архиве PL/SQL, располо- расположенном ло адресу http://www.quest-pipelines.com/PIpellnes/PLSQt7archlves.com. Вы найдете его так- такм«дет ПЕРЕВОД ОБЪЕКТОВ В НЕРАБОТОСПОСОБНОЕ СОСТОЯНИЕ Если время создания объекта базы данных меняется, Oracle, как правило, сразу помечает все зависимые от него объекты локальной базы данных как неработоспособные. Однако в версии 8.1.7.3 появилась возможность сообщить РСУБД, что вы хотите пометить зависимые объекты как нера- неработоспособные лишь в том случае, если исходный код объекта действи- действительно изменился. Однако имейте в виду, что речь идет о недокументированной и неподдер- неподдерживаемой функции, и пользователям можно ее применять только в том случае, если это им рекомендовали технические службы Oracle. Иными словами, компания не несет ответственности за те негативные последст- последствия, к которым может привести ваша самодеятельность. Данная функция для текущего сеанса задается так: SQL> ALTER SESSION SET EVENTS 2 '10520 TRACE NAHE CONTEXT FOREVER. LEVEL 10': Session altered. Инсталлируйте новый код и запустите сценарии перекомпиляции, напри- например $ORACLE_HOME/rdbms/admin/utlrp.sql, затем отключите указанную функ- функцию: SQL> ALTER SESSION SET EVENTS 2 '10520 TRACE NAHE CONTEXT OFF': Session altered. Описанная функция разработана по инициативе Oracle и призвана уско- ускорить процесс внесения исправлений и обновление базы данных. Для дру- других целей она не предназначена. Автоматическая перекомпиляция Третьим методом перекомпиляции, согласно стратегии Oracle, является автома- автоматическая перекомпиляция программ. Этот метод проще других, но имеет опреде- определенные недостатки. Если вы захотите выполнить одну из программ, реализован- реализованных в теле пакета bookworm, Oracle сначала перекомпилирует его. При эксплуатации в промышленных условиях обычно действуют жесткие пра- правила относительно того, когда можно осуществлять перекомпиляцию программ. Установка новой версии программы PL/SQL может потребовать выполнения мно- множества перекомпиляций, что отразиться на активных программах. Возможно, чтобы найти и перекомпилировать все неработоспособные объекты, вы захотите
Управление зависимостями 757 выполнить сценарий, подобный utdrp.sql. Администратор базы данных должен по- помочь вам составить график вызова процедуры обновления, включающей запуск сценария перекомпиляции. Помимо снижения производительности одним из самых больших недостатков автоматической перекомпиляции программ является то, что она может влиять на выполняющиеся в данный момент пакеты. В результате можно получить такую серию сообщений: ORA-04068: existing state of packages nas been discarded ORA-04061: existing state of package "SCOTT.PI" has been invalidated ORA-04065: not executed, altered or dropped package "SCOTT.PI" ORA-06508: PL/SQL: could not find program unit being called Объясняется это тем, что активный в данный момент пакет стал неработоспо- неработоспособным из-за автоматической перекомпиляции. Более того, поскольку Oracle по- позволяет совместно использовать один и тот же байт-код в нескольких сеансах, сбой произойдет и во всех остальных сеансах, в которых выполняется данный па- пакет. Последствия обычно бывают самими разнообразными. О Выполнение программы прерывается сообщением об ошибке 0RA-04068 (если только программы не включают сложные обработчики ошибок). О Всем общим и личным переменным пакетов присваиваются значения по умол- умолчанию; если такое значение явно не задано, переменная принимает значение NULL. Сказанное относится ко всем пакетам, выполняемым в данном сеансе, а не только к тому, который подлежит перекомпиляции. Следовательно, если в этот момент производятся вычисления с использованием переменных паке- пакетов, содержимое переменных будет утеряно. О Пакет DBMS_OUTPUT прекращает работу, но лишь в том случае, если он ранее на- находился в режиме выполнения. Объясняется это тем, что переключатель паке- пакета устанавливается в состояние по умолчанию off. Поскольку пакеты, выполняемые в текущем сеансе, возвращаются в исходное состояние, делу поможет простое повторение вызова; иными словами, повторный вызов не повлечет за собой ошибку ORA-04068. Автор книги написал множество сценариев перекомпиляции, и при их провер- проверке столкнулся со странными результатами. В частности, каждые два из десяти за- запусков одного и того же сценария завершались сообщениями об ошибке. Кроме того, было замечено, что перекомпиляция пакетов с помощью явной инструкции ALTER.. COMPILE снижает «волновой» эффект; похоже, что вместо возврата в ис- исходное состояние всех пакетов текущего сеанса она переводит в таковое лишь ком- компилируемый пакет. На основании изложенного можно сделать следующие выводы. При промыш- промышленной эксплуатации системы не следует делать ничего, что может повлечь за со- собой перевод в неработоспособное состояние или перекомпиляцию (автоматиче- (автоматическую либо выполняемую вручную) любых хранимых объектов в текущих сеансах. Используйте сценарии перекомпиляции — на этапе разработки их применение не вызывает никаких проблем.
758 Глава 20 • Выполнение программ PL/SQL Взаимосвязи в клиентском PL/SQL В приложении, разрабатываемом на основе Oracle Forms и родственных ей прило- приложений, клиентская программа PL/SQL может зависеть от многих компонентов: О других клиентских программ PL/SQL; О серверных объектов, таких как таблицы, представления и программы PL/SQL; О элементов уровня модуля, в том числе от экранных полей в Oracle Forms; О так называемых системных переменных, поддерживаемых средой разработки. Вы можете проанализировать все эти зависимости, просмотрев элементы Re- References под именами всех программ (рис. 20.6). BObiect r«i 1OPU (Package Body) а- ^ Program L ©Q EXC" (Package Spec) t ф>?3 ЕХС* (Package Body) ?Sutcrcgrais ¦References i - LOPtl (Paclage Spec) j -EKC (Package Spec) i -PU8L1C.ILS SESSIO» ?«*KTE«S (Syimi)(«) j -SVS.«LS_S?SSIOI PAMItTERS C«ew) I -«JBLICDBHSOBFUSCATIOII ШШ1 (Sjnm)e -s»s.DeHSO8Fi)saTio«_racuiT " i -PUa.IC.UTL_WU (Synonyi) -SVS.UTL_wii (Package Spec) -PUalC-UTL SMTP (Syno)>f) i-r(S.UTl_S«rp (Package Spec) -Referenced By LAttechedLbrartes й" Object Lfranes l3 &uift-in Packages it'-aFTREE (Package Spec) ^.УА (Package 5p«) ©fflD(EC_5Ql (Packaje Spec) ?>SBORA_F=FI (Packags Spoc) ^fflJ (g p) S>fl9c«A_NlS (Package Spec) Э MT«X_RE5 (Package Spec) Т-РК1О5? (FILEHANDLE !N OUT RFKANOLE); bRFOPEN (SPEC IN VARCHAR2) R?TURN RFHSNDLE; Рис 20.6. Информация элемента References из пакета клиентской библиотеки PL/SQL Хотя в окне утилиты Object Navigator хорошо видны все внешние ссылки кли- клиентских программ, зависимости клиентских триггеров в нем не отображаются. Более того, в списке зависимостей Referenced By показаны только имена программ текущего модуля.
Управление зависимостями 759 Предположим, мы создали клиентскую библиотеку PL/SQL, которой пять раз- разработчиков пользуются в нескольких модулях Oracle Forms. Открыв библиотеку в среде разработки и щелкнув на элементе Referenced By, мы увидим список, по- подобный представленному на рис. 20.7. 3? p j-Menis Э PUSQl libraries ] $ 9 Program Uhtts ЙЕХС [Package Spet) в ЕХС Package Body) e (PockaaeSoeOe ! ^ I Q-Rfferencefl By I i !- ШР13 IPJUsge Bodyle ! - EIC (Pauage Body)S $?3LOPU (Package Bo*/>e -Attached Ubrjwies CBORAJAVA (Package Spec) ¦|SeXEC_SQl (Package Spec) ¦ffi(ORA_FFl (Package Spec) ¦tBORAJKOP (Package Spec) fflORAJAS (Package Spec) f-RFCLOSE (FiLEHAMXE Ш OUT ftFHANDLE); HRFOPEN (SPEC IN VARCHAR2) RETURN RFHAUXE; WrFREAO (RESro IN VARCHAR2, RE5TYPE IN VARCHAH2) RETLRN VARCHAR2; LRFR?AD (FILE IN RFHANOE, RE5D IN VARCHAR2, RESTYPE IN VARCHAR2) RETURN VARCHARZ; ) cNeTOOL.ENV (Package Spec) S>STE>iTJO (Package Spec) ITOOL.ERR (Package Spec) CLEAR; ¦CODE (i IN P15JNTEGER) RETURN NUMSER: •RFOPEN (SPEC !N VARCHAR2) RETURN RFHAMCHE; Рис. 20.7. В списке элемента Referenced By представлены только зависимости текущего модуля Проще говоря, мы ничего не увидим. А ведь при модификации интерфейса программы, входящей в библиотеку PL/SQL, любого из вас наверняка будет ин- интересовать, к каким последствиям это приведет, и узнать об этом вы захотите еще до того, как ваш телефон начнет разрываться от звонков пользователей. Так что придется заняться поиском самостоятельно, воспользовавшись разве что какой- нибудь примитивной утилитой вроде программы дгер для Unix. Если говорить откровенно, недостаток информации о ссылках — черта, свой- свойственная не только клиентскому PL/SQL; с подобной проблемой приходится стал- сталкиваться каждому, кто выпускает новую версию вызываемой программы. Правда, это помогает оценить полноту информации, которая имеется в словаре данных на сервере!
760 Глава 20 • Выполнение программ PL/SQL Удаленные зависимости Серверная программа PL/SQL становится неработоспособной, как только изме- изменяется код какого-либо локального объекта, от которого она зависит. Однако если такой объект находится на другом компьютере, Oracle не пытается тут же изме- изменить состояние программы. Эта задача откладывается до вызова программы, а вы- выполняется она в двух случаях: О когда клиентская программа PL/SQL, скажем, процедура в модуле Oracle Forms, вызывает хранимую программу на любом сервере базы данных; О если серверная программа производит вызов удаленной процедуры через со- соединение с базой данных, например: PROCEDURE synch era_up (tax_s1te in IN VARCHAR2. since jm IN DATE) IS BEGIN IF tax_site in = 'LONDON1 THEN reco^xite_prices?firidat.ldn.world(cutoff_ti(ne -> since in); END IF; В подобных случаях, если исполняющее ядро не будет выполнять удаленную программу, вы получите сообщение об ошибке ORA-04062 с соответствующим тек- текстом, например таким: timestamp or signature of package "SCOTT. recompute_prices" has been changed. Для того чтобы понять, как исполняющее ядро принимает подобное решение, нужно знать, что компилятор PL/SQL всегда хранит для каждой уда- удаленной процедуры временную метку и сигнатуру. О Временная метка — дата и время (с точностью до секунд) последнего измене- изменения спецификации объекта, взятые из столбца TIMESTAMP представления USER_ OBJECTS. В программах PL/SQL это не обязательно то же самое, что время по- последней компиляции, поскольку возможна перекомпиляция объекта без изме- изменения его спецификации. (Указанный столбец имеет тип данных DATE, но сов- совпадение его имени с новым типом данных случайно.) О Сигнатура — упрощенная спецификация объекта. Сигнатура определяет имя объекта, а также типы данных, порядок и режимы использования его парамет- параметров. Таким образом, когда вы компилируете процедуру synch_em_up, Oracle извле- извлекает временную метку и сигнатуру удаленной процедуры recoft)puted_prices и со- сохраняет их в байт-коде процедуры synch_em_up. Дальше все просто. Во время выполнения программы в зависимости от теку- текущего значения параметра REMOTE_DEPENDECCIES_MODE Oracle сравнивает либо вре- временную метку, либо сигнатуру удаленной процедуры со значением в байт-коде локальной процедуры и, если они не совпадают, генерирует ошибку ORA- 04062. По умолчанию сравнивается временная метка, но иногда это ведет к ненуж- ненужным перекомпиляциям. В таком случае администратор базы данных изменяет ее в системном инициализационном файле либо с помощью команды ALTER SYSTEM. Разработчик приложения может установить ее для текущего сеанса посредством команды ALTER SYSTEM SET REMOTE DEPENOECCIES MODE - SIGNATURE:
Как PL/SQL использует память сервера Oracle 761 а в программе PL/SQL - с помощью инструкции EXECUTE IMMEDIATE 'ALTER SYSTEM SET REMOTE_OEPENDECCtES_MODE =¦ SIGNATURE1: После этого до конца сеанса во всех программах PL/SQL будет использоваться метод проверки сигнатуры. Клиентские средства Oracle всегда выполняют ука- указанную выше команду сразу после подключения к базе данных, чтобы не зависеть от параметров, заданных в инициализационном файле Oracle (INIT.ORA). Oracle рекомендует использовать метод проверки сигнатуры в клиентских сред- средствах, например в Oracle Forms, а метод проверки временной метки - в межсер- межсерверных вызовах процедур. Однако учтите, что первый метод может стать причи- причиной возникновения псевдоошибок в приложении. Если исполняющее ядро по- посчитает, что сигнатура программы не изменилась, хотя на самом деле она стала другой, Oracle не станет выполнять ее перекомпиляцию, и тогда могут появиться ошибки вычислений, которые будет трудно обнаружить. Перечислим несколько ситуаций, когда события могут развиваться по описанному сценарию. О Изменение только значения по умолчанию одного из формальных параметров процедуры. Вызывающая программа будет продолжать использовать старое значение. О Добавление в существующий пакет еще одного экземпляра перегруженной про- программы. Он не будет активизирован, когда это потребуется. О Изменение имени формального параметра. Проблема обычно возникает, если в вызывающей программе производится передача параметров по именам. Во всех этих случаях вы вынуждены будете перекомпилировать вызывающую программу вручную. Метод же проверки временной метки, хотя иногда и приво- приводит к ненужной компиляции, позволяет перекомпилировать программу всегда, когда это требуется. Следовательно, одним из его достоинств является надеж- надежность. Именно поэтому Oracle использует данный метод по умолчанию для уда- удаленных вызовов процедур, находящихся на различных серверах. ПРИМЕЧАНИЕ- Если вы пользуетесь методом проверки сигнатуры, Oracle рекомендует добавлять новые функции и процедуры в конец спецификации пакета, что поможет сократить количество ненужных переком- перекомпиляций. Как PL/SQL использует память сервера Oracle Экономно используя машинные ресурсы, в том числе память и процессор, Oracle может поддерживать десятки тысяч одновременно работающих пользователей базы данных. Технологии управления памятью Oracle в последние годы стали до- довольно сложными, и освоить их бывает непросто. Хотя хорошо разбираться в ме- механизмах управления памятью должны в первую очередь администраторы про- промышленных баз данных, такого рода знания необходимы и программистам. И, конечно же, каждому программисту PL/SQL нужно знать, как избежать сбоев ис- используемого в Oracle алгоритма распределения памяти.
762 Глава 20 • Выполнение программ PL/SQL Память сервера Каждый экземпляр базы данных Отас1е формирует в оперативной или виртуаль- виртуальной памяти системную глобальную область (System Global Area, SGA) и запуска- запускает множество фоновых процессов1. Значительную часть SGA обычно занимают буферные пулы, в которых кэшируются табличные данные. В этой же области па- памяти имеется еще одна часть, особенно важная для работы PL/SQL — общий пул. В таком кэш-пуле содержатся, во-первых, метаданные из словаря данных, а во- вторых, откомпилированные представления SQL-инструкций и программ PL/SQL Когда пользовательский сеанс впервые запускает конкретную программу PL/SQ.L, Oracle помещает ее исполняемую часть в общий пул, называемый библиотечным, кэшем. При повторном вызове этой программы, Oracle использует ее кэширован- ную копию, экономя время на обращении к относительно медленному жесткому диску. РСУБД может использовать кэшированную версию и при вызове про- программы другими сеансами. Однако совместный доступ к байт-коду она предос- предоставляет сеансам только при определенных условиях. Но прежде чем приступить к обсуждению этих условий, необходимо рассказать о других важных для разра- разработчиков приложений процессах и областях памяти, включая области PGA (Prog- (Program Global Area), CGA (Call Global Area) и UGA (User Global Area), которые час- часто путают. Когда клиентская программа, скажем SQL*Plus, инициирует сеанс подключе- подключения к базе данных, Oracle запускает на сервере базы данных «теневой» процесс для его обслуживания. Глобальная область программы (Program Global Area, PGA) - это название области памяти, связанной с данным процессом. Программисты поч- почти не имеют контроля над той частью PGA, где хранится информация о состоя- состоянии процесса сеанса, и используемых им ресурсах операционной системы; еще одна часть PGA содержит данные приложений — она называется CGA; в режиме выделенного сервера в PGA может также располагаться область UGA, предназна- предназначенная для хранения информации пользователя. Когда сеанс выполняет S^-инструкцию или блок PL/SQL, Oracle выделяет для них временную память. Это рабочее пространство выделяется в глобальной области программы и называется глобальной областью вызова (Call Global Area, CGA). Используется оно только в течение данного вызова, то есть в течение вы- выполнения инструкции или блока. По завершении вызова область CGA немедлен- немедленно освобождается. Еще одна важная область памяти называется глобальной областью пользова- пользователя (User Global Area, UGA). В ней содержатся данные о состоянии сеанса и соб- собственные области SQL и PL/SQL (здесь располагаются переменные и константы пакета PL/SQL). В отличие от CGA, область UGA сохраняется между вызовами и, как правило, не уменьшается и не освобождается до конца сеанса. Бытует ошибочное мнение, что UGA может располагаться либо в программ- программной, либо в системной глобальной области. Но самом деле все зависит от того, ка- какая конфигурация, выделенного или разделяемого сервера, используется. Фоновые процессы управляют дисковым вводом-выводом, восстанавливают процессы и транзак- транзакции после сбоев, осуществляют мониторинг процессов базы данных.
Как PL/SQL использует память сервера Oracle 763 О Выделенный сервер. Для каждого сеанса Oracle создает выделенный процесс и помещает UGA и CGA в программную глобальную область, выделяемую процессу операционной системой, в среде которой работает сервер. Такая кон- конфигурация целесообразна при больших нагрузках, например при осуществле- осуществлении интенсивной пакетной обработки и обработки транзакций в реальном вре- времени (On-line Transaction Processing, OLTP), долго выполняющихся запро- запросов и резервного копирования базы данных. О Разделяемый сервер. Раньше его называли «многопотоковым» или MTS. Oracle связывает несколько сеансов с одним совместно используемым северным про- процессом и выделяет для глобальной области пользователя память в системной глобальной области. В такой конфигурации теневой процесс и фиксирован- фиксированные части PGA совместно используются несколькими клиентскими процесса- процессами. Однако с ней связаны некоторые издержки и едва ли она принесет дейст- действительную выгоду, если только в системе не будут выполняться одновременно сотни сеансов, каждый из которых может простаивать значительную часть времени. Общий объем PGA может быть разным в зависимости от вида операций, вы- выполняемых сервером для приложения. Например, для инструкций DML, осуще- осуществляющих сортировку большого количества данных, может понадобиться много памяти CGA, а для пакетов PL/SQL, заполняющих коллекции, - памяти UGA. Упрощенная структура двух последних конфигураций сервера представлена на рис. 20.81. Выделенный сервер Разделяемый сервер Рис. 20.8. Архитектура памяти и процессов Oracle а конфигурациях на основе выделенного и разделяемого серверов 1 На рисунке не показаны очереди диспетчеризации и запросов/ответов разделяемого сервера.
764 Глава 20 • Выполнение программ PL/SQL Вы. должны понимать, что если приложение работает с разделяемым серве- сервером, пользовательские процессы могут конкурировать за время и память с тене- теневым процессом. И если ваш пользовательский процесс запускает долго выпол- выполняющиеся блоки PL/SQL или SQL-инструкции, системный администратор дол- должен либо сконфигурировать сервер с большим количеством теневых процессов, либо запустить эти сеансы на выделенном сервере. Теперь, разобравшись в том, что собой представляет память сервера, мы попы- попытаемся выяснить, как выглядит память с точки зрения выполняющейся программы. Курсоры и память Вы наверняка уже знаете, что такое курсоры. Не исключено, что вам сотни раз приходилось объявлять и открывать их, выбирать из них строки, а затем закры- закрывать их. Возможно, вы даже работали с курсорами, используя встроенный пакет DBMS_SQL Ни SQL, ни PL/SQL не могут функционировать без них; многие опера- операторы для выполнения дополнительных операций неявно открывают рекурсив- рекурсивные или дочерние курсоры. А поскольку каждый курсор, явный или неявный, за- занимает память сервера базы данных, процесс оптимизации настройки Oracle часто подразумевает и сокращение количества необходимых приложению курсоров. Oracle ассоциирует курсоры с анонимными блоками PL/SQL практически так же, как с SQL-инструкциями. Например, при обработке первого вызова в теку- текущем сеансе пользователя Oracle открывает в памяти PGA область («собствен- («собственную» область SQL), в которой размещаются переменные связывания и другая специфическая для этого вызова информация. Оказывается, что одни связанные с курсорами серверные структуры данных располагаются в глобальной области пользователя, а другие — в глобальной об- области вызова. Например, поскольку инструкция SELECT идентифицирует записи, которые должны быть доступны при выполнении нескольких выборок, Oracle выделяет для курсора память в области UGA, а для инструкций DML, завершаю- завершающихся за один вызов, — память в области CGA. Аналогичным образом, при вы- выполнении PL/SQL-программ для хранения информации о состоянии выделяется память в UGA, а для других данных — память в CGA. При выполнении SQL-инструкции или блока PL/SQL сервер сначала прове- проверяет содержимое библиотечного кэша на предмет того, нет ли там уже подготов- подготовленного представления этого кода. Если такая общедоступная область PL/SQL найдена, исполняющее ядро связывает ее и личную область PL/SQL. В против- противном случае Oracle придется выполнить синтаксический анализ инструкции или блока. (Кроме того, Oracle подготавливает и каптирует план выполнения анонимных блоков PL/SQL, включающий вызов ядра PL/SQL для интерпретации байт-кода.) Oracle интерпретирует простейшие блоки PL/SQL (блоки, вызывающие под- подпрограммы, и блоки, не содержащие интегрированных SQL-инструкций), исполь- используя только ту память, которая выделена для главного курсора. Если программа содержит вызовы PL/SQL или вызовы SQL, Oracle требуются дополнительные собственные области в памяти PGA. РСУБД освобождает собственные области SQL, которые используются дочерними курсорами, только после освобождения области SQL родительского курсора.
Как PL/SQL использует память сервера Oracle 765 Таким образом, существует два способа закрытия курсора. Если это делается программно, его больше нельзя использовать в приложении, не открыв снова. Про- Программное закрытие выполняется с помощью следующего оператора: CLOSE иня_мурсора: или при автоматическом закрытии неявного курсора. Однако PL/SQL не сразу освобождает области памяти, связанные с этим курсором. После закрытия курсор существует как объект базы данных. Заглянув в представление V$OPEN_CURSOR, вы увидите, что выполнение операции CLOSE не уменьшило количество открытых кур- курсоров сеанса. Как оказалось, PL/SQL поддерживает собственный «сеансовый кэш курсо- курсоров», то есть сам решает, когда какой курсор необходимо освободить. Макси- Максимальное количество курсоров в этом кэше задается инициализационным пара- параметром OPEN_CURSORS базы данных. Выбор курсора, который должен освободить память, производится на основании алгоритма LRU (Least Recently Used — «доль- «дольше всех не использовавшийся»). При аккуратном программировании, когда все курсоры закрываются вовремя, данный подход дает возможность значительно по- повысить производительность приложений за счет уменьшения сетевого трафика. ПРИМЕЧАНИЕ Всегда явно закрывайте курсоры, открытые явным образом. Если оставить курсор незакрытым, мо- может нарушиться работа алгорьггма управления курсорами PL/SQL. У программиста имеется несколько возможностей изменить стандартное по- поведение Oracle. Одна из них состоит в закрытии всех курсоров сеанса до его за- завершения. Кроме этого кардинального метода существуют и другие: О восстановление исходного состояния пакета (см. ниже раздел «Большие кол- коллекции в PL/SQL»); О управление поведением курсоров с помощью пакета DBMSSQL (правда, выиг- выигрыш от использования данного подхода может быть значительно меньше, чем потери от снижения производительности и усложнения программирования). Советы по экономии памяти Вооружив вас теоретически, дадим несколько практических советов, которыми вы сможете воспользоваться при программировании. Совместное использование инструкций Oracle способна предоставить программам совместный доступ к откомпилиро- валным версиям SQL-инструкций и анонимных блоков даже в том случае, если они получены от разных сеансов и разных пользователей. Однако это возможно лишь при соблюдении определенных условий. Чтобы система могла обеспечить совместный доступ к SQL-инструкциям, необходимо придерживаться пяти основ- основных правил, первые три из которых относятся и к анонимным блокам PL/SQL 1. Соглашения о регистре символов и пробелах в исходном коде должны быть одинаковыми. 2. Внешние ссылки должны разрешаться как ссылки на один и гот же объект.
766 Глава 20 • Выполнение программ РЦ/SQL 3. Значения данных необходимо задавать с помощью переменных привязки, а не в виде символьных строк (или же параметр CURSORSHARING должен иметь соот- соответствующее значение). Oracle кэширует SQL-янструкции отдельно от содержащих их программ, по- поэтому правила их совместного использования несколько иные. Для SQL-инст- SQL-инструкций существует два дополнительных правила. 4. Все параметры базы данных, влияющие на работу оптимизатора запросов SQL, должны совладать. Например, в вызывающих сеансах необходимо задавать оди- одинаковый «критерий оптимизации» (установка ALL_ROWS или FIRST_ROWS). 5. Вызывающие сеансы должны поддерживать одни и те же национальные язы- языки (National Lanquage Support, NLS). Мы не станем останавливаться на двух последних правилах, но о значении первых трех правил для программ PL/SQL следует сказать несколько слав. Первое правило, определяющее необходимость наличия пробелов и учета ре- регистра символов, является хорошо известным условием совместного использова- использования инструкций. Несмотря на то что PL/SQL обычно не учитывает регистр, сле- следующие три блока он не воспримет как одинаковые: BEGIN NULL: END: begin null; end; BEGIN NULL: END: Этот малоприятный факт относится и к SQL. Однако если все ваши аноним- анонимные блоки коротки и все «реальные программы» реализованы в виде хранимого кода, например в виде пакетов, вероятность ошибочно запретить их совместное использование значительно меньше. В этой связи хотелось бы дать такой совет; реализуйте код SQL и PL/SQL в виде хранимых программ. Анонимные блоки должны быть как можно более короткими, в общем случае состоящими из един- единственного вызова хранимой программы. Кроме того, можно дать еще одну реко- рекомендацию по SQL: для того чтобы обеспечить возможность совместного исполь- использования SQL-инструкций, помещайте их в программы, которые вызываются из разных мест приложения. Это избавит вас от необходимости по нескольку раз писать одни и те же инструкции. С другой стороны, автор всегда чувствовал, что попытка строго придержи- придерживаться соглашений о форматировании — неудобный и ненадежный способ под- поддержки совместного использования кода; гораздо проще помещать SQL-инструк- SQL-инструкции в вызываемые программы. Второе правило гласит, что внешние ссылки (на таблицы, процедуры и т. д.) должны разрешаться как ссылки на один и тот же объект. Предположим, что вы со Скоттом подключились к Oracle и оба запустили такой блок: BEGIN foo; END: Решение Oracle о том, следует ли вам обоим предоставить возможность ис- использовать котируемую форму данного блока, зависит от того, является ли ссыл- ссылка foo именем одной и той же хранимой процедуры. Если у Скотта имеется сино- синоним foa, указывающий на вашу копию процедуры, РСУБД разрешит совместный
Как PL/SQL использует память сервера игаае доступ к анонимному блоку. При наличии у каждого из вас своей копии процеду- процедуры каждый будет работать со своим блоком. И даже если обе эти копии процеду- процедуры too абсолютно идентичны, Oracle будет кэшировать их как разные объекты. Аналогичным образом, она кэширу ет как различные объекты идентичные тригге- триггеры разных таблиц. Из сказанного вытекает такой вывод: следует избегать созда- создания одинаковых копий таблиц и программ, соответствующих разным учетным записям. И более того, для экономии памяти нужно отделить программный код, общий для нескольких программ (и в особенности триггеров), и реализовать его как отдельный вызов. Систему необходимо сконфигурировать таким образом, что- чтобы только один пользователь базы данных владел программами PL/SQL, предна- предназначенными для совместного использования, и предоставлял разрешение EXECUTE всем, кто в нем нуждается. (О разрешениях на выполнение речь пойдет далее в этой главе.) Из последнего правила существует важное исключение, действующее в среде с высокой степенью параллелизма, то есть с большим количеством пользовате- пользователей, одновременно выполняющих одну и ту же программу PL/SQL. При выпол- выполнении совместно используемых фрагментов кода для закрепления и освобожде- освобождения объекта используются так называемые защелки библиотечного кэша. В среде с высокой степенью параллелизма это может привести к конкуренции за владе- владение защелками. В таких случаях дублирование кода действительно полезно и по- помогает повысить производительность системы. Третье правило, касающееся переменных привязки, настолько важно, что ему стоит посвятить отдельный раздел. Привязка переменных В PL/SQL любая переменная, используемая в анализируемой статической SQL- инструкции, автоматически становится переменной привязки. Если вы следуете хорошей программистской практике задавать символьные значения в виде пара- параметров и констант, вместо того чтобы ссылаться на них непосредственно, то ни- никаких проблем у вас не будет. Приведем простой пример, в котором PL/SQL пре- преобразует параметр threshold_in в переменную привязки: FUNCTION maxcats (threshholdjn IN NUMBER DEFAULT 100) RETURN NUMBER IS CURSOR qcur IS SELECT category, COUNTC*) FROM booklist CROUP BY category HAVING COUNTt*) > threshholdjn: и так далее... Менее известим является тот факт, что переменные привязки можно исполь- использовать и в анонимных блоках SQL*Plus: SQL> VARIABLE hownany NUMBER SQL> EXEC :hoMiany :- maxcats Однако учтите, что присвоить значение переменной привязки в SQL*Plus мож- можно только в анонимном блоке.
/ов Глава 20 « Выполнение программ PL/SQL Еще одна проблема, связанная с переменными привязки, возникает при фор- формировании динамических SQL-инструкций во время выполнения программы. Не- Небрежное программирование может привести к тому, что такие инструкции будут содержать символьные значения: CREATE OR REPLACE FUNCTION count_recent_records ( tablenamejn IN VARCHAR2.since_in IN DATE) RETURN PLS INTEGER AS countj PL5JNTEGER; BEGIN EXECUTE IMMEDIATE 'SELECT COUNTC*) FROM ' |[ tabienamejn 11 ' WHERE lastupdate > TO_OATE(" ' || T0_CHAREince in. 'YYYYMMDD1) || '", "YYYYMMDD")' INTO countj: RETURN countj; END: Этот код динамически формирует инструкцию SELECT COUNTC*) FROM имя_таблицы WHERE lastupdate > TOJWTEC'20020315'. 'YYYYMMDD') При повторных вызовах данной функции с разными значениями аргумента s i nce_i п может быть создано множество инструкций, не пригодных для использо- использования в нескольких сеансах. Например: SELECT COUNTC*) FROM ШЯ_табпщы WHERE lastupdate > TO_DATE('20020105'. 'YYYYMMDD') SELECT COUNTC*) FROM иня_тдОлииы WHERE lastupdate > TODATEC 20010704'. 'YYYYMMDD') SELECT COUNT(*) FROM иня_табпицы WHERE lastupdate > TO_DATE{'20030101', 'YYYYMMDD') Если переписать эту же функцию с использованием переменной привязки, по- получится такой код: CREATE OR REPLACE FUNCTION count_recent_records (tablenamejn IN VARCHAR2,since in IN DATE) RETURN PLSJNTEGER AS countj PLSJNTEGER; BEGIN EXECUTE IMMEDIATE 'SELECT COUNTC*) FROM ' || tablenamejn || ' WHERE lastupdate > thedate' INTO countj USING sincejn; RETURN countj: END: Он генерирует следующую инструкцию: SELECT COUNTC*) FROM иня_таблицы WHERE lastupdate > :thedate Вторая версия не только проще и привлекательнее, но, главное, не зависит от имени таблицы и значений аргумента since in в вызове функции. В Oracle 8.1.6 появилась возможность обойти ограничение оптимизатора от- относительно символьных строк. Речь идет об использовании параметра CURSOR_ SHARING (совместное использование курсора). Эту функцию можно активизиро- активизировать с помощью команды ALTER SESSION SET CURSORJHARING - FORCE; /* а версиях 8.1.6 и выше */
Как PL/SQL использует память сервера Oracle 769 или команды ALTER SESSION SET CURSORJHARING = SIMILAR: /* в версиях 9.0 и выше */ Далее Oracle до конца сеанса будет в скрытом режиме преобразовывать лите- литеральные запросы, генерируемые первой функцией, в инструкции, подобные сле- следующей: SELECT COUNTC*) FROM Ш_таблнцы WHERE lastupdate > T0_DATE(:"SYS_8_0", :"SYS_B_1") Для приложения результаты будут теми же, однако такая форма инструкции позволит уменьшить нагрузку на библиотечный кэш. Отменить возможность совместного использования курсора позволит такая команда: ALTER SESSION SET CURSORJHARING - EXACT: В Oracle8i параметру CURSOR_SHARING невозможно присвоить значение SIMILAR — он может принимать только значение EXACT или FORCE. Если он имеет значение FORCE, SQL-инструкции с символьными значениями всегда переписываются. Но в некоторых случаях это мижет оказать отрицательное влияние на производи- производительность. В версиях Oracle, поддерживающих параметр SIMILAR, литералы заме- заменяются переменными привязки лишь при условии, что оптимизатор не учитыва- учитывает их при анализе. ПРИМЕЧАНИЕ Переменные привязки и соответствующая установка параметра CURSOR_SHARING нужны только для того, чтобы Orade могла обеспечить совместное использование SQL-инструкций. Помните, что типы данных и максимальная длина переменных привязхи должны соответствовать задаваемым значениям. Пакеты и эффективное использование памяти При извлечении байт-кода (или DIANA-кода) хранимой программы PL/SQL счи- тывается вся эта программа. Речь идет не только о процедурах и функциях, но и о пакетах базы данных. Иными словами, не существует возможности сделать так, чтобы Oracle считывала лишь часть пакета — при обращении к любому его эле- элементу, даже к одной-единственной переменной, в библиотечный кэш загружается весь откомпилированный код пакета. Поэтому формирование и совместное хра- хранение элементов пакета полезно не только с точки зрения разработчика, но и с точ- точки зрения производительности системы. ПРИМЕЧАНИЕ- Поскольку Orade считывает в память весь пакет, объединять в пакеты следует только функциональ- функционально связанные элементы, то есть такие, которые с большой вероятностью будут вызываться в одном сеансе. Большие коллекции в PL/SQL Совместное использование объектов — прекрасное решение, но допустимо оно далеко не для всех элементов программы. Даже если несколько пользователей выполняют одну и ту же программу, принадлежащую одной и той же схеме Oracle, каждый сеанс имеет свою собственную область памяти, в которой содержатся специфические для данного вызова данные — значения локальных переменных
770 Глава 20 • Выполнение программ PL/SQL и переменных пакета, константы и курсоры. И пытаться организовать совместное использование этих значений нет никакого смысла. , Наиболее типичные проблемы, связанные с памятью, при выполнении про- программ PL/SQL возникают во время операций над большими коллекциями, кото- которые являются дрекрасным примером данных, не подлежащих совместному ис- использованию. (О коллекциях подробно рассказывалось в главе 11.) Предполо- Предположим, что мы объявили ассоциативный массив (ранее называвшийся индексной таблицей): DECLARE TYPE number_tab_t IS TABLE OF NUMBER INDEX BY BINARYJNTEGER; riumber_tab nurnber_tab_t: empty tab nunfcer_tab_t: Добавим в эту таблицу большое, количество элементов: FOR i IN i..100000 LOOP number_tab(i):- i: END LOOP; Oracle должна их где-то разместить. Согласно изложенным выше правилам, память для этого массива будет выделена в глобальной области пользователя, если он объявлен на уровне пакета, или в глобальной области вызова, если это данные анонимного блока, процедуры или функции верхнего уровня. У вас наверняка возникнет вопрос, каким образом можно будет освободить эту память после завершения работы с коллекцией. Нужно выполнить оператор number_tab.DELETE; или оператор nuraber_tab :- empty_tab: Oracle освободит память, зафиксировав ее в своем списке свободных облас- областей. Это означает, что память для хранения переменной пакета будет возвращена в глобальную область пользователя, а память переменной уровня вызова — в гло- глобальную область программы. То же произойдет и в том случае, если выполнение программы выйдет за пределы области видимости переменной, объявленной как коллекция. Например, если коллекция будет объявлена в отдельной процедуре, Oracle освободит занимаемую ею память сразу по окончании работы процедуры. Однако такая память не будет доступна другим сеансам или текущему сеансу, если ему понадобится память из глобальной области вызова. Поэтому, если в по- последующих операциях DML, допустим, будет выполняться сортировка большого количества данных, требуемый приложению объем памяти может оказаться про- просто огромным. По завершении сеанса Oracle освободит память полностью, вернув ее в свою кучу. Следует, однако, подчеркнуть, что для системы управления виртуальной па- памятью не составляет труда управлять файлом подкачки большого объема, осо- особенно если процесс удерживает в своем адресном пространстве большое количе- количество неактивной виртуальной памяти. Эта неактивная память занимает только место на жестком диске, а не реальную память. Однако иногда нежелательно за- заполнять страничное пространство, поэтому будет лучше, если Oracle освободит
Как PL/SQL использует память сервера Oracle 771 память полностью. Для таких случаев в ней предусмотрена специальная процеду- процедура «уборки мусора» с очень простым синтаксисом: DBMS SESSION.FREEJJNUSED_USER_MEMORY: Эта встроенная процедура найдет большую часть памяти, не используемой пе- переменными программы, и возвратит ее в родительскую кучу; при наличии выде- выделенного сервера — в PGA, а в случае разделяемого сервера — в SGA. Существует два способа передачи Oracle информации о том, что конкретная коллекция больше не нужна. Один из них, автоматический, производится, когда выполнение программы выходит за пределы области видимости коллекции. Од- Однако при работе с данными уровня пакета имеет смысл воспользоваться описан- описанным выше методом DELETE. Автор протестировал процесс освобождения занимаемой коллекциями памя- памяти в разных ситуациях, в частности, при использовании ассоциативных массивов и вложенных таблиц для выделенного и разделяемого сервера, анонимных бло- блоков и данных пакетов. В результате были сделаны следующие заключения. О Для освобождения занимаемой памяти недостаточно присвоить вложенной таб- таблице или массиву VARRAY значение NULL. Нужно либо вызвать метод коллекции DELETE, либо присвоить коллекции другую пустую, но инициализированную, коллекцию, либо подождать, пока выполнение программы выйдет за пределы области видимости коллекции. О Чтобы освободить память и поместить ее в родительскую кучу, следует при- применить программу DBMS_SESSIDN. FREEUNUSEDUSERMEMORY. Ее можно вызвать по- после заполнения приложением одной или нескольких больших таблиц PL/SQL, которые больше не будут использоваться. О В режиме разделяемого сервера обычно возникает больше ошибок из-за не- нехватки памяти, чем в режиме выделенного сервера, поскольку глобальная об- область пользователя принадлежит системной глобальной области, имеющей фик- фиксированный размер. Как рассказывается в разделе «Трассировка памяти», су- существует вероятность возникновения ошибки ORA-04031. О При подключении к разделяемому серверу освободить память, которую зани- занимают таблицы PL/SQL, объявленные на уровне пакета, невозможно. На практике объем памяти, занимаемый коллекцией элементов типа NUMBER, не зависит от того, являются элементы значениями NULL или 38-значными числами. А вот для значений типа VARCHAR2, длина которых превышает 30 символов, Oracle, похоже, выделяет память динамически. В режиме выделенного сервера массив, состоящий из миллиона значений ти- типа NUMBER, занимает около 38 Мбайт. И даже если элементы массива имеют тип BOOLEAN, Oracle9i использует для него почти 15 Мбайт памяти. Умножьте это зна- значение на количество пользователей, например на 100, и вы получите огромные цифры. Их следует учитывать, особенно в тех случаях, если необходимо, чтобы операционная система выполняла свопинг страниц на диск. Для того чтобы определить, сколько памяти UGA и PGA доступно в текущем сеансе, можно запустить такой запрос: SELECT n.name. ROUNDtm.value/1024) kbytes FROM VJSTATNWE n. VSMYSTAT m
772 Глава 20 • Выполнение программ PL/SQL WHERE n,statistic* - m.statistic! AND л.name LIKE 'sessionimemoryi:1 (Для чтения представлений VSSTATNAME и VSMYSTAT недостаточно привилегий по умолчанию.) Единственным способом освобождения памяти PGA при условии, что вы не хотите завершать сеанс, является принудительный перевод пакетов в исходное состояние с помощью одной из двух следующих процедур. О DBMS_SESSION.RESET_PACKAGE. Освобождает всю память, выделенную для хране- хранения информации о состоянии пакета. В результате все переменные пакета по- получают значения по умолчанию. Для пакетов эта встроенная процедура делает больше, чем пакет FREEJJNUSED_USER_MEMORY, поскольку она не заботится тем, как используется память. О DBMSJESSI0N.MODIFYJw:KAGEJTAT?(<iwia™_onepaLjw IN PLSJNTEGER). В качестве фла- флагов операций можно задать одну из двух констант, а именно DBMSJ>ESSIQN. FREE- ALL_RESOURCES или DBMS_SESSION.REINITIALIZE. Использование цервой приводит к тому же эффекту, что и применение процедуры RESET_PACKAGE. Вторая кон- константа восстанавливает переменные состояния, присваивая им значения по умолчанию, но не освобождает и не восстанавливает пакет с нуля. Кроме того, она программно закрывает открытые курсоры и не очищает их кэш. Если ваше приложение допускает подобное, используйте вторую константу, поскольку это лучше, чем возврат пакета в исходное состояние. Сохранение состояния объектов Обычно Oracle сохраняет до окончания сеанса значения констант уровня пакета, переменных и состояние курсора в глобальной области пользователя. С перемен- переменными, которые объявлены внутри модуля, она обходится совсем иначе. Посколь- Поскольку их область видимости ограничена модулем, по завершении его работы зани- занимаемая данными переменными память освобождается. С этого момента их самих больше не существует. Причиной уничтожения информации о состоянии пакета, помимо отсоедине- отсоединения от сервера базы данных, могут послужить еще несколько обстоятельств; О программа стала неработоспособной (например, в результате перекомпиляции); О в текущем сеансе выполнена встроенная процедура DBMS_SESSION. RESET_PACKAGE; О в код программы добавлена директива компилятора SERIALLY_REUSABLE (см. гла- главу 17), предписывающая Oracle сохранять параметры состояния только на вре- время вызова, а не в течение всего сеанса; О использование web-шлюза в режиме по умолчанию, в котором параметры се- сеанса каждого клиента не сохраняются. Структуры данных пакета могут действовать в среде PL/SQL как глобальные. Иными словами, с их помощью программы PL/SQ.L, выполняющиеся в течение одного сеанса, могут обмениваться данными. С точки зрения архитектуры приложения существует два вида глобальных данных: общие и личные. О Общие данные. Структура данных, объявленная в спецификации пакета, является глобальной общей структурой данных. Доступ к ней имеют любые
Как РЦ/SQL использует память сервера Oracle 773 программы и пользователи с привилегией EXECUTE. Программы могут присваи- присваивать переменным пакета, которые не являются константами (CONSTANT), любые, даже бессмысленные значения. Как известно, использование общих глобаль- глобальных данных является потенциальной причиной самых разнообразных ошибок, поскольку существует большое искушение применять их без необходимости и не по назначению, из-за чего код получается неструктурированным и сопро- сопровождается опасными побочными эффектами. Спецификация модуля должна содержать всю информацию, необходимую для его вызова и использования. Если программа выполняет чтение или запись глобальных структур, в спецификации об этом ничего не сказано. Вот почему вы не знаете, что происходит в приложении и какие из программ изменяют данные каждой конкретной структуры. Из этого следует вывод: передавать данные в модули и получать их из таковых всегда лучше в виде параметров. Тогда доступ к такой информации будет документирован в спецификации и программист сможет его учитывать. Например, автор книга старается огра- ограничивать глобальные данные только константами. О Личные данные. Личные глобальные структуры данных (называемые также данными уровня пакета) не вызывают столько проблем. В спецификации пакета они отсутствуют, и извне такового на них ссылаться нельзя. Эти данные предна- предназначены только для использования внутри пакета и только его элементами. Глобальные данные, существующие в пределах одного сеанса Oracle Структуры данных пакета являются глобальными только в пределах одного сеан- сеанса или подключения к базе данных Oracle. Но они не могут совместно использо- использоваться несколькими сеансами. Для обеспечения такой возможности имеются дру- другие средства, в том числе пакет OBMS_PIP?, средство Oracle Advanced Queuing и па- пакет UTL_TCP. Разработчики клиентских приложений должны помнить о том, что не всегда разные модули используют единое подключение к Oracle. Бывает, что для выпол- выполнения некоторой операции приложение устанавливает дополнительное подклю- подключение к базе данных. В этом случае данные, которые хранятся в пакете, открытом через первое подключение, оказываются недоступными. Проанализируем ситуацию, показанную на рис. 20.9. Приложение Oracle Forms сохранило информацию в структурах данных серверного пакета. Хранимая про- процедура, вызываемая из кода формы, может обращаться к тем же переменным па- пакета, что и сама форма, поскольку обе используют общее подключение к Oracle. Если форма вызывает встроенную программу RUN_PRODUCT для вывода диаграммы с помощью Oracle Graphics, для формирования отчета по умолчанию создается второе подключение к базе данных (с тем же именем пользователя и паролем). Поэтому даже в том случае, когда программа при формировании отчета обратит- обратится к структурам данных того же пакета, в них будут записаны не те данные, кото- которые используются формой. Объясняется это просто: открыт другой сеанс Oracle, а значит, формируются другие экземпляры структур данных. (Кроме того, общие транзакции и согласованные представления данных не будут использоваться в разных сеансах)
774 Глава 20 • Выполнение программ PL/SQL Клиентская сторона Модуль Oracle Forms Вызов через }RUN_PRODUCT Модул. Oracle Graphics Серверная старом Сервер РСУБДОяЩ Сеанс 1 ¦ Данные пакета ¦ Контекст транзакции ¦ Согласованность считанных данных Сеанс 2 • Данные пакета ¦ Контекст транзакции ¦ Согласованность считанных данных . . ¦ г . . ¦ I Рис 20.9. Два подключения к Oracle — для Oracle Forms и Oracle Graphics Для обеспечения возможности совместно использовать параметры состояния пакета потребуются дополнительные действия с клиентской стороны PL/SQL. О них рассказывается далее, в разделе -«Клиентские библиотеки PL/SQL во вре- время выполнения приложения»'. Трассировка памяти Предположим, что ваше приложение базы данных прекрасно работало, выполня- выполняло большое количество инструкций PL/SQL и SQL и вдруг выдало сообщение об ошибке ORA-04031: unable to allocate n bytes of shared memory (невозможно выде- выделить и байт общей памяти). Эту ошибку можно встретить только в режиме разде- разделяемого сервера, когда речь идет об обращении к памяти глобальной области поль- пользователя; в режиме выделенного сервера Oracle в таких случаях просто добавляет виртуальную память. Попытаемся выяснить, с чем связано появление указанного сообщения. Пре- Прежде чем запустить программу PL/SQL на выполнение, Oracle должна загрузить в память весь ее байт-код. Для того чтобы узнать, какой объем памяти занимает объект в общем пуле, нужно вызвать встроенную процедуру DBMS_SHARED_POOL.SI- DBMS_SHARED_POOL.SIZES. Эта процедура выводит список всех объектов, размер которых превышает оговоренный ранее. Приведем пример1: SQL> SET SERVEROUTPUT ON SIZE 1000000 SQL> EXEC OBKS_SHARED_POOL.SIZES(m1ns1ze => 100) SIZE(K) KEPT NAME 371 SYS.STANDARD <PACKAGE) Вы наверняка заинтересовались, почему столбцы данных под заголовками не выровнены. Это свя- связано Со строгими ограничениями на использование процедуры DBMSOLJTPUT. Если вам не нравится та- такое положение вещей, напишите собственную процедуру, запрашивающую данные из представле- представления VSSQLAREA.
Как PL/SQL использует память сервера Oracle 775 166 SYS./5ee89977_NamespaceRDBMS (JAVA CLASS) 101 YES SYS.java/math/Biglnteger (JAVA CLASS) Как следует из приведенного выше код^, пакет STAN0AR0 занимает в обшей па- памяти 371 Кбайт1. Эта информация очень важная, но недостаточно полная. Вы должны знать объем памяти общего пула и то, какая его часть занята «многократ- «многократно создаваемыми» объектами, то есть объектами, которые могут удаляться из па- памяти и загружаться туда повторно по мере необходимости. А еще вам как разра- разработчику нужно знать, содержит ли ваше приложение код, который мог бы ис- использоваться совместно, и каков размер такого кода. Существует несколько способов избежать появления сообщения об ошибке ORA-04Q31. Опытный администратор базы данных знает, как настроить общий аул, откорректировав значения перечисленных ниже параметров: О SHARED_PO0L_SI2E - определяет объем памяти, выделяемый для общего пула; О SHARED_P0O1__RESERVED__SIZE - определяет поддерживаемый Oracle объем непре- непрерывной памяти в общем пуле; О LARGE_POOL_SIZE - задает необязательную область памяти, не связанную с об- общим пулом, в которой располагается глобальная область вызова, необходимая для подключений к разделяемому серверу (такой подход предотвращает кон- конкуренцию глобальной области вызова за использование общего пула); О SESSION_CACHED_CURSORS - устанавливает количество элементов в кэше курсо- курсоров сеанса; О CURSOR_SPACE_FOR_TIME - если значение этого параметра равно TRUE, Oracle осво- освобождает память, занимаемую курсором в библиотечном кэше, только при ус- условии, что все приложения закрыли связанные с ним курсоры (обычно он имеет значение TRUE). Разработчик приложения имеет возможность: О модифицировать код таким образом, чтобы количество совместно используе- используемых SQL-инструкций было максимальным; 0 с разрешения администратора базы данных постоянно хранить в буферной па- памяти определенные программы PL/SQL и/или курсоры. Для того чтобы сохранить в общем пуле конкретные программы, нужно вы- вызвать процедуру DBMS_SHARED_POOL. KEEP2 с соответствующим параметром. Напри- Например, вызов BEGIN DBMS_SHARED_POOL.K?EP('SYS.STANDARD'): END; указывает Oracle на необходимость сохранить в памяти пакет SYS.STANDARD. 1 В самых старых версиях процедуры DBMS_SHARED_POOL.SIZES имеется ошибка, приводящая к тому, что вычисляемые объемы памяти на 2,3 % больше действительных. Эта процедура, опреде- определяя значение в килобайтах, по ошибке делит количество байтов не на 1024, а на 1000. Oracle предупреждает, что данная функция может быть упразднена в результате Перехода к луч- лучшим алгоритмам управления памятью.
776 Глава 20 • Выполнение программ PL/SQL ПРИМЕЧАНИЕ В словаре данных Oracle имеется множество представлений, помогающих решить, какие модули стоит держать в памяти. Хотя обычно в общем пуле рекомендуется храншъ пакет STANDARD, экс- эксперт Стив Адаме (Steve Adams) советует держать там «все, что перемещается». Но начиная с Oracle версии В.1.6 это не так важно, поскольку была введена функция, которая позволила устранить кон- конкуренцию за защелки при фрагментации общего пула. Информацию мистера Адамса о возможно- возможностях Orade вы найдете на узле http://www.ixora.com.au/. Описанная тактика наиболее полезна при работе с большими и подолгу не ис- используемыми программами, которые при перезагрузке выталкивают из памяти много маленьких объектов. Процедура KEEP может вызываться для пакетов, про- процедур, функций, триггеров, различных последовательностей, а также, если про- программист приложит определенные усилия, для курсоров. При использований программ PL/SQL, для которых объектный С-код полу- получен путем встроенной компиляции, Oracle применяет другую тактику доступа к общей памяти. Эти программы компонуются с файлами совместно используе- используемых библиотек, общий код которых загружается в память один раз, причем па- память для них выделяется операционной системой. Другие программы, вызывае- вызываемые сервером, в частности хранимые процедуры Java и внешние процедуры С, также используют общую память. В режиме разделяемого сервера может возникнуть еще одна ошибка доступа к памяти— ORA-06500: PL/SQL: storage error. Обычно ее устраняют путем увеличе- увеличения значения параметра LARG?_POOL_SIZE. Выполнение серверного кода PL/SQL В этом разделе описываются действия, которые выполняются Oracle, когда вы с помощью программы SQL* Plus или подобного ей приложения направляете сер- серверу код SQL'Plus. Компиляция анонимного блока Компиляция анонимного блока выполняется следующим образом. 1. Компилятор PL/SQL производит синтаксический анализ кода. При обнару- обнаружении синтаксической ошибки анализ прекращается и вызывающей програм- программе возвращается сообщение об ошибке компиляции. 2. Сервер Oracle определяет наличие в библиотечном кэше реально исполняе- исполняемых версий анонимного блока. Для этого вычисляется значение хэш-функции для всего текста блока и производится текстовый поиск с использованием про- процедуры, подобной mememp. Если поиск завершился успешно, выполняется ряд дополнительных проверок, цель которых — удостовериться, что серверное ок- окружение не изменилось. При условии, что результаты всех проверок положи- положительны, начинается выполнение кода. 3. Если блок в библиотечном кэше отсутствует, Oracle направляет его компиля- компилятору PL/SQL
Выполнение серверного кода PL/SQL 777 4. Компилятор PL/SQL осуществляет преобразование имен идентификаторов PL/SQL, направляя все встроенные SQL-инструкции анализатору SQL. 5. Анализатор SQL выполняет собственную синтаксическую проверку, опреде- определяет корректность имен и производит семантический контроль, после чего сно- снова вызывает PL/SQL для преобразования неразрешенных имен (таких как пе- переменные привязки). 6. Во время компиляции проверяется, действительно ли пользователь, компили- компилирующий код, имеет разрешение на выполнение, во-первых, всех программ, на которые есть ссылки, а во-вторых, SQL-запросов к указанным структурам дан- данных, таким как таблицы. Подобная проверка производится даже при компиля- компиляции программ, на выполнение которых имеется разрешение вызывающего. 7. Автоматически перекомпилируются объекты, помеченные как неработоспо- неработоспособные, на которые имеются ссылки (даже из удаленных процедур). 8. Считывается DIANA-код объектов, на которые имеются ссылки (в том числе удаленных процедур). Делается это с целью проверки соответствия вызовов объектов их сигнатурам. Если в словаре данных еще нет DIANA-кода объекта, таковой генерируется и сохраняется в этом словаре. 9. В случае успешной компиляции создается байт-код, который загружается в биб- библиотечный кэш. DIANA-код анонимного блока удаляется. ч Компиляция хранимого объекта Существует несколько вариантов процесса компиляции хранимых объектов (па- (пакета, тела пакета, процедуры, функции, триггера, типа и тела типа). 1. Перед началом перекомпиляции созданной программы Oracle проверяет, не выполняется ли она в данный момент, и, если это так, ожидает момента ее за- завершения. Стив Адаме отмечает, что для программы на время выполнения ус- устанавливается режим общего доступа (S), а на время компилядии — режим монопольного доступа (X). Учтите, что эти установки могут стать причиной взаимных блокировок процессов или долгих ожиданий. 2. Если компилируемый объект представляет собой пакет с информацией о со- состоянии, Oracle восстанавливает его исходное состояние и, возможно, других пакетов текущего сеанса. 3. При выполнении DDL-инструкции, например CREATE OR REPLACE PROCEDURE, Oracle сохраняет исходный код в таблице SOURCES даже в случае неудачной компиля- компиляции. Кроме того, для каждой хранимой программы она сохраняет в таблице SET- SETTINGS* текущие флаги компилятора. 4. После успешной компиляции любого объекта, кроме тела пакета и тела типа (даже если речь идет о программах, откомпилированных путем встроенной компиляции), его DIANA-код сохраняется в базе данных. 5. Сохранив DIANA-код хранимой программы, Oracle записывает в таблицу DE- DEPENDENCY* информацию об объектах, от которых эта программа зависит.
778 Глава 20 • Выполнение программ PL/SQL 6. Если в программе используются вызовы удаленных процедур, Oracle во время компиляции извлекает их временные метки, а также сигнатуры, и сохраняет их вместе с откомпилированным кодом локальной программы. 7. Если флаги сеанса или базы данных задают режим встроенной компиляции, PL/SQL выполняет обычный анализ синтаксиса и семантики. Затем внутрен- внутренний транслятор преобразует код PL/SQL в С-код и вызывает С-компилятор операционной системы, генерирующий совместно используемый объектный файл. 8. Когда откомпилировать хранимый объект не удается, Oracle вносит соответ- соответствующую запись в таблицу ERRORS, которую можно просмотреть через пред- представление USER_ERRORS. После успешной компиляции информация о предыду- предыдущей ошибке компиляции удаляется из таблицы ERRORS. 9. При успешной компиляции все варианты откомпилированного кода загружа- загружаются в библиотечный кэш. Oracle записывает DIANA-код хранимых программ, байт-код и данные отладки в словарь данных. Выполнение программ PL/SQL Записав байт-код в библиотечный кэш, Oracle может приступать к его реализа- реализации. Программы PL/SQL выполняются в несколько этапов. 1. Если пользователь, запускающий программу, не является ее владельцем, Oracle проверяет наличие у него привилегии EXECUTE. 2. Когда вызывается хранимая программа с атрибутом AUTHID CURRENTJJSER (раз- (разрешения вызывающего), исполняющее ядро PL/SQL повторно производит раз- разрешение внешних ссылок на объекты SQL с учетом идентификационных дан- данных вызывающего. Разрешение внешних ссылок на программы PL/SQL по- повторно не выполняется, если только они не встроены в анонимный блок. 3. Oracle открывает курсор и связывает его с программой, даже если это аноним- анонимный блок. 4. Если в коде имеются удаленные процедуры, исполняющее ядро PL/SQL срав- сравнивает локальную копию временной метки или сигнатуры с данными удален- удаленной стороны. Если они не соответствуют друг другу, Oracle сбрасывает со- состояние текущего сеанса (ORA-04068) и выдает ошибку ORA-04062: timestamp or signature of procedure "имя_процедуры" has been changed. Вторая попытка разре- разрешения ссылок приводит к автоматической перекомпиляции локальной про- программы; в случае успеха таковая осуществляется. 5. Исполняющее ядро PL/SQL производит все необходимые вызовы SQL-про- SQL-процессора Oracle, открывающего и кэширующего курсоры. 6. Обычно программа PL/SQL реализуется в контексте транзакции, созданной текущим сеансом. Однако если исполняющее ядро вызывает программу, вы- выполняющую автономную транзакцию, ядро приостанавливает текущую тран- занкцию и создает для вызванной программы второй контекст транзакции. После фиксации или отката автономной транзакции выполнение исходной транзакции возобновляется. (Подробнее об этом рассказывается в главе 13.)
Клиентский код PiySQL 779 7. Когда вызывающая программа PL/SQL передает параметры вызываемой про- программе, создается копия аргументов. Если вызываемая программа не смогла обработать все возникшие исключения, исполнительное ядро возвратит ис- исходные значения аргументов. Но если для компилятора установлен режим IN OUT NCOPY, ядро не создает копии аргументов и не возвращает их исходные зна- значения. Вследствие этого некоторые изменения в вызываемой программе могут остаться незавершенными. (Аналогичная ситуация рассматривается в главе 16.) 8. Исполняющее ядро вызывает для каждой откомпилированной программы PL/SQL совместно используемую библиотеку, уже связанную с исполняемым файлом Oracle (см. главу 19). 9. Для внешней процедуры Oracle Net создает специальный процесс extproc, по- посредством которого исполняющее ядро PL/SQL обменивается аргументами с общей библиотекой, содержащей внешнюю процедуру. (Более подробно об этом рассказывается в главах 22 и 23.) 10. После завершения работы вызываемая программа возвращает информацию о состоянии и/или выходные параметры. Если вызываемой программе не уда- удалось перехватить и обработать абсолютно все исключения, формируется стек ошибок с информацией об этих исключениях, которая обычно предоставляет- предоставляется пользователю. Клиентский код PL/SQL В случае применения таких средств разработки приложений Oracle, как Oracle Forms Builder или, скажем, Oracle Reports Builder, пользовательский интерфейс и бизнес-логика приложений чаще всего реализуются на языке PL/SQL. Вы може- можете использовать их, даже не подключившись к базе данных. Приложив неболь- небольшие усилия, вы можете связать эти клиентские приложения с базой данных лю- любого типа, однако такая потребность возникает нечасто. Стандартная конфигура- конфигурация среды выполнения представлена на рис. 20.10. Как следует из этого рисунка, клиентская среда времени выполнения, которая на самом деле может работать на промежуточном уровне как «сервер» Oracle Forms, взаимодействует и с серверным исполняющим ядром PL/SQL, и с серверными компонентами, выполняющими SQL Удаленные вызовы процедур не требуют чте- чтения сервером DIANA-кода вызываемой программы. Средства разработки прило- приложений Oracle предоставляют программисту широчайший набор функций, реализо- реализованных во встроенных пакетах. В частности, на клиентском компьютере PL/SQL позволяет выполнять следующие операции: О определять триггеры, обрабатывающие некоторые события (например, щелч- щелчки кнопкой мыши); О помогать пользователю при вводе данных, осуществляя их автоматический поиск и проверку; О создавать код, который будет выполняться при выборе пользователем элемен- элемента меню;
780 Глава 20 • Выполнение программ PL/SQL О сохранять данные в локальных файлах и восстанавливать их из файлов с по- помощью пакета ТЕХТ_Ю; О программно изменять поведение и внешний вид элементов пользовательского интерфейса. Процесс Oracle Forms или Oracle Report» I Исполняющие |; ядре h. PL/SOX Сермр РСУБД Oracle УВалвтьй вьоов !!№!$№!*.... 1.^. Результаты : Исполняющее i ядро ! . PUSQL Текст SQL-инапрукцт Словарь данных Oracle. [ 1 Компилятор /Паи выполнения SQL ¦¦¦"Т SQL f~ i Исполняющее ядро SQL Рис 20.10. Клиектская среда выполнения, вызывающая хранимую процедуру Во всех версиях Oracle, вплоть до Developer 6i, PL/SQL поддерживал взаимо- взаимодействие с клиентской операционной системой. Например, при работе в среде Microsoft Windows можно было непосредственно вызывать DLL, использовать объекты OLE (Object Linking and Embedding), выполнять вызовы функций Win- Windows API. Однако в РСУБД версии Developer 9г все эти специфические для Mic- Microsoft возможности были отменены. Без комментариев. К счастью, непосредственный вызов серверного PL/SQL из клиентской среды PL/SQL выполняется элементарно. Необходимо только (комментарий для но- новичков) иметь в виду, что клиентские и серверные версии выпускаются не одно- одновременно, и поэтому вам могут быть доступны не все возможности языка. Вы должны знать, какая версия PL/SQL установлена на клиентской машине. Поддерживаемые версии и функциональные возможности Для того чтобы узнать, какая версия PL/SQ.L установлена на клиентской маши- машине, достаточно выбрать Developer * Help > About Перечень всех существующих вер- версий приведен в табл. 20.3, но имейте в виду, что установленные вами пакеты ис- исправлений могут изменить десятичный номер версии.
Клиентский код PL/SQL 7B1 Таблица 20.3. Соответствие клиентских версий PL/SQL версиям Orade Developer Версия Oracle Developer Версия Oracle Forms Версия PL/SQL (базовый выпуск) l.X 4.5 1.2 2. х 5.0 2.3 6.0 6.0.5 8.0.5 б! 6.0.8 8.0.6 9JIU Правда, факт установки на клиентской машине той или иной версии PL/SQL еще не означает, что вы получите доступ ко всем соответствующим ей серверным функциям. Например, в клиентской версии PL/SQL 9.0.2 доступны следующие операции: О объявление и использование в клиентской программе локальных типов кол- коллекций и переменных этих типов; О применение подтипов, типов коллекций, типов записей и курсорных перемен- переменных, объявленных в спецификации пакета, который хранится на сервере; О определение в клиентской программе переменных типа, объявленного в спе- спецификации серверного пакета, в том числе записей и одноуровневых коллек- коллекций; О объявление и использование переменных в качестве ссылок на определенные пользователем объектные типы; О формирование блока на основе таблицы с атрибутом типа коллекции; О создание блока на основе таблицы объектов или таблицы с объектным столб- столбцом при условии, что объектный тип не содержит атрибута типа коллекции. Однако некоторые операции на стороне клиента выполнять нельзя. К их чис- числу, в частности, относятся: О создание отдельного пользовательского типа (объектного или типа коллек- коллекции); О определение переменной пользовательского типа, объявленного на сервере. Решить вторую проблему можно двумя способами: О не использовать неподдерживаемых типов данных в спецификациях сервер- серверных модулей; О написать хранимую процедуру, которая будет выполнять преобразование ти- типов данных сервера PL/SQL в соответствующие типы данных клиента (где это возможно). Второй способ, надо полагать, более привлекателен. Если написать хранимые программы, в которых применяются параметры серверных типов данных, специ- спецификацию модуля менять не придется, и вы всегда сможете использовать самые прогрессивные возможности языка. Однако не следует исключать вероятность того, что в какой-то ситуации вы не в состоянии будете изменить спецификацию (список параметров) хранимого
782 Глава 20 • Выполнение программ PL/SQL модуля. Он может быть написан другими программистами, возможно, для друго- другого приложения, и его нельзя будет модифицировать, не изменив само приложе- приложение. В этом случае второй подход сложнее, во все равно применим. Например, если клиентский код, написанный для версии до Oracle9i, должен вызывать про- процедуру с входным параметром типа TIMESTAMP, имеет смысл инкапсулировать ее в другую хранимую процедуру, принимающую значение даты. Фактически эту процедуру можно просто перегрузить. Приведем пример спецификации: PACKAGE logger AS PROCEDURE note_the_time (event_t1me_1n IN TIMESTAMP); PROCEDURE note_the_time Cevent_t1me_1n IN DATE); END: Тело пакета будет содержать следующий код: PROCEDURE note the_tirae (event_time_in IN DATE) IS BEGIN note_theJHmettO_TIMESTAMP(event tlmejn)): END: Теперь процедуру 1ogger.note_the_time можно вызывать из клиентских про- программ, написанных для версий до ОгаскЭг, поскольку она принимает значение поддерживаемого типа данных. ВНИМАНИЕ ¦ — — — Выполняя перегрузку с целью поддержки разных типов данных, не полагайтесь на неявное преоб- преобразование типов в теле процедуры. Автор, например, пытался сделать это следующим образом: PROCEDURE note the_time (eventJUneJn IN DATE) IS BEGIN note_the_tiiDe(event_time_in): /* Плохая идея */ END; В результате был получен бесконечный вложенный цикл, для выхода из которого необходимо унич- уничтожить задачу, выполняющуюся в теневом режиме, с помощью исполняемого файла oraklll.exe в Windows или команды kill -9 в Unix. Следует отметить, что при использовании Oracle Forms 9i такой проблемы с типом данных TIMESTAMP у вас не будет, поскольку эта версия его поддерживает. Однако, как будет показано в следующем разделе, могут возникнуть неприятно- неприятности с переменными серверных пакетов. Ограничения, касающиеся удаленных вызовов в Oracle На момент написания настоящей книги программное обеспечение Oracle не по- позволяло программе PL/SQL непосредственно использовать такие конструкции удаленного сервера, как переменные (включая константы), курсоры и исключе- исключения. Причем ограничение касается не только клиентского PL/SQL, вызывающе- вызывающего сервер базы данных, но и межсерверных вызовов удаленных процедур. Простым решением указанной проблемы в отношении переменных является использование программ установки и чтения, инкапсулирующих нужные данные — тем более что это вообще хорошая программистская практика.
Клиентский код PL/SQL 783 Курсоры можно инкапсулировать в программы открытия, выборки и закры- закрытия. Если, предположим, в спецификации серверного пакета boofcjraint объявлен курсор bookcur, в тело пакета можно поместить такой код: CREATE OR REPLACE PACKAGE 8ODY bookjnaint AS prv_book_cur_status BOOLEAN; PROCEDURE open_book_cur IS BEGIN IF NOT bookjnaint.bookjrurXISOPEN THEN OPEN bookjnaint.book cur; END IF; ¦ END: . FUNCTION next_book_rec аГГиЯН booksJROWTYPE IS l_book_rec bookslROWTYPE; BEGIN FETCH bookjnaint.bookjrur INTO l_book_rec: prvj3ook_cur_status ;- book main.book_CurXFOUND; RETURN l_baolc_rec; END; FUNCTION book_cur_is_found RETURN BOOLEAN IS BEGIN RETURN prv_booli;_cur_status: END: PROCEDURE close_book_cur IS BEGIN IF bookjnaint.book_cur!ISOPEN THEN CLOSE book_maint.book_cur: END IF: END; END; К сожалению, этот подход не годится для перехвата и обработки удаленных исключений, поскольку «тилы данных» исключений интерпретируются иначе, чем остальные типы данных. Поэтому вам придется выполнить процедуру RAISE_AP- PLICATIONERROR с пользовательскими номерами исключений от -20 000 до -20 999. О том, как написать хороший пакет для перехвата и обработки исключений этого типа, рассказывалось в главе 6.
784 Глава 20 • Выполнение программ PL/SQL Клиентские библиотеки PL/SQL Код PL/SQL на стороне клиента может располагаться в двух местах: О в PL/SQL-библиотеке многократного использования; О в модуле приложения (формы, отчета и т. п.). Библиотека может содержать любое количество процедур, функций и паке- пакетов; это что-то вроде «суперпакеткой» структуры. Данная конструкция уникаль- уникальна для клиентских средств Oracle, и ее использование требует отдельного обсуж- обсуждения. Главное, что код PL/SQL может располагаться в самых разных местах (табл. 20.4), но только библиотеки содержат код PL/SQL, доступный для совме- совместного использования несколькими клиентскими модулями. Таблица 20.4. Библиотеки и клиентские файлы, в которых может содержаться код PL/SQL Расширение имени файла Описание .PLL (PL/SQL Library) .PLX (PI/SQL Library «executable») .PLD (PL7SQL library Text) .FMB (Forms Module Binary) .FMX (Forms Module «executable») .FMT (Forms module text) .RDF (Report Definition Rle) .REP (Report runfile) .REX (Report Text File) .MMB (Menu Module Binary) .MMX (Menu Module executable) .MMT (Menu Module Text) Исходный код PL/SQL, байт-код процедур, функций и пакетов Байт-код соответствующих программных элементов библиотеки Необязательная читабельная текстовая версия программных элементов библиотеки Любой исходный код PL/SQL а модуле Oracle Forms, хранящийся в двоичном файле вместе с DIANA-представлением и байт-кодом Байт-код, соответствующий коду PL/SQL в модуле Oracle Forms Необязательная текстовая версия модуля Oracle Forms; однако код PL/SQL представлен только в тестнадцатеричном формате Любой созданный в модуле Orade Reports исходный код PL/SQL, хранящийся в двоичном файле вместе с DIANA-лредставлением и байт-кодом Байт-код, соответствующий коду PL/SQL в модуле Oracle Reports Необязательная текстовая версия модуля Orade Reports, включающая читабельный код PL/SQL Любой созданный в модуле меню исходный код PL/SQL, хранящийся в двоичном файле вместе с DIANA-представлением и байт-кодом Байт-код, соответствующий коду PL/SQL в модуле меню Необязательная текстовая версия модуля меню Поскольку библиотеки PL/SQL могут кардинально улучшить архитектуру и повысить производительность клиентских приложений, следующие два раздела посвящены основным концепциям и неочевидным аспектам их использования.
Клиентский код PL/SQL 785 Клиентские библиотеки во время проектирования Стандартное соглашение, которым обычно руководствуются при разработке кли- клиентского PL/SQL, предполагает наличие в модуле только специфического для него кода, и размещение остального кода, используемого более чем одним моду- модулем, в библиотеке. Поместив код в библиотеку, таковую нужно «присоединить» к модулю. Эта задача выполняется в среде разработки с помощью программы Object Navigator. Причем библиотечный модуль и файл модуля приложения (на- (например, .fmb) остаются автономными, и при распространении кода нужно скопи- скопировать в дистрибутив оба эти файла. Начинающих программистов обычно смущает отображаемый системой запрос об удалении из библиотеки информации о пути. Отвечая на него, вы можете вы- выбрать одну из двух возможностей: О сохранить полную информацию о пути (такую как c:\apps\forms\libs), посколь- поскольку библиотека будет помещена в тот же каталог, где она разрабатывается; О удалить информацию о пути и предоставить клиентским средствам возмож- возможность искать библиотеку в соответствии с установками в системных перемен- переменных. Первый вариант больше подходит для этапа проектирования, а второй, более гибкий, — для этапа выполнения приложения, хотя и требует настройки перемен- переменных пути. Во время выполнения поиск пути файла производится в определенном порядке. О Сначала проверяется значение переменной, специфической для данного сред- средства, например FORMS90_PATH, REPORTS90_PATH или GRAPHICS60_PATH. О Затем, если переменная не найдена, проверяется значение переменной ORAC- LEPATH В среде Windows присвоить значения этим переменным можно либо с помо- помощью панели управления, либо путем установки нужных параметров в реестр. За- Задать их значения в командном файле нельзя. При выполнении разрешения внешних ссылок на программы в тестируемых автором клиентских средствах поиск производится в строгой последовательности: О клиентские встроенные программы; О «программные элементы», определенные в самом модуле {.fmb); О PLX-файлы присоединенных библиотек PL/SQL, путь к которым задан в ука- указанных выше переменных среды; О PLL-файлы присоединенных библиотек PL/SQL (при отсутствии PLX-фай- лов); О сервер базы данных (с использованием стандартного разрешения имени сер- сервера). Если на этапе проектирования программы размещены в легко доступных мес- местах, загрузку библиотеки и присоединенного модуля (формы, отчета и т. п.) в сре- среду разработки лучше выполнять одновременно. И не забывайте сохранить моди- модифицированную библиотеку перед использованием в присоединенном к ней моду- модуле новых элементов.
786 Глава 20 • Выполнение программ PL/SQL ПРИМЕЧАНИЕ Если модификация библиотеки и присоединенного модуля выполняется в течение одного сеанса ре- редактирования, то для того чтобы изменения, произведенные в библиотеке, стали доступными в мо- модуле, необходимо сделать следующее. При условии, что во время выполнения приложения исполь- используется только PLL-файл, библиотеку PL/SQL нужно сохранить (File > Save). Если в таи же каталоге имеется PLX-файл библиотеки, ее нужно сгенерировать повторно (File > Administration > Compile File или Program > Compile). Клиентские библиотеки PL/SQL во время выполнения Поиск библиотеки PL/SQL на этапе выполнения осуществляется точно так же, как и на этапе разработки: если библиотека не вызывается с явным указанием пути, приложение ищет ее в каталогах, задаваемых переменными среды. Одной из особенностей библиотек PL/SQL является способ их загрузки в па- память. Во время выполнения в память сначала считывается только «каталог» биб- библиотеки. Затем, когда модуль в первый раз вызывает конкретную процедуру, функ- функцию или пакет, загружается их код, блоками по 4 Кбайт. Напомним, что при за- запуске приложения PL/SQL в память помещается весь код модуля. ПРИМЕЧАНИЕ- Поскольку библиотеки PL/SQL загружаются в память не сразу и не целиком, а по требованию, раз- размещение болышх клиентских программ в библиотеках, а не в отдельных файлах модулей приложе- приложения, помогает оптимизировать использование памяти. Если присоединить библиотеку PL/SQL к нескольким формам, они смогут совместно использовать информацию о состояния библиотеки, то есть опериро- оперировать переменными пакета как глобальными структурами. Для этого программе CALL_FORM нужно передать специальный аргумент: CALL_FORM(forimiodiile_name -> 'BOOKS', datajnode -> SHARE_LIBRARY_DATA); Если значение параметра datajnode не будет задано, по умолчанию последний будет содержать установку NO_SHARE_LIBRARY_DATA, означающую, что каждая форма создает собственные экземпляры программ и переменных, используемых в биб- библиотеке. РАЗРЕШЕНИЕ ИМЕН В КЛИЕНТСКОМ PL/SQL Перенести многократно используемый код в библиотеку PL/SQL доста- достаточно сложно. Объясняется это, в частности, тем, что в библиотечном коде нельзя непосредственно ссылаться на объекты уровня модуля. Компиля- Компиляция библиотеки должна выполняться успешно независимо от конкретных модулей приложения. Поэтому в модуль Oracle Forms можно включить такую инструкцию: IF :boolcblock.summary IS NULL THEN :bOGkblock.sumnary :- 'TO BE SUPPLIED1; END IF; Здесь :bookblock.surrniai"y — это имя поля на экране. Однако если попро- попробовать выполнить те же действия в коде библиотеки PL/SQJL, компи- компилироваться она не будет, поскольку в ней ничего не известно об этом поле.
Модели разрешений 787 Поэтому разрешение ссылок на поля нужно как-то отложить до времени выполнения. Для решения этой проблемы Oracle предоставляет две специализирован- специализированные встроенные программы: NAME_IN, считывающую значение элемента, имя которого задается строковым аргументом, и COPY, присваивающую элемен- элементу значение. С их помощью предыдущий пример можно переписать по- другому: IF №ME_INC':bookblock.summary'} IS NULL THEN COPYCTO BE SUPPLIED1 .':bookb1ock.summary'): END IF; Это решает проблему компиляции, но, как и в случае использования лю- любой другой технологии динамического кодирования, создает иную про- проблему: обнаружить ошибку теперь можно будет лишь во время выполне- выполнения. Компилятор не станет анализировать строку символов и пытаться определить, является ли она реальным именем элемента. Однако если код тщательно протестировать, данную проблему можно отнести к разряду незначительных. Модели разрешений Во всех версиях системы вплоть до Oracle8i для выполнения хранимых программ всегда требовалось разрешение владельца, или создателя. Это не имело особого значения, если код и данные приложения создавались пользователем с одной и той же учетной записью. Разрешения, задаваемые для дентрализованно хранящегося кода, не переносились автоматически на объекты пользователей (называемые также разрешениями вызывающего). Пользователь мог не иметь разрешения на удале- удаление конкретной таблицы, но если для хранимого кода было задано это разрешение, таблица благополучно удалялась! Однако бывают случаи, например, при исполь- использовании в программе пакета DBMS_SQL (динамический SQL), когда из-за такого по- поведения Oracle могут возникнуть определенные сложности. В Oracle 8.1 у программиста появилась возможность указывать, какие разре- разрешения необходимы для выполнения программы (или всех программ пакета): при- привилегии создателя (что было единственно возможно в Oracle 8.0 и более ранних версиях) или привилегии вызывающего. Модель разрешений владельца Очень важно понимать особенности обеих моделей разрешения на выполнение программ, поскольку во многих приложениях PL/SQL возможно их сочетание. Перед выполнением хранимая программа должна быть откомпилирована и запи- записана в базу данных. Поэтому она всегда сохраняется в конкретной схеме или с кон- конкретной учетной записью пользователя, даже если в ней имеются ссылки на объек- объекты из других схем.
788 Глава 20 • Выполнение программ PL/SQL При использовании модели разрешений создателя действуют следующие пра- правила. О Разрешение всех внешних ссылок в программе выполняется во время компи- компиляции, с использованием непосредственно предоставленных привилегий схе- схемы, в которой эта программа компилируется. О Роли базы данных действительны при компиляции анонимных блоков, но пол- полностью игнорируются при компиляции хранимых программ. О При запуске программы, откомпилированной с использованием модели раз- разрешений создателя (по умолчанию), ее SQL-инструкции выполняются с раз- разрешениями схемы, которой принадлежит эта программа. О Хотя на выполнение компиляции программы требуется особое разрешение, привилегию EXECUTE, позволяющую выполнять эту программу, можно предос- предоставить другим схемам и ролям. На рис. 20.11 показано, как использовать модель разрешений создателя для управления доступом к объектам базы данных. Все данные заказа хранятся в схе- схеме OEData, а код, выполняющий ввод заказов, хранится в схеме OECode. Владельцу схемы OECode предоставлены непосредственные привилегии, необходимые для ком- компиляции пакета Order_Ngt и позволяющие вводить и удалять заказы. Схим OECode Пакет Order.Mgt Схама OEData Задоы Схема Sam.Sale» Процедура Close OkJOrdars 'Нвпосредатвхияя коЛифшацш) таблицы невозтшт Рис. 20.11. Управление доступом к данным с помощью модели разрешений создателя Для обеспечения возможности корректного обновления таблицы заказов не- необходимо запретить прямой доступ к ней (будь то через роли или привилегии) из всех схем, кроме OECode. Предположим, что из схемы Sam_Sal es нужно просмотреть все открытые заказы и захрыть те из них, которые были сделаны раньше опреде- определенной даты. Ассоциированный с этой схемой пользователь Sam не может выпол- выполнить инструкцию DELETE из процедуры Cl ose_01 d_Orders; вместо этого он должен вызвать процедуру OrderMgt. cancel.
подели разрешении Преимущества модели разрешений создателя В некоторых ситуациях вам просто не обойтись без модели разрешений создателя, обладающей в сравнении с моделью с разрешениями вызывающего рядом пре- преимуществ. О Разработчик имеет больше возможностей для управления доступом к струк- структурам данных и может быть уверен, что единственным способом изменения содержимого таблицы является использование созданного им программного интерфейса (обычно пакета). О Производительность приложения значительно выше, поскольку ядру PL/SQL не приходится во время выполнения проверять наличие разрешений на дос- доступ к объектам или, что не менее важно, выяснять, с какой именно таблицей должна работать программа (поскольку одна таблица может очень отличаться от другой таблицы accounts!). О Не нужно беспокоиться о том, что программа может обратиться не к той таб- таблице. При использовании разрешений создателя программный код будет об- обрабатывать те же структуры данных, с которыми работали бы SQL-инструк- SQL-инструкции, выполняемые из SQL*Plus (или другого приложения). Недостатки модели разрешений создателя Однако использование модели разрешений создателя может вызвать определен- определенные проблемы. О них рассказывается в следующих разделах. Куда исчезла таблица Попытаемся разобраться, какое значение имеют перечисленные правила исполь- использования модели разрешений создателя для разработчика программ PL/SQL в его повседневной работе. Разработчики нередко пишут код, который выполняет об- обработку таблиц и представлений из других схем, создавая для них общие синони- синонимы, которые попросту скрывают схемы. Привилегии при этом предоставляются посредством ролей базы данных. Эта очень распространенная модель имеет несколько нехороших особенно- особенностей. Предположим, что в сети вашей компании управление доступом к объектам реализуется посредством ролей. При работе с таблицей accounts вы можете спо- спокойно выполнить в SQL*Plus такой запрос: SQL> SELECT account*, name FROM accounts: Однако если эту же таблицу (и даже тот самый запрос) указать в процедуре, PL/SQL выдаст два сообщения об ошибках: SQL> CREATE OR REPLACE PROCEDURE showaccounts 2 IS 3 BEGIN 4 FOR rec IN (SELECT account*, name FROM accounts) 5 LOOP 6 OBKS_OUTPuT.PUT_LINE (rec.nane); 7 ENO LOOP; 8 END; 9 / Warning: Procedure created with compilation errors.
i лава zv • выполнение программ SQL> sho err Errors for PROCEDURE SHOW_ACCOUNTS: LINE/COL ERROR 4/16 PL/SQL: SOL Statement ignored 4/43 PLS-00201: identifier 'ACCOUNTS' must be declared Странно, не правда ли? Проблема заключается в том, что таблица accounts на самом деле входит в состав другой схемы; для получения доступа к данным вы использовали синонимы и роли, но это не сработало. Так что если вы когда-ни- когда-нибудь столкнетесь с подобной ситуацией, просто обратитесь к администратору базы данных или владельцу объекта и попросите предоставить вам необходимые при- привилегии, Как поддерживать весь этот код Предположим, что в каждом филиале вашей компании используется собственная схема базы данных. Такая ситуация может сложиться в том случае, если в каждом филиале производится сбор и обработка специфических, характерных только для него данных. Во всех схемах имеются собственные таблицы с одинаковой струк- структурой, но с разными данными. (С точки зрения проектирования базы данных это неправильно, но что поделаешь — так уж получилось.) Теперь мы хотели бы так адаптировать код, чтобы сопровождение приложения требовало минимум време- времени и усилий. Для этого можно поместить его в одну схему и предоставить доступ к нему из объектов всех схем филиалов. Однако при использовании модели разрешений создателя так поступить не- невозможно. Если поместить код в определенную схему и задать для объектов всех схем филиалов право на выполнение EXECUTE, все они смогут работать с любым набором таблиц, указанных в центральной схеме (с набором одного филиала или же с набором таблиц-макетов). Но так делать не стоит. Придется добавлять весь наш код в каждую региональную схему, как показано на рис. 20.12. Не имеет смысла говорить, в какой кошмар превратится сопровождение и мо- модификация такого кода. Конечно же, модель разрешений вызывающего должна обеспечить лучшее решение. Динамический SQL и разрешения создателя Еще одним типичным источником проблем, связанных с разрешениями создате- создателя, является динамический SQL (описан в главе 15). Предположим, у нас имеет- имеется универсальная программа для выполнения DDL-инструкций: /* Файл в web: execddl.sp */ CREATE OR REPLACE PROCEDURE execDDL (ddl_String IN VARCHAR2) BEGIN EXECUTE IMMEDIATE ddl_str1ng: EXCEPTION WHEN OTHERS THEN DBMS OUTPUT.PUTJ.INE ('Dynamic SQL Failure: ' || SOIERRM); DBMS~OUTPUT.Pltf_LINE ( ' on statement: "' || ddl_string || ""): RAISE; END;
Модели разрешений 791 Рис. 20.12. Многоразовая инсталляция кола при использовании модели разрешений создателя Протестировав этот код, мы решили, что им могут пользоваться и остальные разработчики. Откомпилировав его в схеме COMMON, где обычно хранится весь об- общедоступный код, мы передали разрешение на его выполнение пользователю с при- привилегией public, создали общий синоним, после чего разослали по электронной почте сообщение о новой замечательной программе. Несколько недель спустя наверняка начнут поступать сообщения от коллег типа: «Я просил создать таблицу, и код выполнился без ошибок, но таблицы по- почему-то нет» или «Я просил удалить таблицу, но программа ответила, что тако- таковой не существует»-. В общем идея ясна. В аналогичной ситуации в свое время очутился и автор настоящей книги. Поначалу он засомневался в такой техноло- технологии совместного использования кода, но потом решил провести некоторые иссле- исследования. Получив доступ к объектам схемы COMMON, автор обнаружил там все объекты, которые пользователи пытались создать с помощь его процедуры. Оказывается, если при вызове процедуры execDDL пользователь попросит создать таблицу и не укажет имя собственной схемы, процедура создаст таблицу схемы COMMON. Что, впрочем, и не удивительно. Иными словами, вызов SQL> EXEC execDDU'CREATE TABLE newoneCnghtnow DATE)') создаст таблицу newone схемы COHMON. Вызов процедуры execDDL SQL> EXEC execDDLC'CREATE TABLE Sccrtt.newonetnghtnow DATE)')
792 Глава 20 • Выполнение программ PL/SQL мог бы решить проблему, но он, к сожалению, завершается следующим сообще- сообщением об ошибке (если только для схемы COMMON не предоставлена привилегия CRE- CREATE ANY TABLE): ORA-01031: Insufficient privileges Увы, такая хорошая процедура, а предоставить ее в распоряжение других про- программистов, не создав им и себе дополнительных проблем, невозможно. А как было бы здорово, если бы все могли запускать ее как свою, с собственными при- привилегиями (а не с привилегиями схемы COMMON), не устанавливая собственную ко- копию кода этой процедуры. Модель разрешений на выполнение программ Для того чтобы помочь разработчикам преодолеть проблемы, связанные с ис- использованием модели разрешений создателя, в Oracle 8.1 была введена альтерна- альтернативная модель разрешений на выполнение программ, а именно модель разрешений вызывающего. Суть ее заключается в том, что разрешение всех внешних ссылок в SQL-инструкциях и программах PL/SQL выполняется в соответствии с приви- привилегиями схемы вызывающего программу пользователя, а не владельца схемы, в ко- которой данная программа определена. Принципиальное отличие модели разрешений вызывающего от модели разре- разрешений создателя иллюстрирует рис. 20.13. Вспомните описываемый выше при- пример (рис. 20.12), когда для получения возможности обрабатывать таблицу копии приложения пришлось поместить в схемы всех филиалов. Модель разрешений вызывающего исключает такую необходимость. Теперь весь код можно помес- поместить в единое централизованное хранилище. И когда пользователь из Северного региона вызовет централизованно хранящуюся программу (возможно, восполь- воспользовавшись синонимом), она автоматически получит доступ к таблицам соответ- соответствующей схемы. Северный офис Цотральнодтпантичаскмй офис : . Рис 20.13. Использование модели разрешений вызывающего
Модели разрешений 793 Как видите, идея модели разрешений вызывающего очень проста. Рассмот- Рассмотрим, как она реализуется программно, а затем поясним, как ее лучше использо- использовать. Синтаксис модели разрешений вызывающего Синтаксис поддержки модели разрешений вызывающего очень прост. В заголо- заголовок программы, перед ключевым словом IS или AS, добавляется следующее пред- предложение: AUTHID CURRENTJJSER В качестве примера рассмотрим все ту же процедуру execDDL, но на этот раз объявленную с разрешениями вызывающего: /* Файл в web: execddl.sp */ CREATE OR REPLACE PROCEDURE execDDL (ddl_Str1ng IN VARCHAR2) AUTHID CURRENTJJSER IS BEGIN EXECUTE IMMEDIATE ddl_5tnng; END: Предложение AUTHID CURRENTJJSER перед ключевым словом IS указывает, что при выполнении процедуры execDDL должны использоваться привилегии вызы- вызывающего или текущего пользователя, но не привилегии ее создателя. Если не включить в программу предложение AUTHID или задать в нем параметр AUTHID DEFINER, разрешение ссылок будет выполняться в соответствии с привилегиями схемы-владельца программы. РАЗРЕШЕНИЯ ВЫЗЫВАЮЩЕГО ДЛЯ ДИНАМИЧЕСКОГО SQL Автор написал сотни программ с использованием динамического SQL, но до появленияОгаск 8.1 ему постоянно приходилось беспокоиться о разре- разрешениях, функциональных возможностях и содержимом схем: в какой схеме выполняется программа? кто ее вызывает? что произойдет, если кто-ни- кто-нибудь ее запустит? Вечные вопросы! Возможно, у вас появится мысль включать предложение AUTHID CURRE(fT_ USER в каждую хранимую программу, в которой используется динамиче- динамический SQL. Сделав так, вы сможете пребывать в полной уверенности, что в какой бы схеме не была откомпилирована программа и кто бы ее ни за- запустил, она всегда будет работать в текущей схеме. Все это так, но есть одна проблема: при запуске программы с разрешения вызывающего необходимо производить ряд дополнительных проверок, что может отрицательно сказаться на производительности системы. Поэтому модель разрешений вызывающего следует использовать продуманно. Ограничения на использование модели разрешений вызывающего При выборе данной модели разрешений нужно помнить о существовании ряда правил ее применения и всегда их выполнять. Не следует игнорировать и уста- установленные ограничения.
794 Глава 20 • Выполнение программ PL/SQL О По умолчанию для программ задается параметр AUTHID DEFINER. О В случае применения любых SQL-ссылок на объекты базы данных модель раз- разрешений вызывающего требует проверки во время выполнения программы привилегий, непосредственно предоставленных вызывающему. О При использовании модели разрешений вызывающего роли действуют во вре- время выполнения программы, если только программа с разрешениями вызываю- вызывающего не была вызвана из программы с разрешениями создателя. О Предложение AUTHID может содержаться только в заголовках отдельных про- программ (процедур или функций), в спецификации пакета или в спецификации объектного типа, но его нельзя задавать в отдельных программах пакета или методах объектного типа. О Разрешение внешних ссылок с привилегиями вызывающего будет действо- действовать для следующих видов инструкций: • инструкций языка манипулирования данными SELECT, UPDATE, INSERT и DELETE; • инструкции управления транзакциями LOCK TABLE; • инструкций управления курсорами OPEN и OPEN FOR; • инструкции динамического SQL EXECUTE IMMEDIATE и OPEN FOR USING; • SQL-инструкций, проанализированных с помощью процедуры DBMSSQL. PARSE. О Для разрешения всех внешних ссылок на программы PL/SQL и методы объект- объектных типов во время компиляции всегда используются привилегии создателя. О Привилегии вызывающего можно использовать для изменения разрешения ссылок на статические элементы данных (таблицы и представления). Привилегии вызывающего могут использоваться для разрешения внешних ссы- ссылок на программы PL/SQL. Вот один из примеров того, как это делается: EXECUTE IMMEDIATE 'BEGIN программ: ЕЖ;1: В данном операторе ссылка програнна будет разрешена во время выполнения программы в соответствии с привилегиями и пространством имен вызывающего. В качестве альтернативы вместо анонимного блока можно использовать SQL-опе- SQL-оператор CALL. (Непосредственно в PL/SQL этот оператор применять нельзя, посколь- поскольку здесь он не поддерживается.) Комбинированная модель разрешений Как вы думаете, что произойдет, если программа с разрешениями создателя вызо- вызовет программу с разрешениями вызывающего? А если наоборот? Все очень просто. О Если программа с разрешениями создателя вызовет программу с разрешения- разрешениями вызывающего, при выполнении такой программы будут действовать при- привилегии вызывающей программы. О Если программа с разрешениями вызывающего вызовет программу с разреше- разрешениями создателя, при выполнении такой программы будут действовать при- , вилегии ее владельца. Когда управление будет возвращено вызывающей про- программе, возобновится действие разрешений вызывающего. Иными словами, разрешения создателя «сильнее» разрешений вызывающего.
Аппаратное обеспечение для PL/SQL: больше — лучше? 795 На узле издательства O'Reilly имеется несколько файлов, которые помогут вам изучить нюансы использования модели разрешений вызывающего. О invdefinv.sql и Invdefinv.tst. Эти два сценария демонстрируют преобладание раз- разрешений создателя над разрешениями вызывающего. О invdef_overhead.tst Демонстрируются издержки использования модели разре- разрешений вызывающего (выполнение разрешения ссылок в процессе компиля- компиляции дешевле, чем в процессе выполнения, так как требует меньше времени). О invrole.sql. Показано, как изменение ролей может отразиться на разрешении ссылок на объекты во время выполнения программы. О Irdynsql.sql. Описаны некоторые сложности, связанные с использованием обе- обеих моделей с динамическим SQL. Аппаратное обеспечение для PL/SQL: больше — лучше? В завершение главы речь пойдет об аппаратном обеспечении сервера и его влия- влиянии на производительность программ PL/SQL Как правило, аппаратная часть сервера имеет одну из следующих конфигураций: О однопроцессорная система; О симметричная многопроцессорная система; О «кластерная» система. Как и любое другое программное обеспечение, которое выполняется в симмет- симметричной многопроцессорной или кластерной системе, Oracle использует дополни- дополнительные процессоры отнюдь не автоматически. Их эффективное применение воз- возможно лишь при условии, что программное обеспечение разработано специально для распределения вычислительных задач между процессорами. Программисты Oracle хорошо потрудились, и теперь этот продукт с успехом использует возмож- возможности мультипроцессорной обработки. Однако такая возможность поддерживается не всеми версиями Oracle. Для того чтобы иметь возможность применять мультипроцессорное аппаратное обес- обеспечение, необходимо установить версию Oracle Enterprise Edition или Personal Edition. Единственным исключением является массовая загрузка данных: во всех версиях Oracle при выполнении этой операции происходит распараллеливание нагрузки между процессорами. Для использования возможностей кластерных сис- систем нужна не только Oracle Enterprise Edition, но и дополнительная лицензия на компонент, называемый в Oracle9i Real Application Clusters (RAC), а в предыду- предыдущих версиях РСУБД - Oracle Parallel Server (OPS). Но смогут ли дополнительные затраты на аппаратное обеспечение повысить производительность PL/SQL? Чтобы ответить на данный вопрос, рассмотрим все три базовые конфигурации более подробно.
796 Глава 20 • Выполнение программ PL/SQL Однопроцессорная система Если вы, занимаясь индивидуальной разработкой приложений, установили Oracle на настольном компьютере, то, вероятнее всего, при реализации всех вычисли- вычислительных задач, в том числе и программ PL/SQL, будете использовать только один процессор. Ведь для работы большинства приложений объема оперативной памя- памяти и памяти на жестких дисках однопроцессорной системы вполне достаточно. Узким местом таких систем является быстродействие центрального процессора. Способ модернизации компьютера в этом случае очевиден: установление более мощного процессора. Однако если тщательно спроектированное приложение вы- выполняется на компьютере с достаточным объемом памяти и высоким быстродей- быстродействием устройств ввода-вывода, мощная однопроцессорная система может позво- позволить Oracle обслуживать сотни одновременно работающих пользователей (по край- крайней мере для некоторых приложений). Если установить в однопроцессорный компьютер более мощный процессор, производительность" PL/SQL почти наверняка повысится, но на быстродействие системы ввода-вывода такого рода модернизация не окажет существенного влияния. Симметричные мультипроцессорные системы Для решения более сложных задач требуются вычислительные системы, обычно включающие не менее двух процессоров. Конфигурация, при которой процессо- процессоры имеют общую память, совместно используют устройства ввода-вывода и рабо- работают под управлением единой операционной системы, называется симметричной. Опыт показывает, что не все программы в многопроцессорных системах вы- выполняются быстрее, чем в однопроцессорных. Например, программа PL/SQL, ко- которая содержит SQL-инструкцию, сортирующую таблицу объемом в миллион строк, в многопроцессорной системе будет выполняться быстрее, чем в однопроцессор- однопроцессорной. Однако приложение, выполняющее обработку единичных записей, вряд ли будет быстрее функционировать в многопроцессорной системе. В общем случае системы параллельного действия больше подходят для выполнения задач, свя- связанных с принятием решений, чем, скажем, системы интерактивной обработки транзакции. Предположим, что вы для создания очень большого хранилища данных при- приобрели многопроцессорную машину с соответствующей операционной системой и лицензированной версией Oracle Enterprise Edition или Personal Edition. Те- Теперь вам предстоит написать программу PL/SQL. Сумеете ли вы воспользовать- воспользоваться преимуществами многопроцессорной обработки? Возможно, но лишь при ус- условии, что ваша программа будет соответствовать определенным требованиям. Перечислим основные из них. О Программа должна быть реализована как функция, а не как процедура или анонимный блок. О Она должна вызываться из SQL-инструкции, которую оптимизатор Oracle в лю- любом случае будет выполнять в параллельном режиме. О В функции не должны применяться инструкции INSERT, UPDATE или DELETE, ко- которые модифицируют информацию в базе данных (причем это требование не- невозможно обойти посредством вызова другой программы).
Что вам нужно знать 797 О Функция не должна читать или модифицировать данные пакета, поскольку информация о его состоянии не может совместно использоваться нескольки- несколькими параллельно выполняющимися экземплярами программы. Итак, Oracle может выполнять копии программы PL/SQL на разных процес- процессорах, но лишь в том случае, если это вычислительная программа, выполняемая как вспомогательный компонент SQL-инструкции. Более подробно о параллель- параллельном выполнении хранимых функций рассказывается в главе 16. Кластерные системы Кластеризация - это процесс объединения нескольких компьютеров, каждый из которых имеет собственный процессор и память, благодаря чему они могут па- параллельно выполнять большие и достаточно независимые задания. Ранее необя- необязательный компонент для поддержки кластеризации назывался Oracle Parallel Server (OPS). В Oracle9i используется новая технология, названная Real Appli- Application Clusters (RAC). Обе технологии предполагают функционирование в раз- разных кластерах взаимодействующих между собой экземпляров РСУБД, которые вместе обрабатывают запросы приложений и используют общую базу данных. Одним из главных преимуществ RAC перед OPS является простота, с которой приложения могут использовать возможности дополнительного аппаратного обес- обеспечения. В документации Oracle утверждается, что «приложения можно масшта- масштабировать, не меняя ни строчки кода». Однако в примечании указано, что это воз- возможно при соблюдении двух условий: О проектирование базы данных в обязательном порядке производится с учетом технологии RAC; О приложения разбиваются на отдельные части, каждая из которых может вы- выполняться на отдельном узле. По мере увеличения размера базы данных систему можно масштабировать, добавляя в кластер новые узлы. Однако увеличение количества пользователей наверняка потребует изменения архитектуры приложения. Честно говоря, у нас не было случая проверить эти утверждения Oracle. Еще одним несомненным преимуществом кластерных систем является их на- надежность. База данных будет поддерживаться в рабочем состоянии даже в том случае, если один из компьютеров кластера выйдет из строя. Некоторые из функ- функций OPS и RAC устанавливаются автоматически, тогда как для других требуется дополнительное конфигурирование базы данных. В Oracle8i применяемый для этой цели компонент назывался Oracle Parallel Fail Safe, а в Oracle9i он был заме- заменен компонентом Real Application Clusters Guard. Мы не станем рассказывать о данных технологиях даже несмотря на их исключительную важность, так как это выходит за рамки книги. Достаточно подробная информация о них содержится в специальной документации по Oracle. Что вам нужно знать Вам действительно нужно помнить все, о чем рассказывалось в данной главе? Ду- Думаем, что нет, хотя у любого администратора базы данных на этот счет имеется собственное мнение. Главная задача главы — расширить ваши представления об
798 Глава 20 • Выполнение программ PL/5QL Oracle и о том, что собой представляют процессы компиляшш и выполнения кода. Однако наряду с изложением информации, просто расширяющей кругозор чита- читателя, здесь освещается ряд важных тем, которые нужно усвоить как следует. При- Примите во внимание следующие рекомендации. О Во избежание дополнительных затрат на перекомпиляцию код, предназначен- предназначенный для многократного использования, следует реализовывать в виде храни- хранимых программ, а не записывать файлы как анонимные блоки. О Помимо уникальной способности пакетов PL/SQL сохранять параметры со- состояния в течение всего сеанса у них есть и другие достоинства. В частности, объединение кода в пакеты позволяет повысить его производительность. По- Поэтому наиболее интенсивно использующуюся логику приложения следует реа- лиэовывать в виде программ, входящих в состав пакетов. О Если вы не хотите изучать структуру словаря данных Oracle, поищите подхо- подходящую утилиту для разработчиков. О Хотя автоматическое управление зависимостями избавляет программистов от многих проблем., обновление приложений в процессе их эксплуатации следует выполнять очень осторожно. О В Oracle реализованы сложные технологии, предназначенные для минимизации ресурсов компьютера, которые необходимы для выполнения программ PL/SQL Однако для их поддержки иногда требуется небольшая помощь разработчи- разработчиков или администратора базы данных (например, когда речь идет о явном ос- освобождении неиспользуемой пользователем памяти или закреплении объек- объектов в памяти). О Открыв явный курсор в программе PL/SQL, обязательно закройте его сразу по окончании выборки данных. О Встроенные средства компиляции PL/SQL не всегда влияют на производи- производительность приложений, в которых интенсивно используются SQL-инструкции, однако они значительно повышают производительность программ, выполняю- выполняющих большой объем вычислений. О К вызову удаленных пакетов предъявляются особые требования, если вы не об- обращаетесь к процедурам, функциям, типам и подтипам, определенным в пакете. О В случае применения клиентских средств Oracle совместно используемый кли- клиентский код, устанавливаемый в различных компьютерных системах, следует помещать в библиотеки PL/SQL. О Для повышения производительности приложений и упрощения процедуры управления привилегиями на доступ к объектам базы данных лучше исполь- использовать модель разрешений создателя. Однако не стоит отказываться и от мо- модели разрешений вызывающего, которая может помочь при решении ряда про- проблем (скажем, связанных с управлением программами, которые посредством динамического SQL создают и удаляют объекты базы данных.) О Повысить производительность PL/SQL можно путем применения более бы- быстрых процессоров, установления памяти большего объема и более быстро- быстродействующей системы ввода-вывода. Для решения сложных задач целесообраз- целесообразно использовать системы параллельного действия, однако обойтись без мощ- мощных процессоров все равно не удастся
21 Объектно- ориентированные возможности PL/SQL > Обзор объектных возможностей Oracle > Пример объектного приложения > Объектные представления > Сопровождение объектных типов и объектных представлений > О целесообразности применения объектно-ориентированного подхода PL/SQL, всегда был языком, поддерживающим традиционные процедурные сти- стили программирования, в частности структурное проектирование и функциональ- функциональную декомпозицию. Пакеты PL/SQ.L позволяют использовать также объектно- ориентированный подход, применяя в работе с реляционными таблицами прин- принципы абстракции и инкапсуляции. В более новых версиях Oracle введена непосредственная поддержка объектно- ориентированного программирования (ООП). Программистам стали доступны бо- богатая и сложная система типов, их иерархия, а также возможность взаимоподста- взаимоподстановки. Хотя тема объектно-ориентированного программирования в Oracle могла бы стать предметом отдельной книги, мы можем привести лишь несколько примеров программного кода, демонстрирующих наиболее значительные аспекты объектно- ориентированного программирования на PL/SQL: О создание и использование объектных типов; О наследование и взаимозаменяемость; О эволюция типов; О излечение данных на основе REF-ссылок; О объектные представления. В этой главе приведены синтаксические диаграммы 5О_Ь-инструкций, рассмот- рассмотрены принципы администрирования баз данных (импорт и экспорт объектных
800 Глава 21 • Объектно-ориентированные возможности PL/SQL данных), а также вопросы, касающиеся хранения данных на диске. Начнем главу с краткого исторического экскурса. Обзор объектных возможностей Oracle Впервые появившиеся в 1997 году как дополнение к Огас1е8 (так называемой объектно-реляционной базе данных) объектные возможности позволили разра- разработчикам расширить набор встроенных типов данных Oracle, включив в него аб- абстрактные типы данных. Помимо них в Огас1е8 были введены определяемые программистом коллекции (о которых рассказано в главе 11), оказавшиеся очень полезными не только потому, что разработчики давно уже искали способы хране- хранения массивов в базах данных, но еще и потому, что PL/SQL предоставил новый способ запрашивания информации из коллекций как из таблиц. Объектная мо- модель Oracle обеспечивает много интересных возможностей, в частности доступ к данным с помощью указателей, но она не поддерживает ни наследования, ни . динамического полиморфизма, и поэтому объектно-ориентированные средства Огас1е8 вряд ли найдут сторонников среди приверженцев настоящего ООП. Слож- Сложность и низкая производительность объектных функций также не способствуют их успеху. В Oracle8i была введена поддержка хранимых процедур Java, которые позво- позволили работать с РСУБД при помощи не столь специализированного языка, как PL/SQL, и стали для приверженцев ООП аргументом в пользу разработки хра- хранимых процедур. Oracle предоставила способ преобразования объектных типов, определенных на сервере, в Java-классы, тем самым обеспечив им переход границы Java-база данных. Версия Oracle8i вышла в период наивысшего интереса к языку Java, и поэтому немногие заметили, что объектные функции Oracle мало измени- изменились — разве что Oracle начала потихоньку интегрировать их с базовым сервером. Это означает, что для использования объектных функций больше не требуется дополнительная лицензия. В то время один из представителей Oracle сказал о бу- будущем ООП на языке PL/SQL: «Если вам требуется настоящее объектно-ориен- объектно-ориентированное программирование, пользуйтесь Java». Однако в Огас1е9: встроенная поддержка объектов была значительно расши- расширена, так что пуристы ООП могут уже начинать к ней присматриваться. Введена поддержка наследования и полиморфизма в базах данных, PL/SQL оснащен но- новыми объектными средствами. Имеет ли смысл расширять объектную модель сис- системы на структуру базы данных? Следует ли переписать существующие прило- приложения клиентского и промежуточного уровней? Как показано в табл. 23.1, Oracle осуществила огромный прорыв в ООП, и переход на эту технологию может пока- показаться очень заманчивой идеей. Наряду с поддерживаемыми элементами (отме- (отмечены звездочкой) в таблице перечислены возможности, которые пока не реализо- реализованы, но были бы очень желательны. Если вы не имеете большого опыта объектно-ориентированного программи- программирования, многие термины из табл. 12.1 могут быть вам не знакомы. О том, что они означают, рассказывается далее в этой главе.
Обзор объектных возможностей игэае Таблица 21.1. Объектно-ориентированные возможности различных версий базы данных Orade Средства и возможности Oradea Orackdl Orade9l Release 1 0racle9l Release 2 Абстрактные типы данных как сущности базы данных верхнего уровня Абстрактные типы данных как параметры PL/SQL Атрибуты типа коллекции Атрибуты типа REF для навигации по объектам внутри базы данных Реализация логики методов в PL/SQL или С Определяемая программистом семантика сравнения объектов Представление реляционных данных как данных объектных типов Статический полиморфизм (перегрузка методов) Возможность «эволюции» типа путем модификации логики существующих методов или добавления методов Реализация логики методов в Java «Статические» методы (выполняемые без экземпляра объекта) Первичный ключ таблицы реляционной базы данных может служить идентификатором хранимого объекта, обеспечивая декларативную целостность REF-ссылок Наследование атрибутов и методов от определяемого пользователем типа Динамическая диспетчеризация методов Супертипы, для которых Hie могут создаваться экземпляры, подобные «абстрактным классам» Java Возможность эволюции типа путем удаления и повторного добавления метода (для изменения сигнатуры) продолжение^
802 Глава 21 • Объектно-Ориентированные возможности PL/SQL Таблица 21.1 (продолжение) Средства и возможности OradeS Oracleai 0racle9l OracleSi Release 1 Release 2 Возможность эволюции типа путем * • добавления и удаления атрибутов, автоматическое распространение изменений на связанные структуры физической базы данных «Анонимные» типы: ANYTYPE, • • ANYDATA, ANYDATASET Операторы приведения базового типа * * к дочернему (TREAT) и определения типа (IS OF), доступные в SQL Операторы TREAT И IS OF в PL/SQL * Определяемые пользователей * конструкторы Личные атрибуты, переменные, константы и методы Наследование от нескольких супертипов Совместное использование объектных типов или экземпляров в распределенных базах данных без обращения к объектным представлениям Пример объектного приложения Пример, к которому мы будем обращаться на протяжении всей главы, основан на новых возможностях, введенных в Oracle9i. Многое, о чем говорится в этой главе, относится только к Огас1е9« Release 2, поэтому если вам требуется обеспечить объектную ориентацию сервера, никакая из версий ниже Огас1е9: для этой задачи не подойдет. Иерархия типов Идея для примера, рассмотренного в этой главе, взята из книги Learning Oracle PL/SQL Рюдгаттщ (издательство O'Reilly). Мы построим систему на базе Oracle, в которой с помощью объектно-ориентированного подхода реализован обыкно- обыкновенный библиотечный каталог. В каталоге хранится информация о книгах, пе- периодических изданиях (журналах и газетах) и непечатных изданиях. Графическое представление типов верхнего уровня каталога можно увидеть на рис. 21.1. В последующем иерархия типов будет дополнена объектами, кото- которые показаны пунктиром.
Пример объектного приложения 803 Подтип, «mopte ВуЛутаявти позже \ Рис. 21.1. Иерархия типов обычного библиотечного каталога Создание базового типа Корневой элемент (вершина иерархии) представляет общие характеристики всех подтипов. Предположим, что у книг и периодики имеются только две общие ха- характеристики: библиотечный идентификационный номер и название. Таким об- образом, объектный тип для элемента каталога можно создать в SQL'Plus с помо- помощью следующей SQL-инструкции: CREATE OR REPLACE TYPE catalog_Uem_t AS OBJECT ( id INTEGER, title VARCHAR2D000), NOT INSTANTIATE MEMBER FUNCTION ck digit_oka,y RETURN BOOLEAN. MEMBER FUNCTION print RETURN VARCHAR2 > NOT INSTANTIATE NOT FINAL; Эта инструкция создает объектный тип, подобный классу Java или C++. Если провести аналогию с реляционной моделью, объектный тип подобен типу запи- записи с набором связанных с ним функций и процедур (в объектной терминологии методов). Ключевые слова NOT FINAL в конце определения типа указывают, что он может служить базовым или а/пертипом, от которого будут производиться другие типы. В данном случае они использованы, поскольку мы собираемся создать подтипы для книг и периодических изданий; если опустить эти ключевые слова, по умол- умолчанию Oracle использует только слово FINAL, означающее, что создание подтипов на основе данного типа не допускается. Обратите внимание на то, что спецификация данного типа содержит предло- предложение NOT INSTANTIATE. Это означает, что PL/SQL позволит объявлять перемен- переменные типа catalog_1tem_t, но им нельзя будет присваивать значения - ни явно, ни другим способом, Подобно абстрактному классу Java, данный тип предназначен исключительно для использования в качестве базовой основы для создания под- подтипов (для которых, конечно, можно создавать и экземпляры объектов).
804 Глава 21 • Объектно-ориентированные возможности PiySQL Для примера и удобства отладки мы включили в определение типа метод pri nt (кстати, это не зарезервированное слово), позволяющий описать объект одной строкой. При создании подтипа метод будет перегружен, то есть подтип будет со- содержать метод с тем же именем, но возвращающий атрибуты подтипа. Заметьте, что вместо того, чтобы определить этот метод как процедуру и жестко закодиро- закодировать в нем вызов процедуры подобно DBMSJDUTPUT. PUT_L INE, он реализован в виде функции, вывод которой при желании можно будет переориентировать. Ничего «объектно-ориентированного»' в этом решении нет - просто хорошая идея. К тому же определен метод ck_digit_olcay, возвращающий TRUE или FALSE в за- зависимости от того, совпала ли контрольная цифра. Предполагается, что все объек- объекты подтипов cata I og_i tem_t будут содержать идентификаторы, которые отлича- отличаются от библиотечных и включают так называемую контрольную цифру1. По- Поскольку мы собираемся работать только с книгами и периодикой, а они обычно идентифицируются кодами ISBN и ISSN, то к этим подтипам применима кон- концепция контрольной цифры. Прежде чем мы перейдем к следующей части примера, обратите внимание на следующие моменты. О Приведенная выше инструкция CREATE TYPE создает спецификацию объектного типа. Соответствующее тело типа с реализацией методов будет создано от- отдельно с помощью инструкции CREATE TYPE BODY. О Для объектных типов используется то же пространство имен, что для таблиц и программ PL/SQL верхнего уровня. Это одна из причин, по которым мы ис- используем в именах типов префикс «_t». О Объектные типы всегда должны принадлежать создавшему их пользователю (схеме) Oracle, который может предоставлять на них привилегию EXECUTE дру- другим пользователям. О Синонимы объектных типов поддерживаются только в OracleSi Release 2. О Как и обычные программы PL/SQL, объектные типы можно определять с раз- разрешениями создателя (по умолчанию) или вызывающего (см. главу 20). О В отличие от объектных моделей других языков, в объектной модели Oracle отсутствует корневой класс, от которого должны образовываться все осталь- остальные определяемые программистом классы. Вместо этого можно создать любое количество независимых типов данных корневого уровня, например cata- logjtem_t. О Если вы получили ошибку компиляции PLS-00103: Encountered the symbol ";" when expecting one of the following..., то, скорее всего, завершили методы точ- точкой с запятой, в то время как в спецификации типа необходимо в качестве символа разделителя использовать запятую. Контрольная цифра.—это число, которое входит в состав идентификатора и вычисляется на основе других его цифр. Проверка данной цифры дает некоторую уверенность, что идентификатор записан правильно. Контрольные цифры входят в состав хода ISBN (International Standard Book Number — стандартный международный номер книги), а тахже кода ISSN (International Standard Serial Number — стандартный международный номер периодического издания).
Пример объектного приложения 805 Создание подтипа Поскольку тип cata1og_item_t объявлен так, что возможность создавать его экзем- экземпляры отсутствует, нужно определить его подтипы. Начнем с подтипа для объек- объектов-книг. Так как книга является одним из элементов каталога, в нашем примере все экземпляры подтипа book_t будут иметь следующие четыре атрибута: Old— наследуется от базового типа catalDgjitem_t; О title - также наследуется от базового типа; О isbn — соответствует коду ISBN книги; 0 pages — количество страниц в книге. Соответствующий программный код выглядит таким образом: 1 CREATE OR REPLACE TYPE book_t UNDER catalog_item_t ( 2 isbn VARCHAR2a3). 3 pages INTEGER. 4 5 CONSTRUCTOR fUNCTION book_t (id IN INTEGER DEFAULT NULL. 6 title IN VARCHAR2 DEFAULT NULL, 7 1sbn IN VARCHARZ DEFAULT NULL, 8 pages IN INTEGER DEFAULT NULL) 9 RETURN SELF AS RESULT, 10 11 OVERRIDING MEMBER FUNCTION clc_d1git_okay 12 RETURN BOOLEAN. 13 14 OVERRIDING MEMBER FUNCTION print 15 RETURN VARCHAR2 16 ); Рассмотрим его более подробно. Строка Описание 1 Подтип определяется с помощью ключевого слова UNDER, которое вы видите в строке 1. В Oracle не используется предложение А5 OBJECT, поскольку оно избыточно, ведь «под» (англ. under) объектным типом в иерархии может находиться только производный объектный тип 2, 3 Нам нужен список только тех атрибутов, которые уникальны для подтипа; атрибуты родительского типа включаются в подтип автоматически. Oracle сначала создает атрибуты базового типа, а затем атрибуты подтипа в тон порядке, в каком они заданы в спецификации 4-15 Это объявления методов, которые мы опишем в следующем разделе Методы В приведенном выше определении типа используются два вида методов. О Конструктор. Функция, которая принимает значения всех атрибутов и поме- помещает их в объект заданного типа. Объявлена в строках 5-9.
806 Глава 21 • Объектно-ориентированные возможности PL/SQL О Методы-члены. Функция или процедура, которая выполняется в кошексте экземпляра объекта, то есть имеет доступ к текущим значениям каждого атри- атрибута. Методы-члены объявлены в строках 11, 12 и 14-15. Наш пример демонстрирует применение определенного пользователем конст- конструктора - элемента, введенного в Oracle9i Release 2. В предыдущих версиях Oracle поддерживались только системно-определяемые конструкторы. Создание собст- собственного конструктора типа позволяет управлять созданием экземпляра этого типа. В конструкторе можно выполнять проверку и инициализацию данных, формиро- формировать управляемые побочные эффекты. Кроме того, можно использовать несколь- несколько перегруженных версий конструктора, что дает возможность адаптироваться к разным вариантам его вызова. Для того чтобы посмотреть методы и типы в действии, выполните следующий анонимный блок: 1 DECLARE 2 gener1c_item catalogjtem^t; 3 aboolt book t: 4 BEGIN 5 abook :- NEW book_t(title -> 'Out of the Silent Planet1. 6 1sbn -> '0-6B4a-238-02-): 7 genericjtem :- abook; 8 DBMS OUTPUT.PUT LINE!'BOOK; ' || abook.print!)); 9 DBn5~0UTPuT.PUTJ-INE<'ITEM: ' |j generic item.ргШО): 10 END; Вызовы метода print объектов abook и generic_item (строки 8 и 9) возвращают следующие данные: BOOK: 1d-; title-Out of the Silent Planet: isbn-0-6848-238-02: pages- ITEM: id-: title-Out of the Silent Planet; 1sbn-C-6848-238-02; pages- Строки Описание 5, 6 Конструктор «собирает» новый объект-книгу и помещает его в переменную abook. В данном примере аргументы передаются ему по именам. Мы задали значения только для двух аргументов, но конструктор все равно создал объект. Синтаксис любого конструктора имеет следующий вид: [ NEW ] имя_типа (гргумент_1, аргументу,...); Ключевое слово NEW, введенное в Orade9l Release 2, не обязательно, но полезно как визуальное указание на то, что данный оператор создает новый объект 7 Здесь происходит нечто очень интересное: хотя экземпляры элемента каталога создавать не разрешается, представляющей его переменной можно присвоить экземпляр подтипа, и в ее значении будут содержаться атрибуты, уникальные для данного подтипа. Этот факт демонстрирует важный аспект «замещаеиостм», поддерживаемой Oracle в PL/SQL, суть которой заключается в том, что в объектной переменной может содержаться экземпляр любого подтипа типа данных этой переменной. Разумеется, с логической точки зрения имеет смысл рассматривать книгу как элемент каталога, В компьютерном мире это соответствует расширению подтипа, содержащего специфические атрибуты, до родительского типа, то есть приведению его типа к родительскому (upcasting). Обратная операция, сужение типа или приведение его к дочернему типу (downcastlng), сложнее, но тоже возможна
Пример объектного приложения 807 Строки Описание 8, 9 Обратите внимание, что для вызова метода prtntO используется классический объектный точечный синтаксис: объект.имя_мегодэ(аргуменг_1, аргумент_2,...) поскольку это метод-член и его можно вызвать только для заранее объявленного экземпляра объекта. Выбор метода print для объектов каждого типа осуществляется следующим способом: всегда вызывается метод самого спеииализированного подтипа (расположенного ниже всех по иерархии), ассоциированный с текущим экземпляром объекта. Выбор метода откладывается до этапа выполнения — эта технология называется динамической диспетчеризацией методов. Она очень удобна, хотя и ухудшает производительность Чтобы лучше понять полученные результаты, рассмотрим содержимое типа book_t. В реализации данного типа используются две важные концепции, о кото- которых рассказывается далее. 1 CREATE OR REPLACE TYPE BODY book_t 2 AS 3 CONSTRUCTOR FUNCTION bookj Clef IN INTEGER, 4 title IN VARCHAR2. 5 isbn IN VARCHAR2. fi pages IN INTEGER) 7 RETURN SELF AS RESULT 8 IS 9 BEGIN 10 SELF.Id :- 1d: U SELF.title :-title: 12 SELF.Isbn :- 1sbn: 13 SELF.pages :- pages; 14 IF 1sbn IS NULL OR SELF.ck_dig1t okay 15 THEN 16 RETURN; 17 ELSE IB RAISE_APPLICATION_ERROR(-2D000. 'ISBN ' || Isbn 19 |! " has bad check d1g1f): 20 END IF; 21 END; 22 23 OVERRIDING MEMBER FUNCTION ck_dig1t_okay 24 RETURN BOOLEAN 25 IS 26 subtotal PLSJNTEGER > 0; 27 1sbn_d1g1tS VARCHAR2U0); 2B BEGIN 29 /* удаление дефисов и пробелов */ 30 1Sbn_d1g1tS :- REPLACE(REPLACE(SELF.1Sbn, '-'!. ' '); 31 IF LENGTHHsbn digits) !- 10 32 THEN 33 RETURN FALSE; 34 END IF; 35
808 Глава 21 • Объектно-ориентированные возможности PL/SQL 36 FOR nth_dig1t IN 1..9 37 LOOP 38 subtotal :- subtotal + 39 A1 - nth_d1g1t) * T0_NUMBER{SUBSTRAsbn_digits. nth_d1git. 1»; 40 END LOOP: 41 42 /* контрольной цифрой кожет бить буква 'X'. имеющая значение 10 */ 43 IF UPPERCSUBSTR(isbn_digits. 10. 1» - 'X' 44 THEN 45 subtotal :- subtotal + 10: 46 ELSE 47 subtotal :- subtotal + TD_NUMBER(SUBSTR(isbn_digits. 10. 1)): 4B END IF; 49 50 RETURN MODCsubtotal. 11) - 0: 51 52 EXCEPTION 53 WHEN OTHERS 54 THEN 55 RETURN FALSE; 56 END; 57 58 OVERRIDING MEMBER FUNCTION prfnt 59 RETURN VARCHAR2 60 IS 61 BEGIN 62 RETURN r1d-' || 1d || '; title-' |] title 63 || '; isbn-1 || isbn || ': pages-' || pages; 64 END; 65 END; Поанализируем этот код, подробно указав какие операции в каких строках производятся. При определении конструктора объектного типа нужно следовать определенным правилам. О Конструктор должен быть объявлен с ключевыми словами CONSTRUCTOR FUN- FUNCTION (строка 3). О Предложение, которое определяет возвращаемое им значение, аолжно выгля- выглядеть следующим образом: RETURN SELF AS RESULT (строка 7). О Конструктор может присваивать значения любым атрибутам текущего объек- объекта (строки 10-13). О Он должен завершаться оператором RETURN или исключением (строка 16; стро- строки 18, 19). Обычно конструктор присваивает значения большинству атрибутов объекта. Как видно из строки 14, перед окончанием работы конструктор проверяет кон- контрольную цифру. Учтите,что функция ck_digit_okay считывает атрибут isbn теку- текущего объекта (строка 30) еще до завершения «сборки» объекта. Программный код в строках 17-21 следует рассматривать как предельно уп- упрощенный пример. В реальном приложении обработка специфических для при- приложения исключений вьшолняется более сложным образом. Об этом мы расска- рассказывали в последнем разделе главы 6.
Пример объектного приложения 809 Теперь обратимся к ключевому слову SELF, которое несколько раз встречается в коде объектного типа (не только в конструкторе, но и других методах). Для про- программистов Java можно сказать, что оно эквивалентно ключевому слову this, для программистов, работающих с другими языками, его значение можно объяснить таким образом: SELF — это ссылка на вызывающий (текущий) объект, используе- используемая исключительно в реализации методов. Ее можно применять для ссылки на весь объект, а с точечным синтаксисом — для ссылки на атрибут или метод объек- объекта. Например: IF SELF.id ... IF SELF.ck_d1git_okay() ... Внутри метода-члена ключевое слово SELF требуется не всегда (см. строки 62, 63), поскольку идентификаторы текущего объекта всегда видимы в методе и обыч- обычно идентифицируются именно как ссылки на его атрибуты и методы. В строках 10-13 оно было необходимо — в данном случае имена формальных параметров конструктора совпадают с именами атрибутов и компилятор идентифицировал бы их как ссылки на параметры. Кроме того, они помогают сделать код более чи- читабельным. Ниже приведено еще несколько рекомендаций, касающихся применения клю- ключевого слова SELF. О Это ключевое слово не может использоваться в статических методах, посколь- поскольку у них нет «текущего объекта». О По умолчанию в функциях ключевое слово SELF является входной перемен* ной (IN), а в процедурах и конструкторах - входной и выходной переменной (IN OUT). О Эти установки можно изменить, если задать SELF первым формальным пара- параметром. Продолжим рассмотрение кода. Вычисление контрольной цифры (строки 23-56) мы выполняем в качестве примера, но в нашем алгоритме новые объектно-ориентированные средства ис- используются не в полной мере. Кроме того, не полностью реализован обработчик исключений. Он должен обрабатывать ряд ситуаций, например получение функ- функцией TO_NUMBER строки вместо числа. Теперь рассмотрим второй подтип — периодические издания: CREATE OR REPLACE TYPE ser1a1_t UNDER catalog_iteni_t t issn VARCHAR2Q0). open_or_c1osed VARCHAR2U). CONSTRUCTOR FUNCTION seriait (id IN INTEGER DEFAULT.NULL. title IN VARCHAR2 DEFAULT NULL. " • issn IN VARCHAR2 DEFAULT NULL. open_or_c1osed IN VARCHAR2 DEFAULT NULL) RETURN SELF AS RESULT.
810 Глава 21 • Объектно-ориентированные возможности PL/SQL OVERRIDING MEMBER FUNCTION cX digtt okay RETURN BOOLEAN, OVERRIDING MEMBER FUNCTION print RETURN VARCHAR2 ) NOT FINAL: Как видите, в нем нет ничего нового. Объектный тип serialt в нашей модели имеет собственный конструктор, свою версию функции проверки контрольной цифры и способ возврата информации о себе в ответ на вызов метода print1. Помимо конструктора и методов-членов Oracle поддерживает еще две катего- категории методов. О Статические методы. К ним относятся функции и процедуры, вызываемые независимо от экземпляра объекта. Такие методы ведут себя подобно обыч- обычным процедурам и функциям PL/SQL. Пример вы найдете во врезке «Стати- «Статический метод как псевдо-конструктор». О Методы сравнения (то есть соответствия или сортировки). Это специальные методы-члены, позволяющие указать, что должна делать система Oracle, когда ей нужно сравнить два объекта данного типа, например при проверке на экви- эквивалентность в PL/SQL или при сортировке объектов в SQL,- На объекты распространяется правило Oracle, гласящее, что значение неини- неинициализированной переменной автоматически устанавливаются равным NULL2. Как и для коллекции, в этом случае значения атрибутам объекта присваивать нельзя. Рассмотрим следующий пример: DECLARE щуЬоок book t; -- объект объявлен, но не инициализирован BEGIN IF nybook IS NULL -• проверка вернет TRUE; объект равен NULL THEN ШУЬооК.title :- 'Leamine Oracle PL/SQL'; -¦ эта строка является причиной ... ?ND IF; EXCEPTION WHEN ACCESS INTO NULL -- ...данного предопределенного исключения THEN END; Прежде чем присваивать атрибутам значения, вы должны инициализировать объект (создать экземпляр) одним из трех способов: с помощью метода-конст- метода-конструктора, путем непосредственного присваивания экземпляра другого объекта или посредством выборки из базы данных. Об этом рассказывается в следующем раз- разделе данной главы. Атрибут open_or_closed возвращает символ О или С. Первый символ означает, что денпый элемент каталога в настоящий момент модифицируется (например, библиотека еще не получила всех номе' ров журнала}, а второй — что данный элемент каталога уже сформирован. Исключением иэ правила являются ассоциативные массивы. Сразу после объявления они не равны NULL, а просто пусты,
Пример объектного приложения ¦ 811 СТАТИЧЕСКИЙ МЕТОД КАК ПСЕВДО-КОНСТРУКТОР В версиях от Огас1е8а до Oracle9i Release 1 пользовательские конструкто- конструкторы не поддерживались, и их заменой были статические методы, которые возвращали объект, созданный системным конструктором. Например: STATIC FUNCTION make (id IN INTEGER, title IN VARCHAR2. 1$bn IN VARCHAR2, pages IN INTEGER) RETURN book_t IS BEGIN IF <проверка разных атрибутов. ..> THEN RETURN book_tAd. title. 1sbn. pages): ELSE RAISE -^соответствующее исключение: END IF: END: Выделенный код — это вызов системного конструктора. Конструктор все- всегда имеет одинаковое с объектным типом имя. Значения атрибутов пере- передаются ему в том же порядке, в каком они указаны при определении объ- объектного типа. Статический метод вызывается так же, как любой другой метод объекта: DECLARE ajiook bookj :- bookj.tnalcedd -> Ш); Конечно, это не единственная область применения статических методов. Запись, извлечение и использование хранимых объектов До сих пор мы рассматривали определение типов данных и создание экземпля- экземпляров объектов в памяти, выделяемой для выполняющихся программ. Однако это только небольшой фрагмент «объектной картины» - конечно же, в Oracle суще- существует возможность сохранять объекты в базе данных. ОРГАНИЗАЦИЯ ЦЕПОЧКИ МЕТОДОВ Объект, определение которого выглядит следующим образом: CREATE OR REPLACE TYPE cha1ndefflO_t AS OBJECT ( X NUMBER, у VARCHAR2U0), z DATE. MEMBER FUNCTION setx (x IN NUMBER) RETURN chaindemo t. MEMBER FUNCTION sety (y IN VARCHAR2) RETURN chaindenio t. MEMBER FUNCTION set* (z JN DATE) RETURN Ch«1ridemo_t); позволяет «сцеплять» методы вместе. Например: DECLARE С chaindemcj :- chaindemo_ttNULL. NULL. NULL); BEGIN с :- c.setx(l).sety('foo').setz(sysdate); •- цепочка визовое продолжением
812 Глава 21 • Объектно-ориентированные возможности PL/SQL Этот оператор эквивалентен трем последовательным вызовам: с :- c.setx(l); с ;- c.sety('foo'): с :- c.setz(sysdate); Каждая функция возвращает типизированный объект, который является входным для следующей функции. Ниже описана реализация одного из трех методов (остальные похожи): MEMBER FUNCTION setx Cx IN NUMBER) RETURN chaindemojt IS l_self chaindemo_t :- SELF; BEGIN l_self.x :- X, RETURN l_self: END: Приведем несколько правил формирования цепочки методов. ? Нельзя использовать возвращаемое функцией значение в качестве Па- Параметра IN OUT следующей функции цепочки. Функции возвращают значения, доступные только для чтения. Q Методы вызываются в порядке слева направо. Q Каждый метод в цепочке должен возвращать объект того типа, метод которого вызывается следующим. ? Цепочка вызовов может включать не более одной процедуры. Если цепочка включает вызов процедуры, он должен быть последним (то есть крайним справа). Создаваемый нами библиотечный каталог можно сохранить в базе данных как минимум двумя способами: в виде одной большой таблицы, которая состоит из объектов, соответствующих элементам каталога, либо в виде набора меньших таб- таблиц, представляющих подтипы элементов каталога. Первый способ реализуется таким образом: CREATE TABLE catalog_iterns OF cata1og_1tOT_t (CONSTRAINT catalog_items_pk PRIMARY KEY (id)): Эта инструкция предписывает Oracle создать таблицу объектов catalogjitems, каждая строка которой является объектом типа catalog_item_t. Объектная табли- таблица обычно содержит по одному столбцу на атрибут. Однако помните, что объект- объектный тип catalog_item_t не допускает создания экземпляров и каждая строка таб- таблицы на самом деле будет объектом одного из его подтипов, например книгой или периодическим изданием. Поэтому в таблице необходимо выделить место для хранения значений атрибутов каждого из возможных подтипов. Чтобы вы- выполнить данную операцию, в таблицу добавляются скрытые столбцы, по одному на каждый уникальный атрибут подтипа. С точки зрения программирования объек- объектов это удобно, поскольку позволяет сохранить абстракцию элементов каталога.
чом Пример объектного приложения 813 Предложение CONSTRAINT указывает, что столбец id является первичным клю- ¦гиМ. Учтите, что и у объектных таблиц могут быть первичные ключи. Кроме то- того, по умолчанию Oracle должен создать системно-генерируемый идентификатор объекта (OID). Идентификация объектов В реляционных базах данных каждая строка таблицы имеет уникальный иденти- идентификатор. Объектно-ориентированные системы также поддерживают уникальные идентификаторы объектов. Как правило, они присваивают объектам невидимые идентификационные номера, служащие дескрипторами. Идентификатор для лю- любого объекта может быть создан на основе одного из следующих элементов. О Значение первичного ключа. Для использования данной возможности нужно добавить в конец инструкции CREATE TABLE предложение OBJECT IDENTIFIER IS PRIMARY KEY. О Значение, генерируемое системой. В этом случае Oracle добавляет в таблицу еще один скрытый столбец с именем SYS_NC_OID$ И помещает в него уникальное 16-байтовое значение типа RAW. По умолчанию идентификатор создается имен- именно таким образом. Не все объекты имеют идентификаторы. В частности, объекты, хранящиеся в переменных PL/SQJL, как и объекты в столбцах таблиц, не имеют ОШ-иденти- фикаторов. Oracle считает, что объект-столбец зависит от первичного ключа таб- таблицы и поэтому не должен идентифицироваться независимо1. Идентификаторы на основе первичных ключей обычно занимают меньше мес- места, чем системно-генерируемые, но последние обладают определенными преиму- преимуществами. Более подробное обсуждение достоинств и недостатков двух указан- указанных подходов вы найдете в документации Oracle Application Developer's Guide — Object-Relational Features. Пока же вам достаточно знать, что системно-генерируе- системно-генерируемые идентификаторы обладают следующими качествами. О Прозрачность. Хотя программы могут использовать их неявно, значение та- такого идентификатора обычно нельзя увидеть. О Потенциальная глобальная уникальность во всех базах данных. Простран- Пространство OID имеет объем, достаточный для хранения 2128 идентификаторов, и тео- теоретически позволяет идентифицировать объекты в распределенных базах дан- данных без явных ссылок на базы данных. О Постоянство. В данном контексте это означает, что их нельзя обновлять. Даже после экспорта и импорта объекта его OID остается тем же, в отличие от ROWID. Для изменения OID нужно удалить объект и создать его вновь. Эхсперты по реляционным базам данных придерживаются иного мнения. Они считают, что ОШ-идентифихаторы не могут использоваться для идентификации строк и только объекты-столб- объекты-столбцы должны иметь OID.
814 Глава 21 • Объектно-ориентированные возможности PL/SQL Функция VALUE Операции с данными объектных таблиц потребовали некоторых изменений в син- синтаксисе SQL. Например, мозкко создать объект с помощью собственного конст- конструктора и задать его в обычной инструкции INSERT1: INSERT INTO catalogjterns VALUES (NEW book_tA0003. 'Pereiandra'. '0-684-82382-9'. 222)): INSERT INTO catalog items VALUES (NEW serial Ш0004. 'Time1, '0040-781X', '0')); ЭМУЛЯЦИЯ ГЛОБАЛЬНЫХ КОНСТАНТ С ПОМОЩЬЮ ОБЪЕКТНЫХ МЕТОДОВ Многие профессионалы, увидев приведенный выше код, будут не согласны с жестким кодированием значений, в частности с применением параметра '0', обозначающего «открытый». Вместо него можно задать константу из пакета или вызвать статический метод, например ореп_с, возвращающий нужное значение. Мы предпочли бы использование статического метода, поскольку в отличие от пакетов объектные типы Oracle не поддерживают глобальных констант, переменных и вообще ничего глобального. В этом случае инструкция INSERT имела бы такой вид: INSERT INTO cataiogjtans VALUES (NEW ser1al_t(l0002. 'Time1. '0040-781X'. ser1al_t.open_c)); Тело соответствующего статического метода состоит из единственного опе- оператора RETURN ' 0'. Этот подход имеет очевидные преимущества — он обес- обеспечивает согласованность кода и облегчает выполнение изменений в бу- будущем. Для извлечения объекта из базы данных Oracle предоставляет SQL-функцию VALUE. Она принимает единственный аргумент, в котором задается указанный в предложении FROM псевдоним таблицы, и возвращает объект того типа, на основе которого определена данная таблица. В инструкции SELECT это выглядит следую- следующим образом: SELECT VALUE(c) FROM catalogjterns с; В качестве псевдонимов таблиц лучше использовать короткие аббревиатуры, в частности с. Функция VALUE возвращает вызывающей программе последователь- последовательность битов, а не текстовое представление значений столбцов. Однако в пакете SQL*Plus имеется встроенная возможность вывода объектов, поэтому результа- результаты приведенного выше запроса он выводит следующим образом; valuecckid. title: BOOKJC10003, 'Perelandra'. '0-684-82382-9'. 222) SERIAL_TC10004, 'Time1, '0040-781X'. '0') 1 В tuub статических вызовах функций лучше передамп параметры по именам, но при аыэок функций PL/SQL ил SQL Oracle этого не позволяет (во всякой случае, Oracleffl Release 2),
Пример объектного приложения 815 Средства, предназначенные для работы с извлеченными из базы данных объек- объектами, имеются и в PL/SQL. Сначала объявляется объектная переменная соответ- соответствующего типа: DECLARE catalog item catalog_1tem_t: CURSOR ccur IS SELECT VALUE(c) FROM cataiogjterns c; BEGIN OPEN ccur; FETCH ccur INTO catalog Item: 0ВМ5_0Ш№.Р1Я_ШЕ('Изменен алеиент #' II catalog_1tem.1d); CLOSE ccur; END; В аргументе процедуры PUT_LINE используется синтаксис переменная .атрибут, а воз- возвращает она значение атрибута catalogjtem.id: Извлечен элемент #10003 Инструкция FETCH присваивает объект локальной переменной cata1og_itefn, ко- которая объявлена как переменная базового объектного типа. Это естественно, по- поскольку мы не знаем заранее, объект какого подтипа будет выбран из базы данных. Рассматриваемый нами код показывает (на примере вывода значения атрибута catai og_i tern И d), что мы имеем непосредственный доступ к атрибутам базового типа. Можно использовать также обычные атрибуты курсоров. Например, приве- приведенный выше анонимный блок можно переписать таким образом: DECLARE CURSOR ccur IS SELECT VALUE(c) obj FROM catalog_iterns c: агес ccurfRWTYPE; BEGIN OPEN ccur; FETCH ccur INTO arec: DBMS_OUTPUT.PUT_LINE('Извлечен элемент #' [| arec.obj.id); CLOSE ccur; END; Если нам необходимо вывести все атрибуты объекта, можно использовать ме- метод pri nt. Это вполне допустимо, поскольку он объявлен в объектном типе корне- корневого уровня и реализован в подтипах. На этапе выполнения Oracle найдет для данного метода перегруженную версию из подтипа. Функция VALUE поддерживает точечный синтаксис, обеспечивающий доступ х атрибутам и методам, но только к тем из них, которые определены в базовом типе. Например, инструкция SELECT VALUE(c).1d. VALUE(c).pMntt) FROM catalog_iterns c: возвращает результат VALUECCJ.ID VALLE(C).PRINTt) 10003 -ld-10003; title-Perelandra: 1sbn-O-684-B2382-9; pages-222 10004 1d-lOQQ4; titie-Tin»: 1ssn-0040-78lX; open_or_closed-Gpen
816 Глава 21 • Объектно-ориентированные возможности PL/SQL При работе в клиентской среде, не поддерживающей объектов Oracle, можно воспользоваться именно этой функцией. Попытайтесь прочитать только те атрибуты, которые являются уникальными для конкретного подтипа, выполнив такую инструкцию: SELECT VALUE(c).issn -- Ошибка. Непосредственный доступ к подтипу невозможен FROM catalogjterns с: В ответ Oracle выдает сообщение об ошибке ORA-00904: Invalid column паше (не- (неверное имя столбца). Это означает, что объект родительского типа не предостав- предоставляет непосредственного доступа к атрибутам подтипа. Можно попробовать объя- объявить переменную book типа book_t и присвоить ей объект данного подтипа, в надеж- надежде, что она покажет скрытые атрибуты: book :- catalogj'tem; /* Ошибка. Непосредственное присваивание подтипу также не работает */ Однако на этот раз мы получим сообщение об ошибке PLS-00382: expression is ' of wrong type (неверный тип выражения). О том, как решается эта проблема, вы узнаете немного позже. Учтите следующие особенности выполнения DML-операций в объектных ре- реляционных таблицах. О Для объектной таблицы, созданной на основе объектного типа, не имеющего подтипов, возможны выборка, вставка, обновление и удаление значений всех столбцов посредством обычных SQL-инструкций. Таким образом, объектно- ориентированные и реляционные программы могут совместно использовать одни и те же данные. О Традиционные DML-инструкции не имеют доступа к скрытым столбцам, пред- представляющим атрибуты подтипов. Для работы с такими столбцами использует- используется «объектный DML». О Чтобы обновить весь объект в таблице базы данных из программы PL/SQL, можно использовать объектную DML-инструкцию, которая обновит все атри- атрибуты (столбцы), включая уникальные для подтипа: UPDATE catalogjteras с SET с - объектная ^переменная WHERE ... О Единственным известным способом обновления заданного столбца, уникаль- уникального для подтипа, является обновление всего объекта. Например, чтобы уста- установить равным 1000 значение количества страниц книги с идентификатором 10007, потребуется такая инструкция: UPDATE catalogjteras с SET с - NEW book tie.id. c.tltle. c.publ1cation_date, c.subject refs. ~ (SELECT TREAT(VALUECy) AS book_t).1sbr> FROM catalogjtems у WHERE id - 10007), 1000) WHERE id - 10007; Функция TREAT Рассмотрим ситуацию, когда в объявлении типа переменной PL/SQL указано имя супертила, а ее значение принадлежит к подтипу. Как нам получить доступ к ат- атрибутам и методам, специфическим для этого подтипа? Предположим, что мы
Пример объектного приложения 817 хотим интерпретировать элемент каталога как книгу- Эта операция называется сужением (narrowing) или приведением базового типа к дочернему (downcasting), и компилятор воспринимает ее не всегда. Для выполнения данной операции нуж- нужно вызвать специальную функцию Oracle с именем TREAT. Это можно сделать не- непосредственно: DECLARE book i»ok_t; catalogjtem cataiogjtem t :» NEW bnok_t{): BEGIN book :- TREAT (catalogjtem AS book t): /* Работает в 91 R2 */ END; / или с помощью SQL (в PL/SQL Oracle9t Release 1 функция TREAT явно не поддер- поддерживается): DECLARE book book t: catalogjtan cstalogjtemt ;- bot*_t(NULL, HULL, NULL. NULL): BEGIN SELECT TREAT (catalogjte AS bookj) INTO book FROM DUAL: END; Синтаксис функции TREAT выглядит следующим образом: TREAT Ызеш\пяр_рбъекта AS подтип) [ . {атрибут | нвтод(аргументы...)} ] Здесь жземпляр_о6ъектд — это значение конкретного супертипа в объектной ие- иерархии, а подтип — имя подтипа в этой иерархии. Если вы попытаетесь преобразо- преобразовать объект к типу, принадлежащему другой объектной иерархии, компилятор выдаст ошибку. Когда функции TREAT передан объект из «правильной» иерархии, она возвращает либо преобразованный объект, либо NULL, но не ошибку. Как и в случае использования функции VALUE, при обращении к функции TREAT можно применить точечный синтаксис, чтобы сразу задать атрибут или метод преобразованного объекта. Например: DBMS_OUTPUT.PUT_LINE<TREAT (VALUE(c) AS serial_t).issn); Если вам понадобится перебрать все объекты в таблице и выполнить опреде- определенные операции с учетом их типов, это можно сделать таким образом: DECLARE CURSOR ccur IS SELECT VALUEtc) item FROM catalogjtems c: arec ccurtROWTYPE; BEGIN FOR arec IN ccur LOOP CASE WHEN arec.Hem IS OF (book t) THEN DBMS_OUTPUT.PLn"_LINE('Найдена книга с ISBN-кодои ' [| TREAT!arec.Item AS book_t).isbn); WHEN arec.item IS OF (serial t)
818 Глава 21 * Объектно-ориентированные возможности PL/5QL THEN DBMS OUTPUT.PUT LINEC'Найдено периодическое издание с ISSN-кодон ' |J TR?AT(arec.1tem AS ser1al_t).1ssn); ELSE DBMS_OUTPUT.PUT_LINEC'HaMfleH неизвестный элемент каталога'): END CASE: ?№ LOOP: END: Данный блок демонстрирует применение предиката IS OF для проверки объект- объектного типа. Синтаксис предиката выглядит многообещающе: объект IS OF ([ONLY] иия^тнпа) Однако на самом деле возможности предиката ограничены - он работает толь- только с объектными типами данных Oracle и не работает с базовыми, такими как NUMBER или DATE. Компилятор выдаст сообщение об ошибке, если при использова- использовании предиката тип объекта не принадлежит той же иерархии, что и тип имя_типа. Обратите внимание на ключевое слово ONLY. По умолчанию (то есть без ONLY) предикат возвращает TRUE, если объект относится к заданному типу или одному из его подтипов. Если ключевое слово ONLY присутствует, предикат IS OF не срав- сравнивает подтипы и возвращает TRUE только в случае точного совпадения типов. Предикат IS OF, как и функция TREAT, может использоваться в SQL Oracle9«, но в PL/SQL до Oracle9i Release 2 он не поддерживается. Чтобы обойти это ограни- ограничение, в Release 1 можно определить в дереве типов один или более дополнитель- дополнительных методов и, пользуясь динамической диспетчеризацией методов, выполнить нужную операцию на соответствующем уровне иерархии. На выбор подходящего метода сужения типа влияет не только номер версии, но и выполняемые в прило- приложении операции. Далее мы обратимся еще к одной интересной теме — возможностям, предла- предлагаемым Oracle в случае изменения структуры приложения. Эволюция и создание типов По сравнению с Oracle8i в Oracle9t достигнут огромный прогресс в области эво- эволюции типов. Теперь Oracle позволяет вносить в объектные типы различные из- изменения, причем допускается это даже в случаях, когда таблицы уже созданы и за- заполнены объектами данных типов. Ранее в этой главе мы определили объектный тип cata1og_item_t на скорую руку. Однако любой библиотекарь скажет, что для каждой находящейся в биб- библиотеке книги или периодического издания желательно хранить в каталоге дату издания1. Поэтому мы делаем следующее: ALTER TYPE catalogjtem t ADD ATTRIBUTE publ1cat1on_date VARCHAR2D00> CASCADE INCLUDING TABLE DATA; Et voilal Система Oracle распространила это изменение на соответствующие таблицы, автоматически выполнив все необходимые модификации. Она добави- добавила атрибут в конец списка атрибутов супертипа и один столбец после последнего Мы не можем определять данный атрибут как DATE, поскольку ияогда это год, месяц или квартал, ¦ иногда к вовсе что-то особенное. Со временем можно будет создать для него объектный тип.
Пример объектного приложения 819 столбца супертипа в соответствующую объектную таблицу. Если теперь выпол- выполнить для типа catalog item_t команду DESCRI8E, которая выводит описание задан- заданного объекта, результат будет таким: SQL» DBC catalog 1tem_t catalog itan_t 1s~NQT FINAL catalogjtem t is NOT INSTANTIABLE Name " Null? Type ID NUM8ERC38) TITLE VARCHAR2{4000) PUBLICATION_DATE VARCHAR2D0D) METHOD MEM8ER FUNCTION CK_DIGIT_OKAY RETURNS BOOLEAN CK_OIGIT_OICAY IS NOT INSTANTIABLE METHOD MEMBER FUNCTION PRINT RETURNS VARCHAR2 Для таблицы эта команда вернет следующее: SQL> DESC cetalogjtems Name Null? Type ID NOT NULL NUMBERC3B) TITLE VARCHAR2D000) PUBLICATIDN_DATE VARCHAR2C400) Фактически инструкция ALTER TYPE исправляет почты все, однако, увы, она не настолько умна, чтобы переписать наши методы. Особенно важны конструкторы, поскольку придется изменить их сигнатуры. Для изменения сигнатуры метода можно удалить его и добавить снова. ПРИМЕЧАНИЕ- При расширении объектного типа иногда генерируется ошибка «ORA-22337: the type of accessed object has been evolved». После этого вы не сможете получить описание типа с помощью команды DESCRIBE. И перекомпиляция делу не поможет, более того, если имеются жесткие зависимости от этого типа, Oracle не позволит перекомпилировать.его спецификацию. Чтобы избавиться от данной ошибки, нужно отсоединиться от Oracle и затем подключиться снова. Б результате буферы сеанса очистятся, и с помощью команды DESCRIBE можно будет увидеть описание новой версии типа. Для удаления метода иэ спецификации типа boolc_t необходимо выполнить та- такую инструкцию: ALTER TYPE bookj DROP CONSTRUCTOR FUNCTION bookj Ad INTEGER DEFAULT NULL. title VARCHAR2 DEFAULT NULL, 1sbn VARCHARZ DEFAULT NULL. pages INTEGER DEFAULT NULL) RETURN SELF AS RESULT CASCADE;
820 Глава 21 ¦ Объектмо-ориентированные возможности PL/SQL Была задана полная спецификация функции. Это гарантирует, что мы удаля- удаляем именно тот метод, который хотим, поскольку могут существовать его перегру- перегруженные версии. (Вообще-то, значения по умолчанию указывать здесь не обяза- обязательно.) Соответствующая операция добавления метода выглядит так же просто (од- (однако Oracle выполняет при этом огромную работу): ALTER TYPE book t ADO CONSTRUCTOR FUNCTION book_t Ad INTEGER DEFAULT NULL. title VARCHAR2 DEFAULT NULL. publication date VARCHAR2 DEFAULT NULL, 1Sbn VARCHAR2 DEFAULT NULL. pages INTEGER DEFAULT NULL) RETURN SELF AS RESULT CASCADE; Далее нужно аналогичным образом модифицировать тип serial_t, а затем за- заменить два соответствующих тела типов с помощью инструкции CREATE OR REPLACE TYPE BOOY (данные фрагменты кода в книге не приведены). Кроме того, следует просмотреть все методы на предмет необходимости изменений (например, жела- желательно включить дату издания в метод print). Для удаления типа (если это потребуется) применяется следующая инструк- инструкция: DROP TYPE инятипэ [FORCE]; Ключевым словом FORCE нужно пользоваться осторожно. Дело в том, что все объектные типы и объектные таблицы, которые зависят от типа, удаленного с указанием этого ключевого слова, станут неприменимыми. Если в таблицах име- имеются столбцы данного типа, Oracle пометит их как UNUSED и сделает недоступны- недоступными. Для удаления подтипа, который используется в определениях таблиц, можно задать инструкцию DROP TYPE в такой форме: DROP TYPE («я_лодгила VALIDATE; Ключевое слово VALIDATE предписывает Oracle просмотреть таблицы и удалить данный подтип только в случае, если они не содержат ни одного экземпляра этого подтипа. Таким образом вы сможете избежать разрушительных последствий, воз- возникающих при использовании ключевого слова FORCE. И снова указатели Среди объектно-ориентированных функций Oracle появилась возможность хра- хранить значения типа REF. Они представляют собой объектные ссылки, то есть логи- логические указатели на конкретную строку объектной таблицы. В ссылке хранится следующая информация: О первичный ключ строки или системно-генерируемый идентификатор объекта; О уникальный идентификатор таблицы; О информация о физическом местоположении строки на диске в виде ее значе- значения типа RQWID (это делается по указанию программиста).
Пример объектного приложения 821 Если содержимое REF представлено в символьном виде, вряд ли вы сможете извлечь из него полезную информацию: SOL> SELECT REF(c) FROM cataiogjtens с WHERE ROHNUH - 1; REF(C) OOOC2802099FC431FBE5FZ0599E0340003BAOF1F139FC431FBE5F10599E0340003BAOF1F130240000COOOO Запросы и программы могут использовать значения REF для выборки строк объектной таблицы без указания ее имени. Далее мы рассмотрим, как REF-ссыяки применяются в библиотечном каталоге. Использование REF-ссылок Библиотечный фонд обычно классифицирован по тематике. Например, в Биб- Библиотеке Конгресса данная книга может относиться к трем категориям: О Oracle (Computer file — компьютеры); О PL/SQL (Computer program language - языки программирования); О реляционные базы данных. В нашем случае для классификации используется древовидная структура: ка- категория Computer file является родительской для Oracle, а категория Computer program language — родительской для PL/SQL. Категории и хранящиеся в библиотеке объекты связаны отношением «мно- гие-ко-многим»: книга может относиться к нескольким категориям, а к категории может принадлежать множество книг. В нашем библиотечном каталоге книги всех категорий размещены в одной таблице. При использовании реляционного подхо- подхода нужно создать промежуточную таблицу, чтобы разделить отношение «мно- гие-ко-многим* на два отношения «один-ко-многим». Однако объектно-реляци- объектно-реляционный подход предлагает другое решение. Начнем с создания объектного типа для категории: CREATE TYPE subject_t AS OBJECT ( name VARCHAR2B000), broaderjterm ref REF subject t В нем должно храниться имя данной категории и так называемый гиперним (Broader Term — ВТ), то есть имя вышестоящей категории. Чтобы не размещать один объект в другом, мы будем хранить только ссылку на вышестоящую катего- категорию. Поэтому в третьей строке определения типа атрибут broader_term_ref задан как REF-ссылка на объект того же типа. Это похоже на рассматриваемую ранее таблицу ЕМР, в которой хранятся идентификаторы строк этой же таблицы, соот~- ветствующие начальникам. Теперь создадим таблицу категорий: CREATE TABLE subjects OF subject t (CONSTRAINT subject_pk PRIMARY KEY (name), CONSTRAINT subject_self ref FOREIGN KEY !broader_term ref) REFERENCES subjects!; Рассмотрим внешний ключ таблицы. Он указывает на таблицу с реляцион- реляционным первичным ключом, но имеет тип REF, поэтому Oracle знает, что в качестве
822 Глава 21 • Объектно-ориентированные возможности PL/SQL внешнего ключа нужно использовать идентификаторы объектов. Поддержка ог- ограничений внешнего ключа типа REF демонстрирует, что в Oracle возможна связь между объектным и реляционным мирами. Ниже приведено несколько инструкций, которые вставляют (используя сис- системный конструктор) данные в таблицу: . INSERT INTO subjects VALUES (subjectjС Computer file'. NULL)); INSERT INTO subjects VALUES (subject_t('Computer program language', NULL)): INSERT INTO subjects VALUES (subject_t('Relational databases'. NULL)); INSERT INTO subjects VALUES (subject_t('Oracle', (SELECT REF(s) FROM subjects s WHERE name - 'Computer file'))). INSERT INTO subjects VALUES (subject_t('PL/SQL'. (SELECT R?F(s) FROM subjects s WHERE name - 'Computer program language'))): Теперь можно вывести содержимое таблицы subjects: SQL> SELECT VALUE(s) FROH subjects S; VALUE{S)(NAME. BROAOER_TERM_REF) SUBJECT_T('Computer file'. NULL) SUBJECTJC'Computer program language', NULL) SUBJECTJC 'Oracle'. 00002202089FC431FBE6F80599EQ3400D3BAOF1F139FC431FBE6690599E03 40D03BA0F1F13) SUBJECT T('PL/SOL'. 00D022D2D89FC431F8E6FC0599E0340003BAOF1F139FC431FBE6690599E03 40003BAQ>1F13) SUBJECTJC'Relational databases', NULL) Полезной является способность Oracle автоматически разрешать ссылки. На- Например, с помощью функции DEREF можно перейти к строке, на которую указыва- указывает ссылка: SELECT s.name, DEREFE.broader_term_ref).name bt FROM subjects s; Это похоже на внешнее автоматическое объединение. Иными словами, если ссылка равна NULL или недействительна, строка включается в результирующий набор, но значение целевого объекта (и столбца) будет равно NULL. В Oracle введено сокращение для функции DEREF ~ точечный синтаксис, ука- указывающий, что извлекаемый атрибут берется из целевого объекта: SELECT s.name, s.broader_termj-ef,name bt FROM subjects s: Оба запроса возвращают следующее: NAME ВТ Computer file Computer program languaoe Oracle Computer file PL/SQL Computer program language Relational databases Учтите, что в обеих формах требуется псевдоним таблицы: SELECT псеацотн_табп>щы.стопбец_геТ FROM ш_табпицы псевйончм_тавпицы
Пример объектного приложения 823 Ссылки могут использоваться и в предложении WHERE. Вывести все подкатего- подкатегории, относящиеся к категории Computer program language, можно с помощью сле- следующего запроса: SELECT VALUEE).name FROM subjects s WHERE s.broader_term_ref.name - 'Computer program language'; Хотя в нашем примере в таблице используется ссылка на эту же таблицу, на практике ссылки могут указывать на любую объектную таблицу базы данных. Чтобы продемонстрировать сказанное, вернемся к определению базового типа catalog_itero_t и добавим в него атрибут, предназначенный для хранения коллек- коллекции ссылок. В результате каждый элемент каталога будет связан с набором кате- категорий. Сначала создадим коллекцию ссылок на категории: CREATE TYPE subject_refs_t AS TABLE OF REF subject_t; Теперь каждый элемент каталога можно ассоциировать с любым количеством категорий: ALTER TYPE catalog_item_t ADO ATTRIBL/TE subject_refs subject_ref$_t CASCADE INCLUDING TABLE DATA; He рассматривая модификацию соответствующих методов зависимых типов, перейдем к добавлению строк в каталог. Это делается с помощью следующей ин- инструкции: INSERT INTO catalogjtems VALUES (NEW book_tA0007, 'Oracle PL/SQL Programing', 'Sept 1997'. CAST(MULTISET(SELECT REF(s) FROM subjects 5 WHERE name IN ('Oracle', 'PL/SQL', 'Relational databases'» AS subject refs_t), ¦1-56592-335-9'. 9B7)); Функции CAST и MULTISET выполняют преобразование набора ссылок в коллек- коллекцию, как рассказывалось в главе 11. Далее следует более понятный эквивалент на PL/SQL: DECLARE subrefs subject refs t; BEGIN SELECT REF(s) BULK COLLECT INTO subrefs FROM subjects s WHERE name IN ('Oracle', 'PL/SQL', 'Relational databases'); INSERT INTO catalog Items VALUES (NEW book Ш0007. 'Oracle PL/SQL Programing', 'Sept 1997', subrefs. '1-56592-335-9', 9B7)); END: Мы извлекаем ссылки на три заданные категории и записываем их в объект данной книги. Рассмотрим еще один пример навигации по ссылкам: SELECT VALUE(s).name |j 'С |j VALUE(s).broader_term_ref.name || ')' plsql_subjects
824 Глава 21 • Объектно-ориентированные возможности PL/SQL FROM TABLECSELECT subject_refs FROM cataiogjtems WHERE id-10007) s; Код извлекает данные из таблицы subjects, при этом она даже не упоминается по имени. (Функция TA8LE преобразует коллекцию в виртуальную таблицу.) Ре- Результаты выполнения приведенного запроса имеют следующий вид: PLSQL SUBJECTS Relational databases О PL/SQL (Computer program language) Oracle (Computer file) К сожалению, кроме возможности выполнять автоматический переход по ссыл- ссылкам в SQL, эта технология практически ничего не дает программисту. Дело в том, что ссылки строготипиэированы, то есть столбец типа REF может указывать толь- только на объект того типа, который указан в его определении. Внешние же ключи могут указывать на любые строки, для которых определен первичный ключ или уникальный индекс. Функция REFTOHEX Ранее в этой главе для получения щестнадцатеричного значения типа REF мы при- применяли такую инструкцию: SELECT REF(c) FROM catalog_iterns с WHERE ROWNUM - 1: При этом результаты запроса отображались в шестнадцатеричном виде, по- поскольку SQL'Plus умеет преобразовать двоичные данные типа REF в шестнадцате- ричную форму при выводе на печать. Такое же преобразование можно задать явно с помощью функции REFTOHEX: SELECT REFTOHEX(REF(c)) FROM catalogjtems с WHERE ROWNUM - 1; Когда речь вдет о запросе в SQ,L*Plus, ее можно не использовать. Однако если вы выполняете запрос в PL/SQL и хотите получить шестнадцатеричное значение REF, обязательно включите в него функцию REFTOHEX, чтобы явно задать преобра- преобразование результата. К сожалению, эту функцию нельзя непосредственно вызвать из PL/SQL. По- Поэтому следующий код, который извлекает значение типа REF из базы данных в пе- переменную itemref и пытается посредством функции REFTOHEX преобразовать его в шестнадцатеричное представление, работать не будет: DECLARE itemref REF catalog_lten t: itemref as_hex VARCHAR2U00): BEGIN -- Считываем значение REF из базы данных. SELECT REFfc) INTO Itemref FROM catalogjtems с WHERE ROWNUM - 1: -- С помощью REFTOHEX пытаенся получить -- шестнадцатеричное представление значения REF. ¦ftemrefjsjiex :- REFTOHEXdtemref): •- Выводим длину результата и сам результат.
Пример объектного приложения 825 DBMS_OUTPUT. PUTJ.INE (LENGTH! itemref_as_hex)): DBMS_CUTPUT.PUT_LINE{1temref_as_hex); END: Попытавшись его выполнить, вы получите тахую ошибку: ERROR at line 10: ORA-06550: line 10. column 22: PLS-00Z01: identifier 'REFTOHEX' must be declared ORA-06550: line 10. column 4: PL/SQL: Statement ignored Как видите, PL/SQL не поддерживает всех функций SQL. Хотя Oracle и объя- объявила, что PL/SQL и SQL используют общий синтаксический анализатор, под- поддержка функции REFTOHEX требует большего, чем синтаксический анализ. Подход, когда для преобразования REF в шестнадцатеричное значение нужно извлекать из базы данных дополнительные данные, является не эффективным, поэтому можно надеяться, что поддержка этой функции все же будет включена в PL/SQL. По- Поскольку Oracle добавила в PL/SQL поддержку множества других встроенных функ- функций (например, NVL2 и TREAT), можно сказать, что в этом отношении имеются по- положительные тенденции. Пакет UTL_REF Встроенный пакет UTL_REF выполняет операцию разыменования (получения зна- значения объекта, на который указывает данная ссылка) без явного вызова SQL Это позволяет приложению программным путем блокировать, извлекать, обновлять и удалять объекты на основе их REF-ссылок. Чтобы рассмотреть эту возможность, добавим в объектный тип subject_t еще один метод; MEMBER FUNCTION printjrt Cstr IN VARCHARZ) RETURN VARCHAR2 IS bt subjectj: BEGIN IF SELF.broader_term_ref IS NULL THEN RETURN str; ELSE UTL_REF.SELECT 08JECT(SELF.broa<ter_tere_ref, bt); RETURN bt.print_bt(NVL{str.SELF.name)) || "¦(' |j bt.name || T: END IF; END: Эта рекурсивная процедура просматривает иерархию от текущей категории до самой верхней - родительской. ПОДДЕРЖКА ССЫПОК В ЯЗЫКЕ С Хотя PL/SQL и предоставляет большие возможности для использования объектных ссылок, в Oracle Call Interface (OCI), интерфейсе языка C/C++ и даже в Pro"C они значительно шире. Помимо навигации по ссылкам, как в PL/SQL, OCI поддерживает комплексное извлечение объектов (Complex Object Retrieval, COR). продолжением
826 Глава 21 • Объектно-ориентированные возможности PL/SQL Это дает возможность за один вызов извлечь объект и все объекты, на ко- которые он ссылается. И OCI, и Рго*С поддерживают клиентское кэширо- кэширование объектов, позволяющее приложению загружать объекты в память клиентской системы и манипулировать ими, как если бы они находились в базе данных (выполнять выборку, вставку, обновление и удаление). По- После этого за один вызов приложение может передать все изменения обрат- обратно на сервер. Помимо расширения функциональных возможностей при- приложения все эти функции сокращают количество обращений к серверу, повышая общую производительность клиент-серверной системы. При использовании процедур из пакета UTL_REF задаваемый вами аргумент REF должен иметь тот же тип, что и объектный аргумент. Ниже приведен полный спи- список процедур этого пакета. UTL_REF.SELECT_OBJECT (.объектная_ссылкв IN, объектная_перененнвя OUT); Находит объект, на который указывает значение аргумента объектная_ссыпкэ, и извлекает его копию в объектную переменную. Lm__REF.SELECT_OBJECT_WITH_CR {объектная ссыпка IN. объектная_переменная OUT): Подобна SELECT_OBJECT, но делает копию («снимок») объекта. Данная версия позволяет избежать ошибки ORA-4091, возможной при обновлении объектной таблицы и присваивании значения функции, использующей пакет UTL_REF для разыменования ссылки объекта из той же таблицы, которая обновляется. UTL_REF.LOCK_OBJECT (.объектна?_ссылка IN): Блокирует объект, на который указывает значение аргумента объектная_ссылка, но не извлекает его. LJTL_REF.LOCK_OBJECT (объектная_ссылка IN, объектная переменная OUT): Блокирует объект, на который указывает значение аргумента объектная_ссылка, и извлекает его копию в объектную переменную. UTL_REF.UPDATE_OBJECT {объектна*_ссыпка IN, объектная_перемеиная OUT): Заменяет объект, на который указывает значение аргумента объектная_ссыпка, значением объектной переменной. UTL_REF.OELETE_OBJECT (оВъектиая_ссылкз INT): Удаляет объект, на который указывает значение аргумента объектная_ссылка. Объектные ссылки и иерархии типов Все перечисленные выше подпрограммы являются процедурами, а не функция- функциями1, а их параметры являются не полностью слаботипизированными. Иными сло- словами, если ссылки соответствуют объектным переменным, то во время компиля- компиляции Oracle не нужно знать их точные типы данных. 1 Было бы неплохо, чтобы как минимум SELECTJBJECT являлась функцией.
Пример объектного приложения 827 Рассмотрим особенности объектных ссылок, которые проявляются при работе с иерархиями типов. Предположим, что в программе объявлены следующие пере- переменные: DECLARE book bookj;: item cataTog_itefli_t; itemref REF catalogjtenrt; bookref REF bookj:" Вы уже видели, что если извлекаемое по ссылке значение присваивается пере- переменной сильнотипизированного типа, то все работает четко: SELECT REF(c) INTO itemref FROM catalog_iterns с WHERE 1d - 10007; Подобным образом можно извлечь объект по ссылке с помощью процедуры SELECT_OBJECT: UTL_REF.select_objectAtemref. item); или: SELECT DEREF(itenref) INTO Item FROM DUAL; Однако непосредственное сужение объектного типа при выполнении данной операции не допускается: SELECT REF(c) INTO bookref /* Ошибка */ FROM catalogjtems с WHERE 1d - 10007: Для сужения типа можно воспользоваться функцией TREAT, которая «умеет» работать и со ссылками: SELECT TREAT(REFCc) AS REF book_t) INTO bookref FROM catalogjtems с WHERE id - 10007: Расширить или привести объект к родительскому типу можно явно: LJTL_REF.select_obJect(TREAT!bookref AS ref catalog_item_t], item); и неявно: SELECT DEREF(bookref) INTO Item FROM DUAL: Хотя непосредственно сузить или привести объект к дочернему типу при ис- использовании функции DEREF нельзя: SELECT DEREFdtrairer) INTO book /* Ошибка */ FROM DUAL; функция TREAT и здесь придет на помощь: SELECT DEREF(TREATAtemref AS REF book_t)) INTO book FROM catalogjtems с WHERE 1d - 10007: Неявное приведение к дочернему типу также можно выполнить с помощью процедуры из пакета UTL_REF: UTLJ4EF.select_objectAtemref. book);
828 Глава 21 • Объектно-ориентированные возможности PL/SQL Висячие ссылки Ссылка REF может ни на что не указывать, тогда она называется висячей. Так мо- может случиться, когда объект, на который указывает хранящаяся в базе данных ссылка, удален. Это можно делать, если не определено ограничение внешнего клю- ключа, препятствующее подобному удалению. Для поиска висячих ссылок используется оператор IS DANGLING: SELECT VALUE(s) FROM subjects s WHERE broaderjermj-ef IS DANGLING; Далее мы познакомимся со средствами Oracle, предназначенными для работы с данными неизвестных или изменяющихся типов. Типы данных «any» В главе 12 мы упоминали о том, что Oracle поддерживает несколько встроенных типов данных, универсально расширяющих модель типов. Пользуясь встроен- встроенным типом данных ANYDATA, подпрограмма PL/SQL может, например, сохранять в базе данных, считывать и обрабатывать элементы данных любого из типов SQL, и вам не нужно создавать десятки ее перегруженных версий. Для работы с произвольными типами данных Oracle предоставляет следую- следующие типы и пакеты. 0 Тип данных ANYDATA. Инкапсулирует любые элементы данных типов SQL в самодокументированные структуры данных. О Тип данных ANYTYPE. Совместно с типом данных ANYDATA применяется для считывания описания структуры данных. Может использоваться и сам по себе для создания временных объектных типов. О Пакет DBMS_TYPES. Пакет, состоящий только из констант, которые помо- помогают интерпретировать типы данных, используемые в объекте ANYDATA О Тип данных ANYDATASET. Подобен типу ANYDATA, но его содержимым явля- является один или несколько экземпляров типа данных. Обработка значений типа ANYDATA Следующая программа возвращает строковую версию содержимого любой пере- переменной. Нынешняя ее версия работает только с числами, строками, датами, объ- объектами и ссылками REF, но ее можно расширить для поддержки практически лю- любых типов данных. /* Файл s web: piintany.fun */ 1 CREATE OR REPLACE FUNCTION print any (adata IN ANYDATA) 2 RETURN VARCHAR2 3 AS 4 aType ANYTYPE; 5 retval VARCHAR2C2767); 6 result_code PLSJNTEGER: 7 BEGIN 8 CASE adata.GetType(aType) 9 WHEN DBMSJYPES.TYPECODEJUMBER THEN 10 RETURN 'NUMBER: ' || TO_CHARtadata.AccessNumber): 11 WHEN DBMS TYPES.TYPECODE VARCHAR2 THEN
Пример объектного приложения 829 12 RETURN 'VARCHAR2: ' || adata.AccessVarcharZ; 13 WHEN 0BMS_TYPES.TYPECO0E_CHAR THEN 14 RETURN 'CHAR: ' || RTRlM(adata.AccessChar]: 15 WHEN DBMS_TYPES.TYPECODE_DATE THEN 16 RETURN 'DATE: ' || TO_CHAR(adata.AccessDate. 'YYYY-MM-DD hh24:mi:ss'); 17 WHEN DBMSjrYPES.TYPECODE_OBJECT THEN IB EXECUTE IMMEDIATE 'DECLARE ' || 19 ' myobj ' j| adata.GetTypeName || ': ' || 20 ' myad ANYOATA :- :ad; ' || 21 'BEGIN ' || 22 ' :res :- myad.GetObJectdnyobj}; ' || 23 ' :ret :- myobj.printt); ' || 24 'END;1 25 USING IN adata. OUT result_code. OUT retval: 26 retval :- adata.GetTypeName || '; ' || retval; 27 WHEN DBMS_TYP?S.TYPECDDE REF THEN 28 EXECUTE MEDIATE 'DECLARE ' || 29 ' myref ' || adata.GetTypeName || '; ' || 30 ' myobj ' |j SU8STRCadata.GetTypeName, 31 INSTRUdata.GetTypeName. ' ')) || ': ' || 32 ' inyad ANYDATA ;- :ad; ' || 33 . 'BEGIN '|| 34 ' :res :- myad.GetREF(myref); ' || 35 ' UTL_REF.SELECT_OB0ECT(myref, myobj>;' || 36 ' :ret :- myobj.print!); ' || 37 'END;' 38 USING IN adata, OUT result_code. OUT retval; 39 retval :- adata.GetTypeName || ': ' || retvaT; 40 ELSE 41 retval ;- '<data of type ' || adata.GetTypeName \\'>': 42 END CASE; 43 • ¦ 44 RETURN retval: 45 46 EXCEPTION 47 WHEN OTHERS 48 THEN 49 IF INSTRCSQLERRM. 'component "PRINT" must be declared1) > О 50 THEN 51 RETURN adata.GetTypeName || ': <=no print О function^ : 52 ELSE 53 RETURN 'Error: ' || SQLERRM: 54 END IF; 55 END; Рассмотрим код данной программы более подробно. Строка Описание 1 Функция получает единственный аргумент типа AMYDATA. Для ее вызова нужно преобразовать переменную к этому типу. Например: DBMS_OLrrPLrr.PUT_LINE(printany(ANYDATA.ConvertNumb€rC.141S9))); 5 На случай, если понадобится временная переменная для хранения результата, резервируем для нее 32 Кбайт. Поскольку для больших переменных типа VARCHAR2 PL/SQL выделяет память динамически, лишнюю память нам не приходится задействовать продолжение^
830 Глава 21 • Объектно-ориентированные возможности PL/SQL Строка Описание 6 Значение переменной result_code (см. строки 25 и 38) не используется в операциях, которые выполняются в примере, но его требует API ANYDATA 8 Тип данных ANYDATA включает метод GetType, возвращающий код соответствующего типа данных. Его спецификация следующая: MEMBER FUNCTION ANYDATA.GefType (OUT NOCOPY ANYTYPE) RETURN код_типа_1п1едег Для использования данного метода необходимо объявить переменную типа ANYTYPE, в значении которой Oracle хранит детальную информацию об инкапсулированном типе 9,11,13,15, В этих выражениях заданы константы из встроенного пакета DBMS_TYPES 17,27 10,12,14,16 В этих операторах используются функции-члены ANYDATA.ConvertNNN, введенные в Oracle9l Release2, S Release 1 можно было использовать процедуры-члены GetNNN, выполняющие похожие операции, однако им требовалась временная локальная переменная 18-25 Чтобы вывести информацию об объекте без многочисленных обращений к словарю данных, динамический анонимный блок создает объект нужного типа и вызывает его метод-член prlntQ 28-36 Задача данного кода — найти объект по указателю и вернуть его содержимое. Он будет работать, только если у объекта имеется метод prlntQ 49-51 Если попытаться вывести информацию об объекте, у которого нет метода print, генерируется ошибка времени выполнения. В этой чзсти кода мы ее перехватываем и обрабатываем Далее мы рассмотрим простой пример вызова этой функции: DECLARE achar CHARB0) :- 'fixed-length string': abook book_t :- NEW book_t(id -> 12345. title -> 'щу book', pages -> 100); sref REF seriait; asub subject_t :- subjectjtCTne World'. NULU: BEGIN DBMS_OUTPUT.PUT_LINE{printany(ANYOATA.ConvertChar(achar))); oeHS_OUTPUT.PUT_LINE(printany(ANYOATA.ConvBrtH)ject(abook))): OBMSJMPUT. PUT_LINE(pr1ntany CANYDATA. ConvertOb ject (asub))); SELECT TREAT(REF(c) AS REF serialj) INTO sref FROM catalog Items с WHERE title - Time1; DBMS OUTPUT.PLrf_LINE(pr1ntany(ANYDATA.ConvertRef(sref))): END: Код выводит следующее: NUMBER: 3.141592654 CHAR: fixed-length string SCOTT.BOOK_T: 1d-l2345; mie-щу book; publ1cat1on_date-; 1sbn-; pages-100 SCOTT.SUBJECT T; <no prlntO funct1on> REF SCOTT.SERTaLJ: 1d-l0004; t1t1e-T1me; publ1CBt1on_date-; 1ssn-0040-78lX; openjjrj:losed-Open Как видите, тип данных ANYDATA не так удобен, как иерархии наследования, по- поскольку требует явного преобразования данных. Однако он позволяет создавать
Пример объектного приложения 831 столбцы таблиц и атрибуты объектов, в которых можно хранить данные практи- практически любого типа1. Создание временного типа данных PL/SQL не поддерживает определения новых объектных типов в разделе объяв- объявлений программы, но с помощью встроенных типов данных ANY можно создавать «временные» типы, существующие только в период выполнения программы. Зна- Значения типа, созданного с помощью ANYTYPE, можно передавать в качестве парамет- параметра. Кроме того, можно создавать его экземпляры как значения типа ANYDATA. Рас- Рассмотрим следующий код: CREATE OR REPLACE FUNCTION create a type RETURN ANYTYPE AS myarv ANYDATA: mytype ANYTYPE; BEGIN /* Создание анонимного временного типа с свумя атрибутами: number, date */ ANYTYPE.Beg1nCreate(typecode -> DBMSJYPES.TYPECODE OBJECT, atype -> retype); mytype.AddAttrttypecode -> DBMSJYPES.TYPECODEJIUMBER, aname -> 'just ajiunber', prec -> 38. scale -> 0. len -> NULL. cs1d -> NULL, csfrm -> NULL):~ mytype.AddAttr(typecode -> DBMS_TYPES.TYPECODE_DATE. aname -> 'just_a_date', prec -> 5. scale -> 5, len -> NULL, cs1d -> NULL, csfrm -> NULL): mytype.EndCreate: RETURN mytype; END: Как видите, процесс проходит в три основных этапа. 1. Создание типа начинается вызовом статической процедуры BeginCreate. Она возвращает инициализированный объект ANYTYPE. 2. С помощью процедуры-члена AddAttr по одному добавляются атрибуты. 3. Вызывается процедура-член EndCreate. Аналогичным образом перед использованием типа присваиваются значения его атрибутам: DECLARE ltype ANYTYPE :- create a type: l_any ANYDATA: BEGIN ANYDATA.Beg1nCreate(dtype -> ltype. adats ¦» l_any); 1 any.SetNunberCnuni -> 12345); Tany.SetDatetdat -> SYSDATE): 1 any.EndCreate; END:" Если структура данных заранее не известна, информацию о ней можно полу- получить с помощью методов ANYTYPE (в частности, GetAttrElemlnfo) в сочетании с ме- методами ANYDATA.Get. (Пример вы найдете в файле anyObJect.sql на web-узле изда- издательства O'Reilly.) 1 На момент создания этой книги не допускалось хранение в таблице значений ANYDATA, которые ин- инкапсулируют эволюционирующий объект или объект, входящий в состав иерархии тип00-
832 Глава 21 « Объектно-ориентированные возможности PL/SQL Все делаем сами Некоторые приверженцы объектно-ориентированного программирования счита- считают, что каждый объектный тип должен быть самодостаточным. Если объект пред- предназначен для хранения в базе данных, необходимо знать, как его сохранить и ка- какие у него должны быть методы для обновления, удаления и выборки, то есть тип следовало бы дополнить такими методами: ALTER TYPE catalogJtem_t ADD MEMBER PROCEDURE remove CASCADE: CREATE OR REPLACE TYPE BODY catalogjtemj AS MEMBER PROCEDURE remove IS BEGIN DELETE catalog_items WHERE id - SELF.id: SELF ;- NULL: END; END: (Oracle не предлагает для объектов методы-деструкторы.) Определив метод на уровне супертипа, мы автоматически включили его и во все подтипы. В дан- данном случае предполагается, что все соответствующие объекты находятся в одной таблице, однако в некоторых приложениях может потребоваться дополнительная логика для поиска объектов. (Кроме того, работающая в реальных условиях вер- версия метода может включать код для выполнения дополнительных операций, та- таких как удаление зависимых объектов и/или архивирование данных перед окон- окончательным удалением объекта.) Если предположить, что перед записью временного объекта на диск приложе- приложение должно его модифицировать в памяти, можно объединить вставку и обновле- обновление в одном методе, который мы назовем save. ALTER TYPE catalogjtem_t ADD MEMBER PROCEDURE save CASCADE: CREATE OR REPLACE TYPE BODY catalog_1tera_t AS MEMBER PROCEDURE save IS BEGIN UPDATE catalogi terns с SET с - SELF WHERE 1d - SELF.id; IF SQL3R0WCOUNT - 0 THEN
Пример объектного приложения 833 INSERT INTO catalog_iterns VALUES (SELF); END IF; END; Эта процедура заменяет значения всех столбцов, даже если они не измени- изменились. В результате подобной операции могут вызываться «липшие» триггеры, что приведет к ненужным операциям ввода-вывода. К сожалению, это один из недос- недостатков объектного подхода. При более внимательном программировании можно было бы избежать модификации неизменившихся значений столбцов супертипа, но столбцы подтипа ни в какой разновидности инструкции UPDATE в отдельности недоступны, Из всех операций над объектными таблицами труднее всего инкапсулировать выборку, что связано с разнообразием возможных условий в предложении WHERE и большим количеством форм данных результата. Как следствие, спецификация критерия запроса может оказаться очень сложной. Каждый, кому приходилось разрабатывать формы для выполнения пользовательских запросов, подтвердит это. Что касается результатов запроса, то и они могут быть представлены в одном из следующих видов: О коллекция объектов; О коллекция ссылок REF; О результирующий набор с конвейерной организацией; О курсорная переменная (сильно- или слаботипизированного типа). На выбор представления могут повлиять требования к приложению и необхо- необходимая для него программная среда. Рассмотрим пример, в котором используется курсорная переменная: ALTER TYPE catalogjtenrt ADD MEMBER FUNCTION retrievejiatching RETURN SYS_REFCLJRSOR CASCADE; В данном случае применяется встроенный тип SYSREFCURSOR - слаботипизи- рованный тип курсора, который Oracle предоставляет просто для удобства. Идея заключается в следующем: вызывающее приложение создает экземпляр типа са - ta1og_item_t, содержащий образец искомых данных, вызывает его функцию ret- rievejnatching и затем использует возвращаемую ею курсорную переменную как указатель на результирующий набор строк. Ниже приведен код выполнения за- запроса с помощью функции retrievejnatching: DECLARE catalog_item catalog_item_t; l_refcur SYS_REFCURSOR; l_sample_object book_t :- NEW book_t(title -> 'Oracle*'): BEGIN l_refcur :- l_sample_object.retr1eve_match1ngC); LOOP FETCH l_refcur INTD catalogjtem; EXIT WHEN lj-efcurSNOTFOUND: DBMSJ)ltfPUT.PUT_LlNE(catalog_i tan. print); END LOOP; CLOSE "Irefcyr; END:
834 Глава 21 • Объектно-ориентированные возможности PL/SQL При реализации данного подхода необходимо решить, должен ли запрос из- извлекать все соответствующие элементы каталога или только элементы типа, х ко- которому относится и экземпляр объекта. В первом случае соответствующая реали- реализация будет такой: CREATE OR REPLACE TYPE BODY catalog_1tem_t AS MEMBER FUNCTION retrievejwtchinfi RETURN SYSJEFCURSOR IS 1 refcur SYS REFCURSOR: BEGIN IF SELF IS OF (book t) THEN OPEN 1 refcur FOR SELECT VALUE(C) FROM catalogj terns с WHERE (SELF.Id IS NULL OR 1d - SELF.Id) AND (SELF.title IS NULL OR title LIKE SELF,title || T) ANO fSELF.pubHtttionjfate IS NULL OR publ1cat1on_date - SELF.pobT1cat1on_date) AND (TREAT(SELF AS book_t).1sbn IS NULL OR TREAT(VALUE(c) AS book t).1sbn - TREATCSELF AS bookj). 1 sbn) AND (TREATCSELF AS book_t).pages IS NULL OR TREAT(VALUE(C) AS book t).pages - TREAT(SELF AS bookj). pages): ELSIF SELF IS OF (serial t) THEN OPEN 1 refcur FOR SELECT VALUE(c) FROM catalogjtems с WHERE (SELF.Id IS NULL OR 1d - SELF.Id) AND (SELF.title IS NULL OR title LIKE SELF.title || T) AND (SELF.publ1cat1on_date IS NULL OR publ1cation_date - SELF.publication date} ANO (TREATCSELF AS serial_t).issn IS NULL OR TREAT(VALUE(c) AS serial_t).issr - TR?AT(SELF AS serial_t).1ssn) AND (TREATtSELF AS serial t).open or_closed IS NULL OR TREAT(VALUEU) AS"serial_t),open_or closed - TREAT(SELF AS serial tj.open or_closed); END IF; RETURN l_refcur: END: Это не самое лучшее решение, поскольку оно требует модификации метода retr1eve_match1 ng супертипа при добавлении каждого нового подтипа. Можно было бы подменять данный метод в подтипах, но когда потребуется результирующий набор, включающий книги и периодику, мы не сможем вызвать метод супертипа. Получится странная ситуация, когда запрос для книги возвращает иформацию о периодике или наоборот,
Пример объектного приложения 835 Конечно, можно модифицировать супертип, разрешив создание его экземпля- экземпляров, но лучше сделать функцию retrievejnatching статическим методом. Попро- Попробуйте выполнить это упражнение самостоятельно. Сравнение объектов До сих пор в наших примерах использовались объектные таблицы, то есть табли- таблицы, в которых каждая строка представляет собой один объект. Такие объекты соз- создаются с помощью инструкции CREATE TABLE., .OF, и для работы с ними Oracle обеспечивает специальные возможности, в частности переходы по ссылкам и ин- интерпретацию целого объекта (а не отдельных его столбцов) как единицы вво- ввода-вывода. Объектный тип может использоваться также в качестве типа отдельного столб- столбца таблицы. Предположим, что мы хотим завести журнал изменений для табли- таблицы catal og_1 terns и фиксировать в нем все операции вставки, обновления и удале- удаления строк этой таблицы1. CREATE TABLE catalog history AS ( id INTEGER NOT NULL PRIMARY KEY, action CHARU) NOT NULL. act1on_t1me TIMESTAMP NOT NULL DEFAULT Т1ИЕ5ТАМР. oldjten catalogjteni_t. newjteu catalog_itefli_t); Когда вы начнете заполнять таблицу объектами, у вас возникнет вопрос, ка- какой метод использует Oracle при сортировке или индексировании таблицы по од- одному из столбцов типа catalog_1tem_t. Существуют четыре метода сравнения для объектов. О Сравнение на уровне атрибутов. При сортировке, создании индексов и срав- сравнении задаются соответствующие атрибуты. О Стандартный способ SQL. SQL Oracle умеет выполнять простейшее сравне- сравнение на эквивалентность. Два объекта считаются эквивалентными, если они определены на основе одного и того же типа, а все их соответствующие атри- атрибуты равны. Это сравнение может быть выполнено, когда объекты имеют толь- только скалярные атрибуты (не коллекции и не LOB) и их объектный тип не со- содержит метод MAP или ORDER. О Мегод-член MAP. Можно создать специальную функцию-метод, возвращаю- возвращающую не значение объекта, а соответствующее ему значение одного из типов данных, которые Oracle умеет сравнивать. Этот метод работает только при от- отсутствии метода ORDER. О Метод-член ORDER. В данном методе используется функция, которая срав- сравнивает два объекта и возвращает флаг, указывающий их относительный поря- порядок. Этот метод работает только при отсутствии метода MAP. Казалось бы, проще всего заполнять таблицу-журнал с помощью триггеров DML. Однако внутри триггера недоступны атрибуты подтипов нижнего уровня. Поэтому в Нем можно фиксировать толь- только изменения общих атрибутов (атрибутов супертипа).
836 Глава 21 • Объектно-ориентированные возможности PL/SQL Стандартному методу сравнения SQL редко находится применение, и поэтому мы не будем его рассматривать. О других, более полезных способах сравнения объектов рассказывается в следующем разделе. Сравнение на уровне атрибутов Сравнение на уровне атрибутов может быть не совсем тем, что требуется, но зато оно выполняется очень просто, как в PL/SQL, так и SQL (если только вы не забу- забудете задать псевдоним таблицы в SQL-инструкции). Oracle позволяет задавать атрибуты с помощью точечного синтаксиса: SELECT * FROM catalog history с WHERE c.oldjitem.id > 10000 OROER BY NVL(TREAT(c.old_itetn as book_t).isbn. TREAT(c.old_it<Hn AS serialJJ.issn) Создание индекса на основе атрибутов выполняется так же просто: CREATE INDEX catalog_history_oldjd_1dx ON catalog_history с (c.old_1tera.1d); Метод MAP Методы MAP и ORDER позволяют выполнять следующие инструкции: SELECT * FROM catalogjiistory ORDER 8Y oldjtem; IF old item > new_item THEN .7. Сначала рассмотрим метод MAP. Его простейший вариант добавляется в cata- log_item_t таким образом: ALTER TYPE catalog_item_t ADD MAP MEMBER FUNCTION mapU RETURN NUMBER CASCADE: CREATE OR REPLACE TYPE BODY catalogjtem t AS ... MAP MEMBER FUNCTION mapit RETURN NUMBER IS BEGIN RETURN id: ' ¦ END; END: Если предположить, что сортировка по идентификаторам имеет смысл, то те- теперь можно сортировать и сравнивать элементы каталога, причем Oracle будет вызывать новый метод автоматически. Функция не обязательно должна быть так же проста; например, она может возвращать скалярное значение, вычисляемое на основе всех атрибутов типа, объединенных способом, который имеет для библио- библиотекарей определенный смысл. У метода MAP есть один побочный эффект: сравнение объектов на эквивалент- эквивалентность может выполняться неподходящим способом. При наличии такого метода два объекта считаются эквивалентными, если равны возвращаемые ими значе- значения. Когда вам нужно сравнивать объекты, по очереди анализируя их атрибуты, создайте собственный метод (не MAP) или же используйте метод ORDER.
Пример объектного приложения 837 Метод ORDER Альтернативой методу MAP является функция-член ORDER, которая сравнивает два объекта: SELF и еще один объект того же типа, заданный в качестве ее аргумента. Эта функция должна возвращать целочисленное значение, которое в зависимо- зависимости от порядка двух объектов может быть положительным, нулевым или отрица- отрицательным (табл. 21.2). Таблица 21.2. Поведение функции-члена ORDER Порядок элементов Соответствующее значение функции ORDER SELF < Объект_в_аргументе Любое отрицательное число (обычно -1) SELF = Объект_в_аргументе О SELF > Объект_в_аргументе Любое положительное число (обычно 1) Результат сравнения не определен NULL Рассмотрим пример кода, в котором используется метод ORDER. 1 ALTER TYPE catalog_item_t 2 DROP MAP MEMBER FUNCTION raapH RETURN NUMBER 3 CASCADE: 4 5 ALTER TYPE catalog_item_t 6 ADD ORDER MEMBER FUNCTION order-It (obj2 IN catalog item t) 7 RETURN INTEGER 8 CASCADE; 9 10 CREATE OR REPLACE TYPE BODY catalogjtemj: 11 AS ... 12 ORDER MEMBER FUNCTION orderit (obj2 IN catalog 1tem_t) 13 RETURN INTEGER 14 [S 15 se1f_gt_o2 CONSTANT PLSJNTEGER :- 1: 16 eq CONSTANT PLSJNTEGER := 0; 17 O2_gt_self CONSTANT PLSJNTEGER :- -1: 18 1jnatching_count NUMBER; 19 BEGIN 20 CASE 21 WHEN obj2 IS OF ibooYJ.) AND SELF IS OF (serial_t) THEN 22 RETURN o2 gt self; 23 WHEN obj2 IsF~(senal_t) AND SELF IS OF (book_t) THEN 24 RETURN self gt_o2: 25 ELSE 26 IF obj2.title - SELF.title 27 AND obj2.pub1ication_date - SELF.publ1cation_date 2B THEN 29 IF obj2.subject refs IS NOT NULL 30 AND SELF.subJect_refs IS NOT NULL 31 AND obj2.subject_refs.COUNT - SELF.subjectjrefs.COUNT 32 THEN 33 SELECT COUNT(*) INTO 1 jnatcMng count FROM 34 (SELECT *
840 Глава 21 • Объектно-ориентированные возможности РЦ/SQL О Триггеры INSTEAD OF. С помощью триггера INSTEAD OF можно указать Oracle, как именно можно выполнять вставку, обновление и удаление данных в пред- представлениях. С точки зрения объектной теории, объектные представления имеют один не- несущественный недостаток — они не обеспечивают инкапсуляции. Поскольку при- приложение применяет инструкции INSERT, UPDATE и DELETE непосредственно к содер- содержимому таблиц реляционных баз данных, ни о какой инкапсуляции речь не идет. Тогда как объектно-ориентированная архитектура приложения обычно не допус- допускает непосредственного доступа к данным. Однако поскольку Oracle не поддер- поддерживает ни личных атрибутов, ни личных методов, ее объектные средства не обес- обеспечивают полной инкапсуляции. Если расположить объектные представления «поверх» существующей систе- системы, новые приложения смогут пользоваться их преимуществами, а старые будут работать, как и прежде. Применение объектных представлений представлено на рис. 21.2. Сшцифмафш объекта Тело ОбъКГ! Обметим представление Извлечение денных Таблицы ре; — базы да 1ЛЦИ( нны )ИН ( ой Обновление данных Обеспечивает программное управление обновлением данные представления Рис. 21.2. Объектные представления позволяют связать определение объектного типа с существующими реляционными таблицами В следующих разделах обсуждаются особенности объектных представлений, в том числе различия между объектными представлениями и объектными табли- таблицами, которые имеют особую важность и представляют интерес для программи- программистов PL/SQL Существующие реляционные системы В качестве второго примера этой главы рассмотрим применение объектных пред- представлений в приложении базы данных компании, занимающейся web-дизайном.
Объектные представления 841 Реляционное приложение компании используется для работы с информацией об изображениях (GIF, JPEG и т. д.), которые размещаются на разрабатываемых ими web-узлах. Эти изображения находятся в соответствующих файлах, но ин- информация о них содержится в таблицах базы данных. Для того чтобы художни- художникам легче было искать нужные изображения, с каждым из них связано одно или несколько ключевых слов. Данные об изображениях и ключевые слова, хранятся в виде двух таблиц, связанных отношением «главная-подчиненная». В базе данных имеется таблица поставщиков изображений: CREATE TABLE suppliers ( id INTEGER NOT NULL PRIMARY KEY. name VARCHAR2D00) NOT HULL ): Ниже приведена таблица метаданных изображений: CREATE TABLE Images ( 1mage_1d INTEGER NOT NULL PRIMARY KEY. filename VARCHAR2C51Z) NOT NULL, fllejype VARCHAR2U2) NOT NULL, suppHerjd INTEGER REFERENCES suppliers Ad). supplier r1ghts_deschptor VARCHAR2B56). bytes INTEGER ): Однако не все изображения поступают от поставщиков. Если значение иден- идентификатора поставщика (supplier id) равно NULL, значит, изображение создано сотрудниками компании. В базе данных имеется еще одна таблица, которая содержит ключевые слова, связанные с изображениями: CREATE TABLE keywords ( 1mage_id INTEGER NOT NULL REFERENCES images (image id). keyword VARCHAR2D5) NOT NULL. CONSTRAINT keywords_pk PRIMARY KEY (imagejd. keyword) ); Предположим, что в эти таблицы помещены такие данные: INSERT INTO suppliers VALUES A01. 'Joe"s Graphics1); INSERT INTO suppliers VALUES A02. 'Image Bar and Grill'); INSERT INTO images VALUES A00001. 7files/web/60s/smiley_face.png', 'image/png'. 101, 'fair use1. 813); INSERT INTO images VALUES A00002. 7files/web/60s/peace_symbol .g-if , 'image/gif. 101, 'fair use1. 972): INSERT INTO Images VALUES A00003. 'files/web/OOs/towers.jpg'. Mmage/jpeg1. NULL. NULL. 2104); INSERT INTO keywords VALUES A00001. 'SIXTIES'): INSERT INTO keywords VALUES A00001. 'HAPPY FACE'); INSERT INTO keywords VALUES A00002. 'SIXTIES'); ¦ " INSERT INTO keywords VALUES A00002. 'PEACE SYMBOL'); INSERT INTO keywords VALUES UOO0D2. 'DERRY RUBIN');
838 Глава 21 • Объектно-ориентированные возможности PL/SQL 35 36 37 за 39 40 41 42 43 44 45 46 47 48 49 END 50 51 END: Ниже Строки FROM TABLEC5ELECT CAST(SELF.5UbJect_ref5 AS subjectj-efs_t) FROM dual) INTERSECT 5ELECT • FROM TABLECSELECT CAST(obj2.subject_refs AS subject refs t) FROM dual»: IF 1 matching count - SELF.subject_refs.COUNT THEN RETURN eq; END IF; END IF; END IF; RETURN NULL: END CASE; ; приведен ряд замечаний относительно данного кода. Описания 21-24 Определено, что при сортировке книги будут размещаться перед периодикой 26-46 Проверка на эквивалентность. Поскольку Oracle не знает, как сравнивать коллекции, мы воспользуемся способностью Orade выполнять выборку из коллекции, как ш таблицы. Проверив, содержит ли реляционное пересечение двух коллекций ожидаемое количество элементов, можно определить, имеет ли каждый элемент первой коллекции эквивалент во второй коллекции (предполагается, что таково определение эквивалентности} Этот метод не в полной мере отвечает всем требованиям, поскольку не прове- проверяет атрибуты, специфические для подтипа, но более длинный код привести было невозможно, поскольку объем книги ограничен. Рекомендации по проведению сравнения В заключение приведем несколько правил и рекомендаций, касающихся методов сравнения объектов. О Методы MAP и ORDER не могут сосуществовать в одном объектном типе, поэтому используйте один из них. О При сортировке или сравнении большого количества объектов, как это бывает в SQL-инструкциях, Oracle рекомендует применять метод MAP. Дело в том, что внутренняя оптимизация сокращает количество его вызовов, тогда как метод ORDER должен обязательно вызываться для каждого сравнения. О Oracle игнорирует имена методов, поэтому можете называть их как угодно. О Подтипы могут включать метод MAP, но только если он есть и у супертипа. О Подтипы не могут включать метод ORDER, реализовать его можно в супертипе.
Объектные представления 839 Объектные представления Объектные расширения Oracle предлагают программистам PL/SQL богатые воз- возможности для разработки'новых систем, однако маловероятно, что ради их при- применения вы захотите полностью перепроектировать старые системы. Для того что- чтобы существующие приложения могли использовать преимущества новых объект- объектных функций, в Oracle реализованы объектные представления. Они имеют ряд важных достоинств, которые описаны ниже. О Объектное представление удаленных данных. В ОгаскЭг еще не поддержива- поддерживаются объектные таблицы и физические REF-ссылхи в распределенных базах данных, но можно создавать объектные представления и виртуальные REF-ссыл- ки, представляющие удаленные данные реляционных баз как объекты. О Виртуальная денормализация. В реляционной базе данных или даже объект- объектно-реляционной базе данных обычно реализованы однонаправленные отно- отношения. Например, книга относится к определенному количеству категорий. Объектные представления позволяют установить обратное соответствие, в ча- частности объект-категория может включать коллекцию REF-ссылок, указываю- указывающих на все книги этой категории. О Эффективность доступа к объектам. В приложениях Oracle Call Interface (OCI) могут использоваться программные конструкции, обеспечивающие удобное из- извлечение, кэширование и обновление объектных данных. Сокращая количест- количество обращений приложения х серверу базы данных, они повышают производи- производительность приложения и, кроме того, делают код более лаконичным. О Большая гибкость в отношении изменения объектной модели. Хотя в Orac- Oracled имеются прекрасные средства для реализации эволюции типов, добавле- добавление и удаление атрибутов объектов по-прежнему сопряжено с перемещением таблицы по диску, чего администраторы баз данных очень не любят. Переком- Перекомпиляция объектных представлений не имеет таких последствий. В то же время у объектных представлений есть определенные недостатки. О Производительность. Объектные представления обрабатываются не так бы- быстро, как этого хотелось бы. О Виртуальные ссылки. Виртуальные ссылки не хранятся в базе данных, а соз- создаются в ходе выполнения программы. Это может вызвать определенные про- проблемы, если однажды вы вдруг захотите преобразовать объектные представле- представления в объектные таблицы'. Зато в Oracle появились дополнительные средства, расширяющие возможно- возможности любых представлений, включая и объектные. Отметим две из них — коллек- коллекции и триггеры, в объявлении которых содержится предложение INSTEAD OF (да- (далее триггеры INSTEAD OF). О Коллекции. Если взять две реляционные таблицы, связанные отношением глав- главная-подчиненная, можно создать для них представление, возвращающее дета- детализирующие строки подчиненной таблицы в виде одного нескалярного атри- атрибута (коллекции) строки главной таблицы.
842 Глава 21 • Объектно-ориентированные возможности PL/SQL В следующих разделах описываются объехтные представления, определенные на основе этих данных: О первое представление будет определено на основе типа изображения, вклю- включающего в качестве атрибута коллекцию ключевых слов; О второе представление является подпредставлением, то есть мы определим его на основе подтипа из иерархии объектных типов. Оно будет включать харак- характеристики изображений от поставщиков; О последнее представление включает ключевые слова и соответствующие ссыл- ссылки иа изображения (оно реализует обратное отношение). Объектное представление с атрибутом-коллекцией Перед созданием объектного типа для первого представления нужно определить тип коллекции для хранения ключевых слов. В данном случае используем вло- вложенную таблицу, поскольку порядок ключевых слов не важен и ограничения на их максимальное количество не существует1. CREATE TYPE keywordJ:abj. AS TABLE OF VARCHAR2DS): Объект для хранения изображения определить очень просто: CREATE TYPE 1mage_t AS OBJECT ( Imagejd INTEGER, image file BFILE, fUeJype VARCHAR2U2), bytes INTEGER, keywords keyword_tab_t ); Если файлы изображений и сервер базы данных располагаются на одном ком- компьютере, то вместо имени файла можно использовать тип данных BFILE. Нужно создать «директорию», то есть используемый только в Oracle псевдоним директо- директории операционной системы, в которой содержатся изображения. Зададим корне- корневую директорию, поскольку в столбце f 11 ename указаны полные пути к файлам. CREATE DIRECTORY rootdir AS 7': Мы пока не определили связь между реляционными таблицами и объектным типом. Это две совершенно независимые сущности, и наложение определения объекта на таблицы происходит только при создании объектного представления: CREATE VIEW Images v OF image t WITH OBJECT IDENTIFIER (image 1d) AS SELECT 1.imagejd. BFILENAMECrootdir1, LfHejiame). i.fHe_type. i.bytes, CAST (MULTISET (SELECT keyword FROM keywords k WHERE k.Imagejd - 1.Imagejd) Если порядок ключевых слов имеет значение или если ограничено их максимальное количество Для одного изображения, лучше использовать коллекцию тип» VARRAY.
Объектные представления 843 AS keyword_tsb_t) FROM images I; Два компонента данной инструкции характерны только для объектных пред- представлений. О OF 1mage_t. Означает, что будут возвращены объекты типа 1mage_t. О WITH OBJECT IDENTIFIER dmagejd). Для того чтобы возвращаемые в представле- представлении данные были похожи на настоящие экземпляры объектов, им нужен объект- объектный идентификатор. Определив первичный ключ как основу для виртуально- виртуального ОЩ можно пользоваться преимуществами REF-навигации по объектам пред- представления. Кроме того, список выборки объектного представления должен соответство- соответствовать (по количеству и типу данных атрибутов) атрибутам объектного типа. Итак, объектное представление мы создали, но что же с ним можно делать? Прежде всего, из него можно извлекать данные, как из объектной таблицы. На- Например, в SQL*Plus запрос SOL> SELECT imagejd, keywords FROM Imagesv; вернет следующие данные: IMAGEJD KEYWORDS 100003 KEYW0RD_TAB_TO 100001 KEYWORD_TAB_TC HAPPY FACE'. 'SIXTIES') 1000D2 KEYWCRQ_TAB_T('JERRY RUBIN', 'PEACE SYMBOL'. 'SIXTIES') Чтобы все это еще более походило на объекты, можно добавить в определение типа методы, в частности метод print С): ALTER TYPE 1mage_t ADD MEMBER FUNCTION print RETURN VARCHAR2 CASCADE; CREATE OR REPLACE TYPE BODY Image t AS MEMBER FUNCTION print RETURN VARCHARZ IS filename images.fi1e_name*TYPE; dirname VARCHAR2O0): keywordjist VARCHAR2O2767): BEGIN DBMSJ.OB.FILEGETNAME(SELF.iMge_fne, d1rname. filename): IF SELF.keywords IS NOT NULL THEN FOR keyjlt IN 1..SELF.keywords.COUNT LOOP keyword 11st :- keywordjist || ', ¦ || SELF, keywords (key elt); END LOOP: END IF: RETURN 'Id-' || SElF.imageJd || '; File-1 || filename || ': keywords-' || SU8STR(keywordJ1st. 3): END; END:
844 Глава' 21 • Объектно-ориентированные возможности PL/SQL NULL ИЛИ НЕ NULL? Вы уже знаете, что коллекция, равная NULL, и инициализированная кол- коллекция, не содержащая ни одного элемента, это не одно и то же. У изобра- изображения 100003 нет ключевых слов, но объектное представление по ошибке возвращает пустую инициализированную коллекцию. Чтобы получить вме- вместо нее значение NULL, для проверки количества ключевых слов можно ис- использовать функцию DECODE: CREATE OR REPLACE VIEW 1mages_v OF 1mage_t WITH OBJECT IDENTIFIER dmagejd) AS SELECT 1.1mage_1d, BFILENAMEC'ROOTDIR', 1.file_name). 1.f1le_type, i.bytes. DECODEC(SELECT COUNTC*) FROM keywords 1B WHERE kZ.imageJd - 1.iiragejd). 0. NULL. CAST (MULTISET (SELECT keyword FROM keywords k WHERE k.imagejd - 1.1mage_1d) AS keyword_tab_t)) FROM images i; Если ключевых слов нет, возвращается значение NULL, в противном слу- случае — выражение CASTCMULTISETC...)). Для этого представления инструк- инструкция SELECT ... WHERE image_id-100003 должна вернуть следующие данные: IMAGE 10 KEYWORDS 100003 Однако вы можете посчитать, что такая концептуальная чистота не стоит того, чтобы выполнять дополнительный ввод-вывод (или писать такую сложную инструкцию SELECT). Описанный выше пример показывает, как составить «плоский» список ключе- ключевых слов, содержащихся в виртуальной коллекции. Кроме того, работая с объектными представленими, можно применять сле- следующие средства: О Виртуальные объектные ссылки. Виртуальные объектные ссылки (REF) — это указатели на виртуальные объекты (см. ниже раздел «Различие между объект- объектными представлениями и объектными таблицами»). О Триггеры INSTEAD OF, Триггеры данного типа позволяют непосредственно манипулировать содержимым представления (см. ниже раздел «Триггеры IN- INSTEAD OF»).
Объектные представления 845 Объектные подпредставления Если в нашем примере одни изображения должны интерпретироваться не так, как другие, можно создать для них подтип. Выделим в подтип изображения, у ко- которых есть поставщики. В нем будет содержаться ссылка на объект поставщика, определенный следующим образом: CREATE TYPE supplier t AS OBJECT ( id INTEGER. name VARCHAR2C400) ): Соответствующее объектное представление будет иметь такой вид: CREATE VIEW suppl1ers_v OF suppl1er_t WITH OBOECT IDENTIFIER {id) AS SELECT 1d, name FROM suppliers; Теперь нужно изменить или удалить и повторно создать базовый тип, но уже с предложением NOT FINAL: ALTER TYPE 1qrage_t NOT FINAL CASCADE; Затем можно создать для него подтип: CREATE TYPE supplied_images_t UNDER Inagej ( supplier_ref REF supplier_t, suppl ier_rights_descriptor VARCHAR2B56) ); После этой подготовительной работы создается представление на основе под- подтипа, и в его определение включается предложение UNDER: CREATE VIEW suppl ied_1inages_v OF suppl ledjwgest UNDER Images v AS SELECT i.imagejd. BFILENAMECROOTDIR1. i.f1le_name>. i.file type. i.bytes. CAST (MULTISET (SELECT keyword FROM keywords k WHERE k.imagejd - 1.1mage_1d) AS keyword tab_t), MAKEJJEFtsuppl1ers_v. suppl1er_1d). suppl1er_right5_descriptor FROM images i WHERE suppl1er_id IS NOT NULL: Oracle не позволяет при формировании подпредставления запрашивать дан- данные из суперпредставлення, поэтому в рассматриваемом случае обращение про- происходит непосредственно к базовой таблице, а предложение WHERE ограничивает набор извлекаемых строк. Обратите внимание на то, что в подпредставлениях не используется предложение WITH OBJECT IDENTIFIER, поскольку они наследуют иден- идентификаторы объектов от суперпредставления.
846 Глава 21 • Объектно-ориентированные возможности PL/SQL В этом запросе используется новая функция MAKE_REF, предназначенная для вычисления ссылки на виртуальный объект. Он в данном случае представляет поставщика, возвращаемого представлением suppliers v. Синтаксис функции МА- KE_REF выглядит следующим образом: FUNCTION MAKE_REF {предстанете, шсок^значений) RETURN Ссылка; Здесь представление — это объектное представление, на которое должна указы- указывать ссылка, заданная в значении аргумента ссылке, а список_значений - это разде- разделенный запятыми список значений столбцов, типы данных которых должны в точ- точности соответствовать типам данных OID-атрибутов представления, указанного в значении аргумента представление. Учтите, что функция MAKE_REF не извлекает идентификатор из представления, ока лищъ использует встроенный алгоритм Oracle для формирования REF -значе- -значения. Однако, как и REF-ссылки, виртуальные ссылки могут не указывать на реаль- реальные объекты. Продемонстрируем неожиданный результат - хотя мы не изменяли супер- суперпредставление, изображения от поставщиков теперь выводятся им дважды (то есть дублируются): SQL> SELECT СШШ*). Imagejd FROM 1raages_v GROUP BY COUNTt*) IMAGE 10 2 100001 2 1OQQQ2 1 100003 Дело в том, что Oracle возвращает логическое объединение (UNION ALL) двух за- запросов: к суперпредставлению и подпредставлению. В этом есть определенный смысл, ведь изображение от поставщика все же является изображением. Для уда- удаления дубликатов добавьте в родительское представление предложение WHERE, ко- которое исключает строки, возвращаемые подпредставлением: CREATE OR REPLACE VIEW 1mages_v AS WHERE supplierjd IS NULL: Объектное представление, реализующее обратное отношение С целью демонстрации виртуальной денормализации можно создать объектный тип для ключевых слов и соответствующее представление, связывающее ключе- ключевые слова с изображениями, которые они описывают: CREATE TYPE iirrage_refs_t AS TABLE OF REF 1mage_t; / CREATE TYPE IceywordJ AS OBJECT ( keyword VARCHAR2D5). 1mage_refs image_refs_t):
Объектные представления 847 Определение представления имеет следующий вид: CREATE OR REPLACE VIEW keywords_v OF lceyword_t WITH OBJECT IDENTIFIER (keyword) AS SELECT keyword, CAST(MULT1SET(SELECT MAKE_REFAniages_v. imagejd) FROM keywords WHERE keyword - main.keyword) AS 1mage_refs t) FROM (SELECT DISTINCT keyword~FROM keywords) main; Запросы к данному представлению не будут производиться очень быстро — ведь при этом нужно выполнять операцию SELECT DISTINCT, так как в базе данных нет справочной таблицы ключевых слов. Даже без использования объектных функций с точки зрения времени выполнения эти запросы были бы дорогостоящими. Вы справедливо заметите, что функция MAKE_REF здесь не обязательна; объект- объектные ссылки можно извлекать с помощью вложенного запроса к представлению 1mages_v, а не к таблице ключевых слов. В большинстве случаев функция MAKE_REF выполняется быстрее поиска в объектном представлении. Кроме того, возмож- возможность произвести такой поиск может у вас вообще отсутствовать. Как бы там ни было, но на этом этапе можно выполнять такие лаконичные за- запросы: SQL> SELECT DEREF(VALUEA)).pr1nt() I FROM keywords v v, TABLE(v.1mage_ret"s) 1 3 WHERE keyword - 'SIXTIES'; DEREF(VALUE(I)).PRINTO Id-100001; F1le-/f1les/weo/60s/sm11ey_face.g1f; Iceywords-HAPPY FACE, SIXTIES Id-100002; F1le-/flles/web/60s/oeace_syfflbol.g1T: keywords-DERRY RUBIN. PEACE SYMBOL, SIXTIES Иными словами, теперь можно вывести все изображения, помеченные ключе- ключевым словом SIXTIES, вместе со всеми их ключевыми словами и атрибутами! Триггеры INSTEAD OF Поскольку триггеры INSTEAD OF описывались в главе 18, в этом разделе принцип их действия не рассматривается. Мы обсудим только их применение для обнов- обновления объектных представлений. Если вы намерены освоить объектный подход, вас может заинтересовать, не являются ли триггеры INSTEAD OF просто реляцион- реляционным дополнением, позволяющим приложениям выполнять DML-операции. Рас- Рассмотрев аргументы «за» и «против», вы сможете решить, какой подход лучше вы- выбрать для своего приложения. Аргументы «против» Для инкапсуляции DML-операций гораздо лучше триггеров INSTEAD OF подходят пакеты и объектные методы PL/SQL. Логику такого триггера легко перенести в альтернативную конструкцию PL/SQL, имеющую более универсальное приме- применение. Иными словами, если в качестве средств выполнения DML в системе используются пакеты и методы, а механизм этот давно стандартизирован и нала- налажен, триггеры в него могут совершенно не вписываться и лишь усложнят вашу работу.
850 Глава 21 • Объектно-ориентированные возможности PL/SQL О Дублирование ОШ в нескольких представлениях. Если объектное представ- представление создано на основе объектной таблицы или представления и ОШ опре- определяется с помощью ключевого слова DEFAULT, представление содержит иден- идентификаторы объектов, соответствующие идентификаторам базовой структуры. Скорее всего, вам подойдет второй способ, поскольку отдельные представле- представления — это, по сути, сохраненные результаты запросов. Хранение в базе данных REF-ссылок Если вы разрабатываете приложение с «физическими» объектными таблицами, REF-ссылки на объекты могут храниться в других таблицах. Ведь REF — это двоич- двоичное значение, используемое Oracle в качестве указателя на объект. Однако если попытаться сохранить в реальной таблице виртуальную ссылку (то есть ссылку на строку объектного представления), Oracle сгенерирует ошибку. Поскольку виртуальная ссылка зависит от значений столбцов, вместо нее нужно сохранять данные значения. Это скорее неудобство, чем серьезное ограничение, но все же жаль, что нельзя совмещать объектные таблицы с объектными пред- представлениями или преобразовывать объектные представления в объектные табли- таблицы. Было бы хорошо иметь возможность создать объектную таблицу: CREATE TABLE images2 OF Image t NESTED TABLE keywords STORE AS teywordjtab; и затем заполнить ее данными из представления: INSERT INTO 1mages2 /* недопустимо, поскольку 1mages_v содержит REF */ SELECT VALUEd) FROM images_v 1: Но, увы, Oracle при этом генерирует ошибку ORA-22979: cannot INSERT object view REF oi" user-defined REF. Ссылки на неуникальные OID В данном случае мы не считаем, что при работе с объектными таблицами возмож- возможно существование ссылки на неуникальный OID, Вряд ли кто-нибудь знает, что произойдет в результате создания ссылки на объект объектного представления, содержащего несколько таких объектов с одинаковыми OID. Поэтому лучше не создавать представлений с дублирующимися OID. Наши эксперименты показали, что вызов функции OEREF для такой виртуаль- виртуальной ссылки возвращает объект, вероятно, первый найденный Oracle. В предыду- предыдущих версиях системы данная операция возвращала NULL. Поэтому не стоит рас- рассчитывать, что Oracle будет вести себя именно так, как вы предполагаете. Сопровождение объектных типов и объектных представлений Проработав некоторое время с объектными типами, вы освоите разные способы получения информации о создаваемых типах и представлениях. Когда информа- информации, которую предоставляет команда SQL'Plus DESCRIBE, станет недостаточно, мож- можно обратиться с запросами непосредственно к словарю данных Oracle.
Сопровождение объектных типов и объектных представлений 851 В словаре определяемый пользователем тип (объект или коллекция) называ- называется просто TYPE. Определение и тело любого объектного типа, а также специфи- спецификация и тело любого пакета находятся в представлении USER_SOURCE, DBAJ5OURCE или ALL_SOURCE. В табл. 21.4 приведены запросы, которые будут вам полезны. Таблица 21.4. Запросы к словарю данных для объектных типов Требуемая информация Запрос позволяющий получить ответ Какие тупы объектов и коллекции созданы пользователем? Какие объектные иерархии созданы пользователем? Какие атрибуты имеет тип foo? Какие методы имеет тип foo? Каковы параметры методов типа foo? Какой тип данных возвращает метод bar типа foo? Каков исходный код типа foo, в том числе все инструкции ALTER? Какие объектные таблицы имеют тип foo? Какие столбцы содержит объектная таблица foo_tab, в том числе скрытые? Какие столбцы имеют тип foo? Какие объекты базы данных зависят от типа foo? Какие объектные представления созданы и с какими OID? SELECT • FROM userjypes; SELECT • FROM user objects WHERE obJedLtype = TYPE'; SELECT RPADf ', 3*(LEVEL-1)) || type_name FROM userjypes WHERE typecode = 'OBJECT CONNECT BY PRIOR typejame » supertype_rwme; SELECT * FROM userJypejttrs WHERE type_name-TOO1; SELECT • FROM user_type_methods WHERE typejiame = 'FOO1; SELECT * FROM user_method_params WHERE type_name = 'FOO'; SELECT* FROM user_method_resuits WHERE typejiame = 'FOO' AND method_name - 'BAR'; SELECT text FROM user_source WHERE name = 'FOO' AND type = TYPE1 /* или TYPE BODY1 */ ORDER BY line; SELECT table_name FROM user.j>bject_tables WHERE tablejype = 'FOO1; SELECT columruname, datajype, hldden_ralumn, vlrtual_cnlumn FROM user_tab_cols WHERE table_name - TOO_TAB'; SELECT tabfe_name, co[umn_name FROM user_tab_columns WHERE datajype = 'FOO'; SELECT name, type FROM user_dependendes WHERE referenced_name = 'FOO'; SELECT ylew_name, vfewjype, oldjext FROM user_views WHERE typejext IS NOT NULL; Продолжением
848 Глава 21 • Объектно-ориентированные возможности PL/SQL Более того, даже Oracle предупреждает, что триггерами не стоит злоупотреб- злоупотреблять, поскольку они могут создавать сложные взаимозависимости. Если триггер INSTEAD OF выполняет DML-инструкцию для таблиц, у которых имеются другие триггеры, выполняющие DML-инструкции для других таблиц с триггерами, то как это все отладить? Аргументы «за» Значительную часть логики, обычно реализуемой в пакете или теле метода, мож- можно перенести в триггер INSTEAD OF. В сочетании с продуманным набором привиле- привилегий это позволит защитить данные лучше, чем с помощью методов и пакетов. Если вы пользуетесь таким клиентским средством, как Oracle Forms, то при создании в форме «блока» на основе представления, а не таблицы, триггеры по- позволяют применять гораздо больше встроенных возможностей этого продукта. При использовании OCI, когда объектное представление не подлежит моди- модификации и нужно быстро отправлять каптируемые данные объектного представ- представления обратно на сервер, триггеры INSTEAD OF просто необходимы. Главный вопрос Куда следует помещать инструкции, выполняющие вставку, обновление и удале- удаление данных, в особенности при использовании объектных представлений? Если вы хотите локализовать эти операции на сервере, то у вас имеются как минимум три варианта: пакеты PL/SQ.L, объектные методы и триггеры INSTEAD OF. В табл. 21.3 приведены сравнительные характеристики этих вариантов. За- Заметьте, что мы рассматриваем их исключительно с точки зрения размещения DML- операций над объектными представлениями. Таблица 21.3. Сравнительные характеристики технологий инкапсуляции DML-операций над объектными представлениями Характеристика Пакет PL/SQL Объектный метод Триггер INSTEAD OF Соответствие объектно-ориентиро- объектно-ориентированному подходу Возможность модификации при изменении схемы Степень риска неожиданных последствий Взаимодействие со стандартными функциями клиентских средств (в частности, Orade Developer} Возможность включаться и отключаться по желанию Потенциально очень хорошее Имеется, код легко модифицировать и независимо перекомпилировать Низкая Программист должен добавить код для всех клиентских транзакционных триггеров Отсутствует Прекрасное Имеется (в Orade9l) Имеется Потенциально очень хорошее Низкая Программист должен добавить код для всех клиентских транзакционных триггеров Отсутствует Высокая; триггеры могут иметь скрытые зависимости Хорошо реализовано для типов верхнего уровня (однако не существует серверного триггера INSTEAD OF LOCK) Имеется (путем отключения и включения триггера)
Объектные представления 849 Как видите, явного победителя здесь нет. В каждом варианте имеются пре- преимущества, которые в конкретном приложении могут сыграть решающую роль. Важной особенностью использования триггеров INSTEAD OF в иерархии пред- представлений является необходимость создания отдельного триггера для каждого уровня иерархии. При выполнении DML-инструкции применительно к подпред- ставлению запускается триггер подпредставлекия, а применительно к суперпред- суперпредставлению — триггер суперпредставления. Триггеры INSTEAD OF могут использоваться в сочетании с пакетами PL/SQL и/или объектными методами для обеспечения уровней инкапсуляции. Например: CREATE OR REPLACE TRIGGER images v_insert INSTEAD OF INSERT ON Tmagesv FOR EACH ROW BEGIN /* Вызов процедуры из пакета для выполенния вставки */ manage_1 mage. create_one [: NEW. 1 magej d. : NEW. fi 1 e_type. :NEW.file_name. :NEW.bytes. :NEW.keywords):, ENO; В идеале разработчики должны заранее выбирать архитектуру и конструктор- конструкторские подходы, а не напичкивать приложение всевозможными функциями Oracle. Используйте любую функцию лишь при условии, что она действительно полезна в приложении и согласуется с принятыми общими концепциями разработки. Мы полностью поддерживаем мнение Oracle, что к триггерам должен применяться взвешенный подход. Различие между объектными представлениями и объектными таблицами Помимо очевидных различий между объектным представлением и объектной таб- таблицей программисты PL/SQL должны понимать и более «тонкие» различия. Ос- Основными из них являются: О уникальность OID-идентификаторов; О возможность хранения в базе данных REF-ссылок; О наличие REF-ссылки на неуникальные ОШ-идентификаторы. Далее мы рассмотрим каждое из перечисленных различий. Уникальность OID-идентификаторов Объектная таблица всегда содержит уникальный идентификатор объекта (сис- (системно-генерируемый или производный от первичного ключа). Можно, хотя это и плохая практика, создать объектную таблицу с дублирующимися строками, но экземпляры объектов в таблице все равно будут иметь уникальные идентифика- идентификаторы. Данную операцию можно выполнить двумя способами. О Дублирование ОШ в Одном представлении. Объектное представление может содержать несколько экземпляров объектов (строк) с одним и тем же ОШ. Вы уже видели пример суперпредставления с дубликатами объектов.
852 Глава 21 • Объектно-ориентированные возможности PL/SQL Таблица 21.4 (продолжение) Требуемая информация Запрос, позволяющий получить ответ Какова иерархия созданных CREATE TABLE uvtemp AS пользователем представлений? SELECT v.vlew_name, v.vlew_type, (Требует временной таблицы, v.superview.name, vl.viewjype supervlewjype поскольку в подзапросе нельзя FROM user_views v, user_views vl использовать предложение WHERE v.supervlew_name = vl.v!ew_name (+); CONNECT BY) SELECT RPAD('', 3*(LEVEL-1)) 11 view_name ||4'l|view_type!ir FROM uvtemp CONNECT BY PRIOR vlewjype = supervlewjype; DROP TABLE uvtemp; На основе какого запроса определено SET LONG 1000 - или больше представление foo_v? SELECT text FROM user_vlews WHERE vlew_name = 'FOO_V; Какие столбцы содержит SELECT column_name, data_type_mod, data_type представление foo_v? FROM user_tab_columns WHERE table_name - 'FOO_V; Учтите, что объектные таблицы невидимы для представления USERTABLES. Их список можно получить из представлений USER_OBJECTJABLES и USER_ALL_TABLES. Привилегии Существует целый ряд системных привилегий, ассоциированных с объектными типами. О CREATE [ANY] TYPE. Создание, изменение или удаление объектных типов и кода типов. ANY означает «в любой схеме». О CREATE [ANY] VIEW. Создание и удаление представлений, в том числе и объект- объектных. ANY означает «в любой схеме». О ALTER ANY TYPE. Выполнение инструкции ALTER TYPE в любой схеме. О EXECUTE ANY TYPE. Использование объектного типа из любой схемы для создания экземпляров, выполнения методов, ссылок и получения объектов по ссылкам. О UNDER ANY TYPE. Создание подтипа в одной схеме, наследующего тип из другой схемы. О UNDER ANY VIEW. Создание подпредставления в одной схеме, наследующего пред- представление из другой схемы. На уровне объекта для объектных типов используются три вида привилегий — EXECUTE, UNDER и DEBUG. Кроме того, важно понимать, как применяются к объектным таблицам и представлениям традиционные DML-привилегии. Привилегия EXECUTE Если вы хотите разрешить пользователю Joe использовать ваши типы в его про- программах и таблицах PL/SQL, предоставьте ему привилегию EXECUTE: GRANT EXECUTE on catalogjtemj TO joe;
Сопровождение объектных типов и объектных представлений 853 В том случае, если пользователь Joe имеет привилегию, необходимую для соз- создания синонимов, и работает с версией Oracle9i Release 2 или выше, он может создать синоним: CREATE SYNONYM cata)og_1tem_t FOR scott.catalog_1tem_t; и использовать его следующим образом: CREATE TABLE catalogjtems OF catalog_item_t: или же так: DECLARE anjtem catalogjtemj;: Кроме того, пользователь Joe имеет возможность применять уточненные ссыл- ссылки на тип scott.catalogjtemj;. Если вы ссылаетесь на объектный тип в хранимой программе и предоставили другому пользователю или роли привилегию EXECUTE на эту программу, такая же привилегия на тип не обязательна, даже когда программа определена как выпол- выполняющаяся с привилегиями вызывающего (см. главу 20). В том случае, когда у поль- пользователя есть DML-привилегия на представление, имеющее для данной DML- операции триггер INSTEAD OF, который ссылается на объектный тип, пользователю не нужна явная привилегия EXECUTE на этот тип, поскольку триггеры всегда вы- выполняются с привилегиями создателя. Однако привилегия EXECUTE на объектный тип необходима пользователям, выполняющим анонимные блоки, в которых он используется. Привилегия UNDER Привилегия UNDER дает право на создание подтипа. Предоставляется она следую- следующим образом: GRAW7 UNDER ON imagej TO scott: Для того чтобы владелец схемы мог создать подтип, супертип должен быть оп- определен с привилегиями вызывающего (AUTHID CURRENTUSER). Кроме того, привилегия UNDER дает право на создание подпредставления: GRANT UNDER ON images_v TO scott; Привилегия DEBUG Если пользователь с помощью отладчика PL/SQL анализирует код, в котором применяется созданный вами тип, можно предоставить ему привилегию DEBUG: GRANT DEBUG ON image_t TO joe; Это даст возможность пользователю исследовать имеющиеся внутри типа пе- переменные и устанавливать точки останова внутри методов. Привилегия DEBUG применима и к объектным представлениям — для них она позволяет отлаживать исходный код триггеров INSTEAD OF. Привилегии DML Для объектных таблиц имеют значение традиционные привилегии SELECT, INSERT, UDPATE и DELETE. Пользователь, который имеет на такую таблицу только привиле- привилегию SELECT, может извлекать из нее столбцы базового типа, лежащего в основе оп- определения таблицы, но не может извлекать объекты. Это означает, что операции
854 Глава 21 • Объектно-ориентированные возможности PC/SQL VALUE, TREAT, REF и DEREF для него недоступны. Остальные DML-привилегии, такие как INSERT, UPDATE и DELETE, также применимы только к реляционной интерпрета- интерпретации таблицы. Тот, кто получает привилегию, может не иметь права на использование конст- конструктора или других методов объектного типа, если владелец объектного типа не предоставил ему на этот тип привилегию EXECUTE. Кроме того, ему будут не видны столбцы, определенные в подтипах. О целесообразности применения объектно-ориентированного подхода Автор должен признаться, что начал программировать задолго до того, как при разработке приложений стали применяться объектные подходы. С годами стало ясно, что ни один стиль программирования не имеет монопо- монополии на такие фундаментальные и важные для нас моменты, как соответствие тре- требованиям, эффективность и производительность, удобство разработки и надеж- надежность системы. Помнится множество восторгов, преувеличенных ожиданий, мод- модных тенденций, горячего энтузиазма (к чему автор сам был причастен) — всего этого в свое время было вдоволь и в отношении объектно-ориентированного про- программирования. Нельзя сказать, что данная технология не помогает решать опре- определенные проблемы, но это не волшебная палочка, как думают многие. Возьмем, к примеру, принцип объектной декомпозиции, используемый при разработке объектных иерархий. При внимательном выполнении моделирования объектов реального мира получаются удобные и понятные программные компо- компоненты, из которых легко собирать даже крупномасштабные системы. Звучит мно- многообещающе, не так ли? Однако существует множество способов декомпозиции объектов реального мира1, и эти объекты редко можно представить в виде про- простой иерархии. Например, декомпозицию нашего библиотечного каталога можно выполнить на основе... ну, скажем, носителей (печатные материалы, аудиозаписи, цифровой формат и т. д.). Хотя Oracle предоставляет прекрасные возможности для эволюции типов, серьезные изменения в иерархии типов ведут к таким тяже- тяжелым последствиям, что никто и никогда не станет их вносить. И не программные средства тому виной. Нельзя сказать, что объединение программной логики (методов) и данных (атрибутов) в абстрактных типах дает какие-либо явные преимущества. Пока ни- никто еще убедительно не доказал, что это лучше, чем хранить структуры данных (логическую и физическую структуру таблиц) отдельно от процессов (процедур, функций и пакетов). И многие признают, что структуры данных реальных компа- компаний меняются гораздо реже, чем алгоритмы работы с этими данными. Даже в от- отношении ООП является несомненным тот факт, что изменчивые элементы систе- системы должны быть отделены от более стабильных. Рекомендуем, в частности, обратиться к материалам Object Oriented Programming Oversold/, пред- представленным по адресу http://www.geocltles.a)m/tzbl&er/ocpt»d.htm.
О целесообразности применения объектно-ориентированного подхода 855 Последнее обстоятельство особенно интересно. Ярые приверженцы объектно- объектного подхода, которые настаивают на объединении данных и действий, в то же вре- время подчеркивают важность подхода «модель-предсгавление-управление», «отде- «отделяющего бизнес-логику от данных». Вам не кажется, что это совершенно проти- противоположные утверждения? Многие сторонники ООП главным преимуществом данного подхода считают возможность многократного использования программного обеспечения. Это го- говорилось столько раз, что должно было бы уже стать правдой! К сожалению, мало кто потрудился привести убедительные доказательства, так как не существует четко определенного понятия «многократного использования». Даже привержен- приверженцы объектного подхода признают многократное использование возможным в пер- первую очередь для компонентов высших уровней, поскольку объекты обычно пред- предназначаются для узкого применения. Исходя из личного опыта можем сказать: возможность повторного использования объектного кода ничуть не выше, чем у обычных хорошо спроектированных подпрограмм. Безусловно, можно применить объектный подход в PL/SQL и разработать ком- • поненты для многократного использования. Например, Дон Бейлз, имеющий ог- огромный опыт объектно-ориентированного программирования, в течение десяти лет использовал пакеты PL/SQL как «типы». Он утверждает, что может взять це- целый пакет (и все сопутствующие таблицы) и перенести его в новый проект без единого изменения. Недостающим элементом большинства объектных подходов он считал точную модель человека, выполняющего приложения (пользователя), моделируемого Доном в виде объекта, поведение которого реализовано в реаль- реальной программе. Независимо от метода разработки важнейшими составляющими ее успеха ос- остаются опыт в решении подобных задач, возможность нанять высококвалифици- высококвалифицированных специалистов и наличие достаточного времени для проектирования. А уж выбор подхода, объектного или нет важен только в том отношении, что он должен быть сделан заранее, с тем чтобы предотвратить стихийное и неупорядо- неупорядоченное развитие системы. В завершение будет приведено несколько рекомендаций, касающихся приме- применения объектных функций Oracle. О Если вы используете Oracle Call Interface (OCI), то возможность выполнить клиентское кэширование и комплексную выборку объектов могут склонить вас к интенсивному использованию объектных средств Oracle. Автор не рабо- работает с OCI и поэтому не может поделиться своим опытом в этом отношении. О В случае, когда на вашем предприятии уже используется объектно-ориенти- объектно-ориентированное программирование, объектные средства Oracle могут облегчить ин- интеграцию технологий баз данных в имеющиеся системы. О Если вы не намерены пользоваться объектным подходом, не отвергайте вме- вместе с ним и коллекции. Помните, что они обладают широчайшими возможно- возможностями и что для их применения не требуются ни объектные типы, ни объект- объектные представления. О Если вы никогда раньше не практиковали объектно-ориентированного про- программирования, новые объектные возможности Oracle могут показаться вам
858 Глава 22 * Взаимодействие Java и PL/SQL Использование Java в базах данных Oracle и программирование на этом язы- языке - обширнейшие темы, подробное освещение которых выходит за рамки нашей книги. Приступая же к работе над настоящей главой, авторы поставили перед со- собой две основные задачи: О предоставить читателю информацию, необходимую для загрузки классов Java в базу данных Oracle и управления новыми объектами базы данных, а также для их объявления с целью дальнейшего использования в PL/SQL; О изложить основные требования и рекомендации по разработке классов Java, достаточные для создания собственных классов, без которых невозможен дос- доступ к функциональным возможностям, предоставляемым Java. Для того чтобы вызвать методы классов Java из Oracle, необходимо выпол- выполнить действия, перечисленные ниже. 1. Создать элементы кода Java в Oracle JDeveloper или любой другой интегриро- интегрированной среде разработки приложений Java. (Более того, это можно сделать в лю- любом текстовом редакторе!) 2. Загрузить классы Java в Oracle с помощью утилиты командной строки load- java или инструкции CREATE JAVA. 3. Предоставить доступ к методам классов Java в PL/SQL путем написания на PL/SQL программ-оболочек для Java-кода. 4. Назначить необходимые атрибуты доступа программам-оболочкам PL/SQL и использующимся в них Java-классам. В результате полученные программы PL/SQ.L можно будет вызывать из са- самых разных сред (рис. 22.1). Для работы с Java программистам предоставляется ряд средств, перечислен- перечисленных в табл. 22.1. Таблица 22.1. Используемые в Oracle компоненты и команды для работы с Зама Название Описания Aurora JVM Виртуальная машина Java, реализованная в составе сервера базы данных Oracle loadjava Утилита операционной системы с интерфейсом командной строки, загружающая элементы кода Java (классы, файлы jar и т, п.) в базу данных Oracle dropjava Утилита операционной системы с интерфейсом командной строки, удаляющая элементы кода Java (классы, файлы .Jar и т. п.) из базы данных Oracle CREATE JAVA Новые DDL-инструкции, выполняющие те же функции, что и утилиты DROP JAVA loadjava, dropjava ALTER JAVA DBMS JAVA . Встроенный пакет, содержащий набор утилит для установки параметров виртуальной машины Java JpubHsher Утилита, используемая для построения Java-классов на основе объектных типов и ftEF, определенных в базе данных Oracle
Подготовка к использованию Java в Oracle 859 (JSP) Аашит i ki прилети™» Jni, < а «агаром ислалиуггся' JDBCwimSQLJ ' Orldi DmlsfXf КлмнтОС) илк Pm*c Рис 22.1. Доступ к JSP из базы данных Oracle Подготовка к использованию Java в Oracle Прежде чем вызывать методы Java из программ PL/SQL, нужно выполнить дей- действия, перечисленные ниже: О установить пакет Java Development Kit (JDK™); предварительно ознакомив- ознакомившись с последней информацией о поддержке версий, которая содержится в документация по Oracle; О определить классы и элементы кода Java, а затем откомпилировать их, создав файлы .class и .jar; О задать привилегии для схемы Oracle. Установка Java Вы можете установить пакет Java Development Kit (JDK) версии 1.1.5 или выше, загрузив его с web-узла Javasoft: http://vAM.javasoft.com/prodLicts/1ndex.htnil Начиная с OracleBi этот пакет входит и в состав Oracle. Обязательно задайте переменную окружения CLASSPATH, и компилятор Java (javac) сможет найти ссылки
856 Глава 21 • Объектно-ориентированные возможности PL/SQL сложными. Однако советуем потратить время на их освоение. В частности, по- попробуйте создать объектные представления для существующих систем. О Не стоит отвергать объектные типы и представления из-за их низкой произво- производительности. Oracle постоянно работает над ее повышением. Кроме того, если вы проведете тестирование, может оказаться, что ООП вполне удовлетворяет требованиям приложения. О Оказывается, что объектные типы можно используются и в некоторых встроен- встроенных функциях Oracle, напрямую не связанных с объектно-ориентированным программированием, в частности в Oracle9j Advanced Queuing и Oracle Spa- Spatial. Для использования этих функций нужно иметь хотя бы общее представ- представление об объектных типах, даже если вы сами создавать их не собираетесь.
22 Взаимодействие Java и PL/SQL > Oracle и Java > Использование Java в Oracle > Утилиты loadjava и dropjava ^ Управление объектами Java в базе данных > Пакет DBMS JAVA ^ Как сделать методы Java доступными в PL/SQL В этой главе рассматривается новая замечательная возможность Oracle, которая была введена в Огас1е8г, а именно вызов из PL/SQL хранимых Java-процедур (Java Stored Prosedure, JSP). Как вы наверняка знаете, Java — исключительно мощный и во многих отношениях более надежный язык, чем PL/SQL. Он вклю- включает сотни классов с четкими и простыми API, используемыми для самых разно- разнообразных целей. Oracle и Java Начиная с OracleBi в состав РСУБД включается продукт под названием JServer, состоящий из следующих компонентов: О виртуальная машина Java для Oracle (Java Virtual Machine, JVM), называемая Aurora, а также соответствующие ей среда выполнения и библиотеки классов Java; О средства интеграции Java с PL/SQL и РСУБД Oracle; О брокер объектных запросов (Object Request Broker, или Aurora/ORB) и тех- технология Enterprise JavaBeans (EJB); О встроенный компилятор JServer Accelerator. Виртуальная машина Java Aurora вызывает методы Java, которые также назы- называются хранимыми Java-процедурами (Java Stored Procedure, JSP), и классы так, как если бы они хранились в базе данных.
860 Глава 22 • Взаимодействие Java и PL/SQL на ваши классы и на классы Oracle. Дополнительная информация о переменной CLASSPATH содержится в документации по языку Java, которую можно получить, обратившись по адресу http://java.sun.com. Проектирование и компиляция кода Java Для многих программистов, не обладающих навыками работы с объектно-ориен- объектно-ориентированными языками, переход к Java может оказаться далеко не простым делом. Поэтому очень уместными здесь будут следующие выводы, основанные на лич- личном опыте автора настоящей главы: О освоение базового синтаксиса языка, достаточного для создания простых клас- классов Java, требует сравнительно немного времени; О использование Java в PL/SQL не сопряжено с преодолением каких-то особых трудностей; О создание объектно-ориентированных приложений на базе Java требует от раз- разработчика PL/SQL полного переосмысления привычных подходов. Ознакомившись с этой главой, вы поймете, как вызываются методы Java из PL/SQL, и научитесь создавать простейшие классы Java. Определение привилегий, необходимых для разработки и выполнения Java-кода Поскольку функции системы безопасности, связанные с разработкой и выполне- выполнением Java-кода, в Oracle8i и Oracle9t реализованы по-разному, мы рассмотрим ка- каждую из версий. Защита данных в OracleSi В РСУБД Oracle8i определены две роли, предоставляющие привилегии на вы- выполнение разных видов операций: О JAVAUSERPRIV - привилегии на выполнение относительно небольшого количе- количества операций, в том числе на просмотр свойств; О JAVASYSPRIV — привилегии на выполнение большей части операций, включая обновление пакетов, защищенных JVM. Эти роли назначаются таким же образом, как любые другие роли базы данных. Для того чтобы пользователю Scott получить, скажем, разрешение на любые дей- действий относительно Java, ему нужно подключиться к Oracle с учетной записью SYSDBA и выполнить такую инструкцию: GRANT JAVASYSPRIV TO SCOTT: При необходимости предоставить этому же пользователю ограниченные пра- права на работу в Java, следует ввести инструкцию GRANT JAVAUSERPRIV TO SCOTT: Например, чтобы получить возможность создать файл с помощью Java, нужно быть членом роли JAVASYSPRIV, а для чтения или записи содержимого этого файла достаточно быть членом роли JAVAUSERPRIV. За дополнительными сведениями по
Подготовка к использованию Java в Oracle 861 вопросу управления разрешениями обращайтесь к документации Oracle, где, в частности, приведена таблица методов Java и указано, какие привилегии для вызова каждого из них необходимо иметь. В процессе инициализации JVM Aurora устанавливает экземпляр менеджера защиты Java (Java Security Manager) — Java. 1 ang. SecurityManager. Каждый пользо- пользователь Oracle имеет динамический идентификатор, определяющий права, или при- привилегии, владельца сеанса при доступе к методам Java из PL/SQL. Если пользователь, не имеющий достаточных полномочий, пытается выпол- выполнить запрещенную операцию, JVM инициирует исключение Java. I ang. Securi- tyException. Вот что увидит пользователь в таком случае в SQL*Plus: ORA-29532: Java call terminated by uncaught Java exception: Java.1ang.SecurityException При вызове методов Java в базе данных возможно возникновение различных проблем, связанных с системой безопасности, особенно при взаимодействии с сер- серверной файловой системой или другими ресурсами операционной системы. Кон- Контролируя операции ввода-вывода, Oracle следует определенным правилам. О Если пользователю, имеющему персональный идентификатор прав доступа, были предоставлены привилегии JAVASYSPRIV, Security Manager разрешает вы- выполнить операцию. О Если же такому пользователю были предоставлены привилегии JAVAUSERPRIV, Security Manager проверяет допустимость операции в соответствии с теми пра- правилами, которые действуют в отношении пакета PL/SQL LCTL_FILE. Иными сло- словами, файл должен находиться в каталоге, определяемом параметром UTLFI- LEDIR, который задается в инициализационном файле базы данных. Защита данных в Oracle9i Система защиты, реализованная в Огас1е9г для JVM, основана на модели защиты Java 2, согласно которой привилегии предоставляются каждому из существую- существующих классов. Подробный анализ этой сложной системы выходит за рамки данной книги. Однако мы приведем несколько примеров, чтобы вы могли составить пред- представление о программном коде, который можно писать в Oracle9i для предостав- предоставления и отмены привилегий на выполнение определенных действий над объекта- объектами базы данных. В общем случае для предоставления привилегий используется процедура DBMS_ JAVA. GRANT_PERMISSION. Пример ее вызова для предоставления схеме BATCH разреше- разрешения на чтение и запись файла lastorder. log выглядит следующим образом: CALL DBMS JAVA.GRANT_PERMISSIQN( 'BATCH7, 'Java.io.FilePermission1, Vapps/OE/lastorder.log', 'read.write'); Приведем последовательность операторов, которая сначала предоставляет раз- разрешения на доступ к файлам в каталоге, а затем ограничивает их возможностью выполнения операций над заданным файлом: CONNECT 0E_adm1n/OE_admin REM Grant permission to all users (PUBLIC) to be REM able to read and write all files in /tmp. ¦
862 Глава 22 • Взаимодействие Java и PL/SQL CALL D8MS_JAVA.GRANT_PERMISSrONC'PUBLIC, 'Java.1o.F1lePerwls5ion'. 7tmp/*', 'read,write1): REM Limit permission to all users [PUBLIC) from reading REM or writing the password file m /tmp. CALL DBMS JAVA.GRANT_PERMISSION('PUBLIC, 'Java,1o,F1lePerm1ss1on', Vtmp/password', 'read.write'); REM By providing a more specific rule that overrides the REM limitation. OE admin can read and write /tup/password. CALL D8MS_JAVA.GRAm_PERMISSI0N( 'OEjdmirT, 'Jave.io.FilePernvtssion', Vtmp/password'. 'read.write'); COMMIT; Примеры использования Java Прежде чем мы приступим к изучению особенностей системы защиты, рассмот- рассмотрим последовательность действий, которые следует выполнить для получения доступа к Java из PL/SQL. Ниже будет рассказано о нескольких интересных прие- приемах, применяемых для реализации этой задачи. Предположим, что при выполнении кода PL/SQL требуется удалить файл опе- операционной системы. Раньше это можно было сделать следующим образом: О в Oracle 7.3 и более поздних версиях в канал базы данных направлялось спе- специальное сообщение, прочитав которое программа-слушатель, написанная на языке С, выполняла указанную задачу; О в Oracle 8.0 появилась возможность объявлять библиотеку, содержащую ссыл- ссылку на DLL или общедоступную библиотеку языка С, а затем из кода PL/SQL вызывать хранящуюся в этой библиотеке программу, которая и производила удаление файла. Способ с использованием канала довольно удобен, но изяществом он, как вы понимаете, не отличается. Более удачным является решение, основанное на вызове внешней процедуры, но если вы не знаете языка С, реализовать его не так просто. Наилучшим оказался способ, в основу которого положен принцип использова- использования классов Java. Для его выполнения необходимо знать язык Java, но не в таком объеме, какой требуется для написания эквивалентного кода на С. Java реализу- реализуется с набором готовых (базовых) классов, предоставляющих широкий набор функ- функций, включая и файловый ввод-вывод. Вот какие действия необходимо выполнить для удаления файла операцион- операционной системы в данном случае: 1. Идентифицировать используемые функциональные элементы Java. 2. Самостоятельно создать класс и таким образом обеспечить доступ из PL/SQL к функциональным элементам Java.
Примеры использования Java 863 3. Откомпилировать класс и загрузить его в базу данных. 4. Написать программу PL/SQL для вызова методов созданного класса. 5. Удалить требуемые файлы, вызвав соответствующие методы класса из PL/S QL ПРИМЕЧАНИЕ- OradeSI Release 2 содержит расширенную версию встроенного пакета UTU.FILE, позволяющую уде- уделять файлы с помощью программы UTL.RLE. FREMOVE. Выбор функциональных элементов Пакет Java. 1о содержит класс File, включающий набор методов для получения информации о файлах и каталогах. Причем он не только позволяет получать ука- указанную информацию, но и удалять, а также переименовывать файлы, создавать каталоги и т. д. Приведем часть интерфейса, предоставляемого классом File: public class java.1o.F1le ( public boolean deletet); public boolean mkdir 0: } Иными словами, для удаления файла нужно вызвать функцию типа Boolean. Если файл будет удален, эта функция возвратит значение TRUE, а в противном случае - значение FALSE. Создание пользовательского класса Java Вы спрашиваете, для чего создается собственный Java-класс поверх класса Fi I e и почему нельзя вызвать нужную функцию прямо из оболочки PL/SQL? На то есть две причины. О Во-первых, методы класса Java всегда (за исключением статических методов) выполняются для конкретного объекта, являющегося экземпляром этого клас- класса. В PL/SQL создавать экземпляры классов Java нельзя, поэтому невозможно и вызывать такие методы. О Во-вторых, хотя и Java и PL/SQL включают типы данных Boolean (a Java даже предлагает примитив Boolean и класс Boolean), эти типы не соответствуют друг другу. Поэтому значение типа Bool ean Java нельзя непосредственно присвоить переменной Boolean PL/SQL. Следовательно, нам придется разработать собственный класс, который: О создаст экземпляр класса File; О выполнит для этого класса метод delete; О возвратит значение, которое PL/SQL сможет корректно интерпретировать. Ниже приведен пример очень простого класса, позволяющего использовать метод File.delete: /* Файл в web: JDelete.java */ Import 3ava.1o.F1le; public class JDelete { ,
864 Глава 22 ¦ Взаимодействие Java и PL/SQL public static 1nt delete (String fileName) { File myFile - new File {fileName); boolean retval - myFiie.deleteO; 1f (retval) return 1: else return 0: Все этапы выполнения кода отражены на рис. 22.2. Принципы, положенные в основу его реализации, очень просты. Метод JDelete,delete создает объект File для указанного имени файла. Объявление этого метода статическим избавит нас от необходимости создавать экземпляр класса JDelete, что принципиально важно для вызова метода JDelete.delete из PL/SQL. Статический метод ассоциирован с классом, а не с конкретным объектом данного класса. Объявляем общедоступный класс Статическая функция delete (для ее вызова не нужен объект) возвращает целочисленное значение Создаем объект-зкзеиштр класса и вызываем метод delete, возвращающий значение типа Boolean Все не используемые по умолчанию классы нужно импортировать явно import Java.io.File: public class JDelete { public static int delete (String fileName) { File myFile - new File (fileName): boolean retval - myFi1e.delete О: if (retval) return 1: else return 0: К Преобразуем тип данных Boolean в INTEGER: значение TRUE заменяет 1, а значение FALSE— 0 Рис. 22.2. Класс Java, используемый для удаления файла Все очень просто! Правда, на рисунке не отражены усилия, затраченные авто- автором на отладку кода. Вы сумеете их оценить, когда попытаетесь сделать что-ни- что-нибудь подобное. Этот класс демонстрирует множество различий между Java и PL/SQL, а именно: О блоки, циклы и условные операторы в Java ограничиваются не ключевыми словами BEGIN и END, а фигурными скобками (начало блока отмечает откры- открывающая фигурная скобка, а конец — закрывающая); О код Java чувствителен к регистру клавиатуры, поэтому ключевое слово if в нем — это вовсе не то же самое, что IF; О оператор присваивания обозначается обычным знаком равенства (=), а не со- составным символом (:¦=), как в PL/SQL;
Примеры использования Java 865 О в вызове метода без аргументов (например, метода delete класса File) после его имени должны стоять открывающая и закрывающая скобки. Иначе компи- компилятор Java попытается интерпретировать метод как член класса или как струк- структуру данных. Компиляция и загрузка класса в Oracle Теперь, когда код класса готов, наступило время компиляции. Открываем в Win- Windows сеанс MS-DOS, переходим в каталог d:\Java (или в любой другой, где уста- установлен пакет Sun JDK) и компилируем класс; D:\Java> Javac JDelete.Java Вообще-то, не лишним было бы протестировать его до того, как помещать в Oracle и вызывать из PL/SQL. Разработку и тестирование удобнее выполнять пошагово, тем более что Java предоставляет для этого эффективное средство — метод main. Если класс содержит метод (процедуру) типа void с именем main и за- заданным набором параметров, можно вызвать указанный класс — и код будет вы- выполнен. ПРИМЕЧАНИЕ- Код метода main может служить одним из примеров тога, как ]eva интерпретирует определенные элементы со стандартной сигнатурой. Еще одним таким примером является метод toString. Если класс содержит данный метод, он автоматически вызывается для вывода пользовательского описа- описания объекта. Это особенно полезно в тех случаях, когда объект состоит из множества элементов, имеющих смысл лишь при определенном представлении или требующих форматирования, без кото- которого они не читабельны. Добавим в класс JDel ete простой метод mai n (его код выделен жирным шрифтом): public class JOelete { public static int delete ... public static void rain (String args[]) { Systen.out.prtntln ( delete (args[o]> ) Метод main вызывает метод delete, передавая ему в качестве аргумента первое из переданных классу значений, и выводит на экран результат. Перекомпилиру- Перекомпилируем класс и запустим его (все операции выполняются в окне MS-DOS): D:\Jdva> javac JDelete.Java D:\Java> Java JDelete c:\tenp\te employee.pks 1 D:\Java> Java JOelete c:\temp\te enployee.pfcs 0 Обратите внимание: при первом запуске метод возвращает единицу (TRUE), свидетельствующую о том, что файл удален. А когда мы запускаем его повторно,
866 Глава 22 * Взаимодействие Java и PL/SQL он возвращает в качестве значения нуль, потому как удалять больше нечего. Все очень просто. ПРИМЕЧАНИЕ- — ~——" — ~ ¦— Еще одним примером превосходства Java над PL/SQL является тот факт, irro для вывода строки в PL/SQL нужно ввести с клавиатуры команду длиной в 20 символов (DBMSJ3UTPUT.PUT_L1NE), тогда как в Java для этой цели достаточно 18 символов (System.out.prinan). А кроме того, если при объяв- объявлении класса ввести строку private static final Print-Stream о = System.out;, то для вывода данных из него достаточно будет команды o.prlntin — всего 9 символов! Теперь, когда класс откомпилирован и проверен и мы точно знаем, что метод delete работает, с помощью команды loadjava класс можно загрузить в схему SCOTT базы данных Oracle: D:\Java> loadjava -user scott/tiger -oci8 -resolve JDeiete.class Можно даже проверить, успешно ли загружен класс. Для этого нужно обра- обратиться к представлению USER_OBJECTS из словаря данных, воспользовавшись спе- специальной утилитой (мы познакомим вас с ней чуть позже): SQL> exec nyjava.showobjects Object Name Object Type Status Timestamp Hello JOelete JFile2 JFile3 JAVA JAVA JAVA JAVA CLAS'S CLASS CLASS CLASS VALID VALID VALID VALID 1999-05-19:16:42 - 1999-06-07:13:20 1999-05-26:17:07 1999-05-27:12:53 На этом работа с Java завершается, и мы возвращаемся в уютный мир PL/SQL. Создание оболочки PL/SQL Чтобы облегчить пользователям процесс удаления файлов из PL/SQL, попыта- попытаемся создать PL/SQL-оболочку для метода Java, которая внешне очень похожа на обычную функцию PL/SQL (но на самом деле она является переходником между PL/SQL и Java): /* Файл в web: fDelete.sf */ CREATE OR REPLACE FUNCTION fDelete ( file IN VARCHAR2) RETURN NUMBER AS LANGUAGE JAVA NAME 'JDelete.delete ( java.lang.String) return 1nt': Реализация функции fDelete представляет собой единственную строку, опи- описывающую вызов метода Java. Список ее параметров должен соответствовать спи- списку параметров метода, но вместо параметров должны указываться полные имена типов данных. В нашем случае это означает, что нельзя просто написать String, а нужно указать полное имя пакета, содержащего класс Stri ng. Обратите внима- внимание на предложение RETURN. Заданное в нем имя int идентифицирует не класс, а примитивный тип данных, так что это полная спецификация.
Утилита loadjava 867 Удаление файлов из PL/SQL Итак, мы должны скомпилировать и вызвать функцию: SQL> Pfdelete.if Function created. Input truncated to 12 characters SQL> exec DBHS_OUTPUT.PLT_LINE ( fdel eteT' c: \temp\te_enipl oyee .pkb' J) 1 SQL> exec DBHS_OUTPuT.PUT_UNE ( fdelete('с:\temp\te_erap1oyee.pkb')) 0 Теперь на основе этой функции можно создавать всевозможные утилиты, на- например процедуру, удаляющую все файлы, которые указаны во вложенной табли- таблице, или процедуру, удаляющую из заданного каталога все файлы, которые удов- удовлетворяют заданному условию фильтрации (например, *.tmp). На практике вы, конечно же, создали бы пакет и поместили бы в него все эти функции и процедуры. Мы это тоже сделаем, но немного позже. А сейчас давайте рассмотрим каждое из только что выполненных действий подробнее. Утилита loadjava Утилита loadjava — это программа операционной системы с интерфейсом команд- командной строки, загружающая в базу данных Java-файлы. При первом запуске в схеме она создает несколько дополнительных элементов, необходимых для нормальной работы Java. О CREATE$JAVA$LO8JTABLE — таблица, которая создается в каждой схеме и содержит элементы кода Java. При загрузке с помощью утилиты loadjava каждого нового класса в эту таблицу добавляется новая строка. О JAVA$CLASS$MD5$TABLE— хэш-таблица, которая также называется дайджест-таб- дайджест-таблицей и используется для отслеживания процесса загрузки элементов Java в указанную схему базы данных. О LQADLOBS — пакет, устанавливаемый в каждой схеме и используемый для за- загрузки элементов кода Java в базу данных в виде больших объектов (LOB). Утилита loadjava с помощью пакета LOADLO8S помещает Java-файлы в BLOB-сгол- бец таблицы базы данных CREATE$JAVA$L08JTABLE. Кроме того, она проверяет хэШ- значение таблицы JAVA$CLASS$MD5$TABLE. MD51, с тем чтобы определить, загружен ли указанный класс в базу данных и был ли он изменен. Данный прием позволяет MD5 — это алгоритм формирования профиля сообщения (message digest) от компании RSA Data Security. Более подробную информацию о нем вы найдете по адресу http:/www. Columbia, edu/~ai1el/ ssleay/rfd 321.html.
868 Глава 22 • Взаимодействие Java и PL/SQL избежать повторной загрузки и перекомпиляции зависимых классов. Затем вы- вызывается новая DDL-инструкция, CREATE JAVA, которая загружает класс Java из BLOB-столбца таблицы CREATE$JAVA$LOB$TABLE в РСУБД. Загрузка производится толь- только в том случае, если выполняется одно из следующих условий: О класс загружается впервые; О класс изменился; О задана опция -force. Процесс загрузки элементов Java в Oracle продемонстрирован на рис. 22.3. Файл .Java : Файл .jar § Рис 22.3. Загрузка элементов Java в Orade Утилита loadjava имеет следующий синтаксис: loadjava {-user | -u} ння_попьзоватепя/паропь[&бдза_паиных'] [-ш_паранетра \_-тя_ параметра] ...] иня_файла Шя_файла~\... Здесь пня_параиетра — это одно из множества значений, часть которых перечисле- перечислена ниже: | debug | {definer | d] | (encoding | e} имясхеныкОпировки I {force | f) {grant j g} {mpпользователя \ им_ро/ш}[.( имя пользователя \ m»jx>n* }]... (odfi | 0} |. {resolve | r) . | {resolver 5} и»я_схеиы | {synonym | s} I {thin f t} | {verbose | v} | {nousage} I {noverify)) В командной строке в любом порядке можно ввести имена файлов исходного кода Java, класса и ресурса, имена входных файлов SQLJ (файлов .sqfj), несжатых файлов .jar и архивов .йр. Например, следующая команда загружает в схему SCOTT класс JFi le: loadjava -user scott/tiger -oc18 -resolve JFile.class
Утилита loadjava 869 Эту команду можно вызвать из окна MS-DOS в системах, функционирующих на платформе Windows, или из командной строки в сеансе Unix. Ее можно вы- выполнить и из SQL*Plus: host loadjava -user scott/tiger -oc18 -resolve JF1le.class Для того чтобы упростить загрузку ресурсов Java в Oracle, мы создали для Windows файл с именем lj.bat, содержащий несколько команд: javac П.java loadjava -user %2 -oci8 -resolve Jl.Java С его помощью компиляция и загрузка Java-класса выполняется за один шаг; D:\Java> lj scott/tiger JFile Приведем несколько особенностей применения утилиты loadjava, о которых полезно знать. О Для вывода справки используется следующая команда: loadjava {-help | -h) О Элементы в списке параметров или файлов должны быть разделены только пробелами: -force, -resolve, -thin // Нет -force -resolve -thin // Да О Элементы в списке пользователей или ролей должны быть разделены тольхо запятыми: SCOTT. PAYROLL. 8LAKE // Нет SCOTT, PAYROLL. BLAKE // Да Параметры командной строки данной утилиты перечислены в табл. 22.2 с ука- указанием выполняемых ими функций. Таблица 22.2, Параметры командной строки утилиты loadjava Параметры Описание -debug Генерирует отладочную информацию. Этот параметр эквивалентен параметру javac -g -definer Указывает, что методы загружаемых классов будут вызываться с привилегиями создателя, а не вызывающего. (По умолчанию методы выполняются с привилегиями вызывающего.) Создающие классы пользователи могут иметь разные привилегии, а приложение может использовать очень много классов, поэтому следует убедиться, что методы класса выполняются только с необходимыми привилегиями -encoding Устанавливает (или переустанавливает) параметр -encoding в таблице базы данных JAVA$OPTION5, присваивая ему заданное значение, которым должно быть имя стандартной схемы кодировки JDK (по умолчанию — latfnl). Это значение используется компилятором, так что кодировка загружаемого исходного файла должна ему соответствовать. Дополнительная информация по данному вопросу приведена далее, в разделе «Функция GET_COMPILER_OPTT.on *i процедуры SET_ и RESET_COMPILEH_OPnON: чтение и установка параметров компилятора» продолжение!?
870 Глава 22 • Взаимодействие Java и РЦ/SQL Таблица 22.2 (продолжение) Параметры Описание -force -grant -ocffi -resolve -resdver -schema -synonym -thin ^verbose Вызывает принудительную загрузку фаллов класса Java независимо от того, были ли они загружены по этого. По умолчанию уже загруженные файлы класса повторно не загружаются. Учтите, что нельзя вызвать принудительную загрузку файла класса, если уже загружен файл исходного кода (сначала нужно удалить из схемы объект, представляющий исходный код) Предоставляет перечисленным пользователям или ролям привилегии EXECUTE на загруженные классы. (Эта привилегия необходима для непосредственного вызова методов класса.) Данный параметр куммулятивен. Указанные пользователи и роли добавляются в список имеющих привилегию EXECUTE. Для отмены прав нужно либо удалить и повторно загрузить объект схемы без параметра -grant, либо воспользоваться SQL-инструкцией REVOKE. Для предоставления прав на объект из схемы другого пользователя нужно иметь привилегию CREATE PROCEDURE WITH GRANT Заставляет утилиту loadjava взаимодействовать с базой данных через драйвер ОСТ JDBC. Этот параметр, используемый по умолчанию, и параметр -thin являются взаимоисключающими. Для вызова loadjava . на клиентском компьютере, где Oracle не установлена, используйте параметр -thin После загрузки и компиляции всех заданных в командной строке файлов выполняет разрешение всех имеющихся в классах внешних ссылок. Если этот параметр не задан, файлы загружаются, но не компилируются, а разрешение ссылок не производится до вызова файла во время выполнения. Этот параметр позволяет откомпилировать ранее загруженные классы и ссылаться на них. Параметр -force при этом задавать не следует, поскольку указанные операции выполняются независимо, после загрузки Связывает новые объекты схемы со спецификацией пользовательской системы разрешения имен. Значения этого параметра и используемого по умолчанию параметра -oraderesalver являются взаимоисключающими Связывает схему с новыми объектами Java. Если этот параметр не задан, используется схема подключения. Для загрузки объектов в схему другого пользователя нужно иметь привилегии CREATE ANY PROCEDURE Создает общедоступный синоним загружаемого класса, который делает его доступным для всех пользователей, находящихся за пределами схемы, в которую он загружен. Для того чтобы задать этот параметр, нужно иметь привилегию CREATE PUBLIC SYNONYM. Если вы задали этот параметр для исходных файлов, он действует и для компилируемых на их основе классов Заставляет утилиту loadjava взаимодействовать с базой данных через тонкий драйвер JDBC. Этот параметр и используемый по умолчанию параметр -oci8 являются взаимоисключающими. При вызове утилиты на клиентском компьютере, на котором Oracle не установлена, используйте параметр -thin Включает режим verbose, в котором выводятся сообщения о выполнении процесса -
Утилита dropjava 871 Как вы уже поняли, сфера использования утилиты loadjava довольно обширна. В частности, утилита может загружать отдельные файлы или сжатые группы эле- элементов, хранящиеся в виде jar- и ,zip-файлов. Более полное ее описание приведе- приведено в документации по Oracle. Утилита dropjava Утилита dropjava выполняет действия, противоположные производимым утили- утилитой loadjava. Она преобразует заданные имена файлов в имена объектов схемы, удаляет эти объекты схемы, а в завершение удаляет соответствующие им строки дайджест-таблицы. При удалении класса другие классы, непосредственно или косвенно от него зависящие, становятся недействительными. Удаление исходно- исходного кода приводит к удалению созданного на его основе класса. Приведем синтаксис утилиты dropjava: dropjava {-user | -u} иня_пользователя/паропь[9бвза_вднных'] [-ш_парапетра [-имя_ параметра] .,.] иня_файла [ння_файпа~\... Здесь и«я_ параметра — это одно из множества значений, часть которых пере- перечислена ниже: { {oci8 | 0} | {schema | s} иня_сх&ш I {thin | t} | {verbose | v) I {help} | {encoding} | {synonym}) В командной строке можно ввести, причем в любом порядке, имена файлов исходного кода Java, классов и ресурсов, имена входных файлов SQU, несжатых .jar-файлов и .zip-архивов. Краткое описание параметров командной строки утилиты dropjava приведено в табл. 22.3. Таблица 22.3. Параметры командной строки утилиты dropjava Параметр Описание ^___ -oci8 Заставляет утилиту взаимодействовать с базой данных через драйвер OCI JDBC. Значения этого параметра, используемого по умолчанию, и параметра -thin являются взаимоисключающими •schema Удаляет объекты из указанной схемы. Если этот параметр не задан, используется схема подключения. Для удаления объектов из схемы другого пользователя нужно иметь привилегию CREATE ANY PROCEDURE -thin Заставляет утилиту dropjava взаимодействовать с базой данных посредством драйвера ЮВС. Этот параметр и используемый по умолчанию параметр -ос!8 являются взаимоисключающими ¦verbose Включает режим verbose, в котором выводятся сообщения о ходе процесса выполнения утилиты
872 Глава 22 • Взаимодействие Java и PL/SQL Управление объектами Java в базе данных В этом разделе подробно рассматриваются вопросы, связанные с использованием Java-объектов в базе данных, а точнее, с их хранением и управлением ими. Пространство имен Java в Oracle Oracle уранит каждый класс Java как объект схемы. Имя этого объекта является производным от полного имени класса (но не совпадает с ним). Например, пол- полное имя класса ОгасП eSimpl eChecker должно быть таким: oracle.sqlj.checker.Oraci eSi mpl eChecker В базе данных же имя соответствующего объекта схемы будет таким: oracl e/sql j/checlcer /Oracl eSimpl eChecker Как видите, При сохранении объекта в СУБД Oracle точки в его имени заменя- заменяются символами косой черты. Длина имени любого объекта в Oracle, будь то имя таблицы базы данных или имя класса Java, не может превышать 30 символов. В Java такого ограничения не существует, а значит, вы можете использовать гораздо более длинные имена. Oracle позволяет загрузить в базу данных класс Java с именем длиной до 4000 символов. Но в том случае, если имя элемента Java содержит более 30 символов, Oracle ав- автоматически генерирует для него допустимый псевдоним. Однако не волнуйтесь — использовать псевдоним вам никогда не придется. Работая с программами, вы будете иметь дело с реальным именем элемента Java, a Oracle, когда потребуется, автоматически выполнит нужную подстановку. Просмотр загруженных элементов Java После загрузки в базу данных исходного кода Java, класса и ресурсов информа- информация о них становится доступной через несколько представлений словаря данных, перечисленных в табл. 22.4. Таблица 22.4. Информация о классах а словаре данных Представление Описание USER_OBJECTS Содержит информацию, извлекаемую из определений объектов типа ALJ^OBJECTS MVA SOURCE, JAVA CLASS И JAVA RESOURCE DBA_OBJECTS USER_ERRORS Содержит информацию об ошибках, происшедших при компиляции ALL_ERRORS объектов DBA_ERRORS U5ER__SOURCE Содержит исходный код Java, но только в том случае, если для создания объекта схемы вы использовали команду CREATE JAVA SOURCE _____ _____ ______ Информацию из этих представлений можно извлекать с помощью SQL-запро- SQL-запросов, а для отображения ее в более удобном виде можно написать специальные
Пакет DBMSJAVA 873 программы. Рассмотрим в качестве примера запрос, возвращающий список всех объектов схемы, относящихся к Java: /* Файл в web; showjava.sql и myjava.pkg */ COLUMN objectjiame FORMAT A30 SELECT objectjiame. object_type. status, timestamp FROM user_objects WHERE (objectjiame NOT LIKE 'SYSJ' AND objectjiame NOT LKE 'CREATED' ANO objectjiame NOT LIKE 'JAVA$r AND objectJiame NOT LIKE 'LOAQLOBX') AND object_type LIKE 'JAVA f ORDER BY object_type, objectjiame; Условия отбора объектов базы данных, которые Oracle использует для управ- управления объектами Java, задаются в предложении WHERE. Вот пример данных, воз- возвращаемых приведенным сценарием: SQL> exec inyOava.shoflbjects DBJECT NAME OBJECTJYPE STATUS TIMESTAMP Hello JAVA CLASS VALID 1999-05-19:16:42:27 JF1le2 JAVA CLASS VALID 1999-05-26:17:07:11 JF1le3 JAVA CLASS VALID 1999-05-27:12:53:46 plsoiiitions/java/putLn JAVA SOURCE VALID 1999-05-19:16:30:29 В файле myJava.pkg на web-узле издательства O'Reilly содержится версия это- этого же запроса, реализованная в виде пакетной процедуры и позволяющая про- просматривать информацию об объектах Java с помощью такого вызова: SQL> exec myJava.showobjects Следующий вызов позволяет получить список всех элементов Java, имена ко- которых начинаются с ОЕ: SQL> exec myJava.showobjects СОЕ*1) В столбце objectjiame представления USER_OBJECTS содержатся полные имена объектов схемы Java, длина которых не превышает 30 символов и которые не включают никаких символов из набора Unicode. Для остальных объектов в этом столбце указаны короткие псевдонимы. Чтобы преобразовать псевдоним в пол- полное имя, можно вызвать функцию LONGNAME из пакета DBMSJAVA, о котором расска- рассказывается в следующем разделе. Пакет DBMS_JAVA Встроенный пакет DBMSJAVA обеспечивает доступ к параметрам виртуальной ма- машины Aurora Java Virtual Machine и даже позволяет их модифицировать. Здесь содержится большое количество программ, многие из которых предназначены для использования только в среде Oracle. Представленные в пакете функции и проце- процедуры очень полезны для программистов, причем большую их часть можно вызы- вызывать и из SQL-инструкций. Краткое описание наиболее интересных программ
874 Глава 22 • Взаимодействие Java v\ PL/SQL приведено в табл. 22.5. Как упоминалось ранее в этой главе, в пакете DBMS_JAVA, кроме прочего, имеются программы для управления защитой и разрешениями. Таблица 22.5. Основные программы пакета DBMSJAVA Имя программы Описание ^ Функция LONGNAME Возвращает полное (длинное) имя объекта Java, соответствующее указанному короткому имени Orade Функция GET_COMPILER_OPTION Возвращает значение параметра компилятора Java из таблицы параметров Процедура SETj:OMPII-ER_OPTION Устанавливает значение параметра компилятора в таблице параметров Java; если таблицы еще нет, создает ее Процедура RESET_COMPILER_OPTION Переустанавливает значение параметра компилятора в таблице параметров Java Процедура SET_OUTPUT Перенаправляет выходные данные Java в текстовый буфер DBMS_OUTPUT Процедура EXPORT_SOURCE Экспортирует объект схемы, представляющий исходный код Java, в большой объект (LOB) Процедура EXPORT_RESOURCE Экспортирует объект схемы, представляющий ресурс Java, а большой объект Процедура EXPORT_CLASS Экспортирует объект схемы, представляющий класс Java, в Большой объект _^_ Эти программы достаточно подробно рассматриваются в следующих разделах. Функция LONGNAME: преобразование длинных имен Java Длина имен классов и методов Java может превышать 30 символов - максималь- максимальный размер идентификаторов SQL. В таких случаях Oracle создает для Java-эле- Java-элемента уникальное краткое имя и использует его в SQL и PL/SQL Для получения длинного имени объекта, соответствующего заданному его ко- короткому имени, используется функция FUNCTION DBMS_JAVA.LONSNAME (shortname VARCHAR2) RETURN VARCHAR2 Следующий запрос выводит длинные имена всех определенных в текущей схе- схеме классов Java, у которых длинные и короткие имена не совпадают: /* Файл в web: longname.sql */ SELECT objectjnarae shortnamB. DBMSJAVA.LONGNAHE (objectjiame) longname FROM USER OBJECTS WHERE objertjype - 'JAVA CLASS' AND objectjiame i- DBMSJAVA. LONGNAME (objectjiame): Приведенный выше запрос реализован и а пакете myJava (в файле myjava.pkg). Предположим, что мы объявили класс с таким именем: . public class DropAnyabjectldentifiedByTypeAndName {
Пакет DBMSJAVA 875 Для Oracle это имя слишком длинное, и с помощью следующего вызова можно убедиться, что РСУБД определила для него короткое имя длиной 30 символов: SQL> exec nyjava.showlongnanes Short Name | Long Name Short: /24742lb0_DropAny0bjectIdent1f Long: QropAnyObjectldentifiedByTypeAndName Функция GET_ COMPILER_OPTION и процедуры SET_ и RESET_COMPILER_OPTION: чтение и установка параметров компилятора Параметры компилятора для классов Java устанавливаются в таблице базы данных JAVASOPTIONS, называемой далее просто таблицей параметров. Их можно выбороч- выборочно переустанавливать с помощью параметров утилиты loadjava. Каждая строка в таблице параметров содержит имена объектов схемы, к которым относится со- соответствующий параметр. Добавив в таблицу несколько строк с одним и тем же именем параметра, можно задать различные значения этого параметра для раз- разных объектов схемы. Компилятор обращается к таблице параметров только в том случае, если инте- интересующий его параметр не задан в командной строке утилиты loadjava. Если же параметр не задан ни в командной строке, ни в таблице, компилятор использует его значение, задаваемое по умолчанию. (Подробную информацию о параметрах компилятора вы найдете в документации по Oracle SQJJDeveloper's Guide and Re- Reference) Ниже приведены значения по умолчанию двух параметров компилятора: encoding - latinl online - true // относится только к исходным файлам SQLJ Для считывания и установки значений параметров в таблице используются следующие функции и процедуры пакета DBMS_JAVA: FUNCTION DBMS JAVA.CET_COMPHER_OPnON ( объект VARCHAR2. ш_параиетра VARCHAR2) PROCEDURE DBMS JAVA. SET_COMP1LER_OPTION ( объект VARCHAR2. ипя_паранетра~Ч/Ю№.2. value VARCHAR2) PROCEDURE DBMS JAVA.RESET_C0MPILER_0PTION ( обьект VARCHAR2, тя_параметра VARCHAR2) Параметр объект задает имя пакета Java, полное имя класса или пустую строку. Компилятор ищет в таблице параметров строку, в которой значение параметра объект в наибольшей степени соответствует полному имени объекта схемы. Если в качестве значения параметра задана пустая строка, поиск распространяется на все объекты схемы. Параметр иня_параметра определяет, какой параметр устанавливается. Перво- Первоначально схема не содержит таблицы параметров. Ее можно создать с помощью процедуры DBMS_JAVA.SET_COMPILER_OPTION, которая к тому же присваивает значение
876 Глава 22 • Взаимодействие Java и PL/SQL заданному параметру. Параметры должны быть заключены в одинарные кавыч- кавычки, как это сделано в нашем примере: SQL> DBMSJAVA.SET_COMP1LER_OPTION CX.sqlj', 'online1, 'false'); Процедура SET_OUTPUT: управление выводом из Java При выполнении Java-кода в базе данных Oracle классы System.out и System.err выводят данные в текущие файлы трассировки. Это не очень удобно, если вам нужно просто протестировать код. В таких случаях с помощью процедуры DBMS_ JAVA. SETOUTPUT можно перенаправить выходные данные в текстовый буфер паке- пакета DBMSJMPUT, затем они автоматически выводятся в SQL*Plus. Синтаксис ука- указанной процедуры таков: PROCEDURE D8MS_JAVA.SET_OUTPUT (разнер_буфера NUMBER): Ниже приведен пример ее вызова в программе: /* Файл a web: ssoo.sql */ SET SERVEROUTPUT ON SIZE 1000000 EXEC DBMS_JAVA.SET_OUTPUT AOOO000); Документация по совместному использованию команды SERVEROUTPUT и проце- процедуры DBMS_JAVA.SET_OLTTPUT слишком скудна. Поэтому автору пришлось самостоя- самостоятельно провести тщательное тестирование, в результате которого были сделаны следующие выводы. О Минимальный размер буфера (он используется по умолчанию) составляет все- всего 2000 байт, максимальный — 1 000 000 байт. При задании большего значения ошибки не возникает, но само это значение игнорируется. О Значение размера буфера, устанавливаемое командой SET SERVEROUTPUT, имеет больший приоритет по сравнению со значением, которое задается с помощью процедуры DBMS_JAVA.SET_OUTPUT. О Если при выводе данных с помощью процедур пакета DBMSJMPUT объем дан- данных превышает размер буфера, отображается сообщение об ошибке: ORA-10027: buffer overflow, limit of nnn bytes О Такая ошибка, не возникает, если операторы вывода заданы в коде Java. В этом случае выходной текст просто усекается до размера буфера, и выполнение кода продолжается. Как и в случае использования пакета DBMSJJUTPUT, вы не увидите выходных данных процедур и функций Java до тех пор, пока не будет завершена работа хра- хранимой процедуры, из которой они были вызваны. Процедуры EXPORT_SOURCE, EXPORT_RESOURCE и EXPORT_CLASS: экспорт объектов схемы Пакет DBMS_JAVA включает следующий набор процедур для экспорта исходного кода, ресурсов и классов: PROCEDURE D6MS_JAVA.EXPDRTSOURCE ( имя VARCHAR2.
Пакет DBMSJAVA 877 [ blob BLOB | dob CLOB ] PROCEDURE DBMSJAVA. EXPORTJOURCE ( имя VARCHAR2. схеме VARCHAR2, С blob BLOB | Clob CLOB ] PROCEDURE DBMS JAVA.EXPORT_RE50URCE ( имя VARCHARZ. [ blob BLOB | clob CLOB ] PROCEDURE DBMS JAVA. EXPORT_RESOURCE ( имя VARCHAR2. схема VARCHAR2. [ blob BLOB | cJob CLOB 1 PROCEDURE DBMSJAVA.EXPORT.CLASS ( имя VARCHAR2. blob BLOB PROCEDURE DBMSJAVA.EXPORT_CLASS { ИМЯ VARCHARZ. схема VARCHAR2. blob BL08 ): Во всех Тфиведенных процедурах имя - это имя экспортируемого Java-объекта схемы (если оно не задано, используется текущая схема), a blob | clob— большой объект, в который записывается указанный Java-объект схемы. Класс нельзя экспортировать в объект типа CLOB — для этого пригоден только большой объект BLOB. Исходный код хранится в формате UTF8, и в этом же фор- формате записывается в BLOB. Рассмотрев следующую процедуру, вы поймете, как с помощью программ экс- экспорта получить исходный код Java-объектов схемы: /* Файл a web: showjava.sp */ CREATE OR REPLACE PROCEDURE show java_source t NAME IN VARCHAR2, SCHEMA IN VARCHAR2 ;= NULL ) -- Выводит исходный код Java (прототип). Автор: VacHm Loevski IS b CLOB: v VARCHAR2 B000): i INTEGER; object_not_ava11able EXCEPTION; PRAGMa'eXCEPTIONJNIT (object_not_available. -29532): BEGIN
878 Глава 22 • Взаимодействие Java и PL/SQL /* Помещаем исходный коа Java в CLQB. */ DBMS_LOB.createtemporary (b, FALSE ); dbmsjava.export_source (NAME. NVL (SCHEMA. USER), b); /* Сохраняем большой объект в переионной типа VARCHAR2 и выводим ее. */ 1 :- 1000; DBMSJ.OB.READ (Ь. 1, 1. V); pi (V); EXCEPTION WHEN object_not_available THEN IF (SQLERRM) LIKE '%no SUCMobJect' THEN DBMSJXJTPUT.putJine ('Java object cannot be found.'); END IF; END: Теперь с помощью инструкции CREATE JAVA можно создать Java-объект, пред- представляющий исходный код; CREATE OR REPLACE JAVA SOURCE NAMED "Hello" AS public class Hello { public static String helloO { return "Hello Oracle World*;} ): Затем вы сможете вывести этот исходный код с помощью следующего вызова (если активизирован вывод в буфер пакета DBMS_OUTPUT): 5QL> exec show javasource ('Hello') public class Hello { public static String hello( ) { return "Hello Oracle World";} Как сделать методы Java доступными в PL/SQL Создав классы Java и загрузив их в РСУБД Oracle, вы можете вызывать из PL/SQL (и SQL) их методы, сделав таковые доступными с помощью оболочки PL/SQL Спецификация вызова Оболочки PL/SQL нужны только для тех методов Java, которые вы хотите сде- сделать доступными через интерфейс PL/SQL. Методы Java могут вызывать другие методы Java непосредственно, без использования объектов-оболочек. Для того чтобы сделать метод Java доступным в Oracle, нужно написать спецификацию вы- вызова — заголовок программы PL/SQL (функции или процедуры), тело которой представляет собой вызов метода Java с помощью предложения LANGUAGE JAVA. Та- Такое предложение будет содержать полное имя метода, типы параметров и тип
Как сделать методы Java доступными в PL/SQL 879 возвращаемого значения. Спецификацию вызова можно объявить как отдельную функцию или процедуру, как программу в пакете или метод объектного типа: CREATE [OR REPLACE] --только для отдельной программы ¦«Стандартный заголовок функции или процедуры PL/SQL> (IS I AS) LANGUAGE JAVA NAME шпопное_иня_нетоца (nonHoe_m»jmaJava [. полное_лш<_т*пдJava]...) [return попное_ш_типа_зача~}": Здесь полноеjtnx_Tuna_java — это строка символов вида java.lang.String. Строка, заданная в предложении NAME, уникально идентифицирует метод Java, для которого создается данная оболочка. Параметры метода в его спецификации, сопоставляемые только по позиции, должны в точности соответствовать парамет- параметрам функции или процедуры PL/SQL, являющейся его оболочкой. Если метод не имеет параметров, включите пустой список параметров в его спецификацию, а не в заголовок процедуры или функции PL/SQL. Проиллюстрируем сказанное не- несколькими примерами. О Независимая функция, вызывающая метод: CREATE OR REPLACE FUNCTION fDelete ( file IN VARCHAR2) RETURN NUMBER AS LANGUAGE JAVA NAME 'JDelete.delete ( Java.lang.String) return 1nt'; О Пакетная процедура, вызывающая метод, которому передается объект. CREATE OR REPLACE PACKAGE nat_health_care IS ROCEDURE consolidate Insurer Ans Insurer) AS LANGUAGE JAVA NAME 'NHC_consol1datior,.process{orade.sql .STRUCT)': END nat_health_care: О Метод объектного типа с предложением LANGUAGE: CREATE TYPE pet_ AS OBJECT ( name VARCHAR2C100), date_of_birth DATE, MEMBER FUNCTION age ( namejn IN VARCHAR2) RETURN DATE AS LANGUAGE JAVA NAME 'petlrvfo.age (Java.lang.String) return java.sql.Timestamp' О Независимая процедура с выходным параметром: CREATE OR REPLACE PROCEDURE read out_f1le ( filename IN VARCHARZ, ~ filejine OUT VARCHAR2 ) AS LANGUAGE DAVA NAME put1 Is.ReadF1le.read(Java.lang.String ,Java.lang.Str1ng[])';
880 Глава 22 • Взаимодействие Java и PL/SQL Некоторые правила написания оболочек для Java-методов Приведем несколько правил, которых следует придерживаться при написании оболочек для методов Java. О Спецификация вызова метода в PL/SQL и указанный в ней метод Java долж- должны принадлежать к одной схеме. О Спецификация вызова делает доступной для Oracle точку входа метода Java верхнего уровня. Поэтому только общедоступные статические методы можно публиковать как независимые процедуры, а методы-члены класса публикуют- публикуются как методы-члены объектного типа SQ.L. О В списке параметров программы PL/SQL, служащей оболочкой для вызова метода Java, нельзя задавать значения параметров по умолчанию. О В объектно-ориентированных языках программирования, если метод должен изменить значения атрибутов объекта, этот объект не передается ему в качест- качестве параметра. Метод вызывается как член данного объекта. Если параметр ме- метода, вызываемого из SQL или PL/SQL, необходимо изменить, в специфика- спецификации вызова для указанного параметра следует определить режим OUT или IN OUT. Соответствующий ему параметр Java должен быть одноэлементным массивом. ПРИМЕЧАНИЕ В качестве параметра можно использовать любой объект Java соответствующего типа, если речь идет о параметрах, для которых задан режим IN OUT, или объект любого совместимого типа дан- данных, если это допускается типом данных Java. Новое значение параметра будет возвращено вызы- вызывающему коду. Например, выходной параметр типа NUMBER в Спецификации вызова можно поста- поставить в соответствие Java-параметру, объявленному KaKfloat[] p, и затем присвоить новое значение элементу р[0]. Функиия, в которой объявлены параметры с режимом использования OUT или IN OUT, не может быть вызвана из DML-инструкции. Сопоставление типов данных Ранее в этой главе приводился очень простой пример оболочки PL/SQL — функ- функция fDeiete, передающая методу Java значение VARCHAR2 как параметр типа Ja- Java. Tang.String. Метод возвращал целочисленное значение, которое передавалось функции PL/SQL посредством предложения RETURN NUMBER. Это пример сопостав- сопоставления типов данных, то есть установки соответствия между типами данных Java и PL/SQL. В спецификации вызова параметры PL/SQL и Java, как и возвращаемое функ- функцией значение, сопоставляются в соответствии с их позициями, их типы данных должны быть совместимыми. О том, как соотносятся типы данных PL/SQL и Java, можно судить по табл. 22.6. Взаимопреобразование соответствующих друг другу типов данных Oracle выполняет автоматически. Вы сможете убедиться, что Oracle автоматически преобразует только типы данных SQL. Произвести автоматическое преобразование специфических типов данных PL/SQL, таких как 8INARYJNTEGER, PLSJNTEGER, BOOLEAN и ассоциативные массивы, в соответствующие типы данных Java невозможно. Преобразование этих типов выполняется вручную. Далее, в разделе «Другие примеры», приводятся
Как сделать методы Java доступными в PL/SQL 881 ссылки на код, выполняющий нестандартное преобразование типов. Еще более подробные примеры с использованием JDBC имеются в документации по Oracle. Таблица 22.6. Соответствие между типами данных SQL и Java Тип данных SQL Класс Java CHAR, NCHAR, LONG, VARCHAR2, NVARCHAR2 DATE NUMBER. RAW, LONG RAW ROWID BHLE BLOB CLOB,NCLOB OBJECT REF TABLE, VARRAY Любой из перечисленных выше типов SQL orade.sql.CHAR java.lang.String Java.sql.Date java.sql.Time java.sql.Timestamp Java.lang.Byte Java.lang.Short java.lang.Integer Java.lang.Long Java.lang.Float ]ava.lang.Double Java.math.BigDecimal byte, short, Int. long, float; double oracle.sql.DATE java.sql.Date java.sql.Time java.sql.Timestamp java.lang.Strfrtg oracle.sql.NUMBER Java.lang. Byte Java.lang.Short java.lang.Integer Java.lang.Long Java.lang.Float Java.lang.Double java.math. BigDecimal byte, short, int, long, float, double oracle.Sql.RAW bytefl * I, J orade.sql.CHAR orade.sql.ROWID Java.lang.String Oracle.sql.BFILE orade.sql.BLOB oracle.jdbc2.Blob orade.sql.CLOB orade.Jdbc2.aob orade.sql.STRUCT oracle.SqljData oracle.jdbc2.Stnjct orade.sql.REF oracle.jdbc2.Ref oracle.sqLARRAY oracle.Jdbc2A-ray oracle.sql.CustomDatum oracle.sql.Datum
882 Глава 22 • Взаимодействие Java и РЦ/SQL Вызов метода Java в SQL Из DML-инструхций можно вызвать пользовательские функция, и в том числе функции-оболочки методов Java. К этим методам предъявляются определенные требования. О Метод, вызываемый из инструкции SELECT или распараллеливаемой инструк- инструкции INSERT, UPDATE либо DELETE, не должен модифицировать таблиды базы дан- данных. О Метод, вызываемый из инструкции INSERT, UPDATE или DELETE, не должен вы- выполнять запросы к таблицам базы данных или модифицировать таблицы, из- изменяемые этой инструкцией. О Метод, вызываемый из инструкции SELECT, INSERT, UPDATE или DELETE, не должен выполнять SQL-инструкции управления транзакциями (например, инструк- инструкцию COMMIT), управления сеансом (SET ROLE) или управления системой (ALTER SYSTEM). Кроме того, он не должен выполнять DDL-инструкции, так как они автоматически производят фиксацию транзакции. Данные ограничения отсут- отсутствуют, если метод выполняется из блока PL/SQL с автономной транзакцией. Цель ввода перечисленных ограничений — предотвращение побочных эффек- эффектов, которые могут помешать корректному выполнению SQL-инструкции. По- Попытка сделать это путем вызова метода, который не удовлетворяет хотя бы одно- одному из требований, приведет к сообщению об ошибке во время выполнения уже при синтаксическом анализе SQL-инструкции. Методы Java можно вызывать из PL/SQL посредством команды CALL и встро- встроенного динамического SQL, что продемонстрировано на примере фрагмента кода, представленного ниже (реализация процедуры dropany приведена в следующем разделе): DECLARE Тр varchar2C0):-'TABLE': Nm varchar2C0):-'mytable'; BEGIN EXECUTE IMMEDIATE 'CALL dropanyC:tp.:rai)' USING tp. nm; END; Обработка исключений с помощью Java Механизм обработки исключений в Java очень похож на подобный механизм, ис- используемый в PL/SQL. Исключение сначала инициируется, а затем перехватыва- перехватывается для обработки. Тем не менее механизм обработки исключений Java гораздо более соверше- совершенен. В нем имеется базовый класс, названный Exception. Все исключения являются объектами этого или производных от него классов. Исключения можно переда- передавать в качестве параметров и манипулировать ими так же, как объектами любого другого класса. Исключение, генерируемое при выполнении SQL-инструкции хранимым ме- методом Java, является объектом подкласса Java .sql .SQLException. Этот подкласс име- имеет два метода, которые возвращают код ошибки Oracle и сообщение об ошибке: getErrorCodeO zgetMessageO.
Как сделать методы Java доступными а РЦ/SQL 883 Если исключение, которое возникает при выполнении хранимой процедуры Java, вызванной из SQL или PL/SQL, не обрабатывается JVM, оно передается вызывающей программе. Ей передаются, в частности, все неперехваченные исклю- исключения, в том числе и такие, которые не относятся к SQL. Рассмотрим несколько способов обработки исключений. Предположим, что для удаления объектов из базы данных посредством JDBC был создан специальный класс (это пример из документации Oracle): /* Файл в web: dropany.Java */ Import java.sql.*; import java.io,*: import oracle.jdbc.driver.*: public class DropAny { public static void object (String object_type. String object_name> throws SQLException { // Подк/шчение к Oracle через драйвер JD8C Connection conn - new OracleDriverO.defaultConnectionO; // Формирование SQL-vmcipyKuw String sql = "DROP " + object_type + " " + object_name: try { Statement stmt - conn.createStatementO: stmt. executeUpdatetsql); stmt.closet): catch (SQLException e) {System.err.prinflnte.getMessageO):} ПРИМЕЧАНИЕ — . Конечно, не имеет смысла производкпъ удаление объектов базы данных через JDBC, поскольку это гораздо проще сделать с помощью PL/SQL. Однако реализация подобной функции в Java делает ее доступной другим Java-программам. Созданный класс перехватывает любое исключение SQLException и выводит ин- информацию о нем с помощью следующего кода: } catch (SQLException e) {System.err.printing.getMessageO):} Загрузим его в базу данных Oracle с помощью утилиты loadjava я создадим для него PL/SQL-оболочку: CREATE OR REPLACE PROCEDURE dropany ( tp IN VARCHAR2, ran IN VARCHAR2 ) ftS LANGUAGE JAVA NAME 'DropAny.object ( Java.lang.String. Java.lang.String)'; При попытке удалить несуществующий объект мы получим один из двух ре- результатов: Sa> CONNECT scott/tlger Connected.
884 Глава 22 • Взаимодействие Java и PL/SQL SQL> SET SERVEROUTPUT ON SQL> BEGIN dropany ('TABLE'. 'bTip'J: END: PL/SOL procedure successfully completed. SQL> CALL D8MS_JAVA.SET_0UTPUT ClOOOOOO); Call completed. SQL> BEGIN dropany ('TABLE', 'blip1); ?N0: ORA-0Q942: table or view does not exist Результатов же работы метода System. err. print 1 n вы не увидите, пока не будет вызвана процедура DBMS_JAVA.SET_OL/TPUT. Однако в любом случае в вызывающем блоке исключение не инициируется, поскольку оно было перехвачено Java. По- После второго вызова функции dropany сообщение об ошибке, полученное классом Java с помощью метода getMessageO, попало в выходной текстовый буфер Oracle и было выведено на экран в SQL'Plus. Если строки метода DropAny. object будут закомментированы, мы получим со- совсем иной результат: SQL> 2 3 4 5 6 7 8 BEGIN dropany EXCEPTION ('TABLE WHEN OTHERS THEN DBMS DBMS END: OUTPUT. .OUTPUT. PUT PUT 'blip' LINE _LINE ); (SQICODE); (SOLERRM): Java.sql.SQLException: QRA-00942: table or view does rot exist at oracle.jdbc.kprb.KprbDBAccess.check_error(KprbDBAccess Java) at oracle.jdbc.kprb.KprbDBAccess.parseExecuteFetchCKprbDBAccess.Java) at oracle.jdbc.dri ver.OracleStatement.doExecuteOther(OracleStatement.Java) at oracle.jdbc.dri ver.OracleStatement.doExecuteWi thBatch COracleStatement.Java) at oracle.jdbc.driver.OracleStatement.doExecute@radeStatement.Java) at oracl e.jdbc.dri ver.Oracl eStatement.doExecuteWithTimeoutCOracleStateraervt .Java) at oracle.jdbc.driver.OracleStatement. executel1pdatetOracleStatement.Java) at DropAny.object(DropAny.Java:14) -29532 ORA-29532: Dava call terminated by uncaught Dava exception: java.sql.SQLException: ORA-00942: table or view does not exist Этот код требует пояснения. Результаты, выведенные между строкой Java.sql.SQLException: ORA-00942: table or view does not exist и строкой -29532 представляют собой содержимое стека ошибок, сгенерированных Java и направ- направляемых на стандартный вывод независимо от используемого способа обработки
Как сделать методы Java доступными в PL/SQL 885 этих ошибок в PL/SQL. Иными словами, даже если бы блок обработки исключе- исключений в вызывающей программе PL/SQL был таким: EXCEPTION WHEN OTHERS THEN NULL; то на экран все равно выводился бы тот же текст, и лишь после этого продолжа- продолжалось бы выполнение внешнего блока (при наличии такового). Обратите внима- внимание: код ошибки Oracle равен не ORA-00942, a 0RA-29532, что свидетельствует лишь о появлении ошибки Java (какая это ошибка, пока что неизвестно). А для того чтобы перехватить ошибку и обработать ее должным образом, придется написать специальную утилиту. Похоже, что утилита SQLERRM возвращает ошибку в такой форме: 0RA- 29532: Java call ...: java.sql .SQLException: ORA-AWm ... ' Поэтому в данной строке сначала ищется элемент java.sql .SQLException, а за- затем с помощью функции SUBSTR устанавливается номер исходной ошибки Oracle. На узле O'Reilly, в файле getErrorlnfo.sp, содержится действующая по этому прин- принципу программа, которая возвращает код текущей ошибки и соответствующее со- сообщение. В следующем разделе для расширения функций класса JOeiete будет разрабо- разработан класс JFi I e, предназначенный для выполнения из PL/SQL различных файло- файловых операций. Мы подробно расскажем о процессе написания классов Java и со- соответствующих им программ-оболочек PL/SQL, которые служат для манипули- манипулирования объектами Oracle. Новые возможности файлового ввода-вывода Встроенный пакет Oracle UTL_FILE примечателен, скорее, не присутствующими, а отсутствующими возможностями. С помощью содержащихся в нем программ можно лишь последовательно считывать и записывать содержимое файла. Но уда- удалять и копировать файлы, изменять привилегии, считывать содержимое катало- каталога, задавать пути, производить какие-либо еще действия невозможно. Зато в Java имеется множество классов для выполнения указанных и ряда других операций с файлами. Вы уже немного знакомы с классом Fi I e и знаете, как просто с его по- помощью добавить в PL/SQL функцию удаления файла. Теперь давайте создадим новый класс с именем JFile, который позволит раз- разработчикам PL/SQL получить ответы на следующие вопросы. О Можно ли считывать данные из указанного файла? Существует ли этот файл? Чем — файлом или каталогом — является указанный элемент? О Сколько байтов содержит файл? Каков его родительский каталог? О Каковы имена всех файлов в каталоге, соответствующих заданному условию отбора? Кроме того, с его помощью можно будет выполнять такие действия: О создавать каталоги; О переименовывать файлы; О изменять расширения файлов.
886 Глава 22 • Взаимодействие Java и PL/SQL Мы не станем описывать все методы класса JFile и соответствующий пакет; в нем много повторений и большинство представленных там методов Java похожи на функцию delete О, упоминавшуюся в начале главы. Нам больше хотелось бы сконцентрировать внимание на технологических моментах, связанных с разными элементами класса и пакета. Полный код класса JFile вы найдете в файлах, хра- хранящихся на web-узле издательства O'Reilly. О JFile. Java — класс Java, собирающий информацию о файлах операционной системы и возвращающий ее через API, доступный из PL/SQL. О xf i I e. pkg — пакет PL/SQL, служащий оболочкой для класса JF11 е. Его назва- название расшифровывается как «eXtra stuff for FILEs» (дополнительные функции для работы с файлами). ПРИМЕЧАНИЕ- В Orade9i Release 2 включена расширенная версия пакета UTL_FILE, который, помимо всего проче- прочего, позволяет удалять файлы, для чего в нем используется процедура UT1__FILE.FREMOVE. Кроме того, данный пакет поддерживает копирование файлов и их переименование. Усовершенствование метода delete Прежде чем приступить к разработке новых методов, проверим, насколько опти- оптимальным является уже написанный нами код. Следует признать, что метод JDele- te. del ete() и функция delete_f i 1 e далеки от совершенства. Приведем код метода: public static int delete {String fileName) { File myFile - new File (fileName); boolean retval - rnyF1le.delete(): if (retval) return 1: else return 0; CREATE OR REPLACE FUNCTION fDelete ( file IN VARCHAR2) RETURN NUMBER AS LANGUAGE JAVA NAME -JDelete.delete ( java .Tang.String) return int'; Проблема заключается прежде всего в том, что здесь используется числовое представление логических значений TRUE/FALSE. В результате функцию приходит- приходится вызывать примерно так: IF fdelete С'c:\temp\teilp.sqr) - 1 THEN ... Это, конечно же, никуда не годится. И не только потому, что код «некрасив» и содержит жестко закодированное значение, но еще и потому, что при вызове метода fdel ete нужно указывать, как будут представлены возвращаемые классом Java значения TRUE и FALSE. Гораздо целесообразнее было бы определить функцию удаления файла с та- таким заголовком: FUNCTION fDelete ( file IN VARCHAR2) RETURN BOOLEAN:
Как сделать методы Java доступными в PL/5QL 887 Поэтому давайте подумаем, можно ли предоставить пользователям пакета xfile более понятный и удобный интерфейс. Начнем мы с переименования класса JDe- 1 ete в класс JFi 1 е. Затем добавим методы, инкапсулирующие значения TRUE/FALSE, и будем вызывать их изо всех методов, возвращающих логические значения, вклю- включая метод delete!): /* Файл a web: JFile.java */ import java.io.File: public class JFile { public static 1nt tVal ( ) ( return 1; }; public static int fVal ( ) ( return 0; }; public static int delete (String fileName) { File niyFile - new File (fileName): boolean retval - myFile.deletet ): if (retval) return tVelO; else return fVaK ); Теперь обратимся к пакету PL/SQL Вот первая версия его спецификации: /* Файл в web: xfile.pkg */ CREATE OR REPLACE PACKAGE xfile IS FUNCTION delete (file IN VARCHAR2) RETURN BOOLEAN: END xfile: Это объявление функции типа BOOLEAN, но как ее реализовать? Перед нами сто- стоят две задачи. 1. Скрыть тот факт, что значения TRUE и FALSE представлены в виде чисел. 2. Избежать жесткого кодирования в пакете значений 1 и 0. Для их решения следует определить в пакете две глобальные переменные: /* Файл в web: xfile.pkg */ CREATE OR REPLACE PACKAGE xfile IS gjxue INTEGER: S_fa1se INTEGER: В конец пакета можно поместить инициализационный раздел, в котором гло- глобальным переменным с помощью функций tVal и fVal присваиваются начальные значения. Выполнение данной операции в инициализационном разделе позволит избежать лишних вызовов методов Java и связанных с этим затрат: 8EGIN g_true :- tval: g_false :- fval; END xfile;
888 Глава 22 • Взаимодействие Java и PIVSQL Теперь вернемся в раздел объявлений пакета и определим две личные функ- функции, единственным назначением которых будет предоставление доступа к мето- методам JFile, инкапсулирующим значения 1 и 0: FUNCTION tval RETURN NUMBER AS LANGUAGE JAVA NAME 'JFile.tVal О return int': FUNCTION fval RETURN NUM8ER AS LANGUAGE JAVA NAME 'JFile.fVal () return int'; Итак, жесткого кодирования значений TRUE/FALSE в пакете больше не будет. Для того чтобы спецификация пакета содержала настоящую логическую функ- функцию, создадим личную функцию-оболочку для метода JFil e. del ete(), возвращаю- возвращающую число: FUNCTION idelete (FILE IN VARCHAR2) RETURN NUMBER AS LANGUAGE JAVA NAME 'JFile.delete (java.lang.String) return int'; Теперь наша общедоступная функция delete может вызывать функцию ide- 1 ete и преобразовывать возвращаемое ею целочисленное значение в тип данных BOOLEAN, сравнивая его со значением глобальной переменной: FUNCTION delete {file IN VARCHAR2) RETURN BOOLEAN AS BEGIN RETURN idelete (file) - g_true: EXCEPTION WHEN OTHERS THEN RETURN FALSE : END; Вот так логическое значение Java преобразуется в логическое значение PL/SQL Вы еще не раз встретитесь с подобным преобразованием в теле пакета xfile. Чтение содержимого каталога Одной из интереснейших функций класса JFi I e является di rContents, возвращаю- возвращающая список файлов заданного каталога. Функция выполняет свою задачу с помо- помощью метода File.listO. Если строка, указанная при создании объекта File, была именем каталога, этот метод возвращает строковый массив имен файлов, обнару- обнаруженных в каталоге. Посмотрим, как можно сделать эту информацию доступной в PL/SQL. Для начала напишем метод dirContents, возвращающий значение типа String: /* Файл г web: JFile.java */ public static String dirContents (String dir) { File inyDir - new File (dir); String[] filesLfst « myOir.listO; String contents - new StringO: for (int 1 - 0; i < filesList.length; 1++)
Как сделать методы Java доступными в PL/SQL 889 contents - contents ¦ listDelimiter + filesL1st[1]: // Utrecht 4/2002 return contents; } Данный метод создает объектную переменную myDi r класса Fi 1 е, а также при- присваивает результат, возвращаемый методом myDi г. 1 i st(), массиву f i 1 esList, кото- который состоит из элементов типа String. После этого в цикле for все имена файлов объединяются в одну строку, разделенную символами, которые были заданы пе- переменной 1 i stDel imiter. И, наконец, метод возвращает новую строку вызывающе- вызывающему коду. В PL/SQL мы создадим оболочку, вызывающую данный метод: FUNCTION dirContents (dir IN VARCHAR2. del in) IN VARCHAR2) RETURN VARCHAR2 AS LANGUAGE JAVA NAME 'JFile.dirContents (Java.lang.String, java.lang.String) return java.lang.String'; Но что нам делать с полученной строкой? Напишем для функции-оболочки еще одну функцию, возвращающую информацию в более удобном для разработ- разработчика виде. Хорошо бы предоставить пользователям пакета xfile возможность ра- работать с именами файлов как со списком строк или с вложенной таблицей (по- (поскольку в таком, более структурированном, виде их легче просматривать и обра- обрабатывать). Для этого определим следующий табличный тип: CREATE TYPE file_list_t IS TABLE OF VARCHAR2B00O): Теперь осталось объявить процедуру, возвращающую список файлов заданно- заданного каталога в виде вложенной таблицы типа file_list_t Обратите внимание на вызов функции-оболочки di rContents и ссылку на переменную g_l 1 stdel i m, содер- содержащую разделитель, который был получен от JFi I e тем же способом, что и число- числовые значения для TRUE и FALSE: PROCEDURE getOirContents С d1r IK VARCHAR2. files IN OUT filejist t) IS filejist VARCHAR2C2767); next_del1m PLSJNTEGER; start jx>s PLS INTEGER :- 1; BEGIN files.DELETE: filejist :- dirContents (dir): LOOP next_delim :- INSTR (file list, gjistdelim. start_pos): EXIT WHEN next del 1m - 0: files.EXTEND: " files(files.LAST) :- SUBSTR (filejist. start_pos. nextdelim - start_pos): start_pos :- next_delim + 1;
890 Глава 22 • Взаимодействие Java и PiySQL END LOOP; END; Ну а дальше вы уже сами может экспериментировать с PL/SQL и Java, осно- основываясь на описанных принципах их взаимодействия. В пакете xfile вы найдете следующие процедуры, использующие функцию getDi rContents: О GetDi rContents — дает возможность пользователю указать типы отбираемых файлов, например *.tmp или %.tmp, и получить их список (символ подчерки- подчеркивания, согласно стандарту SQL, интерпретируется как один произвольный сим- символ в проверяемой строке); О showOi rContents — выводит список файлов из указанного каталога, соответст- соответствующих заданному фильтру; О chgext — изменяет расширение имени заданного файла. Кроме того, вы найдете в пакете xflle точки входа пакета UTLJ-ILE, такие как FOPEN и PUTJ.INE. Мы добавили их для того, чтобы пакет UTLFILE не использовался ни для каких других операций, кроме объявления дескрипторов файлов (UTLFI- LE.FILE_TYPE) и ссылок на декларированные в нем исключения. Другие примеры использования Java На web-узле издательства O'Reilly представлены три интересных класса, в кото- которых Java используется для расширения возможностей PL/SQL, а также для вы- выполнения более сложного сопоставления типов. О utl zi p. sql — этот класс Java и соответствующий ему пакет делают доступны- доступными для PL/SQL функции сжатия файлов. Они также загружают класс в базу данных с помощью инструкции CREATE OR REPLACE JAVA, не прибегая к утилите loadjava. Вот заголовок инструкции, создающей класс Java: CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "LTTLZip" AS import java.uti1.2ip.*: Import java.io.*: public class utlzip { public static void compressfiMstring infilename. string outfilename) f / А это оболочка для вызова метода класса: CREATE OR REPLACE PACKAGE utlzip IS PROCEDURE compressfile tp 1n file IN VARCHAR2. p_out_f1le IN VARCHAR2) AS LANGUAGE JAVA NAWE 'UTLZip.compressFileC Java. lang. String. Java. lang. String)'; END: О Deletefiie.Java и deletefile.sql - код этого класса Java демонстрирует один из способов передачи коллекции (вложенной таблицы или массива VARRAY) в массив Java. Из заданного каталога удаляются все файлы, которые были из- изменены после определенной даты. Для этого в PL/SQL создается вложенная
Как сделать методы Java доступными в PL/SQL 891 таблица объектов, содержимое которой затем передается в Java с помощью класса oracle.sql .ARRAY: CREATE TYPE fi1e_detai1s AS OBJECT ( dirname VARCHAR2 C0). deletedate DATE) CREATE TYPE filejable AS TABLE OF fUe_deta11s: CREATE OR REPLACE PACKAGE delete files IS FUNCTION fdelete (tbl IN file table) RETURN NU^ER AS LANGUAGE JAVA NAME ' DeleteFile. delete Coracle.sql.ARRAY) return inf: END delete_files: Приведем начальные строки метода Java. Обратите внимание, что код извле- извлекает результирующий набор данных из массива и сканирует его. Полная реа- реализация этого метода и подробные комментарии имеются в файле DeleteRle.java. public class DeleteFile { public static int delete(oracle.sql.ARRAY tbl) throws SOLException f try ( // Считываем содержимое таблицы/varray как резултирующий набор ResultSet rs - tbl .getResultSetO; for (int ndx - 0: ndx < tbl.lengthO: ndx++> { rs.nextt): // Извлекаем индекс и элеменг пассива, int aryndx - (int)rs.getlnt(i); STRUCT obj - {STRUCT)rs.getObjectB): О utl cmd. sql - этот Java-класс и соответствующий ему пакет позволяют без тру- труда выполнять из PL/SQ.L команды, операционной системы.
23 Внешние процедуры > Концепция внешних процедур > Конфигурирование Oracle Net > Создание библиотеки Oracle > Вызов внешней функции > Инициирование исключений из программ на С > Агенты, отличные от используемых по умолчанию > Сопровождение внешних процедур Во времена, когда программисты работали с Огас1е7, они часто задавали вопрос; «Какие средства можно вызывать из Oracle?» Как правило, под средствами под- подразумевалась система электронной почты, команды операционной системы или функции, реализованные в других языках программирования. И хотя после вклю- включения в состав Oracle пакета UTLSMTP проблема электронной почты потеряла свою актуальность, методов выполнения других внешних по отношению к Oracle опе- операций по-прежнему существует не очень много. Для реализации внешних опера- операций можно сделать следующее: О написать программу в виде хранимой процедуры Java и затем вызывать ее из кода PL/SQL; О использовать таблицу базы данных или очередь как буфер для записи запро- запросов и создать отдельный процесс для их считывания и обработки; О создать канал базы данных и написать демон, отвечающий на входящие запросы; О написать программу на языке С и вызывать ее как внешнюю процедуру. Кратко рассмотрим каждый из этих методов. Код Java подходит в том случае, если его быстродействия достаточно для вашего приложения. Интересна техно- технология, базирующаяся на использовании очереди, но даже применение обыкно- обыкновенных таблиц требует запуска двух сеансов Oracle: одного для записи запросов в очередь, а другого — для считывания. Более того, для каждого сеанса выделяется отдельное пространство транзакций, что может создать дополнительные сложно- сложности при работе приложения. Применение канала базы данных связано не только с проблемой двух сеансов, но и с упаковкой и распаковкой данных, передаваемых
Концепция внешних процедур 893 по каналу. Кроме того, необходимость одновременной обработки множества за- запросов с использованием любого из этих подходов может потребовать от вас соз- создания собственного процесса прослушивания соединения — листенера (listener) - и системы диспетчеризации процессов. Учитывая все это, мы остановились на четвертом методе. Применение внеш- внешних процедур позволяет выполнять в PL/SQL практически любые задачи, реали- реализуемые средствами других языков программирования, и в то же время избежать проблем, характерных для остальных упомянутых подходов. У программистов PL/SQL обычно возникает множество вопросов относительно внешних проце- процедур: как они работают, насколько защищены, каким образом проектировать их са- самостоятельно, каковы их достоинства и недостатки. В настоящей главе мы не только попытаемся дать ответы на все эти вопросы, но и приведем примеры, де- демонстрирующие большинство возможностей внешних процедур. Концепция внешних процедур Для того чтобы внешнюю программу можно было вызвать из среды Oracle, ее не- необходимо реализовать в виде библиотеки, доступной для совместного использо- использования. Речь идет о библиотеках динамической компоновки (DLL), используемых Microsoft, и общих библиотеках системы UNIX, хранящихся в файлах с расшире- расширением .so (shared object). Теоретически внешнюю процедуру можно написать на любом языке, но компилятор и компоновщик должны сгенерировать библиотеку, которую можно вызывать из кода, написанного на языке С. Чтобы обеспечить возможность вызова внешней программы из PL/SQL, для нее нужно написать специальную оболочку PL/SQL, называемую специфихагулей вызова. Если внеш- внешняя программа возвращает значение, ей ставится в соответствие функция PL/SQL, а если она ничего не возвращает — процедура PL/SQL. Пример выполнения команды операционной системы В качестве первого примера применения внешних процедур рассмотрим вызов из программы PL/SQL команды операционной системы. Конечно, у вас немедленно возникнет масса вопросов, касающихся обеспечения безопасности, но их мы пока рассматривать не будем. Используем очень простую написанную на С функцию extprocshO, которая принимает строку символов и передает ее функции system для выполнения в ка- качестве команды: int extprocshtchar *cmd) { return system(cmd); } Она возвращает код результата, полученный от функции system, которая обыч- обычно хранится в С-библиотеке времени выполнения libc (для систем UNIX) или msvcrt.dll (для систем Microsoft).
894 Глава 23 • Внешние процедуры Сохранив функцию в файле extprocsh.c, при помощи компилятора GNU С мож- можно сгенерировать файл совместно используемой библиотеки: дсс -с extprocsh.c дсс -shared -о extprocsh.so extprocsh.o --Соглашение об именах в большинстве систем UNIX дсс -shared -о extprocsh.dll extprocsh.o --Соглашение об именах в системах Microsoft Windows В результате выполнения данных команд генерируются объектный файл extp- extprocsh.o и файл совместно используемой библиотеки extprocsh.so или extprocsh.dll. Библиотечный файл требуется поместить в каталог, из которого система Oracle могла бы его извлечь1. Если переменная окружения ORACLE_HOME установлена, мож- можно просто скопировать файл из каталога, где он был откомпилирован, выполнив соответствующую команду: ср extprocsh.so JORACLE_HOME/lib/ -- Unix copy extprocsh.dll XORACLE_HOMEJ!\bin\ -- Microsoft Далее нужно создать в Oracle «библиотеку», указывающую на DLL. Если ад- администратор базы данных предоставил вам привилегию CREATE LIBRARY, вы сможе- сможете выполнить приведенные ниже команды: CREATE OR REPLACE LIBRARY extprocshelljib AS '/u01/app/oracle/9.2/lib/extprocsh.so': -- UNIX CREATE OR REPLACE LIBRARY extprocshelljib AS 'c:\oracle\ora9Z\biri/extpracsh.dll': -- Microsoft Теперь у нас имеется библиотека extprocshel IJ i b. Термин -«библиотека» в дан- данном случае применять не совсем корректно, потому что в действительности речь идет о псевдониме полного имени файла операционной системы, который добав- добавлен в пространство имен Oracle. Приступим к написанию PL/SQL-спецификации вызова внешней процедуры: CREATE OR REPLACE FUNCTION shell(and IN VARCHAR2) RETURN PLSJNTEGER AS LANGUAGE С LIBRARY extprocshelljib NAME "extprocsh" PARAMETERS (and STRING, RETURN INT); Эта спецификация ставит в соответствие параметрам функции PL/SQL пара- параметры функции С, позволяя обращаться к функции shel 10 из любого места кода, откуда может быть вызвана функция PL/SQL (SQJL'Plus, Perl, Pro* С и т. п.). С точки зрения программирования приложений вызов внешней процедуры не от- отличается от вызова обычной процедуры PL/SQL. Если администратор базы данных установил параметры системного окруже- окружения для поддержки внешних процедур (см. раздел «Настройка параметров кон- конфигурации листенера» далее в этой главе), вызвать такую процедуру очень про- просто. Достаточно такого блока кода: DECLARE result PLS INTEGER: Усовершенствованная система защиты Oracle Net в Oracle9i Release 2 позволяет по умолчанию хра- хранить файлы библиотек только в указанных каталогах. Но, к сожалению, администратор базы дан- данных не позволит поместить пользовательские файлы в каталога Oracle.
Концепция внешних процедур 895 BEGIN result :- shellCионанда_операционной_снстемы'); END: или: SQL> VAR res NUMBER SQL> CALL shell('комаида_операциоиной_систены') INTO :res; В большинстве случаев команда операционной системы выводит результаты на стандартное устройство stdout или stderr, однако вы можете (если позволяют привилегии) перенаправить вывод в файл. Так, в приведенном ниже примере со- содержимое каталога не выводится на экран, а записывается в файл: result,- shell Г Is / > /tmp/extproc.out'); -- UNIX result:- shell('and /c "dir c:\ > c:\tefflp\extproc.out"'): -- Microsoft Выполнение этих команд операционной системы возможно при наличии та- таких же прав, как у листенера Oracle Net, запустившего процесс extproc. Мы дума- думаем, что администратор базы данных (или администратор, отвечающий за безопас- безопасность) вряд ли захочет изменить эту ситуацию. Механизм вызова внешних процедур Рассмотрим, что происходит в результате вызова внешней процедуры. Для этого вернемся к примеру предыдущего раздела, при выполнении которого использу- используется стандартный «агент» внешней процедуры. Когда исполняющее ядро PL/SQL в результате анализа откомпилированного кода обнаружит, что программа реализована вне Oracle, оно будет искать сервис TNS с именем EXTPROCCONNECTIONDATA. Он должен быть описан в одном из конфи- конфигурационных файлов Oracle Net (например, в файле tnsnames.ora). Как показано на рис. 23.1, листенер Oracle Net реагирует на запрос запуском для сеанса особого процесса, называемого extproc, которому он передает путь к файлу DLL, имя функ- функции и ее аргументы. Этот процесс динамически загружает совместно используе- используемую библиотеку, передавая ей нужные аргументы, принимает выходные данные и возвращает их вызывающему коду. При этом для конкретного сеанса Oracle вы- выполняется только один процесс extproc - он стартует при первом вызове внешней процедуры и завершается по окончании сеанса. Для каждой вызываемой внеш- внешней процедуры процесс extproc загружает соответствующую совместно исполь- используемую библиотеку (если это еще не сделано). С целью повышения эффективности использования внешних процедур в Oracle применяются перечисленные ниже специальные технологии и средства. О Совместно используемая DLL. Внешняя С-программа должна быть реализо- реализована в виде совместно используемой библиотеки динамической компоновки, а не статически скомпонованного модуля. Конечно, если компоновка откла- откладывается до этапа выполнения, то это требует определенных затрат. Однако тем. самым обеспечивается более эффективное использование памяти - за счет того, что одна библиотека может применяться в нескольких сеансах. Операци- Операционная система позволяет нескольким процессам совместно использовать не- некоторые страницы памяти библиотеки. Еще одним преимуществом динамиче- динамически компонуемых модулей является то, что создавать и обновлять их легче,
896 Глава 23 • Внешние процедуры чем статические программы. Кроме того, совместно используемая библиотека может содержать несколько подпрограмм (отсюда и термин «библиотека»), что также снижает издержки, поскольку требуется динамически загружать мень- меньшее количество файлов. О Отдельное пространство памяти. Внешние процедуры Oracle выполняются в области памяти, независимой от процессов ядра базы данных. Поэтому, если внешняя процедура завершается аварийно, это не оказывает никакого влия- влияния на память ядра — процесс extproc просто возвращает код ошибки ядру PL/SQL, а оно передает сообщение о ней приложению. Хотя возможность на- написания внешней процедуры, которая может вызвать сбой работы сервера Oracle, существует, создать ее не легче, чем процедуру PL/SQL О Полная поддержка транзакций. Внешние процедуры полностью поддержива- поддерживают транзакции. Это означает, что они могут быть задействованы в текущей транзакции. Получив от PL/SQJL информацию «контекста», процедура может возвращать в базу данных записи, выполнять SQL- или PL/SQL-запросы и инициировать исключения. Все это требует низкоуровневого программирова- программирования с использованием интерфейса уровня вызовов РСУБД (Oracle Call Inter- Interface, OCI), но главное, что такая возможность существует. Приложение Сервер Oracle Oracle Net Внешняя совместно используемая библиотека Клиентское или серверное приложение Рис 23.1. Вызов внешней процедуры Особенности выполнения внешних процедур в Oracle Хотя внешние процедуры обеспечивают массу преимуществ, их использование сопровождается ростом издержек. Объясняется это тем, что выполнение внеш- внешних процедур в Oracle требует интенсивного взаимодействия процессов. Послед- Последнее вызвано необходимостью разграничения пространств памяти сервера базы данных и процедуры. Проблемы могут возникнуть также из-за того, что каждому
Конфигурирование Oracle Net сеансу, вызывающему внешнюю процедуру, в дополнение к вторичному фо^ му процессу, применяемому для подключения к серверу базы данных, необ}^ в _ собственный процесс extproc. Все это приводит к дополнительным затрату. ^ сурсов, а возможность совместного использования процесса extproc нескол!,,. ^ сеансами, к сожалению, отсутствует. ^ Конфигурирование Oracle Net Рассмотрим, как настраиваются параметры конфигурации, отвечающие за в^ нение внешних процедур и повышающие эффективность системы защиты. Настройка параметров конфигурации листенера Соединение между PL/SQL и совместно используемыми библиотеками ог,еспе- чивается средствами компоненты Oracle Net. Хотя стандартная конфигурация Oracle8i и более поздних версий предусматривает определенную поддержку ццеи]- них процедур, вы, скорее всего, не захотите ограничиться предлагаемыми по умол- чанию возможностями до тех пор, пока разработчики Oracle не усовершенствует систему защиты. На момент написания этой книги в Oracle по-прежнему недостаточно учит*1' вался тот факт, что выполнение внешних процедур является слабым местом сис- системы защиты базы данных. В частности, удаленный несанкционированный полк зователь может подключиться к базе данных через порт TCP/IP листенера (обьиН0 это порт 1521) и без аутентификации запустить процесс extproc. Чтобы избежа*6 этой проблемы, советуем запускать листенеры Oracle за брандмауэром и не ос тавлять доступным порт листенера, предназначенный для соединения с Интерн^ том или другой незащищенной сетью. Для настройки параметров листенера требуется модифицировать файлы ^5П mes.ora и Iistener.ora (вручную или с помощью Oracle Net Manager). ПривеДс _ пример простого файла listener.ora, в котором параметры листернеров, пред11азН* ченных для внешней процедуры и для базы данных, устанавливаются отд[;ЛЬН LISTENER - (ADDRESS = (PROTOCOL = TCPHHOST = иия_хоста)(PORT - 1521)) EXTPROCJ.ISTENER - (ADDRESS = (PROTOCOL - IPCHKEY = Kim_npotiegypu_extproc)) SIO_L1ST_LISTENER - (SIDJESC ¦= (GLOBALJBNAME - гпобапьное_иия) (ORACLE_HOME - каталог_0racle) (SID NAME - SID) sid_list_extprocj.istene:r (SID_OESC - (SID_NAME = 5_ (ORACLE_HOHE - каталог Oracle)
898 Глава 23 • Внешние процедуры (?№15-"ЕИРЯ0С_0И5-Ои1У;список_файРО8_соеместно_исполиуеных_обье1<то8') (PROGRAM - extproc) ) Назначение параметров, использованных в этом файле, описано ниже. О ключ_процецуры_ех1ргос — короткий идентификатор, используемый Oracle Net для того, чтобы отличить данный листенер от других листенеров, участвую- участвующих в межпроцессорном взаимодействии (InterProcess Communication, IPC). Это имя можно выбирать произвольным образом, поскольку программы нико- никогда его «не увидят». По умолчанию при первой инсталляции Oracle Net на кон- конкретной машине используется имя EXTPROCO. В списках ADDRESS файлов trtsna- mes.ora и llstener.ora этот идентификатор должен быть одинаковым. О имя_хостд — имя или IP-адрес машины. Данный параметр не применяется к внешним процедурам, которые выполняют прослушивание только через IPC. О кдТдлог_0гас1е — полный путь к каталогу, в котором установлена система Oracle (ORACLE_HOME), предположим /uOl/app/orade/product/9.2 (в UNIX) или же C:\ORACLE\ora92 (в Microsoft Windows). Обратите внимание, что имя каталога не заключено в кавычки и не завершается косой чертой. О 510_внешней_процеяурь> — это произвольный уникальный идентификатор листе- нера внешней процедуры. В стандартной конфигурации Oracle для него ис- используется значение PLSExtProc. О глобальное_иня — полностью определенное имя базы данных. Этот элемент не относится к внешним процедурам. О ENVS-"EXTPROC_DI_LS-ONLY:список_файлов_совместно_используемых_объектов" В Огас1е9г Release 2 предложение ENVS устанавливает переменные окружения для листенера. В нашем примере переменной EXTPROC_DLLS присваивается зна- значение, обеспечивающее максимальный уровень защиты, — ключевое слово ONLY. Оно разрешает выполнение только тех совместно используемых библиотек, которые перечислены в списке. Ниже приведен пример установки переменных среды для системы Solaris: (ENVS="EXTPROCJLLS-CNLY:/uOl/app/oracle/admi n/1ocal/1ib/extprocsh.so:/uOl/app/ oracle/admin/local/lib/RawdataToPrinter.so") А вот пример для системы Windows XP: CENVS="EXTPROC_DLLS»ONLY:c:\oracle\admin\local\lib\extprocsh.dll.-c:\oracle\admin\ local UibXRawDataToPrinter.dll") Заметьте, что двоеточие в данном случае применяется не только в качестве разделителя элементов списка, но и для того, чтобы отделить букву, обозна- обозначающую диск (как это принято в MS-DOS). Хотя в этом списке названы всего две библиотеки, вы, конечно же, можете задавать любое их количество. Если при работе с Огас1е9г Release 2 вы не зададите список значений в предло- предложении ENVS, Oracle позволит вызывать только те библиотеки, которые располага- располагаются в одном, специально предназначенном для них каталоге Oracle. Для системы Windows это будет каталог bin, а для системы UNIX — каталог lib. Когда в значе- значении переменной EXTPROC_DLLS не указано ключевое слово ONLY, но задан список
Конфигурирование Oracle Net 899 файлов библиотек, помимо указанных библиотек для выполнения будут доступ- доступны и те, что хранятся в подкаталогах каталога Ып или lib. Ну а если вы решите пренебречь безопасностью, можете задать ключевое сло- слово ANY, что позволит вызывать любые совместно используемые библиотеки, дос- доступные пользователю операционной системы, выполняющему листенер внешней процедуры. Уровень защиты, установленный в данной конфигурации Заданные нами параметры конфигурации с точки зрения обеспечения безопасно- безопасности дают следующие преимущества: О системный администратор может запускать листенер внешней процедуры от имени пользователя с учетной записью, предоставляющей ограниченные пра- права. По умолчанию листенер выполняется от имени учетной записи, запустив- запустившей сервер Oracle; О функции листенера внешней процедуры ограничены приемом 1РС-запросов от локальной машины (возможность получения межсетевых запросов, посту- поступающих по протоколу TCP/IP, отсутствует). Настройку параметров листенера можно продолжить, добавив в файл tnsna- гпе5.ога для базы данных, из которой производится вызов, следующую запись; EXTPROC_CONNECTION_DATA - (DESCRIPTION - (ADDRESS - (PROTOCOL - IPCHKEY - ключ_процедуры_ех1ргос)) (CONNECT_DATA - (SID - SIO_npoueuypn_extprac) (PRESENTATION - RO)) ) Большая часть этих установок вам уже знакома из приведенного выше описа- описания конфигурации листенера. Обратите внимание на то, что значения ключ_проце- sypbi_extproc и SID_npoiiegypbi_extproc в файле настройки листенера должны соот- соответствовать значениям в данном файле. Заданное нами значение параметра RE- REPRESENTATION предназначено для повышения производительности, оно указывает серверу, который поддерживает прослушивание по нескольким протоколам, что клиент будет взаимодействовать с ним по протоколу RemoteOps (RO). Внимательно проверьте права, предоставленные учетной записи дополнитель- дополнительного листенера, особенно права на модификацию файлов операционной системы или файлов пользователя с учетной записью oracle. Кроме того, задав значения переменной окружения TNSADMIN в UNIX (в Microsoft она устанавливается в рее- реестре), файлы tnsnames.ora и listener.ora листенера внешней процедуры можно по- поместить в отдельный каталог. ПРИМЕЧАНИЕ- Профессионалы должны следить за информацией о проблемах защиты Oracle, публикуемой на стра- странице http://otn.oracle.com/deploy/security/alerts.htm. Упомянутая в разделе «Настройка параметров конфигурации листенера» проблема, связанная с внешними процедурами, описана на этой страни- странице под номером 29, но рекомендуем вам просмотреть весь список, чтобы заранее принять все воз- возможные меры защиты и установить необходимые «заплаты».
900 Глава 23 • Внешние процедуры Настройка указанных выше конфигурационных файлов и создание дополни- дополнительной учетной записи пользователя операционной системы может показаться до- дополнительной нагрузкой, однако вопрос защиты настолько важен, что вам придется это делать. Создание библиотеки Oracle Инструкция CREATE LIBRARY определяет в словаре данных Oracle псевдоним файла внешней совместно используемой библиотеки, что позволяет исполняющему ядру PL/SQL найти эту библиотеку во время вызова. Создавать библиотеки разреше- разрешено только администраторам и пользователям с правами CREATE LIBRARY или CREATE ANY LIBRARY. В общем случае инструкция CREATE LIBRARY имеет такой вид: CREATE [ OR REPLACE ] LIBRARY иняОиблиотеки AS 'луть_к_файпу' [ AGEWT 'свяэь_базы_данных_агента'У, Здесь и«я_библиотехи — допустимый идентификатор PL/SQL. Это имя будет при- применяться в теле внешних процедур, которым нужно вызвать совместно исполь- используемый объектный файл (DLL). Имя библиотеки не может совпадать с именем таблицы, объекта PL/SQL верхнего уровня или другим именем в основном про- пространстве имен. Параметр путь_к_файпу — полный путь к совместно используемому объектно- объектному файлу (DLL). В Огас1е9г появилась возможность применять в параметре путь_ к_файлу переменные окружения. В частности, если переменная устанавливается до запуска листенера, ее можно указать в инструкции CREATE LIBRARY. Например: CREATE LIBRARY extprocshelljib AS 'S{ORACLE_HOME}/lib/extprocsh.so'; --UNIX CREATE LIBRARY extprocshelljib AS '*0RACLEJ0ME3;\Din\extprocsh.dir: --Microsoft Это удобно с точки зрения переносимости сценария, хотя, возможно, не очень хорошо с точки зрения безопасности. Имеется и другой способ обеспечить возможность применения переменных при создании библиотек, а именно путем добавления их в список значений пара- параметра ENVS листенера внешней процедуры. Так, в системе UNIX, для того чтобы задействовать переменную MYLI8S, можно использовать такую строку: CENVS="EXTPROC_DLLS=ANY,MYLIBS=/usr/local/extproclib') Обратите внимание, что значение переменной окружения отделено от пути к библиотекам символом запятой. Теперь библиотеку можно создать с помощью следующей инструкции: CREATE LIBRARY extprocshelljib AS '$(MYLI8S}/extprocsh.so': --UNIX Практика показала, что помещать переменную в список значений параметра ENVS и ссылаться на нее при создании библиотеки можно только в системе UNIX (в Microsoft Windows XP это не работает). Возможно, вы захотите установить в системе UNIX еще одну переменную — LD_LIBRARY_PATH или ее эквивалент, чтобы предоставить внешней процедуре путь
Вызов внешней функции ¦ 901 для поиска совместно используемых библиотек, который отличается от приме- применяемого по умолчанию. Параметр AGENT 'связь_базы_данных_агента' в инструкции CREATE LIBRARY — это не- необязательный параметр, задающий соединение с базой данных (в версии Oracle9z и выше), к которой имеет доступ владелец библиотеки. Это соединение должно быть ассоциировано с именем сервиса для внешней процедуры. Предложение AGENT позволяет внешней процедуре выполняться на другом сервере базы данных, хотя физически она может располагаться на том же компьютере. Пользуясь инструкцией CREATE LIBRARY, имейте в виду следующее: О инструкция должна выполняться администратором базы данных либо пользо- пользователем С правами CREATE LIBRARY или CREATE ANY LIBRARY; О как большинство других объектов базы данных, библиотека принадлежит кон- конкретному пользователю (схеме) Oracle, который автоматически получает право на ее выполнение и может предоставлять и отменять такое право для других пользователей; О пользователи, получившие на библиотеку право EXECUTE, могут ссылаться на нее в спецификациях вызовов, применяя синтаксис владелец .библиотека, или создавать и использовать для нее синонимы; О выполняя инструкцию CREATE LIBRARY, Oracle не проверяет, существует ли ука- указанный в ней файл. Такая проверка не производится и позднее, когда объявля- объявляется внешняя процедура для функции этой библиотеки. Поэтому, если путь содержит ошибку, вы узнаете об этом только при первом выполнении функции. Для каждой совместно используемой библиотеки необходимо создать одну библиотеку Oracle. Совместно используемая библиотека может включать множе- множество функций С, и обращения к ней могут содержать несколько спецификаций вызова. Ниже подробно описан процесс создания подпрограммы PL/SQL, которая бу- будет служить оболочкой библиотечной функции, вызываемой из Oracle. Вызов внешней функции Вызов функции может быть задан в процедуре или функции PL/SQL верхнего уровня, процедуре или функции пакета либо в методе объекта. Более того, специ- спецификация вызова может определяться как в спецификации, так и в теле пакета, или же в спецификации либо в теле объектного типа. Приведем несколько при- примеров. CREATE FUNCTION имя (.аргументы) RETURN тип_возвращаеных_данных AS спецификация вызова; Вы, вероятно, узнали в первом примере функцию shell О, описанную ранее в этой главе. Можно создать и процедуру: CREATE PROCEDURE пня AS спецификация_8ызо8а; В данном случае соответствующая функция С должна быть объявлена как void.
902 Глава 23 • Внешние процедуры Далее представлена пакетная функция, для которой не требуется создавать тело пакета: CREATE PACKAGE тя_пакета AS FUNCTION пня RETURN типвозвращаеных^панных AS спецнфикдция_аызова; END; Но учтите, что при модификации пакета спецификацию вызова придется пе- перекомпилировать. В зависимости от объема выполненных изменений, можно зна- значительно уменьшить количество объектов, подлежащих перекомпиляции, пере- переместив спецификацию'вызова в тело пакета: CREATE PACKAGE иня_пакета AS PROCEDURE имя; END: CREATE PACKAGE BODY имя_пакетз AS PROCEDURE имя; IS спецификзция_аызова; END; Применяемые внутри пакета личные или не являющиеся общедоступными программы также могут быть реализованы как внешние процедуры. В методе объект- объектного типа спецификация вызова определяется так же, как в пакете, то есть ее можно поместить либо в спецификацию, либо в тело объектного типа. Спецификация вызова Спецификация вызова задается в предложении AS LANGUAGE1 и выглядит следую- следующим образом: AS LANGUAGE С LIBRARY иня_библиотеки [ NAME имя_внешией_фуны1ии} [ WITH CONTEXT ] [ AGENT IN (имя_форнального_паранетра)~} [ PARAMETERS (.список_внешиих_паранетров)] Назначение элементов данной спецификации описано ниже. О AS LANGUAGE С - альтернатива параметру AS LANGUAGE JAVA, который был рас- рассмотрен в главе 22. О LIBRARY иня_библнотеки — имя библиотеки, определенное с помощью инструк- инструкции CREATE LIBRARY, на которую у вас имеется право на выполнение. О NAME имя_внешней_функции — имя функции в том виде, как оно задано в С-биб- лиотеке. Но если имя представлено символами нижнего или обоих (верхнего и нижнего) регистров, оно должно быть заключено в двойные кавычки. Если В Oracle 8.0 этого предложения не было, вместо aero использовалась не поддерживаемая теперь форма AS EXTERNAL.
Вызов внешней функции 903 данный параметр опущен, в этом случае имя внешней процедуры должно сов- совпадать с именем, указанным для соответствующего ей модуля PL/SQL (по умолчанию представлено символами верхнего регистра). О WITH CONTEXT - наличие этого предложения указывает, что PL/SQL должен пе- передавать вызываемой программе указатель на «контекст», а вызываемая про- программа должна принять этот указатель в качестве параметра типа OCIExtProc- Context* (определен в заголовочном С-файле oclextp.h). «Контекст» представ- представляет собой структуру данных, которая содержит разнообразную информацию, специфическую для Oracle. О AGENT IN (имя_ф()рмального_пдрднетра) — это предложение, подобно предложению AGENT в определении библиотеки, задает другой процесс агента, Однако оно по- позволяет отложить выбор агента до этапа выполнения. Имя агента сначала пере- передается в качестве параметра функции-оболочке PL/SQL, а из нее — во внешнюю функцию. При выполнении этой функции переданное ей имя агента заменяет- заменяется именем, заданным для библиотеки (если оно было задано). Имя соответст- соответствующего формального параметра функции PL/SQL указывается в предложе- предложении AGENT IN. О PARAMETERS (список_внешних_пгранегров) — в этом предложении определяются ти- типы данных параметров, которыми обмениваются PL/SQL и С. О слисок_внешних_лэрдметров — это список элементов, разделенных запятыми. По- Позиции параметров в списке и в объявлении функции С должны совпадать. Дан- Данный список представляет собой самую сложную часть спецификации, требую- требующую подробного разъяснения. Список внешних параметров Выполняя обмен данными между PL/SQ.L и С, необходимо учитывать следую- следующие особенности. Во-первых, PL/SQL имеет собственные типы данных, лишь частично совпадающие с типами данных С. Поскольку переменные PL/SQL мо- могут принимать значение NULL, то булевы (логические) переменные в этом языке могут иметь три значения. Для переменных языка С это не применимо. Во-вто- Во-вторых, библиотека С может «не знать», набор символов какого языка используется для представления буквенно-цифровых значений. Кроме этого, необходимо выяс- выяснить, каким методом должен быть передан аргумент С-функции: по значению или по ссылке (то есть в виде указателя). Вернемся к приведенному в начале главы примеру, где используется функция shell О. Для нее не предусмотрен вызов с аргументом NULL, который может быть задан вместо существующей команды. Поэтому результатом вызова функции shelICNLJLLJ будет ошибка времени выполнения ORA-01405: fetched column value is NULL. Возможно, существуют приложения, для которых такого сообщения доста- достаточно, но мы потребуем, чтобы внешняя процедура тоже возвращала NULL в ответ на входное значение NULL. Чтобы в языке С можно было корректно распознать значение NULL, во внеш- внешнюю программу из PL/SQL требуется передать дополнительный параметр, назы- называемый переменной-индикатором. Аналогичным образом, для того чтобы про- программа С могла возвратить Oracle значение NULL, она также должна передать
904 Глава 23 • Внешние процедуры в PL/SQL дополнительный параметр-индикатор. И если со стороны PL/SQL эти значения устанавливает и интерпретирует РСУБД Oracle, то функция С должна получать и устанавливать их явно. Вот как изменится спецификация функции shel 1 в PL/SQJL после реализации поддержки значения NULL; CREATE OR REPLACE FUNCTION shel Hand IN VARCHAR2) RETURN PLSJNTEGER AS LANGUAGE С LIBRARY extprocshelljib NAME ¦extprocsh" PARAMETERS {and STRING, cmd INDICATOR, RETURN INDICATOR. RETURN INT): Формальные параметры, заданные для функции PL/SQL, могут располагать- располагаться в любом месте списка PARAMETERS. Однако сам этот список должен в точности (то есть по позиции и типу данных) совпадать со списком параметров функции С. Возвращаемое значение, идентифицируемое в этом списке ключевым словом RE- RETURN, должно стоять последним. Если вы согласны на порядок сопоставления по умолчанию, возвращаемый параметр можно и не задавать, но индикатор должен присутствовать обязательно: CREATE OR REPLACE FUNCTION shell(cmd IN VARCHARZ) RETURN PLS INTEGER AS LANGUAGE С LIBRARY extprocshelljib NAME "extprocsh" PARAMETERS (and STRING, cmd INDICATOR, RETURN INDICATOR): Следует отметить, что хотя мы и внесли в спецификацию функции некоторые изменения, они не видны коду, вызывающему функцию shel 10, — для него коли- количество и типы данных параметров остались прежними. Теперь рассмотрим новую версию программы на языке С, в которую добавле- добавлены два параметра, по одному для каждого индикатора: 1 finclude <ociextp.h> г 3 int extprocsMchar *crad, short cmdlnd, short *retlnd) 4 { 5 if (cmdlnd — OCI IND_NOTNULL) 6 { 1 *retlnd = (shortHCIJND_NOTNULL: 8 return system!cmd): 9 } else 10 { П *retlnd - (short)OCIJND_NULL: 12 return 0; 13 ) 14 }
Вызов внешней функции 907 Ниже кратко описаны особенности этой программы. Строка Описание 1 Данный подключаемый файл (Include file) в операционных системах Microsoft располагается в подкаталоге %ORACLE_HOME%\ociVndude, а на большинстве UNIX-машин он хранится в каталоге $ORACLEJHOME/rdbms/demo (в вашей системе он может оказаться и в другом месте} 3 Обратите внимание на то, что индикатор параметра cmd имеет тип short, а индикатор возвращаемого значения — short*. Это соответствует соглашению о передаче параметров, согласно которому входные параметры из PL/SQL в С передаются по значению, а выходные параметры и возвращаемое значение из С в PL/SQL — по ссылке 5, 7 Переменная-индикатор может иметь значение либо OCI_IND_NULL, либо Oa_IND_NOTNULL Это специальные значения, которые определены с помощью директив #deftne в подключаемом файле Oracle. Как видите, индикатору возвращаемого значения явно присваивается одно из двух значений 11,12 Если индикатор возвращаемого значения указывает, что функиия вернула NULL, возвращаемое значение 0 просто игнорируется Далее показано несколько простых команд, которые выполняют компиляцию и компоновку приведенной ранее программы. Для UNIX (как и раньше с помо- помощью компилятора GNU С): дсс -с -IS{ORACLE_HOME}/rdtms/deino -I$ORACLE_HOME/rdbms/public extprocsh.c дсс -shared -о extprocsh.so extprocsh.o А вот эквивалентные команды для Windows (с использованием Cygwin-ком- пилятора дсс): дсс -I/cygdrive/c/oracle/ora92/oci/include/ -с extprocsh.c дсс -shared -о extprocsh.dll extprocsh.o Теперь вы готовы перейти к более сложным вопросам, касающимся соответст- соответствия параметров. Сопоставление параметров В предыдущем разделе говорилось, что при обмене данными между PL/SQL я С каждый тип данных PL/SQL ставится в соответствие внешнему типу данных, идентифицируемому ключевым словом PL/SQL, которое, в свою очередь, соот- соответствует определенному набору типов С: Типы данных PL/SQL <-> Внешние типы данных <-» Типы данных С Внешние типы данных идентифицируются в предложении PARAMETERS с помо- помощью ключевых слов PL/SQL, которые в ряде случаев (но не всегда) совпадают с названиями типов данных С. Например, при передаче переменной PL/SQL типа PLS_INTEGER ей назначается внешний тип INT, соответствующий типу данных int языка С. Однако типу данных Oracle VARCHAR2 соответствует внешний тип данных STRING, обычно сопоставляемый с типом char* языка С. Все возможные варианты преобразования типов данных, которые поддержи- поддерживает Oracle-интерфейс, обеспечивающий взаимодействие между PL/SQL и С, пе- перечислены в табл. 23.1. Обратите внимание, что допустимость преобразования
DHt^UHHt: I зависит и от типов данных, и от режима передачи формального параметра PL/SQ.L (это показывает предыдущий пример). В тех случаях, когда возможно сопостав- сопоставление с несколькими типами данных, используемый по умолчанию тип выделен жирным шрифтом. Таблица 23.1. Допустимые сопоставления типов данных PL/SQL и С Тип данных параметра PL/SQL Ключевое слово PL/SQL, идентифици- идентифицирующее внешний тип Типы данных С для параметров PL/SQL, которые используются в режиме IN и для в режиме IN OUT, OUT возвращаемого и для параметров, функцией обозначенных как значения BY REFERENCE Семейство длинных INT, UNSIGNED INT, целых: CHAR, UNSIGNED CHAR, BINARYJNTEGER, SHORT, UNSIGNED BOOLEAN, PLSJNTEGER SHORT, LONG, UNSIGNED LONG, S61, UB2, SB2, UB2, SB4, UB4, SEET Int, unsigned Int, To же, что слева, char, unsigned char, но используются short, unsigned указатели (например, по short, long, умолчанию применяется unsigned long, sbl, не Int, a Int*) ub2, sb2, ub2, 5b4, ub4, size_t Семейство коротких целых: NATURAL, NATURALN, POSITIVE, POSITIVEN, SIGNTYPE Семейство символьных типов: VARCHAR2, CHAR, NCHAR, LONG, NVARCHAR2, VARCHAR, CHARACTER, ROWID To же, что и выше, с той разницей, что по умолчанию используется UNSIGNED INT То же, что и выше, с той разницей, что по умолчанию используется unsigned int То же, что выше, с той разницей, что по умолчанию используется unsigned Int* STRING, OCISTRING diar*, odstring char*, odstring NUMBER DOUBLE PRECISION FLOAT, REAL RAW, LONG RAW DATE Семейство TIMESTAMP: TMESTAMP, TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE INTERVAL DAY TO SECOND, INTERVAL YEAR TO MONTH BFILE, BLOB, CLOB OCINUMBER DOUBLE FLOAT RAW, OdRAW OCIDATE OCIDATETIME OCIINTERVAL OCILOBLOCATOR OCINumber* double* float unsigned char*, OCIRaw* OQDate* OCIDatetlme* OOInterval* OCILOBLOCATOR* OCINumber* double* float* unsigned char*, OCIRaw* OCIDate* OClDatetime* OCIInterval* OCIL08LOCATOR*
оьииы внешней функции Тип данных параметра PL/SQL Дескриптор типа, определяемого пользователем (коллекции или объекта) Значение определяемой пользователем коллекции Значение определяемого пользователем объекта Ключевое слово PL/SQL, идентифици- идентифицирующее внешний тип TDO OCICOLL DVOID " 7 Типы данных С для параметров PL/S1JL, . которые используются в режиме IN и для в режиме IN OUT, OU1 возвращаемого функцией значения ООТуре* oacoii**, ООАгтау", ОСПаЫе** dvoid* и для параметров, обозначенных как BY REFERENCE ОСГТуре* OCICotl**, OCIArray** ОСГГаЫе** dvoid* д/w типов final; dvoid** для типов не-Rnal В некоторых простых случаях, когда передаются только числовые аргументI и допускается применять типы, назначаемые по умолчанию, предложение PARA METERS можно вообще не задавать. Однако при передаче индикаторов или други> свойств данных его применение необходимо. Каждый элемент дополнительной информации, которая используется при об' мене с внешней функцией, передается посредством отдельного параметра и зада' ется как в предложении PARAMETERS, так и в спецификации функции на языке С- Синтаксис предложения PARAMETERS В предложении PARAMETERS задается список, который может содержать следующий разделенные запятыми элементы: О имя параметра, за которым следует идентификатор внешнего типа данных; О ключевое слово RETURN и связанный с ним идентификатор внешнего типа; О «свойство» параметра PL/SQL или возвращаемого значения, например инди катор NULL или целое число, соответствующее длине параметра; О ключевое слово CONTEXT, задающее позицию, в которой передается указателе контекста; О ключевое слово SELF (в случае применения внешней процедуры для метода' члена объектного типа). Если в спецификации вызова нет ключевого слова CONTEXT, то она выглядит следующим образом: { мня | RETURN | SELF } [ свойство ][ BY REFERENCE ] [ в«ейиии_гип_да«ных ] При наличии предложения WITH CONTEXT соответствующий элемент в списке параметров задается одним словом: CONTEXT
908 Глава 23 • Внешние процедуры Кроме того, при использовании предложения WITH CONTEXT ключевое слово CON- CONTEXT принято располагать в списке параметров первым, поскольку именно там оно должно находиться в случае, когда сопоставление остальных параметров про- производится по умолчанию. Элементы списка параметров имеют следующее назначение. О имя | RETURN | SELF — имя параметра из списка формальных параметров моду- модуля PL/SQL, ключевое слово RETURN или SELF (в случае использования мето- метода-члена объектного типа). Имена параметров PL/SQL и параметров в про- программе на языке С не обязательно должны быть одинаковыми, однако поря- порядок параметров должен совпадать. О свойство - одно из следующих ключевых слов: INDICATOR, INDICATOR STRUCT, LENGTH, MAXLEN, TOO, CHARSETID или CHARSETFORM (они будут описаны в следующем разделе). О BY REFERENCE — указывает, что передача параметров производится по ссылке, то есть модуль в совместно используемой библиотеке получает не значение параметра, а указатель на это значение. Установка BY REFERENCE важна только для скалярных параметров, которые используются в режиме IN и не являются строками, то есть принадлежат к типу BINARYJNTEGER, PLSJNTEGER, FLOAT, DOUBLE PRECISION или REAL. Данные других типов (применяемые в режиме IN OUT или OUT, а также параметры IN типа STRING) всегда передаются по ссылке, и в соот- соответствующем прототипе С для них должен быть задан указатель. О внешннй_тнп_дднных — ключевое слово из второго столбца табл. 23.1, которое задает внешний тип данных. Если его не указать, будет использоваться внеш- внешний тип данных по умолчанию, приведенный в этой таблице. Свойства параметров В этом разделе описаны различные свойства, которые могут задаваться в предло- предложении PARAMETERS. Свойство INDICATOR Это свойство представляет собой флаг, указывающий на то, имеет ли параметр значение NULL. Допустимыми для данного свойства являются следующие характе- характеристики. О Внешние типы, которые используются при объявлении переменной-индика- переменной-индикатора — short (по умолчанию), int и long. О Типы PL/SQJL — все скалярные типы данных. При передаче переменной-ин- переменной-индикатора для составного типа данных, такого как пользовательский объект или коллекция, применяется свойство INDICATOR STRUCT. О Режимы использования параметров PL/SQL - IN, IN OUT, OUT и RETURN. О Методы передачи параметров внешней программе — по значению для пара- параметров IN (если не задано BY REFERENCE) и по ссылке для параметров IN OUT, OUT и RETURN.
Вызов внешней функции Таким образом, свойство INDICATOR можно задавать для параметров в любе*, режиме, включая RETURN. Если индикатор не указан, PL/SQ.L считает, что внеш- внешняя программа никогда не возвращает значение NULL (так бывает не во всех случа- случаях; см. врезку «Индикация без индикаторов»). Если внешней процедуре передать переменную в режиме IN с индикатором. Oracle присвоит значение индикатора автоматически, но если возвращаемый мо- модулем параметр имеет режим RETURN или ОПТ, то значение индикатора следует ус- установить в коде С. Для параметра IN соответствующий параметр-индикатор в функции С можно объявить таким образом; short plndicatorFoo а для параметра IN OUT это можно сделать так: short *pIndicatorFoo В теле функции С следует использовать определенные с помощью директивы #define константы OSI_INO_NOTNULL и OSI_INQ_NULL, которые будут доступны, если посредством директивы #i nci tide включить в программу файл oci.h. Oracle опреде- определяет их так: typedef sb2 OCIInd: #define OSIJNOJJOTNULL {OCIInd>0 /* not NULL */ #define OS I IND NOTNULL (OCIIndH-1) /* NULL */ ИНДИКАЦИЯ БЕЗ ИНДИКАТОРОВ Что произойдет, если не задать переменную-индикатор для строки и вер- вернуть пустую строку С? Рассмотрим простую программу: void mynuTKchar *outbuff) ( OUtbuffLO] « '\(Г; } Спецификация вызова этой программы имеет следующий вид: CREATE OR REPLACE PROCEDURE mynul! <res OUT VARCHAR2) AS LANGUAGE С LIBRARY mynul11ib NAME "mynull"; / При вызове такой внешней процедуры PL/SQL интерпретирует значение параметра как NULL. Оказывается, внешний тип данных STRING обладает не- некоторыми особенностями: для того чтобы вернуть Oracle значение NULL, достаточно передать строку длиной 2 байта, в которой первый байт равен \0. Однако учтите, что это работает только при отсутствии свойства LENGTH. Но мы все же рекомендуем не лениться и использовать явный индикатор.
110 Глаза 23 • Внешние процедуры ;войство LENGTH Это свойство представляет собой целочисленное значение, которое задает коли- [ество символов в символьном параметре. Допустимыми для данного свойства шляются следующие характеристики. Э Внешние типы, которые используются при объявлении переменной свойст- свойства — int (по умолчанию), short, unsigned short, unsigned int, long, unsigned long. Э Типы PL/SQL - VARCHAR2, CHAR, RAW, LONG RAW. Э Режимы использования параметров PL/SQL - IN, IN OUT, OUT и RETURN. Э Методы передачи параметров внешней программе - по значению для пара- параметров IN (если не указано BY REFERENCE) и по ссылке для параметров IN OUT, OUT и RETURN. Свойство LENGTH обязательно для параметров типа RAW и LONG RAW, а для других символьных типов данных поддерживается просто из соображений удобства про- программирования на С. При передаче значения типа RAW из PL/SQL в С значение свойства LENGTH присваивается автоматически. Однако для того чтобы программа на языке С возвращала в PL/SQ.L значение типа RAW, необходимо явно установить в ней значение этого свойства. Для параметра IN переменную свойства LENGTH в программе С можно объявить следующим образом: int plenFoo а для параметра IN OUT это можно сделать так: int *pLenFoo Свойство MAXLEN Данное свойство — это целочисленное значение, которое задает максимальное количество знаков символьного параметра. Допустимыми для данного свойства являются следующие характеристики. О Внешние типы, используемые при объявления переменной свойства — Int (по умолчанию), short, unsigned short, unsigned int, long и unsigned long. О Типы PL/SQJ. - VARCHAR2, CHAR, RAW и LONG RAW; О Режимы использования параметров PL/SQL - IN OUT, OUT и RETURN; О Методы передачи параметров внешней программе — только по ссылке. Свойство MAXLEN применимо только к параметрам, имеющим режим IN OUT или OUT. Если попытаться задать его для параметра IN, Oracle выдаст сообщение об ошибке компиляции PLS-00250: Incorrect usage of MAXLEN in parameters clause. В от- отличие от параметров со свойством LENGTH, данные, для которых задается свойство MAXLEN, всегда передаются по ссылке. В программе на С переменная для данного свойства может быть объявлена следующим образом: int *pMaxLenFoo
Инициирование исключений из программ на языке с Свойства CHARSETID и CHARSETFORM Эти два свойства представляют собой флаги, которые применяются для передачи информации о наборе символов языка. Допустимыми для данных свойств явля- являются следующие характеристики. О Внешние типы, используемые при объявлении переменной свойства — unsig- unsigned int (по умолчанию), unsigned short и unsigned long. О Типы PL/SQL - VARCHAR2, CHAR и CLOB. О Режимы использования параметров PL/SQL - IN, IN OUT, OUT и RETURN. О Методы передачи параметров внешней программе — только по ссылке. Свойства CHARSETID и CHARSETFORM позволяют передать в программу С иденти- идентификатор и информацию о наборе символов языка, который применяется для ко- кодировки данных, передаваемых внешней процедуре. Они необходимы в том слу- случае, если набор символов не является используемым по умолчанию. Эти значения доступны только для чтения, а модифицировать их в программе нельзя. Ниже приведен пример предложения PARAMETERS с информацией о наборе символов: PARAMETERS (CONTEXT, and STRING, and INDICATOR, and CHARSETID. and CHARSETFORM): Oracle устанавливает дополнительные значения автоматически, основываясь на наборе символов, который используется для представления значения аргумен- аргумента cmd. Дополнительную информацию о поддержке языков в программах С можно найти в разделе документации Oracle, посвященном описанию интерфейса уров- уровня вызовов (OCI). Инициирование исключений из программ на языке С Программа shel 10, приведенная в качестве примера, является типичной для язы- языка С — эта функция возвращает код состояния, значение которого проверяется в вызывающей программе для определения того, успешно ли завершилась задан- заданная операция. Не лучше ли (с точки зрения PL/SQL) реализовать такую про- программу в виде процедуры, инициирующей исключение при возникновении ка- какой-либо ошибки? Чтобы ответить на этот вопрос, рассмотрим, как реализовать процедуру RAISE_APPLICATION_ERROR с использованием интерфейса уровня вызовов РСУБД Oracle. Для выполнения этой задачи помимо замены функции процедурой нужно сде- сделать следующее: О передать контекст; О указать номер ошибки из диапазона 20001-20999 и записать соответствующее сообщение; О добавить вызов служебной программы OCI, инициирующей исключение.
912 Глава IS • внешние В спецификацию вызова необходимо внести следующие изменения: /* Файл в web: extprocsh.sql */ CREATE OR REPLACE PROCEDURE shellCcmd IN VARCHAR2) AS LANGUAGE С LIBRARY extprxshelllib NAME "extprocsh" WITH CONTEXT PARAMETERS (CONTEXT, and STRING, and INDICATOR): Кроме того, из процедуры удалены возвращаемый параметр и соответствую- соответствующий индикатор, поскольку они больше не нужны. В приведенном ниже коде про- продемонстрировано, как можно получить указатель контекста и инициировать с его помощью исключение: /* Файл в web: extprocsh.с */ 1 #1ndude <ociextp.h> 2 #include <errno.h> 3 4 void extprocshCOCIExtProcContext *ctx. char *cmd. short cmdlnd) 5 { 6 int excNum - 20001; 7 char excMsg[512]: 8 size t excMsgLen; 9 10 1f (cmdlnd -= OCI_IND_NULL) 11 return; 12 13 if (s.ystem(cmd) !- 0) 14 { 15 sprintfCexcMsg, "Error Si during system call: 3i.*s". errno, 475. 16 strerror(errno)); 17 excMsgLen - (size_t)strlen(excMsg): 18 19 1f (OC[ExtProcRa1seExcpWnhMsg(ctx. excNun. Ctext *)exc«sg. excNsgLen) 20 !- OCIEXTPROCJUCCESS) 21 return; 22 } 23 24 } Особенности данного кода кратко описаны в следующей таблице. Строка Описание 4 Первый из формальных параметров — указатель контекста 6 В качестве номера ошибки можно использовать любое значение из определенного в Grade диапазона номеров пользовательских ошибок (хотя 8 общем случае не рекомеццуется кодировать эти значения жестко) 7 Максимальная длина пользовательского сообщения об ошибке равна 512 байт 8 Переменная для хранения длины текста сообщения об ошибке (ее значение применяется при формировании вызова интерфейса OCI, инициирующего исключение)
Строка Описание 10,11 это уже знакомый нам код обработки входного значения NULL. Если функции передается значение NULL, ничего делать не нужно 13 Когда возвращаемое функцией systemO значение равно нулю, это значит, что команда выполнена правильно. Ненулевое значение соответствует ошибке или предупреждению об ошибке. Более сложная программа могла бы обработать разные предупреждения в зависимости от возвращенного функцией systemO значения 15,16 Подготовка переменных, в которых задаются сообщение об ошибке и его длина 19, 20 Функция интерфейса уровня вызовов инициирует пользовательское исключение; в ней применяется указатель контекста Сначала откомпилируем данный код для UNIX. Это делается так: /* Файл в web: bu1ld_extprocsh.sh */ gcc -с -l${ORACLE_HOME}/rdbms/demo -I${0RACLE_HOHE}/rdbms/pub11c extprocsh.c gcc -shared -o extprocsh.so extprocsh.o В Microsoft Windows нужно явно указать файл .clef, чтобы идентифицировать точку входа: /* Файл в web: build_extprocsh.bat */ ECHO LIBRARY extprocsh.dll > extprccsh.def ?CHO EXPORTS » extprocsh.def ECHO extprocsh » extprocsh.def gcc -c -IX0RACLE_H0ME<\oc1\include extprocsh.c gcc -shared -o extprocsh.dll extprocsh.def extprocsh.о * SDRACLE_HOME*\oci\l1b\insvc\oci.lib Теперь выполним простое тестирование: SOL* CALL she!К'garbage'): CALL shell('garbage') * ERROR at line 1: ORA-20001: Error 2 during system call: No such file or directory Программа работает отлично. Сообщение No such file or directory об отсутст- отсутствии файла или каталога с заданным именем генерируется стандартной С-функ- цией strerrorC). Тестирование было выполнено в UNIX, в Microsoft Windows эта функция не возвращает ничего полезного, и сообщение об ошибке всегда одно и то *е: ORA-20001: Error 0 during system call. Существуют программы, специально предназначенные для вызова внешни* процедур. Их полный список приведен ниже. О DCIExtProcAllocCallMemory — выделяет память, которую Oracle автоматически освобождает при возврате управления PL/SQL; О OCIExtProcRaiseExcp -¦ инициирует предопределенное исключение по номеру ошибки Oracle; О OCIExtProcRaiseExcpWithMsg - инициирует пользовательское исключение, в тс'*1 числе пользовательский номер ошибки (как в приведенном выше примере);
О OCIExtProcGetEnv - позволяет внешней процедуре производить обратные вызо- вызовы интерфейса уровня вызовов, обращаясь х базе данных для выполнения за- запросов SQL или кода PL/SQL Все эти программы требуют наличия указателя контекста. Подробная инфор- информация о них и примеры использования содержатся в документации Oracle Appli- Application Developer Guide - Fundamentals. Агенты, отличные от используемых по умолчанию В Oracle9i появилась возможность запускать агентов внешних процедур через со- соединения базы данных с другими серверами. При этом следует иметь в виду, что отдельный процесс запускается даже при отсутствии подключения к другим сер- серверам зо время выполнения внешней процедуры с помощью агента, отличного от используемого по умолчанию. Подход с применением других серверов может быть удобен в том случае, когда ваша внешняя программа не отличается устойчиво- устойчивостью. Загрузка программы посредством другого агента означает, что аварийное завершение ее процесса extproc никак не отразится на других внешних процеду- процедурах, выполняющихся в данном сеансе. Ниже приведен пример простой конфигурации, позволяющей агенту выпол- выполняться в той же базе данных, но внутри отдельного процесса extproc. Для ее соз- создания нужно внести в файл transnames.ora дополнительную запись: agent] - (DESCRIPTION - (ADDRESS - (PROTOCOL - 1РСНКЕУ«кдау_лроуедури extproc)) (CONNECTJATA - (SID - PLSExtProc)) ) В данном случае параметр ключ_процецуры_ехЬргос может иметь то же значение, чю и в записи EXTPRQCJONNECTIONJATA. Для нового агента нужна новая связь базы данных: Sa> CREATE DATABASE LINK «jentlUnk 2 CONNECT TO user-name IDENTIFIED BY password 3 USING agentl': Теперь этот агент может быть задан в инструкции CREATE LIBRARY: CREATE OR REPLACE LIBRARY extprocsnell Hb_w1th_Bgent AS 'c:\oracle\admin\local\1ib\extprocsh,dlV AGENT 'agentl': В любом вызове, который содержит обращение к создаваемой библиотеке, бу- будут выполняться аутентификация и подключение через соединение агента agentl с порождением отдельного процесса extproc. В данном случае, как и для агента по умолчанию, при вызове процедуры несколькими пользователями Oracle порожда- порождает отдельный процесс для каждого из них. Пулинг процесса extproc невозможен.
Oracle предоставляет также возможность передать имя агента внешней проце- процедуре в качестве параметра. Для этого нужно задать предложения AGENT IN в специ- спецификации вызова. Ниже показано, как модифицировать предыдущий пример, что- чтобы включить в него явное определение агента (изменения выделены жирным шрифтом): CREATE OR REPLACE PROCEDURE shell2(rune of agent IN VARCHAR2, and IN VARCHAR2) . AS LANGUAGE С LIBRARY extprocshelijib NAME "extprocsh2" AGENT IN Cname_of agent) WITH CONTEXT PARAMETERS (CONTEXT. name_of_agent STRING, cmd STRING, aid INDICATOR): Обратите внимание, что имя агента указывается в списке параметров. Синтак- Синтаксис требует, чтобы каждому формальному параметру соответствовал элемент спи- списка PARAMETERS, поэтому нам придется модифицировать и внешнюю библиотеку С. В данном случае добавим в библиотеку вторую точку входа extprocsh.2: void extproc5h@CI?xtProcConte>rt 4trtx, char *agent. char *cmd. short mdlnd) { extprocshCctx. cmd. cmdlnd): ) Этот код просто игнорирует полученное имя агента, зато теперь процедуру shell2 можно вызвать таким образом: CALL C'agentl', 'whatever'); Если вам необходимо, чтобы хранимая программа вызывала внешнюю проце- процедуру на удаленной машине, это можно обеспечить одним из трех способов. Во-пер- Во-первых, реализовать на локальной машине внешнюю процедуру-переходник, которая будет выполнять вызов удаленной процедуры средствами С. Во-вторых, реализо- реализовать хранимую программу PL/SQL на удаленной машине как внешнюю процеду- процедуру и вызывать ее с локальной машины, используя соединение с базой данных. А третья возможность — подключиться прямо к удаленному агенту из локальной хранимой процедуры PL/SQL (эта технология, судя по всему, не поддерживается Oracle официально). Можно настроить параметры листенера внешней процеду- процедуры так, чтобы он принимал запросы на установление соединения по сети (то есть по протоколу TCP, а не IPC), но попытки авторов подключиться к удаленному агенту не имели успеха. ОТЛАДКА ВНЕШНИХ ПРОЦЕДУР Чтобы помочь разработчикам в отладке внешних процедур, Oracle пре- предоставляет сценарий dbextp.sql, который находится в каталоге plsql/demo. В результате работы данного сценария создается пакет DEBUG_EXTPROC и свя- связанная с ним библиотека debug_extproc_library. С помощью этого пакета можно подключить отладчик GNU (gdb 4.18) к выполняющемуся процес- процессу и отлаживать внешние процедуры. Далее рассказано, как авторы про- произвели эти действия сначала в системе Solaris, а затем — в Windows XP. продолжение.?
В первую очередь мы откомпилировали файл совместно используемой библиотеки, указав опцию компилятора (д), необходимую для включе- включения в результирующий файл символьной информации для отладчика, а за- затем запустили сценарий dbextp.sql и в новом сеансе SQL'Plus выполнили такую команду: SQL> EXEC OEBUG_fXTPROC.startup_extproc_agent В результате активизировался процесс extproc, идентификатор (PID) ко- которого был получен с помощью команды ps -ef. После запуска отладчиха мы произвели попытку подключиться к выполняющемуся процессу: gdb $ORACLE_HOME/b1n/extproc p1d Однако при этом была получена ошибка типа «permission denied». По- Поскольку мы уже имели разрешение на чтение и выполнение программного файла extproc, то решили обойти эту ошибку, загрузившись с учетной за- записью oracl e (похоже, это наилучший способ), а затем задали точку оста- останова в строке pextproc согласно инструкциям, содержащимся в файле dbextp.sql. Далее, переключившись в SQL*P]us, мы вызвали процедуру: SQL> CALL shell(NULL): и сразу после запуска процедуры extproc прервали ее выполнение в точке останова. Для того чтобы отладчик прочитал символы в нашей только что загруженной библиотеке, была выполнена команда gdb "share", а затем мы смогли установить точку останова во внешней процедуре extprocshO, по- после чего все работало нормально. Подобная отладка возможна и в системах Windows, однако проводить ее не так просто. Ниже перечислены основные изменения, которые необхо- необходимо сделать в процедуре отладки. 1. С помощью панели управления Windows нужно модифицировать сер- сервис листенера внешней процедуры, чтобы он выполнялся с правами собственной учетной записи пользователя, а не «Local System». 2. Поскольку отладчик не может найти строку pextproc и команда gdb "share" к Microsoft Windows не применима, в программе С в точке ос- останова требуется добавить строку кода DebugBreak, а затем перекомпи- перекомпилировать программу. Кроме того, придется включить в нее директиву #indude <wi ndows. h>. 3. Вместо команды ps -ef для получения РШ процесса extproc следует использовать программу Microsoft tasklist.exe. Для платформы UNIX тесты проведены в операционной системе Solaris 2.6, а для платформы Microsoft — в Windows XP с использованием отладчика Cygwin GDB 5.0 (имеет и консольную, и GUI-версии).
Сопровождение внешних процедур В этом разделе приведен ряд сведений, которые могут быть полезны при созда- создании, отладке и сопровождении внешних процедур. Удаление библиотек Синтаксис команды, применяемой для удаления библиотеки, прост: DROP LIBRARY имя_библиотени; Выполняющий ее пользователь Oracle должен иметь право DROP LIBRARY или DROP ANY LIBRARY. Перед удалением библиотеки Oracle не проверяет взаимосвязи между объек- объектами. Это удобно, если вам нужно изменить имя или местоположение совместно используемого объектного файла, на который указывает библиотека. В таком слу- случае ее можно просто удалить и создать заново, причем все зависящие от нее про- программы будут работать по-прежнему. (Пожалуй, было бы неплохо иметь в своем распоряжении команду CROP LIBRARY fORCE, но ее нет.) Прежде чем полностью удалить библиотеку, желательно запросить информа- информацию из представления DEPENDENCIES, чтобы узнать, не зависят ли от нее какие-ни- какие-нибудь модули PL/SQL. Словарь данных В словаре данных появилось несколько новых элементов, облегчающих управле- управление внешними процедурами. Приведенная ниже табл. 23.2 содержит перечень USER-версий таблиц словаря, в дополнение к которым имеются соответствующие DBA_- и ALL_-BepcHH. Таблица 23.2. Представления словаря Данных, предназначенные для работы с внешними процедурами Требуемая информация Представление, позволяющее Пример получить ответ Какие библиотеки мною USERJJBRARIES SELECT * FROM userjlbraries; созданы? В каких программах PL/SQL USER.DEPENDENCIES SELECT * FROM спецификации вызова user_dependendes WHERE содержат ссылку на referenced_name = 'FOO'; библиотеку foo? __ Советы по использованию внешних процедур Применяя внешние процедуры в PL/SQL, важно соблюдать ряд правил и следо- следовать указаниям, приведенным ниже. О Если PL/SQL требует соблюдения правил использования параметров, задан- заданных режимами IN, IN OUT и OUT, то язык С за этим не следит. Несоблюдение пра- правил использования параметров во время компиляции программой С не выявля- выявляется, поэтому вы можете не узнать о нарушении до тех пор, пока не запустите
918 Глава 23 • Внешние процедуры программу. Правила эти вам известны: не присваивать значений параметрам IN, не читать значений параметров OUT, всегда присваивать значения парамет- параметрам IN OUT и OUT, а также обязательно возвращать значение соответствующего типа данных. О Значения модифицируемых свойств INDICATOR и LENGTH всегда передаются по ссылке, а соответствующие им параметры - в режиме IN OUT, OUT или RETURN. Значения немодифицируемых свойств INDICATOR и LENGTH всегда передаются по значению, если ключевые слова BYREFERENCE не указаны явно. В последнем слу- случае, несмотря на то что свойства INDICATOR и LENGTH передаются по ссылке, они доступны только для чтения. О Теоретически в процессе.обмена данными между PL/SQL и С может быть пе- передано до 128 параметров, однако если какой-нибудь из них имеет тип float или doubl e, допустимое количество параметров уменьшается (насколько — за- зависит от операционной системы). О Поскольку в будущих версиях extproc может стать многопотоковым процес- процессом, а операционная система не спрашивает разрешения на выгрузку из памяти совместно используемых библиотек, следует избегать использования во внеш- внешнем коде внешних статических переменных. О Внешняя процедура не может выполнять команды DDL, запускать сеанс или управлять транзакциями с помощью инструкций COMMIT и ROLLBACK. (Полный список неподдерживаемых функций OCI вы найдете в документации Oracle PL/SQf. User's Guide and Reference.)
Алфавитный указатель $, элемент форматирования, 250 %BULK_EXCEPTIONS, атрибут явного курсора, 488 XFOUND, атрибут явного курсора, 486, 506 %ISOPEN, атрибут явного курсора, 488, 505 ^NOTFOUND, атрибут, 141 KNOTFOUND, атрибут явного курсора, 487, 506 %ROWCOUNT, атрибут явного курсора, 487, 506 %ROWTYPE, атрибут, 141, 183, 185, 336 %TYPE, атрибут, 183 , (запятая), элемент форматирования, 251 , тип данных, 412 . (точка), элемент форматирования, 251 0, элемент форматирования, 250 9, элемент форматирования, 250 A.M., элемент форматирования даты-времени, 284 ABS, функция, 264 ACOS, функция, 265 AD, элемент форматирования даты-времени, 282 A.D., элемент форматирования даты-времени, 282 ADD_MONTHS, функция, 322 AGENT IN, предложение, 903, 914 AGENT, предложение, 901 ALL_SOURCE, представление, 851 ALTER ANY TYPE, привилегия, 852 ALTER SESSION, инструкция, 200 ALTER SESSION, оператор, 281 ALTER SYSTEM, инструкция, 200 AM, элемент форматирования даты-времени, 284 ANY, ключевое слово, 613 ANYDATA, тип данных, 180, 439, 828 ANYDATASET, тип данных, 180, 439, 82g ANYTYPE, тип данных, 180, 439, 828 AS LANGUAGE С, предложение, 902 набор символов, 197 функция, 215 ASCIISTR, функция, 215 ASIN, функция, 265 AT AN, функция, 265 ATAN2, функция, 265 AUTHID CURRENTJJSER, параметр, 778 предложение, 793 AUTHID, предложение, 623 AUTONOMOUSJTRANSACTION, директива компилятора, 106, 469 В В, элемент форматирования, 250 ВС, элемент форматирования даты-времени, 282 B.C., элемент форматирования даты-времени, 282 BEGIN, ключевое слово, 90 BETWEEN, оператор, 115 BFILE, тип данных, 178, 411, 422 . BINARY JNTEGER, тип данных, 243, 24 BITAND, функция, 265 BLOB, тип данных, 178, 411 BOOLEAN, тип данных, 178, 403 BULK COLLECT, предложение, 450,484, 510 BY REFERENCE, предложение, 908 BYTE, спецификатор, 201, 205