Текст
                    Профессиональное
руководство по SQL Server:
хранимые процедуры,
XML,HTML
Addison Wesley ^V


The Guru's Guide to SQL Server Stored Procedures, XML, and HTML Ken Henderson W Addison-Wesley Boston San Francisco New York London Toronto Sydney Tokyo Singapore Madrid Mexico City Munich Paris Cape Town Hong Kong Montreal
Профессиональное руководство по SQL Server: хранимые процедуры, XML, HTML КенХендерсон С^ППТЕР ® Москва ■ Санкт-Петербург ■ Нижний Новгород ■ Воронеж Новосибирск ■ Ростов-на-Дону ■ Екатеринбург ■ Самара Киев ■ Харьков ■ Минск 2005
ББК 32.973.233-018 УДК 681.324.016 Х-38 Хендерсон К. Х-38 Профессиональное руководство по SQL Server: хранимые процедуры, XML, HTML (+CD). — СПб.: Питер, 2005. — 620 с: ил. ISBN 5-469-00046-Х Книга посвящена философии программирования в Transact-SQL. Она объясняет, как применять эту философию для создания собственных способов кодирования и решения повседневных проблем. В ней помимо основной темы — хранимых процедур — раскрыто множество вспомогательных, среди которых XML, HTML, .NET. Причина этого проста: когда вы создаете реальное программное обеспечение, вам всегда приходится работать с несколькими технологиями. Эта книга признает данный факт, раскрывая многие сопутствующие технологии и рассматривая их с точки зрения разработки хранимых процедур для SQL-сервера. Книга рассчитана на разработчиков среднего и высокого уровня, которые хотят совершенствоваться в программировании хранимых процедур. ББК 32.973.233-018 УДК 681.324.016 Права на издание получены по соглашению с Addison-Wesley Longman. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. © 2002 Ken Henderson ISBN 0201700468 (англ.) © Перевод на русский язык, ЗАО Издательский дом «Питер», 2005 ISBN 5-469-00046-Х © Издание на русском языке, оформление, ЗАО Издательский дом «Питер», 2005
Краткое содержание Предисловие 18 Введение 26 Часть I. Основы - Глава 1. Основы хранимых процедур 30 Глава 2. Оформление исходного кода 65 Глава 3. Шаблоны проектирования 83 Глава 4. Управление исходным кодом 107 Глава 5. Проектирование баз данных 122 Глава б. Создание тестовых данных 175 Часть II. Объекты Глава 7. Обработка ошибок 188 Глава 8. Триггеры 199 Глава 9. Представления 221 Глава 10. Пользовательские функции 255 Часть III. HTML, XML, .NET Глава 11. HTML 290 Глава 12. Введение в XML 301 Глава 13. XML и SQL Server: HTTP-запросы 324 Глава 14. XML и SQL Server: получение данных 340 Глава 15. XML и SQL Server: OPENXML 355 Глава 16. .NET и грядущая революция 384 Часть IV. Вопросы повышенной сложности Глава 17. Размышления о производительности 400 Глава 18. Отладка и профилирование 428 Глава 19. Автоматизация 442 Глава 20. Расширенные хранимые процедуры 463 Глава 21. Хранимые процедуры для администрирования 486 Глава 22. Недокументированные возможности Transact-SQL 520 Глава 23. Массивы 556 Часть V. Размышления о программной инженерии Глава 24. Обустройство рабочего места 576 Глава 25. Эволюция разработки программного обеспечения 581 Глава 26. Всеобъемлющее тестирование 601 Алфавитный указатель 614
Содержание Предисловие 18 От автора 20 Благодарности 24 Об авторе 25 Введение 26 О примерах баз данных 28 От издательства 28 Часть I. Основы Глава 1. Основы хранимых процедур 30 Что такое хранимая процедура 30 Преимущества хранимых процедур 31 Создание хранимой процедуры 31 Отсроченное разрешение наименований и одно интересное исключение 32 Просмотр текста хранимой процедуры 34 Разрешения и ограничения 35 Советы по созданию 35 Изменение хранимых процедур , 41 Выполнение хранимых процедур 42 Использование команды INSERT вместе с EXEC 42 Компиляция плана выполнения и выполнение 43 Мониторинг выполнения 43 Выполнение хранимой процедуры при помощи протокола удаленного вызова процедур (RPC) 48 Временные процедуры 49 Системные процедуры 49 Разница между системными объектами и системными процедурами 51 Расширенные хранимые процедуры 52 Внутренние процедуры 54 Параметры окружения 54 Параметры 56 Получение кода завершения 57 Выходные параметры 58 Список параметров хранимой процедуры 59 Общие замечания о параметрах 59 Глобальные переменные (системные функции) 60 Команды управления выполнением 60 Ошибки 61 Сообщения об ошибках 62 RAISERROR 62
Содержание 7 Вложенные вызовы 63 Рекурсивные вызовы 63 Итоги 64 Глава 2. Оформление исходного кода 65 Форматирование исходного кода 65 Прописные буквы 65 Отступы и пробелы 66 Скобки 70 Горизонтальные интервалы 71 Псевдонимы столбцов и таблиц 71 Язык определения данных (DDL) 72 Указание владельца 72 Сокращения и необязательные ключевые слова 73 Передача параметров 73 Выбор имен 73 Правила составления программного кода 76 Рекомендации для сценариев 76 Хранимые процедуры и функции 79 Таблицы и представления 80 Transact-SQL 82 Итоги 82 Глава 3. Шаблоны проектирования 83 Закон простоты 84 Идиомы 84 Получение метаданных 85 Создание объекта 85 Установка контекста базы данных 87 Очистка таблицы 88 Копирование таблицы 88 Присваивание значений переменным 89 Циклы 89 Неопределенные значения 90 Получение первых записей 91 Шаблоны проектирования 92 Итератор (Iterator) 92 Пересечение (Intersector) 94 Спецификатор (Qualifier) 95 Исполнитель (Executor) 96 Конвейер (Conveyor) 97 Уборщик (Restorer) 99 Прототип (Prototype) 102 Одиночка (Singleton) 103 Другие шаблоны 105 Итоги 106 Глава 4. Управление исходным кодом 107 Преимущества управления исходными текстами 108 Процедуры dt 109
8 Содержание Удачные решения 109 Храните объекты в сценариях НО Сценарии должны быть раздельными НО Не используйте Unicode ПО Используйте метки для обозначения версий НО Используйте ключевые слова 111 Не используйте шифрование 112 Контроль версий из Query Analyzer 115 Специальные лексемы 116 Автоматизация создания сценариев при помощи контроля версий 117 GGSQLBuilder 118 Принцип работы GGSQLBuilder 118 Преимущества средств формирования сценариев 119 Как GGSQLBuilder находит и упорядочивает сценарии 119 Итоги 121 Глава 5. Проектирование баз данных 122 Общий подход 122 Инструменты моделирования 123 Пример проекта 123 Пять процессов 124 Пять стадий в деталях 125 Анализ 125 Проектирование 125 Конструирование 125 Тестирование и внедрение 126 О сложностях разработки баз данных 126 Теория баз данных на практике 126 Определение предназначения приложения 127 Определение функций приложения 127 Проектирование основы базы данных и процессов приложения 128 Моделирование бизнес-процессов 131 Практическое моделирование бизнес-процессов 132 Добавление внешних сущностей 134 Добавление процессов 134 Добавление хранилищ 134 Добавление объектов потоков 136 Добавление структур данных 137 Моделирование «сущность-связь» 141 Типы E-R-диаграмм 142 Термины E-R-моделирования 142 Построение вашей E-R-модели 145 Нормализация 148 Завершение модели 153 Выбор идентификаторов сущностей 155 Последние штрихи 156 Реляционное моделирование данных 158 Термины логического моделирования 159 От E-R-диаграммы к реляционной модели 161 Создание словаря данных 162 Использование словаря данных 163
Содержание 9 Определение размера столбцов 165 Описание вашего проекта 165 Генерация внешних ключей 165 Проверка целостности модели 167 Генерация DDL 167 Диаграммы баз данных в Enterprise Manager 173 Итоги 174 Глава 6. Создание тестовых данных 175 Подходы к созданию данных 175 Перекрестное объединение 176 Набор случайных данных 179 Удваивание 181 INSERT...EXEC 183 sp_generate_test_data 184 Скорость 185 Итоги 186 Часть II. Объекты Глава 7. Обработка ошибок 188 Сообщения об ошибках 188 RAISERROR 189 xpjogevent 190 Методы обработки ошибок 190 @@ERROR 190 Ошибки пользователя 191 Фатальные ошибки 194 Мнимые ловушки , 194 @@ROWCOUNT 195 Ошибки и управление транзакциями 196 SET XACTJVBORT 197 Итоги 198 Глава 8. Триггеры 199 Определение изменений 200 Управление последовательными значениями 204 Ограничения триггеров 206 INSTEAD-триггеры 207 Триггеры и аудит 210 Транзакции. 213 Выполнение 214 Вызов хранимых процедур 214 Отключение триггеров 217 Полезные советы 218 Итоги 220 Глава 9. Представления 221 Метаданные 221 Исходный код представлений 222
10 Содержание Ограничения 223 ANSIJ4ULLS и QUOTEDJDENTIFIERS 224 Ограничения DML 224 Стандартные представления схемы данных 224 Создание собственных представлений в INFORMATION_SCHEMA 225 Создание собственных UDF в INFORMATION_SCHEMA 230 Вызов хранимых процедур из представлений 233 Обновляемые представления 236 Конструкция WITH CHECK OPTION 237 Вложенные запросы 237 Параметризованные представления 238 Динамические представления 239 Секционированные представления 241 BETWEEN в секционированных представлениях 247 Распределенные секционированные представления 250 Индексированные представления 251 Использование индексированных представлений оптимизатором 251 Индексированные представления в других редакциях SQL Server 252 Проектирование модульных индексированных представлений 253 Обслуживание индексированных представлений 253 Итоги 254 Глава 10. Пользовательские функции 255 Скалярные функции 255 Табличные функции 256 Inline-функции 258 Ограничения : 259 Метаданные 262 Создание собственных системных функций 265 «Рецепты» UDF 267 Улучшенная функция SOUNDEX() 268 Статистические функции 273 Рекурсия 283 Параметризованные пользовательские функции 284 Итоги 288 Часть III. HTML, XML, .NET Глава 11. HTML 290 Истоки 290 Создание HTML посредством Transact-SQL 291 Таблицы 291 Заголовки столбцов 293 Формирование HTML при помощи spjmakewebtask 294 Гиперссылки 296 Шаблоны 297 Итоги 300 Глава 12. Введение в XML 301 Деревянные монеты 301 XML: обзор 303
Содержание 11 HTML: простота не дается даром 304 XML: краткая история 305 Сравнение XML и HTML 305 Нюансы обозначений 308 Определение типа документа (DTD) 310 XML Schema 312 Преобразования Расширяемого языка стилей (XSLT) 315 XML в HTML 315 Объектная модель документа (DOM) 321 Рекомендуемая литература 322 Инструменты 323 Итоги 323 Глава 13. XML и SQL Server: HTTP-запросы 324 Доступ к SQL Server при помощи HTTP 324 Конфигурация виртуального каталога 325 Указание пути и имени виртуального каталога 326 URL-запросы 327 Специальные символы 329 Таблицы стилей 329 Тип содержимого 331 Результаты, не являющиеся XML 331 Хранимые процедуры 332 Шаблонные запросы 333 Параметризованные шаблоны 334 Таблицы стилей 335 Применение таблиц стилей при работе с клиентом 336 Клиентские шаблоны 337 Итоги 339 Глава 14. XML и SQL Server: получение данных 340 SELECT...FOR ХМL 340 Режим RAW 341 Режим AUTO 341 ELEMENTS 343 Режим EXPLICIT 344 Директивы 345 Установление взаимосвязей данных 347 Директива hide 348 Директива cdata 349 Директивы id, idref, idrefs 350 Схемы отображения 351 Аннотированные схемы 353 Итоги 354 Глава 15. XML и SQL Server: OPENXML 355 Параметр flags 358 Формат Edge Table 359 Вставка данных при помощи OPENXML() 360 Web Release 1 362 Апдейтограмы 363 Компонент XML Bulk Load , 370
12 Содержание Ограничения 376 sp_xml_concat 376 sp_run_xml_proc 379 Итоги 383 Глава 16. .NET и грядущая революция 384 .NET —будущее разработки программного обеспечения 389 Что такое .NET? 390 «Избиение» Microsoft 397 Фанатичная привязанность к Microsoft? 398 Итоги 398 Часть IV. Вопросы повышенной сложности Глава 17. Размышления о производительности 400 Индексирование 401 Хранение 401 «Покрывающие» индексы 403 Проблемы с производительностью 403 Пересечение индексов 404 Фрагментация индексов 404 Дефрагментация 406 Индексы на представлениях и вычисляемых столбцах 407 Обязательные требования 407 Блокировки и индексы 410 Статистика 411 Количество элементов 411 Плотность 411 Селективность 411 Проблемы с производительностью 412 Хранение статистики 413 Статистика столбцов 413 Просмотр статистики 413 Обновление статистики 414 sp_showstatdate 415 Оптимизация запросов 416 Простая оптимизация плана 417 Упрощение 418 Загрузка статистики 418 Оптимизация, основанная на оценке стоимости 418 Полная оптимизация 419 Оценка селективности 419 Оптимизация аргументов поиска 419 Порядок объединения и выбор типа объединения 421 Подзапросы и альтернатива объединениям 424 Логические и физические операторы 425 Итоги 427 Глава 18. Отладка и профилирование 428 Отладка 428 Проблемы установки и безопасности , 428
Содержание 13 Советы и предостережения 429 Последовательность действий 430 Отладка без Сети 430 Отладка триггеров и пользовательских функций 430 Профилирование 431 Запуск трассы 431 Трассировка против просмотра 431 Параметры командной строки 431 Общие советы и предостережения 432 Повторный запуск трасс 435 Загрузка файла трассы в таблицу 435 Представление файла трассы в виде XML 436 Группировка данных Profiler 437 ODBC-трассировка 437 Нагрузочное тестирование 437 Итоги 441 Глава 19. Автоматизация 442 Краткий обзор СОМ 442 До появления СОМ 443 Зарождение СОМ 445 Базовая архитектура 446 СОМ в работе 447 SQL Server и автоматизация СОМ 448 Процедуры sp_OA 449 sp_checkspelling 449 sp_exporttable 451 sp_importtable 456 sp_getSQLregistry 459 Итоги 462 Глава 20. Расширенные хранимые процедуры 463 Open Data Services 464 Стартовый код 465 Работа расширенных хранимых процедур 466 Отсылка сообщений 466 Обработка параметров 467 Возвращение данных 468 Простой пример 469 Сложный пример 472 Как сделать расширенные хранимые процедуры проще 478 Отладка расширенных хранимых процедур 479 Изоляция расширенных хранимых процедур 480 xp_setpriority 481 Итоги 485 Глава 21. Хранимые процедуры для администрирования 486 sp_readtextfile 486 sp_diff 489
14 Содержание sp_generate_script 490 sp_start_trace 500 sp_stop_trace , 505 sp_list_trace 507 sp_proc_runner 509 sp_create_backupjob 513 sp_diffdb 517 Итоги 519 Глава 22. Недокументированные возможности Transact-SQL 520 Что есть «недокументированный»? 521 Недокументированные процедуры , 521 sp_checknames [@mode] 521 sp_delete_backuphistory @oldest_date 522 sp_enumerrorlogs 522 sp_enumoledbdatasources 522 sp_fixindex @dbname, @tabname, @indid 522 sp__gettypestring ©tabid, @colid, @typestring output 522 sp_MS_marksystemobject @objname 522 sp_MS_upd_sysobj_category @pSeqMode integer 523 Sp_MSaddguidcol @source_owner, @source_table 523 sp_MSaddguidindex @source_owner, @source_table 523 sp_MSaddlogin_implicit_ntlogin @loginname 523 sp_MSadduser_implicit_ntlogin @ntname 523 sp_MScheck_uid_owns_anything @uid 523 sp_MSdbuseraccess @mode='perm']'db', @qual=db name 524 sp_MSdbuserpriv @mode='perm'rserv1|'verTrole' 524 sp_MSdependencies @objname, @objtype, ©flags int, @objlist 524 sp_MSdrop_object [@object_id] [,@object_name] [,@object_owner] 524 sp_MSexists_file @full_path, ©filename 525 sp_MSforeachdb @commandl @replacechar = ?'[,@command2] [,@command3] [,@precommand][,@postcommand] 525 sp_MSforeachtable @commandl @replacechar = '?' [,@command2] [,@command3] [,@whereand] [,@precommand] [,@postcommand] 526 sp_MSget_oledbinfo ©server [,@infotype] [,@login] [,@password] 529 sp_MSget_qualified_name @object_id, @qualified_name OUT 529 sp_MSget_type ©tabid, @colid, @colname OUT, @type OUT 529 sp_MSguidtostr @guid, @mystr OUT 530 sp_MShelpindex @tablename [,@indexname] [,@flags] 530 sp_MShelptype [@typename] [,@flags='sdt'ruddt' |NULL] 530 sp_MSindexspace @tablename [,@index_name] 531 sp_MSis_pk_col @source_table, @colname, @indid 531 v sp_MSkilldb @dbname 532 sp_MSIoginmappings @loginname 532 sp_MStable_has_unique_index @tabid 533 sp_MStablekeys [tablename] [,@colname] [,@type] [,@keyname] [,@flags] 533 sp_MStablerefs @tablename,@type=N'actualtables',@direction= N'primary', @reftable 533
Содержание 15 sp_MStablespace [@name] 533 sp_MSunc_to_drive @unc_path, @local_server, @local_path OUT 534 sp_MSuniquecolname table_name, @base_colname, @unique_colname OUT 534 sp_MSuniquename @seed, @start 534 sp_MSuniqueobjectname @name_in, @name_out OUT 534 sp_MSuniquetempname @name_in, @name_out OUT 534 sp_readerrorlog [@lognum] 535 sp_remove_tempdb_file ©filename 535 sp_set_local_time [@server_name] [,@adjustment_in_minutes] (for Win9x) 535 sp_tempdbspace 535 xp_dirtree 'rootpath' 536 xp_dsninfo @systemdsn 536 xp_enum_oledb_providers 536 xp_enumdsn 537 xp_enumerrorlogs 537 xp_execresultset 'code query'/database' 538 xp_fileexist 'filename' 538 xp_fixeddrives 538 xp_get_mapi_default_profile 538 xp_get_mapi_profiles 539 xp_getfiledetails 'filename' 539 xp_getnetname 539 xp_oledbinfo @providername, @datasource, ©location, @providerstring, ©catalog, @login, ©password, @infotype 539 xp_readerrorlog [lognum][filename] 540 xp_regenumvalues 540 xp_regaddmultistr, xp_regdeletekey, xp_regdeletevalue, xp_regread, xp_regremovemultistring, xp_regwrite 541 xp_subdirs 541 xp_test_mapi_profile 'profile' 541 xp_varbintohexstr 541 Создание представлений INFORMATION_SCHEMA 542 Создание системных функций 543 Недокументированные команды DBCC 544 Недокументированные функции 551 Недокументированные флаги трассировки 551 Итоги , 555 Глава 23. Массивы 556 xp_array.dll 557 xp_createarray 558 xp_setarray 559 xp_getarray 561 xp_destroyarray 563 xpjistarray 565 Системные функции для работы с массивами 567 Главное блюдо 569 Многомерные массивы 571 Итоги 574
16 Содержание Часть V. Размышления о программной инженерии Глава 24. Обустройство рабочего места 576 Избавьтесь от отвлекающих факторов 577 Закройте дверь 578 Внутренние отвлекающие факторы 578 Форма без содержания 579 Молчим — работая, общаемся — отдыхая 579 Заключение 579 Эпилог 579 Глава 25. Эволюция разработки программного обеспечения 581 Кай-цзэн 582 Преимущества небольших изменений 582 Программное обеспечение 583 Энтропия программного обеспечения 584 Рефакторинг 585 Как приучить начальство (и самого себя) проводить рефакторинг 587 Когда рефакторинг не нужен 590 Базы данных 591 Рефакторинг или проектирование? 592 Избавление от кода 593 Экстремальное программирование 594 Сначала кодируй, потом осмысли 597 За и против 599 Заключение 599 Эпилог 600 Глава 26. Всеобъемлющее тестирование 601 С чего начать 603 Бесполезность тестирования 604 Виды тестов 605 Модульные тесты 605 Функциональные тесты 605 Регрессионные тесты 606 Интеграционные тесты 606 Когда тестировать 606 Значение тестов при рефакторинге 606 Тестирование экономит время 607 Наилучший метод тестирования 608 Другие виды тестирования 609 Проверка кода 610 Чтение кода 611 Инспектирование 611 Сквозной контроль 612 Итоги 612 Эпилог 613 Алфавитный указатель 614
Посвящается Т.
Предисловие Эта книга заслуживает внимания всех тех, кто хочет стать профессиональным разработчиком приложений для SQL Server. Мне посчастливилось быть первым менеджером по разработке SQL Server в Microsoft. В течение одиннадцати с лишним лет я принял на работу многих главных разработчиков этого продукта и работал с ними в команде бок о бок. Я узнал много черт, свойственных великим разработчикам, общая для всех — страсть. Перефразируя Эйнштейна, талант разработчика программного обеспечения на 1 % состоит из вдохновения и на 99 % — из труда. Быть просто фанатиком своего дела уже не является достаточным качеством. Нужно бороться за совершенство. Профессионал пишет эффективный, надежный и документированный код, который может поддерживаться, улучшаться и пониматься на протяжении нескольких лет не только им самим, но и его коллегами. Компьютерная наука требует времени на то, чтобы блестящие выпускники из горячих и умных «кодеров» превратились в профессиональных разработчиков продукта. (Некоторым так и не удалось пройти это превращение: их программистская карьера была коротка, во всяком случае, в моей группе.) Просто разработчики знакомы с наиболее известными алгоритмами в своей области; у них есть известное мастерство в языках программирования и владения инструментами. Профессионалу этого мало. Каждое граничное условие должно быть проверено, обеспечено и протестировано до проверки всего кода. Контроль в коде обязателен. Коды завершения всегда проверяются. Обширные комментарии обязательны. Повторное использование кода необходимо. Инспектирование и критический разбор кода приветствуются и являются повседневной задачей. Проверка кода — это как подпись художника на картине. Это признак того, что код соответствует высоким стандартам. Профессионалы никогда не перестают учиться, они — ученики всегда. В нашей индустрии, если однажды перестаешь совершенствовать навыки, начнешь двигаться назад. Когда я работал в Microsoft, мне часто приходилось общаться с пользователями SQL Server. И чаще всего они жаловались на то, что программа работала не так гладко и хорошо, как обещали разработчики. Так я получил репутацию «Мистер Почини» (ударение на последнем слоге) и помог создать очень успешные программы из почти провальных. Должен признать: мне нравилось активное вмешательство пользователей несмотря на то, что зачастую они начинали разговор на повышенных тонах. Сам SQL Server часто становится «мальчиком для битья» у плохих разработчиков. Конечно, было время, когда в SQL Server были дефекты и недостатки, которые добавляли проблем, но с выпуском каждой новой версии он становился на порядок совер; шеннее, устойчивее и быстрее. Так будет и дальше. Но даже в суровые времена
Предисловие 19 версии 1.x повышение производительности и упрощение могли быть достигнуты при помощи более правильного использования хранимых процедур SQL Server. Никто не удивился, когда за счет разумного использования хранимых процедур мы увидели 100-кратное и более увеличение производительности. Если бы я догадался выпроводить из офиса назойливых менеджеров, для многих проблемных проектов мы достигли бы больших результатов гораздо раньше. Хотя я руководил большой группой (и даже был капитаном баскетбольной команды в лучшем университете), я никогда не считал себя менеджером. Я всегда считал себя разработчиком, а разработчики знают, как разговаривать друг с другом, и уважают сильные идеи и передовые методы. Эта книга — разговор опытного разработчика со своими коллегами. Кен пишет понятным, живым языком, который, к сожалению, не принят в технических книгах. Он не боится общественного мнения. Постепенно решение трудностей в прикладных программах привело к одному простому выводу: разработчики не относятся к коду SQL так серьезно, как должны были бы, или как они относятся к С, C++ и другим языкам программирования, на которых написана часть их программы. Во многих случаях разработчики владеют только поверхностной информацией об использовании баз данных и SQL. Это достаточно умные люди, но не достигшие совершенства в этой области и не ставшие пока в ней профессионалами — в том смысле, в котором я это понимаю. На самом деле, большинство моих рабочих ситуаций и встреч никогда не произошли бы, если бы разработчики прочитали, изучили и проанализировали эту книгу перед тем, как писать свою первую программу для SQL Server. Уделите немного вашего драгоценного времени этой книге. Она станет еще одним шагом на пути к совершенству в профессиональном создании программ для SQL Server. Кен, я поздравляю тебя и благодарю за эту книгу. Жаль, что такой книги не было 10 лет назад (впрочем, как и твоей первой работы «Профессиональноеруководство по Transact-SQL»). Рон Сукап (Ron Soukup), сентябрь 2001 г.
От автора Основная идея этой книги заключается в том, что создание хранимых процедур на Transact-SQL аналогично созданию программ на любом другом языке. Оно требует тех же навыков, планирования, внимания к деталям и, более всего, освоения технологий, необходимых для успешного программирования (так же, как при программировании на других языках). Чтобы совершенствоваться в Transact-SQL, в первую очередь необходимо освоить основы создания программного обеспечения и уже на их базу добавлять Transact-SQL как отдельный язык программирования. В этой книге рассказывается, как это сделать. Если вы новичок в SQL Server, возможно, для введения в хранимые процедуры SQL Server вам стоит поискать более понятные книги. Кроме некоторых вступительных замечаний в начале обсуждений, эта книга не даст новичкам необходимой стартовой информации. Предполагается, что вы уже знаете, как писать запросы на Transact-SQL и как создавать хранимые процедуры. Таким образом, моя книга рассчитана на разработчиков среднего и высокого уровня, которые хотят совершенствоваться в программировании хранимых процедур, — для разработчиков, которые хотят перейти на следующий уровень мастерства в разработке программного обеспечения Transact-SQL, хранимых процедур и XML. Эпиграфом к моей предыдущей книге, Профессиональное руководство по Transact-SQL, стала цитата моего друга, известного автора и лектора Джо Селко (Joe Celko), о важности неизучения процедурного программирования для совершенствования в непроцедурных языках (таких, как SQL). В то время я согласился с Джо в том, что создание кода Transact-SQL процедурным способом было самым большим препятствием на пути написания хорошего кода на Transact-SQL. Когда я закончил первую книгу, я твердо верил, что попытки программировать в Transact- SQL так же, как, скажем, в C++, были основной причиной того, что программисты, компетентные в других языках, часто затруднялись программировать в Transact- SQL. Я считал, что их подход в целом был неверным и поэтому у них были проблемы. Я был уверен, что они не могли думать как программисты баз данных, а вместо этого думали как обычные программисты, — что недостаточно в мире программирования баз данных. Я так думал. С тех пор мое мнение изменилось. Однажды я прочитал интервью, в котором Эдвард Ван Хэлен (Edward Van Halen) сказал, что альбомы музыкальных групп — это моментальные снимки того, где находится группа (и в музыкальном плане). То же можно сказать и о книгах. Профессиональное руководство по Transact-SQL показывает, где я был в 1998-1999 годах. С тех пор мое отношение к процедурному программированию и Transact-SQL эволюционировало (или мне хотелось бы так - думать). Почему? Что ж, позвольте рассказать вам небольшую историю...
От автора 21 Однажды в те два года, которые я потратил на создание первой книги, один из обозревателей технической литературы спросил меня о статье, которую я написал за несколько лет до этого для своей колонки в журнале «Sybase Developer». В статье я рассказывал об уловках при работе с битовыми последовательностями в Transact-SQL. Так вот, этот человек хотел узнать, не мог бы я отправить ему копию статьи, поскольку он работал с битами и хотел использовать один из предложенных мной способов. Я все перерыл, но так и не нашел ее ни на одном из моих компьютеров. Машина, на которой я писал эту статью, давно была отправлена на пенсию, а резервных копий у меня не было. В конце концов, я нашел древний кусок моей статьи в Интернете и переслал его этому человеку, а сам с некоторым изумлением перечитал статью. (Писателям очень нравится читать свои произведения, неважно, сколько им лет и о чем они.) Я был удивлен: что могло заставить меня испробовать все эти способы в Transact-SQL? Почему я вообще задумался о таких вещах? Что заставило меня делать открытия, подобные технологиям, о которых была моя статья? Я решил: если я смогу понять, как или почему я сделал эти открытия, возможно, я смогу открыть секрет всех инноваций или, по крайней мере, путь, ведущий к ним. И, может быть, сам смогу подняться на следующий уровень программирования SQL. Я думал об этом несколько дней и, наконец, понял, как появляются идеи. Вывод, к которому я пришел, был следующий: как бы я ни хотел верить, что до всего додумался сам, большинство из моих «открытий» в Transact-SQL являются результатом так называемого «перекрестного переопыления». Я додумался до некоторых инновационных способах кодирования благодаря (а не вопреки) моему опыту в других языках. Большинство открытий, которые я сделал в Transact-SQL, выросли из семян, посаженных в моем мозгу работой в традиционных языках программирования, таких как Pascal, C/C++, ассемблерах и других. Я осознал, что во всем достаточно зрелом мире программирования на Transact-SQL было всего несколько настоящих новинок. В конце концов, такие языки, как С и Pascal — на много лет опередили Transact-SQL, что уж говорить о COBOL и BASIC. В мире программирования осталось не так много новых задач. Что мы наблюдаем сейчас главным образом? Новые способы решения старых задач. Люди решали эти задачи задолго до того, как Transact-SQL и SQL появились на свет. Совершенно точно, большинство открытий в сфере программного обеспечения уже было сделано. Те из нас, кто пытается изобрести что-то новое в Transact-SQL, буквально стоят на плечах тех гигантов, которые были до нас. В своей книге «Прагматичный программист» Эндрю Хант и Дэйв Томас (The Pragmatic Programmer, Andrew Hunt and Dave Thomas) настойчиво рекомендуют следующее: если вы хотите научиться программировать лучше, вы должны изучать хотя бы по одному новому языку программирования в год. Я присоединяюсь к этой рекомендации. Если вы хотите совершенствоваться в создании хранимых процедур на Transact-SQL, вы сначала должны овладеть умением просто хорошо программировать. Программирование, кодирование, создание программного обеспечения — как ни называйте — требует долгих лет изучения многих языков. Как курсант должен сначала изучить несколько военных наук, прежде чем получит офицерские погоны, так программист, стремящийся изучить Transact-SQL, должен изучить сначала различные аспекты общего программирования и только потом позволить себе надежду на овладение мастерством в Transact-SQL.
22 От автора Открывающиеся перспективы и «перекрестное опыление», которые возможны благодаря работе в нескольких областях одновременно, являются объяснением тому, почему студенты в университетах просят ввести преподавание дополнительных курсов. Изучение приемов, используемых в других областях, поможет им найти много общего между своей профессиональной сферой и другими областями, понять их сходства и различия, а также применить сделанные открытия в своих разработках при помощи новых, неиспытанных способов. Университет дает широкий взгляд на мир, способствующий осознанию своей сферы деятельности более целостно. Благодаря ему философия профессии предстает всеобъемлюще и становится ясно, как она вписывается в общую картину. Другими словами, вы учитесь производить перемены. Я думаю, примерно того же эффекта можно добиться, изучая языки и способы, не относящиеся к области SQL Server. Если бы не моя работа с ассемблером и не изучение работ таких мастеров, как Стив Гибсон (Steve Gibson), я бы никогда не наткнулся на способ работы с последовательностями битов (о котором была та статья). Если бы не моя работа в Pascal и Delphi и не изучение кода таких гуру, как Андерс Хейлсберг (Anders Hejlsberg) и Ким Кокконен (Kim Kokkonen), я бы не • додумался до приемов в Transact-SQL (включая процедуры для работы с данными, описанные в этой книге). Мои исследования паттернов проектирования Transact- SQL были навеяны книгой Приемы объектно-ориентированного проектирования Эрика Гамма (Design Patterns, Erich Gamma), которая у меня под рукой каждый раз, когда я пишу на языках C++ и Object Pascal. Книга Практика программирования Брайана Кернигана и Роба Пайка (The Practice of Programming, Brian Kernighan and Rob Pike) сильно повлияла на мою приверженность к идиоматическому программированию. Я стал сторонником тестирования благодаря книге Золотая лихорадка Стива Макконнелла (After the Gold Rush, Steve McConnell) и Экстремльное программирование Кента Бэка (Extreme Programming Explained, Kent Beck). Я большой защитник значимости рефакторин- га благодаря книге Рефакторинг: Улучшение структуры существующего кода Мартина Фоулера (Refactoring: Improving the Design of Existing Code, Martin Fowler). Многие алгоритмы, приведенные в данной книге, были навеяны такими работами, как Искусство программирования Дональда Кнута (The Art ofComputer Programming, Donald Knuth), Шедевры программирования Джона Бентлей (Programming Pearls, John Bentley) и многими другими. Ни одна из этих книг не посвящена Transact-SQL или хотя бы SQL Server. Ни одна из них не содержит способы, которые можно с легкостью применить к языкам, ориентированным на работу с множествами (как SQL). Но эти книги о программировании — и тому, что я работаю в других языках, я обязан им. Я многое понял благодаря одновременной работе в нескольких областях и объединению работы в Transact-SQL с другими языками. Я осознал возможности, которые такая работа дает программисту. Думаю, вам это тоже будет полезно. Итак, вместо чтения проповедей о том, что необходимо бросить греховное процедурное программирование, чтобы вы могли постичь дао (которое заключается в мастерстве Transact-SQL), я лучше воодушевлю вас на исследование других языков и других методов. Выбирайте один язык в год — это может быть любой язык или инструмент, в котором вы еще не совсем разбираетесь. Все, что угодно, начиная с Visual Basic,' заканчивая Delphi, Ruby, C#, C++ или Java. Придумайте несколько проектов, ко-
От автора 23 торые можно осуществить на этом языке. Идеально (но не обязательно) это может быть что-нибудь, что вернет вас к SQL Server, — и погружайтесь. Купите необходимые книги, читайте новости, исследуйте, создавайте собственное программное обеспечение. Вы удивитесь, как много можно узнать о программировании и как сильно поднять свою профессиональную планку разработчика, имея такой опыт. Иногда, во время своих исследований, задумывайтесь над тем, как можно было бы применить новые знания в работе с Transact-SQL. Как SQL Server использует тот или иной элемент инструмента, который вы изучаете? Как реализуются функции, которые вы посчитали наиболее полезными в новом для вас языке? Чем они отличаются? Чем, например, OLE Automation отличается от Transact-SQL и Delphi? Зная, что Transact-SQL, как и SQL Server, написан на С и C++, какие общие черты можно выделить? Через несколько лет, после того как вы увидите перспективу за границами SQL Server, у вас будут знания, необходимые для настоящего мастерства в Transact- SQL и программирования хранимых процедур. Вы оцените создание программного обеспечения как дисциплину, вы полюбите сам процесс программирования. Это ключ к постижению любого языка программирования, включая Transact-SQL. Итак, приношу свои извинения Джо Селко за то, что больше не верю, что процедурное программирование — единственное препятствие написания хорошего кода на Transact-SQL. Как раз наоборот. Незнание сильных и слабых сторон языка (тех черт, которые делают его уникальным) — вот единственное препятствие написания хорошего программного обеспечения. А определить сильные и слабые стороны языка можно только посредством междисциплинарной работы и скрещивания. Сильная сторона Transact-SQL заключается в его ориентированности на работу с множествами, а главный недостаток — в необходимости нисходящего программировании. Это не значит, что на Transact-SQL можно писать только программы для работы с множествами или что создание процедурного кода на Transact-SQL подходит только для безрассудно храбрых. В конце концов, хранимые процедуры названы так вследствие каких-то причин. Стиль кодирования будет отличаться в Transact-SQL от, например, Visual Basic, что относится не только к Transact-SQL, но и ко многим другим языкам. Многие языки имеют нюансы и средства выражения, делающие их уникальными. В C++ вы не будете программировать так, как в Visual Basic. Просто используйте соответствующие инструменты для выполнения соответствующих задач. Используйте их сильные стороны и избегайте слабых. Научитесь не просто использовать инструмент. Поставьте себе цель стать мастером в программировании, а не просто экспертом в хранимых процедурах. Что касается нашего разговора за ужином в Сан-Франциско, Джо: я все еще считаю, что С# — лучшее, что произошло в мире программирования за последнее время. Кен Хендерсон (Ken Henderson), январь 2001 г.
Благодарности Здесь я могу предложить почти что оскаровскую речь о моей признательности и благодарности всем «маленьким людям», которые сделали эту книгу реальностью. Я могу рассказать о том, как трудно писать книги и как много людей помогли мне создать эту книгу. Но я не буду делать этого. Те, кто помогал мне, знают, как сильно я ценю их усилия. Книга не получилась бы без вас — такая простая и легкая. Как всегда, моя жена смогла организовать семейную жизнь вокруг моего хаотичного расписания таким образом, чтобы еще одна моя мечта увидела свет. За это я всегда буду в долгу перед ней, а она — в моем сердце.
Об авторе ч Кен Хендерсон — муж и отец, живущий в Далласе. В свободное от написания книг и программного обеспечения время он любит читать, играть на музыкальных инструментах и смотреть, как растут его дети. Связаться с Хендерсоном можно по адресу khen@khen.com.
Введение Вместо того чтобы просто показывать вам различные трюки и тонкости синтаксиса, эта книга обучает философии программирования в Transact-SQL. Она объясняет, как применять эту философию для создания собственных способов кодирования и решения повседневных проблем. Эта книга написана с позиции, что «почему» не менее важно, чем «как»; а сбалансированный подход к изучению Transact-SQL (подход, который уделяет одинаковое внимание и теории и практике) — предпочтительнее, чем несбалансированный. Вы заметите, что в этой книге описаны некоторые темы, которые традиционно не относятся к программированию хранимых процедур. Я имею в виду XML, HTML, .NET и некоторые другие вспомогательные темы. Причина проста: когда вы создаете реальное программное обеспечение, вам приходится работать с несколькими технологиями. Хранимые процедуры редко создаются сами по себе. Эта книга признает данный факт, раскрывая многие сопутствующие технологии и рассматривая их с точки зрения разработки хранимых процедур для SQL-сервера. Мы поговорим о XML и HTML, поскольку, используя хранимые процедуры и запросы T-SQL, нам часто приходится работать с XML. Мы обсудим .NET, потому что Microsoft анонсировала, что в следующей версии SQL можно будет разрабатывать хранимые процедуры при помощи новых языков .NET: C# и VB.NET. Мне показалось, что книга о SQL-сервере (даже та, в которой основное внимание уделяется хранимым процедурам) должна затронуть .NET и показать разработчикам Transact-SQL хотя бы некоторые из его многих возможностей. Хотя некоторые программисты Transact-SQL могут возражать против переноса их кода на .NET и Common Language Runtime. Я подумал, что такая книга, как эта, должна помогать разработчику, обеспечивая широкий взгляд на технологию и предлагая подсказки о том, чего можно ожидать от нее. В этой книге обсуждается использование Transact-SQL в различных областях создания программного обеспечения. Она раскрывает несколько основных концепций создания программного обеспечения: лучшие методы, стратегии управления кодом, паттерны проектирования и идиомы, тестирование и многое другое. Все это очень важно для высококлассной разработки и программирования на всех языках, включая Transact-SQL. Эта книга повышает статус Transact-SQL до уровня традиционно используемых языков, таких как Visual Basic и C++. Она учит тому, как овладеть Transact-SQL как языком программирования, а не просто языком запросов или средством для создания сценариев для SQL-сервера. Как и в моей предыдущей книге, в этой книге нет большого количества картинок, что часто используется в книгах по компьютерной тематике. Я постарался не нагружать книгу ненужными цифрами, диаграммами и другими подобными веща-
Введение 27 ми. Я приводил цифры только там, где они действительно полезны. Я достаточно часто обрезал результаты запросов (там, где я это делал, вы увидите надпись «Результаты сокращены»), однако включал полный листинг кода, где это было возможно. Толстые технические книги — то, что меня очень раздражает, поэтому я сам стараюсь избегать этого. Когда я начинал писать эту книгу, я поставил для себя следующие цели: ■ Выбрать темы, которые были упущены в книге Профессиональное руководство по Transact-SQL, и показать эволюцию описанных тем для тех, кто читал мою первую книгу. ■ Научить философии программирования в Transact-SQL, а не просто синтаксису, техникам и скрытым трюкам. ■ Рассказать о сходствах и различиях между программированием в Transact-SQL и созданием программного обеспечения на других языках программирования. ■ По мере возможности избежать повторения Books Online. ■ Полностью раскрыть темы, которые относятся к программированию хранимых процедур в Transact-SQL (такие темы, как: расширенные хранимые процедуры, проектирование баз данных и XML), то есть те темы, которые обычно опускаются или рассматриваются поверхностно. ■ Переходить от основ к более сложным вещам внутри отдельных глав и книги в целом. ■ Создать сбалансированную книгу, которая уделяет одинаковое внимание теоретическим и практическим аспектам создания хранимых процедур на Transact- SQL. ■ Построить каждую главу таким образом, чтобы она была самодостаточна, — то есть ссылалась на объекты, созданные в других главах, как можно реже. ■ Избегать излишних картинок с изображением экрана и другого «мусора», который часто встречается в книгах по компьютерной тематике. ■ Вводить новшества. Показывать техники, подходы и способы мышления о программировании на Transact-SQL, которые до этого не использовались. Усовершенствовать существующее состояние искусства программирования на Transact-SQL в частности и создание программного обеспечения вообще. ■ Злоупотреблять примерами кодов. Не просто рассказывать читателям, как сделать что-либо, а показывать. ■ Включать полные примеры кода в текст главы, чтобы книгу можно было читать без использования компьютера или компакт-диска. ■ Сокращать листинги в тексте, когда речь идет о способах форматирования и когда это не отвлекает от обсуждения основной темы (сохраняя целостность всех листингов на компакт-диске, прилагаемом к книге). ■ Показывать примеры кодов из реальной жизни, то есть те коды, которые имеют определенную ценность не только для книги. Показывать примеры, которые читатель мог бы использовать на своем продукционном сервере. ■ Показать, что компьютерная литература может быть не только хорошо написана и понятна — что она содержит действительно полезную техническую информацию. Доказать, что хороший текст и высокие технологии не исключают друг друга.
28 Введение ■ Использовать современные способы написания кода с особым ударением на совместимость со стандартами ANSI и использованием функциональности текущей версии. ■ Обеспечить легкие, неформальные комментарии. Быть верным партнером читателя в его работе над книгой. Общаться с читателем так, как люди обычно разговаривают между собой. Эти и некоторые другие цели я имел в виду, когда приступил к написанию книги. Профессия писателя имеет свои плюсы и минусы. Каждый день я ставил перед собой цели, которых пытался достичь. Иногда у меня это получалось, а иногда — нет. Главное — продолжать пытаться, а когда терпишь неудачу — понять, где ошибся и как избежать этого в следующий раз. Сложность заключается в том, чтобы избавиться от разочарования неудач, которые не позволяют раскрыть весь потенциал. Вы будете вознаграждены, увидев, чего можете достичь, когда преодолеете все трудности и окажетесь на следующей ступени профессионального развития. О примерах баз данных Чтобы облегчить чтение, в этой книге широко используются примеры баз данных из поставки SQLServer — Northwind и pubs. Использование этих баз данных поможет избежать необходимости создавать многочисленные объекты, которые нужны только для понимания принципов. Почти всегда можно будет определить, какая база используется в примере из комментариев или по самому коду. База данных Northwi nd используется чаще, чем pubs, поэтому, если нет пояснения и есть сомнения, используйте Northwind. Обычно модификации этих баз данных делаются внутри транзакции так, что- , бы их можно было вернуть в первоначальное состояние. Хотя, по соображениям безопасности, стоит пересоздавать их после каждой главы, где они изменялись. Сценарии для их создания (i nstnwnd. sq I Hi nstpubs. sq I) находятся в подкаталоге \Install в корневом каталоге SQL Server. От издательства Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на веб-сайте издательства: http://www.piter.com.
Часть I Основы
1 Основы хранимых процедур Современная практика разработки программного обеспечения пребывает в безмятежном море программирования по принципу «закодируй-и-исправь», неэффективность которого была доказана еще 20 лет назад. Стив Макконнелл' Исходя из того, что человеческий мозг усваивает новую информацию, связывая ее с ранее известной, мы посвятим эту главу формированию некой основы, которая необходима для усвоения знаний, содержащихся в этой книге. Мы затронем все темы, раскрытые на страницах последующих глав, не касаясь деталей. Овладев основными понятиями Transact-SQL, вы сможете связать их с понятиями более высокого уровня. Эта глава посвящена программированию хранимых процедур для SQL Server. Вы познакомитесь с хранимыми процедурами, узнаете, как и зачем они используются. Вы убедитесь, что Transact-SQL — это развитый язык программирования. Главное, помните, что программирование на Transact-SQL очень похоже на программирование на любом другом языке, и чтобы программировать на T-SQL, необходимы те же навыки. Что такое хранимая процедура Хранимая процедура Transact-SQL — это набор T-SQL-кода, который хранится в базе данных SQL Server и компилируется при использовании. Вы можете создавать его при помощи команды CREATEPROCEDURE. Большинство Transact-SQL-команд можно использовать в хранимой процедуре, однако некоторые команды (такие, как: CREATE PROCEDURE, CREATE VIEW, SET SH0WPLAN_TEXT, SET SH0WPLAN_ALL и т. д.) должны быть первыми (или единственными) операторами в пакете команд и поэтому не могут быть использованы в хранимых процедурах. Большинство команд Transact-SQL ведут себя в хранимой процедуре так же, как и в пакете команд, но у некоторых из них в хранимых процедурах особые возможности. В листинге 1.1 представлена хранимая процедура (фактически хранимую процедуру составляет код от строки CREATEPROCEDURE до GO). McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1998.
Создание хранимой процедуры 31 Листинг 1.1. Простая хранимая процедура Use Northwlnd \ GO IF 0BJECTJD('dbo.ListCustomersByCity') IS NOT NULL DROP PROC dbo.ListCustomersByCity GO CREATE PROCEDURE dbo.ListCustomersByCity @Country nvarcharC0)=T AS SELECT City. COUNTt*) AS NumberOfCustomers ■ FROM Customers WHERE Country LIKE @Country GROUP BY City GO EXEC dbo.ListCustomersByCity Преимущества хранимых процедур Хотя большую часть того, что позволяет хранимая процедура, вы можете сделать при помощи простого кода Transact-SQL, хранимые процедуры имеют множество преимуществ перед прямыми запросами, в том числе: ■ перестройка и повторное использование плана выполнения; ■ автопараметризация запроса; ■ инкапсуляция бизнес-логики; ■ обеспечение модульной структуры приложения; ■ совместное использование в нескольких приложениях; ■ авторизованный и единообразный доступ к объектам базы данных; ■ последовательная, безопасная модификация данных; ■ уменьшение сетевого трафика; ■ поддержка автоматического выполнения при запуске системы. Рассмотрим каждое из преимуществ по порядку. Создание хранимой процедуры Как я уже сказал, для создания хранимой процедуры используется Transact-SQL- команда CREATE PROCEDURE. При создании процедуры проверяется ее синтаксис, а исходный код вставляется в системную таблицу syscomments. В общем случае названия объектов, которые используются в процедуре, не проверяются, пока она не выполнится. В SQL Server это известно как отсроченное разрешение наименований. Syscomments, переводимое как системные комментарии, — ошибочное название. В этой таблице не содержатся комментарии, в ней содержится исходный код. Это название пришло из ранних версий, и возникло оно потому, что там хранился дополнительный исходный код к хранимым процедурам (и другим объектам), тогда как в sysprocedures хранилась псевдоскомпилированная версия самих процедур (своего рода нормализованное дерево запроса). На сегодняшний день все обстоит
32 Глава 1. Основы хранимых процедур иначе, и таблиц sysprocedures больше не существует. Теперь syscomments — единственное хранилище для хранимых процедур, представлений, триггеров, пользовательских функций (UDF), правил и стандартных значений. Удалив исходный код объекта из syscomments, этот объект нельзя будет использовать. Отсроченное разрешение наименований и одно интересное исключение Прежде чем двигаться далее, стоит упомянуть, что есть одно интересное исключение из стандартного поведения SQL Server при отсроченном разрешении наименований. Запустите код листинга 1.2 в Query Analyzer. Листинг 1.2. Нельзя использовать несколько операторов CREATE TABLE для одной и той же временной таблицы в той же самой хранимой процедуре CREATE PROC testp @var int AS IF @var=l CREATE TABLE #temp (kl int identity, cl int) ELSE CREATE TABLE #temp (kl int identity, cl varcharB)) INSERT #temp DEFAULT VALUES SELECT cl FROM #temp GO Синтаксис, содержащийся в хранимой процедуре, по-видимому, правильный, однако при запуске мы получим сообщение об ошибке: Server: Msg 2714. Level 16. State 1. Procedure testp. Line 6 There is already an object named 'ftemp' in the database. Почему? Очевидно, что параметр @var не может быть одновременно равным и не равным единице. Чтобы понять, что происходит, измените временную таблицу на постоянную, как показано в листинге 1.3. Листинг 1.3. Замена временной таблицы на постоянную позволяет обойти ограничение по временным таблицам CREATE PROC testp @var int AS IF @var=l CREATE TABLE tempdb..temp (kl int identity, cl int) ELSE CREATE TABLE tempdb..temp (kl int identity, cl varcharB)) INSERT #temp DEFAULT VALUES SELECT cl FROM #temp GO Эта процедура создана без ошибки. Как же так получается? Почему SQL Server заботится о том, является ли создаваемая таблица временной или постоянной? И почему это имеет значение теперь: до того, как процедура выполнена, и прежде, чем значение @var известно? На самом деле, SQL Server определяет ссылку CREATE TABLE на временную таблицу перед вставкой процедуры в syscomments — очевидное наследие прошлых версий, когда ссылки на объекты разрешались при создании процедуры. То же самое ограничение действительно при определении переменных и, следовательно, применимо к типу данных table. Нельзя определить переменную более чем один раз
Создание хранимой процедуры 33 в одной хранимой процедуре, даже если определения находятся во взаимоисключающих частях кода. Постоянные таблицы обрабатываются по-другому, и именно поэтому код в листинге 1.3 запускается без ошибки. Оказывается, что, начиная с SQL Server 7.0, отсроченное разрешение наименований было разрешено для постоянных таблиц, но не для временных. В любом случае нельзя выполнить код, подобный тому, что представлен в листинге 1.2. Решение проблемы следующее. Этот способ создает таблицу только раз, а затем в зависимости от условий изменяет ее структуру. Обратите внимание на использование ЕХЕС() при выборе столбца, добавленного оператором ALTER TABLE. Использование динамического SQL необходимо, так как добавленный столбец сразу не виден добавившей его процедуре. Необходимо создать и выполнить прямой запрос, чтобы получить доступ к нему по названию. (Обратите внимание, что вы можете обратиться к этому столбцу косвенно, например, с помощью оператора SELECT * или по номеру в ORDER BY, но только не по названию.) Другой недостаток этого подхода в том, что он смешивает операторы DDL (CREATE и ALTER) и операторы языка модификации данных DML (операторы INSERT и SELECT). Из-за этого план выполнения процедуры должен быть перекомпилирован перед выполнением оператора I NSERT, так как структура временной таблицы изменилась после первоначального создания плана выполнения. Любая хранимая процедура, которая создает временную таблицу, а затем работает с ней, приведет к перекомпиляции плана, так как информация о структуре таблицы не существовала, когда план выполнения был изначально создан. Однако процедура в листинге 1.4 вызывает дополнительную перекомпиляцию, потому что она изменяет структуру таблицы, а затем работает с ней. Листинг 1.4. Используя один оператор CREATE TABLE и два варианта оператора ALTER TABLE, можно избежать данной проблемы CREATE PROC testp @var int AS CREATE TABLE #temp (kl int identity) IF @var=l ALTER TABLE #temp ADD cl int ELSE ALTER TABLE #temp ADD cl varcharB) INSERT #temp DEFAULT VALUES EXECC'SELECT cl FROM #temp') GO При работе с большими процедурами в приложениях, требующих высокой производительности, это может привести к проблемам с производительностью, а также к проблемам блокировок и параллелизации, так как в это время на хранимую процедуру накладывается блокировка компиляции. В листинге 1.5 представлен вариант, не использующий динамический T-SQL. Листинг 1.5. Решение проблемы создания временной таблицы CREATE PROCEDURE testp4 AS INSERT temp DEFAULT VALUES SELECT cl FROM #temp GO CREATE PROC testp3 продолжение ^>
34 Глава 1. Основы хранимых процедур Листинг 1.5 {продолжение) AS CREATE TABLE #temp (kl int identity, cl varcharB)) EXEC dbo.testp4 GO CREATE PROC testp2 AS CREATE TABLE #temp (kl int identity, cl int) EXEC dbo.testp4 GO CREATE PROC testp @var int AS IF @var=l EXEC dbo.testp2 ELSE ■ ■ EXEC dbo.testp3 GO Хотя эта методика снижает потребность в ЕХЕС(), она также вынуждает нас полностью реорганизовать хранимую процедуру. Фактически нам необходимо разбить первоначальную процедуру на четыре отдельных и вызывать четвертую из второй и третьей. Зачем? Прежде всего, вместо двух операторов CREATE TABLE для одной и той же временной таблицы в одной процедуре, которые, как мы выяснили, не поддерживаются, мы переместили каждый оператор CREATE TABLE в отдельную процедуру. Во-вторых, так как временная таблица автоматически удаляется, как только она выходит за пределы области видимости, мы не можем просто создать ее, затем вернуться к программе уровнем выше и добавить в нее записи или применить к ней оператор SELECT. Мы должны сделать это либо в одной из процедур, которая ее создала, либо в общей процедуре, которая их вызывает. Мы выбрали последнее, так что процедуры два и три вызывают четвертую, которая вставляет запись во временную таблицу и выбирает из нее значения столбца с1. (Поскольку объекты, созданные в процедуре, видны процедурам, которые она вызывает, четвертая «видит» таблицу, созданную вызывающей ее процедурой.) Эта методика хоть и работает, но не является оптимальной. А теперь представьте, насколько сложно работать с действительно большой процедурой. Разбиение ее на множество отдельных частей может оказаться нецелесообразным. Однако это избавляет нас от необходимости создания и выполнения прямого запроса T-SQL, и, в общем, этот способ лучше. Просмотр текста хранимой процедуры Предполагая, что объект не зашифрован, можно просмотреть исходный код процедуры, представления, триггера, пользовательской функции (UDF), правила или значения по умолчанию, используя системную процедуру sp_he I ptext. Пример приведен в листинге 1.6. Листинг 1.6. sp_helptext показывает исходный код хранимой процедуры EXEC dbo.sp_helptext 'ListCustomersByCity' Text CREATE PROCEDURE dbo.ListCustomersByCity @Country nvarcharC0)=T
Создание хранимой процедуры 35 AS \ . •■ SELECT City. COUNTt*) AS NumberOfCustomers FROM Customers WHERE Country LIKE @Country GROUP BY City Разрешения и ограничения CREATE PROCEDURE могут выполнять только члены ролей sysadmin, db_owner или db_dd I adm i n (или те, кому члены вышеназванных ролей явно предоставили разрешение CREATE PR0C). Максимальный размер хранимой процедуры — 128 Мбайт. Максимальное число параметров процедуры — 1024. Советы по созданию Включайте в каждую процедуру заголовок-комментарий, в котором указывайте ее автора, предназначение, дату создания и историю изменений, параметры, которые она получает и т. д. Обычно этот блок с комментариями помещают непосредственно до или сразу после оператора CREATE PR0C (но перед остальной частью процедуры) для того, чтобы комментарии можно было хранить в таблице syscomments и просматривать их, используя утилиты Enterprise Manager и Query Analyzer. Системная хранимая процедура sp_obj ect_sc r i pt_comments генерирует заголовки-комментарии для хранимых процедур, представлений и других объектов (листинг 1.7). Листинг 1.7. Использование sp_object_script_comments для создания заголовков-комментариев хранимых процедур USE master GO IF OBJECT_ID('dbo.sp_object_script_comments') IS NOT NULL DROP PROC dbo.sp_object_script_comments GO CREATE PROCEDURE dbo.sp_object_script_comments -- Required parameters @objectname sysname=NULL. @desc sysname=NULL. -- Optional parameters @parameters varchar(8000)=NULL. @example varchar(8000)=NULL. @author sysname=NULL. @workfile sysname=''. -- Force workfile to be generated @email sysname='(none)'. (aversion sysname=NULL. (Prevision sysname='0'. @datecreated smalldatetime=NULL. @datelastchanged smalldatetime=NULL /* Object: sp_object_scnpt_comments Description: Generates comment headers for SQL scripts Usage: sp_object_script_comments @objectname='ObjectName'. @desc='Description of object".@parameters='paraml[.param2...]' Returns: (None) продолжение ^
36 Глава 1. Основы хранимых процедур Листинг 1.7 {продолжение) $Workfile: sp_object_script_comments.sql $ $Author: Khen $. Email: khen@khen.com $Revision: 1 $ Example: sp_object_script_comments @objectname='sp_who'. @desc='Returns a list of currently running jobs'. @parameters=[@loginname] Created: 1992-04-03. SModtime: 1/4/01 8:35p $. */ AS IF (@objectname+@desc) IS NULL GOTO Help PRINT 7*' PRINT CHARC13) EXEC sp_usage @objectname=@objectname. @desc=@desc. @parameters=@parameters, @example=Cexamp]e. @author=@author. @workfile=@workfi]e, @email=@email. @versi on=@versi on. @revi sion=@revi si on. @datecreated=@datecreated. @date1astchanged=@datelastchanged PRINT CHARA3)+'*/' ;. RETURN 0 Help: EXEC dbo.sp_usage @objectname='sp_object_script_comments'. @desc='Generates comment headers for SQL scripts'. @parameters='@objectname=''ObjectName''. @desc=''Description of object".@parameters=''paraml[,param2...]'''. @example='sp_object_script_cornrnents @objectname=''sp_who''. @desc=''Returns a list of currently running jobs''. @parameters=[@loginname]'. @author='Ken Henderson'. @workfile='spj*ject_script_comments.sql'. @email='khen@khen.com'. @version='3'. @revision='l'. @datecreated='19920403'. @datelastchanged='19990701' RETURN -1 GO EXEC dbo.sp_object_script_comments Эта процедура генерирует заголовки-комментарии для хранимых процедур, используя процедуру sp_usage, которую мы рассмотрим позже в этой главе. Эта процедура может быть выполнена из любой базы данных любой процедурой. Чтобы использовать sp_obj ect_sc r i pt_comments, просто задайте ей необходимые параметры, и она создаст полностью готовый блок комментариев, который описывает процедуру или другой тип объекта и включает описание его использования и другую информацию. Вы можете скопировать этот текстовый блок и вставить его не-
Создание хранимой процедуры 37 посредственно в заголовок программы, тогда у вас появится хорошо отформатированный, информативный блок с комментариями для вашего кода. В системах с большим объемом кода хранимых процедур принято располагать каждую хранимую процедуру в ее собственном файле сценария и хранить каждый сценарий в системе контроля версий или в системе управления исходным кодом. Многие из этих систем поддерживают специальные метки (они известны как ключевые слова в Visual SourceSafe [VSS] — системе управления исходным кодом), которые можно вставить в комментарии T-SQL. С помощью этих меток система управления исходным кодом может автоматически вставлять информацию об изменениях, имя человека, изменившего файл последним, дату и время последнего изменения и т. д. Поскольку метки расположены в комментариях, нет никакой опасности, что изменения в них сделают код неработоспособным. На самом деле вы всего лишь позволяете системе вести часть вашего «домашнего хозяйства» под общим названием «управление исходными текстами». Многие из хранимых процедур, приведенных в этой книге, имеют в заголовках метки, распознаваемые VSS (эти метки начинаются и заканчиваются символом $). Подробнее об этом — в главе 4. Разрешен вызов процедуры со справочным параметром типа /? или без параметров, чтобы вернуть информационное сообщение, указывающее вызывающей программе, как использовать процедуру. Разместите часть, которая генерирует эту справочную информацию, в конце процедуры, чтобы она не мешала и чтобы ее местонахождение было постоянным для любой процедуры. Идеальный способ сделать это — создать и вызвать отдельную процедуру, которая принимает параметры, показывающие информацию по использованию, и возвращает се в единообразном формате. Ниже приведена именно такая хранимая процедура. Листинг 1.8. С помощью sp_usage можно генерировать информацию об использовании хранимой процедуры USE master GO IF OBJECTJDCdbo.spjJsage') IS NOT NULL DROP PROC dbo.sp_usage GO CREATE PROCEDURE dbo.sp_usage -- Required parameters Oobjectname sysname=NULL. @desc sysname=NULL, -- Optional parameters @parameters varchar(8000)=NULL, @returns varchar(8000)='(None)'. @example varchar(8000)=NULL. @workfile sysname=NULL, @author sysname=NULL. @email sysname='(none)'. (aversion sysname=NULL, (Prevision sysname='0'. @datecreated smalldatetime=NULL. @datelastchanged smalldatetime=NULL Object: sp_usage Description: Provides usage information for stored procedures and descriptions of other types of objects продолжение ё>
38 Глава 1. Основы хранимых процедур Листинг 1.8 {продолжение) Usage: sp_usage @objectname='ObjectName'. @desc='Description of object' [, @parameters='paraml,param2...'] [, @example='Example of usage'] [. @workfile='File name of script'] [. @author='Object author'] [. @email='Author email'] [, @version='Version number or info'] [. @revision='Revision number or info'] [. @datecreated='Date created'] [. @datelastchanged='Date last changed'] Returns: (None) SWorkfile: sp_usage.sql $ SAuthor: Khen $. Email: khen@khen.com SReviSion: 7 $ Example: sp_usage @objectname='sp_who', @desc='Returns a list of currently running jobs'. @parameters=[@loginname] Created: 1992-04-03. IModtime: 1/04/01 B:38p $. */ AS SET N0C0UNT ON IF (@objectname+@desc IS NULL) GOTO Help PRINT 'Object: '+@objectname PRINT 'Description: '+@desc IF @BJECTPR0PERTY@BJECT_ID(@objectname).'IsProcedure')=1) OR @BJECTPR0PERTY@BJECT_ID(@objectname).'IsExtendedProc')=l) OR @BJECTPR0PERTY@BJECT_ID(@objectname).'IsReplProc')=1) OR (L0WER(LEFT(@objectname.3))='sp_/) BEGIN -- Special handling for system procedures PRINT CHARA3)+'Usage: '+@objectname+' '+@parameters PRINT CHARA3)+'Returns: '+@returns END -- $NoKeywords: $ -- Prevents the keywords below from being expanded in VSS IF (@workfile IS NOT NULL) PRINT CHARA3)+'$Workfile: '+@workfile+' $' IF (@author IS NOT NULL) PRINT CHARA3)+'$Author: '+@author+' $. Email: '+@email IF «version IS NOT NULL) PRINT CHARA3)+'$Revision: '+@version+'.'+@revision+' $' IF (@example IS NOT NULL) PRINT CHARA3)+'Example: '+@example IF (@datecreated IS NOT NULL) BEGIN -- Crop time if it's midnight DECLARE @datefmt varchar(8000). @dc varcharC0). @lc varcharC0) SET @dc=CONVERT(varcharC0). @datecreated. 120) SET @lc=C0NVERT(varcharC0). @datelastchanged. 120) PRINT CHARA3)+'Created: '+CASE DATEDIFF(ss.C0NVERT(char(8),@datecreated.108).'00:00:00') WHEN 0 THEN LEFT(@dc.lO) ELSE @dc END +'. IModtime: '+CASE DATEDIFF(SS.C0NVERT(char(8).@datelastchanged.108).'00:00:00') WHEN 0 THEN
Создание хранимой процедуры 39 LEFT(@lc,10) ELSE @lc END+' $.' END RETURN 0 Help: EXEC dbo.sp_usage @objectname='sp_usage', -- Recursive call @desc='Provides usage information for stored procedures and descriptions of other types of objects'. @parameters='@objectname=''ObjectName'', @desc=''Description of object'' @parameters=''paraml,param2...''J @example-''Example of usage''] @workfile=''File name of script''] @author=''Object author''] @email=''Author email''] @version=''Version number or info''] @revision=''Revision number or info'1] @datecreated=''Date created''] @datelastchanged=''Date last changed'']', @example='sp_usage @objectname=''sp_who''. @desc=''Returns a list of currently running jobs''. @parameters=[@loginname]'. @author='Ken Henderson'. @workfi1e='sp_usage.sql'. @email='khen@khen.com'. @version='3'. @revision='l'. @datecreated='4/3/92'. @datelastchanged='7/l/99' RETURN -1 GO EXEC dbo.spjjsage Передавая соответствующие параметры, вы можете использовать sp_usage, чтобы вывести информацию об использовании любой процедуры. Sp_usage вызывает сама себя для той же самой цели. Так как Transact-SQL не поддерживает подпрограммы, sp_usage использует переход к метке с помощью команды GOTO, чтобы разместить справочное сообщение в конце процедуры. Этот подход позволяет коду в начале процедуры провести проверку правильности параметров и при необходимости перейти на код, который выведет информацию об использовании. Установите значения настроек QUOTEDJDENTI FIER и ANSLNULLS до выполнения оператора CREATE PROCEDURE (в его собственном командном пакете), так как при выполнении им вновь присваиваются величины, которые у них были при создании процедуры (их значения находятся в столбце status в соответствующей процедуре записи таблицы sysobj ects). Эти изменения действуют, только пока выполняется процедура; потом значения восстанавливаются в то состояние, в котором они были до выполнения процедуры. Установка значений настроек QU0TED_ I DENT I FI ER или ANSI_NULLS внутри хранимой процедуры никак не влияет на выполнение хранимой процедуры. Чтобы увидеть, как это работает, запустите код из листинга 1.9 в Query Analyzer. Листинг 1.9. SET ANSI_NULLS не имеет никакого эффекта внутри хранимой процедуры USE tempdb GO SET ANSI NULLS ON продолжение $■
40 Глава 1. Основы хранимых процедур Листинг 1.9 {продолжение) GO CREATE PROC testn AS SET ANSIJULLS OFF DECLARE @var int SET @var=NULL SELECT * FROM Northwind..Customers WHERE @var=NULL GO EXEC testn (Результаты сокращены) CustomerlD CompanyName ContactName @ row(s) affected) Если бы настройка ANS l_NULLS во время выполнения SELECT имела значение off (выключено), как было задано командой SET внутри процедуры, то SELECT должен был бы вернуть все записи из таблицы Customers. Как вы видите, этого не происходит. Теперь измените команду SET ANS I _NULLS, находящуюся перед CREATE PROCEDURE, так, чтобы отключить сравнение пустых значений в соответствии со спецификацией ANSI (OFF), и перезапустите процедуру. Вы должны увидеть все записи из таблицы Customers. Устанавливайте параметры окружения (например, N0C0UNT, L0CK_J IME0UT и т. п.) в начало процедуры, так как они могут существенно повлиять на нее. Пусть у вас войдет в привычку устанавливать их в самом начале процедуры, чтобы они были понятны другим разработчикам. Избегайте прерывания цепочек владения при работе с хранимыми процедурами и объектами, на которые они ссылаются. Попытайтесь обеспечить, чтобы владелец хранимой процедуры и владелец объектов, на которые она ссылается, был один и тот же. Лучший способ сделать это — указывать для каждого создаваемого объекта в качестве владельца пользователя dbo. Наличие множества объектов с одинаковым названием, но с разными владельцами вносит путаницу в базу данных и в результате от этого больше проблем, чем пользы. Это может быть полезно' на стадии разработки проекта, но при рутинной работе стоит этого избегать. Некоторые команды при использовании в пределах хранимой процедуры требуют, чтобы у объектов, на которые они ссылаются, был указан владелец (считается, что у обозначения объекта определен владелец, когда название объекта имеет префикс с именем владельца и точкой), если процедура выполняется пользователями, отличными от владельца. Вот эти команды: ■ CREATE TABLE ■ ALTER TABLE ■ DROP TABLE ■ TRUNCATE TABLE ■ CREATE INDEX ■ DROP INDEX ■ UPDATE STATISTICS ■ все команды DBCC.
Изменение хранимых процедур 41 Используйте префикс sp_ только для системных процедур. Из-за путаницы, которую это может вызвать, старайтесь не создавать процедуры с префиксом sp_ в пользовательских базах данных. Также не создавайте несистемные процедуры в базе данных maste г. Если процедура не является системной, то, скорее всего, вам вообще не стоит помещать ее в базу данных master. Включите USE <имя базы данных> в начало сценариев создания для процедур, которые должны храниться в определенной базе данных. Это позволяет не заботиться об установке контекста текущей базы данных перед запуском сценария. Старайтесь, чтобы каждая хранимая процедура была как можно более простой и модульной. В идеале хранимая процедура должна выполнять одну задачу или группу тесно связанных задач. Как правило, SET NOCOUNT ON должен быть первым оператором в каждой создаваемой вами хранимой процедуре, так как это уменьшает сетевой трафик между SQL Server и клиентскими приложениями. Установка NOCOUNT отключает сообщения D0NE_IN_PROC — SQL Server обычно посылает эти сообщения клиенту, указывая число записей, обработанных оператором T-SQL. Так как эти сообщения очень редко используются, опустив их, можно уменьшить нагрузку на сеть, не потеряв при этом функциональности и значительно выиграв в скорости работы приложений. Обратите внимание: сообщения D0NE_ I N_PR0C можно отключить для всего сервера, воспользовавшись флагом трассировки C640), а для определенной пользовательской сессии, используя команду sp_conf i gure user opt i ons. (В редких случаях отключение сообщений D0NE_ I N_PR0C может вызвать проблемы с некоторыми приложениями, например с некоторыми старыми версиями Microsoft Access и отдельными OLEDB-провайдерами.) Если вы хотите, чтобы код хранимой процедуры нельзя было просмотреть, создайте ее с параметром WITH ENCRYPTION. He удаляйте ее текст из syscomments, так как это сделает ее неработоспособной и ее придется создавать заново. Изменение хранимых процедур Используя команду CREATE PROCEDURE, вы создаете хранимые процедуры. Для их изменения вы можете использовать команду ALTER PROCEDURE. Преимущество ее использования в том, что сохраняются права доступа, тогда как при использовании CREATE PROCEDURE этого не происходит. Ключевое различие между ними в том, что ALTER PROCEDURE требует использования тех же параметров шифрования и перекомпиляции, какие изначально были использованы при ее создании оператором CREATE PROCEDURE. Если вы опустите или измените их значение при выполнении ALTER PROCEDURE, то они будут также опущены или изменены в самом определении процедуры. Процедура может содержать любую допустимую команду Transact-SQL, кроме следующих: CREATE DEFAULT, CREATE FUNCTION, CREATE PROC, CREATE RULE, CREATE SCHEMA, CREATE TR I GGER, CREATE VI EW, SET SH0WPLAN_TEXT и SET SH0WPLAN_ALL. Эти команды должны находиться в отдельных пакетах команд и поэтому не могут быть частью хранимой процедуры. Из процедур можно создавать базы данных, таблицы и индексы, но нельзя создавать другие процедуры, стандартные значения, функции, правила, схемы, триггеры или представления.
42 Глава 1. Основы хранимых процедур СОВЕТ Вы можете обойти это ограничение (по созданию объектов изнутри хранимых процедур), используя sp_executesql или функцию EXECQ, как показано в листинге 1.10. Листинг 1.10. Используя sp_executesql и ЕХЕС(), можно создавать процедуры, представления, UDF и другие объекты из хранимых процедур CREATE PROC test AS DECLARE @sql nvarchar(lOO) SET @sql=N'create proc dbo.test2 as select ■'i'1' EXEC dbo.sp_executesql @sql EXEC dbo.test2 GO EXEC dbo.test (Результаты сокращены) Cannot add rows to sysdepends for the current stored procedure because it depends on the missing object 'dbo.test2'. The stored procedure will still be created. 1 Это предупреждение появляется вследствие того, что процедуры test_2 еще не существует при создании процедуры test. Вы можете его полностью проигнорировать. Выполнение хранимых процедур Хотя выполнить хранимую процедуру можно, просто написав ее имя в пакете команд, возьмите в привычку писать EXEC перед именем хранимой процедуры: EXEC dbo.sp_who Вызов хранимой процедуры без ключевого слова EXEC должен быть первым в пакете команд, поэтому, если в дальнейшем перед вызовом процедуры добавятся какие-нибудь строки, ваш код перестанет работать. Добавляйте префикс имени владельца при вызове процедуры (dbo в предыдущем примере). Если не указать владельца, сервер установит блокировку компиляции на хранимую процедуру, так как он не может сразу найти его в кэше процедуры. Эта блокировка снимается, как только владелец найден в кэше, однако это может привести к проблемам в приложениях, требующих высокой производительности. Указание владельца — просто хорошая привычка. Это одна из тех вещей, которую вы можете сделать, чтобы заранее избавить себя от проблем. Использование команды INSERT вместе с EXEC С помощью команды INSERT можно вставить в таблицу результат выполнения хранимой процедуры, в листинге 1.11 показано, как это сделать. Листинг 1.11. Использование связки INSERT...EXEC для сохранения результатов выполнения хранимой процедуры в таблице CREATE TABLE #locks (spid int. dbid int, objid int, objectname sysname NULL, indid int. type charD), resource charA5). mode char(lO), status
Выполнение хранимых процедур 43 charF)) INSERT flocks (spid, dbid. objid. indid. type, resource, mode, status) EXEC dbo.spjock SELECT * FROM flocks DROP TABLE flocks Этот способ удобен для сохранения результатов хранимой процедуры в таблице для дальнейшего использования. До появления выходных параметров курсорного типа этот способ был единственным для работы с наборами данных, возвращаемых хранимыми процедурами. Связку INSERT...EXEC также можно использовать и при работе с расширенными хранимыми процедурами. Простой пример приведен в листинге 1.12. Листинг 1.12. INSERT...EXEC можно использовать и с расширенными хранимыми процедурами CREATE TABLE #cmd_result (output varchar(8000)) INSERT #cmd_resu"!t EXEC master.dbo.xp_cmdshell 'TYPE C:\B00T.INI' SELECT * FROM #cmd_result DROP TABLE #cmd_result Компиляция плана выполнения и выполнение При первом запуске хранимой процедуры она компилируется, и для нее создается план выполнения. План компилируется не в машинный код и даже не в байт-код, а псевдокомпилируется для ускорения выполнения. Под «псевдокомпилируется» я имею в виду, что определяются ссылки на объекты, выбираются способы объединения и индексы и оптимизатор SQL Server строит наиболее эффективный план для выполнения хранимой процедуры. Оптимизатор сравнивает различные планы выполнения и выбирает тот, использование которого будет стоить меньше в терминах полного времени выполнения. Оптимизатор принимает решение, основываясь на множестве параметров, включая оценку количества операций ввода- вывода, связанную с каждым планом, потребность в процессорном времени, требуемый объем памяти и т. д. После создания план выполнения помещается в кэш для дальнейшего использования. Этот кэш растет и уменьшается по мере необходимости, чтобы хранить планы выполнения хранимых процедур и прямых запросов, выполненных сервером. SQL Server поддерживает равновесие между объемами памяти, выделяемыми под процедурный кэш и для других целей (например, для кэша данных). Очевидно, что память, используемая для кэширования планов выполнения, не может использоваться для кэширования данных, так что сервер очень осторожно управляет соотношением объемов памяти процедурного кэша и кэша данных. Кэширование плана выполнения позволяет оптимизатору не перестраивать его при каждом запуске процедуры, что очень сильно повышает производительность. Мониторинг выполнения Можно наблюдать за тем, как SQL Server компилирует, сохраняет и использует планы выполнения при помощи утилиты SQL Server — Profiler. Чтобы посмотреть, что происходит, когда создается и запускается процедура, выполните следующие действия.
44 Глава 1. Основы хранимых процедур 1. Запустите Query Analyzer, подключитесь к серверу и загрузите процедуру из листинга 1.1 (вы можете найти полный сценарий на компакт-диске, прилагаемом к этой книге). 2. Запустите утилиту Profiler (Старт ► Программы ► Microsoft SQL Server). 3. Нажмите клавишу New Trace и подключитесь к серверу. 4. На вкладке Events уберите все классы событий, кроме SQLBatchStarting в группе событий TSQL. 5. Добавьте все классы событий из группы Stored Procedures, кроме SP:StmtStarting и SP:StmtComplete. (На прилагаемом к книге компакт-диске вы можете найти файл BasicTrace.TDF, в котором находится шаблон этой трассировки.) 6. Нажмите кнопку Run внизу окна Trace Properties Dialog. 7. Вернитесь в Query Analyzer и запустите сценарий. 8. Вернитесь в Profiler и нажмите кнопку Stop Selected Trace. В окне событий вы должны увидеть подобное такому коду: (Результаты сокращены) EventClass TextData SQL:BatchStartlng Use Northwind SQL:BatchStarting IF 0BJECT_ID('dbo.ListCustomersByCi SQL:BatchStartlng CREATE PROCEDURE dbo.ListCustomersB SQL:BatchStarting EXEC dbo.ListCustomersByCity SP:CacheMiss SP:CacheMiss SP:CacheInsert SP:Starting EXEC dbo.ListCustomersByCity SP:Completed EXEC dbo.ListCustomersByCity Трассировка начинается с четырех отдельных пакетов команд T-SQL. Поскольку команды отделены ключевым словом GO — признаком конца пакета, — каждый набор команд выполняется как отдельный пакет T-SQL. Последний пакет — вызов хранимой процедуры с использованием команды EXEC. Этот вызов генерирует остальные события. Обратите внимание: событие SP: Cache I nsert идет непосредственно перед SP:Starting вместе с событием SP:CacheMiss. Это говорите том, что ListCustomers- ВуС i ty отсутствовала в кэше процедур, когда она была вызвана, так что план выполнения был скомпилирован и помещен в кэш. Два последних события: SP: Sta rt i ng и SP: Сотпр I eted — указывают на то, что, как только план выполнения хранимой процедуры был помещен в кэш, она была выполнена. Чтобы посмотреть, что происходит, когда процедура выполняется напрямую из кэша, выполните следующие действия. 1. Нажмите кнопку Start Selected Trace и перезапустите трассировку. 2. Вернитесь в Query Analyzer, выделите строчку с EXEC и запустите ее. 3. Вернитесь в Profiler и остановите трассировку. Вы увидите: (Результаты сокращены) EventClass TextData SQL:BatchStartlng EXEC dbo.ListCustomersByCity
Выполнение хранимых процедур 45 SP:ExecContextHit / SP:Starting EXEC dbo.ListCustomersByCity SP:Completed EXEC dbo.LiStCustomersByCity Событие ExecContextH i t свидетельствует о том, что версия хранимой процедуры, готовая для выполнения, была найдена в кэше. Обратите внимание на отсутствие событий SP.CacheMiss и Cache Insert. Это говорит о том, что план выполнения, который был создан и помещен в кэш, когда мы запустили хранимую процедуру в первый раз, был использован повторно, когда мы снова выполнили ее. Планы выполнения Когда SQL Server запускает план выполнения, каждый шаг плана обрабатывается и посылается соответствующему внутреннему управляющему процессу (например, T-SQL-менеджеру, DDL- и DML-менеджерам, менеджеру, управляющему транзакциями, менеджеру хранимых процедур, сервисному менеджеру, менеджеру ODSOLE и т. д.). SQL Server вызывает эти процессы, пока не обработает все шаги плана выполнения. Планы выполнения никогда не сохраняются на диске. Единственная часть хранимой процедуры, которая хранится на диске, — ее исходный текст (в таблице syscomments). Поскольку они кэшируются, циклически повторяясь, сервер избавляется от всех текущих планов выполнения (так же, как это делает команда DBCC FREEPR0CCACHEO). SQL Server автоматически пересоздает план выполнения, если: ■ значения установок окружения при выполнении хранимой процедуры значительно отличаются от значений, которые имели установки при создании процедуры (более подробно установки окружения будут рассмотрены в этой главе); ■ значение в столбце schema^ver таблицы sysohjects изменилось для любого из объектов, на которые ссылается хранимая процедура. Значения столбцов schema_ver и base_schema_ver изменяются всегда, когда изменяется структура таблицы. Изменения структуры включают добавление и удаление столбцов, изменение типов, а также изменение правил и значений по умолчанию; ■ изменилась статистика для любого из объектов, на которые ссылается процедура. Это означает, что автоматическое создание и обновление статистики может вызвать перекомпиляцию процедуры; ■ удален индекс, использовавшийся в плане выполнения; ■ план выполнения хранимой процедуры не может быть получен из кэша. Для удаления планов выполнения из кэша используется алгоритм «Удаляется наименее часто используемый» (Least Recently Used, LRU). ■ В некоторых других случаях, например, когда временная таблица изменена установленное количество раз, когда операторы DDL и DML выполняются в произвольном порядке и когда вызывается системная процедура sp_conf igure (sp_conf igure вызывает DBCC FREEPROCCACHE). Ранее в этой главе мы рассматривали ограничения SQL Server, связанные с использованием нескольких операторов CREATE TABLE для создания временных таблиц в одной процедуре. Упоминалось, что подход, связанный с использованием динамического кода (листинг 1.4), вызывает повторную перекомпиляцию плана
46 Глава 1. Основы хранимых процедур во время выполнения. Чтобы убедиться в этом, перезапустите трассировку, которой мы пользовались, и повторно выполните хранимую процедуру из этого запроса. Заглянув в Profiler, вы должны увидеть нечто подобное коду: EventClass TextData SQL:BatchStarting exec testp 2 SQL:StmtStarting exec testp 2 SP:ExecContextHit SP:Starting exec testp 2 SQL:StmtStarting -- testp CREATE TABLE #temp (kl int identity) SQLStmtStarting -- testp IF @var=l SQL:StmtStarting -- testp ALTER TABLE #temp ADD cl varcharB) SP:Recompile SP:CacheMiss SP:CacheMiss SP:CacheInsert SQL:StmtStarting -- testp ALTER TABLE #temp ADD cl varcharB) SQL:StmtStarting -- testp INSERT temp DEFAULT VALUES SP:Recompile SP:CacheMiss SP:CacheMiss SP:CacheInsert SQL:StmtStarting -- testp INSERT #temp DEFAULT VALUES SQL:StmtStarting -- testp EXEC('SELECT cl FROM #temp') SQL:StmtStarting -- Dynamic SQL SELECT cl FROM #temp SP:Completed exec testp 2 Заметьте, что в ходе выполнения процедуры происходит не одно, а два события SP: Recompi le: одно — когда встречается оператор ALTER TABLE (этот оператор ссылается на временную таблицу, тем самым вызывая перекомпиляцию), и второе — когда встречается INSERT (этот оператор обращается к таблице, структура которой только что была изменена, снова вызывая перекомпиляцию плана). Предполагая, что вы заметили события класса SQL: StmtStart i ng или SP: StmtStart i ng в трассировке, вы, скорее всего, увидите и событие SP: Recompi le в окружении двух идентичных событий StmtStart ing. Первое означает, что оператор начал выполняться, но был отложен для выполнения перекомпиляции. Второе означает, что оператор фактически начал выполняться только теперь, после того как перекомпиляция завершилась. Эта последовательность остановок/запусков может сильно повлиять на время выполнения хранимой процедуры. Стоит повториться: создание временной таблицы внутри процедуры, которую вы затем обрабатываете другими способами, заставит план выполнения хранимой процедуры перекомпилироваться (способ избежать этого — использовать вместо временных таблиц локальные переменные типа tab I e). Кроме того, чередование DDL и DML в процедуре может также заставить план компилироваться повторно. Поскольку это может вызвать проблемы с производительностью и распараллеливанием, следует стараться избегать перекомпиляции плана, когда это возможно. Другой интересный факт, следующий из этой трассировки, состоит в том, что план выполнения динамического кода T-SQL, создаваемого процедурой, не кэши- руется. Обратите внимание, что в трассировке нет событий CacheM i ss, Cache I nsert, CacheH i t или ExecContextH i t для динамического запроса. Давайте посмотрим, что произойдет, если мы изменим вызов ЕХЕС() на sp_executesq I (листинг 1.13).
Выполнение хранимых процедур 47 Листинг 1.13. Для выполнения динамического кода T-SQL вместо ЕХЕС() можно использовать sp_executesql USE tempdb GO drop proc testp GO CREATE PROC testp @var int AS CREATE TABLE temp (kl int identity) IF @var=l ALTER TABLE temp ADO cl int ELSE ALTER TABLE temp ADD cl varcharB) INSERT #temp DEFAULT VALUES EXEC dbo.sp_executesql N'SELECT cl FROM #temp' GO exec testp 2 После запуска этой трассировки вы увидите нечто подобное следующему коду: EventClass TextData SOL: BatchSta rt i ng exec testp 2 SQL:Stmt.Starting exec testp 2 SP:CacheMiss SPiCacheMiss SP:CacheInsert SP:Starting SQL:StmtStarting SQL:StmtStarting SQL;StmtStarting SP:Recompile SP:CacheMiss SP:CacheMiss SP:CacheInsert SQL:StmtStarting SQL:StmtStarting SP:Recompile SP:CacheMiss SP:CacheMiss SP:CacheInsert SQL:StmtStarting SQL:StmtStarting SP:CacheMiss SP:CacheMiss SPXachelnsert SQL:StmtStarting SP.-Completed exec testp 2 -- testp CREATE TABLE temp (kl int identity) -- testp IF @var=l -- testp ALTER TABLE #temp ADD cl varcharB) testp ALTER TABLE #temp ADD cl varcharB) testp INSERT #temp DEFAULT VALUES testp INSERT temp DEFAULT VALUES testp EXEC dbo.sp_executesql N'SELECT cl FROM #temp' SELECT cl FROM temp SELECT cl FROM temp exec testp 2 Обратите внимание на то, что в этом случае происходит событие SP: Cache I nse rt для динамического кода, который вызывается посредством sp_executesql. Это
48 Глава 1. Основы хранимых процедур означает, что план выполнения для оператора SELECT был помещен в кэш и может быть использован в дальнейшем. Будет ли он фактически использован — это уже другой вопрос, но, по крайней мере, такая возможность существует. Если снова запустить эту процедуру, обнаружится, что вызов sp_executesq I генерирует событие ExecContextH i t, а не CacheM i ss, которое произошло в первый раз. Sp_executesq I задействует процедурный кэш, и процедура выполняется более эффективно. Отсюда вывод: sp_executesq I в общем более эффективен для выполнения динамического SQL, чем ЕХЕС(). Перекомпиляции плана выполнения Перекомпилировать план выполнения можно: ■ создав процедуру с параметром WITH RECOMPI LE; ■ выполнив процедуру с параметром WITH RECOMPI LE; ■ с помощью системной хранимой процедуры sp_recomp i I e, чтобы охватить все таблицы, на которые ссылается процедура (sp_recomp i I e обновляет поле schema_ve r таблицы sysobjects). Как только план выполнения находится в кэше, последующие вызовы процедуры могут использовать план повторно, без последующей его перестройки. В этом случае не требуется строить дерево запроса и создавать план выполнения, что обычно происходит при первом выполнении хранимой процедуры и является главным преимуществом хранимой процедуры перед пакетами T-SQL с точки зрения производительности. Автоматическая загрузка планов выполнения Наиболее логичный способ загрузки плана выполнения в кэш при запуске системы — использовать автоматически запускающиеся процедуры. Эти процедуры должны находиться в базе данных master, при этом они могут обращаться к процедурам, находящимся в других базах данных, тем самым загружая в память их планы выполнения. Если вы будете использовать этот метод, создайте одну процедуру, которая будет вызывать процедуры, планы выполнения которых следует загрузить в кэш, вместо того чтобы запускать каждую процедуру в отдельности. Это уменьшит количество потоков (так как автоматически запускающаяся процедура работает в отдельном потоке). :овет Чтобы автоматически запускающиеся процедуры не запускались при первоначальной загрузке SQL Server, можно запустить SQL Server с флагом трассировки 4022. Этот флаг указывает SQL Server не запускать автоматически запускающиеся процедуры без изменения их статуса автозапуска. Когда в следующий раз вы запустите сервер без этого флага, эти процедуры запустятся повторно. Выполнение хранимой процедуры при помощи протокола удаленного вызова процедур (RPC) Стоит упомянуть, что вызов хранимой процедуры может осуществляться и не из пакета T-SQL. ADO/OLEDB, ODBC и DB-Library API поддерживают выполнение хранимой процедуры при помощи протокола RPC {remoteprocedure call, уда-
Выполнение хранимых процедур 49 ленный вызов процедур). Поскольку этот способ обходит большую часть обработки параметров, вызов хранимых процедур через интерфейс RPC более эффективен, чем вызов из пакетов T-SQL. В частности, интерфейс RPC упрощает повторные вызовы процедуры с различными наборами параметров. Это можно проверить из Query Analyzer (который использует ODBC API), изменив строку с EXEC в сценарии на строку из листинга 1.14. Листинг 1.14. Вызов процедуры ListCustomersByCity через RPC {CALL dbo.ListCustomersByCity} Этот способ использует понятие ODBC для запуска процедур с помощью RPC. Перезапустите трассировку в Profiler, затем выполните команду CALL в Query Analyzer. В окне Profiler должно появиться следующее: (Результаты сокращены) EventClass TextData RPC:Starting exec dbo.ListCustomersByCity SP:ExecContextHit SP:Starting exec dbo.ListCustomersByCity SP:Completed exec dbo.ListCustomersByCity RPCCompleted exec dbo.ListCustomersByCity Обратите внимание на отсутствие события BatchStart i ng. Вместо этого появляется событие RPC: Start ing, сопровождаемое событием RPC: Completed. Из этого следует, что для вызова процедуры используется RPC API. Видно, что и при RPC API мы по-прежнему выполняем процедуру, используя существующий план из процедурного кэша. Временные процедуры Временные процедуры создаются так же, как и временные таблицы: путем добавления префикса # создается локальная временная процедура, видимая только в текущем соединении, а добавление префикса ## создает глобальную временную процедуру, доступную для всех соединений. Временные процедуры полезны, когда возникает необходимость объединить преимущества хранимых процедур, такие как повторное использование плана выполнения и расширенные возможности обработки ошибок с преимуществами прямых запросов. Поскольку можно создавать и запускать временные процедуры «налету», то в результате мы возьмем лучшее из обоих методов. В принципе, sp_executesqi может избавить нас от использования временных процедур, но мы можем их использовать, если нам необходима функциональность большая, чем может предоставить sp_executesq I. Системные процедуры Системные процедуры находятся в базе данных master и имеют префикс sp_. Вы можете выполнять системные хранимые процедуры из любой базы данных, при этом системная процедура выполняется в контексте этой базы данных. Так, например, если процедура ссылается на таблицу sysobjects (которая существует в каждой базе данных), она получит доступ к таблице sysobjects в базе данных, которая была текущей в момент запуска процедуры, а не к таблице из базы данных
50 Глава 1. Основы хранимых процедур master. В листинге 1.15 приведена системная процедура, которая выводит названия и даты создания для объектов, имена которых совпадают с шаблоном. Листинг 1.15. Пользовательская системная процедура, которая выводит названия объектов и даты их создания USE master IF OBJECTJD('dbo.sp_created') IS NOT NULL DROP PROC dbo.sp_created GO CREATE PROC dbo.sp_created @objname sysname=NULL /* Object: sp_created Description: Lists the creation date(s) for the specified objectCs) Usage: sp_created @objname="Object name or mask you want to display" Returns: (None) SAuthor: Khen $. Email: khen@khen.com SRevision: 2 $ Example: sp_created @objname="myprocsr Created: 1999-08-01. SModtime: 1/04/01 12:16a $. */ AS IF (@objname IS NULL) or (@objname=7?') GOTO Help SELECT name, crdate FROM sysobjects WHERE name like (Pobjname RETURN 0 Help: EXEC dbo.sp_usage @objectname='sp_created'. @desc='Lists the creation date(s) for the specified objectCs)'. @parameters='@objname="Object name or mask you want to display'", @examp1e='sp_created @objname="myprocsr'. @author='Ken Henderson'. @emai1='khen@khen.com', n='0'. ged='19990815' RETURN GO @version=T @datecreated -1 USE Northwind EXEC dbo.sp_created @revision= ='19990801', Order*' '0'. Matel astchan (Результаты сокращены) name crdate Order Details 2000-08-06 01:34 Order Details Extended 2000-08-06 01:34 Order Subtotals 2000-08-06 01:34 Orders 2000-08-06 01:34 Orders Qry 2000-08-06 01:34 08.470 10.873 11.093 06.610 09.780 Как уже было сказано, любая хранимая процедура, созданная вами или поставляемая с SQL сервером, будет выполняться в контексте текущей базы данных. В листинге 1.16 показан пример использования одной из хранимых процедур, по-
Выполнение хранимых процедур 51 ставляемых с SQL-сервером. Эта хранимая процедура может быть запущена из любой базы данных для получения информации об этой базе данных. Листинг 1.16. Системная хранимая процедура запускается в контексте текущей базы данных USE Northwind EXEC dbo.sp_spaceused database_name database_size unallocated space Northwind 163.63 MB 25.92 MB reserved data index_size unused 4944 KB 2592 KB 1808 KB 544 KB Процедура sp_spaceused использует информацию из нескольких системных таблиц SQL Server, чтобы построить отчет. Поскольку это системная хранимая процедура, она выполняется в контексте текущей базы данных несмотря на то, что находится она в базе данных maste r. Обратите внимание, что независимо от текущей базы данных вы можете выполнить системную хранимую процедуру в контексте заданной базы данных, добавив название базы данных к имени процедуры (как если бы она находилась в этой базе данных). Это проиллюстрировано в листинге 1.17. Листинг 1.17. Явное указание базы данных запускает хранимую процедуру в контексте этой базы данных USE pubs EXEC Northwind..sp_spaceused databasejiame database_size unallocated space Northwind 163.63 MB 25.92 MB reserved data index_s1ze unused 4944 KB 2592 KB 1808 KB 544 KB В этом примере процедура sp_spaceused показывает информацию об утилизации дискового пространства базы данных No rt hw i nd, хотя находится эта процедура в базе данных master, а текущая база данных - pubs. Это происходит потому, что мы указали название базы данных в имени хранимой процедуры. SQL Server определяет, что процедура sp_spaceused находится в базе данных master и выполняет ее в контексте базы данных Northw i nd. Разница между системными объектами и системными процедурами Созданные пользователем системные процедуры отображаются в Enterprise Manager как пользовательские объекты, а не как системные. Почему? Потому что бит в столбце status таблицы sysobj ects, указывающий на то, что процедура системная (ОхСООООООО), не устанавливается по умолчанию. Чтобы установить этот бит, можно воспользоваться недокументированной системной хранимой процедурой sp_MS_marksystemobj ect. Единственный параметр этой процедуры — это имя объекта, который вы хотите пометить как системный. Многие недокументированные функции и команды DBCC работают некорректно, будучи вызванными не из системных объектов (дополнительная информация в главе 22). Для того чтобы проверить,
52 Глава 1. Основы хранимых процедур установлен ли системный бит объекта, используйте свойство IsMSShi pped функции OBJECTPROPERTY (). В листинге 1.18 представлен фрагмент кода, демонстрирующий использование этой функции. Листинг 1.18. Системные процедуры и системные объекты — это два разных понятия USE master GO IF OBJECT_ID('dbo.sp_test') IS NOT NULL DROP PROC dbo.sp_test GO CREATE PROC dbo.sp_test AS select 1 GO SELECT OBJECTPROPERTY(OBJECT_ID('dbo.sp_test'),'IsMSSrnpped') AS 'System Object?', status, status & OxCOOOOOOO FROM sysobjects WHERE NAME = 'sp_test' GO EXEC sp_MSjnarksystemobject 'sp_test' GO SELECT OBJECTPROPERTY(OBJECT_ID('dbo.sp_test').'IsMSShipped') AS 'System Object?', status, status & OxCOOOOOOO FROM sysobjects WHERE NAME = 'sp_test' (результаты) System Object? status 0 1610612737 1073741824 A row(s) affected) System Object? status 1 -536870911 -1073741824 A row(s) affected) Как уже отмечалось, существует масса полезных явлений, которые работают неправильно вне системных процедур. Например, хранимая процедура не может управлять полнотекстовыми индексами с использованием DBCC CALLFULLTEXT(), если у нее не установлен системный бит. Независимо от того, будете ли вы этим пользоваться, полезно знать, как это работает. Расширенные хранимые процедуры Расширенные процедуры — это процедуры, располагающиеся в DLL и функционирующие так же, как и обычные процедуры. Они получают параметры и возвращают результаты, используя Open Data Services API SQL Server, и обычно пишутся на С или C++. Они должны находиться в базе данных master и выполняться в адресном пространстве процесса SQL Server. Хотя расширенные хранимые процедуры схожи с системными хранимыми процедурами, вызов расширенной хранимой процедуры немного отличается. Расширенные хранимые процедуры не будут автоматически найдены в базе данных master и не предполагается их выполнение в контексте текущей базы данных. Чтобы выполнить хранимую процедуру не из базы
Расширенные хранимые процедуры 53 данных maste г, следует полностью указать ее имя (например, EXEC maste г. dbo. xp_cmdshel 1 dir). Способ обойти это различие — «обернуть» расширенную хранимую процедуру в системную. Это позволяет выполнять ее из любой базы данных, не указывая префикс master. Этот способ используется для работы со многими расширенными хранимыми процедурами SQL Server. Многие из них «обернуты» в системные хранимые процедуры, которые предназначены только для того, чтобы сделать вызов процедур немного удобнее. В листинге 1.19 показан пример системной процедуры — «обертки» для вызова расширенной хранимой процедуры. Листинг 1.19. Системные процедуры обычно используются в качестве «оберток» для расширенных процедур USE master IF (OBJECTJDC'dbo.spjiexstring') IS NOT NULL) DROP PROC dbo.spjiexstring GO CREATE PROC dbo.spjiexstring @int varcharA0)=NULL, @hexstring > - varcharC0)=NULL OUT /* Object: spjiexstring Description: Return an integer as a hexadecimal string Usage: sp_hexstring @int=Integer to convert, Phexstring=OUTPUT parm to receive hex string Returns: (None) $Author: Khen $. Email: khen@khen.com $Revision: 1 $ Example: spjiexstring 3", @myhex OUT Created: 1999-08-02. $Modtime: 1/4/01 8:23p $. */ AS IF ((Pint IS NULL) OR (@int = 7?') GOTO Help DECLARE @i int. @vb varbinaryC0) SELECT @i=CAST(@int as int). @vb=CAST(@i as varbinary) EXEC master.dbo.xp_varbintohexstr @vb. @hexstring OUT RETURN 0 Help: EXEC sp_usage @objectname-'spjiexstring', @desc='Return an integer as a hexadecimal string'. @parameters='@int=Integer to convert, @hexstring=OUTPUT parm to receive hex string', @example='spjiexstring 3", @myhex OUT', @author='Ken Henderson'. @email = 'khen@khen.com', @version=T. @revision='0'. @datecreated='19990802', @datelastchanged='19990815' RETURN -1 GO DECLARE @hex varcharC0) EXEC spjiexstring 10. @hex OUT SELECT @hex (Результаты) OxOOOOOOOA
54 Глава 1. Основы хранимых процедур Вся задача процедуры sp_hexst ring сводится к тому, чтобы проверить параметры перед обращением к расширенной процедуре xp_varbintohexstr. Поскольку sp_hexstr i ng — системная процедура, ее можно вызывать из любой базы данных, не вызывая напрямую процедуру xp_varbintohexstr. Внутренние процедуры Множество системных хранимых процедур на самом деле не являются ни по-настоящему системными, ни расширенными процедурами — они реализованы внутри SQL Server. Это такие процедуры, как: sp_executesq I, sp_xml_prepareclocument, большинство зр_сиг5ог-процедур, sp_reset_connect ion и т. д. Эти процедуры имеют заглушки в master. . sysobjects и отображаются как расширенные процедуры, но они фактически реализованы внутри сервера, а не во внешних DLL. Это важно знать, так как их нельзя удалить или заменить другими DLL. Их можно изменить только путем правки непосредственно SQL Server, что обычно бывает только тогда, когда устанавливается пакет обновления. Параметры окружения Множество настроек окружения SQL Server затрагивает поведение хранимых процедур. Большинство настроек определяются командами SET. Они влияют на то, каким образом хранимые процедуры обрабатывают пустые значения, кавычки, курсоры, BLOB-поля и т. д. Две из них - QUOTED^ I DENT I FIER и ANSI „NULLS - сохраняются непосредственно в столбце status таблицы sysobjects, как уже упоминалось ранее. То есть когда создается хранимая процедура, значения этих двух настроек будут сохранены вместе с ней. Настройка QU0TED_ I DENT I FI ER определяет, интерпретировать ли строки в двойных кавычках как идентификаторы объектов (например, таблиц или столбцов). ANS I _NULLS определяет, разрешены ли не-ANSI сравнения с неопределенными (NULL) значениями. Настройка QUOTEDJ DENTI Fl ER обычно используется в хранимой процедуре, чтобы обращаться к объектам, содержащим в названии зарезервированные слова, пробелы или другие недопустимые символы. Пример приведен в листинге 1.20. Листинг 1.20. Настройка QUOTED_IDENTIFIER позволяет обращаться к объектам, в именах которых есть пробелы USE Northwind SET QUOTEDJ DENTI F1ER ON GO IF OBJECTJDCdbo.listorders') IS NOT NULL DROP PROC dbo.listorders GO CREATE PROC dbo.lnstorders AS SELECT * FROM "Order Details" GO SET QUOTEDJDENTIFIER OFF GO EXEC dbo.listorders (Результаты сокращены)
Параметры окружения 55 OrderlD 10248 10248 10248 10249 10249 10250 ProductID 11 42 72 14 51 41 UnitPrice 14.0000 9.8000 34.8000 18.6000 42.4000 7.7000 Quantity Discount 12 10 5 9 40 10 0.0 0.0 0.0 0.0 0.0 0.0 Таблица Order Details содержит и зарезервированное слово, и пробел, так что просто так на нее ссылаться нельзя. В этом случае мы включили поддержку идентификаторов в кавычках и заключили имя таблицы в двойные кавычки. Однако лучше использовать квадратные скобки [ ], поскольку тогда отпадает необходимость изменять какие-либо параметры. Имейте в виду, что имена объектов в квадратных скобках не поддерживаются стандартом ANSI/ISO SQL. Настройка ANSI_NULLS более полезна в хранимых процедурах. Она контролирует, работают ли не-ANSI сравнения на равенство с неопределенными значениями должным образом. Это особенно важно для хранимых процедур, параметры которых могут иметь значение NULL. Пример показан в листинге 1.21. Листинг 1.21. Настройка ANSI_NULLS позволяет сравнивать значения переменных и столбцов с NULL-значениями USE Northwind IF {OBJECTJDCdbo.ListRegionalEmployees') IS NOT NULL) DROP PROC dbo.ListRegionalEinployees GO SET ANSIJULLS OFF GO CREATE PROC dbo.LnstRegionalEmployees @regnon nvarcharOO) AS SELECT EmployeelD. LastName, FirstName. Region FROM employees WHERE Region=@region GO SET ANSI NULLS ON GO EXEC dbo.ListRegionalEmployees NULL (Результаты) EmployeelD LastName FirstName 5 Buchanan Steven 6 Suyama Michael 7 King Robert 9 Dodsworth Anne Region NULL NULL NULL NULL Благодаря настройке ANSI_NULLS процедура может успешно сравнить NULL-з начение параметра @region со значениями столбца region таблицы Employees базы данных Northwind. Запрос возвращает записи со значением NULL в поле region, так как SQL Server вопреки спецификации ANSI SQL определяет равенство NULL и значения столбца. Удобство этого метода становится особенно заметным тогда, когда процедура имеет множество параметров, которые могут принимать значение NULL. Если бы не было возможности сравнить значения NULL между собой, так же как и не-NULL-
56 Глава 1. Основы хранимых процедур значения, то каждый параметр, который мог бы принимать значение NULL, потребовал бы дополнительной обработки (возможно, пришлось бы использовать предикат IS NULL), тем самым увеличивая объем кода, необходимого для обработки параметров запроса. Поскольку SQL Server хранит значения настроек QU0TED_ I DENT IFIER и ANSI _NULLS вместе с каждой хранимой процедурой, можно быть уверенным, что они будут иметь необходимые значения, когда хранимая процедура будет запущена. Сервер устанавливает значения этих настроек такими, какими они были при создании хранимой процедуры, и восстанавливает значения после завершения выполнения. Проиллюстрируем это на примере: SET ANSI_NULLS ON EXEC dbo.ListReg-ionalEmployees NULL Хранимая процедура все равно выполняется так, как если бы ANS I _NULLS была установлена в OFF. Значения настроек QU0TED_ IDENTIFIER и ANS I _NULLS хранимой процедуры можно получить с помощью функции 0BJECTPROPERTY(). Пример приведен в листинге 1.22. Листинг 1.22. Получение значений настроек QUOTEDJDENTIFIER и ANSI_NULLS хранимой процедуры USE Northwind SELECT OBJECTPROPERTY(OBJECT_ID('dbo.ListRegionalEmployees'), ExecIsAnsiNullsOn') AS 'AnsiNulls' (Результаты сокращены) AnsiNulls 0 Другие настройки окружения влияют на ход выполнения хранимых процедур. SET XACT_ABORT, SET CURS0R_CL0SE_0N_C0MM IT, SET TEXTS IZE, SET IMPLICI T_TRANSACT IONS и многие другие помогают определить, как поведет себя хранимая процедура в ходе выполнения. Если вам необходимо, чтобы хранимая процедура имела определенное значение соответствующей настройки, определите его в процедуре как можно раньше и опишите в комментариях, зачем вам это понадобилось. Параметры Параметры хранимым процедурам можно задавать по имени или по позиции. Примеры показаны в листинге 1.23. Листинг 1.23. Установка параметров по имени или по позиции EXEC dbo.sp_who 'sa' EXEC dbo.sp_who @loginame='sa' Преимущество ссылки на параметры по имени состоит в том, что при этом параметры можно указывать в произвольном порядке. Параметр, для которого указано значение по умолчанию, можно опустить либо указать вместо него ключевое слово DEFAULT, как в листинге 1.24.
Параметры 57 Листинг 1.24. Установка значения параметра по умолчанию с помощью ключевого слова DEFAULT EXEC dbo.sp_who @"log-iname=DEFAULT Также для отдельных параметров можно указать значение NULL. Это бывает удобно для процедур, которые реализуют какие-либо особенности, когда параметр опущен или указано значение NULL. Это удобно, если процедура должна себя вести по- разному в зависимости от того, был ли при вызове параметр опущен или установлен в NULL. Пример приведен в листинге 1.25. Листинг 1.25. Параметру можно присвоить значение NULL EXEC dbo.sp_who @loginame=NULL (Результаты сокращены) spid 1 2 3 4 5 6 7 8 9 10 11 12 13 51 52 53 ecid 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 status background background sleeping background background sleeping background background background background background background background sleeping sleeping sleeping loginame sa sa sa sa sa sa sa sa sa sa sa sa sa SKREWYTHIN\khen SKREWYTHIN\khen SKREWYTHIN\khen В этом случае процедура sp_who возвращает список всех активных соединений, так как параметр @ I og i name равен NULL. Если процедуре sp_who передать имя учетной записи, то она вернет только те соединения, которые были установлены с использованием этой учетной записи. Мы бы увидели тот же результат, если бы вообще опустили параметр @l og i name — были бы выведены все соединения. Получение кода завершения Процедуры могут возвращать код завершения, используя команду RETURN. Пример приведен в листинге 1.26. Листинг 1.26. Использование команды RETURN для возврата кода завершения RETURN(-100) и RETURN -100 Эти два оператора возвращают код завершения -100. Код завершения 0 означает успех, значения от -1 до -14 указывают на различные ошибки (см. описания в Books Online), значения от -15 до -99 зарезервированы для будущего использования. Получить код завершения хранимой процедуры можно, сохранив его в переменную типа i nteger, как показано в листинге 1.27.
58 Глава 1. Основы хранимых процедур Листинг 1.27. Присваивание целочисленной переменной кода завершения хранимой процедуры DECLARE @res int EXEC @res=dbo.sp_who SELECT @res • ■ Выходные параметры В дополнение к коду завершения (вернуть его можно из любой хранимой процедуры), можно использовать выходные параметры, чтобы вернуть значения других типов. Этими параметрами могут быть целые числа, строки, даты и даже курсоры. Пример приведен в листинге 1.28. Листинг 1.28. Выходные параметры типа cursor удобны для возврата наборов данных USE pubs IF 0BJECTJD('dbo.listsales') IS NOT NULL DROP PROC dbo.listsales GO CREATE PROC dbo.listsales (^bestseller tid OUT. Ptopsales int OUT. fealescursor cursor varying OUT AS SELECT @bestseller=bestseller, @topsales=totalsales FROM ( SELECT TOP 1 titlejd AS bestseller. SUM(qty) AS totalsales FROM sales GROUP BY titlejd ORDER BY 2 DESC) bestsellers DECLARE s CURSOR LOCAL FOR SELECT * FROM sales OPEN s SET @salescursor=s RETURN(O) GO DECLARE @topsales int, (^bestseller tnd. @salescursor cursor EXEC dbo.listsales (^bestseller OUT, @topsales OUT. @salescursor OUT SELECT (^bestseller. @topsales FETCH @salescursor CLOSE @salescursor DEALLOCATE @salescursor (Результаты сокращены) PS2091 108 storjd ordjium ord_date qty payterms title_nd 6380 6871 1994-09-14 5 Net 60 BU1032 Использование выходного параметра типа cursor — это хорошая альтернатива возврату данных вызывающей стороне. Используя выходной параметр типа cursor вместо обычного набора данных, можно позволить вызывающей стороне контролировать, как и когда обрабатывать результат запроса. Вызывающий может получить
Параметры 59 информацию о курсоре, используя системную функцию, еще до обработки результата. Выходные параметры обозначаются ключевым словом 0UPUT (можно сократить его до OUT). Учтите, что ключевое слово OUT необходимо указывать в списке параметров и при запуске процедуры с помощью команды EXEC. Выходные параметры также должны быть определены в списке параметров хранимой процедуры, как и при ее вызове. Ключевое слово VARIYNG необходимо для параметров типа си rsor и означает, что процедура может вернуть более одного значения. Параметры типа си rsor могут быть только выходными, поэтому ключевое слово OUT также должно быть указано. Список параметров хранимой процедуры Можно получить список параметров процедуры (который включает и код завершения —параметр 0), используя запрос к представлению INFORMATI0N_SCHEMA.PARAMETERS (листинг 1.29). Листинг 1.29. Запрос к INFORMATION_SCHEMA.PARAMETERS возвращает информацию о параметрах хранимой процедуры USE Northwind SELECT PARAMETER_MODE. PARAMETER_NAME, DATA_TYPE FROM INF0RMATI0N_SCHEMA.PARAMETERS WHERE SPECIFICJAME-'Employee Sales by Country' (Результаты сокращены) PARAMETER_MODE PARAMETER_NAME DATA_TYPE IN @Begnnning_Date datetime IN @Ending_Date datetime Общие замечания о параметрах В дополнение к тому, что уже было сказано о параметрах, предлагаю еще несколько советов. ■ Проверяйте корректность параметров хранимой процедуры в самом начале. ■ Параметры проще передавать, если они имеют интуитивно понятные имена. ■ Хорошая практика определять значения по умолчанию для параметров, где это возможно. Это делает процедуру более простой в использовании. Значение по умолчанию может быть константой или NULL. ■ Так как названия параметров являются локальными, параметры с одинаковыми названиями можно использовать в различных процедурах. Если у вас есть десять процедур, которые принимают имя пользователя в качестве параметра, назовите этот параметр @UserName во всех десяти из них — для простоты и для общей читабельности вашего кода. ■ Информация о параметрах процедуры хранится в системной таблице sysco I umns. ■ Хранимая процедура может иметь до 1024-х параметров. Если у вас есть процедура, которая, как вы считаете, будет иметь более 1024-х параметров, подумайте, как переписать ее лучше. ■ Количество и объем памяти, занимаемый локальными переменными хранимой процедуры, ограничены только количеством памяти, доступным SQL Server.
60 Глава 1. Основы хранимых процедур Глобальные переменные (системные функции) По своей сущности глобальные переменные (также известные под именем системные функции являются подобластью хранимых процедур. Некоторые их них используются исключительно в хранимых процедурах. Все они представлены в табл. 1.1. Таблица 1.1. Функции, связанные с хранимыми процедурами Название Описание @@FETCH_STATUS Статус последней операции FETCH @@NESTLEVEL Текущий уровень вложенности @@OPTIONS Набор битов, определяющий текущие пользовательские настройки @@PROCID Идентификатор текущей процедуры @@SPID Идентификатор текущего процесса @@TRANCOUNT Количество открытых транзакций Команды управления выполнением Отдельные команды Transact-SQL определяют порядок выполнения команд хранимой процедуры или пакета команд. Эти команды называются командами управления выполнением. Вот они: IF... ELSE, WHILE, GOTO, RETURN, WAITFOR, BREAK, CONTINUE и BEG I N...END. Далее мы обсудим все эти команды, а пока для примера простая хранимая процедура, которая иллюстрирует использование этих команд (листинг 1.30). Листинг 1.30. Команды управления выполнением USE pubs IF OBJECTJDC dbo.listsales') IS NOT NULL DROP PROC dbo.listsales GO CREATE PROC dbo.listsales @title_id tid=NULL AS IF (@titlejd=7?') GOTO Help -- Here's a basic IF -- Here's one with a BEGIN..END block IF NOT EXISTSCSELECT * FROM titles WHERE titlejd=@titlejd) BEGIN PRINT 'Invalid titlejd' WAITFOR DELAY '00:00:03' -- Delay 3 sees to view message RETURN -1 END IF NOT EXISTSCSELECT * FROM sales WHERE titlejd=@titlejd) BEGIN PRINT 'No sales for this title' WAITFOR DELAY '00:00:03' -- Delay 3 sees to view message RETURN -2 END DECLARE @qty int. @totalsales int SET @totalsales=0 DECLARE с CURSOR
Ошибки 61 FOR SELECT qty FROM sales WHERE title_id=@title_id OPEN с FETCH с INTO @qty WHILE ((a@FETCHJTATUS=0) BEGIN -- Here's a WHILE loop IF (@qty<0) BEGIN Print 'Bad quantity encountered' BREAK -- Exit the loop immediately END ELSE IF (@qty IS NULL) BEGIN •' Print 'NULL quantity encountered -- skipping' FETCH с INTO (?qty CONTINUE -- Continue with the next iteration of the loop : END SET @totalsales=@totalsales+@qty FETCH с INTO @qty END CLOSE С DEALLOCATE с SELECT @title_id AS 'TitlelD', @totalsales AS 'TotalSales' RETURN 0 -- Return from the procedure indicating success Help: EXEC sp_usage @objectname='listsales'. @desc='Lists the total sales for a title', ■ • • @parameters='@title_id="ID of the title you want to check"', :.• ' @example='EXEC listsales "PS2091"', ?.'?;i @author='Ken Henderson', @email='khen@khen.com'. @version=T . @revision='0', @datecreated='19990803'. @datelastchanged='19990818' WAITFOR DELAY '00:00:03' -- Delay 3 sees to view message RETURN -1 GO EXEC dbo.listsales 'PS2091' EXEC dbo.listsales 'badone' EXEC dbo.listsales 'PC9999' TitlelD TotalSales PS2091 191 Invalid title_id No sales for this title Ошибки Глобальная переменная @@ERR0R возвращает код ошибки последнего оператора Transact-SQL. Если никакой ошибки не произошло, @@ERR0R возвращает ноль. Поскольку @@ERR0R сбрасывается после каждого оператора Transact-SQL, необходимо сохранить ее значение в переменную, если предполагается использовать это значение в дальнейшем. Если вы хотите создавать устойчивый, работающий годами код, не переписывая его, возьмите в привычку проверять значение @@ERR0R
62 Глава 1. Основы хранимых процедур в своих хранимых процедурах, особенно после операций, связанных с модификацией данных. Показатель хорошего кода — последовательная проверка ошибок. Так как Transact-SQL не поддерживает структурированную обработку исключений, проверка @@ERR0R часто есть лучший способ оградить себя от неожиданностей. Сообщения об ошибках Системная процедура sp_addmessage добавляет в таблицу sysmessages пользовательские сообщения, которые могут быть в дальнейшем инициированы (возвращены клиенту) с помощью команды RAISERROR. Пользовательские сообщения должны иметь номера ошибок, начиная с 50 000. В основном системные сообщения SQL Server используются при разработке программ с многоязыковым интерфейсом. Так как при добавлении сообщения с помощью sp_addmessage указывается идентификатор языка, можно добавить отдельную версию сообщений об ошибках для каждого языка, поддерживаемого вашим приложением. Тогда если хранимая процедура ссылается на сообщение по номеру, то сообщение будет возвращено в приложение в соответствии с текущими языковыми настройками SQL Server. RAISERROR Хранимые процедуры посылают сообщения об ошибках клиентским приложениям, используя команду RAISEERROR. RAISERR0R не влияет на ход выполнения хранимой процедуры, а просто выводит сообщение, устанавливает соответствующее значение переменной ©ERROR и также может записать сообщение в журналы SQL Server и операционной системы. RA ISERR0R может ссылаться на сообщения об ошибках из таблицы sysmessages (в том числе и на добавленные туда с помощью процедуры sp_addmessage), или вы можете указать свое сообщение об ошибке. Если вы передаете в RA ISERR0R свое сообщение, то номер ошибки устанавливается равным 50 000. Если вы инициируете ошибку, используя ее идентификатор из таблицы sysmessages, то переменной ©ERROR присваивается номер ошибки, соответствующий этому сообщению. В команде RAISERROR можно задать формат сообщения, как в функции PR I NTF() языка С, что позволяет вам задать аргументы для возвращаемых сообщений. В команде RA ISERR0R можно указать значение и для уровня, и для статуса ошибки. Значения уровня ошибки меньше 16 генерируют сообщения в журнале приложения (если включена запись в журнал). Значение уровня ошибки 16 генерирует предупреждающее сообщение в журнале событий, а в случае если значения больше 16, в журнале событий регистрируется ошибка. Сообщения со значениями уровня ошибки до 18 могут быть инициированы любыми пользователями. Сообщения со значениями уровня ошибки от 19 до 25 могут быть инициированы только членами роли sysadmin, при этом обязательно указывать опцию WITH LOG. Значения уровня ошибки, начиная с 20 и выше, считаются фатальными и вызывают разрыв клиентского соединения. Статус ошибки не имеет какого-либо специального предназначения и может быть использован вами для возврата информации об ошибке клиентскому приложению. Если вызвать ошибку со значением статуса, равным 127, утилиты ISQL и OSQL установят значение системной переменной ERRORLEVEL, равное номеру ошибки, вызванной оператором RA ISERR0R. Если указать параметр WITH LOG, то информация об ошибке будет записана в журнал событий Windows (в случае если SQL Server работает под управлением опера-
Рекурсивные вызовы 63 ционных систем Windows NT, Windows 2000 или Windows XP) и в журнал ошибок SQL Server, независимо от того был ли указан параметр WI TH_L0G при создании сообщения с помощью sp_addmessage. Если указать параметр WITH N0WAIT, то сообщение будет немедленно возвращено клиенту. С помощью опции WITH SETERR0R можно присвоить @@ERR0R номер последней произошедшей ошибки, несмотря на использованный в RAISERR0R уровень ошибки. Примеры использования RAISERR0R(), @@ERR0R и других механизмов обработки ошибок SQL Server подробно рассматриваются в главе 7. Вложенные вызовы Максимальный уровень вложенных вызовов — 32. Для определения уровня вложенности из хранимой процедуры или триггера используется глобальная переменная ©aNESTLEVEL В случае применения пакета команд @@NESTLEVEL возвращает 0. Для хранимой процедуры, вызванной из пакета или триггера первого уровня, @@NESTLEVEL вернет 1. Для процедуры или триггера, вызванного из уровня 1, @@NESTLEVEL вернет 2 и т. д. Объекты (включая временные таблицы) и курсоры, созданные внутри хранимой процедуры, видимы также всем объектам, которые она вызывает. Объекты и курсоры, созданные в командном пакете, доступны всем объектам, на которые есть ссылки в пакете. Рекурсивные вызовы Так как Transact-SQL поддерживает рекурсию, можно создавать процедуры, которые вызывают сами себя. Рекурсия — это способ решения задачи разбиением ее на более мелкие, к которым применяется тот же алгоритм. Чаще всего рекурсия применяется для решения вычислительных задач. В листинге 1.31 приведена хранимая процедура, которая вычисляет факториал числа. Листинг 1.31. Пример рекурсивного вызова хранимой процедуры SET NOCOUNT ON USE master IF OBJECT_ID('dbo.sp_calcfactonal') IS NOT NULL DROP PROC dbo.sp_calcf acton al GO CREATE PROC dbo.sp_calcfactor1al @base_number decimalC8.0). Ofactorial decimal C8,0) OUT AS SET NOCOUNT ON DECLARE Opreviousjiumber decimalC8,0) IF ((№ase_number>26) and (@(?MAX_PRECISI0N<38)) OR ((?base_number>32) BEGIN RAISERR0R('Computing this factorial would exceed the server''s max. numeric precision of %й or the max. procedure nesting level of 32',16,10.@@MAXJ>RECISION) RETURN(-l) END IF @base_number<0) BEGIN продолжение &
64 Глава 1. Основы хранимых процедур Листинг 1.31 {продолжение) RAISERRORC'Can''t calculate negative factorials',16.10) RETURN(-l) END IF (@base_number<2) SET @factorial=l -- Factorial of 0 or 1=1 ELSE BEGIN SET @previous_number=@base_number-l EXEC dbo.sp_calcfactorial @previous_number, @factorial OUT -- Recursive call IF (@factohal=-l) RETURN(-l) -- Got an error, return SET @factorial=@factonal*@base_number IF (@@ERROR<>0) RETURN(-l) -- Got an error, return END RETURN(O) GO DECLARE @factorial decimalC8.0) EXEC dbo.sp_calcfactorial 32, ^factorial OUT SELECT @factorial Сначала в процедуре проверяется правильность переданного числа, для которого надо вычислить факториал. Затем рекурсивно подсчитывается результат. Как мы увидим в главе 11, пользовательские функции идеально подходят для вычислений типа факториала. Итоги В этой главе вы узнали: ■ как отслеживать действия хранимой процедуры, используя утилиту Profiler; ■ как создавать хранимые процедуры; ■ что такое процедурный кэш, как он используется SQL-сервером и как с его помощью можно определять эффективность кода; ■ о многих нюансах и особенностях языка Transact-SQL применительно к хранимым процедурам и об использовании этих особенностей; ■ как передавать параметры в хранимые процедуры, как получать код завершения хранимых процедур и как получать данные через выходные параметры; ■ о вложенных и рекурсивных вызовах; ■ о мощи Transact-SQL и хранимых процедур.
2 Оформление исходного кода Программирование — как гольф — может утомить или надоесть, но тот удар из тысячи, когда мяч скользит над травой, огибает дерево и останавливается в двух футах от лунки, — именно тот удар заставляет приходить на игру вновь и вновь. X. В. Кентон Должен признать, что я долго не мог выбрать, какой стиль кодирования и оформления предложить. Предпочтения в форматировании и стиле настолько индивидуальны для каждого программиста, что я не имею нрава указывать, какие из них вам следует использовать. Вместо этого я просто расскажу, что я использую сам и почему. Прежде чем начать, позвольте заметить, что использование какого-либо стиля кодирования или форматирования не может улучшить профессиональные качества программиста. Просто следует выбрать то, что вам больше нравится. Вряд ли использование строго определенного набора соглашений поможет вам превзойти в профессиональном мастерстве других разработчиков, хотя пренебрежение правилами может и помешать. Узнайте мнение человека, который пытался разобраться в коде, отформатированном так странно, что его трудно далее читать, — не говоря уже о том, что почти невозможно понять, что имел в виду разработчик. Трудно отлаживать или дописывать код, если сначала приходится его расшифровывать. Кроме того, мне кажется, что стиль вообще не имеет существенного значения. То, что код делает, значительно важнее того, как он выглядит. Однако, так как все постоянно интересуются моим мнением об оформлении, я решил включить эту тему в свою книгу. Форматирование исходного кода Начнем со средств форматирования исходного кода. Помните, что совсем не обязательно делать это именно так, как я говорю. Гораздо важнее делать это разумно и логично и найти систему, подходящую именно вам. Прописные буквы Я часто пишу ключевые слова SQL Servera прописными буквами. Мне это помогает выделить важные ключевые слова, чтобы потом можно было легко найти их 3 Зак. 983
66 Глава 2. Оформление исходного кода в листинге. Конечно, может показаться, что это необязательно, так как Query Analyzer может выделять определенные слова. Но дело в том, что мне часто приходится смотреть код T-SQL не в Query Analyzer, а, например, в журнале ошибок SQL Servera, в трассировочном файле или в текстовом редакторе, который не выделяет зарезервированные слова T-SQL. Исключение составляют типы данных: я пишу их, используя строчные буквы. Почему? Потому что я часто создаю пользовательские типы данных, а названия пользовательских объектов я не пишу заглавными буквами. Использование одного регистра для пользовательских и системных типов данных облегчает чтение исходного кода. Основной принцип, которым я всегда пользуюсь при форматировании кода, заключается в том, что нужна серьезная причина, чтобы отформатировать какую-то часть кода отлично от основной его части. Я заранее знаю, какой тип данных системный, а какой — пользовательский, и поэтому могу их отличить. Выделение прописными буквами в этом случае только мешает. Для переменных, параметров, названий столбцов, таблиц, процедур и представлений я использую и заглавные и строчные буквы. Это помогает отделить их от зарезервированных слов и позволяет использовать составные названия без пробелов или подчеркивания. Исключение составляют системные объекты или те, которые были созданы не мной. В этом случае я оставляю оригинальный стиль. Например, я пишу sysobj ects вместо SysObject или crdate вместо CrOate. Я делаю это для того, чтобы мой код работал и в системах, чувствительных к регистру. А также потому, что я долго создавал программы на языках, чувствительных к регистру, таких как C++ и XML. Я почти бессознательно подстраиваюсь под первоначальный стиль объекта, на который делаю ссылки. То, как вы называете объекты, — очень важно. Поэтому я рекомендую называть их содержательно и логично. Но одной содержательности недостаточно. Может так получиться, что содержание окажется неверным. Как говорил Эмерсон: «Глупое содержание — это слабоумный гоблин, обожаемый маленькими чиновниками, философами и предсказателями»1. Отступы и пробелы Я стараюсь использовать разные средства для выделения кода — по возможности одновременно универсальным и нетрудоемким способом. Хорошее форматирование не должно мешать чтению, но должно минимизировать длину записи, поскольку чем длиннее запись, тем сложнее ее читать. Другими словами, можно так увлечься расширением кода, что, в конце концов, его будет почти невозможно читать. А если появится необходимость изучить его, придется просмотреть намного больше строк, чего можно было бы избежать. Оператор SELECT Мне кажется, нет необходимости разбивать короткий оператор SELECT на несколько строк только для того, чтобы его можно было выравнивать, как и в случае с длинным текстом запроса. Особенно это относится к подзапросам и вычисляемым в запросе столбцам. Я, например, делаю это следующим образом (листинг 2.1). Emerson, Ralph Waldo. Self Reliance. Self Reliance and Other Essays. Mineola, NY: Dover Publications, 1993.
Форматирование исходного кода 67 Листинг 2.1. Использование простого форматирования для простых запросов IF EXISTSCSELECT * FROM Northwind.dbo.Customers) Этот способ форматирования кажется мне более содержательным и легким для чтения, чем код, приведенный в листинге 2.2. Листинг 2.2. Стиль, которого следует избегать IF EXISTS . • , ^ ' , , t. '■ SELECT ■ '• • "■ :rL ■ '• ' '; FROM : Northwind.dbo.Customers ) .... :.,.. Поскольку вертикального пространства никогда не бывает много (и так будет до тех пор, пока код будет растягиваться вертикально, а не горизонтально), я изо всех сил стараюсь сбалансировать удобство чтения и экономию места. Поэтому я не вижу необходимости в разбивании простого оператора SELECT на множество строк, как это показано в листинге 2.2. Читать код от этого не станет легче (мне даже кажется, наоборот — сложнее), и из-за этого он выглядит намного сложнее, чем есть на самом деле. Внимание рассеивается, потому что командой IF мы просто пытаемся проверить, содержит ли таблица записи. По моему мнению, передача относительной значимости части программного кода не менее важна, чем соблюдение принятых стандартов форматирования. Стоит уравновесить стандартизацию и здравый смысл. Если часть кода относительно мала, вы не должны просматривать всю страницу, чтобы найти логические границы (например, скобки или пару BEG I N...END). Несмотря на то что использование форматирования, как в листинге 2.2, сделало бы книгу толще, я, будучи в здравом уме, этим не пользуюсь. Замена одного слова или символа (например, * в листинге 2,2) на целую строку текста значительно удлинит книгу, но не облегчит чтение кода. Я стараюсь экономить место в этой книге, как и вы, возможно, будете экономить место в коде, который будете писать. В более сложных операторах SELECT я, как правило, размещаю каждую важную часть оператора на новой строчке и выравниваю их по левой границе. Столбцы я обычно располагаю справа от слова SELECT и разделяю их запятыми, если это необходимо. Если все столбцы не входят в одну строку, я просто продолжаю их на следующей строке, делая отступы, чтобы выровнять с предыдущей строкой. Приведу пример. Листинг 2.3. Можно выравнивать столбцы в операторах T-SQL SELECT CustomerlD. CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode. Country, Phone. Fax FROM Northwind.dbo.Customers WHERE City IN('London'. 'Madrid') Еще один пример с большим оператором SELECT. Листинг 2.4. Выравнивание по левой границе основных предложений команд T-SQL SELECT Region, COUNTCO AS NumberOfCustomers FROM Northwind.dbo.Customers , ■ ■ WHERE Region IS NOT NULL GROUP BY Region HAVING COUNTCO > 1 ORDER BY Region
68 Глава 2. Оформление исходного кода Предложения и предикаты Основные предложения команды T-SQL я обычно выравниваю так же, как и столбцы в операторе SELECT. Например, я выравниваю по левому краю отдельные предложения при объединении нескольких таблиц и логические условия в сложных предложениях WHERE и HAVING. Мне это кажется логичным. Если выравнивать только главные предложения, второстепенные, естественно, выравниваться не будут. Если не выравнивать их по левой границе, то можно их вообще не выравнивать или выровнять между собой. Мне больше нравится второй вариант потому, что так код читается легче. Итак, сложные предложения я записываю примерно следующим образом. Листинг 2.5. Выравнивание отдельных предложений сложного выражения SELECT CustomerlD, CompanyName, ContactName, ContactTitle. Address, City, Region. Postal Code, Country, Phone, Fax FROM Northwind.dbo.Customers WHERE City='London' OR City='Madrid' OR City='Paris' Если я располагаю предложения сложного выражения на одной строке, то разделяю их скобками (см. раздел «Скобки»), Выражения Если оператор CASE достаточно прост, я обычно располагаю его на одной строке. Если же он сложен, я разбиваю его на несколько строк. Примеры приведены в листингах 2.6 и 2.7. Листинг 2.6. Простой оператор CASE, расположенный на одной строке SELECT CustomerlD, CompanyName, ContactName. ContactTitle, Phone, CASE WHEN Fax IS NULL THEN 'N' ELSE 'Y' END AS [Fax?] FROM Northwind.dbo.Customers WHERE City = 'London' . . Листинг 2.7. Более сложный оператор CASE обычно разбивается на несколько строк SELECT CASE Region WHEN 'WA' THEN 'Phil' WHEN 'SP' THEN 'Xavier' WHEN 'ВС THEN 'Jean-Marc' ELSE 'Unknown' END AS Salesman, CustomerlD. CompanyName. ContactName FROM Northwind.dbo.Customers ORDER BY Salesman Обычно я использую тот же подход для функций и других выражений. Если выражение простое, я использую простое форматирование. Если выражение сложное, я делю его на строки и форматирую соответствующим образом. Я часто располагаю вложенные функции на одной строке и не стесняюсь сложных выражений, когда они необходимы. Например, я использую следующее форматирование. <
Форматирование исходного кода 69 Листинг 2.8. Относительно простые выражения обычно располагаются на одной строке SELECT ContactName+' "s title is ' + REPLACECUPPER(ContactTitle),'SALES','MARKETING') FROM Northwind.dbo.Customers Конечно, вложенное выражение в функции REPLACE можно было бы разбить на несколько строк, но опытный разработчик, взглянув на код, догадается, что этот код просто изменит слово SALES на MARKETING в столбце ContactTit 1е, прежде чем объединить его с ContactName. В этом случае разбиение выражения на несколько строк не даст ничего, кроме удлинения текста программы. BEGIN/END Я не включаю хранимые процедуры в блок BEG IN/END. Это необязательно и просто добавляет лишние строки в текст программы. Кроме того, я располагаю BEGIN на той же строке, что и соответствующую команду управления ходом выполнения, и выравниваю END относительно этой команды. Другими словами, если BEG IN используется командой IF T-SQL, чтобы отделить часть кода, я пишу его на одной строке с командой I F и выравниваю END по команде I F, а не BEG I N. Большинство людей форматируют код Transact-SQL по-другому, но у меня есть основания для того, чтобы делать по-своему. Как я уже отмечал, мне нравится уменьшать вертикальное пространство. Если часть кода не заслуживает отдельной строки с точки зрения значимости, она ее и не получает. Команды BEGIN и END не совсем соответствуют исполняемому коду (например, нельзя установить контрольную точку в отладчике T-SQL на одну из них) и не так важны, как команды управления ходом выполнения, которым они соответствуют. Визуально совмещение BEG IN и END не производит большого эффекта, а объединение END с командой I F или WHILE может помочь. Рассмотрим хранимые процедуры, представленные в листинге 2.9. Листинг 2.9. Пары BEGIN/END можно отформатировать разными способами CREATE PROC testd @var int AS BEGIN IF @var=l ■-■•■■;'■■'• BEGIN • PRINT '1' END ELSE BEGIN PRINT 'not 1' END ' END •" • • ■ ' • -.•:,■'. Во всей процедуре всего три строки исполняемого кода: IF и две команды PRINT. Если запустить эту процедуру под отладчиком в Query Analyzer, контрольные точки можно установить только на этих строках. Расширяя таким образом процедуру, вы вынуждены уделять внимание «лишним» строкам — тем, которые ничего не делают. Конечно, строки с BEG IN и END указывают на отдельный блок команд, поэтому они более значимы, чем, например, комментарии, но все же имеют смысл только в контексте других команд. По той же причине в манере, очень похожей на форматирование фигурных скобок в C++, я располагаю «лишние» строки на одном уровне с командами.
70 Глава 2. Оформление исходного кода Листинг 2.10. Предыдущая процедура с меньшим числом «лишних» строк CREATE PROC testd @var int AS ' * IF @var=l BEGIN PRINT '1' END ELSE BEGIN PRINT 'not 1' ■:,■■■■■ END Обратите внимание на положение ELSE на одной строке с частью END команды I F. ELSE — еще одна «лишняя» команда, которая, в принципе, не указывает на рабочий код: нельзя ввести контрольные точки отладчика на команду ELSE. Она отделяет рабочий код и указывает на ход выполнения процедуры, но сама ничего не делает. В тех случаях, когда для кода, соответствующего командам IF или ELSE, необходима только одна строка, я обычно опускаю BEG I N/END и пишу код на той же строке, что и команды I F или ELSE, как показано в листинге 2.11. Листинг 2.11. Тестовая процедура без «лишних» строк CREATE PROC testd @var int AS . . IF @var=l PRINT '1' . • , ELSE PRINT 'not 1' Аргументом против такого способа может быть то, что строка IF объединяет две выполняемые строки: I F и PR I NT — в одну, усложняя процесс поиска кода в отладчике. Несмотря на это, отладчик Query Analyzer способен правильно их определить, оставаясь на этой строке до тех пор, пока все ее выполняемые части не закончатся. То есть, если переходить по строкам по F10, отладчик будет оставаться на строке еще один цикл, чтобы показать действие команды PR I NT (если в процедуру будет передана 1). В случае с ELSE вы не достигнете строки с ELSE, пока вторая команда PR I NT не будет выполнена. Другими словами, не требуется располагать на отдельной строке код, соответствующий команде ELSE, в противном случае ELSE не будет обработана. В отличие от I F ELSE не является исполняемым кодом. Как можно заметить, уменьшение числа «лишних» строк в сценарии может сильно повлиять на конечное число строк в процедуре. В нашем случае число строк можно уменьшить с 11 ДоЗ. Скобки У меня есть привычка использовать скобки чаще, чем это необходимо, особенно в логических выражениях. Например, я часто использую скобки, чтобы отделить предикатные фразы в сложных предложениях WHERE и HAVING, особенно когда размещаю их на одной строке. Кроме того, я использую скобки в предложении ON предложения JO IN, а также для логических условий команд IF и WHI LE. Частое использование скобок помогает мне облегчить процесс чтения кода, хотя главная причина злоупотребления скобками заключается в моей работе с языками С и C++, где скобки используются для логических выражений. Пример приведен в листинге 2.12. Листинг 2.12. Заключение логического выражения в скобки CREATE PROC testd @var int AS IF (@var=l)
Форматирование исходного кода 71 SELECT C.CompanyName. SUM@.Freight) AS Freight FROM Customers С JOIN Orders 0 ON (C.CustomerlD-O.CustomerlD) WHERE (C.City='London') OR (C.City='Portland') GROUP BY C.CompanyName ORDER BY C.CompanyName "" '" "' " ?-il--s-'-i .-• ,■ . ': ELSE SELECT C.CompanyName, SUM@.Freight) AS Freight FROM Customers С JOIN Orders 0 ON (C.CustomerlDO.CustomerlD) WHERE (C.City='Paris') OR (C.City='Barcelona') GROUP BY C.CompanyName ORDER BY C.CompanyName GO .. Скобки нужны, главным образом, для того, чтобы помогать определить порядок выполнения операций. Они контролируют порядок вычисления в выражении. Поэтому, кроме чисто эстетических целей, скобки влияют на то, как работает код. Горизонтальные интервалы Продолжая устранять «лишнюю» информацию из сценариев T-SQL, я часто скуплюсь на горизонтальные интервалы. Я не ставлю пробелы между операторами (например, +, =, <> и т. д.), скобками и выражениями. Обычно одним пробелом отделяется ключевое слово SELECT и список его столбцов, а также другие основные предложения команд T-SQL и их второстепенные части. Псевдонимы столбцов и таблиц В качестве псевдонимов столбцов и таблиц я предпочитаю использовать комбинации из ANSI- и не-ANSI-элементов. Несмотря на личное предпочтение формата Labe I = Co I umnName, я чаще использую формат Со I umnName As Labe I. Хотя первый метод более компактен, ANSI-метод стал таким популярным, что я использую его чаще. Псевдонимы таблиц — другое дело. В псевдонимах таблиц я пропускаю AS и просто пишу псевдоним таблицы после ее названия. Мне это помогает отличить псевдоним таблиц от псевдонима столбцов и удовлетворяет мою склонность избегать «лишних» слов в коде. Следует заметить, что я не всегда использую псевдонимы обоих типов в своем коде. Все зависит от ситуации. В коротких запросах с одной таблицей я обычно опускаю псевдонимы. Когда запрос состоит хотя бы из двух таблиц, представлений или табличных функций, было бы не плохо добавить псевдоним к названию столбца, даже если столбец встречается во всем запросе всего один раз. Во-первых, потому что это облегчает процесс чтения кода. В этом случае не приходится угадывать, где находится и какому объекту принадлежит данный столбец. Во-вторых, код становится более понятным. Если позже в запрос добавить таблицу, в которой один из столбцов носит уже используемое имя, появится сообщение об ошибке: «Неоднозначное название столбца», а это никому не понравится. Так что избавьте себя от лишних хлопот, устраните неоднозначность до того, как SQL Server заставит вас сделать это. Чтобы не путаться, я обычно использую одно- или двухбуквенное сокращение для псевдонимов таблиц. Если в запросе больше одного экземпляра таблицы, я добавляю к псевдониму цифру, чтобы определить уровень ее вложенности. Это заставляет всегда указывать префикс псевдонима таблицы в названии столбца (по-
72 Глава 2. Оформление исходного кода тому что псевдонимы такие короткие) и помогает разобраться в своем собственном коде, когда в него добавляются запросы, подзапросы и вложенные таблицы. Язык определения данных (DDL) В DDL (Data Detennination Language, язык определения данных) я использую те же принципы форматирования, что и в остальном T-SQL. Возможно, вы уже заметили, что я располагаю список параметров и AS для хранимых процедур на той же строке, что и CREATE PROCEDURE. Это опять же соответствует моей теории о «лишних» строках в коде. AS не является исполнимым кодом для хранимых процедур, поэтому ему приходится делить строку со своим «благодетелем». Что же касается CREATE TABLE, то названия столбцов, а иногда и типов данных выравниваются по левому краю. Я считаю, что если форматирование CREATE TABLE не влияет на собственно процесс создания таблицы, не стоит тратить на него много времени. Обычно таблиц создают намного меньше, чем процедур и других объектов. Так что CREATE TABLE выглядит у меня примерно следующим образом. Листинг 2.13. Можно не усложнять DDL CREATE TABLE dbo.Customer (CustomerlD int identity PRIMARY KEY. CustomerName varcharD0) NOT NULL. Address varcharF0) NULL. City varcharB0) NULL. State charB) NULL. Zip varchar(lO) NULL DEFAULT 'TX'. Country varcharB0) NULL. Phone varcharB4) NULL, Fax varcharB4) NULL ) Указание владельца Поскольку указание владельца в имени объекта может увеличить производительность, я стараюсь указывать владельца во всех ссылках на объекты. Это не только помогает избавиться от неоднозначности в названиях объектов, но и позволяет ускорить доступ к хранимым процедурам, потому что отсутствие имени владельца приводит к тому, что на процедуру накладывается блокировка компиляции, которая снимается, когда процедура уже находится в кэше. Время блокировки может быть достаточно коротким, хотя если в имени процедуры указан владелец, блокировки вообще не будет, если, конечно, процедура не нуждается в компиляции. Это значит, что лучше использовать EXEC dbo.sp_who чем EXEC sp_who хотя оба эти способа будут работать. Аналогично лучше использовать CREATE PROCEDURE dbo.MyProc чем CREATE PROCEDURE MyProc потому что это устраняет любую неоднозначность при ссылке на объекты.
Форматирование исходного кода 73 Указание имени владельца — это просто хорошая привычка вне зависимости от того, влияет она на производительность системы или нет. (Обратите внимание, что для скалярных пользовательских функций (UDF) владелец должен быть указан обязательно в отличие от других типов объектов, где префикс владельца носит необязательный характер.) Сокращения и необязательные ключевые слова Я часто сокращаю такие слова, как PROCEDURE, в командах CREATE PROCEDURE и DROP PROCEDURE. Если синтаксис необязателен и не делает код более ясным, я не вижу нужды использовать его. Ключевые слова Это относится и к необязательным ключевым словам. Я часто опускаю их в коде. Например, ключевое слово INT0 с командой INSERT или слово FROM в команде DELETE. Совместимость с ANSI-стандартами позволяет опускать лишний синтаксис. Самым надежным и простым кодом является тот, которого нет. В нем никогда не бывает синтаксических ошибок, он не ломается, не устаревает и не занимает драгоценного места на экране. Сокращение часто употребляемых слов Когда вы сокращаете часто употребляемые слова в названиях создаваемых объектов, постарайтесь быть последовательными. Если вы сокращаете слово number как пит в одном названии, сокращайте его аналогично и в других названиях. Не делайте его No в одной таблице (например, CustNo) и number (например, InvoiceNumber) в другой. Будьте последовательны. Хорошо бы разработать стандартную систему сокращений до того, как вы приступите к созданию объектов. Передача параметров Я передаю значения параметров хранимых процедур чаще по позиции, чем по имени, если, конечно, процедура не содержит большого количества параметров, а я хочу передать только некоторые из них или смысл передаваемых значений недостаточно очевиден из порядка их передачи. Мне кажется, основная причина здесь — лень. Я не люблю набирать без надобности, а чтобы опустить имена параметров, набирать требуется гораздо меньше. Выбор имен Давая название объекту, необходимо быть осторожным, чтобы не вызвать конфликт между таблицами, представлениями, пользовательскими функциями (UDF), процедурами, триггерами, объектами def au 11 и ru I e, потому что их имена должны быть уникальными. Например, нельзя назвать хранимую процедуру и таблицу одинаково. Как уже отмечалось, я стараюсь точно описать объект. Одним из важных факторов при выборе имени является то, насколько легко произносится его название. Если имя слишком технологичное, оно может звучать глупо, а у вас будут трудности в разговоре с другими людьми. Сравните следующие названия: SWCustomers и CUSTOMERSJN_THE_SOUTHWEST_REGION. Второе название
74 Глава 2. Оформление исходного кода очень трудно произносится, так как оно слишком длинное и громоздкое для использования в обычной речи. Те, кому придется произносить это название, будут инстинктивно стараться сократить его. Так почему же сразу его не уменьшить? В чем причина использования такого длинного названия? Стандарт? Так почему бы ни придать стандарту здравый смысл? Таблицы .■■,.:,-, <Л - .-..-. В таблицах и представлениях я обычно использую простые существительные во множественном числе. Если таблица объединяет две другие таблицы в отношении «многие-ко-многим», я называю ее по именам объединяемых таблиц, например CustomerSuppI iers. Индексы Мне кажется разумным называть индексы по их ключам. Например, если индекс создан по таблице Customers на основе столбцов CompanyName и ContactName, я, скорее всего, назову его CompanyNameContactName. Поскольку названия индексов не должны быть уникальными, это позволяет мне с первого взгляда понять, на основе каких ключей был построен этот индекс. Иногда к индексу я добавляю префикс, указывающий, кластерный он или нет. Триггеры ;;■■>-.**.•• -"--„л- Для триггера я использую имя, которое не только показывает, для какой таблицы он был создан (например, DeleteCustomer или InsertUpdateOrder), ной что он делает. Если у триггера есть отличительные черты (например, это триггер INSTEAD OF), я обозначаю это префиксом (например, I nsteadOf De I eteCustomer). Переменные Называя локальные переменные, я никогда не использую больше одного знака @ и часто называю переменные по столбцам, к которым они относятся (если возможно). Если переменная является каким-то счетчиком, я стараюсь назвать ее одной буквой, так же как вы, может быть, называете переменные циклов в C++ или Java буквами i или х. Процедуры Процедуры я обычно называю на основе глаголов, например PostPurchases или Bui IdHistory. Иногда я даю процедурам и представлениям префиксы (например, sp или V_) в зависимости от количества процедур и представлений, которые я создал, и от схожести их имен с именами других объектов. Пользовательские функции (UDF) Пользовательские функции (UDF — User Determined Functions) я называю так же, как хранимые процедуры. Иногда я использую префикс Get..., потому что эти функции возвращают некоторые значения, хотя и так понятно, что функция возвращает значение. Ограничения Я обычно позволяю системе самой давать названия ограничениям, потому что я, как правило, использую GUI-средства для работы с ними. Если я даю названия
Форматирование исходного кода 75 сам, то обычно использую приставку РК_ для ограничений первичных ключей, FK_ — для ограничений внешних ключей, UK_ — для ограничений уникальных ключей и СК_ — для ограничений проверок. Должен признаться, что иногда я даю ограничениям очень длинные названия, которые говорят, что именно делает это ограничение. Это делает название более стильным и выдает информативное сообщение, когда ограничение срабатывает. Например, иногда я делаю примерно следующее. Листинг 2.14. Можно использовать длинные названия ограничений для вывода легко читаемых сообщений CREATE TABLE Samples (SampleDate datetime NULL DEFAULT getdateO, EmployeeID int NULL. SampleAmount Int NULL CONSTRAINT [Sample Amount must not equal 0] CHECK (SampleAmount<>0). CONSTRAINT [Invalid Employee ID] FOREIGN KEY (EmployeelD) REFERENCES Employees (EmployeelD) ) GO INSERT Samples (SampleAmount) VALUES @) INSERT Samples (EmployeelD) VALUES @) (Результаты сокращены) Server: Msg 547. Level 16, State 1. Line 1 INSERT statement conflicted with COLUMN CHECK constraint 'Sample Amount must not equal 0'. The conflict occurred in database 'Northwind', table 'Samples', column 'SampleAmount'. The statement has been terminated. Server: Msg 547. Level 16. State 1, Line 1 INSERT statement conflicted with COLUMN FOREIGN KEY constraint 'Invalid Employee ID'. The conflict occurred in database 'Northwind'. table 'Employees', column 'EmployeelD'. The statement has been terminated. Поскольку название ограничения включено в сообщение об ошибке, можно догадаться, в чем дело, из названия самого ограничения, даже не просматривая все сообщение. Из-за того, что названия объектов могут содержать только 128 символов, метод присоединения пользовательского сообщения к ограничению считается методом для «бедных» (такая возможность существует в Sybase уже несколько лет). Обратите внимание на то, что при включении символа перевода строки в название ограничения мы автоматически включаем его и в сообщение об ошибке (листинг 2.15). Листинг 2.15. Можно разбивать длинные названия объектов CREATE TABLE Samples (SampleDate datetime NULL DEFAULT getdateO. EmployeelD int NULL. SampleAmount int NULL CONSTRAINT [ Sample Amount must not equal 0 ] CHECK (SampleAmountoO). CONSTRAINT [ Invalid Employee ID ] FOREIGN KEY (EmployeelD) REFERENCES Employees (EmployeelD) ) go - продолжение &
76 Глава 2. Оформление исходного кода Листинг 2.15 {продолжение) INSERT Samples (SampleAmount) VALUES @) INSERT Samples (EmployeelD) VALUES @) (Результаты) Server: Msg 547. Level 16. State 1. Line 1 INSERT statement conflicted with COLUMN CHECK constraint ' Sample Amount must not equal 0 '. The conflict occurred in database 'Northwind'. table 'Samples', column 'SampleAmount'. The statement has been terminated. Server: Msg 547, Level 16, State 1, Line 1 INSERT statement conflicted with COLUMN FOREIGN KEY constraint ' Invalid Employee ID '. The conflict occurred in database 'Northwind'. table 'Employees', column 'EmployeelD'. The statement has been terminated. Конечно, лучше перехватывать такие ошибки внутри вашего приложения, а вместо них отображать более осмысленные сообщения, но легко читаемое название ограничения, которое вызвало ошибку, значительно лучше того, которое не дает никакой информации о проблеме. Правила составления программного кода Общие шаблоны проектирования и кодирования будут рассмотрены более детально в части 3, но принципы составления программного кода стоит обсудить здесь. Эти правила будут использоваться во всей книге, поэтому имеет смысл рассказать кое- что заранее. Как я уже сказал, я не могу настаивать на том, что именно следует вам использовать. Вы должны сами найти подходящую систему. Несмотря на это, я разработал некоторые рекомендации в форме «стоит/не стоит», которые могут пригодиться в качестве руководящих принципов в вашей работе. Можете принять их или отказаться. Но я бы посоветовал прочесть их все и выбрать самые разумные. Рекомендации для сценариев Существует несколько рекомендаций, которые могут облегчить вашу жизнь и подходят для всех сценариев T-SQL, причем неважно, что это — сценарий создания объекта или пакет команд T-SQL, которые вы иногда используете. Вообще, большинство из них легко применяются на практике вне зависимости от того, пишете вы сценарий, хранимую процедуру или какой-либо другой объект T-SQL Servera. Удаление объектов Я обычно проверяю, существует ли объект, прежде чем пытаюсь удалить его. Если этого не делать, появляются сообщения об ошибках, даже если команда DROP находится в отдельном пакете T-SQL. Сообщение об ошибке должно привлекать внимание, а не игнорироваться. Я стараюсь избегать создания ненужных сообщений об ошибках, чтобы не отвлекать внимания.
Правила составления программного кода 77 Комментарии При написании комментариев я пытаюсь уравновесить необходимость в пояснении отдельных элементов кода желанием не перегружать его лишней информацией. Избыток комментариев так же плох, как и его недостаток. Я изо всех сил стараюсь писать самодокументируемый код. Избыток комментариев добавляет работы, но не облегчает чтение кода. Экран быстро заполняется ерундой, которую приходится пролистывать, не говоря уже о том, что надо прилагать дополнительные усилия для поддержания актуальности комментариев. Программисты, работающие с кодом, перенасыщенным комментариями, часто путаются в огромном объеме информации, потому что не знают, что важно, а что — нет. Они не знают, в каких комментариях необходимо внимательно разобраться, а какие можно пропустить. Комментарии, которые повторяют то, что уже написано в самом коде, просто удлиняют сценарий. И если код должен быть полностью прокомментирован, чтобы его можно было читать, часто приходится переписывать его заново. Одним словом, если я вынужден создавать программный код, который использует неочевидные приемы, и я считаю нужным поставить об этом в известность потенциальных пользователей моего кода, то я комментирую данные приемы. Я считаю, что в начале любой хранимой процедуры должен быть блок кода, который описывает, для чего предназначена эта процедура. Например, в каждом исходном тексте доллсна быть информация о том, кто автор последнего изменения кода, что изменялось и т. д. Для более подробной информации смотрите главу 4. Кроме того, я постоянно забываю о самом важном факторе, с которым сталкиваются разработчики на T-SQL в XXI веке: у меня нет проблем с так называемыми комментариями «старого стиля». Я считаю, что комментарии, начинающиеся знаком «слэш» со звездочкой /*, подходят, даже являются более предпочтительными, когда комментарий занимает несколько строк. Неважно, что это: заголовок процедуры или функции (которые иногда переформатируются, и выясняется, что комментарии на отдельных строках — настоящая головная боль), или это закомментированная часть кода процедуры или сценария (чтобы часть кода не могла выполняться). В определенных ситуациях комментарии, начинающиеся с /*, удобнее, чем комментарии вида --. Как и для остальных задач программирования, стоит использовать те средства, которые подходят для выполнения данной работы. Нет ничего плохого в комментариях /«, если они используются по назначению. Расширенные свойства Так же как логичные имена позволяют сделать базу данных самодокументируемой, расширенные свойства позволяют более подробно описать объекты. Расширенные свойства — это пары «имя/значение», которые можно определить для различных объектов базы данных: для пользователей, столбцов, таблиц и т. д. Они удобны для добавления описания в создаваемые объекты — вы просто добавляете расширенные свойства, используя хранимую процедуру sp_addextendedproperty, Enterprise Manager или Object Browser Query Analyzer. Системная функция fn_ I i stextendedproperty() позволяет получить список расширенных свойств объек-
78 Глава 2. Оформление исходного кода та. Процедура sp_dropextendedproperty удаляет расширенные свойства. Вот пример, демонстрирующий расширенные свойства. USE Northwind GO CREATE TABLE CustomerList (cl int identity, name varcharOO)) GO EXEC sp_addextendedproperty 'Label', 'Customer Number (NN-XX-NNNN)', 'user', dbo, 'table'. CustomerList, 'column', cl GO SELECT value FROM : :fnJistextendedproperty (NULL, 'user', 'dbo', 'table'. 'CustomerList'. 'column', default) GO DROP TABLE CustomerList (Результаты) value Customer Number (NN-XX-NNNN) Файлы сценариев Конечно, бывают исключения, но в основном я храню исходный текст для каждого объекта базы данных в отдельном файле сценария. Это обеспечивает большую гибкость при пересоздании или модификации объекта. Также это предотвращает ошибочное удаление и изменение объектов, указание специфических установок, например QU0TED__ IDENTIFIER не для соответствующих процедур. Кроме того, мне легче управлять исходным кодом за счет использования системы контроля версий — когда приходится работать с большим количеством объектов. Сегментирование сценариев Если сценарий состоит из нескольких отдельных сегментов, которые не имеют общих переменных, я, как правило, завершаю каждый из них командой GO, чтобы отделить работу одного блока от другого. Таким образом, если в одном из сегментов есть ошибка, она не мешает остальным сегментам выполнить работу правильно. И наоборот, если я хочу, чтобы сценарий сегмента выполнился, только если в сегменте перед ним нет ошибки, я не вставляю команду GO и кодирую оба пакета как один. Если в начале пакета появляется ошибка, последующие команды не выполнятся. < .... Ключевое слово USE Если сценарий должен запускаться из определенной базы данных, я как можно раньте включаю в сценарий соответствующую команду USE . Как я уже говорил, это помогает удостовериться, что каждый объект создается именно там, где он должен быть, и освобождает от необходимости изменять базу вручную каждый раз, когда сценарий выполняется. Когда я включаю в сценарий одну команду USE, я почти всегда располагаю ее в самом верху сценария. Так ее легче искать и легче изменять нижеследующий код. Другие разработчики, взглянув на сценарий, легко определят, где находятся объекты.
Правила составления программного кода 79 Хранимые процедуры и функции Следующие рекомендации относятся в основном к хранимым процедурам и функциям. Эти рекомендации не отличаются от тех, что используются в любом другом языке: одновременное (централизованное) объявление переменных, модульные программы, проверка ошибок и т. д. Разработчики T-SQL часто пренебрегают этими основными правилами. Следуя их примеру, можно сохранить время, затрачиваемое на поиск ошибок, и написать содержательный и легко расширяемый код. Описание переменных По возможности, основные переменные я описываю в начале. Хотя синтаксис языка позволяет описывать переменные почти в любом месте, на их поиск в случае необходимости тратится много времени, а код воспринимается сложнее. Значения, возвращаемые хранимыми процедурами Если хранимая процедура может возвращать значения отличные от нуля (а многие могут), чаще всего эти значения означают ошибку. Поэтому я стараюсь проверять значения, возвращаемые моими процедурами. Обычно значение, равное О, означает успех, ненулевые значения говорят о произошедшей ошибке. Параметрьв Я проверяю значения, переданные хранимой процедуре или функции, и возвращаю ошибку (или вывожу подсказку) в том случае, если значение неправильное. Это облегчает использование программы и предотвращает влияние неверных значений или операций на данные. Параметры:значения по умолчанию Мне кажется хорошей идеей определять значения по умолчанию для параметров хранимых процедур и функций. Это делает их использование более легким, гибким и менее подверженным ошибкам. Ошибки Более подробно на эту тему мы поговорим позже, но признаком качественного кода является полная проверка ошибок. Я стараюсь проверять ошибки после основных операций и предпринимать соответствующие действия. Я, как правило, проверяю значение @@ERR0R после каждого оператора, который может вызвать ошибку, и с помощью @@R0WC0UNT, когда оператор должен повлиять на количество строк. Модульность Работать с несколькими логичными маленькими программами легче и удобнее, чем с одной длинной хранимой процедурой-чудовищем. Когда есть возможность, я разбиваю сложные программы на несколько маленьких. Такое разбиение позволяет повысить эффективность, давая возможность кэшу без потерь избавиться от части программы, а мне облегчает работу над кодом.
80 Глава 2. Оформление исходного кода Таблицы и представления Данные рекомендации относятся к таблицам и представлениям, особенно к тому, как использовать их в своем коде. Неважно, сценарий это, процедура или функция. То, как вы используете таблицы, — эти контейнеры данных — иногда так же важно, как и то, что вы с ними делаете. Временные таблицы Я стараюсь не использовать временные таблицы слишком часто. На это есть две причины. Во-первых, они могут стать причиной проблем с производительностью из-за конкуренции за обладание ресурсами в tempdb. Во-вторых, SQL Server более жестко подходит к обновлению статистик во временных таблицах, чем в постоянных. Это может привести к проблемам при выполнении или к повторной компиляции хранимых процедур. Один из способов отказа от временных таблиц — это использование переменных типа tab I е. С ними можно выполнять все те же операции, что и с временными таблицами, но в tempdb они образуют меньше проблем в плане конкуренции за обладание ресурсами. Как и все переменные, они перестают использоваться, как только выходят из области видимости. Чистка ресурсов Если я все-таки использую временные таблицы, я стараюсь не забывать удалять их, когда перестаю их применять. Ресурсы системы будут тратиться зря, а в некоторых случаях это может даже помешать коду правильно выполняться при следующем запуске, если хранить временные таблицы до тех пор, пока хранимая процедура не завершится или пока вы не выйдете из системы. То же касается и курсоров: хорошо бы применять CLOSE и DEALLOCATE после того, как вы закончили работать с ними. Мама была права — чистота сродни благочестию. Так что убирайте за собой. Системные таблицы Если есть возможность, я стараюсь не обращаться напрямую к системным таблицам. Начиная с SQL Server 7.0, T-SQL обладает большим набором функций для работы со свойствами (например, DATABASEPROPERTY(), C0LUMNPROPERTY(), 0BJECTPROPERTY() и т. д.), которые существенно снижают необходимость обращаться к системным таблицам за метаданными или системной информацией. Делать запросы к системным таблицам напрямую плохо по двум причинам. Во-первых, системные таблицы могут изменяться в зависимости от версии. Код, который вы пишете сегодня, может не запуститься завтра, если он зависит от определенного формата таблицы. Во-вторых, прямое обращение к системным таблицам для получения системной информации обычно выдает информацию, которую читать труднее, чем аналогичную информацию, которую возвращают соответствующие функции для работы со свойствами или метаданными. Например, представьте, что мы создали следующую статистику для таблицы Customers базы данных Northwi nd. CREATE STATISTICS ContactTltle ON Customers(ContactTitle) Однажды мы заметим ContactTi 11 e в листинге sp_he I p i ndex и захотим узнать, что это: настоящий индекс или метка-заполнитель для статистики. Здесь нам по-
Правила составления программного кода 81 могут два запроса (листинги 2.16 и 2.17). Один запрашивает системные таблицы напрямую, а другой — нет. Листинг 2.16. Запрос метаданных напрямую из системных таблиц SELECT CASE WHEN i.status & 64 = 64 THEN 1 ELSE 0 END FROM sysindexes i JOIN sysobjects о ON (i.id=o.id) WHERE o.name='Customers' • •».-- - ■-' ,;,: AND i,name='ContactTitle' Листинг 2.17. Способ получения метаданных, который не только безопаснее, но и проще SELECT INDEXPROPERTYtOBJECTJDC Customers'),'ContactTitle'.' IsStati sties') Оба запроса возвращают 1, если ContactT i 11 e — это индекс статистики. Какой из них легче? Запрос I NDEXPROPERTY() не только легче читать, но он еще и значительно короче. И у него есть дополнительное преимущество — невосприимчивость к изменениям в системной таблице. В дополнение к функциям для работы со свойствами, SQL Server содержит несколько представлений и системных хранимых процедур для упрощения доступа к метаданным. Например, можно попробовать сделать запрос к INFORMAT10N_SCHEMA. VIEW, чтобы получить список представлений базы данных. Листинг 2.18. Для получения системной информации можно использовать представления INFORMATION_SCHEMA SELECT TABLE_NAME AS VIEWJAME. CHECKJDPTION. ISJJPDATABLE FROM INFORMATION_SCHEMA.VIEWS ORDER BY VIEWJAME (Результаты сокращены) ;- ..-. 'n>- •. ' ■ .- VIEW NAME CHECK OPTION IS UPDATABLE Alphabetical list of products Category Sales for 1997 Current Product List Customer and Suppliers by City Invoices Order Details Extended Order Subtotals Orders Qry Product Sales for 1997 Products Above Average Price Products by Category Quarterly Orders Sales by Category Sales Totals by Amount Summary of Sales by Quarter NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NONE NO NO NO NO NO NO NO NO NO NO NO NO NO NO NO Summary of Sales by Year NONE NO Как я уже сказал, существует несколько системных хранимых процедур, которые возвращают информацию о системе и метаданных. Их использование предпочтительней и проще, чем использование прямых запросов к системным таблицам. Например, sp_tables возвращает информацию о таблицах в базы данных, sp_stored_procedures — о хранимых процедурах. Есть и масса других. Читайте «Catalog Stored Procedures» в Books Online и сценарий i nstcat. sq I (в дистрибутиве SQL Servera) для более детальной информации.
82 Глава 2. Оформление исходного кода Transact-SQL Последние рекомендации относятся ко всему Transact-SQL. Неважно, используете ли вы хранимые процедуры или просто пакеты T-SQL, следующие рекомендации помогут написать хороший код. Специализированный T-SQL Я стараюсь по возможности не выполнять код T-SQL, используя функцию ЕХЕС(). Во-первых, планы выполнения для прямых запросов вряд ли будут использоваться повторно в отличие от хранимых процедур. Во-вторых, специализированный T-SQL чрезвычайно трудно отлаживать. При его использовании мне приходится выполнять большое количество команд PR I NT только для того, чтобы проверить, какие значения имеют переменные в различных точках процедуры. Поскольку они препятствуют способности Query Analyzer автоматически находить ошибки, приходится самому искать их во время написания и запуска T-SQL. Поэтому, если есть возможность, я помещаю код в обычные хранимые процедуры, функции и т. д. и вызываю эти объекты. Если мне требуется запустить T-SQL, который был создан «на лету», я использую расширенную хранимую процедуру sp_executesq I. Чаще всего она выполняется быстрее, чем ЕХЕС(), а план выполнения, созданный в этом случае, будет помещен в кэш и может быть использован повторно. COMPUTE и PRINT COMPUTE — неудачное решение, поскольку создает несколько наборов результатов. Приходится обрабатывать их все, чтобы получить результат. Лучше использовать R0LLUP или CUBE, потому что они выполняют все те же функции, что и COMPUTE, и даже больше, не создавая при этом дополнительных наборов. PR I NT нельзя назвать идеальной из-за того, что ADO возвращает неверные информационные сообщения, кроме тех случаев, когда сообщение было создано с важностью выше 10. Другими словами, запустив запрос, используя ADO, вы никогда не получите сообщение команды PR I NT, если только не было сгенерировано сообщение об ошибке. Итоги В этой главе вы узнали: ■ о некоторых правилах форматирования и кодирования, которые помогут создать более качественный T-SQL, многие из которых справедливы как для хранимых процедур, так и для других объектов T-SQL; ■ о том, что адаптация и постоянное использование правил форматирования и кодирования помогут добиться лучших результатов при меньших затратах.
3 Шаблоны проектирования Чем сложнее читать код, тем сложнее поддерживать его. Мартин Фоулер1 В книге «Шаблоны проектирования» Эрих Гамма и компания (Design Pattern, Erich Gamma), известные как «Банда четырех», пишут о том, что есть некоторые шаблоны проектирования программного обеспечения, которые опытные разработчики регулярно используют и распознают в коде, написанном другими. В этой книге делается попытка формально описать эти шаблоны так, чтобы разработчики могли не тратить годы на их изучение, пользуясь методом проб и ошибок. Идея состоит в том, чтобы выделить шаблоны проектирования в отдельную область информатики. Будучи формализованными, они смогут продолжать развиваться, как это происходит с другими областями инженерии программного обеспечения. Подобная философия описывается в книге «Практика программирования» Брайена Кёрнигана (The Practice of Programming, Brian Kernighan). В ней автор объясняет, что языки программирования имеют наборы идиом — соглашений, которые используют опытные разработчики для создания элементов своего кода2. Идиомы похожи на шаблоны проектирования, однако более детальны. Это своего рода мини-шаблоны или фрагменты шаблонов. Они более специфичны, чем шаблоны проектирования, и имеют больше отношения к языку, чем к решаемой проблеме. Хотя обе эти книги рассматривают проектирование программного обеспечения с точки зрения объектно-ориентированных языков программирования, я считаю, что общие шаблоны проектирования и идиомы также существуют и в языках запросов, таких, например, как Transact-SQL. Опытные разработчики постоянно используют общие техники для построения кода. Цель этой главы — собрать воедино несколько техник, чтобы обеспечить отправную точку для обсуждения шаблонов проектирования и идиом Transact-SQL. В целях экономии идиомы и шаблоны описаны вместе. Однако имейте в виду, что, хотя эти понятия и связаны, различия между ними весьма существенны. 1 Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 55. 2 Kernighan, Brian. The Practice of Programming. Reading, MA: Addison-Wesley, 1999. С 11.
84 Глава 3. Шаблоны проектирования Закон простоты Существующий в философии закон простоты, известный также как «бритва Ок- кама», гласит, что из двух похожих теорий наиболее предпочтительна самая простая. Для разработчика программного обеспечения это означает, что наиболее простой подход, реализующий необходимую функциональность, — самый лучший. Умение создавать ненужные сложности еще никого не сделало хорошим программистом. Исходя из моего опыта, лучшие программисты — это те, которые могут решить проблему наиболее простыми способами, независимо от языка, на котором они пишут. Хороший код носит отпечаток элегантности, который является результатом его простоты. Лучший код — простой код. Мартин Фоулер выразил эту мысль так: «Создайте самую простую вещь, которая только сможет работать»1. Я согласен с такой точкой зрения, но с двумя замечаниями. Первое: то, что для одного просто, — для другого может быть сложным. Оценка сложности предложенного решения чаще всего субъективна. То, что для вас очевидно и интуитивно понятно, может ввести меня в ступор. Второе замечание: чересчур сильное упрощение задачи или неспособность предположить дальнейшее развитие ваших проектных решений может вызвать проблемы в дальнейшем. Закон простоты не стоит использовать в качестве оправдания неспособности охватить всю картину целиком1. Закон бережливости подходит для многих идиом и шаблонов проектирования, описанных в этой главе. Лучший способ написать хороший код или улучшать существующий — не создавать ненужных сложностей. Это облегчит вам жизнь не только сейчас, но и через полгода, когда вы или кто-то другой будет работать над кодом. Это — инвестиции в будущее здоровье вашего кода. Если в одном месте кода вы используете прямое выполнение цикла, а в другом — обратное без видимой на то причины, это свидетельствует о том, что код содержит ненужную сложность. Люди, читающие его, должны знать, что даже если код выглядит по-другому, он на самом деле функционирует так же, как и другой, более простой. Главный принцип хорошего проектирования программного обеспечения — устранение бесцельных вариаций так, чтобы остались только лишь стоящие. Так что лучший способ — чаще всего самый простой. У нас и так хватает проблем в области программного обеспечения. Давайте не будем создавать новых, появляющихся в результате плохих подходов к кодированию. Идиомы Как я уже отмечал, каждый язык программирования может быть охарактеризован его идиомами, то есть теми методами, которые используют опытные разработчики для решения общих задач. Transact-SQL не исключение. Здесь я расскажу о некоторых общих идиомах Transact-SQL. Этот список не претендует на полноту, но должен быть достаточен для того, чтобы у вас появилось представление об идио- Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999, С 68.
Идиомы 85 мах как об основе языка. Как только вы начнете так думать, вы станете видеть идиомы во всем. Они сродни простым инструментам из большого набора, причем эти наборы инструментов могут быть различными. Опытные разработчики владеют множеством инструментов, подобно опытному ремесленнику или механику. Получение метаданных Хотя существует множество способов получения метаданных, опытные разработчики обычно используют следующий подход. 1. Используйте функции для работы с метаданными (например, OBJECTPROPERTY()), когда это возможно. 2. Если функция, которая возвращает необходимые вам данные, не существует, используйте представления INFORMATION_SCHEMA (например, INFORMATION SCHEMA. PARAMETERS). 3. Если ни одно из представлений INFORMATION_SCHEMA вам не подходит, воспользуйтесь процедурами для работы с системным каталогом (например, sp_tab I es). 4. Если и это вам не подходит, обращайтесь напрямую к системным таблицам (например, sysobj ects, sysco I umns и т. д.). Итак, способов получения метаданных много. Вот пример двух запросов, делающих одно и то же. Листинг 3.1. Два способа получения имени базы данных по ее идентификатору SELECT dbid FROM master..sysdatabases where name='pubs' SELECT DBJDCpubs') • Идиоматическим или обычным среди опытных разработчиков считается второй. Он не только короче, он еще и нечувствителен к изменениям в структуре таблицы sysdatabases. Возьмите за правило для всех запросов, связанных с получением метаданных, избегать прямых запросов к системных таблицам. Вместо этого пройдите четыре шага, описанные выше. Создание объекта Опытные программисты T-SQL проверяют существование объекта перед его созданием. На это есть три причины. Первая: если объект уже существует, но его не должно быть, вам может понадобиться как-то исправить эту ситуацию (например, выйти из сценария или немедленно прервать процедуру). Вторая: если объект уже существует и ситуация правильная, вам может понадобиться удалить его. Третья причина: попытка создать объект, который уже существует, вызовет ошибку, которая может быть не обработана вашим сценарием. Например, ваш сценарий может попытаться создать таблицу, которая уже существует в рамках текущего пакета. Когда произойдет ошибка, выполнение текущего пакета будет прервано, однако выполнение продолжится с первого оператора в следующем пакете. Если в оставшейся части сценария будет обращение к столбцам или данным таблицы, которых нет, результаты могут быть непредсказуемыми. Как и для получения метаданных, существует масса способов проверки существования объекта. Давайте взглянем на некоторые из них. В листинге 3.2 представлен первый способ.
86 Глава 3. Шаблоны проектирования Листинг 3.2. Метод проверки существования объекта IF EXISTS(SELECT * FROM sysobjects WHERE name = 'authors') DROP TABLE dbo.authors GO CREATE TABLE dbo.authors '■ '"' ' . • Способ, подобный этому, чаще всего встречается в коде, созданном для SQL Server до версии 7.0. Этот код, конечно, работает, однако имеет несколько серьезных изъянов. Первый состоит в том, что код обращается напрямую к таблице sysobjects, и если эта таблица изменится, то код уже не будет работать. Второй изъян состоит в том, что в этом коде не проверяется владелец таблицы. В том случае, если объект authors был создан не пользователем dbo, оператор DROP TABLE будет выполнен, даже если объект dbo. aut ho rs существует. Давайте взглянем на другой способ. Листинг 3.3. Другой способ проверки существования объекта IF EXISTSCSELECT * FROM INF0RMATI0N_SCHEMA.TABLES WHERE TABLEJAME = 'authors' AND TABLE_SCHEMA='dbo') ' DROP TABLE dbo.authors GO CREATE TABLE dbo.authors Этот способ намного лучше предыдущего, так как он не обращается напрямую к системным таблицам. Вместо этого используется ANSI-совместимый подход: представление INFORMAT10N_SCHEMA.TABLES, чтобы определить, имеет ли пользователь dbo таблицу с названием authors. Хотя этот способ работает, он, во-первых, несколько длинноват, во-вторых, он использует подзапрос и представление для проверки существования объекта. Существует более общий подход, который показан в листинге 3.4. „ .,-. Листинг 3.4. Идиоматический подход к проверке существования объекта IF OBJECTJDCdbo.authors') IS NOT NULL DROP TABLE dbo.authors :> " " " "• '■■'• GO CREATE TABLE dbo.authors •• Этот способ, наверное, более предпочтителен, именно его я и использую. Он не зависит от структуры таблицы sysobjects; он короткий и лаконичный — такой, каким должен быть код. Он также эффективный с точки зрения выполнения. Единственный недостаток этого способа в том, что он проверяет тип объекта, как это делается во втором методе, используя представление I NFORMAT 10N_SCHEMA, TABLES, автоматически исключает все объекты, кроме таблиц и представлений. Решить эту проблему можно, немного изменив предыдущий способ. Четвертый способ (листинг 3.5) использует функцию для работы с метаданными 0BJECTPR0PERTY() для проверки типа объекта. Листинг 3.5. Улучшенный метод, который также проверяет тип объекта IF @BJECT_I0('dbo.authors') IS NOT NULL) AND @BJECTPR0PERTY@BJECTJDCdbo. authors'). TsTable')=l)
Идиомы 87 DROP TABLE dbo, authors ,„-•'«; --.'';-'.- •-"•'• i GO CREATE TABLE dbo.authors . .; Сам я чаще пользуюсь третьим способом, а не четвертым потому, что, во-первых, он короче, а во-вторых, имена объектов даже разных типов в любом случае должны быть уникальными. Подводя итог, можно сказать, что опытные разработчики используют либо только третий, либо только четвертый способ. Идиоматический способ проверки существования объектов состоит в использовании одной из этих техник. Установка контекста базы данных Обычно, чтобы установить необходимую базу данных, USE стараются вставить в сценарии как можно раньше для тех сценариев, которые должны выполняться в контексте определенной базы данных. Вы можете изменить текущую базу данных, выбрав ее из выпадающего списка в Query Analyzer или с помощью ключа -d утилиты OSQL, но идиоматический подход, который обычно используется способными разработчиками, предусматривает включение оператора USE в свои сценарии. Вы спросите: «А как проверить, что оператор USE отработал? Ведь если этого не сделать, то результаты могут оказаться ужасными, все может закончиться удалением объектов в базе данных master?» Обычный подход показан в листинге 3.6. Листинг 3.6. Смена текущей базы данных с последующей проверкой USE pubs2 ' ' ; ' ' :ч'': ' . " GO - ' ' "ч ■" • -^ ■' ' IF DBJAME()<>'pubs2' BEGIN - - ■ '■ . '-••*■.<■■.. .- ■ RAISERRORC'Wrong database. Мб. 10) : <■- . ..- < .-. RETURN END GO ' ' '"••"""' '" '"' ■■■'■■'■■■'■■■■■• ' ''. Здесь мы используем функцию DB_NAME() для проверки текущей базы данных. Может показаться, что мы можем просто проверить значение автоматической переменной @@ERR0R после выполнения оператора USE, чтобы убедиться, что он корректно отработал, но это сделать не удастся. Ошибка при выполнении оператора USE прервет выполнение текущего пакета команд, поэтому обработать ошибку не удастся. Это значит, что следующий код (листинг 3.7) не будет работать так, как мы хотим. Листинг 3.7. Ошибка при выполнении оператора USE прерывает выполнение текущего пакета, поэтому дальнейшая обработка ошибок становится невозможной -- Этот код работать не будет USE pubs2 . . - .,-■•,,■■ IF C(aERROR<>0 BEGIN RAISERRORCWrong DB.' .16,10) RETURN ' ' END • '■ ■ " GO • • " .
88 Глава 3. Шаблоны проектирования Очистка таблицы Существует два метода для полной очистки таблицы. Какой из них подойдет вам, зависит от ваших целей. Наиболее очевидный способ удалить все записи из таблицы — выполнить оператор DELETE без фильтра (то есть без части WHERE): DELETE Customers Это, конечно, работает, но удаление каждой записи записывается в журнал транзакций, что неприемлемо для больших таблиц. Более быстрый и прямой метод удаления всех записей из таблицы состоит в использовании команды TRUNCATE TABLE: TRUNCATE TABLE Customers Обычно TRUNCATE TABLE срабатывает моментально даже для очень больших таблиц. TRUNCATE TABLE быстрее, чем DELETE из-за того, что эта операция минимально журналируема, только ее операции с экстентами будут записаны в журнал транзакций. А это скажется на возможности восстановления базы данных. За все надо платить. Также помните, что TRUNCATE TABLE не получится использовать для таблиц, задействованных в ограничениях внешних ключей. Таким образом, идиоматический способ быстрой очистки таблицы состоит в использовании TRUNCATE TABLE, кроме случаев, когда это неприемлемо по причинам восстановления или когда есть внешние ключи, ссылающиеся на таблицу. Копирование таблицы Копирование структуры (и, возможно, данных) существующей таблицы в новую — довольно частая задача при разработке приложений баз данных. Мы берем некоторый шаблон, на основе которого создаем рабочую таблицу, куда затем вставляем записи, добавляем столбцы, индексы, ограничения и т. д. Есть несколько способов копирования таблицы, но действительно хороший — только один. Первый способ предполагает использование оператора CREATE TABLE с такой же структурой, как и у исходной таблицы. Не существует простого способа убедиться в том, что он на самом деле соответствует исходной таблице, а если даже и соответствует, изменение структуры первоначальной таблицы приведет к разрушению связи между ними. Другой способ состоит в использовании хранимых процедур sp_0A Automat i on, чтобы создать объект SQL-DMO Tab I e, соответствующий исходной таблице, и вызвать метод Sc г i pt этого объекта. Это создаст сценарий с CREATE TABLE для таблицы, который затем может быть выполнен из T-SQL (или через SQL-DMO, или при помощи xp_cmdshe I I, чтобы вызвать 0SQL. ЕХЕ). Хотя этот метод и работает (в главе 21 есть пример процедуры sp_generate_scr i pt, которая использует этот подход), его не стоит использовать, если все, что вы хотите сделать, — это скопировать таблицу. Есть более простой и, в том числе, более идиоматичный способ сделать это. Связка SELECT... INT0 — прекрасное расширение T-SQL для простого копирования таблицы (без ее ограничений) или создания постоянной копии результата запроса. Все, что вы можете получить с помощью оператора SELECT, может быть сохранено в постоянную таблицу. Итак, создать копию таблицы довольно просто, и это показано в листинге 3.8.
Идиомы 89 Листинг 3.8. SELECT...INTO — это быстрый способ создания копии таблицы SELECT * INTO newtable FROM oldtable Чтобы создать пустую копию таблицы, используйте условие WHERE, которое всегда возвращает fa I se (листинг 3.9). Листинг 3.9. С помощью SELECT...INTO можно также создать пустую копию существующей таблицы SELECT * INTO newtable FROM oldtable WHERE 0=1 Ноль никогда не равен единице, так что данные скопированы не будут, но, несмотря на это, таблица будет создана. Так же как TRUNCATE TABLE, SELECT...INTO — минимально журналируемая операция, так что ее использование сказывается на возможности восстановления базы данных. Этот способ часто используется, особенно для создания временных таблиц, поэтому вы должны быть способны распознать эту идиому с первого взгляда. Оператор SELECT...INTO, используемый так, как здесь, — это практически реализация шаблона Прототип «Банды четырех», и об этом я расскажу далее в этой главе. Присваивание значений переменным Хотя для присваивания значений можно использовать оператор SELECT, использование команды SET более предпочтительно. В принципе, они делают одно и то же, однако SET короче, и с помощью этой команды можно присваивать значения не только скалярным переменным, но и курсорным. С другой стороны, с помощью оператора SELECT можно присваивать значения одновременно нескольким переменным, а также значения из таблиц и представлений без использования подзапросов. Таким образом, в некоторых ситуациях можно использовать и SELECT (обычно для присваивания значений нескольким переменным). Мое повествование может показаться слишком подробным, но все же следует отметить, что, несмотря на кажущиеся минимальные различия между этими способами, присваивание значения переменной и возврат результирующего множества — это две фундаментально различные операции. Так что имеет смысл использовать для них две различные команды. Когда вы видите оператор SET в блоке кода T-SQL, вы точно знаете, что он делает — присваивает значение переменной. Если вы будете следовать соглашению о том, что для присваивания значений переменным используется исключительно оператор SET, а для получения данных — только оператор SELECT, ваш код будет проще читать — и вам, и тем, кто также будет работать над ним. Циклы Хотя использование конструкции WHILE — самый простой способ организации циклов в T-SQL, есть еще и другие подходы. Взгляните на листинг 3.10. Листинг 3.10. Существует несколько способов организации циклов в Transact-SQL DECLARE @var int SET @var=0 mytag: SET @var=@var+l ' "• ■ . IF №var<10) GOTO mytag
90 Глава 3. Шаблоны проектирования Этот способ работает, однако он не идиоматичный. Почему? Потому что этот способ не используется опытными разработчиками. Он неестественный, в нем нет реальных преимуществ и в нем используется неструктурный подход с использованием GOTO. В листинге 3.11 представлен способ лучше предыдущего, однако тоже не идиоматичный. Листинг 3.11. Использование WHILE не всегда идиоматично DECLARE @var int SET @var=10 WHILE (@var>0) BEGIN SET @var=@var-l . .. • END Почему этот способ не идиоматичный? Цикл в нем выполняется в обратном порядке без видимой на то причины. Запомните: идиомы языка — это набор естественных подходов к решению общих задач. Выполнение цикла в обратном порядке — не самый естественный порядок выполнения циклов, циклы должны выполняться в прямом порядке. Далее представлена идиоматичная форма. Листинг 3.12. Идиоматичная форма цикла в T-SQL DECLARE @var int _, к,-.с ■'• ■■-.," SET @var=0 "' ' ' ' '"' ' ' WHILE (@var<10) BEGIN , SET @var=@var+l END Этот код не только более короткий, он еще и более очевидный. Разработчику достаточно одно взгляда, чтобы понять, что код в блоке BEGIN/END выполняется 10 раз. Это пример того, что я подразумеваю, когда говорю, что в T-SQL есть идиомы, как и в любом другом языке. Хотя вы можете создавать циклы разными способами, способ с прямым циклом WHILE самый естественный, а, значит, также и самый идиоматичный. Неопределенные значения Правильная обработка неопределенных (NULL) значений всегда была большой проблемой для программистов. У разных производителей рекомендуемые методы работы с неопределенными значениями отличаются, к тому же с годами методы меняются, и все это, конечно, усложняет дело. Однако в Transact-SQL есть идиоматическая форма работы с неопределенными значениями. Выглядит она так, как показано в листинге 3.13. Листинг 3.13. Идиоматическая обработка неопределенных значений SELECT * FROM Customers WHERE Region IS NOT NULL Обратите внимание на то, что часть WHERE не такая — WHERE RegionoNULL ,. .-;, ' и не такая — WHERE ISNULLCRegion.'•)='' Хотя оба этих варианта могут работать (если установить правильное значение настройки ANSI JiULLS), они неестественны и непрозрачны. Для использования о необ-
Идиомы 91 ходимо, чтобы значение ANSI_NULLS было равно FALSE, так как, в соответствии со стандартом ANSI/ISO SQL, сравнение NULL-значений всегда возвращает NULL. Невозможность правильно установить значение ANSI_NULLS перед выполнением сравнения (или в случае хранимой процедуры перед ее компиляцией) приведет к тому, что не будет возвращено ни одной записи. Подход с использованием ISNULL() без особой надобности преобразует значение NULL в пустую строку и не принимает во внимание тот факт, что некоторые значения Reg i on действительно могут быть пустыми. Таким образом, ни один из альтернативных подходов не работает так же хорошо и естественно, как первый. Получение первых записей Довольно частая задача — получение п первых записей множества или таблицы. Есть несколько способов сделать это, но идиоматичный только один. Он показан в листинге 3.14. Листинг 3.14. Идиоматичная форма получения первых записей SELECT TOP 10 * FROM Customers ORDER BY CompanyName Поскольку в Transact-SQL есть расширение специально для получения п первых записей, его и следует использовать. Как я уже упоминал, есть и другие подходы. Один из них представлен в листинге 3.15. Листинг 3.15. Неидиоматичный запрос для получения первых записей SET R0WC0UNT 10 SELECT * FROM Customers , ORDER BY CompanyName SET ROWCOUNT 0 В этом способе SET ROWCOUNT используется без особой необходимости. Здесь больше часть кода, и он не всегда хорошо работает. Существует еще одна альтернатива. Листинг 3.16. Еще один подход к получению первых записей DECLARE с CURSOR FOR SELECT * . - - FROM Customers ' '■>■' ORDER BY CompanyName ■ ,; FOR READ ONLY . OPEN с ' DECLARE @i int SET @i=0 ■ •'■' " "''' ■ FETCH с WHILE (O@FETCH_STATUS=0) AND (@i<9) BEGIN SET @i=@i+l FETCH с END CLOSE С DEALLOCATE с
92 Глава 3. Шаблоны проектирования Очевидно, что этот способ самый худший. Он просто ужасающий. В нем используется курсор, переменная, цикл — все это нелогично. В этом способе каждая запись возвращается как отдельный результирующий набор (за счет последовательных вызовов FETCH), и код в нем больше, чем в идиоматичном способе. Также этот способ медленнее и требует больше памяти. К несчастью, этот способ иногда используют неопытные разработчики. Шаблоны проектирования Как я уже говорил в начале главы, умение распознавать и использовать шаблоны проектирования так же полезно в Transact-SQL, как и в других языках. Хотя большинство опубликованных шаблонов относятся к объектно-ориентированным языкам, многие из них можно также применять в Transact-SQL. Чтобы распознавать объектно-ориентированные шаблоны в не объектно-ориентированном языке, таком, например, как Transact-SQL, необходимо чутье и способность мыслить абстрактно. У объектно-ориентированных шаблонов и шаблонов Transact-SQL много общего. Однако хватает и таких, которые находят применение только в языках, ориентированных на работу с множествами, таких как Transact-SQL. О некоторых из них я расскажу в этой главе, но список их не претендует на полноту. Когда вы будете просматривать чей-нибудь код на Transact-SQL, попытайтесь выделить используемые шаблоны. Способность распознавать шаблоны помогает строить легко читаемый и модульный код. Итератор (Iterator) Часто бывает необходимо осуществить сложные операции (может быть, не одним оператором T-SQL) над каждым элементом последовательности. Это может быть вызов хранимой процедуры или выполнение некоторого динамического T-SQL для каждого элемента коллекции схожих элементов. Итератор — это шаблон для такой ситуации. В книге «Шаблоны проектирования» этому шаблону дается такое определение: «Итератор обеспечивает способ последовательного доступа к элементам составного объекта без демонстрации его внутреннего представления»1. Не вдаваясь в подробности, синоним итератора — курсор. Курсор обеспечивает способ последовательного доступа к записям таблицы (как эти записи получаются, мы не рассматриваем). Курсор позволяет разработчику на T-SQL сконцентрировать внимание на записях, которые отыскивает разработчик, а не на том, как их найти. Пример показан в листинге 3.17. Листинг 3.17. Пример шаблона Итератор DECLARE customer-list CURSOR FOR SELECT CompanyName FROM Customers FOR READ ONLY DECLARE @CompanyName varcharD0) Gamma, Erich, et al. Design Patterns. Reading, MA: Addison-Wesley, 1995. C. 257. (Гамма Э. и др. Приемы объектно-ориентированного проектирования. СПб.: Питер, 2003.)
Шаблоны проектирования 93 OPEN customerlist .. :. FETCH customerlist INTO @CompanyName WHILE (@@FETCH_STATUS=0) BEGIN '■ EXEC CalcCompanyTaxes @CompanyName FETCH customerlist INTO @CompanyName . . . .• . • END CLOSE customerlist ' ■'''• >' - "'' ' '"" DEALLOCATE customerlist По определению шаблоны идиоматичны. Итератор не исключение: предыдущий пример кода представляет собой идиоматичную форму использования курсора в Transact-SQL. Хотя есть и другие способы организации циклов и освобождения курсора, этот способ самый естественный и прямой. Хотя Итератор не является идиомой, это шаблон, который опытные разработчики Transact-SQL легко распознают и часто используют. Это стандартный способ прохода по списку любых объектов. Как только курсор определен, код Transact-SQL и операции, с помощью которых будут получены данные, становятся неважны коду, который будет их использовать. Команда FETCH не заботится о деталях курсора — ей просто нужен курсор. Она запрашивает данные, а курсор их доставляет. Чтобы понять применение этого шаблона, взгляните на пример в листинге 3.18. Листинг 3.18. Выполнение команды для всех объектов базы данных DECLARE tables CURSOR FOR SELECT TABLEJAME FROM INFORMATIONJCHEMA.TABLES ,- . ■■? ~<-f FOR READ ONLY DECLARE stable sysname ' ' OPEN tables • . • -:i - ■•■. FETCH tables INTO stable ' '"',., WHILE (@@FETCH_STATUS=0) BEGIN " ' -J :' ' " ''' "' EXEC spjielp stable -• ■"''--■ ■ ■[■>■! ■■• ■-- '■ FETCH tables INTO @table - .,, ■.■•:..>. • .' ■' •■ ' END 1;i , . . .-•....• CLOSE tables DEALLOCATE tables Этот код выводит подробную информацию обо всех таблицах базы данных. Обратите внимание на то, как этот код похож на код в предыдущем примере. Такая форма и есть шаблон. Даже если какие-то детали этого шаблона меняются от приложения к приложению, общая форма остается такой же. Опытным разработчикам на T-SQL достаточно одного взгляда на этот код, чтобы точно понять, что он делает. Они сразу начинают смотреть на переменные части шаблона, то есть те части, которые могут изменяться от приложения к приложению, чтобы понять, что этот шаблон делает. ПРИМЕЧАНИЕ Так как курсор можно вернуть в качестве выходного параметра хранимой процедуры, можно полностью изолировать код, который использует курсор, от кода, который его создает. Это еще более абарагирует понятие «итератор» Transact-SQL и еще больше приближает его к соответствующему шаблону «Банды четырех».
94 Глава 3. Шаблоны проектирования В дополнение к тому, что Итератор Transact-SQL сам является идиоматичной формой, он содержит несколько идиоматичных форм, точнее, три. Первая — это определение курсора. Есть несколько способов определить курсор. Например, вы можете определить локальную переменную типа cursor, а затем присвоить ей определение курсора. Этот способ будет работать, однако это лишнее отклонение. Помните, не стоит отступать от установленных соглашений, не имея на то серьезных оснований. Разработчики, которые будут читать ваш код в будущем, должны знать, на что обращать внимание, а на что — нет, и не должны отвлекаться напрасно на несущественные различия. Вторая форма — это цикл. Ранее мы рассмотрели идиоматичную форму цикла WHILE в T-SQL. Конечно, цикл можно сделать и другими способами, например с использованием GOTO, однако это снова будет отклонением от естественной формы без видимой на то причины. Таким образом, сам цикл представляет собой идиоматичную форму. И, наконец, код для очистки курсора. За вызовом CLOSE сразу следует вызов DEALLOCATE. Зачем? Нельзя ли просто освободить курсор? Разве он не будет автоматически закрыт? В общем, будет. Однако этот подход не самый естественный и не часто используемый опытными разработчиками. Опытные разработчики убирают за собой и закрывают то, что открыли. CLOSE является обратным для OPEN. DEALLOCATE — для DECLARE. Таким образом, включая оба оператора, мы сохраняем код и симметричным и логичным. Пересечение (Intersector) Так как Transact-SQL — язык, ориентированный на работу с множествами, операции с множествами — это то, в чем Transact-SQL особенно хорош. Наиболее часто встречающаяся операция — пересечение множеств. В SQL пересечение множеств реализуется при помощи объединений. Члены одного множества (таблицы или представления) сравниваются с членами другого, и элементы, присутствующие в обоих, возвращаются в качестве пересечения. Шаблон Пересечение представляет собой шаблон для пересечения множеств в Transact-SQL. Он использует внутреннее объединение в стиле ANSI для определения общей части двух таблиц. Пример приведен в листинге 3.19. Листинг 3.19. Выполнение шаблона Пересечение SELECT c.CompanyName, o.OrderlO FROM Customers с INNER JOIN Orders о ON (c.CustomerID=o.CustomerID) (Результаты сокращены) CompanyName OrderlD ' Alfreds Futterkiste 10643 Alfreds Futterkiste " " 10692 Alfreds Futterkiste 10702 Alfreds Futterkiste , 10835 Alfreds Futterkiste 10952 Alfreds Futterkiste 11011 Ana Trujillo Emparedados у he!ados 10308 Ana Trujillo Emparedados у helados 10625
Шаблоны проектирования 95 Ana Trujillo Emparedados y helados 10759 ■< Ana Trujillo Emparedados у helados 10926 Antonio Moreno Taqueria 10365 Antonio Moreno Taqueria 10507 Antonio Moreno Taqueria ; " ' 10535 ' - » ■ Antonio Moreno Taqueria - 10573 Довольно просто, не так ли? Хотя этот код бывает довольно разнообразен, важно уметь распознавать этот шабл он с первого взгляда. Когда вы смотрите код, вы должны подумать: «Мы ищем записи в первой таблице, соответствующие записям во второй». Распознавание шаблонов — вот секрет понимания сложного кода. Конечно, есть много разновидностей шаблона Пересечение. Вместо простого пересечения мы можем использовать внешние объединения и theta-объединения. Однако все они используют один и тот же шаблон. Условие объединения может различаться, но вопрос, на который мы отвечаем для каждого типа объединения, тот же: «Какие записи одной таблицы соответствуют (или не соответствуют) записям в другой?» Помните, что есть и другие способы реализации пересечения множеств и их вариации. Однако только один из них идиоматичный, тот, что показан в листинге 3.19. Хотя вы можете объединять таблицы с помощью условий в части WHERE, этого не стоит делать, потому что некоторые типы объединений (внешние объединения) могут фактически вернуть неправильные результаты, когда они выражены в части WHERE. (Это должно происходить с ассоциативными запросами и при задании порядка объединения; более подробно читайте «The Guru's Guide to Trans- act-SQL».) Наиболее естественный подход к созданию объединения — это подход, представленный в листинге 3.19. Спецификатор (Qualifier) Чаще, чем процесс получения данных, программисты Transact-SQL, наверное, используют процесс спецификации (фильтрации) получаемых данных. Определение возвращаемых данных основывается на фильтрации записей, возвращаемых запросом, с использованием значений столбца или столбцов. В SQL данные обычно определяются с использованием части WHERE оператора SELECT. Листинг 3.20. Шаблон Спецификатор SELECT * FROM Customers WHERE Country^'Mexico' Это очень простой пример, но шаблон виден. Является ли часть WHERE простой, как в листинге 3.20, или более сложной, с составными частями и подзапросами, шаблон оказывается тем же самым: это способ, которым вы определяете записи в результирующем наборе записей Transact-SQL. Помните, что существуют другие способы фильтрации результирующего множества. Например, выражение фильтра можно поместить в часть HAVING вместо части WHERE, как показано в листинге 3.21. Листинг 3.21. Неестественный фильтр SELECT City, C0UNT(*) AS NumberlnCity FROM Customers GROUP BY City HAVING City LIKE 'АГ
96 Глава 3. Шаблоны проектирования Проблема кода в листинге 3.21 в том, что в нем без причины используется часть HAVING, хотя должна использоваться часть WHERE. Подход с использованием WHERE более идиоматичен или естественен или то и другое. Другие разработчики, читающие этот код, могут придать значимость тому факту, что используется часть HAV ING вместо WHERE, тогда как на самом деле в этом нет никакой необходимости. Вот пересмотренный код, использующий WHERE. Листинг 3.22. Идиоматическая форма шаблона Спецификатор SELECT City, COUNK*) AS NumberlnCity FROM Customers .... , , . WHERE City Like 'АГ GROUP BY City Предназначение части HAV I NG состоит в фильтрации запроса после того, как результирующее множество будет собрано (например, на основании агрегатной функции). Поэтому в нем нет необходимости в листинге 3.21, к тому же SQL Server преобразует часть HAVING в часть WHERE. Если вы сравните планы выполнения запросов из листинга 3.21 и листинга 3.22, вы увидите что они одинаковые. Если SQL Server не произведет оптимизацию и таблица содержит большое количество записей, производительность, вероятно, сильно ухудшится, потому что все записи будут собраны перед наложением фильтра. Исполнитель (Executor) Хотя Transact-SQL обладает большой мощью, довольно часто возникает необходимость в создании и выполнении динамического T-SQL кода из хранимой процедуры или пакета команд. У вас просто нет другого выхода, особенно если вам необходимо параметризовать название объекта или столбца (то, что в Transact-SQL обычными способами не сделать). Шаблон Исполнитель — это шаблон для создания и выполнения динамического кода T-SQL. Листинг 3.23. Шаблон Исполнитель DECLARE @s int, @sql nvarcharA28) ' ' DECLARE spids CURSOR FOR ' ' ' ■ ...--• SELECT spid ,. > FROM master..sysprocesses WHERE spid<>@@SPID AND net_address<>" FOR READ ONLY OPEN spids . . FETCH spids INTO @s WHILE (@@FETCH_STATUS=0) BEGIN SET @sql='KILL '+CAST(@s AS varchar) EXEC sp_executesql @sql FETCH spids INTO @s END CLOSE spids DEALLOCATE spids В этом примере открывается курсор по псевдотаблице sysprocesses и динамически создается оператор T-SQL, который прерывает каждое пользовательское
Шаблоны проектирования 97 соединение, кроме текущего. Обратите внимание на то, что я сказал о пользовательском соединении. Мы различаем пользовательские и системные соединения, проверяя значение столбца net_add ress таблицы sysp rocesses. У системных соединений значение net_address пустое. Мы воздерживаемся от попыток прервать текущее соединение (это в любом случае нельзя сделать), проверяя для этого значение автоматической переменной @@SP ID. Вы можете поместить любой правильный T-SQL код в переменную @sq I, а хранимая процедура sp_executesq I попытается его выполнить. Также вы можете сделать цикл с другими условиями или по другим объектам. Например, вы можете сделать цикл по объектам текущей базы данных и динамически создать команду T-SQL, чтобы сделать с ними что-нибудь. Обратите внимание на использование sp_executesq I. Вместо нее можно использовать ЕХЕС(), однако я предпочитаю использовать sp_executesq I, потому что она более гибкая и будет работать хорошо в большем числе сценариев. К тому же sp_executesq I поддерживает параметризованные запросы. Это приводит к повторному использованию плана, и, соответственно, к увеличению производительности. Хотя в примере в листинге 3.23 не используются параметры (разрешены только правильные параметры поиска; например, вы не можете подставлять названия объектов или идентификаторы соединений), но если таковые были бы, sp_executesq I оказался бы лучшим выбором, чем ЕХЕС(). sp_executesq I, который также может возвращать код завершения из вызова динамического T-SQL. Если в динамическом коде возникнет ошибка с уровнем 11 и выше, sp_executesql вернет номер ошибке в своем коде завершения. Таким образом, хотя ЕХЕС() в этом случае будет работать так же хорошо, подход с использованием sp_executesq I используется чаще опытными разработчиками T-SQL по описанным мною причинам. Это делает его более идиоматичным, чем ЕХЕС(), и поэтому я использовал этот подход в приведенном примере шаблона. Конвейер (Conveyor) Шаблон Конвейер представляет собой механизм, с помощью которого можно вернуть код или результат через последовательность вызовов (стек вызовов). Например, у вас есть три процедуры: РгосА, РгосВ и РгосС. РгосА вызывает РгосВ, и РгосВ вызывает РгосС. Во время выполнения РгосС возникает непредвиденная проблема, и вы хотите передать информацию об этой проблеме назад по цепочке в процедуру РгосА. Как это сделать? Шаблон Конвейер показывает (листинг 3.24). Листинг 3.24. Шаблон Конвейер CREATE PR0C РгосС AS IF OBJECTJD('no_exist') IS NOT NULL SELECT * FROM no_exist ELSE RETURN(-l) . , GO CREATE PROC ProcB AS DECLARE @res int ' EXEC @res=ProcC RETURN(@res) продолжение &
98 Глава 3. Шаблоны проектирования Листинг 3.24 {продолжение) GO CREATE PROCEDURE ProcA AS DECLARE @res int EXEC Ores=ProcB SELECT Ores GO EXEC ProcA (Результаты сокращены) -1 Обратите внимание на способ, который мы используем для передачи кода завершения хранимой процедуры от процедуры к процедуре. Этот способ хорошо работает для целых значений. Но что делать, если мы хотим вернуть сообщение об ошибке вместо кода? Шаблон все равно работает. Листинг 3.25. С помощью шаблона Конвейер можно передавать данные любого типа USE tempdb GO DROP PROC ProcA. ProcB, ProcC GO CREATE PROC ProcC OMsg varcharA28) OUT AS IF DBJECTJD('no_exist') IS NOT NULL SELECT * FROM no_exist ELSE SET @Msg='Table doesrT't exist' GO CREATE PROC ProcB @Msg varcharA28) OUT AS EXEC ProcC @Msg OUT GO CREATE PROCEDURE ProcA AS DECLARE @Msg varcharA28) EXEC ProcB @Msg OUT SELECT @Msg GO EXEC ProcA (Результаты сокращены) Table doesn't exist В этом случае мы просто используем выходные параметры, чтобы передать сообщение вверх по стеку вызова самой верхней процедуре. Поскольку мы можем использовать здесь любой тип данных (включая курсорный), мы можем возвращать любую информацию. И последнее применение этого шаблона состоит в том, чтобы передать настоящий код ошибки вверх по цепочке вызвавшей процедуре. Пример приведен в листинге 3.26.
Шаблоны проектирования 99 Листинг 3.26. Шаблон Конвейер может передавать ошибки так же хорошо, как и сообщения CREATE PR0C РгосС AS DECLARE @err int IF @@TRANCOUNT=0 ROLLBACK TRAN -- Ошибка, мы не в транзакции SET @err=@@ERR0R RETURN(Oerr) GO CREATE PROC ProcB AS DECLARE @res int EXEC @res=ProcC RETURN(@res) GO CREATE PROCEDURE ProcA AS DECLARE @res int EXEC @res=ProcB SELECT @res GO EXEC ProcA (Результаты сокращены) Server: Msg 3903, Level 16, State 1, Procedure ProcC, Line 4 The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION. 3903 Независимо от применения, шаблон Конвейер предоставляет механизм передачи информации по цепочке хранимых процедур. В этом смысле Конвейер похож на шаблон Цепочка ответственности «Банды четырех». Уборщик (Restorer) Шаблон Уборщик предоставляет механизм для очистки использованных ресурсов при возникновении ошибки. Восстановление рабочего окружения особенно важно в середине транзакции. Чтобы избежать повисших (осиротевших) транзакций, очень важно правильно обрабатывать ошибки в то время, когда транзакция активна. «Осиротевшие» транзакции могут накладывать блокировки и не давать работать другим соединениям. Реализация шаблона Уборщик показана в листинге 3.27. Листинг 3.27. Шаблон Уборщик очищает окружение при возникновении проблем CREATE PROC ProcR AS DECLARE @err int BEGIN TRAN Update Customers SET City = 'Dallas' SELECT 1/0 -- Force an error SET @err=@@ERR0R IF @err<>0 BEGIN продолжение &
100 Глава 3. Шаблоны проектирования Листинг 3.27 {продолжение) ROLLBACK TRAN RETURN(@err) END COMMIT TRAN GO DECLARE Ores int EXEC Ores=ProcR SELECT Ores (Результаты сокращены) Server: Msg B134. Level 16, State 1, Procedure ProcR. Line 8 Divide by zero error encountered. 8134 Ключевые участки этого шаблона — сохранение кода ошибки и блок IF, реагирующий на ненулевое значение кода ошибки. Мы сохраняем значение @@ERROR, потому что оно сбрасывается следующим успешно выполненным оператором. После того как мы сохранили значение кода ошибки, мы проверяем его и откатываем активную транзакцию в случае ошибки. Также просто мы можем очистить другие типы ресурсов (листинг 3.28). Листинг 3.28. Шаблон Уборщик очищает окружение при возникновении проблем CREATE PROC ProcR AS DECLARE @err int CREATE TABLE ##myglobal - . (cl int) INSERT ftfmyglobal DEFAULT VALUES * ■ ■ ,, .- , -',-• SELECT 1/0 -- Force an error SET 0err=0@ERR0R " IF 0err<>0 BEGIN ' DROP TABLE ftfayglobal RETURN(Oerr) END DROP TABLE ##myglobal GO DECLARE @res int EXEC Ores=ProcR SELECT @res (Результаты сокращены) Server: Msg 8134, Level 16, State 1, Procedure ProcR, Line 9 Divide by zero error encountered. 8134
Шаблоны проектирования 101 Здесь мы удаляем глобальную временную таблицу, когда случается ошибка. Есть несколько типов очистки, которые мы можем сделать здесь, самая важная из них — транзакционная. Применяйте шаблон Уборщик, чтобы избежать повисших транзакций и ненужных блокировок. С помощью разновидностей шаблона Уборщик можно действенно избежать проблем, которые могли быть унаследованы блоком кода, а не вызваны им. Пример представлен в листинге 3.29. Листинг 3.29. С помощью шаблона Уборщик можно предотвратить проблемы CREATE PROC ProcR AS ■ .. * ■ . • . .. IF @@TRANCOUNT<>0 - Откатить старую транзакцию перед началом новой ROLLBACK TRAN DECLARE @err int '.'---^ j i ■ ' ,.** BEGIN TRAN Update Customers SET City = 'Dallas' SELECT 1/0 - Вызвать ошибку SET @err=(a(aERROR IF @err<>0 BEGIN ROLLBACK TRAN RETURN(@err) ■ . ■ • : END COMMIT TRAN GO DECLARE @res int EXEC @res=ProcR SELECT @res (Результаты сокращены) Server: Msg 8134, Level 16, State 1. Procedure ProcR, Line 12 Divide by zero error encountered. 8134 Обратите внимание на первый оператор ROLLBACK в процедуре. Он выполнится, если процедура определит, что уже есть активная транзакция @@TRANCOUNT<>0 при первом старте. Поскольку это рассматривается как ошибка, процедура откатывает открытую транзакцию (единственный вызов ROLLBACK откатывает все активные транзакции, независимо от вложенности) перед началом новой. В этом смысле шаблон Уборщик реализован превентивно — он используется, чтобы очистить пространство после тех, кто мог оставить окружение в промежуточном состоянии. Кодирование такой логики в ваших приложениях особенно важно, когда для соединения с SQL Server используется пулинг соединений (это очень часто применяется при работе с веб-серверами). Поскольку какое-нибудь виртуальное соединение может оставить открытую транзакцию, что в дальнейшем может повлиять на пользователей того же самого физического соединения; важно, чтобы ваш код
102 Глава 3. Шаблоны проектирования знал, как защитить себя от этих бандитских транзакций и их нежелательных последствий. СОВЕТ Механизмы обработки ошибок в Transact-SQL далеки от совершенства. Они не всегда работают так, как вы хотели бы. Например, есть множество ошибок, которые являются достаточно серьезными, чтобы прервать выполнение текущего пакета команд. Когда они случаются, любой код обработки ошибок, следующий за кодом, вызвавшим ошибку (даже в хранимой процедуре), не будет достигнут. Так что, даже если вы проверяете в своем коде @@ERROR и выполняете ROLLBACK в случае возникновения проблем, существуют ошибки, которые не дадут им выполниться. Это, наверное, самая частая причина возникновения «повисших» транзакций, поэтому в своем коде проверяйте наличие транзакции перед началом новой. Прототип (Prototype) Согласно книге «Шаблоны проектирования», задача шаблона Прототип состоит в том, чтобы «определить типы объектов, которые можно создавать на основе прототипа, и создавать новые объекты, копируя прототип»1. Другими словами, вы используете шаблон Прототип для создания объектов. Наиболее очевидное применение шаблона Прототип состоит в использовании конструкции SELECT... INT0. Поскольку эта конструкция помещает результаты запроса в новую таблицу, ее можно использовать, чтобы легко скопировать содержимое таблицы или представления, как показано в листинге 3.30. Листинг 3.30. Реализация шаблона Прототип на Transact-SQL SELECT * INTO NewCustomers FROM Customers Поскольку SELECT...INTO обладает всей мощью обычного оператора SELECT, то можно изменить прототип, указав список столбцов, условие в части WHERE или даже часть GROUP BY или HAVING. Поэтому реализация этого шаблона на Transact-SQL более гибкая, чем большинство объектно-ориентированных реализаций, так как он использует всю мощь SQL как языка, ориентированного на работу с множествами. Например, вы можете указать условие в части WHERE, которое всегда ложно, чтобы создать пустую копию таблицы. Листинг 3.31. Реализация шаблона Прототип, которая создает пустую копию SELECT * INTO NewCustomers FROM Customers WHERE 0=1 Здесь мы клонируем структуру таблицы, опуская данные. Использование ложного условия в части WHERE оператора SELECT... INT0 — самый простой способ скопировать структуру таблицы без лишних затрат, связанных с журналированием при копировании данных. Другая разновидность этого шаблона позволяет указать новые данные в процессе клонирования таблицы (листинг 3.32). 1 Там же. С. 117.
Шаблоны проектирования 103 Листинг 3.32. Эта реализация шаблона Прототип на Transact-SQL позволяет вставлять новые данные в процессе копирования SELECT IDENTITY(int. 1,1) AS CustNo. * INTO NewCustomers FROM Customers Вы также можете указать новые столбцы, столбцы из других таблиц и представлений (с помощью объединений), константы или функции. Возможности безграничны. Суть всего сказанного здесь в том, что вы должны уметь с первого взгляда распознавать шаблон Прототип и знать, что в Transact-SQL он чаще всего реализуется с помощью конструкции SELECT... INT0. Одиночка (Singleton) Предназначение шаблона Одиночка — обеспечить существование только одного экземпляра класса, а также обеспечить доступ к этому экземпляру. В терминах реляционных баз данных этот шаблон может выглядеть по-разному. Строго говоря, эквивалент класса из ООП в СУБД — таблица. Соответственно, экземпляр класса — запись в таблице, так что наиболее очевидная задача этого шаблона состоит в том, чтобы обеспечить существование только одной записи в таблице (листинг 3.33). Листинг 3.33. Реализация шаблона Одиночка на Transact-SQL USE tempdb GO DROP TABLE LastCustNo GO CREATE TABLE LastCustNo - (LastCustNo int) GO INSERT LastCustNo VALUES A) GO CREATE TRIGGER LastCustNoInsert ON LastCustNo FOR INSERT AS IF (SELECT C0UNT(*) FROM LastCustNoM BEGIN RAISERRORCYou may not insert more than one row into this table',16.10) ROLLBACK TRAN END ■ ■ , GO INSERT LastCustNo VALUES B) - Вызовет ошибку из-за триггера GO SELECT * FROM LastCustNo Благодаря триггеру в таблице может быть только одна запись. Если вы попытаетесь вставить запись, а в таблице уже есть хотя бы одна, вы получите ошибку, и транзакция будет откачена. Конечно, с помощью BULK INSERT (с отключенными триггерами) можно обойти это ограничение, но так как мы не будем делать это намеренно, наша реализация шаблона. Одиночка с помощью триггера довольно ошибкоустойчива. Обратите внимание на использование конструкции IF (SELECT C0UNT(*) FROM LastCustNo)>1 для определения наличия записи в таблице. Почему мы используем условие >1 вместо =1 ? Все просто. За исключением триггеров I NSTEAD OF, триггеры Transact-SQL выполняются после того, как операция завершилась, но до того, как операция записана в базу данных. Это значит, что с, точки зрения триггера, в таблице LastCustNo существует две записи, пока мы не откатили транзакцию. По этой же причине мы не можем использовать предикат EX ISTS () (напри-
104 Глава 3. Шаблоны проектирования мер, IF EX ISTS( SELECT * FROM LastCustNo)), чтобы проверить наличие записей в таблице, поскольку только что вставленные записи видны триггеру, пока мы не откатим транзакцию. Таким образом, использование EX I STS() не позволило бы нам вставить запись в таблицу, даже если она была пуста. Другое приложение шаблона Одиночка в реляционных базах данных состоит в использовании ограничений первичного ключа и уникальности, чтобы существовал только один экземпляр записи. Другими словами, если приравнять таблицу к классу, а записи к экземплярам (объектам) этого класса, применение этого шаблона для предотвращения существования нескольких копий записи состоит в добавлении ограничения первичного ключа или ограничения уникальности. Еще одно возможное применение шаблона Одиночка в SQL Server, состоит в том, чтобы предотвратить подключение к серверу нескольких копий приложения. Например, если одна копия программы Check Writer уже подключена к базе данных, может потребоваться не дать другим подключаться, пока первая копия не отсоединится. Есть несколько средств SQL Servera, которые удобно использовать в этой ситуации. Первое — блокировки приложения. SQL Server позволяет использовать менеджер блокировок извне для управления ресурсами. Таким образом, можно наложить блокировку приложения, когда ваше приложение будет запущено, а потом, когда приложение будет закрыто, снять ее. Установка эксклюзивной блокировки не даст работать другой копии приложения, пока вы не снимите ее. Вот код, показывающий это: DECLARE @res int BEGIN TRAN EXEC @res - sp_getapplock ©Resource = 'Check Writer'. (PLockMode = 'Exclusive.' -- Return to your app -- Then execute this when the app exits EXEC @res * sp_releaseapplock (^Resource = 'Check Writer' ROLLBACK TRAN Проблема этого подхода в том, что транзакция может быть открыта долгое время. Вообще говоря, вы не должны оставлять транзакцию открытой долгое время или в ожидании ввода пользователя. Из-за того, что для использования sp_getapplock необходимо открыть пользовательскую транзакцию, чтобы наложить необходимую нам блокировку, этот инструмент не самый лучший в нашей ситуации. Давайте взглянем на второе, более подходящее средство SQL Server: IF EXISTSCSELECT * FROM master, .sysprocesses WHERE contextjnf0=0x123456) RAISERRORCYou can run only one copy of this application at a time'.20.1) WITH LOG ELSE SET C0NTEXTJNF0 0x123456 В этом коде используется команда SET CONTEXTJNFO для того, чтобы вставить пользовательское значение в sysprocesses во время запуска. Тогда при каждом запуске приложение проверяет это значение. Если значение присутствует — это означает, что соединение со специальным маркером уже открыто и приложение генерирует ошибку, которая прерывает соединение. Если значения нет, процедура помещает его в sysprocesses и загрузка приложения продолжается. Это один из способов проверить, что только одна копия приложения подключается к SQL Server, однако существуют и другие. Например, прило-
Шаблоны проектирования 105 жение может установить значение переменной сессии hostname, а затем при загрузке проверить sysprocesses. Итак, есть множество различных применений шаблона Одиночка в Transact-SQL. Другие шаблоны Множество других объектно-ориентированных шаблонов имеют соответствующие или похожие шаблоны в Transact-SQL. Например, представление SQL Server близко шаблону Композит (Composite) «Банды четырех», а представление с триггером INSTEAD OF — по функциональности схоже с шаблоном Фасад (Facade): оно предоставляет единый интерфейс для множества интерфейсов подсистемы. Следуя этой аналогии, множество интерфейсов — это набор таблиц, использующихся в представлении и кода Transact-SQL, необходимого для их обновления. Пользователь обновляет данные, полученные с помощью представления, как если бы они были получены из одной таблицы, следовательно, представление — это единый интерфейс. Триггер INSTEAD OF принимает изменения и распределяет их по соответствующим таблицам, на которых основано представление. Вся совокупность элементов — представление, триггер и таблицы — представляют собой шаблон, сходный с шаблоном Фасад (Facade). Когда вы видите представление с триггером I NSTEAD OF, должно быть очевидно, что тот, кто его создал, предоставил интерфейс для чего-то более сложного за счет обновления таблиц, на которых основано представление. Другой пример объектно-ориентированного шаблона Transact-SQL — Цепочка ответственности. Я упоминал этот шаблон ранее, когда обсуждал шаблон Конвейер. Рассмотрим его немного подробнее. В книге «Шаблоны проектирования» шаблон Цепочка ответственности описывается как шаблон, который позволяет предотвратить ситуацию, когда запрос от отправителя к получателю может быть обработан разными объектами'. Развивая эту мысль, можно сказать, что при применении этого шаблона вы должны «Связать все получающие (вызываемые) объекты в одну цепочку и передавать запрос по этой цепочке до того объекта, который и обработает его»2. Подумайте об этом. Какая конструкция Transact-SQL наиболее точно реализует описанное поведение? Вложенные триггеры и несколько триггеров для таблицы. Триггеры могут осуществлять операции, которые запускают другие триггеры. Это поведение и есть цепочка. То же самое можно сказать и о нескольких триггерах на одну и ту же таблицу. Скажем, вы определили несколько INSERT-триггеров для некоторой таблицы, и каждый из этих триггеров ответственен за проверку значений различных столбцов в новой вставленной записи. Запрос на вставку в том же самом виде с точки зрения функциональности передается от триггера триггеру, но триггеры срабатывают в произвольном порядке. В любом случае при наличии нескольких триггеров на одну таблицу или в случае вложенных триггеров, если любой из них решает отклонить вставку и откатить транзакцию, вся операция будет прервана. Этим реализуется часть шаблона, который «вызывается для передачи запроса через всю цепочку объектов до объекта, который его обрабатывает». Хотя аналогия и не совсем точна, шаблон есть, если вы ищете его. 1 Там же. С. 223. 2 Там же.
106 Глава 3. Шаблоны проектирования Команда — это еще один шаблон «Банды четырех», который имеет соответствие в TSQL. В книге «Шаблоны проектирования» он определяется так: «Инкапсулирует запрос как объект, тем самым позволяя параметризовать клиентов с различными запросами, организовать очередь и журналировать запросы, реализовать поддержку отменяемых операций»1. Сможете найти этот шаблон? Какой элемент SQL Server более всего соответствует этому определению? Подумайте над возможностями «организации очереди и журналирования запросов» и «поддержки отменяемых операций». Правильно! Реализация шаблона Команда в Transact-SQL — это транзакция! Журнал транзакций SQL Server — это механизм, с помощью которого запросы (изменения данных) журналируются. Само изменение — это запрос, а операции являются отменяемыми на основании того факта, что вы можете откатить транзакцию. Существуют и другие параллели между шаблонами «Банды четырех» и техниками, которые часто используются при разработке приложений с использованием Transact-SQL. Знание шаблонов, умение использовать и распознавать их так же важно в Transact-SQL, как и в любом другом языке. Распознавание шаблонов помогает сделать всю структуру приложения проще и легче для понимания, а использование шаблонов позволяет сохранять программы модульными и легко расширяемыми. Итоги В этой главе вы узнали: ■ о важности применения идиоматичных и естественных подходов при создании программного обеспечения; ■ о том, что отклонение от принятых правил и прямых методов приводит к усложнению программы; ■ о некоторых идиомах и шаблонах проектирования, которые обычно используются в Transact-SQL, и о разнице между идиомами и шаблонами. 1 Там же. С. 233.
4 Управление исходным кодом Я не выдающийся программист — я программист с выдающимися привычками. К. Бек1 Я поместил в книгу эту главу сразу же, как счел возможным, поскольку я полагаю, что для разработки надежного кода в сложных проектах принципиально важно иметь хорошую привычку управления исходными текстами. Успешное построение сложной системы при небрежном обращении с исходным кодом так же вероятно, как и построение космического корабля в гараже: это возможно теоретически, но поверить в это трудно. Среди разработчиков Transact-SQL, особенно неопытных, распространено такое обращение с кодом Transact-SQL, как будто бы это «ненастоящий» код. Они не пользуются подходящим редактором и вполне удовлетворены, работая по восемь часов в день со средствами, предназначенными скорее для редактирования командных файлов, а не программного кода. Они не комментируют свой код и не следуют никаким правилам, которые можно увидеть в языках программирования других типов. Для управления своим кодом на Transact-SQL они не пользуются средствами управления исходным программным кодом (системами контроля версий), такими как: VSS, PVCS, Source Integrity (Vertical Sky Software Manager). Напротив, код хранимых процедур рассматривается скорее как ресурс базы данных, нежели как программный код и, исходя из этого, управляется в атомарной, транзакционной манере, которая обычно применяется к другим типам объектов базы данных. В этой перспективе код процедур на Transact-SQL — просто данные. Более того, код хранимых процедур располагается в базе данных в таблице syscomments и защищен от повреждения, как и другие данные, посредством резервного копирования — такова логика рассуждений. Цель этой главы — развеять миф, что код Transact-SQL является «ненастоящим» кодом, и показать важность управления исходными текстами (то есть управления версиями) с помощью специального программного обеспечения. Хотя вы можете использовать любую систему для управления версиями текстовых файлов, в этой главе будет рассказано о VSS, поскольку я использую это средство. Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 57.
108 Глава 4. Управление исходным кодом Преимущества управления исходными текстами Полагаю, что обсуждение следует начать с разговора о преимуществах хранения вашего кода на Transact-SQL в системах контроля версий. Не все из этих преимуществ очевидны сразу, поэтому их будет полезно перечислить и пояснить. Во-первых, контроль версий позволяет вам вернуться к предыдущей версии, в случае если вы обнаружили ошибку или решили забраковать начатое изменение кода. Поскольку в системе каждая зафиксированная версия легкодоступна, вы можете получить ее без проблем. Во-вторых, системы контроля версий обычно обеспечивают возможность нахождения различий между версиями. Это просто неоценимо. Когда вы обнаруживаете проблему, порожденную изменениями в коде, вы можете проверить различия между ошибочной версией и последней рабочей версией и найти, в чем ваша ошибка. VSS обладает визуальным средством контроля различий, которое размещает два файла рядом друг с другом на экране, выделяя отличия две- ■ том. В-третьих, системы контроля версий гарантируют согласованность версий. Когда вы разрабатываете и выпускаете программное обеспечение, со временем, естественно, начинают появляться новые версии. И, скажем, через шесть месяцев после выпуска очередной версии продукта вам понадобится снова скомпилировать «ту самую» версию. Система контроля версий позволяет присвоить метки и получить всю версию целиком за один шаг при нажатии кнопки. Вы просто находите версию с необходимой меткой и даете системе команду восстановить ее для вас. Подумайте, каким бы сложным оказался этот процесс без помощи такой системы контроля. Найдя файлы требуемой, как вам кажется, версии, вы должны были бы проверить каждый файл — сотни или даже тысячи файлов — и убедиться, что они не только соответствуют требуемой версии приложения, но также имеют одинаковую версию. И это подразумевает, что вы прежде всего сохраняете исходный код старой версии. В сущности, вы были бы вынуждены сами исполнять роль системы контроля версий. Вместо создания программного обеспечения, вы бы проводили изрядное количество времени, занимаясь вещами, которые лучше поручить компьютеру. В-четвертых, система контроля версий помогает защитить программный код от случайных потерь и разрушений. Поскольку программный код обычно хранится в центральном хранилище (базе данных особого типа), то сохранять резервные копии и управлять ими в таком случае гораздо легче, нежели сотнями и тысячами файлов, разбросанных по рабочим станциям команды разработчиков. В-пятых, системы контроля версий значительно упрощают управление изменениями. Поскольку те, кто меняют код, должны перед изменением пометить файл и изменения возымеют силу только после регистрации изменений, легко отследить, кому принадлежит каждое внесенное изменение. Если изменение имело плохие последствия для приложения, оно может быть легко отменено. Если изменение требует пояснения или комментария, которые уточняют его цель и сферу воздействия, они могут быть добавлены при регистрации изменений в системе. Поскольку изменения данного элемента программного кода
Удачные решения 109 могут быть сделаны в одно время только одним разработчиком, системы контроля версий предотвращают случайную перезапись изменений, сделанных кем- то другим. Короче говоря, хорошие системы контроля версий помогают управлять исходными текстами программ. Они поднимают управление программным кодом с уровня захламленного стола до уровня хорошо организованной картотеки. Это может служить хорошим подспорьем для разработки сложных проектов. ПРИМЕЧАНИЕ Я пишу об этом несмотря на то, что на горизонте новое поколение систем контроля версий, которое серьезно изменяет тенденции развития этих систем. Управление изменениями и другие операции по управлению программным кодом в этих новых системах скорее ориентированы на большие команды разработчиков. Например, существует широко разрекламированная возможность одновременного изменения одного файла с исходным текстом несколькими разработчиками. При этом система контроля версий выполняет трехсторонний (или многосторонний) контроль различий и разрешает конфликты между разными версиями одного и того же члена команды. Очевидно, это может быть большим преимуществом для больших команд с большими файлами программного кода. Традиционные системы контроля версий предотвращают изменения файла с кодом —даже независимые и несвязанные изменения — до тех пор, пока кто-либо не пометит файл. В больших командах разработчиков нередко случается, что кто-то один просит другого сделать регистрацию изменений файла для того, чтобы он мог продолжить свою работу. Как я сказал, эта глава рассматривает контроль версий в перспективе использования более традиционных средств, вроде VSS, но не стоит забывать о существовании средств нового поколения. Процедуры dt Если после установки Visual Studio Enterprise вы долго бродили по системным таблицам на вашем сервере, возможно, вы заметили встроенные Microsoft хранимые процедуры, чьи имена начинаются на dt. Эти процедуры используются Visual Studio для обеспечения управления исходными текстами хранимых процедур на Transact-SQL. Беглый просмотр этих процедур показывает, что они используют COM-o6beicTSQLVers ionContro I. VCS_SQL Этот объект используется Visual InterDev для управления кодом хранимых процедур прямо из среды разработки (IDE). Если • вы установили серверные компоненты (Visual InterDev Server Components), в вашей системе уже установлен этот объект, и вы вполне можете использовать процедуры dt_% прямо из InterDev. Поскольку не у всех установлена версия Visual Studio Enterprise, в этой главе будет показано, как управлять сценариями Transact-SQL, используя среду VSS и поставляемый VSS СОМ-интерфейс. Удачные решения Поскольку контроль версий относится к сфере управления, имеет смысл начать обсуждение с того, что в кругах менеджеров известно под названием удачные решения. Удачные решения — это лучшие технические приемы и методики решения каких-либо задач. Все области — особенно инженерные — имеют лучшие решения; в большинстве сфер квалифицированного труда существуют подходы, которые
110 Глава 4. Управление исходным кодом работают лучше других. Я расскажу о нескольких подходах, заслуживающих того, чтобы их использовали в области контроля версий. Храните объекты в сценариях Возможно, вы и правы, храня программный код Transact-SQL создаваемых объектов исключительно в базе данных SQL Server, которой принадлежат объекты, но мне кажется, это не очень хорошая идея. Почему? Потому что без средств, вроде процедур dt_%, вы лишены способа осуществлять над ними контроль версий. То есть вы не можете управлять изменениями, возвращаться к предыдущей версии (без восстановления резервной копии всей базы) и отслеживать изменения между версиями. Итак, храните исходные тексты ваших объектов в файлах со сценариями. Сценарии должны быть раздельными Храните каждый объект в отдельном сценарии. Это будет поддерживать высокую модульность системы и позволит избежать ограничений на изменения несвязанного кода другими разработчиками. Вы можете редактировать процедуры или другие типы сценариев, не беспокоясь, что оставите других без работы. Не используйте Unicode Сохраняйте каждый файл со сценарием в кодировке ANSI. He все системы контроля версий могут использовать Unicode (текущая1 версия VSS не может), и хотя эта кодировка является параметром по умолчанию в мастере создания сценариев Enterprise Manager, ее не следует использовать, если вы не хотите потерять совместимость с большинством средств для работы с текстовыми файлами. Например, вы можете поместить файл в Unicode-кодировке в VSS, но он обрабатывается как бинарный файл, поскольку VSS не распознает кодировку Unicode. А это означает, что вы не сможете отслеживать изменения между версиями таких файлов, что является серьезным ограничением. Используйте метки для обозначения версий Большинство систем контроля версий обладают возможностью, которая позволяет помечать версию вашего программного кода так, что вы можете впоследствии, благодаря этому, иметь ссылку на версию как на связанную группу файлов. Используйте эту возможность для пометки версии программного обеспечения или приложения. Впоследствии это избавит вас от лишних хлопот. Очевидно, что разные части кода меняются с разной частотой. Номера их внутренних версий будут отличаться. Тем не менее, если вы присвоите метку, обозначающую общую версию, всем файлам, входящим в текущий релиз программного продукта, вы сможете получить и скомпилировать этот релиз, как только возникнет необходимость, не нуждаясь в ручной синхронизации разных номеров внутренних версий файлов. Имеется в виду 6.0. — Примеч. перев.
Удачные решения 111 Используйте ключевые слова Общая возможность систем контроля версий — средство, позволяющее вносить в исходные тексты специальные ключевые слова, которые могут послужить расширенной информацией при регистрации изменений в файлах. В VSS эта возможность известна как ключевые слова, которые позволяют записывать такую полезную информацию, как: имя разработчика, внесшего последние изменения в файл; дату и время последнего изменения; номер внутренней версии файла и много других полезных вещей в сами файлы с исходными текстами. Ключевые слова VSS заключены между двумя символами $. Например, для записи имени автора данного исходного файла в этот файл используется ключевое слово $Author $. При регистрации изменений ключевое слово SAuthor $ будет заменено именем пользователя VSS, который последним внес изменения в файл. Как правило, вы вставляете эти ключевые слова в комментарии, что не затрагивает ваш код. Хорошее место для размещения этих комментариев — заголовок вашего файла со сценарием. Ниже приведен пример часто используемого мной блока комментария на Transact-SQL. Листинг 4.1. Пример блока комментариев Object: sp_usage Description: Provides usage Information for stored procedures and descriptions of other types of objects Usage: sp_usage @objectname='ObjectName'. @desc='Description of object' [. @pa rameters='pa rami,pa ram2...'] [, @example='Example of usage'] [. @workfile='File name of script'] [, @author='Object author'] [, @email='Author email'] [. @version='Version number or info'] [, @revision='Revision number or info'] [. @datecreated='Date created'] [. @datelastchanged='Date last changed'] Returns: (None) $Workfile: sp_usage.sql $ SAuthor: Khen $. Email: khen@khen.com $Revision: 7 $ Example: sp_usage @objectname='sp_who'. @desc='Returns a list of currently running jobs', @parameters=[@loginname] Created: 1992-04-03. SModtime: 4/07/00 8:38p $. */ Обратите внимание, что VSS вставил текст в каждую метку ключевого слова. В случае $Workf i le $ VSS вставил sp_usage. sql. В случае $Author $ было вставлено khen.
112 Глава 4. Управление исходным кодом ВНИМАНИЕ Ключевые слова VSS чувствительны к регистру. Если вы используете VSS и решили вставлять ключевые слова в ваши исходные файлы на Transact-SQL, убедитесь, что вы их вводите в требуемом регистре. Если вы использовали ключевые слова и заметили, что некоторые из них не были корректно восприняты VSS, проверьте регистр. Параметр использования ключевых слов VSS включается с помощью программы VSS Administrator для каждого типа файлов по его расширению. Чтобы включить использование для отдельного типа файлов, перейдите на вкладку General в диалоге Tools ► Options программы VSS Administrator. В окне ввода Expand keywords in files of type введите маску файлов, в которых будут использоваться ключевые слова (например, *. SQL). В табл. 4.1 приведен список ключевых слов VSS и их значения. Таблица 4.1. Ключевые слова VSS их значение Ключевые слова Значения, подставляемые VSS $Author: $ Имя пользователя, который последним изменил файл $Modtime: $ Дата и время последнего изменения $Revision: $ Номер внутренней версии VSS $Workfile: $ Имя файла $Archive: $ Имя архива VSS $Date: $ Дата и время последней регистрации изменений $Header: $ Комбинация меток $Logfile: $, $Revision: $, $Date: $ и $Author: $ $History: $ История файла в формате VSS $JustDate: $ Дата последней регистрации изменений $Log: $ История файла в формате RCS $Logfile: $ Дубликат метки $Archive: $ $NoKeywords: $ Выключить использование ключевых слов Не используйте шифрование При распространении своих приложений на основе SQL Server пользователям и другим третьим сторонам вы, возможно, можете поддаться искушению зашифровать программный код ваших хранимых процедур, функций и других подобных объектов, предполагая, что это защитит ваш код от любопытных глаз, а также от несанкционированного изменения вашего кода. Хочу вас предостеречь: пока вы не имеете дело с хищением конфиденциальной или частной информации, я не советую вам шифровать объекты. Исходя из моего опыта, шифрование объектов SQL Server обычно приносит больше проблем, нежели пользы. У шифрования исходных текстов объектов SQL Server существует целый ряд отрицательных сторон. Обсудим некоторые из них. Первое: невозможно создать сценарий шифрованных объектов даже с помощью Enterprise Manager. To есть, если вы однажды зашифровали процедуру или функцию, вы не сможете получить ее исходный текст из SQL Server. Хорошо известные, но недокументированные методы дешифрации зашифрованных объектов, которые существовали в ранних версиях SQL Server, больше не ра-
Удачные решения 113 ботают, а другие методы, которые могут быть использованы, — не поддерживаются Microsoft. Ко всему прочему, если вы попытаетесь создать сценарий зашифрованного объекта с помощью Enterprise Manager, используя настройки по умолчанию, ваш сценарий будет содержать для каждого объекта вместо CREATE команду DROP. Все, что вы увидите в действительности, — «полезный» комментарий, информирующий вас о том, что невозможно создать сценарий зашифрованного объекта (притом, что в действительности создается сценарий, уничтожающий объект). Если вы запустите этот сценарий, ваш объект будет потерян. Он не будет создан, а будет удален. Второе: зашифрованные объекты не могут быть опубликованы как часть репликации SQL Server. При использовании механизма репликации для синхронизации нескольких серверов у ваших клиентов они окажутся в сложном положении, если ваш код будет зашифрован. Третье: вы не сможете проверить версию зашифрованного объекта. Поскольку заказчики могут восстановить резервную копию, которая может заменить новою версию вашего кода более старой, очень удобно иметь возможность проверки версии кода на сервере заказчика. Если ваш код зашифрован, вам будет непросто это сделать. В противном случае, если вы включили информацию о версии в исходный код, вы сможете легко определить точный номер версии объекта, используемого заказчиком. В листинге 4.2 показана хранимая процедура, которую вы можете использовать для отображения информации о версии объектов вашего SQL Server. В ней в основном производится поиск в таблице syscomments ключевых слов VSS и генерируется отчет по объектам, содержащим эти ключевые слова. Запуск этой процедуры может дать вам общее представление о данных версии для всего кода Transact- SQL в вашей базе данных. Листинг 4.2. Процедура для вывода информации о версии VSS хранимых процедур USE master GO IF OBJECTJDCdbo.sp_GGShowVersion') IS NOT NULL DROP PROC dbo.sp_GGShowVersion GO CREATE PROC dbo.sp_GGShowVersion @Mask varcharC0)=T . @0bjType varcharB)=X /* GGVersion: 2.0.1 Object: sp_GGShowVersion Description: Shows version, revision and other info for procedures, views, triggers, and functions Usage: sp_GGShowVersion @Mask, PObjType -- @Mask is an object name mask (supports wildcards) indicating which objects to list @0bjType is an object type mask (supports wildcards) indicating which object types to list Supported object types include: P Procedures V Views TR Triggers FN Functions Returns: (none) продолжение #
114 Глава 4. Управление исходным кодом Листинг 4.2 {продолжение) SWorkfile: sp_ggshowversion.SQL $ $Author: Khen $. Email: khen@khen.com $Revision: 1 $ Example: sp_GGShowVersion Created: 2000-04-03. SModtime: 4/29/00 2:49p $. */ AS DECLARE @GGVersion varcharC0), @Revision varcharC0). @author varcharC0). @Date varcharC0), @Modtime varcharC0) SELECT @GGVersion='GGVersion: ' .(<>Revision='$' + 'Revision: '.@Date='$'+'Date: @Modtime='$'+'Modtime: '.@Author-'$'+'Author: ' SELECT DISTINCT 0bject=SUBSTRING(o.name.1,30), Type=CASE o.Type WHEN 'P' THEN 'Procedure' WHEN 'V THEN 'View' WHEN 'TR' THEN 'Trigger' WHEN 'FN' THEN 'Function' ELSE o.Type END, Version=CASE WHEN CHARINDEX(@GGVersion,c.text)<>0 THEN SUBSTRING(LTRIM(SUBSTRING(c.text,CHARINDEX(CGGVersion,c,text)+LEN((aGGVersio n),10)),l,ISNULL(NULLIF(CHARINDEX(CHARA3),LTRIM(SUBSTRING(c.text,CHARINDEX (PGGVersion,c.text)+LEN(@GGVersion).10)))-1.-1),1)) ELSE NULL END, Revision=C0NVERT(int, CASE WHEN CHARINDEX((aRevision,c.text)<>0 THEN SUBSTRING(LTRIM(SUBSTRING(c.text,CHARINDEX(@Revis1on,c.text)+LEN(PRevision) .10)) ,1,ISNULL(NULLIF(CHARINDEXC \LTRIM(SUBSTRING(c.text.CHARINDEX((aRevision,c.text)+LEN(CRevision).10)))- 1,-1),D) ELSE '0' END), Created=o.crdate. 0wner=SUBSTRING(USER_NAME(uid),l,10), 'Last Modified By' = SUBSTRING(LTRIM(SUBSTRING(c.text.CHARINDEX(CAuthor,c.text)+LEN((aAuthor),10) ),1,ISNULL(NULLIF(CHARINDEX(' $'.LTRIM(SUBSTRING(c.text.CHARINDEX(@Author.c.text)+LEN(@Author).10)))-l.- D.D), 'Last Checked In'=CASE WHEN CHARINDEX(@Date,c.text)<>0 THEN SUBSTRING(LTRIM(SUBSTRING(c.text,CHARINDEX(@Date,c.text)+LEN(@Date),15)),1, ISNULL(NULLIF(CHARINDEXC $',LTRIM(SUBSTRING(c.text,CHARINDEX(@Date,с.text)+LEN(@Date),20)))-1,- 1),D) ELSE NULL END,
Контроль версий из Query Analyzer 115 'Last . . ■ Modified'=SUBSTRING(LTRIM(SUBSTRING(c.text,CHARINDEX(CModtime,c.text)+LEN(@ Modtime),20)).1.ISNULL(NULLIF(CHARINDEX(' $,.LTRIM(SUBSTRING(c.text,CHARINDEX((aModtime,c.text)+LEN((aModtime),20)))- 1,-1), D) FROM dbo.syscomments с RIGHT OUTER JOIN dbo.sysobjects о ON c.id=o.id WHERE o.name LIKE @Mask AND (o.type LIKE @0bjType AND o.TYPE in ('P','V,'FN','TR')) AND (c.text LIKE X+@Revision+T OR c.text IS NULL) AND (c.colid=(SELECT MIN(cl.colid) FROM syscomments cl WHERE cl.id=c.id) OR c.text IS NULL) ORDER BY Object GO GRANT ALL ON dbo.sp_GGShowversion TO public GO EXEC dbo.sp_GGShowVersion (Результаты сокращены) Object Type Version Revision Created sp_created Procedure NULL 2 2000-04-OB 00:19:51.680 sp_GGShowVersion Procedure 2.0.1 1 2000-04-29 15:30:56.197 spjiexstring Procedure NULL 1 2000-04-OB 15:12:21.610 sp_object_script_commentsProcedure NULL 1 2000-04-29 12:59:08.250 sp_usage Procedure NULL 6 2000-04-07 20:37:54.930 Эта процедура предоставляет информацию о ключевых словах VSS, которые я использую чаще всего, но может быть легко модифицирована для поиска информации о любом ключевом слове. Обратите внимание на пользовательскую метку GGVers i on. Вы можете использовать эту метку для связи исходного текста на Trans- act-SQL с версией вашего приложения. Для форматирования GGVers i on я использовал традиционное представление поля VERS ION INFO (информация о продукте в Windows), которое состоит из четырех частей — четвертая часть берется из значения метки VSS $Rev i s i on $. Контроль версий из Query Analyzer Хотя сам по себе Query Analyzer не обладает встроенной поддержкой для управления версиями, вы можете использовать утилиты командной строки в соединении с возможностью Query Analyzer запускать средства, определяемые пользователем, для добавления в среду разработки возможности контроля версий. Если ваша система контроля версий (как и большинство систем) включает в себя утилиту командной строки для выполнения задач по управлению исходными текстами, вы можете внедрить эту утилиту в меню Tools Query Analyzer. Более того, поскольку Query Analyzer обеспечивает специальные маркеры среды выполнения, которые позволяют передавать имя текущего файла (и многие другие важные вещи) во внешние программы, вы, используя это, можете связать со средой разработки ваше средство контроля версий. Как я уже упоминал, я использую VSS, поэтому нижеописанные шаги показывают, как использовать утилиту командной строки VSS SS.EXE из Query Analyzer.
116 Глава 4. Управление исходным кодом Это не значит, что вы не можете из Query Analyzer использовать другую систему контроля версий. Напротив, поскольку большинство систем контроля версий работают почти одинаково, шаги, необходимые для доступа к утилитам командной строки, схожи, независимо от используемого инструментария. Для добавления внешних средств к меню Tools Query Analyzer в главном меню выберите Tools ► Customize и затем в окне Customize выберите вкладку Tools. В элементе ввода Menu contents в центре страницы перечислены инструменты, установленные в настоящее время. Для добавления нового элемента сделайте двойной щелчок на пустой строке списка. Таблица 4.2 показывает элементы в моем списке, связанные с использованием VSS. Таблица 4.2. Элементы меню, связанные с использованием VSS Название Команда Аргументы Начальный директорий Set Project Path ss.exe cp $/ggspxml/ch04/code Set Working folder ss.exe workfold $(FileDir) Add Current File ss.exe add $(FilePath) $(FileDir) Check Out Current File ss.exe checkout $(FileName) $(FileExt) -C- $(FileDir) Check In Current File ss.exe checkin $(FileName) $(FileExt) $(FileDir) Undo Check Out of Current File ss.exe undocheckout $(FileName) $(FileExt) $(FileDir) Diff Current File Diff.bat $(FileName)$(FileExt) $(FileDir) После настройки Query Analyzer вам необходимо убедиться в следующем. Во- первых, проверьте, что пункты Set Project Path и Set Working Folder используют настройки вашей папки проекта и рабочей папки соответственно. Значения, перечисленные в табл. 4.2, отображают мои настройки на текущий момент. Во- вторых, имейте в виду, что большинство команд VSS при вызове предлагают ввести комментарий. Я блокировал ввод комментария для команды Check Out Current File, оставив эту возможность для других. Добавьте -С- к любой команде, для которой вы хотите заблокировать приглашение к вводу комментария. В-третьих, установите переменную окружения SSDIR, указывающую на папку, содержащую файл SRCSAFE. INI, если она отличается от пути поиска VSS, заданного по умолчанию. Обратите внимание на командный файл Dl FF. ВАТ, который используется для проверки файла на различия версий. DI FF. ВАТ содержит всего две строчки: ss diff %1 pause Первая строка отображает различия между локальной версией данного файла и версией, хранящейся в базе данных VSS. Команда pause позволяет увидеть различия до возврата в Query Analyzer. Мы вызываем DIFF.BAT вместо того, чтобы непосредственно вызвать ss d i f f, поскольку мы хотим увидеть результаты работы команды до возврата в среду Query Analyzer. Специальные лексемы Как вы видели, Query Analyzer поддерживает несколько специальных маркеров (обозначаемых символом $), которые вы можете использовать для переда-
Автоматизация создания сценариев при помощи контроля версий 117 чи информации времени исполнения внешним программам. Для получения списка доступных лексем щелкните на кнопке со стрелкой справа от элементов Arguments и Initial directory. В табл. 4.3 перечислены используемые мной лексемы. Таблица 4.3. Лексемы времени исполнения Query Analyzer Лексема Значение $(FilePath) Полный путь к текущему файлу $(FileName) Имя текущего файла (без расширения) $(FileExt) Расширение (включая точку) текущего файла $(FileDir) Путь к текущему файлу (без имени файла и расширения) ВНИМАНИЕ Возможно, вы заметили, что Query Analyzer при редактировании добавляет звездочку к заголовку окна текущего файла. Поскольку Query Analyzer получает имя файла из заголовка окна, к значениям лексем $(FileExt) и $(FilePath), передаваемым во внешние программы, ошибочно добавляется звездочка. Таким образом, если вы изменили файл, во внешние программы будет передано неверное значение. Чтобы обойти это препятствие, просто сохраните файл, прежде чем вызвать команду. Так или иначе, вы должны убедиться, что система имеет дело с последней версией файла. На компакт-диске, прилагаемом к этой книге, вы можете найти файл с расширением REG, содержащий элементы реестра, необходимые для добавления этих лексем к вашей установке Query Analyzer. Для добавления настроек, приведенных в табл. 4.3 к вашей установке Query Analyzer, запустите этот . REG- файл. - Автоматизация создания сценариев при помощи контроля версий Еще одна интересная возможность систем контроля версий — это способность автоматизировать файловые операции с помощью консольных приложений и API. В сочетании с компиляторами командной строки и средствами по работе со сценариями эти возможности позволяют вам легко справляться с такими задачами, как: извлечение самой последней версии проекта из базы данных исходного кода, компиляция и ее автоматический запуск. Поскольку код хранится в центральной базе данных и система контроля версий знает, какая версия является актуальной, простое API обеспечит вам все необходимое, чтобы построить автоматизированный процесс, например, для запуска ежедневных тестов или еженедельных сборок. В VSS этот API в действительности представляет собой СОМ-интерфейс, доступ к которому можно получить из любой среды разработки, поддерживающей автоматизацию (например, Visual Basic или Delphi). Используя интерфейс автоматизации VSS, вы можете делать, по существу, все то же самое, что и Проводник VSS, поскольку он сам использует тот же интерфейс. Посредством довольно про-
118 Глава 4. Управление исходным кодом стого программного кода вы можете программно оперировать версиями проекта VSS, помечая файлы и регистрируя изменения, извлекая определенные версии проекта и т. д. GGSQLBuilder Примером того, насколько это просто и вместе с тем эффективно, является утилита, написанная на Delphi, которая ищет в проекте VSS сценарии SQL и извлекает их в два файла со сценариями T-SQL, которые затем можно запустить на выполнение. GGSQLBu i I der находит в проекте каждый файл со сценарием SQL, затем просматривает все версии этого файла и находит последний, помеченный меткой (это предполагает, что перед выпуском вы помечаете версию — обычная практика), или первую версию файла, если не найдено ни одной метки. Как только найдена требуемая версия каждого файла, он добавляется в общий сценарий для построения всей базы данных. GGSQLBui Icier может быть использован в интерактивном режиме, а также вызван из командной строки. При интерактивном использовании он функционирует . как мастер: он предлагает ввести вам данные, необходимые для нахождения файлов со сценариями создания объектов, и выбрать имя для выходных файлов. В результате он находит и извлекает ваши сценарии. Используя интерфейс командной строки GGSQLBu i I der, можно полностью автоматизировать регулярную сборку общего сценария. Принцип работы GGSQLBuilder Я уже упоминал, что GGSQLBu i I der извлекает ваши сценарии в два файла сценариев T-SQL. Почему в два? Дело в том, что GGSQLBu i I de г разработан для формирования сценариев клиентских приложений, которые построены на основе SQL Server. Он специально разработан для помощи в создании сценариев для установки и обновления таких приложений. Обычно клиентские приложения на основе SQL Server вынуждены использовать две базы данных: одна или несколько пользовательских баз данных и база данных master. Как вы можете предположить, пользовательская база данных обычно хранит данные конечного пользователя и T-SQL-код объектов, специфичных для данного приложения, таких как хранимые процедуры и представления. С другой стороны, в базе данных roaster обычно располагаются хранимые процедуры и функции либо системного назначения, либо использующиеся в нескольких базах данных. Как правило, в базе данных master не должно храниться ничего, чтобы не удовлетворяло этому критерию. GGSQLBu i I de r задуман таким образом, чтобы сделать извлечение сценариев этих двух видов безболезненным. Как только эти два файла сформированы, вы можете запустить на исполнение получившиеся сценарии на всех необходимых пользовательских базах данных. Например, если вы намерены использовать итоговый сценарий как часть обновления программного обеспечения, ваш пакет обновления может вызвать утилиту 0SQL, используя флаг -d для определения базы данных, в которой должен быть исполнен сценарий. Вы легко можете автоматизировать распространение сценария на несколько баз данных конечных пользователей, последовательно вызывая 0SQL из командной строки.
Автоматизация создания сценариев при помощи контроля версий 119 Что касается сценария для базы данных master, в процессе обновления вашего программного обеспечения вы, как правило, должны запустить его однократно. Он установит или обновит системные объекты в базе данных master, которые могут быть использованы в системе повсюду, в любой базе данных. Преимущества средств формирования сценариев Чем, в первую очередь, вас могло бы привлечь средство GGSQLBu i I der и подобные ему? Почему бы просто не выгрузить отдельный сценарий для каждого объекта или не использовать Enterprise Manager или что-либо подобное для формирования единого файла сценария? Первое: развертывание сотен или даже тысяч отдельных сценариев у конечных пользователей может оказаться проблематичным. Каждый дополнительно распространяемый файл увеличивает вероятность, что установка или обновление закончатся неудачно. Второе: если при каждом обновлении программного обеспечения вы не перестраиваете данные пользователей, сценарий, формируемый Enterprise Manager, не подходит для обновлений. Вы, вероятно, не захотели бы включать команды DROP/CREATE TABLE в ваш сценарий для обновления существующей базы данных. Как GGSQLBuilder находит и упорядочивает сценарии Как GGSQLBu i I der узнает, что считать сценарием SQL и в какой файл необходимо его извлечь? Он работает исходя из предположений, что ваш проект VSS организован таким образом, что: ■ сценарии проекта располагаются в папках подчиненных проектов, имена которых перечислены в табл. 4.4; ■ все сценарии, которые должны быть выполнены в базе данных master, расположены в подчиненном проекте с именем MasterDB; ■ он находит в вашем дереве проектов VSS сценарии SQL по расширению файлов. По умолчанию файлы с расширениями, перечисленными в табл. 4.5, считаются сценариями T-SQL. Таблица 4.4. Папки VSS, распознаваемые GGSQLBuilder Имя папки Тип MasterDB Сценарии для выполнения в базе данных master Defaults Объекты умолчаний (например, CREATE DEFAULT) Rules Объекты правил (например, CREATE RULE) Tables Объекты таблиц (например, CREATE TABLE) TableAlters Модификация таблиц (например, ALTER TABLE) Triggers Объекты триггеров (например, CREATE TRIGGER) UDFs Объекты пользовательских функций (например, CREATE FUNCTION) Views Объекты представлений (например, CREATE VIEW) StoredProcs Объекты хранимых процедур (например, CREATE PROC или ALTER PROC)
120 Глава 4. Управление исходным кодом Обратите внимание, что папки в табл. 4.4 перечислены в порядке зависимостей объектов. Вероятно, что существование объектов в базе данных masters будет необходимо до создания пользовательских объектов, объекты значений по умолчанию должны быть созданы прежде таблиц, которые их используют, таблицы — прежде выполнения сценариев, которые их изменяют и т. д. Пользуясь приведенным в табл. 4.4 порядком папок, GGSQLBu i I de г запишет найденные в вашей базе данных VSS сценарии в итоговые сценарии. В табл. 4.5 перечислены расширения файлов, распознаваемые GGSQLBu i I der. . < Таблица 4.5. Расширения файлов, распознаваемые GGSQLBuilder по умолчанию Расширение SQL PRC TRG UDF TAB VIW DEF RUL UDT FTX Тип файла Сценарий SQL общего назначения * Хранимые процедуры Триггеры Пользовательские функции Таблицы Представления Умолчания Правила Пользовательские типы данных Полнотекстовые индексы Все это гарантирует, что ваши сценарии будут выполняться в правильном порядке и должным образом сохранится взаимосвязь объектов. Лучший путь изучения GGSQLBu i I der — запустить его в интерактивном режиме. Если у вас есть база данных VSS, содержащая некоторое количество включенных в нее сценариев SQL, не задумываясь, запускайте GGSQLBu i I der. Пусть он попробует найти эти сценарии. Если иерархия проекта велика, вы заметите некоторую задержку: GGSQLBu i I der будет исследовать каждую версию каждого файла в дереве проекта. Если вы сгруппировали ваши проекты со сценариями вместе (согласно табл. 4.4) в единый родительский проект, вы можете настроить GGSQLBu i I der на поиск, начиная с этого проекта. Как только GGSQLBu i I der найдет ваши сценарии, позвольте ему продемонстрировать свою работу и построить два итоговых файла со сценариями. ВНИМАНИЕ Поскольку GGSQLBuilder распознает сценарии SQL, основываясь только лишь на расширениях файлов, возможно, что созданные им итоговые сценарии будут содержать нежелательные команды создания или уничтожения объектов. Иными словами, если вы зарегистрировали в VSS сценарий по созданию таблицы, вы, в конце концов, можете получить сценарий, содержащий не только команду CREATE TABLE, но также и команду DROP TABLE, если она содержалась в исходном сценарии. Мораль заключается в следующем: используйте дерево проектов GGSQLBuilder для тщательного исследования содержащихся в нем сценариев SQL и снимите пометку с тех, которые не должны появиться в итоговом сценарии. Поэкспериментируйте с GGSQLBu i I der и посмотрите, насколько он мог бы быть полезен в разработке обновлений для приложений, основанных на использовании SQL Server, автоматизации сборок сценариев и тестирования. Однако имейте в виду одну вещь: средства подобные GGSQLBu i I der будут бесполезны для вас, пока ваш T-SQL-код не будет храниться в системах контроля версий.
Итоги 121 Итоги В этой главе вы узнали: ■ о системах контроля версий и преимуществах их использования для управления кодом T-SQL; ■ о T-SQL как о настоящем коде; о коде, нуждающемся в управлении (как и любой тип программного кода); ■ о некоторых приемах, которые делают использование систем контроля версий для Transact-SQL максимально полезным; ■ об использовании двух новых программ, которые работают совместно с VSS; ■ о необходимости управлять своим программным кодом и использовать системы контроля версий независимо от исходного инструментария и языка программирования.
5 Проектирование баз данных Разница между знаниями и умениями состоит в том, что знания приходят и уходят, а умения остаются. Умения остаются, даже когда знания исчезают по прошествии времени или из-за непрочности человеческой памяти. X. В. Кентон В этой главе описывается процесс проектирования баз данных с практической точки зрения. Эта глава в основном для тех, у кого немного опыта проектирования баз данных. Если вы уже знакомы с основами проектирования реляционных баз данных, вы можете пропустить данную главу. Общий подход Мой подход к проектированию приложений баз данных может отличаться от описанных в других книгах. Я не буду вдаваться в подробности современной теории баз данных и не собираюсь детально обсуждать фундаментальный труд Др. Кодда Реляционная модель информации для больших банков данных общего доступа {A Relational Model of Data for Large Shared Data Banks, Dr. Codd) или математические процессы, скрывающиеся за нормализацией баз данных в данной книге. Есть много работ, в которых рассматриваются эти темы. Я прагматик и поэтому уделю больше внимание практике — тому, что поможет вам в проектировании и построении баз данных. Мы, конечно, затронем вопросы теории, а затем рассмотрим, как применить теоретические знания для создания приложений. Одна из самых больших трудностей при написании технической литературы — это необходимость гармонично сочетать теоретическую и практическую информацию. Если в книге делается больший уклон в сторону практики, то она становится незаменимым справочным руководством. Такая литература пытается ответить на вопрос «Как?», не задавая вопроса «Почему?». С другой стороны, книга, слишком подробно рассматривающая абстрактные понятия, может оказаться бесполезной с практической точки зрения. Ведь обычно компьютерную литературу покупают, чтобы научиться что-либо делать на практике, а книги, которые не могут этому научить, не нужны среднему специалисту.
Пример проекта 123 Золотая середина — как раз та цель, которой я попытался достичь в этой книге и в этой главе, в частности. Я пишу о том, как использовать различные инструменты при проектировании баз данных, и также стараюсь дать вам теоретическую основу, опираясь на которую вы сможете применять имеющуюся практическую информацию. Я надеюсь, что вы найдете здесь ответы как на вопрос «Почему?», так и на вопрос «Как?». Инструменты моделирования Я специально не заостряю внимание на конкретном инструменте для построения моделей, которые мы будем исследовать. Не существует самого лучшего инструмента. Выбор зависит от того, что вам необходимо. Visio — хорошее средство моделирования, особенно если вы используете Microsoft Office. Может быть, для ваших нужд хорошо подойдет AppViewer от CAST Software. Возможно, вы выберете Sybase's PowerDesigner, ERwin или ER/Studio. Оба эти продукта хороши по-своему. Если вам необходима кросс-платформенная система, вы можете использовать Silverun от Magna solutions. Какое бы из этих средств вы ни выбрали, вы сможете прекрасно работать с примерами, приведенными в этой главе. Важен не столько инструмент, сколько само понятие. А большинство лидирующих инструментов моделирования данных поддерживают один и тот же основной набор функциональных возможностей. Enterprise Manager SQL Server нельзя отнести к лидирующим инструментам моделирования данных. Его средство Database Diagram является довольно упрощенным инструментом, который обеспечивает только самые элементарные средства проектирования физической модели. Как вы скоро увидите, проектирование базы данных не начинается с проектирования физической модели — на ней оно заканчивается. А начинается оно с понимания требований, предъявляемых бизнесом, так как при проектировании бизнес-процессов необходимо учитывать эти требования. После того как фаза проектирования бизнес-процессов закончена, можно выделить сущности и установить взаимосвязи между ними. Затем можно приступать к проектированию логической модели. И только после того, как вы завершили эти этапы, можно приступать к проектированию физической модели. Никак не раньше. Я хочу, чтобы вы поняли, что проектирование базы данных — это не физический процесс, а мыслительный. Физическая модель базы данных основывается на бизнес-процессах, взаимосвязях между сущностями и логической моделью. Они определяют, какую форму, в конечном счете, приобретет база данных. Проектировщики баз данных, пренебрегающие этими фундаментальными аспектами, работают на свой страх и риск, и часто их деятельность завершается созданием базы данных, не отвечающей требованиям бизнеса, или оказывается сложной для расширения и поддержки. Главное — это понять основы, и все встанет на свои места. Пример проекта В этой главе мы создадим базу данных для вымышленной системы управления арендованной собственностью под названием RENTMAN. По своему опыту, я знаю,
124 Глава 5. Проектирование баз данных что узнать больше можно из практики, поэтому я создал проект, который мы сможем вместе проработать. Как я уже сказал, если у вас последняя версия полнофункционального инструмента моделирования данных, у вас не должно возникнуть проблем с работой над примером. Пример с RENTMAN, который мы будем конструировать, является на самом деле довольно простым, и любой приличный инструмент моделирования должен справиться с ним достаточно легко. Пять процессов Процесс разработки приложения баз данных можно разбить на пять шагов или этапов. Эти этапы образуют стандартную последовательность, которой необходимо следовать при построении приложений баз данных. Считайте, что это догмы и вам лучше следовать им всегда. Далее перечислены пять основных процессов, необходимых для создания приложения базы данных. 1. Определение предназначения и функций приложения. * 2. Проектирование основы базы данных и процессов приложения, необходимых для осуществления этих функций. 3. Преобразование проекта в приложение путем создания необходимой базы данных и объектов программы. 4. Тестирование приложения на соответствие заранее определенным функциям и предназначению. 5. Установка приложения для непосредственного использования. Эти пять процессов можно приравнять к пяти стадиям: 1) анализ; 2) проектирование; 3) конструирование; 4) тестирование; 5) внедрение. ВНИМАНИЕ Исторически сложилось, что раньше стадию конструирования называли стадией кодирования. Однако в наш век визуальных средств разработки термин кодирование не очень применим, поэтому я заменил его конструированием. Этот термин одинаково хорошо описывает кодирование, визуальную разработку или генерацию приложений. В этой главе для обозначения пяти процессов я буду использовать названия пяти этапов. Они не только более сжаты и точны, чем формальные определения пяти процессов, они также являются терминами, используемыми большинством разработчиков программного обеспечения. Благодаря им становится очевидным тот факт, что разработка программного обеспечения базы данных — это точно такой же процесс, как и разработка любого другого вида программного обеспечения: для достижения желаемых результатов нужен системный подход.
Пять стадий в деталях 125 Пять стадий в деталях Когда вы строите что-либо — программный продукт, дом или памятник, — вы проходите несколько фаз или шагов для того, чтобы получить законченный продукт. То же самое происходит, когда вы разрабатываете приложение. Эти шаги не стоит пытаться заучивать, так как они являются полностью интуитивными и опираются на логику, а не на какой-то свод правил, которому нужно следовать. Обычно вы интуитивно проходите пять процессов, выделенных мною ранее при проектировании баз данных и построении приложений. Что же касается непосредственной работы по конструированию приложений баз данных, то вы можете сконцентрироваться только на первых трех процессах из пяти. Именно в течение первых трех стадий происходит разработка приложения, и основные проблемы возникают в этих трех стадиях. Это, однако, не означает, что тестирование и внедрение должны быть оттеснены в конец процесса разработки. Напротив, они должны сопровождать весь процесс. Но формально тестирование проекта (например, предварительный выпуск или бета-тестирование) обычно происходит, когда программное обеспечение почти готово, а внедрение (производственный выпуск или RTM) — уже после того, как программное обеспечение считается законченным. Анализ Первая стадия разработки приложения — это стадия анализа. Здесь вы тщательно продумываете, что должно выполнять ваше приложение. Вы начинаете с общей постановки цели, затем уточняете цель перечислением определенных функций, которые приложение должно выполнять или должно позволять пользователю выполнять для достижения поставленной им задачи. Именно в течение стадии анализа начинается процесс моделирования, который состоит из описания основополагающих бизнес-процессов, ресурсов и потоков данных, необходимых для выполнения приложением поставленной цели. Проектирование Вторая стадия — это стадия проектирования. Здесь анализ, выполненный вами в первой стадии, преобразуется в логический проект. Вы переводите бизнес-процессы, смоделированные в течение вашего анализа, в логическое приложение и элементы проекта базы данных. Этот логический проект описывает то, что вы строите, специальными терминами. Фокус с того, что будет делать приложение, смещается на то, как оно будет это делать. Здесь вы определяете компоненты приложения и базы данных, необходимые для претворения в жизнь моделей бизнес-процессов, намеченных в стадии анализа. Один из способов сделать это — логическое моделирование данных и E-R-диаграммы (диаграммы «сущность-связь»). Конечный результат — разработка отдельного проекта для каждого слоя вашего приложения. Конструирование Третья стадия разработки приложения базы данных — стадия конструирования. Здесь логический проект, разработанный на стадии проектирования, становится
126 Глава 5. Проектирование баз данных физическим объектом. Это означает, что созданный вами логический проект базы данных будет переведен в реальные объекты базы данных. Также проект приложения, созданный на стадии проектирования, реализуется в виде форм, кода, сервисов, компонентов и других объектов программы. Тестирование и внедрение Последние две из пяти стадий — тестирование и внедрение — относятся главным образом к стадии, следующей за разработкой, поэтому я не буду здесь подробно их описывать. Как я уже говорил, каждая из этих стадий может и должна быть непрерывной по своей природе, но, несмотря на это, они все же являются отдельными стадиями процесса в целом. Часто они так или иначе приводят к возвращению к первым трем стадиям (через обнаруживаемые ошибки или запросы пользователей об улучшении качества), так что я лишь затрону их в нашем обсуждении проекта базы данных. О сложностях разработки баз данных Процесс разработки приложений баз данных простой и интуитивный. Он мало чем отличается от подхода, используемого при построении любого другого типа приложений независимо от того, имеет ли приложение доступ к базам данных. Может показаться, что я сильно упрощаю, но именно так я рассматриваю задачу разработки приложений баз данных. Однако есть проблема, возникающая при применении данного основного принципа при разработке приложений для SQL Server. Она состоит в том, что даже в самом простом приложении для SQL Server можно четко выделить две части: серверную и клиентскую. Кроме того, объекты, созданные на сервере, являются основными для приложения, поэтому предпочтительно и даже необходимо, чтобы они уже существовали до конструирования самого приложения. Приложение не будет работать правильно, если есть проблемы в базе данных. Поэтому работа по конструированию надежного приложения удваивается. Пройти через стадии анализа, проектирования и конструирования должно не только приложение, но и база данных. В дополнение к трудностям создания клиентской и серверной частей существует проблема их взаимодействия друг с другом. А добавление ко всему этому программного обеспечения промежуточного уровня делает процесс еще более «занимательным». Полностью устранить сложности, сопровождающие приложения баз данных, непросто. CASE-инструменты помогают, но все-таки приложения пока должны создаваться разработчиками. Современные приложения баз данных — это достаточно сложные, многослойные части программного обеспечения. Понятно, что их создание требует некоторого опыта. В этой главе вы пройдете через стадии анализа, проектирования и конструирования, касающиеся баз данных. Проект базы данных настолько же важен, как и проект приложения. Если в одном из них имеются ошибки, то приложение не будет работать хорошо. Теория баз данных на практике Вы заметите, что в этой главе проект базы данных и проект приложения базы данных будут рассматриваться как органически связанные. Для меня это очевидно,
Пять стадий в деталях 127 хотя некоторые считают, что это два совершенно разных процесса. С прагматической точки зрения, процесс проектирования базы данных является основополагающим в процессе проектирования приложений, его использующих. Оба, конечно же, влияют друг на друга. Я никогда не разрабатывал базу данных, которая не была предназначена для использования приложением. Вы должны помнить, что база данных — это только средство достижения конечной цели, это ваш способ удовлетворения потребностей заказчика. Точно так же и приложение-клиент функционирует как канал между пользователем и сервером базы данных. Это тоже только инструмент для обслуживания вашего заказчика. И приложение-клиент, и сервер баз данных должны работать вместе, чтобы вырабатывать приемлемые для ваших пользователей решения. В этой главе вы увидите, как проходит процесс проектирования приложений баз данных. Я буду основывать свои рассуждения на теории баз данных, делая акцент на задачу конструирования реальных приложений. Определение предназначения приложения Первый шаг в проектировании любого приложения — это определение его предназначения. Что оно должно делать? Постановка предназначения приложения должна состоять из одного предложения, включающего подлежащее, сказуемое и дополнение к сказуемому. Подлежащим обычно является приложение, например: «Эта система...» или «Система RENTMAN...». Сказуемое описывает, что должно делать приложение, например: «Система будет управлять...» или «Система RENTMAN поможет...». Дополнение указывает, на что направлены действия приложения. Например: «Система будет управлять системой регистрации в летнем лагере» или «Система RENTMAN поможет управлять арендованной собственностью». Постановка предназначения должна быть выражена как можно проще и более кратко. Не тратьте время на витиеватый язык или бесполезные детали. Например, избегайте употребления таких фраз, как «для организации» и «для клиента» (поскольку они подразумеваются). Также старайтесь избегать употребления сложносочиненных предложений и союзов. Сокращение предложения до его простейшей формы поможет вам придерживаться заданного курса при дальнейшем описании функций приложения. Более детально можно разъяснить цель вашего приложения при определении его функций. Когда вы окончательно определитесь с постановкой предназначения приложения, покажите ее потенциальным пользователям и посмотрите, согласны ли они с ней. Не удивляйтесь, если они не поймут вашей краткости, но постарайтесь, чтобы они утвердили вашу постановку предназначения как точную и полную. Хотя в постановке предназначения и не указываются детали приложения, она все же должна быть достаточно полной и отражающей основную цель приложения. Убедите ваших пользователей, что следующим шагом в вашей работе будет определение конкретных функций приложения. Пусть они поделятся своими соображениями о том, какие это должны быть функции. Определение функций приложения Когда постановка предназначения готова, можно определять необходимые функции приложения. Что должно делать приложение, чтобы достичь поставленной
128 Глава 5. Проектирование баз данных вами цели? Постарайтесь по возможности свести все функции к основным задачам. Лучше всего разрабатывать эти объекты по плану. Функции должны более детально разъяснять цель, но не более того. Используйте все ту же структуру из подлежащего, сказуемого и дополнения к сказуемому, которой вы пользовались при написании постановки цели. Например, «Система RENTMAN поможет управлять арендованной собственностью: ■ она будет регистрировать и обслуживать арендованную собственность; ■ она будет следить за работой по обслуживанию арендованной собственности; ■ она будет выдавать информацию о счетах арендатора; ■ она будет выдавать данные по некоторой собственности за предыдущие периоды». Проверьте, все ли основные функции приложения вы раскрыли, но следите, чтобы не было переизбытка деталей. Также проверьте, чтобы поставленные вами задачи не пересекались. Не вносите в список задачи, которые уже раскрыты в другом пункте списка. Проектирование основы базы данных и процессов приложения Возможно, вас удивит, что проектирование баз данных — это не только проектирование. Большую роль в этом процессе играют также моделирование бизнес-процессов, E-R-диаграммы (диаграммы «сущность-связь») и логическое моделирование данных. Ступени от моделирования бизнес-процессов до конструирования объектов базы данных складываются в непрерывный процесс. Вы начинаете с общего обзора бизнес-процессов, составляющих ваше приложение, и постепенно доходите до таких мелких деталей, как определение отдельных столбцов, то есть вы движетесь от общего к частному. Ваше понимание цели постепенно становится все более четким, пока в конце концов оно не материализуется в самом приложении. Путь уточнений обеспечивает превращение представления о том, что должно делать приложение в реальные элементы программы. Сама работа по проектированию баз данных не так сложна, как кажется. Она начинается с процесса моделирования и заканчивается проектированием физических объектов, составляющих базу данных. В этой главе мы сведем процесс проектирования баз данных к трем шагам. 1. Описание бизнес-процессов, необходимых для выполнения требуемых функций приложения. 2. Схематическое изображение отношений между сущностями, которые необходимы для обслуживания этих процессов. 3. Создание логического проекта базы данных, необходимого для воплощения этих бизнес-процессов и связей между сущностями. Рисунок 5.1 иллюстрирует связь между пятью процессами разработки приложения и данными тремя шагами. После постановки предназначения приложения и выделения его основных функций оставшаяся работа по анализу и проектированию сводится к выполнению этих трех шагов. Вы начинаете с моделирования бизнес-процессов, которые соответствуют определенным вами функциям, затем создаете диаграмму отношений между сущностями, поддерживающими эти процессы.
Пять стадий в деталях 129 Завершается все переводом разработанных вами E-R-диаграмм и бизнес-процессов в логическую модель ваших данных. Именно эта логическая схема будет использована для построения вашей базы данных на стадии конструирования. а», £д ;ы1г*и1чииД?г'.1й>^г«¥А &3,,;,&Z'i>U£$-*-<&it,*$ifa?b йч. rtiBfliii>ft'afcii чйыиЬйМЗиЛЦиШчЙ» Ижяаы*уйишь <а.&А-1; -1s f ff' ~ = = "П Определение предназначения и функций приложения 1 Проектирование основы базы данных и процессов приложения, необходимых для достижения поставленных целей 2 Трансформация проекта в приложения создание объектов базы данных и программы 3 Тестирование приложения на соответствие поставленным целям 4 Установка приложения для использования 5 III ы Рис. 5.1. Модель, показывающая различные элементы пяти процессов разработки приложения Переход от моделирования процессов к моделированию логических данных не следует делать вручную. С этим отлично справятся CASE-инструменты. Как вы вскоре увидите, они могут и в дальнейшем значительно облегчить процесс разработки. CASE-инструменты Мне кажется, что нужно начать с того, что CASE-инструменты подходят не для любого проекта. Я уверен, что они никогда не смогут заменить адекватного планирования или мастерства разработчиков программного обеспечения. Являясь компьютерными средствами, CASE-инструменты имеют те же слабости, что и любое другое программное обеспечение — они делают то, что вы говорите им сделать, а это не всегда то, что вы хотите, чтобы они сделали. Я также считаю, что CASE-инструменты, стремящиеся автоматизировать все стороны процесса разработки, зачастую мешают конструированию приложения. Таким образом, они не только не помогают процессу, но и тормозят его, пытаясь автоматизировать то, что не может или не должно быть автоматизировано. Подобно Джорджу Джетсону (George Jetson) со своей компьютеризацией однообразного механического труда, разработчики тратят гораздо больше времени на исправле-
130 Глава 5. Проектирование баз данных ние результатов чрезмерной автоматизации, чем если бы с самого начала весь процесс не был бы так автоматизирован. При использовании любого средства, предназначенного для экономии времени, может наступить момент, когда результаты станут абсолютно противоположными. Когда это произойдет, нужно вернуться к основам и сконцентрироваться на автоматизации, которая помогает, а не мешает. Роль, которую CASE-инструменты должны выполнять в разработке баз данных, — это роль одного из инструментов в наборе инструментов разработчика. Для обращения с инструментами необходимо мастерство. Инструменты не могут думать за вас и не решают, какой подход является лучшим для решения задач. Они просто помогают вам быстрее выполнить задачи, которые вы, возможно, могли бы выполнить с помощью других средств. Такая роль в идеале должна быть отведена в процессе моделирования приложения CASE-инструментам. Неважно, какими инструментами вы пользуетесь для разработки оптимальных приложений баз данных, главное, чтобы у вас были общие представления о проектировании баз данных и понимание функционирования системы управления базами данных. Так как основная часть исследовательской работы, которая должна привести к проекту базы данных, сводится к моделированию объектов реального мира, наряду с другими имеет смысл использовать CASE-инструменты. Сегодняшние CASE- инструменты намного отличаются от своих предшественников. Без сомнения, сейчас намного безопаснее и быстрее использовать CASE-инструменты для облегчения процесса проектирования, чем делать все вручную. Позволяя нам моделировать объекты, которые еще не существуют, CASE-инструменты позволяют выполнять анализ что-если элементов проекта, до того как они станут реальными. Также CASE-инструменты могут облегчить и сам процесс проектирования и даже предупредить о возможных проблемах при выборе каких-либо решений проекта. Конструируя модель того, что мы хотим сделать, мы даем CASE- инструменту ту информацию, которая ему необходима, чтобы помочь нам. Вот некоторые типы моделирования, для которых особенно подходят CASE-инструменты. Моделирование бизнес-процессов. Приложения состоят из взаимосвязанных процессов, выполняющих необходимые для приложения функции. Моделирование бизнес-процессов включает формальное схематическое изображение этих процессов и их взаимосвязей. Существуют CASE-инструменты, специально созданные для поддержки моделирования процессов. Один из таких инструментов — BPwin компании Computer Associates, другой — Visio компании Microsoft. Моделирование «сущность-связь». E-R-диаграммы считаются многими разработчиками обязательными. Они обеспечивают отделение логического представления данных от их физического воплощения. E-R-диаграммы помогают взглянуть на элементы данных с точки зрения объектов реального мира, которые они представляют. Существует много CASE-инструментов, посвященных моделированию «сущность-связь». Например, Erwin от Computer Associates и ER/Studio от Embarcadero. Функции (и предназначение) приложения Моделирование бизнес-процессов Моделирование сущность-связь Логическое моделирование данных Физическое конструирование] базы данных. Рис. 5.2. CASE-инструменты помогают превратить идеи в физические объекты баз данных и приложений
Моделирование бизнес-процессов 131 Реляционное моделирование данных. Несмотря на популярность E-R-моде- лирования, оно представляет лишь часть процесса реляционного моделирования данных. E-R-диаграммы — один из способов выражения отношений базы данных, но существует и множество других. Большое число CASE-инструментов выходят за рамки E-R-диаграмм и выполняют более объемную задачу проектирования всей логической схемы базы данных. Один из таких инструментов — PowerDesigner от Sybase, другой — AppBuilder от CAST Software. В этой главе я покажу вам, как использовать CASE-инструменты для моделирования реальных приложений и как трансформировать эти модели в базы данных и объекты приложения. Как показано на рис. 5.2, физические объекты баз данных возникают из логических моделей данных. Логические модели, в свою очередь, порождаются E-R-диаграммами, а E-R-диаграммы выводятся из моделей бизнес- процессов, являющихся воплощением предназначения приложения и его ключевых функций. Процесс от определения предназначения приложения и его основных функций до его конструирования представляет эволюцию нашего понимания того, что должно делать приложение. Моделирование бизнес-процессов Вернемся непосредственно к моделированию. После того как вы определили предназначение и функции вашего приложения, вы можете начинать моделирование бизнес-процессов, которые являются определяющими для приложения. Я проведу вас шаг за шагом через моделирование бизнес-процессов для приложений баз данных. Когда моделирование процессов завершено, вы продолжаете работу, создавая E-R-диаграммы, затем переходите к логическому моделированию данных. После завершения стадий анализа и проектирования вы реализуете ваш проект базы данных, создавая объекты, из которых она состоит. На этой стадии у вас готова основа базы данных, и вы можете перейти к разработке приложения и/или элементов промежуточного программного обеспечения. Модели бизнес-процессов в общем виде представляют отношения между четырьмя основными элементами моделирования: процессами, внешними сущностями, хранилищами и потоками данных. В дополнение к этому квалификаторы, ресурсы и структуры данных далее определяют отношения между этими элементами. Таблица 5.1 кратко раскрывает каждый элемент модели и связь с другими элементами. Итак, теперь вы знакомы с основными терминами моделирования, и можно начинать. СОВЕТ Естественно, вы должны определиться со стилем нотации, прежде чем начать моделирование. Очевидный выбор — UML {унифицированный язык моделирования), но есть много других языков. Для моделирования бизнес-процессов популярными являются Gane-Sarson, Merise, Yourdon-DeMarco и Ward-Mellor. Некоторые CASE-инструменты поддерживают несколько нотаций моделирования; некоторые только одну. Есть такие инструменты, которые даже позволяют создать вашу собственную нотацию или синтезировать новую из уже существующих. В любом случае, вам следует выбрать нотацию и придерживаться ее в течение всего процесса моделирования. Лично я предпочитаю нотацию Ward-Mellor.
132 Глава 5. Проектирование баз данных Таблица 5.1. Элементы моделирования бизнес-процессов Элемент моделирования Определение Процесс (Process) Внешняя сущность (External entity) Хранилище (Store) Поток (Flow) Ресурс (Resource) Спецификатор (Qualifier) Структура данных (Data structure) Задача или решение, которое будет выполнено приложением или организацией. Процессы выражаются через действия, которые выполняются при помощи ресурсов. Примеры процессов: наем новых служащих, составление счетов, отслеживание жалоб клиентов и т. д. Физическое лицо, организация или другой объект, находящиеся вне описанных бизнес-единиц или приложения, но взаимодействующие с ними. Внешние сущности являются либо источником, либо пунктом назначения информации в моделируемой системе. Примеры внешних объектов: клиенты, арендаторы, Конгресс, маркетинг и т. д. Данные, созданные, используемые или изменяемые моделируемой системой. Примеры хранилищ: записи о клиентах, планы счетов, бланки счетов, данные о собственности и т. д. Товары или данные, перемещающиеся между внешними сущностями, процессами и хранилищами. Примеры потоков: информация клиента, заказ, поставка, экспресс-доставка почты, служебные запросы и т. д. Элемент моделируемой системы, который каким-то образом используется процессом. Примеры ресурсов: сервер базы данных, ленточный накопитель, менеджер по персоналу, канцелярские принадлежности и т. д. Дополнительно определяет внешнюю сущность, поток, процесс или хранилище. Например, спецификатор может показывать, что заявки на услуги обычно поступают по телефону или что информация по найму служащих посылается в головной офис по e-mail Детальная информация о данных, содержащихся в хранилище. Структуры данных перечисляют признаки содержимого хранилища Вы получили некое представление о том, что вы будете моделировать, и поэтому мы перейдем к переводу ранее определенных вами функций приложения в модели бизнес-процессов. Практическое моделирование бизнес-процессов Чтобы увидеть, как происходит перевод функций в процессы, давайте вернемся к ранее упомянутому демонстрационному проекту. В качестве примера я приводил приложение, где постановка цели звучала следующим образом: «Система RENTMAN поможет управлять арендованной собственностью». Некоторые из функций демонстрационного приложения были определены следующим образом: ■ оно будет регистрировать и обслуживать арендованную собственность; ■ оно будет следить за работой по обслуживанию арендованной собственности; • ■ оно будет выдавать информацию о счетах арендатора; ■ оно будет выдавать данные по некоторой собственности за предыдущие периоды. Моделирование похоже на плавание: лучший способ научиться — прыгнуть прямо в воду. Чтобы посмотреть, как вы моделируете бизнес-процессы, соответ-
Моделирование бизнес-процессов 133 ствующие функциям приложения, мы попытаемся смоделировать процессы, необходимые для осуществления первой функции — регистрации и обслуживания арендованной собственности. Задача моделирования любого бизнес-процесса состоит из следующих пунктов. 1. Определение необходимых внешних сущностей, процессов, потоков и хранилищ. 2. Выбор связей между этими элементами. 3. Схематическое изображение этих элементов и отношений между ними в виде модели процесса. Вы уже знаете, что будете моделировать управление арендованной собственностью, поэтому вам нужно получить ответы на следующие вопросы. ■ Какие внешние сущности необходимы для регистрации и обслуживания арендованной собственности? ■ Какие процессы задействованы? ■ Каких ресурсов потребуют данные процессы? ■ Какие хранилища данных понадобятся? ■ Какие потоки данных будут связывать один элемент с другим? В данном случае вы уже можете сделать следующие выводы. ■ Вам потребуется, по крайней мере, одна внешняя сущность — будущий арендатор. ■ Отдельные процессы понадобятся для управления обработкой договоров об "■. аренде и исполнения этих договоров. ■ Предположим, что компания по управлению арендованной собственностью захочет отслеживать звонки от потенциальных арендаторов и хранить информацию об арендаторах, арендных договорах и собственности отдельно; тогда вам придется создать четыре хранилища данных. В этих хранилищах будут накапливаться данные о звонках, арендаторах, собственности и договорах аренды. ■ Что касается потоков данных, связывающих эти элементы, то можно предположить следующее. Q Потенциальные арендаторы связываются с ответственным за аренду служащим, который находится в офисе по управлению собственностью, чтобы узнать о наличии собственности, которую можно арендовать, или для того, *'• чтобы заключить договор аренды. ; Q Служащий регистрирует каждый звонок независимо от того, занимается ли он заключением договора. < а Служащий проверяет, есть ли свободная для аренды собственность. ?■■ □ Когда договор аренды проверен служащим, документ направляется для исполнения к менеджеру по аренде. ! □ Информация об арендаторе, полученная служащим во время заключения | договора, заносится им в картотеку. I □ Менеджер по аренде ведет записи о выполненных договорах аренды. Обладая данными фактами, мы начнем построение модели бизнес-процессов для управления информацией об аренде.
134 Глава 5. Проектирование баз данных Добавление внешних сущностей Начните с запуска выбранного вами инструмента моделирования и создания новой модели бизнес-процессов. Затем найдите в вашем инструменте объект Внешняя сущность и поместите его в верхнем левом углу вашей пока еще пустой модели. Дайте ему имя Потенциальный арендатор. Этот объект будет представлять потенциального арендатора, который либо интересуется доступной собственностью, либо звонит для заключения договора аренды на определенную собственность. На рис. 5.3 показано, как должна выглядеть ваша модель на этом этапе. Рис. 5.3. Новая модель с ее первой внешней сущностью Добавление процессов Поместите в вашу модель два объекта процессов: один в центре сверху и второй в правом верхнем углу. Задайте первому процессу имя Обработка договоров об аренде (Lease Processing), а второму— Исполнение договоров об аренде (Lease Execution). Объект Обработка договоров об аренде занимается получением и проверкой информации об аренде. Это — основа всего процесса аренды. Процесс Исполнение договоров об аренде занимается фактической арендой собственности. Он связан с передачей ключей и других вещей новому арендатору и созданием отчета о новом договоре аренды. На рис. 5.4 показано, как должна выглядеть ваша модель с этими двумя процессами. Добавление хранилищ Теперь внизу модели поместите четыре объекта хранилищ данных, с которыми будут взаимодействовать другие элементы модели. Дайте название первому хранилищу ЗВОНКИ (CALL), второму - СОБСТВЕННОСТЬ (PROPERTY), третьему - АРЕНДАТОР
Моделирование бизнес-процессов 135 (TENANT) и четвертому — АРЕНДА (LEASE). Ваши объекты процессов будут посылать и получать данные из этих хранилищ. На рис. 5.5 показано, как может выглядеть ваша модель на этом этапе. ; SILVERRUN-BPM - [Context IRentalMan System BPM Oiaaiara for Lease Process 1.011 _r: £d+ £'os-rs* yt LJ*'fc' r t-,e • Ml ^. (й-л J-_s Prospective Tenant Рис. 5.4. Новая модель с объектами внешней сущности и процессов —з ;. SILVERRUN-BPM - [Context IRentalMan System BPM Oiagiam fai Lease Process 1 A11 £ft gRfWFU^JP &ИЫ Р*ф<* U& №%&№ jg& Prospective Tenant Рис. 5.5. Поместите четыре объекта хранилищ в новую модель
136 Глава 5. Проектирование баз данных Добавление объектов потоков Теперь, когда все объекты на месте, вы можете определить взаимосвязи между ними. Для этого вам понадобятся объекты потоков. Соедините объекты Потенциальный арендатор и Обработка договоров об аренде, используя объект потока, проходящий слева направо. Задайте потоку текстовую метку Заявка на аренду (Арр I i es for Lease). Он обозначает обращение потенциального арендатора для заключения нового договора аренды. Теперь соедините объекты Обработка договоров об аренде и Потенциальный арендатор, используя объект потока, проходящий справа налево. Задайте ему метку Уведомление о приеме (Notifies of Acceptance). Этот объект представляет взаимосвязь между офисом по аренде и потенциальным арендатором. Соедините объекты Обработка договоров об аренде и Исполнение договоров об аренде, используя объект потока, проходящий слева направо. Задайте ему метку Подтверждение договора об аренде (Verifies Lease). Он обозначает, что договор об аренде после подтверждения передан менеджеру для исполнения. Теперь соедините объекты Исполнение договоров об аренде и Потенциальный арендатор, используя объект потока, проходящий справа налево. Задайте ему метку Аренда собственности (Leases Property). На рис. 5.6 показано, как должна выглядеть ваша модель, когда все эти объекты на своих местах. Все оставшиеся соединения имеют дело с взаимосвязями между вашими хранилищами данных. Изучите рис. 5.7 и добавьте объекты потоков, как показано на рисунке. SII.VERHUN ВРМ - (Contract [RentalMan System BPM Diagiam tw Lease Piocess 1,01] J £.* gjtewntstor Jt«fc fK- Ш* y/ndm. Helu Prospective Tenant Applies for j Lease : Lease \ ; ■ Verities Processing \ Lease : Notifies of :Acceptance Leases Property Рис. 5.6. Объекты потоков позволяют устанавливать отношения между элементами модели
Моделирование бизнес-процессов 137 ; SILVEHnUN ВРМ - [Context [RenlalHan Syjlum BPM Dingrara lor Loan: Pioccsi 1 01] ftg» i_ tiTi'iliTnf i?mF Я Рис. 5.7. Эта модель схематически изображает процесс сдачи в аренду собственности новому арендатору Добавление структур данных Вы успешно смоделировали процесс сдачи собственности в аренду новому арендатору. В дальнейшем можно улучшить модель, но та, которая уже имеется, вполне функциональна. На этой стадии будет полезным (но необязательным) определение структуры, представляющей данные, которые ваши объекты хранилищ будут содержать. Большинство инструментов моделирования позволяет задать признаки для ваших данных хранилищ, которые могут использоваться далее в процессе E-R-mo- делирования. Их часто называют структурами данных, и они приблизительно соответствуют сущностям в E-R-диаграммах и таблицам в реляционном проекте базы данных. Обычно вы можете получить информацию об атрибутах из исходных документов и форм, заполненных вашим клиентом, или получив ее от пользователей. Ваша задача — как можно раньше узнать, какая информация будет храниться в системе. Чем раньше вы перечислите атрибуты вашей базы данных, тем больше времени вы сэкономите. Задача добавления определений структуры данных к модели бизнес-процессов обычно делится на две части: определение структур данных и объединение структур данных с объектами хранилищ.
138 Глава 5. Проектирование баз данных Начните с добавления трех структур данных: ЗВОНКИ (CALL), ДОГОВОР АРЕНДЫ (LEASE) И АРЕНДАТОР (TENANT) — к уже существующей модели (рис. 5.8). r\^.^A'J'x£>"J,:J^"x^> .'■■■:■■)< - _j< <мЩЩ Prospective Tenant взят ...Applies Jor.- С «,*c,.\*b.i»e4e*ns I^n-tCNa.4 i A^I J ~J£ \ ш \ i vf~y"( . рш& \ Рис. 5.8. Добавление структур данных Следующим шагом после добавления трех структур данных является определение их атрибутов. Некоторые инструменты позволяют сделать это за один шаг, другие — за несколько. На рис. 5.9 показан процесс добавления атрибутов к структуре данных. Давайте зададим атрибуты каждой из ранее определенных структур данных. Добавьте следующие элементы к структуре данных ЗВОНКИ: ■ Номер телефона (Cal I Number); ■ Дата и время звонка (Cal I DateTime); ■ Номер собственности (Property Number); ■ Описание звонка (Cal I Description). Добавьте эти атрибуты к структуре данных ДОГОВОР АРЕНДЫ: ■ Номер договора об аренде (Lease Number); ■ Номер арендатора (Tenant Number); ■ Адрес собственности (Property Address); ■ Город, в котором находится собственность (Property City); ■ Штат, в котором находится собственность (Property State);
Моделирование бизнес-процессов 139 (к| £fe £<Й В*мг*»вяк Й«*>) ft$M Ш» ИМа* Help Prospective Tenar.t * 1 Ж" CALL PR с Applies tor / I зч* ? err-e i ЗДОДис ше * ' 1 , ■-! - 1 CallDateTime 1 property Number 1 Celt Description Hs«№ - <S"'aK«t Г'.-.(:-: Lease \ j Verities X - 1Г(-51 | 4»И/$«иСП<в ; Items... ] 5Ыс1иге$,„ j j A^ i w^c/v I i. Lease *\ <*. 1 .■nMiftt 21 -j Рис. 5.9. Можно добавлять элементы в структуру данных ■ Почтовый индекс населенного пункта, в котором находится собственность (Property Zip); ■ Пристройки (Property Add it ion); ■ Спальни (Property Bedrooms); ■ Жилая площадь (Property Li vingAreas); ■ Ванные комнаты (Property Bathrooms); ■ Тип гаража (Property GarageType); ■ Школьный округ (Property SchooiDistrict); ■ Залог (Property Deposit); ■ Квартирная плата (Property Rent); ■ Диапазон колебаний платы (Property Range); ■ Холодильник (Property Refrigerator); ■ Посудомоечная машина (Property Dishwasher); ■ Центральное отопление (Property Centra I Heat); ■ Центральное кондиционирование (Property Centra i Ai r); ■ Газовое отопление (Property GasHeat); ■ Ограждение (Property PrivacyFence);
140 Глава 5. Проектирование баз данных ■ Дата последнего полива газона (Property LastSprayDate); ■ Дата последней стрижки газона (Property LastLawnDate); ■ Дата начала договора аренды (Lease Beg inDate); ■ Дата окончания договора аренды (Lease EndDate); ■ Дата въезда (Lease Moved InDate); ■ Дата выезда (Lease MovedOutDate); , > ■ Арендная плата (Lease Rent); ■ Взнос за содержание животного (Lease PetDeposit); ■ Срок взимания арендной платы (Lease RentDueDay); ■ Услуги стрижки газонов (Lease LawnService). Добавьте эти элементы к структуре данных АРЕНДАТОР: ■ Номер арендатора (Tenant Number); ■ Имя арендатора (Tenant Name); ■ Работодатель арендатора (Tenant Employer); ■ Адрес работы арендатора (Tenant EmployerAddress); ■ Город работы арендатора (Tenant EmployerCity); ■ Штат работы арендатора (Tenant Emp I oye rState); ■ Почтовый индекс населенного пункта, где работает арендатор (Tenant Emp I oyerZ i p); ■ Домашний телефон арендатора (Tenant HomePhone); ■ Рабочий телефон арендатора (Tenant WorkPhone); ■ Сотовый телефон арендатора (Tenant ICEPhone); ■ Примечания (Tenant Comments). Источником подобной информации могут быть договоры аренды, данные об арендаторах и т. д. Заметьте, что на этой стадии перед вами не стоит задача нормализации данных. Она появится позже. Сейчас вы должны как можно более правдиво смоделировать объекты реального мира, с которыми ваша система будет взаимодействовать. Заметьте, что каждый добавленный нами атрибут имеет в виде префикса имя хранилища. Это хорошее правило, так как позже эти хранилища превратятся в таблицы в физической базе данных. Работа но такой схеме поможет нам на стадии E-R-моделирования. После того как вы создали структуры данных, вы можете соединить их с объектами хранилищ (рис. 5.10). Соединим структуры данных с соответствующими им объектами хранилищ: структуру данных ЗВОНКИ — с объектом хранилища ЗВОНКИ, структуру данных АРЕНДАТОР — с объектом хранилища Арендатор, структуру данных ДОГОВОР АРЕНДЫ — с объектом хранилища ДОГОВОР АРЕНДЫ. Когда структуры данных соединены с соответствующими им хранилищами, можно считать моделирование бизнес-процессов, связанных с арендой собственности, завершенным. Следующая стадия — это моделирование «сущность-
Моделирование «сущность-связь» 141 связь», необходимое для обслуживания модели бизнес-процесса, которую вы построили. jSILVEflBUM-Bi'M • [Context (RentalMan System BPM Diagram for Lease Process 1 0П Я ■тмм-.ииа :' LEASE Verities Lease Leases Property' Record tenant information" Рис. 5.10. Можно соединить структуры данных с хранилищами Однако пока вы не начали, вам следует сохранить созданную модель бизнес- процессов на диске. Если ваш инструмент позволяет добавить модель в репозита- рий для повторного использования в других моделях, то также сохраните ее и в репозитарии. В дальнейшем это облегчит создание E-R- и логических моделей данных. Моделирование «сущность-связь» В 1976 году Питер Чен (Peter Chen) опубликовал первую спецификацию, представляющую реляционные данные как совокупность отношений между сущностями, которая называется «Модель отношений между сущностями — путь к унифицированному представлению данных» («The Entity Relationship Model — Toward a Unified View of Data», Peter Chen). Работа Чена наряду с работами других теоретиков, таких как Хаммер (Hammer) и Маклеод (McLeod), сторонников семантической модели данных, положила начало E-R-диаграмм — наглядным изображениям отношений между сущностями, являющимся на сегодняшний день основой логического моделирования данных.
142 Глава 5. Проектирование баз данных Первое, что вы обнаружите в E-R-моделировании, — это то, что нет четкого последовательного перехода от бизнес-процессов к полностью определенным логическим моделям данных. Конструирование отношений между сущностями требует больших мыслительных затрат. Вы, вероятно, не раз все обдумаете, изменяя свою модель, прежде чем поймете, что она вас устраивает. Типы E-R-диаграмм E-R-диаграммы обычно принадлежат к одному из двух основных типов. Первый — стандартная нотация Чена. Подход Чена к E-R-диаграммам можно охарактеризовать как очень гранулированный. Каждая диаграмма обычно представляет только одну связь между двумя сущностями. На рис. 5.11 показана простая E-R-модель в стиле Чена. м Property Рис. 5.11. E-R-диаграмма, построенная в соответствии с оригинальной методикой Чена Второй тип E-R-моделей — это детальные E-R-диаграммы. Детальные E-R- модели появились, потому что отрывочный подход Чена приводит к созданию сотен (и даже тысяч) отдельных диаграмм при построении сложных проектов баз данных. С детальными диаграммами дело обстоит иначе, так как все отношения для данной сущности отражены в одной диаграмме. В центре такой диаграммы находятся сущности, а не отношения. Детальные E-R-диаграммы обычно содержат информацию уровня атрибутов и значительно отличаются от простых диаграмм Чена. На рис. 5.12 показано, как должна выглядеть детальная E-R-диаграмма. Вообще, сущности в E-R-диаграммах соответствуют таблицам вашей будущей реляционной модели и физического воплощения вашей базы данных. Термины Е-R-моделирования Пока мы не углубились в E-R-моделирование, следует дать определение некоторым основным терминам моделирования, необходимым для понимания нашего дальнейшего разговора. Список, приведенный в табл. 5.2, не всеобъемлющий, но в нем приведена хорошая подборка терминов, с которыми вы встретитесь здесь и в другой технической литературе, освещающей тему E-R-моделирования и логического моделирования данных. Некоторые из этих терминов применимы только к Е- R-моделированию, другие связаны с логическим моделированием данных вообще. Существует прямая связь между терминами E-R-моделирования и терминами реляционного моделирования. Эта взаимосвязь отражена в табл. 5.3. В основных инструментах моделирования E-R-моделирование представлено как подраздел реляционного моделирования данных. E-R-моделирование не только не отражает полноты логического моделирования данных, оно также не зависит от физического проекта.
Моделирование «сущность-связь» 143 Рис. 5.12. Более детальная E-R-диаграмма, отступающая от классического стиля Чена Таблица 5.2. Глоссарий терминов E-R-моделирования Термин Определение Сущность (Entity) Экземпляр сущности (Entity instance) E-R-моделирование (E-R modeling) Подтип (Subtype) Реальный объект — человек, место, событие или вещь, — данные о котором вы хотите сохранить. Сущности также называют классами сущностей Отдельный элемент, представленный классом сущностей. Например, клиент Джон До является экземпляром сущности, представляющим класс сущностей КЛИЕНТ (CUSTOMER). Экземпляр сущности также называют явление сущности Тип логического моделирования данных, который исходит из того, что все бизнес-элементы могут быть обобщены в основные образы — абстрактные понятия. Эти понятийные сущности описаны через их характеристики или атрибуты. Они связаны друг с другом через действия, которые они выполняют по отношению друг к другу. Эти действия устанавливают отношения между сущностями. E-R-моделирование визуально изображает эти отношения Класс сущностей, являющийся подмножеством большего, более содержательного типа сущности, называемого супертипом. Например, ПОЖАРНЫЙ (FIREMAN) может являться подтипом супертипа ГОРОДСКИЕ РАБОТНИКИ (CITYWORKER). Подтипы обычно обладают теми же признаками и отношениями, что и супертипы, но сохраняют за собой право определять собственные группы подтипов. Например, ПОЖАРНЫЙ (FIREMAN), ПОЛИЦЕЙСКИЙ (POLICEMAN), МУСОРЩИК (GARBAGEMAN) известны как кластеры подтипов продолжение &
144 Глава 5. Проектирование баз данных Таблица 5.2 {продолжение) Термин Определение Супертип (Supertype) Атрибут (Attribute) Домен (Domain) Доменная целостность (Domain integrity) Отношение (Relationship) Реляционная целостность (Relational integrity) Связность (Connectivity) Количество элементов (Cardinality) Модальность (Modality) Нормализация (Normalization) Идентификатор сущности (Entity Identifier) Класс сущностей, являющийся супермножеством меньших, менее содержательных классов сущностей, называется супертип. Например, класс сущностей АВТОМОБИЛИ может быть супертипом подтипов сущностей АВТОМОБИЛИ FORD (FORDAUTO), АВТОМОБИЛИ GM (GMAUTO) и АВТОМОБИЛИ CHRYSLER (CHRYSLERAUTO). Часто подтипы и супертипы вместе называют s-типы Характеристика сущности или отношений сущности. Атрибуты детально описывают сущности. Например, Номер социального страхования (SocialSecurityNo) может являться атрибутом класса сущностей СЛУЖАЩИЕ (EMPLOYEE) Определенный тип данных или область значений, которые допускает атрибут. Например, применение домена TDate к признаку Дата найма (HireDate) может потребовать, чтобы все вхождения Даты найма были правильными датами. Аналогично, применение домена Цена выше нуля (TNonZeroCost) к признаку Цена (Price) может потребовать, чтобы все значения Цены были выше нуля Правила, которые контролируют типы данных, допустимые доменом. Доменная целостность, например, обеспечивает, что значения, содержащиеся в домене Дата (Date), на самом деле являются правильными датами и что значения, хранящиеся в числовых атрибутах, действительно являются числами Связь между двумя сущностями, которая задает поведение одной сущности, в случае, когда что-то происходит с другой. Существует пять основных видов связей сущностей: один-ко-многим, многие-ко-многим, один-к-одному, взаимоисключающие и рекурсивные Соблюдение правил, определенных отношениями между сущностями. Например, реляционная целостность не позволит удалить экземпляр сущности КЛИЕНТ (CUSTOMER), если он все еще связан с экземпляром сущности СЧЕТ-ФАКТУРА (INVOICE) Отображает отношение связанных экземпляров сущностей. Например, определение связности между сущностями СЧЕТ-ФАКТУРА (INVOICE) и КЛИЕНТ (CUSTOMER) может показать, что на каждый экземпляр сущности КЛИЕНТ может существовать несколько экземпляров класса сущностей СЧЕТ-ФАКТУРА, так как каждый клиент может размещать несколько заказов Фактическое число связанных экземпляров между сущностями в отношениях сущностей. Оно определяет количество абстрактных отношений, уточняя связность. Например, количество элементов между сущностями СЧЕТ-ФАКТУРА (INVOICE) и КЛИЕНТ (CUSTOMER) может обозначать, что для каждого экземпляра СЧЕТ-ФАКТУРА существует хотя бы один, соответствующий ему экземпляр КЛИЕНТ Определяет, является ли наличие экземпляра сущности опциональным, или отношение требует его наличия. Модальность (также называемая опциональность или наличие) подразумевает минимальное количество элементов в отношениях между сущностями Удаление из модели данных лишних, необоснованных и запутанных элементов. Нормализация обеспечивает представление каждого экземпляра сущности не более, чем одним объектом реального мира Комбинация атрибутов, необходимых для того, чтобы отличать один экземпляр сущности от другого
Моделирование «сущность-связь» 145 Таблица 5.3. Взаимосвязь терминов E-R-моделирования с терминами реляционного моделирования Термин E-R-моделирования Термин реляционного или логического проектирования Сущность (Entity) Таблица (Table) Экземпляр сущности (Entity occurrence/instance) Запись (Row) Идентификатор сущности (Entity identifier) Первичный ключ (Primary key) Уникальный идентификатор (Unique identifier) Ключ-кандидат, потенциальный ключ (Candidate key) Отношение (Relationship) Внешний ключ (Foreign key) Атрибут (Attribute) Столбец (Column) Домен (Domain) Тип данных (Data type) Теперь, когда вы знакомы с основными понятиями, перейдем к моделированию «сущность-связь», необходимому для поддержки ранее определенных процессов. Построение вашей E-R-модели Благодаря тому, что вы полностью определили модель бизнес-процессов до начала процесса E-R-моделирования, вы потратите гораздо меньше времени на разработку работающей E-R-модели. В этой главе будет показано, что основное, чего вы добьетесь конструированием E-R-модели, — это нормализация данных. ВНИМАНИЕ Как правило, нормализацию относят к функциям реляционного моделирования данных или процесса проектирования базы данных. В этой книге мы рассматриваем E-R-моделирование как первый шаг в этом процессе. Некоторые программисты полностью разделяют эти два типа моделирования, другие — от E-R-моделирования сразу переходят к конструированию объектов базы данных, а третьи — строят реляционные модели, не утруждая себя построением E-R-диа- грамм. Вы научитесь выполнять оба типа моделирования. Завершая моделирования «сущность- связь», вы заканчиваете логический проект базы данных. Хотя интуитивно может казаться, что нормализацию следует проводить после стадии логического моделирования данных, большинство E-R-инструментов позволяют выполнять нормализацию того или иного рода, поэтому здесь мы затронем эту тему. Давайте начнем построение E-R-модели для модели бизнес-процессов аренды, которую вы создали ранее. Создайте новую модель и, если ваш инструмент это позволяет, импортируйте модель бизнес-процессов, созданную ранее (отправьте ее по необходимости в репозитарий вашего инструмента). Вставьте каждое хранилище из модели бизнес-процессов в новую E-R-диа- грамму. В зависимости от того, какой инструмент вы используете, будут созданы объекты сущностей, соответствующие объектам хранилищ, ранее определенных в модели процессов. Ваш инструмент может помешать вставить хранилище СОБСТВЕННОСТЬ (PROPERTY), так как оно не имеет связанной с ним структуры данных. На рис. 5.13 показано, как должна выглядеть на этот момент ваша модель.
146 Глава 5. Проектирование баз данных antCJutiиJuМИД Ari йИМУчч ЩЬЛи дин» жши ^мшйшшмшю^ uffjxj CALL CaJ Number Can DateTime Property Number Call Description TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCrty Tenant EmptoyerState Tenant EmployerZip Tenant HomePhone Tenant Workphone Tenant ICEPhone Tenant Comments LEASE Lease Number Tenant Number Properly Address Properly City Property State Properly Zip Property Addition Property Bedrooms Property LivmgAreas Properly Bathrooms Property GarageType Property ScrioolDislrict Properly Deposit Properly Rent Properly Range Properly Refigerator properly Dishwasher Properly CentralHeat Properly CentralAir Properly GasHeat Properly PrivacyFence Properly LastSprayDate Property LastLawnDate Lease BegmDate Lease EndDate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PetDeposit Lease RentDueDay Lease LawnService Lease Comments 1 Рис. 5.13. Модель с объектами сущностей, полученными из репозитария Если ваш инструмент E-R-моделирования имеет средства нормализации, можно избежать самостоятельного выполнения скучной работы по нормализации. Если у вас нет мастера нормализации, то следующим шагом будет декомпозиция ваших сущностей в нормализованные классы сущностей, а затем соединение их друг с другом через отношения сущностей. Вот, собственно, каким образом идет процесс E-R-моделирования. Если же в вашем инструменте есть мастера нормализации, вы можете пропустить некоторые ступени реальной работы по моделированию данных. Во многих случаях эти средства могут полностью нормализовать вашу модель, задав лишь несколько несложных вопросов. ВНИМАНИЕ Я должен упомянуть, что некоторые мастера нормализации выполняют неполную нормализацию базы данных. Поэтому всегда необходимо перепроверять работу программы нормализации, пока вы не убедитесь, что все в порядке (это является еще одним серьезным основанием для того, чтобы самому иметь глубокое представление о нормализации). Предположим, что ваш инструмент построения E-R-диаграмм поддерживает мастера нормализации. Запустите его, чтобы нормализовать модель. На рис. 5.14 показано, как должна выглядеть модель после нормализации. Расположите ваши
Моделирование «сущность-связь» 147 сущности и ассоциированные с ними отношения так, чтобы на диаграмме они не закрывали друг друга. р- ке ^я t'tseitatjbi fc-__-*4l t£&& <-гэрс1 Лчк лЧкич ti^p Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmpioyerState Tenant EmployerZip Tenant Homephone Tenant WorkPhone Tenant ICEPhone Tenant Comments -аЙШ ц i Propt Properly Properly Properly Properly Properly Properly Properly Properly Properly Properly Property Properly Property Properly Properly Properly Properly .P.rnnprt.v. LastLawnDate GasHeat CentralAir Last Spray Date CentralHeat Refigerator Disfwvssher Rent SchoolDtslnct Deposit Bedrooms GarageType City Bathrooms LivingAreas Zip State flriftaess LEASE Lease Number Lease BegmDate Lease EndDate Lease MovedlnDate Lease MpvedOutDate Lease 'Rent .Lease PetDeposit Lease RentDueDay Lease LawnService Lease Comments d Рис. 5.14. Так должна выглядеть модель после нормализации После нормализации модели вы должны увидеть новую сущность — сущность Собственность (PROPERTY). Эта сущность должна была быть добавлена мастером нормализации из-за избыточности в сущности ДОГОВОР АРЕНДЫ (LEASE). Задайте ей имя СОБСТВЕННОСТЬ (PROPERTY). На рис. 5.15 показано, как должна выглядеть модель на этом этапе. Когда вы спроектировали объект структуры данных для ранее определенной модели процессов, у вас может появиться вопрос: «Почему автор не попросил создать структуру данных для хранилища СОБСТВЕННОСТЬ?» Теперь вам должна быть ясна причина. Атрибуты, необходимые для хранилища СОБСТВЕННОСТЬ (PROPERTY), были вложены в структуру данных ДОГОВОР АРЕНДЫ (LEASE). Я оставил их там, чтобы продемонстрировать, насколько CASE-инструменты могут быть полезны в моделировании данных. Ваш инструмент E-R-моделирования должен был переместить атрибуты, относящиеся к собственности из сущности ДОГОВОР АРЕНДЫ (LEASE), в новую сущность СОБСТВЕННОСТЬ (PROPERTY). Атрибут Номер собственности (Property Number) должен был быть также скопирован из сущности ЗВОНКИ (CALL). . *г:. 1, -
148 Глава 5. Проектирование баз данных тЩЩ ■■ ■ 7 ; ; ■ ■ ....t... г ; 1 ;""" ..;. . ■: ...U s s ii PROPERTY Properly Number Properly Range Property LastLawnDate Property GasHeat Property CentralAir Property LastSprayDerte Property CentralHeat Property Refigerator Property Dish^vasher Property Rent ' Properly SchoolDistrict 11 Property Deposit Property Bedrooms Property GarageType Properly City Properly Bathrooms Properly LivingAreas Properly Zip Property State Property Address Property Addition Property PrivacyFence 5 ■■■T,.;.^ rr—ii :0,N ■'Vr ■-. 4- 1,1 CALL Call Number Call DateTime Call Description 3 ": ; ' y- i LEASE Lease Number Lease BeginDate Lease EndDaie Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PetDeposit Lease RentDueDay Lease LawnService Lease Comments L.I ■ ;l TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmployerState Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Tenant Comments J Рис. 5.15. В результате нормализации в проекте появляется новая сущность Нормализация Теперь я должен описать точнее, что такое нормализация. В дополнение к данному ранее определению можно сказать, что нормализация стремится оптимизировать действия и уменьшить вероятность потенциальных ошибок при обновлении записей базы данных. Например, если вы храните адрес клиента в каждой записи таблицы I nvo i ce (Счета), для изменения адреса клиента придется изменить каждую соответствующую запись в таблице I nvo i се. С другой стороны, если адресная информация хранится отдельно в таблице Customer, ее необходимо изменить только в одном месте — и это значительно уменьшит время, необходимое для изменения адреса клиента, но вместе с тем будет невозможен пропуск записи в таблице Invoice. Нормализация формально разделена на пять форм или стадий: с первой нормальной формы по пятую. Эти не очень ясные термины на самом деле представляют собой пять наборов реляционных критериев, которым сущность соответствует или не соответствует. Каждая следующая форма строится на основании предыдущей. Хотя технически есть пять основных форм, на практике вы, скорее всего, будете использовать только первые три. Последние две, в целом, слишком специализированы для обычного проектирования баз данных.
Моделирование «сущность-связь» 149 ПРИМЕЧАНИЕ Несмотря на то что мы еще не обсуждали подробно реляционное проектирование, я буду использовать такие реляционные термины, как: таблица (table), запись /row) и столбец (column) — вместо таких терминов E-R, как: сущность (entity) экземпляр сущности (entity occurrence), атрибут (attribute). Я считаю нужным это делать потому, что примеры к каждой нормальной форме будет проще понять, если они будут выражены в терминах реляционных и физических объектов базы данных вместо абстрактных понятий. Первая нормальная форма Чтобы таблица считалась приведенной к первой нормальной форме AНФ), каждый ее столбец должен быть полностью атомарен и не должен содержать повторяющихся групп. Столбец атомарен, если он содержит только один элемент данных. Например, столбец Address, который содержит не только улицу, но также город, штат и почтовый индекс, не является полностью атомарным. Столбцы, спроектированные таким способом, должны быть разбиты на несколько столбцов, чтобы полностью соответствовать первой нормальной форме. Помните, что степень, до которой следует разбивать столбцы, зависит от их предназначения — будьте разумны! Повторяющаяся группа — это столбец, который повторяется в определении записи только для хранения нескольких значений данного атрибута. Например, такой подход мы могли использовать при проектировании таблицы Tenant (Арендатор) — хранить информацию об арендуемом имуществе в таблице Tenant, а не отдельно в таблице Property (Имущество). Здесь не учитывается возможность того, что один арендатор (например, корпорация, арендующая множество офисов) может арендовать более одного вида имущества. Чтобы решить эту проблему, используя исключительно таблицу Tenant, мы должны определить максимальное количество видов имущества, которое можно арендовать, а затем добавить соответствующие столбцы в таблицу. Эти дублирующиеся столбцы составили бы повторяющуюся группу. Некоторые инструменты баз данных и языки предоставляют прямую поддержку повторяющихся групп, поощряя таким образом нереляционное проектирование. Очевидно, дизайн не может быть реляционным, если он нарушает первую нормальную форму. Пример такого инструмента — Advanced Revelation. Его многозначные столбцы на самом деле — повторяющиеся группы. Их использование, которое популярно в приложениях AREV, нарушает первую нормальную форму. Другой пример похожей нереляционной конструкции — ассоциативные массивы в Perl. Ассоциативные массивы — это наборы пар название/значение, которые хранятся как одна переменная. Хотя это мощное и удобное на практике средство, оно поощряет кодирование, и таким образом проектирование баз данных нарушает первую нормальную форму. Другой хороший пример нереляционной языковой поддержки — синтаксическая конструкция языка COBOL — group-name occurs several times. Поддержка нереляционных элементов в COBOL не так удивительна, как может показаться, поскольку он создавался до реляционных баз данных. Избегайте этих типов конструкций, если вы хотите создавать хорошо спроектированные приложения баз данных. Создаете ли вы базу данных или приложение для работы с базами данных, повторяющиеся группы — это очень плохая практика проектирования. Я упоминаю здесь приложения, так как дизайн приложения неизбежно сказывается на дизайне
150 Глава 5. Проектирование баз данных базы данных. В конце концов, информация, которая перетасовывается внутри приложения, должна будет, скорее всего, где-то храниться. С практической точки зрения, использование повторяющихся групп связано со множеством трудностей. Во- первых, возвращаясь к примеру таблицы Tenant: если хранить в ней информацию 0 количестве всего арендованного имущества для каждого арендатора, каждая запись об арендаторе будет включать повторяющиеся столбцы с информацией об имуществе независимо от того, используются они или нет. Это, вне всякого сомнения, пустая трата места в базе данных. Во-вторых, сложность представляет обработка данных, содержащихся в повторяющихся группах. В частности, будет довольно тяжело должным образом отформатировать их в печатных отчетах. Вы имеете дело не только со множеством записей, но и со множеством столбцов — превращая тем самым одномерную задачу в двумерную. И, в-третьих, если максимальное количество имущества, которое можно арендовать, должно быть увеличено, придется вносить изменения в структуру таблицы Tenant, а также в приложения, которые эту таблицу используют. Вторая нормальная форма •Чтобы таблица соответствовала второй нормальной форме BНФ), каждый из ее столбцов должен полностью зависеть от ее первичного ключа и от каждого атрибута первичного ключа, если ключ состоит из нескольких столбцов. Это означает, что каждый неключевой столбец в таблице должен уникально идентифицироваться по первичному ключу. Таблицы с первичным ключом, состоящим из одного столбца, удовлетворяющие 1НФ, также удовлетворяют 2НФ. Давайте снова посмотрим пример таблицы I nvo i се. Если первичный ключ этой таблицы состоит из столбцов Locat i onNo и I nvo i ceNo, хранение названия местоположения в каждой записи нарушит вторую нормальную форму. Это происходит потому, что столбец Locat ionName не будет уникально идентифицирован полностью первичным ключом. Он будет зависеть только от столбца Locat i onNo; столбец 1 nvo i ceNo не будет иметь влияния. Вместо этого столбец Locat i onName должен быть получен из таблицы Locat i on с помощью объединения, когда это необходимо, а не храниться постоянно в таблице I nvo i се. Чтобы таблица соответствовала 2НФ, все неключевые столбцы должны полностью функционально зависеть от первичного ключа. Однако транзитивные зависимости все еще разрешены. Третья нормальная форма Чтобы таблица соответствовала третьей нормальной форме (ЗНФ), каждый из ее столбцов должен полностью зависеть (определяться) от ее первичного ключа и не зависеть друг от друга. Итак, наряду с требованиями второй нормальной формы каждый неключевой столбец таблицы должен быть независим от других неключевых столбцов. Это означает, что в отличие от 2НФ ЗНФ не позволяет иметь транзитивные зависимости. Давайте вернемся к примеру с таблицей Invoice. Допустим, первичный ключ снова состоит из Locat i onNo и I nvo i ceNo. Одним из неключевых столбцов таблицы, возможно, будет столбец CustomerNo. Если наряду со столбцом CustomerNo столбец CustomerName будет также находиться в таблице I nvo i се, таблица не будет удовлетворять требованиям третьей нормальной формы, так как столбцы CustomerNo и CustomerName будут зависеть друг от друга. Если столбец CustomerNo изменится,
Моделирование «сущность-связь» 151 столбец CustomerName, скорее всего, также потребуется изменить, и наоборот. Вместо этого столбец CustomerName должен находиться в отдельной таблице (например, в таблице Customer), а доступ к нему должен осуществляться с помощью соединения. ПРИМЕЧАНИЕ Вариант третьей нормальной формы, называемый нормальной формой Бойса-Кодда (БКНФ), требует, чтобы каждый столбец, от которого зависит другой столбец, сам должен быть уникальным ключом. Наборы ключей, которые могут уникально определить запись, также известны как альтернативные ключи. Первичный ключ таблицы выбирается из этих ключей. БКНФ требует, чтобы любые столбцы, которые зависят от других столбцов, были зависимы только от альтернативных ключей. Это означает, что все определители должны быть альтернативными ключами. Итак, БКНФ улучшает третью нормальную форму, позволяя иметь межстолбцовые зависимости между альтернативными ключами и неключевыми столбцами. Однако это не нарушает третью нормальную форму, как это может показаться, потому что ключи, от которых зависят подчиненные столбцы, к тому же являются альтернативными ключами. Они также уникально идентифицируют запись, как и первичный ключ таблицы. Таким образом, зависимость столбца от альтернативного ключа, отличного от первичного ключа таблицы, просто академическое различие, поскольку и первичный ключ, и не первичный альтернативный ключ уникально идентифицируют каждую запись. Если вам это кажется запутанным, не беспокойтесь. Соответствия третьей нормальной форме обычно достаточно, чтобы можно было сказать, что таблица или сущность нормализованы. Разница, которая не вносит никакой разницы, — и не разница вовсе. Четвертая нормальная форма Четвертая нормальная форма запрещает существование многозначных зависимостей между столбцами. Если столбец вместо того, чтобы уникально идентифицировать другой столбец, ограничивает его значения некоторым предопределенным множеством значений — это означает, что между ними существует многозначная зависимость. Давайте взглянем на таблицу Tenant, которую мы уже обсуждали. Предположим, что вся информация о работодателе, которую вы хотите хранить для арендатора (Tenant), — это название его работодателя, поэтому вы включаете атрибут Emp I oye r в сущность TENANT. Чтобы обеспечить возможность иметь арендатору больше одного работодателя (допустим, он трудоголик и по ночам пишет книги), необходимо иметь отдельную запись в таблице Tenant для каждого из работодателей. Все атрибуты каждой записи, за исключением атрибута Employer, будут идентичны. Атрибут Emp I oyer будет отличаться в записях, соответствующих данному арендатору. Отношение между другими столбцами таблицы Tenant и столбцом Emp I oye r составило бы многозначную зависимость. Для каждого столбца TenantNo вы можете иметь несколько значений Emp I oyer. На практике вам придется учитывать возможность того, что арендатор может иметь более одного работодателя. Также вам может быть интересно, кто из арендаторов работает у данного работодателя. Далее, чтобы удовлетворить четвертой нормальной форме, вы должны были бы создать отдельную таблицу, все предназначение которой — хранить перекрестные ссылки между арендаторами и работодателями. В идеале эта новая таблица содержала бы всего два столбца: TenantNo и Emp I oyer, которые оба были бы частями составного первичного ключа. Затем, когда вам понадобилось бы получить всю информацию для заданного арендатора, вы соединили бы таблицу Tenant с новой таблицей с перекрестными ссылками, используя их общий столбец TenantNo.
152 Глава 5. Проектирование баз данных В реальном мире таблицы, не соответствующие четвертой нормальной форме, — не редкость. Декомпозиция сущностей свыше требований третьей нормальной формы иногда приводит к тому, что сущностей становится слишком много. Что хорошо в теории — не всегда применимо на практике. Пятая нормальная форма Пятая нормальная форма предусматривает, что если таблица имеет три или более альтернативных ключей и можно провести их декомпозицию без потери данных, эти ключи должны быть разбиты на отдельные таблицы. Пятая нормальная форма редко «вступает в игру» по нескольким причинам. Во-первых, это редкость — найти таблицу с тремя или более отдельными наборами столбцов, которые бы уникально определяли записи. Во-вторых, чрезмерная декомпозиция может привести к неточным объединениям, в результате которых могут появляться новые записи. По большому счету, вы не увидите применения (или даже обсуждения) пятой нормальной формы в реальной жизни. Я включил ее здесь только для справочной информации. Нормализуйте, но не слишком Когда вы начинаете нормализовывать ваши данные, важно не зайти слишком далеко. Чрезмерная нормализация может очень плохо сказаться на производительности. Также она может усложнить дизайн вашей базы данных. Возьмем пример, который я упоминал в обсуждении четвертой нормальной формы. Вас может одолеть искушение создать отдельную таблицу для информации о работодателе, которая сейчас хранится в таблице Tenant. В конце концов, Егор I oyer и EmpAdd ress точно зависят друг от друга, что противоречит требованиям третьей нормальной формы. Но какую реальную выгоду вы от этого получите? Вероятно, очень небольшую. Информация о работодателе интересна вам только применительно к данному арендатору. Например, вам, скорее всего, не понадобится получать количество всех арендаторов, работающих у заданного работодателя. Предположим, что вы не будете хранить более одного работодателя для каждого арендатора, тогда вам никогда не придется изменять информацию, связанную с работодателями. В таком случае создание отдельной таблицы для работодателей арендаторов только усложнит вашу модель и станет бесполезной тратой времени. Бывают ситуации, когда ограниченная денормализация —- единственный способ добиться необходимой производительности. Особенно часто это бывает при работе с большими объемами данных. Например, представьте, что вы разработали приложение, которое обрабатывает миллион платежей по кредитным картам в день. Среди прочего, в каждом платеже присутствует номер карты, сумма транзакции и дата истечения срока действия карты. В конце рабочего дня ваша система должна распечатать отчет обо всех использованных картах, количестве транзакций по каждой карте и сроках истечения действия каждой из них. Поскольку вы можете легко присоединиться к основной таблице кредитных карт, чтобы получить дату истечения срока действия карты, вы принимаете мудрое решение — нормализовываете таблицу с транзакциями по кредитным картам, исключив из нее дату истечения срока действия, сохраняя место в базе данных и избегая избыточности в дизайне. Это хороший пример реляционного проектирования, но, к сожалению, есть один
Моделирование «сущность-связь» 153 побочный эффект, из-за которого ваш отчет будет выполняться вдвое дольше. (На самом деле, он выполняется настолько долго, что ваш клиент решит, что производительность приложения неприемлема.) Говоря только в терминах проектирования баз данных, возможное решение этой проблемы — хранить и использовать дату истечения срока действия, как если бы она была получена в ходе каждой транзакции. Хотя это и добавляет избыточности в базу данных, это — контролируемая избыточность, так спроектировано и сделано в определенных целях. Часто это приемлемое отклонение от строгих реляционных законов. Правило, которому необходимо следовать при внесении контролируемой избыточности в базу данных: сначала полностью нормализуйте базу данных, а уже потом внесите контролируемую избыточность (но только если это абсолютно необходимо). Сопротивляйтесь искушению денормализовать по своей прихоти или чтобы выдать плохой реляционный дизайн за настройку производительности. Де- нормализация бывает необходима, если только вы работаете с очень большими объемами данных. Завершение модели Технически говоря, ваша модель теперь нормализована, но все же еще остается работа, которую следует проделать. До сих пор в модели не определены идентификаторы сущностей (первичные ключи) и не проверена правильность связей между сущностями. Проверка связности Давайте проверим решения, сделанные нашим инструментом, относительно связности сущностей. Посмотрите на рис. 5.15 и постарайтесь найти неувязку в вашей E-R-диаграмме. Если вы ее обнаружили, исправьте ее в модели. Рисунок 5.16 показывает, как должна выглядеть модель. Сравните ее с рис. 5.15 и обратите внимание на изменение, которое произошло в отношении между сущностями CALL и PROPERTY. Указание количества элементов Обратите внимание, что связность между сущностями CALL и PROPERTY изменилась с 1,1 на 0,1. Что это значит? Числа, которые вы видите на диаграмме на рис. 5.16, представляют собой соответственно минимальное и максимальное количество элементов для связанных сущностей. То есть количество 1,1 между сущностями CALL и PROPERTY означает, что в сущности PROPERTY для каждого экземпляра сущности CALL должно существовать как минимум не менее одного соответствующего экземпляра. В терминах базы данных это означает, что в каждой записи в таблице Са I I должно быть правильное значение PropertyNo из таблицы Property. PropertyNo не может быть пустым. В жизни тем не менее экземпляр PROPERTY может не существовать для экземпляра CALL (может позвонить кто-то, кто еще не арендовал собственность), так что на рис. 5.16 минимальное количество элементов уменьшено до 0. Нулевое количество элементов необходимо, если компания хочет иметь возможность записывать звонки, не соответствующие какой-то собственности, например, вопросы будущих арендаторов. За счет этого можно оставить пустым столбец PropertyNo таблицы Са I !. Вы не можете требовать существование ссылки на соб-
154 Глава 5. Проектирование баз данных ственность в каждом звонке, если сооираетесь записывать звонки, не касающиеся собственности. si VHWUM-tRX. [RemMtn Syitcm Modeii 1.0| К £* В» assentation £а»к ssxw Fwnsa Щ&, Йй4а» fefe Э PROPERTY Property Number Property Range Property LestLawnDate Property GasHeat Property CentralAir Property LastSprayDate Property CentralHeat Property Refigerator Property Dishwasher Property Rent Property SchoolDistrict Property Deposit Property Bedrooms Property GarageType Property City Property Bathrooms Property LivmcjAreas Property Zip Property State Property Address Property Addition Property PrivacyFence - ; ;; \ ■ LEASE Lease Number Lease BegmDate Lease EndDate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PetDeposit Lease RentDueDay Lease LawnService Lease Comments ;...il. ...i i 1,1 '■ TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmpioyerState Tenant EmpioyerZip Tenant Homephone Tenant WorkPhone Tenant ICEPhone Tenant Comments d Рис. 5.16. Модель после проверки связей Аналогично, максимальное количество элементов, равное 1, предусматривает, что максимально может быть только один экземпляр PROPERTY для каждого экземпляра CALL. Это означает, что данная запись в таблице Са 11 может соответствовать только одной собственности. Она не может ссылаться на несколько экземпляров собственности. И снова это хорошее решение, если считать, что звонок обычно или не ссылается на какую-то собственность (например, для опроса потенциального арендатора), или ссылается на какую-то одну (например, просьба арендатора что- либо починить). Количество элементов в отношении изображается с точки зрения ближайшей сущности. Так что вы можете использовать следующее предложение, чтобы определить количество элементов: «Для каждой записи БЛИЖАЙШЕЙ СУЩНОСТИ мне необходимо МИНИМАЛЬНОЕ КОЛИЧЕСТВО ЭЛЕМЕНТОВ соответствующих записей и МАКСИМАЛЬНОЕ КОЛИЧЕСТВО ЭЛЕМЕНТОВ соответствующих записей в Таблице дальней сущности». Итак, чтобы оценить количество элементов связи CALL-PROPERTY, в этом случае мы получим следующее предложение: «Для каждой записи CALL мне необходим минимум 0 и максимум 1 соответствующая запись в таблице Property». Обратите внимание, что для того, чтобы оценить количество элементов отношения с точки зрения сущности PROPERTY, необходимо отдельное предложение. Одного предло-
Моделирование «сущность-связь» 155 жения обычно недостаточно. Например, экземпляру LEASE необходим соответствующий экземпляр PROPERTY, хотя экземпляр PROPERTY может не требовать существование экземпляра LEASE. Связи сущностей часто выражаются в терминах, подобных этим, и множество CASE-средств используют их в аннотациях к E-R-диаграммам с помощью таких предложений. Связи сущностей можно выразить и по-другому: РОДИТЕЛЬСКАЯ СУЩНОСТЬ (PARENT ENTITY) имеет не менее некоторого числа ДОЧЕРНИХ СУЩНОСТЕЙ (CHILD ENTITY). Итак, возвращаясь к примеру с CALL-PROPERTY, вы напишете: CALL имеет не менее нуля PROPERTY для определения минимального количества элементов. Аналогично, форма РОДИТЕЛЬСКАЯ СУЩНОСТЬ имеет не более некоторого числа ДОЧЕРНИХ СУЩНОСТЕЙ может быть использована, чтобы определить максимальное количество элементов. Вы напишете CALL имеет не более одной PROPERTY для определения максимального количества элементов в отношении между сущностями Call и Property. Есть много вариаций, но основная идея одна и та же. Используйте то, что больше вам подходит. Выбор идентификаторов сущностей Следующий шаг, необходимый для того, чтобы закончить вашу E-R-модель, — это выбор идентификаторов для каждой сущности. Идентификатор сущности будет преобразован в первичный ключ в вашей реляционной модели. Идентификаторы сущностей бывают двух типов: естественные и искусственные, или суррогатные. Естественный идентификатор сущности — это атрибут сущности (или набор атрибутов), который уже есть в сущности и который уникально идентифицирует каждый экземпляр сущности. Например, атрибут SocialSecu rityNo (Номер социального страхования) может быть естественным идентификатором сущности EMPLOYEE (Сотрудник). Искусственный идентификатор— это атрибут, добавляемый в сущность специально, чтобы сущность имела уникальный идентификатор. Добавление искусственного идентификатора может быть необходимо по нескольким причинам. Одна из причин состоит в том, что может существовать другой уникальный идентификатор, который будет неповоротливым или слишком большим для практического использования. Например, пользователь может посчитать, что регулярный набор номера полиса социального страхования сотрудника занимает слишком много времени, и он может запросить более короткий и менее громоздкий атрибут номера сотрудника. Вторая причина добавления искусственного атрибута в том, что сущность может не иметь естественного идентификатора. Например, без атрибута CustomerNo сущность CUSTOMER не будет обладать атрибутом или группой атрибутов, уникально идентифицирующих каждый экземпляр сущности. В описанных ниже случаях каждая сущность имеет искусственный атрибут. Несмотря на тот факт, что вы можете комбинировать атрибуты некоторых сущностей для создания уникальных идентификаторов, я включил для упрощения суррогатные ключи. Это экономит время и гарантирует, что позже у вас не возникнет
156 Глава 5. Проектирование баз данных проблем. Используя возможности вашего средства моделирования для того, чтобы это сделать, добавьте идентификаторы суррогатных ключей в каждую сущность. В табл. 5.4 приведены сущности и их ключи. Таблица 5.4. Суррогатные ключи для сущностей Сущность CALL LEASE PROPERTY TENANT Ключ Call Number Lease Number Property Number Tenant Number На рис. 5.17 показано, как теперь выглядит ваша модель. S& «гмшва [RentMin Synem Mn | Ш Ш {цммМмп Eiifjeft ЩШ - - жтштшшшшштшшмжшжшт®:.- ijt* vv^i-iw Help :*ki -i*J*. LEASE Lease Number Lease BegtnDate Lease EndDate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PatDeposit Lease RentDueDay Lease LawnService Lease Comments TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmployerState Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Tenant Comments Asd Рис. 5.17. Так может выглядеть модель после определения идентификаторов сущностей (ключи подчеркнуты) Последние штрихи Можно проделать некоторую работу, чтобы «отполировать» модель, но мы остановимся на самом важном. Перед тем как перейти к логическому моделированию, давайте сделаем названия элементов модели более чистыми. Я подразумеваю использование аббревиатур или более сжатых названий сущностей,
Моделирование «сущность-связь» 157 атрибутов и других объектов для того, чтобы сделать их более безопасными. Может оказаться, что названия, которые уместны на диаграмме, будет трудно или вообще невозможно использовать в качестве названий объектов базы данных, потому что названия содержат пробелы или другие недопустимые символы. Поскольку сущности, которые вы определили в своей модели, в конечном итоге станут таблицами в вашей новой базе данных, важно использовать названия, приемлемые СУБД, которую вы используете. Хотя квадратные скобки SQL Server позволяют нам работать с названиями, содержащими недопустимые символы, разумнее и проще с самого начала использовать только правильные названия. Множество E-R-инструментов могут исправить названия, которые вы использовали на диаграмме, преобразовав их из удобочитаемых прозвищ в СУБД-совместимые. Иногда никаких изменений не потребуется, а иногда необходимы серьезные изменения. Если в вашем E-R-инструменте есть функция коррекции названий, запустите ее сейчас, чтобы уплотнить названия в модели. Большинство инструментов заменит названия, используемые на диаграмме, аббревиатурами, но не все. Некоторые инструменты хранят новые имена внутри. Другие — позволяют выбрать, какие названия следует отображать на диаграмме (те, которые читаются лучше или их сокращенные версии). Рисунки 5.18 и 5.19 иллюстрируют процесс изменения названий. В В* &И botxmon eswrt fteeel редей да »*>w tS* ■ь** '• *' 3 Property Number Property Property Property Property Property Property Property Property Property 11 Property Property Property Property property Property Property Property Property Property Property Property Range LastLawnDate GasHeat CentralAir LastSprayDate CentralHeat Refigerator Dishwasher Rent SchoolDtstrict Deposit Bedrooms GarageType City Bathrooms LivingAreas *P State Address Addition PrivacyFence LEASE Lease Number Lease BegmDate Lease EndDate nDate 3utDate OSlt eDay ervice fi&rtimHwA 0?ttouNi»!Kf ft4t><&(Зчч? fotf Owsata Property Number PropertyJMi. Property Range Property_Ra Property LasrLa Property_La Property Gasrieat Property _Ga Property CentralAir Property_.Ce Г ГГ F rjTf jj CALL Call Number Call Date Time Call Description lyerAddress >yerCity Tenant EmployerState Tenant EmployerZip Tenant Homephone Tenant Workphone Tenant ICEPhone Tenant Comments Рис. 5.18. Наиболее частая техника работы с названиями, содержащими пробелы, — использование символа подчеркивания
158 Глава 5. Проектирование баз данных fttjest да, йм«» ийя -АЙД1 1 Property Number Property Property Property Property Property Property Property Property Property H Property Property Property Property Property Property Property Property Property Property Property Property Range LastLawnDate GasHeat CentralAir LastSprayDate CentralHeat Refigerator Dishwasher Rent SchoolDistrict Deposit Bedrooms GaracjeType City Bathrooms LivmgAreas Zip State Address Addition PrivacyFence LEASE fc>: !:t i.:-. ^'a::,A^ti^ Lease Number Lease BegmDate Lease EndDate Lease Mo^edlnDate o^edOutDate sposit XieDay lService nents $ф$иеА}|аа Nbt, Depilate p Рнтау Г < Alternated Г < 8H*mmZ> sc—~: CALL Call Number Call DateTime Cat! Description Adaress City State i enant tmployerZip Tenant HomePhone Tenant Workphone Tenant ICEPhone Tenant Comments Рис. 5.19. Лучше использовать простые названия, чем мучиться с квадратными скобками SQL Server Последнее, что следует сделать перед тем, как переходить к реляционному моделированию, — это дать название вашей модели и сохранить ее. Большинство инструментов моделирования позволяют присвоить создаваемой модели текстовое название, не зависящее от названия файла. Хорошее название вашей модели — Е-R-диаграмма процесса аренды (E-R Diagram for Lease Process). После того как вы присвоили название модели, сохраните ее на диске. Теперь вы готовы перейти к реляционному моделированию. На рисунке 5.20 показано, как может выглядеть ваша законченная E-R-модель. Реляционное моделирование данных Теперь, когда вы успешно смоделировали отношения между сущностями, необходимые для правильного функционирования процесса аренды, вы готовы перейти к реляционному моделированию данных. Реляционное (или логическое) моделирование данных еще на один шаг ближе к физической реализации базы данных, по сравнению с созданием E-R-диаграммы. Здесь вы определите первичные ключи, ограничения ссылочной целостности и т. д. Для этого снова необходимо CASE-сред- ство, причем хорошее, которое не только позволит упростить моделирование, но и позволит сгенерировать соответствующие операторы SQL для реализации вашей модели.
Реляционное моделирование данных 159 ЗИ. «ИЮНЯХ • IRentMwi System f* diagram rot lot»e Ptqe»»» 1Л1 l Be Ш &ъиШэШ> Expert Mooef pk»* Ш, И*»*** a*- 1 Property Number Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Range LastLawnDate GasHeat CentralAjr LastSprayDate CentralHeat Refigerator Dishwasher Rent SchoolDistrict Deposit Bedrooms GarageType City Bathrooms LivmgAreas Zip State Address Addition PrivacyFence LEASE Lease Number Lease BegmDate Lease EndDate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PetDeposit Lease RentDueDay Lease LawnService Lease Comments TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmployerState Tenant EmpioyerZip Tenant HomePhone Tenant WorkPhone Tenant iCEPhone Tenant Comments ы Рис. 5.20. Завершенная E-R-модель Термины логического моделирования Прежде чем мы начнем, позвольте представить вам некоторые основные понятия реляционных баз данных и логического моделирования. В табл. 5.5 приведены термины, которые я считаю основными. Я уверен, что с большинством из этих терминов вы уже знакомы, но немного освежить их в памяти не помешает. Это поможет вам лучше понять дальнейшее обсуждение. Глоссарий не претендует на полноту, но, надеюсь, вы найдете его достаточно полезным. Таблица 5.5. Важные термины и понятия реляционного моделирования Термин Определение База данных (Database) Таблица (Table) Совокупность данных, организованных в таблицы. Аналогия базы данных — шкаф для хранения папок. База данных содержит таблицы, которые включают в себя некие данные, — в шкафу стоят папки, которые содержат какие-то документы Основное хранилище данных в базе данных. Вы можете считать реляционную таблицу двумерной поверхностью, разбитой на строки и столбцы. Продолжая аналогию со шкафом, вы можете приравнять каждую таблицу к папке в шкафу. Таблицы содержат все записи определенного типа. Папка в шкафу содержит документы определенного типа (например, все счета определенного поставщика), так и таблица в базе данных содержит все записи определенного типа продолжение #
160 Глава 5. Проектирование баз данных Таблица 5.5 {продолжение) Термин Определение Столбец (Column) Первичный ключ (Primary key) Строка (Row) Соответствует отдельному объекту данных реального мира. Это может быть счет, снятие средств со счета или запись телефонной книги. Строки — «кости и плоть» базы данных. Иногда строка называется запись. В этой книге данные термины используются взаимозаменяемо. В простейшем случае базы данных и таблицы-механизмы для организации строк. Следуя аналогии со шкафом для папок, строка — это документ внутри папки в шкафу. Если бы папка называлась Счета, мы могли бы ожидать, что каждый элемент в ней — некоторый счет Элемент внутри строки. Столбец представляет собой характеристику объекта, представленного записью в таблице. Столбцы часто называются полями, однако сторонники чистоты SQL предпочитают название столбец. В этой книге оба термина используются взаимозаменяемо. Примером столбца может служить столбец Address в таблице Customer. Сам по себе адрес неоднозначен. В контексте таблицы тем не менее этот столбец описывает адрес клиента Столбец или набор столбцов в таблице, который уникально определяет каждую запись. Примером первичного ключа может служить столбец InvoiceNo в таблице Invoice. Номер счета в каждой записи уникален для этой записи и не встречается ни в какой другой. Если вы сохраните значение столбца InvoiceNo для какой-то записи и переместитесь дальше по таблице, вы всегда сможете вернуться к этой записи, используя в качестве ключа только номер счета. По столбцам, составляющим первичный ключ, обычно строится индекс для быстрого доступа к записям Внешний ключ Столбец или набор столбцов, который наследуется из другой таблицы. Обычно (Foreign key) наследуемый ключ является первичным ключом связанной таблицы. Внешний ключ может быть частью первичного ключа таблицы, в которой он расположен, а может и не быть. Обычно он не является частью первичного ключа. Возвращаясь к примеру таблицы Invoice, столбец CustomerNo в таблице Invoice может быть внешним ключом, который наследуется из таблицы Customer. Поле CustomerNo не может быть первичным ключом таблицы Invoice, потому что оно не идентифицирует уникально отдельные записи '? счетов (invoice) — для одного клиента может быть несколько счетов. Однако из-за того, что номера клиентов, хранимые в таблице Invoice, должны быть правильными, значения, сохраняемые в столбце CustomerNo, должны быть сверены со значениями в таблице Customer. Таким образом, столбец CustomerNo в таблице Invoice является внешним ключом, который реляционно связан с первичным ключом таблицы Customer Альтернативный Столбец или набор столбцов в таблице, который уникально определяет ее ключ записи. Альтернативные ключи также называются уникальными ключами. (Candidate key) Первичный ключ таблицы выбирается из ее альтернативных ключей Ограничение Механизм, используемый для того, чтобы гарантировать внесение в таблицу (Constraint) только правильных данных. Существует два основных типа ограничений: ограничения ссылочной и доменной целостности. Ограничения ссылочной целостности гарантируют, что связи между таблицами не будут нарушены. Ограничения доменной целостности гарантируют, что значения несовместимых типов, значения выходящие за границы или неправильные по другим причинам, не попадут в базу данных Представление Логическое представление подмножества данных таблицы. Представления (View) сами по себе не содержат данных. Это запросы SQL, к которым можно адресовать запросы, как если бы они были таблицами. Представление обычно предоставляет доступ к подмножеству столбцов или записей или к тому и другому. С помощью представления можно реализовать ограничения на типы модификации данных, допустимые для таблиц, на которых основано представление
Реляционное моделирование данных 161 Термин Определение Триггер (Trigger) Специальный тип хранимой процедуры, который выполняется, когда некоторый оператор SQL выполняется для таблицы. Триггеры могут использоваться для обеспечения ссылочной или доменной целостности Теперь давайте продолжим обсуждение проектирования баз данных. Вам предстоит превратить E-R-диаграмму, которую вы создали ранее, в полноценную реляционную модель. Конечным результатом вашей работы будет сценарий Transact-SQL, который вы сможете использовать для создания объектов базы данных. От E-R-диаграммы к реляционной модели Загрузите вашу E-R-диаграмму в инструмент реляционного моделирования (часто это тот же самый инструмент или часть того же пакета). Первое, что вы должны указать для вашей новой модели, — СУБД, которая будет использоваться. Предполагая, что ваш инструмент поддерживает ее, выберите SQL Server и версию, с которой вы будете работать, — например, SQL Server 7.0 или SQL Server 2000 (в некоторых инструментах SQL Server 2000 может называться SQL Server 8.0). На рис. 5.21 показано, как может выглядеть модель, после того как она загружена. *.;S SILVEflRUN-RDM - ЦП ■ RentMan System E-fl Oiagiam lot lease Process 1.0 IERX->BDM)I >..МЯДИЯЯ!.."™".. | Property Number Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Property Range LastLawnDate GasHeat CentralAir Lasts pray Date CentralHeart Refigerator Dishwasher Rent SchoolDistnct Deposit Bedrooms GarageType City Bathrooms LivmgAreas Zip State Address Lease Number Lease BegmDate Lease EndDate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PetDeposit Lease RentDueDay Lease LawnService —r— -1,1": 0,N I TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCrty Tenant Employer State Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Рис. 5.21. Так может выглядеть реляционная модель в начале
162 Глава 5. Проектирование баз данных Создание словаря данных Первым делом вы должны создать словарь данных. Множество инструментов для реляционного моделирования поддерживают создание некоторых типов словарей данных с помощью доменов. Как вы помните, домен определяет тип данных столбца. Это грубый эквивалент пользовательского типа данных в SQL Server. Независимо от того, используете ли вы CASE-средство, процесс создания словаря данных (также известного как хранилище атрибутов или полей) довольно однообразен. Чтобы создать словарь данных, выполните следующие шаги. 1. Создайте полный список всех столбцов в таблицах вашей модели. Для этого можно не использовать какой-либо инструмент. 2. Создайте определения доменов для тех столбцов, которые содержатся или могут содержаться (например, через ссылки внешних ключей) более, чем в одной таблице. 3. Определите бизнес-правила для каждого домена вашего словаря данных. Бизнес-правила уровня столбца контролируют значения, которые можно поместить в столбец. Вы, например, можете указать, что столбец Rent должен всегда быть ненулевым или что столбец EndDate в таблице Lease должен всегда быть больше, чем столбец Beg i nDate. 4. Примените домены, которые вы определили для столбцов ваших таблиц. Вместо того чтобы основываться на готовых типах данных, поддерживаемых SQL Server, некоторые столбцы будут основаны на доменах. Домены добавят некоторые специальные характеристики, такие как ограничение значений, которые может иметь столбец. Для этого мы используем ваше средство моделирования — чтобы взять домены, которые вы определили, и применить их к столбцам вашей модели. В табл. 5.6 представлены домены, которые необходимы для вашей модели. Добавьте каждый из них в словарь данных или хранилище полей/столбцов вашего средства моделирования. Таблица 5.6. Домены, Название TAddition TAddress TCity TComments TPhone TPropertyNo TRent TRooms TSchoolDistrict TState TTenantNo TYesNo TZip необходимые для словаря данных Базовый тип varchar varchar varchar varchar varchar integer smallmoney tinyint varchar char integer bit char Длина 20 30 30 80 13 N/A N/A N/A 20 2 N/A N/A 10 Количество десятичных знаков N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A Значение по умолчанию Firewheel Никакого Garland Никакого Никакого Никакого 750 Никакого East Garland TX Никакого Никакого 75080
Реляционное моделирование данных 163 ПРИМЕЧАНИЕ Если ваш инструмент поддерживает понятие опциональности или неопределенности, сделайте домен TComments опциональным. Как правило, столбцы с комментариями необязательны. Обратите%нимание на то, что я использовал префикс Т для каждого домена. Здесь Т означает тип (type). Я использую этот префикс — вы можете использовать то, что вам больше подходит. Среди различных элементов традиционных языков программирования домены наиболее близко соответствуют конструкции typedef s (определенные пользователем типы данных в таких языках, как С и C++), поэтому я считаю, что домены похожи на typedef s. На рис. 5.22 показано определение доменов. PROPERTY Property Number Property Range Property LastLawnDate Property GasHeat Property CentralAtr Property LastSprayDate Property CentralHeat Property Refigerator Property Dishwasher i Property Rent Property SchoolDistricf Property Deposit Property Bedrooms Property GarageType Property City Property Bathrooms Property LivingAreas Property Zip Property State Property Address Prnriet-tiH UHHitmn Lease Number Lease BegmDate *т>тхгг1лшвтвашштвямшясйш ■Ae Date Г'ЭПав|Ул!|.(! » ■'stVane ~™ Call Number Call DateTime Call Description : . : | I ; 1 ; ; i ! !,: = : = :„ ::J 1 - 1»Г- l r- Tenant HomePhone Tenant WorftPhone Tenant ICEPhone Рис. 5.22. Можно определить допустимые значения для создаваемых доменов Использование словаря данных Теперь, когда вы закончили формирование хранилища доменов, вы готовы применить их к столбцам таблиц. Вы также должны связать столбцы, которые не основаны на доменах, с основными типами. Используйте возможности вашего инструмента: отредактируйте каждый столбец в модели, чтобы его домен соответствовал домену в табл. 5.7. Повторите этот процесс для всех таблиц вашей модели.
164 Глава 5. Проектирование баз данных Таблица 5.7. Определения столбцов для таблиц реляционной модели Столбец Домен Длина Addition Address Bathrooms Bedrooms BeginDate Call_DateTime Call_Number CentralAir CentralHeat City Comments Deposit Description DishWasher Employer EmployerAddress EmployerCity EmployerState EmployerZip EndDate GarageType GasHeat HomePhone ICEPhone Last Lawn Date LastSprayDate LawnService Lease_Number LivingAreas MovedlnDate MovedOutDate Name PrivacyFence Property_Number Range Refrigerator Rent RentDueDay SchoolDistrict State Tenant_Number WorkPhone Zip TAddition TAddress TRooms TRooms smalldatetime smalldatetime integer TYesNo TYesNo TCity TCom merits TRent varchar TYesNo varchar TAddress TCity TState TZip smalldatetime TRooms TYesNo TPhone TPhone smalldatetime TYesNo TYesNo integer TRooms smalldatetime smalldatetime varchar TYesNo TPropertyNo TYesNo TYesNo TRent tinyint TSchool District TState TTenantNo TPhone TZip 30 30 30
Реляционное моделирование данных 165 Определение размера столбцов Когда определяете размер столбца, думайте об эффективности. Спросите себя: «Какой наименьший тип данных я могу использовать для этого столбца, чтобы он мог хранить наибольшее значение, которое он когда-либо сможет иметь?» Вот несколько общих рекомендаций для эффективного определения размеров столбцов. ■ Если в поле никогда не будет храниться значение больше 255, используйте тип tiny int. Если столбец может иметь значения больше 255, но меньше 32,767, используйте smal I int. Используйте, по возможности, наименьший целый тип. ■ Используйте целые типы вместо типов с плавающей запятой, когда вы определяете числовые столбцы, в которых нет необходимости хранить цифры, находящиеся справа от запятой. ■ Используйте символьные типы с переменной длиной вместо символьных типов с фиксированной длиной, если длина данных в столбце может меняться от записи к записи. ■ Используйте «уменьшенные» версии типов datetime и money (smal Idatetime и sma I I money соответственно), если вас устроит потеря точности. ■ Для определения булевых столбцов используйте тип данных b 11 вместо i nt или charA). Описание вашего проекта Описание элементов вашей модели с использованием комментариев — полезная и заслуживающая внимания привычка. В некоторых инструментах комментирование в моделях данных даже не является опциональным. Хорошие инструменты моделирования позволяют прикреплять комментарии к любому элементу модели. Часто эти комментарии включаются в SQL, который можно сгенерировать с помощью вашего инструмента, если используемая вами платформа это поддерживает. Добавьте комментарии для описания объектов вашей модели, как показано на рис. 5.23. Обратите внимание, что большинство инструментов позволяют также добавлять комментарии к отдельным столбцам. Это показано на рис. 5.24. Описание вашей модели подобным образом поможет и вам, и другим программистам, работающим вместе с вами над проектом, лучше понять, что вы делаете. Генерация внешних ключей Приближаясь к завершению работы, вы должны сгенерировать определения внешних ключей в вашей модели. Внешние ключи — это физическая реализация связей между сущностями, которые были созданы в фазе E-R-моделирования. В результате создания внешних ключей необходимые столбцы будут размножены между таблицами. Обычно одна таблица наследует первичный ключ другой. Наследуемый столбец или набор столбцов становится внешним ключом в связанной таблице.
166 Глава 5. Проектирование баз данных PPOPERTr Г lEASE 1 Property Number Property Range Property LastLawnDate Property GasHeat Property CentralAir Property LastSprayDate Property CerrtralHeat ■ Property Refigerator Property Dishwasher Л Property Rent Property ScboolDistrict Property Deposit Property Bedrooms Properly GarageType Property City Property Bathrooms Property LivingAreas Property Zip Property State Property Address This is the RentMan System PROPERTY table properties we rent out It contains information (or the Lease Number Lease BegmDate Leass EndDate lovedlnDate ovedOutDate srrt stDeposit sntDueDay' awnService si J Mes% 0.1 Call Number Call DateTime Call Description /er тетшгвгсжрпг/ег Address Tenant EmployerCity Tenant EmployerState Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tendnt ICEPhone Рис. 5.23. Опишите таблицы в модели при помощи комментариев ^ШМЖШЖШ1»ШЖШШШ»1Ш1Ш*Ш««Ж1ШШШШ:ИШШ^ЙШШ |s| Щъ Щ g-esmtatiwi Schema _i^!-£«£^ J» fej™ M* ■ft*- 1 PROPERTY PtoperW Number Property Range Property LastLawnDate Property GasHeat Property CentralAir Property LastSprayDate Property CerrtralHeat Property Retigerator Property Dishwasher Property Rent Й Property SchoolDistrict Property Deposit Property Bedrooms Property GarageType Property City Property Bathrooms Property LivingAreas Property Zip Property State Property Address Property Addition Property PrivacyFence LEASE Lease Number Lease BeymDate Lease EndDate loveslnDdie JOuiDdte J. ОЯмя» *■ ш&щт*^щ$и)Ивт к! <1>1й| c**sil 1 ж ..} it'iiiiiumtnnHiiHtiiT HHHHimnmitir )нщ|М1МИИ'№И M \\ Ц '■'f- ■■ ,i.y.r j-; .> ; This is the primary key lor the PROPERTY table *i z! posit ueDay Service Call DateTime Call Descricrtion address ity Tenant EmployerState Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Рис. 5.24. Добавьте аннотацию к столбцам модели при помощи комментариев
Реляционное моделирование данных 167 Используя метод вашего инструмента, сгенерируйте внешние ключи для таблиц модели. Вы увидите множество новых столбцов, добавленных в ваши таблицы. Обычно инструменты моделирования добавляют к этим столбцам префикс FK, означающий, что это внешние ключи. Посмотрите на рис. 5.25. S1LVERRUN-BDM - (A) FK НспМап 5цме удивит :ess 1 О IERX >HDM|) Леш LEASE -ea^e lv.,r,.x, Lease BegmDate Lease EndOate Lease MovedlnDate Lease MovedOutDate Lease Rent Lease PatDeposit Lease RentDueDay Lease LawnService Lease Comments FK Property Number FK Tenant Number , r_p TENANT Tenant Number Tenant Name Tenant Employer Tenant Employer Address Tenant EmployerCity Tenant EmployerState Tenant Employer Zip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Хжми* ^ww.prt.?-. d Рис. 5.25. Логическая модель с внешними ключами Теперь ваша модель в основном закончена, и вы готовы сгенерировать сценарий Transact-SQL, необходимый для создания объектов, определенных в вашей модели. Проверка целостности модели Прежде чем сгенерировать Transact-SQL для создания объектов базы данных, вы должны проверить целостность модели. Большинство инструментов включают функции, которые помогут вам в этом. Если ваш инструмент имеет такую функцию, запустите ее сейчас, чтобы проверить целостность модели. Такая проверка избавит вас от необходимости удалять и пересоздавать объекты базы данных, если они были определены неправильно. Хорошие инструменты моделирования проверяют массу аспектов модели и предоставляют хорошую возможность проверить, можете ли вы приступить к генерации DDL. Генерация DDL Вы закончили модель и готовы приступить к генерации операторов DDL, необходимых для физической реализации. Если ваш инструмент поддерживает генера-
168 Глава 5, Проектирование баз данных цию DDL-сценариев для создания объектов модели, используйте эту возможность, чтобы сгенерировать сценарий T-SQL для создания объектов базы данных. В листинге 5.1 приведен сценарий SQL, похожий на тот, что вы создадите с помощью своего инструмента. Поскольку ранее вы выбрали в качестве платформы SQL Server, сгенерированный сценарий DDL состоит из операторов Transact-SQL. Листинг 5.1. Пример кода Transact-SQL, который можно сгенерировать по вашей модели USE master GO IF DBJDC rentman') IS NOT NULL DROP DATABASE rentman GO CREATE DATABASE rentman GO USE rentman GO IF (DB_NAME()<>'rentman') BEGIN RAISERRORCDatabase create failed - aborting script'. 20.1) WITH LOG RETURN END GO CREATE RULE RAddition AS lvalue IN ('Deerfield', 'Firewheel', 'Legacy Hills'. 'Switzerland Estates'. 'Sherwood'. 'Rockknoll') GO CREATE DEFAULT DAddition AS 'Firewheel' GO EXEC sp_addtype TAddition . 'varcharB0) '. 'NOT NULL' EXEC sp_bindrule RAddition, TAddition EXEC sp_bindefault DAddition. TAddition GO EXEC sp_addtype TAddress , 'varcharC0)', 'NOT NULL' GO CREATE RULE RCity AS ©value IN ('Oklahoma City'. 'Norman', 'Edmond', 'Dallas'. 'Garland', 'Piano') GO CREATE DEFAULT DCity AS 'Garland' GO EXEC sp_addtype TCity , WarcharOO) '. 'NOT NULL' EXEC sp_bindrule RCity, TCity EXEC sp_bindefault DCity, TCity . . . GO • ■' " ' EXEC sp^addtype TComments , 'varchar(80)'. 'NULL' EXEC sp_addtype TPhone , 'varcharA2)' . 'NOT NULL' EXEC sp_addtype TPropertyNo . 'int', 'NOT NULL' GO CREATE DEFAULT DRent AS 750 GO EXEC sp_addtype TRent , 'smallmoney', 'NOT NULL' EXEC spjiindefault DRent, TRent GO CREATE RULE RRooms AS @value IN @. 1. 2. 3, 4, 5) GO EXEC sp_addtype TRooms , 'tinyint', 'NOT NULL' EXEC sp_bindrule RRooms, TRooms GO CREATE RULE RSchoolDistrict AS @value IN ('Putnam City'. 'Oklahoma City', 'Richardson', 'Edmond', 'East Garland'. 'Dallas'. 'Piano')
Реляционное моделирование данных 169 GO CREATE DEFAULT DSchoolDistrict AS 'East Garland' GO EXEC sp_addtype TSchoolDistrict , 'varcharB0) ', 'NOT NULL' EXEC sp_bindrule RSchoolDistrict, TSchoolDistrict EXEC sp_bindefault DSchoolDistrict. TSchoolDistrict GO CREATE RULE RState AS @value IN СОК'. 'ТХ') GO CREATE DEFAULT DState AS 'TX' GO EXEC sp_addtype TState . 'charB)', 'NOT NULL' EXEC spjnndrule RState, TState EXEC sp_bindefault DState, TState GO EXEC sp_addtype TTenantNo , 'int'. 'NOT NULL' EXEC sp_addtype TYesNo , 'bit', 'NOT NULL' GO CREATE DEFAULT DZip AS 75080' GO EXEC sp_addtype TZip , 'varchar(lO)', 'NOT NULL' EXEC spj)indefault DZip. TZip GO CREATE TABLE PROPERTY ( Property_Nurnber Address City State Zip Addition SchoolDistrict Rent Deposit LivingAreas BedRooms BathRooms GarageType Central Air CentralHeat GasHeat Refigerator Range Dishwasher PrivacyFence LastLawnDate LastSprayDate PRIMARY KEY (Propertyjumber) ) GO CREATE TABLE TENANT ( Tenant_Number Name Employer ErnployerAddress ErnployerCity EmployerState EmployerZip TPropertyNo NOT NULL, TAddress NOT NULL. TCity NOT NULL, TState NOT NULL. TZip NOT NULL, TAddition NOT NULL. TSchoolDistrict NOT NULL. TRent NOT NULL, small money NOT NULL. TRooms NOT NULL. TRooms NOT NULL, TRooms NOT NULL, TRooms NOT NULL, TYesNo NOT NULL, TYesNo NOT NULL. TYesNo NOT NULL. TYesNo NOT NULL. TYesNo NOT NULL, TYesNo NOT NULL. TYesNo NOT NULL, smalldatetime NOT NULL, TYesNo NOT NULL, TTenantNo NOT NULL, . ■ varcharC0) NOT NULL, varcharOO) NOT NULL, TAddress NOT NULL, TCity NOT NULL, TState NOT NULL. Л- >■. TZip NOT null, продолжение^
170 Глава 5. Проектирование баз данных Листинг 5.1 {продолжение) HomePhone TPhone NOT NULL, WorkPhone TPhone NOT NULL. ICEPhone TPhone NOT NULL. Comments TComments NULL. PRIMARY KEY (Tenantjumber) ) GO CREATE TABLE CALL ( Calljumber int NOT NULL. Call_DateTime smalldatetime NOT NULL. Description varcharC0) NOT NULL. Property_Number TPropertyNo NULL, PRIMARY KEY (Cal ljumber). CONSTRAINT FK_PR0PERTY1 FOREIGN KEY (Property_Number) REFERENCES PROPERTY ) GO CREATE TABLE LEASE ( " Lease_Number BeginDate EndDate MovedlnDate MovedOutDate Rent TRent PetDeposit RentDueDay LawnService Comments int NOT NULL, smalldatetime NOT NULL smalldatetime NOT NULL smalldatetime NOT NULL smalldatetime NOT NULL NOT NULL, smallmoney NOT NULL, tinyint NOT NULL. TYesNo NOT NULL, TComments NULL, Property_Number TPropertyNo NOT NULL, Tenantjumber TTenantNo NOT NULL, PRIMARY KEY (Leasejumber). CONSTRAINT FK_PR0PERTY2 FOREIGN KEY (Property_Number) REFERENCES PROPERTY, CONSTRAINT FK_TENANT3 FOREIGN KEY (Tenantjumber) REFERENCES TENANT ) GO Обратите внимание, что сценарий начинается с создания логических доменов, которые вы определили с помощью пользовательских типов данных SQL Server. Затем эти типы данных используются при определении таблиц. Это хорошая техника, потому что она инкапсулирует ваши бизнес-правила, как только это возможно, в типы данных, которые можно использовать повторно. Если понадобится, вы можете использовать типы данных для создания новых столбцов. Например, если вы хотите добавить еще один столбец, который может принимать только значения 1 или 0, вы можете повторно использовать для этого тип TYesNo. В этом состоит преимущество внедрения ваших бизнес-правил в типы данных, а не напрямую в столбцы. Один недостаток данного подхода в том, что здесь используются несколько устаревшие конструкции для обеспечения доменной целостности, а именно: объекты RULE и DEFAULT. Современный лучший метод состоит в использовании ограниче-
Реляционное моделирование данных 171 ний CHECK и DEFAULT таблиц/столбцов. Хорошие инструменты моделирования позволяют генерировать Transact-SQL DDL двумя способами: используя объекты RULE и DEFAULT или стандартные ограничения ANSI. Подход с использованием ограничений на самом деле гибче, потому что вы можете использовать несколько столбцов в СНЕСК-ограничении (например, EndDate должна быть равна или больше Beg i nDate). Вы не можете сделать этого, используя объекты RULE. В листинге 5.2 показан сценарий, сгенерированный с использованием ограничений для обеспечения доменной целостности. Листинг 5.2. Сценарий, сгенерированный при помощи ограничений вместо объектов DEFAULT/RULE USE master GO IF DBJDC rentman') IS NOT NULL DROP DATABASE rentman GO CREATE DATABASE rentman GO USE rentman GO IF (DBJAMEO<>' rentman') BEGIN RAISERRORC'Database create failed - aborting script', 20.1) WITH LOG RETURN END GO EXEC sp_addtype TAddition . 'varcharB0)', 'NOT NULL' EXEC sp_ac!dtype TAddress , 'varcharC0)', 'NOT NULL' EXEC sp_addtype TCity . 'varcharOO)'. 'NOT NULL' EXEC sp_addtype TComments . 'varchar(80)'. 'NULL' EXEC sp^addtype TPhone , 'varcharA2)', 'NOT NULL' EXEC sp_addtype TPropertyNo , 'inf. 'NOT NULL' EXEC sp_addtype TRent . 'smallmoney', 'NOT NULL' EXEC sp_addtype TRooms . 'tinyint', 'NOT NULL' EXEC sp_addtype TSchoolDistrict . 'varcharB0) '. 'NOT NULL' EXEC sp_addtype TState , 'charB)\ 'NOT NULL' EXEC sp_addtype TTenantNo . 'inf. 'NOT NULL' EXEC sp_addtype TYesNo , 'bit'. 'NOT NULL' EXEC sp_addtype TZip . 'varchar(lO)'. 'NOT NULL' GO CREATE TABLE PROPERTY ( Property_Number TPropertyNo NOT NULL, Address TAddress NOT NULL, City TCity NOT NULL, State TState NOT NULL DEFAULT 'TX' CHECK (State IN СОК' 'TX')), Zip TZip NOT NULL DEFAULT 75080'. Addition TAddition NOT NULL DEFAULT 'Firewheel' CHECK (Addition IN ('Deerfield'. 'Firewheel', 'Legacy Hills', 'Switzerland Estates', 'Sherwood', 'Rockknoll')), School District TSchoolDistrict NOT NULL DEFAULT 'East Garland' CHECK (SchoolDisthct IN ('Putnam City', 'Oklahoma City', 'Richardson', 'Edmond', 'East Garland', 'Dallas', 'Piano')). Rent TRent NOT NULL DEFAULT 750. Deposit TRent NOT NULL DEFAULT 750, продолжение^
172 Глава 5. Проектирование баз данных Листинг 5.2 {продолжение) LivingAreas BedRooms BathRoorns GarageType Central Air CentralHeat GasHeat Refigerator Range Dishwasher PrivacyFence LastLawnDate LastSprayDate PRIMARY KEY (Property TRooms NOT NULL CHECK (LivingAreas BETWEEN 0 AND 5) TRooms NOT NULL CHECK (BedRooms BETWEEN 0 AND 5). ' TRooms NOT NULL CHECK (BathRoorns BETWEEN 0 AND 5), TRooms NOT NULL CHECK (GarageType BETWEEN 0 AND 5). TYesNo NOT NULL. TYesNo NOT NULL. TYesNo NOT NULL, TYesNo NOT NULL, TYesNo NOT NULL, TYesNo NOT NULL, TYesNo NOT NULL, smalldatetime NOT NULL, TYesNo NOT NULL, Number) GO CREATE TABLE TENANT ( Tenantjlumber Name Employer EmployerAddress EmployerCity EmployerState EmployerZip HomePhone WorkPhone ICEPhone Comments PRIMARY KEY (Tenant, GO TTenantNo NOT NULL, varcharC0) NOT NULL, varcharC0) NOT NULL, TAddress NOT NULL, TCity NOT NULL DEFAULT 'Garland TState NOT NULL DEFAULT 'TX'. TZip NOT NULL DEFAULT '75080', TPhone NOT NULL, TPhone NOT NULL, TPhone NOT NULL. TComments NULL, _Number) CREATE TABLE CALL ( Calljumber Call_DateTime Description Property__Number PRIMARY KEY (Calljumber), CONSTRAINT FK_PR0PERTY1 FOREIGN KEY (Property Number) REFERENCES PROPERTY int NOT NULL, smalldatetime NOT NULL, varcharC0) NOT NULL, TPropertyNo NULL, GO CREATE ( TABLE LEASE Lease_Number BeginDate EndDate MovedlnDate MovedOutDate Rent Deposit RentDueDay LawnService Comments int NOT NULL. smalldatetime NOT NULL, smalldatetime NOT NULL, smalldatetime NOT NULL, smalldatetime NOT NULL, TRent NOT NULL DEFAULT 750, TRent NOT NULL DEFAULT 750, tinyint NOT NULL CHECK (RentDueDay BETWEEN 1 AND TYesNo NOT NULL. TComments NULL. 15). Property_Number TPropertyNo NOT NULL.
Реляционное моделирование данных 173 TenantJJumber TTenantNo NOT NULL. PRIMARY KEY (Lease_Number). CONSTRAINT FK_PR0PERTY2 FOREIGN KEY (PropertyJJumber) REFERENCES PROPERTY. CONSTRAINT FK_TENANT3 FOREIGN KEY (TenantJJumber) REFERENCES TENANT. CONSTRAINT CK_ENDDATE4 CHECK (EndOate >= BeginDate). CONSTRAINT CKJWE0UTDATE5 CHECK (MovedOutOate >= MovedlnDate) ) GO Ваша модель в основном закончена. Если ваш инструмент позволяет, перед сохранением введите описание схемы базы данных. Это проиллюстрировано на рис. 5.26. jziam ЫЗШ. л Pfouerty Number Property Property Property Property Property Property Property Property Property ^Property Property Property Property Property Property Property Property Property Property Property Property Address City State Zip Addition SchoolDistrid Rent Deposit LivmgAreas Bedrooms Bathrooms GarageType CentralAir CentralHeat GasHeat Retigerator Range Disrwvasher PrivacyFence LastLawnDate LastSprayDate Lease Number П) ScheM De«ciiptiofi £anceJ Pioject J RentM«> System jSQL Server рсГ*^ УтЫ jl 0(ERX->RDM1 jRelationa'Data Model for Le ДЫ ^ j Owner j ■ftsHod Lease BeginDate Lease EndDate Lease MovedlnDate ovedQutDate snt [Deposit sntDueDay JwnService Dmments ;rty Number it Number CafeslNBiwe jc \data\rentman\ren jKen Henderson Year Conc*oL able lolurnn ;onreclor Section rnoice УУйэЬег; A 49 3 6 0 d _i! N*r, «j Ома**' .->•'. 0.1 0_ CALL Call Number Call DateTime Call Description /er yer Address Ten* Tenant EmployerCrty Tenant EmployerState Tenant EmployerZip Tenant HomePhone Tenant WorkPhone Tenant ICEPhone Рис. 5.26. Использование диалога Описание схемы для указания названия модели Диаграммы баз данных в Enterprise Manager Несмотря на то что средство для создания диаграмм баз данных в Enterprise Manager не является полнофункциональным средством моделирования, в нем есть некоторые основные функции для физического моделирования баз данных. Используя мастер Create New Database Diagram, вы можете импортировать существующую фи-
174 Глава 5. Проектирование баз данных зическую модель в диаграмму и работать с ней, используя средство Database Diagram. Оно предлагается бесплатно и оно довольно простое, так что имеет смысл использовать его для физического моделирования, если вам достаточно его простых возможностей. На рис. 5.27 показана база данных RENTMAN, загруженная в средство Database Diagram Enterprise Manager. *J £" t* "V f 2 S '* -' * "W Щ & ~" >1 il 4 Я1ПИаНЯНПш Property _Number ». Address City State Zto Addition Schoo!Distnct Rent Deposit UvirigAreas Bedrooms Bathrooms ■¥■ tfj Lease Number Z BegmDate EndDate MovedlnDate MovedOutDate Rent Deposit RentDueDay LawnService Comments Property .Number Tenant Number :^JC3« ЫитЬег^^^^П '■■Щ Call J)ate Time 1 .>::■;:] Description | ■■S:|:|! Property JVumber 1 Tenant Number Name Employer Employe» Address EmployerCity Employ erState Employer Zip HomePhone WorkPhone ICEPhone Commenr> Рис. 5.27. Enterprise Manager содержит простое средство физического моделирования Итоги В этой главе вы узнали: ■ о взаимодействии моделирования бизнес-процессов и E-R-моделирования в реляционном моделировании; и как сконструировать три разные модели, представляющие три различных аспекта приложения, и требования к базе данных приложения.
Создание тестовых данных Работая над проектом, я могу думать очень быстро, но мысли мои будут полны просчетов. А. Кокберн' Анализируя примеры этой книги и оптимизируя свой код, вы, в конце концов, придете к необходимости создания большого количества тестовых данных. Существует несколько способов создания тестовых данных. Одни хранят тестовые данные во внешних файлах, которые копируются при помощи ВСР на сервер, другие хранят тестовые базы или тестовые таблицы вместе с рабочими файлами на случай необходимости быстрого использования большого объема информации. Третьи — каждый раз создают новые тестовые данные. В этой главе мы поговорим о некоторых способах создания больших объемов данных. Существуют независимые программы для создания тестовых данных для SQL Servera, но простую тестовую базу почти любого объема можно создать самостоятельно с помощью простых запросов. Подходы к созданию данных Две главные составляющие процесса создания тестовых данных — уникальность/ случайность данных и время, потраченное на их создание. Лучшим вариантом является база со случайными данными, созданная в короткие сроки. Причем желательно, чтобы это была как можно более случайная или уникальная информация, созданная за очень короткое время. Как правило, эти две переменные обратно пропорциональны, то есть, увеличивая одну, вы уменьшаете другую. Повышение случайности данных ведет к увеличению времени на создание. Аналогично, уменьшая время на создание данных, снижается уникальность. Несмотря на то что в примерах, которые мы рассмотрим, в основном создаются данные, состоящие из 100 записей, их количество можно расширить почти до любого числа. Я намеренно не привожу примеров, основанных на цикле по значению переменной, в котором добавляется одна запись. Каждая вставленная запись до- Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 67.
176 Глава 6. Создание тестовых данных бавляет запись в журнал транзакций, что негативно влияет на него. Создание данных получается очень медленным, требует много ресурсов и не подходит для создания очень большого числа записей. Перекрестное объединение Первый способ, который я покажу, называется перекрестным объединением (cross join). Он использует возможность SQL возвращать декартово произведение (результат умножения одной таблицы на другую). Посмотрим пример. CREATE TABLE #list . (id int identity) INSERT #1 INSERT #list DEFAULT VALUES INSERT #1 st DEFAULT VALUES st DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #list DEFAULT VALUES SELECT * FROM #list LI CROSS JOIN #1 ist L2 GO DROP TABLE #list (Результаты сокращены) id id . :. 1 1 2 1 3 1 4 1 5 1 6 1 6 10 7 1Q 8 10 9 10 10 10 A00 row(s) affected) Ключевой является строка с оператором SELECT (выделенная жирным шрифтом). Предполагается, что таблица, из которой мы выбираем, уже существует. Можно увеличить число перекрестных объединений, чтобы увеличить число получаемых записей. Например, чтобы создать 1000 записей вместо 100 в предыдущем примере, следует просто добавить еще один CROSS JO IN с таблицей #L i st. С помощью экспоненциального умножения можно быстро создать большие объемы данных. Этот способ хорошо работает для уже существующих данных, но что делать, если мы захотим создать что-то новое в дополнение к тому, что уже есть в таблице? Далее вы видите вариант первого способа, который создает абсолютно новые данные.
Подходы к созданию данных 177 CREATE TABLE #list (id int identity) INSERT #list DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT #11st DEFAULT VALUES INSERT flist DEFAULT VALUES INSERT #11st DEFAULT VALUES SELECT IDENTITYdnt. 1.1) AS Id, RAND(Ll.id) AS RNum INTO #11st2 FROM flist LI CROSS JOIN flist L2 SELECT * FROM #list2 GO DROP TABLE #11st.#11st2 (Результаты сокращены) Id RNum 1 0.71359199321292355 2 0.71359199321292355 3 0.71359199321292355 4 0.71359199321292355 5 0.71359199321292355 6 0.71359199321292355 96 0.71375968995424732 97 0.71375968995424732 98 0.71375968995424732 99 0.71375968995424732 100 0.71375968995424732 A00 row(s) affected) Здесь мы использовали функцию IDENTITYQ, чтобы создать последовательное целое для первого столбца. Обратите внимание на почти случайные значения столбца RNum. Естественно, они не абсолютно случайные, но, используя функцию RAND() и передавая ей значение столбца из перекрестного объединения, получается неплохая смесь. Далее в примере вы видите, что данные перемешаны немного лучше. CREATE TABLE fist (id int identity) INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT f i St DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES SELECT IDENTITYCint, 1,1) AS Id, RAND(CASE WHEN LI,id «2=0 THEN LI.id
178 Глава 6. Создание тестовых данных ELSE L2.id END) AS RNum INTO #list2 FROM #11st LI CROSS JOIN #11st L2 SELECT * FROM #list2 GO DROP TABLE #list.#list2 (Результаты сокращены) Id RNum 1 0.71359199321292355 2 0.7136106261841817 3 0.71359199321292355 4 0.7136478921266981 5 0.71359199321292355 6 0.71368515806921451 96 0.71368515806921451 97 -0.71375968995424732 98 0.71372242401173092 99 0.71375968995424732 100 0.71375968995424732 A00 row(s) affected) Как можно заметить, данные все еще не слишком разнообразны, но, по крайней мере, они достаточно случайны. Можно ли перемешать их еще лучше? Можем ли мы создать более разнообразные данные за то же время? Конечно. Взгляните на следующий вариант. CREATE TABLE #11st (Id int identity) INSERT flist DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT flist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES SELECT IDENTITYCInt. 1.1) AS Id INTO #11st2 FROM fist LI CROSS JOIN fist L2 SELECT Id. RAND(Id)*10000000000000 RNum FROM #11st2 GO DROP TABLE f ist.fist2 (Результаты) Id RNum 1 7135919932129.2354 2 7136106261841.8174 3 7136292591554.3994
Подходы к созданию данных 179 4 7136478921266.9814 5 7136665250979.5635 6 7136851580692.1455 96 7153621254В24.5244 97 7153807584537.1074 98 7153993914249.6885 99 71541В0243962.2715 100 7154366573674.8525 A00 row(s) affected) Здесь мы использовали последовательное число, которое создали с использованием функции IDENTITY() в качестве базового значения для функции RANDQ. Это, конечно, обеспечивает новое базовое число для каждого вызова функции и приводит к появлению случайного числа в каждой строке временной таблицы. К тому же мы умножаем это случайное число на 10 триллионов, чтобы получить положительное действительное число от 1 до 10 триллионов. Единственная проблема в этом подходе — полученные случайные числа расположены по порядку. Можно изменить запрос, добавив сортировку по столбцу RNum: SELECT Id, RAND(Id)*10000000000000 RNum FROM #list2 ORDER BY RNum Но все равно порядок строк в результате не изменится. Это типичный недостаток генератора случайных чисел. Поскольку он использует формулу для генерации значений, можно подумать, что он создает повторяющийся набор данных с использованием одинаковых или подобных входных данных. В нашем случае данные имеют одинаковую связь: значение каждого I d на единицу больше предшествующего. Чтобы избавиться от этого, необходимо создать свой собственный генератор случайных величин. Рассмотрим данную возможность в следующем разделе. Набор случайных данных Чтобы создать набор случайных данных (RANDOM()), можно применить пользовательские функции в дополнение к встроенным функциям SQL Server. Приведу измененный вариант предыдущего примера: USE tempdb GO SET N0C0UNT ON GO DROP FUNCTION dbo.Random GO CREATE FUNCTION dbo.Random(CSeed int) RETURNS int AS /* */ BEGIN DECLARE @MM int. @AA int. @QQ int, @RR int. @MMM int, @AAA int, ?QQQ int. @RRR int, ^Result int, @X decimalC8.0), @Y decimalC8,0) SELECT @MM=2147483647. @AA=48271. «10=44488, ?RR=3399, (ЭМММ=2147483399, (ЭААА=40692. 0000=52774, @RRR=3791 SET @X=@M*(@SeedS;@QQ)-@RR*CAST(((aSeed/(aQQ) AS int) IF (@X<0) SET @X=(aX+(aMM SET @Y=(aAAA*((aSeed3;(aQQQ)-(aRRR*CAST(((aSeed/(aQQQ) AS int) IF (@Y<0) SET @Y=@Y+@MMM SET @Result=(aX-(aY
180 Глава 6. Создание тестовых данных IF (CResult<=0) SET @Result=@Result+@MM RETURN(OResult) END GO DROP FUNCTION dbo.ScrambleFloat GO CREATE FUNCTION dbo.ScrambleFloat(CFloat float. @Seed float) RETURNS float AS BEGIN DECLARE @VFloat as varbinary(8). WSFloat int. (aReturn float SET (aVFloat=CAST(CFloat as varbinary(8)) SET @Return=dbo.Random(@Seed)* ((CAST(CAST(SUBSTRING(CVFloat.5.1) AS int) AS float) * (CAST(SUBSTRING«aVFloat.7.1) AS int)) / ISNULL(NULLIF(CAST(SUBSTRING(CVFloat.6,l) AS int).0).D) + CAST(SUBSTRING(CVFloat.8.1) AS int) - (CAST(SUBSTRING(CVFloat.3,l) AS int) % ISNULL(NULLIF(CAST(SUBSTRING(CVFloat,l.l) AS int).0).l))+ (CAST(SUBSTRING((aVFloat.2.1) AS int) * CASE WHEN CAST(SUBSTRING(CVFloat.4.1) AS int) % 2 = 0 THEN 1 ELSE -1 END)) RETURN(@Return) END GO CREATE TABLE #list (id int identity) INSERT flist DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT flist DEFAULT VALUES INSERT fist DEFAULT VALUES " ; .,•.-. INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES INSERT fist DEFAULT VALUES SELECT IDENTITY(int. 1.1) AS Id INTO #list2 FROM fist LI CROSS JOIN fist L2 SELECT Id. dbo.Random(Id) As Random. dbo.ScrambleFloatdd. DATEPART(ms, GETDATEO)) AS Scrambled FROM fist2 ORDER BY Scrambled GO DROP TABLE #1ist.fT (Результаты) Id Random 29 " 219791 44 333476 13 98527 28 212212 74 560846 ist2 Scrambled -722564027.05882347 -673420171.23333335 -574568537.4000001 -544849779.53631282 -523678459.07142854
Подходы к созданию данных 181 98 96 79 41 25 81 53 27 55 742742 727584 598741 310739 189475 613899 401687 204633 416845 -507777842.00000006 -477779538.77049184 3028523768.1111112 3259160366.647059 3645254956.2000003 3701346966.7777781 4101890347.4999995 4471710800.7000008 4934363189.25 Этот код имеет несколько интересных особенностей. Во-первых, мы получаем случайный порядок строк (он, конечно, не совсем произвольный, но, по крайней мере, не соответствует первоначальному порядку столбца Id). Во-вторых, мы применяем пользовательскую функцию Randora(), причем как в функции SCRAMBLEFLOAT(), так и для возвращения столбца при выводе результатов. Для создания случайного целого используется алгоритм, предложенный в трехтомнике Дональда Кнута «Искусство программирования» (Donald Knuth, «The Art of Computer Programming»). SCRAMBLEFLOAT() используется из-за того, что функции, определенные пользователем (UDF), не могут использовать недетерминистические функции, такие как встроенная функция T-SQL RAND(). Функцию SCRAMBLEFLOATQ стоит обсудить отдельно. Она разделяет значение с плавающей запятой на части, используя функции для работы со строками, и создает на их основе новое значение, используя базовое значение для усиления влияния на случайный характер этого процесса. Сначала она преобразует входное значение к varbinary(8), затем использует SUBSTRI NG(), чтобы разбить его на части и превратить в совершенно новое число с плавающей запятой. Кроме того, эта функция случайным образом изменяет знак числа так, чтобы он варьировался в зависимости от переданного значения. Хотя возвращенные значения должны изменяться при каждом запуске, их отношение к входящему значению и к базовому числу остается неизменным, потому что для трансформации входного значения в результат функции используется детерминистическое вычисление. Запустите этот код несколько раз. Вы заметите, что значения, возвращенные функцией SCRAMBLEFLOAT(), изменяются, тогда как порядок строк остается прежним и отличается от порядка по умолчанию по столбцу I d. Возможно, этого достаточно для тестовых данных. Неизменность порядка является результатом того, что столбцы, которые мы упорядочиваем, фактически состоят не из случайных значений. Несмотря на все действия, SCRAMBLEFLOAT() возвращает предсказуемые значения. Другими словами, имея определенное входное значение, SCRAMBLEFLOATQ всегда возвращает значение с предсказуемым отношением к нему. Удваивание Удваивание — вставка таблицы в саму себя до достижения желаемых размеров — имеет некоторую схожесть с методом перекрестного объединения, который мы рассмотрели. Вы увидите, что этот способ помогает создать достаточно разнообразные данные за приемлемое время. Посмотрим пример. SET N0C0UNT ON CREATE TABLE #list (id int identity. Placeholder int NULL) INSERT #list DEFAULT VALUES : '«'■
182 Глава 6. Создание тестовых данных INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES . INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES INSERT #list DEFAULT VALUES SET NOCOUNT OFF DECLARE @cnt int. @rcnt int. @targ int SELECT @targ=100. @cnt=COUNT(*). @rcnt=@targ-COUNT(*) FROM#list WHILE @rcnt>0 BEGIN SET ROWCOUNT @rcnt INSERT #list (PlaceHolder) SELECT Placeholder FROM flist SET @cnt=@cnt+@@ROWCOUNT SET @rcnt=@targ-@cnt END SET ROWCOUNT 0 SELECT Id. RAND(Id)*10000000000000 FROM #list * GO DROP TABLE flist (Результаты сокращены) A0 row(s) affected) B0 row(s) affected) D0 row(s) affected) B0 row(s) affected) Id 1 2 3 4 5 6 96' 97 98 99 100 7135919932129.2354 7136106261841.8174 7136292591554.3994 7136478921266.9B14 7136665250979.5635 7136851580692.1455 7153621254B24.5244 7153807584537.1074 7153993914249.6885 7154180243962.2715 7154366573674.8525 A00 row(s) affected) Код начинается с присваивания переменной @targ необходимого числа строк, переменной @cnt — числа строк во временной таблице и @rcnt — числа недостающих строк. Затем в цикле мы вставляем таблицу в саму себя до тех пор, пока не получим необходимое число строк. Обратите внимание на использование SET ROWCOUNT для управления числом строк, вставленных в таблицу. Это помогает не превысить необходимое нам количество
Подходы к созданию данных 183 строк. Здесь приходится использовать SET ROWCOUNT вместо SELECT TOP, потому что в SELECT TOP нельзя использовать переменные. INSERT...EXEC Еще один быстрый способ создания большого числа данных — использование INSERT...EXEC для вызова хранимой процедуры, которая возвращает большое число записей (даже содержащих бесполезную информацию). Посмотрите запрос, который сначала вызывает служебную программу SQLDIAG для создания отчета о статистике вашего SQL Server, а затем вызывает xp_cmdshel I, используя INSERT...EXEC, чтобы создать последовательность целых чисел. SET NOCOUNT ON SET ANSI_WARNINGS OFF CREATE TABLE #list (id int identity. Placeholder char(l) NULL) EXEC master..xp_cmdshell 'sqldiag.exe -Oc:\temp\sqldiag.rpt',no_output SET ROWCOUNT 100 INSERT #list (Placeholder) EXEC master..xp_cmdshell 'TYPE c:\temp\sqldiag.rpt' SET ROWCOUNT 0 SELECT Id FROM #1i St GO DROP TABLE flist (Результаты сокращены) Id 1 2 3 4 5 6 96' 97 98 99 100 Здесь мы используем xp_cmdshe 11, чтобы получить содержимое текстового файла с помощью команды TYPE (кроме того, мы создаем файл, используя утилиту SOLD I AG, хотя обычно его не требуется пересоздавать каждый раз). ANSI _WARN INGS отключается, потому что мы специально обрезаем строки, возвращенные из текстового файла. Это механизм создания, а содержание нас не волнует. Название столбца PlaceHolder — говорит само за себя — это место расположения. Этот столбец нужен для того, чтобы мы могли вставить в него по одному символу для каждой строки, возвращенной хранимой процедурой. В то время как заполняется РI aceHo I der, будет создаваться столбец I dent i ty, а именно это нам и необходимо. Вызов xp_cmdshe I I используется для быстрого создания последовательности записей. Обратите внимание на использование SET ROWCOUNT для ограничения числа возвращенных строк. SET ROWCOUNT влияет на I NSERT...EXEC точно так же, как на I NSERT...SELECT.
184 Глава 6. Создание тестовых данных Обычно использование xp_cmdshel I негативно сказывается на производительности. В принципе, вызываемая процедура не так важна, как вы вскоре увидите. Смысл в том, что можно быстро создавать большие объемы данных, вызывая хранимую процедуру через I NSERT...EXEC. Поскольку нельзя передать в процедуру название таблицы и затем вставлять в нее данные (не используя динамический T-SQL), способ I NSERT...EXEC удобен для быстрого создания больших объемов данных. sp_generate_test_data Процедура sp_generate_test_data, представленная в следующем примере, позволяет вставлять любое число строк без использования текстового файла или любых других внешних ресурсов. Эта процедура принимает число создаваемых строк в качестве параметра, а потом возвращает их в виде набора данных. Эти записи могут быть затем вставлены при помощи I NSERT...EXEC. Посмотрите код процедуры. SET NOCOUNT ON USE master GO IF OBJECT_ID('dbo.sp_generate_test_data') IS NOT NULL * DROP PROC dbo.sp_generate_test_data GO CREATE PROC dbo.sp__generate_test_data @rowcount int AS DECLARE @var int DECLARE stable TABLE (Id int identity) SET @var=0 WHILE @var<@rowcount BEGIN INSERT stable DEFAULT VALUES _. SET @var=@var+l END SELECT * FROM stable GO CREATE TABLE #list (id int) INSERT #list (Id) EXEC sp_generate_test_data 10 SELECT Id FROM flist GO DROP TABLE flist (Результаты) Id 1 2 3 4 5 6 7 8 9 10
Скорость 185 • Здесь мы используем хранимую процедуру sp_generate_test__data для создания последовательности целых чисел, которые затем вставляются в таблицу. Число создаваемых строк передается в качестве параметра процедуры. Процедура просто вставляет заданное число строк в переменную типа tab I e, а затем возвращает ее содержимое. Этот способ хорош в случае, если вы хотите вставить в таблицу последовательный список целых, а если нет? Что если вы хотите вставить случайную информацию и вам не нужен последовательный набор целых? Придется ли писать новую процедуру sp_generate_test_data для каждого типа данных, которые следует возвращать? Возможно, вы сможете избежать этого, используя следующий метод: CREATE TABLE #list (PlaceHolder int. • ■'■ • Random float DEFAULT (RANDO). Today datetime DEFAULT (GETDATEO) ) INSERT #list (PlaceHolder) - ■" EXEC sp_generate_test_data 10 SELECT Random, Today FROM #list (Результаты) Random Today 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 0.0629477210736374 Как можно заметить, мы создали набор тестовых данных, которые фактически не используют целые значения, возвращенные sp_generate_test_data. Созданные строки не являются случайными, но, по крайней мере, нам не пришлось писать специальный запрос или хранимую процедуру для их создания. Этот способ удобнее, несмотря на то, что большинство других способов, которые я продемонстрировал, являются более «скоростными», чем I NSERT...EXEC. Его очень просто использовать — достаточно написать оператор INSERT и передать необходимое число строк процедуре sp__generate_test_data. 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 2001-08-05 18 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 07:03.607 Скорость Разные способы создания тестовых данных имеют разную производительность. Как я уже говорил, обычно существует компромисс между случайностью и скоростью создания информации. Таблица 6.1, приведенная ниже, подытожит информацию о способах, предложенных в этой главе. Я засек время использования про-
186 Глава 6. Создание тестовых данных стого цикла WHILE, вставлявшего информацию, чтобы обеспечить достоверность сравнения (время приведено в миллисекундах). Таблица 6.1. Время, затраченное на создание данных (в миллисекундах) Количество записей 100 10 000 100 000 Цикл WHILE 140 1393 13 780 CROSS JOIN 170 173 783 CROSS JOIN RAND() 203 213 1313 RANDOMO/ SCRAMBLEFLOAT() 576 4 970 48 873 Удваивание 106 533 4716 INSERT/ EXEC 170 1670 16 330 Как можно заметить, самые простые способы являются также и самыми быстрыми, в то время как те, которые создают произвольную информацию, требуют большихзатратвремени. Обратите внимание на то, что все способы, кроме RandomO/ Scramb I eF I oat() и I NSERT...EXEC, имеют лучшие характеристики по сравнению с циклом WHI LE. Мне кажется, что лучшим способом создания достаточно разнообразной тестовой информации в самые короткие сроки будет удваивание. Этот способ * в десять быстрее способа Randora() и почти в три раза быстрее цикла WH ILE. Именно этот способ я использую чаще других для создания тестовой информации. Итоги В этой главе вы узнали: ;. ■ о способах создания больших объемов тестовых данных и различиях между ними; ■ о сравнении больших объемов тестовых данных по их скорости и качеству.
Часть 2 Объекты !:!■".' ('•
Обработка ошибок Хорошие проектировщики не страшатся сложностей — некоторые из лучших даже стремятся к ним. Однако их цель в том, чтобы кажущееся сложным сделать простым. Стив Макконпем В этой главе мы поговорим об обычных путях возникновения и обработки ошибок в хранимых процедурах на Transact-SQL, а также о связанных с ними ловушках. Средства Transact-SQL по обработке ошибок вполне приемлемы, однако они не всегда работают наилучшим образом или последовательно настолько, насколько хотелось бы. Даже старательно добавляя код, обрабатывающий ошибки, к каждой хранимой процедуре, вы не можете быть уверены, что он будет работать на 100 % даже для ошибок с низким уровнем. Помимо обработки ошибок на Transact-SQL, вы должны реализовать надежную обработку ошибок в ваших клиентских приложениях. Компания Microsoft заявила, что в следующей версии SQL Server (кодовое название Yukon) Transact-SQL будет обладать встроенными языковыми сред ствами по структурной обработке исключений1. Когда это станет реальностью, мы будем обладать более мощными средствами контроля ошибок и неожиданностей в работе нашего кода. Пока этого не произошло, вам, возможно, придется взять несколько «барьеров», чтобы обработка ошибок в ваших приложениях работала должным образом. В этой главе вы узнаете о том, в какие ловушки вы можете попасть и как этого избежать. Сообщения об ошибках Начнем с небольшого обзора средств, находящихся в вашем распоряжении для передачи сообщений об ошибках в коде на Transact-SQL, и для их передачи в клиентские приложения. Систему сообщений об ошибках на Transact-SQL нельзя назвать ни изящной, ни полнофункциональной, но тем не менее от нее можно добиться того, что вам требуется. 1 См. об этом, например, «An Overview of SQL Server Yukon for the Database Developer» и другие статьи на MSDN. http://search. microsoft.com/search/results. aspx?View=msdn&st=a&qu=Yukon&c=4&s=2. —Примеч. перев. 7
Сообщения об ошибках 189 RAISERROR Вы можете сообщать об ошибках, возникших в хранимой процедуре, при помощи кода завершения и команды RAISERR0R, которая не вызывает завершения процедуры, а просто возвращает заданное сообщение об ошибке и устанавливает значение глобальной переменной @@ERR0R. Вы можете передать свое собственное сообщение, которое будет возвращено функцией RA I SERROR, а также можете сослаться на ранее определенное в таблице sysmessages сочетание «номер ошибки/сообщение». RA I SERROR всегда возвращает номер ошибки независимо от того, определили вы ее или нет. Если вы вызвали RA I SERROR с пользовательским сообщением, номер ошибки будет равным 50 000 — максимальному номеру системной ошибки (пользовательские номера начинаются с 500 001). Вы можете форматировать сообщения RAISERROR, как это делается в функции pri ntf (). Сообщение представляет собой строку, дополненную параметрами форматирования вида %d и %s, и вы можете определить столько аргументов, сколько значений необходимо передать в сообщение. Вы также можете добавить свое собственное сообщение в таблицу sysmessages при помощи sp_addmessage. Эти сообщения также могут содержать параметры форматирования. Используя RA I SERROR для возврата сообщения об ошибке, вы определяете значения и уровня (seve г i ty) и статуса (state) ошибки. Ошибки со значениями уровня менее 16 вызывают запись информирующего сообщения в системный журнал (если он включен); ошибки со значением уровня, равного 16, — запись предупреждающего сообщения; ошибки со значениями уровня выше 16 — запись с сообщением об ошибке. Ошибки со значениями уровня не выше 18 может инициировать любой пользователь, со значениями уровня ошибки от 19 до 25 — только член роли sysadmi n, при этом требуется использовать опцию WITH LOG. Ошибки со значениями уровня ошибки более 20 рассматриваются как фатальные и влекут за собой разрыв соединения. Статус ошибки — значение, которое вы можете вернуть из RA I SERROR для передачи дополнительной информации в клиентское приложение. Если присвоить статусу ошибки значение 127, утилиты ISQL и OSQL установят значение системной переменной окружения ERR0RLEVEL, равной значению номера ошибки, возвращаемого RAISERROR. Для версий SQL Server младше7.0 утилита ISQL при этом сразу завершала работу. Теперь это уже не так: просто устанавливается значение ERR0RLEVEL. 0SQL тем не менее до сих пор сразу завершает работу. Функция RAISERROR поддерживает несколько параметров, которые контролируют ее работу. Параметр WITH LOG копирует вызываемое вами сообщение об ошибке в журнал приложений (при наличии последнего) и в журнал ошибок SQL Server независимо от того, использовался ли параметр w i th_ I og процедуры sp__addmessage при добавлении сообщения в sysmessages. При использовании параметра WITH N0WAIT сообщение возвращается клиенту немедленно, при этом очищается выходной буфер соединения, и, таким образом, можно легко передать PR I NT и другие отложенные сообщения клиенту. Например, чтобы очистить буфер, не вызывая в действительности ошибки, вы можете сделать следующее: RAISERROR (". 0. 1) WITH N0WAIT Все отложенные сообщения будут немедленно посланы клиенту.
190 Глава 7. Обработка ошибок При использовании параметра WITH SETERROR переменной @@ERROR присваивается номер последней ошибки независимо от уровня ошибки. Это удобно для обработки сообщений, определенных пользователем1. Для добавления сообщений в таблицу sysmessages используйте системную хранимую процедуру sp_addmessage. Добавленное однажды сообщение может быть вызвано функцией RAISERR0R. Пользовательские сообщения должны иметь номер 50 001 и более. Большое преимущество использования сообщений SQL Server заключается в независимости от языка. Поскольку можно определить язык для каждого используемого сообщения, вы можете иметь несколько сообщений с одинаковым номером ошибки, но различными языковыми настройками. Затем вы вызываете ошибку, используя ее код, при этом возвращаемое сообщение зависит от языковой настройки, которая была выбрана при установке SQL Server. xpjogevent Расширенная хранимая процедура используется для записи сообщений в журнал SQL Server или журнал операционной системы без оповещения пользователя. , Сообщения, протоколируемые хр_ I ogevent, не посылаются клиенту, номера и описания ошибок записываются в журнал без уведомления пользователя. Методы обработки ошибок Обработка ошибок, пожалуй, может возглавить список проблем, которые разочаровывают и озадачивают разработчиков Transact-SQL. Этому есть несколько причин. Главная из них заключается в том, что при некоторых условиях обработка ошибок в Transact-SQL просто не работает должным образом: либо она работает не так, как описано в документации, либо работает совершенно необъяснимо. @@ERROR После возникновения ошибки @@ERR0R будет содержать ее номер. Ваш код должен проверять ее значение после каждой значительной операции. Листинг 7.1 представляет пример ошибки и типичный участок кода T-SQL, нуждающийся в проверке. Листинг 7.1. Пример кода по обработке ошибки USE Northwind DECLARE @c int SELECT @c=C0UNT(*) FROM ORDERS WHERE OrderDate = '20001201' SELECT l/@c IF @@ERROR<>0 -- Check for an error PRINT 'Zero orders on file for the specified date' Оставим в стороне вопрос, что ошибки можно было вовсе избежать. Вместо этого посмотрим на шаблон, который здесь используется: мы производим некото- В действительности, @@ERROR будет всегда равно 50 000, если в RAISERROR используется сообщение, или msg_id, если оно указано явно — Примеч. перев.
Методы обработки ошибок 191 рое действие, проверяем значение глобальной переменной @@ERR0R, затем реагируем соответствующим образом. Это обычный способ обработки ошибок в Transact - SQL вследствие отсутствия языковых средств по структурированной обработке исключений или синтаксиса, подобного ON ERROR GOTO. И этот шаблон обработки ошибок можно использовать. Ошибки пользователя Согласно Books Online, ошибки со значениями уровня от 11 до 16 являются ошибками пользователя и могут им исправляться. Давайте сознательно внесем такую ошибку и посмотрим, сможем ли мы обработать ее. Листинг 7.2. Некоторые ошибки прерывают выполнение текущего пакета USE Northwind GO SELECT * FROM MyTable IF @@ERROR<>0 BEGIN PRINT 'Creating table...' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM МуТаЫ е END Этот код делает выборку из таблицы и в случае ошибки пытается создать таблицу и заполнить ее данными, предполагая, что ошибка вызвана отсутствием таблицы. Ниже приведен результат запроса: Server: Msg 208, Level 16. State 1. Line 1 Invalid object name 'MyTable'. Заметьте, что мы не попали в участок кода, где проверяется ошибка. Почему? Дело в том, что ошибка 203 (I nva I i d obj ect name, Неверное имя объекта) прерывает выполнение пакета. Код по обработке ошибки тем более не выполняется, и ошибка не может быть обработана. Возможно, вы думаете, что дело в том, что уровень ошибки равен 16? Взгляните на код, приведенный в листинге 7.3. Листинг 7.3. Не все ошибки со значением уровня 16 прерывают пакет.1БЕ RAISERRORC 'Table not found'.16.1) IF №ERROR<>0 BEGIN PRINT 'Creating table...' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM MyTable DROP TABLE MyTable END (Результат) Server: Msg 50000. Level 16. State 1. Line 1 Table not found Creating table... cl NULL
192 Глава 7. Обработка ошибок Пакет не был прерван, хотя мы и вызвали ошибку со значением уровня 16. Код, обрабатывающий ошибку, выполнился, и таблица была создана. Может быть, код в листинге 7.3, следующий за RAISERR0R, выполнился, поскольку вызванная ошибка не была следствием настоящей ошибки — мы вызвали ее функцией RAISERROR. Взгляните на пакет листинга 7.1: там была настоящая ошибка. Мы произвели деление на ноль, и уровень ошибки был равен. 16, однако мы смогли обработать ошибку. Таким образом, некоторые ошибки со значением уровня 16 прерывают выполнение пакета, другие же — нет. И разумного объяснения этому не найти. Вот вам характерный пример странностей Transact-SQL в части обработки ошибок. Что можно с этим сделать? Давайте посмотрим на обходной путь обработки ошибок, прерывающих текущий пакет. Листинг 7.4. Обходной путь для обработки ошибок, прерывающих выполнение пакета USE Northwind GO DECLARE @res int EXEC @res=sp_executesql N'SELECT * FROM MyTable' IF @res<>D BEGIN PRINT 'Creating table...' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM MyTable DROP TABLE MyTable END _ (Результаты) Server: Msg 208. Level 16, State 1. Line 1 Invalid object name 'MyTable'. Creating table... cl NULL Выполняя подозрительные команды SQL и используя sp_executesq I, мы изолируем их от вызывающей процедуры и тем самым не даем возникающей ошибке прервать пакет. Процедура sp_executesq I очень удобна, когда мы имеем дело с ошибками, возникающими при работе с присоединенными серверами. Такие ошибки почти всегда прерывают пакет. Если вы взаимодействуете с присоединенным сервером, вы можете делать запросы посредством sp_executesq I, чтобы изолировать вызов от остального кода и предотвратить прерывание пакета в случае возникновения ошибки. Например: EXEC MyOtherServer.master.dbo.sp_executesql N'SELECT * FROM sysdatabases' При таком использовании sp_executesq I мы запускаем запрос на другом сервере. Это также должно в общем случае повысить производительность, особенно если на удаленном сервере необходимо обрабатывать большой объем данных. Но даже если вы не соединяетесь с другим SQL Server, функция sp_executesql все равно может быть полезна. EXEC sp_executesql N'SELECT * FROM MyOtherServer.Northwind.dbo.MyTable'
Методы обработки ошибок 193 . Преимущество использования sp_executesql состоит в предоставляемой ею изоляции. Даже если запрос не выполняется целиком на другом сервере, любые возникшие ошибки по крайней мере не прервут пакет. При таком использовании значение, возвращаемое sp_executesql, будет содержать код ошибки (если она произойдет). @@ERR0R также должна содержать код ошибки. Таким образом, вы легко можете передать код ошибки через стек вызовов (листинг 7.5). Листинг 7.5. Сообщение об ошибке довольно легко вернуть назад по цепочке CREATE PROC ListTable AS DECLARE @res int EXEC @res=sp_executesql N'SELECT * FROM MyTable' IF @res<>0 BEGIN PRINT 'Error listing table.' RETURN(@res) END GO DECLARE @r int EXEC @r=ListTable SELECT @r (Результат) Server: Msg 208. Level 16. State 1. Line 1 Invalid object name 'MyTable'. Error listing table. 208 Как видите, мы получили сообщение об ошибке и обработали ее, затем вернули код ошибки вызвавшей процедуре. Заметьте, что для использования этой техники не требуется всякий раз вызывать sp_executesql: то же самое делает любая хранимая процедура. Листинг 7.6. Если ошибка приводит к выходу из процедуры, @@ERROR содержит номер ошибки CREATE PROC ListTable AS SELECT * FROM MyTable GO EXEC ListTable IF №ERROR<>0 BEGIN PRINT 'Creating table...' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM MyTable DROP TABLE MyTable END Server: Msg 208. Level 16. State 1. Procedure ListTable. Line 3 Invalid object name 'MyTable'. Creating table... , cl ' * ' '■'*' '
194 Глава 7. Обработка ошибок Вместо того чтобы проверять код выполнения хранимой процедуры, мы просто проверяем значение @@ERR0R сразу после выхода из процедуры. Если выход из процедуры произошел из-за ошибки, @@ERR0R будет иметь ненулевое значение. Заметьте, что мы не смогли бы вернуть ошибку из ListTable, даже если бы захотели это сделать. Повторимся: если произошла ошибка, немедленно происходит выход из процедуры и выполнение возвращается к вызвавшему ее коду. Фатальные ошибки Как я уже упоминал, ошибки с уровнем 20 и более рассматриваются как фатальные и прерывают соединение. Что можно с этим сделать? Один из обходных путей — использование процедуры хр_ехес из главы 20. Эта процедура запускает проблемный код в отдельном соединении, которое разделяет пространство транзакций текущего соединения. Другой путь — использование xp_cmdshe I I для запуска 0SQL. ЕХЕ и выполнения запроса в отдельном соединении (и в отдельном процессе). При желании этот маневр поможет. Но наилучший способ тем не менее — выяснить причину столь серьезной ошибки и устранить ее. Мнимые ловушки Поскольку глобальная переменная @@ERR0R меняет значение каждый раз, когда команда выполняется успешно, то ее последующий анализ может оказаться хитрым делом. Для примера рассмотрим код листинга 7.7. Листинг 7.7. Переменная @@ERROR легко меняет значение CREATE PROC ListTable AS SELECT * FROM MyTable GO EXEC ListTable IF @@ERROR<>0 BEGIN PRINT TERROR PRINT 'An error occurred, attempting to create the table' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM MyTable DROP TABLE MyTable END Server: Msg 208. Level 16. State 1. Procedure ListTable. Line 3 Invalid object name 'MyTable'. 0 An error occurred, attempting to create the table cl NULL Как видите, @@ERR0R изменила значение после выполнения оператора IF, что нам совсем не требуется. Вместо этого вы должны сохранить значение @@ERR0R сразу после выполнения операции, которую вам необходимо проанализировать на на-
Методы обработки ошибок 195 личие ошибки. В этом случае изменение @@ERR0R несущественно: вы можете не волноваться, так как уже кэшировали ее значение. Посмотрите код, переписанный для правильной обработки значения @@ERR0R. Листинг 7.8. Кэширование переменной @@ERROR для предотвращения изменения ее значения CREATE PROC ListTable AS SELECT * FROM MyTable . • 'v go , ■.': . ■ .. DECLARE @res int EXEC ListTable SET @res=@@ERR0R IF @res<>0 BEGIN PRINT @res PRINT 'An error occurred, attempting to create the table' CREATE TABLE MyTable (cl int) INSERT MyTable DEFAULT VALUES SELECT * FROM MyTable DROP TABLE MyTable END Сделайте привычкой кэширование и проверку @@ERR0R после выполнения важного участка кода, особенно команд DML. Главный признак надежного кода — тщательная обработка ошибок, и до тех пор пока Transact-SQL не может поддерживать структурированную обработку ошибок, использование @@ERR0R остается главным инструментом. @@ROWCOUNT Иногда ошибочные условия не приводят к появлению сообщения об ошибке или к определению кода ошибки. Один из таких случаев — модификация таблицы, которая, вопреки нашим ожиданиям, не затронула ни одну запись. Вы можете определить количество записей, затронутых последней инструкцией DML (SELECT, INSERT, UPDATE или DELETE), проверив значение @@R0WC0UNT. Ecjih@@ROWCOUNT показывает, что не была затронута ни одна запись, результат можно обработать как любую другую ошибку. Пример показан в листинге 7.9. Листинг 7.9. Проверка @@ROWCOUNT для обнаружения коварных ошибок CREATE PROC ListTable AS SELECT * FROM Northwind..Customers WHERE 1-0 -- Get no rows GO DECLARE @res int EXEC ListTable SET @res=@@R0WC0UNT IF @res=0 BEGIN PRINT 'ListTable returned no rows' END Заметьте, что я сохранил значение @@R0WC0UNT сразу после EXEC. Подобно @@ERROR, значение @@R0WC0UNT также может быть легко изменено, поэтому важно кэширо- вать ее сразу после выполнения подозрительной команды.
196 Глава 7. Обработка ошибок Ошибки и управление транзакциями Возможно, самая существенная проблема с надежностью обработки ошибок в Тгап- sact-SQL заключается в том, что прерванный пакет может оставить после себя незавершенную транзакцию. Если пакет с открытой транзакцией прерван из-за ошибки, транзакция останется незавершенной и все ресурсы, захваченные ею, будут блокировать других пользователей. Для примера рассмотрим код в листинге 7.10. Листинг 7.10. Сообщение сервера о незавершенной транзакции CREATE PROC TestTran AS DECLARE @ERR int BEGIN TRAN SELECT * FROM MyTable SET @ERR=(a(aERROR IF @ERR<>0 BEGIN RAISERRORt'Encountered an error, rolling back'.16.10) IF №RANCOUNT<>0 ROLLBACK RETURN(@ERR) END COMMIT TRAN GO EXEC TestTran (Результаты) Server: Msg 208. Level 16. State 1. Procedure TestTran. Line 4 Invalid object name 'MyTable'. Server: Msg 266. Level 16. State 1. Procedure TestTran. Line 11 Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 0. current count = 1. Как мы обнаружили ранее, если ошибка с кодом 208 прерывает текущий пакет, то код, обрабатывающий ошибку и производящий откат транзакции, никогда не выполнится (об этом говорит и сообщение SQL Server сразу после сообщения об ошибке). Как бороться с этим? Один из методов — это описанный ранее прием с выделением проблемного кода в отдельную процедуру, тогда ошибка в процедуре не прерывает выполнения кода, вызвавшего ее. Проблема заключается в том, что вы не знаете точно, где находится «проблемный» код — он может быть где угодно. Более надежный метод заключается в проверке числа открытых транзакций на выходе из любой процедуры, которая открывает транзакцию, и вызове ROLLBACK, если есть незавершенная транзакция. В листинге 7.11 приведены процедуры, модифицированные с учетом вышесказанного. Листинг 7.11. Метод обработки незавершенных транзакций CREATE PROC TestTran AS IF (a@TRANCOUNT<>0 ROLLBACK -- Check for orphaned trans and rollback DECLARE @ERR int BEGIN TRAN SELECT * FROM MyTable SET @ERR=@@ERR0R IF @ERR<>0 BEGIN RAISERR0R('Encountered an error, rolling back',16.10) IF @@TRANCOUNT<>0 ROLLBACK RETURN(@ERR) END COMMIT TRAN ■ ' • '
Методы обработки ошибок 197 GO . EXEC TestTran Как вы видите, все, что требуется для того, чтобы избежать весьма серьезной проблемы, — единственная строчка кода. Конечно, этот подход предполагает, что процедуры вызываются в таком порядке, что откат незавершенных транзакций обеспечен. До тех пор пока этого не произошло, транзакция остается активной. Окончательным решением проблемы является обнаружение произошедшей ошибки и необходимость опсата открытой транзакции. За исключением этого, представленный выше способ должен помочь вам об< >н ш некоторые недостатки в обработке ошибок в Transact-SQL. SET XACT_ABORT Настройка SET XACT_A80RT определяет, прерывается ли транзакция при возникновении ошибок определенного вида в процессе выполнения. Предполагается завершение при любых ошибках в процессе выполнения, однако, как показывает мой опыт, это не всегда так. Существуют ошибки, которые не обрабатываются даже в этом случае, и, как следствие, незавершенные транзакции остаются даже при включении данной настройки. Ошибка, которая вызывает автоматический откат транзакции, может быть как системной, так и пользовательской. Желательно проверять значение переменной @@ERR0R после каждой команды и откатывать транзакцию при обнаружении ошибки. Посмотрите пример работы настройки SET XACT_ABORT. Листинг 7.12. SET XACT_ABORT помогает избежать появления незавершенных транзакций CREATE PROC TestTran AS DECLARE @ERR int SET XACT_ABORT ON BEGIN TRAN DELETE Customers SET (aERR=@@ERROR IF @ERR<>0 BEGIN RAISERROR('Encountered an error, rolling back',16.10) IF @@TRANCOUNT<>0 ROLLBACK RETURN(@ERR) END ' • COMMIT TRAN GO EXEC TestTran GO SELECT @@TRANCOUNT (Результаты) Server: Msg 547, Level 16. State 1. Procedure TestTran. Line 5 DELETE statement conflicted with COLUMN REFERENCE constraint 'FK_Orders_Customers'. The conflict occurred in database 'Northwind'. table 'Orders', column 'CustomerlD'. 0 Транзакция была завершена по причине ошибки нарушения логической целостности данных, поскольку была включена настройка SET XACT_ABORT. Это легкий способ откатить транзакцию при возникновении разнообразных ошибок. Однако не
198 Глава 7. Обработка ошибок все ошибки могут быть обработаны включением настройки SET XACT„ABORT. Некоторые типы ошибок не вызывают откат транзакции вопреки вашим ожиданиям. Посмотрите пример в листинге 7.13. Листинг 7.13. Некоторые ошибки не вызывают откат транзакции при включенной настройке SET XACT_ABORT CREATE PROC TestTran AS DECLARE @ERR int SET XACT_ABORT ON BEGIN TRAN SELECT * FROM NoTable SET <3ERR=C(aERR0R IF @ERR<>0 BEGIN RAISERROR('Encountered an error, rolling back'.16.10) IF @@TRANCOUNT<>0 ROLLBACK RETURN(@ERR) END COMMIT TRAN GO EXEC TestTran GO SELECT @@TRANCOUNT (Результаты) Server: Msg 208, Level 16, State 1, Procedure TestTran, Line 5 Invalid object name 'NoTable'. Server: Msg 266, Level 16. State 1. Procedure TestTran. Line 12 Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 0. current count = 1. 1 Мы еще раз убедились, что ошибка 208 является фатальной для всего пакета и в результате дает незавершенную транзакцию. Хотя логично было бы ожидать, что XACT_ABORT сделает свое дело и удалит незавершенную транзакцию, но этого не происходит. Вообще говоря, SET XACT_ABORT не может обнаружить ошибки времени компиляции. Возможно, это удобный случай применить обходной путь из листинга 7.11. Если каждая хранимая процедура превентивно произведет откат незавершенных транзакций, можно надеяться, что общий эффект незавершенных транзакций на параллелизм будет минимальным. Пока Transact-SQL не поддерживает структурированную обработку исключений, вы вынуждены использовать некоторые из обходных путей, описанных в этой главе, чтобы должным образом обрабатывать ошибки и избегать утечки или потери ресурсов. Итоги В этой главе вы узнали: ■ об основах обработки ошибок в Transact-SQL; ■ о важности корректного обращения с транзакциями; ■ об обработке серьезных ошибок при работе с транзакциями.
Триггеры В любой день лучше осознавать, что ты «уже был», нежели «никогда не был». X. В. Кентон Триггер — это специальный тип хранимой процедуры, который запускается при срабатывании одной из DML-операций (INSERT, UPDATE, DELETE и любая их комбинация). Триггеры обычно используются для обеспечения выполнения бизнес-правил, хотя способны проделывать любую работу, необходимую для изменения данных. Триггер в MSSQL — это эквивалент са I I back-функции языков программирования третьего поколения CGL) или перехваченному вектору прерываний. Триггеры конструируются и присоединяются к таблице с помощью команды CREATE TRIGGER. Когда таблица удаляется, удаляются и ее триггеры. Большинство приемов программирования хранимых процедур удобны для использования в триггерах. В тело триггера вы можете поставить вызов хранимой процедуры и — при помощи всей ее компилированной мощи — справиться с поставленной задачей. Единственное ограничение: в триггерах обычно не возвращается набор данных, поскольку большинство протоколов работы с данными не имеет средств обработки наборов данных, возвращаемых триггерами. К тому же, SQL Server этого и не разрешает. Триггеры выполняются один раз для DML-операции вне зависимости от количества записей, затронутых этой командой. В SQL Server существует два типа триггеров: AFTER-триггеры, которые запускаются после DML-операции, и I NSTEAD-триг- геры, которые запускаются вместо нее. Мы рассмотрим каждый тип триггеров в отдельности. Можно сделать любое количество AFTER-триггеров для одной таблицы. С помощью sp_sett г i ggerorder можно определить, какой триггер будет запускаться первым и какой последним. Оставшиеся триггеры выполняются в произвольном порядке, и установить этот порядок не представляется возможным. Аналогично для каждой таблицы или представления можно создать один I NSTEAD-триггер. Либо можно обойтись созданием нескольких представлений для таблицы и присоединением I NSTEAD-триггера к каждому из них. Ограничения ссылочной целостности имеют больший приоритет по сравнению с AFTER-триггерами. Это означает, что при нарушении ссылочной целостности триггер не будет выполняться.
200 Глава 8. Триггеры Определение изменений При помощи функций UPDATE() и COLUMNS_UPDATE() в триггере можно проверить, какие столбцы были затронуты DML-операцией. Функция UPDATE() возвращает истину в случае записи значения в конкретный столбец вне зависимости от того, имело ли место фактическое изменение значения этого столбца. Функция COLUMNS_UPDATE() возвращает битовую маску с установленными битами для всех столбцов, в которые записывались значения. Рассмотрим эти функции на примере. Листинг 8.1. Пример использования функции UPDATE() USE tempdb GO CREATE TABLE Toylnventory (Toy int identity. Type int. Onhand int ) GO CREATE TABLE ToyTypes (Type int identity, MinOnhand int ) GO INSERT ToyTypes (MinOnhand) VALUES A0) INSERT ToyTypes (MinOnhand) VALUES B0) INSERT ToyTypes (MinOnhand) VALUES A5) INSERT ToyTypes (MinOnhand) VALUES E0) INSERT Toylnventory (Type, Onhand) VALUES A. 50) INSERT Toylnventory (Type. Onhand) VALUES B. 50) INSERT Toylnventory (Type. Onhand) VALUES C, 50) INSERT Toylnventory (Type. Onhand) VALUES D. 50) ■ " GO CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS DECLARE @rcnt int SET @rcnt=@@R0WC0UNT IF @rcnt=0 RETURN IF @rcnt > 1 BEGIN ■ • RAISERR0R('Можно изменять только одну строку',16,10) ROLLBACK RETURN END IF (UPDATE(Onhand)) BEGIN IF EXISTS (SELECT * FROM ToyTypes t JOIN inserted i ON t.Type=i.Type WHERE t.MmOnhandM.Onhand) BEGIN RAISERR0R('Столбцу Onhand нельзя присвоить значение меньше допустимого'.16,10) ROLLBACK RETURN END END GO UPDATE Toylnventory SET 0nhand=49 WHERE Toy=4 -- Триггер не пропустит GO
Определение изменений 201 DROP TABLE ToyInventory. ToyTypes GD Этот пример не только показывает, как использовать функцию UPDATE(), — в нем также показан способ кодирования триггеров таким образом, чтобы они корректно обрабатывали операции обновления, затрагивающие более одной записи. В данном случае мы просто не разрешаем выполнять такие изменения. Сначала мы проверяем переменную @@R0WC0UNT, чтобы видеть количество записей, подвергшихся изменениям. Если таковых записей нет, то мы сразу выходим из триггера. Дальше мы проверяем количество измененных записей. Если их больше одной, то мы выводим сообщение об ошибке, откатываем транзакцию и выходим из триггера. Заметьте, что мы сохраняем значение @@R0WC0l)NT в локальную переменную, чтобы иметь возможность проверить ее несколько раз. Первый раз — для проверки того, были ли изменения вообще. И второй раз — для проверки количества затронутых операцией записей. Если мы прошли обе проверки, то у нас имеется ровно одна измененная запись. Мы проверяем, был ли изменен столбец Onhand. Если да, мы проверяем также, что значение не превышает максимально допустимого значения в столбце М i nOnhand таблицы ToyTypes. Если текущее значение Onhand в таблице i nserted меньше допустимого, то мы выводим сообщение об ошибке, откатываем транзакцию и выходим из триггера. Если все в порядке, то изменения принимаются. Вместо UPDATE() мы легко можем использовать функцию COLUMNS_UPDATE(). Дополнительным преимуществом этой функции является возможность проверить при помощи одного сравнивания, имело ли место изменение нескольких столбцов. COLUMNS_UPDATED() возвращает битовую маску типа varbinary, которая указывает измененные столбцы. Каждый бит отвечает за свой столбец. Нумерация битов соответствует порядковым номерам столбцов. Давайте посмотрим на тот же триггер, переписанный с использованием функции COLUMNS_UPDATED(). Листинг 8.2. Пример использования функции COLUMNS_UPDATE() CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AETER UPDATE AS DECLARE @rcnt Int SET Crcnt=(a(aR0WC0UNT IF @rcnt=0 RETURN IF @rcnt > 1 BEGIN RAISERRORC'Можно изменять только одну строку'.16,10) ROLLBACK RETURN END IF ((COLUMNSJJPDATEDC) & 4)<>D) BEGIN IF EXISTS (SELECT * FROM ToyTypes t JOIN inserted i ON t.Type=i.Type WHERE t.MinOnhand>i.Onhand) BEGIN RAISERRORC'Столбцу Onhand нельзя присвоить значение меньше допустимого'.16.10) ROLLBACK RETURN END END Для определения изменения столбца Onhand мы используем бинарную операцию И (которая выполняется оператором &). Это третий по счету столбец в таблице. За него отвечает третий бит в маске (биты нумеруются с нуля, поэтому третий
202 Глава 8. Триггеры бит будет два в квадрате. 2° = 1,21 = 2,22 = 4). Если мы хотим проверить несколько столбцов, это сделать легко. Например, чтобы проверить, изменялось ли значение первого и второго столбцов, нужно сравнить COLUMNS_UPDATE() с тройкой, потому что тройка представляется двумя младшими битами B° = 1, 21 = 2,1 OR 2 = 3). Вот версия триггера, при помощи которой проверяется обновление нескольких столбцов. Листинг 8.3. Проверка нескольких столбцов при помощи функции COLUMNS_UPDATE() CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS DECLARE @rcnt int SET @rcnt=(a(aROWCOUNT IF @rcnt=0 RETURN IF @rcnt > 1 BEGIN RAISERRORCYou may only change one item at a time'.16,10) ROLLBACK RETURN END -- Test for changes to columns 2 and 3 IF UCOLUMNSJJPDATEDO & 6)<>0) BEGIN -- Test for changes to columns 2 and 3 IF NOT EXISTS(SELECT * FROM ToyTypes t JOIN inserted i ON t.Type=i.Type) BEGIN RAISERROR(HenpaBMbHbiki тип игрушки',16.10) ROLLBACK RETURN END IF EXISTS (SELECT * FROM ToyTypes t JOIN inserted i ON t.Type=i.Type WHERE t.Min0nhand>i,0nhand) BEGIN RAISERROR('Столбцу Onhand нельзя присвоить значение меньше допустимого',16.10) ROLLBACK RETURN END END В данном случае мы сравниваем со значением 6, потому что хотим узнать, изменялись ли второй и третий столбцы (Туре и Onhand соответственно). 2! = 2,22 = 4, 2 OR 4 = 6. После логического ИЛИ получаем значение 6, что и соответствует установленным второму и третьему битам. В I NSERT-триггерах COLUMNS_UPDATE() устанавливает биты для всех столбцов, потому что в этом случае все столбцы получают значения явно или неявно из значений по умолчанию. Рассмотрим это на примере. Листинг 8.4. Операция INSERT и COLUMNSJJPDATEQ USE tempdb GO CREATE TABLE Toylnventory (Toy int identity. Type int NULL. Onhand int DEFAULT 10 ) GO CREATE TRIGGER ToylnventoryJNSERT ON Toylnventory AFTER INSERT AS IF @CROWC0UNT=0 RETURN DECLARE @ChangedColumns varbinary(8000). @Size int. @i int
Определение изменений 203 SET OChangedCol umns=COLUMNS_UPDATED() SET @Size=DATALENGTH(@ChangedColumns)*8 SET @i=0 WHILE @i«aSize BEGIN IF (№ChangedCollimns & P0WERB.@i ))<>0) PRINT 'Столбец '+CAST(@i AS varchar)+' изменен' SET @i=@i+l END GO INSERT Toylnventory DEFAULT VALUES GO DROP TABLE Toylnventory GO (Результаты) Столбец О изменен Столбец 1 изменен Столбец 2 изменен Как мы видим, даже при добавлении в таблицу записи с DEFAULT VALUES все биты оказываются установленными. Каждый из столбцов Toy I nventory имеет свой механизм присваивания значения по умолчанию: Toy — это IOENTITY-столбец; Туре допускает NULL-значения; Onhand имеет значение по умолчанию. Как я уже говорил ранее, поскольку каждый из столбцов таблицы получает свое значение во время операции INSERT, то и COLUMNS_UPDATE() будет показывать, что значения всех столбцов были изменены. Обратите внимание на цикл, который мы используем для перечисления столбцов. WHILE @i<(aSize BEGIN IF ((PChangedColumns & P0WERB,@i))<>0) PRINT 'Столбец '+CAST(@i AS varchar)+' изменен' SET @i=@i+l END Мы предварительно сохранили значение COLUMNS_UPDATE() в переменную @ChangedCo I umns, поэтому можем проверять каждый ее бит в цикле. Такой метод будет работать в любых триггерах. Для получения бинарного значения каждого бита по очереди мы используем функцию POWER(). Поскольку мы оперируем бинарными данными, нам необходимы степени двойки. Сначала в переменную §s i ze мы получаем общее количество столбцов в таблице, округленное к большему значению до границы байта. Далее мы перебираем биты в цикле и проверяем каждый. Для установленных битов выводим сообщение. Что же делать, если мы хотим получить не только номера столбцов, но и их название? Давайте рассмотрим следующий пример. Листинг 8.5. Определение названия измененных столбцов USE tempdb GO CREATE TABLE Toylnventory (Toy int identity. продолжение^
204 Глава 8. Триггеры Листинг 8.5 {продолжение) Type int NULL. Onhand Int DEFAULT 10 ) GO CREATE TRIGGER ToylnventoryJNSERT ON Toylnventory AFTER INSERT AS IF @@R0WCOUNT=0 RETURN DECLARE @ChangedColumns varbinary(8000), @S1ze int, @i int. @colname sysname SET CChangedColumns=C0LUMNSJJPDATED() SET @Size=DATALENGTH((aChangedColumns)*8 SET @i=0 WHILE @i<@Size BEGIN IF ((@ChangedCo]umns & P0WERB.(ai ))<>0) BEGIN SELECT @ColName=COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='Toylnventory' AND ORDINAL_POSITION-l=@i PRINT 'Column '+@ColName+' changed' END SET (?i=<ai+l END GO INSERT Toylnventory DEFAULT VALUES GO DROP TABLE Toylnventory GO (Результаты) Столбец Toy изменен -. Столбец Type изменен Столбец OnHand изменен Мы использовали представление INFORMAT i ON_SCHEMA. COLUMNS для получения названий столбцов по порядковому номеру столбца, что предпочтительнее прямых запросов к syscolunms, хотя мы можем получить необходимую информацию и оттуда. Управление последовательными значениями Было время, когда работа с i dent i ty-столбцами в триггерах была похожа на черную магию. Требовались длительные ритуальные приседания, чтобы заставить столбцы работать, и усердные молитвы, чтобы они работали должным образом. После добавления в Transact-SQL функций SC0PE_ I DENT I TY() и I DENT_CURRENT() все стало намного проще. Как видно из названия, SC0PE_ I DENT I TY() дает нам текущее значение i dent i ty-столбца в текущей области видимости. I DENT_CURRENT(), наоборот, игнорирует все области видимости и возвращает последнее присвоенное значение i dent i ty-столбца в конкретной таблице. Эта парочка функций работает гораздо лучше почтенной ШIDENTITY. Глобальная переменная @@IDENTITY возвращает последнее вставленное значение i dent i ty в текущем соединении независимо от области видимости и таблицы, в которую это значение было добав-
Управление последовательными значениями 205 лено. Поэтому если в таблицу с i dent i ty-столбцом добавляется запись, а эта таблица имеет триггер, добавляющий запись в другую таблицу с i dent i ty-столбцом, то вызванное непосредственно после оператора вставки ШIDENTITY возвращает значение, добавленное во вторую таблицу. А это неправильно. Теперь мы не будем утруждать себя заботой о вложенных триггерах. Чтобы получить значение identity, явно сгенерированное нашей командой добавления, мы используем функцию SC0PE_ I DENT I TY(), которая не обращает внимание на то, что происходит в других контекстах (например, в триггерах). IDENT_CURRENT() также полезна в этой ситуации, потому что в ней можно задать таблицу, для которой мы хотим получить последнее значение i dent i ty. Поскольку в нашем сценарии мы в триггере добавляем запись в другую таблицу, то, указав интересующую нас таблицу, мы получаем i dent i ty-значение именно для указанной таблицы. Лучший путь к пониманию — пример. В листинге 8.6 представлены нюансы работы с identity в триггерах. Листинг 8.6. Нюансы работы с identity в триггерах USE tempdb GO CREATE TABLE Toylnventory (Toy int identity, Type int. Onhand int ) CREATE TABLE ToyAudit (ToyAudit int identity. Operation varchar(lO), Toy int. ;• Type int, Change int ) GO r:: v\f- ' , - -- Seed the tables INSERT Toylnventory DEFAULT VALUES INSERT Toylnventory DEFAULT VALUES INSERT Toylnventory DEFAULT VALUES GO '• ' INSERT ToyAudit DEFAULT VALUES GO CREATE TRIGGER ToylnventoryJNSERT ON Toylnventory AFTER INSERT AS IF @@R0WCOUNT=0 RETURN INSERT ToyAudit SELECT 'INSERT'. * FROM inserted PRINT 'Внутри триггера:' PRINT '(a@IDENTITY='+CAST(@@IDENTITY AS varchar)+ ' SC0PEJDENTITY()='+CAST(SCOPEJDENTITY() AS varchar)+ ' IDENT_CURRENT( "ToyAudit" )='+CAST(IDENT_CURRENT( 'ToyAudit') AS varchar) GO INSERT Toylnventory DEFAULT VALUES PRINT 'После операции:' PRINT '@@IDENTITY='+CAST(@@IDENTITY AS varchar)+ ' SCOPEJDENTITY()='+CAST(SC0PE_IDENTITY() AS varcharH ' IDENT_CURRENT( "Toylnventory ")='+CAST(IDENT_CURRENT('Toylnventory') AS varchar) продолжение #
206 Глава 8. Триггеры Листинг 8.6 {продолжение) GO DROP TABLE Toylnventory. ToyAudit GO (Результаты) Внутри триггера: @@IDENTITY-2 SC0PE_IDENTITY()=2 IDENT_CURRENT('ToyAudit')=2 После операции: @(aiDENTITY=2 SC0PE_IDENTITY()=4 IDENT_CURRENT('Toylnventory')=4 Здесь мы добавляем запись в таблицу, имеющую триггер, в котором в другую таблицу тоже добавляется запись. Мы выводим значения @@ I DENT ITY, SCOPE_ I DENT I TY() и I DENT_CURRENT() во время исполнения триггера и сразу после добавления записи. Замечу, что значение §§IDENTITY является одним и тем же в обоих случаях, поскольку эта функция выводит последнее добавленное значение identity для текущей сессии независимо от таблицы и контекста. В нашем случае последнее значение было добавлено в таблицу ToyAud i t, в то время как мы хотим знать знамение identity из таблицы Toylnventory. Тут вступают в игру SC0PE_ I DENT I TY() и I DENT_CURRENT(). Они обе возвращают нужное значение, однако лучше использовать SC0PE_ i DENT 1 TY(), потому что IDENT_CURRENT() возвращает последнее добавленное значение для всех сессий. В этом случае мы можем получить неправильное значение, если кто-нибудь добавил запись в таблицу Toy I nventory сразу после нашей операции добавления, но до того момента, как мы прочитали значение IDENT_CURRENT(). У функции SCOPE_IDENTITY() этого недостатка нет, поэтому я считаю ее более полезной. Ограничения триггеров Есть несколько ограничений для команд, выполняемых внутри триггеров. Ограничения связаны с тем, что триггеры выполняются, по крайней мере, в неявных транзакциях, а данные команды нельзя использовать внутри транзакций вообще. Вот их список. ■ ALTER DATABASE ■ CREATE DATABASE ■ DISK I N IT ■ DISK RESIZE ■ DROP DATABASE ■ LOAD DATABASE ■ LOAD LOG ■ RECONFIGURE ■ RESTORE DATABASE ■ RESTORE LOG ■ UPDATE STATISTICS
INSTEAD-триггеры 207 Некоторые команды должны быть первыми командами в пакете. Например, CREATE VI EW, CREATE PROCEDURE и др. Для их исполнения в хранимой процедуре или в триггере нужно применять специальные методы. Посмотрим на триггер, создающий представление и добавляющий в него запись. Листинг 8.7. Триггеры могут выполнять недопустимые (на первый взгляд) команды USE tempdb GO CREATE TABLE Toylnventory (Toy int identity. Type int. Onhand int ) CREATE TABLE ToyAudit (ToyAudit int identity, Operation varchar(W), Toy int. Type int, Change int ) GO CREATE TRIGGER ToylnventoryJNSERT ON Toylnventory AFTER INSERT AS IF №RDWCOUNT=0 RETURN EXECC'IF OBJECTJDC'TA") IS NOT NULL DROP VIEW ТА') EXECCCREATE VIEW ТА AS SELECT * FROM ToyAudit') INSERT ТА SELECT 'INSERT'. * FROM inserted GO INSERT Toylnventory DEFAULT VALUES Так как CREATE VI EW должна быть первой командой в пакете, мы оборачиваем ее вызов в EXEC. Это приводит к тому, что она помещается в отдельный пакет. С этой же целью мы отдельно вызываем DROP VIEW. За исключением CREATE TABLE, большинство команд по созданию объектов внутри триггеров должны выполняться именно так. Еще одним преимуществом этого подхода является возможность создавать команды CREATE динамически. Существует ограничение для совместного использования I NSTEAD-триггеров и каскадной ссылочной целостности. INSTEAD OF UPDATE-триггер не может быть создан для таблицы, имеющей каскадное ограничение целостности на UPDATE. To же самое относится и к I NSTEAD OF DELETE-триггерам и каскадному ограничению целостности на DELETE. INSTEAD-триггеры Как видно из названия, I NSTEAD-триггеры выполняются вместо DML-оператора, вызвавшего их. В этом их отличие от AFTER-триггеров, которые выполняются после оператора, но до окончания транзакции. I NSTEAD-триггеры удобны для изменения представлений и таблиц, обычная обработка которых слишком сложна и не может быть выполнена ничем, кроме хранимых процедур. Рассмотрим простой пример.
208 Глава 8. Триггеры Листинг 8.8. Как работают INSTEAD-триггеры USE tempdb GO CREATE TABLE AussieArtists (Artistld int Identity. LastName varcharA5). FirstName varcharA5) ) GO INSERT AussieArtists VALUES CGibb'. 'Barry') INSERT AussieArtists VALUES CGibb', 'Maurice') INSERT AussieArtists VALUES CGibb'. 'Robin') INSERT AussieArtists VALUES CGibb', 'Andy') INSERT AussieArtists VALUES ('Newton-John', 'Olivia') INSERT AussieArtists VALUES ('Crowe', 'Russell') INSERT AussieArtists VALUES CHogan', 'Paul') INSERT AussieArtists VALUES ('Kidman', 'Nicole') INSERT AussieArtists VALUES CBozinov', 'Zarko') INSERT AussieArtists VALUES ('Hay', 'Colin') GO CREATE VIEW VAussieArtists AS SELECT FirstName+' '+LastName AS Name FROM AussieArtists GO CREATE TRIGGER VAussieArtistsJNSERT ON VAussieArtists INSTEAD OF INSERT AS INSERT AussieArtists (FirstName, LastName) SELECT LEFT(Name,ISNULL(NULLIF(CHARINDEXC '.Name),0),255)-l), SUBSTRING(Name,NULLIF(CHARINDEXC '.Name),0)+l.255) FROM inserted GO INSERT VAussieArtists (Name) VALUES ('Greg Ham') GO SELECT * FROM AussieArtists GO DROP TABLE AussieArtists DROP VIEW VAussieArtists GO (Результаты) Artistld 1 2 3 4 5 6 7 10 11 Как видите, простое добавление записи в представление трансформируется в чуть более сложное добавление записи в таблицу, на основе которой построено LastName Gibb Gibb Gibb Gibb Newton-John Crowe Hogan Kidman Bozinov Hay Ham FirstName Barry Maurice Robin Andy Olivia Russell Paul Nicole Zarko Colin Greg
INSTEAD-триггеры 209 представление. Поскольку мы хотим обработать данные до того, как они попадут в эту таблицу, мы используем I NSTEAD-триггер, который выполняет необходимые преобразования данных до их записи в таблицу. Хотя у нас может быть только один I NSTEAD-триггер для каждой DML-onepa- ции (INSERT, UPDATE или DELETE), никто не мешает нам обойти это ограничение, создав столько представлений к базовой таблице, сколько нам нужно, — и каждому присвоить свой I NSTEAD-триггер. Посмотрите на этот пример. Листинг 8.9. При помощи представлений можно создать много INSTEAD-триггеров USE tempdb GO CREATE TABLE AussieArtists (Artistld int Identity. LastName varcharOO). FirstName varcharOO) ) GO INSERT AussieArtists VALUES CGibb'. 'Barry') INSERT AussieArtists VALUES CGibb'. 'Maurice') INSERT AussieArtists VALUES CGibb', 'Robin') INSERT AussieArtists VALUES CGibb'. 'Andy') INSERT AussieArtists VALUES С Newton-John'. 'Olivia') INSERT AussieArtists VALUES ('Crowe'. 'Russell') INSERT AussieArtists VALUES CHogan'. 'Paul') INSERT AussieArtists VALUES ('Kidman'. 'Nicole') INSERT AussieArtists VALUES СBozinov'. 'Zarko') v ? ,v. \ INSERT AussieArtists VALUES ('Hay'. 'Colin') GO CREATE VIEW VAussieArtists AS" SELECT FirstName+' '+LastName AS Name FROM AussieArtists GO CREATE TRIGGER VAussieArtistsJNSERT ON VAussieArtists INSTEAD OF INSERT AS INSERT AussieArtists (FirstName, LastName) SELECT LEFT(Name.ISNULL(NULLIF(CHARINDEXC '.Name).0),255)-l). SUBSTRING(Name,NULLIF(CHARINDEXC ' .Name) .0)+l.255) FROM inserted GO CREATE VIEW VAussies AS SELECT Name FROM VAussieArtists GO CREATE TRIGGER VAussiesJNSERT ON VAussies INSTEAD OF INSERT AS INSERT VAussieArtists (Name) SELECT UPPER(Name) FROM inserted GO INSERT VAussies (Name) VALUES ('Greg Ham') GO SELECT * FROM AussieArtists Artistld LastName FirstName продолжение #
210 Глава 8. Триггеры Листинг 8.9 {продолжение) Gibb Gibb Gibb Gibb Newton-John Crowe Hog an Kidman Bozinov Hay HAM Barry Maurice Robin Andy Olivia Russell Paul Nicole Zarko Colin GREG Как вы видите, первый I NSTEAD-триггер отделяет имя от фамилии (так же, как в предыдущем примере). Второй I NSTEAD-триггер преобразует их в верхний регистр и добавляет в первое представление. Заметьте, что он не добавляет данные напрямую в таблицу, — это делается только в первом представлении. Добавляя данные в первое представление, мы будем уверены в том, что имя и фамилия будут корректно разделены. Можно использовать этот метод — своеобразную иерархию представлений с I NSTEAD-триггерами для каждого из них, — чтобы настроить достаточно сложную обработку, не прибегая к помощи хранимых процедур. Триггеры и аудит Я уже затрагивал эту тему в некоторых примерах, но поскольку аудит — это очень популярное использование триггеров, он заслуживает более детального рассмотрения. AFTER-триггеры часто используются для трассировки изменений в таблицах. Можно просто сохранять сам факт изменения записи, а можно фиксировать все изменения, произошедшие с таблицей. Вот простой пример аудита, реализованный с использованием триггеров. Листинг 8.10. Простой триггер аудита USE tempdb GO CREATE TABLE Toylnventory (Toy int identity. Type int. Onhand int ) CREATE TABLE ToyAudit (ToyAudit int identity, Operation varchar(lO), Toy int. Type int. Change int ) GO . .. CREATE TRIGGER ToylnventoryJNSERT ON Toylnventory AFTER INSERT AS IF @(aROWCOUNT=0 RETURN INSERT ToyAudit
Триггеры и аудит 211 SELECT 'INSERT', * FROM inserted GO INSERT Toylnventory DEFAULT VALUES GO Здесь мы просто сохраняем все записываемые в таблицу Toy I nventory данные во вторую таблицу ToyAud it. Таблица ToyAud 11 имеет столбец, который содержит название операции, добавившей запись в лог. Наш триггер для таблицы Toy I nvent о гу идентифицирует себя строчкой INSERT в этом поле. А если мы захотим сделать что-нибудь более сложное? Например, отслеживать данные в записях до и после изменения? В этом нам помогут AFTER UPDATE-тригге- ры. Рассмотрим пример. Листинг 8.11. Триггер, отслеживающий данные в записях до и после изменения CREATE TABLE Toylnventory (Toy int identity, Type int, Onhand int ) GO CREATE TABLE ToyAudit (ToyAudit int identity. Operation varcharB0), Toy int. Type int, Onhand int ) GO INSERT Toylnventory (Type, Onhand) VALUES A, 50) INSERT Toylnventory (Type, Onhand) VALUES B, 50) INSERT Toylnventory (Type: Onhand) VALUES C, 50) INSERT Toylnventory (Type, Onhand) VALUES D, 50) GO CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS IF «aR0WCOUNT=0 RETURN INSERT ToyAudit (Operation, Toy, Type, Onhand) SELECT 'UPDATE--BEFORE', * FROM deleted ORDER BY Toy INSERT ToyAudit (Operation, Toy, Type, Onhand) SELECT 'UPDATE-AFTER', * FROM inserted ORDER BY Toy GO UPDATE Toylnventory SET Onhand = 49 GO SELECT * FROM ToyAudit ORDER BY Toy, Operation, ToyAudit (Результаты) ToyAudit Operation Toy Type Onhand 5 UPDATE-AFTER 1 1 49 1 UPDATE--BEFORE 1 1 50 6 UPDATE-AFTER 2 2 49 2 UPDATE--BEFORE 2 2 50 7 UPDATE-AFTER 3 3 49 3 UPDATE--BEFORE 3 3 50 8 UPDATE-AFTER 4 4 49 4 UPDATE--BEFORE 4 4 50
212 Глава 8. Триггеры В этом коде продемонстрированы несколько методов, которые мы сейчас обсудим. Мы используем виртуальную таблицу de I eted для получения оригинальных записей, подвергающихся изменению, и вставляем их в таблицу аудита. Аналогично из таблицы i nserted мы получаем уже обновленные записи и помещаем их в таблицу аудита. В каждом из этих случаев при вставке мы сортируем записи по столбцу Toy для того, чтобы i dent i ty-значения в таблице ToyAud i t присваивались в требуемом порядке. Позже (при выборке записей из таблицы ToyAud i t) это позволит нам использовать сортировку по столбцам Toy и ToyAud i t. В результате такой сортировки мы получаем выборку, в которой запись с новыми значениями следует непосредственно за записью со старыми значениями несмотря на то, что эти записи прибыли из разных виртуальных таблиц. Обратите внимание на использование двойного дефиса: он необходим для того, чтобы записи со старыми значениями при сортировке были размещены ранее записей с новыми значениями. Например, мы сортируем по символьному значению столбца Operat i on и хотим, чтобы слово BEFORE следовало перед словом AFTER. Добавление второго дефиса — простейший способ сделать это. Хорошо, но что если мы хотим видеть, какие именно столбцы были изменены? Или если мы хотим видеть список измененных столбцов в таблице аудита? Это довольно легко сделать при помощи описанного ранее метода определения названий измененных столбцов, используя функцию COLUMNS_UPDATE() и представление INFORMATI 0N_SCHEMA, COLUMNS (листинг 8.12). Листинг 8.12. При помощи COLUMNS_UPDATE() триггер может фиксировать названия измененных столбцов USE tempdb GO CREATE TABLE Toylnventory (Toy int identity, Type int. Onhand int ) GO CREATE TABLE ToyAudit (ToyAudit int identity, Operation varcharB0), Toy int. Type int. Onhand int, ColumnsModitied varcharG000) ) GO INSERT Toylnventory (Type, Onhand) VALUES A. 50) INSERT Toylnventory (Type, Onhand) VALUES B. 50) INSERT Toylnventory (Type, Onhand) VALUES C, 50) INSERT Toylnventory (Type. Onhand) VALUES D. 50) GO CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS IF @@ROWCOUNT=0 RETURN DECLARE @ChangedColumns varbinary(8000) SET @ChangedColumns=COLUMNS_UPDATED()
Транзакции 213 INSERT ToyAudit (Operation, Toy. Type. Onhand. ColumnsModified) SELECT 'UPDATE--BEFORE', d.*. с.COLUMNJAME FROM deleted d JOIN INFORMATION_SCHEMA.COLUMNS с ON ((c.TABLE_NAME='ToyInventory') AND ((@ChangedColumns & P0WERB.с ORDINAL J>OSITION- 1))<>0)) ORDER BY d.Toy • INSERT ToyAudit (Operation, Toy. Type. Onhand. ColumnsModified) SELECT 'UPDATE-AFTER', i.*. с.COLUMNJAME FROM inserted i JOIN INFORMATIONJCHEMA.COLUMNS с ON ((c.TABLE_NAME='ToyInventory') AND ((@ChangedColumns & P0WERB,c.0RDINAL_P0SITI0N- 1))<>0)) ORDER BY i.Toy GO ' UPDATE ToyInventory SET Onhand = 49. Type=3 GO SELECT * FROM ToyAudit ORDER BY Toy. Operation. ToyAudit. ColumnsModified (Результаты) ToyAudit Operation 1 UPDATE--BEFORE 2 UPDATE--BEFORE 9 UPDATE-AFTER 10 UPDATE-AFTER 3 UPDATE--BEFORE 4 UPDATE--BEFORE 11 UPDATE-AFTER 12 UPDATE-AFTER 5 UPDATE-BEFORE 6 UPDATE--BEFORE 13 UPDATE-AFTER 14 UPDATE-AFTER 7 UPDATE--BEFORE 8 UPDATE--BEFORE 15 UPDATE-AFTER 16 UPDATE-AFTER Toy 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 Type 1 1 3 3 2 2 3 3 3 3 3 3 4 4 3 3 Onhand 50 50 49 49 50 50 49 49 50 50 49 49 50 50 49 49 ColumnsModified Type Onhand Type Onhand Onhand Type Onhand Type Type . Onhand Type Onhand Onhand Type Onhand Type Транзакции Когда нет внешней транзакции, триггер и запустившая его DML-операция помещаются в отдельную транзакцию. Если из триггера вызываются хранимые процедуры или пользовательские функуции, — все они выполняются в этой же транзакции. Если в триггере происходит фатальная ошибка или явный вызов ROLLBACK TRANSACTION, то активная на данный момент транзакция откатывается и выполнение пакета команд прерывается. Если активна внешняя транзакция, то она откатывается целиком вместе со всеми изменениями, произведенными ранее. Если активна неявная транзакция, вызванная выполняемой DML-инструкцией, откатывается только эта DML-инструкция. Для операции INSERT виртуальная таблица inserted содержит добавленные в таблицу записи. Для операции DELETE удаляемые записи помещаются в таблицу delet?.
214 Глава 8. Триггеры В случае изменения данных операция UPDATE помещает в таблицу deleted старые значения изменяемых данных, а в таблицу inserted — новые. Эти таблицы можно опрашивать для разрешения или запрещения изменения данных на основании списка обновляемых столбцов или данных в них. Откат текущей транзакции является обычным способом запрета изменений, потому что Transact SQL не поддерживает команду ROLLBACK TRIGGER (как в Sybase). Заметьте, что в AFTER-триггерах вы не можете изменять эти виртуальные таблицы, — они предоставлены только для чтения. Если данные изменять необходимо, нужно использовать I NSTEAD-триггеры. Минимально регистрируемые операции не запускают триггеры. Например, операция TRUNCATE TABLE удаляет все записи в таблице, но информация о факте удаления каждой записи в отдельности не заносится в журнал, и поэтому не срабатывает запуск триггеров для команды delete. Выполнение Триггеры запускаются сразу после того, как отработала вызвавшая их DML-инст- рукция, но до фиксирования изменений в базе данных. Внутри триггера считается, что вызвавшие триггер изменения данных уже завершились. Например, внутри I NSERT-триггера вставляемые записи уже присутствуют в таблице. План выполнения DML-инструкций показывает нам, что запуск триггеров происходит непосредственно перед завершением команды DML. Если триггер допускает изменения и нет внешней транзакции, результат DML-инструкции фиксируется в базе данных. Вызов хранимых процедур Выигрышный подход проектирования триггеров и уменьшение избыточности кода предлагают нам помещать код, общий для нескольких триггеров, в отдельную хранимую процедуру, которая будет вызываться из них. Недостатком этого подхода является то, что в хранимой процедуре не будут доступны виртуальные таблицы i nserted и de I eted. С другой стороны, с точки зрения триггера, изменения данных, запустившие триггер, уже произошли. Это справедливо и для вызываемого из триггера кода, в том числе и в хранимых процедурах. Таким образом, хранимая проце-! дура имеет доступ к содержимому таблиц i nserted и deleted через обращение к базовой таблице. Триггер может передать в процедуру необходимую информацию для поиска строк, содержащихся в таблицах i nserted и de I eted, через ее параметры. Рассмотрим это на примере. Листинг 8.13. Помещение кода триггеров в хранимые процедуры USE tempdb GO CREATE TABLE Toylnventory - ■ • . -4 (Toy int identity. Type int. Onhand int ) GO
Вызов хранимых процедур 215 CREATE TABLE ToyTypes (Type int identity. MinOnhand int ) GO INSERT ToyTypes (MinOnhand) VALUES A0) INSERT ToyTypes (MinOnhand) VALUES B0) INSERT ToyTypes (MinOnhand) VALUES A5) INSERT ToyTypes (MinOnhand) VALUES E0) INSERT Toylnventory (Type. Onhand)yALUES A, 50) INSERT Toylnventory (Type, OnhandTVALUES B, 50) INSERT Toylnventory (Type. Onharfti) VALUES C. 50) INSERT Toylnventory (Type. O/Kand) VALUES D. 50) GO DROP PROC CheckRowcaflht CREATE PROC OlckRowcount @rcnt int AS IF @rcnt > 1 BEGIN RAISERRORCYou may only change one item at a time. You are attempting to change %d rows in a single operation.',16,10,@rcnt) ROLLBACK RETURN END GO DROP PROC CheckOnhand GO CREATE PROC CheckOnhand @Type int. @Onhand int AS IF EXISTS (SELECT * FROM ToyTypes WHERE Type=@Type AND MinOnhandXsOnHand) BEGIN RAISERRORCYou may not lower an item"s Onhand quantity below its Mininum Onhand quantity' .16.10) ROLLBACK RETURN END GO CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS DECLARE @rcnt int SET @rcnt=@@ROWCOUNT IF @rcnt=D RETURN EXEC CheckRowcount @rcnt IF (UPDATE(Onhand)) BEGIN DECLARE @Type int, @Onhand int SELECT @Type=Type. @Onhand=Onhand FROM inserted EXEC CheckOnhand (PType, @Onhand END GO UPDATE Toylnventory SET Onhand=49 WHERE Toy=4 -- Fails because of trigger (Результаты) Server: Msg 50000, Level 16. State 10, Procedure CheckOnhand, Line 5 You may not lower an item's Onhand quantity below its Mininum Onhand quantity
216 Глава 8. Триггеры В этом примере мы вызываем из триггера две хранимые процедуры: одна - проверяет количество строк, затронутых операцией, и пропускает изменения только одной строки; вторая — проверяет на допустимость значение в столбце Onhand. Поскольку мы ограничиваем действие триггера только одной строкой, все, что мы должны сделать, — это передать процедуре CheckOnhand информацию о том, какие записи были изменены. Эту критическую информацию можно передать в качестве параметров процедуры. CheckOnhand принимает эти параметры и ищет в таблице ToyTypes минимальные значения для Onhand заданного типа. Если значение, помещаемое в таблицу, меньше минимального, мы сообщаем об ошибке и откатываем транзакцию прямо из процедуры. Поскольку обе проверки помещены в хранимые процедуры, мы можем вызвать их из разных триггеров. Более того, при таком подходе изменение бизнес-правил гораздо проще, — ведь они сосредоточены в нескольких хранимых процедурах, а не во множестве триггеров. Например, нам требуется, чтобы в тексте ошибки было указано, сколько именно строк мы хотим обновить. Если проверка количества строк вынесена в хранимую процедуру, то мы просто меняем текст ошибки в хранимой процедуре и... смело идем пить национальный напиток. А если проверка не была вынесена в хранимую процедуру, то нам придется (увы!) редактировать все триггеры. А как нам действовать, если мы допускаем операции с обновлением нескольких строк и хотим вынести код в хранимые процедуры? Это не так уж сложно. Давайте рассмотрим код. Листинг 8.14. Помещение кода триггеров в хранимые процедуры USE tempdb GO CREATE TABLE Toylnventory ( Toy int identity PRIMARY KEY, Type int, Onhand int ) GO CREATE TABLE ToyTypes ( Type int identity PRIMARY KEY, MinOnhand int ) GO INSERT ToyTypes (MinOnhand) VALUES A0) INSERT ToyTypes (MinOnhand) VALUES B0) INSERT ToyTypes (MinOnhand) VALUES A5) INSERT ToyTypes (MinOnhand) VALUES E0) INSERT Toylnventory (Type, Onhand) VALUES A, 50) INSERT Toylnventory (Type, Onhand) VALUES B. 50) INSERT Toylnventory (Type, Onhand) VALUES C, 50) INSERT Toylnventory (Type, Onhand) VALUES D, 50) GO DROP PROC CheckOnhand GO CREATE PROC CheckOnhand OMinToy int. @MaxToy int
Отключение триггеров 217 AS IF EXISTS (SELECT * FROM ToyTypes t JOIN Toylnventory i ON (t.TypeH.Type) WHERE i.Toy BETWEEN PMinToy AND PMaxToy AND MinOnhand>i.Onhand) BEGIN RAISERRORCYou may not lower an itenV's Onhand quantity below its Mininum Onhand quantity',16.10) ROLLBACK RETURN END GO CREATE TRIGGER ToylnventoryJJPDATE ON Toylnventory AFTER UPDATE AS IF @@ROWCOUNT=0 RETURN IF (UPDATE(Onhand)) BEGIN DECLARE (HMinToy int. (PMaxToy int SELECT @MinToy=MIN(Toy), @MaxToy=MAX(Toy) FROM inserted EXEC CheckOnhand @MinToy. @MaxToy END GO UPDATE Toylnventory SET Onhand=Onhand-l (Результаты) Server: Msg 50000. Level 16. State 10. Procedure CheckOnhand. Line 5 You may not lower an item's Onhand quantity below its Mininum Onhand quantity Мы сохраняем максимальное и минимальное значения столбца Toy из виртуальной таблицы i nserted. Затем вызываем процедуру CheckOnhand, передавая ей в качестве параметров границы-диапазона, в которой проверяем значения Onhand для записей из этого диапазона на допустимость. ChechOnhand принимает параметры, которые мы ей передаем, и соединяет таблицы ToyTypes и Toy I nventory по столбцу Туре, используя дополнительный фильтр на столбец Onhand. Если в данном диапазоне находятся записи в Toy I nventory, количество которых меньше допустимого, мы выдаем сообщение об ошибке и откатываем транзакцию. Таким образом, мы эффективно перенесли проверку столбца Onhand из триггера в хранимую процедуру и дали хранимой процедуре полный доступ к изменяемым данным. Отключение триггеров Можно отключить триггер с помощью команды ALTER TABLE... DI SABLE TRIGGER. Отключенный триггер молено включить командой ALTER TABLE... ENABLE TRIGGER. Рассмотрим пример. Листинг 8.15. Запрещение и разрешение триггеров ALTER TABLE sales DISABLE TRIGGER SalesQty_INSERT_UPDATE ALTER TABLE sales ENABLE TRIGGER SalesQty_INSERTJJPDATE ALTER TABLE sales продолжение #
218 Глава 8. Триггеры Листинг 8.15 {продолжение) DISABLE TRIGGER ALL ALTER TABLE sales ENABLE TRIGGER ALL Полезные советы Триггеры являются всего лишь скрытыми хранимыми процедурами. У них есть несколько дополнительных возможностей и нюансов, однако общий подход к написанию триггеров такой же, как и для хранимых процедур. Далее приведены несколько полезных советов. ■ Наделяйте ваши триггеры функциональностью, допускающей изменение нескольких записей за операцию. Триггеры, отлично работающие на операциях с одной записью, часто дают сбой на операциях, затрагивающих несколько записей. Это распространенная ошибка новичков, старайтесь ее избегать. " ■ Начинайте каждый триггер с @@R0WC0UNT, чтобы проверить, изменилась ли хотя бы одна строка. Если @@R0WC0UNT равна нулю, немедленно выходите из триггера. ■ Используйте функции UPDATE() и COLUMNS_UPDATE(), чтобы проверить, что значения, изменения которых вам нужно отследить, действительно произошли. ■ Никогда не ждите действий пользователя внутри триггеров. ■ Проверяйте ошибки после существенных операций (например, DML-инструк- ций) внутри триггера. Как и в хранимых процедурах, команды внутри триггеров должны проверяться на ошибки. ■ Сократите количество операций в триггерах до минимума. Триггеры должны выполняться настолько быстро, насколько это возможно, чтобы не снижать общую производительность системы. ■ Делайте информативные сообщения об ошибках. Пользователю гораздо приятнее получать осмысленный текст вместо безликого номера системной ошибки. ■ Используйте модульный принцип при проектировании триггеров. Старайтесь размещать часто встречающиеся или длинные и сложные куски кода в отдельных хранимых процедурах. ■ Проверяйте на надежность триггеры, реализующие ссылочную целостность. Убедитесь, что возможные комбинации изменения столбцов охвачены вашим триггером. ■ Напишите тестовый скрипт для каждого из ваших триггеров. Удостоверьтесь, что вы не обошли вниманием любую ситуацию, нуждающуюся в обработке. В листинге 8.16 содержится несколько примеров триггеров. Листинг 8.16. Примеры написания триггеров SET NOCOUNT ON USE pubs DROP TRIGGER SalesQtyJNSERTJJPDATE GO CREATE TRIGGER SalesQtyJNSERTJJPDATE ON sales FOR INSERT. UPDATE AS
Полезные советы 219 IF 0(aROWCOUNT=0 RETURN -- No rows affected, exit immediately IF (UPDATE(qty)) AND (SELECT MIN(qty) FROM inserted)<10 BEGIN RAISERRORCMinimum order is 10 units',16,10) ROLLBACK TRAN RETURN END GO -- Test a single-row INSERT BEGIN TRAN INSERT sales VALUES F380,'ORD9997'.GETDATEO,5,'Net 60','BU1032') IF @@TRANCOUNT>0 ROLLBACK TRAN GO -• Test a multi-row INSERT BEGIN TRAN INSERT sales SELECT stor_id, ord_num+'A', ord_date, 5, payterms, titlejd FROM sales IF @@TRANCOUNT>0 ROLLBACK TRAN GO DROP TRIGGER Sales_DELETE GO CREATE TRIGGER Sales_DELETE ON sales FOR DELETE AS IF @@ROWCOUNT=0 RETURN -- No rows affected, exit immediately IF (@@R0WC0UNT>1) BEGIN RAISERROR('Deletions of more than one row at a time are not permitted',16,10) ROLLBACK TRAN RETURN END GO BEGIN TRAN DELETE sales IF @@TRANCOUNT>0 ROLLBACK TRAN GO DROP TRIGGER Salesord_date_qty_UPDATE GO CREATE TRIGGER Salesord_date_qty_UPDATE ON sales FOR INSERT, UPDATE AS IF @@ROWCOUNT=0 RETURN -- No rows affected, exit immediately -- Check to see whether the 3rd and 4th columns are being updated simultaneously IF (COLUMNSJJPDATEDO & (P0WERB,3-1) | P0WERB,4-1) ))=12 BEGIN UPDATE s SET payterms='Cash' FROM sales s JOIN inserted i ON (s ,stor_id=i .storjd AND s ,ord_num=i .ordjium) IF (@@ERROR<>0) -- UPDATE generated an error, rollback transaction ROLLBACK TRANSACTION RETURN END GO - Test with a single-row UPDATE продолжение #
220 Глава 8. Триггеры Листинг 8.16 {продолжение) BEGIN TRAN UPDATE sales SET ord_date=GETDATE(). qty=15 WHERE stor_id=7066 and ord_num='A2976' SELECT * FROM sales WHERE stor_id=7066 and ord_num='A2976' IF @@TRANCOUNT>0 ROLLBACK TRAN GO -- Test with a multi-row UPDATE BEGIN TRAN UPDATE sales SET ord_date=GETDATE(), qty=15 WHERE stor_id-7066 SELECT * FROM sales WHERE stor_id=7066 IF @@TRANCOUNT>0 ROLLBACK TRAN Server: Msg 50000, Level 16, State 10, Procedure SalesQtyJNSERTJJPDATE, Line 6 Minimum order is 10 units Server: Msg 50000, Level 16, State 10, Procedure SalesQtyJNSERTJJPDATE, Line 6 Minimum order is 10 units stor_id ordjium ord_date qty payterms title_id 7066 A2976 2004-02-16 13:26:09.253 15 Cash PC8BB8 stor_id ordjium ord_date qty payterms titlejd 7066 A2976 2004-02-16 13:26:09.283 15 Cash PC8888 7066 QA7442.3 2004-02-16 13:26:09.283 15 Cash PS2091 Итоги В этой главе вы узнали: ■ как использовать триггеры для реализации различных практических задач; ■ как при помощи триггеров реализовать ссылочную целостность в базе данных; ■ как создавать триггеры для таблиц и представлений; ■ какие существуют методы написания эффективных и нетребовательных в обслуживании триггеров; ■ как именно обрабатываются триггеры сервером. )
Представления Я хотел бы лично поблагодарить всех тех, кто сомневался во мне в прошлом. Всех, кто недооценивал меня, кто считал меня безнадежным неудачником, — вы стали для меня источником вдохновения. Вы заставили меня работать усерднее и стараться прилежнее. И за это я вам очень благодарен. X. В. Кептон Представление — это объект SQL Server, представляющий собой статический запрос, с которым можно работать как с виртуальной таблицей при помощи операторов SELECT, I NSERT, UPDATE и DELETE. Представление — это неизменяемое выражение SELECT, сохраненное в особом объекте при помощи команды CREATE VI EW. Представления обычно используются для инкапсулирования сложных соединений и запросов, чтобы впоследствии обращаться с ними, как с таблицей. В качестве столбцов представления могут выступать: столбцы таблиц и других представлений, агрегатные функции, константы и выражения (вычисляемые столбцы). Некоторые представления являются обновляемыми, некоторые — нет. Будет ли представление обновляемым, зависит от большого числа факторов, но, на деле, все сводится к тому, сможет ли SQL Server корректно сопоставить обновление одной записи в представлении обновлению единственной записи в одной используемой таблице. Все представления в конечном итоге должны ссылаться на таблицы или на нетабличные выражения (например, GetDate()). Поскольку допускаются вложения, то каждое представление может ссылаться на другое представление, но в любом случае каждому столбцу представления сопоставляется либо столбец таблицы, либо нетабличное выражение. Метаданные Функция OBJECTPROPERTY() поддерживает несколько свойств, относящихся к представлениям. Эти свойства представлены в табл. 9.1. Таблица 9.1. Свойства OBJECTPROPERTY0, относящиеся к представлениям Свойство Функция ExecIsAnsiNullsOn Показывает, была ли установлена настройка ANSI_NULLS в момент создания или изменения представления продолжение J>
222 Глава 9. Представления Таблица 9.1 {продолжение) Свойство Функция ExecIsQuotedldentOn HasAfterTrigger HasInsertTrigger HasInsteadOfTrigger HasUpdateTrigger IsAnsiNullsOn IsDeterministic IsExecuted Islndexable Islndexed IsQuotedldentOn IsSchemaBound IsView Показывает, была ли установлена опция QUOTED_IDENTIFIER в момент создания или изменения представления Показывает, имеется ли у представления AFTER-триггер Показывает, имеется ли у представления INSERT-триггер Показывает, имеется ли у представления INSTEAD-триггер Показывает, имеется ли у представления UPDATE-триггер Показывает, была ли установлена опция ANSI_NULLS в момент создания или изменения представления (аналогично ExecIsAnsiNullsOn) Показывает, возвращает ли представление постоянно одинаковые результаты независимо от параметров Возвращает 1 для представлений и других вычисляемых объектов: хранимых процедур, пользовательских функций и таблиц с вычисляемыми столбцами Показывает, может ли на представлении быть создан индекс Показывает, создан ли индекс на представлении Показывает, была ли установлена опция QUOTED_IDENTIFIER в момент создания или изменения представления (аналогично ExecIsQuotedldentOn) Показывает, является ли представление привязанным к схеме Возвращает 1 для представлений Исходный код представлений Если представление было создано без опции WITH ENCRYPTION, для получения его исходного кода можно использовать процедуру sp_he I ptext. Представления можно просматривать и изменять из Enterprise Manager или любых других утилит администрирования, использующего SQL-DMO. Рассмотрим простой пример, возвращающий исходный код системного представления sys I од i ns (листинг 9.1). Листинг 9.1. sp_helptext позволяет получать исходный код представлений USE master EXEC sp_hel ptext syslogins Text CREATE VIEW syslogins AS SELECT sid = convert(varbinary(85). sid), status = converttsmallint. 8 + CASE WHEN (xstatus & 2)=0 THEN 1 ELSE 2 END), createdate = converttdatetime, xdatel), updatedate = convert(datetime, xdate2). accdate = convert(datetime. xdatel). totcpu = convertdnt, 0). totio = convert(int. 0). spacelimit = convertdnt. 0). timelimit = convert(int. 0). result!imit = convertdnt. 0), name = convert(sysname. name).
Ограничения 223 dbname = convert(sysname. dbjname(dbid)). password = convert(sysname. password), language = convert(sysname. language). denylogin = convert(int. CASE WHEN (xstatus&l)=l THEN 1 ELSE 0 END), hasaccess = convert(int. CASE WHEN (xstatus&2)=2 THEN 1 ELSE 0 END), isntname = convertdnt. CASE WHEN (xstatus&4)=4 THEN 1 ELSE 0 END), isntgroup = converttint. CASE WHEN (xstatus&12)=4 THEN 1 ELSE 0 END), isntuser = convertCint, CASE WHEN (xstatus&12)=12 THEN 1 ELSE 0 END), sysadmin = convert(int. CASE WHEN (xstatus&16)=16 THEN 1 ELSE 0 END), securityadmin = convertCint. CASE WHEN (xstatus&32)=32 THEN 1 ELSE 0 END), serveradmin = convertCint. CASE WHEN (xstatus&64)=64 THEN 1 ELSE 0 END), setupadmin = convertCint, CASE WHEN (xstatus&128)=128 THEN 1 ELSE 0 END), processadmin = convertdnt. CASE WHEN (xstatus&256)=256 THEN 1 ELSE 0 END), diskadmin = convert(int. CASE WHEN (xstatus&512)=512 THEN 1 ELSE 0 END), dbcreator = convertCint, CASE WHEN (xstatus&1024)=1024 THEN 1 ELSE 0 END), bulkadmin = convertCint. CASE WHEN (xstatus&4096)=4096 THEN 1 ELSE 0 END), loginname = convert(sysname. name) FROM sysxlogins WHERE srvid IS NULL Ограничения Transact-SQL не поддерживает создание временных представлений, хотя можно создать постоянное представление в базе tempdb и добиться похожего эффекта. Вложенные запросы могут рассматриваться как замена временным представлениям так же, как и табличные переменные с сохраненным результатом SQL-запроса. Представления не могут ссылаться на временные таблицы. Допускаются только ссылки на постоянные таблицы и другие представления, у Как правило, ORDER BY не используется в представлениях: Такой синтаксис недопустим: -- недопустимый синтаксис Transact-SQL CREATE VIEW myauthors AS SELECT * FROM AUTHORS ■ , ORDER BY aujname Однако это запрещение можно обойти. Для этого достаточно использовать ключевое слово ТОР для разрешения использования ORDER BY в представлениях, как показано в следующем примере. Листинг 9.2. SELECT TOP убирает ограничение ORDER BY CREATE VIEW myauthors AS SELECT TOP 100 PERCENT * FROM authors ORDER BY aujname В листинге 9.3 показано, какой эффект имеет ORDER BY при простом запросе к представлению. Листинг 9.3. Включение ORDER BY в представление упорядочивает результат SELECT au_id. aujname. au_fname FROM myauthors aii id au Iname au fname 409-56-7008 Bennet Abraham ■ „„„„„„„«„„.«, о ,,&■.: _. . продолжением
224 Глава 9. Представления Листинг 9.3 {продолжение) 648-92-1872 Blotchet-Halls 238-95-7766 Carson 722-51-5454 DeFrance 712-45-1867 del Castillo 427-17-2319 Dull 213-46-8915 Green 527-72-3246 Greene 472-27-2349 Gringlesby 846-92-7186 Hunter 756-30-7391 Karsen 486-29-1786 Locksley 724-80-9391 MacFeather 893-72-1158 McBadden 267-41-2394 O'Leary 807-91-6654 Panteley 998-72-3567 Ringer 899-46-2035 Ringer 341-22-1782 Smith 274-80-9391 Straight 724-08-9931 Stringer 172-32-1176 White 672-71-3249 Yokomoto Reginald Cheryl Michel Innes Ann Marjorie • Morningstar Burt Sheryl Livia Charlene Stearns Heather Michael Sylvia Albert Anne Meander Dean Dirk Johnson Акт ко Однако необходимо понимать, что порядок записей не гарантирован, даже если ORDER BY включен в представление. Параллельная обработка данных и другие операции со стороны SQL Server могут привести к нарушению порядка вывода записей. Для того чтобы несомненно обеспечить нужный порядок вывода записей, необходимо явно указать ORDER BY в SELECT-запросах к представлению. -— ANSI_NULLS и QUOTED_IDENTIFIERS Как в хранимых, процедурах, настройки SETANSI _NULLS и SETQUOTED_ I DENT IFIERS сохраняются вместе с каждым представлением. Это означает, что индивидуальные настройки сессии игнорируются при обращении к представлению. Это также означает, что можно локализовать настройки обработки кавычек и значений NULL внутри представления и не затрагивать внешние настройки при запросах к этому представлению. Ограничения DML UPDATE представления в случае отсутствия у него I NSTEAD-триггера не допускается, если этот оператор затрагивает более одной зависимой таблицы. Если в представлении объединяется более одной таблицы, то UPDATE может изменять данные только в одной из них. Аналогично, добавление записей в представление может оперировать тоже только с одной таблицей — столбцы других таблиц должны иметь значения по умолчанию, допускать NULL или каким-либо другим способом быть необязательными. Стандартные представления схемы данных SQL Server содержит несколько представлений для доступа к системным каталогам. Эти объекты декларируются в стандарте ANSI SQL-92 для доступа к метаданным и системной информации. Рекомендую использовать их вместо прямых запросов к системным таблицам по двум причинам.
Ограничения 225 1. Вам не потребуется делать никаких изменений, если при переходе к следующей версии SQL Server структура системных таблиц изменится. 2. Эти представления являются частью спецификации ANSI SQL-92, поэтому не придется менять код при переходе на другую платформу баз данных. •' В SQL Server присутствуют следующие представления стандарта ANSI SQL-92: ■ CHECK_CONSTRAINTS ■ C0LUMNJ30MAINJJSAGE ,-.....,-.. ■ C0LUMN_PRIVI LEGES ■ COLUMNS . : ■ C0NSTRAINT_C0LUMN_USAGE ■ CONSTRAINT_TABLE_USAGE ■ DOMAIN_CONSTRAINTS ■ DOMAINS ■ KEY_COLUMN_USAGE ■ PARAMETERS ■ REFERENT IAL_CONSTRAI NTS ■ ROUTINE_COLUMNS ■ ROUTINES ■ SCHEMATA ■ TABLE_CONSTRAINTS ■ TABLE_PRIVILEGES --. . ' ■ TABLES V- . ■ VIEW_COLUMN_USAGE ■■■..... ■ VIEW_TABLE_USAGE ■ VIEWS ' Обратите внимание, что обращаться к этим представлениям следует при помощи схемы I NFORMAT 10N_SCHEMA. В синтаксисе SQL Server схема и владелец являются синонимами, поэтому вы будете использовать такой синтаксис: SELECT * FROM INFORMATION_SCHEMA.TABLES вместо SELECT * FROM TABLES Несмотря на то что описанные выше представления находятся только в базе данных master, они вызываются в контексте текущей базы данных. Замечу также, что если мы проверим свойство I sMSSh i pped у представлений из I NFORMAT 10N_SCHEMA, то увидим, что SQL Server относит их к системным объектам (подробнее о системных объектах рассказано в главе 22). :-,- ... ' я"■ •• Sv .-,'- Т Г Создание собственных представлений в INFORMATION_SCHEMA Было бы неплохо создавать представления в базе master, поскольку способность таких представлений работать в контексте текущей базы дает возможность мно-
226 Глава 9. Представления жества практических применений. Например, если у нас есть много пользовательских баз данных с одинаковым набором представлений в каждой, то можно уменьшить занимаемое место на диске и количество проблем администрирования, если поместить все эти представления в базу master (вместо того, чтобы хранить в каждой пользовательской базе по экземпляру). Позже, при вызове, они будут использовать контекст текущей базы данных. К счастью, такая возможность существует, правда, она не документирована. Поэтому, как и с любыми недокументированными приемами, следует помнить, что в будущих версиях SQL Server все может измениться. Чтобы создать собственные системные представления, требуется выполнить следующие действия. 1. Разрешить прямое обновление системных таблиц процедурой sp_conf i gure: sp_configure 'allow updates',1 RECONFIGURE WITH OVERRIDE 2. Установить автоматическое создание системных объектов с помощью недокументированной процедуры sp_MS_upd_sysobj_category (для этого необходимо быть владельцем базы или быть членом роли setupadm i n): sp_MS_upd_sysobj_category 1 Эта процедура устанавливает флаг трассировки 1717, необходимый для автоматической установки бита I sMSSh i pped у всех вновь создаваемых объектов. Этот шаг необходим, потому что создание несистемных объектов I NF0RMA- Т10N_SCHEMA не разрешается. Вообще говоря, после создания каждого представления можно установить этот бит с помощью процедуры sp_MS_mark- systemobj ect (как описано в главе 22). Но объекты INFORMAT10N_SCHEMA не могут быть несистемными, поэтому мы переводим SQL Server в специальный режим, в котором любой созданный объект автоматически становится системным. 3. Создать представление, указав его владельца: I NFORMAT 10N_SCHEMA, как показано в листинге 9.4. 4. Отключить автоматическое создание системных объектов при помощи повторного вызова sp_MS_upd_sysobj_category: sp_MS_upd_sysobj_category 2 5. Отключить прямое обновление системных таблиц: sp_configure 'allow updates'.0 RECONFIGURE WITH OVERRIDE Вот пример нового представления I NFORMAT 10N_SCHEMA. DI RECTORY. Это представление выводит все объекты и типы данных из базы данных в формате, похожем на формат команды DIR операционной системы. Листинг 9.4. Пользовательское представление в INFORMATION_SCHEMA USE master GO - - • ^ti^f •■-,-- \ : '' EXEC sp_configure 'allow'. 1 . GO •' , - " "? *•''*'•' RECONFIGURE WITH OVERRIDE GO v><<ifc*. • ' • ■ V EXEC sp_MS_upd_sysobj_category 1
Ограничения 227 GO -■ - IF OBJECT_ID('INFORMATION_SCHEMA.DIRECTORY') IS NOT NULL DROP VIEW INFORMATION_SCHEMA.DIRECTORY GO CREATE VIEW INFORMATIONJCHEMA.DIRECTORY /* Object: DIRECTORY Description: Lists object catalog information similar to the OS DIR command. Usage: SELECT * FROM INFORMATIONJCHEMA.DIRECTORY WHERE Name LIKE name mask AND Type LIKE object type ORDER BY T. Name name mask=pattern of object names to list object type=type of objects to list The following object types are listed: U=User tables S=System tables V=Views P=Stored procedures X=Extended procedures RF=Replication filter stored procedures TR=Triggers D=Default objects ,. \ R=Rule objects ..'..,- ■ I T=User-defined data types ' IF=Inline user-defined function TF=Table-valued user-defined function FN=Scalar user-defined function Created by: Ken Henderson. Email: khen@khen.com Version: 8.0 Example usage: SELECT * FROM INFORMATIONJCHEMA.DIRECTORY WHERE Name LIKE 'ORDr AND Туpe='U' ORDER BY T. Name Created: 1992-06-12. Last changed: 2000-11-12. */ . ■ AS SELECT TOP 100 PERCENT CASE GROUPING(T) WHEN 1 THEN '*' ELSE T END AS T. Name. Type. DateCreated. SUM(Rows) AS Rows. SUM(RowLenlnBytes) AS RowLenlnBytes. SUM(TotalSizelnKB) AS TotalSizelnKB. SUM(DataSpacelnKB) AS DataSpacelnKB. SUM(IndexSpacelnKB) AS IndexSpacelnKB. SUM(UnusedSpacelnKB) AS UnusedSpacelnKB. Owner FROM ( ■*•'' ",и SELECT-- Get regular objects Ц0> , <*H3^ ' >- продолжение^
228 Глава 9. Представлен Листинг 9.4 {продолжение) 1 ' AS Т. . Name=LEFT(o.name.30). Type=o.type. OateCreated=o.crdate, Rows=ISNULL(rows.O). RowLenInBytes=ISNULL((SELECT SUM(length) FROM syscolumns WHERE id=o.id AND o.type in ('IT . 'S')).0). TotalSizeInKB=ISNULL((SELECT SUM(reserved) FROM sysindexes WHERE indid in @. 1. 255) AND id=o.id).0)*2. DataSpaceInKB=ISNULL(((SELECT SUM(dpages) FROM sysindexes WHERE indid < 2 AND id=o.id)+ (SELECT ISNULL(SUM(used). 0) FROM sysindexes WHERE indid=255 AND id=o.id)).0)*2. IndexSpaceInKB=ISNULL(((SELECT SUM(used) FROM sysindexes WHERE indid in @. 1. 255) AND id=o.id) - ((SELECT SUM(dpages) FROM sysindexes ~"\ WHERE indid < 2 AND id=o.id)+ (SELECT ISNULL(SUM(used). 0) FROM sysindexes WHERE indid=255 AND id=o.id))),0)*2. UnusedSpaceInKB=ISNULL(((SELECT SUM(reserved) FROM sysindexes WHERE indid in @. 1. 255) AND id=o.id) - (SELECT SUM(used) FROM sysindexes WHERE indid in @. 1, 255) AND id=o.id)),0)*2. Owner=USER_NAME(o.uid) FROM sysobjects o, sysindexes i WHERE o.id*=i.id AND i.indid<=l UNION ALL -- Get user-defined data types SELECT ' '. LEFT(name.30). T, NULL. NULL. NULL. NULL. NULL. NULL. NULL. USERJAME(uid) FROM systypes st WHERE (usertype & 256)<>0 ) D GROUP BY T. Name.Type. DateCreated. Owner WITH ROLLUP HAVING (T+Name+Type+Owner IS NOT NULL) OR (COALESCED.Name.Type,Owner) IS NULL) ORDER BY T.name GO EXEC sp MS_upd_sysobj_category 2 GO EXEC sp_configure 'allow'. 0 GO RECONFIGURE WITH OVERRIDE GO Как только представление создано, его можно сразу использовать (как и другие представления из INFORMATI ON_SCHEMA) (листинг 9.5).
Ограничения 229 Листинг 9.5. Новое представление в действии USE pubs GO SELECT * FROM INFORMATION_SCHEMA.DIRECTORY GO (Результаты сокращены) T Name authors byroyalty CK authors au id 77BFCB91 CK authors zip 79A81403 OF publisher count 7D78A4E7 DF titles pubdate 023D5A04 DF titles type 00551192 discounts empid employee employee insupd FK discounts stor 0F975522 FK employee job id 1BFD2C07 FK employee pub id 1E0998B2 FK pub info pub id 173876EA id jobs PK_jobs 117F9D94 roysched sales . *■ stores tid titleauthor titles titleview "'■- UPK storeid UPKCL auidind UPKCL pubind UPKCL pubinfo UPKCL sales UPKCL taind UPKCL titleidind * NULL Type U P С С D D 0 U T U TR F F F F T U К и и и т и и V к к к к к к к NULL ; DateCreated Rows 2000-08-06 01:33:52.123 23 2000-08-06 01:33:58.547 0 2000-08-06 01:33:52.140 0 2000-08-06 01:33:52.153 0 2000-08-06 01:33:52.217 0 2000-08-06 01:33:52.403 0 2000-08-06 01:33:52.327 0 2000-08-06 01:33:52.873 3 NULL NULL 2000-08-06 01:33:53.203 43 2000-08-06 01:33:53.310 0 2000-08-06 01:33:52.873 0 2000-08-06 01:33:53.203 0 2000-08-06 01:33:53.203 0 2000-08-06 01:33:53.093 0 NULL NULL 2000-08-06 01:33:52.983 14 2000-08-06 01:33:52.983 0 2000-08-06 01:33:52.763 86 2000-08-06 01:33:52.653 21 2000-08-06 01:33:52.547 6 NULL NULL 2000-08-06 01:33:52.437 25 2000-08-06 01:33:52.327 18 2000-08-06 01:33:58.437 0 2000-08-06 01:33:52.547 0 2000-08-06 01:33:52.140 0 2000-08-06 01:33:52.217 0 2000-08-06 01:33:53.093 0 2000-08-06 01:33:52.653 0 2000-08-06 01:33:52.437 0 2000-08-06 01:33:52.327 0 NULL 1150 RowLe 151 0 0 0 0 0 0 53 NULL 75 0 0 0 0 0 NULL 54 0 18 52 111 NULL 22 334 ■ 0 0 0 0 0 0 - 0 0 71432 Как с системными хранимыми процедурами, можно указать префикс базы данных для системных представлений (даже если вы находитесь в контексте другой базы), и представление будет выполнено в контексте указанной базы. Давайте выполним это в следующем примере. Листинг 9.6. Представления INFORMATION_SCHEMA подобны системным процедурам USE pubs GO /* other code goes here */ SELECT * FROM Northwind.INFORMATION_SCHEMA.DIRECTORY . „, . -- GO , V4.> f:. ... (Результаты сокращены) э#*- :. ■:■•■ -f ,-<:,■ продолжение J>
230 Глава 9. Представления Листинг 9.6 (продолжение) Name Alphabetical list of prod Categories Category Sales for 1997 CK_Birthdate CK_Discount CK Products UnitPrice CK_Quantity Shippers Summary of Sales by Quart Summary of Sales by Year Suppliers Ten Most Expensive Produc Territories NULL Type V U V С С С С и V V и Р и NULL DateCreated 2000-08-06 01:34:09.420 2000-08-06 01:34:05.077 2000-08-06 01:34:11.530 2000-08-06 01:34:04.653 2000-08-06 01:34:08.470 2000-08-06 01:34:07.700 2000-08-06 01:34:08.470 2000-08-06 01:34:06.060 2000-08-06 01:34:12.187 2000-08-06 01:34:12.403 2000-08-06 01:34:06.187 2000-08-06 01:34:12.623 2000-08-06 01:34:54.077 NULL Rows 0 8 0 0 0 0 0 3 0 0 29 0 53 4580 RowLe 0 66 0 0 0 0 0 132 0 0 546 0 144 73623 Даже с текущей базой pubs представление выполняется в контексте базы No rt hw i ncl, как указано в префиксе. х Создание собственных UDF в INFORMATION_SCHEMA Мы не ограничимся созданием представлений в INFORMAT10N_SCHEMA. У нас есть возможность создавать и пользовательские функции. Эти понятия очень похожи, поскольку табличные пользовательские функции являются своего рода параметризованными представлениями. Ниже создается I NFORMAT 10N_SCHEMA UDF, которая работает как параметризованное представление и может вызываться в контексте любой базы данных (листинг 9.7). Листинг 9.7. Можно создавать не только представления, но и функции USE master GO EXEC sp_configure 'allow'. 1 GO RECONFIGURE WITH OVERRIDE GO EXEC sp_MS_upd_sysobj_category 1 GO IF OBJECT_ID('INFORMATION_SCHEMA.OBJECTS') IS NOT NULL DROP FUNCTION INFORMATION_SCHEMA.OBJECTS GO CREATE FUNCTION INFORMATION_SCHEMA.OBJECTS((amask sysname=X, @obtype varcharC)=T . @orderby varcharA000)=VN') /* . : . ... Object: OBJECTS Description: Lists object catalog information similar to the OS DIR command. Usage: SELECT * FROM INFORMATION_SCHEMA.OBJECTS() WHERE Name LIKE name mask AND Type LIKE object type ORDER BY T, Name name mask=pattern of object names to list
Ограничения 231 object type=type of objects to list The following object types are listed: IMJser tables S=System tables V=Views P=Stored procedures X=Extended procedures RF=Replication filter stored procedures TR=Triggers D=0efault objects R=Rule objects T=User-defined data types IF=Inline user-defined function TF=Table-valued user-defined function FN=Scalar user-defined function Created by: Ken Henderson. Email: khen@khen.com Version: 8.0 Example usage: SELECT * FROM INFORMATION_SCHEMA.OBJECTS('СТО','IT .DEFAULT) ORDER BY T, Name Created: 1992-06-12. Last changed: 2000-11-12. The following orderings are supported: /N = by name /R = by number of rows /S = by total object size /D = by date created /A = by total size of data pages /X = by total size of index pages /U = by total size of unused pages /L = by maximum row length /0 = by owner /T = by type */ RETURNS TABLE AS RETURN( SELECT TOP 100 PERCENT CASE GROUPING(T) WHEN 1 THEN '* Name. Type, DateCreated. SUM(Rows) AS Rows. SUM(RowLenlnBytes) AS RowLenlnBytes. SUM(TotalSizelnKB) AS Total SizelnKB. SUM(DataSpacelnKB) AS DataSpacelnKB, SUM(IndexSpacelnKB) AS IndexSpacelnKB. I"; SUM(UnusedSpacelnKB) AS UnusedSpacelnKB, Owner FROM ( SELECT -- Get regular objects ' ' AS T. •.•.;•■'•?■/ Name=LEFT(o.name,30), V'"'- '' "' ELSE T END AS T, продолжение &
232 Глава 9. Представления Листинг 9.7 (продолжение) Type=o.type. DateCreated=o.crdate, Rows=ISNULL(rows.O). RowLenInBytes=ISNULL((SELECT SUM(length) FROM syscolumns WHERE id=o.id AND o.type in ('U'.'S')).0), TotalSizeInt(B=ISNULL((SELECT SUM(reserved) FROM sysindexes WHERE indid in @, 1, 255) AND id=o.id),0)*2, DataSpaceInKB=ISNULL(((SELECT SUM(dpages) FROM sysindexes WHERE indid < 2 AND id=o.id)+ (SELECT ISNULL(SUMCused). 0) FROM sysindexes WHERE indid=255 AND id=o,id)),0)*2. IndexSpaceInKB=ISNULL(((SELECT SUM(used) FROM sysindexes WHERE indid in @, 1, 255) AND id=o.id) - ((SELECT SUM(dpages) FROM sysindexes WHERE indid < 2 AND id=o.id)+ " (SELECT ISNULL(SUM(used), 0) FROM sysindexes WHERE indid=255 AND id=o.id))).0)*2. UnusedSpaceInKB=ISNULL(((SELECT SUM(reserved) FROM sysindexes WHERE indid in @, 1. 255) AND id=o.id) - (SELECT SUM(used) FROM sysindexes WHERE indid in @, 1. 255) AND id=o.id)).0)*2. Owner=USER_NAME(o.uid) FROM sysobjects o, sysindexes i WHERE o.name LIKE @mask AND o.Type LIKE @obtype AND o.id*=i.id AND i.indid<=l UNION ALL -- Get user-defined data types SELECT ' ', LEFT(name.30). 'T', NULL, NULL. NULL. NULL, NULL, NULL, NULL. USER_NAME(uid) FROM systypes st WHERE name LIKE @mask AND 'Г LIKE @obtype AND (usertype & 256)<>0 ) D GROUP BY T. Name,Type, DateCreated, Owner WITH ROLLUP HAVING (T+Name+Type+Owner IS NOT NULL) OR (COALESCED.Name.Type,Owner) IS NULL) ORDER BY T. CASE UPPER(LEFT(@orderby.2)) WHEN 7D' THEN CONVERT(CHARC0).DateCreated,121) WHEN 7R' THEN REPLACE(STR(SUM(Rows).10.0),' '.'О') WHEN 7A' THEN REPLACE(STR(SUM(DataSpaceInKB),10,0),' '.'0') WHEN 7S' THEN REPLACE(STR(SUM(TotalSizeInKB),10,0),' ','0') WHEN 7X' THEN REPLACE(STR(SUM(IndexSpaceInKB).10,0),' ','0') WHEN 7U' THEN REPLACE(STR(SUM(UnusedSpaceInKB),10,0),' '.'0') WHEN 7L' THEN REPLACE(STR(SUM(RowLenInBytes).10,0),' ','0') WHEN 7T' THEN Type WHEN 70' THEN Owner END, Name -- Always sort by Name to break ties )
Ограничения 233 GO EXEC sp_MS_upd_sysobj_category 2 GO EXEC sp_configure 'allow', 0 GO - •' ' RECONFIGURE WITH OVERRIDE GO В этом коде продемонстрировано несколько приемов, достойных обсуждения. Сначала обратите внимание на метод, которым я объединил таблицы sysobj ects и systypes. Для объединения таблиц я использовал UN I ON ALL, подставляя константы и пустые значения для systypes там, где это необходимо. Объединение было оформлено через вложенный запрос, для сортировки которого я применил трюк с SELECT ТОР 100 PERCENT. Как я уже говорил, при использовании ORDER BY внутри представлений, вложенных запросов и функций порядок вывода результата не гарантируется. Однако в моих тестах результат выводится именно в нужном порядке, а это лучше, чем полное отсутствие сортировки. Далее обратите внимание на особую обработку столбца Т. Этот столбец введен с одной целью: обеспечить вывод суммирующей строчки после основных результатов запроса. Вот как это работает: мы используем GROUP BY... WITH R0LLUP для того, чтобы SQL Server посчитал суммы для цифровых столбцов в нашем запросе. Затем с помощью HAV ING мы отфильтровываем все промежуточные суммы, кроме / общего итога. Теперь с помощью CASE GROUP I NG(T) мы определяем, выполнялась ли группировка, и заносим в столбец Т символ * — если она выполнялась, или символ пробел — если нет. В выражении ORDER BY сортировка по Т выполняется до любой сортировки, указанной во втором параметре. Поскольку ASCII-коды пробела и * соответственно 32 и 42, то при сортировке итоговая запись будет выводиться всегда последней, а у пользователя останется возможность сортировать оставшиеся записи в любом требуемом порядке. В заключение посмотрим на выражение CASE в ORDER BY. Этот прием позволяет нам указывать порядок сортировки в качестве параметра функции. Замечу, что все выражения сортировки должны быть приведены к одному типу; числовые поля дополняются слева нулями. Это необходимо, потому что CASE приводит используемые типы данных к единому типу с максимальным приоритетом (см. Books Online, раздел «Data Type Precedence»). В нашем случае максимальным приоритетом обладает тип datet i me (столбец DateCreated). Как только CASE получает в свое распоряжение datet i me, он пытается преобразовать все остальные выражения в этот тип, что приводит к ошибке синтаксиса или преобразованию типов. Например, сортировка по Name вызовет ошибку, если DateCreated не будет предварительно преобразовано в символьный вид. Связка ORDER BY... CASE дает нам гибкость изменения порядка сортировки через передачу параметра в функцию без использования динамических запросов. Этот прием я привел в демонстрационных целях, поскольку предпочтительнее сортировать результат функции явным указанием сортировки при ее вызове. Вызов хранимых процедур из представлений В отличие от некоторых СУБД, SQL Server не позволяет выполнять SELECT-запро- сы к хранимым процедурам. Можно помечтать о временах, когда такая возможность у нас будет. При помощи хранимой процедуры гораздо удобнее выполнять
234 Глава 9. Представления любое действие с данными, и возможность читать данные из процедур, как из таблиц, сильно облегчила бы жизнь. К сожалению, нет прямого пути, чтобы сделать это, но есть обходной. Мы можем вызвать хранимую процедуру из представления. Для этого необходимо выучить последовательность следующих действий. 1. Создайте Linked Se rve r, ссылающийся сам на себя. Q В Enterprise Manager откройте Security, затем щелкните правой кнопкой мыши на Linked Servers, выберите New Linked Server. а В появившемся окне диалога щелкните на Other Data Source и выберите Microsoft OLE DB Provider for SQL Server, поскольку по умолчанию SQL Server не позволяет сконфигурировать локальный сервер как связанный. а Для ясности в поле Linked Server лучше ввести название, указывающее, что этот связанный сервер ссылается на самого себя (например, Loopback). Это убережет вас от ошибок при дальнейшем использовании удаленных серверов. Q В поле Data Source введите имя сервера или его экземпляра, или псевдоним, созданный в Client Network Utility. а Заполните вкладку Security (лично я не использую явное отображение логином для локального и удаленного серверов, а выбираю опцию Be made using the Login's security context). а Поскольку созданный сервер является не чем иным, как самим собой, можно включить все опции на вкладке Server Options. Этим мы установим максимальный уровень самосовместимости. Q Нажмите ОК. 2. Проверьте правильность выполненных действий. Для этого откройте узел Tables в только что созданном связанном сервере. Если на экране появился список таблиц, то вы все сделали правильно. За сценой Enterprise Manager запускает системную процедуру sp_tab I es_ex для получения списка таблиц на удаленном сервере. 3. Создайте представление, использующее связанный сервер, и функцию OPENQUERY() для запуска необходимой хранимой процедуры, как показано в листинге 9.8. Листинг 9.8. Использование OPENQUERY() для вызова процедур из представлений USE Northwind GO ' DROP VIEW VlewActivity ; GO CREATE VIEW ViewActivity AS SELECT * FROM OPENQUERYCLoopback.'EXEC dbo.sp_who') GO •••.■■■.. SELECT * FROM ViewActivity (Результаты сокращены) ■ ''***'-*•■'<* "*•"" -- spid ecid status loginname hostname blk 1 0 background sa .0
Ограничения 235 2 3 4 5 6 7 8 9 10 11 12 13 14 15 51 52 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ПРИМЕЧАНИЕ background sleeping background background sleeping background background background background background background background background background sleeping sleeping sa sa sa sa sa sa sa sa sa sa sa sa sa sa HEN0ERS0N\khen HENDERSON\khen 0 0 0 0 0 0 0 0 0 0 0 0 0 0 KHEN 0 KHEN 0 Вместо OPENQUERY() можно использовать OPENROWSET(). Это избавит вас от необходимости создавать связанный сервер, но придется указывать реквизиты подключения к серверу прямо в коде T-SQL. А это очень плохая практика (если не сказать порочная). Обязательное использование в коде атрибутов подключения при любом изменении в системе безопасности приведет к сбою. Например, если вы используете постоянный логин SQL Server при соединении с сервером и пароль этого логина прописан в коде, то помимо вероятности обнаружения вашего расположения кем-либо, не имеющим на это прав, — вы также создаете гарантированные проблемы в будущем. Сколько вы знаете паролей, которые никогда не меняются? Использование OPENQUERYQ избавляет нас от подобных проблем, перекладывая на могучие плечи SQL Server всю заботу о безопасности. При создании связанного сервера SQL Server сохраняет всю необходимую информацию в системных таблицах и дает нам удобный графический интерфейс для их редактирования. Одним из неприятных ограничений данного метода является невозможность передавать параметры в вызываемую процедуру. Строка запроса в QPENQUERY() должна быть строковой константой; использование переменных недопустимо. Другим недостатком этого метода является использование ядром сервера механизма распределенных транзакций. А это может приводить к мертвым блокировкам и чрезмерной трате системных ресурсов, потому что мы вынуждены использовать механизм, не приспособленный для этих целей. Другими словами, возможность запускать хранимые процедуры из представлений — это значимая особенность, но использовать ее не следует. Если вы попали в ситуацию, в которой необходимо использовать запрос к хранимой процедуре, то рекомендую применить только что описанный прием. Замечу, что этот прием можно использовать не только в представлениях. Например, поскольку OPENQUERY() возвращает результат, то на его основе можно открыть курсор (листинг 9.9). , Листинг 9.9. Использование OPENQUERY() с курсорами USE Northwind GO DECLARE c CURSOR FOR SELECT * FROM OPENQUERY(Loopback,'EXEC dbo.sp_who') ' :"" " OPEN с . rttil**' ■#■: 'Ш Я'--' продолжение &
236 Глава 9. Представления Листинг 9.9 {продолжение) FETCH с WHILE (a@FETCH_STATUS=0 BEGIN FETCH с END CLOSE с DEALLOCATE с (Результаты сокращены) spid ecid status loginame 11 spid 12 spid 51 spid 52 spid 53 spid 54 spid 0 ecid 0 ecid 0 ecid 0 ecid 0 ecid 0 ecid background status background status sleeping status sleeping status sleeping status sleeping status sa loginame sa loginame HEN0ERS0N\KHEN loginame HEN0ERSON\KHEN loginame HENDERSON\KHEN r loginame HENDERSON\KHEN loginame 55 0 sleeping HENDERSON\KHEN Обновляемые представления Как было замечено ранее, существует несколько факторов, которые определяют, является ли представление без I NSTEAD-триггеров обновляемым. При наличии I NSTEAD-триггеров мы можем контролировать процесс обновления, поэтому нижеследующие ограничения к таким случаям не относятся. Иначе представление должно выполнять следующие ограничения. ■ Агрегирующие функции, конструкции TOP, GEOUP BY, UN I ON и DI ST INCT не допустимы. ■ Вычисляемые столбцы (составленные из других выражений) не могут быть обновлены. ■ Список выражений в SELECT, состоящий только из нетабличных значений, недопустим. Замечу снова, что на нижнем уровне сервер должен преобразовать изменение строки представления в изменение записи таблицы. Если по каким-либо причинам сервер не может этого сделать, он не разрешит изменять представление.
Ограничения 237 Конструкция WITH CHECK OPTION Изменяемое представление может быть создано таким образом, что оно будет контролировать свое изменение для соответствия условиям в конструкции WHERE (если она присутствует). Можно запретить добавление записей, которые не соответствуют условиям отбора, указанным в представлении. Чтобы включить этот механизм, при создании представления достаточно добавить конструкцию WITH CHECK ОРТ I ON. Листинг 9.10. WITH CHECK OPTION может контролировать вводимые данные CREATE VIEW CALIFORNIA_AUTHORS AS SELECT * FROM authors WHERE State='CA' WITH CHECK OPTION Представление в приведенном примере гарантирует нам, что любой автор, добавленный в него, будет непременно проживать в Калифорнии. В качестве примера давайте посмотрим, как выражение из следующего листинга вызовет ошибку. Листинг 9.11. WITH CHECK OPTION не позволяет добавление записи, не удовлетворяющей условиям фильтра INSERT CALIFORNIA_AUTHORS VALUES ('867-53-09ЕГ , 'Henderson', 'Ken'. '972 555-1212'.'57 Riverside'.'Dallas'.'TX'.'75080'.1) Server: Msg 550, Level 16. State 1. Line 1 The attempted insert or update failed because the target view either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION and one or more rows resulting from the operation did not qualify under the CHECK OPTION constraint. The statement has been terminated. To же самое применимо и к операциям UPDATE. Если любое изменение представления с WITH CHECK ОРТ I ON приведет к нарушению условий WHERE, это изменение будет отклонено. Вложенные запросы Вложенные запросы — это выражения SELECT, которые вкладываются в другое выражение SELECT на месте явного указания таблицы. В этой главе следует рассказать немного о запросах, чтобы придать материалу завершенность, а также потому, что они очень напоминают неявные представления. Вложенные запросы делают возможными некоторые типы запросов, которые раньше выполнялись только с использованием представлений. Рассмотрим пример. Листинг 9.12. Вложенные запросы могут применяться там, где требуются представления CREATE TABLE #1996_P0P_ESTIMATE (Region charG). State charC2), Population int) • INSERT #1996_P0P_ESTIMATE VALUES ('West'. 'CA'.31B78234) INSERT #1996_P0PJSTIMATE VALUES ('South'. 'TX',19128261) INSERT #1996_P0P_ESTIMATE VALUES ('North'. 'NY'.18184774) INSERT #1996_P0P_ESTIMATE VALUES ('South'. 'FL'.14399985) INSERT #1996_P0P_ESTIMATE VALUES ('North'. 'NJ'. 7987933) INSERT #1996_P0PJSTIMATE VALUES ('East', 'NC, 7322870) INSERT #1996 POP ESTIMATE VALUES ('West', 'WA', 5532939) продолжение #
238 Глава 9. Представления Листинг 9.12 {продолжение) INSERT #1996_P0PJSTIMATE VALUES ('Central'.'МО', 5358692) INSERT #1996_P0P_ESTIMATE VALUES ('East'. 'MD*. 5071604) INSERT #1996_P0P_ESTIMATE VALUES ('Central'.'OK'. 3300902) SELECT * FROM (SELECT TOP 5 WITH TIES State. Region, Population=Population/1000000 FROM #1996__POP_ESTIMATE ORDER BY Population/1000000) p ORDER BY Population DESC . . . State Region Population NJ North 7 NC East 7 WA West 5 MO Central 5 MO East 5 OK Central 3 ) В этом запросе используется вложенный запрос для вычисления пяти штатов с минимальным населением. Затем, используя ORDER BY во внешнем SELECT, мы сортируем их в обратном порядке. Если бы в этом примере не было поддержки вложенных запросов, нам пришлось бы создавать представление или использовать временную таблицу. Одна тонкость, заслуживающая внимания: вложенному запросу должен быть присвоен псевдоним. Заметьте, что в листинге мы добавили псевдоним, хотя он и не нужен. Это необходимое условие при использовании вложенных запросов независимо от того, используются псевдонимы в запросе или нет. Параметризованные представления В SQL Server встраиваемые табличные функции являются ближайшим приближением параметризованных представлений. Как и представления, встраиваемые функции состоят только из одного выражения SELECT. Конечно, в них могут содержаться операции UNION и сложные соединения таблиц, но, по сути своей, это все ' равно будет выражение SELECT. Использование встраиваемых функций дает нам возможность передавать параметры в SELECT. Иными словами, вместо того чтобы каждый раз добавлять условие в выборку из представления, мы можем использовать функцию, принимающую параметры. Этот путь синтаксически более выразителен и предполагает меньшее количество потенциальных ошибок. Кроме того, он дает нам возможность контролировать план выполнения запроса. Часто, при включении большого количества условий в выборку из представления, мы должны усердно молиться для того, чтобы оптимизатор выбрал оптимальный план выполнения запроса. При использовании функций мы можем быть уверены, что условия будут применены в требуемом месте. Мы можем контролировать, как и где это произойдет. Давайте рассмотрим пример встраиваемой табличной функции. Листинг 9.13. Функция может выступать в качестве параметризованного представления CREATE FUNCTION dbo.ContactCustomersv (@CompanyName nvarchar(80), @ContactName nvarcharF0)) RETURNS TABLE AS , .-■„.■
Ограничения 239 RETURNCSELECT * FROM dbo. Customers , ■ WHERE CASE WHEN @CompanyName IS NULL AND @ContactName IS NOT NULL THEN ContactName WHEN @ContactName IS NULL AND @CompanyName IS NOT NULL THEN CompanyName ELSE •%' END LIKE COALESCE(^CompanyName, @ContactName. X) ) GO SELECT * FROM dbo.ContactCustomersv(DEFAULT.'А1еГ) (Результаты сокращены) CustomerlO CompanyName ContactName ContactTitle MORGK Morgenstern Gesundkost Alexander Feuer Marketing Assistant ROMEY Romero у tomillo Alejandra Camino Accounting Manager Поскольку мы передаем параметры в функцию, можно соединить два запроса в один. Функция выполняет фильтрацию по различным столбцам в зависимости от того, какой из параметров был задан. Несмотря на то что такой тип условий не приводит к оптимальному плану, этот пример демонстрирует нам возможности функций, которые мы никогда бы не реализовали при помощи параметризованных представлений. Динамические представления Когда вы обращаетесь к представлению, план выполнения запроса конструируется объединением первоначальных условий, описанных в SELECT при создании представления, и условий, налагаемых на это выражение в самом запросе. Обычно условия отбора внутри и снаружи представления суммируются и передаются оптимизатору для дальнейшей обработки. Большинство представлений используют статические (или детерминированные) условия для фильтрации данных. Это означает, что логика фильтрации не меняется независимо от того, сколько раз было выполнено обращение к представлению. Динамическая часть сложных запросов обычно размещается в условиях внешних относительно представления. Исключая те случаи, когда внутри представления выполняется соединение с другими представлениями или таблицами, критерии внутренней фильтрации остаются неизменными. Преимущественно мы используем именно такие представления, хотя иногда требуется изготовить представление сдиншлическими {недетерминированными) условиями. Иными словами, представление, которое зависит от внешних по отношению к нему факторов. В динамических представлениях внутренние условия отбора могут меняться в зависимости от вычисления выражений, указанных в WHERE или HAVING. В отличие от статических динамические условия недетерминированы: они возвращают различные результаты от выполнения к выполнению в зависимости от различных условий уровня системы или сессии. Хорошим примером подобных представлений является возвращение результата в зависимости от нетабличных выражений. В листинге 9.14 приведено представление, возвращающее сегодняшние продажи наосновании функции GetDateQ..^ ^машт/йН .-.:■;. .. г.;
240 Глава 9. Представления Листинг 9.14. Динамические представления позволяют использовать недетерминизм в своих интересах CREATE VIEW DAILY_SALES AS SELECT * ' FROM sales ' WHERE ord_date BETWEEN CONVERT(char(B) .GETDATEO .112) AND C0NVERT(char(8).GETDATEO. 112) + ' 23:59:59.999' GO , Добавим несколько записей в таблицу sa I es, чтобы увидеть результат. INSERT sales VALUES ('B042','QA879. Г. GETOATEO. 30. 'Net 30'. 'BU1032') INSERT sales VALUES ('6380','D4482'.GETDATE(),ll.'Net 60','PS2091') INSERT sales VALUES ('6380'.'04492'.GETOATEO+1,53,'Net 30'.'PS2091') SELECT * FROM DAILY_SALES stor id ord 6380 D4482 2004-04-25 21:45:59.077 11 Net 60 PS2091 8042 QA879.1 2004-04-25 21:45:59.060 30 Net 30 BU1032 Это представление использует функцию GetDate() для отбора только тех записей, у которых ord_date попадает на сегодняшний день. Из-за того, что в запросе использована текущая дата, критерий выборки, действительно обрабатываемый сервером, будет варьироваться. Сегодня условия в WHERE будут вычислены как сегодняшняя дата, и выведутся первые две из вставленных записей. Завтра они преобразуются к завтрашней дате, и выведется третья запись. Это одно из свойств динамических представлений: критерий их обработки сервером меняется от запуска к запуску. В следующем примере с использованием CASE представление становится еще динамичнее. Этот код является расширением предыдущего примера с дополнительной обработкой данных по выходным. Поскольку в выходные дни продаж не происходит, то при вызове этой функции в выходной день нам возвращаются данные по продажам за ближайший рабочий день (листинг 9.15). Листинг 9.15. Динамическое представление, использующее CASE CREATE VIEW DAILY_SALES AS SELECT * FROM sales ■ '- ' WHERE ordjate BETWEEN (CASE 0ATEPART(DW.C0NVERT(char(8),GETDATEO. 112)) • : WHEN 1 THEN C0NVERT(char(8),GET0ATE()+1.112) WHEN 7 THEN C0NVERT(char(8),GETDATE()-1.112) ELSE C0NVERT(char(8),GETDATE(),112) END) AND (CASE DATEPART(DW,C0NVERT(char(8).GETOATEO. 112)) WHEN 1 THEN C0NVERT(char(8).GETDATEO+1.112) WHEN 7 THEN C0NVERT(char(8) .GETOATEO-1.112) ELSE C0NVERT(char(8),GETOATEO. 112) END+' 23:59:59.999') Можно использовать и другие функции для создания подобных скользящих или динамических представлений. Например, SUSER_SNAME() можно использовать
Ограничения 241 для разделения выборки по пользователям; H0ST_NAME() — для фильтра по имени компьютера пользователя. В любом случае, само выражение SELECT внутри представления не меняется (в предыдущем примере у нас был всегда одинаковый SELECT). За изменчивость результата представления отвечают только внешние критерии. Секционированные представления Простыми словами, секционированные представления — это представления, объединяющие данные из нескольких таблиц для упрощения обслуживания большого объема данных. Например, секционированное представление можно сделать для логов веб-сервера, расположенных в разных таблицах по месяцам. Объединяя эти таблицы в одну, можно облегчить доступ к ним, контролируя при этом их размер. Существует два типа секционированных представлений: локальные (LPV) и распределенные (DPV). В LVP все используемые таблицы лежат внутри одного экземпляра SQL Server. В DVP используемые таблицы разнесены по нескольким экземплярам и чаще всего по разным серверам (хотя это и необязательно). Размещение DPV на нескольких серверах помогает масштабировать объемные решения, эффективно распределив мощность процессоров и системные ресурсы нескольких машин при выполнении одного запроса. Секционированное представление — это обычное представление, в котором оператор UN I ON объединяет несколько таблиц с определенными атрибутами. Оно ссылается на несколько таблиц одинаковой структуры, чтобы обеспечить унифицированное представление набора данных, а секционирование происходит посредством явно определенного разделяющего столбца, определяемого с помощью CHECK CONSTRAI NT. В дополнение к своим обычным обязанностям — контролировать вводимые данные, — CHECK CONSTRA I NT должен определять поведение оптимизатора при обращении к секционированным представлениям (помогая определить, в каких таблицах находятся запрашиваемые данные). Это позволяет избежать лишнего сканирования таблиц, в которых заведомо недопустимы значения, заданные в запросе. Для лучшего понимания рассмотрим пример. Листинг 9.16. Простое распределенное представление CREATE TABLE CustomersUS ( CustomerTD nchar E) NOT NULL, CompanyName nvarchar D0) NOT NULL , ' . ContactName nvarchar C0) NULL , ContactTitle nvarchar C0) NULL . Address nvarchar F0) NULL , City nvarchar A5) NULL . Region nvarchar A5) NULL . PostalCode nvarchar A0) NULL . ' ' Country nvarchar A5) NOT NULL CHECK (Country='US'), Phone nvarchar B4) NULL , Fax nvarchar B4) NULL. CONSTRAINT PKJIustUS PRIMARY KEY (Country. CustomerlD) ) CREATE TABLE CustomersUK ( CustomerlD nchar E) NOT NULL, CompanyName nvarchar D0) NOT NULL , продолжение &
242 Глава 9. Представления Листинг 9.16 {продолжение) ContactName nvarchar C0) NULL , ContactTitle nvarchar C0) NULL , Address nvarchar F0) NULL , City nvarchar A5) NULL , Region nvarchar A5) MULL , PostalCode nvarchar A0) NULL , Country nvarchar A5) NOT NULL CHECK (Country='UK'), Phone nvarchar B4) NULL , - - Fax nvarchar B4) NULL, CONSTRAINT PK_CustUK PRIMARY KEY (Country, CustomerlD) ) CREATE TABLE CustomersFrance ( CustomerlD nchar E) NOT NULL, " ^ CompanyName nvarchar D0) NOT NULL , ContactName nvarchar C0) NULL , ContactTitle nvarchar C0) NULL , Address nvarchar F0) NULL , City nvarchar A5) NULL , Region nvarchar A5) NULL , PostalCode nvarchar A0) NULL , Country nvarchar A5) NOT NULL CHECK (Country»'France'), Phone nvarchar B4) NULL , Fax nvarchar B4) NULL, CONSTRAINT PK_CustFR PRIMARY KEY (Country. CustomerlD) ) ■ - GO DROP VIEW CustomersV "*■ ' ' • GO CREATE VIEW CustomersV AS SELECT * FROM dbo.CustomersUS UNION ALL SELECT * FROM dbo.CustomersUK - ' ' : UNION ALL SELECT * FROM dbo.CustomersFrance GO Как вы видите, мы создаем три таблицы для хранения разделов таблицы покупателей. Затем мы объединяем эти таблицы с помощью представления. С какой целью? Почему бы не хранить данные в одной таблице? У этого подхода два преимущества. Во-первых, разделяя данные о покупателях, мы делаем наши таблицы более управляемыми, поскольку можем разнести объем данных по нескольким разделам. Во-вторых, планировщик запроса SQL Server умеет распознавать секционированные представления и автоматически определяет таблицу, содержащую требуемые данные. Он делает это, исходя из заданного критерия поиска и СНЕСК- огранйчения для столбца, на котором основано секционирование. В качестве примера посмотрим план выполнения такого запроса: SELECT CompanyName FRDM dbo.CustomersV WHERE Country='US' (Результаты сокращены) StmtText
Ограничения 243 SELECT CompanyName-CompanyName FROM dbo.CustomersV WHERE Country=@l |--Compute Scalar(DEFINE:(CustomersUS.CompanyName=CustomersUS.CompanyName)) |--Clustered Index Scan(OBJECT:(Northwind.dbo.CustomersUS.PK_Cust)) Несмотря на то что в запросе мы обращаемся к представлению, оптимизатор понимает, что интересующие нас данные находятся только в одной таблице, и производит выборку только из нее, не затрагивая остальные таблицы. Для принятия решения оптимизатор использует тот факт, что в условии фильтрации стоит фильтр на разделяющий столбец и он является частью первичного ключа таблицы. Существует несколько ограничений, которым должны соответствовать таблицы и представление для того, чтобы оптимизатор мог самостоятельно разрешать спорные ситуации. Количество и значимость этих ограничений могут заставить пользователей отказаться от использования распределенных представлений (особенно локальных). Использовать их или нет — решать вам. Подробности ограничения описаны в Books Online. Что касается возможности оптимизатора использовать разделяющий столбец для определения таблиц, необходимых для запроса, — то, по моему скромному опыту, этот столбец должен всегда быть указан в начале первичного ключа, хотя это и требует проверки. Посмотрим на такое распределенное представление (листинг 9.17). Листинг 9.17. Отсутствие разделяющего столбца в первичном ключе CREATE TABLE Orders 1996 ( OrderlD int PRIMARY KEY NOT NULL , . CustomerlD nchar E) NULL . ' EmployeelD int NULL . OrderDate datetime NOT NULL CHECK (Year@rderDate)=1996). OrderYear int NOT NULL CHECK @rderYear=1996). RequiredDate datetime NULL, , ~ " ' : ShippedDate datetime NULL , - ' '' ; •-'' ShipVia int NULL .>,,.'• ; :.- • ■ ■ < • ■■■ , • CREATE TABLE 0rdersl997 ( ' : ' OrderlD int PRIMARY KEY NOT NULL . CustomerlD nchar E) NULL . EmployeelD int NULL , OrderDate datetime NOT NULL CHECK (Year@rderDate)-1997). OrderYear int NOT NULL CHECK @rderYear=1997), RequiredDate datetime NULL , ShippedDate datetime NULL , ShipVia int NULL CREATE TABLE Orders 1998 ( OrderlD int PRIMARY KEY NOT NULL . CustomerlD nchar E) NULL . EmployeelD int NULL . OrderDate datetime NOT NULL CHECK (Year@rderDate)=199B), "' "" - OrderYear int NOT NULL CHECK @rderYear=1998). RequiredDate datetime NULL , ShippedDate datetime NULL , -.■■•;'- ShipVia int NULL - - • > ) продолжение $■
244 Глава 9. Представления Листинг 9.17 {продолжение) GO DROP VIEW OrdersV - .. . - GO CREATE VIEW OrdersV "• ; AS •■■■■.• SELECT * FROM 0rdersl996 UNION ALL ' ■ , SELECT * FROM 0rdersl997 UNION ALL SELECT * FROM 0rdersl998 ■ - ■• GO ,- . ,. : - Сможет ли оптимизатор определить, какая таблица нас Интересует? Давайте посмотрим на план выполнения запроса. (Результаты сокращены) ! ' : StmtText • SELECT * FROM [OrdersV] WHERE [OrderYear]=@l |--Concatenation |--Filter(WHERE:(STARTUP EXPR(Convert([@l])=1996))) I |--Clustered Index Scan(OBJECT: ([master]. [dbo].[0rdersl996].[PK_0rd |--Fi 1 ter(WHERE:(STARTUP EXPR(Convert([01])=1997))) J |--Clustered Index Scan(OBJECT:( [master]. [dbo].[0rdersl997].[PK_0rd |--Fi 1 ter(WHERE:(STARTUP EXPR(Convert([@1])=1998))) |--Clustered Index Scan(OBJECT: ([master].[dbo].[0rdersl998].[PK_0rd Несмотря на то что мы запрашиваем данные только из одной таблицы, в плане выполнения запроса мы видим обращение ко всем трем. Почему? Потому что разделяющий столбец не входит в первичный ключ. Давайте добавим разделяющий столбец в первичный ключ и посмотрим, что получится (листинг 9.18). Листинг 9.18. Оптимизатор по-прежнему предпочитает выбирать неправильный план CREATE TABLE 0rdersl996 ( OrderlO int NOT NULL , CustomerlO nchar E) NULL . EmployeelO int NULL . OrderDate datetime NOT NULL CHECK (Year@rderDate)=1996), OrderYear int NOT NULL DEFAULT 1996 CHECK @rderYear=1996), RequiredDate datetime NULL , ShippedDate datetime NULL . ShipVia int NULL. CONSTRAINT PK_0rdersl996 PRIMARY KEY (OrderYear, OrderlD) ) GO CREATE TABLE 0rdersl997 ( OrderlD int NOT NULL . ' - CustomerlD nchar E) NULL . EmployeelD int NULL . OrderDate datetime NOT NULL CHECK (Year@rderDate)=1997). OrderYear int NOT NULL DEFAULT 1997 CHECK @rderYear=1997), RequiredDate datetime NULL .
Ограничения 245 ShippedDate datetime NULL . ShipVia int NULL, CONSTRAINT PK_0rdersl997 PRIMARY KEY (OrderYear, OrderlD) ) GO CREATE TABLE 0rdersl998 ( OrderlD int NOT NULL , CustomerlD nchar E) NULL , EmployeelD int NULL . OrderDate datetime NOT NULL CHECK (Year@rderDate)=1998). OrderYear int NOT NULL DEFAULT 1998 CHECK @rderYear=1998), RequiredDate datetime NULL , ShippedDate datetime NULL . ShipVia int NULL. CONSTRAINT PK_0rdersl998 PRIMARY KEY (OrderYear. OrderlD) ) GO CREATE VIEW OrdersV AS ' SELECT * FROM 0rdersl996 UNION ALL SELECT * FROM Orders 1997 UNION ALL SELECT * FROM 0rdersl99B GO SELECT * FROM OrdersV WHERE 0rderYear=1997 (Результаты сокращены) SELECT * FROM [OrdersV] WHERE [OrderYear]=@l |--Concatenation |--Fi lter(WHERE:(STARTUP EXPR(Convert([@1])=1996))) | |--Clustered Index Scan(OBJECT: ([master] .[dbo].[0rdersl996].[PK_0rd |--Filter(WHERE:(STARTUP EXPR(Convert([@l])=1997))) 1 (--Clustered Index Scan(OBJECT:([master].[dbo].[0rdersl997].[PK_0rd |-Filter(WHERE:(STARTUP EXPR(Convert([@l])=1998))) |--Clustered Index Scan(OBJECT:([master].[dbo].[0rdersl998].[PK_Ord И снова в план выполнения включен поиск по всем трем таблицам. Почему? Кажется, у нас все удовлетворяет ограничениям на распределенные представления? Причина такого странного поведения оптимизатора (вопреки всякой логике) заключается в том, что в условиях WHERE следует указать все столбцы из первичного ключа. В этом мы можем убедиться, посмотрев план следующего запроса. Листинг 9.19. Правильный план получается только при указании всех столбцов из ПК SELECT * FROM OrdersV WHERE 0rderYear=1997 AND OrderID=1000 (Результаты сокращены) '' - StmtText продолжение $■
246 Глава 9. Представления Листинг 9.19 {продолжение) SELECT * FROM [OrdersV] WHERE [OrderYear]=@l AND [0rderID]=@2 |--Fi 1 ter(WHERE:([0rdersl997].[0rderID]=1000)) |--Clustered Index Scan@BJECT:([Northwind].[dbo].[0rdersl997].[PK_0rders Теперь мы получаем эффективный план запроса. Мало того, что разделяющий столбец включен в первичный ключ, но и все столбцы из первичного ключа включены в условие WHERE. Недостаточно, если столбцы из условия составляют основную часть первичного ключа, — необходимо полное перечисление всех полей ключа в конструкции WHERE, иначе оптимизатор производит неэффективный план. Следующий листинг демонстрирует нам эту причуду оптимизатора. Листинг 9.20. Добавление столбца в первичный ключ возвращает проблему CREATE TABLE Orders 1996 ( OrderlD int NOT NULL . CustomerlD nchar E) NOT NULL . EmployeelD int NULL , I OrderDate datetime NOT NULL CHECK (Year@rderDate)=1996). OrderYear int NOT NULL DEFAULT 1996 CHECK @rderYear=1996). RequiredDate datetime NULL . ShippedDate datetime NULL , ShipVia int NULL. CONSTRAINT PK_0rdersl996 PRIMARY KEY (OrderYear, OrderlD. Customerld) ) GO CREATE TABLE 0rdersl997 ( о OrderlD int NOT NULL . CustomerlD nchar E) NOT NULL . EmployeelD int NULL , OrderDate datetime NOT NULL CHECK (Year@rderDate)=1997), OrderYear int NOT NULL DEFAULT 1997 CHECK @rderYear=1997), RequiredDate datetime NULL , ShippedDate datetime NULL . ShipVia int NULL, CONSTRAINT PK_0rdersl997 PRIMARY KEY (OrderYear, OrderlD, Customerld) ) GO CREATE TABLE Orders 1998 ( OrderlD int NOT NULL , CustomerlD nchar E) NOT NULL . EmployeelD int NULL , OrderDate datetime NOT NULL CHECK (Year@rderDate)=1998), OrderYear int NOT NULL DEFAULT 1998 CHECK @rderYear=1998). •■ • RequiredDate datetime NULL . ShippedDate datetime NULL . ■# ShipVia int NULL, ; CONSTRAINT PK_0rdersl998 PRIMARY KEY (OrderYear. OrderlD. Customerld)
Ограничения 247 CREATE VIEW OrdersV AS SELECT * FROM Orders 1996 UNION ALL SELECT * FROM Ordersl997 UNION ALL SELECT * FROM Orders 1998 GO SELECT * FROM OrdersV WHERE 0rderYear=1997 AND OrderID=10OO (Результаты сокращены) SELECT * FROM [OrdersV] WHERE [OrderYear]=@l AND [0rderID]=@2 (--Concatenation |--Filter(WHERE:(STARTUP EXPR(Convert([@l])=1996))) I |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[0rdersl996].[PK_0rder |--Filter(WHERE:(STARTUP EXPR(Convert([@l])=1997))) | |—Clustered Index Seek@BJECT:([Northwind].[dbcj].[Drdersl997].[PK_0rder |-Fi 1ter(WHERE:(STARTUP EXPR(Convert([01])=1998)j) (--Clustered Index Seek(OBJECT:([Northwind].[dbo].[0rdersl998].[PKJDrder Мы всего лишь добавили столбец CustomerlD в первичный ключ каждой таблицы и снова получили неэффективный план. Посмотрим, что получится, если добавить этот столбец в условие WHERE. Листинг 9.21. Добавляя столбец CustomerlD, мы получаем оптимальный план SELECT * FROM OrdersV WHERE OrderYear-1997 AND OrderID=1000 AND CustomerlD = 'AAAAA' SET STATISTICS PROFILE OFF StmtText SELECT * FROM [OrdersV] WHERE [OrderYear]=@l AND [0rderID]=@2 AND [Customed l-Filter(WHERE:([Ordersl997].[OrderID]=1000 AND [0rdersl997].[CustomerID]= (--Clustered Index Scan(OBJECT:([Northwind].[dbo].[0rdersl997].[PKjDrdersl Повторю еще раз: мы получили оптимальный план только потому, что один к одному перечислили в условии WHERE все столбцы из первичного ключа. В нашем случае мы получили оптимальный план, добавив всего лишь один столбец к условию. Также мы могли исключить один столбец из первичного ключа. Как показано в самом первом примере, необязательно перечислять все столбцы из ключа в условии отбора, но будьте готовы к тому, что для получения эффективного плана выполнения запроса вам все же придется это делать. BETWEEN в секционированных представлениях В дополнение к отношениям разделяющего столбца с первичным ключом необходимо рассмотреть использование операторов нестрогого сравнения для секционированных представлений. Даже если операторы в CHECK CONSTRAINT и в запросе одинаковы, оптимизатор все равно не способен корректно распознать нужную таблицу при использовании операторов нестрогого сравнения. Это означает, что будет выполнено сканирование всех таблиц и объединение результатов. Посмотрим на пример.
248 Глава 9. Представлена •Листинг 9.22. Проблема в секционированных представлениях CREATE TABLE CustomersUKUS ( Customer-ID nchar E) NOT NULL. CompanyName nvarchar D0) NOT NULL . ContactName nvarchar C0) NULL , ContactTitle nvarchar C0) NULL , Address nvarchar F0) NULL , City nvarchar A5) NULL , Region nvarchar A5) NULL . PostalCode nvarchar A0) NULL . Country nvarchar A5) NOT NULL PRIMARY KEY CHECK (Country BETWEEN 'UK' and 'US'). Phone nvarchar B4) NULL , Fax nvarchar B4) NULL, CREATE TABLE CustomersFrance ( CustomerlD nchar E) NOT NULL, CompanyName nvarchar D0) NOT NULL , ContactName nvarchar C0) NULL , ContactTitle nvarchar C0) NULL . Address nvarchar F0) NULL , City nvarchar A5) NULL , Region nvarchar A5) NULL , PostalCode nvarchar A0) NULL , Country nvarchar A5) NOT NULL PRIMARY KEY CHECK (Country='France'). Phone nvarchar B4) NULL , Fax nvarchar B4) NULL. DROP VIEW CustomersV GO CREATE VIEW CustomersV AS SELECT * FROM dbo.CustomersUKUS UNION ALL SELECT * FROM dbo.CustomersFrance SELECT * FROM dbo.CustomersV WHERE Country BETWEEN 'UK' AND 'US' Несмотря на то что разделяющий столбец присутствует в первичном ключе каждой таблицы и фильтрующее условие совпадает с условием в CHECK CONSTRAINT, план выполнения будет следующим. (Результаты сокращены) StmtText SELECT * FROM [dbo].[CustomersV] WHERE [Country]>=@l AND [Country]<=@2 |--Concatenation |--Filter(WHERE: (STARTUP EXPR(Convert( [(?!])<='US' AND Convert([02])>='US') | |--Clustered Index Seek@BJECT: ([Northwind] .[dbo]. [CustornersUS]. [PK_Cus |--Filter(WHERE:(STARTUP EXPR(Convert([@l])<='UK' AND Convert([@2])>='UK')
Ограничения 249 | | --Clustered Index SeekCOBJECT: ([Northwind]. [dbo]. [CustomersUK]. [PK Cus ■-■ - ■■-•■■ |--F11ter(WHERE:(STARTUP EXPR(Convert([@l])<='France' AND Convert([@2])>=' |--C1ustered Index Seek(OBJECT:([Northwind].[dbo].[CustomersFrance].[PK_ Таблица CustomerFrance сканируется несмотря на то, что, исходя из условий поиска, в ней не может быть интересующих нас данных. Эта неувязка относится не к диапазону запрашиваемых данных — это проблема оператора BETWEEN и оптимизатора. Несмотря на то что оптимизатор способен определить с помощью CHECK CONSTRAINT, что запрашиваемые значения в таблицах не пересекаются, он этого не делает. Он сканирует таблицу CustomerFrance для поиска значений, которых там не может быть из-за имеющихся ограничений. Следующий листинг показывает нам, что проблема действительно в операторе BETWEEN и в способе обработки его оптимизатором. Листинг 9.23. Даже если нам требуется единственное значение, оператор BETWEEN не позволит его найти SELECT * FROM dbo.CustomersV WHERE Country BETWEEN 'UK' AND 'UK' (Результаты сокращены) . , ■.. ■ : . < StmtText SELECT * FROM [dbo].[CustomersV] WHERE [Country]>=@l AND [Country]<=@2 |--Concatenation |--Filter(WHERE:(STARTUP EXPR(Convert([@l])<='US' AND Convert([02])>='US') | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[CustomersUS].[PK_ |--Filter(WHERE: (STARTUP EXPR(Convert([@l])<='UK' AND Convert([02])>='UK') | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[CustomersUK].[PK_ |--Filter(WHERE:(STARTUP EXPR(Convert([@l])<='France' AND Convert([@2])>=' |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[CustomersFrance].[ Здесь мы изменили запрос, подставив в оба операнда BETWEEN одинаковое значение. Тем не менее план выполнения запроса остался неизменным. Почему? Причина очевидна. Посмотрите на первую строчку запроса. Оптимизатор не в состоянии найти оптимальный план, хотя даже простая логика подсказывает эту возможность. Проблема в операторе BETWEEN. В процессе оптимизации конструкция Column BETWEEN @1 AND @2 преобразуется в составное выражение Column >= @1 AND Column <= @2. Затем оптимизатор ищет данные отдельно в каждой таблице и объединяет результаты. В случаях применения операторов неравенства и LIKE оптимизатор способен обработать их правильно, но для распределенных представлений эта простая логика в него не заложена. Помните это при написании запросов к распределенным представлениям. Если вы хотите избежать сканирования всех таблиц, не используйте операторы неравенства. Обратите внимание, что проблема не в распознавании оптимизаторов какого- либо из двух аргументов поиска (SARG, search argument), В обеих частях составного плана выполнения для нахождения данных используется поиск по индексу. Проблема заключается в синхронизации и связана с тем, в какой именно момент времени происходит преобразование условий запроса (non-SARG) к аргументам поиска (SARG). Очевидно, что такие преобразования происходят на довольно ранних этапах процесса оптимизации. В противном случае это бы негативно сказалось на процессе извлечения данных из секционированного представления. Вот запрос с использованием строгого равенства (листинг 9.24).
250 Глава 9. Представления Листинг 9.24. Переключение на строгое равенство решает проблему SELECT * FROM dbo.CustomersV WHERE Country='UK' (Результаты сокращены) , , StmtText SELECT * FROM [dbo].[CustomersV] WHERE [Country]=@l | - -CI ustered Index Scan (OBJECT: ([Northwind]. [dbo]. [CustomersUS]. [PK_Custom Несмотря на то что мы продолжаем использовать оператор BETWEEN в CHECK CONSTRAI NT для таблицы CustomersUKUS, поиск по строгому равенству приводит к оптимальному плану выполнения. Распределенные секционированные : > представления Распределенные секционированные представления — это представления, в которых составляющие их таблицы находятся на нескольких автономных серверах и доступ к ним производится через механизм связанных серверов. Для создания распределенного представления выполните следующие действия. 1. Создайте связанный сервер для каждого сервера, где располагаются необходимые таблицы. 2. Включите опцию lazy schema validation для каждого связанного сервера. Эта опция недоступна из Enterprise Manager, для ее установки используйте sp_serveropt ion. 3. Создайте распределенное представление, которое ссылается на удаленный сервер, используя стандартный формат именования объектов из четырех частей. 4. Повторите этот шаг, создав одинаковые представления для каждого из серверов, участвующих в объединении данных. Это поможет вам сбалансировать нагрузку, направляя пользователей на разные версии одного и того же представления. Теперь наше секционированное представление стало еще и распределенным (листинг 9.25). Листинг 9.25. Простейшее распределенное представление CREATE VIEW OrdersV AS ,. SELECT * FROM 0rdersl996 UNION ALL SELECT * FROM HOMER.Northwind.dbo.0rdersl997 UNION ALL SELECT * FROM MARGE.Northwind.dbo.0rdersl998 - ■• GO SELECT CustomerlD FROM OrdersV WHERE 0rderYear=1997 AND OrderID=1000 StmtText SELECT CustomerID=CustomerID FROM OrdersV WHERE OrderYear=@l AND 0rderID=@2 |-Compute Sealar(DEFINE:(HOMER.Northwind.dbo.0rdersl997.CustomerID=H0MER.no |-Remote Query(SOURCE:(HOMER),QUERY:(SELECT Col 1024 FROM (SELECT ТЫ 1003. "OrderlD" Col 1023.ТЫ 1003."CustomerlD" Col 1024,ТЫ 1003."OrderYear" Col 1027 FROM "northwind"."dbo".rdersl997" ТЫ 1003) Qryl031 WHERE Col 102X1000)))
Индексированные представления 251 Как вы видите, при совпадении критерия поиска с первичным ключом всех таблиц представления оптимизатор правильно определяет, в какой именно таблице необходимо производить поиск. Поскольку эта таблица находится на другом сервере, оптимизатор добавляет шаг Remote Query к плану выполнения запроса и посылает запрос на обработку удаленному серверу. Обратите внимание, что в отправленном запросе в условии WHERE не содержится фильтр по разделяющему столбцу, хотя он есть в первоначальном запросе. Как только определена правильная таблица для поиска, разделяющий столбец больше не требуется для поиска данных в удаленной таблице. На основании проверки CHECK CONSTRA I NT оптимизатор уже знает, что в таблице 0rders1997 содержатся данные только 1997 года. Индексированные представления Как правило, представления являются как бы виртуальными таблицами и не хранят никаких данных. Когда вы делаете запрос к представлению, SQL Server за сценой выполняет выражение SELECT из определения представления и возвращает вам результаты. Именно так работают представления в SQL Server. Но с индексированными представлениями все по-другому. Путем индексирования мы материализуем возвращаемые представлением результаты для того, чтобы последующие запросы к нему выполнялись как можно быстрее. Если в таблице находится значительное количество записей, а представление возвращает только малую их часть, при помощи индексирования такого представления мы можем резко увеличить производительность. Как и для распределенных представлений, для индексирования представлений существует много ограничений. Про эти ограничения можно прочитать в Books Online. Для определения возможности индексирования какого-либо представления можно запустить функцию OBJECTPROPERTY() с параметром I s I ndexab I e, как было описано выше. Эта функция может выполняться некоторое количество времени, поскольку она проверяет все возможные ограничения, которым должно соответствовать представление. Итак, в этом разделе мы обсудили общие правила разработки индексированных представлений. Использование индексированных представлений оптимизатором То, что оптимизатор может использовать индексы для представления при запросе к самому представлению, очевидно. Как в случае индексов для таблиц, в первую очередь оптимизатор стремится использовать индекс для поиска данных. Однако оптимизатор может использовать индексированные представления, даже если мы не просим его об этом при обычном запросе к составляющим его таблицам. Рассмотрим следующее представление. Листинг 9.26. Простое индексированное представление USE pubs GO DROP VIEW dbo.SalesByMonth GO <■■'■ m*r ^ ■• « продолжение &
252 Глава 9. Представления Листинг 9.26 {продолжение) GO CREATE VIEW dbo.SalesByMonth WITH SCHEMABINDING - AS SELECT LEFT(C0NVERT(char(8),ord_date,112),6) AS SalesMonth. C0UNT_BIG(*) AS TotalNumSales FROM dbo.sales GROUP BY LEFT(C0NVERT(char(8).ord_date,112),6) GO CREATE UNIQUE CLUSTERED INDEX MonthlySales ON dbo.SalesByMonth (SalesMonth) GO В этом представлении продажи группируются по году и месяцу. Посмотрим, какую пользу можно извлечь из индекса. Запустим запрос, очень похожий на SELECT из представления (листинг 9.27). Листинг 9.27. SQL Server использует индексы, даже если запрос сделан не к представлению SELECT LEFT(C0NVERT(char(8).ord_date,112),6) AS SalesMonth. C0UNT(*) AS TotalNumSales FROM dbo.sales GROUP BY LEFT(C0NVERT(char(8).ord_date,112),6) В SQL Server Enterprise Edition план выполнения выглядит так: |--Compute Sealar(DEFINE:([Exprl003]=Convert([SalesByMonth].[TotalNumSales]) |--CIustered Index Scan(OBJECT:([pubs].[dbo].[SalesByMonth].[MonthlySales] Как вы видите, индекс для представления используется при запросе к базовой таблице. Индексированные представления в других редакциях SQL Server Вообще говоря, нельзя использовать индексированные представления во всехре- дакциях SQL Server, кроме Enterprise Edition и Developer Edition. Но есть способ обойти это положение. Для создания индексированного представления в Personal, Standard или MSDE следует предварительно создать такое представление на SQL Server Enterprise или Developer Edition, сделать резервную копию базы на ЕЕ и восстановить его на сервере младшей редакции. Это перенесет объекты на новый сервер, но оптимизатор не будет их использовать, пока в запросах не будет явно содержаться подсказка N0EXPAND. Эта подсказка в сочетании с индексированным представлением указывает оптимизатору на необходимость использовать имеющиеся индексы. Этот способ работает на всех редакциях SQL Server. Совместно с подсказкой INDEX можно заставить оптимизатор использовать индексы на представлениях. Посмотрите на пример использования подсказки N0EXPAND. Листинг 9.28. Можно заставить оптимизатор использовать индексы - при помощи подсказки NOEXPAND SELECT SalesMonth, TotalNumSales FROM dbo.SalesByMonth (NOEXPAND)
Проектирование модульных индексированных представлений 253 Проектирование модульных индексированных представлений В связи с тем что существует большое количество ограничений на использование индексированных представлений (недопустимы UNION, OUTER JOIN, любые агрегаты, кроме SUM() и C0UNT_BIG() и т. п.), у нас будет много сложных запросов, которые нельзя поместить целиком в одно индексированное представление. Однако, если в таких запросах много простых составляющих, таких как: соединения и группировки, — можно успешно использовать индексированные представления для обособленной предварительной обработки. Индексированные представления хороши для простых запросов, которые из-за большого объема обрабатываемых данных выполняются долго. Путем материализации возвращаемых данных, используемых в запросах, мы даем оптимизатору возможность использовать индексы представлений, чтобы избежать выполнения долгих по времени запросов, — а это значительно улучшает производительность. Индексированные представления хорошо работают, если содержащиеся в них данные изменяются не очень часто. Дополнительные затраты на обновление индексированных представлений при частом изменении данных могут свести на нет все их преимущества. Поэтому я не рекомендовал бы массово применять индексированные представления в OLTP-системах, в то время как в хранилищах данных они могут оказаться очень полезными. Индексированные представления дают возможность SQL Server создавать и автоматически обслуживать данные, вводимые пользователями в течение определенного количества лет. Я имею в виду статические итоговые и сводные таблицы. Если у вас большой объем данных, то обычно выполняется суммирование результатов в промежуточных статических таблицах, и эти таблицы используются везде, где это возможно, для оптимизации большого количества запросов к данным. Хотя это обычная практика, но и она требует заботы и внимания со стороны как DBA, так и разработчиков. DBA должны создавать и поддерживать эти таблицы — разработчики должны приспосабливать свои приложения для их использования. Индексированные представления могут быть созданы и забыты, и DBA не должны беспокоиться об актуальности данных в них — SQL Server позаботится об этом. В соответствующих редакциях SQL Server представления используются автоматически, если оптимизатор решит, что их использование уменьшит общее время выполнения запроса и от разработчиков не требуются особые умения для использования всех преимуществ этого метода. Индексированные представления могут использоваться не во многих ситуациях (кроме статических итоговых и сводных таблиц), однако эти две стратегии молено использовать совместно. Если вы в настоящее время обслуживаете сводные таблицы, у вас есть возможность проверить дизайн базы данных для того, чтобы понять, можно ли извлечь какую-нибудь выгоду из использования индексированных представлений. Обслуживание индексированных представлений Поскольку кластерный индекс хранит данные, возвращенные представлением, в конечных узлах, он может быть поврежден так же, как и таблица. Для проверки исправности индексов на представлениях используется команда DBCC CHECKTABLE. Как
254 Глава 9. Представления в таблицах, команда DBCC CHECKTABLE проверяет индексы на наличие ошибок и может исправлять их. Команда DBCC CLEANTABLE для представления может вернуть неиспользуемое пространство, возникшее из-за удаления текстовых столбцов или столбцов переменной длины. DBCC I NDEXDEFRAG может дефрагментировать индексы представлений, как и табличные индексы. DBCC SHOWCONTIG указывает степень фрагментации. Большинство DBCC-команд для обслуживания таблиц работают и для индексированных представлений. Более подробная информация об этом приведена в Books Online. Итоги В этой главе мы изучили объект представление и узнали следующее: ■ существуют различные виды представлений: обновляемые и представления только для чтения, индексированные и неиндексированные, локальные и секционированные распределенные представления; ■ когда разные представления работают лучше всего; ■ как создавать и администрировать представления; ; , '.I ■ как можно контролировать работу представлений.
10 Пользовательские функции Великие дизайнеры имеют неутолимую страсть к созиданию — и создают произведения искусства. Стив Макконнелл Пользовательские функции (User determined functions, UDF) — это специальный тип подпрограмм, которые могут быть использованы в выражениях. UDF напоминают встроенные функции SQL Server, они также принимают обязательные параметры и возвращают результат определенного типа. Пользовательские функции бывают трех типов: скалярные, табличные inline- и табличные multi-statement-функции. Скалярные функции всегда возвращают одно значение. Они могут быть использованы в выражениях подобно стандартным системным функциям. Табличные multi-statement-функции (я предпочитаю называть их просто табличными функциями) возвращают в качестве результата таблицу. Их можно использовать в части FROM выражения SELECT, подобно встроенным TSQL-функциям, возвращающим таблицу @PENXML(), OPENQUERY() и т. п.). Табличные inline-функции (будем их называть просто inline-функциями) возвращают результат единственного выражения SELECT, из которого они и состоят. Они также могут быть использованы в части FROM выражения SELECT. Скалярные функции Листинг 10.1 содержит пример простой скалярной функции. Листинг 10.1. Простая скалярная функция CREATE FUNCTION dbo.Proper((?Name sysname) RETURNS sysname AS BEGIN DECLARE @len int. @i int. @0utname sysname, @LastSpc bit SET (?len=DATALENGTH(@Name) SET @i=l SET @LastSpc=l SET @0utname='' WHILE @i<iaien BEGIN SET @0utname=@0utname+ CASE aastspc . -- продолжение #
256 Глава 10. Пользовательские функции Листинг 10.1 {продолжение) WHEN 1 THEN UPPER(SUBSTRING(CName.(ai.l)) ELSE L0WER(SUBSTRING(CName.(ai.l)) END SET @LastSpc=CASE SUBSTRING(CName.(ai .1) WHEN ' ' THEN 1 ELSE 0 END SET @i=<ai+i END RETURN(@Outname) . ;'. END GO SELECT dbo.ProperCthomas a. edison') (Результаты) Thomas A. Edison Эта функция преобразует строку в «правильный» регистр. Каждый символ строки, следующий за пробелом, преобразуется в верхний регистр; остальные символы преобразуются в нижний регистр. Такое преобразование полезно для имен собственных, в частности для полного имени. Табличные функции Табличные функции — это довольно мощные и относительно простые в использовании функции. Они возвращают результат типа table (UDF не поддерживают вывод результатов через параметры). С некоторыми ограничениями над переменными типа table можно производить те же самые операции, что и с обычными таблицами, — включая вставку, изменение и удаление данных (единственное, нельзя выполнять INSERT. .. EXEC, SELECT INTO и использовать определяемые пользователем типы данных). Столбцы типа text хранятся с включенной опцией text in row. Посмотрите на простой пример табличной функции. Листинг 10.2. Простая табличная функция CREATE TABLE staff (employee int PRIMARY KEY. employeejiame varchar(lO). supervisor int NULL REFERENCES staff (employee)) •"• ; -• ■: • - INSERT staff VALUES A.'GROUCHO'.1) INSERT staff VALUES B.'CHICO'.1) INSERT staff VALUES C,'HARPO'.2) . .,.„ INSERT staff VALUES D.'ZEPPO',2) ' ' ' INSERT staff VALUES E.'M0E'.1) INSERT staff VALUES F.'LARRY'.5) INSERT staff VALUES G.'CURLY'.5) INSERT staff VALUES (8.'SHEMP'.5) INSERT staff VALUES (9.'JOE'.8) INSERT staff VALUES A0,'CURLY JOE',9) GO DROP FUNCTION dbo.ORGTABLE GO CREATE FUNCTION dbo.0RGTABLE(Cemployee_name varcharA0)='r) RETURNS (aorgtable TABLE (sequence int,
Табличные функции 257 ,, supervisor varchar(lO). supervises varchar(lO). . - . . employeejiame varchar(lO)) AS BEGIN DECLARE Pworktable TABLE (seq int identity, chartdepth int. employee int, supervisor int) INSERT @worktab1e (chartdepth. employee, supervisor) SELECT chartdepth=l. employee=o2.employee. supervisor=ol.employee K FROM staff ol INNER JOIN staff o2 ON (ol.employee=o2.supervisor) '■' WHERE ol. empl oyeejiame LIKE @emp1oyeejiame WHILE ((?(arowcount > 0) BEGIN INSERT @worktab1e (chartdepth. employee, supervisor) SELECT DISTINCT ol.chartdepth+1. o2.employee, ol.supervisor FROM @worktable ol INNER JOIN @worktab1e o2 ON (ol.employee=o2.supervisor) WHERE ol.chartdepth=(SELECT MAX (chartdepth) FROM (?worktable) AND ol.supervisorool.employee END INSERT (?orgtable SELECT seq. s.employeejiame. supervises"'supervises'. e.employeejiame FROM @worktable о INNER JOIN staff s ON (o.supervisors.employee) INNER JOIN staff e ON (o.employee=e.employее) WHERE o.supervisoroo.employee ORDER BY seq . , . RETURN END GO SELECT * FROM ORGTABLE(T) ORDER BY Sequence GO . • • • ' DROP TABLE staff (Результаты) sequence г 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 supervisor GROUCHO CHICO CHICO GROUCHO МОЕ МОЕ МОЕ SHEMP JOE GROUCHO GROUCHO GROUCHO GROUCHO GROUCHO МОЕ SHEMP GROUCHO GROUCHO МОЕ GROUCHO ■ supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises supervises empl oyeejiame CHICO HARPO ZEPPO МОЕ LARRY CURLY SHEMP JOE CURLY JOE HARPO ZEPPO LARRY CURLY SHEMP JOE CURLY JOE JOE CURLY JOE CURLY JOE CURLY JOE
258 Глава 10. Пользовательские функции Эта функция создает организационную диаграмму на основе таблицы сотрудников, которую и возвращает в качестве результата. Отметьте использование identity-столбца во внутренней таблице и последующие операции на его основе внутри функции. С точки зрения использования, табличные перемЪнные почти идентичны временным таблицам, но чуть более масштабируемы в терминах согласованности. Обратите внимание на отсутствие упоминания о владельце при вызове функции. Только скалярные функции требуют явного указания имени владельца - в табличных и inline-функциях его можно не указывать. Функция заканчивается оператором RETURN, присутствие которого является обязательным для всех типов функций. Поскольку в табличную переменную уже загружены данные, при завершении функции эта таблица возвращается. Inline-функции Inline-функции — это тоже очень мощный инструмент. Они предоставляют функциональность параметризованных представлений, о которой многие из нас мечтали годами. У inline-функции нет тела, поэтому необязательно использование BEGIN.., END. Эти функции состоят только из одного выражения SELECT, которое уютно поместилось в RETURN. Поскольку SELECT однозначно определяет, какой тип таблицы возвращается, в части RETURNS inline-функции возвращающийся результат определяется просто как TABLE (без определения таблицы). В этом отличие от обычных табличных функций, для которых в RETURNS необходимо явно указывать возвращаемую таблицу. В этом примере показана inline-функция. Листинг 10.3. Простая inline-функция CREATE TABLE tempdb. .singles (band int. single int. title varcharOO)) "я INSERT tempdb..singles VALUESCO.O.'LITTLE BIT') INSERT tempdb..singles VALUESC0.1.'FIRE AND WATER') INSERT tempdb..singles VALUES@,2. 'ALL RIGHT NOW) INSERT tempdb.. singles VALUESd.O,'BAD COMPANY') INSERT tempdb..singles VALUESC1.1.'SHOOTING STAR') INSERT tempdb..singles VALUESC1.2.'FEEL LIKE MAKIN" LOVE') INSERT tempdb..singles VALUESA.3.'ROCK AND ROLL FANTASY') INSERT tempdb..singles VALUESC1.4.'BURNING SKY') INSERT tempdb..singles VALUESC2.0.'SATISFACTION') INSERT tempdb..singles VALUESC2.1.'RADIOACTIVE') INSERT tempdb..singles VALUESC2.2.'MONEY CAN"T BUY') INSERT tempdb..singles VALUESB.3.'TOGETHER') INSERT tempdb..singles VALUESC.0.'GOOD MORNING') INSERT tempdb..singles VALUESC3.1.'H00CHIE-C00CH1E MAN') INSERT tempdb..singles VALUESC.2.'MUDDY WATER BLUES') INSERT tempdb..singles VALUESC.3.'THE HUNTER') GO DROP FUNCTION PaulRodgersSingles -■ GO CREATE FUNCTION Pau1RodgersSing1es((?tit1e varcharE0)=T) RETURNS TABLE AS RETURN(SELECT Free=MIN(CASE band WHEN 0 THEN CAST(title AS charA8)) ELSE NULL END),
Ограничения 259 BadCompany=MIN(CASE band WHEN 1 THEN CASKtitle AS charBD) ELSE NULL END). TheFirm=MIN(CASE band WHEN 2 THEN CASKtitle AS charB3)) ELSE NULL END), So1o=MIN(CASE band WHEN 3 THEN title ELSE NULL END) FROM tempdb..singles WHERE title LIKE ©title •• ■■ \^ GROUP BY single) GO SELECT * FROM PaulRodgersSingles(DEFAULT) GO DROP TABLE tempdb..singles (Результаты сокращены) Free BadCompany TheFirm Solo LITTLE BIT BAD COMPANY SATISFACTION GOOD MORNING FIRE AND WATER SHOOTING STAR RADIOACTIVE HOOCHIE-COOCHIE MAN ALL RIGHT NOW FEEL LIKE MAKIN' LOVE MONEY CAN'T BUY MUDDY WATER BLUES NULL ROCK AND ROLL FANTASY TOGETHER THE HUNTER ' •' NULL BURNING SKY NULL NULL Эта функция принимает линейный набор данных — таблицу холостяков — и создает перекрестную таблицу, в которой люди разбиты по группам (столбец band). Функция принимает единственный параметр @title, который ограничивает количество выводимых записей. В этом случае функция работает как параметризованное представление. Ограничения Существуют ограничения, которые применяются исключительно к функциям. ■ За исключением inline-функций, функции должны иметь внешние BEGIN и END, что отличает их от триггеров и процедур. Любопытно, что в inline-функциях внешние BEGIN и END не только не являются необходимыми, но и просто не допускаются. Применив их, вы получите синтаксическую ошибку. ■ При вызовах скалярных функций необходимо указывать их владельца. Посмотрите на префикс dbo. при вызове функции Ргорег(). Однако есть недокументированная возможность обойти это ограничение — ее мы обсудим немного позже. ■ Последним выражением в функции должен быть RETURN. ■ Есть множество ограничений на TSQL, используемый в функциях. Вообще говоря, в функциях нельзя использовать ничего, что могло бы иметь побочный эффект. Например, в функциях вы не сможете создать постоянный объект, использовать временные таблицы (даже предварительно созданные) и вызывать хранимые процедуры. Вы сможете создавать табличные переменные, но у них есть свои ограничения (нельзя использовать определяемые пользователем типы данных, конструкции INSERT... EXEC и SELECT INTO). Вы сможете вызывать расширенные хранимые процедуры, но их название должно начинаться с хр. Некоторые расширенные хранимые процедуры (например, sp_executesql или sp_xml_preparedocument), с точки зрения Enterprise Manager, являются именно
260 Глава 10. Пользовательские функции расширенными хранимыми процедурами, но начинаются с префикса sp вместо хр. Их вызов из функций невозможен. У меня есть процедура хр_ехес, которая позволяет выполнять различные TSQL-инструкции через интерфейс расширенных хранимых процедур, и ее можно использовать в функциях. Чтобы более подробно узнать об этом, загляните в раздел «Параметризованные пользовательские функции». ■ В функциях нельзя использовать RAlSERROR() или устанавливать переменную @@ERR0R. ■ Большинство настроек окружения, которые можно изменять внутри хранимых процедур или устанавливать только на время их исполнения, нельзя изменять внутри UDF. Например, SET NOCOUNT ON недопустим. ■ Нельзя вызывать UDF, используя стандартный формат именования из четырех частей. Например, вы не сможете сделать следующее. Листинг 10.4. Нельзя удаленно вызывать функции SELECT kufnahte...calc_interestA00000.7.6,30) -В некоторой степени можно обойти это ограничение, используя функцию 0PENQUERY(), например, таким образом: SELECT * FROM OPENQUERYCkufnahte. 'select dbo.calcJnterestQOOOOO.7.6.30)') 0PENQUERY() и подобные функции не позволяют использовать переменные при передаче строки запроса — допустимы только строковые константы, — таким образом, мы не можем передавать параметры. Ужасное ограничение! Един-, ■ ственный рабочий вариант, который я нашел, заключается в использовании'" функции sp_executesql с выходным параметром: DECLARE ©interest int EXEC kufnahte.. .sp_executesql N'SELECT @mt=dbo.calc_interest(@prin,Prate,@years)'. Nuprin int, Grate int. @years int. (?int int OUT'. 100000, 7.6. 30, ^interest OUT SELECT (^interest Достаточно много действий, чтобы вызвать функцию. Было бы гораздо проще использовать хранимые процедуры или скопировать функцию на локальный сервер. ■ В функциях параметры по умолчанию, вообще говоря, не являются таковыми: мы не можем не указывать их при вызове. Вместо пропуска этих параметров мы обязаны использовать ключевое слово DEFAULT, даже если это единственные параметры у функции, — а при использовании функций с большим количеством необязательных параметров это раздражает. Посмотрите пример. Листинг 10.5. Необязательные параметры функций не так уж необязательны CREATE FUNCTION dbo.Sprintf(OFrntStr varcharB55) ,@ParmO varcharB55) ,@Parml varcharB55)='' ,@Parm2 varcharB55)='' ,@Parm3 varcharB55)='' ■ ,@Parm4 varcharB55)='' , с . ,@Parm5 varcharB55)='' , ,@Parm6 varcharB55)='' ,@Parm7 varcharB55)=''
Ограничения 261 .(арагпй varcharB55)='' t ,(аРагш9 varcharB55)=" ( ) RETURNS VARCHARB55) AS BEGIN DECLARE @Result varcharB55) EXEC master.,xp_sprintf @Result OUT. @FmtStr. (aParmO, (aparml, №arm2. @РагтЗ. №arm4. @Parm5. @Рагтб. №arm7. №агт8, @Parm9 RETURN(@Result) END ' ' '■' GO DECLARE @Artist varcharOO). @Song varcharOO). @Band varcharOO) SELECT @Artist='Paul Rodgers', @Song='Fire and Water', №and='Free' SELECT dbo.Spnntf('£s sang the song "%s" for the band fe',@Artist, @Song, @Band, DEFAULT. DEFAULT, DEFAULT. DEFAULT. DEFAULT. DEFAULT. DEFAULT) Видите проблему? Поскольку Sprintf () поддерживает до девяти необязательных параметров, мы должны постоянно использовать ключевое слово DEFAULT. В терминах C/C++ мы не можем создать подобие varargs несмотря на то, что встроенная функция xp_sprintf в действительности так и работает (позволяя передавать себе произвольное количество параметров). ПРИМЕЧАНИЕ Хочу вас предупредить, что максимальная длина параметров и результата функции Sprintf() не может быть более 255 байт. Это объясняется тем, что у вызываемой расширенной хранимой процедуры xp_strintf такие же ограничения. Xp_strintf, подобно многим устаревшим расширенным хранимым процедурам, используют изжившие себя функции ODS srv_paramdata() и srv_paramlen() для получения длины и содержимого входных параметров. В отличие от новой функции spv_paraminfo(), эти функции не поддерживают новые (появившиеся в версиях SQL Server старше 6.5) типы данных и их увеличившийся объем. Таким образом, строки длиной более 255 байт будут обрезаться при выводе процедурой xp_sprintf(). ■ В UDF нельзя использовать недетерминированные функции. Детерминированная функция всегда возвращает одинаковый результат при вызове ее с одинаковыми параметрами. Недетерминированная функция, наоборот, может возвращать различные результаты от вызова к вызову, даже если они вызываются с одинаковыми параметрами. Не позволяется вызывать недетерминированные системные функции из UDF, так же как не позволяется индексировать недетерминированные вычисляемые столбцы (те, которые вычисляются на основе недетерминированных системных или пользовательских функций). Недетерминированными считаются функции GETDATE(), @@C0NNECTI0NS, NEWIDQ hRAND(). В Books Online есть полный список. Можно обернуть недетерминированную функцию в представление CREATE VIEW view_rand AS SELECT rand() AS rand. В UDF допускается использование такого представления, и мы получаем обходной путь для недетерминированных функций. Единственным ограничением остается невозможность передавать параметры в системные функции (например, нельзя инициализировать rand() своим значением). ■ Нельзя создавать первичные ключи на вычисляемых столбцах, если они построены на UDF, которые могут возвращать NULL-значения. Числовые UDF, например, могут вернуть NULL-значение при ошибках переполнения или деления на ноль. Обходным путем является использование при определении вычисляв-
262 Глава 10. Пользовательские функции мого столбца функции ISNULL(), чтобы исключить возможность появления NULL- значений. Метаданные Transact-SQL предоставляет несколько свойств OBJECTPROPERTY() для получения информации о пользовательских функциях. Можно определить тип пользовательской функции, а также узнать, является ли она детерминированной и/или привязанной к схеме. В табл. 10.1 перечислены эти свойства. Таблица 10.1. Свойства OBJECTPROPERTY(), относящиеся к функциям Свойство Значение Тип объекта из sysobjects IsInlineFunction Возвращает 1 для табличных inline-функций IF IsTableFunction Возвращает 1 для табличных multi-statement-функций TF IsScalarFunction Возвращает 1 для скалярных функций FN IsDeterministic Возвращает 1 для детерминированных функций IsSchemaBound Возвращает 1 для привязанных к схеме UDF Посмотрите, как это можно использовать. Листинг 10.6. OBJECTPROPERTY() возвращает метаданные о функциях SELECT LEFT(name.20) AS [Function]. OBJECTPROPERTYtid. OBJECTPROPERTYtid. OBJECTPROPERTYtid. OBJECTPROPERTYtid. OBJECTPROPERTYtid. FROM sysobjects WHERE type in ('IF ORDER BY name (Результаты) Function ORGTABLE PaulRodgersSingles Proper Sprintf IsScalarFunction') AS Scalar. IsTableFunction') AS [Table], IsInlineFunction') AS Inline, IsDeterministic') AS Determ. IsSchemaBound') AS SchemaBound .'TF'.'FN') Scalar Table Inline Determ SchemaBound 0 10 0 0 0 0 10 0 10 0 0 0 10 0 0 0 Обратите внимание, что ни одна из функций не является детерминированной. Вспомните обсуждение детерминизма ранее. Как вы думаете, что делает функцию Рroper () недетерминированной? Для заданных входных значений она возвращает одинаковый результат. Это же относится и к функции Sprintf (). Она тоже возвращает одинаковый результат при вызове с одинаковыми параметрами. Что же мешает этим функциям быть детерминированными? Причина проста: привязка к схеме. SQL Server считает функцию недетерминированной при соблюдении следующих условий:
Метаданные 263 ■ функция не привязана к схеме; г ■ по крайней мере, одна из вызываемых внутри функций недетерминирована; ■ функция обращается к объекту базы данных, находящемуся за пределами ее области видимости; ■ функция вызывает расширенную хранимую процедуру. Таким образом, функция Ргорег() не является детерминированной из-за того, что она была создана без привязки к схеме. Функция Sprintf () тоже нарушает два из этих правил: она не привязана к схеме и внутри нее есть вызов расширенной хранимой процедуры. Что же такое привязка к схеме? Привязка к схеме привязывает функцию (или представление) ко всем объектам, используемым в ней, таким образом, что становится невозможно изменить эти объекты, если при этом изменяется их структура. Возьмем, например, inline-функцию, тип результата которой полностью определяется структурой таблицы, из которой она берет данные. Что произойдет, если изменится структура таблицы? Что произойдет, если изменение типа данных одного из столбцов приведет к тому, что выражение SELECT (из которого состоит функция) станет приводить к ошибке при вызове функции? Просто ваш код перестанет работать. Указание опции SCHEMABINDING при создании функции или представления предназначено для избежания подобных проблем. Эта опция не допускает изменения, влияющие на структуру объектов. В каком-то смысле, все это напоминает ссылочную целостность на внешних ключах: гарантируется структурная целостность зависимых объектов. Перед использованием опции SCHEMABINDING в выражении CREATE FUNCTION необходимо выполнить следующие условия. ■ Если в функции используются вызовы других функций или представлений, они тоже должны быть созданы с опцией SCHEMABINDING. ■ В функции везде должны явно перечисляться столбцы. Использование выра- ■ жений SELECT * без явного перечисления столбцов недопустимо. ■ Если функция ссылается на другие объекты, то они должны находиться в этой же базе данных и на них нельзя ссылаться при помощи формата вызова из трех или четырех частей. ■ В функции не могут использоваться локальные переменные. ■ Если функция ссылается на другие объекты, для этих объектов должны быть установлены разрешения REFERENCES. - Видите, в чем проблема? Функция Ргорег() не является детерминированной, потому что она не привязана к схеме, а к схеме ее привязать нельзя, потому что в ней используются локальные переменные. Нам не удастся сделать функцию Ргорег() детерминированной. Давайте попробуем это проделать с inlme-функци- ей. Создадим ее опять, применив опцию SCHEMABINDING (листинг 10.7). Листинг 10.7. Привязанная к схеме inline-функция CREATE FUNCTION PaulRodgersSingles(@title varcharE0)='r) RETURNS TABLE WITH SCHEMABINDING AS ч продолжение J>
264 Глава 10. Пользовательские функции Листинг 10.7 {продолжение) RETURNCSELECT Free=MIN(CASE band WHEN 0 THEN CASTttitle AS charA8)) ELSE NULL END), BadCompany=MIN(CASE band WHEN 1 THEN CASTttitle AS charBD) ELSE NULL END). TheFirm=MIN(CASE band WHEN 2 THEN CAST(title AS charB3)) ELSE NULL END). Solo=MIN(CASE band WHEN 3 THEN title ELSE NULL END) FROM dbo.singles WHERE title LIKE @title GROUP BY single) Есть два различия между этой функцией и ее первым вариантом. Во-первых, мы добавили в заголовок опцию SCHEMABINDING. Во-вторых, теперь мы обращаемся к таблице singles, используя формат из двух частей (вместо трех в предыдущем варианте). Несмотря на то что обе версии функции обращаются к одной таблице, использование формата вызова из трех и четырех частей недопустимы в детерминированных функциях — я упоминал об этом ранее. Хорошо, теперь функция привязана к схеме. Стала ли она детерминированной? Проверяем: Function Scalar Table Inline Determ SchemaBound ORGTABLE 0 10 0 0 PaulRodgersSingles 0 0 10 1 ' Proper 1 0 0 0 0 Sprintf 10 0 0 0 Она осталась недетерминированной. Почему? Потому что табличные inline- функции не могут быть детерминированными. По определению, их результат полностью зависит от данных в таблице, на которую они ссылаются, поэтому нельзя гарантировать неизменность их результатов в любой момент времени. Вследствие этого опция IsDeterministic функции 0BJECTPR0PERTY() применяется только к скалярным и табличным функциям. Теперь предлагаю создать детерминированную функцию, чтобы увидеть, на что это похоже (листинг 10.8). Листинг 10.8. Эта функция и детерминирована и привязана к схеме * CREATE FUNCTION dbo.FortuneCookie(@Date datetime='20000101') RETURNS varcharB55) WITH SCHEMABINDING ' - '' AS BEGIN RETURNtCASE DATEPART(mm.(aDate) WHEN 1 THEN '10 программистов продукт решили сделать. Один спросил: "А деньги где?" - и их осталось девять.' WHEN 2 THEN '9 программистов предстали перед боссом. Один из них не знал FoxPro, и их осталось 8.' WHEN 3 THEN '8 программистов купили IBM. Один сказал: "Мак лучше!" - и их осталось 7,' WHEN 4 THEN '7 программистов хотели хелп прочесть. У одного накрылся винт, и их осталось 6.' WHEN 5 THEN '6 программистов пытались код понять. Один из них сошел с ума. и их осталось 5.' WHEN б THEN '5 программистов купили CD-ROM. Один принес китайский диск - остались вчетвером.' WHEN 7 THEN '4 программиста работали на Си. Один из них хвалил Паскаль, и их осталось 3.' WHEN 8 THEN '3 программиста в сети играли в DOOM. Один чуть-чуть замешкался, и счет стал равен двум.' WHEN 9 THEN '2 программиста набрали дружно "win". Один устал загрузки ждать - остался лишь 1.'
Создание собственных системных функций 265 WHEN 10 THEN '1 программист взял все под свой контроль. Но встретился с заказчиком, и их осталось 0.' WHEN 11 THEN '0 программистов ругал сердитый шеф. Потом уволил одного и стало их FF.' WHEN 12 THEN 'С новым годом!' END) END Эта функция принимает дату как параметр и в зависимости от месяца возвращает ту или иную фразу. Она не ссылается ни на какие объекты и не использует локальные переменные. Она всегда возвращает один и тот же результат для одних и тех же параметров. Она создана при помощи опции SCHEMABINDING и ничто не мешает ей быть детерминированной. Давайте проверим: Function Scalar Table Inline Determ SchemaBound FortuneCookie 10 0 11 0RGTABLE 0 10 0 0 PaulRodgersSingles 0 0 10 1 Proper 10 0 0 0 Sprintf 10 0 0 0 Таким образом, мы получили функцию, привязанную к схеме и детерминированную. Если мы захотим, то сможем использовать ее в составе индексированного вычисляемого столбца или в индексированном представлении. Для остальных функций это не так. Создание собственных системных функций Вспомните наш разговор про создание собственных системных представлений в главе 9, которые могут быть вызваны из любой базы данных и выполняться в контексте этой базы данных. Подобно представлениям, можно реализовать и системные функции. Можно создать функции в базе master таким образом, чтобы к ним можно было обращаться из любой базы данных без явного указания базы данных при вызове. Вот как это работает. SQL Server создает несколько системных функций во время инсталляции (например, fn_varbintohexstr() или fn_chariswhitespace() и т. п.). Некоторые из них имеют в качестве владельца system_f unction_scbema, некоторые — нет. Те, владелец которых system_f unction_schema, могут вызываться из любой базы данных при помощи формата вызова из одной части. Их список можно получить посредством следующего запроса. Листинг 10.9. Системные функции SQL Server USE master GO SELECT LEFT(name,50) AS name FROM sysobjects WHERE uid4JSER_ID('system_function_schema') AND @BJECTPROPERTY(id. ' IsScalarFunction')=1 - •- ■ . ■■) .- . OR OBJECTPROPERTYdd, ' IsTableFunction')=1 OR OBJECTPROPERTYtid. ' IsInlineFunction')=1) _ * ' ;s,i ■ ■ (Результаты) продолжение #
266 Глава 10. Пользовательские функции Листинг 10.9 {продолжение) name fn_chari swhi tespace fn_dblog fn_escapecmdshel1 symbol s fn_escapecmdshel1 symbol sremovequotes fn_generateparameterpattern fn_get_sql fn_getpersistedservernamecasevariation fn_helpcollations • fn_listextendedproperty fn_removeparameterwithargument fn_repladjustcolumnmap fn_replbitstringtoint fn_replcomposepublicationsnapshotfolder fn_replgenerateshorterfilenameprefix fn_replgetagentcommandlinefromjobid fn_replgetbi nary81odword fn_repli nttobi tstri ng fn_replmakestringliteral fn_replprepadbinary8 fn_replquotename fn_replrotr . .•• fn_repltrimleadingzerosinhexstr fn_replun1quename fn_serverid fn_servershareddri ves fn_skipparameterargument ;- .vs.: ,., .. - "■ ■ •■* ' \i ,.* fn_trace_geteventinfo ' * ' '" fn_trace_getfilterinfo -■ fn_trace_getinfo fn_trace_gettable fn_updateparameterwithargument fn_virtualfilestats fn_vi rtualservernodes В главе 22 рассказано более детально о создании системных функций и других системных объектов. А пока просто запомните, что функции, имеющие владельца system_f unction_schema, могут вызываться из любой базы данных без явного указания названия базы данных. Для создания системных пользовательских функций нужно сделать следующее. 1. Разрешить прямое обновление системных таблиц процедурой sp_conf igure: sp_configure 'allow updates'.1 RECONFIGURE WITH OVERRIDE 2. Создать функцию в базе master, указав владельца. Имя функции должно начинаться с f n_ и быть написано целиком в нижнем регистре. 3. Отключить прямое обновление системных таблиц: Л;' sp_configure 'allow updates'.0 RECONFIGURE WITH OVERRIDE Вот пример пары простых системных функций. : Листинг 10.10. Системные UDF: fn_greatest() и fnjeast() USE master GO
«Рецепты» UDF 267 exec sp_configure 'allow updates'.1 GO reconfigure with override GO DROP FUNCTION system_function_schema.fn_greatest. system_function_schema.fnjeast GO CREATE FUNCTION system_function_schema.fn_greatest(@x bigint, @y bigint) RETURNS bigint AS BEGIN RETURN(CASE WHEN @x>@y THEN @x ELSE @y END) END GO CREATE FUNCTION system_function_schema.fn_least(@x bigint. @y bigint) RETURNS bigint AS BEGIN RETURN(CASE WHEN @x<@y THEN @x ELSE @y END) END GO exec sp_configure 'allow updates'.0 GO reconfigure with override GO use northwind GO SELECT fn_greatestB156875324698752.2156875323698752), fn_least(989. 998) (Результаты) DBCC execution completed. If DBCC printed error messages, contact your system administrator. Configuration option 'allow updates' changed from 0 to 1. Run the RECONFIGURE statement to install. DBCC execution completed. If DBCC printed error messages, contact your system administrator. Configuration option 'allow updates' changed from 1 to 0. Run the RECONFIGURE statement to install. 2156875324698752 989 Здесь мы создали две новые системные функции: f n_greatest() и f n_least(). Они соответствуют функциям GREATEST и LEAST в ORACLE. Эти функции возвращают максимальное и минимальное значение двух 8-битны.х целых. Следуя этому алгоритму, вы можете создавать свои собственные системные функции. Заметьте, что табличные системные функции должны начинаться с двоеточия независимо от того, создаются они при инсталляции или устанавливаются вручную. «Рецепты» UDF Следующий раздел содержит «рецепты» пользовательских функций, которые я написал в свое время сам для себя. Такие разделы я люблю больше всего: в них у меня есть возможность представить на всеобщее обозрение отрывки моего собственного кода и показать этим всему миру, чего я достиг. Одна из целей этой книги — снабдить вас кодом, который имеет практическое применение. Иными слова-
268 Глава 10. Пользовательские функции ми, дать вам код, который вы можете поместить в свою систему, и он станет работать. В этом суть следующего раздела. В нем демонстрируются некоторые реально работающие приемы, о которых мы уже говорили, и несколько новых. Улучшенная функция SOUNDEX() Встроенная функция S0UNDEX() является полезным инструментом, но использует очень примитивный алгоритм, который можно ощутимо улучшить. Следующая UDF — f n__soundex() — является улучшением стандартной функции S0UNDEX(). Она возвращает большее количество уникальных кодов и, вообще говоря, более функциональна. Посмотрим код. Листинг 10.11. Функция, реализующая лучший soundex-алгоритм USE master GO EXEC sp_configure 'allow updates'.1 GO RECONFIGURE WITH OVERRIDE GO DROP FUNCTION system_function_schema.fn_soundex GO CREATE FUNCTION system_function_schema.fn_soundex(@instnng varcharE0)) RETURNS varcharE0) /* Объект: fn_soundex Описание: Возвращает soundex-код строки (оптимизация Расселла) Использование: SELECT fn_soundex(@instring=string to translate) Возвращается: строка, содержащая soundex-индекс Автор: Ken Henderson. Email: khen@khen.com Версия: 8.0 Пример: SELECT fn_soundex('Rodgers') Создан: 1998-05-15. Последнее изменение: 2000-05-20. Замечания: Основана на soundex-алгоритме, опубликованном Robert Russell и Margaret 0'Dell. 1918. расширенном, чтобы включить оптимизацию Расселла для более тонкой степени детализации */ AS BEGIN DECLARE @workstr varchar(lO). @soundex varcharE0) ;-, ,, . • . , . ■ SET @instring=UPPER(@instring) SET @soundex=RIGHT(@instring.LEN((ainstring)-l) -- Put all but the first char in a work buffer (we always return the first char) /* • ■ ',■*■■';< '><*_.'. '•>. Перекодируем символы в числа по этой таблице:
«Рецепты» UDF 269 Символ B.F.P.V C.G.J.K.Q.S.X.Z D.T L M.N R A.E.H.I.O.U.W.Y */ Число 1 2 3 4 5 6 9 SET @workstr='BFPV WHILE (@workstr<>") BEGIN SET @soundex=REPLACE(Ssoundex.LEFT(@workstг,1).'1') SET @workstr=RIGHT(@workstr.LEN(@workstr)-l) END SET @workstr='CGJKQSXZ' WHILE (@workstr<>") BEGIN SET (asoundex=REPLACE(@soundex,LEFT(@workstr.l).'2') SET @workstr=RIGHT(@workstr.LEN(iaworkstr)-l) END SET @workstr='DT' WHILE (@workstr<>''} BEGIN SET @soundex=REPLACE(@soundex.LEFT((aworkstr.l). '3') SET @workstr=RrGHT(@workstr.LEN(@workstr)-l) END SET @soundex=replace(@soundex.'L','4') SET @workstr='MN' WHILE (@workstr<>'') BEGIN SET @soundex=REPLACE(@soundex.LEFT(@workstг,1).'5') SET (aworkstr=RIGHT(@workstr.LEN((aworkstr)-l) END set @soundex=replace((asoundex, 'R'. '6') SET @workstr='AEHIOUWY' WHILE ((aworkstro'') BEGIN SET (asoundex=REPLACE(@soundex. LEFT(@workstr. 1).'9') SET @workstr=RIGHT(@workstr.LEN(@workstr)-l) END -- Теперь заменяем повторяющиеся цифры (e.g., '11' or '22') одиночными DECLARE @c Int SET @c=l WHILE (@c<10) BEGIN SET (asoundex=REPLACE((asoundex.C0NVERT(charB),@c*ll).C0NVERT(char(l).@c)) умножаем на 11 для получения повторяющихся цифр SET @c=@c+l END SET @soL.ndex=REPLACE((asoundex.'00','0') -- Убираем сдвоенные нули SET @soundex=REPLACE(@soundex.'9'. ") -- Убираем 9 SET @soundex=LEFT((asoundex.3) WHILE (LEN(@soundex)<3) SET @soundex=(asoundex+'0' -- дополняем нулями продолжение #
270 Глава 10. Пользовательские функции Листинг 10.11 {продолжение) SET @soundex=LEFT(@instring.l)+@soundex -- добавляем символ и возвращаем RETURN @soundex END GO EXEC sp_configure 'allow updates',0 GO RECONFIGURE WITH OVERRIDE GO SELECT fn_soundex('Rodgers') (Результаты) R326 Эта функция использует алгоритм лучший, чем алгоритм стандартной SOUNDEX(), и имеет сравнимую производительность. Это системная функция, поэтому ее можно использовать в любом случае, где ранее вы использовали SOUNDEX(). Хотя алгоритм, использованный в f n_soundex(), является улучшенным по сравнению с алгоритмом встроенной функции S0UNDEX(), он по-прежнему не так функционален, как хотелось бы. Поскольку алгоритм использует числовые значения в трех позициях из четырех, максимальное количество кодов равно 26 000 B6 х 103). Сравните его с функцией f n_soundex_ex(), которая в коде использует символы во всех позициях и имеет 456 978 B64) возможных значений. Вот эта функция. Листинг 10.12. fn_soundex_ex() — последнее улучшение SOUNDEX() USE master GO EXEC sp_configure 'allow updates'.1 GO RECONFIGURE WITH OVERRIDE GO DROP FUNCTION system_function_schema.fn_soundex_ex GO CREATE FUNCTION system_function_schema.fn_soundex_ex((ainstring varcharE0)) RETURNS varcharE0) /* Объект: fn_soundex_ex Описание; Возвращает soundex-код строки Использование: SELECT fn__soundex_ex(@instring=string to translate) Возвращается: строка, содержащая soundex-индекс Автор; Ken Henderson. Email; khentakhen.com Версия: 8.0 Пример: SELECT fn_soundex__ex('Rodgers ') Создан: 1998-05-15. Последнее изменение: 2000-11-21. Перевод на Transact-SQL осуществлен Ken Henderson. */
«Рецепты» UDF 271 AS BEGIN DECLARE @workstr varchar(lO), @soundex varcharE0) SET @instring=UPPER(@instnng) SET @soundex=RIGHT(@instring,LEN(@-instring)-l) -- Put all but the first char in a work buffer (we always return the first char) SET @workstr='EIOUY' -- Replace vowels with A WHILE (@workstr<>") BEGIN SET @soundex=REPLACE((asoundex.LEFT(@workstr,l), 'A') SET @workstr=RIGHT((aworkstr.LEN((aworkstr)-l) END /* Меняем префиксы слов From To MAC MCC KN NN К С PF FF SCH SSS ■* PH FF */ -- подставляем первую букву * SET @soundex=LEFT(@instnng,l)+@soundex IF (LEFT((asoundex,3)='MAC) SET (asoundex='MCC'+RIGHT(@soundex.LEN((asoundex)-3) ' IF (LEFT(@soundex,2)='KN') SET @soundex='NN'+RlGHT((asoundex,LEN((asoundex)-2) IF (LEFT(@soundex.l)='K') SET (asoundex='C'+RIGHT(@soundex.LEN((asoundex)-l) IF (LEFT((asoundex,2)='PF') SET (asoundex='FF'+RIGHT((asoundex,LEN((asoundex)-2) IF (LEFT(@soundex.3)='SCHr) SET @soundex='SSS'+RIGHT(@soundex,LEN((asoundex)-3) IF (LEFT(@soundex,2)=,PH') SET (asoundex='FF'+RIGHT(@soundex.LEN((asoundex)-2) -- удаляем первую букву SET (ainstring=@soundex ■ SET @soundex=RIGHT((asoundex.LEN(@soundex)-l) /* Меняем Было DG CAAN D NST AV Q Z M KN К H AW фоь нетические префиксы: Стало GG TAAN T NSS AF G S N NN С А (за исключением AHA) A
272 Глава 10. Пользовательские функции Листинг 10.12 {продолжение) РН FF SCH SSS */ SET Csoundex=REPLACE(@soundex. SET 0soundex=REPLACE(@soundex. SET @soundex=REPLACE(@soundex. SET @soundex=REPLACE(@soundex, SET @soundex=REPLACE((asoundex, SET @soundex=REPLACE(@soundex. SET @soundex=REPLACE(@soundex, SET @soundex=REPLACE@soundex. SET @soundex=REPLACE(@soundex. SET @soundex=REPLACE(@soundex. DC .'GG') CAAN'.'TAAN') D'. NST AV Q'. V . M\ KN' K'. 'T') ','NSS') ,'AF') ■G') 'S') 'N') .'NN') 'O -- меняем "Н" на "А" за исключением "AHA" SET Bsoundex=REPLACE(@soundex. 'AHA',' —') SET @soundex=REPLACE№soundex.'H'.'A') SET @soundex=REPLACE(@soundex,' —', 'AHA') SET @soundex=REPLACE(@soundex.'AW.'A') SET @soundex=REPLACE(@soundex.'PH'.'FF') SET @soundex=REPLACE(@soundex.'SCH','SSS') -- удаляем завершающую А или S IF (RIGHT((asoundex,l)='A' or RIGHT(@soundex,l)='S') SET @soundex=LEFT(@soundex,LEN(@soundex)-1) -- меняем последние "NT" на "ТТ" IF (RIGHT(@soundex.2)='NT') SET @soundex=LEFT(@soundex.LEN((asoundex)-2)+'Tr -- Удаляем все А SET @soundex=REPLACE(@soundex,'A', " ) -- подставляем первую букву SET @soundex=LEFT(@instring.l)+@soundex -- удаляем повторы DECLARE @c int SET @c=65 WHILE №<9\) BEGIN WHILE (CHARINDEX(char(Pc)+CHAR(@c),@soundex)<>0) SET @soundex=REPLACE(@soundex,CHAR((ac)+CHAR((ac).CHAR((ac)) SET @c=(ac+l end SET @soundex=LEFT(@soundex,4) IF (LEN(@soundex)<4) SET @soundex=@soundex+SPACED-LEN(@soundex)) -- дополняем пробелами RETURN(@Soundex) END GO EXEC spjconfigure 'allow updates'.0 GO RECONFIGURE WITH OVERRIDE - ■; ■ GO
«Рецепты» UDF 273 USE northwind • ■ - ■'• GO SELECT fn_soundex_ex(LastName) AS ex_Last, fn_soundex_ex(FirstName) AS ex_First. SOUNDEX(LastName) AS bi_Last, SOUNDEX(FirstName) AS bijirst FROM employees (Результаты) ex Last ex First bi Last bi First - DFL FLR LFRL PC BCN SN CNG CLHN DTSW ~~ NC ANTR JNT MRGR STFN MCL RBRT LR AN ~~ D140 F460 L164 P220 B255 S500 K520 C450 D326 — N520 A536 J530 M626 S315 M240 R163 L600 A500 Как видите, UDF берет значения прямо из столбцов таблицы. Главное преимущество функций над хранимыми процедурами в том, что их можно использовать в DML-выражениях, ссылаясь на данные из таблиц. Статистические функции С появлением поддержки функций в SQL Server, Transact-SQL приобрел большие возможности языков статистических вычислений. Учитывая ориентацию Transact-SQL на работу с множествами и прямой доступ к данным, можно понять, что при некоторых определенных обстоятельствах он превосходит по быстродействию стандартные статистические пакеты. Существует график обучения, составленный для большинства коммерческих статистических инструментов, который показывает, что Transact-SQL позволяет обработать простые операции в UDF и хранимых процедурах, — поэтому промышленные статистические пакеты стоит использовать только для сложных вычислений. Отсечение Процесс удаления максимальных и минимальных значений из результирующей выборки называется отсечением. В статистических вычислениях нам часто требуется убрать максимальные и минимальные значения какого-либо множества, чтобы сосредоточить внимание на его типичных членах. Функция в листинге 10.13 — MiddleTemperatures() — показывает, как это можно сделать. Эта функция берет данные из таблицы и отсекает определяемое пользователем количество максимальных и минимальных значений. Она реализована как inline-функция, чтобы избавиться от накладных расходов на хранение уже усеченной выборки. Она действует как параметризованное представление: мы задаем размер отсекаемой области — функция делает все остальное. Посмотрим код. Листинг 10.13. Использование UDF для статистического отсечения USE tempdb GO CREATE TABLE tempdb..TemperatureReadings (MiddayTemp int) INSERT tempdb. .TemperatureReadings VALUES G5) продолжение £>
274 Глава 10. Пользовательские функции Листинг 10.13 {продолжение) INSERT tempdb..TemperatureReadings VALUES (90) INSERT tempdb..TemperatureReadings VALUES G6) INSERT tempdb..TemperatureReadings VALUES (81) INSERT tempdb..TemperatureReadings VALUES (98) INSERT tempdb..TemperatureReadings VALUES F8) GO DROP FUNCTION dbo.MiddleTemperatures GO CREATE FUNCTION dbo.MiddleTemperatures(@ClipSize int = 2) RETURNS TABLE AS RETURN(SELECT v.MiddayTemp FROM tempdb..TemperatureReadings v CROSS JOIN tempdb..TemperatureReadings a GROUP BY v.MiddayTemp HAVING COUNT(CASE WHEN a.MiddayTemp <=v.MiddayTemp THEN 1 ELSE NULL END) > (PClipSize AND COUNT(CASE WHEN a.MiddayTemp >= v.MiddayTemp THEN 1 ELSE NULL END) >@ClipSize) GO SELECT * FROM dbo.MiddleTemperaturesB) ORDER BY MiddayTemp (Результаты) . . MiddayTemp 76 -. :: ". : - -._■ ,■ : :j.< 81 Warning: Null value is eliminated by an aggregate or other SET operation. Параметр @ClipSize определяет размер отсекаемой области. Этот параметр показывает, какое количество максимальных и минимальных элементов множества будут удалены из выборки. Гистограммы Гистограмма — своего рода диаграмма из столбцов, в которой данные определяют ширину или высоту столбцов, — является общепринятым типом отчетов. Гистограммы можно увидеть где угодно: от счета за обслуживание до брошюры с результатами продаж огромных компаний. Все развитые пакеты отчетов и диаграмм способны строить гистограммы по реляционным данным. Обычно они могут группировать, суммировать и экстраполировать данные гистограммы различными способами. Бывают случаи, когда вам необходимо вычислить данные для гистограммы для получения перекрестных запросов из линейных данных, чтобы потом использовать их при рисовании гистограммы или передать их в другую процедуру для последующей обработки. Следующий код показывает нам, как можно создать гистограмму, используя пользовательские функции. В этом коде генерируется двумерная гистограмма, основанная на данных из таблицы sales, Мы определяем условия фильтрования — функция делает остальное. USE pubs ' " " / GO ' - DROP FUNCTION dbo.SalesHistogram и GO * m CREATE FUNCTION dbo.SalesHistogram(@payterms varcharA2)=T) <Z RETURNS TABLE '> AS RETURN( '«
«Рецепты» UDF 275 SELECT ■' '•'■ " ' ' ' ' "- ' ' '""' PayTerms=isnull (s.payterms, 'NA'), "Less than 10"-COUNT(CASE WHEN s.sales >=0 AND s.sales <10 THEN 1 ELSE NULL END), 0-19"=COUNT(CASE WHEN s.sales >=10 AND s.sales <20 THEN 1 ELSE NULL END), 0-29"=COUNT(CASE WHEN s.sales >=20 AND s.sales <30 THEN 1 ELSE NULL END), 0-39"=COUNT(CASE WHEN s.sales >=30 AND s.sales <40 THEN 1 ELSE NULL END), 0-49"=COUNT(CASE WHEN s.sales >=40 AND s.sales <50 THEN 1 ELSE NULL END), 0 or more"=COUNT(CASE WHEN s.sales >=50 THEN 1 ELSE NULL END) FROM (SELECT t.titlejd. s.payterms, sales=ISNULL(SUM(s.qty).0) FROM titles t LEFT OUTER JOIN sales s ON (t.titlejd=s.title_id) GROUP BY t.titlejd, payterms) s WHERE s.payterms LIKE @payterms GROUP BY s.payterms ) GO SELECT * FROM dbo.SalesHistogram(DEFAULT) (Результаты) PayTerms Less than 10 10-19 20-29 30-39 40-49 50 or more Net 30 0 0 4 2 1 2 Net 60 1 3 4 0 0 0 ON invoice 0 2 0 10 1 Warning: Null value is eliminated by an aggregate or other SET operation. Мы определяем интересующие нас условия платежа, а функция SalesHistog ram() создает перекрестную диаграмму, представляющую разбитое по диапазонам количество продаж. В предыдущем примере мы получили данные по всем типам платежа, потому что передали ключевое слово DEFAULT в качестве параметра функции. Мы можем получить результаты только по одному типу платежа (листинг 10.14). Листинг 10.14. Использование 1ЮРдля получения гистограмм SELECT * FROM dbo.SalesHi stogramC"Net 30") (Результаты) PayTerms Less than 10 10-19 20-29 30-39 40-49 50 or more Net 30 0 0 4 2 1 2 Warning: Null value is eliminated by an aggregate or other SET operation. Изменение значений по времени Часто случается, что требуется отслеживать изменение некоторой величины в течение отрезка времени. Очевидный пример — колебания курса акций. Различные биржи публикуют курс акций на постоянной основе каждый рабочий день. Каждая акция имеет стоимость на начало и конец дня. Функция StockPriceFluctuaton (), представленная в листинге 10.15, показывает, как отслеживать колебания во времени на основе колебания курсов акций. Она представляет собой inline-функцию, которая соединяет таблицу курсов акций с собой же путем приравнивания начальных и конечных значений даты для недельных периодов, а затем получает разницу курсов по каждому периоду. В данных содержатся примерные значения курсов акций Microsoft (MSFT) и Oracle (ORCL) за шестимесячный период с июня 2000 по январь 2001 года. Посмотрим данные и код.
276 Глава 10. Пользовательские функции Листинг 10.15. Функция для отслеживания изменений во времени USE tempdb go - • ■ CREATE TABLE dbo.stockprices (Symbol varcharD), TradingDate smalldatetime, CiosingPrice decimalA0.4)) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) , VALUES СMSFTV20000706', 82.000) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES ('MSFT'. ■гООООУЮ'. 78.938) _ ' v'V INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) '" '.;.". VALUES CMSFT','20000717', 72.313) . ,;£ INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) ""';.' VALUES CMSFT'.'20000724', 69.688) INSERT dbo.stockprices (Symbol, TradingDate. CiosingPrice) ,', VALUES СMSFT','20000731', 69.125) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES CMSFT','20000807', 72.438) INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) .. r^o VALUES CMSFT','20000814', 71.000) INSERT dbo.stockprices (Symbol. TradingDate. CiosingPrice) ; VALUES ('MSFT','20000821', 70.625) INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) VALUES CMSFT'.'20000828'. 70.188) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES СMSFT','20000905', 69.313) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) '•'' VALUES CMSFT'.'20000911', 64.188) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES С MSFT','20000918'. 63.250) . _.-, INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) "... VALUES CMSFT','20000925', 60.313) '"''■*'' INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) VALUES ('MSFT','20001002', 55.563) ■•'" '"' ' ='■"''"' INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) -: VALUES CMSFT','20001009'. 53.750) INSERT dbo.stockprices (Symbol, TradingDate. CiosingPrice) VALUES CMSFT','20001016', 65.188) INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) 4*' VALUES CMSFT','20001023'. 67.688) ^, INSERT dbo.stockprices (Symbol, TradingDate. CiosingPrice) ■ V; VALUES CMSFT','20001030', 68.250) INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) ' ■'■'V№ VALUES CMSFT','20001106', 67.375) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES CMSFT','20001113', 69.063) ' *л " • -> ' ■.,.:• INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) VALUES CMSFT','20001120', 69.938) INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES ('MSFT','20001127'. 56.625) .■ , • ■■ W INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) VALUES CMSFT','20001204', 54.438) , \ INSERT dbo.stockprices (Symbol, TradingDate. CiosingPrice) VALUES CMSFT','20001211', 49.188) ' - "^ INSERT dbo.stockprices (Symbol, TradingDate, CiosingPrice) . , • <nv VALUES CMSFT','20001218', 46.438) . ^ INSERT dbo.stockprices (Symbol. TradingDate. CiosingPrice) VALUES ('MSFT'.'20001226', 43.375) ' '"/'- _' INSERT dbo.stockprices (Symbol. TradingDate, CiosingPrice) - >■■' " VALUES CMSFT','20010102', 49.125) - :. i;d
«Рецепты» UDF 277 INSERT dbo.stockprices (Symbol. TradingDate. ClosingPrice) - ■■'; ■•••■'' ■'■■-'■■■ VALUES CMSFT','20010108'. 53.500) INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) , VALUES CORCL','20000706', 37.938) INSERT dbo.stockprices (Symbol. TradingDate. ClosingPrice) VALUES CORCL'.'20000710'. 38.063) INSERT dbo.stockprices (Symbol. TradingDate. ClosingPrice) VALUES CORCL','20000717'. 37.719) INSERT dbo.stockprices (Symbol, TradingDate. ClosingPrice) VALUES CORCL','20000724', 36.188) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL'.'20000731', 40.781) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL'.'20000807'. 40.563) - i INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) VALUES CORCL'.'20000814', 40.656) .|. INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) { VALUES CORCL'.'20000821'. 42.313) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL' ,'2000082B', 46.313) I INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) • I ' • VALUES CORCL','20000905', 43.281) INSERT dbo.stockprices (Symbol, TradingDate. ClosingPrice) VALUES CORCL','20000911'. 39.156) INSERT dbo.stockprices (Symbol, TradingDate. ClosingPrice) VALUES CORCL'.'20000918', 40.367) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) ' ■'" VALUES CORCL','20000925', 39.375) INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) VALUES CORCL'.'20001002', 33.813) INSERT dbo.stockprices (Symbol, TradingDate. ClosingPrice) VALUES CORCL'.'20001009'. 35.625) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL' . '20001016'. 35.250) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL'.'20001023', 34.188) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL'.'20001030', 30.313) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL','20001106', 25.438) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL','20001113'. 28.813) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) VALUES CORCL'.'20001120', 24.125) INSERT dbo.stockprices (Symbol, TradingDate. ClosingPrice) VALUES CORCL','20001127'. 26.438) INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) VALUES CORCL','20001204', 30.063) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) -: '■ VALUES CORCL','20001211', 28.563) INSERT dbo.stockprices (Symbol. TradingDate. ClosingPrice) : VALUES CORCL'.'20001218'. 31.875) INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) ■ VALUES CORCL','20001226', 29.063) « .■■■■, INSERT dbo.stockprices (Symbol, TradingDate, ClosingPrice) • VALUES CORCL'.'20010102', 30.125) INSERT dbo.stockprices (Symbol. TradingDate, ClosingPrice) VALUES CORCL','20010108'. 32.313) продолжение #
278 Глава 10. Пользовательские функции Листинг 10.15 {продолжение) DROP FUNCTION dbo.StockPriсеП uctuation GO CREATE FUNCTION StockPriceFluctuation(@Symbol varchar(lO). @StartDate smalldatetime='19900101'. PEndDate smalldatetime='201O0101') RETURNS TABLE AS RETURN( SELECT v.Symbol, StartDate=C0NVERT(char(8).v.TradingDate,112). EndDate=C0NVERT(char(8). a.TradingDate,112), Starti ngPri ce=v.CIosi ngPri ce, EndingPrice=a.ClosingPrice, Change=SUBSTRINGC- +'. CAST(SIGN(a.ClosingPrice-v.ClosingPrice)+2 AS int),l)+CAST(ABS(a.ClosingPrice-v.ClosingPrice) AS varchar) FROM (SELECT Symbol. TradingDate. ClosingPrice, ranking=(SELECT COUNT(DISTINCT TradingDate) FROM dbo.stockprices u WHERE u.TradingDate <= 1.TradingDate) FROM dbo.stockprices 1) v LEFT OUTER JOIN (SELECT Symbol, TradingDate, ClosingPrice, ranking=(SELECT COUNT(DISTINCT TradingDate) FROM dbo.stockprices u WHERE u.TradingDate <= 1.TradingDate) FROM dbo.stockprices 1) a ON (a.ranking=v.ranking+l) WHERE v.Symbol = @Symbol AND a.Symbol = @Symbol AND a.TradingDate IS NOT NULL AND v.TradingDate BETWEEN @StartDate AND @EndDate AND a.TradingDate BETWEEN @StartDate AND @EndDate ) GO SELECT * FROM StockPriceFluctuation('MSFT'.DEFAULT.DEFAULT) ORDER BY StartDate GO DROP TABLE dbo.stockprices (Результаты) Symbol StartDate EndDate StartingPrice EndingPrice Change ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL 20000706 20000710 20000717 20000724 20000731 20000807 20000814 20000821 20000828 20000905 20000911 20000918 20000925 20001002 20000710 37.9380 20000717 38.0630 20000724 37.7190 20000731 36.1880 20000807 40.7810 20000814 40.5630 20000821 40.6560 20000828 42.3130 20000905 46.3130 20000911 43.2810 20000918 39.1560 20000925 40.3670 20001002 39.3750 20001009 33.8130 38.0630 37.7190 36.1880 40.7810 40.5630 40.6560 42.3130 46.3130 43.2810 39.1560 40.3670 39.3750 33.8130 35.6250 +0.1250 -0.3440 -1.5310 +4.5930 -0.2180 +0.0930 +1.6570 +4.0000 -3.0320 -4.1250 +1.2110 -0.9920 -5.5620 +1.8120
«Рецепты» UDF 279 ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL ORCL 20001009 20001016 20001023 20001030 20001106 20001113 20001120 20001127 20001204 20001211 20001218 20001226 20010102 20001016 35.6250 20001023 35.2500 20001030 34.1880 20001106 30.3130 20001113 25.4380 20001120 28.8130 20001127 24.1250 20001204 26.4380 20001211 30.0630 20001218 28.5630 20001226 31.8750 20010102 29.0630 20010108 30.1250 35.2500 34.1880 30.3130 25.4380 28.8130 24.1250 26.4380 30.0630 28.5630 31.8750 29.0630 30.1250 32.3130 -0.3750 -1.0620 -3.8750 -4.8750 +3.3750 -4.6880 +2.3130 +3.6250 -1.5000 +3.3120 -2.8120 +1.0620 +2.1880 Вы видите, что эта функция играет роль параметризованного представления, icai^yHKUHflSalesHistogram(). Подобно всем табличным функциям (inline и multi- statement), в ней можно суммировать результаты, возвращенные функцией, как если бы это была выборка из таблицы. SELECT SUM(CAST(Change AS decimalA0,2))) FROM StockPriceFluctuati on С ORCL'.DEFAULT, DEFAULT) ' ' ' !' (Результаты) -5.63 Таким образом, стоимость акций «Oracle Corporation» уменьшилась примерно на $5,63 за шестимесячный период с июня 2000 по январь 2001 года. У нас еще есть данные по Microsoft, давайте посмотрим на них. Листинг 10.16. Изменения стоимости акций Microsoft за вторую половину 2000 года SELECT * FROM StockPriceFluctuati on ('MSFT'.DEFAULT. DEFAULT) ■•' • ■ > , • ,i . ORDER BY StartDate . _. , (Результаты) Symbol StartDate EndDate StartingPrice EndingPrice Change •>. ■ MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT 20000706 20000710 20000717 20000724 20000731 20000807 20000814 20000821 20000828 20000905 20000911 20000918 20000925 20001002 20001009 20001016 20001023 20001030 20001106 20000710 82.0000 20000717 78.9380 20000724 72.3130 20000731 69.6880 20000807 69.1250 20000814 72.4380 20000821 71.0000 20000828 70.6250 20000905 70.1880 20000911 69.3130 20000918 64.1880 20000925 63.2500 20001002 60.3130 20001009 55.5630 20001016 53.7500 20001023 65.1880 20001030 67.6880 20001106 68.2500 20001113 67.3750 78.9380 72.3130 69.6880 69.1250 72.4380 71.0000 70.6250 70.1880 69.3130 64.1880 63.2500 60.3130 55.5630 53.7500 65.1880 67.6880 68.2500 67.3750 69.0630 -3.0620 -6.6250 -2.6250 -0.5630 +3.3130 -1.4380 -0.3750 -0.4370 -0.8750 -5.1250 -0.9380 -2.9370 -4.7500 -1.8130 +11.4380 +2.5000 +0.5620 -0.8750 +1.6880 продолжение £>■
280 Глава 10. Пользовательские функции Листинг 10.16 {продолжение) MSFT MSFT MSFT MSFT MSFT MSFT MSFT MSFT 20001113 20001120 20001127 20001204 20001211 20001218 20001226 20010102 20001120 69.0630 20001127 69.9380 20001204 56.6250 20001211 54.4380 20001218 49.1880 20001226 46.4380 20010102 43.3750 20010108 49.1250 69.9380 56.6250 54.4380 49.1880 46.4380 43.3750 49.1250 53.5000 +0.8750 -13.3130 -2.1870 -5.2500 -2.7500 -3.0630 +5.7500 +4.3750 И, поскольку имеются еженедельные изменения, давайте посмотрим суммарные изменения за шесть месяцев (так же, как для «Oracle»): SELECT SUMCCASTCChange AS decimalA0.2))) FROM StockPriceFluctuation('MSFT'.DEFAULT,DEFAULT) (Результаты) ' • ■ ■ , '';';i -28.52 Похоже, у Microsoft в конце 2000 года тоже был плохой период. К счастью, и те и другие акции под конец повысили свою стоимость. Анализ тенденций Кроме просмотра изменений за конкретный промежуток, нам часто требуется идентифицировать тенденции в данных, особенно для временных рядов. Функция StockPriceTrends() показывает, как можно идентифицировать тенденции в данных, представленных в виде рядов. Технически, тенденция — это последовательная область или подраздел данных, которой удовлетворяет некоторый предопределенный критерий. Например, мы можем искать члены распределения, которые имеют одинаковое абсолютное значение, или одинаковое значение относительно друг друга, или квалифицируются каким-либо другим способом. Идентификация этих областей помогает анализировать имеющиеся тенденции. StockPriceTrends() выделяет области, в которых курс акций растет неделя за неделей. В ней используется таблица StockPrice из предыдущего примера с добавленным identity-столбцом. Посмотрим код. Листинг 10.17. Можно использовать UDF для определения тенденций ^ CREATE FUNCTION StockPriceTrend(@Symbol varchar(lO), ' '.', @StartDate smalldatetime='19900101'. - ,,, @EndDate small datetime='20100101') -,„ RETURNS TABLE -.-j, AS -v RETURN( .;;! SELECT v.TradingDate, v.ClosingPrice ..•>.: FROM dbo.StockPrices v JOIN dbo.StockPrices a ON ((a.ClosingPrice >= v.ClosingPrice) AND (a.Sampleld = v.Sampleld+D) OR ((a.ClosingPrice <= v.ClosingPrice) AND (a.Sampleld = v.Sampleld-D) WHERE a.Symbol=@Symbol AND v.Symbol=@Symbol AND v.TradingDate BETWEEN @StartDate AND PEndDate - AND a.TradingDate BETWEEN PStartDate AND @EndDate GROUP BY v.TradingDate. v.ClosingPrice .? ) • . -.. .-. •' ';• GO SELECT * FROM dbo.StockPriceTrendt'MSFT'.DEFAULT,DEFAULT) ' ;.
«Рецепты» UDF 281 ORDER BY TradingDate (Результаты) TradingDate ClosingPrice 2000-07-31 00 2000-08-07 00 2000-10-09 00 2000-10-16 00 2000-10-23 00 2000-10-30 00 2000-11-06 00 2000-11-13 00 2000-11-20 00 2000-12-26 00 2001-01-02 00 2001-01-08 00 69.1250 72.4380 53.7500 65.1880 67.6880 68.2500 67.3750 69.0630 69.9380 43.3750 49.1250 53.5000 Если посмотреть на исходные данные, то можно заметить, что все данные из этой выборки принадлежат к периодам, когда курс акций возрастал от недели к неделе. Можно вывести продолжительность тенденции на основании непрерывной последовательности недель, присутствующих в этих результатах. Например, первая тенденция увеличения стоимости акций начинается 31 июля и продолжается до 7 августа. Потом «просвет» примерно два месяца, затем следующая тенденция увеличения с 10 октября по 20 ноября. Помещение кода в UDF делает идентификацию тенденций столь же простой, как и выборка из таблицы. Линейная аппроксимация методом наименьших квадратов Когда отношение между двумя переменными приблизительно линейное, они могут быть обобщены прямой линией. Метод статистического моделирования для установления таких отношений называется аппроксимация методом наименьших квадратов. Это то, что имеют в виду многие, когда говорят о наименьших квадратах или линейной аппроксимации по отношению к своим моделям данных. Аппроксимация методом наименьших квадратов позволяет нам по заданному ряду точек на двумерном графике нарисовать аппроксимирующую прямую линию. Это помогает установить отношения между Х- и Y-координатами на графике. Функция из следующего листинга — LSLR_StockPnces() — продолжает серию примеров с курсами акций и вычисляет наклон и пересечение с осью аппроксимирующей прямой для тех данных курса акций, с которыми мы работали. В этом примере порядковый номер недели в году является Х-координатой на графике, а курс акций — Y-координатой (хотя мы можем и расширить этот код для работы с любыми двумерными данными). Я выделил порядковый номер недели в отдельный вычисляемый столбец таблицы, чтобы немного упростить функцию (листинг 10.18). Листинг 10.18. UDF наименьших квадратов ' CREATE FUNCTION dbo.LSLR_StockPrices() RETURNS ©LSLR TABLE (SlopeCoefficient decimalC8.4).Slopelntercept decimalC8.3)) AS BEGIN DECLARE @MeanX decimal C8.4). "-■.." ' '. . @MeanY decimal C8.4), -^ ■ -"', ' ' " продолжением
282 Глава 10. Пользовательские функции Листинг 10.18 {продолжение) @Count decimal C8,4), @SlopeCoefficient decimal C8,4) DECLARE @WorkTable TABLE (x decimalC8.4). у decimalC8.4). XDeviation decimalC8.4). YDeviation decimalC8.4). CrossProduct decimalC8.4), XDevSquared decimalC8,4), YDevSquared decimalC8.4) ) -- Находим средние х и у и общее количество параметров SELECT @MeanX=AVG(TradingWeek), @MeanY=AVG(ClosingPrice), @Count=COUNT(*) FROM dbo.StockPrices -- Сохраняем отклонение каждой точки, -- произведение отклонений по осям -- и квадраты отклонений ' '■ INSERT @WorkTable SELECT . ... ■ ■ TradingWeek. .. , ClosingPrice, TradingWeek-@MeanX. ClosingPrice-@MeanY. • - (TradingWeek-@MeanX)*(ClosingPrice-@MeanY), POWER(TradingWeek-@MeanX. 2). POWER(ClosingPrice-@MeanY, 2) FROM dbo.StockPrices -- Вычисляем коэффициент наклона • SELECT @SlopeCoefficient = (№Count * SUM(CrossProduct)) - SUM(x) * SUM(y)) / (((Kount * SUM(XDevSquared)) - PDWER(SUM(x), 2)) FROM WJorkTable -- Заносим коэффициент наклона и пересечение -- с осью в возвращаемую таблицу INSERT @LSLR SELECT @S1opeCoefficient. (@MeanY - (@SlopeCoefficient * @MeanX)) ■ ' AS Slopelntercept -- (For clarity) ' " RETURN > END GO SELECT 'Slope-intercept equation is у - ' +CAST(SlopeCoefficient AS varcharA0))+'x + ' +CAST(SlopeIntercept AS varchar(lO)) FROM LSLR_StOCkPrices() (Результаты) Slope-intercept equation is у = 1.7903x + -2.811
«Рецепты» UDF 283 Мы получили картину отношений между координатами X (номер недели) и Y (курс акций) в форме y=mx+b. Используя эту формулу, мы можем предсказать будущее значение для Y (курса акций), если имеем X. Конечно, какой-либо значимой корреляции между неделей года и курсом акций нет. Прошедшее время никоим образом не может заставить курс акций падать или расти. При помощи формулы нельзя предсказать курсы акций, но тем не менее эта функция дает некое подобие инструмента анализа. Рекурсия Как для хранимых процедур и триггеров, Transact-SQL поддерживает рекурсию в пользовательских функциях. UDF могут вызывать себя самих, не беспокоясь об управлении стеком или повторных вызовах. В листинге 10.19 приведена простейшая рекурсивная функция, возвращающая факториал числа. Листинг 10.19. Пользовательские функции поддерживают рекурсию USE tempdb GO IF OBJECTJDCdbo.Factorial') IS NOT NULL DROP FUNCTION dbo.Factorial GO CREATE FUNCTION dbo.Factorial(@base_number decimalC8.0)) " t RETORNS decimal C8.0) AS ' ' BEGIN DECLARE @previous_number decimalC8.0). ^factorial decimalC8.0) i '' IF (№base_number>26) and (@@MAX_PRECISI0N<38)) OR (@base_number>32) RETURN(NULL) IF (@base_number<0) RETURN(NULL) IF (@base_number<2) SET @factorial=l -- Факториал 0 или 1 равен 1 ELSE BEGIN SET @previous_number=@base_number-l SET @factorial=dbo.Factorial(@previous_number) -- рекурсивный вызов IF (@factorial=-l) RETURN(NULL) -- Возникла ошибка, выходим SET @factorial=@factorial*@base_number IF №@ERROR<>0) RETURN(NULL) -- Возникла ошибка, выходим END RETURN(@factonal) END GO SELECT dbo.Factorial C2) AS Factorial Factorial 263130836933693530167218012160000000 ' " vb;' Обратите внимание на строку, выделенную жирным шрифтом. В этой строке происходит рекурсивный вызов функции. Facto rial () продолжает вызывать себя, пока вычисления не будут закончены.
284 Глава 10. Пользовательские функции Параметризованные пользовательские функции Одним из ограничений пользовательских функций является невозможность задать объекты, с которыми они работают, в виде параметров. Иными словами, если UDF рассчитана на работу с таблицей StockPrice, то ее не заставишь работать с другой таблицей — а это значит, что создавать обобщенные пользовательские функции нельзя. При помощи параметра нельзя передать имя таблицы в функцию. Если у нас 10 таблиц StockPriceO.. StockPrice9, то для работы с ними нам понадобится 10 функций LSLR_StockPrice(). Поскольку мы не можем вызывать хранимые процедуры из функций или использовать INSERT. . . EXEC с табличными переменными, то нет никакого способа обобщить UDF для работы с произвольной таблицей. Microsoft забаррикадировал этот путь, но оставил обходную тропу: расширенные хранимые процедуры. Их можно вызывать из функций. Здесь нам на помощь приходит хр_ехес, расширенная хранимая процедура, которая позволяет выполнять произвольный T-SQL-сце- нарий. Эту процедуру можно вызвать из функции. Она принимает три параметра: строку со сценарием; флаг, определяющий необходимость присоединиться к транзакции вызвавшего хр_ехес кода; и имя базы данных, в контексте которой нужно выполнить сценарий. Полное обсуждение процедуры хр_ехес можно найти в главе 20. А сейчас давайте посмотрим на методику, позволяющую обойти только что указанные недостатки пользовательских функций при помощи хр_ехес (листинг 10.20). Листинг 10.20. UDF, нарушающая все правила для функций USE tempdb GO CREATE TABLE dist (cl int) 90 INSERT dist VALUES A) INSERT dist VALUES B) INSERT dist VALUES B) INSERT dist VALUES C) INSERT dist VALUES C) INSERT dist VALUES D) INSERT dist VALUES E) INSERT dist VALUES (8) GO DROP FUNCTION MEDIAN GO CREATE FUNCTION dbo.MEDIAN(@Tablename sysname. @Colname sysname) RETURNS median Table (Median sql_variant) AS BEGIN DECLARE @funcsql varchar(8000) SET @funcsql=' CREATE FUNCTION dbo.MedianPrimO RETURNS PMedianTab Table(Median sqlj/ariant) AS BEGIN INSERT PMedianTab SELECT Median=AVG(cl) FROM ( SELECT MIN(cl) AS cl FROM (
«Рецепты» UDF 285 SELECT TOP 50 PERCENT '•HaColname*' AS cl FROM '-НЗТаЫ enamel ORDER BY cl DESC ) t . ;.'■.:■ UNION ALL , ,. . SELECT MAX(cl) FROM ( '" ' ' '' '" ""' SELECT TOP 50 PERCENT '+(acolname+' AS cl FROM '+@Tab1ename+' ORDER BY cl ) t ) M . RETURN • ->■-■ -■ !" ; """ '■'. -■--.-•■• - END . .. •••■' -4'; EXEC master..xp_exec 'DROP FUNCTION dbo.MEDIANPRIM'.'N'.'tempdb' \ ' " '","..,' EXEC master..xp_exec @funcsql.'N'.'tempdb' INSERT (Pmedi an SELECT * FROM MEDIANPRIMO ••■■ •■■■ • • ':- '"-^ : ■■"- RETURN ,,; ..,'.;>.• ■*. :■} END . . ,..., - v>,.. . , :, • ,. • GO " " SELECT * FROM MedianCdist'.'cl') ■ ;y • ■<■■'. - . ■•• . '•' ." ! GO .... .,.-.....••: '. \ DROP TABLE dist :" ' \ \ (Результаты) '"' > ■ ' .' ;.' -< ■.>;••'■• ■ ■ , ■ ;' Median '" .'■ - ' : ' ■ : " 1 3 В этом примере мы создали табличную функцию Median (), которая принимает два параметра: имя таблицы и имя столбца, для которых нужно посчитать медиану. Давайте посмотрим, как работает эта функция. Обратите внимание на большую varchar-переменную @f uncsql в начале функции. Что это? В ней хранится исходный текст функции MedianPrim(), которая создается внутри функции с помощью хр_ехес. Эта функция была создана по двум следующим причинам. 1. Она дает нам возможность выполнить INSERT... SELECT медианы в результирующую таблицу. В UDF не допускается использование INSERT... EXEC, но конструкция INSERT... SELECT разрешена. 2. Нет другого способа динамически указать имя таблицы и столбца внутри функции. С точки зрения производительности, способ вычисления медианы в функции MedianPrim() очень эффективен. Его можно применять для вычисления медианы очень больших множеств. Итак, функция Median() создает другую функцию MedianPrim() через хр_ехес, затем вызывает ее с помощью конструкции INSERT... SELECT и возвращает результат. Мы построили обе функции таким образом, чтобы они возвращали числовые значения, но, поскольку мы используем таблицы, нетрудно будет переписать функцию для возвращения множественных значений (например, если нам понадобятся векторные медианы). Мы обошли большинство ограничений функции и создали инструмент, позволяющий запускать из функции любое T-SQL выражение. Мы просто построили свою функцию по образу и подобию Median () и — вуаля! — золотой ключик у нас в кармане. Вы можете переписать большинство табличных функций из этой главы таким образом, чтобы они принимали название таблицы и столбца в качестве параметров. Из подобной гибкости можно извлечь немало выгоды. Например, функции
286 Глава 10. Пользовательские функции StockPriceFluctuation() и StockPriceTrend() сильно «повзрослеют», если им разрешат ссылаться на другие объекты. Перед тем как вы начнете переделывать функции наподобие Median (), советую ознакомиться со следующими противопоказаниями. 1. Как замечено в главе 20, хр_ехес обращается к серверу через ODBC-соединение. Используется системный DNS LocalServer, поэтому он должен быть прописан на SQL-Server. 2. Никогда не используйте Y во втором параметре, если вызываете хр_ехес из функции. Этот параметр определяет, нужно ли присоединяться к имеющейся транзакции. Не следует пытаться присоединять какие-либо действия к транзакциям, начатым в UDF, — иначе возможны рассогласования, и поэтому Microsoft настоятельно рекомендует этого не делать. 3. Запросы в хр_ехес не должны быть длиннее 8000 байт. Это не критичное ограничение, просто имейте его в виду. Причина элементарна: мы используем тип varchar для хранения запроса, а его длина ограничена 8000 байтами. 4. Если вы хотите выполнить запрос в контексте базы данных, не являющейся базой данных по умолчанию в DSN LocalServer, укажите ее третьим параметром в хр_ехес. Этот параметр имеет тип varcharA28). В нем не допускается использование типов Unicode таких, как: nvarchar и sysname. 5. По своей природе, эта методика не предназначена для повторного использования в многопользовательской или многопотоковой среде. Действительно, если несколько пользователей одновременно создадут MedianPrim() для разных таблиц, — начнутся проблемы. Если SQL Server запустит несколько экземпляров Median () в разных потоках, мы также столкнемся с проблемами, потому что эти вызовы могут помешать друг другу. Можно запретить параллелизацию запроса с помощью хинта MAXD0P. И, конечно, нельзя использовать рекурсивные вызовы в этом типе функций. Если в MedianPrim() будет вызов MedianQ, то мы получим ошибку, а сервер не даст удалить функцию MedianPrim(). Позже на примере я покажу обходной путь решения проблем, связанных с мультиполь- зовательским использованием этого типа функций. Но, так как в функциях нельзя обращаться к временным объектам, цельного решения этой проблемы не существует. Поэтому лучше просто знать об этой возможности и использовать подобные функции только в исключительных случаях. Теперь, когда мы получили работающую параметризованную функцию, давайте испытаем ее на разных таблицах. SELECT * FROM Mediant'StockPrices'.'ClosingPrice') (Результаты) Median 67.531500 Это таблица StockPrice, которую мы использовали в предыдущих примерах. Мы определяем имя таблицы и столбца для поиска медианы — функция делает все остальное. Что произойдет, если нам понадобится отфильтровать данные, которые мы хотим обработать? Сейчас функция принимает имя таблицы для обработки. Что, если нам требуется отфильтровать или сгруппировать данные в этой таблице до обра- (
«Рецепты» UDF 287 ботки этих данных в UDF? Это очень просто: достаточно передать в функцию вместо названия таблицы вложенный запрос: SELECT * FROM Mediant' (SELECT * FROM StockPrices WHERE Symbol="MSFT") AS sp', 'ClosingPrice') Жирная строка в этом примере является вложенным запросом. Посмотрите на переменную @funcsql в функции Median(), и вы поймете, почему это работает. Поскольку мы создаем код функции MediaPrim() «на лету», то на место имени таблицы мы спокойно можем поместить вложенный подзапрос. Можно использовать этот прием для фильтрации или группировки данных, чтобы UDF ясно видела, что мы от нее хотим. Как я замечал ранее, параметризованные функции подходят исключительно для однопользовательского применения. Основная причина этого ограничения состоит в том, что мы удаляем и пересоздаем постоянный объект во время исполнения функции и не можем использовать уникальные имена объектов или временные таблицы, потому что функции не позволяют этого. При каждом своем вызове функ- ыция Median () удаляет и создает один и тот же объект: функцию MedianPrim(). Значит, при одновременном вызове этой функции мы можем получить конфликт. Что же теперь делать? Для одновременного выполнения этой функции нет очевидных решений, но все же я подам вам идею. Вовремя формирования кода создания функции Median Prim () владельцем функции может стать текущий пользователь, а затем следует опустить имя пользователя в конструкции INSERT.,, SELECT в функции Median(). Если мы вошли в систему как JoeUser и не являемся членом роли dbo, функция MedianPrim() будет создана как JoeUse г. Median P r im () и, пока никто больше не войдет в систему под этим логином, о многопользовательской проблеме можно забыть. Поскольку явное указание пользователя не используется, SQL Server сначала ищет функцию MedianPrim() с владельцем JoeUser, которую успешно находит и выполняет. Вы поняли, что это решение требует, чтобы все пользователи имели свой логин и не являлись членами роли db_owners (для них USER_NAME() всегда dbo, и мы возвращаемся к конфликту имен). Также необходимо явно дать права всем пользователям на функцию Median() и на все таблицы, к которым эта функция будет обращаться. Посмотрим, что получилось. Листинг 10.21. Можно обойти мультипользовательские ограничения при помощи функции USER_NAME() SET NOCOUNT ON USE tempdb go / DROP TABLE dbo.StockPrices / GO / CREATE TABLE dbo.StockPrices (Symbol varcharD). TradingDate' smalldatetime, ClosingPrice decimal A0.4)) /* 1 Код сокращен \ */ INSERT dbo.StockPrices (Symbol, TradingDate. ClosingPrice) VALUES ('MSFT'.'20010108', 53.500) GO GRANT SELECT ON dbo.StockPrices TO public GO - ' -'■■'■ '<■' ""•'' DROP FUNCTION MEDIAN ,-..,.,.., GO ' ' """'■ "-'■■'" CREATE FUNCTION dbo.MEDIAN(@Tablename sysname. (PColname sysname) продолжение &
88 • Глава 10. Пользовательские функцт Листинг 10.21 {продолжение) < RETURNS ©Median Table (Median sql_variant) AS BEGIN DECLARE @funcsql varchar(8000). @cmd varcharB55) SET @funcsql=' CREATE FUNCTION '+USER_NAME()+'.MedianPrimO RETURNS @MedianTab Table(Median sql_variant) ■ ' AS " BEGIN -• ■ : INSERT PMedianTab , -y SELECT Median=AVG(cl) FROM ( SELECT MIN(cl) AS cl FROM ( ' SELECT TOP 50 PERCENT ' +@Colname+' AS cl FROM '+@Tablename+' ORDER BY cl DESC •'' ) t UNION ALL ... -! SELECT MAX(cl) FROM ( SELECT TOP 50 PERCENT '+@Colname+' AS cl FROM '+@Tablename+' ORDER BY cl ) t ) M nv RETURN END SET @cmd='DR0P FUNCTION '+USER_NAME()+'.MEDIANPRIM' EXEC master..xp_exec @cmd.'N'.'tempdb' , .,• EXEC master.,xp_exec @funcsql.'N','tempdb' INSERT йпесИап SELECT * FROM MEDIANPRIMO RETURN END GO - -г. GRANT SELECT ON dbo.Median TO public GO Этот код создает таблицу и функцию Median (), затем дает права на SELECT для таблицы и функции группе public. Обратите внимание на использование USERJIAMEQ в функции Median(). Для обычных пользователей (не dbo) оно обеспечивает некоторую изоляцию для одновременного запуска функций разными пользователями - некое собственное пространство имен. / Теперь все готово; пользователи могут вызывать параметризованную функцию для получения медианы. Запрос и для dbo и для обычных пользователей, будет таким: USE tempdb GO SELECT * FROM Mediant'dbo.TemperatureData'.'HiTemp') Итоги В этой главе мы узнали: ■ как работают пользовательские функции; ■ как создавать пользовательские функции и на что следует обращать особое внимание; ■ как создавать собственные системные функции; ■ как использовать расширенную хранимую процедуру хр_ехес, чтобы преодолеть ограничения, накладываемые на функции.
Часть 3 HTML, XML, .NET | 10 Зак 983
*| *| HTML s Люди часто спрашивают меня, что я думаю о закате так называемой новой экономики и о конце провозглашенного нового мирового порядка, когда бизнес делается в Интернете. Откровенно говоря, я этому рад. Я рад, что для достижения целей потребуется большая работа. Я рад, что для достижения долгосрочного благоденствия необходимо нечто большее, чем просто хорошая идея. Я рад, что вы, как и прежде, должны разумно вести свой бизнес, чтобы он имел продолжение. Это хорошо, И то, что спекулянты и торговцы змеиным ядом эпохи безумия dotcom утихомирились, является величайшим приобретением Сети со времени изобретения браузера. X. В. Кентом Эта глава посвящена не только HTML (Hypertext Markup Language, гипертекстовый язык разметки) как таковому, но и способам трансформации данных, хранящихся на SQL Server, в HTML. Об HTML написано много книг, и в этой главе будут рассмотрены только те аспекты HTML, которые связаны с SQL Server. Преобразования XML в HTML посредством таблицы стилей будут рассмотрены в главе 12. Истоки J Возникновение Web можно отнести ко времени, когда в женевской лаборатории физики элементарных частиц (CERN) Тим Бернерс-Ли (Tim Berners-Lee) разработал HTML как средство коммуникации членов физического сообщества. В декабре 1990 года HTML был опубликован в CERN для внутреннего использования и стал доступен для открытого использования летом 1991 года. В духе великой традиции Интернета — «делись и пользуйся» — CERN и Тим Бернерс-Ли представили спецификации HTML, HTTP (Hypertext Transfer Protocol, протокол передачи гипертекстовых файлов) и URL (Uniform Resource Locators, унифицированный указатель информационного ресурса). HTML был разработан Бернерсом-Ли на основе SGML (Standard Generalized Markup Language, стандартный обобщенный язык разметки). SGML, как и XML, является метаязыком и может быть использован для определения других языков. Любой определенный таким образом язык называется подмножеством языка SGML. HTML является подмножеством SGML,
Создание HTML посредством Transact-SQL 291 SGML появился в 1960-х годах в результате исследований, проводимых IBM в области обобщенного представления текстовых документов. В результате этих разработок появился GML {General Markup Language, обобщенный язык разметки), предшественник SGML. В 1978 году ANSI выпустил первую версию SGML, в 1985 году — проект первого стандарта, а в 1986 — первый стандарт SGML. Сотрудник CERN Андерс Берглунд (Anders Berglund) разработал SGML-систему, которая опубликовала первый стандарт SGML. Закономерно, что CERN стал организацией, которая впоследствии дала миру HTML и Web. Министерство обороны США, многие другие правительственные организации и большое количество промышленных корпораций выбрали SGML в качестве стандарта для документов. Несмотря на то что SGML сложен и труден в работе, его гибкость привлекла внимание промышленных корпораций, в которых работа с документами отличается сложностью и многообразием. Я присоединяюсь к общему мнению о том, что SGML просто слишком сложен для успешной работы большинства людей, и поэтому он не получил широкого распространения. HTML, напротив, очень прост и даже незамысловат. К сожалению, для многих целей он слишком прост. Создание HTML посредством Transact-SQL Вследствие простоты HTML, формирование HTML-документов — не слишком сложный процесс. Поэтому по сей день многие HTML-кодировщики предпочитают всеохватным средствам вроде Frontpage простые, наподобие Notepad. HTML легко программировать и генерировать. Приложив очень небольшие усилия, вы можете сформировать HTML посредством Transact-SQL. Таблицы ^ Таблицы представляют собой естественный способкотображения реляционных данных. Поскольку HTML поддерживает отображение табличных данных, имеет смысл рассмотреть формирование HTML-таблиц, на основе данных SQL Server. Для разметки таблиц HTML использует пару тегов <ТАВ1_Е>, а для разметки каждого ряда таблицы — пару <TR>. Каждый элемент данных внутри таблицы размещается внутри пары тегов <TD>. При условии, что эти теги и заключенные внутри них данные представляют собой простой текст, вы можете без труда сформировать HTML-таблицу, используя простой запрос Transact-SQL. Листинг 11.1 иллюстрирует один из путей решения этой задачи. Листинг 11.1. Код Transact-SQL для формирования HTML SET N0C0UNT ON USE Northwind - ■'■;•■ : GO - ■-■" ■,.'■• SELECT ■" " "•" ' ' ' '<TABLE B0RDER='T'>'."."," .-■'■' :"••- UNION ALL " '"" " , SELECT TOP 10 '<TR>'.'<TD>'+CompanyName+'</TD>'.,<TD>'+CustomerId+'</TO>'.'</TR>' FROM customers ..-....;,. ,-...;-. ..„.. - „.......,,. продолжением
292 Глава И. HTML Листинг 11.1 {продолжение) UNION ALL . . ; SELECT '</TABLE>\''."." (Результаты) . ■ ■ , • <TABLE BORDER»"> <TR> <TD>Alfreds Futterkiste</TD> <TR> <TD>Ana Trujillo Emparedados у helados</TD> <TR> <TD>Antonio Moreno TaquerHa</TD> <TR> <TD>Around the Horn</TD> <TR> <TD>Berglunds snabbkup</TD> <TR> <TD>Blauer See Delikatessen</TD> <TR> <TD>Blondesddsl риге et fils</TD> <TR> <TD>Bylido Comidas preparadas</TD> • .' <TR> <TD>Bon app'</TD> <TR> <TD>Bottom-Dollar Markets</TD> </TABLE> <TD>ALFKI</TD> <TD>ANATR</TD> <TD>ANTON</TD> <TD>AROUT</TD> <TD>BERGS</TD> <TD>BLAUS</TD> <TD>BLONP</TD> <TD>BOLID</TD> <TD>BONAP</TD> <TD>BOTTM</TD> </TR> </TR> </TR> </TR> </TR> </TR> </TR> </TR> </TR> </TR> Как большинство таблиц, HTML-таблицы имеют три основных элемента: заголовок (header), тело (body) и нижнюю часть (footer). Приведенный запрос формирует все три элемента посредством оператора UNION. Первый элемент SELECT эздает заголовок — обязательный тег <TABLE>, внутри которого содержится атрибут, определяющий ширину линии сетки таблицы. Второй элемент SELECT создает тело таблицы, причем значение каждого столбца заключено в теги <TD>, а каждый ряд — в теги <TR>. Последний элемент SELECT создает закрывающий тег </TABLE>. Если вы выполните этот запрос из OSQL в режиме отключенного показа названий полей и без нумерации строк, результатом будет HTML-таблица, которая может быть отображена через браузер. На рис. 11.1 показано, как это должно выглядеть в браузере. Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno Taquerja Around the Horn Berglunds snabbk"p """ 4.1...11.» И 1..1.1 BJauerSee Delikatessen Blondesddsl pSre etfils B0iido Comidas preparadas Bon app1 Bottom-Dollar Markets ALFKI ANATR ANTON AROUT BERGS BLAUS BLONP BOLID BONAP BOTTM Рис. 11.1. Таблица, созданная при помощи Transact-SQL
Создание HTML посредством Transact-SQL 293 Учитывая тот факт, что сценарий не содержал никаких действий по формированию HTML, очевидно, что можно обобщить вызов OSQL, заменив его файлом . CMD. В листинге 11.2 приведен файл . CMD, который вы можете использовать для создания HTML и открытия HTML-документа, сформированного OSQL. Листинг 11.2. TSQL2HTML.CMD — командный файл, который формирует HTML из T-SQL (fecho off OSQL -E -h-1 -n -m.sql -o3U.html %2 n.html TSQL2HTML. CMD принимает два параметра: имя запускаемого сценария и необязательный параметр (например, для указания имени сервера Sservername). Если переданный вами сценарий сформирует корректный HTML, последний будет отображен сразу после записи содержимого в файл . HTML. Заголовки столбцов Возможно, вы заметили, что наши HTML-таблицы не содержат заголовки столбцов. Это легко исправить. Ниже приведен измененный код из предыдущего примера, включающий в себя заголовки столбцов. Листинг 11.3. Вариант запроса к таблице Customers, включающий заголовки таблиц SET «COUNT ON USE Northwind GO SELECT '<TABLE B0RDER=">'.'<TH>Company Name</TH>'.'<TH>Customer ID</TH>'." UNION ALL SELECT TOP 10 '<TR>' ,'<TD>'+CompanyName+'</TD>'.,<TD>'+CustomerId+,</TD>'.'</TR>' FROM customers UNION ALL SELECT '</TABLE>'. '■."." (Результаты) <TABLE B0RDER="> <TH>Company Name</TH> <TH>Customer ID</TH> <TR> <TD>Alfreds Futterkiste</TD> <TD>ALFKI</TD> </TR> <TR> <TD>Ana Trujillo Emparedados у he1ados</TD><TD>ANATR</TD> </TR> <TR> <TD>Antonio Moreno TaquerHa</TD> <TD>ANTON</TD> </TR> <TR> <TD>Around the Horn</TD> <TD>AROUT</TD> </TR> <TR> <TD>Berglunds snabbkup</TD> <TD>BERGS</TD> </TR> <TR> <TD>Blauer See Delikatessen</TD><TD>BLAUS</TD> </TR> <TR> ' <TD>Blondesddsl риге et fils</TD><TD>BLONP</TD> </TR> <TR> <TD>Bylido Comidas preparadas</TD><TD>BOLID</TD> </TR> <TR> <TD>Bon app'</TD> <TD>BONAP</TD> </TR> <TR> <TD>Bottom-Dollar Markets</TD> <TD>BOTTM</TD> </TR> Единственным изменением было добавление тега <ТН> и текста заголовка столбца к первой команде SELECT, что привело к появлению первого ряда в результатах работы запроса. Тег HTML <TH> определяет заголовки столбцов. Если вы сохраните результирующий набор данных в HTML-файле и откроете его в браузере, вы увидите примерно то, что показано на рис. 11.2.
294 Глава 11. HTML Разумеется, с помощью этого приема вы можете получить не только простую HTML-таблицу. Вы можете сформировать любой корректный HTML-документ и использовать механизм запросов SQL Server для получения данных, что мы и сделали. j Company Name Alfreds Futterkiste Ana Trujillo Emparedados у helados! Antonio Moreno Taquerja (Around the Horn Berglunds snabbk"p Blauer See DeJikatessen Blondesddsl pSreetfils B^lido Comidas preparadas Bon app' Customer ID ALFKI ANATR ANTON A ROUT BERGS В LA US BLONP В OLID BONAP Bottom^Dollar Markets (вОТТМ Рис. 11.2. Таблица с заголовками столбцов Формирование HTML при помощи sp_ma kewebtask До сих пор мы занимались исследованием старомодного способа формирования HTML — при помощи Transact-SQL и OSQL. Однако SQL Server имеет в своем распоряжении более мощное средство преобразования данных из базы данных в HTML: sp_makewebtask. Процедура sp_makewebtask представляет собой развитый механизм формирования HTML, который можно использовать для создания задач, формирующих веб-страницы на основании данных SQL Server. Одна из сложностей написания книг по SQL Server состоит в том, что автор должен избежать повторения написанного в Books Online, при этом всесторонне описать предмет и убедить читателя, что деньги на книгу были потрачены не зря. Онлайновая справочная система до сих пор является одной из сильных сторон SQL Server. Она более подробна, связана и полезна, чем онлайновая документация других СУБД, с которыми я регулярно работаю. Поэтому мне не хотелось бы повторять на иной лад то, что и так хорошо написано в Books Online. Лучше я потрачу отведенное мне количество страниц, обучая вас тому, что там вы не найдете. Итак, синтаксис и список параметров процедуры spjnakewebtask вы можете найти в Books Online. Я же объясню в общих словах принцип работы sp_makewebtask, затем приведу несколько примеров, которые помогут вам использовать ее самостоятельно.
Формирование HTML при помощи sp_makewebtask 295 Spjnakewebtask представляет собой хранимую процедуру, которая создает задание SQL Server и формирует HTML на основе данных, хранящихся в базе данных SQL Server. Можно запускать это задание по расписанию, либо непосредственно, либо тем и другим способом. Sp_ runwebtask можно использовать для ручного вызова запланированных задач. Существует свыше тридцати параметров, которые вы можете передать sp_makewebtask. Два наиболее важных: @query и @outputf i I е. Параметр @query представляет собой запрос для получения данных, которые вы хотите отобразить, а параметр @outputf i I e определяет имя файла, в который будет сохранен вновь сгенерированный HTML. Ниже приведен пример вызова sp_makewebtask, который формирует ту же таблицу, которую мы построили ранее, используя простой Transact-SQL. Листинг 11.4. Процедура sp_makewebtask создает таблицу при помощи всего одной строки кода EXEC spjiakewebtask @outputfile = 'C:\temp\cust_table.HTML'. @query='SELECT CompanyName, CustomerlD FROM Northwind..Customers ORDER BY CompanyName' , @lastupdated=0.@resultstitle=' ' (Результаты сокращены) ^^Customers CompanyName Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno TaquerAa Around the Horn Berglunds snabbkAJp Blauer See Delikatessen VaiTeljernet Vietuailles en stock Vins et aJcools Chevalier Wartian Herkku Wellington lmportadora White Clover Markets Wilman KaJa j Wolski Zajazd CtistomerlD ALFKl iANATR | __™ AROUT | BERGS \ В LA US ! VAFFE \ V1CTE \ VINET | _,„ -^ WELU WH1TC ! W1LMK | WOLZA \ Дополнительно к упомянутым параметрам @query и @outputf i le, мы также использовали параметры @ I astupdated и @resultst itle. Параметр § I astupdated принимает значение 1 или 0 в зависимости от того, хотите ли вы включать в веб-страницу строку Last Updated. Мы отключили ее отображение, присвоив параметру значение 0. Параметр @resu I tst itle определяет заголовок веб-страницы. Мы пе-
296 Глава 11. HTML редали в этот параметр два пробела для отключения отображения названия результатов. Значение по умолчанию — Query Resu I ts. Один из параметров, который мы не определили, — параметр @whentype. Он принимает целочисленное значение, которое определяет, когда будет создана веб-страница. Страница может быть создана немедленно (по умолчанию), по запросу, по расписанию или согласно некоторой комбинации этих трех условий. Не указав явно значение этого параметра, мы оставили значение по умолчанию 1, которое определяет немедленное создание страницы. \ Гиперссылки Л Spjnakewebtask может также включать в создаваемые страницы гиперссылки. Вы можете непосредственно определить одиночный URL или обеспечить запрос, который будет возвращать список ссылок. Ниже приведен пример, который добавляет в конец сформированного файла одиночный URL. Листинг 11.5. Процедура spjnakewebtask может включать гиперссылки в создаваемую веб-страницу EXEC spjnakewebtask @outputfile = 'C:\temp\cust_table.HTML'. @query='SELECT CompanyName. CustomerlD FROM Northwind..Customers ORDER BY CompanyName'. @lastupdated=0, @resultstitle=' ', ~~ ^ @URL='http://www.khen.com' ,@reftext='Ken Henderson^ Home Page' GO (Результаты сокращены) ... CompanyName Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno Taqueria Around the Horn Berglunds snabbkop Blauer See Delikatessen Vaffeljernet Victuailles en stock Vins et alcools Chevalier Wartian Herkku Wellington Importadora White Clover Markets Wilman Kala - - Wolski Zajazd CustomerlD ALFKI ANATR ANTON AROUT BERGS BLAUS VAFFE VICTE VINET WARTH WELLI WHITC WILMK WOLZA Ken Henderson's Home Page Обратите внимание на гиперссылку в конце страницы. Sp_makewebtask может также получать список URL на основании предоставленного вами запроса. Этот запрос должен обеспечить как сам URL, так и текст для отображения каждой гиперссылки, которую вы хотите включить в результат. Пример приведен в листинге 11.6.
Формирование HTML при помощи spjnakewebtask 297 Листинг 11.6. Можно создать таблицу гиперссылок для ее использования процедурой sp_makewebtask CREATE TABLE WebSites(URL varchar(lOO). URL_text varchar(lOO) NULL) GO INSERT WebSites VALUES ('http://vMM.awl .com'. 'Addison-Wesley Longman') INSERT WebSites VALUES ('http://www.khen.com','Ken Henderson''s Home Page') GO EXEC spjnakewebtask @outputfile = 'C:\temp\cust_table.HTML'. @query='SELECT CompanyName, CustomerTD FROM Northwind..Customers ORDER BY CompanyName' (?lastupdated=0, @resultstitle=' ', №able_uris = 1, @url_query= 'SELECT URL, URL_text FROM WebSites' GO (Результаты сокращены) ^ CompanyName CustomerlD Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno Taqueria Around the Horn Berglunds snabbkop Blauer See Delikatessen Vaffeljernet Victuailles en stock Vins et alcools Chevalier Wartian Herkku ■•■ Wellington Importadora White Clover Markets Wilman Kala Wolski Zajazd ALFKI ANATR ANTON AROUT \ BERGS \ BLAUS \ VAFFE jVICTE VINET WARTH WELLI WHITC WILMK WOLZA Addison-Wesley Ken Henderson's Home Page Обратите внимание на гиперссылки внизу страницы. Они были добавлены благодаря использованию двух ключевых параметров: @tabl e_u г I s и @u г I _q ue гу. @tabl e_u г I s сообщает spjnakewebtask, что необходимо сформировать список URL, используя запрос, указанный в @url_query. Обратите внимание, что пары параметров @URL, @ref text и @tab I e_u г I s, @u г I I _queгу взаимно исключают друг друга. Вы можете определить либо одну пару, либо другую, но не обе вместе. Шаблоны Большая часть работы sp_makewebtask может быть сконфигурирована с помощью параметров. Разумеется, это хорошо, но гораздо более мощным и гибким подходом было бы использование шаблонов HTML. Sp_makewebtask поддерживает использование шаблонов HTML с заполнителями, указывающими, куда должны быть вставлены данные. Это дает полный контроль над размещением и форматированием данных и позволяет создавать сложные, управляемые данными веб-страницы. Ниже приведен пример такого шаблона HTML.
298 Глава И. HTML Листинг 11.7. Пример простого шаблона HTML г. <HTML> <HEAD> <TITLE>An HTML template that displays Customer data</TITLE> •? - /' <B0DY> /•■,: <Hl>Customers</Hl> <P> <$insert_data_here£> <P> «^_ <A HREF = "http://www.awl.com">Addison-Wesley</A><P> <A HREF - "http://www.khen.com">Ken Henderson's Home Page</A><P> •,J </B00Y> </HTML> В этом примере ключевую роль играет заполнитель <%insert_data_here%>. Он определяет, куда процедура sp_makewebtask вставляет данные, возвращаемые запросом, переданным в переменной @query. Ниже приведен запрос, демонстрирующий использование шаблона. Листинг 11.8. Вывод значений из таблицы Customers при помощи шаблона / ■■-. v EXEC spjnakewebtask @outputfile = 'C:\temp\cust_table.HTML', / @query='SELECT CompanyName, CustomerlD FROM Northwind..Customers ORDER BY CompanyName'. @templatefi1 е-'с:\temp\cust_table.htp', @lastupdated=0. @resultstitle=' ' (Результаты сокращены) Customers CompanyName Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno Taqueria Around the Horn Berglunds snabbkop Blauer See Delikatessen Vaffeljernet Victuailles en stock Vins et alcools Chevalier Wartian Herkku Wellington Importadora White Clover Markets Wilman Kala Wolski Zajazd CustomerlD ALFKI ANATR ANTON AROUT BERGS BLAUS VAFFE VICTE VINET WARTH WELLI WHITC WILMK WOLZA
Формирование HTML при помощи sp_makewebtask 299 Addison-Wesley Ken Henderson's Home Page Хотя возможность полного контроля кода HTML, который предшествует результатам выполнения запроса и следует за ним, является значительным достижением для формирования гибких, сложных и профессионально выполненных вебстраниц, вам также необходимо управлять форматированием самого результирующего набора данных. Для этой задачи также подходит использование шаблонов. Вместо того чтобы просто вставить полученный набор данных как монолитную часть, вы можете определить форматирование для каждого столбца набора данных. В листинге 11.9 представлен шаблон, выполняющий эту задачу. Листинг 11.9. Пример более сложного шаблона с форматированием рядов и столбцов <HTML> \ <HEAD> <TITLE>An HTML template that displays Customer data</TITLE> <B0DY> <Hl>Customers</Hl> <P> \ <TABLE B0RDER> <TR> <TH><B>Company Name</B></TH><TH><B>Customer ID</B></TH></TR> J <tt>egindetaiU> . / <TR><TD><Xinsert_data_here«></TD><TD><I><3;insert_data_here3;></I></TO></TR> <&enddetaiU> </TABLE> <P> <A HREF = "http://www.awl,com">Addison-Wesley</A><P> <A HREF = "http://www.khen.com">Ken Henderson's Home Page</A><P> </B0DY> </HTML> Обратите внимание на использование заполнителей <%beg i ndeta i I %> и <%enddeta 11 %>. Они «оборачивают» тело таблицы, сгенерированной процедурой spjnakewebtask, и именно они обеспечивают возможность управления форматированием рядов таблицы. Также обратите внимание на использование тега < I > для форматирования второго столбца курсивом. Используя стандартные теги HTML, вы можете выделять столбцы полужирным шрифтом, подчеркиванием, изменять отображения шрифта или цвета и т. д. Ранее использованный нами заполнитель <% i nse rt_date_he re%> определяет, куда будут помещены данные каждого столбца (столбцы вставляются в том порядке, в котором они возвращаются запросом). В листинге 11.10 приведен запрос, использующий вышеприведенный шаблон и полученную веб-страницу. Листинг 11.10. Таблица, сформированная пользовательским шаблоном EXEC spjnakewebtask @outputfile = 'C:\temp\cust_table.HTML'. @query='SELECT CompanyName, CustomerlD FROM Northwind..Customers ORDER BY CompanyName', (tempi atefi 1 e='с:\temp\cust_tabl e2.htp', (?lastupdated=0, @resultstitle=' ' (Результаты сокращены)
300 Глава 11. HTML Customers Company Name Customer ID Alfreds Futterkiste Ana Trujillo Emparedados у helados Antonio Moreno Taqueria Around the Horn Berglunds snabbkop Vaffeljernet Victuailles en stock Vins et alcools Chevalier Wartian Herkku Wellington Importadora White Clover Markets Wilman Kala Wolski Zajazd Addison-Wesley Ken Henderson's Home Page Используя шаблоны и возможности HTML для управления отображением и форматированием текста, мы можем создавать сложные страницы, используя небольшой объем кода или вообще обходясь без кодирования. Более того, стандартизировав при помощи шаблонов создание веб-страниц, вы придадите вашему вебсайту единый стиль и дизайн и можете воспользоваться более продвинутыми технологиями, такими как: таблицы каскадных стилей, DHTML и внедренные эле-/ менты управления. Итоги В этой главе вы узнали: ■ как создавать HTML-документы (веб-страницы) при помощи веб-инструментария SQL Server; ■ как создавать HTML, используя простой Transact-SQL; « ■ как использовать мощную и гибкую процедуру sp_makewebtask. ALFKI ANATR ANTON AROUT BERGS VAFFE VICTE VINET WARTH WELLI WHITC WILMK WOLZA
Введение в XML Кто не применяет новых средств, должен ждать новых бед. Френсис Бэкон1 Я включил вводную главу об XML в эту книгу по двум причинам. Во-первых, поскольку теперь XML является ключевым компонентом SQL Server, полагаю, он заслуживает описания в книгах, посвященных SQL Server. Во-вторых, многие программисты, пишущие на Transact-SQL, не знакомы с языком XML. В то время как знание XML является основополагающим не только для понимания дальнейших глав этой книги (касающихся XML), но и для понимания настоящего и будущего SQL Server. К сожалению, у меня нет возможности привести исчерпывающее описание XML. Этому коду и семейству технологий, основанных на XML, посвящены уже целые книги. Я постараюсь рассказать о главном, что следует знать о XML для функционального применения SQL Server, связанного с XML. Таким образом, вам, вероятно, потребуется дополнить полученную на страницах этой книги информацию собственными исследованиями. Рекомендованные источники перечислены в разделе «Рекомендуемая литература» далее в этой главе. Деревянные монеты Первое, что следует знать об XML, — язык XML не сложен. Изучить его не сложно. XML представляет собой программируемый иерархический текстовый формат. Он не может излечить от рака и выгулять вашу собаку. Это просто очень мощная технология, которая используется для совместной работы приложений и обмена данными. Повторяю еще раз: язык XML и его синтаксис не сложны, — в этом вы вскоре сможете убедиться сами. XML постоянно развивается: его создатели не могут оставить его в покое. Помнится, французский режиссер Марсель Пагнол (Marcel Pagnol) однажды сказал: «За инженерами следует присматривать: они начинают с создания швейной машины, а заканчивают атомной бомбой». Расширяемость XML привела к ошеломительному числу инициатив, основанных на XML. Создается впечатление, что каждую неделю публикуется новая спецификация или рекомендация W3C. Стоит вам подумать, что вы освоили технологию XML, — появляется новое применение XML, Bacon, Francis. The Essays. New York: Penguin, 1985. C. 132. 12
302 Глава 12. Введение в XML решающее новую проблему, с которой вы еще не сталкивались; либо решает известную вам проблему, но другим способом. Как быть в курсе всего этого? Как найти время для изучения всех технологий XML, когда все, что вам требуется, — это совместно использовать данные между базой данных SQL Server вашей компании и UNIX-системой отслеживания заказов вашего поставщика? Мой совет: расслабьтесь. Нет необходимости знать каждое XML-приложение или технологию для того, чтобы использовать XML по основному назначению. А основное назначение XML — обеспечить открытый обмен данными между платформами и приложениями. Понимание назначения XML и его синтаксиса (что несложно) — все, что требуется для продуктивного применения XML. Все технологии, построенные на основе XML и называемые XML-приложениями, сами используют XML. Таким образом, если вы можете прочесть XML, вы сможете прочесть и таблицу стилей XSLT, даже если вы никогда ее раньше не видели и не представляете, в каких целях она может быть использована. Безусловно, чтение - это одно, а понимание — другое. Способность читать текст на английском языке не означает возможность понять книгу по физике, написанную по-английски. Так и здесь: большинство ключевых составляющих семейства XML-технологий легки для понимания несмотря на то, что они могут выполнять весьма сложные задачи. Мой второй совет: не позволяйте никому продавать вам деревянные монеты (как говорил мне мой дед, когда я покидал его дом). В золотой лихорадке, которая окружила XML со времени его появления, появилось много «экспертов», предлагающих свои услуги доверчивым пользователям. Что эти люди делали до того, как стали «экспертами» в XML, остается только гадать, но часто случалось так, что у них был совсем небольшой опыт (или вовсе не было) работы с XML. Будьте внимательны в этом отношении. Интерес к технологии и квалификация — не одно и то же. Энтузиазм — хорошо, опыт — лучше. Я увлекаюсь NBA, но это не означает, что я могу быть тренером. Если человек говорит, что является экспертом в какой- либо технологии (XML или другой), но ничего не создал с ее помощью, — это звучит подозрительно. Невозможно освоить XML, лишь изучая рекомендации W3C или читая MSDN. Безусловно, важно изучать технологию для улучшения своих знаний. Поэтому вы и читаете эту книгу. Читайте больше. Не останавливайтесь на рекомендациях W3C и MSDN. Читайте все, что попадает к вам в руки и касается XML. Это только увеличит объем ваших знаний о XML. Однако, чтобы овладеть любой технологией, следует ее применять. Я не имею в виду создание игрушечных приложений или работу с примерами. Для того чтобы знать что-либо очень хорошо, необходимо создавать настоящие полезные вещи, используя свои знания. Эта ситуация напоминает мне ранние времена Java, когда эксперты по этому языку были на каждом углу. Знание Java считалось престижным. Язык Java был новинкой. Самоучители по Java продавались лучше книг Стивена Кинга, и о Java писали больше, чем о любом другом языке программирования когда-либо. Создавались группы пользователей, проводились семинары, писались книги —Java покорял мир. Но была одна проблема. Большинство людей, восхвалявших достоинства Java, — от создания безопасных программ до возвращения динозавров — сами никогда не использовали Java для создания чего-либо значимого. Язык Java был слишком молод. Как можно быть экспертом того, что появилось только вчера? С тех пор общий уровень знаний в сообществе Java существенно вырос. Настоящие приложения на Java создаются по всему миру, и опыту работы с Java не-
XML: обзор 303 сколько лет. Теперь мы действительно знаем, что можно сделать при помощи Java, а что — нельзя; что Java делает хорошо, а что — не очень. Да и сам язык Java претерпел изменения. Сейчас существует сообщество экспертов по Java, и оно состоит совсем не из тех людей, которые предлагали свои услуги на каждом углу пять лет назад. Это, как правило, ветераны других языков программирования. Они, посмотрев на Java, увидели огромный потенциал этого языка. Они не стали изучать Java в момент его появления (поскольку не стоит принимать технологию только потому, что она является последней программной причудой). Они подождали, дали устояться стандартам и только потом приступили к изучению Java, чтобы определить, можно ли при помощи этого языка лучше и легче решать проблемы, которые годами решались посредством таких языков программирования, как C++. Многие убедились, что Java может улучшить создание приложений. И это действительно стало революционным шагом вперед. Так и должно происходить принятие технологии: не из причуды или желании разбогатеть. Разумное принятие технологии основано на опыте, данных и аргументах, свидетельствующих о том, что рассматриваемая технология лучше применяемой в настоящее время — и выгода от нее будет больше затрат. Итак, мой вывод: опасайтесь обманщиков. Если кто-то говорит, что XML очень сложен (и, следовательно, вам необходима дорогостоящая консультация, чтобы понять XML), — бегите от таких людей. Если они заявляют, что они — эксперты, но без рекомендаций — хлопайте дверью. Помните: энтузиазм хорошо, опыт — лучше. В мире есть два вида людей: те, кто стремится к достижениям, но не желает тратить на них время, и те, кто любит свое ремесло и усердно работает каждый день, оттачивая мастерство. XML: обзор Благодаря World Wide Web HTML завоевал мир. Однако, несмотря на распространенность и популярность, у HTML всегда было множество серьезных ограничений. Чтобы столкнуться с некоторыми из них, вовсе не обязательно иметь большой опыт создания веб-приложений. HTML подходит для форматирования неофициальных документов, но не для решения более сложных задач. Никогда не предполагалось, что HTML будет использоваться для описания структуры данных, но потребности бизнеса привели к этому. По сути, то, что HTML стал использоваться не в тех целях, для которых он предназначен, и выявило ряд его недостатков. Появилась потребность в более мощном языке разметки, ориентированном на данные, а не на отображение, — таком языке, который не знает, как форматировать данные, но может придать данным зависящее от контекста значение. XML является решением многих проблем, связанных с HTML и созданием расширяемых приложений. XML прост в изучении для любого, кто понимает HTML, и в то же время существенно мощнее HTML. XML — это больше, чем просто язык разметки. Это метаязык или язык, который может быть использован для определения новых языков. При помощи XML можно создать язык, ориентированный на нужды конкретного приложения или бизнеса, и использовать его для обмена данными с вашими поставщиками, торговыми партнерами, клиентами и всеми, кто способен работать с XML.
304 Глава 12. Введение в XML XML дополняет, а не заменяет HTML. Помимо предоставления способов форматирования данных, XML дает данным зависящее от контекста значение. Как только у данных появляется контекстуальное значение, отображение становится легкой задачей. Но отображение данных всего лишь одно из возможных действий с данными, имеющими контекст. Правильно разделив представление данных от их хранения и управления, можно получить практически бесконечное число способов применения и обмена данных. В этой главе мы познакомимся с историей языков разметки и историей появления XML. Мы рассмотрим, как данные представляются в HTML и как XML может улучшить это представление. Мы обсудим причины, по которым вам следует создавать свой собственный XML-диалект, и узнаем, как это сделать. Затем мы затронем основы нотации XML и посмотрим, как можно отобразить XML посредством преобразования в HTML при помощи таблиц стилей. Мы поговорим о проверке документов при помощи Определений типа документа (DTD) и XML Schema, а также обсудим нюансы каждого из этих способов. В конце главы мы познакомимся с Объектной моделью документов (D0M) и узнаем, как она может быть использована для работы с XML-документами как с объектами. HTML: простота не дается даром Язык HTML предназначен для форматирования документов. Он указывает элементы отображения: заголовки, шрифты и т. п. Язык HTML сильно ориентирован на представление и хорошо подходит для размещения данных при отображении. Но HTML не подходит для описания этих данных или предоставления доступа к ним. Дизайнеры сайтов обходят многочисленные недостатки HTML удивительно оригинальными способами. И все равно у HTML есть серьезные изъяны, делающие его малопригодным для создания сложных открытых информационных систем. Перечислю некоторые из этих недостатков. ■ HTML не расширяем. Каждый браузер поддерживает фиксированный набор тегов, и возможности добавить новые теги не существует. ■ Язык HTML ориентирован на форматирование данных. Несмотря на то, что HTML хорошо отображает данные, он не может придать данным контекст. Если изменится формат данных, которые считывает некая программа, то она, скорее всего, перестанет работать. ■ Созданный однажды HTML статичен и его нелегко обновить. DHTML и другие технологии помогают смягчить этот аспект, но HTML никогда не был предназначен для работы с меняющимися данными. ■ HTML дает только одно представление данных. Из-за ориентации на отображение изменение представления данных является более сложным, чем следует. Опять же, такие технологии, как DHTML, в некоторой степени помогают, но все же необходим язык разметки, который обладает информацией о данных, которые описывает. ■ У HTM L мало семантики. Нет возможности для представления данных отличным от отображения способом. Как уже было сказано, сильная сторона HTML состоит в отображении данных, но иногда и для этого HTML подходит не очень хорошо.
Сравнение XML и HTML 305 Хотя язык SGML не имеет таких серьезных недостатков, его большая гибкость делает его чрезвычайно сложным. Язык DSSSL {Document Style Semantics and Specification Language), используемый для форматирования SGML, является мощным и гибким, но за это приходится платить чрезвычайной сложностью его применения. Так что нам требуется язык, подобный HTML в плане легкости использования и обладающий гибкостью SGML. XML: краткая история С бурным развитием Сети и большим количеством появившихся HTML-проектов пользователи столкнулись со многими ограничениями HTML. В то же время сторонники SGML, годами работавшие в относительной неизвестности, стали искать пути применения SGML в Интернете вместо одного его приложения (HTML). Они осознавали, что сам SGML слишком сложен и большинство пользователей не смогли бы или не стали его использовать, — им требовалась альтернатива. Им требовалось нечто, совмещающее в себе лучшие стороны HTML и SGML. В середине 1996 года Джон Босак (Jon Bosak) из компании Sun Microsystems обратился в консорциум W3C с предложением сформировать комитет по использованию SGML в Интернете. «Зеленый свет» инициативе дал Дэн Коннолли (Dan Connolly) из W3C, и работы были разделены между Босаком и людьми из других фирм, включая Тима Брэя (Tim Bray) из С. М. Sperberg-McQueen и Джин Паоли (Jean Paoli) из Microsoft (несмотря на то, что работы были организованы и управлялись Sun Microsystems). В ноябре 1996 года комитет обладал исходным описанием упрощенной формы SGML, которая не сложнее HTML в изучении и применении, но сохранила многие из лучших особенностей SGML. Это и было рождение XML. Сравнение XML и HTML Как я уже сказал, в XML можно создавать свои собственные теги. Поскольку это мощная и жизненно важная часть XML, она заслуживает отдельного обсуждения. Если вы привыкли работать с HTML, то эта концепция, вероятно, чужда вам, поскольку HTML не позволяет определять собственные теги. Несмотря на то что различные производители браузеров расширили HTML собственными тегами, в итоге на каком-то этапе вы будете вынуждены остановиться, так как придется использовать теги, которые поддерживает ваш браузер. Свои теги будет невозможно определить. Итак, как определить новый тег в XML? Простейший ответ: это делать необязательно. Просто используйте тег. Можно контролировать, какие теги допустимы в документе, при помощи документов DTD и XML Schema (мы обсудим это позже), но в итоге для определения тега можно просто использовать его в XML-документе. Конструкции наподобие typedef не предусмотрено. Для того чтобы сравнить иувидеть различия между представлением данных в HTML и XML, посмотрим на пример для каждого из языков. Далее представлен пример HTML, отображающий рецепт.
306 Глава 12. Введение в XML Листинг 12.1. Простой HTML-документ <!-- The original html recipe --> ■:. <HTML> <HEAD> <TITLE>Henderson's Hotter-than-Hell Haba?ero Sauce</T1TLE> </HEAD> <B0DY> <H3>Henderson's Hotter-than-Hell Habacero Sauce</H3> Homegrown from stuff in my garden (you don't want to know exactly what). <H4>Ingredients</H4> <TABLE B0RDER="> <TR BGCOLOR="#308030"><TH>Qty</TH><TH>Units</TH><TH>Item</TH></TR> <TR><TD>6</TD><TD>each</TD><TD>Habacero peppers</TD></TR> <TR><TD>12</TD><TD>each</TD><TD>Cowhorn peppers</TD></TR> <TR><TD>12</TD><TD>each</TD><TD>Jalapeco peppers</TD></TR> <TR><TD></TD><TD>dash</TD><TD>Tequila (optional)</TD></TR> </TABLE> <P> <H4>Instructions</H4> <0L> <LI>Chop up peppers, removing their stems, then grind to a liquid.</LI> <!-- and so forth --> </BODY> </HTML> Если вы просмотрите HTML-код листинга 12.1, то, без сомнения, заметите, что рецепт записан в HTML-таблице. На рис. 12.1 показано, как это выглядит в браузере. Игттчтгтиаг.1г.гд1ад^ - т - *-т Ы. iaf vw. f-AvoiJm. 'им ЫлИ ШЯ -ЛЛ*ЛjS* лэн ,гл • - -1-м. jj <** >**» Henderson's Hotter-than-Hell Habaiiero Sauce Homegrown from stuff m my garden (you don't want to know exactly what) Ingredients И1Ш1Ш11^Ш1111| ■6 jeach jHabafSero peppers;! 112 jeach iCowhorn peppers ;| ;12 ieach ;iJalapeno peppers ii jdash jTequila (optional) ij Instructions 1 Chop up peppers, removing their stems, then grind to a liquid. : =—" '^wtm^mmmmmTTZ Рис. 12.1. Простая HTML-страница с данными ,%>
Сравнение XML и HTML 307 Существует несколько положительных особенностей представления HTML этих данных. 1. Данные легко читаются. Если посмотреть внимательно, то можно сказать, какие именно данные содержит HTML-таблица. 2. Данные могут быть отображены в любом браузере, даже не графическом. 3. Для дальнейшего форматирования могут быть применены каскадные таблицы стилей. Однако есть действительно отрицательная особенность, которая перевешивает положительные: в коде нет ничего, что могло бы указать на значение элементов. Данные документа лишены контекста. Программа может читать документ, выбирая элементы из таблицы, но она не знает, что представляют собой эти элементы. И хотя можно жестко закодировать в программу предположения о данных (например, в столбце 1 находится Количество, в столбце 2 — Единицы измерения и т. п.), при изменении формата страницы приложение может перестать работать. Проблема усугубляется, если требуется извлечь и сохранить данные в базе данных. Так как семантическая информация о данных была утеряна при преобразовании в HTML, потребуется предоставить эту информацию повторно для того, чтобы сохранить в базе данных корректную информацию. Другими словами, придется преобразовывать данные из HTML обратно, так как HTML ^'неподходящее хранилище для семантической информации. Теперь давайте посмотрим на те же данные, представленные в XML (листинг 12.2). Вы заметите, что разметка не имеет ничего общего с отображением данных, — это исключительно описание содержимого. Листинг 12.2. Данные рецепта в XML <?xml vers1on=.0" ?> <Reci pe> <Name>Henderson&apos;s Hotter-than-Hell Habacero Sauce</Name> <Oescription> Homegrown from stuff in my garden (you don&apos;t want to know exactly what).</Description> <Ingredients> <Ingredient> <Qty unit="each">6</Qty> <Item>Habanero peppers</Item> </Ingredient> <Ingredient> :" <Qty unit="each">12</Qty> <Item>Cowhorn peppers</Item> </Ingredient> <Ingredient> <Qty unit-"each">12</Qty> <Item>Jalapeno peppers</Item> </Ingredient> <Ingredient> <Qty unit="dash" /> <Item optional="l">Tequila</Item> </Ingredient> </Ingredients> <Instructions> <Step> Chop up peppers, removing their stems, then grind to a liquid.</Step> продолжение &
308 Глава 12. Введение в ХМ1 Листинг 12.2 {продолжение) <!-- and so forth... --> </Instructions> </Recipe> Видите различие? Теги в этих данных относятся к рецептам, а не к форматированию. Файл читается, сохраняя, таким образом, простоту формата HTML, но данные теперь имеют контекст. Программа, читающая этот файл, будет точно знать что такое Ja I epeno— это I tern из I ngred i ent в Rec i pe. Относительно простоты использования: я полагаю, что вы найдете XML более легко читаемым, чем HTML. Язык XML достиг цели: он прост в использовании (как HTML), но при этом не на один порядок мощнее. XML объясняет информацию в рецепте в терминах рецепта, а не в терминах отображения рецептов. Оставим форматирование отображения для инструментов, лучше подходящих для этой задачи. Нюансы обозначений Прежде чем углубиться в обсуждение XML, важно определиться с терминологией. Давайте еще раз рассмотрим часть нашего XML-документа: <Item optional="l">Tequila</Item> / В этом коде: / 1. I tern представляет собой имя тега. Как в HTML, теги обозначают начало элемента в XML. Элементы представляют собой ключевые составляющие XML Документы состоят, главным образом, из элементов и атрибутов. 2. opt i ona I — это имя атрибута. Атрибут — поле, описывающее элемент. Имя атрибута можно выбрать любое (не только optional). Обратите внимание, что другие элементы в этом документе не имеют этого атрибута. 3. 1 — значение атрибута opt i ona I; часть от opt i ona I до 1 составляет атрибут. 4. /Item — завершающий тег элемента I tern. 5. Часть от I tern до /1 tem и есть элемент I tem. Теги XML не всегда содержат текст. Они могут быть пустыми или содержать только атрибуты. Например: <Qty unit="dash" /> Здесь Qty — имя элемента, unit — его единственный атрибут. Прямая наклонная черта в конце текста указывает на то, что элемент пустой и, таким образом, не нуждается в закрывающем теге. Вот сокращенная форма записи для такого фрагмента: <Qty unit="dash"></Qty> Пустые теги могут иметь, а могут и не иметь атрибуты. В дополнение к этим основным правилам структуры, XML-документы требуют более строгого форматирования, чем документы HTML. Документы XML должны быть формально правильными для того, чтобы XML-анализатор смог их обработать. В математике, чтобы быть логичными, уравнения должны иметь определенную форму. Уравнения без правильной формы не очень полезны. К XML-документам предъявляется
Нюансы обозначений 309 сходное требование. Чтобы XML-анализатор смог обработать XML-документ, последний должен соответствовать определенным правилам. Далее представлены наиболее важные из этих правил. ■ Каждый документ должен иметь корневой элемент, который содержит весь документ. Необязательно, чтобы корневой элемент назывался root. В предыдущем примере корневой элемент назывался Rec i pe. ■ Все теги должны быть закрыты либо закрывающим тегом, либо символом пустого тега. HTML часто не требует этого. Браузеры обычно пытаются угадать, где должен быть закрывающий тег, если он отсутствует. ■ Все теги должны быть правильно вложены. Если Qty содержится в I ng red i ent, то следует закрыть Qty до того, как будет закрыт I ng red i ent. Это также не очень строго отслеживается в HTML, но XML-анализатор не будет разбирать неправильно вложенные теги. ■ В отличие от текста элементов, значения атрибутов должны быть заключены в одинарные или двойные кавычки. ■ Символы <, >, и " не могут быть представлены буквально. Вместо них следует использовать символьное представление. Символьное представление — это строка, начинающаяся с амперсанда & и заканчивающаяся точкой с запятой (то есть специальный символ, чтобы XML-анализатор мог прочесть документ правильно). Так как символы <, >, и " имеют специальное значение в XML, их следует представлять при помощи & 11;, &gt; и &quot; соответственно. Существует еще два специальных символьных представления, которые вы будете использовать при необходимости: &атр; и &apos;. Первый из них &атр; заменяет амперсаид. Так как амперсанд входит в символьные представления в XML-документе, то его непосредственное использование в данных не позволит XML-анализатору правильно работать с документом. Аналогично &apos; представляет апостроф. Так как значения атрибутов могут быть заключены в одинарные кавычки, одиночный апостроф может сбить анализатор с толку. ■ В отличие от HTML, если вы хотите использовать символьные представления, отличные от пяти указанных выше, следует сначала описать их в DTD (мы поговорим о DTD далее). ■ Имя элемента, равно как и имя атрибута, не может начинаться с букв XML в любом регистре. XML резервирует это сочетание для своих нужд. ■ Язык ХМL чувствителен к регистру символов. Это означает, что элементы с именами Customer и customer разные. Есть различие между формально правильным и валидным XML-документом. Валидный документ — это формально правильный XML-документ, удовлетворяющий дополнительному критерию. Формально правильный документ — это только начальный этап. Кроме того, что XML-документ должен удовлетворять определенным требованиям, чтобы его мог обработать XML-анализатор, обычно существуют некоторые взаимосвязи между данными, которые делают документ смысловым. Нарушающий эти правила документ (даже формально правильный) не является валидным. Например, рассмотрим следующий XML- фрагмент.
310 Глава 12. Введение в XML Листинг 12.3. Формально правильный, но не валидный фрагмент <Саг Name="Mustang" Make="Ford" Model=966" LicensePlate=U812"> <Engine Type="Cleveland">341</Engi ne> <Engine Type="Winchester">302</Engine> </Car> ' Является ли этот фрагмент формально правильным? Да. Является ли он валидным? Наверное, нет. Большинство автомобилей не имеет двух двигателей. Посмотрите на измененный фрагмент из нашего примера: <Ingredient> <Qty unit="each">12</Qty> <Qty unit="each">10</Qty> <Item>Jalapeno peppers</Item> </Ingredient> Есть ли смысл в том, что ингредиент содержит два указания количества? Вероятно, нет. Хотя документ формально правильный, он, скорее всего, не валидный. Как же определяются правила, по которым можно судить о том, валидный документ или нет? Это делается при помощи DTD и XML Schema. Мы рассмотрим каждый из них по отдельности в следующих разделах. Определение типа документа (DTD) Существует два типа XML-анализаторов: проверяющие валидность документам не проверяющие. Последние проверяют XML-документ на предмет того, является ли он формально правильным, и возвращают представление документа в виде дерева объектов. Первые же проверяют документ также на соответствие DTD или XML Schema, чтобы выяснить, валидный документ или нет. В этом разделе мы обсудим первый из указанных методов: DTD (Определение типа документа). DTD представляет собой устаревший, хотя и очень распространенный, метод проверки валидности XML-документов. У DTD своеобразный и весьма ограниченный синтаксис, но документы DTD до сих пор можно встретить в большом числе реализаций XML. Со временем, вероятно, XML-схемы вытеснят DTD. Существует DTD-код и явления, которые DTD позволяет реализовать, а XML-схемы — нет. Поэтому DTD заслуживает отдельного рассмотрения. DTD может формализовать и систематизировать теги, используемые в определенном типе документа. Так как сам XML позволяет применять практически любые теги (лишь бы документ был формально правильный), требуется средство для обеспечения структуры документов — нечто, гарантирующее, что документ имеет смысл. DTD стал первой попыткой сделать это. Поскольку DTD определяет, какие теги могут быть использованы в документе, а также определенные характеристики этих тегов, он используется для определения новых диалектов XML, формализованных подмножеств XML-тегов и правил валидации. Изначально DTD олицетворял X в XML: DTD был средством, которое позволяло создавать новые приложения XML. Давайте посмотрим на DTD в нашем примере с рецептом. Листинг 12.4. DTD в примере с рецептом <!-- Recipe.DTD. an example OTD for Recipe.XML --> <!ELEMENT Recipe (Name, Description?, Ingredients?, Instructions?, Step?)> <!ELEMENT Name (#PCDATA)>
Определение типа документа (DTP) 311 <!ELEMENT Description (#PCDATA)> <!ELEMENT Ingredients (Ingredient)*> <!ELEMENT Ingredient (Qty, Item)> <!ELEMENT Qty (#PCDATA)> <!ATTLIST Qty unit CDATA #REQUIRED> <!ELEMENT Item (#PCDATA)> <!ATTLIST Item optional CDATA "> <!ELEMENT Instructions (Step)+> <!ELEMENT Step (#PCDATA)> -' Этот DTD определяет несколько характеристик документа, которые заслуживают обсуждения. Во-первых, обратите внимание на верхнюю строку, не являющуюся комментарием (она выделена полужирным шрифтом). Эта строка перечисляет элементы, которые могут быть представлены в документе, использующем этот DTD. Знак вопроса после элемента обозначает, что элемент необязательный. Во-вторых, обратите внимание на флаги #PCDATA. Они показывают, что элемент или атрибут может содержать только символьные данные. В-третьих, посмотрите на флаг #REQU I RED. Он указывает, что атрибут unit элемента Qty является обязательным. Документы, использующие данный DTD, не могут не содержать этот атрибут. В-четвертых, заметьте значение по умолчанию для атрибута opt i ona I элемента Item. Вместо того чтобы делать этот атрибут обязательным, его можно пропустить (о чем говорит его название). Для элементов, которые не содержат этот атрибут, его значение по умолчанию равно 0. Из листинга 12.4 видно, что синтаксис DTD не является XML-диалектом и непонятен интуитивно. Вот почему растет популярность XML-схем, которые мы вскоре обсудим. Для связывания XML-документа и DTD используется элемент декларации типа документа в начале документа (сразу после строки <?хш I ...>). Декларация ти<га документа может содержать либо сам DTD, либо ссылку на имя файла в виде URI (Универсальный идентификатор ресурса). Для файла recipe.xml это выглядит так: <!DDCTYPE Recipe SYSTEM "recipe.dtd"> Давайте посмотрим на документ с указанной ссылкой на DTD. Листинг 12.5. Документ с указанной ссылкой на DTD <?xm1 version=.0" ?> <!D0CTYPE Recipe SYSTEM "recipe.dtd"> <Recipe> <Name>Henderson&apos;s Hotter-than-Hell Habaero Sauce</Name> <Description> Homegrown from stuff in my garden (you don&apos;t want to know exactly what).</Description> <Ingredients> <Ingredient> <Qty unit="each">6</Qty> <Item>Habanero peppers</Item> </Ingredient> <Ingredient> <Qty umt="each">12</Qty> <Item>Cowhorn peppers</Item> </Ingredient> <Ingredient> <Qty unit="each">12</Qty> <Item>Ja1apeno peppers</Item> </Ingredient> продолжение &
312 Глава 12. Введение в XML Листинг 12,5 {продолжение) <Ingredient> <Qty unit="dash" /> <Item optiona1="l">Tequi1a</Item> </Ingredient> </Ingredients> <Instructions> <Step> Chop up peppers, removing their stems, then grind to a liquid.</Step> <!-- and so forth... --> </Instructions> </Recipe> Проверка данных на предмет соответствия DTD может осуществляться различными способами. Если вы используете Internet Explorer 5.0 или более позднюю версию, попробуйте использовать встроенный механизм проверки DTD, просто загрузив XML-документ в браузер. Для этого следует нажать правую кнопку мыши и выбрать пункт меню Validate. По использованию встроенного механизма проверки DTD существует большое количество графических и консоль- "ных приложений. Некоторые из них перечислены на сайте консорциума W3C http://www.w3c.org. XML Schema ПРИМЕЧАНИЕ Во время написания этой книги (январь 2001 года) синтаксис XML Schema все еще находится в процессе утверждения. W3C рассматривает данные от нескольких источников и, вероятно, опубликует стандарт позже. Скорее всего, окончательный синтаксис XML Schema будет семантически отличаться от XML Data-Reduced (XDR)-cxeM, которые сейчас поддерживаются продуктами Microsoft, работающими с XML (включая SQL Server). Microsoft заявила о поддержке окончательного синтаксиса XML Schema, каким бы он ни был. Поэтому следите за изменениями. Я говорил вам ранее, что DTD уже устарел. Существует новая, лучшая технология для проверки валидности XML-документов. Она называется XML Schema. В отличие от DTD документы XML Schema основаны на XML. Они состоят из элементов и атрибутов (как и XML-документы), которые будут проверяться при помощи схем. XML Schema обладает большим количеством преимуществ по сравнению с DTD, включая следующие. ■ DTD не может управлять типом информации в указанном элементе или атрибуте. Простого указания, что элемент храпит текст, недостаточно для большинства потребностей бизнеса. Может возникнуть необходимость указать формат, который должен иметь текст, или то, является ли текст датой или числом. XML Schema имеет большие возможности для проверки данных. ■ DTD охватывает только десять типов данных. XML Schema предлагает более 44 базовых типов данных и возможность создавать свои типы. ■ Все декларации в DTD являются глобальными. Это означает, что невозможно определить несколько элементов с одним и тем же именем, даже если они существуют в различных контекстах.
XML Schema 313 ■ Так как синтаксис DTD не базируется на XML, он требует специального обращения. Синтаксис DTD не может быть обработай XML-анализатором. Это усложняет документы, связанные с DTD, и потенциально замедляет их обработку. Всеохватывающее описание XML Schema выходит за рамки этой книги, но о некоторых моментах следует поговорить особо. Давайте посмотрим на XML Schema для проверки созданного ранее документа recipe.xml. Листинг 12.6. XML Schema для документа, содержащего рецепт <?xml version=.0" ?> <xsd:schema xmlns:xsd="http://www.w3.org/2000/10/XMLSchema" elementFormDefault="quali fied"> <xsd:element name="Recipe"> <xsd:complexType> <xsd:sequence> <xsd:element name="Name" type="xsd:string" /> <xsd:element name="Description" type="xsd-.string" /> <xsd:el ement name="Ingredients"> <xsd;complexlype> ■у - <xsd:sequence> <xsd:element name="Ingredient" maxOccurs="unbounded"> <xsd:complexType> <xsd:sequence> <xsd:element name="Qty"> <xsd:complexType> <xsd:simpleContent> <xsd:restriction base="xsd:byte"> <xsd:attribute name="umt" use="required"> <xsd:simpleType> <xsd:restriction base="xsd:NMTOKEN"> <xsd:enumeration value="dash" /> <xsd:enumeration value="each" /> .:■ <xsd:enumeration value="dozen" /> <xsd:enumeration value="cups" /> <xsd:enumeration value="teasp" /> '' ' <xsd:enumeration value="tbls" /> ■■'■ </xsd: restrict! on> ,-Ai: </xsd:simpleType> 'V </xsd;attribute> </xsd:restriction> , </xsd:simpleContent> :;''' </xsd:complexType> .>'• </xsd:element> >.::. <xsd:element name="Item"> <xsd:complexType> <xsd:simpleContent> : <xsd:restriction base="xsd:string"> <xsd:attribute name="optional" type="xsd:boolean" /> </xsd:restriction> </xsd:simpleContent> </xsd:complexType> </xsd:element> </xsd:sequence> .," </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="Instructions"> продолжение ^>
314 Глава 12. Введение в XML Листинг 12.6 {продолжение) <xsd:complexType> <xsd:sequence> <xsd:element name="Step" type="xsd:string" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd-.element> </xsd:schema> Выглядит устрашающе? Этот документ немного больше, чем рассмотренный DTD, не так ли? Однако все не так плохо. Большую часть этого документа составляют открывающие и закрывающие теги. Сама же XML Schema не так сложна. Первое, на что следует обратить внимание, — для каждого элемента и атрибута XML-документа указан тип данных. Когда документ проходит проверку на соответствие этой схеме, каждый фрагмент данных в документе проверяется на предмет соответствия указанному типу данных. Если такое соответствие не обнаруживается, то документ не проходит проверку. Далее, посмотрите на элемент maxOccu rs. В XML Schema можно указать количество подчиненных свойств элемента, включая как максимальную, так и минимальную границы. По умолчанию значение m i nOccurs и maxOccurs равно единице. Можно указать для mi nOccurs значение 0 — это будет означать необязательность элемента. Обратите внимание на элементы xsd: enumerat ion под атрибутом unit. В XML Schema можно указать список допустимых значений элемента или атрибута. Если в элементе или атрибуте обнаруживается значение, не входящее в этот список, то документ не проходит проверку. Последнее, на что следует обратить внимание, — это новый тип данных для атрибута opt i ona I элемента I tem. Я изменил его с i ntege г на Boo I ean— один из основных типов данных, поддерживаемых XML Schema. Я подчеркиваю это изменение, чтобы показать, насколько богат набор типов данных, предлагаемый XML Schema. Можно создавать новые типы, расширяя существующие. Можно создавать сложные типы — элементы, содержащие другие элементы и атрибуты. В приведенной ранее схеме тип данных Qty является сложным типом так же, как и типы I ng red i ents и I nstruct i ons. Любой элемент XML Schema, содержащий другие элементы или атрибуты, по определению является сложным типом. Возможно, вам стало интересно, каким образом можно связать XML Schema и XML-документ. Это делается при помощи добавления пары атрибутов в корневой элемент XML-документа. Например, корневой элемент в нашем файле recipe.xml будет выглядеть так: <Recipe xmlns:xsi""http://www.w3.org/2000/10/XMLSchema-instance" xsi:noNamespaceSchemaLocation="C:\_data\ggssp\Chl2\code\recipe.xsd"> Первый атрибут делает доступными для документа элементы из пространств имен xs i (XML Schema Instance). Пространство имен — это набор имен, определяемый ссылкой URI. Можно определять свои пространства имен или поступить, как в нашем примере: просто указать ссылку на пространства имен, определенные на сайте W3C. Как во многих языках программирования, пространство имен XML
XML в HTML 315 предоставляет область имен для приложения, что предотвращает конфликты имен из различных источников. В отличие от традиционных пространств имен, имена в пространстве имен XML не должны быть уникальными. Не задумываясь, просто примите к сведению, что пространство имен предоставляет область имен для использования в XML. В данном случае оно обеспечивает доступ в пространство xs i, в котором находятся элементы XML Schema Instance. Ссылаясь на пространство имен таким способом, у нас появляется возможность использовать элементы XML Schema Instance в документе, указывая префикс xs i:. Второй атрибут указывает расположение документа с XML Schema. Это приведенный ранее документ, который содержит XML Schema для нашего документа. После того как эти атрибуты указаны, инструменты, работающие с XML Schema, будут проверять документ на соответствие указанной схеме. Преобразования Расширяемого языка стилей (XSLT) Каскадные таблицы стилей широко используются для преобразования HTML-документов — подобно этому, XSLT преобразует XML-документы. XML-документы могут быть преобразованы из одного формата в другой, в другие XML-диалекты и даже в совершенно другие форматы файлов, такие как: PostScript, RTF и ТеХ. Самое приятное заключается в том, что XSLT — это, по сути, XML. Документ XSLT — это обычный XML-документ. «Как такое может быть? Не возникнет ли проблем с циклическими ссылками?» — спросите вы. Нет, поскольку XSLT — это диалект XML. Современные XML-анализаторы достаточно интеллектуальны, чтобы понять инструкции, указанные в XSLT-документе (которые представляют собой простые XML-теги и атрибуты и т. п.) для преобразования в другой документ. XML в HTML Таблица стилей XSLT — это XML-документ, состоящий из набора правил, называемых шаблонами, которые применяются к другому XML-документу для создания третьего документа. Эти шаблоны написаны на языке XML при помощи особых тегов определенного назначения. Каждый раз, когда шаблон соответствует чему-либо в исходном XML-документе, новая структура создается в исходящем документе. Часто это HTML-документ (как в примере, который мы рассмотрим), но необязательно. В листинге 12.7 показана таблица стилей XSLT, которая преобразует XML-документ с рецептом в HTML-документ, напоминающий HTML-документ, созданный вручную в этой главе. Листинг 12.7. Таблица стилей XSLT для преобразования XML-документа в HTML-документ <?xml version-' 1.0' ?> <xsl .-stylesheet version=.0" xm1ns:xsl="http.7/irt*.w3.org/1999/XSL/Transform"> <xsl:tempi ate match="/"> продолжение & i
316 Глава 12. Введение в XML Листинг 12.7 {продолжение) <html> <HEAD> <TITLE>Henderson&apos:s Hotter-than-Hel1 Habanero Sauce</TITLE> </HEAD> <body> <H3>Henderson&apos;s Hotter-than-НеП Habanero Sauce</H3> Homegrown from stuff in my garden (you don&apos;t want to know exactly what). <H4>Ingredients</H4> <table border="> <tr BGCOLOR="#00FF00"> <TH>Qty</TH> <TH>Units</TH> <TH>Item</TH> </tr> <xsl:for-each select="Recipe/Ingredients/Ingredient"> <tr> <td> <xsl:value-of select="Qty" /> </td> v-... <td> <xsl :value-of select="Qty/@unit" /> </td> <td> <xsl :value-of select="Item" /> </td> </tr> </xsl:for-each> </table> <P /> -. <H4>Instructions</H4> <0L> <xsl:for-each select="Recipe/Instructions'^ <LI> <x5l:value-of select="Step" /> </LI> </xsl:for-each> </0L> </body> </html> </xsl:template> </xsl:stylesheet> Эта таблица стилей содержит несколько интересных особенностей. Во-первых, обратите внимание на элемент xs I: temp I ate match="/"- Как я сказал, XSLT-преоб- разования происходят посредством применения шаблонов к определенным частям XML-документа. Атрибут match этого элемента посредством XPath указывает, к каким именно частям документа следует применить шаблон. В данном случае это корневой элемент. Таким образом, таблица стилей говорит: «Найти корневой элемент документа и, когда он будет найден, добавить указанный текст в итоговый документ». Далее идут несколько строк стандартного HTML-кода, который формирует заголовок веб-страницы. Далее, префикс xs I: перед элементом template. Он ссылается на пространство имен xs I. В пространстве имен определен элемент temp I ate и др. Добавление ссылки на пространство имен делает доступным для использования в доку-
XML в HTML 317 менте префикс xs 1:. URI-ссылка находится в начале таблицы стилей и имеет такой формат: <xs1:stylesheet version-"!.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> Посмотрите на заголовок таблицы HTML, который генерируется таблицей стилей. Он содержит три набора HTML-тегов <ТН>, которые формируют заголовки столбцов таблицы. Эта часть кода соответствует части кода из HTML-документа, созданного нами ранее. Наиболее интересная часть документа — это цикл. Именно здесь проявляется настоящая сила XSLT. Посмотрите на первый цикл xs I: for-each (выделен полужирным шрифтом). Цикл for-each делает в точности то, что следует из его названия: перебирает коллекцию узлов одного уровня в документе. Базовый узел, начиная с которого происходит цикл, определяется атрибутом se I ect. В данном случае это узел Reci pe/Ingredients/Ingredient. Как для атрибута match, это выражение XPath, указывающее на узел, который необходимо выбрать. Это означает, что происходит цикл по ингредиентам рецепта. Для каждого ингредиента создается новая строка в таблице. "~-— Обратите внимание на способ, посредством которого выбираются узлы для каждого элемента I ng red i ent. Для выборки значения каждого поля в каждом ингредиенте используется элемент xs I: va I ue-of. Для доступа к атрибуту unit элемента Qty используется синтаксис XPath /@name, где name — это атрибут, который необходимо получить. Тег параграфа <Р/> завершает код цикла. Традиционный HTML позволит указать этот тег без соответствующего закрывающего тега, но XML — нет. Это важный момент: когда при помощи таблицы стилей создается HTML-код, он должен быть формально правильным. HTML-код должен соответствовать правилам, которые применяются к XML-документу, чтобы он считался формально правильным. Помните: таблица стилей — это XML-документ в полном смысле. И он должен быть формально правильным, иначе XML-анализатор не сможет его прочесть. Код завершается другим циклом for-each. Этот цикл перебирает элементы Steps в каждом элементе I nst ruct i ons. Обратите внимание на использование тегов упорядоченного списка HTML (<0L>) и элементов списка (<LI>). Они работают так же, как в HTML: создают упорядоченный список. Существует несколько способов применить этот стиль для преобразования документа rec i р I е. хш I. Можно воспользоваться преобразователем XSLT от Microsoft, можно использовать преобразователь XSLT сторонних поставщиков или использовать встроенный в браузер преобразователь (если последний поддерживает XSLT-преобразования). Обратитесь к разделу «Инструменты» в конце этой главы за дополнительной информацией. Лично я применил встроенный в Internet Explorer XSLT-преобразователь. Для этого необходимо внести элемент <?xm I -sty I esheet> в сам XML-документ сразу под тегом <?XMLVERS 10N>. Вот полный элемент: <?xml-stylesheet type="text/xsl" href ="reciреЗ.xsl "?> Как вы видите, этот элемент содержит атрибут href, ссылающийся на таблицу стилей при помощи URI. Теперь каждый раз, когда я просматриваю XML-документ в Internet Explorer, таблица стилей автоматически применяется для преобра- F
318 Глава 12. Введение в XML зования документа. В листинге 12.8 показан HTML-код, созданный при помощи стиля. Листинг 12.8. HTML-код, полученный в результате преобразования <html> <HEAD> <TITLE>Henderson's Hotter-than-Hell Habanero Sauce</TITLE> </HEAD> <body> <H3>Henderson's Hotter-than-Hell Habanero Sauce</H3> Homegrown from stuff in my garden (you don't want to know exactly what). <H4>Ingredients</H4> <table border="> <tr BGCOLOR="#00FF00"> <TH>Qty</TH> <TH>Units</TH> <TH>Item</TH> \ </tr> ^--.. _- <tr> <td>6</td> <td>each</td> <td>Habanero peppers</td> </tr> <tr> <td>12</td> <td>each</td> <td>Cowhorn peppers</td> </tr> <tr> <td>12</td> ••■ <td>each</td> <td>Jalapeno peppers</td> </tr> <tr> <td></td> <td>dash</td> <td>Tequila</td> </tr> </table> <P /> <H4>Instructions</H4> <0L> <LI>Chop up peppers, removing their stems, then grind to a liquid.</LI> </0L> </body> </html> Так этот код выглядит в браузере (рис. 12.2). Возможность преобразовывать XML-документ в формально правильный HTML, который соответствует начальному примеру, подкупает. Однако не будет ли проще просто создать собственно HTML-документ? Именно этот документ будет проще создать в HTML, без использования XML и таблицы стилей. Однако, отделив хранение данных от их представления, мы можем коренным образом изменить их форматирование без изменения самих данных. Для HTML это не так. Чтобы понять, что я имею в виду, посмотрите на таблицу стилей из листинга 12.9.
XML в HTML 319 ЫмИи/иГ^.ЙЬп.ЧГп.мДи пигп кЛптЫЫШшаЫттЛ^ти^Л^м, t, Vw„ ' ч **..Ль»*Г ' ■" V ЛлИХЖл -X 'jh Vr* FevfitrS Тор* -адр ^^^B «Аил'*.' vs.. i t - ч- J у i'l и**'* Henderson's Hotter-than-Hell Habanero Sauce Homegrown from stuff in my garden (you don't want to know exactly what). Ingredients :Qtylfafa:. Даю , j б each Habanero peppers 12 each Cowhom peppers j \2 each JaJapeno peppers J dash Tequila j Instructions 1 Chop up peppers, removing their stems, then grind to a liquid. Рис. 12.2. HTML-документ в браузере Листинг 12.9. Другое преобразование того же XML-документа <?xml version='1.0'?> <xsl.-stylesheet version=.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl template match="/"> <html> <HEAD> <TITLE>Henderson&apos:s Hotter-than-Hell Habanero Sauce</TITLE> </HEAD> <body> <H3>Henderson&apos;s Hotter-than-Hell Habanero Sauce</H3> Homegrown from stuff in my garden (you don&apos:t want to know exactly what). <H4>Ingredients</H4> <UL> <xsl:for-each select="Recipe/Ingredients/Ingredient"> <LI> <xsl:value-of select="Qty"/>&#9:<xsl :value-of select="Qty/@unit"/> of <xsl :value-of select»"Item"/> </LI> </xsl:for-each> </UL> <P/> <H4>Instructions</H4> <table border="> <tr BGCOLOR="#00FF00"> <TH>#</TH> <TH>Step</TH> продолжение &
320 Глава 12. Введение в XML Листинг 12.9 {продолжение) </tr> <xsl:for-each select="Recipe/Instructions"> <tr> <td><xsl:value-of select="position()"/></td> <td><xsl:value-of select="Step"/></td> </tr> </xsl:for-each> </table> </body> </html> </xsl:tempiate> </xsl:stylesheet> Мы можем применить этот стиль для преобразования XML-документа в совершенно иной HTML-дизайи, чем полученный нами при помощи первого стиля (новый стиль можно указать, изменив элемент <?xm I -sty I esheet> или переопределив его в используемом вами средстве преобразования XSLT). На рис. 12.3 показана веб-страница, полученная при помощи нового стиля. ч. 5 Henderson'* Hutter-ttian-HetlHahaftwo S^ut* к iu £«rt vmn f ****** ''At чц ^^т fftci'* >J w Л Г£р*гимИ*» jjie»vr а£Г«т*к J j- J J J-Й -^ J*. -_....- '■ • -3 Henderson's Hotter-than-Hell Habanero Sauce Homegrown from stuff in my garden (you don't want to know exactly what) Ingredients I • 6 each of Habanero peppers ;,.. • 12 each of Cowhom peppers • 12 each of Jalapeno peppers • dash of Tequila Щ Step 1 Chop up peppers removing their stems then grind to a liquid Рис. 12.3. Преобразованный документ в браузере Как вы видите, форматирование совершенно иное. Таблица с ингредиентами исчезла, ее заменил список. В то время как шаги инструкции превратились из упорядоченного списка в таблицу. Форматирование изменилось полностью, но данные остались прежние, XML-документ не изменился.
Объектная модель документа (DOM) 321 |4 Теперь данные обладают контекстом, и мы можем получить непосредственный доступ к ним. Нет необходимости строго прописывать в коде ссылки на столбцы или строки таблицы и преобразовывать данные из HTML в пригодный для использования формат. Данные уже находятся в таком формате. Независимо от того, как мы решим преобразовывать или форматировать данные, это всегда будет истинно. Поскольку данные хранятся в XML, ими можно манипулировать как угодно. Элемент xs I: f о r-each в нашей таблице стилей показывает одну из сильных сторон XSLT. Как в большинстве языков, преимущество XSLT состоит в том, что он позволяет выполнять повторяющиеся задачи. XSLT определяет несколько мощных конструкций. Среди них: ■ xsl: if ■ xst:choose ■ xsksort ■ xsl:attribute \ ■ встроенный язык сценариев. Пакет LotusXSL от IBM предоставляет большую часть функциональности XSLT, включая возможность вызывать встроенный I .,. ECMAScript и стандартизированный JavaScript из шаблонов XSLT. Для получения полного списка можно обратиться к спецификации XSLT, но хочу еще раз сказать, что XSLT обладает мощью и расширяемостью XML. Это пример того, что я называю аспектом программируемых данных XML. Посредством XSLT можно не только указать, как форматировать данные, но и программно изменять данные из самих данных. Безусловно, это очень сильная особенность. Так как мы выполняли задачи, связанные с форматированием, при помощи XSLT и XML, то у вас могло возникнуть ощущение, что XML представляет собой технологию управления содержимым. Это неверно, XML — это намного больше. Определенно, с точки зрения веб-мастеров, XML и XSLT — огромное достижение по сравнению с HTML. XML — это не только форматирование данных и управление содержимым. Это данные и контекст данных, достаточный для того, чтобы эти данные были полезны в огромном количестве случаев. Существует целый мир приложений вне браузеров и веб-страниц. Чтобы добавить мощь XML к этим типам приложений, мы должны использовать опцию, называемую Объектной моделью документа. Объектная модель документа (DOM) До этого момента мы изучали XML с точки зрения обобщения форматов документов. Но истинная мощь XML проявляется в случае его применения для структурирования информации. Все XML-документы состоят из вложенных наборов элементов. Каждый документ «обернут» в корневой элемент, который содержит и другие элементы. Это древовидная структура: дерево элементов и объектов, которые представляют собой содержимое документа. Объектная модель документа (DOM) выходит за рамки простого обращения с текстовым потоком и предоставляет независимые от языка программирования способы работы с XML-документом как с деревом объектов. Ориентированный на объекты способ доступа к XML позволяет реализовать множество других применений XML. Он делает простым использование XML в ка- I. 11 3av QX4
322 Глава 12. Введение в XML честве механизма обмена данными между процессами или приложениями, так как работа происходит только с объектами в каждом из используемых языков. Неважно, какой язык используется: Visual Basic, Java или С#. Можно читать, манипулировать и обрабатывать XML-документы при помощи вызова методов объектов и работы со свойствами. Задумайтесь над открывающимися перспективами. Например, представьте базу данных в виде XML-документа. Требуется схема базы данных? Нет проблем. Извлеките XML-схему из DOM, пропустите ее через XSLT-преобразование, — и вы получите схему базы, которая всегда будет актуальной. Хотите создать универсальное средство для администрирования объектов SQL Server, Oracle, DB2 и других ключевых игроков рынка СУБД без кодирования с использованием API для администрирования каждой из СУБД? Сделайте так, чтобы каждая из этих СУБД предоставляла свою схему базы в виде деревьев DOM, и вы сможете создать единое приложение, которое будет способно работать с каждой из этих СУБД. Уже сейчас производители используют DOM и XML в сценариях, подобных только что описанным. Определенно, в SQL Server уже применяются эти навыки—о них мы узнаем более подробно в следующих главах. Рекомендуемая литература ■ Книга Лиз Кастро (Liz Castro) «XML for the World Wide Web Visual Quickstart Guide» достаточно кратка, но познавательна. Лиз пишет хорошие книги, и эту я считаю особенно полезной. ■ Книга «XML in a Nutshell» издательства O'Reilly также содержит полезную информацию о XML. К сожалению, в ней не рассматриваются схемы, но в достаточной мере освещено большинство других важных вопросов. ■ Книга Стива Холзнера (Steve Holzner) «Inside XML» также полезна для прочтения. Она носит всеобъемлющий характер и описывает многие ключевые темы в деталях. ■ Книга «LearningXML», еще одна книга издательства O'Reilly, является хорошим введением в тему. Она содержит удачное описание многих XML-анализаторов и подробно рассматривает некоторые вопросы, пропущенные в других книгах (например, XML-схемы). ■ Книга Майкла Кэя (Michael Kay) «XSLTProgrammer's Reference» научит всему, что необходимо знать о XSLT. Майкл является автором SAXON — одного из лучших XSLT-обработчиков. ■ Сайт W3C (http://www.w3c.org) — это ценный источник информации по XML, HTML и всему, что имеет отношение к Сети. Временами спецификации могут показаться немного сухими, но они заслуживают прочтения (если вы их все же осилите). Сайт также содержит множество ссылок на учебные пособия, инструменты и другие ресурсы, связанные с XML. ■ Большинство основных производителей программного обеспечения имеют большие XML-порталы на своих веб-сайтах. Я обнаружил, что наиболее насыщены информацией в этом отношении сайты Microsoft и Sun Microsystems.
Итоги 323 Инструменты ■ Начать следует с приобретения хорошего редактора XML/XSLT/XSD. Лично мне нравится XMLSpy (http://www.xmtspy.com), но есть и несколько других хороших инструментов. Графический интерфейс — не только для обывателей! Многочасовое использование Notepad для выполнения задач, которые инструменты с графическим интерфейсом могут сделать за секунды, просто неразумно. Можно потратить массу времени в неуклюжих программах вместо того, чтобы совершенствоваться в технологиях. ■ Далее, вам необходим валидатор XML Schema/DTD. Мне больше всего нравится инструмент XSV, созданный Генри Томпсоном (Henry Thompson) и Ричардом Тобином (Richard Tobin), но есть и другие программы. ■ В зависимости от того, какие инструменты вы используетег^ам может понадобиться отдельная программа для XSLT-преобразований. Я использую встроен- i ный в Internet Explorer преобразователь и программу XT Джеймса Кларка (James Clark). Существуют также бесплатные программы. ■ Если вы работаете в Windows, следует обновить MSXML-анализатор до послед- ' ней версии. Он лучший среди ХМL-анализаторов для платформы Windows. У корпорации IBM также есть хороший продукт. ■ SAXON Майкла Кэя (Michael Kay) также заслуживает внимания, даже если у ,ч вас есть другие XSLT-процессоры. Это отличная программа, созданная профес- ; сионалом. ■ Стоит также иметь MSXML SDK, если вы работаете на платформе Windows. Этот продукт содержит хороший код примеров и документацию, что будет по- I ■ лезно, если вы создаете приложения с использованием MSXML API. Итоги В этой главе: ■ вы познакомились с кодом XML; ■ вы сделали краткий экскурс в историю XML и рассмотрели ключевые концепции XML: XML-документы, DTD, схемы и XSLT; ■ вы узнали, что XML представляет собой не одну технологию, а целое семейство i технологий, и эти технологии продолжают развиваться и каждый день находят по всему миру новых приверженцев. Надеюсь, вы осознали: чрезвычайно важно узнать о XML как можно больше сейчас, чтобы наилучшим образом использовать связанные с XML возможности SQL Server (существующие и те, что появятся в будущем). Мое предсказание: наступит день, когда XML будет не менее значим при разработке приложений для SQL Server, чем Transact-SQL сегодня. Определенно настало время изучать XML.
«JO XML и SQL Server: I *J НТТР-запросы Обычно тот, кто первый занимает поле боя и ждет врага, чувствует себя уверенно; тот, кто приходит позже и бросается в бой, — изнурен. Сан Цзу1 Как я констатировал в предыдущей главе, XML покоряет мир. Вероятно, однажды он вытеснит HTML как наиболее распространенный язык разметки в Интернете. Поэтому нет ничего удивительного в том, что SQL Server обладает встроенной поддержкой XML. Как большинство современных СУБД, SQL Server часто необходимо обработать и сохранить данные, которые могут поступать в формате XML Без встроенной поддержки получение и запись XML в SQL Server потребовали бы от разработчика приложения преобразования XML-данных перед отправкой в SQL Server и обратной операции при получении данных из SQL Server. Учитывая повсеместность и высокую популярность XML, это быстро бы всех утомило. SQL Server является СУБД, обладающей поддержкой XML. Это означает, что он может читать и писать XML-данные. SQL Server может возвращать данные из баз в формате XML, может читать и обновлять данные, хранящиеся в XML-документах. Как показано в табл. 13.1, связанные с XML возможности SQL Server можно разделить на четыре категории. Бета-версия SQLXML Web Release 1, судя по всему, расширит этот список. Обратитесь к последнему разделу главы 15 для получения дополнительной информации. Таблица 13.1. Связанные с XML возможности SQL Server Функция Назначение FOR XML Расширение команды SELECT, позволяющее получать данные в формате XML OPENXML() Позволяет читать и записывать данные в XML-документы Запросы XPath Позволяет выполнять XPath-запросы к базам данных SQL Server XDR-схемы Поддерживает XDR-схемы и XPath-запросы Доступ к SQL Server при помощи HTTP Первое, что необходимо сделать для начала работы с XML-данными в Transact- SQL, — создать виртуальный каталог. Для этого следует выбрать пункт меню Tzu, Sun. The Art of War. Cambridge, England: Oxford University Press, 1963. C. 96.
Доступ к SQL Server при помощи HTTP 325 Configure SQL XML Support in IIS в группе программ Microsoft SQL Server. Обратите внимание, что можно использовать ADO для обработки XML-данных, полученных из SQL Server, не создавая виртуальный каталог. Можно использовать этот подход, например, для создания компонентов, осуществляющих обработку в соответствии с правилами бизнес-логики, не затрагивая обработку самих документов. Конфигурация виртуального каталога позволяет работать с XML-функциональностью SQL Server при помощи HTTP, что открывает самые широкие возможности. Начнем с того, что у нас появляется возможность создавать веб-сайты, содержащие данные из СУБД, с меньшими усилиями, чем требуется при использовании таких технологий, как ASP или JSP. Кроме того, можно использовать таблицы стилей для преобразования XML-данных, полученных из SQ% Server, в документы других форматов, например HTML или WML (Wireless Markup Language). Плюс внутренние и внешние клиенты смогут работать непосредственно с SQL Server, получая XML-данные при помощи простых HTTP-запросов. Виртуальный каталог служит для поддержания соответствия базы данных SQL Server и частью URL. Он обеспечивает доступ из корневого каталога веб-сервера к базе данных на сервере SQL Server. Способность SQL Server публиковать данные при помощи HTTP реализуется SQLISAPI, ISAPI-приложением, входящим в состав продукта. SQLISAPI использует SQLOLEDB («родной» OLE-DB-провайдер SQL Server) для доступа к ассоциированной с виртуальным каталогом базе данных и возвращения результатов клиенту. У клиентских приложений есть четыре способа доступа к SQL Server при помощи HTTP. Их можно разделить на два основных типа: для интранет-доступа (по соображениям безопасности) и для доступа через Интернет (безопасные). Посмотрим на них. ■ Интранет: □ отправить шаблонный XML-запрос SQLISAPI; Q указать SELECT...FOR XML-запрос в URL. ■ Интернет: □ указать в виртуальном каталоге XML-схему, находящуюся на сервере; □ указать в виртуальном каталоге шаблон XML-запроса, находящегося на сервере. Вследствие своей открытой природы первый и второй способы представляют угрозу безопасности при использовании через Интернет, но отлично подходят для внутренних корпоративных сетей. Обычно веб-приложения используют схемы, находящиеся на сервере, и шаблоны запросов для обеспечения доступа к XML-данным. Конфигурация виртуального каталога Как я упомянул ранее, виртуальные каталоги настраивают при помощи специальной программы Configure SQL XML Support in IIS из каталога Microsoft SQL Server. Запустите эту программу, чтобы увидеть IIS сервера на выбранном компьютере. Щелкните на знаке «плюс» слева от сервера для того, чтобы раскрыть его. Если сервера нет в списке (например, если это удаленный сервер), щелкните правой кнопкой мыши на узле IIS Virtual Directory Manager и выберите пункт меню Connect для того, чтобы произвести соединение с сервером. Для добавления нового виртуального каталога щелкните правой кнопкой мыши на узле Default Web Site и выберите New ► Virtual Directory. Откроется диалог свойств нового виртуального каталога.
326 Глава 13. XML и SQL Server: HTTP-запросы Указание пути и имени виртуального каталога Окно ввода имени виртуального каталога служит (как следует из его названия) для указания имени нового каталога, которое будет использовано в URL для доступа к данным виртуального каталога. Поэтому важно, чтобы это имя было информативным. Общепринятое соглашение о наименовании виртуальных каталогов состоит в том, чтобы называть их по имени базы данных, на которую они ссылаются. Для работы с примерами в этой главе следует указать Northwi nd в качестве имени виртуального каталога. Хотя локальный путь иногда не будет использоваться, его все равно следует указать. В обычном ASP- или HTML-приложении это путь к исходным файлам приложения. В приложениях SQLIS API этот путь не обязательно должен что- либо содержать, но существовать должен. В разделах NTFS пользователям необходимо разрешение на чтение этого каталога. На вкладке Security следует указать учетные записи, которые будут использоваться для работы с приложением. Щелкните на вкладке Security для того, чтобы выбрать режим аутентификации, Можно использовать определенную учетную запись: W i ndows I nteg rated Authent i cation или Bas i с Authent i cat i on. Выберите наиболее подходящий вариант. Для примеров в этой главе предпочтительнее использовать Windows Integrated Authentication. Далее откройте вкладку Data Source. Здесь указывается сервер и база данных, на которые ссылается виртуальный каталог. Выберите SQL Server из списка и укажите в качестве базы данных базу Northwi nd. В списке Virtual Names укажите два виртуальных имени: templates и schemas. Создайте два каталога под Northwi nd с названиями Temp I ates и Schemas так, чтобы каждое из виртуальных имен ссылалось на локальный каталог. Укажите тип schema для schemas и тип temp I ate для temp I ates. Каждый из них будет использован позже. Последняя вкладка диалога, с которой необходимо разобраться, — Settings. Откройте ее и убедитесь, что все флажки проставлены. Это необходимо для того, чтобы включить все опции, которые мы будем тестировать позже. Далее приведено краткое описание каждой опции вкладки Settings. Allow URL Queries URL-запросы позволяют указывать полные Transact-SQL-запросы в URL. Специальные символы заменяются заполнителями, но, по сути, запрос передается серверу как есть, и результаты его выполнения отсылаются обратно по HTTP. Поскольку это позволяет выполнять произвольные запросы в базе данных, эта опция обычно отключается в рабочих системах. Сейчас давайте включим ее, чтобы опробовать немного позже. Allow Template Queries Шаблонные запросы представляют собой наиболее распространенный способ получения XML-данных из SQL Server. XML-документы, хранящие шаблоны запроса, — это обобщенные параметризованные запросы с заполнителями вместо параметров, которые находятся на сервере и обеспечивают контролируемый доступ к данным. Результаты шаблонного запроса возвращаются пользователю по HTTP. Allow XPath Когда включена эта опция, для получения данных из SQL Server можно использовать подмножество языка XPath. При этом будут использованы аннотированные
URL-запросы 327 схемы, хранящиеся на веб-сервере в виде ХМL-документов, в которых указаны отображения XML-элементов и атрибутов на данные из базы, на которую ссылается виртуальный каталог. Запросы XPath позволяют выбирать данные, определенные в аннотированной схеме. Allow POST Протокол HTTP поддерживает передачу данных на сервер при помощи команды POST. Когда включена опция Allow POST, можно отсылать шаблон запроса (обычно он содержится в скрытом поле формы веб-страницы) на веб-сервер при помощи HTTP. Это приводит к выполнению запроса и отправке результатов обратно клиенту. Чтобы эта опция работала, необходимо включить Allow POST и Allow URL queries.KaK было сказано ранее, открытость такого решения ограничивает область его применения локальными сетями. Злоумышленник может сформировать свой шаблон и nfcmy- чить данные, доступ к которым нежелателен, и даже внести изменения в эти данные. После того как все опции включены, следует нажать ОК, и виртуальный каталог будет создан. СОВЕТ На вкладке Advanced есть полезная опция Disable caching of mapping schemas. Обычно схемы отображения кэшируются после первого обращения к ним, и последующие запросы идут в кэш. Во время разработки схемы отображения вам, вероятно, захочется отключить эту опцию, чтобы схема перегружалась при каждом запросе. URL-запросы Простейший способ проверить работу созданного виртуального каталога — выполнить URL-запрос из браузера, поддерживающего XML, например из Internet Explorer. Запрос имеет такой вид: http: //localhost/Northw1nd?sql=SELECT+*+FR0M+Customers+F0R+XML+AUT0&root=Customers ВНИМАНИЕ Все приведенные URL-запросы следует набирать в одну строку. Ограниченный размер страницы может привести к тому, что некоторые URL в этой книге будут размещены на нескольких строках, хотя на самом деле они представляют собой одну строку. Здесь local host — имя веб-сервера. Это может быть полное имя домена, например www.nkandescent.com. Northwind — имя виртуального каталога, созданного ранее. Знак вопроса разделят URL и его параметры. Множественные параметры разделены амперсандами. Первый переданный в этом запросе параметр называется sql. Он содержит запрос, который необходимо выполнить. Второй параметр указывает имя корневого элемента итогового XML-документа. По определению, XML- документ может содержать только один такой элемент. Если имя корневого элемента не указано, а запрос возвращает несколько элементов верхнего уровня, то произойдет ошибка. Чтобы посмотреть, как это работает, запросите следующий URL в браузере (не забудьте изменить local host на имя вашего веб-сервера, если он расположен на другом компьютере): I
328 Глава 13. XML и SQL Server: HTTP-запросы http://1ocalhost/Northwi nd?sql=SELECT+*+FROM+Customers+WHERE+CustomerId='ALFKI'+FOR+XML+AUT0 (Результаты) <Customers CustomerID="ALFKI" CompanyName="Alfreds Futterkiste" ContactName="Maria Anders" ContactTitle="Sales Representative" Address="Obere Str. 57" City="Berlin" PostalCode=2209" Country="Germany Phone=30-0074321" Fax=30-0076545" /> Обратите внимание, что корневой элемент не указан. Посмотрим, что произойдет, если вернуть на одну запись больше: http://1 ocal host/ Northwi nd?sql=SELECT+*+FROM+Customers+ WHERE+CustomerId='ALFKI'+0R+CustomerId='ANATR'+FOR+XML+AUTO (Результаты сокращены) The XML page cannot be displayed Only one top level element is allowed in an XML document. Line 1. Position 243 Так как результаты содержат несколько элементов верхнего уровня (если быть точным: два элемента), XML-документ имеет два корневых элемента Customers, что, разумеется, недопустимо, поскольку этот документ не является формально правильным. Для того чтобы исправить ситуацию, следует указать корневой элемент, который может быть назван произвольно. Его единственное назначение состоит в том, чтобы получить формально правильный документ. Вот пример: http://localhost/Northwind?sql=SELECT+*+FROM+Customers+WHERE+CustomerId='ALFKI' +OR+CustomerId='ANATR'+FOR+XML+AUTO&root=CustomerList (Результаты) <?xml version=.0" encoding="utf-8" ?> <CustomerList> <Customers CustomerID="ALFKI" CompanyName="Alfreds Futterkiste" ContactName="Maria Anders" ContactTitle="Sales Representative" Address="Obere Str. 57" City="Berlin" PostalCode=2209" Country="Germany" Phone=30-0074321" Fax=30-0076545" /> <Customers CustomerID="ANATR" CompanyName="Ana Trujillo Emparedados у helados" ContactName="Ana Trujillo" ContactTitle="Owner" Address="Avda. de la Constituci?n 2222" City="M?xico D.F." PostalCode=5021" Country="Mexico" Phone="E) 555-4729" Fax="E) 555-3745" /> </CustomerList> Корневой элемент может быть указан также как часть параметра sq I: http .7/1ocalhost/Northwi nd?sql=SELECT+'<CustomerLi st>': SELECT+*+FROM+Customers+WHERE+CustomerId='ALFKI'+OR+CustomerId='ANATR'+FOR+XML+AUTO: SELECT+'</CustomerList>': (Результаты сокращены) <CustomerList> <Customers CustomerID="ALFKI" CompanyName="Alfreds Futterkiste" ContactName="Maria Anders" ContactTitle="Sales Representative" Address="Obere Str. 57" City="Berlin" PostalCode=2209" Country="Germany" Phone=30-0074321" Fax=30-0076545" /> <Customers CustomerID="ANATR" CompanyName="Ana Trujillo Emparedados у helados" ContactName="Ana Trujillo" ContactTitle="Owner" Address="Avda. de la Constituci?n 2222" City="M?xico D.F." PostalCode=5021" Country="Mexico" Phone="E) 555-4729" Fax="E) 555-3745" /> </CustomerList>
URL-запросы 329 Параметр sq I этого URL-запроса содержит три запроса. В нем применен тот же метод получения трех различных фрагментов данных, что и в примерах с UN I ON главы о HTML: использование нескольких запросов. Первый запрос создает открывающий тег корневого элемента, второй запрос получает сами данные, а третий — создает закрывающий тег корневого элемента. Запросы разделены точкой с запятой. Как видно, конструкция FOR XML возвращает XML-фрагменты, поэтому, чтобы получить формально правильный документ, необходимо указать корневой элемент. Специальные символы \ Определенные символы, являющиеся допустимыми в Transact-SQL, могут ^вызвать проблемы в URL-запросах, так как они имеют специальное значение в URL. Вы уже заметили, что вместо пробела используется символ +. Очевидно, что это исключает непосредственное использование символа + в самом запросе. Вместо этого следует кодировать символы, имеющие особое значение в URL-запросе, таким образом, чтобы SQLISAPI мог правильно преобразовать их перед передачей запроса в SQL Server. Кодирование состоит в указании знака процента % и шестнадцатеричного значения кода символа ASCII. В табл. 13.2 перечислены специальные символы, которые распознает SQLISAPI и соответствующие им значения. Таблица 13.2, Символы, имеющие специальное значение в URL-запросе Символ Шестнадцатеричное значение _ _ & 26 ? 3F % 25 / 2F # 23 Посмотрите на URL-запрос, показывающий, как следует кодировать специальные символы: http: //localhost/Northwind?sql=SELECT+'<CustomerList>':SELECT+*+FROM+Customers+ WHERE+CustomerId+LIKE+'АЖ25'+F0R+XML+AUT0:SELECT+'</CustomerLi st>': Этот запрос содержит предикат LIKE, в котором присутствует кодированный символ процента. Шестнадцатеричное значение 25 (десятичное 37) представляет собой ASCII-значение символа процента, поэтому в кодированном виде оно представлено как %25. Таблицы стилей Кроме параметров sq I и root URL-запрос может содержать параметр xs I, указывающий XML-таблицу стилей, которую следует применить для преобразования полученного XML-документа в другой формат. Наиболее часто эта возможность используется для преобразования документа в формат HTML, что позволяет просматривать документы в не поддерживающих XML браузерах, а также дает возможность контролировать отображение документа в поддерживающих ХМ L браузерах. Посмотрим пример URL-запроса, содержащего параметр xs I.
330 Глава 13. XML и SQL Server: HTTP-запросы http://1 oca 1 host/Northwi nd?sql=SELECT+CustomerId,+CompanyName+FROM+ Customers+FOR+XML+AUTO&root=CustomerLi st&xs1=CustomerList.xsl Вот таблица стилей, на которую ссылается этот запрос, и результат: <?xm1 version=.0" ?> <xsl:stylesheet xmlns:xs1="http://www.w3.org/1999/XSL/Transform" version=.0"> <xs1:tempi ate match="/"> <HTML> <B0DY> <TABLE border»"> <TR> <TD> <B>Customer ID</B> <B>Company Name</B> </TD> </TR> <xsl :for-each select="CustomerList/Customers"> <TR> <TD> <xsl:value-of select="@CustomerId" /> </TD> <TD> <xsl :value-of select="@CompanyName" /> </TD> </TR> </xsl:for-each> </TABLE> </B0DY> </HTML> </xsl:tempi ate> </xsl:stylesheet> (Результаты сокращены) Customers CompanyName Alfreds Futterkiste AnaTrujiiio Emparedados у helados Antonio Moreno TaquerAa Around the Horn Berglunds snabbkAfp Blauer See Delikatessen Vaffeljernet Victuailles en stock Vins et alcools Clievalier Wartian Herkku Wellington Importadora White Clover Markets Wilraan Kala Wolski Zajazd [ CustomerlD ^.LFKI ^NATR iANTON AROUT BERGS BLAUS VAFFE V1CTE VINEJ WARTH WELL! WHITC WILMK WOLZA
URL-запросы 331 Тип содержимого По умолчанию SQLISAPI возвращает результаты URL-запроса вместе с соответствующим указанием типа данных в HTTP-заголовке, что позволяет браузеру корректно отобразить результаты. Когда в запросе используется FOR XML, то типом содержимого является text/xm I — если не указан параметр xs I, содержащий таблицу стилей, преобразующую результаты в HTML. В этом случае тип равен text/htm I. Можно явно указать тип содержимого при помощи параметра contenttype: http://local host/Northwind?sql=SELECT+CustomerId,+CompanyNarne+FROM+Customers+ FOR+XML+AUTO&root=CustomerList&xsl=CustomerList.xsl&contenttype=text/xml Далее приведена таблица стилей из предыдущего примера с типом содержимого text/htm I. Затем при помощи параметра contenttype тип явно меняется на text/ xm I. В итоге мы получаем XML-документ. <HTML> <B0DY> <TABLE border="l"> <TR> <TD> <B>Customer I0</B> </T0> <TD> <B>Company Name</B> </T0> </TR> <TR> <TO>ALFKI</TD> <TD>Alfreds Futterkiste</TD> **" </TR> <TR> <TO>ANATR</TD> <TD>Ana Trujillo Emparedados у helados</T0> </TR> <TR> <TO>WILMK</TD> <T0>W11man Kala</T0> </TR> <TR> <T0>W0LZA</TD> <T0>Wolski Zajazd</T0> </TR> </TABLE> </B0DY> </HTML> Таким образом, даже если документ состоит из формально правильного HTML, он отображается как XML-документ, так как явно указан тип содержимого text/ Результаты, не являющиеся XML Возможность указать тип содержимого особенно полезна при работе с XML-фрагментами в поддерживающем XML браузере. Как я уже упомянул ранее, выполнение запроса FOR XML без корневого элемента приводит к ошибке. Однако ошибку можно обойти, указав тип содержимого HTML, как в этом примере:
332 Глава 13. XML и SQL Server: HTTP-запросы http://localhost/Northwind?sq1=SELECT+*+FR0M+Customers+WHERE+ CustomerId='AlFKr+OR+CustomerId='ANATR'+FOR+XMl+AUTO &contenttype=text/html Если запросить этот URL в браузере, то, вероятно, будет показана пустая страница, так как большинство браузеров игнорируют теги, которые им неизвестны. Однако можно посмотреть на исходный код страницы и увидеть XML-фрагмент, который мы хотим получить. Это полезно в том случае, когда взаимодействие с SQLISAPI при помощи HTTP происходит не через браузер, а через какое-то другое приложение. Можно вернуть XML-фрагмент клиенту и применить клиентский код для добавления корневого элемента и/или дальнейшей обработки. SQLISAPI также позволяет не указывать конструкцию FOR XML для того, чтобы вернуть один столбец таблицы, представления или табличной пользовательской функции в виде текстового потока: http://localhost/Northwind?sql=SELECT+CAST(CustomerId+AS+charA0))+AS+ CustomerId+FR0M+Customers+0RDER+BY+CustomerId&contenttype=text/litm1 (Результаты) ALFKI ANATR ANTON AROUT BERGS BLAUS BLONP BOLID BONAP BOTTM BSBEV CACTU CENTC CHOPS COMMI CONSH DRACD DUMON EASTC ERNSH FAMIA FISSA FOLIG FOLKO FRANK FRANR FRANS FURIB GALED G000S GOURL GREAL GROSR HANAR HILAA HUNGC HUNGO ISLAT KOENE LACOR LAMAI LAUGB LAZYK LEHMS LETSS LILAS LINOD LONEP MAGAA MAISD MEREP MORGK NORTS OCEAN 0L0W0 OTTIK PARIS PERIC PICCO PRINI QUEOE QUEEN QUICK RANCH RATTC REGGC RICAR RICSU ROMEY SANTG SAVEA SEVES SIMOB SPECD SPLIR SUPRO THEBI THECR TOMSP TORTU TRADH TRAIH VAFFE VICTE VINET WANDK WARTH WELLI WHITC WILMK WOLZA Обратите внимание, что SQLISAPI не поддерживает возвращение результатов из нескольких столбцов. Тем не менее он весьма удобен для того, чтобы быстро вернуть простой список данных. Хранимые процедуры Помимо разных видов Transact-SQL-запросов, в URL-запросах можно выполнять хранимые процедуры. Конечно, процедура должна возвращать результаты в виде XML, если их требуется обрабатывать как XML в браузере или клиентском приложении. Вот пример такой процедуры: CREATE PROC ListCustomersXML @CustomerId varcharA0)=T. @CompanyName varchar(80)=T AS SELECT Customerld. CompanyName FROM Customers WHERE Customerld LIKE @CustomerId AND CompanyName LIKE @CompanyName FOR XML AUTO Если процедура возвращает корректные результаты в формате XML, ее можно вызывать в URL-запросе при помощи Transact-SQL-команды EXEC. Далее приведен пример вызова процедуры: http://localhost/Northwind?sql=EXEC+Li stCustomersXML+@CustomerId='A%25'. @CompanyName=' f\n%25' &root=CustomerLi st (Результаты) . .'
Шаблонные запросы 333 <?xml version=.0" encoding="utf-8" ?> <CustomerList> I Customers CustomerId="ANATR" CompanyName="Ana Trujillo Emparedados у helados" /> [ <Customers CustomerId="ANTON" CompanyName="Antonio Moreno Taquer?a" /> ,: </CustomerList> СОВЕТ Для вызова хранимых процедур из URL-запроса можно также использовать синтаксис ODBC CALL. Поскольку способа указать параметры RPC из URL-запроса не существует, то, если указаны параметры, переданные процедуре, ее вызов будет преобразован в простое событие языка на сервере. Однако если процедура не принимает параметров, она будет выполнена при помощи RPC, что обычно быстрее и эффективнее. На сильно загруженных веб-сайтах небольшое различие в производительности может быть ощутимым. Посмотрим на URL-запрос, использующий синтаксис ODBC CALL: http://local host/Northwind?sql={CALL+ListCustomersXML}+&root<ustomerList Если запросить такой URL в браузере при запущенном сеансе трассировки, включающем события RPC, то для этой процедуры мы обнаружим событие RPCiStarting. Это говорит о том, что процедура вызывается более эффективным способом, нежели простой запрос. Обратитесь к разделу «Шаблонные запросы» этой главы для получения информации о поддерживающих параметры RPC вызовах из XML. ' Обратите внимание, что для указания Transact-SQL-символа % используется его закодированный эквивалент %25. Это обязательно, поскольку символ процента имеет специальное значение в URL-запросе. Шаблонные запросы Более распространенный и безопасный способ получения данных при помощи HTTP состоит в использовании серверных XML-шаблонов, содержащих Trans- act-SQL-запросы. Поскольку эти шаблоны хранятся на сервере и на них ссылаются при помощи виртуального имени, конечный пользователь никогда не видит их исходного кода. Шаблоны представляют собой XML-документы, использующие пространство имен XML-SQL и служащие в качестве механизма преобразования URL в запрос, который может быть обработан SQL Server. Как с обычными URL- запросами, результаты выполнения шаблонных запросов возвращаются в виде XML или HTML. Вот простой шаблон XML-запроса: <?xml version»'1.0' ?> <CustomerLi st xmlns:sql ='urn:schemas•mi crosoft-com:xml -sqV > <sql:query> SELECT Customerld. CompanyName FROM Customers FOR XML AUTO : </sql:query> </CustomerList> Обратите внимание на использование префикса пространства имен sq I в самом запросе. Использование префикса становится возможным благодаря ссылке на пространство имен во второй строке шаблона (она выделена полужирным шрифтом). Здесь просто возвращаются два столбца таблицы Customers базы данных Northwi nd, как в большинстве примеров этой главы. Конструкция FOR XML AUTO ука-
334 Глава 13. XML и SQL Server: HTTP-запросы зана для того, чтобы данные пришли в виде XML. Вот URL, который использует шаблон, и данные, которые получаются в итоге: http://1ocalhost/Northwind/templates/CustomerLi st.XML (Результаты сокращены) <?xml version="l.0" ?> <CustomerList xmlns:sql="urn:schemas-microsoft-com:xml-sql"> Customers CustomerId="ALFKI" CompanyName="Alfreds Futterkiste" /> <Customers CustomerId="VAFFE" CompanyName="Vaffeljernet" /> <Customers CustomerId="VICTE" CompanyName="Victual lies en stock" /> Customers CustomerId="VINET" CompanyName="Vins et alcools Chevalier" /> Customers CustomerId="WARTH" CompanyName="Wartian Herkku" /> <Customers CustomerId="WELLI" CompanyName="Wellington Importadora" /> <Customers CustomerId="WHITC" CompanyName="White Clover Markets" /> <Customers CustomerId="WILMK" CompanyName="Wilman Kala" /> <Customers CustomerId="WOLZA" CompanyName="Wolski Zajazd" /> </CustomerList> Обратите внимание, что используется виртуальное имя шаблона, которое было создано под виртуальным каталогом Northwi nd ранее. Параметризованные шаблоны Также можно создавать параметризованные XML-запросы, дающие пользователю возможность указывать параметры исполняемого запроса. Параметры определяются в заголовке шаблона, который находится в элементе sq I: header. Каждый параметр определяется при помощи тега sq I: pa ram и может содержать необязательное значение по умолчанию. Посмотрим пример: <?xml version-'1.0' ?> <CustomerList xmlns:sql ='urn:schemas-microsoft-com:xml-sql'> <sql:header> <sq1 :param name='Customerld'>Wsql :param> </sql:header> <sql:query> SELECT Customerld, CompanyName FROM Customers WHERE Customerld LIKE @CustomerId FOR XML AUTO </sql:query> </CustomerList> Обратите внимание на использование sq I: pa ram для определения параметра. В данном примере параметру присваивается значение %, так как он используется в предикате LI KE. Это означает, что будет получен список всех клиентов, если значение параметра не указано при выполнении запроса. SQLISAPI достаточно интеллектуален, поэтому шаблонный запрос выполняется на сервере как RPC при указанных параметрах. Происходит связывание указанных в шаблоне параметров с RPC и отправка запроса на SQL Server при помощи вызовов RPC API. Этот способ более эффективен, нежели использование событий языка Trans- act-SQL, и работает быстрее, особенно в системах с большой пропускной способностью. Вот пример URL, в котором указан параметризованный шаблонный запрос, а также результаты его выполнения:
Шаблонные запросы 335 http: //localhost/Northwind/Templates/CustomerList2.XML?CustomerId=Au25 (Результаты) <?xml version=.0" ?> <CustomerList xmlns:sq1="urn:schemas-m1crosoft-com:xml-sql"> <Customers CustomerId="ALFKI" CompanyName="A1freds Futterkiste" /> Customers CustomerId="ANATR" CompanyName="Ana Trujillo Emparedados у helados" /> <Customers CustomerId="ANTON" CompanyName="Antonio Moreno Taqueria" /> •Customers CustomerId="AROUT" CompanyName="Around the Horn" /> </Customerlist> Таблицы стилей Как в обычных URL-запросах, можно указать таблицу стилей для применения с шаблонным запросом. Это можно сделать в самом шаблоне или в URL. Вот пример URL, в котором применяется таблица стилей: http: //local host/ Northwi nd/Templ ates/CustomerLi st3. XML? xsl templates/ CustomerLi st3.xs 1 &contenttype=text/html Чтобы результаты трактовались как HTML, указан параметр contenttype (выделен полужирным шрифтом). Это выполняется, так как заранее известно, что преобразование XML, полученного от SQL Server, при помощи таблицы стилей должно привести к созданию HTML-таблицы. Относительный путь от виртуального каталога к таблице стилей указан потому, что она не находится автоматически в каталоге Templates (несмотря на то, что сам XML-документ находится именно там). Как я уже упомянул, пространство имен XML-SQL также поддерживает указание таблицы стилей в самом шаблоне. Вот такой шаблон: <?xml version-'1.0' ?> <Customerl_ist xmlns:sql = 'urn:schemas-microsoft-com:xml-sql' sql:xsl = 'CustomerList3.xsl'> <sql:query> SELECT Customerld. CompanyName FROM Customers FOR XML AUTO </sql:query> </CustomerList> Вот таблица стилей, на которую ссылается шаблон: <?xml version=.0"?> <xsl .-stylesheet xmlnS:xsl="http: //www.w3.org/1999/XSL/Transform" version=.0"> <xsl template match="/"> <HTML> <B00Y> <TABLE border="l"> <TR> <TD><I>Customer I0</I></TD> <TD><I>Company Name</I></TD> </TR> <xsl:for-each select="CustomerList/Customers"> <TR> <T0><B> <xsl:value-of select="@CustomerId"/> </B></TD> /-. <TD> <xsl :value-of select="(aCompanyName"/>
336 Глава 13. XML и SQL Server: HTTP-запросы </TD> \ </TR> </xsl:for-each> </TABLE> </BODY> </HTML> </xsl .-tempiate> </xsl:stylesheet> А это URL, который использует указанный шаблон и таблицу стилей: http://localhost/Northwind/Templates/CustomerUst4.XMt?contenttype=text/htm1 (Результаты сокращены) (Customer ГО [alfki Janatr |anton [arout (bergs JBLAUS pLONP [WARTH JWELLI [whitc (WILMK |W0LZA Company Name Alfreds Futterkiste -— ~™—"""—-— ——4 AnaTrujillo Emparedadosy helados Antonio Moreno TaquerAa Around the Horn Berglunds snabbkAfp j Blauer See Delikatessen Blondesddsl pA're et fils Wartian Herkku Wellington Importadora j White Clover Markets WiJman Kala Wolski Zajazd Обратите внимание: чтобы результаты трактовались как HTML, должен быть указан параметр contenttype. Это необходимо, потому что браузеры типа Internet Explorer автоматически рассматривают результаты выполнения XML-шаблонов как text/xml. Так как итоговый HTML представляет собой формально правильный XML, браузер не знает, что его необходимо отобразить как HTML. СОВЕТ Во время разработки XML-шаблонов и подобных документов, которые затем проверяются в браузере, может возникнуть проблема, связанная с кэшированием старых версий документов (даже при нажатой кнопке Refresh или клавише F5). В Internet Explorer можно нажать Ctrl+F5, чтобы перезагрузить документ, даже если браузер не видит в этом необходимости. Обычно это помогает решить проблему кэширования старых версий. Применение таблиц стилей при работе с клиентом Если клиент поддерживает работу с XML, то можно применять стили к шаблонным запросам на стороне клиента. Это немного разгружает сервер, но требует до-
Шаблонные запросы 337 полнительного шага для загрузки таблицы стилей клиентом. Если клиент не поддерживает работу с XML, то таблица стилей будет проигнорирована, что подходит для корпоративных приложений. Вот шаблон, указывающий стиль для клиента, поддерживающего работу с XML: <?xml version»'1.0' ?> <?xml-stylesheet type='text/xsV href='Customerlist3.xsl'?> <CustomerList xmlns: sql =' urn■. schemas-microsoft-com: xml -sql' > <sql:query> SELECT Customerld, CompanyName FROM Customers FOR XML AUTO </sql:query> </CustomerList> Обратите внимание на указание хш I -sty I esheet в начале документа (выделено полужирным шрифтом). Оно указывает XML-анализатору клиента на необходимость загрузить указанный в атрибуте href стиль и произвести преобразование. Вот URL и результаты: http://1ocalhost/Northwind/Templates/CustomerL i st5.XML?contenttype=text/html (Результаты сокращены) Customer ID ALFKI ANATR ANTON AROUT VICTE VINET WARTH WELLI WHITC WILMK WOLZA Company Name Alfreds Futterkiste Ana Trujillo Emparedados у helados j Antonio Moreno Taqueria Around the Horn Victuailles en stock Vins et alcools Chevalier Wartian Herkku Wellington Importadora White Clover Markets Wilman Kala Wolski Zajazd Клиентские шаблоны Как я уже говорил, намного более распространено и безопасно хранение шаблонов на веб-сервере и осуществление доступа к ним при помощи виртуальных имен. Также удобно, если шаблон применяется при работе с клиентом, поддерживающем работу с XML. Указание клиентских шаблонов в HTML или в приложении облегчает задачу настройки шаблонов и виртуальных каталогов, связанных с ними. С точки зрения администрирования, последнее определенно проще, но потенциально небезопасно для применения в Интернете, так как клиенты могут выполнять свой код на SQL Server.
338 Глава 13, XML и SQL Server: HTTP-запросы Использование этого метода должно быть ограничено интранет и корпоративными сетями. Посмотрим на веб-страницу, содержащую клиентский шаблон: <НТМ1_> <HEAD> <TITLE>Customer List</TITLE> </НЕА0> <B0DY> <F0RM action='http-.//localhost/Northwind' method='POST'> <B>Customer ID Number</B> <INPUT type=text name=CustomerId value=T> <INPUT type=hidden name=xsl value=Templates/CustomerList2.xsl> <INPUT type=hidden name=template value=' <CustomerList xmlns:sql="urn:schemas-microsoft-com:xml-sql"> <sql:header> <sq1: param name="CustomerId">Wsql: param> </sql:header> <sq1:query> SELECT CompanyName, ContactName FROM Customers WHERE Customerld LIKE PCustomerld FOR XML AUTO </sql:query> </CustomerList> '> <P><input type='submlt'> </F0RM> </B0DY> </HTML> Клиентский шаблон (выделен полужирным шрифтом) встроен в скрытое поле формы. Если открыть эту страницу в браузере, можно увидеть поле для ввода идентификатора пользователя и кнопку отправки формы. Указание идентификатора пользователя или маски и нажатие кнопки приводит к отправке шаблона на сервер. SQLISAPI извлечет запрос из шаблона и выполнит его в базе данных Northwi nd, так как именно на эту базу данных установлена ссылка виртуального каталога. Далее будет применена таблица стилей CustomerList2.xsl для преобразования результирующего XML-документа, полученного от SQL Server, в HTML, и итог будет возвращен клиенту. На рисунке показан пример работы формы (вверху) и результат ее работы (внизу): Customer ID Number I А% [[Company Name fAlfreds Futterkiste JAna Trujillo Emparedados у heJados |Antonio Moreno Taqueria jAround the Horn [Contact Name j [Maria Anders [Ana Trujillo [Antonio Moreno! [Thomas Hardy
Итоги 339 Подобно шаблонам, расположенным на сервере, клиентские шаблоны, содержащие параметры, отправляются на SQL Server при помощи RPC. Итоги В этой главе: ■ вы изучили доступ к SQL Server при помощи HTTP; ■ вы узнали, как настроить SQL Server для получения такого доступа; как этот доступ работает; как отсылать запросы и получать результаты по HTTP; ■ вы рассмотрели шаблонные запросы и причины, по которым шаблонные запросы, в целом, предпочтительнее URL-запросов. В следующей главе мы используем их для доступа к другим связанным с XML функциям SQL Server.
14 XML и SQL Server: получение данных Процесс подготовки программ для цифрового компьютера — это очень увлекательное занятие. И дело не только в том, что оно оправдывает себя с экономической и научной точек зрения; оно может вызвать также эстетические переживания, подобные тем, которые испытывают творческие личности при написании музыки или стихов. Д. Кнут} До сих пор мы применяли FOR XML AUTO для получения данных из SQL Server в базовом формате, с которым можно работать в браузере. Однако синтаксис FOR XML намного богаче. Он поддерживает несколько опций, которые существенно расширяют его возможности. В этой главе мы обсудим несколько таких опций и рассмотрим примеры, иллюстрирующие их применение. SELECT...FOR XML Как показывают примеры предыдущей главы, для получения XML-данных из SQL Server можно использовать опцию FOR XML команды SELECT. Опция FOR XML заставляет SQL Server возвращать данные в виде XML-потока вместо обычного набора строк. Этот поток может быть представлен в одном из трех режимов: RAW, AUTO и EXPLIСI Т. Базовый синтаксис FOR XML выглядит так: SELECT column list FROM table list WHERE filter criteria FOR XML RAW | AUTO | EXPLICIT [. XMLDATA] [, ELEMENTS] [. BINARY BASE64] Хотя эти опции будут подробно рассмотрены отдельно, давайте все же кратко опишем их здесь. RAW возвращает значения полей в виде атрибутов и представляет каждую запись в виде элемента. AUTO возвращает значения полей в виде атрибутов и представляет каждую запись в виде элемента, названного по имени таблицы, из кото- Knuth, Donald. The Art of Computer Programming. Vol. 1. Fundamental Algorithms. Reading, MA: Addison-Wcslcy, 1997.
Режим AUTO / 341 рой извлекаются данные. На самом деле этот процесс более сложный. SQL Server использует несколько эвристик для того, чтобы определить имена элементов в результатах запроса FOR XML AUTO. Опция EXPLIСIT позволяет полностью контролировать формат результирующего XML-фрагмента. XMLDATA приводит к тому, что возвращается XDR-схема для результирующего документа. ELEMENTS приводит к тому, что столбцы в режиме XML AUTO возвращаются как элементы, а не атрибуты. ВI NARY BASE64 указывает на то, что двоичные данные (если они есть в результатах запроса) должны быть представлены в кодировке BASE64. Режим RAW Режим RAW является самым простым из трех базовых. Он производит очень простое преобразование результирующего набора в XML. Вот пример: SELECT Customerld. CompanyName FROM Customers FOR XML RAW (Результаты сокращены) XML_F52E2B61-18Al-lldl-B105-00805F49916B ' ' <row CustomerId="ALFKI" CompanyName="Alfreds Futterkiste"/><row CustomerId="ANATR" CompanyName="Ana Trujillo Emparedados у helados"/><row CustomerId="ANTON" CompanyName="Antonio Moreno Taquer?a"/><row CustomerId="AROUT" CompanyName="Around the Horn"/><row CustomerId="BERGS" CompanyName="Berglunds snabbk?p"/><row CustomerId="BLAUS" CompanyName="Blauer See Delikatessen"/><row CustomerId="BLONP" CompanyName="Blondesddsl pere et fils"/><row CustomerId="WELLI" CompanyName="Wellington Importadora"/><row CustomerId="WHITC" CompanyName="White Clover Markets"/><row CustomerId="WILMK" CompanyName="Wilman Kala"/><row CustomerId="WOLZA" CompanyName="Wolski Zajazd"/> Каждый столбец результирующего набора становится атрибутом, каждая запись — элементом с именем row. Однако помните, что XML, который возвращает FOR XML, не является формально правильным, так как у него отсутствует корневой элемент. Это XML-фрагмент, и вам следует добавить корневой элемент для того, чтобы документ мог быть обработан XML-анализатором. Режим AUTO Режим FOR XML AUTO предоставляет больше возможностей для того, чтобы контролировать итоговый XML-фрагмент, чем режим RAW. Каждая строка результирующего набора получает имя в соответствии с именем таблицы, именем представления или табличной функции, определяемой пользователем, которая является источником строки. Вот пример простого запроса FOR XML AUTO: SELECT Customerld. CompanyName FROM Customers FOR XML AUTO
342 Глава 14. XML и SQL Server: получение данных | (Результаты сокращены) XML F52E2B61-18Al-lldl-B105-00805F49916B <Customers CustomerId="ALFKI" CompanyName="Alfreds Futterkiste"/><Customers CustomerId="ANATR" CompanyName="Ana Trujillo Emparedados у helados"/><Customers CustomerId="ANTON" CompanyName="Antonio Moreno Taquer?a"/><Customers CustomerId="AROUT" CompanyName="Around the Horn7>Oustomers CustomerId="VINET" CompanyName="Vins et alcools Chevalier"/><Customers CustomerId="WARTH" CompanyName="Wartian Herkku"/>Oustomers CustomerId="WELLI" CompanyName="Wellington Importadora"/><Customers Customerld="WHITC" CompanyName="White Clover Markets"/><Customers CustomerId="WILMK" CompanyName="Wilman Kala"/><Customers CustomerId="WOLZA" CompanyName="Wolski Zajazd"/> Обратите внимание на то, что каждая строка названа по имени таблицы, из которой она была взята: Customers. Если результаты содержат более одной строки, то возникает несколько корневых элементов фрагмента, что недопустимо в XML Большое различие между режимами AUTO и RAW состоит в том, как происходят соединения. В режиме RAW происходит простое преобразование столбцов результирующего набора в атрибуты XML-фрагмента — один к одному. Каждая строка становится элементом фрагмента с именем row. Эти элементы пустые и не содержат значений или вложенных элементов, они имеют только атрибуты. Рассматривайте атрибуты как описатели характеристик элемента, а данные и вложенные элементы как его содержимое. В режиме AUTO каждая строка называется в соответствии с источником, из которого она была извлечена, и строки из соединенных таблиц вкладываются друг в друга. Вот пример: SELECT Customers.CustomerlD. CompanyName, Orderld FROM Customers JOIN Orders ON (Customers. Customer IdOrders. Customer Id) FOR XML AUTO (Результаты сокращены и отформатированы) XML F52E2B61-18Al-lldl-B105-00805F49916B Customers CustomerID="ALFKI" CompanyName="Alfreds Futterkiste"> Orders OrderId=06437><Orders Orderld=06927> Orders OrderId=0702"/>Orders Orderld=0835'7> Orders OrderId=0952"/>Orders Orderld=1011'7> </Customers> Oustomers CustomerID="ANATR" CompanyName="Ana Trujillo Emparedados у helados"> Orders OrderId=0308"/>Orders OrderId=0625"/> Orders 0rderld=" 10759"/><0rders Orderld=09267></Customers> Oustomers CustomerID="FRANR" CompanyName="France restauration"> Orders OrderId=0671"/>Orders OrderId=0860"/> Orders 0rderld=" 109717> </Customers> Я отформатировал фрагмент для того, чтобы его было легче читать. Если вы выполните запрос из Query Analyzer, то увидите поток неформатированного XML- текста. Обратите внимание на то, как расположены элементы Orders для каждого элемента Customer. Как мы говорили, в режиме AUTO происходит вложение строк при объединении. Не упустите из виду то, что в критерии объединения исполь-
ELEMENTS 343 зуется полное имя таблицы. Вы спросите, почему мы не стали использовать псевдоним? Потому что в режиме AUTO псевдонимы будут использованы для имен элементов в результатах. Если вы примените сокращенное имя таблицы, то элементы в результирующем XML-фрагменте будут носить это сокращенное имя. Хотя в традиционном Transact-SQL псевдонимы полезны, в данном случае они могут затруднить процесс понимания XML-фрагмента, если будут недостаточно наглядны. ELEMENTS Опция ELEMENTS предложения FOR XML AUTO приводит к тому, что в режиме AUTO вместо атрибутов возвращаются вложенные элементы. В зависимости от ваших потребностей или потребностей ваших клиентов отображение, ориентированное на элементы, может быть предпочтительнее, чем отображение, ориентированное на атрибуты и используемое по умолчанию. Вот пример запроса FOR XML, который возвращает элементы вместо атрибутов: SELECT CustomerID, CompanyName FROM Customers FOR XML AUTO. ELEMENTS (Результаты сокращены) XML F52E2B61-1BA1-Ildl-B105-00B05F49916B <Customers> <CustomerID>ALFKI</CustomerID> <CompanyName>Alfreds Futterkiste</CompanyName> </Customers> <Customers> <CustomerID>ANATR</CustomerID> <CompanyName>Ana Trujillo Emparedados у helados</CompanyName> </Customers> <Customers> <CustomerID>ANTON</CustomerID> <CompanyName>Antonio Moreno Taqueria</CompanyName> </Customers> <Customers> <CustomerID>AROUT</CustomerID> <CompanyName>Around the Horn</CompanyName> </Customers> <Customers> <CustomerID>WILMK</CustomerID> <CompanyName>Wilman Kala</CompanyName> </Customers> <Customers> <CustomerID>WOLZA</CustomerID> <CompanyName>Wolski Zajazd</CompanyName> </Customers> Используя опцию ELEMENTS, вы возвращаете в качестве вложенных элементов то, что раньше возвращалось как атрибуты элемента Customers. Каждый атрибут представляет собой пару тегов элемента, которые окружают значение из столбца таблицы.
344 Глава 14. XML и SQL Server: получение данных ПРИМЕЧАНИЕ В настоящее время режим AUTO не поддерживает конструкцию GROUP BY или агрегатные функции, Эвристики, которые использует этот режим для определения имен элементов, несовместимы с этими конструкциями, поэтому их нельзя использовать в запросах режима AUTO. Кроме того, FOR XML сам по себе несовместим с COMPUTE, так что нельзя использовать это выражение в любых запросах FOR XML. Режим EXPLICIT Если требуется установить полный контроль над получаемым XML-фрагментом, вам нужен режим EXPLIСI Т. Он более гибок (и, соответственно, сложнее в использовании), чем режимы RAW и AUTO. Запросы в режиме EXPLIСIT определяют XML-документы в терминах универсальной таблицы — механизма для возвращения результирующих наборов из SQL Server, который позволяет не составлять документ, а описать, как он должен выглядеть. Универсальная таблица представляет собой результирующий набор со специальными заголовками столбцов, которые сообщают серверу, как создать XML-документ из ваших данных. Рассматривайте это как ориентированный на наборы способ вызова API и передачи ему параметров. Вы используете доступные в Transact-SQL функции для того, чтобы сделать вызов и передать ему параметры. Универсальная таблица содержит по одному столбцу для каждого столбца исходной таблицы, которые необходимо вернуть в XML-фрагмент плюс два дополнительных столбца: Tag и Parent. Столбец Tag содержит положительное целое число, однозначно идентифицирующее каждый тег, который необходимо вернуть в документе, столбец Parent устанавливает связь «родитель-потомок» между тегами. Остальные столбцы в универсальной таблице — те, что соответствуют данным, которые требуется включить в XML-фрагмент, — имеют специальные имена из нескольких частей, разделенных восклицательными знаками. Эти имена столбцов дают инструкции анализатору SQL Server относительно создаваемого XML-фрагмента. Они имеют такой формат: El ement!Tag!Attri bute!Di recti ve Далее мы рассмотрим это на примерах. Первое, что необходимо сделать для построения запроса в режиме EXPLICIT, — определить вид XML-документа, который требуется получить. После этого необходимо построить универсальную таблицу, которая создаст требуемый формат XML-фрагмента. Например, пусть требуется получить простой список клиентов, основанный на данных таблицы Customers базы данных Northw i nd, в котором идентификатор клиента присутствует в виде атрибута, а название компании — в виде элемента. XML-фрагмент может выглядеть так: -Customers CustomerId="ALFKI">Alfreds Futterkiste</Customers> Вот запрос Transact-SQL, который возвращает универсальную таблицу, определяющую вид XML-документа. SELECT 1 AS Tag. NULL AS Parent,
Директивы 345 Customerld AS [Customers!l!CustomerId], CompanyName AS [Customers!!] FROM Customers (Результаты сокращены) Tag Parent Customers!!!Customerld Customers!! 1 NULL ALFKI Alfreds Futterkiste 1 NULL ANATR '■ Ana Trujillo Emparedados у 1 NULL ANTON Antonio Moreno Taqueria Первые два столбца — дополнительные (о них мы уже говорили). Tag указывает идентификатор тега, который требуется создать. Так как нам необходимо в строке получить только один элемент, здесь указана единица. То же самое для Pa rent. Этот единственный элемент верхнего уровня не имеет родителя, поэтому в качестве Parent для каждой строки возвращается значение NULL. Поскольку нам требуется получить Customerld в виде атрибута, имя атрибута указано в заголовке третьего столбца (оно выделено полужирным шрифтом). И поскольку мы хотим получить CompanyName в виде элемента, а не атрибута, имя атрибута не указано в четвертом столбце. Сама по себе универсальная таблица ничего не делает. В конце мы указываем FOR XML EXPL IСIT для того, чтобы специальные столбцы приобрели особое значение. Добавьте FOR XML EXPL IСIT к этому запросу и выполните его в Query Analyzer. Вот то, что вы должны получить: SELECT 1 AS Tag, NULL AS Parent. Customerld AS [Customers!1!Customerld]. CompanyName AS [Customers!!] FROM Customers FOR XML EXPLICIT (Результаты сокращены и отформатированы) XML_F52E2B61-18Al-lldl-B105-00805F49916B Customers CustomerId="ALFKI">Alfreds Futterkiste</Customers> <Customers CustomerId="ANATR">Ana Trujillo Emparedados у helados</Customers> •Customers CustomerId="WHITC">White Clover Markets</Customers> Oustomers CustomerId="WILMK">Wilman Kala</Customers> <Customers CustomerId="WOLZA">Wolski Zajazd</Customers> Как можно видеть, каждое значение Customer I d возвращается в виде атрибута, а каждое значение CompanyName возвращается в виде элемента данных для Customers, как и было указано. Директивы Четвертая часть составного заголовка столбца в режиме EXPL IСIT представляет собой директиву. Она используется для того, чтобы в дальнейшем вы могли управлять тем, как будут представлены данные в итоговом XML-фрагменте. Директива может принимать одно из восьми значений. I
346 Глава 14. XML и SQL Server: получение данных Таблица 14.1. Значения директивы Значение Функция Element Данные столбца кодируются и возвращаются в виде вложенных элементов Xml Данные столбца не кодируются и возвращаются в виде вложенных элементов Xmltext Извлекает содержимое столбца, уже содержащее XML-данные, и добавляет их в документ Cdata Данные столбца представлены в виде секции CDATA в итоговом документе Hide Скрывает столбец, присутствующий в универсальной таблице, из итогового XML-фрагмента id, idref, idrefs Вместе с XMLDATA указывают на взаимосвязи элементов различных XML-фрагментов Наиболее часто используется директива е I ement. Выбрав ее, вы кодируете и возвращаете данные в виде вложенных элементов, а не атрибутов. Например, допустим, что для Customer Id и CompanyName необходимо дополнительно получить данные ContactName, причем в виде вложенного элемента, а не атрибута. Вот как может выглядеть запрос: SELECT I AS Tag. NULL AS Parent, Customerld AS [Customers!HCustomerld]. CompanyName AS [Customers!1]. ContactName AS [Customers!l!ContactName!el ement] FROM Customers FOR XML EXPLICIT (Результаты сокращены и отформатированы) XML_F52E2B61-18Al-lldl-B105-00805F49916B <Customers CustomerId="ALFKI">Alfreds Futterkiste <ContactName>Maria Anders</ContactName> </Customers> <Cust"omers CustomerId="ANATR">Ana Trujillo Emparedados у <ContactName>Ana Truj i11o</ContactName> </Customers> <Customers CustomerId="ANTON">Antonio Moreno Taqueria <ContactName>Antonio Moreno</ContactName> </Customers> <Customers CustomerId="AROUT">Around the Horn <ContactName>Thomas Hardy</ContactName> </Customers> <Customers CustomerId="BERGS">Berglunds snabbkop <ContactName>Christina Berglund</ContactName> </Customers> <Customers CustomerId="WILMK">Wilman Kala <ContactName>Matti Karttunen</ContactName> </Customers> <Customers CustomerId="WOLZA">Wolski Zajazd <ContactName>Zbyszek Piestrzeniewicz</ContactName> </Custoiriers> Как видно, элемент ContactName вложен в элемент Customers. Директива е I ements кодирует данные, которые будут возвращены. Можно вернуть те же самые данные без кодирования при помощи директивы xm I:
Установление взаимосвязей данных 347 SELECT 1 AS Tag. NULL AS Parent, Customerld AS [Customersll.'Customerld]. CompanyName AS [Customers!!]. ContactName AS [Customers!l!ContactName!xml] FROM Customers FOR XML EXPLICIT Использование директивы xm I (выделена полужирным шрифтом) приводит к тому, что столбцы возвращаются без предварительного кодирования специальных символов, которые могут в них содержаться. Установление взаимосвязей данных До сих пор мы получали данные из одной таблицы, поэтому наши запросы EXPLIСIT не были очень сложными. Они будут оставаться такими же, даже если мы станем опрашивать несколько таблиц или если данные из каждой таблицы будут повторяться для каждого элемента верхнего уровня в XML-фрагменте. Так как значения столбцов из объединенных таблиц часто повторяются в итоговых наборах Transact-SQL запросов, то мы можем создать XML-фрагмент, который будет содержать повторяющиеся данные из различных таблиц в каждом элементе. Однако это не самый рациональный способ представления данных в XML. Вспомните, что язык XML поддерживает иерархические связи между элементами. Выявить эти связи можно при помощи запросов режима EXPL IСIT и операторов UN I ON. Вот пример: SELECT I AS Tag. NULL AS Parent, Customerld AS [Customers!1!Customerld]. CompanyName AS [Customers!1]. NULL AS [Orders!2!0rderld]. NULL AS [Orders!2!0rderDate!e1ement] FROM Customers UNION SELECT 2 AS Tag. 1 AS Parent, Customerld. NULL, OrderId, OrderDate FROM Orders ORDER BY [Customers!1!Customerld]. [Orders!2!0rderDate!element] FOR XML EXPLICIT Этот запрос делает несколько любопытных вещей. Во-первых, он связывает таблицы Customers и Orders при помощи общего столбца Customer I d. Обратите внимание на третий столбец в каждом из операторов SELECT. Он возвращает столбец Custome r I d из каждой таблицы. В столбцах Tag и Pa rent представлены детали взаимосвязи таблиц. Значения Tag и Pa rent во втором запросе связаны с первым запросом. Они указывают, что записи ORDER являются дочерними по отношению к CUSTOMER. Во-вторых, обратите внимание на предикат ORDER BY. Он упорядочивает элементы таблицы: сначала по полю Customer I d, затем по OrderDate для каждого заказа. Вот результирующий набор:
348 Глава 14. XML и SQL Server: получение данных (Результаты сокращены и отформатированы) XML F52E2B61-18Al-lldl-B105-00805F49916B <Customers CustomerId="ALFKI">A1freds Futterkiste Orders Orderld=0643"> OrderDate>1997-08-25T00:00:00</OrderDate> </0rders> <0rders Orderld=0692"> <OrderDate>1997-10-03T00:00:00</OrderDate> </0rders> <0rders Orderld=0702"> <0rderDate>1997-10-13T00:00:00</OrderDate> </0rders> Orders Orderld=0835"> <OrderDate>1998-01-15T00:00:00</OrderDate> </0rders> Orders Orderld=0952"> OrderDate>1998-03- 16T00:00:00</OrderDate> </0rders> Orders Orderld=1011"> OrderDate>1998-04-09T00:00:00</0rderDate> </0rders> </Customers> Oustomers CustomerId="ANATR">Ana Trujillo Emparedados у helados Orders Orderld=0308"> OrderDate>1996-09-18T00:00:00</OrderDate> </0rders> Orders Orderld=0625"> OrderDate>1997-08-08T00:00:00</OrderDate> </0rders> Orders Orderld=0759"> OrderDate>1997-11-28T00:00:00</OrderDate> </0rders> Orders Orderld=0926"> OrderDate>1998-03-04T00:00:00</OrderDate> </0rders> </Customers> Как вы видите, каждый заказ клиента вложен в свой элемент. Директива hide Директива h i de используется для скрытия столбца, присутствующего в универсальной таблице, в итоговом XML-фрагменте. Применяя эту функцию, вы можете упорядочить результирующий набор по полю, которое не будет присутствовать в итоговом XML-фрагменте. Если для слияния таблиц не был использован UN I ON, то сделать это будет несложно, так как данные можно упорядочить по любому полю. Однако если в запросе вы используете UN I ON, то столбец, по которому происходит сортировка, должен присутствовать в результирующем наборе. Директива hide позволяет выполнить это требование, не добавляя нежелательные данные в результаты. Пример: SELECT 1 AS Tag. NULL AS Parent. Customerld AS [Customers!l!CustomerId]. CompanyName AS [Customers!1], Postal Code AS [Customers!1!Postal Code!hide].
Установление взаимосвязей данных 349 NULL AS [Orders!2!0rderld]. NULL AS [Orders!2!0rderDate!element] FROM Customers UNION SELECT 2 AS Tag. 1 AS Parent. Customer-Id, .,,••, , . NULL, ' ' .•■••. NULL, Orderld, OrderDate FROM Orders ORDER BY [Customers!l!CustomerId]. [Orders!2!OrderDate!element]. [Customers!1! Postal Code! hi de] FOR XML EXPLICIT Обратите внимание на то, что директива h i de (выделена полужирным шрифтом) включена в заголовок пятого столбца. Это позволяет указать пятый столбец в ORDER BY, не возвращая его в результирующий XML-фрагмент. Директива cdata Нередко в XML-документ необходимо включить неразобранные данные. CDATA представляет собой неразобранные символьные данные. Секция CDATA выводится XML-анализатором в том виде, в каком была получена. Кодирования или какого- либо иного преобразования не происходит. Секции CDATA позволяют без последствий включать те секции XML, которые могли бы запутать XML-анализатор, Для того чтобы отобразить секцию CDATA в запросе EXPLIСIT, следует использовать директиву cdata. Например: SELECT 1 AS Tag. NULL AS Parent. Customerld AS [Customers!l!CustomerId]. CompanyName AS [Customers!1], Fax AS [Customers!1!Icdata] FROM Customers FOR XML EXPLICIT (Результаты сокращены и отформатированы) XML F52E2B61-18Al-lldl-B105-00805F49916B Customers CustomerId="ALFKI">Alfreds Futterkiste <![CDATA[030-0076545]]> </Customers> Customers CustomerId="ANATR">Ana Trujillo Emparedados у helados <![CDATA[E) 555-3745]]> </Customers> <Custorners CustomerId="ANTON">Antonio Moreno Taqueria </Customers> Customers CustomerId="AROUT">Around the Horn <![CDATA[A71) 555-6750]]> </Customers> «Customers CustomerId="BERGS">Berglunds snabbkop <![CDATA[0921-12 34 67]]> ■ . " </Customers> Как видите, каждое значение столбца Fax представлено в виде секции CDATA в итоговом XML-фрагменте. Обратите внимание на то, что в заголовке столбца cdata
350 Глава 14. XML и SQL Server: получение данных имя атрибута не указано, поскольку для секций CDATA имена атрибутов недопустимы. Секции CDATA представляют собой неразобранные секции документа, поэтому XML-анализатор не сможет обработать содержащиеся в них атрибуты или элементы (если таковые имеются). Директивы id, id ref, idrefs Директивы id, id ref, id ref s можно использовать для представления реляционных данных в XML-документе. Расположенные в DTD или XML Schema, эти директивы устанавливают взаимосвязи между элементами. Их удобно использовать в тех случаях, когда вам необходимо обмениваться с коллегами сложными данными и минимизировать количество дублирования данных в документе. В запросах режима EXPLICIT можно использовать директивы id, id ref и id ref s для указания реляционных полей в XML-документе. Конечно, этот подход эффективен только в том случае, если используется схема для определения документа и идентификации столбцов, по которым строятся связи между сущностями. Опция XMLDATA позволяет создать вложенную схему для XML-фрагмента. Вместе с директивой id эта опция может определять реляционные поля в XML-фрагменте. Например: SELECT 1 AS Tag. NULL AS Parent. Customerld AS [Customers!l!CustomerId!id]. CompanyName AS [Customers!l!CompanyName]. NULL AS [Orders!2!0rderID], NULL AS [Orders!21 CustomerldMdref] FROM Customers UNION SELECT 2. NULL. NULL. NULL. OrderlO, Customerld FROM Orders ORDER BY [Orders!2!0rderID] FOR XML EXPLICIT. XMLDATA . (Результаты сокращены и отформатированы) XML F52E2B61-18Al-lldl-B105-00B05F49916B <Schema name="Schema2" xmlns="urn:schemes-mi crosoft- com:xml-data" xmlns:dt="urn:schemes-mi crosoft-com:datatypes"> <ElementType name="Customers" content="mixed" model="open"> <AttributeType name="CustomerId" dt:type="id"/> <AttributeType name="CompanyName" dt:type="string"/> <attribute type="CustomerId"/> <attribute type="CompanyName"/> </ElementType> <ElementType name="Orders" content="mixed" model="open"> <AttributeType name="OrderID" dt:type="i4"/> <AttributeType name="CustomerId" dt:type="idref"/> <attribute type="OrderID"/> attribute type="CustomerId"/> </ElementType> </Schema>
Схемы отображения 351 Customers xmlns="x-scnema:#Scnema2" CustomerId="ALFKI" CompanyName="Alfreds Futterkiste"/> Customers xmlns="x-scnema:#Schema2" CustomerId="ANATR" CompanyName="Ana Trujillo Emparedados у helados"/> <Customers xmlns="x-schema:#Schema2" CustomerId="ANTON" CompanyName="Antonio Moreno Taqueria"/> <Customers xmlns="x-schema:#Schema2" CustomerId="AROUT" CompanyName="Around the Horn"/> <0rders xmlns="x-schema:#Schema2" OrderID=0248" CustomerId="VINET"/> <0rders xmlns="x-schema:#Schema2" OrderID=0249" CustomerId="T0MSP7> <0rders xmlns="x-schema:#Schema2" OrderID=0250" CustomerId="HANAR"/> Orders xmlns="x-schema:#Schema2" OrderID=0251" CustomerId="VICTE"/> Orders xmlns="x-schema:#Schema2" OrderID=0252" CustomerId="SUPRD"/> Orders xmlns="x-schema:#Schema2" OrderID=0253" CustomerId="HANAR"/> Orders xmlns="x-schema:#Schema2" OrderID=0254" CustomerId="CHOPS"/> Orders xmlns="x-schema:#Schema2" OrderID=0255" CustomerId="RICSU"/> Обратите внимание на использование директив id и id ref в столбцах Custome rid таблиц Customers и Orders (выделены полужирным шрифтом). Эти директивы связывают две таблицы по общему полю Customerld. Если рассмотреть XML-фрагмент, возвращаемый запросом, то можно увидеть, что он начинается со схемы XML-Data, созданной директивой xmldata. В следующем XML-фрагменте есть ссылка на эту схему. Схемы отображения Мы обсуждали XML-схемы в главе 12, поэтому здесь мы не станем снова подробно говорить о них. Достаточно сказать, что XML-схемы — это XML-документы, определяющие тип данных, которые могут содержать другие XML-документы. Схемы призваны заменить устаревшую технологию DTD, изначально использовавшуюся для этих целей. Схемы проще в использовании, так как сами состоят из XML. По своей природе схемы определяют форматы обмена документами. Так как схемы определяют, что документ может содержать, а что нет, — компании, желающие обмениваться XML-данными, должны согласиться с некой общей схемой. Схемы позволяют компаниям, работающим в абсолютно разных сферах бизнеса, без проблем обмениваться информацией. Листинг 14.1 содержит пример XDR- схемы. Листинг 14.1. ProductsCat.xdr <?xml version=.0" ?> <Schema name="NorthwindProducts" xml ns="urn:schemas-mi crosoft-com:xml-data" xmlns:dt="urn:schemas-microsoft-com:datatypes'^ <ElementType name="Description" dt:type="string" /> f,.': OlementType name="Price" dt:type="fixed.l9.4" /> <ElementType name="Product" model="closed"> <AttributeType name="ProductCode" dt:type="string" /> attribute type="ProductCode" required="yes" /> <element type="Description" minOccurs="l" maxOccurs="l" /> <element type="Price" minOccurs="l" maxOccurs="l" /> </ElementType> <ElementType name="Category" model="closed"> продолжением
352 Глава 14. XML и SQL Server: получение данных Листинг 14.1 {продолжение) <AttributeType name="CategoryID" dt:type="string" /> <AttributeType name="CategoryName" dt:type="string" /> attribute type="CategoryID" required="yes" /> <attribute type="CategoryName" required="yes" /> <element type="Product" minOccurs="l" maxOccurs="*" /> </ElementType> <ElementType name="Catalog" model="closed"> <element type="Category" minOccurs="l" maxOccurs="l" /> </ElementType> </Schema> Эта схема определяет, как может выглядеть каталог продуктов (мы используем таблицы и данные базы Northwind). Она использует пространство имен datatypes (выделено полужирным шрифтом) для определения допустимых типов данных элементов и атрибутов документа. Каждое вхождение dt: в документе есть ссылка на пространство имен datatypes. Использование замкнутой модели гарантирует, что только элементы из схемы могут быть применены в документе, использующем эту схему. В листинге 12.2 показан XML-документ, использующий Р roductsCat. xdr. Листинг 14.2. ProductsCat.xml <?xml version=.0" ?> <Catalog xmlns="x-schema:http:/7local host/ProductsCat.xdr"> <Category CategoryID="l" CategoryName="Beverages"> <Product ProductCode="l"> <Description>Chai</Description> <Price>18</Price> </Product> <Product ProductCode="> <Descri pti on>Chang</Descri pti on> <Price>19</Price> </Product> </Category> <Category CategoryID=" CategoryName="Condiments"> <Product ProductCode="> <Description>Aniseed Syrup</Descnption> <Price>10</Price> </Product> </Category> </Catalog> Если скопировать эти файлы в корневой каталог вашего веб-сервера и обратиться к такому URL: http://localhost/ProductsCat.xml. то в браузере появится следующий документ: <?xml version-.0" ?> <Cata1og xmlns="x-schema:http://1ocalhost/ProductsCat.xdr"> <Category CategoryID="l" CategoryName="Beverages"> <Product ProductCode="l"> <Description>Chai</Description> <Price>18</Price> </Product> <Product ProductCode="> <Description>Chang</Description> <Price>19</Price> </Product> </Category>
Аннотированные схемы 353 <Category CategoryID=" CategoryName="Condiments"> <Product ProductCode="> <Description>Aniseed Syrup</Description> <Price>10</Price> </Product> </Category> . ,,. . - . </Catalog> У Вы уже знаете, что XML-данные можно извлекать и форматировать разными способами. Одной из проблем при обмене данными в формате XML является проблема их гибкости. Однако схемы отображения помогают решить ее. Они позволяют получать данные из базы в требуемом формате. Они позволяют отображать столбцы и таблицы как атрибуты и элементы. Простейший способ использовать схему для отображения данных из SQL Server как сущностей XML — использовать режим отображения по умолчанию. Это означает, что каждая таблица становится элементом, каждый столбец — атрибутом. Вот схема, при помощи которой мы выполним это: <?xml version=.0"?> <Schema name="customers" xmlns="urn:schemas-mi crosoft-com:xml-data"> <ElementType name="Customers"> <AttributeType name="CustomerId"/> <AttributeType name="CompanyName"/> </ElementType> </Schema> Здесь из таблицы Customers извлекаются только два столбца. Если сохранить эту схему под виртуальным именем схем, созданным ранее, и выполнить запрос по URL, то вы увидите простой XML-документ, содержащий данные из таблицы Customers с ориентированным на атрибуты отображением. Для отображения столбца таблицы как элемента в результирующем XML-документе используется элемент схемы ElementType. Например: <?xml version-.0"?> <Schema name="customers" xmlns="urn:schemas-microsoft-com:xml-data"> <ElementType name="Customers"> <ElementType name="CustomerId" content="text0nly'7> <ElementType name="CompanyName" content="textOnly"/> </ElementType> </Schema> Обратите внимание на использование атрибута content="textOnly" с каждым элементом. Применяя его вместе с элементом ElementType, вы гарантируете отображение столбца как элемента в результирующем XML-документе. Заметьте, что соответствующие каждому столбцу элементы пусты. Они содержат только атрибуты, а не данные. Аннотированные схемы Аннотированная схема представляет собой схему отображения со специальными аннотациями из пространства имен XML-SQL, которые связывают элементы и атрибуты с таблицами и столбцами. Вот код, который используется в знакомом нам примере со списком клиентов: 17 Чяг 983
354 Глава 14. XML и SQL Server: получение данных <?xml version-.0"?> <Schema name="customers" xmlns="urn: schemes-mi crosoft-com.-xml-data''> xmlns:sql=''urn:schemas-microsoft-com:xml-sql"> <ElementType name="Customer" sql:relation="Customers"> <AttributeType name="CustomerNumber" sql:field="CustomerId"/> <AttributeType name="Name" sql :field="CompanyName'7> </ElementType> </Schema> Во-первых, обратите внимание на ссылку на пространство имен XML-SQL в начале схемы. Так как далее будет использоваться пространство имен XML-SQL, то в начале документа на него есть ссылка. Во-вторых, посмотрите на атрибут sql: relation первого элемента ElementType. Он свидетельствует о том, что элемент Customer в результирующем документе связан с таблицей Customers в базе данных. Это позволит вызывать элемент, указывая любое его имя. В-третьих, ссылки sql: field подтверждают, например, что элемент CustomerNumbe г относится к столбцу Customerld соответствующей таблицы. Дело усложняется, когда в запросе участвует несколько таблиц, но сама идея проста: аннотированная схема позволяет определить точные соответствия между сущностями документа и сущностями базы данных. Итоги В этой главе вы подробно изучили FOR XML и познакомились со схемами отображения. Вы узнали, что: ■ FOR XML позволяет переводить результирующий набор SQL Server в XML, предоставляя широкие возможности для управления формируемым XML-фрагментом; ■ схемы отображения предоставляют механизм для преобразования данных SQL Server в формат XML; ■ схемы отображения позволяют установить более полный контроль над результирующим XML, чем FOR XML, но они сложнее в использовании.
15 XML и SQL Server: OPENXML В сражении используй обычные силы — чтобы участвовать; силы, превосходящие противника, — чтобы победить. Сан Цзу] В этой главе мы поговорим о функции SQL Server 0PENXML() и о том, как она применяется для чтения XML-документов. Мы также затронем новые особенности и изменения в XML for SQL Server Web Release 1. Это последняя глава, касающаяся работы с XML в SQL Server. 0PENXML() — встроенная функция Transact-SQL, которая может возвращать XML- документ как набор строк. Вместе с sp_xm I _preparedocument и sp_xm I _removedocument, 0PENXML() позволяет преобразовывать нереляционные XML-документы в реляционные элементы, которые могут быть вставлены в таблицы. Функция 0PENXMLQ хорошо описана в Books Online, так что я не буду повторяться. Вот простой пример использования 0PENXML(): DECLARE @hDoc int EXEC sp_xml_preparedocument @nDoc output. '<songs> <songxname>Somebody to Love</name></song> <song><name>These Are the Days of Our Lives</name></song> <song><name>Bicycle Race</name></song> <song><name>Who Wants to Live Forever</name></song> <song><name>I Want to Break Free</name></song> <song><name>Friends Will Be Friends</name></song> </songs>' SELECT * FROM OPENXML(@hdoc, '/songs/song', 2) WITH (name varchar(80)) EXEC sp_xml_removedocument @hDoc (Результаты) name Somebody to Love These Are the Days of Our Lives Bicycle Race Who Wants to Live Forever I Want to Break Free Friends Will Be Friends Tzu, Sun. The Art of War. Cambridge, England: Oxford University Press, 1963. C. 91.
356 Глава 15. XML и SQL Server: OPENXML Если вы хотите использовать OPENXMLO, вы должны сделать следующее: 1. Вызвать sp_xm Lpreparedocument для загрузки XML-документа в память. При этом вызывается MSXML DOM-napcep для преобразования документа в дерево, к которому затем можно будет обращаться с помощью XPath-запро- сов. Указатель на это дерево возвращается процедурой в переменной типа integer. 2. Вызвать SELECT из OPENXMLO, передав дескриптор, который вы получили на этапе 1. 3. Включить XPath-синтаксис в вызов OPENXMLO для того, чтобы указать, к каким узлам вы хотите получить доступ. 4. Можно указать часть WITH для отображения XML-документа в схему определенной таблицы. Это может быть полная схема таблицы или ссылка на саму таблицу. Функция 0PENXML() очень гибкая, так что некоторые из вышеописанных этапов имеют вариации, однако это основной процесс разбиения и использования XML- документов с 0PENXML(). Вот вариант предыдущего запроса, в котором используется таблица, чтобы определить схему для отображения документа: USE tempdb . . • , GO create table songs (name varchar(80)) go DECLARE @hDoc int EXEC sp_xml_preparedocument @hDoc output. '<songs> <song><name>Somebody to Love</name></song> <song><name>These Are the Days of Our Lives</name></song> <song><name>Bicycle Race</name></song> <song><name>Who Wants to Live Forever</name></song> <song><name>I Want to Break Free</name></song> <song><name>Friends Will Be Friends</name></song> </songs>' SELECT * FROM OPENXML(@hdoc. '/songs/song'. 2) WITH songs EXEC sp_xml_removedocument @hDoc GO DROP TABLE songs (Результаты) name Somebody to Love These Are the Days of Our Lives Bicycle Race Who Wants to Live Forever I Want to Break Free Friends Will Be Friends Вы также можете использовать часть WITH для детального сопоставления XML- документа и таблиц базы данных. Вот пример: DECLARE @hDoc int EXEC sp_xml_preparedocument @hDoc output, '<songs> <artist name="Johnny Hartman">
XML и SQL Server: OPENXML 357 <song> <name>It Was Almost Like a Song</name></song> <song> <name>I See Your Face Before Me</name></song> <song> <name>For All We Know</name></song> <song> <name>Easy Living</name></song> </artist> <artist name="Harry Connick. Jr."> <song> <name>Sonny Cried</name></song> <song> <name>A Nightingale Sang in Berkeley Square</name></song> с <song> <name>Heavenly</name></song> <song> <name>You Didn''t Know Me When</name></song> </artist> </songs>' SELECT * FROM OPENXML(@hdoc. '/songs/artist/song'. 2) WITH (artist varcharOO) '../(Pname', song varcharE0) 'name') EXEC sp_xml_removedocument @hDoc (Результаты) artist song Johnny Hartman It Was Almost Like a Song Johnny Hartman I See Your Face Before Me Johnny Hartman For All We Know Johnny Hartman Easy Living Harry Connick. Jr. Sonny Cried Harry Connick. Jr. A Nightingale Sang in Berkeley Square Harry Connick, Jr. Heavenly Harry Connick. Jr. You Didn't Know Me When Обратите внимание на то, что ссылки на атрибуты имеют префикс @. В этом примере мы используем XPath-запрос, который проходит дерево до элемента Song, затем получает ссылку на атрибут с названием Name в родительском элементе песни Art i st. Для второго столбца мы получаем дочерний элемент песни, который также называется Name. Вот другой пример: DECLARE @hDoc int EXEC sp_xml_preparedocument @hDoc output. '<songs> <artist> <name>Johnny Hartman</name> <song> <name>It Was Almost Like a Song</name></song> <song> <name>I See Your Face Before Me</name></song> <song> <name>For All We Know</name></song> <song> <name>Easy Living</name></song> </artist> <artist> <name>Harry Connick, Jr.</name> <song> <name>Sonny Cried</name></song> <song> <name>A Nightingale Sang in Berkeley Square</name></song> , <song> <name>Heavenly</name></song> <song> <name>You Didn''t Know Me When</name></song> </artist> </songs>' SELECT * FROM OPENXML(@hdoc, '/songs/artist/name'. 2) WITH (artist varcharOO) song varcharE0) '../song/name') EXEC sp_xml_removedocument @hDoc (Результаты)
358 Глава 15. XML и SQL Server: OPENXML artist song Johnny Hartman It Was Almost Like a Song Harry Connick, Jr. Sonny Cried Обратите внимание на то, что мы получили только две записи. Почему? Потому что наш XPath-шаблон доходит до узла Art i st/Name, которых всего два. В дополнение к получению элемента Name каждого исполнителя мы также получаем название его первого элемента Song. В предыдущем запросе XPath-шаблон проходил элементы Song, которых всего восемь, а затем получал родительский узел каждой песни (ее узел Art i st) при помощи указателя «..». Обратите внимание на использование в предыдущем запросе спецификатора«.» в строке XPath. Это просто ссылка на текущий элемент. Он необходим, потому что мы изменяем название текущего элемента с name на a rt i st. Помните об этом способе, когда вам понадобится переименовать элемент, получаемый с помощью 0PENXML(). Параметр flags Параметр f I ags 0PENXML() позволяет указать, как будет обрабатываться документ: на основе атрибутов или на основе элементов, или их комбинации. До этого мы передавали значение f I ags равное 2, то есть использовалось отображение, ориентированное на элементы. Вот пример отображения, ориентированного на атрибуты: DECLARE @hDoc int EXEC sp_xml_preparedocument @hDoc output. '<songs> <artist name="Johnny Hartman"> <song name="It Was Almost Like a Song"/> <song name-"I See Your Face Before Me"/> <song name="For All We Know"/:» <song name="Easy Living"/> </artist> <artist name="Harry Connick. Jr."> <song name="Sonny Cried"/> <song name="A Nightingale Sang in Berkeley Square"/> <song name="Heavenly"/> <song name="You DidrT't Know Me When"/> </artist> </songs>' SELECT * FROM OPENXML(@hdoc. '/songs/artist/song', 1) WITH (artist varcharOO) '../@name'. song varcharE0) '@name') EXEC sp_xml_removedocument @hDoc (Результаты) artist song Johnny Hartman It Was Almost Like a Song Johnny Hartman I See Your Face Before Me Johnny Hartman For All We Know Johnny Hartman Easy Living Harry Connick, Jr. Sonny Cried Harry Connick. Jr. A Nightingale Sang in Berkeley Square
Формат Edge Table 359 Harry Connick. Jr. Heavenly Harry Connick, Jr. You Didn't Know Me When Формат Edge Table Вы можете полностью опустить часть WITH 0PENXML(), чтобы получить часть XML- документа в так называемом edge table-формате, — это по существу двумерное представление дерева XML. Вот пример: DECLARE (PhDoc int EXEC sp_xml_preparedocument @hDoc output. '<songs> <artist name="Johnny Hartman"> <song> <name>It Was Almost Like a Song</name></song> <song> <name>I See Your Face Before Me</name></song> <song> <name>For All We Know</name></song> <song> <name>Easy Living</name></song> </artist> <artist name="Harry Connick. Jr."> <song> <name>Sonny Cried</name></song> <song> <naroe>A Nightingale Sang in Berkeley Square</name></song> <song> <name>Heavenly</name></song> <song> <name>You Didn''t Know Me When</name></song> </artist> </songs>' SELECT * FROM OPENXML(@hdoc, Vsongs/artist/song'. 2) EXEC sp_xml_removedocument @hDoc (Результаты сокращены) id 4 5 22 6 7 23 8 9 24 10 11 25 14 15 26 16 17 27 18 19 28 20 21 29 parentid 2 4 5 2 6 7 2 8 9 2 10 11 12 14 15 12 16 17 12 18 19 12 20 21 nodetype 1 1 3 1 1 3 1 1 3 1 1 3 1 1 3 1 1 3 1 1 3 1 1 3 local name song name #text song name #text song name #text song name #text song name #text song name #text song name #text song name #text
360 Глава 15. XML и SQL Server: OPENXML Вставка данных при помощи OPENXML() Учитывая, что 0PENXML() — табличная функция, будет естественно предположить, что вы захотите вставить результаты оператора SELECT с 0PENXML() в какую-нибудь таблицу. Есть несколько способов это сделать. Вы можете выполнить отдельный проход по XML-документу для каждой части, которую вы хотите выделить. Вам придется применить I NSERT...SELECT FROM 0PENXML() для каждой таблицы базы данных, в которую вы хотите вставить записи. Каждый раз вы будете получать разные части XML-документа для каждого прохода. Вот пример: USE tempdb GO CREATE TABLE Artists (Artistld varcharE), Name varcharOO)) GO CREATE TABLE Songs (Artistld varcharE), Songld int, Name varcharE0)) GO DECLARE @hDoc int EXEC sp_xml_preparedocument @hDoc output. '<songs> <artist id="JHART" name="Johnny Hartman"> <song id="l" name="It Was Almost Like a Song"/> <song id=" name="I See Your Face Before Me"/> <song id=" name="For All We Know"/> <song id=" name="Easy Living"/> </artist> <artist id="HCONN" name="Harry Connick. Jr."> <song id="l" name="Sonny Cried"/> <song id=" name="A Nightingale Sang in Berkeley Square"/> <song id=" name="Heavenly"/> <song id=" name="You Didn''t Know Me When"/> </artist> </songs>' INSERT Artists (Artistld, Name) SELECT id.name FROM OPENXML(@hdoc. '/songs/artist', 1) WITH (id varcharE) '(aid'. name varcharOO) '@name') INSERT Songs (Artistld. Songld. Name) SELECT artistid, id.name FROM OPENXML(@hdoc. '/songs/artist/song'. 1) WITH (artistid varcharE) '../(aid1. id int '@id'. name varcharE0) '@name') EXEC sp_xml_removedocument @hDoc GO SELECT * FROM Artists SELECT * FROM Songs GO DROP TABLE Artists, Songs
Вставка данных при помощи OPENXMLQ 361 (Результаты) Artistld Name JHART Johnny Hartman HCONN Harry Connick. Jr. Artistld Songld Name JHART 1 It Was Almost Like a Song JHART 2 I See Your Face Before Me JHART 3 For All We Know JHART 4 Easy Living HCONN 1 Sonny Cried HCONN 2 A Nightingale Sang in Berkeley Square HCONN 3 Heavenly HCONN 4 You Didn't Know Me When Как вы видите, мы осуществляем отдельный вызов 0PENXML() для каждой таблицы. Таблицы нормализованы, XML-документ — нет, так что мы разделяем его на несколько таблиц. Существует другой способ сделать то же самое, не используя при этом множественные вызовы 0PENXML(): USE tempdb GO CREATE TABLE Artists (Artistld varcharE). Name varcharC0)) GO CREATE TABLE Songs (Artistld varcharE). Songld int, » - Name varcharE0)) GO CREATE VIEW ArtistSongs AS SELECT a. Artist Id. a.Name AS ArtistName, s.Songld, s.Name as SongName FROM Artists a JOIN Songs s ON (a.Artistlchs.Artistld) GO CREATE TRIGGER ArtistSongsInsert ON ArtistSongs INSTEAD OF INSERT AS INSERT Arti sts SELECT DISTINCT Artistld. ArtistName FROM inserted INSERT Songs SELECT Artistld. Songld, SongName FROM inserted GO . , DECLARE @hDoc int ' EXEC sp_xml_preparedocument @hDoc output, '<songs> <artist id="JHART" name="Johnny Hartman"> , .,,• <song id="l" name="It Was Almost Like a Song"/> <song id=" name="I See Your Face Before Me"/> ■ <song id=" name="For All We Know"/> <song id=" name="Easy Living"/> </artist> 1 <artist id="HC0NN" name-"Harry Connick, Jr."> : • <song Id—"l" name="Sonny Cried"/>
362 Глава 15. XML и SQL Server: OPENXML <song id=" name="A Nightingale Sang in Berkeley Square"/> <song id=" name="Heavenly"/> <song id=" name="You Didn''t Know Me When"/> </artist> </songs>' INSERT ArtistSongs (Artistld. ArtistName. Songld. SongName) SELECT artistid, artistname. songid. songname FROM OPENXML(@hdoc. 7songs/artist/song'. 1) ' WITH (artistid varcharE) '../@id'. artistname varcharC0) '../@name', songid int '@id', songname varcharEQ) '@name') EXEC sp_xml_removedocument @hDoc GO SELECT * FROM Artists SELECT * FROM Songs GO DROP VIEW ArtistSongs GO DROP TABLE Artists. Songs (Результаты) Artistid Name HCONN Harry Connick. Jr. JHART Johnny Hartman Artistid Songid Name JHART JHART JHART JHART HCONN HCONN HCONN HCONN 1 2 3 4 1 2 3 4 It Was Almost Like a Song I See Your Face Before Me For All We Know Easy Living Sonny Cried A Nightingale Sang in Berkeley Square Heavenly You Didn't Know Me When Этот способ использует представление и триггер INSTEAD OF, чтобы избавиться от двух проходов с 0PENXML(). Мы используем представление для симуляции де- нормализованной структуры XML-документа, затем устанавливаем триггер I NSTEAD OF для вставки данных из XML-документа в это представление. На самом деле вся работа по разделению документа осуществляется в триггере, он делает это намного эффективнее, по сравнению с двойным вызовом OPENXML(). Триггер осуществляет два прохода по логической таблице inserted и распределяет содержащиеся там столбцы (которые являются зеркальным отображением столбцов представления) по двум отдельным таблицам. Web Release l Компания Microsoft объявила о своих намерениях обновлять поддержку XML в SQLServer, используя периодические обновления, называемые Web Releases. Когда я пишу эту книгу, первое уже находится в состоянии бета-тестирования. Оно будет выпущено к концу этого года. Обещают, что будет добавлено много новых
Web Release 1 363 функций для работы с XML в SQL Server. Вот некоторые из них, анонсированные Microsoft. ■ Поддержка апдейтограмов. Апдеитограмы — это документы, похожие на шаблоны, с помощью которых можно вставлять, обновлять и удалять данные SQL Server. В инструмент Configure SQL XML Support In IIS будет добавлена опция, позволяющая их применять. ■ Массовая загрузка XML (XML bulk load). Это будет СОМ-компонент, который вы сможете использовать, чтобы быстро загрузить данные из XML в базу данных, что-то наподобие команды T-SQL BULK INSERT. Этот способ будет более эффективным, по сравнению с 0PENXML(). ■ Поддержка дополнительных типов данных в схемах. В настоящее время аннотированные схемы SQL Server позволяют использовать аннотацию sq I: datatype для BLOB-типов, таких как text и i mage. Web Release 1 добавит поддержку всех типов данных SQL Server (например, i nt, varchar). ■ Усовершенствованные шаблоны. Microsoft анонсировала, что шаблоны будут усовершенствованы путем добавления нескольких пространств имен XML-SQL. Информация об этом пока довольно скудная, но кажется, что эти нововведения направлены на упрощение создания шаблонов. Одна из печальных особенностей написания технических книг заключается в том, что любая технология успеет измениться, пока вы пишете вашу книгу. Поэтому мы кратко обсудим два основных новшества в бета-версии Web Release l, которые лично я уже использую: апдеитограмы и компонент массовой загрузки XML. Помните, что это только бета-версия. К тому моменту, когда вы будете читать эту книгу, некоторые детали могут очень сильно измениться. Апдеитограмы Использование апдейтограмов — это метод обновления данных в базе данных SQL Server, основанный на XML. Это просто шаблоны со специальными атрибутами и элементами, которые позволяют указать, какие данные вы хотите обновить и как это сделать. Апдеитограмы содержат значения Before и After (До и После) данных, которые вы хотите изменить. Аапдейтограмы передаются в SQL Server так же, как шаблоны. Все механизмы выполнения, доступные при работе с шаблонами, работают и для апдейтограм. Вы можете передавать апдеитограмы по HTTP, сохранять их в файлы и выполнять их через URL. Вы можете выполнять апдеитограмы напрямую, используя ADO и OLEDB. Апдеитограмы: подробнее Апдеитограмы основаны на пространстве имен xml-updategram. Вы ссылаетесь на это пространство имен с помощью спецификатора xmlns: updg. Каждый апдейтограм содержит, по меньшей мере, один элемент Sync. Этот элемент указывает изменения, которые вы хотите осуществить, в виде элементов Before и After. Элемент Bef о re содержит значения данных до их изменения. Обычно он также содержит первичный ключ или ссылку на альтернативный ключ, чтобы SQL Server мог найти запись, которую необходимо изменить. Помните, что только одна запись может быть выбрана для обновления элементом Bef о re. Если элементы и атрибуты, вклю-
364 Глава 15. XML и SQL Server: OPENXML ченные в элемент Before, определяют более одной записи, вы получите сообщение об ошибке. Для удаления записей апдейтограм будет содержать снимок before без снимка after. Для вставки записей — снимок after без снимка before. И, конечно, для обновлений апдейтограм будет содержать и снимок before, и снимок after. Вот пример: <?xml version=.0"?> <employeeupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync> <updg:before> employees EmployeeID=7> </updg:before> <updg:after> employees City="Scotts Valley" Region="CA"/> </updg:after> </updg:sync> </employeeupdate> В этом примере мы изменяем значения столбцов Город (City) и Регион (Region) для Сотрудника (Emp I oyee) под номером 4 в таблице Emp I oyees базы данных No rthwi nd. Атрибут Emp I oyee ID в элементе Before определяет запись, которую надо изменить, а атрибуты City и Region в элементе After определяют, какие столбцы требуется изменить и какие значения им присвоить. Каждый пакет обновлений в элементе Sync рассматривается как отдельная транзакция. Либо все обновления, указанные в элементе Sync, проходят успешно, либо ни одно из них. Вы можете включить несколько элементов Sync, чтобы разбить обновления на несколько транзакций. Отображение данных Естественно, когда мы передаем серверу данные для обновления, удаления или вставки посредством XML, нам требуется каким-либо способом связать значения в XML-документе со столбцами в таблице базы данных. SQL Server предоставляет для этого два механизма: отображение по умолчанию и схемы отображения. Отображение по умолчанию Самый простой способ связать данные в апдейтограме со столбцами таблицы заключается в использовании отображения по умолчанию (так называемое внутреннее отображение). В этом случае теги верхнего уровня для элементов Before или Af te r считаются ссылкой на таблицу базы данных, а каждый подэлемент или атрибут — ссылкой на столбец с таким же названием в таблице. Вот пример, показывающий, как отобразить столбец Order ID в таблице Orders: Orders OrderID=0248"/> В этом примере XML-атрибут отображается в столбце таблицы. Вы также можете отобразить подэлементы в столбцах таблицы: <0rders> <0rderID>lQ248</0rderID> </0rders> Вам не требуется выбирать отображение на основе атрибутов или отображение на основе элементов. Вы можете свободно использовать и то и другое отображение внутри заданного элемента Before или After. Вот пример:
Web Release 1 365 Orders OrderID=0248"> <ShipCity>Reims</ShipCity> </0rders> Используйте 4-цифирные шестнадцатеричные UCS-2 коды для символов в названиях таблиц, которые недопустимы в элементах XML (например, пробелы). Например, для ссылки на таблицу Order Detai Is базы данных Northwind используйте: <0rder_x0020_Details OrderID=02487> Схемы отображения Вы также можете использовать схемы отображения для связи данных в апдейтограме с таблицами и столбцами в базе данных. Мы уже говорили о схемах отображения, поэтому я не хочу к ним возвращаться, скажу только, что для указания схемы отображения применяется атрибут sync's updg:mapping-schema. Вот пример, в котором определяется схема отображения для таблицы Orders: <?xml version="l.0"?> <orderupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync updg:mapping-schema="OrderSchema.xml"> <updg:before> Order OID=0248"/> </updg:before> <updg:after> <0rder City="Reims"/> </updg.-after> </updg:sync> </orderupdate> И вот схема отображения: <?xml version=.0"?> <Schema xmlns="urn:schemas-microsoft-com:xml-data" xmlns:sql="urn:schemas-microsoft-corn:xml-sql"> <ElementType name="Order" sql:relation="Orders"> <AttributeType name=ID"/> <AttributeType name="City"/> attribute type=ID" sql :field=rderID'7> •• ' <attribute type="City" sql:field="ShipCity"/> </ElementType> </Schema> Как вы видите, схема отображения преобразует представление XML-документа в представление таблицы Orders базы данных Northwind. Неопределенные значения Для хранения в базе данных отсутствующих или неподходящих значений используются неопределенные (NULL) значения. Для работы с NULL-значениями в апдейтограме используется атрибут nu I I va I ие элемента sync для определения названия неопределенного значения. Это название в апдейтограме используется везде, где необходимо указать NULL-значение. Вот пример: <?xml version=.0"?> <employeeupdate xmlns:updg="urn:schemas-microsoft-corn:xml-updategram"> <updg:sync updg:nul1value="N0NE"> <updg:before> Orders OrderID=0248'7> </updg:before>
366 Глава 15. XML и SQL Server: OPENXML <updg:after> <0rders ShipCity-"Reims" ShipRegion="NONE" ShipName="NONE"/> </updg:after> </updg:sync> </employeeupdate> Как видите, мы определили заполнитель NONE для NULL-значения. Затем мы использовали этот заполнитель, чтобы присвоить значение NULL столбцам Sh i pReg ion nShipName. Параметры Любопытный факт: параметры в апдейтограмах функционируют не так, как параметры шаблонов. Для указания параметров апдейтограма вместо символа @ используется знак доллара $: <?xm1 version=.0"?> <orderupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:header> <updg:param name="OrderID"/> <updg:param name="ShipCity"/> </updg:header> <updg:sync> <updg:before> <0rders 0rderID="$0rderID'7> </updg:before> <updg:after> <0rders ShipCity="$ShipCity"/> </updg:after> </updg:sync> ;■ </orderupdate> Эта особенность интересным образом сказывается на передаче значений в валюте в качестве параметров. Чтобы передать параметр типа currency для столбца таблицы (например, столбца Fre i ght таблицы Orders), вы должны отобразить данные, используя схему отображения. Для передачи параметра со значением NULL в апдеитограм включите атрибут- заполнитель неопределенного значения в элементе Header. После этого вы сможете передавать значение этого названия в апдеитограм в качестве неопределенного значения параметра. Это похоже на указание NULL-значения для столбца в апдей- тограме, единственное отличие заключается в том, что для значений столбцов вы указываете неопределенное значение в элементе Sync, а для параметров — в элементе Header. Вот пример: <?xml version=.0"?> <orderupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:header nullvalue="NONE"> <updg:param name="OrderID"/> <updg:param name="ShipCity"/> </updg:header> <updg:sync> <updg:before> Orders 0rderID="$0rderID'7> </updg:before> <updg:after> Orders ShipCity="$ShipCity"/> </updg:after>
Web Release 1 367 </updg:sync> </orderupdate> Этот апдейтограм принимает два параметра. Если передать значение NONE для соответствующего заказа, значение столбца Sh i pC i ty будет установлено как NULL. Обратите внимание на то, что мы не включили спецификатор xml-updategram (updg:), когда указывали заполнитель для неопределенного значения параметров в элементе Header апдейтограма. Обработка множества записей Мы уже упоминали о том, что каждый элемент Before может определять единственную запись. Это значит, что для обновления нескольких строк вы должны включить этот элемент для каждой строки, которую необходимо изменить. Атрибут Id Когда вы указываете несколько подэлементов в элементах Bef о re и Af te r, SQL Server требует, определить соответствие элементов Before и After. Один из способов сделать это — использовать атрибут Id. Атрибут Id позволяет указать уникальное строковое/символьное значение, которое можно использовать для определения соответствия Before и After элементов. Вот пример: <?xml version=.0"?> <orderupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync> <updg:before> <0rders updg:id="IDl" OrderID=0248"/> Orders updg:id="ID2" OrderID=0249"/> </updg:before> <updg:after> <0rders updg:id="ID2" ShipCity="Munster"/> <0rders updg:id="IDl" ShipCity="Reims"/> </updg:after> </updg:sync> </orderupdate> Здесь мы использовали атрибут Updg: i d для определения соответствия подэлементов в элементах Before и After. Даже если эти подэлементы указаны непоследовательно, SQL Server способен правильно обновлять записи. Множественные элементы Before и After Другой способ определить соответствие элементов Before и Af te r—указать несколько элементов Before и After (вместо того указания нескольких подэлементов). Для каждой записи, которую следует изменить, мы указываем отдельную пару элементов Before/After. Вот пример: <?xml version=.0"?> <orderupdate xmlns:updg="urn:schemas-mlcrosoft-com:xml-updategram"> <updg:sync> <updg:before> Orders OrderID=" 10248"/> </updg:before> <updg:after> Orders ShipCity="Reims"/> </updg:after> <updg:before> Orders OrderID=0249"/>
368 Глава 15. XML и SQL Server: OPENXML </updg:before> <updg:after> Orders ShipCity="Munster"/> </updg:after> </updg:sync> </orderupdate> Как вы видите, этот апдейтограм обновляет две записи. Для каждого обновления в нем используется отдельная пара элементов before/after. Результаты Результат, возвращаемый клиентскому приложению, которое запустило апдейтограм, — это XML-документ, содержащий пустой корневой элемент, указанный в апдейтограме. Например, мы получим такой результат после выполнения апдей- тограма orderupdate: <?xml version=.0"?> <orderupdate xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> </orderupdate> Любые ошибки, произошедшие во время выполнения апдейтограма, возвра- * щаются в виде элементов <?MSSQLError>, внутри корневого элемента апдейтограма. Identity-значения столбцов В реальных приложениях вам часто требуется получить i dent i ty-значение, сгенерированное SQL Server, для одной таблицы и вставить его в другую. Это особенно необходимо, когда вы вставляете данные в таблицу с первичным ключом, являющимся i dent i ty-столбцом, и в таблицу, которая ссылается на этот первичный ключ посредством ограничения внешнего ключа. Например, вставка данных в таблицы Orders and Order Detai Is базы данных Northwind. Как следует из названия, таблица Order Detai Is хранит подробную информацию о заказах в таблице Orders. Часть первичного ключа таблицы Order Detai Is — столбец Order ID таблицы Orders. Когда мы вставляем новую запись в таблицу Orders, нам необходимо иметь возможность получить это значение и вставить его в таблицу Order Detai Is. В Transact-SQL мы обычно решаем эту проблему, используя триггер INSTEAD OF insert или хранимую процедуру. Для решения такой проблемы в апдейтограме используется атрибут at- i dent ity. Так же как атрибут i d, at- i dent i ty служит заполнителем везде, где вы используете его значение в апдейтограме, SQL Server использует identity-значение для соответствующей таблицы (каждая таблица может иметь только один i dent i ty-столбец). Вот пример: <?xml version=.0"?> <orderinsert xmlns:updg=''urn: schemes-mi crosoft-com:xml-updategram''> <updg:sync> <updg:before> </updg:before> <updg:after> <0rders updg:at-identity="ID" ShipCity="Reims"/> <Order_x0020_Details OrderID="ID" ProductID=1" UnitPnce="$14.00" Quantity=2"/> <Order_x0020J)etails OrderID="ID" ProductID=2" UnitPrice="$9.80" Quantity=0"/> </updg:after> </updg:sync> </ordennsert>
Web Release 1 369 Здесь мы использовали строку ID для указания identity-столбца в таблице Orders. После того как значение ID присвоено, мы можем его использовать для вставки в таблицу Order Detai Is. Кроме того, что можно использовать значения i dent i ty-столбцов в любом месте апдейтограма, весьма вероятно, что вам потребуется вернуть его значение клиенту. Для этого можно воспользоваться атрибутом return i d элемента after, указав заполнитель at- i dent i ty в качестве его значения: <?xml version=.0"?> <orderinsert xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync> <updg:before> </updg:before> <updg:after updg:returnid-"ID"> Orders updg:at-identity"ID" ShipCity="Reims"/> <Order_x0020_Details OrderID="ID" ProductID="ll" UnitPnce="$14.00" Quantity=27> <Order_x0020_Details OrderID="ID" ProductID=2" UnitPrice="$9.80" Quantity=0"/> </updg:after> </updg:sync> </orderinsert> В результате выполнения этого апдейтограма мы получим такой XML-документ: <?xml version=.0"?> <orderinsert xmlns;updg="urn:schemas-microsoft-com:xml-updategram"> <returnid> <ID>10248</ID> </returnid> :' </orderinsert> Глобальные уникальные идентификаторы (GUID) Использование GUID (глобальных уникальных идентификаторов) в качестве ключевых значений в распределенных представлениях и в различных распределенных системах — обычное явление. Как правило, вы используете функцию NEW I D() Transact-SQL для генерации новых GUID. Эквивалент NEW I D() в апдейтограмах— атрибут Gu i d. Вы можете указать атрибут Gu i d, чтобы сгенерировать GUID для использования внутри элемента Sync. Так же как с I d, Nu I I va I ue и другими атрибута-. ми, представленными в этом разделе, атрибут Gu i d определяет заполнитель, который вы можете применять к другим элементам и атрибутам в апдейтограме для использования сгенерированного GUID. Вот пример: <orderinsert> xmlns:updg="urn:schemas-mi crosoft-com:xml-updategram"> <updg:sync> <updg:before> </updg:before> <updg:after> <0rders updg:guid="GUID"> <OrderID>GUID</OrderID> <ShipCity>Reims</ShipCity> </0rders> <Order_x0020_Details OrderID="GUID" ProductID="ll" UnitPrice="$14.00" Quantity=2"/> Order x0020 Details OrderID="GUID•' ProductID=2"
370 Глава 15. XML и SQL Server: OPENXML UnitPrice="9.80" Quantity»0"/> </updg:after> </updg-. sync> </orderinsert> Компонент XML Bulk Load Как мы увидели в предыдущих разделах, касающихся апдейтограмов и 0PENXML(), вставлять данные из XML в базы данных SQL Server сравнительно просто. Однако оба этих метода имеют один серьезный недостаток: их нельзя использовать для загрузки больших объемов данных. Так же как использование оператора INSERT Transact-SQL не совсем оптимально для загрузки большого количества записей, загрузка больших объемов XML-данных в SQL Server при помощи апдейтограмов и OPENXML() — процесс довольно медленный и ресурсоемкий. В Web Release 1 будет представлен новый механизм, созданный специально для решения этой проблемы. Этот механизм называется компонент XML Bulk Load. По сути, это СОМ-компонент, который можно вызывать из языков/инструментов, поддерживающих OLE-автоматизацию, таких как: Visual Basic, Delphi и даже Transact-SQL. Этот компонент представляет собой объектно-ориентированный интерфейс для массовой загрузки XML-данных, наподобие команды Transact-SQL BULK INSERT. Использование компонента XML Bulk Load Первый шаг при использовании компонента XML Bulk Load — определить схему отображения для связи импортируемых XML-данных с таблицами и столбцами в вашей базе данных. Когда компонент загружает ваши XML-данные, он читает их как поток и использует схему отображения, чтобы решить, куда в базе данных поместить эти данные. Схема отображения определяет контекст каждой записи, добавляемой компонентом XML Bulk Load. Как только прочитан закрывающий тег записи, соответствующие данные записываются в базу данных. Доступ к компоненту может быть получен с помощью интерфейса SQLXMLBulkLoad СОМ-объекта SQLXMLBu I kLoad. Первое, что вам надо сделать, — подключить его к базе данных, используя строку соединения OLE-DB или присвоив свойству Connect i onCommand существующий объект ADO Command. Второе — вызвать метод Execute. Вот код на VBScript, иллюстрирующий это: Set objBulkLoad = CreateObjectt"SQLXMLBulkLoad.SQLXMLBulkLoad") ODjBulkLoad.ConnectionString = _ "provnder=SQLOLEDB;data source=KUFNATHE;database=Northwind;" & _ "Integrated Security=SSPI:" objBulkLoad.Execute d:\xml\OrdersSchema.xml. d:\xml\0rders0ata.xml Set objBulkLoad = Nothing Также для загрузки вы можете указать XML-поток (вместо файла), чтобы упростить процесс передачи данных между различными СУБД (поддерживающими XML). Загрузка фрагментов XML Установка значения свойства XMLFragment в TRUE позволяет загрузить данные из фрагмента XML (XML-документа без корневого элемента, например, такого, ко-
Web Release 1 371 торый возвращается при использовании расширения Transact-SQL FOR XML). Вот пример: Set objBulkLoad = С reateObj ect("SQLXMLBul kLoad.SQLXMLBul к Load") objBulkLoad.Connect!onString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind;" & _ "Integrated Security-SSPI;" objBulkLoad.XMLFragment = True objBulkLoad.Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersFrag.xml Set objBulkLoad = Nothing Проверка ограничений По умолчанию компонент XML Bulk Load не учитывает check-ограничения и ограничения ссылочной целостности. Проверка ограничений во время загрузки данных значительно замедляет процесс, так что этот компонент не проверит ограничения, если вы ему об этом не скажете. Вам может понадобиться проверить ограничения только тогда, когда вы загружаете данные напрямую в ваши рабочие таблицы и вам необходимо удостовериться в том, что целостность ваших данных не нарушена. Для того чтобы компонент учитывал ограничения при загрузке данных, установите значение его свойства CheckConst ra i nts в TRUE, как показано ниже: Set objBulkLoad = CreateObjectC'SQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad. Connecti onStri ng = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind;" & _ "Integ rat ed Sec u г i ty=SSPI;" objBul kLoad.CheckConstraints = True objBul kLoad.Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Дублирующиеся ключи Обычно необходимо остановить процесс загрузки, если встречается повторяющийся ключ. Чаще всего это означает, что вы получили неправильные и искаженные данные, и прежде, чем продолжить, вам следует посмотреть их источник. Есть, однако, и исключения. Например, вы получаете ежедневные данные из внешнего источника, содержащие таблицу целиком. Ежедневно добавляется несколько новых записей, но большая часть данных XML-документа уже содержится в вашей таблице. Вам необходимо загрузить новые записи, но внешний источник, который предоставляет вам данные, может не знать, какие записи у вас уже есть, а каких еще нет. Этот внешний источник может предоставлять данные многим компаниям и ему может быть неизвестно, какую информацию содержит именно ваша база данных. В этом случае перед загрузкой вы можете установить свойство I gno reDup I i cateKeys, и компонент будет игнорировать повторяющиеся значения ключа. Загрузка не остановится, если встретит повторяющийся ключ. Запись, содержащая этот ключ, будет проигнорирована, а записи с неповторяющимися значениями ключей будут загружены. Вот пример: Set objBulkLoad = CreateObjectC"SQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad.ConnectionString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind:" & _ "Integrated Security=SSPI;" • ' ' objBulkLoad.IgnoreDuplicateKeys = True objBulkLoad.Execute d:\xml\OrdersSchema.xml. d:\xml\OrdersData.xml Set objBulkLoad = Nothing
372 Глава 15. XML и SQL Server: OPENXML Когда свойство I gnoreDup I i cateKeys имеет значение TRUE, вставка, которая приведет к появлению повторяющегося ключа, осуществлена не будет, но процесс массовой загрузки не будет прекращен. Оставшиеся записи будут обработаны, как если бы никакой ошибки и не случилось. Identity-столбцы Свойство Keep I dent i ty объекта SQLXMLBu I kLoad по умолчанию установлено как TRUE. Это означает, что значения для i dent i ty-столбцов в ваших XML-данных будут загружены в базу данных, а не сгенерированы SQL Server «на лету». Обычно именно это вам и требуется, но можно установить значение этого свойства, равное FALSE, чтобы SQL Server генерировал эти значения. Однако есть несколько проблем, связанных с использованием данного свойства. Во-первых, когда значение Keep Identity равно TRUE, SQL Server использует команду SET I DENT I TY_ INSERT, чтобы разрешить вставку i dent i ty-значений в таблицу. Команда SET I DENT I TY_ I NSERT предъявляет несколько требований, связанных с правами: по умолчанию эту команду могут выполнять члены роли sysadm i n или ролей базы данных db_owner и db_dd I adm i n, а также владелец таблицы. Это значит, что пользователь, который не является владельцем таблицы, а также не является sysadm i n, dbo или DDL-администратором, скорее всего не сможет загружать данные при помощи компонента XML Bulk Load. Недостаточно просто иметь права bulk admin. Другая проблема заключается в том, что чаще всего i dent i ty-значения требуется сохранить при загрузке данных в таблицу, имеющую зависимые таблицы. Если вы разрешите SQL Server генерировать эти значения, последствия могут быть ужасающими. Вы можете нарушить связи между таблицами без надежды на их восстановление. Если первичный ключ родительской таблицы является i dent i ty- столбцом и Keep I dent i ty при загрузке установлено как FALSE, вы не сможете ресин- хронизировать ее с данными, которые вы загружаете в дочернюю таблицу. К счастью, значение Keep I dent i ty no умолчанию установлено как TRUE, так что можно не беспокоиться. Но все же лишний раз задумайтесь, что вы делаете, если решите установить значение Keep I dent i ty в FALSE. Вот код, показывающий использование свойства Keep I dent i ty: Set objBulkLoad = CreateObject("SQLXMLBulkLoad.SQLXMLBu!kLoad") objBulkLoad.ConnectionString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind;" &_ "Integrated Security=SSPI;" objBulkLoad.KeepIdentity = False objBulkLoad.Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Еще следует помнить, что Keep I dent i ty — двоичная опция: она либо включена, либо нет. Независимо от того, установите вы ее как TRUE или как FALSE, это повлияет на каждый объект, в который вставляются записи этой массовой загрузкой. Вы не можете параллельно сохранить i dent i ty-значения для одних таблиц и позволить SQL Server сгенерировать их для других. NULL-значения Для столбца, не отображенного в схеме, SQLXMLBu I kLoad вставляет значение этого столбца по умолчанию. Если столбец не имеет значения по умолчанию, указыва-
Web Release 1 373 ется значение NULL. Если столбец не может принимать значение NULL, процесс останавливается и появляется сообщение об ошибке. Использование свойства KeepNul Is позволяет сообщить механизму массовой загрузки то, что значение NULL необходимо указать вместо значения столбца по умолчанию, если столбец не отображен в схеме. Вот код, демонстрирующий, как это сделать: Set objBulkLoad = CreateObjectC'SQLXMLBulkLoad.SQLXMLBu!kLoad") objBul kLoad.Connecti onStri ng = _ '•provider=SQLOLEDB;data source=KUFNATHE;database-Northwind;" & _ "Integrated Security=SSPI;" objBul kLoad.KeepNulls = True objBulkLoad.Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Блокировка таблицы Как и другие механизмы массовой загрузки SQL Server, вы можете настроить SQLXMLBu I kLoad таким образом, что таблица будет блокироваться перед загрузкой в нее данных. Этот способ более эффективен, чем использование детальных блокировок, однако у него есть недостаток: пользователи не смогут получить доступ к таблице во время работы процесса массовой загрузки. Для принудительной блокировки таблицы установите значение свойства ForceTab I eLock как TRUE в процесс массовой загрузки, что показано ниже: Set objBulkLoad = CreateObjectC'SQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad.ConnectionString = "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind:" & _ "Integrated Security=SSPI;" objBulkLoad.ForceTab! eLock = True objBul kLoad. Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Транзакции По умолчанию операции массовой XML-загрузки выполняются не в транзакции. То есть если в процессе загрузки произошла ошибка, записи, загруженные до этого момента, останутся в базе данных. Это наиболее быстрый способ, но его недостаток состоит в том, что таблица может оказаться не полностью загруженной. Для того чтобы операция массовой загрузки выполнялась как одна транзакция, установите свойство Transact i on объекта SQLXMLBu I kLoad как TRUE перед вызовом Execute. Когда свойство Transact i on имеет значение TRUE, все операции вставки кэширу- ются во временный файл, перед тем как будут загружены в SQL Server. Вы можете контролировать местонахождение этого файла с помощью свойства TempF i I ePath. TempFi lePath имеет значение, только если свойство Transaction установлено как TRUE. Если при этом значение TempF i I ePat h не установлено, используется путь, указанный в переменной окружения temp на сервере. Должен предупредить, что массовая загрузка внутри транзакции идет намного медленнее, чем вне транзакции. Вот почему компонент по умолчанию не загружает данные внутри транзакции. К тому же вы не можете загружать двоичные XML- данные внутри транзакции, так что помните об этом. Вот код, иллюстрирующий массовую загрузку с использованием транзакции: Set objBulkLoad = CreateObjectCSQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad.ConnectionString = _
374 Глава 15. XML и SQL Server: OPENXML "provider=SQLOLEDB;data source-KUFNATHE;database=Northwind;" & "Integrated Security=SSPI;" objBulkLoad.Transaction = True objBulkLoad.TempFilePath = "c:\temp\xmlswap" objBulkLoad.Execute d: \xml\OrdersSchema.xral, d:\xml\OrdersData.xml Set objBulkLoad = Nothing В этом примере SQLXMLBu I kLoad устанавливает собственное соединение с сервером при помощи OLE-DB, так что он работает внутри своего контекста транзакций. Если в процессе массовой загрузки возникает ошибка, компонент откатывает свою собственную транзакцию. Когда SQLXMLBu I kLoad использует существующее OLE-DB соединение, установленное в свойстве Connect i onCommand, контекст транзакции принадлежит этому соединению и таким образом контролируется клиентским приложением. После того как процесс массовой загрузки завершится, клиентское приложение должно завершить или откатить транзакцию. Вот пример: On Error Resume Next Err.Clear Set objCmd = CreateObjectC'ADODB.Command") objCmd.ActiveConnection- __ "provider=SQLOLEDB;data source-KUFNATHE;database=Northwi nd:" & "Integrated Security=SSPI;" Set objBulkLoad = CreateObjectrSQLXMLBulkLoad.SQLXMLBulkLoad") objBulkLoad.Transaction = True objBuikLoad.ConnectionCommand = objCmd objBul kLoad.Execute d:\xml\OrdersSchema.xml. d:\xml\OrdersData.xml If Err.Number = 0 Then objCmd.ActiveConnection.CommitTrans Else objCmd.Acti veConnecti on.RollbackTrans End If Set objBulkLoad = Nothing Set objCmd = Nothing Помните, когда вы используете свойство Connect i onCommand, значение Transact i on необходимо установить как TRUE. Ошибки Компонент XML Bulk Copy поддерживает регистрацию сообщений об ошибках в файл, указанный в свойстве ErrorLogFi le. Этот файл представляет собой XML-документ, содержащий любые ошибки, которые произошли в процессе массовой загрузки. Вот код, который демонстрирует использование этой возможности: Set objBulkLoad = CreateObject("SQLXMLBulkLoad.SQLXMLBulkLoad") objBulkLoad.ConnectionString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind:" & _ "Integrated Security=SSPI:" objBul kLoad.ErrorLogFi le = "c:\temp\xmlswap\errors.xml" objBulkLoad.Execute d:\xml\OrdersSchema.xml. d:\xml\OrdersData.xml Set objBulkLoad = Nothing Указанный файл будет содержать элементы Record для каждой ошибки, произошедшей в процессе последней массовой загрузки. Наиболее частые ошибки будут расположены в начале файла.
Web Release 1 375 Генерация схемы базы данных Компонент XML Bulk может не только загружать данные в существующие таблицы, но и создавать соответствующие таблицы, если они не существуют, или удалять и пересоздавать их, если они существуют. Для того чтобы создать несуществующие таблицы, установите значение свойства SchemaGen как TRUE: Set objBulkLoad = CreateObjecK"SQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad.ConnectionString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind;" & _ "Integrated Security-SSPI:" objBulkLoad.SchemaGen = True objBul kLoad.Execute d: \xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad - Nothing Так как свойство SchemaGen имеет значение TRUE, любые таблицы, которые еще не существуют, будут созданы при запуске процесса массового копирования. Данные будут просто загружены в уже существующие таблицы. Если вы установите значение свойства Bu I kLoad как FALSE, данные загружены не будут. Таким образом, если SchemaGen установлено в TRUE, a Bu I kLoad — в FALSE, вы получите пустые таблицы — те, которые описаны в схеме отображения, но не существуют в базе данных (но все равно данные загружены не будут). Вот пример: Set objBulkLoad = CreateObjectrSQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad.ConnectionString = _ "provider=SQLOLEDB:data source=KUFNATHE:database=Northwind;" & _ "Integrated Security=SSPI;" objBul kLoad.SchemaGen = True objBul kLoad.Bui kLoad = False objBul kLoad. Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Когда XML Bulk Load создает таблицы, он использует информацию из схемы отображения для определения столбцов в каждой таблице. Аннотация sq I: datatype определяет тип данных столбца, а атрибут Dt: type определяет тип столбца. Чтобы определить первичный ключ в схеме отображения, установите значение атрибута столбца Dt: type, равное I d, а значение свойства SGUse ID компонента XML Bulk Load установите в положение True. Вот схема отображения, которая это иллюстрирует: <ElementType name="Orders" sql:relation="Orders"> <AttributeType name="OrderTD" sql:datatype»"int" dt:type="id"/> <AttributeType name="ShipCity" sql:datatype="nvarcharC0)'7> attribute type="OrderID" sql :field=rderID'7> attribute type="ShipCity" sql :field="ShipCity"/> </ElementType> Далее вы видите код на VBScript, который устанавливает свойство SGUse ID таким образом, что для всех таблиц, создаваемых на сервере, будут автоматически определены первичные ключи: Set objBulkLoad = CreateObjectCSQLXMLBulkLoad.SQLXMLBulkLoad") objBul kLoad. ConnectionString = _ "provider=SQLOLEDB;data source=KUFNATHE;database=Northwind;" &_ "Integrated Security=SSPI;" objBulkLoad.SchemaGen - True objBulkLoad.SGUselD = True objBul kLoad. Execute d:\xml\OrdersSchema.xml. d:\xml\OrdersData.xml Set objBulkLoad - Nothing
376 Глава 15. XML и SQL Server: OPENXML Вот код на Transact-SQL, сгенерированный в результате запуска процесса массового копирования: CREATE TABLE Orders ( OrderlD int NOT NULL, ShipCity nvarcharC0) NULL, PRIMARY KEY CLUSTERED (OrderlD) ) Кроме того, что SQLXML BulkLoad может создать новые таблицы по их описанию в схеме отображения, он также может удалить и пересоздать таблицы. Установите значение свойства SGDropTables в положение TRUE, чтобы компонент удалял и пересоздавал таблицы, указанные в схеме отображения. Вот пример: Set objBulkLoad = CreateObjectCSQLXMLBulkLoad.SQLXMLBulkLoad") objBulkLoad.ConnectionString = "provider=SQLOLEDB:data source=KUFNATHE:database=Northwind;" & _ "Integrated Security=SSPI:" objBulkLoad.SchemaGen = True objBulkLoad.SGDropTables = True objBulkLoad.Execute d:\xml\OrdersSchema.xml, d:\xml\OrdersData.xml Set objBulkLoad = Nothing Ограничения Несмотря на новые функции, представленные в Web Release l, SQL Server имеет ряд значительных ограничений, связанных с XML, которые в некоторых ситуациях могут создать немало трудностей. В этом разделе мы рассмотрим некоторые из этих ограничений, а также способы их обхода. sp_xml_concat Учитывая, что sp_xml_preparedocument может обрабатывать документы фактически любого размера (до 2 Гбайт), вы можете решить, что механизмы SQL Server для работы с XML могут отлично справляться с большими документами — на самом деле это не так. Хотя параметр xmltext процедуры sp_xml_preparedocument может иметь тип как varchar, так и text, проблема заключается в том, что Transact-SQL не поддерживает локальные переменные типа text. Все, что вы можете сделать с локальной текстовой переменной, — это создать процедуру с параметром типа text. Однако вам не удастся ни получить значение этого параметра, ни присвоить ему значение, полученное с помощью команды READTEXT. Все, что вы можете сделать с ним, — вставить его значение в таблицу. Проблема станет очевидной, когда вы попытаетесь сохранить большой XML- документ в таблице, а затем обработать его с помощью sp_xml_preparedocument. Когда документ уже помещен в таблицу, как получить его, чтобы передать в sp_xml_preparedocument? К сожалению, простого пути здесь нет. Так как мы не можем объявлять локальные переменные типа text, все, что мы можем сделать с этим документом, — это разделить его на несколько переменных типа varchar размером 8000 байт и использовать конкатенацию параметров в вызове sp_xm I _preparedocument. Эта трудность небольшая, и я написал хранимую процедуру, которая справится
Ограничения 377 с ней за вас. Она называется sp_xm I _concat, и вы можете использовать ее для обработки больших XML-документов, которые хранятся в таблицах в столбцах типа text, varchar или char. Sp_xm I _concat принимает три параметра: название таблицы, название столбца, где находится документ, и выходной параметр, содержащий дескриптор, сгенерированный процедурой sp_xm I _prepa redocument. Вы можете использовать этот дескриптор в 0PENXML() и sp_xml_unpreparedocument. Параметр @tab I e может быть как названием реальной таблицы или представления, так и производной таблицей с указанием псевдонима. Используя производную таблицу, вы можете отфильтровать ту таблицу, которую видит процедура. Таким образом, если вам понадобится обработать конкретную запись в таблице или ограничить множество записей, которые видит процедура, вы можете использовать для этого выражение с производной таблицей. Ниже представлен полный исходный код sp^xm I _concat: USE master ; • GO IF OBJECTJDCspj^'LconcatVP') IS NOT NULL DROP PROC sp_xml_concat GO CREATE PROC sp_xml_concat @hdl int OUT. stable sysname, ^column sysname AS EXECC SET TEXTSIZE 4000 DECLARE •• @cnt int, @c nvarcharD000) DECLARE @declare varchar(8000), @assign varchar(8000), @concat varchar(8000) SELECT @c = C0NVERT(nvarcharD000).•+(aco^umn+,) FROM '+@table+' SELECT ^declare = "DECLARE". @concat = , @assign = " " . @cnt - 0 WHILE (LEN(@c) > 0) BEGIN SELECT (^declare = ^declare + " @c"+CAST(@cnt as nvarcharA5))+" nvarcharD000),'', gassign = @assign + "SELECT @c"+C0NVERT(nvarcharA5) ,@cnt) + " = SUBSTRINGC + Ccolumn+',"+C0NVERT(nvarcharA5). l+@cnt*4000)+ ". 4000) FROM '+(atable+' ", @concat = @concat + "+(ac"+C0NVERT(nvarcharA5),Ccnt) SET @cnt = @cnt+l SELECT @c = C0NVERT(nvarcharD000),SUBSTRING('+@column+',l+@cnt*4000.4000)) FROM '+@table+' END IF (tart = 0) SET ^declare = "" ELSE SET ^declare = SUBSTRING(@declare.l,LEN(@declare)-l) SEt @concat = @concat + ''+'
378 Глава 15. XML и SQL Server: OPENXML EXEC(@declare+'' ' '+@assign+'' " + "EXEC( ""DECLARE @hdl_doc int EXEC sp_xml_preparedocument @hdl_doc OUT, ''+@concat+'' DECLARE hdlcursor CURSOR GLOBAL FOR SELECT @hdl_doc AS DocHandle' ' ")" ) ') OPEN hdlcursor FETCH hdlcursor INTO @hdl DEALLOCATE hdlcursor GO Эта процедура динамически генерирует необходимые операторы DECLARE и SELECT для того, чтобы разделить большой текстовый столбец на части nvarcharD000) (например, DECLARE @c1 nvarcharD000) SELECT @c1 = . ,.). По мере того как процедура делает это, она также генерирует выражение, содержащее конкатенацию всех этих переменных (например, @с1+@с2+@сЗ, ...). Так как функция ЕХЕС() поддерживает конкатенацию строк размером до 2 Гбайт, мы динамически передаем ей это выражение, тем самым позволяя ЕХЕС() осуществлять конкатенацию налету. Мы просто реконструируем документ, извлеченный из таблицы. Затем эта строка для обработки передается в sp_xml_preparedocument. Конечный результат — дескриптор документа, который мы можем использовать в 0PENXML(). Вот пример: (Код сокращен) USE Northwind GO CREATE TABLE xmldoc (id int identity. doc text) INSERT xmldoc VALUES('<Customers> <Customer CustomerID="VINET" ContactName="Paul Henriot"> Order CustomerID="VINET" EmployeeID=" OrderDate=996-07-04T00:00:QQ,,> OrderDetail OrderID=0248" ProductID="ll" Quantity=2"/> <OrderDetail OrderID=0248" ProductID=2" Quantity=0"/> //More code lines here... </0rder> </Customer> <Customer CustomerID="LILAS" ContactName="Carlos Gonzlez"> <0rder CustomerID="LILAS" EmployeeID=" OrderDate=996-08-16T00:00:00"> <OrderDetail OrderID=0283" ProductID=,,72" Quantity="/> </0rder> </Customer> </Customers>') DECLARE @hdl int EXEC sp_xml_concat @hdl OUT, '(SELECT doc FROM xmldoc WHERE id=l) a', 'doc' SELECT * FROM OPENXML(@hdl, '/Customers/Customer') WITH (CustomerlD nvarcharE0)) EXEC sp_xml_removedocument @hdl SELECT DATALENGTH(doc) from xmldoc GO DROP TABLE xmldoc (Результаты)
Ограничения 379 Custorae rID VI NET LI LAS 36061 На компакт-диске, прилагаемом к книге, вы можете найти полный тестовый запрос. Хотя я обрезал XML-документ в тестовом запросе, XML-документ на компакт-диске имеет размер более 36 000 байт, так как выводит запрос DATALENGTH() в конце тестового кода. Мы передаем выражение производной таблицы в sp_xm I _concat вместе с названием столбца, значение которого мы хотим извлечь, а процедура делает все остальное. Как видите, мы можем извлекать узлы, которые ищем, даже если один из них находится в самом конце довольно большого документа. sp_ru n_xm l_proc Другое ограничение XML в SQL Server заключается в том, что результаты XML не выводятся в виде обычных наборов записей. Такой подход имеет множество преимуществ, но один недостаток состоит в том, что вы не можете вызвать хранимую процедуру, которая возвращает XML, при помощи имени из четырех частей1 или OPENQUERY() и получить при этом хороший результат. Вы получите нераспознаваемое двоичное множество, так как архитектура связанных серверов SQL Server не поддерживает XML-потоки. Вы столкнетесь с теми же ограничениями, если попытаетесь вставить в таблицу результаты запроса с помощью FOR XML или поместить их в переменную. SQL Server просто не позволит вам это сделать. Почему? Потому что (как мы уже говорили) XML- документ, который возвращает SQL Server, не является традиционным набором строк. Чтобы обойти это ограничение, я написал хранимую процедуру под названием sp_run_xm I _proc. Вы можете использовать ее для вызова хранимых процедур связанных серверов (она должна находиться на связанном сервере), которые возвращают XML-документы, а также для вызова локальных процедур, результаты которых вы хотите поместить в таблицу или в переменную. Эта хранимая процедура открывает собственное соединение с сервером (предполагается Windows-аутентификация) и выполняет вашу процедуру. После того как процедура завершается, sp_run_xml_proc обрабатывает XML-поток с помощью вызовов SQL-DMO, затем преобразует его в традиционный набор строк и возвращает. Результирующее множество может быть вставлено в таблицу или обработано, как любое другое результирующее множество. Ниже представлен исходный код sp_run_xm I _proc: USE master GO IF OBJECT_ID('sp_run_xml_proc','P') IS NOT NULL DROP PROC sp_run_xml_proc GO CREATE PROC sp_run_xml_proc @procname sysname - Процедура, которую надо выполнить AS Имеется в виду exec [server].[database].[owner].[procedure name]. — Примеч. перев.
380 Глава 15. XML и SQL Server: OPENXML DECLARE @dbname sysname, @sqlobject int. --объект SQL Server @object int. -- Рабочая переменная для доступа к СОМ-объектам @hr int. -- Содержит HRESULT, полученный из СОМ @resu1ts int. -- объект QueryResults @msgs varchar(8000) - сообщения запроса IF (@procname-7?') GOTO Help -- Создаем объект SQLServer EXEC @hr-sp_OACreate 'SQLDMO.SQLServer'. @sqlobject OUT IF (@hr <> 0) BEGIN EXEC sp_displayoaerrohnfo @sqlobject. @hr RETURN END -- Объект SQLServer будет использовать доверительное соединение EXEC @hr = sp_OASetProperty @sqlobject, 'LoginSecure', 1 IF (@hr <> 0) BEGIN EXEC sp_displayoaerrorinfo @sqlobject, @hr RETURN END -- Выключаем префиксы ODBC для сообщений EXEC @hr - sp_OASetProperty @sqlobject, 'ODBCPrefix'. 0 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo @sqlobject, @hr RETURN END -- Открываем новое соединение EXEC @hr = sp_OAMethod @sqlobject. 'Connect'."NULL, (agSERVERNAME IF (@hr <> 0) BEGIN EXEC sp_displayoaerrorinfo @sqlobject. @hr RETURN -- Получаем указатель на коллекцию Databases объекта SQLServer EXEC @hr = sp_OAGetProperty (asqlobject, 'Databases', @object OUT IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @sqlobject. @hr RETURN END -- Получаем указатель для текущей базы данных SET @dbname=DB_NAME() EXEC @hr = sp_OAMethod @object. 'Item'. @object OUT, @dbname IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object, @hr RETURN END -- Вызываем метод ExecuteWithResultsAndMessages2 объекта Database method для запуска процедуры EXEC @hr = sp_OAMethod @object, 'ExecuteWithResultsAndMessages2'.^results OUT. @procname, @msgs OUT IF @hr <> 0 BEGIN EXEC spjjisplayoaerrorinfo @object, @hr RETURN END
Ограничения 381 -- Показываем сообщения, которые вернула процедура PRINT @msgs DECLARE Prows int. #cols int. @x int, @y int. @col varchar(8000). @row varchar(8000) -- Вызываем метод Rows объекта QueryResult для получения количества записей EXEC @hr = sp_OAMethod (Presults. 'Rows'.Prows OUT .,• ' IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object. @hr RETURN END -- Вызываем метод Columns объекта QueryResult object's для получения количества столбцов EXEC @hr = sp_OAMethod (^results, 'Column',@cols OUT IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object. @hr RETURN . - . END . , - DECLARE stable TABLE (XMLText varchar(8000)) ' " ' -' -- Столбец за столбцом получаем результирующее множество, используя метод GetColumnStnng SET @y=l . . . . • . WHILE (iay<=iarows) BEGIN SET @x=l SET @row='' WHILE (@x<=(acols) BEGIN EXEC @hr = sp_OAMethod ^results. 'GetColumnString',@col OUT. @y. @x IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object, @hr RETURN END SET @row=@row+(acol + ' ' SET (ax=@x+l END INSERT stable VALUES (@row) SET @y=(ay+l END SELECT * FROM stable EXEC sp_OADestroy @sqlobject -- For cleanliness RETURN 0 Help; PRINT 'Необходимо указать название процедуры для запуска' RETURN -1 GO Хотя открытие отдельного соединения с сервером для преобразования документа — не самый лучший метод, к несчастью, это единственный способ провести данную операцию не на стороне клиента (по крайней мере, в настоящее время). Вот тестовый код, который показывает, как использовать sp_run_xml_proc: USE pubs GO DROP PROC testxml
382 Глава 15. XML и SQL Server: OPENXML GO CREATE PROC testxml as PRINT 'a message here' SELECT * FROM pubs..authors FOR XML AUTO GO EXEC [BUC\FRYIN].pubs.dbo.sp_run_xml_proc 'testxml' (Результаты сокращены) a message here XMLText <pubs..authors au_id=72-32-1176" au_lname="White" au_fname="Johnson" <pubs..authors au_id=72-71-3249" au_lname="Yokomoto" au_fname="Akiko" Хотя я значительно обрезал результирующий документ, вы, запустив представленный выше код из Query Analyzer (измените ссылку на связанный сервер на свою собственную), увидите, что весь документ получен как результирующее множество. Затем для дальнейшей обработки это результирующее множество можно вставить в таблицу с помощью конструкции INSERT... EXEC. Например, вы можете использовать этот метод, чтобы присвоить этот документ переменной (первые 8000 байт) или изменить его при помощи Transact-SQL. После того как вы измените документ в соответствии со своими желаниями, вы сможете использовать sp_xml_concat (эта процедура была описана выше), чтобы получить дескриптор документа. Вы сможете работать с дескриптором документа при помощи 0PENXML(). Ниже показано, как это сделать: SET N0C0UNT ON GO USE pubs GO ;. DROP PROC testxml GO CREATE PROC testxml as SELECT aujname. au_fname FROM authors FOR XML AUTO GO CREATE TABLE #XMLTextl (XMLText varchar(8000)) GO -- Вставляем XML-документ в таблицу, используя sp_run_xml_proc INSERT #XMLTextl EXEC sp_run_xml_proc 'testxml' -- Помещаем документ в переменную и добавляем корневой элемент DECLARE @doc varchar(8000) SET @doc=" SELECT (adoc=@doc+XMLText FROM #XMLTextl SET @doc='<root>'+(adoc+'</root>' -- Помещаем документ назад в таблицу, чтобы можно было передать его -- в sp_xml_concat SELECT @doc AS XMLText INTO #XMLText2 GO DECLARE @hdl int EXEC sp_xml_concat @hdl OUT, '#XMLText2'. 'XMLText'
Итоги 383 SELECT * FROM OPENXML(@hdl. '/root/authors') WITH (aujname nvarcharD0)) EXEC sp_xml_removedocument @hdl GO DROP TABLE #XMLTEXT1. #XMLTEXT2 После того как документ получен с помощью sp_run_xml_proc и помещен в таблицу, мы загружаем его в переменную, добавляем корневой элемент и помещаем во вторую таблицу, чтобы передать в sp_xml_concat. После выполнения sp_xml_concat мы передаем дескриптор документа, который она вернула в 0PENXML(), и получаем часть документа: (Результаты сокращены) aujname Bennet Blotchet-Halls Carson DeFrance Ringer Ringer Smith ' ■ '■•'.- Straight , , Stringer White Yokomoto Таким образом, используя sp_xml_concat и sp_run_xml_proc совместно со встроенными инструментами SQL Server для работы с XML, мы можем осуществлять все операции по обработке XML. Мы начинаем с фрагмента XML, полученного с помощью FOR XML AUTO, затем помещаем его в таблицу, получаем из таблицы, добавляем корневой узел и передаем документ в 0PENXML(), чтобы получить часть оригинального документа как набор записей. Вы найдете, что эти две процедуры значительно расширяют возможности SQL Server, связанные с обработкой XML. Итоги В этой главе мы рассказали об OPENXML(). Вы узнали: ■ как разбивать или разрезать нереляционные XML-данные на реляционные части, с которыми можно работать при помощи Transact-SQL; ■ как создавать простые XPath-запросы для навигации по XML-документу при помощи 0PENXMLO; ■ как вставлять данные в таблицы, используя 0PENXML() в качестве источника данных; ■ что такое Web Release 1 и как его появление скажется на возможностях XML SQL Server; ■ несколько удобных хранимых процедур, которые должны облегчить вашу жизнь, если вы собираетесь много работать с механизмами XML SQL Server.
Л г -NET и грядущая IО революция Организации, где царствуют бессмысленные процессы, производят плохое программное обеспечение. Стив Макконнем} ПРИМЕЧАНИЕ Я включил обсуждение .NET в эту книгу, поскольку компания Microsoft анонсировала, что в следующую версию SQL Server войдет поддержка .NET Common Language Runtime. Даже если этого не произойдет, стоит поторопиться с рассказом о .NET. .NET все равно появится — и произойдет это очень скоро. Во время написания этой книги .NET Framework и Visual Studio.NET еще не были выпущены, но с тех пор многое могло измениться. За всю свою жизнь я испытал это чувство по крайней мере три раза. Обычно оно быстро исчезает, но иногда требуется время, чтобы его перебороть. Это неприятное чувство. Его трудно описать: это смесь раздражения, одиночества и скептического настроя. Оно заставляет оглянуться вокруг себя и спросить: «Неужели весь мир сошел с ума? Как мы могли так ошибаться?» Первый раз я испытал его, когда меня познакомили с программированием пользовательского интерфейса на мэйнфрейме IBM. Однажды я вошел в офис моего приятеля и увидел всю его команду, столпившуюся вокруг компьютера. «Иди сюда! — взволнованно позвал меня один из них. — Посмотри, что сделал Нил!» Нил написал электронную таблицу на «тупом» терминале. Поскольку терминал — необходимое средство для отображения информации, хранимой в компьютере, это не было большим подвигом. Чтобы выполнить команду, следовало переслать экран к мэйнфрейму, чтобы он создал новый экран, и ждать результатов. Это значит, что программа Нила не могла выполнять простые действия: например, по ней нельзя было передвигаться, используя стрелки; она не могла обновлять результат, когда исходные ячейки изменялись. Требовалось нажимать кнопку Отправить каждый раз, когда была необходимость что-либо изменить на экране. Каждый экран, по существу, был мертв. Терминал показывал экран, но когда пользователь вводил новую информацию, все начиналось по новой: весь экран посылался мэйнфрейму для дальнейшей обработки. У современной системы просмотра информации в Сети много общего с той программой. Я ушел с импровизированного показа сильно подавленный. «Это, должно быть, шутка! — думал я. — Уверен, существуют лучшие методы создания электронных McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 67.
.NET и грядущая революция 385 таблиц для такой сильной машины. Таблицы моего маленького компьютера на уровень выше того, чем когда-нибудь сможет стать этот вымысел для мэйнфрейма!» Однако чувство разочарования не оставляло меня. Я удивлялся тому, что люди были готовы мириться с ужасной глупостью только ради того, чтобы заставить неподходящую или неверно выбранную технологию создавать то, что им необходимо. Особенно сильно я почувствовал его влияние в мае 1990 года. Никогда этого не забуду! Был чудесный весенний день в горах юго-западного Миссури, когда я получил посылку из Microsoft. Я забрал посылку у администратора и осмотрел ее. Затем встряхнул. «Что бы это могло быть?» — думал я. Вернувшись в офис, я вскрыл посылку. Документы, лежащие в ней, разлетелись в стороны. Подобрав их, я достал небольшую коробку и открыл ее. Windows 3.0 SDK предстал передо мной во всей своей удивительной, искрометной славе! Я вывалил содержимое коробки на письменный стол. «Я создам приложение для Windows, — решил я. — И создам его прямо сейчас!» Я наблюдал за Windows все эти годы и был особенно взволнован выходом версии 3.0. Microsoft, объединившей Windows, Windows 286 и Windows 386 и предоставившей единый API для разработки приложений. Интерпретатор команд MS- DOS был заменен более дружественным Диспетчером программ. В общем, Windows теперь производила впечатление. Я решил: пришло время увидеть, что представляет собой этот Macintosh-подобный пользовательский интерфейс, и попробовать SDK для создания своего первого GUI-приложения. Итак, я установил программное обеспечение и быстро нашел в примерах необходимое мне приложение. Это была простая маленькая программа — многооконный текстовый редактор, наподобие Notepad (с точки зрения сложности). Я запустил компилятор Microsoft С и начал компиляцию приложения. Несколько минут спустя она завершилась — 80 000 строк кода! Зачем такой большой код для такого простого приложения? Мои самые худшие опасения подтвердились, когда я открыл исходный код в текстовом редакторе. То, что я увидел, было худшим из кодов: заголовочный файл после заголовочного файла и еще раз после заголовочного файла; цикл сообщений внутри цикла сообщений и еще раз внутри цикла сообщений. Чем дальше, тем запутанней. И не процедурный и не объектно-ориентированный код. «Какая бессмыслица, — подумал я. — Неужели нет лучшего способа?» В течение следующих нескольких лет я пытался найти этот лучший способ. Я просмотрел множество разных продуктов. Но все они преодолевали тысячи строк кода, чтобы создать приложение средней сложности для Windows. Некоторые из них стали сами создавать большую часть кода, но все равно приходилось долго ждать, пока десятки тысяч строк кода будут обработаны многопроходным компилятором. Страшно даже подумать о модификации какой-нибудь части этого сгенерированного кода — это был один из самых уродливых процессов, которые видел компьютерный мир. К сожалению, мы (кто создавал ранние приложения для Windows) были вынуждены работать при помощи Гольдберга (Goldberg). Это можно объяснить плачевным состоянием технологии. У нас просто не было выбора. Раньше, если технологии, основанные на DOS, были плохо продуманы или трудны, мы ими просто не пользовались. Не нравится Lattice С или Panels? Используй Turbo Pascal. He нравится работа с базами данных в Turbo Pascal? Нет проблем! Используй Clipper или Quicksilver. Многие средства DOS имели совершенно разные парадигмы программирования, и если одно не нравилось или не подходило для выполнения зада- 13 Зак 983
386 Глава 16. .NET и грядущая революция ния, можно было использовать другое. К несчастью, где-то по дороге к универсальности Windows мы потеряли вкус к средствам программирования. Все стало однообразным, а мы с этим смирились. Я уже почти собирался бросить поиск разумного пути создания приложений для Windows, как появился Visual Basic. Он изменил все. Несмотря на то что он был далек от совершенства — все же это был шаг к идеалу: ведь простые приложения должны быть построены просто (независимо от операционной системы), а более сложные приложения должны быть сложными линейно, а не экспоненциально. С течением лет, такие средства, как Delphi и Power Builder, усовершенствовали модель Visual Basic, и методология быстрой разработки прикладных программ возникла сама собой. Мое презрительное мнение о том, как утомительно было бы что- либо разрабатывать для Windows, было велико. Успех RAD отомстил мне за это. В третий раз то странное чувство, о котором я говорил в начале главы, посетило меня, когда я познакомился с системным администрированием в UNIX. Из-за постоянных проблем с SQL Server на OS/2 моей команде пришлось переместить большую систему на Sybase, который функционировал под Solaris. Я просто подумал, что администрирование OS/2 было примитивно и неоправданно сложно. Мне по- - казалось, что UNIX повторяла эти ненужные конвульсии OS/2. Я был поражен, узнав, что прародители UNIX надеялись, что люди будут управлять прикладными программами с помощью тайных команд ОС и скрытых инструментов, которые выглядели так, как будто их объединила пара студентов-выпускников, чтобы сбить весь мир с толку. Как и в случае с программированием на языке С, я обнаружил, что на UNIX нападали близорукие новички, что каждый выпускник CompSci со времен Dyson играл в игру «приделай ослу хвост», весело сменяя одну несвязную позу другой, до тех пор, пока не падал под своим собственным весом. Я удивлялся: «Неужели вы, люди, управляете системой таким образом? Кто может разобраться во всем этом ? Кто захочет ? Мы ведь руководим не посадкой ракеты на Луну — мы управляем базой данных!» Через какое-то время сами производители ОС осознали этот недостаток и стали пытаться сделать лицо UNIX более дружелюбным. Первым, кто сделал это удачно, была IBM с ее инициативой AIX. Вскоре после этого другие производители последовали ее примеру. UNIX стало намного легче устанавливать, использовать и администрировать. Сегодня это вполне зрелая ОС, представленная на нескольких платформах многими производителями. На мой взгляд, она все еще не так проста, как могла бы быть, и я сомневаюсь, что Linux вытеснит Windows с вашего рабочего стола в ближайшее время, несмотря на ее значительное усовершенствование. Но мое удивление относительно излишней сложности UNIX в значительной мере уменьшилось. В четвертый, и последний, раз я испытал странное чувство озадаченности, когда увидел свое первое сложное приложение для Web. Я только начал руководить небольшой компанией по разработке программного обеспечения для Интернета, когда наш технический директор устроил двухчасовой показ исходного кода компании. Он рассказал не только о нашей продукции, но и о современных технологиях разработки для Web: HTML, JavaScript, cookies и т. д. Я был озадачен. Старое чувство снова вернулось. Без сомнения, никто не потратил бы столько сил, чтобы создать столь простое приложение. Никто не стал бы использовать такие примитивные, неподходящие технологии для создания качественного программного обеспечения.
.NET и грядущая революция 387 Начнем с того, что я был удивлен тем, какими примитивными были инструменты разработки. Казалось, их было всего два: всеохватывающие — для полного создания веб-сайтов ценой чрезмерной похожести и раздражающей неточности; и инструменты «сделай сам», которые в основном состояли из чьих-то идей по созданию HTML-ориентированного блокнота. Между ними не было почти ничего общего, а разнообразие технологий и конкурирующие стандарты ухудшали ситуацию. Кроме того, я был шокирован тем, какими слабыми и негибкими было большинство технологий. Я разочаровался в HTML, как в языке, из-за его непреклонности и недостатка силы. Я был шокирован тем, что люди хотели вернуться в те дни, когда приложения приходилось запускать в интерпретаторе, а синтаксические ошибки показывались в коде. Я не мог поверить в то, что мои коллеги хотели отказаться от визуальной разработки, которая сделала Visual Basic таким популярным, или терпеть инструменты, которые заставляли их кодировать вручную все, независимо от сложности. Более того, я не мог не заметить огромного числа попыток обойти недостатки Web. Никто не подумал об управлении состоянием. Но — нет проблем! Мы сделаем это через загадочные URL, cookies и другие обходные пути. Мы недооценили, как сильно люди будут скучать по интерактивному содержанию. Не беспокойтесь. Мы напишем интерпретируемые сценарии, которые будут работать в браузере (возможно, эти сценарии будут разными для разных браузеров). Мы напишем первоклассные элементы пользовательского интерфейса на Java, C++, VB или на каком- нибудь другом языке и загрузим их в машину пользователя, и, чтобы уменьшить время загрузки, мы воспользуемся кэшированием этих элементов на машине пользователя, не применяя контроль версий или какой-либо способ обновления. Чтобы ужиться со злонамеренным кодом, мы сделаем шаг назад и вернемся к системе безопасности на основе приложений. Добавим механизмы безопасности в веб-браузеры, чтобы дополнить систему безопасности операционной системы. Поскольку технически каждый пользователь веб-сервера является пользователем одной и той же операционной системы, перенесем механизмы безопасности и аутентификации в сам браузер. И, проделав все это, получим интерфейс пользователя, который, возможно, применит только половину тех знаний и опыта, которые люди имели задолго до того, как Сеть вошла в моду. Мы буквально тратим годы, раз за разом выполняя одну и ту же работу и исправляя ошибки, допускать которые мы не имели право. Поскольку весь мир принял язык разметки как основное средство создания программ и избрал документо-центрированный взгляд на программное обеспечение, нам пришлось отказаться от многих достижений, на которые ушли годы работы: объектно-ориентированное программирование; активный пользовательский интерфейс; логические функции и управление состоянием; безопасность Сети, основанная на доменах; скорость и стабильность компилируемого, а не интерпретируемого кода; удобочитаемые поддерживаемые программы; возможность повторного использования кода; централизованное управление исполняемыми файлами и т. д. Я был поражен тем, что люди хотели создать критические приложения, используя глупые текстовые файлы, создаваемые с помощью мэйнфрейм подобного, перешли-и-создай интерфейса. Я думал, мы оставили эту модель позади. Я думал, мы выросли из нее. Сидя на той презентации, я понял, что благодаря Web пользовательские интерфейсы сильно упростились, что приложения стали менее (а не более) удобными
388 Глава 16. .NET и грядущая революция и что создавать мощное дружелюбное и расширяемое программное обеспечение стало намного сложнее. Если не считать межсетевого взаимодействия, казалось, что изобретение Web отбросило всю индустрию программного обеспечения назад на два-три года во многих областях, особенно в области создании приложений и дизайне пользовательского интерфейса. С тех пор я ищу технологию или несколько технологий, которые сделают создание простых приложений для Web легким, а создание более сложных приложений — более сложным линейно. Я ищу технологию, которая была бы полностью объектно-ориентированной, — что позволит мне использовать наследование, инкапсуляцию и полиморфизм в той мере, в которой я захочу. Мне необходимо найти такое, что компилирует, а не интерпретирует, что находит мои опечатки и синтаксические ошибки во время компиляции, а не во время выполнения и что работает со скоростью машинного кода. Я хочу иметь возможность применять один язык ко всему. Я не желаю создавать страницы на одном языке, элементы управления — на другом, а сценарии - на третьем. Я хочу писать только на одном языке и хочу, чтобы этот язык был формализованным и достаточно систематичным. Я хочу создать такой богатый пользовательский интерфейс, какой только смогу. Я не хочу быть ограниченным в инструментах. Я желаю, чтобы они помогали мне, а не мешали, поскольку тогда я смогу сосредоточиться на том, что должно делать мое приложение, а не на том, как это сделать. Я хочу иметь такой инструмент, который за меня позаботится об обработке ошибок. Но он должен быть основан на HTTP и дизайне приложений без соединений. Я хочу иметь инструмент, обладающий большим набором элементов управления для работы с базами данных, которые смогут отображать сами себя на вебстраницах, даже если HTML будет создан не из базы данных или не заполнен вручную каким-то другим способом. Я хочу иметь такой набор инструментов, который позволит визуально создавать веб-страницы примерно так, как это делают инструменты RAD. Я хочу положить кнопку на форму, щелкнуть на ней дважды и прикрепить к ней какой-нибудь код. Я не хочу думать о передаче данных в формы или о переменных формы; я хочу написать код объектов и позволить программному обеспечению системного уровня позаботиться об остальном. Я хочу, чтобы инструмент не был ориентирован на один язык, а позволял мне выбирать язык из широкого ряда языков программирования, и при этом производительность работы компьютера не уменьшалась бы, а мне не надо было бы работать с ужасными преобразованиями данных между языками. Если я хочу создать класс на одном языке, а кто-то захочет использовать его на другом — он должен получить возможность сделать это. Я хочу иметь инструмент, который позволит мне разработать любой тип приложения. Я не хочу быть обязанным использовать один тип инструментов для создания GUI-приложений, а другой — для разработки программ системного уровня. Я хочу иметь возможность создавать любой тип приложений, который мне понадобится, — от консольного приложения до сервиса, приложения для Windows и Web — с помощью одного инструмента. И я не согласен на меньшее. Я хочу, чтобы технология была мощной, легкой в использовании и расширяемой для каждой поддерживаемой ее платформы.
.NET — будущее разработки программного обеспечения 389 Я хочу обеспечить безопасность и надежность улучшенной структуры моего приложения. Я желаю использовать технологию, которая будет за меня управлять ресурсами, которая предохранит меня от доступа к той памяти, к которой я не должен иметь доступ, и которая позволит мне сконцентрироваться на задачах, которые должно решать мое приложение, а не на деталях реализации моих желаний. Я хочу найти технологию, позволяющую просто создавать многозвенные приложения, которые будут небольшими по размеру и легко расширяемыми. Я желаю найти технологию, позволяющую обмениваться информацией с другими приложениями и кодом, созданным не мной. Я хочу иметь инструмент, совместимый с кодом, который был написан в ранее созданных инструментах. Я хочу, чтобы этот инструмент сохранил мои вложения в приложения, которые я уже создал. Мне нужен инструмент, который позволил бы использовать удачные находки в уже созданных приложениях. В общем, я хочу все. Я желаю в корне изменить способ создания приложений, особенно веб-приложений. К счастью, кое-кто уже подумал и начал работать над всем этим. И, к счастью, этот кое-то — Microsoft. .NET — будущее разработки программного обеспечения Мои друзья знают, что я не стремлюсь к новизне. Я не гонюсь за новейшими разработками и никогда не гнался. Когда Java только появилась, я не сразу присоединился к числу ее поклонников. Я подождал, пока активность снизится, а потом сам исследовал Java и сделал собственные заключения относительно ее полезности. Когда Linux впервые появился в заголовках, я не особенно много времени потратил, пытаясь научиться пользоваться этой ОС или найти инструменты, которые помогли бы мне создать для нее программное обеспечение. И когда XML стал объектом общего увлечения, я подождал, пока стандарты не утвердились, прежде чем совершил на него первый набег. Да, я консервативен, но это качество всегда помогало мне. Поэтому, когда я говорю, что Microsoft .NET изменит подход к созданию приложений, вы можете быть уверены, что я хорошо обдумал это замечание, прежде чем сделать его, и что, по крайней мере, я верю в то, что говорю. Я верю, что .NET превратит разработку веб-приложений из рая для хакеров в место, где искусство программного обеспечения и качественная инженерия будут процветать. Много опытных инженеров остались в стороне от разработок приложений для Web, потому что пока это похоже на революционные беспорядки. Я думаю, .NET все изменит. Лучшие разработчики в мире будут привлечены к программированию веб-приложений с помощью .NET, потому что эта платформа продемонстрирует инструменты и способы разработки программ, которые, наконец, будут иметь значение, а не казаться полусырыми и смогут обеспечить ту силу и гибкость, которую ожидают эксперты. Я верю, что .NET будет особенно полезна для начинающих и посредственных разработчиков. Мне кажется, она защитит новобранцев от возможности навредить самим себе (или своим клиентам). Я думаю, она сумеет защитить нас от самих себя, пока мы будем совершенствоваться. Она поможет простые вещи сделать простыми, а сложные — сравнительно сложными. .NET избавит нас от необходимости
390 Глава 16. .NET и грядущая революция изучать десять разных технологий только для того, чтобы создать одно расширяемое приложение, и позволит нам сконцентрироваться на решении задач бизнеса, а не на тривиальности создания программ. Что такое .NET? Хороший вопрос. Microsoft выпустила так много нового под маркой .NET, что иногда трудно на него ответить. Разрешите рассказать, как я это понимаю. Для начала общее определение: .NET — это набор технологий, который: ■ собирает лучшее из многих разрозненных технологий разработки программного обеспечения, особенно связанных с Web; ■ объединяет изолированные веб-приложения и платформы; ■ значительно увеличивает продуктивность разработчиков; ■ делает доступными для веб-разработчиков лучшие приемы проектирования программного обеспечения, годами отрабатывавшиеся на других платформах; ■ обеспечивает безопасную среду, в которой приложения могут не уделять слишком много времени управлению ресурсами или низкоуровневым деталям; ■ исправляет давние недостатки инструментов программирования от Microsoft. Хотя Microsoft занимает большую долю рынка инструментов разработки, чем кто-либо другой, я не считаю продукты Microsoft лучшими (пока не появилась .NET). По-моему, правильнее назвать лучшими продукты компании Borland. Так как я использовал продукты обеих компаний многие годы, мне кажется, я могу делать такие заявления (и я также уверен, что многие со мной не согласятся); ■ обеспечивает инструменты для создания приложений, которые претворяют в жизнь устоявшееся желание пользователей сделать информацию доступной в любое время, в любом месте и на любом устройстве. Теперь расскажем подробнее. ■ .NET состоит из структуры классов, семейства языков программирования и средств системного уровня, необходимых для создания и выполнения кода и для совместного использования данных несколькими приложениями. Это семейство языков включает: Visual Basic.NET, C++, Jscript и абсолютно новый язык С# (произносится си шарп), более легкий в использовании язык семейства C/C++. ■ Структура классов .NET (.NET Framework) — это всеобъемлющая, необычайно полная, объектно-ориентированная библиотека времени выполнения (RTL — RunTime Library), которая очень сильно упрощает и стандартизирует процесс создания приложений для любых платформ, поддерживаемых .NET. Она придает объектно-ориентированный вид практически всему, что вы делаете в программе. Например, вместо того, чтобы вызывать CreateWindow для создания окна или ожидать в цикле обработки сообщений появление приглашения нажать клавишу, вы просто добавляете новый объект WinForm в свой проект и накладываете на него элементы управления, как если бы вы делали это в Visual Basic. В отличие от Visual Basic код, соответствующий форме, по-настоящему объект-
Что такое .NET? 391 но-ориентированный, во всех смыслах этого понятия. Когда вы присоединяете код к клавише, вы создаете метод класса формы. Структура WinForm использует ту же модель делегирования, основанную на формах, которую мы впервые увидели в Delphi. Вы получаете средства объектно-ориентированной и визуальной разработки в одном инструменте, то есть то, чего нет в текущей версии Visual Studio1. ■ Все языки .NET объектно-ориентированные. Все они поддерживают наследование, инкапсуляцию и полиморфизм, а интерфейсы — как отдельные языковые конструкции. ■ Языки .NET перекрестно совместимы. Можно создать класс на одном языке и наследовать его в другом. Такого никогда раньше не было в семействе инструментов разработки Microsoft. Можно было использовать объекты в одном языке, которые были созданы на другом, но нельзя было расширить объект, созданный на другом языке, путем наследования. ■ Все языки .NET поддерживают одинаковые типы данных. Преобразования, которые часто приходится делать при совместном доступе к данным, например, если одно приложение написано на VB, а другое на C++, — ушли в прошлое. ■ Вся языки .NET поддерживают структурированное управление исключениями. Если исключение вызвано классом, оно правильно взаимодействует с кодом, вызвавшим его, независимо от того, на каком языке написано исключение и вызвавший его код. ■ В дополнение к языкам, которые будут поставляться вместе с .NET, Microsoft обеспечила открытый API, который позволяет создавать собственные языки, совместимые с .NET. Некоторые компании уже сообщили о намерении воспользоваться этой возможностью. (Я видел около дюжины анонсированных .NET- языков сторонних компаний. Например, я знаю, что планируется выпустить Perl.NET и COBOL.NET.) Каждый язык .NET, независимо от того, кто его создал, будет иметь одинаково полный набор компонентов. ■ Программы компилируются в промежуточный код MSIL (Microsoft Intermediate Language) с помощью компилятора языков .NET. Этот код известен как управляемый код. У управляемого кода есть ряд следующих преимуществ. □ Автоматическая сборка мусора. Как и виртуальная MauiHHaJava, единая среда выполнения .NET (Common Language RunTime .NET, CLR) автоматически удаляет объекты, когда они становятся не нужны и системе необходимо освободить физическую память. Но в отличие от Java VM CLR обеспечивает методы контроля за тем, что именно и когда следует делать. Объекты могут быть исключены из процедуры сборки мусора, а частота и объем сборки мусора могут контролироваться разработчиком. □ Проверка границ и диапазонов. Не позволяется перезаписывать чужую память. ., □ Возможность перенесения CLR на другую платформу. Хотя большинство разработчиков платформы Wintel не считают это первоочередным мероприятием, многие, несомненно, хотели бы иметь такую возможность. Имеется в виду версия 6.0. — Примеч. перев.
392 Глава 16. .NET и грядущая революция ■ При запуске программа автоматически транслируется в машинный код с помощью компилятора .NET (JIT — just-in-time). Этот подход имеет ряд положительных черт. Ниже представлены некоторые из них. □ Компилятор может генерировать разные инструкции для разных процессоров. Вместо использования подхода «один размер подходит всем», компилятор^ может создавать код, который будет оптимален для данного процессора. Например, JIT может создавать разный код для процессоров Intel, AMD или Cyrix. □ Из-за того, что код создается после запуска приложения, есть вероятность, что обновления среды выполнения улучшат качество и производительность генерируемого машинного кода и при этом не потребуется перекомпилировать приложения конечных пользователей. То есть если компилятор Microsoft JIT улучшится, ваши приложения улучшатся вместе с ним, не будучи перекомпилированы. ■ .NET предоставляет средства для обратной совместимости и возможности взаимодействия с СОМ и неуправляемым кодом. ■ .NET делает создание распределенных приложений таким же простым занятием, как и создание GUI-приложений. .NET веб-сервисы основаны на XML и SOAP {Simple Object Access Protocol), но нет необходимости иметь дело непосредственно с одним из них (если только вы сами этого не захотите). Вы кодируете классы, a .NET заботится обо всем остальном. Веб-сервисы автоматически обнаруживаются в Сети, и .NET создает код, необходимый для их использования, как если бы они были логическими классами. ■ .NET поддерживает создание полнофункционального пользовательского интерфейса как для Web, так и для Windows: □ можно создавать формы для любой платформы, используя визуальное конструирование, такое же, как в Visual Basic. Вы размещаете элементы управления на форме и присоединяете к ним код; □ можно создавать свои собственные элементы управления, которые созданы на основе или с использованием других элементов управления; □ для веб-форм вы можете определить в визуальном дизайнере, будет ли элемент управления постоянно храниться на сервере или на клиенте; □ вы можете использовать элементы управления ActiveX на одной форме вместе с элементами .NET. ■ Архитектура форм .NET поддерживает визуальное наследование форм. Можно создавать формы для Web или Windows и наследовать их, чтобы создавать другие похожие формы. Можно перекрывать методы, обращаться с формами полиморфически и т. д. Эта возможность была впервые представлена в Delphi (я не считаю Data Windows Power Builder, потому что они не были объектно-ориентированными). Как бы то ни было, .NET усовершенствовала подход, представленный в Delphi тем, что наследование форм теперь полностью основано на коде. В Delphi сложный алгоритм разделения форм определял вид наследуемых форм во время запуска так же, как и в Visual Basic. Значения свойств формы хранились в отдельном файле ресурсов для каждой формы. Каждый член в дереве наследования должен был быть проверен и должен иметь правильные значения свойств для определения влияния на унаследованную форму. Этот процесс не всегда
Что такое .NET? 393 эффективен и его несложно нарушить. В .NET все значения свойств хранятся в самом классе, поэтому, если положить кнопку на форму и задать ей заголовок, автоматически создается код, который присваивает ей определенное вами значение. Но среда разработки Visual Studio 7 достаточна умна, чтобы скрыть этот код из вида таким образом, чтобы вы не сталкивались с ним во время работы. Вы получаете два преимущества: настоящее наследование форм, основанное на ООП, и отсутствие огромного объема автоматически создаваемого кода. ■ Так как .NET не полагается на системный реестр, развертывание приложений существенно упростилось. Возможно, вы слышали о знаменитом XCOPY-разверты- вании. Это не выдумка. Оно действительно работает — я сам проверял. Поскольку нет необходимости регистрировать сборки .NET (термин .NET для скомпилированных двоичных данных: DLL и ЕХЕ), для развертывания можно просто копировать сборку приложения и сборки компонентов, от которых оно зависит. В отличие от СОМ на одной машине можно иметь несколько копий одной и той же сборки .NET-объекта. Для этого не требуется прибегать к дополнительным ухищрениям, как в развертывании СОМ и переназначении DLL. И это не запоздалая мысль — именно так и была спроектирована среда выполнения с самого начала. При этом сильно уменьшается вероятность того, что при установке одного приложения перестанет работать другое (что мы обычно называем адом DLL). ■ Хотя чаще всего вы будете развертывать сборки приложения в его корневой папке, не забывайте о том, что вы также можете совместно использовать сборки несколькими приложениями, поместив их в глобальный кэш сборок. ■ .NET обеспечивает прямую поддержку контроля над версиями объектов. .NET Framework включает встроенную функцию поддержки: установка новой версии объекта не повредит приложения, зависящие от старых версий, а также позволяет легко определить, от каких версий и сборок зависит ваше приложение. ■ .NET избавилась от множества глупых, устаревших и проблематичных черт С/ C++, таких как: подключаемые файлы, макросы, множественное наследование, , изменяемые/неизменяемые классы и от других причуд языка. С#, в частности, , такой же элегантный и мощный язык программирования, как и любой другой, которым я когда-либо пользовался. ■ Указатели в .NET автоматически разыменовываются (это еще один метод, заимствованный из Delphi). Поэтому вы выигрываете в скорости, используя 4-бай- . товый указатель на объект, а не сохраняя его в вариантном типе. Вы будете избавлены от суеты, связанной с указателями и их арифметикой. ■ .NET поддерживает настоящий строковый тип. Можно легко работать со строками в любых языках .NET, так же как сейчас вы работаете в Visual Basic. ■ Каждый тип данных в .NET является объектом. Это относится и к Java, но .NET улучшает подход Java во многих отношениях. Например, .NET поддерживает ..- принципы упаковки и распаковки — метод внутреннего увеличения производительности простых типов данных без потери возможности использования их ; как объектов. Другое преимущество .NET заключается в том, что ее таблицы ,-; виртуальных методов основных типов относительно невелики, потому что мно- •; жество малоиспользуемых методов было перенесено в отдельные классы-помощники. Другими словами, вместо того чтобы включать в каждый тип данных . метод преобразования его в другие типы данных, в .NET эти методы перенесе-
394 Глава 16. .NET и грядущая революция ны в отдельные классы для преобразований. Каждый базовый тип несет в себе необходимые методы для решения общих задач, а малоиспользуемый код централизованно хранится в классах-помощниках. ■ Сборки .NET предоставляют обширные метаданные вовне. Любой класс, поле, метод, свойство или событие, которые предоставляет внешнему миру код сборки, доступны из самого исполняемого файла. Вам не нужны исходные тексты или файлы символов. Именно так работает межъязыковое наследование в .NET. Наследование происходит напрямую из сборки, а не из исходного кода или заголовочных файлов. Поэтому, на самом деле, вы наследуете из MSIL, а не из языка, на котором первоначально была написана сборка. Эти метаданные более обширны, чем данные библиотеки типов, и не отделимы от сборки. Они хранятся в двоичном формате и используются для чего угодно, начиная от межъязыкового наследования до технологии IntelliSense в IDE Visual Studio 7. ■ .NET наводит порядок в области современных разработок для Web. Вместо того чтобы писать файлы в формате HTML, а потом внедрять в них VBScript или JavaScript (а иногда еще и использовать элементы управления ActiveX или ап- „ плеты Java), вы просто визуально создаете веб-формы. Код веб-формы — класс. Вам не придется иметь дело со спагетти-образным кодом, который обычно ассоциируется со сложными веб-страницами. Веб-технологии .NET широко известны как ASP.NET, это естественное развитие ASP. Создавая веб-страницу в IDE Visual Studio 7, вы размещаете элементы управления на форме, определяете, где они должны постоянно храниться (на сервере или на клиенте), и присоединяете любой код к их событиям (например, к событию — нажатие кнопки). Код, необходимый для функционирования вашей формы в браузере, автоматически создается и развертывается, когда вы используете Visual Studio 7 IDE для развертывания приложения на своем веб-сервере. (Visual Studio 7 поддерживает развертывание напрямую на веб-сервер так же, как это было в InterDev и Frontpage.) ■ ASP.NET поддерживает элементы управления, связанные с данными, и их автоматическое заполнение из источников данных. Можно использовать элементы управления в виде сеток или однострочные элементы управления и связывать их с любыми источниками данных, поддерживаемыми .NET. ■ Код веб-форм ASP.NET компилируется, а не интерпретируется. Приложения ASP.NET будут запускаться со скоростью машинного кода. Это важное усовершенствование в технологии ASP, которая является интерпретируемой и часто использует вариантные типы данных. ■ Кроме того, .NET поддерживает новую версию ADO, известную как ADO.NET. ADO.NET основана на XML. На самом деле, XML — родной формат файлов для наборов данных ADO.NET. Отсоединение наборов данных не запоздалая мысль в ADO .NET, это ее основная особенность. ADO.NET состоит из иерархии классов, которая чем-то напоминает классическую технологию ADO, а чем-то отличается от нее. Она обеспечивает расширяемый, легковесный, не использующий постоянное подключение уровень сервиса данных, которому изначально было предназначено быть мощным, но простым инструментом в использовании в Web так же, как и в обычных офисных приложениях. Базируясь на XML, ADO.NET обеспечивает готовые средства обмена данных с приложениями, написанными не в .NET. Это позволяет разработчику создавать приложения, завязанные на
Что такое .NET? 395 XML, не работая непосредственно с XML, кроме случаев, когда это абсолютно необходимо. Вы кодируете структуру классов, a .NET заботится обо всем остальном. ■ Доступ к данным .NET нейтрален в языковом отношении. Он одинаково работает в любом языке .NET. В отличие от классической технологии ADO, которую, очевидно, лучше использовать на Visual Basic, все языки .NET работают одинаково хорошо с ADO.NET. ADO.NET использует те же типы данных, что и CLR (они обе основаны на типах данных XML), и предоставляет всю свою функциональность через обычные классы .NET. ■ ADO.NET поддерживает такие расширенные функции XML, как трансформации и схемы XML. Она поддерживает прямую связь с такими OLEDB- провайдерами, как SQLOLOEDB SQL Server, и другими источниками, основанными на XML. Эта технология позволяет создавать приложения, объединяющие информацию из множества разных источников, за одно мгновение. ■ Visual Studio .NET обеспечивает самую умную и продуктивную среду разработки, которая когда-либо была создана. Эта среда имеет всю основную функциональность: подсказки IntelliSense, когда вы печатаете, подсветка синтаксиса и универсальный менеджер проектов. Она также имеет несколько расширенных функций, которые не часто встречаются в основных средах разработки, представленных на рынке. Одна из моих самых любимых — это редактор кода. Возможно, это один из лучших инструментов, который я когда-либо видел в средах разработки. Такие его характеристики, как свертывание кода (способность свернуть узлы кода так, чтобы их не было видно), очень важны, если вы пытаетесь сосредоточиться на определенном методе и хотите очистить окно редактирования от всего, что может вас отвлечь. Еще одна полезная функция — это динамическая помощь: IDE следит за тем, что вы вводите, и показывает подсказки из библиотеки MSDN на ненавязчивой панели справа на экране (а-ля Office XP). Панели инструментов спрятаны слева на экране. Они появляются, когда вы наводите на них указатель мыши. Кроме того, их можно переместить по вашему желанию. Веб-сервисы и другие типы внешних ссылок могут быть сделаны прямо из IDE. Несколько щелчков мышью — и ссылка добавлена вместе со всем кодом, необходимым для ее поддержки. IDE представляет собой веб-подобный интерфейс, дополненный стартовой страницей, ссылками на последние проекты и информацией от Microsoft для разработчиков в Web. Обсуждение .NET не было бы законченным без примеров кода. В листинге 16.1 показан исходный код приложения Hello World на Visual Basic.NET. Обратите внимание на то, что я написал только одну строчку кода — вызов MsgBox() (выделено жирным шрифтом). Остальное было автоматически сгенерировано IDE. Я опустил такие автоматически сгенерированные части, как установка значений свойств и код создания формы, потому что они обычно не отображаются. Листинг 16.1. Простое приложение на Visual Basic.NET Public Class Forml Inherits System.Windows.Forms.Form Private Sub Buttonl_Click(ByVal sender As System.Object. ByVal e As System.EventArgs) Handles Buttonl.Click MsgBox("Hello world!") End Sub End Class
396 Глава 16. .NET и грядущая революция Как вы видите, Visual Basic теперь объектно-ориентированный и поддерживает наследование. Наш класс формы Forml наследовался от основного класса формы в .NET Framework — System.W i ndows.Forms.Form. Этот класс фактически написан на С#, так что перед вами межъязыковое наследование в действии — то, чем СОМ никогда не обладала. Метод Button1_CI ick () — метод класса Forml. Нажав Button"!, вы начинаете выполнение этого метода, а ссылка на элемент управления, который вызвал его, передается в параметре sender. Теперь то же самое приложение на С#. Листинг 16.2. Hello World на С# using System; using System.Drawing; using System.Col lections; using System.Componenttiodel; using System.Windows.Forms: using System.Data; namespace HelloWorldCS { /// <summary> /// Summary description for Forml. /// </summary> public class Forml : System.Windows.Forms.Form { private System.Windows.Forms.Button buttonl; /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.Container components: public FormlО { // // Required for Windows Form Designer support // Initial izeComponentO; // // TODO: Add any constructor code after // InitializeComponent call // } /// <summary> /// Clean up any resources being used. /// </summary> public override void DisposeO { base.DisposeO; if(components != null) components. DisposeO: } /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void MainO { Application.Runtnew FormlO): } private void buttonl_Click(object sender.System.EventArgs e) {
«Избиение» Microsoft 397 MessageBox.ShowC'Hello world!"); } } } Несмотря на то что код здесь немного больше, чем в примере на Visual Basic, я написал только одну строку (выделено жирным шрифтом), остальное было создано автоматически. Если вы знакомы с С и C++, то синтаксис, вероятно, покажется вам очень знакомым. С# не нуждается в заголовочных файлах и не использует их — все определения находятся внутри класса, то есть реализация также является интерфейсом. Вы решаете, что будет видимо или невидимо внешнему миру с помощью модификаторов (например, р г i vate, pub I i с и т. д.). Избиение» Microsoft Я полностью уверен в том, что Microsoft будет раскритикована, как только ее конкуренты узнают о .NET больше. Скорее всего, они начнут беспокоиться — сильно беспокоиться, — что это будет очередная победа Microsoft, и будут абсолютно правы. Я так думаю. .NET изменит способ создания программного обеспечения. Она перевернет разработку для Web в той же степени, в которой Visual Basic перевернул разработку для Windows. Людям, которые боятся Microsoft или конкурируют с ней, это не понравится. Я уверен, что рекламная машина завертится с огромной скоростью, как только .NET будет выпущена. Определенная часть людей (надеюсь, меньшинство) не будет использовать преимущества .NET просто потому, что ее создала Microsoft. Они никогда не отдадут должное этой технологии. Они предпочтут все, что угодно, кроме продукции Microsoft, — в особенности, такую смелую идею как .NET. На мой взгляд, пренебрегать технологией только потому, что она произведена Microsoft, все равно, что ехать во Францию и отказываться учить французский или говорить на нем только потому, что вам не нравятся французы. Технологии Microsoft — это lingua franca в компьютерном мире. Отказ от них причиняет вред только нам как разработчикам и никак не влияет на Microsoft. Мы не можем повлиять на них, независимо от того, нравится нам это или нет. Мои друзья знают, что я не привык прислуживать и не раз конфликтовал с Microsoft. Я соглашаюсь не со всем, что она делает, и не считаю каждый ее продукт высшим достижением. Все зависит от продукта — я использую то, что работает. Поэтому я использую SQL Server, и поэтому я выбрал .NET. Когда я слышу о разработчиках, которые отказываются от использования технологий только потому, что они произведены в Microsoft, мне становится грустно. Почему? Потому что уже не раз компании терпели унизительное поражение, из принципа отказываясь использовать находки Microsoft. Как WordPerfect. Или Novell. Или Lotus. Этот путь «вымощен» компаниями, которые пытались соревноваться с Microsoft вместо того, чтобы думать о том, как дать своим клиентам то, что они хотят, и тогда, когда им это необходимо. Я считаю, разработчики должны выбирать те технологии, которые подходят им для выполнения работы. Конечно, нельзя быть близоруким и выбирать технологии, которые используются сегодня, а завтра уже будут забыты или которые не очень подходят друг другу. С другой стороны, мы не должны следовать каждой
398 Глава 16. .NET и грядущая революция модной тенденции. Как бы то ни было, позволять предубеждениям против Microsoft заставлять нас не обращать внимания на нововведения просто потому, что они нам не нравятся, — глупо. Это снижает продуктивность и плохо влияет как на нас, так и на всю компьютерную индустрию. Если мы будем следовать этим путем достаточно долго, мы скоро окажемся без технологий и сил в борьбе за клиентов. Это мир, в котором одна технология поедает другую, и выживают только самые способные. Иногда они оказываются созданиями Microsoft. Фанатичная привязанность к Microsoft? Недавно на одном совещании меня попытались спровоцировать, сказав: «Хорошо, если хочешь быть фанатиком Microsoft, скажи об этом прямо сейчас, чтобы мы могли начать с этим бороться». На что я ответил: «Я не фанатик Microsoft, но и не Microsoft-ненавистник. Я сторонник хороших технологий. Я использую все, что работает, и не важно, кто это создал». В отрицании технологий Microsoft по моральным соображениям появился определенный романтизм, но он фальшив. Существует мнение, что отказ от работы с Microsoft помогает столь же ощутимо, как посвящение собственной жизни служению обществу или контактам со странами третьего мира. Я думаю, это заявление претенциозно. Дело в том, что всегда будет множество разработчиков, которые будут использовать технологии Microsoft, даже если мы решим этого не делать. Так же как всегда будут находиться механики, которые будут обслуживать автомобили General Motors, даже если сильные мира сего решат порицать негуманную тактику больших корпораций и перейдут исключительно на Yugos. Необходимо обуздать тщеславие и не позволить чьему-то недостатку знаний технологий привести к моментальному отказу от технологии, даже не испытав ее. Отказ от рассмотрения технологии только по той причине, что она произведена Microsoft, показывает неуважение ко всем, кто тяжело трудился, создавая ее. Даже если вам не нравится то, как работает Microsoft, люди, создающие программное обеспечение, в большей части, не несут за это ответственность. Они такие же, как и вы и я. Они создают продукт и этим зарабатывают себе на жизнь. Они не более и не менее злы, чем разработчики технологий в других компаниях. Иногда у них появляются замечательные идеи, а иногда — не очень. Итоги В этой главе вы познакомились с .NET. Вы узнали: ■ к чему может привести .NET и какие у нее сильные стороны; ■ почему .NET была так необходима, а также о типах задач, для решения которых была создана эта технология; ■ о грядущей революции .NET. .NET придет. Скоро. Пора готовиться.
Часть 4 Вопросы повышенной сложности
1 Размышления / о производительности Быть счастливым — значит не более, чем приобрести то, чего хочешь. И наоборот, несчастье — это страх перед тем, что произойдет не то, чего ты желаешь. Фокус в том, чтобы приобрести, а не потерять. X. В. Кентон Обычно, рассуждая о настройке производительности, я подхожу к этому с точки зрения способов, при помощи которых можно было бы ускорить код. Людям нравится такой подход к решению проблемы — поэтому они и покупают книги вроде той, которую вы сейчас держите в руках. Существует несколько простых методов ускорения кода. Складывается впечатление, что в наше время все куда-то спешат. Мы все хотим получить простые и быстрые решения задач. Насколько глубоко мы понимаем философию технологии — не имеет значения. Нам необходимо решение — и необходимо оно еще вчера. Я считаю, что самый простой способ обсудить проблему настройки производительности SQL Sever — это перечислить наиболее часто встречаемые проблемы, связанные с производительностью, и дать их решения. Конечно, мы можем перечислить и наиболее частые ошибки, которые возникают при написании кода Transact-SQL и при создании баз данных SQL Server, и поговорить о способах их исправления. Проще говоря, мы можем за минуту придумать большой набор средств, которые подойдут ко многим сценариям настройки производительности. Именно этот подход я использовал в главе «Настройка производительности» в своей книге «The Guru's Guide to Transact-SQL». Я считаю, что книги, содержащие готовые решения, также могут быть полезны. Мне кажется, что понимать, как что-либо сделать, важнее и, в конце концов, выгоднее, чем читать многочисленные подсказки, особенно когда дело доходит до настройки производительности. Как уже было сказано в предисловии, понимать философию технологии значительно важнее, чем изучать синтаксическую структуру. Ваша способность оптимизировать хранимые процедуры — результат ваших знаний и понимания самого SQL Server: как он работает, что делает, когда обрабатывает запрос, какие ресурсы ему необходимы для создания эффективного плана выполнения и т. д.
Индексирование 401 Итак, в этой главе рассматриваются внутренние процессы, происходящие при обработке запросов, рассказывается о разных уровнях обработки запросов и о том, что происходит на каждом из них. Как только вы поймете, как SQL Server обрабатывает запросы, вы выработаете собственные методы ускорения SQL-кода. Вы будете применять целесообразные и безопасные методы, так как будете понимать принципы работы SQL Server. Индексирование Существует несколько более выгодных способов повышения производительности запросов, чем создание полезных, эффективных индексов. Основная проблема, возникающая при работе с большими объемами данных, — операции ввода-вывода. Вам захочется оптимизировать их как можно больше. В этом помогает кэширование, мощный процессор, быстрый жесткий диск. Однако ничто так не повышает производительность запросов, как индексирование. Если нет хороших индексов, SQL Server остается только сканировать таблицу или таблицы, чтобы найти необходимые данные. Если вы объединяете две (или более) таблицы, SQL Server может сканировать некоторые из них по несколько раз, чтобы найти все данные, удовлетворяющие запрос. Индексы могут так же существенно ускорить процесс поиска данных, как и процесс объединения таблиц. Хранение Системная таблица sysIndexes хранит системную информацию об индексах SQL Server. Каждый индекс записан в этой таблице. Каждый индекс идентифицируется по столбцу i nd i d. Кластерный индекс всегда имеет значение i nd i d, равный 1. Если таблица не имеет кластерного индекса, то в sys i ndexes будет храниться запись о самой таблице, и i nd i d будет равен 0. Карта распределения индексов (Index Allocation Map — IAM) SQL Server определяет экстенты, принадлежащие таблице или индексам, используя страницы IAM. Таблица или индекс имеет по крайней мере один IAM для каждого файла, в котором ему выделены экстенты. IAM — это битовая маска, которая определяет принадлежность экстента объекту. Каждый бит показывает, принадлежит ли соответствующий экстент объекту, который владеет IAM. Каждый IAM содержит 512 000 страниц. (8000 страниц. 8 бит/байт. 8 страниц/экстент = 512 000.) Первая страница индексов IAM хранится в колонке F i rst I AM таблицы sys i ndexes. Страницы IAM выделяются произвольно в файле базы данных и связаны в цепочку. Даже если IAM позволяет SQL Server эффективно считывать данные таблицы, каждая запись все равно должна рассматриваться отдельно. IAM служит доступом к самим страницам. Типы индексов SQL Server поддерживает два типа индексов: кластерные и некластеризованные. Эти типы имеют много общего. Они состоят из страниц, которые хранятся в сба- I
402 Глава 17. Размышления о производительности лансированных В-деревьях. В каждом узле содержатся указатели на страницы следующего уровня, а листья содержат значения узлов. В-деревья Индексы SQL Server физически хранятся в В-деревьях. В-деревья поддерживают поиск, используя алгоритм бинарного поиска. Индексы В-деревьев хранят ключи с похожими значениями близко друг от друга, а само дерево постоянно перестраивается для того, чтобы данное значение можно было найти с минимальным количеством проходов по дереву. Поскольку В-деревья сбалансированы, время на поиск тратится относительно одно и то же, независимо от искомой записи. Первый узел в В-дереве — корневой. Указатель на корневой узел каждого индекса хранится в столбце root таблицы sysindexes. Когда осуществляется поиск данных с использованием индекса, SQL Server начинает с корневого узла, затем проходит все промежуточные уровни (если таковые имеются) и в конечном итоге находит или не находит данные на нижних листовых узлах индекса. Количество промежуточных уровней зависит от размера таблицы, размера ключа индекса и количества столбцов в ключе. Очевидно, что чем больше данных или чем больше ключ, тем больше требуется страниц. Страницы индекса выше уровня листьев известны как узловые страницы. Каждая запись в узловой странице содержит ключ или ключи и указатель на страницу следующего уровня, чей первый ключ соответствует ему (то есть указателю). Это общая структура В-дерева. SQL Server просматривает эти связи, пока не найдет данные, которые ищет, или пока не достигнет конца на уровне листьев. Уровень листьев В-дерева содержит значения ключей и, в случае некластеризованных индексов, закладки для соответствующего кластерного индекса или для кучи. Эти значения ключей хранятся последовательно и в SQL Server 2000 могут быть отсортированы и по возрастанию и по убыванию. В отличие от некластеризованных индексов, листья кластерного индекса хранят сами данные. Там нет закладок, да они и не нужны. Когда существует кластерный индекс, сами данные находятся на листовом уровне индекса. Страницы данных в таблице хранятся в цепочке страниц — двусвязном списке страниц. Если существует кластерный индекс, порядок записей на каждой странице и порядок страниц в цепочке определяется ключом индекса. Учитывая, что наличие кластерного индекса приводит к сортировке данных, важно выбирать кластерный индекс с умом, руководствуясь несколькими соображениями. В том числе необходимо помнить, что: ■ ключ должен быть мал настолько, насколько это возможно, потому что он будет служить закладкой для каждого ^кластеризованного индекса; ■ ключ должен быть выбран таким образом, чтобы он соответствовал большинству запросов ORDER BY и GROUP BY; ■ ключ должен хорошо сочетаться со стандартными запросами, выбирающими диапазон записей (это запрос, в котором выбираются несколько записей на основе значения для столбца (столбцов)). Начиная с SQL Server 7.0, все кластерные индексы имеют уникальные ключи. Если кластерный индекс создан без указания ключевого слова UN I QUE, SQL Server
Индексирование 403 обеспечивает уникальность индекса, добавляя к значениям ключа 4-байтовое значение, когда необходимо отличить одинаковые значения ключа друг от друга. Листовые страницы в некластеризованном индексе содержат ключи индекса и закладки для соответствующего кластерного индекса или таблицы. Закладки могут быть двух видов. Когда у таблицы существует кластерный индекс, закладка — это ключ кластерного индекса. Если ключевой столбец содержится и в кластерном, и в некластеризованном индексе, он хранится только в одном месте. Когда кластерного индекса нет, закладка содержит RID (идентификатор записи), который состоит из номера файла, номера страницы и номера места записи, соответствующей значению некластеризованного ключа. В случае кучи (таблицы без кластерного индекса) некластеризованные индексы используют информацию о физическом расположении. Это достаточно веская причина для того, чтобы создавать кластерный индекс для каждой таблицы. Без него изменения таблицы, приводящие к разделению страниц, приведут к эффекту домино для некластеризованных индексов, потому что физическое расположение записей, на которые они ссылаются, будет изменяться, возможно, довольно часто. Это было одним из главных недостатков индексирования в SQL Server до версии 7.0: некластеризованные индексы всегда хранили информацию о физическом расположении, вместо того чтобы хранить значение кластерного ключа. Вследствие этого некластеризованные индексы были восприимчивы к изменению физического расположения записи в таблице. Некластеризованные индексы лучше всего подходят для запросов, которые возвращают единственную запись. При просмотре некластеризованного В-дерева данные могут быть получены с помощью одной операции чтения страницы — чтения страницы из таблицы. «Покрывающие» индексы Говорят, что некластеризованный индекс «покрывает» запрос, когда он содержит все столбцы, получаемые запросом. Это позволяет пропустить поиск по закладке и без трудностей вернуть искомые данные из В-дерева. Когда существует кластерный индекс, запрос может быть «покрыт» с использованием комбинации ключевых столбцов некластеризованного и кластерного индекса, так как кластерный индекс — это закладка для некластеризованного индекса. То есть если некластеризованный индекс построен по столбцам LastName и Fi rstName, а кластерный индекс — по столбцу Customer ID, запрос, который получают столбцы Customer ID и LastName, может быть «покрыт» некластеризованным индексом. «Покрывающий» некластеризованный индекс — это индекс, который можно использовать вместо нескольких кластерных индексов на одну таблицу. Проблемы с производительностью Старайтесь, чтобы ключи ваших индексов были как можно более компактными. Чем больше ключи, тем больше будет операций чтения/записи, и тем меньше записей поместится на каждой странице В-дерева. В результате индексу понадобится больше страниц, а значит, больше дискового пространства. На практике вы, возможно, будете адаптировать свою стратегию индексирования в соответствии с потребностями бизнеса. Например, если у вас есть запрос, который выполняется
404 Глава 17. Размышления о производительности очень долго из-за того, что ему требуется индекс с ключевыми столбцами, которых нет ни в одном существующем индексе, вы можете либо расширить существующий индекс, либо создать новый. Естественно, при добавлении нового индекса необходимо решить проблему производительности обновления. Каждый новый индекс приводит к увеличению затрат при изменении таблицы, так как индексы таблицы должны поддерживаться и обновляться, когда вы добавляете или изменяете данные. Чем больше индексов вы добавляете, тем медленнее происходит обновление таблицы, поэтому важно, чтобы индексы были как можно меньше и при этом удовлетворяли те потребности бизнеса, для которых была разработана ваша система. Пересечение индексов До версии 7.0 оптимизатор запросов SQL Server использовал только один индекс на таблицу для выполнения запроса. SQL Server 7.0 и более поздние версии могут использовать несколько индексов на таблицу, применяя пересечение множеств их закладок перед получением данных из таблицы. Это имеет смысл при проектировании индексов и выборе ключей, и вскоре мы это обсудим. Фрагментация индексов Вы можете контролировать уровень фрагментации индекса, используя его параметр fill factor, а также с помощью регулярных операций дефрагментации. Параметр fill factor индекса влияет на производительность. Во-первых, создание индекса со сравнительно низким значением fill factor позволяет избежать расщепления страниц во время вставки. Очевидно, что если страницы заполнены только частично, вероятность того, что понадобится провести расщепление одной из них для вставки новых записей, ниже, чем в случае, когда страницы заполнены полностью. Во-вторых, при высоком значении f i I I factor страницы могут быть размещены более компактно. Таким образом, для работы запроса понадобится меньше операций ввода-вывода. Этот подход часто применяется при создании хранилищ данных. Так как SQL Server осуществляет операции ввода- вывода на уровне экстентов (а экстенты — это наборы страниц), получение страницы, которая заполнена только частично, ведет к увеличению числа операций ввода-вывода. Параметр f i I I factor индекса влияет только на листовые страницы. SQLServer сам резервирует достаточно много пустого места на промежуточных страницах индекса для того, чтобы иметь возможность сохранить хотя бы одну запись максимального для индекса размера. Если вы хотите, чтобы параметр fill factor влиял и на промежуточные страницы, укажите опцию PAD_ INDEX в операторе CREATE TABLE. PAD INDEX заставляет SQL Server применять fill factor к промежуточным страницам индекса. Если значение параметра fill factor настолько большое, что на промежуточной странице нет места даже для одной записи (например, при значении fill factor, равном 100 %), SQL Server будет использовать значение, которое позволит разместить хотя бы одну запись. Если значение f i I I factor настолько мало, что промежуточные страницы не могут хранить как минимум две записи, SQL Server будет использовать такое значение f i I I facto г, чтобы на промежуточных страницах можно было разместить как минимум две записи.
Индексирование 405 Поймите, что параметр индекса fill factor не поддерживается во времени. Он применяется, когда индекс создается первый раз, но в дальнейшем может не использоваться. DBCC SHOWCONTIG — это инструмент для определения, насколько на самом деле заполнены страницы таблицы и/или индексы. Основные показатели, на которые надо обратить внимание, — Logical Scan Fragmentation иAvg. Page Density. DBCC SHOWCONTIG показывает три типа фрагментации: фрагментацию при сканировании экстента, логическую фрагментацию и плотность сканирования. Используйте DBCC INDEXDEFRAG для устранения логической фрагментации. Для полной деф- рагментации таблицы и/или индексов пересоздавайте индексы. В листинге 17.1 показан пример информации, выводимой DBCC SHOWCONTIG для таблицы Customers базы данных Northwind. Листинг 17.1. DBCC SHOWCONTIG DBCC SHOWCONTIG (Customers) (Результаты) DBCC SHOWCONTIG scanning 'Customers' table... Table: 'Customers' B073058421): index ID: 1. database ID: 6 TABLE level scan performed. - Pages Scanned : 5 - Extents Scanned : 3 - Extent Swi tches : 4 - Avg. Pages per Extent : 1.7 - Scan Density [Best Count:Actual Count] : 20.00% [1:5] ■ Logical Scan Fragmentation : 40.00* - Extent Scan Fragmentation : 66.67% - Avg. Bytes Free per Page : 3095.2 - Avg. Page Density (full) : 61.76* Как вы видите, таблица Customers немного фрагментирована. Logical Scan Fragmentation находится на уровне 40 % и Avg. Page Density — 61,76 %. Другими словами, страницы в таблице в среднем пусты на 40 %. Давайте дефрагментируем кластерный индекс таблицы и посмотрим, улучшится ли что-нибудь (листинг 17.2). Листинг 17.2. Таблица Customers после дефрагментации DBCC IMDEXDEFRAG(Morthwi nd.Customers.1) (РезультатьО Pages Scanned Pages Moved Pages Removed 1 0 1 (РезультатьО DBCC SHOWCONTIG (Customers) (Results) DBCC SHOWCONTIG scanning 'Customers' table... Table: 'Customers' B073058421): index ID: 1, database ID: 6 TABLE level scan performed. - Pages Scanned : 4 - Extents Scanned : 3 - Extent Swi tches : 2 - Avg. Pages per Extent : 1.3 продолжение &
406 Глава 17. Размышления о производительности Листинг 17.2 {продолжение) - Scan Density [Best Count:Actual Count] : 33.33* [1:3] - Logical Scan Fragmentation : 25.00* - Extent Scan Fragmentation : 66.67X - Avg. Bytes Free per Page : 1845.0 - Avg. Page Density (full) : 77.21* Как вы видите, очень помогло выполнение DBCC INDEXDEFRAG. Logical Scan Fragmentat i on уменьшился до 25 %, и Avg. Page Dens i ty теперь немного больше 77 %, то есть улучшился примерно на 15 %. По умолчанию DBCC SH0WC0NTIG выводит информацию только о листовом уровне таблицы/индекса. Чтобы получить информацию о других уровнях таблицы/индекса, укажите параметр ALL_LEVELS (листинг 17.3). Листинг 17.3. DBCC SHOWCONTIG может показывать фрагментацию на всех уровнях DBCC SHOWCONTIG (Customers) WITH TABLERESULTS. ALL_LEVELS (Результаты сокращены) ObjectName IndexName AveragePageDensity ScanDensity LogicalFragmenta Customers PK_Customers 77.205337524414063 33.333333333333329 25.0 Customers PK_Customers 0.95132195949554443 0.0 0.0 В табл. 17.1 приведены основные элементы, выводимые DBCC SHOWCONTIG. Я использую Logical Scan Fragmentat ion и Avg. Page Density для определения общей фрагментации таблицы/индекса. Вы увидите, что они изменяются одновременно, если фрагментация увеличивается или уменьшается. Таблица 17.1. Поля DBCC SHOWCONTIG Поле SHOW CONTIG Значение Avg. Bytes Free per Page Среднее количество свободных байт на странице Pages Scanned Количество обработанных страниц Extents Scanned Количество обработанных экстентов Out of order pages (не отображается, Количество случаев, когда номер страницы но используется для вычисления при сканировании был меньше номера предыдущей Logical Scan Fragmentation) страницы Extent Switches Количество случаев, когда страница при сканировании оказывалась в другом экстенте, по сравнению с предыдущей страницей Дефрагментация Как вы только что убедились, DBCC INDEXDEFRAG представляет собой удобный метод дефрагментации индекса. Это онлайновая операция, так что индекс может использоваться во время ее работы. Она перестраивает индекс только на уровне листьев, используя для этого разновидность пузырьковой сортировки. Для того чтобы полностью дефрагментировать индекс, необходимо его пересоздать. Это можно сделать несколькими способами. Во-первых, вы можете просто удалить его и пересоздать с помощью DROP/CREATE I NDEX. Недостаток этого метода заключается в том, что индекс будет недоступен, пока вы его пересоздадите, а также вы не сможете удалить индекс, используемый для поддержки ограничения. Вы можете использовать
Индексирование 407 ОВСС DBRE I NDEX или опцию DROP_EX I ST I NG оператора CREATE I NDEX, однако индекс все равно будет недоступен, пока он не пересоздан. Если вы используете SQL Server Enterprise Edition, индекс может создаваться параллельно. Поскольку параллельное создание индексов в SQL Server равномерно распределяется по нескольким процессорам, время, в течение которого индекс недоступен, может быть уменьшено, если добавить больше процессоров. В общем, DBCC INDEXDEFRAG — лучший инструмент, если вы не обнаружите значительную фрагментацию на нелистовых уровнях индекса и не решите, что эта фрагментация плохо сказывается на производительности запроса. Как было сказано выше, фрагментацию промежуточных уровней индекса можно проверить, добавив к вызову DBCC SHOWCONTIG опцию ALL_LEVELS. В дополнение к дефрагментации страниц листового уровня, DBCC INDEXDEFRAG также производит уплотнение, целью которого является уплотнение страниц индекса с использованием первоначального значения параметра f i I I facto г. Эта операция пытается оставить достаточно места как минимум для одной записи на каждой странице, после того как завершит работу. Если она не может заблокировать какую-то страницу в процессе уплотнения, эта страница пропускается. Эта операция удаляет все страницы, которые становятся полностью пустыми в результате уплотнения. Индексы на представлениях и вычисляемых столбцах Создание индекса на представлении или вычисляемом столбце сохраняет данные, которые иначе существовали бы только в логическом смысле. Обычно данные, возвращаемые представлением, существуют только в таблицах, на которых основано представление. Когда вы осуществляете запрос к представлению, ваш запрос складывается из представления и данных из соответствующих объектов. Это же справедливо и для вычисляемых столбцов. Обычно данные, возвращаемые вычисляемым столбцом, на самом деле зависят от столбцов или выражений, которые используются вычисляемым столбцом. Каждый раз, когда вы запрашиваете эти данные из основной таблицы, выражение, используемое для их получения, вычисляется заново, и данные генерируются «на лету». Если вы создаете индексы на представлении, вы должны начать с создания кластерного индекса. Это то место, где на самом деле находятся данные. Так же как и с таблицами, кластерный индекс, созданный на представлении, на самом деле сам хранит данные в своих листовых узлах. Как только создан кластерный индекс, вы можете создавать некластеризованные индексы для представления. Это отличается от создания вычисляемых столбцов в таблице. В случае с вычисляемыми столбцами не обязательно сначала создавать кластерный индекс для создания некластеризованных индексов. Поскольку столбец просто будет служить значением ключа индекса, некластеризованный индекс работает прекрасно. Обязательные требования SQL Server требует указать правильно семь SET-установок, чтобы можно было создавать индексы на представлениях или вычисляемых столбцах. В табл. 17.2 приведены эти установки с необходимыми значениями. Как вы видите из таблицы, значение всех установок, кроме NUMERI C_R0UNDAB0RT, должно быть ON.
408 Глава 17. Размышления о производительности Таблица 17.2. Необходимые установки для индексов на представлениях/вычисляемых столбцах Установка Необходимое значение ARITHABORT ON CONCAT_NULL_YIELDS_NULL ON QUOTEDJDENTIFIER ON ANSI_NULLS ON ANSI_PADDING ON ANSI_WARNING ON NUMERIC_ROUNDABORT OFF Только детерминистические выражения могут быть использованы с индексированными представлениями и индексами на вычисляемых столбцах. Выражение называется детерминистическим, если для заданного входного значения оно всегда возвращает одинаковое выходное значение. SELECT SUBSTR I NG(' He who I oves money more thantruthwi I I end up poor' ,23,7) —детерминистическое выражение; GETDATE() —нет. Вы можете проверить, можно ли проиндексировать представление или столбец при помощи функций Transact-SQL OBJECTPROPERTY() и COLUMNPROPERTY() (листинг 17.4). Листинг 17.4. Не все представления можно проиндексировать SELECT OBJECTPROPERTY @BJECT_ID('Invoices'). 'Islndexable') SELECT COLUMNPROPERTY @BJECT_ID('syscomments'). 'text' . 'Islndexable') SELECT COLUMNPROPERTY @BJECT_ID('syscomments'). 'text' . 'IsDetermimstic')ing Required value (Результаты) 0 0 0 Последнее обязательное требование для представлений заключается в следующем: представление может быть проиндексировано только в том случае, если оно было создано с опцией SCH EMABINDING. Создание представления с указанием SCHEMABINDING приводит к тому, что SQL Server предотвращает попытки удаления объектов, на которые ссылается представление, пока само представление не будет удалено или изменено таким образом, что будет устранена опция SCHEMAB I ND I NG. Оператор ALTER TABLE для таблиц, на которых основано представление, также не будет выполнен, если изменение таблицы может привести к изменению определения представления. В предыдущем примере представление I nvo i се не может быть проиндексировано, так как при его создании не была указана опция SCH EMAB I ND ING. В листинге 17.5 представлена другая его версия с последующей проверкой I s I ndexab I e. Листинг 17.5. Теперь представление Invoice2 можно проиндексировать CREATE VIEW Invoices2 WITH SCHEMABINDING AS SELECT Orders.ShipName. Orders.ShipAddress, Orders.ShipCity. Orders.ShipRegion.
Индексирование 409 Orders.ShipPostalCode, Orders.ShipCountry, Orders.CustomerlD. Customers.CompanyName AS CustomerName. Customers.Address, Customers.City. Customers.Region. Customers.Postal Code, Customers.Country. Orders.OrderlD, Orders.OrderDate. Orders.RequiredDate, Orders.ShippedDate. Shippers.CompanyName As ShipperName. [Order Details].ProductlD. Products.ProductName. [Order Details].UnitPrice. [Order Details].Quantity. [Order Details].Discount, Orders.Freight FROM dbo.Shippers INNER JOIN (dbo.Products INNER JOIN ( (dbo.Employees INNER JOIN (dbo.Customers INNER JOIN dbo.Orders ON Customers.CustomerlD = Orders.CustomerlD) ON Employees.EmployееID = Orders.EmployeelD) INNER JOIN dbo.[Order Details] ON Orders.OrderlD = [Order Details].OrderlD) ON Products.ProductlD = [Order Details].ProductlD) ON Shippers.ShipperlD = Orders.ShipVia GO SELECT OBJECTPROPERTY (OBJECT_ID('Invoices2'). 'Islndexable') (Результаты) Обратите внимание на то, что для ссылок на объекты теперь используются имена из двух частей (первоначально в представлении I nvo i ces этого не было). При создании представления с SCHEMABINDING требуется, чтобы все ссылки на объекты состояли из двух частей (владелец/объект). После того как представление было проиндексировано, оптимизатор может использовать индекс, когда осуществляется запрос к представлению. К тому же, в случае SQL Server ЕЕ, оптимизатор будет использовать этот индекс для запросов к таблицам, на которых основано представление, если решит, что это позволит уменьшить стоимость запроса (время выполнения). Обычно индексированные представления вообще не используются оптимизатором, если только вы не используете версию Enterprise Edition. Например, подумайте о таком индексе и запросе: CREATE UNIQUE CLUSTERED INDEX inv ON invoices2 (orderid. productid) GO SELECT * FROM invoices2 WHERE orderid=10844 AND productid=22 (Результаты сокращены) ShipName ShipAddress ShipCity ShipRegion ShipPostalCode Piccolo und mehr Geislweg 14 Salzburg NULL 5020 ,; Далее представлена часть плана этого запроса. Листинг 17.6. Индексы представления не используются по умолчанию в версиях SQL Server, отличных от ЕЕ StmtText SELECT * FROM [invoices2] WHERE [orderid]=@l AND [productid]=@2 продолжение $■
410 Глава 17. Размышления о производительности Листинг 17.6 {продолжение) |--Nested Loops(Inner Join) j--Nested Loops(Inner Join) I |--Nested Loopsdnner Join. OUTER REFERENCES: ([Orders]. [ShipVia])) | | |--Nested Loopsdnner Join. OUTER REFERENCES: ([Orders]. [Employ j j j |--Nested Loopsdnner Join. OUTER REFERENCES: ([Orders]. [C | | | | |--Clustered Index SeekfOBJECT:([Northwind].[dbo].[0 | | | | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[C j j | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Employ j | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Shippers].[ | |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Order Details].[ I --CIustered Index Seek(OBJECT:([Northwind].[dbo].[Products].[PK_Product Несмотря на то что план справа обрезан, вы можете сказать, что индекс представления, очевидно, не был использован, хотя он и включает оба столбца, по которым проводится фильтрация запроса. Это нормальный эффект для версий SQL Server, отличных от ЕЕ. Только SQL Server ЕЕ будет учитывать индексы представления при создании плана выполнения. Однако это можно обойти. Вы можете использовать в запросе подсказку NOEXPAND на не-ЕЕ версиях SQL Server для того, чтобы индекс представления принимался во внимание. В листинге 17.7 представлен тот же запрос, но на этот раз с ключевым словом NOEXPAND, и результирующий план запроса. Листинг 17.7. NOEXPAND заставляет использовать индекс представления SELECT * FROM invoices2 (NOEXPAND) WHERE orderid=10844 AND productid=22 StmtText SELECT * FROM invoices2 (NOEXPAND) WHERE orderid=10844 AND productid=22 |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Invoices2].[inv]). SEEK:([ Обратите внимание на то, что теперь используется индекс. Совместно с подсказкой INDEX, NOEXPAND может заставить оптимизатор использовать индекс представления, даже если это приведет к созданию не совсем оптимального плана. Так что относитесь к этой подсказке с той же долей скептицизма, с которой вы относитесь к другим подсказкам в запросе. Лучше позволить оптимизатору делать свою работу и вмешиваться только тогда, когда у вас нет другого выбора. Блокировки и индексы Таблице необходим кластерный индекс в том случае, если на нее накладываются RID-блокировки. SQL Server никогда не накладывает RID-блокировки на таблицу с кластерным индексом — вместо этого он накладывает блокировки ключей. Вы должны позволить SQL Server контролировать блокировки всех типов, включая блокировки, связанные с индексами. Обычно он принимает правильные решения и очень хорошо выполняет работу по управлению своими собственными ресурсами. Вы можете использовать системную хранимую процедуру sp_ i ndexopt i on, чтобы вручную контролировать типы блокировок, которые разрешены для индексированной таблицы. Вы можете использовать ее, чтобы запретить блокировки строк и/или страниц, что может быть удобно, если механизмы блокировки SQL Server работают неправильно (особенно в отношении эскалации блокировок).
Статистика 411 Помните, что sp_ i ndexopt i on применяется только к индексам, так что вы не можете контролировать блокировки страниц в куче. Это значит, что когда таблица имеет кластерный индекс, на нее влияют настройки, указанные с использованием sp_indexoption. Статистика Возможно, вы слышали термин статистика, который достаточно часто упоминается при обсуждении производительности запросов SQL Server. Статистика — это метаданные, которые SQL Server поддерживает для ключей индексов и иногда для значений неиндексируемых столбцов. SQL Server использует статистику для того, чтобы определить, увеличится ли скорость запроса при использовании индекса. Совместно с индексами статистика — самый важный источник информации, помогающий оптимизатору создавать оптимальные планы выполнения. Если статистика отсутствует или устарела, оптимизатор теряет свою способность создавать наилучший план выполнения запроса. Давайте рассмотрим несколько простых терминов, связанных со статистикой, прежде чем перейдем к более подробному обсуждению. Количество элементов Количество данных определяет количество уникальных значений. В строгой теории реляционных баз данных одинаковые строки (кортежи) в отношении (таблице) не разрешаются. Таким образом, количество элементов — это общее количество кортежей. Это означает, что SQL Server позволяет существование одинаковых строк в таблице, так что в нашем случае термин количество элементов означает количество уникальных значений во множестве данных. Плотность Плотность определяет уникальность значений во множестве данных. Плотность индекса вычисляется путем деления количества записей, соответствующих данному значению ключа на количество записей в таблице. Для уникального индекса это означает деление 1 на общее количество записей в таблице. Плотность варьируется от 0 до 1. Чем плотность меньше, тем лучше. Селективность Селективность — мера количества записей, которые будут получены для некоторого критерия запроса. Она определяет, как взаимосвязаны между собой критерий вашего запроса и значения ключей индекса. Селективность вычисляется путем деления количества запрошенных ключей на количество записей, которым они соответствуют. Селективный критерий запроса (обычно он указывается в части WHERE) наиболее полезен для оптимизатора, потому что он позволяет оптимизатору с большой долей уверенности предсказать, сколько операций ввода-вывода потребуется произвести для осуществления запроса.
412 Глава 17. Размышления о производительности Проблемы с производительностью Индексы с большой плотностью, скорее всего, будут проигнорированы оптимизатором. Наиболее полезные для оптимизатора те индексы, чье значение плотности равно 0,10 или меньше. Приведем пример: возьмем таблицу под названием VoterRegistration, в которой 10,000 записей, кластерного индекса нет и существует некластеризованный индекс по столбцу Pa rtyAff i I i at i on (Принадлежность к партии). Если в зоне голосования зарегистрированы три политические партии, и у каждой примерно одинаковое число представителей по всей зоне голосования, Pa rtyAff illation, скорее всего, будет содержать только три уникальных значения. Это означает, что заданное значение ключа индекса может определять 3,333 записи в таблице и, возможно, больше. Получаем значение плотности индекса, равное 0,33 C,333 х 10,000), и мы можем предположить, что оптимизатор не будет использовать этот индекс при построении плана выполнения запросов, не покрываемых этим индексом. Для того чтобы понять это лучше, давайте сравним стоимость выполнения простого запроса с использованием индекса и без его использования. Если мы хотим получить информацию обо всех проголосовавших представителях демократической партии, мы говорим почти о трети таблицы — о 3,333 записи. Если мы будем использовать индекс Pa rtyAff i I I at i on для доступа к этим записям, мы столкнемся с 3,333 отдельными логическими чтениями страниц из таблицы. Другими словами, как только мы найдем ключевое значение в индексе, мы должны будем использовать его закладку, чтобы получить столбцы, не содержащиеся в индексе. И каждый раз, когда мы будем это делать, мы будем терпеть дополнительные накладные расходы, связанные с логическими (и, возможно, физическими) операциями ввода-вывода. Мы можем получить 26 Мбайт накладных расходов, связанных с поиском по закладкам C,333 ключа х 8 К/страницу). Теперь подсчитайте стоимость простого последовательного сканирования таблицы. Если на каждой странице данных помещается в среднем 50 записей и мы должны просмотреть всю таблицу, чтобы найти тех, кто принадлежит к демократической партии, нам понадобится всего лишь около 200 логических операций ввода-вывода A0,000 записей х 50 записей на страницу = 200 страниц). Эта разница — основная причина, по которой некластеризованный индекс игнорируется и вместо него используется сканирование таблицы/кластерного индекса. Когда некластеризованный индекс становится достаточно полезным оптимизатору? В нашем примере магическое число — 200. Оптимизатор должен был полагать, что получение данных с использованием индекса потребует не менее 200 операций ввода-вывода. Соответственно, можно сделать вывод, что использование индекса — более эффективный способ доступа к информации по сравнению с простым сканированием всей таблицы. Первоначальное значение 3,333 может быть уменьшено, если к индексу добавить столбцы (а также к запросу), которые сделают его более селективным. Однако здесь есть одно «но». Когда вы добавляете столбцы к индексу, чтобы сделать его более селективным, возрастает количество накладных расходов, связанных с тем, что индекс проходит В-дерево. Увеличивая индекс, вы тем самым увеличиваете стоимость перемещения по нему. Иногда бывает дешевле просто просканировать сами данные, чем связываться с накладными расходами, возникающими в результате прохождения по дереву.
Статистика 413 Хранение статистики SQL Server хранит статистику для индекса или столбца в столбце statb I ob таблицы sys indexes. Statblob имеет тип данных image и хранит гистограмму выборок значений ключа индекса или столбца. Для составных индексов используется выборка только по значениям первого столбца, но плотность значений вычисляется и для других столбцов. Выбирая индекс при оптимизации запроса, оптимизатор решает, соответствует ли индекс столбцам в критерии фильтра, определяет селективность индекса для этого критерия и оценивает стоимость доступа к тем данным, которые ищет запрос. Если индекс содержит только один столбец, его статистика состоит из гистограммы и одного значения плотности. Если индекс содержит несколько столбцов, в статистике содержится единственная гистограмма, а также значения плотности для комбинаций, получаемых путем последовательного (слева направо) прибавления префиксов столбцов. Оптимизатор использует комбинацию гистограммы индекса и плотностей — то есть статистику — для того, чтобы определить, насколько полезен индекс для данного запроса. Тот факт, что гистограмма хранится только для первого столбца составного индекса, является одной из причин, по которой вы должны первым в индексе помещать наиболее селективный столбец, состоящий из нескольких столбцов. Тогда гистограмма будет более полезна оптимизатору. Более того, это одна из причин, по которой иногда полезно разбивать составные индексы на несколько индексов, состоящих из одного столбца. Поскольку Server может использовать пересечение и объединение нескольких индексов одной таблицы, у вас остаются преимущества индексирования, и вы получаете дополнительное преимущество — гистограмму для каждого столбца (здесь также может помочь статистика для столбцов). Это не общая инструкция. Не бросайтесь удалять все свои составные индексы. Просто помните, что разбиение составных индексов иногда полезно для увеличения производительности. Статистика столбцов Помимо статистики для индексов, SQL Server может также создавать статистику для неиндексируемых столбцов (это делается автоматически, когда вы запрашиваете неиндексируемый столбец и опция AUTO_CREATE_STAT I ST ICS разрешена для базы данных). Имея возможность определить вероятность того, что данное значение может встретиться в столбце, оптимизатор может определить, как лучше выполнить запрос. Это позволяет оптимизатору оценить, сколько записей определенной таблицы будет участвовать в объединении, и соответственно более тщач ельно выбрать порядок объединения. Также оптимизатор может использовать статистику столбцов, чтобы предоставить гистограммы для других столбцов в составном индексе. Проще говоря, чем больше информации о своих данных вы можете предоставить оптимизатору, тем лучше. Просмотр статистики SQL Server использует статистику для того, чтобы распределить значения ключа по таблице. Гистограмма, которая хранится как часть статистики, содержит рас-
414 Глава 17. Размышления о производительности пределение по меньшей мере 200 значений первого ключевого столбца индекса. Помимо гистограммы, в statb I ob также содержится: ш количество записей, по которым рассчитана гистограмма и плотность; ■ средняя длина ключа индекса; ■ дата и время, когда в последний раз была сгенерирована статистика; ■ значения распределения для других комбинаций ключевых столбцов с первым ключевым столбцом в начале. Диапазон значений ключа между каждыми двумястами значениями гистограммы называется шагом. Каждое значение выборки обозначает начало шага, а каждый шаг хранит три значения: ■ EQ_R0WS — количество записей со значением ключа, соответствующих значению выборки; ■ RANGE_R0WS — количество других значений в диапазоне; ■ RANGE_0ENS I TY — значение плотности самого диапазона. ОВСС SH0W_STAT I ST ICS показывает значения EQ_R0WS и RANGE_ROWS и используетзна- " чение RANGE_DENS I TY для того, чтобы вычислить 01 ST I NCT_RANGE_R0WS и AVGJANGEJOWS для каждого шага. 01 ST I NCT_RANGE_R0WS (общее число различных записей в диапазоне шага) вычисляется путем деления 1 на RANGE_DENS I TY, a AVG_RANGE_R0WS (среднее количество записей для каждого уникального значения ключа) — путем умножения RANGE_R0WS на RANGE_DENS ITY. Обновление статистики Статистика может обновляться несколькими способами. Первый и наиболее очевидный — заключается в использовании опции AUTO_UPDATE_STAT I ST I CS базы данных (ее можно включить с помощью ALTER DATABASE или sp_dbopt i on). Когда статистика генерируется автоматически, SQL Server использует выборочные данные (чтобы не сканировать всю таблицу целиком) для того, чтобы ускорить процесс. Чаще всего это работает, но иногда может привести к тому, что статистика станет менее полезной, чем она могла бы быть. С автоматическим обновлением статистики тесно связано автоматическое создание статистики. Статистика создается автоматически, если для базы данных разрешена опция AUTO_CREATE_STAT I ST I CS и вы осуществляете запрос, фильтр которого основан на неиндексируемом столбце. SQL Server автоматически создаст множество статистики для столбцов. Второй способ обновления статистики заключается в использовании команды UPDATE STAT I ST I CS. Этот способ был единственным в SQL Server до версии 7.0. UPDATE STAT I ST I CS может использовать либо выборочные данные, как в случае с автоматическим обновлением, либо полное сканирование таблицы, за счет чего статистика получается лучше, хотя сканирование и длится дольше. С командой UPDATE STATISTICS тесно связана команда CREATE STATISTICS. Она используется для ручного создания статистики столбцов. После того как статистика создана, она может обновляться либо с использованием автоматического обновления, либо с помощью команды UPDATE STATISTICS так же, как для обычной статистики индексов.
Статистика 415 В SQL Server есть несколько хранимых процедур для упрощения создания и обновления статистики. Sp_updatestats выполняет UPDATE STAT I ST ICS для всех пользовательских таблиц в текущей базе данных. В отличие от команды UPDATE STAT I ST I CS, sp_updatestats не может выполнять полное сканирование таблицы для создания статистики — она всегда использует выборочные данные. Если вам требуется статистика, созданная полным сканированием, используйте UPDATE STAT I ST I CS. Sp_createstats также удобна. Она может автоматизировать процесс создания статистики столбцов для всех подходящих столбцов во всех подходящих таблицах базы данных. Подходящие столбцы включают невычисляемые столбцы с типами данных, отличными от text, ntext или i mage, для которых нет статистики столбцов или индексной статистики с этим столбцом на первом месте. Подходящие таблицы включают все пользовательские (не системные) таблицы. Sp_autostats позволяет вам контролировать автоматическое создание статистики на уровне таблицы или индекса. Вместо того чтобы полагаться на опцию базы данных AUTO_UPDATE_STAT I ST I CS, вы можете включать/отключать автоматическую генерацию статистики на более детальном уровне. Например, если у вас есть задача, которая запускается ночью для обновления статистики большой таблицы с полным сканированием, вы можете отключить автоматическое обновление статистики для этой таблицы. Используя sp_autostats, вы можете отключить автоматическое обновление статистики для одной конкретной таблицы и оставить его включенным для остальной базы данных. Для обновления статистики больших таблиц, даже с использованием выборочных данных, может потребоваться некоторое время, а также значительная часть различных ресурсов (процессора, подсистемы ввода-вывода). Помните, что негативное влияние отсутствия статистики или наличия устаревшей статистики на производительность значительно превышает выгоды, связанные с отключением автоматического создания/обновления статистики. Вы должны отключать автоматическое обновление/создание статистики только тогда, когда многочисленные тесты показывают, что не существует другого способа повышения производительности. sp_showstatdate В листинге 17.8 приведена хранимая процедура, которую можно использовать для того, чтобы следить за обновлением статистики. Она показывает тип статистики, последнюю дату обновления и массу другой информации, которая может быть полезна при управлении статистикой индексов и столбцов. Вот код этой процедуры. Листинг 17,8. Процедура sp_showstatdate помогает следить за обновлением статистики CREATE PROC sp_showstatdate @tabmask sysname=X. @indmask sysname=T AS SELECT LEFT(CAST(USER_NAME(uid)+'.'+o.name AS sysname).30) AS TableName, ; LEFT(i.name,30) AS IndexName, CASE WHEN INDEXPROPERTY(o.id.i.name,'IsAutoStatistics')=l THEN 'AutoStatisties' WHEN INDEXPROPERTY(o.id,i.name.'IsStatistics')=l THEN 'Statistics' ELSE 'Index' END AS Type. STATS_DATE(o.id, i.indid) AS StatsUpdated, продолжением Л
416 Глава 17. Размышления о производительности Листинг 17.8 {продолжение) rowcnt, rowmodctr, ISNULL(CAST(rowmodctr/CAST(NULLIF(rowcnt.O) AS decimalB0,2))*100 AS int).0) AS PercentModifiedRows. CASE i.status & 0x1000000 WHEN 0 THEN 'No' ELSE 'Yes' END AS [NoRecompute?]. i.status FROM dbo.sysobjects о JOIN dbo.sysindexes i ON (o.id = i.id) WHERE o.name LIKE Otabmask AND i.name LIKE @indmask AND OBJECTPROPERTY(o.id.'IsUserTable')=l AND i.indid BETWEEN 1 AND 254 ORDER BY TableName. IndexName GO USE pubs GO EXEC sp_showstatdate (Результаты сокращены) TableName IndexName Type StatsUpdated dbo.authors au_fname Statistics 2000-07-02 19:42:04.487 dbo.authors aunmind Index 2000-06-30 20:54:56.737 dbo.authors UPKCL_auidind Index 2000-06-30 20:54:56.737 dbo.dtproperties pk_dtproperties Index NULL dbo.employee employeejnd Index 2000-06-30 20:54:45.280 dbo.employee PK_emp_id Index 2000-06-30 20:54:45.297 Оптимизация запросов Когда вы посылаете запрос SQL Server, он начинает его выполнение с оптимизации. Используя статистику и индексы, которые есть в его распоряжении, оптимизатор запросов SQL Server разрабатывает план выполнения, который, по его мнению, является наиболее эффективным для выполнения вашего запроса. Эта оптимизация основывается на оценке стоимости: выигрывают планы с меньшей стоимостью (оценочным временем выполнения). Процесс оценки стоимости заключается в уменьшении количества операций ввода-вывода, так как они чаще всего ограничивают повышение производительности системы при работе с большими объемами данных. Как показано на рис. 17.1, процесс разработки плана выполнения состоит из нескольких этапов или шагов. 1. Определение простых планов. 2. Упрощение плана. 3. Загрузка статистики. . 4. Оценка стоимости планов. 5. Оптимизация для параллельного выполнения. 6. Вывод плана. Давайте разберем каждый шаг более подробно.
Оптимизация запросов 417 Поиск тривиального плана Да Упрощение Т Загрузка статистики Повторять, пока предварительные планы не закончатся ->>г Поиск самого дешевого плана Оптимизация плана для распараллеливания Да Да Оптимизация для последовательного плана Создание плана Рис. 17.1. Процесс оптимизации запроса Простая оптимизация плана Так как оценка планов может быть долгим процессом, у SQL Server есть специальные механизмы оптимизации запросов, для которых на самом деле может быть только один план. Например, SELECT из таблицы с единственным уникальным покрывающим индексом или оператор INSERT с частью VALUES. SQL Server использует 14 Зли 9ХЧ
418 Глава 17. Размышления о производительности встроенные эвристические правила для определения всех возможных простых планов. Если план запроса может быть упрощен без потери производительности, оптимизатору не придется оценивать все возможные планы для запроса. Упрощение Если оптимизатор простых планов не может найти план для запроса, он упрощает запрос, используя синтаксические преобразования. Например, свертку констант, преобразование HAV ING в WHERE, где это возможно, преобразование ! = (Эрагтв < @рагт OR >@parm и т. д. Эти преобразования не требуют оценки стоимости, индексов или статистики, но в результате может быть выработан более эффективный план. Загрузка статистики После того как план был упрощен, оптимизатор загружает статистику индексов и столбцов, а также дополнительную информацию для того, чтобы начался процесс генерации плана, основанный на оценке стоимости. Это означает, что оптимизатор обращается к таблице sys i ndexes и другим системным таблицам и загружает необходимую информацию для таблиц, использующихся в запросе. Оптимизация, основанная на оценке стоимости Оптимизация, основанная на оценке стоимости, разделяется на этапы, которые позволяют создать план по частям. Поскольку для оптимизации плана у SQL Server есть масса возможностей, оптимизатор первым делом пытается применить более простые способы оптимизации, чтобы уменьшить время создания плана. На каждом удачном этапе оптимизатор пытается использовать более дорогие трансформации запроса, по сравнению с трансформациями предыдущего этапа, но которые, возможно, позволят создать более эффективный план. Этот процесс повторяется до тех пор, пока не создается план с пороговым значением стоимости. Начальные этапы данного вида оптимизации самые простые. Оптимизатор спроектирован так, что большинство планов может быть создано во время предварительных этапов оптимизации стоимости. За счет этого стоимость генерации планов остается сравнительно низкой и гарантируется, что более сложные способы оптимизации будут применены только для более сложных запросов. Каждый раз, когда генерируется план выполнения, SQL Server, основываясь на внутреннем пороговом значении, оценивает, является ли этот план достаточно дешевым для выполнения. Если план достаточно дешевый, оптимизатор выводит его. Если нет — продолжает искать. На подготовительных этапах обычно находятся планы, использующие вложенные циклы для таблиц с единственным подходящим индексом. Эти простые планы хорошо подходят для значительного числа запросов, поэтому оптимизатор спроектирован так, чтобы создавать эти простые планы в самых различных ситуациях. Каждый этап оптимизации состоит в выполнении множества правил для трансформации запроса в последовательность разрешимых шагов. Если оптимизатор считает, что стоимость выполнения этих шагов достаточно низкая, он выдает план.
Оптимизация запросов 419 Полная оптимизация Если на предварительных этапах оптимизатор не находит подходящего плана, он начинает этап полной оптимизации запроса. Как раз на этом этапе может быть разработан план параллельного выполнения. Оптимизатор принимает решение о распараллеливании, если достигается пороговое значение для параллелизма. Это значение представляет собой число секунд, необходимое для выполнения плана последовательно (не параллельно) на конкретной аппаратной конфигурации. Если SQL Server работает на машине с несколькими процессорами и оптимизатор считает, что выполнение последовательной версии плана превысит пороговое значение, он строит распараллеленный план для запроса. Оптимизатор предполагает, что запрос достаточно долгий и сложный и что его лучше выполнять параллельно. Он полагает, что увеличение производительности за счет параллельного выполнения перекроет потери, связанные с инициализацией, синхронизацией и завершением параллельного плана. Даже если в конечном итоге на основе других факторов последовательный план будет предпочтительнее, при наличии данных условий оптимизатор будет стремиться использовать план параллельного выполнения. Иногда параллельный план выбирается, даже если конечная стоимость запроса получается меньше порогового значения, потому что решение использовать параллельный план или нет основывается на оценке времени запроса, полученном до окончания процесса полной оптимизации. Оценка селективности Для того чтобы правильно оценить относительную стоимость плана запроса, оптимизатор должен быть способен точно подсчитать количество записей, которые вернет запрос. Как я уже упоминал ранее, это называется селективностью, и оценка этого параметра критична для оптимизатора. Селективность оценивается, исходя из сравнения критерия запроса и статистики ключей индексов или столбцов, используемых в критерии. Селективность говорит нам, что одному значению ключа будет соответствовать одна-единствен- ная запись, другому — 10 000 записей. Она позволяет нам подсчитать, сколько записей соответствует значению столбца или ключа, на основании которого мы производим поиск. Это, в свою очередь, помогает нам определить наиболее эффективный способ доступа к этим записям. Очевидно, вы будете применять разные способы для того, чтобы получить одну запись и 10 000 записей. Если оптимизатор обнаружит, что статистика для индекса или столбца в критерии фильтра отсутствует, он сможет автоматически создать статистику для столбца, если для базы данных разрешена опция AUT0_CREATE_STAT ISITCS. Первый раз стоимость создания статистики скажется на скорости выполнения, но последующие запросы будут выполняться быстрее за счет использования новой статистики. Оптимизация аргументов поиска SARG, или аргумент поиска, — это часть запроса, которую оптимизатор потенциально может использовать совместно с индексом для ограничения результатов, возвращаемых запросом. Оптимизатор пытается выделить аргументы поиска в кри- \
420 Глава 17. Размышления о производительности терии запроса, чтобы определить наилучшие индексы, необходимые для выполнения запроса. Аргументы поиска имеют форму: Столбец оператор Константа/Переменная (термины могут быть изменены), где Столбец — столбец таблицы; оператор — =, >=, <=, >, <, о, !=,!>,!<, BETWEEN и LIКЕ (некоторые выражения L! КЕ могут быть преобразованы в аргументы поиска, некоторые — нет); и Константа/Переменная — это значение константы, ссылка на переменную или одна из нескольких функций. Даже если некоторые из этих операторов не могут быть использованы в аргументах поиска, оптимизатор может преобразовать их в выражения, которые могут быть аргументами поиска. Рассмотрим запрос в листинге 17.9. Листинг 17.9. Оптимизатор может превратить != в аргумент поиска SELECT * FROM authors WHERE aujname != 'Greene' Можно ли преобразовать часть WHERE в аргумент поиска? Да, можно. Взгляните на изменение, которое происходит в этой выдержке из плана: SEEK:([authors].[aujname] < 'Greene' OR [authors].[aujname] > 'Greene') Оптимизатор достаточно умен, чтобы знать, что выражение х ! = @parm то же самое, что х < @parm OR x > @parm, и соответственно его преобразовывает. Так как две ветки части OR могут быть выполнены параллельно и затем объединены, это приводит к использованию индекса для обслуживания части WHERE (листинг 17.10). Листинг 17.10. Выражения LIKE также могут представлять собой аргументы поиска SELECT * FROM authors WHERE aujname LIKE 'Grf' Преобразуется ли это в аргумент поиска? Да. И опять оптимизатор преобразует критерий в части WHERE в кое-что более приемлемое: SEEK:([authors]. [aujname] >- 'GQJ AND [authors], [aujname] < 'GS') Вот еще один запрос. Листинг 17.11. Оптимизатор может преобразовать !> в кое-что более приемлемое SELECT * FROM authors WHERE aujname !> 'Greene' Может ли оптимизатор использовать индекс для выполнения этого запроса? Да, может. Вот выдержка из плана: SEEK:([authors].[aujname] <= 'Greene') И еще один запрос. Листинг 17.12. Оптимизатор может использовать выражения, включающие !< SELECT * FROM authors WHERE aujname !< 'Greene' И вот его план: SEEK: ([authors].[aujname] >» 'Greene') Улавливаете связь? Оптимизатор пытается преобразовать те выражения, которые кажутся не удовлетворяющими определению аргументов поиска, в кое-что более подходящее для этих целей.
Оптимизация запросов 421 Аргументы поиска могут быть объединены с помощью AND для создания составных выражений. Правило для определения, является ли выражение аргументом поиска, заключается в следующем: выражение может быть аргументом поиска, если оптимизатор может определить, что оно представляет собой сравнение значения ключа индекса с константой или переменной. Выражение, которое сравнивает два столбца или два выражения, не является аргументом поиска. Типичная ошибка всех новичков заключается в том, что в выражении они используют столбец, сравнивая его с константой или переменной. Чаще всего это приводит к тому, что выражение не становится аргументом поиска, потому что оптимизатор не знает, что оно вычисляет, так как это неизвестно до выполнения. Главное — так изолировать столбцы в выражениях такого типа, чтобы они не участвовали в вычислениях или вообще не были задействованы. Применяйте простые алгебраические принципы, чтобы вычисления в выражении производились с переменными или константами, а сами столбцы не были задействованы. Порядок объединения и выбор типа объединения Кроме того, что оптимизатор выбирает индексы и определяет аргументы поиска, он также выбирает порядок и определяет стратегию объединения. Выбор индексов и определение стратегии объединения идут рука об руку: индексы влияют на типы стратегий объединения, которые могут быть использованы, а стратегия объединения влияет на типы индексов, которые необходимы оптимизатору для создания эффективного плана. SQL Server поддерживает три типа объединений. 1. Вложенный цикл хорошо работает с небольшой внешней таблицей и индексом на внутренней. 2. Объединение хорошо работает, если оба входных потока отсортированы по объединяемому столбцу (если требуется, оптимизатор может отсортировать только один из входных потоков). 3. Хэширование применяется в ситуациях, когда нет подходящих индексов. Обычно создание индекса (такого, что может быть выбрана другая стратегия объединения) приводит к увеличению производительности. Оптимизатор определяет стратегию объединения, которую надо использовать для выполнения запроса. Он вычисляет стоимость каждой стратегии и выбирает из них одну с наименьшей стоимостью. Он оставляет за собой право изменить порядок таблиц в части FROM, если это приведет к увеличению производительности запроса. Вы всегда можете выяснить, произошло ли это, взглянув на план выполнения. Порядок таблиц в плане выполнения соответствует порядку, который оптимизатор посчитал лучшим. Используя опцию ОРТ I ON (FORCE ORDER) для запроса или SET FORCEPLAN ON для сессии, вы можете запретить оптимизатору определять порядок объединения. Тогда он будет объединять таблицы в том порядке, в котором они перечислены в части FROM. Однако помните, что строгое определение порядка объединения может оказать побочный эффект на выбор стратегии объединения. Например, рассмотрим запрос в листинге 17.13.
422 Глава 17. Размышления о производительности Листинг 17.13. При использовании RIGHT OUTER объединений оптимизатор часто изменяет порядок таблиц. SELECT o.Orderld. p.Product Id FROM [Order Details] о RIGHT JOIN Products p ON (o.ProductId=p.ProductId) Вот его план выполнения. Листинг 17.14. Оптимизатор изменяет порядок таблиц Order Details и Products StmtText SELECT o.Orderld, p.Productld FROM [Order Details] о RIGHT JOIN Products p ON (o.ProductId=p.ProductId) |--Nested LoopstLeft Outer Join. OUTER REFERENCES:(p.ProductID)) j--Index Scan(OBJECT:(Northwind.dbo.Products.SuppIiersProducts AS p)) |--Index SeektOBJECT:(Northwind.dbo.[Order Details].ProductID AS o), SEEK:(o.ProductID=p.ProductID) ORDERED FORWARD) Обратите внимание на то, что используется объединение вложенного цикла и что изменен порядок таблиц (в начале плана идет таблица Products, несмотря на то, что в части FROM первой идет Order Deta i I s). Давайте теперь вмешаемся в порядок объединения, используя подсказку запроса FORCE ORDER (листинг 17.15). Листинг 17.15. Можно определить строгий порядок объединения, используя подсказку FORCE ORDER SELECT o.Orderld. p.Productld FROM [Order Details] о RIGHT JOIN Products p ON (o.ProductId=p.ProductId) OPTION(FORCE ORDER) Давайте посмотрим, что стало с планом запроса (листинг 17.16). Листинг 17.16. Строгое определение порядка объединения иногда приводит к изменению стратегии объединения StmtText SELECT o.Orderld, p.Productld FROM [Order Details] о RIGHT JOIN Products p ON (o.ProductId=p.ProductId) 0PTI0N(FORCE ORDER) |--Merge JointRight Outer Join. MANY-TO-MANY MERGE:(o.ProductID)= (p.Product ID), RESIDUAL:(o.ProductID=p.ProductID)) |--Index ScantOBJECT:(Northwind.dbo.[Order Details]. ProductsOrder_Details AS o), ORDERED FORWARD) [--Clustered Index ScanCOBJECT-.(Northwind.dbo.Products. PK_Products AS p), ORDERED FORWARD) Так как оптимизатор не может изменить порядок таблиц, он переключается на стратегию объединения слиянием. Это менее эффективно, чем если бы мы позволили оптимизатору самому определить порядок таблиц и использовать вложенный цикл для объединения таблиц. Объединение с использованием вложенных циклов Объединения с использованием вложенных циклов представляют собой цикл в цикле. В объединениях этого типа одна таблица используется для внешнего цикла, другая — для внутреннего. Для каждой итерации внешнего цикла полностью
Оптимизация запросов 423 проходится внутренний цикл. Это хорошо работает для небольших и средних по размеру таблиц, но с увеличением циклов эта стратегия становится неэффективной. Общая структура процесса такова: 1. Сначала вы находите запись в первой таблице. 2. Затем используете значения этой записи, чтобы найти запись во второй таблице. 3. Повторяете этот процесс, пока в первой таблице не останется записей, соответствующих критерию поиска. Оптимизатор оценивает не менее четырех комбинаций объединения, даже если они не указаны в предикате объединения. Он находит баланс между стоимостью оценки дополнительных комбинаций и необходимостью уменьшить суммарную стоимость создания плана запроса. Объединения с использованием вложенных циклов более эффективны, чем объединения слиянием и хэшированием при работе с данными небольшого или среднего объема. Оптимизатор запросов использует объединения с вложенными циклами, если внешнее множество данных сравнительно небольшое, а внутреннее — проиндексировано и довольно большое. Оптимизатор так определяет порядок таблиц, чтобы меньшее множество являлось внешней таблицей. Для этого объединения необходим хороший индекс по внутренней таблице. Оптимизатор всегда использует эту стратегию с объединениями с нежесткими условиями. Объединения слиянием При работе с большими наборами данных более эффективны объединения слиянием, чем объединения с использованием вложенных циклов. Для работы этой стратегии обе таблицы должны быть отсортированы по объединяемым столбцам. Оптимизатор обычно выбирает объединение слиянием, когда обрабатывает большие объемы данных, которые уже отсортированы по объединяемым столбцам. Он может использовать деревья индексов для сортировки входных данных, а также для увеличения производительности при операциях GROUP BY, CUBE и ORDER BY сортировка будет проводиться только один раз. Если входные данные еще не отсортированы, оптимизатор, возможно, решит сначала отсортировать их, чтобы использовать стратегию объединения слиянием, если он решит, что она более эффективна, чем объединение с использованием вложенных циклов. Это случается очень редко и в плане запроса обозначается оператором SORT. Объединение слиянием состоит из следующих пяти шагов. 1. Получение первых входных значений из каждой таблицы. 2. Их сравнение. 3. Возвращение записей, если значения равны. 4. Если значения не равны, отбрасывание меньшего значения и использование следующего входного значения из этой таблицы для следующего сравнения. 5. Повторение процесса, пока все записи одной из таблиц не будут обработаны. Оптимизатор делает только один проход по таблице. Процесс прекращается после того, как завершается обработка всех входных значений одной из таблиц. Любые значения, оставшиеся в другой таблице, не обрабатываются. Оптимизатор может применять эту стратегию для всех типов реляционных объединений, кроме CROSS JO IN и FULL JO I N. Также эта стратегия может быть использо-
424 Глава 17. Размышления о производительности вана в случае применения оператора UN I ON (потому что таблицы должны быть отсортированы для удаления повторяющихся значений). Объединения с использованием хэширования Объединение с использованием хэширования также более эффективно при работе с большими наборами данных, чем объединение с использованием вложенных циклов. К тому же, объединение с использованием хэширования хорошо работает, если таблицы не отсортированы по столбцам, по которым проводится объединение. Оптимизатор обычно выбирает объединение с использованием хэширования при работе с большими объемами входных данных, когда нет ни одного индекса для объединения или когда индекс существует, но не может быть использован. SQL Server осуществляет объединение с использованием хэширования, вычисляя хэш записей из меньшей таблицы (обозначается как создаваемая таблица) и вставляя их в хэш-таблицу. Затем SQL Server обрабатывает большую таблицу (обозначается как исследуемая таблица) по одной записи, сканируя хэш-таблицу для поиска совпадений. Так как значения для хэш-таблицы получаются из меньшей таблицы, размер таблицы сведен к минимуму, а так как вместо реальных значений используются их хэши, сравнение таблиц может быть осуществлено очень быстро. Объединения с использованием хэширования — вариация концепции хэш-индексов, которые были доступны в некоторых продвинутых СУБД на протяжении ряда лет. В случае с хэш-индексами хэш-таблицы хранятся постоянно — они являются индексом. Данные хэшируются в слоты, которые имеют одинаковое значение хэша. Если индекс имеет уникальный непрерывный ключ, это означает, что существует минимальная совершенная хэш-функция: каждое значение хэшируется в свой собственный слот и между ними в индексе нет промежутков. Если индекс уникальный, но не непрерывный, может существовать совершенная хэш-функция, которая хэширует каждое значение в свой собственный слот, однако между ними могут существовать промежутки. Если создаваемая и исследуемая таблицы выбраны неправильно (например, из- за неточностей при оценке плотности), оптимизатор изменяет порядок работы с ними, используя процесс смена роли. Эта стратегия может применяться для всех типов реляционных объединений (включая операции UN I ON и DIFFERENCE), за исключением CROSS JO I N. Хэширование также может применяться для группировки данных и удаления повторяющихся значений (например, с векторными агрегатами SUM(Quant ity) GROUP BY Product Id). Если хэширование используется таким образом, оптимизатор использует одну и ту же таблицу в качестве создаваемой и исследуемой. Если объединяемые множества большие и примерно одного размера, производительность объединения с использованием хэширования сравнима с производительностью объединения слиянием. Если объединяемые множества большие, но значительно отличаются друг от друга по размеру, производительность объединения с хэшированием значительно выше производительности объединения слиянием. Подзапросы и альтернатива объединениям Подзапрос — это запрос, расположенный внутри другого запроса. Обычно подзапросы получают данные для таких операторов-предикатов, как: IN, ANY и EX ISTS -
Оптимизация запросов 425 или единственное значение для зависимого столбца или для присваивания переменной. Подзапросы могут использоваться в разных точках, в том числе в частях запроса WHERE и HAVING. Важно понять: объединения в действительности не лучше подзапросов. Часто оптимизатор нормализует подзапрос в объединение, но это не означает, что применение подзапроса неэффективно. Когда вы переделываете запрос, чтобы избежать негативного влияния объединений на производительность, помните, что вы можете использовать переменные типа table и временные таблицы для хранения данных с целью их дальнейшей обработки. Для очень сложных запросов это может быть лучшим выходом из ситуации, так как это позволяет контролировать весь процесс оптимизации. Вы можете разбить запрос на несколько этапов и контролировать, что и когда должно выполняться. И для простых и для сложных запросов производные таблицы обеспечивают те же преимущества. Производные таблицы позволяют до некоторой степени сделать обработку запроса последовательной. Они работают, как скобки в выражении (и, в действительности, сами ограничены скобками), то есть определяют порядок событий. Если вы вносите части сложного SELECT в ту производную таблицу, к которой затем применяете оставшуюся часть SELECT, вы, по сути, говорите следующее: «Сделай сначала это, а затем передай результат во внешний SELECT». Возможность последовательной обработки запроса имеет свои преимущества (как в хранимых процедурах) в том смысле, что запрос остается простым и легким в использовании. Логические и физические операторы Физические операторы описывают, что делает SQL Server для выполнения запроса. Логические операторы описывают реляционные операции, используемые для обработки выражения, и соответствуют тем операциям в вашем коде, которые на самом деле вызывают использование физических операций. Часто одному логическому оператору соответствует несколько физических. Так как план состоит из физических операций, это означает, что все шаги плана выполнения будут иметь соответствующие физические операторы, однако не все будут иметь логические. Каждый шаг плана выполнения соответствует физическому оператору: план выполнения состоит из последовательностей физических операторов. Графический план выполнения в Query Analyzer показывает данные операции в появляющемся окне подсказки желтого цвета. Если кроме физического оператора шаг имеет логический оператор, он также будет показан в окне: справа от физического оператора через слэш. В случае текстового представления плана столбец PhysicalOp содержит физический оператор, а столбец LogicalOp содержит логический оператор этого шага. Давайте посмотрим это на примере. Рассмотрим запрос в листинге 17.17. Листинг 17.17. Для этого запроса необходимо внутреннее объединение SELECT * FROM Orders о JOIN [Order Details] d ON (o.OrderlD = d.OrderlD) Оптимизатор выбрал для этого запроса объединение слиянием, хотя сам запрос, очевидно, этого не требует. С реляционной точки зрения, запрос осуществляет внутреннее объединение между двумя таблицами. Вот выдержка из его плана выполнения. i
426 Глава 17. Размышления о производительности Листинг 17.18. Логическая операция — INNER JOIN; физическая — MERGE JOIN PhysicalOp LogicalOp Argument Merge Join Inner Join MERGE:([o].[OrderID])=([d].[Or Clustered Index Scan Clustered Index Scan OBJECT:([Northwind].[dbo].[Ord Clustered Index Scan Clustered Index Scan OBJECT:([Northwind].[dbo].[Ord Обратите внимание на то, что MERGE JO IN показан в столбце Phys i са I Op в качестве оператора. Это происходит на самом деле при выполнении операции, показанной в столбце Log i са I Op: I NNER JO I N. Внутреннее объединение мы запросили в нашем запросе. Сервер выбрал MERGE JO IN в качестве физического оператора, чтобы выполнить запрос. Кроме решения, какой индекс и какую стратегию объединения использовать, оптимизатор также принимает решения относительно других типов операций. Некоторые из них приведены далее. DISTINCT Если оптимизатор встречает в запросе ключевое слово DI ST INCT или UN I ON, он должен будет удалить дубликаты, прежде чем вернуть результирующее множество. Здесь у него есть два варианта: он может отсортировать данные, чтобы удалить повторяющиеся значения, или построить хэш. Оптимизатор может реализовать логические операции DI ST INCT или UN I ON, используя хэширование или сортировку в качестве физических операторов. В этом случае применяется физический оператор St ream Aggregate, часто используемый в запросах с GROUP BY. GROUP BY Оптимизатор может выполнять запросы с GROUP BY, используя обычную сортировку или хэширование. Физические операторы могут быть HASH или SORT, но логический оператор всегда будет AGGREGATE. Также STREAM AGGREGATE часто применяется для операций GROUP BY. Так как для группировки данных оптимизатор может выбрать операцию HASH, результирующее множество может быть не отсортировано. Нельзя полагаться на GROUP BY в случае автоматической сортировки данных. Если вы хотите, чтобы результирующее множество было отсортировано, добавьте в запрос часть ORDER BY. ORDER BY Даже для ORDER BY у оптимизатора может иметься несколько решений. Предположив, что не существует кластерного индекса, который уже имеет отсортированные данные, оптимизатор может придумать способ, как возвратить результирующее множество отсортированным в нужном порядке. Он может отсортировать данные (чего мы и ждем от него) или может пройти листовой уровень подходящего некла- стеризованного индекса. Решение оптимизатора зависит от нескольких факторов, самый важный из которых — селективность (то есть сколько записей вернет запрос). Это верно и в случае покрытия индекса: может ли ^кластеризованный индекс покрыть запрос? Если количество записей сравнительно мало, возможно, дешевле будет использовать некластеризованный индекс, чем сортировать всю таблицу. Аналогично, если индекс может покрыть запрос, у вас практически есть второй кластерный индекс, и оптимизатор, скорее всего, будет его использовать.
Итоги 427 Спулинг Оператор Spoo I i ng в плане запроса означает, что оптимизатор сохраняет результаты промежуточного запроса в таблицу для дальнейшей обработки. В случае I azy spoo! (пассивного заполнения) рабочая таблица заполняется по мере необходимости. В случае eager spool (активного заполнения) таблица заполняется сразу, за один шаг. Оптимизатор предпочитает пассивное заполнение, поскольку это, возможно, позволит избежать того, что рабочая таблица будет полностью заполнена на основании логики в глубине плана. Есть случаи, в которых необходимы активные заполнения. Но в основном оптимизатор предпочитает пассивные заполнения, потому что они уменьшают нагрузку. Spoo I -операции могут осуществляться как с таблицами, так и с индексами. Оптимизатор использует rowcount spoo I в том случае, когда все, что ему требуется, — это знать, существует ли соответствующая запись. В табл. 17.3 приведены физические операторы, используемые оптимизатором SQL Server, и их описание. Таблица 17.3. Физические операторы оптимизатора Физический Значение оператор ASSERT Означает, что подзапрос используется в том случае, если ему разрешено вернуть только одну запись (чтобы избавиться от ASSERT, используйте в подзапросе SELECT TOP 1) BOOKMARK LOOKUP Означает, что RID или значение ключа кластерного индекса используется для поиска данных в соответствующей таблице или кластерном индексе CONSTANT SCAN Означает, что оптимизатор знает, что условие никогда не будет истинным CURSOR OPERATIONS Операции с курсорами на стороне сервера FILTER Означает, что количество данных уменьшается (возможно, с использованием критерия из части WHERE) перед продолжением обработки JOINS Используется объединение nested loop, merge или hash SCAN Означает последовательный поиск на листовом уровне индекса SEEK Двоичный (не последовательный) поиск в В-дереве SORT Означает, что данные сортируются перед следующим шагом SPOOL Означает, что оптимизатор сохраняет результаты промежуточного запроса в рабочую таблицу для дальнейшего использования STREAM AGGREGATE Векторная агрегация или группировка Итоги Вы только что совершили путешествие в глубины процессов обработки запросов и оптимизации планов SQL Server. Вы узнали следующее: * как работают данные процессы — в этом ключ к разработке эффективных баз данных и написанию быстрых запросов; * как работают внутренние механизмы S QL Server для обработки запросов — осознав это, вы получите инструменты, необходимые для создания собственных методов настройки производительности.
л о Отладка I О и профилирование Программу нельзя разрушить полностью — по ее можно сильно испортить. Мартин Фоулер' В этой главе мы поговорим об отладке и профилировании хранимых процедур на Transact-SQL, о нагрузочном тестировании SQL Server с помощью запросов на Transact-SQL, а также о свободно распространяемых средствах нагрузочного тестирования, которые вы сможете найти на прилагаемом к книге компакт-диске. Отладка Лучшее средство для отладки хранимых процедур на Transact-S QL входит в комплект поставки SQL Server. Теперь Query Analyzer включает в себя полнофункциональный отладчик хранимых процедур. Вы можете устанавливать точки прерывания, определять отслеживаемые переменные, то есть делать то, для чего и предназначен отладчик, — отлаживать код. Процедуры можно отлаживать от текущей версии SQL Server вплоть до SQL Server 6.5 Service Pack 2. Интерфейс, посредством которого выполняется отладка, известен как SQL Server Debug Interface (интерфейс отладки SQL Server), или SDI. Впервые он появился вместе с SQL Server 6.5 и был полностью интегрирован в Visual Studio и Query Analyzer. Проблемы установки и безопасности При отладке SQL Server должен быть запущен под учетной записью пользователя, а не под учетной записью Loca I System. Запуск SQL Server под Loca I System блокирует использование точек прерывания. Если вы попытаетесь отладить процедуру и не пройдете код шаг за шагом (без точек прерывания), то, скорее всего, запуск службы SQL Server у вас установлен под учетной записью Loca! System. В ситуации с несколькими экземплярами SQL Server отладчик не будет работать правильно, если процедура отладки не установлена по умолчанию. Ваша процедура будет выполняться без остановок в точках прерывания. Она будет вести Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA; Addison-Wesley, 1999. С 7.
Отладка 429 себя так, как будто SQL Server запущен под Loca I System. Так происходит потому, что сервер ошибочно использует мандат экземпляра по умолчанию для отладки на любом другом экземпляре. Вы можете создать экземпляр по умолчанию и установить на нем учетную запись (под ней будет осуществляться запуск сервера), которая будет соответствовать имеющейся на именованном экземпляре. Если вы занимаетесь отладкой на именованном экземпляре сервера на той же машине, где установлен неименованный экземпляр, отладка окажется невозможной, если она запрещена для учетной записи экземпляра по умолчанию. Это происходит потому, что SQL Server использует учетную запись экземпляра по умолчанию, независимо от того, на каком экземпляре вы производите отладку. Например, если ваш именованный экземпляр настроен для запуска сервиса SQL Server под учетной записью доменного пользователя, T-SQL не будет работать правильно, если экземпляр по умолчанию стартует под Loca I System. Если сведения об учетной записи различаются на экземпляре по умолчанию и на именованном экземпляре, отладчик будет использовать неверный мандат для входа и перестанет работать должным образом. Советы и предостережения ■ На операционных системах Windows NT и 2000 сообщения SDI записываются в системный журнал под учетной записью MSDEVSDI. ■ Можно отлаживать только одну хранимую процедуру. Если вы попытаетесь начать отладку второй процедуры в то время, когда отлаживается первая, вам будет предложено завершить текущую сессию отладки. ■ Не пытайтесь производить отладку хранимых процедур на промышленных серверах. Отладка может задействовать системные ресурсы, блокировать других пользователей и снизить параллелизм. ■ Отладка хранимых процедур в сессии Windows Terminal Server работает неверно. Лучше всего использовать другое средство удаленного управления, например такое, как PC Anywhere, NetMeeting или Laplink. ■ Возникновение в хранимой процедуре в процессе отладки ошибки с уровнем 16 и выше должно вызвать прекращение работы отладчика. На моем сервере при появлении такого сообщения все возможности отладки внезапно отключаются, и отладчик начинает вести себя очень странно. ■ Несмотря на то что окно Local Variables может отображать переменные типа sq!_var i ant, cursor, image, text и ntext, их значения изменить нельзя. ■ Нельзя изменять хранимые процедуры в процессе отладки. ■ Окно отладчика Watch отображает только первые 255 символов переменных char или varchar, независимо от их настоящей длины. ■ Нельзя поместить в окно отладчика Watch табличную переменную. ■ Нельзя с помощью отладчика Query Analyzer отлаживать расширенные хранимые процедуры. Они размещаются в DLL и обычно пишутся на С или C++. Об \ отладке расширенных хранимых процедур рассказывается в главе 20. ■ Во время отладки нельзя открыть новое соединение (в контексте одного экземпляра Query Analyzer). Если вам требуется еще одно соединение, запустите еще I один экземпляр Query Analyzer.
430 Глава 18. Отладка и профилирование Последовательность действий Чтобы отладить процедуру, выполните следующие шаги. 1. Откройте в Query Analyzer Инспектор объектов (Object Browser) и найдите хранимую процедуру, которую вы хотите отладить. 2. Щелкнув правой клавишей мыши на найденной процедуре, вызовите контекстное меню и выберите в нем пункт Debug. 3. Введите все требуемые параметры и щелкните ОК. 4. Запустится отладчик. Теперь вы сможете устанавливать точки прерываний, добавлять отслеживаемые переменные и т. д., используя пункты меню или клавиатуру. 5. Вы также можете запускать сессию отладчика из окна Object Search. Отладка без Сети Далее я привожу несколько советов на тот случай, если вам придется отлаживать хранимые процедуры без использования встроенного отладчика. Например, на удаленном компьютере, не имеющем достаточного количества памяти для установки Query Analyzer. ■ Можно добавить в ваш код команды PR I NT для отсылки отладочной информации в окно результатов. Команда PRINT может отображать любые скалярные переменные, а также она поддерживает соединение строк и другие элементарные операции. ■ Можно использовать xp_logevent для записи диагностической информации в журнал системных ошибок и в журнал ошибок приложений. ■ Можно использовать процедуру sp_t race_generate_event для добавления пользовательского события к трассировке Profiler. ■ Можно использовать недокументированную процедуру sp_user_counterN для установки из Transact-SQL значений пользовательских счетчиков Perfmon/ Sysmon. Это позволит вам, например, следить в журнале или на диаграмме Perfmon за изменениями значений переменной хранимой процедуры. ■ Существует несколько недокументированных флагов, предназначенных для отладки сложного Transact-SQL (см. главу 22, в которой перечислены некоторые из них). ■ Для отладки сложных хранимых процедур я часто использую способ, заключающийся в помещении каждого кода процедуры в отдельный пакетный файл. Затем, решая отдельные проблемы, я выполняю код, комментируя и удаляя комментарии к его частям. Отладка триггеров и пользовательских функций Отладка триггеров и UDF производится посредством отладки хранимой процедуры, использующей их. Возможно, вам потребуется создать процедуру именно для этой цели. Например, для отладки триггера вы должны создать хранимую процедуру, модифицирующую таблицу, которой принадлежит триггер, чтобы вызвать
Профилирование 431 срабатывание триггера. Когда вы будете отлаживать процедуру, вы сможете войти внутрь триггера (войдя внутрь инструкции DML). Этот же способ действует и при отладке пользовательских функций. Создайте хранимую процедуру, которая вызывает функцию, затем начните отлаживать хранимую процедуру. Профилирование Profiler, встроенный в SQL Server, является весьма полезным инструментом. Он мог бы быть и лучше, но, как бы то ни было, он обеспечивает вас всем необходимым и по своим возможностям значительно превосходит утилиту SQL Trace, которая входила в SQL Server до версии 7.0. Profiler представляет собой графический интерфейс, посредством которого можно создавать события, которые хотите отслеживать на сервере. Теоретически вы можете трассировать все, что происходит на сервере: от пакетных команд T-SQL и хранимых процедур до предупреждений о сортировке и событий записи в журнал ошибок. Запуск трассы Для запуска новой трассы с помощью Profiler выполните следующие действия. 1. Выберите пункт меню File ► New ► Trace в графической среде Profiler, чтобы вызвать диалог Trace Properties. 2. Выбирая наиболее подходящие для вашей конкретной ситуации элементы, задайте нужные вам атрибуты трассировки. 3. Нажмите кнопку Run. Запустится трасса. Трассировка против просмотра Я настойчиво подвожу вас к тому, чтобы запускать трассы с помощью хранимых процедур. Трассы, которые вы сгенерируете посредством хранимых процедур, можно будет просматривать и в Profiler. Таким образом, вы ничего не потеряете, анализируя файл трассы после того, как он будет собран. Вы получите средство для сбора трассировочной информации, обрабатывать которую на вашем SQL Server будет гораздо удобнее. Параметры командной строки SQL Profiler не только имеет графический интерфейс, но также поддерживает несколько параметров командной строки, которые можно использовать для управления его работой. В табл. 18.1 приведен их список. Таблица 18.1. Параметры командной строки SQL Profiler Параметр Назначение /S Определяет имя сервера /D Определяет имя базы данных /F Определяет имя трассировочного файла, который необходимо загрузить
432 Глава 18. Отладка и профилирование Общие советы и предостережения ■ По возможности, не запускайте Profiler на машине SQL Server. Запускайте его на отдельной машине, соединенной с сервером по сети. Я наблюдал, как Profiler занимал до 80 % времени CPU на довольно быстрых машинах. На долю SQL Server оставалось совсем немного. ■ Никогда не осуществляйте вывод трассы в таблицу. Трассировка в таблицу вынуждает Profiler открывать обратное ODBC-соединение для работы с таблицей. Трассировка в таблицу — верный способ занять ресурсы на сервере, и вообще считается плохим решением проблемы. ■ Не трассируйте события, которые вам не нужны, особенно события уровня команд. Трассировка слишком большого количества событий может оказать существенное влияние на производительность вашего сервера и приведет к распуханию трассировочного файла. ■ Старайтесь включать в трассу все поля. Это не приведет к большим расходам, а некоторые события могут возвращать полезную информацию или дополнительные подробности только при наличии определенных полей. Так, например, событие Prof i I er showp I an не будет отображаться корректно, пока не будет добавлено поле В i naryData. Если вы все же не хотите включать в трассу все поля, мне кажется, стандартный набор полей должен состоять, по крайней мере, из следующих: Q В i naryData (Бинарные данные); □ CI ientProcessID(Идентификатор клиентского процесса); □ CPU (Загрузка процессора); □ Duration (Продолжительность); Q EndTime (Время окончания); Q EventClass (Класс события); Q EventSubClass (Подкласс события); Q HostName (Имя компьютера, на котором запущено клиентское приложение); Q IntegerData (Целочисленные данные); Q LoginName (Имя пользователя для входа на SQL Server); Q NTUserName (Имя пользователя Windows NT4.0 или Windows 2000); Q Reads (Количество чтений с диска); Q SPID (Идентификатор процесса, присвоенный сервером клиентскому приложению); Q StartTime (Время начала события); □ TextData (Текстовые данные); Q Writes (Количество записей на диск). Имейте в виду, что классы событий разные дополнительные поля используют разными способами. Зачастую поля вроде EventSubClass и IntegerData содержат информацию, определяемую конкретным типом события. ■ Запомните, что просматривая трассировочный файл, вы можете устанавливать в нем закладки (Ctrl+F2) и перемещаться между ними (F2).
Профилирование 433 ■ При возможности, используйте расширенные хранимые процедуры sp_t race_XXXX для запуска и остановки трасс. Пример по использованию sp_trace_XXXX можно найти в главе 21. Использование процедур sp_trace_XXXX может значительно уменьшить снижение производительности вашей системы вследствие запуска трасс Profiler. ■ Можно использовать команду меню File ► Script Trace для генерации пакетного файла T-SQL, который может запускать определенную трассу, используя вызовы расширенной хранимой процедуры. По крайней мере, это хороший способ научиться создавать процедуры такого рода, даже если вы будете использовать полученный сценарий. Листинг 18.1 показывает пример одного такого сценария. Листинг 18.1. Трассировочный сценарий T-SQL, сгенерированный Profiler /* Created by: SQL Profiler */ /* Date: 07/10/2001 00:14:40AM */ -- Пожалуйста, замените текст InsertFileNameHere нужным именем файла -- с указанием полного пути, например c:\MyFolder\MyTrace. Расширение.trc -- будет добавлено к имени файла автоматически. Если вы пишете с удаленного -- сервера на локальный диск, пожалуйста, используйте UNC пути и убедитесь. -- что сервер имеет доступ на запись в ваш сетевой ресурс exec @rc = sp_trace_create @TraceID output. 0. N' InsertFileNameHere'. @maxfilesize. NULL if (@rc != 0) goto error -- Файлы и таблицы, расположенные на клиенте, не могут быть помещены в сценарий -- Создание очереди declare @rc int declare PTracelD int declare @maxfilesize bigint set ©naxfilesize = 5 -- Установка события declare @on bit set @on = 1 exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent PTracelD. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID, exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. exec sp_trace_setevent @TraceID. 10. 10. 10. 10. 10. 10. 10. 10. 10. 10, 10. 12. 12, 12. 12. 12. 12. 1. 6. 9. 10 11 12 13, 14, 16. 17, 18, 1. 6. 9. 10. 11. 12. @on @on @on , @on , @on , @on @on , @on @on @on @on @on @on @on @on @on @on продолжение J>
434 Глава 18. Отладка и профилирование Листинг 18 exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec „exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec exec sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp__trace^ sp_trace^ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ sp_trace_ .1 {продолжение) setevent (pTracelD _setevent @TraceID _setevent (pTracelD _setevent (pTracelD setevent (pTracelD _setevent (pTracelD setevent @TraceID _setevent @TraceID _setevent @TraceID _setevent (pTracelD _setevent (pTracelD _setevent (pTracelD setevent (pTracelD _setevent (pTracelD setevent (PTracelD setevent (pTracelD _setevent (PTracelD setevent (PTracelD _setevent (PTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (PTracelD setevent (pTracelD setevent (PTracelD setevent (PTracelD setevent (pTracelD setevent (pTracelD setevent (PTracelD setevent (PTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD setevent (pTracelD 12. 12. 12. 12. 12. 14. 14. 14. 14. 14. 14, 14, 14. 14. 14. 14. 15. 15. 15. 15. 15. 15. 15. 15. 15. 15. 15. 17. 17. 17. 17. 17. 17. 17, 17, 17, 17. 17. 13. (pon 14. @on 16, @on 17, @on 18, (?on I. @on 6. @on 9. @on 10, (pon II. @on 12. @on 13. @on 14. (pon 16. @on 17. (pon 18. (pon I. (pon 6. @on 9. (pon 10, (pon II. @on 12. @on 13. @on 14. (pon 16. (pon 17. @on 18. @on I. @on 6. @on 9, @on- 10, @on II. @on 12. @on 13. @on 14. @on 16. @on 17. @on 18. @on -- Установка фильтров declare (pintfilter int declare (pbigintfilter bigint exec sp_trace_setfilter (PTracelD. 10. 0. 7. N'SQL Profiler' -- Установка статуса трассы для старта exec sp_trace_setstatus (PTracelD, 1 -- Вывод id трассы для последующих ссылок select TraceID=@TraceID goto finish error: select ErrorCode=(prc finish: go
Профилирование 435 Обратите внимание на @0п — переменную типа bit. Многие расширенные процедуры, включая те, которые управляют трассами Profiler, имеют строго типизированные параметры. Поскольку не существует способа сообщить sp_t race^setevent, что передаваемую 1 необходимо рассматривать как b i t, в сценарии мы определили переменную типа b i t и приравняли ее к 1 для передачи в расширенную хранимую процедуру. Обратите внимание на то, что процедура sp_start_trace в главе 21 делает то же самое. ■ Запомните: можно использовать фильтры для уменьшения количества и типа трассируемых событий. Повторю, что чрезмерная трассировка существенно снизит производительность вашего сервера. ■ Если, используя трассы Profiler, вы обнаружите, что раз от раза используете один и тот же набор событий, полей и фильтров, — создайте файл описания трасс, собрав их всех воедино так, чтобы вы могли связно и просто описать ваши трассы. В состав Profiler входит целый ряд готовых описаний трассировочных файлов. ■ Можно загрузить трассировочные файлы в мастер Index Tuning wizard для получения автоматической помощи по проектированию и настройке индексов. ■ События трассировки Profiler могут быть использованы для аудита действий на сервере, которые связанны с безопасностью. Как указано в главе 22, с помощью DBCC AUDITEVENTO можно создать события аудита из ваших собственных приложений. Они будут отображены в трассе Profiler как события типа Secu r i ty Audit. ■ Убедитесь, что в вашей папке TEMP достаточно места. Profiler использует ее для различных целей, и файлы, которые он там создает, могут иметь большой размер. ■ Операция поиска в Profiler чувствительна к регистру. Если то, что вы ищете, должно присутствовать в Profiler, но вы не можете его найти, проверьте регистр символов в строке поиска. ■ Не забывайте, что можно сохранить трассы Profiler в виде Transact-SQL. Это позволит повторно запустить их в любой среде, поддерживающей T-SQL, включая Query Analyzer и OSQL. Повторный запуск трасс Одним из значительных преимуществ хранения в одном файле событий, которые собрал Profiler, является возможность повторного запуска. Собрав однажды файл трассы, вы загружаете его в Profiler и для повторного запуска нажимаете F5. Некоторые события Profiler не может запустить повторно (одно из таких событий — Attent i on), но в большинстве случаев все работает довольно хорошо. Загрузка файла трассы в таблицу При работе с большими трассировочными файлами очень удобно загрузить их в таблицу и использовать Transact-SQL для выполнения запросов, агрегирования, поворота таблицы и т. д. Системная функция :: f n_t race_gettab I e() позволит вам легко справиться с этой задачей. Это табличная функция, которую можно использовать внутри команды SELECT для чтения трассы Profiler (листинг 18.2).
436 Глава 18. Отладка и профилирование Листинг 18.2. Системная функция ::fn_trace_gettable() выводит содержимое файла трассы SELECT * FROM ::fn_trace_gettable('c:\temp\test.trc'.default) (Результаты сокращены) TextData network protocol: Named Pipes set quotedjdentifier off set implicit_transactions off set cursor_close_onj:ommit off set ansi_warnings on set ansi_padding on set ansijiulls on set concat_null_yields_null on set language us_english set dateformat mdy set datefirst 7 Разумеется, вы сможете вставить результаты SELECT FROM : : f n_t race_gettab I e() * в отдельную таблицу для постоянного хранения или для дальнейшего анализа. Имейте в виду, что файл трассы, который вы используете при работе с функцией :: fn_trace_gettab! е(), должен быть доступен с сервера, и не забудьте, что указанный вами путь относится к серверу, а не к локальной машине. Представление файла трассы в виде XML Если вы предпочитаете работать с файлом трассы не как с таблицей SQL Server, а как с XML-документом, то вам будет несложно преобразовать файл трасс в XML- документ. Самым замечательным преимуществом того, что Transact-SQL способен преобразовывать результаты работы команды SELECT в XML, может послужить преобразование файла трассы Profiler в XML-документ за один шаг (листинг 18.3). Листинг 18.3. Файл трассы можно легко преобразовать в XML-документ SELECT TextData. DatabaselD. TransactionlD. NTUserName. NTDomainName. HostName. ClientProcessID. ApplicationName, LoginName. SPID. Duration, StartTime. EndTime. Reads. Writes. CPU. Permissions. Severity, EventSubClass. ObjectlD. Success.
Нагрузочное тестирование 437 IndexID. IntegerData. ServerName. EventClass, ObjectType, NestLevel. State. Error. Mode. Handle. ObjectName. DatabaseName. FIleName, OwnerName, RoleName, TargetUserName. DBUserName, TargetLoginName. ColumnPermissions FROM ::fn_trace_gettable('c:\_temp\test.trc'.DEFAULT) FOR XML AUTO Обратите внимание на предложение FOR XML AUTO в конце команды SELECT. Оно отвечает за представление результатов работы SELECT в виде XML. После преобразования данных в XML вы можете использовать таблицы стилей XML для представления данных практически в любом другом формате. Группировка данных Profiler При отображении трассы Profiler может группировать данные по одному полю или по нескольким полям. Это упрощает навигацию и анализ трассировочной информации. Чтобы произвести группировку по нескольким полям, сделайте следующее. 1. Вызовите диалог Trace Properties (Свойства трассы). 2. Выберите вкладку Data Columns (Поля данных). 3. Примените кнопки Up и Down для помещения полей в раздел Groups в правой части окна диалога. ODBC-трассировка Если вы подсоединяетесь к SQL Server через ODBC и испытываете трудности с получением необходимой информации из Profiler, вы можете установить отдельную трассу ODBC на уровне драйвера. Вы можете сделать это на вкладке Tracing утилиты ODBC Administrator. Это, по крайней мере, позволит вам узнать, что отсылается на сервер и что возвращается в ответ. После окончания трассировки не забудьте выключить трассу ODBC. Нагрузочное тестирование Существует целый ряд мощных инструментов независимых производителей, предназначенных для нагрузочного тестирования SQL Server. Многие из них весьма
438 Глава 18. Отладка и профилирование дороги. Средство тестирования, которое я собираюсь вам продемонстрировать, нельзя причислить к «продвинутым»: оно не обладает развитым набором возможностей, но у него есть одно большое преимущество: оно абсолютно бесплатно. Я имею в виду средство для нагрузочного тестирования SQL Server — STRESS. CMD. Это «кустарное» средство автоматического тестирования: на вход подается сценарий T-SQL, который запускается повторно на SQL Server заданное количество раз. У STRESS. CMD есть пять основных возможностей. 1. Он может повторно запускать определенный сценарий T-SQL на SQL Server. 2. Многократно запускать один и тот же сценарий или однократно запускать ряд сценариев, либо делать то и другое в некоторой заданной комбинации. 3. Направлять вывод на консоль либо записывать результаты работы каждого сценария отдельно. 4. Показывать, сворачивать или прятать окно каждого сценария. 5. Запускать несколько сценариев одновременно или запускать их один за другим. СОВЕТ . Компания Microsoft опубликовала средство SQL70IOStress для тестирования производительности дисковой подсистемы SQL Server. Оно не относится к обсуждаемым выше средствам нагрузочного тестирования. То, о чем я говорил, представляет собой средство эмуляции работы нескольких пользователей, одновременно присоединенных к SQL Server и выполняющих на нем запросы Transact-SQL, которые так или иначе загружают его. В листинге 18.4 представлен полный код STRESS. CMD. Это процедура, которую я написал много лет назад (для OS/2) и развивал ее в течение нескольких лет. Для использования этого кода вам необходимо соблюсти одно условие: в системе должно быть разрешено применение командных файлов Windows 2000 (включено по умолчанию в Windows 2000). Листинг 18.4. STRESS.CMD — «древнее» средство нагрузочного тестирования SQL Server @echo off REM Проверка недостаточного или излишнего количества параметов IF («)==() GOTO ERROR IF NOT (%9)==() GOTO ERROR REM Установка некоторых значений по умолчанию SET ggmask=W SET ggtimes=l SET ggwait=N0 SET ggserv=(local) SET gguser=-E SET ggwaitparnWNORMAL SET ggwin=N0RMAL SET ggout=YES REM Запись параметров в переменные IF NOT (*2)==() SET ggtimes=&2 IF NOT «3)==0 SET ggwait=M
Нагрузочное тестирование 439 IF NOT «4)==0 SET ggwirHM IF NOT №)==() SET ggout=£5 IF NOT «6)==0 SET ggserv=£6 IF NOT «7)==0 SET gguser=&7 REM Если параметр не введен, оставляем значение пустым SET ggpwd=&8 REM Оставим этот парамет пустым SET ggoutparm=&9 REM Дальнейшая обработка некоторых параметров IF NOT «ggpwd£)==() SET ggpwd=-P&ggpwd& IF /i &ggwai«==YES SET ggwaitparnWWAIT IF NOT &gguserf==-E SET gguser=-U&ggusert IF /i uggoutt==YES SET ggoutparm=-ott-nf.OUT REM Запускаем сценарии во вложенном цикле (циклах) REM REM Внутренний цикл перебирает файлы, соответствующие маске, и запускает их REM Внешний цикл запускает внутренний определенное количество раз FOR /L Hi in (l.l.lggtimes*) DO FOR »f IN «ggmask» DO START "ИГ SlggwaitparnU Aggwiru OSQL.EXE -S&ggserv& Uggusert XggpviuX -iHf Uggoutparrrt GOTO NOERROR .-ERROR echo Укажите сценарий, который должен быть исполнен SET ERRORLEVEL=l ECHO STRESS.CMD ECHO . ECHO Одновременно запускает определенный(ые) сценарий(ии) Transact-SQL указанное количество раз ECHO . ECHO Copyright (с) 1992. Ken Henderson. Все права защищены. ECHO Основано на коде к статье "User-to-User". PC Magazine. March 26. 1991. ECHD . ECHO Использование: STRESS script [N] [wait] [windowstyle] [saveoutput] [server] [user] [password] ECHO . ECHO где: ECHO script = имя запускаемого сценария или маска файлов ECHO N = число раз. которое должен быть повторен запуск (по умолчанию 1) ECHO wait = YES. если необходимо ждать окончания выполнения сценария для запуска следующего (по умолчанию N0) ECHO windowstyle = тип содаваемго окна для каждого запуска сценария: ECHO . ECHO /MIN = свернутое /MAX = развернутое /В = без вывода в окно /NDRMAL = обычное окно ECHO . ECHO saveoutput = YES для сохранения результатов в файл с использованием параметра о 0SQL (по умолчанию YES) ECHO server = сервер, на котором будет запускаться сценарий (по умолчанию (local) ) ECHO user -= имя пользователя (по умолчанию - use trusted connection) ECHO password = пароль (по умолчанию - use trusted connection) : NOERROR
440 Глава 18. Отладка и профилирование STRESS. CMD может принимать до восьми параметров. Назначение каждого параметра зависит от его расположения в списке, то есть чтобы указать пятый параметр, до этого вы должны указать первые четыре. Назначения параметров перечислены в табл. 18.2. Таблица 18.2. Параметры командной строки STRESS.CMD Параметр Назначение SCRIPT Имя запускаемого сценария или маска файлов N Количество раз, которое должен быть повторен запуск. По умолчанию 1 WAIT YES (если необходимо ждать окончания выполнения сценария для запуска следующего). По умолчанию N0 WINDOWSTYLE Тип создаваемого окна для каждого запуска сценария: MIN — свернутое; МАХ — развернутое; В — без вывода в окно; NORMAL — обычное. По умолчанию обычное окно SAVEOUTPUT YES (для сохранения результатов в файл с использованием параметра -о OSQL). По умолчанию YES SERVER Сервер, на котором будет запускаться сценарий. По умолчанию local " USER Имя пользователя. По умолчанию use trusted connection PASSWORD Пароль. По умолчанию use trusted connection Вызов STRESS. CMD может выглядеть следующим образом: stress stress.sql 10 no normal no dragonzlair В этом примере десять раз будет выполнен сценарий stress, sq I. Мы не будем ждать окончания каждого запуска, также нас не интересует сохранение результатов в файл. Имя сервера, к которому мы подсоединяемся, — d ragonz I a i r, и для подключения к нему мы используем доверительное соединение. Другой вызов может выглядеть так: stress stress*.sql 100 no min yes dragonzlair monty python Здесь мы собираемся выполнить все сценарии из текущей директории, которые соответствуют маске stress*. sq I. Мы выполним асинхронно каждый сценарий 100 раз. Окна всех сценариев будут свернутыми. Мы сохраним результаты каждого запуска в файл (используя параметр -о утилиты OSQL). Наконец, мы подсоединимся к SQL Server d ragonz lair, используя имя пользователя monty и пароль python. Как видите, выполняя на сервере STRESS.CMD, можно придумать довольно экзотические комбинации параметров. Все, что можно выполнить в сценарии Т- SQL, можно выполнить и с помощью STRESS. CMD. Вы можете запустить несколько сценариев или только один — и вы можете запустить их столько раз, сколько захотите. Сценарии можно запускать одновременно и выводить результаты их работы либо в файл, либо на экран. Вы можете вызвать на сервере событие Attent i on, нажав для прекращения выполнения сценария CTRL+C, или остановить весь процесс, нажимая CTRL+C, пока не получите запрос на подтверждение остановки всего командного файла. STRESS.CMD снимает большинство рутинных операций по нагрузочному тестированию SQL Server, причем вся его функциональность заключена в очень небольшом . CMD-файле, который можно модифицировать для совместного использования с другими командами операционной системы.
Итоги 441 Несколько лет назад я написал подобную утилиту на С. Для выполнения большинства действий, содержащихся в командном файле, она использовала DB-Library API. Эта утилита была очень удобна, но проблема заключалась в том, что процесс ее совершенствования оказался бесконечен. Меня постоянно просили добавить еще одну возможность. И я работал до пяти утра, реализовывая эту еще одну возможность, чтобы удовлетворить моих пользователей. В конце концов, я решил избавиться от моей злополучной утилиты и опубликовал код, который можно было бы при желании изменять самостоятельно. Так родился STRESS. CMD. Теперь когда кому- нибудь нужна еще одна возможность, я говорю ему: «Эй, у тебя есть исходный код — дерзай!» Мда, если ли бы я еще мог найти способ убедить людей работать над всем моим кодом бесплатно! Итоги В этой главе вы узнали: ■ как отлаживать, профилировать и тестировать код хранимых процедур на Trans- act-SQL; ■ что SQL Server предоставляет ряд удобных средств для отладки, профилирования и нагрузочного тестирования кода конечного пользователя; ■ как с помощью утилиты STRESS.CMD можно самостоятельно написать свой инструмент, если вас не устраивает тот, что входит в стандартную поставку. Ведь сделать это совсем нетрудно.
Автоматизация Обустраиваем ли мы гостиную, пишем ли книгу в два часа утра или разрабатываем компоненты — большая часть времени и энергии тратится на тысячи мелочей. Дэйл Роджерсон' В этой главе мы поговорим об автоматизации или, другими словами, об управлении СОМ-объектами при помощи языка Transact-SQL. Но вначале я предлагаю вам небольшой обзор самой технологии СОМ и объяснение, почему у вас может возникнуть необходимость управлять СОМ-объектом из Transact-SQL. Краткий обзор СОМ Если вам пришлось написать множество приложений для Windows, вы, вероятно, знакомы с технологиями OLE и ActiveX. Термин OLE первоначально означал вставку и связывание объектов. Это было первое поколение взаимодействия приложений в среде Windows. Идея заключалась в создании документно-ориентированной модели, где объект из одного приложения мог взаимодействовать с объектами из других приложений. Для взаимодействия между объектами OLE 1.0 использовала DDE (динамический обмен данными). DDE представляет собой основанный на сообщениях механизм межпроцессного взаимодействия, построенный на архитектуре сообщений Windows. У DDE масса недостатков: он медленный, негибкий, сложный в программировании, — поэтому вторая версия OLE уже не зависела от DDE. Вторая версия OLE была переписана таким образом, что она полностью зависела от СОМ. И хотя СОМ намного эффективнее и быстрее, чем DDE, OLE все равно остался сложным. Почему? Потому что технология СОМ реализовывалась впервые. С той поры много воды утекло и мы многому научились. Теперь у OLE очень мощная и богатая функциональная нагрузка. И хотя OLE может быть большой, медленной и сложной в программировании, — это не вина СОМ, а, скорее, проблемы реализации OLE. Технология ActiveX также построена на основе СОМ. Главная область применения технологии ActiveX — предназначенные для работы в Интернете компоненты. ActiveX представляет собой набор технологий, основное назначение которых Rogerson, Dale. Inside COM. Redmond, WA: Microsoft Press, 2000. С 128. 19
Краткий обзор СОМ 443 заключается в обеспечении интерактивности (именно поэтому active) содержимого веб-страниц. Ранее известные как элементы управления OLE или OCX, элементы управления ActiveX представляют собой компоненты, которые можно разместить на веб-странице или в приложении Windows, чтобы использовать законченную функциональность, предоставленную сторонними производителями. Основой, на которой построены элементы управления OLE и ActiveX, является СОМ. При помощи СОМ объект может предоставить свою функциональность для использования другими компонентами и приложениями. В дополнение к определению жизненного цикла объекта и тому, как объект предоставляет свою функциональность окружающему миру, технология СОМ также определяет то, как работает механизм предоставления функциональности объекта через границы процессов и при взаимодействии по Сети. Технология СОМ является ответом компании Microsoft на вопросы: «Как предоставить классы кода другим приложениям, используя способ, независимый от языка программирования? Как предоставить пользователям объектно-ориентированный способ работы с динамической библиотекой? Как можно использовать программные компоненты без исходного кода и заголовочных файлов?» До появления СОМ Не так давно в программной индустрии считалось вполне нормальным распространять полный исходный код и/или заголовочные файлы с библиотеками сторонних производителей. Для того чтобы воспользоваться этими библиотеками, их просто компилировали вместе с приложениями (или включали их заголовочные файлы). В итоге получался один исполнимый файл, который мог содержать код от нескольких производителей. Так как многие разработчики чаще всего использовали одну и ту же библиотеку, то среди распространяемых продуктов могла существовать версия одной и той же библиотеки. Исполнимые файлы были относительно большого размера, совместно код не использовали или использовали крайне редко. Обновление одной библиотеки сторонних производителей требовало перекомпиляции и/или перекомпоновки, так как во время компиляции библиотека встраивалась непосредственно в исполнимый файл. Все это изменилось с появлением библиотек динамической компоновки (DLL) Windows. Сторонние производители стали распространять только заголовочные и двоичные файлы. Иногда вместо одного исполнимого файла разработчик распространял вместе со своим приложением большой набор библиотек динамической компоновки. Когда приложение выполняло задачу, следовало (явно или неявно) загрузить библиотеку стороннего производителя. По мере того как приложения усложнялись, стали появляться исполнимые файлы, нуждавшиеся в десятках библиотек динамической компоновки, которые зависели друг от друга. ПРИМЕЧАНИЕ По сути именно так работает сама операционная система Windows. Она представляет собой исполнимый файл с большим набором библиотек динамической компоновки, а приложения Windows вызывают функции из этих библиотек. Этот метод был эффективен, но имел несколько недостатков. Одним из главных недостатков был тот факт, что интерфейсы к библиотекам динамической ком-
444 Глава 19. Автоматизация поновки не были объектно-ориентированными и, следовательно, были сложны для расширения и могли прекратить работу при малейшем изменении экспортируемой функции. Если производитель добавлял параметр к функции в своей библиотеке, это могло привести к сбою всего кода, использующего эту библиотеку. Способ решения этой проблемы, выбранный большинством производителей, заключался в создании новой версии функции (часто ее имя содержало суффикс Ех или подобный), включающей новый параметр. В результате появились интерфейсы уровня вызова (CLI), которые очень быстро стали неуправляемыми. Библиотеки сторонних производителей и даже сама операционная система Windows часто включали в себя несколько версий вызова одной и той же функции для того, чтобы сохранить совместимость со всеми существовавшими когда-либо версиями библиотеки. Вскоре ситуация вышла из-под контроля и осложнилась тем, что не было простого способа узнать, какую именно версию функции следует использовать. Программирование с использованием этих интерфейсов стало методом проб и ошибок, хотя, чтобы программировать, следовало прочитать немалое количество руководств и в результате строить свою работу на догадках. Другая проблема использования интерфейсов уровня вызова (CLI) заключалась в том, что в компьютере пользователя распространялось множество копий одной и той же библиотеки динамической компоновки. Дисковое пространство стоило намного дороже, чем в наши дни, поэтому производители искали способ, как избежать наличия копий библиотеки в системе пользователя. К сожалению, найденное решение проблемы не было до конца продуманным. Оно заключалось в размещении всех библиотек динамической компоновки, которые требовались приложению, в системном каталоге Windows. Это разрешило проблему копий одной библиотеки, но породило массу других. Главной из них была проблема, связанная с конфликтом версий одной и той же библиотеки динамической компоновки. Если производители А и В зависели от разных версий библиотеки производителя С, то была высока вероятность того, что один из продуктов производителей А и В не будет функционировать, поскольку другой продукт использует иную версию библиотеки. Если при переходе от версии к версии интерфейс библиотеки менялся (даже незначительно), то весьма вероятно было то, что хотя бы одно приложение будет вести себя некорректно (если вообще будет работать) при получении не той версии библиотеки, которая ожидалась. Другая проблема совместного хранения библиотек была вызвана централизованной, но неуправляемой конфигурационной информацией. До появления реестра Windows использовали отдельный конфигурационный файл (обычно с расширением .INI) для каждого приложения (и даже несколько конфигурационных файлов для некоторых приложений). Конфигурационные файлы могли включать в себя пути к библиотекам динамической компоновки, используемым приложением. Таким образом, задача по разрешению проблемы конфликта версий усложнялась еще больше. Так как Windows не управляла конфигурационными файлами, не было способа оградить приложение от уничтожения необходимого конфигурационного файла и внесения в него записей, которые могли бы привести к сбоям в работе других приложений или полному игнорированию конфигурационного файла. Конфигурационные файлы были простыми текстовыми файлами, которые приложение могло использовать или не использовать по своему усмотрению.
Краткий обзор СОМ 445 Последовательность, используемая операционной системой Windows для поиска библиотек, была логичной и хорошо документированной. Однако тот факт, что приложение могло использовать функцию Windows Load Lib rary для загрузки библиотеки из любой области жесткого диска, означало, что нельзя было точно установить, от каких именно библиотек зависело приложение. Приложение могло использовать путь для загрузки библиотек из того конфигурационного файла, о котором никто не знал, или просто найти на жестком диске и загрузить ту библиотеку, которая, по мнению самого приложения, имела самую подходящую версию. Часто связи между приложениями были очень сложными, что делало приложения еще более «хрупкими». Мы прошли длинный путь: от огромных исполнимых файлов с небольшим или отсутствующим разделением кода до ситуации, когда все взаимосвязано и установка одного приложения часто нарушает работу другого. Зарождение СОМ Технология СОМ стала решением Microsoft этих проблем. Проще говоря, СОМ предоставляет интерфейс к стороннему коду, который: ■ объектно-ориентирован; ■ централизован; ■ поддерживает версии; ■ не зависит от языка программирования. Времена неуправляемой и неправильно используемой конфигурационной информации прошли благодаря тому, что СОМ использует системный реестр. Когда приложение создает экземпляр СОМ-объекта (обычно при помощи вызова С reateOb j ect ()), Windows ищет в системном реестре информацию о расположении объекта на диске и загружает его. Никаких догадок и множества копий одного объекта — каждый СОМ-объект находится строго в одном месторасположении системы. ПРИМЕЧАНИЕ Недавно компания Microsoft представила концепцию перенаправления СОМ и параллельного развертывания. Она позволяет сосуществовать нескольким версиям одного и того же СОМ-объекта в рамках одной системы. Эта функциональность применима в ограниченном числе случаев. Например, нельзя использовать перенаправление СОМ для загрузки различных копий объекта в различные веб-приложения на сервере IIS. Хотя веб-страницы и кажутся конечным пользователям различными приложениями, на самом деле это одно приложение — в данном случае IIS. COM ограничивает приложение использованием одной копии заданного объекта. Подавляющее большинство СОМ- приложений придерживаются стандартных ограничений по версиям СОМ. Однако это не значит, что нельзя иметь несколько версий объекта в системе. В СОМ это возможно благодаря использованию нескольких интерфейсов. Каждая новая версия объекта имеет свой интерфейс и может, с точки зрения пользователя, представлять собой совершенно отдельный объект. Разделение кода между версиями объекта возможно так же, как и его отсутствие. Будучи разработчиком приложений, вы не думаете об этом — вы просто программируете, используя интерфейс. Еще одна важная деталь: интерфейс подобен классу без тела или реализации. Это программная конструкция, определяющая функциональный контракт: контракт между поставщиком функциональности и ее потребителями. Реализуя интерфейс, автор объекта гарантирует, что пользователи объекта могут зависеть от
446 Глава 19. Автоматизация ограниченной функциональности, заложенной в объект. Несмотря на то что делает объект на самом деле, при помощи интерфейса пользователь может программировать, не беспокоясь о деталях. Если автору объекта потребуется улучшить код объекта таким образом, что клиентские приложения, зависящие от объекта, смогут прекратить работу, то автору объекта достаточно будет определить новый интерфейс, оставив старый без изменений. Технология СОМ имеет ограничения (большинство из которых разрешено в .NET), но тем не менее она повсеместно распространена и хорошо стандартизирована. Мир принял технологию СОМ, поэтому SQL Server содержит механизм для работы с СОМ-объектами из Transact-SQL. Базовая архитектура Основные элементы СОМ: ■ интерфейсы; ■ подсчет ссылок; ■ Querylnterface; • ■ IUnknown; ■ агрегации; ■ маршаллинг. Поговорим о каждом элементе отдельно. Интерфейсы С точки зрения ООП (и СОМ), интерфейс — это механизм для предоставления функциональности, как и было упомянуто выше. Обычно объект использует интерфейс для того, чтобы предоставить свою функциональность для использования во внешнем мире. Когда объект использует интерфейс, говорят, что объект реализует интерфейс. Пользователи объекта могут взаимодействовать с интерфейсом, не зная, что на самом деле представляет собой объект. Объект может реализо- вывать несколько интерфейсов. Для того чтобы реализовать интерфейс, методы, предоставляемые интерфейсом, связываются с методами объекта. Сам по себе интерфейс не требует памяти и только определяет функциональность, которой должен обладать объект, реализующий данный интерфейс. Каждый СОМ-интерфейс основан на IUnknown, базовом интерфейсе СОМ. IUnknown обеспечивает доступ к другим интерфейсам объекта. Каждый интерфейс обладает уникальным идентификатором интерфейса (IID), что облегчает поддержку версий интерфейсов. Новая версия СОМ-интерфейса — отдельный интерфейс со своим собственным IID. Для стандартных интерфейсов ActiveX, OLE и СОМ идентификаторы IID предопределены. Подсчет ссылок В отличие от .NET и среды выполнения Java, COM не выполняет автоматической сборки «мусора». Убирать объекты, которые стали ненужными, — задача разработчика. Для того чтобы определить, можно ли уничтожить объект, используется счетчик ссылок объекта.
Краткий обзор СОМ 447 Методы AddRef и Release интерфейса lUnknown управляют счетчиком ссылок интерфейсов СОМ-объекта. Когда клиент получает ссылку на интерфейс-потомок lUnknown, для интерфейса следует вызвать AddRef. Когда клиент закончил работу с интерфейсом, он должен вызвать метод Release. В самом простом случае каждый вызов метода AddRef увеличивает переменную- счетчик, принадлежащую объекту, а каждый вызов метода Release уменьшает ее. Когда значение переменной-счетчика равно нулю, интерфейс не используется клиентами и может быть уничтожен. Можно также создать механизм подсчета ссылок, который будет учитывать каждую ссылку на объект (вместо интерфейса, реализуемого объектом). В этом случае вызовы методов AddRef и Release поручаются централизованному механизму подсчета ссылок. Метод Release освобождает объект, счетчик ссылок становится равным нулю. Query Interface/lUnknown Основополагающим механизмом СОМ для доступа к функциональности объекта является метод Querylnterf асе интерфейса lUnknown. Так как все СОМ-интерфейсы являются производным от lUnknown, то они имеют реализацию метода Querylnterf асе. Метод Querylnterface опрашивает объект, используя IID интерфейса, на который требуется получить указатель. Если объект может реализовывать запрошенный интерфейс, метод Querylnterface возвращает указатель и вызывает метод AddRef. В случае если объект не реализует интерфейс, метод Querylnterface возвращает код ошибки E_NOINTERFACE. Агрегации Если разработчику объекта требуется использовать службы, предоставляемые другими объектами (например, объектами сторонних производителей), СОМ поддерживает механизмы встраивания и агрегации. Под агрегацией понимается создание во внешнем объекте экземпляра внутреннего объекта и предоставление интерфейсов внутреннего объекта как части интерфейсов внешнего объекта. Некоторые объекты могут быть агрегированы, некоторые — нет. Чтобы поддерживать агрегацию, объект должен отвечать определенным требованиям. Маршаллинг Благодаря механизму маршаллинга СОМ-интерфейсы объекта из одного процесса могут быть использованы другим процессом. При помощи маршаллинга СОМ предоставляет или использует предоставленный разработчиком интерфейса код для упаковки и распаковки параметров метода в формат, который можно передавать через границы процессов или по Сети при вызове метода. Когда вызов завершается, действия совершаются в обратной последовательности. В рамках одного процесса маршаллинг обычно не требуется, но он может потребоваться при работе с потоками. СОМ в работе В сущности, СОМ-объекты используются двумя основными способами: при помощи раннего и позднего связывания. Когда приложение ссылается на объект та-
448 Глава 19. Автоматизация ким образом, что ссылки разрешимы на этапе компиляции, то используется раннее связывание. Чтобы осуществить раннее связывание в среде Visual Basic, необходимо во время разработки добавить ссылку на содержащую объект библиотеку и далее использовать выражение Dim для создания отдельных экземпляров объекта. Для использования раннего связывания в средах Visual C++ и Delphi следует импортировать библиотеку типов объекта и работать с интерфейсами, предоставленными этой библиотекой. И в том и в другом случае происходит программирование под интерфейсы объекта, как если бы это были созданные нами интерфейсы. Сам же объект может находиться на другом компьютере и вызываться посредством распределенной COM (DCOM), или может быть использован маршаллинг менеджером транзакций, таким как Microsoft Transaction Server или Component Services. Откровенно говоря, этот процесс прозрачен: разработчик программирует, используя интерфейс. Если ссылки на объект неизвестны до момента выполнения, объект использует позднее связывание. Обычно при позднем связывании экземпляр объекта создается при помощи вызова CreateObject(), и экземпляр объекта сохраняется в переменной типа variant. Так как на момент компиляции компилятору неизвестно, на .какой объект делаются ссылки, то не исключены некорректные вызовы методов и обращения к несуществующим свойствам объекта. Это оборотная сторона позднего связывания: с одной стороны, оно более гибко в том смысле, что во время выполнения можно решать, какие именно объекты необходимо создавать, и даже создавать объекты, не существующие в системе, в которой велась разработка, — а с другой стороны, при использовании позднего связывания очень легко допустить ошибки, так как среда разработки не может предоставить тот же уровень помощи, который предоставляется тогда, когда используемые объекты известны среде разработки. Когда экземпляр объекта создан, можно вызывать его методы и работать с его свойствами. Технология СОМ поддерживает концепцию событий (хотя они несколько сложнее в использовании, чем следовало бы), так что можно подписываться на уведомления о наступлении событий и реагировать на события СОМ- объектов. SQL Server и автоматизация СОМ SQL Server предоставляет набор хранимых процедур для работы с СОМ-объекта- ми из Transact-SQL. Автоматизация — это независимый от языка программирования способ управления и использования СОМ-объектов. Большое число приложений предоставляют функции посредством СОМ-объектов. Многие продукты Microsoft, так же как и продукты других производителей, предоставляют некоторую функциональность через СОМ-объекты. СОМ-объекты можно использовать для манипулирования ведущей системой при помощи контроллера автоматизации — средства, умеющего взаимодействовать с интерфейсами СОМ. Наиболее популярным контроллером автоматизации является Visual Basic, второй по популярности VBScript. Средство ODSOLE из состава SQL Server является контроллером автоматизации и представлено в виде набора системных процедур, которые можно вызывать из Transact-SQL.
SQL Server и автоматизация COM 449 Процедуры sp_OA Процедуры автоматизации Transact-SQL названы по соглашению: sp_OAFunction, где Function обозначает действие, выполняемое процедурой. Например, sp_OACreate создает СОМ-объект, sp_OAMethod вызывает метод, sp_OAGetProperty и sp_OASetProperty получают и устанавливают свойство объекта и т. д. Эти процедуры делают язык Transact-SQL значительно более мощным, так как предоставляют полноценный доступ к миру СОМ. sp_checkspelling Нижеследующий код показывает простую процедуру, использующую sp_0A для автоматизации СОМ-объекта. Процедура создает экземпляр объекта Microsoft Word Application и вызывает его метод CheckSpelling для проверки орфографии слова, переданного процедуре в качестве параметра. Вот код процедуры: USE master GO IF (OBJECT_ID('sp_checkspening') IS NOT NULL) DROP PROC sp_checkspelling GO CREATE PROC sp_checkspe11ing (Pword varcharOO). -- Слово для проверки (^correct bit OUT -- Флаг правильности написания слова /* Обьект: sp_checkspelling Описание: Проверяет орфографию слова, используя обьект Microsoft Word-Application Использование: sp_checkspelling @word varcharA28). -- Слово для проверки (Pcorrect bit OUT -- Флаг правильности написания слова Возвращает: (ничего) $Автор: Ken Henderson $. Email: khen@khen.com Пример: EXEC sp_checkspelling 'asdf. (^correct OUT Создано: 2000-10-14. $Изменено: 2001-01-13 $. */ AS IF ((Pword=7?') GOTO Help DECLARE @object int.-- Рабочая переменная для создания экземпляров объектов @hr int -- Содержит HRESULT. возвращаемый СОМ -- Создать объект Word Application EXEC @hr=sp_0ACreate 'Word.Application', ^object OUT IF (Ohr <> 0) BEGIN EXEC sp_displayoaerrorinfo @object. @hr RETURN END -- Вызвать метод CheckSpelling EXEC @hr = spJDAMethod (Pobject, 'CheckSpelling', (^correct OUT, @word IF (Ohr <> 0) BEGIN EXEC sp_displayoaerrorinfo (Pobject. @hr RETURN (Phr
450 Глава 19. Автоматизация END -- Уничтожить обьект EXEC @hr = spJDADestroy ^object IF (@hr <> 0) BEGIN EXEC sp_displayoaerrorinfo (Pobject. @hr RETURN @hr END RETURN 0 Help: EXEC sp_usage (Pobjectname='sp_checkspe11ing'. @desc='Проверяет орфографию слова, используя объект Microsoft Word Application'. @parameters=' @word varcharOO), -- Слово для проверки (^correct bit OUT -- Флаг правильности написания слова @author='Ken Henderson'. @email = 'khen(i>khen.com'. @datecreated='20001014',@datelastchanged='20010113'. @example='EXEC sp_checkspe11ing ''asdf'. (^correct OUT', - @returns='(None)' RETURN -1 GO sp_checkspelling имеет два параметра: слово, орфографию которого требуется проверить; и выходной параметр, служащий индикатором того, правильно слово написано или нет. Вызов процедуры выглядит так: DECLARE @cor bit EXEC sp_checkspelling 'asdf. @cor OUT SELECT @cor (Результаты) 0 В процедуре три ключевых фрагмента: создание СОМ-объекта, вызов метода и уничтожение объекта. Сначала рассмотрим вызов sp_0AC reate. Он создает экземпляр СОМ-объекта. Word. Application — это то, что известно в мире СОМ как ProgID (программный идентификатор). Программный идентификатор представляет собой строку, определяющую СОМ-объект таким образом, что приложения могут обращаться к объекту по имени. Как узнать, какой программный идентификатор следует использовать? Существует несколько способов. Первый — посмотреть документацию на интерфейс объекта Word в MSDN. Второй — запустить среду Visual Basic, добавить в проект ссылку на библиотеку типов объекта Microsoft Word и воспользоваться встроенной в Visual Studio технологией IntelliSense, которая отобразит объекты и методы, доступные в Word. (To же самое можно сделать в Delphi через опцию Project ► Import Type Library.) Третий — просмотреть системный реестр и найти все интерфейсы, включающие Microsoft Word. В реестре указано, например, что Word. Application представляет собой строку Versionlndepen- dentProglD для Word. Это значит, что создание экземпляра Word.Application должно работать независимо от установленной версии приложения Word. Описатель объекта, который вернула процедура sp_0AC reate, сохраняется в переменной @obj ect. Этот описатель передается далее процедуре sp_0AMethod, кото-
SQL Server и автоматизация COM 451 рая вызывает методы интерфейса Word.Application. В данном случае вызывается только один метод — CheckSpelling — и передается параметр @word, содержащий слово, орфографию которого необходимо проверить, и параметр ^correct, содержащий 0 или 1 как выходное значение метода. Когда работа с объектом завершена, он уничтожается вызовом sp_OADestroy. И снова этой процедуре передается описатель ©object, ранее полученный из процедуры sp_OACreate. СОВЕТ Управлять созданием объектов можно с помощью процедуры sp_OACreate: запускать объект внутри процесса SQL Server или вне его. СОМ-объекты, работающие внутри процесса, известны как внут- рипроцессные серверы. СОМ-объекты, работающие вне процесса, — как внепроцессные серверы. Когда возможно, следует создавать СОМ-объекты, которые могут выполняться вне процесса (ЕХЕ). Они с меньшей долей вероятности приведут к проблемам самого SQL Server, так как выполняются вне его процессного пространства. Если объект поддерживает работу вне процесса, то можно, используя третий параметр процедуры sp_OACreate — context, указать SQL Server, что объект необходимо загрузить вне процесса. Значение 1 указывает на необходимость загрузить объект в процесс, 4 — вне процесса, 5 — использовать любой вариант. Так работают с СОМ-объектами в Transact-SQL. Как и в большинстве языков программирования, сначала создается объект, затем над ним производятся некоторые операции, потом происходит высвобождение ресурсов. sp_exporttable Рассмотрим еще одну процедуру, использующую процедуры sp_0A для управления СОМ-объектом — это sp_expo rttable. Она создает экземпляр объектов SQL-DMO для того, чтобы с их помощью экспортировать данные из указанной таблицы. Процедура работает аналогично встроенной команде BULK INSERT, предоставляя интерфейс к ВСР API из Transact-SQL. Вот код процедуры: USE master GO IF (OBJECTJD('sp_exporttable') IS NOT NULL) DROP PROC sp_exporttable GO CREATE PROC sp_exporttable stable sysname, -- Таблица для экспорта @outputpath sysname=NULL, -- Выходной каталог @outputname sysname=NULL, -- Выходной файл (по умолчанию @table+'.BCP') @server sysname='(local)'. -- Имя сервера @username sysname='sa', -- Имя учетной записи (по умолчанию 'sa') ©password sysname=NULL, -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение /* Обьект: sp_exporttable Описание: Экспортирует данные из указанной таблицы Использование: sp_exporttable @table sysname, -- Таблица для экспорта @outputpath sysname=NULL. -- Выходной каталог @outputname sysname=NULL, -- Выходной файл (по умолчанию @table+'.ВСР') @server sysname='(local)'. -- Имя сервера @username sysname='sa'. -- Имя учетной записи (по умолчанию 'sa') @password sysname=NULL. -- Пароль учетной записи
452 Глава 19. Автоматизация @trustedconnection bit=l -- Использовать доверенное соединение Возвращает: количество экспортированных строк $Автор: Кен Хендерсон $. Email: khen@khen.com Пример использования: EXEC sp_exporttable 'authors'. 'C:\TEMPV Создана: 1999-06-14. Изменена: 2000-12-01 $. */ AS IF (@table=7?') OR (@outputpath IS NULL) GOTO Help DECLARE @srvobject int, -- объект Server ^object int, -- переменная для создания СОМ-объектов @hr int. -- хранит HRESULT @bcobject int, -- переменная для хранения указателя на обьект BulkCopy @TAB_DELIMITED int, -- константа разделения табуляцией @logname sysname, -- Имя файла протокола @errname sysname. -- Имя файла ошибок @dbname sysname. -- Имя базы @rowsexported int -- количество экспортированных строк SET @TAB_DELIMITED=2 -- константа SQL-DM0 для экспорта с разделением табуляцией SET @dbname=ISNULL(PARSENAME((atable.3).DB_NAME()) -- Извлечь имя базы: по умолчанию - текущая база SET @table=PARSENAME(@table.l) -- Убрать лишнее из имени таблицы IF ((atable IS NULL) BEGIN RAISERR0R('Неверное имя таблицы.'.16.1) GOTO Help END IF (RIGHT(@outputpath.l)<>'\') SET @outputpath=@outputpath+'\' -- При необходимости добавить обратный слэш SET @logname=@outputpath+@table+'.LOG' -- составить полное имя файла протокола SET @errname=@outputpath+@table+'.ERR' -- составить полное имя файла ошибок IF (@outputname IS NULL) SET @outputname=@outputpath+@table+'.BCP' -- составить полное имя выходного файла ELSE IF (CHARINDEXC\'.(aoutputname)=0) SET @outputname=@outputpath+@outputname -- Создать обьект SQLServer EXEC @hr=sp_0ACreate 'SQLDM0,SQLServer'. @srvobject OUTPUT IF (@hr <> 0) GOTO ServerError -- Создать обьект BulkCopy EXEC @hr=sp_0ACreate 'SQLDM0.BulkCopy'. @bcobiect OUTPUT IF (@hr <> 0) GOTO BCPError -- Установить свойство DataFilePath объекта BulkCopy EXEC @hr = sp_0ASetProperty @bcobject, 'DataFilePath'. (jtoutputname IF №hr <> 0) GOTO BCPError -- Указать обьекту BulkCopy на необходимость создания файла с разделителем в виде знака табуляции EXEC @hr = sp_0ASetProperty №cobiect. 'DataFileType'. @TAB_DELIMITED IF (@hr <> 0) GOTO BCPError -- Установить свойство LogFilePath обьекта BulkCopy
SQL Server и автоматизация COM 453 EXEC @hr = sp_OASetProperty @bcobject, 'LogFilePath'. @logname IF (@hr <> 0) GOTO BCPError -- Установить свойство ErrorFilePath обьекта BulkCopy EXEC @hr = sp_OASetProperty @bcobject. 'ErrorFilePath', @errname IF (@hr <> 0) GOTO BCPError -- Установить соединение с сервером IF (@trustedconnection=l) BEGIN EXEC @hr = spJDASetProperty @srvobject, 'LoginSecure'. 1 IF (@hr <> 0) GOTO ServerError EXEC @hr = spJDAMethod @srvobject, 'Connect', NULL, ©server END ELSE BEGIN IF (^password IS NOT NULL) EXEC @hr = sp_OAMethod @srvobject. 'Connect'. NULL, @server. @username. ^password ELSE EXEC @hr = sp_OAMethod @srvobject, 'Connect'. NULL. @server, @username END IF (@hr <> 0) GOTO ServerError -- Получить указатель на коллекцию Databases объекта SQLServer EXEC @hr = spJDAGetProperty @srvobject. 'Databases', @object OUT IF (@hr <> 0) GOTO ServerError -- Получить указатель на необходимую базу EXEC @hr = spJDAMethod ^object. 'Item', ^object OUT. @dbnarae IF №hr <> 0) GOTO Error -- Получить указатель на таблицу IF (OBJECTPROPERTY(OBJECT_ID(@table).'IsTable')=l) BEGIN EXEC @hr = spJDAMethod ^object, 'Tables'. @object OUT. stable IF (@hr <> 0) GOTO Error END ELSE -- Получить указатель на представление IF (OBJECTPROPERTY(OBJECT_ID(@table).'IsView')=l) BEGIN EXEC @hr = sp_OAMethod ^object, 'Views', ^object OUT. @table IF (@hr <> 0) GOTO Error END ELSE BEGIN RAISERRORt'Объект должен быть таблицей или представлением'.16.1) RETURN -1 END -- Вызвать метод ExportData для экспорта данных таблицы/представления при помощи BulkCopy EXEC @hr = sp_OAMethod ©object. 'ExportData', @rowsexported OUT, @bcobject IF (@hr <> 0) GOTO Error EXEC spJMtestroy @srvobject -- Уничтожить объект server EXEC spjDADestroy @bcobject -- Уничтожить объект bcp RETURN @rowsexported Error: EXEC sp_displayoaerrorinfo @object, @hr GOTO ErrorCleanUp BCPError: EXEC sp_displayoaerrorinfo @bcobject, @hr GOTO ErrorCleanUp
454 Глава 19. Автоматизация ServerError: EXEC spjjisplayoaerronnfo @srvobject. @hr GOTO ErrorCleanUp ErrorCleanUp: IF @srvobject IS NOT NULL EXEC spJDADestroy @srvobject IF @bcobject IS NOT NULL EXEC spJDADestroy @bcobject RETURN -2 Help: EXEC sp_usage @objectname='sp_exporttable'. @desc='Экспортирует данные из указанной таблицы'. @parameters=' stable sysname, -- Таблица для экспорта @outputpath sysname=NULL, -- Выходной каталог @outputname sysname=NULL, -- Выходной файл (по умолчанию @table+''.ВСР'') ^server sysname=''(local)'',-- Имя сервера @username sysname=''sa''. -- Имя учетной записи (по умолчанию ''sa'1) @password sysnarae=NULL. -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение @author='Ken Henderson'. @emai1='khen@khen.com', @datecreated='19990614',@datelastchanged='20001201'. @exaraple='EXEC sp_exporttable ''authors'1. ''C:\TEMPV'', @returns='количество экспортированных строк' RETURN -1 GO Процедура sp_exporttable работает следующим образом. 1. Создается объект SQLServer. Он потребуется для соединения с сервером. Большинство приложений DMO используют объект SQLServer. К другим объектам на сервере мы получаем доступ через иерархию объекта SQLServer, подобно спуску по дереву объектов в Enterprise Manager. 2. Создается объект BulkCopy. Он будет использован для экспорта данных таблицы. В конечном счете, будет вызван метод ExportData для указанной таблицы или представления, чтобы выгрузить данные в файл операционной системы. Методу ExportData требуется объект BulkCopy. 3. Устанавливаются различные свойства объекта BulkCopy, контролирующие процесс экспорта. 4. При помощи объекта SQLServer происходит соединение с сервером. 5. При помощи вложенных коллекций объекта SQLServer выбирается таблица или представление, данные из которого необходимо экспортировать. 6. Вызывается метод ExportOata для выбранной таблицы или представления. В качестве параметра методу ExportData передается объект BulkCopy. 7. После завершения экспорта данных объекты SQLServer и BulkCopy высвобождаются. -- Уничтожить обьект server -- Уничтожить обьект Ьср
SQL Server и автоматизация COM 455 Комментарии в теле процедуры иллюстрируют ее работу. Процедура достаточно проста. Ее можно запустить, используя такой синтаксис: DECLARE @rc int EXEC @rc=pubs.,sp_exporttable @table='pubs..authors'. @outputpath='c:\temp\' SELECT RowsExported=@rc RowsExported 23 Обратите внимание на использование префикса pubs. . в вызове процедуры. Так как sp_exporttable использует функцию OBJECTPROPERTY() (она не работает между различными базами), то для корректной работы с объектами в различных базах необходимо временно изменить контекст базы данных на ту базу, в которой находится указанный объект (таблица или представление). Как говорилось ранее, указание имени базы при вызове системной хранимой процедуры временно изменяет контекст базы. Предыдущий вызов эквивалентен следующему: USE pubs GO EXEC @rc=sp_exporttable @table='pubs..authors'. @outputpath='c:\temp\' GO USE master - или другая база GO SELECT RowsExported=@rc Возможно, вы обратили внимание на то, что была вызвана системная хранимая процедура sp_displayoaerrorinfo. Эта процедура используется для отображения более подробной информации об ошибках, возвращаемых процедурами sp_0A. Процедура sp_displayoaerrorinfo вызывает spJJAGetErrorlnfo для получения подробной информации об ошибке по коду ошибки автоматизации объекта. Процедура sp_displayoaerrorinf о не создается по умолчанию, но ее можно найти в Books Online. Эта процедура использует в своей работе процедуру sp_hexadecimal (которую также можно найти в Books Online) для преобразования двоичных значений в строки, содержащие шестнадцатеричные представления чисел. Исходный код обеих процедур можно найти в разделе Books Online «OLE Automation Return Codes andEiror Information». Этот и последующие примеры иллюстрируют использование хранимых процедур sp_0A для автоматизации СОМ-объектов (SQL-DMO в данном случае), предоставляемых самим SQL Server. Эти объекты предоставляют большую часть основной функциональности приложения Enterprise Manager и являются удобным способом программного управления сервером. Безусловно, вы не ограничены доступом только к тем объектам, которые предоставлены самим SQL Server. Возможна автоматизация объектов, предоставляемых приложениями PowerBuilder, Excel, Oracle и другими — вплоть до создания своих собственных СОМ-объектов и их использования в SQL Server. Комментарии в теле процедуры sp_expo rttable поясняют ее работу. Используя автоматизацию СОМ, процедура легко справляется с весьма сложной задачей. Объем Transact-SQL кода не сильно превосходит тот, который необходим для написания аналогичной программы на Delphi или Visual Basic.
456 Глава 19. Автоматизация sp_importtable Несмотря на то что для импорта большого количества данных существует команда BULK INSERT, рассмотрим код процедуры sp_importtable, дополняющей процедуру sp_exporttable. USE master GO IF (OBJECTJD('spJmporttable') IS NOT NULL) DROP PROC spjmporttable GO CREATE PROC spjmporttable stable sysname, @inputpath sysname-NULL, @inputname sysname=NULL, ^server sysname='(local)' @username sysname='sa', ^password sysname=NULL, @trustedconnection bit=l /* Обьект: spjmporttable Описание: Импортирует таблицу подобно BULK INSERT Таблица для импорта Входной каталог Входной файл (по умолчанию @table+ Имя сервера Имя учетной записи (по умолчанию 'sa Пароль учетной записи Использовать доверенное соединение ВСР') Использование: spjmporttable stable sysname. @inputpath sysname=NULL. @irrpLitname sysname=NULL, @server sysname='(local)', @username sysname='sa'. ©password sysname-NULL -- Таблица для импорта -- Входной каталог -- Входной файл (по умолчанию @table+'.ВСР') -- Имя сервера -- Имя учетной записи (по умолчанию 'sa') -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение Возвращает: количество импортированных строк $Автор: Кен Хендерсон $. Email: khen@khen.com Пример использования: EXEC spjmporttable 'authors', 'C:\TEMPV Создана: 1999-06-14. Изменена: 2000-12-03 $. */ AS IF (@table=7?') OR (@inputpath IS NULL) GOTO Help DECLARE @srvobject int. ©object int. @hr int. ©bcobject int. @TAB_DELIMITED int. @logname sysname, @errname sysname. @dbname sysname. @rowsimported int обьект Server переменная для создания СОМ-объектов хранит HRESULT хранит указатель на обьект BulkCopy константа разделения табуляцией Имя файла протокола Имя файла ошибок Имя базы количество импортированных строк SET @TAB_DELIMITED=2 -- константа S0L-DM0 для импорта с разделением табуляцией SET №name=ISNULL(PARSENAME(@table.3).DBJAME()) -- Извлечь имя базы SET @table=PARSENAME(@table.l) -- Убрать лишнее из имени таблицы IF «atable IS NULL) BEGIN RAISERRORt'Неверное имя таблицы.',16,1) RETURN -1
SQL Server и автоматизация COM 457 END IF (RIGHT(©inputpath.l)<>'\') SET @inputpath=@inputpath+'\' -- добавить 'V если требуется SET ©logname=@inputpath+©table+'.LOG' -- составить полное имя файла протокола SET ©errname=©inputpath+©table+'.ERR' -- составить полное имя файла ошибок IF (©inputname IS NULL) SET ©inputname=©inputpath+@table+'.BCP' -- составить полное имя входного файла ELSE SET @inputname=©inputpath+©inputname -- Создать обьект SQLServer EXEC ©hr=sp_OACreate 'SQLDMO.SQLServer', ©srvobject OUT IF (©hr <> 0) GOTO ServerError -- Создать обьект BulkCopy EXEC ©hr=sp_0ACreate 'SQLDMO.BulkCopy'. ©bcobject OUT IF (©hr <> 0) GOTO BCPError -- Установить свойство DataFilePath обьекта BulkCopy EXEC ©hr = sp_OASetProperty ©bcobject, 'DataFi1ePath'. ©inputname IF (@hr <> 0) GOTO BCPError -- Указать обьекту BulkCopy на необходимость создать разделенный табуляцией файл EXEC ©hr = sp_OASetProperty ©bcobject. 'DataFi1eType', ©TAB_DELIMITED IF (©hr <> 0) GOTO BCPError -- Установить свойство LogFilePath обьекта BulkCopy EXEC ©hr = sp_OASetProperty ©bcobject, 'LogFilePath'. ©logname IF (©hr <> 0) GOTO BCPError -- Установить свойство ErrorFilePath объекта BulkCopy EXEC ©hr = spJDASetProperty ©bcobject, 'ErrorFilePath', ©errname IF (©hr <> 0) GOTO BCPError -- Установить свойство UseServerSideBCP обьекта BulkCopy EXEC ©hr = sp_0ASetProperty ©bcobject. 'UseServerSideBCP'. 1 IF (©hr <> 0) GOTO BCPError -- Установить соединение с сервером IF (©trustedconnection=l) BEGIN EXEC ©hr = sp_OASetProperty ©srvobject. 'LoginSecure', 1 IF (©hr <> 0) GOTO ServerError EXEC ©hr = spJDAMethod ©srvobject, 'Connect', NULL, ©server END ELSE BEGIN IF (©password IS NOT NULL) EXEC @hr = sp_OAMethod ©srvobject, 'Connect'. NULL, ©server, ©username. ©password ELSE EXEC @hr = sp_OAMethod ©srvobject, 'Connect'. NULL, ©server, ©username END IF (©hr <> 0) GOTO ServerError -- Получить указатель на коллекцию Databases обьекта SQLServer EXEC ©hr = sp__0AGetProperty ©srvobject. 'Databases', ©object OUT IF (©hr <> 0) GOTO ServerError -- Получить указатель на заданную базу из коллекций Databases EXEC ©hr = spJDAMethod ©object. 'Item', ©object OUT, ©dbname IF (©hr <> 0) GOTO Error
458 Глава 19. Автоматизация -- Получить указатель на заданную таблицу из коллекций Tables IF (OBJECTPROPERTY(OBJECT_ID(@table).'IsTable')<>l) BEGIN RAISERRORt'Целевой обьект должен быть таблицей.',16,1) RETURN -1 END BEGIN EXEC @hr = spjDAMethod ^object, 'Tables', ^object OUT, stable IF (@hr <> 0) GOTO Error END -- Вызвать метод ImportData обьекта Table для импорта данных EXEC @hr = spjDAMethod ©object. 'ImportData'. @rowsimported OUT. @bcobject IF (@hr <> 0) GOTO Error EXEC spJDADestroy @srvobject -- Уничтожить обьект server EXEC spJDADestroy @bcobject -- Уничтожить обьект bcp RETURN @rowsimported Error: EXEC sp_displayoaerrorinfo @object. @hr GOTO ErrorCleanUp BCPError: EXEC sp_displayoaerrorinfo @bcobject, @hr GOTO ErrorCleanUp ServerError: EXEC sp_displayoaerrorinfo @srvobject, @hr GOTO ErrorCleanUp ErrorCleanUp: IF @srvobject IS NOT NULL EXEC spJDADestroy @srvobject -- Уничтожить обьект server IF @bcobject IS NOT NULL EXEC spJDADestroy @bcobject -- Уничтожить обьект bcp RETURN -2 Help: EXEC spjjsage @objectname='sp_importtable'. @desc='Импортирует таблицу подобно BULK INSERT'. @parameters=' (stable sysname, -- Таблица для импорта @inputpath sysname=NULL, -- Входной каталог @inputname sysname=NULL. -- Входной файл (по умолчанию @table+' ' .BCP' ') @server sysname=''(local)'', -- Имя сервера @username sysname=''sa''. -- Имя учетной записи (по умолчанию ''sa'') @password sysname=NULL. -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение @author='Ken Henderson', @email='khen@khen.com'.
SQL Server и автоматизация COM 459 @datecreated=' 19990614' ,(adatelastchanged='20001203'. @example='EXEC sp_importtable ''authors'', ''C:\TEMP\'''. @returns='количество импортированных строк' RETURN -1 GO Аналогично команде BULK INSERT, процедура sp_importtable загружает данные из внешних файлов в таблицы SQL Server. Процедура рассматривает входной файл как файл, разделенный знаками табуляции, но при желании это можно изменить. Вот пример совместного использования sp_exporttable и sp_importtable: SET NOCOUNT ON USE pubs DECLARE @rc int -- Сначала экспортирум записи EXEC @rc=pubs..sp_exporttable Otable='pubs..authors', @outputpath='c:\temp\' SELECT @rc AS RowsExported -- Далее создадим новую таблицу SELECT * INTO authorsimp FROM authors WHERE 1=0 -- Импортируем данные EXEC pubs, .spjmporttable @table='authorsimp'. @inputpath='c:\temp\',@inputname='authors.bcp' SELECT COUNTf*) AS RowsLoaded FROM authorsimp GO OROP TABLE authorsimp Сценарий начинается с экспорта таблицы authors базы pubs в текстовый файл с разделителями в виде знаков табуляции. Далее создается пустая копия таблицы, и данные из файла импортируются при помощи sp_impo rttable. Как при использовании BULK INSERT, файл, который загружается с помощью sp_importtable, должен быть доступен для SQL Server. sp_getSQLregistry SQL-DMO предоставляет доступ к богатой функциональности, подобной той, которая есть в Enterprise Manager. Так как SQL-DMO построен на основе технологии СОМ, функциональность SQL-DMO доступна через СОМ-объекты. Один из таких объектов — объект Registry, предоставляющий доступ к той части системного реестра, которая используется SQL Server. Объект Registry может потребоваться для получения следующей информации: имя учетной записи SQL Mail no умолчанию, путь к установке SQL Server по умолчанию, количество процессоров, установленных на сервере и т. д. Ниже представлена хранимая процедура, иллюстрирующая, как используется объект Registry. USE master GO IF OBJECTJD('sp_getSQLregistry') IS NOT NULL DROP PROC sp_getSQLregistry GO CREATE PROC sp_getSQLregistry @regkey varcharA28), -- Ключ реестра @regvalue varchar(8000)=NULL OUTPUT, -- Значение из ветки SQL Server ©server varcharA28)='(local)', -- Имя сервера
460 Глава 19. Автоматизация @username sysname='sa', -- Имя учетной записи (по умолчанию 'sa') ^password sysname=NULL. -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение /* Объект: sp_getSQLregistry Описание: Получает значение из раздела системного реестра, отведенного для SQL Server Использование: sp_getSQLregistry @regkey varcharA28). -- Ключ реестра @regvalue varchar(8000) OUTPUT, -- Значение из ветки SQL Server ©server varcharA28)= '(local)', -- Имя сервера @username varcharA28)= 'sa'. -- Имя учетной записи (по умолчанию 'sa') ©password varcharA28)=NULL -- Пароль учетной записи @trustedconnection bit=l -- Использовать доверенное соединение Возвращает: Длину прочитанного из реестра значения $Автор: Кен Хендерсон $. Email: khen@khen.com $Ревизия: 6.4 $ Пример использования: sp_getSQLregistry 'SQLRootPath'. Osqlpath OUTPUT Создана: 1996-09-03. $Изменена: 2000-11-14 $. */ AS SET N0C0UNT ON IF (@regkey=7?') GOTO Help DECLARE @srvobject int, -- обьект SQLServer ^object int, -- переменная для создания СОМ-обьектов @hr int -- хранит HRESULT -- Создаем обьект SQLServer EXEC @hr=sp_0ACreate 'SQLDM0.SQLServer'. @srvobject OUTPUT IF (@hr <> 0) GOTO ServerError -- Соединяемся с сервером IF (@trustedconnection=l) BEGIN EXEC @hr = sp_OASetProperty @srvobject, 'LoginSecure', 1 IF (@hr <> 0) GOTO ServerError EXEC @hr = spJDAMethod @srvobject, 'Connect'. NULL, @server END ELSE BEGIN IF ((^password IS NOT NULL) EXEC @hr = sp_OAMethod @srvobject, 'Connect'. NULL, (^server, Ousername. ^password ELSE EXEC @hr = sp_OAMethod @srvobject. 'Connect', NULL, @server, @username END IF (Ohr <> 0) GOTO ServerError -- Получаем указатель на обьект Registry EXEC @hr = sp_OAGetProperty @srvobject. 'Registry', Oobject OUT IF (@hr <> 0) GOTO Error -- Получаем значение EXEC @hr = sp_OAGetProperty ^object, @regkey, @regvalue OUT IF (@hr <> 0) GOTO ServerError
SQL Server и автоматизация COM 461 EXEC sp_OADestroy (Psrvobject -- Уничтожить объект Server RETURN datalength(IPregvalue) Error: EXEC sp_displayoaerrorinfo (Pobject. (Phr GOTO ErrorCleanUp ServerError: EXEC sp_displayoaerrorinfo (Psrvobject. (Phr GOTO ErrorCleanUp ErrorCleanUp: IF (Psrvobject IS NOT NULL EXEC sp_OADestroy (Psrvobject -- Уничтожаем объект RETURN -2 Help: EXEC sp_usage (Pobjectname='sp_getSQLregistry'. @desc='Получает значение из раздела системного реестра, отведенного для SQL Server', (Pparameters=' (Pregkey varcharA28). -- Ключ реестра (Pregvalue varchar(8000) OUTPUT, -- Значение из ветки SQL Server (Pserver varcharA28)='(local)', -- Имя сервера (Pusername varcharA28)='sa', -- Имя учетной записи (по умолчанию '"sa") (Ppassword varcharA28)=NULL -- Пароль учетной записи (Ptrustedconnection bit=l -- Использовать доверенное соединение', @author='Ken Henderson', @email='khen@khen.com', @datecreated='19960903'. (Pdatelastchanged='20001114'. (Pversion=' 6', (Previ sion='4', (Preturns='Длина прочитанного из реестра значения', (Pexample='sp_getSQLregistry 'SQLRootPath'. (Psqlpath OUTPUT' GO Процедуру sp_getSQL registry можно применять для получения значений из той части системного реестра, которая используется SQL Server. Например, таким образом: SET N0C0UNT ON DECLARE (Pnumprocs varchar(lO). (Pinstalledmemory varcharB0), (Prootpath varchar(8000) EXEC sp_getSQLregistry 'Physical Memory', (Pinstalledmemory OUT EXEC sp_getSQLregistry 'NumberOfProcessors', (Pnumprocs OUT EXEC sp_getSQLregistry 'SQLRootPath'. (Prootpath OUT SELECT (Pnumprocs AS NumberOfProcessors, (Pinstalledmemory AS Install edRAM. (Prootpath AS RootPath DECLARE (Pcharset varchar(lOO), (Psortorder varchar(lOO) EXEC sp_getSQLregistry 'CharacterSet', (Pcharset OUT SELECT (Pcharset AS CharacterSet EXEC sp_getSQLregistry 'SortOrder', (Psortorder OUT SELECT (Psortorder AS SortOrder
462 Глава 19. Автоматизация Итоги В этой главе вы узнали, что: ■ СОМ представляет собой мощную, повсеместно распространенную технологию, позволяющую приложениям взаимодействовать друг с другом; ■ хотя СОМ не совершенна, она разрешила большое количество проблем предшествующих технологий; ■ благодаря процедурам sp_0A из языка Transact-SQL можно получить доступ к СОМ-интерфейсам объектов других приложений и самого SQL Server (например, таким как SQL-DMO); ■ сочетая мощь реляционной базы данных с гибкостью и распространенностью СОМ, можно создавать действительно мощные приложения.
О Расширенные хранимые процедуры Программировать в машинных кодах — все равно, что есть зубочисткой. Кусочки столь малы и процесс столь утомителен, что обед длится вечно. Чарльз Петцольд' В дополнение к обычным хранимым процедурам Transact-SQL можно написать хранимые процедуры на С, C++ и других языках, которые могут взаимодействовать с ODS API. Эти процедуры, известные как расширенные хранимые процедуры, получают параметры и возвращают результаты через ODS. Они расположены в библиотеках динамической компоновки и работают так же, как и обычные процедуры. Для создания ссылки на процедуры в SQL Server необходимо вызвать системную хранимую процедуру sp_addextendedp roc или сделать это через Enterprise Manager, который также вызовет sp_addextendedproc. Процедура sp_addextendedproc вызывает команду DBCCADDEXTENDEDPROC() для добавления ссылки в системные таблицы sysobj ects и syscomments (sysobj ects хранит имя объекта и тип объекта, syscomments хранит имя библиотеки динамической компоновки, в которой находится код расширенной хранимой процедуры). Эта ссылка является средством, при помощи которого можно вызывать расширенную хранимую процедуру из Transact-SQL. Поэтому расширенные хранимые процедуры очень похожи на обычные процедуры. С точки зрения SQL Server, расширенные хранимые процедуры находятся в базе maste r (они не могут быть созданы в других базах) и выполняются в адресном пространстве процесса SQL Server. Писать расширенные хранимые процедуры можно не только на С или C++. Однако их написание на других языках потребует транслирования заголовочного файла ODS из С на выбранный язык. Я писал расширенные хранимые процедуры на Delphi Object Pascal — вы тоже можете попробовать, но предупреждаю: это занятие не для робких. Если вы хотите в виде хранимых процедур написать интерфейс к программам, созданным не на С или C++, для которых у вас нет исходного кода, то разумным решением будет написать расширенную хранимую процедуру-обертку на C++ и затем вызывать существующие программы из нее. Также можно использовать существующий код и применять удобные языковые средства для работы с мощью ODS Pelzold, Charles. Code. Redmond, WA: Microsoft Press. 2000. С 349.
464 Глава 20. Расширенные хранимые процедуры API. Можно получить максимальную пользу от обеих частей, не транслируя код из языка в язык. Кроме очевидных различий между языками, расширенные и обычные хранимые процедуры имеют еще несколько существенных отличий друг от друга. Расширенные хранимые процедуры при вызове из других баз автоматически не ищутся в базе master, при этом даже не подразумевается, что используется контекст активной базы. Для выполнения расширенной хранимой процедуры из базы, отличной от master, необходимо указать полное имя процедуры, включая имя базы. Другой неожиданностью при работе с расширенными хранимыми процедурами для вас может стать то, что команда DBCC, выгружающая библиотеку динамической компоновки, где расположена процедура (DBCC d 11 name(FREE)), работает неправильно в том случае, если библиотека расположена вне каталога с sqlsewr.exe — главным исполняемым файлом SQL Server. He знаю, почему так происходит, но часто библиотека не выгружается, если изначально она была указана с полным путем. Сообщение об ошибке не появляется, но тем не менее библиотека остается в адресном пространстве процесса SQL Server. Если вы попытаетесь перезаписать ее, то обнаружите, что она все еще используется. Решить эту проблему можно так: скопируйте библиотеку в каталог BINN (где расположен файл sqlsewr.exe) и укажите ее в качестве параметра sp_addextendedproc без полного пути. Внутренние механизмы SQL Server используют Windows API функции LoadL i - bгагу() и FreeLi braryO для загрузки и выгрузки библиотек. Эти функции поддерживают полные имена файлов библиотек, поэтому указание полного пути не должно вызывать проблем. Но поскольку, несмотря на это, описанная выше проблема возникала на нескольких системах, этот вопрос заслуживает внимания. Open Data Services ODS API подробно описан в Books Online, поэтому я предлагаю вам общий обзор. ■ ODS — это интерфейс уровня вызова (CLI) для создания расширенных хранимых процедур и шлюзов к базам данных. Так как компания Microsoft предлагает использовать связанные серверы вместо шлюзов к базам данных, сегодня основное назначение ODS — создание расширенных хранимых процедур. ■ Под CLI понимается тот аспект, что интерфейс не является СОМ-интер- фейсом. Это обычный библиотечный (DLL) интерфейс. Библиотека динамической компоновки экспортирует функции, которые использует приложение. ■ ODS API реализован в библиотеке OPENDS60.DLL. В SQL Server 2000 и последующих версиях эта библиотека представляет собой всего лишь некую прослойку по отношению к ODS-функциям, которые реализованы в самом сервере. Если сравнить размеры файла OPENDS60.DLL для версий SQL Server 7.0 и SQL Server 2000, то можно увидеть, что размер файла для SQL Server 2000 существенно меньше. Это является следствием того, что ODS более не размещается в библиотеке динамической компоновки. ■ Обычно для использования ODS API необходимо скомпоновать C++ проект с библиотекой импорта OPENDS60.LIB. Если для создания расширенных хра-
Open Data Services 465 нимых процедур используется помощник Visual C++ (Extended Procedure Wizard), то он включает ссылку на OPENDS60.LIB автоматически. Стартовый код Каждая расширенная хранимая процедура получает в качестве параметра дескриптор серверного процесса, вызвавшего процедуру. Интерфейс к расширенной хранимой процедуре на языке С выглядит следующим образом. Листинг 20.1. Интерфейс к расширенной хранимой процедуре RETC0DE declspec(dlI export) xp_foo(SRV_PROC *srvproc) { I Директива d 11 export делает функцию доступной извне. Функция будет экспортирована из библиотеки динамической компоновки под своим именем. Когда процедура выполняется из Transact-SQL, SQL Server вызывает функцию Win32 API LoadL i brary() для загрузки библиотеки динамической компоновки (если она еще не загружена) и использует функцию GetProcAdd ress() для получения в библиотеке адреса функции. Как только адрес функции будет установлен, SQL Server приведет этот адрес к указателю функции, соответствующей прототипу из листинга 20.1. SRV_PR0C представляет собой структуру на языке С, определенную в заголовочном файле ODS srvstruc.h, и выглядит эта структура следующим образом. Листинг 20.2. Структура SRV_PROC typedef struct srvjoroc { WORD WORD SRV 10 SRV_L0GINREC void unsigned long unsigned int void char THREAD HANDLE HANDLE HANDLE DBINT SRV COLDESC DBUSMALLINT BYTE WORD void void BYTE BYTE BYTE BYTE BYTE BYTE BYTE tdsversion; status: srvio: login; *langbuff: langlen; event; *server; *threadstack; threadID; threadHDL; iowakeup: exited: rowsent; *coldescp; coldescno: *colnamep: colnamelen; *userdata; *event_data: serverlen: *servername; rpc_active: rpc_server_len; *rpc_server: rpc_databasejen; *rpc_database; I
466 Глава 20. Расширенные хранимые процедуры Листинг 20.2 {продолжение) BYTE BYTE BYTE BYTE unsigned unsigned unsigned unsigned SRV RPCp BYTE unsigned SRV_RPCp char int int short short short SRV SUBCHANNEL rpc_owner_len; *rpc_owner; rpc_proc_len; *rpc_procjiame; rpc_proc_number; rpc_linenumber: rpc_options; rpcjium_params; **rpc_params; non_rpc_active; non_rpc_num_params: **non rpc params: temp_buffer[100]; *(*subprocs); TRANSLATIONJNFO translationjnfo; struct struct PSRV LISTHEAD BOOL long void SRV SUBCHANNEL SRV PEVENTS SRV_PEVENTS void BOOL BOOL , Unsigned unsigned unsigned srvjistentry IOListEntry; srvjistentry CommandListEntry; pNetListHead; bNewPacket; StatusCrit; *serverdata; *subchannel; *pre_events; *post_events: *pjangbuff; fSecureLogin; flnExtendedProc: fLocalPost:!: fMadeBoundCall:!: uFil11:30; SRV_COMPORT_QUEUE comport_queue: void *pSFl: void *pSF2; HANDLE hPreHandlerMutex: HANDLE hPostHandlerMutex: BOOL bSAxp: } SRV_PROC; Если вам понадобилась более подробная информация, вы можете посмотреть содержимое файла srvstruc.h. Однако должен сказать, что каждая структура серверного процесса содержит большой и разнообразный набор данных. Работа расширенных хранимых процедур Обычно расширенная хранимая процедура принимает параметр или параметры и возвращает набор данных или выходное значение в вызвавшее ее соединение. Нередко расширенные хранимые процедуры также порождают сообщения, которые отсылают клиенту. Можно создавать такие расширенные хранимые процедуры, которые не принимают параметры и ничего не возвращают (но случается очень редко). Отсылка сообщений Для того чтобы отослать сообщение из расширенной хранимой процедуры в вызвавшее ее соединение, используется ODS-функция srv_sendmsg(). Вызовы srv_sendmsg() выглядят примерно следующим образом.
Open Data Services 467 Листинг 20.3. Отсылка сообщения при помощи ODS srv_sendmsg(srvproc, SRV_MSG_ERROR. LISTFILEJRROR, SRVJNFO. (DBTINYINT) 0. NULL, 0. 0. "Ошибка при выполнении расширенной хранимой процедуры: неверное число параметров.". SRVJULLTERM); Текстовая строка, которая расположена в середине вызова, и есть само сообщение. Остальные параметры указывают такие атрибуты, как: состояние сообщения; строка, на которой произошло событие, породившее сообщение; тип сообщения. Для получения более подробной информации можно обратиться к документации. Обработка параметров Для работы с параметрами, переданными расширенной хранимой процедуре, используются функции srv_parami nfo(), srv_paramdaLa(), srv_paramtype() и другие подобные функции ODS API. Функция srv_paramLype() позволяет проверить тип указанного параметра, тогда как функция srv_paramdata() позволяет узнать значение параметра. В листинге 20.4 приведен пример использования этих двух функций. ПРИМЕЧАНИЕ Функция srv_paraminfo() может вернуть всю информацию, получаемую функциями srv_paramtype(), srv_paramlen() и srv_paramdata(), за один вызов. Более того, она поддерживает новые типы данных, доступные в SQL Server 7.0 и более поздних версиях. Поэтому предпочтительнее использовать srv_paraminfo(), чем вышеуказанные функции. За подробной информацией обратитесь в Books Online. Листинг 20.4. Обработка параметров // Проверить тип параметра if (srv_paramtype(srvproc. 1) != SRVVARCHAR) { // Отослать сообщение об ошибке и закончить работу srv_sendmsg(srvproc. SRV_MSG_ERROR. LISTFILEJRROR, SRVJNFO. (DBTINYINT) 0, NULL. 0, 0, "Ошибка при выполнении расширенной хранимой процедуры: неверный тип параметра.". SRVJULLTERM): srv_senddone(srvproc, (SRVJ0NEJRR0R | SRVJONEJ0RE), 0, 0); return(XPJRROR): } // Завершить строку параметра нулем memcpy(FileName, srv_paramdata(srvproc. 1), srv_paramlen(srvproc, D): FileName[srv_paramlen(srvproc, 1)] = '\0'; В каждом выделенном полужирным шрифтом вызове функций первый параметр — дескриптор серверного процесса. Это условие для вызова ODS API: дескриптор серверного процесса обычно является первым параметром функции. Второй параметр в обоих примерах — номер параметра, для которого требуется получить информацию. В каждом примере запрашивается информация о первом переданном процедуре параметре.
468 Глава 20. Расширенные хранимые процедуры Возвращение данных Каждая колонка, которая должна входить в набор данных, возвращаемых расширенной хранимой процедурой, должна быть описана при помощи вызова функции srv_describe(). Вот пример: srv_describe(srvproc, I, ColName. SRV_NULLTERM. SRVINT4, sizeof(DBINT). SRVINT4, sizeof(DBINT). 0); Среди прочего здесь указывается имя колонки и тип ее данных. Второй параметр (выделен полужирным шрифтом) — порядковый номер поля в результирующем наборе, то есть номер столбца. Перед тем как отослать запись серверу, необходимо вызвать функцию srv_setco I data() для задания значений полей. Вот пример вызова этой функции: srv_setcoldata(srvproc. 2, LineText); Здесь L i neText — строковая переменная, содержащая данные, которые необходимо занести в колонку. Так как в языках С и C++ строка — это не что иное, как указатель на массив символов, то функции srv_setco I data() передается адрес этого массива. Первые два параметра srv_setco I data() должны быть теперь понятны: дескриптор серверного процесса и порядковый номер поля соответственно. Как только значения полей установлены, каждая запись отсылается серверу при помощи вызова функции srv_sendrow(). Это проиллюстрировано в листинге 20.5. Листинг 20.5. Отсылка результирующих записей if (srv_sendrow(srvproc) == FAIL) { srv_sendmsg(srvproc. SRV_MSG_ERR0R, LISTFILEJRROR, SRVJNFO. (DBTINYINT) 0. NULL. 0, 0. "Ошибка при выполнении расширенной хранимой процедуры: невозможно отослать результаты.", SRV_NULLTERM); return (XPJRR0R): } Сначала вызывается функция srv_sendrow() и проверяется возвращаемое ей значение. Обратите внимание на то, что единственный параметр этой функции — дескриптор серверного процесса. К тому моменту, когда функция вызывается, все значения полей уже добавлены к структуре процесса. В случае неудачного завершения функции серверу отсылается сообщение о проблеме, и из расширенной хранимой процедуры возвращается код ошибки. В случае успешного завершения вызова функции записи продолжают отсылаться серверу. Этот процесс заканчивается вызовом функции srv_senddone(), как показано ниже: srv_senddone(srvproc. SRV_D0NE_M0RE | SRV_D0NE_C0UNT. (DBUSMALLINT)O, i): Обратите внимание на битовую маску (полужирный шрифт), передаваемую в качестве второго параметра функции. Как правило, расширенные хранимые процедуры должны всегда включать флаг SRV_D0NE_M0RE при вызове srv_senddone(): этот флаг говорит о том, что в очереди еще есть результаты. Флаг SRV_D0NE_F INAL означает, что все результаты отосланы. Может показаться, что следует указывать SRV_D0NE_F I NAL в случае, когда больше нет записей для отсылки. Однако SRV_D0NE_F I NAL используется только теми приложениями, которые являются шлюзами к базам данных, а не расширенными хранимыми процедурами. Когда расширенная хранимая процедура завершается, сервер сам отсылает SRV_D0NE_F I NAL, поэтому процедуре не требуется это делать.
Простой пример 469 Простой пример Если вы новичок в разработке приложений на C/C++, то предшествующий обзор разработки расширенных хранимых процедур может вас немного испугать. Однако все не так плохо, как может показаться. Изучив исходный код простой расширенной хранимой процедуры, вы обретете уверенность в себе. Создать расширенную хранимую процедуру так же просто, как вызвать помощник для создания расширенных хранимых процедур в среде Visual C++ (File ► New ► Extended Proc). Помощник создаст рабочее пространство, включит в проект заголовочные файлы ODS и создаст код простой расширенной хранимой процедуры. После этого можно изменить код в соответствии со своими потребностями. Код из листинга 20.6 представляет собой расширенную хранимую процедуру, возвращающую содержимое текстового файла. Она принимает один параметр — имя файла — и возвращает результирующий набор, состоящий из двух полей: номера строки и текста каждой строки файла. Вот исходный код. Листинг 20.6. Процедура xpjistfile RETCODE _declspec(dllexport) xp_!istfile(SRV_PROC *srvproc) { DBINT i = 0: DBCHAR ColName[MAXCOLNAME]; DBCHAR LineText[MAXTEXT]: DBCHAR FileName[MAXFILENAME]; FILE *f: // ШАГ 1: Обработать параметры // Проверить количество параметров if ( srv_rpcparams(srvproc) != 1) { // Отослать сообщение об ошибке и закончить работу srv_sendmsg(srvproc. SRV_MSG_ERR0R. LISTFILEJRROR. SRVJNFO, (DBTINYINT) 0, NULL. 0. 0. "Ошибка при выполнении расширенной хранимой процедуры: неверное число параметров .", SRVJJULLTERM); // SRV_D0NE_M0RE вместо SRV_DONE_FINAL должно завершать результирующий набор srvjenddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0); return(XPJRROR): } // Проверить правильность типа параметра if (srv_paramtype(srvproc. 1) != SRVVARCHAR) { // Отослать сообщение об ошибке и закончить работу srv_sendmsg(srvproc. SRV_MSG_ERR0R. LISTFILEJRROR, SRVJNFO, (DBTINYINT) 0, NULL. 0, 0. "Ошибка при выполнении расширенной хранимой процедуры: неверный тип параметра.", SRVJULLTERM); // SRVJ0NEJI0RE вместо SRVJ0NEJINAL должно завершать результирующий набор srv_senddone(srvproc. (SRVJ0NEJRR0R | SRVJ0NEJ10RE), 0, 0): return(XPJRROR); } // Завершить строку-параметр символом NULL memcpy(FileName, srv_paramdata(srvproc, 1), srv_paramlen(srvproc, D): FileName[srv_paramlen(srvproc, 1)] = '\0': // ШАГ 2: Указать имена столбцов продолжение #
470 Глава 20. Расширенные хранимые процедуры Листинг 20.6 {продолжение) wsprintf(ColName. "LineNo"); srv_describe(srvproc. 1, ColName, SRVJJULLTERM. SRVINT4, sizeof(DBINT). SRVINT4. sizeof(DBINT). 0): wsprintf(Col Name. "Text"); srv_describe(srvproc. 2, ColName. SRVJJULLTERM. SRVCHAR. MAXTEXT. SRVCHAR. 0, NULL); // ШАГ 3: Читать текстовый файл и отсылать результаты строка за строкой if( (f = fopen( FileName. "г" )) != NULL ) { while (!feof(f)) { if (fgets(LineText.MAXTEXT.f) != NULL) { srv_setcoldata(srvproc, 1, (void *)&++i): // Убрать CR/LF в конце строки if (LineText[strlen(LineText)-l]=='\n') { LineText[strlen(LineText)-l]='\0': } if (LineText[strien(LineText)-l]=='\r') { LineText[strlen(LineText)-l]='\0'; } if (strlen(LineText)==0) { //Фильтровать NULL-значения LineText[0]=' ': LineText[l]='\0'; • } srv_setcoldata(srvproc. 2. LineText): srv_setcollen(srvproc. 2, strlen(LineText)); // Отослать строку if (srv_sendrow(srvproc) == FAIL) { srv_sendmsg(srvproc. SRV_MSG_ERROR. LISTFILEJRROR. SRV_INF0. (DBTINYINT) 0, NULL, 0, 0, "Невозможно отослать результаты", SRVJJULLTERM): return (XPJRR0R); } } } fclose(f): } else { // Отослать сообщение об ошибке и закончить работу srv_sendmsg(srvproc, SRVJ1SGJRR0R, LISTFILEJRROR, SRVJNF0, (DBTINYINT) 0, NULL, 0, 0. "Ошибка при выполнении расширенной хранимой процедуры: невозможно открыть файл.", SRV_NULLTERM); // SRV_D0NE_M0RE вместо SRV_DONE_FINAL должно завершать результирующий набор srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0, 0); return (XPJRR0R); } // ШАГ 4: Вернуть количество обработанных строк - srv_senddone(srvproc, SRV_D0NE_M0RE | SRV_D0NE_C0UNT, (DBUSMALLINT)O, i): return XPJJ0ERR0R ; } Код состоит из четырех этапов. Изучите комментарии в коде, указывающие на то, где завершается один этап и начинается другой.
Простой пример 471 На этапе 1 происходит обработка параметров, переданных процедуре. Если получено неверное число параметров или они не того типа, который требуется, клиенту отсылается сообщение об ошибке и выполнение процедуры завершается. На этапе 2 устанавливаются имена столбцов для результирующего набора данных. Как было сказано ранее, это делается в ODS при помощи вызова функции srv_describe(). Как только имена колонок заданы, можно начинать записывать в них данные и отсылать клиенту. На этапе 3 происходит перемещение по текстовому файлу, загрузка каждой его строки в запись результирующего набора данных и отсылка записи клиенту. Также с каждой записью отсылается номер строки для того, чтобы упростить процесс упорядочивания. На этапе 4 возвращается общее число отосланных клиенту строк. Все, что требуется здесь сделать, — отослать конечное значение переменной, которое увеличивалось по мере обработки текстового файла. После того как процедура скомпилирована и скомпонована, в каталоге BINN следует разместить библиотеку динамической компоновки. Теперь можно добавить процедуру в базу master при помощи вызова системной хранимой процедуры sp_addextendedproc, как показано в листинге 20.7. Листинг 20.7. Добавление расширенной хранимой процедуры USE master GO EXEC sp_addextendedproc 'xpj i stfi1e'.'xpj i stfi1 e.dl1' После этого процедуру можно вызывать из Transact-SQL, как любую другую процедуру. EXEC master.dbo.xpJistfile 'C:\WINNT\sqlstp.log' (Результаты сокращены) LineNo Text 1 23:44:44 Begin Setup 2 23:44:44 8.00.194 3 23:44:44 Mode = Normal 4 23:44:44 ModeType = NORMAL 5 23:44:44 GetDefinitionEx returned: 0. Extended: 0x0 6 23:44:44 ValueFTS returned: 1 7 23:44:44 ValuePID returned: 1 8 23:44:44 ValueLic returned: 0 9 23:44:44 System: Windows NT Workstation 10 23:44:44 SQL Server ProductType: Personal Edition [0x2] 11 23:44:44 Begin Action: Setuplmtialize 12 23:44:44 End Action Setuplnitialize 13 23:44:44 Begin Action: Setuplnstall 14 23:44:44 Reading Software\Microsoft\Windows\... 15 23:44:44 CommonFilesDir=C:\Program Files\Common Files 16 23:44:44 Windows Directory=C:\WINNT\ 17 23:44:44 Program Files=C:\Program Files\ 18 23:44:44 TEMPDIR=C:\WINNT\TEMP\ 19 23:44:44 Begin Action 20 23:44:44 digpid size 21 23:44:45 digpid size 22 23:44:45 Begin Action Setuplnstall 256 164 CheckFixedRequirements
472 Глава 20. Расширенные хранимые процедуры 23 23:44:45 Platform ID: OxfOOOOOO 24 23:44:45 Version: 5.0.2195 25 23:44:45 File Version - C:\WINNT\System32\shdocvw.dll: 5.0.2920.0 26 23:44:45 End Action: CheckFixedRequirements 27 23:44:45 Begin Action: ShowDialogs Удалить процедуру так же просто, как и добавить. Необходимо лишь вызвать системную хранимую процедуру sp_dropextendedproc (листинг 20.8). Листинг 20.8. Удаление расширенной хранимой процедуры USE master GO EXEC sp_dropextendedproc 'xpjistfile' Если расширенную хранимую процедуру требуется заменить после того, как она была загружена SQL Server, то сначала необходимо выгрузить библиотеку динамической компоновки, содержащую реализацию расширенной хранимой процедуры, при помощи команды DBCC d I I name(FREE), как показано в листинге 20.9. Листинг 20.9. Выгрузка библиотеки динамической компоновки расширенной хранимой процедуры USE master GO DBCC xpJistfile(FREE) ПРИМЕЧАНИЕ Недокументированная процедура xp_readerrorlog также способна читать текстовые файлы (не только журнал ошибок) и возвращать содержимое файла в виде результирующего набора данных. Более подробно о ней рассказывается в главе 22. Сложный пример После того как вы увидели, насколько легко создание расширенной хранимой процедуры, давайте создадим более сложную процедуру. Код в листинге 20.10 представляет собой реализацию расширенной хранимой процедуры хр_ехес. Как было сказано в главе про пользовательские функции (UDF), из определяемой пользователем функции нельзя выполнить оператор ЕХЕС(). Выполнить можно лишь расширенные хранимые процедуры, имя которых начинается с хр (вызвать sp_executesq I нельзя: хотя это расширенная хранимая процедура, ее имя начинается не с хр). Процедура хр_ехес позволяет решить эту проблему. Вы передаете процедуре запрос, который необходимо выполнить, и она возвращает результирующий набор, если таковой имеется. Этот набор невозможно сохранить в таблице-переменной (оператор I NSERT...EXEC не работает с таблицами-переменными), но, используя его, можно работать с таблицами и данными, решать административные задачи и выполнять большинство дел, которые можно сделать при помощи Transact-SQL. Так как процедура хр_ехес должна выполнять запросы вне соединения, в котором вызвана, ей придется открыть свое собственное соединение (через ODBC) с сервером. Для этого она будет использовать данные, которые получит из вызывающего ее кода. Ключевыми функциями здесь являются SQLAI locHandle() и SQLConnect(). Вот исходный код.
Сложный пример 473 Листинг 20.10. Соединение из ODBC // ШАГ 1: Выделить дескриптор окружения ODBC sret = SQLA11ocHandle(SQL_HANDLE_ENV. NULL. Shenv); if (sret != SQL_SUCCESS) { handle_odbc_err("SQLAllocHandle:Env". sret. (DBINT) REMOTEJAIL. henv. SQL_HANDLE_ENV. srvproc): return(XPJRROR); } SQLSetEnvAttr(henv. SQL_ATTR_ODBCJERSION. (SQLPOINTER) SQL_0V_0DBC3. SQL_IS_INTEGER): // ШАГ 2: Выделить дескриптор соединения ODBC sret = SQLAllocHandle(SQL_HANDLE_DBC. henv. &hdbc): if (sret != SQLJUCCESS) { handle_odbc_err("SQLAllocHandle:Dbc", sret. (DBINT) REMOTEJAIL. henv. SQL_HANDLE_ENV. srvproc); SQLFreeHandle(SQL_HANDLE_ENV. henv); return(XPJRROR); } // ШАГ З: Проверить тип аутентификации if (strcmp(srv_pfield(srvproc. SRV_LSECURE. (int *)NULL). "TRUE") == 0) { // Клиент соединен при помощи Windows Authentication // Имперсонализировать клиента и установить опцию SQL_INTEGRATED_SECURITY blmpersonated = srv_impersonate_client(srvproc); // Соединиться с DSN. используя Windows Authentication SQLSetConnectAttr(hdbc. SQL_INTEGRATED_SECURITY. (SQLPOINTER) SQLJSJN. SQLJSJNTEGER): _tcscpy(acUID, _T("")): _tcscpy(acPWD. _T("")): } else { // Клиент соединен при помощи SQL Server Authentication. Установить имя учетной записи и пароль. #ifdef UNICODE MultiByteToWideChar(CP_ACP. 0. srv_pfield(srvproc. SRV_USER. NULL). -1. acUID. MAXNAME); MultiByteToWideChar(CP_ACP. 0. srv_pfield(srvproc. SRV_PWD. NULL). -1. acPWD. MAXNAME); #else strncpy(acUID. srv_pfield(srvproc. SRV_USER, NULL). MAXNAME); strncpy(acPWD, srv_pfield(srvproc. SRV_PWD, NULL). MAXNAME); fend if } // ШАГ 4: Соединиться if (!SQL_SUCCEEDED( sret = SQLConnect(hdbc. (SQLTCHAR*) szDSN. SQL_NTS. (SQLTCHAR*) acUID, SQL_NTS. (SQLTCHAR*) acPWD, SQLJTS) )) { продолжение &
474 Глава 20. Расширенные хранимые процедуры Листинг 20.10 {продолжение) hand1e_odbc_err("SQLConnect", sret. (DBINT)REMOTE_FAIL. hdbc, SQL_HANDLE_DBC. srvproc): goto SAFEJXIT; } Как вы видите, код разделен на четыре этапа. Первый этап: получение дескриптора окружения ODBC. Позже он будет использован для получения дескриптора соединения ODBC. Как только получен дескриптор соединения, проверяется тип аутентификации, используемый соединением, который вызывает расширенную хранимую процедуру, и выставляется такой же тип аутентификации для создаваемого соединения. В конце дескриптор соединения используется для открытия соединения к указанному источнику ODBC DSN. При этом процедура хр_ехес предполагает, что создан системный источник DSN с именем LocalServer, которое при необходимости можно изменить. После того как соединение установлено, можно выполнять пакеты SQL-команд. Очевидно, что создание нового соединения в рамках процедуры хр_ехес немного усложняет дело, так как это означает, что блокировки, которые держит код, вызвавший хр_ехес, будут блокировать выполнение хр_ехес. Чтобы избежать этого, следует вызвать процедуру sp_b i ndsess i on из хр_ехес для присоединения к блоку транзакций вызвавшего хр_ехес кода. Это освобождает хр_ехес от блокирования транзакциями, порожденными кодом, который вызвал процедуру. Для того чтобы процедура использовала присоединение к транзакции вызвавшего кода, необходимо вторым параметром передать значение Y. Исходный код приведен в листинге 20.11. Листинг 20.11. Присоединение к блоку транзакций вызвавшего кода // ШАГ 1: Получить иаркер клиентской сессии if ((szShareTran[0]=='Y') || (szShareTran[0]=='y')) { re = srv_getbindtoken(srvproc, acBindToken): if (re == FAIL) { s rv_sendmsg(s rvproc. SRV_MSG_ERROR. EXECSQLJRROR, SRVJNFO, (DBTINYINT) 0, NULL, 0. 0, "Ошибка при вызове srv_getbindtoken". SRV_NULLTERM); srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0); return(XP_ERR0R); } // ШАГ 2: Привязать его как параметр процедуры _tcscpy(szQuery. _T("{call sp_bindsessionC?)}")): sret = SQLBindPararaeter(hstrat. 1. SQL_PARAM_INPUT. SQL_C_CHAR. SQLJ/ARCHAR. 255. 0, acBindToken. 256. NULL); if (sret != SQLJUCCESS) { handle_odbc_err("SQLBindParameter".
Сложный пример 475 sret. (DBINT)REMOTE_FAIL. hstmt, SQLJANDLEJTMT. srvproc); return (XPJRROR); } // ШАГ З: Присоединить нашу сессию к сессии клиента sret = SQLExecD1rect(hstmt. (SQLTCHAR*) szQuery. SQLJITS); if (!((sret == SQLJUCCESS) | j (sret == SQL_SUCCESS_WITH_INFO))) { handle_odbc_err("SQLExecDirect". sret. (DBINT) EXECSQLJRROR. hstmt, SQL_HANDLE_STMT. srvproc): return(XP_ERROR): } } Я разделил этот код на три этапа. Во-первых, получаем маркер сессии для текущего соединения клиента, вызвав srv_getb i ndtoken(). Далее используем это значение как ODBC-параметр процедуры, чтобы передать его процедуре sp_b i ndsess i on. Наконец, вызываем sp_b i ndsess i on и передаем маркер сессии из вызывавшего соединения. Это приводит к тому, что вызывавшее соединение и наше новое ODBC- соединение работают в рамках одной транзакции. Для проверки можно начать транзакцию в вызвавшем хр_ехес коде и проверить значение @@TRANCOUNT в хр_ехес: BEGIN TRAN PRINT 'Присоединяется к транзакции вызвавшего' EXEC master..xp_exec 'SELECT OOTRANCOUNT','Y'.'master' PRINT 'Выполняется независимо' EXEC master..xp_exec 'SELECT aaTRANCOUNT'•'N'.'master' GO ROLLBACK (Результаты сокращены) Присоединяется к транзакции вызвавшего 1 Выполняется независимо О Третий параметр процедуры хр_ехес указывает контекст базы, в котором будет исполнен запрос. Это необязательный параметр. В предыдущем примере в качестве целевого контекста была указана база maste г. После того как вы определитесь, требуется ли присоединяться к транзакции кода, вызвавшего процедуру, можно выполнять запрос. Код выглядит так. Листинг 20.12. Использование ODBC для выполнения запроса // ШАГ 4: Выполнить запрос _tcscpy(szQuery. szTSQL); sret = SQLExecDirect(hstmt. (SQLTCHAR*) szQuery. SQLJITS): if (!((sret == SQL_SUCCESS)||(sret == SQL_NO_DATA))) { handle_odbc_err("SQLExecDirect". продолжение &
476 Глава 20. Расширенные хранимые процедуры Листинг 20.12 {продолжение) sret, (DBINT) EXECSQLJRROR. hstmt. SQL_HANDLE_STMT, srvproc); return(XPJRROR); } SQLExecD i rect () является здесь ключевой функцией. Ей передается дескриптор выражения (полученный ранее); запрос, который необходимо выполнить; и тип строки (завершенной нулем), содержащей запрос. Так как процедуре хр_ехес неизвестен тип результирующего набора данных, который может быть получен, хр_ехес делает несколько вызовов ODS-функций для того, чтобы получить описание результирующего набора данных. Ключевая функция здесь SQLCo I Att г i bte(). Вот исходный код. Листинг 20.13. Использование вызовов ODBC для интерпретации результирующего набора for (nCol = 0: nCol < nCols; nCol++) { "// Получить имя, длину и тип данных колонки SQLColAttrlbuteChstmt. (SQLSMALLINT) (nCol + 1). SQL_DESC_NAME. (SQLTCHAR*) acColumnName. // полученное имя колонки MAXNAME, // максимальная длина буфера rgbDesc ScbAttr. // число байт, полученное в rgbDesc SiNumAttr); SQLColAttribute(hstmt. (SQLSMALLINT) (nCol + 1), SQL_DESC_OCTET_LENGTH. NULL. 0. NULL. ScbColData); // Получить тип данных колонки и установить длину полученных данных SQLColAttrlbuteChstmt. (SQLSMALLINT) (nCol + 1), SQL_CA_SS_C0LUMN_SSTYPE, NULL. 0. NULL. SeSQLType); // Перезаписать длину колонки, полученную из ODBC // правильным значением, которое будет использовано DDS switch( eSQLType ) { case SQLM0NEYN: case SQLM0NEY: cbColData = sizeof(DBMONEY); break; case SQLDATETIMN: case SQLDATETIME: CbColData = sizeof(DBDATETIME): break; case SQLNUMERIC: case SQLDECIMAL: cbColData = sizeof(DBNUMERIC);
Сложный пример 477 break; ' case SQLM0NEY4: cbColData = sizeof(DBM0NEY4); break: case SQLDATETIM4: //smalldatetime CbColData = sizeof(DBDATETIM4): break: } // ... Как только колонки, которые входят в результирующий набор данных, становятся известны, можно отсылать данные клиенту. Как и в процедуре хр_ I i stf i I e, для установки свойств каждой колонки результирующего набора вызывается функция srv_describe() и функция srv_sendrow() — для отправки каждой строки обратно клиенту. Листинг 20.14. Как только результирующий набор данных получен, его можно передавать клиенту при помощи ODS // Выделить память для данных строки if ((ppData[nCo1] = (PBYTE) malloc(cbColData)) == NULL) goto SAFEJXIT: memset(ppData[nCol]. 0. cbColData): // Привязать колонку SQLBindCoKhstmt. (SQLSMALLINT) (nCol + 1). SQL_C_BINARY. // Нет преобразования данных ppData[nCol]. cbColData. &(pIndicators[nCol])); // Подготовить структуру, которая будет отослана через 0DS // вызвавшему расширенную хранимую процедуру srv_descri be(srvproc, nCol + 1. acColumnName. SRVJULLTERM. eSQLType, // Целевой тип данных (DBINT) CbColData.// Целевая длина данных eSQLType. // Исходный тип данных (DBINT) CbColData, // Исходная длина данных (PBYTE) NULL); } // Инициализировать счетчик строк rows = 0; // Получать каждую строку данных из ODBC, пока они не закончатся whileUsret = SQLFetch(hstmt)) != SQL_N0_DATA_F0UND) { if (!SQL_SUCCEEDED(sret)) { handle_odbc_err("SQLFetch". sret. (DBINT) EXECSQLJRROR, hstmt. SQL_HANDLE_STMT. srvproc); goto SAFEJXIT: } // Для каждого поля данных в текущей строке // заполнить структуру, которая будет отослана // вызвавшему расширенную хранимую процедуру for (nCol = 0: nCol < nCols: nCol++) { продолжение &
478 Глава 20. Расширенные хранимые процедуры Листинг 20.14 (продолжение) cbColData = (pIndicators[nCol] == SQL_NULL_DATA ? О : plndlcators[nCol]); srv_setcollen(srvproc. nCol+1, (int) cbColData): srv_setcoldata(srvproc. nCol+1. ppDatafnCol]); } // Отослать строку данных SQL Server через ODS if (srv_sendrow(srvproc) == SUCCEED) rows++: } Как вы видите, создать даже среднюю по сложности расширенную хранимую процедуру достаточно просто. Как сделать расширенные хранимые процедуры проще Для того чтобы сделать расширенную хранимую процедуру проще, ее следует «обернуть» в системную процедуру. Это позволит вызывать расширенную хранимую процедуру из контекста любой базы данных, не прибегая к указанию имени базы maste г... Большое количество расширенных хранимых процедур самого SQL Server «обернуты» в системные хранимые процедуры. Вот пример, использующий недокументированную процедуру xp_varbintohexstr. Листинг 20.15. «Обертка» расширенных хранимых процедур для упрощенного вызова USE master IF (OBJECTJDCdbo.spjiexstring') IS NOT NULL) DROP PROC dbo.spjiexstring GO CREATE PROC dbo.spjnexstring @int varcharA0)=NULL. @hexstring varcharC0)=NULL OUT /* Объект: sp_hexstring Описание: Возвращает целое в виде шестнадцатеричной строки Использование: spjiexstring @iп!=целое число для преобразования, @hexstring=OUTPUT параметр для получения шестнадцатеричной строки Возвращает: (ничего) Автор: Ken Henderson. Email: khen@khen.com Версия: 1.0 Пример использования: sp_hexstring 23. @myhex OUT Создана: 1999-08-02. Последнее изменение: 1999-08-15. */ AS IF (@Ш IS NULL) OR ((Pint = '/?') GOTO Help DECLARE @i int. @vb varbinaryC0) SELECT @i=CAST(@int as int). @vb=CAST(@i as varbinary) EXEC master..xp_varbintohexstr @vb. Phexstring OUT RETURN 0 Help: EXEC sp_usage @objectname='spjiexstring', @desc='Возвращает целое в виде шестнадцатеричной строки'. @parameters='@int=ueroe число для преобразования, Ohexstring=0UTPUT параметр для получения шестнадцатеричной строки', @example='spjiexstring 3", йпупех OUT'.
Отладка расширенных хранимых процедур 47S @author='Ken Henderson'. @email='khen@khen.com'. @version=T , @revision='0'. @datecreated='19990802'. @datelastchanged='19990815' RETURN -1 GO DECLARE @hex varcharC0) EXEC sp_hexstring 10. @hex OUT SELECT @hex (Результаты) OxOOOOOOOA Процедура sp_hexst r i ng проверяет параметры, которые будут переданы расширенной хранимой процедуре xp_varbintohexstr до того, как вызвать ее. Так как sp_hexst г i ng является системной процедурой, ее можно вызвать из любой базы без прямой ссылки на расширенную хранимую процедуру. Отладка расширенных хранимых процедур Отладка расширенных хранимых процедур идентична отладке любого кода, размещенного в библиотеке динамической компоновки (DLL): используется приложение, в которое загружается библиотека, и выставляется точка прерывания в коде, который необходимо отладить. Для расширенных хранимых процедур приложением является SQL Server. Для отладки расширенных хранимых процедур нельзя использовать службу SQL Server, следует использовать его консольную версию. Как запустить SQL Server как консольное приложение? Во-первых, следует остановить службу SQL Server, затем запустить sqlservr.exe (в каталоге BINN) с ключом -с. Также, если требуется запустить именованный экземпляр SQL Server, следует указать ключ -s и имя экземпляра, который необходимо запустить. ПРИМЕЧАНИЕ В этом параграфе подразумевается, что для разработки и отладки расширенных хранимых процедур используется Microsoft Visual C++. Если используются другие средства, то необходимо настроить их так, чтобы происходила отладка DLL в приложении sqlservr.exe. Большинство современных средств разработки для Windows позволяют отлаживать библиотеки и указывать произвольные приложения, в рамках которых происходит отладка. В настройках используемой среды разработки SQL Server следует указать как приложение, в рамках которого происходит отладка библиотеки. Если вы используете Visual C++, найдите поле Executable for debug session на вкладке Debug окна диалога Project Settings. Наберите в этом поле полный путь к sq I s ervr. ехе вместе с ключом -с. Если необходимо, добавьте ключ -s. Выставите точку прерывания в коде расширенной хранимой процедуры и после этого запустите приложение. SQL Server запустится в консольном окне. После того как пройдет процедура запуска (это можно определить по появлению сообщения Recovery Complete в консольном окне), откройте Query Analyzer и запустите
480 Глава 20. Расширенные хранимые процедуры расширенную хранимую процедуру. Выполнение должно остановиться на указанной ранее точке прерывания в Visual C++. Начиная с этой точки, можно отлаживать расширенную хранимую процедуру так же, как и любой другой код. Можно выполнять код по этапам, перемещаться в подпрограммы, отслеживать значения переменных и в любой момент остановить сессию отладки. ПРИМЕЧАНИЕ В случае отладки на именованном экземпляре SQL Server необходимо использовать параметр -s, дополненный именем экземпляра (имя компьютера указывать не следует). Если этого не сделать, появится ложное сообщение об ошибке, гласящее, что инсталляция SQL Server повреждена. Если подобное сообщение появляется при запуске SQL Server в виде консольного приложения, следует проверить, что ключ -s указан корректно. Изоляция расширенных хранимых процедур Так как расширенные хранимые процедуры выполняются в адресном пространстве SQL Server, они могут нарушить работу сервера, если будут вести себя некорректно. В частности, возможны утечки памяти, иногда полностью исчерпывающие пул MemToLeave. Память SQL Server состоит из двух основных пулов: пула буфера, из которого происходит выделение памяти для данных, процедурных кэшей и большинства других случаев, и пула MemToLeave, размер которого по умолчанию равен 256 Мбайт в SQL Server 2000 A28 Мбайт в SQL Server 7.0). Выделение памяти для расширенных хранимых процедур и для внутрипроцессных СОМ- объектов (например, OLE-DB) происходит именно из пула MemToLeave. Размер пула MemToLeave можно указать при помощи ключа -д. Если расширенная хранимая процедура, приводящая к утечке памяти, вызывается часто, она постепенно исчерпает весь пул MemToLeave. Когда это произойдет, сервер, скорее всего, прекратит работу или будет работать нестабильно. В журнале ошибок сервера появятся записи об ошибках из серии 17800 и, возможно, 8645. В этом случае единственный выход из сложившейся ситуации — перезапустить SQL Server. Если ошибка в расширенной хранимой процедуре не будет исправлена, проблема будет повторяться, и SQL Server придется периодически останавливать и запускать для того, чтобы сохранить скорость работы на приемлемом уровне. А можно ли изолировать расширенные хранимые процедуры таким образом, чтобы они выполнялись вне процесса SQL Server, подобно внепроцессным СОМ- серверам? Возможно. Метод очень прост: создается именованный экземпляр SQL Server, единственное предназначение которого — выполнение расширенных хранимых процедур. Вот как можно это сделать. 1. Убедитесь, что серверный компьютер обладает большим запасом оперативной памяти. 2. На том же компьютере, где установлен главный экземпляр SQL Server 2000 или более поздней версии, установите именованный экземпляр, исключительной задачей которого будет выполнение расширенных хранимых процедур. 3. Зарегистрируйте расширенные хранимые процедуры в этом именованном экземпляре, а не в главном.
Изоляция расширенных хранимых процедур 481 4. Создайте системные хранимые процедуры на главном экземпляре, которые вызывают расширенные хранимые процедуры из именованного экземпляра, используя полные имена из четырех частей. 5. При кодировании приложений используйте созданные ранее системные хранимые процедуры, вместо расширенных. Использование данного метода позволяет изолировать расширенные хранимые процедуры от рабочего сервера. В случае если какая-либо расширенная хранимая процедура работает неверно, функционирование главного сервера не нарушится. Помните, что можно установить размер пула MemToLeave при помощи ключа командной строки -д на экземпляре сервера, отведенном для выполнения расширенных хранимых процедур. Возможно, вы захотите увеличить это значение по сравнению с размером на главном сервере. xp_setpriority В заключение этой главы я расскажу о расширенной процедуре, которая, по моему мнению, может изменить стиль вашей работы, особенно если вы управляете инсталляцией SQL Server, в которой долго выполняющиеся пакетные задания и более короткие, критические по времени исполнения, задания периодически борются за системные ресурсы. Ранее было сказано, что расширенные хранимые процедуры выполняются в процессе SQL Server. Они также выполняются в контексте вызвавшего их потока. Это означает, что каждое соединение получает свой Win32- поток, если только на сервере не включен режим волокон. Этот поток берется из пула рабочих потоков, который создается при запуске сервера. Когда расширенная хранимая процедура выполняется, она работает в контексте потока. Это означает, что она имеет полный доступ к потоку и может изменить определенные характеристики потока, если требуется. Любые изменения свойств потока, сделанные расширенной хранимой процедурой, сохранятся и после завершения выполнения процедуры. Процедура xp_setp г i о г i ty использует этот нюанс для изменения приоритета потока соединения. Так как потоки создаются сервером с максимально высоким для своего класса потоков приоритетом, то приоритет потока соединений невозможно повысить, но можно понизить. Зачем понижать приоритет потока соединения? Зачем замедлять выполнение задания? Ответ прост: понизить приоритет потока соединения необходимо для того, чтобы позволить другим параллельно выполняющимся заданиям завершиться быстрее. Это означает, что если есть пакетное задание, которое обычно выполняется несколько часов и часто не завершается до середины ночи, замедление его выполнения для ускорения завершения других критических по времени выполнения заданий может быть полезно. Если эти задания обычно выполняются параллельно, небольшое замедление выполнения пакетного задания ускорит выполнение других заданий, возможно, очень существенно. Давайте начнем с исходного кода xp_setpr i or i ty. Он не очень велик. RETCODE xp_setpriority(srvproc) SRV_PR0C *srvproc; {
482 Глава 20. Расширенные хранимые процедуры int threadpriority = THREAD_PRIORITY_TIME_CRITICAL; int nParams; DBINT paramtype; TCHAR szPriority[20] = "": BYTE pbType; ULONG pcbMaxLen; ULONG pcbActualLen: BOOL pfNul1; RETCODE rcXP = XP_ERROR; // Подразумеваем неудачное завершение // Получить число параметров nParams = srv_rpcparams(srvproc); // Проверить число параметров if (nParams != 1) { // Отослать сообщение об ошибке и завершить работу srv_sendmsg(srvproc. SRV_MSG_ERROR, XP_SETPRIORITY_ERROR. SRVJNFO, (DBTINYINT)O. NULL, 0. О, "Ошибка при выполнении расширенной хранимой процедуры: неверное число параметров,". SRVJULLTERM): // Вместо SRVJ0NEJINAL следует использовать SRV_D0NE_M0RE srv_senddone(srvproc. (SRVJ0NEJRR0R | SRVJ0NEJI0RE). 0. 0): return (XPJRROR); } // Отослать сообщение об ошибке и завершить работу, если параметр имеет тип // не varchar (должен иметь значение HIGHEST/LOWEST и т.п.) paramtype = srv_paramtype(srvproc. 1): if (paramtype != SRVVARCHAR) { srv_sendmsg(srvproc, SRVJSGJRROR, XP_SETPRI0RITY_ERR0R, SRVJNFO, (DBTINYINT)O, NULL, 0. 0, "Ошибка при выполнении расширенной хранимой процедуры: неверный тип параметра SRVJULLTERM): // Вместо SRVJONEJINAL следует использовать SRVJONEJI0RE srv_senddone(srvproc, (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0): returrK XPJRROR); // Завершить строку параметра нулем srv_paraminfo(srvproc.l,&pbType. SpcbMaxLen, SpcbActualLen. szPriority. SpfNull): szPriority[pcbActualLen] = '\0'; if (stricmp(szPriority,"HIGHEST")==0) threadpriority=THREADJRIORITYJIMEJRITICAL; else if (stricmp(szPriority,"HIGH")==0) threadpriority=THREADJRIORITY_ABOVEJORMAL; else if (stricmp(szPriority."LOW")==0) threadprionty=THREADJRIORITYJELOW_NORMAL; else if (stricmp(szPriority."LOWEST")==0) threadpri on ty=THREAD JRIORITY JOWEST; else if (stricmp(szPriority."NORMAL")==0)
Изоляция расширенных хранимых процедур 483 { threadpriority=THREAD_PRIORITY_NORMAL: } SetThreadPriority(GetCurrentThread().threadpriority); srv_senddone(srvproc. SRV_D0NE_MORE. (DBUSMALLINT)O. (DBINT)O); // Дошли до этого места без ошибок - сообщим об этом клиенту return XP_N0ERR0R ; } Помимо обычного кода для обработки параметров, в процедуре xp_setpг i or i ty есть одна действительно очень важная строка кода — вызов Windows API SetTh readPr i or i ty (). Этот вызов содержит в себе вызов функции GetCur rentTh read () для получения дескриптора потока, обслуживающего активное соединение. Вызов SetTh readPr i or i ty () устанавливает указанный пользователем приоритет потока. Процедура поддерживает пять уровней приоритета: HIGHEST, HIGH, NORMAL, LOW и LOWEST. Все они относятся к базовому классу потока. Для того чтобы понять практическую пользу этой процедуры, давайте рассмотрим некоторый код. Вам потребуется SQL Server с небольшой или отсутствующей загрузкой для того, чтобы следующие тесты были выразительными. Начните с копирования файлаxp_setpriority.dll в каталог BINN вашего экземпляра SQLServer (обратите внимание на то, что каталог BINN должен принадлежать экземпляру, а не SQL Server Tools). После того как библиотека скопирована, запустите код для того, чтобы зарегистрировать xp_setpnority на сервере. USE master GO EXEC sp_addextendedproc 'xp_setpriority'. 'xp_setpnority.dll' GO После того как расширенная хранимая процедура будет зарегистрирована, загрузите следующий запрос в Query Analyzer и выполните его один раз. Этот запрос запускает ресурсоемкую по отношению к процессору операцию (цикл от единицы до миллиона) и завершается. Запустите запрос и замерьте время его выполнения (Query Analyzer отображает время выполнения в нижней части каждого окна с запросом). SELECT GETDATEO GO --EXEC master..xp_setpriority 'LOWEST' DECLARE @i int SET @i=0 WHILE @i<1000000 BEGIN SET @i=@i+l END EXEC master..xp_setpriority 'NORMAL' GO SELECT GETDATEO На стареньком компьютере, на котором я пишу эту книгу, данный запрос выполняется примерно за 23 секунды. Теперь откройте второе окно в Query Analyzer и загрузите в него тот же самый запрос. После этого выполните два запроса параллельно. Это покажет, насколько сильно конкуренция за ресурсы процессора влияет на время выполнения запроса. На моем компьютере каждая копия запроса выполняется около 42 секунд, то есть
484 Глава 20. Расширенные хранимые процедуры время выполнения увеличилось почти на 100 %. Это обосновано. Как-никак ресурсы процессора ограничены. Вот теперь спросите себя: «А если изменить приоритет выполнения одного из заданий так, чтобы другое могло выполниться быстрее? Что если только одно из заданий является критическим по времени выполнения? Что если снижение скорости выполнения одного задания для ускорения выполнения другого может помочь делу?» Снизить скорость выполнения одного задания для того, чтобы выполнение другого могло завершиться быстрее, легко сделать при помощи процедуры xp_setpr iority. Просто уберите комментарий перед ее вызовом в одном из запросов в окне Query Analyzer и запустите оба запроса. На моем компьютере запрос, работающий с приоритетом NORMAL, теперь выполняется за 23 секунды, несмотря на то, что другой запрос продолжает бороться с ним за ресурсы процессора. Это означает, что теперь первый запрос, запущенный параллельно со вторым, выполняется за то же время, за которое он и выполнялся. Происходит это потому, что приоритет потока первого запроса выше приоритета потока запроса, который вызвал xp_set p г iority. Мы действенно ускорили выполнение первого запроса примерно на 50 %, просто ослабив его борьбу за ресурсы с другим запросом. Однако следует отметить, что второй запрос, чей приоритет не NORMAL, выполняется медленнее всего лишь на несколько секунд. На моем компьютере он выполняется за 58 секунд. Это значит, что 33-процентное снижение производительности одного запроса привело к увеличению производительности другого запроса на 50 %, что весьма выгодно. Позволяя запросу с более высоким приоритетом выполниться быстрее, второй запрос прерывается меньше, чем в случае, когда они оба жестко спорят за процессорные ресурсы. Заметьте, что приоритет потока, обслуживающего запрос, необязательно должен быть постоянным. Вы можете повышать его до нормального или снова понижать, когда это требуется, во время выполнения запроса. Кроме того, можно «обернуть» критические участки кода вызовами, которые мгновенно повысят приоритет потока до нормального для выполнения запроса, а потом снова понизят его. Таким образом, критические участки кода получат все возможные процессорные ресурсы. Используя xp_setpr i or i ty, вы устанавливаете полный контроль SQL ServerHafl временем выполнения потока. Полезной функцией в следующей версии SQL Server могла бы стать возможность задавать приоритет выполнения для соединения средствами Transact-SQL Законно, что администраторы и разработчики захотят иметь возможность настраивать приоритет выполнения отдельных соединений вместо того, чтобы изменять одну и ту же настройку для всех соединений. До тех пор, пока эта функция не включена в SQL Server, можно использовать расширенную хранимую процедуру xp_setpr i or i ty для достижения того же результата. Если у вас есть долго выполняющиеся пакетные запросы (время выполнения которых вас не беспокоит), подумайте о снижении приоритета их потока для того, чтобы более важные задания могли выполняться быстрее. Вы даже можете разделить своих пользователей на классы согласно требованиям бизнеса и установить для них приоритеты — чтобы некоторые классы смогли быстрее выполнять запросы с более высоким приоритетом. При помощи изменения приоритета потоков, обслуживающих соединения SQL Server, можно также увеличить общую производительность сервера.
Итоги 485 ВНИМАНИЕ Перед тем как использовать процедуру xp_setpriority, внимательно прочитайте это предупреждение. Это настолько важно, что я удалил почти все описание процедуры xp_setpriority из книги, потому что неправильное использование этой процедуры может привести к возникновению серьезных проблем. Как большинство мощных инструментов, xp_setpriority может причинить вред при неверном использовании. Пожалуйста, прочитайте это предупреждение от начала до конца и осмыслите его перед тем, как начнете использовать процедуру xp_setpriority на рабочем сервере. Так как SQL Server постоянно чередует рабочие потоки, обслуживающие пользовательские соединения, возможно (и вероятность этого очень высока), что изменения в потоке, вызванные xp_setpriority, затронут другие соединения. Однажды мне довелось увидеть в кафе самообслуживания табличку: «Ваша мама не работает здесь. Пожалуйста, уберите за собой сами». Чрезвычайно важно отменить все изменения, которые коснулись контекста потока до того, как выполнение запроса завершится. Это означает, что последнее выражение в любом пакете, использующем процедуру xp_setpriority, должно выглядеть так: EXEC master..xp_setpriority 'NORMAL' Это гарантирует, что другое соединение не станет наследовать сделанные вами временные изменения приоритета потока. Вам также захочется быть уверенными в том, что ошибки, которые могут быть допущены до заключительного вызова xp_setpriority, будут корректно обработаны, а не проигнорированы, чтобы заключительный вызов xp_setpriority не был пропущен. Некорректное восстановление приоритета потока после его снижения может вызвать, например, снижение производительности случайно выбранных соединений в вашей системе, чего вы точно не хотите и что почти невозможно будет исправить. Для того чтобы посмотреть, как это работает, выполните несколько раз подряд следующий запрос на сильно загруженном сервере: SELECT kpid FROM master, .sysprocesses WHERE spiel = @@spid Значения поля kpid таблицы sysprocesses представляет собой \Мп32-идентификатор потока, обслуживающего соединение. Если выполнить запрос несколько раз подряд на сильно загруженном сервере, то можно заметить изменения идентификатора потока. Поток, использованный данным соединением ранее, может быть выделен для использования другим соединением, а поток, используемый данным соединением, может быть задействован еще одним соединением. Вот почему так важно производить высвобождение ресурсов по окончании своей работы. Так как SQL Server использует пул потоков, то внесенные в поток изменения могут повлиять на другие соединения. Итоги В этой главе вы узнали: ■ как создавать и использовать расширенные хранимые процедуры; ■ что такое ODS API и работа механизмов управления памятью и потоками в SQL Server; ■ что при правильном применении расширенные хранимые процедуры могут предоставить вам широкие возможности, но при неразумном применении они могут вывести сервер из строя.
Хранимые процедуры для администрирования Управлять многим — то же самое, что управлять малым. Это вопрос организации. Сан Цзу* В этой главе мы рассмотрим построение хранимых процедур, предназначенных для задач администрирования. Хотя SQL Server содержит большое количество документированных и недокументированных хранимых процедур, всегда возникает потребность в чем-то новом. Этому и посвящена данная глава. В ней описываются некоторые приемы, которые вы можете использовать для написания кода собственных административных процедур. В процедурах, рассмотренных в этой главе, я продемонстрировал методики, которые вы можете применить в своей работе и создать полезный для вас код. Некоторые из рассмотренных процедур построены на основе других. Например, sp_generate_script использует sp_readtextfile, a sp_diffdb — sp_generate_script и sp_diff. Строение многих процедур аналогично. Изучите их, запустите самостоятельно и убедитесь, что если у вас возникнут проблемы — вы (на основе процедур) сможете написать свой код. Воспользуйтесь каким-либо приемом из представленных мною и разработайте новую процедуру, которая сделает вашу жизнь профессионального разработчика SQL Server намного легче. sp_readtextfile Процедура sp_readtextf ile позволяет прочесть текстовый файл на машине с установленным SQL Server (или на другой доступной ей машине) и либо вернуть его как набор данных, либо сохранить первые 8000 байтов в выходном параметре. Код представлен в листинге 21.1. Листинг 21.1. Процедура sp_readtextfile USE master GO IF OBJECT__ID('sp_readtextfile') IS NOT NULL Tzu, Sun. The Art of War. Cambridge, England: Oxford University Press, 1963. С 90. 21
sp_readtextfile 487 DROP PROC sp__readtextfile GO CREATE PROC sp_readtextfile @textfilename sysname. ©contents varchar(8000)='Results Only' OUT /* Объект: sp_readtextfile Описание: Reads the contents of a text file into a SQL result set Использование: sp_readtextfile @textfilename=name of file to read. @contents=optional output var to receive contents of file (up to 8000 bytes) Выходные данные: (None) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 8.0 $ Пример: sp_readtextfile 'D:\MSSQL7\L0G\errorlog' Created: 1996-05-01. $Modtime: 2000-01-20 $. */ AS SET N0C0UNT ON IF (@textfilename=7?') GOTO Help CREATE TABLE #lines (Ino int identity, line varchar(8000)) DECLARE @cmd varchar(8000). @crlf charB) SET @cmd='TYPE '+@textfilename SET @crlf=CHARA3)+CHARA0) INSERT #lines (line) EXEC master.dbo.xp_cmdshell @cmd IF ISNULL(@contents," К Results Only' SELECT ISNULLdine.") AS line FROM #1i nes ORDER BY Ino ELSE SELECT @contents=CASE Ino WHEN 1 THEN ISNULL(RTRIM(line). " )+@crlf ELSE @contents+ISNULL(RTRIM(line).")+@crlf END FROM #lines ORDER BY Ino DROP TABLE #lines RETURN 0 Help: EXEC sp_usage @objectname='sp_readtextfile'. №sc='Reads the contents of a text file into a SQL result set', @parameters='@textfilename=name of file to read. @contents=optional output var to receive contents of file (up to 8000 bytes)'. touthor='Ken Henderson', @email='khen@khen.com'. @version='8',@revision='0', Watecreated='19960501'. Odatelastchanged='20000120'. (aexample='sp_readtextf-ile "D:\MSSQL7\L0G\errorlog" ' продолжение &
488 Глава 21. Хранимые процедуры для администрирования Листинг 21.1 {продолжение) RETURN -1 GO EXEC sp_readtextfile 'c:\winnt\system32\drivers\gmreadme.txt' (Результаты) line README.TXT Вы используете это программное обеспечение на свой страх и риск. Я не гарантирую, что оно делает что-либо полезное или вредное. Я не могу взять на себя ответственность за его работу. Если мой новый элемент управления с календарем не соответствует вашим требованиям, вы можете вернуть мне этот продукт. Хотя вы и не сможете получить обратно свои деньги, вы. по крайней мере, не будете испытывать неудобств от использования плохого программного продукта, -р Эта процедура включает в себя пару интересных приемов. Во-первых, обратите внимание на значение выходного параметра по умолчанию. Он делает наличие выходного параметра необязательным. Для входных параметров в этом нет ничего .. необычного, но, возможно, вы не рассматривали возможность аналогичного приема для выходных. Я применил данный прием, чтобы предоставить два варианта использования одной процедуры. Если все, что вам требуется, — это получить текстовый файл как набор данных, вы можете опустить выходной параметр. Процедура просто вставит текстовый файл в таблицу, а затем представит текст как набор строк результирующего набора данных. Если же, напротив, вам требуется получить содержимое файла в переменную, вы можете передать его через varchar, и процедура вернет в эту переменную первые 8000 байтов файла. Здесь мы не можем использовать переменную типа text, поскольку локальные переменные этого типа не поддерживаются Transact-SQL. Ниже приведен вариант с выходным параметром. DECLARE @txt varchar(8000) EXEC sp_readtextfile "c:\readme.txt", @txt OUT SELECT @txt Во-вторых, обратите внимание на использование xp_cmdshell для чтения содержимого файла. Все, что было необходимо сделать для загрузки текстового файла во временную таблицу, — это вызвать процедуру xp_cmdshell для передачи операционной системе команды TYPE. Поскольку результат работы xp_cmdshell может быть перенаправлен во временную таблицу посредством INSERT...EXEC, этот прием может служить простым механизмом чтения файлов. Мы загрузили файл во временную таблицу, поскольку xp_cmdshell совершенно некстати заменяет пустые строки в файле значением NULL. Помещение данных в таблицу позволяет нам использовать функцию isnull () для того, чтобы отфильтровать эти «несимпатичные» NULL. ПРИМЕЧАНИЕ Процедура SQL Server xp_readerrorlog также может читать простые текстовые файлы. Для того чтобы посредством xp_readerrorlog прочитать текстовый файл, отличный от файла errorlog, передайте ей -1 в качестве первого параметра, за которым после запятой следует имя файла, который вы хотите прочитать. Процедура sp_readtextfiIe обеспечивает нас средством чтения текстовых файлов без привлечения расширенных хранимых процедур, а процедура xp_readerrorlog работает также хорошо (если вас не смущает использование расширенных процедур).
spjJiff 489 sp_diff Вы, возможно, помните, что в главе 4 мы встраивали в меню Query Analyzer проверку различия версий VSS для обнаружения отличий в сценариях T-SQL. Процедура sp_dif f идет по тому же пути. Она использует интерфейс командной строки SS. ЕХЕ для доступа к механизму проверки различий VSS, чтобы найти отличия между двумя определенными вами файлами. Она возвращает найденную VSS разницу между файлами в виде результирующего набора данных. Вот ее код. Листинг 21.2. Процедура sp_diff USE master GO IF OBJECT_ID('sp_diff') IS NOT NULL DROP PROC sp_diff GO CREATE PROC spjJiff @filel sysname=7?', @file2 sysname=NULL /* Объект: spjjiff Описание: Returns the differences between two text files as a result set (uses VSS) Назначение: sp_diff @filel=full path to first file. @file2=fullpath to second file Выходные данные: (None) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 1.0 $ ;' Пример: sp_di ff 'с:\customers.sql'. 'с:\customers2.sql' Created: 2001-01-14. $Modtime: 2001-01-16 $. */ AS SET N0C0UNT ON IF (C0ALESCE(@filel+@file2,7?')=7?') GOTO Help DECLARE @cmd varchar(lOOO) SET @cmd='SS diff '+@filel+' '+@file2+' -Yadmin' CREATE TABLE #diffs (line int identity, diff varchar(8000)) INSERT #diffs (diff) EXEC master..xp_cmdshell @cmd SELECT ISNULLCdiff,") AS diff FROM fdiffs ORDER BY line DROP TABLE fdiffs RETURN 0 Help: EXEC sp_usage @objectname='sp_diff', @desc='Returns the differences between two text files as a result set (uses VSS)'. @parameters='@filel=full path to first file. @file2=fullpath to second file'. @author='Ken Henderson'. @emai 1 = 'khen@khen.com'. продолжение ■£
490 Глава 21. Хранимые процедуры для администрирования Листинг 21.2 {продолжение) @version='Г,@revision='0', @datecreated='20010114'. @datelastchanged='20010116', @example='sp_d1ff ''c:\customers.sql'', ''c:\customers2.sql' RETURN -1 GO EXEC sp_diff 'c:\customers.sql'. 'c:\customers2.sqV (Результаты) dlff Diffing: c:\customers.sql Against: c:\customers2.sql б Change: [CompanyName] [nvarchar] D0) COLLATE SQL_Latinl_General_CP1_CI_A To: [Company] [nvarchar] D0) COLLATE SQL_Latml_General_CPl__CI_AS N0 15 Del: [Fax] [nvarchar] B4) COLLATE SQL_Latinl_General_CPl_CI_AS NULL В этом примере я передал в sp__diff два сценария для нахождения различий между ними. Процедура в начале вернула два имени файла этих сценариев, затем вывела список различий между ними. Она показала, что поле CompanyName было сокращено до Company во второй таблице и что строка 15 была удалена. Как мы можем узнать, к какой из таблиц относятся эти различия? Взгляните более внимательно на выражение в начале листинга Dif f ing...Against. Оно означает, что сценарий Customer2. SQL рассматривается как главная копия кода, и результат работы процедуры перечисляет шаги, которые бы следовало предпринять, чтобы привести Customer. SQL в полное соответствие с Customer2. SQL. Так же как и sp_readtextf ile, sp__dif f для основной работы использует вызов xp_cmdshell. Она использует xp_cmdshell для вызова SS. ЕХЕ, утилиты командной строки VSS, и передает ей для сравнения два файла. Обратите внимание на параметр -Y. Это имя пользователя, с которым вы будете подключаться к VSS. Здесь я для простоты использовал admin без применения пароля — вы, вероятно, используете другое имя. Мы перехватили выходные данные xp_cmdshell, поместили их во временную таблицу и так же, как и в sp_readtextfile, избавились от значений NULL. Конечный результат является простым, но весьма функциональным способом обнаружения различий с использованием Transact-SQL. sp_generate_script Процедура sp_generate_script — удобная утилита для того, чтобы формировать сценарий создания объектов базы данных. О ее первоначальном виде было рассказано в моей предыдущей книге, «Профессиональное руководство no Transact-SQL» («Питер», 2005), затем эта процедура была модифицирована и слегка улучшена. Для использования sp_generate_script необходимо передать ей имя объекта, сценарий которого вы хотите создать. Вы также можете при желании передать ей файлы по маске или вовсе опустить имя: в этом случае будет создан сценарий текущей базы данных.
sp_generate_script 491 Для своей работы sp_generate_script использует API SQL-DMO. Процедура создает посредством sp_OACreate СОМ-объект DMO и вызывает его методы с помощью sp_OAMethod. Поскольку для создания любого типа сценария DMO требует соединения с сервером, вы должны передать в процедуру sp_generate_script имя пользователя и пароль, чтобы она посредством DMO могла создать новое соединение с сервером. После установления соединения процедура находит объект (или объекты), сценарий которого вы хотите создать, и добавляет их к объекту DMO Transfer так, чтобы они могли быть переданы для записи на диск. Если вы предпочитаете получить набор данных (установка по умолчанию), sp_generate_script вызывает sp_readtextf ile, чтобы прочесть файл сценария, созданного DMO, и возвращает результирующий набор данных. Единственная неприятная особенность этой процедуры заключается в том, что объект DMO Transfer формирует частичный набор данных, от которого, насколько мне известно, невозможно избавиться. В независимости от того, хотите вы получить результирующий набор данных или нет, вы получите небольшой набор в конце каждого вызова sp_generate_script. Я поместил в коде, сразу после передачи объекта Transfer методу ScriptTransfer, команду PRINT с сообщением о том, что выходные данные должны быть проигнорированы. К сожалению, это все, что я мог сделать. Но, к счастью, эта неприятность незначительна, поскольку, несмотря на это сообщение, сценарий будет создан. Ниже приведен код процедуры. Листинг 21.3. Процедура sp_generate_script USE master GO IF OBJECT_ID('sp_generate_script') IS NOT NULL DROP PROC sp_generate_script GO CREATE PROC sp_generate__script @objectname sysname=NULL, -- Маска объектов, подлежащих копированию @outputname sysname=NULL. -- Имя создаваемого файла (по умолчанию: @objectname+'.SQL') (Pscriptoptions int=NULL. -- Битовая маска для параметров Transfer @resu1tset bit—1. -- Определяет, будет ли возвращен сценарий в виде результирующего набора данных @server sysname='(local)'. -- Имя сервера, с которым будет устновлено соединение @username sysname='sa'. -- Имя пользователя, под учетной записью которого будет производиться соединение (по умолчанию'за') ^password sysname=NULL. -- Пароль пользователя @trustedconnection bit=l. -- Использовать доверительное соединение для подключения к серверу @IncludeHeaders bit=l -- Включать в заголовок сценария описание /* Объект: sp_generate_script Описание: Создает сценарий создания объекта или группы объектов Назначение: sp_generate_script [(aobjectname-'Имя объекта или маска объектов (по умолчанию для всех объектов в текущей базе данных) '] [,@outputname=' Имя выходного файла' (По умолчанию: @objectname+'.SQL'. или GENERATED_SCRIPT.SQL для всей базы)] [.@scriptoptions= Битовая маска, определяющая параметры формирования сценария 3 [,@resu1tset=<Pnar. определяющий, будет ли возвращен сценарий в виде результирующего набора данныхЬН specifying whether to generate a result set продолжение &
492 Глава 21. Хранимые процедуры для администрирования Листинг 21.3 {продолжение) [.@includeheaders= Флаг, определяющий Включение в заголовок сценария описанияЬ [,@server=' имя сервера'][. @username=' имя пользователя'][, @password='пароль'][. @trustedconnection=l] Выходные данные: (None) lAuthor: Ken Henderson $. Email: khen@khen.com SRevision: 8.0 $ Пример: sp_generate_script @objectname='authors'. @outputname='authors.sqV Created: 1998-04-01. SModtime: 2000-12-16 $. */ AS -- Переменные SQLDM0_SCRIPT_TYPE DECLARE (aSQLDMOScri pt_Defau1t int DECLARE @SQLDMOScnpt_Drops int DECLARE @SQLDMOScript_ObjectPermissions int DECLARE @SQLDMOScript_PnmaryObject int DECLARE (aSQLDMOScript_Clustered Indexes int DECLARE @SQLDM0Scr DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri pt_Permiss ions int DECLARE (aSQLDMOScri DECLARE @SQLDM0Scr DECLARE @SQLDMOScr DECLARE (aSQLDMOScri pt_ToFile0n1y int pt_Bindings int pt_AppendToFile int ptJJoDRI int DECLARE @SQLDMOScnpt_UDDTsToBaseType int DECLARE @SQLDM0Script_Inc1udeIfNotExists int pt_NonClusteredIndexes int pt_Indexes int pt_Aliases int DECLARE @SQLDMOScri DECLARE @SQLDMOScri DECLARE @SQLDM0Scri DECLARE @SQLDMOScnpt_NoCommandTerm int DECLARE @SQLDM0Scri DECLARE (aSQLDMOScri DECLARE @SQLDMOScn DECLARE @SQLDM0Scri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE @SQLDM0Scn DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri DECLARE (aSQLDMOScri pt_Triggers int pt_DatabasePermissions int pt_DRIIndexes int pt_IndudeHeaders int pt_OwnerQualify int pt_JimestampToBinary int pt_SortedData int pt_SortedDataReorg int pt_TransferDefau1t int pt_DRI_NonClustered int pt_DRI_Clustered int pt_DRI_Checks int pt_DRI_Defaults int pt_DRI_UniqueKeys int pt_DRI_ForeignKeys int pt_DRI_PrimaryKey int pt_DRI_A11Keys int pt_DRI_AllConstraints int pt_DRI_A11 int pt_DRIWithNoCheck int pt_NoIdentity int ptJJseQuotedldentitiers int -- Переменные SQLDM0_SCRIPT2_TYPE
sp_generate_script 493 DECLARE @SQLDM0Scnpt2_Default int DECLARE @SQLDM0Script2_AnsiPadding int DECLARE @SQLDM0Script2_AnsiFile int DECLARE @SQLDM0Script2_UnicodeFile int DECLARE @SQLDM0Script2_NonStop int DECLARE @SQLDM0Script2JoFG int DECLARE @SQLDM0Script2_MarkTriggers int DECLARE @SQLDM0Script2_0nlyUserTriggers int DECLARE @SQLDM0Scnpt2JncryptPWD int DECLARE (?SQLDMDScript2_SeparateXPs int -- Значения SQLDMO_SCRIPT_TYPE SET @SQLDMOScnpt_Default = 4 SET @SQLDMOScript_Drops = 1 SET @SQLDMOScnpt_ObjectPermissions = 2 SET @SQLDMOScnpt_PrimaryObject = 4 SET (aSQLDMOScriptJClusteredlndexes = 8 SET @SQLDMOScript_Triggers = 16 SET @SQLDMOScript_DatabasePermissions = 32 SET @SQLDMOScript_Permissions = 34 SET (aSQLDMOScriptJoFileOnly = 64 SET CSQLDM0Scnpt_Bindings = 128 SET @SQLDMOScript_AppendToFile = 256 SET @SQLDMOScript_NoDRI = 512 SET @SQLDMOScript_UDDTsToBaseType = 1024 SET @SQLDMOScnpt_IncludeIfNotExists = 4096 SET @SQLDMOScript_NonC1usteredIndexes = 8192 SET (PSQLDMOScriptJndexes = 73736 SET @SQLDMOScript_Aliases = 16384 SET (aSQLDMOScriptJJoCommandTerm = 32768 SET (aSQLDMOScript_DRIIndexes = 65536 SET (aSQLDMOScriptJncludeHeaders - 131072 SET CSQLDM0Script_0wnerQu'alify = 262144 SET @SQLDMOScript_TimestampToBinary = 524288 SET @SQLDMOScript_SortedData = 1048576 SET @SQLDMOScnpt_SortedDataReorg = 2097152 SET @SQLDMOScript_TransferDefault = 422143 SET @SQLDMOScnpt_DRI_NonClustered = 4194304 SET @SQLDMOScript_DRI_Clustered = 8388608 SET @SQLDMOScnpt_DRI_Checks = 16777216 SET @SQLDMOScript_DRI_Defaults = 33554432 SET @SQLDMOScnpt_DRI_UniqueKeys = 67108864 SET @SQLDMOScript_DRI_ForeignKeys = 134217728 SET @SQLDMOScnpt_DRI_PrimaryKey = 268435456 SET (aSQLDMOScript_DRI_AHKeys - 469762048 SET (aSQLDMOScript_DRI_AHConstraints = 520093696 SET @SQLDMOScnpt_DRI_All = 532676608 SET @SQLDMOScnpt_DRIWithNoCheck = 536870912 SET (PSQLDMOScriptJoIdentity = 1073741824 SET ASQLDMOScriptJJseQuotedldentifiers = -1 -- Значения SQLDM0_SCRIPT2_TYPE SET @SQLDM0Script2_Default = 0 SET (?SQLDM0Scnpt2_AnsiPadding = 1 SET @SQLDM0Script2_AnsiFile = 2 SET CSQLDMOScript2_UnicodeFile = 4 SET @SQLDM0Script2_NonStop = 8 SET @SQLDM0Script2_NoFG = 16 SET @SQLDM0Script2_MarkTnggers = 32 продолжение ■.
494 Глава 21. Хранимые процедуры для администрирования Листинг 21.3 {продолжение) SET @SQLDM0Script2_0nlyUserTnggers = 64 SET @SQLDM0Script2JncryptPWD = 128 SET @SQLDM0Scnpt2_SeparateXPs = 256 DECLARE @dbname sysname. @srvobject int. -- 06beKTSQL Server @object int. -- Рабочая переменная для доступа к СОМ-объекту @hr int. -- Содержит результат, возвращаемый СОМ @tfobject int. -- Указатель на объект Transfer @res int SET @res=0 IF (@objectname=7?') GOTO Help IF (@objectname IS NOT NULL) AND (CHARINDEX('%' ,@objectname)=0) AND (CHARINDEX('_'.@objectname)=0) BEGIN SET @dbname=ISNULL(PARSENAME(@objectname,3).DB_NAMEO) -- Извлекает имя базы данных: по умолчанию теущая SET @objectname=PARSENAME((aobjectname.l) -- Удаляет лишние части из имени таблицы IF (@objectname IS NULL) BEGIN RAISERROR('Invalid object name.'.16.1) /* Сообщение: неверное имя объекта*/ RETURN -1 END IF (@outputname IS NULL) SET @outputname=@objectname+'.SQL' END ELSE BEGIN SET @dbname=DB_NAME() IF (@outputname IS NULL) SET @outputnarne='GENERATED_SCRIPT.SQL' END -- Создание объекта SQLServer EXEC @hr=sp_OACreate 'SQLDMO.SQLServer'. @srvobject OUTPUT IF (@hr <> 0) BEGIN EXEC sp_displayoaerrorinfo @srvobject. @hr RETURN END -- Создание объекта Transfer EXEC @hr=sp_OACreate 'SQLDMO.Transfer', @tfobject OUTPUT IF (fflir <> 0) BEGIN EXEC sp_displayoaerrorinfo @tfobject. @hr RETURN END - - Устанавливаем свойство объекта Transfer CopyData EXEC @hr = sp_OASetProperty @tfobject. 'CopyData'. 0 IF (@hr <> 0) BEGIN EXEC spjiisplayoaerrorinfo @tfobject. @hr RETURN END -- Указываем объекту Transfer копировать схему данных EXEC @hr = sp_OASetProperty @tfobject, 'CopySchema'. 1 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo @tfobject. @hr RETURN
sp_generate_script 495 END IF (Pobjectname IS NULL) BEGIN -- Получить все объекты в базе -- Указываем объекту Transfer копировать все объекты EXEC @hr = sp_OASetProperty @tfobject, 'СоруАПObjects'. 1 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo @tfobject, @hr RETURN END -- Указываем объекту Transfer получить все группы EXEC @hr = spJDASetProperty @tfobject. ' IncludeGroups'. 1 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo @tfobject, @br RETURN END -- Указываем объекту Transfer включить пользователей EXEC @hr = sp_OASetProperty @tfobject, ' IncludeUsers'. 1 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo Ptfobject. @hr RETURN END -- Указываем объекту Transfer включить логины к серверу EXEC @hr = spJDASetProperty @tfobject. 'IncludeLogins', 1 IF (@hr <> 0) BEGIN EXEC spjjisplayoaerrorinfo @tfobject. @hr RETURN END -- Указываем объекту Transfer включить объекты, от которых зависят указанные явно объекты , too EXEC @hr = spJDASetProperty @tfobject. 'IncludeDependencies'. 1 IF (@br <> 0) BEGIN EXEC spjjisplayoaerrorinfo Ptfobject, @hr RETURN END IF (Pscnptoptions IS NULL) BEGIN SET @scriptoptions=@SQLDMOScript_OwnerQualify | @SQLDMOScript_Defau1t | CSQLDM0Script_Triggers | @SQLDMOScript_Bindings | @SQLDMOScnptJDatabasePermissions | @SQLDMOScript_Permissions | @SQLDMOScript_ObjectPermissions | @SQLDMOScript_ClusteredIndexes | (aSQLDMOScriptJndexes | @SQLDM0Script_A1iases | @SQLDMOScript_ORI_An IF @inc1udeheaders=l SET @scriptoptions=@scriptoptions | @SQLDMOScript_IncludeHeaders END END ELSE BEGIN DECLARE @obname sysname, @obtype varcharB), @obowner sysname. @OBJECT_TYPES varcharE0), @obcode int -- Используется для конвертации sysobjects.type в битовую маску, которая необходима объекту Transfer продолжение &
496 Глава 21. Хранимые процедуры для администрирования Листинг 21.3 {продолжение) SET @OBJECTJYPES='T V U P D R TR FN TF IF ' -- Находим все объекты, которые соответствуют текущей маске, и добавляем их в объект Transfer -- Список объектов, включаемых в сценарий DECLARE ObjectList CURSOR FOR SELECT name.CASE type WHEN 'TF' THEN 'FN' WHEN 'IF' THEN 'FN' ELSE type END AS type.USERJIAME(uid) FROM sysobjects WHERE (name LIKE @objectname) AND (CHARINDEX(type+' '.@OBJECT_TYPES)<>0) AND (OBJECTPROPERTY(id.'IsSystemTable')=0) AND (status>0) UNION ALL -- Включить типы данных, определенных пользователем SELECT name, 'Г ,USER_NAME(md) FROM SYSTYPES WHERE (usertype & 256)<>0 AND (name LIKE @objectname) OPEN ObjectList FETCH ObjectList INTO @obname, @obtype, @obowner WHILE (@(aFETCH_STATUS=0) BEGIN SET (aobcode=P0WERB.(CHARINDEX№obtype+' ' .@0BJECT_TYPES)/3)) EXEC @hr = sp_OAMethod @tfobject. 'AddObjectByName'. NULL. @obname. @obcode. @obowner IF (@hr <> 0) BEGIN EXEC spjiisplayoaerrorinfo @tfobject. @hr RETURN END FETCH ObjectList INTO @obname. @obtype. gobowner END CLOSE ObjectList DEALLOCATE ObjectList IF (Pscriptoptions IS NULL) SET @scriptoptions=@SQLDMOScript_Default -- Сохраняем в случае, если не создается сценарий всей базы данных END -- Устанавливаем свойство ScnptType объекта Transfer EXEC @hr = sp_OASetProperty (Ptfobject. 'ScriptType'. @scriptoptions IF (@hr <> 0) BEGIN EXEC sp_displayoaerrorinfo @tfobject. @hr RETURN END -- Соединяемся с сервером IF (@trustedconnection=l) BEGIN EXEC @hr = sp_OASetProperty @srvobject. 'LoginSecure'. 1 IF №hr <> 0) GOTO ServerError EXEC @hr = sp_OAMethod @srvobject, 'Connect', NULL. @server END ELSE BEGIN IF (^password IS NOT NULL) EXEC @hr = sp_OAMethod @srvobject. 'Connect'. NULL, ^server. @username. @password ELSE EXEC @hr = spJDAMethod @srvobject, 'Connect'. NULL, Pserver, Pusername
sp_generate_script 497 END IF (@hr <> 0) GOTO ServerError -- Получаем указатель на коллекцию обьектов Databases SQLServer EXEC @hr = sp_OAGetProperty @srvobject. 'Databases'. @object OUT IF @br <> 0 BEGIN EXEC sp_displayoaerrorinfo @srvobject. @hr RETURN END -- Получаем указатель на нужную базу данных из коллекции обьектов Databases EXEC @hr = sp_OAMethod ^object. 'Item', ^object OUT. Мэпате IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object. @hr RETURN END -- Вызываем метод ScriptTransfer обьекта Database для создания сценария EXEC @hr = sp_OAMethod @object, 'ScriptTransfer'.NULL. @tfobject, 2. @outputname IF @hr <> 0 BEGIN EXEC sp_displayoaerrorinfo @object, @hr RETURN END ЛБудет выведено сообщение: "ВАЖНО: вышеприведенный код должен быть проигнорирован. Он является побочным эффектом работы метода SQL-DMO, используемого для формирования файла сценария"*/ PRINT 'NOTE: Ignore the code displayed above. If's a remnant of the SQL-DMO method used to produce the script file' IF (@resultset=l) EXEC sp_readtextfile @outputname GOTO ExitPoint ServerError: SET @res=-2 ExitPoint: EXEC sp_OADestroy @srvobject -- Удаляем обьект EXEC spJDADestroy @tfobject -- Удаляем обьект RETURN @res Help: EXEC sp_usage @objectname='sp_generate_script',@desc='Generates a creation script for an object or collection of objects'. @parameters='[@objectname=''Object name or mask (defaults to all object in current database)''] [,@outputname=''Output file name'' (Default: @objectname+''.SQL'', or GENERATED_SCRIPT.SQL for entire database)] [,@scriptoptions=bitmask specifying script generation options] [,@resultset=bit specifying whether to generate a result set [,@includeheaders=bit specifying whether to generate discriptive headers for scripts [,@server=''server name''][, @username=''user name''][. @password=''password''][, @trustedconnection=l]', @author='Ken Henderson'. @email='khen@khen.com'. @version='8', @revision='0'. (?datecreated=' 19980401'. @datelastchanged='20001216', продолжение #
498 Глава 21. Хранимые процедуры для администрирования Листинг 21.3 {продолжение) @example='sp_generate_script @objectname=''authors''. @outputname=''authors.sql'' RETURN -1 GO USE Northwind GO EXEC sp_generate_script 'Customers'. @server='khenmp\ss2000' (Результаты) Columnl set quoted_identifier OFF GO CREATE TABLE [Customers] ( [CustomerlD] [nchar] E) COLLATE SQL_Latinl_General_CPl_CI_AS NOT NULL , [CompanyName] [nvarchar] D0) COLLATE SQL_Latinl_General_CPl_CI_AS NOT NULL. [ContactName] [nvarchar] C0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [ContactTitle] [nvarchar] C0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Address] [nvarchar] F0) COLLATE SQL_Latinl_Genera1_CPl_CI_AS NULL . [City] [nvarchar] A5) COLLATE SQL_Latinl_General_CPl_CI_AS NULL , [Region] [nvarchar] A5) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [PostalCode] [nvarchar] A0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Country] [nvarchar] A5) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Phone] [nvarchar] B4) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Fax] [nvarchar] B4) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [rowguid] uniqueidentifier ROWGUIDCOL NOT NULL CONSTRAINT [DF_Customers_rowgu_0EF836A4] DEFAULT (newidO). CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ( [CustomerlD] ) ON [PRIMARY] ) ON [PRIMARY] GO A row(s) affected) NOTE: Ignore the code displayed above. It's a remnant of the SQL-DMO method used to produce the script file. (Примечание. Проигнорируйте вышеприведенный код. Это побочный эффект работы метода SQL-DM0 генерации сценария.) line set quotedjdentifier OFF GO CREATE TABLE [Customers] ( [CustomerlD] [nchar] E) COLLATE SQL_Latinl_General_CPl_CI_AS NOT NULL , [CompanyName] [nvarchar]D0) COLLATE SQL_Latml_General_CPl_CI_AS NOT NULL [ContactName] [nvarchar] C0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [ContactTitle] [nvarchar] C0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Address] [nvarchar] F0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [City] [nvarchar] A5) COLLATE SQL_Latml_General_CPl_CI_AS NULL . [Region] [nvarchar] A5) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [PostalCode] [nvarchar] A0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Country] [nvarchar] A5) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [Phone] [nvarchar] B4) COLLATE SQL_Latinl_General_CPl_CI_AS NULL .
sp_generate_script 499 [Fax] [nvarchar] B4) COLLATE SQL_Latinl_General_CPl_CI_AS NULL . [rowguid] uniqueidentifier ROWGUIDCOL NOT NULL CONSTRAINT [DF__Customers__rowgu__0EF836A4] DEFAULT (newidO). CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ( [CustomerlD] ) ON [PRIMARY] ) ON [PRIMARY] GO Все, что находится выше сообщения PRINT (оно выделено полужирным шрифтом), является побочным эффектом и может быть проигнорировано. Результаты, находящиеся ниже этого сообщения, в действительности являются требуемым сценарием. Я сформировал сценарий создания таблицы Customers базы данных Northwind. Также легко я мог создать сценарий всей базы или группы объектов по маске. Процедура начинается с создания объектов DMO SQLServer и Transfer. Объект SQLSe rve г является вершиной иерархии DMO. Вы используете его для соединения с сервером, а также для доступа к другим объектам на сервере. Объект Transfer инкапсулирует возможность DMO передавать данные с одного сервера на другой и с сервера в файл. Sp_generate_script использует этот объект для формирования SQL-сценария. Если вам приходилось программировать на DMO, возможно, вы удивились тому, что я использовал объект Transfer, а не вызов метода Script для каждого объекта. Причина, по которой я так поступил, проста: наличие зависимостей между объектами. Объект Transfer записывает схему объектов в сценарий в порядке их зависимостей. Поскольку изначально он был предназначен для передачи информации между базами данных, он должен обладать информацией о зависимостях объектов. В противном случае команда CREATE для объекта, который зависит от другого объекта, не будет выполнена, если требуемый объект на тот момент не был создан. Рассмотрим ограничение внешнего ключа. Если таблица Order Details создает внешний ключ, ссылающийся на таблицу Products, таблица Products должна существовать до начала создания таблицы 0 rde r Details. Команда CREATE TABLE в этом случае выполнена не будет. Объект Transfer, учитывая зависимость объектов, гарантирует корректность создаваемого сценария базы данных. После создания объекта Transfer процедура определяет, что требуется пользователю: сценарий всей базы данных или только выделенных объектов. Разница существенна, поскольку, как я уже упоминал, если необходим сценарий всей базы, DMO помещает объекты в сценарий с учетом зависимостей между ними. Если же должен быть создан сценарий только группы объектов, процедура открывает курсор на таблицах sysobjects и systypes (с использованием UNION ALL) и для каждого выделенного объекта базы данных вызывает метод AddObj ectByName объекта Transfer. После того как процедура получает указатель на нужную базу данных, она вызывает ее метод ScriptTransfer, передавая ей в качестве параметра созданный перед этим объект Transfer. Тем самым формируется сценарий требуемого объекта. В завершение процедура возвращает полученный сценарий в виде результирующего набора данных. Обычно, вызывая процедуру, мы хотим немедленно увидеть результат. Если @resultset = 1 (по умолчанию), sp_generate_script вызывает sp_readtextf ile для того, чтобы вернуть только что сгенерированный сценарий
500 Глава 21. Хранимые процедуры для администрирования посредством результирующего набора данных. В качестве полезной альтернативы этому способу можно было бы вернуть указатель курсора на полученный сценарий (далее вы сделаете это в упражнении). sp_start_trace Утилита SQL Server Profiler является мощным инструментом, она во много раз превосходит по возможностям утилиту SQL Trace, которая входила в состав SQL Server до версии 7.0. Однако она остается ресурсоемким и временами очень медленным приложением. Поскольку мне часто требуется трассировка SQL Server в тех ситуациях, когда я не хочу терпеть накладные расходы, связанные с графическим интерфейсом Profiler, я написал свой собственный набор хранимых процедур для управления трассировками Profiler. Они позволяют мне запускать, останавливать и просматривать трассировки Profiler без использования Profiler. Как они работают? Очень просто: посредством вызова тех же расширенных процедур sp_t race_%, „которые использует сам Profiler. Эти процедуры описаны в Books Online, и онн гораздо легче в использовании, чем в предыдущих версиях. Мой код по управлению трассировками состоит из трех процедур: sp_sta rt_t race, sp_stop_trace и sp_list_trace. Их назначение полностью соответствует их названиям. Вот код процедуры sp_start_trace. Листинг 21.4. Процедура sp_start_trace USE master GO IF OBJECT_ID('sp_start_trace') IS NDT NULL DROP PROC sp_start_trace GO CREATE PROC sp_start_trace @FileName sysname=NULL. @TraceName sysname='tsqltrace'. @0ptions int=2, @MaxFileSize tngint=5, @StopTime datetime=NULL. ^Events varcharC00)= -- 11 - RPC.-Старт -- 13 - SQL:3anycK пакета -- 14 - Соединение -- 15 - Отключение -- 16 - Внимание -- 17 - Наличие соединения -- 33 - Исключение -- 42 - Запуск хранимой процедуры -- 43 - Выполнение хранимой процедуры -- 45 - Начало выполнения операторов хранимой процедуры -- 55 - Предупреждение о замене хэш-операции альтернативной -- 67 - Предупреждения, возникшие во время выполнения хранимой процедуры -- 69 - Предупреждение о сортировке -- 79 - Отсутствует желательная статистика по полю -- 80 - Запрос без объединений '11.13.14.15,16.17.33.42.43.45.55.67.69,79.80', @Cols varcharC00)= -- Все поля '1.2.3,4.5,6.7.8,9,10.11,12,13,14.15.16.17.18,19.20.21,22.23.24.25.26.27,28,29,30.31. 32.33.34.35.36.37,38.39.40.41,42,43,44.'.
sp_start_trace 501 @IncludeTextFiIter sysname=NULL. @ExcludeTextFiIter sysname=NULL. PlncludeObjIdFnlter int=NULL. @ExcludeObjIdFilter int=NULL. PTraceld int = NULL /* Обьект: sp_start_trace Описание: Запускает трассировку подобно Profiler посредством вызова расширенной процедуры Transact-SQL Использование: sp__start_trace @FileName sysname default: с:\temp\YYYYMMDDhhmi ssmmm.tre Определяет имя трассировочного файла (SQL Server всегда добавляет расширение .trc ) tsqltrace -- Определяет имя трассы 2 (TRACEJILEJROLLOVER) 5 (MB) NULL SP-related events and errors/warnings (PTraceName sysname default: ^Options int default: @MaxFileSize bigint default: @StopTime datetime default: @Events varcharC00) default: -- Разделенный запятой список, определяющий трассируемые события @Cols varcharC00) default: All columns -- Разделенный запятой список, определяющий трассируемые поля OlncludeTextFiIter sysname default: NULL -- Строковая маска, определяющая строки TextData. включаемые в трассу (PExcl udeTextFi Iter sysname default: NULL -- Строковая маска, определяющая строки TextData. исключаемые из трассы (aincludeObjIdFilter sysname default: NULL -- Определяет идентификатор обьекта. включаемого в трассу @ExcludeObjIdFilter sysname default: NULL -- Определяет идентификатор обьекта. исключаемого из трассы (PTraceld int default: NULL -- Определяет идентификатор трассы Выходные данные: (None) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 2.0 $ Пример: EXEC sp_start_trace -- Запускает трассу EXEC sp_start_trace @Fi1ename='d:\mssql7\log\mytrace' -- Запускает трассу с определенным именем файла EXEC sp_start_trace @Events='37.43' -- Запускает трассу для отслеживания определенных классов событий EXEC sp_start_trace @Cols='1.2,3' -- Запускает трассу для определенных типов полей EXEC sp_start_trace @lncludeTextFilter='EXEC£ FooProcV -- Запускает трассу, включающую в себя события по маске TextData EXEC sp_start_trace @tracename-'General Performance' -- Запускает трассу, используя определенное имя EXEC sp_start_trace @filename - 'd:\mssql7\log\mytrace'. -- Запускает трассу с определенным набором параметров @TraceName = 'General Performance', @Optnons = 2. @MaxFileSTze = 500. @StopTime = NULL. ©Events = '10.11,14.15.16.17.27.37.40.41.55.58.67.69.79.80,98'. @Cols = DEFAULT, @IncludeTextFiIter = NULL, (aincludeObjIdFilter = NULL, @ExcludeObjIdFilter = NULL Created: 1999-04-01. $Modtnme: 2000-12-16 $. продолжение &
502 Глава 21. Хранимые процедуры для администрирования Листинг 21.4 {продолжение) */ AS SET NOCOUNT ON IF @FileName=7?' GOTO Help -- Объявление переменных DECLARE @01dQueueHandle int -- Указатель на очередь запущенных в текущее время трассировок DECLARE @QueueHandle int -- Указатель на новую очередь трассировок Queue handle for new running trace queue DECLARE @0n bit -- Необходим, поскольку существует ошибка в одной из хранимых процедур sp_trace_xx procs DECLARE OOurObjId int -- Используется, чтобы избежать трассировки наших собственных действий DECLARE @01dTraceFile sysname -- Имя файла текущей трассы DECLARE @res int -- Переменная для результата вызова процедуры SET @On=l -- Проверка некоторых важных параметров IF (@Cols IS NULL) BEGIN RAISERRORCYou must specify the columns to trace.' .16.10) /*Вы должны явно указать поля трассировки*/ RETURN -1 END IF (^Events IS NULL) BEGIN RAISERRORCYou must specify a list of trace events in @Events.'.16.10) /*Вы должны явно указать список трассирумых событий в @Events */ RETURN -l END -- Добавляем текущую дату и время к имени файла, чтобы создать новое, уникальное имя файла IF @FileName IS NULL SELECT @FileName = 'c:\TEMP\tsqltraceJ + C0NVERT(CHAR(8).getdateO.112) + REPLACE(C0NVERT(varcharA5),getdate().114).':','') -- Создаем очередь трассировки Create the trace queue EXEC @res=sp_trace_create @traceid=@QueueHandle OUT, @options=@0ptions. @tracefi1e=@Fi1eName, @maxfi1esi ze=@MaxFi1eSi ze. @stoptime=@StopTime IF @res<>0 BEGIN IF @res=l PRINT 'Trace not started. Reason: Unknown error. 7*Tpacca не запущена. Причина: неизвестная ошибка*/ ELSE IF @res=10 PRINT 'Trace not started. Reason: Invalid options. Returned when options specified are incompatible.7*Tpacca не запущена. Причина: неверные настройки. Возникает при назначении несовместимых настроек */ ELSE IF @res=12 PRINT 'Trace not started. Reason: Error creating file. Returned if the file already exists, drive is out of space, or path does not exist.7*Tpacca не запущена. Причина: ошибка создания файла. Возникает, если файл уже существует или если переполнен диск, либо если не существует указанный путь */ ELSE IF @res=13 PRINT 'Trace not started. Reason: Out of memory. Returned when there is not enough memory to perform the specified action.7*Tpacca не запущена. Причина: недостаточно памяти. Возникает при недостатке памяти для выполнения указанных действий*/ ELSE IF @res=14 PRINT 'Trace not started. Reason: Invalid stop time. Returned when the stop time specified has already happened.7*Tpacca не запущена. Причина: неверное время завершения. Возникает, если указанное время уже наступило */ ELSE IF @res=15 PRINT 'Trace not started. Reason: Invalid parameters. Returned when the user supplied incompatible parameters.7*Tpacca не запущена. Причина: неверный параметр. Возникает при назначении несовместимых параметров */
sp_start_trace 503 RETURN @res END PRINT 'Trace started.7*Tpacca запущена*/ PRINT 'The trace file name is : '+@FileName+'. 7*Имя файла трассы: @Fi1eName */ -- Определяем классы событий и имена трассировки IF ©Events IS NOT NULL BEGIN -- Организуем цикл по строкам ©Events и ©Cols, производя разбор каждого события и поля и добавляя их к определению трассы IF RIGHT(©Events,l)<>'.' SET @Events=@Events+'.' -- Добавляем запятую для работы с циклом IF RIGHT(@Cols,l)<>'.' SET @Cols=@Cols+'.' -- Добавляем запятую для работы с циклом DECLARE @i int. @j int. ©Event int. ©Col int. ©ColStr varcharC00) SET @i=CHARINDEX(',',©Events) WHILE ©i<>0 BEGIN SET @Event=CAST(LEFT(@Events,@i-l) AS int) SET @ColStr=@Cols SET @j=CHARINOEX('.'.©ColStr) WHILE @j<>0 BEGIN SET ©Col=CAST(LEFT(@ColStr,@j-l) AS int) EXEC sp_trace_setevent @traceid=@QueueHandle, @eventid=@Event. @columnid=@Col. @on=@On SET @ColStr=SUBSTRING(@ColStr,@j+l.300) SET @j=CHARINDEX('.',@ColStr) END SET @Events=SUBSTRING(@Events.@i+1.300) SET @i=CHARINDEX('.'JEvents) END END -- Устанавливает фильтры (значения по умолчанию, исключают трассировку действий по трассированию ) -- Вы можете определить другие фильтры как имя приложения и так далее, передавая разделенные запятыми строки в параметры @IncludeTextFi1ter/@Excl udeTextFi1ter SET @ExcludeTextFilter='sp_£tracer+ISNULLC ; '+@ExcludeTextFi Iter,'') -- По умолчанию скрывает ваши собственные действия SET @0ur0bjId=0BJECT_ID('master..sp_start_trace') EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=l, @logical_operator=0, @compari son_operator=7. @va1ue=@ExcludeTextFi1ter EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=l. @logical_operator=0. @comparison_operator=7, @value=N'EXEC£ sp_UraceV IF (Plncl udeTextFi Iter IS NOT NULL EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=l. @logical_operator=0. @comparison_operator=6, @value=@IncludeTextFi Iter IF eincludeObjIdFilter IS NOT NULL EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=22, @logica]_operator=0. @comparison_operator=0, @value=@IncludeObjIdFilter EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=22. @logical_operator=0. @comparison_operator=l. @value=@0ur0bjld IF @ExcludeObjIdFilter IS NOT NULL EXEC sp_trace_setfilter @traceid=@QueueHandle. @columnid=22, @logical_operator=0, @cornparison_operator=l, @va1ue=@Exclude0bjIdFilter -- Включаем трассировку EXEC sp_trace_setstatus @traceid=@QueueHandle. @status=l -- Записываем указатель на очередь трасс для дальнейшей работы (это поможет нам узнать, как остановить трассу) IF 0BJECT_ID('tempdb..TraceQueue') IS NULL BEGIN CREATE TABLE tempdb..TraceQueue (TracelD int. TraceName varcharB0). TraceFile sysname) INSERT tempdb..TraceQueue VALUES(@QueueHandle. @TraceName. @FileName) END ELSE BEGIN IF EXISTStSELECT * FROM tempdb..TraceQueue WHERE TraceName = ©TraceName) BEGIN UPDATE tempdb..TraceQueue SET TracelD = @QueueHandle, TraceFile=@FileName WHERE TraceName = @TraceName продолжение Л>
504 Глава 21. Хранимые процедуры для администрирования Листинг 21.4 {продолжение) END ELSE BEGIN INSERT tempdb. .TraceQueue VALUES(@QueueHandle. (PTraceName. @FileName) END END RETURN 0 Help: EXEC sp_usage @objectname='sp_start_trace',@desc='Starts a Profiler-like trace using Transact-SQL extended Procedure calls.'. @parameters='@FileName sysname default: c:\temp\YYYYMMDDhhmissmmm.trc -- Specifies the trace file name (SQL Server always appends .trc extension) @TraceName ^Options (PMaxFileSize @StopTime ^Events varcharOOO) default sysname int bigint datetime default default default default tsqltrace -- Specifies the name of the trace 2 (TRACE__FILE_ROLLOVER) 5 (MB) NULL SP-related events and errors/warnings -- Comma- delimited list specifying the events numbers to trace @Cols varcharOOO) default: All columns -- Comma-deli mi ted list specifying the column numbers to trace @IncludeTextFilter sysname default: NULL -- String mask specifying what TextData strings to include in the trace @ExcludeTextFilter sysname default: NULL -- String mask specifying what TextData strings to filter out of the trace @IncludeObjIdFilter sysname default: NULL -- Specifies the id of an object to target with the trace @ExcludeObjIdFilter sysname default: NULL -- Specifies the id of an object to exclude from the trace @TraceId int default: NULL -- Specified the id of the trace to list when you specify the LIST option to @OnOff @author='Ken Henderson'. @emai1='khen@khen.com'. @version='2', @revision='0'. (adatecreated=' 19990401'. @datel astchanged=' 20001216'. @example='EXEC sp_start_trace -- Starts a trace EXEC sp_start_trace @Filename=''d:\mssql7\log\mytrace'' -- Starts a trace with the specified file name EXEC sp_start_trace @Events=''37,43'' -- Starts a trace the traps the specified event classes EXEC sp_start_trace @Cols=''1.2.3'' -- Starts a trace that includes the specified columns EXEC sp_start_trace @IncludeTextFilter=''EXECU FooProcV' -- Starts a trace that includes events matching the specified TextData mask EXEC sp_start_trace @tracename=''General Performance'1 -- Starts a trace using the specified name EXEC sp_start_trace @filename = ''d:\mssql7\log\mytrace1', -- Starts a trace with the specified parameters @TraceName = ''General Performance'1. @0ptions = 2. @MaxFileSize = 500, @StopTime = NULL. @Events = 0.11.14,15.16.17.27.37.40.41.55.58.67.69.79.80.9B". @ColS = DEFAULT. @IncludeTextFilter = NULL. @IncludeObjIdFilter = NULL. @ExcludeObjIdFilter = NULL RETURN -1
sp_stop_trace 505 GO exec sp_start_trace '/?' (Результаты) Trace started. (Трасса запущена) The trace file name is: c:\TEMP\tsqltrace_20001204011454350. (Имя трассировочного файла: С:\TEMP\tsql trace_2000l204011454350) По умолчанию sp_start_trace запускает трассировку Profiler, которая включает в себя все поля трассировки и все наборы событий, но вы можете изменить эту настройку, определив параметры @Events и/или @Cols. Оба параметра представляют собой списки с разделителем (в виде запятой) чисел, которые определяют события/поля трассировки. Описания этих чисел вы можете найти в Books Online. Если вы не задали имя выходного файла трассировки, sp_start_trace попытается создать файл в C:\TEMP, используя текущую дату и время для формирования имени файла. Размер файла трассировки может увеличиться до весьма значительного, поэтому вы должны позаботиться о его выводе на диск, на котором имеется достаточно много места. Имейте в виду, что путь, используемый sp_start_trace, относится к SQL Server, на котором вы запускаете трассировку, а не к локальной машине. В отличие от Profiler sp_sta rt_t race запускает трассировку исключительно на сервере. sp_stop_trace Запуск трассировки с помощью хранимой процедуры — это здорово. Но как нам остановить ее, чтобы мы могли потом проанализировать файл трассы? Для этого мы используем sp_stop_trace. Вот ее код. Листинг 21.5. Процедура sp_stop_trace USE master GO IF OBJECT_ID('sp_stop__trace') IS NOT NULL DROP PROC sp_stop_trace GO CREATE PROC sp_stop_trace PTraceName sysname='tsqltrace' /* Объект: sp_stop_trace Описание: Останавливает трассировку Profiler, используя вызов расширенной процедуры Transact-SQL Использование: sp_stop_trace @TraceName sysname default: tsqltrace -- Определяет имя трассы Выходные данные: (нет) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 2.0 $ Пример: EXEC sp_stop_trace -- Останавливает трассировку по умолчанию Created: 1999-04-01. $Modtime: 2000-12-16 $ */ AS продолжение &
506 Глава 21. Хранимые процедуры для администрирования Листинг 21.5 {продолжение) SET NOCOUNT ON IF @TraceName=7?' GOTO Help -- Объявление переменных DECLARE @0]dQueueHandle int -- Указатель на очередь выполняемых в настоящее время трассировок DECLARE KHdTraceFile sysname -- Имя файла текущей трассы -- Если трасса запущена, останавливаем ее IF OBJECT_ID('tempdb..TraceQueue') IS NOT NULL BEGIN IF EXISTSCSELECT * FROM tempdb..TraceQueue WHERE TraceName - @TraceName) BEGIN SELECT MldQueueHandle = TracelD. @01dTraceFile=TraceFile FROM tempdb..TraceQueue WHERE TraceName = @TraceName IF @@ROWCOUNT<>0 BEGIN EXEC sp_trace_setstatus @traceid=@01dQueueHandle. Pstatus=0 EXEC sp_trace_setstatus @traceid=@01dQueueHandle, @status=2 PRINT 'Oeleted trace queue ' + CAST(@01dQueueHandle AS varcharB0))+'.' /*Очередь удаленных трасс*/ PRINT 'The trace output file name is: '+@01dTraceFile /*Имя выходного файла трассы*/ DELETE tempdb..TraceQueue WHERE TraceName = @TraceName END END ELSE PRINT 'No active traces named '+@TraceName+'.' /*Нет активных трасс с именем @TraceName */ END ELSE PRINT 'No active traces.' /*Нет активных трасс */ RETURN О Help: EXEC sp_usage @objectname='sp_stop_trace',@desc='Stops a Profiler-like trace using Transact- SQL extended Procedure calls.'. @parameters='@TraceName sysname default: tsqltrace -- Specifies the name of the trace @author='Ken Henderson', @emai1 ='khen@khen.com'. @version='2', @revision='0', @datecreated='19990401'. @datelastchanged='20001216'. @example='EXEC sp_stop_trace -- Stops the default trace RETURN -1 GO exec sp_stop_trace '/?' Поскольку при старте вы можете задать имя трассы, sp_stop_trace позволяет передать ее имя как параметр. Если вы не задали имя трассы, процедура попытается остановить трассировки с именем по умолчанию — tsqlt race. Обратите внимание на то, что обе процедуры используют таблицу Tempdb. .TraceQueue. Нам необходимо отслеживать трассировку по имени, однако нам также необходимо сохранить ее при отсоединении от сервера. Мы могли бы использовать простую временную таблицу, но она будет уничтожена, как только мы отсоединимся. Мы могли бы, например, использовать постоянную таблицу в другой базе — masters. Но это привело бы к «засорению» сервера ненужным постоянным объектом. Кроме того, все запущенные трассировки будут автоматичес-
sp_list_trace 507 ки остановлены при остановке сервера. Нам не нужен объект, который сохраняется между перезагрузками сервера. Все, что нам требуется, — это механизм сохранения, существующий, пока запущен текущий экземпляр SQL Server. Нам необходим механизм автоматического удаления результатов при перезагрузке, чтобы по ошибке мы не сочли, что та или иная трассировка активна, если в действительности она уже не работает. Вот почему я решил использовать постоянную таблицу в Tempdb: эта база данных создается каждый раз при перезагрузке сервера и поэтому она идеально подходит для достижения этой цели. Таблица TraceQueue является лучшим решением — это временный объект, который сохраняется между сессиями и другими действиями пользователя. sp_list_trace Поскольку утилиты трассировки разработаны так, что вы можете запустить трассировку и оставить ее активной, даже закрыв соединение, возможно, вам пригодится средство для проверки статуса работающих трассировок. Для этого и предназначена процедура sp_list_trace. Вот ее код. Листинг 21.6. Процедура sp_list_trace USE master GO IF OBJECTJQ('spJist_trace') IS NOT NULL DROP PROC sp_list_trace GO CREATE PROC spjistjtrace (aTraceld varcharA0)=NULL /* Обьект: sp_list_trace Описание: Lists the currently running traces. Использование: sp_list_trace @TraceId -- Идентификатор запущенной ранее трассы (необязательный параметр) Выходные данные: (None) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 2.0 $ Пример: EXEC sp_list_trace -- Список запущенных в настоящий момент трасс Created: 1999-04-01. $Modtime: 2000-12-16 $. */ AS SET NOCOUNT ON IF @TraceId=7?' GOTO Help DECLARE @T int SET @T=CAST(@TraceId AS int) IF @BJECT_ID('tempdb..TraceQueue') IS NOT NULL) BEGIN IF (@T IS NULL) BEGIN продолжение ё>
508 Глава 21. Хранимые процедуры для администрирования Листинг 21.6 {продолжение) DECLARE tc CURSOR FOR SELECT * FROM tempdb..TraceQueue FOR READ ONLY DECLARE @tid int. @tname varcharB0). Otfile sysname OPEN tc FETCH tc INTO @tid. @tname. @tfile IF @@ROWCOUNT<>0 BEGIN WHILE @@FETCH_STATUS=0 BEGIN SELECT Traceld. TraceName. TraceFile FROM tempdb..TraceQueue WHERE Traceld=@tid SELECT * FROM ::fn_trace_getinfo(@tid) FETCH tc INTO @tid. @tname. Otfile END END ELSE PRINT 'No traces in the trace queue.' /*Очередь трасс пуста*/ CLOSE tc DEALLOCATE tc END ELSE BEGIN SELECT Traceld. TraceName. TraceFile FROM tempdb..TraceQueue WHERE TraceId=@T SELECT * FROM : :fn_trace_getinfo((?T) END END ELSE PRINT 'No traces to list.' /*Список трасс пуст*/ RETURN О Help: EXEC spjjsage @objectname='sp_list_trace',@desc='Lists the currently running traces.'. @parameters='@TraceId -- the ID number of a previously started trace (optional)', @author='Ken Henderson'. @email = 'khen(akhen.com'. @version='2', @revision='0', @datecreated='19990401'. @datelastchanged='20001216'. (aexample='EXEC sp_list_trace -- Lists the currently running traces' RETURN -1 GO exec sp_list_trace (Результаты сокращены) Traceld TraceName TraceFile 4 tsqltrace c:\TEMP\tsqltrace_20001204012356767 traceid property value 4 4 4 4 4 1 2 3 4 5 C\J C:\TEMP\tsqltrace 20001204012356767 5 NULL 1 Процедура sp_list_t race использует системную функцию : :fn_trace_getinfo() для получения основной информации по каждой трассировке. Таблица результатов листинга 21.6 с первой колонкой traceid сформирована функцией : :fn_trace^ getinf o(). Вы можете взять имя трассировки из первой таблицы результатов (источник которой Tempdb. .TraceQueue) и передать ее процедуре sp_stop_trace, если захотите ее остановить. Эти процедуры представляют собой жизнеспособную замену утилиты Profiler и могут быть использованы в вашей работе. Совсем не трудно объединить три процедуры в одну главную и не держать в голове все три. Это снова остается читателю в качестве упражнения.
sp_proc_runner 509 sp_proc_runner Запустить, остановить и просмотреть трассировки с помощью хранимых процедур удобно. Но что делать, если вам требуется осуществлять запуск и остановку по расписанию? Что если бы вы захотели трассировать до тех пор, пока на сервере не произойдет определенное событие, или до определенного времени? Принимая во внимание то, что размер файла трассировки может стать очень большим, вы можете планировать трассировку так, чтобы она вовсе не запускалась (или запускать ее с очень небольшим набором отслеживаемых событий) в период высокой загрузки системы. Для достижения этих целей и была разработана процедура sp_proc_runner. Она служит для запуска какого-либо кода: это может быть другая хранимая процедура или пакетный файл T-SQL — для sp_proc_runner не имеет значения. Это миниатюрный планировщик, который запускает заданный вами код в установленное время или при соблюдении определенных условий в системе (например, длинная транзакция или блокированный spid). Sp_proc_runner также может запускать команды, повторяя их в цикле. Другими словами, эта процедура может останавливать процесс выполнения команд по истечении указанного времени и перезапускать его. Она может повторять эти действия до тех пор, пока не истечет заданное вами время или пока на сервере не будет выполнено поставленное вами условие. Процедура sp_proc_runner также может управлять файлами, которые являются результатом работы выполняемой вами команды. Вы можете сообщить ей, сколько файлов должно быть сохранено и где их требуется разместить. Если исполняемая процедура имеет выходные данные, sp_proc_runner может автоматически управлять выходными файлами. Предположим, что вы используете sp_proc_runner для запуска sp_start_trace. Возможно, вы захотите через определенные промежутки времени останавливать трассировку и запускать ее заново. Вы можете позволить управлять этими файлами либо sp_start_t race, либо sp_proc_runner. Вы можете дать команду sp_proc_runner сохранять только последние десять трассировочных файлов, и она в точности выполнит ваше требование. Она будет проверять файлы при их создании и при циклическом повторении трассировки при необходимости удалять более старые файлы, придерживаясь установленного вами ограничения. Изначально я разработал sp_proc_runner для запуска sp_start_trace, по в действительности она может запускать все, что угодно. Вы можете назначить свой собственный код и определенные вами условия остановки — об остальном позаботится sp_proc_runner. Вот ее код. Листинг 21.7. Процедура sp_proc_runner USE master GO IF OBJECTJD('sp_proc_runner') IS NOT NULL DROP PROC sp_proc_runner GO CREATE PROC sp_proc_runner @StartCmd nvarcharD000)=7?', @StartTime char(8)=NULL. (aStopCondition nvarcharD000)=NULL. PStopMessage nvarcharD000)='Stop condition met.'. продолжение &
510 Глава 21. Хранимые процедуры для администрирования Листинг 21.7 {продолжение) @IterationTime char(B)=NULL. @Duration char(B)=NULL. @StopCmd nvarcharD000)=NULL, (apollinglnterval char(8)='00:00:10'. MutputDir sysname=NULL, @OutputFileMask sysname=NULL. @NumFiles int=16 /* Объект: sp_proc_runner Описание: Запускает, периодически повторяя, пакет команд TSQL или процедуру указанного времени течение Использование: sp_proc_runner @StartCmd @StartTime @StopCondition @StopMessage @IterationTime @PollingInterval @Duration @StopCmd MutputDir @OutputFileMask @NumFiles nvarchar char(8) nvarchar nvarchar char(8) char(8) char(8) nvarchar sysname sysname int D000) -D000) -D000) ■D000) default: default: default: default: default: default: default: default: default: default: default: (none) NULL NULL NULL NULL 00:00 NULL NULL NULL NULL 16 -- команда TSQL или процедура для запуска -- время начала обработки -- условие, при котором выполняется остановка OStartCmd -- сообщение, выводимое при выполнении условия остановки -- интервал времени между итерациями :10 -- интервал времени между проверками выполнения @StopCondition -- общее время, в течение которого выполняется @StartCmd -- команда TSQL или процедура, запускаемая для остановки @StartCmd -- директория для размещения выходного файла (если это уместно, процедура должна поддерживать параметр @FileName ) -- маска файлов для выходных файлов (если это уместно, процедура должна поддерживать параметр @FileName ) -- количество выходных файлов (если это уместно, процедура должна поддерживать параметр @Fi1eName ) Выходные данные: (нет) $Author: Ken Henderson $. Email: khen@khen.com $Revision: 2.0 $ Пример: EXEC sp_proc_runner @StartCmd=N'EXEC sp_start_trace '. @StopCondition=N'OBJECTJD("tempdb..stoptab") IS NOT NULL', @StopMessage=N'Trace stopped'. @IterationTime='00:30:00'. @StopCmd=N'EXEC sp_stop_trace '. @OutputDir='c:\temp',@OutputFileMask='sp_trace*.trc'. @NumFi1es=16 EXEC sp_proc_runner @StartCmd=N'EXEC sp_start_trace '. @IterationTime='00:30:00'. @Duration='12:00:00'. @StopCmd=N'EXEC sp_stop_trace '. @OutputDir='c:\temp',@0utputFi1eMask='sp_trace*.trc'. @NumFiles=10 Created: 1999-04-01. */ AS SET NOCOUNT ON $Modtime: 2000-12-16 $. IF @StartCmd=7?' GOTO Help
sp_proc_runner 511 -- Минимальная проверка параметров IF COALESCE(@Duration. @StopCondition) IS NULL BEGIN RAISERRORCYou must supply either the @Duration or the @StopCondition parameter.' .16,10) / *Вы должны указать либо продолжительность трассировки @Duration. либо условия остановки @StopCondition */ RETURN -l END IF (aOutputFileMask='*' BEGIN RAISERRORCYou may not specify an empty file mask.' .16.10)/*Вы не можете указывать пустую файловую маску */ RETURN -1 END IF (@OutputOir IS NOT NULL) AND (@OutputFileMask IS NULL) BEGIN RAISERRORCYou must supply a file mask when supplying a directory.' .16.10) /*При указании директории. Вы должны указать файловую маску */ RETURN -1 END -- 1дем до наступления времени старта IF @StartTime IS NOT NULL WAITFOR TIME @StartTime -- Объявление и инициализация переменных DECLARE @Stop int. @i int. @EndTime datetime, @CurDate datetime, @CurDateStr varcharB5), @FName sysname. @DelCmd varcharB55), @OutputDirCmd varcharB55). @SCmd nvarcharD000), @IterationDateTime datetime SET @CurDate=getdate() SET @EndTime=@CurDate+(aDuration SET (?Stop=CASE WHEN @CurDate >= @EndTime THEN 1 ELSE 0 END -- @Duration возможно, равна 00:00:00, ? SET @i=0 SET @StopCondition='IF C+(astopCondition+') RAISERRORC' "+@StopMessage+" ' .11.1)' IF @OutputDir IS NOT NULL BEGIN -- Если мы собираемся формировать имена файлов, удаляем старые файлы IF RIGHT((aOutputDir.l)<>'\' SET @OutputDir=(aOutputDir+'\' SET @DelCmd='DEL '+@OutputDir+(aDutputFileMask EXEC xp_cmdshell @DelCmd, no_output -- Удаляем файлы по маске SET (?OutputDirCmd='DIR ,+(aOutputDir+(aOutputFileMask+' /В /ON' -- Подготавливаемся к выводу содержимого Dir (ниже) END --IF (@Stop<>l) AND (@StopCondition IS NOT NULL) -- Проверяем, не выполнено ли условие остановки - в этом случае не запускаем -- EXEC @Stop=sp__executesql @StopCondition WHILE (@Stop=0) BEGIN IF @OutputDir IS NOT NULL BEGIN -- Генерируем имя файла, используя текущие время и дату SET (aCurDateStr=C0NVERT(CHAR(B),getdate().112) + REPLACE(C0NVERT(varcharA5) ,getdate(), 114),':'.") SET @FName=REPLACE(@OutputFileMask.'*',@CurDateStr) SET (asCmd=(?StartCrad+CAS£ CHARINDEX('@'.@StartCrad) WHEN 0 THEN " ELSE '.' END+' @FileName="'+CAST(@OutputDir+(aFName as nvarcharB55))+" " END ELSE SET @SCmd=(aStartCmd EXEC sp_executesql @SCmd -- Запускаем команду SET merationDateTin^getdateO+ISNULL^IterationTime.'23:59:59.999') WHILE (@Stop=0) AND (getdate()<@IterationDateTime) BEGIN продолжение #
512 Глава 21. Хранимые процедуры для администрирования Листинг 21.7 {продолжение) IF @РоШпдInterval IS NDT NULL -- Устанавливаем отсрочку опроса выполнения условия WAITFOR DELAY (apollinglnterval SET @Stop=CASE WHEN getdateO >= №idTime THEN 1 ELSE 0 END -- Проверка общего времени выполнения IF (@Stop<>l) AND (@StopCondition IS NOT NULL) -- Проверка условия остановки EXEC @Stop=sp_executesql @StopCondition END IF @StopCmd IS NOT NULL -- Выполняем команду остановки при ее наличии EXEC sp_executesql @StopCmd SET @i«@i+l IF (@OutputDir IS NOT NULL) AND (@i>@NumFiles) BEGIN -- Избавляемся от ненужных файлов CREATE TABLE #files (fname varcharB55) NULL) INSERT #files EXEC master..xp_cmdshell @0utputDirCmd SELECT TOP 1 @DelCmd='DEL '+(aOutputDir+fname FROM #files WHERE fname IS NOT NULL ORDER BY fname IF (a@ROWCOUNT<>0 EXEC master..xp_cmdshell @DelCmd. no_output DROP TABLE #files END END " RETURN 0 Help: /* Код сокращен */ RETURN -1 GO EXEC sp_proc_runner @StartCmd=N'EXEC sp_start_trace '. @IterationTime='00:30:00'. @Duration='12:00:00', @StopCmd=N'EXEC sp_stop_trace '. @OutputDir='C:\TEMP' ,C0utputFileMask='sp_trace*.trc'. (?NumFiles=10 В этом примере мы используем sp_proc_runner для запуска sp_start_t race. В общей сложности она будет выполняться в течение двенадцати часов и будет останавливать и заново начинать трассировку каждые 30 минут. Она сохранит трассировочные файлы в C:\TEMP с именами sp_t race*. t re, в которых * будет изменена на текущие дату и время. В любое время она будет хранить максимум десять файлов и по необходимости удалять старые, чтобы не превысить установленный лимит. Взгляните еще на один пример. Листинг 21.8. Процедура sp_proc_runner может проверять условие остановки на сервере EXEC sp_proc_runner @StartCmd=N'EXEC sp_start_trace '. CStopCondition=N'EXISTS(SELECT * FROM sysprocesses WHERE blocked<>0 and waittime>60000)'.
sp_create_backupJob 513 @StopMessage=N'Long-term block detected'. @IterationTime='00:30:00'. @StopCmd=N'EXEC sp_stop_trace '. @OutputDir='c:\temp',@OutputFileMask='sp_trace*.trc'. @NumFiles=16 В этом примере sp_proc_runner снова запускает sp_start_trace, но на этот раз она отслеживает длительные операции на сервере, которые длятся больше 60 секунд. Как только происходит такое событие, sp_proc_runner останавливает трассировку, выводит сообщение и прекращает свою работу. Server: Msg 50000. Level 11. State 1. Line 1 Long-term block detected Trace started. The trace filename is : C:\temp\sp_trace20010704030737887.trc. Deleted trace queue 1. The trace output filename is: c:\temp\sp_trace20010704030737887.trc. Обладая большим потенциалом (выполнение кода в указанный промежуток времени, до наступления определенного события и т. д.), — процедура sp_proc_runner предоставляет возможность более гибкого планирования работы на SQL Server. Не забывайте, что вы не ограничены запуском sp_start_trace и можете использовать ее для запуска любого типа нужных вам T-SQL-команд или хранимых процедур. sp_create_backup_job Назначение процедуры sp_create_backup_job соответствует ее имени: она создает для вас задание резервного копирования. Она использует утилиту SQLMAINT SQL Server для того, чтобы создать задание, осуществляющее резервное копирование данных и файлов журнала указанной базы данных. На вход процедуры подается шесть параметров, приведенных в табл. 21.1. Таблица 21.1. Параметры процедуры sp_create_backupJob Параметр Тип Значение Назначение по умолчанию None Определяет имя базы данных для резервного копирования None Определяет адрес оператора NET SEND 200000 Определяет время запуска задания " Определяет имя создаваемого плана поддержки Определяет имя для той части задания, в которой будет архивироваться файл данных " Определяет имя для той части задания, в которой будет архивироваться файл журнала Ниже приведен код процедуры sp_create_backup_job. @dbname sysname @OperatorNetSendAddress sysname @ScheduledStart int @PlanName sysname @DataBackupName sysname @LogBackupName sysname 17 ^av ОЯЗ
514 Глава 21. Хранимые процедуры для администрирования Листинг 21.9. Процедура sp_create_backupJob USE master GO IF OBJECT_IDCdbo.sp_create_backup_job') IS NOT NULL DROP PROC dbo.sp_create_backup_job GO CREATE PROC dbo.sp_create_backup_job (Pdbname sysname, @OperatorNetSendAddress sysname. @ScheduledStart int=200000. @PlanName sysname=", @DataBackupName sysname='\ @LogBackupName sysname»'' AS DECLARE @execstr varchar(8000), @JobID uniqueidentifier, OStepID int. @devname sysname DECLARE (apianID uniqueidentifier. @DataCmd varchar(8000). @LogCmd varchar(8000) SET @PlanName='Daily Backup for '+ @dbname SET @DataBackupName='Data backup for '+@dbname SET @LogBackupName='Log backup for '+@dbname -- Удаляем оператор, если он уже существует IF EXISTS(SELECT * FROM msdb.dbo.sysoperators WHERE name - 'Oper') EXEC msdb.dbo.sp_delete_operator 'Oper' -- Добавляем оператор EXEC msdb.dbo.sp_add_operator @name = 'Oper', @enabled = 1. @email_address =''. @pager_address = '', @weekday_pager_start_time - 090000, @weekday_pager_end_time = 210000, @pager_days = 127, @netsend_address=@rjperatorNetSendAddress -- Удаляем задание из sysdbmaintplans и связанные таблицы (при наличии) SELECT @PlanID = planjd FROM msdb.dbo.sysdbmaintplans WHERE plan_name=@PlanName; IF @@ROWCOUNT<>0 BEGIN DECLARE job CURSOR FOR SELECT jobjd FROM msdb.dbo.sysdbmaintplanjobs WHERE plan_id=CPlanID OPEN job FETCH job INTO OUoMD WHILE (@@FETCH_STATUS=0) BEGIN EXEC msdb.dbo.sp_delete_job @J0BID FETCH job INTO @JobID END DEALLOCATE job DELETE msdb.dbo.sysdbmaintplanjiistory WHERE planjd =@PlanID DELETE msdb.dbo.sysdbmaintplanjobs WHERE planjd =@PlanID DELETE msdb.dbo.sysdbmaintplanjatabases WHERE planjd =@PlanID DELETE msdb.dbo.sysdbmaintplans WHERE planjd =@PlanID END -- Генерируем новый GUID. затем вставляем его в sysdbmaintplans SELECT @PlanID = NEWIDO INSERT msdb.dbo.sysdbmaintplans (planjd. planjiame. maxJistory_rows, remote Ji story_server, max_remotejistory_rows) VALUES (@PlanID. (apianName. 1000. N". 0) DELETE msdb.dbo.sysdbmaintplanjobs WHERE planjd = @PlanID
sp_create_backupJob 515 -- Устанавливаем вызовы для данных и журнала xp_sqlmaint, которые мы впоследствии используем •.: SET @DataCmd='EXEC master.dbo.xp_sqlmaint "-PlanID '+CAST(@P1anIO AS varcharC6))+' - WriteHistory -VrfyBackup -BkUpMedia DISK -BkUpDB -UseDefDir -BkExt "ВАК" -DelBkUps 7days SET @LogCmd='EXEC master.dbo.xp_sqlmaint "-PlanID '+CAST(CPlanID AS varcharC6))+' - WriteHistory -VrfyBackup -BkUpMedia DISK -BkUpLog -UseDefDir -BkExt "TRN" -DelBkUps 7days'''; -- Удаляем задание, если оно уже существует SELECT @JobID = jobjd FROM msdb.dbo.sysjobs WHERE name=@DataBackupName IF ((a@ROWCOUNT>0) BEGIN -- He удаляем, если это мультисерверное задание IF (EXISTS (SELECT * FROM msdb.dbo.sysjobservers WHERE (job_id=(ajobID) AND (serverjd <> 0))) BEGIN RAISERROR ('Unable to create job because there is already a multi-server job with the same name.' .16,1) /Невозможно создать задание, поскольку уже существует мультисерверное задание с таким же именем*/ END ELSE -- Удаляем задание EXEC msdb.dbo. spjeletejob @job_id=(ajobID END -- Добавляем задание для архивации EXEC msdb.dbo.sp_add_job @job_name = @DataBackupName, (^enabled = 1, @category_id=3. @description = @DataBackupName, @notify_level_eventlog = 2. @notify_level_netsend = 2, @notify_netsend_operator_name='Oper', @delete_level = 0 SELECT @JoMD=job_id FROM msdb.dbo.sysjobs WHERE name=@DataBackupName -- Добавляем задание в sysdbmainplan_jobs INSERT msdb.dbo.sysdbmaintplanjobs VALUES (@PlanID. @JobID) -- Устанавливаем расписание выполнения задания EXEC msdb.dbo.sp_add_jobschedule @job_id=@JobID, @name = 'ScheduledBackup'. @freq_type = 4. -- ежеденевно @freq_interval = 1, @active_start_time = @ScheduledStart -- Добавляем базу данных в sysdbmaintplan_databases IF NOT EXISTS(SELECT * FROM msdb.dbo.sysdbmaintplan_databases WHERE planjd = @PlanID AND database_name = @dbname) INSERT msdb.dbo.sysdbmaintplan_databases (planjd. databasejiame) VALUES (@PlanID, @dbname) -- Добавляем часть по архивации EXEC msdb. dbo. sp_add_jobstep @job_id=(aJobID, @step_name='DataBackup'. (asubsystem='TSQL', @command=@DataCmd. @f 1 ags=4.@on_success_acti on=l -- Связываем задание с сервером, на котором оно будет выполняться EXEC msdb.dbo.sp_add_jobserver @job_id=@JobID -- Добавляем задание и необходимые действия для архивации журнала продолжение ■&
516 Глава 21. Хранимые процедуры для администрирования Листинг 21.9 {продолжение) IF ((adbnameo'master') AND (DATABASEPROPERTYCOdbname. 'IsTruncLog' )=0) BEGIN -- Удаляем задание, если оно уже существует SELECT @JobID - jobjd FROM msdb.dbo.sysjobs WHERE name=@LogBackupName IF (@@ROWCOUNT>0) BEGIN -- He удаляем, если это мультисерверное задание IF (EXISTS (SELECT * FROM msdb.dbo.sysjobservers WHERE (jobJd=(?JobID) AND (serverjd <> 0))) BEGIN RAISERROR ('Unable to create job because there is already a multi- server job with the same name.',16,1) /Невозможно создать задание, поскольку уже существует мультисерверное задание с таким же именем*/ END ELSE -- Удаляем задание EXEC msdb.dbo.spjeletejob @jobJd=(aJobID END авляем задание для архивации EXEC msdb.dbo.sp_add_job @job_name = @LogBackupName. ' (^enabled = 1, @category_id=3. @description = @LogBackupName. @notify_level_eventlog = 2. @notify_level_netsend = 2, @notify_netsend_operator_name='Oper'. @delete_level = 0 SELECT @JoMD=job_id FRDM msdb.dbo.sysjobs WHERE name=@LogBackupName -- Добавляем задание в sysdbmainplanjobs INSERT msdb.dbo.sysdbmaintplanjobs VALUES (OPlanlD. @JobID) -- Устанавливаем расписание выполнения задания EXEC msdb.dbo.sp_addjobschedule @job_id=@JobID. @name = 'ScheduledLogBackup', @freq_type = 4, -- ежедневно @freq_interval = 1, @active_start_time = @ScheduledStart -- Добавляем базу данных в sysdbmaintplan_databases IF NOT EXISTSCSELECT * FROM msdb.dbo.sysdbmaintplan_databases WHERE planjd = @PlanID AND databasejiame = @dbname) INSERT msdb.dbo.sysdbmaintplan_databases (planjd, databasejiame) VALUES (@PlanID. (Pdbname) EXEC msdb.dbo.sp_add_jobstep @jobJd=@JobID. @step_name='LogBackup'. @subsystem='TSQl'. @command=@LogCmd. @flags=4,@on success action=l END -- Связываем задание с сервером, на котором оно будет выполняться EXEC msdb.dbo.sp_add_jobserver @job_id=@JobID Как вы видите, процедура несколько запутанная, однако избежать усложнения невозможно, поскольку необходимо создать и управлять планом поддержки SQL Server. С точки зрения удобства использования, вы можете просто вызвать процедуру и почти забыть о деталях.
sp_diffdb 517 sp_diffdb Последний пример представляет собой своеобразный итог главы. В нем используются две процедуры, рассмотренные выше, для того чтобы реализовать возможность, которой, на удивление, нет в самом SQL Server. Речь идет о способности обнаружения различий между двумя базами данных. Почти каждую неделю я получаю, по крайней мере, одно сообщение по электронной почте, содержащее следующий вопрос: «Как сравнить две версии одной базы данных?» Существуют инструменты сторонних производителей, обеспечивающие эту функциональность, но эти инструменты довольно дороги. Мне самому часто требуется такая функция, поэтому я написал хранимую процедуру, которая может сравнить схему одной базы данных с другой. Она выполняет эту задачу полностью автоматически и отличается удивительной функциональностью. Как она работает? Вспомните процедуры sp_diff и sp_generate_sc r ipt, рассмотренные в начале главы. Sp_dif fdb просто использует sp_generate_script для получения сценариев тех баз данных, которые вы хотите сравнить, и затем вызывает sp_dif f для нахождения различий между ними. Неплохо, не правда ли? Вот ее код. Листинг 21.10. Процедура sp_diffdb USE master GO IF OBJECT_ID('sp_diffdb') IS NOT NULL DROP PROC spjiffdb GO CREATE PROC spjjiffdb @DB1 sysname-7?'. @DB2 sysname=NULL. @TempPath sysname='C:\TEMP'. @server sysname='(local)'. -- Имя сервера, с которым будет установлено соединение @username sysname='sa'. -- Имя пользователя, под учетной записью которого будет производиться соединение (по умолчанию'эа') @password sysname=NULL, -- Пароль пользователя @trustedconnection bit=l -- Доверительное соединение для подключения к серверу /* Обьект: sp_diffdb Описание: Returns the differences between two text files as a result set (uses VSS) Использование: sp_diffdb @filel=full path to first file, @fi 1 e2=ful 1 path to second file Выходные данные: (None) $Author: Ken Henderson $. Email: khen@khen.com JRevision: 1.0 $ Пример: sp_diffdb 'c:\customers.sql'. 'c:\customers2.sql' Created: 2001-01-14. $Modtime: 2001-01-16 $. */ AS SET N0C0UNT ON IF (C0ALESCE(CDBl+(a0B2. 7?' )='/?') GOTO Help DECLARE @cmd varchar(lOOO), @cmdout varchar(lOOO). @trustcon char(l). @filel sysname, @file2 sysname продолжение &
518 Глава 21. Хранимые процедуры для администрирования Листинг 21.10 {продолжение) SET @trustcon=CAST(@trustedconnection AS char(D) IF RIGHT(@TempPath.l)<>'\' SET (aTempPath=(aTempPath+'\' SET @filelHaTempPath+@DBl+'.SQL' SET @cmd=@DBl+'. .sp_generate_script @includeheaders=0. @resultset=0. @outputname='''+@filel+'''. @server='''+@server+''', @username='''+@username+''''+ISNULL(', @password='''+@password+''''.'')+'. @trustedconnection='+@trustcon EXEC(@cmd) print @cmd SET @file2=@TempPath+@DB2+'.SQL' SET @cmd=@DB2+'..sp_generate_script @includeheaders=0. @resultset=0. @outputname='''+@file2+''', @server='''+@server+''', @username='''+@username+''''+ISNULL('. @password='' '+@password+''".")+'. @trustedconnection='+@trustcon EXEC(@cmd) EXEC sp_diff @filel. @file2 RETURN 0 Help: EXEC sp_usage @objectname='sp_diffdb'. @desc='Returns the differences between two text files as a result set (uses VSS)', @parameters='@filel=full path to first file, @file2=fullpath to second file'. @author='Ken Henderson', @email='khen@khen.com'. @version='l',@revision='0', @datecreated='20010114', @datelastchanged='20010116', @example='sp_diffdb "c:\customers.sql". "c:\customers2.sql" ' RETURN -1 GO Поскольку sp_diffdb вызывает sp_generate_script, которая по очереди соединяется с серверами баз данных, мы должны передать в процедуру имя и пароль. Кроме того, два ключевых параметра представляют имена сравниваемых баз данных. Ниже приведен пример вызова sp_dif fdb. Листинг 21.11. Процедура sp_diffdb дает отчет о различиях между двумя базами данных EXEC sp_diffdb 'northwind','northwind5' diff :\TEMP\northwind.SQL :\TEMP\northwind5.SQl CREATE TABLE [dbo].[cust] ( [CustNo] [int] IDENTITY A, 1) NOT NULL , [City] [varchar] C0) CDLLATE SQL_Latinl_General_CPl_CI_AS NULL . [State] [varchar] A0) COLLATE SQL_Latinl_General_CPl_CI_AS NULL ) ON [PRIMARY] GO CREATE CLUSTERED INDEX [citystate] ON [dbo].[cust]([City]. [Sta GO Здесь мы видим, что в структуре базы данных Northwind5 отсутствует таблица cust. Как я уже говорил раньше, обсуждая процедуру sp_diff, второй файл, пере- Diffing: С Agai 94 95 96 97 98 99 100 101 102 103 104 nst: С Del: Del: Del: Del: Del: Del: Del: Del: Del: Del: Del:
Итоги 519 даваемый процедуре, рассматривается как главная копия: результат работы sp_dif f перечисляет, что необходимо сделать, чтобы первый файл привести в соответствие со вторым. Из результатов мы можем заключить, что в базе данных Northwind существует таблица cust, в то время как в Northwind5 она отсутствует. Теперь посмотрим на результаты в случае сравнения идентичных баз. Листинг 21.12. Процедура sp_diffdb может определить отсутствие различий между двумя базами данных EXEC spjdiffdb 'northwind','northwind4' diff Diffing: C:\TEMP\northwind.SQL Against: C:\TEMP\northwind4.SQL No differences. He являясь инструментом с графическим интерфейсом, sp_dif fdb обладает способностью отображать различия между двумя разными базами данных, что является сильной особенностью и может пригодиться во многих ситуациях. Итоги В этой главе вы узнали: ■ как создать административные хранимые процедуры; ■ насколько гибким может оказаться Transact-SQL; ■ как автоматизация СОМ и доступ к операционной системе добавляют Trans- act-SQL специальные возможности. Я надеюсь, что приведенные в этой главе процедуры вдохновят вас на создание собственных хранимых процедур, необходимых для реализации задач администрирования.
Недоку менти рова иные возможности Transact-SQL Для хорошего инженера не использовать знания — все равно, что не иметь их. Стив Маккопнелл '• Лучше воздержаться от использования недокументированных возможностей, если есть другой способ решить проблему. Недокументированные возможности, как правило, являются недокументированными по важной причине: они могут быть небезопасны в использовании при определенных обстоятельствах, они могут быть изменены или удалены в последующих версиях продукта. Неправильное использование недокументированных возможностей ведет к потере данных без получения помощи со стороны производителя. Поэтому будьте осторожны, используя недокументированные хранимые процедуры, команды, функции и синтаксис, описанный далее. Помните, что вы используете их на свой страх и риск. Восторг от открытия недокументированной возможности — ничто по сравнению с огорчением от осознания, что функционирование рабочего сервера, от которого зависят многие клиенты, было нарушено. Будьте дальновидны. Обычно использование недокументированных возможностей в рабочей среде не оправдывает связанный с этим риск. Если вы, взвесив все, решите использовать недокументированные возможности — помните, что они могут быть изменены или удалены из продукта без предупреждения. Например, поведение функции PWOENCRYPT() было изменено при переходе от версии SQL Server 6 к версии SQL Server 7. Поэтому в версии SQL Server 7 это нарушило работу кода, зависящего от старого варианта этой функции. С точки зрения терминологии «недокументированный» означает «неподдерживаемый». Не стоит рассчитывать на поддержку недокументированных возможностей со стороны производителя. Компания Microsoft намеренно оставила некоторые возможности неописанными в документации, так как изменит или удалит их в последующих версиях SQL Server. Это делается для того, чтобы можно было менять SQL Server без последствий для клиентских приложений. Если код прило- McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 27. 22
Недокументированные процедуры 521 жения основан на недокументированных возможностях, то следует понимать, что приложение может перестать работать ввиду изменений в SQL Server, а также может вести себя некорректно потому, что недокументированные возможности используются не по назначению. Что есть «недокументированный»? Возможность считается недокументированной, если она не описана в документации (Books Online). Информацию о некоторых недокументированных возможностях можно найти в открытой для общего доступа документации Microsoft, отличной от Books Online. В контексте данной главы можно сказать: если что-либо не описано в Books Online, — значит, оно не документировано. Кроме недокументированных процедур в этой главе речь пойдет о недокументированных командах DBCC, недокументированных функциях и недокументированных флагах трассировки. Ни один из представленных списков не является исчерпывающим, однако вы, вероятно, обнаружите в них одну или две неизвестные вам ранее недокументированные возможности. Независимо от того, используете вы их или нет, знать о них надо, так как они дают представление о внутренней работе SQL Server. Поэтому в книгу и был включен раздел о недокументированных возможностях. Недокументированные процедуры Существует более сотни недокументированных процедур, если не считать недокументированные процедуры репликации. Многие из них перечислены далее. Я не смог описать все процедуры по следующим причинам. 1. Их слишком много, чтобы описать каждую. Поэтому не приведены недокументированные процедуры, относящиеся к репликации. Репликация требует особого внимания. 2. Некоторые недокументированные процедуры так незначительны и так мало расширяют набор команд Transact-SQL, что нет смысла писать о них. 3. Некоторые недокументированные процедуры ведут себя так нестабильно и так сильно зависят от внешнего по отношению к SQL Server кода (например, Enterprise Manager или SQL-DMO), что не могут быть использованы. Они мало полезны для разработчика. (Важно дать всеобъемлющую картину, но не увлекаться.) Каждая из нижеследующих процедур не описана в Books Online, но многие из них выполняют полезные функции. Вам решать, стоит ли их функциональность риска, связанного с их использованием. sp_checknames [@mode] Проверяет важнейшие системные таблицы на предмет наличия в имени символов, не входящих в ASCII.
522 Глава 22. Недокументированные возможности Transact-SQL sp_delete_backuphistory @oldest_date Очищает историю создания резервных копий до указанной даты. msdb..sp_delete_backuphistory @oldest_date datetime sp_enumerrorlogs Перечисляет текущие файлы журнала ошибок сервера. master.. sp_ (Результаты Archive # 6 5 4 3 2 1 0 enumerrorlogs сокращены) Date Log 06/28/2000 06/29/2000 06/29/2000 06/29/2000 06/29/2000 07/01/2000 07/01/2000 23:13 11:19 11:35 22:55 23:10 12:49 12:51 File Size (Byte) 3139 3602 3486 15998 3349 120082 3532 sp_enumoledbdatasources Перечисляет провайдеров OLEDB, доступных на сервере. sp_enumoledbdatasources sp_fixindex @dbname, @tabname, @indid Позволяет удалить/пересоздать индексы на системных таблицах. USE Northwind EXEC sp_dboption 'Northwind','single'.true EXEC sp_fixindex 'Northwind'. 'sysobjects', 2 EXEC sp_dboption 'Northwind'.'single'.false sp_gettypestring @tabid, @colid, @typestring output Отображает текстовое описание типа данных поля. declare @tabid int. @typestr varcharC0) SET @tabid=OBJECT_ID('authors') EXEC sp_gettypestring @tabid, 1, @typestr OUT SELECT @typestr (Результаты) varchar(ll) sp_MS_marksystemobject @objname Устанавливает бит (OxCOOODOOO), являющийся признаком того, что объект системный. Некоторые функции и команды DBCC выполняются некорректно, если
Недокументированные процедуры 523 вызваны не из системного объекта. Установка этого бита приводит к тому, что свойство IsMSShipped для объекта становится равным 1. sp_MS_marksystemobject 'sp_di г' sp_MS_upd_sysobj_category @pSeqMode integer Устанавливает или сбрасывает специальный системный режим, в котором все создаваемые объекты автоматически становятся системными. Значение параметр;!.-: SeqMode, равное 1, включает режим; равное 2 — выключает. Также sp^MS_upd_sysc: _ ategory позволяет создавать определяемые пользователем представления INFORM,' ". .SCHEMA. Более подробно об этом можно прочитать в главе 9. sp_MS_upd_sysobj_category 1 Sp_MSaddguidcol @source_owner, @source_table Добавляет в таблицу поле ROWGUIDCOL и помечает таблицу для репликации (для обратной операции следует использовать EXEC sp_MSunmarkreplinfo). sp_MSaddguidcolumn dbo.testguid sp_MSaddguidindex @source_owner, @source_table Создает индекс на поле ROWGUIDCOL таблицы. sp_MSaddgu1di ndex dbo. testuid sp_MSaddlogin_implicit_ntlogin @loginname Создает учетную запись SQL Server, соответствующую существующей учетной записи NT. sp_MSaddlogin_implicit_ntlogi n 'GoofyTingler' sp_MSadduser_implicit_ntlogin @ntname Создает пользователя базы данных, соответствующего существующей учетной записи NT. sp_MSadduseMmp1 i ci t_ntl ogi n ' GoofyTi ngl er' sp_MScheck_uid_owns_anything @uid Возвращает 1 в том случае, если пользователь — владелец объектов в текущей базе данных. DECLARE @res int. @uid int SELECT @uid=USER_ID() EXEC @res=sp_MScheck_ind_owns_anything @uid SELECT @res (Результаты сокращены) Server: Msg 15183. Level 16. State 1. Procedure sp_MScheck_uid_owns_anything, Line 17
524 Глава 22. Недокументированные возможности Transact-SQL The user owns objects in the database and cannot be dropped. Name type LastCustNo U 1 sp_MSdbuseraccess @mode='perm' | 'do', @qual=db name Возвращает список баз данных, которые доступны пользователю, и битовую маску доступа для каждой базы. sp_MSdbuseraccess @mode='db' name distribution master model msdb Northwind pubs rentman tempdb version 539 539 539 539 539 539 NULL 539 crdate 2000-11-28 20:46:14.293 2000-08-06 01:29 2000-08-06 01:40 2000-08-06 01:40 2000-08-06 01:41 2000-05-06 14:34 2000-06-30 16:32 2000-07-01 12:51 12.250 52.437 56.810 00.310 09.720 11.813 55.590 owner LEXUALIONIS sa sa sa sa LEXUALIONIS LEXUALIONIS sa sp_MSdbuserpriv @mode='perm' | 'serv' | 'ver' | 'role' Возвращает битовую маску привилегий пользователя. sp_MSdbuserpriv @mode='role' (Результаты) 73855 sp_MSdependencies @objname, @objtype, @flags int, @objlist Отображает зависимости объекта. spJISdependencies (Pobjname = 'titleauthor' (Результаты сокращены) oType oObjName oOwner oSequence 8 authors dbo 1 8 publishers dbo 1 8 titles dbo 2 sp_MSdrop_object [@object_id] [,@object_name] [,@object_owner] Обычно удаляет таблицу, представление, триггер или процедуру. sp_MSdrop object @object_name='authors2'
Недокументированные процедуры 525 sp_MSexists_file @full_path, @filename Проверяет, существует ли указанный файл операционной системы. Работает только в версии SQL Server 7. DECLARE @res int EXEC @res=sp_MSexists_file 'd:\readme.txt'. 'readme.txt' sp_MSforeachdb @commandl @replacechar = l?l[/@command2] [,@command3] [,@precommand][,@postcommand] Выполняет до трех команд для каждой базы данных сервера. Параметр @replacecha r будет заменен именем базы, (aprecommand и (apostcommand можно использовать для объединения результатов выполнения команд в один набор. EXEC sp_MSforeachdb 'DBCC CHECKDB(?)' EXEC sp_MSforeachdb @commandl='PRINT "Listing ?'". @command2='USE ? SELECT DB_NAME()' (Результаты сокращены) DBCC results for 'Northwind'. DBCC results for 'sysobjects'. There are 232 rows in 5 pages for object 'sysobjects'. DBCC results for 'sysindexes'. There are 162 rows in 7 pages for object 'sysindexes'. DBCC results for 'syscolumns'. There are 1056 rows in 23 pages for object 'syscolumns'. DBCC results for 'systypes'. There are 26 rows in 1 pages for object 'systypes'. DBCC results for 'syscomments'. There are 232 rows in 25 pages for object 'syscomments'. DBCC results for 'sysfilesl'. There are 2 rows in 1 pages for object 'sysfilesl'. DBCC results for 'syspermissions'. There are 72 rows in 1 pages for object 'syspermissions'. DBCC results for 'sysusers'. There are 14 rows in 1 pages for object 'sysusers'. DBCC results for 'sysproperties'. There are 0 rows in 0 pages for object 'sysproperties'. DBCC results for 'sysdepends'. There are 760 rows in 4 pages for object 'sysdepends'. DBCC results for 'sysreferences'. There are 14 rows in 1 pages for object 'sysreferences'. DBCC results for 'sysfulltextcatalogs'. There are 0 rows in 0 pages for object 'sysfulltextcatalogs'. DBCC results for 'sysfulltextnotify'. There are 0 rows in 0 pages for object 'sysfulltextnotify'. DBCC results for 'sysfilegroups'. There are 1 rows in 1 pages for object 'sysfilegroups'. DBCC results for 'Orders'. There are 830 rows in 26 pages for object 'Orders'. DBCC results for 'pubs'. ■ DBCC results for 'sysobjects'. There are 108 rows in 3 pages for object 'sysobjects'. DBCC results for 'sysindexes'. There are 54 rows in 3 pages for object 'sysindexes'. DBCC results for 'syscolumns'.
526 Глава 22. Недокументированные возможности Transact-SQL There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are DBCC resul There are Listing di 440 rows in 5 pages for object 'syscolumns' ts for 29 rows ts for 149 rows in 11 pages for object 'syscomments' ts for 2 rows ts for 69 rows ts for 13 rows ts for 0 rows ts for ts for 10 rows ts for 0 rows ts for 0 rows ts for 1 rows ts for 25 rows ts for 6 rows systypes'. in 1 pages for object syscomments'. systypes' sysfilesl'. n 1 pages for object 'sysfilesl'. syspermissions'. in 1 pages for object 'syspermissions'. sysusers'. in 1 pages for object 'sysusers'. sysproperties'. n 0 pages for object 'sysproperties'. sysdepends' 354 rows in 2 pages for object 'sysdepends' sysreferences in 1 pages for object sysfulltextcatalogs'. n 0 pages for object sysfulltextnotify'. n 0 pages for object sysfilegroups'. n 1 pages for object titleauthor'. in 1 pages for object stores'. n 1 pages for object ' 'sysreferences'. sysfulltextcatalogs' sysfulltextnotify'. sysfilegroups'. 'titleauthor'. stores'. stribution distribution Listing master master Listing model Listing msdb Listing model msdb Northwind Northwind Listing pubs pubs Listing rentman Listing rentman tempdb tempdb sp_MSfо reach table @commandl @replacechar = '?' [,@command2] [,@command3] [,@whereand] [,@precommand] [,@postcommand] Выполняет до трех команд для каждой таблицы в базе (дополнительно можно задать условие @whereand). Параметр @replacechar будет заменен именем таблицы.
Недокументированные процедуры 527 @precommand и (apostcommand можно использовать для объединения результатов выполнения команд в один набор. EXEC sp_MSforeachtable @commandl='EXEC spjielp [?]' EXEC sp_MSforeachtable @commandl='PRINT "Listing ? FROM ?',@whereand=' AND name like "title*'" @command2='SELECT * Name Orders Column name Owner dbo Type Type Created_datetime user table 2000-08-06 01:34:06.610 Computed Length Prec Scale N OrderlD CustomerlD EmployееID OrderDate RequiredDate ShippedDate ShipVia Freight ShipNaroe ShipAddress ShipCity ShipRegion ShipPostalCode ShipCountry Identity OrderlD RowGuidCol int nchar int datetime datetime datetime int money nvarchar nvarchar nvarchar nvarchar nvarchar nvarchar Seed 1 no no no no no no no no no no no no no no Increment Not 1 0 4 10 4 8 8 8 4 8 80 120 30 30 20 30 For Replication No rowguidcol column defined. DataJ ocated_on_fi1egroup PRIMARY index name indexjjescription CustomerlD nonclustered located on PRIMARY CustomersOrders nonclustered located on PRIMARY EmployeelD nonclustered located on PRIMARY EmployeesOrders nonclustered located on PRIMARY OrderDate nonclustered located on PRIMARY PK_Orders clustered, unique, primary key located on PRIMARY ShippedDate nonclustered located on PRIMARY ShippersOrders nonclustered located on PRIMARY ShipPostalCode nonclustered located on PRIMARY constraint_type constraintjiame delete_action updat DEFAULT on column Freight DF_Orders_Freight (n/a) (n/a) FOREIGN KEY FK Orders Customers No Action No Ac
528 Глава 22. Недокументированные возможности Transact-SQL FOREIGN KEY FOREIGN KEY PRIMARY KEY (clustered) FK_Orders_Employees No Action FK_Orders_Shippers No Action PK Orders (n/a) No Ac No Ac (n/a) Table is referenced by foreign key Northwind.dbo.Order Details: FK_Order_Details_Orders Table is referenced by views VI Listing [dbo].[Order Details] OrderlD ProductID UnitPrice Quantity Discount 10248 10248 10248 10249 10249 10250 10250 10250 10251 10251 10251 10252 10252 10252 10253 10253 10253 10254 10254 10254 10255 10255 10255 10255 10256 10256 10257 10257 10257 10258 11 42 72 14 51 41 51 65 22 57 65 20 33 60 31 39 49 24 55 74 2 16 36 59 53 77 27 39 77 2 14.0000 9.8000 34.8000 18.6000 42.4000 7.7000 42.4000 16.8000 16.8000 15.6000 16.8000 64.8000 2.0000 27.2000 10.0000 14.4000 16.0000 3.6000 19.2000 8.0000 15.2000 13.9000 15.2000 44.0000 26.2000 10.4000 35.1000 14.4000 10.4000 15.2000 12 10 5 9 40 10 35 15 6 15 20 40 25 40 20 42 40 15 21 21 20 35 25 30 15 12 25 6 15 50 0.0 0 0 0 0 0 0 0 5 5 0 5 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15000001 15000001 0000001E-2 0000001E-2 0 0000001E-2 0000001E-2 0 0 0 0 15000001 15000001 0 0 0 0 0 0 0 0 0 0 2 Listing [dbo].[Orders] OrderlD CustomerlD EmployeelD OrderDate 10248 10249 10250 10251 10252 10253 10254 10255 10256 V1NET TOMSP HANAR VICTE SUPRD HANAR CHOPS RICSU WELL1 5 6 4 3 4 3 5 9 3 1996-07-04 00:00:00 1996-07-05 00 1996-07-08 00 1996-07-08 00 1996-07-09 00 1996-07-10 00 1996-07-11 00 1996-07-12 00 1996-07-15 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Недокументированные процедуры 529 10257 10258 10259 10260 10261 10262 10263 10264 10265 10266 10267 10268 10269 10270 10271 10272 10273 10274 10275 10276 10277 10278 10279 10280 10281 10282 10283 10284 HILAA ERNSH CENTC ОН IK QUEDE RATTC ERNSH FOLKO BLONP WARTH FRANK GROSR WHITC WARTH SPUR RATTC QUICK VINET MAGAA TORTU MORGK BERGS LEHMS BERGS ROMEY ROMEY LI LAS LEHMS 4 1 4 4 4 8 9 6 2 3 4 8 5 1 6 6 3 6 1 8 2 8 8 2 4 4 3 4 1996-07-16 00 1996-07-17 00 1996-07-18 00 1996-07-19 00 1996-07-19 00 1996-07-22 00 1996-07-23 00 1996-07-24 00 1996-07-25 00 1996-07-26 00 1996-07-29 00 1996-07-30 00 1996-07-31 00 1996-08-01 00 1996-08-01 00 1996-08-02 00 1996-08-05 00 ■ 1996-08-06 00 1996-08-07 00 1996-08-08 00 1996-08-09 00 1996-08-12 00 1996-08-13 00 1996-08-14 00 1996-08-14 00 1996-08-15 00 1996-08-16 00 1996-08-19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 sp_MSget_oledbinfo ©server [,@infotype] [,@login] [,@password] Возвращает информацию о провайдере OLEDB для присоединенного сервера. sp_MSget_oledbinfo @server='pythia'. @login='sa' sp_MSget_qualified_name @object_id, @qualified_name OUT Переводит идентификатор объекта в полное имя. DECLARE @oid int. @obname sysname SET @oid=OBJECT_IDCCustomers') EXEC sp_MSget_qualified_name @oid, @obname OUT SELECT @obname (Результаты) [dbo].[Customers] sp_MSget_type ©tabid, @colid, @colname OUT, ©type OUT Возвращает имя и тип поля таблицы. DECLARE Otabid int. Pcolname sysname. @type nvarcharD000) SET @tabid=OBJECT ID('Customers')
530 Глава 22. Недокументированные возможности Transact-SQL EXEC sp_MSget_type Otabid. 1, @colname OUT. @type OUT SELECT @colname. @type (Результаты) CustomerlD ncharE) sp_MSguidtostr @guid, @mystr OUT Возвращает переменную типа uniqueidentif ier в виде строки. DECLARE @guid uniqueidentifier. @guidstr sysname SET @guid=NEWID() EXEC sp_MSguidtostr @guid, @guidstr OUT sp_MShelpindex @tablename [,@indexname] [,@flags] Выводит информацию об индексах. Результаты включают много данных, не предоставляемых процедурой sp_helpindex. spJIShelpindex 'Customers' (Результаты сокращены) name РК Customers City CompanyName Postal Code Region ContactName index 2073058421 WA Sys Country 7B905C75 ContactTitle status 18450 2097152 0 2097152 2097152 2097152 2 10485856 2097248 indid 1 2 3 4 5 6 7 8 9 OrigFi11 Factor 0 0 0 0 0 0 0 0 0 sp_MShelptype [@typename] [,@flags='sdt'I'uddt' | NULL] Выводит информацию о типах данных. EXEC spJShelptype 'id' EXEC sp_MShelptype 'int'.'sdt' EXEC sp_MShelptype (Результаты сокращены) UserDatatypeName owner basetypename defaultname rulename id dbo varchar NULL NULL A row(s) affected) SystemDatatypeName ifvarlenjnax allownulls isnumeric allowidentity int NULL 1 0 1
Недокументированные процедуры 531 SystemDatatypeName bigint binary bit char datetime decimal float image int money nchar ntext numeric nvarchar real smalldatetime smallint small money sql_vari.ant sysname text timestamp tiny int uniqueidentifier varbinary varchar UserDatatypeName empid id tid ifvarl NULL 8000 NULL 8000 NULL NULL NULL NULL NULL NULL 8000 NULL NULL 8000 NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL 8000 8000 owner dbo dbo dbo en_max allownull 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 basetypename char varchar varchar s isnumeric 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 defaultname NULL NULL NULL allowidentity 1 0 0 0 0 1 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 rulename tid NULL 259 NULL 257 NULL 258 sp_MSindexspace @tablename [,@index_name] Выводит информацию о размере индекса. EXEC sp_MSindexspace 'Customers' Inde 1 2 3 4 5 6 7 8 9 'X ID Index Name PK Customers City CompanyName PostalCode Region ContactName index 2073058421 WA Sys Country ContactTitle Size 16 16 16 16 16 16 16 7B905C75 0 0 (KB) Comments Size excludes actual data. (None) (None) (None) (None) (None) (None) (None) (None) sp_MSis_pk_col @source_table, @colname, @indid Выводит информацию о том, является ли указанное поле первичным ключом. DECLARE @res int EXEC @res=sp_MSis_pk_col 'Customers','Customerld'.1
532 Глава 22. Недокументированные возможности Transact-SQL SELECT @res (Результаты) 1 sp_MSkilldb @dbname Использует DBCC DBREPAIR для удаления базы данных, даже если база не повреждена. sp_MSkilldb 'northwind2' sp_MSIoginmappings @loginname Выводит отображения учетных записей, баз данных, пользователей и псевдонимов. sp_MSlogi nmappi ngs (Результаты сокращены) LoginName DBName UserName AliasName BUILTIN\Administrators NULL NULL NULL LoginName DBName UserName AliasName LEXUALIONIS LEXUALIONIS LEXUALIONIS LEXUALIONIS LoginName distributor_admin LoginName sa sa sa sa sa sa sa LoginName puck puck puck LoginName farker farker farker statworld Northwind2 Northwind3 pubs DBName NULL DBName distribution master model msdb Northwind tempdb test DBName Northwind pubs pubs2 DBName Northwind pubs pubs2 dbo dbo dbo dbo UserName NULL UserName dbo dbo dbo dbo dbo dbo dbo UserName puck puck puck UserName farker farker farker NULL NULL NULL NULL AliasName NULL AliasName NULL NULL NULL NULL NULL NULL NULL AliasName NULL NULL NULL AliasName NULL NULL NULL
Недокументированные процедуры 533 LoginName DBName UserName AliasName frank Northwind frank NULL sp_MStable_has_unique_index ©tabid Проверяет, есть ли в указанной таблице уникальный индекс. DECLARE Pobjid int. @res int SET @objid=OBJECTJD('Customers') EXEC @res=sp_MStab1e_has_unique_index @objid SELECT @res (Результаты) 1 sp_MStablekeys [tablename] [,@colname] [,@type] [,@keyname] [,@flags] Перечисляет ключи таблицы. sp_MStablekeys 'Orders' cType cName cFlags cColCount cFi11 Factor 1 PK_0rders 1 1 0 3 FK_Orders_Customers 2067 1 NULL 3 FK_OrdersJmployees 2067 1 NULL 3 FK_Orders_Shippers 2067 1 NULL sp_MStablerefs @tablename,@type=N'actualtables',@direction= N'primary'^reftable Перечисляет объекты, ссылающиеся на таблицу, или объекты, на которые ссылается таблица. sp_MStablerefs 'Orders' (Результаты) candidate_table candidate_key referenced [dbo].[Customers] N/A 1 [dbo].[Employees] N/A 1 [dbo].[Shippers] N/A 1 sp_MStablespace [@name] Выводит информацию о занятом таблицей пространстве. sp_MStablespace 'Orders' (Результаты) Rows DataSpaceUsed IndexSpaceUsed 830 208 328
534 Глава 22. Недокументированные возможности Transact-SQL sp_MSunc_to_drive @unc_path, @local_server, @local_path OUT Преобразует UNC-путь в дисковый путь. DECLARE @path sysname EXEC sp_MSunc_to_drive '\\PYTHIA\C$\'. 'PYTHIA',@path OUT SELECT @path (Результаты) C:\ sp_MSuniquecolname table_name, @base_colname, @unique_colname OUT Генерирует уникальное имя поля для заданной таблицы на основе базового имени. DECLARE @uniquename sysname EXEC sp_MSuniquecolname 'Customers'. 'Customerld' ,@uniquename OUT SELECT Ouniquename (Результаты) CustomerIdl3 sp_MSuniquename @seed, ©start Возвращает набор, содержащий уникальное для данной базы имя объекта, используя заданное имя и стартовое значение. spJISuniquename 'Customers'.3 Name Next Customers3 92 sp_MSuniqueobjectname @name_in, @name_out OUT Возвращает уникальное для данной базы имя объекта. DECLARE Ooutname sysname SET @outname=" -- Can't be NULL EXEC sp_MSuniqueobjectname 'Customers',@outname OUT SELECT @outname (Результаты) Austomers sp_MSuniquetempname @name_in, @name_out OUT Генерирует уникальное имя для временного объекта tempdb, используя базовое имя. USE tempdb CREATE TABLE 1ivr_kp (cl int)
Недокументированные процедуры 535 DECLARE @name_out sysname exec sp_MSuniquetempname 'livr_kp', @name_out OUT SELECT @name_out (Результаты) liar_kp sp_readerrorlog [@lognum] Выводит указанный системный журнал ошибок. Для вывода текущего журнала параметр не указывается. sp_readerrorlog 2 (Результаты сокращены) ERR0RL0G.2 2000-09-29 22:57:38.89 server Microsoft SQL Server 2000 - 8 Aug 6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Personal Edition on Windows NT 5.0 (Build 2195: Service 2000-09-29 22:57:38.96 server Copyright (C) 1988-2000 Micros 2000-09-29 22:57:38.96 server All rights reserved. 2000-09-29 22:57:36.96 server Server Process ID is 780. 2000-09-29 22:57:38.96 server Logging SQL Server messages in 2000-09-29 22:57:45.73 server SQL server listening on TCP, S 2000-09-29 22:57:45.73 server SQL server listening on 192.16 2000-09-29 22:57:45.81 server SQL Server is ready for client 2000-09-29 22:57:45.90 spid5 Clearing tempdb database. 2000-09-29 22:57:49.06 spid5 Starting up database 'tempdb'. 2000-09-29 22:57:51.07 spid4 Recovery complete. sp_remove_tempdb_file ©filename Удаляет указанный файл базы tempdb. master..sp_remove_tempdb_fi1e 'tempdev02' sp_set_local_time [@server_name] [,@adjustment_in_minutes] (for Win9x) Синхронизирует местное время на компьютере с сервером. msdb..sp_set_loca I time sp_tempdbspace Отображает информацию об использовании дискового пространства базой tempdb. sptempdbspace (Результаты) database_name database_size spaceused tempdb 8.750000 .546875
536 Глава 22. Недокументированные возможности Transact-SQL xp_dirtree 'footpath' Выводит список подкаталогов (и их подкаталогов) для заданного пути вместе с информацией об уровне вложенности. master. .xp_dirtree 'c:V (Результаты сокращены) subdirectory WINDOWS SYSTEM OOBE MSNSETUP SETUP HTML MOUSE IMAGES ISPSGNUP IMAGES ERROR MSNHTML ISPSGNUP MOUSE MSNERROR MSN PASSPORT SHELLEXT COLOR VMM32 MACROMED DIRECTOR FLASH Shockwave XTRAS IOSUBSYS VIEWERS WBEM logs MOF bad good depth 1 2 3 4 4 4 5 6 5 4 4 4 5 5 4 4 4 3 3 3 3 4 4 4 5 3 3 3 4 4 5 5 xp_dsninfo @systemdsn Возвращает ODBC DSN-информацию для указанного системного источника данных. master..xp_dsninfo 'pubsdsn' xp_enum_oledb_providers Перечисляет OLEDB-провайдеров, доступных на сервере. master..xp_enum_oledb_providers (Результаты сокращены) Provider Name Provider Description
Недокументированные процедуры 537 EMP0LEDB.1 Source MediaCatalogDB.l SQLOLEDB DTSPackageDSO SQLReplication.OLEDB Medi aCatalogMergedDB.1 MSDMine ADsDSOObject MediaCatalogWebDB.l MSDAIPP.DSO MSSearch.CollatorDSO.l MSDASQL MSUSP Microsoft.Jet.OLEDB.4.0 MSDAOSP MSDAORA MSIDXS xp_enumdsn VSEE Versioning Enlistment Manager Proxy Data ■ ■ MediaCatalogDB OLE DB Provider Microsoft OLE DB Provider for SQL Server Microsoft OLE DB Provider for DTS Packages SQL Server Replication OLE DB Provider for DTS MediaCatalogMergedDB OLE DB Provider Microsoft OLE DB Provider For Data Mining Services OLE DB Provider for Microsoft Directory Services MediaCatalogWebDB OLE DB Provider Microsoft OLE DB Provider for Internet Publishing Microsoft OLE DB Provider for Microsoft Search Microsoft OLE DB Provider for ODBC Drivers Microsoft OLE DB Provider for Outlook Search Microsoft Jet 4.0 OLE DB Provider Microsoft OLE DB Simple Provider Microsoft OLE DB Provider for Oracle Microsoft OLE DB Provider for Indexing Service Перечисляет системные ODBC-источники данных, доступные на сервере. master..xp_enumdsn (Результаты сокращены) Data Source Name Description DeluxeCD Visual FoxPro Database Visual FoxPro Tables dBase Files - Word FoxPro Files - Word SS7 KHENSS2K MS Access Database Excel Files dBASE Files LocalServer MQIS FoodMart ECDCMusic Microsoft Access Microsoft Visual Microsoft Visual Microsoft dBase Microsoft FoxPro SQL Server SQL Server Microsoft Access Microsoft Excel Microsoft dBase SQL Server SQL Server Microsoft Access Microsoft Access Driver (*.mdb) FoxPro Driver FoxPro Driver VFP Driver (*.dbf) VFP Driver (*.dbf) Driver (*.mdb) Driver (*.xls) Driver (*.dbf) Driver (*.mdb) Driver (*.mdb) xp_enumerrorlogs Выводит список файлов журналов ошибок сервера. master..xp_enumerrorlogs (Результаты сокращены) Archive # Date Log File Size (Byte) 06/28/2000 23:13 06/29/2000 11:19 06/29/2000 11:35 06/29/2000 22:55 06/29/2000 23:10 07/01/2000 12:49 07/01/2000 12:51 3139 3602 3486 15998 3349 120082 31086
538 Глава 22. Недокументированные возможности Transact-SQL xp_execresultset 'code query'/database' Первым параметром принимает запрос, возвращающий тот T-SQL-запрос, который будет исполнен. Это удобно для выполнения очень длинных запросов, которые не умещаются в переменную varchar(8000). Можно разместить запрос в таблице и сослаться на эту таблицу в запросе, передаваемом xp_execresultset. exec master. .xp_execresultset 'SELECT ''PRINT ''"test .'pubs' (Результаты) test xp_fileexist 'filename' Возвращает набор данных, содержащий информацию о том, существует ли указанный файл. exec master..xp_fileexist 'd:\winnt\readme.txt' exec master..xp_fi1eexi st 'с:\wi nnt\readme.txt' exec master..xp_fileexist 'c:\winnt\odbc.ini' exec master..xp_fi1eexi st 'с:\wi nnt' (Результаты) File Exists File is a Directory Parent Directory Exists 0 Fi 0 Fi 1 Fi le le le Exists Exists Exists 0 File 0 File 0 File is is is a a a Directory Directory Di rectory 0 Parent 1 Parent 1 Parent Directory Directory Directory Exists Exists Exists 0 1 1 xp_fixeddrives Возвращает набор данных, содержащий информацию об имеющихся на сервере жестких дисках. master..xp_fixeddrives ■(Результаты) drive MB free С 4743 . xp_get__mapi__default_profile Возвращает имя почтового профиля MAPI, установленного по умолчанию, master..xp_get_mapi_default_profile
Недокументированные процедуры 539 (Результаты) ,:._ . .. - . Profile name Microsoft Outlook Internet Setti xp_get_mapi_profiles Возвращает набор профилей MAPI, имеющихся в системе. master..xp_getjnapi_profiles (Результаты) Profile name Is default profile Microsoft Outlook Internet Setti 1 xp_getfiledetails 'filename' Возвращает информацию об указанном файле. master.,xp_getfiledetails 'c:\winnt\odbc.ini' (Результаты сокращены) Alternate Name Size Creation Date Creation Time Last Written Date NULL 2144 20000903 220228 20000628 xp_getnetname Возвращает сетевое имя сервера. master..xp_getnetname (Результаты) Server Net Name TALIONIS xp_oledbinfo ©providername, ©datasource, ©location, ©providerstring, ©catalog, ©login, ©password, @ info type Возвращает подробную OLEDB-информацию для указанного связанного сервера. master..xp_oledbinfo 'SQLOLEDB'. 'PYTHIA'. NULL. NULL. NULL, 'sa'.'drkildare'. NULL (Результаты) Information Type Value DBMS Name Microsoft SQL Server DBMS Version 8.00.194 Database Name master SQL Subscriber TRUE
540 Глава 22. Недокументированные возможности Transact-SQL xp_readerrorlog [lognum][filename] Возвращает набор с 1 charB55), c2 int, содержащий информацию из журнала ошибок, указанного параметром lognum. master..xp_readerrorlog 3 (Результаты сокращены) ' ''" ■> ERR0RL0G.3 2000-09-29 11:36:07.58 server Microsoft SQL Server 2000 - 8.00.194 (I Aug 6 2000 00:57:48 Copyright (с) 19В8-2000 Microsoft Corporation Personal Edition on Windows NT 5.0 (Build 2195: Service Pack 2. R 2000-09-29 11:36:07.58 server Copyright (C) 1988-2000 Microsoft Corpor 2000-09-29 11:36:07.60 server All rights reserved. 2000-09-29 11:36:07.60 server Server Process ID is 1080. 2000-09-29 11:36:07.60 server Logging SQL Server messages in file 'C:\ 2000-09-29 11:36:07.61 server SQL Server is starting at priority class 2000-09-29 11:36:07.72 server SQL Server configured for thread mode pr 2000-09-29 11:36:07.72 server Using dynamic lock allocation. [500] Loc 2000-09-29 11:36:07.85 spid3 Starting up database 'master'. 2000-09-29 11:36:07.99 server Using 'SSNETLIB.DLL' version '8.0.194'. 2000-09-29 11:36:07.99 spid5 Starting up database 'model'. 2000-09-29 11:36:08.02 spid3 Server name is 'KHENMPXSS2000'. 2000-09-29 11:36:0B.02 spid3 Skipping startup of clean database id 4 2000-09-29 11:36:08.02 spid3 Skipping startup of clean database id 5 2000-09-29 11:36:08.18 spid5 Clearing tempdb database. 2000-09-29 11:36:08.51 spid5 Starting up database 'tempdb'. 2000-09-29 11:36:08.71 spid3 Recovery complete. 2000-09-29 22:55:28.36 server SQL Server terminating because of system 2000-09-29 22:55:39.34 spid3 SQL Server is terminating due to 'stop' Можно также указать -1 в качестве параметра lognum и вторым параметром передать имя произвольного файла, который требуется прочитать. Таким образом, процедура хр_readerrorlog позволяет читать любые текстовые файлы, не только журналы ошибок. Например, эта команда читает файл README.TXT: EXEC master..xp_readerrorlog -1. 'C:\README.TXT' xp_regenumvalues Перечисляет значения под указанным ключом реестра. CREATE TABLE #reg (kv nvarcharB55) NOT NULL. л kvdata nvarcharB55) null) INSERT #reg EXEC master..xp_regenumvalues 'HKEY_LOCAL_MACHINE', 'SOFTWARE\Microsoft\MSSQLServer\MSSQLServer' SELECT * FROM #reg (Результаты) kv kvdata FullTextDefaultPath C:\Program Files\Microsoft SQL Server\MSSQL$SS2000\FTData
Недокументированные процедуры 541 UstenOn - Item #1 UstenOn - Item #2 SetHostName AuditLevel LoginMode Tapeloadwaittime DefaultLogin Map Map# Map$ BackupDirectory DefaultDomain DefaultCollationName SSMSSH70 SSNETLIB 0 0 2 -1 guest \ - NULL C:\Program Files\Microsoft SI DAD SQL_Latinl_Genera1_CPl_CI_AS xp_regaddmultistr/ xp_regdeletekey, xp_regdeletevalue, xp_regread, xp_regremovemultistring/ xp_regwrite Позволяют добавлять, изменять и удалять ключи и значения реестра. DECLARE @df nvarcharF4) EXEC master.dbo.xp_regread N'HKEY_CURRENTJJSER', N'Control Panel Unternational N'sShortDate'. @df OUT. N'no_output' SELECT @df (Результаты) M/d/yyyy xp_subdirs Возвращает набор, содержащий непосредственные подкаталоги указанного каталога. master..xp_subdirs 'C:\Program Files\Microsoft SQL Server' (Результаты) subdirectory MSSQLSSS2000 80 xp_test_mapi_profile 'profile' Тестирует указанный профиль MAPI, то есть проверяет, существует ли профиль и возможно ли с ним соединение. master..xp_test_mapi^profile 'SQL' xp_varbintohexstr Преобразует переменную типа varbinary в строку, содержащую шестнадцатерич- ное представление. CREATE PROC spjiex @i int. @hx varcharC0) OUT AS DECLARE @vb varbinaryOO)
542 Глава 22. Недокументированные возможности Transact-SQL SET @vb=CAST(@i as varbinary) EXEC master..xp_varbintohexstr @vb, @hx OUT GO DECLARE @hex varcharC0) EXEC spjiex 343. @hex OUT SELECT @hex (Результаты) 0x00000157 Создание представлений INFORMATION_SCHEMA Возможно, вы знаете, что ANSI-представления INFORMATION_SCHEMA можно опрашивать из любой базы, хотя сами представления расположены в базе master. Создавать представление в базе master и обращаться к нему в контексте активной базы весьма удобно. Представьте себе, какого количества дублированных объектов можно было бы избежать, если бы можно было создавать представления, ведущие себя подобно системным процедурам, исполняющимся в контексте текущей базы, хотя и находящимся в базе master. Как с SQL Server, так и в этом случае, если система может создать объект с особыми свойствами — вы тоже можете создать такой объект. Проблема заключается в том, чтобы найти способ создания такого объекта, поскольку обычно его не описывают. Именно так обстоят дела с представлениями INFORMATION_SCHEMA. Описанные ниже действия не документированы компанией Microsoft. Как и в других случаях с недокументированными возможностями, помните, что все может измениться или перестать работать в следующих версиях SQL Server. Для того чтобы создать собственное представление INFORMATION_SCHEMA, выполните следующее. 1. При помощи вызова sp_conf igu re переведите сервер в режим, допускающий обновление системных таблиц, sp_configu re 'allow updates', 1 RECONFIGURE WITH OVERRIDE. 2. Вызвав sp_MS_upd_sysobj_category, переведите сервер в режим автоматического создания системных объектов (вы должны быть владельцем базы или исполнять роль setupadmin): sp_MS_upd_sysobj_category 1. Эта процедура устанавливает флаг трассировки 1717, чтобы все создаваемые объекты имели установленный бит IsMSShlpped. Это необходимо, так как невозможно создать несистемные объекты, принадлежащие INFORMATION_SCHEMA. Если бы это было возможно, то можно было бы воспользоваться процедурой sp_MS_marksystemobject (также описанной в этой главе) для установки системного бита. Вместо этого приходится переводить сервер в специальный режим, в котором каждый создаваемый объект автоматически помечается как системный. 3. Создайте представление в базе master, указав в качестве владельца INFORMATION_SCHEMA. 4. При помощи вызова sp_MS_upd_sysobj_category отключите режим автоматического создания системных объектов: sp_MS_upd_sysobj_category 2.
Создание системных функций 543 5. Вызвав sp_configure, переведите сервер в режим, не допускающий обновление системных таблиц: sp_configu re 'allow updates', 0 RECONFIGURE WITH OVERRIDE. Вы можете создавать представления, работающие подобно встроенным в SQL Server представлениям INFORMATION_SCHEMA. В главе 9 можно найти примеры системных представлений, определяемых пользователем. Создание системных функций Подобно тому как создают системные представления, можно создавать функции в базе master, обращаться к которым можно из любой базы без указания имени базы master. Вот как это работает: в процессе инсталляции SQL Server создает множество системных функций (например, fn_varbintohexstr(), fn_chariswhitespace() и т. д.). Те функции, которые были созданы с указанием в качестве владельца system_f unc- tion_schema, могут быть вызваны из любой базы по одному имени (без префикса master. . ). Получить список этих функций можно, запустив следующий запрос: USE master GO ' - SELECT name FROM sysobjects WHERE uid=USER_ID('system_function_schema') AND (OBJECTPROPERTYdd. ' IsScalarFunction' )=1 ' - ' OR OBJECTPROPERTYOd, ' IsTableFunction' )=1 OR OBJECTPROPERTYCid. 'IsInlineFunction')=1) (Результаты) name fn_chariswhitespace fn_db!og fn_generateparameterpattern fn_getpersistedservernamecasevariation fn_hel peculations f n_li stextendedproperty fn_removeparameterwithargument fn_replbi tstri ngtoi nt fn_replcomposepub1icationsnapshotfolder fn_replgenerateshorterfilenameprefix fn_replgetagentcommandli nefromjobi d fn_replgetbinary81odword fn_replinttobitstring fn_replmakestringliteral fn_replprepadbinary8 fn_replquotename fn_rep! rotr fn_repltri mleadi ngzerosi nhexstr fn_repluniquename fn_serverid fn_servershareddrives fn_skipparameterargument fn_trace_geteventi nfo fn_trace_getfi 1 teri nfo fn_trace_getinfo fn trace_gettable
544 Глава 22. Недокументированные возможности Transact-SQL fn_updateparameterwithargument fn_virtualfilestats fn_virtualservernodes Для того чтобы создать свои системные функции, вам требуется выполнить следующие действия. 1. Перевести сервер в режим, допускающий обновление системных таблиц: sp_configure 'allow updates',1 RECONFIGURE WITH OVERRIDE 2. Создать функцию при помощи команды CREATE FUNCTION в базе master, указав в качестве владельца system_f unction_schema. Имя функции должно начинаться с f п_ и состоять только из букв нижнего регистра. 3. Вернуть сервер в прежний режим, особенно на рабочей системе: sp_configure 'allow updates'. RECONFIGURE WITH OVERRIDE Пользуясь этой инструкцией, можно создавать функции, определяемые пользователем. При вызове функций, возвращающих таблицы, следует указывать два двоеточия перед именем функции. В главе 10 можно найти несколько примеров, определяемых пользователем системных функций. Недокументированные команды DBCC Первоначально команда DBCC предоставляла для ведения баз данных небольшой набор операций, стоящий отдельно от традиционного Transact-SQL. Команда DBCC сформировала из этих операций универсальный инструмент администрирования баз, который использовался администраторами для того, чтобы организовать регулярное обслуживание баз данных и их проверку на непротиворечивость. С тех пор список опций команды DBCC увеличился и включил в себя десятки аспектов, не связанных с проверкой базы данных на предмет ошибок. Теперь DBCC работает со всем: от сообщений аудита до управления полнотекстовыми индексами. Многие из операций, которые выполняет DBCC, не документированы и вызываются только кодом, предоставленным Microsoft. Некоторые из них мы рассмотрим далее в главе. Перед тем как приступить к изучению недокументированных команд DBCC, следует познакомиться с несколькими важными аспектами. Для того чтобы ограничить результаты выполнения DBCC только сообщениями об ошибках, необходимо использовать опцию WITH N0_INF0MSGS. Это облегчает работу с результатами многих команд DBCC (например, DBCC CHECKALL0C) без потери необходимой информации. DBCC HE LP (команда) отображает информацию по использованию многих команд. Большинство недокументированных команд не имеют такой информации, но всегда лучше проверить. Для того чтобы результаты выполнения команд DBCC отсылались вам, а не в журнал ошибок, следует использовать DBCC TRACE0NC604). Хотя большинство недокументированных команд по умолчанию отправляют результаты своего выполнения в журнал ошибок, можно перенаправить их в ваше клиентское соединение, установив флаг трассировки 3604. В табл. 22.1 перечислены недокументированные команды DBCC.
Таблица 22.1. Недокументированные команды DBCC Команда и назначение Пример ADDEXTENDEDPROC(procname,DLL) Добавляет расширенную хранимую процедуру. Процедура sp_addextendedproc вызывает эту DBCC-команду. Параметр procname представляет собой имя расширенной хранимой процедуры; параметр DDL — название динамической библиотеки, в которой расположена реализация расширенной хранимой процедуры ADDINSTANCE(object,instance) Добавляет экземпляр объекта для отслеживания в Performance Monitor. Хранимые процедуры, инициализирующие счетчики Performance Monitor, используют эту команду для установки различных параметров, служащих для мониторинга производительности SQL Server. Параметр object представляет собой имя объекта (например, SQL Replication Agents); параметр instance — имя экземпляра, который необходимо добавить (например, Logreader) AUDITEVENT(id, subclass, succeeded, loginame, username, grpname, sid) Порождает событие аудита безопасности, которое можно отслеживать в приложении Profiler. Параметры: id — номер события; Subclass — идентификатор подкласса события; loginame — учетная запись, вызывающая событие; username — пользователь базы данных, вызывающий событие; grpname — имя группы или роли, в которую входит пользователь; sid — идентификатор безопасности учетной записи, вызывающей событие DBCCADDEXTENDEDPROC('xp_mode','xp_stats.dll') DBCC ADDINSTANCE("SQL Replication Agents", "Snapshot") DBCC AUDITEVENT A09, 1, 0, @loginame, @name_in_db, @grpname , NULL) /* Допустимые значения id/subclass ID Sub Event 104 1 Add login 104 2 Drop login 105 1 Grant login 105 2 Revoke login 105 3 Deny login 106 1 Change default database 106 2 Change default language 107 1 User change password 107 2 Admin, change password 108 1 Add server role member 108 2 Drop server role member 109 1 Add DB user 109 2 Drop DB user 109 3 Grant DB access 109 4 Revoke DB access 110 1 Add DB role member 110 2 Drop DB role member 110 3 Change DB role member 111 1 Add role 1112 Drop role 112 1 App role change password */ продолжение ^>
Таблица 22.1 (продолжение) Команда и назначение Пример BCPTABLOCK(dbid, tabid, setflag) Устанавливает опцию table lock on bulk load для таблицы. Это может улучшить выполнение операций по вводу большого массива данных, так как исчезает необходимость в получении блокировки на уровне строк для каждой добавляемой записи. Параметр dbid представляет собой идентификатор базы данных; tabid — идентификатор объекта для таблицы; setflag — флаг, указывающий на то, требуется установить или сбросить опцию BUFFER(dbid[,objid][,numberofbuffers][,printopt {0 | 1 | 2}]) Отображает содержимое буферов памяти SQL Server. Можно отобразить содержимое буферов для указанного объекта или для всей базы BYTES(startingaddress,length) Отображает содержимое области памяти длиной length байт, начиная с адреса startingaddress, который должен принадлежать к адресному пространству процесса SQL Server CALLFULLTEXT(funcid[catid][,0bjid]) Допустимые значения параметра funcid: 1 — создает полнотекстовый каталог, 2 — удаляет полнотекстовый каталог, 3 — заполняет полнотекстовый каталог, 4 — прекращает заполнение полнотекстового каталога, 5 — помечает таблицу для индексирования, 6 — удаляет таблицу из индексирования, 7 — удаляет все полнотекстовые каталоги, 8 — производит очистку полнотекстовых каталогов, 9 — задает доступные для Microsoft Search ресурсы процессора. (Значения: 1-5; 1 = фоновый режим, 5 = выделенный режим. По умолчанию значение 3.) 10 — устанавливает тайм-аут соединения (значение в секундах от 1 до 32767). Используется для выполнения множества функций, связанных с механизмом полнотекстового поиска. Параметр funcid указывает функцию, которую необходимо выполнить и какие параметры требуется использовать, catid — идентификатор полнотекстового каталога, objid — идентификатор объекта. Вызов CALLFULLTEXT возможен только из системной хранимой процедуры, имя которой начинается с sp_fulltext_ и имеющей установленный системный бит (см. недокументированную процедуру sp_MS_marksystemobject) DECLARE @dbid int, @objid int SELECT @dbid=DB_ID('pubs'), @objid=OBJECTJD('titles') DBCC BCPTABLOCK(@dbid,@objid,l) DECLARE @dbid int, @objid int SELECT @dbid=DB_ID('pubs'), @objid=OBJECT_ID('pubs..titles') SELECT COUNT(*) FROM pubs, .titles -- Load buf DBCC TRACEONC604) DBCC BUFFER(@dbid,@0bjid,l,2) DBCC BYTES@014767000,50) USE master GO IF OBJECT_ID('sp_fulltext_resource') IS NOT NULL DROP PROC sp_fulltext_resource GO CREATE PROC sp_fulltext_resource @value int -- значение 'resource_usage' AS DBCC CALLFULLTEXT(9,@value) -- FTSetResource (@value) IF (@@error<>0) RETURN 1 -- SUCCESS - RETURN 0 -- sp_fulltext_resource GO EXEC sp_MS_marksystemobject 'sp_fulltext_resource' EXEC sp_fulltext_resource 3
Команда и назначение Пример DBCONTROL(dbname,option) Устанавливает опции базы данных. Вызывается процедурой sp_dboption и выполняет многие из ее функций. Параметр dbname — имя базы данных, option — опция, которую необходимо установить DBINFO(dbname) Выводит системную информацию об указанной базе данных, включая дату создания, идентификатор, статус, следующее значение timestamp и т. д. DBRECOVER(dbname) Восстанавливает базу. Обычно базы восстанавливаются при старте SQL Server. Если этого не произошло из-за ошибки или потому, что режим восстановления отключен (см. флаги трассировки 3607 и 3608), то можно использовать DBRECOVER для принудительного восстановления базы. Параметр dbname — имя восстанавливаемой базы DBREINDEXALL(dbname) Перестраивает все индексы в текущей базе данных. Работает только для пользовательских (не системных) баз данных DBCC DBTABLE(dbid) Выводит информацию DBT (DB Tabie) и FCB (File Control Block) для указанной базы данных DELETEINSTANCE(object,instance) Удаляет экземпляр объекта Performance Monitor, созданный ранее с помощью DBCC ADDINSTANCE. Параметр object указывает имя объекта, instance — имя экземпляра. Для удаления нескольких экземпляров можно указать шаблон для instance DES(dbid,objid) Выводит системную информацию для указанного объекта /* Поддерживаемые опции multi Переводит базу в многопользовательский режим offline Переводит базу в режим 'offline' online Переводит базу в режим 'online' readonly Переводит базу в режим 'только для чтения' readwrite Переводит базу в режим 'чтения и записи' single Переводит базу в однопользовательский режим */ DBCC DBCONTROL('pubs',multi) DBCC DBINFO('pubs') DBCC DBRECOVER('pubs') DBCC DBREINDEXALL('pubs') WITH NOJNFOMSGS DECLARE @dbid Int SET @dbid=DBJD('pubs') DBCC DBTABLE(@dbid) DBCC DELETEINSTANCE("SQL Replication Merge", "%") DECLARE @dbid int, @objid int SELECT @dbid=DB_ID('pubs'), @objid=OBJECTJD('authors) DBCC DES(@dbid, @objid) продолжение $■
Таблица 22.1 (продолжение) Команда и назначение Пример DETACHDB(dbname) Отсоединяет базу данных от сервера. База может быть перенесена на другой сервер и присоединена с помощью процедуры sp_attach_db. Данная DBCC-команда вызывается процедурой sp_detach_db DROPEXTENDEDPROC(procname) Удаляет расширенную хранимую процедуру. Вызывается системной хранимой процедурой sp_dropextendedproc ERRORLOG Приводит к закрытию текущего файла ошибок и созданию нового. При этом происходит ротация расширений файлов, как и при перезапуске сервера. Вызывается процедурой sp_cycle_errorlog EXTENTINFO(dbname, tablename, indid) Выводит информацию обо всех экстентах объекта, dbname — имя базы, tabiename — имя таблицы, indid — идентификатор индекса FLUSHPROCINDB(dbid) Вызывает перекомпиляцию всех хранимых процедур в базе. Параметр dbid — идентификатор базы. Данная команда полезна в том случае, если произошло изменение опции базы, влияющее на запросы, порожденные хранимыми процедурами. Например, системная процедура sp_dboption вызывает DBCC FLUSHPROCINDB при изменении опций, связанных с компиляцией IND(dbid, objid[,indid]) Выводит системную информацию об индексах для указанного объекта INVALIDATE_TEXTPTR(@TextPtrVal) Помечает текстовый указатель как недействительный для транзакции. Если значение параметра @TextPtrVal равно NULL, то все текстовые указатели в текущей транзакции помечаются как недействительные. Данная команда DBCC вызывается процедурой sp_invalidate_textptr DBCC DETACHDB('northwind2') USE master DBCC DROPEXTENDEDPROC('xp_mode') DBCC ERRORLOG DBCC EXTENTINFOCpubs'/titles',!) DECLARE @dbid int SET @dbid=DB_ID('puDs') DBCC FLUSHPROCINDB(@dbid) DECLARE @dbid int, @objid int SELECT @dbid=DB_ID('pubs'), @objid=OBJECT_ID('pubs..authors') DBCC IND(@dbid,@objid, 1) CREATE TABLE #testtxt (cl int, c2 text) EXEC tempdb..sp_tableoption '#testtxt', 'text in row', 'on' INSERT #testtxt VALUES ('1','Text lives here') BEGIN TRAN DECLARE @ptr varbinaryA6) SELECT @ptr = TEXTPTR(c2) FROM #testtxt READTEXT #testtxt.c2 @ptr 0 5 DBCC INVALIDATE_TEXTPTR(@ptr) READTEXT #testtxt.c2 @ptr 0 5 - Fails COMMIT TRAN
Команда и назначение Пример LOCKOBJECTSCHEMA (objname) Препятствует изменению схемы другими соединениями до тех пор, пока текущая транзакция не будет завершена. Также увеличивает значение поля schema_ver в таблице sysobjects. Вне транзакции эта команда ничего не делает LOG(dbid) Отображает записи из журнала транзакций текущей базы. Можно использовать конструкцию INSERT..ЕХЕС() для сохранения в таблице результата этой команды для дальнейшей обработки MEMORYSTATUS Отображает подробную информацию об использовании памяти SQL Server NO_TEXTPTR(@TabId, @InlineSize) Помечает таблицу, как не поддерживающую текстовые указатели A6-байтовые указатели на текстовые страницы), допуская таким образом хранение данных типа text в записях таблицы. @TabId — идентификатор таблицы, @InlineSize — количество символов B4-7000), которые будут храниться в записи таблицы. Данная DBCC-команда вызывается процедурой spjabieoption PAGE (dbid|dbname, filenum, pagenum[, printopt]) Значение параметра printopt: 0 (по умолчанию) — печатать заголовок страницы и буфера, 1 — печатать заголовок страницы и буфера, каждую запись таблицы и таблицу смещения записей, 2 — печатать заголовок страницы и буфера, саму страницу и таблицу смещения записей. Отображает содержимое указанной страницы базы. Параметры: dbid|dbname — идентификатор или имя базы; filenum — номер файла базы, который содержит страницу; pagenum — номер страницы; printopt — опция, указывающая, что именно отображать USE pubs BEGIN TRAN DBCCLOCKOBJECTSCHEMA('titleauthor') CREATE TABLE #logrecs (CurrentLSN varcharC0), Operation varcharB0), Context varcharB0), TransactionID varcharB0)) INSERT #logrecs EXEC('DBCC LOG("pubs")') DBCC MEMORYSTATUS CREATE TABLE testtxt (cl int, c2 text) DECLARE @TabId int SET @TabId=OBJECT_ID('testtxt') DBCC NO_TEXTPTR(@TabId,500) INSERT testtxt VALUES (T,Text lives here') BEGIN TRAN DECLARE @ptr varbinaryA6) SELECT @ptr = TEXTPTR(c2) FROM testtxt READTEXT testtxt.c2 @ptr 0 5 COMMIT TRAN DBCC TRACEONC604) GO DBCC PAGE('pubs',l,70,2) продолжение #
Таблица 22.1 (продолжение) Команда и назначение Пример PRTIPAGE(dbid, objid, indexid[, printopt {0 | 1 | 2}]) Выводит информацию о странице для указанного индекса PSS Отображает структуру статуса процесса (PSS) для указанного соединения. Для каждого соединения, в том числе и для системных, существует структура PSS. Она содержит информацию о блоке управления транзакциями, уровне изоляции, клиентской машине и т. д. RESOURCE Выводит информацию об использовании ресурсов сервером SETINSTANCE(object,counter,instance,val) Устанавливает значение экземпляра счетчика Performance Monitor. Это можно использовать для определения производительности запросов и хранимых процедур при помощи создания пользовательского счетчика в Performance Monitor. По сути, так и работают процедуры sp_user_counterNN: они вызывают команду DBCC SETINSTANCE. object — имя объекта Performance Monitor, instance — имя экземпляра, counter — имя счетчика, val — новое значение счетчика STACKDUMP Сохраняет информацию о стеке вызова для всех активных сессий, а также их входные буферы. Часть информации сохраняется в журнале ошибок сервера, остальное — в файле с расширением .DMP, который располагается в каталоге журналов сервера TAB(dbid,objid[,printopt {0 | 1 | 2}}]) Выводит системную информацию для указанной таблицы UPGRADEDB(dbname) Обновляет системные объекты в указанной базе до текущей версии DECLARE @dbid int, @pagebin varcharA2), @pageid int, @fileid int, @objid int SELECT TOP 1 @dbid=DB_ID('pubs'), @objid=id, @pagebin=first FROM pubs..sysindexes WHERE id=OBJECTJD('pubs..authors') EXEC sp_decodepagebin @pagebin, @fileid OUT, @pageid OUT DBCC PRTIPAGE(@dbid, @objid, 2, @pageid) DBCC PSS DBCC TRACEONC604) DBCC resource DBCC TRACEOFFC604) DBCC SETINSTANCE('SQLServer:User Settable', 'Query', 'User counter 1', 3) DBCC STACKDUMP DECLARE @dbid int, @objid int SELECT @dbid=DBJD('pubs'), @objid=OBJECTJD('pubs..authors') DBCC TAB(@dbid, @0bjid, 2) DBCC UPGRADEDB('oldpubs')
Недокументированные флаги трассировки 551 Недокументированные функции До появления пользовательских функций (UDF) использование недокументированных функций было особенно привлекательным, так как если в Transact-SQL не было требуемой функции, некоторые вещи просто нельзя было сделать. С появлением в SQL Server 2000 определяемых пользователем функций количество причин для использования недокументированных функций уменьшилось. В большинстве случаев можно написать функциональность, отсутствующую в документированных процедурах и функциях. Иногда получить информацию о внутренних механизмах можно только посредством недокументированных функций. В табл. 22.2 приведены некоторые недокументированные функции. Как было сказано ранее, вы можете использовать их, но — на свой страх и риск. Недокументированные флаги трассировки Флаги трассировки SQL Server представляют собой целочисленные значения, которые следует передавать серверу для того, чтобы включать особые функции, обеспечивать лучшую диагностику, получать внутреннюю системную информацию и решать проблемы. Как правило, флаги трассировки устанавливаются при помощи выполнения команды DBCC TRACE0N(). Но также их можно установить при помощи опции командной строки -Т при запуске сервера. Некоторые флаги имеют смысл для сервера, поэтому их лучше указывать как опцию командной строки. Некоторые флаги имеют смысл только в контексте сессии, поэтому их устанавливают, вызвав DBCC TRACE0N(f lagnum), где f lagnum — номер флага. Обратная по отношению к DBCC TRACE0N() функция — DBCC TRACE0FF(). Она сбрасывает указанные флаги трассировки для сессии. Для того чтобы передать несколько флагов, их следует разделить запятыми. Команда DBCC TRACESTATUS(f lagnum) позволяет узнать, установлен ли флаг трассировки flagnum. Передав этой команде в качестве значения параметра flagnum -1, можно узнать, какие флаги трассировки установлены в данный момент. Вот простой пример использования команд DBCC TRACE0N() и DBCC TRACESTATUS(): EXEC master, .xpjogevent 99999. 'CHECKPOINT before setting flag 3502'. informational CHECKPOINT DBCC TRACE0NC604.3502) DBCC TRACESTATUS(-l) EXEC master, .xpjogevent 99999. 'CHECKPOINT after setting flag 3502'. informational CHECKPOINT DBCC TRACEOFFC604,3502) DBCC TRACESTATUS(-l) (Результаты) TraceFlag Status 3502 1 3604 1
552 Глава 22. Недокументированные возможности Transact-SQL Таблица 22.2. Недокументированные Transact-SQL-функции Функция и назначение Пример @@MICROSOFTVERSION Возвращает внутренний номер вереи и сервера, используемый Microsoft ENCRYPT(string) Шифрует строку. Используется сервером для шифрования кода, хранимого в таблице syscomments (для объектов, созданных с опцией WITH ENCRYPTION) GET_SID(username) Возвращает системный (NT) идентификатор пользователя или группы как varbinary(85). Для поиска идентификатора пользователя укажите перед параметром username \U, для поиска идентификатора группы укажите \G. Данная функция работает только из системных процедур с установленным системным битом (см. описание процедуры sp_MS_marksystemobject) SELECT @@MICROSOFTVERSION OBJECT_ID(../local') Хотя функция OBJECT_ID() документирована, ее опционный второй параметр — нет. Так как первым параметром может быть полное имя объекта, OBJECT_ID() возвращает идентификаторы объектов из других баз, а не из текущей базы. Иногда это нежелательно. Например, если производится действие с объектом, требующим доступа к информации каталога текущей базы, необходимо гарантировать, что имя объекта может быть транслировано в идентификатор и что объект находится в текущей базе. Указание значения 'local' для второго параметра функции OBJECT_ID() гарантирует, что будет получен идентификатор только объектов текущей базы 117441211 SELECT ENCRYPT('VALET') 0Х4С0059004Е00410052004400 USE master GO IF (OBJECT_ID('sp_get_sid') IS NOT NULL) DROP PROC sp_get_sid GO CREATE PROCEDURE sp_get_sid @loginame sysname AS DECLARE @sid varbinary(85) IF (charindex('V, (cploginame) = 0) SELECT SUSER_SID(@loginame) AS 'SQL User ID' ELSE BEGIN SELECT @sid=get_sid('\U'+ (cploginame, NULL) IF @sid IS NULL SELECT @sid=get_sid('\G'+ @loginame, NULL) IF @sid IS NULL BEGIN RAISERROR('Heвoзмoжнo определить ID для указанного значения loginame',16,10) RETURN -1 END ELSE SELECT @sid AS 'NT User ID' RETURN 0 END GO EXEC sp_MS_marksystemobject 'sp_get_sid' EXEC sp_get_sid 'LEX_TALIONIS\KHEN' USE pubs SELECT OBJECT_ID('Northwind..Orders'), OBJECT_ID('Northwind..Orders'/local') 357576312 NULL
Недокументированные флаги трассировки 553 Функция и назначение Пример PLATFORM0 Возвращает целое число, описывающее операционную систему и версию SQL Server PWDCOMPARE(str,pwd,oldenc) Сравнивает строку с зашифрованным паролем. Параметр str — это строка, которую необходимо сравнить; параметр pwd — зашифрованный пароль. Значение параметра oldenc равно 1 или О в зависимости от того, необходимо или нет использовать старый механизм шифрования, Зашифрованный пароль можно получить, выполнив запрос к системной таблице sysxiogins из поля password. Также можно использовать недокументированную функцию PWDENCRYPT() для получения зашифрованного пароля из заданной строки PWDENCRYPT(str) Шифрует строковое значение, используя алгоритм шифрования паролей SQL Server. Хранимые процедуры, служащие для управления паролями SQL Server, используют эту процедуру для шифрования паролей пользователей. Можно использовать недокументированную функцию PWDCOMPARE() для сравнения паролей нешифрованной строки с возвращаемым функцией PWDENCRYPT() значением TSEQUAL(tsl,ts2) Сравнивает два значения типа timestamp или rowversion. Возвращает 1, если значения равны, в противном случае генерирует ошибку. Функция TSEQUAL() существует давно. Она появилась в то время, когда Microsoft SQL Server был портированным под операционную систему OS/2 продуктом Sybase SQL Server. Сейчас эта функция используется уже не столь активно, так как в этом исчезла необходимость. Можно непосредственно сравнивать два поля timestamp/rowversion и решать, требуется ли генерировать ошибку. Также использование функции TSEQUAL() не приносит выигрыша в производительности по сравнению с простым сравнением. Поскольку эта функция не описана в Books Online, я вынужден описать ее здесь UNCOMPRESS0 Распаковывает строку SELECT PLATFORM() 1025 SELECT PWDCOMPARE('enmity', password,(CASE WHEN xstatus&2048=2048 THEN 1 ELSE 0 END)) FROM sysxiogins WHERE name='k_reapr' SELECT PWDENCRYPT('vengeance') AS EncryptedString, PWDCOMPARE('vengeance', PWDENCRYPT('vengeance'), 0) AS EncryptedCompare USE tempdb CREATE TABLE #testts (kl int identity, rowversion rowversion) DECLARE @tsl rowversion, @ts2 rowversion SELECT @tsl=@@DBTS, @ts2=@tsl SELECT CASE WHEN TSEQUAL(@tsl, @ts2) THEN 'Equal' ELSE 'Not Equal' END INSERT #testts DEFAULT VALUES SET @ts2=@@DBTS SELECT CASE WHEN TSEQUAL(@tsl, @ts2) THEN 'Equal' ELSE 'Not Equal' END GO DROP TABLE #testts Equal Server: Msg 532, Level 16, State 2, Line 16 The timestamp (changed to 0x0000000000000093) shows that the row has been updated by another user. SELECT CAST(CASE WHEN ([status] & 2 = 2) THEN (UNCOMPRESS([ctext])) ELSE [ctext] END AS nvarcharD000)) FROM syscomments WHERE ID=OBJECT_ID('sp_helptext')
554 Глава 22. Недокументированные возможности Transact-SQL Вот как выглядит журнал ошибок после выполнения этих команд (флаг трассировки 3502 включает режим расширенного протоколирования команды CHECKPOINT): 2000-07-01 01:10:33.89 spid57 Error: 99999. Severity: 10. State: 1 2000-07-01 01:10:33.89 spid57 CHECKPOINT before setting flag 3502. 2000-07-01 01:10:33.97 spid57 DBCC TRACEON 3604. server process ID (SPID) 57. 2000-07-01 01:10:34.00 spid57 DBCC TRACEON 3502. server process ID (SPID) 57. 2000-07-01 01:10:34.00 spid57 Error: 99999. Severity: 10. State: 1 2000-07-01 01:10:34.00 spid57 CHECKPOINT after setting flag 3502. 2000-07-01 01:10:34.00 spid57 Ckpt dbid 4 started A00000) 2000-07-01 01:10:34.00 spid57 Ckpt dbid 4 phase 1 ended A00000) 2000-07-01 01:10:34.00 spid57 Ckpt dbid 4 complete 2000-07-01 01:10:34.00 spid57 DBCC TRACEOFF 3604. server process ID (SPID) 57. 2000-07-01 01:10:34.00 spid57 DBCC TRACEOFF 3502, server process ID (SPID) 57. В табл. 22.3 приведены некоторые из множества недокументированных флагов трассировки SQL Server. Обратитесь к Books Online для получения информации о документированных флагах трассировки. Приведенный ниже список недокументированных флагов неполный, многие флаги здесь не описаны. Таблица 22.3. Некоторые из недокументированных флагов трассировки SQL Server Флаг Назначение 1717 Создаваемые объекты становятся системными (см. недокументированную хранимую процедуру sp_MS_upd_sysobj_category для дополнительной информации) 1200 Отображает детальную информацию о блокировках 1205 Дополняет флаг трассировки 1204 (информация о мертвых блокировках), отображая стек вызовов в случае возникновения мертвой блокировки 1206 Дополняет флаг трассировки 1204, отображая информацию о других блокировках, принадлежащих участникам мертвой блокировки 1211 Отключает эскалацию блокировок 2509 Используется совместно с DBCC CHECKTABLE для получения общего значения записей типа ghost в таблице 3502 В системный журнал ошибок протоколирует дополнительную информацию при выполнении операции CHECKPOINT 3505 Отключает автоматическое выполнение операции CHECKPOINT 3607 Отключает автоматическое восстановление всех баз данных 3608 Отключает автоматическое восстановление всех баз данных за исключением базы master 3609 Не создает базу данных tempdb при запуске сервера 8501 Включает режим трассировки событий DTC 8602 Отключает использование подсказок по применению индексов 8687 Отключает режим параллельного выполнения запроса 8722 Отключает использование всех прочих типов подсказок 8755 Отключает использование подсказок блокировки
Итоги 555 Итоги В этой главе мы рассмотрели множество недокументированных возможностей SQL Server: ■ недокументированные хранимые процедуры; ■ флаги трассировки; ■ команды DBCC; ■ функции. Повторю, что недокументированные возможности вы используете на свой страх и риск. Они не документированы главным образом потому, что компания Microsoft не желает, чтобы их использовали. Если вы решите применить их, то делайте это осторожно, учитывая то, что в последующих версиях SQL Server высока вероятность изменения недокументированной функциональности. Также не рассчитывайте на поддержку со стороны Microsoft. Для производителя важно то, что он не несет ответственность за недокументированную функциональность и может менять ее по своему усмотрению. Использовать недокументированные возможности любого продукта (не только SQL Server) не рекомендуется. Не следует применять их, если существует иной способ достижения желаемой цели. Недокументированные возможности были описаны в этой книге для того, чтобы показать внутреннюю работу SQL Server, понимание которой — ключ к написанию на Transact-SQL эффективных, сильных, расширяемых и быстрых программ.
Массивы Если вы хотите овладеть ремеслом программирования, то никогда не переставайте учиться. Никогда не удовлетворяйтесь уровнем своих знаний, всегда стремитесь расширить их и углубить. Отточите свой интеллект, и тогда в течение всей оставшейся жизни он будет «оттачивать» вас. X. В. Кентон В моей предыдущей книге, «Профессиональноеруководство по Transact-SQL», одна глава была посвящена обработке массивов на Transact-SQL. В ней описывалось несколько вариантов обойти тот факт, что в Transact-SQL нет встроенной поддержки массивов. В основе предложенных способов лежали два метода эмуляции поддержки массивов на Transact-SQL: использование таблиц (со столбцами, моделирующими размерность массива) и использованием больших полей varchar (когда одна размерность целиком хранится в одном поле). Хотя имитация массивов лучше, чем их полное отсутствие, в этой книге я выбрал другое решение проблемы: самостоятельно добавить в Transact-SQL поддержку массивов. Если вы помните, в предисловии я говорил о том, что секрет достижения мастерства в разработке хранимых процедур на Transact-SQL заключается в овладении мастерством программирования как такового и что главный путь приобретения навыков в программировании — изучение нескольких языков программирования. Я говорил о концепции «перекрестного опыления» и о тех преимуществах, которые можно получить, изучая языки с совершенно разными парадигмами создания программного обеспечения. Эта глава — свидетельство жизнеспособности этой философии. Изучение нескольких языков программирования (казалось бы, даже не связанных с Transact- SQL) повысит уровень программирования хранимых процедур и откроет вам новые методики и возможности, которые вы даже представить себе не могли. В этой главе я соединю концепции нескольких различных языков, в том числе C/C++, Clipper и, разумеется, Transact-SQL. Вы спросите: «Почему Clipper»? Зачем требуются С и C++ — понятно, поскольку мы используем их при написании расширенных хранимых процедур. Но Clipper? Дело в том, что Clipper представляет собой компилятор для языка программирования dBase-подобных баз данных, который пользовался популярностью до появления на свет Visual Basic (если вы не знакомы с dBase, поясню: это программируемая база данных, подобная Microsoft Access или Borland Paradox, наиболее популярная 23
xp_array.dll 557 до момента завоевания Access своих позиций). Clipper все еще существует, но он давно сдал свои позиции главного языка программирования баз данных ПК. Одна из удачных находок Clipper — добавление к языку программирования dBase-массивов. Это было сделано посредством функций (пользовательские функции dBase были также нововведением Clipper), которые обеспечивали создание массивов, поиск в них, их уничтожение и т. д. За кулисами происходил вызов С-кода, который в действительности манипулировал массивами. Так как вся игра с массивами осуществлялась посредством С-кода, операция по реализации массивов в Clipper происходила довольно быстро, и поскольку она была обеспечена несколькими простыми функциями, была несложной в использовании. Скоро вы увидите, что я использовал тот же подход для добавления поддержки массивов в Transact-SQL. Я создал UDF, которые служат «оберткой» вызовов расширенных хранимых процедур на C/C++. Обработка массивов происходит быстро вследствие «родной» компиляции всех алгоритмов, и она несложна в использовании, поскольку доступна посредством вызовов UDF. ВНИМАНИЕ Используйте эти процедуры с осторожностью. Помните, что вы делаете это на свой страх и риск. Ошибка освобождения массива сделает память, которую он занимает, недоступной для SQL Server. xp_array.dll Итак, давайте начнем разговор со знакомства с xp_array.dll, поскольку в пей содержится код, который работает с массивами. Вскоре вы увидите, что я сделал «обертку» для этих процедур в виде системных функций, чтобы упростить их использование, но вы, при желании, можете использовать их непосредственно. Вы можете найти xp_array.dll (и ее исходный код) на прилагаемом к книге компакт-диске. Хотя библиотека была скомпилирована бета-версией Visual Studio 7, она также легко может быть скомпилирована Visual Studio 6. Я не использовал ничего из .NET Frameworks, а также ничего специфического из Visual Studio. Xp_array.dll экспортирует пять расширенных процедур: xp_createarray, xp_setarray, xp_getarray, xp_destroyarray и xp_listarray. Чтобы сделать эти процедуры доступными на SQL Server, скопируйтеxp_array.dllв папку \BINN внутри папки с вашим экземпляром SQL Server. (Убедитесь, что это папка BINN, принадлежащая вашему экземпляру SQL Server, а не та, в которой хранятся исполняемые файлы средств для работы с сервером. Вы должны увидеть в ней другие файлы хр*.dll.) После того как DLL будет скопирована, запустите следующий сценарий: EXEC sp_addextendedproc 'xp_createarray','xp_array.dll' EXEC sp_addextendedproc 'xp_setarray'.'xp_array.dll' EXEC sp_addextendedproc 'xp_getarray'.'xp_array.dll' EXEC sp_addextendedproc 'xp_destroyarray'.'xp_array.dn' EXEC sp_addextendedproc 'xpjistarray', 'xp_array.dll' Это сделает расширенные хранимые процедуры, содержащиеся в xp_array.dll, доступными из Transact-SQL. В табл. 23.1 перечислены функции.
558 Глава 23. Массивы Таблица 23.1. Расширенные хранимые процедуры, предоставляемые xp_array.dll Процедура Назначение xp_createarray Создает массив в памяти и возвращает целочисленный указатель на него xp_setarray Устанавливает элемент массива xp_getarray Получает элемент массива xp_destrayarray Освобождает память, занимаемую массивом xpjistarray Возвращает массив как набор данныхн- xp_createarray Лучший способ понять работу каждой процедуры, содержащейся в xp_array.dll, исследовать ее код. Начнем с процедуры xp_createarray. Вот ее код: RETCODE _declspec(dllexport) xp_createarray(SRV_PRDC *srvproc) { int nParams; int size; char sizestr[30]; PBYTE* array - NULL; BYTE pbType; ULONG pcbMaxLen: ULONG pcbActualLen; BDOL pfNul1; nParams =■ srv_rpcparams(srvproc); // Проверка числа параметров if (nParams != 2) { // Посылаем сообщение "Ошибка выполнения расширенной хранимой процедуры: //неверное количество параметров" и возвращаемся srv_sendmsg(srvproc. SRV_MSG_ERROR. XP_ARRAY_ERROR, SRVJNFO. (DBTINYINT)O. NULL, 0, 0, "Error executing extended stored procedure: Invalid number of parameters". SRVJULLTERM); // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_DONE_ERROR | SRV_D0NE_M0RE), 0, 0); return(XP ERRDR); } if (!IntParam(D) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc, SRV_MSG_ERROR, XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O, NULL. 0, 0. "Error executing extended stored procedure: Invalid Parameter Type", SRVJULLTERM); // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRV D0NEJRR0R | SRV_DONE_MORE), 0. 0); return(XPJRROR); } if (!IntParamB)) {
xp_array.dll 559 // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc, SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O. -' NULL, 0, 0. "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM): // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRVJXMJRROR | SRV_DONE_M0RE). 0. 0): return (XPJRROR); } srv_paraminfo(srvproc,2.&pbType, SpcbMaxLen, SpcbActualLen. (BYTE *)&size, SpfNull): ++size: // Add one for the length element /* Шаг 1 - Размещение буфера для массива */ array = (PBYTE*) malloctsize * sizeof(PBYTE)); /* Шаг 2 - Очищаем массив */ memset(array, 0, size * sizeof(PBYTE)): /* Шаг 3 - Устанавливаем первый элемент равным длине массива */ itoa(size.sizestr.lO); setelement(array,0.sizestr): /* Шаг 4 - Возвращаем указатель на массив в выходной параметр */ srv_paramsetoutput(srvproc. 1, (BYTE *)&аггау, 4, FALSE): return XPJ0ERR0R ; } Xp_createarray принимает два параметра: выходной целочисленный параметр, в котором будет находиться указатель на вновь созданный массив, и целочисленный параметр, определяющий размер массива. Сначала она выделяет буфер необходимого размера, получаемого умножением требуемого числа элементов на размер указателя D байта), поскольку каждый элемент будет представлять собой указатель на строку после того, как она будет размещена (шаг 1). Затем процедура инициализирует массив нулями, так чтобы мы могли отличить размещенные элементы от неразмещенных (шаг 2). Потом она устанавливает первый элемент массива (нумерация начинается с нуля), равным строке, содержащей длину массива. Таким образом, массив является самоописывающим: он знает свой собственный размер. Это свойство нам пригодится, когда придет время освобождать массив. И наконец, процедура возвращает указатель на массив в своем выходном параметре. xp_setarray Следующей идет процедура xp_setarray. Она позволяет присваивать элементу массива требуемое значение. Вот ее код: RETCODE _declspec(dllexport) xp_setarray(SRV_PR0C *srvproc) { int nParams; DBINT paramtype: int index:
560 Глава 23. Массивы int handle: TCHAR szValue[8000+l]: PBYTE* array = NULL: BYTE pbType; ULONG pcbMaxLen: ULONG pcbActualLen: BOOL pfNul1: nParams = srv_rpcparams(srvproc): // Проверка числа параметров if (nParams != 3) { // Посылаем сообщение "Ошибка выполнения расширенной хранимой процедуры: //неверное количество параметров" и возвращаемся srv_sendmsg(srvproc. SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O. NULL. 0, 0. "Error executing extended stored procedure: Invalid number of parameters", SRVJULLTERM): // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE), 0. 0): return(XP_ERR0R); } if (UntParamd)) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O, NULL. 0, 0, "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM): // Используем SRV_D0NE_M0RE вместо SRVJ30NEJINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE), 0, 0): return(XP ERROR): if (!IntParamB)) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc, SRV_MSG JRR0R. XP_ARRAY_ERR0R, SRVJNFO. (DBTINYINT)O. NULL. 0, 0. "Error executing extended stored procedure: Invalid Parameter Type", SRVJULLTERM); // Используем SRV_D0NE_M0RE вместо SRVJDONEJINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRVJD0NEJRR0R | SRV_D0NE_M0RE). 0. 0): return(XP ERROR); paramtype = srv_paramtype(srvproc. 3); if (paramtype != SRVVARCHAR) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc, SRV_MSGJRR0R, XP_ARRAY_ERR0R. SRVJNFO, (DBTINYINTH. NULL, 0, 0.
xp_array.dll 561 "Error executing extended stored procedure: Invalid Parameter Type", SRVJULLTERM); // Используем SRVJONEJWRE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0): return(XPJRROR); } srv_paraminfo(srvproc,l.&pbType. SpcbMaxLen, SpcbActualLen, (BYTE *)&handle, SpfNull); srv_paraminfo(srvproc.2,&pbType, SpcbMaxLen, SpcbActualLen. (BYTE *)&index, SpfNull): srv_paraminfo(srvproc.3,&pbType. SpcbMaxLen. SpcbActualLen. (BYTE *)szValue. SpfNull): array=(PBYTE *)handle; /* Шаг 1: Проверка корректности индекса элемента */ if (index>(getarraysize(array)-D) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: // выход индекса за границы массива" srv_sendmsg(srvproc, SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O, NULL. 0. 0, "Error executing extended stored procedure: Array index out of range". SRVJULLTERM): // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRVJXMJRROR | SRVJ30NEJI0RE). 0. 0): return(XP_ERROR): } /* Шаг 2: Добавляем символ конца строки */ szValue[pcbActualLen]='\0'; /* Шаг 3: Устанавливаем значение элемента */ return setelementtarray.index.szValue); } Xp_setarray принимает три параметра: указатель массива, номер устанавливаемого элемента и строку, содержащую устанавливаемое значение. После проверки правильности параметров процедура сначала проверяет указанный индекс: не выходит ли он за границы массива. Если индекс неверен, появляется сообщение об ошибке и процедура завершает свою работу. Далее, xp_seta r ray добавляет символ конца строки к значению, полученному от пользователя. Поскольку xp_setarray работает с элементами массива как со строками, важно, чтобы они имели правильное завершение (оканчивались нулем). Мы находим начало данных каждого элемента массива, вычисляя его смещение в буфере массива, и его конец поиском символа конца строки. Наконец, xp_setarray устанавливает элемент, вызвав внутреннюю функцию set element (), и возвращает результат клиенту. Setelement() просто копирует строку, определенную пользователем, в массив по указанному индексу. xp_getarray Xp_getarray получает значение из массива, ранее установленное посредством xp_setarray. Вот ее код: RETC0DE declspec(dllexport) xp_getarray(SRV_PROC *srvproc) { int nParams; 19 Зак. 983
562 Глава 23. Массивы DBINT paramtype; int index; int handle: TCHAR szValue[8000+l] = "": PBYTE* array = NULL: BYTE pbType; ULONG pcbMaxLen; ULONG pcbActualLen: BOOL pfNull: nParams = srv_rpcparams(srvproc); // Проверка числа параметров if (nParams != 3) { // Посылаем сообщение "Ошибка выполнения расширенной хранимой процедуры: //неверное количество параметров" и возвращаемся srv_sendmsg(Srvproc, SRV_MSG_ERROR. XP_ARRAY_ERROR. SRV_INF0, (DBTINYINT)O. NULL, 0. 0, "Error executing extended stored procedure: Invalid number of parameters", SRVJULLTERM): // Используем SRV_DONE_MORE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRV_DDNE_ERROR | SRV_DONE_MORE). 0. 0): return(XPJRROR); } if (!IntParam(D) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRVJSG JRROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O. NULL. 0. 0. "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM); // Используем SRV_DONE_MORE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_DONE_ERROR | SRV_DONE_MORE). 0. 0); return(XPJRROR); if (!IntParamB)) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRV JSG JRROR. XP_ARRAY_ERROR, SRVJNFO. (DBTINYINT)O, NULL. 0, 0. "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM); // Используем SRV_DONE_MORE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRVJXMJRROR | SRV_DONE_MORE). 0. 0); return(XP_ERROR); paramtype = srv_paramtype(srvproc. 3): if (paramtype != SRVVARCHAR) {
xp_array.dll 563 // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O. NULL. 0. 0. "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM); // Используем SRV_DONE_MORE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_DONE_ERROR | SRV_DONE_MORE). 0. 0): return(XP_ERROR): } srv_paraminfo(srvproc.l.&pbType, SpcbMaxLen. SpcbActualLen. (BYTE *)&handle, SpfNull); srv_paraminfo(srvproc,2,&pbType. SpcbMaxLen. SpcbActualLen, (BYTE *)&index. SpfNull): array=(PBYTE *)handle; /* Шаг 1: Получаем значение, запрошенное пользователем */ if (array[index]!=NULL) { strcpy(szValue.(char *)array[index]); } /* Шаг 2: Возвращаем значение в выходном параметре */ srv_paramsetoutput(srvproc, 3, (BYTE *)szValue. strlen(szValue). FALSE): return XPJOERROR; } Xp_getarray принимает на входе три параметра: указатель массива, индекс элемента, который требуется получить, и выходной параметр для получения значения элемента. После проверки правильности параметров xp_getarray вначале копирует значение, запрошенное пользователем, в символьный буфер, затем копирует этот буфер в выходной параметр. xp_destroyarray Как и следует из имени процедуры, xp_destroyarray удаляет массив, ранее созданный процедурой xp_createarray. Вот код xp_destroyarray: RETC0DE declspec(dllexport) xp_destroyarray(SRV_PROC *srvproc) { int nPa rams: int index; int handle: int size: char msg[255]; PBYTE* array = NULL: BYTE pbType; ULONG pcbMaxLen; ULONG pcbActualLen; BOOL pfNull; nParams = srv_rpcparams(srvproc);
564 Глава 23. Массивы // Проверка числа параметров if (nParams != 1) { // Посылаем сообщение "Ошибка выполнения расширенной хранимой процедуры: //неверное количество параметров" и возвращаемся srv_sendmsg(srvproc. SRV_MSG_ERROR. XP_ARRAY_ERROR. SRVJNFO. (DBTINYINT)O. NULL. 0. 0, "Error executing extended stored procedure: Invalid number of parameters". SRVJULLTERM); // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0): return(XPJRROR): } if (!IntParam(D) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRV_MSG_ERROR, XP_ARRAY_ERROR. SRVJNFO, (DBTINYINT)O. NULL. 0. 0. "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM); // Используем SRV_D0NE_M0RE вместо SRV_DONE_FINAL дпя заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE), 0, 0): return(XPJRROR); } srv_paraminfo(srvproc.l,&pbType. SpcbMaxLen, SpcbActualLen, (BYTE *)&handle, &pfNull): array=(PBYTE *)handle; size=getarraysize(array); /* Шаг 1: Освобождаем все элементы массива */ for (index = 0: index < size: index++) if (array[index]!=NULL) free(array[index]); /* Шаг 2: Освобождаем сам массив */ free(array): return XPJOERROR : } Xp_destгоуаггау принимает один параметр: указатель на массив, который должен быть освобожден. Это целое число, которое должна возвратить процедура xp_createarray. После проверки этого единственного параметра xp_destгоуаггау начинает освобождать все элементы массива. Исследуя значение элемента с индексом 0, она узнает, сколько существует элементов массива (getarraysize() возвращает содержимое нулевого элемента как целое значение). После освобождения всех элементов xp_dest гоуаггау освобождает сам массив. Помните, что массив сам по себе является просто коллекцией указателей. В этих указателях изначально содержится значение null. Элемент массива не указывает на что-либо, для него также не выделена память, пока xp_setarray не вызван для установления его значения. Такой подход позволяет минимизировать объем используемой памяти при работе с очень большими массивами.
xp_array.dll 565 xp_listarray Xp_listar ray возвращает содержимое массива как набор данных. Конечно, мы могли бы в цикле вызвать xp_geta г гау для того, чтобы по одному получить все элементы массива, однако использование xp_listarray более эффективно, поскольку эта процедура захватывает все элементы целиком. Вот ее код: RETC0DE declspec(dlI export) xp_listarray(SRVJ>ROC *srvproc) int nParams: int index: int handle; int size; int len; char* emptystr = ""; PBYTE* array PBYTE* ppData = NULL; = NULL BYTE pbType; ULONG pcbMaxLen; ULONG pcbActualLen; BOOL pfNull; nParams = srv_rpcparams(srvproc); // Проверка числа параметров if (nParams != 1) { // Посылаем сообщение "Ошибка выполнения расширенной хранимой процедуры: //неверное количество параметров" и возвращаемся srv_sendmsg(srvproc, SRV_MSG_ERROR. XP_ARRAY_ERROR, SRVJNFO, (DBTINYINT)O. NULL, 0. 0, "Error executing extended stored procedure: Invalid number of parameters". SRVJULLTERM); // Используем SRVJ30NEJI0RE вместо SRV_DONE_FINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc. (SRV_D0NE_ERR0R | SRV_D0NE_M0RE). 0. 0); return(XP_ERROR); } if (!IntParam(D) { // Вывод сообщения: "Ошибка выполнения расширенной хранимой процедуры: //неверный тип параметра" srv_sendmsg(srvproc. SRVJISGJRROR, XP_ARRAY_ERROR, SRV_INF0. (DBTINYINT)O. NULL. 0. 0, "Error executing extended stored procedure: Invalid Parameter Type". SRVJULLTERM): // Используем SRV_D0NE_M0RE вместо SRVJ30NEJINAL для заполнения // результирующего набора данных расширенной хранимой процедуры. srv_senddone(srvproc, (SRVJONEJRROR | SRV_D0NE_M0RE). 0. 0): return(XP ERROR); srv_paraminfo(srvproc.l,&pbType. SpcbMaxLen, SpcbActualLen. (BYTE *)&handle, SpfNul array=(PBYTE *)handle;
566 Глава 23. Массивы size=getarraysize(array): len = 255: /* Шаг 1: Создание результирующего набора данных */ for (index = 1; index < size; index++) { srv_descnbe(srvproc, 1. "idx", SRVJULLTERM, SRVINT4. // Тип данных получателя. (DBINT) sizeof(SRVINT4). // Размер данных получателя. SRVINT4. // Размер данных источника. (DBINT) sizeof(SRVINT4). // Размер данных источника. (PBYTE) Sindex); srv_descnbe(srvproc. 2, "value", SRVJULLTERM. SRVVARCHAR, // Тип данных получателя. (DBINT) len. // Размер данных получателя. SRVVARCHAR, // Размер данных источника. (DBINT) len. // Размер данных источника. (PBYTE) NULL); // srv_setcollen(srvproc, 1.index): /* Шаг 2: Копируем данные массива в ряд набора данных для посылки ряда клиенту. */ if (array[index]!=NULL) srv_setcoldata(srvproc, 2. array[index]); else srv_setcoldata(srvproc. 2. emptystr); /* Шаг З: Посылаем ряд клиенту */ if (srv_sendrow(srvproc) != SUCCEED) goto safeexit: } safeexit: /* Шаг 4: Указываем, что набор данных заполнен */ if (index > 0) srv_senddone(srvproc, SRV_D0NE_M0RE | SRV_D0NE_C0UNT, (DBUSMALLINT)O. index): else srv_senddone(srvproc, SRV_D0NE_M0RE. (DBUSMALLINT)O. (DBINT)O); return XPJOERROR : } Xp_lista г гау принимает единственный параметр: указатель на массив, который должен быть выведен. Вначале процедура создает набор данных, в котором будет возвращен массив (шаг 1). Этот набор данных состоит из двух полей: idx и value. Поле idx будет содержать индекс каждого элемента массива, поле value — его значение в виде строки. После инициализации набора данных xp_listarray перебирает элементы массива, устанавливая поля набора данных соответственно значениям текущего элемента массива (шаг 2). Как только значения полей каждой строки установлены, xp_listarray возвращает текущий элемент массива посредством вызова процедуры srv_sendrow() (шагЗ). После того как все ряды отправлены клиенту, xp_lista г гау помечает набор данных как завершенный и завершает работу. Как вы вскоре убе-
Системные функции для работы с массивами 567 дитесь, вызов xp_lista r ray является более эффективным и быстрым способом возвращения содержимого массива как набора данных, хотя мы можем с легкостью написать табличную UDF, которая будет возвращать массив как таблицу. Для того чтобы увидеть, как это все работает, запустим следующий сценарий. Он создает массив, устанавливает элемент, возвращает элемент, выводит массив и удаляет его. Вот код этого сценария: DECLARE @hdl int. @siz int set @siz=1000 EXEC master..xpj:reatearray @hdl OUT. @siz SELECT @hdl AS ArrayHandle EXEC master..xp_setarray @hdl.998,'test5' DECLARE lvalue varcharC0) EXEC master..xp_getarray @hdl,998.lvalue OUT SELECT lvalue AS ArrayValue EXEC master, .xpjistarray @hdl EXEC master..xp_destroyarray @hd1 (Результаты сокращены) ArrayHandle 13910056 ArrayValue test5 idx value 1 2 3 4 5 6 996 997 998 test5 999 1000 Системные функции для работы с массивами После добавления расширенных хранимых процедур на сервер для их вызова нам необходимо создать системные функции. Это сделает наше обращение с массивами более простым и функциональным по сравнению с использованием расширенных хранимых процедур. Из главы 10 вы помните, что системные функции могут быть созданы через недокументированный процесс привлечения фиктивного пользователя system_f unction_schema. Мы создадим эти функции для работы с массивами как системные функции, тогда они будут доступны из любой базы без требуемого префикса имени базы данных. Вот код, который создает эти функции: USE master GO EXEC sp_configure 'allow updates'.1 GO
568 Глава 23. Массивы RECONFIGURE WITH OVERRIDE GO DROP FUNCTION system_function_schema.fn_createarray, system_function_schema.fn_setarray, system_function_schema.fn_getarray. system_function_schema.fn_destroyarray. system_function_schema.fn_listarray. system_function_schema.fn_arraylen GO CREATE FUNCTION system_function_schema.fn_createarray(@size int) RETURNS int AS BEGIN DECLARE @hdl int EXEC master, . xpjxeatearray @hdl OUT. @size RETURN(@hdl) END GO CREATE FUNCTION system_function_schema.fn_destroyarray(@hdl int) RETURNS int AS BEGIN DECLARE @res int EXEC @res=master..xp_destroyarray @hdl RETURN(@res) END GO CREATE FUNCTION system_function_schema.fn_setarray(@hdl int. @index int. @value sql_variant) RETURNS int AS BEGIN DECLARE @res int. @valuestr varchar(8000) SET @valuestr=CAST((?value AS varchar(8000)) EXEC @res=master..xp_setarray @hdl, @index. @valuestr RETURN(@res) END GO CREATE FUNCTION system_function_schema.fn_getarray(@hdl int. @index int) RETURNS sql_variant AS BEGIN DECLARE @res int. @valuestr varchar(8000) EXEC @res=master..xp_getarray @hdl. @index. @valuestr OUT RETURN((?valuestr) END GO CREATE FUNCTION systemJiinction_schema.fnJistarray(@hdl int) RETURNS @array TABLE (idx int. value sq1_variant) AS BEGIN DECLARE @i int. @cnt int SET @cnt=CAST(fn_getarray№hdl.O) AS int) SET @i=l WHILE (@i<@cnt) BEGIN INSERT @array VALUES (@i. fn_getarray(@hdl .<?i)) SET @i=(ai+l END RETURN END GO CREATE FUNCTION system_function_schema.fn_arraylen(@hdl int) RETURNS int
Главное блюдо 569 AS BEGIN RETURN(CAST(fn_getarray(@hdl.O) AS int)-l) END GO EXEC sp_configure 'allow updates',0 GO RECONFIGURE WITH OVERRIDE GO Этот сценарий создает шесть функций: fn_createarray(), f n_setarray(), fn_getarray(), fn_destroyarray(), fn_listarray() и fn_arraylen(). Функция fn_createarray() служит для создания массива, fn_setarray() задает элемент массива, fn_getarray() в качестве результата возвращает элемент массива, fn_destroyarray() уничтожает массив, fn_listarray() превращает массив в таблицу, a f n_arraylen() возвращает размер массива. Функции f n_getarray() и fn_setarray() работают с элементами массива как с типами variant, поэтому вы можете хранить в массиве любые типы данных, которые могут быть преобразованы из типа variant в тип string и обратно. Например, вы можете передать дату в функцию f n_setarray () (которая получит ее как va riant), и функция перед вызовом xp_setarray преобразует ее в string. Подобным образом вы можете получить дату, хранящуюся в массиве, используя f n_getarray(), и прямо присвоить ее переменной или полю типа datetime. Функция за вас выполнит преобразование строкового элемента. Главное блюдо Для того чтобы оценить мощь созданного нами инструмента работы с массивами, опробуем его на практике. Ниже представлен пример простого сценария Transact- SQL, который использует нашу новую функциональность для того, чтобы создать массив, произвести манипуляции с ним, вывести его содержимое и уничтожить его: DECLARE @hdl int. @siz int. @res int SET @siz=1000 -- Создаем массив и возвращаем его указатель и размер SET @hdl=fn_createarray(@siz) SELECT @hdl. fn_arraylen(@hdl) -- Устанавливаем значения элементов 10 и 998 SELECT @res=fn_setarray((?hdl.10.'testlO'). @res=fn_setarray(@hdl.998.'test998') -- Получаем элемент 10 SELECT fn_getarray(@hdl,10) -- Получаем элемент 998 SELECT fn_getarray(@hd1.998) -- Выводим содержимое массива SELECT * from ; :fnjistarray(@hdl) SET @res=fn_destroyarray(@hdl) (Результаты сокращены)
570 Глава 23. Массивы 13910056 1000 testlO test998 idx value 2 3 4 5 6 7 8 9 10 testlO 11 12 13 14 15 16 995 996 997 998 test998 999 1000 A000 row(s) affected) Как вы видите, использовать массивы в Transact-SQL стало просто: необходимо всего лишь вызвать функцию. И поскольку массивы доступны посредством функций, мы можем легко пользоваться ими при работе со значениями, содержащимися в таблицах. Например: DECLARE @h int. @res int. @arraybase int -- Создание массива SELECT @h=fn_createarray(l000), @arraybase=10247 -- Загрузка дат таблицы Order в массив SELECT @res=fn_setarray(@h,OrderId-@arraybase.OrderDate) FROM Northwind..orders -- Вывод элементов массива SELECT idx+@arraybase AS Orderld, value AS OrderDate FROM : :fnjistarray(@h) WHERE idx=10249-@arraybase -- Удаление массива SET @res=fn_destroyarray(@h) В этом примере мы загрузили в массив поле 0 rde rDate для всех заказов в таблице Nort hwind. 0 rde rs. Мы установили значение переменной @аг raybase таким образом, чтобы использовать Orderld как индексатор массива (в базе данных Northwind Orderld начинается с 10248, таким образом, вычитая 10247 из каждого значения, мы получим массив, индексируемый начиная с единицы). На машине, которую я
Многомерные массивы 571 использовал для написания этой книги, загрузка массива шла менее, чем полсекунды. После загрузки в память мы использовали табличную функцию f n_listar ray() для нахождения в массиве определенного заказа. Многомерные массивы Поскольку элементы массива могут хранить данные любого типа, они могут также хранить указатели на другие массивы. Это значит, что вы можете легко создать многомерные массивы и что эти массивы могут быть обычными или вложенными. Рассмотрим пример: DECLARE @yhdl int, @xhdl int, @xsiz int. @ysiz int. @res int. @xcnt int. @ycnt int SELECT @ysiz=20. @xsiz=10 -- Размещаем у-размерность SET @yhdl=fn_createarray(@ysiz) -- Размещаем и заполняем каждый ряд SET @ycnt=l WHILE @ycnt<=@ysiz BEGIN SET @xhdl=fn_createarray(@xsiz) SET @res=fn_setarray(@yhdl,@ycnt.@xhdl) SET @xcnt=l WHILE @xcnt<=@xsiz BEGIN SET @res=fn_setarray(@xhdl.@xcnt.RAND()*100) SET @xcnt=@xcnt+l END SET @ycnt=@ycnt+l END -- Выводим каждый ряд SET @ycnt=l WHILE @ycnt<=@ysiz BEGIN PRINT 'Listing row: '+CAST(@ycnt AS varchar) SET @xhdl=CAST(fn_getarray(@yhdl,@ycnt) AS int) SELECT * FROM ::fn_listarray(@xhdl) SET @ycnt=@ycnt+l END -- Получаем значение по координатам х.у SELECT fn_getarray(CAST(fn_getarray(@yhdl,16) AS int),9) AS 'Element at [9.16]' -- Удаляем каждый ряд SET @ycnt=l WHILE @ycnt<=@ysiz BEGIN SET @xhdl=CAST(fn_getarray(@yhdl.@ycnt) AS int) SET @res=fn_destroyarray(@xhdl) SET @ycnt=@ycnt+l END -- Удаляем у-размерность SET @res=fn_destroyarray(@yhdl) (Результаты) Listing row: 1
572 Глава 23. Массивы idx value 28.7541 2 89.3502 3 3.53946 4 23.5332 5 86.0147 6 65.5272 7 55.1878 8 28.106 9 54.9643 10 45.7077 Listing row: 2 idx value 62.1757 2 88.8092 3 83.9364 4 48.1814 5 46.8372 6 11.51 7 66.9179 8 51.3207 9 87.2797 10 3.83372 Listing row: 3 idx value 60.2789 2 4.09385 3 37.9455 4 2.57299 5 52.0562 6 70.8885 7 47.8154 8 54.1449 9 59.3168 10 87.9367 Listing row: 4 idx value 24.996 2 94.1695 3 99.9406 4 18.491 5 87.2228 6 30.5012 7 21.4947 8 68.7588 9 78.544 10 80.717 Listing row: 16 idx value 72.5644
Многомерные массивы 573 г-о 3 4 5 6 7 8 9 10 11.6483 98.446 63.0639 64.6387 71.462 18.1232 69.4337 14.1641 12.0571 Listing row: 17 idx value 12.8128 2 3 4 5 6 7 8 9 10 49.1211 44.5183 97.7341 79.8344 94.7446 93.0003 63.0217 31.3682 41.8146 Listing row: 18 idx value 33.1187 2 23.9623 3 22.8832 4 15.6967 5 88.0725 6 31.4168 7 71.7862 8 99.8463 9 70.8513 10 99.2734 Listing row: 19 idx value .3026 2 3 4 5 6 7 8 9 10 Listing idx 57.3852 2 3 4 26.2505 30.7053 16.8188 25.4275 46.9594 39.897 36.4633 18.7707 15.3608 row: 20 value 70.897 4.85462 17.4024
574 Глава 23. Массивы 5 6 7 8 9 10 Element at 28.5141 92.8508 19.5683 50.7395 89.062 94.2366 [9.16] 14.1641 В этом примере мы сначала создали у-размерность двумерного массива, затем использовали цикл, в котором отдельно поместили каждый ряд и заполнили его серией случайных чисел с плавающей точкой. Конечно, мы могли бы с тем же успехом хранить в массиве любые последовательности значений, в том числе значения, хранящиеся в таблицах, как было показано выше. Мы храним указатель на массив, который представляет собой ряд, как элемент у-массива. Это позволяет нам получить доступ к элементам массива, используя координаты X и Y. Поскольку SQL Server не позволяет произвести неявное преобразование sql_variant в integer, мы должны сделать это сами, используя CAST(), но это всего лишь небольшое неудобство. Теперь очевидно, что вы можете иметь необходимое вам количество размерностей массива и что эти размерности могут быть как обычными, так и вложенными (с переменным количеством элементов). Итоги В этой главе вы получили основные сведения о массивах Transact-SQL. Transact- SQL не предлагает нам встроенной функции поддержки массивов, однако: ■ мы реализовали встроенную функцию поддержки массивов сами, используя расширенные процедуры и системные функции; ■ мы получили функциональность, которая является вполне замкнутой, эффективной и быстрой; ■ мы добавили эту мощную функциональность к Transact-SQL благодаря нашим знаниям и знакомству с другими языками и инструментами. Представленная здесь модель массивов объединяет в себе сильные стороны нескольких языков: она базируется на функциональности, первоначально появившейся в Clipper и реализованной на C/C++ и Transact-SQL.
Часть 5 ■л Размышления о программной инженерии
<ул Обустройство £*т рабочего места Концентрация внимания — залог создания хорошего программного обеспечения. Если вас постоянно прерывают и отвлекают, то большое количество ошибок и плохой код гарантированы. Ким Кокконен Мой первый рабочий день на моей первой работе программистом был самым обыкновенным. Секретарша вела меня по многочисленным коридорам до тех пор, пока мы не оказались в офисе, где не было ни души. «Это ваше рабочее место», — сказала она, направляясь к небольшому столу, рядом с которым расположился стул без подлокотников и принтер, жадно заглатывавший бумагу и что-то печатавший в тот момент. Секретарша заметила мое замешательство. Понятное дело, я не надеялся получить отдельный кабинет, но мне и в голову не приходило, что я буду делить рабочее место с шумной машиной. Со временем я стал более терпимым к шуму принтера — под его шум я пытался научиться программированию, читая книги и изучая коды, — но сконцентрироваться было сложно. Я был неуверен в себе, и шанс получить место, более подходящее для умственной работы и обучения, был очень невелик. Принтер работал постоянно, но иногда поздно вечером он останавливался. В такие моменты я испытывал воодушевление, мне казалось, что у меня есть настоящая работа и у нее есть будущее. В то время я часто работал допоздна. Однажды я решил пройти по зданию в свой офис другим путем и вдруг оказался возле двери, которую никогда раньше не замечал. Я медленно открыл дверь — это была кладовка примерно два метра в ширину и три-четыре в глубину. С одной стороны была лестница, ведущая на чердак, с другой — стол, стул и пара старых компьютеров. Все в комнате было покрыто пылью. Я аккуратно закрыл дверь и пошел было дальше, когда вдруг меня осенила мысль: каким прекрасным рабочим кабинетом это помещение могло бы стать! Я вернулся, открыл дверь и осмотрел кладовку еще раз. «Здесь у меня действительно есть шанс сосредоточиться», — подумал я. Собрав свои книги н распечатки и сказав «прощай» шумному соседу по офису, я устроился в новом «офисе» и следующие несколько часов работал, пребывая в состоянии восторга от тишины и своего открытия. Итак, теперь каждый вечер, когда мои коллеги расходились, я менял место работы. Я собрал работающий терминал, соединив те части, которые я нашел. Я про-
Избавьтесь от отвлекающих факторов 577 тянул кабель к мэйнфрейму через чердак и с юношеской смелостью включил его. К моему удивлению и облегчению, он заработал. Теперь у меня был компьютер. Жизнь налаживалась. Я радовался, как ребенок, пока однажды вечером в дверь моей кладовки не постучали. Дверь открылась. За ней стоял мой начальник. «Что вы тут делаете?» — спросил он. «Работаю, — ответил я робко. — Я не могу сконцентрироваться на своем рабочем месте». «Где вы все это взяли?» — спросил начальник, направляясь к терминалу. «Он уже был здесь, я только собрал вместе все его части», — ответил я. Начальник постоял, обдумывая мои слова. Мне казалось, я слышу, как одна мысль в его голове сменяет другую. «Что ж, придется разобраться с этим, — сказал он. — Сейчас вам лучше пойти домой. Уже поздно». В тот вечер я ушел из офиса через черный ход, будучи уверен в том, что потерял работу. На улице было темно, и я решил немного прогуляться по городу, прежде чем вернуться домой. Я ходил вверх и вниз по улицам старого города, раздумывая о том, что только что произошло. Есть ли еще у меня работа? Что я сделал неправильно? Следовало ли мне сначала спросить разрешения? Что мне делать теперь и как я буду платить за жилье? В конце концов, я сел на ступеньки около дверей здания банка, откуда я отчетливо видел любителей кино, входящих и выходящих из кинотеатров. Я смотрел на них, и мне казалось, что время остановилось. Согласно табличке на фасаде здания, оно было построено в 1920-х годах. Готические украшения и гранитные колонны напоминали о давно прошедших временах. Я сидел и думал, что мне делать дальше, а горгульи и грифоны смотрели на меня с выступов здания. Проанализировав ситуацию раз за разом, я пришел к выводу, что трудно решить, кто здесь не прав: ожидать от меня хороших результатов, предоставив мне такое шумное рабочее место, — было неразумно со стороны компании. Если меня уволят за то, что я хотел исправить несправедливое отношение ко мне, — мне придется искать новую работу. Но я понял одно: тишина и покой кладовки позволили мне сделать работу, за которую я получал деньги. И если меня попросят уйти, я уйду, чтобы найти такую же «кладовку». Избавьтесь от отвлекающих факторов Вы спросите: «Какова мораль этой истории?» Найти тихое место для работы. Очень немногим удается сконцентрироваться, когда вокруг шумно или когда их часто отвлекают. Программирование — это мыслительный процесс. Оно требует большой концентрации внимания. Все фазы разработки программного обеспечения, особенно дизайн и анализ, требуют глубокого обдумывания. Позаботьтесь о том, чтобы создать атмосферу, подходящую для этого. Уединитесь, наденьте наушники, сделайте все для того, чтобы избавиться от отвлекающих факторов (даже если для этого вам придется работать в кладовке). Конечно, есть люди, которые не испытывают неудобств, работая в шумном офисе. Если вы один из них — вам повезло. Остальные простые смертные нуждаются в тишине и покое для плодотворной работы. Чрезвычайно важно, чтобы ваше рабочее место было обустроено до того, как вы приступите к созданию про-
578 Глава 24. Обустройство рабочего места граммы. Вы обнаружите, что делаете меньше ошибок и получаете больше удовольствия от процесса. Закройте дверь В идеале при работе необходимо отгородиться от всего мира дверью, чтобы исключить отвлекающие факторы. Если это невозможно в вашей компании, то, как минимум, попытайтесь добиться некой отгороженной области в офисе. Если вы работаете дома, то всем должно быть известно, что кабинет — это то место, где вы работаете. Делайте все, что в ваших силах, чтобы устранить шум и сконцентрироваться на работе. Некоторые считают, что наушники помогают сосредоточиться. Я иногда отключаю телефон, когда работаю над чем-то очень сложным — для таких случаев есть голосовая почта. Если вы работаете дома, то вам будет полезно установить несколько правил. Например, если я работаю в кабинете, закрывшись иа замок, это означает, что я не хотел бы, чтобы меня беспокоили. Если я оставляю дверь открытой — мои домашние знают, что я не занят ничем таким, что требует большой концентрации внимания, и могут войти ко мне, когда захотят. Я стараюсь оставлять дверь открытой и запираю ее только тогда, когда мне это совершенно необходимо. Если моя дверь открыта, ко мне в кабинет часто приходят мои дети. Я всегда рад им. На самом деле, люди обычно не хотят мешать тем, кто занят чем-то серьезным. Главное — дать им понять, когда это происходит. Внутренние отвлекающие факторы Иногда отвлекающие факторы не являются внешними по своей природе. Иногда их можно обнаружить в самом кабинете. Одним из таких факторов для меня был «одноглазый монстр» — телевизор. Несколько лет назад я все же решил, что в кабинете ему не место и убрал его. Слишком велико было желание включить какую- нибудь программу, пока я ждал завершения компиляции или передачи файла. Возможно, у вас больше силы воли, чем у меня, но я рекомендую вам убрать из кабинета все, что может отвлекать. Очень сильно мешать в работе может Интернет. Всемирная паутина — замечательная информационная кладовая. Благодаря ей, изменился способ работы людей с компьютером, люди на разных континентах стали ближе друг к другу. Однако Интернет — самый большой пожиратель времени из всех когда-либо изобретенных. Если вы вдруг обнаружите, что путешествуете по Интернету в то время, когда следует работать, найдите способ справиться с этим. То же самое касается перерывов на проверку электронной почты. Советую отказаться от звукового извещения о поступлении новых сообщений в вашей почтовой программе. Я рекомендую вознаграждать себя за успешное выполнение задания: «погулять» по Сети, прочитать электронную почту, поиграть в игры, посмотреть новости, поучаствовать в чатах и тому подобное — но потом, когда работа (или ее часть) сделана. Не отвлекайте себя сами. Самоконтроль имеет большое значение.
Эпилог 579 Форма без содержания Вас могут отвлекать не только телефонные звонки. Мешать вашей работе может прекрасный вид из окна, какие-нибудь технологичные приспособления в офисе, «игрушки» программистов. Иногда работе мешают вещи, являющие собой образец формы без содержания. Вы покупаете монитор с плоским экраном и мышь за $100, но никогда не доходите собственно до работы. В своей замечательной книге «On Writing» Стивен Кинг написал следующее о приоритетах рабочего места: «Начните с того, что поставьте свой письменный стол в угол и каждый раз, когда садитесь за него, напоминайте себе, почему стол расположен не в центре комнаты. Жизнь — это не система поддержки искусства. Это другой путь»1. Необходимо соблюдать принцип равновесия и помнить о том, что самое лучшее оборудование в мире не сможет помочь вам, если оно будет отвлекать вас от работы. Тихое рабочее место, компьютер, книги, которые вы покупаете, — вот ваши средства. Вы программист потому, что вам нравится создавать. Так избавьтесь от того, что мешает этому. Молчим — работая, общаемся — отдыхая Конечно, вам не хочется казаться замкнутым и необщительным человеком. Но хочу ободрить вас: если люди знают, что вам можно звонить только в определенное время, то им будет легче связаться с вами именно в это время. В конце концов, они только хотят поговорить с вами. Благодаря тому, что вы сможете полностью отдаваться работе в определенные часы, вам будет легче общаться с друзьями и родными в свободное от работы время. Нет ничего плохого в том, чтобы планировать время работы и отдыха. Разумные люди поддержат вас в этом стремлении. Заключение Первое, что необходимо сделать перед тем, как приступить к программированию, — обустроить свое рабочее место. Убедитесь, что ваше место работы тихое и нет отвлекающих факторов (как внутренних, так и внешних). Создание хороших программ требует концентрации внимания. Сделайте все возможное для того, чтобы способствовать этому. Эпилог На следующее утро мой начальник сказал: «Вам не следовало работать в кладовке, — у меня опустилось сердце. — Сегодня утром я осмотрел коридор и был удив- King, Stephen. On Writing. New York: Scribner, 2000. С 101.
580 Глава 24. Обустройство рабочего места лен тем, что обнаружил. Пойдемте». Он встал из-за стола и жестом пригласил меня следовать за ним. Я шел по знакомому коридору молча, как ягненок на заклание. Начальник подошел к кладовке, остановился на мгновение и открыл соседнюю дверь. Он вошел в комнату, включил свет и сказал: «Это ваше новое рабочее место. Вы проявили инициативу и изобретательность, чтобы обустроить свое рабочее место. Нам нужны инициативные и изобретательные. Конечно, вам следовало прийти ко мне, если возникла проблема. В следующий раз обязательно приходите». Он положил мне в руку ключ и оставил меня стоящим в безмолвии в центре моего собственного кабинета.
Эволюция разработки программного обеспечения В настоящее время то, что происходит в индустрии программного обеспечения, напоминает мне следующую ситуацию: 75 % докторов все еще используют пиявки и горчичники, а 25 % докторов, протестировав пенициллин и обнаружив его полезные свойства, уже начали применять его в своей практике. Стив Маккопнелл* Прохладными весенними вечерами, когда солнце прячется за красными кронами дубов северной Вирджинии, я обычно гуляю от гостиницы, расположенной в центре Вашингтона, к мемориалу Линкольна. Наступает вечер, и я удаляюсь от суматохи и шума города. Я подхожу к широкой аллее, уходящей вдаль. В лучах закатного солнца парк становится похож на средневековой лес, скрывающий в себе множество загадок и тайн. Я прохожу мимо величественного здания Смитсонианского института (Smithsonian Institute). Минуя туристические автобусы и фотолюбителей, занимающих весь холм памятника Вашингтону, я смотрю на мерцающий в сумерках Мемориал. Проходя мимо высокой сосновой часовенки, стоящей на берегу озера, я снова вижу Мемориал, чье отражение в воде можно сравнить с гобеленом, вышитым золотом. Я вижу скачущих белок, в своих гнездах устраиваются птицы, сверчки и лягушки создают особую вечернюю симфонию. Я пересекаю поляну и направляюсь к Мемориалу. Там на вершине сидит «вечный» Линкольн, внимательно смотря вниз. Небо безоблачно и чисто, как никогда, кажется, что оно даже блестит. Время от времени на нем появляются то оранжевые полоски, то серо-синие. Странно, но глубокая печаль, с которой обычно Линкольн смотрел на меня, сегодня незаметна. Кажется, что он восхищен открывшимся перед ним видом. У меня тоже перехватывает дыхание от этой удивительной красоты, от того, как сияет поверхность озера этим вечером. Кажется, что жизнь балует нас, как детей природы, шалящих в спокойствии сумерек. Краем глаза я замечаю белую McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 150. 25
582 Глава 25. Эволюция разработки программного обеспечения цаплю. Испугавшись, она заметалась от одного края озера к другому, затем на мгновенье погрузилась в воду и, наконец, взвилась ввысь, широко размахивая крыльями. Пока я наблюдал за тем, как она пролетала над деревьями и Мемориалом, удаляясь все дальше, я понял, что эта птица — шедевр конструирования. Ее крылья легки и в то же время мощны; ее тело большое и в то же время невесомое. Как она может летать? И все же она летает — такая изящная и легкая. По-моему, цапля — это самое настоящее противоречие. Порой мне кажется, что процесс эволюции и процесс разработки программного обеспечения очень похожи. И тот и другой протекают постепенно, и в том и другом случае результат можно получить только через определенное время. Эти процессы небыстрые, иногда утомительные. На пути к основной цели достигаются промежуточные, и что-то постоянно совершенствуется. Это процесс, для которого потери являются обычным делом. На пути к результату отвергаются идеи, которые вначале казались вполне осуществимыми. Если программное обеспечение необходимо усовершенствовать, то программист должен приложить все свои усилия и использовать весь свой талант. В отличие от природы программное обеспечение не может совершенствоваться самостоятельно. Какова же мораль? Последовательно вносите изменения, работая над частями кода. Если вы изменяете код постепенно, он начинает развиваться самостоятельно. Через какое-то время код становится похожим на то, что хотели бы видеть ваши пользователи. У него появляется больше шансов выжить. Цикл разработки должен быть следующим: изменение-тест, изменение-тест, изменение-тест. Ваш код постоянно улучшается так же, как эволюционируют в природе животные и птицы. КАЙ-ЦЗЭН Кай-цзэн — это название философского течения в Японии, связанного с ведением деловых отношений, а также с совершенствованием личной и профессиональной жизни. Это ключевой принцип управления в японских компаниях и основополагающий элемент японской культуры. Существует мнение, что кай-цзэн — первопричина феноменального экономического роста Японии. В течение последних двадцати лет японцы проявили себя в сфере гарантии качества и производительности и значительно повлияли на состояние рынка западных стран. Эта философия действенна, и она непосредственно применима к производству высококачественного программного обеспечения. Преимущества небольших изменений Небольшое изменение кода имеет множество преимуществ. Наиболее важное из них — это возможность уменьшить риск предполагаемых дефектов, которые являются результатом человеческой ошибки. Чем меньше произведено изменений, тем меньше вероятность того, что вы внесете в свой код баш. Уменьшая вероятность того, что вносимые вами в программный код изменения приведут к ошибке, вы тем самым повышаете вероятность того, что проект в целом будет успешно завершен. Проект представляет собой не более, чем множество инициатив, связанных общей идеей. По словам Кента Бека, «снижение риска неудачи проекта путем со-
Преимущества небольших изменений 583 кращения производственного цикла (в том числе и путем сокращения числа изменений, сделанных в конкретном релизе) позволяет обеспечить успех проекта».1 Эти предостережения имеют значение независимо от того, очищаете вы существующий код или добавляете новые элементы. Последовательно добавляя новые элементы, вы гарантируете, что предыдущий работает перед добавлением следующего (потому что вы проверяете каждый из них перед использованием). И это также делает программное обеспечение более гибким. Если ваши пользователи или начальство решают, что элемент X более важен, чем элемент D, — эта перестановка приоритетов не должна пустить проект под откос, если вы все еще работаете над элементом С. В том беспокойном мире, в котором сейчас происходит разработка программного обеспечения, вы должны уметь быстро приспосабливаться к требованиям пользователей, и работа над небольшими частями кода поможет вам в этом. Подобно скульптуре, конечный облик создаваемого программного обеспечения не известен. Он проявляется, когда процесс близится к завершению. Разработка программного обеспечения состоит из ряда компромиссов между желаемым и возможным, между тем, что воспринято как желаемое, и тем, что воспринято, как возможное. Микеланджело любил говорить, что его скульптуры находились в камне до того, как он прикоснулся к ним. Его работа состояла в том, чтобы найти эти создания в бесформенных каменных глыбах и вдохнуть в них жизнь. Он часто тратил недели и даже месяцы на изучение камня, чтобы определить его возможности перед тем, как сделать первый удар долотом. Несмотря на творческий характер работы, Микеланджело все равно планировал каждый ее шаг. Он изучал и планировал каждое изменение. Мастер программного обеспечения делает то же самое: он извлекает из сырья, данного ему, скульптуры программного обеспечения, ожидающие своего открытия; скульптуры, полученные непосредственно из их исходных материалов, дефектов и всего прочего. «Дефекты» могут состоять из меняющихся пользовательских требований, непредсказуемых ресурсов, преждевременных предположений и заключений относительно того, что является выполненным, а что — нет. Работа мастера состоит в том, чтобы получить результат, который отвечал бы требованиям заказчика, несмотря на все непредсказуемые вещи, а может быть, и благодаря им. Программное обеспечение Другое преимущество внесения изменений небольшими этапами состоит в том, что форма программного обеспечения остается податливой, так как программное обеспечение должно быть гибким. Оно должно быть легко изменяемым. Улучшение или поиск ошибок в какой-то мере помогают сохранять систему в гранулированном виде. Это улучшает гибкость системы и делает ее более легкой в использовании с точки зрения устранения ошибок, добавления новой функциональности или тестирования существующей. Программное обеспечение должно быть гибким. Изменить сложное программное обеспечение так же непросто, как и изменить аппаратное обеспече- Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 4. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.)
584 Глава 25. Эволюция разработки программного обеспечения ние. Вся проблема состоит в том, что проблемы могут проявить себя в различных вариантах. Как отметил Стив Макконнелл в своей книге «After the Gold Rush», «утверждение, что легко изменить поведение сложного программного обеспечения, противоречит логике»1. Любое сложное создание (будь это аппаратное или программное обеспечение, сложная математическая формула или что-то другое) довольно трудно подвергнуть изменениям, потому что его, скорее всего, было сложно создать. Некоторые виды программных изменений жестче и разрушительнее, чем другие. Например, изменение требований в процессе разработки — одно из наиболее разрушительных. На самом деле, изменение требований в ходе работы может дестабилизировать проект до такой степени, что, возможно, он никогда не будет доведен до конца2. Самый частый ответ на жалобы по поводу поздних изменений требований — разработчики должны создавать программное обеспечение настолько гибкое, чтобы его можно было легко изменять в будущем. Легче сказать, чем сделать. Гибкое программное обеспечение стоит дороже, и для его создания требуется больше времени. Гибкость — понятие растяжимое. Высказывание о том, что какая-то часть программы должна быть гибкой, может означать все, что угодно. «Гибкой — до какой степени? Так, чтобы она могла работать на нескольких операционных системах или на нескольких различных компьютерах, поддерживать работу с несколькими валютами или использовать какой-то другой пользовательский интерфейс? Когда далеко — слишком далеко? Когда гибкость утрачивает свое значение? И когда программное обеспечение становится не таким гибким, каким оно должно быть по определению?» Единственный ответ на эти вопросы — опыт. Мастер, руководствуясь своим опытом, определяет, сколько Гибкости необходимо вложить в программу, — не больше и ни меньше. Энтропия программного обеспечения Другое преимущество постепенного создания или улучшения программного обеспечения состоит в противодействии программной энтропии. Как и физическая энтропия, программная энтропия (часто называемая программной гнилью) основывается на существующем беспорядке в системе. Но как же программная энтропия появляется в системе? Любимая лазейка энтропии — незначительные недостатки, на которые никогда не обращается внимание. Все начинается с «заплатки», которая вносится в код на некоторый произвольный срок. Затем, решая проблему в связанной задаче, вы окажетесь загнанным в угол из-за этой «заплатки» в более раннем коде. Затем вы обнаруживаете, что не можете создать тот новый модуль, который вам хочется, из-за «хака» который вы сделали вчера. Энтропия — инфекционная болезнь, особенно в технических дисциплинах типа разработки программного обеспечения. Знайте, что если у вас повсюду «заплатки» — рано или поздно наступит хаос. 1 McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 19. 2 Там же.
Рефакторинг 585 Работа с небольшими частями кода предотвращает появление «заплатки». Постепенно вы создаете новые элементы и работаете над ними до тех пор, пока они не будут закончены. Если вы обнаруживаете ошибку, то лучше остановитесь и зафиксируйте ее. И снова продолжайте работать над максимально маленькими частями кода. Весь процесс — цикл усовершенствования, и развитие кода происходит через незначительные изменения. В своей книге «Прагматичный программист» Эндрю Хант и Дэвид Томас описывают программную энтропию, проводя параллель со сломанным окном. В книге описывается различие между зданиями, которые регулярно подвергались вандализму, и теми, которые оставались более или менее неповрежденными, — в зависимости от того, как ремонтировали их владельцы. Те, кто оставил свою собственность на произвол судьбы, наблюдали значительный рост вандализма до тех пор, пока от их зданий ничего не осталось. Э. Хант и Д. Томас1 утверждают, что «любая оставленная недоработка в вашем приложении (например, недостаточно хорошее проектирование, плохой код и т. д.) может привести к общему беспорядку в проекте, который в конечном итоге нельзя будет исправить. Вы должны устранять недоработки по мере их обнаружения так же, как природа исправляет себя и свои творения через определенное время». Рефакторинг Ни одно обсуждение эволюционной разработки программного обеспечения не может быть закончено, по крайней мере, без упоминания о практикерефакторин- га, а именно об улучшении структуры существующего программного обеспечения без изменения его функциональных возможностей. Рефакторинг позволяет изменять внутреннюю структуру программного обеспечения, упрощая ее для понимания,'а также делая более легкой с точки зрения модификации, не изменяя внешнего поведения2. Термин рефакторинг впервые появился в неформальных кругах программистов на SmallTalk, но теперь встречается почти в каждой дисциплине и каждом языке программирования. Основателями рефакторинга считаются Уорд Каннингхем • (Ward Cunningham), Кент Бек (Kent Beck), Ральф Джонсон (Ralph Johnson), Билл Опдайк (Bill Opdyke), Мартин Фоулер (Martin Fowler) и другие. Безусловно, многие опытные разработчики использовали этот подход задолго до появления термина «рефакторинг». Это лучшая база для программиста. Вы можете использовать рефакторинг, чтобы избежать возможных трудностей в будущем, а также чтобы не наступать на одни и те же грабли дважды. Закаленные разработчики знали это до появления термина как такового. В оригинальной работе Мартина Фоулера на эту тему, «Refactoring: Improving the Design of Existing Code», умело формулируется и каталогизируется то, что стало отдельным разделом инженерии программного обеспечения. Рефакторинг — та- 1 Hunt, Andrew, and David Thomas. The Programmatic Programmer. Reading, MA: Addison-Wesley, 1999. С 4-5. 2 Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 53.
586 Глава 25. Эволюция разработки программного обеспечения кой же важный инструмент, как и все остальные в комплекте программиста. Понимание и изучение того, как можно умело осуществлять рефакторинг существующего кода, столь же необходимо, как и любой другой навык, который вы приобретете как разработчик. Постоянный рефакторинг — это ключ к тому, как держать код в удобочитаемом и легко модифицируемом виде. У опытных разработчиков появляется страсть к рефакторингу, что, в принципе, не так уж и плохо. Наведение порядка (особенно в чьих-нибудь программах и кодах) вряд ли кому-нибудь по душе, так что программисты, которые считают своим долгом заботиться о чистоте кодов, — действительно довольно редкое явление. Вы можете производить много различных изменений в своих программах, оставляя внешнее поведение нетронутым, но только те из них, которые улучшают и упрощают систему, можно отнести к типу рефакторинга. Это означает, что повышение производительности, часто цитируемое как тип рефакторинга, в действительности не является таковым, потому что изменяет внешнее поведение системы. Заметьте, рефакторинг — не слепая модификация кода, он не изменяет ради изменения. Рефакторинг должен быть систематичным, чтобы обеспечить последовательное развитие всего проекта. Иначе вы рискуете все испортить, производя непоследовательные и непродуманные изменения, которые ломают ваши программы вместо того, чтобы их чинить. Как уже было упомянуто ранее, цикл разработки должен быть следующим: изменение-тестирование, изменение-тестирование, изменение-тестирование. Два основных компонента рефакторинга — это небольшие изменения и всеобъемлющее тестирование1. Вы не можете начинать рефакторинг приложения без полного набора тестов. Делать это — значит накликать беду. Если вы не можете проверить, что ваши изменения (неважно, насколько они малы) ничего не «поломали», то как вы можете производить следующие изменения? Вы не можете этого. И именно поэтому вы должны создать всесторонние тесты перед переделыванием кода, который вы можете или не можете хорошо понять2. Тестирование — тема одной из глав этой книги, так что я не буду сейчас детализировать и лишь отмечу, что все тесты должны быть автоматизированы и должны проверять собственные результаты. То есть вы не должны проверять свою работу вручную; вы должны создавать тесты непосредственно в коде программы. Ваш код должен предоставлять способы тестирования, которые можно быстро вызвать для проверки его правильности. Когда вы подвергаете код рефакторингу, протестировать код должно быть так же просто, как нажать на кнопку. Тесты, которые трудно выполнять или которые недостаточно надежны, лучше вовсе не выполнять. Если тест трудно выполнить, разработчики, вероятно, пропустят его, чтобы сэкономить время. Тестам, не устойчивым к ошибкам, нельзя доверять, а их выполнение, в конечном счете, станет лишь пустой тратой времени. Единственный общепринятый тип рефакторинга — удаление дублирования из системы. Вся разработка программного обеспечения склоняется к этому: реализа- 1 Там же. С. 12-13. 2 Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 66. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.)
Как приучить начальство (и самого себя) проводить рефакторинг 587 ция каждой части логики, требуемой пользователем, только в одном месте. Хант и Томас1 называют это ИИ-принципом (Не Повторяться). Этот принцип широко применяется в сфере компьютерной инженерии. Довольно часто рефакторинг сводится к такому устранению дублирования из системы, чтобы каждый логический элемент был представлен в пределах системы один только раз, то есть чтобы он был построен по НП-принципу. Это мало чем отличается от нормализации баз данных. Считайте это нормализацией кода. Дублирование проникает в проект различными способами. Главный путь - нетерпение программиста. Разработчик смотрит на проблему, видит, что она похожа на ту, которую он решил вчера, берет код, который он написал вчера, вставляет его в новый метод и немного изменяет его. Сейчас этот способ сэкономит время, но в дальнейшем вы можете серьезно поплатиться. Иногда предполагаемые быстрые решения — вовсе не быстрые. Иногда они приводят к длительным задержкам. Не вкушайте червивое яблоко простого дублирования. И если вы натолкнетесь на него в своем коде, не задумываясь, примените рефакторинг. Как приучить начальство (и самого себя) проводить рефакторинг Одна из самых сложных вещей при рефакторинге — заставить людей понять его. Начальство косо смотрит на изменения в коде, который «работает* — независимо от того, насколько он ужасен. Существует такое понятие, как близорукость, свойственная всем, кроме закаленных кодеров. «Если оно не сломалось, его не за чем чинить», — это тот ответ, который вы чаще всего услышите на предложение усовершенствовать код. Другой вопрос: «Могу ли я продать это клиенту?» — риторический. Клиент даже не будет знать о рефакторинге, который вы со всем своим непреодолимым рвением хотите провести. Здесь упускается из виду один важный момент, а именно — код, который труден для понимания или не гибок, — неработоспособен и напрямую или косвенно все равно повлияет на работу клиента. Но как же так? Что случится, если клиент захочет внести небольшое изменение в программу, но сделать это будет очень сложно из-за плохого кода? А случится то, что часто является недопустимым для большинства пользователей: долгие задержки, релизы с ошибками, уменьшенные функциональные возможности и т. д. У плохого кода есть дурная привычка — проявляться в самые неподходящие моменты: во время демонстрации программы важному клиенту или в середине продолжительного выполнения. Все дело в том, что код, являющийся тяжелым для понимания, также является тяжелым для расширения и дальнейшей поддержки. Это очень просто. Он сломан, понимаете вы это или нет. Не так давно я беседовал с программистом из своей команды. Звонил клиент и жаловался на то, что часть одного из наших продуктов очень медленно работает. Andrew Hunt, and David Thomas. The Programmatic Programmer. Reading, MA: Addison-Wesley, 1999. С 27.
588 Глава 25. Эволюция разработки программного обеспечения Исследовав проблему, мы нашли некоторый SQL-код, который написал этот самый разработчик. Код работал и работал уже несколько лет, так что он отказывался изменять его. Когда стало очевидно, что его надо исправить, я согласился взять на себя решение этой проблемы, потому что я был самым опытным разработчиком на SQL. Обнаружив проблему с SQL довольно быстро, я написал запрос, который работал намного лучше, затем изменил код приложения, который его использовал. Последствия этого были хорошие и не очень. Хорошие заключались в том, что запрос стал работать в десять раз быстрее. Плохие — в том, что изменение повлекло за собой нарушение трех других частей несвязанного кода. И оказалось, что эти три части были недокументированными, в них имелись нелогичные связи между методами классов. Три других метода были созданы так, что они очень хитро зависели от SQL в первоначальном методе. Даже такая незначительная вещь, как добавление символа «новая строка» в оригинальный SQL- код, привела бы к их неработоспособности. Из-за переделки SQL они все перестали работать. И мне стало ясно, что эти методы нуждаются в рефакторинге, так как они очень плохо написаны. Однако мой оппонент не был так в этом уверен. Он начал с ха- кинга (я имею в виду неформальное значение этого слова), решения, которое бы позволило зависимым методам думать, что SQL-код не изменился. В ответ на мои слова о том, что на самом деле ему требуется провести рефакторинг методов с целью удаления нелогичных связей, он авторитетно заявил: «Оно компилируется и делает то, что должно». И он был прав: все скомпилировалось — ведь компиляторы не знают и не заботятся о том, что компилируют уродливый код. Я ему объяснил, что в изменении системы обычно участвуют люди, а они заботятся о коде. Создавать код, который может «понимать» компьютер, не так уж и трудно, поскольку компьютер никогда не поймет его так, как человек. Он безропотно компилирует его и выполняет. Это машина, ей нет дела до того, что она на самом деле делает. Сознательные существа, подобные нам, нуждаются в большем. Мы нуждаемся в чем-то последовательном и логическом. Мы нуждаемся в том, что имеет смысл. Как заметил Мартин Фоу- лер', «любой идиот может написать код, который прочитает компьютер, но только хороший программист сможет написать такой код, который смогут прочитать люди». После нескольких затяжных «обсуждений» я наконец убедил моего программиста провести рефакторинг кода. Когда он закончил, он сказал мне, что ему было приятно это делать. Я не знаю, понял он это или нет, но как разработчик он сильно вырос. Обратите внимание на то, что рефакторинг не должен быть чем-то таким, к чему вы прикладываете усилия. Большинство процессов рефакторинга протекают очень быстро. Как я уже говорил, типичный рефакторинг — небольшое изменение, сопровождаемое автоматизированным тестом. Так как изменения маленькие, их будет просто произвести, а возможные ошибки — просто исправить. Рефакторинг — нечто такое, что вы можете и должны делать регулярно во 1 Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 15.
Как приучить начальство (и самого себя) проводить рефакторинг 589 время разработки. Кент Бек сравнивает это постоянное чередование двух задач со «сменой шляп»1. Программируя, вы часто будете проводить рефакторинг, так что сравнение верное. Даже если рефакторинг потребует временной приостановки разработки, время, которое вы потратите на него, вернется сторицей. Даже если выпуск уже не за горами, вы не должны откладывать рефакторинг из-за недостатка времени2. Напротив, шансы, что вы сорвете сроки из-за рефакторинга, очень низки. Плохо спроектированный код обычно занимает больше места (и поэтому больше времени). Избавление от него поможет уложиться в сроки. Другими словами, вы пробежите марафон намного лучше, если не будете торопиться, чтобы прийти в форму перед гонкой. Имея дело с массивным раздутым кодом, который уже потерял свою форму, тратить время на рефакторинг просто необходимо. Преимущество рефакторинга для руководства или для других разработчиков состоит в том, что он экономит много времени. Он также поможет вам сохранить ясную голову и воодушевить остальных веселее работать над кодом. Ведь мало кто из нас любит работать над плохим кодом (даже над своим собственным). Если вам трудно понять, что изменять или где изменять, или если при внесении изменений ломается несвязанный код, появляется шанс, что кто- нибудь в вашей команде сделает ошибку и внесет ее в приложение. Единственный способ уменьшить риск появления подобных ошибок заключается в постоянном и последовательном рефакторинге кода. Надо надеяться, что в этом случае вы получите кредит доверия у своих начальников и автономность для осуществления тех действий, которые, по вашему мнению, необходимы для решения поставленных перед вами задач. Это просто вопрос делегирования полномочий и совместной ответственности в том виде, в котором он должен решаться. Рефакторинг — это еще и инструмент обучения, и его применение непременно скажется на ваших собственных методах кодирования. Вы будете учиться на собственных ошибках и ошибках других разработчиков медленно, но верно приближаясь к созданию идеального кода, который не потребует немедленного рефакторинга. В качестве «побочного эффекта» приобретения новых знаний о наведении порядка в коде и пересмотра плохого дизайна у вас появится интуиция для объединения элементов программного обеспечения. Рефакторинг никогда не устареет, но, применяя его, вы определенно вырастете профессионально. И конечно, начальство и ваши коллеги оценят это. Вы также можете попробовать рассказать страшные истории о проектах, которые провалились, так как были слишком сложны для расширения, поддержки или которые не могли предоставить приемлемую производительность. Есть множество примеров таких историй (а может быть, и у вас есть подобный негативный опыт). Немного паники и страха не помешает. И даже если после этого вам не удастся повлиять на начальство, которое считает, что рефакторинг принесет только неприятности, все равно проводите его. Возьмите Мартина Фоулера в советчики и ничего не говорите шефу! 1 Там же. С. 55. 2 Там же. С. 66.
590 Глава 25. Эволюция разработки программного обеспечения Когда рефакторинг не нужен Есть ситуации, в которых рефакторинг не имеет никакого смысла, например, в тех случаях, когда он отрицательно сказывается на производительности системы. В отличие от предлагаемого Фоулером скрупулезного обращения с предметом рефакторинга, я не верю в то, что хорошей идеей является снижение в результате рефакторинга производительности программного кода из-за увеличения числа мест, где он используется. В своей книге «Рефакторинг» Фоулер1 приводит пример, в котором временная переменная заменяется с помощью повторяющихся вызовов функции вместо того, чтобы хранить ее результат. Итак, вместо одного вызова функции, чей результат сохраняется в переменной и которая затем используется в методе, мы получаем несколько вызовов. Каждый раз, когда необходим результат функции, она вызывается, и это несмотря на тот факт, что функция является детерминистической по своей природе, ее результат никогда не изменяется между вызовами. Фоулер2 называет этот тип рефакторинга «заменой временной переменной запросом». Это плохая техника по нескольким причинам. Во-первых, ее применение приводит к возникновению проблем с производительностью, которые, судя по всему, необходимо будет решать позже. Хотя настройка производительности — это не рефакторинг, данный подход перечеркивает все достижения эволюционной разработки кода (рефакторинг в которой — только часть), суть которой — постепенное улучшение. Изменение не приводит к улучшению, если в результате страдает производительность. То, что код стало легче читать и поддерживать, еще ничего не значит. В примере Фоулера, если для выполнения вызываемой функции требуется значительное время, вам придется провести дефакторинг (то есть вернуть все назад), чтобы добиться приемлемой производительности. Во-вторых, даже если коду для выполнения не требуется много времени или если он не вызывается часто, все равно это может негативно отразиться на производительности. Большие проблемы с производительностью часто возникают в результате накопления маленьких. Это значит, что даже если каждый вызов в цепочке вызовов не требует недопустимо много времени для выполнения, их цепочка выполняется слишком долго. Можно сказать: «Мал клоп, да вонюч». Такое кодирование заставит вас не один час чесать затылок в попытках понять, где вы допустили ошибку и что теперь с этим делать. Ничто так не бросается в глаза, как очевидные проблемы с производительностью. Настоящее преступление — создавать неэффективный стиль кодирования и обвинять всех и вся. Например, поговорим об обновлении экрана. Часто в том, что экран медленно обновляется, виновата не какая-то одна строчка кода. Проблема обычно заключается в том, что множество отдельных выполняемых строк кода не были написаны эффективно. Чтобы исправить их, вам придется многое переписать. Намного лучше писать код, думая об эффективности, чем потом работать и перерабатывать код только для того, чтобы добиться приемлемой производительности. Фо- 1 Там же. С. 27. 2 Там же. С. 120.
Базы данных 591 улер1 утверждает, что, «так как временные переменные полезны только внутри содержащей их процедуры, их использование приводит к чрезмерно длинным и сложным процедурам». Это интересное мнение, но не догмат. Мой ответ прост — во-первых, избегайте написания длинных процедур и, во-вторых, разбивайте чрезмерно длинные процедуры, когда находите их. А использовать или не использовать временные переменные — это уже другой вопрос. Фоулер2 также утверждает, что вы не сможете узнать, как скажется метод замены временной переменной запросом на производительности, пока вы не проверите свой код с помощью профайлера. Я с этим не соглашусь. Если у вас есть некоторые соображения по поводу того, как долго та или иная функция будет выполняться, вы можете предположить, как повлияют на производительность лишние вызовы той или иной функции. Может быть, ваша оценка и не будет точной, но вы можете эмпирически высчитать, как скажется на производительности такая неэффективность, и запускать профайлер для этого не потребуется. И все-таки я признаю, что для некоторых вычислений лучше создать функции-методы, чем сохранить результаты вычислений в переменной, особенно если эти результаты необходимы другим методам. Добавление этих функций в интерфейс класса, чтобы сделать их доступными другим методам, — банальное решение проблемы, но оно может оказаться весьма полезно. Это говорит о том, что я и в дальнейшем буду умерять свой энтузиазм хорошей дозой прагматизма. Если рефакторинг — ваша ежедневная задача, то не имеет смысла выносить в интерфейс неэффективный метод, который, возможно, будет использован в будущем3. Вы всегда можете вернуться назад и выделить метод, если в нем действительно появится необходимость. Не проводите рефакторинг, если он отрицательно влияет на производительность приложения и приносит мало плодов в плане упрощения кода. Базы данных Другая область, в которой вы должны тщательно подумать перед рефакторин- гом, — эксплуатируемые базы данных. Когда вы проводите рефакторинг базы данных, вы обременены сведениями старой и новой структуры базы данных. Это может доставить много неприятностей, особенно если база данных уже используется. Однако рефакторинг иногда бывает просто необходим. Все зависит от ситуации. Если вы собираетесь проводить рефакторинг базы данных, делайте это в процессе разработки — и чем раньше, тем лучше. Как это происходит и с любым другим типом программного обеспечения, стоимость изменения базы данных, с которой уже работают приложения, экспоненциально увеличивается во времени, несмотря на все старания Экстремального программирования (ХР). 1 Там же. С. 26. 2 Там же. С. 32. 3 Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 57. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.)
592 Глава 25. Эволюция разработки программного обеспечения ПЕРЕИМЕНОВЫВАТЬ ИЛИ НЕТ? Стоит ли переименовать? Кто-то может сказать, что не стоит. Имена переменных, функций, методов и классов, однажды присвоенные им автором кода, сами по себе могут ничего не значить. В ыконце концов, это все только символьные названия, правильно? Переименование элементов кода, чтобы они стали более осмысленными так, чтобы это приводило к размышлениям, является простым случаем навязчивого поведения программиста. Я не согласен с этой точкой зрения. Во- первых, код должен быть самодокументируемым. По моему опыту, обилие комментариев обычно является следствием плохого кодирования или плохих соглашений об именовании. Чаще всего предназначение этих комментариев — скрыть или оправдать плохой код. Я предпочитаю осмысленные имена и минимальное использование комментариев. Например, сегодня я могу помнить, что Foo — переменная моего цикла, а завтра забыть об этом. Намного лучше адаптировать соглашения, и тогда я смогу не полагаться на свою хрупкую память и не бояться испортить код или потратить впустую время в поисках, что же этот элемент с непонятным названием может делать. Если картина стоит больше тысячи слов, то хорошее имя, по крайней мере, — больше ста. Во-вторых, улучшение доходчивости кода всегда хорошо, за исключением тех случаев, когда это стоит нам производительности. А переименование элементов кода, чтобы их названия стали более осмысленными, ясность в код только добавляет. Это полезно не только для самого разработчика, но и для тех, кто в дальнейшем будет работать над кодом. Переименование для улучшения доходчивости — это такой же хороший инструмент рефакторинга, как и Extract Method и Move Method1, его просто использовать с помощью команд глобального поиска/замены текстовых редакторов. Однажды я услышал историю старого программиста о своем товарище, который пытался повысить безопасность своего кода, называя переменные в длинных программах на COBOL, используя имена из Библии. (Программист говорил: «И вы считаете, что Foo — плохое название переменной цикла? Как вам «Захария»?!») Может быть, это и защищало его код некоторое время, но бьюсь об заклад, что это не увеличило любви к нему со стороны коллег, и то, что пришел тот день, когда ему на самом деле понадобилась помощь Господа Бога, чтобы самому запомнить все эти названия. Хорошие имена помогают всем: и вам, и тем, кто в дальнейшем будет работать над вашим кодом. Рефакторинг или проектирование? Другое сомнительное применение рефакторинга — его альтернатива проектированию. Есть мнение, что проектирование может быть сведено к минимуму, потому что вы всегда можете изменить архитектуру с помощью рефакторинга2. Сторонники ХР часто изображаются предпочитающими рефакторинг первичному проектированию. Этот подход чреват ловушками и нерешенными проблемами. Во-первых, что получится, если кодеры окажутся не очень хорошими проектировщиками? Сможет ли рефакторинг спасти их? Я в этом сильно сомневаюсь. Когда те, кто использует ХР, берутся за проектирование сложной системы и оказываются экстремально плохими проектировщиками, слово экстремальный неожиданно приобретает новое значение. Во-вторых, как можно заставить эффективно работать команду программистов, не имея проекта, по которому они должны работать? Откуда мы узнаем, что команде, работающей над базой данных, необходим OLEDB-провай- дер, который может возвращать данные о рынке в GUI, если эта часть приложения 1 Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 15. 2 Там же. С. 67.
Избавление от кода 593 не была даже задумана — не то, что спроектирована? Должны ли мы делать это по ходу дела? В-третьих, и, наверное, это самое важное: если рефакторинг используется вместо первоначального проектирования, это вовсе и не рефакторинг, потому что он изменяет видимое поведение приложения. Рефакторинг, используемый таким образом, превращается из инструмента для улучшения внутренней архитектуры системы в инструмент для улучшения внешней. Он используется для добавления новой функциональности в приложение. В этом смысле рефакторинг — просто скрытое оправдание «заплаток», кодирования без стратегии в надежде, что кто- нибудь другой приведет проект к финишу, поскольку сам программист не знает, как до него добраться и где этот финиш вообще. Избавление от кода Естественно, бывают ситуации, когда нет смысла дальше развивать код, когда следует от него отказаться и начать все сначала. Обычно мы называем это избавлением. Если вы программируете давно, я уверен, вы видели код, которому уготована участь динозавров. Однажды мне не посчастливилось работать над пакетом финансового программного обеспечения для одной из компаний Wall Street. GUI-часть этого кода была настолько плохой, что я подумывал: «А не сбежать ли мне?» — когда шел в офис. Разработчики этого кода считали, что лучше пустить пыль в глаза нетехническому персоналу компании (которые ничего лучше и не видели), чем вырасти профессионально и научиться правильно использовать инструменты. Они могли постараться и построить свою систему так, чтобы будущая работа других программистов (и их самих в том числе) не была похожа на хождение по минному полю. От этого кода следовало избавиться. В таких ситуациях лучше всего сделать все заново. Если программа просто не работает — значит, пришло время отказаться от проекта и начать все сначала. Если в ней полно «багов», вряд ли получится постепенно изменить ее. Нельзя строить дом на сгнившем фундаменте или надстраивать здание, изъеденное термитами. Наиболее мудрое решение — снести его и строить заново. Помните, что, играя в рефакторинг, вы должны сохранять функциональность системы, улучшая ее внутренне. Сохранять функциональность приложения, если оно не работает, — жестоко. Лучше начать с начала. Некоторые программы настолько плохо сконструированы, что другого пути просто нет. Далее когда вы решились на переделку, работайте над небольшими частями в одно и то же время, постепенно развивая ваше создание, пока оно не будет обладать всей необходимой функциональностью. Жесткая инкапсуляция и модульность позволит вам использовать рефакторинг отдельных модулей вместо переписывания1. Если вы действительно решились переписать код, не забывайте правило: изменение-тест, изменение-тест, изменение-тест. В конце концов либо вы и ваши пользователи начнут понимать, что это усилие приводит к тому, что требуется, либо вы от этого откажетесь и начнете все сначала. Конечно, вы не хотите преж- 1 Там же. С. 66. 20 Зак. 983
594 Глава 25. Эволюция разработки программного обеспечения девременно отказаться от попыток небольших изменений. Ведь вы пока не понимаете, к чему приведет отказ от кода, потому что еще не сделали ни одной попытки понять это. Много времени и сил тратится на то, чтобы отказаться от кода. Руководство имеет привычку хмуриться, когда слышит, что надо выбросить работу, за которую оно уже заплатило. Умение понять, когда время собирать камни и когда их разбрасывать, приходит с годами. Ни одна книга, включая эту, не расскажет вам, когда втыкать штепсель, а когда вытаскивать. Упражнение по этой теме я оставляю читателям. Экстремальное программирование Как обсуждение рефакторинга, так и обсуждение разработки программного обеспечения небольшими частями не будет полным без рассказа об экстремальном программировании (ХР) — молодой методологии, поддерживаемой Кентом Беком и другими. Поскольку короткие циклы выпуска и итеративная разработка — основные принципы этой методологии, естественно, что мы должны немного «покопаться» в ней. ХР представляет собой сравнительно новый подход к разработке программного обеспечения. Его создатели пытаются формализовать в виде методологии способы, которыми многие разработчики пользуются уже давно. В этой методологии акцент ставится на гибкости процесса разработки, а не на анализ и проектирование. Она описывается как суть поведения программистов «в дикой природе», и я подозреваю, что так оно и есть. Большинство программистов, вероятно, не придерживаются твердых методологий проектирования или анализа. Особенно это относится к хакерам — разработчикам без формального обучения или желания использовать свои знания. С этой точки зрения, ХР — хакерская методология. Она описывает то, как хакеры работали много лет. Согласно методологии ХР, цена, время, качество и функциональность — четыре основные переменные в проектах по разработке программ. И если любые три из них изменяются в течение проекта, можно изменить четвертую, чтобы скомпенсировать эти изменения. Переменная функциональность рассматривается как наиболее полезная из этих четырех переменных. Если цена, время или качество изменяются, возможности могут быть изменены, чтобы компенсировать их. Например, если срок окончания проекта сокращается с двенадцати месяцев до шести, функциональности могут быть соответственно сужены. Пока хотя бы одна из четырех переменных остается под контролем программиста, проект продолжает существование. Если все/четыре переменные контролируются кем-то вне команды разработчиков, все обычно заканчивается тем, что сроки не выдерживаются и программное обеспечение получается низкого качества1. В такой ситуации шансы на успех проекта экстремально низкие. Это означает, что простой связи между этими четырьмя переменными нет. Например, увеличение ресурсов (стоимости) проекта необязательно означает, что вы должны увеличить возможности. Некоторые вещи имеют последовательные, зависящие от времени Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 15. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.)
Экстремальное программирование 595 или, иначе говоря, сложные взаимосвязи; которые не подчиняются простому управлению ресурсами. Как гласит народная мудрость: «Девять женщин не смогут родить ребенка за один месяц». Качество — плохо контролируемая переменная, потому что выгода, полученная за счет качества, в лучшем случае будет просто мала. Уменьшение времени, необходимого для производства программного обеспечения, за счет качества — сделка Фауста, которая обрекла на гибель не одну софтверную компанию. Наивно думать, что принесение в жертву внутреннего качества программы (определенного программистами) не скажется на ее внешнем качестве. В конечном счете, это вас погубит. Или поддержка программы будет слишком дорогой, или она никогда не сможет достичь конкурентоспособного уровня внешнего качества. Не делайте этого, если вы хотите спокойно спать и не беспокоиться о месте своей работы1. Функциональность — «безопасный клапан» в ХР по двум причинам. Первая — у вас появляется обратная связь, возможность попрактиковаться и предположить, что реализованные возможности, в общем, будут точными. Вторая — задачи выполняются в зависимости от их значимости, с точки зрения важности для пользователя, и, если придется отказаться от задачи вследствие изменения возможностей, это будет менее значимая задача, чем те, которые остались. ХР полагает, что лучше сделать простую вещь сегодня и немного заплатить завтра, чтобы изменить ее, чем немного заплатить за функциональность, которая завтра может быть и не использована. Если такая функциональность в дальнейшем понадобится, она может быть получена с помощью рефакторинга. Тестирование — один из основных компонентов ХР. В действительности тесты определяют законченность проекта: если проект не проходит тесты, он еще не закончен. Если проходит — то завершен. Создают программу итеративно, небольшими частями. Каждая новая функциональность тестируется с помощью автоматизированных тестов перед тем, как добавляется следующая. Согласно ХР, функциональность, для которой не существует тестов, сама не существует. Цикл разработки по ХР: изменение-тестирование, изменение-тестирование, изменение-тестирование. Все программирование осуществляется двумя разработчиками за одним компьютером. Это концепция ХР называется парное программирование. Разработчики просматривают коды друг друга, вместе решают поставленные перед ними задачи, то есть функционируют как один в процессе разработки. Вы можете участвовать в одной паре утром и в другой — вечером. Если вы сумеете найти общий язык со своим партнером, то вы можете быть более продуктивным в паре, чем индивидуально. ХР использует понятие постоянной интеграции. Каждая новая функциональность интегрируется в основную сборку приложения. Это помогает предотвратить сбои других частей системы в будущем и позволяет на ранней стадии обнаруживать ошибки интеграции. Команды, работающие по ХР, имеют тенденцию становиться «интеллектуальными кочевниками»2. Они готовы собрать пожитки и следовать за караваном по 1 Там же. С. 18. 2 Там же. С. 42..
596 Глава 25. Эволюция разработки программного обеспечения первому зову. Принцип, гласящий, что архитектура программы должна быть более-менее зафиксирована после того, как начинается этап конструирования, оказывается выброшенным в окно. «Экстремалы» адаптируются ко всем желаниям заказчика. Я думаю этим все сказано об ХР и об основных ее положениях. Что лично я думаю об ХР? Как и в большинстве методологий, в ней есть и хорошие, и плохие стороны. Очевидно, она эффективна для Кента Бека и других. Однако я не думаю, что она будет эффективна для большинства разработчиков и что все ее принципы хороши. Позвольте мне объяснить. Когда я спрашиваю себя о том, что неправильно в ХР, первая вещь, которая приходит мне на ум, — парное программирование. Я нахожу само это понятие немного странным. Когда я пишу код, мне надо сконцентрироваться. Если кто-то сидит рядом со мной, это мешает мне сосредоточиться, и я думаю, большинство со мной согласится. Кодирование требует концентрации внимания и глубокого размышления. Вы не сможете этого добиться, если в восемнадцати дюймах от вас кто- то колотит по клавиатуре или постоянно говорит. Прерываясь и отвлекаясь, вы будете проектировать и кодировать плохо, да к тому же еще совершать ошибки. Мне кажется, что работодателям не захочется платить двум высококвалифицированным инженерам, когда они делают работу одного (обычно работодателям нравится делать наоборот). Сторонники лагеря ХР могут возразить, что два программиста по меньшей мере в два раза продуктивнее, чем один, и поэтому различие стирается, но я в этом сомневаюсь. Как большинству программистов, мне удобнее размышлять, набирая на клавиатуре. Мне необходимо визуализировать свои предположения, чтобы глубже погрузиться в них. Работать одному на своем компьютере — лучший способ это сделать. Я не стану более плодовитым программистом, если кто-то будет работать прямо у меня за спиной, а руководство будет получать обоснованные жалобы по поводу того, что я не выполнил из-за этого свои обязательства. Признаюсь, я никогда не применял этот принцип ХР на практике, чтобы определить, подходит ли он как техника разработки, — поэтому я могу быть и не прав. Более того, я признаю, что решил не одну сложную задачу, сидя вместе с коллегой за компьютером и работая над очевидными и не очень очевидными элементами этой задачи. Я знаю, что сотрудничество такого вида может быть эффективно. Но мне трудно понять, как можно работать в паре изо дня в день и отказаться от способа работы один на один с компьютером (что было нормой с момента их появления). Другой основной недостаток, который я вижу в ХР, — отсутствие проектирования. На мой взгляд, это ошибка. Мы должны заранее знать все свои возможности. Конечно, есть вещи, которые мы можем не знать, и попытки узнать о них заранее могут быть пустой тратой времени, но приступать к этапу кодирования, не зная, что надо делать, — гарантия провала. Это все равно, что отправляться в путешествие через всю страну без карты: вы можете придумать цель в пути и, может быть, достигнете ее, но вы точно потеряете много времени, а людям будет очень сложно следовать за вами или ждать вашего прибытия. Если в приложении должна быть какая-то функциональность, я, как руководитель проекта, должен узнать об этом как можно раньше, чтобы дать кому-нибудь задание реализовать ее, прежде чем другие части приложения, которые используют эту функциональность, будут завершены. Если мы создаем новый сервис для пре-
Экстремальное программирование 597 образования данных, который использует XML для передачи данных, я должен об этом знать заранее, чтобы дать задание разработать таблицы стилей XML до того, как они понадобятся той части приложения, которая их использует. Планирование работы над элементами, особенно теми, которые зависят от других элементов, требует тщательного проектирования. Если вы не распределите ресурсы в соответствии со своими потребностями, вы сможете не достичь цели. Все просто. С незапамятных времен существует причина, по которой необходимо управлять проектами, планируя их с самого начала. Причина в стратегии. Отказ от проверенных методов в пользу основанных на интуиции — просто сумасшествие, и мне потребуется много времени, чтобы поверить, что это будет работать. Но самая большая проблема с пренебрежением проектированием в ХР состоит не в путешествии без карты и даже не в том, что для тщательного планирования необходимо детальное проектирование. Самая большая проблема заключается в том, что ХР поощряет программистов полностью отказаться от проектирования приложений. Я не думаю, что избыточное проектирование или планирование — основная проблема для большинства программистов. Возможно, это было проблемой в изолированных проектах или в проектах, которые в качестве примера могут привести сторонники ХР, но это не общая проблема. Проблема заключается в том, что большинство программистов не уделяет достаточно внимания анализу, проектированию и планированию. В книге «After the Gold Rush» Стив Макконнелл1 говорит, что 75 % нынешних разработчиков программного обеспечения либо вообще не занимаются проектированием, либо занимаются им очень мало, перед тем как начать кодировать. Поощряя беспроектное программирование, ХР побуждает их продолжать применять свои греховные методы. Это приводит к плохо продуманному программному обеспечению и к проваленным проектам. В этом мире оценки ничего не значат, и проектирование — запоздалая мысль, а не основополагающий принцип, который держит программное обеспечение на плаву, или фундамент, на котором строится система. Сначала кодируй, потом осмысли В своей книге «After the Gold Rush» Макконнелл2 называет такой подход к созданию программного обеспечения, как закодируй-и-исправъ. Мне больше нравится название сначала кодируй, потом осмысли. Код создается быстро, но из-за его низкого качества и плохо продуманной стратегии его требуется постоянно исправлять. Часто, особенно в конце проекта, больше времени уходит на исправление кода, чем на его написание. Дело усугубляется, если ошибки укоренившиеся: количество требуемых исправлений увеличивается подобно снежному кому, особенно когда приближается время поставки программного продукта. Если мы напишем код, не проводя глубокого анализа решаемых задач, у нас возникнет множество проблем, потому что мы закрываем глаза на неоспоримый факт: создание качественного программного обеспечения — сложная задача, и, как и другие сложные задачи, оно требует тщательного анализа и планирования, если, конечно, мы хотим добиться успеха. 1 McConnell, Steve. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 69. 2 Там же. С. И.
598 Глава 25. Эволюция разработки программного обеспечения Вы должны помнить, что если проект развивается, это еще не значит, что этого достаточно. Если вы тратите первые десять дней трехмесячного проекта сразу на конструирование, вы только на пять дней приближаетесь к завершению, то есть на самом деле тратите время впустую. Теперь вам надо ускорить темп, чтобы успеть. Конструирование приложения — чаще всего самая простая и быстрая часть. Тщательный анализ и проектирование системы требуют больше умственных усилий и значительного времени. Если вы ждете до последней минуты, чтобы проанализировать проблему и спроектировать систему для ее решения, вы, скорее всего, обнаружите себя по колено в болоте, кишащем дефектами размером с аллигатора, совсем не приблизившись к конечному продукту. Как говорит Макконнелл: «Быстрый старт не гарантирует быстрое достижение финишной черты»1. И проблемы с недоанализированными и недопроектированными системами не относятся к простым дефектам программного обеспечения. Иногда код не содержит ошибок, он просто не соответствует нуждам клиента. Другими словами, и здание квадратное, и крыша не течет, но это неправильное здание. Этот вид оплошности — обычно результат плохого планирования, недостаточного проектирования или недостаточного вовлечения пользователя в проект. Первые два из них — признаки работы по методу «сначала кодируй, потом думай» — практика, неэффективность которой была известна уже больше, чем 20 лет назад. Сторонники этого подхода готовы романтизировать прихоть быстрого ныряния в фазу кодирования, как парашютисты, прыгающие на цель по команде. Разработчики, предпочитающие планирование нападения, клеймятся как бездельничающие технократы. Есть некоторая доля самодовольства в желании быть первым и передовым рыцарем кода, солдатом в великом крестовом походе за освобождение затуманенных умов инженеров по всему миру. Но эта идея ошибочна. Отсутствие фаз анализа и проектирования при разработке программного обеспечения неизбежно ведет к замусориванию кода: к бесконечным циклам отладки, исправления и тестирования. В действительности, немного проектов оправляются от этого, даже те, которые в итоге поставляются. Кодирование — это забава, но, если вы применяете метод «сначала кодируй, потом думай», вам приходится больше времени тратить на исправление ошибок, сделанных на ранних стадиях кодирования, чем на написание кода. Поверьте, не этим должен заниматься рыцарь кода. Это порочный круг, загубивший много проектов. Самое смешное в этом стиле разработки — это то, что те, кто его используют, обычно тратят столько же времени на планирование и проектирование, как и те, кто использует более системный подход к созданию программного обеспечения2. Они просто делают это непоследовательно и обычно тогда, когда от этого мало пользы. Цена изменения программного обеспечения увеличивается экспоненциально со временем. Таким образом, анализ и проектирование в начале — то, что избавит вас от ошибок, потому что вы думаете о задаче, и позволит получить дивиденды в дальнейшем (что иначе было бы невозможно). Макконнелл сравнивает подход закодируй-и-исправъ с поговоркой «не все то золото, что блестит». Да, этот подход притягателен для небольших организаций, потому что прогресс проекта можно увидеть сразу и потому что этот подход требует меньших знаний. Пробле- 1 Там же. С. 11. 2 Там же. С. 13.
Заключение 599 мы при его использовании сразу не очевидны и постоянно упускаются организациями, страдающими от них. Любая старая «заплатка» может поломать код, который будет компилироваться и вроде бы правильно работать. Требуется инженерный ум, чтобы тщательно проанализировать, спланировать и осмыслить нечто столь же запутанное, как программная система. За и против Необходимо отметить, что в ХР есть несколько положительных моментов. Лично мне нравится концепция итеративной разработки и коротких циклов выпуска. Внесение небольших изменений, как я уже говорил, — один из ключевых моментов, гарантирующих успех проекта. По моему мнению, четыре составляющие ХР (храбрость, коммуникации, простота и обратная связь) хороши сами по себе, независимо от ХР. Конечно, нужно быть очень смелым, чтобы в наши дни разрабатывать программное обеспечение. Но, как точно подмечает Бек1, быть смелым, не рассуждая здраво, — это просто безрассудство. Рефакторинг применяется в любой серьезной методологии разработки программного обеспечения, и очень хорошо, что ХР делает на нем акцент. Тестирование — также важная составляющая. То, что ХР делает большой упор на тестирование, — дань большому опыту сторонников этой методологии. Опытные разработчики знают, насколько большое значение имеет хорошее тестирование. Опытные разработчики каждый раз становятся увереннее в себе, когда они нажимают кнопку тестирования. Может показаться, что это из-за отсутствия интуиции. Возможно, вы считаете, что опытные разработчики настолько хороши, что им незачем тестировать свой код. Однако это неверно. Чем опытнее вы становитесь, тем больше понимаете, как мало вы знаете, как ненадежна может быть ваша память и как хороши инструменты, с помощью которых можно тестировать свою работу. Подводя итог, можно сказать, что в ХР есть и плюсы и минусы. На мой взгляд, некоторые подходы этой методологии непрактичны, поэтому я не думаю, что в ближайшее время она заменит привычные нам способы создания программного обеспечения. Мне кажется, что еще не скоро наступит такой день, когда работа двух программистов на одном компьютере станет обычным явлением. Более того, я думаю, что мало кто из разработчиков обойдется без анализа и проектирования. Да, я могу предположить, что опытные разработчики могут иногда без этого обходиться, но разработчикам средней руки необходимо планирование и проектирование, а ХР их не признает. Заключение Поэтапная работа над кодом менее рискованна, чем другие методы создания программного обеспечения. Снижение риска — ключ к достижению успеха и выживанию программиста, потому что он просто не знает, что может случиться с его программой в будущем. Постепенное развитие кода — самый безошибочный и менее Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 34. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.)
600 Глава 25. Эволюция разработки программного обеспечения рискованный путь достижения целей вашего проекта. Делать все небольшими частями — неважно, пишете ли вы новый код или совершенствуете существующий, — самый лучший способ избежать ошибок. Эпилог Возвращаясь назад от Мемориала, я решил пройтись через парк. Уже спустилась ночь, и меня охватила какая-то грусть по белой птице. Незаметно для себя я нарушил границы и вошел в заповедную зону, и радостный вид предстал перед моими глазами: белая цапля — моя белая цапля — на фоне исчезающего в темноте Мемориала ринулась в мою сторону. Она то поднималась в небо, то опускалась вниз; легкие изящные крылья несли ее к желаемой цели. Я наблюдал за тем, как она танцевала в лучах лунного света, а то и вовсе сливалась с водами реки Потомак. Когда она скрылась, я понял, что она делала то, что делала всегда, — задолго до Линкольна, Вашингтона и рождения человечества вообще. Следя за таким непринужденным и свободным полетом, мне на ум пришли слова французского писателя и летчика Антуана де Сент-Экзюпери: «Дизайнер знает, что он достиг совершенства не тогда, когда ему больше нечего добавить, а тогда, когда ему больше нечего убрать»1. А цапля и ее изящество были совершенным подарком природы миру. 1 De Saint-Exupery, Antoine. Wind, Sand and Stars. Harvest Books; London, 1967. С 21.
<л г Всеобъемлющее ЛХУ тестирование А всякий, кто слушает сии слова Мои и не исполняет их, уподобится человеку безрассудному, который построил дом свой на песке; и пошел дождь, и разлились реки, и подули ветры, и налегли на дом тот; и он упал, и было падение сто великое. Мф. 7:26-27 Наступает вечер, только что перестал идти дождь. Посреди коричневого поля в Канзасе на зеленом клочке земли стоит уютный фермерский дом. С его белой крыши стекают последние капли прошедшего днем ливня. Дуга радуги, раскинувшись под шелковистым куполом неба, сливается с горизонтом. Солнце вновь обрело свое место на небосводе. Стоящий перед домом величественный дуб смотрит в вечернее небо, зевая от перемены погоды и упиваясь росой, а сквозь его влажные листья струится теплый солнечный свет. Слабый ветерок вращает крылья мельницы. Вдалеке, где-то между небом и землей, сверкнула молния, безмолвно вторя своей предшественнице. Крыша дома остроконечная, справа, около дороги, расположена открытая галерея с широкими ступеньками и окрашенными перилами, ведущая в дом. С обеих сторон парадной двери растет алоэ, и деревянные качели раскачиваются от легкого ветра. Доносится перезвон колокольчиков, висящих на галерее. Воздух свеж. Царит леденящее душу безмолвие. Возникает предчувствие опасности. Животные и насекомые замерли. Слышен только перезвон колокольчиков. Внезапно поднимается ветер, он начинает с невероятной силой раскачивать качели. Листья вперемешку с мусором беспорядочно проносятся перед домом. Гигантское дерево пошатнулось под внезапным напором, и уже весь дом скрипит от напряжения. Колокольчики звенят, как одержимые. Неожиданно буря стихает, и кажется, что штурм дома завершен. Через некоторое время женщина распахивает деревянные ставни, и солнце еще раз озаряет маленький домик своей улыбкой. Колокольчики вновь и вновь повторяют свою простую мелодию. Ничто не предвещало разразившейся бури. Торнадо шириной почти с поле двинулся на восток, и путь его лежал через дом. Пшеница была вмиг скошена, как сухая зимняя трава, в небе смешались грязь, растения и камни.
602 Глава 26. Всеобъемлющее тестирование На очереди громадное дерево. Торнадо начинает осаду дома именно с него — дуба, простоявшего на своем посту почти пятьдесят лет, — яростно вырывает его из земли и откидывает в сторону. Дуб отлетает на сто футов и падает на крышу сарая. Следом идет грузовик, стоящий у гаража. Сначала торнадо высасывает все его содержимое, которое, кружась в вихре, уносится прочь, а затем и сам грузовик, словно гигантское перо, отрывается от земли и взмывает ввысь. Он падает вверх дном на сарай рядом с деревом, которое, проломив крышу, лежит на хранящихся в сарае стогах сена. Последней жертвой стал сам дом. Торнадо шаг за шагом пробирается все дальше, ища уязвимые места. Крыша начинает подниматься. Окна внезапно разбиваются. Кирпичи срываются с трубы дымохода, мгновенно исчезая в зловещей воронке. Дом начинает раскачиваться из стороны в сторону, и вдруг... Вдруг... Ничего. Так же быстро, как и появились, яростные тучи поднимаются обратно в темное небо и рассеиваются. Старый дом пережил еще одну истерику природы. Но на этом они не закончатся. В чем урок? Тестируйте свои коды. Всякое случается. Иногда происходит нечто очень плохое. Рано или поздно буря настигнет ваше программное обеспечение. И прольется дождь, и подует ветер, и земля сотрясется. Выстоит ли ваше программное обеспечение? Все будет зависеть от того, как вы его строили и насколько хорошо протестировали. Тестирование обычно не считается важным этапом процесса разработки (проектирования). Неопытные программисты — по природе своей оптимисты. Опытные программисты более циничны. Ведь они уже написали достаточно кодов, чтобы знать, что очень легко упустить из виду какую-нибудь мелочь, но именно она «всплывет» в конечном коде. Тестирование — это естественное занятие. Если бы мы сразу все правильно понимали, то нам не нужно было бы тестировать, не так ли? Если бы мы были достаточно опытны, чтобы полностью избегать ошибок — «багов», то в тестировании никто не нуждался бы, не правда ли? Нет, не правда. Эти предположения основаны на двух серьезных заблуждениях. Первое — хорошие программисты не пишут код с «багами». И второе — можно сразу понять все, что угодно. Во-первых, необходимо запомнить раз и навсегда: независимо от того, насколько квалифицированным разработчиком вы являетесь, вы всегда можете по случайности внести «баг» в свой код. Мартин Фоулер прямо говорит: «Я все же человек, и мне свойственно ошибаться». Пока не придумано, как внедрить в наш мозг не совершающий ошибок компьютер, мы будем делать ошибки практически во всем, за что бы ни взялись. Таков один из пунктов нашего договора с природой: в обмен на эмоции, абстрактное мышление и сознание мы расстались со способностью делать все с механической точностью. Во-вторых, помните: ничего, кроме простейших приложений, нельзя понять сразу. Все меняется. Меняются требования пользователей. Меняются технические требования. Меняются проектные ресурсы. Меняется и ваше понимание проблем бизнеса, которые вы пытаетесь решить. Все эти факторы влияют на ваше программное обеспечение. Кент Бек утверждает, что проблема заключается не в самом изменении, так как оно неизбежно. Проблема, по его словам, «... заключается в не-
С чего начать 603 способности справиться с переменами, когда они происходят»1. В любом случае сочетание простых человеческих ошибок с состоянием непрерывного изменения, характерным для процесса разработки любого сложного программного обеспечения, может гарантировать, что вы не создадите безупречное программное обеспечение с первой попытки. Поэтому, даже если вам удастся с первого раза создать приложение без ошибок, его скорее всего потребуется доработать. Рассчитывайте на это. Тестирование — ключ к успеху проекта. Оно так же важно, а может быть даже и важнее, чем кодирование. Ведь именно тестирование дает возможность исправлять ошибки не только в программном коде, но и в проекте. Тестирование предоставляет средства для точного измерения прогресса. Проект можно считать завершенным исходя не из того, сколько работы выполнено, а из того, насколько хорошо она выполнена. В своей книге «Экстремальное программирование» Кент Бек пишет следующее: «...пока тестирование не выполнено, вы не закончили. С окончанием тестирования можно считать, что все готово»2. Тестирование должно быть по возможности автоматизировано. Что я имею в виду? Тестирование должно быть таким же простым, как компиляция вашего приложения. Чтобы полностью протестировать приложение, вы должны всего лишь нажать кнопку или ввести простую команду. Как это сделать? Вы можете использовать инструменты автоматического тестирования, вставив в свое приложение поддержку механизма тестирования и настроив этот механизм «под себя». Подробнее мы поговорим об этом в следующих разделах. С чего начать Итак, с чего начать? С чего вы начинаете тестирование кода? Начните с анализа предположений. Неправильная работа обычно является результатом двух видов ошибок: логических ошибок и ошибок в предположениях. Каждый раз, вызывая функцию, вы делаете предположения о том, какие у функции допустимые параметры и допустимый контекст использования. Особенно при вызове кода, который писали не вы, например, при вызове процедур операционной системы или каких-либо библиотек. Вопреки здравому смыслу, наиболее сложным аспектом работы с большим телом кода является не выяснение логики каждой строки кода, а понимание предположений, сделанных при написании данного кода. Вы должны проанализировать предположения, сделанные о коде и самим кодом, который вы тестируете. Например, ради повышения производительности можно не проверять полностью параметры, предаваемые глубоко вложенной процедуре, поскольку они проверяются процедурой более высокого уровня. Однако, если позже вы вставляете эту глубоко вложенную процедуру во внешний мир, вы должны все протестировать. В такой ситуации лучше не вставлять саму процедуру, а создать процедуру- «обертку», которая проверяет параметры до вызова процедуры. 1 Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 28. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.) 2 Там же. С. 33.
604 Глава 26. Всеобъемлющее тестирование Ошибки в предположениях сложнее заметить, чем логические ошибки. Их трудно проследить, тем не менее вам необходимо это сделать, чтобы полностью протестировать свое программное обеспечение. Тестировать собственный код очень сложно, потому что всегда трудно быть объективным к своей работе. Тестировать код, написанный другими, тоже сложно, так как необходимо сделать вывод о предположениях и технических требованиях, лежащих в основе кода. Оба вида тестирования по-разному трудны. Обычно автор части кода лучше всего справляется с детальным рассмотрением предположений, лежащих в основе кода. Для этого нужно проследить, как создавался код. Проследив ход мыслей автора кода, можно понять предположения, положенные в основу кода, способ его дальнейшего использования и окружение, в котором он будет выполняться. Эти предположения составляют основу формальных требований или предусловий, выполнение которых должно быть гарантировано, поэтому их знание — ключевой момент в понимании всего кода. Когда вы приступаете к изучению чужого кода, начните с разговора с его автором, чтобы понять предположения, положенные в основу кода. Когда вы поймете предположения, попытайтесь упростить их. Упрощая предположения, лежащие в основе кода, вы постепенно превращаете проблему в легко решаемую задачу. Иногда требуется переосмыслить суть проблемы, чтобы найти более простые решения, или просто сосредоточиться на конкретном аспекте проблемы. Поймите, что автор части кода меньше всех подходит для объективного тестирования кода. В силу обстоятельств, человек, который разрабатывает часть программного обеспечения, большую часть времени тратит, обдумывая, как оно должно работать, а проводящий тестирование должен сосредоточиться на том, как программное обеспечение работает на самом деле. Иногда, делая акцент на том, как программное обеспечение должно работать, мы перестаем замечать, что оно не соответствует требованиям. Идеально для тестирования подходит человек, хорошо знающий предположения и требования к программному обеспечению, но не его реализацию. Необходимо, чтобы внимание было сконцентрировано на соответствие программного обеспечения требованиям, а не на деталях реализации. Нельзя искажать суть требований только для того, чтобы они соответствовали реализации. Работа по тестированию заключается в проверке логики системы и предположений, лежащих в ее основе. Бесполезность тестирования «Что?! — удивленно восклицаете вы. — Тестирование бесполезно?» Да, в конечном счете, тестирование бесполезно. Вы не можете протестировать каждый сценарий, с которым может столкнуться каждая функция вашего приложения. Вы не можете знать заранее, каким опасностям будет подвержено приложение. Даже если вам кажется, что все протестировано и можно начинать работать, пользователь обязательно найдет способ так «напрячь» ваше приложение, что вам и не снилось. Будучи программистом, вы обычно реализуете один вариант решения проблемы из множества возможных. Во время тестирования вам нужно доказать, что этот вариант работает со всеми возможными комбинациями допустимых входных дан-
Виды тестов 605 ных и не работает со всеми комбинациями недопустимых входных данных. Ваша работа, по сути, бесполезна. Вы пытаетесь доказать обратное: что нет ошибок в процедуре проверки допустимости и недопустимости входных данных. Итак, ключ к успешному тестированию — это сосредоточение усилий там, где это действительно принесет пользу. Сначала признайте, что тестирование никогда не бывает полностью завершенным, затем расположите по приоритетам те области, которые вы хотели бы протестировать, и определите возможную полноту их тестирования. Если бы в вашем распоряжении было неограниченное время и неисчерпаемые ресурсы, вы провели бы полное тестирование. Однако в реальном мире приходится чем-то жертвовать и идти на компромиссы. Какие-то области вы протестируете лучше, чем другие. При тестировании сложного приложения ваши возможности будут меняться в зависимости от тестируемых функций. Например, важнее проверить опции сохранения файлов, чем пункт «О программе». Главный компромисс — это компромисс между широтой и глубиной тестирования. Широта без глубины свидетельствует о наличии хотя бы подобия каждой функции, ноне дает вам сведений о том, хорошо ли эта функция работает. Глубина без широты свидетельствует о хорошей работе нескольких ключевых функций, пренебрегая при этом остальной частью приложения. В удачном тестировании должно быть получено такое сочетание глубины и широты, при котором обеспечивается максимально глубокое тестирование наибольшего количества функциональных возможностей с учетом доступного времени и ресурсов. Виды тестов Существует несколько видов тестов. В своей работе я выделил четыре наиболее полезных: модульные тесты, функциональные тесты, регрессионные тесты и интеграционные тесты. Давайте рассмотрим каждый тип отдельно. Модульные тесты Модульные тесты пишутся программистами для обеспечения той работы программы, которая была изначально задумана. Каким образом можно определить, что должна делать программа? Это можно сделать с помощью заранее созданных спецификаций и проектов. Модульные тесты часто встраиваются прямо в программное обеспечение и могут быть оставлены в конечном варианте программы или исключены из него. Модульные тесты подтверждают, модуль за модулем, что код правильно реализует функциональные возможности, которыми, по предположению программиста, он должен обладать. Модульные тесты помогают найти ошибки в программном обеспечении. Функциональные тесты Функциональные тесты обусловлены пользователями. Эти тесты должны показать, что система в целом работает так, как, по мнению пользователей, она и должна работать. Функциональные тесты более глобальны и всеобъемлющи, чем мо-
606 Глава 26. Всеобъемлющее тестирование дульные. Они также более завершенные, чем модульные. Система может быть очищена от «багов», но все же не пройти функциональные тесты. Последнее слово о том, работает ли система так, как она должна, за пользователями. Регрессионные тесты Регрессионные тесты — это тесты, которые сравнивают результаты многократных тестирований друг с другом, обычно используя при этом автоматизированные инструменты. Регрессионные тесты в ответе за то, что результаты сегодняшних тестов будут соответствовать вчерашним. Интеграционные тесты Интеграционные тесты обеспечивают правильную работу различных модулей, составляющих систему. Они гарантируют, что изменения, сделанные в одном модуле, не нарушат выполнение другого модуля. Поскольку методика ХР способствует ранней и устойчивой интеграции, то сам процесс разработки — это вид интеграционного теста. Другие методики во время процесса разработки проводят интеграционные тесты регулярно (например, еженедельно или ежемесячно), но при этом не забывают изолировать проблематичные модули так, чтобы они не образовывали плохих компоновок — другими словами, компоновок, которые не могут быть использованы или протестированы из-за грубых ошибок в определенном модуле. Когда тестировать Когда тестировать? Всегда. Не дожидайтесь окончания цикла разработки, чтобы произвести тестирование, если, конечно, вы не из числа тех, кто все время срывает сроки. Этот путь неминуемо ведет к отставанию от графика. И не стоит считать, что тестирование — это отдельный шаг в цикле разработки. Это не так. Просто тестируйте, не переставая. Проверяйте каждую часть кода, по мере его написания, лучше всего с помощью автоматизированной системы тестирования, которая сама была тщательно протестирована. Тесты лучше создавать до написания кода. Когда вы приступаете к добавлению какой-нибудь функциональности, сначала напишите ее тест. Может показаться, что это замедлит весь процесс, но это не так. Написание теста помогает вам четко сформулировать, в чем состоит предназначение кода еще до его написания. Скорее всего, тест будет развиваться вместе с кодом, но так и должно быть. Время, израсходованное на написание начального теста, потрачено с пользой, потому что это помогает вам проработать функциональные возможности, которые вы хотите добавить, еще до того, как вы добавите их. Значение тестов при рефакторинге Тесты позволяют провести рефакторинг кода с уверенностью в том, что если вы нарушите код, вы довольно быстро об этом узнаете. Вы не можете безбоязненно начинать рефакторинг части кода, если у кода нет полного набора надежных тес-
Тестирование экономит время 607 тов1. Если вы все же решитесь на такое, то навлечете на себя множество бед. Можно очень легко нарушить рабочий код, даже не подозревая об этом. Без полных тестов можно довольно легко изменить функциональные возможности кода (не в лучшую сторону), а в дальнейшем это будет достаточно сложно обнаружить. Если ваши тесты кода, прошедшего рефакторинг, не проверяют весь диапазон входных данных первоначальной процедуры, то такие тесты недостаточно надежны. Один из рисков рефакторинга заключается в том, что вы можете по неосторожности урезать область входных данных первоначальной процедуры. Только тесты, проверяющие всю область входных данных, справятся с этой задачей. Тестирование экономит время «Как?» — спросите вы. Тестируя, вы сокращаете время, которое уходит на поиск и исправление ошибок. Встраивая тесты в свой код и структурируя эти тесты так, чтобы они тестировали сами себя, вы создаете такие тесты, которые так же легко выполнить, как и само приложение. Если тесты выполняются легко, вы вдруг обнаружите, что постоянно выполняете их. С каждым успешным тестом ваша уверенность в собственном коде будет укрепляться. Вы обнаружите «баги» раньше и справиться с ними будет гораздо легче, чем без постоянного тестирования. Проще говоря, вы будете тратить намного меньше времени на устранение «багов» и, значит, потратите намного меньше времени на разработку хорошего кода. Существует мнение, что большая часть времени при разработке программного обеспечения уходит на написание кода. На самом деле при разработке обычного проекта гораздо больше времени тратится на устранение ошибок, чем на кодирование2. Если вам удастся уменьшить время, затрачиваемое на тестирование, с помощью автоматизированных самопроверяемых тестов, то вы значительно сократите весь цикл разработки. Как встроить самопроверяемые тесты в код? У каждого класса должен быть свой метод тестирования, который можно использовать для проверки этого класса. Этот тест должен быстро проверять каждый функциональный аспект класса. Вместо вывода выходных данных теста на консоль или в файл включите выходные данные в метод тестирования, затем закодируйте метод так, чтобы он сверял выходные данные с ожидаемыми результатами. Если они совпадают, должно появиться сообщение: «Тест успешно завершен». В противном случае необходимо получить сообщение о неудаче. Таким образом, вы предоставляете компьютеру возможность выполнять то, что у него лучше всего получается, — повторяющиеся, относительно скучные (но столь необходимые) задания. Можно автоматизировать тестирование сразу нескольких модулей (и даже целого приложения), используя инструменты автоматизированного тестирования. Среди них есть и такие, которые, например, тестируют пользовательские интерфейсы, а также соответствующий им код. Многие тестируют программные интерфейсы приложений для того, чтобы приложение могло распознать систему своего 1 Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999. С 89. 2 McConnell. After the Gold Rush. Redmond, WA: Microsoft Press, 1999. С 11.
608 Глава 26. Всеобъемлющее тестирование встроенного тестирования, если таковое имеется. Эти инструменты часто бывают очень сложны и обладают такой же функциональностью, как и языки сценариев: средствами анализа кода и отладки. Если вы работаете над большими проектами, инструменты, подобные этим, вам просто необходимы. Будьте осторожны при написании методов тестирования. Плохо написанные или неполные методы тестирования действуют, как розовые очки: они не показывают скрытое уродство и неправильное поведение вашего кода1. Задача методов тестирования обеспечить надежность вашей системы, поэтому необходимо убедиться в том, что они сами надежны. Наилучший метод тестирования Существует мнение, что функциональность программного обеспечения, которую нельзя проверить с помощью автоматизированных тестов, просто не существует2. Боюсь, что не могу с этим согласиться. Существуют функциональные аспекты проектирования программного обеспечения, протестировать которые очень трудно (если вообще возможно), особенно с помощью автоматизированных тестов. Например, аспект расширяемости. Как можно протестировать расширяемость с помощью автоматизированного теста? Это действительно невозможно, и всю расширяемость требуется учитывать при проектировании и кодировании. Другой пример — простота сопровождения (поддержки). Конечно, мы, разработчики, хотим написать поддерживаемый код, и, конечно, мы решим, что код содержит «баги», если он слишком сложен для поддержки. В то же время мы можем и не знать об этом, пока программное обеспечение не установлено и мы не попытались сопровождать его. Код может пройти автоматизированный тест и тем не менее содержать ошибки. Моя точка зрения такова: автоматизированные тесты — это основа, по которой мы можем судить об общем состоянии программного обеспечения, но это еще не конец. Так же как и при прохождении ежегодного медицинского осмотра какие-то болезни могут быть и не выявлены, так и автоматизированные тесты обеспечивают только основную проверку программного обеспечения. Они не гарантируют правильность программы, они даже не могут распознать каждую особенность программы. Некоторые элементы, такие как расширяемость и поддержка, находятся за пределами возможностей автоматизированных тестов, тем не менее они нуждаются в тестировании. Тестирование не может обнаружить все возможные ошибки в программном обеспечении и, следовательно, не может обнаружить все, что, скорее всего, не содержит ошибок. Может существовать такая функциональность программного обеспечения, которую не обнаруживают автоматизированные тесты, но вы тем не менее на нее полагаетесь при расширении и поддержке системы. Особенно это верно по отношению к тем элементам проекта, которые делают систему более легкой в использовании. Тестирование — это не просто еще один инструмент из вашего набора инструментов, оно вселяет в вас уверенность в надежности программы. Однажды обременив себя написанием методов тестирования своей работы, вам придется пи- ' Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison-Wesley, 2000. С 47. (Бек К. Экстремальное программирование. СПб.: Питер, 2002.) 2 Там же. С. 45.
Другие виды тестирования 609 сать их все время и, кроме этого, постоянно их обновлять. Нельзя написать один метод тестирования, никогда не обновлять его и, применив его, сказать, что код проверен. Если вы внедряете тот или иной метод тестирования, разработчики, которые потом смотрят вашу работу (включая вас), вправе ожидать того, что метод справится с тем, о чем говорится в его названии: с полным тестированием класса. Все, что с этим не справляется, считается «багом» и причиняет больше вреда, чем пользы. Приобретая опыт в написании тестов, вам становится легче решать, когда тест необходим, а когда — нет. Все это довольно субъективно, и нужно быть осторожным. Как только под давлением сроков вы начнете думать, какие бы тесты исключить, вы будете вынуждены отказаться от критического теста. Избавляйтесь от тестов только тогда, когда вы абсолютно уверены в этом, и никогда не обосновывайте свои решения соображениями сроков. Не бойтесь тестировать слишком много. Пусть если у вас и будет какой-нибудь недостаток как у разработчика — так только этот. Другие виды тестирования Во многих книгах проводится различие между тестированием, проверкой кода, сквозным контролем и общей проверкой качества программного обеспечения. Однако я считаю, все это различные формы тестирования. Или, лучше сказать, тестирование — это одно из многих средств обеспечения качества программного обеспечения. Говорите ли вы о сквозном контроле или о проверке кода, в любом случае вы говорите о способе выявления ошибок в программном обеспечении. Они являются частью одного и того же процесса, цель которого — выполнение одной и той же функции в цикле разработки программного обеспечения, поэтому, по крайней мере, в этой книге, все они сгруппированы под заголовком «тестирование». Итак, как бы вы ни назвали проверку кода, сквозной контроль, другими формами тестирования или другими средствами обеспечения качества программного обеспечения, — все они являются главными инструментами в разработке хорошего программного обеспечения, поэтому нам следует ознакомиться с ними поближе. И начнем мы с общей проверки качества программного обеспечения. Термин длинный, но он заключает в себе весь смысл тестирования. Я подумывал назвать эту главу «Обеспечение качества программного обеспечения» или «Обеспечение надежности программного обеспечения», но мне показалось, что это будет звучать формально. Конечно, тестирование обеспечивает качество программного обеспечения. Улучшение качества программного обеспечения — это та причина, по которой мы его тестируем. Мы хотим, чтобы приложения были настолько свободными от «багов» и настолько функциональными, насколько это возможно. Мы хотим выпускать высококачественное программное обеспечение, поэтому мы и тестируем его до того, как предоставить его пользователям. Когда программисты говорят о тестировании, они обычно имеют в виду модульные тесты. Существуют различные виды тестирования и модульное тестирование — лишь один из них. Модульное тестирование — не единственное средство обеспечения качества программного обеспечения и, согласно Стиву Макконнел-
610 Глава 26. Всеобъемлющее тестирование лу1, оно даже не самое лучшее. Макконнелл пишет, что «тестирование (модульное, функциональное или интеграционное) поможет найти только меньше половины ошибок до выпуска продукта». О чем это нам говорит? Если даже тестирование не может обнаружить все ошибки, то что может? Главное заключается не в том, чтобы найти ошибки, а в том, чтобы их избежать. Мораль такова: вы не можете тестировать только качество программного обеспечения. Еще вы должны проектировать и планировать. Проверка качества программного обеспечения должна быть систематической. Для этого вы должны проектировать и не отступать от стандартного процесса. Далее мы рассмотрим некоторые другие методы улучшения качества программного обеспечения. Проверка кода Макконнелл утверждает2, что проверка кода — лучший способ улучшения качества программного обеспечения, по сравнению с любыми другими формами традиционного тестирования. Что такое проверка кода? Проверка кода — это изучение кода программиста другим программистом или командой программистов. Проверка кода должна быть конструктивной, объективной, усиливающей положительный эффект и мешающей влиянию отрицательного. Качество программного обеспечения улучшается в результате проверки кода. Во-первых, это позволяет менее опытным разработчикам использовать опыт разработчиков-ветеранов. То, чему опытные разработчики долгое время учились, может быть быстро и легко передано молодым посредством проверки кода. Начинающие разработчики могут повысить свою квалификацию, изучая комментарии разработчика-ветерана о коде. Так улучшается не только качество рассматриваемого кода, но и того кода, который будет написан начинающим разработчиком в будущем, потому что он, благодаря проверке, познакомится с лучшими методами разработки. Проверка кода позволяет техническим менеджерам увидеть, как развивается проект, и оценить качество работы разработчика. Это позволяет менеджерам узнать, выполнено ли кодирование и правильно ли оно выполнено. По существу, проверка кода становится инструментом оценки уровня качества кода и для самого разработчика. Проверка кода дает техническому менеджеру все, что ему нужно для того, чтобы решить проблемы качества, пока эти проблемы не усложнились. После проверки кода обычно улучшается общее качество программного обеспечения. Если вы знаете, что вашу работу будут проверять — а проверка не ограничится компиляцией и запуском кода, — вы, скорее всего, не раз перепроверите всю свою работу. Многочисленные исследования подтверждают это предположение3. Может случиться и такое: объясняя другим суть проблемы, с которой вы столкнулись при кодировании, вы найдете способ ее решения. Ответ придет к вам на полпути вашего объяснения. Со мной такое случалось не раз, и я был свидетелем подобных случаев с другими. Необходимость объяснять кому-то суть проблемы 1 McConnell. Code Complete. Redmond, WA: Micosoft Press, 1993. С 571. 2 Там же. С. 587. 3 Там же. С. 574.
Другие виды тестирования 611 вынуждает вас еще раз обдумать ее, и часто этого бывает достаточно для того, чтобы решить ее. Чтение кода Чтение кода можно назвать «распределенной проверкой кода». Люди берут копии исходного кода, читают их, затем пишут свои комментарии относительно стиля кода, его качества и, конечно, допущенных ошибок. Как правило, эти люди встречаются, чтобы обсудить проблемы с автором кода, но можно воспользоваться и электронной почтой. Ценность личной встречи заключается в том, что читатели могут обсудить свои комментарии с автором и понять, почему он написал код именно так. А автор может лучше понять, какие проблемы с кодом могут возникнуть в будущем и почему он должен обратить на них свое внимание. Инспектирование Инспектирование — это особый тип технических проверок, который направлен на обнаружение (не исправление) проблемных частей системы. Проводит инспектирование не автор программного обеспечения, а человек, специализирующийся на проведении таких проверок. Каждый участник инспектирования выполняет свою особую роль и, подготавливаясь к нему, заранее заявляет об интересующих его областях программного обеспечения. Менеджерам не рекомендуется посещать такого рода собрания, потому что эти собрания должны быть техническими по своей сути, а одно лишь присутствие менеджеров может помешать этому. Основное различие между инспектированием и проверкой кода заключается в том, что после инспектирования готовится доклад со списком всех недочетов, обнаруженных в его ходе. Процесс намного более формален, чем типичный процесс проверки кода. Во время проверки кода автор кода может делать, а может и не делать заметки относительно предложенных изменений. Он может даже сразу менять код. Это зависит от значимости изменений. Никакие изменения кода не производятся во время инспектирования. Его цель — собрать информацию о недостатках программного обеспечения и о проблемах, которые нужно решить. Доклад инспектирования помогает отследить недочеты, которые нужно исправить, и выделить проблемные области в системе. При повторном осмотре доклад предыдущего инспектирования играет роль входных данных: повторно проверяет только проблемные области. Инспектирования обычно проводятся в средних и крупных организациях, и довольно редко — в мелких. Для того чтобы провести инспектирование, требуется команда специально подготовленных людей, а они редко работают в мелких компаниях. Руководитель инспектирования (модератор) играет очень важную роль. Он следит за тем, чтобы не было ссор и выяснений отношений, и направляет усилия собравшихся на выявление технических проблем и проблем бизнеса, а не на решение личных вопросов. Он следит за тем, чтобы инспектирование не переросло в публичное бичевание автора, а оставалось собранием, на котором объективно оценивают программное обеспечение.
612 Глава 26. Всеобъемлющее тестирование Если вы — автор программного обеспечения, проходящего инспектирование, старайтесь не защищать ваше детище. Это самое худшее из того, что вы можете сделать. Прислушивайтесь к критике и выражайте при этом свою благодарность. Это еще не означает, что вы со всем согласны. Это означает только то, что вы понимаете беспокойство человека, критикующего вашу программу. Вы должны быть готовы к необоснованной критике и к спорам. Не волнуйтесь об этом. Позже вы сможете сами исследовать каждый пункт и решить, что с ним делать. Если вы решаете исправить критикуемый пункт, не оправдывайтесь. Ведь никто даже не пытался нанести вам личное оскорбление. Критикуют вашу работу, а не вас, и пока вы ведете себя как профессионал и выполняете свою работу со знанием дела, так и будет. Сквозной контроль Сквозной контроль — это менее регламентированный технический обзор. Термин «сквозной контроль» может означать разное. Обычно он представляет собой своего рода гибрид проверки кода и осмотра. Как правило, он проводится автором кода, и цель такого контроля — определить способы улучшения качество кода. Как и проверка кода, сквозной контроль может быть средством обучения для начинающих разработчиков у своих более опытных коллег. Кроме того, начинающие разработчики могут представить на рассмотрение команды идеи, альтернативные методам, поддерживаемым ветеранами-разработчиками. Метод сквозного контроля популярен. Встреча, на которой проводится сквозной контроль программы, может быть официальной: со слайдами и подготовленным докладами; или она может быть неофициальной, когда автор программного обеспечения просто беседует о своем продукте. Как правило, эти встречи не бывают затянутыми (обычно они длятся меньше чем час) и менеджеры не участвуют в них. На таких собраниях могут решаться разные вопросы в зависимости от требований участников. Итоги Тестируйте вашу работу. Тестируйте почаще. Тестируйте, используя встроенные тесты, автоматизированные, используя проверку кода, осмотры и walkthroughs. Тестируйте снова и снова. Благодарите тех, кто находит ошибки в вашей работе, исправляйте эти ошибки и снова благодарите нашедших за их усилия. Не старайтесь защищаться, когда проверяют вашу работу. Ведь речь идет об улучшении качества программного обеспечения, а не о критике вашей личности. Когда качество программного обеспечения улучшается, каждый от этого выигрывает — не только вы и те, кто связан с разработкой программного обеспечения, но и все, кто использует это программное обеспечение. Цель тестирования — снизить риск неудачи — неудачи выпустить низкокачественный программный продукт, не отвечающий требованиям клиента. Тестирование снижает риск неудачи. Достижение 100-процентного охвата тестирования означает, что ваш риск неудачи равен нулю. Конечно, достигнуть нулевого
Эпилог 613 риска невозможно, но вы должны к этому стремиться. Вы можете научиться снижать риск неудачи до приемлемого уровня, используя методы, представленные в этой главе. *■ Эпилог Как только буря стихает, грузовик, перевернувшись в воздухе, падает так, что только часть кузова торчит из крыши дома. Бензин выливается из бензобака, и от небольшой искры разгорается пламя. И вот уже весь сарай охвачен пламенем, и огромное дерево, чья листва все еще хранит дождевую влагу и которое уже никогда не будет стоять на страже дома, медленно исчезает в погребальном костре. А колокольчики продолжают звенеть...
Алфавитный указатель Символы .NET, 389 @@ERROR, 61, 190 ©©IDENTITY, 204 ©©NESTLEVEL, 63 ©©ROWCOUNT, 195. ActiveX, 442 ADO.NET, 394 ALTER PROCEDURE, 41 В BEGIN/END, 69 BETWEEN, 247 CASE, 68,129 CLI, 444 COM, 446 объекты, 442 . основные элементы, 446 CREATE PROCEDURE, 30 CREATE TRIGGER, 199 CREATE VIEW, 221 CROSS JOIN, 176 DBCC, 544 DBCC INDEXDEFRAG, 406 DBCC SHOWCONTIG, 406 DDE, 442 Dim, 448 DISTINCT, 426 DLL, 443 DOM, 304,321 DSSSL, 305 DTD, 304,310 edge table, 359 EXEC, 42 EXEC(), 82 F fillfactor, 404 GGSQLBuilder, 118 GML, 291 GROUP BY, 426 GUID, 369 H HTML, 304 HTML гиперссылки, 296 таблицы, 291 шаблоны, 297 HTML, 290 HTTP, 325 HTTP, 290 IAM, 401 INFORMATION_SCHEMA, 225, 226, 542
Алфавитный указатель 615 INSERT, 42 INSERT...EXEC, 183 3 just-in-time, 392 L LoadLibrary, 445 N NOEXPAND, 252 NULL, 90,365 О OBJECTPROPERTYO, 455 ODBC, 437,474 ODSAPI, 463 OLE, 442 Open Data Services, 464 OPENQUERYO, 234 OPENXML(), 355 ORDER BY, 426 P Profiler, 431,500 ProgID, 450 Q Querylnterface, 447 R RAISERROR, 62,189 RANDOM(), 179 RCP, 48 RETURN, 57 s SELECT, 66 SELECT...FORXML, 340 AUTO, 341 EXPLICIT, 344 директивы, 345 FOR XML AUTO ELEMENTS, 343 RAW, 341 SELECT...INTO, 88 SET, 89 SETNOCOUNTON, 41 SET XACT_ABORT, 197 «SGML», 290 SOUNDEX(), 268 sp_checkspelling, 449 sp_create_backup_job, 513 sp_diff, 489,517 sp_exporttable, 451 sp_generate_script, 490 sp_generate_test_data, 184 sP_getSQLregistry, 459 sp_importtable, 456 sp_list_trace, 507 sp_makewebtask, 294 sp_OA, 449 sp_object_script_comments, 35 sp_proc_runner, 509 sp_readtextfile, 486 sp_run_xml_proc, 379 sp_showstatdate, 415 sp_start_trace, 500 sp_stop_trace, 505 sp_xml_concat, 377 Spooling, 427 STRESS.CMD, 438 Syscomments, 31 T Transact-SQL, 82 недокументированные возможности, 521 и UPDATE STATISTICS, 414 URL, 290 USE, 78 V Visual Studio .NET, 395 w Watch, 429 Web Release, 362 WITH CHECK OPTION, 237 X XML, 301 XML Schema, 312 сравнение с HTML, 305 XML Bulk Load, 370
616 xml-updategram, 363 XMLFragment, 370 XP, 594 xp_array.dll, 557 xp_createarray, 558 xp_destroyarray, 563 Xp_getarray, 561 Xp_listarray, 565 xp_setarray, 559 xp_setpriority, 481 A автоматизация, 442, 448 агрегация, 447 апдейтограмы, 363 identity-значение, 368 неопределенные значения, 365 параметры, 366 схемы отображения, 365 аппроксимация методом наименьших квадратов, 281 аргумент поиска, 419 Б библиотеки динамической компоновки, 443 загрузка, 445 бизнес-процессы, моделирование, 130 блокировки, 410 В В-деревья, 402 версии контроль при помощи Query Analyzer, 115 системы контроля, 108 вызовы вложенные, 63 рекурсивные, 63 Г гиперссылки, 296 гистограммы, 274, 414 д данные базы, 591 возвращение, 468 время создания, 185 Алфавитный указатель данные (продолжение) вставка при помощи OPENXML(), 360 группировка при помощи Profiler, 437 реляционное моделирование, 158 словарь, 161 создание INSERT...EXEC, 183 RANDOM(), 179 sp generate test data, 184 перекрестное объединение, 176 удваивание, 181 структура, 136 деиормализация, ограниченная, 152 дефрагментация, 404 диаграммы E-R, 142 баз данных в Enterprise Manager, 173 директивы, 345 cdata, 349 hide, 348 id, id'ref, idrefs, 350 значения, 345 документ валидный, 309 объектная модель (DOM), 304,321 определение типа (DTD), 304,310 формально правильный, 309 3 запросы URL, 327 специальные символы, 329 таблицы стилей, 329 хранимые процедуры, 332 вложенные, 237 оптимизация, 416 подзапросы, 424 шаблонные, 333 И идентифи каторы глобальные уникальные (GUID), 369 программные, 450 изоляция, расширенных хранимых процедур, 480 индексирование, 401
Алфавитный указатель 617 индексы, 74, 401 кластерные, 401 ключи, 402 на представлении, 407 некластеризованные, 401 пересечение, 404 фрагментация, 404 интерфейс уровня вызова (CLI), 464 интерфейсы, 446 СОМ-, 446 уровня вызовы (CLI), 444 интранет, 325 К карта распределения индексов (IAM), 401 каталог виртуальный, 324 конфигурация, 325 указание пути и имени, 326 ключи внешние, 166 дублирующиеся, 371 код инспектирование, 611 проверка, 610 сквозной контроль, 612 чтение, 611 код, управляемый, 391 команды, управления выполнением, 60 комментарии, 455 Л лексемы, специальные, 117 м маршаллинг, 447 массивы, 556 многомерные, 571 метаданные, 85 моделирование E-R, 142, 145 бизнес-процессов, 131 логическое, 159 реляционное, данных, 131 сущность-связь, 130,141 н наименования, отсроченное разрешение, 31 нормализация, 148 О объединения, 421 операторы логические, 425 физические, 425 оптимизация, 418 отладка, 428 расширенных хранимых процедур, 479 отсечение, 273 ошибки в XML-документе, 374 обработка, 190 пользователя, 191 сообщения об, 188 фатальные, 194 п переменные глобальные, 60 ©©ERROR, 61 ©©IDENTITY, 204 ©©NESTLEVEL, 63 локальные, 74 плотность, 411 представления, 221 SET ANSI_NULLS и SET QUOTEDJDENTIFIERS, 224 вызов хранимых процедур, 234 динамические, 239 индексированные, 251 проектирование, 253 исходный код, 222 конструкция WITH CHECK OPTION, 237 обновляемые, 236 ограничения, 223 оператор BETWEEN, 249 параметризованные, 238 распределенные секционированные, 250 секционированные, 241
618 Алфавитный указатель представления {продолжение) создание в INFORMATION_SCHEMA, 226 стандарта ANSI SQL-92, 225 статические, 239 функция OBJECTPROPERTY0, 221 программирование, экстремальное (ХР), 594 производительность, 403 профилирование, 431 процедуры dt, 109 временные, 49 системные, 49 хранимые, 30, 79 в URL-запросах, 332 внутренние, 54 вызов из представления, 234 выполнение, 42 для администрирования, 486 изменение, 41 компиляция, 43 отладка, 428 параметры, 56, 58, 59 при помощи протокола RPC, 48 просмотр текста, 34 псевдокомпиляция, 43 расширенные, 52, 463 системные, 49 создание, 31 процессы, 134 псевдонимы, 71 редактор. См. AppBrowser рекурсия, 283 рефакторинг, 585, 606 селективность, 411 серверы внепроцессные, 451 внутрипроцессные, 451 символы, специальные, 329 слияние, 423 слова, ключевые, 37, 111 спулинг, 427 ссылки, счетчик, 446 статистика, 411 загрузка, 418 обновление, 414 просмотр, 413 хранение, 413 строка, командная, 431 сущности добавление внешних, 134 идентификаторы, 155 классы, 143 явления, 143 схемы XML-, 351 аннотированные, 353 сценарии комментарии, 77 контроль версий, 117 расширенные свойства, 77 сегментирование, 78 средство форматирования GGSQLBuilder, 119 файлы, 78 счетчик, ссылок объекта, 446 таблицы HTML, 291 таблицы временные, 80 копирование, 88 очистка, 88 системные, 80 стилей в URL-запросах, 329 стилей при работе с клиентом, 336 тестирование, 602 тестирование, нагрузочное, 437 тесты интеграционные, 606 модульные, 605 регрессионные, 606 функциональные, 605 транзакции, 373 SET XACT_ABORT, 197 в триггерах, 213 управление, 196 трассировка, 431 ODBC-, 437
Алфавитный указатель 619 трассировка, (продолжение) недокументированные флаги, 551 трассы, 431 триггеры, 74, 199 INSTEAD-, 207 аудит, 210 вызов хранимых процедур, 214 ограничения, 206 определение изменений, 200 отключение, 217 отладка, 430 работа с identity, 205 транзакции в, 213 Ф функции inline-, 258 OBJECTPROPERTYO для возврата метаданных, 262 недокументированные, 551 ограничения, 259 пользовательские, 74, 255 отладка, 430 параметризованные, 284 системные, 60, 265, 543, 567 скалярные, 255 табличные, 256 X хэширование, 424 хранилища, 135 ц циклы bXSLT, 317 вложенные, 422 ш шаблоны HTML, 297 шаблоны клиентские, 337 проектирования Исполнитель, 96 Итератор, 92 Конвейер, 97 Одиночка, 103 Пересечение, 94 Прототип, 102 Спецификатор, 95 Уборщик, 99 шифрование, 112 э энтропия, программная, 584 Я язык моделирования, унифицированный (UML), 131 определения данных (DDL), 72 расширяемый, стилей (XSLT), 315
КенХендерсон Профессиональное руководство по SQL Server: хранимые процедуры, XML, HTML (+CD) Перевели с английского В. Голубев, А. Жилин, М. Рахманов, В. Щербинин Главный редактор Е. Строганова Заведующий редакцией А. Кривцов Руководитель проекта В. Шрага Научный редактор В. Брылёв Литературный редактор Ю. Леонтьев Художник Н. Биржаков Корректор В. Листова Верстка А. Келле-Пелле Лицензия ИД № 05784 от 07.09.01. Подписано к печати 26.10.04. Формат 70x100/16. Усл. п. л. 50,31. Тираж 3000. Закач 983 ООО «Питер Принт», 194044, Санкт-Петербург, пр. Б. Сампсониевский, дом 29а. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Отпечатано с готовых диапозитивов в ОАО «Техническая книга» 190005, Санкт-Петербург, Измайловский пр., 29