Вы находитесь на странице: 1из 403

том1

а л ь м а н а х программиста
Тематический сборник материалов
MSDN» Library и MSDN» Magazine

Microsoft

ADO.NET
licrosoft

SQL Server

Доступ к данны
из приложений

КРУССШ
Microsoft*

Microsoft5
TM

Составитель Ю. Е. Купцевич

Москва 2003

fii. P У С С I А 1 P E 1 i К Ц 1 1
УДК 004.45
ББК 32.973.26-018.2
А57

А57 Альманах программиста, том I: Microsoft ADO.NET, Microsoft SQL Server, доступ к
данным из пршюжений/Сост. Ю.Е.Купцевич. — М.: Издательско-торговый дом «Русская
Редакция», 2003. - 400 с.: ил.
ISBN 5-7502-0234-8
Альманах представляет собой тематический сборник статей из журнала MSDN
Magazine/Русская Редакция и Microsoft MSDN Library. Издание адресовано широкому
кругу программистов, интересующихся современными и перспективными информаци-
онными технологиями. Первый том альманаха, посвященный работе с базами данных,
состоит из трех тематических рубрик, содержащих 20 статей.

УДК 004.45
ББК 32.973.26-018.2

.NET. ActiveSync, ActiveX, IntelliSense. JScript, Microsoft, Microsoft Press, MSDN,


Outlook. SQL Server, VBScript, Visual Basic, Visual C+-. Visual J++, Visual Studio,
Win32, Windows и Windows NT ЯНЛЯЕОТСЯ либо охраняемыми товарными знаками, либо
товарными знаками корпорации Microsoft в США и/или других странах. NT — товар-
ный знак компании Northern Telecom Limited. Все другие товарные знаки являются
собственностью соответствующих фирм.
Информация, приведенная в этой книге, в том числе URL и другие ссылки на Web-сай-
ты, может быть изменена без предварительного уведомления.

£) Microsoft Corporation и CMP Media LLC, 200:i


ISBN 5-7502-0234-8 0 ИТД «Русская Редакция», 2003

Альманах программиста, том 1:


Microsoft ADO.NET, Microsoft SQL Server,
доступ к данным из приложений
Составитель Ю. Е. Купцевич
Технический редактор Л. А, Памчук
Компьютерная верстка и дизайн В. Б. Хилъченко
Дизайнер обложки Е. В. Козлова
Главный редактор А. И. Козлов
Подготовлено к печати издательством «Русская Редакция» 12КЖ7, Москва, ул. Заречная, д.9
тел.: (095) 112-0571. тел./факс: {091)) 145-4519,e-mail: info@rusedit.ru, http://www.riistxlil.ru

Подписано в печать 14.03.03 г. Тираж 5000 экз. Формат 70x100/16. Физ. п. л. 25


Отпечатано в ОАО «Типография «Новости* 1(17105. Москва, ул. Фр. Энгельса, Ad
Оглавление

Microsoft ADO.NET 7
Алекс Макмен. Крис Брукс, Стив Басби, Эд Джезирски
Руководство по архитектуре доступа к данным на платформе .NET 9
Джонни Папа
Доступ к данным ADO.NET: концепции и реализация 81
Боб Бьючмин
ADO.NET Разработка собственных провайдеров данных
для .NET Data Access Framework 102
Джонни Папа
Доступ к данным Выражения в ADO.NET 122
Дино Эспозито
На переднем крае Двоичная сериализация
ADO.NET-объектов 133
Прийя Дхаван
Управление транзакциями Разработка распределенных
приложений в .NET 152

Microsoft SQL Server 167


Джонни Папа
Доступ к данным Пять способов подстегнуть
производительность SQL 169
Алок Мехта и Дэниел Уильяме
Сценарии в SQL Преобразование данных и предоставление
отчетов SQL Server 2000 через VBScript-интерфейсы 179
Франческо Балена
SQLServer и DMO Автоматизация выполнения
административных задач в SQL Server 194
Дэйв Грундгейгер, Энсон Голдэйд и Вэрон Фугман
SQL и XML Вызов хранимых процедур и получение
их результатов через Web 213
Оглавление

Марк Браун и Дэвид Менье


Мобильность Компактные и надежные приложения
на основе SQL Server СЕ 2.0 и .NET Compact Framework 239
Марк Браун
SQLServer Доставка информации в реальном времени
с применением Notification Services 259

Доступ к данным из приложений 273


Атиф Азиз
.NET Reflection Динамическое связывание уровня данных
с хранимыми процедурами и командами SQL 275
Майкл Говард и Кит Браун
Советы по защите Десять лучших приемов защиты кода,
о которых должен знать каждый разработчик 303
Джонни Папа
Доступ к данным Объекты PataRelation в ADO.NET 317
Прийя Дхаван
Разработка распределенных приложений в .NET
Операции над данными с иерархической структурой 326
Джонни Папа
Доступ к данным Модификация приложения для отображения
данных в Web 346
Джонни Папа
Доступ к данным ADO и XML: создание уровня доступа
к данным на основе компонента DataManager 360
Марк Герлах
Spider в .NET Сбор информации от Web-сайтов и каталогов
с применением Visual Basic .NET и ADO.NET 370
Кен Спенсер
Основы и тонкости Управление транзакциями между
компонентами .NET .. ... 385
От составителя

Уважаемый читатель!

У вас в руках — первый том альманаха программиста, который издатель-


ство «Русская Редакция» планирует выпускать по мере накопления мате-
риалов (примерно раз в квартал). Это уникальное издание адресовано про-
фессионалам в области современных информационных технологий. Каж-
дый том представляет собой тематический сборник статей из журнала
«MSDN Magazine» и Microsoft MSDN Library по наиболее актуальным и
перспективным технологиям разработки программного обеспечения.

Главная цель, которую мы ставили себе при подготовке альманаха, — из-


бавить вас от поисков нужных материалов, опубликованных в отдельных
номерах «MSDN Magazine» или документах MSDN Library за 2000-2003 гг.
Поэтому альманах, помимо статей, уже напечатанных в журнале «MSDN
Magazine/Русская Редакция», включает материалы, переведенные специ-
ально для данного издания.

Первый том альманаха, посвященный работе с базами данных, состоит из


трех тематических рубрик.

• Технология доступа к данным Microsoft ADO.NET. Подробно опи-


сывается архитектура доступа к данным на платформе .NET, поясня-
ются концепции и реализация ADO.NET, показывается, что представ-
ляют собой выражения (expressions) в ADO.NET, как создавать соб-
ственные провайдеры данных и управлять транзакциями.
• Microsoft SQL Server. Подборка материалов за период 2001-2002 гг.
по оптимизации SQL-запросов, использованию сценариев в SQL, объ-
ектной модели SQL-DMO в SQL Server 7.0 и SQL Server 2000, созда-
нию хранимых процедур SQL Server для автоматизации доставки ин-
формации в XML-формате из базы данных в клиентские компонен-
ты, службе Notification Services, а также по новым возможностям и
практическому применению SQL Server CE 2.0 для мобильных уст-
ройств,
От составителя

• Доступ к данным из приложений. Материалы по динамическому


связыванию уровня данных с хранимыми процедурами и командами
SQL через механизм .NET Reflection, использованию ADO.NET-обьек-
тов DataRelation, операциям над иерархическими наборами строк, со-
зданию уровня доступа к данным через компонент DataManager (для
тех, кто пока работает с ADO), сбору информации от Web-сайтов и
каталогов с применением ADO.NET, реализации универсального сред-
ства отображения данных на основе СОМ+ или MTS (Microsoft Trans-
action Services), выполнению локальных и распределенных транзакций
в .NET-приложениях, созданию транзакций в ADO.NET.

Исходный код для статей альманаха можно скачать по ссылке, указанной


в конкретной статье, или в полном комплекте (для всех статей альманаха)
с Web-сайта издательства «Русская Редакция* по ссылке www.rusedit.ru/
download/code_ap I .zip.

Второй том альманаха будет посвящен тематике, связанной с ASP.NET и


Интернет-приложениями, в том числе использованию HTTP-конвейеров,
работе с элементами управления ASP.NET, написанию безопасного кода,
созданию Web-сервисов, а также разработке приложений электронной
коммерции и других Web-приложений.

В дальнейшем планируется выпуск альманахов по базовым механизмам


.NET Framework (модели зашиты, отражение, удаленное взаимодействие,
сервисы взаимодействия с неуправляемым кодом и т. д.), специфике язы-
ков программирования, поддерживающих .NET, отладке/тестированию и
по другим темам.

Если вас интересует специфическая тематика или определенные матери-


алы из «MSDN Magazine» и MSDN Library, обращайтесь на сайт издатель-
ства www.rusedit.tu или по адресу almanah@rusedit.ru. Мы постараемся
учесть ваши пожелания в будущих выпусках альманаха.
альманах программиста
Алекс Макмен, Крис Брукс, Стив Басби, Эд Джезирски

Руководство по архитектуре
доступа к данным
на платформе .NET

В этом документе излагаются принципы разработки на основе ADO.NET


уровня доступа к данным в многоуровневом .NET-приложении. Основное
внимание уделяется ряду наиболее распространенных задач и ситуаций,
связанных с доступом к данным. Даются рекомендации по выбору наиболее
подходящих методов и приемов.

Введение
При разработке уровня доступа к данным .NET-приложения следует ис-
пользовать модель доступа к данным Microsoft ADO.NET. ADO.NET обла-
дает богатыми возможностями и удовлетворяет требованиям доступа к
данным, предъявляемым многоуровневыми слабосвязанными Web-прило-
жениями и Web-сервисами. Как и многие другие объектные модели с под-
держкой богатой функциональности, ADO.NET позволяет решать одни и
те же задачи несколькими способами.

В этом документе содержатся рекомендации по выбору наиболее подходя-


щего метода доступа к данным. С этой целью детально описывается целый
ряд типичных ситуаций доступа к данным, даются рекомендации по повы-
шению производительности и предлагаются наиболее эффективные спо-
собы работы. Кроме того, в документе даются ответы на другие часто за-
даваемые вопросы: где лучше хранить строки подключения к базам данных?
Как реализовать поддержку пула соединений (connection pooling)? Как ра-
ботать с транзакциями? Как загружать данные постранично (paging), чтобы
пользователи могли пролистывать наборы записей большого объема?

* Alex Mackman, Chris Brooks, Steve Busby, and Ed Jezierski .NET Data Access Architecture
Guide//MSDN Library. Microsoft Corporation. 2001. October. - Прим. изд.
Microsoft ADC.NET

Заметьте, что в этом документе основное внимание уделяется применению


ADO.NET для доступа к данным Microsoft SQL Server 2000 с использова-
нием SQL Server .NET Data Provider — одного из двух провайдеров дан-
ных, поставляемых с ADO.NET. Там, где это нужно, в документе подчер-
киваются особенности, о которых следует знать при использовании OLE
DB .NET Data Provider для доступа к другим источникам данных с под-
держкой OLE DB.

Конкретную реализацию компонента доступа к данным, разработанного с


применением принципов и методов, описанных в этом документе, см. в
Data Access Application Block {http://msdn.microsoft.com/library/en-us/
dnbda/html/daab-rm.asp). Обратите внимание, что для этой реализации
имеется исходный код, который можно напрямую использовать в ваших
.NET-приложениях.

Кому адресован этот документ


В этом документе описываются принципы, которыми должны руковод-
ствоваться архитекторы приложений и корпоративные разработчики при
создании .NET-приложений. Его следует прочитать, если в ваши обязан-
ности входят проектирование и разработка уровня данных в многоуровне-
вом .NET-приложении.

Что вы должны знать


Чтобы применить это руководство для создания .NET-приложений, необ-
ходим практический опыт разработки кода для доступа к данным с ис-
пользованием ADO (ActiveX Data Objects) и/или OLE DB, а также опыт
работы с SQL Server. Вы также должны знать, как разрабатывать управля-
емый код для платформы .NET, и быть в курсе фундаментальных измене-
ний, внесенных в модель доступа к данным с появлением ADO.NET. До-
полнительную информацию по программированию для платформы .NET
см. по ссылке http://msdn.microsoft.com/net.

Введение в ADO.NET
ADO.NET — модель доступа .NET-приложений к данным. Ее можно ис-
пользовать для доступа к реляционным СУБД, таким как SQL Server 2000,
и ко многим дополнительным источникам данных, для работы с которы-
ми предназначен провайдер OLE DB. В известной степени ADO.NET от-
ражает новейшие эволюционные достижения в развитии технологии ADO.
Однако в ADO.NET появился ряд серьезных изменений и новшеств, выз-
ванных слабосвязанной природой Web-приложений и тем фактом, что по
сути они отсоединены от баз данных. Сравнение ADO и ADO.NET см. в
статье «ADO.NET for the ADO Programmer» в MSDN.
Руководство по архитектуре доступа к данным на платформе -NET 1.1

Одно из ключевых новшеств ADO.NET — замена ADO-объекта Recordset


комбинацией объектов DataTable, DataSet, DataAdapter и DataReader.
DataTable представляет набор (collection) записей отдельной таблицы и в
этом отношении аналогичен Recordset. DataSet представляет набор объек-
тов DataTable, а также содержит отношения и ограничения, используемые
при связывании таблиц. На самом деле DataSet — это хранящаяся в памя-
ти реляционная структура данных со встроенной поддержкой XML (Ex-
tensible Markup Language).

Одна из основных особенностей объекта DataSet в том, что ему не извес-


тен источник данных, который использовался для его заполнения. Это
отсоединенный, автономный объект, который представляет некий набор
данных и может передаваться от компонента к компоненту через различ-
ные уровни многоуровневого приложения. Кроме того, DataSet можно се-
риализовать в поток данных XML, благодаря чему этот объект идеально
подходит для передачи данных между гетерогенными платформами. Объ-
ект DataAdapter используется ADO.NET для двухстороннего обмена дан-
ными между DataSet и нижележащим источником данных. DataAdapter
также предоставляет расширенные возможности в пакетном обновлении
данных — функциональность, которая ранее поддерживалась Recordset.

На рис. 1 показана полная объектная модель DataSet.

DataSet

Table Объект

I—| Column

Constraints
*—| Constraint |

4 Row

Relation
Рис. 1. Объектная модель DataSet

Провайдеры данных -NET


В ADO.NET используются так называемые провайдеры данных (Data
Providers) .NET. Они обеспечивают доступ к соответствующим источни-
12 Microsoft ADO.NET

кам данных и содержат четыре ключевых объекта (Connection, Command,


DataReader и DataAdapter). В настоящее время с ADO.NET поставляют-
ся два провайдера:

• SQL Server .NET Data Provider. Предназначен для работы с базами


данных Microsoft SQL Server 7.0 и более поздних версий. Оптимизиро-
ван для доступа к SQL Server и взаимодействует с ним напрямую по
«родному» протоколу передачи данных SQL Server.
Всегда пользуйтесь этим провайдером при работе с SQL Server 7.0 или
SQL Server 2000.
• OLE DB .NET Data Provider. Управляемый провайдер для источников
данных OLE DB. Немного уступает по эффективности SQL Server
.NET Data Provider, так как взаимодействует с базой данных через уро-
вень OLE DB. Имейте в виду, что этим провайдером не поддерживает-
ся провайдер OLE DB для ODBC (Open Database Connectivity). Для
источников данных ODBC используйте ODBC .NET Data Provider,
описанный ниже. Список провайдеров OLE DB, совместимых с
ADO.NET, см. по ссылке http://msdn.microsoft.com/library/en-us/
cpguidnf/html/cpconadonetproviders.asp.
Остальные провайдеры данных .NET в настоящее время находятся в
состоянии бета-тестирования.
• ODBC .NET Data Provider. В данный момент доступна для загрузки
первая бета-версия. Этот провайдер обеспечивает «родной» доступ к
ODBC-драйверам так же, как и OLE DB .NET Data Provider к «род-
ным» провайдерам OLE DB. Получить дополнительную информацию
об ODBC .NET и скачать бета-версию можно по ссылке http://msdn,
microsoft, com/downloads/default, asp? URL=/code/sample.asp?url=/
MSDN-FILES/027/001/668/msdncompositedoc.xml.
• Управляемый провайдер для считывания XML из SQL Server 2000.
XML for SQL Server Web update 2 (в настоящий момент проходит бета-
тестирование) включает, помимо всего прочего, управляемый провай-
дер, предназначенный специально для считывания XML из SQL Server
2000. Дополнительную информацию об этом обновлении см. по ссыл-
ке http://msdn.microson^com/code/default.asp?url=/code/sam
msdn-files/027/001/602/msdncompositedoc.xml.

Структура пространств имен


Типы (классы, структуры, перечислимые и т.д.), связанные с каждым из
провайдеров данных .NET, находятся в собственных пространствах имен.
• System.Data.SqlClient. Содержит типы SQL Server .NET Data Provider,
Руководство по архитектуре доступа к данным на платформе .NET 13

• System.Data.OleDb. Содержит типы OLE DB .NET Data Provider.


• System.Data.Оdbc. Содержит типы ODBC .NET Data Provider.
• System.Data. Содержит типы, независимые от провайдеров, например
DataSet H'DataTable.

Для каждого из провайдеров в его пространстве имен содержатся реали-


зации объектов Connection, Command, DataReader и DataAdapter. Имена
реализаций объектов из пространства имен SqlClient начинаются с пре-
фикса <<Sql», а имена реализаций из пространства имен OleDb — с префик-
са «OleDb». Например, реализация объекта Connection из пространства
имен SqlClient называется SqlConnection, тогда как ее эквивалент из
OleDb — OleDbConnection. Соответствующие реализации объекта Data-
Adapter называются SqlDataAdapter и OleDbDataAdapter.

Базовые принципы программирования


Если вы собираетесь работать с различными источниками данных и пла-
нируете переносить свой код с одного источника данных на другой, поду-
майте о реализации интерфейсов IDbConnection, IDbCommand, IData-
Reader и I Db Data Adapter, принадлежащих пространству имен System.Data.
Все реализации объектов Connection, Command, DataReader и DataAdap-
ter должны поддерживать эти интерфейсы.

Дополнительную информацию о разработке провайдеров данных .NET см.


по ссылке http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpcon-
implementingnetdataprovider.asp.

Рис. 2 иллюстрирует стек доступа ADO.NET к данным и взаимосвязь


ADO.NET с другими технологиями доступа к данным, в частности с ADO
и OLE DB. Кроме того, показаны два управляемых провайдера и основные
объекты, входящие в модель ADO.NET.

Дополнительную информацию об эволюции ADO в ADO.NET см. в статье


«Introducing ADO+: Data Access Services for the Microsoft .NET Frame-
work», опубликованной в номере «MSDN Magazine» за ноябрь 2000 г.

Сравнение хранимых процедур и операторов SQL


В подавляющем большинстве фрагментов кода, приводимых далее в этом
документе, используются объекты SqlCommand для вызова хранимых про-
цедур, выполняющих операции над базами данных. В некоторых случаях
вы не увидите объект SqlCommand, так как имя хранимой процедуры пе-
редается непосредственно объекту SqlDataAdapter. При этом на внутрен-
нем уровне все равно создается объект SqlCommand.
34 Microsoft ADO,NET

Неуправляемые клиенты
Управляемые клиенты .NET

AOO.NET

DataSet

QLfDB ЛЕТ Data Pimlder


E

SQL Server .NET DataReader


TDS

Oracle, SQL Server SQL Server


Access и др. версии 6.5
версии 7.0
и ниже
и выше

Рис. 2. Стек доступа к данным

Используйте хранимые процедуры, а не встраиваемые операторы SQL по


следующим причинам.
• Применение хранимых процедур обычно повышает производитель-
ность, так как база данных может оптимизировать план доступа к дан-
ным, используемым процедурой, и кэшировать эти данные для даль-
нейшего применения.
• Хранимые процедуры можно защищать индивидуально на уровне базы
данных. Клиенту можно выдать разрешения на выполнение хранимой
процедуры, не предоставляя разрешения на доступ к используемым
при этом таблицам.
• Хранимые процедуры проще в сопровождении, так как обычно легче
изменить хранимую процедуру, чем «жестко зашитый» в развертывае-
мый компонент оператор SQL.
Руководство по архитектуре доступа к данным на платформе -NET

• Хранимые процедуры создают дополнительный уровень абстракции от


нижележащей схемы базы данных. Клиенту хранимой процедуры не
требуется что-либо знать ни о деталях работы хранимой процедуры, ни
о схеме базы данных.
• Хранимые процедуры уменьшают сетевой трафик, так как в этом слу-
чае операторы SQL выполняются в пакетном режиме, а не путем пере-
дачи многократных запросов от клиента.

Свойства и аргументы конструкторов


Значения свойств объектов ADO.NET можно задавать напрямую или че-
рез аргументы конструктора. Так, следующие фрагменты кода функцио-
нально эквивалентны:

// Объект Command настраивается через аргументы конструктора


SqlCommand cmd = new SqlCommand( "SELECT - FROM PRODUCTS", conn );

// Предыдущая строка по функциональности эквивалентна следующим


// трем строкам, в которых свойства настраиваются явным образом
sqlCommand cmd = new SqlCommandO;
cmd.Connection = conn;
cmd.CommandText = "SELECT * FROM PRODUCTS";

С точки зрения производительности, разница между этими двумя подхо-


дами незначительна, так как присвоение и получение значений свойств
.NET-объектов выполняется гораздо эффективнее по сравнению с анало-
гичными операциями над СОМ-объектами,

Выбор в данном случае определяется личными предпочтениями и стилем


кодирования. Однако явное присвоение значений свойствам облегчает
понимание кода (особенно если вы не очень хорошо знакомы с объектной
моделью ADO.NET) и упрощает отладку.

Примечание Раньше разработчикам на Microsoft Visual Basic рекомендовалось


избегать создания объектов операторами вида Dim x As New. В СОМ такой код мог
привести к «короткому замыканию» в процессе создания СОМ-объекта, что вызы-
вало самые разнообразные ошибки. В .NET такой проблемы нет.

Управление соединениями с базами данных


Соединение с базой данных — критически важный, дорогостоящий и ог-
раниченный ресурс, особенно в многоуровневых Web-приложениях. Край-
не важно правильно управлять соединениями, так как от этого в значи-
тельной мере зависит общая масштабируемость приложения. Кроме того,
тщательно продумайте, где хранить строки подключения. Вы должны выб-
рать настраиваемое и безопасное место их хранения.
Microsoft ADO.NET

Управляя соединениями с базами данных и строками подключений, ста-


райтесь:

• обеспечивать масштабируемость приложений путем за счет совместного


использования пула соединений с базой данных несколькими клиентами;
• придерживаться стратегии конфигурируемого и высокопроизводи-
тельного пула соединений;
• использовать средства аутентификации Windows при доступе к SQL
Server;
• избегать олицетворения (impersonation) на промежуточном уровне;
• безопасно хранить строки подключений;
• открывать соединения с базой данных как можно позже, а закрывать —
как можно раньше.

В этом разделе рассказывается о создании пула соединений, что поможет


вам выбрать наиболее подходящую стратегию для пула соединений из
множества возможных вариантов. Здесь же рассматривается управление
строками подключения к базам данных, их хранение и администрирова-
ние. Наконец, представлено два шаблона кода, обеспечивающего гаранти-
рованное закрытие соединения и его возврат в пул.

Создание пула соединений


Создание пула соединений с базой данных позволяет приложению повтор-
но использовать существующие соединения из пула вместо многократного
установления новых. Этот прием может значительно повысить масштаби-
руемость приложения, так как ограниченное число соединений позволяет
обслуживать намного большее количество клиентов. Кроме того, попы-
тается производительность, поскольку вы избегаете значительных затрат
времени на установление новых соединений.

Такие технологии доступа к данным, как ODBC и OLE DB, поддержива-


ют свои разновидности пулов соединений, в той или иной мере допуска-
ющих конфигурирование. Оба подхода практически прозрачны для клиен-
тского приложения, работающего с базой данных. Пул соединений OLE
DB часто называют сеансовым (session pooling) или ресурсным (resource
pooling).

Общее описание создания пулов соединений в MDAC (Microsoft Data


Access Components) см. по ссылке http://msdn.microsoft.com/library/en-us/
dnmdac/html/pooling2.asp.

Провайдеры данных ADO. NET обеспечивают незаметное для пользовате-


ля создание пула соединений, точный механизм которого зависит от про-
Руководство по архитектуре доступа к данным на платформе .(ЧЕТ 17

вайдера. В этом разделе рассматривается создание пула соединений для


следующих провайдеров:

• SQ.L Server .NET Data Provider (http://msdn.microsoit.com/library/eri-


us/dnbda/html/#daag_sqlserverproviderpooling);
• OLE DB .NET Data Provider (http://msdn.microsoft.com/library/en-us/
dnbda/html/#daag_oledbproviderpooling).

Поддержка пула соединений


в SQL Server .NET Data Provider
Если Вы работаете с SQL Server .NET Data Provider, используйте поддер-
жку пулов соединений, предлагаемую провайдером. Это эффективный
механизм с поддержкой транзакций, реализуемый в управляемом коде са-
мого провайдера. Пулы создаются для каждого процесса отдельно и не
уничтожаются до завершения соответствующего процесса.

Этот вид пула соединений можно использовать прозрачно, но при этом


следует знать, как управлять пулами и как задействовать различные воз-
можности конфигурирования для более точной настройки пулов,

Настройка пула соединений


в SQL Server .NET Data Provider
Пул настраивается с помощью набора пар «имя-значение», указываемых
в строке подключения. Например, можно настроить, разрешено ли созда-
ние пула (по умолчанию — разрешено), указать максимальный и мини-
мальный размер пула, а также время ожидания запроса на открытие соеди-
нения, поставленного в очередь. В приведенном ниже примере показана
строка подключения, в которой заданы максимальный и минимальный
размеры пула.

"Server=(local); Integrated Security=SSPI; Database=Northwind;


Max Pool Slze=75; Min Pool Slze=5"

Когда соединение открыто и пул создан, в пул добавляется столько соеди-


нений, сколько нужно, чтобы довести их число до заданного минимума.
Впоследствии соединения могут добавляться в пул до тех пор, пока их
число не достигнет максимума, установленного при настройке. После это-
го новые запросы на открытие соединений ставятся в очередь, причем вре-
мя ожидания также настраивается.

Выбор размера пула


Возможность устанавливать максимальный предел очень важна для круп-
номасштабных систем, управляющих параллельными запросами многих
тысяч клиентов. Чтобы выяснить оптимальные размеры пула для вашей
IS Microsoft ADO.NET

системы, необходимо понаблюдать за ним и за производительностью при-


ложения. Оптимальный размер также зависит от аппаратных средств, на
которых работает SQL Server.

В процессе разработки вы, вероятно, захотите уменьшить заданный по


умолчанию максимальный размер пула (100), чтобы упростить поиск уте-
чек соединений.

Если минимальный размер пула задан, возможно небольшое падение про-


изводительности после первоначального заполнения пула до этого уров-
ня, хотя первые несколько подключившихся клиентов получат выигрыш.
Заметьте, что новые соединения создаются последовательно, т. е. сервер не
будет «завален» одновременными запросами при первоначальном запол-
нении пула.

Дополнительную информацию о наблюдении за пулами соединений см. в


разделе этого документа «Наблюдение за пулами соединений».

Полный список ключевых слов, используемых в строках подключений при


создании пула соединения, см. по ссылке http://msdn.microsoft.com/lib-
rary/en-us/cpguidnf/html/cpconconnectionpoolingforsqlservernetdataprovi-
der.asp.

Дополнительная информация
При использовании пула соединений в SQL Server .NET Data Provider
имейте в виду следующее.

• Соединения включаются в пул но алгоритму строгого соответствия


(exact match algorithm) строк подключения. Механизм поддержки
пула чувствителен даже к пробелам между парами «имя-значение».
Так, следующие две строки подключения приведут к созданию двух
разных пулов из-за того, что вторая строка содержит дополнительные
пробелы.
SqlConnection conn = new SqlConnection(
"Inteflrated Security=SSPI;Database=Northwind");
conn.0pen(); // создается пул А

SqlConmection conn = new SqlConnection(


"Integrated Security=SSPI ; Database=Northwlnd");
conn.0pen(); // создается пул В (строка содержит дополнительные пробелы)

• В бета-версиях .NET Framework пул соединений всегда отключается


при выполнении приложения под отладчиком. Без отладчика пул со-
здается и в отладочном (debug), и в финальном (release) вариантах
приложения. В RTM-версии (Released To Manufacture) .NET Frame-
work это ограничение снято, и пул создается во всех случаях.
Руководство по архитектуре доступа к данным на платформе .NET 19

• Пул соединений делится на несколько специфичных для конкретных


транзакций пулов и еще выделяется один пул для соединений, не
используемых транзакциями в данный момент. Для потоков, сопостав-
ленных с контекстом определенной транзакции, возвращается соеди-
нение из того пула, который включает соединения, участвующие в дан-
ной транзакции. Благодаря этому работа с соединениями, используе-
мыми транзакциями, становится транспарентной.

Поддержка пула соединений


в OLE DB .NET Data Provider
OLE DB .NET Data Provider поддерживает пулы соединений, обращаясь
к соответствующим сервисам механизма поддержки ресурсных пулов в
OLE DB. Настройка ресурсного пула возможна несколькими способами:

• использованием строк подключения для настройки, включения и от-


ключения поддержки ресурсного пула соединений;
• через реестр;
• программным конфигурированием ресурсного пула.

Во избежание проблем при развертывании, возникающих в случае на-


стройки пулов через реестр, не пользуйтесь этим способом настройки ре-
сурсного пула OLE DB.

Подробнее о создании ресурсного пула соединений OLE DB см. в MSDN


руководство «OLE DB Programmer's Reference» (глава 19 «OLE DB Ser-
vices», раздел «Resource Pooling»).

Управление пулами соединений


с помощью объектов пула
Как разработчик для Windows DNA, вы можете отключить создание ресур-
сного пула OLE DB и/или создание пула соединений ODBC и использо-
вать в качестве пула соединений к базе данных объектный пул СОМ+. На
то могут быть две основных причины:

• размеры пулов и пороговые значения можно настроить явным образом


(в СОМ+ Catalog);
• производительность пула объектов может быть в 2 раза больше произ-
водительности стандартного пула.

Однако, так как SQL Server .NET Data Provider работает с пулом соедине-
ний на внутреннем уровне, при использовании этого провайдера нет необ-
ходимости разрабатывать собственный объектный механизм поддержки
пула. Таким образом, вы избежите сложностей, связанных с включением
ресурсов в транзакции вручную (manual transaction enlistment).
20 Microsoft ADO.NET

Если вы используете OLE DB .NET Data Provider, а также хотите добить-


ся удобства настройки и повышения производительности, стоит подумать
о поддержке объектного пула СОМ+. Если в этих целях вы разрабатывае-
те объект пула (pooled object), отключите в OLE DB ресурсный пул и ав-
томатическое включение ресурсов в транзакции (например, указав в строке
подключения «OLE DB Services=-4»). В своей реализации объекта пула вам
придется самостоятельно управлять включением ресурсов в транзакции.

Наблюдение за пулами соединений


Для наблюдения за тем, как приложение работает с пулом соединений,
можно воспользоваться утилитой Profiler, поставляемой с SQL Server, или
оснасткой Performance Monitor в Microsoft Windows 2000.

Для наблюдения за пулом с помощью Profiler выполните следующие дей-


ствия.

1. Откройте Start Programs Microsoft SQL Server Profiler для запус-


ка Profiler.
2. Выберите File | New Trace.
3. Укажите параметры соединения и щелкните ОК.
4. В диалоговом окне Trace Properties перейдите на вкладку Events.
5. В списке Selected event classes убедитесь, что под Security Audit по-
казываются события Audit Login и Audit Logout. Чтобы не «засорять»
трассировочную информацию лишними сведениями, удалите из спис-
ка все остальные события.
6. Щелкните Run, чтобы начать трассировку. Вы увидите события Audit
Login при установлении соединений, а события Audit Logout — при
закрытии соединений.

Для наблюдения за пулом с помощью Performance Monitor выполните сле-


дующие действия.

1. Откройте Start Programs Administrative Tools | Performance для за-


пуска Performance Monitor.
2. Щелкните правой кнопкой мыши график, показываемый в окне, и вы-
берите Add Counters.
3. В списке Performance object укажите SQL Server: General Statistics.
4. В появившемся списке щелкните User Connections.
5. Щелкните Add, затем — Close.

Примечание RTM-версия .NET Framework предоставляет дополнительный набор


счетчиков производительности, позволяющих с помощью Performance Monitor на-
Руководство по архитектуре доступа к данным иа платформе .NET 21

блюдать и накапливать статистику использования пула соединений для SQL Server


.NET Data Provider.

Управление защитой
Хотя пул соединений повышает общую масштабируемость приложения,
он лишает вас возможности управлять защитой базы данных. Это объяс-
няется тем, что для поддержки пула нужно, чтобы все строки подключе-
ния были одинаковыми. Если вы хотите отслеживать операции над базой
данных, выполняемые каждым из пользователей, подумайте о добавлении
для каждой операции параметра, через который передается идентифика-
ция пользователя (имя и пароль); тем самым вы сможете вручную регист-
рировать действия, выполняемые пользователями в базе данных.

Аутентификация средствами Windows


При подключении к SQL Server используйте аутентификацию средствами
Windows, так как это дает ряд преимуществ.

• Упрощается управление защитой, поскольку вы имеете дело с одной


моделью защиты (предоставляемой Windows), не используя еще и мо-
дель защиты SQL Server.
• Имена и пароли пользователей не включаются в строки подключения.
• Имена и пароли пользователей не передаются по сети открытым текстом.
• Повышается безопасность входа в базу данных за счет поддержки сро-
ков действия паролей, минимальной длины пароля и блокировки учет-
ной записи после неоднократных неудачных попыток входа.

Дополнительная информация
При аутентификации доступа к SQL Server средствами Windows учиты-
вайте соображения, изложенные в следующих разделах.

Производительность
Тестирование производительности .NET Beta 2 показало, что при аутенти-
фикации средствами Windows на открытие соединения из пула уходит
больше времени, чем при аутентификации средствами SQL Server. Одна-
ко, хотя аутентификация Windows работает медленнее, снижение произ-
водительности незначительно по сравнению со временем, требуемым для
выполнения запроса или хранимой процедуры. В итоге преимущества
аутентификации Windows обычно перевешивают небольшое уменьшение
производительности.

Также следует ожидать, что различия в скорости аутентификации сред-


ствами Windows и средствами SQL Server при открытии входящих в пул
соединений станут менее заметными в RTM-версии .NET Framework.
22 Microsoft ADO.NET

Избегайте олицетворения на промежуточном уровне


Аутентификация через Windows требует для доступа к базе данных учет-
ную запись Windows. Хотя применение олицетворения на промежуточном
уровне (при подключении к SQL Server) может показаться логичным, вы
должны избегать этого, так как тогда теряет смысл поддержка пула соеди-
нений и резко снижается масштабируемость приложения.

Чтобы решить эту проблему, вместо обычных учетных записей, под кото-
рыми пользователи входят в Windows, используйте для подключения к
SQL Server ограниченный набор учетных записей Windows, каждая из
которых соответствует определенной роли.

Попробуйте, например, такой подход.

• Создайте две учетные записи Windows: одну — для операций чтения,


другую — для операций записи. (Или несколько учетных записей, ко-
торые отражают роли, определяемые логикой приложения. Например,
вы могли бы задействовать одну учетную запись для пользователей
Интернета, а другую — для пользователей, работающих только в ло-
кальной сети, и/или администраторов.)
• Сопоставьте каждую учетную запись с ролью, определенной в базе
данных SQL Server, и установите для каждой роли необходимые пра-
ва для доступа к базе данных.
• Перед выполнением какой-либо операции над базой данных опреде-
ляйте через прикладную логику своего уровня доступа к данным, ка-
кую учетную запись Windows следует использовать для доступа к SQL
Server.

Примечание Каждая учетная запись должна находиться в том же домене, что и


IIS (Internet Information Services) и SQL Server, или в доверяемых доменах; можно
также создать соответствующие учетные записи (с одними и теми же именем и
паролем) на каждом компьютере.

Используйте в качестве сетевой библиотеки TCP/IP


SQL Server 7.0 и более поздних версий поддерживает аутентификацию
средствами Windows для всех сетевых библиотек. Используйте TCP/IP,
чтобы добиться выигрыша в возможностях конфигурирования, произво-
дительности и масштабируемости. Более подробную информацию о при-
менении TCP/IP см. в разделе «Подключение через брандмауэры» далее
в этом документе.

Хранение строк подключения


Строки подключения можно хранить несколькими способами, различаю-
щимися по уровням гибкости и защиты. Хотя наилучшую производитель-
Руководство по архитектуре доступа к данным на платформе .NET

ность ооеспечивает «зашивка* строк подключения в исходный код, меха-


низм кэширования файловой системы гарантирует, что снижение произво-
дительности, связанное с хранением строк подключения вне приложения,
будет пренебрежимо мало. Кроме того, дополнительная гибкость при хра-
нении строк подключения вне кода приложения, позволяющая админист-
ратору изменять конфигурацию доступа к данным, практически во всех
случаях предпочтительнее небольшого выигрыша в производительности.

Выбор способа хранения строк подключения определяется двумя важней-


шими факторами: безопасностью и простотой настройки; следует принять
во внимание и чуть менее важный фактор — производительность.

Строки подключения к базе данных можно хранить в:

• файле конфигурации приложения (http://msdn.microsoft.com/library/


en-us/dnbda/html/#daag_usingxmlappconfigfiles), например в файле
Web.config Web-приложения ASP.NET;
• UDL-файле (Universal Data Link) (http://msdn.microsoft.com/Iibrary/
en-us/dnbda/html/#daag_usingudlfiles) — этот способ годится только
для OLE DB .NET Data Provider;
• реестре Windows (http://rnsdn.microsoft.com/library/en-us/dnbda/
html/#daag_usingwindows registry);
• собственном файле (http://msdn.microsoft.com/library/en-us/dnbda/
html/#daag_usingcustomfiles);
• COM+ Catalog (http://msdn.microsoft.com/library/en-us/dnbda/html/
#daag_usingcomplusconstructionstrings), используя строки инициали-
зации (только для обслуживаемых компонентов),

При доступе к SQL Server средствами аутентификации Windows можно


избежать хранения имен пользователей и паролей в строках подключения.
Если вам нужен более высокий уровень защиты, храните строки подклю-
чения в зашифрованном виде.

Безопасное и конфигурируемое решение для Web-приложений ASP.NET -


хранение строк подключения в зашифрованном виде в файле Web.config.

Примечание В строке подключения именованному значению Persist Security Info


можно присвоить false, чтобы параметры, связанные с безопасностью, например
пароль, не возвращались свойством ConnectionString объекта SqIConnection или
OleDbConnection.

В следующих разделах рассматривается, как использовать различные ва-


рианты хранения строк подключения, и показываются преимущества и
недостатки каждого подхода. Это позволит вам сделать обоснованный
выбор, отвечающий условиям работы вашего приложения.
24 Microsoft ADO.NET

Применение конфигурационных
XML-файлов приложения
Для хранения строк подключения к базе данных можно воспользоваться
элементом <appSettings> в разделе нестандартных параметров конфигура-
ционного файла приложения. В этом элементе могут присутствовать про-
извольные пары «имя-значение», как показано в приведенном ниже фраг-
менте кода.

<configuration>
<appSettings>
Odd key="DBConnStr"
value="server=(local);Integrated Security=SSPI;database=northwind"/>
</appSettings>
</configuratlon>

Примечание Элемент <appSettings> следует за элементом <configuration>, а не


непосредственно за <system.web>.

Преимущества
• Простота в распространении. Строка подключения передается вмес-
те с конфигурационными файлами с помощью обычного средства раз-
вертывания хсору, применяемого в .NET.
• Простота в программном доступе. Благодаря свойству AppSettings
класса ConfigurationSettings считать строку подключения к базе дан-
ных в период выполнения приложения очень легко.
• Поддержка динамического обновления (только в ASP.NET). Если
администратор обновляет строку подключения в файле Web.config,
данное изменение вступает в силу при следующем обращении к стро-
ке подключения — для компонента, не поддерживающего состояния
(stateless component), это произойдет скорее всего при следующем
обращении клиента к компоненту для выполнения запроса к базе
данных.

Недостатки
• Проблема с защитой. Хотя DLL интерфейса ISAPI (Internet Server
Application Programming Interface) ASP.NET не допускает прямого об-
ращения клиентов к файлам с расширением .config, а для еще больше-
го ограничения доступа можно использовать разрешения файловой
системы NTFS, вас все равно может не устроить хранение параметров
подключения в виде незашифрованного текста на Web-сервере, взаи-
модействующем с клиентами. Для большей безопасности храните стро-
ки подключения в конфигурационном файле в зашифрованном виде.
Руководство по архитектуре доступа к данным на платформе .NET

Дополнительная информация
• Нестандартные (custom), или пользовательские, параметры приложе-
ния можно считывать через статическое свойство АррSettings класса
System.Configuration.ConfigurationSettings. Это демонстрирует приве-
денный ниже фрагмент кода, где предполагается, что вы считываете
показанный ранее нестандартный ключ DBConnStr:
using System.Configuration;
private string GetDBaseConnectionStringO
{
return ConfigurationSettings.AppSettings["DBConnStr"];
)

• Дополнительную информацию о конфигурировании приложений .NET


Framework см. по ссылке http://msdn.microsoft.com/library/en-us/
cpguidnf/html/cpconconfiguringnetframeworkapplications.asp.

Применение UDL-файлов
OLE DB .NET Data Provider позволяет указывать в строках подключений
имена UDL-файлов (Universal Data Link). Вы можете передавать строку
подключения как аргумент конструктора объекта OleDbConnection или
присваивать ее свойству ConnectionString этого объекта,

Примечание SQL Server .NET Data Provider не поддерживает UDL-файлы в своих


строках подключения. Таким образом, этот способ годится только при работе с OLE
DB .NET Data Provider.

При работе с провайдером OLE DB для ссылки на UDL-файл используй-


те в строке подключения конструкцию «File Name=name.udl>>.

Преимущества
• Стандартный подход. Возможно, вы уже используете UDL-файлы для
управления строками подключения.

Недостатки
• Меньшая производительность. Строки подключения со ссылками на
UDL-файлы читаются и анализируются при каждом открытии соеди-
нения.
• Проблема защиты. UDL-файлы хранятся как простой текст. Вы може-
те защищать их с помощью разрешений NTFS, но тогда возникают те
же проблемы, что и с файлами .config.
• SqlClient не поддерживает UDL-файлы. Этот подход не поддержива-
ется SQL Server .NET Data Provider, применимым для доступа к SQL
Server версии 7.0 и выше.
26 Microsoft ADO.NET

Дополнительная информация
• Вы должны позаботиться, чтобы у администраторов был доступ к фай-
лу для чтения/записи, а у учетной записи, под которой запускается
приложение, — доступ только для чтения. Рабочий процесс Web-при-
ложения AS.P.NET по умолчанию выполняется под учетной записью
SYSTEM, но ее можно переопределить через элемент < process Model >
конфигурационного файла компьютера (Machine.config). Кроме того,
данную учетную запись можно подменить другой (тоже зарегистриро-
ванной в системе) через элемент <identity> файла Web.config.
• Работая с Web-приложениями, убедитесь, что DDL-файл не находит-
ся в виртуальном каталоге, — в ином случае появится потенциальная
возможность скачивания этого файла через Web.
• Дополнительную информацию об этих и других особенностях ASP.NET,
связанных с безопасностью, см. по ссылке http://msdn.rnicrosoft.com/
library /en-us/dnbda/html/authaspdotnet.asp.

Применение реестра Windows


Для хранения строки подключения можно создать свой раздел в реестре
Windows, но это не рекомендуется, так как тогда возникают проблемы с
развертыванием приложения.

Преимущества
• Безопасность. Доступ к определенным разделам реестра можно конт-
ролировать через списки управления доступом (access control lists,
ACL). Для большей безопасности используйте шифрование.
• Простота программного доступа. В .NET есть классы, позволяющие
читать строки из реестра.

Недостатки
• Проблемы с развертыванием. Соответствующие параметры реестра
придется распространять вместе с приложением, что не позволит в
полной мере использовать преимущества хсору.

Применение файла собственного формата


Строку подключения можно хранить в файле собственного формата. Но
этот способ не дает никаких преимуществ и поэтому не рекомендуется,

Преимущества
• Нет.
Руководство по архитектуре доступа к данным на платформе .NET 27

Недостатки
• Дополнительные усилия в программировании. Такой подход требует
дополнительных усилий в программировании и создает сложности в
поддержке одновременного доступа.
• Проблема развертывания. Файл придется копировать вместе с ос-
тальными файлами приложения ASP.NET. Не помещайте файл в ката-
лог приложения ASP.NET или его подкаталог, чтобы этот файл нельзя
было скачать через Web.

Использование СОМ+ Catalog


Строку подключения к базе данных можно хранить в СОМ+ Catalog —
тогда она будет автоматически передаваться вашему объекту через строку
инициализации объекта (object construction string). COM+ будет вызы-
вать метод Construct объекта сразу после создания экземпляра объекта,
передавая указанную строку инициализации.

Примечание Этот способ годится только для обслуживаемых компонентов (ser-


viced components). Его стоит рассматривать, только если ваши управляемые ком-
поненты используют другие сервисы, например поддержку распределенных тран-
закций или объектных пулов.

Преимущества
• Администрирование. Администратор может легко настраивать строку
подключения через оснастку Component Services консоли ММС.

Недостатки
• Проблемы с безопасностью. СОМ+ Catalog считается небезопасным
хранилищем данных (хотя доступ к нему можно ограничить с помо-
щью ролей СОМ+), поэтому помещать в него строки в виде незашиф-
рованного текста нельзя.
• Проблемы с развертыванием. Элементы СОМ+ Catalog должны рас-
пространяться вместе с .NET-приложением. Если вы используете дру-
гие сервисы масштаба предприятия, например распределенные тран-
закции или поддержку объектных пулов, то хранение строки подклю-
чения к базе данных в этом каталоге не приводит к дополнительным
издержкам при развертывании, поскольку тогда СОМ+ Catalog все
равно нужно развертывать для поддержки этих сервисов.
• Компоненты должны быть обслуживаемыми. Строки инициализации
можно использовать только для обслуживаемых компонентов. Не на-
следуйте класс своего компонента от ServicedComponent (что делает
компонент обслуживаемым) лишь для того, чтобы получить возмож-
ность работать со строками инициализации.
28 Microsoft ADO. NET

Дополнительная информация
• Подробнее о том, как настраивать .NET-класс на конструирование объ-
екта (object construction), см. в приложении «Как включить поддержку
конструирования объектов .NET-класса» (http://msdn.microsoft.com/
library /en-us/dnbda/html/#daag_howtoenableobjectconstruction).
• Подробнее о разработке обслуживаемых компонентов см. по ссылке
http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconwritmg-
servicedcomponents.asp.

Примеры использования соединений


Независимо от провайдера данных .NET вы должны всегда соблюдать сле-
дующие правила.

• Открывать соединение с базой данных как можно позже.


• Использовать это соединение в течение как можно более короткого пе-
риода.
• Закрывать его как можно быстрее. Соединение не возвращается в пул,
пока оно не закрыто вызовом метода Close или Dispose. Его также сле-
дует закрывать, если вы обнаружили, что оно разорвано. В последнем
случае соединение возврашается в пул и помечается как неработоспо-
собное. Компонент, управляющий пулом объектов (object pooler), пе-
риодически сканирует пул и ищет объекты, помеченные как неработос-
пособные.

Чтобы гарантированно закрывать соединение до того, как метод возвраща-


ет управление, используйте один из подходов, которые проиллюстрирова-
ны двумя примерами кода (см. ниже). В первом применяется блок finally,
а во втором — оператор using языка С#, обеспечивающий вызов метода
Dispose объекта.

В первом фрагменте кода соединение будет обязательно закрыто в блоке


finally. Заметьте, что этот способ работает не только в С#, но и в Visual
Basic .NET, так как последний тоже поддерживает структурную обработ-
ку исключений (structured exception handling, SEH).

public void DoSomeWorkO


{
SqlConnection conn = new SqlConnection{connectionString);
SqlCommand cmd == new SqlCommandC'CommandProc", conn );
cmd.CommandType = CommandType.StoredProcedure;

try

conn.0pen();
cmd . ExecuteNonQue ry( ) ;
Руководство по архитектуре доступа « данным на платформе .NET 29

catch (Exception e)

// Обрабатываем и протоколируем ошибку

finally

conn.CloseO;

Второй фрагмент кода иллюстрирует альтернативный подход с примене-


нием оператора using языка С#. Обратите .внимание, что в Visual Basic
.NET нет оператора using или его эквивалента.
public void DoSomeWorkC)

// using гарантирует, что для объекта conn будет вызван


// Dispose, и соединение будет закрыто.
using (SqlConnection conn = new SqlConnection(connectionString))

SqlCommand cmd = new SqlCommandC'CommandProc", conn);


fcmd.CommandType = CommandType.StoredProcedure;
conn.0pen();
cmd.ExecuteQue ry();

Этот подход применим и к другим объектам, например к SqlDataReader


или OleDbDataReader, которые нужно закрывать перед тем, как в рамках
текущего соединения делать что-то еще.

Обработка ошибок
Ошибки ADO.NET генерируются и обрабатываются через нижележащую
поддержку SEH — неотъемлемую часть .NET Framework. Благодаря это-
му ошибки при выполнениии кода доступа к данным обрабатываются точ-
но так же, как и ошибки, возникающие в любом другом месте приложения.
Исключения обнаруживаются и обрабатываются по стандартному для
.NET синтаксису и стандартными приемами.

В этом разделе показывается, как разрабатывать надежный код доступа к


данным, и объясняется, как обрабатывать соответствующие ошибки. Здесь
же вы найдете руководство по обработке исключений, специфичных для
SQL Server .NET Data Provider.

.NET-исключения
Провайдеры данных .NET транслируют ошибки, специфичные для баз
данных, в стандартные типы.исключений, которые вы должны обрабаты-
30 Microsoft ADO.NET

вать в коде доступа к данным. Параметры ошибки, относящиеся к базе


данных, доступны через свойства соответствующего объекта исключения.

Все типы .NET-исключений в конечном счете наследуются от базового


класса Exception в пространстве имен System. Провайдеры данных .NET
генерируют типы исключений, специфичные для конкретного провайдера.
Например, SQL Server .NET Data Provider генерирует объекты SqlExcep-
tion, когда в SQL Server возникает ошибочная ситуация. Аналогично OLE
DB .NET Data Provider генерирует исключения типа OleDbException с
параметрами, предоставленными нижележащим провайдером OLE DB.

На рис. 3 показана иерархия исключений, генерируемых провайдерами


данных .NET Заметьте, что класс OleDbException наследуется от External-
Exception — базового класса для всех исключений COM Interop. Свойство
ErrorCode этого объекта содержит COM HRESULT, сгенерированный
OLE DB.

Exception Exception

System Except! cm System Exception

SqlException External Except! on

CT OleDbException
SQL Server .NET OLEDB.I\
Data Provider Data Prov lei
Рис. З. Иерархия исключений провайдеров данных .NET

Перехват и обработка .NET-исключений


Для обработки исключений при доступе к данным поместите свой код
доступа к данным в блок try и перехватывайте генерируемые исключения
в блоках catch с соответствующими фильтрами. Так, в коде доступа к дан-
ным, работающем с SQL Server .NET Data Provider, следует перехватывать
исключения типа SqlException, как показано в коде, приведенном ниже.

try
{
// Код доступа к данным
\
catch (SqlException sqlex) // самое специфичное исключение
I
>
catch (Exception ex) // самое универсальное
// (наименее специфичное) исключение
'.
;
Руководство по архитектуре доступа к данным на платформе .NET 31

Если у вас несколько блоков catch с разными критериями фильтрации, не


забывайте располагать их в порядке от наиболее специфичных к наименее
специфичным (наиболее универсальным). Это гарантирует выполнение
самого специфичного блока catch.

Ряд свойств класса SqlException предоставляет дополнительную инфор-


мацию об исключительной ситуации;

• Message — текст, описывающий ошибку;


• Number — номер ошибки, уникально идентифицирующий ее тип;
• State — дополнительная информация о причине возникновения ошиб-
ки. Например, если в хранимой процедуре есть несколько строк, спо-
собных привести к одной и той же ошибке, свойство State позволяет
определить конкретное место, где она возникла;
• Errors — набор (collection) с подробными сведениями об ошибках, ге-
нерируемых SQL Server. В этот набор всегда входит минимум один
объект типа SqlError.

В следующем фрагменте кода показывается, как обрабатывать ошибочные


ситуации, возникающие в SQL Server при работе с SQL Server .NET Data
Provider.
using System.Data;
using System.Data.SqlCllent;
using System.Diagnostics;

// Метод, предоставляемый компонентом DAL (Data Access Layer)


public string GetProductNameC int ProductID )
{
SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=northwind");
// Весь код доступа к данным помещаем в блок try
try
I
conn.OpenO;
SqlCommand cmd = new SqlCommand("LookupProductName", conn );
cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("@ProductID", ProductID );
SqlParameter paramPN =
cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
paramPN.Direction = ParameterDirection.Output;

cmd.ExecuteNonQueryO;
// Блок finally выполняется до того, как метод возвращает управление
return paramPN.Value.ToStringO;
32 Microsoft ADO.NET

catch (SqlException sqlex)


{
// Обрабатываем исключение, возникшее при доступе к данным,
// и протоколируем сведения о нем
LogException(sqlex);
// Включаем текущее исключение в более подходящее внешнее
// и повторно генерируем исключение
throw new DALException(
"Unknown ProductID: " + ProductlO.ToStringO, sqlex );
}
catch (Exception ex)
{
// Обработка универсальных исключений. . .
throw ex;
}
finally
I
conn.Close(); // соединение будет закрыто в любом случае

// Вспомогательна» процедура, заносящая сведения из SqlException


// в журнал событий приложения
private void LogExceptionC SqlException sqlex )
{
EventLog el = new EventLogO;
el. Source = "CustomAppLog";
string strMessage;
strMessage = "Exception Number : " +• sqlex. Number +
"(" + sqlex. Message + ") has occurred";
el.WriteEntry< strMessage );

foreach (SqlError sqle in sqlex. Errors)


{
strMessage = "Message: " + sqle. Message +
" Number: " + sqle. Number •+
Procedure: " + sqle. Procedure +
" Server: " + sqle. Server +
" Source: " + sqle. Source +
" State: " + sqle. State +
" Severity: " + sqle. Class +
'" LineNumber: " + sqle, LineNumber;
el.Write£ntry( strMessage );
)
!

В блоке catch для SqlException сначала регистрируются параметры исклю-


чения с помощью вспомогательной функции LogException. В ней операто-
ром foreach перебираются специфичные для провайдера объекты из набо-
ра Errors, и полученная информация записывается в журнал ошибок. За-
Руководство по архитектуре доступа к данным иа платформе ,N£T 33

тем исключение SQL Server включается в исключение типа DALException,


имеющее больший смысл для методов, вызывающих GetProductName. Для
передачи этого исключения вызывающему методу используется ключевое
слово throw.

Дополнительная информация
• Полный список членов класса SqlException см. по ссылке http://
msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDataSqlClient-
Sql Except ion Members Topi c. asp.
• Дополнительные сведения о разработке собственных исключений,
протоколировании .NET-исключений и включении их в оболочки, а
также о различных подходах к передаче исключений см. по ссылке
http://msdn.microsoft.coni/library /default. asp?url=/library/en-us/dnb-
da/html/exceptdotnet.asp.

Генерация ошибок в хранимых процедурах


В Transact-SQL (T-SQL) имеется функция RAISERROR (обратите внима-
ние на регистр букв), позволяющая генерировать нестандартные ошибки
и возвращать их клиенту. В случае клиентов ADO.NET провайдер SQL
Server .NET Data Provider перехватывает эти ошибки и преобразует их в
объекты SqlError.

Самый простой способ вызова функции RAISERROR — передать в каче-


стве первого параметра текст сообщения, а затем задать параметры, опре-
деляющие уровень значимости ошибки (severity) и состояние:

RAISERROFK 'Unknown Product ID: X s ' , 16, 1, @ProductID )

Здесь параметр подстановки используется, чтобы вернуть в сообщении об


ошибке текущий идентификатор некоего продукта. Второй параметр -
уровень значимости, а третий — состояние сообщения.

Дополнительная информация
• Чтобы избежать «зашивки» текста сообщений в код, добавьте свои со-
общения в таблицу sysinessag'es, вызвав системную хранимую процеду-
ру sp_addmessage или воспользовавшись Enterprise Manager в SQL
Server. Тогда вы сможете ссылаться на нужное сообщение, передавая
его идентификатор функции RAISERROR. Идентификаторы ваших
сообщений должны быть больше 50 000;
RAISERROFU 50001, 16, 1, §ProductID )

• Полную информацию о функции RAISERROR ищите по предметному


указателю в SQL Server Books Online.

2-5947
Microsoft ADO.NET

Правильное использование уровней значимости


Тщательно выбирайте уровни значимости (severity levels) для определяе-
мых вами ошибок и учитывайте влияние каждого уровня на работу при-
ложения. Уровни значимости ошибок варьируются в диапазоне от 0 до 25
и указывают тип проблемы, с которой столкнулся SQL Server 2000. В кли-
ентском коде вы можете выяснить уровень значимости ошибки через свой-
ство Class объекта SqlError, принадлежащего набору Errors класса SqlEx-
ception. В табл. 1 показаны смысл различных уровней значимости и их
влияние на работу приложения.

Табл. 1. Уровни значимости ошибок: влияние и смысл

Уровень Закрывается ли Генерируется ли


значимости соединение SqlException Описание
10 и ниже Нет Нет Информационные сообщения,
не обязательно связанные
с ошибочными ситуациями
11-16 Нет Да Ошибки, которые могут быть
устранены пользователем,
например повторной попыт-
кой выполнения операции
с исправленными входными
данными
17-19 Нет Да Ошибки ресурсов
или системы
20-25 Да Да Фатальные системные ошибки
(в том числе аппаратные);
соединение с клиентом
завершается

Управление автоматическими транзакциями


При любых ошибках с уровнем значимости выше 10 провайдер SQL Server
.NET Data Provider генерирует SqlException. Если компонент участвует в
автоматической транзакции (транзакции СОМ+) и обнаруживает SqlEx-
ception, он должен «проголосовать* за отмену транзакции. Это может быть
сделано автоматически или вручную — в зависимости от того, помечен ли
метод атрибутом AutoComplete

Подробнее об обработке SqlException в контексте автоматических транзак-


ций см. в разделе «Определение результатов транзакций*- этого документа.

Получение информационных сообщений


Уровни значимости от 10 и ниже используются для передачи информаци-
онных сообщений и не вызывают генерации SqlException.
Руководство по архитектуре доступа к данным на платформе .N£T 35

Чтобы получать информационные сообщения, действуйте по следующей


схеме.
• Создайте обработчик события и подключите его к событию InfoMes-
sage объекта SqlConnection. Делегат этого события показан в следую-
щем фрагменте кода.
public delegate void SqlInfoMessageEventHandler( object sender,
SqllnfoMessageEventArgs e );

Данные сообщения доступны через объект SqllnfoMessageEventArgs, пере-


даваемый вашему обработчику события. У этого объекта имеется свойство
Errors, содержащее несколько объектов SqlError — по одному на каждое
информационное сообщение. Ниже показано, как зарегистрировать обра-
ботчик события, протоколирующий информационные сообщения.
public string GetProductName{ int ProductID )
(
SqlConnection conn = new SqlConnectionf
"serve r=(local); Integrated Security=SSPI;database=northwind");
try
i
// Регистрируем обработчик события для информационного сообщения
conn. InfoMessage += new SqlInfoMessageEventHandler(
MessageEventHandler };
conn.0pen();
// Настраиваем объект команды и выполняем его

catch (SqlException sqlex)


{
// Протоколируем и обрабатываем исключение

;•
finally
<
conn.CloseO;

// Обработчик события для информационного сообщения


void HessageEventHandler( object sender, SqllnfoMessageEventArgs e )
{
foreach( SqlError sqle in e. Errors )
i
// Протоколируем содержимое свойств SqlError
36 Microsoft ADO.NET

Производительность
В этом разделе рассматривается несколько типичных сценариев доступа к
данным, и для каждого из них дается подробное описание наиболее масш-
табируемого и производительного решения, реализуемого в виде кода, ко-
торый использует ADO.NET. Там, где это имеет смысл, сравниваются про-
изводительность, функциональность и трудоемкость разработки. Вот эти
сценарии.

• Чтение нескольких записей. Считывается набор результатов (result


set), затем полученные записи перебираются в цикле.
• Чтение одной записи. Считывается одна запись с заданным первич-
ным ключом.
• Чтение одного поля. Считывается одно поле (item) из заданной записи.
• Проверка существования элемента данных. Проверяется, есть ли за-
пись с заданным первичным ключом. Это не более чем разновидность
сценария чтения одного поля, при которой достаточно вернуть простое
булево значение.

Чтение нескольких записей


В данном случае нужно считать табличный набор данных и перебрать по-
лученные записи, чтобы выполнить определенную операцию. Например,
вам может понадобиться считать набор данных, обработать его в отсоеди-
ненном виде и передать клиентскому приложению в виде XML-докумен-
та (возможно, посредством Web-сервиса). Или такая ситуация: данные
нужно показать в виде HTML-таблицы.

Чтобы определиться с выбором наиболее подходящего способа доступа к


данным, выясните, что вам важнее: повышенная гибкость отсоединенного
объекта DataSet или «голая» производительность объекта SqlDataReader,
идеального для отображения данных в Web-приложениях электронной
коммерции между предприятием и потребителем (business-to consumer,
В2С). Эти два основных подхода показаны на рис. 4.

Примечание SqIDataAdapter, применяемый для заполнения DataSet, обращает-


ся к данным на внутреннем уровне через SqlDataReader.

Сравнение доступных вариантов


Выборка нескольких записей из источника данных возможна следующи-
ми способами.

• Использовать объект SqIDataAdapter для генерации DataSet или


DataTable.
Руководство по архитектуре доступа к данным на платформе ,NET 37

Обработка XML
XML-сериализаций
(Web-сервисы /
гетерогенные
платформы)

Гибкое связывание
с данными

Сьрна&ай с обработкой отсоединенного набора данных

Сч итывани е и в изуал ь ное представление дан н ы х

Рис. 4. Типичные сценарии считывания нескольких записей

• Задействовать SqlDataReader для создания потока данных только для


чтения в направлении только вперед.
• Применить XmlReader для создания потока XML-данных только для
чтения в направлении только вперед.

Выбор между SqlDataReader и DataSet/DataTable — это фактически выбор


между производительностью и функциональностью. SqlDataReader обес-
печивает оптимальную производительность, a DataSet — дополнительную
гибкость и функциональность.

Связывание с данными
Все эти три объекта могут выступать в качестве источников данных для
элементов управления, связываемых с данными (data-bound controls), но
DataSet и DataTable способны работать с более широкой группой элемен-
тов управления, чем SqlDataReader. Это объясняется тем, что DataSet и
DataTable реализуют интерфейс IListSource (возвращающий IList), тогда
как SqlDataReader реализует интерфейс lEnumerable. Некоторые элемен-
ты управления Windows Forms поддерживают связывание с данными, если
их источники реализуют IList,

Различие между объектами объясняется их разным предназначением,


DataSet (включающий в себя DataTable) — это мощная отсоединенная
структура, подходящая для работы как с Web-формами, так и с Windows
38 Microsoft ADO.NET

Forms. С другой стороны, класс чтения данных (data reader) оптимизиро-


ван для Web-приложений, требующих быстрого доступа к данным в на-
правлении только вперед.

Так что проверяйте требования к источникам данных тех элементов управ-


ления, которые вы собираетесь связывать с данными.

Передача данных между уровнями приложения


DataSet дает реляционное представление данных, с которым можно рабо-
тать и как с XML, а также позволяет передавать отсоединенную кзширо-
ванную копию данных между уровнями и компонентами приложения.
Однако SqlDataReader обеспечивает оптимальную производительность
из-за того, что не тратит память и время на создание DataSet. Запомните,
что создание объекта DataSet часто влечет за собой создание массы по-
добъектов вроде DataTable, DataRow и DataColumn и что объекты-наборы
служат контейнерами для этих подобъектов.

Применение DataSet
Используйте DataSet, заполняемый объектом SqlDataAdapter, если:

• вам нужен размещенный в памяти отсоединенный кэш данных, кото-


рый можно было бы передавать другим компонентам или уровням
приложения;
• вам требуется реляционное представление данных в памяти, обрабаты-
ваемых либо в формате XML, либо в другом формате;
• вы работаете с данными, получаемыми из нескольких источников дан-
ных, например из нескольких баз данных, таблиц или файлов;
• вы хотите изменить все или отдельные полученные записи, используя
механизм пакетного обновления, встроенный в SqlDataAdapter;
• вы хотите связать с данными элемент управления, который поддержи-
вает источник, реализующий IList.

Дополнительная информация
Используя SqlDataAdapter для генерации DataSet или DataTable. имейте
в виду следующее.

• Явно открывать или закрывать соединение с базой данных не требует-


ся. Метод Fill объекта SqlDataAdapter открывает соединение с базой
данных, а перед возвратом управления закрывает его. Если соединение
уже открыто, Fill оставляет его открытым.
• Если соединение нужно для каких-то других целей, откройте его перед
вызовом метода Fill. Тогда вы избежите лишних открытий/закрытий
соединения и получите выигрыш в производительности.
Руководство по архитектуре доступа к данным на платформе -NET 39

• Хотя один и тот же объект SqlCommand можно использовать повтор-


но, чтобы несколько раз выполнить одну и ту же команду (оператор
SQL или хранимую процедуру), не делайте этого для выполнения раз-
ных команд.
• Пример кода, показывающий, как с помощью Sql Data Adapter запол-
нить DataSet или DataTable, см. в приложении: «Использование Sql-
DataAdapter для чтения нескольких записей».

Применение SqIDataReader
Используйте SqIDataReader, получаемый при вызове метода ExecuteRea-
der объекта SqlCommand, если:

• вы имеете дело с большими объемами данных — слишком большими,


чтобы они могли уместиться в одном кэше;
• вам нужно уменьшить объем памяти, занимаемый приложением;
• вы хотите избежать издержек, связанных с созданием объектов при ис-
пользовании DataSet;
• вы хотите связать с данными элемент управления, который поддержи-
вает источник, реализующий интерфейс lEnumerable;
• вам нужно упростить и оптимизировать доступ к данным;
» вы считываете записи, содержащие поля с большими двоичными объ-
ектами (binary large objects, BLOB). SqIDataReader позволяет считы-
вать BLOB-данные с разбиением на удобные для обработки порции.
Подробнее об обработке BLOB см. в разделе этого документа «Работа
с большими двоичными объектами (BLOB)».

Дополнительная информация
При использовании SqIDataReader имейте в виду следующее.

• Соединение с базой данных остается открытым и не может использо-


ваться в других целях, пока активен класс чтения данных. Вызывайте
метод Close объекта SqIDataReader как можно быстрее.
• На одно соединение приходится только один класс чтения.
• Вы можете явно закр~ыть соединение, закончив работу с классом чте-
ния, или увязать срок существования соединения со сроком жизни
объекта SqIDataReader, передав методу ExecuteReader значение Сот-
mandBehavior.CloseConnection. Этот параметр означает, что соедине-
ние должно быть закрыто, как только будет закрыт SqIDataReader,
• При доступе к данным с помощью класса чтения используйте типизи-
рованные методы-аксессоры (typed accessor methods) (например, Get-
Int32 или GetString), если вам известен тип данных поля, — тогда со-
40 Microsoft ADO.NET

кращается число преобразований типов, выполняемых при чтении


данных поля.
• Чтобы избежать лишней передачи данных от сервера клиенту в том
случае, когда вы хотите закрыть класс чтения и отбросить оставшиеся
результаты, вызовите метод Cancel объекта команды перед вызовом
метода Close класса чтения. Вызов Cancel гарантирует, что результаты
будут отброшены на сервере и не попадут к клиенту. Вызов только
метода Close класса чтения данных, напротив, приведет к тому, что
оставшиеся результаты будут без всякой необходимости переданы с
сервера, чтобы опустошить поток данных.
• Если вам нужно получить выходные параметры хранимой процедуры
или ее возвращаемое значение и вы используете метод ExecuteReader
объекта SqlCommand, обязательно вызовите метод Close класса чте-
ния перед обращением к выходным параметра или возвращаемому
значению.
• Пример кода, показывающий, как работать с SqlDataReader, см. в прило-
жении «Использование SqlDataReader для чтения нескольких записей».

Применение XmlReader
Используйте XmlReader, получаемый при вызове метода ExecuteXml-
Reader объекта SqlCommand, в следующих случаях.

• Считываемые данные обрабатываются как XML, но нужно избежать


издержек, связанных с созданием DataSet, и нет необходимости в от-
соединенном кэше данных.
• Требуется функциональность блока FOR XML оператора SQL, позво-
ляющая гибко считывать из базы данных XML-фрагменты (т. е. XML-
документы без корневого элемента). Такой подход дает возможность,
например, указывать точные имена элементов независимо от того, надо
ли использовать схему, основанную на элементах или атрибутах (ele-
ment or attribute-centric schema), должна ли она возвращаться вместе
с XML-данными и т. д.

Дополнительная информация
Если вы работаете с XmlReader, учтите следующее.

• Соединение должно оставаться открытым в течение всего времени,


пока XmlReader считывает данные. Метод ExecuteXmlReader объекта
SqlCommand в настоящее время не поддерживает значение Command-
Behavior.CloseConnection, поэтому вы должны явным образом закры-
вать соединение, закончив работу с классом чтения.
Руководство по архитектуре доступа н данным на платформе .NET 41

• Пример кода, в котором показывается, как использовать XmlReader,


см. в приложении «Использование XmlReader для чтения нескольких
записей».

Чтение одной записи


В этом сценарии вы считываете из источника данных одну запись, содер-
жащую заданный набор полей. Например, у вас есть идентификатор кли-
ента и вы хотите получить какие-либо сведения об этом клиенте, или же
у вас имеется идентификатор продукта и вам нужно получить информа-
цию об этом продукте.

Сравнение возможных вариантов


Если требуется связать данные с одной записью, считываемой из источни-
ка данных, можно использовать Sql Data Adapter для заполнения DataSet
или DataTable точно так же, как и при считывании нескольких записей. Но
если функциональность DataSet/DataTable не нужна, избегайте создания
этих объектов.

Считывание одной записи возможно следующими способами:

• использование выходных параметров хранимой процедуры;


• применение объекта SqlDataReader.

Оба варианта позволяют обойтись без ненужных издержек, связанных с


созданием набора результатов на сервере и DataSet на клиенте. Относи-
тельная производительность при этих вариантах зависит от уровня нагруз-
ки и от того, используется ли пул соединений с базой данных. Как пока-
зало тестирование, при наличии пула соединений и в условиях высокой
нагрузки (более 200 одновременных подключений) хранимая процедура
работает примерно на 30% быстрее, чем SqlDataReader.

Использование выходных параметров


хранимой процедуры
Используйте выходные параметры хранимой процедуры, когда:

• нужно считать одну запись из многоуровневого Web-приложения, в


котором включена поддержка пула соединений.

Дополнительная информация
• Пример кода, где показывается, как использовать выходные парамет-
ры хранимой процедуры, см. в приложении: «Использование выход-
ных параметров хранимой процедуры для чтения одной записи».
42 Microsoft ADO.NET

Применение SqIDataReader
Используйте SqIDataReader, когда:

• требуется считать не только значения данных, но и метаданные. Для


получения метаданных поля вызывайте метод GetSchemaTable класса
чтения данных;
• пул соединений не используется. Тогда SqIDataReader является хоро-
шим выбором независимо от нагрузки; тестирование производительно-
сти показало, что при 200 подключенных браузерах SqIDataReader
обеспечивает примерно на 20% более высокую производительность,
чем хранимая процедура.

Дополнительная информация
• Если известно, что запрос вернет только одну запись, то при вызове
метода ExecuteReader объекта SqlCommand указывайте значение Coni-
mandBehavior.SingleRow. Некоторые провайдеры, например OLE DB
.NET Data Provider, используют это значение для оптимизации произ-
водительности. Этот провайдер при задании CommandBehavior.Single-
Row выполняет связывание с данными через интерфейс IRow (если он
доступен), а не через более ресурсоемкий IRowset. При работе с SQL
Server .NET Data Provider этот аргумент ни на что не влияет.
• Используя объект SqIDataReader. всегда считывайте выходные пара-
метры типизированными методами-аксессорами объекта SqIDataRea-
der, например GetString или GetDecimal. Это позволит избежать лиш-
них преобразований типов.
• Пример кода, в котором показывается, как с помощью объекта SqI-
DataReader считывать одну запись, см. в приложении «Использование
SqIDataReader для чтения одной записи».

Чтение одного поля


В этом случае требуется считать одно поле данных. Например, может по-
требоваться найти наименование одного продукта по идентификатору или
оценку кредитоспособности (credit rating) одного клиента по его имени.
Тогда ради чтения одного поля обычно нет необходимости идти на издер-
жки, связанные с применением DataSet или даже DataTable.

Кроме того, вам может понадобиться просто проверить, есть ли в базе дан-
ных определенная: запись. Например, когда на Web-сайте регистрируется
новый пользователь, вы должны проверить, нет ли в базе указанного им
имени. Это частный случай чтения одного поля. Здесь достаточно вернуть
булево значение.
Руководство по архитектуре доступа к данным на платформе .NET 43

Сравнение возможных вариантов


Считать одно поля из источника данных можно:

• вызовом метода Execute Scalar объекта SqlCommand, содержащего хра-


нимую процедуру;
• использованием выходного параметра или возвращаемого значения
хранимой процедуры;
• через объект SqlDataReader.

Метод ExecuteScalar возвращает непосредственно поле данных, так как он


рассчитан на запросы, возвращающие единственное значение. Этот способ
требует меньше кода, чем остальные.

С точки зрения производительности, следует использовать выходной па-


раметр или возвращаемое значение хранимой процедуры, поскольку тес-
ты показали, что применение хранимых процедур обеспечивает примерно
одинаковую производительность в условиях как с высокой, так и с низкой
нагрузкой (от менее чем 100 подключенных браузеров до 200).

Дополнительная информация
• Если запрос, выполняемый через ExecuteQuery, возвращает несколько
полей и/или записей, то способы, рассмотренные в этом подразделе, —
только первое поле первой записи,
• Пример кода, в котором показывается, как работать с ExecuteScalar, см.
в приложении «Использование ExecuteScalar для чтения одного поля».
• Пример кода, иллюстрирующий, как считывать одно поле через выход-
ной параметр или возвращаемое значение хранимой процедуры, см. в
приложении «Использование выходного параметра или возвращаемо-
го значения хранимой процедуры для чтения одного поля»,
• Пример кода, демонстрирующий, как с помощью объекта SqlData-
Reader считать одно поле, см. в приложении «Использование SqlData-
Reader для чтения одного поля*-.

Подключение через брандмауэры


Интернет-приложения часто приходится настраивать на подключение к
SQL Server через брандмауэр (firewall). Так, ключевым элементом архи-
тектуры многих Web-приложений и их брандмауэров является сеть пери-
метра (perimeter network) (также называемая DMZ, или демилитаризован-
ной зоной), используемая для изоляции Web-серверов, взаимодействую-
щих с клиентами, от внутренних сетей.
44 Microsoft ADO.NET

При подключении к SQL Server через брандмауэр требуется специальная


настройка брандмауэра, клиента и сервера. В составе SQL Server постав-
ляются программы Client Network Utility и Server Network Utility, помо-
гающие выполнить эту настройку.

Выбор сетевой библиотеки


Для упрощения настройки при подключении к SQL Server через брандма-
уэр используйте сетевую библиотеку TCP/IP. При установке SQL Server
2000 она устанавливается по умолчанию. Если вы работаете с одной из
предыдущих версий SQL Server, убедитесь, что TCP/IP выбран в качестве
сетевой библиотеки по умолчанию и на клиенте, и на сервере. Это можно
сделать с помощью Client Network Utility и Server Network Utility соответ-
ственно.

Помимо удобства конфигурирования, библиотека TCP/IP обеспечивает


следующие преимущества:

• повышенную производительность при работе с большими объемами


данных и более высокую масштабируемость;
• исключение дополнительных проблем с безопасностью, возникающих
при работе с именованными каналами.

Клиентские и серверные компьютеры нужно настроить под TCP/IP. По-


скольку большинство брандмауэров ограничивает набор портов, через ко-
торые пропускается трафик, тщательно проанализируйте, какие порты
использует SQL Server.

Конфигурирование сервера
По умолчанию экземпляры SQL Server прослушивают порт 1433. Однако
именованным экземплярам (named instances) SQL Server 2000 номер пор-
та назначается динамически при первом запуске. Администратор вашей
сети скорее всего не захочет открывать диапазон номеров портов на бран-
дмауэре, поэтому, если вы используете именованный экземпляр SQL
Server в сети с брандмауэром, настройте с помощью Server Network Utility
свой экземпляр на прослушивание определенного порта. Тогда админист-
ратор сети настроит брандмауэр так, чтобы тот пропускал трафик на за-
данный IP-адрес и порт, прослушиваемый экземпляром сервера.

Примечание Исходный порт, используемый сетевой библиотекой на клиенте,


динамически выбирается из диапазона 1024-5000. Это типично для клиентских
приложений TCP/IP, но означает, что брандмауэр должен разрешать трафик с лю-
бого порта, относящегося к этому диапазону. Подробнее о портах, используемых
SQL Server, см. на сайте Microsoft Product Support Services статью «INF: TCP Ports
Needed for Communication to SQL Server Through a Firewall» (http://msdn.micro-
soft.com/isapi/gosu ppoit.asp?Target=/support/kb/articles/Q287/9/32.ASP).
Руководство по архитектуре доступа к данные на платформе .NET 45

Динамическое обнаружение именованных экземпляров


Если вы изменили номер порта, прослушиваемого SQL Server по умолча-
нию, настройте клиент на подключение к этому порту. Детали см. в разде-
ле «Конфигурирование клиента».

Если вы изменили номер порта для экземпляра SQL Server 2000, исполь-
зуемого по умолчанию, учтите, что неудачное изменение конфигурации
клиента приведет к ошибке соединения. Если у вас несколько экземпля-
ров SQL Server, используйте последнюю версию стека доступа к данным
MDAC (версию 2.6) для динамического распознавания и согласования по
протоколу UDP (User Datagram Protocol) через UDP-порт 1434. Хотя в
среде разработки все это может функционировать нормально, вы должны
понимать, что на практике брандмауэры обычно блокируют трафик согла-
сования по протоколу UDP.

Чтобы обойти эту ситуацию, всегда настраивайте клиент на подключение


к выделенному для него порту на сервере.

Конфигурирование клиента
Для подключения к SQL Server настройте клиент на использование сете-
вой библиотеки TCP/IP. Кроме того, убедитесь, что эта библиотека на
клиентской стороне использует правильный порт назначения.

Использование сетевой библиотеки TCP/IP


Клиент можно настроить с помощью Client Network Utility, поставляемой
с SQL Server. В некоторых вариантах установки эта утилита может отсут-
ствовать на клиенте. Тогда настройте клиент на использование библиоте-
ки TCP/IP одним из следующих способов.

• Укажите в строке подключения пару «имя-значение» («Network Lib-


rary=dbmssocn»). Строка dbmssocn нужна для идентификации TCP/
1Р-сокетов.
Примечание В случае провайдера SQL Server .NET Data Provider параметр сете-
вой библиотеки dbmssocn используется по умолчанию.

• Внесите изменения в реестр клиента, чтобы задать TCP/IP как биб-


лиотеку по умолчанию. Дополнительную информацию о конфигури-
ровании сетевой библиотеки SQL Server см. в «HOWTO: Change SQL
Server Default Network Library Without Using Client Network Utility
(Q250550)» (http://msdn.microsoft.com/isapi/gosupport.asp?Target=/
support/kb/articles/Q250/5/50.ASP).
46 Microsoft A.DO.NET

Задание порта
Если ваш экземпляр SQL Server настроен на прослушивание порта, отлич-
ного от порта 1433 по умолчанию, вы можете указать номер порта для под-
ключения следующими способами:

• через Client Network Utility;


• задав номер порта в паре «имя-значение» («Server» или «Data Sour-
ce»), указываемой в строке подключения. Используйте строку следу-
го шего формата:
"Data Source=ServerName, PortNumber"

Примечание ServerName может быть IP-адресом или DNS-именем. Для опти-


мальной производительности указывайте IP-адрес, чтобы не тратить время на про-
смотр DNS.

Распределенные транзакции
Если вы занимаетесь разработкой обслуживаемых компонентов, использу-
ющих распределенные транзакции СОМ+ и сервисные функции Microsoft
DTC (Distributed Transaction Coordinator), вам может понадобиться на-
стройка брандмауэра на пропуск трафика DTC между разными экземпля-
рами DTC и между DTC и диспетчерами ресурсов (например, SQL Server).

Дополнительную информацию об открытии портов для DTC см. в «INFO:


Configuring Microsoft Distributed Transaction Coordinator (DTC) to Work
Through a Firewall» (http://msdn.microsoft.com/isapi/gosupport.asp7Tar-
get=/support/kb/articles/Q250/3/67.ASP).

Работа с большими двоичными объектами (BLOB)


В настоящее время многим приложениям приходится иметь дело не толь-
ко с традиционными символьными и числовыми данными, но и с такими
форматами данных, как графика и звук, и даже с еще более сложными ти-
пами данных вроде видео. Существует масса разных форматов графики,
звука и видео. Однако, с точки зрения хранения, всю информацию таких
форматов можно рассматривать как крупные массивы двоичных данных,
обычно называемые большими двоичными объектами (Binary Large Ob-
jects, BLOB).

В SQL Server для хранения BLOB предназначены типы данных binary,


varbinary и image. Несмотря на свое название BLOB-данные используют-
ся для хранения и текстовой информации. Например, вам может потребо-
ваться хранить длинные примечания произвольного размера, относящие-
ся к определенным записям. Для этих целей предназначены типы данных
SQL Server ntext и text.
Руководство по архитектуре доступа к данным на платформе ,NET

Вообще говоря, для хранения двоичных данных с размером меньше 8 Кб


лучше применять тип данных varbinary, а для хранения двоичных данных
большего размера — тип image. В табл. 2 перечислены характеристики
каждого из типов данных,

Табл. 2. Характеристики типов данных

Тип данных Размер Описание

binary От 1 до 8000 байтов. При хранении Двоичные данные


отводится указанный размер фиксированного размера
плюс 4 байта.
varbinary От 1 до 8000 байтов. При хранении Дноичные данные
отводится реальный размер данных переменного размера
плюс 4 байта.
image Двоичные данные переменного Большой объем двоичных
размера от 0 до 2 Гб. данных переменного размера
text Данные переменного размера Символьные данные
от 0 до 2 Гб.
ntext Данные переменного размера Символьные данные
от 0 до 2 Гб. в кодировке Unicode

Где хранить BLOB


SQL Server версии 7.0 и выше обеспечивает повышенную производитель-
ность при работе с BLOB, которые содержатся в базе данных. Одна из при-
чин — увеличение размера страницы базы данных до 8 Кб. Благодаря это-
му отпадает необходимость хранить текстовые или графические данные
размером меньше 8 Кб в отдельной двоичной древовидной структуре стра-
ниц. Такие данные теперь можно хранить в одной записи. Таким образом,
чтение/запись данных text, ntext или image выполняется так же быстро,
как и чтение/запись символьных или двоичных строк. При превышении
размера 8 Кб в запись помещается указатель, а сами данные приходится
размещать в узлах древовидной структуры страниц, что неизбежно снижа-
ет производительность.

Дополнительную информацию о том, как добиться, чтобы данные типов


text, ntext и image хранились в одной записи, см. в разделе «Using Text and
Image Data» в SQL Server Books Online.

Широко используемый альтернативный подход к работе с BLOB-данными


заключается в том, что они помещаются в файловую систему, а в поле базы
данных хранится указатель на соответствующий файл (лучше всего подходит
URL-ссылка). В версиях SQL Server до 7.0 хранение BLOB-данных в файло-
вой системе — вне базы данных — может повысить производительность.
48 Microsoft ADO.NET

Однако благодаря усовершенствованной поддержке BLOB в SQL Server


2000 в сочетании с поддержкой чтения и записи BLOB в ADO.NET пред-
почтительным подходом является хранение BLOB в самой базе данных.

Преимущества хранения BLOB в базе данных


Хранение BLOB в базе данных дает следующие преимущества.

• Проще синхронизировать BLOB-данные с остальными полями записи.


• Резервное копирование BLOB выполняется вместе с резервным копи-
рованием всей базы данных. Наличие единственной системы хранения
упрощает администрирование.
• Вы можете обращаться к BLOB через XML, поддерживаемый SQL
Server 2000. При этом возвращается закодированное в формате base 64
представление данных в потоке XML.
• Над полями, содержащими символьные данные постоянной или пере-
менной длины (в том числе в формате Unicode), можно выполнять
операции полнотекстового поиска (Full Text Search, FTS) SQL Server.
Кроме того, можно выполнять FTS-операции над форматированными
текстовыми данными, содержащимися в полях типа image, например
над документами Microsoft Word или Microsoft Excel.

Запись BLOB в базу данных


В следующем фрагменте кода показывается, как использовать ADO.NET
для записи двоичных данных, считываемых из файла, в поле SQL Server
типа image.
public void StorePicture( string filename )
{
// Считываем файл в байтовый массив
FileStream fs = new FileStream(
filename, FileMode.Open, FileAccess.Read );
byte[] imageData = new Byte[fs.Length];
fs.Read( imageOata, 0, (int)fs.Length );
fs.Closef);

SqlConnection conn = new SqlConnectionC'");


SqlCommand cmd = new SqlCommand("StorePicture", conn);
cmd.CoinmandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@filename", filename );
cmd.Parameters["©filename"].Direction = ParameterDirection.Input;
cmd.Parameters.AcJd("@blobdata", SqlDbType. Image);
cmd.Parameter$["@blobdata"]. Direction = ParafneterDirection.Input;
// Записываем байтовый массив е поле типа image
cmd.Pararoeters["@blobdata"].Value = imageData;
try
Руководство по архитектуре доступа к данным на платформе ,NET 49

conn.OpenO;
cmd.ExecuteNonQueryO;

catch

throw;

finally

conn.CloseO;

Чтение BLOB из базы данных


При создании объекта SqlDataReader методом ExecuteReader для чтения
записей, содержащих BLOB, указывайте значение CommandBehavior.Se-
quentialAccess. Без этого значения класс чтения будет передавать данные
с сервера на клиент по одной записи единовременно. Если запись содер-
жит поле BLOB, это может привести к расходу большого объема памяти.
Значение CommandBehavior.SequentialAccess обеспечивает более тонкий
контроль над чтением, так как BLOB-данные извлекаются только при
ссылке на них (например, при вызове метода GetBytes, позволяющего за-
давать число считываемых байтов). Все это показано в следующем фраг-
менте кода.
// Предполагается, что команда и соединение уже подготовлены,
// Эта команда извлекает из таблицы оператором SELECT поле типа IMAGE.
conn.0pen();
SqlDataReader reader = cmd.ExecuteReader(
CommandBehavior.SequentialAccess);
reader. ReadO;
// Получаем размер данных поля типа image;
// в качестве параметра - массива байтов передаем null
long bytesize = reader.GetBytes(Q, 0, null, 0, 0);
// Выделяем память под массив байтов, предназначенный
// для хранения данных поля
byte[] imageData = new byte[bytesize];
long bytesread = 0;
tnt curpos = 0;
while (bytesread < bytesize)
f
// chunkSize - произвольное значение, определяемое приложением
bytesreab += reader.GetBytes(0, curpos, imageData, curpos, chunkSize};
curpos += chunkSize;
}
// Теперь байтовый массив imageData содержит поле BLOB
50 Microsoft ADO.NET

Примечание Применение CommandBehavior.SequentialAccess требует строго


последовательного обращения к полям. Так, если BLOB — это поле 3, а данные
полей 1 и 2 вам тоже нужны, то перед чтением поля 3 вы должны считать поля 1 и 2.

Транзакции
Практически всем коммерческим приложениям, изменяющим данные,
нужна поддержка транзакций. Транзакции гарантируют целостность со-
стояния системы в рамках одного или нескольких источников данных. Это
реализуется за счет общеизвестных свойств транзакции ACID: атомарно-
сти (atomicity), целостности (consistency), изоляции (isolation) и отказоу-
стойчивости (durability).

Рассмотрим, например, Web-приложение для автоматизации розничных


продаж, обрабатывающее заказы на товары. При поступлении каждого за-
каза выполняются три операции, вносящие три изменения в базу данных:

• остаток товара уменьшается в соответствии с заказанным количеством;


• кредит покупателя уменьшается на сумму заказа;
• в базу данных заказов добавляется новый заказ.

Эти три операции должны выполняться атомарно — как единое целое.


Возможны два варианта: либо все операции выполняются успешно, либо
ни одна из них не выполняется. Любой другой вариант означал бы нару-
шение целостности данных. Транзакции гарантируют соблюдение принци-
па «все или ничего» и ряд других возможностей.

Дополнительную информацию по основам обработки транзакций см. по


ссылке http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpcontran-
sactionprocessingfundamentals.asp.

Существует несколько подходов, применимых для включения механизма


управления транзакциями в код доступа к данным. Каждый из этих под-
ходов укладывается в одну из двух основных моделей программирования.

• Транзакции вручную. Код, использующий поддержку транзакций


ADO.NET или Transact-SQL, пишется соответственно или в компонен-
тах, или в хранимых процедурах.
• Автоматические транзакции (транзакции СОМ+). Вы добавляете в
.NET-классы декларативные атрибуты (declarative attributes), в кото-
рых указываете требования объектов к транзакциям в период выпол-
нения. Эта модель позволяет легко настроить несколько компонентов
на работу в рамках одной и той же транзакции.
Руководство по архитектуре доступа к данным на платформе .NET 51

Оба способа годятся как для локальных транзакций (выполняемых одним


диспетчером ресурсов, например SQL Server 2000), так и для распределен-
ных (выполняемых несколькими диспетчерами ресурсов, размещенными
на удаленных компьютерах). Однако модель автоматических транзакций
значительно упрощает обработку распределенных транзакций.

Автоматические транзакции (транзакции СОМ+) могут привлечь ваше


внимание более простой моделью программирования. Особенно очевидно
это преимущество в системах с многочисленными компонентами, модифи-
цирующими содержимое базы данных. Но во многих ситуациях эта модель
приведет к дополнительным издержкам и к снижению производительности.

В этом разделе даются рекомендации, которые помогут выбрать наиболее


подходящую для вашего приложения модель транзакций.

Выбор модели транзакций


Прежде чем выбирать модель транзакций, определитесь, а нужны ли вам
вообще транзакции. Транзакции — один из самых дорогостоящих ресур-
сов, потребляемых серверными приложениями, и их применение без необ-
ходимости снижает масштабируемость приложения. Управляя транзакци-
ями, придерживайтесь следующих правил.

• Используйте транзакции, только когда вам нужны блокировки для вы-


полнения группы операций и введение в действие правил ACID.
• Выполняйте транзакции как можно быстрее, чтобы свести к миниму-
му время, в течение которого в базе данных действуют блокировки.
» Никогда не возлагайте на клиента управление сроком действия тран-
закции.
• Не используйте транзакции для отдельных SQL-операторов. SQL
Server автоматически выполняет каждый оператор в рамках индивиду-
альной транзакции.

Автоматические транзакции и транзакции вручную


Хотя автоматические транзакции несколько упрощают модель программи-
рования — особенно, когда изменения в базе данных выполняются не-
сколькими компонентами, — локальные транзакции вручную всегда зна-
чительно быстрее, так как не требуют взаимодействия с Microsoft DTC.
Это верно (хотя и в меньшей степени), даже если автоматические транзак-
ции выполняются с одним локальным диспетчером ресурсов (например,
SQL Server), так как при локальных транзакциях вручную не требуется
межпроцессного взаимодействия (interprocess communication, IPC) с DTC,
52 Microsoft ADO.NET

Используйте ручные транзакции, когда:

• транзакции выполняются над одной базой данных.

Используйте автоматические транзакции, когда:

• одна транзакция охватывает несколько удаленных баз данных;


• одна транзакция требует участия нескольких диспетчеров ресурсов,
например базы данных и ресурсов MSMQ (Message Queuing) в Win-
dows 2000.
Примечание Не смешивайте модели транзакций. Используйте либо одну модель,
либо другую.

В приложениях, производительность которых вас устраивает, есть смысл


выбирать автоматические транзакции (даже над одной базой данных) для
упрощения модели программирования. Автоматические транзакции упро-
щают выполнение несколькими компонентами операций, являющихся ча-
стями одной транзакции.

Применение транзакций вручную


При применении транзакций вручную код, использующий поддержку
транзакций ADO.NET или Transact-SQL, пишется соответственно в ком-
понентах или хранимых процедурах. В большинстве случаев следует вы-
бирать управление транзакциями в хранимых процедурах, поскольку та-
кой подход обеспечивает инкапсуляцию и дает производительность, срав-
нимую с производительностью транзакций ADO.NET.

Выполнение транзакций вручную в ADO.NET


ADO.NET подерживает объект транзакции, который можно использовать,
чтобы начать новую транзакцию, а затем зафиксировать (commit) или от-
катить (roll back) ее. Объект транзакции сопоставляется с соединением с
базой данных; для получения этого объекта вызывается метод BeginTran-
saction объекта соединения. Вызов данного метода не означает, что после-
дующие команды начнут неявно выполняться в контексте транзакции. Вы
должны явно связать каждую команду с транзакцией, установив свойство
Transaction команды. С объектом транзакции можно связать несколько
команд, тем самым объединив несколько операций над базой данных в
одну транзакцию.

Пример использования поддержки транзакций в ADO.NET см. в приложе-


нии «Программирование ручных транзакций ADO.NET».
Руководство по архитектуре доступа к данным на платформе .NET 53

Дополнительная информация
• По умолчанию в ручных транзакциях ADO.NET используется уровень
изоляции Read Committed. Это означает, что в базе данных на время
чтения из нее устанавливаются разделяемые блокировки (shared locks),
но данные можно изменять до завершения транзакции. При таком
уровне изоляции возможно чтение одной и той же записи по-разному
(non-repeatable reads), или появление фантомным данных (phantom
data). Уровень изоляции транзакции можно изменить, присвоив свой-
ству IsolationLevel ее объекта одно из значений перечислимого Isola-
tionLevel.
• Тщательно выбирайте подходящий уровень изоляции транзакций,
Здесь приходится идти на компромисс между целостностью данных и
производительностью. Самый высокий уровень изоляции (Serialized)
обеспечивает абсолютную целостность данных за счет снижения об-
шей производительности системы. Более низкие уровни изоляции мо-
гут повысить масштабируемость приложения, но при этом возможны
ошибки, связанные с нарушением целостности данных. В приложени-
ях, главным образом читающих и лишь изредка записывающих дан-
ные, лучше использовать более низкие уровни изоляции.
• Ценную информацию о выборе подходящего уровня изоляции тран-
закций можно найти в книге Кэйлин Дилэйни (Kalen Delaney) «Inside
SQL Server 2000» (Microsoft Press).

Выполнение ручных транзакций в хранимых процедурах


Б хранимых процедурах можно напрямую управлять ручными транзакци-
ями с помощью операторов Transact-SQL. Например, можно выполнить
транзакционные операции в одной хранимой процедуре, используя такие
операторы Transact-SQL, как BEGIN TRANSACTION, END TRANSAC-
TION и ROLLBACK TRANSACTION.

Дополнительная информация
• При необходимости в хранимой процедуре можно управлять уровнем
изоляции транзакции с помощью оператора SET TRANSACTION ISO-
LATION LEVEL. По умолчанию в SQL Server используется уровень
изоляции Read Committed. Подробнее об уровнях изоляции транзак-
ций SQL Server см. SQL Server Books Online (раздел «Accessing and
Changing Relation Data», подраздел «Isolation Levels»).
• Пример кода, иллюстрирующий, как выполнять изменения в рамках
транзакции с помощью транзакционных операторов языка Transact-SQL,
см. в приложении «Выполнение транзакций с помощью Transact-SQL».
54 Microsoft AD0.NET

Применение автоматических транзакций


Автоматические транзакции упрощают модель программирования, так как
не требуют явным образом начинать новую транзакцию или фиксировать/
отменять транзакцию. Однако главное преимущество автоматических
транзакций в том, что они работают совместно с DTC, что позволяет ох-
ватывать одной транзакцией несколько распределенных источников дан-
ных, В крупномасштабных распределенных приложениях это очень важно.
Вы можете управлять распределенными транзакциями и вручную, напря-
мую обращаясь к DTC программным путем. Но автоматические транзак-
ции радикально облегчают задачу управления распределенными транзак-
циями и предназначены для компонентных систем. Например, с помощью
соответствующих объявлений легко настроить несколько компонентов
так, чтобы они выполняли действия, составляющие одну транзакцию.

Автоматические транзакции опираются на СОМ+-поддержку распреде-


ленных транзакций и, следовательно, могут использоваться только обслу-
живаемыми компонентами, т. е. компонентами, производными от класса
Serviced Component.

Чтобы класс поддерживал автоматическую транзакцию, выполните следу-


ющие действия.

• Наследуйте свой класс от класса Serviced Component в пространстве


имен System. EnterpriseServices,
• Определите требования класса к транзакциям с помощью атрибута
Transaction. Значение, выбираемое из перечислимого Transaction-
Option, определяет, как класс конфигурируется в СОМ+ Catalog. Этот
атрибут позволяет указать и такие свойства, как уровень изоляции
транзакций и время ожидания.
• Чтобы не приходилось явно определять результат транзакции (фикса-
цию или откат), пометьте методы атрибутом AutoComplete. Если метод
с таким атрибутом генерирует исключение, транзакция автоматически
откатывается. Имейте в виду, что у вас сохраняется возможность явно
указать результат транзакции (transaction outcome). Детали см. в раз-
деле «Определение результатов транзакций»,

Дополнительная информация
• Дополнительную информацию об автоматических транзакциях СОМ+
см. в документации Platform SDK в разделе «Automatic Transactions
Through СОМ+».
• Пример транзакционного .NET-класса см. в приложении «Программи-
рование транзакционного класса .NET».
Руководство по архитектуре доступа к данным на платформе .NET 55

Настройка уровней изоляции транзакций


В СОМ+ 1.0, т. е. в версии, предоставляемой Windows 2000, используется
уровень изоляции транзакций Serialized. Это обеспечивает высшую сте-
пень изоляции, но за счет производительности. Общая производитель-
ность системы снижается, так как задействованные в транзакции диспет-
черы ресурсов (обычно базы данных) должны поддерживать на время
транзакции блокировки и по чтению, и по записи. В течение этого време-
ни остальные транзакции блокируются, что может значительно ухудшить
масштабируемость приложения.

В СОМ+ версии 1.5, поставляемой с Microsoft .NET, допускается настрой-


ка уровня изоляции транзакций в СОМ+ Catalog индивидуально для каж-
дого компонента. Уровень изоляции определяется параметром, сопостав-
ленным с корневым компонентом, участвующим в транзакции. Кроме
того, у внутренних подкомпонентов, являющихся частью той же транзак-
ции, не должен быть более высокий уровень изоляции, чем у корневого
компонента. Иначе при создании экземпляров подкомпонентов будут воз-
никать ошибки.

У управляемых .NET-классов атрибут Transaction поддерживает открытое


свойство Isolation. Это свойство позволяет декларативно указывать опре-
деленный уровень изоляции, как в следующем коде.
[Transaction(TransactionOption. Supported,
Isolation=TransactionIsolationL.evel.ReadCommitted)]
public class Account : ServicedCotnponent

Дополнительная информация
• Подробнее о настраиваемых уровнях изоляции транзакций и других
усовершенствованиях СОМ+ в .NET см. статью «Windows XP: Make
Your Components More Robust with COM+ 1.5 Innovations*- в журнале
«MSDN Magazine» за август 2001 г.

Определение результатов транзакций


Результат автоматической транзакции регулируется флагом отмены тран-
закции (transaction abort flag), а также флагами целостности (consistent
flags) в контексте всех транзакционных компонентов одного потока тран-
закции. Результат транзакции определяется в момент деактивации корне-
вого компонента, когда управление возвращается вызывающему методу
(см. рис. 5, который иллюстрирует классическую банковскую транзакцию
по переводу денег).
56 Microsoft ADO.NET

Рис. 5. Поток транзакции и контекст

Результат транзакции определяется, когда корневой объект (в данном при-


мере — объект Transfer) деактивируется и управление возвращается мето-
ду клиента. Если какой-либо флаг целостности в любом контексте уста-
новлен в false или если флаг отмены транзакции установлен в true, соот-
ветствующая физическая транзакция DTC отменяется.

Вы можете управлять результатом транзакции из .NET-объекта одним из


двух способов.

• Пометить методы атрибутом AutoComplete, что позволит .NET автома-


тически управлять результатом транзакции в соответствии с вашими
требованиями. При наличии этого атрибута — если метод генерирует
исключение — флагу целостности автоматически присваивается false
(что в конечном счете приводит к отмене транзакции). Если метод за-
вершается без генерации исключения, флагу целостности присваива-
ется true, и это указывает на готовность компонента к фиксации тран-
закции. Но фиксация транзакции не гарантируется, поскольку зависит
от того, как проголосуют другие объекты, относящиеся к тому же по-
току транзакции.
• Вызывать статический метод SetComplete или SetAbort класса Соп-
textUtil — при этом флагу целостности присваивается соответственно
true или false.

Ошибки SQL Server с уровнями значимости выше 10 приводят к тому, что


управляемый провайдер данных генерирует исключения типа SqlExcep-
Руководство по архитектуре доступа к данным на платформе .NET 57

tion. Если ваш метод перехватывает и обрабатывает исключение, вы дол-


жны вручную указать, что транзакцию нужно отменить, или, когда метод
помечен как [AutoComplete], передать исключение вызывающему методу.

Методы с [AutoComplete]
Для методов с атрибутом AutoComplete выполните одно из следующих
действий.

• Передайте SqlException вверх по стеку вызовов.


• Поместите SqlException во внешнее исключение и передайте его вызы-
вающему методу. Например, вам может понадобиться поместить ис-
ключение внутрь такого типа исключений, который имеет больший
смысл для вызывающего метода.

Если передать исключение дальше не удастся, объект не проголосует за


отмену транзакции, несмотря на ошибку базы данных. И тогда возможна
ошибочная фиксация других успешных операций, которые были выполне-
ны другими объектами, участвующими в том же потоке транзакции.

В следующем фрагменте кода перехватывается SqlException, которое за-


тем передается непосредственно вызывающему методу. В конечном счете
транзакция отменяется, так как при деактивации объекта его флаг целос-
тности автоматически приравнивается false.
[AutoComplete]
void SomeMethodO
{
try
{
// Открываем соединение и выполняем операции над базой данных

}
catch (SqlException sqlex )
<
LogException( sqlex ); // протоколируем параметры исключения
throw; // снова генерируем то же исключение, чтобы
// флаг целостности получил значение false
}
finally
{
// Закрываем соединение с базой данных
58 Microsoft ADO.MET

Методы без [AutoComplete]


Б случае методов без атрибута AutoComplete вы должны:

• вызывать Con text Util. Set Abort в блоке catch, чтобы при исключении
проголосовать за отмену транзакции. При этом флаг целостности по-
лучит значение false;
• вызывать Context UtiLSetComplete, если исключения не было, и прого-
лосовать таким образом за фиксацию транзакции. При этом флаг це-
лостности получит значение true, что совпадает с его значением по
умолчанию.

Этот подход иллюстрирует следующий фрагмент кода.


void SomeOtherMetliodO
{
try
{
// Открываем соединение и выполняем операции над базой данных

ContextUtil.SetComplete(); // еручнуя голосуем за фиксацию транзакции


I
catch (SqlException sqlex)
{
LogExceptionf sqlex ); // протоколируем параметры исключения
ContextUtil.SetAbortO; // вручную голосуем за отмену транзакции
// Теперь исключение обработано, и нет необходимости
// передавать его вызывающему методу
>
finally
{
// Закрываем соединение с базой данных

Примечание При наличии нескольких блоков catch проще вызвать Context-


Util.SetAbort в начале метода и поместить вызов ContextUtil.SetComplete в конец
блока try. Тогда не придется вызывать метод ContextUtil.SetAbort в каждом блоке
catch. Значение, присваиваемое флагу целостности при вызове этих методов, иг-
рает роль, только когда ваш метод возвращает управление.

Всегда передавайте исключения (или исключения-оболочки) обратно по


стеку вызовов — тогда вызывающий код получает информацию о том, что
транзакция закончится неудачно, и может предпринять соответствующие
действия. Так, в примере с переводом денег компонент Transfer мог бы
отказаться от кредитной операции, зная, что дебетную операцию выпол-
нить не удалось.
Руководство по архитектуре доступа к данным на платформе .NET 59

Если присвоить флагу целостности значение false и возвратить управление


без генерации исключения, то вызывающий код не сумеет заранее узнать,
что транзакция закончится неудачно. Хотя вы можете, например, вернуть
значение типа Boolean или передать выходной параметр этого типа, будь-
те последовательны и для передачи информации об ошибочных ситуаци-
ях генерируйте исключения. Тогда код будет более понятным, а при обра-
ботке ошибок будет использоваться стандартный подход.

Загрузка данных порциями


Загрузка данных порциями (data paging) — типичное требование, предъяв-
ляемое в распределенных приложениях. Например, пользователь может
получить длинный список книг, который нет смысла показывать целиком
за один раз; пользователь предпочел бы выполнять такие привычные дей-
ствия, как просмотр следующей/предыдущей страницы списка или пере-
ход к его началу/концу.

В данном разделе рассматриваются различные варианты реализации этой


функциональности и влияние каждого из них на масштабируемость и про-
изводительность.

Сравнение возможных вариантов


Для загрузки данных порциями выберите один из следующих вариантов,

• Вызов метода Fill объекта SqlDataAdapter для записи в DataSet диапа-


зона результатов.
• Применение ADO через COM Interop и использование серверного
курсора (server-side cursor).
• Загрузка данных порциями вручную с помощью хранимых процедур.

Какой из вариантов окажется для вас наилучшим, зависит от:

• требований к масштабируемости;
• требований к производительности;
• полосы пропускания сети;
• объема памяти и мощности сервера базы данных;
• объема памяти и мощности сервера промежуточного уровня;
• числа возвращаемых записей, которые вы хотите разбить на порции;
• размера нужной вам порции данных.

Тестирование показало, что в широком диапазоне уровней нагрузки наи-


лучшую производительность обеспечивает загрузка порциями вручную с
применением хранимых процедур. Однако в этом случае операции, необ-
60 Microsoft ADO.NET

ходимые для такой загрузки, выполняются на сервере. Поэтому, если зна-


чительная доля функциональности сайта основана на загрузке данных
порциями, могут возникнуть серьезные проблемы, связанные с высокой
нагрузкой на сервер. Чтобы подобрать способ, наилучший в ваших усло-
виях, проверьте все варианты на соответствие требованиям, предъявляе-
мым вами.

Ниже рассматриваются все варианты.

Применение SqIDataAdapter
Как уже говорилось, объект SqIDataAdapter используется для заполнения
DataSet информацией из базы данных. Один из его перегруженных мето-
дов Fill (показанный ниже) принимает два целочисленных индекса.

public int Fill(


DataSet dataSet,
int startRecord,
int maxRecords,
string srcTable
);

Здесь startRecord — индекс перкой записи (отсчет от нуля), a maxRecords —


число записей, которые копируются в новый DataSet, начиная со startRecord.

На внутреннем уровне SqIDataAdapter использует SqlDataReader для вы-


полнения запроса и возвращения результатов. SqIDataAdapter считывает
результаты и создает DataSet, содержащий считанные SqlDataReader дан-
ные. SqIDataAdapter копирует в новый DataSet все результаты, а не толь-
ко maxRecords записей, начиная со startRecord, и отбрасывает все ненуж-
ные. Таким образом, потенциально возможна передача клиенту через сеть
большого объема лишних данных. В этом и заключается основной недо-
статок такого подхода.

Например, если имеется 1000 записей, из которых вас интересуют записи


с 900-й по 950-ю. то первые 899 записей все равно считываются через сеть,
а затем отбрасываются. Такие издержки, возможно, окажутся минималь-
ными при наборах результатов малого размера, но для больших объемов
данных издержки могут быть очень велики.

Применение ADO
Еще один вариант реализации загрузки порциями — использование ADO
средствами СОМ. Основная идея такого подхода — получить доступ к сер-
верным курсорам, предоставляемым ADO-объектом Recordset. Вы присва-
иваете свойству CursorLocation объекта Recordset значение adUseServer,
Если ваш провайдер OLE DB поддерживает этот параметр (как, например,
Руководство по архитектуре доступа к данным на платформе .NET 61

SQLOLEDB), это приведет к использованию курсора на серверной сторо-


не. И вы с помощью этого курсора сможете перейти прямо к начальной
записи, не передавая все записи клиенту через сеть.

У этого подхода два основных недостатка.

• В большинстве случаев требуется преобразовывать записи, возвраща-


емые в объекте Recordset, в DataSet для использования в управляемом
коде клиента. Хотя О leDb Data Adapter содержит перегруженный метод
Fill, позволяющий преобразовать ADO-объект Recordset в DataSet,
этот метод не позволяет задавать начальную и конечную запись. Един-
ственное, что можно сделать, — перейти на начальную запись в объек-
те Recordset, перебрать все записи и вручную скопировать данные в
созданный вами DataSet. Однако затрачиваемые на это усилия и, в
частности, издержки вызовов COM Interop могут перевесить преиму-
щества меньшего сетевого трафика — особенно в случае DataSet не-
большого размера.
• Соединение и серверный курсор остаются открытыми, пока вы извле-
каете с сервера нужные вам данные. Обычно курсоры, открываемые и
поддерживаемые на сервере базы данных, — ресурсы дорогостоящие. И
хотя они позволяют увеличить производительность, есть вероятность
снижения масштабируемости из-за неэкономного расходования цен-
ных ресурсов сервера в течение длительных периодов.

Реализация вручную
Последний вариант загрузки данных порциями, рассматриваемый в этом
разделе, — реализация этой функциональности вручную, на основе храни-
мых процедур. В случае таблиц, имеющих уникальный ключ, создать та-
кую хранимую процедуру сравнительно легко. В случае таблиц без уни-
кального ключа (вряд ли вам слишком часто попадаются такие таблицы)
этот процесс усложняется.

Загрузка данных порциями


из таблиц с уникальным ключом
Если у таблицы есть уникальный ключ, его можно указать в блоке WHERE
оператора SELECT для создания набора результатов, начинающегося с
заданной записи. В сочетании с оператором SET ROWCOUNT для огра-
ничения размера набора результатов это обеспечивает эффективный меха-
низм загрузки данных порциями. Вот код хранимой процедуры, демонст-
рирующей такой подход.
CREATE PROCEDURE GetProductsPaged
gaastProductID int,
62 Microsoft ADO.NET

Size int
AS
SET HOWCOUNT @pageSize
SELECT «
FROM Products
WHERE [стандартное условна поиска]
AND ProductID > §lastProductID
ORDER BY [сортировка, при которой ProductID монотонно возрастает]
GO

Модуль, вызывающий эту хранимую процедуру, должен просто хранить


значение last Product ID и в промежутке между вызовами увеличивать или
уменьшать его на выбранный размер порции.

Загрузка данных порциями


из таблиц без уникального ключа
Если у таблицы, из которой вы загружаете данные, нет уникального клю-
ча, лучше добавьте его — например, через поле со свойством identity- Это
позволит задействовать описанный выше способ загрузки данных порциями.

Но и без уникального ключа можно реализовать эффективное решение


для загрузки данных порциями, если у вас есть возможность формировать
уникальное значение, комбинируя два поля (или более), которые являют-
ся частью набора результатов.

Рассмотрим следующую таблицу.

Coll Со12 Со13 Остальные поля...


\ 1 W
\ 1 X
\ i Y
Л 1 Z
А 2 W
А 2 X
В I W
В 1 X

Б этой таблице можно сформировать такое уникальное значение комбина-


цией полей Coll, Col2 и Со13. Это позволяет реализовать механизм загруз-
ки данных порциями, демонстрируемый следующей хранимой процедурой.

CREATE PROCEDURE RetrieveDataPageu


@lastKey char(40),
&pageSize int
AS
Руководство по архитектуре доступа к данным на платформе .NET 03

SET RGWCOUNT @pageSize


SELECT
Coll, Col2. Col3. CoU, Col1+Col2+Col3 As KeyField
FROM SampleTable
WHERE [стандартное условие поиска]
AND Со11+Со12+Со13 > @lastKey
ORDER BY CoU ASC, Col2 ASC, Col3 ASC
GO

Для управления загрузкой таблицы порциями клиент должен сохранять


последнее значение столбца KeyField, возвращаемого хранимой процеду-
рой, и при новом вызове передавать его этой процедуре.

При реализации загрузки порциями вручную увеличивается нагрузка на


сервер, зато удается избежать передачи лишних данных по сети. Тестиро-
вание производительности показало, что этот подход отлично работает
при разных уровнях нагрузки. Однако выполнение загрузки данных пор-
циями на сервере может снизить масштабируемость приложений — все
зависит от того, насколько широко такая загрузка применяется на вашем
сайте. Чтобы найти оптимальный подход для своего приложения, прове-
ряйте его производительность в среде, где оно должно работать.

Как включить поддержку


конструирования объектов .NET класса
Чтобы управляемый .NET-класс мог конструировать объекты, используя
сервисы Enterprise (COM+) Services, действуйте по следующей схеме.

• Наследуйте свой класс от класса ServicedComponent в пространстве


имен System.EnterpriseServices.
using System.EnterpriseServices;
public class DataAccessComponent : ServicedComponent

• Пометьте класс атрибутом Construction Enabled и при необходимости


укажите строку инициализации по умолчанию (default construction
string). Это значение хранится в СОМ+ Catalog. Для управления этим
значением администраторы могут использовать оснастку Component
Services консоли Microsoft Management Console (MMC).
64 Microsoft ADO.NET

11
[ConstructionEnabled(De"fault= default DSN")]
public class DataAccessComponent : ServicedComponent

• Создайте переопределенную реализацию виртуального метода Const-


ruct. Он вызывается после конструктора, специфичного для конкрет-
ного языка. Единственный аргумент этого метода — строка инициали-
зации, хранящаяся в СОМ+ Catalog.
public override void Construct( string constructString )
{
// Метод Construct вызывается сразу после конструктора,
// Единственный аргумент - DSN (Data Source Name),
// заданное при настройке.
}
• Укажите для сборки строгое имя (strong name), подписав ее с помощью
атрибута AssemblyKeyFile или Assembly Key Name. У любой сборки, за-
регистрированной в СОМ+ Services, должно быть строгое имя. Допол-
нительную информацию о строгих именах сборок см. по ссылке http:/
/msdn.microsoft.com/library/en-us/cpguidnf/html/cpconworkingwithst-
rongly-namedassemblies.asp.
[assembly: AssemblyKeyFile("DataServices.snk")]

• Для поддержки динамической, или отложенной (lazy), регистрации ис-


пользуйте атрибуты уровня сборки ApplicationName и ApplicationActi-
vation и укажите соответственно имя СОМ+-приложения, хранящего
компоненты сборки, и тип активации приложения. Подробнее о реги-
страции сборок см. по ссылке http://msdn.microsoft.com/library/en-us/
cpguidnf/html/cpconregisteringservicedcomponents.asp.
// Атрибут ApplicationName содержит имя СОМ+-приложения,
// в котором будут храниться компоненты сборки
[assembly : ApplicationName("DataServices")]
// Атрибут ApplicationActivation,ActivationOption определяет,
// в какой процесс компоненты загружаются при активации:
// Library - компоненты выполняются в процессе создавшего их приложения,
// Server - компоненты выполняются в системном процессе dllhost.exe
[assembly: ApplicationActivation(ActivationOption.Library)]

В следующем фрагменте кода показан обслуживаемый компонент DataAc-


cessComponent, использующий строки инициализации СОМ+ для получе-
ния строки подключения к базе данных.
using Systen;
using System.EnterpriseServices;

// Атрибут ApplicationName содержит имя СОМ+-приложения,


// в котором будут храниться компоненты сборки
Руководство по архитектуре доступа и данным на платформе .NET 65

[assembly : ApplicationName("DataServices")]
// Атрибут ApplicationActivation.ActivationOption определяет.
// в какой процесс компоненты загружаются при активации:
// Library - компоненты выполняются в процессе создавшего их приложения,
// Server - компоненты выполняются в системном процессе dllhost.exe
[assembly: ApplicationActivation(ActivationOption. Library)]
// Подписываем сборку. Ключевой файл с расширением snk
// создается утилитой sn.exe.
[assembly: AssemblyKeyFile("DataServices.snk")]
[Const ructionEnabled(Default="Default DSN")]
public class DataAccessComponent : ServicedComponent
i
private string connectionString;
public DataAccessComponent О
{
// Конструктор вызывается при создании экземпляра
}
public override void Construct( string constructString )
{
// Метод Construct вызывается сразу после конструктора.
// Единственный аргумент - OSN, заданное при настройке.
this. connectionString = constructString;

Использование Sq I Data Adapter


для чтения нескольких записей
В следующем коде показывается, как использовать объект Sql Data Adapter
для выполнения команды, генерирующей DataSet или DataTable. Здесь
считывается набор категорий продуктов из базы данных Northwind, по-
ставляемой с SQL Server.
using System. Data;
using System. Data. SqlClient;

public DataTable RetrieveRowsWithDataTableO


{
using ( SqlConnection conn = new SqlConnection(connectionString) )
<
SqlCommand cmd = new SqlCommandC'DATRetrieveProducts", conn);
cmd.CommanoType = CommandType.StoredProcedure;
SqlDataAdapter da = new SqlDataAdapter( cmd );
DataTable dt = new DataTable("Products");
da.Fill(dt);
return dt;

3 -5947
66 Microsoft ADO.NET

Чтобы с помощью SqlAdapter сгенерировать DataSet или DataTable, вы-


полните следующие действия.

1. Создайте объект SqlCommand для вызова хранимой процедуры и ука-


жите для него объект SqlConnection (как показано в примере) или
строку подключения (в примере не показана).
2. Создайте объект SqlDataAdapter и свяжите его с объектом SqlCommand.
3. Создайте объект DataTable (или DataSet). В качестве аргумента конст-
руктора укажите имя DataTable.
4. Вызовите метод Fill объекта SqlDataAdapter для заполнения DataSet
или DataTable считанными записями.

Использование SqIData Reader


для чтения нескольких записей
Как это делается, показано в следующем фрагменте кода,
using System.10;
using System.Data;
using System.Data.SqlClient;

public SqlDataReader HetrieveRowsWithDataReader()


1
SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=northwind");
SqlCommand cmd = new SqlCommandC'DATRetrieveProducts", conn );
cmd,CommandType = CommandType.StoredProcedure;
try
{
conn.OpenO;
// Генерируем класс чтения. CommandBehavior.CloseConnection
// означает, что соединение закрывается при закрытии объекта
// класса чтения.
return{ cmd.ExecuteReader( CommandBehavior.CloseConnection ) );
}
catch
<
conn.Closef);
throw;
)

// Показываем список продуктов, используя консоль


private void DisplayProductsO
{
SqlDataReader reader = RetrieveRowsWithDataReaderO;
while (reader.Reacf(»
Руководство по архитектуре доступа к данным на платформе .NET 67

Console. WriteLine("{0} {1} {2}",


reader.GetInt32(0).ToString(),
reader. GetString(l) );
}
reader. Close(); // закрывается и соединение, так как при генерации
// класса чтения было задано значение CommandBenavior

Чтобы считать несколько записей с помощью SqlDataReader, выполните


следующие действия.

1. Создайте объект SqlCommand для выполнения хранимой процедуры и


свяжите его с объектом SqlConnection.
2. Откройте соединение.
3. Сгенерируйте объект SqlDataReader, вызвав метод ExecuteReader
объекта SqlCommand.
4. Для чтения данных из потока вызовите метод Read объекта SqlData-
Reader и извлеките значения полей с помощью типизированных мето-
дов-аксессоров (например, Getlnt32 или GetString).
5. Закончив работу с классом чтения, вызовите его метод Close.

Использование XmlReader
для чтения нескольких записей
Объект SqlCommand можно использовать для генерации объекта XmlRea-
der, предоставляющего доступ к потоку XML-данных в направлении толь-
ко вперед. Команда (обычно хранимая процедура) должна сгенерировать
набор результатов в формате XML. Для этого в SQL Server 2000 обычно
применяется оператор SELECT с блоком FOR XML. Этот подход показан
в следующем фрагменте кода.
public void RetrieveAndDisplayRowsWithXmlReaderO
{
SqlConnection conn = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn );
cmd.CommandType = CommandType.StoredProcedure;
try
I
conn.0pen();
XmlTextReader xreader = (XmlTextReader)cmd. ExecuteXmlReaderf);
while ( xreader. Read() )
{
if ( xreader. Name == "PRODUCTS" )
I
string strOutput = xreader. GetAttributeC'ProductlD");
68 Microsoft ADO.NET

strOutput += " ";


strOutput += xreader.GetAttribute("ProductName");
Console. WriteLineC strOutput );

xreader.CloseC);
}
catch
{
throw;
}
finally
{
conn.Close();
}
:

Предыдущий код вызывает следующую хранимую процедуру.


CREATE PROCEDURE DATRetrieveProductsXML
AS
SELECT * FROM PRODUCTS
FOR XML AUTO
GO

Чтобы считать XML с помощью XmlReader, выполните следующее.

1. Создайте объект SqlComraand для вызова хранимой процедуры, гене-


рирующей набор результатов в формате XML (например, с помощью
блока FOR XML оператора SELECT). Свяжите объект SqlCommand с
объектом соединения.
2. Вызовите метод ExecuteXmlReadcr объекта SqlCommand и присвойте
результаты объекту XmlTextReader с поддержкой перемещения по за-
писям в направлении только вперед. Это самый быстродействующий
тип объекта XmlReader, который следует использовать, если вам не
нужно проверять возвращаемые XML-данные на допустимость.
3. Считайте данные методом Read объекта XmlTextReader.

Использование выходных параметров хранимой


процедуры для чтения одной записи
Для чтения одной записи можно вызвать хранимую процедуру, возвраща-
ющую считанные поля этой записи как именованные выходные парамет-
ры. В приведенном ниже фрагменте кода используется хранимая процеду-
ра, считывающая наименование и цену определенного продукта из табли-
цы Products базы данных Nortlrwind.
Руководство по архитектуре доступа к данным на платформе .NET 69

void GetProductDetails( int ProductID,


out string ProductName, out decimal UnitPrice )
i
SqlConnection conn = new SqlConnection(
"server=( local) integrated Security=SSPI;database=Northwind");
// Создаем объект команды для вызова хранимой процедуры
SqlCommand cmd = new SqlCommandC "DATGetProductDetailsSPOutput", conn );
cmd.CommandType = CommandType.StoredProcedure;
// Указываем параметры хранимой процедуры:
// @ProductID int INPUT
// @ProductName nvarchar(40) OUTPUT
// @UnitPrice money OUTPUT

// Для выходных параметров нужно явно задавать направление


SqlParameter paramProdlD =
cmd. Parameters. Add( "©ProductID", ProductID );
paramProdlD. Direction = ParameterDirection. Input;
SqlParameter paramProdName =
cmd . Parameters. Add( "^ProductName", SqlDbType . VarCtiar, 40 ) ;
paramProdName. Direction = ParameterDirection. Output;
SqlParameter paramUnitPrice =
cmd. Parameters. Add( "@UnitPrice", SqlDbType. Money );
paramUnitPrice. Direct ion = ParameterDirection. Output;
try
<
conn.OpenQ;
// Для выполнения команды используйте ExecuteNonQuery.
// Хотя этот метод не возвращает записей, происходит заполнение
// выходных параметров (и возможно, возвращаемых значений).
cmd . ExecuteNonQuery( ) ;
// Возвращаем выходные параметры хранимой процедуры
ProductName = paramProdName. Value. ToString();
UnitPrice = (decimal)paramUnitPrice. Value;
}
catch
(
throw;
}
finally
{
conn.CloseO;

Чтобы считать одну запись через выходные параметры хранимой процеду-


ры, выполните следующее.

1. Создайте объект SqlCommand и свяжите его с объектом SqlConnection.


70 Microsoft ADO.NET

2. Задайте параметры хранимой процедуры, вызывая метод Add набора


Parameters объекта SqlCommand. По умолчанию параметры считают-
ся входными, поэтому для каждого выходного параметра нужно явно
задать направление.

Примечание Хороший стиль — указывать направление для всех параметров, в


том числе и для входных.

3. Откройте соединение.
4. Вызовите метод ExecuteNon Query объекта SqlCoramand. При этом за-
полняются выходные параметры (и, возможно, возвращаемое значение).
5. Считайте значения выходных параметров через свойство Value объек-
тов Sql Parameter.
6. Закройте соединение.

Предыдущий фрагмент кода вызывает следующую хранимую процедуру.


CREATE PROCEDURE DATGetProductDetailsSPOutput
eProductID int,
@ProductName nvarchar(40) OUTPUT,
@UnitPrice money OUTPUT
AS
SELECT eProductName = ProductName,
9UnltPrice = UnitPrtce
FROM Products
WHERE ProductID = §ProductID
GO

Использование SqIDataReader для чтения одной записи


Объект SqIDataReader можно использовать для чтения одной записи, в
частности для чтения значений определенных полей из возвращаемого
потока данных. Вот пример.
void GetProductDetailsUslngReader( int ProductID,
out string ProductName, out decimal UnitPrice )
{
SqlConnection conn = new SqlConnection(
"server=(local>;Integrated Security=SSPI;database=Northwind");
// Создаем обьект команды для выполнения хранимой процедуры
SqlCommand cmd = new SqlCommand{ "DATGetProductDetailsReader", conn );
cmd.CommandType = CommandType.StoredProcedure;
// Указываем параметры хранимой процедуры
// ©ProductID int INPUT

SqlParameter paraiflProdlD = cmd.Parameters.Add( "@ProductID", ProductID );


paramProdlD.Direction = ParameterDirection.Input;
Руководство по архитектуре доступа к данным на платформе .NET 71

try
{
conn.OpenO;
SqlDataReader reader = cmd.ExecuteReaderf);
reader. ReadO; // читаем единственную запись
// Возвращаем выходные параметры из полученного потока данных
ProductName = reader. GetString(O);
UnitPrice = reader. GetDecimal(l);
reader. Closef);
}
catch
{
throw;
}
finally
{
conn.CloseO;

Для чтения одной записи с помощью объекта SqlDataReader выполните


следующие действия.

1. Создайте объект SqlCommand.


2. Откройте соединение.
3. Вызовите метод ExecuteReader объекта SqlCommand, чтобы получить
объект SqlDataReader.
4. Считайте выходные параметры типизированными методами-аксессора-
ми объекта SqlDataReader (в данном случае — GetString и GetDecimal).

В предыдущем фрагменте кода вызывается следующая хранимая процедура,


CREATE PROCEDURE DATGetProductDetailsReader
&ProductID int
AS
SELECT ProductName, UnitPrice FROM Products
WHERE ProductlD = @ProductID
GO

Использование ExecuteScalar для чтения одного поля


Метод ExecuteScalar предназначен для запросов, возвращающих только
одно значение. Если запрос возвращает несколько полей и/или записей,
ExecuteScalar вернет для этого запроса лишь первое поле первой записи.

В следующем фрагменте кода показывается, как найти наименование про-


дукта по идентификатору.
72 Microsoft ADG.NET

void GetProductNameExecuteScalar{ int ProductID, out string ProductName )


{
SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=northwind");
SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ProductID", ProductID );
try
{
conn.0pen();
ProductName = (string)cind. ExecuteScalar();
}
catch
{
throw;
}
finally
{
conn.Close();
}
;
Чтобы считать одно поле через ExecuteScalar, выполните следующие
действия.

1. Создайте объект SqlCommand для вызова хранимой процедуры.


2. Откройте соединение.
3. Вызовите метод ExecuteScalar. Обратите внимание, что он возвращает
значение первого считанного поля как объектный тип, и вы должны
привести его к нужному типу.
4. Закройте соединение.

В приведенном выше фрагменте кода используется эта хранимая процедура.


CREATE PROCEDURE LookupProductNameScalar
©ProductID int
AS
SELECT TOP 1 ProductName
FROM Products
WHERE ProductID = tProductID
GO
Руководство по архитектуре доступа к данным на платформе .NET 73

Использование возвращаемого значения


или выходного параметра хранимой процедуры
для чтения одного поля
Вы можете получить единственное значение через возвращаемое значение
или выходной параметр хранимой процедуры. В следующем коде демон-
стрируется вариант с выходным параметром.
void GetProductNaraeUsingSPOutpuU int ProductID, out string ProductName )
{
SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=northwind");
SqlCommand cmd = new SqlCommandC'LookupProductNameSPOutput", conn );
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter paramProdID = cmd.Parameters.Add("§ProductID", ProductID );
ParamProdlD.Direction = ParameterDirection.Input;
SqlParameter paramPN =
cind.Parameters.Add("@>ProductName", SqlObType.VarChar, 40 );
paramPN.Direction = ParameterDirectlon.Output;
try
I
conn.OpenO;
cmd.ExecuteNonQue ry();
ProductName = paramPN. Value. ToStringO;
}
catch
{
throw;
}
finally
{
conn.Close();
}
I
Для чтения единственного значения из выходного параметра хранимой
процедуры выполните следующее.

1. Создайте объект SqlCommand для вызова хранимой процедуры.


2. Задайте все входные параметры и единственный выходной параметр, до-
бавляя объекты SqlParameter в набор Parameters объекта SqlCommand.
3. Откройте соединение.
4. Вызовите метод ExecuteNonQuery объекта SqlCommand.
5. Закройте соединение.
6. Извлеките выходное значение из свойства Value объекта SqlParameter.
74 Microsoft ADO.NET

В приведенном выше фрагменте кода используется следующая хранимая


процедура.
CREATE PROCEDURE LookupProductNameSPOutput
SProductlD int,
@ProductName nvarchar(40) OUTPUT
AS
SELECT @ProductName = ProductName
FROM Products
WHERE ProductID = @ProductID
GO

Следующий код иллюстрирует, как воспользоваться возвращаемым значе-


нием хранимой процедуры, чтобы проверить наличие определенной запи-
си. С точки зрения программирования, это аналогично применению вы-
ходных параметров — с тем исключением, что вы должны явно присвоить
объекту SqlParameter значение ParameterDirection. Return Value.
bool CheckProduct( int ProductID )
(
SqlConnectIon conn = new SqlConnection{
"server={local);Integrated Security=SSPI;database=northwind");
SqlCommand cmd = new SqlCommandC'CheckProductSP", conn );
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ProductID", ProductID );
SqlParameter paramRet =
cmd.Parameters.Add("@ProductExists", SqlDbType.Int );
paramRet.Direction = ParameterDirection.ReturnValue;
try
{
conn.0pen();
cmd.ExecuteNonQuery();
\
catch
{
throw;
}
finally

conn.CloseO;
}
return (int)paramRet.Value == 1;
J

Чтобы проверить наличие определенной записи, выполните следующие


действия.

1. Создайте объект SqlCommand для вызова хранимой процедуры.


Руководство по архитектуре доступа к данным на платформе ,NET 75

2. Задайте входной параметр, содержащий значение первичного ключа


той записи, наличие которой вы хотите проверить.
3. Задайте единственный параметр для возвращаемого значения. Добавь-
те объект SqlParameter в набор Parameters объекта SQLCommand и ус-
тановите его направление как ParameterDirection.RetumValue.
4. Откройте соединение.
5. Вызовите метод ExecuteNonQuery объекта SqlCommand.
6. Закройте соединение.
7. Получите возвращаемое значение из свойства Value объекта SqlParameter.

В показанном выше фрагменте кода используется следующая хранимая


процедура.
CREATE PROCEDURE CheckProductSP
isProductlD int
AS
IF EXISTSC SELECT ProductID
FROM Products
WHERE ProductID = @ProductID )
return 1
ELSE
return 0
GO

Использование SqIDataReader для чтения одного поля


Объект SqIDataReader позволяет получать единственное выходное значе-
ние методом ExecuteReader объекта команды. Это требует чуть больше
кодирования, чем в предыдущих случаях, поскольку вы должны вызвать
метод Read объекта SqIDataReader, а затем считать требуемое значение с
помощью одного из методов-аксессоров класса чтения. Использование
объекта SqIDataReader демонстрирует следующий код,
bool CheckProductWithReaderC int ProductID )
{
SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=northwind");
SqlCominand cmd = new SqlCommand("CheckProductExistsWithCount", conn );
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("©ProductID", ProductID };
cmd. Parameters["©ProductID"]. Direction = ParameterDirection.Input;
try
(
conn.OpenO;
SqIDataReader reader = cmd.ExecuteReader(
CommandBehavior.Singleflesult );
76 Microsoft ADO.NET

reader.Read();
bool bRecordExists = reader.Getlnt32{0) > 0;
reader.Closet);
return bRecordExists;

catch

throw;

finally

conn.Close();

В приведенном выше фрагменте кода используется следующая хранимая


процедура.
CREATE PROCEDURE CheckProductExistsWitnCount
@ProductID int
AS
SELECT COUNT(*} FROM Products
WHERE ProductID = eProductID
GO

Программирование ручных транзакций ADO.NET


Ниже показано, как использовать поддержку транзакций в SQL Server
.NET Data Provider, чтобы с помощью транзакции защитить перевод де-
нежных средств. Перевод средств выполняется между двумя счетами, на-
ходящимися в одной базе данных.
public void TransferMoney( string toAccount,
string fromAccount, decimal amount )
{
using ( SqlConnection conn = new SqlConnection(
"server=(local);Integrated Security=SSPI;database=SimpleBank" ) )
{
SqlCommand cmdCredit = new SqlCommand("Credit", conn );
cmdCredit.CommandType = CommandType.StoredProcedure;
cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );
cmdCredit.Parameters.Add( new SqlParameter("©Amount", amount ));
SqlCommand cmdDebit = new SqlCommand("Debit", conn );
cmdOebit.CommandType = CommandType.StoredProcedure;
cmdDebit.Parameters.Add{ new SqlParameter("@AccountNo", fromAccount) );
cindDebit.Parameters.Add( new SqlParameter("@Amount", amount ));
conn.0pen();

// Начинаем новую транзакцию


Руководство по архитектуре доступа к данным на платформе .NET 77

using ( SqlTransaction trans = conn.BeginTransaction() )


{
// Связываем с этой транзакцией два объекта команды
omdCredit. Transaction = trans;
cmdDebit. Transaction = trans;
try
{
cmdCredit . ExecuteNonQueryt ) ;
cmdDebit . ExecuteNonQueryt ) ;
// Обе команды (для дебета и кредита) выполнены успешно
trans. CommitO;
}
catch( Exception ex )
{
// Транзакция потерпела неудачу
trans. RollbackQ;
// Протоколируем параметры исключении...
throw ex;

Выполнение транзакций с помощью Transact-SQL


Вот как выполнить транзакционную операцию по переводу денег с ис-
пользованием хранимой процедуры, написанной на Transact-SQL.
CREATE PROCEDURE MoneyTransfer
@FromAccount char(20),
taToAccount char(20),
^Amount money
AS
BEGIN TRANSACTION
- ВЫПОЛНЯЕМ ОПЕРАЦИЮ ПО ДЕБЕТУ
UPDATE Accounts
SET Balance = Balance - ^Amount
WHERE AccountNurober = ©FromAccount
IF №RowCount = 0
BEGIN
RAISERRORC 'Invalid From Account Number', 11, 1)
GOTO ABORT
END
DECLARE ^Balance money
SELECT ^Balance = Balance FROM ACCOUNTS
WHERE AccountNumber = @FromAccount
IF ^BALANCE < 0
BEGIN
RAISERRORC Insufficient funds', 11, 1)
GOTO ABORT
78 Microsoft ADO.MET

END
- ВЫПОЛНЯЕМ ОПЕРАЦИЮ ПО КРЕДИТУ
UPDATE Accounts
SET Balance = Balance + ©Amount
WHERE AccountNumber = @ToAccount
IF @@RowCount = 0
BEGIN
RAISERROHCInvalid To Account Number', 11, 1)
GOTO ABORT
END
COMMIT TRANSACTION
RETURN 0
ABORT:
ROLLBACK TRANSACTION
GO

В этой хранимой процедуре для управления транзакцией вручную исполь-


зуются операторы BEGIN TRANSACTION, COMMIT TRANSACTION и
ROLLBACK TRANSACTION.

Программирование транзакционного .NET-класса


В следующем примере показано три управляемых обслуживаемых .NET-
класса, сконфигурированных для автоматических транзакций. Каждый
класс помечен атрибутом Transaction, значение которого определяет, сле-
дует ли создавать новый поток транзакции или объект должен участвовать
в потоке транзакции вызывающего объекта. Операция по переводу денег
выполняется при взаимодействии этих компонентов. Класс Transfer поме-
чен атрибутом транзакции RequiresNew, а классы Debit и Credit — атрибу-
том Required, В итоге при выполнении все три объекта участвуют в одной
и той же транзакции.
using System;
using System.EnterpriseServices;

[Transaction(TransactionOption.RequiresNew}]
public class Transfer : ServicedComponent
<
[AutoComplete]
public void Transfer^ string toAccount,
string fromAccount, decimal amount }
{
try

// Выполняем операцию по дебету


Debit debit = new Debit();
debit.DebitAccount( fromAccount, amount );
// Выполняем операцию по кредиту
Руководство по архитектуре доступа к данным на платформе .NET 79

Credit credit = new CreditO;


credit. CreditAccount( toAccount, amount );
}
catch( SqlException sqlex )
{
// Обрабатываем исключение и протоколируем его параметры.
// Помещаем исключение в оболочку и передаем дальше.
throw new TransferException( "Transfer Failure", sqlex );

[Transact ion(TransactionOption. Required)]


public class Credit : ServicedComponent
{
[AutoComplete]
public void CreditAccount( string account, decimal amount )
{
SqlConnection conn = new SqlConnection(
"Serve r=( local); Integrated Security=SSPI";database="SimpleBank");
SqlComtnand cmd = new SqlCommand("Credit", conn );
cmd.CommandType = CommandType.StoredProcedure;
cmd. Parameters. Add{ new SqlParameter("@AccountNo", account) );
cmd. Parameters. Add( new SqlParameter("@Amount", amount ));
try
(
conn.0pen();
cmd . ExecuteNonQuery( ) ;
}
catch (SqlException sqlex)
{
// Протоколируем параметры исключения
throw; // передави исключение дальше

I
[Т ransaction(TransactionOption. Required)]
public class Debit : ServicedComponent
{
public void DebitAccount( string account, decimal amount )
{
SqlConnectton conn = new SqlConnection(
"Server=(local); Integrated Security=SSPI"; database="SimpleBank");
SqlCommand cmd = new SqlCommandC'Debit", conn );
cmd.CommandType = CommandType.StoredProcedure;
cmd. Parameters, Add( new SqlParameter("@AccountNo", account) );
cmd,Parameters.Add( new SqlParameter("$Amount", amount ));
try
{
conn.0pen();
80 Microsoft ADO.NET

cmd. ExecuteNonQueryO;

catch (SqlException sqlex)

// Протоколируем параметры исключения


throw; // передаем исключение обратно вызывающему

Благодарности
Выражаем признательность всем, кто участвовал в подготовке этой статьи:

Bill Vaughn, Mike Pizzo, Doug Rothaus, Kevin White, Blaine Dokter, David
Schleifer, Graeme Malcolm (Content Master), Bernard Chen (Sapient), Matt
Drucker (Turner Broadcasting) и Steve Kirk.

Вопросы? Замечания? Предложения? С авторами статьи можно связаться


(на английском языке) по адресу devfdbck@microsoft.com.

Хотите освоить платформу .NET и задействовать всю ее мощь? Для обме-


на опытом лучше всего поработать бок о бок с экспертами из технологи-
ческих центров Microsoft. Дополнительную информацию можно получить
по ссылке http://www.microsoft.com/business/services/mtc.asp.
Джонни Папа

ADO.NET: концепции
и реализация

В этой статье подробно рассматривается концепция отделения доступа к


данным через ADO.NET от бизнес-логики и показывается, как добиться этого
на практике. Автор реализует компонент, абстрагирующий доступ к данным,
на двух .NET-языках: С# и Visual Basic .NET.

С появлением Microsoft .NET разработчики готовятся воспользоваться


такими достоинствами этой платформы, как улучшенные возможности
доступа к данным. Хотя и СОМ, и Visual Basic 6.0 будут широко приме-
няться еще долгие годы, уже сейчас заметен большой интерес к переходу
на .NET-компоненты. Чаше всего меня спрашивают, чем отличается раз-
работка с использованием ADO.NET от ADO 2л и как выделить логику
доступа к данным через ADO в отдельный компонент. Эти вопросы мы и
рассмотрим.

Архитектура ADO.NET предоставляет богатые возможности манипулиро-


вания данными в программах на С#, Visual Basic .NET и других .NET-co-
вместимых языках. В предыдущей статье из этой рубрики я продемонст-
рировал методику инкапсуляции компонента доступа к данным с исполь-
зованием ADO 1.x и Visual Basic 6.0. Разумеется, концепция отделения
доступа к данным от бизнес-логики относится не только к Visual Basic 6.0.
Аналогичные подходы применяются при разработке для .NET, и в этой
статье мы начнем разбираться с ADO.NET и ее влиянием на программи-

Публиковалогь в MSDN Magazine/Русская Редакция. 2002. Спецвыпуск №1 (янпарь-


март). — Прим. изд.
82 Microsoft ADO.NET

рование под .NET. Я покажу, как создать сервис для доступа к данным че-
рез ADO.NET, отделенный от бизнес-логики, реализуемой посредством
ASP.NET.

В примерах, которые мы рассмотрим, три основных составляющих:


• Web-форма, содержащая серверные элементы управления, такие как
asp:DataGrid, для отображения данных;
• CodeBehind-класс, получающий данные от сервиса доступа к данным
и заполняющий ими элементы на Web-форме;
• исходный код, в котором определяется собственное пространство имен
и классы, реализующие сервис доступа к данным.

Сейчас много спорят, какой .NET-язык использовать. Хотя .NET спроек-


тирована так, что ни один конкретный язык не имеет существенных пре-
имуществ перед другими, в каждом отдельном случае неплохо определить-
ся с этим вопросом. Между тем, пока дискуссии насчет языков продолжа-
ются, я приведу примеры как на Visual Basic, так и на С#. При этом не
забывайте, что сборки, созданные с использованием этих языков, могут
взаимодействовать друг с другом. Иначе говоря, можно создать сборку на
С# и обращаться к ней из класса, написанного на Visual Basic .NET.

Web-форма
Начнем с простой Web-формы, представляющей пользовательский интер-
фейс (UI) приложения. Код формы WebForm.aspx содержит два серверных
элемента управления DataGrid, которые будут заполнены данными из
двух разных ADO.NET-объектов DataSet (рис. 1). DataGrid позволяет
представлять набор данных как HTML-таблицу. (О DataGrid подробно
рассказывает Дино Эспозито в рубрике «Cutting Edge» в апрельском, май-
ском и июньском номерах «MSDN Magazine? за 2001 г.) На моей Web-
форме два элемента DataGrid: grdSql (заполняется SQL-оператором с ис-
пользованием моего сервиса доступа к данным) и grdProc (заполняется
хранимой процедурой с применением того же сервиса).

Заметьте: Web-форма на рис. 1 содержит только директивы, HTML и сер-


верные элементы управления. Собственно кода здесь нет. В .NET Frame-
work код, реализующий Ш, можно отделить от самого интерфейса. Иначе
говоря, весь HTML и серверные элементы управления могут быть внутри
Web-формы, а весь код, взаимодействующий с UI, может храниться в от-
дельном файле класса. Это альтернатива стандартному подходу в ASP, где
код и интерфейс смешаны, так что управлять проектом теперь гораздо
проще.
ADQ.NET: концепции и реализация

Рис. 1. Web-форма из Visual Basic


a
<Х@ Page Language "vtr Codebehind="WebForra.aspx.vb"
a
Innerlts "HyV80ataLayer.WebForerx>
<html>
<ne:ad></head>
<body>
<span>Example of Executing a SQL Statement and Filling a Grid</span>
<asp:Data6rid
ld*"grdSql" '
njnat=" Server"
autogeneratecolumn:s="tnje"
headerstyle-backcolor="8333399"
h e ade rst yle-font~ names="tah oma"
headerstyle-font»size="8pt"
he ade rstyle-forecolo r~"»ff f f66"
itewtyl,e-baekcQlor="White"
i t emstyle- f о nt -name s* " t atiorcia"
itemstyle-font-size="8pt"
alternatingitemstyle~backcolor="LightGoldenrQdYellow"
cellpadding="3"
/>
<br><br>
<span>£xampla of Executing a Stored Procedure and Filling a Grid</span>
<asp:DataGrid
ld="grciPrac"
гunat="Server"
autogeneratecolurons="true"
headerstyle-backcolor=s"»333399"
heade rsty1e-fon t-names="t a homa"
head0rstyle-font-size="8pt"
tiea6erstyle-forecolor="ffffff66"
itemstyle-backcolo r*"White"
i t emst yle-font-names»"tah oraa"
itemstyle-font-size="8pt~
alternatingiteiBstyle-backcolor="Light6oldenrodYellow"
cellpadding="3"
/>
</body>
</html>

Привязка Web-формы к коду сводится к тому, что вы указываете, где на-


ходится файл кода (его также называют CodeBehind-файлом). (Подробнее
о концепции CodeBehind см. http://msdn.microsoft.com/msdnmag/issues/
01/08/cuttmg/cutting0108.asp.) Директива @Page имеет атрибут CodcBe-.
hind (рис, 1). С ее помощью Web-форма связывается с исходным кодом.
Директива @Page также имеет атрибут Inherits, указывающий простран-
ство имен и класс, которые наследует Web-форма.
Ctf-версия этой страницы незначительно отличается от эквивалента на
Visual Basic — фактически только первыми строками. Например, в атри-
буте Language указан С#, а не Visual Basic. Атрибут CodeBehind указыва-
ет на файл WebForm.aspx.cs, a Inherits — на класс My С SDataLayer. Web-
Form. Столь незначительные отличия — следствие того, что весь реальный
код для этих страниц находится в файлах кода.

Рис. 2. Файл кода Web-формы «a Visuaf Basic

Imports System
Imports System. Data
Imports System. Wet. 01
Imports System. Web. UI.WebGontrols

Public Class WebForm


Inherits System. Web. UI. Page

'// ОБЪЯВЛЕНИЯ ПЕРЕЛЕННЫХ

'//- DataGrid для вывода данных, полученных от SQL-запроса


Protected grdSql As DataGrid
'//- DataQrid для вывода данных, полученных от хранимой процедуры
Protected grdProc As DataGrid

'ft Конструктор
' / / ____ _„_ __ __.„,
Public WebForntO

Private Sub Page_Load(8yVal sender As System. Object,


ByVal e As System. EventArgs) Handles MyBase.Load
' Здесь должен быть ваш код, инициализирующий страницу
Dim oDs As DataSet = Nothing
Difli oSql As SqIService = Nothing
. Dim sSql As String = ""
Dim sProcName As String = ""

V/- Создание экземпляра SqIService со строкой подключения


•• oSql = New MyVBDataLayer. SqlService("localhost", "northwind1
"sa", "")

'//- Определение SQL-оператора


sSql * "SELECT CategorylD, CategoryName, Description F80H "
"Categories ORDER 8Y CategoryName"

'//- Выполнение SQL-оператора через объект SQLService


'// и получение DataSet

см. след. стр.


&DO.NET: концепции и реализация

РИС. 2. Файл кода Web-формы на Visual Basic (окончание)


oDs ~ QSql.Run5ql{sSql, "Category")

'//- Заполнение DataGrid данными из DataSet


{jrdSql.DataSoorce = New DataVlew(oBs.Tables("Category")>
grdSql.DataBiRdO

'//- Уничтожение DataSet


oDs = Nothing

*,//- Определение хранимой процедуры и ее параметров


sProcName = "SalesByCategory"
eSqlrAddParaffleterC"®CategoryName",ssenumSqlDataTypes.
ssSOT_String, 15, "Produce")
QE^l.AddParameter("£QrdYear",sseruitfiSqlDataTyp&s. _
ssSDT_Strifi&, 4, "1998")

'//- Выполнение хранимой процедуры через объект SQLService


V/ и получение OataSet
oDs * oSql.RunProc<sProcName, "Sales")

'//- Заполнение DataGrid данными из OataSet


grdProc.DataSouree = Mew DataView{oDs,TabIes("Sales"))
grdProc.DataBindO

'If- Уничтожение DataSet


oDs = Nothing

'//- Уничтожение объекта SQIService


oSql = Mottling

End Sub

End Class

Код для Web-формы


В начале кода на Visual Basic для страницы WebForm.aspx явно указыва-
ется ссылка на пространство имен System.Data ключевым словом Imports
(рис. 2). Это пространство имен обеспечивает странице доступ к объектам
DataSet и Data View DataSet я буду получать из своего компонента досту-
па к данным, a DataView будет служить для привязки данных из DataSet
к элементу DataGrid,

Класс WebForm объявлен как наследующий пространство имен System.-


Web.UI.Page. Это позволяет напрямую ссылаться на объекты страницы.
Затем объявляются два DataGrid из того же пространства имен, что и эле-
86 Microsoft ADO.NET

менты управления в файле с UI, соответствующем данному файлу кода.


Это дает возможность взаимодействовать с элементами управления Data-
Grid из файла кода. Переменные объявлены как Protected, а значит, дос-
тупны только из самого класса или его потомков. Затем вы должны объя-
вить конструктор для WebForm, в который можно поместить код, выпол-
няемый при создании экземпляра формы.

Определив основные части CodeBehind, рассмотрим код обработчика ос-


новного события Page_Load (рис. 2). Здесь я объявляю объект DataSet —
он будет содержать набор результатов (result set), получаемый после каж-
дого обращения к моему компоненту. Также заметьте, что эта процедура
обрабатывает событие My Base. Load. Иначе говоря, событие Page_Load
замещает событие Load для этой страницы.

Я объявляю свой компонент доступа к данным (MyVBDataLayer.Sq]Ser-


vice) как переменную oSQL. Затем создаю экземпляр класса SqlService,
используя конструктор с четырьмя аргументами, которым присваиваются
значения, нужные для соединения. Конструктор определяет свойство
строки подключения объекта SqlClient.Connection, содержащегося в клас-
се SqlService.

Далее я задаю SQL-оператор и передаю его методу RunSql, выполняющий


этот SQL-оператор и генерирующий DataSet, который присваивается пе-
ременной oDs. Свойству DataSource данного DataGrid (grdSql) присваи-
вается Data View по умолчанию (таблицы Category в DataSet). Указав
таким образом элементу DataGrid, откуда брать данные, я вызываю его ме-
тод DataBind. В итоге данные напрямую привязываются к первому эле-
менту DataGrid на странице WebForm.aspx.

Одна из основных причин создания компонента (сервиса) доступа к дан-


ным — упрощение выборки данных. Как видите, этого можно добиться,
написав лишь несколько строк кода. Отказавшись от использования на
странице объектов из пространства имен SqlClient, я могу инкапсулиро-
вать специфические параметры в собственных методах. Так, вам нужен
единственный метод, выполняющий любой SQL-оператор независимо от
того, возвращает ли он набор результатов. Перегружая метод, вы можете
добиться этого в своем классе SqlService. Я использую лишь два объекта
из System.Data: DataSet и Data View, Следовательно, мой компонент дос-
тупа к данным абстрагирует все объекты пространства имен SqlClient.
Можно нойти дальше: получать XML из набора результатов и на его осно-
ве заполнять сетку (DataGrid). При этом полностью отпадает надобность
в ссылке на пространство имен System.Data в CodeBehind,
ADO.NET: концепции и реализация

Первый DataGrid заполняется результатами выполнения SQL-запроса, а


второй — хранимой процедуры. Я передаю параметры процедуры своему
компоненту через его метод Add Parameter (рис. 2). Имя параметра, его тип
данных, длина и значение передаются конструктору метода AddParameter.
Тип данных параметра я идентифицирую с помощью своего перечислимо-
го, определенного в пространстве имен SqlService. Затем вызываю метод
RunProc, запускающий хранимую процедуру и возвращающий DataSet.

Посмотрите на аналогичный код на С#. На пространства имен я ссылаюсь


через ключевое слово using. Далее определяю используемое в проекте про-
странство имен. Еще одно отличие — в способе объявления класса Web-
Form. В С# для наследования одного класса от другого вы указываете ба-
зовый класс после определения нового класса, разделяя их двоеточием
(рис. 3).

Рис. 3. Файл кода Web-Формы на С#

// Пространства имен

using System;
using System. Data;
using System. Web. Ul;
using System . Web. UI . WebContt ol&;

namespace MyCSDataLayer

i /____—«„„»„„ _____ _
// имя файла; WebForm.aSpx.C3
// Автор; Lancelot Web Solutions, LLC
// Дата: 07/04/2001
/ / ______________ _ ____
// Назначение: данный класс содержит код для ASP.NET-етраницы
// WebForts.aspx. При загрузке страницы заполняются два DataGrid
// с использованием собственной сборки MyCSDataLayer. Первый
// DataGrld заполняется из набора результатов, полученного от
// SQL-оператора. Второй - из набора результатов, полученного
// от хранимой процедуры.
//_—.«.-.—-._. _„_____
public class WebForm : System. Web. UI. Page
/_„..»....____ ____ „„„_—-

//__»._„-. _____ ____—

// Объявлений защищенных переменных

с.ч. след. стр.


Microsoft ADO.NET

Рис. 3. Файл кода Web-d>opMw;«a С#


/{ „ _„„„
//- DataGrld для вывода данных от SQL-запроса
protected DataSrid grdSql;
//- DataGrid для вывода данных от хранимой процедуры
protected DataGrid grdProc;
it

//_________ _„„„
// Конструктор
//_ __„_-.„„„„.-.___
protected WebFoneO
i
Page,Init += new System.EventHandler(Paffe_lnit);

// Этот ков выполняется при загрузке страницы


//—__...., _______ — «.„.
private void Page_Load(object sender, System, EventArgs e)

DataSet oDs = null; II DataSet для хранения данных,


// отображаемых DataGrid'ами
SqlService oSql = null; // экземпляр класса SqlService
string sSql = ""; // SQL-оператор для заполнения PataSet
string sProcName - ""; // хранимая процедура для заполнения DataSet

//- Создание экземпляра SqlService со строкой подключения


oSql = new SqlService<"localhost", "northwinct", "sa", "");

//- Определение SQL-оператора


sSql = "SELECT CategorylD, CategoryName, Description "
sSql += " FROM Categories ORDER BY CategoryName";

//- Выполнение SQL-оператора через объект SQLService


// и получение DataSet
oDs = oSql.8unSql(sSql, "Category"};

//- Заполнение DataGrid данными из DataSet


grdSql.DataSource * new SataView(oOs.Tables["Category"]};
grdSql.DataBindC);

//- Уничтожение DataSet


oDs=null;

//- Определение хранимой процедуры и ее параметров


sProcName = "SalesByCategory";
oSql.AddParaiieter<"@CategoryNaffle",

см. след. стр.


ADO.NET: концепций и реализация

Рис. 3- Файл кода Web-формы на С# (окончание)


MyCSOataUyer.ssenumSqlDataTypes.ssSDT.String, 15, /'Produce");
oSql.AddPara[neter("@QrdYear",
MyGSDataLayer.ssenumSqlDataTypes.ssSDT_String, 4, "1998");

//- Выполнение хранимой процедуры через объект SQLService


// и получение DataSet
oDs * QSql.flunProe(sProcNaffle, "Sales");

//- Заполнение OataGrid данными из DataSet


grdProc.DataSouree = new DataView(oDs.Tables["Sales"});
grdProc.DataBindO:

//- Уничтожение DataSet


oDs=nyll;

//- Уничтожение объекта SQiServlce


oSql=null;

Прочих отличий ни так много, и все они следуют из различий в синтакси-


се: точки с запятой в конце операторов, присвоение null (вместо Nothing)
при уничтожении объекта и указание типа перед именем в объявлениях
переменных (рис. 3). Не забывайте, что С# чувствителен к регистру букв —
здесь проще всего сделать ошибку, особенно если вы привыкли к Visual
Basic.

Класс SqIService
Класс SqIService является компонентом доступа к данным, определяя все
его методы, свойства и перечислимые. В начале класса я указываю все
используемые пространства имен:

Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlTypes
Imports System.Collections

Затем определяю перечислимое, нужное вызывающему коду (в данном


случае CodeBehind). Перечислимое указывает различные типы данных,
которые можно передавать методу AddParameter. Так я избавляюсь от не-
обходимости использовать в вызывающем коде ADO.NET-специфичные
пространства имен для SqlDataTypes.
90 Microsoft ADO.NET

Public Enum ssenumSqLDataTypes


ssSDT_Bit
ssSDT_DateTime
ssSDT_Decimal
ssSDT_Integer
ssSDT_Honey
ssSDT_String
End Enum

Следующий шаг — собственно определение класса SqlService (рис. 4).


Здесь объявлены переменные уровня класса для хранения значений, нуж-
ных для соединения с источником данных. Я также определяю объект
ArrayList для хранения параметров, передаваемых методу AddParameter
при использовании хранимой процедуры.

Создав разные конструкторы, я предоставляю вызывающей программе


разные варианты создания экземпляра класса SqlService. Чтобы опреде-
лить несколько конструкторов, я просто указываю для каждого из них раз-
личное число аргументов и/или типов аргументов.

Рис. 4. Класс SqlService

Public Class SqlService

• //_ _„-____„_ _.__ _


'// Объявления переменных уровни класса
'//
Private m_sUsername As String * " '// имя пользователи
Private m_sP3Ssword As String - // пароль
Private ra_sServer As String = "" '// экземпляр SQL Server
Private ift_sDatabase As String = "" '// имя базы данных
Private m^sConnectionStfing As String - "" "// строка подключения

'//- Массив для хранений параметров хранимой процедуры


Private ffl_oParmList As ArrayList ~ New ArrayListO

Всего для класса SqlService определено три конструктора. Первый не име-


ет аргументов и поэтому лишь создает экземпляр класса, не настраивая его
свойства. Второй использует как единственный аргумент строку подклю-
чения к базе данных. Если вы передадите конструктору SqlService один
аргумент, его код присвоит значение аргумента строке подключения. Тре-
тий конструктор принимает четыре аргумента: имя сервера базы данных,
имя базы данных, идентификатор пользователя (имя) и его пароль (рис. 5).

У класса SqlService есть также открытые свойства. Я определил их, чтобы


иметь альтернативный способ для задания значений строки подключения
из вызывающей программы — без использования конструкторов.
ADO.NET: концепции и реализация 91

Рис. 5. Конструкторы класса SqIService

"U Конструктор класса (без аргументов)


'// Перегружаемый; да
7/
Sub NewO
End Sub

'// Конструктор класса (аргумент - полная строка подключении)


'// Перегружаемый; да
7/
Sub Hew(ByV8l sConnectionString As String)
ffl_sCoRnectionString - sConneetionString
£nd Sub

V/ Конструктор класса (аргументы - элементы строки подключения)


V/ Перегружаемый; да
'//
Sub New(8yVal aServer As String, ByVal sOatabase AS Stritvg, _
ByVal sysername As String, _
ByVal sPassword As String)
Server = sServer
Database = sDatabase
Userrame = sttsemaee
Password * sPassword
End Sub

RunSql (рис. 6) — перегружаемый метод, который принимает SQL-опера-


тор и имя нужной вам таблицы (объект DataTable), а возвращает DataSet.
Вторая версия RunSql принимает как аргумент только SQL-оператор и
ничего не возвращает (рис. 7). Хотя оба метода выполняют SQL-операто-
ры, первый можно использовать для выборки данных, а второй — для до-
бавления, обновления и удаления данных. Перегрузка RunSql позволяет
создать набор методов с одним именем, и конкретная версия выбирается
в зависимости от переданных параметров. В данном случае, если вы пере-
дадите этому методу только SQL-оператор, будет вызван второй вариант
RunSql.

В методе, который возвращает DataSet (рис. 6), я объявляю и создаю свои


ADO.NET-объекты. Затем настраиваю объект SqlConnection вызовом ме-
тода Connect. (SqlConnection можно считать аналогом объекта ADODB.Con-
nection в ADO 2.x.) После того как свойства SqlConnection настроены, но
соединение еще не открыто, я могу задать свойства объекта SqlComniand.
(Этот объект можно рассматривать как аналог ADODB.Command в ADO
92 Microsoft ADO.NET

2.x.) Затем я присваиваю SqlCommand, созданный в соответствии с пере-


данным SQL-оператором, свойству SelectCommand объекта SqlDataAdap-
ter. Это указывает SqlDataAdapcer выполнить SqlCommand при выборке
данных. Существуют и другие типы команд (InsertCommand, UpdateCom-
inand и DeleteComniand), но на них я остановлюсь в следующих статьях.
В завершение я вызываю метод Fill объекта SqlDataAdapter, чтобы запол-
нить DataSet данными из набора результатов, полученного при выполне-
нии команды.

Рис. 6. RunSql, возвращающий Dataset


V/ Открытый иетод
V/ Перегружаемый: да
'// Возвращаемое значение: DataSet
'// Назначение: выполняет SQL-оператор
Public Overloads Function RunSqKByVal sSql As String, _.
ByVal sTableName As String) As DataSet

Dim oCrad As SqlCommand = New SqlCoismandO


Dim oCn As SqlConnection = Nothing '// объявить SqlCormection
Dim oOa As SqlDataAdapter = New SqlDataAdapterO
Dim oOs As DataSet = New DataSetO

'//- Подготовить соединение с базой данных


oCn = ConnectO

With oCffid
'//- Установить CommamjText, Connection и СошпапоТуре
'//- для объекта SqlCommand
.Connection - oCn
. ConiBiandText = sSql
.ComrnandType = CommandType.Text
End With

With oDa
'//- Назначить объект SqJGomruand для SqlDataAdapter
.SeleotCommand = oCmd

'//- Выполнить SQL-оператор и заполнить DataSet


.Fill(oDs, sTableName)
End With

*//- Отсоединиться от базы данных


Disconnect(oCn) .

'//- вернуть DataSet


Return oDs

End Function
ADO,NET: концепций я реализация

Рис. 7, RunSql для запросов, не возвращающих данные


V/ Открытый метод
"// Перегружаемый: да
'// Возвращаемое значение: «ет
V/ Назначение: выполняет SQL-оператор и ничего не возвращает
Public Overloads Sub RunSqKByVal sSql As String)

Dim oCind As SqlCoMand = New SqlCommandO


Dim oCn As SqlCorvnection = Nothing '// объявить SqlConnection

'//- Подготовить соединение с базой данных


oCn * Connect{)

With oGuid
'//- Выполнить SOt-one ратор
,CommandText = sSql
.Connection = oCn
.CommandType = ComrnandType.Text
. ExecuteNonQue ry()
End With

'//- Отсоединиться от базы данных


Disconnect(oCn)
End Sub

Объект SqlDataAdapter (в ADO.NET Beta 1 он назывался SqlDataCom-


mand) не имеет прямого аналога в архитектуре ADO 1.x. On играет роль
механизма управления четырьмя командами (SELECT, INSERT, UPDATE
и DELETE), которые можно выполнить над DataSet. SqlDataAdapter мож-
но считать мостиком между подключеиными объектами (SqlCommand и
SqlConnection) и отсоединенным объектом (DataSet).

DataSet примерно соответствует объекту Recordset в ADO 2.x, так как со-
держит данные, получаемые в результате выполнения запроса. Между тем
DataSet может содержать несколько наборов данных благодаря объектам
DataTable. Например, DataSet может содержать три связанных друг с дру-
гом объекта DataTable (вроде таблиц покупателей, заказов и позиций за-
казов). Кроме того, DataSet не знает, из какого источника в него поступи-
ли данные, что совсем не похоже на объект Recordset в ADO 2.x. По сути,
различий у DataSet и Recordset больше, чем общего. К другим особеннос-
тям DataSet относятся Extended-свойства, позволяющие задавать нестан-
дартные свойства объекта; свойства DataRelation для установления связей
между разными объектами DataTable; и средства работы с XML для экс-
порта в DataSet и импорта из него данных в формате XML.
94 Microsoft ADO.NET

Если вам не нужен результирующий DataSet и вы просто изменяете дан-


ные, используйте тот метод RunSql, который показан на рис. 7. Он прини-
мает SQL-оператор и устанавливает SqlConnection, Затем настраиваются
свойства объекта SqlCommand IT вызывается метод ExecuteNonQuery. Пос-
ледний исполняет SQL-запрос, но при этом информирует ADO.NET, что
результирующих данных он не ожидает. (Это аналогично вызову метода
Connection.Execute в ADO 2.x с параметром adExecuteNoRecords.) Если
вы не производите выборку данных, гораздо эффективнее обращаться к
методу ExecuteNon Query.

У меня также есть две перегружаемые версии метода RunProc. Первая


возвращает DataSet (рис. 8), вторая не возвращает ничего (рис. 9). Пер-
вый RunProc работает почти так же, как метод RunSql за исключением
того, что устанавливает параметры из Array List-списка m_oParmList. Что-
бы пройти по списку параметров, я создаю объект oEnumerator как экзем-
пляр класса lEnumerator. Это позволяет организовать цикл, добавляя па-
раметры в набор Sql Parameters.

Рис. 3. RunProc, возвращающий Dataset


'// Открытый метод
V/ Перегружаемый: да
V/ Возвращаемое значение: DataSet
V/ Назначение: выполняет хранимую процедуру
Public Overloads Function RunProc(ByVal sProcName As String,
ByVal sTableName As String) As DataSet

Din oCrad As SqlCoiBiand = New SqlCommandO


Dim oCn As SqlGonnectiorj = Nothing
Dim oOA As SqlDataAdapter = New SqlDataAdapter()
Dim oDs As DataSet = New DataSetO
: Dim oSqlParameter As SqlParaiaeter = Nothing
Dim oP As Parameter = Nothing

'//- Получить перечислитель елиска параметров


Dim oEnumerator As lEnumerator = m_oPamList.i3etEnuieeratorC)

'//- Подготовить соединение с базой данных


oCn = ConnectO

With oCmd
'//- Настройка CommandText, ActiveConneetlon и CommandType
'//- йля объекта SqlConmtand
.Connection = oCn
.CommandText = sProeName
.CommandType = GommandType.StoredProcedure
End With

см. след. стр.


ADO.NET: концепции и реализация

Рис. 8. RunProc, возвращающий Dataset


'//~ Перебор Parameters в ArrayLtst
Do While (oEnumeratQr.MoveNextO)
oP = Hotting
"//- Получить текущий объект Parameter
oP = Q&Himerator. Current
*//- Создать экземпляр SqlPa remoter
oSqlParameter = ConvertPararaeterToSqlParametef(oP)^
'//- Добавить объект SqlParameter к объекту SqlConmiano;
oCmd. Parameters. Add(oSqlPara«ieter)
Loop

With OOA
'//- Назначить объект SqlCoirmiand для ^IDataAdapter
.SelectCoflimand = oCmti

'//- Выполнить храниму!» процедуру и заполнить BataSet


,Fiil(oDs,
£nd

'//- Отсоединиться от базы данных


DiscoMect{oCR>
V/- Вернуть DataSet
Return oDs
'End fraction

Рис. 9. RunProc для запросов, не возвращающих данные


V/ Открытый метод
V/ Оерегружаешй: да
V/ Возвращаемое значение: нет
'// Назначение; выполняет хранимую процедуру
Public Overloads Sub RunProc(ByVal sProcMame As String)

Dim oCmti As SqlCommand = New SqlCommand()


Dim оСл As SqlCoimeetion = Nothing
Din oSqlParameter As SqlParameter = Nothing
Diffl oP As Paraneter = Nothing

'//- Получить перечислитель для списка параметров


Di* «Enumerator As leniwierator <= Bi_oPariel.ist.eetEnuBerator{)
т
//» Подготовить соединение с -базой данных
oCn = Connect О

With
'//- Настрбйка CoaniandText, ActiveCotinection :и CommandType

см. след. стр.


Microsoft ADO.NET

Рис. 9. RunProc для запросов^не возвращающих данные (окончание)

'//- Для обьв1сга SqlGommand


,Connection = oCn
.CommandText = sProcName
. ConwandType = ComBiandType.StoredPrQcedure
. End ltttii

'//- Перебор Parameters a ArrayLlst


Do While (oEnumerator.MoveNextO)
oP = Nothing -
'//- Подучить текущий объект Parameter
of = oEnumerator.Current
V/- Создать экземпляр SqlParameter
oSqlParameter ~ ConvertParameterToSqlPafameter(oP)
'//- Добавить объект SqlParameter к объекту SqlCoramand
oCffld.Parameters.Add(oSqlParameter)
Loop

'//- Выполнить хранимую процедуру


oCmd.ExecuteNonQueryQ

'//- Отсоединиться от базы данных


Disconnect(oCn)
End Sub

Данный цикл выполняется, пока метод MoveNext класса lEnumerator не


возвращает false, т. е. когда параметров в ArrayList больше нет. Индекс
текущего параметра из списка присваивается переменной оР. Затем я
преобразую этот параметр в формат, понятный ADO.NET, т. е. в объект
SqlParameter. В завершение я добавляю объект SqlParameter к набору
SqlParameters объекта SqlCommand и продолжаю цикл. После того как все
параметры добавлены в набор, я выдаю SQL-запрос и заполняю DataSet
данными, вызывая методы Select Command и Fill объекта SqlDataAdapter
так же, как и в RimSql.

Второй метод RunProc (рис. 9) служит для выполнения запросов, не воз-


вращающих DataSet. Здесь я опять указываю ключевое слово Overloads,
сообщая Visual Basic, что этот метод следует вызывать при передаче одно-
го параметра. При передаче двух параметров вызывается другой метод
RunProc (рис. 8).

Метод AddParameter (рис. 10) применяется, когда нужно передать пара-


метры классу SqlService при вызове хранимой процедуры. Этот метод при-
нимает имя параметра, его тип данных, длину и значение. Эти значения
помещаются в локальный объект Parameter, который добавляется к Array-
ADO.NETt концепции и реализация 97

List — тому самому списку, который я просматриваю в методе RunProc,


Заметьте: я преобразую полученный тип данных (он соответствует одно-
му из значений перечислителя ssenuinSqlDataTypes) в соответствующий
тип SqlDBType.

Рис. 10. Метод AddParameter


V/ вткрьгтый метод
V/ Пе.регружае«ый: да
'// Возвращаемое значение: нет
V/ Назначение: добавляет параметр хранимой процедуры
Public Sub AddParameterCByVal sParameterNante As String, „
, ByVal ISqlType As ssemiBiSqlDataTypes» _
ByVal iSize As Integer, ByVal sValue As String)

г OiiB eQatalype As SqlDbType


Dim оРатат As Parameter = Nothing

Select Case ISqlType


Case ssenuffl$qlDataTypes.ssSDT_String
eDataType = SqlQbType.VarChar
Case ssenu№SqlOataTypes,ssSOT__Integer
eDataType = SqlDDType.Int
Case ssenumSqlDataTypes.ssSDT_OateTime
eDataType = SqlDbType.BateTine
Case ssenwiSqlDataTypes.ssSDTjJit
eOataType = SqlDbType.Bit
Case ssenuinSqlBataTypes. ssSDT_Decifial
eDataType = SqlDbType.Decimal
Сазе ssenuraSqlOataTypes.ssSDT_Honey
eDataType = SqlDbType.Money
ind Select

оРагав ~ New Parameter(sParameterNaffls, eDataType, iSize, sValue)

и_оРа rmUst. Add< oFa ram)

End Sub

SqIService на С#
Версия SqIService на С# аналогична описанной выше — различия только
в синтаксисе. Полностью код обоих примеров (на Visual Basic и С#) мож-
но скачать с сайта MSDN Magazine (http://msdn.microsoft.com/msdnmag/
issues/01/ll/code/DataOlll.exe). Метод RunSql, реализованный на С#,
выполняет SQL-запрос и возвращает DataSet (рис. 11). Обратите внима-
ние на ключевую особенность: возвращаемое значение здесь указывается

4-5947
Microsoft ADO.NET

типом данных этого открытого метода. Кроме того, метод является пере-
гружаемым (есть две версии RunSql), но явно указывать это (как делает-
ся в Visual Basic ключевым словом Overloads) не нужно. В С# для пере-
грузки метода достаточно определить два метода с разными списками ар-
гументов.

Рие. 11. Метод RunSqJ на С#, возвращающий Dataset


//- Открытый метед
//- Перегружаемый: да
//- Возвращаемое значение: DataSet
//- Назначение: выполняет SQL-оператор
public DataSet RunSql(string sSql, string sTableName)

SqlCommand oCmd = new SqlCommandO;


SqlConnection oCn •= null;
SqlDataAdapter oda = new SqlDataAdapterO;
DataSet oDs = new DataSetf);

//- Подсоединиться к базе данных


oCn = CorvnectO;

//- Настройка CommandText, Connection и CommandType


//- для обьекта SqlComnand
оСшй.Connection = oCn;
oCiBd.CofflinandText ~ sSql;
I.CoifflBandType = CommandType.Text;

//- Назначить объект SqlCommand для SqlDataAdapter


oDa.SelectConimand = oCmd;

//- Выполнить SQL-оператор и заполнить DataSet


oDa.FilKoDs, sTableName};

'//- Отсоединиться от базы данных


DiscorineGt(oCn);

//- Вернуть OataSet


return oDs;

Второй метод RunSql, не возвращающий DataSet, предназначен для запро-


сов, изменяющих данные. Для этого он объявляется как void:
public void RunSql(string sSql)
ADO.NET: концепции и реализация

Метод RunProc на С# выполняет хранимую процедуру и возвращает


DataSet (рис. 12). От версии на Visual Basic он опять же отличается толь-
ко синтаксисом.

Рис. 12. Метод RunProc на С#, возвращающий Dataset


//- Открытый метод
//- Перегружаемый: ва
//- Возвращаемое значение: DataSet
//- Назначение: выполняет хранимую процедуру
public DataSet RunProc{string sProcName, string зТаЬДеКаве)
{
:
SqlCommand oCmd = new SqlCoramandO i
SqlConnection oCn = null;
SqlPararoeter oSqlParameter = null;
SqlDataAdapter oDa - new SqlDataAdapter О ;
DataSet oDs - new DataSetO;
Parameter oP = null;
//- Подучить перечислитель для списка параметров
lEnumerator oEnutnerator * m_oPariBList.uetEnuroerator();

//- Подготовить соединение с базой данных


оСп = Connect();

//- Настройка CofftmandText, ActiveGonnectlon и CommandType


//- для объекта Sql Command
oCnd.CoinmandText = sProcName;
. Connection = oCn;
, CommandType = CommandType. StoredProcedure;.

//- Перебор Parameters a ArrayList


while ( oEnuraerator.MoveNextO )
{
oP = null;
//- Получить текущий объект Paraaeter
oP = (ParaiReter)oEmjmeratQr. Current;
//- Создать экземпляр SqlParameter
oSqlParanteter = ConvertParameterToSqlParameter(oP);
//- Добавить объект SqlParaieeter к объекту SqlCommand
oCmd,Parameters.Add(oSqlParaaeter};

//- Назначить объект SqlCoiranarnJ для SqlBataAdapter


oDa.SelectComfliand - oCid;

'//- выполнить храниму» процедуру и заполнить DataSet


oDa.Fill(oOs, sTableName);

см. след. стр.


100 Microsoft ADO.NET

Рис. 12. Метод RunProc на C$, возвращающий Dataset (окончание}


//- бтеоединитьей от базы данных
DlsconnGct(oCn);

//- Вернуть DataSet


retarn oDs;

Второй метод RunProc, не возвращающий DataSet, предназначен для зап-


росов, изменяющих данные, Поскольку он не возвращает значений, то
объявляется как void:
public void RunProc(string sProcName)

Выполнение кода
Ознакомившись с кодом, остается загрузить страницу WebForm.aspx
(рис. 13). (Ее внешний вид одинаков для обеих версий — на Visual Basic
и С#.) Поскольку данные привязаны к элементу управления DataGrid, вам
не нужно заботиться о переборе всех записей и их отображении в HTML.

F-gyortes Vcots He p

J ]

3, ccTFeeSj tea;, bea =, aid aies


Cnndirfienrs 'Sweet and s^v-jry чаькм, r*iESt;fs, swssttr, srii se^scnings
3 Confections Desserts, candies, and «wet b'eads
4 Dairy Products Cheeses
5 Grehs^Cereals Breads, crackers, posts, snd cc'tal
6 MeaLJPuJtri/ Prepared riieuU
7 РггАям Dried fruit and bean (urd
;S Seafood ''зевиеей and Hih

Exanple of fisecutmg a Stored Procedure and Pi]Jing a Grid

Longlf* Tofu -lie


Dried uppies 1J090

Sob's Organic Dried Рел s; 12336

Рис. 13. WebForm.aspx


ADO.NET; концепции и реализация 3.01

Заключение
Компонент доступа к данным, описанный в этой статье, выносит код, от-
носящийся к ADO.NET, в отдельные пространство имен и класс. Это по-
зволяет разделить ADO.NET-код и бизнес-логику, а также унифицировать
доступ к данным через ADO.NET.

Разумеется, это пространство имен можно расширить, включив сюда и


пространство имен System.Data.OleDb. Б приведенных примерах исполь-
зуется пространство имен SqIClient. которое при работе с SQL Server обес-
печивает большее быстродействие в сравнении с пространством имен
OleDb. Но если вашим приложениям нужен доступ к базам данных под
управлением СУБД, отличной от SQL Server, можно запросто создать
SqlService, использующий пространство имен System.Data.OleDb (воз-
можно, совместно с SqIClient). Выбор за вами, но я рекомендую простран-
ство имен SqIClient, если вы работаете только с SQL Server.

В следующих публикациях я подробнее расскажу о методиках работы с


ADO.NET, в том числе о применении четырех разных команд SqlDataAd-
pater, об использовании объектов DataSet для обмена данными между
уровнем представления (presentation layer) и уровнем бизнес-логики
(business layer), а также об установлении связей (relations) между несколь-
кими таблицами, представленными в DataSet объектами DataTable.

Джонни Папа (Johnny Papa) — вице-президент компании MJM Investigations


по разработке программного обеспечения (Роли, штат Северная Каролина),
автор нескольких книг по ADO, XML и SQL Server. Часто выступает на конфе-
ренциях, в том числе на VSLive. С ним можно связаться по адресу
datapoints@lancelotweb.com.
Боб Бьючмин

Разработка собственных
провайдеров данных
для .NET Data Access
Framework*

В сборке .NET Framework System.Data.dll содержатся пространства имен, где


определены базовые классы, используемые при создании нестандартных
провайдеров данных (custom data providers). Кроме того, в этих простран-
ствах имен определено несколько интерфейсов доступа к данным и базовых
классов, позволяющих создавать провайдеры данных, способных взаимодей-
ствовать с другими нестандартными провайдерами.

С помощью ADO.NET-классов Connection, Command, DataReader и DataAdapter


написать провайдер ADO.NET легче, чем провайдер OLE DB. В статье расска-
зывается об этих классах и их реализации, а также о том, как на их основе
создавать провайдеры данных разных видов.

Windows-платформы уже давно включают API доступа к данным, осно-


ванные на универсальной парадигме «провайдер-потребитель» (provider-
consumer). Первым появился ODBC (Open Database Connectivity) API,
ориентированный на источники реляционных данных. ODBC почти одно-
значно соответствовал интерфейсу SQL CLI (Call-Level Interface), опреде-
ленному комитетом стандартизации ANSI SQL. ODBC-драйверы либо
инкапсулируют протоколы, специфичные для конкретных баз данных,
вроде TDS (tabular data stream), используемого SQL Server, либо образу-
ют уровень, размещаемый поверх API, специфичных для конкретных по-

* Публиковалось в MSDN Magazine. 2001. №12 (декабрь). — Прим. изд.


Разработка собственных провайдеров данных для .NET Data Access Framework ЮЗ

ставщиков, например поверх OCI (Oracle Call Interface). Затем появился


OLE DB — универсальный набор СОМ-интерфейсов и типов, распростра-
нивший возможности ODBC на источники нереляционных данных. Так
как OLE DB позволял работать с источниками любых нереляционных
данных, создатели любых видов информации — от плоских (flat) файлов
до иерархических и многомерных данных — были заинтересованы в общей
объектной модели OLE DB.

Microsoft .NET поставляется со сборкой System.Data.d 11, содержащей про-


странства имен, которые напоминают OLE DB и ODBC. В ее пространстве
имен Systeni.Data определен общий набор интерфейсов доступа к данным,
а в System.Data.Common присутствует несколько абстрактных базовых
классов, реализующих функциональность, общую для всех провайдеров.
Кроме того, в System.Data.dll входят два пространства имен (System.Da-
ta.OleDb и System.Data.SqlClient), которые предоставляют параллельные
наборы классов и интерфейсов, называемые в документации провайдера-
ми данных. Провайдеры данных инкапсулируют в наборах управляемых
классов и интерфейсов средства взаимодействия с базами данных и дру-
гие API-функции доступа к данным. Типы и интерфейсы, предоставляе-
мые провайдерами данных, наследуются от общего подмножества и обес-
печивают почти эквивалентную (но не идентичную) функциональность.
Так как не требуется, чтобы у всех провайдеров были одинаковые типы и
интерфейсы, разработчики провайдеров могут реализовать функциональ-
ность, уникальную для источника данных. Кроме того, System.Data под-
держивает модель отсоединенных объектов данных (disconnected object
model), основанную на типе DataSet, который является более совершенной
версией библиотеки клиентских курсоров ODBC и отсоединенного объек-
та Recordset (OLE DB).

В документации на .NET SDK Beta 2 поясняется, как разрабатывать про-


вайдеры данных, какие типы и интерфейсы содержит стандартный про-
вайдер и как использовать методы интерфейсов, чтобы обеспечить взаимо-
действие с нестандартными провайдерами данных. Там же дан пример
провайдера для доступа к нестандартному хранилищу данных. Информа-
ции, приведенной в документации, вполне достаточно, чтобы понять, как ре-
ализуются стандартные типы и интерфейсы, определенные в System.Data, и
используются вспомогательные базовые классы из System.Data.Common.

Я покажу простейший пример провайдера данных .NET, предоставляюще-


го стандартный набор типов и интерфейсов. На основе этого примера мы
исследуем объектную модель провайдера данных, отделим базовую функ-
циональность от расширенной и рассмотрим некоторые расширения, спе-
цифичные для конкретных провайдеров. Я также расскажу о причинах, по
которым доступ к данным предоставляется через провайдеры, и об альтер-
104 Microsoft ADO.NET

нативах провайдерам данных. Учтите, что код для этой статьи был напи-
сан в расчете на .NET Beta 2.

Зачем создавать провайдеры данных?


Во времена OLE DB и ODBC драйверы и провайдеры разрабатывались по
разным причинам. Реализовав провайдер, вы получали возможность рабо-
тать с наборами данных через стандартные GUI-элементы. Программы
генерации отчетов, обмена данными и другие аналогичные приложения
использовали один и тот же API. Стандартом обмена данными де-факто
стали ADO Recordset и RDS (Remote Data Services), поэтому многие груп-
пы разработчиков создавали провайдеры, предоставляющие данные толь-
ко в виде ADO-объектов Recordset. Появилась даже перспектива создания
клиентов, работающих по принципу «написан однажды, используется с
чем угодно». Провайдеры и драйверы создавались, даже если способ дос-
тупа к данным мало соответствовал или вообще не соответствовал объек-
тной модели. Так, был разработан ODBC-драйвер для доступа к текстовым
файлам через SQL и архитектуру OLE DB Simple Provider.

Различные среды визуальной разработки для Windows, например Visual


Basic, предоставляли доступ к данным через элементы управления, связы-
ваемые с данными (data-bound controls). Чтобы провайдеры и драйверы
могли работать с такими элементами управления, вы должны были писать
их в соответствии с дополнительным набором правил и реализовать в них
поддержку стандартных объектов и интерфейсов, Последней из таких спе-
цификаций для разработчиков провайдеров OLE DB и элементов управ-
ления, связываемых с данными, была ActiveX Control Writer's Specifica-
tion for OLE DB.

Провайдеры ODBC и OLE DB удобны тем, что их можно напрямую ис-


пользовать в приложениях Microsoft и сторонних фирм. Например, в
Microsoft Access можно задействовать любой ODBC-драйвер для прямого
доступа к данным — тогда Access-приложения работают с ними так, будто
получают их из базы данных Access. SQL Server и DB/2 (в Windows NT)
позволяют применять источники данных OLE DB в распределенных зап-
росах. Кроме того, начиная с SQL Server 7.0, любой провайдер OLE DB
можно использовать для импорта, экспорта и преобразования данных че-
рез DTS (Data Transformation Services). В Crystal Reports ODBC-драйве-
ры или ADO-объекты Recordset служат для получения входных данных,
из которых формируется отчет. В других приложениях с помощью ADO
или ODBC генерируются OLAP-данные или XML-схемы. Эти и другие
примеры показывают, что разработка провайдеров OLE DB или ODBC-
драйверов весьма перспективна.
Разработка собственных провайдеров данных для .NET Data Access Framework 105

Создание провайдера данных


В Visual C++ 6.0 Microsoft ввела набор классов на основе ATL (ActiveX
Template Library), позволяющий разрабатывать провайдеры OLE DB. С
ATL-шаблонами провайдеров OLE DB поставлялся мастер (wizard), кото-
рый использовал инфраструктуру ATL и генерировал пример провайдера,
предоставляющего доступ к структуре WIN32_FIND_DATA (с информа-
цией о файлах и каталогах) через соклассы (cotypes) и интерфейсы OLE
DB. Я настолько часто применял этот провайдер в демонстрационных це-
лях, что даже присвоил ему стандартное имя: DirProv — провайдер OLE
DB для доступа к информации о каталогах. Поскольку в .NET есть клас-
сы со схожей функциональностью, я переделаю тот пример в провайдер
данных и назову его «управляемым dirprov» (managed dirprov).

Написать провайдер данных для ADO.NET гораздо проще, чем для OLE
DB. Интерфейсы в ADO.NET четко определены; создавая класс с опреде-
ленной функциональностью, вы должны реализовать соответствующий
интерфейс. При этом нужно реализовать минимум четыре основных клас-
са: Connection, Command, DataReader и DataAdapter. Вкратце рассмотрим
эти классы.

Connection Этот класс обязателен, даже если вы не собираетесь на деле


подключаться к источнику данных. Он нужен другим классам, так как
предоставляет им базовую функциональность. DataAdapter, например,
вызывает методы Open и Close класса Connection при заполнении Data-
Table, входящего в DataSet.

Command У класса Command как минимум два предназначения. В язы-


ках, используемых при операциях с данными, команды позволяют напря-
мую обновлять хранилища данных. Скажем, в SQL такими командами яв-
ляются INSERT, UPDATE и DELETE. Кроме того, можно передавать коман-
ды, возвращающие результаты. Пример такой команды в SQL — SELECT.

DataReader Класс DataReader используется для обработки результатов,


возвращаемых Command. Его методы позволяют перебирать записи одно-
го или нескольких наборов результатов с перемещением только вперед
(forward-only). Кроме того, он предоставляет методы для считывания дан-
ных из полей этих записей в переменные .NET-типов.

DataAdapter Класс DataAdapter заполняет DataSet результатами, воз-


вращаемыми классом Command, а также позволяет обновлять данные с по-
мощью команд и предоставляет событийную модель для операций обнов-
ления, требующих тонкой настройки.

Эти обязательные классы показаны на рис. 1; там же отражены некоторые


взаимосвязи между ними. Некоторые, но не все — например, класс Com-
106 Microsoft ADO.NET

mand может быть создан не только методом Connection. Create Command,


но и указанием Connection в одной из перегруженных версий конструкто-
ра класса Command. Поскольку ряд классов зависит друг от друга, на ил-
люстрации представлен минимальный набор классов, реализуемых на
практике. Для начала я реализовал провайдер, содержащий эти классы и
поддерживающий только чтение. Создав базовую функциональность (рис. 1),
дополнительные сервисы можно разместить поверх существующих обяза-
тельных классов или определить через вспомогательные классы. Обо всем
этом я и расскажу в статье.

IDbConneclion CHH IDbCommand


Connection
Created о mm and

ctor(Command) ExecuteReader

IDbDalaAdapter О-Ц IDbOataReader

тжввввтвтгявчячч
IDbDataRecord

Рис. 1. Некоторые взаимосвязи между классами провайдера данных

Класс Connection
До реализации класса Connection надо реализовать интерфейс IDbConnec-
tion. В IDbConnection (рис. 2) шесть открытых методов, среди которых
наиболее очевидны Open и Close. Так как в исполняющей среде .NET не
поддерживается концепция детерминированной деструкции (deterministic
destruction), вы должны явно вызывать метод Close, а не просто освобож-
дать все указатели на интерфейс, как это можно было бы сделать в OLE
DB. Метод ChangeDatabase используется, если ваш источник данных дол-
жен поддерживать подключение к другой базе данных.

Метод BeginTransaction запускает локальную транзакцию. BeginTransac-


tion возвращает интерфейс IDbTransaction, через который вы вызываете
методы Commit или Rollback. Имеется перегруженная версия BeginTran-
saction, принимающая в качестве параметра уровень изоляции транзакции
(transaction isolation level). Если ваш источник данных не поддерживает
локальные транзакции, реализовать метод BeginTransaction не нужно.
Наконец, IDbCreateCommand создает объект Command вашего провайде-
ра и возращает ссылку на интерфейс IDbCommand,

У IDbConnection. четыре открытых свойства. ConnectionString, Connec-


tionTimeout и Database — это наиболее часто используемые свойства, от-
носящиеся к соединениям. Четвертое открытое свойство, State, крайне
важно. Оно доступно только для чтения и возвращает значение перечис-
лимого ConnectionState (табл. 1). Смысл значений Open и Closed очеви-
Разработка собственных провайдеров данных для .NET Data Access Framework 107

ден. Они используются, если ваш источник данных поддерживает асинх-


ронную инициализацию, a Fetching применяется, если источник данных
поддерживает асинхронную выборку. Broken соответствует состоянию,
когда соединение открыто, но не работоспособно, например, если исполь-
зуемая вами база данных завершает работу по указанию системного адми-
нистратора.

Рис. 2. Интерфейс iDbConnectipn


public interface IDbConnection
{
IQbTransaction BeginTrartsaction(IsolationLevel iso)
IDbTransaction 8egin.Transaetion()
bool ChaflgeDatabase(striRg newdb)
; void CloseO
IPbCoiamand CreateComroandO
void Qpert()

// Свойства
string ConnectionString // get, set
int ConnectlonTimeout // get, set
string Database // get, set
Connect!onState State // set

Табл. 1. Перечислимое ConnectionState

Член Описание

Broken Связь разорвана. Это возможно только после открытия соеди-


нения. Если соединение находится в таком состоянии,
его можно закрыть, а потом открыть снова.
Closed Объект закрыт.
Connecting Выполняется подключение к объекту.
Executing Объект выполняет команду.
Fetching Выполняется выборка данных.
Open Объект открыт.

Реализация класса Connection


Чтобы реализовать интерфейс IDbConnection, определитесь, к какому ис-
точнику данных вы будете подключаться и какая информация вам нужна.
Если требуемая информация соответствует используемой базе данных,
тем лучше. Если нет, придется включать дополнительные свойства и поля.
Для получения примитивной информации о каталогах я реализовал про-
стые методы Open и Close (они лишь присваивают значения свойству
108 Microsoft ADO.NET

ConnectionState), а формированием строки подключения, выбором теку-


щей базы данных и созданием транзакций я, естественно, не занимался.

Класс Connection должен разрешать создание своих экземпляров другими


классами. Следует написать минимум два конструктора: не принимающий
аргументов и принимающий аргумент типа String (строку подключения).
Так как мой провайдер данных не использует строку подключения, я реа-
лизую только первый конструктор. Состояние создаваемого объекта Con-
nection должно инициализироваться значением ConnectionState.Closed;
для этого закрытой переменной _ConnectionState присваивается соответ-
ствующее значение.

При реализации я придерживался нескольких простых соглашений. Име-


на закрытых полей, к которым обращаются свойства, начинаются со зна-
ка подчеркивания. Когда функция не поддерживается (NotSupported) или
не реализована (Notlmplernented), генерируется исключение вызовом со-
ответствующего закрытого метода. Реализация MDirConnection включена
в набор исходного кода, который можно скачать с сайта MSDN Magazine
по ссылке http://msdn.microsoft.com/msdnmag/code01.asp в разделе за де-
кабрь.

Специфичная функциональность в Connection


Спецификация OLE DB позволяла разработчикам провайдеров реализо-
вать нестандартные интерфейсы, специфичные для конкретных провайде-
ров. То же самое можно делать и при создании .NET-провайдеров данных.
Кроме того, вы можете реализовать методы экземпляра (или их перегру-
женные версии), специфичные для провайдера. Как и в случае OLE DB,
универсальный клиент может выбирать, использовать ли ему методы, спе-
цифичные для провайдера. Самый надежный способ распознавания под-
держиваемых возможностей в период выполнения — приведение типов
интерфейсов/классов. Для этого можно использовать интерфейсы-марке-
ры (marker interfaces).

Специализированные клиенты могут находить специфичные для провай-


дера методы, перегруженные версии и свойства через отражение (reflec-
tion), не зная заранее об их наличии, и получать доступ к ним в соответ-
ствии с разрешениями. Так как все эти действия производятся в период
выполнения, ни один метод не может заблаговременно сообщить потреби-
телю сведения, касающиеся семантики использования специфичной фун-
кциональности.

Примером специфичной функциональности класса Connection является


набор параметров, передаваемых в строке подключения в случае SqlClient.
Кроме стандартных параметров вроде Data Source, User ID и Password (за-
Разработка собственных провайдеров данных для .NET Data Access Framework 109

имствованных из спецификации OLE DB), SqlClient поддерживает пара-


метры, специфичные для SQL Server (например, Network Library и размер
буфера TDS).

Более сложный пример специфичной функциональности — реализация


пула соединений, повышающего эффективность совместного использова-
ния соединений в трехуровневой среде, например в случае Web-сервера. И
OleDb, и SqlClient поддерживают создание пула соединений, но семанти-
ка этой поддержки различна.

Класс Command
Класс Command должен реализовать интерфейс IDbCommand (рис. 3),
Основное назначение IDbCommand — отправлять команды или запросы
хранилищу данных. Команды, изменяющие данные, но не генерирующие
набор результатов, передаются методом IDbCommand.ExecuteNonQuery,
возвращающим общее число записей, на которые воздействовала команда.
Команды-запросы возвращают наборы записей через класс DataReader.
Метод IDbCommand.ExecuteReader вовзращает интерфейс IDataReader
класса DataReader. Метод ExecuteScalar используется, чтобы получить
первое поле первой записи, но применим и для получения скалярного ре-
зультата. Возможное применение этого метода — чтение набора записей,
который на самом деле является целым экземпляром объекта или доку-
ментом.

Для IDbCommand.ExecuteReader имеется перегруженная версия, прини-


мающая параметр CommandBehavior. Этот параметр указывает, что долж-
но происходить при выполнении команды. Значения перечислимого Com-
mandBehavior показаны в табл. 2.

Табл. 2. Перечислимое CommandBehavior

Член Описание

CloseConnection При выполнении команды связанный с ней объект


Connection закрывается, когда закрывается сопоставленный
с той же командой объект DataReader
Keylnfo Запрос возвращает информацию о полях и первичном ключе
SchernaOnly Запрос возвращает информацию только о полях и не влияет
на состояние базы данных
SequentialAccess Результаты считываются последовательно на уровне полей
SingleResult Запрос возвращает один набор результатов; выполнение
запроса может повлиять на состояние базы данных
SinglcRow Ожидается, что запрос вернет одну запись; выполнение
запроса может повлиять на состояние базы данных
110 Microsoft ADO.NET

Рис. 3. Интерфейс IDbCommand


public interface IDbCommand

public void CancelO


public I&ataParameter CreatePararaeterO
public int ExeeuteNonQueryO
public IDataReader ExecuteReaeferQ
public IDataReader ExecutefieaderfCommandBehavior b)
public object ExecuteScalar()

public string CoairoandText // get, set


public int CofflfflandTimeotit // get, set
public ComaancfType CoioiandType // get, set
public XDbCdnnection Connection // get, set
public IDataParameterColleetion Parameters // get
public IDbTransaction Transaction // get, set
public UpdateftowSource UpdatedRowSouree // get, set

Для команд необходимо задавать свойства CommandText (саму команду)


и CommandType. Эти свойства являются открытыми. Кроме того, как от-
мечается в документации, строку CommandText и экземпляр класса Con-
nection можно указать в перегруженной версии конструктора. Единствен-
ный CommandType, который обязательно нужно поддерживать, — это
CommandType.CommandText. Другие типы служат для работы с хранимы-
ми процедурами и использования имен таблиц вместо команд (последнее
только в OLE DB). Команды можно отменять напрямую (методом Cancel)
или по истечении срока ожидания (задаваемого свойством Command-
Timeout).

Типы команд содержат ссылки на другие типы объектной модели. С объ-


ектом Command нужно связать объект Connection (как ссылку на IDbCon-
nection), указав на соединение в конструкторе или напрямую задав соот-
ветствующее свойство. При необходимости с командой можно сопоставить
объект Transaction (как ссылку на IDbTransaction). Transaction также за-
дается в конструкторе или наследуется от нижележащего объекта Con-
nection.

Хранимые процедуры или параметризованные запросы используют набо-


ры параметров. Для работы с параметрами предназначен .NET-тип, явля-
ющийся набором (collection). Набор Parameters — это свойство класса
Command, который включает и метод Create Parameter. Наконец, Updated-
RowSouree указывает, какая версия DataRow используется при обновле-
ниях, выполняемых через класс DataAdapter.
Разработка собственных провайдеров данных для ,NET Data Access Framework

Реализация класса Command


В моем примере провайдера данных в качестве входных данных использу-
ется имя каталога. Для этого достаточно типа команды CommandText.
Поскольку мне не нужны транзакции, параметризованные запросы и воз-
можность отмены команды, основную часть реализации составляют мето-
ды выполнения команды. ExecuteNonQuery и ExecuteReader создают эк-
земпляр DataReader, вызывают его внутренний метод GetDirectory и воз-
вращают либо число элементов в каталоге (количество обработанных
записей), либо DataReader, обрабатывающий эти записи.

Рис. 4. ExecuteReader
public IDataReader ExecuteReader(CominandBehaviQr t>)
{
Debug.WriteLine("MBirCoimancl.ExecuteReader(b)", "MdirCommand"};

// Соединение должно быть установлено и открыто


if ^Connection == sull II .Connection.State !=
ConnectionState.Open)
throw new InvalidOperationExceptionC"
Connection must be valid and open"};

if ((b & CoKiBand8etiavior.KeyInfo) > 0)


Debug.WriteLine("Behavior includes Keylnfo"};

if ((b & CoieiandieftavlQr.SchemaQnly) > 0}


Debug.WriteLinet"Behavior in-cludes Schema&nly");

// Обрабатываем только CloseConrtection или "все остальное"


if <(Ь & CoiemaniiBehavior.CloseConnection) > 0)
{
De&ug.WriteUneC'Behavior Inelttctes CloseCotiRection");
HDirDataReader reader = new HDirDataReader(_CominandText,
„Connection);
reade r. GetOi recto ry („CoiwnandText);
return reader;
I
else
{
HDirCataReader reader = new MDirDataReader(_ComfflandText);
, reade r. Get Di recto ry(_ConnnandText);
return reader;

Самый сложный в программировании — перегруженный мегод Execute-


Reader, принимающий параметр типа Command Behavior. Поскольку неко-
j.12 Microsoft ADO.NET

торые значения CommandBehavior можно объединять логической опера-


цией «или», при реализации метода приходится выполнять побитовую
проверку параметра. Хотя можно указать Keylnfo или SchemaOnly, инфор-
мация о схеме все равно возвращается вместе с данными (через метод
GetSchemaTable класса DataReader). Особый случай — CloseConnection.
При этом значении вызывается конструктор DataReader, который прини-
мает объект Connection, используемый командой. А когда вызывается ме-
тод Close класса DataReader, закрывается и связанный с командой объект
Connection. Реализация метода ExecuteReader показана на рис. 4.

Специфичная функциональность в Command


Провайдеры данных SqlCIient и OleDb реализуют почти всю функцио-
нальность IDbCommand. Так как базы данных во многих случаях быстрее
генерируют отдельные результаты, а не наборы, оба провайдера поддержи-
вают для CommandBehavior значение SingleRow. В обоих провайдерах ре-
ализован метод Prepare, позволяющий отправить базе данных отдельную
команду для анализа запроса и подготовки плана его выполнения. Кроме
того, SqlCIient уникальным образом расширяет возможности класса Com-
mand. Поскольку SQL Server 2000 может возвращать результаты запроса
в виде потока XML-данных, SqlCIient содержит открытый метод Execute-
XmlReader, возвращающий XmlReader вместо IDataReader.

Класс DataReader
У класса DataReader нет открытого конструктора. Этот класс должен реа-
лизовать интерфейсы IDataReader и IDataRecord. Класс DataReader по-
зволяет читать данные по одной записи единовременно в направлении
только вперед и получать значения полей извлеченных записей как типи-
зированные или обобщенные типы данных. Определения интерфейсов
IDataReader и IDataRecord показаны на рис. 5.

IDataReader дает возможность проходить набор результатов методом


Read, который возвращает false, если достигнут конец набора записей. При
выполнении одной команды может возвращаться несколько наборов ре-
зультатов. Для перехода к следующему набору результатов используется
метод NextResult.

Иногда нужно получить описание информации, содержащейся в наборе


результатов, — так называемые метаданные. Метод GetSchemaTable интер-
фейса IDataReader может возвращать DataTable с метаданными для каж-
дого набора результатов. Таблица, возвращаемая GetSchemaTable, содер-
жит как стандартную информацию, так и специфичную для провайдера,
Разработка собственных провайдеров данных для .NET Data Access Framework 113

Рис. 5. Интерфейсы IDataReader и IDataRecord


public interface tuataReader
\
Ц Иетода IDataReader
public BataTable GetScheiftaTable< }
public void Close()
public bool Nextflesult(>
public bool ReadO

// Свойства IDatafieader
public int Depth
public bool IsGlosed
public int Records Affected

public interface IDataRecord


{
•// Методы IDataRecord
public bool Set&Qoleafl{iRt i)
public byte QetSyteCint i)
public int Get8ytes(int i, int fieldoffset, byteH buffer,
int length, int bufferoffset)
putilic char SetChar(int i)
public int GetCharsfirvt i, int fielaoffset, chart] buffer,
Int length, int bufferoffset)
public IDataReader GetCata(int i)
public string 6etOataTypeNarne(int i)
public DateTime 6etDateTiroe(int i)
public decimal QetDecimaKint i)
public double QetDauble(int i)
public Type GetFleldType(int i)
public float SetFloatCint i)
public Quid 6etGuid(int i)
public short Setlntl6(int i)
public int Getlnt32(int i)
public long 6etlnte4{int i)
public string 6etNafne(int i)
public int GetOrdinal(string name)
public string QetString(int i)
public object GetValue(int i)
public int GetValuesCobjectC] values)
public bool IsDBHulldnt i)
// Свойства IDataRecord
public int FleldCount
public object this[string nafae];
public object this[int 1]
12,4 Microsoft ADO.NET

IDataRecord предоставляет методы, позволяющие провайдерам возвра-


щать строго или слабо типизированные данные из каждого поля. Суще-
ствует ряд строго типизированных методов-получателей (getters), прини-
мающих порядковый номер поля с отсчетом от нуля, а есть и универсаль-
ные методы GetValue и GetValues, возвращающие значения типа object.
IDataRecord всегда возвращает управляемые .NET-типы; таким образом,
этот интерфейс инкапсулирует проекцию системы типов источника дан-
ных на систему .NET-типов. Кроме того, IDataRecord содержит перечис-
лители (iterators) полей, доступные как перегруженные версии свойства
Item. Эти перечислители возвращают поле по имени или порядковому
номеру с отсчетом от нуля.

Реализация DataReader
Класс DataReader содержит специфичный для провайдера метод Get-
Directory, который вызывает управляемый метод System.IO.Directory-
Info. GetFileSysternlnfos. Провайдер данных предоставляет доступ к под-
множеству возвращаемой этим методом информации, позволяя получить
поля Name, Size, Type (файл или подкаталог) и CreationDate. Name и Туре
имеют тип String, Size — Int64, a CreateDate — DateTime. Поскольку в каж-
дом наборе результатов возвращаются одни и те же метаданные, результа-
ты помещаются в четыре массива: типов (управляемый тип каждого поля),
размеров (размеры этих типов), имен (имена полей) и полей (значения
типа object). Хотя элементами массива полей являются объекты, каждое
поле — это экземпляр соответствующего типа, т. е. в отличие от ADO поля
не представляются типом Variant. Поскольку метаданные всегда одни и те
же, получение информации о схеме «зашито» в методе GetSchemaTable. За-
щищенная реализация класса DataReader и метода Read показана на рис. 6.

При реализации строго типизированных методов-получателей использу-


ется простое приведение типов. Неправильное приведение типа (напри-
мер, применение Getlnt32 для поля типа String) вызывает генерацию
InvalidCastException, как и в провайдерах данных SqlClient и OleDb. По-
скольку все значения данных уже относятся к управляемым типам, преоб-
разование типов хранилища данных в управляемые типы не требуется.
Свойства Item реализованы с помощью индексаторов (indexers) языка С#.
Провайдер не поддерживает формирование нескольких наборов результа-
тов одной командой.

Чтобы элементы управления, связываемые с данными, могли использо-


вать класс DataReader, этот класс должен реализовать интерфейс lEnume-
rable. Я предпочел реализовать lEnumerable с помощью класса DbEnume-
rator пространства имен System.Data.Common. Провайдеры данных Sql-
Client и OleDb тоже используют этот класс. Чтобы реализовать метод
Get Enumerator интерфейса lEnumerable, я возвращаю экземпляр DbEnu-
Разработка собственных провайдеров данных для .NET Data Access Framework

merator, создаваемый конструктором, в который передается экземпляр


моего класса DataReader:
lEnumerator System.Collections.Innumerable.GetEnumerator()
{
return ((lEnumerator) new DbEnumerator(this));

Рис. 6. Реализация MDirDataReader


// IDataReader.ReadC)
public bool ReadO
i
Debug. WrlteLinerK&lrDataReader. Read", "HdirBataReader");
if (_le i» null)
{
bool notEQF = „ie.MoveNextO;
if (notEOF == true)
{
„CurrentRow++;
if (_fsi[_CurrentRo«] is Filelnfo)
{
FUelnfo f = (Filelnfo)_fsiE_GurrentRow3;
_cols[Q] a f.Narae;
_cols[l] « f.tangttv.ToStringC); ; - - . - •
_cols[2] * "File";
„ео1з£3] = f.CreatiDnTiffle.ToStrlngO;

else

Oireetoryinfo d * (DirectoryInfo)_fsit_GurrentBow];
_cols[0] * d.Hame;
_cols{1] = "0";
_cols[23 * "Directory";
_eols[3] * d. Great ionTiae.ToStringO;

return notEOF;
}
, return false;
}
// Реализация MOir DataReader используется методами
// HDirCoronand, Execute и HOirCoiffiand.ExecuteReader
/*
« Поддерживаем автоматическое закрытие соединения, обрабатывая
* флаг CoBinandfleliavior.GloseConnection. Null задает
* обычное поведение (без автоматического закрытия),
*/

см. след. стр.


116 Microsoft AD0.NET

Рис, 6. Реализация MDirDataReader (окончание)


private IDbConnectiOR „Connection = null;
internal Directorylnfo _dir;
internal meSystemlnfop _fsi;
internal int _GurrentRow;
internal lEnumerator _ie;
internal StringCJ „names * {"Name", "Size", "Type", "CreationDate" };
internal Type[] „types = (typeofCString), typeof(long),
typeof(St ring}, typeof(Dateline)I;
internal object[] _cols = new object[43;
// Максимальный размер в байтах
internal Int32{] „sizes » { 1024, 8, 9, 8 >;
internal void GetDirectory(String command)

_dir = new Plrectorylnfo(coffimand);


_fsi = _dir.GetFileSystemInfos();
„flecordsAffected = „fsi.Length;
_GurrentRow = -t;
_ie = _fsi.GetEnumerator();
isClosed = false;

Специфичная функциональность в DataReader


Провайдеры OleDb и SqlClient no-разному работают с управляемыми ти-
пами. У SqlClient есть перечислимое SqlTypes, а пространство имен Sys-
tem. Data.SqlTypes включает типы, специфичные для SQL Server. Помимо
строго типизированных методов-получателей (например, Getlnt32), пре-
образующих типы данных SQL Server в .NET-типы, в SqlClient имеется
ряд методов-получателей (скажем, GetSqlInt32), которые предоставляют
доступ к «родным» типам SQL Server из пространства имен System.Da-
ta.SqlTypes без всякий преобразований. В документации .NET SDK утвер-
ждается, что при использовании «родных» типов достигается большее
быстродействие, чем при преобразовании в управляемые типы.

Провайдер данных OleDb конвертирует OLE DB DBTYPE в управляемые


типы, поскольку типы, используемые хранилищем данных, отображаются
провайдером на DBTYPE-типы OLE DB. Для нас представляет интерес
только интерфейс IDataReader, возвращаемый методом IDataRecord.Get-
Data. В OLE DB поддерживается концепция разделов (chapter concept), в
соответствии с которой иерархические данные предоставляются через спе-
циальное поле типа chapter, связывающее родительский и дочерний набо-
ры записей (rowsets). IDataRecord.Get Data возвращает DataReader, уста-
навливаемый на дочернюю запись, которая относится к заданной роди-
тельской. Ту же концепцию можно было бы применить и в провайдере
данных MDirProv для рекурсивного обхода подкаталогов файловой системы,
Разработка собственных провайдеров данных для .NET Data Access Framework Ц7

Класс DataAdapter
DataAdapter — один из немногих классов провайдеров данных, который
реализуется на общем базисе. Ваши классы DataAdapter, специфичные для
конкретных провайдеров, наследуются от класса DbDataAdapter, который
в свою очередь является производным от DataAdapter. Эти классы реали-
зуют интерфейсы IDataAdapter (в котором определяются методы Fill и
Update, используемые при взаимодействии с DataSet) и IDbDataAdapter.
Последний предоставляет доступ к четырем объектам Command (Select-
Command, UpdateCommand, InsertCommand и DeleteCommand), определя-
ющим взаимодействие между провайдером и DataSet. Кроме того, класс
DataAdapter содержит стандартный набор событий и делегатов, позволя-
ющих принимать уведомления и влиять на поведение класса до и после
обновлений (под обновлениями подразумеваются операции Insert, Delete
или Update, выполняемые над хранилищем данных). Иерархия классов и
интерфейсов показана на рис. 7.

IDbDataAdapter

t
IDbDataAdapter

Наследование интерфейса t
Наследование класса
I
IDataAdapter
DataAdapter

Рис. 7. Иерархия наследования

Реализация DataAdapter
Реализация DataAdapter провайдера данных DirProv будет «облегчен-
ной», поскольку этот провайдер предоставляет данные только для чтения.
Поддерживается лишь метод SelectCommand, поскольку никакой поддер-
жки обновлений или передачи команд не требуется — как и событий и
делегатов, относящихся к обслуживанию обновлений (они все равно не
будут использоваться).

Базовый класс, DbDataAdapter, содержит почти все необходимые для


класса MDirDataAdapter методы (Fill, FillSchema и др.). MDirDataAdapter
работает так, как и должен, позволяя потребителю данных заполнять
DataSet методом Fill и предоставляя доступ к SelectCommand. Изучая ис-
Microsoft ADO.NET

ходный код провайдера, вы заметите, что метод Fill класса DbDataAdapter


вызывает Command.ExecuteReader(commandBehavior), передавая флаг
поведения SequentialAccess, а метод FillSchema этого класса использует
флаг поведения Keylnfo SchemaOnly.

Специфичная функциональность в DataAdapter


Провайдеры SqlClient и OleDb реализуют подклассы Updating/Updated-
EventArgs и делегаты. В провайдере OleDb реализована перегруженная
версия класса Fill, позволяющая заполнить DataSet или DataTable данны-
ми из классического ADO-объекта Recordset.

Добавление дополнительной функциональности


Итак, я рассказал обо всех обязательных объектах в объектной модели
провайдера данных. В качестве заключительного аккорда хотел бы отме-
тить, что провайдер данных DirProv полностью готов к работе и может
быть расширен для обслуживания трассировки более высокоуровневых
вызовов, поступающих в стек данных, управляемых ADO.NET. Весь ис-
ходный код можно скачать по уже упоминавшейся ссылке с сайта MSDN
Magazine. В моем провайдере реализован только базовый набор типов и
интерфейсов, поэтому, как и в случае провайдеров OLE DB, вы могли бы
добавить в него дополнительную функциональность. Для полноты карти-
ны я кратко опишу доступные расширенные типы, а также определения и
реализации базовых классов: типы транзакций, классы, реализующие па-
раметры и наборы параметров, класс CommandBuilder, типы, применяемые
при обработке ошибок, и типы разрешений. Обратите внимание, что в про-
вайдерах данных SqlClient и OleDb реализованы специфичные вариации
этих типов и перечислимых.

Типы транзакций Типы транзакций инкапсулируют семантику локаль-


ных транзакций, которые могут запускаться внешними по отношеник» к
провайдеру объектами. В состав как OleDb, так и SqlClient входят типы
Transaction, реализующие интерфейс IDbTransaction.

Параметры и наборы параметров Эти классы реализуют наборы (collec-


tions) параметров, используемые хранимыми процедурами и параметризо-
ванными запросами. Параметры должны преобразовывать .NET-типы в
типы базы данных методами, аналогичными методам IDataReader. SqlPara-
meter и OleDb Parameter реализуют один и тот же интерфейс IDataPara-
meter, а наборы параметров — интерфейс IDataParameterColIection, а также
интерфейсы для операций над наборами (lEnumerable, ICollection и IList).

CommandBuilder Этот класс предназначен для того, чтобы упростить


формирование используемых по умолчанию команд InsertCommand, Up-
dateCommand и DeleteCommand класса DataAdapter на основе метадан-
Разработка собственных провайдеров данных для .NET Data Access Framework Ц9

ных, получаемых из базы данных. И в SqlClient, и в OleDb есть своя вер-


сия Command Builder, но общего интерфейса или базового класса у этих
версий нет.

Типы, применяемые при обработке ошибок При разработке любого API


доступа к данным весьма сложно увязать последовательность выполняе-
мых операций с обработкой ошибок (особенно с учетом их разнообразия).
Как в SqlClient, так и в OleDb реализован набор Errors, содержащий
объекты, которые описывают ошибки. Эти типы не являются расширени-
ями общего базового типа. Однако они реализуют интерфейсы наборов.
Оба провайдера генерируют исключения, специфичные для провайдера, —
GleDbException/SqlException. Кроме того, в обоих провайдерах реализо-
вано событие/делегат для предупреждений и информационных сообще-
ний — Sql/OleDblnfoMessageEventArgs/Delegate. Поэтому предупрежде-
ния не прерывают логику выполнения вызывающей программы.

Типы разрешений Провайдеры данных должны соответствовать требо-


ваниям архитектуры защиты в .NET, а клиентам нельзя разрешать вызовы
защищенных функций лишь потому, что они используют высокоуровне-
вый API. Поэтому в провайдере данных следует реализовать типы Permis-
sions и PermissionsAttribute. Базовые классы DBDataPermission и DBData-
PermissionAtribute, на основе которых создаются классы, отвечающие тре-
бованиям защиты, содержатся в System.Data.Common.

Альтернативные варианты реализации


Я создал провайдер данных MdirProv, чтобы исследовать модель провай-
деров данных в .NET. Огромная часть доступной функциональности в
этом провайдере не реализована. А каковы альтернативные варианты?
Является ли провайдер данных лучшим выбором? Причины, по которым
к данным обращаются через провайдер, — упрощение обмена данными,
интеграция с GUI-компонентами и возможность доступа к данным из сто-
ронних приложений вроде Crystal Reports.

На момент написания статьи разработчикам было доступно минимум четы-


ре способа создания провайдеров данных, совместимых с платформой .NET.

.NET-провайдер данных Неплохой вариант, если источник данных


(обычно реляционная или какая-то другая база данных) использует соб-
ственный протокол, с которым можно работать с помощью набора управ-
ляемых классов. За примерами далеко ходить не надо. Вспомните Sql-
Client, в котором реализован анализатор TDS (TDS parser), или OleDb,
который инкапсулирует API, основанный на СОМ. Такой вариант наибо-
лее эффективен, когда источник данных допускает обновление и поддер-
живает локальные транзакции, параметризованные запросы, настраивае-
±20 Microsoft ADO,NET

мые команды и нестандартные наборы ошибок. В идеале источник данных


должен поддерживать множественные табличные наборы результатов
(multiple rectangular resultsets). пакеты команд и обновления, управляе-
мые на основе команд. Заметьте, что сейчас в модели нет поддержки об-
новлений «по месту» (управляемых не на основе команд) и серверных
курсоров.

Нестандартный XmlReader или XPathNavigator Этот вариант особенно


хорош, если ваши данные имеют иерархическую структуру (точнее, не таб-
личную) и требуют доступа на основе навигации (курсоров), а не наборов
данных. Для источников данных, оптимизированных под выборку подмно-
жеств (subsetting), т. е. чтение только части иерархии, отлично подходит
нестандартная реализация XPathNavigator. Кроме навигации, можно ис-
пользовать XPath-запросы и XSLT-преобразования, напрямую работаю-
щие с данными. Аарон Сконнард (Aaron Skonnard) написал серию классов
XPathNavigator (для файловой системы, .NET-сборок и др.), демонстриру-
ющих такой подход. Они доступны на сайте http://staff.develop.com/
aarons.

«Унаследованный» («legacy») провайдер OLE DB или ODBC-драй-


вер Хотя в конечном счете такой подход выйдет из употребления, это
по-прежнему лучший вариант, когда вам нужно выдавать распределенные
запросы к SQL Server, использовать SQL Server DTS, напрямую включать
объекты в MSDTC-транзакции и т. д. Оба API реализуют модель курсоров
с поддержкой обновления «по месту». Если вы разработаете провайдер
OLE DB, то почти со стопроцентной гарантией сумеете интегрировать его
с .NET через провайдер данных OleDb.

Программное заполнение DataSet или XmlDocument Используйте с


этой целью API, специфичный для ваших данных. Если вы хотите лишь
получить набор данных, оба API можно программировать напрямую. Вы
не обязаны реализовать навигацию, поддержку обновлений, соединения и
команды, если все это вам не нужно.

.NET-класс DataSet можно использовать для обмена данными, предостав-


ляя функциональность, аналогичную той, которая есть у ADO-объекта
Recordset. DataSet можно напрямую задействовать в Web-сервисах, и он
поддерживает маршалинг по значению. Но самое интересное — как Data-
Set осуществляет маршалинг по значению. В отличие от ADO-объекта
Recordset, использующего собственный двоичный формат (advanced data
tablegram, ADTG), DataSet выполняет маршалинг в формате XML, как и
принято в .NET Framework. XML — универсальный формат маршалинга и
обмена данными в .NET. Поэтому нет необходимости реализовать провай-
дер данных только для того, чтобы программно заполнять DataSet.
Разработка собственных провайдеров данных для .NET Data Access Framework 121

Ту же функциональность можно реализовать на основе нестандартного


XmlReader или XPathNavigator, который обращается к XML infoset, а не к
DataSet. XML infoset — непрозрачное (opaque) представление данных
XML-документа в памяти. Зачастую infoset является наиболее эффектив-
ным представлением XML-документа, Сериализация в формат XML не
требуется, поскольку доступ к данным обеспечивается с помощью модели
XML infoset. Кроме того, XML infoset лучше подходит для таких нереля-
ционных данных, как гомогенные и гетерогенные иерархии или полу-
структурированные данные (данные в структурированном документе).
Учтите, что вы можете не только предоставлять прямой доступ к данным
как к XML, но и программно заполнять DataSet. Как и в случае ADO-
объекта Recordset, нет необходимости использовать модель Connection-
Command- DataReader.

Visual Studio упрощает интеграцию элементов управления не только с


провайдерами данных, но и с любыми источниками данных. .NET-элемен-
ты управления, связываемые с данными, способны работать с любыми
типами, реализующими lEnumerable или ICollection. Дизайнеры Visual
Studio тоже поддерживают такую интеграцию. Поэтому для поддержки
элементов управления создавать провайдер данных не нужно.

В некоторые сторонние программные продукты уже вводится прямая под-


держка .NET. Так, Crystal Reports .NET, который будет поставляться с
Visual Studio .NET, позволяет использовать при построении отчетов клас-
сы DataSet наряду с ODBC-драйверами и ADO-объектами Recordset.

Заключение
Хотя мой провайдер данных не имеет прямого отношения к объектам
Connection, Transaction и Parameters или к преобразованию типов источ-
ника данных в управляемые типы, он позволяет заполнять DataSet. Этого
можно было бы добиться и по-другому, но в любом случае этот пример
весьма полезен, так как благодаря ему вы изучили архитектуру провайде-
ров данных.

Боб Бьючмин {Bob Beauchemin) — старший преподаватель в DevelopMentor,


занимающийся программированием более 20 лет. Соавтор учебного курса
DevelopMentor .NET. Работает над книгой «Essential ADO.NET» для Addison-
Westey/DevelopMentor. С ним можно связаться по адресу bobb@develop.com.
Джонни Папа

Доступ к данным

Выражения в ADO.NET

Автор рассматривает основы применения в ADO.NET вычисляемых полей и


функцию Compute, подсчитывающую агрегатные значения. Также рассказы-
вается о функциях агрегации в объектах DataColumn, получении итоговых
значений, выполнении других вычислений по всему объекту DataSet и
связывании объектов DataColumn, принадлежащих разным DataTable. Кроме
того, дано несколько практических примеров.

С появлением ADO.NET управлять отношениями в базе данных стало го-


раздо проще. Вместо получения из хранилища данных одного набора за-
писей теперь можно считывать группы наборов записей, а затем связывать
их в объекте DataSet. Связанные ADO.NET-объекты DataTable имеют ряд
преимуществ и, в частности, позволяют хранить данные в виде иерархи-
ческой структуры, облегчают обновление данных и дают возможность ис-
пользовать выражения в полях.

Сегодня я расскажу об основах применения в ADO.NET вычисляемых


полей (column-based expressions) и функции Compute, подсчитывающей
агрегатные значения. Также будут рассмотрены функции агрегации в
объектах DataColumn, получение итоговых значений (totals), выполнение
других вычислений по всему объекту DataSet и связывание объектов
DataColumn, принадлежащих разным DataTable. Кроме того, я приведу
несколько практических примеров.

Суммирование и вычисление средних значений по группам связанных за-


писей в SQL-запросах, вероятно, вам отлично знакомо, поскольку такие
функции агрегации, как SUM и AVG, являются частью стандарта ANSI

Публиковалось в MSDN Magazine/Русская Редакция. 2003. №1 (январь). — Прим. изд.


Выражений в ADO.NET 123

SQL. SQL также позволяет выполнять вычисления по полям, например


умножение цены единицы товара на количество товаров. Благодаря
ADO.NET эти возможности доступны не только в источнике данных, но
и на промежуточном и более высоких уровнях я-уровневых приложений.
Используя вычисляемые поля, можно создавать свои поля, агрегирующие
значения по входящим в DataSet наборам данных, вычислять значения по
другим значениям той же записи и даже обращаться через DataRelation к
полям родительских или дочерних объектов DataTable. С появлением в
ADO.NET вычисляемых полей и функции Compute стали доступны новые
способы управления объединением данных.

Конечно, у вычисляемых полей, функций агрегации и функции Compute


в ADO.NET есть свои плюсы и минусы. Выражения, указываемые для
вычисляемых полей, могут обращаться к полям как одного DataTable, так
и двух объектов DataTable одного и того же DataSet, связанных через
DataRelation. Я объясню, какие преимущества дают вычисляемые поля в
ADO.NET и SQL и в чем между ними разница. Также рассмотрю опера-
ции, связанные с применением объекта DataRelation. (Об объектах Data-
Relation см. мою рубрику за ноябрь 2002 г. на http://rasdn.microsoft.com/
msdnmag/issues/02/ll/datapoints/default.aspx.) Здесь же я покажу, как
создавать объекты DataColumn, содержащие выражения, применять фун-
кции агрегации в объектах DataSet и операторах SQL. Кроме того, вы уз-
наете, как группировать данные и обращаться в дочернем DataTable к по-
лям родительского DataTable, а также выполнять вычисления по полям
наборов данных DataSet с помощью функции Compute.

SQL-выражен ия
SQL-выражения бывают разных видов и служат разным целям, в частно-
сти для форматирования строк, вычисления пользовательских функций и
математических расчетов. Если в SQL-операторе объединяются имя и фа-
милия, вычисляется сумма позиции заказа или содержится пользователь-
ская функция SQL Server 2000, извлекающая дату заказа, значит, в этом
операторе есть выражение.

Выражения очень гибки, позволяя разработчикам считывать в наборы за-


писей значения, определяемые по другим полям базы данных. В реляци-
онной и даже в какой-то мере нормализованной базе данных обычно сле-
дует хранить по каждой позиции заказа не общую сумму, а цену за едини-
цу и количество соответствующего товара. Сумму нужно вычислять по
цене и количеству — это исключает вероятность рассогласования данных.
Так, если хранить в записи количество, цену и сумму, может получиться,
что количество равно 10, цена за единицу — $7.00, а сумма почему-то -
$100.00. Такого быть не должно, но при хранении избыточных данных
124 Microsoft ADO,NET

вполне вероятно. При работе с транзакционными базами данных, как пра-


вило, не хранят информацию, которую можно получить на основе имею-
щейся (в нашем примере это сумма позиции заказа).

В таких случаях применяются SQL-выражения. Сумму вы получаете, со-


здав в SQL-операторе вычисляемое поле, где цена умножается на количе-
ство (рис. 1). Если есть скидка, ее тоже можно учесть при вычислении
суммы.

Рис. 1. Математические выражения в SQL


USE rjorthwind
GO

SEUCI QrderlO, :
ProductID,
UnitPrice,
Quantity,
(UnitPrice * Quantity) AS ExtendedPrice,
Discount,
((UnitPrioe * Quantity) * (1 - Discount)) AS
Extended?riceWithDiscauflt
РЙ0Н [Order Details]
QROER BY
OrderlD,
Product!»

Следующий SQL-код иллюстрирует конкатенацию строк в SQL-выраже-


ниях. Здесь из имени и фамилии формируется строка, содержащая имя и
фамилию (с буквами верхнего регистра), причем сначала идет фамилия:

USE pubs
GO

SELECT au_fname AS FirstName,


au_lname AS LastName,
au_lname + ', ' + au_fname AS FullNamel,
(UPPER(au_fnanie) + ' ' + UPPER(au_lname)) AS FullName2
FROM authors
ORDER BY
au^lname,
au_fname

SQL-выражения позволяют форматировать строки и выполнять математи-


ческие операции, возвращая результаты в наборе записей. Но, используя
их в этих целях, остерегайтесь ошибок. Если DataSet заполнен SQL-опе-
ратором с рис. 1 и, например, в первой записи изменится поле с количе-
Выражения в ADD,NET 125

ством товара, то вычисляемые поля останутся прежними. Так, если коли-


чество равно 10, а цена — $7.00 и мы поменяем количество на 5, то в поле
ExtendedPrice сохранится считанное из базы данных значение $70.00 (10
* $7.00), т. е. нарушится синхронность данных. Таким образом, основная
проблема в том, что выражения не «переносятся» из SQL-оператора в
ADO.NET-объект DataSet.

Выражения в объектах DataColumn


Выражения можно определять и в ADO.NET-объектах DataColumn, созда-
вая вычисляемые поля. Вместо вычисления суммы в SQL-операторе вы
создаете DataColumn, содержащий сумму. Одно из отличий между выра-
жением в SQL-операторе и выражением в DataColumn заключается в том,
что если одно из полей, участвующих в выражении, изменится, Data-
Column с вычисляемым полем тоже автоматически изменится, тогда как
DataColumn с результатом вычисления SQL-выражения (например, пока-
занного на рис. 1) — нет.

Рис. 2 демонстрирует, как заполнить DataTable в DataSet SQL-оператором,


а затем создать DataColumn, содержащий выражение, вычисляемое по
другим полям объектов DataTable этого DataSet. Затем представление по
умолчанию (default view) этого DataTable связывается с объектом Data-
Grid — grdOrderDetail.

Рис. 2. Заполнение DataSet и добавление выражений


private void LoadGrderDetailDataQ

//- Создаем подключение


'-" . " +
ifSqlConnection oCn * new SqlConnection("Data.. Souree=papa;
"Initial Catalog=rn3rthwind;User ID=sa;" +
: • "Password-secretpassword; ");

//- Создав*! команду SELECT

string sSQL * "SELECT od.OrderlD, od.ProductIO, p.ProductName,


od.UnitPrioe, od.Quantity, od.Discount " +
" FROM [order details] od ШЕЙ JOIN Products p ON " +
"od.ProductlO = p.ProductID " + " ORDEfi BY od.Orderlo, " +
"p.ProductNaae ";
SqlCommand oSelCrud = new SqlGoraBand<sSQL,
.oSelCmd-GominartdTvpe = ComniandType.Text;

см. след. стр.


126 Microsoft ADG.NET

Рис. 2. Заполнение DataSet и добавление выражений (окончание)

//- Связываем SqlCommand с DataAdapter и заполняем DataSet


//
SqlDataAdapter oDA = new SqlDataAdapter(oSelCffld);
DataSet oDs * new DataSetO;
aDA,F±ll(oDs, "Orderoetail");

//- Добавляем вычисляемые поля (выражения)


//
oOs.Tables["OrderPetail").ColuiBns.AtM("ExtendedPrice",
typeof(tieeimel), "UnitPrice * Quantity"};

//- Связываем данные с АЗР.Ш"-объектон DataGrid


//
grdGrderQetail.DataSouree = oDs,Tablest"OrderDetail"J.DefaultView;
QrdOrderDetail.DataBindO;
oDs.DisposeO;

В этом коде создается DataSet, заполняемый информацией о позициях


заказа. Затем в DataTable этого DataSet добавляется вычисляемое поле —
Extended Price с типом данных decimal. В выражении вычисляется произ-
ведение полей с количеством и ценой. В таких выражениях можно обра-
щаться к любому DataColumn объекта DataTable, для которого определе-
но выражение. Значения полей берутся для текущего DataRow. Например,
если в первой записи указаны количество 10 и цена $7.00, то значением
поля суммы будет $70,00.

В выражениях могут применяться любые другие поля DataTable, а также


литеральные значения. Например, наше выражение, считающее сумму,
можно изменить, чтобы учитывалась скидка. Для этого код добавления
поля ExtendedPrice надо модифицировать так;
oDs.Tablesf"ОrderDetail"].Columns.Add("ExtendedPrice",
typeof(decimal), "(UnitPrice * Quantity) * (1 - Discount)");

Попробуйте изменить значение любого из объектов DataColumn — Unit-


Price, Discount или Quantity. В отличие от поля с SQL-выражением зна-
чение DataColumn ExtendedPrice сразу изменится. Эта возможность очень
удобна для приложений, работающих с корзиной покупателя: пользова-
тель вносит в нее изменения, сохраняет их и смотрит, какая сумма заказа
получилась.
Выражения в ADO.NET 127

Выражения применяются и для вычисления значений других типов: логи-


ческих или строковых. Так, в следующем фрагменте показано добавление
поля, указывающего, получает ли автор скидку.

oDs.Tables["ОrderDetail"].Columns.Add("GetsDiscount",
typeof(bool), "Discount > 0");

Это выражение можно усложнить, создав составное выражение; в нем про-


веряется несколько условий, к которым применяются операции AND, OR
или NOT В выражениях используются и другие операции, такие как LIKE
или IN.

Выражения также позволяют вычислять строковые значения, например,


объединять в одну строку содержимое полей DataTable с именем и фами-
лией. Взгляните, как выполняется конкатенация ProductName и Рго-
ductlD:

oDs.Tablest"ОrderDetail"].Columns.AddC'stringfield",
typeof(string), "ProductID + '-' + ProductName");
.r •

Функции
Чтобы поле содержало выражение с более сложной логикой, можно задей-
ствовать функции. В выражениях применяются такие функции, как Len,
lif, IsNull, Convert, Trim и Substring. Каждая из них позволяет более гибко
создавать выражения. Функция Len вычисляет длину строки:

oDs.Tables["OrderDetail"].Columns.AddC'LengthOfProductName",
typeof(int), "Len(ProductName)");

Функция lif, аналогичная Ilf в Visual Basic .NET, — это указываемый в


выражении оператор If. Она принимает три аргумента и вычисляет пер-
вый из них, чтобы выяснить равен он true или false. Если при вычислении
первого аргумента получилось true, функция lif возвращает второй аргу-
мент, в ином случае — третий. Вот как легко вставить в выражение конст-
рукцию If...Then...Else, упакованную в функцию Ilf;

oDs.Tables["OrderDetail"].Columns.Add{"Inventory",
typeof(string),
"Iif(Quantity < 10,'A few left', 'Plenty is stock')");

Функция IsNull вычисляет первый аргумент и сравнивает его значение с


System.DbNull. Если результат сравнения — false, возвращается значение
первого аргумента, в ином случае (т. е. когда первый аргумент равен Sys-
tem.DbNull) — значение второго аргумента. Это полезно, когда требуется,
чтобы вместо NULL-значений были пустые строки или подставляемое
значение (placeholder value), как, например, здесь:
128 Microsoft ADO.NET

oDs.Tables["OrderDetail"].Columns,Add("DiscountString",
typeof(string), "IsNull(Discount, '[null value]')");

Функция Trim удаляет из строки ведущие и концевые символы пробелов.


Функция Convert изменяет тип данных для значения в выражении, явля-
ющегося первым аргументом, на тип данных, передаваемый во втором ар-
гументе. Функция Substring, как и ее аналог в T-SQL, возвращает часть
строки (подстроку) передаваемого в нее строкового значения. Substring
позволяет урезать слишком длинные строки, чтобы ограничить показыва-
емое количество символов (см. фрагмент кода ниже). Кроме того, эта фун-
кция применяется для получения любых других частей строк и при необ-
ходимости используется в сочетании с другими функциями.

oDs.Tables["OrderDetail"].Columns.Add("ShortProduct",
typeof(string), "Substring(ProductName, 1, 10)");

Агрегация и отношения
Внедрение функций в выражения позволяет создавать вычисляемые поля
с достаточно сложной логикой. А как быть, если требуется создать поле,
вычисляющее значения по нескольким записям? Используйте функции
агрегации.

Допустим, у вас есть DataSet, в котором связываются таблицы Order и


Order Details базы данных Northwind из SQL Server. Создание вычисляе-
мых полей с выражениями, содержащими функции агрегации, оказывает-
ся довольно простым делом. В коде на рис. 3 показано создание DataSet,
в котором в родительский DataTable помещаются сведения о заказах, а в
дочерний DataTable — о позициях заказов. Эти объекты DataTable связы-
ваются друг с другом через объект DataRelation (Orders2 Order Details). Об
объектах DataRelation и о том, как с их помощью связывать иерархические
и реляционные структуры данных ADO.NET, см. мою рубрику в «MSDN
Magazine» за ноябрь 2002 г.

Рис. 3, Отношение и выражения


private void LoadDataO
С
/ / - - . ' -
//- Создаем подключение
//
SqlGonnaotioR oCn « new SqlConnection("Data Source^papa;
"Initial Gatalog*northwlnd;User ID^sa;" *
"PassworcNsecretpassword;");

см. след. стр.


Выражения в ADQ.NET 129

Рис. 3. Отношение и выражения


//- Создаем команду SELECT для заказов
//
string sSQL = "SELECT GrderlD, CustomerlD, QrdsrDate, ShipCity,
ShipCountry " + " FROM Orders " +
" ORDER BY CustomerlD, OrderDate DESC "; .
SqlCofflroand oSelCmd = new SqlContmand{sSQL, oCn);
. oSelCmd.CommaridType = ComfnandType.Text;

//- Связываем SqlCommand e DataAdapter и заполняем DataSet


//
SqlDataAdapter oOA « new SqlOataAdapter(oSelCmd);
DataSet oBs = new DataSet();
oDA.FilKoDs, "Order");

//- Создаем команду SELECT для позиций заказов


//
string sSQL « "SELECT od.OrderlD, od.ProductID, p.Producttiame,
od.UftitPrice, od. Quantity, od. Discount " +
" FROH [order details] od INNER JOIN Products p 0«
od.ProductID - p.ProductlS " +
" ORDER BY od.OrderlD, p.ProductName ";
SqlCommand oSelCffid = new SqlComniand(sSQL, oCn);
oSelCmd.CoiTiBiandType = CommandType.Text;

I/
//- Связываем SqlCommand с OataAdapter и заполняем DataSet
//
SqlOataAdapter oDA = new SqlDataAdapter(oSelCmd);
oDA.FillCoDs, "OrderDetail");

//- Связываем объекты DataTable


// V \ '_
oDs. Relations. Add("Order20rderDetail",
oDs . Tables[ "Order" ] . Columns
{"OrderlO"), oDs.Tables["OrderOetail"].ColuiinsC"OrderIO''I);

//- Добавляем выражения для таблицы OrderDetail

oDs.Tables["OrderDetail"].Columns.Add("OrderDate",
typeof(string), "Parent.OrderDate");
oDs.Tables["OrderDetail"].Columns,AddC'ExtendedPrlce",
typeof(decifflal), "(UnltPrice * Quantity) * (1 - Discount)");
oDs.Tablesf'OrderDetail"]. Columns. AddC'GetsDiscount",

см. след. стр.


5-5947
£30 Microsoft ADO,NET

Рис, З. Отношение и выражения (окончание)


typeaf(bool), "Discount > О");
oDs,Taules["OrderDetail"].Columns.Add("stringtest",
typeof{string), "ProductID + '-" * ProductName");
oDs.Tablest"OrderDetail"]. Columns. Addt"Len9thOfPrm3«ctH«rfe"f
typeof(int), "Len(ProductNaine)");.
oDs.Tables["OrderOetail"].Columns.AddC"Inventory",
typeof(strinff), "lifCQuantity < 10, 'Only a few left',
'Plenty 1л stock')");
oOs, Та bles[ "Order-Detail"], Columns. Add("DiscountString",
typeof(string), "IsNull(Discount, '[null value]')");
oDs.Tat>les[''QrderDetair']. Columns. Add("ShortProduct",
typeof (string), "Substring(Pro6uctName, 1, 10)+ '...'")

//- Добавляем выражения для таблицы Order

o0s.Tables["Order"l.Columns.Add("OrderTotal", tyjssof (decimal)*


"Suin<CHild(Order20rderDetail).ExtendedPrice)");
oDs.Taules["Orcfer"].Columns.Add("AvgOuantity", typeof(decimal),
"Avg(Child(Order2QrderQetail).Quantity)");

Обратите внимание, как на рис. 3 создаются вычисляемые поля, добавля-


емые в объект DataTable Order. В первом вычисляемом поле — объекте
DataColumn OrderTotal — подсчитывается итоговая сумма по всем пози-
циям текущего заказа. Поле содержит выражение, суммирующее значения
другого вычисляемого поля — DataColumn ExtendedPrice объекта Data-
Table OrderDetail. Так что, как видите, функции агрегации позволяют ра-
ботать с полями таблиц, с которыми устанавливаются отношения через
DataRelation, и даже использовать другие вычисляемые поля.
''*
В ADO.NET есть и другие функции агрегации, в том числе Sum, Avg, Max,
Min, StDev, Var и Count. На рис. З демонстрируется, как применять фун-
кцию Avg для вычисления среднего количества по всем позициям заказа.
В таких случаях приходится использовать ключевые слова Parent и Child,
чтобы обращаться к данным, доступным через DataRelation:

oDs.Tables["Order"].Columns.AddC'AvgQuantity", typeof(decimal),
Avg(Child(Order20rderDetail). Quantity)");

Функция Child принимает в качестве аргумента имя DataRelation, по ко-


торому определяется дочерний набор записей. Этот необязательный аргу-
мент необходим, только когда есть несколько DataRelation, связывающих
исходный DataTable с дочерними наборами записей. Если у DataTable
Выражения в ADO.NET

только один дочерний DataTable, можно упростить синтаксис, убрав имя


DataRelation, так как имеется только один объект DacaRelation:
oDs.Tables["Order"].Columns.Add("AvgQuantity", typeof{decimal),
Avg(Child.Quantity)");

Обращение к полям родительского


набора записей и функция Compute
Функция Parent работает так же, как Child, но, разумеется, обращается к
родительскому DataTable, поднимаясь по цепочке отношений. Эти две
функции позволяют реализовать в ADO.NET-коде функциональность,
аналогичную GROUP BY в SQL.

Эти ключевые слова позволяют выполнять группирование и передавать


между наборами записей значения, не изменяющиеся при переходе от ро-
дительского набора записей к дочернему. Меня часто спрашивают, как за-
ново объединить родительский и дочерний DataTable в один DataTable,
чтобы показать оба набора записей в одном DataGrid (без иерархии). Ис-
пользуя ключевое слово Parent, можно из дочернего DataTable обращать-
ся к полям родительского DataTable и отображать в DataGrid только до-
черний DataTable. Так, если требуется показать дату заказа для каждой
записи DataTable OrderDetail из примера на рис. 3, можно добавить Data-
Column с ключевым словом Parent:
oDs.Tables["OrderDetail"].Columns.Add("QrderDate", typeof(string),
"Parent.OrderDate");

Это позволяет передавать значения из полей родительских наборов запи-


сей в поля дочерних (roll up and down) без вычислений. Используя клю-
чевые слова Parent и Child, можно обращаться к полям родительской таб-
лицы и показывать их в DataGrid. При этом вы получаете единый двух-
мерный набор записей, аналогичный получаемому из SQL-оператора.
Если же нужно задействовать данные в едином наборе записей, то вполне
приемлемо использовать один набор записей, получаемый путем считыва-
ния всех данных в один DataTable. Но если вы хотите применить реляци-
онную структуру DataSet, ключевое слово Parent позволит более гибко
показывать данные.

Еще одно средство, которое стоит рассмотреть, — функция Compute объек-


та DataTable, выполняющая вычисления с использованием функций агре-
гации применительно к текущему DataTable и заданного фильтра. Допус-
тим, вам нужно вычислить общее количество заказов с итоговой суммой
не менее $1000. Ниже показан пример подходящего кода. Первый аргу-
мент функции Compute — функция агрегации, подсчитывающая число
значений OrderTotal, удовлетворяющих условию фильтра:
132 Microsoft ADO.NET

//- Показ общего числа заказов с суммой, большей или равной $1000

int iCnt = (int)oDs.Tables["Order 1 '].Compute("Count(OrderTotal)",


"OrderTotal >= 1000");
IblTest.Text = iCnt.ToStrirtgO + " orders are at least $1000";

Второй аргумент Compute — фильтр, ограничивающий круг записей, для


которых вычисляется функция агрегации, только записями, отвечающими
условию фильтра. Так что подсчитываются записи, для которых Order-
Total равно $1000.00 и более. Compute, благодаря поддержке фильтрации,
отлично подходит для быстрых вычислений по DataTable. Например, вы
могли бы легко определить число клиентов, заказавших товар х, и число
клиентов, заказавших товар у, — не надо ни писать циклы, ни выполнять
запросы к базе данных.

Важно помнить, что объекты DataColumn с вычисляемыми полями нельзя


изменять вручную. Эти поля связаны с выражениями, поэтому их нельзя
переопределить, не удалив выражение. Кроме того, вычисляемым полям
не соответствуют никакие поля в источнике данных — базе данных или
XML-файле. Так что, если вам нужно сохранить измененную информацию
в базе данных, имейте в виду, что выражения в ней не сохраняются.

В этой статье я рассмотрел выражения SQL и ADO.NET. Вы познакоми-


лись с обширной функциональностью, обеспечиваемой выражениями, и
теперь сможете использовать их в своих приложениях, работающих с дан-
ными.

Джонни Папа (Johnny Papa) — вице-президент компании MJM Investigations


по разработке программного обеспечения (Роли, штат Северная Каролина),
автор нескольких книг по ADO, XML и SQL Server. Часто выступает на конфе-
ренциях, в том числе на VSLive. С ним можно связаться по адресу
datapoints@lancelotweb.com.
Дино Эспозито

Двоичная сериализация
ADO.NET-объектов

Автор поясняет, какими способами можно сериализовать объекты ADO.NET.


Однако по-настоящему сериализуемыми являются только DataSet и DataTable,
в которых реализован интерфейс ISerializable. В связи с этим в статье
показывается, как расширить объекты ADO.NET, чтобы они поддерживали
XML-сериалнзацию. Автор рассматривает сериализацию этих объектов как
особый случай .NET-сериалиэации объектов в период выполнения и демонст-
рирует, что за счет переопределения, а не использования суррогатных типов
можно добиться значительного повышения скорости обработки ADO.NET-
объектов форматирующими объектами.

Одно из важнейших преимуществ ADO.NET по сравнению с ADO заклю-


чается в уровне интеграции с XML. XML в ADO — не более чем формат
ввода-вывода. Более того, формат ADO XML совершенно негибок и не
поддается никакой адаптации. Поскольку XML в ADO.NET, к счастью,
интегрирован гораздо теснее, им можно пользоваться при сериализации
ADO.NET-объектов. Такой уровень интеграции делает возможным ис-
пользование программного интерфейса двойного назначения, который
позволяет рассматривать один и тот же набор данных и как иерархичес-
кий, и как реляционный. В этой статье я сосредоточусь на первом аспек-
те, т. е. на сериализации ADO.NET-объектов. Если вас интересует второй
аспект, начните с характеристик класса XmlDaiaDocument, описанного в
документации MSDN.

Сериализацию ADO.NET-объектов можно выполнять двумя функцио-


нально разными способами. Во-первых, через стандартный механизм

* Публиковалось в MSDK Magazine/Русская Редакция. 2002. №6 (декабрь). — Прим. изд.


£34 Microsoft ADO.NET

Microsoft .NET Framework, основанный на форматирующих объектах


(formatters), например с применением форматирующего объекта SOAP
(SOAP formatter) или двоичного (binary formatter). Во-вторых, через
встроенные методы ADO.NET-объектов, записывающие содержимое объ-
екта в XML-документ в соответствии с заданной схемой.

Важно отметить, что по-настоящему сериализуемыми являются только


ADO.KET-объекты DataSet и DataTable, в которых реализован интерфейс
ISerializable. Именно поэтому они доступны любым форматирующим
объектам .NET Framework. Однако лишь DataSet предоставляет дополни-
тельные методы (вроде WriteXral), позволяющие явно сохранять его. со-
держимое в XML. К счастью, добавить такие методы к другим ADO.NET-
объектам наподобие DataTable, Data View и DataRow не слишком трудно.

Для начала я покажу, как расширить ADO.NET-объекты, чтобы они под-


держивали встроенную XML-сериализацию. Затем я рассмотрю сериали-
зацию этих объектов как особый случай .NET-сериализации объектов в
период выполнения, Основы такой сериализации (и многое другое) изло-
жены Джеффри Рихтером (Jeffrey Richter) в серии статей «Сериализация
в период выполнения» из рубрики «.NET»*. Там он показал, как переоп-
ределить способ, которым тип может сериализовать сам себя. Именно пе-
реопределение, а не использование суррогатных типов — ключ к значи-
тельному повышению скорости обработки ADO.NET-объектов формати-
рующими объектами.

Сериализация объектов DataTable


Единственный в пространстве имен System.Data объект DataSet предос-
тавляет набор методов для своей сериализации и десериализации в XML-
формат, определяемый классом (class-defined XML format). Лично я назы-
ваю этот формат нормальной формой ADO.NET XML. Метод WriteXml
класса DataSet сохраняет содержимое всех дочерних таблиц и отношений
(relations) в различные выходные потоки.

В классе DataTable такого метода для сохранения содержимого в XML нет.


Значит, чтобы сохранить в XML отдельный объект DataTable, не включен-
ный как дочерний в какой-либо родительский объект DataSet, без трюка
не обойтись.

При сохранении объекта DataSet в XML все включенные в него объекты


DataTable преобразуются в XML, но соответствующие методы не доступ-
ны извне. Чтобы обойти это препятствие, просто создайте временный пу-

См. MSDN Magazine/Русская Редакция. 2002. Спецвыпуск №2, №1(7), №3(9). - Прим. изд.
Двоичная сериализация ADO.NET-объектов 135

стой объект DataSet, добавьте к нему таблицу, а затем сериализуйте его в


XML. На рис. 1 приведен статический метод, который принимает объект
DataTable и сериализует его в дисковый файл, используя режимы записи,
поддерживаемые объектами DataSet. У метода Write DataTable несколько
перегруженных версий, по мере возможности имитирующих метод Write-
Xml объекта DataSet. Входной объект DataTable в коде па рис. 1 включа-
ется во временный объект DataSet, которому присвоено имя DataTable.
Конечно, в своей реализации вы можете изменить это имя или (что гораз-
до лучше) разрешить пользователю изменять его. Заметьте, что имя слег-
ка влияет на конечный XML-вывод. Дело в том, что имя DataSet факти-
чески представляет корневой узел конечного XML-документа:
DataSet ds = new DataSet("DataTable");
if (dt.DataSet == null)
ds.Tables.Add(dt);
else
ds.Tables.Add(dt.CopyO);

Рис. 1. Класс MsdnMagActoNetSerializer


public class MsdRMagAdoNetSerializer
I
public static void WriteDataTable( DataTable eft, XmlWriter writer)
i
WriteDataTable(dt, writer, XmlWriteMode. IgnoreScnema);
1

public static void WriteDataTableCDataTable dt, Stream stm)


(
WriteDataTable( dt , stm, XmlWriteMode . IgnorsSchema) ;

public static void WriteDataTableCDataTable dt, Stream stm,


XmlWritettode mode)
{
DataSet trap = CreateTeiapDataSet(dt);
tmp.WriteXmlCstis, mode);

public static void WriteOataTable( DataTable dt, string output-File)


{
WriteDataTabIe{dt, output File, XmlWriteHode.IgrtoreScnema);

public static void WriteDataTable(OataTable dt, string outputFils,


XmlWriteMode mode)

см. след. стр.


136 Microsoft ADC .NET

Рис. 1. Класс MsdnMagAdoNet&erializer (окончание)


DataSet trap - CreateTempDataSet(dt);
tmp.WriteXflil(QutputFile, mode);

public static void WriteDataTable{DataTable dt, string outputFile,


XmlWriteHode mode)
{
DataSet tmp - CreateTempDataSet(dt);
tmp.WriteXmKoutputFile, mode);

private static DataSet GreateTempDataSet(DataTable dt)

// Создать временный DataSet


DataSet ds = new DataSet{"DataTable");

// Убедиться, что этот DataTable


// не принадлежит какому-нибудь DataSet
if(dt.DataSet == null)
ds.Tables.Add(dt);
else
ds.Tables.Add(dt.Copy());
return ds:

Также заметьте, что объект DataTable нельзя связать более чем с одним
объектом DataSet; одновременно. Если DataTable принадлежит родитель-
скому объекту, тогда его свойство DataSet не равно null. В этом случае
временный объект DataSet. используемый для сериализации таблицы, сле-
дует связать с копией таблицы в памяти. Метод Сору просто создает пол-
ную копию указанного объекта DataTable. При желании для сериализации
этого объекта можно реализовать интерфейс ISerializable (см. врезку «Ре-
ализация ISerializable»).

Библиотеку классов MsdnMagAdoNetSerializer, содержащую перегружен-


ные версии метода Write DataTable, см. в исходном коде (http://msdn.mic-
rosoft.com/msdnmag/code02.aspx в разделе за декабрь). В клиентском при-
ложении эта библиотека используется так:
StringWriter writer = new StringWriterO;
MsdnMagAdoNetSerial:izer.WriteDataTable( table, writer);

// Показать результат сериализации


OutputText.Text = writer.ToString();
writer.Close();
Двоичная сериализация ADO.NET-объектов 3,37

В этом фрагменте кода метод WriteDataTable записывает свое содержимое


в строку. Класс StringWriter принимает данные через интерфейс класса
записи текста (text writer), а затем ToString возвращает их в виде строки.

Ну и хватит об объектах DataTable. Посмотрим, как сериализовать содер-


жимое представления, которое находится в памяти (и, возможно, отфиль-
тровано).

Внутреннее устройство объекта DataView


Класс DataView является настраиваемым представлением объекта Data-
Table. Связь между ними подчиняется правилам стандартной концепции —
модели «документ-представление» (document/view model). Б соответ-
ствии с этой моделью объект DataTable выступает в роли документа, а
DataView — представления. В любой момент может существовать несколь-
ко представлений одних и тех же данных. Важно, что каждое представле-
ние является отдельным объектом с собственным набором свойств, мето-
дов и событий. Кроме того, создание представлений не приводит к дубли-
рованию или репликации данных.

Под капотом DataView тесно связан с конкретным экземпляром класса


DataTable. Представление реализовано в виде массива, который содержит
индексы строк исходной таблицы, удовлетворяющих заданным для пред-
ставления критериям. По умолчанию представление не отфильтровано и
содержит ссылки на все записи таблицы.

Используя Data View-свойства RowFilter и Row State Filter, вы можете су-


зить набор строк, попадающих в конкретное представление. Свойство Sort
позволяет применять к строкам в представлении выражение, обеспечива-
ющее сортировку. Внутренняя архитектура объекта DataView представле-
на на рис. 2. Когда какие-либо критерии фильтрации заданы, DataView
получает от нижележащего DataTable обновленный индекс строк, удовлет-
воряющих этим критериям. Индекс представляет собой массив абсолют-
ных позиций. Физические объекты-строки не копируются, на них даже не
делаются ссылки до тех пор, пока клиент не запросит их. Связь между
объектами DataTable и DataView можно установить передачей DataTable
конструктору класса DataView:
public DataView(DataTable table);

Кроме того, вы могли бы создать новое представление и связать его с таб-


лицей позже, с помощью свойства Table объекта DataView:
DataView dv = new DataViewO;
dv.Table = dataSet.Tables["Employees"];
Microsoft ADO-NET

Фильтр

Массив индексов
Выборка
Dataflow Dataflow

Ссылка на Dataflow Кэш строк

Рис. 2. Архитектура DataView

Связь между этими объектами на самом деле двусторонняя: вы можете


получить объект DataView из любой таблицы благодаря DataTable-свой-
ству DefaultView. Оно возвращает неотфильтрованный объект DataView,
инициализированный для работы с этой таблицей:

DataView dv = dt.DefaultView;

Получаемый таким образом объект представления содержит столько эле-


ментов, сколько строк в таблице. Обращаться к содержимому этого Data-
View можно через самые разнообразные программные интерфейсы, в том
числе наборы (collections), списки и перечислители (enumerators). В час-
тности, перечислители позволяют через метод GetEnumerator перебирать
записи в представлении, используя привычный оператор foreach. В следу-
ющем фрагменте кода дехюнстрируется обращение ко всем строкам, вклю-
ченным в представление:

DataView myView = new DataView(table);


foreach(DataRowView rowview in myView}
{
// Разыменовать (dereference) объект Dataflow
Dataflow row = rowview.Row;

Когда клиентское приложение обращается к конкретной строке в пред-


ставлении, класс ожидает, что она находится во внутреннем кэше строк
(рис. 2). Строка, уже присутствующая в кэше, упаковывается в промежу-
точный объект DataRowView и возвращается клиенту. DataRowView — это
оболочка ссылки на объект DataRow, содержащий реальные данные. Data-
RowView служит программируемым интерфейсом, который управляет
объектом строки и ссылается на него как на единое целое. При необходи-
мости вы можете обратиться к нижележащему объекту DataRow из экзем-
пляра DataRowView через свойство Row представления.

Если строки, запрошенной клиентом, во внутреннем кэше нет, класс Data-


View загружает ее из исходной таблицы. Точнее, кэш строк DataView очи-
Двоичная сериализация ADO.NET-объентов 139

щается и заполняется при первом запросе. Этот кэш может оказаться пус-
тым либо потому, что еще ни разу не использовался, либо потому, что вы-
ражение сортировки или критерии фильтрации изменились. Всякий
раз, когда фильтр или выражение сортировки изменяется, кэш очищается,
Если какая-либо строка запрашивается впервые и кэш пуст, Data View
быстро заполняет его массивом объектов Data Row View, каждый из кото-
рых ссылается на исходный объект DataRow.

Сериализация объектов DataView


Расширим класс MsdnMagAdoNetSerializer, приведенный на рис. 1, и
включим в него перегруженные методы для сериализации объекта Data-
View, Идея в том, чтобы создать копию исходного DataTable со строками,
которые удовлетворяют критериям представления (рис. 3).

Рис. 3. Копирование DataTable


public class MsdnHagAdoNetSerlalizer

public static void WriteDataViewC DataView dv, string outputFile,


XmlWriteMode mode)
{
OataTable dt = CreateTefflpTable(dv);
WriteDataTable(dt, outputFile, aode);

private static DataTable CreateTefpTable(DataView dv)


{
// Создать временный DataTable, структура которого
// совпадает со структурой оригинала
DataTable dt * dv. Table, CloneO;

// Заполнить DataTable всеми строками представления


foreach<DataRowView rowvlew in dv)
dt . Import Row( rowview. Row) ;

return dt;

Сначала создается временный DataTable с той же структурой, что и у таб-


лицы, для которой создан сохраняемый объект DataView. Затем временная
копия заполняется строками, на которые ссылается представление. Нако-
нец, таблица сериализуется в XML вызовом уже определенных методов
WriteDataTable.
j_4Q Microsoft ADO,NET

DataTable-метод ImportRow играет ключевую роль в коде на рис. 3, созда-


вая новый объект DataRow в контексте таблицы, для которой он вызван.
На DataRow, как и на другие ADO.NET-объекты, не могут ссылаться сра-
зу два объекта-контейнера. Применение ImportRow логически эквивален-
тно копированию строки и добавлению ее клона к таблице в виде ссылки.
Этот метод сохраняет все значения свойств — исходные и текущие. В от-
личие от метода NewRow, который добавляет новые строки со значения-
ми по умолчанию, ImportRow сохраняет текущее состояние строки.

Сериализация объектов DataRow


Два уже рассмотренных примера демонстрируют стандартный способ со-
хранения ADO.NET-объектов в XML. Его суть в создании иерархии роди-
тельских объектов — от сериализуемого до DataSet, Так, чтобы сериализо-
вать один автономный объект DataRow, вам потребуется добавить его к
временному DataTable, который в свою очередь надо добавить к рабочему
DataSet.

ADO.NET-сериализация в период выполнения


Как я уже упоминал, есть два базовых способа сериализации ADO.NET-
объектов: через собственный XML-интерфейс объекта, если таковой име-
ется, или с помощью стандартных в .NET Framework форматирующих
объектов. До сих пор я рассматривал методы для сериализации ADO.NET-
объектов в XML, в том числе API-расширения, необходимые для поддер-
жки этой функциональности объектами, отличными от DataSet. Теперь
обсудим, как сериализовать ADO.NET-объекты, используя стандартный в
.NET Framework механизм сериализации объектов в период выполнения.

Главное отличие между методами вроде WriteXml и форматирующими


объектами .NET Framework в том, что в первом случае объект сам управ-
ляет своей сериализацией. С другой стороны, когда в дело вступают фор-
матирующие объекты, у каждого сериализуемого объекта есть два пути.
Он может просто объявить себя сериализуемым и безучастно взирать на
то, как форматирующий объект будет извлекать все значимые данные,
подлежащие сериализации. Такой тип сериализации поддерживается вклю-
чением атрибута [serializable] в каждый класс индивидуально. Для пере-
числения всех свойств, отвечающих за состояние объекта, в процессе се-
риализации используется API отражения (reflection), поддерживаемый
.NET Framework.

Второй вариант требует реализации интерфейса ISerializable; тогда сериа-


лизуемый объект отвечает за передачу форматирующим объектам всех
данных, которые нужно сериализовать. Однако, как только объект переда-
ет эти данные, он теряет контроль над процессом. Класс, не помеченный
Двоичная сериализация ADO.NET-объектов 141

атрибутом [serializable] и не реализующий интерфейс ISerializable, сериа-


лизовать нельзя. Так вот, никакие классы ADO.NET не объявляют себя
сериализуемыми, а упомянутый интерфейс реализуют лишь DataSet и
DataTable. Например, объект DataColumn или DataRow сериализовать
нельзя. Однако, как я уже говорил, обойти это препятствие иногда позво-
ляют суррогатные типы; при этом сравнительно простые несериализуемые
типы можно сделать сериализуемыми. Но сначала вспомним основные
аспекты .NET-сериализащш в период выполнения.

Двоичная ADO.NET-сериализация в период выполнения


.NET Framework предоставляет два предопределенных форматирующих
объекта в пространстве имен System.Runtime.Serialization.Formatters: дво-
ичный и форматирующий объект SOAP. Их классы — соответственно
BinaryFormatter и SoapFormatter. Первый генерирует более компактный
код, а второй рассчитан на более широкое взаимодействие и создает опи-
сание класса в формате SOAP.

Следующий код иллюстрирует, что нужно сделать для сериализации объ-


екта DataTable с применением двоичного форматирующего объекта:
BinaryFormatter bin = new BinaryFormatterO;
StreamWriter dat = new StreamWriter(outputFile);
bin.Serialize(dat.Basestream, dataTable);
dat.CloseO;

Метод Serialize форматирующего объекта заставляет последний записы-


вать содержимое сериализуемого объекта в двоичный поток. Метод Dese-
rialize выполняет обратную операцию: считывает ранее созданный двоич-
ный поток, воссоздает объект и возвращает его:
DataTable table = new DataTableO;
BinaryFormatter bin = new BinaryFormatterO;
StreamReader reader = new StreamReader(sourceFile);
table = (DataTable) bin.Deserialize(reader.BaseStream);
reader.CloseC);

Пока все замечательно. Однако при запуске этого кода произойдет кое-что
неожиданное. Если вы попытаетесь сериализовать объект DataTable или
DataSet с помощью двоичного форматирующего объекта, то двоичный
файл, конечно, получите — только он окажется весьма большим, с тонной
XML-данных. К сожалению, XML-данные занимают много места в двоич-
ном файле. Десериализация таких файлов может потребовать нескольких
секунд и гораздо больше памяти, чем нужно на самом деле. В результате,
выбрав двоичную сериализацию ADO.NET-объектов в стремлении полу-
чить более компактный файл, вы не достигнете своей цели. Двоичная се-
риализация применительно к ADO.NET-объектам оказывается не столь
142 Microsoft ADO.NET

эффективной, какой могла бы быть. В чем же причина такого довольно


странного поведения?

ADO.NET-объекты, сериализуемые с помощью форматирующих объектов


(только классы DataTable и DataSet), корректно реализуют интерфейс
ISerializable, а значит, именно они отвечают за предоставление сериализу-
емых данных. Этот интерфейс состоит из единственного метода Get-
ObjectData, результат работы которого принимает форматирующий объ-
ект и сбрасывает в выходной поток.

Классы DataTable и DataSet спроектированы так, что сообщают о себе


форматирующим объектам на основе XML-данных. В частности, они ис-
пользуют документ DifrGram, т. е. простой и «многословный» XML. Дво-
ичный форматирующий объект принимает эту достаточно длинную стро-
ку и добавляет ее в поток. Таким образом, эти объекты всегда передаются
между доменами в виде XML, и это здорово. Увы, если вам нужно более
компактное представление сохраненных таблиц, обычная сериализация
периода выполнения, поддерживаемая .NET Framework, для ADO.NET-
объектов не годится (по крайней мере, без модификации).

Следующий псевдокод иллюстрирует сериалшацию этих объектов форма-


тирующим объектом .NET;

void GetObjectData(SerializationInfo Info,


StreamingContext context)
{
info.AddValue("XrnlScfiema", this.GetXmlSchemaO);
this.WriteXmKstrWriter, XmlWriteMode.DiffGram);
info.AddValueC'XmlDiffGram", strWriter.ToStringO);
:

Класс передает свои данные форматирующему объекту, добавляя элемен-


ты в объект Serializationlnfo методом AddValue. Например, объект DataSet
сериализует себя, добавляя несколько элементов для схемы и данных.
Объект DataTable делает то же самое, но использует временный объект
DataSet, чтобы получить свою схему и данные, выраженные в виде XML-
строк, Информация, записанная в Serializationlnfo, затем сбрасывается в
двоичный или SOAP-поток в зависимости от типа форматирующего
объекта.

Нестандартная двоичная сериализация


ADO.NET-объектов
Есть только один способ оптимизировать двоичное представление объек-
та DataTable (или DataSet) — сопоставить класс промежуточному объек-
ту, сериализациеи которого вы можете управлять. Теоретически этого
Двоичная сериализация ADO. NET-объектов 143

можно добиться несколькими методами. Например, создать пользователь-


ские классы — оболочки функциональности классов DataTable и DataSet, —
реализующие свой алгоритм сериализации в период выполнения. Или ис-
пользовать .NET-pecypc, созданный специально для решения таких про-
блем, — суррогатные типы (surrogate types). Суррогатный тип — это класс,
берущий на себя работу по сериализации и десериализации данного типа
независимо от того, сериализуем он или нет. Суррогатный тип для конк-
ретного класса регистрируется в выбранном форматирующем объекте, а
далее обрабатывается, как обычно. Форматирующий объект распознает
суррогатный тип и сериализует его вместо исходного переопределенного
типа. Но по причинам, связанным с архитектурой, суррогаты не работают
с объектами DataTable (я поясню это позже).

Нестандартная двоичная сериализация ADO.NET-объектов требует от вас


создать пользовательский класс, поддерживающий нужный тип сериали-
зации, предоставить класс с сериализуемыми данными и, наконец, выпол-
нить сам процесс сериализации или десериализации.

Если вы пишете пользовательский класс, полностью управляющий сери-


ализацией DataTable (или DataSet), вам придется создать либо своего рода
теневой сериализующий класс (ghost serializer class) и объявить его сери-
ализуемым, либо реализовать интерфейс ISerializable. Затем вы должны
скопировать ключевые свойства объекта DataTable в члены класса. Какие
именно свойства вы будете копировать — решать вам. Однако для объек-
тов DataTable в этот список должны войти имена и типы столбцов (полей),
а также строки данных. Тогда вместо DataTable вы будете сериализовать
собственный класс, а при десериализации использовать восстановленную
информацию для создания нового экземпляра объекта DataTable. Рассмот-
рим эти действия подробнее и проанализируем результаты.

Создание теневого сериализующего класса


Если вам нужно сохранять только строки и столбцы объекта DataTable,
создание теневого класса не займет много времени. Вот пример структу-
ры такого класса:
[Serlalizable]
public class GhostDataTableSerializer
{
protected ArrayList colNames;
protected ArrayList colTypes;
protected ArrayList dataRows;

public GhostDataTableSerializerO { . . . }
public void Load(DataTable dt) {...}
144 Microsoft ADO.NET

public DataTable SaveC) { . . . }


!

Полностью исходный код этого класса приведен на рис. 4. Ghost DataTable -


Serializer содержит три объекта ArrayList, хранящие имена столбцов, их
типы и строки данных. Заметьте, что класс ArrayList — сериалиэуемый. В
приложении объект DataTable сериализуется с использованием сервисов
теневого сериализующего класса:
void BinarySerialize(DataTable dt, string outputFile)
{
BinaryFormatter bin = new BinaryFonnatter();
StreamWriter dat = new StreamWriter(outputFile);

// Создать экземпляр теневого сериализатора


// и заполнить его данными
GhostDataTable ghost = new GhostDataTablef);
ghost.Load(dt);

// Сериализовать объект
bin.Serialize(dat.BaseStream, ghost);
dat.CloseC);

Рис. 4. Класс GhostDataTabteSettalizer


using System;
using Systeffl.Collections;
using System.Data;

// Рабочий класс, выполняющий двоичную сериализацию


[SerlalizableJ
public class GhostDataTatileSerializer
i
public GlrostBataTableSe rialize r()
{
colNames = new ArrayListO;
colTypes = new Arraytistf);
dataRows = new ArrayListO;

// Свойства
protected ArrayList eolNantes;
protected ArrayList colTypes;
protected ArrayList dataRows;

см. след. стр.


Двоичная сериализация AOO.NET-объектой 145

Рис. 4. Класс GhostDataTableSeriatizer

// Заполнить пользовательский объект таблицы для сариализации


public void Uad(DataTable dt)
{
// Вставить информацию о столбцах (имена и типы)
foreachCDataColumn col in dt. Columns)

colTypes. Add(col , DataType. FullHanie);


\
// Вставить информацию о строках
foreachtOataflow row in dt.Ftows)
dataflows. Add(row.ItemAr ray);

// Вернуть объект DataTable, заполненный данными


public fiatsTable SaveC)
i
DataTatle dt = new OataTableO;

// Добавить столбцы
foKirvt 1=0; i<colNames..Count; 1-й-)
{
OataColumn col = new DataColufflrr(colHaiBesti3.ToString(),
Tyoe.6etType(colTypes[l].ToString() ));
dt,Columns.,Add<col);

// Добавить отроки
for(int 1=0; KdataRows, Count;

DataRow row = dt.NewRowO:


row. IteraArray = (ob}ect[]) dataRowstl];
dt, Rows. Add{ row);

dt , AcceptChangesC } ;
return dt;
}
146 Microsoft ADO.NET

Реализация JSerializable

Другой, более элегантный и эффективный способ сериализащш объек-


та DataTable в истинно двоичный поток заключается в создании про-
изводного класса и реализации в нем интерфейса ISerializable. Этот
способ позволяет еериализуемому классу контролировать свою сери-
ализацию. В чем его преимущества? Применяя ISemlizable, вы обра-
щаетесь к данным только раз. В противоположность этому теневой
сериализующий класс сначала копирует данные из DataTable в своя
свойства и только оттуда они действительно сернализуются в поток,
Как видите, в конечном итоге данные перемешаются дважды. Кроме
того, решение на основе интерфейса ISerializable будет в большей мере
совместимым со следующими версиями ADO.NET.

Класс BinDataTable (пример его структуры показан ниже) может ис-


пользовать те же приемы для решения своей задачи, что и теневой се-
риализующий класс,
ESerializatele]
риШе class BinDataTable : DataTable, ISerializable -
{
protected 8inQataTable(SerializaticmInfQ si, StreaningContext ctx}

void System, Runtime, Serialization. ISerializable.


QetObjectDataCSerializatlonlnfo si, StreafltingContext ctx)

Метод GetObjectData копирует информацию, важную для DataTable, в ;


потоковый контекст. Защищенный конструктор сериализующего клас-
са делает обратное: извлекает данные из потока и инициализирует но-
венький объект BinDataTable. Полный исходный код, демонстрирую-
щий данный подход, можно скачать с сайта MSDN Magazine по уже .
приводившейся ссылке.

Самое важное, на что здесь следует обратить внимание: как объект Data-
Table сопоставляется с классом GhostDataTableSerializer. Сопоставление
осуществляется в методе Load.

В нем рабочие массивы (colNames и colTypes) заполняются именами и ти-


пами столбцов, Внутренний массив DataRow содержит массив объектов,
соответствующих всем значениям в строке.
Двоичная сериализация ADO.NET-объектов 147

public void Load(DataTable dt)


{
foreach(DataColunm col in dt.Columns) {
colNames.Add(col.ColumnName);
colTypes.Add(col.Datatype,FullName);

foreach(DataRow row in dt.Rows)


dataRows.Add{row.ItemArray);

Свойство ItemArray объекта DataRow — массив объектов. Оно особенно


полезно, так как позволяет обращаться к содержимому всей строки как к
единому, монолитному блоку данных. Внутренний аксессор чтения (get
accessor) этого свойства реализован в виде простого цикла, извлекающего
и сохраняющего один столбец за другим. Аксессор записи (set accessor)
еще ценнее, так как он автоматически группирует все изменения в вызо-
вы пары BeginEdit/EndEdit и при необходимости генерирует события,
связанные с модификацией столбцов.

Заметьте, что вызов AcceptChanges (рис. 4) сбрасывает состояние всех


строк в Unchanged, препятствуя достижению одной из основных задач се-
риализации — сохранению состояния объекта. Если вы прибегнете к ка-
ким-нибудь уловкам и откажетесь от вызова этой функции, все строки
будут помечены как Added. Другими словами, состояние строк не сохра-
нится, и тогда, чтобы добиться нужной цели, теневому сериализатору при-
дется возиться еще и с информацией о состоянии.

Рис. 5 иллюстрирует работу приложения-примера. Как видите, объект


DataTable, сериализованный с помощью теневого класса, на 80% меньше,
чем такой же объект, сериализованный обычным способом с использова-
нием того же двоичного сериализатора. Возьмем, к примеру, объект Data-
Table, получаемый в результате выполнения запроса:
SELECT * FROM [Order Details]

ilsiiio a Ghost berlaiazef ties*.

Рис. 5. Приложение-пример в действии


148 Microsoft ADO.NEt

Таблица содержит пять столбцов и более 2000 записей. Если ее сериали-


зовать двоичным форматирующим объектом как объект DataTable, она
займет полмегабайта. А при использовании промежуточного теневого се-
риализатора конечный файл будет примерно на 83% меньше.

Конечно, такие впечатляющие результаты получаются не всегда. Чем


больше та часть таблицы, в которой содержатся числа, тем больше места
вы экономите. И наоборот, чем больше полей типа BLOB, тем меньше вы-
года. Попробуйте выдать следующий запрос к базе данных Northwind в
SQL Server, где BLOB-поле содержит фотографию работника:
SELECT photo FROM employees

Здесь экономия составит всего 25%, и это максимум, чего я добился в сво-
их тестах. Причина, думаю, очевидна. BLOB-поля сами содержат двоичные
данные, поэтому экономия невелика.

Десериализация данных
После того как двоичные данные десериализованы, вы должны преобразо-
вать экземпляр теневого класса обратно в объект DataTable. Посмотрим,
как это делается в приложении-примере:
DataTable BinaryDeserialize(string sourceFile)
{
BinaryFormatter bin = new BinaryFormatterO;
StreamReader reader = new StreamReader(sourceFile);
GhostDataTableSerializer ghost;
ghost = (GhostDataTableSerializer) of.Deserialize(reader.BaseStream);
sr.Closef);

return ghost.Savef);
}

Метод Save пользуется информацией, сохраненной в массивах теневого


класса, для добавления строк и столбцов к только что созданному объек-
ту DataTable (рис. 6).

А как насчет суррогатных типов?


Сериализация объекта DataTable с помощью суррогатного класса проста,
эффективна и не создает никаких проблем. Однако из-за того, что проис-
ходит при десериализации, суррогаты непрактичны для ADO.NET-объек-
тов. Ниже показан фрагмент кода, который вы, возможно, захотите напи-
сать для сериализации объекта DataTable через суррогатный класс Data-
TableSurrogate:
Двоичная сериализацмя ADQ.NET-объектов 149

Рис. 6. Добавлениеетйок и столбцов


public DataTable Save()
{
DataTable dt - new DataTableO;

// Добавить столбцы
for(int 1=0; KcolNames. Count;

BataColuim col = new OataColumn(colNames[i].ToStrlng(),


Type.GetType(eolTypes[i].ToString()));
dt.Coiueins.Add(col);

// Добавить строки
forfint i=0; Kdataflows.Count; i-н-)
{
OataRow row = dt.NewRowO;
row.IteieArray = (objectU) dataRows[i];
dt.Rows.Add(row);

dt.AceeptChanges();
return dt;
*

SurrogateSelector ss = new SurrogateSelector();


DataTableSurrogate dts = new DataTableSurrogate();
ss.AddSurrogate(typeof(DataTable),
new StreamingContext(StreamingContextStates.All), dts);
formatter.SurrogateSelector = ss;
formatter.Serialize(dat,BaseStream, dt);

Сначала вы создаете селектор суррогата (surrogate selector) и добавляете


суррогатный тип в цепочку суррогатов. Затем связываете селектор с фор-
матирующим объектом. Суррогатный класс реализует интерфейс ISerializa-
tion Surrogate, состоящий из двух методов: GetObjectData и Set Object Data.
Первый из них добавляет элементы в набор Serializationlnfo, как показа-
но на рис. 7. На рис. 8 приведена возможная реализация метода, имитиру-
ющего поведение теневого сериализующего класса, рассмотренного ранее.
Что касается быстродействия, то результаты практически одинаковы.

Как я уже говорил, причины, по которым суррогаты не годятся для сериа-


лизации ADO.NET-объектов, связаны с архитектурой этих объектов. Они
крайне сложны, и их конструкторы играют очень важную роль, Чтобы
объект DataSet или DataTable работал корректно, вы должны вызвать один
из его предопределенных конструкторов. А исполняющая среда .NET при
150 Microsoft ADO.NET

обработке суррогатов не пользуется конструкторами для создания экзем-


пляров объектов. Вместо этого она обращается к статическому методу
GetUninitializedObject класса FormatterServices, и новый экземпляр объек-
та инициализируется нулями. Кроме того, поскольку конструкторы не за-
пускаются, экземпляр может оказаться в рассогласованном состоянии, и
вроде бы безопасные операции могут вызывать неожиданные сбои.

Рис. 7. Суррогатный класс


public class DataTableSurrogate : ISerializationSurrogate
{
public void GetQftjectData(object obj,
Serializatlonlnfo iRfo, StreamingContext context)
{
DataTable dt = (DataTable) obj;
AddSerializationInfo(<ft, info);

public object SetObjectData(object obj, Serializationlnfo info,


StreamingContext context, JSurrogateSelector selector)

DataTable dt * (DataTable) obj;


ReadSerializationInfo(dt, info);
return null;

P«c. 8. Имитация теневого сериализатора


protected void Flll(BataTable dt. Serializationlnfo info)
{
ArrayList colNames - new ArrayLlstO;
Array-list colTypes = new ArrayListO;
ArrayList dataflows = new ArraylistC);

// Вставить информацию о столбцах (имена и типы)


// в рабочие массивы
foreacnCDataColumfl col in dt.Columns)

colNames.Add<col.ColumnName);
colTypes.Add(col.DataType.FullName);

// Вставить информацию о строках в рабочие массивы


f0reacft(DataRow row in dt.Ro«s)
dataflows. Ш( row. IteaArray);

см. след. стр.


Двоичная сериализация АОО.ЫЕТ-объектов 151

Рис. 8. Имитация теневого сериализатора (окончание)


// Добавить массивы в структуру с информацией,
// необходимой для сериализации
info,AddValae{"ColNaies", colNames);
info.AddValue("ColTypes", colTypes};
info.AddValue<"DataRows", dataflows);

Например, конструктор объекта DataTable по умолчанию выполняет длин-


ный список операций по инициализации членов. И не все внутренние чле-
ны этого объекта инициализируются значениями null. На самом деле этот
объект опирается на множество внутренних массивов и других вспомога-
тельных объектов, инициализируемых конструктором. Метод GetUninitia-
HzedObject очищает блок памяти и не инициализирует ни один из таких
объектов. С другой стороны, суррогатный класс вряд ли посвящен в тон-
кости внутренней реализации замещаемого им класса. То есть, чтобы точ-
но знать, что нужно для корректной инициализации объекта DataTable, вы
должны сначала изучить его исходный код, Теперь допустим, вы сделали
это (с помощью .NET-декомпилятора). Тогда вам придется вовсю пользо-
ваться механизмом отражения для доступа к внутренним и закрытым чле-
нам объекта DataTable, Короче говоря, теневой сериализатор дает практичес-
ки ту же экономию дискового пространства, но гораздо проще в создании.

Заключение
Под занавес хочу еще раз отметить, что объективные трудности с исполь-
зованием суррогатных типов в процессе ADO.NET-сериализации возника-
ют по двум причинам. Первая — упомянутые объекты крайне сложны и
связаны с другими объектами. Однако, согласно Джеффри Рихтеру, есть и
другая причина, которая, надеюсь, скоро будет устранена, — ошибка в коде
форматирующего объекта. Она приводит к тому, что SetObjectData не воз-
вращает совершенно новые типы. После того как ошибку исправят, вы
сможете легко избегать использования системой метода Get Uninitialized,
вызывая конструктор объекта из SetObjectData и возвращая созданный
объект форматирующему объекту. Подробнее об этой ошибке см. статью в
рубрике «.NET» в сентябрьском выпуске журнала за 2002 г. (http://
msdn.microsoft.com/msdnmag/issues/02/09/net/default.aspx).

Дино Эспозито (Dino Esposito) — консультант и преподаватель из Рима.


Автор книг «Building Web Solutions with ASP.NET and ADO.NET» и «Applied XML
Programming for .NET» (обе выпущены издательством Microsoft Press в
2002 г.). С ним можно связаться по адресу dinoe@wintellect.com.
Прийя Дхаван

Разработка
распределенных
приложений в .NET

В этой статье описывается, как выполнять локальные и распределенные


транзакции в приложениях Microsoft .NET.

Введение
Транзакция — это последовательность операций, выполняемых как единое
целое. Благодаря объединению взаимосвязанных операций в транзакцию
гарантируется согласованность и целостность данных в системе, несмотря
на любые ошибки, которые могли возникнуть в ходе транзакции. Для ус-
пешного выполнения транзакции необходимо успешное завершение всех
ее операций.

У транзакции есть начало и конец, определяющие ее границы (transaction


boundaries), внутри которых транзакция может охватывать различные про-
цессы и компьютеры. Все ресурсы, используемые в ходе данной транзак-
ции, считаются участвующими в этой транзакции. Для поддержания цело-
стности используемых ресурсен транзакция должна обладать свойствами
ACID: Atomicity (атомарность). Consistency (целостность), Isolation (изо-
ляция) и Durability (отказоустойчивость). Подробнее об основах обработ-
ки транзакций см. «Processing Transactions» (http://rasdn.microsoft.com/

Priya Dhawan Transaction Control. Building Distributed Applications with .NET//MSDN


Library. 2001. November. — Прим. изд.
Разработка распределенных приложений в .NET J.53

library/en-us/cpguide/html/cpconprocessingtransactions.asp) в Microsoft
.NET Framework SDK и в Microsoft Platform SDK.

В этой статье показывается, как выполнять локальные и распределенные


транзакции в приложениях Microsoft .NET.

Локальные и распределенные транзакции


Локальной называется транзакция, областью действия которой является
один ресурс, поддерживающий транзакции, — база данных Microsoft SQL
Server, очередь сообщений MSMQ и др. Например, отдельно взятая СУБД
может вводить в действие правила ACID, когда в ее распоряжении имеют-
ся все данные, участвующие в транзакции. В SQL Server предусмотрен
внутренний диспетчер транзакций (transaction manager), предоставляю-
щий функциональность для фиксации (commit) и отката (rollback) тран-
закций.

Распределенные транзакции могут использовать гетерогенные ресурсы,


поддерживающие транзакции, включать самые разнообразные операции,
например выборку информации из базы данных SQL Server, считывание
сообщений Message Queue Server и запись в другие базы данных. Програм-
мирование распределенных приложений упрощается программным обес-
печением, способным координировать фиксацию и откат, а также восста-
новление данных, хранящихся в различных ресурсах. Одной из таких тех-
нологий является Microsoft DTC (Distributed Transaction Coordinator).
DTC реализует протокол двухфазной фиксации (two-phase commit proto-
col), гарантирующий непротиворечивость результатов транзакции во всех
ресурсах, участвующих в этой транзакции. DTC поддерживает только при-
ложения, в которых реализуются совместимые с ним интерфейсы управ-
ления транзакциями. Эти приложения называются диспетчерами ресурсов
(Resource Managers). [Дополнительную информацию по этой теме см. в
«Distributed Transactions» (http://msdn.microsoft.com/library/en-us/cpgui-
de/htnil/cpcondistributedtransactions.asp) в .NET Framework Developer's
Guide.] В настоящее время существует довольно много таких приложений -
MSMQ, Microsoft SQL Server, Oracle, Sybase и др,

Транзакции баз данных


Вызов хранимой процедуры (stored procedure), которая заключает необхо-
димые операции в операторы BEGIN TRANSACTION и COMMIT/ROLL-
BACK TRANSACTION, дает наилучшую производительность, позволяя
выполнить транзакцию с разовым обменом данными е сервером (single
round-trip). Кроме того, транзакции баз данных могут быть вложенными,
т. е. внутри активной транзакции можно начать выполнение новой тран-
закции,
154 Microsoft ADO.NET

В следующем фрагменте кода оператор BEGIN TRANSACTION начинает


новую транзакцию. Транзакцию можно завершить двумя способами: либо
фиксацией изменений в базе данных оператором COMMIT TRANSAC-
TION, либо (при возникновении какой-либо ошибки) отменой всех изме-
нений оператором ROLLBACK TRANSACTION.
CREATE PROCEDURE Prod

AS
-- Начинаем транзакцию
BEGIN TRANSACTION
-- Выполняем операции транзакции

— Проверяем наличие ошибок


If @@Error О О
- Откатываем транзакцию
ROLLBACK TRANSACTION

- Фиксируем транзакцию
COMMIT TRANSACTION

Показанная ниже хранимая процедура принимает в качестве входного па-


раметра XML-представление информации о заказе (order). Для выполне-
ния соответствующих вставок в таблицы Orders и OrderDetails хранимая
процедура загружает и анализирует XML с помощью системной хранимой
процедуры sp_xrnlpreparedocument. Как видно из исходного кода, все опе-
рации хранимой процедуры включены в явно выполняемую транзакцию,
поэтому при неудачном завершении любой операции все внесенные изме-
нения отменяются (откатываются).

Заметьте, что процедура устанавливает флаг XACT_ABORT в ON, указы-


вая, что SQL Server должен автоматически откатить транзакцию, если
выполнить какой-нибудь оператор не удастся.
CREATE PROCEDURE InsertOrder
@0rder NVARCHAR(4000) = NULL
, eOrderld int Output
AS
SET NOCOUNT ON
DECLARE @hDoc INT
DECLARE @PKId INT
-- Указываем, что SQL Server должен автоматически откатывать текущую
-- транзакцию, если оператор Transact-SQL генерирует ошибку периода
— выполнения (run-time error)
SET XACT_ABORT ON
- Начинаем транзакцию
BEGIN TRANSACTION
- Загружаем и анализируем содержимое входного XML-представления
Разработка распределенных приложений в .NET 155

-- информации о заказе, а затем помещаем его в XMLDocument


EXEC sp_xml_preparedocument @hDoc OUTPUT. @0rder
-- Выбираем заголовок заказа из XMLDocument-уэла Orders
-- и вставляем его в таблицу Orders
INSERT Orders(CustomerId,
OrderDate,
ShipToName,
ShipToAddressId,
OrderStatus)
SELECT Customerld, CONVERT(DateTime,OrderDate), ShipToName,
ShipToAddressId, OrderStatus
FROM OPENXHL(@hDoc, '/NewDataSet/Orders')
WITH ( Customerld int 'Customerld1,
OrderDate nvarchar(23) 'OrderDate',
ShipToName nvarchar(40) 'ShipToName',
ShipToAddressId int 'ShipToAddressId',
OrderStatus int 'OrderStatus')
-- Выбираем Orderld заказа, только что вставленного в таблицу Orders
— для использования при вставке позиций заказа (order details)
SELECT SPKId = ©©IDENTITY
- Выбираем позиции заказа из XHLDocument-узла Details
-- и вставляем их в таблицу QrderDetails
INSERT OrderDetails (Orderld,
Itemld,
UnitPrice,
Quantity)
SELECT ®PKId as Orderld, Itenld, UnitPrice, Quantity
FROM OPENXML(@hDoc, '/NewDataSet/Details')
WITH (Itemld int 'Itemld',
UnitPrice money 'UnitPrice',
Quantity int 'Quantity')
- Присваиваем значение выходному параметру
Select @0rderld = QPKId
- Фиксируем транзакцию
COMMIT TRANSACTION
EXEC sp_xml_removedocument @hDoc
RETURN 0
GO

Хотя такой подход обеспечивает хорошую производительность, при его


использовании приходится программировать на Transact SQL, а это слож-
нее, чем на языке, совместимом с .NET.

Транзакции вручную
Транзакции, выполняемые вручную (manual transactions), позволяют явно
управлять границами транзакции с помощью команд начала и конца тран-
закции. В этой модели также поддерживаются вложенные транзакции, т. е.
вы можете начинать новую транзакцию в рамках активной транзакции.
156 Microsoft ADO.NET

Расплата за возможность такого управления заключается в том, что на вас


ложится бремя включения в транзакцию нужных ресурсов данных и их
координация. Встроенной поддержки распределенных транзакций нет,
поэтому управление распределенной транзакцией вручную потребует
взять на себя массу обязанностей. Вам придется управлять каждым под-
ключением к ресурсу и его использованием, а также реализовать поддер-
жку свойств ACID в транзакции.

Транзакции ADO.NET, выполняемые вручную


Транзакции вручную поддерживают оба провайдера данных Microsoft
ADO.NET, которые предоставляют набор объектов, позволяющих созда-
вать соединение с хранилищем данных, начинать транзакцию, фиксиро-
вать или откатывать ее и, наконец, закрывать соединение. В примерах мы
будем использовать управляемый ADO.NET-провайдер SQL (ADO.NET
SQL managed provider).

Для выполнения операций в рамках единой транзакции нужно создать


объект SQLTransaction, начать транзакцию с помощью объекта SQLCon-
nection, добиться, чтобы все операции над базой данных проходили в этой
транзакции, и зафиксировать или отменить транзакцию. Объект SQLTran-
saction предоставляет целый ряд свойств и методов для управления тран-
закцией. При успешном выполнении всех операций транзакции вы може-
те зафиксировать изменения в базе данных методом Commit. Для отката
изменений применяется метод Rollback объекта SQLTransaction.

Примечание Для выполнения SQL-команды в транзакции свойство Transaction


объекта Command необходимо установить на уже начатую транзакцию.

Visual Basic .NET


Dim conn as SQLConnection
Dim cmd as SQLComrcand
Dim txn As SQLTransaction
conn = New SQLConnectionC'ConnString")
cmd = New SQLCommand
Открываем соединение
conn.OpenO
Начинаем транзакцию
txn - conn.BeginTransaction()
Настраиваем свойство Transaction на транзакцию, где выполняется
' SQL-команда
cmd.Transaction - Txn
Разработка распределенных приложений в .NET

Visual C# .NET
SQLConnection Conn = New SQLConnectlon(TTConnString");
SQLCommand Cmd = Kew SQLCommand;
// Открываем соединение
Conn.OpenO;
// Начинаем транзакцию
3QLTransaction Txn ~ Conn.Begin!ransaction();
// Настраиваем свойство Transaction на транзакцию, где выполняется
// SQL-команда
Crnd.Transaction - Txn;

В примере, представленном ниже, мы выполняем в рамках транзакции две


SQL-команды. Первая вставляет заголовок заказа (order header) в табли-
цу Orders и возвращает Orderld только что созданного заказа. Этот Order-
Id используется во второй команде, которая вставляет позиции этого за-
каза в таблицу OrderDetails. Транзакция отменяется, если хотя бы одна из
двух команд терпит неудачу; при этом строки в базу данных не добавля-
ются.

Visual Basic .NET


Dim conn As SqlConnection
Dim cmd As SqlComntand
Dim tran As SqlTransaction
1
Создаем новое соединение
conn = New SqlConnection("ConnString")
' Открываем соединение
conn.OpenO
' Создаем объект Command
cmd = New SqlCommandO
1
Создаем транзакцию
tran = conn.BeginTransaction
' Настраиваем свойство Transaction на транзакцию, где выполняется
1
SQL-команда
cmd.Transaction = tran
Try
' Вставляем заголовок заказа. Настраиваем свойства Command.
With cmd
.CommandType = CommandType.StoredProcedure
,CommandText = "InsertOrderHeader"
.Connection = conn
' Добавляем входные и выходные параметры
.Parameters.Add("@Customerid", SqlDbType.Int)
.Parameters("@Customerid").Direction = ParameterDirection.Input
1
Устанавливаем значения параметров
.Parameters("§CustomerId").Value = 1
158 Microsoft ADO.NEt

' Выполняем команду


. ExecuteNonQueryO
' Получаем Qrderld добавленного заголовка заказа
Qrderld = .Pararneters("@0rderld").Value
' Очищаем параметры для следующей команды
.Parameters.clearf)
End With

' Вставляем позиции заказа. Настраиваем свойства Command.


With cmd
.CommandType = CommandType.StoredProcedure
,CommandText = "InsertOrderDetail"
.Connection = conn
' Добавляем параметры
.Parameters.Add("£0rderld", SqlDbType.Int)
. Parameters("@|OrderId"}.SourceColumn = "Orderld"
.Parameters("@0rderld").Direction = ParameterDirection,Input

' Устанавливаем значения параметров


.Parameters*"@0rderld").Value = Orderld
.Parameters("@ltemld").Value = 100

' Выполняем команду


, ExecuteNonQueryO
' Повторяем показанные выше строки для каждой позиции заказа
End With

' Фиксируем транзакцию


tran.Commit()
Catch
1
Откатываем транзакцию
tran.Rollback()
Finally
1
Код очистки. Закрываем соединение.
conn.Close()
End Try

Как видите, две команды выполняются как часть одной транзакции. Если
одна из них терпит неудачу, транзакция отменяется, и любые изменения в
базе данных откатываются. Заключив код в блок try /catch/finally, вы га-
рантируете корректное выполнение транзакции: она фиксируется в самом
конце блока try после успешного выполнения обеих SQL-команд. Любое
исключение перехватывается в блоке catch, где транзакция отменяется и
изменения, внесенные в ходе этой транзакции, откатываются.

Управление транзакциями через объекты ADO.NET приводит к менее


эффективному блокированию, чем при использовании явных транзакций
в хранимых процедурах. Причина в том, что при транзакциях ADO.NET,
Разработка распределенных приложений в .NET 15Э

выполняемых вручную, треоуется как минимум столько же двусторонних


обменов данными с СУБД, сколько операций выполняется в транзакции
плюс два обмена в ее начале и конце. Блокировки удерживаются в течение
всего времени передачи вызовов из кода ADO.NET на сервер базы данных
и обратно.

Транзакции MSMQ, выполняемые вручную


.NET Framework предусматривает два вида поддержки транзакций MSMQ:
внутреннюю (для транзакций вручную) и внешнюю (для автоматических
транзакций). В первом случае в рамках транзакции возможен прием или
передача нескольких сообщений. Во втором — сообщения участвуют в
транзакциях DTC (Distributed Transaction Coordinator).

Транзакции MSMQ, выполняемые вручную, поддерживаются классом


MessageQueueTransaction и обрабатываются исключительно ядром MSMQ.
Подробности см. в статье Дункана Мак-Кензи (Duncan Mackenzie) «Reli-
able Messaging with MSMQ and .NET» (http://msdn.microsoft.com/library/
en-us/dnbda,/html/bdadotnetasync2.asp).

Автоматические транзакции
Поддержка автоматических транзакций в .NET Framework опирается на
службы MTS/COM+. СОМ+ использует DTC в качестве диспетчера и
координатора транзакций в распределенной среде. Это позволяет прило-
жениям .NET выполнять транзакции, охватывающие разнообразные опе-
рации над множеством ресурсов, например вставку заказа в базу данных
SQL Server, запись сообщения в очередь MSMQ (Microsoft Message Queue),
отправку сообщения электронной почты и считывание информации из
базы данных Oracle.

Предоставляя модель программирования на основе декларативных тран-


закций (declarative transactions), COM+ резко упрощает выполнение тран-
закций, в которых участвуют гетерогенные ресурсы. Но учтите, что за это
приходится расплачиваться снижением производительности, связанным с
издержками взаимодействия DTC и СОМ; кроме того, поддержка вложен-
ных транзакций отсутствует.

Страницы ASP.NET, методы Web-сервисов и .NET-классы можно помечать


как транзакционные, присваивая им атрибут декларативной транзакции
(declarative transaction attribute).

ASP.NET

<@ Page Transaction="Required">


160 Microsoft ADO.NET

Web-сервис ASP.NET

<%@ WebService Language="VB" Class="Class1" X>


<W assembly name="System.EnterpriseServices" %>

Public Class Classl


Inherits WebService
<WebMethod(TransactionOption := TransactionOption.RequiresNew)>
Public Function Methodic)

Для участия в автоматических транзакциях .NET-класс должен наследо-


вать от System.EnterpriseServices.ServicedComponent, который обеспечива-
ет выполнение класса в СОМ+. Если вы сделаете именно так, СОМ+, вза-
имодействуя с DTC, создаст распределенную транзакцию и подключит к
ней все необходимые ресурсы без вашего участия. Кроме того, вам нужно
присвоить классу атрибут декларативной транзакции, чтобы определить
его поведение при выполнении транзакции.

Visual Basic .NET


<Transaction(TransactionOption.Required)> Public Class Classl
Inherits ServicedComponent

Visual C# .NET
[Transactlon(TransactionOption. Required)]
public class Classl : ServicedComponent {

Транзакцией ный атрибут класса принимает одно из следующих значений.

• Disabled. Указывает, что объект никогда не создается в транзакции


СОМ+. Для поддержки транзакций объект может обращаться к DTC
напрямую.
• NotSupported. Указывает, что объект никогда не создается в транзак-
ции.
• Supported. Указывает, что объект выполняется в контексте транзакции
своего создателя. Если объект сам является корневым или если его созда-
тель не выполняется в транзакции, объект создается вне транзакции.
• Required. Указывает, что объект выполняется в контексте транзак-
ции своего создателя. Если объект сам является корневым или если его
создатель не выполняется в транзакции, при создании такого объекта
создается новая транзакция.
Разработка распределенных приложений в ,NET 161

* RequiresNew. Указывает, что объекту нужна транзакция и что при


его создании создается новая транзакция.

В следующем коде содержится .NET-класс, настроенный на выполнение в


СОМ+. Кроме того, атрибутам сборки присваиваются значения, необходи-
мые для конфигурирования свойств СОМ+-приложения.

Visual Basic .NET

Imports System
Imports System.Runtime.CompilerServices
Imports System.EnterpriseServices
Imports System.Reflection
1
Детали регистрации.
1
Имя COM-»--приложения в том виде, в каком оно присутствует
' в каталоге СОМ+
<Assembly: ApplicationName("Classl"}>
' Строгое имя (strong name) для сборки (assembly)
<Assembly: AssemblyKeyFileAttribute("class1,snk")>
<Assembly: ApplicationActivation(ActivationOption.Server)>

<Transaction(TransactionOption.Required}> Public Class Classl


Inherits ServicedComponent
Public Sub Example1()

End Sub
End Class

Visual C# .NET
using System;
using System,Runtime.CompilerServices;
using System.EnterpriseServices;
using System.Reflection;

// Детали регистрации,
// Имя СОН+-приложения в том виде, в каком оно присутствует
// в каталоге СОМ+
[Assembly; ApplicationName("Classl")]
// Строгое имя для сборки
[Assembly: AssemblyKeyFileAttribute("class!.snk")]
[Assembly: ApplicationActivation(ActivationOption.Server)]

[Transaction(TransactionOption,Required)]
public class Classl : ServicedComponent {
[AutoComplete]
public void Example1()
i
162 Microsoft ADO.NET

<Assembly; ApplicationName(«Classl»)> указывает имя СОМ+-приложе-


ния, в которое устанавливаются компоненты сборки, a <Assembly: Applica-
tionActivation(ActivationOption.Server)> определяет, является ли это при-
ложение сервером или библиотекой. Когда вы указываете Application Acti-
vation(ActivationOption.Server), сборку необходимо установить в GAC
(global assembly cache) с помощью утилиты командной строки gacutil
(GacUtil.exe).

Для преобразования сборки в библиотеку типов, регистрации библиотеки


типов и ее установки в заданное СОМ+-приложение можно использовать
утилиту командной строки Regsvcs.exe. Кроме того, эта утилита настраи-
вает свойства, добавленные в сборку программным способом. Например,
если в сборке указано Application Activation( Activation Option. Server), ути-
лита создаст серверное приложение. Если вызванная сборка еще не уста-
новлена в СОМ+, исполняющая среда создаст и зарегистрирует библиоте-
ку типов, а затем установит ее в СОМ+. СОМ+-приложение, созданное
для сборки, можно просмотреть и настроить в оснастке Component Ser-
vices.

Процесс создания, регистрации и использования обслуживаемых компо-


нентов (serviced components) подробно рассматривается в разделе «Wri-
ting Serviced Components» (http://msdn.microsoft.com/library/en-us/cpgui-
de/html/cpconwritmgservicedcomponents.asp) в .NET Framework Develo-
per's Guide.

В следующем коде показывается транзакционный класс, сконфигуриро-


ванный для запуска под управлением СОМ+, в котором в рамках транзак-
ции выполняются две SQL-команды. Первая вставляет заголовок заказа в
таблицу заказов и возвращает Orderld добавленного заказа. Этот Orderld
используется второй командой при вставке позиций заказа в таблицу
OrderDetails. Транзакция отменяется, если не удалось выполнить хотя бы
одну из двух команд; при этом записи в базу данных не добавляются.

Visual Basic .NET


<Transaction(TransactionOption.Required)> Public Class Classl
Inherits ServicedComponent

Public Sub Exampl«1()

Try
' Создаем новое соединение
Разработка распределенных приложений е .NET 163

conn = New SqlConnectionC'ConnString")


' Открываем соединение
conn.OpenO
' Создаем новый обьект Command
cmd = New SqlCommandO
' Вставляем заголовок заказа,
1
Присваиваем значения свойствам Command.
With cmdl
.CommandType = CommandType.StoredProcedure
.CommandText = "InsertQrderHeader"
.Connection = conn
' Добавляем входные и выходные параметры
.Parameters.Add("@CustomerId", SqlDbType.Int)

, ExecuteNonQuervO
Очмцаем параметры для следующей команды
.Parameters.clear()
End With

' Вставляем позиции заказа,


1
Настраиваем свойства Command,
With cmd
.CommandType = CommandType.StoredProcedure
.CommandText = "InsertOrderDetail"
.Connection = conn
Добавляем параметры
.Parameters.Add("©Orderld", SqlDbType.Int)

Выполняем команду
. ExecuteNonQueryO
Повторяем эти строки для каждой позиции заказа
End With

' Фиксируем транзакцию


Contextlltil. SetComplete()
Catch
' Откатываем транзакцию
ContextUtil.SetAbortO
Finally
' Код очистки
End Try
End Sub

Используя класс System. Enterprise Services. Context Util, можно получить


информацию о контексте СОМ+-объекта. Этот класс предоставляет мето-
ды SetComplete и SetAbort, позволяющие явным образом фиксировать и
откатывать транзакцию. Легко догадаться, что метод ContextUtil.Set-
Complete вызывается в самом конце блока try, когда все операции выпол-
нены успешно и нужно зафиксировать транзакцию. Все исключения пере-
164 Microsoft ADO.NET

хватывается в блоке catch, где транзакция отменяется с помощью


Context Util.SeiAbort.

Кроме того, с помощью класса -атрибута (attribute class) System. Enterprise-


Services.AutoComplete можно добиться, чтобы обслуживаемый компонент
автоматически определял, фиксировать или откатывать транзакцию. Ком-
понент «голосует» за фиксацию транзакции, если вызов метода завершился
успешно. Если вызов метода привел к генерации исключения, транзакция
автоматически отменяется; явный вызов ContextUtil.SetAbort не нужен.
Чтобы воспользоваться этой возможностью, вставьте атрибут <AutoComp-
lete> перед методом класса:

Visual Basic .NET


<Transaction(TransactionOption.Required)> Public Class Class"!
Inherits ServicedComponent
<AutoComplete()> Public Sub Example1{)

End Sub
End Class

Visual C# .NET
[Transact ion(TransactionOpt ion. Required)]
public class Class"! : ServicedComponent {
[AutoComplete]
public void Example1()

'<

Атрибут <AutoComplete> предлагает самый простой способ программиро-


вания транзакций, позволяя обходиться без явных фиксации и отката
транзакций. Вы получите точно такую же функциональность, что и в пре-
дыдущем примере, где для отмены транзакции явно вызывался метод
ContextUtil.SetAbort в блоке catch. Недостаток этого способа в том, что
выполнение транзакции неочевидно и при сопровождении кода о ней мож-
но забыть. Кроме того, нет возможности вывода дружественного к пользо-
вателю сообщения, если транзакция терпит неудачу. В таких случаях сле-
дует явно перехватывать все исключения, вызывать ContextUtil.SetAbort
и показывать требуемое сообщение.

В системах, где нужно выполнять транзакции, использующие MSMQ, и


другие ресурсы, единственно возможный выбор — применение транзакций
DTC или СОМ+. DTC координирует все диспетчеры ресурсов, участвую-
щие в распределенной транзакции, а также управляет деятельностью, свя-
Разработка распределенных приложений в .NET

занной с транзакциями. Пример распределенной транзакции MSMQ и


SQL Server см. в статье Дункана Мак-Кензи «Reliable Messaging with
MSMQ and .NET» (http://msdn.microsoft.com/library/en-us/dnbda/html/
bdadotnetasync2.asp).

Заключение
При использовании каждой из технологий работы с транзакциями прихо-
дится идти на компромисс между производительностью приложения и
удобством сопровождения кода. Запуск реализованной в хранимой проце-
дуре транзакции базы данных обеспечивает наилучшую производитель-
ность, так как требуется лишь один двусторонний обмен информацией с
базой данных. Кроме того, обеспечивается гибкость управления транзак-
циями, поскольку явно указываются начало и завершение транзакции. Но,
хотя этот способ дает хорошую производительность и гибкость, приходит-
ся программировать на Transact SQL. что не так легко, как па языках .NET.

Транзакции, выполняемые вручную с помощью транзакционных объектов


ADO.NET, просты в программировании. Благодаря явному применению
инструкций начала и завершения транзакции, возможно гибкое управле-
ние границами транзакции. Платой за эту легкость и гибкость является
снижение производительности за счет дополнительных двусторонних об-
менов информацией с базой данных при выполнении транзакции.

Автоматическая транзакция — единственно возможный выбор, когда тран-


закция использует несколько диспетчеров ресурсов, поддерживающих
транзакции, например базы данных SQL Server, очереди сообщений MSMQ
и т. д. Они значительно упрощают разработку приложений и предъявля-
ют более низкие требования к квалификации программиста. Однако из-за
того, что всю работу по координации выполняет служба СОМ+, возмож-
ны дополнительные издержки.
альманах программиста
Джонни Папа

Пять способов подстегнуть


производительность SQL*

Здесь показывается, как увеличить либо производительность, либо масшта-


бируемость приложений на основе SQL Server. Вы узнаете о тонкостях
использования SQL-операторов IDENTITY, LEFT JOIN и CROSS JOIN. Также
рассматриваются ситуации, где замена HAVING на WHERE и GROUP BY на
DISTINCT дает ощутимый выигрыш в производительности.

Иногда, чтобы заставить приложение работать гораздо быстрее, достаточ-


но подкрутить пару «гаек». Только надо знать — где! Рано или поздно
сталкиваешься с ситуацией, когда какой-нибудь SQL-запрос в приложе-
нии действует вовсе не так, как было задумано: или не возвращает нужные
данные, или выполняется слишком долго. Кому понравится, если ваше
корпоративное приложение будет задерживать результаты запросов? По-
мните, как ваши родители и слушать не хотели объяснений, почему вы
явились домой так поздно? Пользователей тоже не волнует, почему их
запросы выполняются столько времени. («Прости, мама. В моем запросе
было слишком много операторов LEFT JOIN.») Они хотят, чтобы прило-
жения отвечали быстро, а аналитические данные в отчетах появлялись
моментально. Меня самого раздражает, если какая-нибудь Интернет-стра-
ница грузится дольше десяти секунд. (Ну хорошо, дольше пяти.)

Для решения этих проблем важно разобраться в их причинах. С чего на-


чать? Главная причина обычно связана со структурой базы данных и зап-
росов. Сегодня я продемонстрирую четыре способа, позволяющих увели-
чить либо производительность, либо масштабируемость приложений на

Публиковалось в MSDN Magazine/Русская Редакция. 2002. №1 (июль). — Прим. изд.


170 Microsoft SQL Server

основе SQL Server. Я рассмотрю применение операторов LEFT JOIN и


CROSS JOIN, а также получение значения IDENTITY. Но учтите, что уни-
версальных решений нет. Настройка базы данных и запросов требует вре-
мени, анализа и тестирования. Хотя все предлагаемые мной способы дока-
зали свою эффективность, в вашем приложении какие-то из них окажут-
ся лучше, чем другие.

Получение IDENTITY из запроса INSERT


Начну, пожалуй, с задачи, по которой мне задают массу вопросов: как по-
лучить значение IDENTITY после выполнения SQL-оператора INSERT?
Зачастую проблема не в том, как написать соответствующий запрос, а в
том, где и когда его выполнять. В SQL Server, чтобы получить значение
IDENTITY, созданное в результате выполнения последнего SQL-операто-
ра применительно к активному соединению с базой данных, используйте;

SELECT @@IDENTITY

Это совсем простой SQL-оператор, но важно иметь в виду вот что: если
последним был не оператор INSERT или если он был выполнен на другом
соединении, ожидаемого результата вы не получите. Этот код надо выпол-
нять сразу после INSERT и на том же соединении, например так:

INSERT INTO Products (ProductName) VALUES ('Chalk')

SELECT ^IDENTITY

Выдав такой запрос по единственному соединению с базой данных North-


wind, вы получите значение IDENTITY для нового товара — Chalk (мел).
Например, в приложении на Visual Basic, использующем ADO, вы могли
бы написать:

Set ofis = oCn.ExecuterSET NOCOUNT ON;INSERT INTO Products (ProductName)


VALUES ('Chalk');SELECT «IDENTITY")

IProductID = oRs(O)

Этот код указывает SQL Server не возвращать число строк, выполнить


оператор INSERT и вернуть значение IDENTITY, созданное для новой
строки, Оператор SET NOCOUNT ON приводит к тому, что возвращае-
мый объект Recordset содержит одну строку и один столбец со значением
IDENTITY. Без этого оператора вы получили бы сначала пустой объект
Recordset (так как INSERT не возвращает никаких данных), а потом еще
один объект Recordset, на этот раз со значением IDENTITY. Тут можно
запутаться — особенно если не знаешь, что оператор INSERT тоже возвра-
щает Recordset. Дело в том, что SQL Server следит за количеством строк
Пять способов подстегнуть производительность SQL 171

и, если оно меняется из-за вставки строки, интерпретирует новую строку


как объект Recordset. А реальные данные помещаются во второй Recordset.
Хотя вы могли бы добраться до второго объекта Recordset ADO-методом
Next Recordset, гораздо проще (и эффективнее) делать так, чтобы возвра-
щался только один Recordset.

Только что показанный способ решает задачу, но требует дополнительно-


го кода в SQL-выражении. Другой способ добиться того же результата —
ввести SET NOCOUNT ON перед INSERT, а в триггер FOR INSERT для
таблицы поместить SELECT ©©IDENTITY, как показано ниже. Тогда
любой оператор INSERT, адресованный данной таблице, автоматически
вернет значение IDENTITY.
CREATE TRIGGER trProducts_Insert ON Products FOR INSERT AS
SELECT @@IDENTITY
GO

Триггер срабатывает, только когда INSERT выполняется применительно к


таблице Products, поэтому он всегда возвращает IDENTITY при успешном
завершении запроса. Этот способ позволяет унифицировать получение
значений IDENTITY в рамках всего приложения.

Встраиваемые представления и временные таблицы


Иногда запросы должны объединять данные с другими данными, которые
можно получить только через GROUP BY с последующей выдачей стан-
дартного запроса. Например, если вам нужна информация о последних
пяти заказах, вы сначала выясняете, что они собой представляют. Для это-
го выдается SQL-запрос, возвращающий идентификаторы заказов. Полу-
ченные данные помещаются во временную таблицу, после чего она объе-
диняется с таблицей Products, и вы можете узнать, какие товары и в каком
количестве проданы по этим заказам:
CREATE TABLE «Tempi (OrderlD INT NOT NULL, OrderDate DATETIME NOT NULL)

INSERT INTO «Tempi (OrderlD, OrderDate}


SELECT TOP 5 o.OrderlD, o.OrderOate
FROM Orders о ORDER BY o.OrderDate DESC

SELECT p.ProductName, SUM(od.Quantity) AS ProductQuantity


FROM «Tempi t
INNER JOIN [Order Details] od ON t.OrderlD = od.OrderlD
INNER JOIN Products p ON od.ProductID = p.ProductID
GROUP BY p.ProductName
ORDER BY p.ProductName

DflOP TABLE #Temp1


172 Microsoft SQL Server

Здесь создается временная таблица, которая заполняется данными, объе-


диняется с другой таблицей и. наконец, удаляется. Этот запрос вызывает
массу операций ввода-вывода, однако его можно переписать так, чтобы
вместо временной таблицы использовалось встраиваемое представление
(inline view) — запрос, выполняющий объединение в блоке FROM. При
этом вы получаете тот же результат, но без интенсивного ввода-вывода:
SELECT p.ProductName,
SUH(od.Quantity) AS ProductQuantity
FROM (
SELECT TOP 5 o.OrderlD, o.OrderDate
FROM Orders о
ORDER BY o.OrderDate DESC
) t
INNER JOIN [Order Details] od ON t.OrderlD = od.OrderlD
INNER JOIN Products p ON od.ProductID = p.ProductID
GROUP BY p.ProductName
ORDER BY p.ProductName

Этот запрос не только эффективнее предыдущего, но и короче. Кроме того,


временные таблицы расходуют уйму ресурсов. Если данные нужны лишь
для объединения, встраиваемые представления предпочтительнее.

Старайтесь не использовать LEFT JOIN и NULL


Конечно, бывают ситуации, когда без LEFT JOIN и значений NULL не
обойтись. Но их применение далеко не всегда оправданно. Изменив струк-
туру SQL-запросов, можно добиться того, что запрос, который выполнял-
ся за несколько минут, будет давать результат за несколько секунд. Иног-
да приходится изменять формат данных в запросе так, как это нужно ва-
шему приложению. Кроме применения типа данных TABLE, снижающего
потребление ресурсов, есть много других возможностей оптимизировать
запрос. В частности, в SQL широко используется такой удобный оператор,
как LEFT JOIN. Он позволяет извлекать все строки из первой таблицы и
строки из второй таблицы, совпадающие и не совпадающие с первой. На-
пример, если вам надо получить список всех клиентов и их заказов, то
LEFT JOIN способен показать перечень клиентов, как сделавших заказы,
так и не сделавших их.

Этим инструментом нередко злоупотребляют. LEFT JOIN приводит к се-


рьезным издержкам, потому что сравнивает данные с NULL (несуществу-
ющими данными). В каких-то случаях без LEFT JOIN не обойтись, но
цена его использования может быть очень высока. LEFT JOIN требует го-
раздо больше ресурсов, чем INNER JOIN, и, если вам удастся так перепи-
сать запрос, чтобы избавиться от всех LEFT JOIN, вы получите колоссаль-
ный выигрыш (рис. 1).
Пять способов подстегнуть производительность SQL 173

Возвращаются в JOIN
Рис. 1. Запрос

Один из способов ускорить выполнение запроса с LEFT JOIN — создать


тип данных TABLE и вставить все строки из первой таблицы (находящей-
ся по левую сторону LEFT JOIN), а затем добавить в TABLE значения из
второй таблицы. Хотя этот процесс проходит в два этапа, он может сэко-
номить массу времени по сравнению со стандартным LEFT JOIN. Но луч-
ше всего пробовать разные запросы и измерять время их выполнения -
так вы найдете, какой запрос в ваших условиях обрабатывается быстрее
остальных.

Проверяя быстродействие запросов, важно проводить измерения по не-


сколько раз и в дальнейшем учитывать усредненные показатели. Запрос
(или хранимая процедура) могут помещаться в соответствующий кэш SQL
Server и при первом запуске выполняться дольше, чем при последующих.
Кроме того, параллельно с вашим запросом могут обрабатываться другие
запросы, выданные к тем же таблицам. Ваш запрос может быть приоста-
новлен, пока другие запросы блокируют нужные таблицы.

Чтобы избавиться от задержек, вызываемых LEFT JOIN, надо соответ-


ствующим образом проектировать базу данных. Допустим, какие-то това-
ры относятся к определенным категориям, какие-то — нет. Если в табли-
це товаров хранится идентификатор категории, а конкретный товар ни под
какую категорию не подпадает, то в соответствующее поле можно было бы
записать значение NULL. Тогда, чтобы получить список всех товаров и их
категорий, вам пришлось бы выполнить LEFT JOIN. Но можно создать
категорию со значением «No Category» и тем самым указать отношение
внешнего ключа (foreign key relationship), запрещающее значения NULL.
Теперь вы сможете получать те же результаты через INNER JOIN. Хотя
174 Microsoft SQL Server

такой подход связан с добавлением лишних данных, он позволяет избе-


жать вызовов ресурсоемких LEFT JOIN. Распространив эту концепцию на
всю базу данных, вы существенно ускорите обработку запросов. Помните:
даже несколько секунд много значат для ваших пользователей, а когда к
базе данных одновременно выдается большое число запросов, эти секун-
ды складываются в минуты.

Декартовы произведения
Здесь я дам совет, идущий в разрез с общепризнанным мнением: в опре-
деленных ситуациях декартово произведение (Cartesian product) очень
полезно. Почему-то декартовы произведения (CROSS JOIN) пользуются
дурной славой, и разработчиков зачастую призывают вообще отказаться от
них. Во многих случаях CROSS JOIN действительно приводят к неэффек-
тивному использованию ресурсов. Но их надо применять с умом — как и
любой другой инструмент SQL. Например, декартово произведение очень
удобно в запросе, который возвращает сводку по всем клиентам за каждый
месяц, в том числе ничего не заказавшим в каком-либо месяце.

Рассмотрим SQL-код на рис. 2. Заметьте: если бы выполнялось стандарт-


ное внутреннее объединение (INNER JOIN) таблиц Customers и Orders с
группированием по месяцам и суммированием продаж, то возвращались
бы данные только по тем месяцам, в которые клиент делал заказы. То есть
вы не получили бы нулевое значение для тех месяцев, когда клиент ниче-
го не заказывал. Если вам нужно построить график, отражающий объем
продаж по каждому клиенту, то для наглядности на этом графике должны
быть нулевые значения по тем месяцам, когда продаж не было. SQL-код на
рис. 2 пропускает месяцы, в которые объем продаж равен нулю, так как в
таблице Orders нет соответствующих строк (предполагается, что вы не
храните того, чего не было).

Код на рис. 3 длиннее, зато возвращает данные и по тем месяцам, когда


никаких продаж не было. Сначала мы получаем список всех месяцев про-
шедшего года и помещаем их в первую таблицу с типом данных TABLE
(@tblMonths). Затем получаем список всех компаний, что-либо закупав-
ших в течение этого периода, и помещаем его во вторую таблицу с тем же
типом данных (@tblCustomers). В этих таблицах хранятся все данные, не-
обходимые для создания набора результатов (resultset), за исключением
реальных цифр, отражающих объемы продаж.

Все месяцы перечислены в первой таблице (12 строк), а все клиенты, что-
либо закупившие в этот период. — во второй (в моем случае — 81 компа-
ния). Не все клиенты делали покупки в каждом из 12 месяцев, поэтому
INNER JOIN или LEFT JOIN пропускал бы тех клиентов, которые в дан-
ном месяце ничего не покупали.
Пять способов подстегнуть производительность SQL 175

Рис. 2. Выборка по всем клиентам и объемам продаж


set nocount on

DECLARE ©dtStartDate DATETIME,


9dtERU&ate DATETINE,
SUtDats ОАШШ

SET SdtEndDate = '5/5/1997'

SET «dtEntfBate * BATEABBCQD, -1. CAST(CAST«MONTH(§tftEndDate) + 1)


AS VABCHAR(2)} + '/01/' + CAST(YEAR(@dtEndDate) AS VARCHftR<4»
23; 59:59' AS DATETIME»
SET $dtStartDate * DATEAEJD(HM, -1 * 12, edtEndDate)

SELECT CAST(YEAR(O.OrderOate) AS VAflC«AR<4» + *-' + •


CASE
WHEN MOmiCo.QrderDate) < W
THEN 000 + CAST(KQ8TH{o.Qr<lerDate) AS VARCHAR(2»
ELSE GAST(HmiTH(o.QrderDate) AS VARCHAR(2)>
ES& AS sMonth,
c.CustomerlD,
с.СотрапуНате,
c.ContactHame,
SyM<od.Quantity * od,UnltPrioe) AS mSales
FROM Gustwiiers e
INNER JOIN Orders о ON c-Customerlt» = o.CustomerlO
INNER JOIN tOrder Details] od ON o.OrderlD - Qd.OrderlD
WHERE o.OrderDate BETWEEN @dtStartDate AND SdtEndDate
GROUP BY
CASTCYEAR(o.OrderBate) AS VARCHAR(4)) * '-"*
CASE
«НЕЙ HGNTH(o.CrderPate) < 19
THQi 000 + GAST(HQNTH(Q,OrderOate} AS VAfiGHAR(2)}
Ei^ CAST(MQNTH<o.OrderDate) AS VARCHAft(2»
END,
c.CustoraerlO, ••"=.;'•
c.CoapanyNarae,
G,CorttactNaffle
OfiDEft BY
c.CompanyNaroe,
sKonth

Декартово произведение возвращает всех клиентов и по всем месяцам.


Фактически первая таблица умножается на вторую, а результатом являет-
ся набор строк, число которых равно произведению количеств строк в пер-
вой и второй таблицах. Таким образом, я получаю таблицу @tblFinal с 972
строками. Далее я обновляю таблицу @tblFinal данными о ежемесячных
176 Microsoft SQL Server

объемах продаж по каждому клиенту в течение выбранного периода и про-


извожу выборку конечного набора строк.

Рис. 3. Декартово произведение на практике


DECLARE etblMonths TABLE (sHonth VARCHAR(7»
DECLARE etblGustomers TABLE £ CustomerlD CHAR(10),
CompanyName VARCHAR(50),
ContactNaroe VARCNAR(SO))
DECLARE ©tblFinal TABLE { sMonth VARCHARC7),
CustomerlD CHAR(1Q),
CompanyName VARCHAR(50),
ContactKame VARCHAR(SO),
mSales KQNEY)

DECLARE @dtStartDate МТШИЕ.


@dtEndDate DATETIME,
§dtDate DATETIME,
91 INTEQER

SET ©dtEndDate = '5/5/1997'

SET SdtEfidDate = DATEADD<DO, -1, CAST(CAST(CMO№TH(@dtEndDate) + T) AS


VARCHARC2» + 701/' + CAST(YEAR(@dtEndBate) AS VARCHARC4» + '
23:59:59' AS DATETIME))
SET ©dtStartDate = OATEADD(MH, -1 * 12, tdtEndDate)

— Заполнить первую таблицу списком всех месяцев


SET W « О
WHILE № < 12)
BEGIN
SET @dtDate = DATEADDCm, -1 * •!, (MtEndDate)
INSERT INTO @tbIMortths SELECT CAST(YEAR(£dtDate) AS VAfiCHAR(4» + 0
CASE
WHEN MONTH(edtOate) < 10
THEN '0' + CAST(HONTH(@dtDate) AS VARCWAR(2))
ELSE CAST(MONTH(&dtOate) AS VARCHAR{2)>
END AS sMonth
SET &i = 91 + 1
EHD

— включить асех клиентов, что-либо закупавших в этот период, в таблицу 'у'


INSERT INTO §tblCustomers
SELECT DISTINCT
с. Customer-ID,
c.CompanyNaroe,
c.ContactName
FROM Customers с
INNER JOIN Orders о ON c.CustomerlD * o.CustofflerlD
. WHERE o.OfderDate BETWEEN eeftStartDate.AND fcJtEndDate

см. след. стр.


Пять способов подстегнуть производительность SQL 177

Рис, 3, Декартово произведение на практике (окончание)


IHSE8T INTO
SELECT m.sHonth,
c.CustofflerIC,
c.CompanyName,
c.ContactNaffle,
0
FHOM @tblHonthS n CROSS JOIN ©tblCustGlsers С

UPDATE ©tblFinal SET


mSales - mydata.fflSales
®tblFinal f INKER JOIN

SELECT c.CustoraerlD,
CAST(YEAR(o.QrderDate> AS VAfiCHAR(4)) •
CASE WKEN MQNTH(o.QrderDate) < 10
THEN '0' + CAST(MONTH£o.QrderDate) AS VARCHARC2))
ELSE CASTCHONTHCo.QrtlerDate) AS VARCHAR(2)}
END AS SHonth,
SUH( od. Quantity * od.UnitPrice) AS raSales
FROM Customers с
ШЕЙ JOIN Orders о ON c.CustoroerlD = o.CustomerlO .
INKER JOIN tQrder Details} od ON o.OrderlD = od.OrderlD
WHERE o.OrderDate BETWEEN 9dtStartDate AND ©dtEndSata
GROUP BY
c.CustojaerlD,
CAST(Y£AR<O.OrderDate) AS VAfiCHAR(4)) + '-; +
CASE WHEN HOHTHCo.OrderDate) < 10
THEN '0' + CAST{MQNTH<O.GrtferOate) AS VARCHAR(2))
ELSE CAST(HONTH(o.OrderOate) AS VARCHAR<2»
END
) mydata on f.CustomerlD * mydata.CustomerlD AND f.sMonth =
mydata.sltonth

SELECT f.sMonth,
f.CustomerlD,
f.Com&anyName,
f.ContactName,
f .mSales
.FROM etblFinal f
ORDER BY

f.sHonth

CROSS JOIN следует использовать с осторожностью, так как его выпол-


нение требует очень много ресурсов. Например, результат, достигаемый
запросом CROSS JOIN, в котором применяются блоки WHERE, DIS-
TINCT или GROUP BY, отфильтровывающие большинство строк, можно
178 Microsoft SQL Server

получить гораздо более эффективным внутренним объединением (INNER


JOIN). Декартовы произведения очень полезны, когда вам нужны сово-
купные данные, например для построения графика. Но для других задач
декартовы произведения лучше не использовать — в большинстве случа-
ев внутренние объединения гораздо эффективнее.

Всякая всячина
Вот еще несколько приемов, увеличивающих производительность SQL-
запросов. Допустим, вы хотите сгруппировать всех продавцов по регионам
и суммировать объемы их продаж, причем вам нужны только продавцы,
помеченные в базе данных как активные. Вы могли бы группировать про-
давцов по регионам и использовать HAVING для исключения неактивных
продавцов, но то же самое можно сделать с помощью WHERE. Блок
WHERE уменьшает число строк, подлежащих группированию, и поэтому
он эффективнее, чем HAVING. Фильтрация строк по какому-либо крите-
рию в HAVING приводит к группированию и тех строк, которые были бы
исключены в WHERE.

Другой прием, направленный на увеличение производительности, — при-


менение ключевого слова DISTINCT вместо блока GROUP BY в тех слу-
чаях, когда нужно найти список различающихся строк (distinct list of data
rows). Тогда запросы с ключевым словом DISTINCT будут эффективнее.
GROUP BY следует использовать при вычислении функции суммирова-
ния вроде SUM, COUNT, МАХ и т. д. Кроме того, ключевое слово DIS-
TINCT нежелательно, когда точно известно, что данный запрос будет все-
гда возвращать уникальные строки. В этой ситуации применение DISTINCT
просто излишне.

Как видите, для оптимизации запросов и реализации специфических биз-


нес-правил можно использовать самые разные способы. Весь фокус в том,
чтобы проверять их в своих условиях и отбирать подходящие. Самое глав-
ное: тестировать, тестировать и еще раз тестировать. В рамках этой рубри-
ки я намерен продолжить исследование концепций SQL Server, проекти-
рования баз данных, способов эффективной индексации и парадигм защи-
ты в SQL Server.

Джонни Папа (Johnny Papa) — вице-президент компании MJM Investigations


по информационным технологиям (Роли, штат Северная Каролина). Автор
книги «Professional ADO 2.5 RDS Programming with ASP 3.0» (Wrox, 2000).
Часто выступает на различных конференциях. С ним можно связаться по
адресу datapoints@lancelotweb.com.
Алок Мехта и Дэниел Уильяме

Сценарии в SQL

Преобразование данных
и предоставление отчетов
SQL Server 2000 через
VBScript-интерфейсы*

Провайдеры программных сервисов (application service providers, ASP) часто


отправляют информацию клиентам автоматически, а не по запросу —
например, какой-то компании нужны ежедневные сводки об объемах продаж
ее продукции в розничной сети. SQL Server идеально подходит для ведения
баз данных такого типа, но, чтобы получать данные в формате, удобном для
клиентов, вы должны писать сценарии (scripts). В этой статье вы увидите, как
с помощью DTS (Data Transformation Services) — набора мощных средств SQL
Server — автоматизировать получение и форматирование данных SQL Server
2000 и упростить их доставку вашим пользователям.

Технология доставки информации (push), также называемая Web-вещани-


ем (Web casting), автоматизирует поиск и получение данных. Эти опера-
ции инициируются не пользователями, а Web-сервером или сервером базы
данных. Исходя из заданных критериев, соответствующее приложение
доставки (push application) автоматически выполняет поиск в базе данных
и самостоятельно определяет время и место доставки информации — за-
частую прямо на рабочий стол пользователя по электронной почте. Это не
только удобный способ получения важной информации, которую не так-
то легко получить каким-то иным образом, но и средство, существенно
влияющее на повседневную работу.

Публиковалось в MSDN Magazine/Русская Редакция. 2002. №2 (июль). — Прим. изд.


18G Microsoft SQL Server

Приложения доставки бывают двух видов. Один из видов, часто предла-


гаемых крупными сайтами телеконференций и баз данных, — электронная
почта, списки рассылки и службы Прямой доставки. Приложения достав-
ки второго вида разрешают пользователям и разработчикам создавать про-
фили и сохранять свои предпочтения, благодаря чему можно получать
информацию, соответствующую критериям поиска, из многих источников,
Программа-пример, которая рассматривается в нашей статье, относится к
первому виду приложений доставки. Но она достаточно универсальна, и в
нее можно добавить поддержку создания профилей.

Организации, которым нужна централизация и консолидация данных,


могут использовать средства DTS (Data Transformation Services), постав-
ляемые с SQL Server 2000. Они позволяют выбирать и преобразовывать
данные из разных источников и передавать заданным адресатам. С их по-
мощью можно не только выполнять просто разовую передачу данных, но
и создавать сложные пакеты, управляемые рабочим процессом (workflow-
driven). Кроме того, средства DTS предоставляют пользовательский гра-
фический интерфейс и относительно простую в программировании объек-
тную модель.

DTS-пакет (DTS package) — это комбинация соединений, заданий, преоб-


разований и ограничений, определенных для рабочего процесса. Каждый
пакет может включать одну или несколько операций (steps) или заданий
(tasks), выполняемых последовательно или параллельно. В процессе сво-
его выполнения пакет подключается к источникам данных, копирует ин-
формацию и объекты баз данных, преобразует данные и оповещает о со-
бытиях других пользователей или другие процессы. Пакеты можно редак-
тировать, защищать паролем, выполнять по расписанию и выбирать в
зависимости от версии. Пакеты легко создаются с помощью DTS Designer,
о котором мы еще поговорим.

VBScript или JScript позволяет создать задание, которое выполняет фун-


кции, недоступные в других DTS-заданиях. Например, вы можете:

• создавать и использовать ADO-соединения, команды, наборы записей


(recordsets) и другие объекты для доступа к данным и управления ими;
• форматировать и преобразовывать данные с помощью функций, про-
цедур и СОМ-объектов;
• создавать, изменять и использовать значения, хранящиеся в глобаль-
ных переменных DTS и в константах ActiveX-сценариев;
• включать другие задания и рабочие процессы DTS.
Преобразование данных и предоставление отчетов SQL Server 2000 ...

Приложение-пример
Наш пример показывает, как использовать VBScript-задания для выпол-
нения функций, недоступных в других заданиях DTS, и каким образом
сделать так, чтобы VBScript-сценарий работал в качестве задания, выпол-
няемого по расписанию (scheduled task). Мы не будем рассматривать гло-
бальные переменные DTS, рабочие процессы или интеграцию с другими
заданиями, так как наша цель — продемонстрировать мощь VBScript в
SQL Server 2000.

Данные
{в базе данных
SQL Server)

Т
Создание
локального
DTS-пакега
s SQLServer 2000

Создание
VBScript-задания
в локальном
DTS-пакете

Т
Написание на VBScript
функций выборки.
преобразования
и рассылки данных
(по электронной почте)

i
Конфигурирование
OTS-пакета как задания,
выполняемого
по расписанию

Рис. 1. Создание и конфигурирование DTS-приложения

Рис. 1 иллюстрирует схему, которой мы придерживались при разработке


приложения-примера DTS. VBScript-задание B'DTS-пакете периодически
рассылает авторам по электронной почте информацию по продажам их
книг. Данные форматируются в отчет. В качестве источника данных мы
взяли базу данных Pubs, поставляемую с SQL Server 2000, но наша мето-
дика носит универсальный характер и применима к любой модели данных.
182 Microsoft SQL Server

Первый шаг — заполнение поля адреса электронной почты соответствую-


щим адресом автора. Как показано на рис. 2, для этого мы выбираем базу
данных Pubs и расширяем таблицу authors, добавляя в нее поле au_email
типа varChar размером 20 символов*. Мы исходим из того, что в реальном
приложении это поле будет заполняться через отдельный GUI-интерфейс
или процесс, но в демонстрационных целях адреса можно вводить и вручную.

(щ 3;OesJtjn ТаЫ« authfirt, in 'put)*' on

*'.$»». Э£
aujd id (varchar) 11
aujname varchar 40
aujnams yardw 20
phone char "12
address varchar 40 V
city varchar 3J ! V
-_ Фstate chat
char "" 5
2 V
V
contract bit !l
г aujsmaill that ' 75 V

Рис. 2. Добавление поля адреса электронной почты

Далее мы создаем новый локальный DTS-пакет в SQL Server 2000. Как


показано на рис. 3;, чтобы вызвать DTS Designer для создания пакета, нуж-
но запустить Enterprise Manager в SQL Server, щелкнуть дерево консоли
Data Transformation Services правой кнопкой мыши и выбрать команду
New Package. С помощью GUI-интерфейса DTS Designer можно создавать
и конфигурировать пакеты, перетаскивая мышью нужные методы и запол-
няя страницы свойств различных DTS-объектов, образующих пакет.

Третий этап (рис. 4) — добавление VBScript-задания перетаскиванием


ActiveX Script Task с панели инструментов Task в рабочую область дизай-

на иллюстрации хорошо видно, что это поле имеет тип char и длину 75 символов. —
Прим. сост.
Преобразование данных и предоставление отчетов SQL Server 2000 ... 183

нера. Вы можете изменить имя задания с «ActiveX Script Task; undefined»


на что-нибудь более подходящее вам.

IM I rronsole Root\Microsolt- SQL ServersXStH Server ti™up\DWlU JAMS (Windows


ПЭ, J-.*SJ : .1 V
V® i Ш -r :> .... ,_, -
т
! С-жэ >^' . -- 11>ч:,;

^CwscJaPcw
1| nscwsefr S'5L 3«v*n
В l|J SQL Server бгоир
. |jk DWUIAMS (Windows NT)

Loiil Packs
Meta D-ifs
Mil* Data

• '£. r'j Security

:
li--Й M«l* Did S»rviMi
pp lh DWIlL!AM£'DWMJdM5_SER'/«R Windows HT-
• IbSQlDEVDEFERRALlWifKjowsNT)

Рис. З. Создание пакета

. ActiveX Script Task Properties

Package Objec
'B Tasks Cons Visual Eass veX bcri

Author AiokMehtaandDi
ale: 1Л/2002
aiy Sales Repent using CDOI
'ou should have 5QL Seivw i

Change ihe *Q losing thiee cc


SeeyowDBAcyNetwokAd

MallTransfeiPitiocol |
Соги(5МТР_ЗЕПУЕЯ-"м!с

d(ire« and Text foi Ihel


Const SENDER_EMAiL - Ю^

ADO Connection Strino to the ц


itDB^CONNECTSTRINCl

Рис. 4. Свойства ActiveX-задания


184 Microsoft SQL Server

Теперь надо написать на VBScript функции для выборки, преобразования


и отправки данных по электронной почте. Чтобы облегчить изучение при-
ложения-примера, просто скопируйте эти функции из исходного кода (см.
ссылку в конце статьи). Убедитесь, что в окне свойств ActiveX Script Task
выбран язык VBScript. Язык указывается на вкладке Language (рис. 4).
Затем скопируйте наш исходный код в дизайнер, удалив код функции
main, предлагаемый по умолчанию.

Кроме того, чтобы наше приложение заработало, надо изменить констан-


ты в SQL Server, так как у них могут быть различные значения (об этом —
позже).

После выполнения всех операций сохраните пакет, как показано на рис. 5.


Мы присвоили ему имя BookSales.

Рис. 5. Сохранение пакета

Запустить приложение-пример можно несколькими способами, Сначала


щелкните кнопку Parse, чтобы убедиться в отсутствии синтаксических
ошибок. Эта кнопка находится в окне свойств ActiveX Script Task. Затем
щелкните кнопку Go* на панели инструментов в верхней части окна DTS
Designer. Еще один вариант — щелкнуть нужное задание правой кнопкой
мыши и выбрать из контекстного меню команду Execute step. Кроме того,
пакет BookSales можно выполнить в Enterprise Manager (рис. 6). При ус-
пешном выполнении задания авторы будут получать электронную почту
в соответствии с расписанием (рис. 7).

Эта кнопка называется Execute. — Пром. сост.


Преобразование данных и предоставление отчетов SQL Server 2000 ...

tn SQL Server ЕгАегичи" Manager - {

'- 'ill Pal*TrinsfonriatLai Semites

m*>i D-Ki Sen/net


Mat, fsis

Рис. 6. Запуск пакета BookSales

••-,/. й-.d

Рис. 7. Расписание передачи по электронной почте


186 Microsoft SQL Server

Чтобы пакет BookSales автоматически запускался по расписанию, выбери-


те из контекстного меню команду Schedule Package (рис. 6) и укажите в
появившемся окне нужные параметры.

Пакет BookSales будет запускаться ежедневно в 11:00 вечера, начиная с


01.01.2002, и предоставлять отчеты о номерах заказов, количестве пози-
ций, виде платежа и названиях книг. Обратите внимание: необходимо за-
пустить SQL Server Agent и настроить его на запуск пакетов DTS по рас-
писанию. Вы увидите пакет BookSales как задание в Enterprise Manager в
узле дерева консоли Management SQL Server Agent j Jobs.

Код на VBScript
Исходный код нашего примера находится в файле SourceCode.txt. После
того как мы поясним константы, которые нужно задать, и кратко рассмот-
рим функцию main, мы расскажем, как этот код обрабатывает данные по
продажам, обращается к базе данных, форматирует набор записей и от-
правляет отчеты.

Следующим трем константам нужно присвоить значения, соответствую-


щие параметрам вашей системы и сети:
Const SHTP_SERVER *= "excfiange.afs-link.com"
Const SENOER_E-MAIL =
Book Sales Reporting Service"" <amehta@afs- " &
"link.com>"
Const DB_CONNECT_STRING =
"Provider=SQLOLEDB.1;Data Source=(local); " & _
"Initial Catalog=Pubs;user id = ' s a ' ; p a s s w o r d = ' '

Первая константа, Const SMTP_SERVER = "exchange.afs-link.com", сооб-


щает DNS-имя вашего SMTP-сервера (почтового сервера). Эта информа-
ция нужна для отправки электронной почты; ее можно получить у вашего
сетевого администратора.

Const SENDER_E-MAIL = ""Book Sales Reporting Service" <amehta@afs-


link.com>" — электронный адрес отправителя. Он обычно имеет вид sys-
1ет@шш_домена.сот. Первая часть этой константы (Book Sales Repor-
ting) может содержать любой текст (обычно там указывается название
отдела или службы, высылающей ежедневный отчет по продажам. В угло-
вых скобках должен быть задан реальный электронный адрес. И в этом
случае сетевой администратор подскажет вам, что здесь надо ввести.

Последняя константа — строка подключения (connection string) к базе


данных, содержащая всю информацию, необходимую для соединения с
базой данных. Строка подключения в ADO выглядит примерно так:
Преобразование данных и предоставление отчетов SQL Server 2000 ... 187

Const DB_CONNECT_STRING = "Provider=SQLOLEDB.1;Data " & _


"Source=(local); Initial Catalog=Pubs;user id = " & " ' s a ' ; p a s s w o r d * ' ' "

Мы исходим из того, что читатель знаком с основами ADO, и поэтому


лишь кратко описываем каждую часть строки подключения.

Элемент «Provider=SQLOLEDB.l;» указывает драйвер базы данных. В


нем определяется тип ODBC-драйвера, нужного для подключения к базе
данных. Мы используем SQL Server. Если ваше приложение работает с ба-
зами данных другого типа, информацию об ODBC-драйвере можно найти
по ссылке http://msdn.microsoft.com/library/en-us/odbc/htm/dasdkodb-
coverview.asp.

Элемент Data Source задает местонахождение сервера. Мы указали в нем


«local», так как у нас SQL Server находится на локальном компьютере. В
ином случае может потребоваться IP-адрес или имя компьютера, на кото-
ром размещена база данных.

Initial Catalog содержит имя базы данных; в нашем примере это Pubs, ко-
торая поставляется с SQL Server. Наконец, user id определяет имя пользо-
вателя, a password — его пароль.

По умолчанию точкой входа в ActiveX-задание является функция main, но


может быть и любая другая. Функция main состоит всего из двух строк.
Сначала она вызывает процедуру Process_Daily_Sales, а затем возвращает
константу, используемую в ActiveX-сценариях, — DTSTaskExecResult^Suc-
cess. В Package Object Browser можно посмотреть все константы проекта
и глобальные переменные (рис. 8).

Эти константы и глобальные переменные позволяют управлять выполне-


нием операций в DTS-пакете. Мы не будем подробно описывать эти кон-
станты и переменные. Важно, что функция main возвращает константу
DTSTaskExecResult_Success, поскольку в нашем примере VBScript-зада-
ние выполняет всего одну операцию.

Обработка данных по продажам


Функция Process_Daily_Sales — главная часть нашего приложения. Она
извлекает набор записей со списком авторов, книги которых продавались
в течение сегодняшнего дня. Затем она форматирует этот набор записей
как HTML-таблицу и отправляет ее соответствующим авторам.

Функция содержит три локальных переменных (два набора записей ADO


и одну дату):
Microsoft SQL Server

4.1ирвмиш
Й ]S т.*rs Con^anl V^.iB^.Adiv^-.rn
Da^a Punp Corijlani
AulHiot: Atok Meht3 and Dad
S В TSTiansformStaLOK Oale 1 Л /2002
£ DTSTiansrojmSialJnlo IJaijji Sales Report using CDG.J|
You should have SQL Serves |is

'Chaige ihe lol^^ing three ей!


!• Я DTSTransiufmSlaLSkpFelch l
SeeyoutDBAcjfNetv*iokAdnJ
Ш DTSTfans-tormStaLSkiplnsert
IB DTSTransformSlaLDeslDalaNoSet
i'iunpte Mail Transfer Protocol Ц
Ш D"STramformStat..Eiror
[Const 5MTP_SERVER = '-:-• :-
S! DTSTfanstofmStat_EriotSkpFlcw
Я DISTransiomiS(aL^ceptionRow jtmail Address and Тзч( for theT
33 DTSTtansiQriTi£)fll_AbcrtPump Consl SENDER_EMAIL = J
SI DTSTrar-islorniStaLNoMoreRom
'ADO Conn&etkin Stnna to thel
^ala Driver. Query Constant Coml D8_mNNECT_STRINl
»! rfTCTrs^,?fjMl^Ck-> ' r-'-j-'llj .^r,r

£В DTSriwisloimSfaUJpditeQuery
-jSl DTSTransrermStaLDfcleteQuefy
:
Name: Main
- JSJ DTSTFertsforrffiaUJserQuwji Aur.hoi: Atak Mehta and Oal1
]
(-..-Щ Active* Script Cnrtstant 'Dale' 1 Л /2002
£51 DTSTa;kFse-?Result_SuccsES Purpose. Calbihe PioceM_Di-
!
-[Я DTSTaskEKseRefull Failure
\3 У Steps Coreienl
--X DTSSiepEKecS(at_Compteted :
MictbnK)ain{l
jf DTSSteptHecSsatjnashve Call P[ocess_Daily_Sa|
! У OTSSiepExecSiatJnProgrew
End Function
JT •. . *e,ik.^..c5rai_ /a. ng
'
jf OTSStepSctiplResuH^ExeculeTask ^
'Name: Fundicin Serid^iii.J"
A l~
V
:
.-ih !VУанаом*
LitoM 'яН 'Authoi: A!ck Mehta and С з,-
'Daie 1/1.^2002
'Puipoje IJsssCDD to spni
1 5глч1Св! J Return Noihing

Рис. 8. Константы проекта

Dim rstAuthors
Dim rstSales
Dim Todays_Date

Так как не все базы данных Pubs содержат данные по продажам за сегод-
няшний день, следующие две строки кода генерируют отчет за 14.09.1994.
Отчет за эту дату можно получить почти в любой базе данных Pubs. Мы
просто убрали признак комментария из строки кода для даты 14.09.1994 и
закомментировали строку с сегодняшней датой:
"Todays_Date = & DateQ &
Todays_Date = "'9/14/1994'"
Преобразование данных и предоставление отчетов SQL Server 2000 ... 189

Для выборки авторов, книги которых были проданы сегодня, использует-


ся несложный запрос, объединяющий несколько таблиц:
strAu_Sales = "Select Distinct Authors.* from " &
"Authors,Sales, TitleAuthor Where " &.
"TitleAuthor.au_id = Authors,au_id and " &
"TitleAuthor.Title_id = Sales.Title.id and " &
"Sales.ord_date = " & Todays_Date

Далее получаем набор записей ADO, соответствующий этому оператору


SQL:

Set rstAuthors = ExecuteSQL(strAu^Sales)

Затем, прежде чем проходить по набору записей rstAuthors и оповещать


авторов, убеждаемся, что этот набор записей не пуст:

If Not (rstAuthors.Eof and rstAuthors.Bof ) Then


While Not rstAuthors.Eof

Мы извлекаем и другую информацию: магазин, принявший заказ, номер


заказа, количество позиций, вид платежа и названия книг. Эта информа-
ция берется из таблиц Stores, Sales, TitleAuthors и Titles. Как видите, и
здесь выполняется простое объединение таблиц* :

strAu_Sales = "SELECT distinct stores.stor_name as [Store Name], " &


"sales.ord^num as [Order Number], sales.qty as [Quantity], " &
"sales.payterms as [Pay Terms], Titles.Title FROH Stores, Sales, " & _
"TitleAuthor, Titles " & "Where TitleAuthor.au_id = '" & _
rstAuthors("au_id") & "' and Sales.ord_date = " & Todays_Date " &
"and Sales.Title^id = Titles.Title_id and sales.stor_id = " & _
"stores.stor^id "

И в этом случае мы получаем набор записей ADO, выполняя оператор


Execute SQL:

Set rstSales=ExecuteSQL(strAu_Sales)

Теперь формируем тело сообщения с именем и адресом автора. Для этого


используется HTML-тэг <br>, так как нам нужен формат HTML:
strTable = rstAuthors("au_fname") & " " 4
rstAuthors{"au_lname") & "<br>" & rstAuthorsC'Address")
& "<br>" & rstAuthors("city") & ", " &
rstAuthors("state") & " " & rstAuthors("Zip")

Этот запрос исправлен в соответствии с исходным кодом, мятым из файла SourceCode.txt. —


Прим. сост.
190 Microsoft SQL Server

Тело сообщения готово. Теперь встраиваем в него HTML-таблицу, содер-


жащую ранее полученные данные по продажам. Функция Format Recordset
преобразует набор записей rstSales в HTML-таблицу:
strTable = strTable & FormatRecorcfset(rstSales)
Далее вызываем функцию отправки электронной почты, параметрами ко-
торой являются тема сообщения, электронный адрес автора и тело сообще-
ния в формате HTML:
Call send_e-mail("Book Sales Report For: " & Todays_Date,
rstAuthors("au_email"), strTable)
Затем переходим к следующему автору:
rstAuthors.movenext
Mend

Функция ExecuteSQL подключается непосредственно к базе данных. В


этой функции мы обращаемся к базе данных, выполняем передаваемую
при вызове строку SQL и возвращаем результаты (если они есть). Снача-
ла создаем ADO-соединение:
Set myConn = CreateObject("ADODB,Connection")

Затем создаем набор записей ADO:


set myRecordset = CreateObject("ADODB.Recordset")
На следующем этапе открываем соединение, используя константу
DB_CONNECT_STRING:
myConn.Open = DB_CONNECT_STRING

Открыв соединение, открываем набор записей:


myRecordset.Open mySQLCmdText, myConn

И, наконец, возвращаем результат:


Set ExecuteSQL = myHecordset

Если у вас есть опыт работы с ADO, все эти действия будут для вас очень
просты.

Функция FormatRecordset получает к качестве параметра набор записей


ADO и возвращает HTML-таблицу (в виде строковой переменной). В ней
три цикла, причем третий цикл вложен во второй. В первом цикле пере-
бирается список полей и формируется заголовок таблицы. Затем таблица
Преобразование данных и предоставление отчетов SQL Server 2000 ... 191

заполняется данными; при этом внешний цикл проходит по записям, внут-


ренний — по полям. В итоге набор записей преобразуется в HTML-таблицу.

HTML-таблица создается как строка. Эта простая таблица определяется


так:

strTable = «<table border=1 width=500>»

Затем переходим на первую запись и создаем строку таблицы, используя


тэг <tr>:
rstTable.MoveFirst
strTable = strTable & "<tr>"

Затем проходим по полям и добавляем в таблицу имена полей с помощью


тэга <td>, выделяя их цветом:
For Index = 0 То rstTable.Fields.Count - 1
strTable = strTable & "<td bgcolor=blue nowrap>" &
"<font color='white'>"
strTable = strTable & rstTable.Fields.Item(Index).Name
strTable = strTable & "</font></td>"
Next
Закрываем тэг <tr> и таким образом завершаем формирование заголовка
таблицы:
strTable = strTable & "</tr>"

Теперь, чтобы заполнить HTML-таблицу информацией, обрабатываем


данные, хранящиеся в наборе записей. Проходим по всем записям и запол-
няем HTML-таблицу тэгами <tr> и <td>:
While (Not rstTable.EOF)
strTable = strTable & "<tr>"
For Index = 0 To rstTable.Fields.Count - 1
strTable = strTable & "<td>"
strTable = strTable &
rstTableCrstTable.Fields.Item(Index).Name).Value
strTable = strTable & "<br>"
strTable = strTable & "</td>"
Next
strTable = strTable & "</tr>"
rstTable.MoveNext
Wend
Наконец таблица заполнена. Осталось вернуть значение функции — пере-
менную строкового типа:
192 Microsoft SQL Server

strTable = strTable & "</table>"


FonnatRecordset = strTable

Отправка отчетов
Для отправки отчетов авторам служит функция Send_Email. Заметьте, в
ней используются константы SMTP_SERVER и SENDER_EMAIL, а так-
же Microsoft Collaboration Data Objects (CDO 2.0) из CDOSYS.DLL. CDO
предоставляет объектную модель для разработки коммуникационных при-
ложений в Windows 2000, базируется на стандартах SMTP и NNTP и дос-
тупен как системный компонент при установке Windows 2000 Server. Это
стандартный API для создания приложений массовой рассылки и переда-
чи почтовых сообщений через Web, Такие приложения работают под уп-
равлением Windows 2000 Server.

У функции Send _Email три параметра: subject, rcpt и msgHTML. Первый —


это тема отправляемого сообщения, второй — электронный адрес получа-
теля, а третий — тело сообщения в формате HTML.

В Send_Email также используются две другие константы: cdoSendUsing-


Pickup (указывает, что сообщение должно быть отправлено ч:ерез локаль-
ный каталог службы SMTP) и cdoSendUsingPort (указывает, что сообще-
ние должно быть отправлено по сети).

Если на локальном компьютере установлена служба SMTP, по умолчанию


берется константа cdoSendUsingPickup. В ином случае (если установлен
Outlook Express) — константа cdoSendUsingPort и параметры учетной за-
писи по умолчанию (default account). Мы используем cdoSendUsing-
Pickup.

Далее в Send_Email создаются СОМ-объекты Message и Configuration:


set iMsg = CreateObjectC'CDO.Message")
set iConf = CreateObjectC'CDO.Configuration")

У объекта Configuration несколько полей. Перед присвоением значений


эти поля связываются с объектом конфигурации:
Set Fids = iConf.Fields

Большинство полей, используемых для задания конфигурации CDO-


объектов, определяется в пространстве имен http://schemas.microsoft.com/
cdo/configuration/. Мы присваиваем значения трем полям (SendUsing,
SMTP_SERVER и TimeOut) объекта Configuration:
With Fids
.Item("http://schemas.microsoft.com/cdo/configtjration/sendusLng") =
Преобразование данных и предоставление отчетов SQL Server 2000 ... 193

cdoSendUsingPickup
. Item( "http://schenias.microsoft.coin/cdo/configuration/snitpserve г") =
SHTP.SERVER
,Item("http://schemas.microsoft.com/cdo/configuration/ " & _
"smtpconnectiontimeout") = 10
.Update
End With

Наконец, указываем, что сообщение будет использовать только что опре-


деленную нами конфигурацию, заполняем остальные поля и отправляем
сообщение:

With iMsg
Set .Configuration = iConf
.To = rcpt
.From = SENDER_EMAIL
.Subject = subject
.HTMLBody = msgHTML
.Send
End With

Заключение
В этой статье мы рассказали, как реализовать технологию доставки инфор-
мации на основе SQL Server 2000, VBScript, ADO, CDO и DTS-пакетов.
Наш пример прост, но функционален. Чтобы расширить его, вы могли бы
использовать более сложную модель данных и внешние компоненты, по-
зволяющие отправлять пользователям более полные отчеты. Кроме того,
вы могли бы интегрировать в VBScript-задание другие DTS-задания. Ме-
тодика, описанная в этой статье, применима и к рассылке самой разнооб-
разной информации: регулярных финансовых отчетов, сведений о состоя-
нии заказов и любых других данных.

Исходный код для этой статьи можно скачать по ссылке: http://down-


load.microsoft.com/download/msdnmagazine/code/Aug02/WXP/EN-US/
VBScriptAndSQLServer2000.exe.

Алок Мехта (Alok Mehta) — старший вице-президент и директор по техноло-


гиям в American Financial Systems Inc. (AFS) (Уэстон, штат Миннесота). С ним
можно связаться по адресу amehta@afs-link.com.

Дэниел Уильяме (Daniel Williams) — заместитель вице-президента по


технологиям в Deferral.com (компании в составе AFS), а также инженер ПО,
участвующий в проекте Deferral.com. С ним можно связаться по адресу
dwilliams@afs-link.com.

7-5947
Франческо Балена

Автоматизация выполнения
административных задач
в SQL Server*

SQL Server можно программно администрировать на основе системных


хранимых процедур, но зачастую удобнее использовать современную
объектно-ориентированную технологию DM0 {Distributed Management
Objects). В статье описывается SQL-DMO в SQL Server 7.0 и SQL Server 2000,
а также рассматривается объектная модель SQL-DMO. Основное внимание
уделяется поддеревьям Databases и JobServer дерева этой модели. Примеры
кода демонстрируют, как с помощью разнообразных объектов вроде Registry,
Configuration и Database автоматизировать выполнение типичных задач
администрирования, в том числе программное считывание параметров
конфигурации, создание баз данных, запуск сценариев (scripts) T-SQL
и резервное копирование данных по расписанию.

Вместе с Microsoft SQL Server поставляется набор хранимых процедур для


автоматизации административных операций (создания баз данных и таб-
лиц, резервного копирования и г. д.), Однако раньше вам бы понадобился
опыт программирования на T-SQL, чтобы в полной мере задействовать эти
возможности. С появлением DMO (Distributed Management Objects) си-
туация изменилась. Теперь любой программист может решать такие зада-
чи, используя тот язык, который ему больше нравится.

SQL-DMO — это набор из примерно 150 объектов, отражающих практи-


чески все аспекты управления SQL Server 7.0 и SQL Server 2000. Эта
объектная модель полностью поддерживает двойственные интерфейсы

Публиковалось в MSDN Magazine. 2001. №5 (май). — Прим. изд.


Автоматизация выполнения административных задач в SQL Server

(dual interfaces), поэтому она применима почти в любом языке, в том чис-
ле в Visual Basic, C++, VBScript, JScript, Windows Script Host (WSH) и
ASP-сценариях. В статье приводится исходный код, показывающий, как
программно считывать параметры конфигурации, создавать базы данных,
запускать сценарии T-SQL, создавать задания на резервное копирование
и указывать расписание их выполнения. Основная часть кода написана на
Visual Basic, но концепции, о которых рассказывается в статье, легко реа-
лизовать на любом другом языке программирования.

Обзор SQL-DMO
С физической точки зрения, объектная модель DMO реализована в SQL-
DMO.DLL. Вспомогательный файл SQLDMO.RLL содержит все локали-
зуемые ресурсы. В SQL Server 7.0 этот RLL-файл находится в каталоге
\Mssql7\Binn\Resources'vrxYA- (если при установке выбран путь, предлага-
емый по умолчанию), где хххх — десятичное значение идентификатора
языка (например, 1033 для U.S. English), В SQL Server 2000 по умолчанию
используется каталог C:\Program Files\Microsoft SQL Server\80\Tools\
Binn\Resources\;u:xr. Упомянутые файлы копируются на диск стандарт-
ной программой установки SQL Server, так что для активизации DMO
дополнительных действий не требуется. Кроме того, программа установ-
ки запускает сценарий SQLDMO.SQL (написанный на T-SQL) для уста-
новки нескольких системных процедур, необходимых DMO. Вы можете
самостоятельно запустить его из каталога \Mssql7\lnstall (для SQL Server
7.0) или Microsoft SQL Server\MSSQL$w.w,a cepeepa\lnsta\\ (для SQL
Server 2000), если полагаете, что эти процедуры были удалены или изме-
нены. DMO также устанавливается в составе инструментальных средств,
развертываемых на клиентских рабочих станциях. В файле REDIST.TXT
в корневом каталоге установочного компакт-диска SQL Server содержит-
ся информация по установке и редистрибуции SQL-DMO.

Компонент SQLDMO.DLL обращается к SQLSVC.DLL (модулю абстрак-


ции базы данных), а тот — к ODBC32.DLL, которая в свою очередь вызы-
вает ODBC-драйвер SQL Server. ODBC-драйвер должен быть не ниже
версии 3.70 (эта версия поставляется с SQL Server 7.0). Если определен
псевдоним и вы указываете его в качестве имени сервера, то и SQL-DMO
будет использовать этот псевдоним при поиске источника данных ODBC.
SQLDMO.DLL работает с протоколом по умолчанию, заданным через
Client Network Utility, и не может самостоятельно перейти на другой се-
тевой протокол.

Объекты SQL-DMO верхних уровней


Несмотря на большое количество объектов в DMO, эта модель удивитель-
но проста в использовании; ее структура напоминает иерархию объектов
196 Microsoft SQL Server

SQL Server Enterprise Manager. Корнем DMO-дерева является объект


Application; под ним находится объект ServerGroup, далее идет объект
SQLServer. Все объекты верхних уровней модели показаны на рис. 1. У
объекта SQLServer три важных поддерева: Databases (включает все объек-
ты, нужные для создания и управления базами данных, таблицами, индек-
сами и т. д.), JobServer (содержит все объекты, предназначенные для опре-
деления заданий и расписаний их выполнения) и Replication (позволяет
реплицировать базы данных, определять издателей и подписчиков и т. д.).
В этой статье основное внимание уделяется поддеревьям Database и
JobServer.

BackupDeuices

Languages

RemoteServers

Re in ale Logins

Server Roles

Рис. 1. Объект Application


Автоматизация выполнения административных задач в SQL Server 197

Работа с объектами SQL-DMO


Все объекты в иерархии SQL-DMO можно разбить на три большие груп-
пы: индивидуальные объекты (например, Database, Table и Column), набо-
ры (например, Databases, Tables и Columns) и списки. Объекты-списки
аналогичны наборам, но не имеют встроенной поддержки для добавления
и удаления элементов. Обычно списки — это значения, возвращаемые ме-
тодами или свойствами. Так, в следующем коде поле со списком (combo-
box) заполняется именами серверов:
Dim sqlApp As New SQLDMO.Application
Dim NL As SQLDMO.NameList

Set NL = sqlApp.ListAvailableSQLServers
For index = 1 To NL.Count
cboServers.Addltem NL.Item(index)
Next

Для SQL Server 7.0 этот код выведет список серверов, которые работают
под управлением Windows NT, используют протокол Named Pipes и нахо-
дятся в одном домене сети. (Так как при поиске серверов применяется
широковещание по NetBIOS, список серверов может стать еще короче в
зависимости от инфраструктуры маршрутизации. И еще одно: серверы
SQL Server под управлением Windows 9.r не показываются, поскольку они
не прослушивают пакеты по протоколу Named Pipes.) В SQL Server 2000
при поиске серверов наряду с широковещанием по NetBIOS применяется
широковещание по UDP, поэтому в список попадут и те серверы, которые
используют стек протоколов TCP/IP. Кроме того, если на локальном ком-
пьютере работает сервер, он тоже добавляется в список.

Далее я покажу, как использовать объект SQLServer для выполнения не-


которых типичных задач администрирования.

Подключение и отключение
Для подключения к заданному экземпляру SQL Server нужно создать
объект SQLServer, при необходимости присвоить подходящее значение
полю LoginTimeout и вызвать метод Connect:
Dim SQLServer As New SQLDMO.SQLServer
SQLServer.LoginTimeout = 10

If UseNTAuthentication Then
1
Аутентификация средствами Windows NT
SQLServer.LoginSecure = True
SQLServer.Connect strServer
Else
£98 Microsoft SQL Server

1
Аутентификация средствами SQL Server
SQLServer.Connect strServer, strlogin, strPassword
End If

Как именно осуществляется подключение, зависит от вида аутентифика-


ции — через Windows NT или SQL Server.

В предыдущем фрагменте кода strServer — имя сервера, a strLogin и


strPassword — имя и пароль при аутентификации через SQL Server. Учти-
те, что при неудачной попытке входа генерируется ошибка периода выпол-
нения, поэтому в реальных приложениях эти операторы надо защищать
обработчиком ошибок.

В отличие от ADO-объекта Connection объект SQLServer при выходе из


диапазона действия не закрывает соединение автоматически, поэтому,
если вы сами его не закроете, оно остается открытым, пока не истечет вре-
мя ожидания. Весьма важно по завершении работы с SQL Server отклю-
читься от него (вызовом SQLServer.Disconnect), поскольку формируемый
SQL-DMO кэш метаданных не уничтожается до отсоединения (хотя SQL
Server 2000 по возможности пытается побыстрее освободить объекты кэша
метаданных). Поэтому, чтобы DMO не удерживала лишние ресурсы, унич-
тожайте все объекты явно:

set oDatabase = Nothing

Кэш метаданных поддерживается индивидуально для каждого процесса,


использующего SQL-DMO (другими словами, кэш метаданных локален
для процесса, в чьем адресном пространстве он размещается, и не разделя-
ется процессами, выполняемыми на одном компьютере).

Кроме того, с помощью объекта SQLServer можно в один прием запустить


службу Microsoft SQL Server и подключиться к ней либо приостановить,
возобновить или остановить ее выполнение (рис. 2). Пример на Visual
Basic (stoplite), демонстрирующий, как реализовать эти операции, можно
скачать по ссылке http://msdn.microsott.com/msdnmag/code01.asp в разде-
ле за май. Получить текущее состояние службы позволяет свойство SQL-
Server. Status.

Объект SQLServer напрямую управляет службой MSSQLServer. Для за-


пуска и останова службы SQLServerAgent, управляющей выполнением
заданий по расписанию, нужно использовать объект JobServer:

SQLSe rver.JobSe rve г.Start


SQLSe rver.JobSe rve r.Stop

Для настройки служб SQL Server на автоматический запуск при загрузке


операционной системы установите свойства объекта Registry:
Автоматизация выполнения айминистратианых задач в SQL Server 199

SQLServer.Registry.AutostartServer = True
SQLServer.Registry.AutostartDTC = True

Рис. 2. Приостановка, возобновление и остановка выполнения службы


Public Sub SQLServiceManager{NewState as Int)
Dim oSQLServer As SQLDMO.SQLServer
Set oSQLServer = New SQLServer
oSQLServer.Name = "(local)"

On Error GoTo errHandler

Select Case Index


Case 1 ' запуск
If oSQLServer.Status = SQLDHQSvc_Stopped Then
oSQLServer. Start False
Else
HsgBox "Cannot start server, server is either"
1 "running or paused' vbOKOnly, „
"SQL Server Service Manager"
End If
Case 2 ' приоетановка/возобнаалени-е
If oSQLServer.Status = SQLDMQSvc.Paused Then
oSQLServer.Continue
Elself oSQLServer.Status = SQLDMOSvc_Bunnjng Then
oSQLServer,Pause
Else
HsgBox "Cannot pause a server which is running",
& vbOKOnly, "SQL Server Service Manager"
End If
Case 3 ' остановка
If oSQLServer.Status = SQLDMOSvc_Running Then
oSQLServer.Stop
' oSQLServer.Shutdown выдает TSQL-конанду "shutdown"
' и не использует диспетчер управления службами (SCM)
Else
HsgBox "Cannot stop a server which is not running",
vbOKOnly, "SQt Server Service Manager"
End If
End Select

Exit Sub

errHandler:
HsgBox Err.Number & " " & Err.Description & " <" & _
Err.Source & " " & Err.LastDUError & ")", _
vbQKOnly, "SQL Server Service Manager"

€nd Sub
200 Microsoft SQL Server

Заметьте, что служба SQLAgent не работает в Windows 98, Windows 98 SE


и Windows Me, так как в отличие от Windows NT в этих операционных
системах нет диспетчера управления службами (Service Control Manager,
SCM). (SQL Server 2000 больше не поддерживает Windows 95.) Соответ-
ственно в Windows 9x нет и учета взаимозависимостей служб,

Объекты Registry и Configuration


Объект Registry обеспечивает доступ ко многим важным параметрам, оп-
ределяющим каталог установки, чувствительность к регистру букв, путь к
базе данных master и имя зарегистрированного владельца. Не удивитель-
но, что не все эти параметры можно изменить без переустановки SQL
Server.

Объект Configuration открывает доступ к набору элементов ConfigValue.


Каждый элемент соответствует одному из примерно 40 параметров конфи-
гурации, значения которых можно просматривать и изменять с помощью
хранимой процедуры sp_configure. Но проще работать с этими значения-
ми, используя объектно-ориентированный подход. Так, следующий код
выводит список значений основных и дополнительных параметров конфи-
гурации:
Dim cv As SOLDMO.ConfigValue
With SQLServer.Configuration
.ShowAdvancedOptions = True
For Each cv In .ConfigValues
Print cv.Name, cv.RunningValue, cv.CurrentValue
Next
End With

Вы можете изменить параметр конфигурации и зафиксировать изменения,


вызвав метод ReconfigureCurrentValues или ReconfigureWithOverride:
' Разрешаем изменение системных таблиц
With SQLServer.Configuration
.ConfigValuesC'allow updates").CurrentValue = 1
.ReconfigureWithOverride
End With

Извлечение данных из объекта QueryResults


Не все параметры конфигурации так легко прочитать. Объект SQLServer
содержит 10 методов Епигплхг, возвращающих информацию об учетной
записи, атрибутах сервера, группах в домене Windows NT и такие важные
динамические данные, как сведения о выполняемых процессах и текущих
блокировках. Все методы Enunuxr возвращают объекты QueryResults.
Автоматизация выполнения административных задач в SQL Server 201

Объект Query Results можно рассматривать как контейнер, хранящий один


или несколько доступных только для чтения двухмерных массивов, из
которых информация извлекается методом GetColumnString, GetColumn-
Long или Get Column Double. Применение этих методов довольно утоми-
тельно, потому что сначала нужно определить тип каждого столбца по
свойству ColumnType и только потом выбрать соответствующий метод
GetColummxt.

Однако есть более простой и быстрый способ извлечения данных из объ-


екта QueryResults, основанный на использовании метода GetRangeString.
Этот метод возвращает текст, который выводился бы в окне Query Analy-
zer при запросе той же информации через соответствующую хранимую
процедуру (например, sp_\vho при получении списка пользователей и про-
цессов или sp_locks при получении информации о текущих блокировках).
Полученную строку можно быстро обработать для извлечения нужных
значений вызовом функции Split языка Visual Basic или VBScript.

На рис. 3 показана универсальная процедура, которая преобразовывает


объект QueryResults в ADO-объект Recordset. Я решил использовать
Recordset вместо, например, обычного двухмерного массива строк, так как
это позволяет сразу же показать результаты, связав с Recordset элемент
управления DataGrid (рис. 4).

Рис- 3. Преобразование QueryResults в АОСЬобъект Recordset


Function QueryResultToRecordset(qres As SQLDHQ.QueryResylts) _
A3 ADQDS.Recordset

. Dim qresString As String


DliB rs As New AOODB. Recordset
Dim rowsO, As String, cols() As String
Dim rowlndex As Long, callndex As Long

* Получаем все результаты в виде одной длинной строки со столбцами


' и записями, разделенными специальными Символами; в данном
' случав используются необычные разделители столбцов/записей,
" так как некоторые значения иогут содержать CRLF
qresString = qres.GetRangeString(t , , , СЬг(2), Chr<1))
' Первая запись длинной строки содержит заголовки столбцов .
row&Q = SplitCqresString, Dhr<2)}
cols() * Split(rQws(0), Ghr(1»
1
Создаем поля Recordset
For collnitex = 0 To UBoijnd(cols)
rs.Fields.Append RTrim$(cols(colIndex)), adVarCtiar, „
cfres.ColurflnMaxLength(colIndex + 1)
Next
см. след. стр.
Microsoft SQL Server

Рис. З. Преобразование QuervResults в ADO-объект... (окончание)


Добавляем записи в Recordset
rs.Open
Вторая запись, rows(1), состоит из символов -,
отделяющих заголовки
For rowlntiex = 2 То UBound(rows)
' Получаем отдельные столбцы
colsC) = Split(rows(rowlndex), Chr(1))
' Добавляем запись
rs.AddNew
For collndex = 0 To UBound{cols)
rs.Fields(collndex) - RTrim$(cols(colIndex))
Next
rs.Update
Next

" Возвращен Recordset вызывающему


Set QueryResultToflecordset = rs

End Function

Рис. 4. Отображение QueryResults

Объект SQLServer — не единственный член иерархии, возвращающий


объект QueryResults. Я насчитал в объектной модели SQL-DMO более 80
методов Enumjcxu:, позволяющих получать информацию практически
любого вида, в том числе зависимости между объектами базы данных, под-
писки репликации (replication subscriptions), таблицы связанных серверов
и др. Конечно, эти методы позволяют не только показывать результаты
пользователю, но и решать более важные задачи. Например, вы можете
написать изощренные утилиты, помогающие оптимизировать работу сис-
темы за счет мониторинга блокировок и процессов.

Объект Database
Это, пожалуй, самый интересный с точки зрения разработчика объект в
иерархии DMO-SQL. Получив на него ссылку, вы можете создавать и уда-
Автоматизация выполнения административных задач в SQL Server

лять все таблицы, представления, хранимые процедуры, пользователей и


группы базы данных вплоть до отдельных столбцов, индексов и триггеров,
перебирать эти объекты и изменять их поведение (рис. 5).

DataBaseRoles

FullTeitCattlogs

StoredProcedures

SystemDatalypes

UserDefinedDataTypes

FileGroups

Рис. 5. Объект Database

Поскольку все зависимые объекты группируюгся в наборы, перебирать


объекты очень легко. Так, код на рис. 6 перечисляет все таблицы (и их
столбцы) базы данных Pubs, поставляемой с SQL Server.

У большинства объектов базы данных есть свойство SystemObject, возвра-


щающее True (если объект сгенерирован системой) или False (если объект
определен пользователем), поэтому нетрудно показывать при выводе
только те объекты, которые вас интересуют:
1
Перечисляем все несистемные хранимые процедуры
' в базе данных Pubs и их определения на языке T-SQL
Dim sp As SQLDMO.StoredProcedure

For Each sp In db.StoredProcedures


204 Microsoft SQL Server

If Not sp.SystemObject Then


Print "- PROCEDURE " & sp.Name
Print sp.Text
End If
Next

Рис. 6. Список таблиц и их столбцов в базе данных Pubs


' Предполагается, что объект SQLServer подключен к серверу
Dim db As SQLDMO.Database
Dins tbl As SQLDMO. Table
Dim col As SQLOMQ.Column

Set db - SQLServer.Databases("pubs")
For Each tbl In db.Tables
PriRt "TA8L£ " 4 tbl.Nase
For Each col In tbl.Columns
Print " " 8 col.Name
fiext
Next

Создание и выполнение сценариев Т-SQL


Многие объекты SQL-DMO, в том числе Database, Table, Index, Key, Job,
Alert, Trigger, User, Rule, Check и большинство объектов репликации, со-
держат метод Script, который возвращает сценарий Т-SQL, генерирующий
этот объект. Но учтите, что данный сценарий не создает внутренние объек-
ты. Например, следующий код создает сценарий, содержащий только
оператор CREATE DATABASE и группу вызовов хранимой процедуры
sp_option,
' Получаем сценарий на Т-SQL для базы данных Pubs
Dim pubsDB As SQLDMO.Database
Set pubsDB = SQLServer.Databases("pubs")
Print pubsDB.Script

Чтобы воссоздать всю структуру базы данных, необходимо вызвать метод


Script для всех внутренних объектов иерархии Database, в частности для
всех таблиц, представлений и хранимых процедур. Однако метод Script
объекта-таблицы облегчает генерацию сценариев, создающих базы дан-
ных. Он принимает аргумент с набором битовых флагов, указывающих,
надо ли также генерировать сценарии для создания индексов, ограничений
(constraints), триггеров и внешних ключей (рис. 7).

SQL-DMO выполняет один пакет (batch) единовременно и при анализе


SQL-сценариев не проверяет наличие разделителей пакетов. Поэтому в
Visual Basic сгенерированный сценарий нельзя выполнить с помощью ме-
Автоматизация выполнения административных задач в SQL Server 205

года Execute Immediate или любого другого метода Ехеси1еЛда;, не интер-


претируя сценарий самостоятельно. При программной генерации сценари-
ев T-SQL очень удобно, что их можно обрабатывать как строки. Например,
можно взять таблицу и создать новую таблицу с такой же структурой и
другим именем (рис, 8).

Рис, 7. Генераций сценария T-SQL для создания таблиц

' Генерируем сценарий T-SQL для создания всех таблиц


' в базе данных Pubs и их внутренних объектов
Dim tbl As SQLDHQ.Table
Dim allParams As Long, щ! As String

' Зти константы определены в библиотеке типов ОНО


allParams = SOLDMQScript_Default Or SQLDMGScript.lndexes
Or SQLOHOScript_DRI_AllConstraints Or _
SGLDMOSerlptJYiQgers Or SQLDMOScript_DflI_ForeignKeys
For Each tbl In pubsDB.Tables
sql * sql & tbl.Script(allParaiBS)
Next

Рис. 8., Создание таблицы с той же структурой, что и у существующей

' Чтобы создать сценарий для таблицы о другим именем,


' используйте параметр NewName метода Script, как
1
,показано ниже

Din oSQLServer As SOLDHO.SQLServer


Set oSOLServer * Hew SQLDMO.SQLServer.

oSQLServer.LoginSecure * True
,oSQLServer.Connect "(local)"

Dim oTabla As SQLDMO.Table


Set oTable = oSQLServer.Databases("pubs"}.Table8C"sales")

Dim sqlText As String


Dim ScriptOptionsI As SQLDMO.SQLDMQ_SCRIPT_TYPE
Dim ScriptDptions2 As SQLDMQ.SQLBM(LSCRIPT2J"YP€

ScriptOptionsI = SQLDMOScript.Default * SQLDHOScriptJJwnerQualify


ScriptOptions2 = SULOMOScript2_Default

sqlText = oTa61e.Script(ScriptQptiotis1, , "New Sales", ScriptOptions2)


1
Метод надо ввполнять в контексте базы данных Pubs, чтобы разрешать
" (resolve) используемые в таблице sales пользовательские типы данных,

aw. след. стр.


206 Microsoft SQL Server

Рис. 8. Создание таблицы с той же структурой ... (окончание)


' а также из-за отсутствия в sqlText оператора "use pubs"

oSQLServer.Databases("pubs").Exeeutelmmediate sqlText

' Для выполнения метода в контексте сервера удалите ссылки


" н а базу данных, так как здесь это будет приводить к ошибке
' из-за отсутствия oSQLServer,Exeeutelmmediate sqlText

oSQLServer.Disconnect

Set oSQLServer = Nothing

Создание заданий и расписаний их выполнения


Управление заданиями (jobs) и их выполнение по расписанию — та об-
ласть, где в полной мере проявляется полезность технологии SQL-DMO.
И действительно вам, как правило, нужно привести поведение SQL Server
в соответствие с потребностями пользователей, а эти потребности часто
меняются. Объекты поддерева JobServer иерархии DMO (рис. 9) позволя-
ют разрабатывать клиентские приложения, которые дают возможность
пользователям с помощью простого и удобного CI создавать и выполнять
по расписанию комплексные задания, связанные с обслуживанием базы
данных.

В рамках одной статьи нельзя детально рассказать о каждом из дочерних


объектов объекта JobServer, поэтому я сосредоточусь на самых важных и
полезных из них; объекте Job и зависимых от него объектах JobStep и
JobSchedule. Если вы знакомы с созданием заданий и настройкой распи-
сания их выполнения в SQL Server Enterprise Manager, вам будет неслож-
но применять эти объекты.

Посмотрим, как создать задание, проверяющее целостность базы данных


и выполняющее ее резервное копирование, если ошибок не обнаружено.
Создадим объект Job. присвоим значения его свойствам Name и Descrip-
tion, а затем добавим этот объект в набор Jobs объекта JobServer:
Создаем и инициализируем объект Job
Dim SQLJob As New SQLDMO.Job
SQLJob.Name = "Northwind Backup"
SQLJob.Description = "Check and Backup Northwind"
1
Добавляем его в набор Jobs
SQLServer.JobServer.Jobs.Add SQLJob

Каждая операция задания (job step) в Enterprise Manager соответствует


объекту JobStep иерархии SQL-DMO. Операции задания бывают трех ти-
Автоматизация выполнения административны* задач в SQL Server

нов: команды T-SQL, команды Windows Script и команды операционной


системы. Тип операции задается присвоением свойству Subsystem соот-
ветствующего строкового значения,

AlerlCategories

JabCategories

JabSchedules

JobSteps
QperatorCategories

Operators
TargetSermGroups

MemberServers

TargetServers

Рис. 9. Поддерево JobServer

Вот как, например, создать операцию задания, выполняющую команду


DBCC CHECKDB применительно к базе данных Northwind:

Dim aJobStep As SQLDMO.JobStep


Set aJobStep = New SQLDHO.JobStep
aJobStep.Name = "Step 1: Check Northwind"
aJobStep.StepId = 1
aJobStep.Subsystem = "TSQL"
aJobStep.DatabaseName = "Northwind"
aJobStep.Command = "DBCC CHECKDB ('Northwind', REPAIR.FAST)"
aJobStep.OutputFileName = "c:\temp\job1.log"
208 Microsoft SQL Server

Свойство OutputFileName служит для задания имени и пути файла, в ко-


торый выводятся данные при выполнении данной операции задания. При
использовании этого свойства возникает проблема: для каждой операции
задания содержимое файла перезаписывается (тогда как Enterprise Mana-
ger позволяет дописывать информацию в конец файла). Это одна из не-
многих ситуаций, где модель SQL-DMO менее гибка, чем Enterprise Mana-
ger. Такое ограничение можно обойти, используя для каждой операции
задания свой временный файл, а потом объединяя их содержимое в файл
реального вывода.

Чтобы указать, чем закончилось выполнение операции задания — успехом


или ошибкой, вы присваиваете значения свойствам OnSuccessAction и
ОII Failure Action соответственно- Эти свойства принимают одно из четы-
рех значений перечислимого типа: SQLDMOJobStepAction_QuitWith-
Success, SQLDMOJobStepAction_QuitWithFailure, SQLDMOJobStepAc-
tion_GotoNextStep или SQLDMOJobStepAction_GotoStep.

Допустим, если команда DBCC CHECKDB выполнена успешно, нужно


перейти к следующей (второй) операции, а при неудаче задание требует-
ся завершить:
aJobStep.OnSuccessAction = SQLDMOJobStepAction_GotoNextStep
aJobStep.OnFailureAction = SQLDMOJobStepAction_QuitWithFailure

Можно перейти и к любой другой операции, но для этого надо присвоить


значение свойства Stepld этой операции свойству OnSuccessStep или
OnFailureStep:
Если DBCC CHECKDB потерпит неудачу, переходим к 3-й операции
aJobStep.OnFailureStep = 3
aJobStep.OnFailureAction = SQLDKOJobStepAction_GotoStep

Закончив определение текущей операции задания, добавьте объект Job-


Step в набор JobSteps родительского объекта Job:
SQLJob.JobSteps.Add aJobStep
Теперь можно определить остальные операции задания. Например, на
рис. 10, если команда DBCC CHECKDB завершилась успешно, вторая
операция выполняет резервное копирование базы данных, а третья — за-
пускает ActiveX-сценарий, объединяющий вывод предыдущих операций в
файл журнала приложения. Для запуска ActiveX-сценария свойству Sub-
System объекта JobStep присваивается значение «ActiveScripting», свой-
ству DatabaseName — имя языка сценариев (VBScript или JScript), а свой-
ству Command — код сценария. (Можно также выполнить команду опера-
ционной системы, присвоив свойству Subsystem строку «CmdExec». a
свойству Command — текст команды операционной системы. Скажем, для
Автоматизация выполнения административных задач в SQL Server 209

запуска служб, отвечающих за работу с Интернетом, свойству Command


присваивается значение IISRESET START.)

Рис. 10. Проверка базы данных, ее резервное копирование


и генерация файла журнала
* - Создаем к инициализируем объект Job
Dim SQLJob As New SQLDMO.Job

SQUob.Naaie - "Korthwind Saekup"


SQLJob.Description = "Check and Backup Northwind"
' Добавляем объект в набор Jobs
SQLServer.JobServer. Jobs. Add SQUob

' - Первая операция: проверка базы данных


Din aJobStep As SQLDMO.JobStep
Set aJobStep = New SQLDMO.JobStep
aJobStep.Naiae = "Step 1: Check NorthwiiKl"
aJobStep.StepId = 1
aJobStep,Subsystem = "TSQi"
aJobStep,BatabaseName = "Northwind"
aJobStep.Comiiarwt = "BBCC CHECKOB {'Northwlnd', REPAIR_FAST)"
aJobStep.OutpytFileHaHie *= "c:\temp\job1.tinp"
' Если DBCC CttECKDB терпит неудачу, переходим к 3-й операций
aJobStep.OnFailureStep = 3
aJotjStep.OnFailureAetion = SQLDMQjobStepAction_QotoStep
SQLJob.JobSteps.Add aJobStep

' - вторая операция: резервное копирование базы данных.


1
йожно повторно использовать объект JobStep
Set aJobStep = New SQLDHQ.JobStep
1
Указываем, где размещать резервные копии
bakdir = SQLServer.flegistry.SQLDataPath & "\backup"
aJohStep,Kerne * "Step 2: Backup Northwlnd"
aJobStep.StepId * 2
aJooStep.Subsystem = "TSQI"
aJobStep.DatabaseHaiue - "Northwind"
aJobStep.Command = "BACKUP DATABASE [Northwind] TO DISK = '" _
bakdir & "\Korthwlnd.bak1 "
' За«етьте; испопьзуетсв другой файл журнала,
' чтобы не перезаписывать вывод 1-й операции
.aJobStep.GutpytFlleNaroe ^ "c:\terBp\job2.tmp"
' В любом случае переходик к следующей операции
ajobStep.<toSiUceesaActiQn =• SQLDHOJobStepActionjjotoNextStep
aJobStep.QnFailureAction = SQLDMOJobStepAction_Got«HextStep
SQLJob.JobSteps.Add aJobStep

- Третья операция: с помощью сценария создаем общий файл журнала


Set aJobStep = New SQLDMO.JobStep
см. след, стр.
210 Microsoft SQL Server

Рис. 10. Проверка базы данных, ее резервное..,


aJobStep.Name - "Gather output and delete tap files"
JobStep.StepID » 3
JobStep.Subsystem = "AetiveScrtpting"
' При использовании сценария свойству DatabaseName
' присваивается название языка сценариев
avIobStep.DatabasefiaBe = "VBScript"

' Создаем сценарий


Ш!в Script As String
Script = "Did fso* stepld, file, text, filename" & vbCrlf & _
"Set fso - CreateObject(""Scrlptlne-PibSystemObjecf'")" _
4 vbCrlf & _
"* gather text front all temporary files and delete them '" _
& vbCftf 4 _
"For stepIO = 1 To 2" & vbCrLf & „
filemne - ""c:\temp\Job"" & CStr(stepID) & "".trap""" „
4 vbCrlf & _
Set file = fsQ.OpenTextFileCfilename, 1, True, True)" „
& vbCrlf & _
text * text 4 file.ReadAll" & vbGrLf & _
file.Close" 4 vbCrlf & _
fso.DeleteFiie filenaffle" & vbCrtf & _
"Next" & vbGrtf S _
"Set file * fsD.CreateTextFUeC""" & txtFile & """, True)" _
& VbCrLf
' Завершаем сценарий - обратите внимание, что сценарии объекта Jo
' ДОЛЖНЫ явно присваивать всем объектам значение Nothing
Script = Script 4 „
"file.WriteLine ""Output from '" & SQLOot.Nflffle „
4 "' job at "" S №ж£)" 4 vbCrLf & _
"file.Wrltellra StrlRf(40, ""--")" & vbGrlf & _
"file.Write text" 4 vbCrLf 4 _
"file,Close" 4 vbGrlf & vbCrtf & „
"Set fso * Nothing" 4 vbCrLf 4 _
"Set file » Nothing" 4 vbCrLf

* Присваиваен созданный сценарий свойству Command объекта JobStep


aJebStep.Coeraand ;= Script
' В любом случае выполнение задания завершается
aJobStep.GflSuccessActlon = SQiOMOJobStepActioR_QuitWithSuccess
adobStep.OnFailActien * SQlD№)JobStepAction_QuitWithFailMre
SQUeb.JebSteps.Add aJobStep

'- Применяем задание.


' Указываем операции, с которой начинается выполнение
SQUofe.StartStepIO « 1
' Применяем к локальному SQL-сервзру
SQLJob.ApplyloTargetServer "(local)"
Автоматизация выполнения административных задач в SQL Server 211

После создания объект Job применяется к указанному объекту SQLServer


и запускается методом Invoke или Start. Эти методы делают одно и то же,
но Start позволяет указать операцию, с которой начинается выполнение
задания.

Последний штрих — подготовка одного или нескольких расписаний вы-


полнения задания. Для этого вы создаете объект JobSchedule, инициали-
зируете его свойства и добавляете этот объект в набор JobSchedules со-
зданного объекта Job. В свойствах поля Schedule объекта JobSchedule
указываются время начала и окончания действия расписания, а также пе-
риодичность выполнения задания (рис. 11). Вы можете установить ежед-
невное, еженедельное или ежемесячное выполнение и выбрать, в какие
дни недели будет выполняться задание. Заметьте, что свойства Active-
StartDate и ActiveEndDate объекта Schedule — значения типа Long в фор-
мате ггггммдд, а свойства ActiveStartTime и ActiveEndTime — значения
типа Long в формате ччммсс.

Рис. 11. Расписание выполнения задания


' Получаем ссылку ма задание, для которого формируется расписание
Dim SQLJob As SQLDMO.Jotj
Set SQLJob = SQLServer.JobServer.Jobs£"Horthwind Backup")

'; Объект JobSchedule будет задавать ежедневное резервное копированию


Dim SQLSchedule As New SQLBHQ.JobSchedule
SQLSchedule.Name = "Daily &ackup"
SQLSchedule.Schedule.FrequeneyType = SGieKOFreo_Dally
SQLSchedulS.Schedule.Frequencylnterval = 1
* Начало действия - 18 октября 2000 г, в 23:55
SQLSchedule.Schedule,ActiveStartDate = 20001018
SOLSchedyle.Schedule.ActiveStartTiffleOfBay » 2355ФО
' Расписание никогда не прекращает действовать
' (можно было бы не указывать дату и время прекращения действия)
SQLSehedule.Schedule.ActiveEndDate » SQLDMO.NOEHDDATE
SQtSchedule. Schedule, Active En dTimeOf Day *= SQLDMOJIQENOTIHE

• Добавляем расписание в объект Job


SQLJob.BeginAlter .
SSLJob. JobSchedules.Add SQLSchedule
SQUob.DoAlter

Заключение
В примерах кода к статье содержится демонстрационное приложение, по-
зволяющее выбрать одну или несколько баз данных и либо сразу же вы-
полнить их резервное копирование, либо указать расписание: каждые п
Microsoft SQL Server

дней, начиная с заданных даты и времени (рис. 12). Если щелкнуть кноп-
ку (Ж, программа динамически создаст объект Job, состоящий из несколь-
ких операций, и инициализирует его дочерний объект JobScheduIe в соот-
ветствии с параметрами, заданными пользователем. Это пример простого,
но надежного UI, который по достоинству оценят большинство пользова-
телей.

Рис. 12. Выбор расписания резервного копирования баз данных

В своей статье я лишь прошелся по верхам объектной модели SQL-DMO,


тем не менее она позволяет ознакомиться с этой моделью. Большинство
объектов в иерархии DMO работает весьма предсказуемым образом, так
что освоить их несложно. Конечно, на это уйдет определенное время, но
информация и примеры кода в моей статье помогут вам быстро добиться
своих целей.

Франческо Балена (Francesco Balena) — главный редактор «Visual Basic


Journal» (Италия), редактор и ведущий постоянной рубрики в «Visual Basic
Programmer's Journal», автор книги «Programming Visual Basic 6.0» (Microsoft
Press, 1999). Часто выступает на конференциях разработчиков. С ним
можно связаться через сайт http://www.vb2themax.com.
Дэйв Грундгейгер, Энсон Голдэйд и Вэрон Фугман

Вызов хранимых процедур


и получение их результатов
через Web*

Разработчикам клиентских интерфейсов часто приходится расширять


функциональность презентационного уровня в многоуровневых приложениях,
но даже простое получение данных и их отображение требует внесения
изменений во все уровни. Этот процесс можно упростить и сделать более
гибким, используя хранимые процедуры SQL Server для автоматизации
передачи данных в формате XML из базы данных в клиентские компоненты.
В статье представлен компонент, в котором хранимые процедуры вызывают-
ся с помощью XML-строк. Данные возвращаются также в XML-формате, над
ними выполняется XSL-преобразование, и они предоставляются клиенту в
виде HTML. Эта методика позволяет быстро вносить изменения в приложе-
ние, не отказываясь от его многоуровневой архитектуры. Описанный в статье
подход применим в работе с SQL Server 7.0 или SQL Server 2000.

Как и многие другие разработчики, мы считаем, что в n-уровневых бизнес-


приложениях традиционные Windows-клиенты эволюционируют в уни-
версальные клиенты на основе браузера. В таких системах для доставки
Web-страниц применяются Microsoft IIS (Internet Information Services) и
ASP, для реализации бизнес-объектов промежуточного уровня (middle-
tier) — компоненты, написанные на Visual Basic, а для хранения данных —
Microsoft SQL Server.

Это замечательные средства, обеспечивающие высокую гибкость, но за


гибкость приходится расплачиваться. Когда пользователь требует доба-

* Публиковалось в MSDN Magazine. 2001. №8 (август). — Прим. изд.


214 Microsoft SQL Server

вить в приложение новые функции, разработчикам приходится тратить


много времени, чтобы реализовать их поддержку на каждом уровне. Это
верно даже для простых изменений вроде добавления поля адреса в окно
заказа товара. При этом приходится определять новые методы (а иногда и
классы) и заново развертывать компоненты.

Разработчикам клиентских интерфейсов настолько часто требуется реали-


зовать ввод какой-либо дополнительной информации, что мы решили най-
ти способ, который позволил бы ускорить внесение нужных изменений в
многоуровневое приложение. В созданной нами архитектуре XML исполь-
зуется клиентской частью приложения для вызова специально написан-
ных нами хранимых процедур SQL Server. Результаты, возвращаемые эти-
ми процедурами, передаются клиенту тоже в XML-формате. Поскольку
клиент может вызывать только определенные хранимые процедуры и по-
скольку XML перехватывается и обрабатывается на промежуточном уров-
не, мы сохранили все преимущества многоуровневой архитектуры и вновь
получили возможность использовать средства RAD (Rapid Application
Development) при работе над многоуровневой системой. И еще одно при-
ятное преимущество. Так как данные передаются в XML-формате, разра-
ботчики клиентских интерфейсов могут легко создавать красиво оформ-
ленное визуальное представление данных. В этой статье рассказывается об
архитектуре, которую мы разработали, чтобы реализовать такое взаимо-
действие компонентов.

Исходный код и скомпилированные компоненты доступны на сайте MSDN


Magazine по ссылке http://msdn.microsoft.com/ msdnmag/codeOl.asp в раз-
деле за август.

Первая попытка
Мы с самого начала понимали, что данные должны передаваться на пре-
зентационный уровень в XML-формате, так как это позволило бы нам ис-
пользовать XSL для преобразования XML в HTML (поддержка наиболее
распространенных браузеров была для нас очень важна, равно как и воз-
можность изменять визуальное представление без перекомпиляции при-
ложения). Однако было неясно, как создавать XML-данные и передавать
их на презентационный уровень. Для надежности мы хотели инкапсули-
ровать создание и синтаксический анализ XML. Инкапсуляция позволи-
ла бы скрыть детали использования XML от разработчиков, незнакомых
с синтаксическим анализатором (parser) MSXML.

Мы добились этих целей, создавая оболочки для XML-схем в виде объек-


тов Visual Basic. Для каждого набора данных, показываемого на презента-
ционном уровне, определяется своя схема. Например, для поддержки окна
заказа на приобретение товара мы определили соответствующую XML-
Вызов хранимых процедур и получение их результатов через Web 215

схему. Как оказалось, определив нужную XML-схему и разработав на


Visual Basic класс-шаблон, создавать классы, свойства которых соответ-
ствуют элементам схемы, несложно. Надо просто задать свойства объекта,
а затем считать свойство XML объекта, чтобы получить соответствующие
X ML-данные.

Объекты работали наоборот. То есть вы могли бы присвоить XML-строку


свойству XML объекта, что заставило бы объект выполнить разбор XML
(с помощью синтаксического анализатора MSXML) и соответствующим
образом настроить свои открытые свойства. Двое из нашей группы разра-
ботчиков, Дэйв Грундгейгер и Патрик Эскарсега (Patrick Escarcega) рас-
сказали об этой методологии в статье «XML Wrapper Template: Transform
XML Documents into Visual Basic Classes» (http://msdn.microsoft.com/
msdnmag/issues/01/Ol/xmlwrap/xmlwrap.asp), опубликованной в номере
«MSDN Magazine» за январь 2001 г.

Наши усилия увенчались успехом. Мы получили унифицированный спо-


соб генерации XML-данных и их передачи между уровнями приложения.
Это ускорило разработку многоуровневых приложений, поскольку мы
использовали простой в модификации класс-шаблон. Однако мы чувство-
вали, что разработчики клиентской части, реализуя отображение дополни-
тельной информации, по-прежнему тратили слишком много усилий. При
внесении таких изменений приходилось изменять XML-схему, чтобы
учесть вывод новой информации, модифицировать класс — оболочку схе-
мы и уровень хранения данных, который должен предоставлять новые
данные. Кроме того, это требовало изменений в бизнес-уровне (уровне
прикладной логики) для передачи новых данных на презентационный уро-
вень и в самом презентационном уровне, чтобы он мог использовать эти
данные. Значит, нам нужно более эффективное решение.

Решение
А что если вместо определения уникальной XML-схемы для каждого на-
бора данных, поступающего на презентационный уровень, сделать так,
чтобы сами данные определяли формат XML? И почему бы, вместо того
чтобы поддерживать уникальные классы для каждого набора данных, не
разработать универсальный код, который будет считывать данные и созда-
вать XML? Тогда мы смогли бы экономить по два-три дня на введении
поддержки каждого нового набора данных! Именно это и было целью раз-
работки нашей архитектуры. Она показана на рис. 1. Вот что представля-
ют собой ее компоненты.

ASP-страницы В нашей архитектуре ASP-страницы служат лишь для


создания экземпляров СОМ-объектов, выполняющих реальную работу.
216 Microsoft SQL Server

УРОВЕНЬ
s KpSH.BHI
aotryni
[ лроиещри
Ш$*ИШ1С
«iSKSener

Рис. 1. Архитектура доступа данным с использованием XML

Объекты презентационного уровня Этот компонент обращается к биз-


нес-уровню, чтобы получить данные в XML-формате. Затем он преобразо-
вывает XML в HTML и записывает HTML в US-объект Response.

Объекты бизнес-уровня Этот компонент предоставляет объекты и мето-


ды, используемые презентационным уровнем. Какие объекты и методы до-
ступны на этом уровне, зависит от требований бизнеса; объектам бизнес-
уровня не известен механизм чтения данных (об этом — потом).

Адаптер доступа к данным и компонент доступа к данным Эти два ком-


понента используются совместно, чтобы с помощью XML вызывать храни-
мые процедуры и возвращать результаты в формате XML.

Хранимые процедуры Неотъемлемая часть архитектуры. Мы не хотели,


чтобы универсальный механизм доступа к базе данных опирался на XML,
так как это привело бы к размытию границ между уровнями. Вместо это-
го он обращается только к хранимым процедурам, написанным в опреде-
ленном формате. То есть лишь хранимым процедурам известна структура
базы данных.

В следующих разделах мы рассмотрим, как работает каждый из уровней,


и вы увидите, как в нашей архитектуре обслуживаются запросы на полу-
чение информации. Центральное место в статье занимают разделы, где
описываются адаптер доступа к данным, компонент доступа к данным и
хранимые процедуры. Но сначала мы расскажем о том, как вызываются
базовые компоненты и как используются возвращаемые значения.

Пример Web-приложения
В код, который можно скачать к этой статье, мы включили пример Web-
приложения, использующего нашу технологию. Оно предоставляет про-
стой Ш к базе данных Northwind, поставляемой с SQL Server. Как развер-
тывать компоненты, мы поясним позже.

После установки компонентов Web-приложение можно запустить, открыв


его страницу login.asp (рис. 2). Введите Employee ID (1-9) и щелкните
кнопку Log In. Появится страница Employee Detail (сведения о сотрудни-
Вызов хранимых процедур и получение их результатов через Web 217

ке), где показывается информация о вошедшем в систему сотруднике и


заказах, за обработку которых он отвечает (рис. 3).

http://myserver/ Microsoft Internet KMptoner

Histor

Address'! http;//rnyserver/lagin, asp

Enter Your Employee ID To Log In


(fD's 1 Through 9 are Valid)

Рис. 2. Страница для входа сотрудника

Ц http://niyserver/empdetai
Edit

Employee Information:
Ms. Nancy Davolio
Sales ^Representative

Create New Order

Here is a list of the orderer? for which you are the sales representative.
Order ID Customer Name Shipped Date Total
11077;, Hattiesnake Canyon Grocery 1255^
П071 LILA-Supennercado S-484.50
.00
\ 10 67 Drachenblut Delikatessen 05/06И9Э
ji|M Save-a-bt Markets '--:-:! 05/04/199
11039 LINO-Delicateses
Supremes delices
1 1 027 Bottom-Dollar Markets

Рис. З. Страница сведений о сотруднике


218 Microsoft SQL Server

Страница Employee Detail создается путем запроса XML-данных от уни-


версального компонента доступа к данным SQL Server и XSL-преобразо-
вания этих XML-данных в HTML. XSL не является темой статьи, поэто-
му мы не будем рассматривать, как выполняется преобразование. Вместо
этого поясним, как наши универсальные компоненты позволяют преобра-
зовывать данные SQL Server в XML и наоборот.

ASP-страницы
Выборка данных начинается, когда пользователь открывает одну из ASP-
страниц системы. Мы рассмотрим процесс выборки на нескольких примерах
исходного кода. Наш код реализует гипотетическое приложение, работа-
ющее в интрасети и позволяющее сотруднику входить в систему и офор-
млять заказы. Пользователь входит в систему, открывая в браузере стра-
ницу login.asp и вводя свой идентификатор (employee ID). После успеш-
ного входа происходит перенаправление на страницу empdetail.asp. Эта
страница полностью основана на нашей RAD- архитектуре, поэтому мы
детально рассмотрим ее работу.

Вот что представляет собой страница empdetail.asp:

' Создаем компонент записи и вызываем функцию EmployeeDetail,


' чтобы создать показываемую страницу. Функция учитывает, что
' возможны попытки входа с неправильным идентификатором.
Set Writer = Server. CreateObjectC'COMASP.cWriter")
Response. Write(Writer. EmployeeDetail( ) )

' Уничтожаем компонент записи


Set Writer = Nothing

Empdetail.asp создает экземпляр объекта cWriter презентационного уров-


ня (рассматривается в следующем разделе) и вызывает метод Employee-
Detail этого объекта. Метод EmployeeDetail генерирует HTML, показыва-
емый в браузере.

ASP-страницы нашего приложения-примера и функции, которые они вы-


полняют, показаны в табл. 1.

Табл. 1. ASP-страницы, используемые в примере

Страница Функции

login.asp Предлагает сотруднику ввести свой идентификатор. Остальные


страницы перенаправляют на эту страницу, если идентифика-
тор сотрудника не указан.

см. след. стр.


Вызов хранимых процедур и получение их результатов через Web 219

Табл. 1. ASP-страницы, используемые в примере (окончание)

Страница Функции

empdetail.asp Вызывает метод EmployeeDetail объекта cWriter и выводит


результаты в браузер методом Response.Write. Создается
страница, где показываются заказы, принятые данным сотруд-
ником.
orddetail.asp Вызывает метод OrderDetail объекта cWriter и выводит
результаты в браузер методом Response.Write. Создается
страница, где показываются позиции по данному заказу.
neworder.asp Вызывает метод CreateOrder объекта cWriter и выводит
результаты в браузер методом Response.Write. Создается
страница, где показывается список клиентов. Из этого списка
выбирается клиент, для которого оформляется новый заказ.
additem.asp Вызывает метод AddOrderltem объекта cWriter и выводит
результаты в браузер методом Response.Write. Создается
страница, где показывается список товаров, добавляемых
в заказ.

Объекты презентационного уровня


Презентационный уровень реализован в проекте COMASP.vbp на Visual
Basic. Этот проект компилируется в spdaui.dll. Он содержит единственный
класс, cWriter, несколько методов которого вызываются ASP-страницами.
Мы стремились к тому, чтобы в ASP-страницах было как можно меньше
кода. Наш компонент поддерживает ссылку на контекст ASP-сценариев
(ASP scripting context), который в свою очередь предоставляет компонен-
ту доступ к IIS-объектам Application, Request, Response, Server и Session.
Имея доступ к этим объектам, компонент может делать то же, что и ASP-
сценарий (и даже больше, так как используется Visual Basic, а не VBScript).

Ссылку на контекст ASP-сценариев возвращает метод OnStartPage наше-


го класса. Когда ASP-страница создает экземпляр объекта, IIS автомати-
чески вызывает этот метод объекта и передает ему ссылку на контекст сце-
нариев. Затем метод OnStartPage сохраняет эту ссылку для дальнейшего
использования другими методами класса. Обратите внимание, что для ра-
боты с контекстом сценариев в проекте на Visual Basic должна быть ссыл-
ка на «Microsoft Active Server Pages Object Library» (asp.dll).

Методы, предоставляемые классом cWriter, показаны в табл. 2. Рассмот-


рим метод EmployeeDetail, вызываемый VBScript-сценарием, который за-
пускается страницей empdetail.asp. Этот метод создает экземпляр объекта
бизнес-уровня MiddleTier.cEmployees и вызывает метод GetDetail этого
объекта, чтобы получить информацию о сотруднике, идентификатор кото-
рого указывается при вызове. Значение, возвращаемое GetDetail, — стро-
ка в XML-формате. Ниже показан соответствующий код.
220 Microsoft SQL Server

Set oEmployees = New MidflleTler.cEmployees


strEmployeeDetailXHL = oEmployees.GetDetail(lEmpID)

Табл. 2. Методы cWriter

Метод Вызывается: Описание

AddOrderltcm Страницей Определяет, что выбрал пользователь


additem.asp (добавить позицию в заказ, изменить
позицию или завершить ввод заказа),
и выполняет соответствующее действие.
CreateOrder Страницей Определяет, выбрал ли пользователь
neworder.asp клиента, для которого создается заказ.
Если да, в базе данных создается запись
заказа, и пользователь направляется
на страницу orderdetail.asp.
EmployeeDetail Страницей Создает список заказов, которые принял
empdetail.asp сотрудник, вошедший в систему. Иденти-
фикаторы заказов показываются как
гиперссылки на страницы с информацией
о позициях заказа.
EmployeeLogin Страницей Проверяет, есть ли в базе данных иденти-
login.asp фикатор сотрудника, указанный при входе,
а затем направляет пользователя
на страницу со сведениями о сотруднике
(empdetail.asp).
OnStartPage Когда на ASP- Используется, чтобы получить ссылку
странице, загружа- на текущий контекст сценариев.
емой IIS, создается
экземпляр класса
OrderDetail Страницей Выводит сведения о сотруднике и заказ-
orddetail.asp чике, а также список товаров, включенных
в заказ.

Затем XML загружается в DOM-объект MSXML для преобразования в


соответствии с XSL, определенным в файле empdetail.xsl. Вот код вызова
этого преобразования, также содержащийся в методе EmployeeDetail:

Set oXML = New MSXHL.DOMDocument


Set oXSL = New MSXML.DONDocument
oXHL.loadXML strEmployeeDetailXML

oXSL.Load App.Path & "\xsl\empdetail.xsl"


EmployeeDetail = oXHL.transfonnNode(oXSL)

Результат — строка в HTML-формате, которая является возвращаемым


значением функции EmployeeDetail и в конечном итоге показывается в
браузере.
Вызов хранимых процедур и получение их результатов через Web 221

Объекты бизнес-уровня
Объекты бизнес-уровня реализованы в проекте MiddleTier.vbp, написан-
ном на Visual Basic и скомпилированном в spdamid.dll. В этом проекте два
класса: cEmployees и cOrders (считывающие сведения о сотрудниках и за-
казах соответственно). В предыдущем разделе мы показали, как презента-
ционный уровень вызывает метод GetDetail класса cEmployees, чтобы по-
лучить информацию о вошедшем в систему сотруднике. Рассмотрим этот
метод, чтобы понять, что происходит, когда презентационный уровень
вызывает этот метод.

Метод GetDetail создает экземпляр объекта cAdapter компонента Data-


AccessAdapter (о нем — в следующем разделе), инициализирует свойства
объекта и вызывает его метод CallSP;

Set oAdapter = New DataAccesssAdapter.cAdapter


with oAdapter
.SPName = "Employeejjet.Detail"
.Parameters.Item("EmployeeID").Value = lEmployeelD
strXML = .CallSP(daRecords)
End With

Инициализируя свойства, вызывающий код сообщает адаптеру доступа к


данным, какая хранимая процедура вызывается и какие значения прини-
мают параметры. Возвращаемое методом CallSP значение — XML-строка,
в которой закодированы результаты, получаемые от хранимой процедуры,
О том, как осуществляется кодирование, мы расскажем чуть позже. Полу-
ченный XML при необходимости можно обработать на бизнес-уровне, но
в нашем примере XML просто возвращается на презентационный уровень
для преобразования в HTML.

Адаптер доступа к данным


Адаптер доступа к данным — один из двух универсальных компонентов,
используемых в примере (о втором мы расскажем в следующем разделе).
Эти два компонента позволяют вызывать специализированные хранимые
процедуры и получать их результаты в виде XML. Адаптер доступа к дан-
ным реализован в проекте DataAccessAdapter.vbp, написанном на Visual
Basic и скомпилированном в spdaad.dll. В проекте четыре класса: cAdapter,
cParameter, cParameters и lExpose.

cAdapter Основной класс компонента и единственный, чей экземпляр


могут создавать внешние объекты. Чтобы воспользоваться адаптером, вы-
зывающий код создает экземпляр этого класса, инициализирует его свой-
ства и вызывает метод CallSP. Свойствами класса cAdapter являются
SPName и Parameters.
222 Microsoft SQL Server

Свойство SPNarae указывает вызываемую хранимую процедуру базы дан-


ных. Его значение должно соответствовать имени существующей храни-
мой процедуры, написанной в подходящем формате. Важно заметить, что
универсального механизма вызова хранимых процедур нет. Попытка вы-
зова хранимой процедуры, имеющей неправильный формат, приводит к
ошибке.

Присвоение значения свойству SPNanie дает побочный эффект; у базы


данных запрашивается (через компонент доступа к данным) список пара-
метров, используемых этой хранимой процедурой. Их список заносится в
набор Parameters объекта cAdapter, что удобно для вызывающего кода.

Свойство Parameters (только для чтения) содержит ссылку на объект


cParameters. Через это свойство вызывающий код задает значения пара-
метров.

В классе cAdapter определен лишь один метод, CallSP:

Public Function CallSP(ReturnType As ReturnDataEnum) As String

Эта функция принимает один параметр — ReturnType типа ReturnData-


Enum. Его значение указывает, какие результаты вызывающий код ожида-
ет получить от хранимой процедуры. Перечислимое ReturnDataEnum оп-
ределяется так:
Public Enum ReturnDataEnum
daParameters == О
daRecords = 1
End Enum

Значение daParameters указывает, что хранимая процедура возвращает


данные в одном или нескольких выходных параметрах, daRecords — что
хранимая процедура возвращает данные в наборах результатов. Заметьте,
что задавать эту информацию должен вызывающий код.

cParameter Этот класс представляет параметр, передаваемый хранимой


процедуре. Вызывающий код получает ссылку на объект cParameter через
набор Parameters объекта cAdapter. Имя параметра используется в Parame-
ters как ключевое значение. Так, следующая строка кода присваивает зна-
чение параметру EmployeelD:
oAdapter. Parameters. Item("EntployeeID"). Value = lEmployeelD

Если вызывающий код указывает имя параметра, который не поддержива-


ется хранимой процедурой, заданной в свойстве SPName объекта cAdapter,
генерируется ошибка.

cParameters Этот класс является набором объектов cParameter.


Вызов хранимых процедур и получение их результатов через Web 223

lExpose Этот класс — не более чем определение интерфейса. Мы опре-


делили интерфейс lExpose для поддержки отладки и тестирования. Он ре-
ализуется классом cAdapter.

Метод CallSP класса cAdapter упаковывает имя и параметры хранимой


процедуры в XML-строку и передает эту строку компоненту доступа к
данным. Тот распаковывает имя и параметры хранимой процедуры и вы-
зывает хранимую процедуру через ADO. Затем получает возвращаемые
процедурой данные, упаковывает их в XML и передает адаптеру доступа
к данным. Мы пошли таким путем, поскольку хотели, чтобы компонент
доступа к данным был полностью ориентирован на использование XML,
Таким образом, при вызове хранимой процедуры передается XML, и ре-
зультаты тоже возвращаются в XML-формате. Адаптер доступа к данным
предназначен для того, чтобы вызывающий код мог обращаться к функци-
ям компонента доступа к данным, ничего не зная об XML.

Для большей наглядности в статье мы приводим упрощенную версию кри-


тически важного кода CallSP. (Реальный код отслеживает промежуточную
информацию, предоставляя доступ к ней через интерфейс lExpose.)
1
Следующие значения уже заданы:
strSPName (имя хранимой процедуры),
m_objParmCol (содержит набор cParameters объекта cAdapter),
ReturnType (значение перечислимого типа ReturnDataEnum)

Set oAccessor = New DataAccess.cAccessor


Set oXML = New MSXML.DOMDocuinent

strlnvokingXML = m_objParmCol.ParametersXML(strSPName)
strResultXHL = oAccessor.GetDataXML(strInvokingXML, ReturnType)

Здесь метод CallSP вызывает метод ParameterXML набора cParameters,


чтобы заключить параметры (и имя хранимой процедуры) в XML-строку
должного формата. (О ее формате мы расскажем потом.) После этого
XML-строка передается методу GetDataXML класса cAccessor компонен-
та доступа к данным.

Компонент доступа к данным


Компонент доступа к данным — рабочая лошадка системы. Он принимает
запросы в XML-формате, разбирает их, получая имя и параметры храни-
мой процедуры, вызывает хранимую процедуру и возвращает результаты
в XML-формате. Этот компонент реализован в проекте DataAccess.vbp,
написанном на Visual Basic и скомпилированном в spda.dll. В проекте один
класс, cAccessor, в котором доступно два метода: GetDataXML и Parame-
terXML.
224 Microsoft SQl Server

GetDataXML При вызове этого метода вызывается хранимая процедура


и принимаются возвращаемые результаты. Метод GetDataXML имеет сле-
дующий синтаксис:
Public Function GetDataXML(ByVal SPXMLState As String, ReturnType _
As ReturnTypeEnum) As String

SPXMLState — это XML-строка, задающая, какая хранимая процедура


вызывается и какие значения принимают ее параметры (если таковые
есть). Формат XML-строки поясняется в разделе «XML-описание вызова
хранимых процедур». Значение ReturnType задает, какой тип результатов
ожидает от хранимой процедуры вызывающий код. Перечислимое Return-
TypeEnum определяется так:
Public Enum ReturnTypeEnum
daParameters == О
daRecords = 1
End Enum

Значение daParameters указывает, что хранимая процедура возвращает


данные в одном или нескольких выходных параметрах. Значение daRe-
cords указывает, что хранимая процедура возвращает данные как набор
результатов. Обратите внимание, что это перечислимое в точности соот-
ветствует перечислимому ReturnDataEnum, используемому адаптером
доступа к данным.

Значение, возвращаемое GetDataXML, — это XML-строка, содержащая


результаты хранимой процедуры. О формате этой XML-строки мы расска-
жем в дальнейшем.

ParameterXML Этот метод вызывается после задания свойства SPName,


чтобы получить XML-описание хранимой процедуры. У него следующий
синтаксис:

Public Function ParameterXNL(6yVal StoredProcedureName As String) _


As String

Единственный параметр, StoredProcedureName, указывает имя хранимой


процедуры. Возвращаемое значение — XML-строка, содержащая XML-
представление хранимой процедуры, в том числе имена параметров, их
типы и направление. Формат этой XML-строки описывается далее.

Хранимые процедуры
Чтобы весь этот механизм работал, в нашей базе данных должны быть хра-
нимые процедуры, имеющие строго определенный формат. При разработке
этого формата мы стремились к гибкости в задании типа и объема возвра-
щаемых данных Кроме того, мы учитывали, что единственным модулем,
Вызов хранимых процедур я получение их результатов через Web

напрямую вызывающим наши хранимые процедуры, является компонент


доступа к данным, который формирует XML по возвращаемым данным.
Поэтому мы разработали схему, поддерживающую возврат результатов
хранимой процедуры в XML-формате.

Чтобы получить данные, необходимые для формирования XML, исполь-


зуется способность хранимой процедуры возвращать несколько наборов
результатов. Если вы еще не знаете, что ADO способна возвращать не-
сколько наборов результатов за один вызов, примите к сведению: это от-
личный способ сократить число обращений к базе данных при получении
данных, связанных между собой, но не принадлежащих одному набору
результатов. Так, Web-страница, показывающая заказы, принятые сотруд-
ником, содержит заголовок со сведениями о сотруднике и записи с инфор-
мацией о заказах. Вместо двух обращений к базе данных для получения
этих двух наборов данных, можно один раз вызвать хранимую процедуру,
которая возвращает сразу два набора результатов.

Хранимая процедура, которая с помощью операторов SELECT возвраща-


ет пять наборов результатов, показана на рис. 4. Код, который вызывает
эту хранимую процедуру и перебирает наборы результатов, мог бы выгля-
деть так:

' Предполагается, что где-то выше есть операторы Dim и Set для cmd

' Получаем первый набор результатов


Set rs = cmd.Execute()

Do While Not (rs Is Nothing)

1
Делаем что-то с rs...

' Получаем следующий набор результатов


Set rs = rs.NextRecordset

Loop

Все делается так же, как и при использовании одного набора результатов, —
просто добавляются цикл While и вызов NcxtRecordset

Первый набор результатов задает имя создаваемого набора XML-данных.


Компонент доступа к данным присваивает это имя самому внешнему
(outermost) элементу возвращаемого XML. За этим набором результатов
следует одна или несколько пар наборов результатов. Первый набор ре-
зультатов каждой пары содержит два дополнительных имени XML-эле-
ментов, используемых для указания имен, а второй — собственно возвра-
щаемые данные. Рассмотрим, например, хранимую процедуру Emplo-

В-5947
226 Microsoft SQL Server

yee_GetDetail, показанную на рис. 4. Компонент доступа к данным пере-


бирает эти наборы результатов в цикле (как показывалось в предыдущем
фрагменте кода) и генерирует XML (рис. 5). (Для краткости на рисунке
приведено лишь два элемента <Order>. В реальных данных элементов го-
раздо больше,)

Как уже упоминалось, хранимая процедура на рис. 4 возвращает пять на-


боров данных — по одному для каждого оператора SELECT. Рассмотрим
каждый из этих операторов.

Первый набор результатов генерируется оператором SELECT:


SELECT "EnployeeDetail" AS DataSet

Рис. 4. Хранимая процедура Etipioyee.J3eiDet.ail

CREATE PROCEDURE EfflployeejzetDetail


@Efliploy«eIO AS INT
AS

-- Задаем имя набора данных, которое возвращается,


— и используется в DOM-объекте на уровне документа
SELECT "EmployeeDetail" AS OataSet

— Указываем имена для элементов о сотруднике


SELECT "Employees" AS NextResultset, "Eaiployee" AS RecordType

-- Возвращаем записи, образующие часть DOM с информацией о сотруднике ,


SELECT EfoployeelD, LastName, FirstKams, Title, TitleOfCourtesy
FROM Employees
WHERE EmployeelD = «EmployeeZD

-- Указываем имена элементов, описывающих заказы, принятые сотрудником


SELECT "Orders" AS HextResultset, "Order" AS SecordType

— Возвращаем записи заказов


SELECT ord.QrderlO, ord-CustoffierlD, cus.CompanyNaeie,
CGNVERT(NVARCHAR, ord.OrderDate, 101} AS OrderDate,
CQNVERT(WARCHAR, orrf.ShippedDate, 101) AS ShippedOate,
Total = CAST(CAST{SU«(det,Quantity • (1 - det.Discount) *
det.UnitPrice) AS DECIMAL*10,2)> AS NVARCHA8)
FROM Orders ord JOIN Customers cus
ON cus.CustomerlD a ord.CustoirerlO
JOIN [Order Details] det
ON det.OrderlD = ord.OrderlD
«НЕЯЕ ord.EmployeelD = ^EmployeelD
GROUP BY ord.OrderlD, ord.OrderlO, ord.CustoinerlO, cu
SftlppedDate, orcf.OrderOate
0№ER BY ord.OrderlD DESC
Вызов хранимых процедур и получение их результатов через Web 22?

Рие. 5. Результаты хранимой процедуры Employee_GetDetail


<?xral version="1.G"?>
<EfflployeeDetail xmlns; dt="urn: schereas-nticrosoft-coni-r datatypes" >
<£шр1оуев8>
<£raployee>
<EmployeeIO dt:dt=*"i4">K/EBiployeeID>
; <LastName dt:dt="strirrg">DavQllo</LastName>
<fi rstName dt : dt= "string ">Narvcy</Fi rstName>
!
<Title dt;dt="string">Sales Representative</TLtle>
<TitleOf Courtesy dt:dt="string">Ms.</TitleOfCourtesy>
</Employee>
</Employees>
<0rders>

dt;dt*"i4">11077</OrdefIO>
dt: dt="string">RATTC</CLJStomerIO>
,<CoiapanyNaaie dt:dt="string">Rattlesr>ake
Canyon Grocery</CompanyName>
<0rder0ate dt:dt="string">05/06/199a</0rderpate>
<Shipped8ate dt:dt="string"x/ShippedDate>
<Total dt;dt="string">1255.72</Total>
</Qrder>
<0rder>
<OrcferID dt:dt="i4">n07K/OrderIB>
<CustOBier:D (Jt:dt=1'strin9">LIUS</CiJstoinerII)>
<CompanyName dt:dt="string">LILA-SiJpermerca(Jo</CoiBpanytJarne>
<OrderDate dt:at="string">05/05/1998</OrderDate>
<ShippedDate dt:dt="string"></ShippedDate>
<Total cft:dt="string">484.50<Aotal>
</Srder>
</Orders>
</ Etnp loyeeDet ail>

Он создает набор записей, содержащий одну запись с одним полем Data-


Set. Значением этого поля является EmployeeDctail. Первый набор резуль-
татов всегда должен содержать одну запись с одним полем DataSet. Ком-
понент доступа к данным использует значение этого поля для задания
имени самого внешнего элемента возвращаемого XML (рис. 5).

Затем идет пара наборов результатов, создаваемых следующими двумя


операторами SELECT:

SELECT "Employees" AS NextResultset, "Employee" AS RecordType

SELECT EmployeelD, LastName, FirstName, Title, TitleOfCourtesy


FROM Employees
WHEHE EmployeelD = @EmployeeID
Первый набор результатов задает имена, используемые в XML. Этот набор
результатов содержит одну запись с двумя полями: NextResultSet и Re-
cordType. Значение поля NextResultSet (в данном случае «Employees»)
указывает имя XML-элемента, содержащего набор XML-данных, а значе-
ние RecordType (в данном случае «Employee») — имя XML-элемента, со-
держащего отдельную запись. Второй набор результатов — сами данные.
Здесь считывается одна запись таблицы Employee, описывающая сотруд-
ника с идентификатором, переданным хранимой процедуре. На рис. 5 вид-
но, как имена полей возвращаемого набора результатов отображаются на
элементы XML-кода, возвращаемого компоненту доступа к данным.

Наконец, посмотрим на рис. 5 результаты двух последних операторов


SELECT хранимой процедуры, показанной на рис. 4. В этом случае пер-
вый набор результатов также задает имена, используемые для данных, воз-
вращаемых компонентом доступа. Второй набор содержит собственно дан-
ные — список заказов, принятых сотрудником,

Возврат данных через выходные параметры


Кроме возврата данных в наборах результатов, наша инфраструктура под-
держивает возврат данных и в выходных параметрах (либо один вариант,
либо второй, а не и то и другое одновременно). Ниже показана хранимая
процедура, возвращающая значение в выходном параметре.

CREATE PROCEDURE Employee J3etIDFromOrder


@OrderID AS INT,
eEmployeelD AS INT = NULL OUTPUT
AS
:

— Получаем идентификатор сотрудника для данного заказа


SELECT ©EmployeelD = EmployeelD FROM Orders WHERE OrderlD = eOrderlD

Хранимая процедура принимает идентификатор заказа и возвращает в


выходном параметре идентификатор сотрудника. Метод GetEmployeelD
класса cOrder в промежуточном уровне приложения вызывает эту проце-
дуру через адаптер доступа к данным:

Set oAdapter = New DataAccessAdapter.cAdapter


With oAdapter
.SPName = "Employee_GetIDFromOrder"
.Parameters.Item("OrderID").Value = lOrderlD
.CallSP daParaieters
lEmployeelD = .Parameters.Item("EmployeeID"}. Value
End With

Как видите, после вызова MeTozia CallSP объекта cAdapter возвращаемое


хранимой процедурой значение EmployeelD берется из набора параметров
этого объекта.
Вызов хранимых, процедур и получение их результатов через Web 229

Хранимая процедура может вообще не возвращать данные (ни в выходных


параметрах, ни в возвращаемом значении). Так, на рис. 6 показана храни-
мая процедура, создающая запись заказа. Эта процедура возвращает иден-
тификатор созданной записи. В класс cOrders промежуточного уровня
входит метод CreateOrder. содержащий код:
Set oAdapter = New DataAccessAdapter.cAdapter
With oAdapter
.SPName = "Order^CreateOrder"
,Parameters.Item("EmployeeID").Value = lEmployeelD
.Parameters.Item("CustomerID").Value = ICustomerlD
.CallSP daParameters
INewOrderlD = .Parameters, Item("RETURN_VALUE").Value
End With

Рие. 6< Хранимая процедура, создающая новый заказ


CREATE PROCEDURE Grder_CreateOrder
@EmployeelO AS INT,
^Customer-ID AS NCHAR(5)
AS

— Переменная с идентификатором создаваемой записи о заказе


DECLARE «OrderlU AS INT

— Добавляем запись заказа и возвращаем сгенерированный идентификатор


INSERT Orders (EmployeelD, Customer-ID) VALUES (@EmployeeID, @CustonierГО)

— Получаем только что сгенерированный идентификатор записи


SET $0rderXD - ^IDENTITY

"- Обновляем запись заказа: заносим в нее данные об отгрузке,


-- которые берутся из записи клиента
UPDATE Orders SET ShipName = cus.CompanyName, SnipAddreSS = cus.Address,
ShipCity = cus.City, ShipRegion ~ cus.Region,
ShipPostalCode = cus.PostalCode, ShipCountry = cus.Country,
QrderDate = GETDATEO
FROM Customers cus
WHERE cus.CustomerlD = ©CustomerlD AHD Orders.OrderlD = @>OrderID

-- Возвращаем идентификатор созданной записи заказа


RETURN ®OrderIO

Как показано в этом коде, наша инфраструктура добавляет элемент RET-


URN_VALUE в набор Parameters объекта cAdapter. Этот элемент содержит
значение, возвращаемое хранимой процедурой (если она возвращает ка-
кое-либо значение).
230 Microsoft SQL Server

XML-описание вызова хранимых процедур


Хранимые процедуры можно вызывать, напрямую передавая XML-строки
компоненту доступа к данным. Для этого создается экземпляр объекта
cAccessor и вызывается его метод GetDataXML. Синтаксис метода Get-
DataXML описывается в разделе «Компонент доступа к данным».

XML передается как параметр SPXMLState метода GetDataXML и содер-


жит информацию о том, какая хранимая процедура вызывается и какие
параметры в нее передаются. Ниже показан пример XML, описывающего
пашу хранимую процедуру Employee_GetDetail (рис. 4).
<CallSP SPName="Employee_GetOetail">
<RETURN_VALUE Direction="4" Datatype="3" />
<EmployeeID Direction="1" Datatype="3">K/EmployeeID>
</CallSP>

Внешним элементом должен быть <CallSP>. Атрибут SPName этого эле-


мента задает имя вызываемой хранимой процедуры. Если у хранимой про-
цедуры есть параметры или возвращаемое значение, они передаются как
дочерние элементы элемента <CallSP>. В случае параметров именем до-
чернего элемента является имя параметра, а в случае возвращаемого зна-
чения — RETURN_VALUE. Атрибут Direction указывает направление па-
раметра. Значение этого атрибута передается ADO при вызове хранимой
процедуры. Направление определяется одним из значений перечислимо-
го PararneterDirectionEnum, определенного в ADO:

Public Епш ParameterDirectionEntm


adParamUnknown = О
adParamInput = 1
adParamOutput = 2
adParamInputOul:put = 3
adParamReturnValue = 4
End Enum

Атрибут Datatype сообщает о передаче «родного* типа данных. Его значе-


ния берутся из перечислимого DataTypeEnum, объявленного в ADO. В
показанном выше XML возвращаемое значение и параметр EmployeelD
имеют тип данных adlnteger (3 -- это значение константы adlnteger, опре-
деленное в ADO). Полный список констант, входящих в это перечислимое,
слишком длинный, чтобы приводить его в статье; более подробную инфор-
мацию о DataTypeEnum см. в MSDN Online Library.

Тестирование хранимых процедур


Мы написали тестовое приложение, позволяющее экспериментировать с
компонентами доступа к данным и хранимыми процедурами, поддержива-
Вызов хранимых процедур и получение их результатов через Web

ющими эти компоненты. На наш взгляд, это приложение крайне полезно,


когда нужно проверить корректность хранимой процедуры или визуали-
зировать XML, возвращаемый компонентом при вызове данной хранимой
процедуры. Кроме того, мы добавили в компонент доступа к данным спе-
циальный интерфейс, используемый только тестовым приложением. Этот
интерфейс позволяет детальнее изучать XML, с которым имеет дело тес-
тируемый компонент.

Чтобы запустить тестовое приложение, запустите файл spdatest.exe. Вне-


шний вид тестового приложения показан на рис. 7. Оно «жестко» запрог-
раммировано на использование источника данных Northwind_SQL. Что-
бы получить список хранимых процедур, предоставляемых этим источни-
ком данных, щелкните стрелку, указывающую вниз, в поле со списком
Stored Procedure Name. Тестовое приложение выполняет запрос к источ-
нику данных и показывает имена хранимых процедур в раскрывающемся
списке. Заметьте, что перечисляются все хранимые процедуры базы дан-
ных, а не только поддерживающие нашу архитектуру. Поэтому будьте ос-
торожны и выбирайте только те хранимые процедуры, о которых извест-
но, что они удовлетворяют требованиям нашей архитектуры. К таким про-
цедурам относятся:

• Customer ListCustomers;

XML Store-d Procedure Date Access TesiITnhty Kelp Pfl

i Click til* "Lift Psraraftters" iroiuniaiid button The


ListViewvril bs populated with the parameter; for the stcs
procedures

3 For each required inptit parameter

За Doabls cbck on ths parameter плие ^ fh;


ListVisw.

3b Snter a value in the Value тейЪох

3-: Cbek die Update- conanand bjttwi :o store ths

Рис. 7. Тестовое приложение


Microsoft SQL Server

• Einployee_GetDetail;
• Employee_GetIDFromOrder;
• Order_CreateOrder;
• Order_GetDetail;
• Order_Updat:eOrder;
• Product_ListProducts;
• Valid Employee.

Рассмотрим тестовое приложение на примере хранимой процедуры Emp-


loyee_GetDetail.

После выбора или ввода имени хранимой процедуры в поле со списком


щелкните кнопку List Parameters, чтобы получить список параметров хра-
нимой процедуры. Тестовое приложение запрашивает эту информацию у
источника данных (см, дополнительную информацию на врезке «Про-
граммное определение параметров хранимой процедуры»). Результаты
запроса показываются в левой части окна тестового приложения (рис. 8).

«Crdsrs;.
^;"."7inr>
s.Ordi>|-iD dl -l=11t.i">11077c/Ofd£-l'I!J>

Jt:iit=!'itrifta">Rattiesnake
Canyon Grocery c/CompanyName?

Рис. 8. Возвращаемый XML

На рис. 7 видно, что хранимая процедура Employee_GetDetail возвраща-


ет значение и принимает один входной параметр Employee! D. Перед ее
Вызов кранипяых процедур и получение их результатов через Web 233

вызовом вы должны присвоить значение этому параметру. Для этого дваж-


ды щелкните имя параметра, введите требуемое значение и щелкните
кнопку Update. Затем щелкните кнопку Call Stored Procedure для вызова
хранимой процедуры. Тестовое приложение по введенным вами данным
создаст XML-строку и передаст ее методу GetDataXML компонента дос-
тупа к данным. Компонент проанализирует строку, вызовет соответствую-
щую хранимую процедуру и сформирует возвращаемую XML-строку, в
которой будут закодированы результаты. Эта XML-строка показывается
тестовым приложением (рис. 8).

Обратите внимание, что помимо вкладки Help в окне тестового приложе-


ния есть и другие вкладки. На этих вкладках показываются разные виды
XML, используемые и генерируемые компонентом доступа к данным.

• На вкладке Parameter List XML отображаются внутренние XML-дан-


ные, которые компонент доступа к данным передает адаптеру доступа
к данным после задания имени хранимой процедуры. Эти данные со-
держат описание параметров хранимой процедуры и используются
адаптером при заполнении набора cParameters. (Вкладка Parameter List
XML становится доступной после щелчка кнопки List Parameters.)
• На вкладке Call SP XML выводятся внутренние XML-данные, переда-
ваемые адаптером доступа к данным компоненту доступа к данным
при вызове хранимой процедуры SQL Server. Это XML-сообщение
содержит значения параметров, передаваемые вызываемой хранимой
процедуре.
• На вкладке Raw Return XML отображаются XML-данные, возвращаемые
компонентом доступа к данным адаптеру после выполнения хранимой
процедуры. В этих необработанных XML-данных содержатся либо дан-
ные набора записей, либо выходные параметры хранимой процедуры.
• На вкладке Return XML показывается окончательный набор XML-
данных, возвращаемый коду, вызвавшему адаптер. Эти XML-данные
являются возвращаемым значением метода CallSP класса cAdapter. Тэг
<NoRecordsReturned/> означает, что набор записей не запрашивался, —
это типично для хранимых процедур, обновляющих базу данных.

Программное определение
параметров хранимой процедуры
Один из способов передачи параметров хранимой процедуре — вручную
сформировать набор ADO-параметров. Пишется код примерно такого вида:

' Предполагается, что cmd уже объявлена оператором Dim


' как ADOOB.Command и ей присвоено соответствующее значение
' оператором Set. Также предполагается, что ргт уже объявлена
234 Microsoft SQL Server

' оператором Dim как ADODB.Parameter


Set prm = cmd.CreateParametersC'faParaTieter", adlnteger, adParamlnput)
cmd.Parameters.Append prm
Set prm = cmd.CreateParameter("@P<iraffleter2", adVarChar, _
AdParamlnput, 40)
cmd.Parameters.Append prm
cmd.Execute

Однако, если вы работаете с ба:юй данных SQL Server 7.0, то можете ини-
циализировать объект Command и вызвать метод Refresh его набора Para-
meters, Тогда в набор Parameters загрузятся описания параметров из базы
данных. После этого вы можете обращаться к параметрам, используя име-
на параметров в качестве индексов. При таком подходе пишется пример-
но такой код:
' Предполагается, что cmd уже объявлена оператором Dim
' как ADODB.Command и ей присвоено соответствующее значение
' оператором Set
With cmd
.Parameters.Refresh
With .Parameters
,Item("@Parameter").Value = 10
.Item("saParameter2"}.Value = "Some Text"
End With
.Execute
End With

Второй подход дает ряд преимуществ. Во-первых, если кто-то решит изме-
нить имя параметра в базе данных, вы сможете точно узнать, какой пара-
метр изменился, так как при попытке обращения к этому параметру воз-
никнет ошибка. При использонании первого способа ошибки не будет,
пока вы не выполните команду. Кроме того, второй подход позволяет пе-
ребирать объекты Parameter, запрашивать их свойства и действовать в со-
ответствии с полученными значениями свойств. Мы применили этот под-
ход при формировании XML-представления параметров, передаваемого
адаптеру доступа к данным.

Развертывание компонентов
Развертывание лучше выполнять в указанном нами порядке. Все ссылки
на имена папок даются по отношению к корневому каталогу примеров
кода (каталогу, где вы сохранили этот код).

Для развертывания хранимых процедур SQL Server выполните следую-


щие действия,

1. Запустите SQL Query Analyzer и войдите на сервер, на котором вы хо-


тите создать хранимые процедуры. (Это должен быть SQL Server с де-
лгонстрационной базой данных Northwind, поставляемой с SQL Server.)
Вызов хранимых процедур и получение их результатов через Web 235

2. В поле со списком смените имя базы данных на Northwind.


3. Откройте файл SQL_XML.sql, находящийся в папке Database.
4. Щелкните кнопку запуска на панели инструментов или нажмите кла-
вишу F5, чтобы выполнить в базе данных сценарий, создающий храни-
мые процедуры.

Заметьте: сценарий изменит имя базы данных на Northwind, но перед вы-


полнением сценария всегда лучше заранее перейти на нужную базу дан-
ных. Сценарий удалит существующие хранимые процедуры, имена кото-
рых совпадают с именами создаваемых. Мы полагаем, что это не вызовет
проблем, поскольку не так уж много людей создает хранимые процедуры
в базе данных Northwind. Однако, если вы пишете хранимые процедуры,
размещаемые в этой базе данных, то, чтобы не потерять результаты своего
труда, убедитесь, что у вас нет процедур с совпадающими именами.

Развертывание компонента доступа к данным


Компонент доступа к данным (spda.dll) использует «зашитое» в код DSN-
имя при открытии ADO-объекта Connection, соединяющего с сервером баз
данных. Вам придется создать DSN Northwind_SQL на компьютере, где
будет работать компонент доступа к данным. Используйте аутентифика-
цию Integrated Security* и выберите Northwind в качестве базы данных по
умолчанию.

Если вы не очень хорошо знакомы с созданием DSN, см. SQL Server Books
Online.

Развертывание в средах с поддержкой СОМ+


и без такой поддержки
Скомпилированный компонент в составе скачиваемого к этой статье кода
предназначен для развертывания в среде, не поддерживающей СОМ+.
Однако он написан так, что для развертывания компонента в среде СОМ+
требуется лишь перекомпилировать проект, изменив аргумент условной
компиляции и раскомментировав кое-какой код. Чуть позже мы расска-
жем о развертывании компонента в среде СОМ+. А если вы не собираетесь
использовать компонент в среде СОМ+, выполните следующие действия.

1. Перейдите в папку DataAccess.


2. Зарегистрируйте находящийся в ней файл spda.dll с помощью Reg-
Svr32.exe. Более подробную информацию о регистрации СОМ-компо-
нентов и DLL см. в «Registering COM Applications» по ссылке http://
msdn.microsoft.com/ library/psdk/com/registry_32er.htrn.

Видимо, имеется в виду аутентификация средствами Windows NT. — Ярш/. сост.


236 Microsoft SQL Server

Чтобы развернуть DLL доступа к данным в среде СОМ+, сделайте вот что.

1. Откройте в Visual Basic проект DataAccess.vbp.


2. Откройте класс cAccessor и раскомментируйте строку кода, содержа-
щую текст «Implements COMSVCSLib.ObjectControl», и убедитесь, что
в проекте есть ссылка на СОМ+ Services Type Library.
3. Откройте Project Properties, перейдите на вкладку Make и измените
аргумент условной компиляции с «Compile__MTS = 0» на «Согп-
pile_MTS = ~l».
4. Перекомпилируйте проект и поместите полученную DLL в пакет СОМ+.

Заметьте: вы должны экспортировать пакет, содержащий эту DLL, в пакет


MSI Installer, чтобы другие клиентские компьютеры могли получить ссыл-
ку на данный пакет. Подробную информацию об экспорте пакетов см. в
Platform SDK (раздел «Component Services Help»).

Кроме того, вы можете запускать этот компонент в среде Microsoft Tran-


saction Server (MTS) в Windows NT 4.0. Для этого установите ссылку на
«MTS object control library» и измените оператор Implements так, чтобы он
указывал на библиотеку MTS вместо библиотеки сервисов СОМ+.

Развертывание адаптера доступа к данным


Адаптер доступа к данным (spdaad.dll) нужно развернуть на компьютере,
где устанавливаются сервисы бизнес-уровня, а также на любом компьюте-
ре, на котором запускается тестовое приложение.

1. Перейдите в папку Data Access Adapter.


2. Зарегистрируйте библиотеку spdaad.dll, находящуюся в этой папке, с
помощью RegSvr32.exe. Если DLL развертывается не на компьютере,
где находится компонент доступа к данным, вам придется взаимодей-
ствовать с DLL, обеспечивающей доступ к данным, через DCOM. Если
DLL доступа к данным установлена в среде СОМ+, то, чтобы к этой
DLL можно было обращаться, ее нужно экспортировать в пакет MSI
Installer и запустить пакет на клиентском компьютере.

Развертывание компонентов промежуточного


и презентационного уровней
На промежуточном уровне в нашем проекте находятся бизнес-сервисы.
Для развертывания spdamid.dll выполните следующие действия.

1. Перейдите в папку MiddleTier.


2. Зарегистрируйте spdamid.dll, находящуюся в этой папке, с помощью
RegSvr32.exe.
Вызов хранимых процедур и получение их результатов через Web 23?

На компьютере, где выполняется Web-сервер, нужно развернуть компо-


нент COMASP презентационного уровня (spdaui.dll). Обратите внимание,
что папку XSL и все ее содержимое следует поместить в ту же папку, в
которой развернута и зарегистрирована COMASP DLL.

1. Перейдите в папку COMASP.


2. Зарегистрируйте файл spdaui.dll, находящийся в этой папке, с помо-
щью RegSvr32.exe.

Развертывание ASP-страниц
Для развертывания ASP-страниц выполните следующее.

1. Скопируйте папку ASP вместе со всем содержимым в папку, из кото-


рой IIS загружает ASP-страницы. Если хотите, можете не копировать
папку. Просто сделайте так, чтобы виртуальный каталог (создаваемый
на следующем этапе) указывал на папку ASP.
2. Создайте на IIS-сервере новый виртуальный каталог, из которого бу-
дут загружаться ASP-страницы. Настройте этот виртуальный каталог
• так, чтобы он указывал на папку ASP.
3. Откройте диалоговое окно Properties только что созданного виртуаль-
ного каталога и выберите вкладку Directory Security. Щелкните кноп-
ку Edit в секции Anonymous Access and Authentication Control. В по-
явившемся диалоговом окне щелкните кнопку Edit в секции Anony-
mous Access Frame. Выберите себя или какого-то другого пользователя,
у которого есть разрешение на запуск хранимых процедур базы данных
Northwind. Введите пароль и сбросьте флажок Allow IIS to Control
Password. Щелкните OK во всех диалоговых окнах, чтобы сохранить
изменения. Эта операция обязательна, так как IIS по умолчанию ис-
пользует удостоверения защиты пользователя Anonymous. Поскольку
этот пользователь обычно не имеет разрешения на выполнение храни-
мых процедур базы данных, мы просто указываем, что вместо учетной
записи анонимного пользователя IIS должен использовать учетную
запись того, у кого такое разрешение есть.

Развертывание тестового приложения


Запустите тестовое приложение (spdatest.exe) на компьютере, где установ-
лен адаптер доступа к данным.
При разработке решения, о котором мы рассказывали в статье, использо-
вался редактор XMLwriter (http://www.xmlwriter.com). Список доступных
XML/XSL-редакторов можно посмотреть на сайте XML.com издательства
O'Reilly (http://www.xnil.eom/piib/pt/r/874). Учтите, что некоторые ре-
дакторы предназначены специально для преобразований XML в HTML,
238 Microsoft SQL Server

но не очень хорошо справляются с преобразованиями «XML-XML». Вы-


бирайте редактор, отвечающий вашим потребностям,

Заключение
Разработанная нами методология оказалась вполне удачной. На написа-
ние универсальных компонентов потребовалось около недели. После это-
го передача информации между клиентской частью и базой данных пре-
вратилась в обмен соответствующими XML-данными. Это упростило пе-
редачу информации между компьютерами, так как все данные передаются
по значению, а не по ссылке на объект. Представление данных в XML-
формате облегчило разработку клиентского интерфейса: чтобы изменить
представление данных на экране, нужно лишь изменить XSL-преобразова-
ние, применяемое к XML.

Дэйв Грундгейгер (Dave Grundgeiger) — консультант в Тага Software Inc.


(http://www.tarasoftware.com) в Мэдисоне (штат Висконсин), где он и живет,
погруженный в технологии Microsoft. Автор книги «CDO & MAPI Programming
with Visual Basic» (O'Reilly, 2000). Работает над книгой по Visual Basic .NET,
которая выйдет в издательстве O'Reilly.
Энсон Голдэйд {Anson Goldade) — независимый консультант. Сейчас вовсю
осваивает С#.
Вэрон Фугман (Varon Fugman) — консультант в Beacon Technologies Inc. в
Мэдисоне (штат Висконсин). Как и его коллеги, Дэйв и Энсон, специализиру-
ется на многоуровневых бизнес-решениях для вертикальных рынков,
разрабатываемых с применением Visual Basic, IIS, MTS/COM+ и SQL Server.
Марк Браун и Дэвид Менье

Компактные и надежные
приложения на основе SQL
Server CE 2.0 и .NET Compact
Framework*

Разработчикам мобильных приложений предлагается целое море возможнос-


тей. Однако создать компактное и в то же время надежное локальное
хранилище данных очень нелегко. Эту проблему обещает снять SQL Server CE
2.0. С точки зрения функциональности и производительности, новая версия
SQL Server CE — настоящий прорыв по сравнению со своей предшественни-
цей. Авторы рассматривают некоторые из современных платформ и инстру-
ментов и сравнивают возможности SQL Server CE версии 2.0 с предыдущей,
особо выделяя наиболее важную новую функциональность. Статья заверша-
ется созданием приложения-примера.

Взрывное развитие мобильных устройств в последние годы вызвало к


жизни новое поколение мобильных приложений, предназначенных как
для потребителей, так и для организаций. Эта тенденция не осталась не-
замеченной Microsoft, создавшей целый ряд инструментов и инфраструк-
тур, позволяющих разработчикам в полной мере использовать возможно-
сти интеллектуальных устройств. Новые инструментальные наборы вку-
пе с широкими возможностями этих устройств подготовили почву для
создания самых передовых мобильных приложений.

Microsoft SQL 2000 Windows CE Edition (также известный как SQL Server
СЕ 2.0) является дальнейшим развитием версии Microsoft SQL Server,

Публиковалось Б MSDN Magazine/Русская Редакция. 2003. №1 (январь). — Прим. изд.


240 Microsoft SQL Server

предназначенной для устройств под управлением Windows CE. SQL Server


СЕ 2.0 предоставляет всю важнейшую функциональность для разработки
отказоустойчивых приложений, работающих с базами данных в среде
Windows СЕ или Windows CE .NET; хранилище данных, оптимизирующий
процессор запросов, поддержку различных видов подключений и т. д. Но-
вая редакция этой СУБД совместима с SQL Server 2000. Невероятно, но
несмотря на все эти возможности SQL Server CE 2.0 занимает чрезвычай-
но малый объем памяти.

В новой версии появился ряд важных функций и заметно повышена про-


изводительность по сравнению с предшествующей версией. Если вы уже
работали с SQL Server CE, то непременно заметите, что новая версия ра-
ботает быстрее, а ее модель программирования расширена для поддержки
.NET Compact Framework.

Мы познакомим вас с некоторыми из ключевых функций, появившихся в


новой версии SQL Server CE, и рассмотрим ряд фрагментов кода, иллюс-
трирующих эти функции. Однако всестороннее освещение SQL Server CE
не является задачей нашей статьи — она лишь должна подтолкнуть вете-
ранов и новичков разработки для мобильных устройств к созданию мо-
бильных приложений на основе новой редакции SQL Server CE.

Разработка мобильных решений


Длинный список Windows-устройств, инструментов разработки и инфра-
структур угнетающе действует на новичков в создании мобильных прило-
жений. SQL Server CE 2.0 — лишь один из инструментов в арсенале раз-
работчика ПО для мобильных устройств, Очень трудно, если вообще воз-
можно, порекомендовать единственно верную комбинацию инструментов
и устройств, идеально подходящую для создания некоего мобильного ре-
шения, но есть ряд факторов, которые следует обязательно учитывать.

Современные разработчики могут ориентироваться на различные типы


устройств, работающих под управлением Windows CE. Достаточно давно
известны такие платформы, как Pocket PC (РРС) и Handheld PC (HPC).
Большинство из них оснащается модулями для беспроводной связи по
протоколам Bluetooth или 802.11Ь, что делает эти устройства весьма фун-
кциональными и способными обеспечить постоянное сетевое соединение.
Устройства под управлением Windows CE З.„г обычно рассчитаны на ко-
нечных потребителей. Но в организациях начинают понимать, что подоб-
ные устройства существенно повышают производительность труда не
только их клиентов, но и сотрудников.

Совсем недавно производители стали встраивать технологии беспроводно-


го доступа в сами устройства. Прошли те времена, когда приходилось че-
Компактные и надежные приложения иа основе SQL Server CE 2.0 241

рез громоздкие адаптеры PC Card подключать платы беспроводной связи.


Также наблюдается тенденция к объединению сотовых телефонов с уст-
ройствами Pocket PC, в результате чего на свет появляются весьма инте-
ресные гибриды.

Встраиваемые устройства с Windows СЕ проникают и в заводские цеха.


Эти устройства стали прочнее и выживают даже после падения с высоты
до двух метров, что существенно расширяет их область применения.

Любопытно, что большинство встраиваемых OEM-устройств оснащаются


более новыми версиями Windows СЕ, чем устройства, ориентированные на
конечного потребителя. Новейшие OEM-устройства уже работают под
управлением Windows СЕ 4,х. Кроме того, на этих устройствах .NET
Compact Framework имеется по умолчанию, что делает такую платформу
привлекательной для разработчиков, специализирующихся на создании
корпоративного ПО и желающих использовать все преимущества предла-
гаемых Microsoft инструментов и инфраструктур следующего поколения,

Windows СЕ также комбинируется с самыми разнообразными аппаратны-


ми технологиями, в том числе с устройствами для считывания карт памя-
ти Compact Flash (CF), сканерами штрих-кодов и отпечатков пальцев,
приемниками Global Positioning System (GPS), а также с цифровыми ка-
мерами. Некоторые из типичных мобильных решений, создаваемых с по-
мощью современных инструментов и устройств на основе Windows СЕ,
перечислены в табл. 1. Очевидно, что эти мобильные приложения требу-
ют компактного и в то же время надежного решения для создания локаль-
ного хранилища данных.

Табл. 1. Примеры мобильных приложений

Сфера применения Предназначение

Автоматизация отдела Обеспечение доступа вне офиса к спецификациям


продаж {Sales Force товаров и информации о клиентах
Automation, SFA)
Грузоперевозки Регистрация времени, адреса доставки и объема груза,
(логистика) а также подписание квитанций о доставке
Недвижимость Службы Multiple Listing System (MLS)
Медицина Ведение карт пациентов (выявленные аллергены,
результаты исследований, текущие назначения)
Консалтинг Учет затрат времени, расходов и комиссионных,
выписка счетов и планирование ресурсов

Главная проблема, стоящая перед разработчиками, — сравнительно малый


объем памяти у мобильных устройств (как правило, 16-32 Мб). С ростом
242 Microsoft SQL Server

объема памяти мобильных устройств возрастет и сложность мобильных


приложений. Естественно, механизм локального хранилища данных не
должен занимать много памяти. Кроме того, вашему приложению навер-
няка понадобится выполнение сложных запросов к локальному хранили-
щу данных.

Такие приложения должны одинаково хорошо работать как в подключен-


ном, так и в автономном режимах. Эти требования обуславливают необхо-
димость в API-функциях для локального и удаленного доступа к данным,
равно как и в механизме синхронизации с серверной базой данных. К сча-
стью, предоставляемая SQL Server CE 2.0 инфраструктура позволяет со-
здавать приложения, удовлетворяющие всем этим требованиям.

В автономном режиме SQL Server СЕ обеспечивает хранилище данных на


локальном устройстве. Она позволяет надежно отслеживать все измене-
ния в базе данных на устройстве и после восстановления подключения
синхронизировать их с серверной базой данных. То же верно и в отноше-
нии изменений на серверной стороне. SQL Server СЕ поддерживает фун-
кцию Merge Replication (репликация со слиянием) для двусторонней син-
хронизации изменений во всех базах данных (при использовании в соче-
тании с SQL Server 2000).

Инструменты для разработки мобильных приложений


SQL Server CE 2.0 позволяет разработчикам использовать как текущие
инструменты, так и инструменты следующего поколения. Для тех, кто
предпочитает создавать мобильные приложения с помощью eMbedded
Visual Tools 3.0, в SQL Server CE 2.0 сохранена поддержка этого инстру-
ментального набора, Так что вы можете по-прежнему писать мобильные
приложения как на eMbedded Visual Basic, так и на eMbedded Visual C++.
И если ваш выбор — eMbedded Visual Tools, знайте, что SQL Server CE 2.0
требует наличия на устройстве Windows CE версии 3.x или выше.

Инструменты и инфраструктуры следующего поколения от Microsoft под-


держивают технологию быстрой разработки мобильных приложений. Пе-
реход к разработке на платформе Windows .NET позволит мобильным
приложениям использовать многие инновации, доступные настольным
системам. Исходный код приложений, написанный на eMbedded Visual
Basic или Visual C++, можно перевести на использование Visual Basic
.NET, C# и ADO.NET.

Сравнительно недавно Microsoft представила технологию Smart Device


Extensions (SDE), позволяющую использовать при разработке мобильных
и настольных приложений унифицированный набор инструментов Visual
Компактные и надежные приложения на основе SQL Server CE 2.0 243

Studio .NET. За счет бесшовной интеграции Visual Studio .NET и SDE об-
разуют платформу для разработки приложений Windows СЕ.

В следующую версию Visual Studio .NET (под кодовым названием «Eve-


rett») будет встроена поддержка .NET Compact Framework, Pocket PC 200дг
и устройств с Windows CE .NET k.x.

Языковые средства SQL Server CE 2.0


В SQL Server CE 2.0 есть свой провайдер OLE DB. В отличие от аналогично-
го провайдера из SQL Server 2000 он предоставляет всего одно свойство -
DBPROP_INIT_DATASOURCE — для задания полного пути к файлу от-
крываемой базы данных. Например, чтобы открыть файл \Windows\Nor-
thwind.sdf, надо передать при вызове IDBProperties::SetProperties строку
\Windows\Northwind.sdf через свойство DBPROP_INIT_DATASOURCE.

Кроме того, SQL Server CE поддерживает механизм управления доступом


на уровне файлов — пароли баз данных. При каждом открытии какой-либо
базы данных вы должны передавать ее пароль через свойство DBPROP_
SSCE_DBPASSWORD из набора свойств DBPROPSET_SSCE_DBINIT,
специфичного для провайдера. А при создании новой базы данных это
свойство позволяет задать пароль для нее.

Теперь поддерживаются и внутренние функции, которые знают и любят


пользователи SQL Server 2000. Для выполнения операций и возврата ска-
лярных значений в запросах можно вызывать математические, строковые
и системные функции. Ранее для этого приходилось использовать функ-
ции eMbedded Visual Basic (или eMbedded Visual C++). Применение внут-
ренних функций дает потрясающий прирост производительности по срав-
нению с SQL Server CE 1.0.

Пример на рис. 1 демонстрирует, как преобразовать поле в char(50) для


облегчения форматирования результатов запроса, которыми заполняется
список элемента управления ListBox. В SQL Server CE конструкция UNI-
ON в операторе SELECT создает из результатов двух и более запросов
единый набор результатов — множество строк, возвращаемых всеми зап-
росами в объединении. Следующий фрагмент кода показывает, как ис-
пользовать новую конструкцию UNION в операторе SELECT:

Dim strSQL As String =


"SELECT * FROM TableA UNION SELECT * FROM TableB"

В SQL Server CE 2.0 добавлена поддержка параметризованных запросов


через ADO.NET. Эта функция доступна и разработчикам, использующим
eMbedded Visual Tools 4л; с провайдером OLE DBCE. Кто знаком с пара-
метризованными запросами в SQL Server 2000, сразу заметит некоторые
отличия. Во-первых, поскольку в SQL Server СЕ нет встроенного механиз-
ма для хранения запросов, параметры в действительности являются сим-
волами подстановки («?») для значений, предоставляемых в период вы-
полнения. Во-вторых, в SQL Server СЕ нет поддержки именованных пара-
метров. Следующий код демонстрирует новинку SQL Server СЕ 2.0 -
поддержку параметризованных запросов:

Dim strSQL As String = _


"INSERT INTO TableA (coll, co!2) VALUES (?, ?)";

Рие. 1. Поддержка внутренних;функций


Dim strSQL As String « "SELECT CONV£RT(char(5Q), title), .
td.sales FRQH titles;"
Dim ceCn As New SqlCeConnection("data source\ssceSample.SDF")
1
Создать экземпляр класса чтения данных для заполнения списка
Diffl ceDr As SqlCsOatafteader

Try
:
ceCn.QpenO
Catch a As SqlCeException
MsgBox(a.ToStrinaO)
End Try

Try
Dim ceCiad As New System.Data.SqIServerCe.SqlCeComraand^strSQl, ceCn)
' Создать экземпляр ListBox
Dim listfloxl As Hew ListBoxO

With listBoxl
' Задать размеры и позицию списка
.Size = New System.CeDrawing.Size(20B, 160)
.Location = New System.CeDrawing.Point(8, 64)

Добавить список на форму


fle.Controls.Add(HstBox1}
ceDr « ceCmd.ExecijteReaderO
№lle ceDr.ReadO
. Items.Add(ceDr.GetString(0»
End While
End With
Catch a As SqlCeExceptlQfi
MsgBox(a.ToStringO)
Catch a As Exception
MsgBox{a.ToString()J
End Try

ceOr.CloseO

см. след. стр.


Компактные и надежные приложения на основе SQL Server CE 2.0 245

Рис. 1. Поддержка внутренних функций (окончание)


Try
ceCn.Glose<)
Catch a As SqlCeException
Msg8ox(a,ToString())
End Try

Еще одна новая функция — извлечение индекса (index pull). Приложения


вызывают метод Pull для извлечения информации из базы данных SQL
Server и сохранения ее в таблице базы данных SQL Server СЕ. В первой
версии SQL Server СЕ можно извлечь из таблицы лишь значения первич-
ного ключа, а в SQL Server CE 2.0 — и все дополнительные индексы.

Применение в приложениях этой функции с соответствующими парамет-


рами позволяет отслеживать изменения в таблице SQL Server CE, напри-
мер все операции вставки, обновления и удаления (рис. 2).

Рис. 2. Применение метода Pull


1
Создать объект RDA
Dint ceflda As New RemoteDataAccessO
Din* strSQL = "SELECT * FROM Customers;"
bin* strftemoteConnect = "provider=sqloiedb;data source=" &_
strDataSource & _
"; Initial Catalog=Uorthwirtd;user idssa;password=sa"

Try
ceRda.InterrretUrl = strlnternetURL.
,ceRda.LocalConnectionString = strLocalConnect

' Отслеживать таблицу FOO


ce8da.Pull("FOQ", strSQL, strRemoteConnect, _
Data.SqlServerCe.RdaTrackOption.TrackingGrO
Catch a As SqlCeExcept!on
MsgBox<a,ToString(})
End Try

Следующее усовершенствование в SQL Server CE коснулось обработки


ошибок, которая более надежна, чем в предыдущей версии. Как видно из
уже показанных фрагментов кода, приложения, написанные на Visual
Basic .NET и С# с применением .NET Compact Framework, могут исполь-
зовать встроенную поддержку блоков try/catch. Если вы писали приложе-
ния для настольных систем в Visual Studio .NET, то скорее всего знакомы
с подобным стилем обработки исключений. Эту новинку по достоинству
оценят разработчики, пишущие на eMbedded Visual Basic, до сих пор вы-
246 Microsoft SQL Server

нужденные обрабатывать ошибки в стиле «On Error GoTo». В дополнение


к блокам try/catch в SQL Server СЕ добавлен новый класс SqlCeException,
позволяющий перехватывать и корректно обрабатывать встроенные типы
исключений.

В отличие от SQL Server CE 1.0 в новом выпуске разработчикам доступ-


ны не только коды, но и развернутые описания ошибок. Механизм описа-
ния ошибок реализован как необязательная DLL. При отладке эту DLL
можно хранить на устройстве в каталоге приложения SQL Server СЕ, а по
завершении отладки — удалить ее.

Серверные средства
В SQL Server CE 2.0 есть новый мастер Connectivity Setup Wizard, облег-
чающий создание виртуальных каталогов и разрешений на доступ к дан-
ным (рис. 3). Пользователи предыдущих версий SQL Server CE знают, что
для взаимодействия SQL Server с устройствами, работающими под управ-
лением SQL Server CE, нужны определенные виртуальные каталоги и пра-
ва доступа. Новый мастер позволяет создавать на компьютере, где установ-
лен Microsoft Internet Information Services (IIS), виртуальные каталоги и
управлять ими, Кроме того, при помощи этого мастера можно конфигури-
ровать и настраивать разрешения NTFS на компьютере с IIS и SQL Server.

4* Microsoft !ад £епя?г CE Connectivity Wizard - ft<rasote RacrtVrtfcrwraft SOI- Server Ctterceweb^J и|0|Щ]

^j Fie Aden yew Favorihw Window Це1р .!i,i*!Kl|

*• -* И ЕВ Lf
_J ^..tcde Root HTTp Car!en
,Fo|(ja HTTPAu!hfnlice!ion ! HTFS Pamisaons j
-; Щ Microsoft SQL Sever CE
:|ПЩ| Thf! foldia should contain the Seiwi Agent [Ssceja20 d([

p?r^^R^™5iWs^^7^^^ - . Bl2Wie I

Sel the HTTP perrrmiHis Id" the conterU fokta Елеси1в p^imission is lequneti by SQL Server CE

P Bead

P Runicripl5[aJchasASP]

C7 ' f :- !: - '- '-!;.;," '. I "'A

Г W,it,

fi< Browse

Рис. 3. Connectivity Wizard


Компактные и надежные приложения на основе SQL Server CE 2.0 247

Как и в предыдущей версии SQL Server СЕ, при работе под управлением
IIS в новой версии для взаимодействия между Windows СЕ и SQL Server
2000 используется библиотека ISAPI DLL. Таким образом, если устрой-
ство имеет доступ к виртуальному каталогу SQL Server СЕ через HTTP
(т. е. возможен просмотр этого каталога через Pocket Internet Explorer),
оно может подключаться к серверной базе данных с использованием Re-
mote Data Access (RDA) или Merge Replication.

Как и в прежней версии SQL Server СЕ, локальную базу данных можно
защищать только паролем или паролем и 128-битным шифрованием. Если
локальная база данных защищена паролем, к ней нельзя получить доступ
программным способом, но это не мешает просмотру файла базы данных
как обычного текстового файла.

Используя защиту паролем вместе с шифрованием, вы не только блокиру-


ете программный доступ, но и шифруете данные. При использовании рас-
ширений Pocket PC 2002 по умолчанию устанавливается библиотека,
обеспечивающая более стойкую криптографическую защиту с применени-
ем алгоритма RSA со 128-битным ключом. Если же у вас имеется лишь
стандартная версия Pocket PC 2002, скачайте соответствующий пакет с
http://www.microsoft.com/rnobile/pocketpc/downloads.

В настоящее время Microsoft рекомендует использовать SSL в IIS и аутен-


тификацию Basic Authentication. В будущем Microsoft планирует добавить
в Windows CE .NET (версий 4.x и выше) поддержку Kerberos, что позво-
лит делегировать удостоверения защиты (credentials) между Windows
2000 Server (или .NET Server) и CE.NET-устройствами, Когда начнется
выпуск устройств, оснащенных Windows CE .NET 4.дг, в комплект постав-
ки SQL Server CE 2.0 будет включена документация на Kerberos.

Благодаря еще одному усовершенствованию SQL Server CE 2.0 теперь


поддерживает столько же индексов, сколько SQL Server 2000. Если первая
версия SQL Server CE поддерживала не более 32 индексов, то нынеш-
няя — до 249.

С SQL Server CE 2.0 также поставляется компонент SQL Server Client


Data Provider. Этот компонент, реализованный как управляемая оболоч-
ка, позволяет приложениям взаимодействовать непосредственно с сервер-
ной базой данных SQL Server. Его, видимо, лучше использовать только
при наличии постоянного подключения к сети, когда потребность в ло-
кальном хранилище данных отсутствует.

Кроме того, через SQL Server Client Data Provider можно выдавать запро-
сы к SQL Server для выборки данных с последующей записью их в локаль-
ную базу данных SQL Server CE.
248 Microsoft SQL Server

В SQL Server CE 2.0 также существенно улучшен ISQLW — эквивалент


Enterprise Manager из SQL Server (рис. 4). ISQLW автоматически устанав-
ливается в меню Start устройства при установке Visual Studio .NET SDE
или приложения eMbedded Visual Tools.

Рис. 4. ISQLW

В прежней версии ISQLW не было простого способа просмотра схемы


базы данных, поэтому писать запросы было трудно, если только не выу-
чить всю схему наизусть. Теперь объекты базы данных доступны для про-
смотра в виде дерева, почти как в Query Analyzer из SQL Server для на-
стольных систем.

SQL Server CE 2.0 поддерживает два основных способа подключения к


серверным базам данных SQL Server: через RDA или через Merge Replica-
tion. Доступ через RDA обеспечивает взаимодействие с SQL Server 7.0 (и
выше). Merge Replication требует наличия SQL Server 2000. Схема взаимо-
децртвия между базами данных SQL Server CE и SQL Server показана на
рис. 5.

Механизм RDA в SQL Server CE 2.0 позволяет приложению Windows CE


легко извлекать информацию из таблицы удаленной базы данных SQL
Server и сохранять ее в таблице локальной базы данных, после чего при-
ложение может читать и модифицировать эту таблицу. В дальнейшем оно
может инициировать передачу модифицированных записей из локальной
таблицы обратно в таблицу SQL Server.

Приложения Windows CE также могут передавать через RDA команды


SQL применительно к удаленной базе данных SQL Server, например, для
вставки, обновления или удаления строк.
Компактные м надежные приложения на основе SQL Server G£ 2.0

Клиент
Приложение

Т
SOL Server База данных
Client Agent SGI Server СЕ

Сервер
ИВА
IIS
SQL Server CE Hi
service Agent
out

Репликация
SQL Server Reconciler
(синхронизатор)

.out

Рис. 5. Взаимодействие между SQL Server и SQL Server CE

В SQL Server СЕ механизм RDA работает через Интернет. Иначе говоря,


SQL Server СЕ взаимодействует с серверной базой данных SQL Server
через IIS. Благодаря этому RDA доступны службы аутентификации и ав-
торизации IIS. Так как взаимодействие осуществляется по протоколу
HTTP, машина под управлением SQL Server может находиться за бранд-
мауэром, а доступ к ней может быть получен по правилу публикации
(publishing rule) через Microsoft ISA Server (или Microsoft Proxy Server).

Для уменьшения объема передаваемых данных RDA применяет сжатие,


что позволяет эффективно использовать RDA с беспроводными транспор-
тными протоколами. При желании конфиденциальные пользовательские
данные можно защищать шифрованием. RDA также поддерживает обра-
ботку коммуникационных ошибок: в случае неудачи данные передаются
повторно, начиная с последнего успешно переданного содержимого буфе-
ра сообщений.
250 Microsoft SQL Server

Элемент управления RDA предоставляет программный доступ к базе дан-


ных SQL Server 2000 или SQL Server 7,0. Доступ через RDA поддержива-
ется Visual Studio .NET и eMbedded Visual Tools.

В SQL Server СЕ функция Merge Replication основана на механизмах SQL


Server 2000 и использует ту же модель «издатель-подписчик» (Publisher/
Subscriber). Merge Replication позволяет независимо обновлять данные на
устройстве и на сервере, а позднее, при подключении устройства к серве-
ру, синхронизировать их. Чтобы создать подписку, вы должны сначала
настроить репликацию в SQL Server 2000, создать публикацию SQL Server
(в том числе общий каталог моментальных снимков для хранения опубли-
кованных данных) и разрешить анонимные подписки.

У объекта Replication в SQL Server СЕ имеются методы для добавления и


удаления подписки, а также для инициализации, вызова и завершения
Merge Replication применительно к конкретной подписке. Этот объект
SQL Server устроен сложнее, чем RDA, но его легко программировать при
правильной настройке.

Другое замечательное новшество SQL Server СЕ 2.0 — появление провай-


дера (или оболочки) SQL Server CE Managed Provider, предоставляюще-
го уровень взаимодействия с мобильными приложениями, написанными
на Visual Basic .NET и С# с использованием .NET Compact Framework.
Если вам доводилось работать с ADO.NET в настольных приложениях, то
и SQL Server CE Managed Provider в сочетании с .NET Compact Frame-
vvork не доставит вам неудобств. Эта модель программирования предостав-
ляет интуитивно понятный способ взаимодействия с базой данных SQL
Server CE.

В действительности эта оболочка использует родные библиотеки SQL


Server CE. Изящество такого решения в том, что оно позволяет приложе-
нию, написанному на eMbedded Visual Basic (или eMbedded Visual C++),
мирно сосуществовать с приложением, созданном средствами Visual Stu-
dio .NET. Однако это не подразумевает параллельного доступа к одной и
той же базе данных SQL Server CE: просто приложения, рассчитанные на
эти платформы, могут сосуществовать на одном устройстве. SQL Server
СЕ — по-прежнему однопользовательская СУБД.

Для поддержки управляемого кода, использующего .NET Compact Frame-


work, в SQL Server CE 2.0 введено два новых пространства имен; System.
Data.SqlClient и System.Data.SqlServerCe. Появился и класс System.Da-
ta.SqlServerCE.SqlCeException для поддержки блоков try /catch в языках
Visual Basic .NET и С#.
Компактные и надежные приложения на основе SQL Server CE 2.0

Представьте, какую мощь дает применение в приложениях новой оболоч-


ки управляемого кода, использующей Merge Replication, если вызовом
единственного метода Synchronize можно полностью скопировать на уст-
ройство схему базы данных и ее информацию, не написав ни строчки кода
на языке описания данных (DDL).

Пишем приложение
А теперь проиллюстрируем новые функции SQL Server CE 2.0 на приме-
ре реального кода. Для этого напишем на С# простое приложение Smart-
Device, используя Visual Studio .NET и .NET Compact Framework. Чтобы
упростить разработку и тестирование этого приложения, возьмем эмуля-
тор Pocket PC 2002.

Для нашего примера мы задействуем базу данных, устанавливаемую при


установке приложения-примера NorthwindCE из .NET Compact Frame-
work SDK. Если вы — ветеран программирования для SQL Server, то на-
верняка знакомы с базой данных Northwind из SQL Server 2000. В Nor-
thwindCE есть сценарий установки SetupRepl.bat, создающий копию базы
данных Nwind_SQLCE. Этот сценарий также устанавливает на компьютер
с SQL Server 2000 компоненты Publisher и локальный Distributor. Далее он
создает на основе базы данных Nwind_SQLCE публикацию SQLCERepl-
DemoNet для репликации со слиянием. Мы сделаем наше приложение
подписчиком этой публикации.

~гв SQL Server Errterfmse Manager - [Cens&te RootUJicrosoft SQL SwversVSQL Serve... |- ItljfXj
"|э Fife Action $ei4 Look ^indo-rt help -...jeJKJ-
<&=•••, (t] щ gf* Ц| щ, rj§ 4; f s S^ Q 33 ^
Is О Console Root 5QLCERep(DemoNet N wind_5QL^ 8 Items
Hi ^fj Microsoft SQL Servers gents__ ^I^KL_J
'•— jjjj£ S(3_ Server Group
~. i$3 MBLAPOl (Windows NT)
•?i)MBPPC01:-7 Anonymous
!*. \2\ Databases
^SQLCE5Lib*l:-l Anonymous
:*' О Data Transformation Services
•^)SQLCE5ub#J:-2 Anonymous
+ Sj.1 Management
^)SQLCESub#1;-3 Anonymous
'rl CiJ Replication
E
£ 4-^ Publications §)SQLCE5ub#lH Anonymous
^ SQLCEReplDernoNet:Nwind_SQLCE €)SQLCESub#l:-5 Anonymous
••JiB Subscript iori5 €j|sQLCESUb#l;-6 Anonymous
-; Ц Replication Monitor
'r."-i^ Publishers
-i ^ MBLAP01

t. -.^J Agents
О Replication Alerts
± Cil Security
* ;£j Support Services
'i1 jii] Meta Data Services
<: ; Ш

Рис. 6. Новая публикация


252 Microsoft SQL Server

Сконфигурируйте свой экземпляр SQL Server так, чтобы в репликации он


играл роль Publisher/Distributor, и создайте каталог моментальных сним-
ков с соответствующими разрешениями, а затем запустите сценарий Se-
tupRepl.bat из программы-примера NorthwindCE. Если все пройдет удач-
но, вы увидите новую публикацию (рис. 6).

Обзор приложения
Разработанное нами приложение иллюстрирует некоторые из обсуждав-
шихся ранее новых возможностей SQL Server CE 2.0, в том числе репли-
кацию через метод SqlCeReplication.Synchronize. Мы также продемонстри-
руем полную (двухстороннюю) синхронизацию базы данных Nwind_SQL-
СЕ (хранящуюся на компьютере с SQL Server 2000) с локальной базой
данных подписок SQL Server СЕ и ряд других средств, например обьект
SqlCeException и параметризованные запросы,

Windows-форма приложения SmartDevice показана на рис. 7. Приложение


позволяет запрашивать таблицу Employees локальной базы данных подпи-
сок, вводя часть значения поля LastName как критерий поиска. Результа-
ты запроса возвращаются через элемент управления List View. В меню при-
ложения добавлена команда Synchronize, которая заставляет реплициро-
вать все изменения в базе данных SQL Server 2000 в нашу локальную базу
данных SQL Server СЕ. Наверное, SmartDevice — не самое потрясающее
приложение, зато его реализация весьма интересна!

PiSQl-CE Z.GReplDenm lVjfH||fj


: Last Name: ::::::•:••:-:::•:••••••:•••;:••:

:::::::.•!•:: " : • • : 1: : : . : : • . ! : Find Now ;

: Employe E ID

Synchronize
EX* ;::::
^Enroll 1

Ш • "--'•
Рис. 7. Windows-форма

Прежде чем начать, запустите сценарий SetupRepl.bat из NorthwindCE, a


в SQL Server CE Server Agent сконфигурируйте виртуальный каталог IIS
с именем sqlce, открытый для анонимного доступа через HTTP (надеюсь,
вы понимаете, что такая политика доступа не годится для рабочего прило-
жения). Проверить необходимые настройки можно через оснастку SQL
Server СЕ Connectivity Management, поставляемую с SQL Server CE 2.0
для консоли ММС.

Первая часть кода демонстрирует, как создать пустую базу данных через
объект SqlCeEngine в SQL Server СЕ и синхронизировать ее с существую-
щей публикацией SQL Server для базы данных Nwind_SQLCE через объ-
ект Replication. Функция DBInit, вызываемая обработчиком Form_Load,
создает пустую базу данных подписки (если ее еще нет) и обращается к
DВ Sync, чтобы синхронизировать базу данных публикации с базой дан-
ных подписки (рис. 8).

Рис. 8. Создание и синхронизация базы данных


private void OBInltO
I
try

if (! System, 10- File. ExistsCsSubscriptionDB))


<
SqlCeEngine oEng = new SqlCeEngine{"data source=" +
sSubscriptionDB); •
oEng. CreateQatabaseQ;
DBSync();

catch (SqlCeException ex)

ShowErrors(ex);

catch (System.Exception ex)

MessageBox.Show(ex.Message);

В методе DBSync все значения свойств объекта Replication «зашиты» в


код лишь для наглядности. В реальных приложениях их следует хранить
в таблице SQL или XML-файле. В нашем примере используется аутенти-
фикация SQL Server. Метод Synchronize создает базу данных подписки и
реплицирует в нее полную схему объектов базы данных публикации вме-
сте с индексами (рис. 9).

После создания базы данных подписки и после репликации в нее всех


объектов публикации наше приложение сможет работать в автономном
режиме. При следующем вызове метода DBSync базы данных SQL Server
254 Microsoft SQL Server

2000 и SQL Server СЕ обменяются модифицированными записями за счет


репликации со слиянием.

Рис. 9. Синхронизирующий метод


private void DBSyncO
// Создать экземпляр обьекта Replication
SqlCeReplication oRepl - new SqlCeReplicationO;

try

// Задать свойства издателя


oflepl.Publisher = "MBLAPQT;
oRepl.PublisherDatabase = "Nwind_SQLCE";
oFtepl. Publication = "SQLCEReplDerooNet";

// Задать свойства для защиты издателя


ofiepl.P^blisherLogin = "sa";
oRepl.PublisherPassword » "sa";

// Задать саойства подписчика


cRepl-SubscriberConnectionString =
"Provlder=MicrosQft.SQLServer.QL£DB.CE.2.Q;
Data Source=" * sSubscriptionDB;
oflepl.Subscriber * "H8PPC01";

// Задать саойства для Интернета


oRepl.InternetUrl = "http://mblap01/sqlce/sscesa20.dll";

// Выполнить полную синхронизацию


oRepl.Exchangetype = ExchangeType.BiDirectional;
oRepi.Synchronize();

catch (SqlCeException ex)

ShowErrors(ex);

catch {System.Exception ex)

MessageBox.Show(ex.Messag«);

finally

oRepl,8ispose();
Компактные и надежные приложения на основе SQL Server CE 2.0 255

Заметьте, что для перехвата исключений в SDE-приложении применяют-


ся блоки try/catch/finally. Метод ShowErrors показывает детальные сведе-
ния об исключениях SQL Server СЕ. Код на рис. 10 демонстрирует рас-
крутку (unwinding) стека исключений SQL Server СЕ и вывод сообщения
об ошибке на клиенте.

Рис. 10. Перехват исключений


public static void ShowErrors(SqlCeException ex)

SqlCeErrorCollection oErrors * ex.Errors;


StrirtgBullder oStrBlcJ = new String8uilder();
Exception Dinner = ex.InnerException;

foreach (SqlCeError oErr In oErrors)

oStrBld.Append("\nError Doee: " +


4:; oErr.Hflesult.TaStringC'X"));
eStrBld.AppendC"\nMessage ; " + oErr.Message);
o8trBld.Append<"\nMinor Err.: " + oErr.NativeError);
QStrBId.Append<"\nSource : " + oErr,Source);

foreach (int iHuffiPar in oErr.NurnericErrorPararBeters)

if (iNumPar != 0)
oStrBld.Append("\nNum. Par. : " -f iNumPer);

foreach (String sErrPar in oErr.ErrorParafneters)

if (sErrPar 1= String.Empty)
oStrBld.Append{"\R Err. Par. '•*• sSrrPar);

HessageBox.ShQfcKoStr&ld.ToStrifigO,
oStrBld.RemoveCO, ostraid.l

Напоследок обсудим код метода Find Employees, вызываемого обработчи-


ком события Click кнопки Find Now. Сначала мы создаем экземпляр объ-
екта SqlCeConnection и подключаемся к локальной базе данных подписки.
Далее создается объект SqlCeCommand с параметризованным запросом к
таблице Employees (рис. 11). Обратите внимание на вызовы внутренней
функции RTRIM, удаляющие концевые пробелы в полях FirstName и
LastName для упрощения форматирования этих полей в ListView.
256 Microsoft SQL Server

Рис, 11. Удаление концевых пробелов


private void Find Employees (}
{
// Создать экземпляр объекта SqlCeCormection
SqlCeGonnection oCon ~ new SqlCeConneotion("DataSource="
sSubscriptionDB);

try

oCon.OpenC);
SqlCeCommand oCmd = oCon,CreateCoinBiand();
oCfnd.CoitiraandText =* "SELECT EmployeelQ, RTRDt(LastName) +
-
, * + RTRlM(FirstName) AS Y'Full Hanre\" mm
Employees WHERE lastName LIKE ? 0ROER BY LastNaffle";

// Передать критерий поиска через параметр


oCmd. Parameters. Add( new SqlCeFaraffleter("p1",
txtUstNaae.Text + "Г1));

SqlCeDataReader oReader = oCffld.ExeeuteReader<>;

// Заполнить элемент управления ListView


ivwEfflployees. Items, С1еаг();
while (oReader.ReadO)
{
ListVlewItem oltem = IvwEmployees. Items. Add<new
ListViewItem(new String [3i(oReader.6etString(1),
oReader.Get!nt32(0) . ToStringf) J)};

catch (SqlCeException ex)

ShowErrors(ex);

catch (System.Exception ex)

Messa§eBox.Show(ex.Message);

finally

// Закрьпъ и освободить соединение


if CoCon.State == ConnectionState.Open)
oCon.CloseQ;

if (oGon I- null)
oCon.Dispase();
} • -
Компактные и надежные приложения на основе SQL Server CE 2.0

Набор Parameters объекта SqlCeCommand позволяет передать шаблон по-


иска оператору LIKE в конструкции SELECT. Мы используем объект
SqlCeDataReader для выполнения запроса и заполнения элемента управ-
ления List View. Наконец, ранее открытое соединение с базой данных под-
писки закрывается и освобождается.

Как выглядит законченное приложение в эмуляторе Pocket PC 2002 Emu-


lator, показано на рис. 12.

Pocket PC 2002
Emulator Help

Е:Г 1ЕМУ8ЕЯ
Buchananj Steven
I ID Л*
CaSahan, Laura :
DavollOj Nancy 1 -----
Dodsworthj Anne i
Fuller, Andrew
King, Robert 7
Levelling, Janet 3

Рис. 12. Приложение в эмуляторе Pocket PC

Что следует учесть при установке


Пара слов о том, что нужно учесть при установке приложений. Сама по
себе SQL Server СЕ чрезвычайно надежна, но устройства, на которых она
работает, подвержены сбоям (в частности, из-за ограниченного ресурса
аккумуляторов). Следовательно, при разработке любого мобильного реше-
ния нужно помнить о восстановлении после сбоев.

Надежное хранилище данных можно создать на основе технологий энер-


гонезависимой памяти вроде CF Media. Чтобы пользователи могли восста-
новить данные после сбоя, закончившегося «холодной» перезагрузкой ус-

9-5917
258 Microsoft SQL Server

тройства, следует размещать базу данных и исполняемые файлы приложе-


ния на карте CF. Кроме того, создав САВ-файл для своего приложения, вы
существенно упростите его установку вне офиса.

SQL Server CE 2.0 — стимул к действию


SQL Server CE 2.0 позволит разрабатывать корпоративные решения в об-
ласти управления данными для мобильных устройств. Новая версия SQL
Server CE — настоящий прорыв в функциональности и производительно-
сти по сравнению с предыдущей версией. Мы советуем всем опробовать
эту замечательную СУБД, и лучший способ сделать это — самому создать
«китовое» приложение для мобильных устройств. SQL Server CE 2.0 мож-
но скачать по ссылке http://www.microsoft.com/sql/ce/downloads/ce20.asp.

Марк Браун (Mark Brown) — главный архитектор ПО, а Дэвид Менье (David
Meunier) — ведущий инженер ПО в компании IdentityMine Inc. (http://
www.identitymine.com), которая является независимым поставщиком ПО
(Такома, штат Вашингтон). IdentityMine занимается проектированием и
реализацией бизнес-решений следующего поколения с применением
серверных технологий Microsoft .NET Enterprise. С авторами можно связать-
ся по адресам mark.brown@identityrnine.com и
david.meunier@identitymine.com соответственно.
Марк Браун

Доставка информации
в реальном времени
с применением Notification

Популярность беспроводных устройств постоянно растет, и организации ищут


новые способы установления контактов с пользователями этих мобильных
устройств. Раньше для целевой рассылки информации клиентам применялись
MR слишком успешные- подходы, например п ЮЛО1 ия досовки (push
technology). Теперь с появлением службы Notification Services, использующей
ядро баз данных SQL Server 2000 и платформу .NET Framework, стала возмож-
ной разработка уведомляющих приложений (notification applications) нового
поколения, которые позволяют отправлять на любые мобильные устройства
информацию, интересующую подписчиков таких приложений.

Автор рассматривает архитектуру основной функциональности службы


Notification Services в SQL Server. Кроме того, он поясняет, как использовать
эту службу для доставки Web-контента (информационного наполнения).

С середины 90-х провайдеры контента и услуг создали бессчетное множе-


ство схем генерации и доставки пользователям информации и уведомле-
ний, или извещений (alerts). К сожалению, технологии доставки из-за сво-
ей широковещательной природы с годами приобрели плохую репутацию,
Реализация многих методов доставки оставляла желать лучшего, полоса
пропускания сетей использовалась слишком интенсивно, а некоторые со-
общения были весьма назойливы и имели сомнительную ценность.

* Публиковалось в MSDN Magazine/Русская Редакция. 2002. №5 (ноябрь). — Прим. изЗ,


260 Microsoft SQL Server

Провал ранних вариантов модели доставки можно отчасти объяснить от-


сутствием стандартов на службы уведомления. Однако, очевидно, что тра-
диционные формы этой модели были обречены на неудачу прежде всего
из-за громоздких схем доставки, которые не масштабировались, и из-за
невозможности более тонкого управления подпиской. Вдобавок большин-
ство служб уведомления для доставки сообщений и контента требовало
специализированных клиентских приложений или Web-браузеров с под-
держкой соответствующих технологий. Также не было никаких гарантий,
что уведомления попадут действительно к тому, кому они нужны.

Варианты подписки тоже были весьма ограниченны: пользователи подпи-


сывались либо на весь контент определенной тематики, либо на каналы
вроде новостных, спортивных или биржевых, тогда как на самом деле нуж-
на была информация, отвечающая личным интересам пользователей и от-
бираемая по заданным ими критериям фильтрации. Поэтому, чтобы уве-
домляющее приложение было полезным своим подписчикам, первооче-
редное внимание следует уделять персонализации контента.

Очевидно, что разработка своего уведомляющего приложения и соответ-


ствующей инфраструктуры — весьма тяжелая задача для любой организа-
ции, при решении которой может возникнуть масса проблем. Из-за мно-
гочисленных ограничений во многих современных приложениях доставки
уведомлений и контента прибегают к широко распространенной и простой
в использовании электронной почте (протокол SMTP) или системе обме-
на текстовыми сообщениями (SMS). Однако эти приложения по-прежне-
му опираются на собственные схемы сбора событий, генерации уведомле-
ний и управления подпиской.

В последние несколько лет появились более эффективные протоколы, раз-


работанные для передачи уведомлений. Эти протоколы обладают высокой
масштабируемостью, большей надежностью и защитой. С повсеместным
распространением беспроводных мобильных устройств и инфраструктур,
обеспечивающих доставку сообщений почти в реальном времени (напри-
мер, SMS, WAP, Instant Messaging и Microsoft .NET Alerts), компании на-
чинают осознавать, что им нужна мощная платформа для уведомлений,
способная задействовать возможности этих каналов доставки.

Беспроводные устройства нового поколения с поддержкой доступа к Ин-


тернету смогут передавать информацию о местонахождении пользователя
через интегрированную GPS (Global Positioning System). Операторы бес-
проводной связи получат возможность безопасной доставки такой инфор-
мации через Web-сервисы (с согласия пользователя и при условии сохра-
нения конфиденциальности). Появление информации о местонахождении
Доставка информации в реальном времени 261

пользователя в сочетании с мощной платформой для уведомлений приве-


дет к созданию нового поколения уведомляющих приложений.

Перспективы бизнеса на новом рынке


К типичным уведомлениям относятся напоминания клиентам и сотрудни-
кам о мероприятиях и запланированных встречах, извещения о доставке
грузов, финансовые и биржевые сводки, «горячие» новости, отчеты о си-
туации на дорогах и даже результаты спортивных матчей.

Раньше из-за ограниченных возможностей технологии доставки компани-


ям было трудно создавать уведомляющие приложения, достаточно надеж-
ные в обслуживании миллионов (или хотя бы тысяч) подписчиков. Кро-
ме того, разработка по-настоящему конкурентоспособных уведомляющих
приложений осложнялась отсутствием стандартной платформы.

Благодаря последним достижениям производителей мобильных устройств


и лавинообразному росту популярности Web-сервисов теперь гораздо про-
ще связаться с кем угодно — в любом месте и в любое время. С распрост-
ранением таких мобильных устройств, как смартфоны, PDA, наладонные
устройства для работы с электронной почтой, компании вновь ищут спо-
собы своевременной доставки пользователям персонализированной ин-
формации.

Однако сейчас основное внимание уделяется тому, чтобы клиентам посы-


лались сообщения только по интересующим их вопросам. Такие сообще-
ния должны побуждать клиентов посетить Web-сайт компании в поисках
дополнительной информации или для размещения заказа. Все это, несом-
ненно, открывает компаниям новые возможности в получении прибыли.

Использование новых возможностей


Notification Services в SQL Server — новая платформа приложений, снима-
ющая ограничения традиционных технологий доставки. Она использует
модель «издатель-подписчик», в которой доставка контента всегда согла-
суется с клиентом. Уведомления отправляются только тем, кто подписал-
ся на их получение. Сообщения можно форматировать в соответствии с
требованиями устройств разных типов, а информационное наполнение —
приводить в соответствие с личными предпочтениями подписчика. Это
позволяет воспользоваться открывающимися на рынке возможностями
как мелким компаниям, так и крупным предприятиям.

Платформа Notification Services предоставляет разработчикам интуитив-


но понятную модель создания централизованных уведомляющих прило-
жений и их развертывания в любом масштабе. Она использует простую
262 Microsoft SQL Server

инфраструктуру декларативною программирования, основанную на язы-


ках, уже известных разработчикам, например на Transact-SQL и XML.
Кроме того, Notification Services- предназначена для бесшовной интеграции
с приложениями сторонних поставщиков, с провайдерами событий и
службами доставки, которые, возможно, работают в операционных систе-
мах, отличных от Windows. Так как платформа Notification Services ограж-
дает разработчика от сложностей, связанных с генерацией и доставкой,
типичное уведомляющее приложение можно создать и развернуть очень
быстро.

Архитектура Notification Services


Рассмотрим высокоуровневую архитектуру службы Notification Services в
SQL Server, чтобы понять, как разработчики уведомляющих приложений
могут задействовать богатые возможности этой платформы для достиже-
ния максимальной продуктивности своего труда.

Основное требование к любому уведомляющему приложению — способ-


ность собирать события, сравнивать их с предпочтениями подписчика
(подписками), генерировать и форматировать уведомления и в конечном
счете доставлять сообщения на устройства подписчика. Платформа Noti-
fication Services предоставляет всю эту функциональность и инфраструк-
туру, позволяющую быстро создавать и развертывать уведомляющие при-
ложения.

Notification Services позволяет хранить данные о подписке и подписчиках


и управлять ими через Notification Services API. Вы можете собирать со-
бытия, получаемые из самых разнообразных внешних источников с помо-
щью стандартных или собственных провайдеров событий. Эти события
можно сравнивать с предпочтениями индивидуальных подписчиков по
факту возникновения событий или по расписанию, а затем генерировать
уведомления. Наконец, можно форматировать уведомления в соответ-
ствии с типом устройства, используемого подписчиком, и его региональ-
ными стандартами, после чего передавать уведомления внешней службе
доставки.

Ключевая особенность архитектуры Notification Services в том, что служ-


бы доставки отделены от средств генерации уведомлений. Форматирова-
ние и доставка уведомлений возлагается на специальный компонент.

Распределенная архитектура позволяет рассредоточить компоненты и


службы, необходимые для выполнения уведомляющего приложения, по
нескольким компьютерам. В следующих разделах я подробно рассмотрю
основные особенности архитектуры всех базовых компонентов Notification
Services.
Доставка информации 8 реальном времени 263

Архитектура приложения управления подпиской


Подписки (subscriptions) — главная функциональность любого уведомля-
ющего приложения, так как именно они определяют, что и когда получит
подписчик. Кроме того, в подписке может быть указано определенное ус-
тройство, на которое следует отправлять уведомления.

Подписки бывают двух категорий: по расписанию (scheduled subscriptions)


и управляемые событиями (event-driven subscriptions). В нервом случае
уведомления доставляются в соответствии с календарной датой, временем
дня, заданной периодичностью или по определенным дням недели (все
параметры можно комбинировать). Во втором — уведомления доставляют-
ся сразу после получения события от внешнего источника.

Поскольку каждое уведомляющее приложение уникально, вам придется


создать собственный компонент для управления своими подписчиками и
их предпочтениями. Такой компонент называется приложением управле-
ния подпиской (subscription management application).

Приложения управления подпиской обычно разрабатываются как Web-


приложения на основе ASP или ASP.NET. Однако с помощью Notification
Services API вы можете создать его как стандартное Windows-приложение.
Высокоуровневая архитектура приложения управления подпиской пока-
зана на рис. 1.

управления подянсиав

Notification Services API

ТЛ
Рис. 1. Приложение управления подпиской

Учтите, что в терминологии Notification Services под данными подписчи-


ка (subscriber data) подразумевается информация о подписчике и его уст-
ройствах, а под данными подписки (subscription data) — информация о
конкретных классах событий, интересующих подписчика, и соответствую-
щие параметры.
264 Microsoft SQL Server

Notification Services API


В Notification Services информация о подписчике, устройстве и подписке
помещается в центральную базу данных этой службы, за счет чего данные
подписчика хранятся в одном экземпляре, а данные подписки могут быть
разными для каждого уведомляющего приложения.

Notification Services API позволяет создавать данные подписчика и управ-


лять ими через классы управляемого кода (или COM Interop) с использо-
ванием перечислимых (enumerations). Эти классы значительно упрощают
создание приложений управления подпиской, абстрагируя доступ к базе
данных.

Классы событий и ADF


В Notification Services событием называется некое обстоятельство или ус-
ловие, о котором пользователь хочет получить уведомление. Создание уве-
домляющих приложений практически полностью основано на событиях.

Чтобы приложение могло принимать события, сначала опишите классы


событий с помощью XML-файла метаданных, также называемого файлом
определения приложения (application definition file, ADF). ADF описыва-
ет данные и структуру приложения Notification Services. В ADF обычно
определяется один или несколько классов событий.

Рис. 2 показывает, как создается класс события Stock с использованием


ADF. Обратите внимание на то, как объявлена структура данных этого
класса события в разделе <Schema> узла <EventClass>. Также заметьте,
что для описания типа данных события в элементе <FieldType> использу-
ются синтаксические соглашения Transact-SQL. Раздел <FieldTypeMods>
указывает, может ли поле содержать пустые значения.

Рис. 2. Определение класса события


<EventClasses>
<1 --Пример определения класса события Stock. -->
<EventClass>
<! — Имя класса события. — >
<EventCZassName>Stock</EventClassNaBie>\
<!--Поля события определяются в разделе Schema, — >

<! —Определяем поле для обозначения акционерной компаний


(акции которой котируются на бирже). -->
<Field>
<FieldNarae>StGekSymboa</FieldNarBe>
<FieldType>char(10)</FIeldType>
<FieldTypeMo<Js>fiot nuU</FieldTypel*ods>

см. след. стр.


Доставка информации в реальном времени 265

Рис. 2. Определение класса события (окончание)


</Field>
<!—Определяем поле для курса акций.—> ;
<Field>
<FieldNaine>StQckVaIue</Field.Name>
<FieldType>money</FieldType>
<FieldTypeMods>not nuIl</FleldTypeMods>
</Field>

</Schema>
</EventClas5>
</EventClasses>

Эта схема нужна провайдерам событий для проверки допустимости дан-


ных события. На основе определенных в ней полей при создании нового
экземпляра уведомляющего приложения в базе данных этого приложения
автоматически генерируется таблица SQL Server, в которой сохраняются
данные события.

Теперь, когда вы получили первое представление о том, как определяют-


ся события в приложении Notification Services, рассмотрим архитектуру
сбора событий.

Архитектура сбора событий


Платформа Notification Services позволяет собирать данные событий из
самых разнообразных источников с помощью провайдеров событий (event
providers). Провайдеры событий могут быть реализованы в виде DLL, раз-
мещаемые в самой Notification Services, или как внешние исполняемые
файлы. Провайдеры событий первого вида могут выполняться постоянно
или по расписанию.

Провайдеры событий отвечают за мониторинг источника событий и пос-


ледующую запись данных события в таблицу событий уведомляющего
приложения. Они могут передавать события в базу данных приложения
через три API. Первый из них — API управляемого кода, позволяющий
записывать данные прямо в таблицу событий (этот API может предостав-
ляться через COM Interop в виде интерфейсов). Второй — XML API, рас-
считанный на пакетную загрузку и запись событий в таблицу событий из
XML-документа или потока (stream). И третий — хранимые процедуры,
которые разработчик может вызывать для прямой загрузки данных собы-
тия из объектов базы данных и их записи в таблицу событий.

Хотя события добавляются в таблицу по одной записи единовременно,


провайдеры обычно отправляют события службам Notification Services
группами, называемыми пакетами (batches). После передачи пакета про-
266 Microsoft SQL Server

ваидером событий уведомляющее приложение может сразу же заняться


обработкой событий, чтобы приступить к генерации уведомлений. Одна-
ко уведомляющее приложение может отложить обработку событий соглас-
но расписанию. Высокоуровневая архитектура провайдера событий пока-
зана на рис. 3.

Рис. 3. Архитектура провайдера событий

Модели сбора событий


Провайдеры событий могут наблюдать за данными и собирать их многи-
ми способами. Однако большинство провайдеров, как правило, работает
по одной из двух моделей. При использовании компонента сбора оповеще-
нием (push collector) или сбора опросом (pull collector) события либо дос-
тавляются (push) провайдеру либо запрашиваются (pull) от внешнего ис-
точника по расписанию. В модели на основе расписания или в модели,
управляемой событиями, провайдер событий может запускаться периоди-
чески — так, как задано приложением. Возможен и другой вариант: про-
вайдер, напротив, ведет непрерывный мониторинг событий (или ожидает
обратного вызова), чтобы сразу после того, как событие станет доступно,
отправить его службе Notification Services.

Стандартные и нестандартные провайдеры событий


Платформа Notification Services включает два стандартных провайдера
событий: File System Watcher Event Provider и SQL Server Event Provider.
С их помощью можно тут же приступить к разработке собственного уве-
домляющего приложения и поэкспериментировать с правилами генерации
уведомлений, а также с форматированием и доставкой сообщений.

File System Watcher Event Provider наблюдает за файлами, добавляемыми


в каталог файловой системы. Единственное требование заключается в том,
Доставка информации в реальном времени 267

что это должны быть синтаксически корректные (welt-formed) XML-фай-


лы. Когда в каталог, указанный разработчиком, помещается новый файл.
File System Watcher Event Provider выполняет массовую загрузку XML-
данных события в память через XML API, который в свою очередь запи-
сывает события в таблицу событий уведомляющего приложения. Затем
исходный файл переименовывается, чтобы было видно: он уже обработан.

SQL Server Event Provider извлекает данные события прямо из таблицы


базы данных по запросу на языке T-SQL. Для сбора данных новых собы-
тий и их записи в таблицу событий этот провайдер использует хранимые
процедуры, о которых я уже говорил. Кроме того, он позволяет выполнять
пре- и постпроцессную обработку событий в течение интервала сбора
(collection interval), определенного приложением.

Конечно, на основе ранее упомянутых API можно создавать более гибкие


нестандартные провайдеры событий. А это дает возможность наблюдать
практически за любым внешним источником событий (например, за Web-
сервисами, почтовыми серверами, Web-сайтами) и передавать его события
службе Notification Services для дальнейшей обработки.

Хронологии событий
Еще одна особенность архитектуры сбора событий — поддержка хроноло-
гии событий (event chronicles). Полное объяснение того, как их можно
использовать в уведомляющем приложении, я приведу в следующем раз-
деле. А пока скажу, что пакет событий обрабатывается только в процессе
генерации уведомлений (не считая случаев системных сбоев и перезагру-
зок), и таблицы хронологии событий позволяют в этот период сохранять
(или архивировать) данные событий для последующей обработки.

Генерация уведомлений
После того как необработанные данные событий собраны провайдерами и
записаны в таблицу хронологии событий, можно приступать к обработке
подписок.

Центральное место в Notification Services занимает генератор уведомлений


(notification generator). Его задача — сравнить собранные события с под-
писками и определить, выполнены ли условия, необходимые для генера-
ции неформатированного уведомления (raw notification). Такое уведомле-
ние содержит неформатированные данные события, к которым относится
информация, требуемая для передачи уведомления, например сведения о
подписчике и об устройстве, которое он использует. Форматирование и
доставка уведомления выполняются отдельным процессом (см. раздел
«Архитектура форматирования и доставки уведомлений»).
268 Microsoft SQL Server

Условия генерации формулируются в виде правил подписки на события.


Правило — это оператор Transact-SQL, определяющий связь между собы-
тиями и подписками, а также другие условия, которые должны быть вы-
полнены для генерации уведомления. Правила подписки на события про-
веряются всякий раз, когда генератор получает очередной пакет событий.

При генерации уведомлений могут использоваться и данные предыдущих


событий, хранящиеся в таблице хронологии. Это позволяет создавать
сложные правила, основанные на состоянии, в которых новые события
сравниваются с предыдущими и тем самым определяется, надо ли генери-
ровать дополнительное уведомление (рис. 4).

Необработанные
Подписки
события

Хронология
^ событий

Рис, 4. Обработка подписок

После генерации уведомление еще не готово к отправке подписчику. Сна-


чала оно записывается во внутреннюю таблицу уведомлений вместе с ин-
формацией об исходном событии, подписчике, устройстве, на которое
выполняется доставка, и прочими данными, необходимыми для передачи
сообщения.

По аналогии с тем, как провайдер событий записывает пакеты событий


для последующей обработки генератором уведомлений, последний запи-
сывает уведомления, готовые к доставке, тоже пакетами. Дистрибутор уве-
домлений (notification distributor) в свою очередь обрабатывает пакеты
неформатированных уведомлений и преобразует их в удобочитаемые со-
общения, форматированные в соответствии с требованиями устройства, на
которое они передаются,

Архитектура форматирования и доставки уведомлений


На этой стадии процесса неформатированные уведомления, созданные
генератором, хранятся в таблицах уведомляющего приложения в виде па-
кетов. После закрытия пакета уведомлений генератором эти уведомления
готовы к форматированию и доставке подписчикам дистрибутором (рис. 5).
Доставка информации в реальное времени 269

Данные
1едомления

Компонент,
объединяющий уведомления

КОМПОНЕНТ,
форматирующий кояшт

:
.'.Какая I Какая 1 Канав
Яввтавкй ЬоЕтав** 1 до&таекв
1 & I В; I С
Рис. 5. Форматирование и доставка уведомлений

Для большей производительности дистрибутор обрабатывает сразу не-


сколько пакетов уведомлений, используя пул потоков (threads). Пакеты
состоят из, всех уведомлений, которые будут переданы по одному каналу
доставки. Это позволяет распределить рабочую нагрузку между несколь-
кими логическими системами доставки,

После того как дистрибутор выбрал пакет для обработки, уведомления,


адресованные одному и тому же подписчику, объединяются в единое уве-
домление, называемое сводным (digest notification). Кроме того, компо-
нент, объединяющий уведомления (aggregator), способен оптимизировать
форматирование в тех случаях, когда содержимое уведомления для не-
скольких подписчиков одинаково (или похоже). Тогда данные уведомле-
ний, общие для группы подписчиков, форматируются только один раз.

Компонент, форматирующий контент (content formatter), на основе ин-


формации о получателе (типа устройства подписчика, сетевого адреса,
канала доставки и региона) определяет, как должен выглядеть контент.
Параметры, определяемые локализующим идентификатором (locale), ко-
торый задается в соответствии с региональными стандартами, можно ис-
пользовать для автоматического форматирования контента на родном
языке подписчика, а наличие сведений о мобильном устройстве позволя-
ет подготовить контент так, чтобы он корректно отображался на этом уст-
ройстве.

После форматирования уведомления передаются доступному каналу дос-


тавки для передачи подписчику. Каналы доставки формируют из уведом-
лений сообщения, специфичные для применяемого протокола, и направ-
ляют их внешней системе доставки. Обычно каналы доставки используют
общеизвестные протоколы типа SMTP; однако возможно применение и
270 Microsoft SQL Server

нестандартных протоколов. Для этого вы реализуете протокол как класс,


помещаемый в сборку управляемого кода, которая доступна вашему уве-
домляющему приложению.

Принцип доставки многоадресных уведомлений (multicast notifications)


зависит от разработчика протокола: либо одно сообщение широковеща-
тельно рассылается всем подписчикам, либо каждому подписчику направ-
ляется отдельное сообщение.

Для распределения нагрузки и ускорения передачи сообщений можно ис-


пользовать сразу несколько каналов доставки, реализующих один и тот же
протокол. Например, у вас может быть несколько протоколов, применяе-
мых для доставки данных на одни сервер. Каждый экземпляр канала дос-
тавки напрямую взаимодействует с внешней службой доставки, поэтому
имя сервера и аутентификационную информацию нужно указывать для
каждого канала в конфигурационном файле.

Еще одна важная особенность архитектуры распространения уведомле-


ний — поддержка обработки ошибок доставки. Дистрибутор может пы-
таться передавать уведомления, которые не удалось доставить сразу, в со-
ответствии с расписанием повторной доставки, заданным разработчиком.
Но учтите, что логика повторной доставки применяется ко всему пакету
уведомлений: если не доставлено хотя бы одно уведомление из пакета, не-
удачной считается доставка всего пакета.

В файле определения приложения можно указать число повторных попы-


ток доставки для данного класса события. Кроме того, для уведомлений
можно задать срок их действия. По истечении этого срока дистрибутор
больше не будет пытаться доставить такие уведомления.

Развертывание и масштабируемость
Как я уже говорил, приложения Notification Sen-ices отличаются высокой
масштабируемостью — во многом благодаря нижележащей архитектуре
распределенных сервисов. Более того, каждое уведомляющее приложение
делится на экземпляры. Каждый экземпляр использует собственные запи-
си в реестре и запускается как отдельная Windows-служба. Это позволяет
выполнять множество экземпляров одного уведомляющего приложения
как на одном компьютере, так и в системе из нескольких компьютеров.

Свой вклад в высокую масштабируемость Notification Services вносит и


тесная интеграция с SQL Server. Поскольку SQL Server хорошо оптими-
зирован под запросы объединения (joins), такая интеграция гарантирует
отличную производительность уведомляющих приложений. И конечно
же, для сбора событий или создания правил генерации уведомлений нет
ничего эффективнее запросов T-SQL!
Доставка информации в реальном времени 271

Конфигурации с вертикальным
и горизонтальным масштабированием
С помощью XML-файлов определения и конфигурации экземпляры уве-
домляющего приложения можно настраивать по-разному и при необходи-
мости размещать их на отдельных компьютерах. Кроме того, большинство
компонентов таких приложений (например, сбора событий, генерации
уведомлений, дистрибуции и доставки) являются многопоточными, что
позволяет распределять нагрузку между несколькими процессорами. Это
обеспечивает возможность вертикального масштабирования (scale-up),
когда производительность повышается простым добавлением памяти и
процессоров в единственный сервер.

Однако в ситуациях, когда одномашинная конфигурация не справляется


с нагрузкой, многие компоненты Notification Services можно распределить
между несколькими серверами. Данные экземпляров и приложения всегда
должны храниться на одном SQL-сервере, но провайдеры событий, гене-
ратор уведомлений и компоненты дистрибутора можно регистрировать на
разных серверах.

Чтобы задействовать такие конфигурации с горизонтальным масштабиро-


ванием (scale-out), установите исполняющую среду Notification Services на
каждом компьютере, где будет зарегистрировано уведомляющее приложе-
ние. Развертывание подобной конфигурации значительно упрощается за
счет использования конфигурационных файлов и файлов определения
приложения в формате XML — они облегчают регистрацию и создание
экземпляров приложения на каждом компьютере.

Заключение
Notification Services позволяет отслеживать любые источники данных, в
том числе предыдущие версии SQL Server, файловые системы и нестан-
дартные источники. Notification Services (она доступна для скачивания с
http://www.microsoft.com/sql/ns) упрощает разработку уведомляющих
приложений, которые своевременно доставляют нужную клиентам инфор-
мацию.

Марк Браун (Mark Brown) — главный архитектор программного обеспечения


в компании IdentityMine Inc. (http://www.identitymine.com), находящейся в
Такоме (штат Вашингтон). IdentityMine занимается проектированием и
разработкой нового поколения Интернет-решений для бизнеса на основе
серверных технологий и программного обеспечения Microsoft .NET. С ним
можно связаться по адресу mark.brown@identitymine.com.
альманах программиста
Атиф Азиз

Динамическое связывание
уровня данных
с хранимыми процедурами
и командами SQL*

Крупный недостаток, связанный с вызовом хранимых процедур SQL (stored


procedures), — необходимость предоставлять соответствующую информацию
о типах. Приходится писать объемистые функции — оболочки хранимых
процедур только для того, чтобы обеспечить доступ к нужным типам данных.
Однако в .NET Framework сервисы отражения (reflection services) простран-
ства имен System. Reflect ion позволяют использовать метаданные, описываю-
щие типы данных. В этой статье объясняется, как с помощью механизма
Reflection в .NET покончить с кошмаром хранимых процедур. Автор создает
четыре собственных класса, один из которых генерирует объект
SQLCommand на основе метаданных вашего метода. Используя эту библиоте-
ку, вы сможете автоматизировать генерацию объектов команд.

Если вы часто пользуетесь хранимыми процедурами, то, уверен, отлично


знаете, как это утомительно — писать один и тот же код, определяющий
имя, тип и размер каждого параметра, всякий раз, когда готовишься выз-
вать объект команды (command object). А если вы когда-нибудь изменяли
интерфейс хранимой процедуры, вам скорее всего приходилось возвра-
щаться к своему уровню сервисов доступа к данным (data service layer) и
соответственно модифицировать вызовы хранимой процедуры. Програм-
мисты готовы на что угодно, лишь бы упростить этот процесс: централи-
зовать вызовы хранимых процедур в уровне сервисов доступа к данным,

* Публиковалось в MSDN Magazine/Русская Редакция. 2002. №2 (август). — Прим. изд.


276 Доступ н данным из приложений

писать оболочки вокруг них и даже реализовать генераторы. Я собираюсь


продемонстрировать, как с помощью атрибутов в Microsoft .NET можно
определить хранимую процедуру так, словно это функция, которую вы
пишете на своем любимом языке — будь то С#, Visual Basic .NET или дру-
гой язык, поддерживающий объявления с атрибутами и ориентированный
на общеязыковую исполняющую среду (common language runtime, CLR).

В классическом ADO вызов даже самой тривиальной хранимой процеду-


ры, которая принимает единственный параметр, требует создания объек-
та команды и настройки набора параметров. Рассмотрим хранимую проце-
дуру из приложения-примера IBuySpy Portal (http://www.ibuyspy.com)
(рис. 1).

Рис. 1. GetAn noun cements (из IBuySpy)


С8ЕАТЕ PROCEDURE GetAnnounceraents
<;
tHodulelD int
)
AS
SELECT

CreatedByllser,
CreatedDate,
Title,
Mo relink,
НоШеМо relink,
Expi reOate,
Description
FROM
Announcements

WHERE
KodulelD = @HoduleXB
AND
ExpireDate > 6etDate()

В данном случае не имеет никакого значения, знаете ли вы этот образец


портала IBuySpy и структуру его базы данных. Здесь важно другое: интер-
фейс хранимой процедуры. Если бы вы использовали ADO и Visual Basic
for Applications (VBA), функция-оболочка для вызова этой хранимой про-
цедуры выглядела бы так, как показано на рис. 2.

Написание такой функции-оболочки позволяет не только централизовать


определение хранимой процедуры в проекте, но и выдать хранимую про-
цедуру за обычную VBA-функцию. Благодаря этому вызов хранимой про-
цедуры упрощается до:
Динамическое связывание уровня данных 277

Dim Connection As ADODB.Connection


Set Connection = OpenConnection()

Dim Recordset As ADODB.Recordset


Set Recordset = GetAnnouncements(Connection, 13)

Connection.Close

Рис. 2. Вызов GetArmouncements через ADO


function GetAftnoLmcementsCByVal Connection As ADODB.Connection,
ByVal HodulelD As Integer) As ADODB.Recordset
Dim Command As AMOB,Command
Set Command = New ADODB.Command
Set Command.ActiveConnectlon = Connection
Command. ComfliancfText = "GetAnnouncements"
Command, CommandType = adCmtfStoredProc
Dim Parameters As AOODB.Parameters
Set Parameters = Command.Parameters
Parameters.Append Command.CreateParaineter("eRETURN_VAI_UE", ;„
adlnteger, adPararaReturnValue, 0}
Parameters.Append Command. CreateParameter("@Mo£fuleIQ", _
adlnteger, adParamlnput, 0)
PafaiBeters("@ModuleID").Value * HodulelD
Dim Recordset As ADQDB.Recordset
Set fteoordset = New AD08B.Recordset
Recordset.CursorLocation = adUseClient
Recordset,Open Command, , adOpenStatic, adLockReadOnly
Set Recordset.ActiveConnection = Nothing
Set GetAnnouncemertts = Recordset
End Function

Это даже ужесточает контроль типов: свойство Value объекта Parameter


определено как Variant, но параметр ModulelD функции GetAnnounce-
ments — как Integer.

Но когда для вызова простейшей хранимой процедуры вы пишете код,


подобный функции GetAnnouncements, в вашей голове звенит сигнал тре-
воги. Вы начинаете создавать какие-то вспомогательные функции, кото-
рые возвращают отсоединенный набор записей (disconnected recordset)
при наличии любого объекта Command. Это уменьшает размер оболочек
хранимых процедур, хотя настройка параметров вручную и синхрониза-
ция оболочек с определениями в базе данных — занятие скучное и чрева-
тое ошибками. Следующим интуитивным шагом может стать написание
генератора, автоматически создающего в Visual Basic на основе объектной
библиотеки SQLDMO функции — оболочки хранимых процедур. Многие
разработчики даже пишут XML-определения хранимых процедур, а затем
используют в качестве генератора кода XSLT-преобразование.
278 Доступ к данным из приложений

Рис- 3. AnnouncementsDB.GetAnnouncements

public class AnnQuneententsBB {

public DataSet SetAnnouncesentsUnt module!^) {


// Создаем экземпляр Connection и объект Coffimand
SqlCormeetion RyConnection = new SqlConnection{
ConfigurationSettings.ApE^ettingsr'connectioflStrtng"3);
к
SqlDataAdapter sryCoflffliand new SqlDataAdaprter{"8etAnnounceBeRts",
^Connection);
// Помечаем Command как SPROC
rayCoMaod.SelectComi»aRd.CQmantfType = CoiimandType.StoredProeedure;
// Добавляем параметры в SPROC
SqlParajseter pararaeterModuleld = new SqlPararaeterClN'loduleld",
SqlBbType.Int, 4);
parameterModuleld.Value = moduleld;
myCosimaRcf.SelectCofnraand.Parameters.AddCparMeterHoduleld);
// Создаем и заполняем DataSet
DataSet myDataSet » new SataSetO;
myCoraiBafld. Fill(myOataSet);
// Возвращаем DataSet
return nyBataSet;

Когда вы переносите свой код в .NET, ситуация во многом остается пре-


жней. Вместо объекта ADODB.Connection используется объект SqlCon-
nection из пространства имен System.Data.SqlClient (при работе с SQL
Server). Вместо ADO-объекта Command создается экземпляр SqlCom-
rnand. Вместо Parameter создается SqlParameter. Сам процесс фактически
не меняется. По сути на рис. 3 показана Си-версия оболочки Get Anno Lin-
cements из IBuySpy.

Хорошая новость в том, что перенос вашего кода доступа к данным с клас-
сической клиент-серверной архитектуры или Windows DNA достаточно
прост. А плохая — вам все равно нужно поддерживать в коде определения
хранимых процедур и писать объемистые функции-оболочки на С# или
Visual Basic .NET.

Так что же, засучить рукава, и пусть этот генератор создает код не на VIJA,
а на С# или Visual Basic .NET? Нет, этим мы заниматься не будем. Более
современный подход — использовать преимущества некоторых инноваций
в CLR и автоматически генерировать вызовы хранимых процедур на осно-
ве сигнатур функций. Я продемонстрирую, как это делается, и предложу
одну библиотеку, которая послужит вам отправной точкой,
Динамическое связывание уровня данных 279

Метаданные и Reflection
Компилятор, рассчитанный на CLR, генерирует метаданные, описываю-
щие все аспекты типа и его членов. Эти метаданные обычно помещаются
в сборку (assembly) (статическую на диске или даже динамическую в па-
мяти), и к ним можно обращаться в период выполнения через сервисы
отражения из пространства имен System.Reflection. Рис. 4 иллюстрирует,
как это реализуется на С#. Класс PortalDatabase содержит Си-эквивалент
функции Get Announcements, которую вы видели в VBA-коде. Поскольку
на данном этапе я лишь показываю, что представляют собой сервисы от-
ражения, реального кода в самой функции нет. На выходе этот пример
кода дает имя функции, типы всех ее параметров и тип возвращаемого
значения:

Рис. 4, Пример использования Reflection в С#


using System;
using System. Reflection;
using System. Data;
using System. Data. SqlClieot;
sealed class .PortalDatabase
t
public static DataSet GetAnnoundements(SqlConnection connection,
int moduleld)
{
return null;

sealed class ReflectionSaeiple


{
static void Main()
{
MethodlnfoO methods = typeof(PortalOatatiase).QetMethods(
BindingFlags. Static | BlndingFlags, Public f
SindingFlags. DeclaredOnly);
fo reacti (Methodlnfo method in methods)
{
Console. WriteLine('40},{1} [{2}]", method. DeclaringType. Name,
method. Name, method. ReturnType);
foraach (Parameter Info paramlnfo in method. SetParaaetersO)

Console. WriteUneCAttO} {1}",


pararalnfo. ParaineterType,
280 Доступ к данным из приложений

PortalDatabase.GetAnnouncements [System.Data.DataSet]
System.Data.SqlClient.SqlConnection connection
System.Int32 moduleld

Функция Main начинает с получения всех методов класса Announce-


mentsDB. Маска BindingFlags позволяет указывать, какие методы следует
включать в массив, возвращаемый GetMethods. BindingFlags.Static и Bind-
ingFlags. Public обязательны, так как GetAnnouncements объявляется как
public и static, — иначе возвращаемый массив окажется пуст. Флаг Bin-
dingFlags. DeclaredOnly гарантирует, что в возвращаемый массив будут
включены методы только от запрошенного типа. Помните, что Announ-
cementsDB — это класс и поэтому он неявно наследует от System.Object.
Если вы не хотите включать методы своих базовых классов, то знайте, что
BindingFlags.DeclaredOnly отбрасывает их. Остальной код в Main занима-
ется тем, что перебирает каждый Methodlnfo, перечисляя параметры и
отображая интересующую нас информацию.

Рис. 5. Генерация SqICommand на основе метаданных метода


sealed class SqlCommandGenerator
{
private SqlCoiranandGeneratorO 0
public static SqICommand GemrateCommaRflKSqlConnectiora connection,
Metnodlnfo metnod, objectt] values)
i
Sq3.Command command = new SqlCoHimand(method.Namef connection};
command. CoroBiaRdType = CofUBandType.StoredProcedure;
ParaffleterlnfoH parameters - method.GetPararoetersO;
for (int i * 1; I < parameters.LengtK; i++)
{
SqlParameter sqlParameter = new SqIParameter();
sqlParameter.ParametsrName = "@" + parameters[i].Name;
sqlParameter.Value - values[l];
comm and. Paraaeters.AdcJ{ sqlParameter);
}
return command;

Теперь вы представляете, как перечислить параметры нужной функции и


создать объект SqICommand вкупе с соответствующими объектами SqlPa-
rameter. Все, что от вас требуется, — определить в коде на С# или Visual
Basic .NET функции, представляющие ваши хранимые процедуры. Осталь-
ное могла бы взять на себя единственная вспомогательная функция, кото-
рая динамически создает и конфигурирует объекты SqICommand в пери-
од выполнения. Такая функция могла бы выглядеть так, как показано на
Динамическое связывание уровня данных

рис. 5. Располагая объектом соединения (connection object), метаданными


метода и значениями, которые должны быть переданы хранимой процеду-
ре, Generate Command генерирует полностью сконфигурированный и гото-
вый к выполнению объект SqlConimand.

Внимательные читатели, вероятно, заметили, что я не указывал тип дан-


ных для параметра метода, подразумевая SqlDbType, и не настраивал свой-
ство Туре объекта SqlParameter. Это вызвано тем, что объект SqlParameter
получает нужную информацию при настройке свойства Value. SqlDbType
устанавливается, исходя из типа значения Value. А если вы все же явно
настроите свойство Туре, объект SqlParameter отключит свой внутренний
механизм «предположений» («guessing» mechanism). Какой SqlDbType
сопоставляется с тем или иным типом в .NET Framework (System), C# или
Visual Basic .NET, показано в табл. 1.

Табл. 1. Сопоставления типов

SqlDbType System C# Visual Basic .NET

(не поддерживается) System. Char char Char


Biglnt System.Int64 long Long
Binary System. Array byte Byte
of System.Byte
Bit System. Boolean bool Boolean
Char System. String string String
DateTime System. DateTime System.DateTime Date
Decimal System. Decimal decimal Decimal
Float System.Double double Double
Image System.Array byte Byte
of System.Byte
IM System.Int32 int Integer
Money System. Decimal decimal Decimal
NChar System. String string String
NText System.String string String
NVarChar System. String string String
Real System. Single float Single
Small Dale Time System. DateTime System.DateTime Date
Smalllnt System. Intl6 short Short
SmallMoney System. Decimal decimal Decimal
Text System.String string String
Timestamp System. Date Time DateTime Date
Tinylnt System.Byte byte Byte
Uniqueldentifier System. Guid System. Guid System, Guid
см, след. стр.
Доступ н данным из приложений

Табл. 1. Сопоставления типов (окончание)

SqIDbType System C# Visual Basic .NET

VarBinary System. Array byte[] Byte()


of System. Byte
VarChar System. String string String
Variant System.Object object Object

Далее нужно поместить вызов GenerateCommand в PortalDatabase.GetAn-


nouncements, а затем использовать сгенерированную команду. Для этого
функция GetAnnouncements должна передавать объект Methodlnfo с мета-
данными, полученный через Type.GetMethod. Как видно на рис. 6, поиск
подходящей информации по заданным имени метода и типам его парамет-
ров берет на себя один из перегруженных методов Type.GetMethod. В ре-
альности он переадресует вызов защищенному абстрактному методу Type.Ge-
tMethodlrnpl, подменяя недостающие фрагменты информации значениями
по умолчанию. Так что мой простой вызов Type.GetMethod внутренне
транслируется в вызов Type.GetMethodlmpl:

Type.GetMethodlmpl("GetAnnouncements",
BindingFlags.Public | BindingFlags.Instance |
BindingFlags.Static, null, CallingConventions.Any,
new Type[] { i:ypeof(SqlConnection), typeof(int)
}, null);

Рис. 6. Метаданные метода


ptfblic static DataSet GetAnnouncements(SqlGonnectlon connection,
Int moduleld)

Hethodlnfo methodlnfo =
typeof(PortalDatabase).GetMethod ("QetARnoufseenmnts",
RSW Type[] { typeof(SqlConnection), typeof(int) });
SqlCommand command =
SqlGofflffiandGerte rato r. Gene rateCommand (connection.,
methodlnfo, new object[] { noduleld });
DataSet dataSet = new DataSetC);
SqlDataAdapter dataAdapter = new SqlDataAdapter{commaRd};
dataAdapter.Fill(dataSet);
return dataSet;

Любопытно, что класс Type и его метод GetMethodlmpl являются абстрак-


тными. Так кто же предоставляет конкретную реализацию для них? Класс
System.RuntimeType, наследующий от System.Type. RuntimeType — закры-
тый класс из сборки MSCORLIB.DLL, и, поскольку обращаться к нему
Динамическое связывание уровня данных 283

напрямую нельзя, CLR раздает объекты System.RuntimeType всякий раз,


когда вы запрашиваете информацию о типе через typeof в С# или Get Type
в Visual Basic .NET. Таким образом, Type. Get Method заканчивает вызовом
RuntimeType-реализации Get Method Impl, которая в свою очередь исполь-
зует функцию SelectMethod объекта Binder. Поскольку вы никогда не пе-
редаете собственную реализацию Binder в этой цепочке вызовов, Runtime-
Type. Get Method Impl получает реализацию Binder по умолчанию через ста-
тическое свойство Type.DefaultBinder. За поиск метода, наиболее полно
удовлетворяющего заданному набору критериев, в конечном счете отвеча-
ет Binder.SelectMethod, реализованный в Binder.

Хотя код на рис. 6 делает то, что нужно, он все еще далек от идеала, так как
теперь вы должны заботиться о синхронизации объявления Си-функции
и параметров Type.GetMethod. Если вы измените имя функции или тип
какого-нибудь параметра, вам придется соответственно модифицировать
вызов Type.GetMethod. Эту проблему, связанную с получением метадан-
ных метода, можно решить простым проходом по стеку и захватом фрей-
ма, принадлежащего нужному методу. В библиотеке Framework Class
Library (FCL) есть очень удобная для этого функция, но она скрыта, и вот
так сходу ее не найдешь. Я подскажу вам, что это за функция: Method-
Base. GetCurrentMethod из пространства имен System.Reflection. У нее нет
никаких аргументов, и она возвращает MethodBase (сейчас это либо Ме-
thodlnfo для обычного метода, либо Constructorlnfo для конструктора
типа), который представляет вызвавшую функцию. Чтобы передать его в
GenerateCommand, вы должны привести возвращаемое значение к Ме-
thodlnfo. В окончательном виде вызов выглядит так:
public static DataSet GetAnnouncementsCSqlConnection connection,
int moduleld)
{
SqlConrnand command = SqlCommandGenerator.GenerateCommand(
connection, (Methodlnfo) MethodBase.GetCurrentMethodQ,
new object[] { moduleld });

// Остальной код опущен для краткости...


}
Небольшое отступление от основной темы: MethodBase.GetCurrentMe-
thod крайне удобен при отладке, трассировке вызовов и т. д. Сколько раз
вам приходилось писать простые тестовые функции, которые выводят на
экран какое-то сообщение вроде «Inside FooBar»? Так вот, MethodBa-
se.GetCurrentMethodQ.ToString, вероятно, станет вашим новым лучшим
другом. Строковое представление класса, производного от MethodBase,
включает полную сигнатуру метода вместе с возвращаемым значением.
Это настоящее спасение от болевого синдрома запястий — особенно если
284 Доступ к данным из приложений

вы пользуетесь перегрузкой метода и вам лень возиться с выводом сооб-


щений типа «Inside Fool» и «Inside Foo2».

Потом я покажу вам еще один способ, позволяющий добраться до мета-


данных метода даже без Ту ре. Get Met hod или MethodBase.GetCurrent-
Method.

Следующая проблема. А что, если у вашей хранимой процедуры и ее па-


раметров не те имена, которыми вы хотели бы пользоваться в своем коде?
Например, во многих проектах была принята схема именования, согласно
которой имена хранимых процедур начинались с префикса «sp». Более
того, иногда параметры нужно передать в С#-функцию просто как данные —
без пересылки хранимой процедуре. Здесь можно вспомнить все тот же
пример с GetAnnouncements, первый параметр которой — объект SqlCon-
nection. Если вы не уберете его в этом случае из списка параметров объек-
та SqlCommand, рано или поздно возникнет исключение.

Наконец, вы можете попасть в такую ситуацию, где тип параметра в вашей


Си-функции должен слегка отличаться от типа соответствующего пара-
метра в хранимой процедуре. Так что было бы неплохо каким-то образом
избавиться от жесткой зависимости между базой данных и приложением,
А значит, нужно сопоставить с функцией и ее параметрами дополнитель-
ные метаданные.

Библиотека-пример
Эта библиотека, которую можно скачать вместе с другим исходным кодом
для моей статьи по ссылке http://download.microsoft.com/download/msdn-
magazine/code/Aug02/WXP/EN-US/NETReflection.exe, решает все ранее
упомянутые проблемы. Вероятно, вы воспользуетесь ею как отправной
точкой и адаптируете или расширите библиотеку под свои, более специ-
фические потребности. В моей библиотеке предполагается, что вы имеете
дело с SQL Server, поэтому она работает в основном с объектами из про-
странства имен System.Data.SqlClient и типами данных, перечисленными
в System.Data.SqlDbType.

Она включает четыре базовых класса: SqlCommand Method Attribute, SqlPa-


rameterAttribute, NonCommandParameterAttribute и SqlCommandGene-
rator. Как и подсказывают их имена, первые три класса представляют со-
бой нестандартные (custom) атрибуты, a SqlCommandGenerator — класс со
статическими функциями для генерации объекта SqlCommand на основе
метаданных какого-либо метода. Эта реализация сложнее показанной на
рис. 5, поскольку она рассчитана на адаптацию через атрибуты.
Динамическое связывание уровня данных

SqICommandMethodAttribute
Атрибут SqICommandMethodAttribute служит трем целям. Во-первых, он
помечает функцию, написанную на С# или Visual Basic .NET, как ориен-
тированную на команду базы даннЪтх. Для большей безопасности я сделал
его обязательным, так что SqlCommandGenerator заглохнет и сообщит о
неудаче проверки (assertion failure), если вы случайно подсунете ему ме-
тод без этого атрибута. (Наверное, вы предпочтете заменить эти проверки
собственными исключениями.) Во-вторых, поскольку у SqICommand-
MethodAttribute нет конструктора по умолчанию, вам придется хотя бы
указать тип команды, представляемой функцией. Поддерживаются два
значения из свойства System. Data. CommandType перечислимого типа:
CommandType.StoredProcedure (для хранимой процедуры) и Command-
Type. Text (для параметризованного SQL-запроса). В-третьих, у SqICom-
mandMethodAttribute имеется свойство CommandText, позволяющее зада-
вать имя целевой хранимой процедуры или SQL-оператора. В первом слу-
чае вам понадобится лишь предоставить имя хранимой процедуры, если
оно вдруг отличается от имени вашей функции-оболочки. Например, если
бы хранимой процедуре IBuySpy Portal было присвоено имя spGetAnno-
uncements, а вы захотели бы назвать свою функцию просто GetAnnoun-
cements, то вы могли бы применить этот атрибут следующим образом:
[ SqlCommandMethod( CommandType.StoredProcedure, "spGetAnnouricements") ]
public static DataSet GetAnnouncements(SqlCofinection connection,
int moduleld)

Если же их имена идентичны, достаточно сделать так:


[ SqlCommandMethod(CommandType.StoredProcedure) ]
public static DataSet GetAnnouncements(SqlConnection connection,
int moduleld)

Наконец, если ваша функция представляет параметризованное SQL-выра-


жение, укажите для конструктора атрибута CommandType. Text как первый
параметр и SQL как второй. Вот так вы могли бы получить Announcements
из базы данных портала, используя вместо хранимой процедуры парамет-
ризованный SQL-запрос:
[ SqlCommandMethod(CommandType.Text, "SELECT * FROM Announcements WHERE
ModulelD = @ModuleID AND ExpireDate > GetDateO") ]
public static DataSet GetAnnouncements(SqlConnection connection,
286 Доступ к данным из приложений

int moduleld)

Реализация SqiCominandMethodAttribute настолько прямолинейна, что


упоминания заслуживает лишь один аспект; атрибут Attribute Usage само-
го класса. По умолчанию он устанавливается в AttributeTargets.Method,
чтобы разрешить его применение только к методу. Компилятор сообщит об
ошибке, если вы попытаетесь применить экземпляр SqiCominandMethod-
Attribute, скажем, к событию, свойству или даже к конструктору.

NonCommandParameterAttribute
В классе NonCommandAttribute нет абсолютно никакого кода; помимо
того, что он наследует от System.Attribute, его определение пусто. Это ти-
пично для атрибутов, действующих просто как метки. Фактически един-
ственный член в подобных атрибутах — конструктор по умолчанию, гене-
рируемый компилятором в отсутствие такового. NonCommandParameter-
Attribute полезен, если вы не хотите, чтобы определенные параметры
вашей функции, написанной на СП или Visual Basic .NET, включались в ге-
нерируемый SqlCommand:
[ SqlCommandMethod(CommandType.StoredProcedure} ]
public static DataSet GetAnnouncementsC
[ NonCommandPa гаmete г ] SqlConnection connection,
int moduleld)
I

1
Бот так я позаботился о том, чтобы объект SqlConnection передавался как
первый параметр, Он — часть интерфейса функции, а не хранимой проце-
дуры в базе данных и, кроме того, не является параметром в параметризо-
ванном SQL-выражении. Класс SqlCommandGenerator пропускает любой
параметр, помеченный этим атрибутом; в ином случае предполагается об-
работка по умолчанию. В большинстве рассмотренных до сих пор приме-
ров я использовал статические функции, но если бы вы сделали объект
соединения членом класса, содержащего ваши функции-оболочки, этот
атрибут вам вообще бы не понадобился.

У класса NonCommandParameterAttribute, как и у SqlCommandMethod-


Attribute, имеется атрибут Attribute Usage — только на этот раз он нацелен
исключительно на параметры. Кстати, я поместил этот атрибут в про-
странство имен Sample.Data, а не Sample.Data.Sql, поскольку он не специ-
фичен для определенного провайдера данных. Хотя это вынуждает вас
Динамическое связывание уровня данных 287

включать в свою программу два пространства имен, зато вы можете где


угодно повторно использовать данный атрибут в аналогичных целях.

SqIParameterAttribute
SqlParameterAttribute обрабатывает любые расхождения между парамет-
ром функции и параметром целевой команды. В отличие от SqlCommand-
Met hod Attribute и SqlNonParameterAttribute этот атрибут не предназначен
просто для того, чтобы помечать некий параметр, — хотя и это возможно
при использовании его конструктора по умолчанию. Реальная необходи-
мость в нем возникает, только когда между параметрами есть какие-то раз-
личия; иначе SqlCommandGenerator пытается извлечь максимум инфор-
мации из метаданных соответствующего параметра метода.

У SqlParameterAttribute шесть свойств, контролируемых индивидуально;


Name, SqlDbType, Size, Precision, Scale и Direction. Его конструктор пере-
гружен несколькими вариантами, чтобы вы могли задавать Name, SqlDb-
Type, Size или комбинацию этих свойств в зависимости от того, в чем
именно проявляются различия между параметрами. Если вам нужно ука-
зать направление (direction), точность (precision) или масштаб (scale) па-
раметра, вы должны вручную настроить свойства Direction, Precision и
Scale. Несколько примеров использования SqlParameterAttribute показано
на рис. 7.

Рис. 7. Применение SqiParameterAttribute


£ SqlCofBfflandHethc^(Goffl«andType.StorectProce6ure} }
public static OataSet GetCustowrsC
E NonCoaiiBand'Paraffleter ] SqlConrtection connection)
{ ... $
ESqlCo™af!dHett№d{CoiranandType.StoredProcedure) ]
public static SataSet GetCustomersByStateC
t HofiCoRimandPararfteter 3 Sql Connection connection,.
[ SqlParamter(2) 1 string state)
{ ... }
i SqlComfftandNethQd( СошапйТуре . Stored? rocedu re ) 3
public static DataSet GetCu atoms rBy!d(
E Мол Command-Parameter 3 SqlConrveetion connection,
int customerld)

. StoredProcedure) 3
public static int AddQrderltesK
;
E NonOommandParameter 3 SqlConnection connection,
{ SqlParameterC'PartNr", 20) ] string partHuiaber,
[ SqlParameterCSqlDbTypa. Decimal, Scale = 9, Precision = 4) ] decimal
«sltPrice, Int quantity)
{ ... } . ' " .,- • -r - - -
288 Доступ к данным из приложений

При проектировании сложного атрибута важно определить, какие свой-


ства естественнее инициализировать в конструкторе, а какие — устанавли-
вать явно. Поскольку свойств у SqlParameterAttribute немало, возмож-
ность инициализации их всех через комбинации перегруженных конструк-
торов привела бы только к путанице, а исходный код было бы трудно
читать. Бот почему я предпочел инициализировать через конструктор
лишь самые популярные свойства параметра* — имя, тип данных и раз-
мер. Помните об этом, разрабатывая собственные атрибуты.

Реализация SqlParameterAttribute требует пояснений. У этого класса име-


ется шесть свойств только для чтения: IsNameDefined, IsSizeDefined, IsPre-
cisionDefmed, IsScaleDefined, IsTypeDefined и IsDirectionDefined. SqlPara-
meterAttribute поддерживает для каждого свойства два состояния: опреде-
ленное (defined) или неопределенное (undefined). Все состояния по
умолчанию инициализируются как неопределенные, а это означает, что
имя, размер, точность, масштаб, тип и направление объекта параметра не
заданы явным образом и поэтому должны быть установлены на основе
контекста. Для SqlCommandGenerator контекстом являются метаданные
параметра функции. Неопределенное состояние свойств Name, Size, Preci-
sion и Scale отражается специфическим значением, допустимым для их
типа. Например, имя считается неопределенным, если закрытое поле
_name равно null или занято пустой строкой; в ином случае оно рассмат-
ривается как определенное. То же правило применяется к размеру, точно-
сти и масштабу. Однако _рагатТуре и _direction относятся к перечисли-
мому типу, так что у них нет значения, которое можно было бы безопасно
использовать для индикации неопределенного состояния. Поэтому на их
состояние указывают отдельные поля: __typeDefined и _directionDefined
соответственно.

SqlCommandGenerator
Именно этот класс в конечном счете принимает метаданные метода, при-
меняет все переопределения, заданные моими атрибутами, и генерирует
готовый к выполнению объект SqlCommand. Его единственный открытый
метод GenerateCommand представляет собой более полную реализацию
того, что вы уже видели на рис. 5. Как и раньше, второй параметр в Gene-
rateCommand идентифицирует функцию, на основе метаданных которой
следует генерировать команду, — только на этот раз я сделал его необяза-
тельным. Если вы передаете NULL (или Nothing в Visual Basic .NET),
Generate Command автоматически использует метаданные вызвавшей фун-
кции. Свою работу он начинает с класса StackTrace из пространства имен
System.Diagnostics, чтобы инициировать трассировку стека (stack trace).

* Здесь подразумевается объект параметра. — Прим. сост.


Динамическое связывание уровня данных 2S9

Затем он захватывает метод из предыдущего фрейма стека, передавая его


индекс в StackTrace.GetFrame (индекс, равный 0, соответствовал бы само-
му вызову GenerateCommand):

if (method == null)
method = (Methodlnfo) (new StackTrace<).GetFrame(1).GetMethod());

Теперь можно одним махом получить метаданные для вызвавшей функции


из фрейма стека. При этом нет никакой необходимости в сложном вызове
Type.GetMethod. Черт возьми, даже вызывать MethodBase.GetCurrent-
Method и то не нужно. Проще некуда! Но два требования вы обязаны со-
блюдать: вызвавшей функцией должна быть прокеи-функция для коман-
ды базы данных и она не должна быть конструктором. Последнее требова-
ние вызвано тремя причинами. Во-первых, это просто бессмысленно, даже
если бы было возможно с технической точки зрения. Во-вторых, Attribute-
Usage в SqlCommandMethodAttribute все равно запрещает применение
этого атрибута к конструктору. И в-третьих, хотя StackTrace.GetFrame воз-
вращает MethodBase (который является надклассом Methodlnfo и абстра-
гирует методы и конструкторы), GenerateCommand приводит его к Me-
thodlnfo. Поэтому, если бы конструктор попытался вызвать Generate-
Command, возникло бы исключение InvalidCastException.

Из-за новой функциональности (распознавания вызвавшей функции),


добавленной в генератор, у вас может появиться соблазн всегда передавать
NULL во втором параметре, но берегитесь: эта простота в использовании
больно бьет по производительности. Прогнав серию тестов на своей маши-
не, я обнаружил, что максимальное быстродействие дает Type.GetMethod,
MethodBase.GetCurrentMethod лишь немного уступает ему, а проход по
стеку с помощью StackTrace обойдется вам 12-кратным падением произ-
водительности. На практике разницей в быстродействии между первыми
двумя способами можно пренебречь.

Тем не менее вариант на основе MethodBase.GetCurrentMethod следует


выделить как основной — по возможности выбирайте именно его и не пе-
редавайте NULL. Кстати, если вы подумали, что MethodBase.GetCurrent-
Method тоже использует какую-то разновидность трассировки стека, вы не
ошиблись, но разработчики Framework, похоже, оптимизировали его для
частых вызовов. Достаточно посмотреть, сколько раз сама FCL обращает-
ся к стеку для проверки разрешений (permissions) у вызвавшей функции.

Покончив с этой задачей, GenerateCommand приступает к основной рабо-


те и проверяет, дополнен ли метод атрибутом SqlCommandMethodAttri-
bute. (Если нет, проверка заканчивается неудачей, и сообщается, кто вино-
ват в этом.) Далее GenerateCommand создает объект SqlCommand и ини-
циализирует его свойства Connection, CommandType и CommandText.

10-5947
290 Доступ к данным из приложений

Настраивая свойство CommandText, он проверяет размер значения в одно-


именном свойстве атрибута. Если это значение представляет собой пустую
строку, в свойство CommandText объекта команды записывается имя ме-
тода; в ином случае берется значение из атрибута. Если строка пуста,
GenerateCommand делает дополнительную проверку, чтобы убедиться,
действительно ли тип команды соответствует хранимой процедуре, так как
для параметризованного SQL-запроса бессмысленно использовать имя
метода. GenerateCommandParameters, вызываемая следующей, — закрытая
функция, предназначенная исключительно для разделения труда. Она от-
вечает за обработку параметров метода и добавление нужных объектов
SqlParameter в объект SqlComraand. Исходный код этой функции содер-
жит подробные комментарии, поэтому я лишь вкратце опишу алгоритм ее
цикла:

• получить следующий параметр метода;


• если у него есть атрибут KonCommandPararaeterAttribute, пропустить
его;
• если у него есть атрибут SqlParameterAttribute, использовать значения,
определенные в атрибуте, для настройки соответствующих свойств
объекта SqlParameter;
• если атрибута SqlParameterAttribute нет, создать временный атрибут
через конструктор по умолчанию;
• настроить все аспекты SqlParameter на основе метаданных для этого
параметра метода.

Кроме того, в исходном коде присутствуют две контрольные точки (asser-


tions), срабатывающие, когда число параметров (кроме помеченных атри-
бутом NonComrnandParameterAttribute), объявленных для метода, не со-
впадает с количеством значений, переданных в GenerateCommandPara-
meters. Это полезно в тех случаях, когда объявляешь какой-то параметр
метода, но забываешь передать его значение генератору.

Проблема выходных параметров


Одно из ограничений, заслуживающих упоминания, связано с направле-
нием (direction) объекта SqlParameter. Если в атрибуте оно не определено,
то выбирается в соответствии с тем, как передается параметр метода. В С#
параметр можно передавать по значению (по умолчанию), по ссылке (клю-
чевое слово ref) или только для возврата (output only) (ключевое слово
out), поэтому направление соответственно задается как ParameterDirec-
tion.Input, ParameterDirection.InputOutput или как ParameterDirection.Out-
put. В Visual Basic .NET никакой разницы между последними двумя слу-
чаями нет, и направление задается либо как ParameterDirection.Input (для
Динамическое связывание уровня данных 291

параметров с ByVal), либо как ParameterDirection.InputOutput (для пара-


метров с ByRef).

Задав направление ParameterDirection.Output для параметров, вы сможе-


те получать нужные значения от хранимой процедуры или параметризо-
ванного SQL-запроса, но проблема в том, что их нельзя просто так (без
дополнительных усилий) передать вызвавшей функции. Это связано с тем,
что они возвращаются в GenerateCommand по значению и в итоге не мо-
гут быть автоматически переданы вызвавшей функции. То же самое (и
даже в большей степени) относится к типам значений, которые упаковы-
ваются (boxed) и копируются из-за того, что массив значений является
массивом объектов. Поэтому после выполнения команды вам придется
вручную копировать все выходные параметры из объекта SqlParaineter в
вызвавшую функцию.

Один пример. Возьмем хранимую процедуру AddAnnouncements из IBuy-


Spy Portal (рис. 8). Как и любая другая хранимая процедура такого типа,
она вставляет строку в таблицу и возвращает в выходном параметре авто-
матически назначенный идентификатор (auto-assigned identity). На рис, 9
показано, как передать выходной параметр из хранимой процедуры в out-
параметр вызвавшей Ctt-функции. Хотя SqlCommandGenerator устанавли-
вает правильное направление для параметров с ключевым словом ref, он
не может автоматически передать в них значения, возвращенные храни-
мой процедурой.

Рис. 8. Вставка строки и возврат identity


CREATE PROCEDURE AtWAnnGunceflient

nvarchar(IQQ),
«Title nvarcHar(150),
tHoreLlRk nvarchar{150),
@MobileMo relink nvarchar(15Q), .
©ExpireOate OateTime,
^Description nvarchar(20QO),
int OUTPUT

AS
INSERT INTO AnfflOtmceiiBnts
(
Module ID,
CreatetfByUser,
CreatedDate,
Title,
HoreLink,
см. след. стр.
292 Доступ к данным из приложений

Рис. 8. Вставка строки и возврат Identity (окинчаше)


MobileMo relink,
ExpireDate,
Description
)
VALUES
£
@ModuleID,
SUserName,
aetDeteO,

@Mo relink,

©ExpireDate,
^Description

SELECT

Рис. 9. С#-вызов хранимой процедуры с выходными значениями

public static void AddAnnouncefflent(


[ NonCoBimandParameter ] SqlConnection connection, int nroduleld*
[ SqlPara»eter(1QQ) ] string userName,
С SqlParameter(150) ] string title,
[ SqlParameter{l50) ] string ntoreLink,
С SqiParameter(150) ] string mobileHoreLink,
DateTirne expireDate,
' [ SqlParameter(2000) ] string description,
out int itemld)

= 0; // запрещает прямое использование локальной,


// переменной "itemld"
SqlCommand command = SqlGofflfnand6enerator.6enerateCoMand(connection,
null, new object{3 { moduleld, userKame,
title, raorelink, mobileHoreLink,
expireDate, description, itemld });
command. ExecuteNonQueryO;
itemld = (int) command.Parameters["@IteeiID"].Value;

Кстати, та же проблема возникает и при использовании механизма Reflec-


tion для вызова метода, принимающего параметры по ссылке. На рис. 10
показан метод Swap, вызываемый прокси с применением позднего связы-
вания (late binding). После того как Swap заканчивает свою работу, Swap-
Динамическое связывание уровня данных

Proxy копирует значения из массива параметров в переменные, выделен-


ные вызвавшей функцией.

Заодно интересно посмотреть, как с этой ситуацией справляется Visual


Basic .NET, потому что он позволяет использовать позднее связывание (в
режиме Option Strict Off) по более естественному и элегантному синтак-
сису, чем С#. Итак, на рис. 11 показана версия того же кода, что и на
рис. 10, но написанная на Visual Basic .NET. Функция SwapProxy исчезла,
так как все, что нужно для вызова члена по механизму Reflection, Visual
Basic .NET делает сам. В конечном счете он вызывает Type.InvokeMember,
и это всего лишь еще один способ выполнения Methodlnfо.Invoke.

Рис. 10. Reflection и объекты Parameters, передаваемые по ссылке


class Sample

public static void Swap(ref int a, ref int tj)

int teiap = b;
b = a;
a - temp;

public static void SwapProxyCref int a, ref int b)

objectl} parameters = new ob]ect[] { a , b }',


typeof(Sample).GetMethod("3wap").InvQke(nuXl, parameters);

a = (int) parameters[0};
Ь = (in-t) paraiaeter&Et];

static void Hain(stringn args)


I
int a = 1;
Int b * 2;
SwapPraxy(ref a, ref b);
System. Console, WriteLine<"a = IQ>. b =

Рис. 11. Применение к Swap позднего связывания в Visual Basic -NET


Class Sample
Public Sub SwapCByBef a As Integer, Byflef b As Integer)
Dim temp As Integer == b ,
b=a
a * temp

см. след, стр.


294 Доступ к данным из приложений

Рис. 11. Применение к Swap позднего связывания ... (окончание)


End Sub
End Class
Module Module!
Sub Main<)
Difo a As Integer = 1
Dim b As Integer = 2
Dim о As Object * New San*ple<)
o.Swap(a, b)
System.Console.WriteLine("a = {Q}, b = Ш", a, b)
End Sub
End Module

Этот код выглядит обманчиво простым, но за кулисами Visual Basic .NET


генерирует тот же IL-код, что и С#. Дамп метода Main в том виде, в каком
он показывается ILDASM, приведен на рис. 12. Те, кто не хочет лишней
головной боли от чтения IL-кода, должны поверить мне на слово: IL-код
копирует элементы из временного массива _Vb_X_array_2 обратно в пере-
менные а и Ь.

Рис. 12. Дамп функции Main в IIDASM


.method public static void Hair*() oil managed
{
.entrypoint
.custom instanoe void [mscQrlibJSystem.STAThreadAttribote::.ctor()
* ( 01 00 00 00 )
// Code size 114 (0x72)
: .naxstack €
.locals init ({0] int32 a,
[1] int32 b,
[2] object 0,
[3] objeetn _Vto_t_array_2,
[4] obJectE] _V&..t_array_1,
E5] boolU _Vb_t_array_Q)
.Шдиаде "f3At2P088-C2SG-HDO-B442-OOA0244A1DD2}'",
4994B45C4-E6E9-11D2-9Q3F-OGC04FA3G2A1J",
"100000000-0000-0 000» 0000-OOOOOOOOOOQO)"
// Source File "C:\Documents and Settings\atifa\Hy Documents\Visual Studio
Projects\vbref\Module1.vb"
//000015: Sub MainO
|L_0000: пор
//000016:
//000017: DiiB a As Integer = t
Idc.i4.1
IL.0002: StlOG.O
см. след. стр.
Динамическое связывание уровня данных 295

Рис. 12, Дамп функции Main в ILDASM (продолжтше)


//000018: Din b As Integer = 2
1ЦКЮЗ: lde.14.2
IL_GQQ4: stloc.1
//00001»: Dim о As Object = SarapleO
,11.0605: newobj instance void vbref .Sample: : .ctor<)
ILJKKte: stloc.2
//000020; o.Swap(a, b)
ILJJOQb: ldloc.2
IL.OOQc; Umll
ILJJOGd: idstr "Swap"
IL_0012: ldc.14.2
IiJH>l3; newarr Imscorlib]SysterB. Object
ILJ)Q18: stloc.s _¥b_t_array_1
IL_001a: Idloc.s _Vb_t_array_1
ILJWic : Idc.i4.0
IL_001d: ldloc.0
IL_001e: box [msGorlib]System.Int32
ILJ3C23; stelern. ref
ХЦМ24: Idloc.s _Vb_t_array_1
ILJ3026: Idc.i4.1
ILJ3§27: ldloc.1
IL_0028: box [mscorlib}System, Int32
Il_002d; steleffl. ref
IL_002e: Idloc.s _Vb_t_array^1
IL_0030: Stloc.3
: 11.0031: Idloc.s
11.0032: Idnull
11^0033: Idloca . s „Vb^t.array^S
IL_0035; call void
[Microsoft.VisualBasiclMierosoft.VisualBasic.Helpers.LateSinding
: :UteCall(object, class [mscorllblSystera.Type, string,
objectC], striRa[3,boolCl&)
IL_003a: пор
IL_003b: Idloc. s _Vb_t.array_0
It_0t3d: ldc.14.1
IL_003e; Idelem.i1
IL.003f: brfalse.s IL_OQ4a
IL_0041 : Idloc. 3
Il_OQ42: ldc.14,1
IL_0043 : Ideleei. ref
ILJ)Q44: call tntaa
[Microsoft.Visual&asiclHicrosoft.VisualBasic.Helpers.IntegerType
:: FroinObject(object)
ILJXMS: stJoc. 1
IL_004a: Idloc.s .Vb
IL_.004c: Ide,i4,0
1L 004d: Idelem.i1
см. след. стр.
296 Доступ к данным из приложений

Рис. 12. Дамп функции Main в-ILDASM (окончание}


ILJ)Q4e: brfalse.s ILJHffit
IL_0050: ldloc.3
IL_0051: Idc,i4.0
ILJ)052; Idelem.ref
IL_0053: call int32
[HierosQft.VisualBasiGJMlerosoft.VIsualBasic,Helpers.IntegerType
::FromQbJect(object)
IL_0058: stloc.O
//000021 : sSystem.Console.WriteLineC'a = {0}, b = {1}", a., b)
IL_0059: Idstr "a = {0}, b = U)"
It^OOSe: ldloc.0
ILJ)05f: box CfflscorUb]System.Int32
1L,0064: ldloc.1
Il_0065: box tniscorlib3Systent.Int32
И„006а: call void [BiscorlibjSystem.Console; ;WriteLine(string,
object,
ooject)
Il_006f: пор
//000022:
//000023: End Sub
IL_0070: пор
II 0071: ret
} // end of method Modulel:;Nain

Вообще говоря, вы могли бы полностью решить проблему выходных пара-


метров, генерируя код так, как это делает Visual Basic .NET, и используя
для этого средства, предлагаемые пространством имен System.Reflec-
tion.Emit, но это тема для другой статьи.

Обработка NULL-значений
О чем я вам еще не рассказал, так это об обработке NULL-значений. Если
хранимая процедура допускает NULL-значения в одном или двух парамет-
рах, в вашем прокси-методе нельзя использовать предопределенные типы
вроде int в С# и Integer в Visual Basic .NET. Вместо этого вы должны объя-
вить свой метод принимающим один из типов значений из пространства
имен System.Data.SqlTypes. Допустим, в базе данных Pubs имеется храни-
мая процедура:

CREATE PROCEDURE [GetEmployeesByJob](rjob_id SMALLINT = NULL) AS

SELECT *
FROM [employee]
WHERE [job_id] = ISNULL(@job_id, [job_id])
Динамическое связывание уровня данных 297

Поскольку параметр @job_id может быть NULL, вам следует объявить


свой С#-метод с использованием Sqllntl6 вместо short, как показано в
следующем фрагменте кода:
[ SqlCommandMethod (CommandType.StoredProcedure)]
public static DataSet GetEmployeesByJob(
[ NonCommandParameter ] SqlConnection connection,
[ SqlParameter("Job_id") ] Sqllnt16 Jobld}
{
SqlCommand command = SqlCommandGenerator.GenerateCommand(
connection, null, new object[] { jobld });
DataSet dataSet = new DataSetQ;
SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
dataAdapter.Fill(dataSet);
return dataSet;
I
А вот как вы должны вызывать метод в тех случаях, когда параметр jobld
равен NULL и когда он не равен NULL;
DataSet dataSet;
dataSet = GetEmployeesByJob(connection, new Sqllnt16(5));
dataSet = GetEmployeesByJob(connection, 5); // неявное приведение
// к Sqllnt16
dataSet = GetEmployeesByJob(connection, SqlInt16.Null);

Второй вызов GetEmployeesByJob возможен только в С#, поскольку Visu-


al Basic .NET не поддерживает напрямую операторы преобразований. Тем
не менее вы вправе сами вызвать Sqllntl6.op_lmplicit, даже если Intelli-
Sense прячет ее от вас в Visual Studio, правда тогда вызов GetEmployees-
ByJob получается таким громоздким, что с тем же успехом можно пользо-
ваться версией конструктора. Вот версия предыдущего кода для Visual
Basic .NET:
Dim dataSet As DataSet
dataSet = GetEmployeesByJobfconnection, New Sqllnt16{5))
dataSet = GetEmployeesByJob(connection, Sqllnt16.op_lmplicit{5))
dataSet = GetEmployeesByJob(connection, SqlInt16.Hull)

Определение пользовательских типов данных


АЛЯ параметра через наследование
Из всех атрибутов в моей библиотеке только класс Sql Parameter Attribute
определен без ключевого слова sealed (или Nonlnheritable в Visual Basic
.NET). Это диктуется его структурой — пусть даже в нем нет открытых
или защищенных членов, которые могли бы быть переопределены каким-
нибудь подклассом. Обычно атрибуты предназначены для хранения лишь
298 Доступ к данным из приложений

дополнительных метаданных и не несут в себе никакой функциональнос-


ти, которая выходила бы за рамки того, что определено классом Attribute;
поэтому вы должны запечатывать (seal) их. Помимо всего прочего, запе-
чатывание еще и ускоряет поиск атрибутов исполняющей средой. В слу-
чае SqlParameterAttribute я оставил атрибут открытым для специализации
через наследование, что позволяет вам создавать собственные пользова-
тельские типы (user-defined types), уменьшающие вероятность ошибок в
программе и упрощающие ее модификацию. Здесь полная аналогия с при-
менением пользовательских типов в SQL Server.

Допустим, в нескольких таблицах базы данных вы храните адреса элект-


ронной почты. Если такой адрес определен как NVARCHAR(IOO), то со-
здание для него пользовательского типа в SQL Server обеспечит согласо-
ванность определения адресов электронной почты во всех объектах базы
данных. Когда вам понадобится изменить длину адресов, вы просто моди-
фицируете пользовательский тип данных — единственную точку измене-
ний. Аналогичным образом можно создать подкласс SqlParameterAttribute
для централизации определения параметров, используемых во многих ко-
мандах. Ниже показано, как определить новый атрибут, основанный на
SqlParameterAttribute и служащий оболочкой пользовательского типа
sysname в SQL Server. Заметьте, что сам SysNameParameterAttribute запе-
чатан:

sealed class SysNameParameterAttribute : SqlParameterAttribute


{
public SysNameParameterAttributeC) : base{SqlDbType.NVarChar,
128} {}
public Sy$NameParameterAttribute{string name) :
base(name, SqlDbType.NVarChar, 128) {}
I

Бонус
Итак, вы видели, как с помощью атрибутов можно автоматически генери-
ровать команды в период выполнения, но они же позволяют создавать
вспомогательные утилиты и инструменты, полезные при разработке при-
ложений. Чтобы вы получили представление о том, как создать простой
инструмент, посмотрите на программу, приведенную на рис. 13. Если при
ее запуске в командной строке указывается какая-нибудь сборка, програм-
ма просматривает все экспортируемые типы и их методы и сообщает о тех
из них, у которых есть атрибут SqlCommandMethodAttribute со свойством
CommandType, установленным в CommandType.StoredProcedure. Какой-то
десяток строк кода — и вы сможете находить в сборках все прокси-функ-
ции хранимых процедур!
Динамическое связывание уровня данных 299

Рис. 13. Перечисление всех прокси-функций хранимых процедур

using System;
.usinf System. Reflection;
using Sample.Data.Sql;
class Sample

static void Hain(string[3 args)

Assembly assembly = Assembly.LoadFrom(args[0]);


foreach {Type type in assembly.GetExportedTypesO)

foreach <HethodInfo aietlwdlnfo iR type.GetHethodsO)

SqlCofflfflandHeth-odAttribute attribute =
CSqlCofBffiandHethodAttribute) Attribute.GetGustomAttribute
(iiethodlnfo, typeof(SqlCoramandMethodAttri&ute»;
if (attribute != null &&
attribute. CoiwnandType ==
System.Data.CormandType.StoredProcedure)

Console, Write("{6}.{1}",

nethodlnfo.NaBe);
If (attribute.CofflaiandText.Lervjth 1= 0)
Console.WriteC -> {0}", attribute.CoiwaandText);
.Writellne();

И последнее. В исходный код, прилагаемый к статье, я включил образец


SQL-сценария (рис. 14), который генерирует С#-прокси для вызова хра-
нимой процедуры, при необходимости устанавливая соответствующие ат-
рибуты параметров. Этот сценарий особенно полезен, когда вам нужны
«тупые» оболочки для хранимых процедур. Только учтите, что он не рас-
считан на все случаи жизни (в частности, он не решает проблему выход-
ных параметров). Но если вы имеете дело с наиболее распространенным
классом хранимых процедур, сценарий сделает всю работу за вас.

Рис. 14. SQt-сценарий для генерации сигнатуры С#-прокси

set noeount on
declare @sp varchar(lOO)
set esp = '« здесь указывается имя хранимой процедуры »'

см. след. стр.


300 Доступ к данным из приложений

Рис. 14. SQL-сценарий для генерации сигнатуры ... (продолжение)


declare @oid int
select eoid = o.id from sysobjects о where о.паяе = @sp
declare @last int
— function signature
select iglast = maxCc.eolid)
froffl dtjQ.syscolufftns с
where c.id = ®oid
select case c.colid
r
when 1 then [ SqlComiriandMethodCCQBfflandType.StoredProcedure) ]'
+ char(l3) +
'public static SqlCoifimaRcJ ' + esp + •'(* + char(13)+
t tteiConnnandParaieeter ] ScilConftection
' connection'
+ спаг(13) *
else ' '
end
+ ' ' +
case t.name
when "char' then
С SqlPararaeter('*convert(nvarchar(10}, c.length)*') '
when 'varchar' then
[ SqlParameter(f+convert(nvarchar(tO>, c.lengthH') J*
'nchar' then
[ SqlParameter('+convert(nvarchar(10)h c.length /2)+')3'
when "nvarchar" then
'[ SqlParameterC'+cor>vert(nvarchar(10),c.length/2)+')]*
else
end

case t.name
when 'char' then 'string'
when 'nchar' then 'string'
when 'varchar' then 'string'
when 'nvarchar' then 'string'
when 'bit' then 'bool1
when 'datetiiae' then 'DateTime'
when 'float' then 'double'
when 'real' then 'float'
when 'int' then 'int'
else 'object /* ' + t.name + ' */'
end
' + lower(substrlng(c.name, 2, 1)} + su*string(c.name, 3, tOO)

case c.colid
when 91ast then ')' + char(13) + "{'
else ','
end

см. след. стр.


Динамическое связывание уровня данных 301

Рие. 14. SQt-сценарий для генерации сигнатуры ... (окончание)


front dbo.syscolumns с
left outer join dbo.systypes t on c.xusertype = t.xusertype
where c.id = @old
order by c.colid
— вызов генератора

,select case c.colld


when 1 then
return SqlGo[nmandGenerator.GenerateCofflffland(connection,'
+ char(13)
else " "
end
+ ' ' + lowerCsubstrlnsCc.name, 2, 1» + substring
c.name. 3, 100)

case o.colid
when elast ttierv ');' + char(13) + ',}*
else ','
end
from dbo.syscolumns с
where c.ld = @oid
order by c.colic)

Чтобы использовать этот сценарий, просто загрузите его в isqlw (SQL


Query Analyzer), перейдите в свою базу данных, присвойте переменной
@sp имя хранимой процедуры — и вперед! Потом скопируйте выходной
код из секции результатов (result pane), включите его в свое решение и при
необходимости внесите в него изменения. В сценарии предполагается, что
вы будете возвращать из своей функции объект SqlCommand, а это полез-
но в основном для тех запросов, где выбор метода сбора данных оставля-
ется на усмотрение вызвавшей функции. В одних ситуациях вызвавшая
функция может подключить команду к SqlDataAdapter, а в других — бу-
дет достаточно SqlDataRcader.

Для получения корректных результатов надо также сообщить isqlw, чтобы


тот удалил заголовки и показал результаты в виде текста, а не сетки (grid).
Соответствующие параметры настраиваются на вкладке Results диалого-
вого окна Options (рис. 15). Кроме того, учтите, что сценарий тестировал-
ся только в SQL Server 2000, хотя, по идее, он должен нормально работать
и в версии 7.0.

Перенастроить сценарий на генерацию кода для Visual Basic .NET не


сложно, и эту задачу я оставляю как упражнение для читателей.
302 Доступ к данным из приложений

Default results target:

Results output fornut: ("]

Maximum characters per column: [256

f" Flint column headers Выберите в списке


Г~ £crol results a; received ("] «Results (о Text»
Г" Output queijj
Г" Right align numerics ["]
Г~ Discard results Сбросьте флажок
Г" Щпеп a query batch c:orrplBtes: Print column headers»

Рис. 15. Удаление заголовков и отображение результатов в виде текста

Заключение
Поддержка создания собственных атрибутов, связывания их с различны-
ми элементами программы и запроса метаданных через механизм Reflec-
tion открывает колоссальные возможности в автоматизации и в разработ-
ке совершенно нового класса динамичных приложений. Я продемонстри-
ровал использование атрибутов и механизма Reflection на примере
решения реальной проблемы (упрощения вызовов хранимых процедур) и
надеюсь, что вы теперь понимаете, как применить их на практике в других
ситуациях. Моя библиотека годится для любого CLR-совместимого язы-
ка программирования. В ней много чего можно усовершенствовать. Так, вы
могли бы реализовать кэширование, чтобы часто используемые и сложные
команды с массой параметров не становились «узким местом» в вашей
программе. Однако я не стал бы слишком увлекаться кэшированием без
предварительного профилирования кода. В целом, по сравнению с тради-
ционной настройкой объекта команды мой Sql Command Generator должен
работать лишь чуть медленнее.

Атиф Азиэ (Atif Aziz) — главный консультант в Skybow AG и бывший


Microsoft'oBeq (ex-Microsoftie). Основное направление его деятельности —
помощь заказчикам в переходе на платформу .NET Framework. Регулярно
выступает на конференциях Microsoft. С ним можно связаться по адресу
atif.aziz@skybow.com.
Майкл Говард и Кит Браун

Советы по защите

Десять лучших приемов


защиты кода, о которых
должен знать каждый
разработчик

Когда дело касается безопасности, есть много способов попасть в неприятно-


сти: доверять любому коду, выполняемому в вашей сети, предоставлять
доступ к важным файлам кому угодно и никогда не проверять, не модифици-
рован ли код на вашей машине. А еще можно работать без антивирусных
программ, не встраивать защиту в собственный код и выдавать чрезмерные
привилегии слишком широкому кругу лиц. Или вручить все отмычки для
взлома, легкомысленно используя некоторые встроенные функции. Наконец,
можно оставить все порты сервера открытыми и не следить за ними.
Очевидно, это далеко не полный список. Какие проблемы защиты важны по-
настоящему, каких ошибок избегать в первую очередь, чтобы не подвергать
опасности данные или систему? Эксперты в области безопасности, Майкл
Говард и Кит Браун, дадут вам десять советов, которые помогут уберечься от
неприятностей.

Безопасность — проблема многоплановая. Угроза безопасности может ис-


ходить откуда угодно. От плохо написанного кода для обработки ошибок
или от слишком щедро розданных разрешений. От забытых сервисов, ра-
ботающих на вашем сервере. От неразборчивости в приеме вводимых дан-
ных... список можно продолжать еще долго. Вот 10 советов, которые дадут
вам фору в извечной борьбе щита и меча — помогут в защите компьюте-
ров, сеги, кода и в выборе более безопасной сетевой стратегии.

Публиковалось в MSDN Magazine/Русская Редакция. 2002. №3 (сентябрь). — Прим. изд.


304 Доступ к данным из приложени