Text
                    Профессиональное
руководство по 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 Гл